Canyon Run
thinkingcap qwen3.6 27b · basic

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 34,097 bytes · SHA-256 247d0ccc4bde
<!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', Arial, sans-serif; cursor: none; }
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: 8px; height: 200px; background: rgba(255,255,255,0.1);
border-radius: 4px; overflow: hidden;
}
#speed-bar {
position: absolute; bottom: 0; width: 100%; height: 50%;
background: linear-gradient(to top, #ff6a00, #ffcc00);
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.05);
border-radius: 4px; overflow: hidden;
}
#turbo-bar {
position: absolute; bottom: 0; width: 100%; height: 0%;
background: linear-gradient(to top, #ff006a, #ff6a00);
border-radius: 4px; transition: height 0.15s;
}
#multiplier {
position: absolute; right: 80px; bottom: 250px;
font-size: 48px; font-weight: 900; color: #ffcc00;
text-shadow: 0 0 20px rgba(255,204,0,0.5);
opacity: 0; transition: opacity 0.3s, transform 0.1s;
}
#multiplier.visible { opacity: 1; }
#multiplier.pulse { transform: scale(1.3); }
#score-display {
position: absolute; top: 20px; right: 30px;
font-size: 24px; font-weight: 700; color: #fff;
text-shadow: 0 0 10px rgba(255,255,255,0.3);
}
#energy-container {
position: absolute; bottom: 30px; 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%;
background: linear-gradient(to right, #ff006a, #ff6a00, #ffcc00);
border-radius: 3px; transition: width 0.2s;
}
#game-over {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(10,5,20,0.92); display: none;
justify-content: center; align-items: center; flex-direction: column;
z-index: 100; pointer-events: all; cursor: default;
}
#game-over h1 {
font-size: 64px; font-weight: 900; color: #ff3366;
text-shadow: 0 0 40px rgba(255,51,102,0.5); margin-bottom: 20px;
letter-spacing: 8px; text-transform: uppercase;
}
#game-over .stat {
font-size: 22px; color: #ccc; margin: 8px 0; letter-spacing: 2px;
}
#game-over .stat span { color: #ffcc00; font-weight: 700; }
#restart-btn {
margin-top: 40px; padding: 16px 50px;
background: linear-gradient(135deg, #ff3366, #ff6a00);
border: none; border-radius: 8px; color: #fff;
font-size: 20px; font-weight: 700; cursor: pointer;
letter-spacing: 3px; text-transform: uppercase;
pointer-events: all; transition: transform 0.15s, box-shadow 0.15s;
}
#restart-btn:hover { transform: scale(1.05); box-shadow: 0 0 30px rgba(255,51,102,0.6); }
#start-screen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(ellipse at center, #1a0a2e 0%, #05020f 100%);
display: flex; justify-content: center; align-items: center; flex-direction: column;
z-index: 200; cursor: default;
}
#start-screen h1 {
font-size: 72px; font-weight: 900; color: #ff6a00;
text-shadow: 0 0 60px rgba(255,106,0,0.4); margin-bottom: 10px;
letter-spacing: 12px; text-transform: uppercase;
}
#start-screen .subtitle {
font-size: 18px; color: #aa77ff; letter-spacing: 6px; margin-bottom: 50px;
text-transform: uppercase;
}
#start-btn {
padding: 18px 60px;
background: linear-gradient(135deg, #ff3366, #ff6a00);
border: none; border-radius: 8px; color: #fff;
font-size: 22px; font-weight: 700; cursor: pointer;
letter-spacing: 4px; text-transform: uppercase;
transition: transform 0.15s, box-shadow 0.15s;
}
#start-btn:hover { transform: scale(1.05); box-shadow: 0 0 30px rgba(255,51,102,0.6); }
.controls-info {
margin-top: 40px; color: #887; font-size: 14px; letter-spacing: 2px; text-align: center; line-height: 2;
}
#near-miss-flash {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(ellipse at center, rgba(255,204,0,0.3), transparent 70%);
opacity: 0; pointer-events: none; z-index: 5; transition: opacity 0.1s;
}
</style>
</head>
<body>
<div id="hud">
<div id="score-display">SCORE: 0</div>
<div id="multiplier"></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="game-over">
<h1>CRASHED</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 RACER</h1>
<div class="subtitle">Procedural Speed Run</div>
<button id="start-btn" onclick="startGame()">LAUNCH</button>
<div class="controls-info">
W/S or ↑/↓ — Pitch | A/D or ←/→ — Roll & Turn<br>
SPACE — Boost | Collect orbs to refuel energy
</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 { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';
// ─── GLOBALS ──────────────────────────────────────────────
let scene, camera, renderer, composer, bloomPass, afterimagePass;
let player, playerBody;
let chunks = [];
let particles = [];
let orbs = [];
let gameActive = false;
let gameTime = 0;
// Player state
const playerState = {
x: 0, y: 0, z: 0,
vx: 0, vy: 0, vz: 0,
roll: 0, pitch: 0, yaw: 0,
speed: 0.8,
maxSpeed: 2.5,
energy: 100,
score: 0,
multiplier: 1,
turbo: 0,
boosting: false,
nearMissTimer: 0,
distance: 0,
maxSpeedReached: 0
};
// Input
const keys = {};
window.addEventListener('keydown', e => { keys[e.code] = true; e.preventDefault(); });
window.addEventListener('keyup', e => { keys[e.code] = false; });
// Audio context
let audioCtx;
function initAudio() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
// ─── SOUND SYNTHESIS ──────────────────────────────────────
const engineOsc = { osc: null, gain: null };
function startEngineSound() {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.value = 60;
gain.gain.value = 0.04;
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
engineOsc.osc = osc;
engineOsc.gain = gain;
}
function updateEngineSound() {
if (!engineOsc.osc) return;
const speedRatio = playerState.speed / playerState.maxSpeed;
engineOsc.osc.frequency.value = 50 + speedRatio * 120;
engineOsc.gain.gain.value = 0.03 + speedRatio * 0.06;
}
function playCollectSound() {
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.2);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
}
function playNearMissSound() {
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 filter = audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 2000;
filter.Q.value = 2;
const gain = audioCtx.createGain();
gain.gain.value = 0.12;
source.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
source.start();
}
function playCrashSound() {
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 initThree() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x1a0a2e, 0.008);
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 500);
camera.position.set(0, 3, -8);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
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.8;
bloomPass.radius = 0.6;
composer.addPass(bloomPass);
afterimagePass = new AfterimagePass(0.3);
composer.addPass(afterimagePass);
// Lighting
const ambientLight = new THREE.AmbientLight(0x4422aa, 0.4);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xff8833, 2.5);
sunLight.position.set(50, 80, -100);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 300;
sunLight.shadow.camera.left = -60;
sunLight.shadow.camera.right = 60;
sunLight.shadow.camera.top = 60;
sunLight.shadow.camera.bottom = -60;
scene.add(sunLight);
const rimLight = new THREE.DirectionalLight(0x8844ff, 0.8);
rimLight.position.set(-30, 20, 50);
scene.add(rimLight);
// Sky dome
createSky();
window.addEventListener('resize', onResize);
}
function createSky() {
const skyGeo = new THREE.SphereGeometry(400, 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;
vec3 bottom = vec3(0.6, 0.2, 0.05);
vec3 mid = vec3(0.4, 0.1, 0.3);
vec3 top = vec3(0.05, 0.02, 0.15);
vec3 col = mix(bottom, mid, smoothstep(-0.1, 0.3, h));
col = mix(col, top, smoothstep(0.3, 0.9, h));
gl_FragColor = vec4(col, 1.0);
}
`
});
const sky = new THREE.Mesh(skyGeo, skyMat);
scene.add(sky);
}
// ─── PLAYER CRAFT ─────────────────────────────────────────
function createPlayer() {
player = new THREE.Group();
// Main body - sleek arrow shape
const bodyGeo = new THREE.ConeGeometry(0.3, 2.5, 6);
bodyGeo.rotateX(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0xccccdd, flatShading: true, metalness: 0.8, roughness: 0.2
});
playerBody = new THREE.Mesh(bodyGeo, bodyMat);
playerBody.castShadow = true;
player.add(playerBody);
// Wings
const wingGeo = new THREE.BufferGeometry();
const wingVerts = new Float32Array([
-0.15, 0, -0.5, -2.0, -0.3, -0.8, -0.15, 0, 0.3,
0.15, 0, -0.5, 0.15, 0, 0.3, 2.0, -0.3, -0.8
]);
wingGeo.setAttribute('position', new THREE.BufferAttribute(wingVerts, 3));
wingGeo.computeVertexNormals();
const wingMat = new THREE.MeshStandardMaterial({
color: 0xff4466, flatShading: true, metalness: 0.5, roughness: 0.3
});
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.castShadow = true;
player.add(wings);
// Engine glow
const engineGeo = new THREE.SphereGeometry(0.2, 6, 4);
const engineMat = new THREE.MeshBasicMaterial({ color: 0xff6600 });
const engine = new THREE.Mesh(engineGeo, engineMat);
engine.position.z = 1.3;
player.add(engine);
// Engine light
const engineLight = new THREE.PointLight(0xff4400, 2, 8);
engineLight.position.z = 1.5;
player.add(engineLight);
scene.add(player);
}
// ─── PROCEDURAL CHUNK GENERATION ──────────────────────────
const CHUNK_LENGTH = 60;
const CHUNK_COUNT = 12;
let nextChunkZ = 0;
let difficulty = 1;
function createChunk(zPos) {
const chunk = new THREE.Group();
chunk.userData.z = zPos;
chunk.userData.obstacles = [];
chunk.userData.orbs = [];
// Canyon walls on both sides
generateCanyonWalls(chunk, zPos);
// Obstacles
if (Math.random() < 0.4 + difficulty * 0.1) {
generateMonoliths(chunk, zPos);
}
// Tight squeezes
if (difficulty > 1.5 && Math.random() < 0.2) {
generateSqueeze(chunk, zPos);
}
// Energy orbs
const orbCount = 2 + Math.floor(Math.random() * 3);
for (let i = 0; i < orbCount; i++) {
createOrbInChunk(chunk, zPos);
}
scene.add(chunk);
return chunk;
}
function generateCanyonWalls(chunk, zPos) {
const wallMat1 = new THREE.MeshStandardMaterial({
color: 0x8B4513, flatShading: true, roughness: 0.9
});
const wallMat2 = new THREE.MeshStandardMaterial({
color: 0x6B3410, flatShading: true, roughness: 0.9
});
const accentMat = new THREE.MeshStandardMaterial({
color: 0xff8844, flatShading: true, emissive: 0x331100, emissiveIntensity: 0.3
});
for (let i = 0; i < CHUNK_LENGTH; i += 4) {
const z = zPos + i;
const canyonWidth = Math.max(8, 25 - difficulty * 2);
const heightVar = 15 + Math.random() * 20;
// Left wall segments
const lw = 3 + Math.random() * 4;
const lh = heightVar + Math.sin(i * 0.3) * 8;
const leftWallGeo = new THREE.BoxGeometry(lw, lh, 5);
const leftWall = new THREE.Mesh(leftWallGeo, i % 8 === 0 ? accentMat : wallMat1);
leftWall.position.set(-canyonWidth - lw / 2 + Math.sin(i * 0.1) * 3, lh / 2 - 5, z);
leftWall.castShadow = true;
leftWall.receiveShadow = true;
chunk.add(leftWall);
// Right wall segments
const rw = 3 + Math.random() * 4;
const rh = heightVar + Math.cos(i * 0.25) * 8;
const rightWallGeo = new THREE.BoxGeometry(rw, rh, 5);
const rightWall = new THREE.Mesh(rightWallGeo, i % 8 === 0 ? accentMat : wallMat2);
rightWall.position.set(canyonWidth + rw / 2 - Math.cos(i * 0.1) * 3, rh / 2 - 5, z);
rightWall.castShadow = true;
rightWall.receiveShadow = true;
chunk.add(rightWall);
// Ground
const groundGeo = new THREE.BoxGeometry(canyonWidth * 2 + lw + rw, 1, 5);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x3a1a0a, flatShading: true });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.position.set(0, -8, z);
ground.receiveShadow = true;
chunk.add(ground);
// Ceiling (occasional)
if (Math.random() < 0.3) {
const ceilGeo = new THREE.BoxGeometry(canyonWidth * 1.5, 2, 4);
const ceilMat = new THREE.MeshStandardMaterial({ color: 0x5a2a1a, flatShading: true });
const ceil = new THREE.Mesh(ceilGeo, ceilMat);
ceil.position.set(Math.sin(i * 0.15) * 3, 18 + Math.random() * 5, z);
ceil.castShadow = true;
chunk.add(ceil);
}
}
// Store wall bounds for collision
const canyonWidth = Math.max(8, 25 - difficulty * 2);
chunk.userData.canyonBounds = { minX: -canyonWidth, maxX: canyonWidth };
}
function generateMonoliths(chunk, zPos) {
const monolithMat = new THREE.MeshStandardMaterial({
color: 0x9944cc, flatShading: true, emissive: 0x220033, emissiveIntensity: 0.5
});
const count = Math.floor(1 + difficulty * 0.5);
for (let i = 0; i < count; i++) {
const z = zPos + Math.random() * CHUNK_LENGTH;
const canyonWidth = chunk.userData.canyonBounds ?
(chunk.userData.canyonBounds.maxX - chunk.userData.canyonBounds.minX) / 2 : 15;
const x = (Math.random() - 0.5) * canyonWidth * 1.4;
const y = (Math.random() - 0.5) * 10;
const w = 1 + Math.random() * 3;
const h = 6 + Math.random() * 12;
const d = 1 + Math.random() * 2;
const geo = new THREE.BoxGeometry(w, h, d);
const mesh = new THREE.Mesh(geo, monolithMat);
mesh.position.set(x, y, z);
mesh.rotation.y = Math.random() * Math.PI;
mesh.castShadow = true;
chunk.add(mesh);
chunk.userData.obstacles.push({
minX: x - w / 2, maxX: x + w / 2,
minY: y - h / 2, maxY: y + h / 2,
minZ: z - d / 2, maxZ: z + d / 2
});
}
}
function generateSqueeze(chunk, zPos) {
const squeezeMat = new THREE.MeshStandardMaterial({
color: 0xaa5533, flatShading: true, roughness: 0.8
});
const squeezeZ = zPos + CHUNK_LENGTH * 0.5;
const gapWidth = Math.max(4, 12 - difficulty);
const canyonHalfW = chunk.userData.canyonBounds ?
(chunk.userData.canyonBounds.maxX - chunk.userData.canyonBounds.minX) / 2 : 15;
// Left squeeze wall
const lwGeo = new THREE.BoxGeometry(canyonHalfW * 0.6, 20, 3);
const lw = new THREE.Mesh(lwGeo, squeezeMat);
lw.position.set(-canyonHalfW + gapWidth / 2 + canyonHalfW * 0.3, 5, squeezeZ);
lw.castShadow = true;
chunk.add(lw);
// Right squeeze wall
const rwGeo = new THREE.BoxGeometry(canyonHalfW * 0.6, 20, 3);
const rw = new THREE.Mesh(rwGeo, squeezeMat);
rw.position.set(canyonHalfW - gapWidth / 2 - canyonHalfW * 0.3, 5, squeezeZ);
rw.castShadow = true;
chunk.add(rw);
// Top squeeze
const twGeo = new THREE.BoxGeometry(gapWidth + 4, 3, 3);
const tw = new THREE.Mesh(twGeo, squeezeMat);
tw.position.set(0, 12, squeezeZ);
tw.castShadow = true;
chunk.add(tw);
// Bottom squeeze
const bwGeo = new THREE.BoxGeometry(gapWidth + 4, 3, 3);
const bw = new THREE.Mesh(bwGeo, squeezeMat);
bw.position.set(0, -6, squeezeZ);
bw.castShadow = true;
chunk.add(bw);
chunk.userData.obstacles.push({
minX: -gapWidth / 2, maxX: gapWidth / 2,
minY: -4.5, maxY: 10.5,
minZ: squeezeZ - 1.5, maxZ: squeezeZ + 1.5
});
}
function createOrbInChunk(chunk, zPos) {
const orbGeo = new THREE.IcosahedronGeometry(0.4, 1);
const orbMat = new THREE.MeshBasicMaterial({ color: 0xffcc00 });
const orb = new THREE.Mesh(orbGeo, orbMat);
const canyonHalfW = chunk.userData.canyonBounds ?
(chunk.userData.canyonBounds.maxX - chunk.userData.canyonBounds.minX) / 2 : 15;
orb.position.set(
(Math.random() - 0.5) * canyonHalfW * 1.6,
(Math.random() - 0.5) * 12,
zPos + Math.random() * CHUNK_LENGTH
);
// Glow light
const orbLight = new THREE.PointLight(0xffcc00, 1.5, 8);
orb.add(orbLight);
chunk.add(orb);
chunk.userData.orbs.push({ mesh: orb, collected: false });
}
// ─── PARTICLE SYSTEMS ─────────────────────────────────────
function createTrailParticle() {
const geo = new THREE.BufferGeometry();
const positions = new Float32Array([0, 0, 0]);
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0xff6600, size: 0.15, transparent: true, opacity: 0.8,
blending: THREE.AdditiveBlending, depthWrite: false
});
const particle = new THREE.Points(geo, mat);
particle.userData.life = 1.0;
particle.userData.decay = 0.02 + Math.random() * 0.03;
scene.add(particle);
particles.push(particle);
}
function createDebrisParticle(x, y, z) {
const geo = new THREE.TetrahedronGeometry(0.15, 0);
const mat = new THREE.MeshBasicMaterial({ color: 0xff8844 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
mesh.userData.vx = (Math.random() - 0.5) * 2;
mesh.userData.vy = Math.random() * 2;
mesh.userData.vz = -Math.random() * 3;
mesh.userData.life = 1.0;
scene.add(mesh);
particles.push(mesh);
}
// ─── CAMERA SHAKE ─────────────────────────────────────────
let cameraShakeIntensity = 0;
function addCameraShake(amount) {
cameraShakeIntensity = Math.min(cameraShakeIntensity + amount, 1.5);
}
// ─── COLLISION DETECTION ──────────────────────────────────
const playerBounds = { radius: 0.8 };
function checkCollisions() {
const px = playerState.x;
const py = playerState.y;
const pz = playerState.z;
const r = playerBounds.radius;
for (let i = chunks.length - 1; i >= 0; i--) {
const chunk = chunks[i];
if (!chunk.userData.obstacles) continue;
// Check canyon bounds
if (chunk.userData.canyonBounds) {
const cb = chunk.userData.canyonBounds;
const inChunkZ = pz > chunk.userData.z && pz < chunk.userData.z + CHUNK_LENGTH;
if (inChunkZ) {
if (px - r < cb.minX || px + r > cb.maxX) {
return true; // Collision with canyon wall
}
}
}
// Check obstacles
for (const obs of chunk.userData.obstacles) {
const inZ = pz > obs.minZ - 2 && pz < obs.maxZ + 2;
if (!inZ) continue;
const closestX = Math.max(obs.minX, Math.min(px, obs.maxX));
const closestY = Math.max(obs.minY, Math.min(py, obs.maxY));
const dx = px - closestX;
const dy = py - closestY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < r) {
return true; // Collision
}
// Near-miss detection
if (dist < r + 2.5 && dist > r) {
triggerNearMiss();
}
}
}
return false;
}
function checkOrbCollection() {
const px = playerState.x;
const py = playerState.y;
const pz = playerState.z;
for (const chunk of chunks) {
if (!chunk.userData.orbs) continue;
for (const orbData of chunk.userData.orbs) {
if (orbData.collected) continue;
const orbPos = orbData.mesh.position;
const dx = px - orbPos.x;
const dy = py - orbPos.y;
const dz = pz - orbPos.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < 2.0) {
orbData.collected = true;
orbData.mesh.visible = false;
playerState.energy = Math.min(100, playerState.energy + 15);
playerState.score += 50 * playerState.multiplier;
playCollectSound();
// Burst particles
for (let i = 0; i < 8; i++) {
createDebrisParticle(orbPos.x, orbPos.y, orbPos.z);
}
}
}
}
}
// ─── NEAR MISS SYSTEM ─────────────────────────────────────
function triggerNearMiss() {
if (playerState.nearMissTimer > 0) return;
playerState.nearMissTimer = 0.5;
playerState.multiplier = Math.min(playerState.multiplier + 0.5, 10);
playerState.score += 100 * playerState.multiplier;
addCameraShake(0.3);
playNearMissSound();
const flash = document.getElementById('near-miss-flash');
flash.style.opacity = '1';
setTimeout(() => { flash.style.opacity = '0'; }, 150);
const multEl = document.getElementById('multiplier');
multEl.textContent = 'x' + playerState.multiplier.toFixed(1);
multEl.classList.add('visible', 'pulse');
setTimeout(() => multEl.classList.remove('pulse'), 200);
}
// ─── GAME LOOP ────────────────────────────────────────────
let lastTime = 0;
function gameLoop(timestamp) {
if (!gameActive) return;
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
gameTime += dt;
updatePlayer(dt);
updateChunks();
updateParticles(dt);
updateCamera(dt);
updateHUD();
updateEngineSound();
// Difficulty scaling
difficulty = 1 + playerState.score / 2000;
composer.render();
requestAnimationFrame(gameLoop);
}
function updatePlayer(dt) {
const accel = 8.0;
const damping = 0.92;
const turnSpeed = 3.5;
const pitchSpeed = 2.5;
// Input handling
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 boosting = keys['Space'] && playerState.turbo > 0;
if (boosting) {
playerState.boosting = true;
playerState.turbo -= dt * 30;
if (playerState.turbo <= 0) {
playerState.turbo = 0;
playerState.boosting = false;
}
} else {
playerState.boosting = false;
// Build turbo on straightaways at high speed
if (Math.abs(inputX) < 0.1 && playerState.speed > 1.5) {
playerState.turbo = Math.min(100, playerState.turbo + dt * 8);
}
}
const boostMultiplier = boosting ? 2.0 : 1.0;
const targetSpeed = (0.8 + difficulty * 0.15) * boostMultiplier;
playerState.speed += (targetSpeed - playerState.speed) * dt * 3;
// Track max speed
if (playerState.speed > playerState.maxSpeedReached) {
playerState.maxSpeedReached = playerState.speed;
}
// Apply forces with momentum
playerState.vx += inputX * accel * dt;
playerState.vy += inputY * accel * dt;
playerState.vx *= damping;
playerState.vy *= damping;
// Clamp velocity
const maxV = 15;
playerState.vx = Math.max(-maxV, Math.min(maxV, playerState.vx));
playerState.vy = Math.max(-maxV, Math.min(maxV, playerState.vy));
// Update position
playerState.x += playerState.vx * dt;
playerState.y += playerState.vy * dt;
playerState.z += playerState.speed * 60 * dt;
playerState.distance = playerState.z;
// Energy decay
playerState.energy -= dt * (3 + difficulty * 0.5);
if (playerState.energy <= 0) {
gameOver();
return;
}
// Score from distance
playerState.score += playerState.speed * dt * 10;
// Near-miss timer decay
if (playerState.nearMissTimer > 0) {
playerState.nearMissTimer -= dt;
if (playerState.nearMissTimer <= 0) {
playerState.multiplier = Math.max(1, playerState.multiplier - dt * 2);
}
}
// Banking animation
const targetRoll = -inputX * 0.6;
const targetPitch = inputY * 0.3;
playerState.roll += (targetRoll - playerState.roll) * dt * 8;
playerState.pitch += (targetPitch - playerState.pitch) * dt * 8;
// Apply to mesh
if (player) {
player.position.set(playerState.x, playerState.y, playerState.z);
player.rotation.z = playerState.roll;
player.rotation.x = playerState.pitch;
// Engine glow intensity based on speed
const engineLight = player.children.find(c => c.isPointLight);
if (engineLight) {
engineLight.intensity = 1 + playerState.speed * 2;
}
}
// Trail particles
if (Math.random() < 0.5 + playerState.speed * 0.3) {
createTrailParticle();
}
// Debris for speed feel
if (playerState.speed > 1.5 && Math.random() < 0.2) {
createDebrisParticle(
playerState.x + (Math.random() - 0.5) * 4,
playerState.y + (Math.random() - 0.5) * 4,
playerState.z - 3
);
}
// Collision check
if (checkCollisions()) {
gameOver();
return;
}
// Orb collection
checkOrbCollection();
}
function updateChunks() {
const playerZ = playerState.z;
// Generate new chunks ahead
while (nextChunkZ < playerZ + CHUNK_LENGTH * CHUNK_COUNT) {
const chunk = createChunk(nextChunkZ);
chunks.push(chunk);
nextChunkZ += CHUNK_LENGTH;
}
// Remove old chunks behind
for (let i = chunks.length - 1; i >= 0; i--) {
if (chunks[i].userData.z + CHUNK_LENGTH < playerZ - CHUNK_LENGTH * 2) {
scene.remove(chunks[i]);
chunks.splice(i, 1);
}
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.userData.life -= p.userData.decay || 0.02;
if (p.isPoints) {
// Trail particle
p.position.copy(player.position);
p.material.opacity = p.userData.life * 0.6;
} else {
// Debris
p.position.x += (p.userData.vx || 0) * dt;
p.position.y += (p.userData.vy || 0) * dt;
p.position.z += (p.userData.vz || 0) * dt;
p.material.opacity = p.userData.life;
}
if (p.userData.life <= 0) {
scene.remove(p);
particles.splice(i, 1);
}
}
}
function updateCamera(dt) {
const targetX = playerState.x * 0.7;
const targetY = playerState.y * 0.5 + 3;
const camZ = playerState.z - (boosting ? 12 : 8);
camera.position.x += (targetX - camera.position.x) * dt * 4;
camera.position.y += (targetY - camera.position.y) * dt * 4;
camera.position.z = camZ;
// Camera shake
if (cameraShakeIntensity > 0.01) {
camera.position.x += (Math.random() - 0.5) * cameraShakeIntensity;
camera.position.y += (Math.random() - 0.5) * cameraShakeIntensity;
cameraShakeIntensity *= 0.9;
}
// FOV based on speed
const targetFov = boosting ? 90 : 70 + playerState.speed * 8;
camera.fov += (targetFov - camera.fov) * dt * 3;
camera.updateProjectionMatrix();
// Look ahead
const lookTarget = new THREE.Vector3(
playerState.x * 0.3,
playerState.y * 0.3 + 1,
playerState.z + 40
);
camera.lookAt(lookTarget);
// Bloom intensity based on boost
bloomPass.strength = boosting ? 2.5 : 1.8;
}
function updateHUD() {
const speedRatio = (playerState.speed - 0.5) / (playerState.maxSpeed * 2);
document.getElementById('speed-bar').style.height = Math.max(0, Math.min(100, speedRatio * 100)) + '%';
document.getElementById('turbo-bar').style.height = playerState.turbo + '%';
document.getElementById('energy-bar').style.width = playerState.energy + '%';
if (playerState.energy < 25) {
document.getElementById('energy-bar').style.background = '#ff0044';
} else {
document.getElementById('energy-bar').style.background = 'linear-gradient(to right, #ff006a, #ff6a00, #ffcc00)';
}
document.getElementById('score-display').textContent = 'SCORE: ' + Math.floor(playerState.score);
const multEl = document.getElementById('multiplier');
if (playerState.multiplier <= 1.0) {
multEl.classList.remove('visible');
} else {
multEl.textContent = 'x' + playerState.multiplier.toFixed(1);
}
}
// ─── GAME OVER / START ────────────────────────────────────
function gameOver() {
gameActive = false;
playCrashSound();
if (engineOsc.osc) {
engineOsc.osc.stop();
engineOsc.osc = null;
}
document.getElementById('final-score').textContent = Math.floor(playerState.score);
document.getElementById('max-speed').textContent = (playerState.maxSpeedReached * 60).toFixed(0) + ' km/h';
document.getElementById('distance').textContent = Math.floor(playerState.distance);
document.getElementById('game-over').style.display = 'flex';
}
window.startGame = function() {
document.getElementById('start-screen').style.display = 'none';
initAudio();
startEngineSound();
gameActive = true;
lastTime = performance.now();
requestAnimationFrame(gameLoop);
};
window.restartGame = function() {
// Clean up
for (const chunk of chunks) scene.remove(chunk);
chunks = [];
for (const p of particles) scene.remove(p);
particles = [];
nextChunkZ = 0;
gameTime = 0;
// Reset player state
Object.assign(playerState, {
x: 0, y: 0, z: 0,
vx: 0, vy: 0, vz: 0,
roll: 0, pitch: 0, yaw: 0,
speed: 0.8,
energy: 100,
score: 0,
multiplier: 1,
turbo: 0,
boosting: false,
nearMissTimer: 0,
distance: 0,
maxSpeedReached: 0
});
difficulty = 1;
cameraShakeIntensity = 0;
if (player) {
player.position.set(0, 0, 0);
player.rotation.set(0, 0, 0);
}
document.getElementById('game-over').style.display = 'none';
startEngineSound();
gameActive = true;
lastTime = performance.now();
requestAnimationFrame(gameLoop);
};
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
// ─── INIT ─────────────────────────────────────────────────
initThree();
createPlayer();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"c827920f-0f54-4e72-b6a3-e74d8e4f420f","tokensIn":981,"tokensOut":11584,"tokensTotal":12565,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T01:39:22.913Z"} -->