← Race the Sun results

Race the Sun

Qwen 3.6 27b MTP · typescript

Initial view of Race the Sun

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 66,663 bytes · SHA-256 87fca0cd26cb
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Race The Sun — Endless Runner</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a12; font-family: 'Segoe UI', system-ui, sans-serif; }
#game-container { position: relative; width: 100vw; height: 100vh; }
#game-canvas { display: block; width: 100%; height: 100%; }
.hidden { display: none !important; }

/* HUD */
#hud { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; }
#hud-top-left, #hud-top-right { position: absolute; top: 16px; color: #fff; font-size: 14px; text-shadow: 0 0 8px rgba(255,140,0,.6); letter-spacing: .05em; }
#hud-top-left { left: 20px; }
#hud-top-right { right: 20px; text-align: right; }
#score-display { font-size: 22px; font-weight: 700; color: #ffd68a; }
#distance-display { margin-top: 4px; opacity: .75; }
#multiplier-display { font-size: 28px; font-weight: 900; color: #ffaa33; text-shadow: 0 0 12px rgba(255,170,51,.8); }
#speed-display { margin-top: 4px; opacity: .75; }

/* Energy bar */
#energy-bar-container { position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%); width: 320px; height: 18px; border-radius: 9px; overflow: hidden; background: rgba(0,0,0,.5); border: 1px solid rgba(255,140,0,.3); }
#energy-bar-bg { position: absolute; inset: 0; background: linear-gradient(90deg, #ff3300, #ff8800, #ffcc00); opacity: .25; border-radius: 9px; }
#energy-bar-fill { height: 100%; width: 100%; background: linear-gradient(90deg, #ff4400, #ffaa00, #ffee44); border-radius: 9px; transition: width .15s ease-out; }
#energy-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 10px; font-weight: 700; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.8); letter-spacing: .1em; }

