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


【更新履歴】

・2026/6/8 バージョン1.0公開。

画像

【あらすじ】

・ロボット同士で腕を飛ばしあって対決するゲームです。

・データ量を増やすことで
 相手の予測を上回る行動が取れるようになります。

【操作説明】

・方向キーで自機を移動。

・自機が攻撃可能な時は、
 ・Zキーで左腕を発射し攻撃。
 ・Xキーで右腕を発射し攻撃。

・自機がガート可能な時は、
 ・Zキーで左腕を発射しガード。
 ・Xキーで右腕を発射しガード。

・回避は自動的に行う。(データ量で負けてると失敗する)

【アイテム】

・水色 … データ量アップ。攻撃、ガード、回避の成功率がアップ。
・黄緑色 … 資源。徐々に回復。


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

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

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>WIRE ARMS</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #050510; font-family: 'Courier New', monospace; user-select: none; }
        canvas { display: block; position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; z-index: -1; transition: filter 0.3s; }
        body.blur canvas { filter: blur(6px) brightness(0.6); }
        
        #ui { position: absolute; top: 10px; left: 10px; width: 100%; pointer-events: none; transition: opacity 0.3s; }
        .bar-container { display: flex; justify-content: space-between; width: 90%; margin: 0 auto; }
        .status-box { background: rgba(0,20,0,0.7); border: 1px solid #0f0; padding: 10px; width: 40%; text-shadow: 0 0 5px #0fff0f; color: #0fff0f; transition: all 0.3s; }
        .status-box.enemy { background: rgba(20,0,0,0.7); border-color: #f00; color: #f33; text-shadow: 0 0 5px #f33; text-align: right; }
        .regen-active { box-shadow: 0 0 15px #adff2f; border-color: #adff2f !important; }
        
        #log { position: absolute; bottom: 20px; left: 20px; color: #aaa; pointer-events: none; font-size: 14px; text-shadow: 0 0 2px #000; line-height: 1.5; }
        .instructions { position: absolute; bottom: 20px; right: 20px; color: #fff; text-align: right; font-size: 14px; background: rgba(0,0,0,0.8); padding: 10px; border: 1px solid #555; pointer-events: none;}
        
        #centerPrompt { position: absolute; top: 35%; width: 100%; text-align: center; font-size: 32px; font-weight: bold; color: #fff; text-shadow: 0 0 10px #000, 0 0 20px #0ff; pointer-events: none; transition: color 0.2s; }
        .locked-on { color: #0ff !important; text-shadow: 0 0 20px #0ff, 0 0 40px #00f !important; }
        .obstructed { color: #f55 !important; text-shadow: 0 0 10px #f00 !important; font-size: 24px !important; }

        h2, p { margin: 0 0 5px 0; }

        .screen { display: none; position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,5,15,0.85); flex-direction: column; justify-content: center; align-items: center; z-index: 100; color: #fff; text-align: center; }
        .screen.active { display: flex; }
        .screen h1 { font-size: 72px; margin-bottom: 20px; letter-spacing: 10px; text-shadow: 0 0 20px #0ff; }
        .screen h2 { font-size: 36px; margin-bottom: 20px; color: #0ff; }
        .screen p { font-size: 24px; color: #ccc; max-width: 600px; line-height: 1.5; }
        .blink { animation: blinker 1.5s linear infinite; }
        @keyframes blinker { 50% { opacity: 0; } }

        #stageIndicator { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); color: #0ff; font-size: 18px; text-shadow: 0 0 10px #0ff; pointer-events: none; opacity: 0; transition: opacity 0.3s; }
        #stageIndicator.visible { opacity: 1; }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
</head>
<body>

<div id="ui" style="opacity: 0;">
    <div class="bar-container">
        <div class="status-box" id="uiPlayer">
            <h2>PLAYER [HP: <span id="hpPlayer">1000</span>] <span id="regenPlayer" style="color:#adff2f; font-size:12px;"></span></h2>
            <p>DATA LEVEL: <span id="dataPlayer">0</span></p>
        </div>
        <div class="status-box enemy" id="uiEnemy">
            <h2><span id="regenEnemy" style="color:#adff2f; font-size:12px;"></span> [HP: <span id="hpEnemy">100</span>] ENEMY</h2>
            <p>DATA LEVEL: <span id="dataEnemy">0</span></p>
        </div>
    </div>
</div>

<div id="stageIndicator"></div>
<div id="centerPrompt"></div>
<div id="log"></div>

<div class="instructions" id="instructions" style="opacity: 0;">
    [][↓][←][→] 機体移動 / [ESC] PAUSE<br><br>
    <b>【攻撃】 [Z]キー(左腕) or [X]キー(右腕)</b><br>
    通常時: ビット発射 → レーザー照射<br>
    敵攻撃時: バリア展開(ガード)<br><br>
    <span style="color:#00ffff;">■ 水色アイテム: データ量UP(+100)</span><br>
    <span style="color:#adff2f;">■ 黄緑アイテム: HP継続回復(+1/sec)</span>
</div>

<div id="screenTitle" class="screen active">
    <h1>WIRE ARMS</h1>
    <p class="blink">CLICK TO START</p>
</div>
<div id="screenOpening" class="screen">
    <h2>INITIALIZING SYSTEM</h2>
    <p>神経接続パス確立...<br>自律戦闘AI、起動準備完了。</p>
</div>
<div id="screenStageStart" class="screen">
    <h1 id="stageText">STAGE 1</h1>
    <p>敵対プログラムを殲滅せよ</p>
</div>
<div id="screenPause" class="screen">
    <h1 style="color:#ffa500; text-shadow: 0 0 20px #ffa500;">PAUSED</h1>
    <p class="blink">[ESC]キーで戦闘再開</p>
</div>
<div id="screenStageClear" class="screen">
    <h1 style="color:#0f0; text-shadow: 0 0 20px #0f0;">STAGE CLEAR</h1>
    <p>システム修復完了。<br>次フェーズへ移行します。</p>
</div>
<div id="screenGameOver" class="screen">
    <h1 style="color:#f00; text-shadow: 0 0 20px #f00;">GAME OVER</h1>
    <p>機体大破。接続をロストしました。<br><br>画面クリックでシステムを再起動してください。</p>
</div>
<div id="screenGameClear" class="screen">
    <h1 style="color:#ff0ff0; text-shadow: 0 0 20px #ff0ff0;">ALL CLEARED</h1>
    <p>MISSION ACCOMPLISHED.<br>全脅威の排除を確認しました。<br><br>画面クリックでシステムを再起動してください。</p>
</div>

<script>
// ==========================================
// 0. ゲーム状態管理
// ==========================================
let gameState = 'TITLE';
let currentStage = 1;
const maxStages = 5;

let enemies = [];

function switchState(newState) {
    document.querySelectorAll('.screen').forEach(el => el.classList.remove('active'));
    gameState = newState;
    
    const ui = document.getElementById('ui');
    const inst = document.getElementById('instructions');
    const stageInd = document.getElementById('stageIndicator');
    const body = document.body;

    if (newState === 'PLAYING') {
        ui.style.opacity = '1'; inst.style.opacity = '1';
        stageInd.classList.add('visible');
        body.classList.remove('blur');
    } else {
        ui.style.opacity = '0'; inst.style.opacity = '0';
        stageInd.classList.remove('visible');
        body.classList.add('blur');
        document.getElementById('centerPrompt').innerText = "";
    }

    if (newState === 'TITLE') document.getElementById('screenTitle').classList.add('active');
    else if (newState === 'OPENING') document.getElementById('screenOpening').classList.add('active');
    else if (newState === 'STAGE_START') {
        document.getElementById('stageText').innerText = "STAGE " + currentStage;
        document.getElementById('screenStageStart').classList.add('active');
        setTimeout(() => {
            initStage();
            switchState('PLAYING');
        }, 2000);
    }
    else if (newState === 'PAUSE') document.getElementById('screenPause').classList.add('active');
    else if (newState === 'STAGE_CLEAR') document.getElementById('screenStageClear').classList.add('active');
    else if (newState === 'GAME_OVER') document.getElementById('screenGameOver').classList.add('active');
    else if (newState === 'GAME_CLEAR') document.getElementById('screenGameClear').classList.add('active');
}

window.addEventListener('click', () => {
    window.focus();
    if(audioCtx.state === 'suspended') audioCtx.resume();
    if (gameState === 'TITLE') {
        switchState('OPENING');
        setTimeout(() => switchState('STAGE_START'), 2500);
    } else if (gameState === 'GAME_OVER' || gameState === 'GAME_CLEAR') {
        currentStage = 1;
        switchState('TITLE');
    }
});

// ==========================================
// 1. サウンドシステム
// ==========================================
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();

function playSound(type) {
    if (audioCtx.state === 'suspended') audioCtx.resume();
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    
    const now = audioCtx.currentTime;
    if(type === 'attack') {
        osc.type = 'sawtooth'; osc.frequency.setValueAtTime(300, now); osc.frequency.exponentialRampToValueAtTime(80, now + 0.15);
        gain.gain.setValueAtTime(0.25, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
        osc.start(now); osc.stop(now + 0.15);
    } else if(type === 'laser') {
        osc.type = 'sine'; osc.frequency.setValueAtTime(1200, now); osc.frequency.linearRampToValueAtTime(400, now + 0.25);
        gain.gain.setValueAtTime(0.3, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
        osc.start(now); osc.stop(now + 0.25);
    } else if(type === 'guard') {
        osc.type = 'square'; osc.frequency.setValueAtTime(400, now); osc.frequency.linearRampToValueAtTime(600, now + 0.1);
        gain.gain.setValueAtTime(0.3, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
        osc.start(now); osc.stop(now + 0.1);
    } else if(type === 'dodge') {
        osc.type = 'sine'; osc.frequency.setValueAtTime(800, now); osc.frequency.exponentialRampToValueAtTime(300, now + 0.3);
        gain.gain.setValueAtTime(0.3, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
        osc.start(now); osc.stop(now + 0.3);
    } else if(type === 'hit') {
        osc.type = 'sawtooth'; osc.frequency.setValueAtTime(100, now); osc.frequency.exponentialRampToValueAtTime(20, now + 0.3);
        gain.gain.setValueAtTime(0.5, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
        osc.start(now); osc.stop(now + 0.3);
    }
}

// ==========================================
// 2. UIシステム
// ==========================================
const game = {
    messages: ["SYSTEM: 起動待機中..."],
    addMessage(msg) {
        this.messages.push(msg);
        if(this.messages.length > 6) this.messages.shift();
        document.getElementById('log').innerHTML = this.messages.join('<br>');
    },
    updateUI() {
        document.getElementById('hpPlayer').innerText = Math.floor(player.hp);
        const firstAliveEnemy = enemies.find(e => e.hp > 0);
        document.getElementById('hpEnemy').innerText = firstAliveEnemy ? Math.floor(firstAliveEnemy.hp) : 0;
        document.getElementById('dataPlayer').innerText = player.data;
        document.getElementById('dataEnemy').innerText = firstAliveEnemy ? firstAliveEnemy.data : 0;
        document.getElementById('regenPlayer').innerText = player.regenTimer > 0 ? "(Regen)" : "";
        document.getElementById('regenEnemy').innerText = (firstAliveEnemy && firstAliveEnemy.regenTimer > 0) ? "(Regen)" : "";
        document.getElementById('uiPlayer').classList.toggle('regen-active', player.regenTimer > 0);
        document.getElementById('uiEnemy').classList.toggle('regen-active', !!(firstAliveEnemy && firstAliveEnemy.regenTimer > 0));
        document.getElementById('stageIndicator').innerText = `STAGE ${currentStage} / ${maxStages}  ENEMIES: ${enemies.filter(e=>e.hp>0).length}`;
    }
};

const floatingTexts = [];
function createFloatingText(text, pos, color, size = "24px") {
    const div = document.createElement('div');
    div.innerText = text;
    div.style.position = 'absolute';
    div.style.color = color;
    div.style.fontWeight = 'bold';
    div.style.textShadow = '0 0 5px #000';
    div.style.pointerEvents = 'none';
    div.style.fontSize = size;
    document.body.appendChild(div);
    floatingTexts.push({ el: div, pos: pos.clone(), life: 1.5 });
}

function updateFloatingTexts(dt) {
    for (let i = floatingTexts.length - 1; i >= 0; i--) {
        let ft = floatingTexts[i];
        ft.life -= dt;
        if (ft.life <= 0) { ft.el.remove(); floatingTexts.splice(i, 1); continue; }
        ft.pos.y += dt * 10; 
        let proj = ft.pos.clone().project(camera);
        let x = (proj.x * 0.5 + 0.5) * window.innerWidth;
        let y = (proj.y * -0.5 + 0.5) * window.innerHeight;
        ft.el.style.left = `calc(${x}px - 20px)`;
        ft.el.style.top = `${y}px`;
        ft.el.style.opacity = Math.min(1.0, ft.life);
    }
}

// ==========================================
// 3. Three.js セットアップ & 障害物生成
// ==========================================
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050510, 0.012);

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

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

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

const ambientLight = new THREE.AmbientLight(0x404040, 2.0);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(20, 50, 20);
scene.add(dirLight);

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

const blocks = [];
const blockGeo = new THREE.BoxGeometry(8, 10, 8);
const blockMat = new THREE.MeshStandardMaterial({ color: 0x224455, roughness: 0.8, metalness: 0.5 });
const fragments = [];

function generateBlocks() {
    blocks.forEach(b => scene.remove(b));
    blocks.length = 0;
    for (let x = -40; x <= 40; x += 20) {
        for (let z = -40; z <= 40; z += 20) {
            if (Math.abs(x) < 15 && Math.abs(z) < 30) continue; 
            if (Math.random() < 0.4) continue; 
            let mesh = new THREE.Mesh(blockGeo, blockMat);
            mesh.position.set(x, 5, z);
            scene.add(mesh);
            blocks.push(mesh);
        }
    }
}

function checkCollision(pos, radius) {
    for (let b of blocks) {
        let dx = Math.max(b.position.x - 4, Math.min(pos.x, b.position.x + 4)) - pos.x;
        let dz = Math.max(b.position.z - 4, Math.min(pos.z, b.position.z + 4)) - pos.z;
        if (dx * dx + dz * dz < radius * radius) return true;
    }
    return false;
}

function checkLineClear(p1, p2, radius) {
    let dist = p1.distanceTo(p2);
    let steps = Math.ceil(dist / 2);
    let dir = new THREE.Vector3().subVectors(p2, p1).normalize();
    let p = new THREE.Vector3();
    for(let i=1; i<=steps; i++) {
        p.copy(p1).add(dir.clone().multiplyScalar((i/steps)*dist));
        if (checkCollision(p, radius)) return false;
    }
    return true;
}

// ==========================================
// 4. A* アルゴリズム (ビット軌道用)
// ==========================================
const PATH_GRID_SIZE = 4;
const PATH_GRID_W = 25; 
function worldToGrid(x, z) {
    return { c: Math.floor((x + 50) / PATH_GRID_SIZE), r: Math.floor((z + 50) / PATH_GRID_SIZE) };
}
function gridToWorld(c, r) {
    return new THREE.Vector3(c * PATH_GRID_SIZE - 50 + PATH_GRID_SIZE/2, 1, r * PATH_GRID_SIZE - 50 + PATH_GRID_SIZE/2);
}

function findPath(startPos, endPos, radius) {
    let start = worldToGrid(startPos.x, startPos.z);
    let end = worldToGrid(endPos.x, endPos.z);
    
    start.c = Math.max(0, Math.min(PATH_GRID_W-1, start.c)); start.r = Math.max(0, Math.min(PATH_GRID_W-1, start.r));
    end.c = Math.max(0, Math.min(PATH_GRID_W-1, end.c)); end.r = Math.max(0, Math.min(PATH_GRID_W-1, end.r));

    let open = [{c: start.c, r: start.r, g: 0, h: Math.hypot(start.c-end.c, start.r-end.r), parent: null}];
    let closed = new Set();
    let getKey = (c, r) => `${c},${r}`;
    
    while(open.length > 0) {
        open.sort((a,b) => (a.g + a.h) - (b.g + b.h));
        let current = open.shift();
        
        if (current.c === end.c && current.r === end.r) {
            let path = [];
            let curr = current;
            while(curr) {
                path.push(gridToWorld(curr.c, curr.r));
                curr = curr.parent;
            }
            path.reverse();
            path[0].copy(startPos);
            path[path.length-1].copy(endPos);
            return path;
        }
        closed.add(getKey(current.c, current.r));
        
        const dirs = [[0,1], [1,0], [0,-1], [-1,0], [1,1], [1,-1], [-1,1], [-1,-1]];
        for(let d of dirs) {
            let nc = current.c + d[0], nr = current.r + d[1];
            if (nc < 0 || nc >= PATH_GRID_W || nr < 0 || nr >= PATH_GRID_W) continue;
            if (closed.has(getKey(nc, nr))) continue;
            
            if (checkCollision(gridToWorld(nc, nr), radius)) {
                if (nc !== end.c || nr !== end.r) continue; 
            }
            
            let moveCost = (d[0] !== 0 && d[1] !== 0) ? 1.414 : 1;
            let g = current.g + moveCost;
            let existing = open.find(n => n.c === nc && n.r === nr);
            
            if (existing) {
                if (g < existing.g) { existing.g = g; existing.parent = current; }
            } else {
                open.push({c: nc, r: nr, g: g, h: Math.hypot(nc-end.c, nr-end.r), parent: current});
            }
        }
    }
    return null;
}

// ==========================================
// 5. パーティクルシステム
// ==========================================
const particles = [];

function createHitParticles(pos, color) {
    const count = 18;
    const geo = new THREE.SphereGeometry(0.25, 4, 4);
    for (let i = 0; i < count; i++) {
        const mat = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
        const mesh = new THREE.Mesh(geo, mat);
        mesh.position.copy(pos);
        scene.add(mesh);
        const vel = new THREE.Vector3((Math.random() - 0.5) * 20, Math.random() * 15 + 5, (Math.random() - 0.5) * 20);
        particles.push({ mesh, vel, life: 0.6 + Math.random() * 0.4, maxLife: 1.0 });
    }
}

function createDodgeParticles(pos, color) {
    const count = 8;
    const geo = new THREE.OctahedronGeometry(0.4);
    for (let i = 0; i < count; i++) {
        const mat = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
        const mesh = new THREE.Mesh(geo, mat);
        mesh.position.copy(pos);
        scene.add(mesh);
        const angle = (i / count) * Math.PI * 2;
        const vel = new THREE.Vector3(Math.cos(angle) * 8, 3, Math.sin(angle) * 8);
        particles.push({ mesh, vel, life: 0.4, maxLife: 0.4 });
    }
}

function updateParticles(dt) {
    for (let i = particles.length - 1; i >= 0; i--) {
        const p = particles[i];
        p.life -= dt;
        if (p.life <= 0) {
            scene.remove(p.mesh);
            particles.splice(i, 1);
            continue;
        }
        p.vel.y -= 20 * dt;
        p.mesh.position.addScaledVector(p.vel, dt);
        p.mesh.rotation.x += dt * 5;
        p.mesh.rotation.z += dt * 3;
        const alpha = p.life / p.maxLife;
        p.mesh.material.opacity = alpha;
        p.mesh.material.transparent = true;
    }
}

// ==========================================
// 6. 残像(アフターイメージ)システム
// ==========================================
const afterImages = [];

function createAfterImage(robotGroup, color) {
    const cloneGroup = new THREE.Group();
    robotGroup.traverse(child => {
        if (child.isMesh) {
            const clone = new THREE.Mesh(
                child.geometry,
                new THREE.MeshBasicMaterial({ color: color, wireframe: true, transparent: true, opacity: 0.55 })
            );
            clone.position.copy(child.getWorldPosition(new THREE.Vector3()));
            clone.quaternion.copy(child.getWorldQuaternion(new THREE.Quaternion()));
            clone.scale.copy(child.getWorldScale(new THREE.Vector3()));
            cloneGroup.add(clone);
        }
    });
    scene.add(cloneGroup);
    afterImages.push({ group: cloneGroup, life: 0.35, maxLife: 0.35 });
}

function updateAfterImages(dt) {
    for (let i = afterImages.length - 1; i >= 0; i--) {
        const ai = afterImages[i];
        ai.life -= dt;
        if (ai.life <= 0) {
            scene.remove(ai.group);
            afterImages.splice(i, 1);
            continue;
        }
        const alpha = (ai.life / ai.maxLife) * 0.55;
        ai.group.traverse(child => {
            if (child.isMesh) child.material.opacity = alpha;
        });
    }
}

// ==========================================
// 7. ビット(飛翔体)システム
// ==========================================
const bits = [];

function getActiveBitCount(entity) {
    return bits.filter(b => b.owner === entity).length;
}

class Bit {
    constructor(startPos, path, color, owner, target, predLine) {
        this.owner = owner;
        this.target = target;
        this.path = path;
        this.pathIndex = 0;
        this.speed = 40; 
        this.pos = startPos.clone();
        this.color = color;
        
        this.bitState = 'OUTBOUND'; 
        this.laserTimer = 0;
        this.laserLine = null;
        this.life = 15.0; 

        const geo = new THREE.OctahedronGeometry(0.7, 0);
        const mat = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
        this.mesh = new THREE.Mesh(geo, mat);
        this.mesh.position.copy(this.pos);
        scene.add(this.mesh);

        const ringGeo = new THREE.TorusGeometry(1.2, 0.1, 4, 8);
        const ringMat = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
        this.ring = new THREE.Mesh(ringGeo, ringMat);
        this.mesh.add(this.ring);

        this.light = new THREE.PointLight(color, 2, 8);
        this.mesh.add(this.light);

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

    update(dt) {
        this.life -= dt;
        this.mesh.rotation.y += dt * 4;
        this.ring.rotation.x += dt * 6;

        if (this.bitState === 'OUTBOUND') {
            if (this.path && this.pathIndex < this.path.length - 1) {
                const targetPos = this.path[this.pathIndex + 1].clone().setY(5);
                const dir = new THREE.Vector3().subVectors(targetPos, this.pos).normalize();
                const moveDist = this.speed * dt;
                const remaining = this.pos.distanceTo(targetPos);

                if (remaining < moveDist) {
                    this.pos.copy(targetPos);
                    this.pathIndex++;
                } else {
                    this.pos.addScaledVector(dir, moveDist);
                }
                this.mesh.position.copy(this.pos);

                if (this.target && this.pos.distanceTo(this.target.pos) < 5) {
                    this.fireLaser();
                }
            } else {
                this.fireLaser();
            }
        } else if (this.bitState === 'FIRING') {
            this.laserTimer -= dt;
            if (this.laserTimer <= 0) {
                if (this.laserLine && this.laserLine.parent) scene.remove(this.laserLine);
                this.laserLine = null;
                this.mesh.visible = true; 
                this.bitState = 'RETURN';
                
                if (this.trajectoryLine && this.trajectoryLine.parent) scene.remove(this.trajectoryLine);
                this.trajectoryLine = null;
            }
        } else if (this.bitState === 'RETURN') {
            const targetPos = this.owner.pos.clone().setY(5);
            const dir = new THREE.Vector3().subVectors(targetPos, this.pos).normalize();
            const moveDist = this.speed * 1.5 * dt; 
            const remaining = this.pos.distanceTo(targetPos);

            if (remaining < moveDist || remaining < 2.0) {
                this.destroy(); 
                return true;
            } else {
                this.pos.addScaledVector(dir, moveDist);
            }
            this.mesh.position.copy(this.pos);
        }

        return this.life <= 0;
    }

    fireLaser() {
        if (this.bitState === 'FIRING') return;
        this.bitState = 'FIRING';
        playSound('laser');

        if (this.trajectoryLine && this.trajectoryLine.parent) scene.remove(this.trajectoryLine);
        this.trajectoryLine = null;

        if (!this.target || this.target.hp <= 0) { 
            this.laserTimer = 0.1; 
            return; 
        }

        const points = [this.pos.clone(), this.target.pos.clone().setY(3)];
        const laserGeo = new THREE.BufferGeometry().setFromPoints(points);
        const laserMat = new THREE.LineBasicMaterial({ color: this.color, linewidth: 3 });
        this.laserLine = new THREE.Line(laserGeo, laserMat);
        scene.add(this.laserLine);
        this.laserTimer = 0.3;

        this.owner.resolveAttack(this.target);
        createHitParticles(this.target.pos.clone().setY(3), this.color);

        this.mesh.visible = false;
    }

    destroy() {
        if (this.mesh.parent) scene.remove(this.mesh);
        if (this.laserLine && this.laserLine.parent) scene.remove(this.laserLine);
        if (this.trajectoryLine && this.trajectoryLine.parent) scene.remove(this.trajectoryLine);
        this.life = 0;
    }
}

function updateBits(dt) {
    for (let i = bits.length - 1; i >= 0; i--) {
        const done = bits[i].update(dt);
        if (done) {
            bits[i].destroy();
            bits.splice(i, 1);
        }
    }
}

// ==========================================
// 8. ワイヤーフレーム人型ロボット生成
// ==========================================
function createWireframeRobot(color) {
    const group = new THREE.Group();
    const mat = () => new THREE.MeshBasicMaterial({ color: color, wireframe: true });

    const torso = new THREE.Mesh(new THREE.BoxGeometry(3, 4, 2), mat());
    torso.position.set(0, 5, 0);
    group.add(torso);

    const head = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.8, 1.8), mat());
    head.position.set(0, 7.8, 0);
    group.add(head);

    const visor = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.4, 0.3), mat());
    visor.position.set(0, 7.9, 0.9);
    group.add(visor);

    const lUpperArm = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 2, 6), mat());
    lUpperArm.position.set(-2.5, 5.5, 0);
    group.add(lUpperArm);

    const lForeArm = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.28, 1.8, 6), mat());
    lForeArm.position.set(-3.5, 4.0, 0);
    group.add(lForeArm);

    const lHand = new THREE.Mesh(new THREE.OctahedronGeometry(0.6, 0), mat());
    lHand.position.set(-4.3, 3.0, 0);
    group.add(lHand);

    const rUpperArm = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 2, 6), mat());
    rUpperArm.position.set(2.5, 5.5, 0);
    group.add(rUpperArm);

    const rForeArm = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.28, 1.8, 6), mat());
    rForeArm.position.set(3.5, 4.0, 0);
    group.add(rForeArm);

    const rHand = new THREE.Mesh(new THREE.OctahedronGeometry(0.6, 0), mat());
    rHand.position.set(4.3, 3.0, 0);
    group.add(rHand);

    const lThigh = new THREE.Mesh(new THREE.CylinderGeometry(0.45, 0.38, 2.2, 6), mat());
    lThigh.position.set(-1.0, 1.8, 0);
    group.add(lThigh);

    const lShin = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.28, 2.0, 6), mat());
    lShin.position.set(-1.1, -0.3, 0.1);
    group.add(lShin);

    const lFoot = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.4, 1.4), mat());
    lFoot.position.set(-1.1, -1.6, 0.3);
    group.add(lFoot);

    const rThigh = new THREE.Mesh(new THREE.CylinderGeometry(0.45, 0.38, 2.2, 6), mat());
    rThigh.position.set(1.0, 1.8, 0);
    group.add(rThigh);

    const rShin = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.28, 2.0, 6), mat());
    rShin.position.set(1.1, -0.3, 0.1);
    group.add(rShin);

    const rFoot = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.4, 1.4), mat());
    rFoot.position.set(1.1, -1.6, 0.3);
    group.add(rFoot);

    const lShoulder = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.7, 1.0), mat());
    lShoulder.position.set(-2.1, 6.6, 0);
    group.add(lShoulder);

    const rShoulder = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.7, 1.0), mat());
    rShoulder.position.set(2.1, 6.6, 0);
    group.add(rShoulder);

    const antenna = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 1.5, 4), mat());
    antenna.position.set(0.5, 9.3, 0);
    group.add(antenna);

    const backpack = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 0.8), mat());
    backpack.position.set(0, 5.5, -1.5);
    group.add(backpack);

    const thrL = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.45, 0.8, 6), mat());
    thrL.position.set(-0.7, 5.0, -2.0);
    group.add(thrL);
    const thrR = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.45, 0.8, 6), mat());
    thrR.position.set(0.7, 5.0, -2.0);
    group.add(thrR);

    scene.add(group);
    return group;
}

