• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
07Games
└ Rockfall└ Speak & Math└ Space Invaders└ Apache└ Asteroids└ Molly Roger└ Carrom└ Chess└ Connect 4└ Anagrams
08Voronoi
└ 001└ 002└ 003└ 004└ 005
09Terrain
└ Simple Polygon└ Simple Flyover└ Infinite Flyover 01└ Infinite Flyover 02└ Hexagonal Plane└ Mountain Ridges
10Critter Models 01
└ Wireframe Spider└ Wireframe Fly└ Wireframe Scorpion└ Wireframe Mosquito└ Wireframe Bee└ Wireframe Butterfly└ Wireframe Locust
11Chromatophores
└ 01└ 02└ 03└ 04
12Conformal Maps
└ 01└ 02└ 03└ 04└ 05
02 / 03

APACHE - INTERACTIVE ARCADE GAME SOURCE CODE

<!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>Apache//Can.yon — Dusk Ops</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Oswald:wght@500;600;700&family=Share+Tech+Mono&display=swap');

        /* ===== Design tokens: sunset desert combat ===== */
        :root {
            --ink: #140b08;
            --panel: #1c110b;
            --sky-top: #1e1330;
            --sky-mid: #7a2f3d;
            --sky-horizon: #e8763a;
            --sun-core: #ffe9b8;
            --sun-glow: #ffb454;
            --sand-shadow: #3a1c10;
            --sand-dark: #6b2f1a;
            --sand-mid: #9c4a26;
            --sand-light: #d98a4f;
            --rim-light: #ffcf8a;
            --amber: #f0a44c;
            --amber-bright: #ffc36b;
            --rust-red: #d9502f;
            --dust: #c9a67a;
        }

        * {
            margin: 0; padding: 0; box-sizing: border-box;
            -webkit-tap-highlight-color: transparent;
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            user-select: none;
        }
        body {
            background: var(--ink);
            color: var(--amber);
            font-family: 'Share Tech Mono', 'Courier New', monospace;
            overflow: hidden;
            width: 100vw; height: 100vh;
            display: flex; justify-content: center; align-items: center;
            background-image:
                linear-gradient(rgba(240, 164, 76, 0.04) 1px, transparent 1px),
                linear-gradient(90deg, rgba(240, 164, 76, 0.04) 1px, transparent 1px);
            background-size: 40px 40px;
            touch-action: none;
            position: relative;
        }
        #gameContainer {
            position: relative;
            width: 100%; height: 100%; max-width: 1200px;
            display: flex; flex-direction: column;
            border: 1px solid var(--sand-mid);
            box-shadow: 0 0 40px rgba(217, 138, 79, 0.15), inset 0 0 90px rgba(0,0,0,0.65);
            overflow: hidden;
        }

        #hud {
            display: flex; flex-direction: row; flex-wrap: wrap;
            align-items: center; justify-content: center;
            gap: clamp(6px, 1.4vw, 18px);
            padding: clamp(4px, 0.8vh, 8px) clamp(8px, 1.5vw, 14px);
            background: linear-gradient(180deg, rgba(28,17,11,0.97), rgba(20,11,8,0.92));
            border-bottom: 1px solid var(--sand-mid);
            font-size: clamp(9px, 1.3vh, 13px);
            flex-shrink: 0;
            z-index: 3;
        }
        .hud-item {
            display: flex; align-items: baseline; gap: 5px;
            padding: 0 6px;
            border-left: 2px solid rgba(240, 164, 76, 0.5);
            transition: filter 0.2s ease;
        }
        .hud-item:first-child { border-left: none; }
        .hud-label {
            font-family: 'Oswald', sans-serif;
            color: var(--dust);
            font-size: clamp(7px, 1vh, 10px);
            letter-spacing: 0.08em;
            text-transform: uppercase;
            opacity: 0.85;
            white-space: nowrap;
        }
        .hud-value {
            color: var(--amber-bright);
            font-size: clamp(11px, 1.7vh, 16px);
            font-weight: bold;
            text-shadow: 0 0 8px rgba(255, 180, 84, 0.5);
            letter-spacing: 0.03em;
            white-space: nowrap;
            transition: color 0.3s ease, text-shadow 0.3s ease;
        }
        .hud-bar-item { gap: 6px; }
        .hud-bar {
            position: relative;
            width: clamp(46px, 8vw, 90px);
            height: clamp(10px, 1.8vh, 15px);
            border: 1px solid var(--sand-mid);
            background: rgba(0, 0, 0, 0.55);
            overflow: hidden;
            box-shadow: inset 0 0 8px rgba(217, 138, 79, 0.12);
        }
        .hud-bar-fill {
            position: absolute; top: 0; bottom: 0; left: 0;
            background: linear-gradient(to right, var(--rust-red), var(--amber));
            transition: width 0.3s ease, background 0.3s ease;
            box-shadow: 0 0 10px rgba(240, 164, 76, 0.5);
        }
        .hud-bar-value {
            position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
            font-size: clamp(7px, 1.1vh, 10px); font-weight: bold;
            color: var(--amber-bright);
            text-shadow: 0 0 4px rgba(0,0,0,0.9);
            white-space: nowrap;
        }
        #gameCanvas {
            flex: 1; min-width: 0; min-height: 0;
            background: #000;
            touch-action: none; display: block;
        }
        #message {
            position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
            font-family: 'Oswald', sans-serif;
            font-size: clamp(20px, 4vh, 32px);
            color: var(--amber-bright);
            text-align: center;
            text-shadow: 0 0 20px rgba(255,180,84,0.6), 2px 0 rgba(217,80,47,0.4), -2px 0 rgba(255,233,184,0.3);
            display: none; z-index: 20;
            background: rgba(20, 11, 8, 0.97);
            padding: clamp(16px, 4vh, 32px);
            border: 2px solid var(--amber);
            box-shadow: 0 0 50px rgba(217, 138, 79, 0.35);
            max-width: 90vw; max-height: 90vh; overflow: auto;
        }
        .title-screen { text-align: center; line-height: 1.5; }
        .controls-hint {
            position: absolute; bottom: clamp(8px, 2vh, 16px); left: 50%;
            transform: translateX(-50%);
            font-size: clamp(10px, 1.4vh, 14px);
            color: var(--amber);
            text-align: center; letter-spacing: 0.05em;
            background: rgba(20, 11, 8, 0.97);
            padding: clamp(6px, 1vh, 10px) clamp(12px, 2vh, 16px);
            border: 1px solid var(--sand-mid);
            opacity: 0.9; max-width: 90vw; white-space: normal; line-height: 1.4;
            z-index: 7;
        }
        #muteStatus { margin-left: 6px; opacity: 0.9; }

        /* ---- atmosphere overlays: dust haze + warm vignette (replaces CRT phosphor look) ---- */
        .dust-grain {
            position: absolute; inset: 0; pointer-events: none; z-index: 5;
            background:
                radial-gradient(1px 1px at 10% 20%, rgba(255,220,170,0.5), transparent),
                radial-gradient(1px 1px at 80% 60%, rgba(255,220,170,0.4), transparent),
                radial-gradient(1px 1px at 40% 80%, rgba(255,220,170,0.35), transparent),
                radial-gradient(1px 1px at 65% 15%, rgba(255,220,170,0.4), transparent),
                radial-gradient(1px 1px at 25% 55%, rgba(255,220,170,0.3), transparent);
            background-size: 300px 300px;
            opacity: 0.5;
            animation: driftDust 22s linear infinite;
            mix-blend-mode: screen;
        }
        @keyframes driftDust {
            0% { background-position: 0 0; }
            100% { background-position: -300px 400px; }
        }
        .warm-vignette {
            position: absolute; inset: 0; pointer-events: none; z-index: 6;
            background: radial-gradient(ellipse at 50% 60%, rgba(0,0,0,0) 45%, rgba(20,8,4,0.7) 100%);
            transition: background 0.2s ease;
        }
        .heat-flicker { animation: flicker 7s infinite; }
        @keyframes flicker {
            0%, 100% { opacity: 1; }
            91% { opacity: 1; }
            92% { opacity: 0.93; }
            93% { opacity: 1; }
            96% { opacity: 0.88; }
            97% { opacity: 1; }
        }
        #gameContainer.critical .warm-vignette {
            animation: critPulse 0.55s ease-in-out infinite;
        }
        @keyframes critPulse {
            0%, 100% { background: radial-gradient(ellipse at 50% 60%, rgba(217,80,47,0) 40%, rgba(217,80,47,0.35) 100%); }
            50% { background: radial-gradient(ellipse at 50% 60%, rgba(217,80,47,0) 50%, rgba(217,80,47,0.6) 100%); }
        }
        #gameContainer.critical .hud-item {
            filter: drop-shadow(0 0 5px rgba(217,80,47,0.7));
        }

        .mobile-warning {
            display: none;
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background: radial-gradient(ellipse at 50% 30%, #3a1c10 0%, #140b08 70%);
            z-index: 1000;
            flex-direction: column; justify-content: center; align-items: center;
            padding: 20px; text-align: center;
            border: 3px solid var(--amber);
            box-shadow: 0 0 60px rgba(217, 138, 79, 0.4) inset;
        }
        .warning-content {
            max-width: 800px; width: 90%; padding: 40px;
            background: rgba(20, 11, 8, 0.96);
            border: 2px solid var(--amber);
            box-shadow: 0 0 40px rgba(217, 138, 79, 0.3);
        }
        .warning-title {
            font-family: 'Oswald', sans-serif;
            font-size: clamp(24px, 5vh, 48px);
            color: var(--amber-bright);
            margin-bottom: 30px;
            text-shadow: 0 0 20px rgba(255,180,84,0.6), 2px 0 rgba(217,80,47,0.5), -2px 0 rgba(255,233,184,0.4);
            letter-spacing: 0.1em; line-height: 1.3;
        }
        .warning-message { font-size: clamp(16px, 3vh, 24px); color: var(--amber); margin-bottom: 40px; line-height: 1.6; opacity: 0.9; }
        .warning-subtitle { font-size: clamp(14px, 2.5vh, 20px); color: var(--dust); margin-top: 40px; opacity: 0.8; border-top: 1px solid var(--sand-mid); padding-top: 20px; }
        .scan-effect {
            position: absolute; top: 0; left: 0; width: 100%; height: 2px;
            background: rgba(255, 180, 84, 0.5); box-shadow: 0 0 10px var(--amber);
            animation: scan 3s linear infinite;
        }
        @keyframes scan { 0% { top: 0; opacity: 1; } 50% { opacity: 0.7; } 100% { top: 100%; opacity: 1; } }
        .grid-overlay-warning {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background-image: linear-gradient(rgba(240, 164, 76, 0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(240, 164, 76, 0.04) 1px, transparent 1px);
            background-size: 30px 30px; pointer-events: none;
        }
        .version-label {
            position: absolute; top: clamp(8px, 2vh, 16px); right: clamp(8px, 2vw, 16px);
            font-size: clamp(8px, 1.2vh, 12px); color: var(--dust); opacity: 0.6; letter-spacing: 0.05em; z-index: 7;
        }
        .grid-overlay {
            position: absolute; top: 0; left: 0; right: 0; bottom: 0;
            background-image: linear-gradient(rgba(240, 164, 76, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(240, 164, 76, 0.03) 1px, transparent 1px);
            background-size: 20px 20px; pointer-events: none; z-index: 1;
        }

        @media (max-width: 480px) {
            #hud { gap: clamp(4px, 1.5vw, 10px); padding: 4px 6px; }
            .hud-item { padding: 0 4px; }
            .hud-bar { width: clamp(38px, 14vw, 60px); }
        }
        @media (max-height: 400px) {
            #hud { padding: 2px 6px; gap: 4px; }
            .hud-label { display: none; }
        }

        body.mobile-active #gameContainer { display: none !important; }
        body.mobile-active .mobile-warning { display: flex !important; }
    </style>
</head>
<body>
    <div class="mobile-warning" id="mobileWarning">
        <div class="grid-overlay-warning"></div>
        <div class="scan-effect"></div>
        <div class="warning-content">
            <div class="warning-title">APACHE//CAN.YON<br>DUSK OPS — VER 5.0GB</div>
            <div class="warning-message">
                <div style="margin-bottom: 20px; color: #d9502f; text-shadow: 0 0 10px #d9502f;">⚠️ DESKTOP KEYBOARD REQUIRED ⚠️</div>
                <div style="margin-bottom: 30px;">This simulation requires physical keyboard controls<br>and cannot be played on mobile devices.</div>
                <div style="font-size: 0.9em; opacity: 0.85;">
                    REQUIRED CONTROLS:<br>
                    [Z]: FIRE WEAPON<br>
                    [X]: LAUNCH MISSILE<br>
                    [←] [→]: NAVIGATE CANYON<br>
                    [SPACE]: START/PAUSE<br>
                    [M]: MUTE AUDIO
                </div>
            </div>
            <div class="warning-subtitle">Please use a desktop or laptop computer<br>with a keyboard to play this game.</div>
        </div>
    </div>

    <div id="gameContainer">
        <div class="grid-overlay"></div>
        <div class="version-label">DUSK OPS · VER 5.0GB</div>

        <div id="hud">
            <div class="hud-item">
                <span class="hud-label">HI-SC</span><span class="hud-value" id="hiscore">000000</span>
            </div>
            <div class="hud-item">
                <span class="hud-label">SCORE</span><span class="hud-value" id="score">000000</span>
            </div>
            <div class="hud-item">
                <span class="hud-label">LIVES</span><span class="hud-value" id="lives">03</span>
            </div>
            <div class="hud-item">
                <span class="hud-label">TIME</span><span class="hud-value" id="time">000</span>
            </div>
            <div class="hud-item">
                <span class="hud-label">MSL</span><span class="hud-value" id="missiles">10</span>
            </div>
            <div class="hud-item">
                <span class="hud-label">SPD</span><span class="hud-value" id="speed">2.5</span>
            </div>
            <div class="hud-item hud-bar-item">
                <span class="hud-label">SHLD</span>
                <div class="hud-bar">
                    <div class="hud-bar-fill" id="shieldsBar"></div>
                    <span class="hud-bar-value" id="shieldsValue">100%</span>
                </div>
            </div>
            <div class="hud-item hud-bar-item">
                <span class="hud-label">FUEL</span>
                <div class="hud-bar">
                    <div class="hud-bar-fill" id="fuelBar"></div>
                    <span class="hud-bar-value" id="fuelValue">100%</span>
                </div>
            </div>
        </div>

        <canvas id="gameCanvas"></canvas>

        <div class="dust-grain heat-flicker"></div>
        <div class="warm-vignette"></div>

        <div id="message"></div>
        <div class="controls-hint">[Z]: FIRE | [X]: MISSILE | [←]: LEFT | [→]: RIGHT | [SPACE]: START/PAUSE<span id="muteStatus">[M]: MUTE (OFF)</span></div>
    </div>

    <script>
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        const body = document.body;
        if (isMobile) body.classList.add('mobile-active');

        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');

        // ===================== Palette =====================
        const PAL = {
            skyTop: '#1e1330',
            skyMid: '#7a2f3d',
            skyHorizon: '#e8763a',
            sunCore: '#ffe9b8',
            sunGlow: '#ffb454',
            sandShadow: '#3a1c10',
            sandDark: '#6b2f1a',
            sandMid: '#9c4a26',
            sandLight: '#d98a4f',
            rim: '#ffcf8a',
            amber: '#f0a44c',
            amberBright: '#ffc36b',
            rust: '#d9502f',
            dust: '#c9a67a'
        };

        // ===================== AUDIO ENGINE (procedural, no external files) =====================
        const AudioEngine = {
            ctx: null, master: null, muted: false, noiseBuffer: null,
            engineOsc: null, engineGain: null,
            init() {
                if (this.ctx) return;
                try { this.ctx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { return; }
                this.master = this.ctx.createGain();
                this.master.gain.value = this.muted ? 0 : 0.35;
                this.master.connect(this.ctx.destination);
                const bufferSize = this.ctx.sampleRate * 1;
                this.noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
                const data = this.noiseBuffer.getChannelData(0);
                for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
            },
            resume() { if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); },
            tone(freq, duration, type = 'square', vol = 0.25, sweepTo = null) {
                if (!this.ctx) return;
                const osc = this.ctx.createOscillator();
                const gain = this.ctx.createGain();
                osc.type = type;
                const t0 = this.ctx.currentTime;
                osc.frequency.setValueAtTime(freq, t0);
                if (sweepTo) osc.frequency.exponentialRampToValueAtTime(Math.max(20, sweepTo), t0 + duration);
                gain.gain.setValueAtTime(vol, t0);
                gain.gain.exponentialRampToValueAtTime(0.001, t0 + duration);
                osc.connect(gain); gain.connect(this.master);
                osc.start(t0); osc.stop(t0 + duration + 0.02);
            },
            noise(duration, vol = 0.3, filterFreq = 1000) {
                if (!this.ctx) return;
                const src = this.ctx.createBufferSource();
                src.buffer = this.noiseBuffer;
                const filter = this.ctx.createBiquadFilter();
                filter.type = 'lowpass'; filter.frequency.value = filterFreq;
                const gain = this.ctx.createGain();
                const t0 = this.ctx.currentTime;
                gain.gain.setValueAtTime(vol, t0);
                gain.gain.exponentialRampToValueAtTime(0.001, t0 + duration);
                src.connect(filter); filter.connect(gain); gain.connect(this.master);
                src.start(t0); src.stop(t0 + duration + 0.02);
            },
            laser() { this.tone(880, 0.08, 'square', 0.14, 440); },
            missile() { this.tone(220, 0.3, 'sawtooth', 0.18, 90); this.noise(0.2, 0.08, 2200); },
            explosion(big = false) {
                this.noise(big ? 0.55 : 0.28, big ? 0.45 : 0.28, big ? 400 : 900);
                this.tone(big ? 80 : 150, big ? 0.5 : 0.25, 'sawtooth', 0.18, 35);
            },
            hit() { this.tone(200, 0.06, 'square', 0.14, 80); },
            powerup() { this.tone(440, 0.08, 'sine', 0.18, 880); this.tone(880, 0.1, 'sine', 0.14, 1320); },
            scrape() { this.noise(0.09, 0.07, 3200); },
            gameover() { this.tone(220, 0.6, 'sawtooth', 0.22, 55); },
            start() { this.tone(440, 0.1, 'square', 0.18, 880); },
            alarmBeep() { this.tone(660, 0.1, 'square', 0.16, 560); },
            setEngine(speedRatio) {
                if (!this.ctx) return;
                if (!this.engineOsc) {
                    this.engineOsc = this.ctx.createOscillator();
                    this.engineOsc.type = 'sawtooth';
                    this.engineGain = this.ctx.createGain();
                    this.engineGain.gain.value = 0;
                    const filter = this.ctx.createBiquadFilter();
                    filter.type = 'lowpass'; filter.frequency.value = 220;
                    this.engineOsc.connect(filter); filter.connect(this.engineGain); this.engineGain.connect(this.master);
                    this.engineOsc.start();
                }
                this.engineOsc.frequency.setValueAtTime(55 + speedRatio * 35, this.ctx.currentTime);
                this.engineGain.gain.setTargetAtTime(0.035, this.ctx.currentTime, 0.15);
            },
            stopEngine() { if (this.engineGain) this.engineGain.gain.setTargetAtTime(0, this.ctx.currentTime, 0.25); },
            toggleMute() {
                this.muted = !this.muted;
                if (this.master) this.master.gain.value = this.muted ? 0 : 0.35;
                return this.muted;
            }
        };

        function resizeCanvas() {
            if (isMobile) return;
            const container = document.getElementById('gameContainer');
            const hud = document.getElementById('hud');
            const containerRect = container.getBoundingClientRect();
            const hudHeight = hud ? hud.getBoundingClientRect().height : 0;

            canvas.width = containerRect.width;
            canvas.height = containerRect.height - hudHeight;
            if (canvas.width < 100) canvas.width = 100;
            if (canvas.height < 100) canvas.height = 100;

            if (game && game.player) {
                game.player.x = Math.max(10, Math.min(canvas.width - game.player.width - 10, game.player.x));
                game.player.y = canvas.height * 0.7;
            }
            if (game && game.canyon && game.canyon.leftWall.length > 0) {
                game.canyon.leftWall = []; game.canyon.rightWall = []; game.canyon.segmentCounter = 0;
                seedCanyonSegments();
            }
            initDust();
            initRidges();
        }

        window.addEventListener('load', () => { if (!isMobile) { setTimeout(resizeCanvas, 100); setTimeout(resizeCanvas, 500); } });
        let resizeTimeout;
        window.addEventListener('resize', () => {
            if (isMobile) return;
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(resizeCanvas, 100);
        });

        let game = {
            state: 'start',
            score: 0, hiscore: 0, fuel: 100, shields: 100, missiles: 10, lives: 3,
            scrollSpeed: 2.5, time: 0, frame: 0, idleFrame: 0,
            canyon: { leftWall: [], rightWall: [], width: 300, segmentCounter: 0 },
            player: { x: 0, y: 0, width: 16, height: 16, speed: 4 },
            bullets: [], playerMissiles: [], enemies: [], powerups: [], forceFields: [],
            particles: [], shockwaves: [], dustMotes: [], ridges: [],
            keys: {},
            enemySpawnTimer: 0, powerupSpawnTimer: 0,
            shake: { intensity: 0 },
            muzzleFlash: 0, playerFlash: 0
        };

        if (!isMobile) {
            function seedCanyonSegments() {
                // Seed from below the bottom of the screen up through the lookahead
                // buffer, so the corridor already covers the whole visible area
                // (including the player's row) on the very first frame.
                const startY = canvas.height + 100;
                const count = Math.ceil((startY + 800) / 4);
                for (let i = 0; i < count; i++) addCanyonSegment(startY - i * 4);
            }

            function initCanyon() {
                game.canyon.leftWall = []; game.canyon.rightWall = []; game.canyon.segmentCounter = 0;
                seedCanyonSegments();
            }

            function addCanyonSegment(y) {
                const centerX = canvas.width / 2;
                const t = game.canyon.segmentCounter;
                const variation = Math.sin(t * 0.0234) * 70 + Math.cos(t * 0.0431) * 55 + Math.sin(t * 0.0627) * 40 + Math.cos(t * 0.0089) * 60 + Math.sin(t * 0.0153) * 35;
                const narrowing = Math.sin(t * 0.0179) * 55 + Math.cos(t * 0.0347) * 45 + Math.sin(t * 0.0521) * 30;
                const leftX = centerX - game.canyon.width / 2 + variation - narrowing;
                const rightX = centerX + game.canyon.width / 2 + variation + narrowing;
                game.canyon.leftWall.push({ x: leftX, y: y });
                game.canyon.rightWall.push({ x: rightX, y: y });
                game.canyon.segmentCounter++;
            }

            function updateCanyon() {
                game.canyon.width = Math.min(300, canvas.width * 0.7);
                game.canyon.leftWall.forEach(p => p.y += game.scrollSpeed);
                game.canyon.rightWall.forEach(p => p.y += game.scrollSpeed);
                while (game.canyon.leftWall[0] && game.canyon.leftWall[0].y > canvas.height + 50) {
                    game.canyon.leftWall.shift(); game.canyon.rightWall.shift();
                }
                while (game.canyon.leftWall.length < canvas.height / 4 + 100) {
                    const lastY = game.canyon.leftWall[game.canyon.leftWall.length - 1].y;
                    addCanyonSegment(lastY - 4);
                }
            }

            // ---------------- Dust motes drifting through the corridor ----------------
            function initDust() {
                game.dustMotes = [];
                const count = Math.floor((canvas.width * canvas.height) / 9000);
                for (let i = 0; i < count; i++) {
                    game.dustMotes.push({
                        x: Math.random() * canvas.width, y: Math.random() * canvas.height,
                        size: Math.random() * 1.6 + 0.5, speed: Math.random() * 0.6 + 0.2,
                        drift: (Math.random() - 0.5) * 0.3,
                        alpha: Math.random() * 0.4 + 0.1, phase: Math.random() * Math.PI * 2
                    });
                }
            }
            function updateDust() {
                game.dustMotes.forEach(s => {
                    s.y += s.speed * (game.scrollSpeed / 2.5);
                    s.x += s.drift;
                    s.phase += 0.03;
                    if (s.y > canvas.height) { s.y = 0; s.x = Math.random() * canvas.width; }
                });
            }
            function drawDust() {
                game.dustMotes.forEach(s => {
                    const tw = 0.5 + Math.sin(s.phase) * 0.5;
                    ctx.fillStyle = `rgba(255,208,150,${(s.alpha * tw).toFixed(3)})`;
                    ctx.fillRect(s.x, s.y, s.size, s.size);
                });
            }

            // ---------------- Distant mountain ridgelines (parallax) ----------------
            function initRidges() {
                game.ridges = [[], []];
                for (let layer = 0; layer < 2; layer++) {
                    let pts = [];
                    let h = canvas.height * (layer === 0 ? 0.42 : 0.5);
                    for (let x = -50; x <= canvas.width + 50; x += 40) {
                        h += (Math.random() - 0.5) * (layer === 0 ? 24 : 34);
                        h = Math.max(canvas.height * 0.25, Math.min(canvas.height * 0.6, h));
                        pts.push({ x, y: h });
                    }
                    game.ridges[layer] = pts;
                }
            }
            function drawRidges() {
                const layers = [
                    { pts: game.ridges[0], color: 'rgba(58, 28, 16, 0.75)' },
                    { pts: game.ridges[1], color: 'rgba(107, 47, 26, 0.6)' }
                ];
                layers.forEach(layer => {
                    if (!layer.pts.length) return;
                    ctx.fillStyle = layer.color;
                    ctx.beginPath();
                    ctx.moveTo(layer.pts[0].x, canvas.height * 0.65);
                    layer.pts.forEach(p => ctx.lineTo(p.x, p.y));
                    ctx.lineTo(layer.pts[layer.pts.length - 1].x, canvas.height * 0.65);
                    ctx.closePath();
                    ctx.fill();
                });
            }

            function drawSky() {
                const grad = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.7);
                grad.addColorStop(0, PAL.skyTop);
                grad.addColorStop(0.45, PAL.skyMid);
                grad.addColorStop(1, PAL.skyHorizon);
                ctx.fillStyle = grad;
                ctx.fillRect(0, 0, canvas.width, canvas.height);

                // sun with soft glow, low on the horizon
                const sunX = canvas.width * 0.5;
                const sunY = canvas.height * 0.44;
                const sunR = Math.min(70, canvas.width * 0.09);
                const glow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunR * 3.2);
                glow.addColorStop(0, 'rgba(255,233,184,0.55)');
                glow.addColorStop(0.4, 'rgba(255,180,84,0.22)');
                glow.addColorStop(1, 'rgba(255,180,84,0)');
                ctx.fillStyle = glow;
                ctx.beginPath(); ctx.arc(sunX, sunY, sunR * 3.2, 0, Math.PI * 2); ctx.fill();

                ctx.save();
                ctx.shadowColor = PAL.sunGlow; ctx.shadowBlur = 25;
                ctx.fillStyle = PAL.sunCore;
                ctx.beginPath(); ctx.arc(sunX, sunY, sunR, 0, Math.PI * 2); ctx.fill();
                ctx.restore();

                // heat-haze shimmer band near horizon
                ctx.save();
                ctx.globalAlpha = 0.25;
                for (let i = 0; i < 4; i++) {
                    const yy = canvas.height * 0.6 + i * 6;
                    const wobble = Math.sin(game.idleFrame * 0.05 + i) * 3;
                    ctx.strokeStyle = 'rgba(255,207,138,0.5)';
                    ctx.beginPath();
                    ctx.moveTo(0, yy + wobble);
                    for (let x = 0; x <= canvas.width; x += 20) {
                        ctx.lineTo(x, yy + Math.sin(x * 0.03 + game.idleFrame * 0.06 + i) * 3);
                    }
                    ctx.stroke();
                }
                ctx.restore();

                drawRidges();
            }

            function drawCanyon() {
                drawSky();
                drawDust();

                // filled rock mass either side of the flight corridor
                if (game.canyon.leftWall.length > 1) {
                    const rockGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
                    rockGrad.addColorStop(0, PAL.sandDark);
                    rockGrad.addColorStop(1, PAL.sandShadow);

                    ctx.fillStyle = rockGrad;
                    ctx.beginPath();
                    ctx.moveTo(0, game.canyon.leftWall[0].y);
                    game.canyon.leftWall.forEach(p => ctx.lineTo(p.x, p.y));
                    ctx.lineTo(0, game.canyon.leftWall[game.canyon.leftWall.length - 1].y);
                    ctx.closePath(); ctx.fill();

                    ctx.beginPath();
                    ctx.moveTo(canvas.width, game.canyon.rightWall[0].y);
                    game.canyon.rightWall.forEach(p => ctx.lineTo(p.x, p.y));
                    ctx.lineTo(canvas.width, game.canyon.rightWall[game.canyon.rightWall.length - 1].y);
                    ctx.closePath(); ctx.fill();

                    // sedimentary strata bands
                    ctx.save();
                    ctx.globalAlpha = 0.35;
                    for (let i = 0; i < game.canyon.leftWall.length; i += 14) {
                        const lp = game.canyon.leftWall[i], rp = game.canyon.rightWall[i];
                        if (!lp || !rp) continue;
                        ctx.strokeStyle = i % 28 === 0 ? PAL.sandLight : PAL.sandMid;
                        ctx.lineWidth = 2;
                        ctx.beginPath(); ctx.moveTo(0, lp.y); ctx.lineTo(lp.x, lp.y); ctx.stroke();
                        ctx.beginPath(); ctx.moveTo(canvas.width, rp.y); ctx.lineTo(rp.x, rp.y); ctx.stroke();
                    }
                    ctx.restore();

                    // sun-facing rim light along corridor edges
                    ctx.save();
                    ctx.shadowColor = PAL.rim; ctx.shadowBlur = 12;
                    ctx.strokeStyle = PAL.rim;
                    ctx.lineWidth = Math.max(1.5, canvas.width / 700);
                    ctx.beginPath();
                    for (let i = 0; i < game.canyon.leftWall.length; i += 2) {
                        const p = game.canyon.leftWall[i];
                        if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y);
                    }
                    ctx.stroke();
                    ctx.beginPath();
                    for (let i = 0; i < game.canyon.rightWall.length; i += 2) {
                        const p = game.canyon.rightWall[i];
                        if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y);
                    }
                    ctx.stroke();
                    ctx.restore();

                    // rock outcrop ticks
                    ctx.strokeStyle = 'rgba(58, 28, 16, 0.6)';
                    ctx.lineWidth = 1;
                    for (let i = 0; i < game.canyon.leftWall.length; i += 10) {
                        const lp = game.canyon.leftWall[i], rp = game.canyon.rightWall[i];
                        if (!lp || !rp) continue;
                        ctx.beginPath(); ctx.moveTo(lp.x, lp.y); ctx.lineTo(lp.x + 10, lp.y + 6); ctx.stroke();
                        ctx.beginPath(); ctx.moveTo(rp.x, rp.y); ctx.lineTo(rp.x - 10, rp.y + 6); ctx.stroke();
                    }
                }

                // ground haze near the bottom of the screen (closer = warmer/hazier)
                const hazeGrad = ctx.createLinearGradient(0, canvas.height * 0.75, 0, canvas.height);
                hazeGrad.addColorStop(0, 'rgba(217,138,79,0)');
                hazeGrad.addColorStop(1, 'rgba(217,138,79,0.08)');
                ctx.fillStyle = hazeGrad;
                ctx.fillRect(0, canvas.height * 0.75, canvas.width, canvas.height * 0.25);
            }

            function drawPlayer() {
                const p = game.player;
                const scale = Math.max(1, canvas.width / 800);
                const x = Math.floor(p.x), y = Math.floor(p.y);

                ctx.save();
                ctx.shadowColor = PAL.rust; ctx.shadowBlur = 6;
                ctx.fillStyle = '#241108';

                // sand kicked up beneath the chopper
                const exhaustPulse = 0.6 + Math.sin(game.frame * 0.4) * 0.15;
                ctx.save();
                ctx.globalAlpha = exhaustPulse * 0.45;
                ctx.fillStyle = PAL.dust;
                ctx.beginPath();
                ctx.ellipse(x + 8 * scale, y + 15 * scale, 5 * scale, 3 * scale, 0, 0, Math.PI * 2);
                ctx.fill();
                ctx.restore();

                // rotor blur
                const rotorPhase = Math.floor(game.frame / 2) % 2;
                ctx.save();
                ctx.globalAlpha = 0.3;
                ctx.fillRect(x - 8 * scale, y + 2 * scale, 32 * scale, 1 * scale);
                ctx.restore();
                if (rotorPhase === 0) ctx.fillRect(x - 6 * scale, y + 2 * scale, 28 * scale, 2 * scale);
                else ctx.fillRect(x + 6 * scale, y, 2 * scale, 6 * scale);

                // dark silhouette body, rim-lit by the sun above
                ctx.fillRect(x + 4 * scale, y + 6 * scale, 8 * scale, 6 * scale);
                ctx.fillRect(x + 2 * scale, y + 8 * scale, 12 * scale, 4 * scale);
                ctx.fillRect(x + 12 * scale, y + 9 * scale, 4 * scale, 2 * scale);
                ctx.fillRect(x + 15 * scale, y + 7 * scale, 2 * scale, 6 * scale);
                ctx.fillRect(x + 2 * scale, y + 12 * scale, 2 * scale, 2 * scale);
                ctx.fillRect(x + 10 * scale, y + 12 * scale, 2 * scale, 2 * scale);

                // amber rim light along the top edge
                ctx.strokeStyle = PAL.rim; ctx.lineWidth = 1;
                ctx.strokeRect(x + 2 * scale, y + 8 * scale, 12 * scale, 4 * scale);

                // running light + cockpit glint
                ctx.fillStyle = PAL.amberBright;
                ctx.fillRect(x + 5 * scale, y + 7 * scale, 2 * scale, 1.5 * scale);
                ctx.restore();

                if (game.muzzleFlash > 0) {
                    ctx.save();
                    ctx.shadowColor = '#fff5e0'; ctx.shadowBlur = 16;
                    ctx.fillStyle = `rgba(255,230,180,${game.muzzleFlash / 4})`;
                    ctx.beginPath();
                    ctx.arc(x + p.width / 2, y - 2, 5 * scale * (game.muzzleFlash / 4), 0, Math.PI * 2);
                    ctx.fill();
                    ctx.restore();
                }

                if (game.playerFlash > 0) {
                    ctx.save();
                    ctx.strokeStyle = `rgba(217,80,47,${game.playerFlash / 6})`;
                    ctx.lineWidth = 3;
                    ctx.shadowColor = PAL.rust; ctx.shadowBlur = 12;
                    ctx.beginPath();
                    ctx.arc(x + p.width / 2, y + p.height / 2, 16 * scale, 0, Math.PI * 2);
                    ctx.stroke();
                    ctx.restore();
                }
            }

            function spawnEnemy() {
                const wallIndex = Math.min(60, Math.floor(game.canyon.leftWall.length * 0.3));
                if (wallIndex >= game.canyon.leftWall.length) return;
                const leftBound = game.canyon.leftWall[wallIndex].x + 15;
                const rightBound = game.canyon.rightWall[wallIndex].x - 30;
                const types = ['drone', 'uav', 'mine'];
                const type = types[Math.floor(Math.random() * types.length)];
                game.enemies.push({
                    x: leftBound + Math.random() * (rightBound - leftBound), y: -20,
                    vx: (Math.random() - 0.5) * 1.5, vy: type === 'mine' ? 0.5 : 1.5,
                    width: 16, height: 16, type: type,
                    health: type === 'mine' ? 1 : (type === 'uav' ? 3 : 2),
                    shootTimer: 60 + Math.random() * 60, flash: 0
                });
            }

            function spawnPowerup(type) {
                const wallIndex = Math.min(60, Math.floor(game.canyon.leftWall.length * 0.3));
                if (wallIndex >= game.canyon.leftWall.length) return;
                const leftBound = game.canyon.leftWall[wallIndex].x + 15;
                const rightBound = game.canyon.rightWall[wallIndex].x - 30;
                game.powerups.push({ x: leftBound + Math.random() * (rightBound - leftBound), y: -20, width: 12, height: 12, type: type, vy: 1.5, rotation: 0 });
            }

            function spawnForceField() {
                const wallIndex = Math.min(60, Math.floor(game.canyon.leftWall.length * 0.3));
                if (wallIndex >= game.canyon.leftWall.length) return;
                const leftBound = game.canyon.leftWall[wallIndex].x + 15;
                const rightBound = game.canyon.rightWall[wallIndex].x - 50;
                game.forceFields.push({ x: leftBound + Math.random() * (rightBound - leftBound), y: -30, width: Math.min(40, canvas.width * 0.1), height: 80, health: 6, vy: 1, pulse: 0, flash: 0 });
            }

            function createExplosion(x, y, rgb = { r: 255, g: 140, b: 40 }, count = 12) {
                for (let i = 0; i < count; i++) {
                    const angle = (i * Math.PI * 2) / count;
                    const spd = 2 + Math.random() * 2.5;
                    game.particles.push({
                        x: x, y: y, vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd,
                        life: 28, maxLife: 28, size: 2 + Math.random() * 2.5,
                        r: rgb.r, g: rgb.g, b: rgb.b
                    });
                }
                game.shockwaves.push({ x, y, radius: 2, maxRadius: count > 15 ? 65 : 38, life: 20 });
            }

            function triggerShake(intensity) { game.shake.intensity = Math.max(game.shake.intensity, intensity); }

            function update() {
                if (game.state !== 'playing') return;
                game.frame++;
                if (game.frame % 60 === 0) game.time++;

                updateCanyon();
                updateDust();

                if (game.shake.intensity > 0.1) game.shake.intensity *= 0.88; else game.shake.intensity = 0;
                if (game.muzzleFlash > 0) game.muzzleFlash--;
                if (game.playerFlash > 0) game.playerFlash--;

                AudioEngine.setEngine(game.scrollSpeed / 4.5);

                const p = game.player;
                if (game.keys[','] || game.keys['<'] || game.keys['arrowleft']) p.x -= p.speed;
                if (game.keys['.'] || game.keys['>'] || game.keys['arrowright']) p.x += p.speed;

                if (game.frame % 2 === 0) {
                    game.particles.push({
                        x: p.x + p.width / 2 + (Math.random() - 0.5) * 6, y: p.y + p.height,
                        vx: (Math.random() - 0.5) * 0.5, vy: 1 + Math.random(),
                        life: 16, maxLife: 16, size: 1.5 + Math.random(), r: 201, g: 166, b: 122
                    });
                }

                let playerCenterY = 0, minDist = Infinity;
                for (let i = 0; i < game.canyon.leftWall.length; i++) {
                    const dist = Math.abs(game.canyon.leftWall[i].y - game.player.y);
                    if (dist < minDist) { minDist = dist; playerCenterY = i; }
                }

                if (playerCenterY < game.canyon.leftWall.length) {
                    const leftBound = game.canyon.leftWall[playerCenterY].x + 10;
                    const rightBound = game.canyon.rightWall[playerCenterY].x - p.width - 10;
                    p.x = Math.max(leftBound, Math.min(rightBound, p.x));

                    if (p.x <= leftBound + 2 || p.x >= rightBound - 2) {
                        game.shields -= 0.5;
                        if (game.frame % 8 === 0) {
                            AudioEngine.scrape();
                            const sparkX = p.x <= leftBound + 2 ? p.x : p.x + p.width;
                            game.particles.push({ x: sparkX, y: p.y + p.height / 2, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, life: 10, maxLife: 10, size: 2, r: 255, g: 210, b: 130 });
                        }
                        if (game.shields <= 0) loseLife();
                    }
                }

                if (game.keys['z'] && game.frame % 6 === 0) {
                    game.bullets.push({ x: p.x + p.width / 2 - 1, y: p.y, vy: -8, width: 2, height: 8 });
                    game.muzzleFlash = 4;
                    AudioEngine.laser();
                }

                if (game.keys['x'] && !game.keys['x_fired'] && game.missiles > 0) {
                    game.missiles--;
                    let target = game.enemies[0];
                    game.playerMissiles.push({ x: p.x + p.width / 2 - 2, y: p.y, vx: 0, vy: -6, target: target, width: 4, height: 12 });
                    game.keys['x_fired'] = true;
                    AudioEngine.missile();
                    triggerShake(3);
                }
                if (!game.keys['x']) game.keys['x_fired'] = false;

                game.bullets = game.bullets.filter(b => { b.y += b.vy; return b.y > -10; });

                game.playerMissiles = game.playerMissiles.filter(m => {
                    if (m.target && game.enemies.includes(m.target)) {
                        const dx = m.target.x - m.x, dy = m.target.y - m.y;
                        const dist = Math.sqrt(dx * dx + dy * dy);
                        if (dist > 0) { m.vx += (dx / dist) * 0.5; m.vy += (dy / dist) * 0.5; }
                    }
                    m.x += m.vx; m.y += m.vy;
                    const speed = Math.sqrt(m.vx * m.vx + m.vy * m.vy);
                    if (speed > 8) { m.vx = (m.vx / speed) * 8; m.vy = (m.vy / speed) * 8; }
                    return m.y > -10 && m.y < canvas.height;
                });

                game.enemySpawnTimer++;
                if (game.enemySpawnTimer > 80 && game.enemies.length < 6) {
                    spawnEnemy(); game.enemySpawnTimer = 0;
                    if (Math.random() < 0.15) spawnForceField();
                }

                game.powerupSpawnTimer++;
                if (game.powerupSpawnTimer > 240) {
                    const rand = Math.random();
                    if (rand < 0.4) spawnPowerup('fuel'); else if (rand < 0.7) spawnPowerup('shield'); else spawnPowerup('missile');
                    game.powerupSpawnTimer = 0;
                }

                game.enemies = game.enemies.filter(e => {
                    if (e.flash > 0) e.flash--;
                    e.x += e.vx; e.y += e.vy;

                    for (let i = game.bullets.length - 1; i >= 0; i--) {
                        const b = game.bullets[i];
                        if (b.x >= e.x && b.x <= e.x + e.width && b.y >= e.y && b.y <= e.y + e.height) {
                            e.health--; e.flash = 4;
                            game.bullets.splice(i, 1);
                            AudioEngine.hit();
                            if (e.health <= 0) {
                                createExplosion(e.x + e.width / 2, e.y + e.height / 2, { r: 255, g: 140, b: 40 }, e.type === 'uav' ? 18 : 12);
                                AudioEngine.explosion(e.type === 'uav');
                                triggerShake(e.type === 'uav' ? 6 : 4);
                                game.score += e.type === 'uav' ? 300 : (e.type === 'mine' ? 100 : 200);
                                if (game.score > game.hiscore) game.hiscore = game.score;
                                return false;
                            }
                            break;
                        }
                    }

                    for (let i = game.playerMissiles.length - 1; i >= 0; i--) {
                        const m = game.playerMissiles[i];
                        if (m.x >= e.x && m.x <= e.x + e.width && m.y >= e.y && m.y <= e.y + e.height) {
                            createExplosion(e.x + e.width / 2, e.y + e.height / 2, { r: 255, g: 140, b: 40 }, e.type === 'uav' ? 18 : 12);
                            AudioEngine.explosion(e.type === 'uav');
                            triggerShake(e.type === 'uav' ? 7 : 5);
                            game.playerMissiles.splice(i, 1);
                            game.score += e.type === 'uav' ? 300 : (e.type === 'mine' ? 100 : 200);
                            if (game.score > game.hiscore) game.hiscore = game.score;
                            return false;
                        }
                    }

                    if (e.x < p.x + p.width && e.x + e.width > p.x && e.y < p.y + p.height && e.y + e.height > p.y) {
                        game.shields -= 25; game.playerFlash = 6;
                        createExplosion(e.x + e.width / 2, e.y + e.height / 2, { r: 217, g: 80, b: 47 }, 14);
                        AudioEngine.explosion(false); triggerShake(10);
                        if (game.shields <= 0) loseLife();
                        return false;
                    }
                    return e.y < canvas.height + 20;
                });

                game.powerups = game.powerups.filter(pw => {
                    pw.y += pw.vy; pw.rotation += 0.1;
                    if (pw.x < p.x + p.width && pw.x + pw.width > p.x && pw.y < p.y + p.height && pw.y + pw.height > p.y) {
                        if (pw.type === 'fuel') game.fuel = Math.min(100, game.fuel + 30);
                        else if (pw.type === 'shield') game.shields = Math.min(100, game.shields + 40);
                        else if (pw.type === 'missile') game.missiles = Math.min(99, game.missiles + 5);
                        AudioEngine.powerup();
                        createExplosion(pw.x + pw.width / 2, pw.y + pw.height / 2, { r: 255, g: 195, b: 107 }, 8);
                        return false;
                    }
                    return pw.y < canvas.height;
                });

                game.forceFields.forEach(f => { f.y += f.vy; f.pulse = (f.pulse + 0.1) % (Math.PI * 2); if (f.flash > 0) f.flash--; });

                game.forceFields = game.forceFields.filter(f => {
                    for (let i = game.bullets.length - 1; i >= 0; i--) {
                        const b = game.bullets[i];
                        if (b.x >= f.x && b.x <= f.x + f.width && b.y >= f.y && b.y <= f.y + f.height) {
                            f.health--; f.flash = 4;
                            game.bullets.splice(i, 1);
                            AudioEngine.hit();
                            if (f.health <= 0) {
                                createExplosion(f.x + f.width / 2, f.y + f.height / 2, { r: 255, g: 140, b: 40 }, 22);
                                AudioEngine.explosion(true); triggerShake(8);
                                game.score += 400;
                                if (game.score > game.hiscore) game.hiscore = game.score;
                                return false;
                            }
                            break;
                        }
                    }
                    for (let i = game.playerMissiles.length - 1; i >= 0; i--) {
                        const m = game.playerMissiles[i];
                        if (m.x >= f.x && m.x <= f.x + f.width && m.y >= f.y && m.y <= f.y + f.height) {
                            f.health -= 2; f.flash = 4;
                            game.playerMissiles.splice(i, 1);
                            createExplosion(f.x + f.width / 2, f.y + f.height / 2, { r: 255, g: 140, b: 40 }, 10);
                            AudioEngine.hit();
                            if (f.health <= 0) {
                                AudioEngine.explosion(true); triggerShake(8);
                                game.score += 400;
                                if (game.score > game.hiscore) game.hiscore = game.score;
                                return false;
                            }
                            break;
                        }
                    }
                    if (f.x < p.x + p.width && f.x + f.width > p.x && f.y < p.y + p.height && f.y + f.height > p.y) {
                        game.shields -= 30; game.playerFlash = 6;
                        createExplosion(p.x + p.width / 2, p.y + p.height / 2, { r: 217, g: 80, b: 47 }, 16);
                        AudioEngine.explosion(false); triggerShake(12);
                        if (game.shields <= 0) loseLife();
                        return false;
                    }
                    return f.y < canvas.height;
                });

                game.particles = game.particles.filter(pt => {
                    pt.x += pt.vx; pt.y += pt.vy; pt.life--; pt.size *= 0.96;
                    return pt.life > 0 && pt.size > 0.4;
                });

                game.shockwaves = game.shockwaves.filter(s => { s.radius += (s.maxRadius - s.radius) * 0.25; s.life--; return s.life > 0; });

                game.fuel -= 0.03;
                if (game.fuel <= 0) loseLife();

                if ((game.fuel < 25 || game.shields < 25) && game.frame % 40 === 0) AudioEngine.alarmBeep();
                if (game.frame % 900 === 0) game.scrollSpeed = Math.min(4.5, game.scrollSpeed + 0.2);

                updateHUD();
            }

            function drawEnemies() {
                game.enemies.forEach(e => {
                    const x = Math.floor(e.x), y = Math.floor(e.y);
                    const flashing = e.flash > 0;
                    ctx.save();
                    ctx.shadowColor = flashing ? '#fff5e0' : PAL.rust;
                    ctx.shadowBlur = flashing ? 14 : 6;
                    ctx.strokeStyle = flashing ? '#fff5e0' : PAL.rust;
                    ctx.lineWidth = 1;
                    ctx.fillStyle = flashing ? 'rgba(255,245,224,0.5)' : 'rgba(30,15,8,0.85)';

                    if (e.type === 'drone') {
                        ctx.fillRect(x, y, e.width, e.height);
                        ctx.strokeRect(x, y, e.width, e.height);
                        ctx.beginPath();
                        ctx.moveTo(x, y); ctx.lineTo(x + e.width, y + e.height);
                        ctx.moveTo(x + e.width, y); ctx.lineTo(x, y + e.height);
                        ctx.stroke();
                    } else if (e.type === 'uav') {
                        ctx.beginPath();
                        for (let i = 0; i < 6; i++) {
                            const angle = (i * Math.PI) / 3;
                            const px = x + e.width / 2 + Math.cos(angle) * e.width / 2;
                            const py = y + e.height / 2 + Math.sin(angle) * e.height / 2;
                            if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
                        }
                        ctx.closePath(); ctx.fill(); ctx.stroke();
                    } else {
                        ctx.beginPath();
                        ctx.arc(x + e.width / 2, y + e.height / 2, e.width / 2, 0, Math.PI * 2);
                        ctx.fill(); ctx.stroke();
                    }
                    // glowing targeting eye
                    ctx.fillStyle = flashing ? '#ffffff' : PAL.amberBright;
                    ctx.beginPath();
                    ctx.arc(x + e.width / 2, y + e.height / 2, 2, 0, Math.PI * 2);
                    ctx.fill();
                    ctx.restore();

                    const healthRatio = e.health / (e.type === 'uav' ? 3 : (e.type === 'mine' ? 1 : 2));
                    ctx.fillStyle = healthRatio > 0.5 ? PAL.amberBright : healthRatio > 0.2 ? '#ffcf3a' : PAL.rust;
                    ctx.fillRect(x, y - 4, e.width * healthRatio, 2);
                });
            }

            function drawPowerups() {
                game.powerups.forEach(pw => {
                    const x = Math.floor(pw.x), y = Math.floor(pw.y);
                    ctx.save();
                    ctx.translate(x + pw.width / 2, y + pw.height / 2);
                    ctx.rotate(pw.rotation);
                    ctx.shadowColor = PAL.amberBright; ctx.shadowBlur = 8;
                    ctx.strokeStyle = PAL.amberBright;
                    ctx.lineWidth = 1;
                    ctx.fillStyle = 'rgba(240, 164, 76, 0.25)';
                    ctx.beginPath();
                    ctx.moveTo(0, -pw.height / 2); ctx.lineTo(pw.width / 2, 0);
                    ctx.lineTo(0, pw.height / 2); ctx.lineTo(-pw.width / 2, 0);
                    ctx.closePath(); ctx.fill(); ctx.stroke();
                    ctx.shadowBlur = 0;
                    ctx.fillStyle = PAL.amberBright;
                    ctx.font = '8px "Share Tech Mono", monospace';
                    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
                    ctx.fillText(pw.type === 'fuel' ? 'F' : pw.type === 'shield' ? 'S' : 'M', 0, 0);
                    ctx.restore();
                });
            }

            function drawGlitchTitle(text, x, y, size) {
                ctx.save();
                ctx.font = `bold ${size}px 'Oswald', sans-serif`;
                ctx.textAlign = 'center';
                ctx.globalCompositeOperation = 'lighter';
                ctx.fillStyle = 'rgba(217,80,47,0.5)';
                ctx.fillText(text, x - 2, y);
                ctx.fillStyle = 'rgba(255,233,184,0.5)';
                ctx.fillText(text, x + 2, y);
                ctx.globalCompositeOperation = 'source-over';
                ctx.shadowColor = PAL.sunGlow; ctx.shadowBlur = 18;
                ctx.fillStyle = PAL.amberBright;
                ctx.fillText(text, x, y);
                ctx.shadowBlur = 0;
                ctx.restore();
            }

            function draw() {
                if (canvas.width <= 0 || canvas.height <= 0) { resizeCanvas(); return; }
                game.idleFrame++;

                ctx.setTransform(1, 0, 0, 1, 0, 0);
                ctx.fillStyle = '#000';
                ctx.fillRect(0, 0, canvas.width, canvas.height);

                ctx.save();
                const sh = game.shake.intensity;
                if (sh > 0.1) ctx.translate((Math.random() - 0.5) * sh, (Math.random() - 0.5) * sh);

                drawCanyon();

                // clip everything below to the flight corridor so nothing is
                // visible beyond the canyon walls (occluded by the rock)
                ctx.save();
                if (game.canyon.leftWall.length > 1) {
                    ctx.beginPath();
                    ctx.moveTo(game.canyon.leftWall[0].x, game.canyon.leftWall[0].y);
                    for (let i = 1; i < game.canyon.leftWall.length; i++) {
                        ctx.lineTo(game.canyon.leftWall[i].x, game.canyon.leftWall[i].y);
                    }
                    for (let i = game.canyon.rightWall.length - 1; i >= 0; i--) {
                        ctx.lineTo(game.canyon.rightWall[i].x, game.canyon.rightWall[i].y);
                    }
                    ctx.closePath();
                    ctx.clip();
                }

                game.forceFields.forEach(f => {
                    const x = Math.floor(f.x), y = Math.floor(f.y);
                    ctx.strokeStyle = f.flash > 0 ? '#fff5e0' : PAL.amber;
                    ctx.lineWidth = 2;
                    ctx.globalAlpha = 0.3 + Math.sin(f.pulse) * 0.2;
                    for (let i = 0; i < f.width; i += 8) {
                        const offset = Math.sin(f.pulse + i * 0.2) * 4;
                        ctx.beginPath(); ctx.moveTo(x + i, y); ctx.lineTo(x + i + offset, y + f.height); ctx.stroke();
                    }
                    ctx.globalAlpha = 1;
                    ctx.strokeRect(x, y, f.width, f.height);
                    for (let i = 0; i < 3; i++) {
                        for (let j = 0; j < 6; j++) {
                            const sparkX = x + (i + 1) * f.width / 4;
                            const sparkY = y + (j + 1) * f.height / 7;
                            const pulse = Math.sin(f.pulse + i + j) * 2;
                            ctx.fillStyle = PAL.amberBright;
                            ctx.fillRect(sparkX + pulse, sparkY + pulse, 2, 2);
                        }
                    }
                });

                drawPowerups();
                drawEnemies();

                game.bullets.forEach(b => {
                    ctx.save();
                    ctx.shadowColor = PAL.amberBright; ctx.shadowBlur = 8;
                    ctx.fillStyle = '#fff5e0';
                    ctx.fillRect(Math.floor(b.x), Math.floor(b.y), b.width, b.height);
                    ctx.restore();
                });

                game.playerMissiles.forEach(m => {
                    ctx.save();
                    ctx.shadowColor = PAL.rust; ctx.shadowBlur = 10;
                    ctx.fillStyle = PAL.amberBright;
                    ctx.fillRect(Math.floor(m.x), Math.floor(m.y), m.width, m.height);
                    ctx.restore();
                    for (let i = 0; i < 3; i++) {
                        ctx.fillStyle = `rgba(217, 80, 47, ${0.5 - i * 0.15})`;
                        ctx.fillRect(Math.floor(m.x), Math.floor(m.y) + m.height + i * 3, m.width, 2);
                    }
                });

                game.particles.forEach(pt => {
                    ctx.fillStyle = `rgba(${pt.r},${pt.g},${pt.b},${(pt.life / pt.maxLife).toFixed(3)})`;
                    ctx.fillRect(Math.floor(pt.x), Math.floor(pt.y), pt.size, pt.size);
                });

                game.shockwaves.forEach(s => {
                    ctx.strokeStyle = `rgba(255,180,84,${((s.life / 20) * 0.6).toFixed(3)})`;
                    ctx.lineWidth = 2;
                    ctx.beginPath(); ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2); ctx.stroke();
                });

                drawPlayer();
                ctx.restore(); // end corridor clip

                ctx.restore(); // end shake transform

                if (game.state === 'start') {
                    ctx.fillStyle = 'rgba(20, 11, 8, 0.94)';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    drawGlitchTitle('APACHE//CAN.YON', canvas.width / 2, canvas.height / 2 - 60, Math.min(48, canvas.width / 15));
                    ctx.fillStyle = PAL.amber;
                    ctx.textAlign = 'center';
                    ctx.font = `${Math.min(24, canvas.width / 25)}px 'Share Tech Mono', monospace`;
                    ctx.fillText('DUSK OPS — SYSTEM BOOT...', canvas.width / 2, canvas.height / 2 - 10);
                    ctx.fillText('VER 5.0GB', canvas.width / 2, canvas.height / 2 + 20);
                    ctx.font = `${Math.min(16, canvas.width / 35)}px 'Share Tech Mono', monospace`;
                    ctx.fillText('INITIATE SEQUENCE [SPACE]', canvas.width / 2, canvas.height / 2 + 60);

                    const loadWidth = Math.min(300, canvas.width * 0.7);
                    const loadProgress = (game.idleFrame % 120) / 120;
                    ctx.strokeStyle = PAL.amber;
                    ctx.strokeRect(canvas.width / 2 - loadWidth / 2, canvas.height / 2 + 90, loadWidth, 4);
                    ctx.shadowColor = PAL.amberBright; ctx.shadowBlur = 8;
                    ctx.fillStyle = PAL.amberBright;
                    ctx.fillRect(canvas.width / 2 - loadWidth / 2, canvas.height / 2 + 90, loadWidth * loadProgress, 4);
                    ctx.shadowBlur = 0;
                }
            }

            function updateHUD() {
                document.getElementById('score').textContent = game.score.toString().padStart(6, '0');
                document.getElementById('hiscore').textContent = game.hiscore.toString().padStart(6, '0');
                document.getElementById('lives').textContent = game.lives.toString().padStart(2, '0');
                document.getElementById('missiles').textContent = game.missiles.toString().padStart(2, '0');
                document.getElementById('time').textContent = game.time.toString().padStart(3, '0');
                document.getElementById('speed').textContent = game.scrollSpeed.toFixed(1);

                const fuelBar = document.getElementById('fuelBar');
                const shieldsBar = document.getElementById('shieldsBar');
                const fuelValue = document.getElementById('fuelValue');
                const shieldsValue = document.getElementById('shieldsValue');

                fuelBar.style.width = game.fuel + '%';
                shieldsBar.style.width = game.shields + '%';
                fuelValue.textContent = Math.floor(game.fuel) + '%';
                shieldsValue.textContent = Math.floor(game.shields) + '%';

                if (game.fuel < 30) { fuelBar.style.background = 'linear-gradient(to right, #6b2f1a, #d9502f)'; fuelValue.style.color = '#ffb454'; }
                else { fuelBar.style.background = 'linear-gradient(to right, #d9502f, #f0a44c)'; fuelValue.style.color = '#ffc36b'; }

                if (game.shields < 30) { shieldsBar.style.background = 'linear-gradient(to right, #6b2f1a, #d9502f)'; shieldsValue.style.color = '#ffb454'; }
                else { shieldsBar.style.background = 'linear-gradient(to right, #d9502f, #f0a44c)'; shieldsValue.style.color = '#ffc36b'; }

                const gameContainer = document.getElementById('gameContainer');
                if (game.state === 'playing' && (game.fuel < 25 || game.shields < 25)) gameContainer.classList.add('critical');
                else gameContainer.classList.remove('critical');
            }

            function loseLife() {
                game.lives--;
                triggerShake(15);
                AudioEngine.explosion(true);
                if (game.lives <= 0) {
                    gameOver();
                } else {
                    game.shields = 100; game.fuel = 100;
                    game.player.x = canvas.width / 2 - game.player.width / 2;
                    game.enemies = []; game.bullets = []; game.playerMissiles = [];
                }
            }

            function gameOver() {
                game.state = 'gameover';
                AudioEngine.gameover();
                AudioEngine.stopEngine();
                const msg = document.getElementById('message');
                msg.innerHTML = `<div class="title-screen">
                    <div style="font-size: clamp(20px, 32px, 4vh); margin-bottom: 20px;">SYSTEM FAILURE</div>
                    <div>MISSION TERMINATED</div>
                    <div style="margin: 20px 0; font-size: clamp(16px, 24px, 3vh);">SCORE: ${game.score.toString().padStart(6, '0')}</div>
                    <div>TIME: ${game.time.toString().padStart(3, '0')} SECONDS</div>
                    <div style="margin-top: 30px; font-size: clamp(12px, 16px, 2vh);">[SPACE] TO REBOOT</div>
                </div>`;
                msg.style.display = 'block';
            }

            function restart() {
                game.score = 0; game.fuel = 100; game.shields = 100; game.missiles = 10; game.lives = 3;
                game.scrollSpeed = 2.5; game.time = 0; game.frame = 0;
                game.bullets = []; game.playerMissiles = []; game.enemies = []; game.powerups = [];
                game.forceFields = []; game.particles = []; game.shockwaves = [];
                game.enemySpawnTimer = 0; game.powerupSpawnTimer = 0;
                game.shake.intensity = 0; game.muzzleFlash = 0; game.playerFlash = 0;
                game.player.x = canvas.width / 2 - game.player.width / 2;
                game.player.y = canvas.height * 0.7;

                initCanyon();
                document.getElementById('gameContainer').classList.remove('critical');
                document.getElementById('message').style.display = 'none';
                game.state = 'playing';
            }

            function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); }

            document.addEventListener('keydown', e => {
                game.keys[e.key.toLowerCase()] = true;

                if (e.key.toLowerCase() === 'm') {
                    const muted = AudioEngine.toggleMute();
                    document.getElementById('muteStatus').textContent = `[M]: MUTE (${muted ? 'ON' : 'OFF'})`;
                }

                if (e.key === ' ') {
                    e.preventDefault();
                    AudioEngine.init();
                    AudioEngine.resume();
                    if (game.state === 'start' || game.state === 'gameover') {
                        AudioEngine.start();
                        restart();
                    } else if (game.state === 'playing') {
                        game.state = 'paused';
                        AudioEngine.stopEngine();
                        document.getElementById('message').innerHTML = `<div class="title-screen">
                            <div style="font-size: clamp(20px, 32px, 4vh); margin-bottom: 20px;">SYSTEM PAUSED</div>
                            <div>[SPACE] TO RESUME</div>
                        </div>`;
                        document.getElementById('message').style.display = 'block';
                    } else if (game.state === 'paused') {
                        game.state = 'playing';
                        document.getElementById('message').style.display = 'none';
                    }
                }

                if (e.key === 'Escape' && game.state === 'playing') {
                    game.state = 'paused';
                    AudioEngine.stopEngine();
                    document.getElementById('message').innerHTML = `<div class="title-screen">
                        <div style="font-size: clamp(20px, 32px, 4vh); margin-bottom: 20px;">SYSTEM PAUSED</div>
                        <div>[SPACE] TO RESUME</div>
                    </div>`;
                    document.getElementById('message').style.display = 'block';
                }
            });

            document.addEventListener('keyup', e => { game.keys[e.key.toLowerCase()] = false; });

            window.addEventListener('load', () => {
                setTimeout(() => {
                    game.player.x = canvas.width / 2 - game.player.width / 2;
                    game.player.y = canvas.height * 0.7;
                    initCanyon();
                    initDust();
                    initRidges();
                    updateHUD();
                    gameLoop();
                }, 500);
            });
        }

        document.addEventListener('contextmenu', e => e.preventDefault());
    </script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.