🎮ArcadeLab

Geordie Lander - Newcastle Arcade

by LunarPanda99
675 lines19.7 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>Geordie Lander - Newcastle Arcade</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            touch-action: none; /* Prevents mobile scrolling/zooming interference */
        }
        body {
            background-color: #0b0f19;
            color: #f3f4f6;
            font-family: 'Courier New', Courier, monospace;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            overflow: hidden;
            user-select: none;
            -webkit-user-select: none;
        }
        #game-container {
            position: relative;
            width: 100%;
            max-width: 420px;
            aspect-ratio: 3 / 4;
            box-shadow: 0 0 30px rgba(0, 229, 255, 0.3);
            border: 3px solid #1f2937;
            border-radius: 12px;
            background: #050508;
            overflow: hidden;
        }
        canvas {
            width: 100%;
            height: 100%;
            display: block;
            background: linear-gradient(to bottom, #020617 0%, #0f172a 75%, #1e1b4b 100%);
        }
        /* Very subtle CRT scanline effect */
        #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.05) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.005), rgba(0, 255, 0, 0.002), rgba(0, 0, 255, 0.005));
            z-index: 10;
            background-size: 100% 4px, 6px 100%;
            pointer-events: none;
        }
        .ui-overlay {
            position: absolute;
            top: 10px;
            left: 10px;
            right: 10px;
            display: flex;
            justify-content: space-between;
            pointer-events: none;
            z-index: 20;
            font-size: clamp(10px, 2.8vw, 13px);
            font-weight: bold;
            text-shadow: 2px 2px #000;
            color: #38bdf8;
            letter-spacing: 1px;
        }
        .hud-box {
            background: rgba(15, 23, 42, 0.88);
            padding: 6px 10px;
            border: 2px solid #38bdf8;
            border-radius: 6px;
        }
        .fuel-bar-container {
            width: 75px;
            height: 9px;
            background: #334155;
            border: 1px solid #94a3b8;
            margin-top: 4px;
            border-radius: 2px;
            overflow: hidden;
        }
        .fuel-bar-fill {
            height: 100%;
            background: #22c55e;
            width: 100%;
            transition: width 0.1s linear;
        }
        #mobile-controls {
            position: absolute;
            bottom: 10px;
            left: 0;
            right: 0;
            display: flex;
            justify-content: space-between;
            padding: 0 12px;
            z-index: 25;
            pointer-events: auto;
        }
        .mob-group {
            display: flex;
            gap: 8px;
        }
        .mob-btn {
            background: rgba(30, 41, 59, 0.92);
            border: 2px solid #38bdf8;
            color: #38bdf8;
            font-size: 16px;
            padding: 12px 16px;
            border-radius: 10px;
            font-family: inherit;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 0 3px 8px rgba(0,0,0,0.5);
            outline: none;
        }
        .mob-btn:active, .mob-btn.pressed {
            background: #38bdf8;
            color: #0f172a;
            transform: scale(0.95);
        }
        #btn-thrust {
            border-color: #22c55e;
            color: #22c55e;
            padding: 12px 22px;
        }
        #btn-thrust:active, #btn-thrust.pressed {
            background: #22c55e;
            color: #0f172a;
        }
    </style>
</head>
<body>

    <div id="game-container">
        <div class="ui-overlay">
            <div class="hud-box">
                <div>SCORE: <span id="score-val">0</span></div>
                <div>PILOT: <span id="pilot-val" style="color: #facc15;">PJ</span></div>
            </div>
            <div class="hud-box" style="text-align: right;">
                <div>TOON FUEL</div>
                <div class="fuel-bar-container">
                    <div id="fuel-fill" class="fuel-bar-fill"></div>
                </div>
            </div>
        </div>

        <canvas id="canvas" width="600" height="800"></canvas>

        <div id="mobile-controls">
            <div class="mob-group">
                <button class="mob-btn" id="btn-left">◀</button>
                <button class="mob-btn" id="btn-right">▶</button>
            </div>
            <button class="mob-btn" id="btn-thrust">🚀 THRUST</button>
        </div>
    </div>

<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// UI Elements
const scoreEl = document.getElementById('score-val');
const pilotEl = document.getElementById('pilot-val');
const fuelFillEl = document.getElementById('fuel-fill');

