← Canyon Run results

Canyon Run

Qwen 3.5 9b · composer

Initial view of Canyon Run

Reviews & generation details

Reviews

Human review · Jul 17, 2026 · rubric 1

Historical path-based review. Association with these exact bytes is unverified.

The reviewer marked this result as broken. It remains in the archive.

Visual design
Unscored
UX / feel
Unscored
Works
Unscored
Prompt spirit
Unscored
Delight
Unscored

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 42,374 bytes · SHA-256 a80a02c42537
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>High-Speed Procedural Canyon Racer</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
<style>
/* ── css: hud ── */
#hud {
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 30px;
    font-family: 'Arial', sans-serif;
    z-index: 100;
    pointer-events: none;
}

.energy-bar-container {
    width: 200px;
    height: 16px;
    background: rgba(0, 0, 0, 0.5);
    border: 2px solid rgba(255, 255, 255, 0.3);
    border-radius: 8px;
    overflow: hidden;
}

.energy-fill {
    height: 100%;
    width: 100%;
    background: linear-gradient(90deg, #00ffff, #ff00ff);
    transition: width 0.3s ease;
    box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
}

.speedometer {
    font-size: 48px;
    font-weight: bold;
    color: rgba(255, 255, 255, 0.9);
    text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
    min-width: 80px;
    text-align: center;
}

.multiplier {
    font-size: 72px;
    font-weight: bold;
    color: rgba(255, 107, 87, 0.95);
    text-shadow: 0 0 30px rgba(255, 107, 87, 0.6);
    min-width: 100px;
    text-align: center;
    transition: transform 0.1s ease;
}

.multiplier.pulse {
    transform: scale(1.3) rotate(-5deg);
}
/* ── css: world ── */
#app {
    width: 100vw;
    height: 100vh;
    position: fixed;
    overflow: hidden;
}

canvas {
    display: block;
    outline: none;
}

/* Bloom and post-processing simulated */
.bloom-effect {
    mix-blend-mode: screen;
}

/* Particle trail effects */
.trail-particle {
    position: absolute;
    pointer-events: none;
}
/* ── css: game-over ── */
.game-over-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: rgba(26, 10, 46, 0.9);
    z-index: 1000;
    font-family: 'Arial', sans-serif;
}

.game-over-overlay h1 {
    animation: shake 0.5s ease-in-out infinite;
}

@keyframes shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-10px) rotate(-2deg); }
    75% { transform: translateX(10px) rotate(2deg); }
}

#game-over {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: linear-gradient(180deg, 
        rgba(26, 10, 46, 0.95) 0%,
        rgba(61, 37, 73, 0.95) 50%,
        rgba(201, 79, 104, 0.9) 100%
    );
    z-index: 1000;
}

#game-over h1 {
    font-size: 64px;
    color: #ff6b57;
    text-shadow: 0 0 30px rgba(255, 107, 87, 0.8);
    margin-bottom: 30px;
}

#game-over p {
    font-size: 24px;
    color: #fff;
    text-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
    margin: 10px 0;
}

#game-over strong {
    font-size: 32px;
    color: #00ffff;
    text-shadow: 0 0 10px rgba(0, 255, 255, 0.6);
}

#game-over button {
    margin-top: 20px;
    padding: 18px 45px;
    font-size: 22px;
    background: linear-gradient(90deg, #ff6b57, #c94f68);
    color: white;
    border: none;
    border-radius: 10px;
    cursor: pointer;
    box-shadow: 0 0 25px rgba(255, 107, 87, 0.7);
    transition: transform 0.2s, box-shadow 0.2s;
}

#game-over button:hover {
    transform: scale(1.05);
    box-shadow: 0 0 35px rgba(255, 107, 87, 0.9);
}
</style>
</head>
<body>
<div id="app"></div>
<div id="hud" class="hud">
    <div class="energy-bar-container">
        <div id="energy-fill" class="energy-fill"></div>
    </div>
    <div id="speedometer" class="speedometer">0</div>
    <div id="multiplier" class="multiplier">x1</div>
</div>
<script>
'use strict';

// ── const: ENEMY_SPEED ──
const ENEMY_SPEED = 15;

// ── const: TUBE_RADIUS ──
const TUBE_RADIUS = 0.4;

// ── const: SEGMENT_LENGTH ──
const SEGMENT_LENGTH = 80;

// ── class: Player ──
class Player {
    constructor(scene = null) {
        this.mesh = new THREE.Group();
        this.position = new THREE.Vector3(0, 0, 0);
        this.velocity = new THREE.Vector3(0, 0, -15);
        this.acceleration = new THREE.Vector3(0, 0, 0);
        this.angularVelocity = new THREE.Vector3(0, 0, 0);
        
        // Build the player craft geometry
        const material = new THREE.MeshStandardMaterial({
            color: 0xffffff,
            roughness: 0.4,
            metalness: 0.6
        });
        
        const fuselage = new THREE.ConeGeometry(0.5, 1.5, 8);
        fuselage.rotateX(Math.PI / 2);
        const mesh = new THREE.Mesh(fuselage, material);
        this.mesh.add(mesh);
        
        // Engine glow
        const engineGeo = new THREE.ConeGeometry(0.2, 0.5, 8);
        engineGeo.rotateX(-Math.PI / 2);
        const engineMat = new THREE.MeshBasicMaterial({
            color: 0x00ffff,
            transparent: true,
            opacity: 0.8
        });
        const engine = new THREE.Mesh(engineGeo, engineMat);
        engine.position.z = -1;
        this.mesh.add(engine);
        
        // Add to scene if provided
        if (scene) {
            scene.add(this.mesh);
        }
    }
    