/* Title screen */
#title-screen { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: radial-gradient(ellipse at 50% 60%, rgba(255,100,0,.15) 0%, transparent 70%), #0a0a12; z-index: 10; }
#title-screen h1 { font-size: clamp(36px, 8vw, 80px); font-weight: 900; color: #ffcc44; text-shadow: 0 0 40px rgba(255,140,0,.5), 0 0 80px rgba(255,80,0,.3); letter-spacing: .15em; margin-bottom: 8px; }
.subtitle { color: #cc9966; font-size: clamp(12px, 2vw, 18px); margin-bottom: 40px; opacity: .8; }
.controls-info { display: flex; gap: 32px; margin-bottom: 40px; color: #aa8855; font-size: 13px; letter-spacing: .05em; }
#start-btn { pointer-events: all; padding: 14px 48px; font-size: 18px; font-weight: 700; color: #fff; background: linear-gradient(135deg, #ff6600, #cc3300); border: none; border-radius: 6px; cursor: pointer; letter-spacing: .1em; text-transform: uppercase; box-shadow: 0 4px 20px rgba(255,80,0,.4); transition: transform .1s, box-shadow .1s; }
#start-btn:hover { transform: scale(1.05); box-shadow: 0 6px 30px rgba(255,80,0,.6); }

/* Game over */
#gameover-screen { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: radial-gradient(ellipse at 50% 40%, rgba(255,30,0,.1) 0%, transparent 60%), rgba(10,10,18,.92); z-index: 10; }
#gameover-screen h1 { font-size: clamp(32px, 7vw, 72px); font-weight: 900; color: #ff4422; text-shadow: 0 0 40px rgba(255,40,20,.5); letter-spacing: .1em; margin-bottom: 32px; }
#final-score { font-size: 28px; color: #ffd68a; font-weight: 700; }
#final-distance { font-size: 16px; color: #cc9966; margin-top: 4px; }
#high-score-display { font-size: 14px; color: #aa8855; margin-top: 12px; }
.restart-hint { margin-top: 36px; color: #776644; font-size: 14px; letter-spacing: .05em; animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100%{ opacity:.5; } 50%{ opacity:1; } }
/* ── css: hud-extras ── */
#sun-warning {
  position: absolute;
  top: 80px;
  left: 20px;
  color: #ff6633;
  font-size: 14px;
  font-weight: 700;
  text-shadow: 0 0 10px rgba(255,80,30,.6);
  letter-spacing: .08em;
  animation: sunPulse 1s ease-in-out infinite;
}
@keyframes sunPulse {
  0%, 100% { opacity: 0.7; }
  50% { opacity: 1; }
}

/* Low energy pulse on bar */
#energy-bar-container.low-energy {
  animation: energyPulse 0.8s ease-in-out infinite;
  border-color: rgba(255,40,0,.6);
}
@keyframes energyPulse {
  0%, 100% { box-shadow: 0 0 5px rgba(255,40,0,.3); }
  50% { box-shadow: 0 0 15px rgba(255,40,0,.6); }
}

/* Boost active indicator */
#energy-bar-container.boosting #energy-label::after {
  content: ' BOOST';
  color: #ffaa33;
}
</style>
</head>
<body>
<div id="game-container">
  <canvas id="game-canvas"></canvas>
  <div id="hud" class="hidden">
    <div id="hud-top-left">
      <div id="score-display">SCORE: 0</div>
      <div id="distance-display">DISTANCE: 0m</div>
    </div><div id="sun-warning" class="hidden">⚠ SUN SETTING — FIND ENERGY!</div>
    <div id="hud-top-right">
      <div id="multiplier-display">×1.0</div>
      <div id="speed-display">SPEED: 0</div>
    </div>
    <div id="energy-bar-container">
      <div id="energy-bar-bg"></div>
      <div id="energy-bar-fill"></div>
      <span id="energy-label">⚡ ENERGY</span>
    </div>
  </div>
  <div id="title-screen">
    <h1>RACE THE SUN</h1>
    <p class="subtitle">Pilot through the geometric void. Stay in the light.</p>
    <div class="controls-info">
      <span>WASD / Arrows — Move</span>
      <span>Space — Boost</span>
      <span>R — Restart</span>
    </div>
    <button id="start-btn">LAUNCH</button>
  </div>
  <div id="gameover-screen" class="hidden">
    <h1>SIGNAL LOST</h1>
    <div id="final-score"></div>
    <div id="final-distance"></div>
    <div id="high-score-display"></div>
    <p class="restart-hint">Press R or click to restart</p>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./input":"src/input.ts","./player":"src/player.ts","./world":"src/world.ts","./particles":"src/particles.ts","./audio":"src/audio.ts","./render":"src/render.ts","./constants":"src/constants.ts"},"src/state.ts":{"./constants":"src/constants.ts"},"src/player.ts":{"./state":"src/state.ts","./input":"src/input.ts","./constants":"src/constants.ts"},"src/world.ts":{"./state":"src/state.ts","./player":"src/player.ts","./constants":"src/constants.ts"},"src/particles.ts":{"./state":"src/state.ts","./constants":"src/constants.ts"},"src/audio.ts":{"./state":"src/state.ts","./constants":"src/constants.ts"},"src/render.ts":{"./state":"src/state.ts","./constants":"src/constants.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// === MAIN — GAME LOOP & ENTRY POINT ===
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const input_1 = require("./input");
const player_1 = require("./player");
const world_1 = require("./world");
const particles_1 = require("./particles");
const audio_1 = require("./audio");
const render_1 = require("./render");
const constants_1 = require("./constants");
// === DOM REFERENCES ===
const titleScreen = document.getElementById('title-screen');
const gameoverScreen = document.getElementById('gameover-screen');
const hudEl = document.getElementById('hud');
const startBtn = document.getElementById('start-btn');
const finalScoreEl = document.getElementById('final-score');
const finalDistanceEl = document.getElementById('final-distance');
const highScoreDisplay = document.getElementById('high-score-display');
// === GAME STATE ===
let g = (0, state_1.resetGame)();
let lastBoostState = false;
let crashParticlesSpawned = false;
let collectedPickupIds = new Set();
let prevNearMissFlash = 0; // Track near-miss flash for audio feedback
// === INITIALIZATION ===
function init() {
    (0, render_1.initCanvas)();
    (0, input_1.initInput)();
    (0, audio_1.initAudio)();
    startBtn.addEventListener('click', () => {
        (0, audio_1.resumeAudio)();
        launchGame();
    });
    gameoverScreen.addEventListener('click', () => {
        if (g.state === 'gameover') {
            (0, audio_1.resumeAudio)();
            resetAndPlay();
        }
    });
    showTitleScreen();
    requestAnimationFrame(gameLoop);
}
function launchGame() {
    g = (0, state_1.resetGame)();
    g.state = 'playing';
    (0, world_1.initWorld)(g.runSeed);
    collectedPickupIds.clear();
    crashParticlesSpawned = false;
    lastBoostState = false;
    prevNearMissFlash = 0;
    titleScreen.classList.add('hidden');
    gameoverScreen.classList.add('hidden');
    hudEl.classList.remove('hidden');
    (0, audio_1.startAmbient)();
}
function resetAndPlay() {
    launchGame();
}
// === SCREEN MANAGEMENT ===
function showTitleScreen() {
    titleScreen.classList.remove('hidden');
    gameoverScreen.classList.add('hidden');
    hudEl.classList.add('hidden');
}
function showGameOver() {
    const hs = (0, player_1.getHighScore)();
    finalScoreEl.textContent = `SCORE: ${Math.floor(g.score).toLocaleString()}`;
    finalDistanceEl.textContent = `DISTANCE: ${Math.floor(g.distance / 10)}m`;
    highScoreDisplay.textContent = `HIGH SCORE: ${hs.toLocaleString()}`;
    gameoverScreen.classList.remove('hidden');
    hudEl.classList.add('hidden');
}
// === GAME LOOP ===
let lastTime = 0;
function gameLoop(timestamp) {
    const dt = Math.min(0.05, (timestamp - lastTime) / 1000);
    lastTime = timestamp;
    if (g.state === 'playing') {
        update(dt);
    }
    else if (g.state === 'gameover') {
        // Still render particles and effects after game over
        (0, particles_1.updateParticles)(g, dt);
        (0, world_1.updateEffects)(g, dt);
    }
    (0, render_1.render)(g);
    requestAnimationFrame(gameLoop);
}
// === UPDATE ===
function update(dt) {
    const input = (0, input_1.readInput)();
    // Update player physics & movement
    (0, player_1.updatePlayer)(g, input, dt);
    // Sun sinks over time
    g.sunY += constants_1.SUN_SINK_RATE * dt;
    if (g.sunY >= constants_1.SUN_SET_THRESHOLD) {
        (0, player_1.triggerGameOver)(g);
    }
    // Update world objects
    (0, world_1.updateWorld)(g, dt);
    // Check collisions
    (0, world_1.checkCollisions)(g);
    // Spawn thrust particles
    (0, particles_1.spawnThrustParticles)(g);
    // Ambient atmospheric particles
    (0, particles_1.spawnAmbientParticles)(g);
    // Detect newly collected pickups for particle effects
    for (const p of g.pickups) {
        if (p.collected && !collectedPickupIds.has(p.z + p.x * 1000)) {
            collectedPickupIds.add(p.z + p.x * 1000);
            const color = p.type === 'energy' ? constants_1.COLORS.energyCell :
                p.type === 'score' ? constants_1.COLORS.scoreToken : constants_1.COLORS.chainOrb;
            (0, particles_1.spawnPickupParticles)(g, p.x, p.y, p.z, color);
            (0, audio_1.playPickupSound)(p.type);
        }
    }
    // Boost state change detection
    if (g.isBoosting !== lastBoostState) {
        (0, audio_1.playBoostSound)(g.isBoosting);
        lastBoostState = g.isBoosting;
    }
    // Update particles
    (0, particles_1.updateParticles)(g, dt);
    // Near-miss audio feedback (detect when nearMissFlash goes up)
    if (g.nearMissFlash > prevNearMissFlash && g.nearMissFlash > 0.1) {
        (0, audio_1.playNearMissSound)();
    }
    prevNearMissFlash = g.nearMissFlash;
    // Update effects (shake, flash)
    (0, world_1.updateEffects)(g, dt);
    // Update ambient audio
    (0, audio_1.updateAmbient)(g);
    // Handle game over transition
    if (g.state === 'gameover' && !crashParticlesSpawned) {
        crashParticlesSpawned = true;
        (0, particles_1.spawnCrashParticles)(g);
        (0, audio_1.playCrashSound)();
        (0, world_1.addShake)(g, constants_1.COLLISION_SHAKE);
        showGameOver();
    }
    // Restart from game over / title
    if ((g.state === 'gameover' || g.state === 'title') && (0, input_1.consumeRestart)()) {
        resetAndPlay();
    }
}
// === START ===
init();
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// === TUNABLE CONSTANTS ===
Object.defineProperty(exports, "__esModule", { value: true });
exports.NEAR_MISS_BONUS = exports.DISTANCE_SCORE_MULT = exports.COLORS = exports.AUDIO_ENABLED = exports.CRASH_PARTICLES = exports.PICKUP_PARTICLES = exports.THRUST_PARTICLES = exports.NEARMISS_SHAKE = exports.COLLISION_SHAKE = exports.NEAR_MISS_SCORE = exports.NEAR_MISS_DIST = exports.SUN_SET_THRESHOLD = exports.SUN_SINK_RATE = exports.MAX_ENERGY = exports.BOOST_DRAIN_MULT = exports.SHADOW_DRAIN_MULT = exports.SUN_ENERGY_DRAIN = exports.LANE_SPACING = exports.LANE_COUNT = exports.SPAWN_INTERVAL_MIN = exports.SPAWN_INTERVAL_BASE = exports.WORLD_DEPTH = exports.BOOST_MULTIPLIER = exports.SPEED_RAMP = exports.MAX_SPEED = exports.BASE_SPEED = exports.PITCH_LEAN = exports.BANK_ANGLE_MAX = exports.INERTIA_TAIL = exports.EASING_RATE = exports.MOVE_SPEED = exports.PLAYER_HEIGHT = exports.PLAYER_WIDTH = exports.CANVAS_H = exports.CANVAS_W = void 0;
exports.CANVAS_W = 1280;
exports.CANVAS_H = 720;
// Player movement
exports.PLAYER_WIDTH = 36;
exports.PLAYER_HEIGHT = 24;
exports.MOVE_SPEED = 520; // lateral/vertical speed (px/s)
exports.EASING_RATE = 12; // input easing (higher = snappier, ~80ms response)
exports.INERTIA_TAIL = 0.92; // inertia carryover per frame
exports.BANK_ANGLE_MAX = 0.35; // max roll angle in radians when strafing
exports.PITCH_LEAN = 0.15; // forward lean on pitch
// Speed & difficulty
exports.BASE_SPEED = 300; // starting forward speed (world units/s)
exports.MAX_SPEED = 900; // max forward speed
exports.SPEED_RAMP = 8; // speed increase per second
exports.BOOST_MULTIPLIER = 1.6; // boost speed multiplier
// World generation
exports.WORLD_DEPTH = 2400; // how far ahead we generate
exports.SPAWN_INTERVAL_BASE = 1.2; // seconds between obstacle waves at start
exports.SPAWN_INTERVAL_MIN = 0.35; // minimum spawn interval
exports.LANE_COUNT = 5; // number of lateral lanes
exports.LANE_SPACING = 140; // px between lane centers
// Sun & energy
exports.SUN_ENERGY_DRAIN = 2.8; // base drain per second
exports.SHADOW_DRAIN_MULT = 3.5; // multiplier when in shadow
exports.BOOST_DRAIN_MULT = 2.0; // extra drain while boosting
exports.MAX_ENERGY = 100;
exports.SUN_SINK_RATE = 0.012; // sun position drops per second (0=top, 1=bottom) — ~33s to set from start
exports.SUN_SET_THRESHOLD = 0.75; // game over when sun below this
// Collision
exports.NEAR_MISS_DIST = 60; // distance for near-miss bonus
exports.NEAR_MISS_SCORE = 50; // score bonus per near miss
exports.COLLISION_SHAKE = 12; // screen shake magnitude on collision
exports.NEARMISS_SHAKE = 4; // screen shake on near miss
// Particles
exports.THRUST_PARTICLES = 3; // particles per frame from thrust
exports.PICKUP_PARTICLES = 15; // particles on pickup
exports.CRASH_PARTICLES = 60; // particles on crash
// Audio
exports.AUDIO_ENABLED = true;
// Colors (sunset palette)
exports.COLORS = {
    skyTop: '#0a0a2e',
    skyMid: '#1a0a3e',
    skyBot: '#4a1a2e',
    sunCore: '#fff8e0',
    sunGlow: '#ffaa33',
    sunOuter: '#ff6600',
    groundFar: '#1a0a20',
    groundNear: '#2a1530',
    obstacleLight: '#cc4400',
    obstacleShadow: '#882200',
    obstacleEdge: '#ffaa44',
    playerBody: '#ddeeff',
    playerAccent: '#44aaff',
    playerGlow: '#66ccff',
    energyCell: '#ffee44',
    scoreToken: '#88ddff',
    chainOrb: '#ff88cc',
    speedLine: 'rgba(255,200,100,',
};
// Scoring
exports.DISTANCE_SCORE_MULT = 1;
exports.NEAR_MISS_BONUS = 50;
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// === GAME STATE ===
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInitialState = createInitialState;
exports.resetGame = resetGame;
const constants_1 = require("./constants");
function createInitialState() {
    return {
        state: 'title',
        playerX: 0,
        playerY: 80,
        targetPlayerX: 0,
        targetPlayerY: 80,
        velocityX: 0,
        velocityY: 0,
        bankAngle: 0,
        pitchLean: 0,
        distance: 0,
        speed: constants_1.BASE_SPEED,
        isBoosting: false,
        boostEnergy: constants_1.MAX_ENERGY,
        sunY: 0.35,
        score: 0,
        multiplier: 1,
        chainCount: 0,
        chainTimer: 0,
        shakeX: 0,
        shakeY: 0,
        shakeDecay: 0,
        flashAlpha: 0,
        nearMissFlash: 0,
        obstacles: [],
        pickups: [],
        particles: [],
        spawnTimer: 2,
        lastTime: 0,
        runSeed: Math.floor(Math.random() * 999999),
    };
}
function resetGame() {
    return createInitialState();
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// === INPUT HANDLING ===
Object.defineProperty(exports, "__esModule", { value: true });
exports.initInput = initInput;
exports.readInput = readInput;
exports.consumeRestart = consumeRestart;
const keys = new Set();
let lastRestart = false;
function initInput() {
    window.addEventListener('keydown', (e) => {
        const k = e.key.toLowerCase();
        if (!keys.has(k)) {
            if (k === 'r')
                lastRestart = true;
        }
        keys.add(k);
        // Prevent scrolling for game keys
        if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright', ' '].includes(e.key)) {
            e.preventDefault();
        }
    });
    window.addEventListener('keyup', (e) => {
        const k = e.key.toLowerCase();
        keys.delete(k);
    });
}
function readInput() {
    return {
        left: keys.has('a') || keys.has('arrowleft'),
        right: keys.has('d') || keys.has('arrowright'),
        up: keys.has('w') || keys.has('arrowup'),
        down: keys.has('s') || keys.has('arrowdown'),
        boost: keys.has(' ') || keys.has('space'),
        restart: lastRestart,
    };
}
function consumeRestart() {
    const r = lastRestart;
    lastRestart = false;
    return r;
}
};

// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
// === PLAYER PHYSICS & MOVEMENT ===
Object.defineProperty(exports, "__esModule", { value: true });
exports.updatePlayer = updatePlayer;
exports.triggerGameOver = triggerGameOver;
exports.startNewGame = startNewGame;
exports.getHighScore = getHighScore;
const input_1 = require("./input");
const constants_1 = require("./constants");
const HALF_WIDTH = (constants_1.LANE_COUNT * constants_1.LANE_SPACING) / 2;
const MIN_Y = 0;
const MAX_Y = 350;
function updatePlayer(g, input, dt) {
    // --- Forward speed ramp ---
    const effectiveSpeed = g.isBoosting ? g.speed * constants_1.BOOST_MULTIPLIER : g.speed;
    if (g.state === 'playing') {
        g.speed = Math.min(constants_1.MAX_SPEED, g.speed + constants_1.SPEED_RAMP * dt);
    }
    // --- Lateral movement with snappy easing + inertia tail ---
    let targetVX = 0;
    if (input.left)
        targetVX -= constants_1.MOVE_SPEED;
    if (input.right)
        targetVX += constants_1.MOVE_SPEED;
    // Smooth acceleration toward target velocity
    const accel = constants_1.EASING_RATE * dt;
    g.velocityX += (targetVX - g.velocityX) * Math.min(1, accel);
    // Apply inertia tail when no input
    if (!input.left && !input.right) {
        g.velocityX *= constants_1.INERTIA_TAIL;
    }
    // --- Vertical movement ---
    let targetVY = 0;
    if (input.up)
        targetVY -= constants_1.MOVE_SPEED * 0.85;
    if (input.down)
        targetVY += constants_1.MOVE_SPEED * 0.85;
    g.velocityY += (targetVY - g.velocityY) * Math.min(1, accel);
    if (!input.up && !input.down) {
        g.velocityY *= constants_1.INERTIA_TAIL;
    }
    // --- Apply velocity to position ---
    g.playerX += g.velocityX * dt;
    g.playerY += g.velocityY * dt;
    // Clamp lateral bounds
    g.playerX = Math.max(-HALF_WIDTH, Math.min(HALF_WIDTH, g.playerX));
    // Clamp vertical bounds with soft bounce
    if (g.playerY < MIN_Y) {
        g.playerY = MIN_Y;
        g.velocityY = Math.abs(g.velocityY) * 0.3;
    }
    if (g.playerY > MAX_Y) {
        g.playerY = MAX_Y;
        g.velocityY = -Math.abs(g.velocityY) * 0.3;
    }
    // --- Bank angle (roll when strafing) ---
    const targetBank = Math.sign(targetVX || g.velocityX) * constants_1.BANK_ANGLE_MAX * Math.min(1, Math.abs(g.velocityX) / constants_1.MOVE_SPEED);
    g.bankAngle += (targetBank - g.bankAngle) * Math.min(1, accel * 1.5);
    // --- Pitch lean on vertical movement ---
    const targetPitch = Math.sign(targetVY || g.velocityY) * constants_1.PITCH_LEAN * Math.min(1, Math.abs(g.velocityY) / constants_1.MOVE_SPEED);
    g.pitchLean += (targetPitch - g.pitchLean) * Math.min(1, accel * 1.5);
    // --- Distance & score ---
    g.distance += effectiveSpeed * dt;
    if (g.state === 'playing') {
        g.score += effectiveSpeed * dt * g.multiplier * 0.01;
    }
    // --- Boost energy ---
    const drainRate = constants_1.SUN_ENERGY_DRAIN + (constants_1.BOOST_DRAIN_MULT * Number(g.isBoosting));
    g.boostEnergy -= drainRate * dt;
    if (g.boostEnergy <= 0) {
        g.boostEnergy = 0;
        g.state = 'gameover';
        triggerGameOver(g);
    }
    // --- Boost toggle ---
    const wasBoosting = g.isBoosting;
    g.isBoosting = input.boost && g.boostEnergy > 5;
    if (g.isBoosting !== wasBoosting) {
        // Toggle feedback handled in audio/render
    }
    // --- Chain timer decay ---
    if (g.chainTimer > 0) {
        g.chainTimer -= dt;
        if (g.chainTimer <= 0) {
            g.chainCount = 0;
            g.multiplier = Math.max(1, g.multiplier - g.chainCount * 0.5);
        }
    }
    // --- Restart check ---
    if ((g.state === 'gameover' || g.state === 'title') && (0, input_1.consumeRestart)()) {
        startNewGame(g);
    }
}
function triggerGameOver(g) {
    g.flashAlpha = 0.8;
    // Save high score
    const prev = getHighScore();
    if (g.score > prev) {
        localStorage.setItem('rts-highscore', String(Math.floor(g.score)));
    }
}
function startNewGame(g) {
    g.state = 'playing';
    g.playerX = 0;
    g.playerY = 80;
    g.velocityX = 0;
    g.velocityY = 0;
    g.bankAngle = 0;
    g.pitchLean = 0;
    g.distance = 0;
    g.speed = constants_1.BASE_SPEED;
    g.isBoosting = false;
    g.boostEnergy = constants_1.MAX_ENERGY;
    g.sunY = 0.35;
    g.score = 0;
    g.multiplier = 1;
    g.chainCount = 0;
    g.chainTimer = 0;
    g.shakeX = 0;
    g.shakeY = 0;
    g.shakeDecay = 0;
    g.flashAlpha = 0;
    g.nearMissFlash = 0;
    g.obstacles = [];
    g.pickups = [];
    g.particles = [];
    g.spawnTimer = 1.5;
    g.runSeed = Math.floor(Math.random() * 999999);
}
function getHighScore() {
    try {
        return Number(localStorage.getItem('rts-highscore')) || 0;
    }
    catch {
        return 0;
    }
}
};

