← Race the Sun results

Race the Sun

thinkingcap qwen3.6 27b · 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,479 bytes · SHA-256 ac9abe74693d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solar Runner</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
// === CONSTANTS ===
const CFG = {
  // Player
  PLAYER_WIDTH: 28,
  PLAYER_HEIGHT: 16,
  MOVE_SPEED: 0.14,
  MOVE_EASING: 0.15,
  INERTIA_TAIL: 0.92,
  BANK_ANGLE: 0.35,
  PITCH_LEAN: 0.15,
  BOOST_SPEED_MULT: 1.6,
  BOOST_DRAIN: 0.8,
  BOOST_RECHARGE: 0.04,

  // World
  BASE_SPEED: 8,
  MAX_SPEED: 32,
  SPEED_RAMP: 0.003,
  LANE_WIDTH: 120,
  WORLD_HALF_W: 360,
  WORLD_HALF_H: 140,

  // Sun & Energy
  SUN_START_Y: 0.35,
  SUN_SET_RATE: 0.00008,
  ENERGY_MAX: 100,
  ENERGY_DRAIN: 0.025,
  ENERGY_SHADOW_MULT: 3.5,
  ENERGY_PICKUP: 25,

  // Obstacles
  SPAWN_INTERVAL_BASE: 60,
  SPAWN_MIN: 18,
  OBSTACLE_DEPTH: 40,
  NEAR_MISS_DIST: 45,

  // Scoring
  DISTANCE_SCORE: 1,
  NEAR_MISS_BONUS: 50,
  TOKEN_VALUE: 100,
  MULTIPLIER_MAX: 8,

  // Effects
  SHAKE_NEAR_MISS: 3,
  SHAKE_CRASH: 12,
  SHAKE_DECAY: 0.82,
  SPEED_LINE_THRESHOLD: 16,
  PARTICLE_COUNT: 24,

  // Colors
  SKY_TOP: [15, 10, 40],
  SKY_MID: [80, 30, 60],
  SKY_BOT: [200, 80, 40],
  SUN_COLOR: [255, 220, 100],
  GROUND_COLOR: [20, 15, 35],
  OBSTACLE_COLORS: [[255,60,80],[255,140,40],[255,200,50],[100,200,255]],
};

// === GLOBALS ===
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
let state = 'title'; // title | playing | gameover
let seed = Date.now();
let rngState = seed;

// Player
let player = {};
let obstacles = [];
let pickups = [];
let particles = [];
let speedLines = [];
let scoreTokens = [];

// Game vars
let distance = 0;
let score = 0;
let multiplier = 1;
let energy = CFG.ENERGY_MAX;
let boostEnergy = 100;
let isBoosting = false;
let speed = CFG.BASE_SPEED;
let sunY = CFG.SUN_START_Y;
let frameCount = 0;
let shakeX = 0, shakeY = 0;
let flashAlpha = 0;
let nearMissTimer = 0;
let highScore = parseInt(localStorage.getItem('solarRunnerHS')) || 0;

// Input
let keys = {};
let inputLX = 0, inputLY = 0;

// Audio
let audioCtx = null;
let masterGain = null;
let droneOsc = null;
let droneGain = null;

// === SEEDED RNG ===
function rng() {
  rngState = (rngState * 16807 + 0) % 2147483647;
  return (rngState - 1) / 2147483646;
}

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

// === AUDIO SYSTEM ===
function initAudio() {
  if (audioCtx) return;
  audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  masterGain = audioCtx.createGain();
  masterGain.gain.value = 0.3;
  masterGain.connect(audioCtx.destination);

  // Drone
  droneOsc = audioCtx.createOscillator();
  droneGain = audioCtx.createGain();
  droneOsc.type = 'sawtooth';
  droneOsc.frequency.value = 55;
  droneGain.gain.value = 0.06;
  const lfo = audioCtx.createOscillator();
  const lfoGain = audioCtx.createGain();
  lfo.frequency.value = 2;
  lfoGain.gain.value = 8;
  lfo.connect(lfoGain);
  lfoGain.connect(droneOsc.frequency);
  lfo.start();
  droneOsc.connect(droneGain);
  droneGain.connect(masterGain);
  droneOsc.start();
}

function playTone(freq, dur, type, vol) {
  if (!audioCtx) return;
  const o = audioCtx.createOscillator();
  const g = audioCtx.createGain();
  o.type = type || 'square';
  o.frequency.value = freq;
  g.gain.setValueAtTime(vol || 0.15, audioCtx.currentTime);
  g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur);
  o.connect(g);
  g.connect(masterGain);
  o.start();
  o.stop(audioCtx.currentTime + dur);
}