// ==========================================
// 9. シールドメッシュ生成
// ==========================================
function createShieldMesh() {
    const geo = new THREE.SphereGeometry(5.0, 12, 12);
    const mat = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.25, wireframe: true });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.visible = false;
    scene.add(mesh);
    return mesh;
}

// ==========================================
// 10. Entityクラス
// ==========================================
class Entity {
    constructor(color, x, z, isPlayer) {
        this.isPlayer = isPlayer;
        this.color = color;
        this.pos = new THREE.Vector3(x, 1, z);
        this.lastPos = this.pos.clone(); 
        this.radius = 2.5; 
        this.maxHp = isPlayer ? 1000 : 100;
        this.hp = this.maxHp;
        this.data = 0;
        this.regenTimer = 0;

        this.robotGroup = createWireframeRobot(color);
        this.robotGroup.position.copy(this.pos);

        this.shieldMesh = createShieldMesh();

        this.facing = new THREE.Vector3(0, 0, isPlayer ? -1 : 1);
        this.targetFacing = this.facing.clone();

        this.state = 'idle'; 
        this.timer = 0;
        this.activeArm = null;
        this.currentAttackPath = null;
        this.dodgeTargetPos = null;
        
        this.predLine = null;

        this.afterImageTimer = 0;
        this.afterImageInterval = 0.04;
        this.walkCycle = 0;

        this.aiActionTimer = isPlayer ? 0 : 1.0;
        this.moveTarget = new THREE.Vector3(x, 1, z);
    }

