Canyon Run
Qwen 3.6 35b a3b · 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 54,887 bytes · SHA-256 9866147def26
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CANYON RUNNER</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/shaders/AfterImageShader.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/AfterImagePass.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/shaders/ConvolutionShader.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0010; font-family: 'Segoe UI', system-ui, sans-serif; }
#gameCanvas { display: block; width: 100vw; height: 100vh; }
/* ── css: hud ── */
#hud { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#score-display { position: absolute; top: 20px; right: 30px; text-align: right; }
#multiplier { font-size: 56px; font-weight: 900; color: #ff9900; text-shadow: 0 0 30px rgba(255,153,0,0.9), 0 0 60px rgba(255,100,0,0.4); letter-spacing: 3px; transition: transform 0.1s; }
#multiplier.pulse { transform: scale(1.5); color: #ffcc00; }
#score { font-size: 24px; color: #ddd; letter-spacing: 5px; margin-top: -8px; text-shadow: 0 0 10px rgba(255,255,255,0.3); }
#distance-display { position: absolute; top: 20px; left: 30px; text-align: left; }
#distance-label { font-size: 12px; color: #666; letter-spacing: 3px; text-transform: uppercase; }
#distance-value { font-size: 28px; color: #aaa; letter-spacing: 3px; text-shadow: 0 0 10px rgba(170,170,170,0.3); }
#speed-bar-container { position: absolute; bottom: 80px; left: 50%; transform: translateX(-50%); width: 300px; height: 3px; background: rgba(255,255,255,0.08); border-radius: 2px; }
#speed-bar { height: 100%; width: 0%; background: linear-gradient(90deg, #ff6600, #ffcc00); border-radius: 2px; transition: width 0.1s; box-shadow: 0 0 10px rgba(255,102,0,0.5); }
#energy-container { position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%); width: 200px; height: 6px; background: rgba(255,255,255,0.06); border-radius: 3px; overflow: hidden; }
#energy-bar { height: 100%; width: 100%; background: linear-gradient(90deg, #00ccff, #00ffcc); border-radius: 3px; transition: width 0.15s; box-shadow: 0 0 12px rgba(0,204,255,0.6); }
#energy-container.low #energy-bar { background: linear-gradient(90deg, #ff3300, #ff6600); box-shadow: 0 0 12px rgba(255,51,0,0.6); }
#boost-container { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); width: 150px; height: 4px; background: rgba(255,255,255,0.04); border-radius: 2px; overflow: hidden; }
#boost-bar { height: 100%; width: 0%; background: linear-gradient(90deg, #ff3300, #ff6600, #ffcc00); border-radius: 2px; transition: width 0.1s; box-shadow: 0 0 8px rgba(255,51,0,0.5); }
#boost-label { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); font-size: 10px; color: #555; letter-spacing: 4px; text-transform: uppercase; }
#near-miss-flash { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; background: radial-gradient(ellipse at center, rgba(255,200,0,0.2) 0%, transparent 70%); transition: opacity 0.05s; pointer-events: none; }
#near-miss-flash.active { opacity: 1; }
/* ── css: overlays ── */
.hidden { display: none !important; }
#game-over { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; z-index: 100; background: rgba(5,0,10,0.85); backdrop-filter: blur(8px); }
#game-over-content { text-align: center; }
#game-over-title { font-size: 72px; font-weight: 900; color: #ff3300; text-shadow: 0 0 40px rgba(255,51,0,0.6); letter-spacing: 8px; margin-bottom: 30px; }
#final-score { font-size: 28px; color: #ffcc00; letter-spacing: 4px; margin-bottom: 10px; text-shadow: 0 0 15px rgba(255,204,0,0.4); }
#max-speed { font-size: 18px; color: #888; letter-spacing: 3px; margin-bottom: 40px; }
#restart-btn { padding: 14px 50px; font-size: 20px; font-weight: 700; letter-spacing: 6px; color: #fff; background: transparent; border: 2px solid #ff6600; cursor: pointer; pointer-events: all; transition: all 0.2s; text-transform: uppercase; }
#restart-btn:hover { background: #ff6600; color: #000; box-shadow: 0 0 30px rgba(255,102,0,0.5); }
#start-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; z-index: 100; background: rgba(5,0,10,0.9); backdrop-filter: blur(8px); }
#start-content { text-align: center; }
#game-title { font-size: 64px; font-weight: 900; color: #ff9900; text-shadow: 0 0 40px rgba(255,153,0,0.6); letter-spacing: 10px; margin-bottom: 10px; }
#game-subtitle { font-size: 18px; color: #888; letter-spacing: 4px; margin-bottom: 40px; }
#controls-info { margin-bottom: 40px; color: #666; font-size: 14px; line-height: 2.2; }
.key { display: inline-block; padding: 2px 10px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; font-size: 12px; color: #aaa; margin: 0 2px; vertical-align: middle; }
#start-btn { padding: 14px 50px; font-size: 20px; font-weight: 700; letter-spacing: 6px; color: #fff; background: transparent; border: 2px solid #ff6600; cursor: pointer; pointer-events: all; transition: all 0.2s; text-transform: uppercase; }
#start-btn:hover { background: #ff6600; color: #000; box-shadow: 0 0 30px rgba(255,102,0,0.5); }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
<div id="score-display">
<div id="multiplier">x1</div>
<div id="score">0</div>
</div>
<div id="speed-bar-container">
<div id="speed-bar"></div>
</div>
<div id="energy-container">
<div id="energy-bar"></div>
</div>
<div id="boost-container">
<div id="boost-bar"></div>
</div>
<div id="near-miss-flash"></div>
<div id="distance-display">
<div id="distance-label">DISTANCE</div>
<div id="distance-value">0m</div>
</div>
<div id="boost-label">BOOST</div></div>
<div id="game-over" class="hidden">
<div id="game-over-content">
<h1 id="game-over-title">WRECKED</h1>
<div id="final-score">SCORE: 0</div>
<div id="max-speed">MAX SPEED: 0</div>
<button id="restart-btn">RESTART</button>
</div>
</div>
<div id="start-screen">
<div id="start-content">
<h1 id="game-title">CANYON RUN</h1>
<p id="game-subtitle">Navigate the infinite canyon</p>
<div id="controls-info">
<div><span class="key">A</span><span class="key">D</span> or <span class="key">←</span><span class="key">→</span> — Steer</div>
<div><span class="key">W</span><span class="key">S</span> or <span class="key">↑</span><span class="key">↓</span> — Pitch</div>
<div><span class="key">SPACE</span> — Boost (when charged)</div>
</div>
<button id="start-btn">START</button>
</div>
</div>
<script>
'use strict';
// ── const: CFG ──
const CFG = {
CHUNK_SIZE: 200,
CHUNK_COUNT: 8,
CANYON_WIDTH: 120,
CANYON_HEIGHT: 80,
BASE_SPEED: 80,
MAX_SPEED: 250,
BOOST_SPEED: 400,
PLAYER_SIZE: 2.5,
PLAYER_START_Y: 10,
ENERGY_MAX: 100,
ENERGY_DECAY: 3.5,
ENERGY_RECHARGE: 25,
BOOST_MAX: 100,
BOOST_DRAIN: 40,
BOOST_REGEN: 8,
BOOST_DURATION: 3,
NEAR_MISS_THRESHOLD: 8,
NEAR_MISS_MAX: 35,
NEAR_MISS_DECAY: 0.3,
SCROLL_SPEED: 600,
FOV_BASE: 65,
FOV_BOOST: 90,
PARTICLE_COUNT: 600,
TRAIL_COUNT: 200,
};
// ── let: audioCtx ──
let audioCtx;
// ── let: masterGain ──
let masterGain;
// ── let: engineOsc ──
let engineOsc;
// ── let: engineGain ──
let engineGain;
// ── function: initAudio ──
function initAudio() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.3;
masterGain.connect(audioCtx.destination);
engineOsc = audioCtx.createOscillator();
engineGain = audioCtx.createGain();
engineOsc.type = 'sawtooth';
engineOsc.frequency.value = 55;
engineGain.gain.value = 0.08;
const engineFilter = audioCtx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 300;
engineOsc.connect(engineFilter);
engineFilter.connect(engineGain);
engineGain.connect(masterGain);
engineOsc.start();
}
// ── function: updateEngineSound ──
function updateEngineSound(speed, boostActive) {
if (!engineOsc) return;
const freq = 55 + (speed / CFG.MAX_SPEED) * 120;
engineOsc.frequency.setTargetAtTime(freq, audioCtx.currentTime, 0.1);
const vol = 0.05 + (speed / CFG.MAX_SPEED) * 0.08 + (boostActive ? 0.06 : 0);
engineGain.gain.setTargetAtTime(vol, audioCtx.currentTime, 0.1);
}
// ── function: playCollectSound ──
function playCollectSound() {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(880, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1760, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(audioCtx.currentTime + 0.3);
}
// ── function: playNearMissSound ──
function playNearMissSound(multiplier) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
const baseFreq = 400 + multiplier * 80;
osc.frequency.setValueAtTime(baseFreq, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(baseFreq * 1.5, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
}
// ── function: playBoostSound ──
function playBoostSound() {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.3);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.4);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(audioCtx.currentTime + 0.4);
}
// ── function: playCrashSound ──
function playCrashSound() {
const bufferSize = audioCtx.sampleRate * 0.5;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.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 = audioCtx.createBufferSource();
source.buffer = buffer;
const gain = audioCtx.createGain();
gain.gain.value = 0.3;
source.connect(gain);
gain.connect(masterGain);
source.start();
}
// ── const: canyonChunks ──
const canyonChunks = [];
// ── const: activeObstacles ──
const activeObstacles = [];
// ── const: activeCollectibles ──
const activeCollectibles = [];
// ── let: chunkSeed ──
let chunkSeed = 0;
// ── function: seededRandom ──
function seededRandom(seed) {
const x = Math.sin(seed) * 43758.5453123;
return x - Math.floor(x);
}
// ── function: initChunks ──
function initChunks(playerZ) {
for (let i = 0; i < CFG.CHUNK_COUNT; i++) {
const z = playerZ - CFG.CHUNK_SIZE * CFG.CHUNK_COUNT / 2 + i * CFG.CHUNK_SIZE;
canyonChunks.push(generateCanyonChunk(z));
}
}
// ── const: player ──
const player = {
x: 0, y: CFG.PLAYER_START_Y, z: 0,
vx: 0, vy: 0, vz: 0,
roll: 0, pitch: 0,
speed: CFG.BASE_SPEED,
energy: CFG.ENERGY_MAX,
boost: 0,
boostActive: false,
boostTimer: 0,
multiplier: 1,
nearMissTimer: 0,
score: 0,
maxSpeed: 0,
alive: true,
};
// ── const: playerMesh ──
const playerMesh = new THREE.Group();
// ── function: updatePlayerPhysics ──
function updatePlayerPhysics(dt) {
if (!player.alive) return;
const targetSpeed = player.boostActive ? CFG.BOOST_SPEED : CFG.BASE_SPEED + (player.speed - CFG.BASE_SPEED) * 0.95;
player.speed += (targetSpeed - player.speed) * dt * 2;
player.speed = Math.max(CFG.BASE_SPEED, Math.min(CFG.MAX_SPEED, player.speed));
if (player.speed > player.maxSpeed) player.maxSpeed = player.speed;
// Energy decay
player.energy -= CFG.ENERGY_DECAY * dt;
if (player.energy <= 0) {
player.energy = 0;
player.alive = false;
return;
}
// Boost logic
if (keys['Space'] && player.boost >= 50 && !player.boostActive) {
player.boostActive = true;
player.boostTimer = CFG.BOOST_DURATION;
playBoostSound();
}
if (player.boostActive) {
player.boost -= CFG.BOOST_DRAIN * dt;
player.boostTimer -= dt;
if (player.boostTimer <= 0 || player.boost <= 0) {
player.boostActive = false;
player.boost = Math.max(0, player.boost);
}
} else {
player.boost = Math.min(CFG.BOOST_MAX, player.boost + CFG.BOOST_REGEN * dt);
}
// Steering input with flight-based feel
const steerForce = 200;
const pitchForce = 100;
let steerX = 0;
let steerY = 0;
if (keys['KeyA'] || keys['ArrowLeft']) steerX = -1;
if (keys['KeyD'] || keys['ArrowRight']) steerX = 1;
if (keys['KeyW'] || keys['ArrowUp']) steerY = 1;
if (keys['KeyS'] || keys['ArrowDown']) steerY = -1;
// Acceleration (weighty feel)
player.vx += steerX * steerForce * dt;
player.vy += steerY * pitchForce * dt;
// Stronger damping for weighty feel
const damping = 1 - 2.5 * dt;
player.vx *= damping;
player.vy *= damping;
// Clamp max velocity
const maxVX = 60;
const maxVY = 40;
player.vx = Math.max(-maxVX, Math.min(maxVX, player.vx));
player.vy = Math.max(-maxVY, Math.min(maxVY, player.vy));
// Apply velocity
player.x += player.vx * dt;
player.y += player.vy * dt;
// Bank (roll) based on steering - more dramatic
const targetRoll = steerX * 0.7;
player.roll += (targetRoll - player.roll) * dt * 6;
// Pitch based on vertical input
const targetPitch = steerY * 0.35;
player.pitch += (targetPitch - player.pitch) * dt * 6;
// Clamp position to canyon bounds
const canyonWidth = CFG.CANYON_WIDTH * (1 - Math.min(player.z / 8000, 1) * 0.5);
const canyonHeight = CFG.CANYON_HEIGHT * (1 - Math.min(player.z / 8000, 1) * 0.35);
const margin = 6;
if (player.x < -canyonWidth / 2 + margin) {
player.x = -canyonWidth / 2 + margin;
player.vx *= -0.3;
}
if (player.x > canyonWidth / 2 - margin) {
player.x = canyonWidth / 2 - margin;
player.vx *= -0.3;
}
if (player.y < -canyonHeight / 2 + margin) {
player.y = -canyonHeight / 2 + margin;
player.vy *= -0.3;
}
if (player.y > canyonHeight / 2 - margin) {
player.y = canyonHeight / 2 - margin;
player.vy *= -0.3;
}
// Move forward
player.z += player.speed * dt;
// Update mesh
playerMesh.position.set(player.x, player.y, player.z);
playerMesh.rotation.z = player.roll;
playerMesh.rotation.x = player.pitch;
// Engine glow pulse
const engines = playerMesh.children.filter(c => c.name === 'engineGlow');
for (const glow of engines) {
const pulse = 0.6 + Math.sin(Date.now() * 0.01) * 0.2 + (player.boostActive ? 0.4 : 0);
glow.scale.set(pulse, pulse, 1 + player.boostActive * 3);
glow.material.color.setHex(player.boostActive ? 0xff3300 : 0xff6600);
}
const eLight = playerMesh.getObjectByName('engineLight');
if (eLight) {
eLight.intensity = 1.5 + (player.speed / CFG.MAX_SPEED) * 2 + (player.boostActive ? 4 : 0);
}
// Update engine sound
updateEngineSound(player.speed, player.boostActive);
}
// ── function: checkCollectibles ──
function checkCollectibles() {
if (!player.alive) return;
for (const chunk of canyonChunks) {
for (const col of chunk.collectibles) {
if (col.collected) continue;
const mesh = col.mesh;
const worldPos = new THREE.Vector3();
mesh.getWorldPosition(worldPos);
const dx = player.x - worldPos.x;
const dy = player.y - worldPos.y;
const dz = player.z - worldPos.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < col.radius + 3) {
col.collected = true;
player.energy = Math.min(CFG.ENERGY_MAX, player.energy + CFG.ENERGY_RECHARGE);
player.score += 50 * Math.max(1, Math.floor(player.multiplier));
player.boost = Math.min(CFG.BOOST_MAX, player.boost + 20);
player.multiplier = Math.min(10, player.multiplier + 0.3);
cameraShake.intensity = 1.5;
playCollectSound();
// Remove from scene
chunk.meshes = chunk.meshes.filter(m => m !== mesh);
scene.remove(mesh);
}
}
}
}
// ── function: checkCollisions ──
function checkCollisions() {
if (!player.alive) return;
const playerRadius = CFG.PLAYER_SIZE * 0.5;
for (const chunk of canyonChunks) {
for (const obs of chunk.obstacles) {
const mesh = obs.mesh;
const worldPos = new THREE.Vector3();
mesh.getWorldPosition(worldPos);
const dx = Math.abs(player.x - worldPos.x);
const dy = Math.abs(player.y - worldPos.y);
const dz = Math.abs(player.z - worldPos.z);
// Only check obstacles nearby
if (dz > obs.radius + 10) continue;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < obs.radius + playerRadius) {
player.alive = false;
playCrashSound();
cameraShake.intensity = 8;
return;
}
}
}
}
// ── const: particleSystem ──
const particleSystem = {
positions: null,
colors: null,
velocities: null,
sizes: null,
ages: null,
maxAge: null,
count: 0,
geometry: null,
material: null,
points: null,
};
// ── function: initParticles ──
function initParticles() {
const count = CFG.PARTICLE_COUNT;
particleSystem.count = count;
particleSystem.positions = new Float32Array(count * 3);
particleSystem.colors = new Float32Array(count * 3);
particleSystem.velocities = new Float32Array(count * 3);
particleSystem.sizes = new Float32Array(count);
particleSystem.ages = new Float32Array(count);
particleSystem.maxAge = new Float32Array(count);
particleSystem.geometry = new THREE.BufferGeometry();
particleSystem.geometry.setAttribute('position', new THREE.BufferAttribute(particleSystem.positions, 3));
particleSystem.geometry.setAttribute('color', new THREE.BufferAttribute(particleSystem.colors, 3));
particleSystem.geometry.setAttribute('size', new THREE.BufferAttribute(particleSystem.sizes, 1));
particleSystem.material = new THREE.ShaderMaterial({
uniforms: {},
vertexShader: `
attribute float size;
attribute vec3 color;
varying vec3 vColor;
void main() {
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (200.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying vec3 vColor;
void main() {
float d = length(gl_PointCoord - vec2(0.5));
if (d > 0.5) discard;
float alpha = 1.0 - smoothstep(0.0, 0.5, d);
gl_FragColor = vec4(vColor, alpha * 0.7);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
particleSystem.points = new THREE.Points(particleSystem.geometry, particleSystem.material);
}
// ── function: spawnParticle ──
function spawnParticle(x, y, z, vx, vy, vz, r, g, b, size, age) {
for (let i = 0; i < particleSystem.count; i++) {
if (particleSystem.ages[i] < particleSystem.maxAge[i]) {
particleSystem.positions[i * 3] = x;
particleSystem.positions[i * 3 + 1] = y;
particleSystem.positions[i * 3 + 2] = z;
particleSystem.velocities[i * 3] = vx;
particleSystem.velocities[i * 3 + 1] = vy;
particleSystem.velocities[i * 3 + 2] = vz;
particleSystem.colors[i * 3] = r;
particleSystem.colors[i * 3 + 1] = g;
particleSystem.colors[i * 3 + 2] = b;
particleSystem.sizes[i] = size;
particleSystem.ages[i] = 0;
particleSystem.maxAge[i] = age;
return;
}
}
}
// ── function: updateParticles ──
function updateParticles(dt) {
for (let i = 0; i < particleSystem.count; i++) {
particleSystem.ages[i] += dt;
if (particleSystem.ages[i] >= particleSystem.maxAge[i]) {
particleSystem.ages[i] = particleSystem.maxAge[i] + 1;
continue;
}
particleSystem.positions[i * 3] += particleSystem.velocities[i * 3] * dt;
particleSystem.positions[i * 3 + 1] += particleSystem.velocities[i * 3 + 1] * dt;
particleSystem.positions[i * 3 + 2] += particleSystem.velocities[i * 3 + 2] * dt;
const lifeRatio = particleSystem.ages[i] / particleSystem.maxAge[i];
particleSystem.sizes[i] *= (1 - lifeRatio * 0.3);
}
particleSystem.geometry.attributes.position.needsUpdate = true;
particleSystem.geometry.attributes.color.needsUpdate = true;
particleSystem.geometry.attributes.size.needsUpdate = true;
}
// ── function: emitTrail ──
function emitTrail(dt) {
if (!player.alive) return;
const trailCount = player.boostActive ? 8 : 4;
for (let i = 0; i < trailCount; i++) {
const offset = (Math.random() - 0.5) * 1.5;
const p = spawnParticle(
player.x + offset,
player.y + (Math.random() - 0.5) * 0.5,
player.z + 3,
-player.speed * 0.3 + (Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
player.boostActive ? 1 : 0.8,
player.boostActive ? 0.4 : 0.6,
0,
player.boostActive ? 3 : 2,
player.boostActive ? 1.5 : 0.8
);
if (p) {
p.color.setHex(player.boostActive ? 0xff4400 : 0xff6600);
}
}
}
// ── function: emitDebris ──
function emitDebris(dt) {
if (!player.alive) return;
const debrisCount = Math.floor(2 + player.speed / CFG.BASE_SPEED * 3);
for (let i = 0; i < debrisCount; i++) {
const angle = Math.random() * Math.PI * 2;
const radius = 30 + Math.random() * 40;
spawnParticle(
player.x + Math.cos(angle) * radius,
player.y + (Math.random() - 0.5) * 20,
player.z + 50 + Math.random() * 30,
-player.speed * 0.9 + (Math.random() - 0.5) * 30,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 20,
0.3 + Math.random() * 0.2,
0.3 + Math.random() * 0.1,
0.35 + Math.random() * 0.1,
0.5 + Math.random() * 1.5,
0.5 + Math.random() * 1.0
);
}
}
// ── let: scene ──
let scene;
// ── let: camera ──
let camera;
// ── let: renderer ──
let renderer;
// ── let: composer ──
let composer;
// ── let: sunLight ──
let sunLight;
// ── let: ambientLight ──
let ambientLight;
// ── let: fillLight ──
let fillLight;
// ── let: clock ──
let clock;
// ── let: keys ──
let keys = {};
// ── let: gameState ──
let gameState = 'start';
// ── let: lastTime ──
let lastTime = 0;
// ── let: scoreDisplay ──
let scoreDisplay;
// ── let: multiplierDisplay ──
let multiplierDisplay;
// ── let: energyBar ──
let energyBar;
// ── let: boostBar ──
let boostBar;
// ── let: speedBar ──
let speedBar;
// ── let: gameOverScreen ──
let gameOverScreen;
// ── let: startScreen ──
let startScreen;
// ── let: nearMissFlash ──
let nearMissFlash;
// ── let: cameraShake ──
let cameraShake = { intensity: 0 };
// ── let: totalScore ──
let totalScore = 0;
// ── let: distanceScore ──
let distanceScore = 0;
// ── function: createMotionBlurPass ──
function createMotionBlurPass(intensity) {
const copyShader = new THREE.ShaderPass(THREE.CopyShader);
copyShader.uniforms['tDiffuse'].value = null;
copyShader.uniforms['opacity'].value = 1.0;
const motionBlurShader = {
uniforms: {
tDiffuse: { value: null },
tPrevious: { value: null },
intensity: { value: intensity },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform sampler2D tPrevious;
uniform float intensity;
varying vec2 vUv;
void main() {
vec4 current = texture2D(tDiffuse, vUv);
vec4 previous = texture2D(tPrevious, vUv);
gl_FragColor = mix(current, previous, intensity);
}
`,
};
const motionBlurPass = new THREE.ShaderPass(motionBlurShader);
motionBlurPass.renderToScreen = true;
return { pass: motionBlurPass, uniforms: motionBlurPass.uniforms };
}
// ── function: generateCanyonChunk ──
function generateCanyonChunk(zBase) {
const chunk = {
zBase: zBase,
zEnd: zBase + CFG.CHUNK_SIZE,
meshes: [],
obstacles: [],
collectibles: [],
};
const difficulty = Math.min(zBase / 8000, 1);
const canyonWidth = CFG.CANYON_WIDTH * (1 - difficulty * 0.5);
const canyonHeight = CFG.CANYON_HEIGHT * (1 - difficulty * 0.35);
const wallSegments = 20;
const wallVerts = {};
// Generate wall vertex strips
for (let side = -1; side <= 1; side += 2) {
wallVerts[side === -1 ? 'left' : 'right'] = [];
for (let i = 0; i <= wallSegments; i++) {
const t = i / wallSegments;
const z = zBase + t * CFG.CHUNK_SIZE;
const noise1 = seededRandom(zBase * 0.01 + i * (side === -1 ? 1.3 : 2.7));
const noise2 = seededRandom(zBase * 0.01 + i * (side === -1 ? 3.1 : 4.3));
const offset = (noise1 - 0.5) * 40 * (1 - difficulty * 0.6);
const topNoise = seededRandom(zBase * 0.02 + i * 5.1);
const bottomNoise = seededRandom(zBase * 0.02 + i * 6.3);
const baseX = side * canyonWidth / 2;
const wallX = baseX + offset;
const topY = canyonHeight / 2 + (topNoise - 0.5) * 25;
const bottomY = -canyonHeight / 2 + (bottomNoise - 0.5) * 20;
wallVerts[side === -1 ? 'left' : 'right'].push({ x: wallX, yTop: topY, yBot: bottomY, z });
}
}
// Create wall faces with triangles
for (let side = -1; side <= 1; side += 2) {
const name = side === -1 ? 'left' : 'right';
const verts = wallVerts[name];
for (let i = 0; i < wallSegments; i++) {
const a = verts[i], b = verts[i + 1];
// Wall face (vertical)
const wallGeo = new THREE.BufferGeometry();
const wVerts = new Float32Array([
a.x, a.yBot, a.z, b.x, b.yBot, b.z, b.x, b.yTop, b.z,
a.x, a.yBot, a.z, b.x, b.yTop, b.z, a.x, a.yTop, a.z,
]);
wallGeo.setAttribute('position', new THREE.BufferAttribute(wVerts, 3));
wallGeo.computeVertexNormals();
const wMat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.06 + seededRandom(zBase + i * 10) * 0.05, 0.25 + difficulty * 0.2, 0.12 + seededRandom(zBase + i * 11) * 0.08),
side: THREE.DoubleSide,
flatShading: true,
roughness: 0.95,
metalness: 0.05,
});
const wall = new THREE.Mesh(wallGeo, wMat);
chunk.meshes.push(wall);
// Floor
const floorGeo = new THREE.BufferGeometry();
const fVerts = new Float32Array([
a.x, a.yBot, a.z, b.x, b.yBot, b.z, side * canyonWidth, a.yBot - 5, a.z,
b.x, b.yBot, b.z, side * canyonWidth, b.yBot - 5, b.z, side * canyonWidth, b.yBot - 5, b.z,
]);
floorGeo.setAttribute('position', new THREE.BufferAttribute(fVerts, 3));
floorGeo.computeVertexNormals();
const fMat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.07, 0.15, 0.08 + seededRandom(zBase + i * 12) * 0.05),
side: THREE.DoubleSide,
flatShading: true,
roughness: 1.0,
metalness: 0.0,
});
const floor = new THREE.Mesh(floorGeo, fMat);
chunk.meshes.push(floor);
// Ceiling
const ceilGeo = new THREE.BufferGeometry();
const cVerts = new Float32Array([
a.x, a.yTop, a.z, side * canyonWidth, a.yTop + 5, a.z, b.x, b.yTop, b.z,
b.x, b.yTop, b.z, side * canyonWidth, b.yTop + 5, b.z, side * canyonWidth, b.yTop + 5, b.z,
]);
ceilGeo.setAttribute('position', new THREE.BufferAttribute(cVerts, 3));
ceilGeo.computeVertexNormals();
const cMat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.72, 0.1, 0.06 + seededRandom(zBase + i * 13) * 0.04),
side: THREE.DoubleSide,
flatShading: true,
roughness: 1.0,
metalness: 0.0,
});
const ceiling = new THREE.Mesh(ceilGeo, cMat);
chunk.meshes.push(ceiling);
}
}
// Place obstacles with more variety
const obstacleCount = Math.floor(2 + difficulty * 5);
for (let i = 0; i < obstacleCount; i++) {
const oz = zBase + CFG.CHUNK_SIZE * 0.15 + seededRandom(zBase + i * 7) * CFG.CHUNK_SIZE * 0.7;
const oType = Math.floor(seededRandom(zBase + i * 13) * 4);
let mesh;
if (oType === 0) {
// Monolith - tall pillar
const w = 4 + seededRandom(zBase + i * 17) * 8;
const h = 12 + seededRandom(zBase + i * 19) * 20;
const d = 3 + seededRandom(zBase + i * 23) * 6;
const geo = new THREE.BoxGeometry(w, h, d);
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.68 + seededRandom(zBase + i * 29) * 0.15, 0.12, 0.18 + seededRandom(zBase + i * 31) * 0.1),
flatShading: true,
roughness: 0.7,
metalness: 0.3,
});
mesh = new THREE.Mesh(geo, mat);
mesh.position.set(
(seededRandom(zBase + i * 37) - 0.5) * canyonWidth * 0.55,
(seededRandom(zBase + i * 41) - 0.5) * canyonHeight * 0.25,
oz
);
mesh.rotation.y = seededRandom(zBase + i * 43) * Math.PI;
} else if (oType === 1) {
// Floating crystal
const s = 3 + seededRandom(zBase + i * 47) * 5;
const geo = new THREE.OctahedronGeometry(s);
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.75, 0.4, 0.25 + seededRandom(zBase + i * 53) * 0.1),
flatShading: true,
roughness: 0.4,
metalness: 0.6,
});
mesh = new THREE.Mesh(geo, mat);
mesh.position.set(
(seededRandom(zBase + i * 59) - 0.5) * canyonWidth * 0.5,
(seededRandom(zBase + i * 61) - 0.5) * canyonHeight * 0.3,
oz
);
mesh.rotation.set(seededRandom(zBase + i * 67) * Math.PI, seededRandom(zBase + i * 71) * Math.PI, 0);
mesh.userData.rotSpeed = { x: seededRandom(zBase + i * 73) * 0.5, y: seededRandom(zBase + i * 77) * 0.5 };
} else if (oType === 2) {
// Narrow gate - two pillars with a gap
const gapWidth = 8 + seededRandom(zBase + i * 83) * 6;
const pillarW = 3 + seededRandom(zBase + i * 89) * 3;
const pillarH = 20 + seededRandom(zBase + i * 91) * 15;
const geo = new THREE.BoxGeometry(gapWidth, pillarH, 4);
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.08, 0.2, 0.15 + seededRandom(zBase + i * 97) * 0.08),
flatShading: true,
roughness: 0.9,
metalness: 0.1,
});
mesh = new THREE.Mesh(geo, mat);
mesh.position.set(
(seededRandom(zBase + i * 101) - 0.5) * canyonWidth * 0.3,
0,
oz
);
mesh.userData.isGate = true;
mesh.userData.gapWidth = gapWidth;
} else {
// Large boulder
const s = 4 + seededRandom(zBase + i * 107) * 7;
const geo = new THREE.DodecahedronGeometry(s, 0);
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color().setHSL(0.07, 0.25, 0.14 + seededRandom(zBase + i * 113) * 0.08),
flatShading: true,
roughness: 0.95,
metalness: 0.05,
});
mesh = new THREE.Mesh(geo, mat);
mesh.position.set(
(seededRandom(zBase + i * 119) - 0.5) * canyonWidth * 0.45,
(seededRandom(zBase + i * 127) - 0.5) * canyonHeight * 0.2,
oz
);
}
chunk.meshes.push(mesh);
const box = new THREE.Box3().setFromObject(mesh);
const size = box.getSize(new THREE.Vector3());
const radius = Math.max(size.x, size.y, size.z) / 2;
chunk.obstacles.push({
mesh: mesh,
box: box,
radius: radius,
});
}
// Place collectibles
const collectibleCount = Math.floor(2 + seededRandom(zBase * 0.5) * 3);
for (let i = 0; i < collectibleCount; i++) {
const cz = zBase + CFG.CHUNK_SIZE * 0.15 + seededRandom(zBase * 0.3 + i * 99) * CFG.CHUNK_SIZE * 0.7;
const geo = new THREE.OctahedronGeometry(1.0, 0);
const mat = new THREE.MeshBasicMaterial({ color: 0x00ffcc });
const collectible = new THREE.Mesh(geo, mat);
// Outer glow
const glowGeo = new THREE.OctahedronGeometry(1.8, 0);
const glowMat = new THREE.MeshBasicMaterial({
color: 0x00ffcc,
transparent: true,
opacity: 0.15,
});
const glow = new THREE.Mesh(glowGeo, glowMat);
collectible.add(glow);
// Inner bright core
const coreGeo = new THREE.SphereGeometry(0.3, 6, 6);
const coreMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const core = new THREE.Mesh(coreGeo, coreMat);
collectible.add(core);
// Light
const light = new THREE.PointLight(0x00ffcc, 2, 10);
collectible.add(light);
collectible.position.set(
(seededRandom(zBase * 0.2 + i * 101) - 0.5) * canyonWidth * 0.5,
(seededRandom(zBase * 0.2 + i * 103) - 0.5) * canyonHeight * 0.3,
cz
);
chunk.meshes.push(collectible);
const cBox = new THREE.Box3().setFromObject(collectible);
chunk.collectibles.push({
mesh: collectible,
box: cBox,
radius: 2,
collected: false,
baseY: collectible.position.y,
phase: seededRandom(zBase + i * 200) * Math.PI * 2,
});
}
return chunk;
}
// ── function: createPlayerCraft ──
function createPlayerCraft() {
playerMesh.clear();
// Main body - sleek delta wing shape
const bodyVerts = new Float32Array([
// Nose
0, 0, 5, -2, 0.3, 0, 2, 0.3, 0,
// Nose bottom
0, 0, 5, 2, 0.3, 0, 0, -0.3, 0,
// Left wing
-2, 0.3, 0, -8, 0.1, -3, -2, -0.2, -1,
// Right wing
2, 0.3, 0, 2, -0.2, -1, 8, 0.1, -3,
// Wing tips left
-8, 0.1, -3, -8, 0.3, -2, -6, 0.1, -4,
// Wing tips right
8, 0.1, -3, 6, 0.1, -4, 8, 0.3, -2,
// Tail
-2, -0.2, -1, 0, -0.3, 0, 2, -0.2, -1,
// Cockpit
0, 0.5, 2, -1.5, 0.3, 0, 1.5, 0.3, 0,
// Cockpit top
0, 0.5, 2, 1.5, 0.3, 0, 0, -0.3, 0,
]);
const bodyGeo = new THREE.BufferGeometry();
bodyGeo.setAttribute('position', new THREE.BufferAttribute(bodyVerts, 3));
bodyGeo.computeVertexNormals();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xd0d0d0, flatShading: true, roughness: 0.2, metalness: 0.9 });
const body = new THREE.Mesh(bodyGeo, bodyMat);
playerMesh.add(body);
// Cockpit glass
const cockpitGeo = new THREE.BufferGeometry();
const cockpitVerts = new Float32Array([
0, 0.8, 3, -1, 0.5, 1, 1, 0.5, 1,
0, 0.8, 3, 1, 0.5, 1, 0, 0.3, 1,
]);
cockpitGeo.setAttribute('position', new THREE.BufferAttribute(cockpitVerts, 3));
cockpitGeo.computeVertexNormals();
const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x44aaff, flatShading: true, roughness: 0.1, metalness: 0.5, transparent: true, opacity: 0.7 });
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
playerMesh.add(cockpit);
// Engine glow left
const engineGeo = new THREE.ConeGeometry(0.6, 2.5, 6);
engineGeo.rotateX(Math.PI / 2);
const engineMat = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.9 });
const engineL = new THREE.Mesh(engineGeo, engineMat);
engineL.position.set(-1, 0, 2);
engineL.name = 'engineGlow';
playerMesh.add(engineL);
const engineR = new THREE.Mesh(engineGeo.clone(), engineMat.clone());
engineR.position.set(1, 0, 2);
engineR.name = 'engineGlow';
playerMesh.add(engineR);
// Engine lights
const eLight = new THREE.PointLight(0xff6600, 3, 20);
eLight.position.set(0, 0, 3);
eLight.name = 'engineLight';
playerMesh.add(eLight);
// Wing tip lights
const tipGeo = new THREE.SphereGeometry(0.15, 6, 6);
const tipL = new THREE.Mesh(tipGeo, new THREE.MeshBasicMaterial({ color: 0xff0000 }));
tipL.position.set(-8, 0.1, -3);
playerMesh.add(tipL);
const tipR = new THREE.Mesh(tipGeo.clone(), new THREE.MeshBasicMaterial({ color: 0x00ff00 }));
tipR.position.set(8, 0.1, -3);
playerMesh.add(tipR);
}
// ── function: checkNearMisses ──
function checkNearMisses(dt) {
if (!player.alive) return;
let closestDist = Infinity;
for (const chunk of canyonChunks) {
for (const obs of chunk.obstacles) {
const mesh = obs.mesh;
const worldPos = new THREE.Vector3();
mesh.getWorldPosition(worldPos);
const dx = Math.abs(player.x - worldPos.x);
const dz = Math.abs(player.z - worldPos.z);
if (dz < obs.radius + 15) {
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist < obs.radius + CFG.NEAR_MISS_THRESHOLD && dist > obs.radius) {
if (dist < closestDist) closestDist = dist;
}
}
}
// Check canyon walls
for (let i = 0; i < chunk.meshes.length; i += 3) {
const wall = chunk.meshes[i];
if (!wall) continue;
const wallPos = new THREE.Vector3();
wall.getWorldPosition(wallPos);
const dz = Math.abs(player.z - wallPos.z);
if (dz > 30) continue;
const dx = Math.abs(player.x - wallPos.x);
const canyonCenterX = (wallPos.x + (wallPos.x > 0 ? -CFG.CANYON_WIDTH : CFG.CANYON_WIDTH)) / 2;
const wallDist = Math.abs(player.x - wallPos.x);
if (wallDist < CFG.NEAR_MISS_THRESHOLD + 10 && wallDist > 5) {
if (wallDist < closestDist) closestDist = wallDist;
}
}
}
if (closestDist < Infinity) {
const proximity = 1 - (closestDist / (CFG.NEAR_MISS_THRESHOLD + 10));
const bonus = Math.floor(proximity * CFG.NEAR_MISS_MAX * player.multiplier);
if (bonus > 0) {
player.score += bonus;
player.multiplier = Math.min(10, player.multiplier + proximity * 0.5);
player.nearMissTimer = 1.0;
player.boost = Math.min(CFG.BOOST_MAX, player.boost + proximity * 15);
player.energy = Math.min(CFG.ENERGY_MAX, player.energy + proximity * 5);
cameraShake.intensity = proximity * 2;
playNearMissSound(proximity);
}
}
// Decay multiplier slowly
if (player.nearMissTimer > 0) {
player.nearMissTimer -= dt * CFG.NEAR_MISS_DECAY;
if (player.nearMissTimer <= 0) {
player.nearMissTimer = 0;
player.multiplier = Math.max(1, player.multiplier - 0.1);
}
}
}
// ── function: createSkyDome ──
function createSkyDome() {
// Distant mountain silhouettes for depth
const mountainGeo = new THREE.BufferGeometry();
const vertices = [];
const segments = 100;
const radius = 400;
for (let i = 0; i <= segments; i++) {
const angle = (i / segments) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const height = 30 + seededRandom(Math.floor(angle * 10) + 100) * 60;
vertices.push(x, height, z);
}
mountainGeo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
mountainGeo.computeVertexNormals();
const mountainMat = new THREE.MeshBasicMaterial({
color: 0x1a0520,
side: THREE.BackSide,
fog: false,
});
const mountain = new THREE.Mesh(mountainGeo, mountainMat);
mountain.position.y = 0;
scene.add(mountain);
// Second ring of mountains
const m2Geo = new THREE.BufferGeometry();
const m2Verts = [];
for (let i = 0; i <= segments; i++) {
const angle = (i / segments) * Math.PI * 2;
const x = Math.cos(angle) * (radius - 50);
const z = Math.sin(angle) * (radius - 50);
const height = 20 + seededRandom(Math.floor(angle * 10) + 200) * 40;
m2Verts.push(x, height, z);
}
m2Geo.setAttribute('position', new THREE.Float32BufferAttribute(m2Verts, 3));
m2Geo.computeVertexNormals();
const m2Mat = new THREE.MeshBasicMaterial({
color: 0x2a0a30,
side: THREE.BackSide,
fog: false,
});
const m2 = new THREE.Mesh(m2Geo, m2Mat);
m2.position.y = 0;
scene.add(m2);
// Sun glow
const sunGeo = new THREE.CircleGeometry(40, 32);
const sunMat = new THREE.MeshBasicMaterial({
color: 0xff6600,
transparent: true,
opacity: 0.6,
fog: false,
});
const sun = new THREE.Mesh(sunGeo, sunMat);
sun.position.set(100, 80, -350);
sun.lookAt(0, 0, 0);
scene.add(sun);
// Sun outer glow
const sunGlowGeo = new THREE.CircleGeometry(80, 32);
const sunGlowMat = new THREE.MeshBasicMaterial({
color: 0xff4400,
transparent: true,
opacity: 0.2,
fog: false,
});
const sunGlow = new THREE.Mesh(sunGlowGeo, sunGlowMat);
sunGlow.position.copy(sun.position);
sunGlow.lookAt(0, 0, 0);
scene.add(sunGlow);
}
// ── main ──
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0010);
scene.fog = new THREE.FogExp2(0x1a0520, 0.003);
camera = new THREE.PerspectiveCamera(CFG.FOV_BASE, window.innerWidth / window.innerHeight, 0.1, 1000);
const canvas = document.getElementById('gameCanvas');
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.3;
composer = new THREE.EffectComposer(renderer);
const renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);
const bloomPass = new THREE.UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, 0.4, 0.85
);
bloomPass.threshold = 0.15;
bloomPass.strength = 2.0;
bloomPass.radius = 0.8;
composer.addPass(bloomPass);
composer.bloomPass = bloomPass;
const rtA = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
});
const rtB = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
});
composer.motionBlurRTA = rtA;
composer.motionBlurRTB = rtB;
const motionBlurShader = {
uniforms: {
tDiffuse: { value: null },
tPrevious: { value: null },
intensity: { value: 0.3 },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform sampler2D tPrevious;
uniform float intensity;
varying vec2 vUv;
void main() {
vec4 current = texture2D(tDiffuse, vUv);
vec4 previous = texture2D(tPrevious, vUv);
gl_FragColor = mix(current, previous, intensity);
}
`,
};
const motionBlurPass = new THREE.ShaderPass(motionBlurShader);
motionBlurPass.renderToScreen = true;
composer.addPass(motionBlurPass);
composer.motionBlurPass = motionBlurPass;
ambientLight = new THREE.AmbientLight(0x331122, 0.35);
scene.add(ambientLight);
sunLight = new THREE.DirectionalLight(0xff8844, 1.8);
sunLight.position.set(80, 120, -300);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 600;
sunLight.shadow.camera.left = -150;
sunLight.shadow.camera.right = 150;
sunLight.shadow.camera.top = 150;
sunLight.shadow.camera.bottom = -150;
scene.add(sunLight);
fillLight = new THREE.DirectionalLight(0x4422aa, 0.4);
fillLight.position.set(-80, 80, 200);
scene.add(fillLight);
const rimLight = new THREE.DirectionalLight(0xff4400, 0.6);
rimLight.position.set(0, 30, -100);
scene.add(rimLight);
createSkyDome();
createPlayerCraft();
scene.add(playerMesh);
initParticles();
scene.add(particleSystem.points);
initChunks(0);
for (const chunk of canyonChunks) {
for (const mesh of chunk.meshes) {
scene.add(mesh);
}
}
scoreDisplay = document.getElementById('score');
multiplierDisplay = document.getElementById('multiplier');
energyBar = document.getElementById('energy-bar');
boostBar = document.getElementById('boost-bar');
speedBar = document.getElementById('speed-bar');
gameOverScreen = document.getElementById('game-over');
startScreen = document.getElementById('start-screen');
nearMissFlash = document.getElementById('near-miss-flash');
distanceValue = document.getElementById('distance-value');
window.addEventListener('keydown', (e) => {
keys[e.code] = true;
if (e.code === 'Space') e.preventDefault();
});
window.addEventListener('keyup', (e) => {
keys[e.code] = false;
});
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', restartGame);
window.addEventListener('resize', onResize);
lastTime = 0;
requestAnimationFrame(gameLoop);
}
function startGame() {
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
else initAudio();
startScreen.classList.add('hidden');
gameOverScreen.classList.add('hidden');
gameState = 'playing';
player.alive = true;
player.x = 0;
player.y = CFG.PLAYER_START_Y;
player.z = 0;
player.vx = 0;
player.vy = 0;
player.speed = CFG.BASE_SPEED;
player.energy = CFG.ENERGY_MAX;
player.boost = 0;
player.boostActive = false;
player.multiplier = 1;
player.nearMissTimer = 0;
player.score = 0;
player.maxSpeed = 0;
totalScore = 0;
distanceScore = 0;
cameraShake.intensity = 0;
for (const chunk of canyonChunks) {
for (const mesh of chunk.meshes) {
scene.remove(mesh);
}
}
canyonChunks.length = 0;
initChunks(0);
for (const chunk of canyonChunks) {
for (const mesh of chunk.meshes) {
scene.add(mesh);
}
}
}
function restartGame() {
gameOverScreen.classList.add('hidden');
startGame();
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
let distanceValue;
function updateHUD() {
if (!scoreDisplay) return;
const currentScore = Math.floor(player.score + distanceScore);
scoreDisplay.textContent = currentScore;
multiplierDisplay.textContent = `x${Math.max(1, player.multiplier.toFixed(1))}`;
if (distanceValue) {
distanceValue.textContent = `${Math.floor(player.z)}m`;
}
if (player.nearMissTimer > 0.5) {
multiplierDisplay.classList.add('pulse');
nearMissFlash.classList.add('active');
} else {
multiplierDisplay.classList.remove('pulse');
nearMissFlash.classList.remove('active');
}
const energyPct = (player.energy / CFG.ENERGY_MAX) * 100;
energyBar.style.width = `${energyPct}%`;
const energyContainer = document.getElementById('energy-container');
if (energyPct < 25) {
energyContainer.classList.add('low');
} else {
energyContainer.classList.remove('low');
}
boostBar.style.width = `${(player.boost / CFG.BOOST_MAX) * 100}%`;
speedBar.style.width = `${(player.speed / CFG.BOOST_SPEED) * 100}%`;
}
function animateCollectibles(time) {
for (const chunk of canyonChunks) {
for (const col of chunk.collectibles) {
if (col.collected) continue;
const mesh = col.mesh;
mesh.position.y = col.baseY + Math.sin(time * 0.003 + col.phase) * 1.5;
mesh.rotation.y = time * 0.002 + col.phase;
mesh.rotation.x = Math.sin(time * 0.001 + col.phase) * 0.2;
// Pulse glow
const glow = mesh.children[0];
if (glow) {
const pulse = 1 + Math.sin(time * 0.005 + col.phase) * 0.3;
glow.scale.set(pulse, pulse, pulse);
glow.material.opacity = 0.1 + Math.sin(time * 0.004 + col.phase) * 0.08;
}
}
}
}
function animateObstacles(time) {
for (const chunk of canyonChunks) {
for (const obs of chunk.obstacles) {
const mesh = obs.mesh;
if (mesh.userData.rotSpeed) {
mesh.rotation.x += mesh.userData.rotSpeed.x * 0.01;
mesh.rotation.y += mesh.userData.rotSpeed.y * 0.01;
}
}
}
}
function gameLoop(time) {
requestAnimationFrame(gameLoop);
const dt = Math.min((time - lastTime) / 1000, 0.05);
lastTime = time;
if (gameState === 'start') {
camera.position.set(0, 20, 50);
camera.lookAt(0, 0, -50);
camera.position.x += Math.sin(time * 0.0003) * 10;
renderer.render(scene, camera);
return;
}
if (gameState !== 'playing') {
renderer.render(scene, camera);
return;
}
updatePlayerPhysics(dt);
if (player.alive) {
checkNearMisses();
checkCollectibles();
checkCollisions();
distanceScore += player.speed * dt * 0.5 * player.multiplier;
emitTrail(dt);
emitDebris(dt);
updateParticles(dt);
animateCollectibles(time);
animateObstacles(time);
// Camera follow
const camOffset = new THREE.Vector3(
-player.vx * 0.15,
8 + player.vy * 0.1,
25 + (player.boostActive ? 8 : 0)
);
camera.position.set(
player.x + camOffset.x,
player.y + camOffset.y,
player.z + camOffset.z
);
// Dynamic FOV
const targetFov = CFG.FOV_BASE + (player.speed - CFG.BASE_SPEED) / (CFG.BOOST_SPEED - CFG.BASE_SPEED) * (CFG.FOV_BOOST - CFG.FOV_BASE);
camera.fov += (targetFov - camera.fov) * dt * 5;
camera.updateProjectionMatrix();
// Bloom intensity based on speed
const bloomIntensity = 1.5 + (player.speed / CFG.MAX_SPEED) * 1.5 + (player.boostActive ? 1.0 : 0);
composer.bloomPass.strength = bloomIntensity;
const lookTarget = new THREE.Vector3(
player.x + player.vx * 0.3,
player.y + player.vy * 0.3,
player.z - 50
);
camera.lookAt(lookTarget);
// Camera shake
if (cameraShake.intensity > 0) {
camera.position.x += (Math.random() - 0.5) * cameraShake.intensity;
camera.position.y += (Math.random() - 0.5) * cameraShake.intensity;
cameraShake.intensity *= (1 - 5 * dt);
if (cameraShake.intensity < 0.01) cameraShake.intensity = 0;
}
// Move sun light with player
sunLight.position.set(player.x + 80, 120, player.z - 300);
sunLight.target.position.set(player.x, player.y, player.z);
sunLight.target.updateMatrixWorld();
updateChunks();
updateHUD();
} else {
gameState = 'gameover';
const finalScore = Math.floor(player.score + distanceScore);
document.getElementById('final-score').textContent = `SCORE: ${finalScore}`;
document.getElementById('max-speed').textContent = `MAX SPEED: ${Math.floor(player.maxSpeed)}`;
gameOverScreen.classList.remove('hidden');
cameraShake.intensity = 5;
}
// Motion blur render pipeline
const rtA = composer.motionBlurRTA;
const rtB = composer.motionBlurRTB;
const mbPass = composer.motionBlurPass;
// Render scene to rtB
renderer.setRenderTarget(rtB);
renderer.render(scene, camera);
// Copy previous to rtA
const copyPass = new THREE.ShaderPass(THREE.CopyShader);
copyPass.renderToScreen = false;
copyPass.uniforms['tDiffuse'].value = rtA.texture;
copyPass.uniforms['opacity'].value = 1.0;
copyPass.setSize(window.innerWidth, window.innerHeight);
renderer.setRenderTarget(rtA);
renderer.render(copyPass, camera);
// Blend current + previous to screen
mbPass.uniforms['tDiffuse'].value = rtB.texture;
mbPass.uniforms['tPrevious'].value = rtA.texture;
const speedIntensity = 0.15 + (player.speed / CFG.MAX_SPEED) * 0.25 + (player.boostActive ? 0.15 : 0);
mbPass.uniforms['intensity'].value = Math.min(speedIntensity, 0.5);
mbPass.renderToScreen = true;
renderer.setRenderTarget(null);
renderer.render(scene, camera);
// Swap render targets
const tempRT = rtA;
composer.motionBlurRTA = rtB;
composer.motionBlurRTB = tempRT;
}
function updateChunks() {
for (let i = canyonChunks.length - 1; i >= 0; i--) {
const chunk = canyonChunks[i];
if (chunk.zEnd < player.z - 80) {
for (const mesh of chunk.meshes) {
scene.remove(mesh);
if (mesh.geometry) mesh.geometry.dispose();
}
canyonChunks.splice(i, 1);
}
}
const frontChunk = canyonChunks[canyonChunks.length - 1];
if (frontChunk && frontChunk.zEnd < player.z + CFG.CHUNK_SIZE * (CFG.CHUNK_COUNT - 1)) {
const newZ = frontChunk.zEnd + 1;
const newChunk = generateCanyonChunk(newZ);
canyonChunks.push(newChunk);
for (const mesh of newChunk.meshes) {
scene.add(mesh);
}
}
}
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"b6869931-89aa-4d7b-9f27-3b4261f229aa","tokensIn":1142908,"tokensOut":26373,"tokensTotal":1169281,"turns":39,"toolCalls":51,"failedToolCalls":9,"timestamp":"2026-07-20T22:10:33.776Z"} -->