Race the Sun
thinkingcap qwen3.6 27b · composer

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 40,211 bytes · SHA-256 5375fc17e1fe
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Race the Sun — Solar Runner</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a12; font-family: 'Segoe UI', system-ui, sans-serif; }
#game { display: block; width: 100vw; height: 100vh; }
/* HUD */
#hud { position: fixed; top: 0; left: 0; right: 0; pointer-events: none; z-index: 10; padding: 16px 24px; display: flex; align-items: center; gap: 20px; }
#score-line { color: #fff; font-size: 18px; font-weight: 700; text-shadow: 0 0 8px rgba(255,160,50,.6); letter-spacing: .5px; white-space: nowrap; }
#multiplier { color: #ffd43b; font-size: 22px; font-weight: 900; text-shadow: 0 0 12px rgba(255,212,59,.7); min-width: 50px; }
#energy-bar-wrap { flex: 1; max-width: 260px; height: 14px; background: rgba(0,0,0,.5); border-radius: 7px; overflow: hidden; border: 1px solid rgba(255,255,255,.15); }
#energy-bar { width: 100%; height: 100%; background: linear-gradient(90deg, #ff6b35, #ffd43b, #7bed9f); border-radius: 7px; transition: width .15s ease-out; }
#speed-display { color: rgba(255,255,255,.7); font-size: 14px; font-weight: 600; min-width: 70px; text-align: right; white-space: nowrap; }
/* Title & Game Over screens */
#title-screen, #gameover-screen { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; background: radial-gradient(ellipse at 50% 60%, rgba(30,15,40,.85), rgba(5,5,15,.97)); }
#title-screen h1 { font-size: clamp(36px, 7vw, 72px); color: #ffd43b; text-shadow: 0 0 40px rgba(255,180,50,.5), 0 0 80px rgba(255,100,20,.3); letter-spacing: 6px; font-weight: 900; animation: titlePulse 3s ease-in-out infinite; }
@keyframes titlePulse { 0%,100% { text-shadow: 0 0 40px rgba(255,180,50,.5), 0 0 80px rgba(255,100,20,.3); } 50% { text-shadow: 0 0 60px rgba(255,180,50,.7), 0 0 100px rgba(255,100,20,.5); } }
.subtitle { color: #ff9f43; font-size: clamp(16px, 3vw, 28px); margin-top: 4px; letter-spacing: 4px; text-transform: uppercase; }
.controls { color: rgba(255,255,255,.5); font-size: 14px; margin-top: 24px; letter-spacing: 1px; }
#high-score-line { color: #ffd43b; font-size: 16px; margin-top: 12px; opacity: .8; }
button { pointer-events: all; margin-top: 32px; padding: 14px 48px; font-size: 18px; font-weight: 700; letter-spacing: 3px; border: none; border-radius: 6px; cursor: pointer; background: linear-gradient(135deg, #ff6b35, #ee5a24); color: #fff; box-shadow: 0 4px 24px rgba(255,107,53,.4); transition: transform .1s, box-shadow .1s; }
button:hover { transform: scale(1.05); box-shadow: 0 6px 32px rgba(255,107,53,.6); }
button:active { transform: scale(.97); }
#gameover-screen h1 { font-size: clamp(32px, 6vw, 64px); color: #ff4757; text-shadow: 0 0 40px rgba(255,71,87,.5); letter-spacing: 4px; }
#gameover-screen p { color: rgba(255,255,255,.8); font-size: 20px; margin-top: 8px; }
/* Sun indicator on HUD */
#sun-indicator { width: 16px; height: 16px; border-radius: 50%; background: #ffd43b; box-shadow: 0 0 8px rgba(255,212,59,.6); flex-shrink: 0; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud"><div id="sun-indicator"></div>
<div id="score-line"><span id="dist">0m</span> · <span id="score">0</span></div>
<div id="multiplier">×1.0</div>
<div id="energy-bar-wrap"><div id="energy-bar"></div></div>
<div id="speed-display">0 km/h</div>
</div>
<div id="title-screen">
<h1>SOLAR RUNNER</h1>
<p class="subtitle">Race the Sun</p>
<p class="controls">WASD / Arrows — Move · Space — Boost · R — Restart</p>
<div id="high-score-line"></div>
<button id="start-btn">LAUNCH</button>
</div>
<div id="gameover-screen" style="display:none">
<h1>SOLAR FLARE</h1>
<p id="go-dist">Distance: 0m</p>
<p id="go-score">Score: 0</p>
<p id="go-highscore"></p>
<button id="restart-btn">RELAUNCH (R)</button>
</div>
<script>
'use strict';
// ── const: CFG ──
// === CONSTANTS ===
const CFG = {
// Player
PLAYER_W: 28, PLAYER_H: 16, PLAYER_DEPTH: 40,
MOVE_SPEED: 20, MOVE_EASE: 0.18, BANK_ANGLE: 0.35, PITCH_LEAN: 0.2,
BOOST_SPEED_MULT: 1.6, BOOST_DRAIN: 0.7, BOOST_COOLDOWN: 0.4,
// World
WORLD_WIDTH: 800, LANE_COUNT: 5,
SEGMENT_LENGTH: 80, DRAW_DISTANCE: 60,
BASE_SPEED: 120, MAX_SPEED: 400, SPEED_RAMP: 0.35,
// Sun & Energy
SUN_START_Y: 0.35, SUN_SET_TIME: 100,
ENERGY_MAX: 100, ENERGY_DRAIN: 0.6, SHADOW_DRAIN_MULT: 2.5,
PICKUP_ENERGY: 18, SUN_RESCUE_AMOUNT: 0.03,
// Obstacles
OBSTACLE_SPAWN_RATE: 0.35, OBSTACLE_MIN_GAP: 3,
NEAR_MISS_DIST: 42,
// Scoring
BASE_SCORE_PER_METER: 1, NEAR_MISS_BONUS: 50,
// Visuals
PARTICLE_MAX: 200, SHAKE_DECAY: 0.85,
};
// ── function: seededRandom ──
// === UTILITIES ===
function seededRandom(seed) {
let s = seed;
return function() {
s = (s * 16807 + 0) % 2147483647;
return (s - 1) / 2147483646;
};
}
// ── function: lerp ──
function lerp(a, b, t) { return a + (b - a) * t; }
// ── function: clamp ──
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
// ── function: randRange ──
function randRange(lo, hi) { return lo + Math.random() * (hi - lo); }
// ── let: _rng ──
// Seeded RNG for world generation
let _rng = null;
// ── function: getRNG ──
function getRNG() { return _rng || (_rng = seededRandom(Date.now() & 0x7fffffff)); }
// ── let: _audioCtx ──
// === AUDIO SYSTEM ===
let _audioCtx = null;
// ── let: _droneOsc ──
let _droneOsc = null;
// ── let: _droneGain ──
let _droneGain = null;
// ── function: initAudio ──
function initAudio() {
if (_audioCtx) return;
try {
_audioCtx = new (window.AudioContext || window.webkitAudioContext)();
_droneGain = _audioCtx.createGain();
_droneGain.gain.value = 0.03;
_droneGain.connect(_audioCtx.destination);
_droneOsc = _audioCtx.createOscillator();
_droneOsc.type = 'sawtooth';
_droneOsc.frequency.value = 55;
const droneFilter = _audioCtx.createBiquadFilter();
droneFilter.type = 'lowpass';
droneFilter.frequency.value = 120;
_droneOsc.connect(droneFilter);
droneFilter.connect(_droneGain);
_droneOsc.start();
} catch(e) {}
}
// ── function: updateDrone ──
function updateDrone(speedRatio) {
if (!_droneOsc || !_audioCtx) return;
const t = _audioCtx.currentTime;
_droneOsc.frequency.setTargetAtTime(50 + speedRatio * 40, t, 0.1);
_droneGain.gain.setTargetAtTime(0.02 + speedRatio * 0.03, t, 0.1);
}
// ── function: playPickup ──
function playPickup(freq) {
freq = freq || 880;
if (!_audioCtx) return;
const t = _audioCtx.currentTime;
const o = _audioCtx.createOscillator();
const g = _audioCtx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(freq, t);
o.frequency.exponentialRampToValueAtTime(freq * 1.5, t + 0.08);
g.gain.setValueAtTime(0.12, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
o.connect(g).connect(_audioCtx.destination);
o.start(t);
o.stop(t + 0.15);
}
// ── function: playCrash ──
function playCrash() {
if (!_audioCtx) return;
const t = _audioCtx.currentTime;
for (let i = 0; i < 4; i++) {
const o = _audioCtx.createOscillator();
const g = _audioCtx.createGain();
o.type = 'square';
o.frequency.value = 80 + Math.random() * 200;
g.gain.setValueAtTime(0.15, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
o.connect(g).connect(_audioCtx.destination);
o.start(t);
o.stop(t + 0.3);
}
}
// ── function: playNearMiss ──
function playNearMiss() {
if (!_audioCtx) return;
const t = _audioCtx.currentTime;
const o = _audioCtx.createOscillator();
const g = _audioCtx.createGain();
o.type = 'triangle';
o.frequency.setValueAtTime(1200, t);
o.frequency.exponentialRampToValueAtTime(600, t + 0.1);
g.gain.setValueAtTime(0.08, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
o.connect(g).connect(_audioCtx.destination);
o.start(t);
o.stop(t + 0.12);
}
// ── function: playBoost ──
function playBoost() {
if (!_audioCtx) return;
const t = _audioCtx.currentTime;
const o = _audioCtx.createOscillator();
const g = _audioCtx.createGain();
o.type = 'sawtooth';
o.frequency.setValueAtTime(200, t);
o.frequency.exponentialRampToValueAtTime(800, t + 0.15);
g.gain.setValueAtTime(0.06, t);
g.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
o.connect(g).connect(_audioCtx.destination);
o.start(t);
o.stop(t + 0.2);
}
// ── let: particles ──
// === PARTICLE SYSTEM ===
let particles = [];
// ── function: spawnParticle ──
function spawnParticle(x, y, vx, vy, life, color, size) {
if (particles.length >= CFG.PARTICLE_MAX) return;
particles.push({ x, y, vx, vy, life, maxLife: life, color, size });
}
// ── function: emitThrust ──
function emitThrust(cx, cy, angle) {
for (let i = 0; i < 2; i++) {
const spread = randRange(-0.3, 0.3);
spawnParticle(
cx + randRange(-4, 4), cy + 8,
Math.sin(angle + spread) * -150 + randRange(-20, 20),
Math.cos(angle + spread) * -150 + randRange(-20, 20),
randRange(0.3, 0.6),
i % 2 === 0 ? '#ff9f43' : '#ffd43b',
randRange(2, 5)
);
}
}
// ── function: emitPickupBurst ──
function emitPickupBurst(x, y, color) {
for (let i = 0; i < 16; i++) {
const a = Math.random() * Math.PI * 2;
const sp = randRange(40, 150);
spawnParticle(x, y, Math.cos(a) * sp, Math.sin(a) * sp, randRange(0.3, 0.7), color, randRange(2, 6));
}
}
// ── function: emitCrashBurst ──
function emitCrashBurst(x, y) {
for (let i = 0; i < 40; i++) {
const a = Math.random() * Math.PI * 2;
const sp = randRange(50, 300);
spawnParticle(x, y, Math.cos(a) * sp, Math.sin(a) * sp, randRange(0.4, 1.2),
['#ff4757', '#ff6b81', '#ffa502', '#fff'][Math.floor(Math.random() * 4)],
randRange(3, 8));
}
}
// ── function: updateParticles ──
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
}
// ── function: drawParticles ──
function drawParticles(ctx) {
for (const p of particles) {
const alpha = clamp(p.life / p.maxLife, 0, 1);
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// ── let: player ──
// === PLAYER ===
let player = null;
// ── function: createPlayer ──
function createPlayer() {
return {
x: 0, y: 0, // world position (x=strafe, y=pitch)
targetX: 0, targetY: 0,
bankAngle: 0, pitchLean: 0,
energy: CFG.ENERGY_MAX,
alive: true,
invincibleTimer: 0,
};
}
// ── function: updatePlayer ──
function updatePlayer(p, dt) {
// Smooth movement with easing
const ease = 1 - Math.pow(1 - CFG.MOVE_EASE, dt * 60);
p.x += (p.targetX - p.x) * ease;
p.y += (p.targetY - p.y) * ease;
// Bank angle follows horizontal velocity
const dx = p.targetX - p.x;
const targetBank = clamp(dx * 0.015, -CFG.BANK_ANGLE, CFG.BANK_ANGLE);
p.bankAngle += (targetBank - p.bankAngle) * ease * 2;
// Pitch lean follows vertical velocity
const dy = p.targetY - p.y;
const targetPitch = clamp(dy * 0.01, -CFG.PITCH_LEAN, CFG.PITCH_LEAN);
p.pitchLean += (targetPitch - p.pitchLean) * ease * 2;
// Clamp position to world bounds
const halfW = CFG.WORLD_WIDTH / 2 - 30;
p.targetX = clamp(p.targetX, -halfW, halfW);
p.targetY = clamp(p.targetY, -150, 100);
// Invincibility timer
if (p.invincibleTimer > 0) p.invincibleTimer -= dt;
}
// ── function: drawPlayer ──
function drawPlayer(ctx, cx, cy, bankAngle, pitchLean, flashAlpha) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(bankAngle);
const w = CFG.PLAYER_W;
const d = CFG.PLAYER_DEPTH;
// Engine trail glow
const engineGlow = 0.4 + Math.sin(Date.now() * 0.015) * 0.2;
ctx.fillStyle = `rgba(255,180,50,${engineGlow})`;
ctx.beginPath();
ctx.arc(-w/3, d/2 + 4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(w/3, d/2 + 4, 4, 0, Math.PI * 2);
ctx.fill();
// Shadow beneath craft
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.beginPath();
ctx.ellipse(2, 16, w * 0.7, 5, 0, 0, Math.PI * 2);
ctx.fill();
// Main body - sleek fighter shape
const grad = ctx.createLinearGradient(-w/2, -d/2, w/2, d/2);
grad.addColorStop(0, '#e84393');
grad.addColorStop(0.5, '#fd79a8');
grad.addColorStop(1, '#ff6b35');
ctx.fillStyle = grad;
ctx.beginPath();
// Nose point
ctx.moveTo(0, -d/2);
// Right wing tip (swept back)
ctx.lineTo(w/2 + 6, d/4);
// Right engine housing
ctx.lineTo(w/3, d/2);
// Tail notch
ctx.lineTo(-w/8, d/2 + 3);
// Left engine housing
ctx.lineTo(-w/3, d/2);
// Left wing tip (swept back)
ctx.lineTo(-w/2 - 6, d/4);
ctx.closePath();
ctx.fill();
// Body outline for definition
ctx.strokeStyle = 'rgba(255,200,180,0.3)';
ctx.lineWidth = 1;
ctx.stroke();
// Cockpit highlight
const cockpitGrad = ctx.createLinearGradient(0, -d/2 + 4, 0, d/6);
cockpitGrad.addColorStop(0, 'rgba(255,255,255,0.5)');
cockpitGrad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = cockpitGrad;
ctx.beginPath();
ctx.moveTo(0, -d/2 + 6);
ctx.lineTo(w/4, d/6);
ctx.lineTo(-w/4, d/6);
ctx.closePath();
ctx.fill();
// Wing accents
ctx.strokeStyle = 'rgba(255,180,100,0.4)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-w/3, d/6);
ctx.lineTo(-w/2 - 4, d/4 + 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(w/3, d/6);
ctx.lineTo(w/2 + 4, d/4 + 2);
ctx.stroke();
// Damage flash overlay
if (flashAlpha > 0) {
ctx.fillStyle = `rgba(255,100,100,${flashAlpha})`;
ctx.fill();
}
ctx.restore();
}
// ── let: worldSegments ──
// === WORLD & OBSTACLES ===
let worldSegments = [];
// ── let: pickups ──
let pickups = [];
// ── let: distanceTraveled ──
let distanceTraveled = 0;
// ── let: gameTime ──
let gameTime = 0;
// ── const: OBS_TYPES ──
const OBS_TYPES = ['block', 'tall', 'wide', 'low'];
// ── const: PICKUP_TYPES ──
const PICKUP_TYPES = ['energy', 'score', 'multiplier'];
// ── function: resetWorld ──
function resetWorld() {
worldSegments = [];
pickups = [];
distanceTraveled = 0;
gameTime = 0;
_rng = seededRandom(Date.now() & 0x7fffffff);
}
// ── function: spawnSegment ──
function spawnSegment(z) {
const rng = getRNG();
const seg = { z, obstacles: [], hasShadow: false };
// Difficulty scales with time - more obstacles as game progresses
const difficulty = clamp(gameTime / 120, 0, 1);
const spawnChance = CFG.OBSTACLE_SPAWN_RATE + difficulty * 0.3;
if (rng() < spawnChance) {
// Ensure minimum gap between obstacle groups
const lastSeg = worldSegments[worldSegments.length - 1];
if (lastSeg && lastSeg.obstacles.length > 0 &&
z - lastSeg.z < CFG.OBSTACLE_MIN_GAP * CFG.SEGMENT_LENGTH) {
return; // Skip this segment to maintain gaps
}
const count = Math.floor(rng() * (2 + difficulty * 2)) + 1;
for (let i = 0; i < count; i++) {
const typeRoll = rng();
let type, ow, oh;
if (typeRoll < 0.35) {
// Standard block
type = 'block';
ow = randRange(30, 60);
oh = randRange(40, 80);
} else if (typeRoll < 0.55) {
// Tall pillar - creates shadow zones
type = 'tall';
ow = randRange(25, 45);
oh = randRange(100, 160);
} else if (typeRoll < 0.75) {
// Wide barrier - harder to dodge
type = 'wide';
ow = randRange(80, 140);
oh = randRange(30, 50);
} else {
// Low obstacle - must fly over
type = 'low';
ow = randRange(40, 70);
oh = randRange(20, 35);
}
const ox = (rng() - 0.5) * CFG.WORLD_WIDTH * 0.8;
const oy = -oh / 2 + randRange(-10, 20);
seg.obstacles.push({ x: ox, y: oy, w: ow, h: oh, type });
if (type === 'tall') seg.hasShadow = true;
}
}
worldSegments.push(seg);
}
// ── function: spawnPickup ──
function spawnPickup(z) {
const rng = getRNG();
if (rng() > 0.3) return; // 30% chance per segment
const typeRoll = rng();
let type;
if (typeRoll < 0.45) type = 'energy';
else if (typeRoll < 0.8) type = 'score';
else type = 'multiplier';
const px = (rng() - 0.5) * CFG.WORLD_WIDTH * 0.7;
const py = randRange(-80, 30);
pickups.push({ x: px, y: py, z, type, collected: false, bobPhase: rng() * Math.PI * 2 });
}
// ── function: updateWorld ──
function updateWorld(dt, speed) {
distanceTraveled += speed * dt;
gameTime += dt;
// Move segments toward player (z decreases)
for (const seg of worldSegments) {
seg.z -= speed * dt;
}
// Move pickups
for (const p of pickups) {
p.z -= speed * dt;
p.bobPhase += dt * 3;
}
// Remove passed segments/pickups
worldSegments = worldSegments.filter(s => s.z > -CFG.SEGMENT_LENGTH);
pickups = pickups.filter(p => p.z > -50 && !p.collected);
// Spawn new segments ahead
const maxZ = worldSegments.length ? Math.max(...worldSegments.map(s => s.z)) : 0;
const spawnAhead = CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH + CFG.SEGMENT_LENGTH * 5;
if (maxZ < spawnAhead) {
for (let z = maxZ + CFG.SEGMENT_LENGTH; z < spawnAhead; z += CFG.SEGMENT_LENGTH) {
spawnSegment(z);
spawnPickup(z);
}
}
}
// ── function: checkCollisions ──
function checkCollisions(p, segs) {
let nearMiss = false;
for (const seg of segs) {
if (seg.z < -10 || seg.z > CFG.SEGMENT_LENGTH * 2) continue;
for (const obs of seg.obstacles) {
// Simple AABB in world space
const px = p.x, py = p.y;
const halfW = CFG.PLAYER_W / 2 + 4;
const halfH = CFG.PLAYER_H / 2 + 4;
if (px + halfW > obs.x - obs.w/2 && px - halfW < obs.x + obs.w/2 &&
py + halfH > obs.y && py - halfH < obs.y + obs.h) {
// Collision!
return { hit: true, nearMiss: false };
}
// Near miss check
const margin = CFG.NEAR_MISS_DIST;
if (px + halfW + margin > obs.x - obs.w/2 && px - halfW - margin < obs.x + obs.w/2 &&
py + halfH + margin > obs.y && py - halfH - margin < obs.y + obs.h) {
nearMiss = true;
}
}
}
return { hit: false, nearMiss };
}
// ── function: checkPickupCollisions ──
function checkPickupCollisions(p) {
for (const pk of pickups) {
if (pk.collected || pk.z < -10 || pk.z > CFG.SEGMENT_LENGTH * 2) continue;
const dx = p.x - pk.x;
const dy = p.y - pk.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 35) {
pk.collected = true;
return pk.type;
}
}
return null;
}
// ── function: isInShadow ──
function isInShadow(p, segs) {
for (const seg of segs) {
if (!seg.hasShadow || seg.z < -10 || seg.z > CFG.SEGMENT_LENGTH * 3) continue;
// If player is behind a tall obstacle relative to sun position
return true;
}
return false;
}
// ── let: canvas ──
// === RENDERING STATE ===
let canvas = null;
// ── let: ctx ──
let ctx = null;
// ── let: W ──
let W = 0;
// ── let: H ──
let H = 0;
// ── function: resizeCanvas ──
function resizeCanvas() {
if (!canvas) return;
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
// ── function: getSunY ──
function getSunY() {
const progress = clamp(gameTime / CFG.SUN_SET_TIME, 0, 1);
const baseY = lerp(CFG.SUN_START_Y, 1.15, Math.pow(progress, 0.7));
// Sun reserve pushes sun back up slightly
return clamp(baseY - sunReserve * 0.15, CFG.SUN_START_Y - 0.05, 1.15);
}
// ── function: lerpColor ──
function lerpColor(c1, c2, t) {
const r1 = parseInt(c1.slice(1,3),16), g1 = parseInt(c1.slice(3,5),16), b1 = parseInt(c1.slice(5,7),16);
const r2 = parseInt(c2.slice(1,3),16), g2 = parseInt(c2.slice(3,5),16), b2 = parseInt(c2.slice(5,7),16);
const r = Math.round(lerp(r1,r2,t)), g = Math.round(lerp(g1,g2,t)), b = Math.round(lerp(b1,b2,t));
return `rgb(${r},${g},${b})`;
}
// ── function: drawSky ──
function drawSky() {
const sunY = getSunY();
const grad = ctx.createLinearGradient(0, 0, 0, H);
const darkness = clamp(sunY - 0.35, 0, 1) * 0.6;
grad.addColorStop(0, lerpColor('#1a0533', '#050208', darkness));
grad.addColorStop(0.3, lerpColor('#4a1942', '#0a0510', darkness));
grad.addColorStop(0.5, lerpColor('#c0392b', '#1a0a0a', darkness));
grad.addColorStop(0.65, lerpColor('#e67e22', '#2d1810', darkness));
grad.addColorStop(0.75, lerpColor('#f39c12', '#3d2510', darkness));
grad.addColorStop(1, lerpColor('#ffeaa7', '#4a3020', darkness));
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
// ── function: drawSun ──
function drawSun() {
const sunY = getSunY();
const sx = W * 0.5;
const sy = H * sunY;
const radius = Math.min(W, H) * 0.12;
for (let i = 4; i >= 0; i--) {
const r = radius + i * radius * 0.6;
const alpha = 0.08 - i * 0.012;
ctx.fillStyle = `rgba(255,200,80,${alpha})`;
ctx.beginPath();
ctx.arc(sx, sy, r, 0, Math.PI * 2);
ctx.fill();
}
const sunGrad = ctx.createRadialGradient(sx, sy, 0, sx, sy, radius);
sunGrad.addColorStop(0, '#fff8e1');
sunGrad.addColorStop(0.3, '#ffd43b');
sunGrad.addColorStop(0.7, '#ff9f43');
sunGrad.addColorStop(1, 'rgba(255,100,20,0)');
ctx.fillStyle = sunGrad;
ctx.beginPath();
ctx.arc(sx, sy, radius, 0, Math.PI * 2);
ctx.fill();
}
// ── function: drawGround ──
function drawGround() {
const horizonY = H * getSunY() + 30;
const groundGrad = ctx.createLinearGradient(0, horizonY, 0, H);
groundGrad.addColorStop(0, '#1a0a2e');
groundGrad.addColorStop(0.5, '#0d0618');
groundGrad.addColorStop(1, '#050310');
ctx.fillStyle = groundGrad;
ctx.fillRect(0, horizonY, W, H - horizonY);
const vanishX = W / 2;
const numLines = 15;
ctx.strokeStyle = 'rgba(255,160,80,0.08)';
ctx.lineWidth = 1;
for (let i = 0; i < numLines; i++) {
const t = Math.pow(i / numLines, 1.5);
const y = lerp(horizonY, H, t);
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
for (let i = -8; i <= 8; i++) {
const bottomX = vanishX + i * W / 12;
ctx.beginPath();
ctx.moveTo(vanishX, horizonY);
ctx.lineTo(bottomX, H);
ctx.stroke();
}
}
// ── function: projectPoint ──
function projectPoint(worldZ, worldX, worldY) {
const camDist = 200;
const fov = 500;
const z = worldZ + camDist;
if (z <= 1) return null;
const scale = fov / z;
const screenX = W / 2 + worldX * scale;
const horizonY = H * getSunY() + 30;
const groundOffset = lerp(0, H - horizonY, clamp(worldZ / (CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH), 0, 1));
const screenY = horizonY + groundOffset - worldY * scale;
return { x: screenX, y: screenY, scale };
}
// ── function: drawObstacles ──
function drawObstacles() {
const sortedSegs = [...worldSegments].sort((a, b) => b.z - a.z);
for (const seg of sortedSegs) {
if (seg.z < 0 || seg.z > CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH) continue;
// Distance-based alpha fade for far obstacles
const distFade = clamp(seg.z / (CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH), 0, 1);
const alpha = lerp(0.3, 1, 1 - distFade);
for (const obs of seg.obstacles) {
const proj = projectPoint(seg.z, obs.x, obs.y + obs.h / 2);
if (!proj) continue;
const sw = obs.w * proj.scale;
const sh = obs.h * proj.scale;
ctx.globalAlpha = alpha;
// Shadow beneath obstacle
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.fillRect(proj.x - sw/2 + 5, proj.y - sh/2 + 5, sw, sh);
// Obstacle body with sunset-themed gradient
const obsGrad = ctx.createLinearGradient(proj.x - sw/2, proj.y - sh/2, proj.x + sw/2, proj.y + sh/2);
obsGrad.addColorStop(0, '#8e44ad');
obsGrad.addColorStop(0.5, '#6c3483');
obsGrad.addColorStop(1, '#4a235a');
ctx.fillStyle = obsGrad;
ctx.fillRect(proj.x - sw/2, proj.y - sh/2, sw, sh);
// Edge highlight (sunlit side)
ctx.strokeStyle = 'rgba(255,180,120,0.4)';
ctx.lineWidth = 1;
ctx.strokeRect(proj.x - sw/2, proj.y - sh/2, sw, sh);
// Top face (pseudo-3D effect)
ctx.fillStyle = 'rgba(255,200,160,0.2)';
ctx.fillRect(proj.x - sw/2, proj.y - sh/2, sw, Math.max(2, sh * 0.08));
// Side face for depth
ctx.fillStyle = 'rgba(30,15,40,0.5)';
ctx.fillRect(proj.x + sw/2 - 3, proj.y - sh/2, 3, sh);
ctx.globalAlpha = 1;
}
}
}
// ── function: drawPickups ──
function drawPickups() {
for (const pk of pickups) {
if (pk.collected || pk.z < 5 || pk.z > CFG.DRAW_DISTANCE * CFG.SEGMENT_LENGTH) continue;
const proj = projectPoint(pk.z, pk.x, pk.y);
if (!proj) continue;
const size = 12 * proj.scale;
const bob = Math.sin(pk.bobPhase) * 4 * proj.scale;
const py = proj.y + bob;
ctx.save();
ctx.translate(proj.x, py);
let color, glowColor;
switch (pk.type) {
case 'energy': color = '#7bed9f'; glowColor = 'rgba(123,237,159,'; break;
case 'score': color = '#ffd43b'; glowColor = 'rgba(255,212,59,'; break;
case 'multiplier': color = '#ff6b81'; glowColor = 'rgba(255,107,129,'; break;
}
ctx.fillStyle = glowColor + '0.3)';
ctx.beginPath();
ctx.arc(0, 0, size * 1.8, 0, Math.PI * 2);
ctx.fill();
// Diamond shape
ctx.fillStyle = color;
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();
// Inner highlight
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.beginPath();
ctx.moveTo(0, -size * 0.5);
ctx.lineTo(size * 0.3, 0);
ctx.lineTo(0, size * 0.3);
ctx.lineTo(-size * 0.3, 0);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
// ── function: drawSpeedLines ──
function drawSpeedLines(speedRatio) {
if (speedRatio < 0.5) return;
const intensity = (speedRatio - 0.5) * 2;
const numLines = Math.floor(intensity * 30);
ctx.strokeStyle = `rgba(255,255,255,${intensity * 0.15})`;
ctx.lineWidth = 1;
for (let i = 0; i < numLines; i++) {
const x = Math.random() * W;
const y = H * getSunY() + Math.random() * (H - H * getSunY());
const len = randRange(20, 80) * intensity;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + (x - W/2) * 0.1, y + len);
ctx.stroke();
}
}
// ── function: drawChromaticAberration ──
function drawChromaticAberration(speedRatio) {
if (speedRatio < 0.7) return;
const intensity = (speedRatio - 0.7) / 0.3 * 0.05;
ctx.globalCompositeOperation = 'screen';
ctx.fillStyle = `rgba(255,0,0,${intensity})`;
ctx.fillRect(2, 0, W, H);
ctx.fillStyle = `rgba(0,0,255,${intensity})`;
ctx.fillRect(-2, 0, W, H);
ctx.globalCompositeOperation = 'source-over';
}
// ── let: gameState ──
// === GAME STATE & INPUT ===
let gameState = 'title';
// ── let: currentSpeed ──
let currentSpeed = CFG.BASE_SPEED;
// ── let: score ──
let score = 0;
// ── let: multiplier ──
let multiplier = 1;
// ── let: screenShakeX ──
let screenShakeX = 0;
// ── let: screenShakeY ──
let screenShakeY = 0;
// ── let: shakeIntensity ──
let shakeIntensity = 0;
// ── let: flashAlpha ──
let flashAlpha = 0;
// ── let: boostEnergy ──
let boostEnergy = 100;
// ── let: isBoosting ──
let isBoosting = false;
// ── let: nearMissTimer ──
let nearMissTimer = 0;
// ── let: lastTime ──
let lastTime = null;
// ── const: keys ──
const keys = {};
// ── function: resetGameState ──
function resetGameState() {
player = createPlayer();
currentSpeed = CFG.BASE_SPEED;
score = 0;
multiplier = 1;
screenShakeX = 0;
screenShakeY = 0;
shakeIntensity = 0;
flashAlpha = 0;
boostEnergy = 100;
isBoosting = false;
nearMissTimer = 0;
sunReserve = 0;
particles = [];
resetWorld();
}
// ── function: triggerShake ──
function triggerShake(intensity) {
shakeIntensity = Math.max(shakeIntensity, intensity);
}
// ── function: handleInput ──
function handleInput(dt) {
if (gameState !== 'playing') return;
const moveAmt = CFG.MOVE_SPEED * dt * 60;
if (keys['ArrowLeft'] || keys['KeyA']) player.targetX -= moveAmt;
if (keys['ArrowRight'] || keys['KeyD']) player.targetX += moveAmt;
if (keys['ArrowUp'] || keys['KeyW']) player.targetY -= moveAmt;
if (keys['ArrowDown'] || keys['KeyS']) player.targetY += moveAmt;
if (keys['Space'] && boostEnergy > 0 && !isBoosting) {
isBoosting = true;
playBoost();
}
}
// ── function: updateHUD ──
function updateHUD() {
document.getElementById('dist').textContent = `${Math.floor(distanceTraveled)}m`;
document.getElementById('score').textContent = Math.floor(score);
document.getElementById('multiplier').textContent = `×${multiplier.toFixed(1)}`;
document.getElementById('energy-bar').style.width = `${player.energy}%`;
const speedKmh = Math.floor(currentSpeed * 2.5);
document.getElementById('speed-display').textContent = `${speedKmh} km/h`;
// Energy bar color based on level
const eb = document.getElementById('energy-bar');
if (player.energy < 20) {
eb.style.background = '#ff4757';
} else if (player.energy < 40) {
eb.style.background = 'linear-gradient(90deg, #ff6b35, #ffa502)';
} else {
eb.style.background = 'linear-gradient(90deg, #ff6b35, #ffd43b, #7bed9f)';
}
// Sun indicator - changes color and size based on sun position
const si = document.getElementById('sun-indicator');
if (si) {
const sunY = getSunY();
const sunProgress = clamp((sunY - CFG.SUN_START_Y) / 0.8, 0, 1);
const size = lerp(16, 8, sunProgress);
si.style.width = `${size}px`;
si.style.height = `${size}px`;
if (sunProgress > 0.7) {
si.style.background = '#ff4757';
si.style.boxShadow = '0 0 12px rgba(255,71,87,.8)';
} else if (sunProgress > 0.4) {
si.style.background = '#ffa502';
si.style.boxShadow = '0 0 10px rgba(255,165,2,.6)';
} else {
si.style.background = '#ffd43b';
si.style.boxShadow = '0 0 8px rgba(255,212,59,.6)';
}
}
}
// ── function: gameOver ──
function gameOver() {
gameState = 'gameover';
playCrash();
triggerShake(15);
flashAlpha = 0.8;
const proj = projectPoint(15, player.x, player.y);
if (proj) emitCrashBurst(proj.x, proj.y);
const hs = parseInt(localStorage.getItem('solarRunnerHighScore') || '0');
const finalScore = Math.floor(score);
if (finalScore > hs) {
localStorage.setItem('solarRunnerHighScore', finalScore.toString());
}
document.getElementById('go-dist').textContent = `Distance: ${Math.floor(distanceTraveled)}m`;
document.getElementById('go-score').textContent = `Score: ${finalScore}`;
document.getElementById('go-highscore').textContent = `Best: ${Math.max(hs, finalScore)}`;
document.getElementById('gameover-screen').style.display = 'flex';
}
// ── function: updateGame ──
function updateGame(dt) {
if (gameState !== 'playing') return;
const speedMult = isBoosting ? CFG.BOOST_SPEED_MULT : 1;
currentSpeed += CFG.SPEED_RAMP * dt;
currentSpeed = Math.min(currentSpeed, CFG.MAX_SPEED);
const effectiveSpeed = currentSpeed * speedMult;
// Boost energy management
if (isBoosting) {
boostEnergy -= CFG.BOOST_DRAIN * dt * 60;
if (boostEnergy <= 0) {
boostEnergy = 0;
isBoosting = false;
}
} else {
boostEnergy = Math.min(100, boostEnergy + 5 * dt);
}
// Sun reserve slowly decays
sunReserve *= Math.pow(0.98, dt * 60);
// Update player
updatePlayer(player, dt);
// Update world
updateWorld(dt, effectiveSpeed);
// Energy drain
const shadowMult = isInShadow(player, worldSegments) ? CFG.SHADOW_DRAIN_MULT : 1;
player.energy -= CFG.ENERGY_DRAIN * shadowMult * dt;
if (player.energy <= 0) {
gameOver();
return;
}
// Collision detection
const collision = checkCollisions(player, worldSegments);
if (collision.hit && player.invincibleTimer <= 0) {
gameOver();
return;
}
if (collision.nearMiss) {
score += CFG.NEAR_MISS_BONUS * multiplier;
triggerShake(3);
nearMissTimer = 0.2;
playNearMiss();
}
// Pickup collection
const pickupType = checkPickupCollisions(player);
if (pickupType) {
const proj = projectPoint(15, player.x, player.y);
if (proj) emitPickupBurst(proj.x, proj.y, '#ffd43b');
switch(pickupType) {
case 'energy':
player.energy = Math.min(CFG.ENERGY_MAX, player.energy + CFG.PICKUP_ENERGY);
sunReserve += 1; // Each energy pickup also helps the sun
playPickup(660);
break;
case 'score':
score += 100 * multiplier;
playPickup(880);
break;
case 'multiplier':
multiplier = Math.min(multiplier + 0.5, 10);
playPickup(1100);
break;
}
}
// Distance score
score += effectiveSpeed * CFG.BASE_SCORE_PER_METER * dt * multiplier * 0.01;
// Screen shake decay
if (shakeIntensity > 0) {
screenShakeX = (Math.random() - 0.5) * shakeIntensity * 2;
screenShakeY = (Math.random() - 0.5) * shakeIntensity * 2;
shakeIntensity *= Math.pow(CFG.SHAKE_DECAY, dt * 60);
if (shakeIntensity < 0.1) {
shakeIntensity = 0;
screenShakeX = 0;
screenShakeY = 0;
}
}
// Flash decay
flashAlpha *= Math.pow(0.9, dt * 60);
// Near miss timer
if (nearMissTimer > 0) nearMissTimer -= dt;
// Update particles
updateParticles(dt);
// Thrust particles
emitThrust(W/2 + screenShakeX, H * 0.65 + screenShakeY, player.pitchLean);
// Audio drone
const speedRatio = (currentSpeed - CFG.BASE_SPEED) / (CFG.MAX_SPEED - CFG.BASE_SPEED);
updateDrone(speedRatio);
// Update HUD
updateHUD();
}
// ── function: render ──
function render() {
ctx.save();
ctx.translate(screenShakeX, screenShakeY);
drawSky();
drawStars(ctx);
drawSun();
drawGround();
drawObstacles();
drawPickups();
// Draw player craft at fixed screen position
const px = W / 2;
const py = H * 0.65;
// Boost glow effect around player
if (isBoosting) {
ctx.fillStyle = `rgba(100,200,255,${0.1 + Math.sin(Date.now() * 0.01) * 0.05})`;
ctx.beginPath();
ctx.arc(px, py, 40, 0, Math.PI * 2);
ctx.fill();
// Boost trail lines
for (let i = 0; i < 3; i++) {
const offset = randRange(-15, 15);
ctx.strokeStyle = `rgba(100,200,255,${randRange(0.1, 0.3)})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(px + offset, py + 20);
ctx.lineTo(px + offset * 1.5, py + randRange(40, 80));
ctx.stroke();
}
}
drawPlayer(ctx, px, py, player.bankAngle, player.pitchLean, flashAlpha);
// Particles on top
drawParticles(ctx);
// Speed effects
const speedRatio = (currentSpeed - CFG.BASE_SPEED) / (CFG.MAX_SPEED - CFG.BASE_SPEED);
drawSpeedLines(speedRatio);
drawChromaticAberration(speedRatio);
// Damage flash overlay
if (flashAlpha > 0.01) {
ctx.fillStyle = `rgba(255,80,60,${flashAlpha * 0.3})`;
ctx.fillRect(-10, -10, W + 20, H + 20);
}
// Near miss indicator
if (nearMissTimer > 0) {
ctx.fillStyle = `rgba(255,212,59,${nearMissTimer * 3})`;
ctx.font = 'bold 24px system-ui';
ctx.textAlign = 'center';
ctx.fillText('NEAR MISS!', W/2, H * 0.5);
}
// Boost indicator text
if (isBoosting) {
ctx.fillStyle = `rgba(100,200,255,${0.3 + Math.sin(Date.now() * 0.01) * 0.1})`;
ctx.font = 'bold 18px system-ui';
ctx.textAlign = 'center';
ctx.fillText('⚡ BOOST ⚡', W/2, H * 0.75);
}
// Sun warning when low
const sunY = getSunY();
if (sunY > 0.8) {
const warnAlpha = Math.sin(Date.now() * 0.005) * 0.15 + 0.15;
ctx.fillStyle = `rgba(255,50,30,${warnAlpha})`;
ctx.font = 'bold 16px system-ui';
ctx.textAlign = 'center';
ctx.fillText('☀ SUN SETTING — GRAB ENERGY!', W/2, H * 0.4);
}
// Vignette effect
const vigGrad = ctx.createRadialGradient(W/2, H/2, W*0.3, W/2, H/2, W*0.8);
vigGrad.addColorStop(0, 'rgba(0,0,0,0)');
vigGrad.addColorStop(1, 'rgba(0,0,0,0.4)');
ctx.fillStyle = vigGrad;
ctx.fillRect(-10, -10, W + 20, H + 20);
ctx.restore();
}
// ── function: gameLoop ──
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
let dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
dt = Math.min(dt, 0.05);
handleInput(dt);
updateGame(dt);
if (gameState === 'playing') {
render();
} else if (gameState === 'title' || gameState === 'gameover') {
drawSky();
drawStars(ctx);
drawSun();
drawGround();
}
requestAnimationFrame(gameLoop);
}
// ── function: startGame ──
function startGame() {
initAudio();
resetGameState();
gameState = 'playing';
document.getElementById('title-screen').style.display = 'none';
document.getElementById('gameover-screen').style.display = 'none';
document.getElementById('hud').style.display = 'flex';
}
// ── function: showHighScore ──
function showHighScore() {
const hs = localStorage.getItem('solarRunnerHighScore');
if (hs) {
document.getElementById('high-score-line').textContent = `Best: ${hs}`;
}
}
// ── let: sunReserve ──
let sunReserve = 0;
// ── let: stars ──
let stars = [];
// ── function: initStars ──
function initStars() {
stars = [];
for (let i = 0; i < 80; i++) {
stars.push({
x: Math.random(),
y: Math.random() * 0.6,
size: randRange(0.5, 2),
twinkleSpeed: randRange(1, 3),
twinklePhase: Math.random() * Math.PI * 2,
});
}
}
// ── function: drawStars ──
function drawStars(ctx) {
const t = Date.now() * 0.001;
for (const s of stars) {
const alpha = 0.3 + Math.sin(t * s.twinkleSpeed + s.twinklePhase) * 0.25;
ctx.fillStyle = `rgba(255,240,220,${alpha})`;
ctx.beginPath();
ctx.arc(s.x * W, s.y * H, s.size, 0, Math.PI * 2);
ctx.fill();
}
}
// ── main ──
// === MAIN ENTRY POINT ===
canvas = document.getElementById('game');
ctx = canvas.getContext('2d');
resizeCanvas();
initStars();
window.addEventListener('resize', function() {
resizeCanvas();
initStars();
});
// Input handling
document.addEventListener('keydown', function(e) {
keys[e.code] = true;
// Restart on R key during game over
if (e.code === 'KeyR' && gameState === 'gameover') {
startGame();
}
// Prevent scrolling with arrow keys/space
if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(e.code)) {
e.preventDefault();
}
});
document.addEventListener('keyup', function(e) {
keys[e.code] = false;
if (e.code === 'Space') isBoosting = false;
});
// Button handlers
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', startGame);
// Show high score on title screen
showHighScore();
// Start the game loop
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"78f6f33e-ac4a-49ca-8763-341d8241dbf9","tokensIn":2021081,"tokensOut":32637,"tokensTotal":2053718,"turns":67,"toolCalls":66,"failedToolCalls":8,"timestamp":"2026-07-18T15:38:14.138Z"} -->