function sfxPickup() { playTone(880, 0.12, 'square', 0.12); setTimeout(() => playTone(1320, 0.1, 'sine', 0.1), 60); }
function sfxEnergy() { playTone(440, 0.15, 'triangle', 0.15); setTimeout(() => playTone(660, 0.12, 'triangle', 0.12), 80); setTimeout(() => playTone(990, 0.1, 'sine', 0.1), 140); }
function sfxCrash() { playTone(80, 0.4, 'sawtooth', 0.25); playTone(60, 0.5, 'square', 0.15); }
function sfxNearMiss() { playTone(1200, 0.06, 'sine', 0.08); }
function sfxBoost() { playTone(200, 0.3, 'sawtooth', 0.08); }

// === PARTICLES ===
function spawnParticles(x, y, count, color, spread, life) {
  for (let i = 0; i < count; i++) {
    particles.push({
      x, y,
      vx: (rng() - 0.5) * spread,
      vy: (rng() - 0.5) * spread,
      life: life || (20 + rng() * 30),
      maxLife: life || (20 + rng() * 30),
      color,
      size: 1 + rng() * 3,
    });
  }
}

function updateParticles() {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.life--;
    if (p.life <= 0) particles.splice(i, 1);
  }
}

function drawParticles() {
  for (const p of particles) {
    const a = p.life / p.maxLife;
    ctx.globalAlpha = a;
    ctx.fillStyle = `rgb(${p.color[0]},${p.color[1]},${p.color[2]})`;
    ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size * a + 1, p.size * a + 1);
  }
  ctx.globalAlpha = 1;
}

// === PROJECTION HELPERS ===
function project(wx, wy, wz) {
  const camZ = 300;
  const fov = 500;
  const z = Math.max(wz, 1);
  const scale = fov / (fov + z - camZ);
  return {
    x: W/2 + wx * scale,
    y: H/2 + wy * scale,
    scale,
  };
}

// === PLAYER ===
function resetPlayer() {
  player = {
    x: 0, y: 0, z: 0,
    vx: 0, vy: 0,
    bankAngle: 0,
    pitchLean: 0,
    trail: [],
  };
}

function updatePlayer() {
  // Input with easing + inertia tail
  inputLX = (keys['ArrowLeft'] || keys['KeyA']) ? -1 : (keys['ArrowRight'] || keys['KeyD']) ? 1 : 0;
  inputLY = (keys['ArrowUp'] || keys['KeyW']) ? -1 : (keys['ArrowDown'] || keys['KeyS']) ? 1 : 0;

  player.vx += inputLX * CFG.MOVE_SPEED;
  player.vy += inputLY * CFG.MOVE_SPEED;
  player.vx *= CFG.INERTIA_TAIL;
  player.vy *= CFG.INERTIA_TAIL;

  // Clamp velocity
  const maxV = CFG.WORLD_HALF_W / (H/2) * 1.5;
  player.vx = Math.max(-maxV, Math.min(maxV, player.vx));
  player.vy = Math.max(-maxV, Math.min(maxV, player.vy));

  player.x += player.vx * H;
  player.y -= player.vy * H;

  // Clamp position
  player.x = Math.max(-CFG.WORLD_HALF_W, Math.min(CFG.WORLD_HALF_W, player.x));
  player.y = Math.max(-CFG.WORLD_HALF_H, Math.min(CFG.WORLD_HALF_H, player.y));

  // Bank angle from lateral velocity
  const targetBank = -player.vx * CFG.BANK_ANGLE;
  player.bankAngle += (targetBank - player.bankAngle) * 0.25;

  // Pitch lean from vertical velocity
  const targetPitch = player.vy * CFG.PITCH_LEAN;
  player.pitchLean += (targetPitch - player.pitchLean) * 0.25;

  // Trail
  if (frameCount % 2 === 0) {
    player.trail.push({ x: player.x, y: player.y, life: 15 });
    if (player.trail.length > 30) player.trail.shift();
  }
  for (const t of player.trail) t.life--;
  player.trail = player.trail.filter(t => t.life > 0);

  // Thrust particles
  if (frameCount % 2 === 0) {
    const px = W/2 + player.x * 0.8;
    const py = H/2 - player.y * 0.8 + 15;
    spawnParticles(px, py, 2, isBoosting ? [255,200,50] : [100,180,255], 3, 12);
  }
}