// ── module: src/world.ts ──
__mods["src/world.ts"] = function (exports, require, module) {
"use strict";
// === PROCEDURAL WORLD GENERATION ===
Object.defineProperty(exports, "__esModule", { value: true });
exports.initWorld = initWorld;
exports.spawnObstacles = spawnObstacles;
exports.updateWorld = updateWorld;
exports.checkCollisions = checkCollisions;
exports.addShake = addShake;
exports.updateEffects = updateEffects;
const player_1 = require("./player");
const constants_1 = require("./constants");
// Simple seeded PRNG (mulberry32)
function mulberry32(seed) {
    return function () {
        seed |= 0;
        seed = seed + 0x6D2B79F5 | 0;
        let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
        t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
        return ((t ^ t >>> 14) >>> 0) / 4294967296;
    };
}
let rng = null;
function initWorld(seed) {
    rng = mulberry32(seed);
}
function rand() { return rng ? rng() : Math.random(); }
// === SPAWN OBSTACLES ===
function spawnObstacles(g, dt) {
    if (!rng)
        initWorld(g.runSeed);
    g.spawnTimer -= dt;
    if (g.spawnTimer > 0)
        return;
    // Spawn interval decreases with speed
    const progress = Math.min(1, (g.speed - 300) / 600);
    const interval = constants_1.SPAWN_INTERVAL_BASE - progress * (constants_1.SPAWN_INTERVAL_BASE - constants_1.SPAWN_INTERVAL_MIN);
    g.spawnTimer = interval * (0.7 + rand() * 0.6);
    // Spawn at fixed distance ahead of player (relative z)
    const spawnZ = constants_1.WORLD_DEPTH;
    const halfWidth = (constants_1.LANE_COUNT * constants_1.LANE_SPACING) / 2;
    const patternRoll = rand();
    if (patternRoll < 0.4) {
        // Single pillar in a random lane
        const lane = Math.floor(rand() * constants_1.LANE_COUNT);
        const x = -halfWidth + lane * constants_1.LANE_SPACING;
        g.obstacles.push({
            x, z: spawnZ, w: 50 + rand() * 30, h: 80 + rand() * 120,
            type: 'pillar', passed: false, nearMissed: false,
        });
    }
    else if (patternRoll < 0.7) {
        // Wall with gap — block some lanes, leave others open
        const gapLane = Math.floor(rand() * constants_1.LANE_COUNT);
        for (let i = 0; i < constants_1.LANE_COUNT; i++) {
            if (i === gapLane || i === gapLane + 1)
                continue;
            const x = -halfWidth + i * constants_1.LANE_SPACING;
            g.obstacles.push({
                x, z: spawnZ, w: constants_1.LANE_SPACING * 0.85, h: 60 + rand() * 80,
                type: 'wall', passed: false, nearMissed: false,
            });
        }
    }
    else {
        // Arch — tall obstacles on sides with space in middle (or vice versa)
        const midGap = rand() > 0.5;
        for (let i = 0; i < constants_1.LANE_COUNT; i++) {
            if (midGap && i >= 1 && i <= 3)
                continue;
            if (!midGap && (i === 0 || i === constants_1.LANE_COUNT - 1))
                continue;
            const x = -halfWidth + i * constants_1.LANE_SPACING;
            g.obstacles.push({
                x, z: spawnZ, w: constants_1.LANE_SPACING * 0.85, h: 40 + rand() * 60,
                type: 'arch', passed: false, nearMissed: false,
            });
        }
    }
    // Spawn pickups alongside obstacles (sometimes)
    if (rand() < 0.7) {
        spawnPickup(g, spawnZ + rand() * 200);
    }
}
function spawnPickup(g, z) {
    const halfWidth = (constants_1.LANE_COUNT * constants_1.LANE_SPACING) / 2;
    const lane = Math.floor(rand() * constants_1.LANE_COUNT);
    const x = -halfWidth + lane * constants_1.LANE_SPACING + (rand() - 0.5) * 40;
    const roll = rand();
    let type;
    if (roll < 0.35)
        type = 'energy';
    else if (roll < 0.7)
        type = 'score';
    else
        type = 'chain';
    g.pickups.push({
        x, z, y: 40 + rand() * 120,
        type, collected: false, bobPhase: rand() * Math.PI * 2,
    });
}
// === UPDATE WORLD OBJECTS (move toward player using RELATIVE Z) ===
function updateWorld(g, dt) {
    const effectiveSpeed = g.isBoosting ? g.speed * constants_1.BOOST_MULTIPLIER : g.speed;
    const moveAmount = effectiveSpeed * dt;
    // Move obstacles toward player (relative z decreases)
    for (const obs of g.obstacles) {
        obs.z -= moveAmount;
    }
    // Move pickups toward player
    for (const p of g.pickups) {
        p.z -= moveAmount;
        p.bobPhase += dt * 3;
    }
    // Remove objects that passed behind the camera
    g.obstacles = g.obstacles.filter(o => o.z > -200);
    g.pickups = g.pickups.filter(p => p.z > -100 && !p.collected);
    // Spawn new obstacles
    spawnObstacles(g, dt);
}
// === COLLISION DETECTION (using relative Z) ===
function checkCollisions(g) {
    const pw = 28; // player collision half-width
    const ph = 16; // player collision half-height
    for (const obs of g.obstacles) {
        if (obs.passed || obs.nearMissed && obs.z < -30)
            continue;
        // Relative Z distance from player (z=0 means at player position)
        const relZ = obs.z;
        // Only check obstacles near the player plane
        if (relZ > 60 || relZ < -40)
            continue;
        // AABB overlap in X-Z plane
        const dx = Math.abs(g.playerX - obs.x);
        const halfObsW = obs.w / 2 + pw;
        const halfObsD = 30 + pw;
        if (dx < halfObsW && Math.abs(relZ) < halfObsD) {
            // Check vertical overlap
            const playerBottom = g.playerY - ph;
            const playerTop = g.playerY + ph;
            const obsBottom = 0;
            const obsTop = obs.h;
            if (playerBottom < obsTop && playerTop > obsBottom) {
                // COLLISION!
                (0, player_1.triggerGameOver)(g);
                return;
            }
        }
        // Near-miss detection
        if (!obs.nearMissed && Math.abs(relZ) < constants_1.NEAR_MISS_DIST * 0.5) {
            const nearDx = Math.abs(g.playerX - obs.x);
            if (nearDx > pw * 1.2 && nearDx < halfObsW + 20) {
                obs.nearMissed = true;
                g.score += constants_1.NEAR_MISS_SCORE * g.multiplier;
                g.nearMissFlash = 0.4;
                addShake(g, constants_1.NEARMISS_SHAKE);
            }
        }
        // Mark as passed when behind player
        if (relZ < -50 && !obs.passed) {
            obs.passed = true;
        }
    }
    // Pickup collection
    for (const p of g.pickups) {
        if (p.collected)
            continue;
        const dx = Math.abs(g.playerX - p.x);
        const dy = Math.abs(g.playerY - p.y);
        const dz = Math.abs(p.z);
        if (dx < 40 && dy < 50 && dz < 40) {
            p.collected = true;
            collectPickup(g, p);
        }
    }
}
function collectPickup(g, p) {
    switch (p.type) {
        case 'energy':
            g.boostEnergy = Math.min(constants_1.MAX_ENERGY, g.boostEnergy + 20);
            break;
        case 'score':
            g.score += 100 * g.multiplier;
            break;
        case 'chain':
            g.chainCount++;
            g.chainTimer = 4;
            g.multiplier = Math.min(8, 1 + g.chainCount * 0.5);
            break;
    }
}
// === SCREEN SHAKE ===
function addShake(g, amount) {
    g.shakeDecay = Math.max(g.shakeDecay, amount);
}
function updateEffects(g, dt) {
    // Screen shake decay
    if (g.shakeDecay > 0.1) {
        const angle = Math.random() * Math.PI * 2;
        g.shakeX = Math.cos(angle) * g.shakeDecay;
        g.shakeY = Math.sin(angle) * g.shakeDecay;
        g.shakeDecay *= 0.85;
    }
    else {
        g.shakeX = 0;
        g.shakeY = 0;
        g.shakeDecay = 0;
    }
    // Flash decay
    if (g.flashAlpha > 0)
        g.flashAlpha *= 0.92;
    if (g.nearMissFlash > 0)
        g.nearMissFlash -= dt * 3;
}
};

// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
// === PARTICLE SYSTEM ===
Object.defineProperty(exports, "__esModule", { value: true });
exports.spawnThrustParticles = spawnThrustParticles;
exports.spawnAmbientParticles = spawnAmbientParticles;
exports.spawnPickupParticles = spawnPickupParticles;
exports.spawnCrashParticles = spawnCrashParticles;
exports.updateParticles = updateParticles;
const constants_1 = require("./constants");
function spawnThrustParticles(g) {
    const count = g.isBoosting ? constants_1.THRUST_PARTICLES * 3 : constants_1.THRUST_PARTICLES;
    for (let i = 0; i < count; i++) {
        // Particles use relative Z (positive = ahead of player)
        g.particles.push({
            x: g.playerX + (Math.random() - 0.5) * 12,
            y: g.playerY - 8 + Math.random() * 4,
            z: 15 + Math.random() * 10, // Behind player in relative coords
            vx: (Math.random() - 0.5) * 60,
            vy: -30 - Math.random() * 40,
            vz: -(g.speed * 0.3 + Math.random() * 80),
            life: 0.4 + Math.random() * 0.3,
            maxLife: 0.7,
            size: g.isBoosting ? 4 + Math.random() * 4 : 2 + Math.random() * 3,
            color: g.isBoosting ? constants_1.COLORS.sunGlow : constants_1.COLORS.playerAccent,
        });
    }
}
// Ambient floating particles for atmosphere
function spawnAmbientParticles(g) {
    if (g.state !== 'playing')
        return;
    const halfWidth = ((constants_1.LANE_COUNT || 5) * (constants_1.LANE_SPACING || 140)) / 2;
    // Spawn a few ambient particles per frame
    for (let i = 0; i < 2; i++) {
        g.particles.push({
            x: (Math.random() - 0.5) * halfWidth * 2,
            y: Math.random() * 300 + 20,
            z: constants_1.WORLD_DEPTH || 2400,
            vx: (Math.random() - 0.5) * 10,
            vy: (Math.random() - 0.5) * 5,
            vz: 0,
            life: 3 + Math.random() * 2,
            maxLife: 5,
            size: 1 + Math.random() * 2,
            color: `rgba(255,${140 + Math.floor(Math.random() * 60)},${80 + Math.floor(Math.random() * 40)},0.3)`,
        });
    }
}
function spawnPickupParticles(g, x, y, z, color) {
    for (let i = 0; i < constants_1.PICKUP_PARTICLES; i++) {
        const angle = Math.random() * Math.PI * 2;
        const speed = 80 + Math.random() * 160;
        g.particles.push({
            x, y, z: Math.max(5, z), // Ensure positive relative Z
            vx: Math.cos(angle) * speed,
            vy: Math.sin(angle) * speed - 40,
            vz: (Math.random() - 0.5) * 60,
            life: 0.3 + Math.random() * 0.4,
            maxLife: 0.7,
            size: 2 + Math.random() * 4,
            color,
        });
    }
}
function spawnCrashParticles(g) {
    for (let i = 0; i < constants_1.CRASH_PARTICLES; i++) {
        const angle = Math.random() * Math.PI * 2;
        const speed = 100 + Math.random() * 350;
        g.particles.push({
            x: g.playerX, y: g.playerY, z: 20 + Math.random() * 40,
            vx: Math.cos(angle) * speed,
            vy: Math.sin(angle) * speed - 60,
            vz: (Math.random() - 0.5) * 120,
            life: 0.5 + Math.random() * 1.0,
            maxLife: 1.5,
            size: 3 + Math.random() * 8,
            color: ['#ff4400', '#ffaa00', '#ffee44', '#ffffff'][Math.floor(Math.random() * 4)],
        });
    }
}
function updateParticles(g, dt) {
    const effectiveSpeed = g.isBoosting ? g.speed * constants_1.BOOST_MULTIPLIER : g.speed;
    for (const p of g.particles) {
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        // Particles move with world flow + their own velocity
        p.z -= effectiveSpeed * dt + p.vz * dt;
        p.life -= dt;
    }
    g.particles = g.particles.filter(p => p.life > 0);
}
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
// === PROCEDURAL AUDIO (WebAudio) ===
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAudio = initAudio;
exports.resumeAudio = resumeAudio;
exports.startAmbient = startAmbient;
exports.updateAmbient = updateAmbient;
exports.playPickupSound = playPickupSound;
exports.playCrashSound = playCrashSound;
exports.playNearMissSound = playNearMissSound;
exports.playBoostSound = playBoostSound;
const constants_1 = require("./constants");
let audioCtx = null;
let masterGain = null;
let droneOsc = null;
let droneGain = null;
let windNoise = null;
let windGain = null;
function initAudio() {
    if (!constants_1.AUDIO_ENABLED)
        return;
    try {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        masterGain = audioCtx.createGain();
        masterGain.gain.value = 0.35;
        masterGain.connect(audioCtx.destination);
    }
    catch { /* Audio not available */ }
}
function resumeAudio() {
    if (audioCtx && audioCtx.state === 'suspended') {
        audioCtx.resume();
    }
}
// Start ambient drone + wind
function startAmbient() {
    if (!audioCtx || !masterGain)
        return;
    // Low drone oscillator
    droneOsc = audioCtx.createOscillator();
    droneGain = audioCtx.createGain();
    droneOsc.type = 'sawtooth';
    droneOsc.frequency.value = 55;
    droneGain.gain.value = 0.06;
    // Low-pass filter for warmth
    const lp = audioCtx.createBiquadFilter();
    lp.type = 'lowpass';
    lp.frequency.value = 200;
    lp.Q.value = 2;
    droneOsc.connect(lp);
    lp.connect(droneGain);
    droneGain.connect(masterGain);
    droneOsc.start();
    // Wind noise (filtered white noise)
    const bufferSize = audioCtx.sampleRate * 2;
    const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
        data[i] = Math.random() * 2 - 1;
    }
    windNoise = audioCtx.createBufferSource();
    windGain = audioCtx.createGain();
    const windFilter = audioCtx.createBiquadFilter();
    windFilter.type = 'bandpass';
    windFilter.frequency.value = 400;
    windFilter.Q.value = 0.5;
    windNoise.buffer = buffer;
    windNoise.loop = true;
    windGain.gain.value = 0.03;
    windNoise.connect(windFilter);
    windFilter.connect(windGain);
    windGain.connect(masterGain);
    windNoise.start();
}
function updateAmbient(g) {
    if (!droneOsc || !windGain)
        return;
    // Modulate drone frequency with speed
    const freq = 50 + (g.speed / 900) * 30;
    droneOsc.frequency.setTargetAtTime(freq, audioCtx.currentTime, 0.1);
    // Wind volume increases with speed
    windGain.gain.setTargetAtTime(0.02 + (g.speed / 900) * 0.06, audioCtx.currentTime, 0.1);
}
// Short blip for pickup
function playPickupSound(type) {
    if (!audioCtx || !masterGain)
        return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    switch (type) {
        case 'energy':
            osc.type = 'sine';
            osc.frequency.setValueAtTime(600, audioCtx.currentTime);
            osc.frequency.exponentialRampToValueAtTime(900, audioCtx.currentTime + 0.1);
            gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
            break;
        case 'score':
            osc.type = 'triangle';
            osc.frequency.setValueAtTime(800, audioCtx.currentTime);
            osc.frequency.exponentialRampToValueAtTime(1200, audioCtx.currentTime + 0.08);
            gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
            break;
        case 'chain':
            osc.type = 'sine';
            osc.frequency.setValueAtTime(500, audioCtx.currentTime);
            osc.frequency.exponentialRampToValueAtTime(1400, audioCtx.currentTime + 0.15);
            gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
            break;
    }
    gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2);
    osc.connect(gain);
    gain.connect(masterGain);
    osc.start();
    osc.stop(audioCtx.currentTime + 0.25);
}
// Low thud for crash
function playCrashSound() {
    if (!audioCtx || !masterGain)
        return;
    // Noise burst
    const bufferSize = audioCtx.sampleRate * 0.3;
    const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
        data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (audioCtx.sampleRate * 0.05));
    }
    const noise = audioCtx.createBufferSource();
    noise.buffer = buffer;
    const gain = audioCtx.createGain();
    gain.gain.value = 0.4;
    const lp = audioCtx.createBiquadFilter();
    lp.type = 'lowpass';
    lp.frequency.value = 300;
    noise.connect(lp);
    lp.connect(gain);
    gain.connect(masterGain);
    noise.start();
    // Sub bass thud
    const osc = audioCtx.createOscillator();
    const oscGain = audioCtx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(80, audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(20, audioCtx.currentTime + 0.3);
    oscGain.gain.setValueAtTime(0.5, audioCtx.currentTime);
    oscGain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.4);
    osc.connect(oscGain);
    oscGain.connect(masterGain);
    osc.start();
    osc.stop(audioCtx.currentTime + 0.5);
}
// Near-miss whoosh
function playNearMissSound() {
    if (!audioCtx || !masterGain)
        return;
    const bufferSize = audioCtx.sampleRate * 0.15;
    const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
        data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (audioCtx.sampleRate * 0.03));
    }
    const noise = audioCtx.createBufferSource();
    noise.buffer = buffer;
    const gain = audioCtx.createGain();
    gain.gain.value = 0.12;
    const bp = audioCtx.createBiquadFilter();
    bp.type = 'bandpass';
    bp.frequency.value = 800;
    bp.Q.value = 3;
    noise.connect(bp);
    bp.connect(gain);
    gain.connect(masterGain);
    noise.start();
}
// Boost engage/disengage sound
function playBoostSound(engaging) {
    if (!audioCtx || !masterGain)
        return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = 'sawtooth';
    if (engaging) {
        osc.frequency.setValueAtTime(200, audioCtx.currentTime);
        osc.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 0.15);
        gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2);
    }
    else {
        osc.frequency.setValueAtTime(600, audioCtx.currentTime);
        osc.frequency.exponentialRampToValueAtTime(200, audioCtx.currentTime + 0.1);
        gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);
    }
    osc.connect(gain);
    gain.connect(masterGain);
    osc.start();
    osc.stop(audioCtx.currentTime + 0.25);
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// === RENDERING ===
Object.defineProperty(exports, "__esModule", { value: true });
exports.initCanvas = initCanvas;
exports.render = render;
const constants_1 = require("./constants");
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d', { alpha: false });
// HUD elements
const scoreDisplay = document.getElementById('score-display');
const distanceDisplay = document.getElementById('distance-display');
const multiplierDisplay = document.getElementById('multiplier-display');
const speedDisplay = document.getElementById('speed-display');
const energyBarFill = document.getElementById('energy-bar-fill');
const energyBarContainer = document.getElementById('energy-bar-container');
const sunWarning = document.getElementById('sun-warning');
// Camera constants
const CAM_HEIGHT = 180;
const FOV = 600;
function initCanvas() {
    canvas.width = constants_1.CANVAS_W;
    canvas.height = constants_1.CANVAS_H;
}
// Project a relative point (x, y, z) to screen coordinates
// z is RELATIVE distance from player (positive = ahead)
function project(wx, wy, wz) {
    if (wz <= 10)
        return null; // Behind camera
    const scale = FOV / wz;
    const sx = constants_1.CANVAS_W / 2 + wx * scale;
    const sy = constants_1.CANVAS_H / 2 - (wy - CAM_HEIGHT) * scale;
    return [sx, sy, scale];
}
// === DRAW SKY ===
function drawSky(g) {
    const sunProgress = g.sunY;
    const grad = ctx.createLinearGradient(0, 0, 0, constants_1.CANVAS_H);
    if (sunProgress < 0.5) {
        const t = sunProgress * 2;
        grad.addColorStop(0, lerpColor('#0a0a3e', '#1a0820', t));
        grad.addColorStop(0.4, lerpColor('#2a1050', '#3a1030', t));
        grad.addColorStop(0.7, lerpColor('#6a2040', '#8a3020', t));
        grad.addColorStop(1, lerpColor('#cc5520', '#aa4410', t));
    }
    else {
        const t = Math.min(1, (sunProgress - 0.5) * 2);
        grad.addColorStop(0, lerpColor('#1a0820', '#050510', t));
        grad.addColorStop(0.4, lerpColor('#3a1030', '#0a0a15', t));
        grad.addColorStop(0.7, lerpColor('#8a3020', '#15101a', t));
        grad.addColorStop(1, lerpColor('#aa4410', '#1a1020', t));
    }
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, constants_1.CANVAS_W, constants_1.CANVAS_H);
}
// === DRAW SUN ===
function drawSun(g) {
    const sunScreenY = g.sunY * constants_1.CANVAS_H * 0.8 + constants_1.CANVAS_H * 0.1;
    const sunX = constants_1.CANVAS_W / 2;
    const sunRadius = 60 + (1 - g.sunY) * 30;
    // Glow layers
    for (let i = 4; i >= 0; i--) {
        const r = sunRadius + i * 40;
        const alpha = Math.max(0, 0.15 - i * 0.025);
        ctx.beginPath();
        ctx.arc(sunX, sunScreenY, r, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(255,${140 + i * 20},${30 + i * 10},${alpha})`;
        ctx.fill();
    }
    // Sun body
    const sunGrad = ctx.createRadialGradient(sunX, sunScreenY, 0, sunX, sunScreenY, sunRadius);
    sunGrad.addColorStop(0, constants_1.COLORS.sunCore);
    sunGrad.addColorStop(0.5, constants_1.COLORS.sunGlow);
    sunGrad.addColorStop(1, constants_1.COLORS.sunOuter);
    ctx.beginPath();
    ctx.arc(sunX, sunScreenY, sunRadius, 0, Math.PI * 2);
    ctx.fillStyle = sunGrad;
    ctx.fill();
    // Horizon glow line
    const horizonAlpha = Math.max(0, 1 - g.sunY * 2) * 0.3;
    if (horizonAlpha > 0.01) {
        const hGrad = ctx.createLinearGradient(0, sunScreenY, 0, constants_1.CANVAS_H);
        hGrad.addColorStop(0, `rgba(255,140,50,${horizonAlpha})`);
        hGrad.addColorStop(1, 'transparent');
        ctx.fillStyle = hGrad;
        ctx.fillRect(0, sunScreenY - 20, constants_1.CANVAS_W, constants_1.CANVAS_H - sunScreenY + 20);
    }
}
// === DRAW GROUND / TRACK ===
function drawGround(g) {
    const halfWidth = (constants_1.LANE_COUNT * constants_1.LANE_SPACING) / 2;
    const time = performance.now() / 1000;
    const effectiveSpeed = g.isBoosting ? g.speed * constants_1.BOOST_MULTIPLIER : g.speed;
    ctx.save();
    // Animated ground plane — horizontal lines that move toward player
    const lineSpacing = 50;
    const offset = (g.distance % lineSpacing);
    for (let relZ = 20 + offset; relZ < constants_1.WORLD_DEPTH; relZ += lineSpacing) {
        const scale = FOV / relZ;
        const y = constants_1.CANVAS_H / 2 - (-CAM_HEIGHT + 50) * scale;
        // Fade with distance
        const alpha = Math.max(0, 1 - relZ / constants_1.WORLD_DEPTH);
        const lineAlpha = alpha * (g.isBoosting ? 0.4 : 0.25);
        ctx.strokeStyle = `rgba(255,140,80,${lineAlpha})`;
        ctx.lineWidth = Math.max(0.5, scale * 1.5);
        ctx.beginPath();
        ctx.moveTo(constants_1.CANVAS_W / 2 + (-halfWidth - 80) * scale, y);
        ctx.lineTo(constants_1.CANVAS_W / 2 + (halfWidth + 80) * scale, y);
        ctx.stroke();
        // Lane dividers — vertical perspective lines
        for (let i = 0; i <= constants_1.LANE_COUNT; i++) {
            const lx = -halfWidth + i * constants_1.LANE_SPACING;
            const sx = constants_1.CANVAS_W / 2 + lx * scale;
            const nextScale = FOV / (relZ + lineSpacing);
            const nextY = constants_1.CANVAS_H / 2 - (-CAM_HEIGHT + 50) * nextScale;
            ctx.strokeStyle = `rgba(255,140,80,${lineAlpha * 0.6})`;
            ctx.lineWidth = Math.max(0.5, scale);
            ctx.beginPath();
            ctx.moveTo(sx, y);
            ctx.lineTo(constants_1.CANVAS_W / 2 + lx * nextScale, nextY);
            ctx.stroke();
        }
    }
    // Ground fill below horizon with gradient
    const groundGrad = ctx.createLinearGradient(0, constants_1.CANVAS_H * 0.5, 0, constants_1.CANVAS_H);
    groundGrad.addColorStop(0, 'rgba(26,10,32,0)');
    groundGrad.addColorStop(0.4, 'rgba(26,10,32,0.5)');
    groundGrad.addColorStop(1, constants_1.COLORS.groundNear);
    ctx.fillStyle = groundGrad;
    ctx.fillRect(0, constants_1.CANVAS_H * 0.5, constants_1.CANVAS_W, constants_1.CANVAS_H * 0.5);
    // Track edge glow lines
    const edgeAlpha = Math.max(0, 1 - g.sunY) * 0.3;
    if (edgeAlpha > 0.02) {
        for (const side of [-1, 1]) {
            ctx.strokeStyle = `rgba(255,140,80,${edgeAlpha})`;
            ctx.lineWidth = Math.max(1, FOV / constants_1.WORLD_DEPTH * 3);
            ctx.beginPath();
            const edgeX = side * (halfWidth + 60);
            for (let relZ = 20; relZ < constants_1.WORLD_DEPTH; relZ += 5) {
                const scale = FOV / relZ;
                const y = constants_1.CANVAS_H / 2 - (-CAM_HEIGHT + 50) * scale;
                const sx = constants_1.CANVAS_W / 2 + edgeX * scale;
                if (relZ === 20)
                    ctx.moveTo(sx, y);
                else
                    ctx.lineTo(sx, y);
            }
            ctx.stroke();
        }
    }
    ctx.restore();
}
// === DRAW OBSTACLES (relative Z) ===
function drawObstacles(g) {
    // Sort by Z (far to near) for proper depth ordering
    const sorted = [...g.obstacles].sort((a, b) => b.z - a.z);
    for (const obs of sorted) {
        if (obs.z < -50 || obs.z > constants_1.WORLD_DEPTH)
            continue;
        const relZ = obs.z; // Already relative to player
        if (relZ <= 10)
            continue;
        const scale = FOV / relZ;
        const sx = constants_1.CANVAS_W / 2 + obs.x * scale;
        const syBase = constants_1.CANVAS_H / 2 - (-CAM_HEIGHT) * scale; // Ground level
        const sw = obs.w * scale;
        const sh = obs.h * scale;
        if (sx < -sw || sx > constants_1.CANVAS_W + sw)
            continue;
        // Shadow side based on sun position
        const shadowFactor = Math.max(0, 1 - g.sunY * 2);
        ctx.save();
        // Different visual styles per obstacle type
        switch (obs.type) {
            case 'pillar':
                drawPillar(sx, syBase, sw, sh, scale, shadowFactor, relZ);
                break;
            case 'wall':
                drawWall(sx, syBase, sw, sh, scale, shadowFactor, relZ);
                break;
            case 'arch':
                drawArch(sx, syBase, sw, sh, scale, shadowFactor, relZ);
                break;
        }
        ctx.restore();
    }
}
function drawPillar(x, yBase, w, h, scale, shadow, relZ) {
    const bodyColor = lerpColor(constants_1.COLORS.obstacleShadow, constants_1.COLORS.obstacleLight, shadow);
    // Main pillar body with slight taper
    ctx.fillStyle = bodyColor;
    ctx.beginPath();
    ctx.moveTo(x - w / 2, yBase);
    ctx.lineTo(x + w / 2, yBase);
    ctx.lineTo(x + w * 0.4, yBase - h);
    ctx.lineTo(x - w * 0.4, yBase - h);
    ctx.closePath();
    ctx.fill();
    // Top cap
    const topColor = lerpColor('#aa3300', '#ff8844', shadow);
    ctx.fillStyle = topColor;
    ctx.fillRect(x - w * 0.5, yBase - h - scale * 4, w, scale * 4);
    // Edge highlights
    ctx.strokeStyle = `rgba(255,170,68,${shadow * 0.5})`;
    ctx.lineWidth = Math.max(1, scale * 2);
    ctx.beginPath();
    ctx.moveTo(x - w / 2, yBase);
    ctx.lineTo(x - w * 0.4, yBase - h);
    ctx.stroke();
    // Warning glow for nearby obstacles
    if (relZ < 400) {
        const proximity = 1 - relZ / 400;
        ctx.shadowColor = '#ff6600';
        ctx.shadowBlur = 25 * proximity;
        ctx.strokeStyle = `rgba(255,100,0,${proximity * 0.5})`;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(x - w / 2, yBase);
        ctx.lineTo(x + w / 2, yBase);
        ctx.lineTo(x + w * 0.4, yBase - h);
        ctx.lineTo(x - w * 0.4, yBase - h);
        ctx.closePath();
        ctx.stroke();
    }
}
function drawWall(x, yBase, w, h, scale, shadow, relZ) {
    const bodyColor = lerpColor(constants_1.COLORS.obstacleShadow, '#cc5522', shadow);
    // Solid wall block
    ctx.fillStyle = bodyColor;
    ctx.fillRect(x - w / 2, yBase - h, w, h);
    // Horizontal lines for texture
    const lineCount = Math.max(1, Math.floor(h / (scale * 15)));
    ctx.strokeStyle = `rgba(0,0,0,${0.15 + shadow * 0.1})`;
    ctx.lineWidth = Math.max(0.5, scale);
    for (let i = 1; i < lineCount; i++) {
        const ly = yBase - h + (h / lineCount) * i;
        ctx.beginPath();
        ctx.moveTo(x - w / 2, ly);
        ctx.lineTo(x + w / 2, ly);
        ctx.stroke();
    }
    // Top edge highlight
    ctx.strokeStyle = `rgba(255,170,68,${shadow * 0.7})`;
    ctx.lineWidth = Math.max(1, scale * 3);
    ctx.beginPath();
    ctx.moveTo(x - w / 2, yBase - h);
    ctx.lineTo(x + w / 2, yBase - h);
    ctx.stroke();
    // Warning glow for nearby obstacles
    if (relZ < 400) {
        const proximity = 1 - relZ / 400;
        ctx.shadowColor = '#ff6600';
        ctx.shadowBlur = 25 * proximity;
        ctx.strokeStyle = `rgba(255,100,0,${proximity * 0.5})`;
        ctx.lineWidth = 2;
        ctx.strokeRect(x - w / 2, yBase - h, w, h);
    }
}
function drawArch(x, yBase, w, h, scale, shadow, relZ) {
    const bodyColor = lerpColor(constants_1.COLORS.obstacleShadow, '#bb4411', shadow);
    // Arch shape — two pillars with a curved top
    const pillarW = w * 0.25;
    // Left pillar
    ctx.fillStyle = bodyColor;
    ctx.fillRect(x - w / 2, yBase - h, pillarW, h);
    // Right pillar
    ctx.fillRect(x + w / 2 - pillarW, yBase - h, pillarW, h);
    // Arch top (curved)
    ctx.beginPath();
    ctx.moveTo(x - w / 2, yBase - h);
    ctx.quadraticCurveTo(x, yBase - h - scale * 15, x + w / 2, yBase - h);
    ctx.lineWidth = Math.max(3, pillarW * 0.6);
    ctx.strokeStyle = bodyColor;
    ctx.stroke();
    // Edge highlights
    ctx.strokeStyle = `rgba(255,170,68,${shadow * 0.4})`;
    ctx.lineWidth = Math.max(1, scale * 2);
    ctx.beginPath();
    ctx.moveTo(x - w / 2, yBase);
    ctx.lineTo(x - w / 2, yBase - h);
    ctx.stroke();
    // Warning glow for nearby obstacles
    if (relZ < 400) {
        const proximity = 1 - relZ / 400;
        ctx.shadowColor = '#ff6600';
        ctx.shadowBlur = 25 * proximity;
        ctx.strokeStyle = `rgba(255,100,0,${proximity * 0.5})`;
        ctx.lineWidth = 2;
        // Draw glow around the arch shape
        ctx.beginPath();
        ctx.moveTo(x - w / 2, yBase);
        ctx.lineTo(x + w / 2, yBase);
        ctx.lineTo(x + w / 2, yBase - h);
        ctx.quadraticCurveTo(x, yBase - h - scale * 15, x - w / 2, yBase - h);
        ctx.closePath();
        ctx.stroke();
    }
}
// === DRAW PICKUPS (relative Z) ===
function drawPickups(g) {
    const time = performance.now() / 1000;
    for (const p of g.pickups) {
        if (p.collected || p.z < -50 || p.z > constants_1.WORLD_DEPTH)
            continue;
        const relZ = p.z; // Already relative to player
        if (relZ <= 10)
            continue;
        const scale = FOV / relZ;
        const bobY = Math.sin(p.bobPhase + time * 3) * 8 * scale;
        const sx = constants_1.CANVAS_W / 2 + p.x * scale;
        const sy = constants_1.CANVAS_H / 2 - (p.y - CAM_HEIGHT) * scale + bobY;
        const size = Math.max(4, 16 * scale);
        if (sx < -size || sx > constants_1.CANVAS_W + size)
            continue;
        ctx.save();
        let color;
        switch (p.type) {
            case 'energy':
                color = constants_1.COLORS.energyCell;
                break;
            case 'score':
                color = constants_1.COLORS.scoreToken;
                break;
            case 'chain':
                color = constants_1.COLORS.chainOrb;
                break;
        }
        // Glow
        ctx.shadowColor = color;
        ctx.shadowBlur = 15 * scale;
        // Diamond shape for pickups
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.moveTo(sx, sy - size);
        ctx.lineTo(sx + size * 0.7, sy);
        ctx.lineTo(sx, sy + size);
        ctx.lineTo(sx - size * 0.7, sy);
        ctx.closePath();
        ctx.fill();
        // Inner highlight
        ctx.fillStyle = 'rgba(255,255,255,0.4)';
        ctx.beginPath();
        ctx.moveTo(sx, sy - size * 0.5);
        ctx.lineTo(sx + size * 0.35, sy);
        ctx.lineTo(sx, sy + size * 0.5);
        ctx.lineTo(sx - size * 0.35, sy);
        ctx.closePath();
        ctx.fill();
        // Proximity pulse when close
        if (relZ < 200) {
            const pulse = Math.sin(time * 8) * 0.5 + 0.5;
            ctx.strokeStyle = `rgba(255,255,255,${pulse * 0.6})`;
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.arc(sx, sy, size * 1.5, 0, Math.PI * 2);
            ctx.stroke();
        }
        ctx.restore();
    }
}
// === DRAW PLAYER CRAFT ===
function drawPlayer(g) {
    const time = performance.now() / 1000;
    // Player is drawn at a fixed screen position with lateral/vertical offset
    const cx = constants_1.CANVAS_W / 2 + g.playerX * (FOV / 300);
    const cy = constants_1.CANVAS_H / 2 - (g.playerY - CAM_HEIGHT) * (FOV / 300);
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(g.bankAngle); // Bank/roll
    // Pitch lean effect — slight vertical stretch
    const pitchScale = 1 + g.pitchLean * 0.3;
    ctx.scale(1, pitchScale);
    const w = constants_1.PLAYER_WIDTH;
    const h = constants_1.PLAYER_HEIGHT;
    // Engine glow
    const enginePulse = Math.sin(time * 12) * 0.2 + 0.8;
    const engineColor = g.isBoosting ? constants_1.COLORS.sunGlow : constants_1.COLORS.playerAccent;
    ctx.shadowColor = engineColor;
    ctx.shadowBlur = 20 * enginePulse;
    // Craft body — sleek triangle shape
    ctx.fillStyle = constants_1.COLORS.playerBody;
    ctx.beginPath();
    ctx.moveTo(0, -h); // Nose
    ctx.lineTo(-w / 2, h * 0.6); // Left wing tip
    ctx.lineTo(-w * 0.15, h * 0.3); // Left inner
    ctx.lineTo(0, h * 0.5); // Tail center
    ctx.lineTo(w * 0.15, h * 0.3); // Right inner
    ctx.lineTo(w / 2, h * 0.6); // Right wing tip
    ctx.closePath();
    ctx.fill();
    // Accent line along body
    ctx.strokeStyle = constants_1.COLORS.playerAccent;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(0, -h + 4);
    ctx.lineTo(0, h * 0.5);
    ctx.stroke();
    // Wing accents
    ctx.strokeStyle = constants_1.COLORS.playerGlow;
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(-w / 2, h * 0.6);
    ctx.lineTo(0, -h + 4);
    ctx.lineTo(w / 2, h * 0.6);
    ctx.stroke();
    // Engine exhaust
    const exhaustLen = g.isBoosting ? 35 : 18;
    const exGrad = ctx.createLinearGradient(0, h * 0.5, 0, h * 0.5 + exhaustLen);
    exGrad.addColorStop(0, engineColor);
    exGrad.addColorStop(1, 'transparent');
    ctx.fillStyle = exGrad;
    ctx.beginPath();
    ctx.moveTo(-6, h * 0.5);
    ctx.lineTo(6, h * 0.5);
    ctx.lineTo(2 + Math.sin(time * 20) * 2, h * 0.5 + exhaustLen * enginePulse);
    ctx.lineTo(-2 - Math.sin(time * 20) * 2, h * 0.5 + exhaustLen * enginePulse);
    ctx.closePath();
    ctx.fill();
    // Boost aura
    if (g.isBoosting) {
        ctx.shadowColor = constants_1.COLORS.sunGlow;
        ctx.shadowBlur = 30;
        ctx.strokeStyle = `rgba(255,170,50,${enginePulse * 0.4})`;
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.moveTo(0, -h);
        ctx.lineTo(-w / 2, h * 0.6);
        ctx.lineTo(w / 2, h * 0.6);
        ctx.closePath();
        ctx.stroke();
    }
    ctx.restore();
}
// === DRAW PARTICLES (relative Z) ===
function drawParticles(g) {
    for (const p of g.particles) {
        const relZ = p.z; // Already relative to player
        if (relZ <= 10 || relZ > constants_1.WORLD_DEPTH)
            continue;
        const scale = FOV / relZ;
        const sx = constants_1.CANVAS_W / 2 + p.x * scale;
        const sy = constants_1.CANVAS_H / 2 - (p.y - CAM_HEIGHT) * scale;
        const size = Math.max(1, p.size * scale);
        const alpha = Math.max(0, p.life / p.maxLife);
        ctx.globalAlpha = alpha;
        ctx.fillStyle = p.color;
        ctx.fillRect(sx - size / 2, sy - size / 2, size, size);
    }
    ctx.globalAlpha = 1;
}
// === DRAW SPEED LINES ===
function drawSpeedLines(g) {
    const effectiveSpeed = g.isBoosting ? g.speed * constants_1.BOOST_MULTIPLIER : g.speed;
    if (effectiveSpeed < 500)
        return;
    const intensity = Math.min(1, (effectiveSpeed - 500) / 400);
    ctx.save();
    for (let i = 0; i < 20 * intensity; i++) {
        const x = Math.random() * constants_1.CANVAS_W;
        const y = Math.random() * constants_1.CANVAS_H;
        const len = 30 + Math.random() * 60;
        ctx.strokeStyle = constants_1.COLORS.speedLine + (0.1 + Math.random() * 0.2 * intensity) + ')';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(x, y);
        const angle = Math.atan2(y - constants_1.CANVAS_H / 2, x - constants_1.CANVAS_W / 2);
        ctx.lineTo(x + Math.cos(angle) * len, y + Math.sin(angle) * len);
        ctx.stroke();
    }
    // Chromatic aberration at very high speed
    if (effectiveSpeed > 700) {
        const caIntensity = (effectiveSpeed - 700) / 200;
        ctx.globalCompositeOperation = 'screen';
        ctx.fillStyle = `rgba(255,0,0,${caIntensity * 0.03})`;
        ctx.fillRect(2, 0, constants_1.CANVAS_W, constants_1.CANVAS_H);
        ctx.fillStyle = `rgba(0,0,255,${caIntensity * 0.03})`;
        ctx.fillRect(-2, 0, constants_1.CANVAS_W, constants_1.CANVAS_H);
        ctx.globalCompositeOperation = 'source-over';
    }
    ctx.restore();
}
// === DRAW NEAR-MISS FLASH ===
function drawNearMissFlash(g) {
    if (g.nearMissFlash <= 0)
        return;
    const alpha = g.nearMissFlash * 0.3;
    ctx.fillStyle = `rgba(255,200,100,${alpha})`;
    ctx.fillRect(0, 0, constants_1.CANVAS_W, constants_1.CANVAS_H);
    if (g.nearMissFlash > 0.1) {
        ctx.save();
        ctx.font = 'bold 24px system-ui';
        ctx.fillStyle = `rgba(255,200,100,${alpha * 3})`;
        ctx.textAlign = 'center';
        ctx.fillText('CLOSE!', constants_1.CANVAS_W / 2, constants_1.CANVAS_H / 2 - 60);
        ctx.restore();
    }
}
// === DRAW DAMAGE FLASH ===
function drawDamageFlash(g) {
    if (g.flashAlpha <= 0.01)
        return;
    ctx.fillStyle = `rgba(255,40,20,${g.flashAlpha})`;
    ctx.fillRect(0, 0, constants_1.CANVAS_W, constants_1.CANVAS_H);
}
// === DRAW VIGNETTE ===
function drawVignette() {
    const grad = ctx.createRadialGradient(constants_1.CANVAS_W / 2, constants_1.CANVAS_H / 2, constants_1.CANVAS_W * 0.3, constants_1.CANVAS_W / 2, constants_1.CANVAS_H / 2, constants_1.CANVAS_W * 0.8);
    grad.addColorStop(0, 'transparent');
    grad.addColorStop(1, 'rgba(0,0,0,0.4)');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, constants_1.CANVAS_W, constants_1.CANVAS_H);
}
// === DRAW TITLE BACKGROUND ANIMATION ===
function drawTitleBackground() {
    const time = performance.now() / 1000;
    // Floating geometric shapes in background
    ctx.save();
    for (let i = 0; i < 8; i++) {
        const x = constants_1.CANVAS_W * 0.2 + Math.sin(time * 0.3 + i * 1.2) * constants_1.CANVAS_W * 0.3;
        const y = constants_1.CANVAS_H * 0.3 + Math.cos(time * 0.4 + i * 0.8) * constants_1.CANVAS_H * 0.25;
        const size = 20 + Math.sin(time + i) * 10;
        const alpha = 0.05 + Math.sin(time * 0.5 + i) * 0.03;
        ctx.strokeStyle = `rgba(255,140,80,${alpha})`;
        ctx.lineWidth = 1;
        ctx.beginPath();
        // Draw diamond shapes
        ctx.moveTo(x, y - size);
        ctx.lineTo(x + size * 0.7, y);
        ctx.lineTo(x, y + size);
        ctx.lineTo(x - size * 0.7, y);
        ctx.closePath();
        ctx.stroke();
    }
    // Subtle scan lines
    for (let y = 0; y < constants_1.CANVAS_H; y += 4) {
        ctx.fillStyle = `rgba(0,0,0,${0.02 + Math.sin(y * 0.1 + time) * 0.01})`;
        ctx.fillRect(0, y, constants_1.CANVAS_W, 1);
    }
    ctx.restore();
}
// === MAIN RENDER ===
function render(g) {
    // Apply screen shake
    ctx.save();
    ctx.translate(g.shakeX, g.shakeY);
    drawSky(g);
    drawSun(g);
    drawGround(g);
    drawObstacles(g);
    drawPickups(g);
    drawParticles(g);
    drawPlayer(g);
    drawSpeedLines(g);
    drawNearMissFlash(g);
    drawDamageFlash(g);
    drawVignette();
    ctx.restore();
    // Title screen background animation
    if (g.state === 'title') {
        drawTitleBackground();
    }
    // Update HUD DOM elements
    updateHUD(g);
}
// === UPDATE HUD ===
function updateHUD(g) {
    if (g.state !== 'playing')
        return;
    const effectiveSpeed = g.isBoosting ? g.speed * constants_1.BOOST_MULTIPLIER : g.speed;
    scoreDisplay.textContent = `SCORE: ${Math.floor(g.score).toLocaleString()}`;
    distanceDisplay.textContent = `DISTANCE: ${Math.floor(g.distance / 10)}m`;
    multiplierDisplay.textContent = `×${g.multiplier.toFixed(1)}`;
    speedDisplay.textContent = `SPEED: ${Math.floor(effectiveSpeed)}`;
    const energyPct = (g.boostEnergy / 100) * 100;
    energyBarFill.style.width = `${energyPct}%`;
    // Low energy visual feedback
    if (g.boostEnergy < 25) {
        energyBarFill.style.background = `linear-gradient(90deg, #ff2200, #ff4400)`;
        energyBarContainer.classList.add('low-energy');
    }
    else {
        energyBarFill.style.background = '';
        energyBarContainer.classList.remove('low-energy');
    }
    // Boost indicator
    if (g.isBoosting) {
        energyBarContainer.classList.add('boosting');
    }
    else {
        energyBarContainer.classList.remove('boosting');
    }
    // Sun warning when sun is getting low
    if (g.sunY > 0.55) {
        sunWarning.classList.remove('hidden');
    }
    else {
        sunWarning.classList.add('hidden');
    }
    // Pulsing multiplier when chaining
    if (g.chainTimer > 0) {
        const pulse = Math.sin(performance.now() / 150) * 0.3 + 0.7;
        multiplierDisplay.style.opacity = String(pulse);
    }
    else {
        multiplierDisplay.style.opacity = '1';
    }
}
// === COLOR UTILITIES ===
function lerpColor(a, b, t) {
    const ca = hexToRgb(a);
    const cb = hexToRgb(b);
    if (!ca || !cb)
        return a;
    const r = Math.round(ca.r + (cb.r - ca.r) * t);
    const g2 = Math.round(ca.g + (cb.g - ca.g) * t);
    const b2 = Math.round(ca.b + (cb.b - ca.b) * t);
    return `rgb(${r},${g2},${b2})`;
}
function hexToRgb(hex) {
    if (!hex.startsWith('#'))
        return null;
    const v = parseInt(hex.slice(1), 16);
    return { r: (v >> 16) & 255, g: (v >> 8) & 255, b: v & 255 };
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>