Canyon Run
longcat 2.0 · composer
◈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 34,614 bytes · SHA-256 b536d4d3c2d4
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Canyon Run — Procedural Canyon Racer</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/MaskPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a0f; font-family: 'Courier New', monospace; }
#game-container { position: relative; width: 100%; height: 100%; }
#game-canvas { display: block; width: 100%; height: 100%; }
.hidden { display: none !important; }
/* ── css: hud ── */
#hud {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10;
}
#hud-top {
position: absolute; top: 24px; left: 0; right: 0;
display: flex; justify-content: space-between; padding: 0 36px;
}
#score-display {
font-size: 32px; font-weight: bold; color: #fff;
text-shadow: 0 0 20px rgba(255, 140, 50, 0.8), 0 0 40px rgba(255, 100, 20, 0.4);
letter-spacing: 2px;
}
#multiplier-display {
font-size: 28px; font-weight: bold; color: #ffcc00;
text-shadow: 0 0 15px rgba(255, 200, 0, 0.8);
transition: transform 0.1s, color 0.2s;
}
#multiplier-display.pulse {
transform: scale(1.3); color: #fff;
}
#hud-bottom {
position: absolute; bottom: 24px; left: 36px; right: 36px;
display: flex; gap: 24px; align-items: flex-end;
}
#energy-container, #speed-container {
flex: 1; max-width: 300px;
}
#energy-bar, #speed-bar {
height: 4px; border-radius: 2px; transition: width 0.15s;
}
#energy-bar {
background: linear-gradient(90deg, #00ffaa, #00ccff);
box-shadow: 0 0 12px rgba(0, 255, 170, 0.6);
}
#speed-bar {
background: linear-gradient(90deg, #ff6600, #ffcc00);
box-shadow: 0 0 12px rgba(255, 150, 0, 0.6);
}
#energy-label, #speed-label {
font-size: 10px; color: rgba(255,255,255,0.5);
letter-spacing: 3px; margin-top: 4px;
}
#turbo-container {
position: absolute; bottom: 24px; right: 36px;
width: 60px; height: 60px; text-align: center;
}
#turbo-ring {
width: 60px; height: 60px;
}
#turbo-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
#turbo-bg {
fill: none; stroke: rgba(255,255,255,0.1); stroke-width: 4;
}
#turbo-fill {
fill: none; stroke: #ff4400; stroke-width: 4;
stroke-dasharray: 251.2; stroke-dashoffset: 251.2;
stroke-linecap: round; transition: stroke-dashoffset 0.2s;
filter: drop-shadow(0 0 6px rgba(255, 68, 0, 0.8));
}
#turbo-label {
font-size: 9px; color: rgba(255,255,255,0.4);
letter-spacing: 2px; margin-top: 2px;
}
/* ── css: screens ── */
#game-over, #start-screen {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
z-index: 20; background: rgba(5, 5, 15, 0.85);
backdrop-filter: blur(4px);
}
#game-over-content, #start-content {
text-align: center; color: #fff;
}
#game-over h1, #start-content h1 {
font-size: 56px; letter-spacing: 8px; font-weight: bold;
background: linear-gradient(135deg, #ff6600, #ffcc00);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text; margin-bottom: 12px;
}
#start-content h1 {
font-size: 64px;
}
.subtitle {
font-size: 14px; letter-spacing: 6px; color: rgba(255,255,255,0.5);
margin-bottom: 40px;
}
#final-score {
font-size: 48px; font-weight: bold; color: #ffcc00;
text-shadow: 0 0 20px rgba(255, 200, 0, 0.6);
margin: 16px 0;
}
#final-stats {
display: flex; gap: 24px; justify-content: center;
font-size: 12px; color: rgba(255,255,255,0.5); letter-spacing: 2px;
margin-bottom: 32px;
}
.controls-info {
display: flex; flex-direction: column; gap: 8px;
font-size: 12px; color: rgba(255,255,255,0.4); letter-spacing: 2px;
margin-bottom: 36px;
}
#restart-btn, #start-btn {
pointer-events: all; cursor: pointer;
background: linear-gradient(135deg, #ff6600, #ff8800);
border: none; color: #fff; font-family: 'Courier New', monospace;
font-size: 16px; letter-spacing: 4px; padding: 14px 48px;
border-radius: 4px; font-weight: bold;
transition: transform 0.15s, box-shadow 0.15s;
box-shadow: 0 0 20px rgba(255, 100, 0, 0.4);
}
#restart-btn:hover, #start-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 30px rgba(255, 100, 0, 0.7);
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="hud">
<div id="hud-top">
<div id="score-display">0</div>
<div id="multiplier-display">x1.0</div>
</div>
<div id="hud-bottom">
<div id="energy-container">
<div id="energy-bar"></div>
<div id="energy-label">ENERGY</div>
</div>
<div id="speed-container">
<div id="speed-bar"></div>
<div id="speed-label">SPEED</div>
</div>
</div>
<div id="turbo-container">
<div id="turbo-ring">
<svg viewBox="0 0 100 100">
<circle id="turbo-bg" cx="50" cy="50" r="40"></circle>
<circle id="turbo-fill" cx="50" cy="50" r="40"></circle>
</svg>
</div>
<div id="turbo-label">TURBO</div>
</div>
</div>
<div id="game-over" class="hidden">
<div id="game-over-content">
<h1>CRASHED</h1>
<div id="final-score">0</div>
<div id="final-stats">
<span id="max-speed-stat">MAX SPEED: 0</span>
<span id="max-multiplier-stat">MAX MULTI: x1.0</span>
</div>
<button id="restart-btn">RESTART</button>
</div>
</div>
<div id="start-screen">
<div id="start-content">
<h1>CANYON RUN</h1>
<p class="subtitle">PROCEDURAL CANYON RACER</p>
<div class="controls-info">
<span>← → or A/D : STEER</span>
<span>↑ ↓ or W/S : PITCH</span>
<span>SPACE : BOOST</span>
</div>
<button id="start-btn">LAUNCH</button>
</div>
</div>
</div>
<script>
'use strict';
// ── function: createAudioSystem ──
function createAudioSystem() {
let ctx = null;
let masterGain = null;
let engineOsc = null;
let engineGain = null;
function init() {
if (ctx) return;
ctx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = ctx.createGain();
masterGain.gain.value = 0.3;
masterGain.connect(ctx.destination);
// Engine hum
engineOsc = ctx.createOscillator();
engineOsc.type = 'sawtooth';
engineOsc.frequency.value = 80;
engineGain = ctx.createGain();
engineGain.gain.value = 0.08;
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 200;
engineOsc.connect(filter);
filter.connect(engineGain);
engineGain.connect(masterGain);
engineOsc.start();
}
function updateEngine(speed, boost) {
if (!ctx) return;
const baseFreq = 60 + speed * 2;
engineOsc.frequency.setTargetAtTime(baseFreq + (boost ? 80 : 0), ctx.currentTime, 0.05);
engineGain.gain.setTargetAtTime(0.06 + speed * 0.003 + (boost ? 0.06 : 0), ctx.currentTime, 0.05);
}
function playCollect() {
if (!ctx) return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.value = 880;
gain.gain.value = 0.15;
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
osc.frequency.exponentialRampToValueAtTime(1760, ctx.currentTime + 0.15);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(ctx.currentTime + 0.2);
}
function playNearMiss() {
if (!ctx) return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'triangle';
osc.frequency.value = 440;
gain.gain.value = 0.12;
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
osc.frequency.exponentialRampToValueAtTime(220, ctx.currentTime + 0.25);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(ctx.currentTime + 0.3);
}
function playCrash() {
if (!ctx) return;
const bufferSize = ctx.sampleRate * 0.5;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
}
const source = ctx.createBufferSource();
source.buffer = buffer;
const gain = ctx.createGain();
gain.gain.value = 0.3;
source.connect(gain);
gain.connect(masterGain);
source.start();
}
function playBoost() {
if (!ctx) return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.value = 200;
osc.frequency.exponentialRampToValueAtTime(600, ctx.currentTime + 0.15);
gain.gain.value = 0.1;
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(ctx.currentTime + 0.2);
}
return { init, updateEngine, playCollect, playNearMiss, playCrash, playBoost };
}
// ── function: createPlayerCraft ──
function createPlayerCraft(scene) {
const group = new THREE.Group();
// Main body - sleek angular shape
const bodyGeom = new THREE.ConeGeometry(0.8, 3.5, 4);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0xccddee, flatShading: true, metalness: 0.7, roughness: 0.3
});
const body = new THREE.Mesh(bodyGeom, bodyMat);
body.rotation.x = Math.PI / 2;
group.add(body);
// Wings
const wingGeom = new THREE.BufferGeometry();
const wingVerts = new Float32Array([
0, 0, -0.5, -3, -0.3, 1.5, 0, 0, 1,
0, 0, -0.5, 0, 0, 1, 3, -0.3, 1.5,
0, 0, -0.5, 3, -0.3, 1.5, 0, 0.4, 0,
0, 0, -0.5, 0, 0.4, 0, -3, -0.3, 1.5,
-3, -0.3, 1.5, 0, 0.4, 0, 0, 0, -0.5,
3, -0.3, 1.5, 0, 0, -0.5, 0, 0.4, 0,
]);
wingGeom.setAttribute('position', new THREE.BufferAttribute(wingVerts, 3));
wingGeom.computeVertexNormals();
const wingMat = new THREE.MeshStandardMaterial({
color: 0x8899bb, flatShading: true, metalness: 0.6, roughness: 0.4, side: THREE.DoubleSide
});
const wings = new THREE.Mesh(wingGeom, wingMat);
group.add(wings);
// Engine glow
const engineGeom = new THREE.SphereGeometry(0.4, 8, 8);
const engineMat = new THREE.MeshBasicMaterial({ color: 0xff6600 });
const engine = new THREE.Mesh(engineGeom, engineMat);
engine.position.z = 1.8;
engine.name = 'engine';
group.add(engine);
// Engine trail light
const trailLight = new THREE.PointLight(0xff6600, 2, 15);
trailLight.position.z = 2.5;
group.add(trailLight);
group.scale.set(0.7, 0.7, 0.7);
scene.add(group);
return {
group, body, wings, engine, trailLight,
roll: 0, pitch: 0, targetRoll: 0, targetPitch: 0
};
}
// ── function: createTrailParticles ──
function createTrailParticles(scene, playerGroup) {
const maxParticles = 60;
const positions = new Float32Array(maxParticles * 3);
const opacities = new Float32Array(maxParticles);
const sizes = new Float32Array(maxParticles);
for (let i = 0; i < maxParticles; i++) {
positions[i*3] = 0; positions[i*3+1] = 0; positions[i*3+2] = 0;
opacities[i] = 0; sizes[i] = 0.3;
}
const geom = new THREE.BufferGeometry();
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geom.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1));
geom.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const mat = new THREE.PointsMaterial({
color: 0xff8800, size: 0.5, transparent: true, opacity: 0.6,
blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true
});
const particles = new THREE.Points(geom, mat);
scene.add(particles);
let idx = 0;
function emit(pos, speed) {
const i = idx % maxParticles;
positions[i*3] = pos.x + (Math.random() - 0.5) * 0.3;
positions[i*3+1] = pos.y + (Math.random() - 0.5) * 0.3;
positions[i*3+2] = pos.z + 2 + Math.random() * 0.5;
opacities[i] = 0.8;
sizes[i] = 0.3 + speed * 0.01;
idx++;
geom.attributes.position.needsUpdate = true;
geom.attributes.opacity.needsUpdate = true;
}
function update(dt) {
for (let i = 0; i < maxParticles; i++) {
if (opacities[i] > 0) {
positions[i*3+2] += 35 * dt;
opacities[i] -= 2.0 * dt;
if (opacities[i] < 0) opacities[i] = 0;
}
}
geom.attributes.position.needsUpdate = true;
geom.attributes.opacity.needsUpdate = true;
mat.opacity = 0.6;
}
return { emit, update, particles };
}
// ── function: createTerrainSystem ──
function createTerrainSystem(scene) {
const chunks = [];
const chunkLength = 100;
const visibleChunks = 4;
const canyonWidth = 18;
// Shared materials (reuse to reduce draw call overhead)
const wallMat = new THREE.MeshStandardMaterial({ color: 0xcc6633, flatShading: true, roughness: 0.85 });
const wallMat2 = new THREE.MeshStandardMaterial({ color: 0x994422, flatShading: true, roughness: 0.9 });
const groundMat = new THREE.MeshStandardMaterial({ color: 0x553322, flatShading: true, roughness: 1.0 });
const orbMat = new THREE.MeshBasicMaterial({ color: 0x00ffcc });
// Shared geometries
const groundGeom = new THREE.PlaneGeometry(canyonWidth + 30, chunkLength);
const wallGeom = new THREE.BoxGeometry(4, 35, chunkLength);
const orbGeom = new THREE.OctahedronGeometry(0.7, 0);
function createChunk(zOffset, difficulty) {
const group = new THREE.Group();
group.position.z = zOffset;
// Ground
const ground = new THREE.Mesh(groundGeom, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.set(0, -5, -chunkLength/2);
group.add(ground);
// Walls
const leftWall = new THREE.Mesh(wallGeom, wallMat);
leftWall.position.set(-canyonWidth/2 - 2, 12.5, -chunkLength/2);
group.add(leftWall);
const rightWall = new THREE.Mesh(wallGeom, wallMat2);
rightWall.position.set(canyonWidth/2 + 2, 12.5, -chunkLength/2);
group.add(rightWall);
// Obstacles - fewer, simpler
const obstacles = [];
const numObstacles = Math.floor(1.5 + difficulty * 0.8);
for (let i = 0; i < numObstacles; i++) {
const z = -Math.random() * chunkLength * 0.9;
const type = Math.random();
if (type < 0.4) {
// Monolith
const h = 8 + Math.random() * 10;
const w = 2.5 + Math.random() * 2;
const monolithGeom = new THREE.BoxGeometry(w, h, w);
const monolith = new THREE.Mesh(monolithGeom, wallMat);
const xPos = (Math.random() - 0.5) * (canyonWidth - w - 4);
monolith.position.set(xPos, -5 + h/2, z);
group.add(monolith);
obstacles.push({ pos: monolith.position, size: { x: w, y: h, z: w }, active: true });
} else if (type < 0.7) {
// Gate
const gapWidth = Math.max(4, 9 - difficulty * 0.6);
const gateH = 10;
const gateY = 0;
const gateGeom = new THREE.BoxGeometry(2.5, gateH, 2.5);
const leftGate = new THREE.Mesh(gateGeom, wallMat2);
leftGate.position.set(-gapWidth/2 - 1.25, gateY, z);
group.add(leftGate);
obstacles.push({ pos: leftGate.position, size: { x: 2.5, y: gateH, z: 2.5 }, active: true });
const rightGate = new THREE.Mesh(gateGeom, wallMat2);
rightGate.position.set(gapWidth/2 + 1.25, gateY, z);
group.add(rightGate);
obstacles.push({ pos: rightGate.position, size: { x: 2.5, y: gateH, z: 2.5 }, active: true });
} else {
// Barrier
const bw = 5 + Math.random() * 4;
const barrierGeom = new THREE.BoxGeometry(bw, 2.5, 2.5);
const barrier = new THREE.Mesh(barrierGeom, wallMat);
barrier.position.set((Math.random() - 0.5) * (canyonWidth - bw - 2), 3 + Math.random() * 3, z);
group.add(barrier);
obstacles.push({ pos: barrier.position, size: { x: bw, y: 2.5, z: 2.5 }, active: true });
}
}
// Orbs
const orbs = [];
const numOrbs = 2 + Math.floor(Math.random() * 2);
for (let i = 0; i < numOrbs; i++) {
const orb = new THREE.Mesh(orbGeom, orbMat);
const ox = (Math.random() - 0.5) * (canyonWidth - 6);
const oy = -2 + Math.random() * 5;
const oz = -Math.random() * chunkLength * 0.9;
orb.position.set(ox, oy, oz);
group.add(orb);
const glowLight = new THREE.PointLight(0x00ffcc, 1.5, 8);
glowLight.position.copy(orb.position);
group.add(glowLight);
orbs.push({ mesh: orb, light: glowLight, pos: orb.position, collected: false });
}
scene.add(group);
return { group, obstacles, orbs, zOffset, length: chunkLength };
}
function init() {
for (let i = 0; i < visibleChunks; i++) {
chunks.push(createChunk(-i * chunkLength, 0));
}
}
function update(playerZ, difficulty) {
const lastChunk = chunks[chunks.length - 1];
if (lastChunk && playerZ < lastChunk.zOffset + chunkLength * 2) {
chunks.push(createChunk(lastChunk.zOffset - chunkLength, difficulty));
}
while (chunks.length > 0 && chunks[0].zOffset > playerZ + chunkLength * 2) {
const old = chunks.shift();
scene.remove(old.group);
old.group.traverse(obj => {
if (obj.geometry && obj.geometry !== groundGeom && obj.geometry !== wallGeom && obj.geometry !== orbGeom) obj.geometry.dispose();
});
}
}
function getAllObstacles() {
const all = [];
for (const chunk of chunks) {
for (const obs of chunk.obstacles) {
if (obs.active) {
all.push({ pos: { x: obs.pos.x, y: obs.pos.y, z: obs.pos.z + chunk.group.position.z }, size: obs.size });
}
}
}
return all;
}
function getAllOrbs() {
const all = [];
for (const chunk of chunks) {
for (const orb of chunk.orbs) {
if (!orb.collected) {
all.push({ pos: { x: orb.pos.x, y: orb.pos.y, z: orb.pos.z + chunk.group.position.z }, mesh: orb.mesh, light: orb.light, orbRef: orb });
}
}
}
return all;
}
return { init, update, getAllObstacles, getAllOrbs, chunks, chunkLength };
}
// ── function: createDebrisParticles ──
function createDebrisParticles(scene) {
const maxParticles = 100;
const positions = new Float32Array(maxParticles * 3);
const velocities = new Float32Array(maxParticles * 3);
const lifetimes = new Float32Array(maxParticles);
const geom = new THREE.BufferGeometry();
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0xcc8844, size: 0.3, transparent: true, opacity: 0.7,
blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true
});
const particles = new THREE.Points(geom, mat);
scene.add(particles);
let spawnTimer = 0;
function spawn(pos, speed) {
for (let j = 0; j < 3; j++) {
for (let i = 0; i < maxParticles; i++) {
if (lifetimes[i] <= 0) {
positions[i*3] = pos.x + (Math.random() - 0.5) * 8;
positions[i*3+1] = pos.y + (Math.random() - 0.5) * 4;
positions[i*3+2] = pos.z + (Math.random() - 0.5) * 3;
velocities[i*3] = (Math.random() - 0.5) * 2;
velocities[i*3+1] = (Math.random() - 0.5) * 2;
velocities[i*3+2] = -speed * 0.3 - Math.random() * 10;
lifetimes[i] = 0.5 + Math.random() * 0.5;
break;
}
}
}
}
function update(dt) {
for (let i = 0; i < maxParticles; i++) {
if (lifetimes[i] > 0) {
positions[i*3] += velocities[i*3] * dt;
positions[i*3+1] += velocities[i*3+1] * dt;
positions[i*3+2] += velocities[i*3+2] * dt;
lifetimes[i] -= dt;
if (lifetimes[i] <= 0) {
positions[i*3+2] = 999;
}
} else {
positions[i*3+2] = 999;
}
}
geom.attributes.position.needsUpdate = true;
}
return { spawn, update, particles };
}
// ── function: createHUD ──
function createHUD() {
const scoreEl = document.getElementById('score-display');
const multEl = document.getElementById('multiplier-display');
const energyBar = document.getElementById('energy-bar');
const speedBar = document.getElementById('speed-bar');
const turboFill = document.getElementById('turbo-fill');
const gameOverEl = document.getElementById('game-over');
const finalScoreEl = document.getElementById('final-score');
const maxSpeedEl = document.getElementById('max-speed-stat');
const maxMultEl = document.getElementById('max-multiplier-stat');
const startScreen = document.getElementById('start-screen');
let lastMult = 1.0;
function update(score, multiplier, energy, maxEnergy, speed, maxSpeedRef, turbo) {
scoreEl.textContent = Math.floor(score).toLocaleString();
if (Math.abs(multiplier - lastMult) > 0.1) {
multEl.textContent = 'x' + multiplier.toFixed(1);
multEl.classList.add('pulse');
setTimeout(() => multEl.classList.remove('pulse'), 150);
lastMult = multiplier;
}
const energyPct = Math.max(0, energy / maxEnergy * 100);
energyBar.style.width = energyPct + '%';
if (energyPct < 25) {
energyBar.style.background = 'linear-gradient(90deg, #ff3300, #ff6600)';
energyBar.style.boxShadow = '0 0 12px rgba(255, 50, 0, 0.8)';
} else {
energyBar.style.background = 'linear-gradient(90deg, #00ffaa, #00ccff)';
energyBar.style.boxShadow = '0 0 12px rgba(0, 255, 170, 0.6)';
}
const speedPct = Math.min(100, speed / (maxSpeedRef * 1.5) * 100);
speedBar.style.width = speedPct + '%';
// Turbo ring - circumference is 2*PI*40 ≈ 251.2
const turboPct = Math.max(0, Math.min(1, turbo / 100));
turboFill.style.strokeDashoffset = 251.2 * (1 - turboPct);
}
function showGameOver(score, maxSpeed, maxMult) {
finalScoreEl.textContent = Math.floor(score).toLocaleString();
maxSpeedEl.textContent = 'MAX SPEED: ' + Math.floor(maxSpeed);
maxMultEl.textContent = 'MAX MULTI: x' + maxMult.toFixed(1);
gameOverEl.classList.remove('hidden');
}
function hideGameOver() {
gameOverEl.classList.add('hidden');
}
function hideStartScreen() {
startScreen.classList.add('hidden');
}
function showStartScreen() {
startScreen.classList.remove('hidden');
}
return { update, showGameOver, hideGameOver, hideStartScreen, showStartScreen, startScreen, gameOverEl };
}
// ── function: createGame ──
function createGame() {
const canvas = document.getElementById('game-canvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false, powerPreference: 'high-performance' });
const isMobile = window.innerWidth < 768;
const renderScale = isMobile ? 0.6 : 0.85;
renderer.setSize(window.innerWidth * renderScale, window.innerHeight * renderScale);
canvas.style.width = '100%';
canvas.style.height = '100%';
renderer.setPixelRatio(1);
renderer.shadowMap.enabled = false;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.3;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x2a1538, 0.007);
scene.background = new THREE.Color(0x2a1538);
// Starfield background
const starGeom = new THREE.BufferGeometry();
const starCount = 200;
const starPositions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
starPositions[i*3] = (Math.random() - 0.5) * 200;
starPositions[i*3+1] = (Math.random() - 0.5) * 100 + 20;
starPositions[i*3+2] = -Math.random() * 200 - 20;
}
starGeom.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
const starMat = new THREE.PointsMaterial({ color: 0xffaaff, size: 0.8, transparent: true, opacity: 0.6, sizeAttenuation: true });
const stars = new THREE.Points(starGeom, starMat);
scene.add(stars);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 150);
camera.position.set(0, 2, 8);
// Lighting
scene.add(new THREE.AmbientLight(0x7755aa, 1.4));
const dirLight = new THREE.DirectionalLight(0xff9955, 2.5);
dirLight.position.set(-8, 15, -5);
scene.add(dirLight);
const fillLight = new THREE.DirectionalLight(0x6688ff, 0.8);
fillLight.position.set(10, 8, 5);
scene.add(fillLight);
const rimLight = new THREE.DirectionalLight(0xff6633, 1.0);
rimLight.position.set(0, -5, 10);
scene.add(rimLight);
// Systems
const audio = createAudioSystem();
const terrain = createTerrainSystem(scene);
const debris = createDebrisParticles(scene);
const hud = createHUD();
// Player
const player = createPlayerCraft(scene);
const trail = createTrailParticles(scene, player.group);
// State
let state = 'start';
let playerX = 0, playerY = 0, playerZ = 0;
let velX = 0, velY = 0;
let speed = 35, baseSpeed = 35, maxSpeedReached = 35;
let energy = 100, score = 0, multiplier = 1.0, maxMultiplier = 1.0;
let turbo = 0, boosting = false, difficulty = 0;
let nearMissTimer = 0, cameraShake = 0, time = 0;
let lastTime = performance.now();
const keys = {};
window.addEventListener('keydown', e => { keys[e.code] = true; });
window.addEventListener('keyup', e => { keys[e.code] = false; });
let touchX = 0, touchY = 0, touching = false;
canvas.addEventListener('touchstart', e => {
touching = true;
touchX = e.touches[0].clientX;
touchY = e.touches[0].clientY;
e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchmove', e => {
touchX = e.touches[0].clientX;
touchY = e.touches[0].clientY;
e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchend', () => { touching = false; });
function start() {
audio.init();
state = 'playing';
playerX = 0; playerY = 0; playerZ = 0;
velX = 0; velY = 0;
speed = 35; baseSpeed = 35; maxSpeedReached = 35;
energy = 100; score = 0; multiplier = 1.0; maxMultiplier = 1.0;
turbo = 0; difficulty = 0; nearMissTimer = 0; cameraShake = 0; time = 0;
lastTime = performance.now();
terrain.init();
hud.hideGameOver();
hud.hideStartScreen();
}
function gameOver() {
state = 'over';
audio.playCrash();
hud.showGameOver(score, maxSpeedReached, maxMultiplier);
}
function reset() {
for (const chunk of terrain.chunks) {
scene.remove(chunk.group);
chunk.group.traverse(obj => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
else obj.material.dispose();
}
});
}
terrain.chunks.length = 0;
start();
}
function update(dt) {
if (state !== 'playing') return;
dt = Math.min(dt, 0.033); // Cap at ~30fps worth of dt
time += dt;
difficulty = Math.min(10, time * 0.08);
let inputX = 0, inputY = 0;
if (keys['ArrowLeft'] || keys['KeyA']) inputX -= 1;
if (keys['ArrowRight'] || keys['KeyD']) inputX += 1;
if (keys['ArrowUp'] || keys['KeyW']) inputY += 1;
if (keys['ArrowDown'] || keys['KeyS']) inputY -= 1;
if (touching) {
const cx = window.innerWidth / 2, cy = window.innerHeight / 2;
inputX = Math.max(-1, Math.min(1, (touchX - cx) / cx));
inputY = Math.max(-1, Math.min(1, -(touchY - cy) / cy));
}
boosting = (keys['Space'] || keys['ShiftLeft']) && turbo > 10 && energy > 5;
// Physics - momentum based
velX += inputX * 50 * dt - velX * 2.5 * dt;
velY += inputY * 20 * dt - velY * 2.5 * dt;
playerX += velX * dt;
playerY += velY * dt;
// Canyon bounds
if (playerX > 7) { playerX = 7; velX *= -0.3; }
if (playerX < -7) { playerX = -7; velX *= -0.3; }
if (playerY > 5) { playerY = 5; velY *= -0.3; }
if (playerY < -3) { playerY = -3; velY *= -0.3; }
// Speed management
baseSpeed = 35 + difficulty * 4;
if (boosting) {
speed = baseSpeed * 2.2;
turbo -= 25 * dt;
energy -= 6 * dt;
} else {
speed = baseSpeed + turbo * 0.4;
if (speed > baseSpeed * 1.1) turbo += 20 * dt;
}
turbo = Math.max(0, Math.min(100, turbo));
speed = Math.min(speed, baseSpeed * 2.2);
maxSpeedReached = Math.max(maxSpeedReached, speed);
// Forward motion
playerZ -= speed * dt;
// Energy decay
energy -= (1.5 + difficulty * 0.2) * dt;
if (energy <= 0) { energy = 0; gameOver(); return; }
// Score
score += speed * dt * multiplier * 0.8;
// Banking animation
player.targetRoll = -velX * 0.04;
player.targetPitch = velY * 0.03 - (boosting ? 0.1 : 0);
player.roll += (player.targetRoll - player.roll) * 5 * dt;
player.pitch += (player.targetPitch - player.pitch) * 5 * dt;
player.group.position.set(playerX, playerY, playerZ);
player.group.rotation.z = player.roll;
player.group.rotation.x = player.pitch;
// Engine glow scales with speed
player.engine.scale.setScalar(0.8 + speed * 0.015 + (boosting ? 0.5 : 0));
player.trailLight.intensity = 1.5 + speed * 0.05 + (boosting ? 3 : 0);
// Particles
trail.emit(player.group.position, speed);
trail.update(dt);
if (Math.random() < speed * 0.003) debris.spawn(player.group.position, speed);
debris.update(dt);
// Animate orbs (spin)
const orbsData = terrain.getAllOrbs();
for (const orb of orbsData) {
orb.mesh.rotation.y += 3 * dt;
orb.mesh.rotation.x += 1.5 * dt;
}
// Terrain management
terrain.update(playerZ, difficulty);
// Collision detection
const obstacles = terrain.getAllObstacles();
let hit = false, closestDist = Infinity;
for (const obs of obstacles) {
const ox = Math.max(obs.pos.x - obs.size.x/2, Math.min(playerX, obs.pos.x + obs.size.x/2));
const oy = Math.max(obs.pos.y - obs.size.y/2, Math.min(playerY, obs.pos.y + obs.size.y/2));
const oz = Math.max(obs.pos.z - obs.size.z/2, Math.min(playerZ, obs.pos.z + obs.size.z/2));
const dx = playerX - ox, dy = playerY - oy, dz = playerZ - oz;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (dist < closestDist) closestDist = dist;
if (dist < 1.2) hit = true;
}
if (hit) { gameOver(); return; }
// Near-miss detection
if (closestDist < 3.5 && closestDist > 1.2 && nearMissTimer <= 0) {
multiplier = Math.min(5.0, multiplier + 0.2);
maxMultiplier = Math.max(maxMultiplier, multiplier);
turbo += 8;
audio.playNearMiss();
nearMissTimer = 0.3;
cameraShake = 0.3;
}
nearMissTimer -= dt;
if (multiplier > 1.0) multiplier = Math.max(1.0, multiplier - 0.3 * dt);
// Orb collection
for (const orb of orbsData) {
const dx = playerX - orb.pos.x, dy = playerY - orb.pos.y, dz = playerZ - orb.pos.z;
if (Math.sqrt(dx*dx + dy*dy + dz*dz) < 2.0) {
orb.orbRef.collected = true;
orb.mesh.visible = false;
orb.light.visible = false;
energy = Math.min(100, energy + 15);
score += 100 * multiplier;
audio.playCollect();
}
}
// Camera follow with smoothing
camera.position.x += (playerX * 0.4 - camera.position.x) * 3 * dt;
camera.position.y += (playerY * 0.3 + 3 - camera.position.y) * 3 * dt;
camera.position.z += (playerZ + 10 - camera.position.z) * 3 * dt;
camera.lookAt(playerX * 0.3, playerY * 0.2, playerZ - 8);
// Camera shake on near-miss
if (cameraShake > 0) {
camera.position.x += (Math.random() - 0.5) * cameraShake * 0.5;
camera.position.y += (Math.random() - 0.5) * cameraShake * 0.3;
cameraShake -= dt * 2;
}
// FOV widens with speed
const targetFov = 75 + (speed - baseSpeed) * 0.5 + (boosting ? 15 : 0);
camera.fov += (targetFov - camera.fov) * 3 * dt;
camera.updateProjectionMatrix();
// Audio engine pitch
audio.updateEngine(speed / baseSpeed, boosting);
// Update HUD
hud.update(score, multiplier, energy, 100, speed, maxSpeedReached, turbo);
}
function loop() {
requestAnimationFrame(loop);
const now = performance.now();
const dt = (now - lastTime) / 1000;
lastTime = now;
update(dt);
renderer.render(scene, camera);
}
function handleResize() {
const rs = isMobile ? 0.6 : 0.85;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth * rs, window.innerHeight * rs);
}
window.addEventListener('resize', handleResize);
return { start, reset, loop, hud };
}
// ── main ──
const game = createGame();
game.loop();
// Start button
document.getElementById('start-btn').addEventListener('click', () => {
game.start();
});
// Restart button
document.getElementById('restart-btn').addEventListener('click', () => {
game.reset();
});
// Keyboard start
window.addEventListener('keydown', (e) => {
if (e.code === 'Enter' || e.code === 'Space') {
if (game.hud.startScreen && !game.hud.startScreen.classList.contains('hidden')) {
game.start();
} else if (!game.hud.gameOverEl.classList.contains('hidden')) {
game.reset();
}
}
});
</script>
</body>
</html>
<!-- agent-meta {"model":"meituan/longcat-2.0","provider":"openrouter","persona":"composer","sessionId":"1f9925f6-ff74-4e9a-9e75-a05611da1180","tokensIn":3244792,"tokensOut":63759,"tokensTotal":3308551,"turns":75,"toolCalls":86,"failedToolCalls":2,"timestamp":"2026-07-21T03:04:36.499Z"} -->