    update(dt) {
        this.velocity.add(this.acceleration.multiplyScalar(dt));
        
        // Banking on turns - roll around Z axis
        const lateralForce = Math.abs(this.velocity.x + this.velocity.y);
        if (lateralForce > 0.1) {
            this.mesh.rotation.z += lateralForce * 2.5 * dt;
        }
    }
    
    applyInput(input, dt) {
        // Pitch up/down for acceleration
        if (input.pitch !== undefined) {
            this.acceleration.z += input.pitch * 20 * dt;
        }
        
        // Roll/bank controls
        if (input.roll !== undefined) {
            this.mesh.rotation.x -= input.roll * 8 * dt;
            this.mesh.rotation.y += input.roll * 6 * dt;
        }
    }
}

// ── class: PhysicsWorld ──
class PhysicsWorld {
    constructor() {
        this.gravity = new THREE.Vector3(0, -9.8, 0);
        this.timeScale = 1;
    }
    
    applyGravity(player) {
        player.acceleration.add(this.gravity.multiplyScalar(0.01));
    }
    
    applyDrag(player) {
        const drag = new THREE.Vector3();
        drag.set(-player.velocity.x, -player.velocity.y, -player.velocity.z);
        drag.divideScalar(Math.max(player.velocity.length(), 0.1));
        player.acceleration.add(drag.multiplyScalar(0.2));
    }
}

// ── class: ProceduralGen ──
class ProceduralGen {
    constructor(scene) {
        this.scene = scene;
        this.segments = [];
        this.segmentCount = 0;
    }
    
    createSegment(zPosition) {
        const segment = {
            z: zPosition,
            widthLeft: 25 + Math.random() * 30,
            widthRight: 25 + Math.random() * 30,
            heightTop: 40 + Math.random() * 60,
            obstacles: [],
            type: this.getSegmentType(zPosition),
            age: 0
        };
        
        // Add canyon walls as geometry
        const wallLeft = new THREE.Mesh(
            new THREE.CylinderGeometry(segment.widthLeft/2, segment.widthLeft/1.8, SEGMENT_LENGTH + 20, 8),
            this.getWallMaterial('left')
        );
        wallLeft.position.set(-segment.widthLeft/2 - (SEGMENT_LENGTH/2) + 10, 30, zPosition);
        
        const wallRight = new THREE.Mesh(
            new THREE.CylinderGeometry(segment.widthRight/2, segment.widthRight/1.8, SEGMENT_LENGTH + 20, 8),
            this.getWallMaterial('right')
        );
        wallRight.position.set(segment.widthRight/2 + (SEGMENT_LENGTH/2) - 10, 30, zPosition);
        
        segment.walls = [wallLeft, wallRight];
        segment.mesh = new THREE.Group();
        segment.mesh.add(wallLeft);
        segment.mesh.add(wallRight);
        this.scene.add(segment.mesh);
        
        // Add obstacles based on type
        this.addObstacles(segment);
        
        return segment;
    }
    
    getSegmentType(z) {
        const difficultyMultiplier = 1 + Math.min(Math.abs(z) / 5000, 2);
        const rand = Math.random();
        
        if (rand < 0.2 * difficultyMultiplier) {
            return 'squeeze';
        } else if (rand < 0.5 * difficultyMultiplier) {
            return 'monoliths';
        }
        return 'standard';
    }
    
    getWallMaterial(side) {
        const colors = [
            0xff4422, 0x8B3A3A, 0x5C4033, 
            0xD2691E, 0xFF7F50, 0xCD853F
        ];
        const color = colors[Math.floor(Math.random() * colors.length)];
        
        return new THREE.MeshStandardMaterial({
            color: color,
            roughness: 0.3,
            metalness: 0.2,
            flatShading: true
        });
    }
    
