Canyon Run
ling 3.0 tiny:free · full

Browser check: Artifact runtime error. 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 44,255 bytes · SHA-256 35a138a1d1bb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>High-Speed Procedural Canyon Racer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Segoe UI', sans-serif; }
canvas { display: block; }
#hud {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10;
}
.hud-top {
position: absolute; top: 16px; left: 50%; transform: translateX(-50%);
display: flex; gap: 30px; align-items: flex-end;
}
.hud-item {
background: rgba(0,0,0,0.5); border: 1px solid rgba(255,180,60,0.3);
border-radius: 8px; padding: 8px 18px; text-align: center;
min-width: 90px;
}
.hud-label { font-size: 10px; color: rgba(255,200,100,0.8); text-transform: uppercase; letter-spacing: 1px; }
.hud-value { font-size: 22px; font-weight: 700; color: #FFD700; font-variant-numeric: tabular-nums; }
.hud-value.boost { color: #00E5FF; text-shadow: 0 0 10px rgba(0,229,255,0.6); }
.hud-value.nearmiss { color: #FF6B35; text-shadow: 0 0 10px rgba(255,107,53,0.6); }
.speedometer-wrap {
position: absolute; top: 12px; right: 24px;
width: 130px; height: 130px;
}
.speedometer-wrap canvas { width: 100%; height: 100%; }
.energy-bar-wrap {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
width: 200px; height: 12px;
background: rgba(0,0,0,0.6); border-radius: 6px;
border: 1px solid rgba(255,100,50,0.3);
}
.energy-bar-fill {
height: 100%; border-radius: 6px;
background: linear-gradient(90deg, #FF6B35, #FFD700, #00E5FF);
transition: width 0.1s;
}
.turbo-indicator {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 14px; color: rgba(0,229,255,0.9);
font-weight: 700; letter-spacing: 2px;
text-shadow: 0 0 10px rgba(0,229,255,0.8);
opacity: 0; transition: opacity 0.2s;
pointer-events: none;
}
.turbo-indicator.active { opacity: 1; }
.multiplier-display {
position: absolute; top: 60px; left: 50%; transform: translateX(-50%);
font-size: 48px; font-weight: 900; color: #FFD700;
text-shadow: 0 0 20px rgba(255,215,0,0.8);
opacity: 0; transition: all 0.15s;
pointer-events: none;
}
.multiplier-display.active { opacity: 1; }
.hud-bottom {
position: absolute; bottom: 10px; left: 16px; right: 16px;
display: flex; justify-content: space-between; align-items: flex-end;
}
.btn {
pointer-events: all; cursor: pointer;
background: rgba(255,180,60,0.2); border: 1px solid rgba(255,180,60,0.5);
color: #FFD700; padding: 8px 18px; border-radius: 6px;
font-size: 13px; font-weight: 600; letter-spacing: 1px;
transition: all 0.15s;
}
.btn:hover { background: rgba(255,180,60,0.4); border-color: #FFD700; }
.btn:active { transform: scale(0.95); }
.btn:focus-visible { outline: 2px solid #FFD700; outline-offset: 2px; }
#gameOverScreen {
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;
}
#gameOverScreen.visible { display: flex; }
.go-title {
font-size: 56px; font-weight: 900; color: #FFD700;
text-shadow: 0 0 30px rgba(255,215,0,0.6);
margin-bottom: 8px; letter-spacing: 3px;
}
.go-subtitle {
font-size: 18px; color: rgba(255,200,100,0.7); margin-bottom: 30px;
}
.go-stats {
display: flex; gap: 40px; margin-bottom: 30px;
}
.go-stat {
text-align: center;
}
.go-stat-label { font-size: 11px; color: rgba(255,200,100,0.6); text-transform: uppercase; letter-spacing: 2px; }
.go-stat-value { font-size: 28px; font-weight: 700; color: #FFD700; margin-top: 4px; }
.go-stat-value.high { color: #FF6B35; }
.restart-btn {
pointer-events: all; cursor: pointer;
background: rgba(0,229,255,0.15); border: 1px solid rgba(0,229,255,0.4);
color: #00E5FF; padding: 12px 36px; border-radius: 6px;
font-size: 15px; font-weight: 700; letter-spacing: 2px;
transition: all 0.2s;
}
.restart-btn:hover { background: rgba(0,229,255,0.3); border-color: #00E5FF; }
.restart-btn:focus-visible { outline: 2px solid #00E5FF; outline-offset: 2px; }
#pauseOverlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6); display: none;
align-items: center; justify-content: center;
z-index: 90;
}
#pauseOverlay.visible { display: flex; }
.pause-text {
font-size: 48px; font-weight: 900; color: #FFD700;
text-shadow: 0 0 20px rgba(255,215,0,0.6); letter-spacing: 6px;
}
#challengeIndicator {
position: absolute; top: 100px; left: 50%; transform: translateX(-50%);
font-size: 11px; color: rgba(255,107,53,0.7); letter-spacing: 1px;
pointer-events: none;
}
#mobileControls {
position: fixed; bottom: 15px; left: 0; right: 0;
display: none; justify-content: space-between; padding: 0 20px;
z-index: 20; pointer-events: none;
}
.mobile-btn {
width: 60px; height: 60px; border-radius: 50%;
background: rgba(255,180,60,0.2); border: 2px solid rgba(255,180,60,0.4);
color: #FFD700; font-size: 22px;
display: flex; align-items: center; justify-content: center;
pointer-events: all; user-select: none;
}
.mobile-btn:active { background: rgba(255,180,60,0.5); }
@media (max-width: 768px) {
.hud-value { font-size: 16px; }
.hud-item { padding: 4px 10px; min-width: 60px; }
.speedometer-wrap { width: 80px; height: 80px; }
.go-title { font-size: 32px; }
.go-stat-value { font-size: 20px; }
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
<div class="hud-top">
<div class="hud-item">
<div class="hud-label">Speed</div>
<div class="hud-value" id="speedVal">0</div>
</div>
<div class="hud-item">
<div class="hud-label">Score</div>
<div class="hud-value" id="scoreVal">0</div>
</div>
<div class="hud-item">
<div class="hud-label">Near-Miss</div>
<div class="hud-value nearmiss" id="multiplierVal">1.0x</div>
</div>
<div class="hud-item">
<div class="hud-label">Turbo</div>
<div class="hud-value boost" id="turboVal">0%</div>
</div>
</div>
<div class="speedometer-wrap">
<canvas id="speedometerCanvas" width="130" height="130"></canvas>
</div>
<div class="energy-bar-wrap">
<div class="energy-bar-fill" id="energyBarFill" style="width:100%"></div>
</div>
<div class="turbo-indicator" id="turboIndicator">BOOST!</div>
<div class="multiplier-display" id="multiplierDisplay"></div>
<div class="hud-bottom">
<button class="btn" id="pauseBtn" aria-label="Pause">PAUSE</button>
<button class="btn" id="restartBtn" aria-label="Restart">RESTART</button>
</div>
</div>
<div id="challengeIndicator">SCORE: 0</div>
<div id="gameOverScreen">
<div class="go-title">GAME OVER</div>
<div class="go-subtitle">Energy depleted. The canyon claims another pilot.</div>
<div class="go-stats">
<div class="go-stat">
<div class="go-stat-label">Final Score</div>
<div class="go-stat-value" id="finalScore">0</div>
</div>
<div class="go-stat">
<div class="go-stat-label">Max Speed</div>
<div class="go-stat-value high" id="maxSpeed">0</div>
</div>
<div class="go-stat">
<div class="go-stat-label">Near-Miss</div>
<div class="go-stat-value" id="finalMultiplier">1.0x</div>
</div>
<div class="go-stat">
<div class="go-stat-label">Distance</div>
<div class="go-stat-value" id="finalDistance">0m</div>
</div>
</div>
<button class="restart-btn" id="restartBtn2">RACE AGAIN</button>
</div>
<div id="pauseOverlay">
<div class="pause-text">PAUSED</div>
</div>
<div id="mobileControls">
<div class="mobile-btn" id="btnUp" aria-label="Pitch up">▲</div>
<div class="mobile-btn" id="btnDown" aria-label="Pitch down">▼</div>
<div class="mobile-btn" id="btnLeft" aria-label="Steer left">◀</div>
<div class="mobile-btn" id="btnRight" aria-label="Steer right">▶</div>
<div class="mobile-btn" id="btnBoost" aria-label="Boost">BOOST</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/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';
const CFG = {
canyonWidth: 320,
wallHeight: 280,
playerRadius: 4.5,
playerSpeed: 2,
playerMaxSpeed: 18,
playerAccel: 0.45,
playerDecel: 0.3,
playerSteer: 0.35,
energyDecay: 0.35,
energyOrbValue: 22,
turboFillRate: 0.5,
turboMax: 100,
nearMissThreshold: 25,
nearMissDebounce: 0.5,
chunkWidth: 600,
chunkDepth: 400,
};
const canvas = document.getElementById('gameCanvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a12);
scene.fog = new THREE.FogExp2(0x0a0a12, 0.0015);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.5, 2000);
camera.position.set(0, 60, 200);
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
0.8, 0.4, 0.85
);
composer.addPass(bloomPass);
let audioCtx;
function initAudio() {
if (audioCtx) return;
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const engineOsc = audioCtx.createOscillator();
engineOsc.type = 'sine'; engineOsc.frequency.value = 80;
const engineGain = audioCtx.createGain(); engineGain.gain.value = 0.12;
engineOsc.connect(engineGain); engineGain.connect(audioCtx.destination); engineOsc.start();
const collectOsc = audioCtx.createOscillator();
collectOsc.type = 'sine'; collectOsc.frequency.value = 1200;
const collectGain = audioCtx.createGain(); collectGain.gain.value = 0.1;
collectOsc.connect(collectGain); collectGain.connect(audioCtx.destination); collectOsc.start();
const nearMissOsc = audioCtx.createOscillator();
nearMissOsc.type = 'sine'; nearMissOsc.frequency.value = 1800;
const nearMissGain = audioCtx.createGain(); nearMissGain.gain.value = 0.08;
nearMissOsc.connect(nearMissGain); nearMissGain.connect(audioCtx.destination); nearMissOsc.start();
const boostOsc = audioCtx.createOscillator();
boostOsc.type = 'triangle'; boostOsc.frequency.value = 200;
const boostGain = audioCtx.createGain(); boostGain.gain.value = 0.1;
boostOsc.connect(boostGain); boostGain.connect(audioCtx.destination); boostOsc.start();
} catch (e) { console.warn('Audio init failed:', e); }
}
function playCollect() {
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
osc.type = 'sine'; osc.frequency.value = 1200;
const gain = audioCtx.createGain(); gain.gain.value = 0.15;
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
osc.stop(audioCtx.currentTime + 0.3);
} catch (e) { /* ignore */ }
}
function playNearMiss() {
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
osc.type = 'sine'; osc.frequency.value = 1800;
const gain = audioCtx.createGain(); gain.gain.value = 0.1;
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(); osc.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 0.25);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
osc.stop(audioCtx.currentTime + 0.3);
} catch (e) { /* ignore */ }
}
function playBoost() {
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
osc.type = 'triangle'; osc.frequency.value = 200;
osc.frequency.exponentialRampToValueAtTime(400, audioCtx.currentTime + 0.3);
const gain = audioCtx.createGain(); gain.gain.value = 0.12;
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.4);
osc.stop(audioCtx.currentTime + 0.4);
} catch (e) { /* ignore */ }
}
function playGameOver() {
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth'; osc.frequency.value = 440;
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.8);
const gain = audioCtx.createGain(); gain.gain.value = 0.15;
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1.0);
osc.stop(audioCtx.currentTime + 1.0);
} catch (e) { /* ignore */ }
}
const sunLight = new THREE.DirectionalLight(0xFFD700, 1.5);
sunLight.position.set(500, 300, 200);
sunLight.target.position.set(0, 0, 0);
scene.add(sunLight); scene.add(sunLight.target);
const hemiLight = new THREE.HemisphereLight(0xFFD700, 0x1a0a2e, 0.3);
scene.add(hemiLight);
const ambientLight = new THREE.AmbientLight(0x1a0a2e, 0.2);
scene.add(ambientLight);
const skyGeo = new THREE.SphereGeometry(1200, 32, 32);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(0x0a0a1a) },
bottomColor: { value: new THREE.Color(0xFF8C00) },
offset: { value: 200 },
exponent: { value: 0.5 },
},
vertexShader: `
varying vec3 vWorldPos;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPos = worldPos.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPos;
void main() {
float h = normalize(vWorldPos + vec3(0.0, offset, 0.0)).y;
vec3 col = mix(bottomColor, topColor, pow(max(h, 0.0), exponent));
gl_FragColor = vec4(col, 1.0);
}
`,
side: THREE.BackSide,
});
const sky = new THREE.Mesh(skyGeo, skyMat);
scene.add(sky);
const groundGeo = new THREE.PlaneGeometry(3000, 3000);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.95, metalness: 0.05 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.5;
scene.add(ground);
const playerGroup = new THREE.Group();
const bodyGeo = new THREE.ConeGeometry(2.5, 3.5, 6);
bodyGeo.rotateZ(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0x00E5FF, emissive: 0x004466, emissiveIntensity: 0.3,
metalness: 0.8, roughness: 0.2,
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.castShadow = true;
playerGroup.add(body);
const cockpitGeo = new THREE.BoxGeometry(1.2, 1.5, 0.8);
const cockpitMat = new THREE.MeshStandardMaterial({
color: 0x00CCFF, emissive: 0x002244, emissiveIntensity: 0.5,
metalness: 0.6, roughness: 0.3,
});
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(0, 0.4, 0.5);
playerGroup.add(cockpit);
const engineGeo = new THREE.SphereGeometry(0.8, 6, 6);
const engineMat = new THREE.MeshStandardMaterial({
color: 0xFF6B35, emissive: 0xFF6B35, emissiveIntensity: 1.5,
transparent: true, opacity: 0.9,
});
const engine = new THREE.Mesh(engineGeo, engineMat);
engine.position.set(0, -1.2, -1.8);
playerGroup.add(engine);
const exhaustGeo = new THREE.BoxGeometry(0.3, 1.5, 0.3);
const exhaustMat = new THREE.MeshStandardMaterial({
color: 0xFF6B35, emissive: 0xFF6B35, emissiveIntensity: 0.8,
transparent: true, opacity: 0.6,
});
const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
exhaust.position.set(0, -0.8, -1.8);
playerGroup.add(exhaust);
const trailGeo = new THREE.BufferGeometry();
const trailPositions = new Float32Array(120 * 3);
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPositions, 3));
const trailMat = new THREE.PointsMaterial({
color: 0x00E5FF, size: 0.15, transparent: true, opacity: 0.7,
blending: THREE.AdditiveBlending, depthWrite: false,
});
const trail = new THREE.Points(trailGeo, trailMat);
playerGroup.add(trail);
const shadowGeo = new THREE.CircleGeometry(3, 8);
const shadowMat = new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.3 });
const shadow = new THREE.Mesh(shadowGeo, shadowMat);
shadow.rotation.x = -Math.PI / 2;
shadow.position.set(0, 0.01, 0);
playerGroup.add(shadow);
playerGroup.position.set(0, 0, 0);
scene.add(playerGroup);
const wallGeo = new THREE.BoxGeometry(1, CFG.wallHeight, 15);
const wallMat = new THREE.MeshStandardMaterial({
color: 0x3D2B1F, flatShading: true, roughness: 0.95, metalness: 0.1,
});
const leftWallMeshes = [];
const rightWallMeshes = [];
const gapMeshes = [];
const monolithMeshes = [];
const orbMeshes = [];
const boostItemMeshes = [];
function createCanyonWalls(chunkX, chunkZ) {
leftWallMeshes.forEach(m => scene.remove(m));
rightWallMeshes.forEach(m => scene.remove(m));
gapMeshes.forEach(m => scene.remove(m));
leftWallMeshes = [];
rightWallMeshes = [];
gapMeshes = [];
const gapCount = 2 + Math.floor(Math.random() * 3);
const gapData = [];
for (let i = 0; i < gapCount; i++) {
gapData.push({
x: -CFG.canyonWidth / 2 + (i + 1) * (CFG.canyonWidth / (gapCount + 1)),
width: CFG.gapMinWidth + Math.random() * (CFG.gapMaxWidth - CFG.gapMinWidth),
});
}
gapData.sort((a, b) => a.x - b.x);
for (const g of gapData) {
const gx = g.x;
const gw = g.width;
const gH = CFG.wallHeight;
const lw = new THREE.Mesh(wallGeo, wallMat);
lw.position.set(gx - gw / 2 - 1, gH / 2, chunkZ);
lw.renderOrder = 1;
scene.add(lw);
leftWallMeshes.push(lw);
const rw = new THREE.Mesh(wallGeo, wallMat);
rw.position.set(gx + gw / 2 + 1, gH / 2, chunkZ);
rw.renderOrder = 1;
scene.add(rw);
rightWallMeshes.push(rw);
const gapFloorGeo = new THREE.BoxGeometry(gw, 1, 30);
const gapFloorMat = new THREE.MeshStandardMaterial({
color: 0x0a0a1a, transparent: true, opacity: 0.15,
side: THREE.DoubleSide,
});
const gapFloor = new THREE.Mesh(gapFloorGeo, gapFloorMat);
gapFloor.position.set(gx, 0.01, chunkZ);
gapFloor.renderOrder = 0;
scene.add(gapFloor);
gapMeshes.push(gapFloor);
}
}
function createMonolith(chunkX, chunkZ, diff) {
const height = 50 + Math.random() * 80;
const mono = new THREE.Mesh(
new THREE.BoxGeometry(2, height, 3),
new THREE.MeshStandardMaterial({
color: 0x8B4513, flatShading: true, roughness: 0.9, metalness: 0.1,
})
);
mono.position.set(chunkX + 150, height / 2, chunkZ);
mono.rotation.y = Math.random() * Math.PI;
scene.add(mono);
monolithMeshes.push(mono);
return mono;
}
function createOrb(chunkX, chunkZ) {
const orb = new THREE.Mesh(
new THREE.SphereGeometry(1.2, 8, 8),
new THREE.MeshStandardMaterial({
color: 0xFFD700, emissive: 0xFFD700, emissiveIntensity: 2.0,
metalness: 0.3, roughness: 0.1,
})
);
orb.position.set(chunkX + 100, 3 + Math.random() * 20, chunkZ);
orb.scale.setScalar(0.5 + Math.random() * 0.8);
orb.userData = { active: true, pulse: Math.random() * Math.PI * 2 };
scene.add(orb);
orbMeshes.push(orb);
}
function createBoostItem(chunkX, chunkZ) {
const boostItem = new THREE.Mesh(
new THREE.OctahedronGeometry(1.5),
new THREE.MeshStandardMaterial({
color: 0x00E5FF, emissive: 0x00E5FF, emissiveIntensity: 2.5,
metalness: 0.3, roughness: 0.1,
})
);
boostItem.position.set(chunkX + 100, 5 + Math.random() * 15, chunkZ);
boostItem.scale.setScalar(0.6 + Math.random() * 0.6);
boostItem.userData = { active: true, pulse: Math.random() * Math.PI * 2 };
scene.add(boostItem);
boostItemMeshes.push(boostItem);
}
const particleSystem = new THREE.Points(new THREE.BufferGeometry(), new THREE.PointsMaterial({
color: 0xFFD700, size: 0.3, transparent: true, opacity: 0.6,
blending: THREE.AdditiveBlending, depthWrite: false,
}));
scene.add(particleSystem);
const particlePool = [];
const MAX_PARTICLES = 200;
function initParticlePool() {
for (let i = 0; i < MAX_PARTICLES; i++) {
const geo = new THREE.BoxGeometry(0.15, 0.15, 0.15);
const mat = new THREE.MeshStandardMaterial({
color: 0xFFD700, emissive: 0xFFD700, emissiveIntensity: 0.5,
transparent: true, opacity: 0.8,
blending: THREE.AdditiveBlending, depthWrite: false,
});
const m = new THREE.Mesh(geo, mat);
m.visible = false;
scene.add(m);
particlePool.push({ mesh: m, life: 0, maxLife: 0, vx: 0, vy: 0, vz: 0, active: false });
}
}
function emitParticle(x, y, z, vx, vy, vz, life) {
for (let p of particlePool) {
if (!p.active) {
p.mesh.visible = true;
p.mesh.position.set(x, y, z);
p.mesh.scale.setScalar(0.8 + Math.random() * 0.4);
p.mesh.material.color.setHex(0xFFD700);
p.mesh.material.emissive.setHex(0xFFD700);
p.mesh.material.opacity = 0.9;
p.vx = vx; p.vy = vy; p.vz = vz;
p.life = life; p.maxLife = life;
p.active = true;
return;
}
}
}
function updateParticles(dt) {
for (let p of particlePool) {
if (!p.active) continue;
p.mesh.position.x += p.vx * dt;
p.mesh.position.y += p.vy * dt;
p.mesh.position.z += p.vz * dt;
p.vy -= 2 * dt;
p.life -= dt;
if (p.life <= 0) {
p.active = false;
p.mesh.visible = false;
}
}
}
const state = {
phase: 'menu',
score: 0,
multiplier: 1.0,
multiplierTimer: 0,
energy: 100,
turbo: 0,
maxTurbo: CFG.turboMax,
speed: 0,
maxSpeed: 0,
nearMiss: 0,
nearMissTimer: 0,
highSpeed: 0,
distance: 0,
difficulty: 1,
player: {
position: new THREE.Vector3(0, 0, 0),
velocity: new THREE.Vector3(0, 0, 0),
yaw: 0,
pitch: 0,
roll: 0,
prevYaw: 0,
prevPitch: 0,
prevRoll: 0,
thrust: 0,
},
cameraShake: { x: 0, y: 0, intensity: 0, decay: 0.9 },
chunkHistory: [],
nextChunkX: 100,
lastChunkZ: 0,
spawnTimer: 0,
challengeScore: 0,
nearMissActive: false,
lastNearMissTime: 0,
highSpeedCount: 0,
};
function checkWallCollision(playerPos, wall) {
const w = wall.position;
if (isNaN(w.x) || isNaN(w.y) || isNaN(w.z)) return false;
const wCenter = new THREE.Vector3(w.x, w.y, w.z);
const wallHalfW = 0.5;
const wallHalfH = CFG.wallHeight / 2;
const wallHalfD = 7.5;
const px = playerPos.x, py = playerPos.y, pz = playerPos.z;
const pr = CFG.playerRadius;
if (px > wCenter.x - wallHalfW && px < wCenter.x + wallHalfW &&
py > wCenter.y - wallHalfH && py < wCenter.y + wallHalfH &&
pz > wCenter.z - wallHalfD && pz < wCenter.z + wallHalfD) {
return true;
}
return false;
}
function checkObstacleCollision(playerPos, obj) {
const p = playerPos;
const pr = CFG.playerRadius;
if (isNaN(p.x) || isNaN(p.y) || isNaN(p.z)) return false;
if (isNaN(obj.position.x) || isNaN(obj.position.y) || isNaN(obj.position.z)) return false;
if (p.distanceTo(obj.position) < pr + 0.5) return true;
return false;
}
function checkOrbCollision(playerPos, orb) {
const p = playerPos;
const pr = CFG.playerRadius;
if (isNaN(p.x) || isNaN(p.y) || isNaN(p.z)) return false;
if (isNaN(orb.position.x) || isNaN(orb.position.y) || isNaN(orb.position.z)) return false;
if (p.distanceTo(orb.position) < pr + orb.geometry.parameters.radius) return true;
return false;
}
function checkBoostItemCollision(playerPos, boost) {
const p = playerPos;
const pr = CFG.playerRadius;
if (isNaN(p.x) || isNaN(p.y) || isNaN(p.z)) return false;
if (isNaN(boost.position.x) || isNaN(boost.position.y) || isNaN(boost.position.z)) return false;
if (p.distanceTo(boost.position) < pr + boost.geometry.parameters.radius) return true;
return false;
}
function isNaN(v) { return typeof v === 'number' && isNaN(v); }
function isValidPos(v) { return !isNaN(v) && isFinite(v); }
function checkWallCollision(player) {
const p = player.position;
const px = p.x, py = p.y, pz = p.z;
const pr = CFG.playerRadius;
for (const wall of state.chunkHistory[0].walls) {
const w = wall.position;
if (!isValidPos(w.x) || !isValidPos(w.y) || !isValidPos(w.z)) continue;
if (px > w.x - 0.5 && px < w.x + 0.5 &&
py > w.y - 140 && py < w.y + 140 &&
pz > w.z - 7.5 && pz < w.z + 7.5) {
return true;
}
}
return false;
}
function checkObstacleCollision(player) {
const p = player.position;
const pr = CFG.playerRadius;
for (const obj of state.chunkHistory[0].obstacles) {
if (!isValidPos(obj.position.x) || !isValidPos(obj.position.y) || !isValidPos(obj.position.z)) continue;
if (p.distanceTo(obj.position) < pr + 0.5) {
return true;
}
}
return false;
}
function checkOrbCollision(player) {
const p = player.position;
const pr = CFG.playerRadius;
for (const orb of state.chunkHistory[0].orbs) {
if (!orb.userData.active) continue;
if (!isValidPos(orb.position.x) || !isValidPos(orb.position.y) || !isValidPos(orb.position.z)) continue;
if (p.distanceTo(orb.position) < pr + orb.geometry.parameters.radius) {
state.energy = Math.min(100, state.energy + CFG.energyOrbValue);
state.score += 10 * state.multiplier;
playCollect();
orb.userData.active = false;
for (let i = 0; i < 5; i++) {
emitParticle(orb.position.x, orb.position.y, orb.position.z,
(Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, 0.5 + Math.random() * 0.5);
}
return true;
}
}
return false;
}
function checkBoostItemCollision(player) {
const p = player.position;
const pr = CFG.playerRadius;
for (const boost of state.chunkHistory[0].boostItems) {
if (!boost.userData.active) continue;
if (!isValidPos(boost.position.x) || !isValidPos(boost.position.y) || !isValidPos(boost.position.z)) continue;
if (p.distanceTo(boost.position) < pr + boost.geometry.parameters.radius) {
state.turbo = state.maxTurbo;
state.score += 25 * state.multiplier;
playBoost();
boost.userData.active = false;
for (let i = 0; i < 8; i++) {
emitParticle(boost.position.x, boost.position.y, boost.position.z,
(Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, 0.5 + Math.random() * 0.5);
}
return true;
}
}
return false;
}
const speedValEl = document.getElementById('speedVal');
const scoreValEl = document.getElementById('scoreVal');
const multiplierValEl = document.getElementById('multiplierVal');
const turboValEl = document.getElementById('turboVal');
const energyBarFill = document.getElementById('energyBarFill');
const multiplierDisplay = document.getElementById('multiplierDisplay');
const turboIndicator = document.getElementById('turboIndicator');
const speedometerCanvas = document.getElementById('speedometerCanvas');
const speedometerCtx = speedometerCanvas.getContext('2d');
const challengeEl = document.getElementById('challengeIndicator');
function updateHUD() {
speedValEl.textContent = state.speed.toFixed(1) + ' u/s';
scoreValEl.textContent = state.score.toLocaleString();
if (state.multiplier > 1) {
multiplierValEl.textContent = state.multiplier.toFixed(1) + 'x';
multiplierValEl.classList.add('nearmiss');
} else {
multiplierValEl.textContent = '1.0x';
multiplierValEl.classList.remove('nearmiss');
}
const turboPct = Math.round((state.turbo / state.maxTurbo) * 100);
turboValEl.textContent = turboPct + '%';
turboValEl.classList.toggle('boost', state.turbo > 50);
energyBarFill.style.width = state.energy + '%';
energyBarFill.style.background = `linear-gradient(90deg, #FF6B35, #FFD700, #00E5FF)`;
multiplierDisplay.textContent = state.multiplier.toFixed(1) + 'x';
multiplierDisplay.classList.toggle('active', state.multiplier > 1);
turboIndicator.classList.toggle('active', state.turbo > 50);
}
function showMultiplier(mult) {
multiplierDisplay.textContent = mult.toFixed(1) + 'x';
multiplierDisplay.classList.add('active');
}
function updateSpeedometer() {
const ctx = speedometerCtx;
const w = 130, h = 130;
ctx.clearRect(0, 0, w, h);
const cx = w / 2, cy = h / 2;
const r = 50;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.lineWidth = 3;
ctx.stroke();
const speedNorm = Math.min(1, state.speed / CFG.playerMaxSpeed);
const startAngle = Math.PI;
const endAngle = Math.PI + Math.PI * 2 * speedNorm;
ctx.beginPath();
ctx.arc(cx, cy, r - 3, startAngle, endAngle);
ctx.strokeStyle = state.speed > CFG.playerMaxSpeed * 0.8 ? '#FF6B35' : '#FFD700';
ctx.lineWidth = 5;
ctx.stroke();
ctx.beginPath();
ctx.arc(cx, cy, r - 3, startAngle, endAngle);
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(Math.round(state.speed * 10) + 'km/h', cx, cy - 2);
const needleAngle = startAngle + Math.PI * 2 * speedNorm;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(needleAngle) * 30, cy + Math.sin(needleAngle) * 30);
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(cx, cy, 3, 0, Math.PI * 2);
ctx.fillStyle = '#FFD700';
ctx.fill();
}
function startGame() {
initAudio();
state.phase = 'playing';
state.score = 0;
state.multiplier = 1.0;
state.energy = 100;
state.turbo = 0;
state.speed = 0;
state.maxSpeed = 0;
state.nearMiss = 0;
state.nearMissTimer = 0;
state.highSpeed = 0;
state.distance = 0;
state.difficulty = 1;
state.challengeScore = 0;
state.player.position.set(0, 0, 0);
state.player.velocity.set(0, 0, 0);
state.player.yaw = 0;
state.player.pitch = 0;
state.player.roll = 0;
state.player.prevYaw = 0;
state.player.prevPitch = 0;
state.player.prevRoll = 0;
state.player.thrust = 0;
state.cameraShake.intensity = 0;
state.chunkHistory = [];
state.nextChunkX = 100;
state.lastChunkZ = 0;
state.spawnTimer = 0;
leftWallMeshes.forEach(m => scene.remove(m));
rightWallMeshes.forEach(m => scene.remove(m));
gapMeshes.forEach(m => scene.remove(m));
monolithMeshes.forEach(m => scene.remove(m));
orbMeshes.forEach(m => scene.remove(m));
boostItemMeshes.forEach(m => scene.remove(m));
leftWallMeshes = [];
rightWallMeshes = [];
gapMeshes = [];
monolithMeshes = [];
orbMeshes = [];
boostItemMeshes = [];
particlePool.forEach(p => { p.active = false; p.mesh.visible = false; });
playerGroup.position.set(0, 0, 0);
playerGroup.rotation.set(0, 0, 0);
camera.position.set(0, 60, 200);
camera.lookAt(0, 0, 0);
document.getElementById('gameOverScreen').classList.remove('visible');
document.getElementById('pauseOverlay').classList.remove('visible');
multiplierDisplay.classList.remove('active');
turboIndicator.classList.remove('active');
energyBarFill.style.width = '100%';
state.chunkHistory.push(generateChunk(100, 0, 1));
updateHUD();
}
function pauseGame() {
if (state.phase !== 'playing') return;
state.phase = 'paused';
document.getElementById('pauseOverlay').classList.add('visible');
}
function resumeGame() {
if (state.phase !== 'paused') return;
state.phase = 'playing';
document.getElementById('pauseOverlay').classList.remove('visible');
}
function gameOver() {
state.phase = 'gameover';
playGameOver();
document.getElementById('finalScore').textContent = state.score;
document.getElementById('maxSpeed').textContent = state.maxSpeed.toFixed(1) + ' u/s';
document.getElementById('finalMultiplier').textContent = state.multiplier.toFixed(1) + 'x';
document.getElementById('finalDistance').textContent = Math.floor(state.distance) + 'm';
setTimeout(() => {
document.getElementById('gameOverScreen').classList.add('visible');
}, 500);
}
function restartGame() {
startGame();
}
const keys = { up: false, down: false, left: false, right: false, boost: false };
document.addEventListener('keydown', e => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.up = true;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.down = true;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.left = true;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.right = true;
if (e.code === 'Space') { keys.boost = true; e.preventDefault(); }
if (e.code === 'KeyP' || e.code === 'Escape') {
if (state.phase === 'playing') pauseGame();
else if (state.phase === 'paused') resumeGame();
}
if (e.code === 'KeyR') restartGame();
});
document.addEventListener('keyup', e => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.up = false;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.down = false;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.left = false;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.right = false;
if (e.code === 'Space') keys.boost = false;
});
const btnUp = document.getElementById('btnUp');
const btnDown = document.getElementById('btnDown');
const btnLeft = document.getElementById('btnLeft');
const btnRight = document.getElementById('btnRight');
const btnBoost = document.getElementById('btnBoost');
function setupMobileControls() {
btnUp.addEventListener('touchstart', e => { e.preventDefault(); keys.up = true; });
btnUp.addEventListener('touchend', () => { keys.up = false; });
btnDown.addEventListener('touchstart', e => { e.preventDefault(); keys.down = true; });
btnDown.addEventListener('touchend', () => { keys.down = false; });
btnLeft.addEventListener('touchstart', e => { e.preventDefault(); keys.left = true; });
btnLeft.addEventListener('touchend', () => { keys.left = false; });
btnRight.addEventListener('touchstart', e => { e.preventDefault(); keys.right = true; });
btnRight.addEventListener('touchend', () => { keys.right = false; });
btnBoost.addEventListener('touchstart', e => { e.preventDefault(); keys.boost = true; });
btnBoost.addEventListener('touchend', () => { keys.boost = false; });
}
setupMobileControls();
let lastTime = 0;
function update(dt) {
if (state.phase !== 'playing') return;
state.difficulty = 1 + state.score / 500;
const pitchInput = keys.up ? 1 : -1;
const yawInput = keys.right ? 1 : (keys.left ? -1 : 0);
const boostInput = keys.boost;
state.player.thrust = 0;
if (pitchInput > 0) state.player.thrust += CFG.playerAccel * pitchInput * dt;
if (pitchInput < 0) state.player.thrust -= CFG.playerAccel * Math.abs(pitchInput) * dt;
const steerInput = yawInput * CFG.playerSteer * dt;
state.player.yaw += steerInput;
state.player.roll += steerInput * 0.3 * dt;
state.player.pitch = pitchInput * 0.15 * dt;
if (boostInput && state.turbo < state.maxTurbo) {
state.turbo = Math.min(state.maxTurbo, state.turbo + CFG.turboFillRate * dt);
} else if (boostInput && state.turbo >= state.maxTurbo) {
state.turbo = state.maxTurbo;
} else if (!boostInput && state.turbo > 0) {
state.turbo = Math.max(0, state.turbo - 0.5 * dt);
}
state.energy = Math.max(0, state.energy - CFG.energyDecay * dt);
const speedMultiplier = 1 + state.turbo / state.maxTurbo * 0.5;
const maxSpd = CFG.playerMaxSpeed * speedMultiplier;
state.speed = Math.max(-5, Math.min(maxSpd, state.speed + state.player.thrust * (1 - CFG.playerDecel * dt)));
state.player.position.x += state.player.velocity.x * dt;
state.player.position.y += state.player.velocity.y * dt;
state.player.position.z += state.player.speed * dt;
playerGroup.position.copy(state.player.position);
const camDist = 100;
const camY = 50 + state.player.speed * 0.02;
const camX = state.player.position.x;
const camZ = state.player.position.z + camDist;
if (state.cameraShake.intensity > 0) {
const shakeX = (Math.random() - 0.5) * state.cameraShake.intensity * 2;
const shakeY = (Math.random() - 0.5) * state.cameraShake.intensity * 2;
camera.position.x = camX + shakeX;
camera.position.y = camY + shakeY;
camera.position.z = camZ;
state.cameraShake.intensity *= 0.95;
if (state.cameraShake.intensity < 0.01) state.cameraShake.intensity = 0;
} else {
camera.position.x = camX;
camera.position.y = camY;
camera.position.z = camZ;
}
const lookAt = new THREE.Vector3(state.player.position.x, state.player.position.y + 30, state.player.position.z);
camera.lookAt(lookAt);
const targetFov = 75 + state.turbo / state.maxTurbo * 15;
camera.fov = THREE.MathUtils.lerp(camera.fov, targetFov, 0.1);
camera.updateProjectionMatrix();
if (state.energy <= 0) {
gameOver();
return;
}
const playerMinX = state.player.position.x - CFG.playerRadius;
const playerMaxX = state.player.position.x + CFG.playerRadius;
const playerMinY = state.player.position.y - CFG.playerRadius;
const playerMaxY = state.player.position.y + CFG.playerRadius;
const playerMinZ = state.player.position.z - CFG.playerRadius;
const playerMaxZ = state.player.position.z + CFG.playerRadius;
for (const wall of state.chunkHistory[0].walls) {
const w = wall.position;
if (playerMaxX > w.x - 0.5 && playerMinX < w.x + 0.5 &&
playerMaxY > w.y - 140 && playerMinY < w.y + 140 &&
playerMaxZ > w.z - 7.5 && playerMinZ < w.z + 7.5) {
gameOver();
return;
}
}
for (const obj of state.chunkHistory[0].obstacles) {
checkNearMiss(state.player, obj);
}
for (const orb of state.chunkHistory[0].orbs) {
if (orb.userData.active && state.player.position.distanceTo(orb.position) < CFG.playerRadius + orb.geometry.parameters.radius) {
state.energy = Math.min(100, state.energy + CFG.energyOrbValue);
state.score += 10 * state.multiplier;
playCollect();
orb.userData.active = false;
for (let i = 0; i < 5; i++) {
emitParticle(orb.position.x, orb.position.y, orb.position.z,
(Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, 0.5 + Math.random() * 0.5);
}
}
}
for (const boost of state.chunkHistory[0].boostItems) {
if (boost.userData.active && state.player.position.distanceTo(boost.position) < CFG.playerRadius + boost.geometry.parameters.radius) {
state.turbo = state.maxTurbo;
state.score += 25 * state.multiplier;
playBoost();
boost.userData.active = false;
for (let i = 0; i < 8; i++) {
emitParticle(boost.position.x, boost.position.y, boost.position.z,
(Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, 0.5 + Math.random() * 0.5);
}
}
}
state.spawnTimer -= dt;
if (state.spawnTimer <= 0) {
state.spawnTimer = 1.5 / (state.difficulty * 0.5);
state.chunkHistory.push(generateChunk(state.nextChunkX, state.lastChunkZ, state.difficulty));
state.nextChunkX += CFG.chunkWidth;
}
if (state.speed > 3) {
const spread = state.speed * 0.02;
const angle = state.player.yaw;
emitParticle(
state.player.position.x - Math.cos(angle) * 2,
state.player.position.y - 1,
state.player.position.z - Math.sin(angle) * 2,
Math.cos(angle + Math.PI) * spread + (Math.random() - 0.5) * 2,
-1 - Math.random() * 2,
Math.sin(angle) * spread + (Math.random() - 0.5) * 2,
0.4 + Math.random() * 0.3
);
}
if (Math.random() < 0.1) {
const px = state.player.position.x + (Math.random() - 0.5) * 100;
const py = state.player.position.y + Math.random() * 200;
const pz = state.player.position.z + 100 + Math.random() * 200;
emitParticle(px, py, pz, (Math.random() - 0.5) * 0.5, -Math.random() * 0.5, (Math.random() - 0.5) * 0.5, 1.0 + Math.random() * 1.0);
}
state.score += state.speed * 0.01 * dt;
state.distance += state.speed * dt;
if (state.nearMissTimer > 0) {
state.nearMissTimer -= dt;
if (state.nearMissTimer <= 0) {
state.multiplier = Math.max(1.0, state.multiplier - 0.3 * dt);
}
}
if (state.speed > CFG.playerMaxSpeed * 0.8) {
state.highSpeedCount++;
} else {
state.highSpeedCount = Math.max(0, state.highSpeedCount - 1);
}
state.maxSpeed = Math.max(state.maxSpeed, state.speed);
updateParticles(dt);
updateSpeedometer();
updateHUD();
challengeEl.textContent = `SCORE: ${state.score.toLocaleString()}`;
}
function updateLoop(timestamp) {
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
if (state.phase === 'playing') {
update(dt);
}
composer.render();
requestAnimationFrame(updateLoop);
}
function generateChunk(x, z, diff) {
const chunk = {
x, z,
walls: [],
obstacles: [],
orbs: [],
boostItems: [],
gaps: [],
wallData: [],
gapData: [],
};
const gapCount = 2 + Math.floor(Math.random() * 3);
const gapData = [];
for (let i = 0; i < gapCount; i++) {
gapData.push({
x: -CFG.canyonWidth / 2 + (i + 1) * (CFG.canyonWidth / (gapCount + 1)),
width: CFG.gapMinWidth + Math.random() * (CFG.gapMaxWidth - CFG.gapMinWidth),
});
}
gapData.sort((a, b) => a.x - b.x);
for (const g of gapData) {
const gx = g.x;
const gw = g.width;
const gH = CFG.wallHeight;
const lw = new THREE.Mesh(wallGeo, wallMat);
lw.position.set(gx - gw / 2 - 1, gH / 2, z);
lw.renderOrder = 1;
scene.add(lw);
chunk.walls.push(lw);
chunk.wallData.push({ pos: new THREE.Vector3(gx - gw / 2 - 1, gH / 2, z), halfW: 0.5, halfH: gH / 2, halfD: 7.5, type: 'wall' });
const rw = new THREE.Mesh(wallGeo, wallMat);
rw.position.set(gx + gw / 2 + 1, gH / 2, z);
rw.renderOrder = 1;
scene.add(rw);
chunk.walls.push(rw);
chunk.wallData.push({ pos: new THREE.Vector3(gx + gw / 2 + 1, gH / 2, z), halfW: 0.5, halfH: gH / 2, halfD: 7.5, type: 'wall' });
const gapFloorGeo = new THREE.BoxGeometry(gw, 1, 30);
const gapFloorMat = new THREE.MeshStandardMaterial({
color: 0x0a0a1a, transparent: true, opacity: 0.15,
side: THREE.DoubleSide,
});
const gapFloor = new THREE.Mesh(gapFloorGeo, gapFloorMat);
gapFloor.position.set(gx, 0.01, z);
gapFloor.renderOrder = 0;
scene.add(gapFloor);
chunk.gaps.push(gapFloor);
chunk.gapData.push({ x: gx, width: gw });
}
const numMonoliths = 1 + Math.floor(Math.random() * Math.min(3, Math.floor(diff * 0.5)));
for (let i = 0; i < numMonoliths; i++) {
const mono = createMonolith(x, z, diff);
chunk.obstacles.push(mono);
}
const numOrbs = 2 + Math.floor(Math.random() * 3);
for (let i = 0; i < numOrbs; i++) {
createOrb(x, z);
chunk.orbs.push(chunk.orbs[chunk.orbs.length - 1]);
}
const numBoosts = 1 + Math.floor(Math.random() * 2);
for (let i = 0; i < numBoosts; i++) {
createBoostItem(x, z);
chunk.boostItems.push(chunk.boostItems[chunk.boostItems.length - 1]);
}
return chunk;
}
function runLoop(timestamp) {
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
if (state.phase === 'playing') {
update(dt);
}
composer.render();
requestAnimationFrame(runLoop);
}
function resize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', resize);
initParticlePool();
state.chunkHistory.push(generateChunk(100, 0, 1));
requestAnimationFrame(runLoop);
</script>
</body>
</html>