ロボットミニゲーム「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>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
ロボットミニゲーム「Wire Arms」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1