Canyon Run
Qwen 3.6 27B MTP GGUF · typescript
◈Preview unavailable — try playing the result
Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 64,441 bytes · SHA-256 5f6143d83934
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Canyon Racer — High-Speed Procedural Arcade</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; }
body { overflow: hidden; background: #000; font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; }
#app { position: relative; width: 100vw; height: 100vh; }
#gameCanvas { display: block; width: 100%; height: 100%; }
#hud {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10;
}
#multiplier {
position: absolute; top: 25px; left: 50%; transform: translateX(-50%);
font-size: 42px; font-weight: 900; color: #ff8c00;
text-shadow: 0 0 20px rgba(255, 140, 0, 0.6), 0 0 40px rgba(255, 140, 0, 0.3);
letter-spacing: 4px; transition: transform 0.1s;
font-variant-numeric: tabular-nums;
}
#energyContainer {
position: absolute; bottom: 35px; left: 50%; transform: translateX(-50%);
width: 280px; height: 6px; background: rgba(255,255,255,0.08);
border-radius: 3px; overflow: hidden;
box-shadow: 0 0 10px rgba(255, 100, 0, 0.15);
}
#energyBar {
width: 100%; height: 100%;
background: linear-gradient(90deg, #ff4444, #ff8c00, #ffcc00);
border-radius: 3px; transition: width 0.15s, background 0.3s;
box-shadow: 0 0 10px rgba(255, 140, 0, 0.5);
}
#speedContainer {
position: absolute; right: 25px; top: 50%; transform: translateY(-50%);
width: 5px; height: 160px; background: rgba(255,255,255,0.06);
border-radius: 3px; overflow: hidden;
}
#speedBar {
position: absolute; bottom: 0; width: 100%; height: 50%;
background: linear-gradient(to top, #4488ff, #88ccff);
border-radius: 3px;
box-shadow: 0 0 8px rgba(68, 136, 255, 0.5);
transition: height 0.1s;
}
#turboContainer {
position: absolute; right: 45px; top: 50%; transform: translateY(-50%);
width: 5px; height: 160px; background: rgba(255,255,255,0.04);
border-radius: 3px; overflow: hidden;
}
#turboBar {
position: absolute; bottom: 0; width: 100%; height: 0%;
background: linear-gradient(to top, #ff2200, #ff6600, #ffcc00);
border-radius: 3px;
box-shadow: 0 0 8px rgba(255, 68, 0, 0.5);
transition: height 0.1s;
}
#startScreen {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: radial-gradient(ellipse at center, rgba(20,10,30,0.95), rgba(5,5,15,0.98));
z-index: 100; color: #fff;
}
#startScreen h1 {
font-size: 60px; font-weight: 900; letter-spacing: 8px;
background: linear-gradient(135deg, #ff8c00, #ff4444, #cc44ff);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text; margin-bottom: 10px;
}
.subtitle { font-size: 16px; color: rgba(255,255,255,0.4); letter-spacing: 4px; margin-bottom: 40px; }
.controls-info { text-align: center; margin-bottom: 40px; }
.controls-info p { font-size: 15px; color: rgba(255,255,255,0.6); margin: 8px 0; letter-spacing: 1px; }
.tip { color: rgba(255, 140, 0, 0.8) !important; font-style: italic; font-size: 13px !important; }
#gameOver {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: rgba(5,5,15,0.92); z-index: 100; color: #fff;
}
#gameOver h1 { font-size: 52px; font-weight: 900; color: #ff4444; letter-spacing: 6px; margin-bottom: 30px; }
#finalScore, #finalSpeed, #finalMaxSpeed {
font-size: 24px; margin: 8px 0; color: rgba(255,255,255,0.7); letter-spacing: 2px;
}
button#startBtn, button#restartBtn {
margin-top: 30px; padding: 14px 55px;
font-size: 18px; font-weight: 900; letter-spacing: 4px;
border: 2px solid #ff8c00; background: transparent; color: #ff8c00;
cursor: pointer; pointer-events: all;
transition: all 0.2s;
}
button#startBtn:hover, button#restartBtn:hover {
background: #ff8c00; color: #000;
box-shadow: 0 0 30px rgba(255, 140, 0, 0.4);
}
</style>
</head>
<body>
<div id="app">
<canvas id="gameCanvas"></canvas>
<div id="hud">
<div id="energyContainer">
<div id="energyBar"></div>
</div>
<div id="multiplier">x1</div>
<div id="speedContainer">
<div id="speedBar"></div>
</div>
<div id="turboContainer">
<div id="turboBar"></div>
</div>
</div>
<div id="gameOver" style="display:none">
<h1>GAME OVER</h1>
<div id="finalScore">0</div>
<div id="finalSpeed">0</div>
<div id="finalMaxSpeed">0</div>
<button id="restartBtn">RESTART</button>
</div>
<div id="startScreen">
<h1>CANYON RACER</h1>
<p class="subtitle">High-Speed Procedural Arcade</p>
<div class="controls-info">
<p>W/S — Pitch Up/Down</p>
<p>A/D — Roll Left/Right</p>
<p>SPACE — Boost</p>
<p class="tip">Stay close to walls for near-miss multipliers!</p>
<p class="tip">Collect orbs to keep energy!</p>
</div>
<button id="startBtn">LAUNCH</button>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./gameState":"src/gameState.ts","./input":"src/input.ts","./player":"src/player.ts","./world":"src/world.ts","./audio":"src/audio.ts","./particles":"src/particles.ts","./collision":"src/collision.ts","./hud":"src/hud.ts","./renderer":"src/renderer.ts"},"src/player.ts":{"./gameState":"src/gameState.ts","./input":"src/input.ts"},"src/world.ts":{"./gameState":"src/gameState.ts"},"src/particles.ts":{"./gameState":"src/gameState.ts"},"src/collision.ts":{"./gameState":"src/gameState.ts","./player":"src/player.ts","./world":"src/world.ts","./audio":"src/audio.ts"},"src/hud.ts":{"./gameState":"src/gameState.ts"},"src/renderer.ts":{"./gameState":"src/gameState.ts","./player":"src/player.ts","./world":"src/world.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 });
const gameState_1 = require("./gameState");
const input_1 = require("./input");
const player_1 = require("./player");
const world_1 = require("./world");
const audio_1 = require("./audio");
const particles_1 = require("./particles");
const collision_1 = require("./collision");
const hud_1 = require("./hud");
const renderer_1 = require("./renderer");
// Game state
let state = (0, gameState_1.createInitialState)();
let input = (0, input_1.createInputState)();
let player = (0, player_1.createPlayerState)();
let chunks = new Map();
let audio = new audio_1.AudioEngine();
let particles = null;
let renderer = null;
// Timing
let lastTime = 0;
let deltaTime = 0;
let gameStartTime = 0;
let spawnTimer = 0;
let debrisTimer = 0;
let trailCounter = 0;
const canvas = document.getElementById('gameCanvas');
function init() {
// Setup input
(0, input_1.setupInput)(input);
// Create renderer
renderer = new renderer_1.GameRenderer(canvas);
// Create particles
particles = new particles_1.ParticleSystem(renderer.getScene());
// Generate initial chunks
for (let i = -2; i < gameState_1.CONFIG.chunksAhead; i++) {
const z = i * gameState_1.CONFIG.chunkLength;
const chunk = (0, world_1.generateChunk)(i, 1, z);
chunks.set(i, chunk);
renderer.getScene().add(chunk.group);
}
// Start screen
(0, hud_1.showStartScreen)(true);
// Handle resize
window.addEventListener('resize', () => {
if (renderer)
renderer.resize(window.innerWidth, window.innerHeight);
});
renderer.resize(window.innerWidth, window.innerHeight);
// Start button
const startBtn = document.getElementById('startBtn');
startBtn.addEventListener('click', startGame);
// Restart button
const restartBtn = document.getElementById('restartBtn');
restartBtn.addEventListener('click', restartGame);
// Start render loop
lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function startGame() {
(0, hud_1.showStartScreen)(false);
audio.init();
audio.resume();
audio.startEngine();
state.running = true;
gameStartTime = performance.now() / 1000;
}
function restartGame() {
// Clean up old chunks
chunks.forEach(chunk => {
renderer?.getScene().remove(chunk.group);
(0, world_1.disposeChunk)(chunk);
});
chunks.clear();
// Reset state
state = (0, gameState_1.createInitialState)();
player = (0, player_1.createPlayerState)();
spawnTimer = 0;
debrisTimer = 0;
trailCounter = 0;
// Re-generate chunks
for (let i = -2; i < gameState_1.CONFIG.chunksAhead; i++) {
const z = i * gameState_1.CONFIG.chunkLength;
const chunk = (0, world_1.generateChunk)(i, 1, z);
chunks.set(i, chunk);
renderer.getScene().add(chunk.group);
}
(0, hud_1.hideGameOver)();
audio.resume();
audio.startEngine();
state.running = true;
gameStartTime = performance.now() / 1000;
}
function gameLoop(timestamp) {
requestAnimationFrame(gameLoop);
deltaTime = Math.min((timestamp - lastTime) / 1000, 0.05); // cap dt
lastTime = timestamp;
if (!renderer || !particles)
return;
const time = timestamp / 1000;
if (state.running) {
update(time, timestamp);
}
// Render every frame
renderer.updateFov(state, time);
if (state.running) {
renderer.updateCamera(state, time);
renderer.animateOrbs(chunks, time);
renderer.updateSunPosition(state.distance);
}
renderer.render();
(0, hud_1.updateHUD)(state);
// Game over handling
if (state.gameOver) {
(0, hud_1.showGameOver)(state);
}
}
function update(time, timestamp) {
// === Difficulty scaling ===
state.difficulty = Math.min(gameState_1.CONFIG.maxDifficulty, 1 + state.score * gameState_1.CONFIG.speedScale);
// === Speed ===
let targetSpeed = gameState_1.CONFIG.playerSpeed + (state.difficulty - 1) * 0.08;
// === Turbo/Boost ===
if (state.boosting && state.turbo > 0) {
state.turbo -= gameState_1.CONFIG.turboDrain * deltaTime * 60;
if (state.turbo <= 0) {
state.turbo = 0;
state.boosting = false;
}
targetSpeed += gameState_1.CONFIG.turboBoost;
}
else {
// Auto-charge turbo
state.turbo = Math.min(gameState_1.CONFIG.turboMax, state.turbo + gameState_1.CONFIG.turboChargeRate * deltaTime * 60);
// Allow manual boost
if (input.boost && state.turbo >= 20) {
state.boosting = true;
audio.playBoost();
}
}
// Smooth speed transition
state.speed += (targetSpeed - state.speed) * 0.05;
state.maxSpeed = Math.max(state.maxSpeed, state.speed);
// === Player movement ===
(0, player_1.updatePlayer)(player, input, deltaTime, state.speed);
// === Distance tracking ===
state.distance += state.speed * deltaTime * 60;
// === Score ===
state.score += state.speed * deltaTime * 10 * state.multiplier;
// === Energy decay ===
state.energy -= gameState_1.CONFIG.energyDecay * deltaTime * 60;
if (state.energy <= 0) {
state.energy = 0;
// Gradual slowdown
targetSpeed *= 0.95;
}
// === Near-miss timer ===
if (state.nearMissTimer > 0) {
state.nearMissTimer -= deltaTime;
if (state.nearMissTimer <= 0) {
state.nearMissActive = false;
// Decay multiplier
state.multiplier = Math.max(1, state.multiplier - 1);
}
}
// === Camera shake decay ===
if (state.cameraShake > 0) {
state.cameraShake *= 0.95;
if (state.cameraShake < 0.001)
state.cameraShake = 0;
}
// === Update player visual ===
renderer.updatePlayerVisuals(player, state.boosting, time);
// === Particles ===
if (particles) {
trailCounter++;
if (trailCounter % 2 === 0) {
particles.addTrailPoint(0, player.x - 0.5, player.y, state.distance + 2);
particles.addTrailPoint(1, player.x + 0.5, player.y, state.distance + 2);
}
// Speed lines / debris
debrisTimer += deltaTime;
if (debrisTimer > 0.02 / state.speed) {
debrisTimer = 0;
particles.spawnDebris(player.x + (Math.random() - 0.5) * 40, player.y + (Math.random() - 0.5) * 30, state.distance - 10 - Math.random() * 20);
}
particles.update(deltaTime, state.speed, state.boosting);
}
// === Chunk management ===
const currentChunkIdx = Math.floor(state.distance / gameState_1.CONFIG.chunkLength);
// Generate new chunks ahead
let maxChunkIdx = chunks.size > 0 ? Math.max(...Array.from(chunks.keys())) : -1;
while (maxChunkIdx < currentChunkIdx + gameState_1.CONFIG.chunksAhead) {
maxChunkIdx++;
const z = maxChunkIdx * gameState_1.CONFIG.chunkLength;
const chunk = (0, world_1.generateChunk)(maxChunkIdx, state.difficulty, z);
chunks.set(maxChunkIdx, chunk);
renderer.getScene().add(chunk.group);
}
// Remove old chunks behind
chunks.forEach((chunk, idx) => {
if (idx < currentChunkIdx - gameState_1.CONFIG.chunksBehind) {
renderer.getScene().remove(chunk.group);
(0, world_1.disposeChunk)(chunk);
chunks.delete(idx);
}
});
// === Move chunks relative to player ===
// Instead of moving the player forward, we keep player at z=0
// and move all chunks backward
// Actually, let's track player distance and position chunks absolutely
// then move camera with player
chunks.forEach(chunk => {
const worldZ = state.distance - (chunk.chunkIndex * gameState_1.CONFIG.chunkLength);
chunk.group.position.z = worldZ;
});
// Position camera
const cam = renderer.getCamera();
cam.position.z = state.distance + 15;
// Update sun position to follow player
renderer.updateSunPosition(state.distance);
// Cycle dust particles
const dustPos = renderer.getScene().children.find(c => c.userData && c.userData.isDust);
if (dustPos) {
dustPos.position.z = state.distance;
// Slight drift
const dg = dustPos.geometry;
const d = dg.attributes.position.array;
for (let i = 0; i < d.length; i += 3) {
d[i + 2] += 0.3; // move toward player
if (d[i + 2] > 20)
d[i + 2] -= 420; // recycle
d[i] += Math.sin(time + i) * 0.02; // gentle drift
}
dg.attributes.position.needsUpdate = true;
}
// === Collision detection ===
(0, collision_1.checkCollisions)(state, player, chunks, state.distance, audio);
// === Audio update ===
audio.updateEngine(state.speed / gameState_1.CONFIG.boostSpeed, state.boosting);
}
// Start the game
init();
};
// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};
// ── module: src/gameState.ts ──
__mods["src/gameState.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
exports.createInitialState = createInitialState;
// Game configuration and state
exports.CONFIG = {
// Player
playerSpeed: 0.25,
playerMaxSpeed: 0.55,
boostSpeed: 0.95,
turnRate: 0.05,
bankAmount: 0.7,
pitchAmount: 0.035,
energyDecay: 0.02,
energyMax: 100,
energyRecharge: 30,
turboChargeRate: 0.08,
turboMax: 100,
turboDrain: 0.5,
turboBoost: 0.4,
// World
chunkLength: 200,
chunksAhead: 8,
chunksBehind: 2,
canyonWidth: 80,
canyonHeight: 50,
wallHeight: 35,
// Collision
playerRadius: 1.5,
nearMissDistance: 8,
nearMissDuration: 1.5,
// Difficulty
speedScale: 0.0001, // speed increase per score point
maxDifficulty: 3,
// Visual
fogNear: 50,
fogFar: 300,
baseFov: 75,
boostFov: 105,
};
function createInitialState() {
return {
running: false,
score: 0,
distance: 0,
speed: exports.CONFIG.playerSpeed,
maxSpeed: 0,
multiplier: 1,
energy: exports.CONFIG.energyMax,
turbo: 0,
boosting: false,
nearMissTimer: 0,
nearMissActive: false,
lastNearMissTime: 0,
difficulty: 1,
gameOver: false,
cameraShake: 0,
trailTimer: 0,
};
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInputState = createInputState;
exports.setupInput = setupInput;
function createInputState() {
return { up: false, down: false, left: false, right: false, boost: false };
}
function setupInput(input) {
const keyMap = {
'w': 'up', 'W': 'up', 'ArrowUp': 'up',
's': 'down', 'S': 'down', 'ArrowDown': 'down',
'a': 'left', 'A': 'left', 'ArrowLeft': 'left',
'd': 'right', 'D': 'right', 'ArrowRight': 'right',
' ': 'boost',
};
window.addEventListener('keydown', (e) => {
if (keyMap[e.key] !== undefined) {
input[keyMap[e.key]] = true;
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
if (keyMap[e.key] !== undefined) {
input[keyMap[e.key]] = false;
e.preventDefault();
}
});
}
};
// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createPlayerState = createPlayerState;
exports.updatePlayer = updatePlayer;
const gameState_1 = require("./gameState");
function createPlayerState() {
return { x: 0, y: 0, vx: 0, vy: 0, bankAngle: 0, pitchAngle: 0 };
}
function updatePlayer(player, input, dt, speed) {
const turnRate = gameState_1.CONFIG.turnRate * dt * 60;
const bankLerp = 0.08 * dt * 60;
const pitchLerp = 0.06 * dt * 60;
const damping = 0.92;
const maxLateralSpeed = gameState_1.CONFIG.playerMaxSpeed;
// Target angles based on input
let targetBank = 0;
let targetPitch = 0;
if (input.left)
targetBank = gameState_1.CONFIG.bankAmount;
if (input.right)
targetBank = -gameState_1.CONFIG.bankAmount;
if (input.up)
targetPitch = gameState_1.CONFIG.pitchAmount;
if (input.down)
targetPitch = -gameState_1.CONFIG.pitchAmount;
// Smooth bank and pitch
player.bankAngle += (targetBank - player.bankAngle) * bankLerp * 3;
player.pitchAngle += (targetPitch - player.pitchAngle) * pitchLerp * 3;
// Apply lateral forces with momentum
if (input.left)
player.vx += turnRate * speed * 20;
if (input.right)
player.vx -= turnRate * speed * 20;
if (input.up)
player.vy += turnRate * speed * 15;
if (input.down)
player.vy -= turnRate * speed * 15;
// Damping (weighty feel)
player.vx *= damping;
player.vy *= damping;
// Clamp
player.vx = THREE.MathUtils.clamp(player.vx, -maxLateralSpeed, maxLateralSpeed);
player.vy = THREE.MathUtils.clamp(player.vy, -maxLateralSpeed * 0.7, maxLateralSpeed * 0.7);
// Update position
player.x += player.vx * dt * 60;
player.y += player.vy * dt * 60;
// Bounds
const halfWidth = gameState_1.CONFIG.canyonWidth * 0.4;
const halfHeight = gameState_1.CONFIG.canyonHeight * 0.4;
player.x = THREE.MathUtils.clamp(player.x, -halfWidth, halfWidth);
player.y = THREE.MathUtils.clamp(player.y, -halfHeight, halfHeight);
}
};
// ── module: src/world.ts ──
__mods["src/world.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateChunk = generateChunk;
exports.disposeChunk = disposeChunk;
const gameState_1 = require("./gameState");
function seededRandom(seed) {
let s = Math.abs(seed) || 1;
return () => {
s = (s * 16807) % 2147483647;
return (s - 1) / 2147483646;
};
}
function generateChunk(chunkIndex, difficulty, globalZ) {
const rand = seededRandom(chunkIndex * 7919 + 42);
const group = new THREE.Group();
group.position.z = globalZ;
const chunkLen = gameState_1.CONFIG.chunkLength;
const canyonW = gameState_1.CONFIG.canyonWidth;
const canyonH = gameState_1.CONFIG.canyonHeight;
const wallH = gameState_1.CONFIG.wallHeight;
const narrowFactor = Math.max(0.3, 1 - (difficulty - 1) * 0.15);
const effectiveWidth = canyonW * narrowFactor;
// === Canyon walls - cyber sunset palette ===
const sunsetColors = [
new THREE.Color(0.65, 0.30, 0.12), // burnt orange
new THREE.Color(0.50, 0.22, 0.10), // deep rust
new THREE.Color(0.70, 0.35, 0.15), // golden orange
new THREE.Color(0.45, 0.20, 0.12), // warm brown
new THREE.Color(0.55, 0.25, 0.18), // terracotta
new THREE.Color(0.40, 0.18, 0.20), // purple-brown (shadowed)
];
const shadowColors = [
new THREE.Color(0.25, 0.12, 0.10), // deep shadow
new THREE.Color(0.20, 0.10, 0.15), // purple shadow
new THREE.Color(0.22, 0.10, 0.08), // dark brown
];
const wallSegs = 6;
const segLen = chunkLen / wallSegs;
for (let si = 0; si < wallSegs; si++) {
const zOff = si * segLen - chunkLen / 2 + segLen / 2;
const hVar = wallH * (0.8 + rand() * 0.5);
const thick = 5 + rand() * 8;
const xOffset = rand() * 3 - 1.5;
const yVar = rand() * 4 - 2;
const ci = Math.floor(rand() * sunsetColors.length);
const si_idx = Math.floor(rand() * shadowColors.length);
const matL = new THREE.MeshStandardMaterial({
flatShading: true, roughness: 0.9, metalness: 0.1,
color: sunsetColors[ci].clone(),
});
const lGeo = new THREE.BoxGeometry(thick, hVar, segLen + 0.5);
const lMesh = new THREE.Mesh(lGeo, matL);
lMesh.position.set(-effectiveWidth / 2 + 1 + xOffset, -wallH / 2 + hVar / 2 + yVar, zOff);
group.add(lMesh);
const matR = new THREE.MeshStandardMaterial({
flatShading: true, roughness: 0.9, metalness: 0.1,
color: shadowColors[si_idx].clone(),
});
const rGeo = new THREE.BoxGeometry(thick, hVar, segLen + 0.5);
const rMesh = new THREE.Mesh(rGeo, matR);
rMesh.position.set(effectiveWidth / 2 - 1 - xOffset, -wallH / 2 + hVar / 2 + yVar, zOff);
group.add(rMesh);
}
const wallLeft = group.children[0];
const wallRight = group.children[1];
// === Obstacles ===
const obstacles = [];
// Safe zone: first 4 chunks have no obstacles (player starts there)
if (chunkIndex >= 4) {
const numObs = Math.floor(rand() * 3) + Math.floor(difficulty);
const maxObs = Math.min(numObs, 8);
for (let i = 0; i < maxObs; i++) {
const zSpacing = chunkLen / (maxObs + 1);
const adjustedZ = zSpacing * (i + 0.5) + (rand() - 0.5) * 15;
if (rand() < 0.5) {
// Floating monolith
const monoW = 4 + rand() * 6;
const monoH = 10 + rand() * 15;
const x = (rand() - 0.5) * (effectiveWidth - monoW - 10);
const y = (rand() - 0.5) * canyonH * 0.6;
const geo = new THREE.BoxGeometry(monoW, monoH, 3);
const mat = new THREE.MeshStandardMaterial({
flatShading: true,
color: new THREE.Color(0.15, 0.1, 0.2 + rand() * 0.15),
roughness: 0.7,
metalness: 0.3,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, adjustedZ);
group.add(mesh);
obstacles.push({
mesh, type: 'monolith',
x, y, z: adjustedZ,
width: monoW, height: monoH, depth: 3,
active: true,
});
}
else {
// Squeeze wall
const gapW = Math.max(6, 18 - difficulty * 3);
const gapH = Math.max(5, 12 - difficulty * 1.5);
const gapX = (rand() - 0.5) * (effectiveWidth - gapW - 10);
const gapY = (rand() - 0.5) * canyonH * 0.5;
const barrierD = 2;
const mat = new THREE.MeshStandardMaterial({
flatShading: true,
color: new THREE.Color(0.2, 0.12, 0.25),
roughness: 0.8,
metalness: 0.2,
});
const leftW = (effectiveWidth + gapW) / 2 - gapX;
if (leftW > 1) {
const g = new THREE.BoxGeometry(leftW, canyonH * 0.8, barrierD);
const m = new THREE.Mesh(g, mat);
m.position.set(gapX - gapW / 2 - leftW / 2, gapY + canyonH * 0.4, adjustedZ);
group.add(m);
}
const rightW = (effectiveWidth + gapW) / 2 + gapX;
if (rightW > 1) {
const g = new THREE.BoxGeometry(rightW, canyonH * 0.8, barrierD);
const m = new THREE.Mesh(g, mat);
m.position.set(gapX + gapW / 2 + rightW / 2, gapY + canyonH * 0.4, adjustedZ);
group.add(m);
}
const aboveH = (canyonH * 0.8 - gapH) / 2;
if (aboveH > 0.5) {
const g = new THREE.BoxGeometry(gapW, aboveH, barrierD);
const m = new THREE.Mesh(g, mat);
m.position.set(gapX, gapY + canyonH * 0.4 + gapH / 2 + aboveH / 2, adjustedZ);
group.add(m);
}
if (aboveH > 0.5) {
const g = new THREE.BoxGeometry(gapW, aboveH, barrierD);
const m = new THREE.Mesh(g, mat);
m.position.set(gapX, gapY + canyonH * 0.4 - gapH / 2 - aboveH / 2, adjustedZ);
group.add(m);
}
const dummyGeo = new THREE.BoxGeometry(effectiveWidth, canyonH * 0.8, barrierD);
const dummyMat = new THREE.MeshBasicMaterial({ visible: false });
const dummyMesh = new THREE.Mesh(dummyGeo, dummyMat);
dummyMesh.position.set(gapX, gapY + canyonH * 0.4, adjustedZ);
obstacles.push({
mesh: dummyMesh, type: 'squeeze',
x: gapX, y: gapY + canyonH * 0.4, z: adjustedZ,
width: effectiveWidth, height: canyonH * 0.8, depth: barrierD,
active: true,
gapInfo: { gapX, gapY: gapY + canyonH * 0.4, gapW, gapH },
});
}
}
}
// === Energy orbs ===
const orbs = [];
const numOrbs = Math.floor(rand() * 3) + 2;
for (let i = 0; i < numOrbs; i++) {
const z = rand() * chunkLen;
let x = (rand() - 0.5) * effectiveWidth * 0.7;
let y = (rand() - 0.5) * canyonH * 0.5;
if (obstacles.length > 0 && rand() < 0.4) {
const obs = obstacles[Math.floor(rand() * obstacles.length)];
x = obs.x + (rand() - 0.5) * obs.width * 0.6;
y = obs.y + (rand() - 0.5) * obs.height * 0.6;
}
const geo = new THREE.IcosahedronGeometry(1.2, 0);
const mat = new THREE.MeshPhongMaterial({
color: new THREE.Color(1.0, 0.6, 0.1),
emissive: new THREE.Color(1.0, 0.4, 0.0),
emissiveIntensity: 2,
flatShading: true,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
group.add(mesh);
orbs.push({ mesh, x, y, z, collected: false, phase: rand() * Math.PI * 2 });
}
return {
chunkIndex,
zPosition: globalZ,
group,
wallLeft,
wallRight,
obstacles,
orbs,
removed: false,
};
}
function disposeChunk(chunk) {
if (chunk.removed)
return;
chunk.removed = true;
chunk.group.children.forEach(child => {
if (child instanceof THREE.Mesh) {
child.geometry.dispose();
if (Array.isArray(child.material)) {
child.material.forEach(m => m.dispose());
}
else {
child.material.dispose();
}
}
});
chunk.group = new THREE.Group();
}
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioEngine = void 0;
// Procedural sound synthesis using Web Audio API
class AudioEngine {
constructor() {
this.ctx = null;
this.masterGain = null;
this.engineOsc = null;
this.engineGain = null;
this.engineFilter = null;
}
init() {
try {
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.3;
this.masterGain.connect(this.ctx.destination);
}
catch {
console.warn('Web Audio not available');
}
}
resume() {
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
startEngine() {
if (!this.ctx || !this.masterGain)
return;
// Low engine hum - layered oscillators
this.engineOsc = this.ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 55;
this.engineFilter = this.ctx.createBiquadFilter();
this.engineFilter.type = 'lowpass';
this.engineFilter.frequency.value = 200;
this.engineFilter.Q.value = 2;
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0.12;
this.engineOsc.connect(this.engineFilter);
this.engineFilter.connect(this.engineGain);
this.engineGain.connect(this.masterGain);
this.engineOsc.start();
// Second harmonic
const osc2 = this.ctx.createOscillator();
osc2.type = 'sine';
osc2.frequency.value = 110;
const g2 = this.ctx.createGain();
g2.gain.value = 0.05;
osc2.connect(g2);
g2.connect(this.masterGain);
osc2.start();
this.engineOsc = this.engineOsc; // primary ref kept
}
updateEngine(speed, boosting) {
if (!this.ctx || !this.engineOsc || !this.engineFilter || !this.engineGain)
return;
const t = this.ctx.currentTime;
const freq = 45 + speed * 200;
const cutoff = boosting ? 800 : 150 + speed * 300;
const vol = boosting ? 0.2 : 0.08 + speed * 0.06;
this.engineOsc.frequency.setTargetAtTime(freq, t, 0.05);
this.engineFilter.frequency.setTargetAtTime(cutoff, t, 0.05);
this.engineGain.gain.setTargetAtTime(vol, t, 0.05);
}
stopEngine() {
if (this.engineOsc) {
try {
this.engineOsc.stop();
}
catch { }
this.engineOsc = null;
}
}
playCollect() {
if (!this.ctx || !this.masterGain)
return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, t);
osc.frequency.exponentialRampToValueAtTime(1600, t + 0.1);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.2);
}
playNearMiss() {
if (!this.ctx || !this.masterGain)
return;
const t = this.ctx.currentTime;
// 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) * (1 - i / bufferSize);
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(2000, t);
filter.frequency.exponentialRampToValueAtTime(400, t + 0.3);
filter.Q.value = 3;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
noise.start(t);
noise.stop(t + 0.3);
}
playCrash() {
if (!this.ctx || !this.masterGain)
return;
const t = this.ctx.currentTime;
// Noise burst
const bufferSize = this.ctx.sampleRate * 0.5;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 1500;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.5, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.5);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
noise.start(t);
noise.stop(t + 0.5);
// Low impact thud
const osc = this.ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(80, t);
osc.frequency.exponentialRampToValueAtTime(30, t + 0.3);
const oscGain = this.ctx.createGain();
oscGain.gain.setValueAtTime(0.4, t);
oscGain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
osc.connect(oscGain);
oscGain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.3);
}
playBoost() {
if (!this.ctx || !this.masterGain)
return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, t);
osc.frequency.exponentialRampToValueAtTime(1200, t + 0.15);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.15, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
const filter = this.ctx.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.value = 500;
osc.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.2);
}
destroy() {
this.stopEngine();
if (this.ctx) {
this.ctx.close();
}
}
}
exports.AudioEngine = AudioEngine;
};
// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParticleSystem = void 0;
// Particle system for trails and debris
class ParticleSystem {
constructor(scene) {
this.trailMax = 1200;
this.debrisMax = 400;
// Trail particles
this.trailPositions = new Float32Array(this.trailMax * 3);
this.trailOpacities = new Float32Array(this.trailMax);
this.trailHeads = [0, 0]; // two streams (left and right engine)
this.trailGeo = new THREE.BufferGeometry();
this.trailGeo.setAttribute('position', new THREE.BufferAttribute(this.trailPositions, 3));
this.trailMat = new THREE.PointsMaterial({
color: new THREE.Color(1, 0.5, 0.1),
size: 0.8,
transparent: true,
opacity: 0.7,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true,
});
this.trailPoints = new THREE.Points(this.trailGeo, this.trailMat);
scene.add(this.trailPoints);
// Debris particles (speed lines past camera)
this.debrisPositions = new Float32Array(this.debrisMax * 3);
this.debrisVelocities = new Array(this.debrisMax * 3).fill(0);
this.debrisLifetimes = new Array(this.debrisMax).fill(0);
this.debrisGeo = new THREE.BufferGeometry();
this.debrisGeo.setAttribute('position', new THREE.BufferAttribute(this.debrisPositions, 3));
this.debrisMat = new THREE.PointsMaterial({
color: new THREE.Color(0.8, 0.7, 0.5),
size: 0.3,
transparent: true,
opacity: 0.5,
depthWrite: false,
sizeAttenuation: true,
});
this.debrisPoints = new THREE.Points(this.debrisGeo, this.debrisMat);
scene.add(this.debrisPoints);
// Init debris positions offscreen
for (let i = 0; i < this.debrisMax; i++) {
this.debrisPositions[i * 3] = 0;
this.debrisPositions[i * 3 + 1] = 0;
this.debrisPositions[i * 3 + 2] = -9999;
this.debrisLifetimes[i] = 0;
}
}
addTrailPoint(stream, x, y, z) {
const head = this.trailHeads[stream];
const idx = head;
this.trailPositions[idx * 3] = x;
this.trailPositions[idx * 3 + 1] = y;
this.trailPositions[idx * 3 + 2] = z;
this.trailHeads[stream] = (head + 1) % this.trailMax;
}
spawnDebris(x, y, z) {
for (let i = 0; i < this.debrisMax; i++) {
if (this.debrisLifetimes[i] <= 0) {
this.debrisPositions[i * 3] = x + (Math.random() - 0.5) * 5;
this.debrisPositions[i * 3 + 1] = y + (Math.random() - 0.5) * 5;
this.debrisPositions[i * 3 + 2] = z;
this.debrisVelocities[i * 3] = (Math.random() - 0.5) * 0.3;
this.debrisVelocities[i * 3 + 1] = (Math.random() - 0.5) * 0.3;
this.debrisVelocities[i * 3 + 2] = -Math.random() * 2 - 1;
this.debrisLifetimes[i] = 1.0;
return;
}
}
}
update(dt, speed, boosting) {
// Decay debris
for (let i = 0; i < this.debrisMax; i++) {
if (this.debrisLifetimes[i] > 0) {
this.debrisLifetimes[i] -= dt * 2;
this.debrisPositions[i * 3] += this.debrisVelocities[i * 3];
this.debrisPositions[i * 3 + 1] += this.debrisVelocities[i * 3 + 1];
this.debrisPositions[i * 3 + 2] += this.debrisVelocities[i * 3 + 2];
if (this.debrisLifetimes[i] <= 0) {
this.debrisPositions[i * 3 + 2] = -9999;
}
}
}
this.trailGeo.attributes.position.needsUpdate = true;
this.debrisGeo.attributes.position.needsUpdate = true;
// Update trail color based on boost
if (boosting) {
this.trailMat.color.setRGB(1, 0.2, 0.1);
this.trailMat.size = 1.2;
this.trailMat.opacity = 0.9;
}
else {
this.trailMat.color.setRGB(1, 0.5, 0.1);
this.trailMat.size = 0.8;
this.trailMat.opacity = 0.7;
}
}
destroy() {
this.trailPoints.geometry.dispose();
this.trailMat.dispose();
this.debrisPoints.geometry.dispose();
this.debrisMat.dispose();
}
}
exports.ParticleSystem = ParticleSystem;
};
// ── module: src/collision.ts ──
__mods["src/collision.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkCollisions = checkCollisions;
const gameState_1 = require("./gameState");
function checkCollisions(state, player, chunks, playerZ, audio) {
const pRadius = gameState_1.CONFIG.playerRadius;
const nearMissDist = gameState_1.CONFIG.nearMissDistance;
// Check nearby chunks
const currentChunkIdx = Math.floor(playerZ / gameState_1.CONFIG.chunkLength);
for (let ci = currentChunkIdx - 1; ci <= currentChunkIdx + 2; ci++) {
const chunk = chunks.get(ci);
if (!chunk || chunk.removed)
continue;
const chunkZ = chunk.zPosition;
// Check obstacles
for (const obs of chunk.obstacles) {
if (!obs.active)
continue;
const obsWorldZ = chunkZ + obs.z;
const dz = Math.abs(playerZ - obsWorldZ);
// Only check if player is near the obstacle in Z
const zReach = (obs.depth / 2) + pRadius;
if (dz > zReach)
continue;
const dx = Math.abs(player.x - obs.x);
const dy = Math.abs(player.y - obs.y);
const hw = obs.width / 2 + pRadius;
const hh = obs.height / 2 + pRadius;
if (obs.type === 'squeeze' && obs.gapInfo) {
// For squeeze obstacles, check if player is within the gap
const gi = obs.gapInfo;
const inGapX = Math.abs(player.x - gi.gapX) < gi.gapW / 2;
const inGapY = Math.abs(player.y - gi.gapY) < gi.gapH / 2;
if (inGapX && inGapY) {
// Player is through the gap - check near-miss on edges
const distToGapEdgeX = gi.gapW / 2 - Math.abs(player.x - gi.gapX);
const distToGapEdgeY = gi.gapH / 2 - Math.abs(player.y - gi.gapY);
const edgeDist = Math.min(distToGapEdgeX, distToGapEdgeY);
if (edgeDist > 0 && edgeDist < nearMissDist) {
triggerNearMiss(state, edgeDist, audio);
}
continue;
}
// Outside gap - collision
if (dx < hw && dy < hh && dz < zReach) {
crash(state, audio);
return;
}
continue;
}
// Monolith collision
if (dx < hw && dy < hh && dz < zReach) {
crash(state, audio);
return;
}
// Near-miss for monoliths
const dist = Math.sqrt(Math.max(0, hw - dx) ** 2 + Math.max(0, hh - dy) ** 2 + Math.max(0, zReach - dz) ** 2);
if (dist < nearMissDist && dx < hw + nearMissDist && dy < hh + nearMissDist && dz < zReach + nearMissDist) {
triggerNearMiss(state, dist, audio);
}
}
// Check orbs
for (const orb of chunk.orbs) {
if (orb.collected)
continue;
const orbWorldZ = chunkZ + orb.z;
const dx = player.x - orb.x;
const dy = player.y - orb.y;
const dz = playerZ - orbWorldZ;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < pRadius + 2) {
orb.collected = true;
orb.mesh.visible = false;
state.energy = Math.min(gameState_1.CONFIG.energyMax, state.energy + gameState_1.CONFIG.energyRecharge);
state.turbo = Math.min(gameState_1.CONFIG.turboMax, state.turbo + 15);
audio.playCollect();
state.score += 50;
}
}
// Check canyon wall proximity
const narrowFactor = Math.max(0.3, 1 - (state.difficulty - 1) * 0.15);
const effectiveWidth = gameState_1.CONFIG.canyonWidth * narrowFactor;
const distToLeftWall = Math.abs(player.x) - (effectiveWidth / 2 - pRadius);
const distToRightWall = Math.abs(player.x) - (effectiveWidth / 2 - pRadius);
const wallDist = effectiveWidth / 2 - Math.abs(player.x) - pRadius;
if (wallDist < nearMissDist && wallDist > 0) {
// Only trigger if we're very close
if (wallDist < 3) {
triggerNearMiss(state, wallDist, audio);
}
}
}
}
function triggerNearMiss(state, distance, audio) {
const now = performance.now() / 1000;
if (now - state.lastNearMissTime < gameState_1.CONFIG.nearMissDuration)
return;
state.lastNearMissTime = now;
state.nearMissTimer = gameState_1.CONFIG.nearMissDuration;
state.nearMissActive = true;
const bonus = Math.floor((1 / (distance + 0.5)) * 100);
state.score += bonus;
// Build multiplier based on closeness
if (distance < 3) {
state.multiplier = Math.min(10, state.multiplier + 3);
}
else if (distance < 5) {
state.multiplier = Math.min(8, state.multiplier + 2);
}
else {
state.multiplier = Math.min(5, state.multiplier + 1);
}
state.turbo = Math.min(gameState_1.CONFIG.turboMax, state.turbo + 5);
state.cameraShake = 0.3;
audio.playNearMiss();
}
function crash(state, audio) {
state.gameOver = true;
state.running = false;
state.cameraShake = 1.0;
audio.playCrash();
}
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateHUD = updateHUD;
exports.showGameOver = showGameOver;
exports.hideGameOver = hideGameOver;
exports.showStartScreen = showStartScreen;
const gameState_1 = require("./gameState");
const elMultiplier = document.getElementById('multiplier');
const elEnergyBar = document.getElementById('energyBar');
const elSpeedBar = document.getElementById('speedBar');
const elTurboBar = document.getElementById('turboBar');
const elGameOver = document.getElementById('gameOver');
const elFinalScore = document.getElementById('finalScore');
const elFinalSpeed = document.getElementById('finalSpeed');
const elFinalMaxSpeed = document.getElementById('finalMaxSpeed');
const elStartScreen = document.getElementById('startScreen');
function updateHUD(state) {
// Energy bar
const energyPct = (state.energy / gameState_1.CONFIG.energyMax) * 100;
elEnergyBar.style.width = energyPct + '%';
if (energyPct < 20) {
elEnergyBar.style.background = 'linear-gradient(90deg, #ff0000, #ff4444)';
elEnergyBar.style.boxShadow = '0 0 15px rgba(255, 0, 0, 0.8)';
}
else if (energyPct < 50) {
elEnergyBar.style.background = 'linear-gradient(90deg, #ff4444, #ff8c00)';
elEnergyBar.style.boxShadow = '0 0 10px rgba(255, 100, 0, 0.5)';
}
else {
elEnergyBar.style.background = 'linear-gradient(90deg, #ff4444, #ff8c00, #ffcc00)';
elEnergyBar.style.boxShadow = '0 0 10px rgba(255, 140, 0, 0.5)';
}
// Speed bar
const maxPossibleSpeed = gameState_1.CONFIG.boostSpeed;
const speedPct = (state.speed / maxPossibleSpeed) * 100;
elSpeedBar.style.height = Math.min(100, speedPct * 1.5) + '%';
// Turbo bar
elTurboBar.style.height = (state.turbo / gameState_1.CONFIG.turboMax) * 100 + '%';
// Multiplier
elMultiplier.textContent = 'x' + state.multiplier;
if (state.nearMissActive) {
const scale = 1 + Math.sin(performance.now() / 50) * 0.15;
elMultiplier.style.transform = `translateX(-50%) scale(${scale})`;
elMultiplier.style.color = '#ffcc00';
elMultiplier.style.textShadow = '0 0 30px rgba(255, 200, 0, 0.8), 0 0 60px rgba(255, 140, 0, 0.5)';
}
else {
elMultiplier.style.transform = 'translateX(-50%) scale(1)';
elMultiplier.style.color = '#ff8c00';
elMultiplier.style.textShadow = '0 0 20px rgba(255, 140, 0, 0.6), 0 0 40px rgba(255, 140, 0, 0.3)';
}
// Score display in multiplier when no near miss
if (state.running) {
elMultiplier.textContent = Math.floor(state.score).toLocaleString();
}
}
function showGameOver(state) {
elGameOver.style.display = 'flex';
elFinalScore.textContent = 'SCORE: ' + Math.floor(state.score).toLocaleString();
elFinalSpeed.textContent = 'FINAL SPEED: ' + (state.speed * 1000).toFixed(0) + ' km/h';
elFinalMaxSpeed.textContent = 'MAX SPEED: ' + (state.maxSpeed * 1000).toFixed(0) + ' km/h';
}
function hideGameOver() {
elGameOver.style.display = 'none';
}
function showStartScreen(show) {
elStartScreen.style.display = show ? 'flex' : 'none';
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameRenderer = void 0;
const gameState_1 = require("./gameState");
class GameRenderer {
constructor(canvas) {
// Main renderer
this.renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: false,
});
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
this.renderer.toneMapping = 2; // ACESFilmic
this.renderer.toneMappingExposure = 1.2;
// Scene
this.scene = new THREE.Scene();
// Fog - cyber sunset gradient
this.scene.fog = new THREE.Fog(0x1a0a2e, gameState_1.CONFIG.fogNear * 0.7, gameState_1.CONFIG.fogFar * 1.2);
this.scene.background = new THREE.Color(0x1a0a2e);
// Sky dome with gradient colors
const skyGeo = new THREE.SphereGeometry(400, 16, 8);
const skyMat = new THREE.ShaderMaterial({
side: THREE.BackSide,
uniforms: {
topColor: { value: new THREE.Color(0.15, 0.05, 0.25) },
bottomColor: { value: new THREE.Color(0.6, 0.25, 0.1) },
offset: { value: 20 },
exponent: { value: 0.4 },
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPosition = 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 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}
`,
depthWrite: false,
});
const sky = new THREE.Mesh(skyGeo, skyMat);
sky.userData.isSky = true;
this.scene.add(sky);
// Camera
const aspect = window.innerWidth / window.innerHeight;
this.camera = new THREE.PerspectiveCamera(gameState_1.CONFIG.baseFov, aspect, 0.1, 500);
this.camera.position.set(0, 5, 15);
// Lighting - dramatic golden hour
const ambient = new THREE.AmbientLight(0x2a1540, 0.5);
this.scene.add(ambient);
// Sun (directional) - low angle, warm
const sun = new THREE.DirectionalLight(0xff8844, 2.0);
sun.position.set(30, 20, -50);
sun.castShadow = true;
sun.shadow.mapSize.width = 1024;
sun.shadow.mapSize.height = 1024;
sun.shadow.camera.near = 1;
sun.shadow.camera.far = 200;
sun.shadow.camera.left = -60;
sun.shadow.camera.right = 60;
sun.shadow.camera.top = 40;
sun.shadow.camera.bottom = -40;
this.scene.add(sun);
// Rim light (cool purple from behind)
const rim = new THREE.DirectionalLight(0x8844ff, 0.5);
rim.position.set(0, 10, 20);
this.scene.add(rim);
// Warm point light from below (ground bounce)
const bounceLight = new THREE.PointLight(0xff6633, 0.8, 100);
bounceLight.position.set(0, -gameState_1.CONFIG.canyonHeight / 2, -20);
bounceLight.userData.isBounce = true;
this.scene.add(bounceLight);
// Sun disc - glowing quad in the distance
const sunGeo = new THREE.PlaneGeometry(30, 30);
const sunMat = new THREE.MeshBasicMaterial({
color: new THREE.Color(1.0, 0.5, 0.1),
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending,
depthWrite: false,
side: THREE.DoubleSide,
});
const sunDisc = new THREE.Mesh(sunGeo, sunMat);
sunDisc.position.set(0, 8, -300);
sunDisc.userData.isSunDisc = true;
this.scene.add(sunDisc);
// Sun glow (larger, more transparent)
const glowGeo = new THREE.PlaneGeometry(80, 80);
const glowMat = new THREE.MeshBasicMaterial({
color: new THREE.Color(1.0, 0.3, 0.0),
transparent: true,
opacity: 0.15,
blending: THREE.AdditiveBlending,
depthWrite: false,
side: THREE.DoubleSide,
});
const sunGlow = new THREE.Mesh(glowGeo, glowMat);
sunGlow.position.set(0, 8, -300);
sunGlow.userData.isSunGlow = true;
this.scene.add(sunGlow);
// Floating dust particles for atmosphere
const dustCount = 300;
const dustGeo = new THREE.BufferGeometry();
const dustPos = new Float32Array(dustCount * 3);
for (let i = 0; i < dustCount; i++) {
dustPos[i * 3] = (Math.random() - 0.5) * 100;
dustPos[i * 3 + 1] = (Math.random() - 0.5) * 50;
dustPos[i * 3 + 2] = -Math.random() * 400;
}
dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos, 3));
const dustMat = new THREE.PointsMaterial({
color: new THREE.Color(0.8, 0.6, 0.3),
size: 0.2,
transparent: true,
opacity: 0.3,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true,
});
const dust = new THREE.Points(dustGeo, dustMat);
dust.userData.isDust = true;
this.scene.add(dust);
// Ground plane (dark canyon floor)
const groundGeo = new THREE.PlaneGeometry(500, 2000);
const groundMat = new THREE.MeshStandardMaterial({
color: new THREE.Color(0.08, 0.04, 0.12),
roughness: 0.95,
metalness: 0.05,
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -gameState_1.CONFIG.canyonHeight / 2 - 5;
ground.position.z = -500;
ground.userData.isGround = true;
this.scene.add(ground);
// Ceiling plane (dark, creating enclosed canyon feel)
const ceilGeo = new THREE.PlaneGeometry(200, 2000);
const ceilMat = new THREE.MeshStandardMaterial({
color: new THREE.Color(0.05, 0.02, 0.1),
roughness: 1,
metalness: 0,
});
const ceiling = new THREE.Mesh(ceilGeo, ceilMat);
ceiling.rotation.x = Math.PI / 2;
ceiling.position.y = gameState_1.CONFIG.canyonHeight / 2 + 10;
ceiling.position.z = -500;
ceiling.userData.isCeiling = true;
this.scene.add(ceiling);
// === Player Craft ===
this.playerGroup = this.createPlayerCraft();
this.scene.add(this.playerGroup);
}
createPlayerCraft() {
const group = new THREE.Group();
// Main body - sleek arrow shape
const bodyGeo = new THREE.ConeGeometry(0.6, 3, 6);
const bodyMat = new THREE.MeshStandardMaterial({
color: new THREE.Color(0.9, 0.85, 0.8),
flatShading: true,
metalness: 0.8,
roughness: 0.2,
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.rotation.x = Math.PI / 2;
group.add(body);
// Nose cone
const noseGeo = new THREE.ConeGeometry(0.3, 1.5, 4);
const noseMat = new THREE.MeshStandardMaterial({
color: new THREE.Color(1, 0.4, 0.1),
flatShading: true,
emissive: new THREE.Color(1, 0.2, 0.0),
emissiveIntensity: 0.5,
metalness: 0.5,
roughness: 0.3,
});
const nose = new THREE.Mesh(noseGeo, noseMat);
nose.rotation.x = Math.PI / 2;
nose.position.z = -2.0;
group.add(nose);
// Wings
const wingGeo = new THREE.BoxGeometry(4, 0.1, 1.5);
const wingMat = new THREE.MeshStandardMaterial({
color: new THREE.Color(0.15, 0.1, 0.25),
flatShading: true,
metalness: 0.7,
roughness: 0.3,
});
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.z = 0.2;
group.add(wings);
// Wing tips (glowing)
const tipGeo = new THREE.BoxGeometry(0.3, 0.15, 0.5);
const tipMat = new THREE.MeshPhongMaterial({
color: new THREE.Color(1, 0.3, 0.0),
emissive: new THREE.Color(1, 0.3, 0.0),
emissiveIntensity: 2,
});
const tipL = new THREE.Mesh(tipGeo, tipMat);
tipL.position.set(-2.1, 0, 0.2);
group.add(tipL);
const tipR = new THREE.Mesh(tipGeo, tipMat);
tipR.position.set(2.1, 0, 0.2);
group.add(tipR);
// Engine glow (two small cones at back)
const engineGeo = new THREE.ConeGeometry(0.3, 1.2, 4);
const engineMat = new THREE.MeshPhongMaterial({
color: new THREE.Color(1, 0.4, 0.0),
emissive: new THREE.Color(1, 0.4, 0.0),
emissiveIntensity: 3,
transparent: true,
opacity: 0.8,
});
const engineL = new THREE.Mesh(engineGeo, engineMat);
engineL.rotation.x = -Math.PI / 2;
engineL.position.set(-0.5, 0, 1.5);
group.add(engineL);
const engineR = new THREE.Mesh(engineGeo, engineMat);
engineR.rotation.x = -Math.PI / 2;
engineR.position.set(0.5, 0, 1.5);
group.add(engineR);
// Engine glow sphere
const glowGeo = new THREE.IcosahedronGeometry(0.5, 1);
const glowMat = new THREE.MeshBasicMaterial({
color: new THREE.Color(1, 0.5, 0.1),
transparent: true,
opacity: 0.3,
blending: THREE.AdditiveBlending,
});
const glowL = new THREE.Mesh(glowGeo, glowMat);
glowL.position.set(-0.5, 0, 1.8);
group.add(glowL);
const glowR = new THREE.Mesh(glowGeo, glowMat);
glowR.position.set(0.5, 0, 1.8);
group.add(glowR);
group.position.set(0, 0, 0);
return group;
}
getPlayerMesh() {
return this.playerGroup;
}
updatePlayerVisuals(player, boosting, time) {
this.playerGroup.position.x = player.x;
this.playerGroup.position.y = player.y;
// Banking (roll)
this.playerGroup.rotation.z = player.bankAngle;
// Pitch (nose up/down)
this.playerGroup.rotation.x = -player.pitchAngle * 2 - 0.15;
// Engine glow intensity based on boost
const intensity = boosting ? 5 : 2 + Math.sin(time * 10) * 0.5;
const engineColor = boosting ? new THREE.Color(1, 0.2, 0.0) : new THREE.Color(1, 0.5, 0.1);
this.playerGroup.children.forEach(child => {
if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshPhongMaterial) {
child.material.emissiveIntensity = intensity;
child.material.emissive = engineColor.clone();
}
});
}
updateCamera(state, time) {
// Camera follows player with smooth interpolation
const targetX = this.playerGroup.position.x * 0.5;
const targetY = this.playerGroup.position.y * 0.4 + 5;
this.camera.position.x += (targetX - this.camera.position.x) * 0.05;
this.camera.position.y += (targetY - this.camera.position.y) * 0.05;
// Move sky to follow camera
this.scene.children.forEach(child => {
if (child.userData && child.userData.isSky) {
child.position.copy(this.camera.position);
}
// Keep ground following camera in X and Z
if (child.userData && child.userData.isGround) {
child.position.x = this.camera.position.x * 0.5;
child.position.z = this.camera.position.z - 500;
}
if (child.userData && child.userData.isCeiling) {
child.position.x = this.camera.position.x * 0.3;
child.position.z = this.camera.position.z - 500;
}
if (child.userData && child.userData.isSunDisc) {
child.position.z = this.camera.position.z - 300;
}
if (child.userData && child.userData.isSunGlow) {
child.position.z = this.camera.position.z - 300;
}
if (child.userData && child.userData.isBounce) {
child.position.z = this.camera.position.z - 20;
}
});
// Camera shake
if (state.cameraShake > 0.01) {
const shake = state.cameraShake;
this.camera.position.x += Math.sin(time * 43) * shake;
this.camera.position.y += Math.cos(time * 37) * shake;
this.camera.position.z += Math.sin(time * 51) * shake * 0.5;
}
// Look ahead
const lookTarget = new THREE.Vector3(this.playerGroup.position.x * 0.3, this.playerGroup.position.y * 0.3, -30);
this.camera.lookAt(lookTarget);
}
updateFov(state, _time) {
let targetFov = gameState_1.CONFIG.baseFov;
// Speed-based FOV
const speedRatio = state.speed / gameState_1.CONFIG.boostSpeed;
targetFov += speedRatio * 10;
// Boost FOV
if (state.boosting) {
targetFov = gameState_1.CONFIG.boostFov;
}
// Near-miss FOV kick
if (state.nearMissActive) {
targetFov += 5;
}
this.camera.fov += (targetFov - this.camera.fov) * 0.05;
this.camera.updateProjectionMatrix();
}
animateOrbs(chunks, time) {
chunks.forEach(chunk => {
if (chunk.removed)
return;
chunk.orbs.forEach(orb => {
if (orb.collected)
return;
orb.mesh.rotation.x = time * 2 + orb.phase;
orb.mesh.rotation.y = time * 1.5 + orb.phase;
const pulse = 1 + Math.sin(time * 4 + orb.phase) * 0.2;
orb.mesh.scale.set(pulse, pulse, pulse);
});
});
}
updateSunPosition(playerZ) {
this.scene.children.forEach(child => {
if (child instanceof THREE.DirectionalLight) {
child.position.z = playerZ - 50;
}
});
}
render() {
this.renderer.render(this.scene, this.camera);
}
resize(w, h) {
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
}
getScene() {
return this.scene;
}
getCamera() {
return this.camera;
}
}
exports.GameRenderer = GameRenderer;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"8031a2b2-2731-4292-b7d4-725c9a680579","tokensIn":4007694,"tokensOut":58503,"tokensTotal":4066197,"cost":0,"turns":82,"toolCalls":91,"failedToolCalls":10,"timestamp":"2026-08-02T05:12:05.928Z"} -->