ロボットミニゲーム「Wire Arms」
【更新履歴】
・2026/6/8 バージョン1.0公開。
・2026/6/8 バージョン1.1公開。(ルート選択を導入)
・2026/6/8 バージョン1.2公開。(コマンド選択を導入)
【概要】
・ロボット同士で腕を飛ばしあって対決するゲームです。
・データ量を増やすことで
相手の予測を上回る行動が取れるようになります。
【ルール・勝利条件】
勝利条件: ステージ内に出現する敵対プログラム(ガードロボット)をすべて殲滅すること。全5ステージの踏破を目指します。
敗北条件: 自機(PLAYER)の本体HPが0になり、機体が大破すること。
フェーズ移行: フィールド内の敵を全滅させるとクリアとなり、空間に残存している敵のデータ(DATA)と資材(MATERIAL)を自機がすべて吸収して次のステージへ進みます。
【操作説明】
自機の移動は「常に最優先」され、攻撃アクション中の硬直であっても移動キーを入力すれば即座にキャンセルして動くことができます。
[↑] [↓] [←] [→]: 自機の移動
[Z] [X] [C] [V]: 兵装(アーム)の個別操作 / タクティカルモードへの移行・決定
[Z]: ARM 1 を制御
[X]: ARM 2 を制御
[C]: ARM 3 を制御
[V]: ARM 4 を制御
[ESC]: ゲームのポーズ / タクティカルモードのキャンセル
【システム解説】
1. コマンドモード(未来軌道予測)
敵をロックオンしている状態で、いずれかのアームキー([Z]〜[V])を押すと、ゲーム内の時間の流れが20%に減速し「コマンドモード」へ移行します。
ルート選択: 画面上に、敵へ到達するための複数の攻撃ルート(最短距離の直線、左右からの迂回、フェイント、螺旋を描くカオス軌道など)がホログラムラインで可視化されます。
実行: [←] [→] または [↑] [↓] キーでルートを選び、再度同じアームキーを押すと、指定した軌道でアームが超高速射出されます。
データ優位性: プレイヤーの「DATA」が敵を上回っている場合、敵の予測アルゴリズムを破壊しやすい「複雑な迂回ルート」が自動的に初期選択されるようになります。
2. アクティブガード&自動迎撃システム(インターセプト)
敵のアーム(ビット)が自機に向かって飛んできている際、画面中央に「WARNING」が表示されます。
このタイミングで待機中のアームキーを押すと、自機にバリアを展開すると同時に、指定したアームが「迎撃モード」として射出されます。
迎撃アームは空中で敵の弾を自動追尾し、激突して完全に相殺(撃墜)します。
3. 部位破壊とアーム管理
機体には「本体HP」のほかに、最大4基のアームそれぞれに独立した「アームHP」が存在します。
被弾時、一定確率で本体ではなくアームにダメージが入ります。
アームHPが0になると「CRUSHED(大破)」となり、修理されるまでそのアームのキーは使用不能になります。画面左下のコマンドメニューで状況を常に確認できます。
4. リソース管理と自己進化(クラフト機能)
フィールドに定期的にスポーンするアイテムを回収することで、機体を自動的に強化・修復します。
DATA(水色アイテム / +10):
敵の先読みアルゴリズムを上回るための情報リソースです。
上限解放: 累計DATAが200ポイントに達するごとに、自機の「最大アーム搭載枠」が1つずつ拡張されます(初期2枠 → 最大4枠まで)。
MATERIAL(黄緑アイテム / +10):
物理的な修復や新造に用いる資源です。蓄積された資材は、状況に応じて以下の優先度で自動消費されます。
自己修復: HPが減っている場合、毎秒 MATERIAL を 1 消費し、本体HPを 10 回復します。
アーム修理: 大破(CRUSHED)したアームがある場合、MATERIAL を 10 消費してHPを全回復させ再起動します。
アーム新造: DATA拡張によってマウント枠に空きがある場合、MATERIAL を 20 消費して新たなアームを瞬時に製造・追加します。
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>WIRE ARMS - TACTICAL CORE</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; z-index: 10; }
.bar-container { display: flex; justify-content: space-between; width: 90%; margin: 0 auto; }
.status-box { background: rgba(0,20,0,0.75); border: 1px solid #0f0; padding: 10px; width: 40%; text-shadow: 0 0 5px #000; color: #fff; transition: all 0.3s; }
.status-box.enemy { background: rgba(20,0,0,0.75); border-color: #f00; text-align: right; }
.gauge-label { font-size: 11px; margin-top: 4px; font-weight: bold; }
.hp-bar-bg { width: 100%; height: 14px; background-color: #400; border: 1px solid #777; position: relative; overflow: hidden; margin-bottom: 2px;}
.hp-bar-fg { height: 100%; background-color: #0f0; width: 100%; transition: width 0.2s linear; box-shadow: 0 0 10px #0f0; }
.enemy .hp-bar-fg { background-color: #f00; box-shadow: 0 0 10px #f00; }
.data-bar-fg { height: 100%; background-color: #00ffff; box-shadow: 0 0 8px #00ffff; transition: width 0.2s; }
.mat-bar-fg { height: 100%; background-color: #adff2f; box-shadow: 0 0 8px #adff2f; transition: width 0.2s; }
.enemy .data-bar-fg { background-color: #ff00ff; box-shadow: 0 0 8px #ff00ff; }
#log { position: absolute; bottom: 20px; right: 20px; color: #aaa; pointer-events: none; font-size: 14px; text-shadow: 0 0 2px #000; line-height: 1.5; z-index: 10; text-align: right;}
#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; z-index: 10;}
.tactical-mode { color: #ffea00 !important; text-shadow: 0 0 20px #ffea00, 0 0 40px #ff8800 !important; font-size: 28px !important; }
.target-hud { position: absolute; color: #fff; text-shadow: 0 0 5px #000; font-size: 12px; pointer-events: none; text-align: center; white-space: nowrap; transform: translate(-50%, -100%); display: none; background: rgba(0,0,0,0.6); border: 1px solid #0ff; padding: 4px; z-index: 5;}
.target-hud.enemy-hud { border-color: #f00; color: #f33; }
/* COMMAND MENU (Z, X, C, V) */
#commandMenu { position: absolute; bottom: 30px; left: 20px; display: flex; flex-direction: column; gap: 8px; z-index: 10; opacity: 0; transition: opacity 0.3s; }
.cmd-item { background: rgba(0,30,0,0.85); border: 2px solid #0f0; color: #0f0; padding: 12px 20px; font-size: 22px; font-weight: bold; text-shadow: 0 0 10px #0f0; width: 280px; transition: all 0.2s; box-shadow: 0 0 10px rgba(0,255,0,0.2); }
.cmd-item.in-use { background: rgba(30,30,30,0.8); border-color: #555; color: #777; text-shadow: none; box-shadow: none; }
.cmd-item.crushed { background: rgba(50,0,0,0.85); border-color: #f00; color: #f55; text-decoration: line-through; text-shadow: 0 0 5px #f00; }
.cmd-item.empty { background: rgba(0,0,0,0.5); border-color: #333; color: #444; text-shadow: none; box-shadow: none; }
/* TACTICAL ROUTES MENU */
#tacticalMenu { position: absolute; top: 45%; left: 330px; transform: translateY(-50%); z-index: 10; display: none; background: rgba(0,0,0,0.8); padding: 20px; border: 2px solid #ffea00; box-shadow: 0 0 20px #ffea00; }
#tacticalMenu h3 { margin: 0 0 15px 0; color: #ffea00; text-shadow: 0 0 10px #ffea00; font-size: 24px; letter-spacing: 2px; }
.route-item { padding: 10px 15px; margin-bottom: 5px; color: #00ffff; font-size: 20px; border-left: 4px solid transparent; transition: all 0.2s; }
.route-item.active { background: rgba(255,234,0,0.2); color: #ffea00; border-left: 4px solid #ffea00; font-weight: bold; text-shadow: 0 0 10px #ffea00; }
h2, p { margin: 0; padding: 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; z-index: 10;}
#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 style="color:#0f0; text-shadow:0 0 5px #0f0;">PLAYER</h2>
<div class="gauge-label">HP (<span id="hpPlayerTxt">1000</span>)</div>
<div class="hp-bar-bg"><div class="hp-bar-fg" id="hpBarPlayer"></div></div>
<div class="gauge-label" style="color:#00ffff;">DATA (<span id="dataPlayerTxt">0</span>) - ARM MAX: 4</div>
<div class="hp-bar-bg" style="height:6px; background:#004444;"><div class="data-bar-fg" id="dataBarPlayer"></div></div>
<div class="gauge-label" style="color:#adff2f;">MATERIAL (<span id="matPlayerTxt">0</span>)</div>
<div class="hp-bar-bg" style="height:6px; background:#224400;"><div class="mat-bar-fg" id="matBarPlayer"></div></div>
</div>
<div class="status-box enemy" id="uiEnemy">
<h2 style="color:#f33; text-shadow:0 0 5px #f00;">ENEMY</h2>
<div class="gauge-label">HP (<span id="hpEnemyTxt">300</span>)</div>
<div class="hp-bar-bg"><div class="hp-bar-fg" id="hpBarEnemy"></div></div>
<div class="gauge-label" style="color:#ff00ff;">DATA (<span id="dataEnemyTxt">0</span>)</div>
<div class="hp-bar-bg" style="height:6px; background:#404;"><div class="data-bar-fg" id="dataBarEnemy"></div></div>
<div class="gauge-label" style="color:#adff2f;">MATERIAL (<span id="matEnemyTxt">0</span>)</div>
<div class="hp-bar-bg" style="height:6px; background:#224400;"><div class="mat-bar-fg" id="matBarEnemy"></div></div>
</div>
</div>
</div>
<div id="commandMenu"></div>
<div id="tacticalMenu">
<h3>TACTICAL ROUTES</h3>
<div id="routeList"></div>
</div>
<div id="stageIndicator"></div>
<div id="centerPrompt"></div>
<div id="log"></div>
<div id="targetHud" class="target-hud"></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;">PAUSED</h1><p class="blink">[ESC]キーで戦闘再開</p></div>
<div id="screenStageClear" class="screen"><h1 style="color:#0f0;">STAGE CLEAR</h1><p>敵データの残滓を吸収...<br>次フェーズへ移行します。</p></div>
<div id="screenGameOver" class="screen"><h1 style="color:#f00;">GAME OVER</h1><p>機体大破。接続をロストしました。</p></div>
<div id="screenGameClear" class="screen"><h1 style="color:#ff0ff0;">ALL CLEARED</h1><p>MISSION ACCOMPLISHED.<br>全脅威の排除を確認しました。</p></div>
<script>
// ==========================================
// 0. ゲーム状態管理
// ==========================================
let gameState = 'TITLE';
let currentStage = 1;
const maxStages = 5;
let enemies = [];
let accumulatedEnemyData = 0;
let accumulatedEnemyMat = 0;
const combatState = {
active: false,
timeScale: 1.0,
routes: [],
selectedIndex: 0,
lines: [],
target: null,
arm: null,
timer: 0,
triggerKey: null
};
function switchState(newState) {
document.querySelectorAll('.screen').forEach(el => el.classList.remove('active'));
gameState = newState;
const ui = document.getElementById('ui');
const cmdMenu = document.getElementById('commandMenu');
const stageInd = document.getElementById('stageIndicator');
const targetHud = document.getElementById('targetHud');
const body = document.body;
if (newState === 'PLAYING') {
ui.style.opacity = '1'; cmdMenu.style.opacity = '1';
stageInd.classList.add('visible');
body.classList.remove('blur');
} else {
ui.style.opacity = '0'; cmdMenu.style.opacity = '0';
stageInd.classList.remove('visible');
targetHud.style.display = 'none';
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');
player.data += accumulatedEnemyData;
player.materials += accumulatedEnemyMat;
game.addMessage(`SYSTEM: DATA +${accumulatedEnemyData} / MAT +${accumulatedEnemyMat}`);
player.checkCrafting();
}
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;
player.data = 0; player.materials = 0;
player.maxArms = 2;
player.arms = [{id: 'ARM1', hp: 100, maxHp: 100}, {id: 'ARM2', hp: 100, maxHp: 100}];
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 === '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);
} else if(type === 'craft') {
osc.type = 'square'; osc.frequency.setValueAtTime(600, now); osc.frequency.setValueAtTime(800, now + 0.1); osc.frequency.setValueAtTime(1200, now + 0.2);
gain.gain.setValueAtTime(0.4, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
osc.start(now); osc.stop(now + 0.4);
} else if(type === 'crush') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(50, now); osc.frequency.exponentialRampToValueAtTime(10, now + 0.4);
gain.gain.setValueAtTime(0.8, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
osc.start(now); osc.stop(now + 0.4);
} else if(type === 'intercept') {
osc.type = 'square'; osc.frequency.setValueAtTime(1500, now); osc.frequency.exponentialRampToValueAtTime(500, 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 === 'menu') {
osc.type = 'sine'; osc.frequency.setValueAtTime(800, now); osc.frequency.linearRampToValueAtTime(1200, now + 0.05);
gain.gain.setValueAtTime(0.2, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.05);
osc.start(now); osc.stop(now + 0.05);
}
}
// ==========================================
// 2. UI更新 (コマンドメニュー含む)
// ==========================================
const game = {
messages: ["SYSTEM: 起動待機中..."],
addMessage(msg) {
this.messages.push(msg);
if(this.messages.length > 5) this.messages.shift();
document.getElementById('log').innerHTML = this.messages.join('<br>');
},
updateUI() {
document.getElementById('hpBarPlayer').style.width = Math.max(0, (player.hp / player.maxHp) * 100) + '%';
document.getElementById('hpPlayerTxt').innerText = Math.floor(player.hp);
document.getElementById('dataPlayerTxt').innerText = player.data;
document.getElementById('dataBarPlayer').style.width = Math.min(100, ((player.data % 200) / 200) * 100) + '%';
document.getElementById('matPlayerTxt').innerText = player.materials;
document.getElementById('matBarPlayer').style.width = Math.min(100, (player.materials / 50) * 100) + '%';
const firstAliveEnemy = enemies.find(e => e.hp > 0) || enemies[0];
if (firstAliveEnemy) {
document.getElementById('hpBarEnemy').style.width = Math.max(0, (firstAliveEnemy.hp / firstAliveEnemy.maxHp) * 100) + '%';
document.getElementById('hpEnemyTxt').innerText = Math.floor(firstAliveEnemy.hp);
document.getElementById('dataEnemyTxt').innerText = firstAliveEnemy.data;
document.getElementById('dataBarEnemy').style.width = Math.min(100, ((firstAliveEnemy.data % 200) / 200) * 100) + '%';
document.getElementById('matEnemyTxt').innerText = firstAliveEnemy.materials;
document.getElementById('matBarEnemy').style.width = Math.min(100, (firstAliveEnemy.materials / 50) * 100) + '%';
} else {
document.getElementById('hpBarEnemy').style.width = '0%';
}
document.getElementById('stageIndicator').innerText = `STAGE ${currentStage} / ${maxStages} ENEMIES: ${enemies.filter(e=>e.hp>0).length}`;
this.updateCommandMenu();
},
updateCommandMenu() {
if(gameState !== 'PLAYING') return;
const keyMap = ['Z', 'X', 'C', 'V'];
const container = document.getElementById('commandMenu');
container.innerHTML = '';
// 敵の弾が来ているかチェック(ガード可能か判定)
const threateningBit = bits.find(b => b.targetObj === player && b.pos.distanceTo(player.pos) < 35 && b.bitState === 'OUTBOUND');
for (let i = 0; i < 4; i++) {
const arm = player.arms[i];
const key = keyMap[i];
const div = document.createElement('div');
if (!arm) {
div.className = 'cmd-item empty';
div.innerText = `[${key}] - EMPTY -`;
} else if (arm.hp <= 0) {
div.className = 'cmd-item crushed';
div.innerText = `[${key}] ${arm.id} CRUSHED`;
} else if (bits.find(b => b.owner === player && b.armId === arm.id)) {
div.className = 'cmd-item in-use';
div.innerText = `[${key}] ${arm.id} IN USE`;
} else {
div.className = 'cmd-item';
// 弾が来ていればガード&迎撃、そうでなければATTACK
if (threateningBit) {
div.innerText = `[${key}] GUARD / INTERCEPT`;
div.style.color = '#00ffff';
div.style.borderColor = '#00ffff';
} else {
div.innerText = `[${key}] ATTACK PREDICT`;
}
}
container.appendChild(div);
}
}
};
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;
div.style.zIndex = 20;
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);
}
}
function updateTargetHud() {
const targetHud = document.getElementById('targetHud');
let target = combatState.active ? combatState.target : null;
if (!target) {
const aliveEnemies = enemies.filter(e => e.hp > 0);
target = player.getNearestOpponent(aliveEnemies);
}
if (gameState === 'PLAYING' && target && target.hp > 0) {
let proj = target.pos.clone().add(new THREE.Vector3(0, 10, 0)).project(camera);
let x = (proj.x * 0.5 + 0.5) * window.innerWidth;
let y = (proj.y * -0.5 + 0.5) * window.innerHeight;
targetHud.style.left = `${x}px`;
targetHud.style.top = `${y}px`;
targetHud.style.display = 'block';
targetHud.className = target.isPlayer ? 'target-hud' : 'target-hud enemy-hud';
targetHud.innerHTML = `TARGET HP: ${Math.floor(target.hp)}`;
} else {
targetHud.style.display = 'none';
}
}
// ==========================================
// 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);
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 fragments = [];
function generateBlocks() {
blocks.forEach(b => scene.remove(b));
blocks.length = 0;
const currentStageWireColor = new THREE.Color().setHSL(Math.random(), 0.8, 0.5);
const blockMat = new THREE.MeshBasicMaterial({ color: currentStageWireColor, wireframe: true });
// ボンバーマン風配置
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;
let mesh = new THREE.Mesh(blockGeo, blockMat);
mesh.position.set(x, 5, z);
scene.add(mesh);
blocks.push(mesh);
}
}
}
function checkCollision(pos, radius = 4.0) {
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;
}
// ==========================================
// 4. A* アルゴリズム & 複数ルート生成
// ==========================================
const PATH_GRID_SIZE = 4;
const PATH_GRID_W = Math.ceil(100 / PATH_GRID_SIZE);
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 = 4.0) {
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] = startPos.clone(); path[path.length-1] = endPos.clone();
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;
}
function generateMultiPaths(startPos, targetPos, attackerData, defenderData) {
const routes = [];
const start = startPos.clone().setY(5);
const target = targetPos.clone().setY(5);
let direct = findPath(start, target, 4.0);
if (!direct) direct = [start.clone(), target.clone()];
routes.push({ path: direct, name: "DIRECT" });
const dir = target.clone().sub(start).normalize();
const leftSide = new THREE.Vector3(-dir.z, 0, dir.x);
const leftMid = start.clone().lerp(target, 0.5).add(leftSide.multiplyScalar(25));
let pathL1 = findPath(start, leftMid, 4.0) || [start.clone(), leftMid.clone()];
let pathL2 = findPath(leftMid, target, 4.0) || [leftMid.clone(), target.clone()];
routes.push({ path: pathL1.concat(pathL2.slice(1)), name: "FLANK_L" });
const rightSide = new THREE.Vector3(dir.z, 0, -dir.x);
const rightMid = start.clone().lerp(target, 0.5).add(rightSide.multiplyScalar(25));
let pathR1 = findPath(start, rightMid, 4.0) || [start.clone(), rightMid.clone()];
let pathR2 = findPath(rightMid, target, 4.0) || [rightMid.clone(), target.clone()];
routes.push({ path: pathR1.concat(pathR2.slice(1)), name: "FLANK_R" });
return routes;
}
// ==========================================
// 4.5 戦術スローモーションUI & メニュー
// ==========================================
function enterSlowMotion(armObj, target, triggerKey) {
combatState.active = true;
combatState.timeScale = 0.20;
combatState.target = target;
combatState.arm = armObj;
combatState.timer = 0.2;
combatState.triggerKey = triggerKey;
const routesData = generateMultiPaths(player.pos, target.pos, player.data, target.data);
combatState.routes = routesData;
combatState.selectedIndex = player.isDataSuperior(target) ? routesData.length - 1 : 0;
buildRouteLines();
renderTacticalMenu();
document.getElementById('tacticalMenu').style.display = 'block';
playSound('menu');
}
function exitSlowMotion(executeAttack) {
combatState.active = false;
combatState.timeScale = 1.0;
clearRouteLines();
document.getElementById('tacticalMenu').style.display = 'none';
if (executeAttack && combatState.target && combatState.target.hp > 0) {
const chosenPath = combatState.routes[combatState.selectedIndex].path;
player.executeAttack(combatState.arm, combatState.target, chosenPath);
}
}
function renderTacticalMenu() {
const list = document.getElementById('routeList');
list.innerHTML = '';
for (let i = 0; i < combatState.routes.length; i++) {
const div = document.createElement('div');
div.className = i === combatState.selectedIndex ? 'route-item active' : 'route-item';
div.innerText = `[${i+1}] ${combatState.routes[i].name}`;
list.appendChild(div);
}
}
function buildRouteLines() {
clearRouteLines();
for (let i = 0; i < combatState.routes.length; i++) {
const route = combatState.routes[i].path;
const points = route.map(p => p.clone().setY(5));
const geo = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineDashedMaterial({
color: i === combatState.selectedIndex ? 0xffff00 : 0x00ffff,
dashSize: 1.5, gapSize: 0.5, transparent: true, opacity: i === combatState.selectedIndex ? 1.0 : 0.35, linewidth: 2
});
const line = new THREE.Line(geo, mat);
line.computeLineDistances();
scene.add(line);
combatState.lines.push(line);
}
}
function updateRouteColors() {
renderTacticalMenu();
for (let i = 0; i < combatState.lines.length; i++) {
const line = combatState.lines[i];
if (i === combatState.selectedIndex) {
line.material.color.setHex(0xffff00);
line.material.opacity = 1.0;
} else {
line.material.color.setHex(0x00ffff);
line.material.opacity = 0.35;
}
}
}
function clearRouteLines() {
for (let line of combatState.lines) {
scene.remove(line);
line.geometry.dispose();
line.material.dispose();
}
combatState.lines = [];
}
// ==========================================
// 5. パーティクル & 残像
// ==========================================
const particles = [];
function createHitParticles(pos, color) {
for (let i = 0; i < 18; i++) {
const mesh = new THREE.Mesh(new THREE.SphereGeometry(0.25, 4, 4), new THREE.MeshBasicMaterial({ color: color, wireframe: true }));
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 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;
p.mesh.material.opacity = p.life / p.maxLife; p.mesh.material.transparent = true;
}
}
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; });
}
}
// ==========================================
// 6. ビット(飛翔体)
// ==========================================
const bits = [];
class Bit {
constructor(startPos, path, color, owner, targetObj, predLine, armId) {
this.owner = owner; this.targetObj = targetObj; this.path = path;
this.pathIndex = 0; this.speed = 90; this.pos = startPos.clone();
this.color = color; this.armId = armId; this.isInterceptor = false;
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.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.isInterceptor && this.targetObj && this.targetObj.life > 0) {
const targetPos = this.targetObj.pos.clone();
const dir = new THREE.Vector3().subVectors(targetPos, this.pos).normalize();
this.pos.addScaledVector(dir, this.speed * dt); this.mesh.position.copy(this.pos);
if (this.pos.distanceTo(targetPos) < 3.0) this.fireLaser();
} else 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.targetObj && this.targetObj.hp > 0 && this.pos.distanceTo(this.targetObj.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';
}
} 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 * 4.0 * 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; this.mesh.visible = false;
if (this.isInterceptor) {
if (this.targetObj && this.targetObj.life > 0) {
const points = [this.pos.clone(), this.targetObj.pos.clone()];
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);
createHitParticles(this.targetObj.pos, 0xffaa00);
createFloatingText("INTERCEPTED!", this.targetObj.pos.clone().add(new THREE.Vector3(0,5,0)), "#ffaa00");
playSound('hit');
this.targetObj.destroy();
}
} else {
if (!this.targetObj || this.targetObj.hp <= 0) { this.laserTimer = 0.1; return; }
const points = [this.pos.clone(), this.targetObj.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.owner.resolveAttack(this.targetObj);
createHitParticles(this.targetObj.pos.clone().setY(3), this.color);
}
this.laserTimer = 0.3;
}
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); } }
}
// ==========================================
// 7. ワイヤーフレーム
// ==========================================
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);
scene.add(group); return group;
}
function createShieldMesh() {
const mesh = new THREE.Mesh(new THREE.SphereGeometry(5.0, 12, 12), new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.25, wireframe: true }));
mesh.visible = false; scene.add(mesh); return mesh;
}
// ==========================================
// 8. 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 : 300; this.hp = this.maxHp;
this.data = 0; this.materials = 0; this.maxArms = 2;
this.arms = [ { id: 'ARM1', hp: 100, maxHp: 100 }, { id: 'ARM2', hp: 100, maxHp: 100 } ];
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.regenTickTimer = 0;
this.activeArm = null; this.currentAttackPath = null; this.attackTarget = null;
this.predLine = null; this.afterImageTimer = 0; this.walkCycle = 0;
this.aiActionTimer = isPlayer ? 0 : 2.0; this.moveTarget = new THREE.Vector3(x, 1, z);
}
get dataLevel() { return Math.floor(this.data / 100); }
isDataSuperior(opponent) { return this.data > opponent.data; }
getAvailableArm() { return this.arms.find(a => a.hp > 0 && !bits.find(b => b.owner === this && b.armId === a.id)); }
checkCrafting() {
this.maxArms = Math.min(4, 2 + Math.floor(this.data / 200));
for (let arm of this.arms) {
if (arm.hp <= 0 && this.materials >= 10) {
this.materials -= 10; arm.hp = arm.maxHp;
if (this.isPlayer) game.addMessage(`SYSTEM: [${arm.id}] REPAIRED.`);
playSound('craft');
}
}
while (this.arms.length < this.maxArms && this.materials >= 20) {
this.materials -= 20; const newId = 'ARM' + (this.arms.length + 1);
this.arms.push({ id: newId, hp: 100, maxHp: 100 });
if (this.isPlayer) game.addMessage(`SYSTEM: [${newId}] BUILT.`);
playSound('craft');
}
}
fireBit(targetEntity, preCalcPath, armObj) {
if (!targetEntity || targetEntity.hp <= 0 || !armObj) return;
const startPos = this.pos.clone().add(new THREE.Vector3(0, 5, 0));
let path = preCalcPath || generateMultiPaths(startPos, targetEntity.pos, this.data, targetEntity.data)[0].path;
const bit = new Bit(startPos, path, this.color, this, targetEntity, this.predLine, armObj.id);
this.predLine = null; bits.push(bit); playSound('attack');
}
update(dt, opponents) {
this.regenTickTimer += dt;
if (this.regenTickTimer > 1.0) {
this.regenTickTimer = 0;
if (this.hp < this.maxHp && this.materials >= 1) {
this.hp = Math.min(this.maxHp, this.hp + 10); this.materials -= 1;
createFloatingText("+10 HP", this.pos.clone().add(new THREE.Vector3(0,4,0)), "#0f0");
}
}
if (this.targetFacing.lengthSq() > 0.001) { this.facing.lerp(this.targetFacing, 0.12); this.facing.y = 0; this.facing.normalize(); }
this.robotGroup.rotation.y = Math.atan2(this.facing.x, this.facing.z); 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 === 'windup' || this.state === 'guarding') {
this.afterImageTimer -= dt;
if (this.afterImageTimer <= 0) { createAfterImage(this.robotGroup, this.color); this.afterImageTimer = 0.04; }
} 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;
this.fireBit(this.attackTarget, this.currentAttackPath, this.activeArm);
}
} else if (this.state === 'attacking' || this.state === 'guarding') {
this.timer -= dt;
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 === '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 = 25; this.data += 100;
const aliveArms = defender.arms.filter(a => a.hp > 0);
if (aliveArms.length > 0 && Math.random() < 0.4) {
const hitArm = aliveArms[Math.floor(Math.random() * aliveArms.length)];
hitArm.hp -= dmg;
createFloatingText(`ARM HIT! -${dmg}`, defender.pos.clone().add(new THREE.Vector3(0,5,0)), "#ffaa00", "28px");
playSound('crush');
if (hitArm.hp <= 0) createFloatingText(`[${hitArm.id}] CRUSHED!`, defender.pos.clone().add(new THREE.Vector3(0,6,0)), "#ff0000", "32px");
} else {
defender.hp -= dmg; 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);
}
this.checkCrafting(); defender.checkCrafting(); 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[3].position.set(-2.5, 5.5, 0); ch[6].position.set(2.5, 5.5, 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) {
const isLeft = (this.activeArm && parseInt(this.activeArm.id.replace('ARM','')) % 2 !== 0);
if (isLeft || !this.activeArm) { ch[3].rotation.set(-Math.PI / 2, 0, Math.PI * 0.1); ch[4].rotation.set(0, 0, 0); ch[3].position.z = -1.5; }
if (!isLeft || !this.activeArm) { ch[6].rotation.set(-Math.PI / 2, 0, -Math.PI * 0.1); ch[7].rotation.set(0, 0, 0); ch[6].position.z = -1.5; }
}
} 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(opponentList) {
if (this.state !== 'idle') return false;
const arm = this.getAvailableArm(); if (!arm) return false;
const target = this.getNearestOpponent(opponentList); if (!target) return false;
const startPos = this.pos.clone().setY(5);
const routes = generateMultiPaths(startPos, target.pos, this.data, target.data);
this.executeAttack(arm, target, routes[0].path);
return true;
}
executeAttack(armObj, target, preCalcPath) {
this.targetFacing.subVectors(target.pos, this.pos).normalize(); this.targetFacing.y = 0;
this.state = 'windup'; this.activeArm = armObj; this.timer = 0.2;
this.currentAttackPath = preCalcPath; this.attackTarget = target;
if (this.isPlayer) {
if (this.predLine) scene.remove(this.predLine);
const points = preCalcPath.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);
}
}
startGuard(attacker) {
if (this.state !== 'idle' && this.state !== 'guarding') return;
this.state = 'guarding'; this.timer = 0.5;
if (attacker) { this.targetFacing.subVectors(attacker.pos, this.pos).normalize(); this.targetFacing.y = 0; }
playSound('hit'); // Guard sound
}
}
// ==========================================
// 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;
let attackInterval = isSuperior ? Math.max(2.0, 4.0 - dataLevel * 0.1) : Math.max(3.0, 5.0 - dataLevel * 0.1);
if (enemy.aiActionTimer <= 0 && Math.random() < 0.03 + dataLevel * 0.005) {
if (enemy.getAvailableArm()) {
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([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.data = Math.floor(Math.random() * 200) + (currentStage * 150);
e.materials = Math.floor(Math.random() * 30); e.checkCrafting();
e.moveTarget = new THREE.Vector3(x, 1, z); e.aiActionTimer = 2.0 + Math.random() * 2.0;
return e;
}
function initStage() {
player.hp = player.maxHp; player.pos.set(0, 1, 20); player.lastPos.copy(player.pos);
player.state = 'idle'; 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);
combatState.active = false; combatState.timeScale = 1.0; clearRouteLines();
for (const e of enemies) scene.remove(e.robotGroup); 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++) enemies.push(createEnemyAt(spawnPositions[i][0], spawnPositions[i][1]));
fragments.forEach(f => scene.remove(f)); fragments.length = 0;
generateBlocks();
game.messages = ["SYSTEM: STAGE " + currentStage + " START"]; 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 (combatState.active) { exitSlowMotion(false); return; }
if (gameState === 'PLAYING') switchState('PAUSE');
else if (gameState === 'PAUSE') switchState('PLAYING');
return;
}
if (gameState === 'PLAYING' && combatState.active) {
if (!keys[e.code]) {
keys[e.code] = true;
if (e.code === 'ArrowUp') {
combatState.selectedIndex = (combatState.selectedIndex - 1 + combatState.routes.length) % combatState.routes.length;
updateRouteColors(); playSound('menu');
} else if (e.code === 'ArrowDown') {
combatState.selectedIndex = (combatState.selectedIndex + 1) % combatState.routes.length;
updateRouteColors(); playSound('menu');
} else if (e.code === combatState.triggerKey) {
if (combatState.timer <= 0) exitSlowMotion(true);
}
}
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' || combatState.active) return;
let armIndex = null;
if (code === 'KeyZ') armIndex = 0;
else if (code === 'KeyX') armIndex = 1;
else if (code === 'KeyC') armIndex = 2;
else if (code === 'KeyV') armIndex = 3;
if (armIndex === null) return;
const armObj = player.arms[armIndex];
if (!armObj || armObj.hp <= 0) { game.addMessage("SYSTEM: 兵装が使用不可です。"); return; }
if (bits.find(b => b.owner === player && b.armId === armObj.id)) return; // 使用中
// 迎撃対象の探索(自分に向かってきているビット)
const incomingBits = bits.filter(b => b.targetObj === player && b.bitState === 'OUTBOUND');
incomingBits.sort((a,b) => a.pos.distanceToSq(player.pos) - b.pos.distanceToSq(player.pos));
// 敵の弾が近い場合は「アクティブガード&迎撃」
if (incomingBits.length > 0 && incomingBits[0].pos.distanceTo(player.pos) < 35) {
const targetBit = incomingBits[0];
player.startGuard(targetBit.owner);
const path = [player.pos.clone().setY(5), targetBit.pos.clone().setY(5)];
const interceptBit = new Bit(player.pos.clone().add(new THREE.Vector3(0,5,0)), path, player.color, player, targetBit, null, armObj.id);
interceptBit.isInterceptor = true; bits.push(interceptBit);
playSound('intercept');
createFloatingText("INTERCEPT!", player.pos.clone().add(new THREE.Vector3(0,5,0)), "#00ffff");
return;
}
// 弾が来ていなければタクティカルモード(攻撃予測)へ
const aliveEnemies = enemies.filter(e => e.hp > 0);
const targetEnemy = player.getNearestOpponent(aliveEnemies);
if (targetEnemy && (player.state === 'idle' || player.state === 'guarding')) {
enterSlowMotion(armObj, targetEnemy, code);
}
}
// ==========================================
// 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 < 3; k++) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(2,2,2), 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 = 'material'; scene.add(mesh); fragments.push(mesh);
}
}, 4500);
// ==========================================
// 15. 更新ロジック
// ==========================================
function updateLogic(dt) {
const aliveEnemies = enemies.filter(e => e.hp > 0);
const promptEl = document.getElementById('centerPrompt');
if (combatState.active) {
combatState.timer -= dt;
const pulse = (Math.sin(performance.now() * 0.01) + 1.0) * 0.5;
if (combatState.lines[combatState.selectedIndex]) {
combatState.lines[combatState.selectedIndex].material.opacity = 0.6 + pulse * 0.4;
}
}
const enemyDt = dt * combatState.timeScale;
const bitDt = dt * combatState.timeScale;
if (combatState.active) {
promptEl.innerText = `[↑][↓] SELECT / [${combatState.triggerKey.replace('Key','')}] EXECUTE`;
promptEl.className = 'tactical-mode';
} else {
const threateningBit = bits.find(b => b.targetObj === player && b.pos.distanceTo(player.pos) < 35 && b.bitState === 'OUTBOUND');
if (threateningBit) {
promptEl.innerText = "WARNING: [Z/X/C/V] でガード&迎撃";
promptEl.className = 'obstructed';
} else if (player.state === 'idle' && aliveEnemies.length > 0 && player.getAvailableArm()) {
promptEl.innerText = "LOCK ON - [Z/X/C/V] で攻撃予測";
promptEl.className = 'locked-on';
} else {
promptEl.innerText = "";
promptEl.className = '';
}
}
// 移動制御(手動移動を常に許可し、硬直をキャンセル)
if (!combatState.active) {
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(35 * 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;
if (player.state === 'guarding') player.state = 'idle'; // 移動でガード解除
}
}
for (const e of enemies) if (e.hp > 0) runEnemyAI(e, player, enemyDt);
player.update(dt, aliveEnemies);
for (const e of enemies) e.update(enemyDt, [player]);
updateBits(bitDt); updateParticles(dt); updateAfterImages(dt);
if (!combatState.active) {
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); updateTargetHud(); 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') {
accumulatedEnemyData = enemies.reduce((sum, e) => sum + e.data, 0);
accumulatedEnemyMat = enemies.reduce((sum, e) => sum + e.materials, 0);
game.addMessage("SYSTEM: 全敵AI沈黙。"); 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 += 10; createFloatingText("+DATA 10", collector.pos.clone().add(new THREE.Vector3(0,4,0)), "#00ffff");
} else if (itemMesh.userData.type === 'material') {
collector.materials += 10; createFloatingText("+MATERIAL 10", collector.pos.clone().add(new THREE.Vector3(0,4,0)), "#adff2f");
}
if (collector.isPlayer) collector.checkCrafting();
}
// ==========================================
// 16. メインループ
// ==========================================
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05);
if (gameState === 'PLAYING') updateLogic(dt);
else if (gameState !== 'PAUSE') { updateFloatingTexts(dt); updateAfterImages(dt); updateParticles(dt); }
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>

コメント