Race the Sun
inkling · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 26,242 bytes · SHA-256 8b69e4ef1f2a
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solar Drift — Race the Sun</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@500;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #050a14; font-family: 'Orbitron', sans-serif; }
canvas { display: block; width: 100vw; height: 100vh; }
#hud { position: fixed; top: 0; left: 0; right: 0; height: 72px; pointer-events: none; display: flex; align-items: center; justify-content: space-between; padding: 0 32px; z-index: 10; text-shadow: 0 2px 8px rgba(0,0,0,0.6); }
#hud .label { font-family: 'Rajdhani', sans-serif; font-weight: 700; letter-spacing: 2px; font-size: 11px; text-transform: uppercase; color: rgba(255,255,255,0.4); margin-bottom: -2px; }
#hud .value { font-family: 'Orbitron', sans-serif; font-weight: 900; font-size: 22px; color: #fff; }
#hud .energy-box { width: 140px; height: 10px; background: rgba(255,255,255,0.08); border-radius: 5px; margin-top: 4px; overflow: hidden; position: relative; }
#hud .energy-fill { height: 100%; width: 100%; background: linear-gradient(90deg, #ffaa00, #ff4400); border-radius: 5px; transition: width 0.05s linear; }
.hud-section { display: flex; flex-direction: column; }
#titleOverlay, #gameOverOverlay { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; background: rgba(5,10,20,0.75); backdrop-filter: blur(16px); transition: opacity 0.3s ease; }
#gameOverOverlay { display: none; opacity: 0; }
h1 { font-size: clamp(48px, 10vw, 120px); font-weight: 900; letter-spacing: 8px; color: #fff; text-transform: uppercase; text-shadow: 0 0 60px rgba(255,200,50,0.5), 0 8px 0 rgba(255,100,0,0.4); margin-bottom: 8px; line-height: 0.9; }
.subtitle { font-family: 'Rajdhani', sans-serif; font-size: 20px; color: rgba(255,255,255,0.7); letter-spacing: 6px; text-transform: uppercase; margin-bottom: 48px; }
.btn { font-family: 'Orbitron', sans-serif; font-weight: 700; letter-spacing: 4px; text-transform: uppercase; padding: 16px 48px; border: 2px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.05); color: #fff; font-size: 16px; cursor: pointer; border-radius: 4px; transition: all 0.1s ease; }
.btn:hover { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.5); box-shadow: 0 0 20px rgba(255,255,255,0.15); }
.btn:active { transform: scale(0.98); }
.key-hint { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); color: rgba(255,255,255,0.35); font-family: 'Rajdhani', sans-serif; font-size: 13px; letter-spacing: 3px; z-index: 5; pointer-events: none; }
#highScoreDisplay { position: fixed; top: 84px; right: 32px; color: rgba(255,255,255,0.25); font-family: 'Rajdhani', sans-serif; font-size: 13px; letter-spacing: 2px; z-index: 10; pointer-events: none; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
<div class="hud-section">
<div class="label">Distance</div>
<div class="value" id="hudDist">0</div>
</div>
<div class="hud-section">
<div class="label">Score</div>
<div class="value" id="hudScore">0</div>
</div>
<div class="hud-section">
<div class="label">Multiplier</div>
<div class="value" id="hudMult">x1</div>
</div>
<div class="hud-section">
<div class="label">Energy</div>
<div class="energy-box"><div class="energy-fill" id="energyFill"></div></div>
</div>
<div class="hud-section">
<div class="label">Speed</div>
<div class="value" id="hudSpeed">0</div>
</div>
</div>
<div id="highScoreDisplay">BEST: <span id="highScoreVal">0</span></div>
<div id="titleOverlay">
<h1>SOLAR DRIFT</h1>
<div class="subtitle">Race the Sun</div>
<button class="btn" onclick="startGame()">START FLIGHT</button>
<div style="margin-top:32px; color:rgba(255,255,255,0.35); font-size:13px; letter-spacing:2px; font-family:'Rajdhani',sans-serif;">WASD / ARROWS · SPACE BOOST</div>
</div>
<div id="gameOverOverlay">
<h1 style="font-size:48px; letter-spacing:6px;">CRASHED</h1>
<div style="font-family:'Rajdhani',sans-serif; font-size:22px; color:rgba(255,255,255,0.7); letter-spacing:4px; margin-bottom:24px;">SCORE <span id="finalScore" style="color:#fff; font-weight:700;">0</span></div>
<button class="btn" onclick="startGame()">RETRY (R)</button>
</div>
<div class="key-hint">STAY IN THE SUNLIGHT · DODGE · COLLECT · SURVIVE</div>
<script>
// === SETUP ===
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// === TUNABLE CONSTANTS ===
const CONFIG = {
SEED: 1,
PLAYER_SPEED: 0,
MAX_SPEED: 120,
ACCEL: 0.008,
STRAFE_SPEED: 10,
PITCH_SPEED: 4,
BANK_ANGLE_MAX: 35,
BOOST_MULT: 2.2,
BOOST_DRAIN: 2.5,
BOOST_COST: 5,
ENERGY_MAX: 100,
ENERGY_DRAIN: 0.15,
SHADOW_DRAIN: 0.7,
ENERGY_PICKUP: 25,
MULT_PICKUP: 1,
SCORE_PER_DIST: 10,
NEAR_MISS_RANGE: 60,
NEAR_MISS_SCORE: 50,
SHAKE_DECAY: 0.9,
SUN_DROP_SPEED: 0.0003,
SUN_START_ANG: 0.15,
SUN_END_ANG: -0.1,
OBSTACLE_SPAWN_RATE: 0.015,
PICKUP_SPAWN_RATE: 0.02,
LANE_WIDTH: 350,
WORLD_DEPTH: 3000,
};
// === STATE ===
let state = 'title'; // title, playing, over
let seed = 12345;
let rngState = seed;
function rng() { rngState = (rngState * 16807 + 0) % 2147483647; return (rngState - 1) / 2147483646; }
function seedRun(s) { seed = s; rngState = seed; }
let player = { x: 0, y: 0, z: 0, vx: 0, vy: 0, pitch: 0, roll: 0, energy: CONFIG.ENERGY_MAX, speed: 0, distance: 0, score: 0, multiplier: 1, boost: false, alive: true, nearMissTimer: 0 };
let sunY = CONFIG.SUN_START_ANG;
let sunColor = [255, 200, 80];
let camera = { x: 0, y: 0, shake: 0 };
let obstacles = [];
let pickups = [];
let particles = [];
let speedLines = [];
let time = 0;
// === KEYBOARD ===
const keys = { w:false, a:false, s:false, d:false, arrowleft:false, arrowright:false, arrowup:false, arrowdown:false, space:false, r:false };
window.addEventListener('keydown', e => {
const k = e.key.toLowerCase();
if (k in keys) keys[k] = true;
if (e.key === 'r' || e.key === 'R') { if (state === 'over') startGame(); else if (state === 'title') startGame(); }
});
window.addEventListener('keyup', e => {
const k = e.key.toLowerCase();
if (k in keys) keys[k] = false;
});
// === SOUND ===
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioCtx();
let droneOsc, droneGain;
function startDrone() {
if (droneOsc) return;
droneOsc = audioCtx.createOscillator();
droneOsc.type = 'triangle';
droneOsc.frequency.value = 110;
droneGain = audioCtx.createGain();
droneGain.gain.value = 0.05;
droneOsc.connect(droneGain);
droneGain.connect(audioCtx.destination);
droneOsc.start();
}
function updateDrone(s) {
if (!droneOsc) return;
droneOsc.frequency.value = 80 + Math.min(s, 120) * 2;
droneGain.gain.value = 0.02 + Math.min(s, 120) / 6000;
}
function playBlip(freq, gain, type='triangle', dur=0.08) {
try {
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.type = type;
o.frequency.setValueAtTime(freq, audioCtx.currentTime);
g.gain.setValueAtTime(gain, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur);
o.connect(g); g.connect(audioCtx.destination);
o.start(); o.stop(audioCtx.currentTime + dur);
} catch(e){}
}
function playCrash() {
try {
const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
o.type = 'sawtooth'; o.frequency.setValueAtTime(80, audioCtx.currentTime);
o.frequency.exponentialRampToValueAtTime(20, audioCtx.currentTime + 0.3);
g.gain.setValueAtTime(0.15, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime + 0.3);
} catch(e){}
}
// === PARTICLES ===
function spawnParticles(x, y, count, color, speedRange, lifeRange) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const spd = speedRange.min + Math.random() * (speedRange.max - speedRange.min);
particles.push({ x, y, vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd, life: lifeRange.min + Math.random() * (lifeRange.max - lifeRange.min), maxLife: 0, color, size: 2 + Math.random() * 4 });
}
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx; p.y += p.vy; p.vy += 0.02; p.life -= 1;
if (p.life <= 0) particles.splice(i, 1);
}
}
// === WORLD GENERATION ===
function spawnObstacle() {
const z = player.z + 800 + Math.random() * 1200;
const height = 30 + Math.random() * 80;
const width = 40 + Math.random() * 40;
const x = (Math.random() - 0.5) * 500;
const lane = Math.floor(Math.random() * 3) - 1; // -1, 0, 1 lanes-ish
const xLane = lane * CONFIG.LANE_WIDTH + (Math.random() - 0.5) * 180;
obstacles.push({
x: xLane, y: height / 2, z, width, height, depth: 20 + Math.random() * 30,
color: [20, 30, 50], shadow: true,
});
}
function spawnPickup() {
const z = player.z + 600 + Math.random() * 800;
const x = (Math.random() - 0.5) * 500;
const type = Math.random() < 0.5 ? 'energy' : (Math.random() < 0.5 ? 'token' : 'mult');
const y = 40 + Math.random() * 80;
pickups.push({ x, y, z, type, life: 1, pulse: 0 });
}
// === DRAWING ===
function drawSun(t) {
const cx = canvas.width / 2;
const sunYPos = canvas.height * (0.3 + Math.max(0, sunY) * 0.4);
const radius = Math.max(80, 200 - sunY * 300);
// Gradient glow
const grad = ctx.createRadialGradient(cx, sunYPos, radius * 0.2, cx, sunYPos, radius * 1.5);
grad.addColorStop(0, `rgba(255, 220, 120, 0.9)`);
grad.addColorStop(0.3, `rgba(255, 180, 60, 0.5)`);
grad.addColorStop(0.7, `rgba(255, 100, 20, 0.15)`);
grad.addColorStop(1, `rgba(255, 80, 0, 0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(cx, sunYPos, radius * 1.5, 0, Math.PI * 2);
ctx.fill();
// Core sun
ctx.fillStyle = `rgba(255, 250, 220, 1)`;
ctx.beginPath();
ctx.arc(cx, sunYPos, radius * 0.35, 0, Math.PI * 2);
ctx.fill();
}
function drawWorld(t) {
// Sky gradient
const top = `rgba(255, 160, 60, 1)`;
const bot = `rgba(20, 20, 50, 1)`;
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, `rgba(255, 140, 40, 0.4)`);
grad.addColorStop(0.35 + Math.max(0, sunY) * 0.3, `rgba(255, 200, 80, 0.8)`);
grad.addColorStop(0.7, `rgba(20, 30, 60, 1)`);
grad.addColorStop(1, `rgba(5, 8, 20, 1)`);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawSun(t);
// Ground plane with lanes
const groundY = canvas.height * 0.72;
// Shadow zone under sun
const shadowGrad = ctx.createLinearGradient(0, groundY, 0, canvas.height);
shadowGrad.addColorStop(0, `rgba(0,0,20,0.6)`);
shadowGrad.addColorStop(1, `rgba(5,8,25,0.9)`);
ctx.fillStyle = shadowGrad;
ctx.fillRect(0, groundY, canvas.width, canvas.height - groundY);
// Lane markers moving backward
const laneCount = 3;
ctx.save();
ctx.translate(0, groundY);
const zOffset = (player.z * 0.015) % 80;
for (let i = -10; i < 15; i++) {
for (let j = -1; j <= 1; j++) {
const x = canvas.width / 2 + j * CONFIG.LANE_WIDTH + Math.sin(i * 0.3 + t * 0.02) * 20;
const yOffset = (i * 80) - zOffset + 20;
if (yOffset < 0 || yOffset > 600) continue;
const alpha = Math.max(0, 1 - yOffset / 500);
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.15})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x - 20, yOffset);
ctx.lineTo(x - 20, yOffset + 30);
ctx.stroke();
}
}
ctx.restore();
// Draw obstacles
for (const o of obstacles) {
const projX = canvas.width / 2 + o.x * 0.2;
const projY = groundY - (o.z - player.z) * 0.015 + o.y - 30;
const scale = Math.max(0.1, 1 - (o.z - player.z) / 1600);
if (projY > canvas.height + 100 || projY < -200) continue;
ctx.save();
ctx.translate(projX, projY);
ctx.scale(scale, scale);
// Shadow
ctx.fillStyle = `rgba(0,0,10,${0.3 * scale})`;
ctx.fillRect(-o.width/2, 5, o.width, 15);
// Body
const grad = ctx.createLinearGradient(0, -o.height/2, 0, o.height/2);
grad.addColorStop(0, `rgba(60, 60, 90, 0.95)`);
grad.addColorStop(0.5, `rgba(30, 35, 60, 0.95)`);
grad.addColorStop(1, `rgba(10, 15, 30, 0.9)`);
ctx.fillStyle = grad;
ctx.fillRect(-o.width/2, -o.height/2, o.width, o.height);
// Edge highlight
ctx.strokeStyle = `rgba(255,255,255,${0.1 * scale})`;
ctx.lineWidth = 1;
ctx.strokeRect(-o.width/2, -o.height/2, o.width, o.height);
ctx.restore();
}
// Draw pickups
for (const p of pickups) {
const projX = canvas.width / 2 + p.x * 0.2;
const projY = groundY - (p.z - player.z) * 0.015 - 20;
const scale = Math.max(0.2, 1 - (p.z - player.z) / 1200);
if (projY > canvas.height + 50 || projY < -50) continue;
ctx.save();
ctx.translate(projX, projY);
ctx.scale(scale, scale);
p.pulse += 0.15;
const pulse = 1 + Math.sin(p.pulse) * 0.2;
const size = (p.type === 'energy' ? 14 : (p.type === 'mult' ? 12 : 16)) * pulse;
// Glow
ctx.shadowColor = p.type === 'energy' ? '#ffaa00' : (p.type === 'mult' ? '#ff00aa' : '#ffffff');
ctx.shadowBlur = 15 * pulse;
if (p.type === 'energy') {
ctx.fillStyle = '#ffaa00';
ctx.beginPath(); ctx.arc(0, 0, size, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#ffeeaa';
ctx.beginPath(); ctx.arc(0, 0, size*0.4, 0, Math.PI*2); ctx.fill();
} else if (p.type === 'mult') {
ctx.fillStyle = '#ff44aa';
ctx.beginPath();
for (let a = 0; a < 5; a++) {
const ang = (a / 5) * Math.PI * 2;
const rx = Math.cos(ang) * size; const ry = Math.sin(ang) * size * 0.6;
if (a === 0) ctx.moveTo(rx, ry); else ctx.lineTo(rx, ry);
}
ctx.closePath(); ctx.fill();
} else {
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(0, 0, size, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(0, 0, size*0.3, 0, Math.PI*2); ctx.stroke();
}
ctx.shadowBlur = 0;
ctx.restore();
}
// Player craft
const px = canvas.width / 2 + player.x * 0.2;
const py = groundY - 40 + player.y - player.z * 0.015 + player.pitch * 2;
ctx.save();
ctx.translate(px, py);
ctx.rotate(Math.PI * player.roll / 180);
ctx.rotate(Math.PI * player.pitch / 120); // Lean forward/back
// Shadow
ctx.fillStyle = `rgba(0,0,0,0.3)`;
ctx.fillRect(-20, 10, 40, 8);
// Main body
ctx.save();
ctx.translate(0, -10);
const craftGrad = ctx.createLinearGradient(0, -30, 0, 20);
craftGrad.addColorStop(0, `rgba(255, 220, 240, 1)`);
grad.addColorStop(0.5, `rgba(255, 160, 80, 0.9)`);
grad.addColorStop(1, `rgba(200, 100, 30, 1)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, -20);
ctx.lineTo(25, 15);
ctx.lineTo(-25, 15);
ctx.closePath();
ctx.fill();
// Cockpit glow
ctx.fillStyle = `rgba(255, 255, 240, 0.95)`;
ctx.beginPath();
ctx.arc(0, -2, 5, 0, Math.PI*2); ctx.fill();
// Wings
ctx.fillStyle = `rgba(255, 200, 100, 0.9)`;
ctx.beginPath();
ctx.moveTo(-22, 5);
ctx.lineTo(-55, 2);
ctx.lineTo(-30, 15);
ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(22, 5);
ctx.lineTo(55, 2);
ctx.lineTo(30, 15);
ctx.closePath(); ctx.fill();
// Side boosters glowing if boosting
if (keys.space && player.boost) {
ctx.fillStyle = `rgba(255, 255, 150, 0.9)`;
ctx.beginPath();
ctx.arc(-22, 8, 4, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = `rgba(255, 255, 150, 0.9)`;
ctx.beginPath();
ctx.arc(22, 8, 4, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
// Thrust particles
if (state === 'playing') {
for (let i = 0; i < 3; i++) {
const px2 = -10 + Math.random() * 20;
const py2 = 18 + Math.random() * 10;
const spd = 1 + Math.random() * 3;
particles.push({
x: px + px2, y: py + py2,
vx: (Math.random() - 0.5) * spd - 3,
vy: Math.random() * spd + 2,
life: 5 + Math.random() * 10,
maxLife: 20,
color: ['rgba(255,200,80,0.8)', 'rgba(255,150,50,0.9)', 'rgba(255,255,240,0.7)'][Math.floor(Math.random()*3)],
size: 2 + Math.random() * 3
});
}
}
ctx.restore();
// Speed lines
if (player.speed > 60) {
ctx.save();
ctx.strokeStyle = `rgba(255, 255, 255, ${Math.min(0.4, (player.speed - 60) / 100)})`;
ctx.lineWidth = 1.5 + (player.speed - 60) / 40;
const count = 40 + Math.floor(player.speed / 3);
for (let i = 0; i < count; i++) {
const lx = Math.random() * canvas.width;
const ly = Math.random() * canvas.height;
const len = 20 + Math.random() * 60;
ctx.beginPath();
ctx.moveTo(lx, ly);
ctx.lineTo(lx - len * 0.3, ly + len);
ctx.stroke();
}
ctx.restore();
}
// Chromatic aberration at high speed
if (player.speed > 80) {
const offset = (player.speed - 80) * 2;
// Just shift drawing slightly - done via canvas composite will be complex; we'll rely on visual effects
}
}
function drawParticles() {
for (const p of particles) {
const alpha = Math.max(0, p.life / p.maxLife);
ctx.fillStyle = p.color.replace(/\)$/, `,${alpha})`).replace(/rgba/, 'rgba');
// Simple approach
ctx.fillStyle = p.color.includes('rgba') ? p.color.replace(')', `,${alpha})`) : `rgba(255,255,255,${alpha})`;
// Better parse
ctx.fillStyle = p.color; // We'll just draw with alpha
ctx.globalAlpha = alpha;
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
ctx.globalAlpha = 1;
}
}
// === UPDATE ===
function update(t) {
time = t;
if (state === 'title') {
// Subtle sun animation
sunY = CONFIG.SUN_START_ANG + Math.sin(t * 0.0002) * 0.05;
camera.shake *= CONFIG.SHAKE_DECAY;
sunColor = [255, 200, 80];
return;
}
if (state === 'playing') {
// Speed
player.speed = Math.min(CONFIG.MAX_SPEED, player.speed + CONFIG.ACCEL);
player.z += player.speed * 0.02;
player.distance += player.speed * 0.02;
player.score += player.distance * 0.0002 * (1 + player.multiplier);
// Sun drops
sunY = Math.max(CONFIG.SUN_END_ANG, sunY - CONFIG.SUN_DROP_SPEED);
if (sunY <= CONFIG.SUN_END_ANG) { endGame(); return; }
// Controls
const targetPitch = (keys.arrowdown || keys.s ? 1 : 0) - (keys.arrowup || keys.w ? 1 : 0);
const targetRoll = (keys.arrowright || keys.d ? 1 : 0) - (keys.arrowleft || keys.a ? 1 : 0);
player.pitch += (targetPitch * CONFIG.PITCH_SPEED - player.pitch) * 0.15;
player.roll += (targetRoll * CONFIG.BANK_ANGLE_MAX - player.roll) * 0.12;
// Strafe / vertical
const strafeTarget = (keys.arrowright || keys.d ? 1 : 0) - (keys.arrowleft || keys.a ? 1 : 0);
const pitchTarget = (keys.arrowdown || keys.s ? 1 : 0) - (keys.arrowup || keys.w ? 1 : 0);
player.x += strafeTarget * CONFIG.STRAFE_SPEED * 0.15;
player.y += pitchTarget * 1.5 * 0.15;
player.y = Math.max(-60, Math.min(80, player.y));
// Boost
player.boost = keys.space && player.energy > CONFIG.BOOST_COST;
if (player.boost) {
player.energy -= CONFIG.BOOST_DRAIN;
player.speed = CONFIG.MAX_SPEED * CONFIG.BOOST_MULT;
playBlip(600 + Math.random() * 400, 0.05, 'triangle', 0.05);
}
// Energy drain
const inShadow = isInShadow();
player.energy -= inShadow ? CONFIG.SHADOW_DRAIN : CONFIG.ENERGY_DRAIN;
if (player.energy < 0) { player.energy = 0; }
if (player.energy <= 0 && sunY < 0.05) { endGame(); return; }
// Spawn
if (Math.random() < CONFIG.OBSTACLE_SPAWN_RATE) spawnObstacle();
if (Math.random() < CONFIG.PICKUP_SPAWN_RATE) spawnPickup();
// Cleanup far objects
obstacles = obstacles.filter(o => o.z - player.z < 1500);
pickups = pickups.filter(p => p.z - player.z < 800);
// Collision
for (const o of obstacles) {
if (isColliding(player, o)) {
player.alive = false;
camera.shake = 30;
spawnParticles(
canvas.width / 2 + player.x * 0.2,
canvas.height * 0.72 - 30 + player.y - player.z * 0.015,
60,
['rgba(255,120,0,0.8)', 'rgba(255,200,50,0.9)'],
{min:1, max:8}, {min:10, max:40}
);
playCrash();
// Flash
setTimeout(() => endGame(), 400);
return;
}
// Near miss
const projX = canvas.width / 2 + o.x * 0.2;
const projY = canvas.height * 0.72 - (o.z - player.z) * 0.015 + o.y - 30;
const dist = Math.hypot(projX - (canvas.width/2 + player.x * 0.2), projY - (canvas.height * 0.72 - 30));
if (dist < CONFIG.NEAR_MISS_RANGE && o.z > player.z - 200 && o.z < player.z + 50) {
player.nearMissTimer = 5;
player.score += CONFIG.NEAR_MISS_SCORE * player.multiplier;
camera.shake = 4;
playBlip(800 + Math.random() * 400, 0.04, 'sine', 0.06);
spawnParticles(projX, projY, 8, 'rgba(255,255,255,0.6)', {min:0.5, max:3}, {min:5, max:15});
}
}
// Pickups
for (let i = pickups.length - 1; i >= 0; i--) {
const p = pickups[i];
const projX = canvas.width / 2 + p.x * 0.2;
const projY = canvas.height * 0.72 - (p.z - player.z) * 0.015 - 20;
const dist = Math.hypot(projX - (canvas.width/2 + player.x * 0.2), projY - (canvas.height * 0.72 - 30 + player.y));
if (dist < 30) {
if (p.type === 'energy') { player.energy = Math.min(CONFIG.ENERGY_MAX, player.energy + CONFIG.ENERGY_PICKUP); playBlip(900, 0.08, 'triangle', 0.1); }
else if (p.type === 'token') { player.score += 100 * player.multiplier; playBlip(1200, 0.08, 'sine', 0.1); }
else if (p.type === 'mult') { player.multiplier = Math.min(10, player.multiplier + CONFIG.MULT_PICKUP); player.score += 200; playBlip(1600, 0.1, 'square', 0.15); }
spawnParticles(projX, projY, 20, p.type === 'energy' ? 'rgba(255,200,50,0.8)' : (p.type === 'mult' ? 'rgba(255,60,150,0.8)' : 'rgba(255,255,255,0.9)'), {min:1, max:5}, {min:8, max:25});
pickups.splice(i, 1);
}
}
// Update HUD
document.getElementById('hudDist').textContent = Math.floor(player.distance);
document.getElementById('hudScore').textContent = Math.floor(player.score).toLocaleString();
document.getElementById('hudMult').textContent = 'x' + player.multiplier;
document.getElementById('energyFill').style.width = Math.round(player.energy) + '%';
document.getElementById('hudSpeed').textContent = Math.floor(player.speed);
// Near miss timer
if (player.nearMissTimer > 0) player.nearMissTimer--;
// Speed lines spawn
if (player.speed > 60 && Math.random() < 0.3) {
speedLines.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, len: 30 + Math.random() * 50, life: 1 });
}
// Camera shake decay
camera.shake *= CONFIG.SHAKE_DECAY;
camera.x = (Math.random() - 0.5) * camera.shake;
camera.y = (Math.random() - 0.5) * camera.shake;
}
updateParticles();
updateDrone(player.speed);
}
// === COLLISION ===
function isInShadow() {
// Simple check: if sun is low and player is under obstacle shadow height approx
return sunY < 0.05 || player.y < -20; // In shadow when low sun or diving under
}
function isColliding(p, o) {
const dx = p.x - o.x;
const dz = (p.z + 100) - o.z; // Craft depth
return Math.abs(dx) < o.width / 2 + 20 && Math.abs(dz) < o.depth / 2 + 20 && p.y > o.y - o.height / 2 && p.y < o.y + o.height / 2;
}
// === GAME FLOW ===
function startGame() {
seedRun(Math.floor(Math.random() * 100000));
player = { x: 0, y: 0, z: 0, vx: 0, vy: 0, pitch: 0, roll: 0, energy: CONFIG.ENERGY_MAX, speed: 0, distance: 0, score: 0, multiplier: 1, boost: false, alive: true, nearMissTimer: 0 };
sunY = CONFIG.SUN_START_ANG;
obstacles = []; pickups = []; particles = []; speedLines = [];
camera = { x: 0, y: 0, shake: 0 };
state = 'playing';
document.getElementById('titleOverlay').style.display = 'none';
document.getElementById('gameOverOverlay').style.display = 'none';
document.getElementById('gameOverOverlay').style.opacity = 0;
time = 0;
if (audioCtx.state === 'suspended') audioCtx.resume();
startDrone();
}
function endGame() {
state = 'over';
const hs = parseInt(localStorage.getItem('solarDriftHS') || '0');
if (player.score > hs) { localStorage.setItem('solarDriftHS', Math.floor(player.score).toString()); document.getElementById('highScoreVal').textContent = Math.floor(player.score).toLocaleString(); }
else { document.getElementById('highScoreVal').textContent = hs.toLocaleString(); }
document.getElementById('finalScore').textContent = Math.floor(player.score).toLocaleString();
document.getElementById('gameOverOverlay').style.display = 'flex';
setTimeout(() => { document.getElementById('gameOverOverlay').style.opacity = 1; }, 10);
}
// === MAIN LOOP ===
function gameLoop() {
const t = performance.now() / 1000;
update(t);
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(camera.x, camera.y);
drawWorld(t);
drawParticles();
ctx.restore();
requestAnimationFrame(gameLoop);
}
// Initialize
function init() {
document.getElementById('highScoreDisplay').style.display = 'block';
document.getElementById('highScoreVal').textContent = parseInt(localStorage.getItem('solarDriftHS') || '0').toLocaleString();
}
init();
gameLoop();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"bf6e1a34-15f3-48b6-8951-44cbd7b2b852","tokensIn":514224,"tokensOut":16486,"tokensTotal":530710,"turns":26,"toolCalls":25,"failedToolCalls":2,"timestamp":"2026-07-18T22:28:18.598Z"} -->