Canyon Run
inkling · basic
◈Preview unavailable — try playing the result
Browser check: Preview unavailable or timed out. 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 24,383 bytes · SHA-256 8d431c512783
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>High-Speed Procedural Canyon Racer</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;900&family=Rajdhani:wght@300;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0214; font-family: 'Rajdhani', sans-serif; color: #fff; }
#canvas-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#multiplier { position: absolute; top: 40px; left: 50%; transform: translateX(-50%); font-family: 'Orbitron', sans-serif; font-size: 90px; font-weight: 900; color: #ffaa55; text-shadow: 0 0 30px #ff6600, 0 0 80px #ff4400; letter-spacing: -6px; line-height: 1; opacity: 0.95; }
#score-display { position: absolute; top: 50px; right: 40px; font-size: 28px; font-weight: 300; color: #eeddcc; letter-spacing: 4px; text-shadow: 0 2px 10px rgba(0,0,0,0.6); }
#speed-display { position: absolute; bottom: 30px; left: 40px; text-align: left; }
#speed-display .label { font-size: 14px; text-transform: uppercase; letter-spacing: 3px; color: #aa7755; margin-bottom: 4px; }
#speed-display .val { font-family: 'Orbitron', sans-serif; font-size: 36px; color: #ffaa88; text-shadow: 0 0 15px rgba(255,150,80,0.4); }
#energy-bar-container { position: absolute; bottom: 30px; right: 40px; width: 300px; height: 8px; background: rgba(255,255,255,0.08); border-radius: 4px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1); }
#energy-fill { height: 100%; width: 100%; background: linear-gradient(90deg, #ffaa55, #ff4400); border-radius: 4px; transition: width 0.15s ease-out; box-shadow: 0 0 10px rgba(255,150,80,0.6); }
#boost-meter { position: absolute; bottom: 50px; right: 40px; width: 300px; height: 4px; background: rgba(255,255,255,0.05); border-radius: 2px; overflow: hidden; }
#boost-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #44ffaa, #00ffaa); border-radius: 2px; box-shadow: 0 0 8px rgba(0,255,100,0.5); transition: width 0.15s ease-out; }
#gameover { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10,3,30,0.88); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 20; backdrop-filter: blur(8px); }
#gameover h1 { font-family: 'Orbitron', sans-serif; font-size: 100px; letter-spacing: -10px; color: #ff3322; text-shadow: 0 0 60px #ff1100; margin-bottom: 10px; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.05); opacity: 0.9; } }
#gameover .stats { font-size: 26px; color: #eeddcc; margin: 6px 0; letter-spacing: 2px; }
#gameover .stats span { color: #ffaa55; font-family: 'Orbitron', sans-serif; font-weight: 900; }
#gameover button { margin-top: 30px; padding: 18px 50px; font-family: 'Orbitron', sans-serif; font-size: 22px; background: linear-gradient(135deg, #ffaa55, #ff6600); border: none; color: #0a0214; border-radius: 30px; cursor: pointer; box-shadow: 0 0 30px rgba(255,120,30,0.6); pointer-events: auto; transition: transform 0.1s; }
#gameover button:hover { transform: translateY(-3px); box-shadow: 0 0 50px rgba(255,140,40,0.8); }
#hint { position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); font-size: 12px; color: rgba(255,255,255,0.3); letter-spacing: 2px; pointer-events: none; }
</style>
</head>
<body>
<div id="canvas-container"></div>
<div id="hud">
<div id="multiplier">x1.0</div>
<div id="score-display">0</div>
<div id="speed-display">
<div class="label">VELOCITY</div>
<div class="val" id="speed-val">60 m/s</div>
</div>
<div id="energy-bar-container"><div id="energy-fill"></div></div>
<div id="boost-meter"><div id="boost-fill"></div></div>
</div>
<div id="gameover">
<h1>CRASHED</h1>
<div class="stats">FINAL SCORE: <span id="final-score">0</span></div>
<div class="stats">MAX VELOCITY: <span id="final-speed">0</span> m/s</div>
<div class="stats">NEAR-MISS MULTIPLIER: <span id="final-mult">1.0</span>x</div>
<button onclick="restartGame()">RESTART FLIGHT</button>
</div>
<div id="hint">MOUSE TO BANK / PITCH | SHIFT FOR BOOST | COLLECT ORBS</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
// --- STATE ---
let scene, camera, renderer, composer, renderPass, bloomPass, motionPass;
let clock, playerMesh, sunLight, ambientLight;
let chunks = [], particles = [];
let audioCtx, engineOsc, engineGain, nearMissGain;
let running = true;
let score = 0, multiplier = 1, energy = 100, turbo = 0, maxTurbo = 100;
let speed = 55, maxBaseSpeed = 130, speedTarget = 55;
let mouseX = window.innerWidth / 2, mouseY = window.innerHeight / 2;
let boostActive = false, shakeIntensity = 0;
let chunkLength = 50;
let pathOffset = 0;
const container = document.getElementById('canvas-container');
// --- AUDIO SETUP ---
function initAudio() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
engineOsc = audioCtx.createOscillator();
engineOsc.type = 'triangle';
engineGain = audioCtx.createGain();
engineOsc.connect(engineGain);
engineGain.connect(audioCtx.destination);
engineGain.gain.setValueAtTime(0.08, audioCtx.currentTime);
engineOsc.frequency.setValueAtTime(180, audioCtx.currentTime);
engineOsc.start();
nearMissGain = audioCtx.createGain();
nearMissGain.gain.setValueAtTime(0, audioCtx.currentTime);
} catch(e) { console.log('Audio init failed'); }
}
function playSound(type) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
if (type === 'boost') {
osc.type = 'sine';
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(900, audioCtx.currentTime + 0.4);
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.02, audioCtx.currentTime + 0.4);
osc.start(); osc.stop(audioCtx.currentTime + 0.4);
} else if (type === 'near') {
osc.type = 'sine';
osc.frequency.setValueAtTime(700, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1200, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);
osc.start(); osc.stop(audioCtx.currentTime + 0.15);
} else if (type === 'collect') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(1000, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(300, audioCtx.currentTime + 0.2);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
osc.start(); osc.stop(audioCtx.currentTime + 0.2);
}
}
function updateEngineSound() {
if (!audioCtx || !engineOsc) return;
const baseFreq = 180 + speed * 6 + (boostActive ? 300 : 0);
engineOsc.frequency.linearRampToValueAtTime(baseFreq, audioCtx.currentTime + 0.05);
const vol = 0.06 + (speed / 200) * 0.08 + (boostActive ? 0.08 : 0);
engineGain.gain.linearRampToValueAtTime(Math.min(vol, 0.3), audioCtx.currentTime + 0.05);
}
// --- WORLD FUNCTIONS ---
function pathX(z) {
return Math.sin(z * 0.018) * 14 + Math.sin(z * 0.06) * 8 + Math.sin(z * 0.003) * 18;
}
function generateChunk(z) {
const group = new THREE.Group();
const segmentCount = Math.ceil(chunkLength / 8);
const currentDifficulty = 1 + score / 800;
const currentWidth = Math.max(8, 36 - score * 0.15);
for (let i = 0; i < segmentCount; i++) {
const localZ = i * 8;
const worldZ = z + localZ;
const cx = pathX(worldZ);
const leftX = cx - currentWidth / 2;
const rightX = cx + currentWidth / 2;
// Wall columns with varied heights / angles for geometric aesthetic
const h1 = 7 + Math.random() * 10;
const h2 = 7 + Math.random() * 10;
const wallMat = new THREE.MeshStandardMaterial({
color: 0xcc5522 + Math.floor(Math.random() * 0x221111),
metalness: 0.1, roughness: 0.9, flatShading: true
});
const colGeo = new THREE.BoxGeometry(2.5, h1, 1.5);
const leftCol = new THREE.Mesh(colGeo, wallMat);
leftCol.position.set(leftX, h1 / 2, worldZ);
leftCol.rotation.y = Math.sin(worldZ * 0.03) * 0.3 - 0.2;
group.add(leftCol);
const rightCol = new THREE.Mesh(colGeo, wallMat);
rightCol.position.set(rightX, h2 / 2, worldZ);
rightCol.rotation.y = -Math.sin(worldZ * 0.03 + 1) * 0.3 + 0.15;
group.add(rightCol);
// Floating Monolith obstacles
if (Math.random() < 0.18 * currentDifficulty && worldZ > 30) {
const size = 2 + Math.random() * 3;
const obsGeo = new THREE.BoxGeometry(size, 7 + Math.random() * 5, size);
const obsMat = new THREE.MeshStandardMaterial({
color: 0x5533aa, emissive: 0x221144, metalness: 0.3, roughness: 0.7, flatShading: true
});
const obs = new THREE.Mesh(obsGeo, obsMat);
const ox = cx + (Math.random() - 0.5) * (currentWidth * 0.55);
obs.position.set(ox, 4, worldZ);
group.add(obs);
group.userData.obstacles = group.userData.obstacles || [];
group.userData.obstacles.push({ mesh: obs, x: ox, y: 4, z: worldZ, r: size * 0.7 });
}
// Glowing energy orbs
if (Math.random() < 0.35 && worldZ > 20) {
const orbGeo = new THREE.IcosahedronGeometry(0.45, 0);
const orbMat = new THREE.MeshStandardMaterial({
color: 0xffffaa, emissive: 0xff6600, emissiveIntensity: 3, metalness: 0.2, roughness: 0.2
});
const orb = new THREE.Mesh(orbGeo, orbMat);
const ox = cx + (Math.random() - 0.5) * (currentWidth * 0.75);
const oy = 3 + Math.random() * 5;
orb.position.set(ox, oy, worldZ);
group.add(orb);
const pointLight = new THREE.PointLight(0xff6600, 3, 12);
pointLight.position.set(ox, oy, worldZ);
group.add(pointLight);
group.userData.orbs = group.userData.orbs || [];
group.userData.orbs.push({ mesh: orb, light: pointLight, x: ox, y: oy, z: worldZ, r: 0.7 });
}
}
// Ground strip for depth reference
const floorGeo = new THREE.PlaneGeometry(currentWidth + 4, chunkLength + 4);
const floorMat = new THREE.MeshStandardMaterial({ color: 0x150622, metalness: 0.1, roughness: 1, side: THREE.DoubleSide });
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.position.set(pathX(z + chunkLength / 2), -8, z + chunkLength / 2);
group.add(floor);
scene.add(group);
chunks.push({ z: z, group: group });
}
// --- INIT ---
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x140822);
scene.fog = new THREE.FogExp2(0x2a0a3a, 0.018);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 600);
camera.position.set(0, 6, -12);
renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
// Lighting: Golden Hour / Cyber-Sunset palette
sunLight = new THREE.DirectionalLight(0xffaa55, 3);
sunLight.position.set(40, 80, -60);
sunLight.castShadow = true;
scene.add(sunLight);
const hemiLight = new THREE.HemisphereLight(0xff7755, 0x331144, 1.2);
scene.add(hemiLight);
ambientLight = new THREE.AmbientLight(0x442255, 0.6);
scene.add(ambientLight);
const accentLight = new THREE.PointLight(0xaa44ff, 10, 200);
accentLight.position.set(0, 30, 0);
scene.add(accentLight);
// Player craft (low-poly sleek wedge)
const shipGeo = new THREE.ConeGeometry(0.7, 3, 5);
const shipMat = new THREE.MeshStandardMaterial({
color: 0xdddddd, emissive: 0x441100, emissiveIntensity: 1.5, metalness: 0.9, roughness: 0.2, flatShading: true
});
playerMesh = new THREE.Mesh(shipGeo, shipMat);
playerMesh.rotation.x = -Math.PI / 2;
playerMesh.rotation.y = Math.PI; // face forward (positive Z)
scene.add(playerMesh);
// Post-processing
composer = new EffectComposer(renderer);
renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
motionPass = new ShaderPass({
uniforms: { tDiffuse: { value: null }, blurStrength: { value: 0 } },
vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float blurStrength;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
vec2 dir = vUv - 0.5;
float samples = 10.0;
for(float i = 0.0; i < samples; i++) {
float f = (i + 0.5) / samples;
float dist = blurStrength * 0.05 * f;
color += texture2D(tDiffuse, vUv + dir * dist);
}
gl_FragColor = color / (samples + 1.0);
}
`
});
composer.addPass(motionPass);
bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.6, 0.35, 0.85);
composer.addPass(bloomPass);
// Generate initial world
for (let i = 0; i < 8; i++) {
generateChunk(i * chunkLength);
}
clock = new THREE.Clock();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
bloomPass.resolution.set(window.innerWidth, window.innerHeight);
});
window.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
window.addEventListener('keydown', (e) => {
if ((e.code === 'ShiftLeft' || e.code === 'ShiftRight') && !boostActive && turbo > 10) {
boostActive = true;
playSound('boost');
playerMesh.material.emissiveIntensity = 4;
}
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') {
boostActive = false;
playerMesh.material.emissiveIntensity = 1.5;
}
});
document.addEventListener('click', () => {
if (!audioCtx) initAudio();
}, { once: true });
requestAnimationFrame(animate);
}
function spawnTrail() {
const geo = new THREE.SphereGeometry(0.15, 4, 4);
const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xff4400, roughness: 1 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(
playerMesh.position.x + (Math.random() - 0.5) * 0.4,
playerMesh.position.y + (Math.random() - 0.5) * 0.3,
playerMesh.position.z
);
scene.add(mesh);
particles.push({
mesh: mesh, life: 1.0,
velocity: new THREE.Vector3(0, 0, -speed * 3)
});
}
function spawnDebris() {
if (Math.random() < 0.15) {
const geo = new THREE.BoxGeometry(0.2 + Math.random() * 0.4, 0.2, 0.2 + Math.random() * 0.4);
const mat = new THREE.MeshStandardMaterial({ color: 0x554433, roughness: 0.9 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(
pathX(playerMesh.position.z + 40) + (Math.random() - 0.5) * 25,
1 + Math.random() * 8,
playerMesh.position.z + 30 + Math.random() * 50
);
scene.add(mesh);
particles.push({
mesh: mesh, life: 15.0,
velocity: new THREE.Vector3(0, 0, -speed * 1.5)
});
}
}
function updateParticles(delta) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.mesh.position.add(p.velocity.clone().multiplyScalar(delta));
p.life -= delta;
if (p.life <= 0) {
scene.remove(p.mesh);
particles.splice(i, 1);
}
}
for (let i = 0; i < 2; i++) spawnTrail();
spawnDebris();
updateParticles(0); // immediate cleanup of new spawns? No, just keep loop.
}
// Actually fix particle loop
function processParticles(delta) {
for (let i = 0; i < 2; i++) {
const geo = new THREE.SphereGeometry(0.15, 4, 4);
const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xff4400, roughness: 1 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(playerMesh.position);
mesh.position.x += (Math.random() - 0.5) * 0.5;
mesh.position.y += (Math.random() - 0.5) * 0.2;
scene.add(mesh);
particles.push({ mesh: mesh, life: 0.8 + Math.random() * 0.5, velocity: new THREE.Vector3(0, 0, -speed * 3) });
}
if (Math.random() < 0.2) {
const geo = new THREE.BoxGeometry(0.3, 0.3, 0.3);
const mat = new THREE.MeshStandardMaterial({ color: 0x665544, roughness: 0.9 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(
pathX(playerMesh.position.z + 50) + (Math.random() - 0.5) * 30,
3 + Math.random() * 6,
playerMesh.position.z + 60
);
scene.add(mesh);
particles.push({ mesh: mesh, life: 12, velocity: new THREE.Vector3(0, 0, -speed * 2) });
}
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.mesh.position.add(p.velocity.clone().multiplyScalar(delta));
p.life -= delta;
if (p.life <= 0) {
scene.remove(p.mesh);
particles.splice(i, 1);
}
}
}
function updatePhysics(delta) {
if (!running) return;
// Mouse controls mapped to flight axes
const mxNorm = (mouseX / window.innerWidth - 0.5) * 2;
const myNorm = (mouseY / window.innerHeight - 0.5) * 2;
// Roll (banking) controls lateral shift and craft roll
const rollTarget = mxNorm * 1.5;
const rollLerp = THREE.MathUtils.lerp;
playerMesh.rotation.z = rollLerp(playerMesh.rotation.z, rollTarget * 0.6, 0.12);
// Pitch controls acceleration (down = faster, up = slower)
const pitchTarget = myNorm * 1.2;
playerMesh.rotation.x = rollLerp(playerMesh.rotation.x, -Math.PI / 2 + pitchTarget * 0.5, 0.1);
// Speed dynamics
const pitchEffect = pitchTarget;
speedTarget = 60 + pitchEffect * 50;
if (boostActive) {
speedTarget *= 2.2;
turbo = Math.max(0, turbo - delta * 40);
} else {
turbo = Math.min(maxTurbo, turbo + delta * 1.5 + (pitchEffect > 0 ? delta * 2 : 0));
}
speed = THREE.MathUtils.lerp(speed, speedTarget, 0.06);
speed = THREE.MathUtils.clamp(speed, 30, maxBaseSpeed + (boostActive ? 80 : 0));
// Lateral movement from banking
const lateralShift = rollTarget * speed * delta * 0.35;
playerMesh.position.x += lateralShift;
// Forward motion
playerMesh.position.z += speed * delta;
// Energy decay / boost consumption
energy -= delta * (6 + speed * 0.08 + (boostActive ? 15 : 0));
if (energy < 0) energy = 0;
// Near-miss logic using procedural path
const cx = pathX(playerMesh.position.z);
const currentW = Math.max(8, 36 - score * 0.14);
const leftX = cx - currentW / 2;
const rightX = cx + currentW / 2;
const dL = Math.abs(playerMesh.position.x - leftX);
const dR = Math.abs(playerMesh.position.x - rightX);
const nearestWallDist = Math.min(dL, dR);
let nearMiss = 0;
if (nearestWallDist < 5 && nearestWallDist > 0.4) {
nearMiss = 1 - (nearestWallDist / 5);
score += delta * 30 * multiplier * nearMiss;
multiplier = Math.min(10, multiplier + nearMiss * 0.3 * delta);
shakeIntensity = Math.min(4, shakeIntensity + nearMiss * 0.8);
if (Math.random() < 0.15) playSound('near');
} else {
multiplier = Math.max(1, multiplier - delta * 0.3);
shakeIntensity *= 0.92;
}
// Collision: walls
if (nearestWallDist < 0.9) {
triggerCrash();
return;
}
// Obstacles & Orbs
for (const chunk of chunks) {
if (chunk.group.userData.obstacles) {
for (const obs of chunk.group.userData.obstacles) {
const dx = playerMesh.position.x - obs.x;
const dz = playerMesh.position.z - obs.z;
if (Math.abs(dx) < obs.r + 0.6 && Math.abs(dz) < obs.r + 2) {
triggerCrash();
return;
}
}
}
if (chunk.group.userData.orbs) {
for (let o = chunk.group.userData.orbs.length - 1; o >= 0; o--) {
const orbData = chunk.group.userData.orbs[o];
const dx = playerMesh.position.x - orbData.x;
const dy = playerMesh.position.y - orbData.y;
const dz = playerMesh.position.z - orbData.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (dist < orbData.r + 0.6) {
energy += 25;
turbo += 20;
score += 150 * multiplier;
multiplier += 0.5;
playSound('collect');
chunk.group.remove(orbData.mesh);
chunk.group.remove(orbData.light);
chunk.group.userData.orbs.splice(o, 1);
}
}
}
}
// Camera follow with shake
const camOffset = new THREE.Vector3(
THREE.MathUtils.lerp(camera.position.x, playerMesh.position.x, 0.1),
THREE.MathUtils.lerp(camera.position.y, playerMesh.position.y + 4 + (boostActive ? 2 : 0), 0.1),
THREE.MathUtils.lerp(camera.position.z, playerMesh.position.z - 14 - (boostActive ? 6 : 0), 0.1)
);
if (shakeIntensity > 0.1) {
camOffset.x += (Math.random() - 0.5) * shakeIntensity * 0.08;
camOffset.y += (Math.random() - 0.5) * shakeIntensity * 0.04;
shakeIntensity *= 0.88;
}
camera.position.copy(camOffset);
camera.lookAt(
playerMesh.position.x,
playerMesh.position.y,
playerMesh.position.z + 25
);
// Generate new chunks as we move
const aheadZ = Math.ceil((playerMesh.position.z + 300) / chunkLength) * chunkLength;
const lastChunkZ = chunks[chunks.length - 1].z;
if (aheadZ > lastChunkZ) {
generateChunk(lastChunkZ + chunkLength);
}
// Cleanup old chunks (behind player)
while (chunks.length > 0 && chunks[0].z < playerMesh.position.z - 150) {
const old = chunks.shift();
scene.remove(old.group);
}
// Update HUD
document.getElementById('multiplier').textContent = 'x' + multiplier.toFixed(1);
document.getElementById('score-display').textContent = Math.floor(score);
document.getElementById('speed-val').textContent = Math.floor(speed) + ' m/s';
document.getElementById('energy-fill').style.width = Math.max(0, energy) + '%';
document.getElementById('boost-fill').style.width = (turbo / maxTurbo * 100) + '%';
// Motion blur intensity based on speed & boost
motionPass.uniforms.blurStrength.value = (speed / maxBaseSpeed) * (boostActive ? 3.5 : 1.5) + (shakeIntensity / 10);
updateEngineSound();
}
function triggerCrash() {
running = false;
playSound('near');
document.getElementById('gameover').style.display = 'flex';
document.getElementById('final-score').textContent = Math.floor(score);
document.getElementById('final-speed').textContent = Math.floor(speed) + ' m/s';
document.getElementById('final-mult').textContent = multiplier.toFixed(1);
}
function restartGame() {
window.location.reload(); // simplest reset
}
function animate() {
requestAnimationFrame(animate);
if (!running) {
composer.render();
return;
}
const delta = Math.min(clock.getDelta(), 0.05);
updatePhysics(delta);
processParticles(delta);
composer.render();
}
window.restartGame = restartGame;
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"basic","sessionId":"b84c8afa-e009-4349-8261-d261d452e1ed","tokensIn":950,"tokensOut":20573,"tokensTotal":21523,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T20:28:51.195Z"} -->