← Race the Sun results

Race the Sun

inkling · full

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 26,242 bytes · SHA-256 8b69e4ef1f2a
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solar Drift — Race the Sun</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@500;700&display=swap');
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #050a14; font-family: 'Orbitron', sans-serif; }
  canvas { display: block; width: 100vw; height: 100vh; }
  #hud { position: fixed; top: 0; left: 0; right: 0; height: 72px; pointer-events: none; display: flex; align-items: center; justify-content: space-between; padding: 0 32px; z-index: 10; text-shadow: 0 2px 8px rgba(0,0,0,0.6); }
  #hud .label { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 2px; font-size: 11px; text-transform: uppercase; color: rgba(255,255,255,0.4); margin-bottom: -2px; }
  #hud .value { font-family: 'Orbitron', sans-serif; font-weight: 900; font-size: 22px; color: #fff; }
  #hud .energy-box { width: 140px; height: 10px; background: rgba(255,255,255,0.08); border-radius: 5px; margin-top: 4px; overflow: hidden; position: relative; }
  #hud .energy-fill { height: 100%; width: 100%; background: linear-gradient(90deg, #ffaa00, #ff4400); border-radius: 5px; transition: width 0.05s linear; }
  .hud-section { display: flex; flex-direction: column; }
  #titleOverlay, #gameOverOverlay { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; background: rgba(5,10,20,0.75); backdrop-filter: blur(16px); transition: opacity 0.3s ease; }
  #gameOverOverlay { display: none; opacity: 0; }
  h1 { font-size: clamp(48px, 10vw, 120px); font-weight: 900; letter-spacing: 8px; color: #fff; text-transform: uppercase; text-shadow: 0 0 60px rgba(255,200,50,0.5), 0 8px 0 rgba(255,100,0,0.4); margin-bottom: 8px; line-height: 0.9; }
  .subtitle { font-family: 'Rajdhani', sans-serif; font-size: 20px; color: rgba(255,255,255,0.7); letter-spacing: 6px; text-transform: uppercase; margin-bottom: 48px; }
  .btn { font-family: 'Orbitron', sans-serif; font-weight: 700; letter-spacing: 4px; text-transform: uppercase; padding: 16px 48px; border: 2px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); color: #fff; font-size: 16px; cursor: pointer; border-radius: 4px; transition: all 0.1s ease; }
  .btn:hover { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.5); box-shadow: 0 0 20px rgba(255,255,255,0.15); }
  .btn:active { transform: scale(0.98); }
  .key-hint { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); color: rgba(255,255,255,0.35); font-family: 'Rajdhani', sans-serif; font-size: 13px; letter-spacing: 3px; z-index: 5; pointer-events: none; }
  #highScoreDisplay { position: fixed; top: 84px; right: 32px; color: rgba(255,255,255,0.25); font-family: 'Rajdhani', sans-serif; font-size: 13px; letter-spacing: 2px; z-index: 10; pointer-events: none; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>

<div id="hud">
  <div class="hud-section">
    <div class="label">Distance</div>
    <div class="value" id="hudDist">0</div>
  </div>
  <div class="hud-section">
    <div class="label">Score</div>
    <div class="value" id="hudScore">0</div>
  </div>
  <div class="hud-section">
    <div class="label">Multiplier</div>
    <div class="value" id="hudMult">x1</div>
  </div>
  <div class="hud-section">
    <div class="label">Energy</div>
    <div class="energy-box"><div class="energy-fill" id="energyFill"></div></div>
  </div>
  <div class="hud-section">
    <div class="label">Speed</div>
    <div class="value" id="hudSpeed">0</div>
  </div>
</div>

<div id="highScoreDisplay">BEST: <span id="highScoreVal">0</span></div>

