ロボットミニゲーム「Wire Arms」


【更新履歴】

・2026/6/8 バージョン1.0公開。
・2026/6/8 バージョン1.1公開。(ルート選択を導入)
・2026/6/8 バージョン1.2公開。(コマンド選択を導入)
・2026/6/8 バージョン1.3公開。(コマンド1パス方式に変更)
・2026/6/9 バージョン1.4公開。(見た目を改善)

画像


■タクティカルモード(未来軌道予測)


・敵機に近づくとロックオンされ、攻撃軌道が表示されます。

・Z、X、C、Vのいずれかのキーを押すと、
 待機中のアームが、ターゲットに向けて発射されます。

(ゲーム開始時には、ZとXの2つのアームしか有りません。)
 

■アクティブガード&自動迎撃


・敵のアームが自機に向かって発射され、
 自機と衝突する危険がある(距離35以内かつ接近中)の場合は、
 「WARNING: [Z/X/C/V] でガード&迎撃」 と警告が表示されます。


インターセプトの瞬時発動:

・この警告中に、待機中のアームキーを押すと即座に発動します。

バリアと自動追尾:

・自機に防御バリアを展開すると同時に、
 指定したアームを「迎撃用インターセプター」として射出します。

完全相殺:

・迎撃アームは飛来する敵の弾を空中で自動追尾し、
 激突して完全に相殺(相打ち撃墜)します。

・成功時には、画面に 「INTERCEPT!」 と表示されます。

■部位破壊とアーム管理


・各機体は、「本体HP」のほかに、
 最大4基のアームそれぞれに独立した「アームHP」を持っています。

・被弾時、一定確率でアームが身代わりとなってダメージを受けます。

ATTACK PREDICT (緑表示):

・アーム待機状態。
・いつでも攻撃予測モードへ移行可能です。

GUARD / INTERCEPT (水色表示):

・敵の攻撃を感知した迎撃可能状態。
・押すことでアクティブガードが発動します。

IN USE (グレー表示):

・現在アームを射出中。
・手元に戻るまで再使用はできません。

CRUSHED (赤・取り消し線表示):

・アームHPが0になり大破した状態。
・資材(MATERIAL)による自動修理が完了するまで
 キー入力は受け付けられません。

EMPTY (黒表示):

・拡張マウント枠はあるが、
 まだアームが製造されていない空きスロットです。

■ナノマシン・クラフト


・フィールド上のアイテムを回収することで、
 機体はDATA(情報)とMATERIAL(資材)を蓄積し、
 リアルタイムに自己進化・修復を行います。

【DATA(水色アイテム / 1回収につき +10)】


・敵を凌駕するための戦術情報リソースです。

最大マウント枠の拡張:

・ゲーム開始時の初期アームは2基ですが、
 累計DATAが200ポイントに達するごとに
 「最大アーム搭載枠」が1つずつ自動解放
されます。
 (最大4枠まで拡張可能)

【MATERIAL(黄緑アイテム / 1回収につき +10)】


・機体の物理的構成を書き換えるためのナノマシン資材です。

・蓄積された資材は、戦闘状況に応じて
 以下の優先順位で自動的に消費(クラフト)されます。

自己修復(本体HPの再生):

・本体HPが最大値未満のとき、毎秒 MATERIAL を 1 消費し、
 本体HPを 10 ずつ継続回復
します。

アーム修理(CRUSHEDからの再起動):

・大破(CRUSHED)状態のアームが存在する場合、
 MATERIAL を 10 消費 してアームHPを全回復させ、
 即座に再起動・戦線復帰させます。

アーム新造(空き枠への追加武装):

・DATA拡張によってアームの最大搭載枠が増え、
 スロットに空き(EMPTY)がある場合、
 MATERIAL を 20 消費 して
 新たな武装アームを瞬時に製造し、自動でマウントします。

・戦闘中は画面上部のゲージ
 (緑:本体HP、水色:DATA進行度、黄緑:MATERIAL貯蔵量)を
 常に意識し、リソースの枯渇に注意して立ち回ってください。

・ダウンロードされる方はこちら。↓