function drawPlayer() {
  const px = W/2 + player.x * 0.8;
  const py = H/2 - player.y * 0.8;

  ctx.save();
  ctx.translate(px, py);
  ctx.rotate(player.bankAngle);

  // Glow
  const glowGrad = ctx.createRadialGradient(0, 0, 5, 0, 0, 40);
  glowGrad.addColorStop(0, 'rgba(100,200,255,0.3)');
  glowGrad.addColorStop(1, 'rgba(100,200,255,0)');
  ctx.fillStyle = glowGrad;
  ctx.fillRect(-40, -40, 80, 80);

  // Craft body (low-poly triangle shape)
  const w = CFG.PLAYER_WIDTH;
  const h = CFG.PLAYER_HEIGHT;

  // Main body
  ctx.fillStyle = '#1a3a5c';
  ctx.beginPath();
  ctx.moveTo(0, -h);
  ctx.lineTo(-w/2, h * 0.6);
  ctx.lineTo(-w/4, h);
  ctx.lineTo(w/4, h);
  ctx.lineTo(w/2, h * 0.6);
  ctx.closePath();
  ctx.fill();

  // Highlight
  ctx.fillStyle = '#3a7abd';
  ctx.beginPath();
  ctx.moveTo(0, -h);
  ctx.lineTo(-w/4, h * 0.3);
  ctx.lineTo(0, h * 0.5);
  ctx.closePath();
  ctx.fill();

  // Cockpit
  ctx.fillStyle = '#88ccff';
  ctx.beginPath();
  ctx.moveTo(0, -h + 2);
  ctx.lineTo(-4, 2);
  ctx.lineTo(4, 2);
  ctx.closePath();
  ctx.fill();

  // Wing accents
  ctx.strokeStyle = '#5599cc';
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(-w/2 + 2, h * 0.6);
  ctx.lineTo(0, -h + 4);
  ctx.lineTo(w/2 - 2, h * 0.6);
  ctx.stroke();

  // Engine glow
  const engineGlow = isBoosting ? 1 : 0.5 + Math.sin(frameCount * 0.3) * 0.2;
  ctx.fillStyle = `rgba(100,200,255,${engineGlow})`;
  ctx.beginPath();
  ctx.moveTo(-w/4, h);
  ctx.lineTo(0, h + (isBoosting ? 18 : 8));
  ctx.lineTo(w/4, h);
  ctx.closePath();
  ctx.fill();

  ctx.restore();
}

// === OBSTACLES ===
function spawnObstacle() {
  const types = ['pillar', 'wall', 'spike'];
  const type = types[Math.floor(rng() * types.length)];
  const laneX = (Math.floor(rng() * 5) - 2) * CFG.LANE_WIDTH;

  let w, h;
  if (type === 'pillar') {
    w = 30 + rng() * 40;
    h = 60 + rng() * 100;
  } else if (type === 'wall') {
    w = 80 + rng() * 120;
    h = 30 + rng() * 50;
  } else {
    w = 40 + rng() * 60;
    h = 50 + rng() * 70;
  }

  const colorIdx = Math.floor(rng() * CFG.OBSTACLE_COLORS.length);

  obstacles.push({
    x: laneX,
    y: (rng() - 0.5) * CFG.WORLD_HALF_H * 0.6,
    z: 1200,
    w, h, type,
    color: CFG.OBSTACLE_COLORS[colorIdx],
    passed: false,
  });

  // Sometimes spawn pickups near obstacles
  if (rng() < 0.4) {
    const pTypes = ['energy', 'token', 'multiplier'];
    const pType = pTypes[Math.floor(rng() * pTypes.length)];
    pickups.push({
      x: laneX + (rng() - 0.5) * 80,
      y: player.y + (rng() - 0.5) * 100,
      z: 1200,
      type: pType,
      bobPhase: rng() * Math.PI * 2,
    });
  }
}

function updateObstacles() {
  const spd = speed * (isBoosting ? CFG.BOOST_SPEED_MULT : 1);

  for (let i = obstacles.length - 1; i >= 0; i--) {
    const o = obstacles[i];
    o.z -= spd;

    // Collision check
    if (o.z < 50 && o.z > -20) {
      const dx = Math.abs(player.x - o.x);
      const dy = Math.abs(player.y - o.y);
      const halfW = o.w / 2 + CFG.PLAYER_WIDTH / 2;
      const halfH = o.h / 2 + CFG.PLAYER_HEIGHT / 2;

      if (dx < halfW && dy < halfH) {
        // Crash!
        gameOver();
        return;
      }

      // Near miss check
      if (!o.passed && dx < CFG.NEAR_MISS_DIST + halfW && dy < halfH * 1.5) {
        o.passed = true;
        score += CFG.NEAR_MISS_BONUS * multiplier;
        nearMissTimer = 10;
        shakeX = (rng() - 0.5) * CFG.SHAKE_NEAR_MISS;
        shakeY = (rng() - 0.5) * CFG.SHAKE_NEAR_MISS;
        sfxNearMiss();
        spawnParticles(W/2 + player.x * 0.8, H/2 - player.y * 0.8, 8, [255,255,100], 4, 15);
      }
    }

    if (o.z < -60) obstacles.splice(i, 1);
  }

  // Pickups
  for (let i = pickups.length - 1; i >= 0; i--) {
    const p = pickups[i];
    p.z -= spd;

    if (p.z < 50 && p.z > -20) {
      const dx = Math.abs(player.x - p.x);
      const dy = Math.abs(player.y - p.y);
      if (dx < 40 && dy < 40) {
        // Collect!
        if (p.type === 'energy') {
          energy = Math.min(CFG.ENERGY_MAX, energy + CFG.ENERGY_PICKUP);
          sfxEnergy();
        } else if (p.type === 'token') {
          score += CFG.TOKEN_VALUE * multiplier;
          sfxPickup();
        } else if (p.type === 'multiplier') {
          multiplier = Math.min(CFG.MULTIPLIER_MAX, multiplier + 1);
          sfxEnergy();
        }
        spawnParticles(W/2 + player.x * 0.8, H/2 - player.y * 0.8, 15, [255,255,200], 6, 20);
        pickups.splice(i, 1);
        continue;
      }
    }

    if (p.z < -60) pickups.splice(i, 1);
  }
}

