Canyon Run
thinkingcap qwen3.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 67,129 bytes · SHA-256 668f34af301c
<!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 Racing</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: #0a0215; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; }
#app { position: relative; width: 100%; height: 100%; }
canvas { display: block; }
/* ── css: hud ── */
#hud {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 10;
}
/* Speed bar - left side */
.speed-container {
position: absolute;
left: 24px;
top: 50%;
transform: translateY(-50%);
width: 6px;
height: 200px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
overflow: hidden;
}
.speed-bar {
position: absolute;
bottom: 0;
width: 100%;
height: 50%;
background: linear-gradient(to top, #ff6b35, #ffaa00);
border-radius: 3px;
transition: height 0.1s ease-out;
}
.speed-bar.boost {
background: linear-gradient(to top, #ff4500, #ffdd00);
box-shadow: 0 0 12px rgba(255, 69, 0, 0.8);
}
/* Multiplier */
#multiplier {
position: absolute;
top: 30px;
left: 50%;
transform: translateX(-50%);
font-size: 48px;
font-weight: 900;
color: #ffaa00;
text-shadow: 0 0 20px rgba(255, 170, 0, 0.6), 0 0 40px rgba(255, 107, 53, 0.3);
letter-spacing: 4px;
transition: transform 0.1s ease-out;
}
#multiplier.pulse {
animation: multiplierPulse 0.3s ease-out;
}
@keyframes multiplierPulse {
0% { transform: translateX(-50%) scale(1); }
50% { transform: translateX(-50%) scale(1.4); color: #ffdd00; }
100% { transform: translateX(-50%) scale(1); }
}
/* Score */
#score {
position: absolute;
top: 85px;
left: 50%;
transform: translateX(-50%);
font-size: 20px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
letter-spacing: 3px;
}
/* Energy bar - bottom center */
.energy-container {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
width: 200px;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
}
.energy-bar {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #ff6b35, #ffaa00);
border-radius: 4px;
transition: width 0.2s ease-out;
}
.energy-bar.critical {
background: linear-gradient(90deg, #ff0040, #ff4500);
animation: criticalPulse 0.5s infinite alternate;
}
@keyframes criticalPulse {
from { opacity: 1; }
to { opacity: 0.5; }
}
/* Turbo bar - below energy */
.turbo-container {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
width: 120px;
height: 4px;
background: rgba(255, 255, 255, 0.08);
border-radius: 2px;
overflow: hidden;
}
.turbo-bar {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #7b3fa0, #c084fc);
border-radius: 2px;
transition: width 0.15s ease-out;
}
.turbo-bar.active {
background: linear-gradient(90deg, #ff6b35, #ffdd00);
box-shadow: 0 0 8px rgba(255, 170, 0, 0.6);
}
/* Game Over Screen */
#game-over {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(10, 2, 21, 0.85);
pointer-events: auto;
}
.go-title {
font-size: 64px;
font-weight: 900;
color: #ff4500;
text-shadow: 0 0 30px rgba(255, 69, 0, 0.8);
letter-spacing: 12px;
margin-bottom: 30px;
}
.go-score {
font-size: 28px;
color: #ffaa00;
letter-spacing: 4px;
margin-bottom: 10px;
}
.go-speed {
font-size: 18px;
color: rgba(255, 255, 255, 0.6);
letter-spacing: 3px;
margin-bottom: 40px;
}
.restart-btn {
padding: 14px 48px;
font-size: 18px;
font-weight: 700;
color: #fff;
background: linear-gradient(135deg, #ff6b35, #7b3fa0);
border: none;
border-radius: 6px;
cursor: pointer;
letter-spacing: 4px;
text-transform: uppercase;
transition: transform 0.2s, box-shadow 0.2s;
pointer-events: auto;
}
.restart-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 20px rgba(255, 107, 53, 0.6);
}
/* Menu overlay */
#menu-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(10, 2, 21, 0.9);
z-index: 20;
}
#menu-overlay h1 {
font-size: 56px;
font-weight: 900;
color: #ffaa00;
text-shadow: 0 0 30px rgba(255, 170, 0, 0.6);
letter-spacing: 8px;
margin-bottom: 12px;
}
#menu-overlay .subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 6px;
text-transform: uppercase;
margin-bottom: 40px;
}
#menu-overlay .controls {
font-size: 13px;
color: rgba(255, 255, 255, 0.35);
letter-spacing: 2px;
line-height: 2;
text-align: center;
}
#menu-overlay .start-hint {
margin-top: 40px;
font-size: 18px;
color: #ff6b35;
letter-spacing: 4px;
animation: blink 1.5s infinite alternate;
}
@keyframes blink {
from { opacity: 0.4; }
to { opacity: 1; }
}
/* ── css: flash ── */
#flash-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 5;
}
/* ── css: vignette ── */
/* Vignette overlay for cinematic feel */
#app::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 4;
background: radial-gradient(ellipse at center, transparent 50%, rgba(10, 2, 21, 0.6) 100%);
}
/* ── css: turbo-text ── */
#turbo-text {
position: absolute;
bottom: 52px;
left: 50%;
transform: translateX(-50%);
font-size: 14px;
font-weight: 900;
color: #ff6b35;
letter-spacing: 8px;
opacity: 0;
transition: opacity 0.2s ease-out;
text-shadow: 0 0 10px rgba(255, 107, 53, 0.6);
}
</style>
</head>
<body>
<div id="app"><div id="menu-overlay"><h1>CANYON RACER</h1><div class="subtitle">High-Speed Procedural Racing</div><div class="controls">WASD / Arrow Keys — Steer<br>SPACE — Boost (when turbo charged)</div><div class="start-hint">PRESS SPACE TO START</div></div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts"},"src/player.ts":{"./config":"src/config.ts"},"src/world.ts":{"./config":"src/config.ts"},"src/collision.ts":{"./world":"src/world.ts","./player":"src/player.ts","./config":"src/config.ts"},"src/particles.ts":{"./config":"src/config.ts"},"src/hud.ts":{"./player":"src/player.ts","./config":"src/config.ts"},"src/game.ts":{"./player":"src/player.ts","./world":"src/world.ts","./collision":"src/collision.ts","./particles":"src/particles.ts","./hud":"src/hud.ts","./audio":"src/audio.ts","./config":"src/config.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 game_1 = require("./game");
let game = null;
function init() {
game = new game_1.Game();
// Restart handler
window.addEventListener('restart', () => {
if (game)
game.restart();
});
// Game loop
function loop() {
requestAnimationFrame(loop);
if (game) {
game.update();
game.render();
}
}
loop();
}
init();
};
// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
// Game configuration constants
exports.CONFIG = {
// Player physics
maxSpeed: 2.5,
boostMaxSpeed: 4.5,
acceleration: 0.03,
deceleration: 0.015,
turnSpeed: 0.04,
bankAngle: 0.6,
pitchAccelFactor: 0.02,
// Energy system
energyDecayRate: 0.8,
energyMax: 100,
orbRechargeAmount: 25,
// Turbo system
turboBuildRate: 0.3,
turboDrainRate: 1.5,
turboBoostMultiplier: 1.8,
turboMax: 100,
// Near-miss scoring
nearMissDistance: 4.0,
nearMissMultiplierGain: 2,
maxMultiplier: 10,
multiplierDecayRate: 0.5,
// Chunk system
chunkLength: 80,
renderDistance: 6,
canyonWidthBase: 30,
canyonWidthMin: 12,
// Difficulty scaling
difficultySpeedIncrease: 0.0001,
difficultyObstacleDensity: 0.00005,
// Camera
cameraDistance: 8,
cameraHeight: 3,
cameraLerpFactor: 0.08,
boostFov: 75,
normalFov: 60,
// Visuals
bloomIntensity: 1.2,
shakeAmountNearMiss: 0.3,
shakeAmountBoost: 0.15,
// Colors (cyber-sunset palette)
colors: {
skyTop: 0x1a0533,
skyBottom: 0xff6b35,
canyonWall: 0x4a2c6e,
canyonAccent: 0x7b3fa0,
monolith: 0x8b5cf6,
playerBody: 0xc0c0c0,
playerGlow: 0xff6b35,
orbColor: 0xffaa00,
ground: 0x2d1b4e,
fog: 0x1a0533,
},
};
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioSystem = void 0;
// Procedural sound synthesis using Web Audio API
class AudioSystem {
constructor() {
this.ctx = null;
this.engineOsc = null;
this.engineGain = null;
this.masterGain = null;
this.initialized = false;
}
init() {
if (this.initialized)
return;
try {
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.3;
this.masterGain.connect(this.ctx.destination);
// Engine hum - low oscillator
this.engineOsc = this.ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 60;
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0.15;
// Low-pass filter for engine rumble
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 200;
this.engineOsc.connect(filter);
filter.connect(this.engineGain);
this.engineGain.connect(this.masterGain);
this.engineOsc.start();
this.initialized = true;
}
catch (e) {
console.warn('Audio init failed:', e);
}
}
setEnginePitch(speedRatio) {
if (!this.engineOsc || !this.engineGain)
return;
const freq = 50 + speedRatio * 120;
this.engineOsc.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.1);
this.engineGain.gain.setTargetAtTime(0.1 + speedRatio * 0.15, this.ctx.currentTime, 0.1);
}
playCollect() {
if (!this.ctx || !this.masterGain)
return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1200, this.ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.3);
// Second harmonic ping
const osc2 = this.ctx.createOscillator();
const gain2 = this.ctx.createGain();
osc2.type = 'triangle';
osc2.frequency.setValueAtTime(1600, this.ctx.currentTime + 0.05);
osc2.frequency.exponentialRampToValueAtTime(2400, this.ctx.currentTime + 0.2);
gain2.gain.setValueAtTime(0.15, this.ctx.currentTime + 0.05);
gain2.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.35);
osc2.connect(gain2);
gain2.connect(this.masterGain);
osc2.start(this.ctx.currentTime + 0.05);
osc2.stop(this.ctx.currentTime + 0.35);
}
playNearMiss() {
if (!this.ctx || !this.masterGain)
return;
// Whoosh sound - filtered noise burst
const bufferSize = this.ctx.sampleRate * 0.4;
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) * 0.5;
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(600, this.ctx.currentTime);
filter.frequency.exponentialRampToValueAtTime(2000, this.ctx.currentTime + 0.15);
filter.Q.value = 2;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.4, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.35);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
noise.start();
}
playBoost() {
if (!this.ctx || !this.masterGain)
return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(400, this.ctx.currentTime + 0.3);
gain.gain.setValueAtTime(0.25, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.5);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.5);
}
playCrash() {
if (!this.ctx || !this.masterGain)
return;
// Explosion-like crash sound
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 / (this.ctx.sampleRate * 0.3));
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(2000, this.ctx.currentTime);
filter.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.6);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.8, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.7);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
noise.start();
// Low boom
const osc = this.ctx.createOscillator();
const g2 = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(80, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(30, this.ctx.currentTime + 0.5);
g2.gain.setValueAtTime(0.6, this.ctx.currentTime);
g2.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
osc.connect(g2);
g2.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.6);
}
stopEngine() {
if (this.engineOsc) {
this.engineOsc.stop();
this.engineOsc = null;
}
}
resume() {
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
}
exports.AudioSystem = AudioSystem;
};
// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlayerCraft = void 0;
const config_1 = require("./config");
class PlayerCraft {
constructor() {
this.position = new THREE.Vector3(0, 2, 0);
this.velocity = new THREE.Vector3(0, 0, 0);
this.speed = 0.5;
this.bankAngle = 0;
this.pitchAngle = 0;
// Energy & turbo
this.energy = config_1.CONFIG.energyMax;
this.turbo = 0;
this.isBoosting = false;
// Near-miss scoring
this.multiplier = 1;
this.nearMissTimer = 0;
// Trail points for visual effect
this.trailPositions = [];
this.mesh = new THREE.Group();
this.buildCraft();
}
buildCraft() {
// Main body - sleek elongated wedge
const bodyGeo = new THREE.ConeGeometry(0.35, 2.2, 6);
bodyGeo.rotateX(Math.PI / 2);
const bodyMat = new THREE.MeshPhongMaterial({
color: config_1.CONFIG.colors.playerBody,
flatShading: true,
shininess: 100,
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
this.mesh.add(body);
// Cockpit accent - smaller cone on top
const cockpitGeo = new THREE.ConeGeometry(0.15, 0.8, 6);
cockpitGeo.rotateX(Math.PI / 2);
const cockpitMat = new THREE.MeshPhongMaterial({
color: config_1.CONFIG.colors.playerGlow,
flatShading: true,
emissive: config_1.CONFIG.colors.playerGlow,
emissiveIntensity: 0.3,
});
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.y = 0.2;
this.mesh.add(cockpit);
// Wings - swept back delta wings
const wingShape = new THREE.CylinderGeometry(0.03, 1.4, 0.25, 4);
wingShape.rotateZ(Math.PI / 4);
const wingMat = new THREE.MeshPhongMaterial({
color: config_1.CONFIG.colors.playerBody,
flatShading: true,
shininess: 60,
});
const leftWing = new THREE.Mesh(wingShape, wingMat);
leftWing.position.set(-0.7, -0.15, 0.2);
this.mesh.add(leftWing);
const rightWing = new THREE.Mesh(wingShape.clone(), wingMat);
rightWing.position.set(0.7, -0.15, 0.2);
this.mesh.add(rightWing);
// Tail fins
const tailGeo = new THREE.ConeGeometry(0.3, 0.6, 4);
tailGeo.rotateX(Math.PI / 2);
const tailMat = new THREE.MeshPhongMaterial({ color: config_1.CONFIG.colors.playerBody, flatShading: true });
const leftTail = new THREE.Mesh(tailGeo, tailMat);
leftTail.position.set(-0.35, -0.1, 0.8);
this.mesh.add(leftTail);
const rightTail = new THREE.Mesh(tailGeo.clone(), tailMat);
rightTail.position.set(0.35, -0.1, 0.8);
this.mesh.add(rightTail);
// Engine glow sphere (index 6)
const engineGeo = new THREE.SphereGeometry(0.2, 8, 8);
const engineMat = new THREE.MeshBasicMaterial({
color: config_1.CONFIG.colors.playerGlow,
transparent: true,
opacity: 0.9,
});
const engine = new THREE.Mesh(engineGeo, engineMat);
engine.position.z = 1.1;
this.mesh.add(engine);
// Engine light
const engineLight = new THREE.PointLight(config_1.CONFIG.colors.playerGlow, 2, 8);
engineLight.position.z = 1.3;
this.mesh.add(engineLight);
}
update(dt, input) {
// Speed management
const maxSpd = this.isBoosting ? config_1.CONFIG.boostMaxSpeed : config_1.CONFIG.maxSpeed;
if (input.up) {
this.speed += config_1.CONFIG.acceleration * dt * 60;
}
else if (input.down) {
this.speed -= config_1.CONFIG.deceleration * dt * 60;
}
else {
// Natural cruise - slight acceleration toward max speed
if (this.speed < maxSpd * 0.7) {
this.speed += config_1.CONFIG.acceleration * 0.3 * dt * 60;
}
}
this.speed = Math.max(0.2, Math.min(this.speed, maxSpd));
// Pitch angle based on input (affects vertical movement and acceleration feel)
const targetPitch = input.up ? -0.3 : input.down ? 0.3 : 0;
this.pitchAngle += (targetPitch - this.pitchAngle) * 0.15;
// Lateral movement with momentum
let turnInput = 0;
if (input.left)
turnInput -= 1;
if (input.right)
turnInput += 1;
const lateralAccel = config_1.CONFIG.turnSpeed * this.speed * dt * 60;
this.velocity.x += turnInput * lateralAccel;
this.velocity.x *= 0.92; // damping
// Vertical movement
const verticalAccel = config_1.CONFIG.turnSpeed * 0.7 * this.speed * dt * 60;
if (input.up)
this.velocity.y += verticalAccel;
if (input.down)
this.velocity.y -= verticalAccel;
this.velocity.y *= 0.92;
// Apply velocity to position
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
// Clamp position within reasonable bounds
const canyonHalf = config_1.CONFIG.canyonWidthBase * 0.45;
this.position.x = Math.max(-canyonHalf, Math.min(canyonHalf, this.position.x));
this.position.y = Math.max(0.5, Math.min(12, this.position.y));
// Banking animation (roll during turns)
const targetBank = -turnInput * config_1.CONFIG.bankAngle;
this.bankAngle += (targetBank - this.bankAngle) * 0.12;
// Apply to mesh
this.mesh.position.copy(this.position);
this.mesh.rotation.z = this.bankAngle;
this.mesh.rotation.x = this.pitchAngle;
// Engine glow intensity based on speed
const engineMesh = this.mesh.children[3];
if (engineMesh) {
const scale = 0.8 + this.speed / config_1.CONFIG.maxSpeed * 0.6;
engineMesh.scale.set(scale, scale, scale);
}
// Energy decay
this.energy -= config_1.CONFIG.energyDecayRate * dt;
this.energy = Math.max(0, this.energy);
// Turbo management
if (this.isBoosting) {
this.turbo -= config_1.CONFIG.turboDrainRate * dt * 60;
if (this.turbo <= 0) {
this.turbo = 0;
this.isBoosting = false;
}
}
else {
// Build turbo at high speed on straightaways
if (Math.abs(turnInput) < 0.5 && this.speed > config_1.CONFIG.maxSpeed * 0.8) {
this.turbo += config_1.CONFIG.turboBuildRate * dt * 60;
}
}
this.turbo = Math.min(config_1.CONFIG.turboMax, this.turbo);
// Near-miss multiplier decay
if (this.nearMissTimer > 0) {
this.nearMissTimer -= dt;
if (this.nearMissTimer <= 0) {
this.multiplier = Math.max(1, this.multiplier - config_1.CONFIG.multiplierDecayRate * dt);
}
}
// Trail positions for particle effect
this.trailPositions.push(this.position.clone());
if (this.trailPositions.length > 30) {
this.trailPositions.shift();
}
}
triggerNearMiss() {
this.multiplier = Math.min(config_1.CONFIG.maxMultiplier, this.multiplier + config_1.CONFIG.nearMissMultiplierGain);
this.nearMissTimer = 2.0;
}
recharge(amount) {
this.energy = Math.min(config_1.CONFIG.energyMax, this.energy + amount);
}
activateBoost() {
if (this.turbo > 20 && !this.isBoosting) {
this.isBoosting = true;
return true;
}
return false;
}
getSpeedRatio() {
const maxSpd = this.isBoosting ? config_1.CONFIG.boostMaxSpeed : config_1.CONFIG.maxSpeed;
return this.speed / maxSpd;
}
}
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;
const config_1 = require("./config");
class WorldGenerator {
constructor(scene) {
this.scene = scene;
this.chunks = [];
this.nextChunkZ = 0;
this.seed = Math.random() * 10000;
this.difficultyScale = 1;
this.time = 0;
this._lastPlayerZ = 0;
}
setDifficulty(scale) {
this.difficultyScale = scale;
}
update(playerZ) {
// Generate new chunks ahead
while (this.nextChunkZ < playerZ + config_1.CONFIG.renderDistance * config_1.CONFIG.chunkLength) {
this.generateChunk(this.nextChunkZ);
this.nextChunkZ += config_1.CONFIG.chunkLength;
}
// Remove old chunks behind
const removeThreshold = playerZ - 2 * config_1.CONFIG.chunkLength;
while (this.chunks.length > 0 && this.chunks[0].z < removeThreshold) {
const chunk = this.chunks.shift();
this.scene.remove(chunk.group);
// Dispose geometries and materials
chunk.group.traverse((obj) => {
if (obj.geometry)
obj.geometry.dispose();
if (obj.material)
obj.material.dispose();
});
}
}
getObstaclesInRange(playerZ, range) {
const obstacles = [];
for (const chunk of this.chunks) {
if (chunk.z > playerZ - range && chunk.z < playerZ + range) {
obstacles.push(...chunk.obstacles);
}
}
return obstacles;
}
generateChunk(z) {
const group = new THREE.Group();
const obstacles = [];
// Pseudo-random based on chunk position
const rng = this.seededRng(Math.floor(z / config_1.CONFIG.chunkLength));
// Canyon width varies with difficulty
const widthVariation = (rng() - 0.5) * config_1.CONFIG.canyonWidthBase * 0.4;
const canyonWidth = Math.max(config_1.CONFIG.canyonWidthMin, config_1.CONFIG.canyonWidthBase + widthVariation);
// Generate canyon walls for this chunk
this.generateCanyonWalls(group, obstacles, z, canyonWidth, rng);
// Generate floating monoliths
const monolithCount = Math.floor(1 + rng() * 2 * this.difficultyScale);
for (let i = 0; i < monolithCount; i++) {
this.generateMonolith(group, obstacles, z, canyonWidth, rng);
}
// Generate tight squeezes at higher difficulty
if (this.difficultyScale > 1.3 && rng() > 0.5) {
this.generateSqueeze(group, obstacles, z, canyonWidth, rng);
}
// Ground plane for the chunk
const groundGeo = new THREE.PlaneGeometry(canyonWidth * 2, config_1.CONFIG.chunkLength);
const groundMat = new THREE.MeshLambertMaterial({ color: config_1.CONFIG.colors.ground, flatShading: true });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.set(0, -0.5, z + config_1.CONFIG.chunkLength / 2);
group.add(ground);
// Energy orbs scattered through the chunk
const orbCount = Math.floor(2 + rng() * 3);
for (let i = 0; i < orbCount; i++) {
this.generateOrb(group, z, canyonWidth, rng);
}
group.position.z = 0;
this.scene.add(group);
this.chunks.push({ group, z, obstacles });
}
generateCanyonWalls(group, obstacles, chunkZ, width, rng) {
const wallSegments = 5;
const segLength = config_1.CONFIG.chunkLength / wallSegments;
for (let side = -1; side <= 1; side += 2) {
let prevHeight = 6 + rng() * 8;
let prevWidthOffset = width * 0.45;
for (let i = 0; i < wallSegments; i++) {
const segZ = chunkZ + i * segLength + segLength / 2;
const height = 6 + rng() * 12;
const wOffset = Math.max(width * 0.3, prevWidthOffset + (rng() - 0.5) * width * 0.25);
// Main wall segment with jagged top
const wallGeo = new THREE.BoxGeometry(6 + rng() * 4, height, segLength * 0.85);
const shade = rng();
let color;
if (shade > 0.7) {
color = config_1.CONFIG.colors.canyonAccent;
}
else if (shade > 0.3) {
color = config_1.CONFIG.colors.canyonWall;
}
else {
color = 0x2d1b4e; // Darker variant
}
const wallMat = new THREE.MeshLambertMaterial({ color, flatShading: true });
const wall = new THREE.Mesh(wallGeo, wallMat);
const xPos = side * wOffset;
const yPos = height / 2 - 1;
wall.position.set(xPos, yPos, segZ);
group.add(wall);
// Add rocky protrusions on top of walls
if (rng() > 0.4) {
const spikeGeo = new THREE.ConeGeometry(1 + rng() * 2, 3 + rng() * 3, 5);
const spikeMat = new THREE.MeshLambertMaterial({ color: config_1.CONFIG.colors.canyonAccent, flatShading: true });
const spike = new THREE.Mesh(spikeGeo, spikeMat);
spike.position.set(xPos + (rng() - 0.5) * 3, height + 1, segZ);
group.add(spike);
}
obstacles.push({
mesh: wall,
center: new THREE.Vector3(xPos, yPos, segZ),
radius: Math.max(4, wOffset * 0.25) + 1,
type: 'wall',
});
prevHeight = height;
prevWidthOffset = wOffset;
}
}
}
generateMonolith(group, obstacles, chunkZ, width, rng) {
const zPos = chunkZ + rng() * config_1.CONFIG.chunkLength;
const xPos = (rng() - 0.5) * width * 0.6;
const yPos = 3 + rng() * 4;
const height = 2 + rng() * 4;
const radius = 0.8 + rng() * 1.2;
const monoGeo = new THREE.CylinderGeometry(radius * 0.5, radius, height, 6);
const monoMat = new THREE.MeshPhongMaterial({
color: config_1.CONFIG.colors.monolith,
flatShading: true,
emissive: config_1.CONFIG.colors.monolith,
emissiveIntensity: 0.15,
});
const monolith = new THREE.Mesh(monoGeo, monoMat);
monolith.position.set(xPos, yPos, zPos);
// Slight random rotation for visual interest
monolith.rotation.y = rng() * Math.PI;
group.add(monolith);
obstacles.push({
mesh: monolith,
center: new THREE.Vector3(xPos, yPos, zPos),
radius: radius + 0.5,
type: 'monolith',
});
}
generateSqueeze(group, obstacles, chunkZ, width, rng) {
// Create a narrow gap - two walls close together with a passage through
const zPos = chunkZ + config_1.CONFIG.chunkLength * 0.5;
const gapX = (rng() - 0.5) * width * 0.3;
const gapWidth = 4;
// Left wall of squeeze
const leftGeo = new THREE.BoxGeometry(2, 10, 6);
const squeezeMat = new THREE.MeshLambertMaterial({ color: config_1.CONFIG.colors.canyonAccent, flatShading: true });
const leftWall = new THREE.Mesh(leftGeo, squeezeMat);
leftWall.position.set(gapX - gapWidth / 2, 4, zPos);
group.add(leftWall);
// Right wall of squeeze
const rightWall = new THREE.Mesh(leftGeo.clone(), squeezeMat);
rightWall.position.set(gapX + gapWidth / 2, 4, zPos);
group.add(rightWall);
obstacles.push({
mesh: leftWall,
center: new THREE.Vector3(gapX - gapWidth / 2, 4, zPos),
radius: 2.5,
type: 'squeeze',
});
obstacles.push({
mesh: rightWall,
center: new THREE.Vector3(gapX + gapWidth / 2, 4, zPos),
radius: 2.5,
type: 'squeeze',
});
// Top barrier (ceiling)
const ceilGeo = new THREE.BoxGeometry(gapWidth + 1, 1, 6);
const ceiling = new THREE.Mesh(ceilGeo, squeezeMat);
ceiling.position.set(gapX, 9.5, zPos);
group.add(ceiling);
obstacles.push({
mesh: ceiling,
center: new THREE.Vector3(gapX, 9.5, zPos),
radius: 1.5,
type: 'squeeze',
});
}
generateOrb(group, chunkZ, width, rng) {
const zPos = chunkZ + rng() * config_1.CONFIG.chunkLength;
const xPos = (rng() - 0.5) * width * 0.5;
const yPos = 1.5 + rng() * 5;
// Glowing orb
const orbGeo = new THREE.SphereGeometry(0.4, 8, 8);
const orbMat = new THREE.MeshBasicMaterial({
color: config_1.CONFIG.colors.orbColor,
transparent: true,
opacity: 0.9,
});
const orb = new THREE.Mesh(orbGeo, orbMat);
orb.position.set(xPos, yPos, zPos);
orb.userData.isOrb = true;
group.add(orb);
// Outer glow ring
const glowGeo = new THREE.SphereGeometry(0.7, 8, 8);
const glowMat = new THREE.MeshBasicMaterial({
color: config_1.CONFIG.colors.orbColor,
transparent: true,
opacity: 0.3,
});
const glow = new THREE.Mesh(glowGeo, glowMat);
orb.add(glow);
// Point light for the orb
const orbLight = new THREE.PointLight(config_1.CONFIG.colors.orbColor, 1, 5);
orb.add(orbLight);
}
collectOrbs(playerZ) {
let collected = false;
const collectRange = 2.0;
for (const chunk of this.chunks) {
if (Math.abs(chunk.z - playerZ) > config_1.CONFIG.chunkLength * 1.5)
continue;
chunk.group.traverse((obj) => {
if (!obj.userData.isOrb || !obj.visible)
return;
const dist = obj.position.distanceTo(new THREE.Vector3(0, 0, playerZ));
// We need to compare with actual player position - this is handled in collision module
});
}
return collected;
}
seededRng(seed) {
let s = seed;
return () => {
s = (s * 16807 + 0) % 2147483647;
return (s - 1) / 2147483646;
};
}
animate(dt) {
this.time += dt;
for (const chunk of this.chunks) {
if (Math.abs(chunk.z - this._lastPlayerZ) > config_1.CONFIG.chunkLength * 2)
continue;
chunk.group.traverse((obj) => {
if (!obj.userData?.isOrb || !obj.visible)
return;
// Pulse effect
const pulse = 1 + Math.sin(this.time * 4 + obj.position.z * 0.5) * 0.2;
obj.scale.set(pulse, pulse, pulse);
// Gentle bobbing
obj.position.y += Math.sin(this.time * 2 + obj.position.x) * 0.003;
});
}
}
dispose() {
for (const chunk of this.chunks) {
this.scene.remove(chunk.group);
chunk.group.traverse((obj) => {
if (obj.geometry)
obj.geometry.dispose();
if (obj.material)
obj.material.dispose();
});
}
this.chunks = [];
}
}
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;
const config_1 = require("./config");
class CollisionSystem {
constructor() {
this.playerRadius = 0.8;
}
checkCollisions(player, obstacles) {
let crashed = false;
let nearMiss = false;
for (const obs of obstacles) {
const dx = player.position.x - obs.center.x;
const dy = player.position.y - obs.center.y;
const dz = player.position.z - obs.center.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
// Collision check
if (dist < this.playerRadius + obs.radius * 0.5) {
crashed = true;
break;
}
// Near-miss detection
if (dist < config_1.CONFIG.nearMissDistance + obs.radius * 0.6 && dist > this.playerRadius + obs.radius * 0.5) {
nearMiss = true;
}
}
return { crashed, nearMiss };
}
checkOrbCollection(player, scene) {
let collected = false;
const collectRadius = 2.0;
scene.traverse((obj) => {
if (!obj.userData?.isOrb || !obj.visible)
return;
const dx = player.position.x - obj.position.x;
const dy = player.position.y - obj.position.y;
const dz = player.position.z - obj.position.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < collectRadius) {
// Collect the orb
obj.visible = false;
collected = true;
}
});
return collected;
}
}
exports.CollisionSystem = CollisionSystem;
};
// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParticleSystem = void 0;
const config_1 = require("./config");
class ParticleSystem {
constructor(scene) {
this.scene = scene;
this.trailPoints = null;
this.debrisPoints = null;
this.trailPositions = [];
this.debrisParticles = [];
}
init() {
// Trail particles - behind the player craft
const trailGeo = new THREE.BufferGeometry();
const maxTrail = 200;
const positions = new Float32Array(maxTrail * 3);
const colors = new Float32Array(maxTrail * 3);
const sizes = new Float32Array(maxTrail);
trailGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
trailGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
trailGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const trailMat = new THREE.PointsMaterial({
color: config_1.CONFIG.colors.playerGlow,
size: 0.3,
transparent: true,
opacity: 0.7,
blending: THREE.AdditiveBlending,
depthWrite: false,
vertexColors: true,
sizeAttenuation: true,
});
this.trailPoints = new THREE.Points(trailGeo, trailMat);
this.scene.add(this.trailPoints);
// Debris particles - speed lines flying past
const debrisGeo = new THREE.BufferGeometry();
const maxDebris = 300;
const dPositions = new Float32Array(maxDebris * 3);
const dColors = new Float32Array(maxDebris * 3);
debrisGeo.setAttribute('position', new THREE.BufferAttribute(dPositions, 3));
debrisGeo.setAttribute('color', new THREE.BufferAttribute(dColors, 3));
const debrisMat = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.15,
transparent: true,
opacity: 0.4,
blending: THREE.AdditiveBlending,
depthWrite: false,
vertexColors: true,
sizeAttenuation: true,
});
this.debrisPoints = new THREE.Points(debrisGeo, debrisMat);
this.scene.add(this.debrisPoints);
}
addTrailPoint(x, y, z) {
this.trailPositions.push({ x, y, z, life: 1.0 });
if (this.trailPositions.length > 200) {
this.trailPositions.shift();
}
}
spawnDebris(playerZ, speedRatio) {
const count = Math.floor(speedRatio * 5);
for (let i = 0; i < count; i++) {
this.debrisParticles.push({
x: (Math.random() - 0.5) * 20,
y: Math.random() * 10 + 1,
z: playerZ + 10 + Math.random() * 30,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.2,
vz: -(speedRatio * 8 + 5),
life: 1.0,
});
}
if (this.debrisParticles.length > 300) {
this.debrisParticles.splice(0, count);
}
}
update(dt, playerZ, speedRatio) {
// Update trail particles
for (let i = this.trailPositions.length - 1; i >= 0; i--) {
const p = this.trailPositions[i];
p.life -= dt * 2.5;
if (p.life <= 0) {
this.trailPositions.splice(i, 1);
}
}
// Update debris particles
for (let i = this.debrisParticles.length - 1; i >= 0; i--) {
const p = this.debrisParticles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.z += p.vz * dt;
p.life -= dt * 2;
if (p.life <= 0) {
this.debrisParticles.splice(i, 1);
}
}
// Spawn debris based on speed
if (Math.random() < speedRatio * 0.8) {
this.spawnDebris(playerZ, speedRatio);
}
this.renderTrail();
this.renderDebris();
}
renderTrail() {
if (!this.trailPoints)
return;
const geo = this.trailPoints.geometry;
const posAttr = geo.attributes.position;
const colAttr = geo.attributes.color;
const maxCount = posAttr.array.length / 3;
for (let i = 0; i < maxCount; i++) {
if (i < this.trailPositions.length) {
const p = this.trailPositions[i];
posAttr.array[i * 3] = p.x;
posAttr.array[i * 3 + 1] = p.y;
posAttr.array[i * 3 + 2] = p.z;
// Color fades from orange to purple as trail ages
const r = 1.0 - p.life * 0.5;
const g = p.life * 0.4;
const b = (1 - p.life) * 0.6;
colAttr.array[i * 3] = r;
colAttr.array[i * 3 + 1] = g;
colAttr.array[i * 3 + 2] = b;
}
else {
posAttr.array[i * 3] = 0;
posAttr.array[i * 3 + 1] = -100;
posAttr.array[i * 3 + 2] = 0;
}
}
posAttr.needsUpdate = true;
colAttr.needsUpdate = true;
}
renderDebris() {
if (!this.debrisPoints)
return;
const geo = this.debrisPoints.geometry;
const posAttr = geo.attributes.position;
const colAttr = geo.attributes.color;
const maxCount = posAttr.array.length / 3;
for (let i = 0; i < maxCount; i++) {
if (i < this.debrisParticles.length) {
const p = this.debrisParticles[i];
posAttr.array[i * 3] = p.x;
posAttr.array[i * 3 + 1] = p.y;
posAttr.array[i * 3 + 2] = p.z;
colAttr.array[i * 3] = p.life;
colAttr.array[i * 3 + 1] = p.life * 0.8;
colAttr.array[i * 3 + 2] = p.life * 0.6;
}
else {
posAttr.array[i * 3] = 0;
posAttr.array[i * 3 + 1] = -100;
posAttr.array[i * 3 + 2] = 0;
}
}
posAttr.needsUpdate = true;
colAttr.needsUpdate = true;
}
dispose() {
if (this.trailPoints) {
this.scene.remove(this.trailPoints);
this.trailPoints.geometry.dispose();
this.trailPoints.material.dispose();
}
if (this.debrisPoints) {
this.scene.remove(this.debrisPoints);
this.debrisPoints.geometry.dispose();
this.debrisPoints.material.dispose();
}
}
}
exports.ParticleSystem = ParticleSystem;
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUD = void 0;
const config_1 = require("./config");
class HUD {
constructor() {
this.finalScoreEl = null;
this.maxSpeedEl = null;
this.restartBtn = null;
this.container = document.createElement('div');
this.container.id = 'hud';
this.buildHUD();
}
buildHUD() {
// Speed bar (left side)
const speedContainer = document.createElement('div');
speedContainer.className = 'speed-container';
this.speedBar = document.createElement('div');
this.speedBar.className = 'speed-bar';
speedContainer.appendChild(this.speedBar);
this.container.appendChild(speedContainer);
// Multiplier (center top)
this.multiplierEl = document.createElement('div');
this.multiplierEl.id = 'multiplier';
this.multiplierEl.textContent = 'x1';
this.container.appendChild(this.multiplierEl);
// Score (top center, below multiplier)
this.scoreEl = document.createElement('div');
this.scoreEl.id = 'score';
this.scoreEl.textContent = '0';
this.container.appendChild(this.scoreEl);
// Energy bar (bottom center)
const energyContainer = document.createElement('div');
energyContainer.className = 'energy-container';
this.energyBar = document.createElement('div');
this.energyBar.className = 'energy-bar';
energyContainer.appendChild(this.energyBar);
this.container.appendChild(energyContainer);
// Turbo bar (bottom, below energy)
const turboContainer = document.createElement('div');
turboContainer.className = 'turbo-container';
this.turboBar = document.createElement('div');
this.turboBar.className = 'turbo-bar';
turboContainer.appendChild(this.turboBar);
// Turbo text indicator (hidden by default)
this.turboText = document.createElement('div');
this.turboText.id = 'turbo-text';
this.turboText.textContent = 'TURBO';
this.container.appendChild(this.turboText);
this.container.appendChild(turboContainer);
// Game over screen (hidden by default)
this.gameOverScreen = document.createElement('div');
this.gameOverScreen.id = 'game-over';
this.gameOverScreen.style.display = 'none';
const goTitle = document.createElement('h1');
goTitle.textContent = 'CRASHED';
goTitle.className = 'go-title';
this.gameOverScreen.appendChild(goTitle);
this.finalScoreEl = document.createElement('div');
this.finalScoreEl.className = 'go-score';
this.finalScoreEl.textContent = '';
this.gameOverScreen.appendChild(this.finalScoreEl);
this.maxSpeedEl = document.createElement('div');
this.maxSpeedEl.className = 'go-speed';
this.maxSpeedEl.textContent = '';
this.gameOverScreen.appendChild(this.maxSpeedEl);
this.restartBtn = document.createElement('button');
this.restartBtn.className = 'restart-btn';
this.restartBtn.textContent = 'RESTART';
this.restartBtn.addEventListener('click', () => {
window.dispatchEvent(new CustomEvent('restart'));
});
this.gameOverScreen.appendChild(this.restartBtn);
this.container.appendChild(this.gameOverScreen);
}
getElement() {
return this.container;
}
update(player, score) {
// Speed bar
const speedRatio = player.getSpeedRatio();
this.speedBar.style.height = `${speedRatio * 100}%`;
if (player.isBoosting) {
this.speedBar.classList.add('boost');
}
else {
this.speedBar.classList.remove('boost');
}
// Multiplier
const multDisplay = Math.max(1, Math.floor(player.multiplier));
this.multiplierEl.textContent = `x${multDisplay}`;
if (player.nearMissTimer > 0) {
this.multiplierEl.classList.add('pulse');
}
else {
this.multiplierEl.classList.remove('pulse');
}
// Score
this.scoreEl.textContent = Math.floor(score).toLocaleString();
// Energy bar
const energyPct = player.energy / config_1.CONFIG.energyMax;
this.energyBar.style.width = `${energyPct * 100}%`;
if (energyPct < 0.2) {
this.energyBar.classList.add('critical');
}
else {
this.energyBar.classList.remove('critical');
}
// Turbo bar
const turboPct = player.turbo / config_1.CONFIG.turboMax;
this.turboBar.style.width = `${turboPct * 100}%`;
if (player.isBoosting) {
this.turboBar.classList.add('active');
this.turboText.style.opacity = '1';
}
else {
this.turboBar.classList.remove('active');
this.turboText.style.opacity = turboPct > 0.5 ? `${turboPct}` : '0';
}
}
showGameOver(score, maxSpeed) {
this.gameOverScreen.style.display = 'flex';
if (this.finalScoreEl)
this.finalScoreEl.textContent = `SCORE: ${Math.floor(score).toLocaleString()}`;
if (this.maxSpeedEl)
this.maxSpeedEl.textContent = `TOP SPEED: ${Math.floor(maxSpeed)} U/S`;
}
hideGameOver() {
this.gameOverScreen.style.display = 'none';
}
}
exports.HUD = HUD;
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Game = void 0;
const player_1 = require("./player");
const world_1 = require("./world");
const collision_1 = require("./collision");
const particles_1 = require("./particles");
const hud_1 = require("./hud");
const audio_1 = require("./audio");
const config_1 = require("./config");
class Game {
constructor() {
this.state = 'menu';
this.score = 0;
this.maxSpeed = 0;
this.distanceTraveled = 0;
// Camera shake
this.shakeX = 0;
this.shakeY = 0;
this.shakeIntensity = 0;
// Input state
this.input = { left: false, right: false, up: false, down: false, boost: false };
this._lastTap = 0;
// Flash effect state
this.flashAlpha = 0;
this.flashColor = '#ffffff';
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(config_1.CONFIG.normalFov, window.innerWidth / window.innerHeight, 0.1, 500);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.clock = new THREE.Clock();
this.setupScene();
this.setupInput();
}
setupScene() {
// Fog for atmosphere
this.scene.fog = new THREE.FogExp2(config_1.CONFIG.colors.fog, 0.012);
// Sky gradient background using a large sphere
const skyGeo = new THREE.SphereGeometry(300, 16, 16);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(config_1.CONFIG.colors.skyTop) },
bottomColor: { value: new THREE.Color(config_1.CONFIG.colors.skyBottom) },
},
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;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition).y * 0.5 + 0.5;
gl_FragColor = vec4(mix(bottomColor, topColor, h), 1.0);
}
`,
side: THREE.BackSide,
});
const sky = new THREE.Mesh(skyGeo, skyMat);
this.scene.add(sky);
// Renderer settings
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('app').appendChild(this.renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0x6b3fa0, 0.5);
this.scene.add(ambientLight);
// Main directional light (sun) - golden hour feel
const sunLight = new THREE.DirectionalLight(0xff8c42, 1.8);
sunLight.position.set(10, 20, -30);
this.scene.add(sunLight);
// Secondary fill light from behind
const fillLight = new THREE.DirectionalLight(0x7b3fa0, 0.5);
fillLight.position.set(-5, 10, 20);
this.scene.add(fillLight);
// Stars in the sky (distant particles)
const starCount = 200;
const starGeo = new THREE.BufferGeometry();
const starPositions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI * 0.4 + 0.1;
const r = 250;
starPositions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
starPositions[i * 3 + 1] = r * Math.cos(phi);
starPositions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
}
const starPosAttr = new THREE.BufferAttribute(starPositions, 3);
starGeo.setAttribute('position', starPosAttr);
const starMat = new THREE.PointsMaterial({
color: 0xffffff,
size: 1.5,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const stars = new THREE.Points(starGeo, starMat);
this.scene.add(stars);
// Sun disc on the horizon
const sunGeo = new THREE.SphereGeometry(15, 16, 16);
const sunMat = new THREE.MeshBasicMaterial({
color: 0xff8c42,
transparent: true,
opacity: 0.9,
});
this.sunMesh = new THREE.Mesh(sunGeo, sunMat);
this.sunMesh.position.set(30, 15, -250);
this.scene.add(this.sunMesh);
// Sun glow ring
const glowGeo = new THREE.SphereGeometry(22, 16, 16);
const glowMat = new THREE.MeshBasicMaterial({
color: 0xffaa00,
transparent: true,
opacity: 0.3,
});
this.sunGlow = new THREE.Mesh(glowGeo, glowMat);
this.sunGlow.position.copy(this.sunMesh.position);
this.scene.add(this.sunGlow);
// Handle resize
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
if (this.flashCanvas) {
this.flashCanvas.width = window.innerWidth;
this.flashCanvas.height = window.innerHeight;
}
});
// Screen flash overlay for near-miss effects
this.flashCanvas = document.createElement('canvas');
this.flashCanvas.id = 'flash-overlay';
this.flashCtx = this.flashCanvas.getContext('2d');
document.getElementById('app').appendChild(this.flashCanvas);
}
setupInput() {
const keyDown = (e) => {
switch (e.code) {
case 'ArrowLeft':
case 'KeyA':
this.input.left = true;
break;
case 'ArrowRight':
case 'KeyD':
this.input.right = true;
break;
case 'ArrowUp':
case 'KeyW':
this.input.up = true;
break;
case 'ArrowDown':
case 'KeyS':
this.input.down = true;
break;
case 'Space':
e.preventDefault();
if (this.state === 'menu') {
this.startGame();
}
else if (this.state === 'playing') {
this.player.activateBoost();
if (this.player.isBoosting)
this.audio.playBoost();
}
break;
}
};
const keyUp = (e) => {
switch (e.code) {
case 'ArrowLeft':
case 'KeyA':
this.input.left = false;
break;
case 'ArrowRight':
case 'KeyD':
this.input.right = false;
break;
case 'ArrowUp':
case 'KeyW':
this.input.up = false;
break;
case 'ArrowDown':
case 'KeyS':
this.input.down = false;
break;
}
};
window.addEventListener('keydown', keyDown);
window.addEventListener('keyup', keyUp);
// Touch controls for mobile
let touchStartX = 0, touchStartY = 0;
const canvas = this.renderer.domElement;
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
if (this.state === 'menu') {
this.startGame();
return;
}
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
// Double tap for boost
const now = Date.now();
if (this._lastTap && now - this._lastTap < 300) {
this.player.activateBoost();
if (this.player.isBoosting)
this.audio.playBoost();
}
this._lastTap = now;
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const dx = e.touches[0].clientX - touchStartX;
const dy = e.touches[0].clientY - touchStartY;
this.input.left = dx < -20;
this.input.right = dx > 20;
this.input.up = dy < -20;
this.input.down = dy > 20;
}, { passive: false });
canvas.addEventListener('touchend', () => {
this.input.left = false;
this.input.right = false;
this.input.up = false;
this.input.down = false;
});
}
startGame() {
// Hide menu overlay
const menuOverlay = document.getElementById('menu-overlay');
if (menuOverlay)
menuOverlay.style.display = 'none';
// Initialize audio on user gesture
if (!this.audio)
this.audio = new audio_1.AudioSystem();
this.audio.init();
// Reset game state
this.player = new player_1.PlayerCraft();
this.scene.add(this.player.mesh);
this.world = new world_1.WorldGenerator(this.scene);
this.collision = new collision_1.CollisionSystem();
this.particles = new particles_1.ParticleSystem(this.scene);
this.particles.init();
this.hud = new hud_1.HUD();
document.getElementById('app').appendChild(this.hud.getElement());
this.state = 'playing';
this.score = 0;
this.maxSpeed = 0;
this.distanceTraveled = 0;
this.shakeIntensity = 0;
// Set initial camera position
this.camera.position.set(0, config_1.CONFIG.cameraHeight + 2, -config_1.CONFIG.cameraDistance);
this.camera.lookAt(this.player.position.x, this.player.position.y, this.player.position.z + 10);
}
update() {
if (this.state !== 'playing')
return;
const dt = Math.min(this.clock.getDelta(), 0.05); // Cap delta to prevent spiral of death
// Update player physics
this.player.update(dt, this.input);
// Move player forward
this.player.position.z += this.player.speed * dt * 60;
this.distanceTraveled = this.player.position.z;
// Track max speed
const currentSpeedKmh = this.player.speed * 100;
if (currentSpeedKmh > this.maxSpeed) {
this.maxSpeed = currentSpeedKmh;
}
// Update world generation and animation
this.world.update(this.player.position.z);
this.world.animate(dt);
this.world.setDifficulty(1 + this.score * config_1.CONFIG.difficultyObstacleDensity);
// Collision detection
const obstaclesInRange = this.world.getObstaclesInRange(this.player.position.z, 60);
const { crashed, nearMiss } = this.collision.checkCollisions(this.player, obstaclesInRange);
if (crashed) {
this.gameOver();
return;
}
// Near-miss scoring
if (nearMiss && this.player.nearMissTimer <= 0) {
this.player.triggerNearMiss();
this.audio.playNearMiss();
this.shakeIntensity = config_1.CONFIG.shakeAmountNearMiss;
this.triggerFlash(0.4, '#ffaa00');
}
// Orb collection
const orbCollected = this.collision.checkOrbCollection(this.player, this.scene);
if (orbCollected) {
this.player.recharge(config_1.CONFIG.orbRechargeAmount);
this.audio.playCollect();
}
// Score accumulation
this.score += this.player.speed * this.player.multiplier * dt * 10;
// Energy depletion = game over
if (this.player.energy <= 0) {
this.gameOver();
return;
}
// Update particles
const speedRatio = this.player.getSpeedRatio();
this.particles.addTrailPoint(this.player.position.x, this.player.position.y - 0.3, this.player.position.z + 1);
this.particles.update(dt, this.player.position.z, speedRatio);
// Update audio engine pitch
this.audio.setEnginePitch(speedRatio);
// Camera follow with shake
this.updateCamera(dt);
// FOV effect for boost
const targetFov = this.player.isBoosting ? config_1.CONFIG.boostFov : config_1.CONFIG.normalFov;
this.camera.fov += (targetFov - this.camera.fov) * 0.05;
this.camera.updateProjectionMatrix();
// Update HUD
this.hud.update(this.player, this.score);
}
updateCamera(dt) {
const targetX = this.player.position.x * 0.7;
const targetY = this.player.position.y + config_1.CONFIG.cameraHeight;
// Camera shake decay
if (this.shakeIntensity > 0) {
this.shakeX = (Math.random() - 0.5) * this.shakeIntensity * 2;
this.shakeY = (Math.random() - 0.5) * this.shakeIntensity * 2;
this.shakeIntensity *= 0.9;
if (this.shakeIntensity < 0.01) {
this.shakeIntensity = 0;
this.shakeX = 0;
this.shakeY = 0;
}
}
// Boost shake
if (this.player.isBoosting && this.shakeIntensity === 0) {
this.shakeX = (Math.random() - 0.5) * config_1.CONFIG.shakeAmountBoost;
this.shakeY = (Math.random() - 0.5) * config_1.CONFIG.shakeAmountBoost;
}
const camZ = this.player.position.z - config_1.CONFIG.cameraDistance;
this.camera.position.x += (targetX + this.shakeX - this.camera.position.x) * config_1.CONFIG.cameraLerpFactor;
this.camera.position.y += (targetY + this.shakeY - this.camera.position.y) * config_1.CONFIG.cameraLerpFactor;
this.camera.position.z = camZ;
// Look ahead of player
const lookAhead = 15 + this.player.speed * 3;
this.camera.lookAt(this.player.position.x * 0.5, this.player.position.y + 1, this.player.position.z + lookAhead);
// Move sun to follow camera (stays on horizon)
if (this.sunMesh && this.sunGlow) {
this.sunMesh.position.set(30, 15, this.camera.position.z - 250);
this.sunGlow.position.copy(this.sunMesh.position);
}
}
triggerFlash(alpha, color) {
this.flashAlpha = alpha;
this.flashColor = color;
}
updateFlash() {
if (this.flashAlpha <= 0 || !this.flashCtx)
return;
const ctx = this.flashCtx;
ctx.clearRect(0, 0, this.flashCanvas.width, this.flashCanvas.height);
ctx.fillStyle = this.flashColor + Math.floor(this.flashAlpha * 255).toString(16).padStart(2, '0');
ctx.fillRect(0, 0, this.flashCanvas.width, this.flashCanvas.height);
this.flashAlpha *= 0.85;
if (this.flashAlpha < 0.01)
this.flashAlpha = 0;
}
gameOver() {
this.state = 'gameover';
this.audio.playCrash();
this.audio.stopEngine();
if (this.hud) {
this.hud.showGameOver(this.score, this.maxSpeed);
}
// Dramatic camera shake on crash
this.shakeIntensity = 1.5;
}
render() {
this.updateFlash();
this.renderer.render(this.scene, this.camera);
}
restart() {
// Clean up old game objects
if (this.player) {
this.scene.remove(this.player.mesh);
}
if (this.world) {
this.world.dispose();
}
if (this.particles) {
this.particles.dispose();
}
if (this.hud) {
const el = this.hud.getElement();
if (el.parentNode)
el.parentNode.removeChild(el);
}
// Reset input
this.input = { left: false, right: false, up: false, down: false, boost: false };
// Start new game
this.startGame();
}
dispose() {
if (this.player)
this.scene.remove(this.player.mesh);
if (this.world)
this.world.dispose();
if (this.particles)
this.particles.dispose();
if (this.hud) {
const el = this.hud.getElement();
if (el.parentNode)
el.parentNode.removeChild(el);
}
this.audio.stopEngine();
this.renderer.dispose();
}
}
exports.Game = Game;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"a572a3a6-1f32-4888-ad06-ebc9ab9e561d","tokensIn":2755950,"tokensOut":36130,"tokensTotal":2792080,"cost":0,"turns":80,"toolCalls":79,"failedToolCalls":3,"timestamp":"2026-07-31T19:46:26.790Z"} -->