    addObstacles(segment) {
        const numObstacles = Math.floor(Math.random() * 3) + 1;
        
        for (let i = 0; i < numObstacles; i++) {
            const obstacleType = this.getRandomObstacleType();
            
            let mesh, geometry, material, position;
            
            switch(obstacleType) {
                case 'monolith':
                    const height = 15 + Math.random() * 20;
                    const radius = 3 + Math.random() * 4;
                    geometry = new THREE.CylinderGeometry(radius/2, radius, height, 6);
                    material = new THREE.MeshStandardMaterial({
                        color: 0x666666,
                        roughness: 0.5,
                        metalness: 0.3,
                        flatShading: true
                    });
                    mesh = new THREE.Mesh(geometry, material);
                    const angle = Math.random() * Math.PI * 2;
                    const x = (segment.widthLeft + segment.widthRight) / 4 * Math.cos(angle) + (SEGMENT_LENGTH/2);
                    const y = height / 2;
                    position = new THREE.Vector3(x, y, segment.z - SEGMENT_LENGTH/2 + 15);
                    break;
                    
                case 'debris':
                    const debrisSize = 1 + Math.random() * 3;
                    geometry = new THREE.BoxGeometry(debrisSize, debrisSize * 0.6, debrisSize);
                    material = new THREE.MeshStandardMaterial({
                        color: 0x444444,
                        roughness: 0.8,
                        metalness: 0.1,
                        flatShading: true
                    });
                    mesh = new THREE.Mesh(geometry, material);
                    const dx = (Math.random() - 0.5) * 30;
                    const dy = 2 + Math.random() * 15;
                    position = new THREE.Vector3(dx, dy, segment.z - SEGMENT_LENGTH/2 + Math.random() * SEGMENT_LENGTH);
                    break;
                    
                case 'floating':
                    geometry = new THREE.TorusGeometry(4, 0.8, 8, 16);
                    material = new THREE.MeshStandardMaterial({
                        color: 0x777777,
                        roughness: 0.3,
                        metalness: 0.5,
                        flatShading: true
                    });
                    mesh = new THREE.Mesh(geometry, material);
                    const fx = (Math.random() - 0.5) * (segment.widthLeft + segment.widthRight) / 2;
                    const fy = 8 + Math.random() * 12;
                    position = new THREE.Vector3(fx, fy, segment.z - SEGMENT_LENGTH/2);
                    break;
            }
            
            if (mesh) {
                mesh.position.copy(position);
                this.scene.add(mesh);
                segment.mesh.add(mesh);
                segment.obstacles.push({ mesh: mesh, type: obstacleType });
            }
        }
    }
    
    getRandomObstacleType() {
        const types = ['monolith', 'debris', 'floating'];
        return types[Math.floor(Math.random() * types.length)];
    }
}

// ── class: EnergySystem ──
class EnergySystem {
    constructor(scene) {
        this.scene = scene;
        this.orbs = [];
        this.spawnDistance = 800;
    }
    
    spawnOrb(playerPosition, segment) {
        const angle = Math.random() * Math.PI * 2;
        const radius = (segment.widthLeft + segment.widthRight) / 3;
        
        let x, y, z;
        if (Math.random() > 0.5) {
            x = -radius/1.5 + (Math.random() - 0.5);
        } else {
            x = radius/1.5 + (Math.random() - 0.5);
        }
        y = 2 + Math.random() * 15;
        z = segment.z - SEGMENT_LENGTH/2 + Math.random() * SEGMENT_LENGTH;
        
        const color = this.getOrbColor();
        const glowIntensity = 0.6 + Math.random() * 0.4;
        
        const geometry = new THREE.IcosahedronGeometry(0.6, 1);
        const material = new THREE.MeshBasicMaterial({
            color: color,
            transparent: true,
            opacity: glowIntensity
        });
        
        const mesh = new THREE.Mesh(geometry, material);
        mesh.position.set(x, y, z);
        
        // Add point light for glow
        const light = new THREE.PointLight(color, 1, 20);
        light.position.copy(mesh.position);
        
        this.scene.add(light);
        
        const orb = {
            mesh: mesh,
            light: light,
            radius: 0.5,
            age: Math.random() * SEGMENT_LENGTH + 40
        };
        
        this.orbs.push(orb);
        
        return orb;
    }
    
    getOrbColor() {
        const colors = [0x00ffff, 0xff00ff, 0xffff00, 0x00ff00];
        return colors[Math.floor(Math.random() * colors.length)];
    }
    
    update(playerPosition) {
        for (let i = this.orbs.length - 1; i >= 0; i--) {
            const orb = this.orbs[i];
            const distance = playerPosition.distanceTo(orb.mesh.position);
            
            if (distance < orb.radius + TUBE_RADIUS) {
                this.onCollect(orb);
                this.scene.remove(orb.mesh);
                this.scene.remove(orb.light);
                this.orbs.splice(i, 1);
            } else {
                // Move with world
                const dz = orb.age * 0.5;
                orb.mesh.position.z += dz;
                orb.light.position.copy(orb.mesh.position);
                
                // Rotate
                orb.mesh.rotation.y += 2 * dt;
                orb.mesh.rotation.x += 1 * dt;
            }
        }
    }
    
    onCollect(orb) {
        this.onRecharge();
    }
    
    onRecharge() {
        // Reset energy - boost player forward
        gameState.player.velocity.z = THREE.MathUtils.lerp(
            gameState.player.velocity.z, 
            35, 
            0.2
        );
    }
}