function drawObstacles() {
  // Sort by z (far to near)
  const all = [...obstacles].sort((a, b) => b.z - a.z);

  for (const o of all) {
    if (o.z < 0 || o.z > 1200) continue;
    const p = project(o.x, o.y, o.z);
    const s = p.scale;
    const w = o.w * s;
    const h = o.h * s;

    // Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.fillRect(p.x - w/2 + 4*s, p.y - h/2 + 4*s, w, h);

    // Main body
    const c = o.color;
    const brightness = Math.max(0.3, 1 - o.z / 1500);
    ctx.fillStyle = `rgb(${Math.floor(c[0]*brightness)},${Math.floor(c[1]*brightness)},${Math.floor(c[2]*brightness)})`;

    if (o.type === 'pillar') {
      // Tall pillar with top face
      ctx.fillRect(p.x - w/2, p.y - h/2, w, h);
      ctx.fillStyle = `rgb(${Math.min(255,c[0]+40)},${Math.min(255,c[1]+40)},${Math.min(255,c[2]+40)})`;
      ctx.fillRect(p.x - w/2 + 2*s, p.y - h/2, w - 4*s, 3*s);
    } else if (o.type === 'wall') {
      ctx.fillRect(p.x - w/2, p.y - h/2, w, h);
      // Edge highlight
      ctx.strokeStyle = `rgba(255,255,255,0.15)`;
      ctx.lineWidth = 1;
      ctx.strokeRect(p.x - w/2, p.y - h/2, w, h);
    } else {
      // Spike (triangle)
      ctx.beginPath();
      ctx.moveTo(p.x, p.y - h/2);
      ctx.lineTo(p.x - w/2, p.y + h/2);
      ctx.lineTo(p.x + w/2, p.y + h/2);
      ctx.closePath();
      ctx.fill();
    }

    // Glow edge at close range
    if (o.z < 300) {
      ctx.strokeStyle = `rgba(${c[0]},${c[1]},${c[2]},${(1 - o.z/300)*0.5})`;
      ctx.lineWidth = 2;
      ctx.strokeRect(p.x - w/2, p.y - h/2, w, h);
    }
  }

  // Draw pickups
  for (const pk of pickups) {
    if (pk.z < 0 || pk.z > 1200) continue;
    const p = project(pk.x, pk.y + Math.sin(frameCount * 0.08 + pk.bobPhase) * 15, pk.z);
    const s = p.scale;
    const size = 12 * s;

    let color;
    if (pk.type === 'energy') color = [100, 255, 150];
    else if (pk.type === 'token') color = [255, 220, 80];
    else color = [255, 100, 255];

    // Glow
    const glowGrad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, size * 2);
    glowGrad.addColorStop(0, `rgba(${color[0]},${color[1]},${color[2]},0.4)`);
    glowGrad.addColorStop(1, `rgba(${color[0]},${color[1]},${color[2]},0)`);
    ctx.fillStyle = glowGrad;
    ctx.fillRect(p.x - size*2, p.y - size*2, size*4, size*4);

    // Diamond shape
    ctx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
    ctx.beginPath();
    ctx.moveTo(p.x, p.y - size);
    ctx.lineTo(p.x + size * 0.7, p.y);
    ctx.lineTo(p.x, p.y + size);
    ctx.lineTo(p.x - size * 0.7, p.y);
    ctx.closePath();
    ctx.fill();

    // Inner highlight
    ctx.fillStyle = `rgba(255,255,255,0.5)`;
    ctx.beginPath();
    ctx.moveTo(p.x, p.y - size * 0.5);
    ctx.lineTo(p.x + size * 0.3, p.y);
    ctx.lineTo(p.x, p.y + size * 0.5);
    ctx.lineTo(p.x - size * 0.3, p.y);
    ctx.closePath();
    ctx.fill();
  }
}