<div id="titleOverlay">
  <h1>SOLAR DRIFT</h1>
  <div class="subtitle">Race the Sun</div>
  <button class="btn" onclick="startGame()">START FLIGHT</button>
  <div style="margin-top:32px; color:rgba(255,255,255,0.35); font-size:13px; letter-spacing:2px; font-family:'Rajdhani',sans-serif;">WASD / ARROWS · SPACE BOOST</div>
</div>

<div id="gameOverOverlay">
  <h1 style="font-size:48px; letter-spacing:6px;">CRASHED</h1>
  <div style="font-family:'Rajdhani',sans-serif; font-size:22px; color:rgba(255,255,255,0.7); letter-spacing:4px; margin-bottom:24px;">SCORE <span id="finalScore" style="color:#fff; font-weight:700;">0</span></div>
  <button class="btn" onclick="startGame()">RETRY (R)</button>
</div>

<div class="key-hint">STAY IN THE SUNLIGHT · DODGE · COLLECT · SURVIVE</div>

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

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

// === TUNABLE CONSTANTS ===
const CONFIG = {
  SEED: 1,
  PLAYER_SPEED: 0,
  MAX_SPEED: 120,
  ACCEL: 0.008,
  STRAFE_SPEED: 10,
  PITCH_SPEED: 4,
  BANK_ANGLE_MAX: 35,
  BOOST_MULT: 2.2,
  BOOST_DRAIN: 2.5,
  BOOST_COST: 5,
  ENERGY_MAX: 100,
  ENERGY_DRAIN: 0.15,
  SHADOW_DRAIN: 0.7,
  ENERGY_PICKUP: 25,
  MULT_PICKUP: 1,
  SCORE_PER_DIST: 10,
  NEAR_MISS_RANGE: 60,
  NEAR_MISS_SCORE: 50,
  SHAKE_DECAY: 0.9,
  SUN_DROP_SPEED: 0.0003,
  SUN_START_ANG: 0.15,
  SUN_END_ANG: -0.1,
  OBSTACLE_SPAWN_RATE: 0.015,
  PICKUP_SPAWN_RATE: 0.02,
  LANE_WIDTH: 350,
  WORLD_DEPTH: 3000,
};

// === STATE ===
let state = 'title'; // title, playing, over
let seed = 12345;
let rngState = seed;
function rng() { rngState = (rngState * 16807 + 0) % 2147483647; return (rngState - 1) / 2147483646; }
function seedRun(s) { seed = s; rngState = seed; }

let player = { x: 0, y: 0, z: 0, vx: 0, vy: 0, pitch: 0, roll: 0, energy: CONFIG.ENERGY_MAX, speed: 0, distance: 0, score: 0, multiplier: 1, boost: false, alive: true, nearMissTimer: 0 };
let sunY = CONFIG.SUN_START_ANG;
let sunColor = [255, 200, 80];
let camera = { x: 0, y: 0, shake: 0 };
let obstacles = [];
let pickups = [];
let particles = [];
let speedLines = [];
let time = 0;

// === KEYBOARD ===
const keys = { w:false, a:false, s:false, d:false, arrowleft:false, arrowright:false, arrowup:false, arrowdown:false, space:false, r:false };
window.addEventListener('keydown', e => {
  const k = e.key.toLowerCase();
  if (k in keys) keys[k] = true;
  if (e.key === 'r' || e.key === 'R') { if (state === 'over') startGame(); else if (state === 'title') startGame(); }
});
window.addEventListener('keyup', e => {
  const k = e.key.toLowerCase();
  if (k in keys) keys[k] = false;
});

