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



画像

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

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: 10px; left: 10px; color: white;
            text-shadow: 2px 2px 4px #000; pointer-events: none;
            z-index: 10;
        }
        h1 { margin: 0 0 5px 0; font-size: 28px; color: #ffeb3b; }
        #timerBox { font-size: 24px; font-weight: bold; color: #ff5252; margin-bottom: 10px; }
        #instructions { font-size: 13px; line-height: 1.5; background: rgba(0,0,0,0.6); padding: 10px; border-radius: 8px; }
        .highlight { color: #00e5ff; font-weight: bold; }
        .cpu-highlight { color: #ff00ff; font-weight: bold; }
    </style>
</head>
<body>
    <div id="ui">
        <h1>BuildRace</h1>
        <div id="timerBox">Time: <span id="timeDisplay">90</span></div>
        <div id="instructions">
            ※画面を一度クリックして操作を開始してください<br>
            [矢印キー] 移動 / [Aキー] 持つ・投げる / [Zキー] 弾く<br>
            <br>
            <span class="highlight">★勝利条件:CPUより先に最奥(マップ終端)へ到達しろ!</span><br>
            <span class="cpu-highlight">【警告】CPUが賢くなりました。壁を掘り進み、<br>こちらにブロックを投げて妨害してきます!</span><br>
            <br>
            <span style="color:#aaa">■白(軽): 8セル</span> | <span style="color:#888">■灰(中): 4セル</span> | <span style="color:#444">■黒(重): 2セル</span>
        </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        // --- Web Audio API ---
        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        function playSound(type, pitchModifier = 1) {
            if(audioCtx.state === 'suspended') audioCtx.resume();
            const osc = audioCtx.createOscillator();
            const gain = audioCtx.createGain();
            osc.connect(gain); gain.connect(audioCtx.destination);
            
            let now = audioCtx.currentTime;
            if (type === 'jump') {
                osc.type = 'sine'; osc.frequency.setValueAtTime(400, now); osc.frequency.linearRampToValueAtTime(600, 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 * pitchModifier, 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 === 'win') {
                osc.type = 'square'; osc.frequency.setValueAtTime(440, now); osc.frequency.linearRampToValueAtTime(880, now + 0.5);
                gain.gain.setValueAtTime(0.5, now); gain.gain.linearRampToValueAtTime(0, now + 1);
                osc.start(now); osc.stop(now + 1);
            } else if (type === 'lose') {
                osc.type = 'sawtooth'; osc.frequency.setValueAtTime(300, now); osc.frequency.linearRampToValueAtTime(100, now + 0.8);
                gain.gain.setValueAtTime(0.5, now); gain.gain.linearRampToValueAtTime(0, now + 1);
                osc.start(now); osc.stop(now + 1);
            }
        }

        // --- Three.js 基本セットアップ ---
        const scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x000011, 0.03);
        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 15, 15); 
        camera.lookAt(-5, 1, -3);

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

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

        // --- ゲーム管理・グリッドシステム ---
        const MAP_LENGTH = 50; // ちょっと短くして白熱しやすく
        const gridMap = new Map();
        const blocks = [];
        const animatingBlocks = []; 
        let gameActive = true;
        let remainingTime = 90;

        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); }
        function getGrid(x, y, z) { return gridMap.get(getGridKey(x, y, z)); }
        function removeGrid(x, y, z) { gridMap.delete(getGridKey(x, y, z)); }

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

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

        const trajMat = new THREE.LineDashedMaterial({ color: 0xff5555, dashSize: 0.2, gapSize: 0.2 });
        const trajGeo = new THREE.BufferGeometry();
        trajGeo.setFromPoints([new THREE.Vector3(0,0,0), new THREE.Vector3(1,0,0)]);
        const trajLine = new THREE.Line(trajGeo, trajMat);
        trajLine.computeLineDistances(); trajLine.visible = false; scene.add(trajLine);

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

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

        function getLiftTarget(px, py, pz, dx, dz) {
            let tx = px + dx, tz = pz + dz;
            let bWall = getGrid(tx, py, tz);
            if (bWall && !bWall.userData.isStatic) return { block: bWall, x: tx, y: py, z: tz };
            let bFloor = getGrid(tx, py - 1, tz);
            if (bFloor && !bFloor.userData.isStatic) return { block: bFloor, x: tx, y: py - 1, z: tz };
            return null;
        }

        // --- コース生成 ---
        const blockGeo = new THREE.BoxGeometry(1, 1, 1);
        const weightColors = [0xffffff, 0xaaaaaa, 0x444444];
        const weightsDropDist = [0, 8, 4, 2];

        function createBlock(x, y, z, weight, isStatic) {
            const mat = new THREE.MeshLambertMaterial({ color: isStatic ? 0x111133 : weightColors[weight-1] });
            const mesh = new THREE.Mesh(blockGeo, mat);
            mesh.position.set(x, y, z);
            mesh.userData = { weight: weight, isStatic: isStatic };
            scene.add(mesh); blocks.push(mesh); setGrid(x, y, z, mesh);
            return mesh;
        }

        // ゴールラインの作成
        const goalMat = new THREE.MeshBasicMaterial({color: 0x00ff00, transparent: true, opacity: 0.5});
        const goalLine = new THREE.Mesh(new THREE.BoxGeometry(20, 50, 1), goalMat);
        goalLine.position.set(0, 0, -MAP_LENGTH - 2);
        scene.add(goalLine);

        let h1 = 0, h2 = 0; 
        for (let z = 0; z >= -MAP_LENGTH; z--) {
            if (z < -3 && z % 5 === 0) {
                if (Math.random() < 0.7) h1 += Math.floor(Math.random() * 4) + 1;
                if (Math.random() < 0.7) h2 += Math.floor(Math.random() * 4) + 1;
            }
            
            [[-7, -3, h1], [3, 7, h2]].forEach(([startX, endX, h]) => {
                for (let x = startX; x <= endX; x++) {
                    createBlock(x, h - 5, z, 1, true); 
                    for (let y = h - 4; y <= h; y++) {
                        let w = 2; 
                        if (y === h && Math.random() < 0.4) w = 1;
                        else if (y < h - 2) w = 3;
                        createBlock(x, y, z, w, false);
                    }
                }
            });
        }

        // --- キャラクタークラス ---
        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: 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.gridX = x; this.gridY = y; this.gridZ = z;
                this.dirX = 0; this.dirZ = -1;
                this.isPlayer = isPlayer;
                this.heldBlock = null;

                this.isJumping = false; this.jumpTime = 0; this.jumpDuration = 0.25;
                this.startPos = new THREE.Vector3(); this.endPos = new THREE.Vector3();
            }

            update(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;
                }
                
                if (this.isPlayer && gameActive) {
                    if (this.heldBlock && !this.isJumping) {
                        liftCursor.visible = false;
                        let dist = weightsDropDist[this.heldBlock.userData.weight];
                        let tx = this.gridX + this.dirX * dist;
                        let tz = this.gridZ + this.dirZ * dist;
                        let ty = getDropY(tx, tz, this.gridY + 1);

                        targetCursor.position.set(tx, Math.max(ty, -5) + 0.51, tz);
                        targetCursor.visible = true;

                        let pts = [];
                        let sx = this.mesh.position.x, sy = this.mesh.position.y + 1, sz = this.mesh.position.z;
                        for(let i=0; i<=20; i++){
                            let t = i/20;
                            let px = sx + (tx - sx) * t;
                            let pz = sz + (tz - sz) * t;
                            let py = sy + (ty - sy) * t + Math.sin(t * Math.PI) * (dist * 0.4); 
                            pts.push(new THREE.Vector3(px, py, pz));
                        }
                        trajGeo.setFromPoints(pts);
                        trajLine.computeLineDistances();
                        trajLine.visible = true;
                    } else if (!this.heldBlock && !this.isJumping) {
                        targetCursor.visible = false; trajLine.visible = false;
                        let target = getLiftTarget(this.gridX, this.gridY, this.gridZ, this.dirX, this.dirZ);
                        if (target) {
                            liftCursor.position.set(target.x, target.y + 0.51, target.z);
                            liftCursor.visible = true;
                        } else liftCursor.visible = false;
                    }
                }
            }

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

                let front = getGrid(nx, ny, nz);
                let below = getGrid(nx, ny - 1, nz);
                let above = getGrid(nx, ny + 1, nz);

                if (!front && below) this.doJump(nx, ny, nz);
                else if (front && !above) this.doJump(nx, ny + 1, nz);
                else if (!front && !below) {
                    let targetY = ny - 1;
                    while (targetY >= ny - 4) {
                        if (getGrid(nx, targetY - 1, nz)) break;
                        targetY--;
                    }
                    if(targetY >= ny - 4) this.doJump(nx, targetY, nz);
                }
            }

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

            actionLiftOrThrow() {
                if (this.isJumping) return;
                if (this.heldBlock) {
                    let dist = weightsDropDist[this.heldBlock.userData.weight];
                    let tx = this.gridX + this.dirX * dist, tz = this.gridZ + this.dirZ * dist;
                    let ty = getDropY(tx, tz, this.gridY + 1);

                    let block = this.heldBlock;
                    this.heldBlock = null; scene.attach(block);
                    playSound('throw', 1 / block.userData.weight);

                    animatingBlocks.push({
                        mesh: block, sx: block.position.x, sy: block.position.y, sz: block.position.z,
                        ex: tx, ey: ty, ez: tz, time: 0, duration: 0.4 + (dist * 0.05), arcMax: dist * 0.4
                    });
                } else {
                    let target = getLiftTarget(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);
                    }
                }
            }

            actionKick() {
                let target = getLiftTarget(this.gridX, this.gridY, this.gridZ, this.dirX, this.dirZ);
                if (target && !this.heldBlock) {
                    removeGrid(target.x, target.y, target.z);
                    let b = target.block;
                    let dist = weightsDropDist[b.userData.weight] / 2;
                    let endX = target.x, endZ = target.z, endY = target.y;

                    for (let i = 0; i < dist; i++) {
                        let nx = endX + this.dirX, nz = endZ + this.dirZ;
                        if (getGrid(nx, endY, nz)) break;
                        if (!getGrid(nx, endY - 1, nz)) { endX = nx; endZ = nz; endY--; break; }
                        endX = nx; endZ = nz;
                    }
                    b.position.set(endX, endY, endZ); setGrid(endX, endY, endZ, b);
                    dropColumn(target.x, target.y, target.z);
                }
            }
        }

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

        // --- キー入力 ---
        const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false };
        let aPressed = false, zPressed = false;

        window.addEventListener('keydown', (e) => {
            if (!gameActive) return;
            if (keys.hasOwnProperty(e.code)) keys[e.code] = true;
            if (e.code === 'KeyA' && !aPressed) { player.actionLiftOrThrow(); aPressed = true; }
            if (e.code === 'KeyZ' && !zPressed) { player.actionKick(); zPressed = true; }
        });
        window.addEventListener('keyup', (e) => {
            if (keys.hasOwnProperty(e.code)) keys[e.code] = false;
            if (e.code === 'KeyA') aPressed = false;
            if (e.code === 'KeyZ') zPressed = false;
        });

        // --- メインループ ---
        const clock = new THREE.Clock();
        let cpuTimer = 0;

        function animate() {
            requestAnimationFrame(animate);
            const dt = clock.getDelta();

            // 勝敗判定
            if (gameActive) {
                if (player.gridZ <= -MAP_LENGTH) {
                    gameActive = false; playSound('win');
                    document.getElementById('timerBox').innerHTML = "<span style='color:#00e5ff; font-size:36px'>YOU WIN!!</span>";
                    targetCursor.visible = false; liftCursor.visible = false; trajLine.visible = false;
                } else if (cpu.gridZ <= -MAP_LENGTH) {
                    gameActive = false; playSound('lose');
                    document.getElementById('timerBox').innerHTML = "<span style='color:#ff00ff; font-size:36px'>CPU WINS...</span>";
                    targetCursor.visible = false; liftCursor.visible = false; trajLine.visible = false;
                } else {
                    remainingTime -= dt;
                    if (remainingTime <= 0) {
                        remainingTime = 0; gameActive = false; playSound('lose');
                        document.getElementById('timerBox').innerHTML = "TIME UP!";
                        targetCursor.visible = false; liftCursor.visible = false; trajLine.visible = false;
                    }
                    document.getElementById('timeDisplay').innerText = Math.ceil(remainingTime);
                }
            }

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

                // --- 強化版 CPU AI (ブルドーザー型) ---
                cpuTimer += dt;
                if (cpuTimer > 0.3) { // 0.3秒間隔で高速に判断
                    cpuTimer = 0;
                    
                    if (cpu.heldBlock) {
                        // ブロックを持っていたら、プレイヤー側(-X方向)を向いて投げ捨てる!
                        cpu.dirX = -1; cpu.dirZ = 0; 
                        cpu.actionLiftOrThrow();
                        cpu.dirX = 0; cpu.dirZ = -1; // 向きを前に戻す
                    } else {
                        // 基本は前(ゴール)を向く
                        cpu.dirX = 0; cpu.dirZ = -1;
                        let f = getGrid(cpu.gridX, cpu.gridY, cpu.gridZ - 1);
                        let f2 = getGrid(cpu.gridX, cpu.gridY + 1, cpu.gridZ - 1);
                        let floor = getGrid(cpu.gridX, cpu.gridY - 1, cpu.gridZ - 1);

                        if (f && f2) {
                            // 2段以上の壁にぶつかったら「掘る(持つ)」
                            cpu.actionLiftOrThrow();
                        } else if (f && !f2) {
                            // 1段の壁ならジャンプで登れるので前進
                            cpu.tryMove(0, -1);
                        } else if (!f && floor) {
                            // 道が開けていたら前進
                            cpu.tryMove(0, -1);
                        } else if (!f && !floor) {
                            // 崖がある場合、安全に降りられる深さ(4段以内)か確認
                            let dropY = cpu.gridY - 1;
                            while(dropY >= cpu.gridY - 4) {
                                if(getGrid(cpu.gridX, dropY - 1, cpu.gridZ - 1)) break;
                                dropY--;
                            }
                            if (dropY >= cpu.gridY - 4) {
                                cpu.tryMove(0, -1); // 安全なので飛び降りる
                            } else {
                                // 深すぎる、あるいは奈落の場合は横に避ける
                                let canGoLeft = cpu.gridX > 3 && !getGrid(cpu.gridX - 1, cpu.gridY, cpu.gridZ);
                                let canGoRight = cpu.gridX < 7 && !getGrid(cpu.gridX + 1, cpu.gridY, cpu.gridZ);
                                
                                if (canGoLeft && Math.random() < 0.5) cpu.tryMove(-1, 0);
                                else if (canGoRight) cpu.tryMove(1, 0);
                                else if (canGoLeft) cpu.tryMove(-1, 0);
                                else {
                                    // 完全に手詰まりなら足元の床を抜いて下の階層へ活路を見出す
                                    cpu.dirX = 0; cpu.dirZ = 0;
                                    cpu.actionLiftOrThrow();
                                    cpu.dirX = 0; cpu.dirZ = -1;
                                }
                            }
                        }
                    }
                }
            }

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

            if (gameActive) {
                const targetCamPos = new THREE.Vector3(player.mesh.position.x + 5, player.mesh.position.y + 12, player.mesh.position.z + 12);
                camera.position.lerp(targetCamPos, 0.1);
                camera.lookAt(player.mesh.position.x, player.mesh.position.y, player.mesh.position.z - 3);
            }

            for (let i = animatingBlocks.length - 1; i >= 0; i--) {
                let ab = animatingBlocks[i];
                ab.time += dt;
                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);

                if (t >= 1) {
                    if (ab.ey < -10) scene.remove(ab.mesh);
                    else {
                        let rx = Math.round(ab.ex), ry = Math.round(ab.ey), rz = Math.round(ab.ez);
                        ab.mesh.position.set(rx, ry, rz); setGrid(rx, ry, rz, ab.mesh);
                    }
                    animatingBlocks.splice(i, 1);
                }
            }
            renderer.render(scene, camera);
        }

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

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

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

コメント

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

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