// === SUN & SKY ===
function drawSky() {
  const sunScreenY = H * (1 - sunY);
  const sunX = W * 0.7;

  // Sky gradient
  const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
  const t = Math.max(0, sunY);
  const r1 = CFG.SKY_TOP[0] + (30 - CFG.SKY_TOP[0]) * t;
  const g1 = CFG.SKY_TOP[1] + (20 - CFG.SKY_TOP[1]) * t;
  const b1 = CFG.SKY_TOP[2] + (50 - CFG.SKY_TOP[2]) * t;
  skyGrad.addColorStop(0, `rgb(${r1},${g1},${b1})`);
  skyGrad.addColorStop(0.5, `rgb(${CFG.SKY_MID[0]},${CFG.SKY_MID[1]},${CFG.SKY_MID[2]})`);
  skyGrad.addColorStop(1, `rgb(${CFG.SKY_BOT[0]},${CFG.SKY_BOT[1]},${CFG.SKY_BOT[2]})`);
  ctx.fillStyle = skyGrad;
  ctx.fillRect(0, 0, W, H);

  // Sun glow
  const sunGlowSize = Math.max(30, 150 - sunY * 200);
  const sunGlow = ctx.createRadialGradient(sunX, sunScreenY, 0, sunX, sunScreenY, sunGlowSize);
  sunGlow.addColorStop(0, `rgba(${CFG.SUN_COLOR[0]},${CFG.SUN_COLOR[1]},${CFG.SUN_COLOR[2]},0.8)`);
  sunGlow.addColorStop(0.3, `rgba(${CFG.SUN_COLOR[0]},${CFG.SUN_COLOR[1]},${CFG.SUN_COLOR[2]},0.3)`);
  sunGlow.addColorStop(1, 'rgba(255,200,100,0)');
  ctx.fillStyle = sunGlow;
  ctx.fillRect(sunX - sunGlowSize, sunScreenY - sunGlowSize, sunGlowSize*2, sunGlowSize*2);

  // Sun disc
  const sunRadius = Math.max(8, 40 - sunY * 60);
  ctx.fillStyle = `rgb(${CFG.SUN_COLOR[0]},${CFG.SUN_COLOR[1]},${CFG.SUN_COLOR[2]})`;
  ctx.beginPath();
  ctx.arc(sunX, sunScreenY, sunRadius, 0, Math.PI * 2);
  ctx.fill();

  // Horizon line
  const horizonY = H * (1 - sunY + 0.15);
  ctx.strokeStyle = `rgba(${CFG.SUN_COLOR[0]},${CFG.SUN_COLOR[1]},${CFG.SUN_COLOR[2]},0.3)`;
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(0, horizonY);
  ctx.lineTo(W, horizonY);
  ctx.stroke();

  // Ground plane (perspective grid)
  const groundTop = H * 0.55;
  const groundGrad = ctx.createLinearGradient(0, groundTop, 0, H);
  groundGrad.addColorStop(0, `rgba(${CFG.GROUND_COLOR[0]},${CFG.GROUND_COLOR[1]},${CFG.GROUND_COLOR[2]},0.8)`);
  groundGrad.addColorStop(1, `rgb(${CFG.GROUND_COLOR[0]},${CFG.GROUND_COLOR[1]},${CFG.GROUND_COLOR[2]})`);
  ctx.fillStyle = groundGrad;
  ctx.fillRect(0, groundTop, W, H - groundTop);

  // Grid lines on ground
  const gridAlpha = Math.max(0.05, 0.15 - sunY * 0.2);
  ctx.strokeStyle = `rgba(100,150,200,${gridAlpha})`;
  ctx.lineWidth = 0.5;

  // Horizontal lines (perspective)
  for (let i = 0; i < 15; i++) {
    const t = i / 15;
    const y = groundTop + (H - groundTop) * Math.pow(t, 1.5);
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(W, y);
    ctx.stroke();
  }

  // Vertical lines converging to vanishing point
  const vanishX = W * 0.7;
  for (let i = -10; i <= 10; i++) {
    const xSpread = i * W / 8;
    ctx.beginPath();
    ctx.moveTo(vanishX, groundTop);
    ctx.lineTo(xSpread + vanishX * 2, H);
    ctx.stroke();
  }
}

// === SPEED LINES ===
function updateSpeedLines() {
  if (speed > CFG.SPEED_LINE_THRESHOLD) {
    const intensity = Math.min(1, (speed - CFG.SPEED_LINE_THRESHOLD) / (CFG.MAX_SPEED - CFG.SPEED_LINE_THRESHOLD));
    for (let i = 0; i < Math.floor(intensity * 4); i++) {
      speedLines.push({
        x: rng() * W,
        y: H * 0.3 + rng() * H * 0.5,
        len: 20 + rng() * 60,
        life: 15 + rng() * 15,
        maxLife: 15 + rng() * 15,
      });
    }
  }
  for (let i = speedLines.length - 1; i >= 0; i--) {
    speedLines[i].life--;
    if (speedLines[i].life <= 0) speedLines.splice(i, 1);
  }
}

function drawSpeedLines() {
  for (const sl of speedLines) {
    const a = sl.life / sl.maxLife;
    ctx.strokeStyle = `rgba(255,255,255,${a * 0.4})`;
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(sl.x, sl.y);
    ctx.lineTo(sl.x - player.vx * 30, sl.y + sl.len);
    ctx.stroke();
  }
}

