Canyon Run
thinkingcap qwen3.6 27b · 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 40,294 bytes · SHA-256 10912e5d9491
<!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: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; position: fixed; top: 0; left: 0; z-index: 1; }
#hud {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10;
}
#speed-bar-container {
position: absolute; right: 30px; bottom: 40px;
width: 8px; height: 200px; background: rgba(255,255,255,0.1);
border-radius: 4px; overflow: hidden;
}
#speed-bar {
position: absolute; bottom: 0; width: 100%; height: 0%;
background: linear-gradient(to top, #ff6a00, #ee0979);
border-radius: 4px; transition: height 0.1s;
}
#turbo-bar-container {
position: absolute; right: 50px; bottom: 40px;
width: 8px; height: 200px; background: rgba(255,255,255,0.1);
border-radius: 4px; overflow: hidden;
}
#turbo-bar {
position: absolute; bottom: 0; width: 100%; height: 0%;
background: linear-gradient(to top, #00f2fe, #4facfe);
border-radius: 4px; transition: height 0.1s;
}
#multiplier {
position: absolute; right: 80px; bottom: 60px;
font-size: 48px; font-weight: 900; color: #fff;
text-shadow: 0 0 20px rgba(255,106,0,0.8);
opacity: 0.3; transition: all 0.2s;
}
#multiplier.active {
opacity: 1; transform: scale(1.2);
text-shadow: 0 0 40px rgba(255,106,0,1), 0 0 80px rgba(238,9,121,0.6);
}
#energy-container {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
width: 300px; height: 6px; background: rgba(255,255,255,0.1);
border-radius: 3px; overflow: hidden;
}
#energy-bar {
width: 100%; height: 100%;
background: linear-gradient(to right, #ff6a00, #ee0979);
border-radius: 3px; transition: width 0.15s;
}
#score-display {
position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
font-size: 24px; font-weight: 700; color: #fff;
text-shadow: 0 0 10px rgba(255,106,0,0.5);
letter-spacing: 2px;
}
#speed-text {
position: absolute; right: 34px; bottom: 250px;
font-size: 14px; color: rgba(255,255,255,0.6);
text-align: center; writing-mode: vertical-rl;
letter-spacing: 2px;
}
#game-over {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.85); display: none;
flex-direction: column; align-items: center; justify-content: center;
z-index: 100; pointer-events: all;
}
#game-over h1 {
font-size: 64px; color: #ee0979; margin-bottom: 20px;
text-shadow: 0 0 30px rgba(238,9,121,0.5);
}
#game-over .stat {
font-size: 24px; color: #fff; margin: 8px 0; opacity: 0.8;
}
#game-over .final-score {
font-size: 72px; font-weight: 900; color: #ff6a00;
text-shadow: 0 0 40px rgba(255,106,0,0.8); margin: 20px 0;
}
#restart-btn {
margin-top: 30px; padding: 15px 50px; font-size: 20px;
background: linear-gradient(135deg, #ff6a00, #ee0979);
border: none; color: #fff; cursor: pointer;
border-radius: 8px; font-weight: 700; letter-spacing: 2px;
transition: transform 0.2s, box-shadow 0.2s;
}
#restart-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 30px rgba(255,106,0,0.5);
}
#start-screen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(ellipse at center, #1a0a2e 0%, #0d0015 100%);
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 200; pointer-events: all;
}
#start-screen h1 {
font-size: 72px; color: #ff6a00; margin-bottom: 10px;
text-shadow: 0 0 40px rgba(255,106,0,0.6);
}
#start-screen .subtitle {
font-size: 18px; color: rgba(255,255,255,0.5); margin-bottom: 40px;
letter-spacing: 4px; text-transform: uppercase;
}
#start-btn {
padding: 18px 60px; font-size: 22px;
background: linear-gradient(135deg, #ff6a00, #ee0979);
border: none; color: #fff; cursor: pointer;
border-radius: 8px; font-weight: 700; letter-spacing: 3px;
transition: transform 0.2s, box-shadow 0.2s;
}
#start-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 40px rgba(255,106,0,0.5);
}
.controls-info {
margin-top: 30px; color: rgba(255,255,255,0.4); font-size: 14px;
text-align: center; line-height: 1.8;
}
#near-miss-flash {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(ellipse at center, rgba(255,106,0,0.3), transparent);
opacity: 0; pointer-events: none; z-index: 5;
transition: opacity 0.1s;
}
#boost-flash {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(ellipse at center, rgba(79,172,254,0.2), transparent);
opacity: 0; pointer-events: none; z-index: 5;
}
</style>
</head>
<body>
<div id="hud">
<div id="score-display">0</div>
<div id="multiplier">x1</div>
<div id="speed-text">SPEED</div>
<div id="speed-bar-container"><div id="speed-bar"></div></div>
<div id="turbo-bar-container"><div id="turbo-bar"></div></div>
<div id="energy-container"><div id="energy-bar"></div></div>
</div>
<div id="near-miss-flash"></div>
<div id="boost-flash"></div>
<div id="game-over">
<h1>CRASHED</h1>
<div class="final-score" id="final-score">0</div>
<div class="stat" id="final-speed">Max Speed: 0 km/h</div>
<div class="stat" id="final-distance">Distance: 0 m</div>
<button id="restart-btn">RESTART</button>
</div>
<div id="start-screen">
<h1>CANYON RACER</h1>
<div class="subtitle">High-Speed Procedural Racing</div>
<button id="start-btn">LAUNCH</button>
<div class="controls-info">
WASD / Arrow Keys — Steer<br>
SHIFT — Boost | SPACE — Brake
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.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';
// ─── AUDIO ENGINE ──────────────────────────────────────
class AudioEngine {
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
this.engineOsc = this.ctx.createOscillator();
this.engineGain = this.ctx.createGain();
const engineFilter = this.ctx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 300;
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 60;
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 = 50 + speedRatio * 120;
this.engineOsc.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.05);
this.engineGain.gain.setTargetAtTime(0.04 + speedRatio * 0.08, this.ctx.currentTime, 0.05);
}
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.1);
gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.2);
}
playNearMiss() {
if (!this.initialized) return;
const bufferSize = this.ctx.sampleRate * 0.15;
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.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) * Math.exp(-i / (bufferSize * 0.4));
}
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
source.connect(gain);
gain.connect(this.ctx.destination);
source.start();
const osc = this.ctx.createOscillator();
const g2 = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(30, this.ctx.currentTime + 0.4);
g2.gain.setValueAtTime(0.2, 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 = 'sine';
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.001, this.ctx.currentTime + 0.4);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.4);
}
stop() {
if (this.engineOsc) {
try { this.engineOsc.stop(); } catch(e) {}
}
}
}
// ─── GAME STATE ────────────────────────────────────────
const CHUNK_SIZE = 200;
const RENDER_DIST = 8;
const PLAYER_RADIUS = 1.5;
const BASE_SPEED = 40;
const MAX_SPEED = 120;
const BOOST_SPEED = 180;
let scene, camera, renderer, composer, bloomPass, motionBlurPass;
let player, playerBody;
let chunks = new Map();
let orbs = [];
let particles = [];
let trailParticles = [];
let debrisParticles = [];
// Game state
const game = {
running: false,
speed: BASE_SPEED,
targetSpeed: BASE_SPEED,
energy: 100,
score: 0,
multiplier: 1,
turbo: 0,
boosting: false,
maxSpeed: 0,
distance: 0,
difficulty: 1,
shakeAmount: 0,
nearMissTimer: 0,
chunkZ: 0,
};
// Player physics
const playerState = {
x: 0, y: 5, z: 0,
vx: 0, vy: 0, vz: 0,
roll: 0, pitch: 0, yaw: 0,
targetRoll: 0,
};
// Input
const keys = {};
// ─── INITIALIZATION ────────────────────────────────────
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x1a0a2e, 0.003);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
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.2;
document.body.appendChild(renderer.domElement);
// Post-processing
composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, 0.4, 0.85
);
bloomPass.threshold = 0.2;
bloomPass.strength = 1.2;
bloomPass.radius = 0.8;
composer.addPass(bloomPass);
// Motion blur shader pass
const motionBlurShader = {
uniforms: {
tDiffuse: { value: null },
blendFactor: { value: 0.15 }
},
vertexShader: `varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.); }`,
fragmentShader: `
uniform sampler2D tDiffuse; uniform float blendFactor; varying vec2 vUv;
void main(){
vec4 c = texture2D(tDiffuse, vUv);
vec2 dir = vec2(0.0, -blendFactor * 0.01);
vec4 sum = vec4(0.);
for(int i=0; i<5; i++){
sum += texture2D(tDiffuse, vUv + dir * float(i));
}
gl_FragColor = (c + sum * 0.5) / 1.5;
}`
};
motionBlurPass = new ShaderPass(motionBlurShader);
composer.addPass(motionBlurPass);
// Lighting - Golden hour / cyber sunset
const ambientLight = new THREE.AmbientLight(0x4a2060, 0.5);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xff8c42, 2.0);
sunLight.position.set(-100, 80, -200);
sunLight.castShadow = true;
sunLight.shadow.mapSize.set(2048, 2048);
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 500;
sunLight.shadow.camera.left = -100;
sunLight.shadow.camera.right = 100;
sunLight.shadow.camera.top = 100;
sunLight.shadow.camera.bottom = -100;
scene.add(sunLight);
const rimLight = new THREE.DirectionalLight(0x9b59b6, 0.8);
rimLight.position.set(50, 30, 100);
scene.add(rimLight);
const hemiLight = new THREE.HemisphereLight(0xff7744, 0x2d1b69, 0.4);
scene.add(hemiLight);
// Skybox gradient via a large sphere
createSky();
// Create player craft
createPlayer();
// Generate initial chunks
for (let i = -2; i < RENDER_DIST; i++) {
generateChunk(i * CHUNK_SIZE);
}
// Event listeners
window.addEventListener('keydown', e => keys[e.code] = true);
window.addEventListener('keyup', e => keys[e.code] = false);
window.addEventListener('resize', onResize);
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', restartGame);
}
function createSky() {
const skyGeo = new THREE.SphereGeometry(900, 16, 16);
const skyMat = new THREE.ShaderMaterial({
side: THREE.BackSide,
uniforms: {},
vertexShader: `varying vec3 vPos; void main(){ vPos=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.); }`,
fragmentShader: `
varying vec3 vPos;
void main(){
float h = normalize(vPos).y * 0.5 + 0.5;
vec3 bottom = vec3(0.6, 0.15, 0.05);
vec3 mid = vec3(0.4, 0.1, 0.2);
vec3 top = vec3(0.05, 0.02, 0.1);
vec3 col = mix(bottom, mid, smoothstep(0.0, 0.4, h));
col = mix(col, top, smoothstep(0.4, 1.0, h));
gl_FragColor = vec4(col, 1.);
}`
});
const sky = new THREE.Mesh(skyGeo, skyMat);
scene.add(sky);
}
function createPlayer() {
player = new THREE.Group();
// Main body - sleek arrow shape
const bodyGeo = new THREE.ConeGeometry(0.8, 4, 6);
bodyGeo.rotateX(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0xcccccc, flatShading: true, metalness: 0.7, roughness: 0.3
});
playerBody = new THREE.Mesh(bodyGeo, bodyMat);
playerBody.castShadow = true;
player.add(playerBody);
// Wings
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(-3, -1.5);
wingShape.lineTo(-2.5, -0.5);
wingShape.lineTo(0, 0.5);
const wingGeoL = new THREE.ExtrudeGeometry(wingShape, { depth: 0.15, bevelEnabled: false });
const wingMat = new THREE.MeshStandardMaterial({
color: 0xee4422, flatShading: true, metalness: 0.5, roughness: 0.4
});
const wingL = new THREE.Mesh(wingGeoL, wingMat);
wingL.position.set(0, -0.3, 0.5);
player.add(wingL);
const wingR = new THREE.Mesh(wingGeoL.clone(), wingMat);
wingR.scale.x = -1;
wingR.position.set(0, -0.3, 0.5);
player.add(wingR);
// Cockpit
const cockpitGeo = new THREE.SphereGeometry(0.4, 6, 4, 0, Math.PI * 2, 0, Math.PI / 2);
const cockpitMat = new THREE.MeshStandardMaterial({
color: 0x33ccff, emissive: 0x1188aa, emissiveIntensity: 0.5,
flatShading: true, metalness: 0.9, roughness: 0.1, transparent: true, opacity: 0.7
});
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(0, 0.5, -0.3);
player.add(cockpit);
// Engine glow
const engineGeo = new THREE.SphereGeometry(0.4, 8, 8);
const engineMat = new THREE.MeshStandardMaterial({
color: 0xff6a00, emissive: 0xff6a00, emissiveIntensity: 3, flatShading: true
});
const engine = new THREE.Mesh(engineGeo, engineMat);
engine.position.z = 2.2;
player.add(engine);
// Engine flame cone
const flameGeo = new THREE.ConeGeometry(0.3, 1.5, 6);
flameGeo.rotateX(-Math.PI / 2);
const flameMat = new THREE.MeshStandardMaterial({
color: 0xffaa00, emissive: 0xff8800, emissiveIntensity: 4,
flatShading: true, transparent: true, opacity: 0.8
});
const flame = new THREE.Mesh(flameGeo, flameMat);
flame.position.z = 3;
player.add(flame);
// Engine light
const engineLight = new THREE.PointLight(0xff6a00, 3, 15);
engineLight.position.z = 2.5;
player.add(engineLight);
player.position.set(0, 5, 0);
scene.add(player);
}
// ─── PROCEDURAL GENERATION ─────────────────────────────
function generateChunk(zPos) {
const chunkKey = Math.round(zPos / CHUNK_SIZE);
if (chunks.has(chunkKey)) return;
const chunkGroup = new THREE.Group();
const difficulty = game.difficulty;
const canyonWidth = Math.max(20, 60 - difficulty * 3);
const canyonHeight = Math.max(15, 40 - difficulty * 1.5);
// Canyon floor
const floorGeo = new THREE.PlaneGeometry(canyonWidth * 2 + 40, CHUNK_SIZE, 8, 4);
const floorMat = new THREE.MeshStandardMaterial({
color: 0x3d1c02, flatShading: true, roughness: 0.9
});
// Add some vertex displacement for terrain feel
const posAttr = floorGeo.attributes.position;
for (let i = 0; i < posAttr.count; i++) {
const x = posAttr.getX(i);
const y = posAttr.getY(i);
posAttr.setZ(i, (Math.sin(x * 0.1) + Math.cos(y * 0.05)) * 2);
}
floorGeo.computeVertexNormals();
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.position.set(0, -canyonHeight / 2 + 2, zPos + CHUNK_SIZE / 2);
floor.receiveShadow = true;
chunkGroup.add(floor);
// Canyon ceiling
const ceilGeo = new THREE.PlaneGeometry(canyonWidth * 2 + 40, CHUNK_SIZE, 8, 4);
const ceilMat = new THREE.MeshStandardMaterial({
color: 0x2a1530, flatShading: true, roughness: 0.9
});
const ceilPosAttr = ceilGeo.attributes.position;
for (let i = 0; i < ceilPosAttr.count; i++) {
const x = ceilPosAttr.getX(i);
const y = ceilPosAttr.getY(i);
ceilPosAttr.setZ(i, -(Math.sin(x * 0.15) + Math.cos(y * 0.08)) * 2);
}
ceilGeo.computeVertexNormals();
const ceiling = new THREE.Mesh(ceilGeo, ceilMat);
ceiling.rotation.x = Math.PI / 2;
ceiling.position.set(0, canyonHeight / 2 - 2, zPos + CHUNK_SIZE / 2);
ceiling.receiveShadow = true;
chunkGroup.add(ceiling);
// Canyon walls (left and right)
const wallColors = [0x8b4513, 0xa0522d, 0xcd853f, 0xb87333];
for (let side = -1; side <= 1; side += 2) {
// Main wall segments with varying height and shape
const numSegments = 4 + Math.floor(Math.random() * 3);
for (let s = 0; s < numSegments; s++) {
const segZ = zPos + (s / numSegments) * CHUNK_SIZE;
const wallHeight = canyonHeight * (0.6 + Math.random() * 0.4);
const wallWidth = 8 + Math.random() * 15;
const wallDepth = CHUNK_SIZE / numSegments - 2;
// Low-poly wall with random vertex displacement
const wGeo = new THREE.BoxGeometry(wallWidth, wallHeight, wallDepth, 3, 4, 2);
const wPosAttr = wGeo.attributes.position;
for (let i = 0; i < wPosAttr.count; i++) {
const px = wPosAttr.getX(i);
const py = wPosAttr.getY(i);
const pz = wPosAttr.getZ(i);
wPosAttr.setX(i, px + (Math.random() - 0.5) * 3);
wPosAttr.setY(i, py + (Math.random() - 0.5) * 2);
}
wGeo.computeVertexNormals();
const colorIdx = Math.floor(Math.random() * wallColors.length);
const wMat = new THREE.MeshStandardMaterial({
color: wallColors[colorIdx], flatShading: true, roughness: 0.85
});
const wall = new THREE.Mesh(wGeo, wMat);
wall.position.set(side * (canyonWidth / 2 + wallWidth / 2), 0, segZ + wallDepth / 2);
wall.castShadow = true;
wall.receiveShadow = true;
chunkGroup.add(wall);
// Store collision data
wall.userData.isObstacle = true;
wall.userData.bounds = {
minX: side * (canyonWidth / 2 + wallWidth / 2) - wallWidth / 2,
maxX: side * (canyonWidth / 2 + wallWidth / 2) + wallWidth / 2,
minY: -wallHeight / 2,
maxY: wallHeight / 2,
minZ: segZ,
maxZ: segZ + wallDepth,
};
}
}
// Floating monoliths
const numMonoliths = Math.floor(1 + difficulty * 0.5);
for (let m = 0; m < numMonoliths; m++) {
const mHeight = 8 + Math.random() * 12;
const mWidth = 3 + Math.random() * 4;
const mGeo = new THREE.BoxGeometry(mWidth, mHeight, mWidth, 1, 2, 1);
// Displace vertices for low-poly look
const mPosAttr = mGeo.attributes.position;
for (let i = 0; i < mPosAttr.count; i++) {
mPosAttr.setX(i, mPosAttr.getX(i) + (Math.random() - 0.5) * 1);
mPosAttr.setY(i, mPosAttr.getY(i) + (Math.random() - 0.5) * 1);
}
mGeo.computeVertexNormals();
const mMat = new THREE.MeshStandardMaterial({
color: 0x6a3d7d, flatShading: true, metalness: 0.3, roughness: 0.6
});
const monolith = new THREE.Mesh(mGeo, mMat);
const mz = zPos + Math.random() * CHUNK_SIZE;
const mx = (Math.random() - 0.5) * canyonWidth * 0.7;
const my = (Math.random() - 0.5) * canyonHeight * 0.5;
monolith.position.set(mx, my, mz);
monolith.rotation.y = Math.random() * Math.PI;
monolith.castShadow = true;
monolith.userData.isObstacle = true;
monolith.userData.bounds = {
minX: mx - mWidth / 2, maxX: mx + mWidth / 2,
minY: my - mHeight / 2, maxY: my + mHeight / 2,
minZ: mz - mWidth / 2, maxZ: mz + mWidth / 2,
};
chunkGroup.add(monolith);
}
// Tight squeezes (narrow gaps with pillars on both sides)
if (Math.random() < 0.3 * difficulty) {
const squeezeZ = zPos + Math.random() * CHUNK_SIZE;
const gapWidth = Math.max(6, 14 - difficulty);
for (let side = -1; side <= 1; side += 2) {
const sqGeo = new THREE.BoxGeometry(8, canyonHeight * 0.7, 3);
const sqMat = new THREE.MeshStandardMaterial({
color: 0x5c3a1e, flatShading: true, roughness: 0.9
});
const squeezeWall = new THREE.Mesh(sqGeo, sqMat);
squeezeWall.position.set(
side * (gapWidth / 2 + 4),
0,
squeezeZ
);
squeezeWall.castShadow = true;
squeezeWall.userData.isObstacle = true;
squeezeWall.userData.bounds = {
minX: side * (gapWidth / 2 + 4) - 4,
maxX: side * (gapWidth / 2 + 4) + 4,
minY: -canyonHeight * 0.35,
maxY: canyonHeight * 0.35,
minZ: squeezeZ - 1.5,
maxZ: squeezeZ + 1.5,
};
chunkGroup.add(squeezeWall);
}
}
// Energy orbs
const numOrbs = 2 + Math.floor(Math.random() * 3);
for (let o = 0; o < numOrbs; o++) {
const orbGeo = new THREE.OctahedronGeometry(1, 0);
const orbMat = new THREE.MeshStandardMaterial({
color: 0xffaa00, emissive: 0xff8800, emissiveIntensity: 2,
flatShading: true, metalness: 0.5
});
const orb = new THREE.Mesh(orbGeo, orbMat);
// Place orbs in interesting positions (sometimes in tight spots)
const oz = zPos + Math.random() * CHUNK_SIZE;
const ox = (Math.random() - 0.5) * canyonWidth * 0.6;
const oy = (Math.random() - 0.5) * canyonHeight * 0.4;
orb.position.set(ox, oy, oz);
// Orb glow light
const orbLight = new THREE.PointLight(0xff8800, 1, 10);
orb.add(orbLight);
chunkGroup.add(orb);
orbs.push({ mesh: orb, collected: false });
}
scene.add(chunkGroup);
chunks.set(chunkKey, { group: chunkGroup, zPos: zPos });
}
function removeChunk(zPos) {
const chunkKey = Math.round(zPos / CHUNK_SIZE);
const chunk = chunks.get(chunkKey);
if (!chunk) return;
scene.remove(chunk.group);
chunk.group.traverse(child => {
if (child.geometry) child.geometry.dispose();
if (child.material) child.material.dispose();
});
chunks.delete(chunkKey);
}
// ─── PARTICLES ─────────────────────────────────────────
function createTrailParticle() {
const geo = new THREE.SphereGeometry(0.15, 4, 4);
const mat = new THREE.MeshBasicMaterial({
color: 0xff6a00, transparent: true, opacity: 0.8
});
const mesh = new THREE.Mesh(geo, mat);
return { mesh, life: 1.0, decay: 0.02 + Math.random() * 0.03 };
}
function createDebrisParticle(zPos) {
const geo = new THREE.TetrahedronGeometry(0.3 + Math.random() * 0.5, 0);
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.08 + Math.random() * 0.05, 0.6, 0.4),
transparent: true, opacity: 0.7
});
const mesh = new THREE.Mesh(geo, mat);
return { mesh, life: 1.0, decay: 0.01 + Math.random() * 0.02 };
}
function spawnTrailParticle() {
if (trailParticles.length > 80) return;
const p = createTrailParticle();
p.mesh.position.copy(player.position);
p.mesh.position.z += 2.5;
scene.add(p.mesh);
trailParticles.push(p);
}
function spawnDebris(zPos) {
if (debrisParticles.length > 60) return;
const difficulty = game.difficulty;
const canyonWidth = Math.max(20, 60 - difficulty * 3);
const canyonHeight = Math.max(15, 40 - difficulty * 1.5);
const p = createDebrisParticle(zPos);
p.mesh.position.set(
(Math.random() - 0.5) * canyonWidth * 1.2,
(Math.random() - 0.5) * canyonHeight,
zPos + Math.random() * CHUNK_SIZE
);
scene.add(p.mesh);
debrisParticles.push(p);
}
function updateParticles(dt) {
// Trail particles
for (let i = trailParticles.length - 1; i >= 0; i--) {
const p = trailParticles[i];
p.life -= p.decay;
p.mesh.material.opacity = p.life * 0.8;
p.mesh.scale.setScalar(p.life);
if (p.life <= 0) {
scene.remove(p.mesh);
p.mesh.geometry.dispose();
p.mesh.material.dispose();
trailParticles.splice(i, 1);
}
}
// Debris particles - move toward player to simulate speed
for (let i = debrisParticles.length - 1; i >= 0; i--) {
const p = debrisParticles[i];
const dz = game.speed * dt * 2;
p.mesh.position.z += dz;
p.life -= p.decay;
p.mesh.material.opacity = p.life * 0.7;
p.mesh.rotation.x += dt * 3;
p.mesh.rotation.y += dt * 2;
if (p.life <= 0 || p.mesh.position.z > player.position.z + 50) {
scene.remove(p.mesh);
p.mesh.geometry.dispose();
p.mesh.material.dispose();
debrisParticles.splice(i, 1);
}
}
}
// ─── COLLISION DETECTION ───────────────────────────────
function checkCollisions() {
const px = player.position.x;
const py = player.position.y;
const pz = player.position.z;
let closestDist = Infinity;
chunks.forEach((chunk, key) => {
chunk.group.traverse(child => {
if (!child.userData.isObstacle || !child.userData.bounds) return;
const b = child.userData.bounds;
// Check if player is within Z range of this obstacle
if (pz + PLAYER_RADIUS < b.minZ || pz - PLAYER_RADIUS > b.maxZ) return;
// AABB vs sphere check
const closestX = Math.max(b.minX, Math.min(px, b.maxX));
const closestY = Math.max(b.minY, Math.min(py, b.maxY));
const closestZ = Math.max(b.minZ, Math.min(pz, b.maxZ));
const dx = px - closestX;
const dy = py - closestY;
const dz = pz - closestZ;
const distSq = dx * dx + dy * dy + dz * dz;
const dist = Math.sqrt(distSq);
if (dist < PLAYER_RADIUS) {
// Collision!
gameOver();
return;
}
if (dist < closestDist) {
closestDist = dist;
}
});
});
// Near-miss detection
if (closestDist < PLAYER_RADIUS * 4 && closestDist > PLAYER_RADIUS) {
game.nearMissTimer = 0.3;
const proximityBonus = Math.max(1, Math.floor((PLAYER_RADIUS * 4 - closestDist) / PLAYER_RADIUS));
game.score += proximityBonus * game.multiplier;
if (closestDist < PLAYER_RADIUS * 2.5) {
game.multiplier = Math.min(game.multiplier + 0.5, 10);
audio.playNearMiss();
game.shakeAmount = 0.3;
document.getElementById('near-miss-flash').style.opacity = '1';
setTimeout(() => {
document.getElementById('near-miss-flash').style.opacity = '0';
}, 150);
}
updateMultiplierDisplay();
}
// Orb collection
for (let i = orbs.length - 1; i >= 0; i--) {
const orb = orbs[i];
if (orb.collected) continue;
const dx = px - orb.mesh.position.x;
const dy = py - orb.mesh.position.y;
const dz = pz - orb.mesh.position.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < 3) {
orb.collected = true;
game.energy = Math.min(100, game.energy + 20);
game.turbo = Math.min(100, game.turbo + 15);
game.score += 50 * game.multiplier;
audio.playCollect();
// Remove orb mesh visually
scene.remove(orb.mesh);
orbs.splice(i, 1);
}
// Clean up far behind orbs
if (orb.mesh.position.z < pz - CHUNK_SIZE) {
scene.remove(orb.mesh);
orbs.splice(i, 1);
}
}
}
// ─── GAME LOGIC ────────────────────────────────────────
const audio = new AudioEngine();
function startGame() {
document.getElementById('start-screen').style.display = 'none';
audio.init();
game.running = true;
resetGameState();
}
function restartGame() {
document.getElementById('game-over').style.display = 'none';
// Clean up old chunks and particles
chunks.forEach((chunk) => {
scene.remove(chunk.group);
chunk.group.traverse(c => {
if (c.geometry) c.geometry.dispose();
if (c.material) c.material.dispose();
});
});
chunks.clear();
trailParticles.forEach(p => {
scene.remove(p.mesh);
p.mesh.geometry.dispose();
p.mesh.material.dispose();
});
trailParticles.length = 0;
debrisParticles.forEach(p => {
scene.remove(p.mesh);
p.mesh.geometry.dispose();
p.mesh.material.dispose();
});
debrisParticles.length = 0;
orbs.forEach(o => scene.remove(o.mesh));
orbs.length = 0;
resetGameState();
game.running = true;
}
function resetGameState() {
game.speed = BASE_SPEED;
game.targetSpeed = BASE_SPEED;
game.energy = 100;
game.score = 0;
game.multiplier = 1;
game.turbo = 0;
game.boosting = false;
game.maxSpeed = 0;
game.distance = 0;
game.difficulty = 1;
game.shakeAmount = 0;
game.nearMissTimer = 0;
game.chunkZ = 0;
playerState.x = 0;
playerState.y = 5;
playerState.z = 0;
playerState.vx = 0;
playerState.vy = 0;
playerState.roll = 0;
playerState.pitch = 0;
player.position.set(0, 5, 0);
player.rotation.set(0, 0, 0);
for (let i = -2; i < RENDER_DIST; i++) {
generateChunk(i * CHUNK_SIZE);
}
}
function gameOver() {
game.running = false;
audio.playCrash();
document.getElementById('final-score').textContent = Math.floor(game.score).toLocaleString();
document.getElementById('final-speed').textContent = `Max Speed: ${Math.floor(game.maxSpeed * 3.6)} km/h`;
document.getElementById('final-distance').textContent = `Distance: ${Math.floor(game.distance)} m`;
document.getElementById('game-over').style.display = 'flex';
}
function updateMultiplierDisplay() {
const el = document.getElementById('multiplier');
el.textContent = `x${game.multiplier.toFixed(1)}`;
if (game.multiplier > 1) {
el.classList.add('active');
} else {
el.classList.remove('active');
}
}
// ─── MAIN UPDATE LOOP ──────────────────────────────────
let lastTime = performance.now();
let debrisTimer = 0;
let trailTimer = 0;
function update(timestamp) {
requestAnimationFrame(update);
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
if (!game.running) {
renderer.render(scene, camera);
return;
}
// ── Input handling ──
const moveSpeed = 25 * dt;
const accelForce = 15 * dt;
let inputX = 0, inputY = 0;
if (keys['ArrowLeft'] || keys['KeyA']) inputX -= 1;
if (keys['ArrowRight'] || keys['KeyD']) inputX += 1;
if (keys['ArrowUp'] || keys['KeyW']) inputY += 1;
if (keys['ArrowDown'] || keys['KeyS']) inputY -= 1;
// Boost
const boostInput = keys['ShiftLeft'] || keys['ShiftRight'];
const brakeInput = keys['Space'];
if (boostInput && game.turbo > 0) {
game.boosting = true;
game.turbo -= 30 * dt;
game.targetSpeed = BOOST_SPEED;
bloomPass.strength = 2.5;
document.getElementById('boost-flash').style.opacity = '0.5';
} else if (brakeInput) {
game.boosting = false;
game.targetSpeed = BASE_SPEED * 0.6;
bloomPass.strength = Math.max(1.2, bloomPass.strength - dt * 3);
document.getElementById('boost-flash').style.opacity = '0';
} else {
game.boosting = false;
game.targetSpeed = BASE_SPEED + (game.difficulty - 1) * 5;
bloomPass.strength += (1.2 - bloomPass.strength) * dt * 3;
document.getElementById('boost-flash').style.opacity = '0';
}
if (game.turbo <= 0) game.turbo = 0;
// ── Player physics ──
playerState.vx += inputX * accelForce;
playerState.vy += inputY * accelForce;
// Damping
playerState.vx *= Math.pow(0.92, dt * 60);
playerState.vy *= Math.pow(0.92, dt * 60);
playerState.x += playerState.vx * dt;
playerState.y += playerState.vy * dt;
// Clamp to canyon bounds (soft)
const difficulty = game.difficulty;
const canyonWidth = Math.max(20, 60 - difficulty * 3);
const canyonHeight = Math.max(15, 40 - difficulty * 1.5);
playerState.x = Math.max(-canyonWidth / 2 + 3, Math.min(canyonWidth / 2 - 3, playerState.x));
playerState.y = Math.max(-canyonHeight / 2 + 5, Math.min(canyonHeight / 2 - 5, playerState.y));
// Banking (roll) based on lateral velocity
playerState.targetRoll = -playerState.vx * 0.04;
playerState.roll += (playerState.targetRoll - playerState.roll) * dt * 8;
// Pitch based on vertical movement
playerState.pitch = -inputY * 0.15;
// Speed interpolation
game.speed += (game.targetSpeed - game.speed) * dt * 3;
game.distance += game.speed * dt;
game.maxSpeed = Math.max(game.maxSpeed, game.speed);
// Move player forward
playerState.z += game.speed * dt;
// Apply position and rotation to player mesh
player.position.set(playerState.x, playerState.y, playerState.z);
player.rotation.z = playerState.roll;
player.rotation.x = playerState.pitch;
// ── Energy decay ──
game.energy -= (2 + difficulty * 0.5) * dt;
if (game.energy <= 0) {
game.energy = 0;
gameOver();
}
// ── Difficulty scaling ──
game.difficulty = 1 + game.distance / 2000;
// ── Multiplier decay ──
if (game.nearMissTimer <= 0) {
game.multiplier += (1 - game.multiplier) * dt * 0.5;
updateMultiplierDisplay();
} else {
game.nearMissTimer -= dt;
}
// ── Chunk management ──
const currentChunkKey = Math.round(playerState.z / CHUNK_SIZE);
// Generate new chunks ahead
for (let i = -2; i < RENDER_DIST; i++) {
generateChunk((currentChunkKey + i) * CHUNK_SIZE);
}
// Remove old chunks behind
const keysToRemove = [];
chunks.forEach((chunk, key) => {
if (key < currentChunkKey - 3) {
keysToRemove.push(key);
}
});
keysToRemove.forEach(k => removeChunk(k * CHUNK_SIZE));
// ── Collision detection ──
checkCollisions();
// ── Particles ──
trailTimer += dt;
if (trailTimer > 0.03) {
spawnTrailParticle();
trailTimer = 0;
}
debrisTimer += dt;
const debrisInterval = Math.max(0.05, 0.2 - difficulty * 0.01);
if (debrisTimer > debrisInterval) {
spawnDebris(playerState.z);
debrisTimer = 0;
}
updateParticles(dt);
// ── Camera follow ──
const camDist = game.boosting ? 12 : 8;
const camHeight = game.boosting ? 5 : 3.5;
const targetCamX = playerState.x * 0.4;
const targetCamY = playerState.y + camHeight;
const targetCamZ = playerState.z - camDist;
camera.position.x += (targetCamX - camera.position.x) * dt * 5;
camera.position.y += (targetCamY - camera.position.y) * dt * 5;
camera.position.z += (targetCamZ - camera.position.z) * dt * 8;
// Camera shake
if (game.shakeAmount > 0.01) {
camera.position.x += (Math.random() - 0.5) * game.shakeAmount;
camera.position.y += (Math.random() - 0.5) * game.shakeAmount;
game.shakeAmount *= Math.pow(0.9, dt * 60);
}
// FOV based on speed
const targetFov = 75 + (game.speed / BOOST_SPEED) * 20;
camera.fov += (targetFov - camera.fov) * dt * 3;
camera.updateProjectionMatrix();
camera.lookAt(playerState.x, playerState.y, playerState.z + 20);
// ── Motion blur intensity based on speed ──
motionBlurPass.uniforms.blendFactor.value = (game.speed / BOOST_SPEED) * 0.3;
// ── Update orb rotation ──
orbs.forEach(orb => {
if (!orb.collected) {
orb.mesh.rotation.y += dt * 2;
orb.mesh.rotation.x += dt * 1.5;
orb.mesh.position.y += Math.sin(timestamp * 0.003 + orb.mesh.position.z) * 0.01;
}
});
// ── Update HUD ──
document.getElementById('score-display').textContent = Math.floor(game.score).toLocaleString();
document.getElementById('speed-bar').style.height = `${(game.speed / BOOST_SPEED) * 100}%`;
document.getElementById('turbo-bar').style.height = `${game.turbo}%`;
document.getElementById('energy-bar').style.width = `${game.energy}%`;
// Energy bar color warning
const energyBar = document.getElementById('energy-bar');
if (game.energy < 20) {
energyBar.style.background = 'linear-gradient(to right, #ff0000, #ff4444)';
} else {
energyBar.style.background = 'linear-gradient(to right, #ff6a00, #ee0979)';
}
// Audio engine pitch
audio.setEnginePitch(game.speed / BOOST_SPEED);
// ── Render ──
composer.render();
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
// ─── START ─────────────────────────────────────────────
init();
requestAnimationFrame(update);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"full","sessionId":"a6d21427-76be-4500-abb3-0705b6e9cbb7","tokensIn":522963,"tokensOut":17430,"tokensTotal":540393,"turns":22,"toolCalls":21,"failedToolCalls":0,"timestamp":"2026-07-18T20:09:56.236Z"} -->