Canyon Run
Qwen 3.6 27b · typescript

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 73,129 bytes · SHA-256 9b3172190bcd
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Canyon Run — High-Speed Procedural Racer</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0514; }
#app { width: 100%; height: 100%; position: relative; }
#canvas-container { width: 100%; height: 100%; }
#canvas-container canvas { display: block; width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./player":"src/player.ts","./world":"src/world.ts","./collision":"src/collision.ts","./audio":"src/audio.ts","./particles":"src/particles.ts","./hud":"src/hud.ts","./renderer":"src/renderer.ts","./types":"src/types.ts"},"src/state.ts":{"./types":"src/types.ts"},"src/player.ts":{"./types":"src/types.ts"},"src/world.ts":{"./types":"src/types.ts"},"src/collision.ts":{"./types":"src/types.ts"},"src/particles.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/// <reference path="../globals.d.ts" />
const state_1 = require("./state");
const player_1 = require("./player");
const world_1 = require("./world");
const collision_1 = require("./collision");
const audio_1 = require("./audio");
const particles_1 = require("./particles");
const hud_1 = require("./hud");
const renderer_1 = require("./renderer");
// Module-level references
let renderer;
let playerCraft;
let world;
let collision;
let audio;
let particles;
let hud;
let playerState;
let gameState;
let inputState;
let lastTime = 0;
let animFrameId = 0;
let scoreTimer = 0;
function setupInput() {
window.addEventListener('keydown', (e) => {
state_1.keys[e.code] = true;
// Prevent scrolling
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
state_1.keys[e.code] = false;
});
// Touch controls
let touchStartX = 0;
let touchStartY = 0;
window.addEventListener('touchstart', (e) => {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}, { passive: true });
window.addEventListener('touchmove', (e) => {
const dx = e.touches[0].clientX - touchStartX;
const dy = e.touches[0].clientY - touchStartY;
state_1.keys['ArrowLeft'] = dx < -30;
state_1.keys['ArrowRight'] = dx > 30;
state_1.keys['ArrowUp'] = dy < -30;
state_1.keys['ArrowDown'] = dy > 30;
}, { passive: true });
window.addEventListener('touchend', () => {
state_1.keys['ArrowLeft'] = false;
state_1.keys['ArrowRight'] = false;
state_1.keys['ArrowUp'] = false;
state_1.keys['ArrowDown'] = false;
});
}
function getInput() {
return {
left: state_1.keys['ArrowLeft'] || state_1.keys['KeyA'],
right: state_1.keys['ArrowRight'] || state_1.keys['KeyD'],
up: state_1.keys['ArrowUp'] || state_1.keys['KeyW'],
down: state_1.keys['ArrowDown'] || state_1.keys['KeyS'],
boost: state_1.keys['Space'],
};
}
function startGame() {
// Cancel old loops
if (animFrameId)
cancelAnimationFrame(animFrameId);
// Clear canvas container
const cc = document.getElementById('canvas-container');
if (cc)
cc.innerHTML = '';
// Reset state
playerState = (0, state_1.createPlayer)();
gameState = (0, state_1.createGameState)();
gameState.started = true;
// Create fresh renderer
const camState = (0, state_1.createCameraState)();
renderer = new renderer_1.Renderer(state_1.CONFIG, camState);
// Create fresh systems
playerCraft = new player_1.PlayerCraft(state_1.CONFIG);
renderer.scene.add(playerCraft.mesh);
world = new world_1.WorldGenerator(state_1.CONFIG, renderer.scene);
collision = new collision_1.CollisionSystem(state_1.CONFIG);
particles = new particles_1.ParticleManager(renderer.scene, state_1.CONFIG);
// Generate initial chunks
for (let i = 0; i < state_1.CONFIG.chunkCount; i++) {
world.generateChunk(i * state_1.CONFIG.chunkLength, 1);
}
// Reset player
playerCraft.reset(playerState);
// Hide screens
hud.hideStartScreen();
hud.hideGameOver();
// Start audio
audio.resume();
audio.startEngine();
// Start game loop
lastTime = performance.now();
gameLoop(performance.now());
}
function gameOver() {
gameState.gameOver = true;
playerState.alive = false;
audio.playCrash();
audio.playGameOver();
audio.stopEngine();
// Crash particles
particles.emitCrash(playerState.position);
// Shake camera
renderer.triggerShake(4);
// Show game over after brief delay
setTimeout(() => {
hud.showGameOver(gameState.score, gameState.maxSpeed, gameState.maxMultiplier);
}, 800);
}
function gameLoop(timestamp) {
animFrameId = requestAnimationFrame(gameLoop);
const dt = Math.min((timestamp - lastTime) / 1000, 0.05); // Cap dt to prevent spiral
lastTime = timestamp;
if (!gameState.started || gameState.gameOver) {
// Still render for visual effect
renderer.render();
return;
}
// Get input
inputState = getInput();
// --- UPDATE ---
// Move player forward
const moveAmount = playerState.speed * dt;
playerState.position.z += moveAmount;
gameState.distance += moveAmount;
// Energy decay
playerState.energy -= state_1.CONFIG.energyDecay * dt;
if (playerState.energy <= 0) {
playerState.energy = 0;
gameOver();
renderer.render();
return;
}
// Turbo management
if (inputState.boost && playerState.turbo > 10 && !playerState.isBoosting) {
playerState.isBoosting = true;
audio.playBoost();
renderer.triggerShake(0.5);
}
if (playerState.isBoosting) {
playerState.turbo -= state_1.CONFIG.turboDecay * dt;
if (playerState.turbo <= 0) {
playerState.turbo = 0;
playerState.isBoosting = false;
}
}
else {
// Charge turbo while going fast
if (playerState.speed > state_1.CONFIG.maxSpeed * 0.6) {
playerState.turbo = Math.min(playerState.maxTurbo, playerState.turbo + state_1.CONFIG.turboChargeRate * dt);
}
}
// Update player physics
playerCraft.update(dt, inputState, playerState);
// Track max speed
if (playerState.speed > gameState.maxSpeed) {
gameState.maxSpeed = playerState.speed;
}
// Update world chunks
world.updateChunks(playerState.position.z);
// Get active obstacles and orbs
const obstacles = world.getActiveObstacles(playerState.position.z);
const orbs = world.getActiveOrbs(playerState.position.z);
// Check collisions
if (collision.checkObstacleCollisions(playerState, obstacles)) {
gameOver();
renderer.render();
return;
}
// Check near misses
const nearMissResult = collision.checkNearMisses(playerState, obstacles);
if (nearMissResult.hasNearMiss && !gameState.nearMissActive) {
gameState.nearMissActive = true;
gameState.nearMissTimer = state_1.CONFIG.nearMissDuration;
gameState.multiplier = Math.min(10, gameState.multiplier + state_1.CONFIG.nearMissMultiplierGain);
if (gameState.multiplier > gameState.maxMultiplier) {
gameState.maxMultiplier = gameState.multiplier;
}
hud.updateMultiplier(gameState.multiplier, true);
hud.flashNearMiss();
audio.playNearMiss();
renderer.triggerShake(1.0);
}
// Near miss timer decay
if (gameState.nearMissActive) {
gameState.nearMissTimer -= dt;
if (gameState.nearMissTimer <= 0) {
gameState.nearMissActive = false;
// Gradual multiplier decay
gameState.multiplier = Math.max(1, gameState.multiplier - dt * 0.3);
hud.updateMultiplier(gameState.multiplier, false);
}
}
// Collect orbs
const orbsCollected = collision.checkOrbCollection(playerState, orbs);
if (orbsCollected > 0) {
playerState.energy = Math.min(playerState.maxEnergy, playerState.energy + state_1.CONFIG.energyRecharge * orbsCollected);
gameState.score += orbsCollected * 100 * gameState.multiplier;
audio.playCollect();
particles.emitBurst(playerState.position, 0xffaa00, 10);
}
// Score accumulation
scoreTimer += dt;
if (scoreTimer >= 0.5) {
scoreTimer -= 0.5;
gameState.score += playerState.speed * gameState.multiplier * 0.1;
}
// Difficulty scaling
gameState.difficulty = 1 + gameState.distance * 0.0003;
// Animate world
world.animateOrbs(dt);
// Particles
particles.emitTrail(playerState.position, playerState.speed);
particles.emitDebris(playerState.position);
particles.update(dt, playerState.position.z);
// Update camera
renderer.updateCamera(playerState.position, playerState.speed, dt);
// Update audio
audio.updateEngine(playerState.speed, state_1.CONFIG.maxSpeed);
// --- RENDER ---
renderer.render();
// --- HUD ---
hud.updateSpeed(playerState.speed, state_1.CONFIG.maxSpeed * 1.5);
hud.updateEnergy(playerState.energy, playerState.maxEnergy);
hud.updateTurbo(playerState.turbo, playerState.maxTurbo, playerState.isBoosting);
hud.updateScore(gameState.score);
}
// --- ENTRY ---
// --- ENTRY ---
function init() {
// Initialize systems
audio = new audio_1.GameAudio();
hud = new hud_1.HUD();
// Setup input
setupInput();
// Create initial renderer and world (for background)
const camState = (0, state_1.createCameraState)();
renderer = new renderer_1.Renderer(state_1.CONFIG, camState);
world = new world_1.WorldGenerator(state_1.CONFIG, renderer.scene);
playerCraft = new player_1.PlayerCraft(state_1.CONFIG);
renderer.scene.add(playerCraft.mesh);
// Generate initial chunks
for (let i = 0; i < state_1.CONFIG.chunkCount; i++) {
world.generateChunk(i * state_1.CONFIG.chunkLength, 1);
}
// Show start screen
hud.showStartScreen();
// Start button callback
hud.onStart(() => {
audio.init();
startGame();
});
// Restart button callback
hud.onRestart(() => {
startGame();
});
// Start render loop for background
lastTime = performance.now();
backgroundLoop(performance.now());
}
function backgroundLoop(timestamp) {
if (gameState && gameState.started)
return; // Game loop takes over
requestAnimationFrame(backgroundLoop);
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
// Slow ambient camera movement
const t = timestamp * 0.0001;
renderer.camera.position.x = Math.sin(t) * 5;
renderer.camera.position.y = 5 + Math.sin(t * 0.7) * 2;
renderer.camera.position.z = -15;
const lookAt = new THREE.Vector3(0, 0, 100);
renderer.camera.lookAt(lookAt);
world.animateOrbs(dt);
renderer.render();
}
// Auto-run entry
init();
};
// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
/// <reference path="../globals.d.ts" />
// Game type definitions
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.touchPos = exports.touchActive = exports.mousePos = exports.keys = exports.CONFIG = void 0;
exports.createPlayer = createPlayer;
exports.createInput = createInput;
exports.createGameState = createGameState;
exports.createCameraState = createCameraState;
exports.CONFIG = {
baseSpeed: 80,
maxSpeed: 250,
acceleration: 40,
turnSpeed: 2.5,
pitchSpeed: 2.0,
bankFactor: 0.6,
energyDecay: 3.5,
energyRecharge: 25,
chunkLength: 200,
chunkCount: 8,
canyonWidth: 120,
canyonHeight: 80,
nearMissDistance: 18,
nearMissDuration: 1.5,
nearMissMultiplierGain: 0.5,
turboBoostSpeed: 150,
turboDecay: 8,
turboChargeRate: 2,
};
function createPlayer() {
return {
position: new THREE.Vector3(0, 0, 0),
velocity: new THREE.Vector3(0, 0, 0),
rotation: { roll: 0, pitch: 0, yaw: 0 },
speed: exports.CONFIG.baseSpeed,
energy: 100,
maxEnergy: 100,
turbo: 0,
maxTurbo: 100,
isBoosting: false,
alive: true,
};
}
function createInput() {
return { left: false, right: false, up: false, down: false, boost: false };
}
function createGameState() {
return {
score: 0,
multiplier: 1,
maxMultiplier: 1,
maxSpeed: 0,
distance: 0,
difficulty: 1,
nearMissTimer: 0,
nearMissActive: false,
gameOver: false,
paused: false,
started: false,
};
}
function createCameraState() {
return {
shake: 0,
shakeDecay: 0.9,
shakeDuration: 0,
fov: 70,
targetFov: 70,
distance: 15,
height: 5,
};
}
exports.keys = {};
exports.mousePos = { x: 0, y: 0 };
exports.touchActive = false;
exports.touchPos = { x: 0, y: 0 };
};
// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlayerCraft = void 0;
class PlayerCraft {
constructor(config) {
this.config = config;
this.trailPoints = [];
this.mesh = this.buildCraft();
this.mesh.castShadow = true;
}
buildCraft() {
const group = new THREE.Group();
// Main body - sleek arrow shape
const bodyGeo = new THREE.ConeGeometry(1.2, 5, 6);
const bodyMat = new THREE.MeshPhongMaterial({
color: 0xcccccc,
emissive: 0x333333,
flatShading: true,
specular: 0x666666,
shininess: 30,
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.rotation.x = Math.PI / 2;
body.receiveShadow = true;
group.add(body);
// Nose cone
const noseGeo = new THREE.ConeGeometry(0.6, 2, 6);
const noseMat = new THREE.MeshPhongMaterial({
color: 0xff6633,
emissive: 0xff3300,
emissiveIntensity: 0.3,
flatShading: true,
specular: 0xff8844,
shininess: 50,
});
const nose = new THREE.Mesh(noseGeo, noseMat);
nose.rotation.x = Math.PI / 2;
nose.position.z = -3.5;
nose.receiveShadow = true;
group.add(nose);
// Wings - left
const wingGeo = new THREE.BoxGeometry(4, 0.15, 2);
const wingMat = new THREE.MeshPhongMaterial({
color: 0x999999,
emissive: 0x222222,
flatShading: true,
});
const leftWing = new THREE.Mesh(wingGeo, wingMat);
leftWing.position.set(-2.2, -0.2, -0.5);
leftWing.rotation.z = 0.15;
leftWing.receiveShadow = true;
group.add(leftWing);
// Wings - right
const rightWing = new THREE.Mesh(wingGeo, wingMat);
rightWing.position.set(2.2, -0.2, -0.5);
rightWing.rotation.z = -0.15;
rightWing.receiveShadow = true;
group.add(rightWing);
// Wing tips (glowing)
const tipGeo = new THREE.BoxGeometry(0.3, 0.3, 0.3);
const tipMat = new THREE.MeshPhongMaterial({
color: 0xff4400,
emissive: 0xff2200,
emissiveIntensity: 1.5,
flatShading: true,
});
const leftTip = new THREE.Mesh(tipGeo, tipMat);
leftTip.position.set(-4.1, -0.2, -0.5);
group.add(leftTip);
const rightTip = new THREE.Mesh(tipGeo, tipMat);
rightTip.position.set(4.1, -0.2, -0.5);
group.add(rightTip);
// Engine glow
const engineGeo = new THREE.SphereGeometry(0.8, 8, 6);
const engineMat = new THREE.MeshBasicMaterial({
color: 0xff6600,
transparent: true,
opacity: 0.9,
});
this.engineGlow = new THREE.Mesh(engineGeo, engineMat);
this.engineGlow.position.z = 2.5;
group.add(this.engineGlow);
// Engine core (brighter)
const coreGeo = new THREE.SphereGeometry(0.4, 8, 6);
const coreMat = new THREE.MeshBasicMaterial({
color: 0xffdd44,
transparent: true,
opacity: 1,
});
const core = new THREE.Mesh(coreGeo, coreMat);
core.position.z = 2.5;
group.add(core);
return group;
}
update(dt, input, state) {
if (!state.alive)
return;
// Speed control based on pitch input
let targetSpeed = state.speed;
if (input.up) {
targetSpeed = Math.min(targetSpeed + this.config.acceleration * dt, this.config.maxSpeed);
}
else if (input.down) {
targetSpeed = Math.max(targetSpeed - this.config.acceleration * dt * 1.5, this.config.baseSpeed * 0.5);
}
// Boost adds extra speed
if (state.isBoosting) {
targetSpeed = Math.min(targetSpeed + this.config.turboBoostSpeed * dt * 0.5, this.config.maxSpeed * 1.5);
}
// Smooth speed transition
state.speed += (targetSpeed - state.speed) * Math.min(1, dt * 5);
// Lateral movement with momentum
const turnForce = this.config.turnSpeed;
const pitchForce = this.config.pitchSpeed;
if (input.left) {
state.velocity.x -= turnForce * dt;
}
if (input.right) {
state.velocity.x += turnForce * dt;
}
if (input.up) {
state.velocity.y += pitchForce * dt * 0.5;
}
if (input.down) {
state.velocity.y -= pitchForce * dt * 0.5;
}
// Damping (air resistance)
const damping = Math.pow(0.92, dt * 60);
state.velocity.x *= damping;
state.velocity.y *= damping;
// Apply velocity to position
state.position.x += state.velocity.x * dt;
state.position.y += state.velocity.y * dt;
// Clamp position to canyon bounds
const halfWidth = this.config.canyonWidth * 0.42;
const halfHeight = this.config.canyonHeight * 0.42;
state.position.x = Math.max(-halfWidth, Math.min(halfWidth, state.position.x));
state.position.y = Math.max(-halfHeight, Math.min(halfHeight, state.position.y));
// Banking (roll) based on lateral velocity
const targetRoll = -state.velocity.x * this.config.bankFactor * 0.08;
state.rotation.roll += (targetRoll - state.rotation.roll) * Math.min(1, dt * 8);
// Pitch based on vertical velocity
const targetPitch = -state.velocity.y * 0.04;
state.rotation.pitch += (targetPitch - state.rotation.pitch) * Math.min(1, dt * 6);
// Apply rotation to mesh
this.mesh.position.copy(state.position);
this.mesh.rotation.x = state.rotation.pitch;
this.mesh.rotation.y = state.rotation.yaw;
this.mesh.rotation.z = state.rotation.roll;
// Engine glow pulsation
const glowScale = 1 + Math.sin(Date.now() * 0.01) * 0.2;
const speedFactor = state.speed / this.config.maxSpeed;
this.engineGlow.scale.set(glowScale + speedFactor * 0.5, glowScale + speedFactor * 0.5, glowScale * 1.5 + speedFactor);
// Engine color shifts with speed
const engineColor = this.engineGlow.material;
if (state.isBoosting) {
engineColor.color.setHex(0x44ddff);
engineColor.opacity = 0.95 + Math.sin(Date.now() * 0.02) * 0.05;
}
else if (speedFactor > 0.7) {
engineColor.color.setHex(0xff4400);
}
else {
engineColor.color.setHex(0xff6600);
}
// Trail points
if (this.trailPoints.length === 0 ||
this.trailPoints[this.trailPoints.length - 1].distanceTo(state.position) > 1.5) {
this.trailPoints.push(state.position.clone());
if (this.trailPoints.length > 60) {
this.trailPoints.shift();
}
}
}
getTrailPoints() {
return this.trailPoints;
}
reset(state) {
state.position.set(0, 0, 0);
state.velocity.set(0, 0, 0);
state.rotation = { roll: 0, pitch: 0, yaw: 0 };
state.speed = this.config.baseSpeed;
state.energy = state.maxEnergy;
state.turbo = 0;
state.isBoosting = false;
state.alive = true;
this.trailPoints = [];
this.mesh.position.set(0, 0, 0);
this.mesh.rotation.x = 0;
this.mesh.rotation.y = 0;
this.mesh.rotation.z = 0;
}
}
exports.PlayerCraft = PlayerCraft;
};
// ── module: src/world.ts ──
__mods["src/world.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorldGenerator = void 0;
class WorldGenerator {
constructor(config, scene) {
this.chunks = [];
this.chunkIndex = 0;
this.config = config;
this.scene = scene;
// Cyber-sunset palette variations - brighter, more dramatic
this.colors = [
{ wall: 0x9a5068, ground: 0x4a2838, sky: 0x1a0a20, accent: 0xff6633 },
{ wall: 0xaa6070, ground: 0x4a2848, sky: 0x0a0018, accent: 0xff4422 },
{ wall: 0xb06878, ground: 0x503048, sky: 0x100820, accent: 0xff8844 },
{ wall: 0x905060, ground: 0x402838, sky: 0x100a20, accent: 0xff5533 },
];
}
generateChunk(z, difficulty) {
const chunk = {
z: z,
meshes: [],
orbs: [],
obstacles: [],
loaded: true,
active: true,
};
const colorSet = this.colors[Math.floor(Math.random() * this.colors.length)];
const canyonWidth = this.config.canyonWidth * (1 - difficulty * 0.005);
const canyonHeight = this.config.canyonHeight * (1 - difficulty * 0.003);
// Generate canyon walls for this chunk
this.generateCanyonWalls(chunk, z, canyonWidth, canyonHeight, colorSet, difficulty);
// Generate floating monoliths
const monolithCount = Math.floor(1 + difficulty * 0.3);
for (let i = 0; i < monolithCount; i++) {
this.generateMonolith(chunk, z + Math.random() * this.config.chunkLength, canyonWidth, canyonHeight, colorSet);
}
// Generate tight squeezes
if (difficulty > 2 && Math.random() < 0.3 * difficulty * 0.1) {
this.generateSqueeze(chunk, z + Math.random() * this.config.chunkLength, canyonWidth, canyonHeight, colorSet);
}
// Generate collectible orbs
const orbCount = 3 + Math.floor(Math.random() * 3);
for (let i = 0; i < orbCount; i++) {
this.generateOrb(chunk, z + Math.random() * this.config.chunkLength, canyonWidth * 0.5, canyonHeight * 0.5);
}
return chunk;
}
generateCanyonWalls(chunk, z, width, height, colorSet, difficulty) {
const segments = 8;
const segLength = this.config.chunkLength / segments;
for (let i = 0; i < segments; i++) {
const segZ = z + i * segLength;
// Wall height variation
const leftHeight = height * (0.4 + Math.random() * 0.6) * (1 + difficulty * 0.05);
const rightHeight = height * (0.4 + Math.random() * 0.6) * (1 + difficulty * 0.05);
// Wall thickness
const thickness = 8 + Math.random() * 12;
// Left wall - brighter with gradient
const leftWallGeo = new THREE.BoxGeometry(thickness, leftHeight, segLength * 0.9);
const leftWallMat = new THREE.MeshLambertMaterial({
color: colorSet.wall,
emissive: 0x221111,
emissiveIntensity: 0.1,
flatShading: true,
});
const leftWall = new THREE.Mesh(leftWallGeo, leftWallMat);
leftWall.position.set(-width / 2 - thickness / 2, leftHeight / 2 - height * 0.1, segZ);
leftWall.castShadow = true;
leftWall.receiveShadow = true;
this.scene.add(leftWall);
chunk.meshes.push(leftWall);
// Right wall
const rightWallGeo = new THREE.BoxGeometry(thickness, rightHeight, segLength * 0.9);
const rightWallMat = new THREE.MeshLambertMaterial({
color: colorSet.wall,
emissive: 0x221111,
emissiveIntensity: 0.1,
flatShading: true,
});
const rightWall = new THREE.Mesh(rightWallGeo, rightWallMat);
rightWall.position.set(width / 2 + thickness / 2, rightHeight / 2 - height * 0.1, segZ);
rightWall.castShadow = true;
rightWall.receiveShadow = true;
this.scene.add(rightWall);
chunk.meshes.push(rightWall);
// Wall accent strips (glowing edges)
if (Math.random() < 0.4) {
const stripGeo = new THREE.BoxGeometry(0.5, leftHeight * 0.8, segLength * 0.3);
const stripMat = new THREE.MeshBasicMaterial({
color: colorSet.accent,
transparent: true,
opacity: 0.4,
});
const strip = new THREE.Mesh(stripGeo, stripMat);
strip.position.set(-width / 2, leftHeight / 2 - height * 0.1, segZ);
this.scene.add(strip);
chunk.meshes.push(strip);
}
// Ceiling segments (occasional)
if (Math.random() < 0.3 + difficulty * 0.02) {
const ceilGeo = new THREE.BoxGeometry(width, 3, segLength * 0.5);
const ceilMat = new THREE.MeshLambertMaterial({
color: colorSet.wall,
flatShading: true,
});
const ceil = new THREE.Mesh(ceilGeo, ceilMat);
ceil.position.set(0, height * 0.4, segZ);
ceil.castShadow = true;
ceil.receiveShadow = true;
this.scene.add(ceil);
chunk.meshes.push(ceil);
}
// Floor
const floorGeo = new THREE.BoxGeometry(width, 2, segLength);
const floorMat = new THREE.MeshLambertMaterial({
color: colorSet.ground,
emissive: 0x110811,
emissiveIntensity: 0.05,
flatShading: true,
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.position.set(0, -height * 0.45, segZ);
floor.receiveShadow = true;
this.scene.add(floor);
chunk.meshes.push(floor);
// Stalactites from ceiling
if (Math.random() < 0.3) {
const stalHeight = 2 + Math.random() * 6;
const stalGeo = new THREE.ConeGeometry(0.5 + Math.random(), stalHeight, 4);
const stalMat = new THREE.MeshLambertMaterial({
color: colorSet.wall,
flatShading: true,
});
const stal = new THREE.Mesh(stalGeo, stalMat);
stal.position.set((Math.random() - 0.5) * width * 0.8, height * 0.35 - stalHeight / 2, segZ);
stal.rotation.x = Math.PI;
this.scene.add(stal);
chunk.meshes.push(stal);
}
}
}
generateMonolith(chunk, z, width, height, colorSet) {
const h = 10 + Math.random() * 20;
const w = 3 + Math.random() * 5;
const d = 3 + Math.random() * 5;
const xPos = (Math.random() - 0.5) * width * 0.7;
const yPos = (Math.random() - 0.5) * height * 0.5;
const geo = new THREE.BoxGeometry(w, h, d);
const mat = new THREE.MeshLambertMaterial({
color: colorSet.accent,
emissive: colorSet.accent,
emissiveIntensity: 0.15,
flatShading: true,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(xPos, yPos, z);
mesh.rotation.y = Math.random() * Math.PI;
mesh.castShadow = true;
mesh.receiveShadow = true;
this.scene.add(mesh);
chunk.meshes.push(mesh);
chunk.obstacles.push({
type: 'monolith',
position: new THREE.Vector3(xPos, yPos, z),
bounds: {
minX: xPos - w / 2, maxX: xPos + w / 2,
minY: yPos - h / 2, maxY: yPos + h / 2,
width: w, height: h, depth: d,
},
mesh: mesh,
passed: false,
nearMissChecked: false,
});
}
generateSqueeze(chunk, z, width, height, colorSet) {
// Two walls with a narrow gap
const gapSize = 8 + Math.random() * 6;
const gapY = (Math.random() - 0.5) * height * 0.3;
// Top wall
const topGeo = new THREE.BoxGeometry(width * 0.8, height * 0.3, 15);
const topMat = new THREE.MeshLambertMaterial({
color: colorSet.wall,
flatShading: true,
});
const top = new THREE.Mesh(topGeo, topMat);
top.position.set(0, gapY + gapSize / 2 + height * 0.15, z);
top.castShadow = true;
top.receiveShadow = true;
this.scene.add(top);
chunk.meshes.push(top);
// Bottom wall
const botGeo = new THREE.BoxGeometry(width * 0.8, height * 0.3, 15);
const botMat = new THREE.MeshLambertMaterial({
color: colorSet.wall,
flatShading: true,
});
const bot = new THREE.Mesh(botGeo, botMat);
bot.position.set(0, gapY - gapSize / 2 - height * 0.15, z);
bot.castShadow = true;
bot.receiveShadow = true;
this.scene.add(bot);
chunk.meshes.push(bot);
chunk.obstacles.push({
type: 'squeeze',
position: new THREE.Vector3(0, gapY, z),
bounds: {
minX: -width * 0.4, maxX: width * 0.4,
minY: gapY - gapSize / 2, maxY: gapY + gapSize / 2,
width: width * 0.8, height: gapSize, depth: 15,
},
mesh: top,
passed: false,
nearMissChecked: false,
});
}
generateOrb(chunk, z, halfWidth, halfHeight) {
const x = (Math.random() - 0.5) * halfWidth * 1.2;
const y = (Math.random() - 0.5) * halfHeight * 1.0;
const geo = new THREE.OctahedronGeometry(1.2, 0);
const mat = new THREE.MeshPhongMaterial({
color: 0xffaa00,
emissive: 0xff8800,
emissiveIntensity: 1.5,
flatShading: true,
transparent: true,
opacity: 0.9,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
this.scene.add(mesh);
chunk.meshes.push(mesh);
// Glow sprite around orb
const glowCanvas = document.createElement('canvas');
glowCanvas.width = 64;
glowCanvas.height = 64;
const ctx = glowCanvas.getContext('2d');
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
gradient.addColorStop(0, 'rgba(255, 170, 0, 0.8)');
gradient.addColorStop(0.3, 'rgba(255, 120, 0, 0.4)');
gradient.addColorStop(1, 'rgba(255, 80, 0, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64);
const glowTex = new THREE.CanvasTexture(glowCanvas);
const glowMat = new THREE.SpriteMaterial({
map: glowTex,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const glowSprite = new THREE.Sprite(glowMat);
glowSprite.scale.set(6, 6, 1);
mesh.add(glowSprite);
chunk.orbs.push({
position: new THREE.Vector3(x, y, z),
mesh: mesh,
collected: false,
glowPhase: Math.random() * Math.PI * 2,
value: 1,
});
}
updateChunks(playerZ) {
// Remove chunks behind player
const removeThreshold = this.config.chunkLength * 2;
const chunksToRemove = this.chunks.filter(c => c.z < playerZ - removeThreshold);
for (const chunk of chunksToRemove) {
for (const mesh of chunk.meshes) {
this.scene.remove(mesh);
if (mesh.geometry) {
mesh.geometry.dispose?.();
}
if (mesh.material) {
mesh.material.dispose?.();
}
}
chunk.active = false;
}
this.chunks = this.chunks.filter(c => c.z >= playerZ - removeThreshold);
// Add new chunks ahead
let maxChunkZ = this.chunks.length > 0
? Math.max(...this.chunks.map(c => c.z))
: 0;
while (maxChunkZ < playerZ + this.config.chunkLength * this.config.chunkCount) {
const newChunk = this.generateChunk(maxChunkZ, this.chunks.length * 0.5);
this.chunks.push(newChunk);
maxChunkZ += this.config.chunkLength;
}
}
getActiveObstacles(playerZ) {
const range = this.config.chunkLength * 2;
const result = [];
for (const chunk of this.chunks) {
if (chunk.z >= playerZ - range && chunk.z <= playerZ + range) {
result.push(...chunk.obstacles);
}
}
return result;
}
getActiveOrbs(playerZ) {
const range = this.config.chunkLength * 2;
const result = [];
for (const chunk of this.chunks) {
if (chunk.z >= playerZ - range && chunk.z <= playerZ + range) {
result.push(...chunk.orbs);
}
}
return result;
}
getActiveWallMeshes(playerZ) {
const range = this.config.chunkLength * 3;
const result = [];
for (const chunk of this.chunks) {
if (chunk.z >= playerZ - range && chunk.z <= playerZ + range) {
result.push(...chunk.meshes);
}
}
return result;
}
reset(scene) {
for (const chunk of this.chunks) {
for (const mesh of chunk.meshes) {
scene.remove(mesh);
}
}
this.chunks = [];
this.chunkIndex = 0;
}
animateOrbs(dt) {
for (const chunk of this.chunks) {
for (const orb of chunk.orbs) {
if (orb.collected)
continue;
orb.glowPhase += dt * 3;
orb.mesh.rotation.y += dt * 2;
orb.mesh.rotation.x += dt * 1;
const pulse = 1 + Math.sin(orb.glowPhase) * 0.2;
orb.mesh.scale.set(pulse, pulse, pulse);
}
}
}
}
exports.WorldGenerator = WorldGenerator;
};
// ── module: src/collision.ts ──
__mods["src/collision.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CollisionSystem = void 0;
class CollisionSystem {
constructor(config) {
this.playerRadius = 2.5;
this.config = config;
}
checkObstacleCollisions(player, obstacles) {
if (!player.alive)
return false;
for (const obs of obstacles) {
const b = obs.bounds;
const px = player.position.x;
const py = player.position.y;
const pz = player.position.z;
// AABB vs sphere check
const closestX = Math.max(b.minX, Math.min(px, b.maxX));
const closestY = Math.max(b.minY, Math.min(py, b.maxY));
const closestZ = Math.max(obs.position.z - b.depth / 2, Math.min(pz, obs.position.z + b.depth / 2));
const dx = px - closestX;
const dy = py - closestY;
const dz = pz - closestZ;
const distSq = dx * dx + dy * dy + dz * dz;
if (distSq < this.playerRadius * this.playerRadius) {
return true; // Collision!
}
}
return false;
}
checkNearMisses(player, obstacles) {
let closestDist = Infinity;
let hasNearMiss = false;
for (const obs of obstacles) {
if (obs.nearMissChecked)
continue;
const b = obs.bounds;
const px = player.position.x;
const py = player.position.y;
const pz = player.position.z;
const closestX = Math.max(b.minX, Math.min(px, b.maxX));
const closestY = Math.max(b.minY, Math.min(py, b.maxY));
const closestZ = Math.max(obs.position.z - b.depth / 2, Math.min(pz, obs.position.z + b.depth / 2));
const dx = px - closestX;
const dy = py - closestY;
const dz = pz - closestZ;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < closestDist) {
closestDist = dist;
}
// Near miss check
if (dist < this.config.nearMissDistance && dist > this.playerRadius) {
hasNearMiss = true;
obs.nearMissChecked = true;
}
// Mark as passed if player is beyond it
if (pz > obs.position.z + b.depth / 2 + 5) {
obs.passed = true;
obs.nearMissChecked = true;
}
}
return { hasNearMiss, closestDist };
}
checkOrbCollection(player, orbs) {
let collected = 0;
const collectRadius = 4;
for (const orb of orbs) {
if (orb.collected)
continue;
const dx = player.position.x - orb.position.x;
const dy = player.position.y - orb.position.y;
const dz = player.position.z - orb.position.z;
const distSq = dx * dx + dy * dy + dz * dz;
if (distSq < collectRadius * collectRadius) {
orb.collected = true;
orb.mesh.visible = false;
collected += orb.value;
}
}
return collected;
}
checkWallProximity(player) {
const halfWidth = this.config.canyonWidth * 0.42;
const halfHeight = this.config.canyonHeight * 0.42;
const wallDistX = halfWidth - Math.abs(player.position.x);
const wallDistY = halfHeight - Math.abs(player.position.y);
return Math.min(wallDistX, wallDistY);
}
}
exports.CollisionSystem = CollisionSystem;
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameAudio = void 0;
class GameAudio {
constructor() {
this.ctx = null;
this.engineOsc = null;
this.engineGain = null;
this.masterGain = null;
this.enabled = false;
}
init() {
try {
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.3;
this.masterGain.connect(this.ctx.destination);
this.enabled = true;
}
catch (e) {
console.warn('Web Audio not available:', e);
}
}
resume() {
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
startEngine() {
if (!this.ctx || !this.masterGain)
return;
// Low engine hum
this.engineOsc = this.ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 55;
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0.08;
// Low-pass filter for engine rumble
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 200;
filter.Q.value = 2;
this.engineOsc.connect(filter);
filter.connect(this.engineGain);
this.engineGain.connect(this.masterGain);
this.engineOsc.start();
}
updateEngine(speed, maxSpeed) {
if (!this.engineOsc || !this.engineGain)
return;
const ratio = speed / maxSpeed;
this.engineOsc.frequency.value = 40 + ratio * 80;
this.engineGain.gain.value = 0.05 + ratio * 0.1;
}
stopEngine() {
if (this.engineOsc) {
this.engineOsc.stop();
this.engineOsc.disconnect();
this.engineOsc = null;
}
if (this.engineGain) {
this.engineGain.disconnect();
this.engineGain = null;
}
}
playCollect() {
if (!this.ctx || !this.masterGain)
return;
const osc = this.ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(880, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1760, this.ctx.currentTime + 0.1);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.25);
}
playNearMiss() {
if (!this.ctx || !this.masterGain)
return;
// Whoosh sound
const bufferSize = this.ctx.sampleRate * 0.3;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.15));
}
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 1200;
filter.Q.value = 3;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.2, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);
source.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
source.start();
}
playBoost() {
if (!this.ctx || !this.masterGain)
return;
const osc = this.ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(400, this.ctx.currentTime + 0.3);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.02, this.ctx.currentTime + 0.5);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.6);
}
playCrash() {
if (!this.ctx || !this.masterGain)
return;
// Explosion noise
const bufferSize = this.ctx.sampleRate * 0.8;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3));
}
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 800;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.4, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.8);
source.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
source.start();
// Low boom
const osc = this.ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(80, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(20, this.ctx.currentTime + 0.5);
const oscGain = this.ctx.createGain();
oscGain.gain.setValueAtTime(0.3, this.ctx.currentTime);
oscGain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
osc.connect(oscGain);
oscGain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.6);
}
playGameOver() {
if (!this.ctx || !this.masterGain)
return;
const ctx = this.ctx;
const mg = this.masterGain;
const notes = [440, 370, 311, 261];
notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.value = freq;
const gain = ctx.createGain();
const t = ctx.currentTime + i * 0.2;
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(0.1, t + 0.05);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
osc.connect(gain);
gain.connect(mg);
osc.start(t);
osc.stop(t + 0.5);
});
}
setVolume(v) {
if (this.masterGain) {
this.masterGain.gain.value = v;
}
}
}
exports.GameAudio = GameAudio;
};
// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParticleManager = void 0;
class ParticleManager {
constructor(scene, config) {
this.burstParticles = [];
this.scene = scene;
this.config = config;
this.trailSystem = {
particles: [],
maxParticles: 200,
emitRate: 8,
emitTimer: 0,
};
this.debrisSystem = {
particles: [],
maxParticles: 100,
emitRate: 3,
emitTimer: 0,
};
}
emitTrail(position, speed) {
const system = this.trailSystem;
system.emitTimer += speed * 0.05;
while (system.emitTimer >= 1 && system.particles.length < system.maxParticles) {
system.emitTimer -= 1;
const p = this.createParticle(position, {
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
vz: 2 + Math.random() * 2,
life: 0.8 + Math.random() * 0.4,
size: 0.3 + Math.random() * 0.3,
color: speed > this.config.maxSpeed * 0.8 ? 0x44aaff : 0xff6633,
});
system.particles.push(p);
this.scene.add(p.mesh);
}
}
emitDebris(position) {
const system = this.debrisSystem;
system.emitTimer += 1;
while (system.emitTimer >= 1 && system.particles.length < system.maxParticles) {
system.emitTimer -= 1;
const angle = Math.random() * Math.PI * 2;
const dist = this.config.canyonWidth * 0.35 + Math.random() * 5;
const p = this.createParticle(new THREE.Vector3(position.x + Math.cos(angle) * dist, position.y + (Math.random() - 0.5) * 20, position.z + Math.random() * 10), {
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
vz: 5 + Math.random() * 10,
life: 0.5 + Math.random() * 0.5,
size: 0.2 + Math.random() * 0.4,
color: 0x886655,
});
system.particles.push(p);
this.scene.add(p.mesh);
}
}
emitBurst(position, color, count = 15) {
for (let i = 0; i < count; i++) {
const p = this.createParticle(position, {
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
vz: (Math.random() - 0.5) * 8,
life: 0.3 + Math.random() * 0.5,
size: 0.3 + Math.random() * 0.5,
color: color,
});
this.burstParticles.push(p);
this.scene.add(p.mesh);
}
}
emitCrash(position) {
for (let i = 0; i < 40; i++) {
const p = this.createParticle(position, {
vx: (Math.random() - 0.5) * 20,
vy: (Math.random() - 0.5) * 20,
vz: (Math.random() - 0.5) * 20,
life: 0.5 + Math.random() * 1.0,
size: 0.5 + Math.random() * 1.5,
color: Math.random() > 0.5 ? 0xff4400 : 0xffaa00,
});
this.burstParticles.push(p);
this.scene.add(p.mesh);
}
}
createParticle(pos, opts) {
const geo = new THREE.BoxGeometry(opts.size, opts.size, opts.size);
const mat = new THREE.MeshBasicMaterial({
color: opts.color,
transparent: true,
opacity: 0.8,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
return {
position: pos.clone(),
velocity: new THREE.Vector3(opts.vx, opts.vy, opts.vz),
life: opts.life,
maxLife: opts.life,
size: opts.size,
color: opts.color,
mesh: mesh,
};
}
update(dt, playerZ) {
const allParticles = [
...this.trailSystem.particles,
...this.debrisSystem.particles,
...this.burstParticles,
];
for (let i = allParticles.length - 1; i >= 0; i--) {
const p = allParticles[i];
p.life -= dt;
if (p.life <= 0 || !p.mesh) {
this.removeParticle(p, i, allParticles);
continue;
}
// Update position
p.position.x += p.velocity.x * dt;
p.position.y += p.velocity.y * dt;
p.position.z += p.velocity.z * dt;
if (p.mesh) {
p.mesh.position.copy(p.position);
const alpha = Math.max(0, p.life / p.maxLife);
p.mesh.material.opacity = alpha * 0.8;
p.mesh.rotation.x += dt * 3;
p.mesh.rotation.y += dt * 2;
}
// Remove if too far behind player
if (p.position.z < playerZ - 50) {
this.removeParticle(p, i, allParticles);
}
}
// Clean up empty systems
this.trailSystem.particles = this.trailSystem.particles.filter(p => p.life > 0);
this.debrisSystem.particles = this.debrisSystem.particles.filter(p => p.life > 0);
this.burstParticles = this.burstParticles.filter(p => p.life > 0);
}
removeParticle(p, idx, allParticles) {
if (p.mesh) {
this.scene.remove(p.mesh);
p.mesh.geometry.dispose?.();
if (p.mesh.material) {
p.mesh.material.dispose?.();
}
}
// Remove from the right system
const trailIdx = this.trailSystem.particles.indexOf(p);
if (trailIdx >= 0)
this.trailSystem.particles.splice(trailIdx, 1);
const debrisIdx = this.debrisSystem.particles.indexOf(p);
if (debrisIdx >= 0)
this.debrisSystem.particles.splice(debrisIdx, 1);
const burstIdx = this.burstParticles.indexOf(p);
if (burstIdx >= 0)
this.burstParticles.splice(burstIdx, 1);
}
reset() {
const all = [
...this.trailSystem.particles,
...this.debrisSystem.particles,
...this.burstParticles,
];
for (const p of all) {
if (p.mesh) {
this.scene.remove(p.mesh);
p.mesh.geometry.dispose?.();
if (p.mesh.material)
p.mesh.material.dispose?.();
}
}
this.trailSystem.particles = [];
this.debrisSystem.particles = [];
this.burstParticles = [];
}
}
exports.ParticleManager = ParticleManager;
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUD = void 0;
class HUD {
constructor() {
this.container = document.getElementById('app');
this.buildHUD();
}
buildHUD() {
// Create canvas container if it doesn't exist
if (!document.getElementById('canvas-container')) {
const canvasContainer = document.createElement('div');
canvasContainer.id = 'canvas-container';
canvasContainer.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;';
this.container.appendChild(canvasContainer);
}
// HUD overlay
const hud = document.createElement('div');
hud.id = 'hud';
hud.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:10;pointer-events:none;';
// Speed bar (left side)
this.speedBar = document.createElement('div');
this.speedBar.id = 'speed-bar';
this.speedBar.style.cssText = `
position:absolute;left:30px;top:50%;transform:translateY(-50%);
width:6px;height:200px;background:rgba(255,255,255,0.1);border-radius:3px;overflow:hidden;
`;
const speedFill = document.createElement('div');
speedFill.id = 'speed-fill';
speedFill.style.cssText = `
width:100%;height:50%;background:linear-gradient(to top,#ff4400,#ffaa00);
border-radius:3px;transition:height 0.1s;position:absolute;bottom:0;
`;
this.speedBar.appendChild(speedFill);
hud.appendChild(this.speedBar);
// Speed text
const speedText = document.createElement('div');
speedText.id = 'speed-text';
speedText.style.cssText = `
position:absolute;left:45px;top:50%;transform:translateY(-50%);
color:#ff8844;font-family:'Courier New',monospace;font-size:14px;font-weight:bold;
text-shadow:0 0 10px rgba(255,68,0,0.5);
`;
speedText.textContent = '0';
hud.appendChild(speedText);
// Multiplier
this.multiplierEl = document.createElement('div');
this.multiplierEl.id = 'multiplier';
this.multiplierEl.style.cssText = `
position:absolute;right:30px;top:20px;
color:#ffcc00;font-family:'Courier New',monospace;font-size:48px;font-weight:bold;
text-shadow:0 0 20px rgba(255,204,0,0.5);opacity:0.8;transition:all 0.2s;
`;
this.multiplierEl.textContent = 'x1';
hud.appendChild(this.multiplierEl);
// Score
this.scoreEl = document.createElement('div');
this.scoreEl.id = 'score';
this.scoreEl.style.cssText = `
position:absolute;right:30px;top:80px;
color:#ffffff;font-family:'Courier New',monospace;font-size:18px;font-weight:bold;
text-shadow:0 0 10px rgba(255,255,255,0.3);opacity:0.7;
`;
this.scoreEl.textContent = '0';
hud.appendChild(this.scoreEl);
// Energy bar (bottom center)
const energyContainer = document.createElement('div');
energyContainer.id = 'energy-container';
energyContainer.style.cssText = `
position:absolute;bottom:30px;left:50%;transform:translateX(-50%);
width:250px;height:8px;background:rgba(255,255,255,0.1);border-radius:4px;overflow:hidden;
`;
this.energyBar = document.createElement('div');
this.energyBar.id = 'energy-bar';
this.energyBar.style.cssText = `
width:100%;height:100%;background:linear-gradient(to right,#ff4400,#ffaa00);
border-radius:4px;transition:width 0.15s;
`;
energyContainer.appendChild(this.energyBar);
hud.appendChild(energyContainer);
// Energy label
const energyLabel = document.createElement('div');
energyLabel.id = 'energy-label';
energyLabel.style.cssText = `
position:absolute;bottom:45px;left:50%;transform:translateX(-50%);
color:#ff8844;font-family:'Courier New',monospace;font-size:11px;font-weight:bold;
text-transform:uppercase;letter-spacing:2px;opacity:0.6;
`;
energyLabel.textContent = 'ENERGY';
hud.appendChild(energyLabel);
// Turbo bar (below energy)
const turboContainer = document.createElement('div');
turboContainer.id = 'turbo-container';
turboContainer.style.cssText = `
position:absolute;bottom:15px;left:50%;transform:translateX(-50%);
width:200px;height:5px;background:rgba(255,255,255,0.05);border-radius:3px;overflow:hidden;
`;
this.turboBar = document.createElement('div');
this.turboBar.id = 'turbo-bar';
this.turboBar.style.cssText = `
width:0%;height:100%;background:linear-gradient(to right,#00aaff,#44ddff);
border-radius:3px;transition:width 0.1s;
`;
turboContainer.appendChild(this.turboBar);
hud.appendChild(turboContainer);
// Near-miss flash
this.nearMissFlash = document.createElement('div');
this.nearMissFlash.id = 'near-miss-flash';
this.nearMissFlash.style.cssText = `
position:absolute;top:0;left:0;width:100%;height:100%;
background:radial-gradient(ellipse at center,rgba(255,200,0,0.15),transparent 70%);
opacity:0;transition:opacity 0.1s;pointer-events:none;
`;
hud.appendChild(this.nearMissFlash);
this.container.appendChild(hud);
// Game Over screen
this.gameOverScreen = document.createElement('div');
this.gameOverScreen.id = 'game-over';
this.gameOverScreen.style.cssText = `
position:fixed;top:0;left:0;width:100%;height:100%;
background:rgba(10,5,20,0.92);z-index:100;display:none;
flex-direction:column;align-items:center;justify-content:center;
pointer-events:auto;
`;
this.gameOverScreen.innerHTML = `
<div style="color:#ff4400;font-family:'Courier New',monospace;font-size:64px;font-weight:bold;
text-shadow:0 0 40px rgba(255,68,0,0.5);margin-bottom:10px;">CRASHED</div>
<div style="color:#ffffff;font-family:'Courier New',monospace;font-size:16px;opacity:0.6;margin-bottom:40px;">
SIGNAL LOST
</div>
<div style="color:#ffcc00;font-family:'Courier New',monospace;font-size:42px;font-weight:bold;
text-shadow:0 0 20px rgba(255,204,0,0.4);margin-bottom:8px;" id="final-score">0</div>
<div style="color:#ffffff;font-family:'Courier New',monospace;font-size:14px;opacity:0.5;margin-bottom:40px;">
FINAL SCORE
</div>
<div style="display:flex;gap:40px;margin-bottom:50px;">
<div style="text-align:center;">
<div style="color:#44ddff;font-family:'Courier New',monospace;font-size:28px;font-weight:bold;" id="final-max-speed">0</div>
<div style="color:#ffffff;font-family:'Courier New',monospace;font-size:11px;opacity:0.4;margin-top:4px;">MAX SPEED</div>
</div>
<div style="text-align:center;">
<div style="color:#ff8844;font-family:'Courier New',monospace;font-size:28px;font-weight:bold;" id="final-max-mult">x1</div>
<div style="color:#ffffff;font-family:'Courier New',monospace;font-size:11px;opacity:0.4;margin-top:4px;">MAX MULTIPLIER</div>
</div>
</div>
<button id="restart-btn" style="
background:none;border:2px solid #ff4400;color:#ff4400;
font-family:'Courier New',monospace;font-size:18px;font-weight:bold;
padding:12px 40px;cursor:pointer;letter-spacing:3px;text-transform:uppercase;
transition:all 0.2s;pointer-events:auto;
">RESTART</button>
`;
this.container.appendChild(this.gameOverScreen);
// Start screen
this.startScreen = document.createElement('div');
this.startScreen.id = 'start-screen';
this.startScreen.style.cssText = `
position:fixed;top:0;left:0;width:100%;height:100%;
background:rgba(10,5,20,0.95);z-index:100;
display:flex;flex-direction:column;align-items:center;justify-content:center;
pointer-events:auto;
`;
this.startScreen.innerHTML = `
<div style="color:#ff4400;font-family:'Courier New',monospace;font-size:56px;font-weight:bold;
text-shadow:0 0 40px rgba(255,68,0,0.5);margin-bottom:8px;letter-spacing:4px;">CANYON RUN</div>
<div style="color:#ff8844;font-family:'Courier New',monospace;font-size:14px;opacity:0.5;margin-bottom:50px;
letter-spacing:6px;">HIGH-SPEED PROCEDURAL RACER</div>
<div style="color:#ffffff;font-family:'Courier New',monospace;font-size:13px;opacity:0.4;
text-align:center;line-height:2;margin-bottom:50px;">
WASD / ARROWS — STEER<br>
SPACE — BOOST<br>
COLLECT ORBS — RECHARGE ENERGY<br>
STAY ALIVE — SURVIVE
</div>
<button id="start-btn" style="
background:none;border:2px solid #ff4400;color:#ff4400;
font-family:'Courier New',monospace;font-size:20px;font-weight:bold;
padding:14px 50px;cursor:pointer;letter-spacing:4px;text-transform:uppercase;
transition:all 0.2s;pointer-events:auto;
">LAUNCH</button>
`;
this.container.appendChild(this.startScreen);
// References
this.finalScoreEl = document.getElementById('final-score');
this.finalMaxSpeedEl = document.getElementById('final-max-speed');
this.finalMaxMultEl = document.getElementById('final-max-mult');
this.restartBtn = document.getElementById('restart-btn');
}
updateSpeed(speed, maxSpeed) {
const fill = document.getElementById('speed-fill');
const pct = Math.min(1, speed / maxSpeed);
fill.style.height = (pct * 100) + '%';
const speedText = document.getElementById('speed-text');
speedText.textContent = String(Math.round(speed));
}
updateMultiplier(multiplier, pulse) {
this.multiplierEl.textContent = 'x' + multiplier.toFixed(1);
if (pulse) {
this.multiplierEl.style.transform = 'scale(1.2)';
this.multiplierEl.style.textShadow = '0 0 30px rgba(255,204,0,0.8)';
setTimeout(() => {
this.multiplierEl.style.transform = 'scale(1)';
this.multiplierEl.style.textShadow = '0 0 20px rgba(255,204,0,0.5)';
}, 200);
}
}
updateEnergy(energy, maxEnergy) {
const pct = Math.max(0, energy / maxEnergy);
this.energyBar.style.width = (pct * 100) + '%';
if (pct < 0.2) {
this.energyBar.style.background = 'linear-gradient(to right,#ff0000,#ff4400)';
}
else {
this.energyBar.style.background = 'linear-gradient(to right,#ff4400,#ffaa00)';
}
}
updateTurbo(turbo, maxTurbo, boosting) {
const pct = Math.max(0, turbo / maxTurbo);
this.turboBar.style.width = (pct * 100) + '%';
if (boosting) {
this.turboBar.style.background = 'linear-gradient(to right,#00ffff,#88ffff)';
this.turboBar.style.boxShadow = '0 0 10px rgba(0,255,255,0.5)';
}
else {
this.turboBar.style.background = 'linear-gradient(to right,#00aaff,#44ddff)';
this.turboBar.style.boxShadow = 'none';
}
}
updateScore(score) {
this.scoreEl.textContent = Math.round(score).toLocaleString();
}
flashNearMiss() {
this.nearMissFlash.style.opacity = '1';
setTimeout(() => {
this.nearMissFlash.style.opacity = '0';
}, 150);
}
showGameOver(score, maxSpeed, maxMult) {
this.gameOverScreen.style.display = 'flex';
this.finalScoreEl.textContent = Math.round(score).toLocaleString();
this.finalMaxSpeedEl.textContent = String(Math.round(maxSpeed));
this.finalMaxMultEl.textContent = 'x' + maxMult.toFixed(1);
}
hideGameOver() {
this.gameOverScreen.style.display = 'none';
}
showStartScreen() {
this.startScreen.style.display = 'flex';
}
hideStartScreen() {
this.startScreen.style.display = 'none';
}
onRestart(callback) {
this.restartBtn.onclick = () => {
callback();
};
// Hover effects
this.restartBtn.onmouseenter = () => {
this.restartBtn.style.background = 'rgba(255,68,0,0.15)';
this.restartBtn.style.boxShadow = '0 0 20px rgba(255,68,0,0.3)';
};
this.restartBtn.onmouseleave = () => {
this.restartBtn.style.background = 'none';
this.restartBtn.style.boxShadow = 'none';
};
}
onStart(callback) {
const startBtn = document.getElementById('start-btn');
if (startBtn) {
startBtn.onclick = () => {
callback();
};
startBtn.onmouseenter = () => {
startBtn.style.background = 'rgba(255,68,0,0.15)';
startBtn.style.boxShadow = '0 0 20px rgba(255,68,0,0.3)';
};
startBtn.onmouseleave = () => {
startBtn.style.background = 'none';
startBtn.style.boxShadow = 'none';
};
}
}
}
exports.HUD = HUD;
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
class Renderer {
constructor(config, cameraState) {
this.config = config;
this.cameraState = cameraState;
this.canvasContainer = document.getElementById('canvas-container');
// Scene
this._scene = new THREE.Scene();
this._scene.fog = new THREE.FogExp2(0x1a0a20, 0.0015);
// Camera
this._camera = new THREE.PerspectiveCamera(cameraState.fov, window.innerWidth / window.innerHeight, 0.5, 2000);
this._camera.position.set(0, cameraState.height, -cameraState.distance);
// Renderer
this._renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
});
this._renderer.setSize(window.innerWidth, window.innerHeight);
this._renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this._renderer.setClearColor(0x1a0a20, 1);
this._renderer.shadowMap.enabled = true;
this._renderer.shadowMap.type = THREE.BasicShadowMap;
this.canvasContainer.appendChild(this._renderer.domElement);
// Sun light (directional, golden hour)
this.sunLight = new THREE.DirectionalLight(0xffaa66, 1.8);
this.sunLight.position.set(50, 80, -100);
this.sunLight.castShadow = true;
this.sunLight.shadow.mapSize.width = 1024;
this.sunLight.shadow.mapSize.height = 1024;
this.sunLight.shadow.camera.near = 1;
this.sunLight.shadow.camera.far = 500;
this.sunLight.shadow.camera.left = -100;
this.sunLight.shadow.camera.right = 100;
this.sunLight.shadow.camera.top = 100;
this.sunLight.shadow.camera.bottom = -100;
this._scene.add(this.sunLight);
// Warm fill light from behind
const fillLight = new THREE.DirectionalLight(0xff6633, 0.5);
fillLight.position.set(0, 20, 50);
this._scene.add(fillLight);
// Ambient light (purple-ish)
this.ambientLight = new THREE.AmbientLight(0x553377, 0.6);
this._scene.add(this.ambientLight);
// Hemisphere light (sky/ground)
this.hemiLight = new THREE.HemisphereLight(0xff9966, 0x331144, 0.5);
this._scene.add(this.hemiLight);
// Sun glow sphere (visual sun in distance)
const sunGeo = new THREE.SphereGeometry(30, 12, 8);
const sunMat = new THREE.MeshBasicMaterial({
color: 0xff6622,
transparent: true,
opacity: 0.7,
});
const sunMesh = new THREE.Mesh(sunGeo, sunMat);
sunMesh.position.set(0, 40, -500);
this._scene.add(sunMesh);
// Sun glow sprite
const sunCanvas = document.createElement('canvas');
sunCanvas.width = 256;
sunCanvas.height = 256;
const sctx = sunCanvas.getContext('2d');
if (sctx) {
const sgrad = sctx.createRadialGradient(128, 128, 0, 128, 128, 128);
sgrad.addColorStop(0, 'rgba(255,150,50,0.9)');
sgrad.addColorStop(0.2, 'rgba(255,100,30,0.5)');
sgrad.addColorStop(0.5, 'rgba(200,50,20,0.2)');
sgrad.addColorStop(1, 'rgba(100,20,10,0)');
sctx.fillStyle = sgrad;
sctx.fillRect(0, 0, 256, 256);
}
const sunTex = new THREE.CanvasTexture(sunCanvas);
const sunGlowMat = new THREE.SpriteMaterial({
map: sunTex,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const sunGlow = new THREE.Sprite(sunGlowMat);
sunGlow.scale.set(200, 200, 1);
sunGlow.position.set(0, 40, -500);
this._scene.add(sunGlow);
// Handle resize
window.addEventListener('resize', () => {
this._camera.aspect = window.innerWidth / window.innerHeight;
this._camera.updateMatrixWorld();
this._renderer.setSize(window.innerWidth, window.innerHeight);
});
}
get scene() { return this._scene; }
get camera() { return this._camera; }
get renderer() { return this._renderer; }
updateCamera(playerPos, playerSpeed, dt) {
// Smooth FOV transition based on speed
const speedRatio = playerSpeed / (this.config.maxSpeed * 1.5);
this.cameraState.targetFov = 70 + speedRatio * 25;
this.cameraState.fov += (this.cameraState.targetFov - this.cameraState.fov) * Math.min(1, dt * 3);
this._camera.fov = this.cameraState.fov;
this._camera.updateMatrixWorld();
// Camera follows player with slight lag
const targetX = playerPos.x * 0.6;
const targetY = playerPos.y * 0.5 + this.cameraState.height;
const targetZ = playerPos.z - this.cameraState.distance;
this._camera.position.x += (targetX - this._camera.position.x) * Math.min(1, dt * 5);
this._camera.position.y += (targetY - this._camera.position.y) * Math.min(1, dt * 4);
this._camera.position.z += (targetZ - this._camera.position.z) * Math.min(1, dt * 8);
// Camera shake
if (this.cameraState.shake > 0.01) {
this._camera.position.x += (Math.random() - 0.5) * this.cameraState.shake;
this._camera.position.y += (Math.random() - 0.5) * this.cameraState.shake;
this.cameraState.shake *= this.cameraState.shakeDecay;
}
// Look ahead of player
const lookTarget = new THREE.Vector3(playerPos.x * 0.3, playerPos.y * 0.3, playerPos.z + 80);
this._camera.lookAt(lookTarget);
// Update sun to follow player
this.sunLight.position.set(playerPos.x + 50, 80, playerPos.z - 100);
}
triggerShake(intensity) {
this.cameraState.shake = Math.min(this.cameraState.shake + intensity, 3);
}
render() {
this._renderer.render(this._scene, this._camera);
}
dispose() {
this._renderer.dispose?.();
}
}
exports.Renderer = Renderer;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"638c8e4a-2243-4dfd-a66a-8c3dac457531","tokensIn":3582108,"tokensOut":51450,"tokensTotal":3633558,"cost":0,"turns":83,"toolCalls":82,"failedToolCalls":7,"timestamp":"2026-08-01T08:26:28.267Z"} -->