← Race the Sun results

Race the Sun

inkling · basic

Initial view of Race the Sun

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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 29,064 bytes · SHA-256 4fb314f8815b
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solar Drift — Endless Flight</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap');

:root {
  --bg: #0a0214;
  --sun-glow: #ff6a3d;
}

* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }

html, body {
  width: 100%; height: 100%;
  background: linear-gradient(180deg, #0a0214 0%, #140a2a 60%, #1f0e1a 100%);
  overflow: hidden;
  font-family: 'Orbitron', sans-serif;
  color: #fff;
}

#canvas-wrapper {
  position: relative;
  width: 100%; height: 100%;
  overflow: hidden;
}

canvas {
  display: block;
  width: 100%; height: 100%;
}

.overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  z-index: 10;
  background: radial-gradient(circle at 50% 80%, rgba(255, 106, 61, 0.15) 0%, transparent 70%);
}

.overlay.hidden { display: none; }

.title-text {
  font-size: clamp(3rem, 8vw, 7rem);
  font-weight: 900;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  text-shadow:
    0 0 20px rgba(255, 106, 61, 0.6),
    0 0 60px rgba(255, 106, 61, 0.3),
    0 4px 0 rgba(255, 60, 20, 0.8);
  color: #ffccaa;
  animation: pulse 3s ease-in-out infinite;
}

.subtitle {
  font-family: 'Rajdhani', sans-serif;
  font-size: clamp(1rem, 2.5vw, 1.8rem);
  font-weight: 300;
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: rgba(255, 200, 200, 0.9);
  margin-top: 0.5em;
  text-shadow: 0 2px 10px rgba(255, 106, 61, 0.4);
}

.prompt {
  margin-top: 2.5em;
  font-family: 'Rajdhani', sans-serif;
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  color: #ffd166;
  text-shadow: 0 0 15px rgba(255, 209, 102, 0.5);
  animation: blink 1.2s infinite;
}

@keyframes pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.03); opacity: 0.95; }
}

@keyframes blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

.hud {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 5;
  padding: 2rem;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  font-family: 'Rajdhani', sans-serif;
  text-shadow: 0 2px 8px rgba(0,0,0,0.8);
}

.hud-top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
}

.hud-bottom {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
}

.stat {
  background: rgba(10, 2, 20, 0.4);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.05);
  padding: 0.6rem 1.2rem;
  border-radius: 12px;
}

.stat-label {
  font-size: 0.75rem;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.5);
  display: block;
}

.stat-value {
  font-size: 1.5rem;
  font-weight: 700;
  color: #ffd166;
}

#game-over .title-text { font-size: 5rem; color: #ff5555; text-shadow: 0 0 20px rgba(255,85,85,0.6); animation: none; }
#game-over .subtitle { color: rgba(255,200,200,0.8); }

.speed-lines {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 6;
  background: repeating-linear-gradient(
    90deg,
    transparent 0px,
    transparent 40px,
    rgba(255, 106, 61, 0.06) 41px,
    rgba(255, 200, 150, 0.04) 42px,
    transparent 43px,
    transparent 80px
  );
  opacity: 0;
  transition: opacity 0.2s;
}
.speed-lines.active { opacity: 1; }
</style>
</head>
<body>

<div id="canvas-wrapper">
  <canvas id="game"></canvas>

  <div class="speed-lines" id="speedLines"></div>

  <div class="hud" id="hud">
    <div class="hud-top">
      <div class="stat">
        <span class="stat-label">Distance</span>
        <span class="stat-value" id="dDist">0</span>
      </div>
      <div class="stat">
        <span class="stat-label">Score</span>
        <span class="stat-value" id="dScore">0</span>
      </div>
      <div class="stat">
        <span class="stat-label">Mult</span>
        <span class="stat-value" id="dMult">x1</span>
      </div>
    </div>
    <div class="hud-bottom">
      <div class="stat">
        <span class="stat-label">Energy</span>
        <span class="stat-value" id="dEnergy">100%</span>
      </div>
      <div class="stat">
        <span class="stat-label">Speed</span>
        <span class="stat-value" id="dSpeed">0</span>
      </div>
    </div>
  </div>

  <div class="overlay" id="titleScreen">
    <h1 class="title-text">Solar Drift</h1>
    <p class="subtitle">Race the Dying Light</p>
    <p class="prompt">PRESS SPACE TO LAUNCH</p>
  </div>

  <div class="overlay hidden" id="gameOverScreen">
    <h1 class="title-text">CONTACT LOST</h1>
    <p class="subtitle">The sun swallowed your craft</p>
    <div class="stat" style="margin-top: 1.5em;">
      <span class="stat-label">Final Score</span>
      <span class="stat-value" id="finalScore" style="font-size: 2.5rem;">0</span>
    </div>
    <p class="prompt" style="margin-top: 1.5em;">PRESS R TO RESTART</p>
  </div>
