Race the Sun
muse spark 1.2 · basic

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"} -->