ブラウザで遊べるドライブゲームImageBooster

【操作説明】

・左右キー … 自機を左右に移動。
・上キー … アクセル。
・下キー … ブレーキ。
・スペースキー … ジャンプ。

・F1キー … チューブモードの一人称視点に切り替え。
・F2キー … 平面モードに切り替え。
・F3キー … チューブモードの三人称視点に切り替え。

・障害物を避けて、高得点を目指しましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Image Booster 1.1 - Shortcut Fix</title>
    <style>
        body { margin: 0; overflow: hidden; background: #000; font-family: 'Arial Black', sans-serif; }
        #ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; color: white; text-shadow: 2px 2px 4px #000; }
        
        /* 視点モードを右上端へ */
        #view-mode { position: absolute; top: 20px; right: 20px; font-size: 20px; color: #0f0; }

        /* 画面下部の3連ゲージHUD */
        #hud-bottom { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); width: 85%; display: none; flex-direction: row; justify-content: space-around; }
        .gauge-container { width: 30%; text-align: center; }
        .gauge-label { font-size: 18px; margin-bottom: 5px; }
        .bar-bg { width: 100%; height: 12px; border: 2px solid #fff; border-radius: 6px; overflow: hidden; }
        .bar-fill { height: 100%; width: 100%; transition: width 0.1s ease-out; }

        /* 指定の配色設定 */
        #hp-bg { background: #ff0000; }
        #hp-fill { background: #adff2f; }
        #speed-bg { background: #000080; }
        #speed-fill { background: #00ffff; }
        #time-bg { background: #b8860b; }
        #time-fill { background: #ffff00; }

        #center-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; text-align: center; }
        h1 { font-size: 60px; margin: 0; color: #0ff; }
        .pop-text { position: absolute; font-weight: bold; font-size: 38px; animation: moveUpFade 1s forwards; transform: translate(-50%, -50%); z-index: 999; }
        @keyframes moveUpFade { 0% { transform: translate(-50%, 0); opacity: 1; } 100% { transform: translate(-50%, -150px); opacity: 0; } }
    </style>
    <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } } </script>
</head>
<body>
    <div id="ui-layer">
        <div id="view-mode">VIEW: 3RD PERSON</div>
        <div id="hud-bottom">
            <div class="gauge-container">
                <div class="gauge-label" id="hp-txt">HP: 10</div>
                <div class="bar-bg" id="hp-bg"><div id="hp-fill" class="bar-fill"></div></div>
            </div>
            <div class="gauge-container">
                <div class="gauge-label" id="speed-txt">SPEED: 0</div>
                <div class="bar-bg" id="speed-bg"><div id="speed-fill" class="bar-fill"></div></div>
            </div>
            <div class="gauge-container">
                <div class="gauge-label" id="time-txt">TIME: 180</div>
                <div class="bar-bg" id="time-bg"><div id="time-fill" class="bar-fill"></div></div>
            </div>
        </div>
        <div id="center-text"></div>
    </div>

    <script type="module">
        import * as THREE from 'three';

        const MAX_HP = 10, TUBE_R = 15, PLANE_W = 12, MAX_TIME = 180;
        let scene, camera, renderer;
        let gameState = "TITLE", viewMode = "F3"; 
        let score = 0, hp = MAX_HP, timeLeft = MAX_TIME;
        
        let player = { mesh: null, angle: 0, x: 0, z: 0, vz: 0.2, vAngle: 0, vx: 0, altitude: 0, jumpV: 0 };
        let worldObjects = [], groundTube, groundPlane;
        let keys = {};
        let nextSpawnZ = 50;

        function init() {
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000005);
            scene.fog = new THREE.Fog(0x000005, 50, 500);

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

            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
            scene.add(new THREE.AmbientLight(0xffffff, 0.8));
            const sun = new THREE.DirectionalLight(0xffffff, 1.0);
            sun.position.set(0, 20, 10);
            scene.add(sun);

            player.mesh = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.6, 2.5), new THREE.MeshPhongMaterial({ color: 0x00aaff }));
            scene.add(player.mesh);

            groundTube = new THREE.Mesh(
                new THREE.CylinderGeometry(TUBE_R, TUBE_R, 200000, 32, 1, true),
                new THREE.MeshPhongMaterial({ color: 0x333344, side: THREE.BackSide, wireframe: true })
            );
            groundTube.rotation.x = Math.PI / 2;
            scene.add(groundTube);

            groundPlane = new THREE.Mesh(new THREE.PlaneGeometry(100, 200000), new THREE.MeshPhongMaterial({ color: 0x111111 }));
            groundPlane.rotation.x = -Math.PI / 2;
            groundPlane.visible = false;
            scene.add(groundPlane);

            // キー入力制御の強化
            window.addEventListener('keydown', (e) => {
                // F1, F2, F3 のブラウザデフォルト機能を無効化
                if(["F1", "F2", "F3"].includes(e.key) || ["F1", "F2", "F3"].includes(e.code)) {
                    e.preventDefault();
                    viewMode = e.code;
                    updateViewLabel();
                }

                keys[e.code] = true;
                if(e.code === 'Space') {
                    e.preventDefault(); // スペースでのスクロール防止
                    handleSpace();
                }
            });
            window.addEventListener('keyup', (e) => keys[e.code] = false);
            
            updateViewLabel();
            changeState("TITLE");
            animate();
        }

        function updateViewLabel() {
            const labels = { "F1": "1ST PERSON", "F2": "PLANE MODE", "F3": "3RD PERSON" };
            document.getElementById('view-mode').innerText = "VIEW: " + labels[viewMode];
            groundTube.visible = (viewMode !== "F2");
            groundPlane.visible = (viewMode === "F2");
        }

        function handleSpace() {
            if (gameState === "TITLE" || gameState === "GAMEOVER") changeState("STAGE_START");
            else if (gameState === "STAGE_START") changeState("PLAYING");
            else if (gameState === "PLAYING" && player.altitude <= 0.01) player.jumpV = 0.4;
        }

        function changeState(s) {
            gameState = s;
            document.getElementById('hud-bottom').style.display = (s === "PLAYING") ? "flex" : "none";
            const menu = document.getElementById('center-text');
            if(s === "TITLE") menu.innerHTML = "<h1>Image Booster 1.1</h1><p>PRESS SPACE TO START</p>";
            else if(s === "STAGE_START") { menu.innerHTML = "<h1>READY?</h1><p>SPACE: GO!</p>"; resetPlayer(); }
            else if(s === "PLAYING") menu.innerHTML = "";
            else if(s === "GAMEOVER") menu.innerHTML = "<h1 style='color:red;'>GAME OVER</h1><p>SCORE: "+score+"</p><p>SPACE TO RESTART</p>";
        }

        function resetPlayer() {
            player.angle = 0; player.x = 0; player.z = 0; player.vz = 0.5; player.altitude = 0; player.jumpV = 0;
            hp = MAX_HP; score = 0; timeLeft = MAX_TIME;
            worldObjects.forEach(obj => scene.remove(obj.mesh));
            worldObjects = [];
            nextSpawnZ = 50;
        }

        function animate() {
            requestAnimationFrame(animate);
            if (gameState === "PLAYING") {
                updatePhysics();
                updateObjects();
            }
            updateCamera();
            renderer.render(scene, camera);
        }

        function updatePhysics() {
            if (keys['ArrowUp']) player.vz += 0.04;
            else player.vz *= 0.995;
            if (keys['ArrowDown']) player.vz *= 0.90;
            player.vz = Math.max(0.1, Math.min(player.vz, 2.5));

            player.altitude += player.jumpV;
            if (player.altitude > 0) player.jumpV -= 0.02;
            else { player.altitude = 0; player.jumpV = 0; }

            if (viewMode === "F2") {
                if (keys['ArrowLeft']) player.vx = THREE.MathUtils.lerp(player.vx, 0.7, 0.1);
                else if (keys['ArrowRight']) player.vx = THREE.MathUtils.lerp(player.vx, -0.7, 0.1);
                else player.vx *= 0.9;
                player.x = Math.max(-PLANE_W, Math.min(PLANE_W, player.x + player.vx));
                player.mesh.position.set(player.x, 0.5 + player.altitude, player.z);
                player.mesh.rotation.set(0, 0, player.vx * 0.3);
            } else {
                if (keys['ArrowLeft']) player.vAngle = THREE.MathUtils.lerp(player.vAngle, 0.08, 0.1);
                else if (keys['ArrowRight']) player.vAngle = THREE.MathUtils.lerp(player.vAngle, -0.08, 0.1);
                else player.vAngle *= 0.9;
                player.angle += player.vAngle;
                
                const r = TUBE_R - 0.5 - player.altitude;
                player.mesh.position.set(Math.sin(player.angle)*r, -Math.cos(player.angle)*r, player.z);
                player.mesh.rotation.set(0, 0, player.angle);
            }
            player.z += player.vz;
            timeLeft -= 1/60;
            if (hp <= 0 || timeLeft <= 0) changeState("GAMEOVER");
            updateUI();
        }

        function updateCamera() {
            player.mesh.visible = (viewMode !== "F1");
            if (viewMode === "F1") { 
                camera.position.copy(player.mesh.position);
                camera.up.set(-player.mesh.position.x, -player.mesh.position.y, 0).normalize();
                camera.lookAt(player.mesh.position.x, player.mesh.position.y, player.z + 100);
            } else if (viewMode === "F2") { 
                camera.up.set(0, 1, 0);
                camera.position.set(player.x, 6, player.z - 15);
                camera.lookAt(player.x, 1, player.z + 30);
            } else { 
                const camDist = 18, camHeight = 5;
                camera.position.x = player.mesh.position.x - Math.sin(player.angle) * camHeight;
                camera.position.y = player.mesh.position.y + Math.cos(player.angle) * camHeight;
                camera.position.z = player.z - camDist;
                camera.up.set(-player.mesh.position.x, -player.mesh.position.y, 0).normalize();
                camera.lookAt(player.mesh.position.x, player.mesh.position.y, player.z + 20);
            }
        }

        function updateObjects() {
            if (player.z + 250 > nextSpawnZ) { spawnObject(nextSpawnZ); nextSpawnZ += 5; }
            for (let i = worldObjects.length - 1; i >= 0; i--) {
                const obj = worldObjects[i];
                if(obj.flying) {
                    obj.mesh.position.add(obj.vel);
                } else if (player.mesh.position.distanceTo(obj.mesh.position) < 2.5) {
                    handleCollision(obj);
                    if(obj.type !== "block" && obj.type !== "hurdle") {
                        scene.remove(obj.mesh);
                        worldObjects.splice(i, 1);
                        continue;
                    }
                }
                if (obj.mesh.position.z < player.z - 50) {
                    scene.remove(obj.mesh);
                    worldObjects.splice(i, 1);
                }
            }
        }

        function spawnObject(z) {
            let type = (Math.random() < 0.1) ? "score" : (Math.random() < 0.05) ? "heal" : "block";
            if (Math.random() < 0.15) type = "hurdle";
            const colors = { block: 0x884422, hurdle: 0xff3300, score: 0xffff00, heal: 0x00ff00 };
            const mesh = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshPhongMaterial({ color: colors[type] }));
            if (viewMode === "F2") mesh.position.set((Math.random()-0.5)*PLANE_W*2, 1, z);
            else {
                const a = Math.random()*Math.PI*2;
                mesh.position.set(Math.sin(a)*(TUBE_R - 1), -Math.cos(a)*(TUBE_R - 1), z);
                mesh.rotation.z = a;
            }
            scene.add(mesh);
            worldObjects.push({ mesh, type, flying: false, vel: new THREE.Vector3() });
        }

        function handleCollision(obj) {
            if (obj.type === "block") {
                score += 10; showPopText("+10", "#ffaa00"); 
                obj.flying = true; obj.vel.set((Math.random()-0.5)*2, 2, 3);
            } else if (obj.type === "hurdle") {
                hp--; showPopText("HP -1", "#ff0000"); 
                obj.flying = true; obj.vel.set(0, 1, 0.5); 
            } else if (obj.type === "score") { score += 100; showPopText("+100", "#ffff00"); }
            else if (obj.type === "heal") { hp = Math.min(MAX_HP, hp + 1); showPopText("HP +1", "#00ff00"); }
        }

        function showPopText(text, color) {
            const div = document.createElement('div');
            div.className = 'pop-text';
            div.style.color = color;
            div.innerText = text;
            div.style.left = "50%";
            div.style.top = "50%";
            document.getElementById('ui-layer').appendChild(div);
            setTimeout(() => div.remove(), 1000);
        }

        function updateUI() {
            document.getElementById('hp-txt').innerText = "HP: " + hp;
            document.getElementById('hp-fill').style.width = (hp / MAX_HP * 100) + "%";
            document.getElementById('speed-txt').innerText = "SPEED: " + (player.vz * 40).toFixed(0);
            document.getElementById('speed-fill').style.width = (player.vz / 2.5 * 100) + "%";
            document.getElementById('time-txt').innerText = "TIME: " + Math.max(0, timeLeft).toFixed(0);
            document.getElementById('time-fill').style.width = (timeLeft / MAX_TIME * 100) + "%";
        }

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

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
買うたび 抽選 ※条件・上限あり \note クリエイター感謝祭ポイントバックキャンペーン/最大全額もどってくる! 12.1 月〜1.14 水 まで
ブラウザで遊べるドライブゲームImageBooster|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word 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