<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>怪獣大進撃 3D - Kaiju City Destruction</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
<style>
body { margin: 0; overflow: hidden; background-color: #000; font-family: sans-serif; }
#start-screen {
backdrop-filter: blur(8px);
}
#ui-layer {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 10;
display: none;
flex-direction: column;
justify-content: space-between;
padding: 1rem;
}
.pointer-events-auto { pointer-events: auto; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: rgba(0,0,0,0.5); }
::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 3px; }
</style>
</head>
<body>
<div id="canvas-container" class="absolute inset-0 z-0"></div>
<!-- スタート画面 (怪獣選択) -->
<div id="start-screen" class="fixed inset-0 bg-black/70 z-50 flex flex-col items-center justify-center pointer-events-auto transition-opacity duration-500">
<h1 class="text-5xl md:text-7xl font-bold text-white mb-4 tracking-widest drop-shadow-[0_0_15px_rgba(255,255,255,0.5)]">怪獣大進撃 3D</h1>
<p class="text-gray-300 mb-12 text-lg drop-shadow-md">果てしない世界を破壊する大怪獣を選択してください</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl w-full px-4">
<button onclick="startGame('godzilla')" class="group bg-neutral-900/80 border-2 border-blue-900 rounded-xl p-6 hover:bg-blue-900/50 hover:border-blue-500 transition-all transform hover:-translate-y-2 shadow-lg">
<h2 class="text-3xl font-bold text-blue-400 mb-2">ゴジラ</h2>
<p class="text-sm text-gray-400">圧倒的な耐久力と強力な「放射熱線」で全てを焼き尽くす怪獣王。</p>
</button>
<button onclick="startGame('rodan')" class="group bg-neutral-900/80 border-2 border-orange-900 rounded-xl p-6 hover:bg-orange-900/50 hover:border-orange-500 transition-all transform hover:-translate-y-2 shadow-lg">
<h2 class="text-3xl font-bold text-orange-500 mb-2">ラドン</h2>
<p class="text-sm text-gray-400">上空から突風で街を粉砕し「マグマ熱線」を放つ空の大怪獣。</p>
</button>
<button onclick="startGame('anguirus')" class="group bg-neutral-900/80 border-2 border-amber-900 rounded-xl p-6 hover:bg-amber-900/50 hover:border-amber-500 transition-all transform hover:-translate-y-2 shadow-lg">
<h2 class="text-3xl font-bold text-amber-500 mb-2">アンギラス</h2>
<p class="text-sm text-gray-400">背中に無数の鋭いトゲを持つ暴龍。四足歩行の突進と「超音波咆哮」で街を粉砕する。</p>
</button>
<button onclick="startGame('gamera')" class="group bg-neutral-900/80 border-2 border-green-900 rounded-xl p-6 hover:bg-green-900/50 hover:border-green-500 transition-all transform hover:-translate-y-2 shadow-lg">
<h2 class="text-3xl font-bold text-green-400 mb-2">ガメラ</h2>
<p class="text-sm text-gray-400">甲羅からジェット噴射して飛び、「プラズマ火球」を放つ大怪獣。</p>
</button>
<button onclick="startGame('gyaos')" class="group bg-neutral-900/80 border-2 border-red-900 rounded-xl p-6 hover:bg-red-900/50 hover:border-red-500 transition-all transform hover:-translate-y-2 shadow-lg">
<h2 class="text-3xl font-bold text-red-500 mb-2">ギャオス</h2>
<p class="text-sm text-gray-400">超高速で飛行し、全てを両断する「超音波メス」を放つ凶鳥。</p>
</button>
</div>
</div>
<!-- ゲームUI -->
<div id="ui-layer">
<div class="flex flex-col md:flex-row gap-4 justify-between items-start pointer-events-auto">
<div class="bg-neutral-900/80 backdrop-blur-sm p-4 rounded-xl border border-neutral-700 w-full md:w-80 shadow-lg">
<div class="flex justify-between items-center mb-3 pb-2 border-b border-neutral-600">
<h1 id="monster-name" class="text-lg font-bold text-white">Monster City Destruction</h1>
<div class="flex items-center gap-2">
<div class="text-sm font-bold bg-neutral-800 px-3 py-1 rounded text-white border border-neutral-600">
<span id="day-counter">1</span>日目 <span id="time-display" class="text-yellow-300 ml-1">12:00</span>
</div>
<button onclick="returnToHome()" class="text-xs bg-red-900 hover:bg-red-700 text-white px-2 py-1 rounded border border-red-700 transition">終了</button>
</div>
</div>
<div class="space-y-3">
<div><div class="flex justify-between text-xs mb-1 text-gray-200"><span>❤️ HP (無敵)</span><span id="hp-text">100</span></div></div>
<div><div class="flex justify-between text-xs mb-1 text-gray-200"><span>🍖 満腹度</span><span id="food-text">100</span></div></div>
<div><div class="flex justify-between text-xs mb-1 text-gray-200"><span>💧 水分</span><span id="water-text">100</span></div></div>
<div><div class="flex justify-between text-xs mb-1 text-gray-200"><span>⚡ スタミナ (無尽蔵)</span><span id="energy-text">100</span></div></div>
</div>
</div>
<!-- PC用操作ガイド (スマホでは非表示) -->
<div class="hidden md:flex bg-neutral-900/80 backdrop-blur-sm p-4 rounded-xl border border-neutral-700 text-gray-200 text-sm shadow-lg flex-col gap-3">
<div>
<h2 class="font-bold text-gray-400 mb-1 border-b border-neutral-700 pb-1">🎮 操作方法</h2>
<p>十字キー / WASD : 移動 (Shiftでダッシュ)</p>
<p>Spaceキー : <span id="action-beam-name" class="font-bold">ビーム (撃ち放題)</span></p>
<p>体や翼でビル・敵部隊を粉砕!</p>
</div>
<div>
<h2 class="font-bold text-gray-400 mb-1 border-b border-neutral-700 pb-1">🎒 所持品 & アクション</h2>
<ul class="space-y-1">
<li class="flex justify-between items-center"><button onclick="gameAction('eat')" class="hover:text-orange-400 transition">[2] 🍖 食料</button> <span id="inv-food" class="font-bold">2</span></li>
<li class="flex justify-between items-center"><button onclick="gameAction('drink')" class="hover:text-blue-400 transition">[3] 💧 水</button> <span id="inv-water" class="font-bold">2</span></li>
</ul>
<button onclick="gameAction('sleep')" class="w-full mt-2 bg-indigo-700 hover:bg-indigo-600 text-white rounded py-1 transition">[5] ⛺ 休む</button>
</div>
</div>
</div>
<div class="flex justify-start pointer-events-auto max-w-lg mt-auto mb-32 md:mb-0">
<div id="log-container" class="bg-black/60 backdrop-blur p-3 rounded-xl border border-neutral-800 w-full h-24 md:h-32 overflow-y-auto text-xs text-gray-300 space-y-1 flex flex-col-reverse shadow-lg"></div>
</div>
<!-- モバイル用タッチコントローラー -->
<div class="md:hidden absolute bottom-4 left-0 w-full px-4 flex justify-between items-end pointer-events-auto z-20">
<!-- ジョイスティック (左下) -->
<div id="joystick-zone" class="relative w-32 h-32 bg-white/10 rounded-full border border-white/20 touch-none flex items-center justify-center">
<div id="joystick-knob" class="w-12 h-12 bg-white/40 rounded-full absolute"></div>
</div>
<!-- アクションボタン (右下) -->
<div class="flex flex-col gap-2 items-end">
<div class="flex gap-2">
<button onclick="gameAction('eat')" class="w-12 h-12 bg-orange-900/80 text-white rounded-full flex items-center justify-center text-xl shadow-lg border border-orange-700">🍖</button>
<button onclick="gameAction('drink')" class="w-12 h-12 bg-blue-900/80 text-white rounded-full flex items-center justify-center text-xl shadow-lg border border-blue-700">💧</button>
</div>
<button onclick="gameAction('beam')" class="w-20 h-20 bg-red-900/80 text-white font-bold rounded-full flex items-center justify-center text-lg shadow-lg border border-red-700 mt-2">攻撃</button>
</div>
</div>
</div>
<!-- ゲームオーバー -->
<div id="game-over-modal" class="fixed inset-0 bg-red-950/90 z-50 hidden flex flex-col items-center justify-center pointer-events-auto">
<h2 class="text-5xl font-bold text-red-500 mb-4 drop-shadow-lg">GAME OVER</h2>
<p id="death-reason" class="text-xl text-gray-300 mb-8">討伐されてしまった...</p>
<p class="text-lg text-gray-400 mb-8 border border-neutral-700 p-4 rounded bg-black/50">生存日数: <span id="final-days" class="text-white font-bold text-2xl"></span> 日</p>
<button onclick="returnToHome()" class="bg-red-700 hover:bg-red-600 text-white font-bold py-3 px-8 rounded-lg text-xl shadow-lg transition">タイトルへ戻る</button>
</div>
<script>
// === 怪獣データ定義 ===
const MONSTERS = {
godzilla: { name: 'ゴジラ', type: 'walk', height: 0, speed: 0.15, runMult: 2.0, uiClass: 'text-blue-400', color: 0x111111, emissive: 0x0088ff, beamName: '放射熱線', beamColor: 0x88ffff },
rodan: { name: 'ラドン', type: 'fly', height: 12, speed: 0.25, runMult: 2.0, uiClass: 'text-orange-500', color: 0x3b1510, emissive: 0xff3300, beamName: 'マグマ熱線', beamColor: 0xffaa00 },
anguirus: { name: 'アンギラス', type: 'walk', height: 0, speed: 0.18, runMult: 2.2, uiClass: 'text-amber-500', color: 0x8b5a2b, emissive: 0x442200, beamName: '超音波咆哮', beamColor: 0xdddddd },
gamera: { name: 'ガメラ', type: 'walk', height: 0, speed: 0.12, runMult: 1.5, uiClass: 'text-green-400', color: 0x112211, emissive: 0xffaa00, beamName: 'プラズマ火球', beamColor: 0xff8800 },
gyaos: { name: 'ギャオス', type: 'fly', height: 10, speed: 0.28, runMult: 2.2, uiClass: 'text-red-500', color: 0x4a2525, emissive: 0xaa0000, beamName: '超音波メス', beamColor: 0xffffaa }
};
const state = {
isPlaying: false, charId: null,
day: 1, gameTime: 12, isGameOver: false,
stats: { hp: 100, food: 100, water: 100, energy: 100 },
inventory: { food: 2, water: 2, wood: 0 },
cooldowns: { beam: 0 }
};
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x87CEEB, 0.015);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.getElementById('canvas-container').appendChild(renderer.domElement);
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
hemiLight.position.set(0, 200, 0);
scene.add(hemiLight);
const sunLight = new THREE.DirectionalLight(0xffffff, 1.0);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048; sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 0.5; sunLight.shadow.camera.far = 500;
sunLight.shadow.camera.left = -100; sunLight.shadow.camera.right = 100;
sunLight.shadow.camera.top = 100; sunLight.shadow.camera.bottom = -100;
scene.add(sunLight);
scene.add(sunLight.target); // ターゲットもシーンに追加して追従させる
const simplex = new SimplexNoise();
// --- テクスチャ・マテリアル生成 ---
function createBumpMap() {
const size = 512;
const canvas = document.createElement('canvas'); canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
const v = Math.pow(1.0 - Math.abs(simplex.noise2D(i/30, j/30)), 4.0);
const val = Math.floor((simplex.noise2D(i/10, j/10)*0.5+0.5) * 128 + v * 127);
const idx = (i * size + j) * 4;
imgData.data[idx] = imgData.data[idx+1] = imgData.data[idx+2] = val; imgData.data[idx+3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping; tex.repeat.set(3, 3);
return tex;
}
function createCityGround() {
const size = 512, canvas = document.createElement('canvas'), ctx = canvas.getContext('2d');
canvas.width = canvas.height = size;
ctx.fillStyle = '#222'; ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#3a3a3a'; ctx.fillRect(60, 60, size-120, size-120);
ctx.fillStyle = '#ddd';
for(let i=0; i<size; i+=30) {
ctx.fillRect(i, 58, 15, 4); ctx.fillRect(i, size-62, 15, 4);
ctx.fillRect(58, i, 4, 15); ctx.fillRect(size-62, i, 4, 15);
}
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
// 800サイズの平面に対して、1単位=10になるように設定 (10x80=800)
tex.repeat.set(80, 80);
return tex;
}
const bumpTex = createBumpMap();
const matGround = new THREE.MeshStandardMaterial({ color: 0x333333, map: createCityGround(), roughness: 0.9 });
const matTank = new THREE.MeshStandardMaterial({ color: 0x4b5320, roughness: 0.8 });
const matBullet = new THREE.MeshBasicMaterial({ color: 0xffcc00 });
const matRubble = new THREE.MeshLambertMaterial({ color: 0x444444 });
// 空と地面
const sky = new THREE.Mesh(new THREE.SphereGeometry(500, 32, 32), new THREE.MeshBasicMaterial({ color: 0x87CEEB, side: THREE.BackSide }));
scene.add(sky);
const ground = new THREE.Mesh(new THREE.PlaneGeometry(800, 800), matGround);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// --- 都市のビル用マテリアル (グローバル) ---
const bMats = ['#555', '#333', '#433', '#234'].map(c => {
const cMap = document.createElement('canvas'); cMap.width=cMap.height=256;
const ctx = cMap.getContext('2d'); ctx.fillStyle = c; ctx.fillRect(0,0,256,256);
ctx.fillStyle='#111'; for(let i=10;i<250;i+=30)for(let j=10;j<250;j+=40) ctx.fillRect(i,j,20,25);
const eMap = document.createElement('canvas'); eMap.width=eMap.height=256;
const eCtx = eMap.getContext('2d'); eCtx.fillStyle='#000'; eCtx.fillRect(0,0,256,256);
eCtx.fillStyle='#ffddaa'; for(let i=10;i<250;i+=30)for(let j=10;j<250;j+=40) if(Math.random()>0.5) eCtx.fillRect(i,j,20,25);
const tm = new THREE.CanvasTexture(cMap); tm.wrapS=tm.wrapT=THREE.RepeatWrapping;
const em = new THREE.CanvasTexture(eMap); em.wrapS=em.wrapT=THREE.RepeatWrapping;
return new THREE.MeshStandardMaterial({color:0xffffff, map:tm, emissive:0xffffff, emissiveMap:em, emissiveIntensity:0, roughness:0.6});
});
let matBody; // キャラクターごとに生成
let torso; // アニメーション操作用に保存
const player = new THREE.Group();
scene.add(player);
let playerMeshes = {}; // アニメーション用パーツ格納
// --- パーツ生成ヘルパー ---
function createOrganicPart(r, sx, sy, sz, mat, castShadow=true) {
const geo = new THREE.SphereGeometry(r, 16, 16); geo.scale(sx, sy, sz);
const mesh = new THREE.Mesh(geo, mat); mesh.castShadow = castShadow; return mesh;
}
function createTentacle(parent, num, scaleBase, decay, rotXBase, mat, saveTo) {
const segments = []; let currentParent = parent; let currentScale = scaleBase;
for(let i=0; i<num; i++) {
const segGroup = new THREE.Group();
const mesh = new THREE.Mesh(new THREE.CylinderGeometry(currentScale*0.9, currentScale*0.8, 1.2, 12), mat);
mesh.rotation.x = Math.PI/2; mesh.position.z = -0.6; mesh.castShadow = true;
segGroup.add(mesh);
let rx = typeof rotXBase === 'function' ? rotXBase(i) : rotXBase;
segGroup.rotation.x = rx; segGroup.userData.baseAngleX = rx;
segGroup.position.z = i === 0 ? 0 : -1.2;
currentParent.add(segGroup); segments.push(segGroup); currentParent = segGroup;
currentScale *= decay;
}
if (saveTo) saveTo.push({ segments: segments });
return currentParent;
}
function createWing(side, parent, mat, scaleFactor=1.0, shape='normal') {
const pivot = new THREE.Group(); pivot.position.set(side * 1.2 * scaleFactor, 0.2, 0.5);
parent.add(pivot);
const innerGeo = new THREE.BoxGeometry(7.0 * scaleFactor, 0.2, 5.0 * scaleFactor);
innerGeo.translate(side * 3.5 * scaleFactor, 0, -1.0);
const inner = new THREE.Mesh(innerGeo, mat); inner.castShadow = true; pivot.add(inner);
const outerPivot = new THREE.Group(); outerPivot.position.set(side * 7.0 * scaleFactor, 0, -1.0 * scaleFactor);
pivot.add(outerPivot);
const outerGeo = new THREE.BoxGeometry(6.0 * scaleFactor, 0.1, 4.0 * scaleFactor);
const posAttr = outerGeo.attributes.position;
for(let i=0; i<posAttr.count; i++) {
if((side===1 && posAttr.getX(i)>0) || (side===-1 && posAttr.getX(i)<0)) {
posAttr.setZ(i, posAttr.getZ(i) - 2.0 * scaleFactor);
if(shape==='sharp') posAttr.setX(i, posAttr.getX(i) + side*2.0);
}
}
outerGeo.computeVertexNormals(); outerGeo.translate(side * 3.0 * scaleFactor, 0, 0);
const outer = new THREE.Mesh(outerGeo, mat); outer.castShadow = true; outerPivot.add(outer);
playerMeshes.wings.push({ pivot: pivot, outer: outerPivot, side: side });
}
function attachHead(parent, mat, shape='normal') {
const headGrp = new THREE.Group();
let headMesh;
const matTooth = new THREE.MeshStandardMaterial({ color: 0xeeeee0, roughness: 0.5 });
const matEye = new THREE.MeshBasicMaterial({ color: 0xff5500 });
if(shape==='sharp') { // ギャオス
headMesh = new THREE.Mesh(new THREE.ConeGeometry(0.8, 2.5, 4), mat);
headMesh.rotation.x = -Math.PI/2;
} else if (shape === 'gamera') {
headMesh = createOrganicPart(0.9, 1.0, 0.8, 1.2, mat);
const muzzle = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.4, 1.0), mat);
muzzle.position.set(0, -0.1, 0.8); headMesh.add(muzzle);
for (let i=-1; i<=1; i+=0.5) {
if(Math.abs(i) < 0.2) continue;
const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.06, 0.2, 4), matTooth);
tooth.position.set(i*0.4, -0.3, 0.8 + Math.abs(i)*0.2);
tooth.rotation.x = Math.PI;
headMesh.add(tooth);
}
const brow = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.2, 0.6), mat);
brow.position.set(0, 0.5, 0.5); headMesh.add(brow);
const jawGrp = new THREE.Group();
jawGrp.position.set(0, -0.3, 0.2);
const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.3, 1.1), mat);
jaw.position.set(0, -0.1, 0.6); jawGrp.add(jaw);
for (let side of [-1, 1]) {
const bigTusk = new THREE.Mesh(new THREE.ConeGeometry(0.12, 0.8, 8), matTooth);
bigTusk.position.set(side * 0.4, 0.2, 1.0);
bigTusk.rotation.x = 0.2;
bigTusk.rotation.z = side * -0.2;
jawGrp.add(bigTusk);
const eyeWhite = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 8), new THREE.MeshBasicMaterial({color: 0xddddcc}));
eyeWhite.position.set(side * 0.45, 0.3, 0.4);
const eyePupil = new THREE.Mesh(new THREE.SphereGeometry(0.05, 8, 8), new THREE.MeshBasicMaterial({color: 0x114411}));
eyePupil.position.set(side * 0.52, 0.3, 0.45);
headMesh.add(eyeWhite);
headMesh.add(eyePupil);
}
for (let i=-1; i<=1; i+=0.5) {
if(Math.abs(i) < 0.2) continue;
const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.05, 0.2, 4), matTooth);
tooth.position.set(i*0.4, 0.05, 0.8 + Math.abs(i)*0.2);
jawGrp.add(tooth);
}
headMesh.add(jawGrp);
} else if (shape === 'ghidorah') {
headMesh = createOrganicPart(0.7, 0.9, 0.8, 1.2, mat);
const muzzle = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.3, 1.0), mat);
muzzle.position.set(0, -0.1, 0.8); headMesh.add(muzzle);
// ギドラ特有の三日月型の角と赤い目
for (let side of [-1, 1]) {
const horn = new THREE.Mesh(new THREE.ConeGeometry(0.1, 0.7, 4), mat);
horn.position.set(side * 0.35, 0.3, -0.2);
horn.rotation.x = -0.6;
horn.rotation.z = side * -0.3;
headMesh.add(horn);
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.06, 8, 8), new THREE.MeshBasicMaterial({color: 0xff0000}));
eye.position.set(side * 0.3, 0.2, 0.4);
headMesh.add(eye);
}
const jawGrp = new THREE.Group();
jawGrp.position.set(0, -0.2, 0.2);
jawGrp.rotation.x = 0.3;
const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.2, 0.9), mat);
jaw.position.set(0, -0.1, 0.5); jawGrp.add(jaw);
headMesh.add(jawGrp);
} else {
headMesh = createOrganicPart(0.8, 0.9, 0.8, 1.2, mat);
const muzzle = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.4, 1.2), mat);
muzzle.position.set(0, -0.1, 0.8); headMesh.add(muzzle);
const jawGrp = new THREE.Group();
jawGrp.position.set(0, -0.2, 0.2);
jawGrp.rotation.x = 0.3;
const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.35, 1.1), mat);
jaw.position.set(0, -0.15, 0.6); jawGrp.add(jaw);
headMesh.add(jawGrp);
for (let side of [-1, 1]) {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.06, 8, 8), matEye);
eye.position.set(side * 0.4, 0.25, 0.4);
headMesh.add(eye);
}
}
headGrp.add(headMesh);
const mouthPos = new THREE.Object3D(); mouthPos.position.set(0, 0, 1.5);
headGrp.add(mouthPos);
parent.add(headGrp);
playerMeshes.heads.push({ group: headGrp, mouth: mouthPos });
}
function createLegWalk(side, parent, mat, shape='normal') {
const legGrp = new THREE.Group(); legGrp.position.set(side*1.2, -0.5, 0); parent.add(legGrp);
const isGamera = shape === 'gamera';
const thighRad = isGamera ? 1.2 : 1.0;
const thigh = createOrganicPart(thighRad, 0.8, 1.4, 1.0, mat);
thigh.rotation.x = -0.2; legGrp.add(thigh);
const shinGrp = new THREE.Group(); shinGrp.position.set(0, -1.2, 0.2); thigh.add(shinGrp);
const shinRad = isGamera ? 0.8 : 0.6;
const shin = new THREE.Mesh(new THREE.CylinderGeometry(shinRad, shinRad*0.8, 1.4, 8), mat);
shin.position.y = -0.7; shin.rotation.x = 0.2; shinGrp.add(shin);
const foot = new THREE.Mesh(new THREE.BoxGeometry(shinRad*2, 0.4, 1.5), mat);
foot.position.set(0, -1.4, 0.4);
shinGrp.add(foot);
const matClaw = new THREE.MeshStandardMaterial({ color: isGamera ? 0xddddaa : 0xcca300, roughness: 0.6, metalness: 0.3 });
for(let i=-1; i<=1; i++) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.12, 0.6, 8), matClaw);
claw.position.set(i*shinRad*0.7, -1.4, 1.15);
claw.rotation.x = -Math.PI/2 + 0.1;
shinGrp.add(claw);
}
playerMeshes.legs.push({ thigh: thigh, shin: shinGrp });
}
function createArmWalk(side, parent, mat, shape='normal') {
const armGrp = new THREE.Group(); armGrp.position.set(side*1.5, 0.5, 0.8); parent.add(armGrp);
const isGamera = shape === 'gamera';
const rad = isGamera ? 0.4 : 0.3;
// バニシング・フィスト (右腕のみ発光マテリアル)
const isRightArm = side === 1;
const armMat = (isGamera && isRightArm) ? new THREE.MeshStandardMaterial({
color: 0xffaa00, emissive: 0xff5500, emissiveIntensity: 2.0, transparent: true, opacity: 0.9, roughness: 0.2
}) : mat;
const upper = new THREE.Mesh(new THREE.CylinderGeometry(rad, rad*0.8, 1.2, 8), armMat);
upper.rotation.set(0.5, 0, side*0.3); armGrp.add(upper);
const hand = new THREE.Mesh(new THREE.BoxGeometry(rad*2, rad*2, rad*2), armMat);
hand.position.set(0, -0.6, 0);
upper.add(hand);
const matClaw = (isGamera && isRightArm) ? armMat : new THREE.MeshStandardMaterial({ color: isGamera ? 0xddddaa : 0xcca300, roughness: 0.6, metalness: 0.3 });
for(let i=-1; i<=1; i+=0.8) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.08, 0.4, 4), matClaw);
claw.position.set(i*0.2, -0.8, 0.1);
claw.rotation.x = -Math.PI/2;
claw.rotation.y = i * 0.2;
upper.add(claw);
}
// ガメラのポーズ調整 (右腕を前に出す)
if (isGamera) {
if (isRightArm) {
armGrp.rotation.x = -0.5; // 前に突き出す
upper.rotation.z = 0.5; // 少し内側に
} else {
armGrp.rotation.x = 0.5; // 左腕は下ろす
}
}
playerMeshes.arms.push(upper);
}
// === モンスター動的構築 ===
function buildMonster(id) {
while(player.children.length > 0) player.remove(player.children[0]);
playerMeshes = { heads: [], tails: [], wings: [], legs: [], arms: [], necks: [] };
const info = MONSTERS[id];
player.position.set(0, info.height, 0);
matBody = new THREE.MeshStandardMaterial({
color: info.color, emissive: info.emissive, emissiveIntensity: 0,
roughness: 0.9, metalness: (id==='ghidorah'?0.8:0.1), bumpMap: bumpTex, bumpScale: 0.1
});
torso = createOrganicPart(1.6, 1.0, 1.2, 1.0, matBody);
player.add(torso);
if (id === 'godzilla') {
torso.position.y = 2.4;
torso.rotation.x = 0.0; // 直立に近い姿勢
const chest = createOrganicPart(1.6, 1.2, 1.1, 1.0, matBody); // 胸を厚く
chest.position.set(0, 1.4, 0.2);
chest.rotation.x = -0.1;
torso.add(chest);
const neck = createOrganicPart(1.0, 1.0, 1.2, 1.0, matBody); // 首を太く
neck.position.set(0, 1.2, 0.1);
neck.rotation.x = 0.1;
chest.add(neck);
// --- レジェゴジ風のマッシブな頭部 ---
const headGrp = new THREE.Group();
const headMesh = createOrganicPart(0.8, 0.9, 0.7, 1.1, matBody); // やや四角く
headMesh.position.set(0, 0, 0);
const muzzle = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.45, 1.2), matBody);
muzzle.position.set(0, -0.1, 0.7);
headMesh.add(muzzle);
// 口
const jawGrp = new THREE.Group();
jawGrp.position.set(0, -0.2, 0.2);
jawGrp.rotation.x = 0.2;
const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.3, 1.1), matBody);
jaw.position.set(0, -0.1, 0.6);
jawGrp.add(jaw);
// 牙
const matTooth = new THREE.MeshStandardMaterial({ color: 0xddccaa, roughness: 0.5 });
for(let i=-1; i<=1; i+=0.4) {
if(Math.abs(i) < 0.2) continue;
const topTooth = new THREE.Mesh(new THREE.ConeGeometry(0.04, 0.15, 4), matTooth);
topTooth.position.set(i*0.3, -0.2, 0.7 + Math.abs(i)*0.1);
topTooth.rotation.x = Math.PI;
headMesh.add(topTooth);
const btmTooth = new THREE.Mesh(new THREE.ConeGeometry(0.04, 0.15, 4), matTooth);
btmTooth.position.set(i*0.3, 0.05, 0.6 + Math.abs(i)*0.1);
jawGrp.add(btmTooth);
}
headMesh.add(jawGrp);
// 小さく鋭い目
const matEye = new THREE.MeshBasicMaterial({ color: 0xffcc00 }); // 黄色っぽい目
for (let side of [-1, 1]) {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.04, 8, 8), matEye);
eye.position.set(side * 0.38, 0.2, 0.4);
const brow = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.1, 0.2), matBody);
brow.position.set(side * 0.38, 0.25, 0.4);
brow.rotation.z = side * -0.2;
headMesh.add(eye);
headMesh.add(brow);
}
headGrp.add(headMesh);
const mouthPos = new THREE.Object3D(); mouthPos.position.set(0, 0, 1.5);
headGrp.add(mouthPos);
neck.add(headGrp);
playerMeshes.heads.push({ group: headGrp, mouth: mouthPos });
createTentacle(torso, 24, 1.4, 0.94, (i)=>i<5? -0.15 : 0.05, matBody, playerMeshes.tails); // 太く長い尻尾
// --- どっしりとした太い脚 ---
[-1, 1].forEach(side => {
const legGrp = new THREE.Group(); legGrp.position.set(side*1.3, -0.5, 0); torso.add(legGrp);
const thigh = createOrganicPart(1.3, 0.9, 1.6, 1.2, matBody); thigh.rotation.x = -0.2; legGrp.add(thigh);
const shinGrp = new THREE.Group(); shinGrp.position.set(0, -1.5, 0.2); thigh.add(shinGrp);
const shin = new THREE.Mesh(new THREE.CylinderGeometry(0.8, 0.7, 1.6, 12), matBody); shin.position.y = -0.7; shin.rotation.x = 0.2; shinGrp.add(shin);
const foot = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.5, 2.0), matBody);
foot.position.set(0, -1.5, 0.5);
shinGrp.add(foot);
const matClaw = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.8 }); // 黒っぽい爪
for(let i=-1; i<=1; i+=0.8) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.15, 0.6, 8), matClaw);
claw.position.set(i*0.6, -1.5, 1.4);
claw.rotation.x = -Math.PI/2 + 0.1;
shinGrp.add(claw);
}
playerMeshes.legs.push({ thigh: thigh, shin: shinGrp });
});
// --- 腕 ---
[-1, 1].forEach(side => {
const armGrp = new THREE.Group(); armGrp.position.set(side*1.6, 0.5, 0.6); chest.add(armGrp);
const upper = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.3, 1.5, 8), matBody);
upper.rotation.set(0.5, 0, side*0.4); armGrp.add(upper);
const handGrp = new THREE.Group();
handGrp.position.set(0, -0.8, 0.2);
handGrp.rotation.x = -0.6;
upper.add(handGrp);
const hand = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), matBody);
handGrp.add(hand);
const matClaw = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.8 });
for(let i=-1; i<=1; i+=0.8) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.06, 0.5, 4), matClaw);
claw.position.set(i*0.2, -0.3, 0.2);
claw.rotation.x = -Math.PI/2;
claw.rotation.y = i * 0.2;
handGrp.add(claw);
}
playerMeshes.arms.push(upper);
});
// --- 青く強く発光する背びれ ---
const matSpike = new THREE.MeshStandardMaterial({
color: 0x00ffff, // シアン
emissive: 0x00aaff, // 強く発光
emissiveIntensity: 1.5, // 最初から光らせる
roughness: 0.4,
transparent: true,
opacity: 0.9
});
// ギザギザとした山型
const spikeShape = new THREE.Shape();
spikeShape.moveTo(0, 0);
spikeShape.lineTo(0.4, 1.0);
spikeShape.lineTo(0.2, 1.5);
spikeShape.lineTo(0.6, 2.0);
spikeShape.lineTo(0, 3.0); // 頂点
spikeShape.lineTo(-0.6, 2.0);
spikeShape.lineTo(-0.2, 1.5);
spikeShape.lineTo(-0.4, 1.0);
spikeShape.lineTo(0, 0);
const spikeExtrude = { depth: 0.2, bevelEnabled: true, bevelSegments: 2, steps: 1, bevelSize: 0.05, bevelThickness: 0.05 };
const spikeGeo = new THREE.ExtrudeGeometry(spikeShape, spikeExtrude);
// 中央列
for(let i=0; i<18; i++) {
const spike = new THREE.Mesh(spikeGeo, matSpike);
let scale = 1.0;
if(i < 3) scale = 0.3 + i*0.2;
else if(i < 8) scale = 1.0 + (i-3)*0.15; // 大きめ
else scale = 1.6 - (i-8)*0.15;
spike.scale.set(scale, scale, scale);
if (i < 3) {
spike.position.set(0, 0.6 + i*0.3, -0.5);
spike.rotation.x = -0.2;
neck.add(spike);
} else if (i < 7) {
spike.position.set(0, 1.2 - (i-3)*0.4, -0.6);
spike.rotation.x = -0.05;
chest.add(spike);
} else if (i < 11) {
spike.position.set(0, 0.7 - (i-7)*0.4, -0.9);
spike.rotation.x = 0;
torso.add(spike);
} else {
const tailIdx = i - 11;
if(tailIdx < playerMeshes.tails[0].segments.length) {
spike.position.set(0, 0.8*scale, -0.4);
spike.rotation.x = -0.15;
playerMeshes.tails[0].segments[tailIdx].add(spike);
}
}
}
// 左右のサブ列
for (let side of [-1, 1]) {
for(let i=2; i<12; i++) {
const spike = new THREE.Mesh(spikeGeo, matSpike);
let scale = 0.4;
if(i > 3 && i < 8) scale = 0.7;
spike.scale.set(scale, scale, scale);
let target = torso;
let px = side * 0.7;
let py = 1.0 - (i-2)*0.3;
let pz = -0.7;
let rx = -0.05;
if (i < 4) { target = neck; px = side * 0.4; py = 0.6; pz = -0.5; rx = -0.2; }
else if (i < 7) { target = chest; px = side * 0.6; py = 1.1 - (i-4)*0.4; pz = -0.7; rx = -0.05; }
else if (i >= 10) {
const tailIdx = i - 10;
if(tailIdx < playerMeshes.tails[0].segments.length) {
target = playerMeshes.tails[0].segments[tailIdx];
px = side * 0.4; py = 0.6*scale; pz = -0.3; rx = -0.1;
}
}
spike.position.set(px, py, pz);
spike.rotation.x = rx;
spike.rotation.y = side * 0.2;
spike.rotation.z = side * -0.2;
target.add(spike);
}
}
}
else if (id === 'rodan') {
torso.scale.set(1.0, 0.7, 1.6); torso.rotation.x = 0.2;
const neck = createOrganicPart(0.6, 1.0, 1.0, 1.5, matBody); neck.position.set(0, 0.2, 1.8); torso.add(neck);
attachHead(neck, matBody);
createWing(1, torso, matBody); createWing(-1, torso, matBody);
createTentacle(torso, 6, 0.8, 0.8, 0, matBody, playerMeshes.tails);
}
else if (id === 'anguirus') {
torso.position.y = 1.5;
torso.rotation.x = -0.3; // 四足歩行気味に前傾
torso.scale.set(1.2, 1.0, 1.5);
// トゲトゲの甲羅
const matShell = new THREE.MeshStandardMaterial({color:0x5c4033, roughness:0.9, bumpMap:bumpTex, bumpScale:0.3});
const shell = createOrganicPart(1.6, 1.0, 0.6, 1.2, matShell);
shell.position.set(0, 0.8, -0.2);
torso.add(shell);
// 甲羅のトゲ (アンギラスの特徴)
for(let i=0; i<35; i++) {
const spike = new THREE.Mesh(new THREE.ConeGeometry(0.25, 0.8, 4), matShell);
const x = (Math.random()-0.5) * 2.5;
const z = (Math.random()-0.5) * 3.5;
const y = Math.cos(x*0.8)*0.5 + Math.cos(z*0.5)*0.5;
if(x*x + (z*0.7)*(z*0.7) < 3.0) {
spike.position.set(x, y + 0.4, z - 0.2);
spike.rotation.x = -0.2 + z*0.2;
spike.rotation.z = -x*0.3;
shell.add(spike);
}
}
const neck = createOrganicPart(0.7, 0.9, 1.0, 1.2, matBody);
neck.position.set(0, 0.6, 1.4);
neck.rotation.x = 0.4; // 頭を上げる
torso.add(neck);
// 頭部
const headGrp = new THREE.Group();
const headMesh = createOrganicPart(0.7, 0.9, 0.8, 1.2, matBody);
const muzzle = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.4, 1.0), matBody);
muzzle.position.set(0, -0.1, 0.8); headMesh.add(muzzle);
// 後頭部の角
for(let i of [-1, 0, 1]) {
const horn = new THREE.Mesh(new THREE.ConeGeometry(0.15, 0.8, 4), new THREE.MeshStandardMaterial({color: 0xddddcc}));
horn.position.set(i*0.3, 0.4, -0.4);
horn.rotation.x = -1.0;
horn.rotation.z = i*0.2;
headMesh.add(horn);
}
// 下顎
const jawGrp = new THREE.Group();
jawGrp.position.set(0, -0.2, 0.2);
jawGrp.rotation.x = 0.2;
const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.2, 0.9), matBody);
jaw.position.set(0, -0.1, 0.5); jawGrp.add(jaw);
headMesh.add(jawGrp);
headGrp.add(headMesh);
const mouthPos = new THREE.Object3D(); mouthPos.position.set(0, 0, 1.5);
headGrp.add(mouthPos);
neck.add(headGrp);
playerMeshes.heads.push({ group: headGrp, mouth: mouthPos });
// 尻尾
const tailGrp = createTentacle(torso, 10, 1.0, 0.9, (i)=> -0.1, matBody, playerMeshes.tails);
// 尻尾にもトゲ
playerMeshes.tails[0].segments.forEach((seg, i) => {
if(i%2===0) {
for(let j of [-1, 1]) {
const spike = new THREE.Mesh(new THREE.ConeGeometry(0.15, 0.6, 4), matShell);
spike.position.set(j*(0.8 - i*0.05), 0, -0.5);
spike.rotation.z = j * -Math.PI/2;
seg.add(spike);
}
}
});
createLegWalk(1, torso, matBody); createLegWalk(-1, torso, matBody);
// 前足 (四足歩行用)
for(let side of [-1, 1]) {
const armGrp = new THREE.Group(); armGrp.position.set(side*1.2, -0.3, 1.0); torso.add(armGrp);
const upper = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.4, 1.2, 8), matBody);
upper.rotation.set(0.5, 0, side*0.3); armGrp.add(upper);
const hand = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.4, 1.0), matBody);
hand.position.set(0, -0.6, 0.2);
upper.add(hand);
const matClaw = new THREE.MeshStandardMaterial({ color: 0xddddcc, roughness: 0.8 });
for(let i=-1; i<=1; i++) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.1, 0.5, 4), matClaw);
claw.position.set(i*0.3, -0.2, 0.5);
claw.rotation.x = -Math.PI/2 + 0.2;
hand.add(claw);
}
playerMeshes.arms.push(upper);
}
}
else if (id === 'gamera') {
torso.position.y = 2.0; torso.scale.set(1.3, 1.2, 1.0);
// リバース風の刺々しい黒緑の甲羅
const matShell = new THREE.MeshStandardMaterial({color:0x151a15, roughness:0.9, bumpMap:bumpTex, bumpScale:0.4});
const shell = createOrganicPart(2.2, 1.0, 0.4, 1.1, matShell); shell.position.set(0,0.8,-0.2); shell.rotation.x = -0.2; torso.add(shell);
// 鋭いエッジスパイク
for(let i=0; i<16; i++) {
const angle = (i/16) * Math.PI * 2;
if(Math.cos(angle) > 0.6) continue; // 頭の方向は避ける
const edgeSpike = new THREE.Mesh(new THREE.ConeGeometry(0.35, 1.0, 4), matShell);
edgeSpike.position.set(Math.sin(angle)*2.2, -0.2, Math.cos(angle)*2.4 - 0.2);
edgeSpike.rotation.x = Math.PI/2 + 0.4; // より前方に鋭く
edgeSpike.rotation.y = angle;
shell.add(edgeSpike);
}
// 背中の隆起
for(let x=-1; x<=1; x+=1) {
for(let z=-1; z<=1; z+=1) {
if(x===0 && z===0) continue;
const backSpike = new THREE.Mesh(new THREE.ConeGeometry(0.4, 0.8, 6), matShell);
backSpike.position.set(x*1.0, 0.4, z*1.0);
backSpike.rotation.x = z * -0.2;
backSpike.rotation.z = x * -0.2;
shell.add(backSpike);
}
}
// ブロック状の腹部
const matBelly = new THREE.MeshStandardMaterial({color:0x22221c, roughness:0.8, bumpMap:bumpTex, bumpScale:0.2});
const bellyPlate = createOrganicPart(1.6, 1.0, 1.0, 0.5, matBelly);
bellyPlate.position.set(0, -0.3, 0.8);
torso.add(bellyPlate);
const neck = createOrganicPart(0.8, 1.1, 1.0, 1.2, matBody); neck.position.set(0,1.0,1.2); torso.add(neck);
attachHead(neck, matBody, 'gamera');
// ゴツゴツした短い尻尾
createTentacle(torso, 4, 1.2, 0.6, -0.2, matBody, playerMeshes.tails);
createLegWalk(1, torso, matBody, 'gamera'); createLegWalk(-1, torso, matBody, 'gamera');
createArmWalk(1, torso, matBody, 'gamera'); createArmWalk(-1, torso, matBody, 'gamera');
}
else if (id === 'gyaos') {
torso.scale.set(0.9, 1.2, 0.8);
torso.rotation.x = -0.6; // 体を立てる姿勢
const neck = createOrganicPart(0.5, 1.2, 1.0, 0.8, matBody);
neck.position.set(0, 1.0, 0.6);
neck.rotation.x = 0.6; // 胴体の傾きを相殺して前を向く
torso.add(neck);
const headGrp = new THREE.Group();
headGrp.position.set(0, 0.8, 0.2);
neck.add(headGrp);
// 平たい矢尻型の頭部
const snoutGeo = new THREE.ConeGeometry(0.9, 2.2, 4);
snoutGeo.rotateY(Math.PI / 4); // ひし形にする
snoutGeo.rotateX(-Math.PI / 2); // 前に倒す
snoutGeo.scale(1.0, 0.4, 1.0); // 上下を平たく潰す
const snout = new THREE.Mesh(snoutGeo, matBody);
snout.position.set(0, 0, 0.6);
headGrp.add(snout);
// 後頭部の突起(耳のような角)
for(let side of [-1, 1]) {
const crest = new THREE.Mesh(new THREE.ConeGeometry(0.2, 1.2, 4), matBody);
crest.position.set(side * 0.4, 0.05, -0.4);
crest.rotation.x = Math.PI / 2;
crest.rotation.z = side * -0.4;
headGrp.add(crest);
// 赤く光る目
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), new THREE.MeshBasicMaterial({color: 0xff0000}));
eye.position.set(side * 0.35, 0.1, 0.4);
headGrp.add(eye);
}
// 下顎
const jawGeo = new THREE.ConeGeometry(0.7, 1.8, 4);
jawGeo.rotateY(Math.PI / 4);
jawGeo.rotateX(-Math.PI / 2);
jawGeo.scale(0.9, 0.3, 0.9);
const jawGrp = new THREE.Group();
jawGrp.position.set(0, -0.15, 0.5);
jawGrp.rotation.x = 0.2;
const jaw = new THREE.Mesh(jawGeo, matBody);
jaw.position.set(0, -0.05, 0.2);
jawGrp.add(jaw);
headGrp.add(jawGrp);
const mouthPos = new THREE.Object3D(); mouthPos.position.set(0, 0, 1.5);
headGrp.add(mouthPos);
playerMeshes.heads.push({ group: headGrp, mouth: mouthPos });
createWing(1, torso, matBody, 1.6, 'sharp');
createWing(-1, torso, matBody, 1.6, 'sharp');
createTentacle(torso, 6, 0.5, 0.8, -0.4, matBody, playerMeshes.tails);
// 長い脚を追加 (飛行中は垂れ下がる)
for(let side of [-1, 1]) {
const legGrp = new THREE.Group(); legGrp.position.set(side*0.7, -0.6, -0.2); torso.add(legGrp);
const thigh = createOrganicPart(0.5, 0.8, 1.4, 0.8, matBody); thigh.rotation.x = 0.6; legGrp.add(thigh);
const shinGrp = new THREE.Group(); shinGrp.position.set(0, -1.2, 0.2); thigh.add(shinGrp);
const shin = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.15, 1.4, 8), matBody); shin.position.y = -0.7; shin.rotation.x = -0.4; shinGrp.add(shin);
// 爪
for(let i=-1; i<=1; i+=1) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.06, 0.6, 4), new THREE.MeshStandardMaterial({color: 0x111111}));
claw.position.set(i*0.15, -1.4, 0.2);
claw.rotation.x = -0.2;
shinGrp.add(claw);
}
playerMeshes.legs.push({ thigh: thigh, shin: shinGrp });
}
}
}
// --- 街生成 ---
const buildings = [];
const activeBuildings = []; // マテリアル制御用
const generatedChunks = new Set();
const CHUNK_SIZE = 40;
const RENDER_DIST = 160;
function updateEndlessWorld(isInit = false) {
const px = player.position.x;
const pz = player.position.z;
const cx = Math.floor(px / CHUNK_SIZE);
const cz = Math.floor(pz / CHUNK_SIZE);
// 地面をプレイヤーに追従させる
ground.position.set(px, 0, pz);
// チャンクベースのビル生成
for (let x = cx - 3; x <= cx + 3; x++) {
for (let z = cz - 3; z <= cz + 3; z++) {
const key = `${x},${z}`;
if (!generatedChunks.has(key)) {
generatedChunks.add(key);
// 初期生成時のプレイヤー周囲は空ける
if (isInit && Math.hypot(x*CHUNK_SIZE, z*CHUNK_SIZE) < 30) continue;
// 区画内にビルを生成
const numBuildings = Math.floor(Math.random() * 2) + 1;
for (let b = 0; b < numBuildings; b++) {
const building = new THREE.Group();
const height = Math.random() * 20 + 8;
const width = Math.random() * 5 + 4;
const depth = Math.random() * 5 + 4;
const baseMat = bMats[Math.floor(Math.random() * bMats.length)];
const clonedMat = baseMat.clone();
clonedMat.map = baseMat.map.clone();
clonedMat.map.repeat.set(width/4, height/4);
clonedMat.emissiveMap = baseMat.emissiveMap.clone();
clonedMat.emissiveMap.repeat.set(width/4, height/4);
activeBuildings.push(clonedMat);
const buildingGeo = new THREE.BoxGeometry(width, height, depth);
const buildingMesh = new THREE.Mesh(buildingGeo, clonedMat);
buildingMesh.position.y = height / 2;
buildingMesh.castShadow = true;
buildingMesh.receiveShadow = true;
building.add(buildingMesh);
if (Math.random() > 0.4) {
const roofObjGeo = Math.random() > 0.5 ? new THREE.CylinderGeometry(0.8, 0.8, 2) : new THREE.BoxGeometry(2, 2, 2);
const roofObj = new THREE.Mesh(roofObjGeo, new THREE.MeshStandardMaterial({color: 0x666666}));
roofObj.position.set((Math.random()-0.5)*(width-3), height + 1.0, (Math.random()-0.5)*(depth-3));
roofObj.castShadow = true;
building.add(roofObj);
}
if (height > 18 && Math.random() > 0.6) {
const antennaGeo = new THREE.CylinderGeometry(0.05, 0.05, 8);
const antenna = new THREE.Mesh(antennaGeo, new THREE.MeshStandardMaterial({color: 0x333333, metalness: 0.8}));
antenna.position.set((Math.random()-0.5)*(width-2), height + 4, (Math.random()-0.5)*(depth-2));
building.add(antenna);
}
const bx = x * CHUNK_SIZE + (Math.random() - 0.5) * (CHUNK_SIZE - 10);
const bz = z * CHUNK_SIZE + (Math.random() - 0.5) * (CHUNK_SIZE - 10);
building.position.set(bx, 0, bz);
building.rotation.y = Math.floor(Math.random() * 4) * (Math.PI / 2);
scene.add(building);
buildings.push({ x: building.position.x, z: building.position.z, height: height, radius: Math.max(width, depth) * 0.6, group: building, mat: clonedMat, chunkKey: key });
}
}
}
}
// 古いビルの削除
for (let i = buildings.length - 1; i >= 0; i--) {
const b = buildings[i];
if (Math.hypot(px - b.x, pz - b.z) > RENDER_DIST) {
scene.remove(b.group);
const matIndex = activeBuildings.indexOf(b.mat);
if(matIndex > -1) activeBuildings.splice(matIndex, 1);
buildings.splice(i, 1);
generatedChunks.delete(b.chunkKey);
}
}
// 敵とアイテムの無限処理
for (let i = enemies.tanks.length - 1; i >= 0; i--) {
const t = enemies.tanks[i];
if (Math.hypot(px - t.mesh.position.x, pz - t.mesh.position.z) > RENDER_DIST + 50) {
scene.remove(t.mesh); enemies.tanks.splice(i, 1);
}
}
while (enemies.tanks.length < 8) {
enemies.tanks.push(createTank(px, pz));
}
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
if (Math.hypot(px - item.mesh.position.x, pz - item.mesh.position.z) > RENDER_DIST + 50) {
scene.remove(item.mesh); items.splice(i, 1);
}
}
if (items.length < 40) {
if(Math.random() < 0.5) spawnItem('food', px, pz);
else spawnItem('water', px, pz);
}
}
// --- 敵部隊・アイテム ---
const enemies = { tanks: [], bullets: [], beams: [] };
function createTank(px=0, pz=0) {
const grp = new THREE.Group();
const body = new THREE.Mesh(new THREE.BoxGeometry(2,1,3), matTank); body.position.y=0.5; body.castShadow=true; grp.add(body);
const turret = new THREE.Mesh(new THREE.BoxGeometry(1.2,0.6,1.5), matTank); turret.position.y=1.3; turret.castShadow=true; grp.add(turret);
const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.15,0.15,2,8), matTank); barrel.rotation.x=Math.PI/2; barrel.position.set(0,1.3,1.5); barrel.castShadow=true; grp.add(barrel);
let x, z;
do {
x = px + (Math.random()-0.5)*RENDER_DIST*1.5;
z = pz + (Math.random()-0.5)*RENDER_DIST*1.5;
} while(Math.hypot(px-x, pz-z) < 40);
grp.position.set(x, 0, z); scene.add(grp);
return { mesh: grp, x: x, z: z, radius: 1.5, cooldown: Math.random()*2 };
}
function fireBullet(start, target) {
const bullet = new THREE.Mesh(new THREE.SphereGeometry(0.2,8,8), matBullet);
bullet.position.copy(start); scene.add(bullet);
enemies.bullets.push({ mesh: bullet, dir: new THREE.Vector3().subVectors(target, start).normalize(), speed: 50, life: 3, damage: 2 });
}
const items = [];
function spawnItem(type, px=0, pz=0) {
let mesh;
if (type === 'food') mesh = new THREE.Mesh(new THREE.SphereGeometry(0.4,8,8), new THREE.MeshLambertMaterial({color:0xff3333}));
else if (type === 'water') mesh = new THREE.Mesh(new THREE.BoxGeometry(0.6,0.2,0.6), new THREE.MeshLambertMaterial({color:0x3388ff}));
else mesh = new THREE.Mesh(new THREE.DodecahedronGeometry(0.3), matRubble);
mesh.position.set(px + (Math.random()-0.5)*RENDER_DIST*1.5, type==='food'?0.4:0.15, pz + (Math.random()-0.5)*RENDER_DIST*1.5);
scene.add(mesh); items.push({ type: type, mesh: mesh });
}
const particles = [];
function spawnParticles(pos, count) {
for(let i=0;i<count;i++){
const p = new THREE.Mesh(new THREE.DodecahedronGeometry(0.3,0), new THREE.MeshStandardMaterial({color:0x666, roughness:1}));
p.position.copy(pos).add(new THREE.Vector3((Math.random()-0.5)*2, Math.random()*2, (Math.random()-0.5)*2));
scene.add(p); particles.push({mesh: p, vel: new THREE.Vector3((Math.random()-0.5)*0.5, Math.random()*0.5+0.2, (Math.random()-0.5)*0.5), life: 2});
}
}
// === ゲーム制御 ===
let lastEnvUpdate = 0;
function startGame(id) {
state.charId = id; state.isPlaying = true;
state.day = 1; state.gameTime = 12; state.isGameOver = false;
state.stats = { hp: 100, food: 100, water: 100, energy: 100 };
state.inventory = { food: 2, water: 2, wood: 0 };
const info = MONSTERS[id];
document.getElementById('monster-name').innerText = info.name + " City Destruction";
document.getElementById('monster-name').className = "text-xl font-bold " + info.uiClass;
document.getElementById('action-beam-name').innerText = info.beamName + " (撃ち放題)";
document.getElementById('action-beam-name').className = info.uiClass + " font-bold";
document.getElementById('start-screen').classList.add('opacity-0', 'pointer-events-none');
setTimeout(() => document.getElementById('start-screen').style.display = 'none', 500);
document.getElementById('ui-layer').style.display = 'flex';
document.getElementById('game-over-modal').classList.add('hidden');
document.getElementById('log-container').innerHTML = '';
buildMonster(id);
// 環境の初期化
buildings.forEach(b => scene.remove(b.group)); buildings.length = 0; activeBuildings.length = 0;
generatedChunks.clear();
enemies.tanks.forEach(t => scene.remove(t.mesh)); enemies.tanks.length = 0;
items.forEach(i => scene.remove(i.mesh)); items.length = 0;
updateEndlessWorld(true);
lastEnvUpdate = 0;
// カメラ位置を初期化
const isFly = MONSTERS[id].type === 'fly';
camera.position.set(0, isFly ? 25 : 15, isFly ? 35 : 35);
camera.lookAt(0, 0, 0);
log(`${info.name}が街に現れた。全てを破壊しろ!`, true);
updateUI();
}
function returnToHome() {
state.isPlaying = false;
state.isGameOver = false;
document.getElementById('ui-layer').style.display = 'none';
document.getElementById('game-over-modal').classList.add('hidden');
const startScreen = document.getElementById('start-screen');
startScreen.style.display = 'flex';
setTimeout(() => { startScreen.classList.remove('opacity-0', 'pointer-events-none'); }, 10);
while(player.children.length > 0) player.remove(player.children[0]);
}
function log(msg, isUrgent = false) {
const container = document.getElementById('log-container'); const el = document.createElement('div');
const info = state.isPlaying ? MONSTERS[state.charId] : null;
el.innerHTML = `<span class="${info ? info.uiClass : 'text-white'}">[Day ${state.day} ${Math.floor(state.gameTime)}:00]</span> <span class="${isUrgent?'text-red-400 font-bold':''}">${msg}</span>`;
container.prepend(el); if(container.children.length>30) container.removeChild(container.lastChild);
}
function modifyStat(stat, amount) {
// 無敵モード:減少を無効化
if (amount < 0) return;
state.stats[stat] = Math.max(0, Math.min(100, state.stats[stat] + amount));
document.getElementById(`${stat}-text`).innerText = Math.floor(state.stats[stat]);
}
function updateUI() {
document.getElementById('day-counter').innerText = state.day;
document.getElementById('time-display').innerText = `${Math.floor(state.gameTime).toString().padStart(2,'0')}:${Math.floor((state.gameTime%1)*60).toString().padStart(2,'0')}`;
document.getElementById('inv-food').innerText = state.inventory.food;
document.getElementById('inv-water').innerText = state.inventory.water;
}
function gameAction(action) {
if (!state.isPlaying || state.isGameOver) return;
if (action === 'eat') {
if (state.inventory.food > 0 && (state.stats.food < 100 || state.stats.hp < 100)) {
state.inventory.food--; modifyStat('food', 40); modifyStat('hp', 20); log("🍖 捕食して回復した!(満腹度+40, HP+20)");
} else log("⚠️ 食料がない、または満タンだ!", true);
} else if (action === 'drink') {
if (state.inventory.water > 0 && state.stats.water < 100) {
state.inventory.water--; modifyStat('water', 40); log("💧 水分を補給した!(水分+40)");
} else log("⚠️ 水がない、または満タンだ!", true);
} else if (action === 'sleep') {
log("⛺ 休眠に入った...");
state.gameTime = 6; state.day++; modifyStat('energy', 100);
if (Math.random() < 0.2) { log("💥 夜間攻撃を受けたが、傷一つない!", true); } else log("体力を温存し朝を迎えた。");
updateUI();
} else if (action === 'beam') {
if (state.cooldowns.beam > 0) return;
const info = MONSTERS[state.charId];
state.cooldowns.beam = 1.0; // 無敵モードなのでクールダウンを短縮、スタミナ消費なし
log(`🔥 ${info.beamName}を発射した!`);
// ビーム発射時に体全体を青白く照らす効果
matBody.emissiveIntensity = 2.0;
const beamLength = 80; const beamRadius = 3.0;
if(playerMeshes.heads) {
playerMeshes.heads.forEach((headObj) => {
const startPos = new THREE.Vector3(); headObj.mouth.getWorldPosition(startPos);
const dir = new THREE.Vector3(Math.sin(player.rotation.y), 0, Math.cos(player.rotation.y)).normalize();
startPos.add(dir.clone().multiplyScalar(2.0));
let beamGeo;
if(state.charId==='gyaos') beamGeo = new THREE.BoxGeometry(8, 0.2, beamLength);
else beamGeo = new THREE.CylinderGeometry(1.5, 3.0, beamLength, 16);
if(state.charId!=='gyaos') beamGeo.rotateX(Math.PI/2);
const beam = new THREE.Mesh(beamGeo, new THREE.MeshBasicMaterial({ color: info.beamColor, transparent: true, blending: THREE.AdditiveBlending }));
beam.position.copy(startPos.clone().add(dir.clone().multiplyScalar(beamLength/2)));
beam.lookAt(startPos.clone().add(dir.clone().multiplyScalar(beamLength)));
scene.add(beam); enemies.beams.push({ mesh: beam, life: 0.6 });
const checkHit = (arr, name) => {
for (let i = arr.length-1; i>=0; i--) {
const obj = arr[i]; const pos = (obj.group||obj.mesh).position;
const t = new THREE.Vector3().subVectors(pos, startPos).dot(dir);
if (t>0 && t<beamLength && new THREE.Vector3().subVectors(pos, startPos).sub(dir.clone().multiplyScalar(t)).length() < obj.radius+beamRadius) {
spawnParticles(pos, 30); scene.remove(obj.group||obj.mesh); arr.splice(i, 1);
if (name === 'ビル') {
const idx = activeBuildings.indexOf(obj.mat); if(idx>-1) activeBuildings.splice(idx,1);
generatedChunks.delete(obj.chunkKey);
spawnItem('rubble', pos.x, pos.z);
}
log(`💥 ${name}を粉砕した!`);
}
}
};
checkHit(buildings, 'ビル'); checkHit(enemies.tanks, '戦車');
});
}
updateUI();
}
}
const keys = { w:false, a:false, s:false, d:false, ArrowUp:false, ArrowDown:false, ArrowLeft:false, ArrowRight:false, Shift:false };
window.addEventListener('keydown', (e) => {
if (keys.hasOwnProperty(e.key)) keys[e.key] = true;
if (e.code === 'Space') { e.preventDefault(); gameAction('beam'); }
if (e.key === '2') gameAction('eat'); if (e.key === '3') gameAction('drink'); if (e.key === '5') gameAction('sleep');
});
window.addEventListener('keyup', (e) => { if (keys.hasOwnProperty(e.key)) keys[e.key] = false; });
window.addEventListener('resize', () => { camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });
// --- モバイルタッチ操作処理 ---
const touchInputs = { dx: 0, dz: 0 };
const joystickZone = document.getElementById('joystick-zone');
const joystickKnob = document.getElementById('joystick-knob');
let isDragging = false;
let joyCenter = { x: 0, y: 0 };
joystickZone.addEventListener('touchstart', (e) => {
isDragging = true;
const rect = joystickZone.getBoundingClientRect();
joyCenter = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
updateJoystick(e.touches[0]);
}, { passive: false });
joystickZone.addEventListener('touchmove', (e) => {
if (!isDragging) return;
e.preventDefault(); // スクロール防止
updateJoystick(e.touches[0]);
}, { passive: false });
joystickZone.addEventListener('touchend', () => {
isDragging = false;
touchInputs.dx = 0; touchInputs.dz = 0;
joystickKnob.style.transform = `translate(0px, 0px)`;
});
function updateJoystick(touch) {
const maxDist = 40; // ノブが動く最大範囲
let dx = touch.clientX - joyCenter.x;
let dy = touch.clientY - joyCenter.y;
const dist = Math.hypot(dx, dy);
if (dist > maxDist) {
dx = (dx / dist) * maxDist;
dy = (dy / dist) * maxDist;
}
joystickKnob.style.transform = `translate(${dx}px, ${dy}px)`;
// ゲーム内の移動量にマッピング (-1.0 ~ 1.0)
touchInputs.dx = dx / maxDist;
touchInputs.dz = dy / maxDist;
}
const clock = new THREE.Clock();
let animationTime = 0;
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
// 背景描画用 (スタート画面時)
if (!state.isPlaying) {
const t = clock.getElapsedTime();
camera.position.set(Math.sin(t*0.1)*60, 30, Math.cos(t*0.1)*60); camera.lookAt(0,0,0);
renderer.render(scene, camera); return;
}
if (state.isGameOver) return;
animationTime += delta;
// 環境の無限更新 (負荷軽減のため0.5秒おき)
if (animationTime - lastEnvUpdate > 0.5) {
updateEndlessWorld();
lastEnvUpdate = animationTime;
}
// パーティクル&ビーム更新
for(let i=particles.length-1; i>=0; i--){
const p = particles[i]; p.mesh.position.add(p.vel); p.vel.y -= 0.98*delta; p.life-=delta; p.mesh.scale.multiplyScalar(0.95);
if(p.life<=0){ scene.remove(p.mesh); particles.splice(i,1); }
}
for(let i=enemies.beams.length-1; i>=0; i--){
const b=enemies.beams[i]; b.life-=delta; b.mesh.material.opacity=b.life*2; b.mesh.scale.setScalar(1+(0.6-b.life)*2);
if(b.life<=0){ scene.remove(b.mesh); enemies.beams.splice(i,1); }
}
// 環境光・時間
state.gameTime += delta * 0.25; if (state.gameTime >= 24) { state.gameTime = 0; state.day++; log("🌙 日が変わった。"); updateUI(); }
let lightIntensity = 1.0, skyColor = new THREE.Color(0x87CEEB), windowGlow = 0;
if (state.gameTime>18 || state.gameTime<6) { lightIntensity=0.1; skyColor.setHex(0x0a0505); windowGlow=1; }
else if (state.gameTime>16) { lightIntensity=0.5; skyColor.setHex(0xff7733); windowGlow=(state.gameTime-16)/2; }
else if (state.gameTime<8) { lightIntensity=0.7; skyColor.setHex(0xffaa88); windowGlow=Math.max(0,1-(state.gameTime-6)/2); }
hemiLight.intensity = lightIntensity*0.6; sunLight.intensity = THREE.MathUtils.lerp(sunLight.intensity, lightIntensity, 0.05);
sky.material.color.lerp(skyColor, 0.05); scene.fog.color.lerp(skyColor, 0.05);
activeBuildings.forEach(m => m.emissiveIntensity = windowGlow*1.5);
const sa = ((state.gameTime-6)/12)*Math.PI;
sunLight.position.set(player.position.x + Math.cos(sa)*50, player.position.y + Math.sin(sa)*50, player.position.z);
sunLight.target.position.copy(player.position);
sunLight.target.updateMatrixWorld();
if(state.cooldowns.beam > 0) {
state.cooldowns.beam -= delta; matBody.emissiveIntensity = Math.max(0, state.cooldowns.beam*1.5);
}
// 移動入力 (キーボード + タッチ入力)
const info = MONSTERS[state.charId];
let isMoving = false, dx=0, dz=0;
const ms = keys.Shift ? info.speed*info.runMult : info.speed;
// キーボード
if(keys.w||keys.ArrowUp){ dz-=ms; isMoving=true; } if(keys.s||keys.ArrowDown){ dz+=ms; isMoving=true; }
if(keys.a||keys.ArrowLeft){ dx-=ms; isMoving=true; } if(keys.d||keys.ArrowRight){ dx+=ms; isMoving=true; }
// タッチジョイスティック
if (Math.abs(touchInputs.dx) > 0.1 || Math.abs(touchInputs.dz) > 0.1) {
// カメラの向きに合わせて移動方向を補正 (ジョイスティックの上=画面奥)
const camDir = new THREE.Vector3();
camera.getWorldDirection(camDir);
camDir.y = 0;
camDir.normalize();
// カメラの右方向
const camRight = new THREE.Vector3().crossVectors(camDir, new THREE.Vector3(0,1,0)).normalize();
// ジョイスティックの入力をワールド座標の移動量に変換
// touchInputs.dzがマイナス(上)のとき、前進(camDir方向)
const moveVec = new THREE.Vector3()
.addScaledVector(camDir, -touchInputs.dz * ms)
.addScaledVector(camRight, -touchInputs.dx * ms);
dx += moveVec.x;
dz += moveVec.z;
isMoving = true;
}
// === 共通アニメーション ===
const t = animationTime; const ws = isMoving ? (keys.Shift?8:4) : 0; const isFly = info.type === 'fly';
if(isFly) {
player.position.y = info.height + Math.sin(t*ws*1.5)*(isMoving?0.5:1.0);
if (playerMeshes.wings) {
playerMeshes.wings.forEach(w => { w.pivot.rotation.z = Math.sin(t*(isMoving?10:4))*0.6*w.side; w.outer.rotation.z = Math.sin(t*(isMoving?10:4)-1)*0.4*w.side; });
}
if (state.charId === 'gamera') {
// ガメラの飛行ポーズ
torso.rotation.x = THREE.MathUtils.lerp(torso.rotation.x, Math.PI / 2 - 0.1, 0.1); // うつ伏せ
if (playerMeshes.arms) {
playerMeshes.arms.forEach((a, i) => {
const side = i === 0 ? 1 : -1;
// 前方に腕を突き出す
a.rotation.set(Math.PI - 0.2, 0, side * 0.4 + Math.sin(t * (isMoving ? 12 : 6)) * 0.05 * side);
});
}
if (playerMeshes.heads && playerMeshes.heads.length > 0) {
playerMeshes.heads[0].group.parent.rotation.x = -Math.PI / 2 + 0.1; // 首を前へ向ける
}
if (playerMeshes.jets) {
playerMeshes.jets.forEach(j => {
j.scale.set(1.0 + Math.random()*0.1, 1.0 + Math.random()*0.3, 1.0 + Math.random()*0.1);
});
}
} else if (state.charId === 'gyaos') {
// ギャオスの飛行(立ち気味で脚を垂らす)
torso.rotation.x = -0.6 + Math.sin(t*2)*0.05;
if (playerMeshes.legs) {
playerMeshes.legs.forEach((l, i) => {
l.thigh.rotation.x = 0.6 + Math.sin(t*2)*0.1;
});
}
} else {
if (playerMeshes.legs) {
playerMeshes.legs.forEach(l => { l.thigh.rotation.x = -0.2+Math.sin(t*2)*0.1; });
}
}
} else {
player.position.y = info.height + Math.sin(t*ws*2)*0.1;
if(playerMeshes.legs) playerMeshes.legs.forEach((l,i) => {
const wp = t*ws + (i===0?0:Math.PI);
if(isMoving){ l.thigh.rotation.x = -0.2+Math.sin(wp)*0.4; l.shin.rotation.x = 0.3+Math.max(0,Math.sin(wp+Math.PI/2))*0.3; }
else { l.thigh.rotation.x = THREE.MathUtils.lerp(l.thigh.rotation.x,-0.2,0.1); l.shin.rotation.x = THREE.MathUtils.lerp(l.shin.rotation.x,0.3,0.1); }
});
if(playerMeshes.arms) playerMeshes.arms.forEach((a,i) => {
if (state.charId === 'gamera' && i === 0) {
// 右腕(バニシング・フィスト)は常に前に構え、少し揺れる
a.rotation.x = isMoving ? 0.2 + Math.sin(t*ws)*0.1 : 0.5 + Math.sin(t*2)*0.05;
} else if (state.charId === 'anguirus') {
// 四足歩行モーション
const wp = t*ws + (i===0?Math.PI:0); // 後脚と互い違いに
a.rotation.x = isMoving ? 0.5 + Math.sin(wp)*0.4 : 0.5;
} else {
a.rotation.x = isMoving ? Math.sin(t*ws+(i===0?Math.PI:0))*0.3 : 0.5;
}
});
}
if(playerMeshes.tails) playerMeshes.tails.forEach((tailGroup, tIdx) => {
tailGroup.segments.forEach((seg, i) => {
seg.rotation.x = seg.userData.baseAngleX + Math.sin(t*2 - i*0.2)*0.02;
seg.rotation.y = Math.cos(t*(isMoving?ws:2) - i*0.2)*0.08 * (tIdx===0?1:-1);
});
});
if(playerMeshes.necks) playerMeshes.necks.forEach((neckGroup, nIdx) => {
neckGroup.segments.forEach((seg, i) => {
seg.rotation.y = Math.sin(t*2 - i*0.3)*0.05 * (nIdx===1?-1:1);
seg.rotation.x = seg.userData.baseAngleX + Math.cos(t*2 - i*0.3)*0.02;
});
});
if(playerMeshes.heads) playerMeshes.heads.forEach((hGroup, i) => {
if(!playerMeshes.necks || playerMeshes.necks.length === 0) {
hGroup.group.rotation.y = Math.cos(t*1.5)*0.05;
}
});
// 敵AIと衝突判定
const pPos = player.position.clone(); pPos.y += isFly ? 0 : 3;
for (let i = enemies.tanks.length-1; i>=0; i--) {
const tank = enemies.tanks[i]; const dist = Math.hypot(pPos.x-tank.x, pPos.z-tank.z);
const tr = Math.atan2(pPos.x-tank.x, pPos.z-tank.z);
let diff = tr - tank.mesh.rotation.y; while(diff<-Math.PI) diff+=Math.PI*2; while(diff>Math.PI) diff-=Math.PI*2;
tank.mesh.rotation.y += diff*2*delta;
if (dist > 30) { tank.mesh.position.x += Math.sin(tank.mesh.rotation.y)*4*delta; tank.mesh.position.z += Math.cos(tank.mesh.rotation.y)*4*delta; }
else {
tank.cooldown -= delta;
if(tank.cooldown<=0){
fireBullet(new THREE.Vector3(0,1.3,1.5).applyMatrix4(tank.mesh.matrixWorld), pPos.clone().add(new THREE.Vector3((Math.random()-0.5)*4,(Math.random()-0.5)*4,(Math.random()-0.5)*4)));
tank.cooldown = 1.5+Math.random(); spawnParticles(new THREE.Vector3(0,1.3,1.5).applyMatrix4(tank.mesh.matrixWorld), 5);
}
}
tank.x = tank.mesh.position.x; tank.z = tank.mesh.position.z;
}
let dmg = 0;
for (let i=enemies.bullets.length-1; i>=0; i--) {
const b = enemies.bullets[i]; b.mesh.position.addScaledVector(b.dir, b.speed*delta); b.life-=delta;
if(b.mesh.position.distanceTo(pPos) < (isFly?6:4)){ dmg+=b.damage; spawnParticles(b.mesh.position,10); scene.remove(b.mesh); enemies.bullets.splice(i,1); continue; }
if(b.mesh.position.y<=0 || b.life<=0){ scene.remove(b.mesh); enemies.bullets.splice(i,1); }
}
if(dmg>0){ log(`💥 攻撃を受けたが、怪獣には全く効かない!`, true); }
if (isMoving) {
const nx = player.position.x+dx, nz = player.position.z+dz;
const r = isFly ? 6 : 2.5; // 当たり判定
const checkDestruct = (arr, name) => {
for(let i=arr.length-1; i>=0; i--){
const obj=arr[i]; if(Math.hypot(nx-obj.x, nz-obj.z) < obj.radius+r){
if(isFly && obj.height && obj.height < info.height - 3) continue; // 高く飛びすぎているとビルに当たらない
spawnParticles((obj.group||obj.mesh).position, 20); scene.remove(obj.group||obj.mesh); arr.splice(i,1);
if(name==='ビル'){
const idx=activeBuildings.indexOf(obj.mat); if(idx>-1)activeBuildings.splice(idx,1);
generatedChunks.delete(obj.chunkKey);
spawnItem('rubble', obj.x, obj.z);
}
log(`💥 ${name}を粉砕した!`);
}
}
};
checkDestruct(buildings, 'ビル'); checkDestruct(enemies.tanks, '戦車');
// 無限移動のため制限を削除
player.position.x=nx;
player.position.z=nz;
if(dx!==0||dz!==0){
let diff = Math.atan2(dx, dz) - player.rotation.y; while(diff<-Math.PI) diff+=Math.PI*2; while(diff>Math.PI) diff-=Math.PI*2;
player.rotation.y += diff*0.1;
}
}
camera.position.lerp(new THREE.Vector3(player.position.x, player.position.y+(isFly?10:15), player.position.z+(isFly?25:35)), 0.1);
camera.lookAt(player.position.x, player.position.y+(isFly?0:5), player.position.z);
for (let i=items.length-1; i>=0; i--) {
const item = items[i];
if(Math.hypot(player.position.x-item.mesh.position.x, player.position.z-item.mesh.position.z) < 4) {
if(item.type==='food'){ state.inventory.food++; log("🌿 食料をゲットした"); }
if(item.type==='water'){ state.inventory.water++; log("💧 水分をゲットした"); }
scene.remove(item.mesh); items.splice(i,1); updateUI();
// アイテムはupdateEndlessWorldで補充されるのでここでは補充しない
}
}
renderer.render(scene, camera);
}
// 初期化用に関数を呼び出す(最初は背景描画のみ)
// startGameが呼ばれるまでは、背景用の初期世界を生成しておく
updateEndlessWorld(true);
animate();
</script>
</body>
</html>
たくさんの怪獣の中から自分で「これがいい」と選ぶことができます