// ── class: ParticleSystem ──
class ParticleSystem {
    constructor(scene) {
        this.scene = scene;
        this.particles = [];
        this.trailParticles = [];
        this.debrisCount = 0;
        
        // Create debris for speed illusion
        for (let i = 0; i < 80; i++) {
            const size = Math.random() * 0.3 + 0.1;
            const geometry = new THREE.BoxGeometry(size, size, size);
            const material = new THREE.MeshBasicMaterial({
                color: 0x444444,
                transparent: true,
                opacity: 0.6
            });
            
            const mesh = new THREE.Mesh(geometry, material);
            const x = (Math.random() - 0.5) * 200;
            const y = (Math.random() - 0.5) * 100 + 30;
            const z = (Math.random() - 0.5) * 200;
            
            mesh.position.set(x, y, z);
            
            const particle = {
                mesh: mesh,
                velocity: new THREE.Vector3(
                    (Math.random() - 0.5) * 3,
                    (Math.random() - 0.5) * 2 + 1,
                    (Math.random() - 0.5) * 2
                ),
                age: 0,
                life: Math.random() * 100 + 50
            };
            
            this.scene.add(mesh);
            this.particles.push(particle);
        }
    }
    
    update(playerPosition, dt) {
        // Trail particles
        if (Math.random() > 0.3) {
            const size = 0.2 + Math.random() * 0.4;
            const geometry = new THREE.ConeGeometry(size, size * 2, 6);
            geometry.rotateX(-Math.PI / 2);
            const material = new THREE.MeshBasicMaterial({
                color: 0x00ffff,
                transparent: true,
                opacity: 0.7
            });
            
            const mesh = new THREE.Mesh(geometry, material);
            mesh.position.copy(playerPosition);
            const playerZSpeed = gameState.player.velocity.z * 0.5;
            mesh.position.z -= playerZSpeed;
            
            this.scene.add(mesh);
            this.trailParticles.push({ mesh: mesh, age: 0 });
        }
        
        // Move debris and remove off-screen
        for (let i = this.particles.length - 1; i >= 0; i--) {
            const p = this.particles[i];
            p.mesh.position.add(p.velocity.clone().multiplyScalar(dt));
            p.age += dt * 60;
            
            if (p.age > p.life) {
                this.scene.remove(p.mesh);
                this.particles.splice(i, 1);
            }
        }
        
        // Update trail particles
        for (let i = this.trailParticles.length - 1; i >= 0; i--) {
            const t = this.trailParticles[i];
            t.age += dt * 60;
            t.mesh.material.opacity = THREE.MathUtils.clamp(0.7 * (1 - t.age / 5), 0, 0.7);
            
            if (t.age > 5) {
                this.scene.remove(t.mesh);
                this.trailParticles.splice(i, 1);
            }
        }
    }
}

// ── class: ScoringSystem ──
class ScoringSystem {
    constructor() {
        this.multiplier = 1;
        this.maxMultiplier = 50;
        this.proximityScore = 0;
        this.distanceTraveled = 0;
        this.boostMeter = 0;
        this.maxBoost = 100;
    }
    
    checkProximity(playerPosition, segment) {
        const closestWallDistance = Math.min(
            playerPosition.position.x + TUBE_RADIUS,
            segment.widthLeft - TUBE_RADIUS/2,
            (segment.widthRight - TUBE_RADIUS/2) - (playerPosition.position.x + TUBE_RADIUS),
            999
        );
        
        const minSafeDistance = 1.5;
        
        if (closestWallDistance < minSafeDistance) {
            this.proximityScore += (minSafeDistance - closestWallDistance) * 20;
            
            // Camera shake intensity based on danger
            this.cameraShakeIntensity = Math.max(this.cameraShakeIntensity, 
                (minSafeDistance - closestWallDistance) * 5);
        } else {
            this.proximityScore *= 0.98;
            if (this.proximityScore < 1) this.proximityScore = 1;
        }
        
        // Update multiplier based on proximity score
        const dangerLevel = Math.min(1, (20 - closestWallDistance) / 20);
        this.multiplier += dangerLevel * 2;
    }
    
    updateSpeed(speed) {
        // Build boost from speed and distance
        if (speed > 35) {
            this.boostMeter = Math.min(this.boostMeter + 1, this.maxBoost);
        } else {
            this.boostMeter = Math.max(this.boostMeter - 0.5, 0);
        }
        
        // FOV widening based on boost
        const baseFOV = 75;
        const maxFOV = 110;
        this.currentFOV = baseFOV + (maxFOV - baseFOV) * (this.boostMeter / this.maxBoost);
    }
    
    reset() {
        this.multiplier = 1;
        this.proximityScore = 0;
        this.distanceTraveled = 0;
        this.boostMeter = 0;
        this.cameraShakeIntensity = 0;
        this.currentFOV = 75;
    }
    
    onCollect() {
        // Bonus multiplier for collecting items while risky
        if (this.proximityScore > 10) {
            this.multiplier += 2 + Math.random();
        }
    }
}

// ── class: CameraControl ──
class CameraControl {
    constructor() {
        this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        this.cameraPosition = new THREE.Vector3(0, 5, -20);
        
        // Smooth camera follow
        this.targetPosition = new THREE.Vector3();
        this.smoothFactor = 0.1;
    }
    
