🎮ArcadeLab

LUNAR LANDER ARCADE

by PrismPhoenix37
681 lines23.6 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>LUNAR LANDER ARCADE</title>
    <style>
        * {
            box-sizing: border-box;
            user-select: none;
            -webkit-user-select: none;
            touch-action: manipulation;
        }
        body {
            margin: 0;
            padding: 0;
            background-color: #050505;
            color: #00ffcc;
            font-family: 'Courier New', Courier, monospace, sans-serif;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100vh;
            width: 100vw;
        }
        #game-container {
            position: relative;
            width: 100%;
            max-width: 800px;
            height: 100%;
            max-height: 600px;
            display: flex;
            flex-direction: column;
            background: #000;
            border: 4px solid #113322;
            box-shadow: 0 0 20px rgba(0, 255, 204, 0.2);
        }
        canvas {
            display: block;
            background-color: #000;
            width: 100%;
            height: 100%;
            flex-grow: 1;
        }
        /* CRT Scanline Overlay */
        #game-container::after {
            content: " ";
            display: block;
            position: absolute;
            top: 0; left: 0; bottom: 0; right: 0;
            background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
            z-index: 10;
            background-size: 100% 4px, 6px 100%;
            pointer-events: none;
        }
        .hud-overlay {
            position: absolute;
            top: 10px;
            left: 10px;
            right: 10px;
            display: flex;
            justify-content: space-between;
            font-size: 14px;
            font-weight: bold;
            color: #00ffcc;
            text-shadow: 0 0 5px #00ffcc;
            pointer-events: none;
            z-index: 20;
            text-transform: uppercase;
        }
        .hud-group {
            display: flex;
            gap: 15px;
        }
        #ui-screen {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0, 0, 0, 0.85);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            z-index: 30;
            text-align: center;
            padding: 20px;
        }
        h1 {
            font-size: 32px;
            margin: 0 0 10px 0;
            color: #00ffcc;
            text-shadow: 0 0 10px #00ffcc, 0 0 20px #00ffcc;
            letter-spacing: 2px;
        }
        p {
            font-size: 14px;
            color: #88ffcc;
            max-width: 400px;
            line-height: 1.5;
            margin-bottom: 25px;
        }
        .btn {
            background: transparent;
            color: #00ffcc;
            border: 2px solid #00ffcc;
            padding: 12px 30px;
            font-size: 16px;
            font-family: 'Courier New', Courier, monospace;
            font-weight: bold;
            cursor: pointer;
            text-transform: uppercase;
            box-shadow: 0 0 10px rgba(0, 255, 204, 0.3);
            transition: all 0.2s ease;
        }
        .btn:hover, .btn:active {
            background: #00ffcc;
            color: #000;
            box-shadow: 0 0 20px #00ffcc;
        }
        /* Mobile Touch Controls */
        #mobile-controls {
            position: absolute;
            bottom: 0;
            left: 0;
            width: 100%;
            height: 120px;
            display: none;
            justify-content: space-between;
            align-items: center;
            padding: 0 20px;
            z-index: 25;
            pointer-events: none;
        }
        .control-group {
            display: flex;
            gap: 15px;
            pointer-events: auto;
        }
        .ctrl-btn {
            width: 65px;
            height: 65px;
            background: rgba(0, 255, 204, 0.1);
            border: 2px solid #00ffcc;
            border-radius: 50%;
            color: #00ffcc;
            font-size: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 0 10px rgba(0, 255, 204, 0.2);
            outline: none;
        }
        .ctrl-btn:active {
            background: rgba(0, 255, 204, 0.4);
            box-shadow: 0 0 15px #00ffcc;
            transform: scale(0.95);
        }
        @media (pointer: coarse) {
            #mobile-controls {
                display: flex;
            }
        }
    </style>
