建設系レースゲーム「BuildRace」


【更新履歴】

・2026/5/17 バージョン1.0公開。
・2026/5/17 バージョン1.1公開。
・2026/5/17 バージョン1.2公開。
・2026/5/18 バージョン1.3公開。
・2026/5/18 バージョン1.4公開。

画像


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

https://drive.google.com/drive/folders/1ebG3fyxw6jS63gLVqAc603ti-sJryFip?ths=true

※このゲームで遊ぶには、いつものテスト用ブラウザが必要です。↓

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

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>BuildRace</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #000; font-family: sans-serif; user-select: none; }
        
        #ui { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
        
        h1 { position: absolute; top: 10px; left: 15px; margin: 0; font-size: 28px; color: #ffeb3b; text-shadow: 2px 2px 4px #000; }
        
        #topCenter {
            position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
            text-align: center; text-shadow: 2px 2px 4px #000; color: white;
            background: rgba(0,0,0,0.5); padding: 5px 20px; border-radius: 10px;
        }
        #stageBox { font-size: 28px; font-weight: bold; color: #ffeb3b; }

        .hp-container {
            position: absolute; bottom: 20px; width: 250px;
            background: rgba(0,0,0,0.7); padding: 10px; border-radius: 8px; border: 2px solid #555;
            text-shadow: 1px 1px 2px #000; color: white;
        }
        #bottomLeft { left: 20px; }
        #bottomRight { right: 20px; text-align: right; }
        
        .hp-name { font-size: 16px; font-weight: bold; margin-bottom: 5px; }
        .hp-bar-bg { width: 100%; height: 16px; background: #222; border-radius: 8px; overflow: hidden; border: 1px solid #000; }
        .hp-bar-fill { height: 100%; transition: width 0.2s ease-out; }
        #p1-fill { background: linear-gradient(90deg, #0088aa, #00e5ff); width: 100%; }
        #bottomRight .hp-bar-bg { display: flex; }
        #cpu-fill { background: linear-gradient(270deg, #aa00aa, #ff00ff); width: 100%; margin-left: auto; }

        #instructions {
            position: absolute; top: 10px; right: 15px; font-size: 13px; line-height: 1.5; 
            background: rgba(0,0,0,0.6); padding: 10px; border-radius: 8px; color: white;
        }
        .highlight { color: #00e5ff; font-weight: bold; }

        #speedIndicator {
            position: absolute; bottom: 80px; left: 20px;
            font-size: 20px; font-weight: bold; color: #ff9900;
            text-shadow: 0 0 12px #ff6600, 0 0 4px #000;
            display: none; pointer-events: none;
        }

        #jumpIndicator {
            position: absolute; bottom: 110px; left: 20px;
            font-size: 20px; font-weight: bold; color: #00aaff;
            text-shadow: 0 0 12px #0055ff, 0 0 4px #000;
            display: none; pointer-events: none;
        }
        
        #overlay {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.8); display: flex; flex-direction: column;
            justify-content: center; align-items: center; color: white; z-index: 20;
            pointer-events: auto;
        }
        #overlayMsg { font-size: 48px; font-weight: bold; margin-bottom: 20px; text-shadow: 2px 2px 10px #000; text-align: center; }
        #overlayBtn { padding: 15px 30px; font-size: 20px; cursor: pointer; background: #ffeb3b; border: none; border-radius: 10px; font-weight: bold; }
        #overlayBtn:hover { background: #fff59d; }

        #pauseOverlay {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.5); display: none; flex-direction: column;
            justify-content: center; align-items: center; color: white; z-index: 30;
            font-size: 64px; font-weight: bold; letter-spacing: 5px; text-shadow: 4px 4px 10px #000;
        }
    </style>