// Audio Context for sound effects
let audioCtx = null;
function initAudio() {
    if (!audioCtx) {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
}

function playSound(type) {
    if (!audioCtx) return;
    const now = audioCtx.currentTime;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);

    if (type === 'thrust') {
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(80, now);
        osc.frequency.exponentialRampToValueAtTime(40, now + 0.1);
        gain.gain.setValueAtTime(0.15, now);
        gain.gain.linearRampToValueAtTime(0.01, now + 0.1);
        osc.start(now);
        osc.stop(now + 0.1);
    } else if (type === 'success') {
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(300, now);
        osc.frequency.exponentialRampToValueAtTime(600, now + 0.3);
        gain.gain.setValueAtTime(0.2, now);
        gain.gain.linearRampToValueAtTime(0.01, now + 0.3);
        osc.start(now);
        osc.stop(now + 0.3);
    } else if (type === 'crash') {
        osc.type = 'square';
        osc.frequency.setValueAtTime(120, now);
        osc.frequency.linearRampToValueAtTime(30, now + 0.5);
        gain.gain.setValueAtTime(0.3, now);
        gain.gain.linearRampToValueAtTime(0.01, now + 0.5);
        osc.start(now);
        osc.stop(now + 0.5);
    }
}

// Game States
const STATE_START = 0;
const STATE_PLAYING = 1;
const STATE_LANDED = 2;
const STATE_CRASHED = 3;
let gameState = STATE_START;

let score = 0;
let currentPilot = 0; // 0 = PJ, 1 = Duncan
const pilotNames = ["PJ", "Duncan"];

// Lander physics
let lander = {
    x: 300,
    y: 90,
    vx: 0.3,
    vy: 0,
    angle: 0,
    fuel: 1000,
    maxFuel: 1000,
    width: 36,
    height: 44
};

// Alan Shearer Encouragement Coach
let shearer = {
    active: false,
    text: "",
    timer: 0,
    x: -300
};
let shearerCooldown = 0;

const shearerQuotes = [
    "Haway man! Keep her steady, son!",
    "Absolute belta! That's how we do it in Newcastle!",
    "Even Kevin Keegan would be proud of that!",
    "Show them how number 9 does it!",
    "Divvent crash into the Tyne Bridge, pet!",
    "Why aye! Champion flying!",
    "Proper mint landing, that!",
    "Smashing effort, marra!",
    "Gan canny with the throttle, pet!",
    "Sweeter than a Greggs steak bake!",
    "Belter of a flight path, marra!",
    "Even the Mackems couldn't fly it better!",
    "Pure radge behavior up there, son!",
    "Hadaway man, watch your descent!"
];

function triggerShearer(msg = null) {
    shearer.text = msg || shearerQuotes[Math.floor(Math.random() * shearerQuotes.length)];
    shearer.active = true;
    shearer.timer = 240; // 4 seconds visible
}

// Landing Pads definitions
const pads = [
    { x: 60, y: 710, w: 90, multiplier: 4, name: "St. James' Park Roof (Hard)" },
    { x: 230, y: 730, w: 120, multiplier: 2, name: "Grey Street (Medium)" },
    { x: 420, y: 720, w: 120, multiplier: 1, name: "The Quayside (Easy)" }
];

// Controls state
const keys = { left: false, right: false, thrust: false };

window.addEventListener('keydown', e => {
    initAudio();
    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' || e.code === 'Space') {
        keys.thrust = true;
        e.preventDefault();
    }
    if (e.code === 'Enter' && (gameState === STATE_START || gameState === STATE_LANDED || gameState === STATE_CRASHED)) {
        resetGame(gameState === STATE_CRASHED);
    }
});

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' || e.code === 'Space') keys.thrust = false;
});

// Robust Mobile Touch Controls
function bindTouch(id, keyName) {
    const el = document.getElementById(id);
    
    const activate = (e) => {
        initAudio();
        keys[keyName] = true;
        el.classList.add('pressed');
        e.preventDefault();
    };
    
    const deactivate = (e) => {
        keys[keyName] = false;
        el.classList.remove('pressed');
        e.preventDefault();
    };

    el.addEventListener('touchstart', activate, { passive: false });
    el.addEventListener('touchend', deactivate, { passive: false });
    el.addEventListener('touchcancel', deactivate, { passive: false });
    el.addEventListener('mousedown', (e) => { initAudio(); keys[keyName] = true; el.classList.add('pressed'); });
    el.addEventListener('mouseup', (e) => { keys[keyName] = false; el.classList.remove('pressed'); });
    el.addEventListener('mouseleave', (e) => { keys[keyName] = false; el.classList.remove('pressed'); });
}

