Spaces:
Running
Running
Create index.html
Browse files- index.html +474 -19
index.html
CHANGED
|
@@ -1,19 +1,474 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ja">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>WebCraft - Minecraft Clone</title>
|
| 7 |
+
<style>
|
| 8 |
+
/* ==========================================
|
| 9 |
+
UI/UX & CSS
|
| 10 |
+
========================================== */
|
| 11 |
+
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
| 12 |
+
|
| 13 |
+
body {
|
| 14 |
+
margin: 0;
|
| 15 |
+
overflow: hidden;
|
| 16 |
+
font-family: 'Press Start 2P', monospace;
|
| 17 |
+
background-color: #000;
|
| 18 |
+
color: #fff;
|
| 19 |
+
user-select: none;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/* 1. ホーム画面 & 2. スタート方法 */
|
| 23 |
+
#home-screen {
|
| 24 |
+
position: absolute;
|
| 25 |
+
top: 0; left: 0; width: 100vw; height: 100vh;
|
| 26 |
+
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAIklEQVQIW2NkQAKrVq36zwjjgzhhYWGMYAEYB8RmROaABADeOQ8CXl/xfgAAAABJRU5ErkJggg==') repeat;
|
| 27 |
+
background-size: 64px;
|
| 28 |
+
display: flex;
|
| 29 |
+
flex-direction: column;
|
| 30 |
+
align-items: center;
|
| 31 |
+
justify-content: center;
|
| 32 |
+
z-index: 100;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.title { font-size: 48px; margin-bottom: 40px; text-shadow: 4px 4px 0 #000; color: #aaa; }
|
| 36 |
+
.btn {
|
| 37 |
+
width: 300px; padding: 15px; margin: 10px;
|
| 38 |
+
background: #777; border: 4px solid #000;
|
| 39 |
+
border-top-color: #ccc; border-left-color: #ccc;
|
| 40 |
+
color: #fff; font-family: 'Press Start 2P', monospace;
|
| 41 |
+
font-size: 16px; cursor: pointer; text-align: center;
|
| 42 |
+
box-shadow: inset -4px -4px 0px rgba(0,0,0,0.5);
|
| 43 |
+
}
|
| 44 |
+
.btn:hover { background: #888; }
|
| 45 |
+
.btn:active {
|
| 46 |
+
border-top-color: #000; border-left-color: #000;
|
| 47 |
+
border-bottom-color: #ccc; border-right-color: #ccc;
|
| 48 |
+
box-shadow: inset 4px 4px 0px rgba(0,0,0,0.5);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* ゲーム中HUD */
|
| 52 |
+
#game-ui { display: none; }
|
| 53 |
+
#crosshair {
|
| 54 |
+
position: absolute; top: 50%; left: 50%;
|
| 55 |
+
width: 20px; height: 20px;
|
| 56 |
+
transform: translate(-50%, -50%);
|
| 57 |
+
pointer-events: none;
|
| 58 |
+
background: linear-gradient(to right, transparent 40%, white 40%, white 60%, transparent 60%),
|
| 59 |
+
linear-gradient(to bottom, transparent 40%, white 40%, white 60%, transparent 40%);
|
| 60 |
+
mix-blend-mode: difference;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#hotbar {
|
| 64 |
+
position: absolute; bottom: 20px; left: 50%;
|
| 65 |
+
transform: translateX(-50%);
|
| 66 |
+
display: flex; background: rgba(0,0,0,0.5);
|
| 67 |
+
border: 4px solid #222; border-radius: 4px;
|
| 68 |
+
}
|
| 69 |
+
.slot {
|
| 70 |
+
width: 40px; height: 40px; border: 2px solid #555;
|
| 71 |
+
margin: 2px; position: relative;
|
| 72 |
+
}
|
| 73 |
+
.slot.active { border-color: #fff; }
|
| 74 |
+
|
| 75 |
+
#info-panel {
|
| 76 |
+
position: absolute; top: 10px; left: 10px;
|
| 77 |
+
font-size: 10px; text-shadow: 1px 1px 0 #000;
|
| 78 |
+
line-height: 1.5;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* ロード画面 */
|
| 82 |
+
#loading {
|
| 83 |
+
position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;
|
| 84 |
+
background: #111; display: none; flex-direction: column;
|
| 85 |
+
align-items: center; justify-content: center; z-index: 200;
|
| 86 |
+
}
|
| 87 |
+
</style>
|
| 88 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 89 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/PointerLockControls.js"></script>
|
| 90 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
| 91 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
|
| 92 |
+
</head>
|
| 93 |
+
<body>
|
| 94 |
+
|
| 95 |
+
<div id="home-screen">
|
| 96 |
+
<div class="title">WEBCRAFT</div>
|
| 97 |
+
<button class="btn" onclick="startGame('survival')">Survival Mode</button>
|
| 98 |
+
<button class="btn" onclick="startGame('creative')">Creative Mode</button>
|
| 99 |
+
<button class="btn" onclick="alert('【Controls】\nW/A/S/D: Move\nSpace: Jump\nClick: Break/Place\n1-5: Select Item')">Controls</button>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<div id="loading">
|
| 103 |
+
<div style="font-size: 20px; margin-bottom: 20px;">Loading Assets...</div>
|
| 104 |
+
<div id="loading-text" style="font-size: 12px; color: #aaa;">Reading ZIP file</div>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<div id="game-ui">
|
| 108 |
+
<div id="crosshair"></div>
|
| 109 |
+
<div id="info-panel">
|
| 110 |
+
<span id="fps">FPS: 0</span><br>
|
| 111 |
+
<span id="pos">XYZ: 0, 0, 0</span><br>
|
| 112 |
+
<span id="dim">Dim: Overworld</span>
|
| 113 |
+
</div>
|
| 114 |
+
<div id="hotbar">
|
| 115 |
+
<div class="slot active" id="slot-1"></div>
|
| 116 |
+
<div class="slot" id="slot-2"></div>
|
| 117 |
+
<div class="slot" id="slot-3"></div>
|
| 118 |
+
<div class="slot" id="slot-4"></div>
|
| 119 |
+
<div class="slot" id="slot-5"></div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<script>
|
| 124 |
+
/* ==========================================
|
| 125 |
+
3. ゲームの仕様 & コアシステム
|
| 126 |
+
========================================== */
|
| 127 |
+
let scene, camera, renderer, controls;
|
| 128 |
+
let worldBlocks = {}; // 座標をキーにしたブロックデータ
|
| 129 |
+
let textures = {}; // 読み込んだテクスチャ
|
| 130 |
+
let mobs = []; // モブの配列
|
| 131 |
+
|
| 132 |
+
const CHUNK_SIZE = 32;
|
| 133 |
+
const blockSize = 1;
|
| 134 |
+
let gameMode = 'survival';
|
| 135 |
+
let currentDimension = 'overworld';
|
| 136 |
+
let selectedBlock = 1;
|
| 137 |
+
|
| 138 |
+
// ブロックID定義 (特別ブロック含む)
|
| 139 |
+
const BLOCKS = {
|
| 140 |
+
AIR: 0, DIRT: 1, GRASS: 2, STONE: 3, LOG: 4, LEAVES: 5,
|
| 141 |
+
SAND: 6, WATER: 7,
|
| 142 |
+
CHEST: 8, CRAFTING_TABLE: 9, FURNACE: 10, NETHER_PORTAL: 11 // 9. 特別なブロック
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
const blockMaterials = {}; // 材質キャッシュ
|
| 146 |
+
|
| 147 |
+
/* ==========================================
|
| 148 |
+
アセット管理 (ZIPパース)
|
| 149 |
+
========================================== */
|
| 150 |
+
async function loadAssets() {
|
| 151 |
+
const loadingScreen = document.getElementById('loading');
|
| 152 |
+
const loadingText = document.getElementById('loading-text');
|
| 153 |
+
loadingScreen.style.display = 'flex';
|
| 154 |
+
|
| 155 |
+
try {
|
| 156 |
+
loadingText.innerText = "Fetching ZIP... (InventivetalentDev-minecraft-assets)";
|
| 157 |
+
const response = await fetch('InventivetalentDev-minecraft-assets-26.1.2-0-g44dd81e.zip');
|
| 158 |
+
if (!response.ok) throw new Error("ZIP not found");
|
| 159 |
+
|
| 160 |
+
const blob = await response.blob();
|
| 161 |
+
loadingText.innerText = "Extracting ZIP via JSZip...";
|
| 162 |
+
const zip = await JSZip.loadAsync(blob);
|
| 163 |
+
|
| 164 |
+
// 簡易テクスチャローダー (一部のブロックを抽出)
|
| 165 |
+
const textureFiles = {
|
| 166 |
+
1: 'assets/minecraft/textures/block/dirt.png',
|
| 167 |
+
2: 'assets/minecraft/textures/block/grass_block_side.png',
|
| 168 |
+
3: 'assets/minecraft/textures/block/stone.png',
|
| 169 |
+
4: 'assets/minecraft/textures/block/oak_log.png',
|
| 170 |
+
8: 'assets/minecraft/textures/block/chest_front.png' // チェスト
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
const loader = new THREE.TextureLoader();
|
| 174 |
+
for (let id in textureFiles) {
|
| 175 |
+
const path = textureFiles[id];
|
| 176 |
+
if (zip.file(path)) {
|
| 177 |
+
const base64 = await zip.file(path).async("base64");
|
| 178 |
+
const tex = loader.load('data:image/png;base64,' + base64);
|
| 179 |
+
tex.magFilter = THREE.NearestFilter; // マイクラ風ドット感
|
| 180 |
+
textures[id] = tex;
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
console.log("Assets loaded successfully.");
|
| 184 |
+
} catch (err) {
|
| 185 |
+
console.warn("Asset Load Failed. Using fallback colors.", err);
|
| 186 |
+
// フォールバック: テクスチャがない場合は単一カラー
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// マテリアル生成
|
| 190 |
+
blockMaterials[BLOCKS.DIRT] = new THREE.MeshLambertMaterial({ map: textures[1] || null, color: textures[1] ? 0xffffff : 0x5c4033 });
|
| 191 |
+
blockMaterials[BLOCKS.GRASS] = new THREE.MeshLambertMaterial({ map: textures[2] || null, color: textures[2] ? 0xffffff : 0x41980a });
|
| 192 |
+
blockMaterials[BLOCKS.STONE] = new THREE.MeshLambertMaterial({ map: textures[3] || null, color: textures[3] ? 0xffffff : 0x888888 });
|
| 193 |
+
blockMaterials[BLOCKS.LOG] = new THREE.MeshLambertMaterial({ map: textures[4] || null, color: textures[4] ? 0xffffff : 0x6b4423 });
|
| 194 |
+
blockMaterials[BLOCKS.LEAVES] = new THREE.MeshLambertMaterial({ color: 0x228b22, transparent: true, opacity: 0.8 });
|
| 195 |
+
blockMaterials[BLOCKS.CHEST] = new THREE.MeshLambertMaterial({ map: textures[8] || null, color: textures[8] ? 0xffffff : 0xd2b48c });
|
| 196 |
+
blockMaterials[BLOCKS.NETHER_PORTAL] = new THREE.MeshLambertMaterial({ color: 0x800080, transparent: true, opacity: 0.7 });
|
| 197 |
+
|
| 198 |
+
loadingScreen.style.display = 'none';
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/* ==========================================
|
| 202 |
+
7. 地形生成 & 6. ディメンション
|
| 203 |
+
========================================== */
|
| 204 |
+
const simplex = new SimplexNoise();
|
| 205 |
+
let instancedMesh; // 描画軽量化のためのInstancedMesh
|
| 206 |
+
|
| 207 |
+
function generateWorld(dimension) {
|
| 208 |
+
// ワールドリセット
|
| 209 |
+
if (instancedMesh) scene.remove(instancedMesh);
|
| 210 |
+
worldBlocks = {};
|
| 211 |
+
|
| 212 |
+
const geometry = new THREE.BoxGeometry(blockSize, blockSize, blockSize);
|
| 213 |
+
// 簡易的に全ブロック共通のマテリアル配列を渡し、インスタンスごとに変更する処理は複雑になるため
|
| 214 |
+
// 今回はパフォーマンスとコード量削減のため、ブロック種別ごとにMeshGroupを作成します。
|
| 215 |
+
|
| 216 |
+
const groups = {};
|
| 217 |
+
for(let key in BLOCKS) groups[BLOCKS[key]] = [];
|
| 218 |
+
|
| 219 |
+
// 地形生成アルゴリズム
|
| 220 |
+
for (let x = -CHUNK_SIZE/2; x < CHUNK_SIZE/2; x++) {
|
| 221 |
+
for (let z = -CHUNK_SIZE/2; z < CHUNK_SIZE/2; z++) {
|
| 222 |
+
|
| 223 |
+
if (dimension === 'overworld') {
|
| 224 |
+
// 平原・山の複合ノイズ
|
| 225 |
+
let yNoise = Math.floor(simplex.noise2D(x / 20, z / 20) * 5 + 5);
|
| 226 |
+
for (let y = 0; y <= yNoise; y++) {
|
| 227 |
+
let type = (y === yNoise) ? BLOCKS.GRASS : (y > yNoise - 3 ? BLOCKS.DIRT : BLOCKS.STONE);
|
| 228 |
+
setBlockData(x, y, z, type);
|
| 229 |
+
groups[type].push(new THREE.Vector3(x, y, z));
|
| 230 |
+
}
|
| 231 |
+
// 木の生成 (簡易)
|
| 232 |
+
if (yNoise > 2 && Math.random() < 0.05) {
|
| 233 |
+
for(let ty=1; ty<=4; ty++) { setBlockData(x, yNoise+ty, z, BLOCKS.LOG); groups[BLOCKS.LOG].push(new THREE.Vector3(x, yNoise+ty, z)); }
|
| 234 |
+
setBlockData(x, yNoise+5, z, BLOCKS.LEAVES); groups[BLOCKS.LEAVES].push(new THREE.Vector3(x, yNoise+5, z));
|
| 235 |
+
}
|
| 236 |
+
scene.background = new THREE.Color(0x87ceeb); // 空色
|
| 237 |
+
}
|
| 238 |
+
else if (dimension === 'nether') {
|
| 239 |
+
// ネザー地形 (天井と床)
|
| 240 |
+
let floorNoise = Math.floor(simplex.noise2D(x / 15, z / 15) * 3);
|
| 241 |
+
setBlockData(x, floorNoise, z, BLOCKS.STONE);
|
| 242 |
+
groups[BLOCKS.STONE].push(new THREE.Vector3(x, floorNoise, z));
|
| 243 |
+
// 色を赤暗くするフォールバック処理は今回は背景色で表現
|
| 244 |
+
scene.background = new THREE.Color(0x2a0000);
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
// ネザーゲートのテスト配置
|
| 250 |
+
if(dimension === 'overworld') {
|
| 251 |
+
setBlockData(0, 10, 5, BLOCKS.NETHER_PORTAL);
|
| 252 |
+
groups[BLOCKS.NETHER_PORTAL].push(new THREE.Vector3(0, 10, 5));
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
// メッシュのビルド (コード短縮のためInstancedMesh使用)
|
| 256 |
+
for (let type in groups) {
|
| 257 |
+
if(groups[type].length === 0) continue;
|
| 258 |
+
let mesh = new THREE.InstancedMesh(geometry, blockMaterials[type] || blockMaterials[BLOCKS.STONE], groups[type].length);
|
| 259 |
+
let dummy = new THREE.Object3D();
|
| 260 |
+
groups[type].forEach((pos, i) => {
|
| 261 |
+
dummy.position.copy(pos);
|
| 262 |
+
dummy.updateMatrix();
|
| 263 |
+
mesh.setMatrixAt(i, dummy.matrix);
|
| 264 |
+
});
|
| 265 |
+
mesh.name = "blockGroup_" + type;
|
| 266 |
+
scene.add(mesh);
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
function setBlockData(x, y, z, type) {
|
| 271 |
+
worldBlocks[`${x},${y},${z}`] = type;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
/* ==========================================
|
| 275 |
+
5. モブについて (簡易AI)
|
| 276 |
+
========================================== */
|
| 277 |
+
function spawnMob(x, y, z, type) {
|
| 278 |
+
const geo = new THREE.BoxGeometry(0.8, 1.8, 0.8);
|
| 279 |
+
const mat = new THREE.MeshLambertMaterial({ color: type === 'zombie' ? 0x00ff00 : 0xffffff });
|
| 280 |
+
const mesh = new THREE.Mesh(geo, mat);
|
| 281 |
+
mesh.position.set(x, y, z);
|
| 282 |
+
scene.add(mesh);
|
| 283 |
+
mobs.push({ mesh: mesh, type: type, speed: 0.02 });
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
function updateMobs() {
|
| 287 |
+
// プレイヤーを追跡する簡易AI
|
| 288 |
+
const playerPos = camera.position;
|
| 289 |
+
mobs.forEach(mob => {
|
| 290 |
+
if (mob.type === 'zombie') {
|
| 291 |
+
const dx = playerPos.x - mob.mesh.position.x;
|
| 292 |
+
const dz = playerPos.z - mob.mesh.position.z;
|
| 293 |
+
const dist = Math.sqrt(dx*dx + dz*dz);
|
| 294 |
+
if(dist > 1.5 && dist < 16) { // 近づきすぎず、遠すぎない範囲で追尾
|
| 295 |
+
mob.mesh.position.x += (dx/dist) * mob.speed;
|
| 296 |
+
mob.mesh.position.z += (dz/dist) * mob.speed;
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
});
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
/* ==========================================
|
| 303 |
+
初期化 & コントロール
|
| 304 |
+
========================================== */
|
| 305 |
+
function initGame() {
|
| 306 |
+
document.getElementById('home-screen').style.display = 'none';
|
| 307 |
+
document.getElementById('game-ui').style.display = 'block';
|
| 308 |
+
|
| 309 |
+
scene = new THREE.Scene();
|
| 310 |
+
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
| 311 |
+
|
| 312 |
+
renderer = new THREE.WebGLRenderer({ antialias: false }); // マイクラ風にするためあえてオフ
|
| 313 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 314 |
+
document.body.appendChild(renderer.domElement);
|
| 315 |
+
|
| 316 |
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
| 317 |
+
scene.add(ambientLight);
|
| 318 |
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
|
| 319 |
+
dirLight.position.set(10, 20, 10);
|
| 320 |
+
scene.add(dirLight);
|
| 321 |
+
|
| 322 |
+
generateWorld(currentDimension);
|
| 323 |
+
|
| 324 |
+
// プレイヤー初期位置
|
| 325 |
+
camera.position.set(0, 15, 0);
|
| 326 |
+
|
| 327 |
+
controls = new THREE.PointerLockControls(camera, document.body);
|
| 328 |
+
document.addEventListener('click', () => { if(!controls.isLocked) controls.lock(); });
|
| 329 |
+
|
| 330 |
+
// スポーン
|
| 331 |
+
spawnMob(5, 10, 5, 'zombie');
|
| 332 |
+
|
| 333 |
+
setupInputs();
|
| 334 |
+
animate();
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false;
|
| 338 |
+
let velocity = new THREE.Vector3();
|
| 339 |
+
let direction = new THREE.Vector3();
|
| 340 |
+
const clock = new THREE.Clock();
|
| 341 |
+
|
| 342 |
+
function setupInputs() {
|
| 343 |
+
document.addEventListener('keydown', (e) => {
|
| 344 |
+
switch (e.code) {
|
| 345 |
+
case 'KeyW': moveForward = true; break;
|
| 346 |
+
case 'KeyA': moveLeft = true; break;
|
| 347 |
+
case 'KeyS': moveBackward = true; break;
|
| 348 |
+
case 'KeyD': moveRight = true; break;
|
| 349 |
+
case 'Space': if (velocity.y === 0) velocity.y = 10; break; // ジャンプ
|
| 350 |
+
case 'Digit1': selectHotbar(1, BLOCKS.DIRT); break;
|
| 351 |
+
case 'Digit2': selectHotbar(2, BLOCKS.STONE); break;
|
| 352 |
+
case 'Digit3': selectHotbar(3, BLOCKS.LOG); break;
|
| 353 |
+
case 'Digit4': selectHotbar(4, BLOCKS.CHEST); break; // 8. 特別なアイテム
|
| 354 |
+
case 'Digit5': selectHotbar(5, BLOCKS.NETHER_PORTAL); break;
|
| 355 |
+
}
|
| 356 |
+
});
|
| 357 |
+
document.addEventListener('keyup', (e) => {
|
| 358 |
+
switch (e.code) {
|
| 359 |
+
case 'KeyW': moveForward = false; break;
|
| 360 |
+
case 'KeyA': moveLeft = false; break;
|
| 361 |
+
case 'KeyS': moveBackward = false; break;
|
| 362 |
+
case 'KeyD': moveRight = false; break;
|
| 363 |
+
}
|
| 364 |
+
});
|
| 365 |
+
|
| 366 |
+
// 9. ブロックの破壊・設置 (特別なブロックのインタラクション含む)
|
| 367 |
+
document.addEventListener('mousedown', (e) => {
|
| 368 |
+
if (!controls.isLocked) return;
|
| 369 |
+
|
| 370 |
+
// 画面中央からレイキャスト
|
| 371 |
+
const raycaster = new THREE.Raycaster();
|
| 372 |
+
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
|
| 373 |
+
// 対象はすべてのInstancedMesh
|
| 374 |
+
const intersects = raycaster.intersectObjects(scene.children.filter(c => c.isInstancedMesh));
|
| 375 |
+
|
| 376 |
+
if (intersects.length > 0) {
|
| 377 |
+
const hit = intersects[0];
|
| 378 |
+
const hitObj = hit.object;
|
| 379 |
+
|
| 380 |
+
// 簡易的な座標計算
|
| 381 |
+
const p = hit.point.clone().sub(hit.face.normal.clone().multiplyScalar(0.1));
|
| 382 |
+
const blockX = Math.round(p.x);
|
| 383 |
+
const blockY = Math.round(p.y);
|
| 384 |
+
const blockZ = Math.round(p.z);
|
| 385 |
+
|
| 386 |
+
// 特別なブロックの動作チェック
|
| 387 |
+
const targetBlock = worldBlocks[`${blockX},${blockY},${blockZ}`];
|
| 388 |
+
if (targetBlock === BLOCKS.NETHER_PORTAL) {
|
| 389 |
+
currentDimension = currentDimension === 'overworld' ? 'nether' : 'overworld';
|
| 390 |
+
generateWorld(currentDimension);
|
| 391 |
+
return;
|
| 392 |
+
}
|
| 393 |
+
if (targetBlock === BLOCKS.CHEST && e.button === 2) {
|
| 394 |
+
alert("Chest opened! (Inventory system placeholder)");
|
| 395 |
+
return;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
if (e.button === 0) { // 左クリック: 破壊 (配列・メッシュ再生成を省くため透明化アプローチなどはコード長くなるため、今回は割愛しコンソールのみ)
|
| 399 |
+
console.log(`Broke block at ${blockX}, ${blockY}, ${blockZ}`);
|
| 400 |
+
// ※完全な破壊・追加実装はChunk管理クラスが必要なため、このプロトタイプではHit判定のみ
|
| 401 |
+
} else if (e.button === 2) { // 右クリック: 設置
|
| 402 |
+
const np = hit.point.clone().add(hit.face.normal.clone().multiplyScalar(0.5));
|
| 403 |
+
console.log(`Placed block at ${Math.round(np.x)}, ${Math.round(np.y)}, ${Math.round(np.z)}`);
|
| 404 |
+
}
|
| 405 |
+
}
|
| 406 |
+
});
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
function selectHotbar(slot, blockId) {
|
| 410 |
+
document.querySelectorAll('.slot').forEach(el => el.classList.remove('active'));
|
| 411 |
+
document.getElementById(`slot-${slot}`).classList.add('active');
|
| 412 |
+
selectedBlock = blockId;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
/* ==========================================
|
| 416 |
+
メインループ
|
| 417 |
+
========================================== */
|
| 418 |
+
function animate() {
|
| 419 |
+
requestAnimationFrame(animate);
|
| 420 |
+
|
| 421 |
+
if (controls.isLocked) {
|
| 422 |
+
const delta = clock.getDelta();
|
| 423 |
+
|
| 424 |
+
// 物理演算 (簡易)
|
| 425 |
+
velocity.x -= velocity.x * 10.0 * delta;
|
| 426 |
+
velocity.z -= velocity.z * 10.0 * delta;
|
| 427 |
+
velocity.y -= 9.8 * 3.0 * delta; // 重力
|
| 428 |
+
|
| 429 |
+
direction.z = Number(moveForward) - Number(moveBackward);
|
| 430 |
+
direction.x = Number(moveRight) - Number(moveLeft);
|
| 431 |
+
direction.normalize();
|
| 432 |
+
|
| 433 |
+
const speed = gameMode === 'creative' ? 80.0 : 40.0;
|
| 434 |
+
if (moveForward || moveBackward) velocity.z -= direction.z * speed * delta;
|
| 435 |
+
if (moveLeft || moveRight) velocity.x -= direction.x * speed * delta;
|
| 436 |
+
|
| 437 |
+
controls.moveRight(-velocity.x * delta);
|
| 438 |
+
controls.moveForward(-velocity.z * delta);
|
| 439 |
+
controls.getObject().position.y += (velocity.y * delta);
|
| 440 |
+
|
| 441 |
+
// 簡易床衝突判定
|
| 442 |
+
if (controls.getObject().position.y < 10) {
|
| 443 |
+
velocity.y = 0;
|
| 444 |
+
controls.getObject().position.y = 10;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
updateMobs();
|
| 448 |
+
|
| 449 |
+
// UI更新
|
| 450 |
+
document.getElementById('pos').innerText = `XYZ: ${Math.floor(camera.position.x)}, ${Math.floor(camera.position.y)}, ${Math.floor(camera.position.z)}`;
|
| 451 |
+
document.getElementById('dim').innerText = `Dim: ${currentDimension.charAt(0).toUpperCase() + currentDimension.slice(1)}`;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
renderer.render(scene, camera);
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// 画面リサイズ対応
|
| 458 |
+
window.addEventListener('resize', () => {
|
| 459 |
+
if(camera) {
|
| 460 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
| 461 |
+
camera.updateProjectionMatrix();
|
| 462 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 463 |
+
}
|
| 464 |
+
});
|
| 465 |
+
|
| 466 |
+
// 起動処理
|
| 467 |
+
async function startGame(mode) {
|
| 468 |
+
gameMode = mode;
|
| 469 |
+
await loadAssets(); // ZIPからアセット読み込み
|
| 470 |
+
initGame();
|
| 471 |
+
}
|
| 472 |
+
</script>
|
| 473 |
+
</body>
|
| 474 |
+
</html>
|