// === SOUND ===
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioCtx();
let droneOsc, droneGain;
function startDrone() {
  if (droneOsc) return;
  droneOsc = audioCtx.createOscillator();
  droneOsc.type = 'triangle';
  droneOsc.frequency.value = 110;
  droneGain = audioCtx.createGain();
  droneGain.gain.value = 0.05;
  droneOsc.connect(droneGain);
  droneGain.connect(audioCtx.destination);
  droneOsc.start();
}
function updateDrone(s) {
  if (!droneOsc) return;
  droneOsc.frequency.value = 80 + Math.min(s, 120) * 2;
  droneGain.gain.value = 0.02 + Math.min(s, 120) / 6000;
}
function playBlip(freq, gain, type='triangle', dur=0.08) {
  try {
    const o = audioCtx.createOscillator();
    const g = audioCtx.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, audioCtx.currentTime);
    g.gain.setValueAtTime(gain, audioCtx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur);
    o.connect(g); g.connect(audioCtx.destination);
    o.start(); o.stop(audioCtx.currentTime + dur);
  } catch(e){}
}
function playCrash() {
  try {
    const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
    o.type = 'sawtooth'; o.frequency.setValueAtTime(80, audioCtx.currentTime);
    o.frequency.exponentialRampToValueAtTime(20, audioCtx.currentTime + 0.3);
    g.gain.setValueAtTime(0.15, audioCtx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
    o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime + 0.3);
  } catch(e){}
}

// === PARTICLES ===
function spawnParticles(x, y, count, color, speedRange, lifeRange) {
  for (let i = 0; i < count; i++) {
    const angle = Math.random() * Math.PI * 2;
    const spd = speedRange.min + Math.random() * (speedRange.max - speedRange.min);
    particles.push({ x, y, vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd, life: lifeRange.min + Math.random() * (lifeRange.max - lifeRange.min), maxLife: 0, color, size: 2 + Math.random() * 4 });
  }
}
function updateParticles() {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx; p.y += p.vy; p.vy += 0.02; p.life -= 1;
    if (p.life <= 0) particles.splice(i, 1);
  }
}

// === WORLD GENERATION ===
function spawnObstacle() {
  const z = player.z + 800 + Math.random() * 1200;
  const height = 30 + Math.random() * 80;
  const width = 40 + Math.random() * 40;
  const x = (Math.random() - 0.5) * 500;
  const lane = Math.floor(Math.random() * 3) - 1; // -1, 0, 1 lanes-ish
  const xLane = lane * CONFIG.LANE_WIDTH + (Math.random() - 0.5) * 180;
  obstacles.push({
    x: xLane, y: height / 2, z, width, height, depth: 20 + Math.random() * 30,
    color: [20, 30, 50], shadow: true,
  });
}
function spawnPickup() {
  const z = player.z + 600 + Math.random() * 800;
  const x = (Math.random() - 0.5) * 500;
  const type = Math.random() < 0.5 ? 'energy' : (Math.random() < 0.5 ? 'token' : 'mult');
  const y = 40 + Math.random() * 80;
  pickups.push({ x, y, z, type, life: 1, pulse: 0 });
}

// === DRAWING ===
function drawSun(t) {
  const cx = canvas.width / 2;
  const sunYPos = canvas.height * (0.3 + Math.max(0, sunY) * 0.4);
  const radius = Math.max(80, 200 - sunY * 300);
  // Gradient glow
  const grad = ctx.createRadialGradient(cx, sunYPos, radius * 0.2, cx, sunYPos, radius * 1.5);
  grad.addColorStop(0, `rgba(255, 220, 120, 0.9)`);
  grad.addColorStop(0.3, `rgba(255, 180, 60, 0.5)`);
  grad.addColorStop(0.7, `rgba(255, 100, 20, 0.15)`);
  grad.addColorStop(1, `rgba(255, 80, 0, 0)`);
  ctx.fillStyle = grad;
  ctx.beginPath();
  ctx.arc(cx, sunYPos, radius * 1.5, 0, Math.PI * 2);
  ctx.fill();
  // Core sun
  ctx.fillStyle = `rgba(255, 250, 220, 1)`;
  ctx.beginPath();
  ctx.arc(cx, sunYPos, radius * 0.35, 0, Math.PI * 2);
  ctx.fill();
}

