ロボットミニゲーム「Wire Arms」
【更新履歴】
・2026/6/8 バージョン1.0公開。
【あらすじ】
・ロボット同士で腕を飛ばしあって対決するゲームです。
・データ量を増やすことで
相手の予測を上回る行動が取れるようになります。
【操作説明】
・方向キーで自機を移動。
・自機が攻撃可能な時は、
・Zキーで左腕を発射し攻撃。
・Xキーで右腕を発射し攻撃。
・自機がガート可能な時は、
・Zキーで左腕を発射しガード。
・Xキーで右腕を発射しガード。
・回避は自動的に行う。(データ量で負けてると失敗する)
【アイテム】
・水色 … データ量アップ。攻撃、ガード、回避の成功率がアップ。
・黄緑色 … 資源。徐々に回復。
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>WIRE ARMS</title>
<style>
body { margin: 0; overflow: hidden; background-color: #050510; font-family: 'Courier New', monospace; user-select: none; }
canvas { display: block; position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; z-index: -1; transition: filter 0.3s; }
body.blur canvas { filter: blur(6px) brightness(0.6); }
#ui { position: absolute; top: 10px; left: 10px; width: 100%; pointer-events: none; transition: opacity 0.3s; }
.bar-container { display: flex; justify-content: space-between; width: 90%; margin: 0 auto; }
.status-box { background: rgba(0,20,0,0.7); border: 1px solid #0f0; padding: 10px; width: 40%; text-shadow: 0 0 5px #0fff0f; color: #0fff0f; transition: all 0.3s; }
.status-box.enemy { background: rgba(20,0,0,0.7); border-color: #f00; color: #f33; text-shadow: 0 0 5px #f33; text-align: right; }
.regen-active { box-shadow: 0 0 15px #adff2f; border-color: #adff2f !important; }
#log { position: absolute; bottom: 20px; left: 20px; color: #aaa; pointer-events: none; font-size: 14px; text-shadow: 0 0 2px #000; line-height: 1.5; }
.instructions { position: absolute; bottom: 20px; right: 20px; color: #fff; text-align: right; font-size: 14px; background: rgba(0,0,0,0.8); padding: 10px; border: 1px solid #555; pointer-events: none;}
#centerPrompt { position: absolute; top: 35%; width: 100%; text-align: center; font-size: 32px; font-weight: bold; color: #fff; text-shadow: 0 0 10px #000, 0 0 20px #0ff; pointer-events: none; transition: color 0.2s; }
.locked-on { color: #0ff !important; text-shadow: 0 0 20px #0ff, 0 0 40px #00f !important; }
.obstructed { color: #f55 !important; text-shadow: 0 0 10px #f00 !important; font-size: 24px !important; }
h2, p { margin: 0 0 5px 0; }
.screen { display: none; position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,5,15,0.85); flex-direction: column; justify-content: center; align-items: center; z-index: 100; color: #fff; text-align: center; }
.screen.active { display: flex; }
.screen h1 { font-size: 72px; margin-bottom: 20px; letter-spacing: 10px; text-shadow: 0 0 20px #0ff; }
.screen h2 { font-size: 36px; margin-bottom: 20px; color: #0ff; }
.screen p { font-size: 24px; color: #ccc; max-width: 600px; line-height: 1.5; }
.blink { animation: blinker 1.5s linear infinite; }
@keyframes blinker { 50% { opacity: 0; } }
#stageIndicator { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color: #0ff; font-size: 18px; text-shadow: 0 0 10px #0ff; pointer-events: none; opacity: 0; transition: opacity 0.3s; }
#stageIndicator.visible { opacity: 1; }
</style>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
</head>
<body>
<div id="ui" style="opacity: 0;">
<div class="bar-container">
<div class="status-box" id="uiPlayer">
<h2>PLAYER [HP: <span id="hpPlayer">1000</span>] <span id="regenPlayer" style="color:#adff2f; font-size:12px;"></span></h2>
<p>DATA LEVEL: <span id="dataPlayer">0</span></p>
</div>
<div class="status-box enemy" id="uiEnemy">
<h2><span id="regenEnemy" style="color:#adff2f; font-size:12px;"></span> [HP: <span id="hpEnemy">100</span>] ENEMY</h2>
<p>DATA LEVEL: <span id="dataEnemy">0</span></p>
</div>
</div>
</div>
<div id="stageIndicator"></div>
<div id="centerPrompt"></div>
<div id="log"></div>
<div class="instructions" id="instructions" style="opacity: 0;">
[↑][↓][←][→] 機体移動 / [ESC] PAUSE<br><br>
<b>【攻撃】 [Z]キー(左腕) or [X]キー(右腕)</b><br>
通常時: ビット発射 → レーザー照射<br>
敵攻撃時: バリア展開(ガード)<br><br>
<span style="color:#00ffff;">■ 水色アイテム: データ量UP(+100)</span><br>
<span style="color:#adff2f;">■ 黄緑アイテム: HP継続回復(+1/sec)</span>
</div>
<div id="screenTitle" class="screen active">
<h1>WIRE ARMS</h1>
<p class="blink">CLICK TO START</p>
</div>
<div id="screenOpening" class="screen">
<h2>INITIALIZING SYSTEM</h2>
<p>神経接続パス確立...<br>自律戦闘AI、起動準備完了。</p>
</div>
<div id="screenStageStart" class="screen">
<h1 id="stageText">STAGE 1</h1>
<p>敵対プログラムを殲滅せよ</p>
</div>
<div id="screenPause" class="screen">
<h1 style="color:#ffa500; text-shadow: 0 0 20px #ffa500;">PAUSED</h1>
<p class="blink">[ESC]キーで戦闘再開</p>
</div>
<div id="screenStageClear" class="screen">
<h1 style="color:#0f0; text-shadow: 0 0 20px #0f0;">STAGE CLEAR</h1>
<p>システム修復完了。<br>次フェーズへ移行します。</p>
</div>
<div id="screenGameOver" class="screen">
<h1 style="color:#f00; text-shadow: 0 0 20px #f00;">GAME OVER</h1>
<p>機体大破。接続をロストしました。<br><br>画面クリックでシステムを再起動してください。</p>
</div>
<div id="screenGameClear" class="screen">
<h1 style="color:#ff0ff0; text-shadow: 0 0 20px #ff0ff0;">ALL CLEARED</h1>
<p>MISSION ACCOMPLISHED.<br>全脅威の排除を確認しました。<br><br>画面クリックでシステムを再起動してください。</p>
</div>
<script>
// ==========================================
// 0. ゲーム状態管理
// ==========================================
let gameState = 'TITLE';
let currentStage = 1;
const maxStages = 5;
let enemies = [];
function switchState(newState) {
document.querySelectorAll('.screen').forEach(el => el.classList.remove('active'));
gameState = newState;
const ui = document.getElementById('ui');
const inst = document.getElementById('instructions');
const stageInd = document.getElementById('stageIndicator');
const body = document.body;
if (newState === 'PLAYING') {
ui.style.opacity = '1'; inst.style.opacity = '1';
stageInd.classList.add('visible');
body.classList.remove('blur');
} else {
ui.style.opacity = '0'; inst.style.opacity = '0';
stageInd.classList.remove('visible');
body.classList.add('blur');
document.getElementById('centerPrompt').innerText = "";
}
if (newState === 'TITLE') document.getElementById('screenTitle').classList.add('active');
else if (newState === 'OPENING') document.getElementById('screenOpening').classList.add('active');
else if (newState === 'STAGE_START') {
document.getElementById('stageText').innerText = "STAGE " + currentStage;
document.getElementById('screenStageStart').classList.add('active');
setTimeout(() => {
initStage();
switchState('PLAYING');
}, 2000);
}
else if (newState === 'PAUSE') document.getElementById('screenPause').classList.add('active');
else if (newState === 'STAGE_CLEAR') document.getElementById('screenStageClear').classList.add('active');
else if (newState === 'GAME_OVER') document.getElementById('screenGameOver').classList.add('active');
else if (newState === 'GAME_CLEAR') document.getElementById('screenGameClear').classList.add('active');
}
window.addEventListener('click', () => {
window.focus();
if(audioCtx.state === 'suspended') audioCtx.resume();
if (gameState === 'TITLE') {
switchState('OPENING');
setTimeout(() => switchState('STAGE_START'), 2500);
} else if (gameState === 'GAME_OVER' || gameState === 'GAME_CLEAR') {
currentStage = 1;
switchState('TITLE');
}
});
// ==========================================
// 1. サウンドシステム
// ==========================================
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();
function playSound(type) {
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if(type === 'attack') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(300, now); osc.frequency.exponentialRampToValueAtTime(80, now + 0.15);
gain.gain.setValueAtTime(0.25, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
osc.start(now); osc.stop(now + 0.15);
} else if(type === 'laser') {
osc.type = 'sine'; osc.frequency.setValueAtTime(1200, now); osc.frequency.linearRampToValueAtTime(400, now + 0.25);
gain.gain.setValueAtTime(0.3, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
osc.start(now); osc.stop(now + 0.25);
} else if(type === 'guard') {
osc.type = 'square'; osc.frequency.setValueAtTime(400, now); osc.frequency.linearRampToValueAtTime(600, now + 0.1);
gain.gain.setValueAtTime(0.3, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start(now); osc.stop(now + 0.1);
} else if(type === 'dodge') {
osc.type = 'sine'; osc.frequency.setValueAtTime(800, now); osc.frequency.exponentialRampToValueAtTime(300, now + 0.3);
gain.gain.setValueAtTime(0.3, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now); osc.stop(now + 0.3);
} else if(type === 'hit') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(100, now); osc.frequency.exponentialRampToValueAtTime(20, now + 0.3);
gain.gain.setValueAtTime(0.5, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now); osc.stop(now + 0.3);
}
}
// ==========================================
// 2. UIシステム
// ==========================================
const game = {
messages: ["SYSTEM: 起動待機中..."],
addMessage(msg) {
this.messages.push(msg);
if(this.messages.length > 6) this.messages.shift();
document.getElementById('log').innerHTML = this.messages.join('<br>');
},
updateUI() {
document.getElementById('hpPlayer').innerText = Math.floor(player.hp);
const firstAliveEnemy = enemies.find(e => e.hp > 0);
document.getElementById('hpEnemy').innerText = firstAliveEnemy ? Math.floor(firstAliveEnemy.hp) : 0;
document.getElementById('dataPlayer').innerText = player.data;
document.getElementById('dataEnemy').innerText = firstAliveEnemy ? firstAliveEnemy.data : 0;
document.getElementById('regenPlayer').innerText = player.regenTimer > 0 ? "(Regen)" : "";
document.getElementById('regenEnemy').innerText = (firstAliveEnemy && firstAliveEnemy.regenTimer > 0) ? "(Regen)" : "";
document.getElementById('uiPlayer').classList.toggle('regen-active', player.regenTimer > 0);
document.getElementById('uiEnemy').classList.toggle('regen-active', !!(firstAliveEnemy && firstAliveEnemy.regenTimer > 0));
document.getElementById('stageIndicator').innerText = `STAGE ${currentStage} / ${maxStages} ENEMIES: ${enemies.filter(e=>e.hp>0).length}`;
}
};
const floatingTexts = [];
function createFloatingText(text, pos, color, size = "24px") {
const div = document.createElement('div');
div.innerText = text;
div.style.position = 'absolute';
div.style.color = color;
div.style.fontWeight = 'bold';
div.style.textShadow = '0 0 5px #000';
div.style.pointerEvents = 'none';
div.style.fontSize = size;
document.body.appendChild(div);
floatingTexts.push({ el: div, pos: pos.clone(), life: 1.5 });
}
function updateFloatingTexts(dt) {
for (let i = floatingTexts.length - 1; i >= 0; i--) {
let ft = floatingTexts[i];
ft.life -= dt;
if (ft.life <= 0) { ft.el.remove(); floatingTexts.splice(i, 1); continue; }
ft.pos.y += dt * 10;
let proj = ft.pos.clone().project(camera);
let x = (proj.x * 0.5 + 0.5) * window.innerWidth;
let y = (proj.y * -0.5 + 0.5) * window.innerHeight;
ft.el.style.left = `calc(${x}px - 20px)`;
ft.el.style.top = `${y}px`;
ft.el.style.opacity = Math.min(1.0, ft.life);
}
}
// ==========================================
// 3. Three.js セットアップ & 障害物生成
// ==========================================
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050510, 0.012);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 50, 60);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x050510, 1.0);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
const ambientLight = new THREE.AmbientLight(0x404040, 2.0);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(20, 50, 20);
scene.add(dirLight);
const gridHelper = new THREE.GridHelper(100, 25, 0x004444, 0x001111);
scene.add(gridHelper);
const blocks = [];
const blockGeo = new THREE.BoxGeometry(8, 10, 8);
const blockMat = new THREE.MeshStandardMaterial({ color: 0x224455, roughness: 0.8, metalness: 0.5 });
const fragments = [];
function generateBlocks() {
blocks.forEach(b => scene.remove(b));
blocks.length = 0;
for (let x = -40; x <= 40; x += 20) {
for (let z = -40; z <= 40; z += 20) {
if (Math.abs(x) < 15 && Math.abs(z) < 30) continue;
if (Math.random() < 0.4) continue;
let mesh = new THREE.Mesh(blockGeo, blockMat);
mesh.position.set(x, 5, z);
scene.add(mesh);
blocks.push(mesh);
}
}
}
function checkCollision(pos, radius) {
for (let b of blocks) {
let dx = Math.max(b.position.x - 4, Math.min(pos.x, b.position.x + 4)) - pos.x;
let dz = Math.max(b.position.z - 4, Math.min(pos.z, b.position.z + 4)) - pos.z;
if (dx * dx + dz * dz < radius * radius) return true;
}
return false;
}
function checkLineClear(p1, p2, radius) {
let dist = p1.distanceTo(p2);
let steps = Math.ceil(dist / 2);
let dir = new THREE.Vector3().subVectors(p2, p1).normalize();
let p = new THREE.Vector3();
for(let i=1; i<=steps; i++) {
p.copy(p1).add(dir.clone().multiplyScalar((i/steps)*dist));
if (checkCollision(p, radius)) return false;
}
return true;
}
// ==========================================
// 4. A* アルゴリズム (ビット軌道用)
// ==========================================
const PATH_GRID_SIZE = 4;
const PATH_GRID_W = 25;
function worldToGrid(x, z) {
return { c: Math.floor((x + 50) / PATH_GRID_SIZE), r: Math.floor((z + 50) / PATH_GRID_SIZE) };
}
function gridToWorld(c, r) {
return new THREE.Vector3(c * PATH_GRID_SIZE - 50 + PATH_GRID_SIZE/2, 1, r * PATH_GRID_SIZE - 50 + PATH_GRID_SIZE/2);
}
function findPath(startPos, endPos, radius) {
let start = worldToGrid(startPos.x, startPos.z);
let end = worldToGrid(endPos.x, endPos.z);
start.c = Math.max(0, Math.min(PATH_GRID_W-1, start.c)); start.r = Math.max(0, Math.min(PATH_GRID_W-1, start.r));
end.c = Math.max(0, Math.min(PATH_GRID_W-1, end.c)); end.r = Math.max(0, Math.min(PATH_GRID_W-1, end.r));
let open = [{c: start.c, r: start.r, g: 0, h: Math.hypot(start.c-end.c, start.r-end.r), parent: null}];
let closed = new Set();
let getKey = (c, r) => `${c},${r}`;
while(open.length > 0) {
open.sort((a,b) => (a.g + a.h) - (b.g + b.h));
let current = open.shift();
if (current.c === end.c && current.r === end.r) {
let path = [];
let curr = current;
while(curr) {
path.push(gridToWorld(curr.c, curr.r));
curr = curr.parent;
}
path.reverse();
path[0].copy(startPos);
path[path.length-1].copy(endPos);
return path;
}
closed.add(getKey(current.c, current.r));
const dirs = [[0,1], [1,0], [0,-1], [-1,0], [1,1], [1,-1], [-1,1], [-1,-1]];
for(let d of dirs) {
let nc = current.c + d[0], nr = current.r + d[1];
if (nc < 0 || nc >= PATH_GRID_W || nr < 0 || nr >= PATH_GRID_W) continue;
if (closed.has(getKey(nc, nr))) continue;
if (checkCollision(gridToWorld(nc, nr), radius)) {
if (nc !== end.c || nr !== end.r) continue;
}
let moveCost = (d[0] !== 0 && d[1] !== 0) ? 1.414 : 1;
let g = current.g + moveCost;
let existing = open.find(n => n.c === nc && n.r === nr);
if (existing) {
if (g < existing.g) { existing.g = g; existing.parent = current; }
} else {
open.push({c: nc, r: nr, g: g, h: Math.hypot(nc-end.c, nr-end.r), parent: current});
}
}
}
return null;
}
// ==========================================
// 5. パーティクルシステム
// ==========================================
const particles = [];
function createHitParticles(pos, color) {
const count = 18;
const geo = new THREE.SphereGeometry(0.25, 4, 4);
for (let i = 0; i < count; i++) {
const mat = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
scene.add(mesh);
const vel = new THREE.Vector3((Math.random() - 0.5) * 20, Math.random() * 15 + 5, (Math.random() - 0.5) * 20);
particles.push({ mesh, vel, life: 0.6 + Math.random() * 0.4, maxLife: 1.0 });
}
}
function createDodgeParticles(pos, color) {
const count = 8;
const geo = new THREE.OctahedronGeometry(0.4);
for (let i = 0; i < count; i++) {
const mat = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
scene.add(mesh);
const angle = (i / count) * Math.PI * 2;
const vel = new THREE.Vector3(Math.cos(angle) * 8, 3, Math.sin(angle) * 8);
particles.push({ mesh, vel, life: 0.4, maxLife: 0.4 });
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) {
scene.remove(p.mesh);
particles.splice(i, 1);
continue;
}
p.vel.y -= 20 * dt;
p.mesh.position.addScaledVector(p.vel, dt);
p.mesh.rotation.x += dt * 5;
p.mesh.rotation.z += dt * 3;
const alpha = p.life / p.maxLife;
p.mesh.material.opacity = alpha;
p.mesh.material.transparent = true;
}
}
// ==========================================
// 6. 残像(アフターイメージ)システム
// ==========================================
const afterImages = [];
function createAfterImage(robotGroup, color) {
const cloneGroup = new THREE.Group();
robotGroup.traverse(child => {
if (child.isMesh) {
const clone = new THREE.Mesh(
child.geometry,
new THREE.MeshBasicMaterial({ color: color, wireframe: true, transparent: true, opacity: 0.55 })
);
clone.position.copy(child.getWorldPosition(new THREE.Vector3()));
clone.quaternion.copy(child.getWorldQuaternion(new THREE.Quaternion()));
clone.scale.copy(child.getWorldScale(new THREE.Vector3()));
cloneGroup.add(clone);
}
});
scene.add(cloneGroup);
afterImages.push({ group: cloneGroup, life: 0.35, maxLife: 0.35 });
}
function updateAfterImages(dt) {
for (let i = afterImages.length - 1; i >= 0; i--) {
const ai = afterImages[i];
ai.life -= dt;
if (ai.life <= 0) {
scene.remove(ai.group);
afterImages.splice(i, 1);
continue;
}
const alpha = (ai.life / ai.maxLife) * 0.55;
ai.group.traverse(child => {
if (child.isMesh) child.material.opacity = alpha;
});
}
}
// ==========================================
// 7. ビット(飛翔体)システム
// ==========================================
const bits = [];
function getActiveBitCount(entity) {
return bits.filter(b => b.owner === entity).length;
}
class Bit {
constructor(startPos, path, color, owner, target, predLine) {
this.owner = owner;
this.target = target;
this.path = path;
this.pathIndex = 0;
this.speed = 40;
this.pos = startPos.clone();
this.color = color;
this.bitState = 'OUTBOUND';
this.laserTimer = 0;
this.laserLine = null;
this.life = 15.0;
const geo = new THREE.OctahedronGeometry(0.7, 0);
const mat = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
this.mesh = new THREE.Mesh(geo, mat);
this.mesh.position.copy(this.pos);
scene.add(this.mesh);
const ringGeo = new THREE.TorusGeometry(1.2, 0.1, 4, 8);
const ringMat = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
this.ring = new THREE.Mesh(ringGeo, ringMat);
this.mesh.add(this.ring);
this.light = new THREE.PointLight(color, 2, 8);
this.mesh.add(this.light);
this.trajectoryLine = predLine;
if (!this.trajectoryLine) {
const points = this.path.map(p => p.clone().setY(5));
const lineGeo = new THREE.BufferGeometry().setFromPoints(points);
const lineMat = new THREE.LineDashedMaterial({ color: color, dashSize: 1, gapSize: 0.5, transparent: true, opacity: 0.8 });
this.trajectoryLine = new THREE.Line(lineGeo, lineMat);
this.trajectoryLine.computeLineDistances();
scene.add(this.trajectoryLine);
}
}
update(dt) {
this.life -= dt;
this.mesh.rotation.y += dt * 4;
this.ring.rotation.x += dt * 6;
if (this.bitState === 'OUTBOUND') {
if (this.path && this.pathIndex < this.path.length - 1) {
const targetPos = this.path[this.pathIndex + 1].clone().setY(5);
const dir = new THREE.Vector3().subVectors(targetPos, this.pos).normalize();
const moveDist = this.speed * dt;
const remaining = this.pos.distanceTo(targetPos);
if (remaining < moveDist) {
this.pos.copy(targetPos);
this.pathIndex++;
} else {
this.pos.addScaledVector(dir, moveDist);
}
this.mesh.position.copy(this.pos);
if (this.target && this.pos.distanceTo(this.target.pos) < 5) {
this.fireLaser();
}
} else {
this.fireLaser();
}
} else if (this.bitState === 'FIRING') {
this.laserTimer -= dt;
if (this.laserTimer <= 0) {
if (this.laserLine && this.laserLine.parent) scene.remove(this.laserLine);
this.laserLine = null;
this.mesh.visible = true;
this.bitState = 'RETURN';
if (this.trajectoryLine && this.trajectoryLine.parent) scene.remove(this.trajectoryLine);
this.trajectoryLine = null;
}
} else if (this.bitState === 'RETURN') {
const targetPos = this.owner.pos.clone().setY(5);
const dir = new THREE.Vector3().subVectors(targetPos, this.pos).normalize();
const moveDist = this.speed * 1.5 * dt;
const remaining = this.pos.distanceTo(targetPos);
if (remaining < moveDist || remaining < 2.0) {
this.destroy();
return true;
} else {
this.pos.addScaledVector(dir, moveDist);
}
this.mesh.position.copy(this.pos);
}
return this.life <= 0;
}
fireLaser() {
if (this.bitState === 'FIRING') return;
this.bitState = 'FIRING';
playSound('laser');
if (this.trajectoryLine && this.trajectoryLine.parent) scene.remove(this.trajectoryLine);
this.trajectoryLine = null;
if (!this.target || this.target.hp <= 0) {
this.laserTimer = 0.1;
return;
}
const points = [this.pos.clone(), this.target.pos.clone().setY(3)];
const laserGeo = new THREE.BufferGeometry().setFromPoints(points);
const laserMat = new THREE.LineBasicMaterial({ color: this.color, linewidth: 3 });
this.laserLine = new THREE.Line(laserGeo, laserMat);
scene.add(this.laserLine);
this.laserTimer = 0.3;
this.owner.resolveAttack(this.target);
createHitParticles(this.target.pos.clone().setY(3), this.color);
this.mesh.visible = false;
}
destroy() {
if (this.mesh.parent) scene.remove(this.mesh);
if (this.laserLine && this.laserLine.parent) scene.remove(this.laserLine);
if (this.trajectoryLine && this.trajectoryLine.parent) scene.remove(this.trajectoryLine);
this.life = 0;
}
}
function updateBits(dt) {
for (let i = bits.length - 1; i >= 0; i--) {
const done = bits[i].update(dt);
if (done) {
bits[i].destroy();
bits.splice(i, 1);
}
}
}
// ==========================================
// 8. ワイヤーフレーム人型ロボット生成
// ==========================================
function createWireframeRobot(color) {
const group = new THREE.Group();
const mat = () => new THREE.MeshBasicMaterial({ color: color, wireframe: true });
const torso = new THREE.Mesh(new THREE.BoxGeometry(3, 4, 2), mat());
torso.position.set(0, 5, 0);
group.add(torso);
const head = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.8, 1.8), mat());
head.position.set(0, 7.8, 0);
group.add(head);
const visor = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.4, 0.3), mat());
visor.position.set(0, 7.9, 0.9);
group.add(visor);
const lUpperArm = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 2, 6), mat());
lUpperArm.position.set(-2.5, 5.5, 0);
group.add(lUpperArm);
const lForeArm = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.28, 1.8, 6), mat());
lForeArm.position.set(-3.5, 4.0, 0);
group.add(lForeArm);
const lHand = new THREE.Mesh(new THREE.OctahedronGeometry(0.6, 0), mat());
lHand.position.set(-4.3, 3.0, 0);
group.add(lHand);
const rUpperArm = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 2, 6), mat());
rUpperArm.position.set(2.5, 5.5, 0);
group.add(rUpperArm);
const rForeArm = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.28, 1.8, 6), mat());
rForeArm.position.set(3.5, 4.0, 0);
group.add(rForeArm);
const rHand = new THREE.Mesh(new THREE.OctahedronGeometry(0.6, 0), mat());
rHand.position.set(4.3, 3.0, 0);
group.add(rHand);
const lThigh = new THREE.Mesh(new THREE.CylinderGeometry(0.45, 0.38, 2.2, 6), mat());
lThigh.position.set(-1.0, 1.8, 0);
group.add(lThigh);
const lShin = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.28, 2.0, 6), mat());
lShin.position.set(-1.1, -0.3, 0.1);
group.add(lShin);
const lFoot = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.4, 1.4), mat());
lFoot.position.set(-1.1, -1.6, 0.3);
group.add(lFoot);
const rThigh = new THREE.Mesh(new THREE.CylinderGeometry(0.45, 0.38, 2.2, 6), mat());
rThigh.position.set(1.0, 1.8, 0);
group.add(rThigh);
const rShin = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.28, 2.0, 6), mat());
rShin.position.set(1.1, -0.3, 0.1);
group.add(rShin);
const rFoot = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.4, 1.4), mat());
rFoot.position.set(1.1, -1.6, 0.3);
group.add(rFoot);
const lShoulder = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.7, 1.0), mat());
lShoulder.position.set(-2.1, 6.6, 0);
group.add(lShoulder);
const rShoulder = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.7, 1.0), mat());
rShoulder.position.set(2.1, 6.6, 0);
group.add(rShoulder);
const antenna = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 1.5, 4), mat());
antenna.position.set(0.5, 9.3, 0);
group.add(antenna);
const backpack = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 0.8), mat());
backpack.position.set(0, 5.5, -1.5);
group.add(backpack);
const thrL = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.45, 0.8, 6), mat());
thrL.position.set(-0.7, 5.0, -2.0);
group.add(thrL);
const thrR = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.45, 0.8, 6), mat());
thrR.position.set(0.7, 5.0, -2.0);
group.add(thrR);
scene.add(group);
return group;
}
// ==========================================
// 9. シールドメッシュ生成
// ==========================================
function createShieldMesh() {
const geo = new THREE.SphereGeometry(5.0, 12, 12);
const mat = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.25, wireframe: true });
const mesh = new THREE.Mesh(geo, mat);
mesh.visible = false;
scene.add(mesh);
return mesh;
}
// ==========================================
// 10. Entityクラス
// ==========================================
class Entity {
constructor(color, x, z, isPlayer) {
this.isPlayer = isPlayer;
this.color = color;
this.pos = new THREE.Vector3(x, 1, z);
this.lastPos = this.pos.clone();
this.radius = 2.5;
this.maxHp = isPlayer ? 1000 : 100;
this.hp = this.maxHp;
this.data = 0;
this.regenTimer = 0;
this.robotGroup = createWireframeRobot(color);
this.robotGroup.position.copy(this.pos);
this.shieldMesh = createShieldMesh();
this.facing = new THREE.Vector3(0, 0, isPlayer ? -1 : 1);
this.targetFacing = this.facing.clone();
this.state = 'idle';
this.timer = 0;
this.activeArm = null;
this.currentAttackPath = null;
this.dodgeTargetPos = null;
this.predLine = null;
this.afterImageTimer = 0;
this.afterImageInterval = 0.04;
this.walkCycle = 0;
this.aiActionTimer = isPlayer ? 0 : 1.0;
this.moveTarget = new THREE.Vector3(x, 1, z);
}
get dataLevel() { return Math.floor(this.data / 100); }
isDataSuperior(opponent) { return this.data > opponent.data; }
buildUnpredictablePath(fromPos, toPos) {
const mid = new THREE.Vector3().lerpVectors(fromPos, toPos, 0.5);
const offset = new THREE.Vector3((Math.random() - 0.5) * 20, 0, (Math.random() - 0.5) * 20);
mid.add(offset);
mid.x = Math.max(-44, Math.min(44, mid.x));
mid.z = Math.max(-44, Math.min(44, mid.z));
return [fromPos.clone(), mid, toPos.clone()];
}
attemptAutoDodge(attacker) {
if (this.state !== 'idle') return;
let evadeChance;
if (this.isDataSuperior(attacker)) {
const dataGap = (this.data - attacker.data) / 100;
evadeChance = Math.min(0.90, 0.50 + dataGap * 0.08);
} else {
evadeChance = 0.05 + ((this.data / 100) * 0.05) - ((attacker.data / 100) * 0.02);
evadeChance = Math.max(0.03, Math.min(0.40, evadeChance));
}
if (Math.random() > evadeChance) return;
let safeNodes = [];
const dirs = [[0,1],[1,0],[0,-1],[-1,0],[1,1],[-1,-1],[1,-1],[-1,1]];
for (let d of dirs) {
let target = this.pos.clone().add(new THREE.Vector3(d[0]*15, 0, d[1]*15));
target.x = Math.max(-45, Math.min(45, target.x));
target.z = Math.max(-45, Math.min(45, target.z));
if (!checkCollision(target, this.radius)) {
safeNodes.push(target);
}
}
safeNodes.sort((a, b) => b.distanceToSq(attacker.pos) - a.distanceToSq(attacker.pos));
if (safeNodes.length > 0) {
this.state = 'dodging';
this.dodgeTargetPos = safeNodes[0];
this.timer = 0.25;
createFloatingText(this.isDataSuperior(attacker) ? "予測回避!" : "回避!", this.pos, "#fff");
playSound('dodge');
createDodgeParticles(this.pos.clone().add(new THREE.Vector3(0,3,0)), this.color);
}
}
fireBit(targetEntity, preCalcPath) {
if (!targetEntity || targetEntity.hp <= 0) return;
const startPos = this.pos.clone().add(new THREE.Vector3(0, 5, 0));
let path = preCalcPath;
if (!path) {
if (this.isDataSuperior(targetEntity)) {
path = this.buildUnpredictablePath(startPos, targetEntity.pos);
} else {
path = findPath(this.pos, targetEntity.pos, 1.0);
if (!path) path = [startPos, targetEntity.pos.clone()];
}
}
const bit = new Bit(startPos, path, this.color, this, targetEntity, this.predLine);
this.predLine = null;
bits.push(bit);
playSound('attack');
}
update(dt, opponents) {
if (this.regenTimer > 0) {
this.regenTimer -= dt;
if (Math.random() < dt * 1.0) { this.hp = Math.min(this.maxHp, this.hp + 1); }
}
if (this.targetFacing.lengthSq() > 0.001) {
this.facing.lerp(this.targetFacing, 0.12);
this.facing.y = 0;
this.facing.normalize();
}
const angle = Math.atan2(this.facing.x, this.facing.z);
this.robotGroup.rotation.y = angle;
this.robotGroup.position.copy(this.pos);
this.shieldMesh.position.copy(this.pos).add(new THREE.Vector3(0, 4, 0));
this.shieldMesh.visible = (this.state === 'guarding');
if (this.state === 'guarding') {
this.shieldMesh.rotation.y += dt * 3;
}
if (this.state === 'dodging' || this.state === 'windup' || this.state === 'guarding') {
this.afterImageTimer -= dt;
if (this.afterImageTimer <= 0) {
createAfterImage(this.robotGroup, this.color);
this.afterImageTimer = this.afterImageInterval;
}
} else {
this.afterImageTimer = 0;
}
const movedDist = this.pos.distanceTo(this.lastPos);
const actualSpeed = dt > 0 ? movedDist / dt : 0;
this.lastPos.copy(this.pos);
this.animateRobot(dt, actualSpeed);
if (this.state === 'windup') {
this.timer -= dt;
if (this.timer <= 0) {
this.state = 'attacking';
this.timer = 0.3;
const target = this.getNearestOpponent(opponents);
this.fireBit(target, this.currentAttackPath);
}
} else if (this.state === 'attacking') {
this.timer -= dt;
if (this.timer <= 0) this.state = 'idle';
} else if (this.state === 'guarding') {
this.timer -= dt;
if (this.timer <= 0) this.state = 'idle';
} else if (this.state === 'dodging') {
this.timer -= dt;
let moveRatio = dt / Math.max(0.01, this.timer + dt);
const prevPos = this.pos.clone();
this.pos.lerp(this.dodgeTargetPos, moveRatio * 5);
const moved = new THREE.Vector3().subVectors(this.pos, prevPos);
if (moved.lengthSq() > 0.001) {
this.targetFacing.copy(moved).normalize();
}
if (this.timer <= 0) this.state = 'idle';
}
}
getNearestOpponent(opponents) {
if (!opponents || opponents.length === 0) return null;
let nearest = null, minDist = Infinity;
for (const op of opponents) {
if (op.hp <= 0) continue;
const d = this.pos.distanceTo(op.pos);
if (d < minDist) { minDist = d; nearest = op; }
}
return nearest;
}
resolveAttack(defender) {
if (!defender || defender.hp <= 0) return;
let isHit = true;
if (defender.state === 'dodging') {
isHit = false;
defender.data += 50;
createFloatingText("EVADE!", defender.pos.clone().add(new THREE.Vector3(0,4,0)), "#ffffff");
} else if (defender.state === 'guarding') {
let guardChance = 0.5 + ((defender.data / 100) * 0.1) - ((this.data / 100) * 0.05);
guardChance = Math.max(0.1, Math.min(1.0, guardChance));
if (Math.random() < guardChance) {
isHit = false;
defender.data += 100;
createFloatingText("GUARD!", defender.pos.clone().add(new THREE.Vector3(0,4,0)), "#00ffff");
} else {
createFloatingText("GUARD BREAK!", defender.pos.clone().add(new THREE.Vector3(0,4,0)), "#ff8800");
}
}
if (isHit) {
const dmg = 10; // ダメージ固定化
defender.hp -= dmg;
this.data += 100;
playSound('hit');
createFloatingText(`-${dmg}`, defender.pos.clone().add(new THREE.Vector3(0,4,0)), "#ff0000", "36px");
createHitParticles(defender.pos.clone().add(new THREE.Vector3(0,3,0)), 0xff4400);
}
game.updateUI();
}
animateRobot(dt, speed) {
const isMoving = speed > 5;
this.walkCycle += dt * (isMoving ? 15 : 4);
const walkMag = isMoving ? 1.0 : 0.15;
const zL = Math.sin(this.walkCycle) * 2.0 * walkMag;
const zR = Math.sin(this.walkCycle + Math.PI) * 2.0 * walkMag;
const ch = this.robotGroup.children;
if (ch.length >= 15) {
ch[0].position.y = 5.0 + Math.max(0, Math.sin(this.walkCycle * 2)) * 0.4 * walkMag;
ch[1].rotation.y = zL * 0.05;
if (this.state !== 'windup' && this.state !== 'attacking' && this.state !== 'guarding') {
ch[3].rotation.set(0, 0, Math.PI * 0.15 - zL * 0.25);
ch[4].rotation.set(0, 0, Math.PI * 0.2 + (isMoving ? 0.3 : 0));
ch[6].rotation.set(0, 0, -Math.PI * 0.15 - zR * 0.25);
ch[7].rotation.set(0, 0, -Math.PI * 0.2 - (isMoving ? 0.3 : 0));
}
ch[9].position.z = zL * 0.5;
ch[10].position.z = zL * 1.0;
ch[11].position.z = zL * 1.2 + 0.2;
ch[12].position.z = zR * 0.5;
ch[13].position.z = zR * 1.0;
ch[14].position.z = zR * 1.2 + 0.2;
ch[9].position.y = 1.8 + Math.max(0, zL * 0.2);
ch[10].position.y = -0.3 + Math.max(0, zL * 0.4);
ch[11].position.y = -1.6 + Math.max(0, zL * 0.5);
ch[12].position.y = 1.8 + Math.max(0, zR * 0.2);
ch[13].position.y = -0.3 + Math.max(0, zR * 0.4);
ch[14].position.y = -1.6 + Math.max(0, zR * 0.5);
ch[9].rotation.x = zL * 0.2;
ch[10].rotation.x = zL * 0.3 + (isMoving && zL < 0 ? -0.5 : 0);
ch[11].rotation.x = zL * 0.1;
ch[12].rotation.x = zR * 0.2;
ch[13].rotation.x = zR * 0.3 + (isMoving && zR < 0 ? -0.5 : 0);
ch[14].rotation.x = zR * 0.1;
}
// アクション用の上書き(腕突き出しアニメーション等)
if (this.state === 'windup' || this.state === 'attacking') {
if (ch.length >= 9) {
if (this.activeArm === 'L') {
ch[3].rotation.set(-Math.PI / 2, 0, 0);
ch[4].rotation.set(0, 0, 0);
} else if (this.activeArm === 'R') {
ch[6].rotation.set(-Math.PI / 2, 0, 0);
ch[7].rotation.set(0, 0, 0);
} else {
ch[3].rotation.set(-Math.PI / 2, 0, 0);
ch[4].rotation.set(0, 0, 0);
ch[6].rotation.set(-Math.PI / 2, 0, 0);
ch[7].rotation.set(0, 0, 0);
}
}
} else if (this.state === 'guarding') {
if (ch.length >= 9) {
ch[3].rotation.set(0, 0, Math.PI * 0.6);
ch[6].rotation.set(0, 0, -Math.PI * 0.6);
}
}
}
startAttack(arm, opponentList) {
if (this.state !== 'idle') return false;
if (getActiveBitCount(this) >= 1) return false; // 同時1つまでの制限
const target = this.getNearestOpponent(opponentList);
if (!target) return false;
this.targetFacing.subVectors(target.pos, this.pos).normalize();
this.targetFacing.y = 0;
this.state = 'windup';
this.activeArm = arm;
this.timer = 0.3;
const startPos = this.pos.clone().setY(5);
let path;
if (this.isDataSuperior(target)) {
path = this.buildUnpredictablePath(startPos, target.pos);
} else {
path = findPath(this.pos, target.pos, 1.0);
if (!path) path = [startPos, target.pos.clone()];
}
this.currentAttackPath = path;
if (this.isPlayer) {
if (this.predLine) scene.remove(this.predLine);
const points = path.map(p => p.clone().setY(5));
const geo = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineDashedMaterial({ color: this.color, dashSize: 1, gapSize: 0.5, transparent: true, opacity: 0.8 });
this.predLine = new THREE.Line(geo, mat);
this.predLine.computeLineDistances();
scene.add(this.predLine);
}
return true;
}
startGuard(arm, attacker) {
if (this.state !== 'idle' && this.state !== 'guarding') return;
this.state = 'guarding';
this.activeArm = arm;
this.timer = 0.5;
if (attacker) {
this.targetFacing.subVectors(attacker.pos, this.pos).normalize();
this.targetFacing.y = 0;
}
playSound('guard');
}
}
// ==========================================
// 11. AI思考ルーチン(データ量連動)
// ==========================================
function runEnemyAI(enemy, player, dt) {
if (enemy.hp <= 0) return;
enemy.aiActionTimer -= dt;
const dataLevel = enemy.dataLevel;
const isSuperior = enemy.isDataSuperior(player);
if (enemy.state !== 'idle') return;
if (player.state === 'windup' && enemy.aiActionTimer <= 0) {
let guardProb = isSuperior ? Math.min(0.95, 0.65 + (enemy.data - player.data) / 1000) : 0.2 + (dataLevel * 0.06);
if (Math.random() < guardProb) {
enemy.startGuard(player.activeArm || 'L', player);
enemy.aiActionTimer = 0.8;
return;
}
}
let attackInterval = isSuperior ? Math.max(0.4, 1.8 - dataLevel * 0.12) : Math.max(0.8, 2.5 - dataLevel * 0.1);
if (enemy.aiActionTimer <= 0 && Math.random() < 0.06 + dataLevel * 0.015) {
if (getActiveBitCount(enemy) < 1) { // 敵もビット制限
if (isSuperior) {
const predictedPos = player.pos.clone().addScaledVector(new THREE.Vector3(player.targetFacing.x, 0, player.targetFacing.z), 5);
enemy.moveTarget.copy(predictedPos);
}
if (enemy.startAttack(Math.random() < 0.5 ? 'L' : 'R', [player])) {
enemy.aiActionTimer = attackInterval;
return;
}
}
}
if (fragments.length > 0) {
let closest = fragments[0], minDist = enemy.pos.distanceTo(closest.position);
for (let i = 1; i < fragments.length; i++) {
let d = enemy.pos.distanceTo(fragments[i].position);
if (d < minDist) { minDist = d; closest = fragments[i]; }
}
enemy.moveTarget.copy(closest.position);
} else if (enemy.pos.distanceTo(enemy.moveTarget) < 5) {
enemy.moveTarget.set((Math.random() - 0.5) * 80, 1, (Math.random() - 0.5) * 80);
}
const dir = new THREE.Vector3().subVectors(enemy.moveTarget, enemy.pos);
if (dir.lengthSq() > 0.1) {
dir.y = 0; dir.normalize();
enemy.targetFacing.copy(dir);
const speed = 20 + dataLevel * 1.5;
const oldX = enemy.pos.x;
enemy.pos.x += dir.x * speed * dt;
if (checkCollision(enemy.pos, enemy.radius)) enemy.pos.x = oldX;
const oldZ = enemy.pos.z;
enemy.pos.z += dir.z * speed * dt;
if (checkCollision(enemy.pos, enemy.radius)) enemy.pos.z = oldZ;
enemy.pos.x = Math.max(-45, Math.min(45, enemy.pos.x));
enemy.pos.z = Math.max(-45, Math.min(45, enemy.pos.z));
}
}
// ==========================================
// 12. プレイヤー & ステージ初期化
// ==========================================
const player = new Entity(0x00ffaa, 0, 20, true);
function createEnemyAt(x, z) {
const e = new Entity(0xff2222, x, z, false);
e.maxHp = 100; // 敵HP100に修正
e.hp = 100;
e.moveTarget = new THREE.Vector3(x, 1, z);
e.aiActionTimer = 1.0 + Math.random() * 0.5;
return e;
}
function initStage() {
player.hp = player.maxHp;
player.pos.set(0, 1, 20);
player.lastPos.copy(player.pos);
player.state = 'idle';
player.data = 0;
if (player.predLine) scene.remove(player.predLine);
player.predLine = null;
player.facing.set(0, 0, -1);
player.targetFacing.set(0, 0, -1);
player.robotGroup.position.copy(player.pos);
for (const e of enemies) {
scene.remove(e.robotGroup);
scene.remove(e.shieldMesh);
if (e.predLine) scene.remove(e.predLine);
}
enemies.length = 0;
for (const b of bits) b.destroy();
bits.length = 0;
const enemyCount = currentStage;
const spawnPositions = [[0, -20], [-15, -15], [15, -15], [-20, -25], [20, -25]];
for (let i = 0; i < enemyCount && i < 5; i++) {
const sp = spawnPositions[i];
enemies.push(createEnemyAt(sp[0], sp[1]));
}
fragments.forEach(f => scene.remove(f));
fragments.length = 0;
generateBlocks();
game.messages = ["SYSTEM: STAGE " + currentStage + " START - ENEMIES: " + enemyCount];
game.updateUI();
}
// ==========================================
// 13. 入力管理
// ==========================================
const keys = {};
window.addEventListener('keydown', e => {
if(["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].indexOf(e.code) > -1) e.preventDefault();
if(audioCtx.state === 'suspended') audioCtx.resume();
if (e.code === 'Escape') {
if (gameState === 'PLAYING') switchState('PAUSE');
else if (gameState === 'PAUSE') switchState('PLAYING');
return;
}
if (!keys[e.code]) { keys[e.code] = true; handleInput(e.code); }
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
function handleInput(code) {
if (gameState !== 'PLAYING') return;
let arm = (code === 'KeyZ') ? 'L' : (code === 'KeyX') ? 'R' : null;
if (!arm) return;
const aliveEnemies = enemies.filter(e => e.hp > 0);
// ガード判定の緩和: 近くの敵が攻撃中、または近くに飛んできているビットがある場合のみガード
const threateningEnemy = aliveEnemies.find(e =>
(e.state === 'windup' || e.state === 'attacking') &&
e.pos.distanceTo(player.pos) < 20
);
const threateningBit = bits.find(b =>
b.target === player &&
b.pos.distanceTo(player.pos) < 15
);
if ((threateningEnemy || threateningBit) && (player.state === 'idle' || player.state === 'guarding')) {
player.startGuard(arm, threateningEnemy || (threateningBit ? threateningBit.owner : null));
} else {
player.startAttack(arm, aliveEnemies);
}
}
// ==========================================
// 14. アイテムスポーン
// ==========================================
setInterval(() => {
if (gameState !== 'PLAYING') return;
for (let k = 0; k < 5; k++) {
const mesh = new THREE.Mesh(new THREE.OctahedronGeometry(1.2), new THREE.MeshBasicMaterial({ color: 0x00ffff, wireframe: true }));
let valid = false, pos = new THREE.Vector3();
let tries = 0;
while (!valid && tries < 20) {
pos.set((Math.random() - 0.5) * 80, 1.5, (Math.random() - 0.5) * 80);
if (!checkCollision(pos, 2.0)) valid = true;
tries++;
}
if (!valid) continue;
mesh.position.copy(pos);
mesh.userData.type = 'data';
scene.add(mesh); fragments.push(mesh);
}
}, 4000);
setInterval(() => {
if (gameState !== 'PLAYING') return;
for (let k = 0; k < 2; k++) {
const mesh = new THREE.Mesh(new THREE.OctahedronGeometry(1.5), new THREE.MeshBasicMaterial({ color: 0xadff2f, wireframe: true }));
let valid = false, pos = new THREE.Vector3();
let tries = 0;
while (!valid && tries < 20) {
pos.set((Math.random() - 0.5) * 80, 1.5, (Math.random() - 0.5) * 80);
if (!checkCollision(pos, 2.0)) valid = true;
tries++;
}
if (!valid) continue;
mesh.position.copy(pos);
mesh.userData.type = 'regen';
scene.add(mesh); fragments.push(mesh);
}
}, 4000);
// ==========================================
// 15. メインロジック更新
// ==========================================
function updateLogic(dt) {
const aliveEnemies = enemies.filter(e => e.hp > 0);
const promptEl = document.getElementById('centerPrompt');
const threateningEnemy = aliveEnemies.find(e =>
(e.state === 'windup' || e.state === 'attacking') &&
e.pos.distanceTo(player.pos) < 20
);
const threateningBit = bits.find(b =>
b.target === player &&
b.pos.distanceTo(player.pos) < 15
);
if (threateningEnemy || threateningBit) {
promptEl.innerText = "敵攻撃! [Z]か[X]でガード!";
promptEl.className = 'obstructed';
} else if (player.state === 'idle') {
if (aliveEnemies.length > 0) {
promptEl.innerText = "LOCK ON - [Z/X]で攻撃";
promptEl.className = 'locked-on';
} else {
promptEl.innerText = "";
promptEl.className = '';
}
} else {
promptEl.innerText = "";
promptEl.className = '';
}
if (player.state === 'idle' || player.state === 'windup' || player.state === 'guarding') {
let dx = (keys['ArrowRight'] ? 1 : 0) - (keys['ArrowLeft'] ? 1 : 0);
let dz = (keys['ArrowDown'] ? 1 : 0) - (keys['ArrowUp'] ? 1 : 0);
if (dx !== 0 || dz !== 0) {
const moveDir = new THREE.Vector3(dx, 0, dz).normalize();
player.targetFacing.copy(moveDir);
const move = moveDir.clone().multiplyScalar(30 * dt);
const oldX = player.pos.x;
player.pos.x = Math.max(-45, Math.min(45, player.pos.x + move.x));
if(checkCollision(player.pos, player.radius)) player.pos.x = oldX;
const oldZ = player.pos.z;
player.pos.z = Math.max(-45, Math.min(45, player.pos.z + move.z));
if(checkCollision(player.pos, player.radius)) player.pos.z = oldZ;
}
}
for (const e of enemies) {
if (e.hp > 0) runEnemyAI(e, player, dt);
}
player.update(dt, aliveEnemies);
for (const e of enemies) e.update(dt, [player]);
updateBits(dt);
updateParticles(dt);
updateAfterImages(dt);
for (let i = fragments.length - 1; i >= 0; i--) {
fragments[i].rotation.y += dt * 2;
fragments[i].rotation.x += dt;
if (player.pos.distanceTo(fragments[i].position) < 5) {
collectItem(player, fragments[i]);
scene.remove(fragments[i]);
fragments.splice(i, 1);
continue;
}
let collected = false;
for (const e of aliveEnemies) {
if (e.pos.distanceTo(fragments[i].position) < 5) {
collectItem(e, fragments[i]);
scene.remove(fragments[i]);
fragments.splice(i, 1);
collected = true;
break;
}
}
}
updateFloatingTexts(dt);
game.updateUI();
if (player.hp <= 0 && gameState === 'PLAYING') {
game.addMessage("SYSTEM: 機体大破。GAME OVER");
switchState('GAME_OVER');
} else if (aliveEnemies.length === 0 && enemies.length > 0 && gameState === 'PLAYING') {
game.addMessage("SYSTEM: 全敵AI沈黙。STAGE CLEAR!");
switchState('STAGE_CLEAR');
setTimeout(() => {
currentStage++;
if (currentStage > maxStages) switchState('GAME_CLEAR');
else switchState('STAGE_START');
}, 3000);
}
}
function collectItem(collector, itemMesh) {
if (itemMesh.userData.type === 'data') {
collector.data += 100;
createFloatingText("+DATA 100", collector.pos.clone().add(new THREE.Vector3(0,4,0)), "#00ffff");
} else {
collector.regenTimer = 10;
createFloatingText("+REGEN", collector.pos.clone().add(new THREE.Vector3(0,4,0)), "#adff2f");
}
}
// ==========================================
// 16. メインループ
// ==========================================
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
if (gameState === 'PLAYING') {
updateLogic(dt);
} else if (gameState !== 'PAUSE') {
updateFloatingTexts(dt);
updateAfterImages(dt);
updateParticles(dt);
}
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>

コメント