</div>

<script>
/* =========================================
   TUNABLE CONSTANTS
   ========================================= */
const C = {
  // Canvas & world
  WIDTH: 1920,
  HEIGHT: 1080,
  FOV: 90,
  LANE_COUNT: 5,
  LANE_WIDTH: 140,
  WORLD_DEPTH: 4000,

  // Player physics
  BASE_SPEED: 2.0,
  MAX_SPEED: 28.0,
  ACCEL: 0.0005,
  STRAFE_SENS: 0.018,
  PITCH_SENS: 0.015,
  ROLL_SENS: 0.08,
  BANK_FACTOR: 0.35,
  BOOST_MULT: 1.6,
  BOOST_DRAIN: 0.08,

  // Energy
  ENERGY_MAX: 100,
  ENERGY_DRAIN: 0.015,
  SHADOW_DRAIN_MULT: 3.5,
  PICKUP_ENERGY: 25,

  // Sun
  SUN_START_Y: 0.3,
  SUN_SINK_SPEED: 0.00015,

  // Procedural world
  SEED_OFFSET: 42,
  OBSTACLE_SPAWN_RATE: 0.35,
  PICKUP_SPAWN_RATE: 0.45,
  MULT_ORB_RATE: 0.15,
  MIN_DIST_OBSTACLE: 120,
  MIN_Y_SEP: 80,

  // Visual
  SKY_TOP: '#2a0e1f',
  SKY_MID: '#4a1a2a',
  SKY_HORIZON: '#ff6a3d',
  SKY_BOTTOM: '#1a0a14',
  SUN_COLOR: '#ffcc88',
  SUN_HALO: '#ff5522',
  GROUND_TOP: '#ff8844',
  GROUND_BOT: '#2a0e14',
  PLAYER_COLOR: '#ffeeaa',
  SHADOW_COLOR: '#1a0510',

  // Audio
  MASTER_VOL: 0.15,
};

/* =========================================
   SETUP & CANVAS
   ========================================= */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W = window.innerWidth;
let H = window.innerHeight;

function resize() {
  W = window.innerWidth;
  H = window.innerHeight;
  canvas.width = W;
  canvas.height = H;
}
window.addEventListener('resize', resize);
resize();

/* =========================================
   AUDIO ENGINE (WebAudio)
   ========================================= */
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioCtx();
let audioUnlocked = false;

function unlockAudio() {
  if (audioUnlocked) return;
  audioCtx.resume().then(() => {
    audioUnlocked = true;
    playDrone();
  });
}

let droneOsc, droneGain;
let droneLFO;

function initDrone() {
  droneOsc = audioCtx.createOscillator();
  droneGain = audioCtx.createGain();
  droneOsc.type = 'triangle';
  droneOsc.frequency.value = 55;
  droneGain.gain.value = 0.08;
  droneOsc.connect(droneGain);
  droneGain.connect(audioCtx.destination);
  droneLFO = audioCtx.createOscillator();
  droneLFO.type = 'sine';
  droneLFO.frequency.value = 0.3;
  const lfoGain = audioCtx.createGain();
  lfoGain.gain.value = 8;
  droneLFO.connect(lfoGain);
  lfoGain.connect(droneOsc.frequency);
}

function playDrone() {
  if (!audioUnlocked) return;
  if (!droneOsc) initDrone();
  droneOsc.start();
  droneLFO.start();
}

function stopDrone() {
  try { droneOsc?.stop(); droneLFO?.stop(); } catch(e) {}
}