    get dataLevel() { return Math.floor(this.data / 100); }
    isDataSuperior(opponent) { return this.data > opponent.data; }

    buildUnpredictablePath(fromPos, toPos) {
        const mid = new THREE.Vector3().lerpVectors(fromPos, toPos, 0.5);
        const offset = new THREE.Vector3((Math.random() - 0.5) * 20, 0, (Math.random() - 0.5) * 20);
        mid.add(offset);
        mid.x = Math.max(-44, Math.min(44, mid.x));
        mid.z = Math.max(-44, Math.min(44, mid.z));
        return [fromPos.clone(), mid, toPos.clone()];
    }

    attemptAutoDodge(attacker) {
        if (this.state !== 'idle') return;

        let evadeChance;
        if (this.isDataSuperior(attacker)) {
            const dataGap = (this.data - attacker.data) / 100;
            evadeChance = Math.min(0.90, 0.50 + dataGap * 0.08);
        } else {
            evadeChance = 0.05 + ((this.data / 100) * 0.05) - ((attacker.data / 100) * 0.02);
            evadeChance = Math.max(0.03, Math.min(0.40, evadeChance));
        }

        if (Math.random() > evadeChance) return;

        let safeNodes = [];
        const dirs = [[0,1],[1,0],[0,-1],[-1,0],[1,1],[-1,-1],[1,-1],[-1,1]];
        for (let d of dirs) {
            let target = this.pos.clone().add(new THREE.Vector3(d[0]*15, 0, d[1]*15));
            target.x = Math.max(-45, Math.min(45, target.x));
            target.z = Math.max(-45, Math.min(45, target.z));
            if (!checkCollision(target, this.radius)) {
                safeNodes.push(target);
            }
        }

        safeNodes.sort((a, b) => b.distanceToSq(attacker.pos) - a.distanceToSq(attacker.pos));

        if (safeNodes.length > 0) {
            this.state = 'dodging';
            this.dodgeTargetPos = safeNodes[0];
            this.timer = 0.25;
            createFloatingText(this.isDataSuperior(attacker) ? "予測回避!" : "回避!", this.pos, "#fff");
            playSound('dodge');
            createDodgeParticles(this.pos.clone().add(new THREE.Vector3(0,3,0)), this.color);
        }
    }