    update(player) {
        // Calculate ideal camera position based on player movement
        const lookAhead = -player.velocity.z * 2;
        const xOffset = player.mesh.rotation.y * 5;
        
        this.targetPosition.set(
            player.position.x + xOffset,
            player.position.y + 3,
            player.position.z + lookAhead
        );
        
        // Smooth interpolation
        this.cameraPosition.lerp(this.targetPosition, this.smoothFactor);
        
        this.camera.position.copy(this.cameraPosition);
        this.camera.lookAt(
            player.position.x,
            player.position.y,
            player.position.z + 50
        );
    }
    
    applyShake(shakeIntensity) {
        if (shakeIntensity > 0.01) {
            const shakeAmount = THREE.MathUtils.clamp(shakeIntensity, 0, 2);
            this.cameraPosition.x += (Math.random() - 0.5) * shakeAmount;
            this.cameraPosition.y += (Math.random() - 0.5) * shakeAmount;
        }
    }
    
    applyBoostFOV(fovIncrease) {
        const baseFOV = this.camera.fov;
        const maxFOV = Math.min(140, baseFOV + fovIncrease);
        const newFOV = THREE.MathUtils.lerp(baseFOV, maxFOV, 0.5);
        this.camera.fov = newFOV;
    }
}

// ── class: InputHandler ──
class InputHandler {
    constructor() {
        this.keys = {};
        this.mouseX = 0;
        this.mouseY = 0;
        
        // Mouse listeners
        window.addEventListener('mousemove', (e) => {
            const rect = document.getElementById('hud').getBoundingClientRect();
            this.mouseX = e.clientX - rect.left;
            this.mouseY = e.clientY - rect.top;
        });
        
        window.addEventListener('keydown', (e) => {
            this.keys[e.code] = true;
        });
        
        window.addEventListener('keyup', (e) => {
            this.keys[e.code] = false;
        });
        
        // Touch listeners for mobile
        const hud = document.getElementById('hud');
        if (hud) {
            hud.addEventListener('touchstart', (e) => {
                const rect = e.touches[0].target.getBoundingClientRect();
                this.touchX = e.touches[0].clientX - rect.left;
                this.touchY = e.touches[0].clientY - rect.top;
            }, { passive: true });
        }
    }
    
    getPitch() {
        // Mouse Y controls pitch (up/down)
        const sensitivity = 3;
        return -(this.mouseY / window.innerHeight - 0.5) * sensitivity;
    }
    
    getRoll() {
        // Mouse X controls roll/bank (left/right)
        const sensitivity = 2.5;
        return this.mouseX / window.innerWidth - 0.5;
    }
    
    isPitchingUp() {
        return this.keys['ArrowUp'] || this.keys['KeyW'] || this.keys['KeyE'];
    }
    
    isPitchingDown() {
        return this.keys['ArrowDown'] || this.keys['KeyS'] || this.keys['KeyQ'];
    }
    
    getKeyboardRoll() {
        if (this.keys['ArrowLeft'] || this.keys['KeyA']) return -0.8;
        if (this.keys['ArrowRight'] || this.keys['KeyD']) return 0.8;
        return 0;
    }
    
    clearKeys() {
        for (let key in this.keys) {
            this.keys[key] = false;
        }
    }
}

// ── class: World ──
class World {
    constructor() {
        this.scene = new THREE.Scene();
        
        // Use hex color instead of gradient (Three.js doesn't have LinearGradient in older versions)
        const sunsetColor = 0xff6b57;
        this.scene.background = new THREE.Color(sunsetColor);
        this.scene.fog = new THREE.FogExp2(sunsetColor, 0.015);
        
        // Ambient light
        const ambient = new THREE.AmbientLight(0xffffff, 0.3);
        this.scene.add(ambient);
        
        // Directional light (sun) - golden hour lighting
        const sunLight = new THREE.DirectionalLight(0xffaa88, 1.2);
        sunLight.position.set(100, 150, 100);
        this.scene.add(sunLight);
        
        // Add shadows for canyon walls
        sunLight.castShadow = true;
        sunLight.shadow.mapSize.width = 1024;
        sunLight.shadow.mapSize.height = 1024;
        
        this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    }
    
    addLights() {
        // Add rim light for dramatic effect
        const rimLight = new THREE.DirectionalLight(0x6644aa, 0.6);
        rimLight.position.set(-50, -50, 100);
        this.scene.add(rimLight);
        
        // Add fill light from below
        const fillLight = new THREE.PointLight(0xff6b57, 0.5, 200);
        fillLight.position.set(0, -30, -50);
        this.scene.add(fillLight);
    }
}