</head>
<body>

    <div id="game-container">
        <!-- HUD Overlay -->
        <div class="hud-overlay">
            <div class="hud-group">
                <span>FUEL: <span id="fuel-val">1000</span></span>
                <span>ALT: <span id="alt-val">0</span></span>
            </div>
            <div class="hud-group">
                <span>H-SPD: <span id="hspd-val">0.0</span></span>
                <span>V-SPD: <span id="vspd-val">0.0</span></span>
                <span>SCORE: <span id="score-val">0</span></span>
            </div>
        </div>

        <!-- Canvas -->
        <canvas id="landCanvas"></canvas>

        <!-- Mobile Controls -->
        <div id="mobile-controls">
            <div class="control-group">
                <button class="ctrl-btn" id="btn-left">◀</button>
                <button class="ctrl-btn" id="btn-right">▶</button>
            </div>
            <div class="control-group">
                <button class="ctrl-btn" id="btn-thrust">▲</button>
            </div>
        </div>

        <!-- Start / Game Over Screen -->
        <div id="ui-screen">
            <h1 id="screen-title">LUNAR LANDER</h1>
            <p id="screen-desc">Land safely on the flat pads (marked with multipliers). Keep vertical speed under 2.0 and horizontal speed under 1.5. Controls: A/D or Left/Right to rotate, W or Up to thrust.</p>
            <button class="btn" id="start-btn">INSERT COIN</button>
        </div>
    </div>

    <script>
        // Audio Synthesizer using Web Audio API
        class SoundFX {
            constructor() {
                this.ctx = null;
                this.thrusterNode = null;
                this.thrusterGain = null;
            }

            init() {
                if (!this.ctx) {
                    const AudioContext = window.AudioContext || window.webkitAudioContext;
                    this.ctx = new AudioContext();
                }
                if (this.ctx.state === 'suspended') {
                    this.ctx.resume();
                }
            }

            playThrust(active) {
                this.init();
                if (active) {
                    if (!this.thrusterNode) {
                        // White noise generator for thruster roar
                        const bufferSize = this.ctx.sampleRate * 2;
                        const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
                        const data = buffer.getChannelData(0);
                        for (let i = 0; i < bufferSize; i++) {
                            data[i] = Math.random() * 2 - 1;
                        }

                        this.thrusterNode = this.ctx.createBufferSource();
                        this.thrusterNode.buffer = buffer;
                        this.thrusterNode.loop = true;

                        // Lowpass filter for deep rocket rumble
                        const filter = this.ctx.createBiquadFilter();
                        filter.type = 'lowpass';
                        filter.frequency.value = 350;

                        this.thrusterGain = this.ctx.createGain();
                        this.thrusterGain.gain.setValueAtTime(0.15, this.ctx.currentTime);

                        this.thrusterNode.connect(filter);
                        filter.connect(this.thrusterGain);
                        this.thrusterGain.connect(this.ctx.destination);

                        this.thrusterNode.start();
                    }
                } else {
                    if (this.thrusterNode) {
                        try {
                            this.thrusterNode.stop();
                            this.thrusterNode.disconnect();
                        } catch(e) {}
                        this.thrusterNode = null;
                        this.thrusterGain = null;
                    }
                }
            }

            playExplosion() {
                this.init();
                const bufferSize = this.ctx.sampleRate * 1.5;
                const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
                const data = buffer.getChannelData(0);
                for (let i = 0; i < bufferSize; i++) {
                    data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (this.ctx.sampleRate * 0.3));
                }

                const noise = this.ctx.createBufferSource();
                noise.buffer = buffer;

                const filter = this.ctx.createBiquadFilter();
                filter.type = 'lowpass';
                filter.frequency.setValueAtTime(800, this.ctx.currentTime);
                filter.frequency.linearRampToValueAtTime(50, this.ctx.currentTime + 1.5);

                const gain = this.ctx.createGain();
                gain.gain.setValueAtTime(0.8, this.ctx.currentTime);
                gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 1.5);

                noise.connect(filter);
                filter.connect(gain);
                gain.connect(this.ctx.destination);

                noise.start();
            }

            playSuccess() {
                this.init();
                const now = this.ctx.currentTime;
                const osc = this.ctx.createOscillator();
                const gain = this.ctx.createGain();

                osc.type = 'triangle';
                osc.frequency.setValueAtTime(440, now);
                osc.frequency.setValueAtTime(554.37, now + 0.1);
                osc.frequency.setValueAtTime(659.25, now + 0.2);
                osc.frequency.setValueAtTime(880, now + 0.3);

                gain.gain.setValueAtTime(0.3, now);
                gain.gain.exponentialRampToValueAtTime(0.01, now + 0.6);

                osc.connect(gain);
                gain.connect(this.ctx.destination);

                osc.start(now);
                osc.stop(now + 0.6);
            }
        }

        const sfx = new SoundFX();
        const canvas = document.getElementById('landCanvas');
        const ctx = canvas.getContext('2d');

        // Resize Canvas to container
        function resize() {
            const container = document.getElementById('game-container');
            canvas.width = container.clientWidth;
            canvas.height = container.clientHeight;
        }
        window.addEventListener('resize', resize);
        resize();

        // Game State
        let gameState = 'START'; // START, PLAYING, LANDED, CRASHED, GAMEOVER
        let score = 0;
        let fuel = 1000;
        let alt = 0;
        let hSpd = 0;
        let vSpd = 0;

        // Lander Object
        let lander = {
            x: canvas.width / 2,
            y: 50,
            vx: 0,
            vy: 0,
            angle: 0, // radians, 0 is pointing up
            width: 16,
            height: 24,
            rotationSpeed: 0.035,
            thrustPower: 0.07,
            gravity: 0.02
        };

        // Terrain & Pads
        let terrain = [];
        let pads = [];

        function generateTerrain() {
            terrain = [];
            pads = [];
            const segments = 24;
            const segWidth = canvas.width / segments;
            let currentY = canvas.height * 0.65 + Math.random() * (canvas.height * 0.15);

            terrain.push({x: 0, y: canvas.height});
            terrain.push({x: 0, y: currentY});

            let padIndex = Math.floor(Math.random() * 6) + 8; // Pick a segment for landing pad

            for (let i = 1; i <= segments; i++) {
                let nextX = i * segWidth;
                let nextY;

                if (i === padIndex) {
                    // Flat landing pad
                    nextY = currentY;
                    pads.push({
                        x1: (i - 1) * segWidth,
                        x2: nextX,
                        y: nextY,
                        multiplier: Math.random() > 0.6 ? 5 : (Math.random() > 0.3 ? 3 : 2)
                    });
                } else {
                    nextY = currentY + (Math.random() * 80 - 40);
                    // Keep bounds safe
                    nextY = Math.max(canvas.height * 0.4, Math.min(canvas.height * 0.85, nextY));
                }

                terrain.push({x: nextX, y: nextY});
                currentY = nextY;
            }
            terrain.push({x: canvas.width, y: canvas.height});
        }

        function resetLander() {
            lander.x = Math.random() * (canvas.width - 200) + 100;
            lander.y = 40;
            lander.vx = (Math.random() - 0.5) * 1.5;
            lander.vy = 0.5;
            lander.angle = 0;
            fuel = 1000;
        }

        // Input Handling
        const keys = {left: false, right: false, thrust: false};

        window.addEventListener('keydown', e => {
            if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
            if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
            if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.thrust = true;
        });

        window.addEventListener('keyup', e => {
            if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
            if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
            if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.thrust = false;
        });

        // Mobile Button bindings
        const bindTouch = (id, keyName) => {
            const btn = document.getElementById(id);
            btn.addEventListener('touchstart', (e) => { e.preventDefault(); keys[keyName] = true; });
            btn.addEventListener('touchend', (e) => { e.preventDefault(); keys[keyName] = false; });
            btn.addEventListener('mousedown', (e) => { e.preventDefault(); keys[keyName] = true; });
            btn.addEventListener('mouseup', (e) => { e.preventDefault(); keys[keyName] = false; });
        };
        bindTouch('btn-left', 'left');
        bindTouch('btn-right', 'right');
        bindTouch('btn-thrust', 'thrust');

        // UI triggers
        const uiScreen = document.getElementById('ui-screen');
        const startBtn = document.getElementById('start-btn');
        const screenTitle = document.getElementById('screen-title');
        const screenDesc = document.getElementById('screen-desc');

        startBtn.addEventListener('click', () => {
            sfx.init();
            if (gameState === 'GAMEOVER' || gameState === 'START') {
                score = 0;
                generateTerrain();
                resetLander();
            }
            gameState = 'PLAYING';
            uiScreen.style.display = 'none';
        });

        // Game Loop
        let particles = [];

        function update() {
            if (gameState !== 'PLAYING') return;

            // Rotation
            if (keys.left) lander.angle -= lander.rotationSpeed;
            if (keys.right) lander.angle += lander.rotationSpeed;

            // Thrust
            let isThrusting = false;
            if (keys.thrust && fuel > 0) {
                isThrusting = true;
                fuel -= 0.8;
                lander.vx += Math.sin(lander.angle) * lander.thrustPower;
                lander.vy -= Math.cos(lander.angle) * lander.thrustPower;

                // Add exhaust particles
                particles.push({
                    x: lander.x - Math.sin(lander.angle) * 12,
                    y: lander.y + Math.cos(lander.angle) * 12,
                    vx: -Math.sin(lander.angle) * 2 + (Math.random() - 0.5),
                    vy: Math.cos(lander.angle) * 2 + (Math.random() - 0.5),
                    life: 20
                });
            }
            sfx.playThrust(isThrusting && fuel > 0);

            // Gravity
            lander.vy += lander.gravity;

            // Position update
            lander.x += lander.vx;
            lander.y += lander.vy;

            // Screen wrapping horizontally
            if (lander.x < 0) lander.x = canvas.width;
            if (lander.x > canvas.width) lander.x = 0;

            // Update particles
            for (let i = particles.length - 1; i >= 0; i--) {
                let p = particles[i];
                p.x += p.vx;
                p.y += p.vy;
                p.life--;
                if (p.life <= 0) particles.splice(i, 1);
            }

            // Calculate Altitude above terrain
            let terrainYUnderLander = canvas.height;
            for (let i = 0; i < terrain.length - 1; i++) {
                let p1 = terrain[i];
                let p2 = terrain[i+1];
                if (lander.x >= p1.x && lander.x <= p2.x) {
                    let t = (lander.x - p1.x) / (p2.x - p1.x);
                    terrainYUnderLander = p1.y + t * (p2.y - p1.y);
                    break;
                }
            }
            alt = Math.max(0, Math.floor(terrainYUnderLander - lander.y - lander.height / 2));
            hSpd = lander.vx;
            vSpd = lander.vy;

            // Collision detection with terrain
            if (lander.y + lander.height / 2 >= terrainYUnderLander) {
                sfx.playThrust(false);
                checkLanding(terrainYUnderLander);
            }
        }

        function checkLanding(terrainY) {
            // Check if landed on a pad and within safe velocity and upright angle
            let landedOnPad = false;
            let padMultiplier = 1;

            for (let pad of pads) {
                if (lander.x >= pad.x1 && lander.x <= pad.x2 && Math.abs((lander.y + lander.height/2) - pad.y) < 6) {
                    landedOnPad = true;
                    padMultiplier = pad.multiplier;
                    break;
                }
            }

            let safeAngle = Math.abs(lander.angle) < 0.2;
            let safeVSpd = vSpd < 2.2;
            let safeHSpd = Math.abs(hSpd) < 1.5;

            if (landedOnPad && safeAngle && safeVSpd && safeHSpd) {
                // SUCCESSFUL LANDING
                gameState = 'LANDED';
                sfx.playSuccess();
                let earned = Math.floor((Math.floor(fuel) + Math.abs(vSpd)*10) * padMultiplier);
                score += earned;

                screenTitle.innerText = "SUCCESSFUL LANDING!";
                screenDesc.innerText = `Lander secured safely.\nBonus multiplier: x${padMultiplier}\nPoints Earned: ${earned}\nTotal Score: ${score}`;
                startBtn.innerText = "NEXT MISSION";
                uiScreen.style.display = 'flex';

                // Generate new terrain for next level after success
                setTimeout(() => {
                    generateTerrain();
                    resetLander();
                }, 500);

            } else {
                // CRASH
                gameState = 'CRASHED';
                sfx.playExplosion();

                // Create explosion debris particles
                for(let i=0; i<40; i++) {
                    particles.push({
                        x: lander.x,
                        y: lander.y,
                        vx: (Math.random() - 0.5) * 6,
                        vy: (Math.random() - 0.5) * 6,
                        life: 60
                    });
                }

                screenTitle.innerText = "CRASH DETECTED";
                screenDesc.innerText = `Ship destroyed upon impact!\nSpeed or angle was out of safety parameters.\nFinal Score: ${score}`;
                startBtn.innerText = "TRY AGAIN";
                uiScreen.style.display = 'flex';
                gameState = 'GAMEOVER';
            }
        }

        function draw() {
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // Draw Stars / Background
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(100, 50, 2, 2);
            ctx.fillRect(300, 80, 1.5, 1.5);
            ctx.fillRect(550, 40, 2, 2);
            ctx.fillRect(700, 120, 1, 1);
            ctx.fillRect(200, 200, 2, 2);
            ctx.fillRect(450, 150, 1, 1);

            // Draw Terrain
            ctx.strokeStyle = '#00ffcc';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(terrain[0].x, terrain[0].y);
            for (let i = 1; i < terrain.length; i++) {
                ctx.lineTo(terrain[i].x, terrain[i].y);
            }
            ctx.stroke();

            // Highlight Landing Pads
            ctx.strokeStyle = '#ffff00';
            ctx.lineWidth = 4;
            for (let pad of pads) {
                ctx.beginPath();
                ctx.moveTo(pad.x1, pad.y);
                ctx.lineTo(pad.x2, pad.y);
                ctx.stroke();

                // Draw pad multiplier text
                ctx.fillStyle = '#ffff00';
                ctx.font = '10px Courier New';
                ctx.textAlign = 'center';
                ctx.fillText(`[ x${pad.multiplier} ]`, (pad.x1 + pad.x2) / 2, pad.y - 8);
            }

            // Draw Particles (Exhaust & Explosions)
            ctx.fillStyle = '#ff5500';
            for (let p of particles) {
                ctx.fillRect(p.x, p.y, 2, 2);
            }

            // Draw Lander (if not crashed/gameover explosion)
            if (gameState !== 'GAMEOVER' || particles.length > 0) {
                ctx.save();
                ctx.translate(lander.x, lander.y);
                ctx.rotate(lander.angle);

                // Lander Hull Vector Art
                ctx.strokeStyle = '#00ffcc';
                ctx.lineWidth = 1.5;
                ctx.fillStyle = '#001111';
                ctx.beginPath();
                // Body
                ctx.moveTo(0, -12);
                ctx.lineTo(8, 6);
                ctx.lineTo(-8, 6);
                ctx.closePath();
                ctx.fill();
                ctx.stroke();

                // Legs
                ctx.beginPath();
                ctx.moveTo(-8, 6);
                ctx.lineTo(-12, 12);
                ctx.moveTo(8, 6);
                ctx.lineTo(12, 12);
                ctx.stroke();

                // Thrust Flame
                if (keys.thrust && fuel > 0 && gameState === 'PLAYING') {
                    ctx.strokeStyle = '#ff3300';
                    ctx.beginPath();
                    ctx.moveTo(-4, 6);
                    ctx.lineTo(0, 18 + Math.random() * 6);
                    ctx.lineTo(4, 6);
                    ctx.stroke();
                }

                ctx.restore();
            }

            // Update HUD text elements
            document.getElementById('fuel-val').innerText = Math.max(0, Math.floor(fuel));
            document.getElementById('alt-val').innerText = alt;
            document.getElementById('hspd-val').innerText = hSpd.toFixed(1);
            document.getElementById('vspd-val').innerText = vSpd.toFixed(1);
            document.getElementById('score-val').innerText = score;
        }

        // Main Loop
        function loop() {
            update();
            draw();
            requestAnimationFrame(loop);
        }

        // Initialize terrain first time
        generateTerrain();
        loop();
    </script>
</body>
</html>

Game Source: LUNAR LANDER ARCADE

Creator: PrismPhoenix37

Libraries: none

Complexity: complex (681 lines, 23.6 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: lunar-lander-arcade-prismphoenix37" to link back to the original. Then publish at arcadelab.ai/publish.