    fireBit(targetEntity, preCalcPath) {
        if (!targetEntity || targetEntity.hp <= 0) return;
        const startPos = this.pos.clone().add(new THREE.Vector3(0, 5, 0));
        let path = preCalcPath;
        if (!path) {
            if (this.isDataSuperior(targetEntity)) {
                path = this.buildUnpredictablePath(startPos, targetEntity.pos);
            } else {
                path = findPath(this.pos, targetEntity.pos, 1.0);
                if (!path) path = [startPos, targetEntity.pos.clone()];
            }
        }
        
        const bit = new Bit(startPos, path, this.color, this, targetEntity, this.predLine);
        this.predLine = null; 
        bits.push(bit);
        playSound('attack');
    }

    update(dt, opponents) {
        if (this.regenTimer > 0) {
            this.regenTimer -= dt;
            if (Math.random() < dt * 1.0) { this.hp = Math.min(this.maxHp, this.hp + 1); }
        }

        if (this.targetFacing.lengthSq() > 0.001) {
            this.facing.lerp(this.targetFacing, 0.12);
            this.facing.y = 0;
            this.facing.normalize();
        }
        const angle = Math.atan2(this.facing.x, this.facing.z);
        this.robotGroup.rotation.y = angle;
        this.robotGroup.position.copy(this.pos);

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

        if (this.state === 'dodging' || this.state === 'windup' || this.state === 'guarding') {
            this.afterImageTimer -= dt;
            if (this.afterImageTimer <= 0) {
                createAfterImage(this.robotGroup, this.color);
                this.afterImageTimer = this.afterImageInterval;
            }
        } else {
            this.afterImageTimer = 0;
        }

        const movedDist = this.pos.distanceTo(this.lastPos);
        const actualSpeed = dt > 0 ? movedDist / dt : 0;
        this.lastPos.copy(this.pos);
        this.animateRobot(dt, actualSpeed);

        if (this.state === 'windup') {
            this.timer -= dt;
            if (this.timer <= 0) {
                this.state = 'attacking';
                this.timer = 0.3;
                const target = this.getNearestOpponent(opponents);
                this.fireBit(target, this.currentAttackPath);
            }
        } else if (this.state === 'attacking') {
            this.timer -= dt;
            if (this.timer <= 0) this.state = 'idle';
        } else if (this.state === 'guarding') {
            this.timer -= dt;
            if (this.timer <= 0) this.state = 'idle';
        } else if (this.state === 'dodging') {
            this.timer -= dt;
            let moveRatio = dt / Math.max(0.01, this.timer + dt);
            const prevPos = this.pos.clone();
            this.pos.lerp(this.dodgeTargetPos, moveRatio * 5);
            const moved = new THREE.Vector3().subVectors(this.pos, prevPos);
            if (moved.lengthSq() > 0.001) {
                this.targetFacing.copy(moved).normalize();
            }
            if (this.timer <= 0) this.state = 'idle';
        }
    }