// ── class: Game ──
class Game {
    constructor() {
        this.world = new World();
        this.scene = this.world.scene;
        this.camera = this.world.camera;
        
        // Create Three.js renderer
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        
        document.getElementById('app').appendChild(renderer.domElement);
        
        // Store renderer for use later
        this.renderer = renderer;
        
        // Setup game state
        this.player = new Player();
        this.physics = new PhysicsWorld();
        this.proceduralGen = new ProceduralGen(this.scene);
        this.energySystem = new EnergySystem(this.scene);
        this.particleSystem = new ParticleSystem(this.scene);
        this.scoring = new ScoringSystem();
        this.cameraControl = new CameraControl();
        this.input = new InputHandler();
        
        // Game parameters
        this.speed = 25;
        this.maxSpeed = 60;
        this.boostActive = false;
        this.energyDecay = 0.3;
        this.distance = 0;
        
        // Camera shake
        this.cameraShakeIntensity = 0;
        
        this.sound = new SoundSynthesizer();
        this.hudPatcher = new HUDPatcher(this);
    }
    
    init() {
        this.sound.startEngine();
        for (let i = 0; i < 5; i++) {
            const zPos = -i * SEGMENT_LENGTH;
            this.proceduralGen.createSegment(zPos);
        }
        
        // Initial energy orb
        setTimeout(() => {
            const firstSegment = this.proceduralGen.segments[0];
            if (firstSegment && firstSegment.mesh) {
                this.energySystem.spawnOrb(this.player.position, firstSegment);
            }
        }, 100);
    }
    
    spawnEnergyOrb() {
        const segment = this.proceduralGen.segments[this.proceduralGen.segments.length - 1];
        if (segment && segment.mesh) {
            const orb = this.energySystem.spawnOrb(this.player.position, segment);
            return orb;
        }
        return null;
    }
    
    spawnDebris() {
        const segment = this.proceduralGen.segments[Math.floor(Math.random() * this.proceduralGen.segments.length)];
        if (!segment || !segment.mesh) return null;
        
        const geometry = new THREE.BoxGeometry(1.5, 1, 1.5);
        const material = new THREE.MeshStandardMaterial({
            color: 0x555555,
            roughness: 0.7,
            metalness: 0.2,
            flatShading: true
        });
        
        const mesh = new THREE.Mesh(geometry, material);
        const x = (Math.random() - 0.5) * 40;
        const y = 3 + Math.random() * 20;
        const z = segment.z - SEGMENT_LENGTH/2 + Math.random() * SEGMENT_LENGTH;
        
        mesh.position.set(x, y, z);
        this.scene.add(mesh);
        segment.mesh.add(mesh);
        return mesh;
    }
    
    applyPlayerControls(dt) {
        this.input.clearKeys();
        
        const pitch = this.input.getPitch();
        const roll = this.input.getRoll() + this.input.getKeyboardRoll();
        
        if (pitch !== undefined || roll !== undefined) {
            this.player.applyInput({ pitch, roll }, dt);
        }
    }
    
    update(dt) {
        // Apply gravity and drag
        this.physics.applyGravity(this.player);
        this.physics.applyDrag(this.player);
        
        // Update player movement
        this.player.update(dt);
        
        // Move player forward based on speed
        const currentSpeed = Math.abs(this.player.velocity.z);
        this.speed = THREE.MathUtils.lerp(this.speed, currentSpeed * 1.2, 0.1);
        
        // Boost effect - check if scoring has boost meter
        const boostThreshold = this.scoring.maxBoost * 0.8;
        if (this.scoring.boostMeter > boostThreshold) {
            this.boostActive = true;
            this.player.velocity.z *= 1.15;
            this.energyDecay *= 2;
        } else {
            this.boostActive = false;
        }
        
        // Update camera
        this.cameraControl.update(this.player);
        this.cameraControl.applyShake(this.cameraShakeIntensity);
        if (this.scoring.boostMeter > this.scoring.maxBoost * 0.9) {
            this.cameraControl.applyBoostFOV(40);
        }
        
        // Update energy decay based on boost state
        this.energyDecay = THREE.MathUtils.lerp(
            this.energyDecay, 
            this.energyDecay + (this.boostActive ? 0.1 : -0.05), 
            0.1
        );
        
        // Spawn new segments as player moves forward
        const playerZ = this.player.position.z;
        const lastSegment = this.proceduralGen.segments[this.proceduralGen.segments.length - 1];
        
        if (lastSegment && lastSegment.mesh) {
            const lastZ = lastSegment.z;
            if (playerZ < lastZ + SEGMENT_LENGTH * 2) {
                const newZ = lastZ + SEGMENT_LENGTH;
                this.proceduralGen.createSegment(newZ);
            }
        }
        
        // Remove old segments
        while (this.proceduralGen.segments[0]) {
            const first = this.proceduralGen.segments[0];
            if (first.z > playerZ + SEGMENT_LENGTH * 3) {
                if (first.mesh) this.scene.remove(first.mesh);
                for (let j = 0; j < first.walls.length && first.walls[j]; j++) {
                    this.scene.remove(first.walls[j]);
                }
                this.proceduralGen.segments.shift();
            } else {
                break;
            }
        }
        
        // Spawn energy orbs randomly
        if (Math.random() < 0.01 && this.proceduralGen.segments.length > 2) {
            const lastSegment = this.proceduralGen.segments[this.proceduralGen.segments.length - 1];
            if (lastSegment && lastSegment.mesh) {
                this.energySystem.spawnOrb(this.player.position, lastSegment);
            }
        }
        
        // Spawn debris for speed illusion
        if (Math.random() < 0.05) {
            this.spawnDebris();
        }
        
        // Update systems
        this.energySystem.update(this.player.position);
        this.particleSystem.update(this.player.position, dt);
        
        // Check collisions with debris
        this.checkCollisions();
        
        // Apply controls
        this.applyPlayerControls(dt);
    }
    
