Canyon Run
thinkingcap qwen3.6 27b · middle
◈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 41,200 bytes · SHA-256 8cea1965c8d8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canyon Rush</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #0a0510; font-family: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; }
#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: 6px; height: 200px; background: rgba(255,255,255,0.1);
border-radius: 3px; overflow: hidden;
}
#speed-bar {
position: absolute; bottom: 0; width: 100%; height: 60%;
background: linear-gradient(to top, #ff6a00, #ffcc00);
border-radius: 3px; transition: height 0.1s;
}
#multiplier {
position: absolute; right: 50px; bottom: 250px;
font-size: 48px; font-weight: 900; color: #ffcc00;
text-shadow: 0 0 20px rgba(255,204,0,0.6);
opacity: 0; transition: opacity 0.3s, transform 0.1s;
}
#multiplier.visible { opacity: 1; }
#multiplier.pulse { transform: scale(1.3); }
#energy-container {
position: absolute; left: 50%; bottom: 20px;
transform: translateX(-50%);
width: 300px; height: 8px; background: rgba(255,255,255,0.1);
border-radius: 4px; overflow: hidden;
}
#energy-bar {
height: 100%; width: 100%;
background: linear-gradient(to right, #ff3366, #ff9900, #00ffaa);
border-radius: 4px; transition: width 0.15s;
}
#turbo-container {
position: absolute; left: 30px; bottom: 40px;
width: 6px; height: 200px; background: rgba(255,255,255,0.1);
border-radius: 3px; overflow: hidden;
}
#turbo-bar {
position: absolute; bottom: 0; width: 100%; height: 0%;
background: linear-gradient(to top, #00aaff, #aa44ff);
border-radius: 3px; transition: height 0.1s;
}
#score-display {
position: absolute; left: 50%; top: 20px;
transform: translateX(-50%);
font-size: 28px; font-weight: 700; color: #fff;
text-shadow: 0 0 10px rgba(255,255,255,0.4);
letter-spacing: 2px;
}
#game-over {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(10,5,16,0.92); display: none;
flex-direction: column; align-items: center; justify-content: center;
z-index: 100; pointer-events: all;
}
#game-over h1 {
font-size: 64px; color: #ff3366; margin-bottom: 20px;
text-shadow: 0 0 40px rgba(255,51,102,0.5);
}
#game-over .stat {
font-size: 22px; color: #ccc; margin: 8px 0; letter-spacing: 1px;
}
#game-over .stat span { color: #ffcc00; font-weight: 700; }
#restart-btn {
margin-top: 30px; padding: 14px 50px;
background: linear-gradient(135deg, #ff6a00, #ee0979);
border: none; color: #fff; font-size: 20px; font-weight: 700;
border-radius: 8px; cursor: pointer; letter-spacing: 2px;
transition: transform 0.15s, box-shadow 0.15s;
}
#restart-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 30px rgba(238,9,121,0.6);
}
#start-screen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(ellipse at center, #1a0a2e 0%, #0a0510 70%);
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 200; pointer-events: all;
}
#start-screen h1 {
font-size: 72px; color: #ffcc00; margin-bottom: 10px;
text-shadow: 0 0 60px rgba(255,204,0,0.4);
letter-spacing: 6px;
}
#start-screen p {
font-size: 18px; color: #aaa; margin-bottom: 30px;
text-align: center; line-height: 1.6;
}
#start-btn {
padding: 16px 60px; background: linear-gradient(135deg, #ff6a00, #ee0979);
border: none; color: #fff; font-size: 24px; font-weight: 700;
border-radius: 8px; cursor: pointer; letter-spacing: 3px;
transition: transform 0.15s, box-shadow 0.15s;
}
#start-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 40px rgba(238,9,121,0.6);
}
#controls-hint {
position: absolute; bottom: 60px; left: 50%; transform: translateX(-50%);
font-size: 14px; color: #666; letter-spacing: 1px;
}
</style>
</head>
<body>
<div id="hud">
<div id="score-display">0</div>
<div id="multiplier"></div>
<div id="speed-bar-container"><div id="speed-bar"></div></div>
<div id="turbo-container"><div id="turbo-bar"></div></div>
<div id="energy-container"><div id="energy-bar"></div></div>
</div>
<div id="near-miss-flash" style="position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;opacity:0;background:radial-gradient(ellipse at center,transparent 40%,rgba(255,170,0,0.3) 100%);transition:opacity 0.1s;"></div>
<div id="vignette" style="position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:4;background:radial-gradient(ellipse at center,transparent 50%,rgba(10,5,16,0.7) 100%);"></div>
<div id="game-over">
<h1>CRAASHED</h1>
<div class="stat">Final Score: <span id="final-score">0</span></div>
<div class="stat">Max Speed: <span id="max-speed">0</span></div>
<div class="stat">Distance: <span id="distance">0</span>m</div>
<button id="restart-btn" onclick="restartGame()">RESTART</button>
</div>
<div id="start-screen">
<h1>CANYON RUSH</h1>
<p>Race through an infinite canyon at breakneck speed.<br>Avoid walls. Collect orbs. Don't run out of energy.</p>
<button id="start-btn" onclick="startGame()">LAUNCH</button>
<div id="controls-hint">W/S or ↑/↓ — Pitch | A/D or ←/→ — Roll | SPACE — Boost</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
"three/addons/": "https://unpkg.com/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';
// ─── GLOBALS ──────────────────────────────────────
let scene, camera, renderer, composer, bloomPass;
let player, playerBody;
let clock = new THREE.Clock();
let gameActive = false;
let score = 0, maxSpeedVal = 0, distanceTraveled = 0;
let energy = 100, turbo = 0;
let speed = 0.8, baseSpeed = 0.8, boostSpeed = 2.5;
let multiplier = 1, nearMissTimer = 0;
let difficulty = 1;
// Physics
let velocity = new THREE.Vector3();
let angularVel = { roll: 0, pitch: 0 };
const MAX_ROLL = Math.PI * 0.4;
const MAX_PITCH = Math.PI * 0.25;
// Input
const keys = {};
// Chunk system
const CHUNK_LENGTH = 200;
const CHUNK_COUNT = 8;
let chunks = [];
let nextChunkZ = 0;
// Particles
let trailParticles = [];
let debrisParticles = [];
let speedLines = null;
// Audio
let audioCtx, engineOsc, engineGain;
// ─── AUDIO SYSTEM ─────────────────────────────────
function initAudio() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// Engine hum
engineOsc = audioCtx.createOscillator();
engineGain = audioCtx.createGain();
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 300;
engineOsc.type = 'sawtooth';
engineOsc.frequency.value = 60;
engineGain.gain.value = 0.04;
engineOsc.connect(filter);
filter.connect(engineGain);
engineGain.connect(audioCtx.destination);
engineOsc.start();
}
function updateEngineSound() {
if (!engineOsc) return;
const t = audioCtx.currentTime;
engineOsc.frequency.setTargetAtTime(50 + speed * 120, t, 0.1);
engineGain.gain.setTargetAtTime(0.03 + speed * 0.04, t, 0.1);
}
function playPing() {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(880, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1760, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.3);
}
function playNearMiss() {
if (!audioCtx) return;
const bufferSize = audioCtx.sampleRate * 0.15;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3));
}
const source = audioCtx.createBufferSource();
source.buffer = buffer;
const gain = audioCtx.createGain();
gain.gain.value = 0.08;
const filter = audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 2000;
source.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
source.start();
}
function playCrash() {
if (!audioCtx) return;
const bufferSize = audioCtx.sampleRate * 0.8;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.4));
}
const source = audioCtx.createBufferSource();
source.buffer = buffer;
const gain = audioCtx.createGain();
gain.gain.value = 0.3;
source.connect(gain);
gain.connect(audioCtx.destination);
source.start();
}
// ─── INIT THREE.JS ────────────────────────────────
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);
composer.addPass(new RenderPass(scene, camera));
bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, 0.4, 0.85
);
bloomPass.threshold = 0.3;
bloomPass.strength = 1.8;
bloomPass.radius = 0.6;
composer.addPass(bloomPass);
// Lighting - Golden hour / cyber sunset
const ambientLight = new THREE.AmbientLight(0x4a2060, 0.6);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xff8833, 2.5);
sunLight.position.set(100, 80, -200);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 600;
sunLight.shadow.camera.left = -150;
sunLight.shadow.camera.right = 150;
sunLight.shadow.camera.top = 100;
sunLight.shadow.camera.bottom = -100;
scene.add(sunLight);
const rimLight = new THREE.DirectionalLight(0x6633cc, 0.8);
rimLight.position.set(-50, 30, 100);
scene.add(rimLight);
// Sky dome
createSky();
// Player craft
createPlayer();
// Initial chunks
for (let i = 0; i < CHUNK_COUNT; i++) {
generateChunk(nextChunkZ);
nextChunkZ -= CHUNK_LENGTH;
}
// Speed lines
createSpeedLines();
window.addEventListener('resize', onResize);
window.addEventListener('keydown', e => keys[e.code] = true);
window.addEventListener('keyup', e => keys[e.code] = false);
}
function createSky() {
const skyGeo = new THREE.SphereGeometry(900, 16, 12);
const skyMat = new THREE.ShaderMaterial({
side: THREE.BackSide,
uniforms: {},
vertexShader: `
varying vec3 vWorldPos;
void main() {
vWorldPos = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec3 vWorldPos;
void main() {
float h = normalize(vWorldPos).y * 0.5 + 0.5;
vec3 bottom = vec3(0.6, 0.15, 0.05);
vec3 mid = vec3(0.2, 0.08, 0.3);
vec3 top = vec3(0.04, 0.02, 0.1);
vec3 col = mix(bottom, mid, smoothstep(0.0, 0.5, h));
col = mix(col, top, smoothstep(0.4, 1.0, h));
gl_FragColor = vec4(col, 1.0);
}
`
});
scene.add(new THREE.Mesh(skyGeo, skyMat));
// Stars
const starCount = 500;
const starPositions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(Math.random() * 0.8 + 0.2);
const r = 850;
starPositions[i*3] = r * Math.sin(phi) * Math.cos(theta);
starPositions[i*3+1] = r * Math.cos(phi);
starPositions[i*3+2] = r * Math.sin(phi) * Math.sin(theta);
}
const starGeo = new THREE.BufferGeometry();
starGeo.setAttribute('position', new THREE.Float32BufferAttribute(starPositions, 3));
const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 1.5, transparent: true, opacity: 0.6 });
scene.add(new THREE.Points(starGeo, starMat));
}
function createPlayer() {
player = new THREE.Group();
// Main body - sleek arrow shape
const bodyShape = new THREE.ConeGeometry(0.5, 3, 4);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0xcccccc, flatShading: true, metalness: 0.8, roughness: 0.2
});
playerBody = new THREE.Mesh(bodyShape, bodyMat);
playerBody.rotation.x = Math.PI / 2;
playerBody.castShadow = true;
player.add(playerBody);
// Wings
const wingGeo = new THREE.BoxGeometry(4, 0.15, 1.2);
const wingMat = new THREE.MeshStandardMaterial({
color: 0x8844aa, flatShading: true, metalness: 0.6, roughness: 0.3
});
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.z = 0.2;
wings.castShadow = true;
player.add(wings);
// Engine glow
const engineGeo = new THREE.SphereGeometry(0.35, 6, 4);
const engineMat = new THREE.MeshBasicMaterial({ color: 0xff6600 });
const engineGlow = new THREE.Mesh(engineGeo, engineMat);
engineGlow.position.z = 1.6;
player.add(engineGlow);
// Engine light
const eLight = new THREE.PointLight(0xff4400, 3, 15);
eLight.position.z = 2;
player.add(eLight);
player.userData.engineGlow = engineGlow;
player.userData.engineLight = eLight;
player.position.set(0, 5, 0);
scene.add(player);
}
// ─── CHUNK GENERATION ─────────────────────────────
function generateChunk(zPos) {
const chunkGroup = new THREE.Group();
chunkGroup.userData = { z: zPos, obstacles: [], orbs: [] };
// Canyon floor
const floorGeo = new THREE.PlaneGeometry(120, CHUNK_LENGTH);
const floorMat = new THREE.MeshStandardMaterial({
color: 0x2a1530, flatShading: true, roughness: 0.9
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.position.set(0, -8, zPos - CHUNK_LENGTH / 2);
floor.receiveShadow = true;
chunkGroup.add(floor);
// Canyon ceiling
const ceilGeo = new THREE.PlaneGeometry(120, CHUNK_LENGTH);
const ceilMat = new THREE.MeshStandardMaterial({
color: 0x1a0a25, flatShading: true, roughness: 0.9
});
const ceiling = new THREE.Mesh(ceilGeo, ceilMat);
ceiling.rotation.x = Math.PI / 2;
ceiling.position.set(0, 35, zPos - CHUNK_LENGTH / 2);
chunkGroup.add(ceiling);
// Ground details - scattered rocks and ridges
const rockCount = 8 + Math.floor(Math.random() * 6);
for (let r = 0; r < rockCount; r++) {
const rh = 1 + Math.random() * 3;
const rw = 1 + Math.random() * 2;
const rockGeo = new THREE.ConeGeometry(rw, rh, 4);
const rockMat = new THREE.MeshStandardMaterial({
color: new THREE.Color(0.15 + Math.random()*0.1, 0.08, 0.12), flatShading: true
});
const rock = new THREE.Mesh(rockGeo, rockMat);
rock.position.set(
(Math.random() - 0.5) * 70,
rh / 2 - 8,
zPos - Math.random() * CHUNK_LENGTH
);
rock.rotation.y = Math.random() * Math.PI;
chunkGroup.add(rock);
}
// Glowing ground lines (path markers)
const lineCount = 4;
for (let l = 0; l < lineCount; l++) {
const lineGeo = new THREE.BoxGeometry(0.3, 0.1, 8 + Math.random() * 12);
const lineMat = new THREE.MeshBasicMaterial({
color: 0xff6600, transparent: true, opacity: 0.4
});
const line = new THREE.Mesh(lineGeo, lineMat);
line.position.set(
(Math.random() - 0.5) * 50,
-7.8,
zPos - Math.random() * CHUNK_LENGTH
);
chunkGroup.add(line);
}
// Canyon walls with procedural variation
const wallSegments = 12;
for (let i = 0; i < wallSegments; i++) {
const segZ = zPos - (i / wallSegments) * CHUNK_LENGTH;
const widthVariation = Math.sin(i * 0.7 + zPos * 0.01) * 25 + 35;
// Left wall
createWallSegment(chunkGroup, -widthVariation, segZ, i);
// Right wall
createWallSegment(chunkGroup, widthVariation, segZ, i);
}
// Obstacles based on difficulty
const obstacleCount = Math.floor(2 + difficulty * 1.5);
for (let i = 0; i < obstacleCount; i++) {
const type = Math.random();
if (type < 0.4) {
createMonolith(chunkGroup, zPos - Math.random() * CHUNK_LENGTH);
} else if (type < 0.7) {
createSqueeze(chunkGroup, zPos - Math.random() * CHUNK_LENGTH);
} else {
createWallPillar(chunkGroup, zPos - Math.random() * CHUNK_LENGTH);
}
}
// Energy orbs
const orbCount = Math.floor(3 + difficulty * 0.5);
for (let i = 0; i < orbCount; i++) {
createOrb(chunkGroup, zPos - Math.random() * CHUNK_LENGTH);
}
scene.add(chunkGroup);
chunks.push(chunkGroup);
}
function createWallSegment(group, xPos, zPos, index) {
const height = 30 + Math.sin(index * 1.3) * 15;
const depth = CHUNK_LENGTH / wallSegmentsCount() - 2;
const geo = new THREE.BoxGeometry(8, height, depth);
const hueShift = (index * 0.07) % 1;
const r = 0.3 + Math.sin(hueShift * Math.PI * 2) * 0.15;
const g = 0.1 + Math.cos(hueShift * Math.PI * 1.5) * 0.08;
const b = 0.2 + Math.sin(hueShift * Math.PI * 0.7) * 0.1;
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color(r, g, b), flatShading: true, roughness: 0.85
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(xPos, height / 2 - 8, zPos);
mesh.castShadow = true;
mesh.receiveShadow = true;
// Add collision data
mesh.userData.isObstacle = true;
mesh.userData.radius = Math.max(4, height * 0.3);
group.userData.obstacles.push(mesh);
group.add(mesh);
// Jagged top
const jagCount = 3 + Math.floor(Math.random() * 3);
for (let j = 0; j < jagCount; j++) {
const jh = 5 + Math.random() * 12;
const jw = 2 + Math.random() * 4;
const jGeo = new THREE.ConeGeometry(jw, jh, 3);
const jMesh = new THREE.Mesh(jGeo, mat.clone());
jMesh.position.set(
xPos + (Math.random() - 0.5) * 6,
height / 2 - 8 + jh / 2,
zPos + (Math.random() - 0.5) * (depth / 2)
);
jMesh.castShadow = true;
group.add(jMesh);
}
}
function wallSegmentsCount() { return 12; }
function createMonolith(group, zPos) {
const height = 15 + Math.random() * 20;
const width = 3 + Math.random() * 4;
const geo = new THREE.BoxGeometry(width, height, width);
const mat = new THREE.MeshStandardMaterial({
color: 0x663399, flatShading: true, metalness: 0.5, roughness: 0.3
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set((Math.random() - 0.5) * 40, height / 2 - 8 + Math.random() * 10, zPos);
mesh.castShadow = true;
mesh.userData.isObstacle = true;
mesh.userData.radius = width * 0.7;
group.userData.obstacles.push(mesh);
group.add(mesh);
// Glow edges
const edgeGeo = new THREE.BoxGeometry(width + 0.3, height + 0.3, width + 0.3);
const edgeMat = new THREE.MeshBasicMaterial({
color: 0xaa44ff, transparent: true, opacity: 0.15, wireframe: true
});
group.add(new THREE.Mesh(edgeGeo, edgeMat).translateX(mesh.position.x)
.translateY(mesh.position.y).translateZ(mesh.position.z));
}
function createSqueeze(group, zPos) {
// Narrow gap - two walls close together with a passage through the middle
const gapWidth = 6 + Math.random() * 4;
const wallHeight = 25 + Math.random() * 10;
for (let side = -1; side <= 1; side += 2) {
const geo = new THREE.BoxGeometry(3, wallHeight, 8);
const mat = new THREE.MeshStandardMaterial({
color: 0x442266, flatShading: true, roughness: 0.7
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(side * (gapWidth / 2 + 1), wallHeight / 2 - 8, zPos);
mesh.castShadow = true;
mesh.userData.isObstacle = true;
mesh.userData.radius = 3.5;
group.userData.obstacles.push(mesh);
group.add(mesh);
}
// Top bar to force going through the gap
const topGeo = new THREE.BoxGeometry(gapWidth + 6, 2, 8);
const topMat = new THREE.MeshStandardMaterial({
color: 0x553377, flatShading: true
});
const topMesh = new THREE.Mesh(topGeo, topMat);
topMesh.position.set(0, wallHeight - 8, zPos);
topMesh.castShadow = true;
topMesh.userData.isObstacle = true;
topMesh.userData.radius = 2.5;
group.userData.obstacles.push(topMesh);
group.add(topMesh);
}
function createWallPillar(group, zPos) {
const geo = new THREE.CylinderGeometry(1.5, 3, 20 + Math.random() * 15, 5);
const mat = new THREE.MeshStandardMaterial({
color: 0x774488, flatShading: true, roughness: 0.6
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set((Math.random() - 0.5) * 30, Math.random() * 15, zPos);
mesh.castShadow = true;
mesh.userData.isObstacle = true;
mesh.userData.radius = 3;
group.userData.obstacles.push(mesh);
group.add(mesh);
}
function createOrb(group, zPos) {
const geo = new THREE.OctahedronGeometry(0.8, 0);
const mat = new THREE.MeshBasicMaterial({ color: 0x00ffaa });
const mesh = new THREE.Mesh(geo, mat);
// Place orbs in interesting positions - sometimes through gaps
const x = (Math.random() - 0.5) * 35;
const y = Math.random() * 20 + 2;
mesh.position.set(x, y, zPos);
const orbLight = new THREE.PointLight(0x00ffaa, 1.5, 8);
mesh.add(orbLight);
mesh.userData.isOrb = true;
group.userData.orbs.push(mesh);
group.add(mesh);
}
// ─── PARTICLES ────────────────────────────────────
function spawnTrailParticle() {
const geo = new THREE.SphereGeometry(0.15, 3, 2);
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.08 + Math.random() * 0.05, 1, 0.6),
transparent: true, opacity: 0.9
});
const p = new THREE.Mesh(geo, mat);
p.position.copy(player.position).add(new THREE.Vector3(
(Math.random() - 0.5) * 0.5,
(Math.random() - 0.5) * 0.5,
1.8 + Math.random() * 0.5
));
p.userData.life = 1.0;
scene.add(p);
trailParticles.push(p);
}
function spawnDebrisParticle() {
const geo = new THREE.TetrahedronGeometry(0.2, 0);
const mat = new THREE.MeshBasicMaterial({
color: 0xffaa44, transparent: true, opacity: 0.7
});
const p = new THREE.Mesh(geo, mat);
// Spawn from the sides to simulate passing by terrain
const side = Math.random() > 0.5 ? 1 : -1;
p.position.set(
player.position.x + side * (3 + Math.random() * 8),
player.position.y + (Math.random() - 0.5) * 10,
player.position.z - 20 - Math.random() * 30
);
p.userData.life = 1.0;
p.userData.vel = new THREE.Vector3(
side * (1 + Math.random()),
(Math.random() - 0.5) * 0.5,
speed * 40
);
scene.add(p);
debrisParticles.push(p);
}
function updateParticles(dt) {
// Trail particles
for (let i = trailParticles.length - 1; i >= 0; i--) {
const p = trailParticles[i];
p.userData.life -= dt * 2.5;
p.material.opacity = Math.max(0, p.userData.life);
p.scale.setScalar(p.userData.life);
if (p.userData.life <= 0) {
scene.remove(p);
p.geometry.dispose();
p.material.dispose();
trailParticles.splice(i, 1);
}
}
// Debris particles
for (let i = debrisParticles.length - 1; i >= 0; i--) {
const p = debrisParticles[i];
p.userData.life -= dt * 2;
p.position.add(p.userData.vel.clone().multiplyScalar(dt));
p.material.opacity = Math.max(0, p.userData.life);
p.rotation.x += dt * 5;
p.rotation.y += dt * 3;
if (p.userData.life <= 0) {
scene.remove(p);
p.geometry.dispose();
p.material.dispose();
debrisParticles.splice(i, 1);
}
}
}
// ─── SPEED LINES ──────────────────────────────────
function createSpeedLines() {
const count = 120;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
resetSpeedLine(i, positions, colors);
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
const mat = new THREE.PointsMaterial({
size: 0.5, vertexColors: true, transparent: true, opacity: 0.6,
blending: THREE.AdditiveBlending, depthWrite: false
});
speedLines = new THREE.Points(geo, mat);
scene.add(speedLines);
}
function resetSpeedLine(i, positions, colors) {
const angle = Math.random() * Math.PI * 2;
const radius = 5 + Math.random() * 40;
positions[i*3] = player.position.x + Math.cos(angle) * radius;
positions[i*3+1] = player.position.y + (Math.random() - 0.5) * 30;
positions[i*3+2] = player.position.z - Math.random() * 80;
const brightness = 0.5 + Math.random() * 0.5;
colors[i*3] = brightness;
colors[i*3+1] = brightness * 0.7;
colors[i*3+2] = brightness * 0.4;
}
function updateSpeedLines(dt) {
if (!speedLines || !gameActive) return;
const posAttr = speedLines.geometry.attributes.position;
const colAttr = speedLines.geometry.attributes.color;
const positions = posAttr.array;
const colors = colAttr.array;
// Only show at higher speeds
const intensity = Math.max(0, (speed - baseSpeed * 1.2) / (boostSpeed - baseSpeed));
speedLines.material.opacity = intensity * 0.7;
for (let i = 0; i < positions.length / 3; i++) {
// Move lines toward player (positive Z direction relative to world)
positions[i*3+2] += speed * 60 * dt;
// Reset if passed the camera
if (positions[i*3+2] > player.position.z + 15) {
resetSpeedLine(i, positions, colors);
}
}
posAttr.needsUpdate = true;
}
// ─── CAMERA SHAKE ─────────────────────────────────
let shakeIntensity = 0;
function addShake(amount) { shakeIntensity = Math.min(shakeIntensity + amount, 2.0); }
// ─── PHYSICS UPDATE ──────────────────────────────
function updatePhysics(dt) {
if (!gameActive) return;
// Input handling
const rollInput = (keys['ArrowLeft'] || keys['KeyA']) ? -1 : (keys['ArrowRight'] || keys['KeyD']) ? 1 : 0;
const pitchInput = (keys['ArrowUp'] || keys['KeyW']) ? -1 : (keys['ArrowDown'] || keys['KeyS']) ? 1 : 0;
// Roll physics with momentum
angularVel.roll += rollInput * dt * 6.0;
angularVel.roll *= Math.pow(0.05, dt); // damping
const targetRoll = -angularVel.roll;
player.rotation.z += (targetRoll - player.rotation.z) * dt * 8;
player.rotation.z = THREE.MathUtils.clamp(player.rotation.z, -MAX_ROLL, MAX_ROLL);
// Pitch physics
angularVel.pitch += pitchInput * dt * 5.0;
angularVel.pitch *= Math.pow(0.05, dt);
const targetPitch = -angularVel.pitch;
player.rotation.x += (targetPitch - player.rotation.x) * dt * 8;
player.rotation.x = THREE.MathUtils.clamp(player.rotation.x, -MAX_PITCH, MAX_PITCH);
// Movement based on roll/pitch
const moveSpeed = speed * 35;
velocity.x = Math.sin(player.rotation.z) * moveSpeed;
velocity.y = -Math.sin(player.rotation.x) * moveSpeed * 0.7;
// Boost
let isBoosting = keys['Space'] && turbo > 0;
if (isBoosting) {
speed += dt * 3;
turbo -= dt * 25;
addShake(0.1);
} else {
speed += (baseSpeed - speed) * dt * 2;
}
// Speed cap
const maxSpd = baseSpeed + difficulty * 0.3;
if (!isBoosting) speed = Math.min(speed, maxSpd);
speed = Math.max(speed, 0.5);
// Move player forward (negative Z)
velocity.z = -speed * 40;
const moveDelta = velocity.clone().multiplyScalar(dt);
player.position.add(moveDelta);
// Clamp lateral position
player.position.x = THREE.MathUtils.clamp(player.position.x, -50, 50);
player.position.y = THREE.MathUtils.clamp(player.position.y, -4, 32);
// Distance tracking
distanceTraveled += Math.abs(moveDelta.z);
// Max speed tracking
if (speed > maxSpeedVal) maxSpeedVal = speed;
// Energy decay
energy -= dt * (1.5 + difficulty * 0.3);
if (energy <= 0) {
gameOver();
return;
}
// Turbo build from straightaways
if (!isBoosting && Math.abs(rollInput) < 0.5) {
turbo += dt * 8;
}
turbo = Math.min(turbo, 100);
// Difficulty scaling
difficulty = 1 + score / 2000;
updateEngineSound();
}
// ─── COLLISION DETECTION ──────────────────────────
let lastNearMissTime = 0;
function checkCollisions(dt) {
if (!gameActive) return;
const playerPos = player.position;
const playerRadius = 1.5;
for (const chunk of chunks) {
// Check obstacles
for (const obs of chunk.userData.obstacles) {
if (!obs.userData.isObstacle) continue;
const dx = playerPos.x - obs.position.x;
const dy = playerPos.y - obs.position.y;
const dz = playerPos.z - obs.position.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
const minDist = playerRadius + (obs.userData.radius || 2);
// Collision
if (dist < minDist * 0.65) {
playCrash();
gameOver();
return;
}
// Near miss (with cooldown to prevent spamming)
const now = clock.elapsedTime;
if (dist < minDist * 1.8 && dist > minDist * 0.65 && now - lastNearMissTime > 0.3) {
nearMissTimer = 0.5;
multiplier = Math.min(multiplier + 0.5, 10);
score += Math.floor(50 * multiplier);
addShake(0.3);
playNearMiss();
lastNearMissTime = now;
// Flash effect
const flashEl = document.getElementById('near-miss-flash');
if (flashEl) {
flashEl.style.opacity = '1';
setTimeout(() => flashEl.style.opacity = '0', 150);
}
}
}
// Check orbs
for (let i = chunk.userData.orbs.length - 1; i >= 0; i--) {
const orb = chunk.userData.orbs[i];
if (!orb || !orb.visible) continue;
const dx = playerPos.x - orb.position.x;
const dy = playerPos.y - orb.position.y;
const dz = playerPos.z - orb.position.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (dist < 3) {
energy = Math.min(energy + 15, 100);
score += Math.floor(25 * multiplier);
playPing();
scene.remove(orb);
chunk.userData.orbs.splice(i, 1);
}
}
}
// Near miss timer decay
if (nearMissTimer > 0) {
nearMissTimer -= dt;
} else {
multiplier = Math.max(1, multiplier - 0.3 * dt);
}
// Score from distance
score += speed * dt * 5 * multiplier;
}
// ─── CHUNK MANAGEMENT ─────────────────────────────
function updateChunks() {
const playerZ = player.position.z;
// Remove old chunks
for (let i = chunks.length - 1; i >= 0; i--) {
if (chunks[i].userData.z > playerZ + CHUNK_LENGTH * 2) {
scene.remove(chunks[i]);
// Dispose geometries and materials
chunks[i].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();
}
}
});
chunks.splice(i, 1);
}
}
// Generate new chunks ahead
while (nextChunkZ > playerZ - CHUNK_LENGTH * (CHUNK_COUNT - 2)) {
generateChunk(nextChunkZ);
nextChunkZ -= CHUNK_LENGTH;
}
}
// ─── CAMERA UPDATE ────────────────────────────────
function updateCamera(dt) {
const camDist = 12 + speed * 3;
const camHeight = 5 + speed * 2;
// FOV based on speed
const targetFov = 70 + (speed - baseSpeed) * 20;
camera.fov += (targetFov - camera.fov) * dt * 3;
camera.updateProjectionMatrix();
// Camera position behind player
const idealPos = new THREE.Vector3(
player.position.x * 0.4,
player.position.y + camHeight,
player.position.z + camDist
);
camera.position.lerp(idealPos, dt * 5);
// Look ahead of player
const lookTarget = new THREE.Vector3(
player.position.x * 0.6,
player.position.y + 1,
player.position.z - 30
);
camera.lookAt(lookTarget);
// Camera shake
if (shakeIntensity > 0) {
camera.position.x += (Math.random() - 0.5) * shakeIntensity;
camera.position.y += (Math.random() - 0.5) * shakeIntensity * 0.5;
shakeIntensity *= Math.pow(0.1, dt);
if (shakeIntensity < 0.01) shakeIntensity = 0;
}
}
// ─── HUD UPDATE ───────────────────────────────────
function updateHUD() {
document.getElementById('score-display').textContent = Math.floor(score).toLocaleString();
// Speed bar
const speedPct = ((speed - 0.5) / (boostSpeed - 0.5)) * 100;
document.getElementById('speed-bar').style.height = Math.max(5, speedPct) + '%';
// Turbo bar
document.getElementById('turbo-bar').style.height = turbo + '%';
// Energy bar
document.getElementById('energy-bar').style.width = energy + '%';
// Multiplier display
const multEl = document.getElementById('multiplier');
if (nearMissTimer > 0 || multiplier > 1) {
multEl.textContent = '×' + multiplier.toFixed(1);
multEl.classList.add('visible');
if (nearMissTimer > 0.3) multEl.classList.add('pulse');
else multEl.classList.remove('pulse');
} else {
multEl.classList.remove('visible', 'pulse');
}
// Energy bar color warning
const eBar = document.getElementById('energy-bar');
if (energy < 25) {
eBar.style.background = '#ff3366';
eBar.style.boxShadow = '0 0 10px #ff3366';
// Red warning pulse on screen edges
const vignette = document.getElementById('vignette');
if (vignette && Math.sin(clock.elapsedTime * 4) > 0.5) {
vignette.style.background = 'radial-gradient(ellipse at center,transparent 40%,rgba(120,10,10,0.8) 100%)';
}
} else {
eBar.style.background = '';
eBar.style.boxShadow = '';
const vignette = document.getElementById('vignette');
if (vignette && !keys['Space']) {
vignette.style.background = 'radial-gradient(ellipse at center,transparent 50%,rgba(10,5,16,0.7) 100%)';
}
}
}
// ─── GAME OVER / START ────────────────────────────
function gameOver() {
gameActive = false;
playCrash();
document.getElementById('final-score').textContent = Math.floor(score).toLocaleString();
document.getElementById('max-speed').textContent = (maxSpeedVal * 100).toFixed(0) + ' km/h';
document.getElementById('distance').textContent = Math.floor(distanceTraveled);
document.getElementById('game-over').style.display = 'flex';
if (engineOsc) {
engineGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.1);
}
}
window.startGame = function() {
document.getElementById('start-screen').style.display = 'none';
initAudio();
gameActive = true;
score = 0; maxSpeedVal = 0; distanceTraveled = 0;
energy = 100; turbo = 0; speed = baseSpeed; multiplier = 1; difficulty = 1;
nearMissTimer = 0; shakeIntensity = 0;
player.position.set(0, 5, 0);
player.rotation.set(0, 0, 0);
velocity.set(0, 0, 0);
};
window.restartGame = function() {
// Clear all chunks
for (const chunk of chunks) {
scene.remove(chunk);
chunk.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();
}
});
}
chunks = [];
nextChunkZ = 0;
// Clear particles
for (const p of trailParticles) { scene.remove(p); p.geometry?.dispose(); p.material?.dispose(); }
trailParticles.length = 0;
for (const p of debrisParticles) { scene.remove(p); p.geometry?.dispose(); p.material?.dispose(); }
debrisParticles.length = 0;
document.getElementById('game-over').style.display = 'none';
// Regenerate chunks
for (let i = 0; i < CHUNK_COUNT; i++) {
generateChunk(nextChunkZ);
nextChunkZ -= CHUNK_LENGTH;
}
score = 0; maxSpeedVal = 0; distanceTraveled = 0;
energy = 100; turbo = 0; speed = baseSpeed; multiplier = 1; difficulty = 1;
nearMissTimer = 0; shakeIntensity = 0;
player.position.set(0, 5, 0);
player.rotation.set(0, 0, 0);
velocity.set(0, 0, 0);
// Reset visual effects
if (player.userData.engineGlow) {
player.userData.engineGlow.material.color.setHex(0xff6600);
}
const vignette = document.getElementById('vignette');
if (vignette) {
vignette.style.background = 'radial-gradient(ellipse at center,transparent 50%,rgba(10,5,16,0.7) 100%)';
}
gameActive = true;
};
// ─── ANIMATION LOOP ──────────────────────────────
let trailTimer = 0;
let debrisTimer = 0;
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05); // cap delta to prevent spiral
if (gameActive) {
updatePhysics(dt);
checkCollisions(dt);
updateChunks();
// Spawn particles
trailTimer += dt;
debrisTimer += dt;
if (trailTimer > 0.03) { spawnTrailParticle(); trailTimer = 0; }
if (debrisTimer > 0.15 / Math.max(speed, 0.5)) { spawnDebrisParticle(); debrisTimer = 0; }
// Animate orbs
for (const chunk of chunks) {
const t = clock.elapsedTime;
for (const orb of chunk.userData.orbs) {
if (!orb.visible) continue;
orb.rotation.y += dt * 3;
orb.position.y += Math.sin(t * 2 + orb.position.z) * dt * 0.5;
}
}
updateParticles(dt);
updateSpeedLines(dt);
// Engine glow pulse
if (player.userData.engineGlow) {
const pulse = 1 + Math.sin(clock.elapsedTime * 8) * 0.2;
player.userData.engineGlow.scale.setScalar(pulse * (0.8 + speed * 0.5));
player.userData.engineLight.intensity = 3 + speed * 4;
// Boost color shift
const isBoostingNow = keys['Space'] && turbo > 0;
if (isBoostingNow) {
player.userData.engineGlow.material.color.setHex(0x8844ff);
player.userData.engineLight.color.setHex(0x6633cc);
// Boost screen effect
const vignette = document.getElementById('vignette');
if (vignette) {
vignette.style.background = 'radial-gradient(ellipse at center,transparent 45%,rgba(80,20,120,0.6) 100%)';
}
} else {
player.userData.engineGlow.material.color.setHex(0xff6600);
player.userData.engineLight.color.setHex(0xff4400);
const vignette = document.getElementById('vignette');
if (vignette) {
vignette.style.background = 'radial-gradient(ellipse at center,transparent 50%,rgba(10,5,16,0.7) 100%)';
}
}
}
updateCamera(dt);
updateHUD();
// Bloom intensity based on speed
bloomPass.strength = 1.5 + (speed - baseSpeed) * 2;
} else {
// Idle camera rotation when not playing
if (player) {
player.rotation.y += dt * 0.3;
}
}
composer.render();
}
// ─── RESIZE ───────────────────────────────────────
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
// ─── BOOT ─────────────────────────────────────────
init();
animate();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"middle","sessionId":"465f3e0b-3e32-45c3-b122-0ddce057d4b8","tokensIn":770529,"tokensOut":20832,"tokensTotal":791361,"turns":32,"toolCalls":31,"failedToolCalls":0,"timestamp":"2026-07-18T22:38:23.288Z"} -->