function playSound(type, pitch = 440, duration = 0.15) {
  if (!audioUnlocked) return;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.connect(gain);
  gain.connect(audioCtx.destination);
  if (type === 'pickup') {
    osc.type = 'sine';
    osc.frequency.setValueAtTime(pitch, audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(pitch * 3, audioCtx.currentTime + duration);
    gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
  } else if (type === 'mult') {
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(pitch, audioCtx.currentTime);
    osc.frequency.linearRampToValueAtTime(pitch * 1.5, audioCtx.currentTime + duration);
    gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
  } else if (type === 'crash') {
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(80, audioCtx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(20, audioCtx.currentTime + 0.3);
    gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4);
  }
  osc.start();
  osc.stop(audioCtx.currentTime + duration * 1.2);
}

function updateDrone(speed) {
  if (!audioUnlocked || !droneOsc) return;
  const freq = 55 + speed * 1.8;
  const vol = Math.min(0.08 + speed * 0.003, 0.25);
  droneOsc.frequency.linearRampToValueAtTime(freq, audioCtx.currentTime + 0.1);
  droneGain.gain.linearRampToValueAtTime(vol, audioCtx.currentTime + 0.1);
}

/* =========================================
   SEEDED RANDOM
   ========================================= */
let runSeed = 0;

function setSeed(s) {
  runSeed = s;
}

function rand() {
  runSeed = (runSeed * 16807 + 0) % 2147483647;
  return (runSeed - 1) / 2147483646;
}

/* =========================================
   GAME STATE
   ========================================= */
const keys = {};
let state = 'title'; // title, playing, over
let score = 0;
let distance = 0;
let multiplier = 1;
let speed = C.BASE_SPEED;
let energy = C.ENERGY_MAX;
let sunY = C.SUN_START_Y;
let cameraShake = 0;
let nearMissTimer = 0;
let boostActive = false;

let player = {
  x: 0, y: 0, z: 150,
  roll: 0, pitch: 0,
  vx: 0, vy: 0,
  boostTimer: 0,
};

const particles = [];
const obstacles = [];
const pickups = [];

function spawnObstacle(z) {
  const lane = Math.floor(rand() * C.LANE_COUNT);
  const lx = (lane - (C.LANE_COUNT - 1) / 2) * C.LANE_WIDTH;
  const yOffset = (rand() - 0.5) * 200;
  const type = rand() > 0.7 ? 'tower' : (rand() > 0.5 ? 'pillar' : 'block');
  obstacles.push({
    x: lx,
    y: yOffset,
    z: z + 800 + rand() * 1200,
    w: 40 + rand() * 60,
    h: 300 + rand() * 500,
    d: 40 + rand() * 60,
    type,
  });
}

function spawnPickup(z) {
  const r = rand();
  const lane = Math.floor(rand() * C.LANE_COUNT);
  const lx = (lane - (C.LANE_COUNT - 1) / 2) * C.LANE_WIDTH;
  let yOff = (rand() - 0.5) * 180;
  if (r < C.MULT_ORB_RATE) {
    pickups.push({ x: lx, y: yOff, z: z + 600 + rand() * 1200, type: 'mult', w: 30, h: 30, d: 30, life: 300 });
  } else if (r < C.PICKUP_SPAWN_RATE + C.MULT_ORB_RATE) {
    pickups.push({ x: lx, y: yOff, z: z + 600 + rand() * 1200, type: 'energy', w: 30, h: 30, d: 30, life: 300 });
  } else {
    pickups.push({ x: lx, y: yOff, z: z + 600 + rand() * 1200, type: 'score', w: 25, h: 25, d: 25, life: 300, val: 50 });
  }
}

/* =========================================
   INPUT
   ========================================= */
window.addEventListener('keydown', e => {
  keys[e.key.toLowerCase()] = true;
  if (e.key === ' ' || e.key === 'Enter') {
    if (state === 'title') {
      startRun();
    } else if (state === 'playing') {
      boostActive = true;
      player.boostTimer = 30;
      playSound('pickup', 600, 0.1);
    }
  }
  if (e.key === 'r' || e.key === 'R') {
    if (state === 'over') startRun();
  }
  if (e.key === ' ' && state === 'title') {
    e.preventDefault();
    startRun();
  }
});
window.addEventListener('keyup', e => {
  keys[e.key.toLowerCase()] = false;
  if (e.key === ' ') boostActive = false;
});

/* =========================================
   GAME LOOP
   ========================================= */
function initRun() {
  setSeed(Math.floor(Math.random() * 1000000));
  score = 0;
  distance = 0;
  multiplier = 1;
  speed = C.BASE_SPEED;
  energy = C.ENERGY_MAX;
  sunY = C.SUN_START_Y;
  cameraShake = 0;
  nearMissTimer = 0;
  player = { x: 0, y: 0, z: 150, roll: 0, pitch: 0, vx: 0, vy: 0, boostTimer: 0 };
  obstacles.length = 0;
  pickups.length = 0;
  particles.length = 0;
  updateHUD();
  document.getElementById('titleScreen').classList.add('hidden');
  document.getElementById('gameOverScreen').classList.add('hidden');
  document.getElementById('hud').style.display = 'flex';
}

function startRun() {
  unlockAudio();
  initRun();
  state = 'playing';
  requestAnimationFrame(loop);
}

function gameOver() {
  state = 'over';
  playSound('crash', 120, 0.5);
  stopDrone();
  document.getElementById('gameOverScreen').classList.remove('hidden');
  document.getElementById('finalScore').textContent = Math.floor(score);
  document.getElementById('hud').style.display = 'none';
  localStorage.setItem('solarDriftBest', Math.max(parseInt(localStorage.getItem('solarDriftBest') || '0'), Math.floor(score)));
}

function updateHUD() {
  document.getElementById('dDist').textContent = Math.floor(distance);
  document.getElementById('dScore').textContent = Math.floor(score);
  document.getElementById('dMult').textContent = 'x' + multiplier;
  document.getElementById('dEnergy').textContent = Math.floor(energy) + '%';
  document.getElementById('dSpeed').textContent = Math.floor(speed);
}

/* =========================================
   UPDATE
   ========================================= */
function update() {
  if (state !== 'playing') return;

  // Speed curve
  speed += C.ACCEL;
  speed = Math.min(speed, C.MAX_SPEED);

  // Sun sink
  sunY += C.SUN_SINK_SPEED * speed;

  // Energy drain
  let drain = C.ENERGY_DRAIN * (sunY < 0.45 ? 1 + (0.45 - sunY) * 4 : 1);
  // Shadow check: simple approximation — if sun is low, extra drain
  if (sunY > 0.55) drain *= C.SHADOW_DRAIN_MULT;

  if (boostActive && player.boostTimer > 0) {
    drain += C.BOOST_DRAIN;
    player.boostTimer--;
  }

  energy -= drain;
  energy = Math.max(0, energy);

  // Auto-death from sun below horizon or energy depleted
  if (sunY > 0.92 || energy <= 0) {
    gameOver();
    return;
  }

  // Player input
  let ax = 0, ay = 0;
  if (keys['a'] || keys['arrowleft']) ax -= 1;
  if (keys['d'] || keys['arrowright']) ax += 1;
  if (keys['w'] || keys['arrowup']) ay -= 1;
  if (keys['s'] || keys['arrowdown']) ay += 1;

  // Strafe & pitch with easing
  const ease = 0.08 + Math.min(speed * 0.005, 0.04);
  player.vx += ax * C.STRAFE_SENS * ease;
  player.vy += ay * C.PITCH_SENS * ease;
  player.vx *= 0.82;
  player.vy *= 0.82;

  // Apply movement
  player.x += player.vx;
  player.y += player.vy;
  player.z -= speed * (boostActive ? C.BOOST_MULT : 1.0);

  // Roll based on strafe
  player.roll = -(player.vx * C.BANK_FACTOR);
  player.roll *= 0.9; // dampen slightly

  // Pitch lean
  player.pitch = -(player.vy * 0.4);

  // Clamp position
  player.x = Math.max(-C.LANE_WIDTH * 2.5, Math.min(C.LANE_WIDTH * 2.5, player.x));
  player.y = Math.max(-200, Math.min(300, player.y));

  // Distance & score
  distance += speed * (boostActive ? C.BOOST_MULT : 1.0);
  score += speed * multiplier * (boostActive ? 1.5 : 1.0) * 0.1;

  // Spawn waves
  const spawnZ = player.z + 300;
  if (rand() < C.OBSTACLE_SPAWN_RATE) spawnObstacle(player.z);
  if (rand() < C.PICKUP_SPAWN_RATE) spawnPickup(player.z);

  // Update world objects
  for (let i = obstacles.length - 1; i >= 0; i--) {
    const o = obstacles[i];
    o.z -= speed * (boostActive ? C.BOOST_MULT : 1.0);
    if (o.z < player.z - 200) {
      obstacles.splice(i, 1);
    } else {
      // Collision
      const dx = Math.abs(o.x - player.x);
      const dy = Math.abs(o.y - player.y);
      // Simplified AABB vs point with small craft size
      if (dx < (o.w / 2 + 30) && dy < (o.h / 2 + 20) && Math.abs(o.z - player.z) < 80) {
        // Crash!
        addParticles(player.x, player.y, 40, '#ff5522');
        cameraShake = 25;
        energy -= 40;
        playSound('crash', 100, 0.3);
        // Near miss bonus
        score += 200 * multiplier;
        multiplier += 0.5;
        obstacles.splice(i, 1);
        if (energy <= 0) {
          gameOver();
          return;
        }
      }
    }
  }

  // Pickups
  for (let i = pickups.length - 1; i >= 0; i--) {
    const p = pickups[i];
    p.z -= speed * (boostActive ? C.BOOST_MULT : 1.0);
    p.life--;
    if (p.life <= 0) {
      pickups.splice(i, 1);
    } else {
      const dx = Math.abs(p.x - player.x);
      const dy = Math.abs(p.y - player.y);
      if (dx < (p.w / 2 + 20) && dy < (p.h / 2 + 20) && Math.abs(p.z - player.z) < 40) {
        if (p.type === 'energy') {
          energy = Math.min(C.ENERGY_MAX, energy + C.PICKUP_ENERGY);
          score += 100 * multiplier;
          playSound('pickup', 880, 0.15);
          addParticles(p.x, p.y, 15, '#ffd166');
        } else if (p.type === 'mult') {
          multiplier += 1;
          score += 500;
          playSound('mult', 1320, 0.2);
          addParticles(p.x, p.y, 15, '#ffaa55');
        } else {
          score += p.val * multiplier;
          playSound('pickup', 660, 0.15);
          addParticles(p.x, p.y, 12, '#ffeeaa');
        }
        pickups.splice(i, 1);
      }
    }
  }

  // Near miss detection (close to obstacle but not hitting)
  let nearMiss = false;
  for (const o of obstacles) {
    const dx = Math.abs(o.x - player.x);
    const dy = Math.abs(o.y - player.y);
    const distZ = Math.abs(o.z - player.z);
    if (distZ < 120 && dx < (o.w/2 + 50) && dy < (o.h/2 + 50) && !(dx < (o.w/2 + 30) && dy < (o.h/2 + 20) && distZ < 80)) {
      nearMiss = true;
      score += 2 * multiplier * speed;
    }
  }
  if (nearMiss) {
    nearMissTimer = 10;
    cameraShake = Math.min(cameraShake + 3, 10);
  }

  // Camera shake decay
  cameraShake *= 0.85;

  // Particles update
  for (let i = particles.length - 1; i >= 0; i--) {
    const pt = particles[i];
    pt.x += pt.vx;
    pt.y += pt.vy;
    pt.z += pt.vz;
    pt.life--;
    pt.alpha = pt.life / pt.maxLife;
    if (pt.life <= 0) particles.splice(i, 1);
  }

  // Update sound
  updateDrone(speed);

  updateHUD();
}

/* =========================================
   DRAWING
   ========================================= */
function draw() {
  // Clear with gradient
  const grad = ctx.createLinearGradient(0, 0, 0, H);
  grad.addColorStop(0, C.SKY_TOP);
  grad.addColorStop(0.35, C.SKY_MID);
  grad.addColorStop(0.58, C.SKY_HORIZON);
  grad.addColorStop(0.7, C.SKY_BOTTOM);
  // Slight variation over time
  const shift = Math.sin(Date.now() * 0.0005) * 0.02;
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, W, H);

  // Camera shake
  const sx = (Math.random() - 0.5) * cameraShake;
  const sy = (Math.random() - 0.5) * cameraShake;

  ctx.save();
  ctx.translate(W/2 + sx, H/2 + sy);

  // Scale for perspective
  const scale = W / 1000;

  // Draw sun on horizon
  const sunX = 0;
  const sunDrawY = H * sunY - H/2;
  const sunR = 120 + (1 - sunY) * 200;
  // Halo glow
  const haloGrad = ctx.createRadialGradient(sunX, sunDrawY, 0, sunX, sunDrawY, sunR * 2.5);
  haloGrad.addColorStop(0, 'rgba(255, 200, 100, 0.3)');
  haloGrad.addColorStop(0.5, 'rgba(255, 106, 61, 0.15)');
  haloGrad.addColorStop(1, 'rgba(200, 50, 20, 0)');
  ctx.fillStyle = haloGrad;
  ctx.beginPath();
  ctx.arc(sunX, sunDrawY, sunR * 2.5, 0, Math.PI * 2);
  ctx.fill();

  // Sun body
  const sunGrad = ctx.createRadialGradient(sunX, sunDrawY, 0, sunX, sunDrawY, sunR);
  sunGrad.addColorStop(0, '#fff8e7');
  sunGrad.addColorStop(0.4, C.SUN_COLOR);
  sunGrad.addColorStop(1, '#ff5522');
  ctx.fillStyle = sunGrad;
  ctx.beginPath();
  ctx.arc(sunX, sunDrawY, sunR, 0, Math.PI * 2);
  ctx.fill();

  // Ground plane (perspective grid)
  const groundY = H * 0.6 - H/2;
  ctx.fillStyle = '#1a0510';
  ctx.fillRect(-W, groundY, W*2, H);

  // Perspective lines
  for (let i = 0; i <= 8; i++) {
    const t = i / 8;
    const gx = (t - 0.5) * W * 1.2;
    const gy = groundY + Math.sin(t * Math.PI) * 100;
    ctx.strokeStyle = `rgba(255, 136, 68, ${0.15 + 0.15 * Math.abs(Math.sin(i * 0.5))})`;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(gx, gy);
    ctx.lineTo(gx, H + 200);
    ctx.stroke();
  }

  // Ground gradient
  const gGrad = ctx.createLinearGradient(0, groundY, 0, H);
  gGrad.addColorStop(0, C.GROUND_TOP);
  gGrad.addColorStop(1, C.GROUND_BOT);
  ctx.fillStyle = gGrad;
  ctx.fillRect(-W, groundY, W*2, H/2);

  // Draw world objects sorted by z
  const worldItems = [];
  for (const o of obstacles) worldItems.push({ ...o, kind: 'obst' });
  for (const p of pickups) worldItems.push({ ...p, kind: 'pick' });
  worldItems.sort((a, b) => a.z - b.z);

  for (const item of worldItems) {
    const proj = project(item.x, item.y, item.z);
    const scaleFactor = proj.s;

    // Skip if behind camera or too small
    if (item.z < player.z - 50 || proj.s < 0.01) continue;

    if (item.kind === 'obst') {
      // Draw obstacle
      const w = item.w * scaleFactor;
      const h = item.h * scaleFactor;
      const x = proj.x - w/2;
      const y = proj.y - h;
      const d = item.d * scaleFactor;

      // Shadow side
      const isInShadow = sunY > 0.6;
      const baseColor = isInShadow ? '#1a0510' : '#ff8844';
      const lightColor = isInShadow ? '#2a0e14' : '#ffaa66';

      // Main block
      ctx.fillStyle = baseColor;
      drawBox(ctx, x, y + h*0.2, w, h*0.8, d, 0.3);

      // Highlight face
      ctx.fillStyle = lightColor;
      ctx.beginPath();
      ctx.moveTo(x + w*0.1, y + h*0.2);
      ctx.lineTo(x + w*0.1 + d*0.3, y + h*0.1);
      ctx.lineTo(x + w*0.1 + d*0.3, y + h*0.2 + h*0.8);
      ctx.lineTo(x + w*0.1, y + h*0.2 + h*0.8);
      ctx.fill();

    } else {
      // Pickup orb
      const r = item.w/2 * scaleFactor;
      const x = proj.x;
      const y = proj.y - r;
      const color = item.type === 'energy' ? '#ffcc22' : (item.type === 'mult' ? '#ffaa55' : '#ffeeaa');
      const glow = item.type === 'mult' ? '#ff8800' : '#ffee88';

      ctx.save();
      ctx.translate(x, y);
      // Glow
      const g = ctx.createRadialGradient(0, 0, r*0.2, 0, 0, r*2.5);
      g.addColorStop(0, glow);
      g.addColorStop(0.3, color + '44');
      g.addColorStop(1, 'transparent');
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(0, 0, r*2.5, 0, Math.PI*2);
      ctx.fill();
      // Body
      ctx.fillStyle = color;
      ctx.beginPath();
      ctx.arc(0, 0, r, 0, Math.PI*2);
      ctx.fill();
      // Highlight
      ctx.fillStyle = '#fff';
      ctx.beginPath();
      ctx.arc(-r*0.3, -r*0.3, r*0.25, 0, Math.PI*2);
      ctx.fill();
      ctx.restore();
    }
  }

  // Draw player
  const pProj = project(player.x, player.y + 30, player.z);
  const ps = pProj.s;

  // Shadow under player
  ctx.fillStyle = 'rgba(10, 2, 20, 0.5)';
  ctx.beginPath();
  ctx.ellipse(pProj.x, pProj.y + 20, 25 * ps, 12 * ps, 0, 0, Math.PI * 2);
  ctx.fill();

  ctx.save();
  ctx.translate(pProj.x, pProj.y);
  ctx.rotate(player.roll);
  ctx.rotate(player.pitch * 0.3);

  // Craft body
  const craftW = 50 * ps;
  const craftH = 18 * ps;
  const craftD = 70 * ps;

  // Main hull
  ctx.fillStyle = '#ffeecc';
  drawRoundBody(ctx, -craftW/2, -craftH/2, craftW, craftH, craftD, 0.4);

  // Wings
  ctx.fillStyle = '#ffddaa';
  ctx.beginPath();
  ctx.moveTo(-craftW/2 - 15*ps, 0);
  ctx.lineTo(-craftW/2 - 60*ps, -25*ps);
  ctx.lineTo(-craftW/2 - 35*ps, 10*ps);
  ctx.lineTo(-craftW/2 - 15*ps, 0);
  ctx.fill();

  ctx.beginPath();
  ctx.moveTo(craftW/2 + 15*ps, 0);
  ctx.lineTo(craftW/2 + 60*ps, -25*ps);
  ctx.lineTo(craftW/2 + 35*ps, 10*ps);
  ctx.lineTo(craftW/2 + 15*ps, 0);
  ctx.fill();

  // Cockpit glow
  const glowGrad = ctx.createRadialGradient(0, -craftH/4, 2, 0, -craftH/4, 15*ps);
  glowGrad.addColorStop(0, '#ffcc88');
  glowGrad.addColorStop(0.5, '#ffaa66');
  glowGrad.addColorStop(1, 'transparent');
  ctx.fillStyle = glowGrad;
  ctx.beginPath();
  ctx.arc(0, -craftH/4, 12*ps, 0, Math.PI*2);
  ctx.fill();

  // Thrust particles behind craft
  for (let i = 0; i < 3; i++) {
    const px = (Math.random() - 0.5) * 20 * ps - craftD * 0.3;
    const py = (Math.random() - 0.5) * 10 * ps;
    const size = (3 + Math.random() * 6) * ps;
    ctx.fillStyle = `rgba(255, 200, 150, ${0.4 + Math.random() * 0.4})`;
    ctx.beginPath();
    ctx.arc(px, py, size, 0, Math.PI*2);
    ctx.fill();
  }

  ctx.restore();

  // Particles
  for (const pt of particles) {
    const proj = project(pt.x, pt.y, pt.z);
    if (proj.s < 0.01 || proj.z < player.z - 50) continue;
    ctx.save();
    ctx.globalAlpha = pt.alpha;
    ctx.translate(proj.x, proj.y);
    ctx.fillStyle = pt.color;
    ctx.beginPath();
    ctx.arc(0, 0, pt.size * proj.s, 0, Math.PI*2);
    ctx.fill();
    ctx.restore();
  }

  ctx.restore();

  // Post-process speed lines overlay (drawn on wrapper via CSS, but add chromatic abberation via canvas)
  if (speed > 18) {
    ctx.save();
    ctx.globalAlpha = Math.min((speed - 18) / 10, 0.2);
    // Subtle chromatic split
    ctx.save();
    ctx.globalCompositeOperation = 'screen';
    ctx.translate(W*0.002, H*0.001);
    ctx.fillStyle = 'rgba(255, 106, 61, 0.1)';
    ctx.fillRect(0, 0, W, H);
    ctx.restore();
    ctx.restore();
  }
}