    getNearestOpponent(opponents) {
        if (!opponents || opponents.length === 0) return null;
        let nearest = null, minDist = Infinity;
        for (const op of opponents) {
            if (op.hp <= 0) continue;
            const d = this.pos.distanceTo(op.pos);
            if (d < minDist) { minDist = d; nearest = op; }
        }
        return nearest;
    }

    resolveAttack(defender) {
        if (!defender || defender.hp <= 0) return;
        let isHit = true;
        if (defender.state === 'dodging') {
            isHit = false;
            defender.data += 50;
            createFloatingText("EVADE!", defender.pos.clone().add(new THREE.Vector3(0,4,0)), "#ffffff");
        } else if (defender.state === 'guarding') {
            let guardChance = 0.5 + ((defender.data / 100) * 0.1) - ((this.data / 100) * 0.05);
            guardChance = Math.max(0.1, Math.min(1.0, guardChance));
            if (Math.random() < guardChance) {
                isHit = false;
                defender.data += 100;
                createFloatingText("GUARD!", defender.pos.clone().add(new THREE.Vector3(0,4,0)), "#00ffff");
            } else {
                createFloatingText("GUARD BREAK!", defender.pos.clone().add(new THREE.Vector3(0,4,0)), "#ff8800");
            }
        }

        if (isHit) {
            const dmg = 10; // ダメージ固定化
            defender.hp -= dmg;
            this.data += 100;
            playSound('hit');
            createFloatingText(`-${dmg}`, defender.pos.clone().add(new THREE.Vector3(0,4,0)), "#ff0000", "36px");
            createHitParticles(defender.pos.clone().add(new THREE.Vector3(0,3,0)), 0xff4400);
        }
        game.updateUI();
    }