// === CHROMATIC ABERRATION (simulated) ===
function drawChromaticAberration() {
  if (speed < CFG.SPEED_LINE_THRESHOLD) return;
  const intensity = Math.min(4, (speed - CFG.SPEED_LINE_THRESHOLD) / (CFG.MAX_SPEED - CFG.SPEED_LINE_THRESHOLD) * 4);

  // Draw offset colored edges for the player craft area
  const px = W/2 + player.x * 0.8;
  const py = H/2 - player.y * 0.8;

  ctx.globalCompositeOperation = 'screen';
  ctx.fillStyle = `rgba(255,0,0,${intensity * 0.03})`;
  ctx.fillRect(px - intensity - CFG.PLAYER_WIDTH, py - CFG.PLAYER_HEIGHT, CFG.PLAYER_WIDTH*2, CFG.PLAYER_HEIGHT*2);
  ctx.fillStyle = `rgba(0,0,255,${intensity * 0.03})`;
  ctx.fillRect(px + intensity - CFG.PLAYER_WIDTH, py - CFG.PLAYER_HEIGHT, CFG.PLAYER_WIDTH*2, CFG.PLAYER_HEIGHT*2);
  ctx.globalCompositeOperation = 'source-over';
}

// === HUD ===
function drawHUD() {
  const pad = 20;
  const barW = 180;
  const barH = 14;

  // Distance
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 16px monospace';
  ctx.textAlign = 'left';
  ctx.fillText(`DIST ${Math.floor(distance)}m`, pad, pad + 20);

  // Score
  ctx.font = 'bold 24px monospace';
  ctx.fillStyle = '#ffd700';
  ctx.fillText(`${score}`, pad, pad + 50);

  // Multiplier
  if (multiplier > 1) {
    ctx.fillStyle = `hsl(${(frameCount * 3) % 360},80%,70%)`;
    ctx.font = 'bold 18px monospace';
    ctx.fillText(`x${multiplier}`, pad + 80, pad + 50);
  }

  // Speed
  const spdDisplay = Math.floor(speed * (isBoosting ? CFG.BOOST_SPEED_MULT : 1) * 10);
  ctx.fillStyle = '#aaa';
  ctx.font = '14px monospace';
  ctx.fillText(`${spdDisplay} km/h`, pad, pad + 72);

  // Energy bar
  const ey = pad + 85;
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
  ctx.fillRect(pad, ey, barW, barH);
  const energyPct = energy / CFG.ENERGY_MAX;
  const eColor = energyPct > 0.3 ? [80, 200, 120] : [255, 80, 60];
  ctx.fillStyle = `rgb(${eColor[0]},${eColor[1]},${eColor[2]})`;
  ctx.fillRect(pad, ey, barW * energyPct, barH);
  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
  ctx.lineWidth = 1;
  ctx.strokeRect(pad, ey, barW, barH);
  ctx.fillStyle = '#fff';
  ctx.font = '10px monospace';
  ctx.fillText('ENERGY', pad + 4, ey + 11);

  // Boost bar
  const by = ey + barH + 6;
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
  ctx.fillRect(pad, by, barW * 0.7, barH - 2);
  ctx.fillStyle = `rgb(${100 + boostEnergy*1.55},${150 + boostEnergy*0.5},${255})`;
  ctx.fillRect(pad, by, barW * 0.7 * (boostEnergy / 100), barH - 2);
  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
  ctx.strokeRect(pad, by, barW * 0.7, barH - 2);
  ctx.fillStyle = '#fff';
  ctx.font = '10px monospace';
  ctx.fillText('BOOST [SPACE]', pad + 4, by + 9);

  // Sun warning
  if (sunY > 0.6) {
    const warnAlpha = Math.sin(frameCount * 0.15) * 0.3 + 0.3;
    ctx.fillStyle = `rgba(255,50,50,${warnAlpha})`;
    ctx.font = 'bold 14px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('⚠ SUN SETTING ⚠', W/2, pad + 30);
    ctx.textAlign = 'left';
  }

  // Near miss flash text
  if (nearMissTimer > 0) {
    const a = nearMissTimer / 10;
    ctx.fillStyle = `rgba(255,255,100,${a})`;
    ctx.font = 'bold 20px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('NEAR MISS!', W/2, H * 0.3);
    ctx.textAlign = 'left';
  }

  // High score
  ctx.fillStyle = 'rgba(255,255,255,0.4)';
  ctx.font = '12px monospace';
  ctx.textAlign = 'right';
  ctx.fillText(`HI ${highScore}`, W - pad, pad + 16);
  ctx.textAlign = 'left';
}

// === SCREEN SHAKE ===
function updateShake() {
  shakeX *= CFG.SHAKE_DECAY;
  shakeY *= CFG.SHAKE_DECAY;
  if (Math.abs(shakeX) < 0.1) shakeX = 0;
  if (Math.abs(shakeY) < 0.1) shakeY = 0;
}