function project(x, y, z) {
  const camZ = player.z - 100;
  const relZ = z - camZ;
  const s = (C.FOV * W) / (relZ + C.FOV);
  const screenX = (x * s) + W/2;
  const screenY = (y * s) + H/2;
  return { x: screenX, y: screenY, s: s / (C.FOV * W / 600), z: z };
}

function drawBox(ctx, x, y, w, h, d, tilt) {
  // Simple isometric box
  const halfW = w/2;
  const halfD = d/2;
  // Top face (slightly tilted)
  ctx.fillStyle = '#ffaa66';
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x + halfW + halfD * tilt, y - h*0.15);
  ctx.lineTo(x + halfW + halfD * tilt + w - halfD*tilt*2, y - h*0.15);
  ctx.lineTo(x + w, y);
  ctx.fill();

  // Front face
  ctx.fillStyle = '#ff8844';
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x + w, y);
  ctx.lineTo(x + w, y + h);
  ctx.lineTo(x, y + h);
  ctx.fill();

  // Side face
  ctx.fillStyle = '#e05522';
  ctx.beginPath();
  ctx.moveTo(x + w, y);
  ctx.lineTo(x + w + halfD * tilt, y - h*0.15);
  ctx.lineTo(x + w + halfD * tilt, y + h - h*0.15);
  ctx.lineTo(x + w, y + h);
  ctx.fill();
}