    animateRobot(dt, speed) {
        const isMoving = speed > 5;
        this.walkCycle += dt * (isMoving ? 15 : 4);
        const walkMag = isMoving ? 1.0 : 0.15;
        
        const zL = Math.sin(this.walkCycle) * 2.0 * walkMag;
        const zR = Math.sin(this.walkCycle + Math.PI) * 2.0 * walkMag;

        const ch = this.robotGroup.children;
        if (ch.length >= 15) {
            ch[0].position.y = 5.0 + Math.max(0, Math.sin(this.walkCycle * 2)) * 0.4 * walkMag;
            ch[1].rotation.y = zL * 0.05; 
            
            if (this.state !== 'windup' && this.state !== 'attacking' && this.state !== 'guarding') {
                ch[3].rotation.set(0, 0, Math.PI * 0.15 - zL * 0.25); 
                ch[4].rotation.set(0, 0, Math.PI * 0.2 + (isMoving ? 0.3 : 0)); 
                ch[6].rotation.set(0, 0, -Math.PI * 0.15 - zR * 0.25); 
                ch[7].rotation.set(0, 0, -Math.PI * 0.2 - (isMoving ? 0.3 : 0));
            }

            ch[9].position.z = zL * 0.5;   
            ch[10].position.z = zL * 1.0;  
            ch[11].position.z = zL * 1.2 + 0.2; 

            ch[12].position.z = zR * 0.5;  
            ch[13].position.z = zR * 1.0;  
            ch[14].position.z = zR * 1.2 + 0.2; 

            ch[9].position.y = 1.8 + Math.max(0, zL * 0.2);
            ch[10].position.y = -0.3 + Math.max(0, zL * 0.4);
            ch[11].position.y = -1.6 + Math.max(0, zL * 0.5);

            ch[12].position.y = 1.8 + Math.max(0, zR * 0.2);
            ch[13].position.y = -0.3 + Math.max(0, zR * 0.4);
            ch[14].position.y = -1.6 + Math.max(0, zR * 0.5);

            ch[9].rotation.x = zL * 0.2;
            ch[10].rotation.x = zL * 0.3 + (isMoving && zL < 0 ? -0.5 : 0); 
            ch[11].rotation.x = zL * 0.1;

            ch[12].rotation.x = zR * 0.2;
            ch[13].rotation.x = zR * 0.3 + (isMoving && zR < 0 ? -0.5 : 0);
            ch[14].rotation.x = zR * 0.1;
        }

        // アクション用の上書き(腕突き出しアニメーション等)
        if (this.state === 'windup' || this.state === 'attacking') {
            if (ch.length >= 9) {
                if (this.activeArm === 'L') {
                    ch[3].rotation.set(-Math.PI / 2, 0, 0);
                    ch[4].rotation.set(0, 0, 0);
                } else if (this.activeArm === 'R') {
                    ch[6].rotation.set(-Math.PI / 2, 0, 0);
                    ch[7].rotation.set(0, 0, 0);
                } else {
                    ch[3].rotation.set(-Math.PI / 2, 0, 0);
                    ch[4].rotation.set(0, 0, 0);
                    ch[6].rotation.set(-Math.PI / 2, 0, 0);
                    ch[7].rotation.set(0, 0, 0);
                }
            }
        } else if (this.state === 'guarding') {
            if (ch.length >= 9) {
                ch[3].rotation.set(0, 0, Math.PI * 0.6);
                ch[6].rotation.set(0, 0, -Math.PI * 0.6);
            }
        }
    }