bindTouch('btn-left', 'left');
bindTouch('btn-right', 'right');
bindTouch('btn-thrust', 'thrust');

canvas.addEventListener('click', () => {
    initAudio();
    if (gameState === STATE_START || gameState === STATE_LANDED || gameState === STATE_CRASHED) {
        resetGame(gameState === STATE_CRASHED);
    }
});

function resetGame(hardReset = false) {
    if (hardReset) {
        score = 0;
        currentPilot = 0;
    } else if (gameState === STATE_LANDED) {
        currentPilot = 1 - currentPilot;
    }
    
    lander.x = 80 + Math.random() * 440;
    lander.y = 80;
    lander.vx = (Math.random() - 0.5) * 1.2;
    lander.vy = 0.4;
    lander.angle = 0;
    lander.fuel = lander.maxFuel;
    gameState = STATE_PLAYING;
    shearerCooldown = 0;
    
    pilotEl.innerText = pilotNames[currentPilot];
    triggerShearer("Right then, let's fly over the Toon!");
}

// Draw Newcastle Background Landmarks
function drawBackground() {
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(80, 60, 2, 2);
    ctx.fillRect(200, 120, 2, 2);
    ctx.fillRect(350, 50, 3, 3);
    ctx.fillRect(510, 100, 2, 2);
    ctx.fillRect(550, 40, 2, 2);
    
    // Grey's Monument
    ctx.fillStyle = '#334155';
    ctx.fillRect(45, 520, 22, 210);
    ctx.fillStyle = '#475569';
    ctx.fillRect(35, 505, 42, 18);
    ctx.fillStyle = '#facc15';
    ctx.fillRect(48, 488, 16, 18);

    // Tyne Bridge
    ctx.strokeStyle = '#1e293b';
    ctx.lineWidth = 5;
    ctx.beginPath();
    ctx.arc(300, 730, 140, Math.PI, 0, false);
    ctx.stroke();
    ctx.fillStyle = '#1e293b';
    ctx.fillRect(190, 665, 220, 9);
    ctx.fillRect(210, 674, 14, 60);
    ctx.fillRect(376, 674, 14, 60);

    // River Tyne
    const grad = ctx.createLinearGradient(0, 730, 0, 800);
    grad.addColorStop(0, '#0284c7');
    grad.addColorStop(1, '#0369a1');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 730, 600, 70);

    // Landing Pads
    pads.forEach(pad => {
        ctx.fillStyle = '#facc15';
        ctx.fillRect(pad.x, pad.y, pad.w, 10);
        ctx.fillStyle = '#ef4444';
        ctx.fillRect(pad.x - 4, pad.y + 4, 4, 6);
        ctx.fillRect(pad.x + pad.w, pad.y + 4, 4, 6);
        
        ctx.fillStyle = '#ffffff';
        ctx.font = '11px Courier New';
        ctx.textAlign = 'center';
        ctx.fillText(`${pad.multiplier}x`, pad.x + pad.w/2, pad.y - 5);
    });
}

