Race the Sun
inkling · 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 29,064 bytes · SHA-256 4fb314f8815b
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solar Drift — Endless Flight</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap');
:root {
--bg: #0a0214;
--sun-glow: #ff6a3d;
}
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; }
html, body {
width: 100%; height: 100%;
background: linear-gradient(180deg, #0a0214 0%, #140a2a 60%, #1f0e1a 100%);
overflow: hidden;
font-family: 'Orbitron', sans-serif;
color: #fff;
}
#canvas-wrapper {
position: relative;
width: 100%; height: 100%;
overflow: hidden;
}
canvas {
display: block;
width: 100%; height: 100%;
}
.overlay {
position: absolute;
inset: 0;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
background: radial-gradient(circle at 50% 80%, rgba(255, 106, 61, 0.15) 0%, transparent 70%);
}
.overlay.hidden { display: none; }
.title-text {
font-size: clamp(3rem, 8vw, 7rem);
font-weight: 900;
letter-spacing: 0.15em;
text-transform: uppercase;
text-shadow:
0 0 20px rgba(255, 106, 61, 0.6),
0 0 60px rgba(255, 106, 61, 0.3),
0 4px 0 rgba(255, 60, 20, 0.8);
color: #ffccaa;
animation: pulse 3s ease-in-out infinite;
}
.subtitle {
font-family: 'Rajdhani', sans-serif;
font-size: clamp(1rem, 2.5vw, 1.8rem);
font-weight: 300;
letter-spacing: 0.4em;
text-transform: uppercase;
color: rgba(255, 200, 200, 0.9);
margin-top: 0.5em;
text-shadow: 0 2px 10px rgba(255, 106, 61, 0.4);
}
.prompt {
margin-top: 2.5em;
font-family: 'Rajdhani', sans-serif;
font-size: 1.2rem;
font-weight: 700;
letter-spacing: 0.2em;
color: #ffd166;
text-shadow: 0 0 15px rgba(255, 209, 102, 0.5);
animation: blink 1.2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.03); opacity: 0.95; }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.hud {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 5;
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: space-between;
font-family: 'Rajdhani', sans-serif;
text-shadow: 0 2px 8px rgba(0,0,0,0.8);
}
.hud-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.hud-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.stat {
background: rgba(10, 2, 20, 0.4);
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.05);
padding: 0.6rem 1.2rem;
border-radius: 12px;
}
.stat-label {
font-size: 0.75rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: rgba(255,255,255,0.5);
display: block;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: #ffd166;
}
#game-over .title-text { font-size: 5rem; color: #ff5555; text-shadow: 0 0 20px rgba(255,85,85,0.6); animation: none; }
#game-over .subtitle { color: rgba(255,200,200,0.8); }
.speed-lines {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 6;
background: repeating-linear-gradient(
90deg,
transparent 0px,
transparent 40px,
rgba(255, 106, 61, 0.06) 41px,
rgba(255, 200, 150, 0.04) 42px,
transparent 43px,
transparent 80px
);
opacity: 0;
transition: opacity 0.2s;
}
.speed-lines.active { opacity: 1; }
</style>
</head>
<body>
<div id="canvas-wrapper">
<canvas id="game"></canvas>
<div class="speed-lines" id="speedLines"></div>
<div class="hud" id="hud">
<div class="hud-top">
<div class="stat">
<span class="stat-label">Distance</span>
<span class="stat-value" id="dDist">0</span>
</div>
<div class="stat">
<span class="stat-label">Score</span>
<span class="stat-value" id="dScore">0</span>
</div>
<div class="stat">
<span class="stat-label">Mult</span>
<span class="stat-value" id="dMult">x1</span>
</div>
</div>
<div class="hud-bottom">
<div class="stat">
<span class="stat-label">Energy</span>
<span class="stat-value" id="dEnergy">100%</span>
</div>
<div class="stat">
<span class="stat-label">Speed</span>
<span class="stat-value" id="dSpeed">0</span>
</div>
</div>
</div>
<div class="overlay" id="titleScreen">
<h1 class="title-text">Solar Drift</h1>
<p class="subtitle">Race the Dying Light</p>
<p class="prompt">PRESS SPACE TO LAUNCH</p>
</div>
<div class="overlay hidden" id="gameOverScreen">
<h1 class="title-text">CONTACT LOST</h1>
<p class="subtitle">The sun swallowed your craft</p>
<div class="stat" style="margin-top: 1.5em;">
<span class="stat-label">Final Score</span>
<span class="stat-value" id="finalScore" style="font-size: 2.5rem;">0</span>
</div>
<p class="prompt" style="margin-top: 1.5em;">PRESS R TO RESTART</p>
</div>
</div>
<script>
/* =========================================
TUNABLE CONSTANTS
========================================= */
const C = {
// Canvas & world
WIDTH: 1920,
HEIGHT: 1080,
FOV: 90,
LANE_COUNT: 5,
LANE_WIDTH: 140,
WORLD_DEPTH: 4000,
// Player physics
BASE_SPEED: 2.0,
MAX_SPEED: 28.0,
ACCEL: 0.0005,
STRAFE_SENS: 0.018,
PITCH_SENS: 0.015,
ROLL_SENS: 0.08,
BANK_FACTOR: 0.35,
BOOST_MULT: 1.6,
BOOST_DRAIN: 0.08,
// Energy
ENERGY_MAX: 100,
ENERGY_DRAIN: 0.015,
SHADOW_DRAIN_MULT: 3.5,
PICKUP_ENERGY: 25,
// Sun
SUN_START_Y: 0.3,
SUN_SINK_SPEED: 0.00015,
// Procedural world
SEED_OFFSET: 42,
OBSTACLE_SPAWN_RATE: 0.35,
PICKUP_SPAWN_RATE: 0.45,
MULT_ORB_RATE: 0.15,
MIN_DIST_OBSTACLE: 120,
MIN_Y_SEP: 80,
// Visual
SKY_TOP: '#2a0e1f',
SKY_MID: '#4a1a2a',
SKY_HORIZON: '#ff6a3d',
SKY_BOTTOM: '#1a0a14',
SUN_COLOR: '#ffcc88',
SUN_HALO: '#ff5522',
GROUND_TOP: '#ff8844',
GROUND_BOT: '#2a0e14',
PLAYER_COLOR: '#ffeeaa',
SHADOW_COLOR: '#1a0510',
// Audio
MASTER_VOL: 0.15,
};
/* =========================================
SETUP & CANVAS
========================================= */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W = window.innerWidth;
let H = window.innerHeight;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
}
window.addEventListener('resize', resize);
resize();
/* =========================================
AUDIO ENGINE (WebAudio)
========================================= */
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioCtx();
let audioUnlocked = false;
function unlockAudio() {
if (audioUnlocked) return;
audioCtx.resume().then(() => {
audioUnlocked = true;
playDrone();
});
}
let droneOsc, droneGain;
let droneLFO;
function initDrone() {
droneOsc = audioCtx.createOscillator();
droneGain = audioCtx.createGain();
droneOsc.type = 'triangle';
droneOsc.frequency.value = 55;
droneGain.gain.value = 0.08;
droneOsc.connect(droneGain);
droneGain.connect(audioCtx.destination);
droneLFO = audioCtx.createOscillator();
droneLFO.type = 'sine';
droneLFO.frequency.value = 0.3;
const lfoGain = audioCtx.createGain();
lfoGain.gain.value = 8;
droneLFO.connect(lfoGain);
lfoGain.connect(droneOsc.frequency);
}
function playDrone() {
if (!audioUnlocked) return;
if (!droneOsc) initDrone();
droneOsc.start();
droneLFO.start();
}
function stopDrone() {
try { droneOsc?.stop(); droneLFO?.stop(); } catch(e) {}
}
function playSound(type, pitch = 440, duration = 0.15) {
if (!audioUnlocked) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
if (type === 'pickup') {
osc.type = 'sine';
osc.frequency.setValueAtTime(pitch, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(pitch * 3, audioCtx.currentTime + duration);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
} else if (type === 'mult') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(pitch, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(pitch * 1.5, audioCtx.currentTime + duration);
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
} else if (type === 'crash') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(80, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(20, audioCtx.currentTime + 0.3);
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4);
}
osc.start();
osc.stop(audioCtx.currentTime + duration * 1.2);
}
function updateDrone(speed) {
if (!audioUnlocked || !droneOsc) return;
const freq = 55 + speed * 1.8;
const vol = Math.min(0.08 + speed * 0.003, 0.25);
droneOsc.frequency.linearRampToValueAtTime(freq, audioCtx.currentTime + 0.1);
droneGain.gain.linearRampToValueAtTime(vol, audioCtx.currentTime + 0.1);
}
/* =========================================
SEEDED RANDOM
========================================= */
let runSeed = 0;
function setSeed(s) {
runSeed = s;
}
function rand() {
runSeed = (runSeed * 16807 + 0) % 2147483647;
return (runSeed - 1) / 2147483646;
}
/* =========================================
GAME STATE
========================================= */
const keys = {};
let state = 'title'; // title, playing, over
let score = 0;
let distance = 0;
let multiplier = 1;
let speed = C.BASE_SPEED;
let energy = C.ENERGY_MAX;
let sunY = C.SUN_START_Y;
let cameraShake = 0;
let nearMissTimer = 0;
let boostActive = false;
let player = {
x: 0, y: 0, z: 150,
roll: 0, pitch: 0,
vx: 0, vy: 0,
boostTimer: 0,
};
const particles = [];
const obstacles = [];
const pickups = [];
function spawnObstacle(z) {
const lane = Math.floor(rand() * C.LANE_COUNT);
const lx = (lane - (C.LANE_COUNT - 1) / 2) * C.LANE_WIDTH;
const yOffset = (rand() - 0.5) * 200;
const type = rand() > 0.7 ? 'tower' : (rand() > 0.5 ? 'pillar' : 'block');
obstacles.push({
x: lx,
y: yOffset,
z: z + 800 + rand() * 1200,
w: 40 + rand() * 60,
h: 300 + rand() * 500,
d: 40 + rand() * 60,
type,
});
}
function spawnPickup(z) {
const r = rand();
const lane = Math.floor(rand() * C.LANE_COUNT);
const lx = (lane - (C.LANE_COUNT - 1) / 2) * C.LANE_WIDTH;
let yOff = (rand() - 0.5) * 180;
if (r < C.MULT_ORB_RATE) {
pickups.push({ x: lx, y: yOff, z: z + 600 + rand() * 1200, type: 'mult', w: 30, h: 30, d: 30, life: 300 });
} else if (r < C.PICKUP_SPAWN_RATE + C.MULT_ORB_RATE) {
pickups.push({ x: lx, y: yOff, z: z + 600 + rand() * 1200, type: 'energy', w: 30, h: 30, d: 30, life: 300 });
} else {
pickups.push({ x: lx, y: yOff, z: z + 600 + rand() * 1200, type: 'score', w: 25, h: 25, d: 25, life: 300, val: 50 });
}
}
/* =========================================
INPUT
========================================= */
window.addEventListener('keydown', e => {
keys[e.key.toLowerCase()] = true;
if (e.key === ' ' || e.key === 'Enter') {
if (state === 'title') {
startRun();
} else if (state === 'playing') {
boostActive = true;
player.boostTimer = 30;
playSound('pickup', 600, 0.1);
}
}
if (e.key === 'r' || e.key === 'R') {
if (state === 'over') startRun();
}
if (e.key === ' ' && state === 'title') {
e.preventDefault();
startRun();
}
});
window.addEventListener('keyup', e => {
keys[e.key.toLowerCase()] = false;
if (e.key === ' ') boostActive = false;
});
/* =========================================
GAME LOOP
========================================= */
function initRun() {
setSeed(Math.floor(Math.random() * 1000000));
score = 0;
distance = 0;
multiplier = 1;
speed = C.BASE_SPEED;
energy = C.ENERGY_MAX;
sunY = C.SUN_START_Y;
cameraShake = 0;
nearMissTimer = 0;
player = { x: 0, y: 0, z: 150, roll: 0, pitch: 0, vx: 0, vy: 0, boostTimer: 0 };
obstacles.length = 0;
pickups.length = 0;
particles.length = 0;
updateHUD();
document.getElementById('titleScreen').classList.add('hidden');
document.getElementById('gameOverScreen').classList.add('hidden');
document.getElementById('hud').style.display = 'flex';
}
function startRun() {
unlockAudio();
initRun();
state = 'playing';
requestAnimationFrame(loop);
}
function gameOver() {
state = 'over';
playSound('crash', 120, 0.5);
stopDrone();
document.getElementById('gameOverScreen').classList.remove('hidden');
document.getElementById('finalScore').textContent = Math.floor(score);
document.getElementById('hud').style.display = 'none';
localStorage.setItem('solarDriftBest', Math.max(parseInt(localStorage.getItem('solarDriftBest') || '0'), Math.floor(score)));
}
function updateHUD() {
document.getElementById('dDist').textContent = Math.floor(distance);
document.getElementById('dScore').textContent = Math.floor(score);
document.getElementById('dMult').textContent = 'x' + multiplier;
document.getElementById('dEnergy').textContent = Math.floor(energy) + '%';
document.getElementById('dSpeed').textContent = Math.floor(speed);
}
/* =========================================
UPDATE
========================================= */
function update() {
if (state !== 'playing') return;
// Speed curve
speed += C.ACCEL;
speed = Math.min(speed, C.MAX_SPEED);
// Sun sink
sunY += C.SUN_SINK_SPEED * speed;
// Energy drain
let drain = C.ENERGY_DRAIN * (sunY < 0.45 ? 1 + (0.45 - sunY) * 4 : 1);
// Shadow check: simple approximation — if sun is low, extra drain
if (sunY > 0.55) drain *= C.SHADOW_DRAIN_MULT;
if (boostActive && player.boostTimer > 0) {
drain += C.BOOST_DRAIN;
player.boostTimer--;
}
energy -= drain;
energy = Math.max(0, energy);
// Auto-death from sun below horizon or energy depleted
if (sunY > 0.92 || energy <= 0) {
gameOver();
return;
}
// Player input
let ax = 0, ay = 0;
if (keys['a'] || keys['arrowleft']) ax -= 1;
if (keys['d'] || keys['arrowright']) ax += 1;
if (keys['w'] || keys['arrowup']) ay -= 1;
if (keys['s'] || keys['arrowdown']) ay += 1;
// Strafe & pitch with easing
const ease = 0.08 + Math.min(speed * 0.005, 0.04);
player.vx += ax * C.STRAFE_SENS * ease;
player.vy += ay * C.PITCH_SENS * ease;
player.vx *= 0.82;
player.vy *= 0.82;
// Apply movement
player.x += player.vx;
player.y += player.vy;
player.z -= speed * (boostActive ? C.BOOST_MULT : 1.0);
// Roll based on strafe
player.roll = -(player.vx * C.BANK_FACTOR);
player.roll *= 0.9; // dampen slightly
// Pitch lean
player.pitch = -(player.vy * 0.4);
// Clamp position
player.x = Math.max(-C.LANE_WIDTH * 2.5, Math.min(C.LANE_WIDTH * 2.5, player.x));
player.y = Math.max(-200, Math.min(300, player.y));
// Distance & score
distance += speed * (boostActive ? C.BOOST_MULT : 1.0);
score += speed * multiplier * (boostActive ? 1.5 : 1.0) * 0.1;
// Spawn waves
const spawnZ = player.z + 300;
if (rand() < C.OBSTACLE_SPAWN_RATE) spawnObstacle(player.z);
if (rand() < C.PICKUP_SPAWN_RATE) spawnPickup(player.z);
// Update world objects
for (let i = obstacles.length - 1; i >= 0; i--) {
const o = obstacles[i];
o.z -= speed * (boostActive ? C.BOOST_MULT : 1.0);
if (o.z < player.z - 200) {
obstacles.splice(i, 1);
} else {
// Collision
const dx = Math.abs(o.x - player.x);
const dy = Math.abs(o.y - player.y);
// Simplified AABB vs point with small craft size
if (dx < (o.w / 2 + 30) && dy < (o.h / 2 + 20) && Math.abs(o.z - player.z) < 80) {
// Crash!
addParticles(player.x, player.y, 40, '#ff5522');
cameraShake = 25;
energy -= 40;
playSound('crash', 100, 0.3);
// Near miss bonus
score += 200 * multiplier;
multiplier += 0.5;
obstacles.splice(i, 1);
if (energy <= 0) {
gameOver();
return;
}
}
}
}
// Pickups
for (let i = pickups.length - 1; i >= 0; i--) {
const p = pickups[i];
p.z -= speed * (boostActive ? C.BOOST_MULT : 1.0);
p.life--;
if (p.life <= 0) {
pickups.splice(i, 1);
} else {
const dx = Math.abs(p.x - player.x);
const dy = Math.abs(p.y - player.y);
if (dx < (p.w / 2 + 20) && dy < (p.h / 2 + 20) && Math.abs(p.z - player.z) < 40) {
if (p.type === 'energy') {
energy = Math.min(C.ENERGY_MAX, energy + C.PICKUP_ENERGY);
score += 100 * multiplier;
playSound('pickup', 880, 0.15);
addParticles(p.x, p.y, 15, '#ffd166');
} else if (p.type === 'mult') {
multiplier += 1;
score += 500;
playSound('mult', 1320, 0.2);
addParticles(p.x, p.y, 15, '#ffaa55');
} else {
score += p.val * multiplier;
playSound('pickup', 660, 0.15);
addParticles(p.x, p.y, 12, '#ffeeaa');
}
pickups.splice(i, 1);
}
}
}
// Near miss detection (close to obstacle but not hitting)
let nearMiss = false;
for (const o of obstacles) {
const dx = Math.abs(o.x - player.x);
const dy = Math.abs(o.y - player.y);
const distZ = Math.abs(o.z - player.z);
if (distZ < 120 && dx < (o.w/2 + 50) && dy < (o.h/2 + 50) && !(dx < (o.w/2 + 30) && dy < (o.h/2 + 20) && distZ < 80)) {
nearMiss = true;
score += 2 * multiplier * speed;
}
}
if (nearMiss) {
nearMissTimer = 10;
cameraShake = Math.min(cameraShake + 3, 10);
}
// Camera shake decay
cameraShake *= 0.85;
// Particles update
for (let i = particles.length - 1; i >= 0; i--) {
const pt = particles[i];
pt.x += pt.vx;
pt.y += pt.vy;
pt.z += pt.vz;
pt.life--;
pt.alpha = pt.life / pt.maxLife;
if (pt.life <= 0) particles.splice(i, 1);
}
// Update sound
updateDrone(speed);
updateHUD();
}
/* =========================================
DRAWING
========================================= */
function draw() {
// Clear with gradient
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, C.SKY_TOP);
grad.addColorStop(0.35, C.SKY_MID);
grad.addColorStop(0.58, C.SKY_HORIZON);
grad.addColorStop(0.7, C.SKY_BOTTOM);
// Slight variation over time
const shift = Math.sin(Date.now() * 0.0005) * 0.02;
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Camera shake
const sx = (Math.random() - 0.5) * cameraShake;
const sy = (Math.random() - 0.5) * cameraShake;
ctx.save();
ctx.translate(W/2 + sx, H/2 + sy);
// Scale for perspective
const scale = W / 1000;
// Draw sun on horizon
const sunX = 0;
const sunDrawY = H * sunY - H/2;
const sunR = 120 + (1 - sunY) * 200;
// Halo glow
const haloGrad = ctx.createRadialGradient(sunX, sunDrawY, 0, sunX, sunDrawY, sunR * 2.5);
haloGrad.addColorStop(0, 'rgba(255, 200, 100, 0.3)');
haloGrad.addColorStop(0.5, 'rgba(255, 106, 61, 0.15)');
haloGrad.addColorStop(1, 'rgba(200, 50, 20, 0)');
ctx.fillStyle = haloGrad;
ctx.beginPath();
ctx.arc(sunX, sunDrawY, sunR * 2.5, 0, Math.PI * 2);
ctx.fill();
// Sun body
const sunGrad = ctx.createRadialGradient(sunX, sunDrawY, 0, sunX, sunDrawY, sunR);
sunGrad.addColorStop(0, '#fff8e7');
sunGrad.addColorStop(0.4, C.SUN_COLOR);
sunGrad.addColorStop(1, '#ff5522');
ctx.fillStyle = sunGrad;
ctx.beginPath();
ctx.arc(sunX, sunDrawY, sunR, 0, Math.PI * 2);
ctx.fill();
// Ground plane (perspective grid)
const groundY = H * 0.6 - H/2;
ctx.fillStyle = '#1a0510';
ctx.fillRect(-W, groundY, W*2, H);
// Perspective lines
for (let i = 0; i <= 8; i++) {
const t = i / 8;
const gx = (t - 0.5) * W * 1.2;
const gy = groundY + Math.sin(t * Math.PI) * 100;
ctx.strokeStyle = `rgba(255, 136, 68, ${0.15 + 0.15 * Math.abs(Math.sin(i * 0.5))})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(gx, gy);
ctx.lineTo(gx, H + 200);
ctx.stroke();
}
// Ground gradient
const gGrad = ctx.createLinearGradient(0, groundY, 0, H);
gGrad.addColorStop(0, C.GROUND_TOP);
gGrad.addColorStop(1, C.GROUND_BOT);
ctx.fillStyle = gGrad;
ctx.fillRect(-W, groundY, W*2, H/2);
// Draw world objects sorted by z
const worldItems = [];
for (const o of obstacles) worldItems.push({ ...o, kind: 'obst' });
for (const p of pickups) worldItems.push({ ...p, kind: 'pick' });
worldItems.sort((a, b) => a.z - b.z);
for (const item of worldItems) {
const proj = project(item.x, item.y, item.z);
const scaleFactor = proj.s;
// Skip if behind camera or too small
if (item.z < player.z - 50 || proj.s < 0.01) continue;
if (item.kind === 'obst') {
// Draw obstacle
const w = item.w * scaleFactor;
const h = item.h * scaleFactor;
const x = proj.x - w/2;
const y = proj.y - h;
const d = item.d * scaleFactor;
// Shadow side
const isInShadow = sunY > 0.6;
const baseColor = isInShadow ? '#1a0510' : '#ff8844';
const lightColor = isInShadow ? '#2a0e14' : '#ffaa66';
// Main block
ctx.fillStyle = baseColor;
drawBox(ctx, x, y + h*0.2, w, h*0.8, d, 0.3);
// Highlight face
ctx.fillStyle = lightColor;
ctx.beginPath();
ctx.moveTo(x + w*0.1, y + h*0.2);
ctx.lineTo(x + w*0.1 + d*0.3, y + h*0.1);
ctx.lineTo(x + w*0.1 + d*0.3, y + h*0.2 + h*0.8);
ctx.lineTo(x + w*0.1, y + h*0.2 + h*0.8);
ctx.fill();
} else {
// Pickup orb
const r = item.w/2 * scaleFactor;
const x = proj.x;
const y = proj.y - r;
const color = item.type === 'energy' ? '#ffcc22' : (item.type === 'mult' ? '#ffaa55' : '#ffeeaa');
const glow = item.type === 'mult' ? '#ff8800' : '#ffee88';
ctx.save();
ctx.translate(x, y);
// Glow
const g = ctx.createRadialGradient(0, 0, r*0.2, 0, 0, r*2.5);
g.addColorStop(0, glow);
g.addColorStop(0.3, color + '44');
g.addColorStop(1, 'transparent');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(0, 0, r*2.5, 0, Math.PI*2);
ctx.fill();
// Body
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(0, 0, r, 0, Math.PI*2);
ctx.fill();
// Highlight
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(-r*0.3, -r*0.3, r*0.25, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
}
// Draw player
const pProj = project(player.x, player.y + 30, player.z);
const ps = pProj.s;
// Shadow under player
ctx.fillStyle = 'rgba(10, 2, 20, 0.5)';
ctx.beginPath();
ctx.ellipse(pProj.x, pProj.y + 20, 25 * ps, 12 * ps, 0, 0, Math.PI * 2);
ctx.fill();
ctx.save();
ctx.translate(pProj.x, pProj.y);
ctx.rotate(player.roll);
ctx.rotate(player.pitch * 0.3);
// Craft body
const craftW = 50 * ps;
const craftH = 18 * ps;
const craftD = 70 * ps;
// Main hull
ctx.fillStyle = '#ffeecc';
drawRoundBody(ctx, -craftW/2, -craftH/2, craftW, craftH, craftD, 0.4);
// Wings
ctx.fillStyle = '#ffddaa';
ctx.beginPath();
ctx.moveTo(-craftW/2 - 15*ps, 0);
ctx.lineTo(-craftW/2 - 60*ps, -25*ps);
ctx.lineTo(-craftW/2 - 35*ps, 10*ps);
ctx.lineTo(-craftW/2 - 15*ps, 0);
ctx.fill();
ctx.beginPath();
ctx.moveTo(craftW/2 + 15*ps, 0);
ctx.lineTo(craftW/2 + 60*ps, -25*ps);
ctx.lineTo(craftW/2 + 35*ps, 10*ps);
ctx.lineTo(craftW/2 + 15*ps, 0);
ctx.fill();
// Cockpit glow
const glowGrad = ctx.createRadialGradient(0, -craftH/4, 2, 0, -craftH/4, 15*ps);
glowGrad.addColorStop(0, '#ffcc88');
glowGrad.addColorStop(0.5, '#ffaa66');
glowGrad.addColorStop(1, 'transparent');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(0, -craftH/4, 12*ps, 0, Math.PI*2);
ctx.fill();
// Thrust particles behind craft
for (let i = 0; i < 3; i++) {
const px = (Math.random() - 0.5) * 20 * ps - craftD * 0.3;
const py = (Math.random() - 0.5) * 10 * ps;
const size = (3 + Math.random() * 6) * ps;
ctx.fillStyle = `rgba(255, 200, 150, ${0.4 + Math.random() * 0.4})`;
ctx.beginPath();
ctx.arc(px, py, size, 0, Math.PI*2);
ctx.fill();
}
ctx.restore();
// Particles
for (const pt of particles) {
const proj = project(pt.x, pt.y, pt.z);
if (proj.s < 0.01 || proj.z < player.z - 50) continue;
ctx.save();
ctx.globalAlpha = pt.alpha;
ctx.translate(proj.x, proj.y);
ctx.fillStyle = pt.color;
ctx.beginPath();
ctx.arc(0, 0, pt.size * proj.s, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
ctx.restore();
// Post-process speed lines overlay (drawn on wrapper via CSS, but add chromatic abberation via canvas)
if (speed > 18) {
ctx.save();
ctx.globalAlpha = Math.min((speed - 18) / 10, 0.2);
// Subtle chromatic split
ctx.save();
ctx.globalCompositeOperation = 'screen';
ctx.translate(W*0.002, H*0.001);
ctx.fillStyle = 'rgba(255, 106, 61, 0.1)';
ctx.fillRect(0, 0, W, H);
ctx.restore();
ctx.restore();
}
}
function project(x, y, z) {
const camZ = player.z - 100;
const relZ = z - camZ;
const s = (C.FOV * W) / (relZ + C.FOV);
const screenX = (x * s) + W/2;
const screenY = (y * s) + H/2;
return { x: screenX, y: screenY, s: s / (C.FOV * W / 600), z: z };
}
function drawBox(ctx, x, y, w, h, d, tilt) {
// Simple isometric box
const halfW = w/2;
const halfD = d/2;
// Top face (slightly tilted)
ctx.fillStyle = '#ffaa66';
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + halfW + halfD * tilt, y - h*0.15);
ctx.lineTo(x + halfW + halfD * tilt + w - halfD*tilt*2, y - h*0.15);
ctx.lineTo(x + w, y);
ctx.fill();
// Front face
ctx.fillStyle = '#ff8844';
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x, y + h);
ctx.fill();
// Side face
ctx.fillStyle = '#e05522';
ctx.beginPath();
ctx.moveTo(x + w, y);
ctx.lineTo(x + w + halfD * tilt, y - h*0.15);
ctx.lineTo(x + w + halfD * tilt, y + h - h*0.15);
ctx.lineTo(x + w, y + h);
ctx.fill();
}
function drawRoundBody(ctx, x, y, w, h, d, tilt) {
// Streamlined craft body with simple 3D effect
const cx = x + w/2;
const cy = y + h/2;
// Main body ellipse
ctx.fillStyle = '#ffeecc';
ctx.beginPath();
ctx.ellipse(cx, cy, w/2, h/2, 0, 0, Math.PI*2);
ctx.fill();
// Nose extension
ctx.fillStyle = '#ffeecc';
ctx.beginPath();
ctx.ellipse(cx + w/2 + d*0.3, cy, d*0.25, h/2*0.9, -0.2, 0, Math.PI*2);
ctx.fill();
// Side shading
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.beginPath();
ctx.ellipse(cx + w/2 - 5, cy, w/2 - 5, h/2, 0, 0, Math.PI*2);
ctx.fill();
}
function addParticles(x, y, count, color) {
for (let i = 0; i < count; i++) {
particles.push({
x, y, z: player.z + (Math.random()-0.5)*50,
vx: (Math.random()-0.5)*4,
vy: (Math.random()-0.5)*4 + 2,
vz: -Math.random()*6 - 2,
life: 30 + Math.random() * 30,
maxLife: 60,
color,
size: 3 + Math.random() * 6,
});
}
}
function loop() {
update();
draw();
// Speed lines activation
document.getElementById('speedLines').classList.toggle('active', speed > 20);
if (state !== 'over') {
requestAnimationFrame(loop);
}
}
/* =========================================
INIT & HIGH SCORE DISPLAY
========================================= */
function showTitle() {
state = 'title';
document.getElementById('titleScreen').classList.remove('hidden');
document.getElementById('gameOverScreen').classList.add('hidden');
document.getElementById('hud').style.display = 'none';
// Show best score
const best = localStorage.getItem('solarDriftBest') || '0';
const bestEl = document.getElementById('bestScore');
if (!bestEl) {
const el = document.createElement('p');
el.id = 'bestScore';
el.style.cssText = 'margin-top:1em;font-family:Rajdhani,sans-serif;font-size:1rem;color:#ffaa55;text-shadow:0 2px 8px rgba(255,106,61,0.3);';
el.innerHTML = 'BEST: ' + best;
document.getElementById('titleScreen').appendChild(el);
} else {
bestEl.innerHTML = 'BEST: ' + best;
}
}
// Start on load
showTitle();
// Pre-warm audio context on any interaction
window.addEventListener('click', unlockAudio);
window.addEventListener('keydown', unlockAudio);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"basic","sessionId":"049c4d6b-7a2b-4c48-bf3f-31662d5b59a2","tokensIn":939,"tokensOut":10208,"tokensTotal":11147,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T21:07:04.610Z"} -->