・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>WIRE ARMS - TACTICAL CORE</title>
    <style>
        * { box-sizing: border-box; }
        body { margin: 0; overflow: hidden; background-color: #020810; 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.2s; }
        body.blur canvas { filter: blur(6px) brightness(0.5); }

        /* ── スキャンライン オーバーレイ ── */
        #scanlines {
            position: fixed; top:0; left:0; width:100%; height:100%;
            background: repeating-linear-gradient(to bottom, transparent 0px, transparent 3px, rgba(0,0,0,0.07) 3px, rgba(0,0,0,0.07) 4px);
            pointer-events: none; z-index: 50; animation: scanMove 8s linear infinite;
        }
        @keyframes scanMove { from{background-position:0 0} to{background-position:0 100px} }

        /* ── グリッチ フラッシュ ── */
        #flashOverlay {
            position:fixed; top:0; left:0; width:100%; height:100%;
            background:rgba(0,255,200,0); pointer-events:none; z-index:60;
            transition: background 0.05s;
        }

        /* ── UI ── */
        #ui { position:absolute; top:0; left:0; width:100%; pointer-events:none; z-index:10; opacity:0; transition:opacity 0.4s; padding:12px; }
        .bar-container { display:flex; justify-content:space-between; width:92%; margin:0 auto; gap:10px; }

        .status-box {
            background: linear-gradient(135deg, rgba(0,30,20,0.9) 0%, rgba(0,10,30,0.9) 100%);
            border: 1px solid #0f6; padding:10px 14px; width:42%;
            color:#fff; position:relative; overflow:hidden;
            clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
        }
        .status-box::before {
            content:''; position:absolute; top:0; left:0; width:100%; height:2px;
            background:linear-gradient(90deg,transparent,#0f6,transparent);
            animation: scanBar 3s linear infinite;
        }
        @keyframes scanBar { from{transform:translateX(-100%)} to{transform:translateX(100%)} }
        .status-box.enemy { background:linear-gradient(135deg,rgba(30,5,0,0.9) 0%,rgba(20,0,10,0.9) 100%); border-color:#f33; }
        .status-box.enemy::before { background:linear-gradient(90deg,transparent,#f33,transparent); }

        .entity-name { font-size:13px; font-weight:bold; letter-spacing:3px; color:#0ff; text-shadow:0 0 8px #0ff; margin-bottom:6px; }
        .enemy .entity-name { color:#f55; text-shadow:0 0 8px #f55; }

        .gauge-row { margin-bottom:5px; }
        .gauge-label { font-size:10px; color:#aaa; margin-bottom:2px; display:flex; justify-content:space-between; }
        .gauge-bg { width:100%; height:10px; background:rgba(0,0,0,0.6); border:1px solid #333; position:relative; overflow:hidden; }
        .gauge-fg { height:100%; transition:width 0.15s; position:relative; }
        .gauge-fg::after { content:''; position:absolute; top:0; right:0; width:3px; height:100%; background:rgba(255,255,255,0.8); }
        .hp-fg { background:linear-gradient(90deg,#0a6,#0f6); box-shadow:0 0 8px #0f6; }
        .enemy .hp-fg { background:linear-gradient(90deg,#a00,#f44); box-shadow:0 0 8px #f44; }
        .data-fg { background:linear-gradient(90deg,#006,#0ff); box-shadow:0 0 6px #0ff; }
        .enemy .data-fg { background:linear-gradient(90deg,#600,#f0f); box-shadow:0 0 6px #f0f; }
        .mat-fg { background:linear-gradient(90deg,#260,#af2); box-shadow:0 0 6px #af2; }

        /* ── コマンドメニュー ── */
        #commandMenu {
            position:absolute; bottom:24px; left:50%; transform:translateX(-50%);
            display:flex; flex-direction:row; gap:12px; z-index:10; opacity:0; transition:opacity 0.3s;
        }
        .cmd-item {
            background:rgba(0,15,25,0.9); border:1px solid #0f6; padding:8px 12px;
            font-size:14px; font-weight:bold; width:130px; text-align:center;
            white-space:pre-line; border-radius:2px;
            clip-path:polygon(0 0,calc(100% - 8px) 0,100% 8px,100% 100%,8px 100%,0 calc(100% - 8px));
            transition:all 0.15s;
        }
        .cmd-item.in-use  { border-color:#333; color:#555; }
        .cmd-item.crushed { border-color:#f00; color:#f55; text-decoration:line-through; }
        .cmd-item.empty   { border-color:#222; color:#333; }
        .cmd-item.selected{ transform:scale(1.1) translateY(-4px); box-shadow:0 0 20px currentColor; animation: cmdBlink 0.5s infinite alternate; }
        
        @keyframes cmdBlink {
            from { opacity: 1; }
            to { opacity: 0.6; }
        }

        /* ── ログ ── */
        #log { position:absolute; bottom:110px; right:20px; color:#aaa; pointer-events:none; font-size:12px; line-height:1.7; z-index:10; text-align:right; }

        /* ── センタープロンプト ── */
        #centerPrompt {
            position:absolute; top:38%; width:100%; text-align:center;
            font-size:20px; font-weight:bold; color:#fff; pointer-events:none; z-index:10;
            letter-spacing:2px; text-transform:uppercase;
        }
        .locked-on { color:#ffe000 !important; text-shadow:0 0 15px #ffe000, 0 0 30px #ff8000 !important; animation:promptPulse 1s ease-in-out infinite; }
        .obstructed { color:#ff2200 !important; text-shadow:0 0 15px #ff2200, 0 0 30px #ff0000 !important; animation:promptFlash 0.3s ease-in-out infinite; }
        @keyframes promptPulse { 0%,100%{opacity:1} 50%{opacity:0.7} }
        @keyframes promptFlash { 0%,100%{opacity:1} 50%{opacity:0.3} }

        /* ── ターゲットHUD ── */
        .target-hud { position:absolute; color:#fff; font-size:11px; pointer-events:none; text-align:center; white-space:nowrap; transform:translate(-50%,-100%); display:none; background:rgba(0,0,0,0.7); border:1px solid #0ff; padding:4px 8px; z-index:10; animation:hudBlink 2s linear infinite; }
        .target-hud.enemy-hud { border-color:#f55; color:#f55; }
        @keyframes hudBlink { 0%,90%{opacity:1} 95%{opacity:0.5} 100%{opacity:1} }

        /* ── ステージ ── */
        #stageIndicator { position:absolute; top:12px; left:50%; transform:translateX(-50%); color:#0ff; font-size:16px; text-shadow:0 0 10px #0ff; pointer-events:none; opacity:0; transition:opacity 0.3s; z-index:10; letter-spacing:3px; }
        #stageIndicator.visible { opacity:1; }

        /* ── スクリーン ── */
        .screen { display:none; position:absolute; top:0; left:0; width:100vw; height:100vh; background:rgba(0,3,10,0.88); 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:68px; margin-bottom:16px; letter-spacing:10px; text-shadow:0 0 30px #0ff; animation:titleGlow 2s ease-in-out infinite; }
        @keyframes titleGlow { 0%,100%{text-shadow:0 0 30px #0ff,0 0 60px #0ff} 50%{text-shadow:0 0 10px #0ff,0 0 20px #0ff} }
        .screen h2 { font-size:32px; margin-bottom:16px; color:#0ff; letter-spacing:4px; }
        .screen p  { font-size:20px; color:#aaa; max-width:600px; line-height:1.7; }
        .blink { animation:blinker 1.2s linear infinite; }
        @keyframes blinker { 50%{opacity:0} }

        h2,p { margin:0; padding:0; }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
</head>
<body>

<div id="scanlines"></div>
<div id="flashOverlay"></div>

<div id="ui" style="opacity:0;">
    <div class="bar-container">
        <div class="status-box" id="uiPlayer">
            <div class="entity-name">◈ PLAYER UNIT</div>
            <div class="gauge-row"><div class="gauge-label"><span>HP</span><span id="hpPlayerTxt">1000</span></div><div class="gauge-bg"><div class="gauge-fg hp-fg" id="hpBarPlayer"></div></div></div>
            <div class="gauge-row"><div class="gauge-label"><span>DATA</span><span id="dataPlayerTxt">0</span></div><div class="gauge-bg" style="height:6px;"><div class="gauge-fg data-fg" id="dataBarPlayer"></div></div></div>
            <div class="gauge-row"><div class="gauge-label"><span>MATERIAL</span><span id="matPlayerTxt">0</span></div><div class="gauge-bg" style="height:6px;"><div class="gauge-fg mat-fg" id="matBarPlayer"></div></div></div>
        </div>
        <div class="status-box enemy" id="uiEnemy">
            <div class="entity-name">◈ ENEMY UNIT</div>
            <div class="gauge-row"><div class="gauge-label"><span>HP</span><span id="hpEnemyTxt">300</span></div><div class="gauge-bg"><div class="gauge-fg hp-fg" id="hpBarEnemy"></div></div></div>
            <div class="gauge-row"><div class="gauge-label"><span>DATA</span><span id="dataEnemyTxt">0</span></div><div class="gauge-bg" style="height:6px;"><div class="gauge-fg data-fg" id="dataBarEnemy"></div></div></div>
            <div class="gauge-row"><div class="gauge-label"><span>MATERIAL</span><span id="matEnemyTxt">0</span></div><div class="gauge-bg" style="height:6px;"><div class="gauge-fg mat-fg" id="matBarEnemy"></div></div></div>
        </div>
    </div>
</div>

<div id="commandMenu"></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" style="font-size:18px;letter-spacing:3px;">▶ 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 style="letter-spacing:2px;">敵対プログラムを殲滅せよ</p></div>
<div id="screenPause"      class="screen"><h1 style="color:#ffa500;text-shadow:0 0 20px #fa0;">PAUSED</h1><p class="blink">[ESC] 戦闘再開</p></div>
<div id="screenStageClear" class="screen"><h1 style="color:#0f6;text-shadow:0 0 30px #0f6;">STAGE CLEAR</h1><p>敵データの残滓を吸収...<br>次フェーズへ移行します。</p></div>
<div id="screenGameOver"   class="screen"><h1 style="color:#f44;text-shadow:0 0 30px #f00;">GAME OVER</h1><p>機体大破。接続をロストしました。</p><p class="blink" style="margin-top:20px;font-size:16px;letter-spacing:3px;">▶ CLICK TO RETRY</p></div>
<div id="screenGameClear"  class="screen"><h1 style="color:#f0f;text-shadow:0 0 30px #f0f;">ALL CLEARED</h1><p>MISSION ACCOMPLISHED.<br>全脅威の排除を確認しました。</p><p class="blink" style="margin-top:20px;font-size:16px;letter-spacing:3px;">▶ CLICK TO RESTART</p></div>

<script>
// ==========================================
// 0. ゲーム状態管理 & コマンドカラー定義
// ==========================================
let gameState = 'TITLE';
let currentStage = 1;
const maxStages = 5;
let enemies = [];
let accumulatedEnemyData = 0;
let accumulatedEnemyMat  = 0;

const CMD_COLORS     = [0x00bfff, 0x32cd32, 0xffd700, 0xda70d6];
const CMD_COLORS_CSS = ['#00bfff', '#32cd32', '#ffd700', '#da70d6'];

const playerTactics = {
    routes: [], lines: [], target: null, interceptBit: null,
    selectedIndex: null, updateTimer: 0
};

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 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');
        document.getElementById('targetHud').style.display = 'none';
        body.classList.add('blur');
        document.getElementById('centerPrompt').innerText = '';
        playerTactics.selectedIndex = null;
    }

    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 = 4;
        player.arms = [{ id:'ARM1', hp:100, maxHp:100 }, { id:'ARM2', hp:100, maxHp:100 }, { id:'ARM3', hp:100, maxHp:100 }, { id:'ARM4', 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 now = audioCtx.currentTime;

    const mkOsc = (t, freq, endFreq, dur, gainVal, waveType = 'sawtooth') => {
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.connect(gain); gain.connect(audioCtx.destination);
        osc.type = waveType;
        osc.frequency.setValueAtTime(freq, now);
        if (endFreq) osc.frequency.exponentialRampToValueAtTime(endFreq, now + dur);
        gain.gain.setValueAtTime(gainVal, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
        osc.start(now); osc.stop(now + dur);
    };

    if (type === 'attack')    { mkOsc(0, 300, 80, 0.15, 0.25); }
    else if (type === 'laser') {
        mkOsc(0, 1800, 300, 0.22, 0.35, 'sine');
        mkOsc(0, 900, 200, 0.22, 0.15, 'sawtooth');
    }
    else if (type === 'hit') {
        mkOsc(0, 120, 20, 0.35, 0.6);
        mkOsc(0, 200, 40, 0.2, 0.3, 'square');
    }
    else if (type === 'bigHit') {
        mkOsc(0, 80, 15, 0.5, 0.9);
        mkOsc(0, 300, 50, 0.3, 0.4, 'square');
    }
    else if (type === 'craft')  { mkOsc(0, 600, 1200, 0.3, 0.4, 'square'); }
    else if (type === 'crush')  { mkOsc(0, 50, 10, 0.4, 0.8); }
    else if (type === 'intercept') { mkOsc(0, 1500, 500, 0.1, 0.3, 'square'); }
    else if (type === 'menu')   { mkOsc(0, 800, 1200, 0.05, 0.2, 'sine'); }
    else if (type === 'death') {
        mkOsc(0, 400, 30, 0.8, 0.6);
        mkOsc(0, 200, 15, 1.0, 0.4, 'square');
    }
}

// ==========================================
// 2. 画面演出(フラッシュ・カメラシェイク)
// ==========================================
const flashOverlay = document.getElementById('flashOverlay');
let flashTimer = 0;

function triggerFlash(color = 'rgba(0,255,180,0.35)', duration = 0.12) {
    flashOverlay.style.background = color;
    flashTimer = duration;
}

let cameraShake = { active:false, timer:0, intensity:0 };
const cameraBasePos = new THREE.Vector3(0, 50, 60);
function triggerCameraShake(intensity = 1.5, duration = 0.25) {
    cameraShake.active    = true;
    cameraShake.timer     = duration;
    cameraShake.intensity = intensity;
}

function updateCameraShake(dt) {
    if (!cameraShake.active) return;
    cameraShake.timer -= dt;
    if (cameraShake.timer <= 0) {
        cameraShake.active = false;
        camera.position.copy(cameraBasePos);
        return;
    }
    const ratio = cameraShake.timer / 0.25;
    const s = cameraShake.intensity * ratio;
    camera.position.set(
        cameraBasePos.x + (Math.random()-0.5)*s,
        cameraBasePos.y + (Math.random()-0.5)*s*0.5,
        cameraBasePos.z + (Math.random()-0.5)*s
    );
}

// ==========================================
// 3. 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 fe = enemies.find(e => e.hp > 0) || enemies[0];
        if (fe) {
            document.getElementById('hpBarEnemy').style.width   = Math.max(0,(fe.hp/fe.maxHp)*100)+'%';
            document.getElementById('hpEnemyTxt').innerText     = Math.floor(fe.hp);
            document.getElementById('dataEnemyTxt').innerText   = fe.data;
            document.getElementById('dataBarEnemy').style.width = Math.min(100,((fe.data%200)/200)*100)+'%';
            document.getElementById('matEnemyTxt').innerText    = fe.materials;
            document.getElementById('matBarEnemy').style.width  = Math.min(100,(fe.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 = '';
        for (let i = 0; i < 4; i++) {
            const arm = player.arms[i];
            const key = keyMap[i];
            const div = document.createElement('div');
            const colorCss = CMD_COLORS_CSS[i];
            div.className = 'cmd-item';
            if (!arm) {
                div.className += ' empty'; div.innerText = `[${key}]\nEMPTY`;
            } else if (arm.hp <= 0) {
                div.className += ' crushed'; div.innerText = `[${key}]\nCRUSHED`;
            } else if (bits.find(b => b.owner === player && b.armId === arm.id)) {
                div.className += ' in-use'; div.innerText = `[${key}]\nIN USE`;
            } else {
                if (playerTactics.selectedIndex === i) {
                    div.className += ' selected';
                    div.style.cssText = `border-color:${colorCss};color:#fff;background-color:${colorCss}20;text-shadow:0 0 10px ${colorCss};box-shadow:0 0 20px ${colorCss};`;
                } else {
                    div.style.cssText = `border-color:${colorCss};color:${colorCss};text-shadow:0 0 5px ${colorCss};`;
                }
                const modeText = playerTactics.interceptBit ? 'GUARD' : 'ATTACK';
                const routeName = playerTactics.routes[i] ? playerTactics.routes[i].name : modeText;
                div.innerText = `[${key}]\n${routeName}`;
            }
            container.appendChild(div);
        }
    }
};

// ── フローティングテキスト ──
const floatingTexts = [];
function createFloatingText(text, pos, color, size='24px') {
    const div = document.createElement('div');
    div.innerText = text;
    Object.assign(div.style, {
        position:'absolute', color, fontWeight:'bold',
        textShadow:`0 0 8px ${color}, 0 0 15px ${color}`,
        pointerEvents:'none', fontSize:size, zIndex:20,
        fontFamily:"'Courier New',monospace", letterSpacing:'2px'
    });
    document.body.appendChild(div);
    floatingTexts.push({ el:div, pos:pos.clone(), life:1.8 });
}
function updateFloatingTexts(dt) {
    for (let i = floatingTexts.length-1; i >= 0; i--) {
        const ft = floatingTexts[i]; ft.life -= dt;
        if (ft.life <= 0) { ft.el.remove(); floatingTexts.splice(i,1); continue; }
        ft.pos.y += dt * 12;
        const proj = ft.pos.clone().project(camera);
        ft.el.style.left    = `calc(${(proj.x*0.5+0.5)*window.innerWidth}px - 30px)`;
        ft.el.style.top     = `${(proj.y*-0.5+0.5)*window.innerHeight}px`;
        ft.el.style.opacity = Math.min(1, ft.life);
        ft.el.style.transform = `scale(${0.7+ft.life*0.15})`;
    }
}

function updateTargetHud() {
    const targetHud = document.getElementById('targetHud');
    const target = playerTactics.target;
    if (target && target.hp > 0 && gameState === 'PLAYING') {
        const proj = target.pos.clone().add(new THREE.Vector3(0,10,0)).project(camera);
        targetHud.style.left    = `${(proj.x*0.5+0.5)*window.innerWidth}px`;
        targetHud.style.top     = `${(proj.y*-0.5+0.5)*window.innerHeight}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';
    }
}

// ==========================================
// 4. Three.js セットアップ
// ==========================================
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x020810, 0.013);

const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.copy(cameraBasePos);
camera.lookAt(0,0,0);

const renderer = new THREE.WebGLRenderer({ antialias:true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x020810, 1.0);
document.body.appendChild(renderer.domElement);

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth/window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

scene.add(new THREE.AmbientLight(0x304060, 2.5));
const dirLight = new THREE.DirectionalLight(0x80c0ff, 1.8);
dirLight.position.set(20,50,20); scene.add(dirLight);
const rimLight = new THREE.DirectionalLight(0x002040, 0.8);
rimLight.position.set(-20,10,-30); scene.add(rimLight);

const gridHelper = new THREE.GridHelper(100, 25, 0x003366, 0x001122);
scene.add(gridHelper);

const groundGeo = new THREE.PlaneGeometry(100,100);
const groundMat = new THREE.MeshBasicMaterial({ color:0x010812, transparent:true, opacity:0.6 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI/2; ground.position.y = 0.01; scene.add(ground);

const blocks = [];
const blockGeo = new THREE.BoxGeometry(8, 10, 8);
const fragments = [];

function generateBlocks() {
    blocks.forEach(b => scene.remove(b)); blocks.length = 0;
    const hue = Math.random();
    const blockMat = new THREE.MeshBasicMaterial({ color:new THREE.Color().setHSL(hue, 0.9, 0.45), 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;
            const mesh = new THREE.Mesh(blockGeo, blockMat);
            mesh.position.set(x, 5, z); scene.add(mesh); blocks.push(mesh);
        }
    }
}

function checkCollision(pos, radius=2.5) {
    for (const b of blocks) {
        const expand = radius;
        if (pos.x > b.position.x - 4 - expand &&
            pos.x < b.position.x + 4 + expand &&
            pos.z > b.position.z - 4 - expand &&
            pos.z < b.position.z + 4 + expand) {
            return true;
        }
    }
    return false;
}

// ==========================================
// 5. 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=1.5) {
    let s = worldToGrid(startPos.x, startPos.z);
    let e = worldToGrid(endPos.x, endPos.z);
    s.c = Math.max(0,Math.min(PATH_GRID_W-1,s.c)); s.r = Math.max(0,Math.min(PATH_GRID_W-1,s.r));
    e.c = Math.max(0,Math.min(PATH_GRID_W-1,e.c)); e.r = Math.max(0,Math.min(PATH_GRID_W-1,e.r));
    let open = [{c:s.c,r:s.r,g:0,h:Math.hypot(s.c-e.c,s.r-e.r),parent:null}];
    let closed = new Set();
    const key = (c,r) => `${c},${r}`;
    while (open.length > 0) {
        open.sort((a,b)=>(a.g+a.h)-(b.g+b.h));
        const cur = open.shift();
        if (cur.c===e.c && cur.r===e.r) {
            const path=[]; let c=cur;
            while(c){ path.push(gridToWorld(c.c,c.r)); c=c.parent; }
            path.reverse(); path[0]=startPos.clone(); path[path.length-1]=endPos.clone();
            return path;
        }
        closed.add(key(cur.c,cur.r));
        for (const d of [[0,1],[1,0],[0,-1],[-1,0],[1,1],[1,-1],[-1,1],[-1,-1]]) {
            const nc=cur.c+d[0], nr=cur.r+d[1];
            if (nc<0||nc>=PATH_GRID_W||nr<0||nr>=PATH_GRID_W) continue;
            if (closed.has(key(nc,nr))) continue;
            if (checkCollision(gridToWorld(nc,nr),radius)) { if(nc!==e.c||nr!==e.r) continue; }
            const g = cur.g + (d[0]!==0&&d[1]!==0 ? 1.414 : 1);
            const ex = open.find(n=>n.c===nc&&n.r===nr);
            if (ex) { if(g<ex.g){ex.g=g;ex.parent=cur;} }
            else open.push({c:nc,r:nr,g,h:Math.hypot(nc-e.c,nr-e.r),parent:cur});
        }
    }
    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,1.5) || [start.clone(),target.clone()];
    routes.push({path:direct, name:'DIRECT'});
    const dir = target.clone().sub(start).normalize();
    const L = new THREE.Vector3(-dir.z,0,dir.x);
    const R = new THREE.Vector3(dir.z,0,-dir.x);
    const lMid  = start.clone().lerp(target,0.5).add(L.clone().multiplyScalar(25));
    const rMid  = start.clone().lerp(target,0.5).add(R.clone().multiplyScalar(25));
    const wMid  = start.clone().lerp(target,0.4).add(L.clone().multiplyScalar(40));
    const pL1=findPath(start,lMid,1.5)||[start.clone(),lMid.clone()];
    const pL2=findPath(lMid,target,1.5)||[lMid.clone(),target.clone()];
    routes.push({path:pL1.concat(pL2.slice(1)),name:'FLANK_L'});
    const pR1=findPath(start,rMid,1.5)||[start.clone(),rMid.clone()];
    const pR2=findPath(rMid,target,1.5)||[rMid.clone(),target.clone()];
    routes.push({path:pR1.concat(pR2.slice(1)),name:'FLANK_R'});
    const pW1=findPath(start,wMid,1.5)||[start.clone(),wMid.clone()];
    const pW2=findPath(wMid,target,1.5)||[wMid.clone(),target.clone()];
    routes.push({path:pW1.concat(pW2.slice(1)),name:'WIDE_ARC'});
    return routes;
}

// ==========================================
// 6. 戦術ルート表示
// ==========================================
function updatePlayerTrajectories(interceptTarget, attackTarget) {
    playerTactics.routes = [];
    const sp = player.pos.clone().setY(5);
    if (interceptTarget) {
        const dest = interceptTarget.pos.clone().setY(5);
        for (let i=0; i<4; i++) playerTactics.routes.push({path:[sp,dest], name:'INTERCEPT'});
        playerTactics.target = interceptTarget.owner || null;
        playerTactics.interceptBit = interceptTarget;
    } else if (attackTarget) {
        playerTactics.routes = generateMultiPaths(sp, attackTarget.pos, player.data, attackTarget.data);
        playerTactics.target = attackTarget;
        playerTactics.interceptBit = null;
    } else {
        playerTactics.target = null;
        playerTactics.interceptBit = null;
    }
}

function drawPlayerTrajectories() {
    while (playerTactics.lines.length < 4) {
        const mat  = new THREE.LineDashedMaterial({dashSize:1.5,gapSize:0.5,transparent:true,opacity:0.5});
        const line = new THREE.Line(new THREE.BufferGeometry(), mat);
        scene.add(line); playerTactics.lines.push(line);
    }
    for (let i=0;i<4;i++) {
        const line  = playerTactics.lines[i];
        const arm   = player.arms[i];
        const route = playerTactics.routes[i];
        const inUse = bits.find(b=>b.owner===player&&b.armId===(arm?arm.id:''));
        if (!arm||arm.hp<=0||inUse||!route) { line.visible=false; continue; }
        line.visible=true;
        line.geometry.setFromPoints(route.path.map(p=>p.clone().setY(5)));
        line.computeLineDistances();
        line.material.color.setHex(CMD_COLORS[i]);
        const t = performance.now()*0.001;
        if (playerTactics.selectedIndex===i) {
            line.material.opacity = 0.6 + Math.sin(t*6)*0.4;
        } else if (playerTactics.selectedIndex!==null) {
            line.material.opacity = 0.12;
        } else {
            line.material.opacity = 0.45 + Math.sin(t*2+i)*0.1;
        }
    }
}

function clearRouteLines() {
    for (const l of playerTactics.lines) { scene.remove(l); l.geometry.dispose(); l.material.dispose(); }
    playerTactics.lines = [];
}

// ==========================================
// 7. パーティクル & 残像
// ==========================================
const particles = [];

function createHitParticles(pos, color, count=22, speed=22) {
    for (let i=0;i<count;i++) {
        const size = 0.2 + Math.random()*0.3;
        const geo  = Math.random()<0.5 ? new THREE.OctahedronGeometry(size,0) : new THREE.SphereGeometry(size,4,4);
        const mesh = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({color,wireframe:true}));
        mesh.position.copy(pos); scene.add(mesh);
        const vel = new THREE.Vector3(
            (Math.random()-0.5)*speed,
            Math.random()*speed*0.8+4,
            (Math.random()-0.5)*speed
        );
        particles.push({mesh, vel, life:0.6+Math.random()*0.6, maxLife:1.2, spin:new THREE.Vector3(Math.random()*8,Math.random()*8,Math.random()*8)});
    }
}

function createExplosionRing(pos, color) {
    const geo  = new THREE.TorusGeometry(0.1, 0.15, 6, 16);
    const mat  = new THREE.MeshBasicMaterial({color, wireframe:true, transparent:true, opacity:1});
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.copy(pos);
    mesh.rotation.x = Math.PI/2;
    scene.add(mesh);
    particles.push({mesh, vel:new THREE.Vector3(0,0,0), life:0.5, maxLife:0.5, spin:new THREE.Vector3(0,0,0), ring:true, ringGrow:20});
}

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; }
        const ratio = p.life/p.maxLife;
        if (p.ring) {
            const scale = 1 + (1-ratio)*p.ringGrow;
            p.mesh.scale.set(scale,scale,scale);
            p.mesh.material.opacity = ratio*0.8;
        } else {
            p.vel.y -= 25*dt;
            p.mesh.position.addScaledVector(p.vel, dt);
            p.mesh.rotation.x += p.spin.x*dt;
            p.mesh.rotation.y += p.spin.y*dt;
            p.mesh.rotation.z += p.spin.z*dt;
            p.mesh.material.opacity = ratio;
            p.mesh.material.transparent = true;
        }
    }
}

const afterImages = [];
function createAfterImage(robotGroup, color, opacity=0.5) {
    const cg = new THREE.Group();
    robotGroup.traverse(child => {
        if (child.isMesh) {
            const clone = new THREE.Mesh(child.geometry, new THREE.MeshBasicMaterial({color, wireframe:true, transparent:true, opacity}));
            clone.position.copy(child.getWorldPosition(new THREE.Vector3()));
            clone.quaternion.copy(child.getWorldQuaternion(new THREE.Quaternion()));
            clone.scale.copy(child.getWorldScale(new THREE.Vector3()));
            cg.add(clone);
        }
    });
    scene.add(cg);
    afterImages.push({group:cg, life:0.3, maxLife:0.3});
}
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.5;
        ai.group.traverse(c=>{ if(c.isMesh) c.material.opacity=alpha; });
    }
}

// ==========================================
// 8. ビット(飛翔体)
// ==========================================
const bits = [];
class Bit {
    constructor(startPos, path, color, owner, targetObj, armId) {
        this.owner=owner; this.targetObj=targetObj; this.path=path;
        this.pathIndex=0; this.speed=120; 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.ConeGeometry(0.6, 4.5, 4);
        const mat = new THREE.MeshBasicMaterial({color, wireframe:true});
        this.mesh = new THREE.Mesh(geo, mat);
        this.mesh.position.copy(this.pos); scene.add(this.mesh);

        this.light = new THREE.PointLight(color, 3, 10);
        this.mesh.add(this.light);

        const pts = this.path.map(p=>p.clone().setY(5));
        this.trajectoryLine = new THREE.Line(
            new THREE.BufferGeometry().setFromPoints(pts),
            new THREE.LineDashedMaterial({color, dashSize:1, gapSize:0.5, transparent:true, opacity:0.7})
        );
        this.trajectoryLine.computeLineDistances(); scene.add(this.trajectoryLine);
    }

    update(dt) {
        this.life -= dt;

        if (this.bitState==='OUTBOUND' && Math.random()<0.3) {
            const tPos = this.pos.clone().add(new THREE.Vector3((Math.random()-0.5)*0.5,0,(Math.random()-0.5)*0.5));
            const tMesh = new THREE.Mesh(new THREE.OctahedronGeometry(0.15,0), new THREE.MeshBasicMaterial({color:this.color,wireframe:true,transparent:true,opacity:0.7}));
            tMesh.position.copy(tPos); scene.add(tMesh);
            particles.push({mesh:tMesh,vel:new THREE.Vector3(0,1,0),life:0.15,maxLife:0.15,spin:new THREE.Vector3(0,5,0)});
        }

        if (this.bitState==='OUTBOUND') {
            if (this.isInterceptor && this.targetObj && this.targetObj.life>0) {
                const dir = new THREE.Vector3().subVectors(this.targetObj.pos, this.pos).normalize();
                this.pos.addScaledVector(dir, this.speed*dt);
                this.mesh.position.copy(this.pos);
                this.mesh.quaternion.slerp(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0), dir), 0.3);
                if (this.pos.distanceTo(this.targetObj.pos)<3.0) this.fireLaser();
            } else if (this.path && this.pathIndex < this.path.length-1) {
                const tp  = this.path[this.pathIndex+1].clone().setY(5);
                const dir = new THREE.Vector3().subVectors(tp, this.pos).normalize();
                const md  = this.speed*dt;
                const rem = this.pos.distanceTo(tp);
                if (rem<=md) { this.pos.copy(tp); this.pathIndex++; }
                else { this.pos.addScaledVector(dir, md); }
                this.mesh.position.copy(this.pos);
                this.mesh.quaternion.slerp(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,1,0), dir), 0.3);
                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 tp  = this.owner.pos.clone().setY(5);
            const dir = new THREE.Vector3().subVectors(tp, this.pos).normalize();
            const md  = this.speed*4.0*dt;
            const rem = this.pos.distanceTo(tp);
            if (rem<md||rem<2.0) { this.destroy(); return true; }
            this.pos.addScaledVector(dir, md);
            this.mesh.position.copy(this.pos);
            this.mesh.quaternion.slerp(new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,-1,0), dir), 0.3);
        }
        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 pts=[this.pos.clone(),this.targetObj.pos.clone()];
                this.laserLine=new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),new THREE.LineBasicMaterial({color:this.color}));
                scene.add(this.laserLine);
                createHitParticles(this.targetObj.pos,0xffaa00,12,15);
                createExplosionRing(this.targetObj.pos,0xffaa00);
                createFloatingText('INTERCEPTED!',this.targetObj.pos.clone().add(new THREE.Vector3(0,5,0)),'#ffaa00');
                triggerFlash('rgba(255,180,0,0.25)',0.1);
                this.targetObj.destroy();
            }
        } else {
            if (!this.targetObj||this.targetObj.hp<=0) { this.laserTimer=0.1; return; }
            const pts=[this.pos.clone(),this.targetObj.pos.clone().setY(3)];
            this.laserLine=new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),new THREE.LineBasicMaterial({color:this.color,linewidth:2}));
            scene.add(this.laserLine);
            this.owner.resolveAttack(this.targetObj);
            createHitParticles(this.targetObj.pos.clone().setY(3),this.color,22,24);
            createExplosionRing(this.targetObj.pos.clone().setY(3),this.color);
        }
        this.laserTimer=0.35;
    }

    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--) {
        if (bits[i].update(dt)) { bits[i].destroy(); bits.splice(i,1); }
    }
}

// ==========================================
// 9. 多腕メカモデル生成(ローポリ・鋭角化)
// ==========================================
function createWireframeRobot(color) {
    const g = new THREE.Group();
    const mat = () => new THREE.MeshBasicMaterial({color, wireframe:true});
    const parts = {};
    g.userData.parts = parts;

    // ── 胴体 ──
    const torso = new THREE.Group();
    g.add(torso);
    parts.torso = torso;

    // 鋭角的な胴体
    const torsoMesh = new THREE.Mesh(new THREE.CylinderGeometry(1.8, 0.6, 4.5, 4), mat());
    torsoMesh.rotation.y = Math.PI / 4; 
    torso.add(torsoMesh);

    // 胸部装甲
    const chestArmor = new THREE.Mesh(new THREE.ConeGeometry(1.5, 2.5, 4), mat());
    chestArmor.rotation.x = Math.PI / 2;
    chestArmor.rotation.y = Math.PI / 4;
    chestArmor.position.set(0, 0.5, 1.2);
    torso.add(chestArmor);

    // ── 頭 ──
    const head = new THREE.Group();
    head.position.set(0, 3.0, 0);
    torso.add(head);
    parts.head = head;
    
    const headMesh = new THREE.Mesh(new THREE.ConeGeometry(1.0, 1.8, 4), mat());
    headMesh.rotation.y = Math.PI / 4;
    head.add(headMesh);
    
    // 角・バイザー
    const horn = new THREE.Mesh(new THREE.ConeGeometry(0.3, 1.5, 3), mat());
    horn.rotation.x = -Math.PI / 4;
    horn.position.set(0, 0.5, 0.8);
    head.add(horn);

    // ── 脚 ──
    parts.lLeg = new THREE.Group(); parts.lLeg.position.set(-1.2, -2.0, 0); torso.add(parts.lLeg);
    const lThigh = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.4, 2.8, 4), mat()); 
    lThigh.position.set(0, -1.4, 0); lThigh.rotation.y = Math.PI/4; parts.lLeg.add(lThigh);
    parts.lKnee = new THREE.Group(); parts.lKnee.position.set(0, -2.8, 0); parts.lLeg.add(parts.lKnee);
    const lShin = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.2, 3.0, 4), mat()); 
    lShin.position.set(0, -1.5, 0); lShin.rotation.y = Math.PI/4; parts.lKnee.add(lShin);

    parts.rLeg = new THREE.Group(); parts.rLeg.position.set(1.2, -2.0, 0); torso.add(parts.rLeg);
    const rThigh = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.4, 2.8, 4), mat()); 
    rThigh.position.set(0, -1.4, 0); rThigh.rotation.y = Math.PI/4; parts.rLeg.add(rThigh);
    parts.rKnee = new THREE.Group(); parts.rKnee.position.set(0, -2.8, 0); parts.rLeg.add(parts.rKnee);
    const rShin = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.2, 3.0, 4), mat()); 
    rShin.position.set(0, -1.5, 0); rShin.rotation.y = Math.PI/4; parts.rKnee.add(rShin);

    // ── 通常腕 ──
    parts.lArm = new THREE.Group(); parts.lArm.position.set(-2.2, 1.5, 0); torso.add(parts.lArm);
    const lUA = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.4, 2.5, 4), mat()); 
    lUA.position.set(0, -1.25, 0); lUA.rotation.y = Math.PI/4; parts.lArm.add(lUA);
    const lShoulder = new THREE.Mesh(new THREE.ConeGeometry(1.2, 2.0, 4), mat());
    lShoulder.position.set(0, 0.5, 0); parts.lArm.add(lShoulder);
    parts.lElbow = new THREE.Group(); parts.lElbow.position.set(0, -2.5, 0); parts.lArm.add(parts.lElbow);
    const lFA = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.1, 2.5, 4), mat()); 
    lFA.position.set(0, -1.25, 0); lFA.rotation.y = Math.PI/4; parts.lElbow.add(lFA);

    parts.rArm = new THREE.Group(); parts.rArm.position.set(2.2, 1.5, 0); torso.add(parts.rArm);
    const rUA = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.4, 2.5, 4), mat()); 
    rUA.position.set(0, -1.25, 0); rUA.rotation.y = Math.PI/4; parts.rArm.add(rUA);
    const rShoulder = new THREE.Mesh(new THREE.ConeGeometry(1.2, 2.0, 4), mat());
    rShoulder.position.set(0, 0.5, 0); parts.rArm.add(rShoulder);
    parts.rElbow = new THREE.Group(); parts.rElbow.position.set(0, -2.5, 0); parts.rArm.add(parts.rElbow);
    const rFA = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.1, 2.5, 4), mat()); 
    rFA.position.set(0, -1.25, 0); rFA.rotation.y = Math.PI/4; parts.rElbow.add(rFA);

    // ── 背中の4本腕(飛翔ブレード) ──
    parts.backArms = [];
    const backArmPos = [
        { x: -1.2, y: 1.5, z: -1.5, rx: 0.5, ry: -0.5, rz: 0.6 }, // Top Left
        { x:  1.2, y: 1.5, z: -1.5, rx: 0.5, ry:  0.5, rz:-0.6 }, // Top Right
        { x: -1.0, y: -0.5, z: -1.5, rx: 0.2, ry: -0.4, rz: 0.9 }, // Bottom Left
        { x:  1.0, y: -0.5, z: -1.5, rx: 0.2, ry:  0.4, rz:-0.9 }  // Bottom Right
    ];
    for (let i = 0; i < 4; i++) {
        const baPiv = new THREE.Group();
        baPiv.position.set(backArmPos[i].x, backArmPos[i].y, backArmPos[i].z);
        baPiv.rotation.set(backArmPos[i].rx, backArmPos[i].ry, backArmPos[i].rz);
        torso.add(baPiv);

        // 鋭利なブレード状のアーム
        const blade = new THREE.Mesh(new THREE.ConeGeometry(0.6, 5.0, 4), mat());
        blade.position.set(0, 2.5, 0);
        baPiv.add(blade);
        parts.backArms.push(baPiv);
    }

    scene.add(g);
    return g;
}

function createShieldMesh() {
    const mesh = new THREE.Mesh(
        new THREE.SphereGeometry(6.0,12,12),
        new THREE.MeshBasicMaterial({color:0x00ffff, transparent:true, opacity:0.22, wireframe:true})
    );
    mesh.visible=false; scene.add(mesh); return mesh;
}

// ==========================================
// 10. Entity クラス
// ==========================================
class Entity {
    constructor(color, x, z, isPlayer) {
        this.isPlayer=isPlayer; this.color=color;
        this.pos=new THREE.Vector3(x,0,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;
        
        // プレイヤーは初期から4本腕
        this.maxArms = isPlayer ? 4 : 2;
        this.arms = isPlayer 
            ? [{id:'ARM1',hp:100,maxHp:100},{id:'ARM2',hp:100,maxHp:100},{id:'ARM3',hp:100,maxHp:100},{id:'ARM4',hp:100,maxHp:100}]
            : [{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.activeArmColor=null;
        this.afterImageTimer=0; this.walkCycle=0;
        this.aiActionTimer=isPlayer?0:2.0; this.moveTarget=new THREE.Vector3(x,0,z);

        this.staggerTimer  = 0;   
        this.staggerDir    = new THREE.Vector3();
        this.launchTimer   = 0;   
        this.launchArmId   = null;
        this.deathTimer    = -1;  
        this.isDeadDone    = false;
    }

    get dataLevel() { return Math.floor(this.data/100); }
    isDataSuperior(o) { return this.data>o.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, (this.isPlayer ? 4 : 2) + Math.floor(this.data/200));
        for (const 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 nid='ARM'+(this.arms.length+1);
            this.arms.push({id:nid,hp:100,maxHp:100});
            if (this.isPlayer) game.addMessage(`SYSTEM: [${nid}] BUILT.`);
            playSound('craft');
        }
    }

    fireBit(targetEntity, preCalcPath, armObj, customColor) {
        if (!targetEntity||targetEntity.hp<=0||!armObj) return;
        const sp  = this.pos.clone().add(new THREE.Vector3(0,5,0));
        const path= preCalcPath||generateMultiPaths(sp,targetEntity.pos,this.data,targetEntity.data)[0].path;
        bits.push(new Bit(sp,path,customColor||this.color,this,targetEntity,armObj.id));
        playSound('attack');
    }

    update(dt, opponents) {
        if (this.deathTimer >= 0) {
            this.deathTimer += dt;
            this._animateDeath(this.deathTimer);
            return;
        }

        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)),'#0f6');
            }
        }

        if (this.targetFacing.lengthSq()>0.001) {
            this.facing.lerp(this.targetFacing,0.14); 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);

        // よろけ移動
        if (this.staggerTimer>0) {
            this.staggerTimer-=dt;
            const sx = this.pos.x + this.staggerDir.x * dt * 15;
            const sz = this.pos.z + this.staggerDir.z * dt * 15;
            const testX = new THREE.Vector3(sx, this.pos.y, this.pos.z);
            const testZ = new THREE.Vector3(this.pos.x, this.pos.y, sz);
            if (!checkCollision(testX, this.radius) && sx>=-45 && sx<=45) this.pos.x = sx;
            if (!checkCollision(testZ, this.radius) && sz>=-45 && sz<=45) this.pos.z = sz;
        }

        this.shieldMesh.position.copy(this.pos).add(new THREE.Vector3(0,5,0));
        this.shieldMesh.visible=(this.state==='guarding');
        if (this.state==='guarding') this.shieldMesh.rotation.y+=dt*4;

        if (this.state==='windup'||this.state==='guarding'||this.staggerTimer>0) {
            this.afterImageTimer-=dt;
            if (this.afterImageTimer<=0) {
                createAfterImage(this.robotGroup, this.color, this.staggerTimer>0?0.4:0.5);
                this.afterImageTimer=0.035;
            }
        } 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,this.activeArmColor);
                this.launchTimer=0.35;
                this.launchArmId=this.activeArm ? this.activeArm.id : 'ARM1';
            }
        } else if (this.state==='attacking'||this.state==='guarding') {
            this.timer-=dt;
            if (this.timer<=0) this.state='idle';
        }
        if (this.launchTimer>0) this.launchTimer-=dt;
    }

    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') {
            const gc=Math.max(0.1,Math.min(1.0, 0.5+((defender.data/100)*0.1)-((this.data/100)*0.05)));
            if (Math.random()<gc) {
                isHit=false; defender.data+=100;
                createFloatingText('GUARD!',defender.pos.clone().add(new THREE.Vector3(0,4,0)),'#0ff','28px');
                triggerFlash('rgba(0,200,255,0.2)',0.08);
            } else {
                createFloatingText('GUARD BREAK!',defender.pos.clone().add(new THREE.Vector3(0,4,0)),'#f80','26px');
            }
        }
        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)),'#fa0','28px');
                playSound('crush');
                if (hitArm.hp<=0) createFloatingText(`[${hitArm.id}] CRUSHED!`,defender.pos.clone().add(new THREE.Vector3(0,6,0)),'#f00','32px');
            } else {
                defender.hp-=dmg;
                playSound('bigHit');
                createFloatingText(`-${dmg}`,defender.pos.clone().add(new THREE.Vector3(0,4,0)),'#f44','40px');
                const kbDir=new THREE.Vector3().subVectors(defender.pos,this.pos).normalize();
                defender.staggerTimer=0.35;
                defender.staggerDir.copy(kbDir);
                triggerCameraShake(1.8, 0.25);
            }
            triggerFlash('rgba(255,60,0,0.3)',0.1);
            if (defender.hp<=0) {
                defender.hp=0;
                defender.deathTimer=0;
                playSound('death');
                triggerCameraShake(3.0, 0.5);
                triggerFlash('rgba(255,0,0,0.5)',0.2);
                createHitParticles(defender.pos.clone().add(new THREE.Vector3(0,4,0)),0xff4400,40,28);
                createExplosionRing(defender.pos.clone().add(new THREE.Vector3(0,2,0)),defender.color);
                createExplosionRing(defender.pos.clone().add(new THREE.Vector3(0,5,0)),0xffffff);
            }
        }
        this.checkCrafting(); defender.checkCrafting(); game.updateUI();
    }

    _animateDeath(t) {
        const parts = this.robotGroup.userData.parts;
        if (!parts) return;

        const fallAngle = Math.min(Math.PI * 0.5, t * 2.5);
        parts.torso.rotation.x = fallAngle;
        parts.torso.position.y = 6.0 - Math.sin(fallAngle) * 5;

        parts.lLeg.rotation.x = -Math.min(1.0, t * 2);
        parts.rLeg.rotation.x = -Math.min(1.0, t * 2);
        parts.lKnee.rotation.x = Math.min(1.5, t * 3);
        parts.rKnee.rotation.x = Math.min(1.5, t * 3);

        parts.lArm.rotation.set(-Math.min(1.5, t*2), 0, 0);
        parts.rArm.rotation.set(-Math.min(1.5, t*2), 0, 0);

        if (t > 0.8 && !this.isDeadDone) {
            this.isDeadDone = true;
            this.shieldMesh.visible = false;
        }
    }

    animateRobot(dt, speed) {
        const parts = this.robotGroup.userData.parts;
        if (!parts) return;

        const isRunning = speed > 5;
        this.walkCycle += dt * (isRunning ? 14 : 2.5);
        const w = Math.sin(this.walkCycle);
        const wAbs = Math.abs(w);

        // 基準高(地面に埋まらないように保持)
        let baseHeight = 6.2;

        // ── ダメージによるのけぞり ──
        if (this.staggerTimer > 0) {
            const sRatio = this.staggerTimer / 0.35;
            parts.torso.rotation.x = -0.6 * sRatio; // 後ろへ大きくのけぞる
            parts.torso.position.y = baseHeight - (1.0 * sRatio);
            parts.lArm.rotation.set(-0.5, 0, 0.8);
            parts.rArm.rotation.set(-0.5, 0, -0.8);
            parts.head.rotation.x = -0.5 * sRatio;
            return;
        }

        // ── ガード姿勢 ──
        if (this.state === 'guarding') {
            parts.torso.rotation.x = 0.2;
            parts.torso.position.y = baseHeight - 0.5;
            parts.lArm.rotation.set(0.5, 0, 0.5);
            parts.lElbow.rotation.set(-1.0, 0, 0);
            parts.rArm.rotation.set(0.5, 0, -0.5);
            parts.rElbow.rotation.set(-1.0, 0, 0);
            return;
        }

        // ── 走行・アイドル(胴体・脚・通常腕) ──
        const bobY = isRunning ? Math.max(0, Math.sin(this.walkCycle * 2)) * 0.5 : wAbs * 0.05;
        parts.torso.position.y = baseHeight + bobY;
        parts.torso.rotation.x = isRunning ? 0.35 : 0.05; 
        parts.torso.rotation.y = isRunning ? w * 0.1 : 0;
        parts.head.rotation.x = isRunning ? -0.2 : 0;
        parts.head.rotation.y = w * 0.05;

        if (isRunning) {
            parts.lArm.rotation.set(w * 0.8, 0, 0.2);
            parts.rArm.rotation.set(-w * 0.8, 0, -0.2);
            parts.lElbow.rotation.set(-0.5, 0, 0);
            parts.rElbow.rotation.set(-0.5, 0, 0);
            
            parts.lLeg.rotation.x = -w * 0.9;
            parts.rLeg.rotation.x = w * 0.9;
            parts.lKnee.rotation.x = w > 0 ? 0 : -w * 1.5;
            parts.rKnee.rotation.x = w < 0 ? 0 : w * 1.5;
        } else {
            parts.lArm.rotation.set(w * 0.05 + 0.1, 0, 0.2);
            parts.rArm.rotation.set(-w * 0.05 + 0.1, 0, -0.2);
            parts.lElbow.rotation.set(-0.2, 0, 0);
            parts.rElbow.rotation.set(-0.2, 0, 0);
            
            parts.lLeg.rotation.x = 0;
            parts.rLeg.rotation.x = 0;
            parts.lKnee.rotation.x = 0;
            parts.rKnee.rotation.x = 0;
        }

        // ── 背中の4本腕の制御 ──
        const baseRot = [
            { rx: 0.5, ry: -0.5, rz: 0.6 },
            { rx: 0.5, ry:  0.5, rz:-0.6 },
            { rx: 0.2, ry: -0.4, rz: 0.9 },
            { rx: 0.2, ry:  0.4, rz:-0.9 }
        ];

        for (let i = 0; i < 4; i++) {
            const ba = parts.backArms[i];
            const armId = 'ARM' + (i + 1);
            
            const armObj = this.arms.find(a => a.id === armId);
            const isDeployed = bits.some(b => b.owner === this && b.armId === armId);
            const isDestroyed = !armObj || armObj.hp <= 0;

            if (isDeployed || isDestroyed) {
                ba.visible = false;
            } else {
                ba.visible = true;
                if (this.state === 'windup' && this.activeArm && this.activeArm.id === armId) {
                    // 発射直前の構え(前方を向く)
                    ba.rotation.x = THREE.MathUtils.lerp(ba.rotation.x, -Math.PI/2, 0.3);
                    ba.rotation.y = THREE.MathUtils.lerp(ba.rotation.y, 0, 0.3);
                    ba.rotation.z = THREE.MathUtils.lerp(ba.rotation.z, 0, 0.3);
                } else {
                    // 通常時の羽ばたき・揺らぎ
                    ba.rotation.x = baseRot[i].rx + Math.sin(this.walkCycle + i)*0.08;
                    ba.rotation.y = baseRot[i].ry;
                    ba.rotation.z = baseRot[i].rz + Math.cos(this.walkCycle + i)*0.08;
                }
            }
        }
    }

    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 sp=this.pos.clone().setY(5);
        const routes=generateMultiPaths(sp,target.pos,this.data,target.data);
        this.executeAttack(arm,target,routes[0].path); return true;
    }

    executeAttack(armObj, target, preCalcPath, customColor) {
        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;
        this.activeArmColor=customColor;
    }

    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');
    }
}

// ==========================================
// 11. AI
// ==========================================
function runEnemyAI(enemy, player, dt) {
    if (enemy.hp<=0||enemy.deathTimer>=0) return;
    enemy.aiActionTimer-=dt;
    const dataLevel=enemy.dataLevel; const isSuperior=enemy.isDataSuperior(player);
    if (enemy.state!=='idle') return;

    const 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 pp=player.pos.clone().addScaledVector(new THREE.Vector3(player.targetFacing.x,0,player.targetFacing.z),5);
                enemy.moveTarget.copy(pp);
            }
            if (enemy.startAttack([player])) { enemy.aiActionTimer=attackInterval; return; }
        }
    }

    const vf=fragments.filter(f=>f.userData.type!=='data');
    if (vf.length>0) {
        let cl=vf[0], md=enemy.pos.distanceTo(cl.position);
        for (let i=1;i<vf.length;i++) { const d=enemy.pos.distanceTo(vf[i].position); if(d<md){md=d;cl=vf[i];} }
        enemy.moveTarget.copy(cl.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 sp=(20+dataLevel*1.5)*dt;
        const nx = enemy.pos.x + dir.x * sp;
        const nz = enemy.pos.z + dir.z * sp;
        const testX = new THREE.Vector3(nx, enemy.pos.y, enemy.pos.z);
        const testZ = new THREE.Vector3(enemy.pos.x, enemy.pos.y, nz);
        if (!checkCollision(testX, enemy.radius) && nx>=-45 && nx<=45) enemy.pos.x = nx;
        if (!checkCollision(testZ, enemy.radius) && nz>=-45 && nz<=45) enemy.pos.z = nz;
    }
}

// ==========================================
// 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,0,20); player.lastPos.copy(player.pos);
    player.state='idle'; player.facing.set(0,0,-1); player.targetFacing.set(0,0,-1);
    player.robotGroup.position.copy(player.pos);
    player.deathTimer=-1; player.isDeadDone=false; player.staggerTimer=0;

    clearRouteLines(); playerTactics.selectedIndex=null;

    for (const e of enemies) { scene.remove(e.robotGroup); scene.remove(e.shieldMesh); }
    enemies.length=0;
    for (const b of bits) b.destroy(); bits.length=0;

    const sp=[[0,-20],[-15,-15],[15,-15],[-20,-25],[20,-25]];
    for (let i=0;i<Math.min(currentStage,5);i++) enemies.push(createEnemyAt(sp[i][0],sp[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'].includes(e.code)) e.preventDefault();
    if(audioCtx.state==='suspended') audioCtx.resume();
    if (e.code==='Escape') {
        if(gameState==='PLAYING') switchState('PAUSE');
        else if(gameState==='PAUSE') switchState('PLAYING');
        return;
    }
    if (!keys[e.code]) { keys[e.code]=true; handleInput(e.code); }
});
window.addEventListener('keyup', e => { keys[e.code]=false; });

function handleInput(code) {
    if (gameState!=='PLAYING') return;
    let 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;
    if (!playerTactics.routes[armIndex]) return;

    // ここで2回押しを判定
    if (playerTactics.selectedIndex === armIndex) {
        executePlayerAction(armIndex);
        playerTactics.selectedIndex = null; // 実行したら選択解除
    } else {
        playerTactics.selectedIndex = armIndex; // 別のキーなら単に選択状態にする
        playSound('menu');
    }
    game.updateUI();
}

function executePlayerAction(armIndex) {
    if (player.state!=='idle'&&player.state!=='guarding') return;
    const armObj=player.arms[armIndex];
    const routeInfo=playerTactics.routes[armIndex];
    if (playerTactics.interceptBit) {
        player.startGuard(playerTactics.target);
        const ib=new Bit(player.pos.clone().add(new THREE.Vector3(0,5,0)),routeInfo.path,CMD_COLORS[armIndex],player,playerTactics.interceptBit,armObj.id);
        ib.isInterceptor=true; bits.push(ib);
        playSound('intercept');
        createFloatingText('INTERCEPT!',player.pos.clone().add(new THREE.Vector3(0,5,0)),CMD_COLORS_CSS[armIndex]);
    } else if (playerTactics.target) {
        player.executeAttack(armObj,playerTactics.target,routeInfo.path,CMD_COLORS[armIndex]);
    }
}

// ==========================================
// 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(),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(),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 && e.deathTimer < 0);
    const promptEl = document.getElementById('centerPrompt');

    flashTimer -= dt;
    if (flashTimer <= 0) { flashOverlay.style.background = 'rgba(0,0,0,0)'; }

    updateCameraShake(dt);

    playerTactics.updateTimer -= dt;
    if (playerTactics.updateTimer<=0) {
        playerTactics.updateTimer=0.1;
        const incoming=bits.filter(b=>b.targetObj===player&&b.bitState==='OUTBOUND');
        incoming.sort((a,b)=>a.pos.distanceToSq(player.pos)-b.pos.distanceToSq(player.pos));
        const intercept=(incoming.length>0&&incoming[0].pos.distanceTo(player.pos)<35)?incoming[0]:null;
        updatePlayerTrajectories(intercept, player.getNearestOpponent(aliveEnemies));
    }
    drawPlayerTrajectories();

    if (playerTactics.selectedIndex!==null) {
        promptEl.innerText='COMMAND SELECTED — PRESS AGAIN TO EXECUTE';
        promptEl.className='locked-on';
    } else if (playerTactics.interceptBit) {
        promptEl.innerText='▲ WARNING: ENEMY ATTACK INCOMING ▲';
        promptEl.className='obstructed';
    } else if (player.state==='idle'&&aliveEnemies.length>0&&player.getAvailableArm()) {
        promptEl.innerText='[ Z / X / C / V ] SELECT COMMAND';
        promptEl.className='locked-on';
    } else {
        promptEl.innerText=''; promptEl.className='';
    }

    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 md=new THREE.Vector3(dx,0,dz).normalize();
        player.targetFacing.copy(md);
        const spd = 40 * dt;
        const nx = player.pos.x + md.x * spd;
        const testX = new THREE.Vector3(nx, player.pos.y, player.pos.z);
        if (!checkCollision(testX, player.radius) &&
            nx >= -45 && nx <= 45) {
            player.pos.x = nx;
        }
        const nz = player.pos.z + md.z * spd;
        const testZ = new THREE.Vector3(player.pos.x, player.pos.y, nz);
        if (!checkCollision(testZ, player.radius) &&
            nz >= -45 && nz <= 45) {
            player.pos.z = nz;
        }
        if(player.state==='guarding') player.state='idle';
    }

    for (const e of enemies) if (e.hp>0&&e.deathTimer<0) runEnemyAI(e,player,dt);
    player.update(dt, aliveEnemies);
    for (const e of enemies) e.update(dt,[player]);

    updateBits(dt); updateParticles(dt); updateAfterImages(dt);

    for (let i=fragments.length-1;i>=0;i--) {
        fragments[i].rotation.y+=dt*2; fragments[i].rotation.x+=dt;
        if (player.pos.distanceTo(fragments[i].position)<5) {
            collectItem(player,fragments[i]); scene.remove(fragments[i]); fragments.splice(i,1); continue;
        }
        for (const e of aliveEnemies) {
            if (fragments[i]&&e.pos.distanceTo(fragments[i].position)<5) {
                if(fragments[i].userData.type==='data') continue;
                collectItem(e,fragments[i]); scene.remove(fragments[i]); fragments.splice(i,1); break;
            }
        }
    }

    updateFloatingTexts(dt); updateTargetHud(); game.updateUI();

    if (player.deathTimer >= 0.8 && gameState === 'PLAYING') {
        game.addMessage('SYSTEM: 機体大破。GAME OVER'); switchState('GAME_OVER');
    } else if (enemies.length > 0 && enemies.every(e => e.deathTimer >= 0.8) && gameState === 'PLAYING') {
        accumulatedEnemyData = enemies.reduce((s,e) => s + e.data, 0);
        accumulatedEnemyMat  = enemies.reduce((s,e) => s + e.materials, 0);
        game.addMessage('SYSTEM: 全敵AI沈黙。');
        switchState('STAGE_CLEAR');
        setTimeout(() => {
            if (gameState !== 'STAGE_CLEAR') return; 
            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)),'#0ff');
    } else {
        collector.materials+=10; createFloatingText('+MAT 10',collector.pos.clone().add(new THREE.Vector3(0,4,0)),'#af2');
    }
    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>

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

ピックアップされています

気楽に遊べるゲーム

  • 25本

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
ロボットミニゲーム「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