Canyon Run
thinkingcap qwen3.6 27b · 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 39,624 bytes · SHA-256 9720d9187755
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Canyon Racer</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0510; font-family: 'Segoe UI', system-ui, sans-serif; }
#game-container { position: relative; width: 100vw; height: 100vh; }
#game-canvas { display: block; width: 100%; height: 100%; }
/* HUD */
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#speed-bar { position: absolute; right: 30px; bottom: 40px; width: 6px; height: 200px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; }
#speed-fill { position: absolute; bottom: 0; width: 100%; height: 50%; background: linear-gradient(to top, #ff6b35, #ffd700); border-radius: 3px; transition: height 0.1s; box-shadow: 0 0 10px rgba(255,107,53,0.5); }
#multiplier { position: absolute; right: 50px; bottom: 40px; font-size: 48px; font-weight: 900; color: #ffd700; text-shadow: 0 0 20px rgba(255,215,0,0.5); letter-spacing: -2px; }
#energy-bar { position: absolute; left: 30px; bottom: 40px; width: 200px; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; }
#energy-fill { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: linear-gradient(to right, #7209b7, #ff6b35); border-radius: 3px; transition: width 0.1s; box-shadow: 0 0 10px rgba(114,9,183,0.5); }
#turbo-bar { position: absolute; left: 30px; bottom: 60px; width: 200px; height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; }
#turbo-fill { position: absolute; left: 0; top: 0; width: 0%; height: 100%; background: linear-gradient(to right, #00d4ff, #76ff03); border-radius: 2px; transition: width 0.1s; box-shadow: 0 0 8px rgba(0,212,255,0.5); }
/* Screens */
#start-screen, #game-over { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(10,5,16,0.85); z-index: 10; }
#start-screen h1 { font-size: 72px; font-weight: 900; color: #ff6b35; text-shadow: 0 0 40px rgba(255,107,53,0.6); letter-spacing: -3px; margin-bottom: 10px; }
#game-over h1 { font-size: 64px; font-weight: 900; color: #ff3d3d; text-shadow: 0 0 40px rgba(255,61,61,0.6); letter-spacing: -2px; margin-bottom: 30px; }
.subtitle { color: #c0a080; font-size: 18px; margin-bottom: 20px; }
.controls { color: #908070; font-size: 14px; margin-bottom: 40px; line-height: 1.8; }
#final-score, #max-speed { color: #ffd700; font-size: 28px; margin: 8px 0; font-weight: 600; }
button { pointer-events: all; padding: 15px 50px; font-size: 20px; font-weight: 700; border: none; border-radius: 4px; cursor: pointer; background: linear-gradient(135deg, #ff6b35, #e84118); color: white; letter-spacing: 2px; text-transform: uppercase; box-shadow: 0 4px 20px rgba(255,107,53,0.4); transition: transform 0.1s, box-shadow 0.1s; }
button:hover { transform: scale(1.05); box-shadow: 0 6px 30px rgba(255,107,53,0.6); }
/* ── css: score-display ── */
#score-display { position: absolute; top: 20px; right: 30px; font-size: 36px; font-weight: 900; color: #ffd700; text-shadow: 0 0 15px rgba(255,215,0,0.4); letter-spacing: -1px; }
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="hud">
<div id="speed-bar"><div id="speed-fill"></div></div>
<div id="multiplier">x1.0</div>
<div id="energy-bar"><div id="energy-fill"></div></div>
<div id="turbo-bar"><div id="turbo-fill"></div></div>
<div id="score-display">0</div></div>
<div id="game-over" style="display:none;">
<h1>CANYON BREACH</h1>
<p id="final-score">Score: 0</p>
<p id="max-speed">Max Speed: 0</p>
<button id="restart-btn">RESTART</button>
</div>
<div id="start-screen">
<h1>CANYON RACER</h1>
<p class="subtitle">Navigate the procedural canyon at extreme speed</p>
<p class="controls">WASD / Arrow Keys — Steer<br>SPACE — Boost</p>
<button id="start-btn">LAUNCH</button>
</div>
</div>
<script>
'use strict';
// ── const: CHUNK_SIZE ──
const CHUNK_SIZE = 200;
// ── const: CHUNKS_AHEAD ──
const CHUNKS_AHEAD = 6;
// ── const: CHUNKS_BEHIND ──
const CHUNKS_BEHIND = 2;
// ── const: PLAYER_SPEED_BASE ──
const PLAYER_SPEED_BASE = 80;
// ── const: PLAYER_MAX_SPEED ──
const PLAYER_MAX_SPEED = 180;
// ── const: BOOST_MULTIPLIER ──
const BOOST_MULTIPLIER = 1.8;
// ── const: TURN_RATE ──
const TURN_RATE = 3.5;
// ── const: BANK_ANGLE ──
const BANK_ANGLE = 0.4;
// ── const: ENERGY_DECAY ──
const ENERGY_DECAY = 3.0;
// ── const: ENERGY_MAX ──
const ENERGY_MAX = 100;
// ── const: ORB_ENERGY ──
const ORB_ENERGY = 25;
// ── const: NEAR_MISS_DIST ──
const NEAR_MISS_DIST = 6;
// ── const: COLLISION_RADIUS ──
const COLLISION_RADIUS = 2.5;
// ── const: COLORS ──
const COLORS = {
skyTop: new THREE.Color(0x1a0533),
skyBottom: new THREE.Color(0xff6b35),
canyonWall: [new THREE.Color(0x4a2040), new THREE.Color(0x3d1f3c), new THREE.Color(0x5c2e50)],
monolith: new THREE.Color(0x7209b7),
squeeze: new THREE.Color(0x6b3fa0),
orb: new THREE.Color(0xffd700),
playerBody: new THREE.Color(0xc0c0c0),
playerAccent: new THREE.Color(0xff6b35),
engineGlow: new THREE.Color(0x00d4ff),
};
// ── class: AudioSystem ──
class AudioSystem {
constructor() {
this.ctx = null;
this.engineOsc = null;
this.engineGain = null;
this.initialized = false;
}
init() {
if (this.initialized) return;
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
// Engine hum - low frequency oscillator with filter
this.engineOsc = this.ctx.createOscillator();
this.engineGain = this.ctx.createGain();
const engineFilter = this.ctx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 200;
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 55;
this.engineGain.gain.value = 0.08;
this.engineOsc.connect(engineFilter);
engineFilter.connect(this.engineGain);
this.engineGain.connect(this.ctx.destination);
this.engineOsc.start();
this.initialized = true;
}
setEnginePitch(speedRatio) {
if (!this.initialized) return;
const freq = 55 + speedRatio * 80;
this.engineOsc.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.1);
this.engineGain.gain.setTargetAtTime(0.06 + speedRatio * 0.06, this.ctx.currentTime, 0.1);
}
playCollect() {
if (!this.initialized) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(880, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1760, this.ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.3);
}
playNearMiss() {
if (!this.initialized) return;
const bufferSize = this.ctx.sampleRate * 0.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 / (bufferSize * 0.3));
}
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 2000;
filter.Q.value = 2;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
source.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
source.start();
}
playCrash() {
if (!this.initialized) return;
const bufferSize = this.ctx.sampleRate * 0.8;
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 / (bufferSize * 0.4));
}
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(3000, this.ctx.currentTime);
filter.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.6);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
source.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
source.start();
// Sub bass hit
const osc = this.ctx.createOscillator();
const g2 = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(80, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(30, this.ctx.currentTime + 0.5);
g2.gain.setValueAtTime(0.25, this.ctx.currentTime);
g2.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
osc.connect(g2);
g2.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.5);
}
playBoost() {
if (!this.initialized) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(600, this.ctx.currentTime + 0.3);
gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.02, this.ctx.currentTime + 0.4);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.4);
}
}
// ── class: PlayerCraft ──
class PlayerCraft {
constructor(scene) {
this.group = new THREE.Group();
this.position = new THREE.Vector3(0, 0, 0);
this.velocity = new THREE.Vector3(0, 0, 0);
this.speed = PLAYER_SPEED_BASE;
this.targetSpeed = PLAYER_SPEED_BASE;
this.rollAngle = 0;
this.pitchAngle = 0;
this.buildModel(scene);
}
buildModel() {
// Main body - sleek wedge shape
const bodyGeo = new THREE.ConeGeometry(1.2, 4, 6);
bodyGeo.rotateX(Math.PI / 2);
const bodyMat = new THREE.MeshLambertMaterial({ color: COLORS.playerBody, flatShading: true });
this.bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
this.group.add(this.bodyMesh);
// Cockpit accent
const cockpitGeo = new THREE.BoxGeometry(0.6, 0.4, 1.5);
const cockpitMat = new THREE.MeshLambertMaterial({ color: COLORS.playerAccent, flatShading: true });
this.cockpitMesh = new THREE.Mesh(cockpitGeo, cockpitMat);
this.cockpitMesh.position.set(0, 0.3, -0.2);
this.group.add(this.cockpitMesh);
// Wings
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(3, -1);
wingShape.lineTo(3, -1.5);
wingShape.lineTo(0, -0.8);
wingShape.closePath();
const wingGeoL = new THREE.ExtrudeGeometry(wingShape, { depth: 0.15, bevelEnabled: false });
const wingMat = new THREE.MeshLambertMaterial({ color: COLORS.playerBody, flatShading: true });
this.wingL = new THREE.Mesh(wingGeoL, wingMat);
this.wingL.rotation.x = -Math.PI / 2;
this.wingL.position.set(0.5, -0.1, 0.3);
this.group.add(this.wingL);
const wingR = new THREE.Mesh(wingGeoL.clone(), wingMat);
wingR.rotation.x = Math.PI / 2;
wingR.rotation.z = Math.PI;
wingR.position.set(-0.5, -0.1, 0.3);
this.group.add(wingR);
// Engine glow (emissive sphere)
const engineGeo = new THREE.SphereGeometry(0.4, 6, 6);
const engineMat = new THREE.MeshBasicMaterial({ color: COLORS.engineGlow });
this.engineMesh = new THREE.Mesh(engineGeo, engineMat);
this.engineMesh.position.set(0, 0, 2);
this.group.add(this.engineMesh);
// Engine point light
this.engineLight = new THREE.PointLight(COLORS.engineGlow.getHex(), 3, 15);
this.engineLight.position.copy(this.engineMesh.position);
this.group.add(this.engineLight);
}
update(dt) {
// Smooth speed transition
const accelRate = 40;
if (this.speed < this.targetSpeed) {
this.speed += accelRate * dt;
} else if (this.speed > this.targetSpeed) {
this.speed -= accelRate * 2 * dt;
}
// Apply velocity from input
const moveX = this.velocity.x * dt;
const moveY = this.velocity.y * dt;
this.position.x += moveX;
this.position.y += moveY;
// Banking (roll) based on horizontal movement
const targetRoll = -this.velocity.x * 0.04;
this.rollAngle += (targetRoll - this.rollAngle) * Math.min(1, dt * 8);
// Pitch based on vertical movement
const targetPitch = this.velocity.y * 0.03;
this.pitchAngle += (targetPitch - this.pitchAngle) * Math.min(1, dt * 8);
// Apply rotation to group
this.group.position.copy(this.position);
this.group.rotation.z = this.rollAngle;
this.group.rotation.x = this.pitchAngle;
// Engine glow pulse based on speed
const speedRatio = (this.speed - PLAYER_SPEED_BASE) / (PLAYER_MAX_SPEED - PLAYER_SPEED_BASE);
const engineScale = 1 + speedRatio * 0.5;
this.engineMesh.scale.setScalar(engineScale);
this.engineLight.intensity = 2 + speedRatio * 4;
}
}
// ── class: ParticleSystem ──
class ParticleSystem {
constructor(scene) {
this.scene = scene;
this.trailParticles = [];
this.speedDust = [];
this.maxTrail = 60;
this.maxDust = 80;
// Trail particle pool
const trailGeo = new THREE.SphereGeometry(0.15, 4, 4);
const trailMat = new THREE.MeshBasicMaterial({ color: COLORS.engineGlow });
for (let i = 0; i < this.maxTrail; i++) {
const p = new THREE.Mesh(trailGeo, trailMat.clone());
p.visible = false;
scene.add(p);
this.trailParticles.push({ mesh: p, life: 0, maxLife: 1 });
}
// Speed dust pool
const dustGeo = new THREE.BoxGeometry(0.2, 0.2, 0.8);
const dustMat = new THREE.MeshBasicMaterial({ color: 0xffd700, transparent: true, opacity: 0.6 });
for (let i = 0; i < this.maxDust; i++) {
const p = new THREE.Mesh(dustGeo, dustMat.clone());
p.visible = false;
scene.add(p);
this.speedDust.push({ mesh: p, life: 0, maxLife: 1, vx: 0, vy: 0 });
}
this.trailTimer = 0;
this.dustTimer = 0;
}
emitTrail(pos) {
const particle = this.trailParticles.find(p => !p.mesh.visible);
if (!particle) return;
particle.mesh.position.copy(pos);
particle.mesh.position.x += (Math.random() - 0.5) * 0.3;
particle.mesh.position.y += (Math.random() - 0.5) * 0.3;
particle.life = 0;
particle.maxLife = 0.6 + Math.random() * 0.4;
particle.mesh.visible = true;
particle.mesh.scale.setScalar(1);
}
emitDust(pos, speedRatio) {
const particle = this.speedDust.find(p => !p.mesh.visible);
if (!particle) return;
particle.mesh.position.copy(pos);
particle.mesh.position.x += (Math.random() - 0.5) * 15;
particle.mesh.position.y += (Math.random() - 0.5) * 12;
particle.life = 0;
particle.maxLife = 0.3 + Math.random() * 0.5;
particle.vx = (Math.random() - 0.5) * 2;
particle.vy = (Math.random() - 0.5) * 2;
particle.mesh.visible = true;
const s = 1 + speedRatio * 3;
particle.mesh.scale.set(1, 1, s);
particle.mesh.material.opacity = 0.4 + Math.random() * 0.3;
}
update(dt) {
// Update trail particles
for (const p of this.trailParticles) {
if (!p.mesh.visible) continue;
p.life += dt;
const t = p.life / p.maxLife;
if (t >= 1) {
p.mesh.visible = false;
continue;
}
p.mesh.material.opacity = 1 - t;
p.mesh.scale.setScalar(1 - t * 0.5);
}
// Update dust particles
for (const p of this.speedDust) {
if (!p.mesh.visible) continue;
p.life += dt;
const t = p.life / p.maxLife;
if (t >= 1) {
p.mesh.visible = false;
continue;
}
p.mesh.position.x += p.vx * dt;
p.mesh.position.y += p.vy * dt;
p.mesh.material.opacity = (1 - t) * 0.6;
}
}
}
// ── class: CollisionDetector ──
class CollisionDetector {
constructor() {
this.nearMissCooldown = 0;
}
check(playerPos, playerRadius, chunkGroups) {
let collision = false;
let nearMiss = false;
let minDist = Infinity;
for (const [idx, group] of chunkGroups) {
const worldZOffset = group.position.z;
// Quick Z-range cull
if (Math.abs(playerPos.z - worldZOffset) > CHUNK_SIZE * 0.6) continue;
for (const col of (group.userData.colliders || [])) {
// AABB vs sphere check
const dx = Math.max(0, Math.abs(playerPos.x - col.x) - col.w);
const dy = Math.max(0, Math.abs(playerPos.y - col.y) - col.h);
const dz = Math.max(0, Math.abs(playerPos.z - (col.z + worldZOffset)) - col.d);
const distSq = dx * dx + dy * dy + dz * dz;
const dist = Math.sqrt(distSq);
if (dist < playerRadius) {
collision = true;
}
if (dist < NEAR_MISS_DIST && dist > playerRadius) {
nearMiss = true;
minDist = Math.min(minDist, dist);
}
}
// Check orbs for collection
group.traverse(child => {
if (!child.userData.isOrb) return;
const worldPos = new THREE.Vector3();
child.getWorldPosition(worldPos);
const orbDist = playerPos.distanceTo(worldPos);
if (orbDist < 2.5) {
child.userData.collected = true;
// Hide the orb and its children
child.visible = false;
child.children.forEach(c => c.visible = false);
}
});
}
return { collision, nearMiss, minDist };
}
}
// ── class: CanyonGenerator ──
class CanyonGenerator {
constructor(scene) {
this.scene = scene;
this.chunks = new Map();
this.nextChunkZ = 0;
this.rngSeed = Math.random() * 999999;
}
seededRandom() {
this.rngSeed = (this.rngSeed * 16807 + 0) % 2147483647;
return (this.rngSeed - 1) / 2147483646;
}
getChunkIndex(z) {
return Math.floor((z + CHUNK_SIZE * CHUNKS_AHEAD) / CHUNK_SIZE);
}
generateChunk(chunkZ, difficulty) {
const group = new THREE.Group();
group.position.z = chunkZ;
const colliders = [];
const baseWidth = Math.max(12, 30 - (difficulty || 0) * 2);
const canyonType = this.seededRandom();
if (canyonType < 0.5) {
this.buildCanyonWalls(group, colliders, chunkZ, baseWidth, difficulty || 0);
} else if (canyonType < 0.8) {
this.buildMonoliths(group, colliders, chunkZ, baseWidth, difficulty || 0);
} else {
this.buildTightSqueeze(group, colliders, chunkZ, baseWidth, difficulty || 0);
}
this.addOrbs(group, chunkZ, baseWidth);
group.userData.colliders = colliders;
return group;
}
buildCanyonWalls(group, colliders, chunkZ, width, difficulty) {
const wallMats = COLORS.canyonWall.map(c => new THREE.MeshLambertMaterial({ color: c, flatShading: true }));
for (let z = 0; z < CHUNK_SIZE; z += 15) {
const w = width + Math.sin(z * 0.05 + chunkZ * 0.01) * 8;
const heightL = 20 + this.seededRandom() * 15;
const heightR = 20 + this.seededRandom() * 15;
const lwGeo = new THREE.BoxGeometry(4, heightL, 16);
const lWall = new THREE.Mesh(lwGeo, wallMats[Math.floor(this.seededRandom() * 3)]);
lWall.position.set(-w / 2 - 2, heightL / 2 - 5, z - CHUNK_SIZE / 2);
group.add(lWall);
const rwGeo = new THREE.BoxGeometry(4, heightR, 16);
const rWall = new THREE.Mesh(rwGeo, wallMats[Math.floor(this.seededRandom() * 3)]);
rWall.position.set(w / 2 + 2, heightR / 2 - 5, z - CHUNK_SIZE / 2);
group.add(rWall);
const groundGeo = new THREE.BoxGeometry(width, 1, 16);
const groundMat = new THREE.MeshLambertMaterial({ color: 0x2a1530, flatShading: true });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.position.set(0, -8, z - CHUNK_SIZE / 2);
group.add(ground);
if (this.seededRandom() > 0.4) {
const ceilGeo = new THREE.BoxGeometry(width + 10, 3, 16);
const ceilMat = new THREE.MeshLambertMaterial({ color: 0x2a1530, flatShading: true });
const ceiling = new THREE.Mesh(ceilGeo, ceilMat);
ceiling.position.set(0, 18 + this.seededRandom() * 5, z - CHUNK_SIZE / 2);
group.add(ceiling);
}
colliders.push({ x: -w/2 - 2, y: heightL/2 - 5, z: z - CHUNK_SIZE/2, w: 4, h: heightL, d: 16 });
colliders.push({ x: w/2 + 2, y: heightR/2 - 5, z: z - CHUNK_SIZE/2, w: 4, h: heightR, d: 16 });
}
}
buildMonoliths(group, colliders, chunkZ, width, difficulty) {
const count = Math.floor(3 + (difficulty || 0) * 2);
for (let i = 0; i < count; i++) {
const zPos = this.seededRandom() * CHUNK_SIZE - CHUNK_SIZE / 2;
const xPos = (this.seededRandom() - 0.5) * width * 0.7;
const yPos = (this.seededRandom() - 0.3) * 15;
const h = 8 + this.seededRandom() * 12;
const w = 2 + this.seededRandom() * 3;
const d = 2 + this.seededRandom() * 3;
const geo = new THREE.BoxGeometry(w, h, d);
const mat = new THREE.MeshLambertMaterial({ color: COLORS.monolith, flatShading: true });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(xPos, yPos, zPos);
group.add(mesh);
const edgeGeo = new THREE.BoxGeometry(w + 0.2, h + 0.2, d + 0.2);
const edges = new THREE.EdgesGeometry(edgeGeo);
const lineMat = new THREE.LineBasicMaterial({ color: 0xff6b35 });
const wireframe = new THREE.LineSegments(edges, lineMat);
mesh.add(wireframe);
colliders.push({ x: xPos, y: yPos, z: zPos, w: w/2 + COLLISION_RADIUS * 0.5, h: h/2, d: d/2 });
}
const groundGeo = new THREE.BoxGeometry(width, 1, CHUNK_SIZE);
const groundMat = new THREE.MeshLambertMaterial({ color: 0x2a1530, flatShading: true });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.position.set(0, -8, 0);
group.add(ground);
for (let side of [-1, 1]) {
const wallGeo = new THREE.BoxGeometry(2, 30, CHUNK_SIZE);
const wallMat = new THREE.MeshLambertMaterial({ color: COLORS.canyonWall[0], flatShading: true });
const wall = new THREE.Mesh(wallGeo, wallMat);
wall.position.set(side * (width/2 + 1), 5, 0);
group.add(wall);
}
}
buildTightSqueeze(group, colliders, chunkZ, width, difficulty) {
const squeezeWidth = Math.max(6, width * 0.4);
for (let side of [-1, 1]) {
const wallGeo = new THREE.BoxGeometry(3, 25, CHUNK_SIZE);
const wallMat = new THREE.MeshLambertMaterial({ color: COLORS.squeeze, flatShading: true });
const wall = new THREE.Mesh(wallGeo, wallMat);
wall.position.set(side * squeezeWidth / 2, 3, 0);
group.add(wall);
for (let z = 10; z < CHUNK_SIZE - 10; z += 25) {
const protrudeW = 1 + this.seededRandom() * 2;
const pGeo = new THREE.BoxGeometry(protrudeW, 3 + this.seededRandom() * 4, 8);
const pMat = new THREE.MeshLambertMaterial({ color: COLORS.squeeze, flatShading: true });
const protrusion = new THREE.Mesh(pGeo, pMat);
protrusion.position.set(side * (squeezeWidth / 2 - protrudeW / 2), this.seededRandom() * 10 - 3, z - CHUNK_SIZE / 2);
group.add(protrusion);
colliders.push({
x: side * (squeezeWidth/2 - protrudeW/2),
y: protrusion.position.y,
z: z - CHUNK_SIZE/2,
w: protrudeW/2 + COLLISION_RADIUS * 0.3,
h: 2, d: 4
});
}
colliders.push({ x: side * squeezeWidth/2, y: 3, z: 0, w: 1.5, h: 12.5, d: CHUNK_SIZE/2 });
}
const groundGeo = new THREE.BoxGeometry(width, 1, CHUNK_SIZE);
const groundMat = new THREE.MeshLambertMaterial({ color: 0x2a1530, flatShading: true });
group.add(new THREE.Mesh(groundGeo, groundMat)).position.set(0, -8, 0);
const ceilGeo = new THREE.BoxGeometry(squeezeWidth + 4, 2, CHUNK_SIZE);
const ceilMat = new THREE.MeshLambertMaterial({ color: 0x3d1f3c, flatShading: true });
group.add(new THREE.Mesh(ceilGeo, ceilMat)).position.set(0, 16, 0);
colliders.push({ x: 0, y: 15, z: 0, w: squeezeWidth/2 + 2, h: 1, d: CHUNK_SIZE/2 });
}
addOrbs(group, chunkZ, width) {
const orbCount = Math.floor(4 + this.seededRandom() * 3);
for (let i = 0; i < orbCount; i++) {
const zPos = this.seededRandom() * CHUNK_SIZE - CHUNK_SIZE / 2;
const xPos = (this.seededRandom() - 0.5) * width * 0.6;
const yPos = (this.seededRandom() - 0.3) * 12;
const orbGeo = new THREE.OctahedronGeometry(0.8, 0);
const orbMat = new THREE.MeshBasicMaterial({ color: COLORS.orb });
const orb = new THREE.Mesh(orbGeo, orbMat);
orb.position.set(xPos, yPos, zPos);
orb.userData.isOrb = true;
group.add(orb);
const glowGeo = new THREE.SphereGeometry(1.5, 6, 6);
const glowMat = new THREE.MeshBasicMaterial({ color: COLORS.orb, transparent: true, opacity: 0.2 });
const glow = new THREE.Mesh(glowGeo, glowMat);
orb.add(glow);
const orbLight = new THREE.PointLight(COLORS.orb.getHex(), 1, 8);
orb.add(orbLight);
}
}
update(playerZ) {
while (this.nextChunkZ <= playerZ + CHUNK_SIZE * CHUNKS_AHEAD) {
const idx = this.getChunkIndex(this.nextChunkZ);
if (!this.chunks.has(idx)) {
const group = this.generateChunk(this.nextChunkZ, 0);
this.scene.add(group);
this.chunks.set(idx, group);
}
this.nextChunkZ += CHUNK_SIZE;
}
for (const [idx, grp] of this.chunks) {
if (grp.position.z < playerZ - CHUNK_SIZE * CHUNKS_BEHIND) {
this.scene.remove(grp);
this.disposeGroup(grp);
this.chunks.delete(idx);
}
}
}
disposeGroup(group) {
group.traverse(child => {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(m => m.dispose());
} else {
child.material.dispose();
}
}
});
}
}
// ── class: Game ──
class Game {
constructor() {
this.state = 'menu';
this.score = 0;
this.maxSpeed = 0;
this.energy = ENERGY_MAX;
this.turbo = 0;
this.multiplier = 1;
this.multiplierTimer = 0;
this.difficulty = 0;
this.cameraShake = { x: 0, y: 0, intensity: 0 };
this.fovTarget = 75;
this.boostActive = false;
this.keys = {};
// Three.js setup
this.canvas = document.getElementById('game-canvas');
this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, antialias: true });
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.scene.fog = new THREE.FogExp2(0x1a0533, 0.008);
// Lighting
const ambientLight = new THREE.AmbientLight(0xff9966, 0.4);
this.scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffaa44, 1.2);
dirLight.position.set(30, 50, -20);
this.scene.add(dirLight);
const hemiLight = new THREE.HemisphereLight(COLORS.skyBottom.getHex(), COLORS.skyTop.getHex(), 0.6);
this.scene.add(hemiLight);
// Background gradient sphere
const bgGeo = new THREE.SphereGeometry(400, 16, 16);
const bgMat = new THREE.ShaderMaterial({
side: THREE.BackSide,
uniforms: {
topColor: { value: COLORS.skyTop },
bottomColor: { value: COLORS.skyBottom }
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPos.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition).y * 0.5 + 0.5;
gl_FragColor = vec4(mix(bottomColor, topColor, h), 1.0);
}
`
});
this.scene.add(new THREE.Mesh(bgGeo, bgMat));
// Systems
this.audio = new AudioSystem();
this.player = new PlayerCraft(this.scene);
this.generator = new CanyonGenerator(this.scene);
this.particles = new ParticleSystem(this.scene);
this.collisionDetector = new CollisionDetector();
// HUD elements
this.hudSpeedFill = document.getElementById('speed-fill');
this.hudMultiplier = document.getElementById('multiplier');
this.hudEnergyFill = document.getElementById('energy-fill');
this.hudTurboFill = document.getElementById('turbo-fill');
this.hudScoreDisplay = document.getElementById('score-display');
this.screenGameOver = document.getElementById('game-over');
this.screenStart = document.getElementById('start-screen');
// Events
window.addEventListener('resize', () => this.onResize());
window.addEventListener('keydown', e => { this.keys[e.code] = true; });
window.addEventListener('keyup', e => { this.keys[e.code] = false; });
document.getElementById('start-btn').addEventListener('click', () => this.startGame());
document.getElementById('restart-btn').addEventListener('click', () => this.restartGame());
this.onResize();
this.lastTime = performance.now();
this.animate();
}
onResize() {
const w = window.innerWidth;
const h = window.innerHeight;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
}
startGame() {
this.audio.init();
this.screenStart.style.display = 'none';
this.resetGameState();
this.state = 'playing';
}
restartGame() {
this.screenGameOver.style.display = 'none';
for (const [idx, grp] of this.generator.chunks) {
this.scene.remove(grp);
this.generator.disposeGroup(grp);
}
this.generator.chunks.clear();
this.generator.nextChunkZ = 0;
this.resetGameState();
this.state = 'playing';
}
resetGameState() {
this.score = 0;
this.maxSpeed = 0;
this.energy = ENERGY_MAX;
this.turbo = 0;
this.multiplier = 1;
this.difficulty = 0;
this.boostActive = false;
this.player.position.set(0, 0, 0);
this.player.velocity.set(0, 0, 0);
this.player.speed = PLAYER_SPEED_BASE;
this.player.targetSpeed = PLAYER_SPEED_BASE;
this.player.rollAngle = 0;
this.player.pitchAngle = 0;
this.camera.position.set(0, 3, 12);
this.camera.lookAt(0, 0, -50);
this.camera.fov = 75;
this.camera.updateProjectionMatrix();
}
handleInput(dt) {
const turnSpeed = TURN_RATE * (this.player.speed / PLAYER_SPEED_BASE);
if (this.keys['ArrowLeft'] || this.keys['KeyA']) {
this.player.velocity.x -= turnSpeed * dt;
}
if (this.keys['ArrowRight'] || this.keys['KeyD']) {
this.player.velocity.x += turnSpeed * dt;
}
if (this.keys['ArrowUp'] || this.keys['KeyW']) {
this.player.velocity.y += turnSpeed * dt;
}
if (this.keys['ArrowDown'] || this.keys['KeyS']) {
this.player.velocity.y -= turnSpeed * dt;
}
const damping = Math.exp(-5 * dt);
this.player.velocity.x *= damping;
this.player.velocity.y *= damping;
// Clamp position to canyon bounds (soft)
if (this.player.position.x > 20) { this.player.position.x = 20; this.player.velocity.x *= -0.5; }
if (this.player.position.x < -20) { this.player.position.x = -20; this.player.velocity.x *= -0.5; }
if (this.player.position.y > 12) { this.player.position.y = 12; this.player.velocity.y *= -0.5; }
if (this.player.position.y < -6) { this.player.position.y = -6; this.player.velocity.y *= -0.5; }
// Boost
const boosting = this.keys['Space'] && this.turbo > 10;
if (boosting) {
this.turbo -= 30 * dt;
this.player.targetSpeed = PLAYER_MAX_SPEED * BOOST_MULTIPLIER;
this.fovTarget = 95;
if (!this.boostActive) {
this.audio.playBoost();
this.boostActive = true;
}
} else {
this.player.targetSpeed = Math.min(PLAYER_MAX_SPEED, PLAYER_SPEED_BASE + this.difficulty * 8);
this.fovTarget = 75;
this.boostActive = false;
// Build turbo on straightaways
if (Math.abs(this.player.velocity.x) < 1 && Math.abs(this.player.velocity.y) < 1) {
this.turbo = Math.min(100, this.turbo + 8 * dt);
}
}
this.turbo = Math.max(0, Math.min(100, this.turbo));
}
updateGame(dt) {
if (this.state !== 'playing') return;
const speedRatio = (this.player.speed - PLAYER_SPEED_BASE) / (PLAYER_MAX_SPEED * BOOST_MULTIPLIER - PLAYER_SPEED_BASE);
// Move player forward
this.player.position.z -= this.player.speed * dt;
if (this.player.speed > this.maxSpeed) {
this.maxSpeed = this.player.speed;
}
this.handleInput(dt);
this.player.update(dt);
// Energy decay
this.energy -= ENERGY_DECAY * dt;
if (this.energy <= 0) {
this.gameOver();
return;
}
// Difficulty scaling
this.difficulty = Math.min(15, this.score / 500);
// Update canyon generator
this.generator.update(this.player.position.z);
// Collision detection & orb collection
const result = this.collisionDetector.check(
this.player.position, COLLISION_RADIUS, this.generator.chunks
);
if (result.collision) {
this.audio.playCrash();
this.gameOver();
return;
}
// Check for collected orbs and award energy/score
let orbsCollected = 0;
for (const [idx, group] of this.generator.chunks) {
group.traverse(child => {
if (child.userData.isOrb && child.userData.collected) {
orbsCollected++;
}
});
}
if (orbsCollected > 0) {
this.energy = Math.min(ENERGY_MAX, this.energy + orbsCollected * ORB_ENERGY);
this.score += orbsCollected * 25;
this.audio.playCollect();
}
// Near miss bonus
if (result.nearMiss && this.multiplierTimer <= 0) {
this.multiplier = Math.min(10, this.multiplier + 0.5);
this.multiplierTimer = 2;
this.score += 50 * this.multiplier;
this.cameraShake.intensity = 0.3;
this.audio.playNearMiss();
}
// Multiplier decay
if (this.multiplierTimer > 0) {
this.multiplierTimer -= dt;
if (this.multiplierTimer <= 0) {
this.multiplier = Math.max(1, this.multiplier - 0.5);
}
}
// Score from distance traveled
this.score += this.player.speed * dt * 0.1 * this.multiplier;
// Camera follow with shake
const camOffsetX = this.player.position.x * 0.4 + this.cameraShake.x;
const camOffsetY = 3 + this.player.position.y * 0.3 + this.cameraShake.y;
this.camera.position.set(camOffsetX, camOffsetY, this.player.position.z + 12);
this.camera.lookAt(this.player.position.x * 0.5, this.player.position.y * 0.5, this.player.position.z - 60);
// FOV transition
this.camera.fov += (this.fovTarget - this.camera.fov) * dt * 3;
this.camera.updateProjectionMatrix();
// Camera shake decay
if (this.cameraShake.intensity > 0) {
this.cameraShake.x = (Math.random() - 0.5) * this.cameraShake.intensity;
this.cameraShake.y = (Math.random() - 0.5) * this.cameraShake.intensity;
this.cameraShake.intensity *= Math.exp(-8 * dt);
} else {
this.cameraShake.x = 0;
this.cameraShake.y = 0;
}
// Particles - trail
this.particles.trailTimer += dt;
if (this.particles.trailTimer > 0.02) {
const trailPos = new THREE.Vector3(
this.player.position.x,
this.player.position.y,
this.player.position.z + 2
);
this.particles.emitTrail(trailPos);
this.particles.trailTimer = 0;
}
// Particles - speed dust (more frequent at higher speeds)
this.particles.dustTimer += dt;
const dustInterval = Math.max(0.01, 0.05 - speedRatio * 0.04);
if (this.particles.dustTimer > dustInterval) {
const dustPos = new THREE.Vector3(
this.player.position.x,
this.player.position.y,
this.player.position.z + 8
);
this.particles.emitDust(dustPos, speedRatio);
this.particles.dustTimer = 0;
}
this.particles.update(dt);
this.audio.setEnginePitch(speedRatio);
// Animate orbs (rotation)
for (const [idx, group] of this.generator.chunks) {
group.traverse(child => {
if (child.userData.isOrb && child.visible) {
child.rotation.y += dt * 3;
child.rotation.x += dt * 1.5;
}
});
}
// Update HUD
this.updateHUD(speedRatio);
}
updateHUD(speedRatio) {
const speedPct = Math.min(1, Math.max(0, speedRatio));
this.hudSpeedFill.style.height = (speedPct * 100) + '%';
this.hudMultiplier.textContent = 'x' + this.multiplier.toFixed(1);
if (this.multiplierTimer > 0) {
this.hudMultiplier.style.transform = 'scale(' + (1 + Math.sin(Date.now() * 0.01) * 0.15) + ')';
this.hudMultiplier.style.color = '#ff6b35';
} else {
this.hudMultiplier.style.transform = 'scale(1)';
this.hudMultiplier.style.color = '#ffd700';
}
const energyPct = Math.max(0, this.energy / ENERGY_MAX);
this.hudEnergyFill.style.width = (energyPct * 100) + '%';
if (energyPct < 0.25) {
this.hudEnergyFill.style.background = 'linear-gradient(to right, #ff3d3d, #ff6b35)';
} else {
this.hudEnergyFill.style.background = 'linear-gradient(to right, #7209b7, #ff6b35)';
}
this.hudTurboFill.style.width = (this.turbo) + '%';
// Score display
if (this.hudScoreDisplay) {
this.hudScoreDisplay.textContent = Math.floor(this.score).toLocaleString();
}
}
gameOver() {
this.state = 'gameover';
document.getElementById('final-score').textContent = 'Score: ' + Math.floor(this.score).toLocaleString();
document.getElementById('max-speed').textContent = 'Max Speed: ' + Math.floor(this.maxSpeed);
this.screenGameOver.style.display = 'flex';
}
animate() {
requestAnimationFrame(() => this.animate());
const now = performance.now();
let dt = (now - this.lastTime) / 1000;
this.lastTime = now;
dt = Math.min(dt, 0.05);
if (this.state === 'playing') {
this.updateGame(dt);
} else if (this.state === 'menu') {
const t = now * 0.0001;
this.camera.position.set(Math.sin(t) * 20, 8 + Math.cos(t * 0.7) * 3, -now * 0.005);
this.camera.lookAt(0, 0, -this.camera.position.z - 60);
if (this.generator.chunks.size < 4 || true) {
this.generator.update(this.camera.position.z);
}
}
this.renderer.render(this.scene, this.camera);
}
}
// ── main ──
const game = new Game();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"97bbf2ee-db39-4f89-9d3b-3b56020f2acc","tokensIn":1916411,"tokensOut":31548,"tokensTotal":1947959,"turns":39,"toolCalls":38,"failedToolCalls":0,"timestamp":"2026-07-18T02:47:16.323Z"} -->