// === GAME STATE ===
function startGame() {
  state = 'playing';
  distance = 0;
  score = 0;
  multiplier = 1;
  energy = CFG.ENERGY_MAX;
  boostEnergy = 100;
  speed = CFG.BASE_SPEED;
  sunY = CFG.SUN_START_Y;
  frameCount = 0;
  obstacles = [];
  pickups = [];
  particles = [];
  speedLines = [];
  shakeX = 0; shakeY = 0;
  flashAlpha = 0;
  nearMissTimer = 0;
  rngState = seed;
  resetPlayer();
  initAudio();
}

function gameOver() {
  state = 'gameover';
  sfxCrash();
  spawnParticles(W/2, H/2, 50, [255,100,50], 8, 40);
  spawnParticles(W/2, H/2, 30, [255,200,50], 6, 30);
  flashAlpha = 1;

  if (score > highScore) {
    highScore = score;
    localStorage.setItem('solarRunnerHS', highScore);
  }

  if (droneOsc) {
    droneGain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
  }
}

// === TITLE SCREEN ===
function drawTitle() {
  // Animated background
  const t = frameCount * 0.005;
  ctx.fillStyle = `rgb(${15+Math.sin(t)*10},${10+Math.cos(t*0.7)*8},${40+Math.sin(t*1.3)*15})`;
  ctx.fillRect(0, 0, W, H);

  // Stars
  for (let i = 0; i < 60; i++) {
    const sx = ((i * 137.5 + frameCount * 0.1) % W);
    const sy = ((i * 97.3 + Math.sin(i + t) * 20) % H);
    const sa = 0.3 + Math.sin(frameCount * 0.05 + i) * 0.2;
    ctx.fillStyle = `rgba(255,255,255,${sa})`;
    ctx.fillRect(sx, sy, 1.5, 1.5);
  }

  // Sun on horizon
  const sunYTitle = H * 0.6;
  const sunGlow = ctx.createRadialGradient(W*0.7, sunYTitle, 0, W*0.7, sunYTitle, 120);
  sunGlow.addColorStop(0, 'rgba(255,220,100,0.6)');
  sunGlow.addColorStop(0.5, 'rgba(255,150,50,0.15)');
  sunGlow.addColorStop(1, 'rgba(255,100,30,0)');
  ctx.fillStyle = sunGlow;
  ctx.fillRect(W*0.7-120, sunYTitle-120, 240, 240);

  // Ground
  ctx.fillStyle = '#0a0815';
  ctx.fillRect(0, H * 0.65, W, H * 0.35);

  // Title text
  ctx.textAlign = 'center';
  ctx.fillStyle = '#fff';
  ctx.font = `bold ${Math.min(72, W*0.08)}px monospace`;
  ctx.fillText('SOLAR RUNNER', W/2, H * 0.3);

  // Subtitle
  ctx.font = `${Math.min(18, W*0.025)}px monospace`;
  ctx.fillStyle = 'rgba(255,200,150,0.7)';
  ctx.fillText('Stay in the light. Dodge everything.', W/2, H * 0.38);

  // Controls
  ctx.font = `${Math.min(14, W*0.02)}px monospace`;
  ctx.fillStyle = 'rgba(255,255,255,0.5)';
  const cy = H * 0.5;
  ctx.fillText('WASD / Arrow Keys — Move', W/2, cy);
  ctx.fillText('SPACE — Boost', W/2, cy + 22);

  // Start prompt
  const blinkAlpha = Math.sin(frameCount * 0.08) * 0.4 + 0.6;
  ctx.fillStyle = `rgba(255,255,255,${blinkAlpha})`;
  ctx.font = `bold ${Math.min(22, W*0.03)}px monospace`;
  ctx.fillText('[ PRESS ANY KEY TO START ]', W/2, H * 0.7);

  // High score
  if (highScore > 0) {
    ctx.fillStyle = 'rgba(255,215,0,0.6)';
    ctx.font = `${Math.min(16, W*0.02)}px monospace`;
    ctx.fillText(`HIGH SCORE: ${highScore}`, W/2, H * 0.78);
  }

  ctx.textAlign = 'left';
}

