← Race the Sun results

Race the Sun

muse spark 1.2 · 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 36,205 bytes · SHA-256 11fb52ddc96c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<title>SOLAR DIVE — Race The Sun</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{width:100%;height:100%;overflow:hidden;background:#090a14;font-family:'Segoe UI',system-ui,Arial,sans-serif}
  #c{display:block;width:100%;height:100%}
  #hud{position:absolute;inset:0;pointer-events:none;overflow:hidden}
  .bar-wrap{position:absolute;top:18px;left:50%;transform:translateX(-50%);display:flex;gap:18px;align-items:center}
  .hud-pill{background:rgba(10,12,24,0.72);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.12);border-radius:999px;padding:8px 14px;display:flex;align-items:center;gap:10px;color:#fff;font-size:12px;letter-spacing:0.08em;font-weight:700}
  .hud-pill b{font-size:16px;letter-spacing:0.02em}
  .energy-track{width:140px;height:8px;background:rgba(255,255,255,0.15);border-radius:99px;overflow:hidden;position:relative}
  .energy-fill{height:100%;width:100%;background:linear-gradient(90deg,#ffcc33,#ff5a2b);border-radius:99px;transition:width 0.1s linear;box-shadow:0 0 12px rgba(255,180,40,0.7)}
  .hud-bottom{position:absolute;bottom:22px;left:20px;right:20px;display:flex;justify-content:space-between;align-items:end;color:#fff}
  .big-score{line-height:1}
  .big-score i{font-style:normal;font-size:11px;letter-spacing:0.2em;opacity:0.7;display:block;margin-bottom:4px}
  .big-score b{font-size:42px;font-weight:900;letter-spacing:-0.03em;text-shadow:0 4px 20px rgba(0,0,0,0.6)}
  .speed-read{ text-align:right}
  .speed-read span{font-size:11px;letter-spacing:0.15em;opacity:0.7}
  .speed-read b{font-size:28px;display:block}
  #speedLines{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity 0.3s}
  #titleScreen,#gameOverScreen{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(600px 400px at 50% 30%, rgba(255,120,40,0.25), transparent 70%), linear-gradient(180deg, rgba(6,8,18,0.55) 0%, rgba(6,8,18,0.92) 100%);pointer-events:auto;transition:opacity 0.4s, visibility 0.4s}
  .hidden{opacity:0!important;visibility:hidden!important;pointer-events:none!important}
  .logo{font-size:56px;font-weight:900;letter-spacing:-0.04em;color:#fff;line-height:0.9;text-align:center}
  .logo em{font-style:normal;background:linear-gradient(90deg,#ffe27a 0%,#ff6b35 60%,#ff2e63 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
  .logo small{display:block;font-size:12px;letter-spacing:0.42em;font-weight:700;color:#ffcc66;margin-top:10px}
  .panel{background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.12);backdrop-filter:blur(12px);border-radius:20px;padding:18px 20px;margin-top:22px;min-width:360px}
  .panel p{color:rgba(255,255,255,0.75);font-size:13px;line-height:1.5;text-align:center}
  .btn{margin-top:16px;width:100%;padding:14px 0;border-radius:14px;border:none;font-weight:900;letter-spacing:0.12em;font-size:14px;cursor:pointer;background:linear-gradient(90deg,#ffe27a,#ff7a2b);color:#1a0d00;box-shadow:0 8px 30px rgba(255,122,43,0.4);transition:transform 0.1s}
  .btn:active{transform:scale(0.97)}
  .controls-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}
  .k{display:flex;align-items:center;gap:8px;color:rgba(255,255,255,0.8);font-size:11px;font-weight:600;letter-spacing:0.06em}
  .k i{width:28px;height:28px;display:grid;place-items:center;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.15);border-radius:7px;font-style:normal;font-size:11px}
  .stats{position:absolute;top:18px;right:18px;display:flex;gap:10px}
  .stat{ background:rgba(10,12,24,0.72);border:1px solid rgba(255,255,255,0.1);border-radius:14px;padding:10px 14px;color:#fff;min-width:92px;text-align:center}
  .stat label{font-size:9px;letter-spacing:0.18em;opacity:0.6;display:block}
  .stat b{font-size:18px}
  #toast{position:absolute;left:50%;top:38%;transform:translateX(-50%);color:#fff;font-weight:900;font-size:28px;letter-spacing:0.08em;text-shadow:0 4px 18px rgba(0,0,0,0.8);opacity:0;pointer-events:none;transition:opacity 0.2s}
  .chroma{filter:drop-shadow(2px 0 0 rgba(255,80,80,0.6)) drop-shadow(-2px 0 0 rgba(80,180,255,0.6))}
  @media(max-width:700px){.logo{font-size:38px}.panel{min-width:300px}.bar-wrap{flex-wrap:wrap;justify-content:center;top:10px}.hud-bottom{bottom:12px}}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="speedLines"></div>
<div id="hud">
  <div class="bar-wrap" id="hudBar">
    <div class="hud-pill"><span style="width:8px;height:8px;border-radius:50%;background:#ff3b30;box-shadow:0 0 8px #ff3b30;display:inline-block"></span> ENERGY <div class="energy-track"><div class="energy-fill" id="energyFill"></div></div> <b id="energyTxt">100%</b></div>
    <div class="hud-pill">✦ <b id="hudScore">0</b> <span id="hudMult" style="background:#ffcc33;color:#1a1000;padding:2px 7px;border-radius:99px;font-size:11px">x1.0</span></div>
    <div class="hud-pill" style="gap:4px">◉ <b id="hudDist">0 m</b></div>
  </div>
  <div class="stats">
    <div class="stat"><label>BEST</label><b id="hudBest">0</b></div>
    <div class="stat"><label>SPEED</label><b id="hudSpeed">0</b></div>
  </div>
  <div class="hud-bottom">
    <div class="big-score"><i>DISTANCE</i><b id="bigDist">0 m</b></div>
    <div class="speed-read"><span>VELOCITY</span><b id="speedNum">220 KM/H</b><div style="width:140px;height:4px;background:rgba(255,255,255,0.15);border-radius:99px;margin-top:6px;overflow:hidden"><div id="speedBar" style="height:100%;width:30%;background:linear-gradient(90deg,#00e5ff,#7cff8a);border-radius:99px"></div></div></div>
  </div>
  <div id="toast">NEAR MISS +50</div>
</div>

<div id="titleScreen">
  <div class="logo">SOLAR <em>DIVE</em><small>— RACE THE SUN —</small></div>
  <div class="panel">
    <p>Stay in the light. The sun is sinking. Dodge monoliths, skim the dunes, grab <span style="color:#ffe27a">⚡ energy</span> & <span style="color:#7df9ff">✦ prisms</span> before night falls.</p>
    <div class="controls-grid">
      <div class="k"><i>W</i><i>A</i><i>S</i><i>D</i> or ARROWS — STRAFE / PITCH</div>
      <div class="k"><i>␣</i> HOLD — AFTERBURNER (costs energy)</div>
      <div class="k"><i>R</i> RESTART</div>
      <div class="k"><i>✦</i> NEAR-MISS = BONUS</div>
    </div>
    <button class="btn" id="playBtn">IGNITE ENGINES — PRESS SPACE</button>
    <p style="margin-top:10px;font-size:11px;opacity:0.6;letter-spacing:0.08em">PROCEDURAL SEED RUN • 60 FPS • JUICY PHYSICS</p>
  </div>
</div>

<div id="gameOverScreen" class="hidden">
  <div class="logo" style="font-size:44px">SUN<span style="color:#ff3b30">SET</span></div>
  <div class="panel" style="text-align:center;min-width:380px">
    <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:14px">
      <div><div style="font-size:10px;letter-spacing:0.15em;opacity:0.6">SCORE</div><b id="goScore" style="font-size:22px;color:#fff">0</b></div>
      <div><div style="font-size:10px;letter-spacing:0.15em;opacity:0.6">DISTANCE</div><b id="goDist" style="font-size:22px;color:#fff">0m</b></div>
      <div><div style="font-size:10px;letter-spacing:0.15em;opacity:0.6">BEST</div><b id="goBest" style="font-size:22px;color:#ffe27a">0</b></div>
    </div>
    <div id="goReason" style="font-size:13px;color:rgba(255,255,255,0.7)">You clipped a monolith at 1,240m</div>
    <button class="btn" id="restartBtn">RETRY — PRESS R</button>
  </div>
</div>

<script>
// === TUNABLE CONSTANTS ===
const TUNING = {
  INITIAL_SPEED: 24,
  MAX_SPEED: 92,
  ACCEL: 0.62,              // speed increase per second factor
  STRAFE_SPEED: 11,         // lateral max
  PITCH_SPEED: 7,
  STRAFE_EASE: 0.18,        // lerp factor per frame (snappy 80-120ms)
  PITCH_EASE: 0.16,
  INERTIA_DAMP: 0.92,       // tail inertia
  BOOST_MULT: 1.75,
  BOOST_DRAIN: 22,          // per sec
  BASE_ENERGY_DRAIN: 2.8,   // per sec
  SHADOW_DRAIN_MULT: 3.2,
  ENERGY_PICKUP: 28,
  SCORE_PICKUP: 120,
  MULT_ORB_BONUS: 0.25,
  SUN_DURATION: 95,         // seconds till sunset
  SPAWN_INTERVAL: 1.05,     // seconds between waves
  OBSTACLE_DENSITY: 0.88,
  NEAR_MISS_DIST: 1.15,
  NEAR_MISS_BONUS: 55,
  SHAKE_NEAR: 6,
  SHAKE_CRASH: 18,
  SPEEDLINE_THRESH: 48,
  FOV: 420,
  CAM_Y: 2.15,
  CAM_Z: -6.2,
  HORIZON_RATIO: 0.44,
  PLAYER_LIMIT_X: 3.4,
  PLAYER_LIMIT_Y: [0.7, 3.6],
};

// === SETUP ===
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H, CX, horizonY;
function resize(){ W=canvas.width=innerWidth*devicePixelRatio; H=canvas.height=innerHeight*devicePixelRatio; canvas.style.width=innerWidth+'px'; canvas.style.height=innerHeight+'px'; CX=W/2; horizonY=H*TUNING.HORIZON_RATIO; }
addEventListener('resize', resize); resize();

// deterministic seed
let seed = (Date.now() % 2147483647) >>>0;
function rnd(){ seed = (seed * 16807) % 2147483647; return (seed-1)/2147483646; }
function randRange(a,b){ return a + rnd()*(b-a); }
function choice(arr){ return arr[Math.floor(rnd()*arr.length)]; }

// audio via WebAudio
let actx;
function ensureAudio(){ if(!actx) actx = new (window.AudioContext||window.webkitAudioContext)(); if(actx.state==='suspended') actx.resume(); }
function tone(freq, dur, type='sine', gain=0.2, slide=0){
  if(!actx) return;
  const o=actx.createOscillator(), g=actx.createGain(), f=actx.createBiquadFilter();
  o.type=type; o.frequency.value=freq;
  if(slide) o.frequency.exponentialRampToValueAtTime(freq*slide, actx.currentTime+dur);
  g.gain.value=gain; g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime+dur);
  o.connect(f); f.connect(g); g.connect(actx.destination);
  o.start(); o.stop(actx.currentTime+dur);
}
function playPickup(t){ if(t==='energy'){ tone(880,0.12,'sine',0.25); setTimeout(()=>tone(1320,0.12,'sine',0.2),80);} else if(t==='score'){ tone(600,0.09,'square',0.14,1.5);} else if(t==='orb'){ tone(400,0.2,'triangle',0.22,2.2);}}
function playCrash(){ tone(120,0.4,'sawtooth',0.4,0.5); tone(60,0.5,'square',0.3); }
function playNear(){ tone(1200,0.08,'sine',0.2); }

// game state
const STATE={ TITLE:0, PLAYING:1, GAMEOVER:2 };
let gameState=STATE.TITLE;
let time=0, distance=0, score=0, multiplier=1, energy=100, speed=TUNING.INITIAL_SPEED;
let highScore = parseInt(localStorage.getItem('solar_high')||'0');
document.getElementById('hudBest').textContent=highScore;

let player={ x:0, y:1.35, vx:0, vy:0, roll:0, pitch:0, targetX:0, targetY:1.35 };
let keys={};
let shake=0, shakeDecay=0.88;
let sunProgress=0;
let obstacles=[], pickups=[], particles=[], trails=[];
let spawnTimer=0, waveCount=0;
let inShadow=false, boosting=false;
let lastNearMiss=0;

// input
addEventListener('keydown',e=>{
  keys[e.code]=true;
  if(e.code==='Space'){ boosting=true; e.preventDefault(); ensureAudio(); }
  if((e.code==='KeyR' || e.code==='Enter') && gameState===STATE.GAMEOVER) restart();
  if(e.code==='Space' && gameState===STATE.TITLE) startGame();
  if(e.code==='KeyR' && gameState===STATE.TITLE) startGame();
});
addEventListener('keyup',e=>{ keys[e.code]=false; if(e.code==='Space') boosting=false; });
document.getElementById('playBtn').onclick=startGame;
document.getElementById('restartBtn').onclick=restart;

// helpers
function project(x,y,z, camX, camY){
  const dz = z - TUNING.CAM_Z;
  if(dz<=0.12) return null;
  const scale = TUNING.FOV / dz;
  return { x: CX + (x - camX)*scale, y: horizonY + (camY - y)*scale, scale, dz };
}
function spawnWave(zStart){
  waveCount++;
  const pattern = Math.floor(rnd()*5);
  const z = zStart + randRange(0,6);
  // obstacles
  if(pattern===0){ // gate
    const gap = choice([-1.8,0,1.8]);
    [-3,-1.5,1.5,3].forEach(off=>{
      if(Math.abs(off-gap)<1.0) return;
      obstacles.push({x:off, y:0, z:z+randRange(-1,1), w:1.1, h:randRange(1.6,2.8), d:1.2, type:'pillar', hue: rnd()>0.5? 18: 220});
    });
  } else if(pattern===1){ // wall with opening
    const hole = choice([-2,2,0]);
    for(let i=-3;i<=3;i+=1.5){
      if(i===hole) continue;
      obstacles.push({x:i, y:0, z, w:1.25, h:randRange(1.2,2.0), d:0.9, type:'wall'});
    }
  } else if(pattern===2){ // scattered blocks
    const count = 2+Math.floor(rnd()*3);
    for(let i=0;i<count;i++){
      obstacles.push({x:randRange(-3.2,3.2), y:0, z:z+randRange(-2,2), w:randRange(0.9,1.4), h:randRange(1.0,2.6), d:0.9, type:'block'});
    }
  } else if(pattern===3){ // floating shards needing pitch
    for(let i=0;i<2;i++){
      obstacles.push({x:randRange(-2.5,2.5), y:randRange(1.8,2.8), z:z+randRange(0,3), w:1.6, h:0.7, d:2.2, type:'shard'});
    }
    obstacles.push({x:choice([-2.2,2.2]), y:0, z:z, w:1.0, h:1.7, d:1});
  } else { // canyon
    obstacles.push({x:-3.6, y:0, z, w:1.5, h:3.2, d:8, type:'canyon'});
    obstacles.push({x:3.6, y:0, z, w:1.5, h:3.2, d:8, type:'canyon'});
  }
  // pickups in wave
  const pickCount = 1 + Math.floor(rnd()*2);
  for(let i=0;i<pickCount;i++){
    const px = randRange(-2.8,2.8);
    const py = rnd()>0.6? randRange(1.2,2.4):0.6;
    const pz = z + randRange(2,8);
    const kind = rnd()<0.42? 'energy' : rnd()<0.72? 'score' : 'orb';
    pickups.push({x:px,y:py,z:pz, kind, taken:false, rot:rnd()*Math.PI, pulse:rnd()*Math.PI*2});
  }
  // occasional ramp visual is just ground color stripe handled in render
}

function resetWorld(){
  seed = (Date.now() % 2147483647) ^ 0x9e3779b9;
  obstacles=[]; pickups=[]; particles=[]; trails=[];
  for(let i=0;i<10;i++) spawnWave(18 + i*14);
  spawnTimer=0; waveCount=0;
}

function startGame(){
  ensureAudio();
  gameState=STATE.PLAYING;
  document.getElementById('titleScreen').classList.add('hidden');
  document.getElementById('gameOverScreen').classList.add('hidden');
  time=0; distance=0; score=0; multiplier=1; energy=100; speed=TUNING.INITIAL_SPEED;
  sunProgress=0; shake=0; player.x=0; player.y=1.35; player.vx=0; player.vy=0; player.roll=0; player.pitch=0;
  resetWorld();
  tone(220,0.3,'triangle',0.25,2);
}
function restart(){ startGame(); }

function triggerShake(v){ shake = Math.max(shake, v); }
function showToast(txt){
  const el=document.getElementById('toast'); el.textContent=txt; el.style.opacity=1;
  setTimeout(()=>el.style.opacity=0,420);
}

// === UPDATE ===
let lastT=performance.now();
function update(dt){
  if(gameState!==STATE.PLAYING) return;
  time+=dt;
  // speed ramp: logistic-ish
  const targetSpeed = TUNING.INITIAL_SPEED + Math.min(TUNING.MAX_SPEED - TUNING.INITIAL_SPEED, time * TUNING.ACCEL * (1 + time*0.008));
  speed += (targetSpeed - speed)*0.02;
  const effectiveSpeed = speed * (boosting && energy>0 ? TUNING.BOOST_MULT : 1);
  distance += effectiveSpeed * dt;

  // input -> target
  let ix=0, iy=0;
  if(keys['ArrowLeft']||keys['KeyA']) ix-=1;
  if(keys['ArrowRight']||keys['KeyD']) ix+=1;
  if(keys['ArrowUp']||keys['KeyW']) iy+=1;
  if(keys['ArrowDown']||keys['KeyS']) iy-=1;
  // target position
  player.targetX += ix * TUNING.STRAFE_SPEED * dt * 0.9;
  player.targetY += iy * TUNING.PITCH_SPEED * dt * 0.7;
  player.targetX = Math.max(-TUNING.PLAYER_LIMIT_X, Math.min(TUNING.PLAYER_LIMIT_X, player.targetX));
  player.targetY = Math.max(TUNING.PLAYER_LIMIT_Y[0], Math.min(TUNING.PLAYER_LIMIT_Y[1], player.targetY));
  // decay target to center slightly when no input for inertia tail feel
  if(ix===0) player.targetX *= Math.pow(0.96, dt*60);
  if(iy===0) player.targetY += (1.35 - player.targetY)*0.02;

  // snappy easing with inertia
  const easeX = 1 - Math.pow(1 - TUNING.STRAFE_EASE, dt*60);
  const easeY = 1 - Math.pow(1 - TUNING.PITCH_EASE, dt*60);
  const prevX = player.x, prevY=player.y;
  player.x += (player.targetX - player.x)*easeX;
  player.y += (player.targetY - player.y)*easeY;
  // velocity for roll/pitch
  player.vx = (player.x - prevX)/(dt||0.016);
  player.vy = (player.y - prevY)/(dt||0.016);
  player.roll = player.vx * 0.08;
  player.roll = Math.max(-0.75, Math.min(0.75, player.roll));
  player.pitch = player.vy * 0.06;

  // clamp player inside world during update (already)
  // energy & sun
  // shadow check: if any obstacle within shadow cone behind it and player behind it low
  inShadow=false;
  for(let o of obstacles){
    if(o.z < 4 && o.z > -1.5 && Math.abs(o.x - player.x)< o.w*0.9 && player.y < o.h + 0.4) {
      // also check if sun still high: shadow stronger when sun low? invert
      if(o.h>1.4) { inShadow=true; break; }
    }
  }
  let drain = TUNING.BASE_ENERGY_DRAIN * (inShadow ? TUNING.SHADOW_DRAIN_MULT : 1);
  if(boosting && energy>0) drain += TUNING.BOOST_DRAIN;
  // sun covered? faster drain when sun low
  drain *= (1 + sunProgress*0.6);
  energy -= drain * dt;
  if(boosting && energy<=0){ energy=0; boosting=false; }
  energy = Math.max(0, Math.min(100, energy));
  if(energy<=0){
    gameOver('Systems blackout — energy depleted');
    return;
  }
  sunProgress = Math.min(1, time / TUNING.SUN_DURATION);
  if(sunProgress>=1){
    gameOver('The sun has set. Night consumes you');
    return;
  }

  // spawn waves
  spawnTimer+= dt * (effectiveSpeed/22);
  if(spawnTimer > TUNING.SPAWN_INTERVAL){
    spawnTimer=0;
    const furthest = Math.max(...obstacles.map(o=>o.z), ...pickups.map(p=>p.z), 0);
    spawnWave(furthest + 14 + randRange(0,4));
  }
  // move world
  const move = effectiveSpeed * dt;
  obstacles.forEach(o=> o.z -= move);
  pickups.forEach(p=> p.z -= move);
  // cull behind
  obstacles = obstacles.filter(o=> o.z > -10);
  pickups = pickups.filter(p=> p.z > -10 && !p.taken);

  // collisions
  const pcx=player.x, pcy=player.y, pcz=0;
  const pw=1.0, ph=0.55; // craft size
  let collided=false;
  for(let o of obstacles){
    if(o.z < 1.1 && o.z > -0.9){
      const dx = Math.abs(pcx - o.x);
      const dy = pcy - o.h/2; // approximate? check y overlap
      const yOverlap = (pcy - ph/2 < o.h && pcy + ph/2 > (o.type==='shard'? o.y- o.h/2:0));
      const xOverlap = dx < (pw/2 + o.w/2);
      if(xOverlap && yOverlap){
        // For shard floating, need y check differently
        if(o.type==='shard'){
          const shardYOverlap = Math.abs(pcy - o.y) < (ph/2 + o.h/2);
          if(shardYOverlap && xOverlap) collided=true;
        } else {
          collided=true;
        }
        if(collided){ playCrash(); triggerShake(TUNING.SHAKE_CRASH); createExplosion(pcx,pcy); gameOver('Hull breach — you slammed a monolith'); return; }
      } else if(!yOverlap && !collided){
        // near miss detection
        const dist = Math.hypot(dx - (pw/2+o.w/2), 0);
        if(dist < TUNING.NEAR_MISS_DIST && dist> -0.2 && time - lastNearMiss > 0.25){
          lastNearMiss=time;
          score += TUNING.NEAR_MISS_BONUS * multiplier;
          multiplier = Math.min(5, multiplier + 0.04);
          showToast('NEAR MISS +'+Math.round(TUNING.NEAR_MISS_BONUS*multiplier));
          triggerShake(TUNING.SHAKE_NEAR);
          playNear();
          spawnNearParticles(o);
        }
      }
    }
  }
  // pickups
  for(let p of pickups){
    if(p.taken) continue;
    if(p.z < 0.9 && p.z > -0.7 && Math.abs(p.x - pcx)<1.0 && Math.abs(p.y - pcy)<1.0){
      p.taken=true;
      if(p.kind==='energy'){ energy = Math.min(100, energy + TUNING.ENERGY_PICKUP); showToast('⚡ +ENERGY'); }
      else if(p.kind==='score'){ score += TUNING.SCORE_PICKUP * multiplier; showToast('✦ +'+Math.round(TUNING.SCORE_PICKUP*multiplier)); }
      else if(p.kind==='orb'){ multiplier = Math.min(5, multiplier + TUNING.MULT_ORB_BONUS); showToast('MULTIPLIER x'+multiplier.toFixed(1)); }
      playPickup(p.kind);
      for(let i=0;i<12;i++) particles.push({x:pcx+randRange(-0.3,0.3), y:pcy+randRange(-0.3,0.3), z:0, vx:randRange(-2,2), vy:randRange(-1,2), life:0.4, max:0.4, col:p.kind});
      // remove
    }
    p.rot+= dt*2.5;
    p.pulse+= dt*6;
  }
  pickups = pickups.filter(p=>!p.taken || p.z<-5);

  // slight multiplier decay
  multiplier = Math.max(1, multiplier - dt*0.015);

  // score from distance
  score += effectiveSpeed * dt * 1.8 * multiplier;

  // particles update
  for(let pt of particles){
    pt.x += pt.vx*dt; pt.y += pt.vy*dt; pt.vy -= 1.2*dt; pt.life-=dt;
  }
  particles = particles.filter(p=>p.life>0);
  trails.push({x:player.x, y:player.y, life:0.5});
  if(trails.length>18) trails.shift();
  trails.forEach(t=> t.life-=dt*1.2);
  trails = trails.filter(t=>t.life>0);

  // shake decay
  shake *= Math.pow(shakeDecay, dt*60);
  if(shake<0.1) shake=0;
}

function createExplosion(x,y){
  for(let i=0;i<28;i++) particles.push({x:x+randRange(-0.5,0.5), y:y+randRange(-0.5,0.5), z:0, vx:randRange(-5,5), vy:randRange(-2,5), life:randRange(0.3,0.7), max:0.7, col:'crash'});
}
function spawnNearParticles(o){
  for(let i=0;i<8;i++) particles.push({x:o.x, y:o.h*0.5, z:o.z, vx:randRange(-1,1), vy:randRange(0,1.5), life:0.35, max:0.35, col:'near'});
}

function gameOver(reason){
  gameState=STATE.GAMEOVER;
  document.getElementById('gameOverScreen').classList.remove('hidden');
  document.getElementById('goScore').textContent=Math.floor(score).toLocaleString();
  document.getElementById('goDist').textContent=Math.floor(distance)+'m';
  if(score>highScore){ highScore=Math.floor(score); localStorage.setItem('solar_high', highScore); }
  document.getElementById('goBest').textContent=highScore.toLocaleString();
  document.getElementById('hudBest').textContent=highScore;
  document.getElementById('goReason').textContent=reason;
}

// === RENDER ===
function drawSky(){
  // gradient
  const g = ctx.createLinearGradient(0,0,0,horizonY);
  g.addColorStop(0,'#05102a');
  g.addColorStop(0.35,'#1a2a6a');
  g.addColorStop(0.65,'#b14a68');
  g.addColorStop(0.85,'#ff7a3d');
  g.addColorStop(1,'#ffb86b');
  ctx.fillStyle=g;
  ctx.fillRect(0,0,W,H);
  // sun
  const sunX = W*0.72;
  const sunBaseY = H*0.28;
  const sunY = sunBaseY + sunProgress * (horizonY - sunBaseY + 40);
  const sunR = 54*devicePixelRatio;
  // glow
  const glow = ctx.createRadialGradient(sunX,sunY, sunR*0.5, sunX,sunY, sunR*3);
  glow.addColorStop(0,'rgba(255,220,120,0.9)');
  glow.addColorStop(0.4,'rgba(255,120,40,0.35)');
  glow.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=glow;
  ctx.beginPath(); ctx.arc(sunX,sunY,sunR*3,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='rgba(255,245,180,0.95)';
  ctx.beginPath(); ctx.arc(sunX,sunY,sunR,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='rgba(255,255,255,0.9)';
  ctx.beginPath(); ctx.arc(sunX- sunR*0.25, sunY- sunR*0.2, sunR*0.18,0,Math.PI*2); ctx.fill();
  // horizon haze
  ctx.fillStyle='rgba(255,170,80,0.18)';
  ctx.fillRect(0,horizonY- 30*devicePixelRatio, W, 30*devicePixelRatio);
  // subtle stars / clouds
  ctx.fillStyle='rgba(255,255,255,0.05)';
  for(let i=0;i<18;i++){
    const cx_ = (i* 173 % W);
    const cy_ = (i*47 % (horizonY*0.6));
    ctx.beginPath(); ctx.ellipse(cx_, cy_, 80, 12, 0, 0, Math.PI*2); ctx.fill();
  }
  // sun progress indicator on horizon line
  ctx.strokeStyle='rgba(255,255,255,0.4)'; ctx.lineWidth=1;
  ctx.beginPath(); ctx.moveTo(0,horizonY); ctx.lineTo(W,horizonY); ctx.stroke();
}

function drawGround(camX, camY){
  // ground plane
  ctx.fillStyle='#0e1220';
  ctx.fillRect(0,horizonY,W,H-horizonY);
  // perspective grid
  ctx.strokeStyle='rgba(90,220,255,0.14)';
  ctx.lineWidth=1*devicePixelRatio;
  // longitudinal lines
  for(let xi=-6; xi<=6; xi++){
    const a = project(xi,0,80,camX,camY);
    const b = project(xi,0,-4,camX,camY);
    if(!a||!b) continue;
    ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
  }
  // lateral lines (moving with distance)
  const offset = distance % 6;
  for(let z= -4; z<80; z+=2){
    const zz = z - offset;
    const l = project(-6,0,zz,camX,camY);
    const r = project(6,0,zz,camX,camY);
    if(!l||!r) continue;
    const alpha = Math.max(0, Math.min(1, 1 - zz/70));
    ctx.strokeStyle=`rgba(90,220,255,${0.18*alpha})`;
    ctx.beginPath(); ctx.moveTo(l.x,l.y); ctx.lineTo(r.x,r.y); ctx.stroke();
    // speed stripe center
    if(Math.floor(zz)%6===0){
      const cl = project(-0.35,0,zz,camX,camY);
      const cr = project(0.35,0,zz,camX,camY);
      const cl2 = project(-0.35,0,zz+1.2,camX,camY);
      const cr2 = project(0.35,0,zz+1.2,camX,camY);
      if(cl&&cr&&cl2&&cr2){
        ctx.fillStyle=`rgba(255,220,90,${0.75*alpha})`;
        ctx.beginPath(); ctx.moveTo(cl.x,cl.y); ctx.lineTo(cr.x,cr.y); ctx.lineTo(cr2.x,cr2.y); ctx.lineTo(cl2.x,cl2.y); ctx.closePath(); ctx.fill();
      }
    }
  }
  // side dunes silhouette
  ctx.fillStyle='rgba(22,18,35,0.9)';
  ctx.beginPath();
  ctx.moveTo(0,horizonY);
  for(let x=0;x<W;x+=18){
    const h = Math.sin(x*0.008 + distance*0.02)* 18*devicePixelRatio + Math.cos(x*0.003)*12*devicePixelRatio;
    ctx.lineTo(x, horizonY + h);
  }
  ctx.lineTo(W,H); ctx.lineTo(0,H); ctx.closePath(); ctx.fill();
}

function drawObstacles(camX,camY){
  // sort far to near
  const sorted=[...obstacles].sort((a,b)=>b.z - a.z);
  for(let o of sorted){
    const projFront = project(o.x,0,o.z,camX,camY);
    const projTop = project(o.x,o.h,o.z,camX,camY);
    const projBack = project(o.x,0,o.z+o.d,camX,camY);
    if(!projFront||!projTop) continue;
    const wScale = projFront.scale;
    const halfW = o.w * wScale * 0.5;
    // shadow on ground
    const shA = project(o.x- o.w*0.6,0.02,o.z,camX,camY);
    const shB = project(o.x+ o.w*0.6,0.02,o.z,camX,camY);
    const shC = project(o.x+ o.w*0.6,0.02,o.z+o.d*1.2,camX,camY);
    const shD = project(o.x- o.w*0.6,0.02,o.z+o.d*1.2,camX,camY);
    if(shA&&shB&&shC&&shD){
      ctx.fillStyle='rgba(0,0,0,0.35)';
      ctx.beginPath(); ctx.moveTo(shA.x,shA.y); ctx.lineTo(shB.x,shB.y); ctx.lineTo(shC.x,shC.y); ctx.lineTo(shD.x,shD.y); ctx.closePath(); ctx.fill();
    }
    // front face
    const left = projFront.x - halfW;
    const right = projFront.x + halfW;
    const bottom = projFront.y;
    const top = projTop.y;
    const height = bottom - top;
    if(height<1) continue;
    // is shadowed?
    const isShadowed = inShadow && Math.abs(o.x - player.x)<2;
    // gradient for front
    const grad = ctx.createLinearGradient(left, top, right, bottom);
    if(o.type==='canyon'){
      grad.addColorStop(0,'#1e1b2e'); grad.addColorStop(1,'#3a2a4a');
    } else if(isShadowed){
      grad.addColorStop(0,'#2a1e1e'); grad.addColorStop(1,'#1a0f12');
    } else {
      grad.addColorStop(0,'#ff8a4a'); grad.addColorStop(0.5,'#d9403a'); grad.addColorStop(1,'#4a0f1a');
    }
    ctx.fillStyle=grad;
    ctx.fillRect(left, top, right-left, height);
    // top face
    if(projBack){
      const topBack = project(o.x,o.h,o.z+o.d,camX,camY);
      const frontTopY = top;
      const backTopY = topBack? topBack.y: top-6;
      const backHalfW = o.w * (projBack.scale)*0.5;
      ctx.fillStyle = isShadowed? 'rgba(80,40,40,0.9)' : 'rgba(255,190,120,0.95)';
      ctx.beginPath();
      ctx.moveTo(left, top);
      ctx.lineTo(right, top);
      ctx.lineTo(projBack.x + backHalfW, backTopY);
      ctx.lineTo(projBack.x - backHalfW, backTopY);
      ctx.closePath(); ctx.fill();
      // side face
      ctx.fillStyle='rgba(0,0,0,0.28)';
      ctx.beginPath();
      ctx.moveTo(right, top); ctx.lineTo(projBack.x+backHalfW, backTopY); ctx.lineTo(projBack.x+backHalfW, projBack.y); ctx.lineTo(right,bottom); ctx.closePath(); ctx.fill();
    }
    // edge highlight
    ctx.strokeStyle='rgba(255,255,255,0.18)'; ctx.lineWidth=1*devicePixelRatio; ctx.strokeRect(left,top,right-left,height);
    // glowing edge when close
    if(o.z<6){
      ctx.strokeStyle='rgba(255,120,60,0.5)'; ctx.lineWidth=2*devicePixelRatio; ctx.strokeRect(left,top,right-left,height);
    }
  }
}

function drawPickups(camX,camY){
  for(let p of pickups){
    const pr = project(p.x,p.y,p.z,camX,camY);
    if(!pr) continue;
    const s = pr.scale;
    if(s<0.8) continue;
    ctx.save();
    ctx.translate(pr.x, pr.y);
    const pulse = Math.sin(p.pulse)*0.15 +1;
    const size = (p.kind==='orb'? 22:18)*devicePixelRatio * Math.min(1.4, s/22) * pulse;
    // glow
    ctx.shadowBlur=18; ctx.shadowColor= p.kind==='energy'? '#ffcc33' : p.kind==='score'? '#55e0ff' : '#ff55aa';
    ctx.globalAlpha=0.9;
    if(p.kind==='energy'){
      // diamond
      ctx.fillStyle='#ffe27a';
      ctx.rotate(p.rot);
      ctx.beginPath(); ctx.moveTo(0,-size); ctx.lineTo(size*0.7,0); ctx.lineTo(0,size); ctx.lineTo(-size*0.7,0); ctx.closePath(); ctx.fill();
      ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(-size*0.2,-size*0.2,size*0.18,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='rgba(0,0,0,0.8)'; ctx.font=`${9*devicePixelRatio}px sans-serif`; ctx.textAlign='center'; ctx.fillText('⚡',0,4*devicePixelRatio);
    } else if(p.kind==='score'){
      ctx.fillStyle='#7df9ff';
      ctx.rotate(p.rot*0.6);
      // hexagon
      ctx.beginPath(); for(let i=0;i<6;i++){ const a=i*Math.PI/3; const x=Math.cos(a)*size, y=Math.sin(a)*size; if(i===0)ctx.moveTo(x,y); else ctx.lineTo(x,y);} ctx.closePath(); ctx.fill();
      ctx.fillStyle='#0a1a2a'; ctx.font=`bold ${10*devicePixelRatio}px sans-serif`; ctx.textAlign='center'; ctx.fillText('✦',0,4*devicePixelRatio);
    } else {
      ctx.fillStyle='#ff6bff';
      ctx.beginPath(); ctx.arc(0,0,size,0,Math.PI*2); ctx.fill();
      ctx.strokeStyle='#fff'; ctx.lineWidth=2*devicePixelRatio; ctx.beginPath(); ctx.arc(0,0,size*0.55,0,Math.PI*2); ctx.stroke();
      ctx.fillStyle='#fff'; ctx.font=`bold ${8*devicePixelRatio}px sans-serif`; ctx.textAlign='center'; ctx.fillText('×',0,3*devicePixelRatio);
    }
    ctx.restore();
  }
}

function drawPlayer(camX,camY){
  const pr = project(player.x, player.y, 0, camX, camY);
  if(!pr) return;
  const s = pr.scale;
  ctx.save();
  ctx.translate(pr.x, pr.y);
  ctx.rotate(player.roll*0.35);
  // shadow
  const sh = project(player.x,0.05,0,camX,camY);
  if(sh){
    ctx.fillStyle='rgba(0,0,0,0.35)';
    ctx.beginPath(); ctx.ellipse(sh.x - pr.x, sh.y - pr.y, 28*devicePixelRatio*s/60, 8*devicePixelRatio*s/60, 0,0,Math.PI*2); ctx.fill();
  }
  // craft body
  const scaleF = Math.min(1.2, s/55);
  ctx.scale(scaleF, scaleF);
  // tilt pitch
  ctx.rotate(player.pitch*0.12);
  // thrust flames
  if(boosting){
    ctx.fillStyle='rgba(0,220,255,0.9)';
    ctx.beginPath(); ctx.moveTo(-10, 16); ctx.lineTo(0, 32+ Math.random()*12); ctx.lineTo(10,16); ctx.closePath(); ctx.fill();
    ctx.fillStyle='rgba(255,240,120,0.9)';
    ctx.beginPath(); ctx.moveTo(-6,16); ctx.lineTo(0,28); ctx.lineTo(6,16); ctx.closePath(); ctx.fill();
  } else {
    ctx.fillStyle='rgba(255,120,40,0.7)';
    ctx.beginPath(); ctx.moveTo(-7,14); ctx.lineTo(0,22); ctx.lineTo(7,14); ctx.closePath(); ctx.fill();
  }
  // main hull
  ctx.fillStyle='#eaf2ff';
  ctx.strokeStyle='rgba(0,0,0,0.2)'; ctx.lineWidth=2;
  ctx.beginPath();
  ctx.moveTo(0,-28); // nose
  ctx.lineTo(18,12);
  ctx.lineTo(8,18);
  ctx.lineTo(-8,18);
  ctx.lineTo(-18,12);
  ctx.closePath(); ctx.fill(); ctx.stroke();
  // cockpit
  const grad = ctx.createRadialGradient(0,-6,2,0,-6,14);
  grad.addColorStop(0,'#7df9ff'); grad.addColorStop(1,'#0a1a3a');
  ctx.fillStyle=grad;
  ctx.beginPath(); ctx.ellipse(0,-6,7,9,0,0,Math.PI*2); ctx.fill();
  ctx.fillStyle='rgba(255,255,255,0.85)'; ctx.beginPath(); ctx.ellipse(-2,-9,2.5,3, -0.4,0,Math.PI*2); ctx.fill();
  // wings detail
  ctx.fillStyle='#ffcc33';
  ctx.fillRect(-18,6, 8,3); ctx.fillRect(10,6,8,3);
  // energy glow when low
  if(energy<28){
    ctx.shadowBlur=22; ctx.shadowColor='rgba(255,50,50,0.9)'; ctx.strokeStyle='rgba(255,50,50,0.9)'; ctx.lineWidth=2; ctx.stroke();
  }
  ctx.restore();

  // trail particles
  for(let t of trails){
    const tp = project(t.x, t.y-0.08, 0.2, camX,camY);
    if(!tp) continue;
    ctx.globalAlpha = t.life*0.5;
    ctx.fillStyle= boosting? '#00e5ff' : '#ff9a3c';
    ctx.beginPath(); ctx.arc(tp.x + randRange(-2,2), tp.y, 3*devicePixelRatio,0,Math.PI*2); ctx.fill();
  }
  ctx.globalAlpha=1;
}

function render(){
  // camera follow
  const camX = player.x * 0.28;
  const camY = TUNING.CAM_Y + (player.y -1.35)*0.22;
  // apply shake
  ctx.save();
  if(shake>0.2){
    const sx = (Math.random()-0.5)*shake*2*devicePixelRatio;
    const sy = (Math.random()-0.5)*shake*2*devicePixelRatio;
    ctx.translate(sx,sy);
  }
  drawSky();
  drawGround(camX,camY);
  drawObstacles(camX,camY);
  drawPickups(camX,camY);
  drawPlayer(camX,camY);
  // particles (screen space already)
  for(let pt of particles){
    // project if has z? simplify: draw at player space with scale?
    // we stored x,y in world but for explosion near player, just draw offset from player projection
    const pp = project(pt.x, pt.y, pt.z||0, camX, camY);
    if(!pp) continue;
    const a = pt.life/ (pt.max||0.5);
    ctx.globalAlpha=a;
    const col = pt.col==='crash'? '#ff3b30' : pt.col==='near'? '#ffe27a' : pt.col==='energy'? '#ffcc33' : pt.col==='score'? '#7df9ff' : '#ff55aa';
    ctx.fillStyle=col;
    ctx.beginPath(); ctx.arc(pp.x, pp.y, 4*devicePixelRatio*a,0,Math.PI*2); ctx.fill();
    ctx.shadowBlur=8; ctx.shadowColor=col; ctx.fill();
    ctx.shadowBlur=0;
  }
  ctx.globalAlpha=1;
  // vignette
  const vig = ctx.createRadialGradient(CX,H*0.5, H*0.3, CX,H*0.5, H);
  vig.addColorStop(0,'rgba(0,0,0,0)'); vig.addColorStop(1,'rgba(0,0,0,0.45)');
  ctx.fillStyle=vig; ctx.fillRect(0,0,W,H);
  // speed lines overlay opacity via ctx
  if(speed > TUNING.SPEEDLINE_THRESH || boosting){
    const inten = Math.min(1, (speed - TUNING.SPEEDLINE_THRESH)/32 + (boosting?0.5:0));
    ctx.strokeStyle=`rgba(255,255,255,${0.12*inten})`;
    ctx.lineWidth=1*devicePixelRatio;
    for(let i=0;i<18;i++){
      const x = (i* 97 % W);
      const y = horizonY + (i*53 % (H - horizonY));
      const len = 40*devicePixelRatio * inten + Math.random()*30;
      ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x- len*0.2, y - len*0.02); ctx.stroke();
    }
    // chromatic aberration simple duplicate
    if(inten>0.6){
      ctx.globalAlpha=0.08;
      ctx.drawImage(canvas, 2*devicePixelRatio, 0);
      // restore alpha
      ctx.globalAlpha=1;
    }
  }
  // shadow indicator
  if(inShadow){
    ctx.fillStyle='rgba(0,0,0,0.22)';
    ctx.fillRect(0,horizonY,W, H-horizonY);
    ctx.fillStyle='rgba(255,255,255,0.9)'; ctx.font=`bold ${11*devicePixelRatio}px sans-serif`; ctx.textAlign='center';
    ctx.fillText('IN SHADOW — ENERGY DRAINING', CX, horizonY + 28*devicePixelRatio);
  }
  ctx.restore();

  // HUD dom update
  document.getElementById('energyFill').style.width = energy.toFixed(0)+'%';
  document.getElementById('energyTxt').textContent = Math.floor(energy)+'%';
  document.getElementById('energyFill').style.background = energy<30? 'linear-gradient(90deg,#ff3b30,#ff6a00)' : 'linear-gradient(90deg,#ffcc33,#ff5a2b)';
  document.getElementById('hudScore').textContent=Math.floor(score).toLocaleString();
  document.getElementById('hudMult').textContent='x'+multiplier.toFixed(1);
  document.getElementById('hudDist').textContent=Math.floor(distance)+' m';
  document.getElementById('bigDist').textContent=Math.floor(distance)+' m';
  const kph = Math.floor(speed*9 + (boosting? 80:0));
  document.getElementById('hudSpeed').textContent=kph;
  document.getElementById('speedNum').textContent=kph+' KM/H';
  document.getElementById('speedBar').style.width = Math.min(100, (speed/ TUNING.MAX_SPEED)*100)+'%';
  // speedlines dom opacity
  document.getElementById('speedLines').style.opacity = speed> TUNING.SPEEDLINE_THRESH ? '0.35':'0';
  document.getElementById('speedLines').style.background = `repeating-linear-gradient(90deg, transparent 0 40px, rgba(255,255,255,0.07) 40px 41px)`;
}

// === MAIN LOOP ===
function loop(now){
  const dt = Math.min(0.033, (now - lastT)/1000);
  lastT=now;
  update(dt);
  render();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

// initial render of best
document.getElementById('hudBest').textContent=highScore;

// small intro particles
for(let i=0;i<6;i++) particles.push({x:0,y:1.5,z: 8+ i*4, vx:0,vy:0,life:1,max:1,col:'score'});

</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"6d9ec7dd-1a1a-4f1d-a829-97d75f8a3697","tokensIn":883,"tokensOut":14583,"tokensTotal":15466,"cost":0.0629572,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:31:55.153Z"} -->