    checkCollisions() {
        const playerBox = new THREE.Box3().setFromObject(this.player.mesh);
        
        for (let i = 0; i < this.proceduralGen.segments.length; i++) {
            const segment = this.proceduralGen.segments[i];
            if (!segment) continue;
            
            // Check wall collisions
            for (let j = 0; j < segment.walls.length && segment.walls[j]; j++) {
                const wall = segment.walls[j];
                const wallBox = new THREE.Box3().setFromObject(wall);
                
                if (playerBox.intersectsBox(wallBox)) {
                    this.handleCollision();
                }
            }
            
            // Check obstacle collisions
            for (let k = 0; k < segment.obstacles.length; k++) {
                const obs = segment.obstacles[k];
                if (!obs.mesh) continue;
                const obsBox = new THREE.Box3().setFromObject(obs.mesh);
                
                if (playerBox.intersectsBox(obsBox)) {
                    this.handleCollision();
                }
            }
        }
    }
    
    handleCollision() {
        // Game over on collision
        gameOver(0);
    }
    
    render() {
        const renderer = this.renderer;
        
        // Camera shake interpolation
        this.cameraShakeIntensity = THREE.MathUtils.lerp(
            this.cameraShakeIntensity, 0, 0.15
        );
        
        renderer.render(this.scene, this.camera);
    }
    
    getGameState() {
        return {
            energy: this.player.velocity.z / this.maxSpeed * 100,
            multiplier: this.scoring.multiplier,
            speed: this.speed,
            boostMeter: this.scoring.boostMeter
        };
    }
}

// ── let: gameState ──
// Game state for rendering updates
let gameState = null;

// ── class: HUDPatcher ──
class HUDPatcher {
    constructor(game) {
        this.game = game;
        this.energyFill = document.getElementById('energy-fill');
        this.speedometer = document.getElementById('speedometer');
        this.multiplier = document.getElementById('multiplier');
    }
    
    update() {
        const state = this.game.getGameState();
        
        // Update energy bar
        if (this.energyFill) {
            this.energyFill.style.width = Math.max(0, state.energy) + '%';
            
            // Change color based on energy level
            if (state.energy > 50) {
                this.energyFill.style.background = 'linear-gradient(90deg, #00ffff, #ff00ff)';
                this.energyFill.style.boxShadow = '0 0 15px rgba(0, 255, 255, 0.8)';
            } else if (state.energy > 20) {
                this.energyFill.style.background = 'linear-gradient(90deg, #ffff00, #ff6b00)';
                this.energyFill.style.boxShadow = '0 0 10px rgba(255, 107, 87, 0.6)';
            } else {
                this.energyFill.style.background = 'linear-gradient(90deg, #ff4400, #cc0000)';
                this.energyFill.style.boxShadow = '0 0 5px rgba(255, 68, 32, 0.5)';
            }
        }
        
        // Update speedometer
        if (this.speedometer) {
            const displaySpeed = Math.floor(this.game.speed);
            this.speedometer.textContent = displaySpeed;
            
            // Color based on speed
            if (displaySpeed > 40) {
                this.speedometer.style.color = '#ff6b35';
                this.speedometer.style.textShadow = '0 0 20px rgba(255, 107, 53, 0.8)';
            } else if (displaySpeed > 30) {
                this.speedometer.style.color = '#ffff00';
                this.speedometer.style.textShadow = '0 0 15px rgba(255, 255, 0, 0.6)';
            } else {
                this.speedometer.style.color = 'rgba(255, 255, 255, 0.9)';
                this.speedometer.style.textShadow = '0 0 20px rgba(255, 255, 255, 0.5)';
            }
        }
        
        // Update multiplier
        if (this.multiplier) {
            const multDisplay = Math.floor(this.game.scoring.multiplier);
            this.multiplier.textContent = 'x' + multDisplay;
            
            // Pulse effect on high multiplier
            if (multDisplay > 15) {
                this.multiplier.classList.add('pulse');
            } else {
                this.multiplier.classList.remove('pulse');
            }
        }
    }
}

// ── class: SoundSynthesizer ──
class SoundSynthesizer {
    constructor() {
        this.ctx = new (window.AudioContext || window.webkitAudioContext)();
        this.masterGain = this.ctx.createGain();
        this.masterGain.gain.value = 0.3;
        this.masterGain.connect(this.ctx.destination);
        
        // Engine hum oscillator
        this.engineOsc = null;
        this.engineGain = null;
    }
    
    startEngine() {
        if (this.ctx.state === 'suspended') {
            this.ctx.resume();
        }
        
        // Low hum for engine
        this.engineOsc = this.ctx.createOscillator();
        this.engineGain = this.ctx.createGain();
        
        this.engineOsc.type = 'sawtooth';
        this.engineOsc.frequency.value = 50;
        
        // Filter to smooth the sound
        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.engineGain.gain.value = 0.1;
        this.engineOsc.start();
    }
    