// Draw Lander Ship
function drawLander() {
    ctx.save();
    ctx.translate(lander.x, lander.y);
    ctx.rotate(lander.angle);

    if (keys.thrust && lander.fuel > 0) {
        ctx.fillStyle = '#f97316';
        ctx.beginPath();
        ctx.moveTo(-8, 22);
        ctx.lineTo(0, 38 + Math.random() * 10);
        ctx.lineTo(8, 22);
        ctx.closePath();
        ctx.fill();
        
        ctx.fillStyle = '#facc15';
        ctx.beginPath();
        ctx.moveTo(-4, 22);
        ctx.lineTo(0, 30 + Math.random() * 6);
        ctx.lineTo(4, 22);
        ctx.closePath();
        ctx.fill();
    }

    ctx.strokeStyle = '#94a3b8';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(-16, 15);
    ctx.lineTo(-24, 25);
    ctx.moveTo(16, 15);
    ctx.lineTo(24, 25);
    ctx.stroke();

    ctx.fillStyle = '#e2e8f0';
    ctx.fillRect(-16, -14, 32, 30);
    ctx.strokeStyle = '#64748b';
    ctx.lineWidth = 2;
    ctx.strokeRect(-16, -14, 32, 30);

    ctx.fillStyle = '#0284c7';
    ctx.beginPath();
    ctx.arc(0, -10, 11, Math.PI, 0, false);
    ctx.fill();
    ctx.stroke();

    if (currentPilot === 0) {
        ctx.fillStyle = '#854d0e';
        ctx.fillRect(-6, -18, 12, 6);
        ctx.fillStyle = '#fde047';
        ctx.fillRect(-8, -10, 16, 4);
    } else {
        ctx.fillStyle = '#1e293b';
        ctx.fillRect(-6, -18, 12, 5);
        ctx.fillStyle = '#ec4899';
        ctx.fillRect(-8, -10, 16, 4);
    }

    ctx.fillStyle = '#000000';
    ctx.fillRect(-4, 2, 8, 8);
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(-2, 4, 4, 4);

    ctx.restore();
}

// Draw Alan Shearer Speech Bubble (Moved down below HUD at boxY = 115)
function drawShearer() {
    if (!shearer.active) return;

    if (shearer.x < 15) {
        shearer.x += 8;
    }

    const boxX = shearer.x;
    const boxY = 115;
    const boxW = 310;
    const boxH = 68;

    ctx.fillStyle = 'rgba(15, 23, 42, 0.95)';
    ctx.strokeStyle = '#facc15';
    ctx.lineWidth = 2;
    ctx.fillRect(boxX, boxY, boxW, boxH);
    ctx.strokeRect(boxX, boxY, boxW, boxH);

    const avX = boxX + 8;
    const avY = boxY + 8;

    ctx.fillStyle = '#000000';
    ctx.fillRect(avX, avY + 32, 46, 18);
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(avX + 9, avY + 32, 9, 18);
    ctx.fillRect(avX + 27, avY + 32, 9, 18);

    ctx.fillStyle = '#facc15';
    ctx.font = 'bold 9px Courier New';
    ctx.textAlign = 'center';
    ctx.fillText("9", avX + 23, avY + 44);

    ctx.fillStyle = '#f5d0b1';
    ctx.fillRect(avX + 10, avY + 8, 26, 26);

    ctx.fillStyle = '#d4d4d4';
    ctx.fillRect(avX + 10, avY + 6, 26, 6);

    ctx.fillStyle = '#0f172a';
    ctx.fillRect(avX + 15, avY + 16, 4, 3);
    ctx.fillRect(avX + 26, avY + 16, 4, 3);
    ctx.fillRect(avX + 18, avY + 25, 10, 2);

    ctx.textAlign = 'left';
    ctx.fillStyle = '#facc15';
    ctx.font = 'bold 10px Courier New';
    ctx.fillText("ALAN SHEARER (#9)", boxX + 62, boxY + 22);

    ctx.fillStyle = '#f3f4f6';
    ctx.font = '10px Courier New';
    ctx.fillText(shearer.text, boxX + 62, boxY + 44, 235);

    shearer.timer--;
    if (shearer.timer <= 0) {
        shearer.x -= 8;
        if (shearer.x <= -320) {
            shearer.active = false;
        }
    }
}

// Update game physics
function update() {
    if (gameState !== STATE_PLAYING) return;

    if (keys.left) lander.angle -= 0.04;
    if (keys.right) lander.angle += 0.04;

    if (keys.thrust && lander.fuel > 0) {
        const power = 0.12;
        lander.vx += Math.sin(lander.angle) * power;
        lander.vy -= Math.cos(lander.angle) * power;
        lander.fuel -= 1.5;
        if (Math.random() < 0.3) playSound('thrust');
    }

    lander.vy += 0.032;

    lander.x += lander.vx;
    lander.y += lander.vy;

    if (lander.x < 0) lander.x = 600;
    if (lander.x > 600) lander.x = 0;

    shearerCooldown++;
    if (!shearer.active && shearerCooldown > 500) {
        shearerCooldown = 0;
        triggerShearer();
    }

    scoreEl.innerText = score;
    fuelFillEl.style.width = Math.max(0, (lander.fuel / lander.maxFuel) * 100) + '%';

    if (lander.y + 22 >= 730) {
        let landedPad = null;
        pads.forEach(pad => {
            if (lander.x >= pad.x && lander.x <= pad.x + pad.w) {
                landedPad = pad;
            }
        });

        const safeSpeed = lander.vy < 2.2 && Math.abs(lander.vx) < 1.2;
        const safeAngle = Math.abs(lander.angle % (Math.PI * 2)) < 0.25;

        if (landedPad && safeSpeed && safeAngle) {
            gameState = STATE_LANDED;
            const points = landedPad.multiplier * 150;
            score += points;
            playSound('success');
            triggerShearer(`Howay! Cracking landing on ${landedPad.name}! (+${points} pts)`);
        } else {
            gameState = STATE_CRASHED;
            playSound('crash');
            triggerShearer("Divvent do that again! Total write-off!");
        }
    }
}

