Canyon Run
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 27,328 bytes · SHA-256 b1193c4b3c35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Golden Canyon Racer</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; -webkit-user-select: none; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #050510; font-family: 'Rajdhani', sans-serif; color: #fff; }
#canvas-wrap { position: fixed; inset: 0; z-index: 1; }
canvas { display: block; width: 100%; height: 100%; }
/* HUD */
#hud {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 10;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 2rem;
text-shadow: 0 0 10px rgba(255,180,60,0.4);
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: flex-start;
font-family: 'Orbitron', sans-serif;
letter-spacing: 0.08em;
}
.score-display { font-size: 2.2rem; color: #ffcc66; text-shadow: 0 0 15px rgba(255,180,60,0.6); }
.multiplier-display { font-size: 3rem; font-weight: 900; color: #ffaa44; transition: transform 0.15s ease; }
.multiplier-display.active { animation: pulse 0.4s ease; color: #ff5500; text-shadow: 0 0 25px rgba(255,85,0,0.8); }
@keyframes pulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.15)} }
.energy-ring-wrap {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 180px; height: 180px;
pointer-events: none;
}
.energy-ring-svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.energy-ring-track { stroke: rgba(255,255,255,0.1); stroke-width: 4; fill: none; }
.energy-ring-fill { stroke: #ffcc66; stroke-width: 6; fill: none; stroke-linecap: round; filter: drop-shadow(0 0 6px rgba(255,200,100,0.6)); transition: stroke-dashoffset 0.2s linear; }
.center-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; pointer-events: none; }
.center-label .label { font-size: 0.7rem; letter-spacing: 0.2em; color: rgba(255,255,255,0.6); text-transform: uppercase; }
.center-label .value { font-family: 'Orbitron', sans-serif; font-size: 1.6rem; color: #fff; text-shadow: 0 0 10px rgba(255,255,255,0.3); }
.bottom-bar {
display: flex;
justify-content: space-between;
align-items: flex-end;
font-size: 0.9rem;
color: rgba(255,255,255,0.7);
}
.speed-bar-wrap { width: 220px; height: 6px; background: rgba(255,255,255,0.08); border-radius: 3px; overflow: hidden; position: relative; }
.speed-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #ffaa44, #ff5500); border-radius: 3px; box-shadow: 0 0 8px rgba(255,150,0,0.5); transition: width 0.1s linear; }
.turbo-bar-wrap { width: 160px; height: 4px; background: rgba(255,255,255,0.08); border-radius: 2px; overflow: hidden; }
.turbo-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #44ffaa, #00ffaa); border-radius: 2px; box-shadow: 0 0 8px rgba(0,255,170,0.4); }
/* Overlays */
#start-screen, #game-over-screen {
position: fixed; inset: 0; z-index: 20;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: rgba(5,5,16,0.85); backdrop-filter: blur(12px);
text-align: center;
padding: 2rem;
}
#start-screen { display: flex; }
#game-over-screen { display: none; }
h1 { font-family: 'Orbitron', sans-serif; font-size: clamp(2.5rem, 6vw, 5rem); letter-spacing: 0.15em; color: #ffcc66; text-shadow: 0 0 30px rgba(255,200,100,0.5), 0 0 60px rgba(255,150,60,0.2); margin-bottom: 0.5rem; }
h1 span { color: #aa66ff; }
.subtitle { font-size: 1.1rem; color: #aaaacc; letter-spacing: 0.12em; margin-bottom: 3rem; }
.btn {
font-family: 'Orbitron', sans-serif; font-size: 1rem; letter-spacing: 0.12em;
padding: 1rem 3rem; border: 2px solid rgba(255,200,100,0.6); color: #ffcc66;
background: rgba(255,200,100,0.05); backdrop-filter: blur(4px);
border-radius: 4px; cursor: pointer; transition: all 0.2s ease; text-transform: uppercase;
box-shadow: 0 0 15px rgba(255,200,100,0.1);
}
.btn:hover { background: rgba(255,200,100,0.15); border-color: rgba(255,200,100,1); box-shadow: 0 0 25px rgba(255,200,100,0.3); color: #fff; }
.game-over-stats { margin-bottom: 3rem; }
.stat-row { display: flex; justify-content: center; gap: 4rem; margin-bottom: 0.5rem; }
.stat-label { font-size: 0.8rem; color: rgba(255,255,255,0.5); letter-spacing: 0.15em; text-transform: uppercase; }
.stat-value { font-family: 'Orbitron', sans-serif; font-size: 2rem; color: #fff; }
</style>
<script src="https://unpkg.com/three@0.160.0/build/three.js"></script>
</head>
<body>
<div id="canvas-wrap">
<canvas id="gl"></canvas>
</div>
<div id="hud">
<div class="top-bar">
<div>
<div style="font-size:0.7rem; color:rgba(255,255,255,0.5); letter-spacing:0.15em;">SCORE</div>
<div class="score-display" id="scoreUI">0</div>
</div>
<div style="text-align:center;">
<div style="font-size:0.7rem; color:rgba(255,255,255,0.5); letter-spacing:0.15em;">MULTIPLIER</div>
<div class="multiplier-display" id="multUI">1.0x</div>
</div>
</div>
<div class="energy-ring-wrap" aria-label="Energy">
<svg class="energy-ring-svg" viewBox="0 0 160 160">
<circle class="energy-ring-track" cx="80" cy="80" r="70"/>
<circle class="energy-ring-fill" id="energyRing" cx="80" cy="80" r="70" stroke-dasharray="440" stroke-dashoffset="440"/>
</svg>
<div class="center-label">
<div class="label">Energy</div>
<div class="value" id="energyUI">100%</div>
</div>
</div>
<div class="bottom-bar">
<div>
<div style="font-size:0.7rem; color:rgba(255,255,255,0.5); letter-spacing:0.1em;">SPEED</div>
<div class="speed-bar-wrap"><div class="speed-bar-fill" id="speedUI"></div></div>
</div>
<div style="text-align:right;">
<div style="font-size:0.7rem; color:rgba(255,255,255,0.5); letter-spacing:0.1em;">TURBO</div>
<div class="turbo-bar-wrap"><div class="turbo-bar-fill" id="turboUI"></div></div>
</div>
</div>
</div>
<div id="start-screen">
<h1>GOLDEN <span>CANY</span></h1>
<div class="subtitle">PROCEDURAL HIGH-SPEED RACER</div>
<p style="max-width:420px; color:#aaa; line-height:1.6; margin-bottom:2.5rem; font-size:0.95rem;">
Fly through the canyon. Collect energy. Avoid walls. The closer you fly, the higher your score. Turbo builds with speed — use it to break the sound barrier.
</p>
<button class="btn" id="startBtn">INITIATE FLIGHT</button>
<div style="margin-top:2rem; color:#666; font-size:0.75rem;">WASD / Arrows — Pitch & Roll | SHIFT — Turbo</div>
</div>
<div id="game-over-screen">
<h1 style="font-size:3.5rem;">CRASHED</h1>
<div class="subtitle">FLIGHT RECORD</div>
<div class="game-over-stats">
<div class="stat-row">
<div><div class="stat-label">FINAL SCORE</div><div class="stat-value" id="finalScore">0</div></div>
<div><div class="stat-label">MAX SPEED</div><div class="stat-value" id="finalSpeed">0</div></div>
</div>
<div class="stat-row">
<div><div class="stat-label">DISTANCE</div><div class="stat-value" id="finalDist">0</div></div>
<div><div class="stat-label">MULTIPLIER</div><div class="stat-value" id="finalMult">1.0x</div></div>
</div>
</div>
<button class="btn" id="restartBtn">RE-ENGAGE</button>
</div>
<script type="module">
import * as THREE from 'three';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
/* ===== Constants ===== */
const CHUNK_LENGTH = 160;
const LANE_WIDTH = 24;
const WALL_HEIGHT = 50;
const WALL_THICK = 3;
/* ===== Setup ===== */
const canvas = document.getElementById('gl');
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x050510);
scene.fog = new THREE.FogExp2(0x1a0a2e, 0.003);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 300);
/* ===== Lighting ===== */
const hemi = new THREE.HemisphereLight(0xffaa66, 0x330022, 0.8);
scene.add(hemi);
const dir = new THREE.DirectionalLight(0xffcc88, 1.0);
dir.position.set(20, 60, -30);
dir.castShadow = true;
dir.shadow.mapSize.width = 1024;
dir.shadow.mapSize.height = 1024;
dir.shadow.bias = -0.001;
scene.add(dir);
const dir2 = new THREE.DirectionalLight(0xaa66ff, 0.5);
dir2.position.set(-20, 20, 40);
scene.add(dir2);
/* ===== Post-Processing Targets ===== */
const width = window.innerWidth;
const height = window.innerHeight;
const bloomTarget = new THREE.WebGLRenderTarget(width, height, { samples: 4 });
const prevTarget = new THREE.WebGLRenderTarget(width, height, { samples: 4 });
const bloomComposer = new EffectComposer(renderer, bloomTarget);
bloomComposer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(new THREE.Vector2(width, height), 1.6, 0.3, 0.9);
bloomComposer.addPass(bloomPass);
/* ===== Motion Blur ===== */
const motionMat = new THREE.ShaderMaterial({
uniforms: {
tDiffuse: { value: null },
prevTexture: { value: null },
blendFactor: { value: 0.15 },
time: { value: 0 }
},
vertexShader: `
varying vec2 vUv;
void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform sampler2D prevTexture;
uniform float blendFactor;
uniform float time;
varying vec2 vUv;
void main() {
vec4 curr = texture2D(tDiffuse, vUv);
vec4 prev = texture2D(prevTexture, vUv);
// Add slight chromatic separation on high speed
vec4 currR = texture2D(tDiffuse, vUv + vec2(0.001, 0));
vec2 shift = (blendFactor > 0.3) ? vec2(0.002, -0.002) : vec2(0,0);
curr = mix(curr, texture2D(tDiffuse, vUv + shift), 0.15);
gl_FragColor = mix(curr, prev, blendFactor);
}
`
});
const motionPlane = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), motionMat);
const motionScene = new THREE.Scene();
motionScene.add(motionPlane);
const motionCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10);
/* ===== Copy pass for previous frame ===== */
const copyMat = new THREE.MeshBasicMaterial({ map: bloomTarget.texture });
const copyPlane = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), copyMat);
const copyScene = new THREE.Scene();
copyScene.add(copyPlane);
const copyCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10);
/* ===== Craft ===== */
const craft = new THREE.Group();
const craftBody = new THREE.Mesh(
new THREE.BoxGeometry(1.8, 0.5, 5),
new THREE.MeshStandardMaterial({ color: 0xeeddff, roughness: 0.15, metalness: 0.7 })
);
craft.add(craftBody);
const craftWingL = new THREE.Mesh(
new THREE.BoxGeometry(0.2, 0.15, 2.5),
new THREE.MeshStandardMaterial({ color: 0x9999aa, roughness: 0.3, metalness: 0.5 })
);
craftWingL.position.set(-1.0, 0, 0);
craft.add(craftWingL);
const craftWingR = craftWingL.clone();
craftWingR.position.set(1.0, 0, 0);
craft.add(craftWingR);
// Engine glow
const glowGeo = new THREE.SphereGeometry(0.35, 16, 16);
const glowMat = new THREE.MeshStandardMaterial({ color: 0xffffaa, emissive: 0xff6600, emissiveIntensity: 4, roughness: 0.1 });
const engineGlow = new THREE.Mesh(glowGeo, glowMat);
engineGlow.position.set(0, 0, 2.6);
craft.add(engineGlow);
// Tip glow
const tipGlow = new THREE.Mesh(new THREE.SphereGeometry(0.2, 8, 8), new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0x88ccff, emissiveIntensity: 3 }));
tipGlow.position.set(0, 0, -2.5);
craft.add(tipGlow);
scene.add(craft);
/* ===== Particles / Trail ===== */
const trailPoints = [];
const trailGeo = new THREE.BufferGeometry();
const trailMat = new THREE.LineBasicMaterial({ color: 0xffaa66, transparent: true, opacity: 0.4, linewidth: 2 });
const trailLine = new THREE.Line(trailGeo, trailMat);
scene.add(trailLine);
const dustGeo = new THREE.BufferGeometry();
const dustMat = new THREE.PointsMaterial({ color: 0xffcc88, size: 0.15, transparent: true, opacity: 0.6, sizeAttenuation: true });
const dustPoints = new THREE.Points(dustGeo, dustMat);
scene.add(dustPoints);
/* ===== Chunk System ===== */
const chunks = new Map(); // index -> group
function makeWallMesh(xPos, yPos, zPos, w, h, d, colorHex) {
const geo = new THREE.BoxGeometry(w, h, d);
const mat = new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.9, metalness: 0.05, flatShading: true });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(xPos, yPos, zPos);
return mesh;
}
function generateChunk(index) {
const group = new THREE.Group();
group.position.z = index * CHUNK_LENGTH;
// Curved walls using multiple segments
const segments = 6;
for (let i = 0; i < segments; i++) {
const zLocal = (i / (segments - 1)) * CHUNK_LENGTH - CHUNK_LENGTH / 2;
const noise = Math.sin(index * 0.8 + i * 0.6) * 8 - 4 + Math.sin(index * 2.3 + i) * 3;
const squeeze = (index > 10 && Math.random() > 0.7) ? 8 : 0;
const leftX = -LANE_WIDTH / 2 - 6 + noise - squeeze / 2;
const rightX = LANE_WIDTH / 2 + 6 - noise + squeeze / 2;
// Left wall
group.add(makeWallMesh(leftX - WALL_THICK / 2, WALL_HEIGHT / 2, zLocal, WALL_THICK, WALL_HEIGHT, CHUNK_LENGTH / segments + 1, 0xff6a22));
// Right wall
group.add(makeWallMesh(rightX + WALL_THICK / 2, WALL_HEIGHT / 2, zLocal, WALL_THICK, WALL_HEIGHT, CHUNK_LENGTH / segments + 1, 0x9933cc));
}
// Obstacles: floating monoliths and tight pillars
if (index > 0) {
const obstacleCount = Math.min(4, 1 + Math.floor(index / 10));
for (let i = 0; i < obstacleCount; i++) {
const zLocal = (Math.random() - 0.5) * CHUNK_LENGTH * 0.8;
const xLocal = (Math.random() - 0.5) * 14;
const yLocal = 8 + Math.random() * 15;
const size = 2 + Math.random() * 3;
const isMonolith = Math.random() > 0.3;
const h = isMonolith ? 12 + Math.random() * 14 : 20 + Math.random() * 10;
const color = isMonolith ? 0xddccff : 0x222233;
const obs = makeWallMesh(xLocal, yLocal, zLocal, size, h, size, color);
obs.userData = { type: 'obstacle', radius: size * 0.8 };
group.add(obs);
}
}
// Collectibles
for (let i = 0; i < 3; i++) {
const xLocal = (Math.random() - 0.5) * 16;
const zLocal = (Math.random() - 0.5) * CHUNK_LENGTH * 0.7;
const yLocal = 6 + Math.random() * 12;
const orbGeo = new THREE.IcosahedronGeometry(1.2, 0);
const orbMat = new THREE.MeshStandardMaterial({ color: 0xffffaa, emissive: 0xffaa00, emissiveIntensity: 3, roughness: 0.2, metalness: 0.8 });
const orb = new THREE.Mesh(orbGeo, orbMat);
orb.position.set(xLocal, yLocal, zLocal);
orb.rotation.y = Math.random() * Math.PI * 2;
orb.userData = { type: 'collectible', value: 15 };
group.add(orb);
}
return group;
}
function getChunkIndex(z) {
return Math.floor(z / CHUNK_LENGTH);
}
function regenerateWorld() {
const idx = getChunkIndex(craft.position.z);
for (let i = idx - 2; i <= idx + 10; i++) {
if (!chunks.has(i)) {
const chunk = generateChunk(i);
scene.add(chunk);
chunks.set(i, chunk);
}
}
for (const [k, v] of chunks) {
if (k < idx - 3 || k > idx + 12) {
scene.remove(v);
chunks.delete(k);
// Dispose geometry/materials to avoid memory leak
v.traverse(child => {
if (child.geometry) child.geometry.dispose();
if (child.material && child.material.dispose) child.material.dispose();
});
}
}
}
/* ===== Sound ===== */
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playTone(freq, duration, vol = 0.1, type = 'sine', detune = 0) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
osc.detune.value = detune;
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
return osc;
}
function playEngineSound(freq, vol) {
// Create low hum using a continuous oscillator handled separately
// We'll just return nothing and manage a continuous engine oscillator
return null;
}
let engineOsc = null;
let engineGain = null;
function startEngineSound() {
if (engineOsc) return;
engineOsc = audioCtx.createOscillator();
engineGain = audioCtx.createGain();
engineOsc.type = 'triangle';
engineOsc.frequency.value = 55;
engineGain.gain.value = 0.04;
engineOsc.connect(engineGain);
engineGain.connect(audioCtx.destination);
engineOsc.start();
}
function stopEngineSound() {
if (engineOsc) {
engineOsc.stop();
engineOsc = null;
engineGain = null;
}
}
function updateEngineSound(speed) {
if (!engineOsc) return;
engineOsc.frequency.value = 55 + speed * 6;
engineGain.gain.value = 0.04 + speed * 0.03;
}
/* ===== Game State ===== */
let running = false;
let gameOver = false;
let score = 0;
let multiplier = 1.0;
let energy = 100;
let speed = 0; // current velocity
let turbo = 0; // turbo charge 0-1
let distance = 0;
let maxSpeed = 0;
let nearMissTimer = 0;
// Physics state
const craftState = {
roll: 0,
pitch: 0,
bankTarget: 0,
pitchTarget: 0
};
/* ===== Input ===== */
const keys = { left: false, right: false, up: false, down: false, shift: false };
window.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true;
if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true;
if (e.key === 'ArrowUp' || e.key === 'w') keys.up = true;
if (e.key === 'ArrowDown' || e.key === 's') keys.down = true;
if (e.key === 'Shift') keys.shift = true;
if (e.key === ' ' || e.key === 'Enter') {
if (!running && !gameOver) startGame();
else if (gameOver) startGame();
}
});
window.addEventListener('keyup', e => {
if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false;
if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false;
if (e.key === 'ArrowUp' || e.key === 'w') keys.up = false;
if (e.key === 'ArrowDown' || e.key === 's') keys.down = false;
if (e.key === 'Shift') keys.shift = false;
});
/* ===== HUD Elements ===== */
const scoreUI = document.getElementById('scoreUI');
const multUI = document.getElementById('multUI');
const energyUI = document.getElementById('energyUI');
const energyRing = document.getElementById('energyRing');
const speedUI = document.getElementById('speedUI');
const turboUI = document.getElementById('turboUI');
/* ===== Game Functions ===== */
function startGame() {
if (audioCtx.state === 'suspended') audioCtx.resume();
startEngineSound();
running = true;
gameOver = false;
score = 0;
multiplier = 1.0;
energy = 100;
speed = 0;
turbo = 0;
distance = 0;
maxSpeed = 0;
craft.position.set(0, 15, 20);
craft.rotation.set(0, 0, 0);
craftState.roll = 0;
craftState.pitch = 0;
craftState.bankTarget = 0;
craftState.pitchTarget = 0;
chunks.clear();
for (const child of scene.children.slice()) {
if (child !== craft && child !== trailLine && child !== dustPoints && child !== motionPlane) {
scene.remove(child);
}
}
scene.add(craft);
scene.add(trailLine);
scene.add(dustPoints);
regenerateWorld();
document.getElementById('start-screen').style.display = 'none';
document.getElementById('game-over-screen').style.display = 'none';
}
function endGame() {
running = false;
gameOver = true;
stopEngineSound();
document.getElementById('finalScore').textContent = Math.floor(score);
document.getElementById('finalSpeed').textContent = Math.floor(maxSpeed);
document.getElementById('finalDist').textContent = Math.floor(distance);
document.getElementById('finalMult').textContent = multiplier.toFixed(1) + 'x';
document.getElementById('game-over-screen').style.display = 'flex';
}
/* ===== Update ===== */
function updatePhysics(dt) {
if (!running || gameOver) return;
// Energy decay
energy -= 0.8 * dt;
if (energy < 0) { endGame(); return; }
// Base acceleration
let baseAccel = 0.35;
if (keys.up) baseAccel += 0.4;
if (keys.down) baseAccel -= 0.2;
// Pitch target
craftState.pitchTarget = keys.up ? 0.15 : keys.down ? -0.15 : 0;
craftState.pitch += (craftState.pitchTarget - craftState.pitch) * 0.08;
// Roll / bank
craftState.bankTarget = keys.left ? -0.5 : keys.right ? 0.5 : 0;
craftState.roll += (craftState.bankTarget - craftState.roll) * 0.1;
// Turbo
if (keys.shift && turbo > 0.2) {
baseAccel *= 2.0;
turbo -= 0.025 * dt * 60;
speed *= 1.05; // extra boost
} else {
if (speed > 20) turbo = Math.min(1, turbo + 0.005 * dt * 60);
else turbo = Math.max(0, turbo - 0.002 * dt * 60);
}
speed += baseAccel * dt * 60;
speed = Math.max(5, Math.min(120, speed));
if (speed > maxSpeed) maxSpeed = speed;
// Move craft
craft.rotation.x = craftState.pitch;
craft.rotation.z = craftState.roll;
// Forward motion based on pitch slightly
const forwardMult = 1.0 + Math.abs(craftState.pitch) * 0.3;
craft.position.z += speed * dt * 1.2 * forwardMult;
distance += speed * dt * 1.2 * forwardMult;
// Vertical movement
const verticalTarget = (keys.up ? 1 : keys.down ? -1 : 0) * 2;
craft.position.y += (verticalTarget - craft.position.y + 15) * 0.02; // spring back to middle
craft.position.y = Math.max(5, Math.min(35, craft.position.y));
craft.position.x += craftState.roll * 0.3 * dt * 60;
craft.position.x = Math.max(-15, Math.min(15, craft.position.x));
// Score and multiplier
const baseScoreRate = 20;
score += baseScoreRate * multiplier * dt * 60;
multiplier = 1.0 + Math.min(4, speed / 30) * 0.5;
// Near miss
const nearThreshold = 4.0;
let closestDist = Infinity;
for (const [k, chunk] of chunks) {
chunk.children.forEach(child => {
if (!child.userData || !child.userData.type) return;
const dist = craft.position.distanceTo(child.position);
if (dist < closestDist) closestDist = dist;
});
}
if (closestDist < nearThreshold && closestDist > 1.5) {
multiplier += 0.05;
nearMissTimer = 1.0;
playTone(1200, 0.08, 0.05, 'sine', 200);
} else {
nearMissTimer *= 0.9;
}
// Regenerate chunks
regenerateWorld();
}
function updateCollisions() {
if (!running || gameOver) return;
const craftRadius = 2.5;
for (const [k, chunk] of chunks) {
chunk.children.forEach(child => {
if (child === craft) return;
if (!child.userData || child.userData.type !== 'obstacle') return;
// Bounding sphere approximation
const dist = craft.position.distanceTo(child.position);
const childR = child.userData.radius || 2;
if (dist < craftRadius + childR) {
// Crash
playTone(150, 0.4, 0.1, 'square', -300);
playTone(80, 0.6, 0.08, 'sine');
endGame();
}
if (child.userData.type === 'collectible') {
if (dist < 3) {
energy = Math.min(100, energy + child.userData.value);
playTone(800 + Math.random() * 400, 0.15, 0.05, 'sine');
chunk.remove(child);
child.geometry.dispose();
child.material.dispose();
}
}
});
}
}
function updateVisuals() {
// Trail
trailPoints.push(new THREE.Vector3().copy(craft.position));
if (trailPoints.length > 40) trailPoints.shift();
const positions = new Float32Array(trailPoints.length * 3);
for (let i = 0; i < trailPoints.length; i++) {
positions[i * 3 + 0] = trailPoints[i].x;
positions[i * 3 + 1] = trailPoints[i].y;
positions[i * 3 + 2] = trailPoints[i].z - 1; // slightly behind
}
trailGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
// Dust particles
if (Math.random() > 0.7) {
const pts = [];
for (let i = 0; i < 3; i++) {
pts.push(
craft.position.x + (Math.random() - 0.5) * 1,
craft.position.y + (Math.random() - 0.5) * 0.5,
craft.position.z - (Math.random() * 5 + 2)
);
}
const arr = new Float32Array(pts);
// Rebuild dust geo each frame is okay with small count
dustGeo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
}
// Engine glow intensity based on speed
if (running) {
engineGlow.scale.set(1 + speed * 0.01, 1 + speed * 0.01, 1);
}
// HUD
scoreUI.textContent = Math.floor(score);
multUI.textContent = multiplier.toFixed(1) + 'x';
multUI.classList.toggle('active', nearMissTimer > 0.3);
energyUI.textContent = Math.floor(energy) + '%';
energyRing.setAttribute('stroke-dashoffset', 440 * (1 - energy / 100));
speedUI.style.width = Math.min(100, (speed / 100) * 100) + '%';
turboUI.style.width = Math.min(100, turbo * 100) + '%';
// Motion blur blend factor based on speed
motionMat.uniforms.blendFactor.value = 0.15 + Math.min(0.45, speed / 120);
motionMat.uniforms.time.value = performance.now() / 1000;
}
/* ===== Render Loop ===== */
const clock = new THREE.Clock();
function renderLoop() {
requestAnimationFrame(renderLoop);
const dt = Math.min(0.05, clock.getDelta());
if (running) {
updatePhysics(dt);
updateCollisions();
updateEngineSound(speed);
} else {
updateEngineSound(0);
}
updateVisuals();
// Camera follow
const targetX = craft.position.x;
const targetY = craft.position.y + 4;
const targetZ = craft.position.z + 15;
camera.position.lerp(new THREE.Vector3(targetX, targetY, targetZ), 0.05);
camera.rotation.set(-0.2 + craftState.pitch * 3, 0, -craftState.roll * 0.1);
camera.lookAt(craft.position.x, craft.position.y + 2, craft.position.z + 10);
// Render bloom
bloomComposer.render();
// Render motion blur to screen
motionMat.uniforms.tDiffuse.value = bloomTarget.texture;
motionMat.uniforms.prevTexture.value = prevTarget.texture;
renderer.setRenderTarget(null);
renderer.render(motionScene, motionCam);
// Save current bloom output to previous target for next frame
renderer.setRenderTarget(prevTarget);
renderer.render(copyScene, copyCam);
renderer.setRenderTarget(null);
}
/* ===== Resize ===== */
window.addEventListener('resize', () => {
const w = window.innerWidth;
const h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
bloomTarget.setSize(w, h);
prevTarget.setSize(w, h);
bloomComposer.setSize(w, h);
});
/* ===== Init ===== */
document.getElementById('startBtn').addEventListener('click', () => {
startGame();
});
document.getElementById('restartBtn').addEventListener('click', () => {
startGame();
});
// Pre-warm a little
regenerateWorld();
// Start loop
renderLoop();
</script>
</body>
</html>