// === GAME OVER SCREEN ===
function drawGameOver() {
  // Darken
  ctx.fillStyle = `rgba(10,5,20,${Math.min(0.7, flashAlpha * 2)})`;
  ctx.fillRect(0, 0, W, H);

  if (flashAlpha > 0) {
    ctx.fillStyle = `rgba(255,100,50,${flashAlpha * 0.3})`;
    ctx.fillRect(0, 0, W, H);
    flashAlpha *= 0.95;
  }

  ctx.textAlign = 'center';

  // Game Over text
  ctx.fillStyle = '#ff4444';
  ctx.font = `bold ${Math.min(60, W*0.07)}px monospace`;
  ctx.fillText('GAME OVER', W/2, H * 0.3);

  // Stats
  ctx.fillStyle = '#fff';
  ctx.font = `${Math.min(20, W*0.025)}px monospace`;
  ctx.fillText(`Distance: ${Math.floor(distance)}m`, W/2, H * 0.42);
  ctx.fillText(`Score: ${score}`, W/2, H * 0.48);

  if (score >= highScore && score > 0) {
    ctx.fillStyle = '#ffd700';
    ctx.font = `bold ${Math.min(22, W*0.03)}px monospace`;
    ctx.fillText('★ NEW HIGH SCORE ★', W/2, H * 0.56);
  } else {
    ctx.fillStyle = 'rgba(255,215,0,0.6)';
    ctx.font = `${Math.min(16, W*0.02)}px monospace`;
    ctx.fillText(`High Score: ${highScore}`, W/2, H * 0.56);
  }

  // Restart prompt
  const blinkAlpha = Math.sin(frameCount * 0.08) * 0.4 + 0.6;
  ctx.fillStyle = `rgba(255,255,255,${blinkAlpha})`;
  ctx.font = `${Math.min(18, W*0.025)}px monospace`;
  ctx.fillText('[ PRESS R OR CLICK TO RESTART ]', W/2, H * 0.7);

  ctx.textAlign = 'left';
}

// === MAIN UPDATE ===
function update() {
  frameCount++;

  if (state !== 'playing') return;

  const spdMult = isBoosting ? CFG.BOOST_SPEED_MULT : 1;
  const effectiveSpeed = speed * spdMult;

  // Speed ramp
  speed = Math.min(CFG.MAX_SPEED, speed + CFG.SPEED_RAMP);

  // Distance & score
  distance += effectiveSpeed * 0.1;
  score += Math.floor(effectiveSpeed * 0.5) * multiplier;

  // Sun sinks
  sunY += CFG.SUN_SET_RATE * (isBoosting ? 0.7 : 1);
  if (sunY >= 1) {
    gameOver();
    return;
  }

  // Energy drain
  const shadowFactor = Math.max(0, (sunY - 0.3) / 0.5);
  energy -= CFG.ENERGY_DRAIN * (1 + shadowFactor * CFG.ENERGY_SHADOW_MULT);
  if (isBoosting) {
    boostEnergy -= CFG.BOOST_DRAIN;
    if (boostEnergy <= 0) {
      boostEnergy = 0;
      isBoosting = false;
    }
  } else {
    boostEnergy = Math.min(100, boostEnergy + CFG.BOOST_RECHARGE);
  }

  if (energy <= 0) {
    energy = 0;
    gameOver();
    return;
  }

  // Spawn obstacles
  const spawnInterval = Math.max(CFG.SPAWN_MIN, CFG.SPAWN_INTERVAL_BASE - speed * 1.2);
  if (frameCount % Math.floor(spawnInterval) === 0) {
    spawnObstacle();
  }

  updatePlayer();
  updateObstacles();
  updateParticles();
  updateSpeedLines();
  updateShake();

  // Drone pitch follows speed
  if (droneOsc) {
    droneOsc.frequency.value = 50 + speed * 3;
    droneGain.gain.value = 0.04 + speed * 0.002;
  }

  nearMissTimer = Math.max(0, nearMissTimer - 1);
}

// === MAIN DRAW ===
function draw() {
  ctx.clearRect(0, 0, W, H);

  if (state === 'title') {
    drawTitle();
    return;
  }

  // Apply screen shake
  ctx.save();
  ctx.translate(shakeX, shakeY);

  drawSky();
  drawObstacles();
  drawSpeedLines();
  drawPlayer();
  drawParticles();
  drawChromaticAberration();

  ctx.restore();

  drawHUD();

  if (state === 'gameover') {
    drawGameOver();
  }
}

// === GAME LOOP ===
function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

// === INPUT ===
window.addEventListener('keydown', e => {
  keys[e.code] = true;

  if (state === 'title') {
    seed = Date.now();
    startGame();
    return;
  }

  if (state === 'gameover' && (e.code === 'KeyR')) {
    seed = Date.now();
    startGame();
    return;
  }

  // Boost
  if (state === 'playing' && e.code === 'Space') {
    e.preventDefault();
    if (!isBoosting && boostEnergy > 10) {
      isBoosting = true;
      sfxBoost();
    }
  }
});

window.addEventListener('keyup', e => {
  keys[e.code] = false;
  if (e.code === 'Space') isBoosting = false;
});

canvas.addEventListener('click', () => {
  if (state === 'title') {
    seed = Date.now();
    startGame();
  } else if (state === 'gameover') {
    seed = Date.now();
    startGame();
  }
});

// Prevent scrolling
window.addEventListener('keydown', e => {
  if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) {
    e.preventDefault();
  }
});

// === START ===
loop();
</script>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"351c9526-58cd-45ce-9fbc-4f7112b47204","tokensIn":944,"tokensOut":11906,"tokensTotal":12850,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T02:36:11.617Z"} -->