function drawRoundBody(ctx, x, y, w, h, d, tilt) {
  // Streamlined craft body with simple 3D effect
  const cx = x + w/2;
  const cy = y + h/2;

  // Main body ellipse
  ctx.fillStyle = '#ffeecc';
  ctx.beginPath();
  ctx.ellipse(cx, cy, w/2, h/2, 0, 0, Math.PI*2);
  ctx.fill();

  // Nose extension
  ctx.fillStyle = '#ffeecc';
  ctx.beginPath();
  ctx.ellipse(cx + w/2 + d*0.3, cy, d*0.25, h/2*0.9, -0.2, 0, Math.PI*2);
  ctx.fill();

  // Side shading
  ctx.fillStyle = 'rgba(0,0,0,0.15)';
  ctx.beginPath();
  ctx.ellipse(cx + w/2 - 5, cy, w/2 - 5, h/2, 0, 0, Math.PI*2);
  ctx.fill();
}

function addParticles(x, y, count, color) {
  for (let i = 0; i < count; i++) {
    particles.push({
      x, y, z: player.z + (Math.random()-0.5)*50,
      vx: (Math.random()-0.5)*4,
      vy: (Math.random()-0.5)*4 + 2,
      vz: -Math.random()*6 - 2,
      life: 30 + Math.random() * 30,
      maxLife: 60,
      color,
      size: 3 + Math.random() * 6,
    });
  }
}

