建設系レースゲーム「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公開。
・2026/5/19 バージョン1.5公開。(マルチモード追加)
・2026/5/22 バージョン1.6公開。(ストーリーモード追加)

画像
シングルモード( 人間 vs CPU )


画像
マルチモード( 人間 vs 人間 )


画像
ストーリーモード

【操作説明】

プレイヤー1
 ・方向キーで移動。
 ・Aキーを押すと、手前のブロックを持ち上げ、
  再度Aキーを押すと、手前にブロックを置く。
 ・Zキーを押すと、相手にブロックを投げる。

プレイヤー2
 ・テンキーで移動。
 ・Hキーを押すと、手前のブロックを持ち上げ、
  再度Hキーを押すと、手前にブロックを置く。
 ・Nキーを押すと、相手にブロックを投げる。

【基本ルール】

・先にゴールしたほうが勝ち。
・または、先に相手のHPをゼロにしたほうが勝ち。

◆アイテム

 ・オレンジ色 ⋯ ジャンプ力が一時的に一段アップ。
 ・水色    ⋯ スピードが一時的にアップ。
 ・黄緑色   ⋯ HPが少し回復。

◆ブロック

 ・白色   ⋯ 軽い
 ・薄い灰色 ⋯ 少し重い
 ・濃い灰色 ⋯ 重い
 ・赤色   ⋯ 着弾地点が隆起する。

【スートリーモード】

・ブロックを複数持ち上げて前方に倒し、
 足場を作って浮島から浮島へと渡っていき、
 ゴール地点を目指すモードです。

