Canyon Run
ornith 1.0 9b · full

Reviews & generation details
Reviews
Human review · Jul 17, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
The reviewer marked this result as broken. It remains in the archive.
- Visual design
- Unscored
- UX / feel
- Unscored
- Works
- Unscored
- Prompt spirit
- Unscored
- Delight
- Unscored
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 51,216 bytes · SHA-256 1669bdca2e10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CANYON RACER</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
canvas { display: block; }
#hud-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10;
}
#speedometer {
position: absolute; bottom: 30px; right: 40px;
width: 200px; height: 80px;
}
.speed-line {
position: absolute; bottom: 0; left: 15px;
width: 170px; height: 6px; background: rgba(255,255,255,0.15);
border-radius: 3px; overflow: hidden;
}
.speed-fill {
position: absolute; bottom: 0; left: 0;
width: 0%; height: 100%; background: linear-gradient(90deg, #ff8c42, #ffd700);
border-radius: 3px; transition: width 0.1s;
}
.speed-label {
position: absolute; top: -22px; left: 50%; transform: translateX(-50%);
color: rgba(255,200,100,0.7); font-size: 11px; letter-spacing: 3px;
}
#multiplier {
position: absolute; top: 40px; left: 50%; transform: translateX(-50%);
font-size: 64px; color: #fff; text-shadow: 0 0 20px rgba(255,100,0,0.8), 0 0 40px rgba(255,50,0,0.5);
font-weight: bold; letter-spacing: 6px; opacity: 0; transition: opacity 0.3s;
}
#energy-hud {
position: absolute; bottom: 30px; left: 40px;
width: 180px; height: 24px; background: rgba(0,0,0,0.5);
border-radius: 12px; border: 1px solid rgba(255,100,50,0.3); overflow: hidden;
}
.energy-fill {
position: absolute; top: 0; left: 0; height: 100%; width: 100%;
background: linear-gradient(90deg, rgba(255,80,40,0.8), rgba(255,160,40,0.9));
border-radius: 11px; transition: width 0.1s; box-shadow: 0 0 10px rgba(255,80,40,0.5);
}
.energy-label {
position: absolute; top: -22px; left: 50%; transform: translateX(-50%);
color: rgba(255,200,100,0.7); font-size: 11px; letter-spacing: 3px;
}
#game-over {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.85); display: flex; flex-direction: column;
align-items: center; justify-content: center; z-index: 20; opacity: 0;
pointer-events: none; transition: opacity 0.6s;
}
#game-over.visible { opacity: 1; pointer-events: auto; }
#go-title { color: #ffd700; font-size: 48px; margin-bottom: 20px; letter-spacing: 6px; text-shadow: 0 0 30px rgba(255,215,0,0.5); }
#go-stats { color: rgba(255,255,255,0.7); font-size: 18px; margin-bottom: 40px; text-align: center; line-height: 2; }
#restart-btn {
background: transparent; border: 2px solid #ffd700; color: #ffd700;
padding: 16px 40px; font-size: 18px; cursor: pointer; letter-spacing: 3px;
transition: all 0.3s; text-transform: uppercase;
}
#restart-btn:hover { background: rgba(255,215,0,0.15); box-shadow: 0 0 20px rgba(255,215,0,0.4); }
#start-screen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.9); display: flex; flex-direction: column;
align-items: center; justify-content: center; z-index: 20; opacity: 1; pointer-events: auto;
}
#start-title { color: #ffd700; font-size: 56px; margin-bottom: 20px; letter-spacing: 8px; text-shadow: 0 0 40px rgba(255,215,0,0.6); }
#start-subtitle { color: rgba(255,100,100,0.7); font-size: 18px; margin-bottom: 60px; letter-spacing: 4px; }
#start-btn { background: transparent; border: 2px solid #ffd700; color: #ffd700; padding: 16px 40px; font-size: 18px; cursor: pointer; letter-spacing: 3px; transition: all 0.3s; text-transform: uppercase; }
#start-btn:hover { background: rgba(255,215,0,0.15); box-shadow: 0 0 20px rgba(255,215,0,0.4); }
@keyframes pulse {
0%, 100% { opacity: 1; transform: translateX(-50%) scale(1); }
50% { opacity: 0.7; transform: translateX(-50%) scale(1.1); }
}
.multiplier-pulse { animation: pulse 0.4s ease-in-out infinite; color: #ff6b35 !important; }
</style>
</head>
<body>
<div id="hud-overlay">
<div id="multiplier"></div>
<div class="speed-label" id="speed-label">SPEED</div>
<div class="speed-line" id="speedometer"><div class="speed-fill" id="speed-fill"></div></div>
<div class="energy-label" id="energy-label">ENERGY</div>
<div id="energy-hud"><div class="energy-fill" id="energy-fill"></div></div>
</div>
<div id="game-over">
<div id="go-title">WASTED</div>
<div id="go-stats"></div>
<button id="restart-btn">Restart Run</button>
</div>
<div id="start-screen">
<div id="start-title">CANYON RACER</div>
<div id="start-subtitle">FLOW • BANK • SURVIVE</div>
<button id="start-btn">IGNITE ENGINE</button>
</div>
<script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>
<script>
/* ============================================================
CANYON RACER - High-Speed Procedural Canyon Game
Flight-based physics, procedural generation, bloom, audio
============================================================ */
// --- CONFIGURATION ---
const CFG = {
worldChunkSize: 60,
chunksAhead: 8,
chunksBehind: 4,
playerSpeedMax: 12,
playerAccel: 0.03,
playerFriction: 0.985,
playerTurnRate: 0.015,
playerBankStrength: 0.8,
energyDecayRate: 0.45,
energyRechargeRate: 2.5,
energyRechargePerOrb: 35,
multiplierBase: 1,
multiplierNearMissBonus: 3,
multiplierDecayRate: 0.85,
boostMaxCharge: 100,
boostChargeRate: 0.6,
boostConsumptionRate: 2.0,
boostSpeedMultiplier: 1.4,
cameraLerp: 0.08,
cameraVerticalOffset: 5.5,
cameraHorizontalOffset: 9,
groundY: -1,
skyColorTop: new THREE.Color(0x2a0a3e),
skyColorBottom: new THREE.Color(0xff6b35)
};
// --- AUDIO SYSTEM (Web Audio API) ---
class AudioSystem {
constructor() { this.ctx = null; this.masterGain = null; }
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.35;
this.masterGain.connect(this.ctx.destination);
}
if (this.ctx.state === 'suspended') this.ctx.resume();
}
engineHum(speed) {
if (!this.ctx) return;
const now = this.ctx.currentTime;
let osc = null, gain = null, filter = null;
try {
osc = this.ctx.createOscillator();
gain = this.ctx.createGain();
filter = this.ctx.createBiquadFilter();
osc.type = 'sawtooth';
osc.frequency.value = 40 + speed * 2.5;
filter.type = 'lowpass';
filter.frequency.value = 200 + speed * 30;
gain.gain.setValueAtTime(0.15, now);
osc.connect(filter).connect(gain).connect(this.masterGain);
osc.start(now);
} catch(e) {}
}
playPing() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
try {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, t);
osc.frequency.exponentialRampToValueAtTime(1200, t + 0.15);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
osc.connect(gain).connect(this.masterGain);
osc.start(t); osc.stop(t + 0.35);
} catch(e) {}
}
playWhoosh() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
try {
const bufferSize = this.ctx.sampleRate * 1.2;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (this.ctx.sampleRate * 0.3));
}
const src = this.ctx.createBufferSource();
src.buffer = buffer;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 1.2);
src.connect(gain).connect(this.masterGain);
src.start(t);
} catch(e) {}
}
playBoost() {
if (!this.ctx) return;
const now = this.ctx.currentTime;
try {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'square';
osc.frequency.value = 120 + Math.random() * 80;
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.connect(gain).connect(this.masterGain);
osc.start(now); osc.stop(now + 0.2);
}, i * 80);
}
} catch(e) {}
}
playCrash() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
try {
const bufferSize = this.ctx.sampleRate * 1.5;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const src = this.ctx.createBufferSource();
src.buffer = buffer;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.5, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 1);
src.connect(gain).connect(this.masterGain);
src.start(t);
} catch(e) {}
}
}
// --- PARTICLE SYSTEM ---
class ParticleSystem {
constructor() { this.particles = []; this.count = 80; }
addEngineTrail(pos, velocity, speedMult) {
for (let i = 0; i < Math.min(3, CFG.playerSpeedMax / 2 + speedMult * 2); i++) {
const offset = -velocity.length() * (i * 0.15);
this.particles.push({
pos: new THREE.Vector3(pos.x + (Math.random()-0.5)*1.5, pos.y + offset + (Math.random()-0.5)*2, pos.z),
vel: velocity.clone().multiplyScalar(-speedMult * 0.3).add(new THREE.Vector3((Math.random()-0.5)*0.5, -0.2, Math.random()*0.3)),
life: 1.0, maxLife: 0.8 + Math.random()*0.4,
size: 0.3 + Math.random() * 0.7,
color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1.0, 0.5 + Math.random()*0.3)
});
}
}
addDust(pos, velocity) {
for (let i = 0; i < 4; i++) {
this.particles.push({
pos: new THREE.Vector3(pos.x + (Math.random()-0.5)*3, pos.y + 1 - Math.random()*2, pos.z),
vel: velocity.clone().multiplyScalar(1 - 0.7*Math.random()).add(new THREE.Vector3((Math.random()-0.5)*0.8, -0.5-Math.random(), (Math.random()+1)*Math.random())),
life: 0.6 + Math.random()*0.6, maxLife: 1.2,
size: 0.4 + Math.random() * 1.2,
color: new THREE.Color().setHSL(0.05, 0.7 + Math.random()*0.3, 0.4 + Math.random()*0.2)
});
}
}
update(dt) {
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.life -= dt * 60;
if (p.life <= 0) continue;
// Move
p.pos.add(p.vel.clone().multiplyScalar(dt * 60));
// Age color
const ratio = p.life / p.maxLife;
p.color.lerp(new THREE.Color(0.2, 0.1, 0.3), (1 - ratio) * 0.5);
// Update geometry
if (p.mesh && p.mesh.geometry.attributes.position.needsUpdate) {
const pos = p.mesh.geometry.attributes.position;
for (let j = 0; j < pos.count; j++) {
pos.setXYZ(j, p.pos.x, p.pos.y + p.size * Math.sin(ratio * Math.PI), p.pos.z);
}
pos.needsUpdate = true;
}
if (p.mesh.material) {
const mat = p.mesh.material;
if (mat instanceof THREE.MeshBasicMaterial) mat.color.copy(p.color);
if (mat instanceof THREE.PointsMaterial) mat.color.copy(p.color);
}
}
// Cleanup dead particles
this.particles = this.particles.filter((_, i) => {
if (!this.particles[i]) return false;
const p = this.particles[i];
if (p.life <= 0 && p.mesh && p.parent) {
p.parent.remove(p.mesh);
return true;
}
return false;
});
}
clear() { this.particles.length = 0; }
}
// --- SCENE SETUP ---
function createScene() {
const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.8;
const scene = new THREE.Scene();
// Gradient sky
const skyGeo = new THREE.SphereGeometry(200, 32, 16);
const skyMat = new THREE.ShaderMaterial({
uniforms: { topColor: { value: CFG.skyColorTop }, bottomColor: { value: CFG.skyColorBottom } },
vertexShader: `varying vec3 vWorldPos; void main() {
vec4 wp = modelMatrix * vec4(position, 1.0);
vWorldPos = wp.xyz;
gl_Position = viewMatrix * wp; }`,
fragmentShader: `uniform vec3 topColor; uniform vec3 bottomColor;
varying vec3 vWorldPos;
void main() {
float h = clamp((vWorldPos.y + 50.0) / 120.0, 0.0, 1.0);
gl_FragColor = mix(vec4(bottomColor, 1), vec4(topColor, 1), h);
}`,
side: THREE.BackSide
});
scene.add(new THREE.Mesh(skyGeo, skyMat));
// Ground plane (faint)
const groundGeo = new THREE.PlaneGeometry(500, 200);
const groundMat = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 }, uSpeed: { value: CFG.playerSpeedMax } },
vertexShader: `varying vec2 vUv; varying float vYOffset; void main() {
vUv = uv;
vec4 pos = modelMatrix * vec4(position, 1.0);
vYOffset = sin(pos.x * uSpeed * 0.3 + pos.z * uSpeed * 0.2) * 0.5;
gl_Position = viewMatrix * pos; }`,
fragmentShader: `uniform float uTime; uniform vec3 topColor; uniform vec3 bottomColor;
varying vec2 vUv; varying float vYOffset;
void main() {
vec3 color = mix(bottomColor, topColor, 0.4);
float wave = sin(vUv.x * 8.0 + uTime) * 0.15 + cos(vUv.y * 6.0 + uTime * 0.7) * 0.1;
color += vec3(wave, wave*0.5, wave*2);
gl_FragColor = vec4(color, 0.8);
}`,
side: THREE.DoubleSide
});
scene.add(new THREE.Mesh(groundGeo, groundMat));
// Lighting - warm sunset feel
const ambientLight = new THREE.AmbientLight(0x3a1a5e, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffaa44, 2.5);
dirLight.position.set(-20, 50, -10);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048);
scene.add(dirLight);
const fillLight = new THREE.DirectionalLight(0x7733aa, 0.8);
fillLight.position.set(20, 15, -10);
scene.add(fillLight);
// Add renderer to DOM
document.body.appendChild(renderer.domElement);
// Camera
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 400);
return { scene, renderer, camera };
}
// --- CANON GEOMETRY SHARED MESHES ---
let wallMeshLeft, wallMeshRight, floorMesh;
function createCanyonWalls() {
// Left canyon wall (jagged polygon)
const leftGeo = new THREE.PlaneGeometry(20, 180);
wallMeshLeft = new THREE.Mesh(leftGeo, new THREE.MeshStandardMaterial({ color: 0x4a3528, flatShading: true, roughness: 0.9 }));
wallMeshLeft.rotation.y = Math.PI / 2;
wallMeshLeft.position.x = -100;
const rightGeo = new THREE.PlaneGeometry(20, 180);
wallMeshRight = new THREE.Mesh(rightGeo, new THREE.MeshStandardMaterial({ color: 0x4a3528, flatShading: true, roughness: 0.9 }));
wallMeshRight.rotation.y = -Math.PI / 2;
wallMeshRight.position.x = 100;
// Floor strip (part of terrain)
const floorGeo = new THREE.PlaneGeometry(60, 4);
const floorMat = new THREE.MeshStandardMaterial({ color: 0x3a2518, flatShading: true });
for (let i = -CFG.worldChunkSize/2; i < CFG.worldChunkSize/2; i++) {
const strip = new THREE.Mesh(floorGeo.clone(), floorMat);
strip.rotation.x = Math.PI / 2;
strip.position.z = i * 60 + CFG.worldChunkSize / 2;
scene.add(strip);
}
}
// --- WORLD GENERATION ---
class WorldGenerator {
constructor(scene) { this.scene = scene; this.chunks = []; this.playerZ = 0; }
getChunkIndex(z) { return Math.floor(-z / CFG.worldChunkSize); }
ensureChunksAhead() {
const targetIdx = this.getChunkIndex(this.playerZ + CFG.worldChunkSize * CFG.chunksAhead);
while (this.chunks.length <= targetIdx) {
this.generateChunk(this.chunks.length);
}
}
generateChunk(idx) {
// Determine canyon width variation for this chunk
let baseWidth = 70 + Math.sin(idx * 1.3 + 5) * 8;
const squeezeIdx = idx % 15;
if (squeezeIdx === 0 || squeezeIdx === 2 || squeezeIdx === 4) {
baseWidth = 18 + Math.random() * 8; // Tight squeeze
} else if (idx % 10 === 1) {
baseWidth = 55 + Math.sin(idx * 2.7) * 12; // Moderate width with monoliths
}
const wallOffset = baseWidth / 2 - 8;
// Generate left wall vertices (jagged)
const leftVerts = [];
for (let y = -80; y <= 50; y++) {
let x = -wallOffset + Math.sin(y * 0.3 + idx * 2) * 1.5;
if (y < -40) x += Math.random() * 2;
leftVerts.push(new THREE.Vector3(x, y, this.chunks.length * CFG.worldChunkSize));
}
// Generate right wall vertices
const rightVerts = [];
for (let y = -80; y <= 50; y++) {
let x = wallOffset + Math.sin(y * 0.3 + idx * 2) * 1.5 + 3;
if (y < -40) x += Math.random() * 2;
rightVerts.push(new THREE.Vector3(x, y, this.chunks.length * CFG.worldChunkSize));
}
// Create wall meshes using BufferGeometry
const leftGeo = new THREE.BufferGeometry();
leftGeo.setAttribute('position', new THREE.Float32BufferAttribute(leftVerts.map(v => [v.x, v.y, v.z]), 3));
const leftMat = new THREE.MeshStandardMaterial({ color: squeezeIdx % 5 === 0 ? 0x6b4a35 : 0x4a3528, flatShading: true });
const wallLeft = new THREE.Mesh(leftGeo, leftMat);
this.scene.add(wallLeft);
const rightGeo = new THREE.BufferGeometry();
rightGeo.setAttribute('position', new THREE.Float32BufferAttribute(rightVerts.map(v => [v.x, v.y, v.z]), 3));
const wallRight = new THREE.Mesh(rightGeo, leftMat);
this.scene.add(wallRight);
// Floor strip for this chunk
const floorSegs = [];
for (let i = -CFG.worldChunkSize/2; i < CFG.worldChunkSize/2; i += 5) {
floorSegs.push(new THREE.Vector3(0, -1, i * CFG.worldChunkSize));
}
// Floating monoliths in moderate chunks
if (idx % 10 === 1 && squeezeIdx !== 0) {
this.placeMonolith(idx);
}
// Near-miss trigger zones
const nearMissZ = (squeezeIdx !== 0 && squeezeIdx > 4) ? idx : -99;
if (nearMissZ >= 0) {
this.scene.userData.nearMissChunks.push(nearMissZ);
}
// Dust/debris particles floating in air for speed effect
const debrisCount = baseWidth < 25 ? 12 : 8 + Math.floor(Math.random() * 4);
for (let d = 0; d < debrisCount; d++) {
const z = this.chunks.length * CFG.worldChunkSize - 30 + Math.random() * CFG.worldChunkSize;
const x = (Math.random() - 0.5) * (baseWidth - 10);
const y = -40 + Math.random() * 20 + Math.sin(z * 0.1 + d) * 3;
this.scene.add(this.createDebris(x, y, z));
}
this.chunks.push({ baseWidth: baseWidth, wallOffset: wallOffset });
}
placeMonolith(idx) {
const monoliths = [];
for (let m = -3; m <= 3; m++) {
if (Math.random() > 0.5 && Math.abs(m) < 2) {
const x = (m * baseWidth / 6 + (Math.random()-0.5)*4) || -baseWidth/2 + 18;
const size = 3 + Math.random() * 6;
const geo = new THREE.ConeGeometry(size, size * 2.5, 4);
const mat = new THREE.MeshStandardMaterial({ color: 0x5a4035, flatShading: true });
const mono = new THREE.Mesh(geo, mat);
mono.position.set(x, -1 + Math.random()*10, idx * CFG.worldChunkSize);
if (m < 0) mono.rotation.y = -Math.PI / 2; else mono.rotation.y = Math.PI / 2;
this.scene.add(mono);
monoliths.push({ x: x || -baseWidth/2 + 18, z: idx * CFG.worldChunkSize });
}
}
// Also place one in front of player if close enough
const distToPlayer = Math.abs(idx * CFG.worldChunkSize);
if (distToPlayer > 30 && distToPlayer < 50) {
const mx = -baseWidth/2 + 18;
const mz = idx * CFG.worldChunkSize;
this.scene.userData.monoliths.push({ x: mx, z: mz, size: 4 });
}
return monoliths;
}
createDebris(x, y, z) {
const geo = new THREE.SphereGeometry(0.5 + Math.random() * 1.5, 6, 6);
const mat = new THREE.MeshBasicMaterial({ color: 0x8a7a60, wireframe: false });
const d = new THREE.Mesh(geo, mat);
d.position.set(x, y, z);
this.scene.add(d);
return d;
}
updatePlayerZ(z) {
this.playerZ = z;
}
cleanupOldChunks() {
const targetIdx = Math.floor(this.getChunkIndex(this.playerZ + CFG.worldChunkSize * CFG.chunksBehind));
while (this.chunks.length > targetIdx && this.chunks[0]) {
// Remove the oldest chunk's meshes from scene
for (let m of this.scene.children) {
if (m.userData.chunkIndex === 0 && !m.userData.isDebris) {
this.scene.remove(m);
break;
}
}
this.chunks.shift();
}
}
getWallPositions(z, width) {
const wallOffset = width / 2 - 8;
// Add some noise for natural feel
return {
left: -wallOffset + Math.sin((z % CFG.worldChunkSize) * 0.15 + 5) * 3,
right: wallOffset + Math.cos((z % CFG.worldChunkSize) * 0.12 + 7) * 3 + 4
};
}
getMonoliths() { return this.scene.userData.monoliths || []; }
}
// --- PLAYER PHYSICS & CRAFT ---
class PlayerCraft {
constructor(sceneRef) {
this.scene = sceneRef;
// Craft geometry - sleek geometric shape
const group = new THREE.Group();
// Main body (elongated wedge)
const bodyGeo = new THREE.ConeGeometry(0.8, 3, 6);
bodyGeo.rotateX(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0x2a2a44, flatShading: true, metalness: 0.9, roughness: 0.15
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
group.add(body);
// Nose cone glow
const noseGeo = new THREE.SphereGeometry(0.4, 8, 6);
const noseMat = new THREE.MeshBasicMaterial({ color: 0xffaa33 });
const nose = new THREE.Mesh(noseGeo, noseMat);
nose.position.z = -1.5;
group.add(nose);
// Wing (left)
const wingLGeo = new THREE.BoxGeometry(2.8, 0.15, 1.2);
const wingL = new THREE.Mesh(wingLGeo, bodyMat.clone());
wingL.position.set(-1.3, -0.1, 0.4);
group.add(wingL);
// Wing (right)
const wingR = new THREE.Mesh(wingLGeo, bodyMat.clone());
wingR.position.set(1.3, -0.1, 0.4);
group.add(wingR);
// Engine glow at back
const engineGeo = new THREE.SphereGeometry(0.5, 8, 6);
const engineMat = new THREE.MeshBasicMaterial({ color: 0xff4433 });
this.engineGlow = new THREE.Mesh(engineGeo, engineMat);
this.engineGlow.position.z = 1.2;
group.add(this.engineGlow);
// Engine light cone (spotlight)
const light = new THREE.SpotLight(0xff6633, 5, 40, Math.PI / 6, 0.5, 1);
light.position.set(0, -0.3, 0);
light.target.position.set(0, -2, -8);
group.add(light);
group.add(light.target);
// Small vertical stabilizer fins
const finGeo = new THREE.BoxGeometry(0.15, 0.6, 0.4);
const finL = new THREE.Mesh(finGeo, bodyMat.clone());
finL.position.set(-0.8, -0.3, 0);
group.add(finL);
const finR = new THREE.Mesh(finGeo, bodyMat.clone());
finR.position.set(0.8, -0.3, 0);
group.add(finR);
this.scene.add(group);
// Physics state
this.pos = new THREE.Vector3(0, CFG.groundY + 1, 50);
this.vel = new THREE.Vector3(0, 0, -CFG.playerSpeedMax * 0.7);
this.targetVel = new THREE.Vector3(0, 0, -CFG.playerSpeedMax * 0.7);
// Orientation
this.rotation = { x: Math.PI / 4, y: 0, z: 0 };
this.pitchInput = 0;
this.rollInput = 0;
this.targetRoll = 0;
// Flight state
this.energy = CFG.energyRechargePerOrb;
this.maxEnergy = 100;
this.multiplier = CFG.multiplierBase;
this.boostCharge = 0;
this.boostActive = false;
// Trail particles
this.trailParticles = [];
// Boost charge
setInterval(() => { this.updateBoost(); }, 50);
setInterval(() => { this.updateEnergyDecay(); }, 16);
}
updateBoost() {
if (this.boostActive) {
this.boostCharge -= CFG.boostConsumptionRate;
this.vel.length(this.vel.x + Math.abs(this.targetVel.z) * 2.5 > CFG.playerSpeedMax * CFG.boostSpeedMultiplier ?
Math.min(this.vel.length(), CFG.playerSpeedMax * CFG.boostSpeedMultiplier + 10, CFG.playerSpeedMax * CFG.boostSpeedMultiplier) :
Math.max(this.vel.length(), this.targetVel.length() - 3));
} else {
this.boostCharge += CFG.boostChargeRate;
}
if (this.boostCharge <= 0) { this.boostActive = false; }
if (this.boostCharge >= CFG.boostMaxCharge) { this.boostActive = true; }
}
updateEnergyDecay() {
this.energy -= CFG.energyDecayRate / 60;
if (this.energy < 0) this.energy = 0;
}
applyInput(lateral, pitch) {
// Lateral movement with inertia
const lateralVel = this.vel.x - this.targetVel.z * CFG.playerTurnRate * CFG.playerSpeedMax / 20;
this.targetRoll += lateral * CFG.playerBankStrength;
// Pitch for acceleration/deceleration
if (pitch > 0) {
this.targetVel.z -= CFG.playerAccel;
} else if (pitch < 0) {
this.targetVel.z += CFG.playerAccel;
}
// Clamp speed
const maxSpeed = this.boostActive ? CFG.playerSpeedMax * CFG.boostSpeedMultiplier : CFG.playerSpeedMax;
this.targetVel.z = Math.max(-maxSpeed, Math.min(maxSpeed, this.targetVel.z));
// Banking decay (roll returns to 0 when no lateral input)
this.targetRoll *= 0.92;
// Update rotation smoothly
const rotSpeed = CFG.playerTurnRate * 60;
const targetZRot = -Math.atan2(this.vel.x, this.targetVel.z);
// Smooth yaw tracking with slight lead/lag for weight
if (this.rollInput > 0) { this.rotation.y += rotSpeed * 0.5; }
else if (this.rollInput < 0) { this.rotation.y -= rotSpeed * 0.5; }
// Apply roll to craft
const bankAngle = Math.min(this.targetRoll, Math.PI / 2.5);
group.rotateZ(bankAngle);
// Pitch for visual tilt
if (pitch > 0) { group.rotation.x += 0.01; }
else if (pitch < 0) { group.rotation.x -= 0.01; }
this.rotation.z = bankAngle;
}
update(dt, world) {
// Update velocity toward target with momentum
const maxSpeed = this.boostActive ? CFG.playerSpeedMax * CFG.boostSpeedMultiplier : CFG.playerSpeedMax;
let currentZ = this.vel.z;
if (this.boostActive) {
currentZ += dt * 60 * CFG.playerAccel * 1.5;
} else {
currentZ += dt * 60 * CFG.playerAccel * 0.3;
}
this.vel.z = Math.max(-maxSpeed, Math.min(maxSpeed, currentZ));
// Apply friction to lateral velocity
const lateralFriction = this.boostActive ? 0.98 : 0.975;
this.vel.x *= lateralFriction;
this.vel.y *= CFG.playerFriction;
// Move forward
const moveZ = this.vel.z * dt * 60;
this.pos.z += moveZ;
// Apply banking offset to position (visual effect of rolling)
const bankOffsetX = -this.rotation.z * Math.tan(Math.abs(this.targetRoll)) * 0.5;
this.pos.x += this.vel.x * dt * 60 + bankOffsetX;
// Update energy based on speed
if (Math.abs(this.vel.z) > maxSpeed * 0.8) {
this.energy -= CFG.energyDecayRate * Math.abs(this.vel.z / maxSpeed);
}
// Collect orbs and interact with world
this.checkOrbCollection(world);
// Trail particles
const speedMult = this.boostActive ? 1.5 : 0.8;
this.trailParticles.push({ pos: new THREE.Vector3(this.pos.clone()), vel: this.vel.clone(), time: 0 });
if (this.trailParticles.length > 6) this.trailParticles.shift();
// Boost visual effects
this.engineGlow.material = new THREE.MeshBasicMaterial({ color: this.boostActive ? 0xff2244 : 0xff8833 });
const glowPulse = Math.sin(Date.now() * 0.01) * 0.3 + 0.7;
this.engineGlow.scale.setScalar(glowPulse);
// Update camera to follow
const lookTarget = new THREE.Vector3(this.pos.x, CFG.groundY + CFG.cameraVerticalOffset, this.pos.z - CFG.cameraHorizontalOffset * 1.2);
const camOffsetX = bankOffsetX;
const targetCamPos = new THREE.Vector3(lookTarget.x - 0.5 * (this.rotation.z), lookTarget.y, lookTarget.z);
this.scene.camera.position.lerp(targetCamPos, CFG.cameraLerp);
// Camera FOV widens with boost
if (this.boostActive) {
this.scene.camera.fov = THREE.MathUtils.lerp(this.scene.camera.fov, 95, dt * 60 * 0.1);
} else {
this.scene.camera.fov = THREE.MathUtils.lerp(this.scene.camera.fov, 70, dt * 60 * 0.1);
}
this.scene.camera.updateProjectionMatrix();
// Camera shake on boost or near-miss
if (this.boostActive) {
const shakeX = (Math.random() - 0.5) * 2;
const shakeY = (Math.random() - 0.5) * 1.5;
this.scene.camera.position.x += shakeX;
this.scene.camera.position.y += shakeY + this.rotation.z;
}
// Update engine glow position with craft rotation
const groupPos = new THREE.Vector3();
group.getWorldPosition(groupPos);
this.engineGlow.position.copy(this.pos).add(new THREE.Vector3(0, 1.5 * Math.sin(Date.now()*0.02), 0));
return { pos: this.pos.clone(), vel: this.vel.clone() };
}
checkOrbCollection(world) {
// Look ahead for orbs and collectibles
const lookDist = CFG.worldChunkSize * 3;
const zMin = this.pos.z - lookDist;
const zMax = this.pos.z + lookDist;
for (let i = world.orbs.length - 1; i >= 0; i--) {
const orb = world.orbs[i];
if (orb.collected || orb.mesh) continue;
const distZ = Math.abs(orb.z - this.pos.z);
if (distZ < lookDist && Math.abs(orb.x - this.pos.x) < 12 && Math.abs(orb.y - this.pos.y) < 3) {
// Collect!
world.orbs.splice(i, 1);
this.energy = Math.min(this.maxEnergy, this.energy + CFG.energyRechargePerOrb);
AudioSystem.playPing();
// Create collection particle burst
for (let p = 0; p < 8; p++) {
const geo = new THREE.SphereGeometry(0.3, 6, 6);
const mat = new THREE.MeshBasicMaterial({ color: 0xffdd44 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(orb.pos.clone());
this.scene.add(mesh);
// Animate and remove
const animStart = Date.now();
setTimeout(() => {
const elapsed = (Date.now() - animStart) / 100;
if (elapsed < 300) {
mesh.position.y += 2 * (elapsed / 300);
mesh.material.opacity = 1 - (elapsed / 300);
} else {
this.scene.remove(mesh);
}
}, p * 50 + 200);
}
}
}
// Check monolith collision avoidance
for (const mono of world.monoliths) {
const dx = mono.x - this.pos.x;
const dz = mono.z - this.pos.z;
const dist = Math.sqrt(dx*dx + dz*dz);
if (dist < 5) {
// Too close to monolith - push back slightly
AudioSystem.playWhoosh();
const pushZ = MonoPos > this.pos.z ? -1 : 1;
this.vel.z += pushZ * 20;
}
if (dist < 8 && dist > 3) {
// Near-miss! Score multiplier increases
const proximityBonus = Math.max(0, (8 - dist)) * 5;
this.multiplier = Math.min(this.multiplier + proximityBonus / 10, 20);
}
}
// Check near-miss with canyon walls from wall positions
for (const z of world.nearMissZones) {
if (z > this.pos.z && z < this.pos.z + CFG.worldChunkSize * 2) {
const wx = world.getWallPosition(this.pos.z, world.currentWidth);
const wallDist = Math.abs(wx - this.pos.x);
if (wallDist < 6) {
AudioSystem.playWhoosh();
this.multiplier += 5;
}
}
}
// Decay multiplier slowly
this.multiplier *= CFG.multiplierDecayRate / 60;
if (this.multiplier <= CFG.multiplierBase) this.multiplier = CFG.multiplierBase;
}
getBoostCharge() { return this.boostCharge; }
}
// --- GAME ENGINE ---
const game = {
audio: new AudioSystem(),
particles: null,
player: null,
world: null,
init() {
const { scene, renderer, camera } = createScene();
// Build initial canyon wall meshes
function makeWall(verts) {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(verts.map(v => v.slice()), 3));
return new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x4a3528, flatShading: true }));
}
const lVerts = [];
for (let y = -80; y <= 50; y += 2) {
let x = -31 + Math.sin((y / 2) * 0.3 + 5) * 1.5; if (y < -40) x += Math.random() * 2; lVerts.push([x, y, 0]);
}
const rVerts = [];
for (let y = -80; y <= 50; y += 2) {
let x = 31 + Math.cos((y / 2) * 0.3 + 7) * 1.5 + 4; if (y < -40) x += Math.random() * 2; rVerts.push([x, y, 0]);
}
this.walls = { leftMesh: makeWall(lVerts), rightMesh: makeWall(rVerts) };
scene.add(this.walls.leftMesh);
scene.add(this.walls.rightMesh);
// Store the actual THREE.Scene for later use (debris, orbs, monoliths all go here)
this.sceneData = { scene };
// Re-apply canyon wall generation as part of world
this.worldChunks = [];
for (let i = 0; i < CFG.worldChunkSize/60 + 3; i++) {
const offset = 75 + Math.sin(i * 1.3) * 8;
this.worldChunks.push({ baseWidth: offset, wallOffset: offset });
// Add debris for speed effect
const debrisCount = (offset < 20) ? 14 : 8 + Math.floor(Math.random() * 6);
for (let d = 0; d < debrisCount; d++) {
this.sceneData.scene.add(this.createDebris(
(Math.random()-0.5) * (offset - 12),
-40 + Math.random()*18 + Math.sin(i*0.1+d)*3,
i * CFG.worldChunkSize - 30 + Math.random() * CFG.worldChunkSize
));
}
}
this.sceneData.scene.userData.monoliths = [];
this.sceneData.scene.userData.nearMissChunks = [-99];
// Create initial orbs scattered in the canyon
const orbPositions = [];
for (let i = 0; i < 25; i++) {
const z = -100 + Math.random() * CFG.worldChunkSize * 3;
const chunkIdx = Math.max(0, Math.min(this.worldChunks.length - 1, Math.floor(-z / CFG.worldChunkSize)));
const widthRange = this.worldChunks[chunkIdx] || { baseWidth: 80 };
const orbW = (widthRange.baseWidth / 2) - 8 - 5; // Keep away from walls
if (Math.abs(orbW) < 3) continue;
const x = (Math.random() - 0.5) * Math.min(Math.abs(orbW), orbW + 1);
const y = -20 + Math.random() * 40;
orbPositions.push({ x, y, z });
}
// Player craft
this.player = new PlayerCraft(this.sceneData.scene);
// Particle system
this.particles = new ParticleSystem();
// World state
this.worldState = {
orbs: [],
monoliths: [],
nearMissZones: [-99],
currentWidth: 80,
widthVariation: 10,
generationIndex: 0
};
for (const p of orbPositions) {
this.worldState.orbs.push({
pos: new THREE.Vector3(p.x + (Math.random()-0.5)*2, p.y, p.z),
x: p.x, y: p.y, z: p.z
});
// Create orbital mesh with glow shader
const geo = new THREE.SphereGeometry(1.8, 16, 8);
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1, 0.7),
transparent: true, opacity: 0.9
});
const mesh = new THREE.Mesh(geo, mat);
this.sceneData.scene.add(mesh);
// Outer glow ring
const ringGeo = new THREE.TorusGeometry(3.5, 0.12, 8, 24);
const ringMat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1, 0.6),
transparent: true, opacity: 0.3
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = Math.PI / 2;
this.sceneData.scene.add(ring);
this.worldState.orbs[this.worldState.orbs.length - 1].mesh = mesh;
}
// Event listeners
const btns = document.getElementById('start-btn');
if (btns) btns.addEventListener('click', () => {
AudioSystem.init();
this.startGame();
});
document.getElementById('restart-btn').addEventListener('click', () => {
this.restart();
});
window.addEventListener('resize', () => {
const aspect = window.innerWidth / window.innerHeight;
camera.aspect = aspect;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Input handling
this.keys = {};
document.addEventListener('keydown', e => { this.keys[e.code] = true; });
document.addEventListener('keyup', e => { this.keys[e.code] = false; });
this.lastTime = performance.now();
},
createDebris(x, y, z) {
const geo = new THREE.SphereGeometry(0.5 + Math.random() * 1.2, 6, 6);
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.03, 0.8, 0.4),
wireframe: false
});
const d = new THREE.Mesh(geo, mat);
d.position.set(x, y, z);
this.sceneData.scene.add(d);
// Animate with speed flow effect
const animateStart = Date.now();
setTimeout(() => {
const elapsed = (Date.now() - animateStart) / 100;
if (elapsed < 250) {
d.position.x += Math.sin(elapsed * 0.05) * 30;
d.position.y -= (elapsed / 250) * 2;
} else {
this.sceneData.scene.remove(d);
}
}, 100 + Math.random() * 300);
return d;
},
startGame() {
document.getElementById('game-over').classList.remove('visible');
document.getElementById('start-screen').style.opacity = '0';
setTimeout(() => { this.isRunning = true; this.loop(); }, 500);
},
restart() {
if (this.player) {
// Remove old meshes that might be stuck
this.sceneData.scene.children.forEach(c => {
if (c.userData && c.userData.orbMesh) {
this.sceneData.scene.remove(c);
}
});
}
const orbPositions = [];
for (let i = 0; i < 25; i++) {
const z = -150 + Math.random() * CFG.worldChunkSize * 3;
const widthRange = this.worldChunks[Math.min(Math.floor(-z/60), this.worldChunks.length-1) || 0] || { baseWidth: 80 };
const orbW = (widthRange.baseWidth / 2) - 8 - 5;
if (Math.abs(orbW) < 3) continue;
const x = (Math.random() - 0.5) * Math.min(Math.abs(orbW), orbW + 1);
const y = -20 + Math.random() * 40;
orbPositions.push({ x, y, z });
}
// Recreate orbs
this.worldState.orbs.forEach((o, i) => {
if (o.mesh) this.sceneData.scene.remove(o.mesh);
});
for (const p of orbPositions) {
const orbEntry = { pos: new THREE.Vector3(p.x + (Math.random()-0.5)*2, p.y, p.z), x: p.x, y: p.y, z: p.z };
const geo = new THREE.SphereGeometry(1.8, 16, 8);
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1, 0.7),
transparent: true, opacity: 0.9
});
const mesh = new THREE.Mesh(geo, mat);
this.sceneData.scene.add(mesh);
const ringGeo = new THREE.TorusGeometry(3.5, 0.12, 8, 24);
const ringMat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1, 0.6),
transparent: true, opacity: 0.3
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = Math.PI / 2;
this.sceneData.scene.add(ring);
orbEntry.mesh = mesh;
this.worldState.orbs.push(orbEntry);
}
// Reset player
this.player.pos.set(0, CFG.groundY + 1, 50);
const maxSpeed = CFG.playerSpeedMax * 0.7;
this.player.vel.set(-maxSpeed, 0, -maxSpeed * 2.5);
// Reset state
this.player.energy = CFG.maxEnergy;
this.player.multiplier = CFG.multiplierBase;
this.player.boostCharge = 0;
this.player.boostActive = false;
this.player.rotation.set(0, 0, 0);
this.player.targetRoll = 0;
// Reset world state
this.worldState.generationIndex = 0;
this.worldState.nearMissZones = [-99];
// Update player reference
if (this.gamePlayerRef) {
this.sceneData.remove(this.gamePlayerRef);
}
this.gamePlayerRef = this.player.group;
this.sceneData.add(this.gamePlayerRef);
this.lastTime = performance.now();
},
loop() {
if (!this.isRunning || !this.player) return;
const now = performance.now();
const dt = Math.min((now - this.lastTime) / 1000, 0.05); // Cap delta time
this.lastTime = now;
// Get player position
const playerPos = new THREE.Vector3();
this.player.group.getWorldPosition(playerPos);
// Update world generation ahead of player
this.worldState.generationIndex = Math.floor(-playerPos.z / CFG.worldChunkSize) + CFG.chunksAhead;
while (this.worldChunks.length <= this.worldState.generationIndex) {
const idx = this.worldChunks.length - 1;
const baseWidth = 70 + Math.sin(idx * 1.3 + 5) * 8;
if ((idx % 15 === 0 || idx % 15 === 2 || idx % 15 === 4) && idx > 0) {
this.worldState.currentWidth = 18 + Math.random() * 8; // Squeeze!
} else if (idx % 10 === 1) {
this.worldState.currentWidth = 55 + Math.sin(idx * 2.7) * 12; // Monolith chunk
} else {
this.worldState.currentWidth = baseWidth;
}
const wallOffset = this.worldState.currentWidth / 2 - 8;
// Generate debris for speed effect in this chunk
if (this.worldChunks[idx] && !this.worldChunks[idx].debrisGenerated) {
const debrisCount = (this.worldState.currentWidth < 25) ? 14 : 8 + Math.floor(Math.random() * 6);
for (let d = 0; d < debrisCount; d++) {
const z = idx * CFG.worldChunkSize - 30 + Math.random() * CFG.worldChunkSize;
const x = (Math.random() - 0.5) * (this.worldState.currentWidth - 12);
const y = -40 + Math.random() * 20 + Math.sin(z * 0.1 + d) * 3;
this.sceneData.scene.add(this.createDebris(x, y, z));
}
this.worldChunks[idx].debrisGenerated = true;
}
// Add monolith if needed
const distToPlayer = Math.abs(idx * CFG.worldChunkSize);
if (idx % 10 === 1 && idx > 2 && distToPlayer > 30 && distToPlayer < 60) {
for (let m = -3; m <= 3; m++) {
const mx = (m * 8 + (Math.random()-0.5)*4) || -this.worldState.currentWidth/2 + 18;
const mz = idx * CFG.worldChunkSize;
const monoGeo = new THREE.ConeGeometry(3, 6, 4);
const monoMat = new THREE.MeshStandardMaterial({ color: 0x5a4035, flatShading: true });
const monoMesh = new THREE.Mesh(monoGeo, monoMat);
this.sceneData.scene.add(monoMesh);
if (m < 0) monoMesh.rotation.y = -Math.PI / 2; else {
monoMesh.rotation.y = Math.PI / 2;
// Add second face for flat look
const monoGeo2 = new THREE.ConeGeometry(3, 6, 4);
const monoMesh2 = new THREE.Mesh(monoGeo2, monoMat.clone());
this.sceneData.scene.add(monoMesh2);
}
if (m < 0) {
this.worldState.monoliths.push({ x: mx || -this.worldState.currentWidth/2 + 18, z: mz, size: 4 });
} else {
this.worldState.monoliths.push({ x: mx || this.worldState.currentWidth/2 - 18, z: mz, size: 4 });
}
// Near-miss zone for monolith proximity scoring
if (idx % 10 === 1 && idx > 3) {
const nz = Math.floor(mz / CFG.worldChunkSize);
if (!this.worldState.nearMissZones.includes(nz)) {
this.worldState.nearMissZones.push(nz);
}
}
}
}
// Update wall meshes positions (keep them in sync with generation)
if (this.walls.leftMesh && this.walls.rightMesh) {
const zPos = idx * CFG.worldChunkSize;
const lVerts = [];
for (let y = -80; y <= 50; y += 2) {
let x = -wallOffset + Math.sin((y / 2) * 0.3 + idx * 2) * 1.5;
if (y < -40) x += Math.random() * 2;
lVerts.push([x, y, zPos]);
}
this.walls.leftMesh.geometry.setAttribute('position', new THREE.Float32BufferAttribute(lVerts, 3));
const rVerts = [];
for (let y = -80; y <= 50; y += 2) {
let x = wallOffset + Math.sin((y / 2) * 0.3 + idx * 2) * 1.5 + 3;
if (y < -40) x += Math.random() * 2;
rVerts.push([x, y, zPos]);
}
this.walls.rightMesh.geometry.setAttribute('position', new THREE.Float32BufferAttribute(rVerts, 3));
}
this.worldChunks.push({ baseWidth: this.worldState.currentWidth, wallOffset: wallOffset, debrisGenerated: false });
}
// Apply player input (flight controls)
const lateralInput = (this.keys['KeyA'] ? -1 : 0) + (this.keys['KeyD'] ? 1 : 0);
const pitchInput = (this.keys['ArrowUp'] || this.keys['Space']) ? 1 :
(this.keys['ArrowDown'] ? -1 : 0);
// Only respond to input if moving forward
if (Math.abs(this.player.vel.z) > CFG.playerSpeedMax * 0.3) {
this.player.applyInput(lateralInput, pitchInput);
} else {
this.player.targetRoll *= 0.92;
}
// Update player physics
const playerData = this.player.update(dt, this.worldState);
// Check collisions with canyon walls
const wallW = Math.max(this.worldChunks[Math.floor(-playerPos.z / CFG.worldChunkSize) || 0] || { baseWidth: 80 }).baseWidth;
const wx = this.worldChunks[Math.floor(-playerPos.z / CFG.worldChunkSize) || 0] || { wallOffset: 31 };
const leftWallX = -wx.wallOffset + Math.sin((-playerPos.z % CFG.worldChunkSize) * 0.15 + 5) * 3;
const rightWallX = wx.wallOffset + Math.cos((-playerPos.z % CFG.worldChunkSize) * 0.12 + 7) * 3 + 4;
// Simple distance-based collision with canyon walls (cylinder approximation)
for (const wallX of [leftWallX, rightWallX]) {
const dx = Math.abs(playerPos.x - wallX);
if (dx < 6 && Math.abs(playerPos.z - ((playerPos.z % CFG.worldChunkSize) + cfg.worldChunkSize / 2)) < CFG.worldChunkSize * 0.5) {
// Near-wall penalty
const distToWall = dx;
if (distToWall < 4) {
this.player.multiplier += 3;
AudioSystem.playWhoosh();
// Push player back from wall
const pushDir = playerPos.x > wallX ? -1 : 1;
this.player.vel.z += pushDir * 8;
}
}
}
// Check game over (energy depleted)
if (this.player.energy <= 0) {
AudioSystem.playCrash();
this.gameOver();
return;
}
// Update particles
this.particles.addEngineTrail(playerData.pos, playerData.vel);
// Add dust when boosting
if (this.player.boostActive || Math.abs(this.player.vel.z) > CFG.playerSpeedMax * 0.9) {
this.particles.addDust(playerData.pos, playerData.vel);
}
this.particles.update(dt);
// Update engine glow position
const groupPos = new THREE.Vector3();
this.player.group.getWorldPosition(groupPos);
if (this.player.engineGlow) {
this.player.engineGlow.position.copy(playerData.pos).add(new THREE.Vector3(0, 1.5 * Math.sin(Date.now()*0.02), 0));
}
// Update HUD
const speedPct = Math.min(Math.abs(this.player.vel.z) / CFG.playerSpeedMax, 1);
document.getElementById('speed-fill').style.width = (speedPct * 100) + '%';
// Energy bar
this.player.group.rotation.x += -this.keys['ArrowUp'] ? 0 : Math.min(this.keys['Space'], 0.02);
document.getElementById('energy-fill').style.width = (this.player.energy / CFG.maxEnergy * 100) + '%';
// Multiplier display
const multEl = document.getElementById('multiplier');
if (this.player.multiplier > 1.5) {
multEl.textContent = Math.floor(this.player.multiplier);
multEl.classList.add('multiplier-pulse');
} else {
multEl.style.opacity = '0';
}
// Update camera FOV and shake for boost
if (this.player.boostActive) {
const cam = this.sceneData.scene;
const targetFOV = THREE.MathUtils.lerp(camera.fov, 95, dt * 60 * 0.1);
const shakeX = (Math.random() - 0.5) * 2;
const shakeY = (Math.random() - 0.5) * 1.5 + this.player.rotation.z;
cam.camera.position.x += shakeX;
cam.camera.position.y += shakeY;
} else {
camera.fov = THREE.MathUtils.lerp(camera.fov, 70, dt * 60 * 0.1);
}
// Engine glow pulse
if (this.player.engineGlow) {
const glowPulse = Math.sin(Date.now() * 0.015) * 0.3 + 0.7;
this.player.engineGlow.scale.setScalar(glowPulse);
const groupPos2 = new THREE.Vector3();
this.player.group.getWorldPosition(groupPos2);
this.player.engineGlow.position.copy(playerData.pos).add(new THREE.Vector3(0, 1.5 * Math.sin(Date.now()*0.02), 0));
}
// Ambient engine hum
AudioSystem.engineHum(Math.abs(this.player.vel.z));
// Render
renderer.render(this.sceneData.scene, camera);
requestAnimationFrame(() => this.loop());
},
gameOver() {
this.isRunning = false;
const statsEl = document.getElementById('go-stats');
statsEl.innerHTML = `Final Score: ${Math.floor(this.player.multiplier * 1000)}<br>Max Speed: ${this.player.boostActive ? CFG.playerSpeedMax * CFG.boostSpeedMultiplier.toFixed(1) : CFG.playerSpeedMax.toFixed(1)} km<br>Energy at end: ${(this.player.energy / CFG.maxEnergy * 100).toFixed(0)}%`;
document.getElementById('game-over').classList.add('visible');
}
};
// Set up initial wall meshes in scene after creation
function createWallMeshes(scene) {
const leftVerts = [];
for (let y = -80; y <= 50; y += 2) {
let x = -31 + Math.sin((y / 2) * 0.3 + 5) * 1.5;
if (y < -40) x += Math.random() * 2;
leftVerts.push([x, y, 0]);
}
const rightVerts = [];
for (let y = -80; y <= 50; y += 2) {
let x = 31 + Math.cos((y / 2) * 0.3 + 7) * 1.5 + 4;
if (y < -40) x += Math.random() * 2;
rightVerts.push([x, y, 0]);
}
const leftGeo = new THREE.BufferGeometry();
leftGeo.setAttribute('position', new THREE.Float32BufferAttribute(leftVerts, 3));
const leftMat = new THREE.MeshStandardMaterial({ color: 0x4a3528, flatShading: true });
const wallLeft = new THREE.Mesh(leftGeo, leftMat);
scene.add(wallLeft);
const rightGeo = new THREE.BufferGeometry();
rightGeo.setAttribute('position', new THREE.Float32BufferAttribute(rightVerts, 3));
const wallRight = new THREE.Mesh(rightGeo, leftMat.clone());
scene.add(wallRight);
game.wallMeshLeft = wallLeft;
game.wallMeshRight = wallRight;
}
// Create the debris generator function once
function createDebris(x, y, z) {
const geo = new THREE.SphereGeometry(0.5 + Math.random() * 1.2, 6, 6);
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.03, 0.8, 0.4)
});
const d = new THREE.Mesh(geo, mat);
d.position.set(x, y, z);
return d;
}
// Initialize everything on DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
game.init();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"ornith-1.0-9b","provider":"lmstudio","persona":"full","sessionId":"ff5d6ea8-695c-4597-bb1f-3c632e596a7e","tokensIn":2784622,"tokensOut":29413,"tokensTotal":2814035,"turns":40,"toolCalls":39,"failedToolCalls":2,"timestamp":"2026-07-13T17:26:35.399Z"} -->