function loop() {
  update();
  draw();

  // Speed lines activation
  document.getElementById('speedLines').classList.toggle('active', speed > 20);

  if (state !== 'over') {
    requestAnimationFrame(loop);
  }
}

/* =========================================
   INIT & HIGH SCORE DISPLAY
   ========================================= */
function showTitle() {
  state = 'title';
  document.getElementById('titleScreen').classList.remove('hidden');
  document.getElementById('gameOverScreen').classList.add('hidden');
  document.getElementById('hud').style.display = 'none';
  // Show best score
  const best = localStorage.getItem('solarDriftBest') || '0';
  const bestEl = document.getElementById('bestScore');
  if (!bestEl) {
    const el = document.createElement('p');
    el.id = 'bestScore';
    el.style.cssText = 'margin-top:1em;font-family:Rajdhani,sans-serif;font-size:1rem;color:#ffaa55;text-shadow:0 2px 8px rgba(255,106,61,0.3);';
    el.innerHTML = 'BEST: ' + best;
    document.getElementById('titleScreen').appendChild(el);
  } else {
    bestEl.innerHTML = 'BEST: ' + best;
  }
}

// Start on load
showTitle();

// Pre-warm audio context on any interaction
window.addEventListener('click', unlockAudio);
window.addEventListener('keydown', unlockAudio);

</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"basic","sessionId":"049c4d6b-7a2b-4c48-bf3f-31662d5b59a2","tokensIn":939,"tokensOut":10208,"tokensTotal":11147,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T21:07:04.610Z"} -->