    stopEngine() {
        if (this.engineOsc) {
            this.engineOsc.stop();
        }
    }
    
    update(engineSpeed) {
        if (this.engineOsc) {
            // Pitch modulate with speed
            const targetFreq = 50 + engineSpeed / 3;
            this.engineOsc.frequency.setTargetAtTime(targetFreq, this.ctx.currentTime, 0.1);
            
            // Volume based on boost
            if (this.game && this.game.scoring.boostActive) {
                this.engineGain.gain.setTargetAtTime(0.2, this.ctx.currentTime, 0.1);
            } else {
                this.engineGain.gain.setTargetAtTime(0.1, this.ctx.currentTime, 0.1);
            }
        }
    }
    
    playCollectSound() {
        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.1);
        
        gain.gain.setValueAtTime(0.5, this.ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.3);
        
        osc.connect(gain);
        gain.connect(this.masterGain);
        
        osc.start();
        osc.stop(this.ctx.currentTime + 0.3);
    }
    
    playNearMissSound() {
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(150, this.ctx.currentTime);
        osc.frequency.exponentialRampToValueAtTime(50, this.ctx.currentTime + 0.4);
        
        gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
        gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.4);
        
        osc.connect(gain);
        gain.connect(this.masterGain);
        
        osc.start();
        osc.stop(this.ctx.currentTime + 0.4);
    }
}

// ── let: dt ──
// Shared time delta variable
let dt = 0;

// ── main ──
// Global variables
let gameLoopId = null;
let lastTime = 0;
let score = 0;
let maxSpeed = 0;
let isGameOver = false;

// Start the game
function initGame() {
    gameState = new Game();
    
    // Initialize player position and velocity directly on mesh
    const startPos = new THREE.Vector3(0, 0, 0);
    gameState.player.position.copy(startPos);
    gameState.player.velocity.set(0, 0, -25);
    gameState.player.acceleration.set(0, 0, 0);
    
    // Bind resize event
    window.addEventListener('resize', () => {
        gameState.camera.aspect = window.innerWidth / window.innerHeight;
        gameState.camera.updateProjectionMatrix();
        const renderer = document.querySelector('#app canvas');
        renderer.setSize(window.innerWidth, window.innerHeight);
    });
    
    // Start animation loop
    lastTime = performance.now();
    requestAnimationFrame(gameLoop);
}

function gameLoop(timestamp) {
    if (isGameOver || !gameState) return;
    
    const currentTime = timestamp / 1000;
    dt = currentTime - lastTime;
    lastTime = currentTime;
    
    // Cap dt to prevent huge jumps
    if (dt > 0.1) {
        dt = 0.1;
        lastTime = currentTime - dt;
    }
    
    // Update game
    gameState.update(dt);
    gameState.hudPatcher.update();
    gameState.render();
    
    // Update score
    const currentSpeed = Math.abs(gameState.player.velocity.z);
    score += currentSpeed * 10 * dt;
    maxSpeed = Math.max(maxSpeed, currentSpeed);
    
    gameLoopId = requestAnimationFrame(gameLoop);
}

// Game over handler
function gameOver(finalScore) {
    isGameOver = true;
    cancelAnimationFrame(gameLoopId);
    
    // Show game over screen
    const hud = document.getElementById('hud');
    if (hud) {
        hud.style.display = 'none';
    }
    
    // Create game over overlay
    const gameOverOverlay = document.createElement('div');
    gameOverOverlay.id = 'game-over';
    gameOverOverlay.className = 'game-over-overlay';
    gameOverOverlay.innerHTML = `
        <h1 style="font-size: 48px; color: #ff6b57; text-shadow: 0 0 20px rgba(255,107,87,0.8); margin-bottom: 20px;">CRASHED</h1>
        <div style="font-size: 24px; color: #fff; text-shadow: 0 0 10px rgba(255,255,255,0.5);">
            <p>Final Score: <strong style="color: #00ffff;">${Math.floor(score)}</strong></p>
            <p>Max Speed: <strong style="color: #ffff00;">${maxSpeed.toFixed(1)} km/h</strong></p>
            <button onclick="initGame()" style="margin-top: 20px; padding: 15px 40px; font-size: 20px; background: linear-gradient(90deg, #ff6b57, #c94f68); color: white; border: none; border-radius: 8px; cursor: pointer; box-shadow: 0 0 20px rgba(255,107,87,0.6);">RESTART</button>
        </div>
    `;
    
    document.getElementById('app').appendChild(gameOverOverlay);
}

// Start on page load
initGame();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.5-9b","provider":"lmstudio","persona":"composer","sessionId":"9ef03626-3c5b-467d-aa05-03a027643d54","tokensIn":1609797,"tokensOut":36098,"tokensTotal":1645895,"turns":54,"toolCalls":53,"failedToolCalls":0,"timestamp":"2026-07-14T03:29:00.875Z"} -->