    startAttack(arm, opponentList) {
        if (this.state !== 'idle') return false;
        if (getActiveBitCount(this) >= 1) return false; // 同時1つまでの制限

        const target = this.getNearestOpponent(opponentList);
        if (!target) return false;

        this.targetFacing.subVectors(target.pos, this.pos).normalize();
        this.targetFacing.y = 0;

        this.state = 'windup';
        this.activeArm = arm;
        this.timer = 0.3;

        const startPos = this.pos.clone().setY(5);
        let path;
        if (this.isDataSuperior(target)) {
            path = this.buildUnpredictablePath(startPos, target.pos);
        } else {
            path = findPath(this.pos, target.pos, 1.0);
            if (!path) path = [startPos, target.pos.clone()];
        }
        this.currentAttackPath = path;

        if (this.isPlayer) {
            if (this.predLine) scene.remove(this.predLine);
            const points = path.map(p => p.clone().setY(5));
            const geo = new THREE.BufferGeometry().setFromPoints(points);
            const mat = new THREE.LineDashedMaterial({ color: this.color, dashSize: 1, gapSize: 0.5, transparent: true, opacity: 0.8 });
            this.predLine = new THREE.Line(geo, mat);
            this.predLine.computeLineDistances();
            scene.add(this.predLine);
        }
        return true;
    }

    startGuard(arm, attacker) {
        if (this.state !== 'idle' && this.state !== 'guarding') return;
        this.state = 'guarding';
        this.activeArm = arm;
        this.timer = 0.5;
        if (attacker) {
            this.targetFacing.subVectors(attacker.pos, this.pos).normalize();
            this.targetFacing.y = 0;
        }
        playSound('guard');
    }
}

// ==========================================
// 11. AI思考ルーチン(データ量連動)
// ==========================================
function runEnemyAI(enemy, player, dt) {
    if (enemy.hp <= 0) return;
    
    enemy.aiActionTimer -= dt;
    const dataLevel = enemy.dataLevel;
    const isSuperior = enemy.isDataSuperior(player);

    if (enemy.state !== 'idle') return;

    if (player.state === 'windup' && enemy.aiActionTimer <= 0) {
        let guardProb = isSuperior ? Math.min(0.95, 0.65 + (enemy.data - player.data) / 1000) : 0.2 + (dataLevel * 0.06);
        if (Math.random() < guardProb) {
            enemy.startGuard(player.activeArm || 'L', player);
            enemy.aiActionTimer = 0.8;
            return;
        }
    }

    let attackInterval = isSuperior ? Math.max(0.4, 1.8 - dataLevel * 0.12) : Math.max(0.8, 2.5 - dataLevel * 0.1);

    if (enemy.aiActionTimer <= 0 && Math.random() < 0.06 + dataLevel * 0.015) {
        if (getActiveBitCount(enemy) < 1) { // 敵もビット制限
            if (isSuperior) {
                const predictedPos = player.pos.clone().addScaledVector(new THREE.Vector3(player.targetFacing.x, 0, player.targetFacing.z), 5);
                enemy.moveTarget.copy(predictedPos);
            }
            if (enemy.startAttack(Math.random() < 0.5 ? 'L' : 'R', [player])) {
                enemy.aiActionTimer = attackInterval;
                return;
            }
        }
    }

    if (fragments.length > 0) {
        let closest = fragments[0], minDist = enemy.pos.distanceTo(closest.position);
        for (let i = 1; i < fragments.length; i++) {
            let d = enemy.pos.distanceTo(fragments[i].position);
            if (d < minDist) { minDist = d; closest = fragments[i]; }
        }
        enemy.moveTarget.copy(closest.position);
    } else if (enemy.pos.distanceTo(enemy.moveTarget) < 5) {
        enemy.moveTarget.set((Math.random() - 0.5) * 80, 1, (Math.random() - 0.5) * 80);
    }

    const dir = new THREE.Vector3().subVectors(enemy.moveTarget, enemy.pos);
    if (dir.lengthSq() > 0.1) {
        dir.y = 0; dir.normalize();
        enemy.targetFacing.copy(dir);
        const speed = 20 + dataLevel * 1.5;
        const oldX = enemy.pos.x;
        enemy.pos.x += dir.x * speed * dt;
        if (checkCollision(enemy.pos, enemy.radius)) enemy.pos.x = oldX;
        const oldZ = enemy.pos.z;
        enemy.pos.z += dir.z * speed * dt;
        if (checkCollision(enemy.pos, enemy.radius)) enemy.pos.z = oldZ;
        enemy.pos.x = Math.max(-45, Math.min(45, enemy.pos.x));
        enemy.pos.z = Math.max(-45, Math.min(45, enemy.pos.z));
    }
}

// ==========================================
// 12. プレイヤー & ステージ初期化
// ==========================================
const player = new Entity(0x00ffaa, 0, 20, true);

function createEnemyAt(x, z) {
    const e = new Entity(0xff2222, x, z, false);
    e.maxHp = 100; // 敵HP100に修正
    e.hp = 100;
    e.moveTarget = new THREE.Vector3(x, 1, z);
    e.aiActionTimer = 1.0 + Math.random() * 0.5;
    return e;
}

function initStage() {
    player.hp = player.maxHp;
    player.pos.set(0, 1, 20);
    player.lastPos.copy(player.pos);
    player.state = 'idle';
    player.data = 0;
    if (player.predLine) scene.remove(player.predLine);
    player.predLine = null;
    player.facing.set(0, 0, -1);
    player.targetFacing.set(0, 0, -1);
    player.robotGroup.position.copy(player.pos);

    for (const e of enemies) {
        scene.remove(e.robotGroup);
        scene.remove(e.shieldMesh);
        if (e.predLine) scene.remove(e.predLine);
    }
    enemies.length = 0;

    for (const b of bits) b.destroy();
    bits.length = 0;

    const enemyCount = currentStage;
    const spawnPositions = [[0, -20], [-15, -15], [15, -15], [-20, -25], [20, -25]];
    for (let i = 0; i < enemyCount && i < 5; i++) {
        const sp = spawnPositions[i];
        enemies.push(createEnemyAt(sp[0], sp[1]));
    }

    fragments.forEach(f => scene.remove(f));
    fragments.length = 0;

    generateBlocks();

    game.messages = ["SYSTEM: STAGE " + currentStage + " START - ENEMIES: " + enemyCount];
    game.updateUI();
}