function drawWorld(t) {
  // Sky gradient
  const top = `rgba(255, 160, 60, 1)`;
  const bot = `rgba(20, 20, 50, 1)`;
  const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
  grad.addColorStop(0, `rgba(255, 140, 40, 0.4)`);
  grad.addColorStop(0.35 + Math.max(0, sunY) * 0.3, `rgba(255, 200, 80, 0.8)`);
  grad.addColorStop(0.7, `rgba(20, 30, 60, 1)`);
  grad.addColorStop(1, `rgba(5, 8, 20, 1)`);
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  drawSun(t);

  // Ground plane with lanes
  const groundY = canvas.height * 0.72;
  // Shadow zone under sun
  const shadowGrad = ctx.createLinearGradient(0, groundY, 0, canvas.height);
  shadowGrad.addColorStop(0, `rgba(0,0,20,0.6)`);
  shadowGrad.addColorStop(1, `rgba(5,8,25,0.9)`);
  ctx.fillStyle = shadowGrad;
  ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY);

  // Lane markers moving backward
  const laneCount = 3;
  ctx.save();
  ctx.translate(0, groundY);
  const zOffset = (player.z * 0.015) % 80;
  for (let i = -10; i < 15; i++) {
    for (let j = -1; j <= 1; j++) {
      const x = canvas.width / 2 + j * CONFIG.LANE_WIDTH + Math.sin(i * 0.3 + t * 0.02) * 20;
      const yOffset = (i * 80) - zOffset + 20;
      if (yOffset < 0 || yOffset > 600) continue;
      const alpha = Math.max(0, 1 - yOffset / 500);
      ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.15})`;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(x - 20, yOffset);
      ctx.lineTo(x - 20, yOffset + 30);
      ctx.stroke();
    }
  }
  ctx.restore();

  // Draw obstacles
  for (const o of obstacles) {
    const projX = canvas.width / 2 + o.x * 0.2;
    const projY = groundY - (o.z - player.z) * 0.015 + o.y - 30;
    const scale = Math.max(0.1, 1 - (o.z - player.z) / 1600);
    if (projY > canvas.height + 100 || projY < -200) continue;
    ctx.save();
    ctx.translate(projX, projY);
    ctx.scale(scale, scale);
    // Shadow
    ctx.fillStyle = `rgba(0,0,10,${0.3 * scale})`;
    ctx.fillRect(-o.width/2, 5, o.width, 15);
    // Body
    const grad = ctx.createLinearGradient(0, -o.height/2, 0, o.height/2);
    grad.addColorStop(0, `rgba(60, 60, 90, 0.95)`);
    grad.addColorStop(0.5, `rgba(30, 35, 60, 0.95)`);
    grad.addColorStop(1, `rgba(10, 15, 30, 0.9)`);
    ctx.fillStyle = grad;
    ctx.fillRect(-o.width/2, -o.height/2, o.width, o.height);
    // Edge highlight
    ctx.strokeStyle = `rgba(255,255,255,${0.1 * scale})`;
    ctx.lineWidth = 1;
    ctx.strokeRect(-o.width/2, -o.height/2, o.width, o.height);
    ctx.restore();
  }

  // Draw pickups
  for (const p of pickups) {
    const projX = canvas.width / 2 + p.x * 0.2;
    const projY = groundY - (p.z - player.z) * 0.015 - 20;
    const scale = Math.max(0.2, 1 - (p.z - player.z) / 1200);
    if (projY > canvas.height + 50 || projY < -50) continue;
    ctx.save();
    ctx.translate(projX, projY);
    ctx.scale(scale, scale);
    p.pulse += 0.15;
    const pulse = 1 + Math.sin(p.pulse) * 0.2;
    const size = (p.type === 'energy' ? 14 : (p.type === 'mult' ? 12 : 16)) * pulse;
    // Glow
    ctx.shadowColor = p.type === 'energy' ? '#ffaa00' : (p.type === 'mult' ? '#ff00aa' : '#ffffff');
    ctx.shadowBlur = 15 * pulse;
    if (p.type === 'energy') {
      ctx.fillStyle = '#ffaa00';
      ctx.beginPath(); ctx.arc(0, 0, size, 0, Math.PI*2); ctx.fill();
      ctx.fillStyle = '#ffeeaa';
      ctx.beginPath(); ctx.arc(0, 0, size*0.4, 0, Math.PI*2); ctx.fill();
    } else if (p.type === 'mult') {
      ctx.fillStyle = '#ff44aa';
      ctx.beginPath();
      for (let a = 0; a < 5; a++) {
        const ang = (a / 5) * Math.PI * 2;
        const rx = Math.cos(ang) * size; const ry = Math.sin(ang) * size * 0.6;
        if (a === 0) ctx.moveTo(rx, ry); else ctx.lineTo(rx, ry);
      }
      ctx.closePath(); ctx.fill();
    } else {
      ctx.fillStyle = '#ffffff';
      ctx.beginPath(); ctx.arc(0, 0, size, 0, Math.PI*2); ctx.fill();
      ctx.strokeStyle = '#ffffff';
      ctx.lineWidth = 2;
      ctx.beginPath(); ctx.arc(0, 0, size*0.3, 0, Math.PI*2); ctx.stroke();
    }
    ctx.shadowBlur = 0;
    ctx.restore();
  }

  // Player craft
  const px = canvas.width / 2 + player.x * 0.2;
  const py = groundY - 40 + player.y - player.z * 0.015 + player.pitch * 2;
  ctx.save();
  ctx.translate(px, py);
  ctx.rotate(Math.PI * player.roll / 180);
  ctx.rotate(Math.PI * player.pitch / 120); // Lean forward/back

  // Shadow
  ctx.fillStyle = `rgba(0,0,0,0.3)`;
  ctx.fillRect(-20, 10, 40, 8);

  // Main body
  ctx.save();
  ctx.translate(0, -10);
  const craftGrad = ctx.createLinearGradient(0, -30, 0, 20);
  craftGrad.addColorStop(0, `rgba(255, 220, 240, 1)`);
  grad.addColorStop(0.5, `rgba(255, 160, 80, 0.9)`);
  grad.addColorStop(1, `rgba(200, 100, 30, 1)`);
  ctx.fillStyle = grad;
  ctx.beginPath();
  ctx.moveTo(0, -20);
  ctx.lineTo(25, 15);
  ctx.lineTo(-25, 15);
  ctx.closePath();
  ctx.fill();
  // Cockpit glow
  ctx.fillStyle = `rgba(255, 255, 240, 0.95)`;
  ctx.beginPath();
  ctx.arc(0, -2, 5, 0, Math.PI*2); ctx.fill();
  // Wings
  ctx.fillStyle = `rgba(255, 200, 100, 0.9)`;
  ctx.beginPath();
  ctx.moveTo(-22, 5);
  ctx.lineTo(-55, 2);
  ctx.lineTo(-30, 15);
  ctx.closePath(); ctx.fill();
  ctx.beginPath();
  ctx.moveTo(22, 5);
  ctx.lineTo(55, 2);
  ctx.lineTo(30, 15);
  ctx.closePath(); ctx.fill();
  // Side boosters glowing if boosting
  if (keys.space && player.boost) {
    ctx.fillStyle = `rgba(255, 255, 150, 0.9)`;
    ctx.beginPath();
    ctx.arc(-22, 8, 4, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = `rgba(255, 255, 150, 0.9)`;
    ctx.beginPath();
    ctx.arc(22, 8, 4, 0, Math.PI*2); ctx.fill();
  }
  ctx.restore();

  // Thrust particles
  if (state === 'playing') {
    for (let i = 0; i < 3; i++) {
      const px2 = -10 + Math.random() * 20;
      const py2 = 18 + Math.random() * 10;
      const spd = 1 + Math.random() * 3;
      particles.push({
        x: px + px2, y: py + py2,
        vx: (Math.random() - 0.5) * spd - 3,
        vy: Math.random() * spd + 2,
        life: 5 + Math.random() * 10,
        maxLife: 20,
        color: ['rgba(255,200,80,0.8)', 'rgba(255,150,50,0.9)', 'rgba(255,255,240,0.7)'][Math.floor(Math.random()*3)],
        size: 2 + Math.random() * 3
      });
    }
  }

  ctx.restore();

  // Speed lines
  if (player.speed > 60) {
    ctx.save();
    ctx.strokeStyle = `rgba(255, 255, 255, ${Math.min(0.4, (player.speed - 60) / 100)})`;
    ctx.lineWidth = 1.5 + (player.speed - 60) / 40;
    const count = 40 + Math.floor(player.speed / 3);
    for (let i = 0; i < count; i++) {
      const lx = Math.random() * canvas.width;
      const ly = Math.random() * canvas.height;
      const len = 20 + Math.random() * 60;
      ctx.beginPath();
      ctx.moveTo(lx, ly);
      ctx.lineTo(lx - len * 0.3, ly + len);
      ctx.stroke();
    }
    ctx.restore();
  }

  // Chromatic aberration at high speed
  if (player.speed > 80) {
    const offset = (player.speed - 80) * 2;
    // Just shift drawing slightly - done via canvas composite will be complex; we'll rely on visual effects
  }
}

function drawParticles() {
  for (const p of particles) {
    const alpha = Math.max(0, p.life / p.maxLife);
    ctx.fillStyle = p.color.replace(/\)$/, `,${alpha})`).replace(/rgba/, 'rgba');
    // Simple approach
    ctx.fillStyle = p.color.includes('rgba') ? p.color.replace(')', `,${alpha})`) : `rgba(255,255,255,${alpha})`;
    // Better parse
    ctx.fillStyle = p.color; // We'll just draw with alpha
    ctx.globalAlpha = alpha;
    ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
    ctx.globalAlpha = 1;
  }
}

// === UPDATE ===
function update(t) {
  time = t;

  if (state === 'title') {
    // Subtle sun animation
    sunY = CONFIG.SUN_START_ANG + Math.sin(t * 0.0002) * 0.05;
    camera.shake *= CONFIG.SHAKE_DECAY;
    sunColor = [255, 200, 80];
    return;
  }

  if (state === 'playing') {
    // Speed
    player.speed = Math.min(CONFIG.MAX_SPEED, player.speed + CONFIG.ACCEL);
    player.z += player.speed * 0.02;
    player.distance += player.speed * 0.02;
    player.score += player.distance * 0.0002 * (1 + player.multiplier);

    // Sun drops
    sunY = Math.max(CONFIG.SUN_END_ANG, sunY - CONFIG.SUN_DROP_SPEED);
    if (sunY <= CONFIG.SUN_END_ANG) { endGame(); return; }

    // Controls
    const targetPitch = (keys.arrowdown || keys.s ? 1 : 0) - (keys.arrowup || keys.w ? 1 : 0);
    const targetRoll = (keys.arrowright || keys.d ? 1 : 0) - (keys.arrowleft || keys.a ? 1 : 0);
    player.pitch += (targetPitch * CONFIG.PITCH_SPEED - player.pitch) * 0.15;
    player.roll += (targetRoll * CONFIG.BANK_ANGLE_MAX - player.roll) * 0.12;

    // Strafe / vertical
    const strafeTarget = (keys.arrowright || keys.d ? 1 : 0) - (keys.arrowleft || keys.a ? 1 : 0);
    const pitchTarget = (keys.arrowdown || keys.s ? 1 : 0) - (keys.arrowup || keys.w ? 1 : 0);
    player.x += strafeTarget * CONFIG.STRAFE_SPEED * 0.15;
    player.y += pitchTarget * 1.5 * 0.15;
    player.y = Math.max(-60, Math.min(80, player.y));

    // Boost
    player.boost = keys.space && player.energy > CONFIG.BOOST_COST;
    if (player.boost) {
      player.energy -= CONFIG.BOOST_DRAIN;
      player.speed = CONFIG.MAX_SPEED * CONFIG.BOOST_MULT;
      playBlip(600 + Math.random() * 400, 0.05, 'triangle', 0.05);
    }

    // Energy drain
    const inShadow = isInShadow();
    player.energy -= inShadow ? CONFIG.SHADOW_DRAIN : CONFIG.ENERGY_DRAIN;
    if (player.energy < 0) { player.energy = 0; }
    if (player.energy <= 0 && sunY < 0.05) { endGame(); return; }

    // Spawn
    if (Math.random() < CONFIG.OBSTACLE_SPAWN_RATE) spawnObstacle();
    if (Math.random() < CONFIG.PICKUP_SPAWN_RATE) spawnPickup();

    // Cleanup far objects
    obstacles = obstacles.filter(o => o.z - player.z < 1500);
    pickups = pickups.filter(p => p.z - player.z < 800);

    // Collision
    for (const o of obstacles) {
      if (isColliding(player, o)) {
        player.alive = false;
        camera.shake = 30;
        spawnParticles(
          canvas.width / 2 + player.x * 0.2,
          canvas.height * 0.72 - 30 + player.y - player.z * 0.015,
          60,
          ['rgba(255,120,0,0.8)', 'rgba(255,200,50,0.9)'],
          {min:1, max:8}, {min:10, max:40}
        );
        playCrash();
        // Flash
        setTimeout(() => endGame(), 400);
        return;
      }
      // Near miss
      const projX = canvas.width / 2 + o.x * 0.2;
      const projY = canvas.height * 0.72 - (o.z - player.z) * 0.015 + o.y - 30;
      const dist = Math.hypot(projX - (canvas.width/2 + player.x * 0.2), projY - (canvas.height * 0.72 - 30));
      if (dist < CONFIG.NEAR_MISS_RANGE && o.z > player.z - 200 && o.z < player.z + 50) {
        player.nearMissTimer = 5;
        player.score += CONFIG.NEAR_MISS_SCORE * player.multiplier;
        camera.shake = 4;
        playBlip(800 + Math.random() * 400, 0.04, 'sine', 0.06);
        spawnParticles(projX, projY, 8, 'rgba(255,255,255,0.6)', {min:0.5, max:3}, {min:5, max:15});
      }
    }

    // Pickups
    for (let i = pickups.length - 1; i >= 0; i--) {
      const p = pickups[i];
      const projX = canvas.width / 2 + p.x * 0.2;
      const projY = canvas.height * 0.72 - (p.z - player.z) * 0.015 - 20;
      const dist = Math.hypot(projX - (canvas.width/2 + player.x * 0.2), projY - (canvas.height * 0.72 - 30 + player.y));
      if (dist < 30) {
        if (p.type === 'energy') { player.energy = Math.min(CONFIG.ENERGY_MAX, player.energy + CONFIG.ENERGY_PICKUP); playBlip(900, 0.08, 'triangle', 0.1); }
        else if (p.type === 'token') { player.score += 100 * player.multiplier; playBlip(1200, 0.08, 'sine', 0.1); }
        else if (p.type === 'mult') { player.multiplier = Math.min(10, player.multiplier + CONFIG.MULT_PICKUP); player.score += 200; playBlip(1600, 0.1, 'square', 0.15); }
        spawnParticles(projX, projY, 20, p.type === 'energy' ? 'rgba(255,200,50,0.8)' : (p.type === 'mult' ? 'rgba(255,60,150,0.8)' : 'rgba(255,255,255,0.9)'), {min:1, max:5}, {min:8, max:25});
        pickups.splice(i, 1);
      }
    }

    // Update HUD
    document.getElementById('hudDist').textContent = Math.floor(player.distance);
    document.getElementById('hudScore').textContent = Math.floor(player.score).toLocaleString();
    document.getElementById('hudMult').textContent = 'x' + player.multiplier;
    document.getElementById('energyFill').style.width = Math.round(player.energy) + '%';
    document.getElementById('hudSpeed').textContent = Math.floor(player.speed);

    // Near miss timer
    if (player.nearMissTimer > 0) player.nearMissTimer--;

    // Speed lines spawn
    if (player.speed > 60 && Math.random() < 0.3) {
      speedLines.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, len: 30 + Math.random() * 50, life: 1 });
    }

    // Camera shake decay
    camera.shake *= CONFIG.SHAKE_DECAY;
    camera.x = (Math.random() - 0.5) * camera.shake;
    camera.y = (Math.random() - 0.5) * camera.shake;
  }

  updateParticles();
  updateDrone(player.speed);
}

// === COLLISION ===
function isInShadow() {
  // Simple check: if sun is low and player is under obstacle shadow height approx
  return sunY < 0.05 || player.y < -20; // In shadow when low sun or diving under
}

function isColliding(p, o) {
  const dx = p.x - o.x;
  const dz = (p.z + 100) - o.z; // Craft depth
  return Math.abs(dx) < o.width / 2 + 20 && Math.abs(dz) < o.depth / 2 + 20 && p.y > o.y - o.height / 2 && p.y < o.y + o.height / 2;
}

// === GAME FLOW ===
function startGame() {
  seedRun(Math.floor(Math.random() * 100000));
  player = { x: 0, y: 0, z: 0, vx: 0, vy: 0, pitch: 0, roll: 0, energy: CONFIG.ENERGY_MAX, speed: 0, distance: 0, score: 0, multiplier: 1, boost: false, alive: true, nearMissTimer: 0 };
  sunY = CONFIG.SUN_START_ANG;
  obstacles = []; pickups = []; particles = []; speedLines = [];
  camera = { x: 0, y: 0, shake: 0 };
  state = 'playing';
  document.getElementById('titleOverlay').style.display = 'none';
  document.getElementById('gameOverOverlay').style.display = 'none';
  document.getElementById('gameOverOverlay').style.opacity = 0;
  time = 0;
  if (audioCtx.state === 'suspended') audioCtx.resume();
  startDrone();
}

function endGame() {
  state = 'over';
  const hs = parseInt(localStorage.getItem('solarDriftHS') || '0');
  if (player.score > hs) { localStorage.setItem('solarDriftHS', Math.floor(player.score).toString()); document.getElementById('highScoreVal').textContent = Math.floor(player.score).toLocaleString(); }
  else { document.getElementById('highScoreVal').textContent = hs.toLocaleString(); }
  document.getElementById('finalScore').textContent = Math.floor(player.score).toLocaleString();
  document.getElementById('gameOverOverlay').style.display = 'flex';
  setTimeout(() => { document.getElementById('gameOverOverlay').style.opacity = 1; }, 10);
}

// === MAIN LOOP ===
function gameLoop() {
  const t = performance.now() / 1000;
  update(t);

  ctx.save();
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.translate(camera.x, camera.y);
  drawWorld(t);
  drawParticles();
  ctx.restore();

  requestAnimationFrame(gameLoop);
}

// Initialize
function init() {
  document.getElementById('highScoreDisplay').style.display = 'block';
  document.getElementById('highScoreVal').textContent = parseInt(localStorage.getItem('solarDriftHS') || '0').toLocaleString();
}
init();
gameLoop();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"bf6e1a34-15f3-48b6-8951-44cbd7b2b852","tokensIn":514224,"tokensOut":16486,"tokensTotal":530710,"turns":26,"toolCalls":25,"failedToolCalls":2,"timestamp":"2026-07-18T22:28:18.598Z"} -->