Race the Sun
thinkingcap qwen3.6 27b · 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,479 bytes · SHA-256 ac9abe74693d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solar Runner</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
// === CONSTANTS ===
const CFG = {
// Player
PLAYER_WIDTH: 28,
PLAYER_HEIGHT: 16,
MOVE_SPEED: 0.14,
MOVE_EASING: 0.15,
INERTIA_TAIL: 0.92,
BANK_ANGLE: 0.35,
PITCH_LEAN: 0.15,
BOOST_SPEED_MULT: 1.6,
BOOST_DRAIN: 0.8,
BOOST_RECHARGE: 0.04,
// World
BASE_SPEED: 8,
MAX_SPEED: 32,
SPEED_RAMP: 0.003,
LANE_WIDTH: 120,
WORLD_HALF_W: 360,
WORLD_HALF_H: 140,
// Sun & Energy
SUN_START_Y: 0.35,
SUN_SET_RATE: 0.00008,
ENERGY_MAX: 100,
ENERGY_DRAIN: 0.025,
ENERGY_SHADOW_MULT: 3.5,
ENERGY_PICKUP: 25,
// Obstacles
SPAWN_INTERVAL_BASE: 60,
SPAWN_MIN: 18,
OBSTACLE_DEPTH: 40,
NEAR_MISS_DIST: 45,
// Scoring
DISTANCE_SCORE: 1,
NEAR_MISS_BONUS: 50,
TOKEN_VALUE: 100,
MULTIPLIER_MAX: 8,
// Effects
SHAKE_NEAR_MISS: 3,
SHAKE_CRASH: 12,
SHAKE_DECAY: 0.82,
SPEED_LINE_THRESHOLD: 16,
PARTICLE_COUNT: 24,
// Colors
SKY_TOP: [15, 10, 40],
SKY_MID: [80, 30, 60],
SKY_BOT: [200, 80, 40],
SUN_COLOR: [255, 220, 100],
GROUND_COLOR: [20, 15, 35],
OBSTACLE_COLORS: [[255,60,80],[255,140,40],[255,200,50],[100,200,255]],
};
// === GLOBALS ===
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
let state = 'title'; // title | playing | gameover
let seed = Date.now();
let rngState = seed;
// Player
let player = {};
let obstacles = [];
let pickups = [];
let particles = [];
let speedLines = [];
let scoreTokens = [];
// Game vars
let distance = 0;
let score = 0;
let multiplier = 1;
let energy = CFG.ENERGY_MAX;
let boostEnergy = 100;
let isBoosting = false;
let speed = CFG.BASE_SPEED;
let sunY = CFG.SUN_START_Y;
let frameCount = 0;
let shakeX = 0, shakeY = 0;
let flashAlpha = 0;
let nearMissTimer = 0;
let highScore = parseInt(localStorage.getItem('solarRunnerHS')) || 0;
// Input
let keys = {};
let inputLX = 0, inputLY = 0;
// Audio
let audioCtx = null;
let masterGain = null;
let droneOsc = null;
let droneGain = null;
// === SEEDED RNG ===
function rng() {
rngState = (rngState * 16807 + 0) % 2147483647;
return (rngState - 1) / 2147483646;
}
// === RESIZE ===
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// === AUDIO SYSTEM ===
function initAudio() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.3;
masterGain.connect(audioCtx.destination);
// Drone
droneOsc = audioCtx.createOscillator();
droneGain = audioCtx.createGain();
droneOsc.type = 'sawtooth';
droneOsc.frequency.value = 55;
droneGain.gain.value = 0.06;
const lfo = audioCtx.createOscillator();
const lfoGain = audioCtx.createGain();
lfo.frequency.value = 2;
lfoGain.gain.value = 8;
lfo.connect(lfoGain);
lfoGain.connect(droneOsc.frequency);
lfo.start();
droneOsc.connect(droneGain);
droneGain.connect(masterGain);
droneOsc.start();
}
function playTone(freq, dur, type, vol) {
if (!audioCtx) return;
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.type = type || 'square';
o.frequency.value = freq;
g.gain.setValueAtTime(vol || 0.15, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur);
o.connect(g);
g.connect(masterGain);
o.start();
o.stop(audioCtx.currentTime + dur);
}
function sfxPickup() { playTone(880, 0.12, 'square', 0.12); setTimeout(() => playTone(1320, 0.1, 'sine', 0.1), 60); }
function sfxEnergy() { playTone(440, 0.15, 'triangle', 0.15); setTimeout(() => playTone(660, 0.12, 'triangle', 0.12), 80); setTimeout(() => playTone(990, 0.1, 'sine', 0.1), 140); }
function sfxCrash() { playTone(80, 0.4, 'sawtooth', 0.25); playTone(60, 0.5, 'square', 0.15); }
function sfxNearMiss() { playTone(1200, 0.06, 'sine', 0.08); }
function sfxBoost() { playTone(200, 0.3, 'sawtooth', 0.08); }
// === PARTICLES ===
function spawnParticles(x, y, count, color, spread, life) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (rng() - 0.5) * spread,
vy: (rng() - 0.5) * spread,
life: life || (20 + rng() * 30),
maxLife: life || (20 + rng() * 30),
color,
size: 1 + rng() * 3,
});
}
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
}
function drawParticles() {
for (const p of particles) {
const a = p.life / p.maxLife;
ctx.globalAlpha = a;
ctx.fillStyle = `rgb(${p.color[0]},${p.color[1]},${p.color[2]})`;
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size * a + 1, p.size * a + 1);
}
ctx.globalAlpha = 1;
}
// === PROJECTION HELPERS ===
function project(wx, wy, wz) {
const camZ = 300;
const fov = 500;
const z = Math.max(wz, 1);
const scale = fov / (fov + z - camZ);
return {
x: W/2 + wx * scale,
y: H/2 + wy * scale,
scale,
};
}
// === PLAYER ===
function resetPlayer() {
player = {
x: 0, y: 0, z: 0,
vx: 0, vy: 0,
bankAngle: 0,
pitchLean: 0,
trail: [],
};
}
function updatePlayer() {
// Input with easing + inertia tail
inputLX = (keys['ArrowLeft'] || keys['KeyA']) ? -1 : (keys['ArrowRight'] || keys['KeyD']) ? 1 : 0;
inputLY = (keys['ArrowUp'] || keys['KeyW']) ? -1 : (keys['ArrowDown'] || keys['KeyS']) ? 1 : 0;
player.vx += inputLX * CFG.MOVE_SPEED;
player.vy += inputLY * CFG.MOVE_SPEED;
player.vx *= CFG.INERTIA_TAIL;
player.vy *= CFG.INERTIA_TAIL;
// Clamp velocity
const maxV = CFG.WORLD_HALF_W / (H/2) * 1.5;
player.vx = Math.max(-maxV, Math.min(maxV, player.vx));
player.vy = Math.max(-maxV, Math.min(maxV, player.vy));
player.x += player.vx * H;
player.y -= player.vy * H;
// Clamp position
player.x = Math.max(-CFG.WORLD_HALF_W, Math.min(CFG.WORLD_HALF_W, player.x));
player.y = Math.max(-CFG.WORLD_HALF_H, Math.min(CFG.WORLD_HALF_H, player.y));
// Bank angle from lateral velocity
const targetBank = -player.vx * CFG.BANK_ANGLE;
player.bankAngle += (targetBank - player.bankAngle) * 0.25;
// Pitch lean from vertical velocity
const targetPitch = player.vy * CFG.PITCH_LEAN;
player.pitchLean += (targetPitch - player.pitchLean) * 0.25;
// Trail
if (frameCount % 2 === 0) {
player.trail.push({ x: player.x, y: player.y, life: 15 });
if (player.trail.length > 30) player.trail.shift();
}
for (const t of player.trail) t.life--;
player.trail = player.trail.filter(t => t.life > 0);
// Thrust particles
if (frameCount % 2 === 0) {
const px = W/2 + player.x * 0.8;
const py = H/2 - player.y * 0.8 + 15;
spawnParticles(px, py, 2, isBoosting ? [255,200,50] : [100,180,255], 3, 12);
}
}
function drawPlayer() {
const px = W/2 + player.x * 0.8;
const py = H/2 - player.y * 0.8;
ctx.save();
ctx.translate(px, py);
ctx.rotate(player.bankAngle);
// Glow
const glowGrad = ctx.createRadialGradient(0, 0, 5, 0, 0, 40);
glowGrad.addColorStop(0, 'rgba(100,200,255,0.3)');
glowGrad.addColorStop(1, 'rgba(100,200,255,0)');
ctx.fillStyle = glowGrad;
ctx.fillRect(-40, -40, 80, 80);
// Craft body (low-poly triangle shape)
const w = CFG.PLAYER_WIDTH;
const h = CFG.PLAYER_HEIGHT;
// Main body
ctx.fillStyle = '#1a3a5c';
ctx.beginPath();
ctx.moveTo(0, -h);
ctx.lineTo(-w/2, h * 0.6);
ctx.lineTo(-w/4, h);
ctx.lineTo(w/4, h);
ctx.lineTo(w/2, h * 0.6);
ctx.closePath();
ctx.fill();
// Highlight
ctx.fillStyle = '#3a7abd';
ctx.beginPath();
ctx.moveTo(0, -h);
ctx.lineTo(-w/4, h * 0.3);
ctx.lineTo(0, h * 0.5);
ctx.closePath();
ctx.fill();
// Cockpit
ctx.fillStyle = '#88ccff';
ctx.beginPath();
ctx.moveTo(0, -h + 2);
ctx.lineTo(-4, 2);
ctx.lineTo(4, 2);
ctx.closePath();
ctx.fill();
// Wing accents
ctx.strokeStyle = '#5599cc';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-w/2 + 2, h * 0.6);
ctx.lineTo(0, -h + 4);
ctx.lineTo(w/2 - 2, h * 0.6);
ctx.stroke();
// Engine glow
const engineGlow = isBoosting ? 1 : 0.5 + Math.sin(frameCount * 0.3) * 0.2;
ctx.fillStyle = `rgba(100,200,255,${engineGlow})`;
ctx.beginPath();
ctx.moveTo(-w/4, h);
ctx.lineTo(0, h + (isBoosting ? 18 : 8));
ctx.lineTo(w/4, h);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// === OBSTACLES ===
function spawnObstacle() {
const types = ['pillar', 'wall', 'spike'];
const type = types[Math.floor(rng() * types.length)];
const laneX = (Math.floor(rng() * 5) - 2) * CFG.LANE_WIDTH;
let w, h;
if (type === 'pillar') {
w = 30 + rng() * 40;
h = 60 + rng() * 100;
} else if (type === 'wall') {
w = 80 + rng() * 120;
h = 30 + rng() * 50;
} else {
w = 40 + rng() * 60;
h = 50 + rng() * 70;
}
const colorIdx = Math.floor(rng() * CFG.OBSTACLE_COLORS.length);
obstacles.push({
x: laneX,
y: (rng() - 0.5) * CFG.WORLD_HALF_H * 0.6,
z: 1200,
w, h, type,
color: CFG.OBSTACLE_COLORS[colorIdx],
passed: false,
});
// Sometimes spawn pickups near obstacles
if (rng() < 0.4) {
const pTypes = ['energy', 'token', 'multiplier'];
const pType = pTypes[Math.floor(rng() * pTypes.length)];
pickups.push({
x: laneX + (rng() - 0.5) * 80,
y: player.y + (rng() - 0.5) * 100,
z: 1200,
type: pType,
bobPhase: rng() * Math.PI * 2,
});
}
}
function updateObstacles() {
const spd = speed * (isBoosting ? CFG.BOOST_SPEED_MULT : 1);
for (let i = obstacles.length - 1; i >= 0; i--) {
const o = obstacles[i];
o.z -= spd;
// Collision check
if (o.z < 50 && o.z > -20) {
const dx = Math.abs(player.x - o.x);
const dy = Math.abs(player.y - o.y);
const halfW = o.w / 2 + CFG.PLAYER_WIDTH / 2;
const halfH = o.h / 2 + CFG.PLAYER_HEIGHT / 2;
if (dx < halfW && dy < halfH) {
// Crash!
gameOver();
return;
}
// Near miss check
if (!o.passed && dx < CFG.NEAR_MISS_DIST + halfW && dy < halfH * 1.5) {
o.passed = true;
score += CFG.NEAR_MISS_BONUS * multiplier;
nearMissTimer = 10;
shakeX = (rng() - 0.5) * CFG.SHAKE_NEAR_MISS;
shakeY = (rng() - 0.5) * CFG.SHAKE_NEAR_MISS;
sfxNearMiss();
spawnParticles(W/2 + player.x * 0.8, H/2 - player.y * 0.8, 8, [255,255,100], 4, 15);
}
}
if (o.z < -60) obstacles.splice(i, 1);
}
// Pickups
for (let i = pickups.length - 1; i >= 0; i--) {
const p = pickups[i];
p.z -= spd;
if (p.z < 50 && p.z > -20) {
const dx = Math.abs(player.x - p.x);
const dy = Math.abs(player.y - p.y);
if (dx < 40 && dy < 40) {
// Collect!
if (p.type === 'energy') {
energy = Math.min(CFG.ENERGY_MAX, energy + CFG.ENERGY_PICKUP);
sfxEnergy();
} else if (p.type === 'token') {
score += CFG.TOKEN_VALUE * multiplier;
sfxPickup();
} else if (p.type === 'multiplier') {
multiplier = Math.min(CFG.MULTIPLIER_MAX, multiplier + 1);
sfxEnergy();
}
spawnParticles(W/2 + player.x * 0.8, H/2 - player.y * 0.8, 15, [255,255,200], 6, 20);
pickups.splice(i, 1);
continue;
}
}
if (p.z < -60) pickups.splice(i, 1);
}
}
function drawObstacles() {
// Sort by z (far to near)
const all = [...obstacles].sort((a, b) => b.z - a.z);
for (const o of all) {
if (o.z < 0 || o.z > 1200) continue;
const p = project(o.x, o.y, o.z);
const s = p.scale;
const w = o.w * s;
const h = o.h * s;
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(p.x - w/2 + 4*s, p.y - h/2 + 4*s, w, h);
// Main body
const c = o.color;
const brightness = Math.max(0.3, 1 - o.z / 1500);
ctx.fillStyle = `rgb(${Math.floor(c[0]*brightness)},${Math.floor(c[1]*brightness)},${Math.floor(c[2]*brightness)})`;
if (o.type === 'pillar') {
// Tall pillar with top face
ctx.fillRect(p.x - w/2, p.y - h/2, w, h);
ctx.fillStyle = `rgb(${Math.min(255,c[0]+40)},${Math.min(255,c[1]+40)},${Math.min(255,c[2]+40)})`;
ctx.fillRect(p.x - w/2 + 2*s, p.y - h/2, w - 4*s, 3*s);
} else if (o.type === 'wall') {
ctx.fillRect(p.x - w/2, p.y - h/2, w, h);
// Edge highlight
ctx.strokeStyle = `rgba(255,255,255,0.15)`;
ctx.lineWidth = 1;
ctx.strokeRect(p.x - w/2, p.y - h/2, w, h);
} else {
// Spike (triangle)
ctx.beginPath();
ctx.moveTo(p.x, p.y - h/2);
ctx.lineTo(p.x - w/2, p.y + h/2);
ctx.lineTo(p.x + w/2, p.y + h/2);
ctx.closePath();
ctx.fill();
}
// Glow edge at close range
if (o.z < 300) {
ctx.strokeStyle = `rgba(${c[0]},${c[1]},${c[2]},${(1 - o.z/300)*0.5})`;
ctx.lineWidth = 2;
ctx.strokeRect(p.x - w/2, p.y - h/2, w, h);
}
}
// Draw pickups
for (const pk of pickups) {
if (pk.z < 0 || pk.z > 1200) continue;
const p = project(pk.x, pk.y + Math.sin(frameCount * 0.08 + pk.bobPhase) * 15, pk.z);
const s = p.scale;
const size = 12 * s;
let color;
if (pk.type === 'energy') color = [100, 255, 150];
else if (pk.type === 'token') color = [255, 220, 80];
else color = [255, 100, 255];
// Glow
const glowGrad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, size * 2);
glowGrad.addColorStop(0, `rgba(${color[0]},${color[1]},${color[2]},0.4)`);
glowGrad.addColorStop(1, `rgba(${color[0]},${color[1]},${color[2]},0)`);
ctx.fillStyle = glowGrad;
ctx.fillRect(p.x - size*2, p.y - size*2, size*4, size*4);
// Diamond shape
ctx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
ctx.beginPath();
ctx.moveTo(p.x, p.y - size);
ctx.lineTo(p.x + size * 0.7, p.y);
ctx.lineTo(p.x, p.y + size);
ctx.lineTo(p.x - size * 0.7, p.y);
ctx.closePath();
ctx.fill();
// Inner highlight
ctx.fillStyle = `rgba(255,255,255,0.5)`;
ctx.beginPath();
ctx.moveTo(p.x, p.y - size * 0.5);
ctx.lineTo(p.x + size * 0.3, p.y);
ctx.lineTo(p.x, p.y + size * 0.5);
ctx.lineTo(p.x - size * 0.3, p.y);
ctx.closePath();
ctx.fill();
}
}
// === SUN & SKY ===
function drawSky() {
const sunScreenY = H * (1 - sunY);
const sunX = W * 0.7;
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
const t = Math.max(0, sunY);
const r1 = CFG.SKY_TOP[0] + (30 - CFG.SKY_TOP[0]) * t;
const g1 = CFG.SKY_TOP[1] + (20 - CFG.SKY_TOP[1]) * t;
const b1 = CFG.SKY_TOP[2] + (50 - CFG.SKY_TOP[2]) * t;
skyGrad.addColorStop(0, `rgb(${r1},${g1},${b1})`);
skyGrad.addColorStop(0.5, `rgb(${CFG.SKY_MID[0]},${CFG.SKY_MID[1]},${CFG.SKY_MID[2]})`);
skyGrad.addColorStop(1, `rgb(${CFG.SKY_BOT[0]},${CFG.SKY_BOT[1]},${CFG.SKY_BOT[2]})`);
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Sun glow
const sunGlowSize = Math.max(30, 150 - sunY * 200);
const sunGlow = ctx.createRadialGradient(sunX, sunScreenY, 0, sunX, sunScreenY, sunGlowSize);
sunGlow.addColorStop(0, `rgba(${CFG.SUN_COLOR[0]},${CFG.SUN_COLOR[1]},${CFG.SUN_COLOR[2]},0.8)`);
sunGlow.addColorStop(0.3, `rgba(${CFG.SUN_COLOR[0]},${CFG.SUN_COLOR[1]},${CFG.SUN_COLOR[2]},0.3)`);
sunGlow.addColorStop(1, 'rgba(255,200,100,0)');
ctx.fillStyle = sunGlow;
ctx.fillRect(sunX - sunGlowSize, sunScreenY - sunGlowSize, sunGlowSize*2, sunGlowSize*2);
// Sun disc
const sunRadius = Math.max(8, 40 - sunY * 60);
ctx.fillStyle = `rgb(${CFG.SUN_COLOR[0]},${CFG.SUN_COLOR[1]},${CFG.SUN_COLOR[2]})`;
ctx.beginPath();
ctx.arc(sunX, sunScreenY, sunRadius, 0, Math.PI * 2);
ctx.fill();
// Horizon line
const horizonY = H * (1 - sunY + 0.15);
ctx.strokeStyle = `rgba(${CFG.SUN_COLOR[0]},${CFG.SUN_COLOR[1]},${CFG.SUN_COLOR[2]},0.3)`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, horizonY);
ctx.lineTo(W, horizonY);
ctx.stroke();
// Ground plane (perspective grid)
const groundTop = H * 0.55;
const groundGrad = ctx.createLinearGradient(0, groundTop, 0, H);
groundGrad.addColorStop(0, `rgba(${CFG.GROUND_COLOR[0]},${CFG.GROUND_COLOR[1]},${CFG.GROUND_COLOR[2]},0.8)`);
groundGrad.addColorStop(1, `rgb(${CFG.GROUND_COLOR[0]},${CFG.GROUND_COLOR[1]},${CFG.GROUND_COLOR[2]})`);
ctx.fillStyle = groundGrad;
ctx.fillRect(0, groundTop, W, H - groundTop);
// Grid lines on ground
const gridAlpha = Math.max(0.05, 0.15 - sunY * 0.2);
ctx.strokeStyle = `rgba(100,150,200,${gridAlpha})`;
ctx.lineWidth = 0.5;
// Horizontal lines (perspective)
for (let i = 0; i < 15; i++) {
const t = i / 15;
const y = groundTop + (H - groundTop) * Math.pow(t, 1.5);
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
// Vertical lines converging to vanishing point
const vanishX = W * 0.7;
for (let i = -10; i <= 10; i++) {
const xSpread = i * W / 8;
ctx.beginPath();
ctx.moveTo(vanishX, groundTop);
ctx.lineTo(xSpread + vanishX * 2, H);
ctx.stroke();
}
}
// === SPEED LINES ===
function updateSpeedLines() {
if (speed > CFG.SPEED_LINE_THRESHOLD) {
const intensity = Math.min(1, (speed - CFG.SPEED_LINE_THRESHOLD) / (CFG.MAX_SPEED - CFG.SPEED_LINE_THRESHOLD));
for (let i = 0; i < Math.floor(intensity * 4); i++) {
speedLines.push({
x: rng() * W,
y: H * 0.3 + rng() * H * 0.5,
len: 20 + rng() * 60,
life: 15 + rng() * 15,
maxLife: 15 + rng() * 15,
});
}
}
for (let i = speedLines.length - 1; i >= 0; i--) {
speedLines[i].life--;
if (speedLines[i].life <= 0) speedLines.splice(i, 1);
}
}
function drawSpeedLines() {
for (const sl of speedLines) {
const a = sl.life / sl.maxLife;
ctx.strokeStyle = `rgba(255,255,255,${a * 0.4})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(sl.x, sl.y);
ctx.lineTo(sl.x - player.vx * 30, sl.y + sl.len);
ctx.stroke();
}
}
// === CHROMATIC ABERRATION (simulated) ===
function drawChromaticAberration() {
if (speed < CFG.SPEED_LINE_THRESHOLD) return;
const intensity = Math.min(4, (speed - CFG.SPEED_LINE_THRESHOLD) / (CFG.MAX_SPEED - CFG.SPEED_LINE_THRESHOLD) * 4);
// Draw offset colored edges for the player craft area
const px = W/2 + player.x * 0.8;
const py = H/2 - player.y * 0.8;
ctx.globalCompositeOperation = 'screen';
ctx.fillStyle = `rgba(255,0,0,${intensity * 0.03})`;
ctx.fillRect(px - intensity - CFG.PLAYER_WIDTH, py - CFG.PLAYER_HEIGHT, CFG.PLAYER_WIDTH*2, CFG.PLAYER_HEIGHT*2);
ctx.fillStyle = `rgba(0,0,255,${intensity * 0.03})`;
ctx.fillRect(px + intensity - CFG.PLAYER_WIDTH, py - CFG.PLAYER_HEIGHT, CFG.PLAYER_WIDTH*2, CFG.PLAYER_HEIGHT*2);
ctx.globalCompositeOperation = 'source-over';
}
// === HUD ===
function drawHUD() {
const pad = 20;
const barW = 180;
const barH = 14;
// Distance
ctx.fillStyle = '#fff';
ctx.font = 'bold 16px monospace';
ctx.textAlign = 'left';
ctx.fillText(`DIST ${Math.floor(distance)}m`, pad, pad + 20);
// Score
ctx.font = 'bold 24px monospace';
ctx.fillStyle = '#ffd700';
ctx.fillText(`${score}`, pad, pad + 50);
// Multiplier
if (multiplier > 1) {
ctx.fillStyle = `hsl(${(frameCount * 3) % 360},80%,70%)`;
ctx.font = 'bold 18px monospace';
ctx.fillText(`x${multiplier}`, pad + 80, pad + 50);
}
// Speed
const spdDisplay = Math.floor(speed * (isBoosting ? CFG.BOOST_SPEED_MULT : 1) * 10);
ctx.fillStyle = '#aaa';
ctx.font = '14px monospace';
ctx.fillText(`${spdDisplay} km/h`, pad, pad + 72);
// Energy bar
const ey = pad + 85;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(pad, ey, barW, barH);
const energyPct = energy / CFG.ENERGY_MAX;
const eColor = energyPct > 0.3 ? [80, 200, 120] : [255, 80, 60];
ctx.fillStyle = `rgb(${eColor[0]},${eColor[1]},${eColor[2]})`;
ctx.fillRect(pad, ey, barW * energyPct, barH);
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
ctx.strokeRect(pad, ey, barW, barH);
ctx.fillStyle = '#fff';
ctx.font = '10px monospace';
ctx.fillText('ENERGY', pad + 4, ey + 11);
// Boost bar
const by = ey + barH + 6;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(pad, by, barW * 0.7, barH - 2);
ctx.fillStyle = `rgb(${100 + boostEnergy*1.55},${150 + boostEnergy*0.5},${255})`;
ctx.fillRect(pad, by, barW * 0.7 * (boostEnergy / 100), barH - 2);
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.strokeRect(pad, by, barW * 0.7, barH - 2);
ctx.fillStyle = '#fff';
ctx.font = '10px monospace';
ctx.fillText('BOOST [SPACE]', pad + 4, by + 9);
// Sun warning
if (sunY > 0.6) {
const warnAlpha = Math.sin(frameCount * 0.15) * 0.3 + 0.3;
ctx.fillStyle = `rgba(255,50,50,${warnAlpha})`;
ctx.font = 'bold 14px monospace';
ctx.textAlign = 'center';
ctx.fillText('⚠ SUN SETTING ⚠', W/2, pad + 30);
ctx.textAlign = 'left';
}
// Near miss flash text
if (nearMissTimer > 0) {
const a = nearMissTimer / 10;
ctx.fillStyle = `rgba(255,255,100,${a})`;
ctx.font = 'bold 20px monospace';
ctx.textAlign = 'center';
ctx.fillText('NEAR MISS!', W/2, H * 0.3);
ctx.textAlign = 'left';
}
// High score
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '12px monospace';
ctx.textAlign = 'right';
ctx.fillText(`HI ${highScore}`, W - pad, pad + 16);
ctx.textAlign = 'left';
}
// === SCREEN SHAKE ===
function updateShake() {
shakeX *= CFG.SHAKE_DECAY;
shakeY *= CFG.SHAKE_DECAY;
if (Math.abs(shakeX) < 0.1) shakeX = 0;
if (Math.abs(shakeY) < 0.1) shakeY = 0;
}
// === GAME STATE ===
function startGame() {
state = 'playing';
distance = 0;
score = 0;
multiplier = 1;
energy = CFG.ENERGY_MAX;
boostEnergy = 100;
speed = CFG.BASE_SPEED;
sunY = CFG.SUN_START_Y;
frameCount = 0;
obstacles = [];
pickups = [];
particles = [];
speedLines = [];
shakeX = 0; shakeY = 0;
flashAlpha = 0;
nearMissTimer = 0;
rngState = seed;
resetPlayer();
initAudio();
}
function gameOver() {
state = 'gameover';
sfxCrash();
spawnParticles(W/2, H/2, 50, [255,100,50], 8, 40);
spawnParticles(W/2, H/2, 30, [255,200,50], 6, 30);
flashAlpha = 1;
if (score > highScore) {
highScore = score;
localStorage.setItem('solarRunnerHS', highScore);
}
if (droneOsc) {
droneGain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
}
}
// === TITLE SCREEN ===
function drawTitle() {
// Animated background
const t = frameCount * 0.005;
ctx.fillStyle = `rgb(${15+Math.sin(t)*10},${10+Math.cos(t*0.7)*8},${40+Math.sin(t*1.3)*15})`;
ctx.fillRect(0, 0, W, H);
// Stars
for (let i = 0; i < 60; i++) {
const sx = ((i * 137.5 + frameCount * 0.1) % W);
const sy = ((i * 97.3 + Math.sin(i + t) * 20) % H);
const sa = 0.3 + Math.sin(frameCount * 0.05 + i) * 0.2;
ctx.fillStyle = `rgba(255,255,255,${sa})`;
ctx.fillRect(sx, sy, 1.5, 1.5);
}
// Sun on horizon
const sunYTitle = H * 0.6;
const sunGlow = ctx.createRadialGradient(W*0.7, sunYTitle, 0, W*0.7, sunYTitle, 120);
sunGlow.addColorStop(0, 'rgba(255,220,100,0.6)');
sunGlow.addColorStop(0.5, 'rgba(255,150,50,0.15)');
sunGlow.addColorStop(1, 'rgba(255,100,30,0)');
ctx.fillStyle = sunGlow;
ctx.fillRect(W*0.7-120, sunYTitle-120, 240, 240);
// Ground
ctx.fillStyle = '#0a0815';
ctx.fillRect(0, H * 0.65, W, H * 0.35);
// Title text
ctx.textAlign = 'center';
ctx.fillStyle = '#fff';
ctx.font = `bold ${Math.min(72, W*0.08)}px monospace`;
ctx.fillText('SOLAR RUNNER', W/2, H * 0.3);
// Subtitle
ctx.font = `${Math.min(18, W*0.025)}px monospace`;
ctx.fillStyle = 'rgba(255,200,150,0.7)';
ctx.fillText('Stay in the light. Dodge everything.', W/2, H * 0.38);
// Controls
ctx.font = `${Math.min(14, W*0.02)}px monospace`;
ctx.fillStyle = 'rgba(255,255,255,0.5)';
const cy = H * 0.5;
ctx.fillText('WASD / Arrow Keys — Move', W/2, cy);
ctx.fillText('SPACE — Boost', W/2, cy + 22);
// Start prompt
const blinkAlpha = Math.sin(frameCount * 0.08) * 0.4 + 0.6;
ctx.fillStyle = `rgba(255,255,255,${blinkAlpha})`;
ctx.font = `bold ${Math.min(22, W*0.03)}px monospace`;
ctx.fillText('[ PRESS ANY KEY TO START ]', W/2, H * 0.7);
// High score
if (highScore > 0) {
ctx.fillStyle = 'rgba(255,215,0,0.6)';
ctx.font = `${Math.min(16, W*0.02)}px monospace`;
ctx.fillText(`HIGH SCORE: ${highScore}`, W/2, H * 0.78);
}
ctx.textAlign = 'left';
}
// === GAME OVER SCREEN ===
function drawGameOver() {
// Darken
ctx.fillStyle = `rgba(10,5,20,${Math.min(0.7, flashAlpha * 2)})`;
ctx.fillRect(0, 0, W, H);
if (flashAlpha > 0) {
ctx.fillStyle = `rgba(255,100,50,${flashAlpha * 0.3})`;
ctx.fillRect(0, 0, W, H);
flashAlpha *= 0.95;
}
ctx.textAlign = 'center';
// Game Over text
ctx.fillStyle = '#ff4444';
ctx.font = `bold ${Math.min(60, W*0.07)}px monospace`;
ctx.fillText('GAME OVER', W/2, H * 0.3);
// Stats
ctx.fillStyle = '#fff';
ctx.font = `${Math.min(20, W*0.025)}px monospace`;
ctx.fillText(`Distance: ${Math.floor(distance)}m`, W/2, H * 0.42);
ctx.fillText(`Score: ${score}`, W/2, H * 0.48);
if (score >= highScore && score > 0) {
ctx.fillStyle = '#ffd700';
ctx.font = `bold ${Math.min(22, W*0.03)}px monospace`;
ctx.fillText('★ NEW HIGH SCORE ★', W/2, H * 0.56);
} else {
ctx.fillStyle = 'rgba(255,215,0,0.6)';
ctx.font = `${Math.min(16, W*0.02)}px monospace`;
ctx.fillText(`High Score: ${highScore}`, W/2, H * 0.56);
}
// Restart prompt
const blinkAlpha = Math.sin(frameCount * 0.08) * 0.4 + 0.6;
ctx.fillStyle = `rgba(255,255,255,${blinkAlpha})`;
ctx.font = `${Math.min(18, W*0.025)}px monospace`;
ctx.fillText('[ PRESS R OR CLICK TO RESTART ]', W/2, H * 0.7);
ctx.textAlign = 'left';
}
// === MAIN UPDATE ===
function update() {
frameCount++;
if (state !== 'playing') return;
const spdMult = isBoosting ? CFG.BOOST_SPEED_MULT : 1;
const effectiveSpeed = speed * spdMult;
// Speed ramp
speed = Math.min(CFG.MAX_SPEED, speed + CFG.SPEED_RAMP);
// Distance & score
distance += effectiveSpeed * 0.1;
score += Math.floor(effectiveSpeed * 0.5) * multiplier;
// Sun sinks
sunY += CFG.SUN_SET_RATE * (isBoosting ? 0.7 : 1);
if (sunY >= 1) {
gameOver();
return;
}
// Energy drain
const shadowFactor = Math.max(0, (sunY - 0.3) / 0.5);
energy -= CFG.ENERGY_DRAIN * (1 + shadowFactor * CFG.ENERGY_SHADOW_MULT);
if (isBoosting) {
boostEnergy -= CFG.BOOST_DRAIN;
if (boostEnergy <= 0) {
boostEnergy = 0;
isBoosting = false;
}
} else {
boostEnergy = Math.min(100, boostEnergy + CFG.BOOST_RECHARGE);
}
if (energy <= 0) {
energy = 0;
gameOver();
return;
}
// Spawn obstacles
const spawnInterval = Math.max(CFG.SPAWN_MIN, CFG.SPAWN_INTERVAL_BASE - speed * 1.2);
if (frameCount % Math.floor(spawnInterval) === 0) {
spawnObstacle();
}
updatePlayer();
updateObstacles();
updateParticles();
updateSpeedLines();
updateShake();
// Drone pitch follows speed
if (droneOsc) {
droneOsc.frequency.value = 50 + speed * 3;
droneGain.gain.value = 0.04 + speed * 0.002;
}
nearMissTimer = Math.max(0, nearMissTimer - 1);
}
// === MAIN DRAW ===
function draw() {
ctx.clearRect(0, 0, W, H);
if (state === 'title') {
drawTitle();
return;
}
// Apply screen shake
ctx.save();
ctx.translate(shakeX, shakeY);
drawSky();
drawObstacles();
drawSpeedLines();
drawPlayer();
drawParticles();
drawChromaticAberration();
ctx.restore();
drawHUD();
if (state === 'gameover') {
drawGameOver();
}
}
// === GAME LOOP ===
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
// === INPUT ===
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (state === 'title') {
seed = Date.now();
startGame();
return;
}
if (state === 'gameover' && (e.code === 'KeyR')) {
seed = Date.now();
startGame();
return;
}
// Boost
if (state === 'playing' && e.code === 'Space') {
e.preventDefault();
if (!isBoosting && boostEnergy > 10) {
isBoosting = true;
sfxBoost();
}
}
});
window.addEventListener('keyup', e => {
keys[e.code] = false;
if (e.code === 'Space') isBoosting = false;
});
canvas.addEventListener('click', () => {
if (state === 'title') {
seed = Date.now();
startGame();
} else if (state === 'gameover') {
seed = Date.now();
startGame();
}
});
// Prevent scrolling
window.addEventListener('keydown', e => {
if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) {
e.preventDefault();
}
});
// === START ===
loop();
</script>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"351c9526-58cd-45ce-9fbc-4f7112b47204","tokensIn":944,"tokensOut":11906,"tokensTotal":12850,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T02:36:11.617Z"} -->