</head>
<body>
    <div id="ui">
        <h1>BuildRace</h1>
        <div id="topCenter"><div id="stageBox">STAGE <span id="stageDisplay">1</span> / 8</div></div>
        <div id="bottomLeft" class="hp-container">
            <div class="hp-name" style="color:#00e5ff">P1 HP: <span id="p1HP">10</span>/10</div>
            <div class="hp-bar-bg"><div id="p1-fill" class="hp-bar-fill"></div></div>
        </div>
        <div id="bottomRight" class="hp-container">
            <div class="hp-name" style="color:#ff00ff">CPU HP: <span id="cpuHP">10</span>/10</div>
            <div class="hp-bar-bg"><div id="cpu-fill" class="hp-bar-fill"></div></div>
        </div>
        <div id="speedIndicator">&#9889; SPEED BOOST!</div>
        <div id="jumpIndicator">&#8593; JUMP BOOST! x<span id="jumpCharges">0</span></div>
        <div id="instructions">
            [&#x77E2;&#x5370;]&#x79FB;&#x52D5; / [Esc]PAUSE<br>
            <span style="color:#ffeb3b">[A] 道を作る / 引っこ抜く</span> (自動アシスト)<br>
            <span style="color:#ff9900">[Z] 相手コートへ投げる</span> (妨害&資源奪取)<br>
            &#x203B;山積みのブロックは一番下から引っこ抜き可能!<br>
            <span style="color:#ff5555; font-weight:bold;">&#x25A0;爆弾(&#x8D64;)</span>: 周囲隆起 / <span style="color:#55ff55; font-weight:bold;">&#x25A0;回復(&#x7DD1;)</span>: HP+1<br>
            <span style="color:#ff9900; font-weight:bold;">&#x25A0;速(&#x6A59;)</span>: SPD+4秒 / <span style="color:#00aaff; font-weight:bold;">&#x25A0;跳(&#x9752;)</span>: JUMP+2(3回)<br>
            <span class="highlight">相手より先にゴールへ!落ちてくる物も資材になる!</span>
        </div>
    </div>
    
    <div id="overlay">
        <div id="overlayMsg">BuildRace</div>
        <button id="overlayBtn">GAME START</button>
    </div>

    <div id="pauseOverlay">PAUSE</div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
    
    <script>
        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        function playSound(type, pitch = 1) {
            if(audioCtx.state === 'suspended') audioCtx.resume();
            const osc = audioCtx.createOscillator();
            const gain = audioCtx.createGain();
            osc.connect(gain); gain.connect(audioCtx.destination);
            let now = audioCtx.currentTime;
            
            if (type === 'jump') {
                osc.type = 'sine'; osc.frequency.setValueAtTime(400 * pitch, now); osc.frequency.linearRampToValueAtTime(600 * pitch, now + 0.15);
                gain.gain.setValueAtTime(0.2, now); gain.gain.linearRampToValueAtTime(0, now + 0.15);
                osc.start(now); osc.stop(now + 0.15);
            } else if (type === 'throw') {
                osc.type = 'triangle'; osc.frequency.setValueAtTime(800 * pitch, now); osc.frequency.exponentialRampToValueAtTime(200, now + 0.3);
                gain.gain.setValueAtTime(0.3, now); gain.gain.linearRampToValueAtTime(0, now + 0.3);
                osc.start(now); osc.stop(now + 0.3);
            } else if (type === 'bomb') {
                osc.type = 'square'; osc.frequency.setValueAtTime(100, now); osc.frequency.exponentialRampToValueAtTime(10, now + 0.5);
                gain.gain.setValueAtTime(0.5, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
                osc.start(now); osc.stop(now + 0.5);
            } else if (type === 'damage') {
                osc.type = 'sawtooth'; osc.frequency.setValueAtTime(200, now); osc.frequency.linearRampToValueAtTime(50, now + 0.2);
                gain.gain.setValueAtTime(0.5, now); gain.gain.linearRampToValueAtTime(0, now + 0.2);
                osc.start(now); osc.stop(now + 0.2);
            } else if (type === 'heal') {
                osc.type = 'sine'; osc.frequency.setValueAtTime(800, now); osc.frequency.linearRampToValueAtTime(1200, now + 0.2);
                gain.gain.setValueAtTime(0.3, now); gain.gain.linearRampToValueAtTime(0, now + 0.2);
                osc.start(now); osc.stop(now + 0.2);
            } else if (type === 'speed') {
                osc.type = 'sine'; osc.frequency.setValueAtTime(600, now); osc.frequency.linearRampToValueAtTime(1400, now + 0.15);
                gain.gain.setValueAtTime(0.35, now); gain.gain.linearRampToValueAtTime(0, now + 0.25);
                osc.start(now); osc.stop(now + 0.25);
            } else if (type === 'hit') {
                osc.type = 'square'; osc.frequency.setValueAtTime(150, now); osc.frequency.exponentialRampToValueAtTime(50, now + 0.1);
                gain.gain.setValueAtTime(0.5, now); gain.gain.linearRampToValueAtTime(0, now + 0.1);
                osc.start(now); osc.stop(now + 0.1);
            } else if (type === 'fall') {
                osc.type = 'sine'; osc.frequency.setValueAtTime(300, now); osc.frequency.exponentialRampToValueAtTime(50, now + 0.6);
                gain.gain.setValueAtTime(0.3, now); gain.gain.linearRampToValueAtTime(0, now + 0.6);
                osc.start(now); osc.stop(now + 0.6);
            }
        }

        const scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x000011, 0.02);
        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        const controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enablePan = false; 
        controls.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.DOLLY };
        controls.maxPolarAngle = Math.PI / 2.5; 
        controls.minPolarAngle = Math.PI / 6;
        controls.maxDistance = 30; controls.minDistance = 5;

        const light = new THREE.DirectionalLight(0xffffff, 1.2); light.position.set(10, 20, 10); scene.add(light);
        scene.add(new THREE.AmbientLight(0x555555));

        const starsGeo = new THREE.BufferGeometry();
        const starsMat = new THREE.PointsMaterial({color: 0xffffff, size: 0.15});
        const starVerts = [];
        for(let i=0; i<1500; i++) starVerts.push((Math.random()-0.5)*100, Math.random()*50-10, (Math.random()-0.5)*100);
        starsGeo.setAttribute('position', new THREE.Float32BufferAttribute(starVerts, 3));
        const stars = new THREE.Points(starsGeo, starsMat); scene.add(stars);

        let currentStage = 1;
        const MAX_STAGE = 8;
        let MAP_LENGTH = 50;
        let gameActive = false;
        let isPaused = false; 
        
        const gridMap = new Map();
        const blocks = [];
        const animatingBlocks = []; 
        let goalLine = null;
        let baseCamOffset = new THREE.Vector3(0, 14, 12); 

        const passTypes = ['heal', 'speed', 'jump'];

        function getGridKey(x, y, z) { return `${Math.round(x)},${Math.round(y)},${Math.round(z)}`; }
        
        function setGrid(x, y, z, block) { 
            gridMap.set(getGridKey(x, y, z), block); 
            if (typeof cpuNotifyWorldChanged === 'function') cpuNotifyWorldChanged();
        }
        
        function getGrid(x, y, z) { return gridMap.get(getGridKey(x, y, z)); }
        
        function removeGrid(x, y, z) { 
            gridMap.delete(getGridKey(x, y, z)); 
            if (typeof cpuNotifyWorldChanged === 'function') cpuNotifyWorldChanged();
        }

        function getDropY(x, z, startY) {
            for (let y = startY + 5; y >= -15; y--) if (getGrid(x, y, z)) return y + 1;
            return -20;
        }

        function dropColumn(x, emptyY, z) {
            let currentY = emptyY;
            while (true) {
                let above = getGrid(x, currentY + 1, z);
                if (above && !above.userData.isStatic && !passTypes.includes(above.userData.type)) {
                    removeGrid(x, currentY + 1, z);
                    above.position.y = currentY; 
                    setGrid(x, currentY, z, above);
                    currentY++;
                } else break;
            }
        }

        function dropColumnAnimated(x, emptyY, z, owner) {
            let checkY = emptyY + 1;
            let landY  = emptyY;
            while (true) {
                let above = getGrid(x, checkY, z);
                if (!above || above.userData.isStatic || passTypes.includes(above.userData.type)) break;
                removeGrid(x, checkY, z);
                animatingBlocks.push({
                    mesh: above, sx: x, sy: checkY, sz: z, ex: x, ey: landY,  ez: z,
                    fallStartY: checkY, time: 0, duration: Math.max(0.18, (checkY - landY) * 0.12),
                    arcMax: 0, owner: owner, isFalling: true, isKicked: false
                });
                landY++; checkY++;
            }
        }

        const blockGeo = new THREE.BoxGeometry(1, 1, 1);
        const healGeo  = new THREE.BoxGeometry(0.5, 0.5, 0.5);
        const weightColors   = [0xffffff, 0xaaaaaa, 0x444444]; 

        function createBlock(x, y, z, weight, isStatic, type = 'normal') {
            let mat, geo = blockGeo;
            if      (type === 'bomb')  { mat = new THREE.MeshLambertMaterial({ color: 0xff0000, emissive: 0x550000 }); }
            else if (type === 'heal')  { mat = new THREE.MeshLambertMaterial({ color: 0x55ff55, emissive: 0x225522 }); geo = healGeo; }
            else if (type === 'speed') { mat = new THREE.MeshLambertMaterial({ color: 0xff9900, emissive: 0x442200 }); geo = healGeo; }
            else if (type === 'jump')  { mat = new THREE.MeshLambertMaterial({ color: 0x0088ff, emissive: 0x002288 }); geo = healGeo; }
            else                       { mat = new THREE.MeshLambertMaterial({ color: isStatic ? 0x111133 : weightColors[weight - 1] }); }
            
            const mesh = new THREE.Mesh(geo, mat);
            mesh.position.set(x, y, z);
            if (passTypes.includes(type)) mesh.position.y -= 0.25; 
            
            mesh.userData = { weight, isStatic, type };
            scene.add(mesh); blocks.push(mesh); setGrid(x, y, z, mesh);
            return mesh;
        }

        function explodeBomb(bx, by, bz) {
            playSound('bomb');
            for (let dx = -1; dx <= 1; dx++) {
                for (let dz = -1; dz <= 1; dz++) {
                    let tx = Math.round(bx + dx), tz = Math.round(bz + dz);
                    if ((tx >= -7 && tx <= -3) || (tx >= 3 && tx <= 7)) {
                        let topY = getDropY(tx, tz, by + 10) - 1; 
                        let addCount = Math.floor(Math.random() * 2) + 2; 
                        for (let i = 1; i <= addCount; i++) {
                            createBlock(tx, topY + i, tz, 2, false, 'normal'); 
                        }
                    }
                }
            }
        }

        const cursorGeo = new THREE.PlaneGeometry(0.95, 0.95);
        const cursorMat = new THREE.MeshBasicMaterial({color: 0xff3333, transparent: true, opacity: 0.7, side: THREE.DoubleSide});
        const targetCursor = new THREE.Mesh(cursorGeo, cursorMat);
        targetCursor.rotation.x = -Math.PI / 2; targetCursor.visible = false; scene.add(targetCursor);

        const liftCursorMat = new THREE.MeshBasicMaterial({color: 0xffff00, transparent: true, opacity: 0.8, side: THREE.DoubleSide});
        const liftCursor = new THREE.Mesh(cursorGeo, liftCursorMat);
        liftCursor.rotation.x = -Math.PI / 2; liftCursor.visible = false; scene.add(liftCursor);

        const dropDashMat = new THREE.LineDashedMaterial({ color: 0xff3333, dashSize: 0.2, gapSize: 0.2 });
        const dropPts = [
            new THREE.Vector3(-0.45, 0, -0.45), new THREE.Vector3(0.45, 0, -0.45),
            new THREE.Vector3(0.45, 0, 0.45),   new THREE.Vector3(-0.45, 0, 0.45),
            new THREE.Vector3(-0.45, 0, -0.45)
        ];
        const dropDashGeo = new THREE.BufferGeometry().setFromPoints(dropPts);

        function createDropCursor(x, y, z) {
            const line = new THREE.Line(dropDashGeo, dropDashMat);
            line.computeLineDistances();
            line.position.set(x, Math.max(y, -5) + 0.52, z);
            scene.add(line); return line;
        }

        function getSmartLiftTarget(px, py, pz, dirX, dirZ) {
            let visited = new Set();
            let queue = [];
            queue.push({x: Math.round(px), y: Math.round(py), z: Math.round(pz), cost: 0, depth: 0});
            visited.add(getGridKey(px, py, pz));

            let bestTarget = null;
            let bestScore = Infinity;
            let iterations = 0;
            
            let pxR = Math.round(px);
            let pzR = Math.round(pz);

            while (queue.length > 0 && iterations < 100) {
                iterations++;
                queue.sort((a, b) => a.cost - b.cost);
                let curr = queue.shift();

                if (curr.cost >= bestScore) continue;
                if (curr.depth > 2) continue; 

                const dirs = [
                    { dx: dirX, dy: 0, dz: dirZ, cost: 1 },    
                    { dx: 0, dy: -1, dz: 0, cost: 2 },         
                    { dx: 0, dy: 1, dz: 0, cost: 2 },          
                    { dx: -dirX, dy: 0, dz: -dirZ, cost: 3 },  
                    { dx: dirZ, dy: 0, dz: -dirX, cost: 3 },   
                    { dx: -dirZ, dy: 0, dz: dirX, cost: 3 }    
                ];

                for (let d of dirs) {
                    let nx = curr.x + d.dx;
                    let ny = curr.y + d.dy;
                    let nz = curr.z + d.dz;
                    let key = getGridKey(nx, ny, nz);

                    if (Math.abs(nx - pxR) + Math.abs(nz - pzR) > 1) continue;

                    if (visited.has(key)) continue;
                    visited.add(key);

                    let nextCost = curr.cost + d.cost;
                    
                    let b = getGrid(nx, ny, nz);
                    if (b) {
                        if (passTypes.includes(b.userData.type)) {
                            queue.push({x: nx, y: ny, z: nz, cost: nextCost, depth: curr.depth + 1});
                        }
                        else if (!b.userData.isStatic) {
                            if (nextCost < bestScore) {
                                bestScore = nextCost;
                                bestTarget = { block: b, x: nx, y: ny, z: nz };
                            }
                        }
                    } else {
                        queue.push({x: nx, y: ny, z: nz, cost: nextCost, depth: curr.depth + 1});
                    }
                }
            }
            return bestTarget;
        }

        class Slime {
            constructor(x, y, z, color, isPlayer) {
                this.mesh = new THREE.Group();
                const body = new THREE.Mesh(new THREE.SphereGeometry(0.4, 16, 16), new THREE.MeshLambertMaterial({color}));
                body.scale.y = 0.8; body.position.y = 0.4; this.mesh.add(body);
                this.mesh.position.set(x, y, z); scene.add(this.mesh);
                
                this.isPlayer = isPlayer;
                this.hp = 10;
                this.reset(x, y, z);
            }

            reset(x, y, z) {
                this.gridX = x; this.gridY = y; this.gridZ = z;
                this.dirX = 0; this.dirZ = -1;
                this.mesh.position.set(x, y, z);
                
                if (this.heldBlock) { scene.remove(this.heldBlock); this.heldBlock = null; }
                this.isJumping   = false;
                this.jumpTime    = 0;
                this.jumpDuration = 0.25;
                this.startPos    = new THREE.Vector3();
                this.endPos      = new THREE.Vector3();
                this.speedBoostTimer = 0; 
                this.jumpBoostCharges = 0; 
                this.updateHPUI();
            }

            takeDamage(dmg) {
                if (this.hp <= 0) return;
                this.hp -= dmg;
                playSound('damage');
                if (this.hp < 0) this.hp = 0;
                this.updateHPUI();
                
                const mat = this.mesh.children[0].material;
                const oldColor = mat.emissive.getHex();
                mat.emissive.setHex(0xff0000);
                setTimeout(() => mat.emissive.setHex(oldColor), 200);
            }

            heal(amt) {
                if (this.hp <= 0) return;
                this.hp += amt;
                if (this.hp > 10) this.hp = 10;
                playSound('heal');
                this.updateHPUI();
            }

            updateHPUI() {
                if (this.isPlayer) {
                    document.getElementById('p1HP').innerText = this.hp;
                    document.getElementById('p1-fill').style.width = (this.hp * 10) + '%';
                    const ji = document.getElementById('jumpIndicator');
                    if (this.jumpBoostCharges > 0) {
                        ji.style.display = 'block';
                        document.getElementById('jumpCharges').innerText = this.jumpBoostCharges;
                    } else {
                        ji.style.display = 'none';
                    }
                } else {
                    document.getElementById('cpuHP').innerText = this.hp;
                    document.getElementById('cpu-fill').style.width = (this.hp * 10) + '%';
                }
            }

            knockback() {
                const dirs = [[1,0], [-1,0], [0,1], [0,-1], [1,1], [1,-1], [-1,1], [-1,-1]];
                dirs.sort(() => Math.random() - 0.5); 
                for (let [dx, dz] of dirs) {
                    let nx = this.gridX + dx, nz = this.gridZ + dz;
                    if ((nx >= -7 && nx <= -3) || (nx >= 3 && nx <= 7)) {
                        if (!getGrid(nx, this.gridY + 1, nz) && !getGrid(nx, this.gridY + 2, nz)) {
                            let targetY = this.gridY;
                            while(targetY >= this.gridY - 10) {
                                if (getGrid(nx, targetY - 1, nz)) break;
                                targetY--;
                            }
                            if (targetY >= this.gridY - 10) {
                                this.doJump(nx, targetY, nz);
                                return;
                            }
                        }
                    }
                }
            }

            update(dt, timeElapsed) {
                if (this.hp <= 0) return;

                if (this.speedBoostTimer > 0) this.speedBoostTimer -= dt;

                if (this.isJumping) {
                    this.jumpTime += dt;
                    let t = Math.min(this.jumpTime / this.jumpDuration, 1);
                    this.mesh.position.lerpVectors(this.startPos, this.endPos, t);
                    this.mesh.position.y += Math.sin(t * Math.PI) * 1.2;
                    if (t >= 1) { this.isJumping = false; this.checkItemPickup(); }
                }
                
                if (this.isPlayer && gameActive) {
                    targetCursor.visible = false;
                    liftCursor.visible = false;

                    if (this.heldBlock) {
                        let tgt = this.getSmartPlaceTarget();
                        if (tgt) {
                            targetCursor.position.set(tgt.x, Math.max(tgt.y, -5) + 0.51, tgt.z);
                            targetCursor.visible = true;
                            if (tgt.isSmart) {
                                // スマートに適切な場所(青色で激しく点滅)
                                targetCursor.material.opacity = 0.3 + 0.7 * Math.abs(Math.sin(timeElapsed * 15));
                                targetCursor.material.color.setHex(0x00e5ff);
                            } else {
                                // 近くにない場合のデフォルト(赤色で点灯)
                                targetCursor.material.opacity = 0.7;
                                targetCursor.material.color.setHex(0xff3333);
                            }
                        }
                    } else if (!this.isJumping) {
                        let tgt = getSmartLiftTarget(this.gridX, this.gridY, this.gridZ, this.dirX, this.dirZ);
                        if (tgt) {
                            liftCursor.position.set(tgt.x, Math.max(tgt.y, -5) + 0.51, tgt.z);
                            liftCursor.visible = true;
                        }
                    }
                }
            }

            checkItemPickup() {
                let curB = getGrid(this.gridX, this.gridY, this.gridZ);
                if (curB) {
                    if (curB.userData.type === 'heal') {
                        this.heal(1);
                        removeGrid(this.gridX, this.gridY, this.gridZ); scene.remove(curB);
                    } else if (curB.userData.type === 'speed') {
                        this.speedBoostTimer = 4.0;
                        playSound('speed');
                        removeGrid(this.gridX, this.gridY, this.gridZ); scene.remove(curB);
                    } else if (curB.userData.type === 'jump') {
                        this.jumpBoostCharges = 3;
                        playSound('speed', 1.5);
                        this.updateHPUI();
                        removeGrid(this.gridX, this.gridY, this.gridZ); scene.remove(curB);
                    }
                }
            }

            tryMove(dx, dz) {
                if (this.isJumping || this.hp <= 0) return false;
                
                this.dirX = dx; this.dirZ = dz;
                let nx = this.gridX + dx, nz = this.gridZ + dz, ny = this.gridY;

                let maxJump = this.jumpBoostCharges > 0 ? 3 : 1; 

                for (let dy = maxJump; dy >= -4; dy--) {
                    let targetY = ny + dy;
                    let floorB = getGrid(nx, targetY - 1, nz);
                    let space1 = getGrid(nx, targetY, nz);
                    let space2 = getGrid(nx, targetY + 1, nz);
                    
                    if (space1 && passTypes.includes(space1.userData.type)) space1 = null;
                    if (space2 && passTypes.includes(space2.userData.type)) space2 = null;
                    if (floorB && passTypes.includes(floorB.userData.type)) floorB = null; 

                    if (floorB && !space1 && !space2) {
                        if (dy > 1 && this.jumpBoostCharges > 0) {
                            this.jumpBoostCharges--;
                            this.updateHPUI();
                        }
                        this.doJump(nx, targetY, nz);
                        return true;
                    }
                }
                return false;
            }

            doJump(x, y, z) {
                this.startPos.copy(this.mesh.position); this.endPos.set(x, y, z);
                this.gridX = x; this.gridY = y; this.gridZ = z;
                this.isJumping = true; this.jumpTime = 0;
                this.jumpDuration = this.speedBoostTimer > 0 ? 0.12 : 0.25;
                playSound('jump');
            }

            getSmartPlaceTarget() {
                let dx = this.dirX;
                let dz = this.dirZ;
                if (dx === 0 && dz === 0) dz = -1; // デフォルトは前

                // 1. まず壁登りアシスト(目の前が登れない壁なら足元を押し上げる)
                let frontX = this.gridX + dx;
                let frontZ = this.gridZ + dz;
                let frontTopY = getDropY(frontX, frontZ, this.gridY + 4) - 1;
                let maxJump = this.jumpBoostCharges > 0 ? 3 : 1;
                
                if (frontTopY > this.gridY - 1 + maxJump) {
                    return {x: this.gridX, y: getDropY(this.gridX, this.gridZ, this.gridY + 4), z: this.gridZ, isSmart: true};
                }

                // 2. 進行方向に向かって穴埋めや階段を探す (A* 的な前進探索)
                let bestTarget = null;
                let queue = [{x: this.gridX, z: this.gridZ, y: this.gridY - 1, depth: 0}];
                let visited = new Set();
                visited.add(`${this.gridX},${this.gridZ}`);

                while (queue.length > 0) {
                    let curr = queue.shift();
                    if (curr.depth >= 4) continue; // 最大4マス先まで探索

                    let nx = curr.x + dx;
                    let nz = curr.z + dz;

                    if (nx < -7 || (nx > -3 && nx < 3) || nx > 7) continue;

                    let key = `${nx},${nz}`;
                    if (visited.has(key)) continue;
                    visited.add(key);

                    let targetTopY = getDropY(nx, nz, this.gridY + 4) - 1;

                    if (targetTopY < curr.y) {
                        // 穴を見つけた -> 穴を埋める
                        bestTarget = {x: nx, y: targetTopY + 1, z: nz, isSmart: true};
                        break;
                    } else if (targetTopY > curr.y + maxJump) {
                        // 登れない壁を見つけた -> その手前に積む
                        bestTarget = {x: curr.x, y: getDropY(curr.x, curr.z, this.gridY + 4), z: curr.z, isSmart: true};
                        break;
                    }

                    // 平坦なら次を探索
                    queue.push({x: nx, z: nz, y: targetTopY, depth: curr.depth + 1});
                }

                if (bestTarget) return bestTarget;

                // 3. 適した場所が近くになければ、射程距離内(デフォルト2マス先)に置く
                let defDist = 2;
                let tx = this.gridX + dx * defDist;
                let tz = this.gridZ + dz * defDist;
                
                // マップ外にはみ出ないように補正
                if (tx < -7 || (tx > -3 && tx < 3) || tx > 7) {
                    tx = this.gridX + dx;
                    tz = this.gridZ + dz;
                }
                
                return {x: tx, y: getDropY(tx, tz, this.gridY + 4), z: tz, isSmart: false};
            }

            actionLiftOrPlace() {
                if (this.hp <= 0 || this.isJumping) return;
                
                if (this.heldBlock) {
                    let tgt = this.getSmartPlaceTarget();
                    if (!tgt) return;

                    let placeCheck = getGrid(cpu.gridX, cpu.gridY - 1, cpu.gridZ - 1);
                    if (placeCheck && !this.isPlayer) {
                        // CPUの重複設置防止用
                    }

                    let block = this.heldBlock;
                    this.heldBlock = null; scene.attach(block);
                    playSound('throw', 1.5);

                    let pos = new THREE.Vector3(); block.getWorldPosition(pos); 
                    animatingBlocks.push({
                        mesh: block, sx: pos.x, sy: pos.y, sz: pos.z,
                        ex: tgt.x, ey: tgt.y, ez: tgt.z,
                        time: 0, duration: 0.15, arcMax: 1,
                        owner: this, isFalling: false, isKicked: false 
                    });
                } else {
                    let target = getSmartLiftTarget(this.gridX, this.gridY, this.gridZ, this.dirX, this.dirZ);
                    if (target) {
                        removeGrid(target.x, target.y, target.z);
                        this.heldBlock = target.block;
                        this.heldBlock.position.set(0, 1.2, 0); this.mesh.add(this.heldBlock);
                        dropColumn(target.x, target.y, target.z); 
                        playSound('jump', 0.8);
                    }
                }
            }

            actionInterfere() {
                if (this.hp <= 0) return;

                if (!this.isPlayer) {
                    if (Math.abs(player.gridX - this.gridX) <= 1 && Math.abs(player.gridZ - this.gridZ) <= 1) {
                        return; 
                    }
                }
                
                let targetSide = this.isPlayer ? 5 : -5;

                if (this.heldBlock) {
                    let block = this.heldBlock;
                    this.heldBlock = null; scene.attach(block);
                    playSound('throw', 0.5);

                    let tx = targetSide + (Math.floor(Math.random() * 5) - 2);
                    let tz = this.gridZ;
                    let ty = getDropY(tx, tz, 15);

                    let pos = new THREE.Vector3(); block.getWorldPosition(pos);
                    animatingBlocks.push({
                        mesh: block, sx: pos.x, sy: pos.y, sz: pos.z,
                        ex: tx, ey: ty, ez: tz,
                        time: 0, duration: 0.5, arcMax: 4,
                        owner: this, isFalling: false, isKicked: true, finalY: ty
                    });
                } else {
                    let tgt = getSmartLiftTarget(this.gridX, this.gridY, this.gridZ, this.dirX, this.dirZ);
                    if (tgt) {
                        removeGrid(tgt.x, tgt.y, tgt.z);
                        let block = tgt.block;
                        playSound('throw', 0.5);

                        let tx = targetSide + (Math.floor(Math.random() * 5) - 2);
                        let tz = tgt.z;
                        let ty = getDropY(tx, tz, 15);

                        let pos = new THREE.Vector3(); block.getWorldPosition(pos);
                        animatingBlocks.push({
                            mesh: block, sx: pos.x, sy: pos.y, sz: pos.z,
                            ex: tx, ey: ty, ez: tz,
                            time: 0, duration: 0.5, arcMax: 4,
                            owner: this, isFalling: false, isKicked: true, finalY: ty
                        });
                        dropColumnAnimated(tgt.x, tgt.y, tgt.z, this);
                    }
                }
            }
        }

        const player = new Slime(-5, 1, 0, 0x00e5ff, true);
        const cpu    = new Slime(5,  1, 0, 0xff00ff, false);

        // =========================================================
        // CPU AI CORE SYSTEM
        // =========================================================

        const CPU_AI = {
            THINK_INTERVAL: 0.16,
            MAX_SEARCH_NODE: 220,
            MAX_PATH_DEPTH: 14,
            STUCK_TIME: 1.5,
            DANGER_FALL_HEIGHT: 4,
            FORWARD_SCORE: 120,
            SIDE_PENALTY: 10,
            HEIGHT_PENALTY: 7,
            VISITED_PENALTY: 30,
            HOLE_PENALTY: 120,
            CEILING_PENALTY: 40,
            INTERFERE_RATE: 0.16
        };

        const cpuBrain = {
            stuckTimer: 0,
            lastX: 0, lastY: 0, lastZ: 0,
            recentPositions: [],
            currentPath: [],
            repathCooldown: 0,
            emergencyEscape: false,
            actionLockTimer: 0,
            bestForwardZ: 0,
            mode: 'normal',
            buildTarget: null,
            fetchTarget: null
        };

        let cpuWorldVersion = 0;
        let cpuPathCache = null;
        let cpuPathVersion = -1;
        let cpuLastSafePosition = { x: 5, y: 1, z: 0 };

        function cpuNotifyWorldChanged() {
            cpuWorldVersion++;
            if (cpuWorldVersion > 9999999) cpuWorldVersion = 0;
        }

        function cpuRememberPosition(x, y, z) {
            cpuBrain.recentPositions.push({ x: x, y: y, z: z });
            if (cpuBrain.recentPositions.length > 24) cpuBrain.recentPositions.shift();
        }

        function cpuVisitedCount(x, y, z) {
            let count = 0;
            for (let p of cpuBrain.recentPositions) {
                if (p.x === x && p.y === y && p.z === z) count++;
            }
            return count;
        }

        function cpuIsPassable(block) {
            if (!block) return true;
            return passTypes.includes(block.userData.type);
        }

        function cpuIsAnimatingBlock(block) {
            if (!block) return false;
            for (let a of animatingBlocks) {
                if (a && a.mesh === block) return true;
            }
            return false;
        }

        function cpuCanStand(x, y, z) {
            if (isNaN(x) || isNaN(y) || isNaN(z)) return false;
            if (y < -20 || y > 60) return false;
            if (!((x >= 3 && x <= 7) || (x >= -7 && x <= -3))) return false;

            let center = getGrid(x, y, z);
            if (center && cpuIsAnimatingBlock(center)) return false;

            let floor = getGrid(x, y - 1, z);
            let body = getGrid(x, y, z);
            let head = getGrid(x, y + 1, z);

            if (cpuIsPassable(floor)) floor = null;
            if (cpuIsPassable(body)) body = null;
            if (cpuIsPassable(head)) head = null;

            if (!floor) return false;
            if (body || head) return false;

            return true;
        }

        function cpuIsDangerFall(fromY, toY) {
            return (fromY - toY) >= CPU_AI.DANGER_FALL_HEIGHT;
        }

        function cpuEvaluatePosition(x, y, z) {
            let score = 0;
            score += (-z) * CPU_AI.FORWARD_SCORE;
            score -= Math.abs(x - cpu.gridX) * CPU_AI.SIDE_PENALTY;
            score -= Math.abs(y - cpu.gridY) * CPU_AI.HEIGHT_PENALTY;
            score -= cpuVisitedCount(x, y, z) * CPU_AI.VISITED_PENALTY;
            if (cpuIsDangerFall(cpu.gridY, y)) score -= CPU_AI.HOLE_PENALTY;
            let ceiling = getGrid(x, y + 2, z);
            if (ceiling && !cpuIsPassable(ceiling)) score -= CPU_AI.CEILING_PENALTY;
            return score;
        }

        function cpuGenerateMoves(node, jumpPower) {
            if (!node) return [];
            const result = [];
            const dirs = [{ x: 0, z: -1 }, { x: -1, z: 0 }, { x: 1, z: 0 }, { x: 0, z: 1 }];
            for (let d of dirs) {
                for (let dy = jumpPower; dy >= -5; dy--) {
                    let tx = node.x + d.x;
                    let ty = node.y + dy;
                    let tz = node.z + d.z;
                    if (!cpuCanStand(tx, ty, tz)) continue;
                    result.push({ x: tx, y: ty, z: tz, score: cpuEvaluatePosition(tx, ty, tz) });
                }
            }
            return result;
        }

        function cpuFindPath() {
            if (cpu.hp <= 0) return null;
            if (cpu.isJumping) return null;

            let jumpPower = cpu.jumpBoostCharges > 0 ? 3 : 1;
            const open = [];
            const closed = new Set();
            open.push({ x: cpu.gridX, y: cpu.gridY, z: cpu.gridZ, path: [], score: 0 });

            let bestNode = null;
            let bestScore = -999999;
            let searchCount = 0;

            while (open.length > 0 && searchCount < CPU_AI.MAX_SEARCH_NODE) {
                searchCount++;
                open.sort(function(a, b) { return b.score - a.score; });
                const current = open.shift();
                const key = current.x + "," + current.y + "," + current.z;
                if (closed.has(key)) continue;
                closed.add(key);

                let currentScore = cpuEvaluatePosition(current.x, current.y, current.z);
                if (currentScore > bestScore) {
                    bestScore = currentScore;
                    bestNode = current;
                }

                if (current.path.length >= CPU_AI.MAX_PATH_DEPTH) continue;

                const nextMoves = cpuGenerateMoves(current, jumpPower);
                for (let n of nextMoves) {
                    const nk = n.x + "," + n.y + "," + n.z;
                    if (closed.has(nk)) continue;

                    const nextPath = current.path.slice();
                    nextPath.push({ x: n.x, y: n.y, z: n.z });
                    
                    if (open.length > CPU_AI.MAX_SEARCH_NODE) break;
                    
                    open.push({
                        x: n.x, y: n.y, z: n.z,
                        path: nextPath,
                        score: current.score + n.score
                    });
                }
            }
            return bestNode;
        }

        function cpuFindPathCached() {
            if (cpuPathCache && cpuPathVersion === cpuWorldVersion) {
                if (cpuPathCache.path && cpuPathCache.path.length > 0) return cpuPathCache;
            }
            let result = cpuFindPath();
            if (!result || !result.path) {
                cpuPathCache = null;
                return null;
            }
            cpuPathCache = result;
            cpuPathVersion = cpuWorldVersion;
            return result;
        }

        function cpuIsBoxed() {
            let blocked = 0;
            const dirs = [[1,0], [-1,0], [0,1], [0,-1]];
            for (let d of dirs) {
                let b = getGrid(cpu.gridX + d[0], cpu.gridY, cpu.gridZ + d[1]);
                if (b && !cpuIsPassable(b)) blocked++;
            }
            return blocked >= 3;
        }

        function cpuRememberSafePosition() {
            let floor = getGrid(cpu.gridX, cpu.gridY - 1, cpu.gridZ);
            if (floor) {
                cpuLastSafePosition.x = cpu.gridX;
                cpuLastSafePosition.y = cpu.gridY;
                cpuLastSafePosition.z = cpu.gridZ;
            }
        }

        // =========================================================
        // CPU 建築・探索用アシスト関数
        // =========================================================

        function getTopY(x, z) {
            return getDropY(x, z, 50) - 1;
        }

        function cpuFindBestBuildTask() {
            let maxJump = cpu.jumpBoostCharges > 0 ? 3 : 1;
            let maxDrop = 4;
            let startX = Math.round(cpu.gridX);
            let startZ = Math.round(cpu.gridZ);
            let startY = getTopY(startX, startZ);
            
            let costs = new Map();
            let queue = [{x: startX, z: startZ, y: startY, cost: 0, path: []}];
            costs.set(`${startX},${startZ}`, 0);
            
            let bestGoalNode = null;
            let minTargetZ = startZ;
            let iterations = 0;
            
            while(queue.length > 0 && iterations < 300) {
                iterations++;
                queue.sort((a,b) => a.cost - b.cost);
                let curr = queue.shift();
                
                if (curr.z < minTargetZ) {
                    minTargetZ = curr.z;
                    bestGoalNode = curr;
                }
                if (curr.z <= startZ - 6) break;
                
                const dirs = [ {dx: 0, dz: -1}, {dx: -1, dz: 0}, {dx: 1, dz: 0}, {dx: 0, dz: 1} ];
                for (let d of dirs) {
                    let nx = curr.x + d.dx;
                    let nz = curr.z + d.dz;
                    if (nx < 3 || nx > 7) continue;
                    
                    let targetTopY = getTopY(nx, nz);
                    let cost = curr.cost;
                    let buildTarget = null;
                    
                    if (targetTopY > curr.y + maxJump) {
                        let neededBlocks = targetTopY - (curr.y + maxJump);
                        cost += neededBlocks * 10;
                        buildTarget = {x: curr.x, z: curr.z}; 
                    } else if (targetTopY < curr.y - maxDrop) {
                        let neededBlocks = (curr.y - maxDrop) - targetTopY;
                        cost += neededBlocks * 10;
                        buildTarget = {x: nx, z: nz}; 
                    } else {
                        cost += 1;
                    }
                    
                    let key = `${nx},${nz}`;
                    if (!costs.has(key) || cost < costs.get(key)) {
                        costs.set(key, cost);
                        let newPath = curr.path.slice();
                        if (buildTarget) newPath.push(buildTarget);
                        queue.push({
                            x: nx, z: nz, 
                            y: buildTarget ? (targetTopY > curr.y + maxJump ? curr.y + 1 : targetTopY + 1) : targetTopY,
                            cost: cost, 
                            path: newPath
                        });
                    }
                }
            }
            if (bestGoalNode && bestGoalNode.path.length > 0) return bestGoalNode.path[0];
            return null;
        }

        function cpuFindClosestBlockToFetch() {
            let bestBlock = null;
            let minDist = 999999;
            for (let z = cpu.gridZ - 1; z <= cpu.gridZ + 15; z++) {
                for (let x = 3; x <= 7; x++) {
                    let topY = getTopY(x, z);
                    let b = getGrid(x, topY, z);
                    if (b && !b.userData.isStatic && !passTypes.includes(b.userData.type)) {
                        if (x === cpu.gridX && z === cpu.gridZ) continue; 
                        let dist = Math.abs(x - cpu.gridX) + Math.abs(z - cpu.gridZ);
                        if (dist < minDist) {
                            minDist = dist;
                            bestBlock = {x: x, y: topY, z: z};
                        }
                    }
                }
            }
            return bestBlock;
        }

        function cpuFindPathTo(goalX, goalZ) {
            let queue = [{x: cpu.gridX, z: cpu.gridZ, path: []}];
            let visited = new Set([`${cpu.gridX},${cpu.gridZ}`]);
            let iterations = 0;
            while(queue.length > 0 && iterations < 200) {
                iterations++;
                let curr = queue.shift();
                if (Math.abs(curr.x - goalX) + Math.abs(curr.z - goalZ) === 1) {
                    if (curr.path.length > 0) return {dx: curr.path[0].x - cpu.gridX, dz: curr.path[0].z - cpu.gridZ};
                    return {dx: 0, dz: 0};
                }
                if (curr.path.length > 8) continue;
                
                const dirs = [{dx: 0, dz: -1}, {dx: -1, dz: 0}, {dx: 1, dz: 0}, {dx: 0, dz: 1}];
                for (let d of dirs) {
                    let nx = curr.x + d.dx;
                    let nz = curr.z + d.dz;
                    if (nx < 3 || nx > 7) continue;
                    let key = `${nx},${nz}`;
                    if (visited.has(key)) continue;
                    visited.add(key);
                    
                    let topY = getTopY(nx, nz);
                    let currTopY = getTopY(curr.x, curr.z);
                    if (Math.abs(topY - currTopY) <= (cpu.jumpBoostCharges > 0 ? 3 : 1)) {
                        let newPath = curr.path.slice();
                        newPath.push({x: nx, z: nz});
                        queue.push({x: nx, z: nz, path: newPath});
                    }
                }
            }
            return null;
        }

        function rollItem(stage) {
            let r = Math.random();
            let base = 0.02 + stage * 0.01;
            if (r < base) return 'bomb';
            if (r < base * 3) return 'heal';
            if (r < base * 5) return 'speed';
            if (r < base * 7) return 'jump';
            return 'normal';
        }

        const BASE_FLOOR_Y = -4; 

        const mapChunks = [
            (z, h, sideX, stage) => {
                for (let dz = 0; dz >= -5; dz--) {
                    for (let x = sideX - 2; x <= sideX + 2; x++) {
                        for(let py = h-1; py >= h-4; py--) createBlock(x, py, z + dz, 1, true, 'normal');
                        if (Math.random() < 0.15) createBlock(x, h, z + dz, 1, false, rollItem(stage));
                    }
                }
                return { outZ: z - 6, outH: h };
            },
            (z, h, sideX, stage) => {
                for (let dz = 0; dz >= -5; dz--) {
                    let isWall = (dz === -2 || dz === -3);
                    for (let x = sideX - 2; x <= sideX + 2; x++) {
                        for(let py = h-1; py >= h-4; py--) createBlock(x, py, z + dz, 1, true, 'normal');
                        if (isWall) {
                            createBlock(x, h, z + dz, 2, false, 'normal');
                        } else if (Math.random() < 0.2) {
                            createBlock(x, h, z + dz, 1, false, rollItem(stage));
                        }
                    }
                }
                return { outZ: z - 6, outH: h };
            },
            (z, h, sideX, stage) => {
                for (let dz = 0; dz >= -5; dz--) {
                    let stepH = dz <= -3 ? h + 2 : h;
                    for (let x = sideX - 2; x <= sideX + 2; x++) {
                        for(let py = stepH-1; py >= stepH-4; py--) createBlock(x, py, z + dz, 1, true, 'normal');
                        if (Math.random() < 0.2) createBlock(x, stepH, z + dz, 1, false, rollItem(stage));
                    }
                }
                return { outZ: z - 6, outH: h + 2 };
            },
            (z, h, sideX, stage) => {
                for (let dz = 0; dz >= -5; dz--) {
                    let stepH = dz <= -3 ? h - 2 : h;
                    for (let x = sideX - 2; x <= sideX + 2; x++) {
                        for(let py = stepH-1; py >= stepH-4; py--) createBlock(x, py, z + dz, 1, true, 'normal');
                        if (Math.random() < 0.2) createBlock(x, stepH, z + dz, 1, false, rollItem(stage));
                    }
                }
                return { outZ: z - 6, outH: h - 2 };
            },
            (z, h, sideX, stage) => {
                for (let dz = 0; dz >= -5; dz--) {
                    for (let x = sideX - 2; x <= sideX + 2; x++) {
                        for(let py = h-1; py >= h-4; py--) createBlock(x, py, z + dz, 1, true, 'normal');
                        if (dz === -3) {
                            createBlock(x, h, z + dz, 2, false, 'normal');
                            createBlock(x, h + 1, z + dz, 2, false, 'normal');
                        } else if (Math.random() < 0.15) {
                            createBlock(x, h, z + dz, 1, false, rollItem(stage));
                        }
                    }
                }
                return { outZ: z - 6, outH: h };
            }
        ];

        function initStage(stage) {
            currentStage = stage;
            MAP_LENGTH = 30 + stage * 10;
            
            blocks.forEach(b => scene.remove(b));
            blocks.length = 0; gridMap.clear(); 
            
            animatingBlocks.forEach(ab => {
                if (ab.dropCursor) scene.remove(ab.dropCursor);
                scene.remove(ab.mesh);
            });
            animatingBlocks.length = 0;
            
            if (goalLine) scene.remove(goalLine);

            player.reset(-5, 1, 0); cpu.reset(5, 1, 0);
            cpuWorldVersion = 0; cpuPathCache = null; cpuPathVersion = -1;
            cpuBrain.mode = 'normal'; cpuBrain.buildTarget = null; cpuBrain.fetchTarget = null;
            
            isPaused = false;
            document.getElementById('pauseOverlay').style.display = 'none';
            document.getElementById('speedIndicator').style.display = 'none';
            document.getElementById('jumpIndicator').style.display = 'none';
            document.getElementById('stageDisplay').innerText = stage;

            const goalMat = new THREE.MeshBasicMaterial({color: 0x00ff00, transparent: true, opacity: 0.5});
            goalLine = new THREE.Mesh(new THREE.BoxGeometry(20, 50, 1), goalMat);
            goalLine.position.set(0, 0, -MAP_LENGTH - 2); scene.add(goalLine);

            let p1Z = 0, p1H = 1, cpuZ = 0, cpuH = 1;
            
            let p1Res = mapChunks[0](p1Z, p1H, -5, stage); p1Z = p1Res.outZ; p1H = p1Res.outH;
            let cpuRes = mapChunks[0](cpuZ, cpuH, 5, stage); cpuZ = cpuRes.outZ; cpuH = cpuRes.outH;

            while (p1Z > -MAP_LENGTH) {
                let chunkId = Math.floor(Math.random() * mapChunks.length);
                p1Res = mapChunks[chunkId](p1Z, p1H, -5, stage);
                p1Z = p1Res.outZ; p1H = p1Res.outH;
                
                cpuRes = mapChunks[chunkId](cpuZ, cpuH, 5, stage);
                cpuZ = cpuRes.outZ; cpuH = cpuRes.outH;
            }

            for (let z = p1Z; z >= -MAP_LENGTH - 10; z--) {
                for (let x = -7; x <= -3; x++) {
                    for(let py = p1H-1; py >= p1H-4; py--) createBlock(x, py, z, 1, true, 'normal');
                }
            }
            for (let z = cpuZ; z >= -MAP_LENGTH - 10; z--) {
                for (let x = 3; x <= 7; x++) {
                    for(let py = cpuH-1; py >= cpuH-4; py--) createBlock(x, py, z, 1, true, 'normal');
                }
            }

            camera.position.set(-5, 15, 12);
            controls.target.set(-5, 1, 0);
            controls.update();

            gameActive = true;
            document.getElementById('overlay').style.display = 'none';
            targetCursor.visible = false; liftCursor.visible = false;
        }

        document.getElementById('overlayBtn').addEventListener('click', () => {
            if (currentStage === 1 && !gameActive) initStage(currentStage);
        });

        function endGame(msg, color, btnText, nextAction) {
            gameActive = false;
            document.getElementById('overlayMsg').innerHTML =
                `<span style="color:${color}">${msg}</span><br><span style="font-size:24px; color:#fff">Stage ${currentStage}</span>`;
            document.getElementById('speedIndicator').style.display = 'none';
            document.getElementById('jumpIndicator').style.display = 'none';
            
            const btn = document.getElementById('overlayBtn');
            const newBtn = btn.cloneNode(true);
            btn.parentNode.replaceChild(newBtn, btn);
            newBtn.innerText = btnText;
            newBtn.addEventListener('click', nextAction);
            
            document.getElementById('overlay').style.display = 'flex';
        }

        function togglePause() {
            isPaused = !isPaused;
            document.getElementById('pauseOverlay').style.display = isPaused ? 'flex' : 'none';
        }

        const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false };
        let aPressed = false, zPressed = false;

        window.addEventListener('keydown', (e) => {
            if (e.code === 'Escape' && gameActive && player.hp > 0 && cpu.hp > 0) togglePause();
            if (!gameActive || isPaused) return;
            
            if (keys.hasOwnProperty(e.code)) keys[e.code] = true;
            if (e.code === 'KeyA' && !aPressed) { aPressed = true; player.actionLiftOrPlace(); }
            if (e.code === 'KeyZ' && !zPressed) { zPressed = true; player.actionInterfere(); } 
        });
        window.addEventListener('keyup', (e) => {
            if (keys.hasOwnProperty(e.code)) keys[e.code] = false;
            if (e.code === 'KeyA') aPressed = false;
            if (e.code === 'KeyZ') zPressed = false;
        });

        const clock = new THREE.Clock();
        let cpuTimer = 0;

        function animate() {
            requestAnimationFrame(animate);
            let dt = clock.getDelta();
            
            if (dt > 0.08) dt = 0.08;
            if (dt < 0.001) dt = 0.001;

            if (isPaused) {
                renderer.render(scene, camera);
                return;
            }
            
            const timeElapsed = clock.getElapsedTime();

            blocks.forEach(b => {
                if (b.userData.type === 'bomb') {
                    b.material.emissive.setHex(Math.sin(timeElapsed * 15) > 0 ? 0xaa0000 : 0x330000);
                } else if (b.userData.type === 'speed') {
                    b.material.emissive.setHex(Math.sin(timeElapsed * 8) > 0 ? 0x884400 : 0x221100);
                } else if (b.userData.type === 'jump') {
                    b.material.emissive.setHex(Math.sin(timeElapsed * 8) > 0 ? 0x0044aa : 0x001144);
                }
            });

            if (gameActive) {
                if (player.hp <= 0) {
                    endGame("YOU DIED...", "#ff5555", "RETRY STAGE", () => initStage(currentStage));
                } else if (cpu.hp <= 0 || player.gridZ <= -MAP_LENGTH) {
                    if (currentStage >= MAX_STAGE) {
                        endGame("GAME CLEAR!!", "#ffeb3b", "BACK TO TITLE", () => {
                            currentStage = 1;
                            document.getElementById('overlayMsg').innerHTML = "BuildRace";
                            const btn = document.getElementById('overlayBtn');
                            const newBtn = btn.cloneNode(true);
                            btn.parentNode.replaceChild(newBtn, btn);
                            newBtn.innerText = "GAME START";
                            newBtn.addEventListener('click', () => {
                                if (currentStage === 1 && !gameActive) initStage(currentStage);
                            });
                        });
                    } else {
                        endGame("STAGE CLEAR!", "#00e5ff", "NEXT STAGE", () => initStage(currentStage + 1));
                    }
                } else if (cpu.gridZ <= -MAP_LENGTH) {
                    endGame("CPU WINS...", "#ff00ff", "RETRY STAGE", () => initStage(currentStage));
                }

                document.getElementById('speedIndicator').style.display =
                    (player.speedBoostTimer > 0 && player.hp > 0) ? 'block' : 'none';

                if (Math.random() < 0.005 + currentStage * 0.001) {
                    let tChar = Math.random() < 0.5 ? player : cpu;
                    if (tChar.hp > 0) {
                        let tx = tChar.gridX + (Math.floor(Math.random() * 5) - 2);
                        let tz = tChar.gridZ - Math.floor(Math.random() * 10); 
                        if ((tx >= -7 && tx <= -3) || (tx >= 3 && tx <= 7)) {
                            let type = 'normal';
                            let b = createBlock(tx, 25, tz, 2, false, type);
                            removeGrid(tx, 25, tz); 
                            
                            let ey = getDropY(tx, tz, 25);
                            let cursor = createDropCursor(tx, ey, tz);

                            animatingBlocks.push({
                                mesh: b, sx: tx, sy: 25, sz: tz, ex: tx, ey: ey, ez: tz, 
                                time: 0, duration: 4.0, arcMax: 0,
                                dropCursor: cursor, owner: null, isFalling: true, isKicked: false
                            });
                        }
                    }
                }
            }

            const pos = stars.geometry.attributes.position.array;
            for (let i = 1; i < pos.length; i += 3) { pos[i] -= 15 * dt; if (pos[i] < -10) pos[i] = 40; }
            stars.geometry.attributes.position.needsUpdate = true;

            if (gameActive) {
                if (keys.ArrowUp)    player.tryMove(0, -1);
                else if (keys.ArrowDown)  player.tryMove(0,  1);
                else if (keys.ArrowLeft)  player.tryMove(-1, 0);
                else if (keys.ArrowRight) player.tryMove(1,  0);

                if (cpu.hp > 0) {
                    cpuTimer += dt;
                    
                    let cpuDifficultyScale = Math.min(1.8, 1.0 + currentStage * 0.06);
                    if (player.gridZ < cpu.gridZ - 12) {
                        cpuDifficultyScale += 0.25;
                    }

                    let cpuThinkInterval = Math.max(
                        0.05,
                        (CPU_AI.THINK_INTERVAL / cpuDifficultyScale) - currentStage * 0.01 - (cpu.speedBoostTimer > 0 ? 0.04 : 0)
                    );

                    if (cpuTimer >= cpuThinkInterval) {
                        cpuTimer = 0;

                        cpuRememberPosition(cpu.gridX, cpu.gridY, cpu.gridZ);

                        if (cpu.gridX === cpuBrain.lastX && cpu.gridY === cpuBrain.lastY && cpu.gridZ === cpuBrain.lastZ) {
                            cpuBrain.stuckTimer += cpuThinkInterval;
                            
                            if (cpuBrain.stuckTimer > 5.0) {
                                cpuPathCache = null;
                                cpuBrain.actionLockTimer = 0;
                                cpuBrain.mode = 'normal';
                                cpu.tryMove(Math.random() < 0.5 ? -1 : 1, 0);
                            }
                        } else {
                            cpuBrain.stuckTimer = 0;
                            cpuBrain.lastX = cpu.gridX;
                            cpuBrain.lastY = cpu.gridY;
                            cpuBrain.lastZ = cpu.gridZ;
                        }

                        if (cpuBrain.actionLockTimer > 0) {
                            cpuBrain.actionLockTimer -= cpuThinkInterval;
                            if (cpuBrain.actionLockTimer < 0) cpuBrain.actionLockTimer = 0;
                        }

                        let needEmergencyEscape = false;
                        if (cpuBrain.stuckTimer >= CPU_AI.STUCK_TIME) needEmergencyEscape = true;
                        if (cpuIsBoxed()) needEmergencyEscape = true;

                        if (needEmergencyEscape) {
                            cpuBrain.emergencyEscape = true;
                            cpuBrain.mode = 'normal';
                            
                            let escapeDir = Math.random() < 0.5 ? -1 : 1;
                            if (cpu.tryMove(escapeDir, 0)) {
                                cpuBrain.actionLockTimer = 0.25;
                            } else {
                                cpu.tryMove(0, -1);
                                cpuBrain.actionLockTimer = 0.25;
                            }

                            if (cpuBrain.stuckTimer > CPU_AI.STUCK_TIME * 2) {
                                let rdx = cpuLastSafePosition.x - cpu.gridX;
                                let rdz = cpuLastSafePosition.z - cpu.gridZ;
                                if (Math.abs(rdx) > 0) rdx = Math.sign(rdx);
                                if (Math.abs(rdz) > 0) rdz = Math.sign(rdz);
                                cpu.tryMove(rdx, rdz);
                            }
                        } else {
                            cpuBrain.emergencyEscape = false;

                            if (cpuBrain.actionLockTimer <= 0) {
                                
                                if (cpuBrain.mode === 'build') {
                                    if (!cpu.heldBlock) {
                                        cpuBrain.mode = 'normal';
                                    } else if (!cpuBrain.buildTarget) {
                                        cpuBrain.mode = 'normal';
                                    } else {
                                        let tx = cpuBrain.buildTarget.x;
                                        let tz = cpuBrain.buildTarget.z;
                                        let dist = Math.abs(tx - cpu.gridX) + Math.abs(tz - cpu.gridZ);
                                        
                                        if (dist === 1) {
                                            cpu.dirX = tx - cpu.gridX;
                                            cpu.dirZ = tz - cpu.gridZ;
                                            cpu.actionLiftOrPlace();
                                            cpuBrain.actionLockTimer = 0.3;
                                            cpuBrain.mode = 'normal'; 
                                            cpuNotifyWorldChanged();
                                        } else if (dist === 0) {
                                            cpu.tryMove(Math.random() < 0.5 ? 1 : -1, 0); 
                                            cpuBrain.actionLockTimer = 0.2;
                                        } else {
                                            let move = cpuFindPathTo(tx, tz);
                                            if (move) {
                                                cpu.tryMove(move.dx, move.dz);
                                                cpuBrain.actionLockTimer = 0.15;
                                            } else {
                                                cpuBrain.mode = 'normal';
                                            }
                                        }
                                    }
                                } 
                                
                                else if (cpuBrain.mode === 'fetch') {
                                    if (cpu.heldBlock) {
                                        cpuBrain.mode = 'build';
                                    } else {
                                        let target = cpuFindClosestBlockToFetch();
                                        if (target) {
                                            let dist = Math.abs(target.x - cpu.gridX) + Math.abs(target.z - cpu.gridZ);
                                            if (dist === 1) {
                                                cpu.dirX = target.x - cpu.gridX;
                                                cpu.dirZ = target.z - cpu.gridZ;
                                                let tgt = getSmartLiftTarget(cpu.gridX, cpu.gridY, cpu.gridZ, cpu.dirX, cpu.dirZ);
                                                if (tgt) {
                                                    cpu.actionLiftOrPlace();
                                                    cpuBrain.actionLockTimer = 0.3;
                                                    cpuBrain.mode = 'build';
                                                } else {
                                                    cpu.tryMove(Math.random() < 0.5 ? 1 : -1, 0);
                                                    cpuBrain.actionLockTimer = 0.2;
                                                }
                                            } else if (dist === 0) {
                                                cpu.tryMove(0, 1); 
                                                cpuBrain.actionLockTimer = 0.2;
                                            } else {
                                                let move = cpuFindPathTo(target.x, target.z);
                                                if (move) {
                                                    cpu.tryMove(move.dx, move.dz);
                                                    cpuBrain.actionLockTimer = 0.15;
                                                } else {
                                                    cpuBrain.mode = 'normal';
                                                }
                                            }
                                        } else {
                                            cpuBrain.mode = 'normal'; 
                                        }
                                    }
                                } 
                                
                                else {
                                    let pathResult = cpuFindPathCached();
                                    let forwardProgress = false;
                                    
                                    if (pathResult && pathResult.path && pathResult.path.length > 0) {
                                        for(let p of pathResult.path) {
                                            if (p.z < cpu.gridZ) {
                                                forwardProgress = true; break;
                                            }
                                        }
                                    }
                                    
                                    if (forwardProgress) {
                                        let nextNode = pathResult.path[0];
                                        let dx = nextNode.x - cpu.gridX;
                                        let dz = nextNode.z - cpu.gridZ;
                                        
                                        if (dx > 1) dx = 1; if (dx < -1) dx = -1;
                                        if (dz > 1) dz = 1; if (dz < -1) dz = -1;
                                        
                                        if (cpu.tryMove(dx, dz)) {
                                            cpuBrain.actionLockTimer = 0.15;
                                            cpuRememberSafePosition();
                                        } else {
                                            if (!cpu.heldBlock) {
                                                let tgt = getSmartLiftTarget(cpu.gridX, cpu.gridY, cpu.gridZ, dx, dz);
                                                if (tgt) {
                                                    cpu.dirX = dx; cpu.dirZ = dz;
                                                    cpu.actionLiftOrPlace();
                                                    cpuBrain.actionLockTimer = 0.25;
                                                } else {
                                                    cpu.tryMove(Math.random() < 0.5 ? 1 : -1, 0);
                                                    cpuBrain.actionLockTimer = 0.2;
                                                }
                                            } else {
                                                cpu.dirX = 0; cpu.dirZ = -1;
                                                cpu.actionLiftOrPlace();
                                                cpuBrain.actionLockTimer = 0.3;
                                            }
                                        }
                                    } else {
                                        let task = cpuFindBestBuildTask();
                                        if (task) {
                                            cpuBrain.buildTarget = task;
                                            cpuBrain.mode = cpu.heldBlock ? 'build' : 'fetch';
                                        } else {
                                            cpu.tryMove(Math.random() < 0.5 ? 1 : -1, 0);
                                            cpuBrain.actionLockTimer = 0.2;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            player.update(dt, timeElapsed);
            cpu.update(dt, timeElapsed);

            if (gameActive && player.hp > 0) {
                let targetPos = new THREE.Vector3(player.mesh.position.x, player.mesh.position.y, player.mesh.position.z);
                controls.target.lerp(targetPos, 0.1);
                
                let swayX = Math.sin(timeElapsed * 0.5) * 1.5;
                let swayZ = Math.cos(timeElapsed * 0.4) * 0.5;
                
                camera.position.copy(controls.target).add(baseCamOffset).add(new THREE.Vector3(swayX, 0, swayZ));
                controls.update(); 
            }

            for (let i = animatingBlocks.length - 1; i >= 0; i--) {
                if (!animatingBlocks[i] || !animatingBlocks[i].mesh) continue;
                let ab = animatingBlocks[i];
                ab.time += dt;
                
                if (!ab.isFalling) {
                    let t = Math.min(ab.time / ab.duration, 1);
                    let cx = ab.sx + (ab.ex - ab.sx) * t;
                    let cz = ab.sz + (ab.ez - ab.sz) * t;
                    let cy = ab.sy + (ab.ey - ab.sy) * t + Math.sin(t * Math.PI) * ab.arcMax;
                    ab.mesh.position.set(cx, cy, cz);

                    let hitTarget = null;
                    for (let char of [player, cpu]) {
                        if (char.hp > 0 && Math.abs(cx - char.mesh.position.x) < 0.8 &&
                            Math.abs(cz - char.mesh.position.z) < 0.8 &&
                            Math.abs(cy - char.mesh.position.y) < 1.5) {
                            hitTarget = char; break;
                        }
                    }

                    if (hitTarget && hitTarget !== ab.owner && !passTypes.includes(ab.mesh.userData.type)) {
                        let dmg = ab.mesh.userData.type === 'bomb' ? 2 : 1;
                        hitTarget.takeDamage(dmg);
                        if (ab.mesh.userData.type === 'bomb') explodeBomb(Math.round(cx), Math.round(cy), Math.round(cz));
                        if (ab.dropCursor) scene.remove(ab.dropCursor);
                        scene.remove(ab.mesh); animatingBlocks.splice(i, 1);
                        continue;
                    }

                    if (!ab.isKicked) {
                        let gX = Math.round(cx), gY = Math.round(cy), gZ = Math.round(cz);
                        let hitBlock = getGrid(gX, gY, gZ);
                        if (t > 0.15 && hitBlock && !passTypes.includes(hitBlock.userData.type)) {
                            playSound('hit'); 
                            ab.isFalling = true; ab.ex = gX; ab.ez = gZ; 
                            ab.fallStartY = cy; ab.ey = getDropY(gX, gZ, gY);
                            if (ab.ey < -10) playSound('fall'); 
                            ab.time = 0; ab.duration = Math.max(0.2, (ab.fallStartY - ab.ey) * 0.05); 
                            if (ab.dropCursor) { scene.remove(ab.dropCursor); ab.dropCursor = null; }
                            continue;
                        }
                    }

                    if (t >= 1) {
                        if (ab.isKicked && ab.ey > ab.finalY) {
                            ab.isKicked = false; ab.isFalling = true;
                            ab.sx = ab.ex; ab.sy = ab.ey; ab.sz = ab.ez; ab.ey = ab.finalY;
                            ab.fallStartY = ab.sy; ab.time = 0;
                            ab.duration = Math.max(0.2, (ab.fallStartY - ab.ey) * 0.05);
                            if (ab.ey < -10) playSound('fall');
                        } else {
                            processLanding(ab, i);
                        }
                    }

                } else {
                    let t  = Math.min(ab.time / ab.duration, 1);
                    let cy = ab.fallStartY ? ab.fallStartY + (ab.ey - ab.fallStartY) * t : ab.sy + (ab.ey - ab.sy) * t;
                    let cx = ab.ex, cz = ab.ez;
                    ab.mesh.position.set(cx, cy, cz);

                    let hitTarget = null;
                    for (let char of [player, cpu]) {
                        if (char.hp > 0 && Math.abs(cx - char.mesh.position.x) < 0.8 &&
                            Math.abs(cz - char.mesh.position.z) < 0.8 && Math.abs(cy - char.mesh.position.y) < 1.5) {
                            hitTarget = char; break;
                        }
                    }

                    if (hitTarget && hitTarget !== ab.owner && !passTypes.includes(ab.mesh.userData.type)) {
                        let dmg = ab.mesh.userData.type === 'bomb' ? 2 : 1;
                        if (ab.owner === null) { dmg = 2; hitTarget.knockback(); }
                        hitTarget.takeDamage(dmg);
                        if (ab.mesh.userData.type === 'bomb') explodeBomb(Math.round(cx), Math.round(cy), Math.round(cz));
                        if (ab.dropCursor) scene.remove(ab.dropCursor);
                        scene.remove(ab.mesh); animatingBlocks.splice(i, 1);
                        continue;
                    }
                    if (t >= 1) processLanding(ab, i);
                }
            }
            
            if (isNaN(cpu.mesh.position.x)) cpu.mesh.position.x = 5;
            if (isNaN(cpu.mesh.position.y)) cpu.mesh.position.y = 5;
            if (isNaN(cpu.mesh.position.z)) cpu.mesh.position.z = 0;
            if (cpu.mesh.position.y < -50) {
                cpu.mesh.position.x = cpuLastSafePosition.x;
                cpu.mesh.position.y = cpuLastSafePosition.y + 2;
                cpu.mesh.position.z = cpuLastSafePosition.z;
            }

            renderer.render(scene, camera);
        }

        function processLanding(ab, index) {
            if (ab.ey < -10) {
                scene.remove(ab.mesh);
            } else {
                if (ab.mesh.userData.type === 'bomb') {
                    explodeBomb(Math.round(ab.ex), Math.round(ab.ey), Math.round(ab.ez));
                    scene.remove(ab.mesh); 
                } else {
                    let rx = Math.round(ab.ex), ry = Math.round(ab.ey), rz = Math.round(ab.ez);
                    ab.mesh.position.set(rx, ry, rz); setGrid(rx, ry, rz, ab.mesh);
                    
                    for (let char of [player, cpu]) {
                        if (char.hp > 0 && Math.round(char.gridX) === rx && Math.round(char.gridZ) === rz) {
                            if (char.gridY <= ry) {
                                char.gridY = ry + 1;
                                char.mesh.position.y = char.gridY;
                            }
                        }
                    }
                }
            }
            if (ab.dropCursor) scene.remove(ab.dropCursor);
            animatingBlocks.splice(index, 1);
        }

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

        animate();
    </script>
</body>
</html>

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
建設系レースゲーム「BuildRace」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word 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