Canyon Run
Qwen 3.6 27B MTP GGUF UD Q5 K XL · 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 60,252 bytes · SHA-256 573ffcc3dda9
<!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 Run</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./player":"src/player.ts","./chunks":"src/chunks.ts","./input":"src/input.ts","./audio":"src/audio.ts","./collision":"src/collision.ts","./particles":"src/particles.ts","./hud":"src/hud.ts"},"src/state.ts":{"./types":"src/types.ts"},"src/player.ts":{"./input":"src/input.ts"},"src/collision.ts":{"./chunks":"src/chunks.ts"},"src/hud.ts":{"./state":"src/state.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 });
exports.start = start;
// Main game loop and scene management
const state_1 = require("./state");
const player_1 = require("./player");
const chunks_1 = require("./chunks");
const input_1 = require("./input");
const audio_1 = require("./audio");
const collision_1 = require("./collision");
const particles_1 = require("./particles");
const hud_1 = require("./hud");
const CHUNK_LENGTH = 80;
class Game {
constructor() {
this.cameraTarget = new THREE.Vector3();
this.cameraLookTarget = new THREE.Vector3();
this.currentFov = 65;
this.shakeX = 0;
this.shakeY = 0;
// Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.setClearColor(0x2a1020);
this.renderer.shadowMap.enabled = true;
document.getElementById('app').appendChild(this.renderer.domElement);
// Scene
this.scene = new THREE.Scene();
this.scene.fog = new THREE.FogExp2(0x2a1020, 0.002);
this.scene.background = new THREE.Color(0x2a1020);
// Camera
this.camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 500);
this.camera.position.set(0, 5, 14);
this.cameraTarget.set(0, 5, 14);
this.cameraLookTarget.set(0, 1, -8);
// === LIGHTING ===
this.scene.add(new THREE.AmbientLight(0xffcc88, 5.0));
this.scene.add(new THREE.HemisphereLight(0xffeedd, 0x8844aa, 4.0));
this.dirLight = new THREE.DirectionalLight(0xffeedd, 6.0);
this.dirLight.position.set(-15, 25, 15);
this.dirLight.castShadow = true;
this.dirLight.shadow.camera.left = -40;
this.dirLight.shadow.camera.right = 40;
this.dirLight.shadow.camera.top = 40;
this.dirLight.shadow.camera.bottom = -40;
this.dirLight.shadow.camera.near = 1;
this.dirLight.shadow.camera.far = 120;
this.scene.add(this.dirLight);
this.scene.add(this.dirLight.target);
// Purple fill from opposite side
const fillLight = new THREE.DirectionalLight(0xcc88ff, 1.5);
fillLight.position.set(25, 12, -10);
this.scene.add(fillLight);
// Sunset glow point
this.sunLight = new THREE.PointLight(0xff6633, 5, 200);
this.scene.add(this.sunLight);
// Sun sphere
const sunGeo = new THREE.SphereGeometry(10, 8, 8);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xff8844 });
this.sunSphere = new THREE.Mesh(sunGeo, sunMat);
this.sunSphere.position.set(0, 12, -120);
this.scene.add(this.sunSphere);
// === SKY DOME ===
const skyGeo = new THREE.SphereGeometry(200, 16, 8);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
uTop: { value: new THREE.Color(0x331144) },
uMid: { value: new THREE.Color(0xcc4455) },
uHorizon: { value: new THREE.Color(0xff8844) },
uBottom: { value: new THREE.Color(0xffcc88) },
},
vertexShader: `
varying float vY;
void main() {
vec4 wp = modelMatrix * vec4(position, 1.0);
vY = normalize(wp.xyz).y;
gl_Position = projectionMatrix * viewMatrix * wp;
}
`,
fragmentShader: `
uniform vec3 uTop, uMid, uHorizon, uBottom;
varying float vY;
void main() {
vec3 col;
if (vY > 0.3) col = mix(uMid, uTop, smoothstep(0.3, 1.0, vY));
else if (vY > 0.0) col = mix(uHorizon, uMid, smoothstep(0.0, 0.3, vY));
else col = mix(uBottom, uHorizon, smoothstep(-0.5, 0.0, vY));
gl_FragColor = vec4(col, 1.0);
}
`,
side: 2,
depthWrite: false,
});
this.skyDome = new THREE.Mesh(skyGeo, skyMat);
this.scene.add(this.skyDome);
// === PLAYER ===
this.player = new player_1.Player();
this.scene.add(this.player.mesh);
if (this.player.trailMesh)
this.scene.add(this.player.trailMesh);
// === SPEED PARTICLES ===
this.speedParticles = new particles_1.SpeedParticles();
this.scene.add(this.speedParticles.particles);
// === CHUNKS ===
this.chunkGen = new chunks_1.ChunkGenerator();
for (let i = 0; i < 8; i++) {
const chunk = this.chunkGen.generateChunk(-i * CHUNK_LENGTH - 40, 1);
this.chunkGen.chunks.push(chunk);
this.scene.add(chunk.group);
}
// === INIT ===
this.clock = new THREE.Clock();
window.addEventListener('resize', () => this.onResize());
window.addEventListener('start', () => this.onStart());
window.addEventListener('restart', () => this.onRestart());
this.autoStart();
this.animate();
}
onStart() {
if (!state_1.state.started) {
state_1.state.started = true;
audio_1.audio.init();
}
}
autoStart() {
state_1.state.started = true;
// Force-hide start overlay
const startEl = document.getElementById('start-btn');
if (startEl) {
const parent = startEl.parentElement;
if (parent)
parent.style.display = 'none';
}
// Also hide via HUD reference
hud_1.hud.hideStart();
try {
audio_1.audio.init();
}
catch (e) { }
}
onRestart() {
this.chunkGen.chunks.forEach(c => {
this.scene.remove(c.group);
c.group.traverse((o) => {
if (o instanceof THREE.Mesh) {
o.geometry.dispose();
if (o.material && !Array.isArray(o.material))
o.material.dispose();
}
});
});
this.chunkGen.chunks = [];
(0, state_1.resetState)();
this.player.pos.set(0, 2, 0);
this.player.vel.set(0, 0, 0);
this.player.bankAngle = 0;
this.player.pitchAngle = 0;
this.player.yawAngle = 0;
this.camera.position.set(0, 5, 14);
this.cameraTarget.set(0, 5, 14);
this.cameraLookTarget.set(0, 1, -8);
for (let i = 0; i < 8; i++) {
const chunk = this.chunkGen.generateChunk(-i * CHUNK_LENGTH - 40, 1);
this.chunkGen.chunks.push(chunk);
this.scene.add(chunk.group);
}
hud_1.hud.hideGameOver();
}
onResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
updateChunks() {
const pz = this.player.pos.z;
const last = this.chunkGen.chunks[this.chunkGen.chunks.length - 1];
const lz = last ? last.z : 0;
if (pz < lz + CHUNK_LENGTH * 5) {
const chunk = this.chunkGen.generateChunk(lz - CHUNK_LENGTH, state_1.state.difficulty);
this.chunkGen.chunks.push(chunk);
this.scene.add(chunk.group);
}
for (let i = 0; i < this.chunkGen.chunks.length; i++) {
const c = this.chunkGen.chunks[i];
if (c.z > pz + CHUNK_LENGTH * 2) {
this.scene.remove(c.group);
c.group.traverse((o) => {
if (o instanceof THREE.Mesh) {
o.geometry.dispose();
if (o.material && !Array.isArray(o.material))
o.material.dispose();
}
});
this.chunkGen.chunks.splice(i, 1);
i--;
}
}
}
updateWorldPositions() {
for (const chunk of this.chunkGen.chunks) {
if (!chunk.active)
continue;
const gz = chunk.group.position.z;
chunk.obstacles.forEach(o => {
o.worldSphere.center.set(o.localSphere.center.x, o.localSphere.center.y, o.localSphere.center.z + gz);
});
chunk.orbs.forEach(o => {
const wp = new THREE.Vector3(o.localPos.x, o.localPos.y, o.localPos.z + gz);
o.mesh.position.copy(wp);
o.glow.position.copy(wp);
});
}
}
updateCamera(dt) {
const p = this.player.pos;
const camX = p.x * 0.3;
const camY = p.y + 5;
const camZ = p.z + 12 + (state_1.state.boosting ? 4 : 0);
let sx = 0, sy = 0;
if (state_1.state.shakeIntensity > 0) {
sx = (Math.random() - 0.5) * state_1.state.shakeIntensity * 0.4;
sy = (Math.random() - 0.5) * state_1.state.shakeIntensity * 0.4;
}
this.cameraTarget.set(camX + sx, camY + sy, camZ);
this.camera.position.lerp(this.cameraTarget, 4 * dt);
this.cameraLookTarget.set(p.x * 0.5, p.y + 1, p.z - 8);
this.camera.lookAt(this.cameraLookTarget);
const tFov = 60 + (state_1.state.speed - 15) * 0.8 + (state_1.state.boosting ? 15 : 0);
this.currentFov += (tFov - this.currentFov) * 3 * dt;
this.camera.fov = this.currentFov;
this.camera.updateProjectionMatrix();
}
updateLights() {
const p = this.player.pos;
this.dirLight.position.set(p.x - 15, p.y + 25, p.z + 15);
this.dirLight.target.position.set(p.x, p.y - 5, p.z - 20);
this.dirLight.target.updateMatrixWorld();
this.sunLight.position.set(p.x, p.y + 12, p.z - 80);
this.sunSphere.position.set(p.x, 12, p.z - 120);
this.skyDome.position.set(p.x, 0, p.z);
}
updateGame(dt) {
if (!state_1.state.started || (0, state_1.isGameOver)())
return;
const ed = Math.min(dt, 0.05);
// Boost
if (input_1.input.wantsBoost() && state_1.state.boost >= 10 && !state_1.state.boosting) {
state_1.state.boosting = true;
audio_1.audio.playBoost();
}
if (state_1.state.boosting) {
state_1.state.boost -= 25 * ed;
if (state_1.state.boost <= 0) {
state_1.state.boost = 0;
state_1.state.boosting = false;
}
}
if (!state_1.state.boosting && state_1.state.speed > state_1.state.baseSpeed) {
state_1.state.boost = Math.min(state_1.state.maxBoost, state_1.state.boost + 4 * ed);
}
// Speed
const target = state_1.state.baseSpeed + state_1.state.difficulty * 1.5;
state_1.state.speed += ((target * (state_1.state.boosting ? 1.6 : 1)) - state_1.state.speed) * 2.5 * ed;
state_1.state.maxSpeed = Math.max(state_1.state.maxSpeed, state_1.state.speed);
state_1.state.distance += state_1.state.speed * ed;
this.player.pos.z -= state_1.state.speed * ed;
// Energy
state_1.state.energy -= (2.5 + state_1.state.difficulty * 0.5) * ed;
if (state_1.state.boosting)
state_1.state.energy -= 2 * ed;
if (state_1.state.energy <= 0) {
state_1.state.energy = 0;
state_1.state.gameOver = true;
audio_1.audio.playExplosion();
hud_1.hud.showGameOver();
return;
}
state_1.state.difficulty = 1 + state_1.state.score / 400;
this.player.update(ed, state_1.state.speed, state_1.state.boosting);
this.updateCamera(ed);
this.updateLights();
audio_1.audio.updateEngine(state_1.state.speed);
this.updateChunks();
this.updateWorldPositions();
// Collision
collision_1.collision.updatePlayer(this.player.pos);
const obs = this.chunkGen.getActiveObstacles();
const nm = collision_1.collision.checkNearMisses(obs);
if (nm.triggered) {
state_1.state.nearMissPulse = 1.0;
state_1.state.multiplier = Math.min(10, state_1.state.multiplier + 0.3);
state_1.state.score += 8 * state_1.state.multiplier * ed;
if (Math.random() < 0.015)
audio_1.audio.playNearMiss();
state_1.state.shakeIntensity = Math.min(0.8, 2.0 / nm.closestDist);
}
const orbs = this.chunkGen.getActiveOrbs();
const collected = collision_1.collision.checkOrbs(orbs, ed);
for (const orb of collected) {
state_1.state.energy = Math.min(state_1.state.maxEnergy, state_1.state.energy + 15);
state_1.state.boost = Math.min(state_1.state.maxBoost, state_1.state.boost + 20);
state_1.state.score += 5 * state_1.state.multiplier;
orb.mesh.visible = false;
orb.glow.intensity = 0;
audio_1.audio.playCollect();
}
if (collision_1.collision.checkCollision(obs)) {
state_1.state.gameOver = true;
audio_1.audio.playExplosion();
hud_1.hud.showGameOver();
return;
}
state_1.state.nearMissPulse = Math.max(0, state_1.state.nearMissPulse - 4 * ed);
state_1.state.shakeIntensity = Math.max(0, state_1.state.shakeIntensity - 5 * ed);
state_1.state.multiplier = Math.max(1, state_1.state.multiplier - 0.5 * ed);
state_1.state.score += state_1.state.speed * state_1.state.multiplier * 0.01 * ed;
// Animate orbs
const t = this.clock.getElapsedTime();
for (const chunk of this.chunkGen.getActiveChunks()) {
for (const orb of chunk.orbs) {
if (orb.collected)
continue;
orb.mesh.rotation.y += 3 * ed;
orb.mesh.rotation.x += 1.5 * ed;
const bob = Math.sin(t * 2.5 + orb.localPos.x * 0.5) * 0.15;
orb.mesh.position.y += bob * 0.02;
orb.glow.intensity = 1.0 + Math.sin(t * 4) * 0.3;
}
}
this.speedParticles.update(ed, this.player.pos, state_1.state.speed);
hud_1.hud.update();
}
animate() {
requestAnimationFrame(() => this.animate());
const dt = this.clock.getDelta();
this.updateGame(dt);
this.renderer.render(this.scene, this.camera);
}
}
function start() { new Game(); }
start();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGameState = createGameState;
function createGameState() {
return {
score: 0,
multiplier: 1,
multiplierTimer: 0,
energy: 100,
maxEnergy: 100,
speed: 15,
baseSpeed: 15,
boost: 0,
maxBoost: 100,
boosting: false,
boostingTimer: 0,
gameOver: false,
started: false,
distance: 0,
maxSpeed: 0,
difficulty: 1,
nearMissPulse: 0,
shakeIntensity: 0,
};
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.state = void 0;
exports.resetState = resetState;
exports.setGameOver = setGameOver;
exports.isGameOver = isGameOver;
const types_1 = require("./types");
exports.state = (0, types_1.createGameState)();
function resetState() {
exports.state = (0, types_1.createGameState)();
exports.state.started = true;
}
function setGameOver() {
exports.state.gameOver = true;
}
function isGameOver() {
return exports.state.gameOver;
}
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.audio = void 0;
// Procedural audio using Web Audio API
class SoundEngine {
constructor() {
this.ctx = null;
this.masterGain = null;
this.engineOsc = null;
this.engineGain = null;
this.engineFilter = null;
}
init() {
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.3;
this.masterGain.connect(this.ctx.destination);
// Engine hum
this.engineOsc = this.ctx.createOscillator();
this.engineGain = this.ctx.createGain();
this.engineFilter = this.ctx.createBiquadFilter();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 60;
this.engineFilter.type = 'lowpass';
this.engineFilter.frequency.value = 200;
this.engineFilter.Q.value = 2;
this.engineGain.gain.value = 0.08;
this.engineOsc.connect(this.engineFilter);
this.engineFilter.connect(this.engineGain);
this.engineGain.connect(this.masterGain);
this.engineOsc.start();
}
updateEngine(speed) {
if (!this.engineOsc || !this.engineGain || !this.engineFilter)
return;
const freq = 40 + speed * 4;
this.engineOsc.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.1);
this.engineFilter.frequency.setTargetAtTime(150 + speed * 15, this.ctx.currentTime, 0.1);
this.engineGain.gain.setTargetAtTime(0.05 + speed * 0.003, 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(1600, this.ctx.currentTime + 0.1);
osc.frequency.exponentialRampToValueAtTime(2400, this.ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.25);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.25);
}
playNearMiss() {
if (!this.ctx || !this.masterGain)
return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(600, this.ctx.currentTime + 0.08);
osc.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.2);
}
playExplosion() {
if (!this.ctx || !this.masterGain)
return;
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.exp(-i / (bufferSize * 0.1));
}
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(2000, this.ctx.currentTime);
filter.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.5);
source.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
source.start();
}
playBoost() {
if (!this.ctx || !this.masterGain)
return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(300, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1200, this.ctx.currentTime + 0.3);
gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.4);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.4);
}
destroy() {
if (this.engineOsc) {
this.engineOsc.stop();
this.engineOsc.disconnect();
}
if (this.ctx) {
this.ctx.close();
}
}
}
exports.audio = new SoundEngine();
};
// ── types: src/globals.d.ts ──
__mods["src/globals.d.ts"] = function (exports) {};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.input = exports.Input = void 0;
// Input handling
class Input {
constructor() {
this.keys = new Set();
this.mouseX = 0;
this.mouseY = 0;
this.touchActive = false;
window.addEventListener('keydown', (e) => {
this.keys.add(e.code);
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
this.keys.delete(e.code);
});
window.addEventListener('mousemove', (e) => {
this.mouseX = (e.clientX / window.innerWidth) * 2 - 1;
this.mouseY = -(e.clientY / window.innerHeight) * 2 + 1;
});
window.addEventListener('touchstart', (e) => {
this.touchActive = true;
const t = e.touches[0];
this.mouseX = (t.clientX / window.innerWidth) * 2 - 1;
this.mouseY = -(t.clientY / window.innerHeight) * 2 + 1;
e.preventDefault();
}, { passive: false });
window.addEventListener('touchmove', (e) => {
const t = e.touches[0];
this.mouseX = (t.clientX / window.innerWidth) * 2 - 1;
this.mouseY = -(t.clientY / window.innerHeight) * 2 + 1;
e.preventDefault();
}, { passive: false });
window.addEventListener('touchend', () => {
this.touchActive = false;
});
}
isDown(code) {
return this.keys.has(code);
}
// Returns -1 to 1 for left/right
getLateralInput() {
let v = 0;
if (this.isDown('ArrowLeft') || this.isDown('KeyA'))
v -= 1;
if (this.isDown('ArrowRight') || this.isDown('KeyD'))
v += 1;
return v;
}
// Returns -1 to 1 for up/down
getVerticalInput() {
let v = 0;
if (this.isDown('ArrowUp') || this.isDown('KeyW'))
v += 1;
if (this.isDown('ArrowDown') || this.isDown('KeyS'))
v -= 1;
return v;
}
wantsBoost() {
return this.isDown('Space') || this.isDown('ShiftLeft') || this.isDown('ShiftRight');
}
}
exports.Input = Input;
exports.input = new Input();
};
// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Player = void 0;
// Player flight-based physics model
const input_1 = require("./input");
class Player {
constructor() {
this.trailMesh = null;
// Physics state
this.pos = new THREE.Vector3(0, 2, 0);
this.vel = new THREE.Vector3(0, 0, 0);
this.bankAngle = 0; // roll
this.pitchAngle = 0;
this.yawAngle = 0;
// Config
this.lateralSpeed = 18;
this.verticalSpeed = 14;
this.lateralFriction = 8;
this.verticalFriction = 8;
this.bankFactor = 0.7;
// Craft body — sleek low-poly geometric shape
const group = new THREE.Group();
// Main body — elongated diamond
const bodyGeo = new THREE.ConeGeometry(0.3, 1.8, 6);
bodyGeo.rotateX(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0xc0c0d0,
flatShading: true,
metalness: 0.8,
roughness: 0.2,
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
group.add(body);
// Cockpit
const cockpitGeo = new THREE.OctahedronGeometry(0.2, 0);
const cockpitMat = new THREE.MeshStandardMaterial({
color: 0x4488ff,
flatShading: true,
emissive: 0x2244aa,
emissiveIntensity: 0.5,
metalness: 0.3,
roughness: 0.1,
});
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(0, 0.15, 0.3);
group.add(cockpit);
// Wings
const wingGeo = new THREE.BoxGeometry(3.2, 0.05, 0.8);
const wingMat = new THREE.MeshStandardMaterial({
color: 0xaa8866,
flatShading: true,
metalness: 0.6,
roughness: 0.3,
});
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.set(0, -0.05, 0.1);
group.add(wings);
// Wing tips
const tipGeo = new THREE.TetrahedronGeometry(0.15);
const tipMat = new THREE.MeshStandardMaterial({
color: 0xff6633,
flatShading: true,
emissive: 0xff4400,
emissiveIntensity: 0.8,
});
[-1, 1].forEach(side => {
const tip = new THREE.Mesh(tipGeo, tipMat);
tip.position.set(side * 1.6, -0.05, 0.1);
group.add(tip);
});
// Engine glow light
this.engineGlow = new THREE.PointLight(0xff6633, 2, 6);
this.engineGlow.position.set(0, 0, 1.2);
group.add(this.engineGlow);
// Engine nozzle
const nozzleGeo = new THREE.CylinderGeometry(0.08, 0.2, 0.3, 6);
nozzleGeo.rotateX(Math.PI / 2);
const nozzleMat = new THREE.MeshStandardMaterial({
color: 0xff8844,
flatShading: true,
emissive: 0xff4400,
emissiveIntensity: 1.0,
});
const nozzle = new THREE.Mesh(nozzleGeo, nozzleMat);
nozzle.position.set(0, 0, 1.0);
group.add(nozzle);
this.mesh = group;
this.mesh.position.copy(this.pos);
// Trail particles
const trailCount = 80;
this.trailGeometry = new THREE.BufferGeometry();
const trailPos = new Float32Array(trailCount * 3);
const trailSizes = new Float32Array(trailCount);
for (let i = 0; i < trailCount; i++) {
trailPos[i * 3] = 0;
trailPos[i * 3 + 1] = 0;
trailPos[i * 3 + 2] = 0;
trailSizes[i] = 0;
}
this.trailGeometry.setAttribute('position', new THREE.BufferAttribute(trailPos, 3));
this.trailGeometry.setAttribute('size', new THREE.BufferAttribute(trailSizes, 1));
const trailMat = new THREE.ShaderMaterial({
uniforms: {
uColor: { value: new THREE.Color(0xff6633) },
},
vertexShader: `
attribute float size;
varying float vAlpha;
void main() {
vAlpha = size / 2.0;
vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * 80.0 / -mvPos.z;
gl_Position = projectionMatrix * mvPos;
}
`,
fragmentShader: `
uniform vec3 uColor;
varying float vAlpha;
void main() {
float d = length(gl_PointCoord - 0.5) * 2.0;
if (d > 1.0) discard;
float a = smoothstep(1.0, 0.0, d) * vAlpha * 0.7;
gl_FragColor = vec4(uColor, a);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
});
this.trailMesh = new THREE.Points(this.trailGeometry, trailMat);
this.trailMesh.frustumCulled = false;
this.trailMesh.visible = true;
// Initialize trail with invisible particles behind player
for (let i = 0; i < trailCount; i++) {
trailPos[i * 3 + 2] = 2 + i * 0.3;
trailSizes[i] = 0;
}
this.trailGeometry.attributes.position.needsUpdate = true;
this.trailGeometry.attributes.size.needsUpdate = true;
}
update(dt, speed, boosting) {
// Get input
const lateral = input_1.input.getLateralInput();
const vertical = input_1.input.getVerticalInput();
// Apply acceleration
this.vel.x += lateral * this.lateralSpeed * dt;
this.vel.y += vertical * this.verticalSpeed * dt;
// Apply friction (weighty feel)
this.vel.x *= Math.max(0, 1 - this.lateralFriction * dt);
this.vel.y *= Math.max(0, 1 - this.verticalFriction * dt);
// Clamp velocity
const maxV = 25;
const vlen = Math.sqrt(this.vel.x * this.vel.x + this.vel.y * this.vel.y);
if (vlen > maxV) {
this.vel.x *= maxV / vlen;
this.vel.y *= maxV / vlen;
}
// Integrate position
this.pos.x += this.vel.x * dt;
this.pos.y += this.vel.y * dt;
// Clamp to playable area
this.pos.x = Math.max(-18, Math.min(18, this.pos.x));
this.pos.y = Math.max(-8, Math.min(14, this.pos.y));
// Banking (roll) based on lateral velocity
const targetBank = -this.vel.x * this.bankFactor * 0.15;
this.bankAngle += (targetBank - this.bankAngle) * 8 * dt;
// Pitch based on vertical velocity
const targetPitch = -this.vel.y * 0.1;
this.pitchAngle += (targetPitch - this.pitchAngle) * 6 * dt;
// Yaw subtle drift with lateral movement
this.yawAngle += (lateral * -0.15 - this.yawAngle) * 4 * dt;
// Apply to mesh
this.mesh.position.copy(this.pos);
const euler = new THREE.Euler(this.pitchAngle, this.yawAngle, this.bankAngle, 'YXZ');
this.mesh.quaternion.setFromEuler(euler);
// Engine glow intensity
const glowIntensity = 1.5 + (boosting ? 3 : 0) + Math.sin(Date.now() * 0.01) * 0.3;
this.engineGlow.intensity = glowIntensity;
// Update trail particles
this.updateTrail(dt, speed);
}
updateTrail(dt, speed) {
if (!this.trailMesh)
return;
const positions = this.trailGeometry.attributes.position.array;
const sizes = this.trailGeometry.attributes.size.array;
const count = sizes.length;
// Shift trail backwards
for (let i = count - 1; i > 0; i--) {
positions[i * 3] = positions[(i - 1) * 3];
positions[i * 3 + 1] = positions[(i - 1) * 3 + 1];
positions[i * 3 + 2] = positions[(i - 1) * 3 + 2] + speed * dt * 0.5;
sizes[i] = sizes[i - 1] * 0.95;
}
// Insert new particle at origin (player position)
positions[0] = this.pos.x + (Math.random() - 0.5) * 0.2;
positions[0 + 1] = this.pos.y - 0.1 + (Math.random() - 0.5) * 0.1;
positions[0 + 2] = this.pos.z + 1.5;
sizes[0] = 0.8 + Math.random() * 0.4;
this.trailGeometry.attributes.position.needsUpdate = true;
this.trailGeometry.attributes.size.needsUpdate = true;
}
getBounds() {
return { radius: 0.6 };
}
}
exports.Player = Player;
};
// ── module: src/chunks.ts ──
__mods["src/chunks.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.chunkGenerator = exports.ChunkGenerator = void 0;
// Color palette — vibrant sunset canyon
const WALL_COLORS = [0xFF9944, 0xFFAA55, 0xFF8833, 0xFFBB66, 0xFF7722, 0xFFAA55];
const GROUND_COLOR = 0x996644;
const MONOLITH_COLORS = [0x9944FF, 0x8833EE, 0xAA55FF, 0x7722DD];
const CHUNK_LENGTH = 80;
function seededRandom(seed) {
let s = seed;
return () => {
s = (s * 16807 + 0) % 2147483647;
return (s - 1) / 2147483646;
};
}
// Helper: create a bright wall material with emissive glow
function makeWallMat(col, glow) {
const c = new THREE.Color(col);
const m = new THREE.MeshStandardMaterial({
color: c,
flatShading: true,
roughness: 0.9,
metalness: 0.05,
emissive: c,
emissiveIntensity: glow,
});
return m;
}
class ChunkGenerator {
constructor() {
this.chunks = [];
this.seed = 42;
this.difficulty = 1;
}
getActiveChunks() {
return this.chunks.filter(c => c.active);
}
getActiveObstacles() {
const all = [];
this.chunks.filter(c => c.active).forEach(c => all.push(...c.obstacles));
return all;
}
getActiveOrbs() {
const all = [];
this.chunks.filter(c => c.active).forEach(c => all.push(...c.orbs));
return all;
}
generateChunk(z, difficulty) {
const rng = seededRandom(this.seed + Math.floor(z / CHUNK_LENGTH) * 137);
this.difficulty = difficulty;
const group = new THREE.Group();
group.position.z = z;
const obstacles = [];
const orbs = [];
const typeRoll = rng();
let type;
if (typeRoll < 0.5)
type = 'canyon';
else if (typeRoll < 0.8)
type = 'monolith';
else
type = 'squeeze';
this.createFloor(group, rng, difficulty);
if (type === 'canyon')
this.createCanyonWalls(group, obstacles, rng, difficulty);
else if (type === 'monolith')
this.createMonoliths(group, obstacles, rng, difficulty);
else
this.createSqueeze(group, obstacles, rng, difficulty);
this.createOrbs(group, orbs, rng);
return { group, z, type, obstacles, orbs, active: true };
}
createFloor(group, rng, difficulty) {
const fw = 34 + rng() * 8;
const m = makeWallMat(GROUND_COLOR, 0.2);
const floor = new THREE.Mesh(new THREE.BoxGeometry(fw, 2, CHUNK_LENGTH), m);
floor.position.y = -10;
floor.receiveShadow = true;
group.add(floor);
const bumps = Math.floor(rng() * 2 * difficulty);
for (let i = 0; i < bumps; i++) {
const sz = 1 + rng() * 2;
const col = WALL_COLORS[Math.floor(rng() * WALL_COLORS.length)];
const b = new THREE.Mesh(new THREE.TetrahedronGeometry(sz), makeWallMat(col, 0.3));
b.position.set((rng() - 0.5) * fw * 0.7, -9 + sz * 0.3, (rng() - 0.5) * CHUNK_LENGTH * 0.8);
b.rotation.set(rng() * Math.PI, rng() * Math.PI, rng() * Math.PI);
group.add(b);
}
}
createCanyonWalls(group, obstacles, rng, diff) {
const wh = 25 + diff * 4;
const gw = Math.max(10, 28 - diff * 2);
const lw = this.createWallSegment(CHUNK_LENGTH, wh, rng);
lw.position.set(-gw / 2 - 5, wh / 2 - 5, 0);
group.add(lw);
obstacles.push(this.groupToObstacle(lw, -gw / 2 - 2, wh / 2 - 5, 0, 5));
const rw = this.createWallSegment(CHUNK_LENGTH, wh, rng);
rw.position.set(gw / 2 + 5, wh / 2 - 5, 0);
group.add(rw);
obstacles.push(this.groupToObstacle(rw, gw / 2 + 2, wh / 2 - 5, 0, 5));
if (rng() < 0.25 * diff) {
const col = WALL_COLORS[Math.floor(rng() * WALL_COLORS.length)];
const oh = new THREE.Mesh(new THREE.BoxGeometry(gw * 0.3, 2, 6), makeWallMat(col, 0.5));
oh.position.set((rng() - 0.5) * gw * 0.3, 8 + rng() * 5, (rng() - 0.5) * CHUNK_LENGTH * 0.6);
group.add(oh);
obstacles.push(this.meshToObstacle(oh, 3));
}
}
createWallSegment(length, height, rng) {
const wg = new THREE.Group();
const segs = Math.floor(3 + rng() * 4);
const sw = length / segs;
for (let i = 0; i < segs; i++) {
const w = sw + (rng() - 0.5) * 4;
const h = height * (0.5 + rng() * 0.7);
const d = 3 + rng() * 5;
const col = WALL_COLORS[Math.floor(rng() * WALL_COLORS.length)];
const m = makeWallMat(col, 0.5);
const seg = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), m);
seg.position.set(0, h / 2 - 5, -length / 2 + i * sw + sw / 2);
seg.castShadow = true;
seg.receiveShadow = true;
wg.add(seg);
}
return wg;
}
createMonoliths(group, obstacles, rng, diff) {
const count = Math.floor(3 + rng() * 4 * diff);
for (let i = 0; i < count; i++) {
const h = 8 + rng() * 12, w = 2 + rng() * 3, d = 2 + rng() * 3;
const x = (rng() - 0.5) * 22, y = -8 + h / 2, z = (rng() - 0.5) * CHUNK_LENGTH * 0.75;
const col = MONOLITH_COLORS[Math.floor(rng() * MONOLITH_COLORS.length)];
let mesh;
if (rng() < 0.3) {
const m = makeWallMat(col, 0.5);
m.metalness = 0.6;
m.roughness = 0.3;
mesh = new THREE.Mesh(new THREE.OctahedronGeometry(w, 0), m);
mesh.scale.y = h / w;
}
else {
const m = makeWallMat(col, 0.3);
m.metalness = 0.5;
m.roughness = 0.4;
mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), m);
}
mesh.position.set(x, y, z);
mesh.rotation.y = rng() * Math.PI;
mesh.castShadow = true;
group.add(mesh);
const r = Math.max(w, d) * 0.7 + 1;
const c = new THREE.Vector3(x, y, z);
obstacles.push({ mesh, localSphere: new THREE.Sphere(c.clone(), r), worldSphere: new THREE.Sphere(c, r), nearMissRadius: r + 2.5 });
}
[-1, 1].forEach(side => {
const col = WALL_COLORS[Math.floor(rng() * WALL_COLORS.length)];
const wall = new THREE.Mesh(new THREE.BoxGeometry(4, 30, CHUNK_LENGTH), makeWallMat(col, 0.5));
wall.position.set(side * 18, 7, 0);
wall.receiveShadow = true;
group.add(wall);
obstacles.push(this.meshToObstacle(wall, 4));
});
}
createSqueeze(group, obstacles, rng, diff) {
const gs = Math.max(4, 10 - diff * 1.2);
const gy = 2 + rng() * 5;
const sz = (rng() - 0.5) * CHUNK_LENGTH * 0.4;
const tx = (rng() - 0.5) * 4;
const col = WALL_COLORS[Math.floor(rng() * WALL_COLORS.length)];
const sm = makeWallMat(col, 0.5);
const tw = new THREE.Mesh(new THREE.BoxGeometry(18, 3, 8 + diff * 2), sm);
tw.position.set(tx, gy + gs / 2 + 1.5, sz);
tw.castShadow = true;
group.add(tw);
obstacles.push(this.meshToObstacle(tw, 2));
const bw = new THREE.Mesh(new THREE.BoxGeometry(18, 3, 8 + diff * 2), sm);
bw.position.set(tx, gy - gs / 2 - 1.5, sz);
bw.castShadow = true;
group.add(bw);
obstacles.push(this.meshToObstacle(bw, 2));
const col2 = WALL_COLORS[Math.floor(rng() * WALL_COLORS.length)];
const sm2 = makeWallMat(col2, 0.5);
[-1, 1].forEach(side => {
const sw = new THREE.Mesh(new THREE.BoxGeometry(2, gs + 4, 10), sm2);
sw.position.set(side * 7, gy, sz);
sw.castShadow = true;
group.add(sw);
obstacles.push(this.meshToObstacle(sw, 2));
});
[-1, 1].forEach(side => {
const col3 = WALL_COLORS[Math.floor(rng() * WALL_COLORS.length)];
const wall = new THREE.Mesh(new THREE.BoxGeometry(4, 30, CHUNK_LENGTH), makeWallMat(col3, 0.5));
wall.position.set(side * 18, 7, 0);
wall.receiveShadow = true;
group.add(wall);
obstacles.push(this.meshToObstacle(wall, 4));
});
}
createOrbs(group, orbs, rng) {
const count = 3 + Math.floor(rng() * 4);
for (let i = 0; i < count; i++) {
const om = new THREE.MeshStandardMaterial({
color: 0xffcc00,
flatShading: true,
});
om.emissive.set(0xffaa00);
om.emissiveIntensity = 3;
const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(0.5), om);
const ox = (rng() - 0.5) * 16, oy = 1 + rng() * 7, oz = (rng() - 0.5) * CHUNK_LENGTH * 0.8;
const lp = new THREE.Vector3(ox, oy, oz);
mesh.position.copy(lp);
const glow = new THREE.PointLight(0xffcc00, 2, 6);
glow.position.copy(lp);
group.add(glow);
group.add(mesh);
orbs.push({ mesh, collected: false, glow, localPos: lp });
}
}
meshToObstacle(mesh, padding) {
const box = new THREE.Box3().setFromObject(mesh);
const center = new THREE.Vector3();
box.getCenter(center);
const sphere = new THREE.Sphere();
box.getBoundingSphere(sphere);
sphere.radius += padding;
return { mesh, localSphere: new THREE.Sphere(sphere.center.clone(), sphere.radius), worldSphere: new THREE.Sphere(sphere.center.clone(), sphere.radius), nearMissRadius: sphere.radius + 3 };
}
groupToObstacle(group, cx, cy, cz, padding) {
const box = new THREE.Box3().setFromObject(group);
const center = new THREE.Vector3(cx, cy, cz);
const radius = Math.max(Math.abs(box.max.x - box.min.x), Math.abs(box.max.y - box.min.y)) * 0.5 + padding;
return { mesh: group, localSphere: new THREE.Sphere(center.clone(), radius), worldSphere: new THREE.Sphere(center, radius), nearMissRadius: radius + 3 };
}
}
exports.ChunkGenerator = ChunkGenerator;
exports.chunkGenerator = new ChunkGenerator();
};
// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpeedParticles = void 0;
// Speed lines / debris particles that fly past the player to emphasize speed
class SpeedParticles {
constructor() {
this.particles = null;
this.count = 200;
const geo = new THREE.BufferGeometry();
this.positions = new Float32Array(this.count * 3);
this.sizes = new Float32Array(this.count);
for (let i = 0; i < this.count; i++) {
this.resetParticle(i, true);
this.sizes[i] = 0.5 + Math.random() * 1.5;
}
geo.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
geo.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
const mat = new THREE.ShaderMaterial({
uniforms: {
uColor: { value: new THREE.Color(0xffeedd) },
},
vertexShader: `
attribute float size;
varying float vAlpha;
void main() {
vAlpha = size / 2.0;
vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * 40.0 / -mvPos.z;
gl_Position = projectionMatrix * mvPos;
}
`,
fragmentShader: `
uniform vec3 uColor;
varying float vAlpha;
void main() {
float d = length(gl_PointCoord - 0.5) * 2.0;
if (d > 1.0) discard;
float a = smoothstep(1.0, 0.0, d) * vAlpha * 0.3;
gl_FragColor = vec4(uColor, a);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
this.particles = new THREE.Points(geo, mat);
this.particles.frustumCulled = false;
}
resetParticle(i, randomZ) {
this.positions[i * 3] = (Math.random() - 0.5) * 40;
this.positions[i * 3 + 1] = (Math.random() - 0.5) * 25 - 3;
this.positions[i * 3 + 2] = randomZ ? -Math.random() * 80 : -80;
}
update(dt, playerPos, speed) {
if (!this.particles)
return;
const s = speed * dt * 3;
for (let i = 0; i < this.count; i++) {
this.positions[i * 3 + 2] += s;
if (this.positions[i * 3 + 2] > playerPos.z + 20) {
this.resetParticle(i, false);
}
}
this.particles.geometry.attributes.position.needsUpdate = true;
this.particles.position.copy(playerPos);
}
}
exports.SpeedParticles = SpeedParticles;
};
// ── module: src/collision.ts ──
__mods["src/collision.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.collision = exports.CollisionSystem = void 0;
class CollisionSystem {
constructor() {
this.playerPos = new THREE.Vector3();
this.playerRadius = 0.5;
}
updatePlayer(pos) {
this.playerPos.copy(pos);
}
checkCollision(obstacles) {
const pSphere = new THREE.Sphere(this.playerPos, this.playerRadius);
for (const obs of obstacles) {
if (pSphere.intersectsSphere(obs.worldSphere)) {
return true;
}
}
return false;
}
checkNearMisses(obstacles) {
let closestDist = Infinity;
for (const obs of obstacles) {
const dist = this.playerPos.distanceTo(obs.worldSphere.center);
const collisionRadius = this.playerRadius + obs.worldSphere.radius - 0.5;
if (dist < obs.nearMissRadius && dist > collisionRadius) {
closestDist = Math.min(closestDist, dist);
}
}
return {
triggered: closestDist < Infinity,
closestDist,
};
}
checkOrbs(orbs, dt) {
const collected = [];
for (const orb of orbs) {
if (orb.collected)
continue;
const dist = this.playerPos.distanceTo(orb.mesh.position);
if (dist < this.playerRadius + 1.5) {
orb.collected = true;
collected.push(orb);
}
}
return collected;
}
}
exports.CollisionSystem = CollisionSystem;
exports.collision = new CollisionSystem();
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.hud = exports.HUD = void 0;
// Minimalist HUD rendered as DOM overlay
const state_1 = require("./state");
class HUD {
constructor() {
this.container = document.createElement('div');
this.container.id = 'hud';
this.container.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; font-family: 'Courier New', monospace;
z-index: 100; color: #fff;
`;
// Speed bar (right side vertical)
this.speedBar = document.createElement('div');
this.speedBar.style.cssText = `
position: absolute; right: 30px; top: 50%; transform: translateY(-50%);
width: 4px; height: 200px; background: rgba(255,255,255,0.1);
border-radius: 2px; overflow: hidden;
`;
const speedFill = document.createElement('div');
speedFill.id = 'speed-fill';
speedFill.style.cssText = `
width: 100%; height: 50%; background: linear-gradient(to top, #ff6633, #ffcc33);
border-radius: 2px; transition: height 0.1s ease-out; position: absolute; bottom: 0;
`;
this.speedBar.appendChild(speedFill);
this.container.appendChild(this.speedBar);
// Speed label
const speedLabel = document.createElement('div');
speedLabel.id = 'speed-label';
speedLabel.style.cssText = `
position: absolute; right: 20px; top: calc(50% + 115px);
font-size: 11px; color: rgba(255,255,255,0.5); letter-spacing: 2px;
`;
speedLabel.textContent = 'SPD';
this.container.appendChild(speedLabel);
// Multiplier (center-top)
this.multiplierEl = document.createElement('div');
this.multiplierEl.style.cssText = `
position: absolute; top: 30px; left: 50%; transform: translateX(-50%);
font-size: 48px; font-weight: bold; color: #ffcc33;
text-shadow: 0 0 20px rgba(255,204,51,0.5);
transition: transform 0.1s, color 0.2s;
`;
this.multiplierEl.textContent = '×1';
this.container.appendChild(this.multiplierEl);
// Score
this.scoreEl = document.createElement('div');
this.scoreEl.style.cssText = `
position: absolute; top: 85px; left: 50%; transform: translateX(-50%);
font-size: 16px; color: rgba(255,255,255,0.7); letter-spacing: 3px;
`;
this.scoreEl.textContent = '0';
this.container.appendChild(this.scoreEl);
// Energy bar (bottom center)
this.energyBar = document.createElement('div');
this.energyBar.style.cssText = `
position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
width: 200px; height: 6px; background: rgba(255,255,255,0.1);
border-radius: 3px; overflow: hidden;
`;
const energyFill = document.createElement('div');
energyFill.id = 'energy-fill';
energyFill.style.cssText = `
width: 100%; height: 100%;
background: linear-gradient(to right, #ff3333, #ffcc33, #33ff66);
border-radius: 3px; transition: width 0.1s ease-out;
`;
this.energyBar.appendChild(energyFill);
this.container.appendChild(this.energyBar);
// Energy label
const energyLabel = document.createElement('div');
energyLabel.style.cssText = `
position: absolute; bottom: 42px; left: 50%; transform: translateX(-50%);
font-size: 10px; color: rgba(255,255,255,0.4); letter-spacing: 2px;
`;
energyLabel.textContent = 'ENERGY';
this.container.appendChild(energyLabel);
// Boost bar (below energy)
this.boostBar = document.createElement('div');
this.boostBar.style.cssText = `
position: absolute; bottom: 55px; left: 50%; transform: translateX(-50%);
width: 140px; height: 4px; background: rgba(255,255,255,0.08);
border-radius: 2px; overflow: hidden;
`;
const boostFill = document.createElement('div');
boostFill.id = 'boost-fill';
boostFill.style.cssText = `
width: 0%; height: 100%;
background: linear-gradient(to right, #00ccff, #00ffaa);
border-radius: 2px; transition: width 0.1s ease-out;
`;
this.boostBar.appendChild(boostFill);
this.container.appendChild(this.boostBar);
// Boost label
const boostLabel = document.createElement('div');
boostLabel.style.cssText = `
position: absolute; bottom: 67px; left: 50%; transform: translateX(-50%);
font-size: 10px; color: rgba(255,255,255,0.4); letter-spacing: 2px;
`;
boostLabel.textContent = 'TURBO';
this.container.appendChild(boostLabel);
// Near miss flash
this.nearMissFlash = document.createElement('div');
this.nearMissFlash.style.cssText = `
position: absolute; inset: 0; background: rgba(255,204,51,0.15);
opacity: 0; transition: opacity 0.05s; pointer-events: none;
`;
this.container.appendChild(this.nearMissFlash);
// Game over overlay
this.gameOverOverlay = document.createElement('div');
this.gameOverOverlay.style.cssText = `
position: absolute; inset: 0; background: rgba(0,0,0,0.85);
display: none; flex-direction: column; align-items: center; justify-content: center;
pointer-events: auto;
`;
const title = document.createElement('div');
title.style.cssText = 'font-size: 48px; color: #ff6633; margin-bottom: 30px; letter-spacing: 6px;';
title.textContent = 'WIPED OUT';
this.gameOverOverlay.appendChild(title);
this.gameOverScore = document.createElement('div');
this.gameOverScore.style.cssText = 'font-size: 24px; color: #ffcc33; margin-bottom: 10px; letter-spacing: 2px;';
this.gameOverOverlay.appendChild(this.gameOverScore);
this.gameOverMaxSpeed = document.createElement('div');
this.gameOverMaxSpeed.style.cssText = 'font-size: 16px; color: rgba(255,255,255,0.6); margin-bottom: 30px; letter-spacing: 2px;';
this.gameOverOverlay.appendChild(this.gameOverMaxSpeed);
const restartBtn = document.createElement('button');
restartBtn.id = 'restart-btn';
restartBtn.style.cssText = `
font-family: 'Courier New', monospace; font-size: 18px; color: #fff;
background: transparent; border: 2px solid #ff6633; padding: 12px 40px;
cursor: pointer; letter-spacing: 3px; pointer-events: auto;
transition: all 0.2s;
`;
restartBtn.textContent = 'RESTART';
restartBtn.addEventListener('mouseenter', () => {
restartBtn.style.background = 'rgba(255,102,51,0.3)';
});
restartBtn.addEventListener('mouseleave', () => {
restartBtn.style.background = 'transparent';
});
restartBtn.addEventListener('click', () => {
window.dispatchEvent(new CustomEvent('restart'));
});
this.gameOverOverlay.appendChild(restartBtn);
this.container.appendChild(this.gameOverOverlay);
// Start overlay
this.startOverlay = document.createElement('div');
this.startOverlay.style.cssText = `
position: absolute; inset: 0; background: rgba(0,0,0,0.9);
display: flex; flex-direction: column; align-items: center; justify-content: center;
pointer-events: auto;
`;
const startTitle = document.createElement('div');
startTitle.style.cssText = 'font-size: 56px; color: #ff6633; margin-bottom: 10px; letter-spacing: 8px; text-shadow: 0 0 30px rgba(255,102,51,0.5);';
startTitle.textContent = 'CANYON RACER';
this.startOverlay.appendChild(startTitle);
const startSub = document.createElement('div');
startSub.style.cssText = 'font-size: 14px; color: rgba(255,255,255,0.5); margin-bottom: 40px; letter-spacing: 3px;';
startSub.textContent = 'HIGH-SPEED PROCEDURAL RUN';
this.startOverlay.appendChild(startSub);
const startControls = document.createElement('div');
startControls.style.cssText = 'font-size: 13px; color: rgba(255,255,255,0.4); margin-bottom: 30px; line-height: 2; text-align: center; letter-spacing: 1px;';
startControls.innerHTML = 'WASD / ARROWS — MOVE<br>SPACE / SHIFT — BOOST<br>COLLECT ORBS — STAY FUELED';
this.startOverlay.appendChild(startControls);
const startBtn = document.createElement('button');
startBtn.id = 'start-btn';
startBtn.style.cssText = `
font-family: 'Courier New', monospace; font-size: 20px; color: #ffcc33;
background: transparent; border: 2px solid #ffcc33; padding: 14px 50px;
cursor: pointer; letter-spacing: 4px; pointer-events: auto;
transition: all 0.2s;
`;
startBtn.textContent = 'LAUNCH';
startBtn.addEventListener('mouseenter', () => {
startBtn.style.background = 'rgba(255,204,51,0.2)';
});
startBtn.addEventListener('mouseleave', () => {
startBtn.style.background = 'transparent';
});
startBtn.addEventListener('click', () => {
window.dispatchEvent(new CustomEvent('start'));
this.startOverlay.style.display = 'none';
});
this.startOverlay.appendChild(startBtn);
this.container.appendChild(this.startOverlay);
document.body.appendChild(this.container);
}
update() {
if (state_1.state.gameOver) {
this.gameOverOverlay.style.display = 'flex';
this.gameOverScore.textContent = `SCORE: ${Math.floor(state_1.state.score).toLocaleString()}`;
this.gameOverMaxSpeed.textContent = `MAX SPEED: ${Math.floor(state_1.state.maxSpeed * 20)} KM/H`;
return;
}
// Speed fill
const speedFill = document.getElementById('speed-fill');
if (speedFill) {
const pct = Math.min(1, state_1.state.speed / 40);
speedFill.style.height = `${pct * 100}%`;
}
// Multiplier
this.multiplierEl.textContent = `×${state_1.state.multiplier}`;
if (state_1.state.multiplier > 1) {
const scale = 1 + Math.sin(state_1.state.nearMissPulse * 10) * 0.1;
this.multiplierEl.style.transform = `translateX(-50%) scale(${scale})`;
this.multiplierEl.style.color = state_1.state.multiplier > 3 ? '#ff6633' : '#ffcc33';
}
else {
this.multiplierEl.style.transform = 'translateX(-50%) scale(1)';
this.multiplierEl.style.color = '#ffcc33';
}
// Score
this.scoreEl.textContent = `${Math.floor(state_1.state.score).toLocaleString()}`;
// Energy
const energyFill = document.getElementById('energy-fill');
if (energyFill) {
const pct = Math.max(0, state_1.state.energy / state_1.state.maxEnergy);
energyFill.style.width = `${pct * 100}%`;
if (pct < 0.2) {
energyFill.style.background = '#ff3333';
}
else if (pct < 0.5) {
energyFill.style.background = '#ffcc33';
}
else {
energyFill.style.background = 'linear-gradient(to right, #ff3333, #ffcc33, #33ff66)';
}
}
// Boost
const boostFill = document.getElementById('boost-fill');
if (boostFill) {
const pct = Math.min(1, state_1.state.boost / state_1.state.maxBoost);
boostFill.style.width = `${pct * 100}%`;
}
// Near miss flash
if (state_1.state.nearMissPulse > 0) {
this.nearMissFlash.style.opacity = `${state_1.state.nearMissPulse * 0.5}`;
}
else {
this.nearMissFlash.style.opacity = '0';
}
}
showGameOver() {
this.gameOverOverlay.style.display = 'flex';
}
hideGameOver() {
this.gameOverOverlay.style.display = 'none';
}
hideStart() {
this.startOverlay.style.display = 'none';
}
}
exports.HUD = HUD;
exports.hud = new HUD();
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-UD-Q5_K_XL","provider":"brigid","persona":"typescript","sessionId":"1171bd06-7305-4452-8d98-31cd4cf2bb64","tokensIn":9734212,"tokensOut":90170,"tokensTotal":9824382,"cost":0,"turns":127,"toolCalls":174,"failedToolCalls":16,"timestamp":"2026-08-04T11:49:02.549Z"} -->