// ==========================================
// 13. 入力管理
// ==========================================
const keys = {};
window.addEventListener('keydown', e => { 
    if(["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].indexOf(e.code) > -1) e.preventDefault();
    if(audioCtx.state === 'suspended') audioCtx.resume();
    
    if (e.code === 'Escape') {
        if (gameState === 'PLAYING') switchState('PAUSE');
        else if (gameState === 'PAUSE') switchState('PLAYING');
        return;
    }
    
    if (!keys[e.code]) { keys[e.code] = true; handleInput(e.code); }
});
window.addEventListener('keyup', e => { keys[e.code] = false; });

function handleInput(code) {
    if (gameState !== 'PLAYING') return;
    
    let arm = (code === 'KeyZ') ? 'L' : (code === 'KeyX') ? 'R' : null;
    if (!arm) return;

    const aliveEnemies = enemies.filter(e => e.hp > 0);
    
    // ガード判定の緩和: 近くの敵が攻撃中、または近くに飛んできているビットがある場合のみガード
    const threateningEnemy = aliveEnemies.find(e => 
        (e.state === 'windup' || e.state === 'attacking') && 
        e.pos.distanceTo(player.pos) < 20
    );
    const threateningBit = bits.find(b => 
        b.target === player && 
        b.pos.distanceTo(player.pos) < 15
    );
    
    if ((threateningEnemy || threateningBit) && (player.state === 'idle' || player.state === 'guarding')) {
        player.startGuard(arm, threateningEnemy || (threateningBit ? threateningBit.owner : null));
    } else {
        player.startAttack(arm, aliveEnemies);
    }
}

// ==========================================
// 14. アイテムスポーン
// ==========================================
setInterval(() => {
    if (gameState !== 'PLAYING') return;
    for (let k = 0; k < 5; k++) {
        const mesh = new THREE.Mesh(new THREE.OctahedronGeometry(1.2), new THREE.MeshBasicMaterial({ color: 0x00ffff, wireframe: true }));
        let valid = false, pos = new THREE.Vector3();
        let tries = 0;
        while (!valid && tries < 20) {
            pos.set((Math.random() - 0.5) * 80, 1.5, (Math.random() - 0.5) * 80);
            if (!checkCollision(pos, 2.0)) valid = true;
            tries++;
        }
        if (!valid) continue;
        mesh.position.copy(pos);
        mesh.userData.type = 'data';
        scene.add(mesh); fragments.push(mesh);
    }
}, 4000);

setInterval(() => {
    if (gameState !== 'PLAYING') return;
    for (let k = 0; k < 2; k++) {
        const mesh = new THREE.Mesh(new THREE.OctahedronGeometry(1.5), new THREE.MeshBasicMaterial({ color: 0xadff2f, wireframe: true }));
        let valid = false, pos = new THREE.Vector3();
        let tries = 0;
        while (!valid && tries < 20) {
            pos.set((Math.random() - 0.5) * 80, 1.5, (Math.random() - 0.5) * 80);
            if (!checkCollision(pos, 2.0)) valid = true;
            tries++;
        }
        if (!valid) continue;
        mesh.position.copy(pos);
        mesh.userData.type = 'regen';
        scene.add(mesh); fragments.push(mesh);
    }
}, 4000);

// ==========================================
// 15. メインロジック更新
// ==========================================
function updateLogic(dt) {
    const aliveEnemies = enemies.filter(e => e.hp > 0);
    const promptEl = document.getElementById('centerPrompt');

    const threateningEnemy = aliveEnemies.find(e => 
        (e.state === 'windup' || e.state === 'attacking') && 
        e.pos.distanceTo(player.pos) < 20
    );
    const threateningBit = bits.find(b => 
        b.target === player && 
        b.pos.distanceTo(player.pos) < 15
    );

    if (threateningEnemy || threateningBit) {
        promptEl.innerText = "敵攻撃! [Z]か[X]でガード!";
        promptEl.className = 'obstructed';
    } else if (player.state === 'idle') {
        if (aliveEnemies.length > 0) {
            promptEl.innerText = "LOCK ON - [Z/X]で攻撃";
            promptEl.className = 'locked-on';
        } else {
            promptEl.innerText = "";
            promptEl.className = '';
        }
    } else {
        promptEl.innerText = "";
        promptEl.className = '';
    }

    if (player.state === 'idle' || player.state === 'windup' || player.state === 'guarding') {
        let dx = (keys['ArrowRight'] ? 1 : 0) - (keys['ArrowLeft'] ? 1 : 0);
        let dz = (keys['ArrowDown'] ? 1 : 0) - (keys['ArrowUp'] ? 1 : 0);
        
        if (dx !== 0 || dz !== 0) {
            const moveDir = new THREE.Vector3(dx, 0, dz).normalize();
            player.targetFacing.copy(moveDir);
            const move = moveDir.clone().multiplyScalar(30 * dt);
            
            const oldX = player.pos.x;
            player.pos.x = Math.max(-45, Math.min(45, player.pos.x + move.x));
            if(checkCollision(player.pos, player.radius)) player.pos.x = oldX;

            const oldZ = player.pos.z;
            player.pos.z = Math.max(-45, Math.min(45, player.pos.z + move.z));
            if(checkCollision(player.pos, player.radius)) player.pos.z = oldZ;
        }
    }

    for (const e of enemies) {
        if (e.hp > 0) runEnemyAI(e, player, dt);
    }

    player.update(dt, aliveEnemies);
    for (const e of enemies) e.update(dt, [player]);

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

    for (let i = fragments.length - 1; i >= 0; i--) {
        fragments[i].rotation.y += dt * 2;
        fragments[i].rotation.x += dt;

        if (player.pos.distanceTo(fragments[i].position) < 5) {
            collectItem(player, fragments[i]);
            scene.remove(fragments[i]);
            fragments.splice(i, 1);
            continue;
        }
        let collected = false;
        for (const e of aliveEnemies) {
            if (e.pos.distanceTo(fragments[i].position) < 5) {
                collectItem(e, fragments[i]);
                scene.remove(fragments[i]);
                fragments.splice(i, 1);
                collected = true;
                break;
            }
        }
    }

    updateFloatingTexts(dt);
    game.updateUI();
    
    if (player.hp <= 0 && gameState === 'PLAYING') {
        game.addMessage("SYSTEM: 機体大破。GAME OVER");
        switchState('GAME_OVER');
    } else if (aliveEnemies.length === 0 && enemies.length > 0 && gameState === 'PLAYING') {
        game.addMessage("SYSTEM: 全敵AI沈黙。STAGE CLEAR!");
        switchState('STAGE_CLEAR');
        setTimeout(() => {
            currentStage++;
            if (currentStage > maxStages) switchState('GAME_CLEAR');
            else switchState('STAGE_START');
        }, 3000);
    }
}

function collectItem(collector, itemMesh) {
    if (itemMesh.userData.type === 'data') {
        collector.data += 100;
        createFloatingText("+DATA 100", collector.pos.clone().add(new THREE.Vector3(0,4,0)), "#00ffff");
    } else {
        collector.regenTimer = 10;
        createFloatingText("+REGEN", collector.pos.clone().add(new THREE.Vector3(0,4,0)), "#adff2f");
    }
}

// ==========================================
// 16. メインループ
// ==========================================
const clock = new THREE.Clock();
function animate() {
    requestAnimationFrame(animate);
    const dt = Math.min(clock.getDelta(), 0.1);
    
    if (gameState === 'PLAYING') {
        updateLogic(dt);
    } else if (gameState !== 'PAUSE') {
        updateFloatingTexts(dt);
        updateAfterImages(dt);
        updateParticles(dt);
    }
    
    renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
小ぶりなプログラムを試しに作っているんですが、 ここではその説明書きをしていこうと思います。 こういう機能をつけてみてほしいだとか要望、 コメント欄か、Xのリプライ欄に書いてみて下さい。 ひまをみて対応します。 (未管理著作物裁定制度に定められた問い合わせも受付中。)
ロボットミニゲーム「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