// Render screens & game objects
function draw() {
    ctx.clearRect(0, 0, 600, 800);

    drawBackground();
    drawLander();
    drawShearer();

    if (gameState === STATE_START) {
        ctx.fillStyle = 'rgba(2, 6, 23, 0.9)';
        ctx.fillRect(0, 0, 600, 800);

        ctx.fillStyle = '#38bdf8';
        ctx.font = 'bold 30px Courier New';
        ctx.textAlign = 'center';
        ctx.fillText("GEORDIE LANDER", 300, 220);

        ctx.fillStyle = '#facc15';
        ctx.font = '14px Courier New';
        ctx.fillText("Pilot PJ & Duncan with Toon Fuel", 300, 265);

        ctx.fillStyle = '#94a3b8';
        ctx.font = '12px Courier New';
        ctx.fillText("Use bottom buttons or keyboard to play", 300, 330);
        ctx.fillText("Land softly on Quayside, Grey St, or SJP!", 300, 360);

        ctx.fillStyle = '#22c55e';
        ctx.font = 'bold 15px Courier New';
        ctx.fillText(">> TAP OR PRESS ENTER TO START HAWAY MAN! <<", 300, 450);
    } 
    else if (gameState === STATE_LANDED) {
        ctx.fillStyle = 'rgba(15, 23, 42, 0.88)';
        ctx.fillRect(60, 250, 480, 280);
        ctx.strokeStyle = '#22c55e';
        ctx.lineWidth = 3;
        ctx.strokeRect(60, 250, 480, 280);

        ctx.fillStyle = '#22c55e';
        ctx.font = 'bold 22px Courier New';
        ctx.textAlign = 'center';
        ctx.fillText("BELTA! SUCCESSFUL LANDING!", 300, 305);

        ctx.fillStyle = '#f3f4f6';
        ctx.font = '14px Courier New';
        ctx.fillText(`Pilot ${pilotNames[currentPilot]} secured the ship!`, 300, 350);
        ctx.fillText("Next pilot stepping into cockpit...", 300, 390);

        ctx.fillStyle = '#facc15';
        ctx.font = 'bold 14px Courier New';
        ctx.fillText("TAP OR PRESS ENTER FOR NEXT LAUNCH", 300, 465);
    } 
    else if (gameState === STATE_CRASHED) {
        ctx.fillStyle = 'rgba(15, 23, 42, 0.9)';
        ctx.fillRect(60, 250, 480, 280);
        ctx.strokeStyle = '#ef4444';
        ctx.lineWidth = 3;
        ctx.strokeRect(60, 250, 480, 280);

        ctx.fillStyle = '#ef4444';
        ctx.font = 'bold 24px Courier New';
        ctx.textAlign = 'center';
        ctx.fillText("HOWAY MAN! YOU CRASHED!", 300, 320);

        ctx.fillStyle = '#f3f4f6';
        ctx.font = '14px Courier New';
        ctx.fillText(`Final Score: ${score}`, 300, 380);

        ctx.fillStyle = '#facc15';
        ctx.font = 'bold 14px Courier New';
        ctx.fillText("TAP OR PRESS ENTER TO TRY AGAIN", 300, 460);
    }
}

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

loop();
</script>

</body>
</html>

Game Source: Geordie Lander - Newcastle Arcade

Creator: LunarPanda99

Libraries: none

Complexity: complex (675 lines, 19.7 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: geordie-lander-newcastle-arcade-lunarpanda99" to link back to the original. Then publish at arcadelab.ai/publish.