・Aキーを押すと、手前のブロックを一つ持ち上げます。
・Zキーを押すと、持っているブロックを手前に倒します。


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

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; }
        
        #splitLine {
            position: absolute; top: 0; left: 50%; width: 4px; height: 100%;
            background: rgba(255, 255, 255, 0.3); transform: translateX(-50%); z-index: 5;
            box-shadow: 0 0 10px #000;
        }

        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.6); padding: 10px 30px; border-radius: 10px;
        }
        #scoreBox { font-size: 20px; font-weight: bold; margin-bottom: 5px; }
        #stageBox { font-size: 24px; 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: 30px; 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; width: 300px; text-align: center;
        }
        .overlayBtn:hover { background: #fff59d; }

        #menuButtons { display: flex; flex-direction: column; gap: 15px; }

        #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">
        <div id="splitLine"></div>
        <h1>BuildRace</h1>
        <div id="topCenter">
            <div id="scoreBox">
                <span style="color:#00e5ff">P1</span> [<span id="p1Wins">0</span>] - [<span id="p2Wins">0</span>] <span style="color:#ff00ff" id="p2ScoreLabel">CPU</span>
            </div>
            <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"><span id="cpuNameLabel">CPU</span> 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">
            <span style="color:#00e5ff; font-weight:bold;">[1P]</span> 矢印:移動 / A:道 / Z:投げる<br>
            <span style="color:#ff00ff; font-weight:bold;">[2P]</span> テンキー8,4,2,6:移動 / H:道 / N:投げる<br>
            [Esc] PAUSE<br>
            ※山積みのブロックは一番下から引っこ抜き可能!<br>
            <span style="color:#ff5555; font-weight:bold;">■爆弾(赤)</span>: 周囲隆起 / <span style="color:#55ff55; font-weight:bold;">■回復(緑)</span>: HP+1<br>
            <span style="color:#ff9900; font-weight:bold;">■速(橙)</span>: SPD+4秒 / <span style="color:#00aaff; font-weight:bold;">■跳(青)</span>: JUMP+2(3回)<br>
            <span class="highlight">相手より先にゴールへ!落ちてくる物も資材に!</span>
        </div>
    </div>
    
    <div id="overlay">
        <div id="overlayMsg">BuildRace</div>
        <div id="menuButtons">
            <button id="btnSingle" class="overlayBtn">SINGLE MODE (VS CPU)</button>
            <button id="btnMulti" class="overlayBtn">MULTI MODE (1P VS 2P)</button>
        </div>
        <button id="overlayBtn" class="overlayBtn" style="display:none;">NEXT</button>
    </div>

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

    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    
    <script>
        let audioCtx = null;
        try {
            audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        } catch (e) {
            console.warn("AudioContext init failed:", e);
        }

        function playSound(type, pitch = 1) {
            if (!audioCtx) return;
            try {
                if(audioCtx.state === 'suspended') {
                    const resumePromise = audioCtx.resume();
                    if (resumePromise !== undefined) {
                        resumePromise.catch(e => console.warn("Audio resume error:", e));
                    }
                }
                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);
                }
            } catch (e) {
                console.warn("PlaySound execution error:", e);
            }
        }

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

        const cameraP1 = new THREE.PerspectiveCamera(60, (window.innerWidth / 2) / window.innerHeight, 0.1, 1000);
        const cameraP2 = new THREE.PerspectiveCamera(60, (window.innerWidth / 2) / window.innerHeight, 0.1, 1000);
        let camTargetP1 = new THREE.Vector3(-5, 1, 0);
        let camTargetP2 = new THREE.Vector3(5, 1, 0);

        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; 
        
        let isMultiplayer = false;
        let scoreP1 = 0;
        let scoreP2 = 0;

        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 removeBlock(mesh) {
            if (!mesh) return;
            if (mesh.parent) mesh.parent.remove(mesh);
            else scene.remove(mesh);
            const index = blocks.indexOf(mesh);
            if (index > -1) blocks.splice(index, 1);
        }

        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 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 > 4) 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;
                
                const cursorGeo = new THREE.PlaneGeometry(0.95, 0.95);
                const tgtMat = new THREE.MeshBasicMaterial({color: 0xff3333, transparent: true, opacity: 0.7, side: THREE.DoubleSide});
                this.targetCursor = new THREE.Mesh(cursorGeo, tgtMat);
                this.targetCursor.rotation.x = -Math.PI / 2; this.targetCursor.visible = false; scene.add(this.targetCursor);

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

                this.reset(x, y, z);
            }

            reset(x, y, z) {
                this.hp = 10; 
                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) { 
                    removeBlock(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.targetCursor.visible = false;
                this.liftCursor.visible = false;
                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 {
                    const cpuHPElem = document.getElementById('cpuHP');
                    if (cpuHPElem) cpuHPElem.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 || (isMultiplayer && this === cpu)) && gameActive) {
                    this.targetCursor.visible = false;
                    this.liftCursor.visible = false;

                    if (this.heldBlock) {
                        let tgt = this.getSmartPlaceTarget();
                        if (tgt) {
                            this.targetCursor.position.set(tgt.x, Math.max(tgt.y, -5) + 0.51, tgt.z);
                            this.targetCursor.visible = true;
                            if (tgt.isSmart) {
                                this.targetCursor.material.opacity = 0.3 + 0.7 * Math.abs(Math.sin(timeElapsed * 15));
                                this.targetCursor.material.color.setHex(0x00e5ff);
                            } else {
                                this.targetCursor.material.opacity = 0.7;
                                this.targetCursor.material.color.setHex(0xff3333);
                            }
                        }
                    } else if (!this.isJumping) {
                        let tgt = getSmartLiftTarget(this.gridX, this.gridY, this.gridZ, this.dirX, this.dirZ);
                        if (tgt) {
                            this.liftCursor.position.set(tgt.x, Math.max(tgt.y, -5) + 0.51, tgt.z);
                            this.liftCursor.visible = true;
                        }
                    }
                } else {
                    this.targetCursor.visible = false;
                    this.liftCursor.visible = false;
                }
            }

            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); removeBlock(curB);
                    } else if (curB.userData.type === 'speed') {
                        this.speedBoostTimer = 4.0;
                        playSound('speed');
                        removeGrid(this.gridX, this.gridY, this.gridZ); removeBlock(curB);
                    } else if (curB.userData.type === 'jump') {
                        this.jumpBoostCharges = 3;
                        playSound('speed', 1.5);
                        this.updateHPUI();
                        removeGrid(this.gridX, this.gridY, this.gridZ); removeBlock(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;

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

                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;

                    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;

                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 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;
                
                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,
            stairQueue: [],
            consecutiveBuildCount: 0
        };

        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 cpuAStarHeuristic(z) {
            return (-z) * 2.0;
        }

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

            let jumpPower = cpu.jumpBoostCharges > 0 ? 3 : 1;

            const openList = [];
            const gScore  = new Map();
            const closed  = new Set();

            const startKey = `${cpu.gridX},${cpu.gridY},${cpu.gridZ}`;
            const startH   = cpuAStarHeuristic(cpu.gridZ);
            openList.push({
                x: cpu.gridX, y: cpu.gridY, z: cpu.gridZ,
                path: [],
                g: 0,
                f: startH
            });
            gScore.set(startKey, 0);

            let bestNode  = null;
            let bestZ     = cpu.gridZ;
            let searchCount = 0;

            while (openList.length > 0 && searchCount < CPU_AI.MAX_SEARCH_NODE) {
                searchCount++;

                let lowestIdx = 0;
                for (let i = 1; i < openList.length; i++) {
                    if (openList[i].f < openList[lowestIdx].f) lowestIdx = i;
                }
                const current = openList.splice(lowestIdx, 1)[0];
                const key = `${current.x},${current.y},${current.z}`;

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

                if (current.z < bestZ || (current.z === bestZ && current.path.length > 0)) {
                    bestZ = current.z;
                    bestNode = current;
                }

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

                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 = current.x + d.x;
                        let ty = current.y + dy;
                        let tz = current.z + d.z;

                        if (!cpuCanStand(tx, ty, tz)) continue;

                        const nk = `${tx},${ty},${tz}`;
                        if (closed.has(nk)) continue;

                        let moveCost = 1.0;
                        if (dy < 0) moveCost += Math.abs(dy) * 0.3;
                        if (dy > 1) moveCost += dy * 0.5;
                        moveCost += cpuVisitedCount(tx, ty, tz) * 1.5;
                        if (cpuIsDangerFall(current.y, ty)) moveCost += 5.0;
                        let ceiling = getGrid(tx, ty + 2, tz);
                        if (ceiling && !cpuIsPassable(ceiling)) moveCost += 1.5;

                        const newG = current.g + moveCost;

                        if (gScore.has(nk) && gScore.get(nk) <= newG) continue;
                        gScore.set(nk, newG);

                        const h = cpuAStarHeuristic(tz);
                        const newPath = current.path.slice();
                        newPath.push({ x: tx, y: ty, z: tz });

                        openList.push({
                            x: tx, y: ty, z: tz,
                            path: newPath,
                            g: newG,
                            f: newG + h
                        });
                    }
                }
            }
            return bestNode;
        }

        function cpuFindPathCached() {
            if (cpuPathCache && cpuPathVersion === cpuWorldVersion) {
                while (cpuPathCache.path && cpuPathCache.path.length > 0) {
                    let p = cpuPathCache.path[0];
                    if (p.x === Math.round(cpu.gridX) && p.z === Math.round(cpu.gridZ)) {
                        cpuPathCache.path.shift();
                    } else {
                        break;
                    }
                }
                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;
            }
        }

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

        function cpuDetectObstacleAhead() {
            let maxJump = cpu.jumpBoostCharges > 0 ? 3 : 1;
            let cx = Math.round(cpu.gridX);
            let cz = Math.round(cpu.gridZ);
            let prevTopY = getTopY(cx, cz);

            for (let dz = 1; dz <= 6; dz++) {
                let nz = cz - dz;
                let nextTopY = getTopY(cx, nz);

                if (nextTopY < -14) break;

                let heightDiff = nextTopY - prevTopY;
                if (heightDiff > maxJump) {
                    let stepsNeeded = Math.min(heightDiff - maxJump, 5);
                    return {
                        type: 'wall',
                        approachX: cx,
                        approachZ: cz - (dz - 1),
                        currentTopY: prevTopY,
                        obstacleTopY: nextTopY,
                        stepsNeeded: stepsNeeded
                    };
                }
                if (heightDiff < -5) {
                    let stepsNeeded = Math.min(Math.abs(heightDiff) - 5, 4);
                    return {
                        type: 'pit',
                        approachX: cx,
                        approachZ: nz,
                        currentTopY: prevTopY,
                        obstacleTopY: nextTopY,
                        stepsNeeded: stepsNeeded
                    };
                }
                prevTopY = nextTopY;
            }
            return null;
        }

        function cpuPlanStairQueue(obstacle) {
            if (!obstacle || obstacle.stepsNeeded <= 0) return [];
            let queue = [];

            if (obstacle.type === 'wall') {
                for (let i = 0; i < obstacle.stepsNeeded; i++) {
                    queue.push({
                        type: 'stack',
                        placeX: obstacle.approachX,
                        placeZ: obstacle.approachZ
                    });
                }
            } else if (obstacle.type === 'pit') {
                let adjacentZ = obstacle.approachZ + 1;
                for (let i = 0; i < obstacle.stepsNeeded; i++) {
                    queue.push({
                        type: 'fill',
                        placeX: obstacle.approachX,
                        placeZ: obstacle.approachZ,
                        standZ: adjacentZ
                    });
                }
            }
            return queue;
        }

        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 - 8) 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: curr.x, z: curr.z};
                    } 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;
            let cx = Math.round(cpu.gridX);
            let cz = Math.round(cpu.gridZ);

            for (let z = cz - 4; z <= cz + 20; z++) {
                for (let x = 3; x <= 7; x++) {
                    for (let yOff = 5; yOff >= 0; yOff--) {
                        let topY = getTopY(x, z);
                        let checkY = topY - yOff;
                        if (checkY < -5) continue;
                        let b = getGrid(x, checkY, z);
                        if (!b || b.userData.isStatic || passTypes.includes(b.userData.type)) continue;
                        if (x === cx && z === cz) continue;
                        if (cpuIsAnimatingBlock(b)) continue;

                        let dist = Math.abs(x - cx) + Math.abs(z - cz);
                        if (dist < minDist) {
                            minDist = dist;
                            bestBlock = {x: x, y: checkY, z: z};
                        }
                        break;
                    }
                }
            }
            return bestBlock;
        }

        function cpuFindPathTo(goalX, goalZ) {
            let startX = Math.round(cpu.gridX);
            let startZ = Math.round(cpu.gridZ);
            let jumpPower = cpu.jumpBoostCharges > 0 ? 3 : 1;

            let queue = [{x: startX, z: startZ, y: Math.round(cpu.gridY), path: []}];
            let visited = new Set([`${startX},${startZ}`]);
            let iterations = 0;

            while (queue.length > 0 && iterations < 300) {
                iterations++;
                let curr = queue.shift();

                if (curr.x === goalX && curr.z === goalZ) {
                    if (curr.path.length > 0) return {dx: curr.path[0].dx, dz: curr.path[0].dz};
                    return {dx: 0, dz: 0};
                }
                if (curr.path.length >= 12) 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 currTopY = getTopY(curr.x, curr.z);
                    let nextTopY = getTopY(nx, nz);
                    let diff = nextTopY - currTopY;
                    if (diff > jumpPower || diff < -6) continue;

                    let newPath = curr.path.slice();
                    newPath.push({dx: d.dx, dz: d.dz});
                    queue.push({x: nx, z: nz, y: nextTopY + 1, 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;
            
            player.reset(-5, 1, 0); cpu.reset(5, 1, 0);

            const blocksToRemove = [...blocks];
            blocksToRemove.forEach(b => removeBlock(b));
            blocks.length = 0; gridMap.clear(); 
            
            const animBlocksToRemove = [...animatingBlocks];
            animBlocksToRemove.forEach(ab => {
                if (ab.dropCursor) scene.remove(ab.dropCursor);
                removeBlock(ab.mesh);
            });
            animatingBlocks.length = 0;
            
            if (goalLine) scene.remove(goalLine);

            cpuWorldVersion = 0; cpuPathCache = null; cpuPathVersion = -1;
            cpuBrain.mode = 'normal'; cpuBrain.buildTarget = null; cpuBrain.fetchTarget = null;
            cpuBrain.stairQueue = [];
            cpuBrain.consecutiveBuildCount = 0;
            cpuBrain.stuckTimer = 0;
            cpuBrain.actionLockTimer = 0;
            cpuBrain.emergencyEscape = false;
            cpuBrain.recentPositions = [];
            cpuTimer = 0; 
            cpuLastSafePosition = { x: 5, y: 1, z: 0 }; 
            
            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;

            updateScoreUI();

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

            camTargetP1.set(-5, 1, 0);
            camTargetP2.set(5, 1, 0);

            gameActive = true;
            document.getElementById('overlay').style.display = 'none';
        }

        function updateScoreUI() {
            document.getElementById('p1Wins').innerText = scoreP1;
            document.getElementById('p2Wins').innerText = scoreP2;
            document.getElementById('p2ScoreLabel').innerText = isMultiplayer ? "P2" : "CPU";
            
            const cpuNameLabel = document.getElementById('cpuNameLabel');
            if(cpuNameLabel) cpuNameLabel.innerText = isMultiplayer ? "P2" : "CPU";
        }

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

        document.getElementById('btnMulti').addEventListener('click', () => {
            isMultiplayer = true;
            updateScoreUI();
            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';
            
            document.getElementById('menuButtons').style.display = 'none';
            const btn = document.getElementById('overlayBtn');
            btn.style.display = 'block';

            const newBtn = btn.cloneNode(true);
            btn.parentNode.replaceChild(newBtn, btn);
            newBtn.innerText = btnText;
            newBtn.addEventListener('click', nextAction);
            
            document.getElementById('overlay').style.display = 'flex';
        }

        function backToTitle() {
            currentStage = 1;
            scoreP1 = 0; scoreP2 = 0;
            updateScoreUI();
            document.getElementById('overlayMsg').innerHTML = "BuildRace";
            document.getElementById('overlayBtn').style.display = 'none';
            document.getElementById('menuButtons').style.display = 'flex';
        }

        function processEndStage(winner) {
            gameActive = false;
            if (winner === 1) scoreP1++;
            else if (winner === 2) scoreP2++;
            
            updateScoreUI();
            
            if (currentStage >= MAX_STAGE) {
                let msg = ""; let color = "#ffeb3b";
                if (scoreP1 > scoreP2) {
                    msg = isMultiplayer ? "P1 WINS THE MATCH!" : "YOU WIN THE MATCH!";
                    color = "#00e5ff";
                } else if (scoreP2 > scoreP1) {
                    msg = isMultiplayer ? "P2 WINS THE MATCH!" : "CPU WINS THE MATCH!";
                    color = "#ff00ff";
                } else {
                    msg = "MATCH DRAW!";
                }
                endGame(`${msg}<br><span style="font-size:32px">FINAL SCORE: P1 [${scoreP1}] - ${isMultiplayer?"P2":"CPU"} [${scoreP2}]</span>`, color, "BACK TO TITLE", backToTitle);
            } else {
                let msg = "DRAW!"; let color = "#ffeb3b";
                if (winner === 1) { msg = "P1 WINS STAGE!"; color = "#00e5ff"; }
                else if (winner === 2) { msg = isMultiplayer ? "P2 WINS STAGE!" : "CPU WINS STAGE!"; color = "#ff00ff"; }
                
                endGame(msg, color, "NEXT STAGE", () => initStage(currentStage + 1));
            }
        }

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

        const keys = { 
            ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false,
            Numpad8: false, Numpad2: false, Numpad4: false, Numpad6: false
        };
        let aPressed = false, zPressed = false;
        let hPressed = false, nPressed = false;

        window.addEventListener('keydown', (e) => {
            if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Numpad8', 'Numpad2', 'Numpad4', 'Numpad6'].includes(e.code)) {
                e.preventDefault(); 
            }
            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(); } 
            
            if (e.code === 'KeyH' && !hPressed) { hPressed = true; if(isMultiplayer) cpu.actionLiftOrPlace(); }
            if (e.code === 'KeyN' && !nPressed) { nPressed = true; if(isMultiplayer) cpu.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;
            if (e.code === 'KeyH') hPressed = false;
            if (e.code === 'KeyN') nPressed = false;
        });

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

        function renderSplitScreen() {
            renderer.setScissorTest(true);
            const width = window.innerWidth;
            const height = window.innerHeight;

            renderer.setViewport(0, 0, width / 2, height);
            renderer.setScissor(0, 0, width / 2, height);
            cameraP1.aspect = (width / 2) / height;
            cameraP1.updateProjectionMatrix();
            renderer.render(scene, cameraP1);

            renderer.setViewport(width / 2, 0, width / 2, height);
            renderer.setScissor(width / 2, 0, width / 2, height);
            cameraP2.aspect = (width / 2) / height;
            cameraP2.updateProjectionMatrix();
            renderer.render(scene, cameraP2);

            renderer.setScissorTest(false);
        }

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

            if (isPaused) {
                renderSplitScreen();
                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 && cpu.hp <= 0) {
                    processEndStage(0);
                } else if (player.hp <= 0) {
                    processEndStage(2);
                } else if (cpu.hp <= 0) {
                    processEndStage(1);
                } else if (player.gridZ <= -MAP_LENGTH && cpu.gridZ <= -MAP_LENGTH) {
                    processEndStage(0);
                } else if (player.gridZ <= -MAP_LENGTH) {
                    processEndStage(1);
                } else if (cpu.gridZ <= -MAP_LENGTH) {
                    processEndStage(2);
                }

                if (gameActive) { 
                    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 (isMultiplayer) {
                    if (keys.Numpad8)      cpu.tryMove(0, -1);
                    else if (keys.Numpad2) cpu.tryMove(0,  1);
                    else if (keys.Numpad4) cpu.tryMove(-1, 0);
                    else if (keys.Numpad6) cpu.tryMove(1,  0);
                } else {
                    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) {
                                if (cpuBrain.mode !== 'stair') {
                                    cpuBrain.stuckTimer += cpuThinkInterval;
                                } else {
                                    cpuBrain.stuckTimer += cpuThinkInterval * 0.3;
                                }
                                
                                if (cpuBrain.stuckTimer > 5.0) {
                                    cpuPathCache = null;
                                    cpuBrain.actionLockTimer = 0;
                                    cpuBrain.mode = 'normal';
                                    cpuBrain.stairQueue = [];
                                    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';
                                cpuBrain.stairQueue = [];
                                cpuBrain.consecutiveBuildCount = 0;
                                cpuPathCache = null;
                                
                                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 === 'stair') {
                                        if (cpuBrain.stairQueue.length === 0 || cpuBrain.consecutiveBuildCount > 8) {
                                            cpuBrain.mode = 'normal';
                                            cpuBrain.stairQueue = [];
                                            cpuBrain.consecutiveBuildCount = 0;
                                            cpuPathCache = null;
                                        } else {
                                            let step = cpuBrain.stairQueue[0];

                                            if (!cpu.heldBlock) {
                                                let target = cpuFindClosestBlockToFetch();
                                                if (!target) {
                                                    cpuBrain.mode = 'normal';
                                                    cpuBrain.stairQueue = [];
                                                } else {
                                                    let dist = Math.abs(target.x - cpu.gridX) + Math.abs(target.z - cpu.gridZ);
                                                    if (dist === 0) {
                                                        cpu.tryMove(0, 1);
                                                        cpuBrain.actionLockTimer = 0.15;
                                                    } else if (dist === 1) {
                                                        cpu.dirX = target.x - cpu.gridX;
                                                        cpu.dirZ = target.z - cpu.gridZ;
                                                        let liftTgt = getSmartLiftTarget(cpu.gridX, cpu.gridY, cpu.gridZ, cpu.dirX, cpu.dirZ);
                                                        if (liftTgt) {
                                                            cpu.actionLiftOrPlace();
                                                            cpuBrain.actionLockTimer = 0.28;
                                                        } else {
                                                            cpu.tryMove(Math.random() < 0.5 ? 1 : -1, 0);
                                                            cpuBrain.actionLockTimer = 0.15;
                                                        }
                                                    } else {
                                                        let move = cpuFindPathTo(target.x, target.z);
                                                        if (move) {
                                                            cpu.tryMove(move.dx, move.dz);
                                                            cpuBrain.actionLockTimer = 0.12;
                                                        } else {
                                                            cpuBrain.mode = 'normal';
                                                            cpuBrain.stairQueue = [];
                                                        }
                                                    }
                                                }
                                            } else {
                                                let px = step.placeX;
                                                let pz = step.placeZ;
                                                let dist = Math.abs(px - cpu.gridX) + Math.abs(pz - cpu.gridZ);

                                                if (step.type === 'stack') {
                                                    if (dist === 0) {
                                                        cpu.dirX = 0;
                                                        cpu.dirZ = -1;
                                                        cpu.actionLiftOrPlace();
                                                        cpuBrain.stairQueue.shift();
                                                        cpuBrain.consecutiveBuildCount++;
                                                        cpuBrain.actionLockTimer = 0.45;
                                                        cpuNotifyWorldChanged();
                                                        cpuPathCache = null;
                                                    } else {
                                                        let move = cpuFindPathTo(px, pz);
                                                        if (move) {
                                                            cpu.tryMove(move.dx, move.dz);
                                                            cpuBrain.actionLockTimer = 0.12;
                                                        } else {
                                                            cpuBrain.stairQueue.shift();
                                                        }
                                                    }
                                                } else if (step.type === 'fill') {
                                                    let standZ = step.standZ !== undefined ? step.standZ : pz + 1;
                                                    let standDist = Math.abs(px - cpu.gridX) + Math.abs(standZ - cpu.gridZ);

                                                    if (standDist === 0 || dist === 1) {
                                                        cpu.dirX = px - cpu.gridX;
                                                        cpu.dirZ = pz - cpu.gridZ;
                                                        if (cpu.dirZ === 0 && cpu.dirX === 0) cpu.dirZ = -1;
                                                        cpu.actionLiftOrPlace();
                                                        cpuBrain.stairQueue.shift();
                                                        cpuBrain.consecutiveBuildCount++;
                                                        cpuBrain.actionLockTimer = 0.45;
                                                        cpuNotifyWorldChanged();
                                                        cpuPathCache = null;
                                                    } else {
                                                        let move = cpuFindPathTo(px, standZ);
                                                        if (move) {
                                                            cpu.tryMove(move.dx, move.dz);
                                                            cpuBrain.actionLockTimer = 0.12;
                                                        } else {
                                                            cpuBrain.stairQueue.shift();
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }

                                    else 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 === 0) {
                                                cpu.dirX = 0;
                                                cpu.dirZ = -1;
                                                cpu.actionLiftOrPlace();
                                                cpuBrain.actionLockTimer = 0.4;
                                                cpuBrain.mode = 'normal';
                                                cpuBrain.buildTarget = null;
                                                cpuNotifyWorldChanged();
                                                cpuPathCache = null;
                                            } else if (dist === 1) {
                                                cpu.dirX = tx - cpu.gridX;
                                                cpu.dirZ = tz - cpu.gridZ;
                                                cpu.actionLiftOrPlace();
                                                cpuBrain.actionLockTimer = 0.4;
                                                cpuBrain.mode = 'normal';
                                                cpuBrain.buildTarget = null;
                                                cpuNotifyWorldChanged();
                                                cpuPathCache = null;
                                            } 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 liftTgt = getSmartLiftTarget(cpu.gridX, cpu.gridY, cpu.gridZ, cpu.dirX, cpu.dirZ);
                                                    if (liftTgt) {
                                                        cpu.actionLiftOrPlace();
                                                        cpuBrain.actionLockTimer = 0.28;
                                                        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 {
                                        cpuBrain.consecutiveBuildCount = 0;

                                        let pathResult = cpuFindPathCached();
                                        let forwardProgress = false;
                                        let nextNode = null;

                                        if (pathResult && pathResult.path && pathResult.path.length > 0) {
                                            for (let p of pathResult.path) {
                                                if (p.z < cpu.gridZ) {
                                                    forwardProgress = true;
                                                    nextNode = pathResult.path[0];
                                                    break;
                                                }
                                            }
                                        }

                                        if (forwardProgress && nextNode) {
                                            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 {
                                                let obstacle = cpuDetectObstacleAhead();
                                                if (obstacle && obstacle.stepsNeeded > 0) {
                                                    let plan = cpuPlanStairQueue(obstacle);
                                                    if (plan.length > 0) {
                                                        cpuBrain.stairQueue = plan;
                                                        cpuBrain.mode = 'stair';
                                                        cpuBrain.consecutiveBuildCount = 0;
                                                        cpuBrain.actionLockTimer = 0.1;
                                                        cpuPathCache = null;
                                                    } 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.28;
                                                            } 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.35;
                                                        }
                                                    }
                                                } 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.28;
                                                        } 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.35;
                                                    }
                                                }
                                            }
                                        } else {
                                            let obstacle = cpuDetectObstacleAhead();
                                            if (obstacle && obstacle.stepsNeeded > 0) {
                                                let plan = cpuPlanStairQueue(obstacle);
                                                if (plan.length > 0) {
                                                    cpuBrain.stairQueue = plan;
                                                    cpuBrain.mode = 'stair';
                                                    cpuBrain.consecutiveBuildCount = 0;
                                                    cpuBrain.actionLockTimer = 0.1;
                                                    cpuPathCache = null;
                                                } 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;
                                                    }
                                                }
                                            } 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;
                                                }
                                            }
                                        }

                                        if (Math.random() < CPU_AI.INTERFERE_RATE && currentStage >= 2) {
                                            let distToPlayer = Math.abs(player.gridX - cpu.gridX) + Math.abs(player.gridZ - cpu.gridZ);
                                            if (distToPlayer > 4 && cpuBrain.mode !== 'stair') {
                                                cpu.actionInterfere();
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

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

            if (gameActive) {
                let swayX = Math.sin(timeElapsed * 0.5) * 1.5;
                let swayZ = Math.cos(timeElapsed * 0.4) * 0.5;

                if (player.hp > 0) {
                    let t1 = new THREE.Vector3(player.mesh.position.x, player.mesh.position.y, player.mesh.position.z);
                    camTargetP1.lerp(t1, 0.1);
                    cameraP1.position.copy(camTargetP1).add(baseCamOffset).add(new THREE.Vector3(swayX, 0, swayZ));
                    cameraP1.lookAt(camTargetP1);
                }
                if (cpu.hp > 0) {
                    let t2 = new THREE.Vector3(cpu.mesh.position.x, cpu.mesh.position.y, cpu.mesh.position.z);
                    camTargetP2.lerp(t2, 0.1);
                    cameraP2.position.copy(camTargetP2).add(baseCamOffset).add(new THREE.Vector3(swayX, 0, swayZ));
                    cameraP2.lookAt(camTargetP2);
                }
            }

            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);
                        removeBlock(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);
                        removeBlock(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;
            }

            renderSplitScreen();
        }

        function processLanding(ab, index) {
            if (ab.ey < -10) {
                removeBlock(ab.mesh);
            } else {
                if (ab.mesh.userData.type === 'bomb') {
                    explodeBomb(Math.round(ab.ex), Math.round(ab.ey), Math.round(ab.ez));
                    removeBlock(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', () => {
            renderer.setSize(window.innerWidth, window.innerHeight);
            cameraP1.aspect = (window.innerWidth / 2) / window.innerHeight;
            cameraP1.updateProjectionMatrix();
            cameraP2.aspect = (window.innerWidth / 2) / window.innerHeight;
            cameraP2.updateProjectionMatrix();
        });

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

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

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

気楽に遊べるゲーム

  • 23本

コメント

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