Canyon Run
Qwen 3.6 35b a3b · full
◈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 37,760 bytes · SHA-256 779a4da1f08c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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 {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10;
}
#score {
position: absolute; top: 30px; right: 40px;
color: #fff; font-size: 28px; letter-spacing: 4px;
text-shadow: 0 0 20px rgba(255,150,50,0.8);
transition: transform 0.1s;
}
#score.pulse { transform: scale(1.3); }
#multiplier {
position: absolute; top: 65px; right: 40px;
color: #ff9933; font-size: 42px; font-weight: bold;
text-shadow: 0 0 30px rgba(255,150,50,1);
opacity: 0; transition: opacity 0.3s;
}
#multiplier.visible { opacity: 1; }
#energy-container {
position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%);
width: 300px; height: 6px; background: rgba(255,255,255,0.1);
border-radius: 3px; overflow: hidden;
}
#energy-bar {
height: 100%; width: 100%; border-radius: 3px;
background: linear-gradient(90deg, #ff3366, #ff9933, #ffdd33);
box-shadow: 0 0 15px rgba(255,150,50,0.6);
transition: width 0.15s;
}
#energy-label {
position: absolute; bottom: 52px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.5); font-size: 10px; letter-spacing: 6px;
}
#speed-container {
position: absolute; bottom: 40px; right: 40px;
width: 120px; height: 80px;
}
#speed-line {
position: absolute; bottom: 10px; left: 0; right: 0; height: 2px;
background: rgba(255,255,255,0.15); border-radius: 1px;
}
#speed-fill {
position: absolute; bottom: 10px; left: 0; height: 2px;
background: linear-gradient(90deg, #ff6633, #ffdd33);
border-radius: 1px; box-shadow: 0 0 8px rgba(255,100,50,0.5);
transition: width 0.1s;
}
#speed-value {
position: absolute; bottom: 22px; left: 50%; transform: translateX(-50%);
color: #fff; font-size: 16px; letter-spacing: 2px;
text-shadow: 0 0 10px rgba(255,150,50,0.5);
}
#speed-unit {
position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.3); font-size: 9px; letter-spacing: 3px;
}
#boost-container {
position: absolute; bottom: 55px; left: 40px;
width: 8px; height: 120px; background: rgba(255,255,255,0.05);
border-radius: 4px; overflow: hidden;
}
#boost-fill {
position: absolute; bottom: 0; width: 100%;
background: linear-gradient(0deg, #3366ff, #66ccff);
border-radius: 4px; box-shadow: 0 0 10px rgba(100,150,255,0.5);
transition: height 0.15s;
}
#boost-label {
position: absolute; bottom: 30px; left: 40px;
color: rgba(100,150,255,0.4); font-size: 9px; letter-spacing: 4px;
transform: rotate(-90deg); transform-origin: left center;
}
#game-over {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.85); display: none; z-index: 100;
flex-direction: column; align-items: center; justify-content: center;
}
#game-over.active { display: flex; }
#game-over h1 {
color: #ff6633; font-size: 64px; letter-spacing: 12px;
text-shadow: 0 0 40px rgba(255,100,50,0.8); margin-bottom: 40px;
}
#game-over .stat {
color: rgba(255,255,255,0.6); font-size: 16px; letter-spacing: 4px;
margin: 8px 0;
}
#game-over .stat span { color: #ffdd33; font-size: 24px; }
#restart-btn {
margin-top: 50px; padding: 15px 50px;
background: transparent; border: 2px solid #ff6633;
color: #ff6633; font-family: 'Courier New', monospace;
font-size: 18px; letter-spacing: 6px; cursor: pointer;
pointer-events: all; transition: all 0.3s;
}
#restart-btn:hover {
background: #ff6633; color: #000;
box-shadow: 0 0 30px rgba(255,100,50,0.5);
}
#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: 100;
}
#start-screen h1 {
color: #ff6633; font-size: 72px; letter-spacing: 16px;
text-shadow: 0 0 60px rgba(255,100,50,0.8); margin-bottom: 20px;
}
#start-screen .subtitle {
color: rgba(255,255,255,0.4); font-size: 14px; letter-spacing: 8px;
margin-bottom: 60px;
}
#start-screen .controls {
color: rgba(255,255,255,0.3); font-size: 12px; letter-spacing: 3px;
margin: 5px 0;
}
#start-screen .start-prompt {
color: rgba(255,200,100,0.6); font-size: 16px; letter-spacing: 6px;
margin-top: 60px; animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
</style>
</head>
<body>
<div id="hud">
<div id="score">0</div>
<div id="multiplier">×1</div>
<div id="energy-label">ENERGY</div>
<div id="energy-container"><div id="energy-bar"></div></div>
<div id="speed-container">
<div id="speed-value">0</div>
<div id="speed-line"></div>
<div id="speed-fill"></div>
<div id="speed-unit">KM/H</div>
</div>
<div id="boost-label">TURBO</div>
<div id="boost-container"><div id="boost-fill"></div></div>
</div>
<div id="game-over">
<h1>CRASHED</h1>
<div class="stat">SCORE <span id="final-score">0</span></div>
<div class="stat">MAX SPEED <span id="max-speed">0</span> KM/H</div>
<div class="stat">NEAR MISSES <span id="near-misses">0</span></div>
<button id="restart-btn">RESTART</button>
</div>
<div id="start-screen">
<h1>CANYON</h1>
<div class="subtitle">PROCEDURAL RACER</div>
<div class="controls">← → or A/D — STEER</div>
<div class="controls">↑ ↓ or W/S — PITCH</div>
<div class="controls">SHIFT — BOOST</div>
<div class="start-prompt">PRESS ENTER TO START</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.163.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
// ─── CONFIG ───
const CFG = {
chunkSize: 120,
chunkCount: 8,
baseSpeed: 80,
maxSpeed: 250,
boostSpeed: 400,
playerZ: 0,
wallWidth: 180,
wallMinHeight: 30,
wallMaxHeight: 120,
energyMax: 100,
energyDecay: 3.5,
energyRecharge: 18,
multiplierDecay: 0.3,
boostMax: 100,
boostDrain: 30,
boostRecharge: 5,
cameraShakeIntensity: 0.08,
nearMissDist: 12,
orbSpawnChance: 0.08,
obstacleSpawnChance: 0.04,
fogNear: 60,
fogFar: 400,
};
// ─── STATE ───
let state = {
running: false,
score: 0,
multiplier: 1,
energy: CFG.energyMax,
boost: 0,
speed: CFG.baseSpeed,
maxSpeedReached: 0,
nearMisses: 0,
playerPos: new THREE.Vector3(0, 10, 0),
playerVel: new THREE.Vector3(0, 0, 0),
playerRot: new THREE.Euler(0, 0, 0),
targetRot: new THREE.Euler(0, 0, 0),
playerBank: 0,
targetBank: 0,
lastTime: 0,
chunks: new Map(),
orbs: [],
debrisParticles: [],
trailParticles: [],
audioCtx: null,
engineOsc: null,
engineGain: null,
gameStartTime: 0,
};
// ─── AUDIO ───
function initAudio() {
state.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// Engine hum
state.engineOsc = state.audioCtx.createOscillator();
state.engineGain = state.audioCtx.createGain();
const filter = state.audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 400;
state.engineOsc.type = 'sawtooth';
state.engineOsc.frequency.value = 55;
state.engineGain.gain.value = 0.04;
state.engineOsc.connect(filter);
filter.connect(state.engineGain);
state.engineGain.connect(state.audioCtx.destination);
state.engineOsc.start();
}
function playPing() {
if (!state.audioCtx) return;
const ctx = state.audioCtx;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(880, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1760, ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.15, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.3);
}
function playNearMiss() {
if (!state.audioCtx) return;
const ctx = state.audioCtx;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, ctx.currentTime + 0.2);
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.25);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.25);
}
function playCrash() {
if (!state.audioCtx) return;
const ctx = state.audioCtx;
const bufferSize = ctx.sampleRate * 0.5;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.1));
}
const source = ctx.createBufferSource();
source.buffer = buffer;
const gain = ctx.createGain();
gain.gain.value = 0.2;
source.connect(gain);
gain.connect(ctx.destination);
source.start();
}
function updateEngineSound() {
if (!state.engineOsc || !state.audioCtx) return;
const speedRatio = state.speed / CFG.boostSpeed;
state.engineOsc.frequency.setTargetAtTime(
55 + speedRatio * 120, state.audioCtx.currentTime, 0.1
);
state.engineGain.gain.setTargetAtTime(
0.03 + speedRatio * 0.06, state.audioCtx.currentTime, 0.1
);
}
// ─── THREE.JS SETUP ───
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 500);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
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.1;
document.body.appendChild(renderer.domElement);
// Post-processing
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.2, 0.4, 0.85
);
composer.addPass(bloomPass);
// Custom motion blur shader
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 ShaderPass(MotionBlurShader);
motionBlurPass.uniforms.intensity.value = 0.15;
composer.addPass(motionBlurPass);
// ─── LIGHTING ───
const sunLight = new THREE.DirectionalLight(0xff8833, 2.5);
sunLight.position.set(50, 80, -100);
sunLight.castShadow = true;
sunLight.shadow.mapSize.set(2048, 2048);
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 300;
sunLight.shadow.camera.left = -100;
sunLight.shadow.camera.right = 100;
sunLight.shadow.camera.top = 100;
sunLight.shadow.camera.bottom = -100;
scene.add(sunLight);
const ambientLight = new THREE.AmbientLight(0x443355, 0.6);
scene.add(ambientLight);
const hemiLight = new THREE.HemisphereLight(0xff9944, 0x332244, 0.8);
scene.add(hemiLight);
// Rim light
const rimLight = new THREE.DirectionalLight(0xff4422, 1.0);
rimLight.position.set(-30, 20, -50);
scene.add(rimLight);
// ─── FOG ───
scene.fog = new THREE.Fog(0x1a0a2e, CFG.fogNear, CFG.fogFar);
// ─── MATERIALS ───
const wallMat = new THREE.MeshStandardMaterial({
color: 0x2a1a3e,
roughness: 0.85,
metalness: 0.1,
flatShading: true,
});
const wallMat2 = new THREE.MeshStandardMaterial({
color: 0x3a2255,
roughness: 0.8,
metalness: 0.15,
flatShading: true,
});
const floorMat = new THREE.MeshStandardMaterial({
color: 0x1a0a2e,
roughness: 0.9,
metalness: 0.05,
flatShading: true,
});
const ceilingMat = new THREE.MeshStandardMaterial({
color: 0x2a1540,
roughness: 0.85,
metalness: 0.1,
flatShading: true,
});
const orbMat = new THREE.MeshStandardMaterial({
color: 0xffdd33,
emissive: 0xffaa00,
emissiveIntensity: 2.0,
roughness: 0.2,
metalness: 0.8,
});
const orbMat2 = new THREE.MeshStandardMaterial({
color: 0x33ffaa,
emissive: 0x00ff88,
emissiveIntensity: 2.0,
roughness: 0.2,
metalness: 0.8,
});
const playerMat = new THREE.MeshStandardMaterial({
color: 0xccccdd,
emissive: 0x2244aa,
emissiveIntensity: 0.3,
roughness: 0.3,
metalness: 0.7,
});
const playerGlowMat = new THREE.MeshStandardMaterial({
color: 0x3366ff,
emissive: 0x3366ff,
emissiveIntensity: 3.0,
roughness: 0.1,
metalness: 0.9,
});
const obstacleMat = new THREE.MeshStandardMaterial({
color: 0x553366,
roughness: 0.7,
metalness: 0.2,
flatShading: true,
});
const obstacleMat2 = new THREE.MeshStandardMaterial({
color: 0x664477,
roughness: 0.65,
metalness: 0.25,
flatShading: true,
});
// ─── PLAYER CRAFT ───
function createPlayerCraft() {
const group = new THREE.Group();
// Main body
const bodyGeo = new THREE.ConeGeometry(0.8, 4, 4);
bodyGeo.rotateX(Math.PI / 2);
const body = new THREE.Mesh(bodyGeo, playerMat);
body.castShadow = true;
group.add(body);
// Cockpit
const cockpitGeo = new THREE.BoxGeometry(0.6, 0.5, 1.2);
cockpitGeo.translate(0, 0.3, -0.3);
const cockpit = new THREE.Mesh(cockpitGeo, playerMat);
cockpit.castShadow = true;
group.add(cockpit);
// Wings
const wingGeo = new THREE.BoxGeometry(5, 0.08, 1.5);
const wing = new THREE.Mesh(wingGeo, playerMat);
wing.position.set(0, -0.1, 0.5);
wing.castShadow = true;
group.add(wing);
// Wing tips (glowing)
const tipGeo = new THREE.ConeGeometry(0.15, 0.6, 4);
tipGeo.rotateZ(Math.PI / 2);
const tipL = new THREE.Mesh(tipGeo, playerGlowMat);
tipL.position.set(-2.6, -0.1, 0.5);
group.add(tipL);
const tipR = new THREE.Mesh(tipGeo, playerGlowMat);
tipR.position.set(2.6, -0.1, 0.5);
group.add(tipR);
// Tail fins
const finGeo = new THREE.ConeGeometry(0.5, 1.2, 3);
finGeo.rotateX(-Math.PI / 2);
const fin1 = new THREE.Mesh(finGeo, playerMat);
fin1.position.set(0.5, 0.3, 1.8);
fin1.rotation.z = Math.PI / 6;
group.add(fin1);
const fin2 = new THREE.Mesh(finGeo, playerMat);
fin2.position.set(-0.5, 0.3, 1.8);
fin2.rotation.z = -Math.PI / 6;
group.add(fin2);
// Engine glow
const engineGeo = new THREE.CylinderGeometry(0.3, 0.6, 0.8, 6);
engineGeo.rotateX(Math.PI / 2);
const engine = new THREE.Mesh(engineGeo, playerGlowMat);
engine.position.set(0, 0, 2.2);
group.add(engine);
group.scale.set(1.2, 1.2, 1.2);
return group;
}
const player = createPlayerCraft();
scene.add(player);
// ─── PLAYER LIGHT ───
const playerLight = new THREE.PointLight(0x3366ff, 3, 30);
playerLight.position.set(0, 0, 2);
player.add(playerLight);
// ─── PROCEDURAL CANYON GENERATION ───
function seededRandom(seed) {
let s = seed;
return function() {
s = (s * 9301 + 49297) % 233280;
return s / 233280;
};
}
function generateCanyonSegment(zStart, zEnd, seed) {
const rng = seededRandom(seed);
const group = new THREE.Group();
const segments = 12;
const dz = (zEnd - zStart) / segments;
// Canyon width varies
let width = CFG.wallWidth * (0.5 + rng() * 0.8);
let leftX = -width / 2;
let rightX = width / 2;
let height = CFG.wallMinHeight + rng() * (CFG.wallMaxHeight - CFG.wallMinHeight);
for (let i = 0; i < segments; i++) {
const z0 = zStart + i * dz;
const z1 = z0 + dz;
// Vary canyon shape
width = CFG.wallWidth * (0.4 + rng() * 0.9);
leftX = -width / 2 + (rng() - 0.5) * 20;
rightX = width / 2 + (rng() - 0.5) * 20;
height = CFG.wallMinHeight + rng() * (CFG.wallMaxHeight - CFG.wallMinHeight);
const h0 = height + (rng() - 0.5) * 30;
const h1 = height + (rng() - 0.5) * 30;
// Left wall
const lwGeo = new THREE.BufferGeometry();
const lwVerts = new Float32Array([
leftX - 10, -15, z0, leftX, h0, z0, leftX, -h1, z1,
leftX - 10, -15, z0, leftX, -h1, z1, leftX - 10, -15, z1,
]);
lwGeo.setAttribute('position', new THREE.BufferAttribute(lwVerts, 3));
lwGeo.computeVertexNormals();
const lw = new THREE.Mesh(lwGeo, i % 2 === 0 ? wallMat : wallMat2);
lw.castShadow = true;
lw.receiveShadow = true;
group.add(lw);
// Right wall
const rwGeo = new THREE.BufferGeometry();
const rwVerts = new Float32Array([
rightX + 10, -15, z0, rightX, -h1, z1, rightX, h0, z1,
rightX + 10, -15, z0, rightX, h0, z0, rightX + 10, -15, z1,
]);
rwGeo.setAttribute('position', new THREE.BufferAttribute(rwVerts, 3));
rwGeo.computeVertexNormals();
const rw = new THREE.Mesh(rwGeo, i % 2 === 0 ? wallMat : wallMat2);
rw.castShadow = true;
rw.receiveShadow = true;
group.add(rw);
// Floor
const fGeo = new THREE.BufferGeometry();
const fVerts = new Float32Array([
leftX, -15, z0, rightX, -15, z0, rightX, -15, z1,
leftX, -15, z0, rightX, -15, z1, leftX, -15, z1,
]);
fGeo.setAttribute('position', new THREE.BufferAttribute(fVerts, 3));
fGeo.computeVertexNormals();
const floor = new THREE.Mesh(fGeo, floorMat);
floor.receiveShadow = true;
group.add(floor);
// Ceiling (sometimes)
if (rng() > 0.3) {
const ceilH = h0 * 0.3 + rng() * 20;
const ceilGeo = new THREE.BufferGeometry();
const cVerts = new Float32Array([
leftX, ceilH, z0, rightX, ceilH, z0, rightX, ceilH, z1,
leftX, ceilH, z0, rightX, ceilH, z1, leftX, ceilH, z1,
]);
ceilGeo.setAttribute('position', new THREE.BufferAttribute(cVerts, 3));
ceilGeo.computeVertexNormals();
const ceil = new THREE.Mesh(ceilGeo, ceilingMat);
ceil.receiveShadow = true;
group.add(ceil);
}
// Store wall positions for collision
group.userData.walls = group.userData.walls || [];
group.userData.walls.push({
z0, z1,
leftX: leftX - 5,
rightX: rightX + 5,
top: Math.max(h0, h1),
bottom: -15,
});
}
// Add obstacles
if (rng() > 0.3) {
const oz = zStart + dz * rng();
const oType = rng();
if (oType < 0.5) {
// Monolith
const monGeo = new THREE.BoxGeometry(
3 + rng() * 5,
15 + rng() * 30,
3 + rng() * 5
);
const mon = new THREE.Mesh(monGeo, rng() > 0.5 ? obstacleMat : obstacleMat2);
mon.position.set(
(rng() - 0.5) * width * 0.6,
-15 + (15 + rng() * 30) / 2,
oz
);
mon.castShadow = true;
mon.receiveShadow = true;
mon.userData.boundingSphere = {
center: mon.position.clone(),
radius: Math.max(mon.geometry.parameters.width, mon.geometry.parameters.height, mon.geometry.parameters.depth) / 2 * 1.2,
};
group.add(mon);
} else {
// Pillar cluster
for (let p = 0; p < 2 + Math.floor(rng() * 3); p++) {
const pGeo = new THREE.CylinderGeometry(1.5 + rng() * 2, 2 + rng() * 3, 20 + rng() * 25, 5);
const pillar = new THREE.Mesh(pGeo, obstacleMat2);
pillar.position.set(
(rng() - 0.5) * width * 0.5,
-15 + (20 + rng() * 25) / 2,
oz + (rng() - 0.5) * 10
);
pillar.castShadow = true;
pillar.userData.boundingSphere = {
center: pillar.position.clone(),
radius: 3 + rng() * 2,
};
group.add(pillar);
}
}
}
// Add energy orbs
if (rng() > 0.5) {
const orbZ = zStart + dz * rng();
const orbX = (rng() - 0.5) * width * 0.5;
const orbY = 5 + rng() * 25;
const orbGeo = new THREE.IcosahedronGeometry(1.2, 0);
const orb = new THREE.Mesh(orbGeo, rng() > 0.5 ? orbMat : orbMat2);
orb.position.set(orbX, orbY, orbZ);
orb.userData.boundingSphere = { center: orb.position.clone(), radius: 1.5 };
orb.userData.value = rng() > 0.7 ? 25 : 15;
group.add(orb);
state.orbs.push(orb);
}
return group;
}
// ─── PARTICLE SYSTEMS ───
function createDebrisParticle() {
const geo = new THREE.BufferGeometry();
const size = 0.1 + Math.random() * 0.3;
const positions = new Float32Array([0, 0, 0]);
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: new THREE.Color().setHSL(0.08 + Math.random() * 0.12, 0.8, 0.6),
size,
transparent: true,
opacity: 0.6,
depthWrite: false,
});
const points = new THREE.Points(geo, mat);
return points;
}
function initDebris() {
for (let i = 0; i < 80; i++) {
const p = createDebrisParticle();
resetDebrisParticle(p);
p.userData.alive = true;
scene.add(p);
state.debrisParticles.push(p);
}
}
function resetDebrisParticle(p) {
const spread = 40;
p.position.set(
(Math.random() - 0.5) * spread,
Math.random() * 40 - 10,
state.playerPos.z + 30 + Math.random() * 50
);
p.userData.speed = 0.5 + Math.random() * 1.5;
}
function updateDebris(dt) {
for (const p of state.debrisParticles) {
p.position.z -= state.speed * dt;
p.position.x += (Math.random() - 0.5) * 0.3;
if (p.position.z < state.playerPos.z - 20) {
resetDebrisParticle(p);
}
}
}
// Trail particles
function spawnTrailParticle() {
const geo = new THREE.BufferGeometry();
const size = 0.05 + Math.random() * 0.15;
const positions = new Float32Array([0, 0, 0]);
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: new THREE.Color().setHSL(0.6 + Math.random() * 0.1, 1, 0.7),
size,
transparent: true,
opacity: 0.8,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
const points = new THREE.Points(geo, mat);
points.position.set(
player.position.x + (Math.random() - 0.5) * 0.5,
player.position.y + (Math.random() - 0.5) * 0.3,
player.position.z + 2.5
);
points.userData.life = 0.5 + Math.random() * 0.5;
points.userData.maxLife = points.userData.life;
scene.add(points);
state.trailParticles.push(points);
}
function updateTrailParticles(dt) {
for (let i = state.trailParticles.length - 1; i >= 0; i--) {
const p = state.trailParticles[i];
p.userData.life -= dt;
p.position.z -= state.speed * dt * 0.3;
p.material.opacity = (p.userData.life / p.userData.maxLife) * 0.8;
if (p.userData.life <= 0) {
scene.remove(p);
state.trailParticles.splice(i, 1);
}
}
}
// ─── CHUNK MANAGEMENT ───
function generateChunk(zCenter) {
const key = Math.round(zCenter / CFG.chunkSize);
if (state.chunks.has(key)) return;
const zStart = zCenter - CFG.chunkSize / 2;
const zEnd = zCenter + CFG.chunkSize / 2;
const chunk = generateCanyonSegment(zStart, zEnd, key * 7919 + 104729);
chunk.userData.key = key;
chunk.userData.zCenter = zCenter;
scene.add(chunk);
state.chunks.set(key, chunk);
}
function removeChunk(key) {
const chunk = state.chunks.get(key);
if (!chunk) return;
scene.remove(chunk);
// Clean up orbs in this chunk
state.orbs = state.orbs.filter(o => {
if (o.parent === chunk) {
scene.remove(o);
o.geometry.dispose();
return false;
}
return true;
});
state.chunks.delete(key);
}
function updateChunks() {
const playerChunk = Math.round(state.playerPos.z / CFG.chunkSize);
// Generate ahead
for (let i = playerChunk + 1; i <= playerChunk + CFG.chunkCount; i++) {
generateChunk(i * CFG.chunkSize);
}
// Remove behind
for (const [key, chunk] of state.chunks) {
if (key < playerChunk - 2) {
removeChunk(key);
}
}
}
// ─── COLLISION DETECTION ───
function checkCollisions() {
const playerRadius = 2.0;
const playerSphere = {
center: state.playerPos.clone(),
radius: playerRadius,
};
// Check walls
let closestWallDist = Infinity;
for (const [key, chunk] of state.chunks) {
const walls = chunk.userData.walls;
if (!walls) continue;
for (const wall of walls) {
// Check if player is near this wall segment
if (state.playerPos.z < wall.z0 - 10 || state.playerPos.z > wall.z1 + 10) continue;
// Distance to left wall
const distL = state.playerPos.x - wall.leftX;
const distR = wall.rightX - state.playerPos.x;
closestWallDist = Math.min(closestWallDist, Math.min(distL, distR));
// Collision check
if (distL < playerRadius || distR < playerRadius) return true;
// Near-miss detection
if (distL < CFG.nearMissDist && distL > playerRadius) {
recordNearMiss(distL);
}
if (distR < CFG.nearMissDist && distR > playerRadius) {
recordNearMiss(distR);
}
}
}
// Check obstacles
for (const [key, chunk] of state.chunks) {
chunk.traverse(child => {
if (child.userData.boundingSphere) {
const bs = child.userData.boundingSphere;
const dist = playerSphere.center.distanceTo(bs.center);
if (dist < bs.radius + playerRadius) {
// Check if close enough to be a near miss
const clearance = dist - bs.radius;
if (clearance < CFG.nearMissDist && clearance > playerRadius) {
recordNearMiss(clearance);
}
return true; // collision
}
}
return false;
});
}
return false;
}
function recordNearMiss(dist) {
const intensity = 1 - (dist / CFG.nearMissDist);
if (intensity > 0.5) {
state.multiplier = Math.min(state.multiplier + 0.1 * intensity, 10);
state.nearMisses++;
state.boost = Math.min(state.boost + 3 * intensity, CFG.boostMax);
document.getElementById('multiplier').textContent = `×${state.multiplier.toFixed(1)}`;
document.getElementById('multiplier').classList.add('visible');
playNearMiss();
// Pulse score display
const scoreEl = document.getElementById('score');
scoreEl.classList.add('pulse');
setTimeout(() => scoreEl.classList.remove('pulse'), 100);
}
}
// ─── ORB COLLECTION ───
function checkOrbCollection() {
for (let i = state.orbs.length - 1; i >= 0; i--) {
const orb = state.orbs[i];
const dist = state.playerPos.distanceTo(orb.position);
if (dist < 4) {
// Collected!
state.energy = Math.min(state.energy + orb.userData.value, CFG.energyMax);
state.score += Math.floor(50 * state.multiplier);
state.boost = Math.min(state.boost + 10, CFG.boostMax);
playPing();
// Remove orb
scene.remove(orb);
orb.geometry.dispose();
state.orbs.splice(i, 1);
}
}
}
// ─── INPUT ───
const keys = {};
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code === 'Enter' && !state.running) {
startGame();
}
if (e.code === 'Enter') {
e.preventDefault();
}
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
// ─── CAMERA ───
const cameraOffset = new THREE.Vector3(0, 4, -12);
const cameraLookOffset = new THREE.Vector3(0, 2, 20);
let cameraShake = 0;
let targetFOV = 70;
function updateCamera(dt) {
// FOV based on speed
const speedRatio = state.speed / CFG.boostSpeed;
targetFOV = 70 + speedRatio * 25;
camera.fov += (targetFOV - camera.fov) * dt * 3;
camera.updateProjectionMatrix();
// Camera position
const targetPos = state.playerPos.clone().add(cameraOffset);
targetPos.x += state.playerVel.x * 2;
targetPos.y += state.playerVel.y * 1;
// Smooth follow
camera.position.lerp(targetPos, dt * 5);
// Look ahead
const lookTarget = state.playerPos.clone().add(cameraLookOffset);
lookTarget.x += state.playerVel.x * 3;
camera.lookAt(lookTarget);
// Camera shake
if (cameraShake > 0.001) {
camera.position.x += (Math.random() - 0.5) * cameraShake;
camera.position.y += (Math.random() - 0.5) * cameraShake * 0.5;
cameraShake *= 0.92;
}
}
// ─── HUD UPDATE ───
function updateHUD() {
const speedKmh = Math.floor(state.speed * 2.5);
document.getElementById('score').textContent = Math.floor(state.score).toLocaleString();
document.getElementById('energy-bar').style.width = `${(state.energy / CFG.energyMax) * 100}%`;
document.getElementById('speed-value').textContent = speedKmh;
document.getElementById('speed-fill').style.width = `${(state.speed / CFG.boostSpeed) * 100}%`;
document.getElementById('boost-fill').style.height = `${(state.boost / CFG.boostMax) * 100}%`;
// Energy bar color
const energyPct = state.energy / CFG.energyMax;
const energyBar = document.getElementById('energy-bar');
if (energyPct < 0.2) {
energyBar.style.background = 'linear-gradient(90deg, #ff0033, #ff3333)';
energyBar.style.boxShadow = '0 0 20px rgba(255,0,50,0.8)';
} else if (energyPct < 0.4) {
energyBar.style.background = 'linear-gradient(90deg, #ff3333, #ff6633)';
energyBar.style.boxShadow = '0 0 15px rgba(255,100,50,0.6)';
} else {
energyBar.style.background = 'linear-gradient(90deg, #ff3366, #ff9933, #ffdd33)';
energyBar.style.boxShadow = '0 0 15px rgba(255,150,50,0.6)';
}
// Boost glow
const boostFill = document.getElementById('boost-fill');
if (state.boost > 50) {
boostFill.style.boxShadow = '0 0 20px rgba(100,150,255,0.8)';
} else {
boostFill.style.boxShadow = '0 0 10px rgba(100,150,255,0.5)';
}
}
// ─── GAME LOOP ───
function update(dt) {
if (!state.running) return;
const now = performance.now();
const elapsed = (now - state.gameStartTime) / 1000;
// Difficulty scaling
const difficulty = 1 + Math.min(elapsed / 120, 2); // max 3x over 2 minutes
const maxSpeed = CFG.maxSpeed * Math.min(1 + elapsed / 180, 2.5);
// Input
const steerInput = (keys['ArrowRight'] || keys['KeyD'] ? 1 : 0) - (keys['ArrowLeft'] || keys['KeyA'] ? 1 : 0);
const pitchInput = (keys['ArrowDown'] || keys['KeyS'] ? 1 : 0) - (keys['ArrowUp'] || keys['KeyW'] ? 1 : 0);
const boosting = keys['ShiftLeft'] || keys['ShiftRight'];
// Speed management
let targetSpeed = CFG.baseSpeed * difficulty;
if (boosting && state.boost > 0) {
targetSpeed = CFG.boostSpeed * difficulty;
state.boost -= CFG.boostDrain * dt;
state.boost = Math.max(state.boost, 0);
cameraShake = Math.max(cameraShake, 0.03);
}
// Speed up gradually
state.speed += (targetSpeed - state.speed) * dt * 0.5;
state.speed = Math.max(CFG.baseSpeed, Math.min(state.speed, maxSpeed));
// Track max speed
const currentKmh = state.speed * 2.5;
if (currentKmh > state.maxSpeedReached) state.maxSpeedReached = currentKmh;
// Speed ratio for visual effects
const speedRatio = state.speed / CFG.boostSpeed;
// Player physics
const steerForce = 40;
const pitchForce = 25;
const damping = 3.5;
state.playerVel.x += (steerInput * steerForce - state.playerVel.x * damping) * dt;
state.playerVel.y += (pitchInput * pitchForce - state.playerVel.y * damping) * dt;
// Bank (roll) proportional to lateral velocity
state.targetBank = -state.playerVel.x * 0.025;
state.playerBank += (state.targetBank - state.playerBank) * dt * 8;
// Pitch visual
state.targetRot.x = pitchInput * 0.15;
state.playerRot.x += (state.targetRot.x - state.playerRot.x) * dt * 6;
// Update position
state.playerPos.x += state.playerVel.x * dt;
state.playerPos.y += state.playerVel.y * dt;
state.playerPos.z += state.speed * dt;
// Constrain player
const currentChunk = state.chunks.get(Math.round(state.playerPos.z / CFG.chunkSize));
let minX = -80, maxX = 80;
if (currentChunk && currentChunk.userData.walls) {
for (const wall of currentChunk.userData.walls) {
if (Math.abs(state.playerPos.z - wall.z0) < 20) {
minX = Math.max(minX, wall.leftX + 3);
maxX = Math.min(maxX, wall.rightX - 3);
}
}
}
state.playerPos.x = Math.max(minX, Math.min(maxX, state.playerPos.x));
state.playerPos.y = Math.max(-10, Math.min(60, state.playerPos.y));
// Energy decay
state.energy -= CFG.energyDecay * dt;
if (state.energy <= 0) {
gameOver();
return;
}
// Multiplier decay
state.multiplier = Math.max(1, state.multiplier - CFG.multiplierDecay * dt);
if (state.multiplier <= 1.1) {
document.getElementById('multiplier').classList.remove('visible');
}
// Boost recharge (slow)
if (!boosting) {
state.boost = Math.min(state.boost + CFG.boostRecharge * dt, CFG.boostMax);
}
// Score
state.score += state.speed * dt * state.multiplier * 0.5;
// Collision check
if (checkCollisions()) {
cameraShake = 0.3;
gameOver();
return;
}
// Orb collection
checkOrbCollection();
// Update chunks
updateChunks();
// Camera
updateCamera(dt);
// Update player mesh
player.position.copy(state.playerPos);
player.rotation.z = state.playerBank;
player.rotation.x = state.playerRot.x;
// Engine glow pulse
const glowPulse = 2.0 + Math.sin(now * 0.008) * 0.5 + (boosting ? 2.0 : 0);
playerGlowMat.emissiveIntensity = glowPulse;
// Animate orbs
for (const orb of state.orbs) {
orb.rotation.y += dt * 2;
orb.rotation.x += dt * 1.5;
orb.position.y += Math.sin(now * 0.003 + orb.position.x) * 0.02;
}
// Debris
updateDebris(dt);
// Trail particles
if (Math.random() < 0.6 + speedRatio * 0.4) {
spawnTrailParticle();
}
updateTrailParticles(dt);
// Camera shake from near misses
if (closestWallDist < 8) {
cameraShake = Math.max(cameraShake, (8 - closestWallDist) * 0.015);
}
// Engine sound
updateEngineSound();
// HUD
updateHUD();
// Update bloom based on speed
bloomPass.strength = 1.0 + (state.speed / CFG.boostSpeed) * 0.8;
}
let closestWallDist = Infinity;
// ─── GAME STATE MANAGEMENT ───
function startGame() {
// Reset state
state.running = true;
state.score = 0;
state.multiplier = 1;
state.energy = CFG.energyMax;
state.boost = 0;
state.speed = CFG.baseSpeed;
state.maxSpeedReached = 0;
state.nearMisses = 0;
state.playerPos.set(0, 10, 0);
state.playerVel.set(0, 0, 0);
state.playerRot.set(0, 0, 0);
state.playerBank = 0;
state.trailParticles.forEach(p => scene.remove(p));
state.trailParticles = [];
state.gameStartTime = performance.now();
// Clear old chunks
for (const [key, chunk] of state.chunks) {
scene.remove(chunk);
}
state.chunks.clear();
for (const orb of state.orbs) {
scene.remove(orb);
}
state.orbs = [];
// Generate initial chunks
for (let i = -2; i < CFG.chunkCount; i++) {
generateChunk(i * CFG.chunkSize);
}
// Reset camera
camera.position.set(0, 14, -12);
camera.fov = 70;
camera.updateProjectionMatrix();
cameraShake = 0;
// UI
document.getElementById('game-over').classList.remove('active');
document.getElementById('start-screen').style.display = 'none';
document.getElementById('multiplier').classList.remove('visible');
// Audio
initAudio();
}
function gameOver() {
state.running = false;
playCrash();
if (state.engineOsc) {
state.engineOsc.stop();
state.engineOsc = null;
}
document.getElementById('final-score').textContent = Math.floor(state.score).toLocaleString();
document.getElementById('max-speed').textContent = Math.floor(state.maxSpeedReached);
document.getElementById('near-misses').textContent = state.nearMisses;
document.getElementById('game-over').classList.add('active');
}
// ─── RENDER LOOP ───
function animate(time) {
requestAnimationFrame(animate);
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
state.lastTime = time;
if (state.running) {
update(dt);
}
// Render
composer.render();
}
// ─── INIT ───
// Generate initial terrain for start screen
for (let i = -2; i < 6; i++) {
generateChunk(i * CFG.chunkSize);
}
// Animate orbs on start screen
function animateOrbs(time) {
const now = time || performance.now();
for (const orb of state.orbs) {
orb.rotation.y += 0.01;
orb.rotation.x += 0.005;
}
requestAnimationFrame(animateOrbs);
}
animateOrbs();
// Restart button
document.getElementById('restart-btn').addEventListener('click', startGame);
// Window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
// Start render loop
animate(0);
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"full","sessionId":"7db1a5b9-3494-4234-9fd0-79a269aff94c","tokensIn":1218797,"tokensOut":19078,"tokensTotal":1237875,"turns":33,"toolCalls":32,"failedToolCalls":0,"timestamp":"2026-07-21T01:09:34.062Z"} -->