{"asset_map": {"orb_collect_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/orb_collect_sfx_2c1e05c1-0295-4036-a661-6862488bcec4.mp3", "type": "audio", "prompt": "A bright, high-pitched electronic chime sound effect, synthesized with a quick rising glissando. Satisfying arcade 'ding' for a positive feedback loop, clean and minimalist 8-bit aesthetic, 0.4 seconds long, sharp attack with a smooth decay.", "description": "A bright, satisfying electronic chime for collecting a single ship orb.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2, "loop": false, "animation": null}, "cluster_collect_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/cluster_collect_sfx_696db027-65ab-49d5-b824-e76d4a2382c7.mp3", "type": "audio", "prompt": "A triumphant and layered electronic collection sound. Features a rapid arpeggio or chord swell with a shimmering finish. Richer and more harmonically complex than a single chime, evoking a sense of big reward, arcade style, sci-fi synth textures.", "description": "An impactful, layered celebratory sound for collecting ship clusters.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.38, "loop": false, "animation": null}, "ship_destroy_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/ship_destroy_sfx_9eebca4a-a3c6-4cf3-95fb-662501a4d7e9.mp3", "type": "audio", "prompt": "A punchy, low-poly electronic explosion. Short white noise burst followed by a quick bit-crushed decay. Impactful and sharp but non-abrasive, sci-fi arcade style, suitable for frequent use during swarm collisions.", "description": "A sharp, electronic explosion sound for ship destruction.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2, "loop": false, "animation": null}, "milestone_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/milestone_sfx_5b845263-be19-452f-b4cc-0cfd1c8c0ef8.mp3", "type": "audio", "prompt": "An ascending electronic fanfare for a major achievement. Layered analog synth leads, rising pitch over 1.5 seconds, bright and celebratory mood. Classic arcade power-up feel, high energy, clean production.", "description": "A triumphant ascending fanfare for reaching swarm size milestones.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.17, "loop": false, "animation": null}, "game_over_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/game_over_sfx_bf5432b6-ec54-430d-888d-707efd7530f6.mp3", "type": "audio", "prompt": "A dramatic descending pitch electronic sound effect. Synthesized 'power-down' or 'falling' motif, conveying finality with a sci-fi flair. Melancholy but encouraging 'try again' vibe, clean synth decay without harsh distortion.", "description": "A dramatic descending electronic sound for the game over screen.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.04, "loop": false, "animation": null}, "background_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/background_music_9f2db783-479f-4ae2-8bd1-dba3099fb4d7.mp3", "type": "audio", "prompt": "Energetic synthwave track for a fast-paced 3D arcade game, 140 BPM. Driving 4/4 electronic beat, pulsing side-chained basslines, and bright arpeggiated lead synths. Forward momentum, futuristic sci-fi atmosphere, evocative of traveling through a wormhole. Catchy but repetitive enough for long-term play, seamless loop transition.", "description": "High-energy synthwave track for the main wormhole flight gameplay.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 75.04979591836735, "loop": true, "animation": null}, "menu_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/menu_music_e0d020f0-6de5-4956-98e0-b82eb3fc3edb.mp3", "type": "audio", "prompt": "Ambient synthwave track, calm and atmospheric. Soft pulsing pads, light rhythmic textures, and mysterious melodic hints. Subdued energy compared to gameplay, creating a sense of space and anticipation. 110 BPM, futuristic and clean, seamless loop.", "description": "Subdued ambient synthwave for the main menu and customization screen.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 49.99836734693878, "loop": true, "animation": null}, "lead_ship_model": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_102494898897174822368/1780525401279_01KT7SB15ZTTT899Y9AHSQ8KMH.lowpoly_enemy_drone.glb", "type": "model_3d", "prompt": null, "description": "Low-poly enemy drone 3D model (GLB) used as the lead ship in the swarm", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "virus_enemy_model": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_102494898897174822368/1780528851142_01KT7WMA664A9DBSVJEER2KBJP.virus_enemy.glb", "type": "model_3d", "prompt": null, "description": "Low-poly virus/mine enemy 3D model (GLB) used as a floating drifting hazard in enemy sections. Dark sci-fi style with magenta/pink glow.", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "health_pickup_model": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_102494898897174822368/1780529474382_01KT7X7ATE56EG9D0TZ5R4CRDM.medpack.glb", "type": "model_3d", "prompt": null, "description": "Low-poly medpack/health pickup 3D model (GLB) used as a rare healing collectible in the wormhole tunnel.", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "health_pickup_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/health_pickup_sfx_5999b5c5-98d1-4a07-ab68-9c792f8ee423.mp3", "type": "audio", "prompt": "A bright, shimmering sci-fi healing sound effect. Features a gentle high-pitched chime layered with a soft, ethereal air whoosh. Positive and restorative tonal quality, electronic but smooth, designed for a satisfying player feedback moment. Clean decay, no metallic clashing, fits a high-energy synthwave aesthetic.", "description": "A bright, shimmering healing sound effect for sci-fi health restoration.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.59, "loop": false, "animation": null}, "cyberarac_model": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_102494898897174822368/1780532006423_01KT7ZMKGQ2P0435PHMZFD66HE.cyberarac.glb", "type": "model_3d", "prompt": null, "description": "Low-poly Cyberarac enemy 3D model (GLB) used as a medium-tier enemy in the wormhole tunnel. Patrols side-to-side guarding lanes, more dangerous than Glytchbug.", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "small_glytchbug_model": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_102494898897174822368/1780535154744_01KT82MP1R6N8BCEY3ZEPE076P.small_glytchbug.glb", "type": "model_3d", "prompt": null, "description": "small glytchbug model", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "cleaver_drone_model": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_102494898897174822368/1780536064925_01KT83GEWX45QBBH32NE2ACMH9.cleaver_drone.glb", "type": "model_3d", "prompt": null, "description": "Low-poly Cleaver Drone enemy 3D model (GLB) used as a spinning drone hazard in the wormhole tunnel. Spawns ahead and moves toward the player along Z axis.", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Wormhole Swarm</title>\n    <style>\n        @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@400;600&display=swap');\n\n        :root {\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --ease-out: cubic-bezier(0.0, 0, 0.2, 1);\n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n        }\n\n        body, html {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            -ms-touch-action: none;\n            background: #000;\n            font-family: 'Inter', sans-serif;\n        }\n\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n        }\n\n        canvas {\n            display: block;\n            width: 100%;\n            height: 100%;\n        }\n\n        .ui-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: 10;\n        }\n\n        .ui-overlay > * {\n            pointer-events: auto;\n        }\n\n        .hud {\n            position: absolute;\n            top: var(--space-sm);\n            left: var(--space-sm);\n            right: var(--space-sm);\n            display: flex;\n            justify-content: space-between;\n            align-items: flex-start;\n            z-index: 5;\n        }\n\n        .hud-center {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            position: absolute;\n            left: 50%;\n            transform: translateX(-50%);\n            width: 100%;\n            max-width: 300px;\n        }\n\n        .hud-energy-bar {\n            width: 100%;\n            max-width: 280px;\n            height: 10px;\n            background: rgba(0, 0, 0, 0.6);\n            border: 1px solid rgba(0, 255, 255, 0.3);\n            border-radius: 5px;\n            overflow: hidden;\n            margin-top: 6px;\n        }\n\n        .hud-energy-fill {\n            height: 100%;\n            width: 100%;\n            background: linear-gradient(90deg, #0f0, #0ff);\n            border-radius: 5px;\n            transition: width 0.3s ease-out, background 0.3s;\n        }\n\n        .hud-energy-fill.low {\n            background: linear-gradient(90deg, #ff4400, #ff8800);\n        }\n\n        .hud-energy-fill.mid {\n            background: linear-gradient(90deg, #ffaa00, #ffdd00);\n        }\n\n        .hud-energy-percent {\n            font-family: 'Orbitron', sans-serif;\n            font-size: 18px;\n            color: rgba(255,255,255,0.7);\n            margin-top: 2px;\n        }\n\n        .hud-action-btns {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 6px;\n            margin-left: 8px;\n        }\n\n        .hud-action-btn {\n            width: 40px;\n            height: 40px;\n            background: rgba(0,0,0,0.6);\n            border: 1px solid rgba(0,255,255,0.3);\n            border-radius: 8px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            color: #0ff;\n            font-size: 18px;\n            transition: background 150ms, transform 150ms;\n            pointer-events: auto;\n            user-select: none;\n        }\n\n        .hud-action-btn:active {\n            transform: scale(0.9);\n            background: rgba(0,255,255,0.15);\n        }\n\n        .pause-overlay {\n            position: absolute;\n            top: 0; left: 0; right: 0; bottom: 0;\n            background: rgba(0,0,0,0.75);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            opacity: 0;\n            pointer-events: none;\n            transition: opacity 300ms;\n            z-index: 25;\n        }\n\n        .pause-overlay.show {\n            opacity: 1;\n            pointer-events: auto;\n        }\n\n        .pause-title {\n            font-family: 'Orbitron', sans-serif;\n            font-size: 52px;\n            font-weight: 900;\n            color: #0ff;\n            text-shadow: 0 0 20px rgba(0,255,255,0.5);\n            margin-bottom: 40px;\n        }\n\n        .pause-btn {\n            font-family: 'Orbitron', sans-serif;\n            font-size: 26px;\n            padding: 14px 40px;\n            background: linear-gradient(135deg, #0ff, #006);\n            color: #fff;\n            border: 2px solid #0ff;\n            border-radius: 10px;\n            cursor: pointer;\n            text-shadow: 0 0 8px rgba(0,255,255,0.6);\n            margin: 8px;\n            transition: transform 150ms, box-shadow 150ms;\n            min-width: 200px;\n        }\n\n        .pause-btn:hover { transform: scale(1.05); box-shadow: 0 0 20px rgba(0,255,255,0.4); }\n        .pause-btn:active { transform: scale(0.95); }\n\n        .hud-item {\n            background: rgba(0, 0, 0, 0.6);\n            border: 1px solid rgba(0, 255, 255, 0.3);\n            border-radius: 8px;\n            padding: 8px 16px;\n            color: #fff;\n            font-family: 'Orbitron', sans-serif;\n            font-size: 20px;\n            text-shadow: 0 0 8px rgba(0, 255, 255, 0.5);\n        }\n\n        .hud-score {\n            font-size: 28px;\n            color: #0ff;\n        }\n\n        .hud-swarm {\n            font-size: 22px;\n            color: #0f0;\n        }\n\n\n        .milestone-popup {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%) scale(0);\n            font-family: 'Orbitron', sans-serif;\n            font-size: 48px;\n            color: #ffd700;\n            text-shadow: 0 0 20px rgba(255, 215, 0, 0.8), 0 0 40px rgba(255, 215, 0, 0.4);\n            white-space: nowrap;\n            opacity: 0;\n            transition: none;\n            pointer-events: none;\n        }\n\n        .milestone-popup.show {\n            animation: milestoneAnim 1.5s ease-out forwards;\n        }\n\n        @keyframes milestoneAnim {\n            0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }\n            20% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; }\n            40% { transform: translate(-50%, -50%) scale(1.0); opacity: 1; }\n            100% { transform: translate(-50%, -50%) scale(1.5); opacity: 0; }\n        }\n\n        .game-over-screen {\n            position: absolute;\n            top: 0; left: 0; right: 0; bottom: 0;\n            background: rgba(0, 0, 0, 0.85);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            opacity: 0;\n            transition: opacity var(--transition-normal) var(--ease-out);\n            pointer-events: none;\n        }\n\n        .game-over-screen.show {\n            opacity: 1;\n            pointer-events: auto;\n        }\n\n        .game-over-title {\n            font-family: 'Orbitron', sans-serif;\n            font-size: 52px;\n            font-weight: 900;\n            color: #ff4444;\n            text-shadow: 0 0 20px rgba(255, 0, 0, 0.5);\n            margin-bottom: var(--space-lg);\n        }\n\n        .game-over-stats {\n            text-align: center;\n            margin-bottom: var(--space-xl);\n        }\n\n        .game-over-stats p {\n            font-family: 'Orbitron', sans-serif;\n            font-size: 24px;\n            color: #ccc;\n            margin: 8px 0;\n        }\n\n        .game-over-stats .value {\n            color: #0ff;\n            font-size: 32px;\n        }\n\n        .restart-btn {\n            font-family: 'Orbitron', sans-serif;\n            font-size: 28px;\n            padding: 16px 48px;\n            background: linear-gradient(135deg, #0ff, #00a);\n            color: #fff;\n            border: 2px solid #0ff;\n            border-radius: 8px;\n            cursor: pointer;\n            text-shadow: 0 0 8px rgba(0, 255, 255, 0.5);\n            transition: transform var(--transition-fast) var(--ease-out), box-shadow var(--transition-fast);\n        }\n\n        .restart-btn:hover {\n            transform: scale(1.05);\n            box-shadow: 0 0 20px rgba(0, 255, 255, 0.5);\n        }\n\n        .restart-btn:active {\n            transform: scale(0.95);\n        }\n\n        .start-screen {\n            position: absolute;\n            top: 0; left: 0; right: 0; bottom: 0;\n            background: rgba(0, 0, 0, 0.95);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 20;\n        }\n\n        .start-title {\n            font-family: 'Orbitron', sans-serif;\n            font-size: 72px;\n            font-weight: 900;\n            color: #0ff;\n            text-shadow: 0 0 30px rgba(0, 255, 255, 0.6);\n            margin-bottom: 16px;\n            text-align: center;\n        }\n\n        .start-subtitle {\n            font-family: 'Inter', sans-serif;\n            font-size: 22px;\n            color: #aaa;\n            margin-bottom: var(--space-xl);\n            text-align: center;\n            padding: 0 32px;\n        }\n\n        .start-btn {\n            font-family: 'Orbitron', sans-serif;\n            font-size: 32px;\n            padding: 20px 64px;\n            background: linear-gradient(135deg, #0ff, #006);\n            color: #fff;\n            border: 2px solid #0ff;\n            border-radius: 12px;\n            cursor: pointer;\n            text-shadow: 0 0 10px rgba(0, 255, 255, 0.8);\n            transition: transform var(--transition-fast) var(--ease-out), box-shadow var(--transition-fast);\n            min-width: 96px;\n            min-height: 96px;\n        }\n\n        .start-btn:hover {\n            transform: scale(1.05);\n            box-shadow: 0 0 30px rgba(0, 255, 255, 0.6);\n        }\n\n        .start-btn:active {\n            transform: scale(0.95);\n        }\n\n        .joystick-area {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: auto;\n            z-index: 0;\n        }\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <div class=\"ui-overlay\" id=\"ui-overlay\">\n            <div class=\"hud\" id=\"hud\" style=\"display:none;\">\n                <div class=\"hud-item hud-score\" id=\"hud-score\">0</div>\n                <div class=\"hud-center\">\n                    <div class=\"hud-energy-bar\" id=\"energy-bar\">\n                        <div class=\"hud-energy-fill\" id=\"energy-bar-fill\"></div>\n                    </div>\n                    <div class=\"hud-energy-percent\" id=\"energy-percent\">100%</div>\n                </div>\n                <div style=\"display:flex;flex-direction:column;align-items:center;\">\n                    <div class=\"hud-item hud-swarm\" id=\"hud-swarm\">\u2b21 3</div>\n                    <div class=\"hud-action-btns\" style=\"flex-direction:row;margin-top:6px;\">\n                        <div class=\"hud-action-btn\" id=\"pause-btn\" title=\"Pause\">\u23f8</div>\n                        <div class=\"hud-action-btn\" id=\"replay-btn\" title=\"Restart\">\u21ba</div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"milestone-popup\" id=\"milestone-popup\"></div>\n            <div class=\"game-over-screen\" id=\"game-over\">\n                <div class=\"game-over-title\">GAME OVER</div>\n                <div class=\"game-over-stats\">\n                    <p>Score</p>\n                    <p class=\"value\" id=\"final-score\">0</p>\n                    <p>Max Swarm</p>\n                    <p class=\"value\" id=\"final-swarm\">0</p>\n                </div>\n                <button class=\"restart-btn\" id=\"restart-btn\">RETRY</button>\n                <button class=\"restart-btn\" id=\"gameover-leaderboard-btn\" style=\"margin-top:12px;font-size:22px;\">\ud83c\udfc6 LEADERBOARD</button>\n            </div>\n            <div class=\"pause-overlay\" id=\"pause-overlay\">\n                <div class=\"pause-title\">PAUSED</div>\n                <button class=\"pause-btn\" id=\"pause-resume-btn\">RESUME</button>\n                <button class=\"pause-btn\" id=\"pause-restart-btn\">RESTART</button>\n                <button class=\"pause-btn\" id=\"pause-leaderboard-btn\">\ud83c\udfc6 LEADERBOARD</button>\n            </div>\n            <div class=\"start-screen\" id=\"start-screen\">\n                <canvas id=\"start-bg-canvas\" style=\"position:absolute;top:0;left:0;width:100%;height:100%;z-index:0;\"></canvas>\n                <div class=\"start-title\" style=\"position:relative;z-index:1;\">WORMHOLE<br>SWARM</div>\n                <div class=\"start-subtitle\" style=\"position:relative;z-index:1;\">Grow your swarm. Dodge hazards. Survive the wormhole.</div>\n                <button class=\"start-btn\" id=\"start-btn\" style=\"position:relative;z-index:1;\">PLAY</button>\n                <button class=\"pause-btn\" id=\"start-leaderboard-btn\" style=\"position:relative;z-index:1;margin-top:16px;font-size:22px;\">\ud83c\udfc6 LEADERBOARD</button>\n            </div>\n            <div class=\"joystick-area\" id=\"joystick-area\" style=\"display:none;\"></div>\n        </div>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js\"></script>\n    <script>\n    /*\n     * WORMHOLE SWARM\n     * A 3D low-poly arcade game where you control a growing swarm of ships\n     * through an infinite wormhole tunnel. Collect orbs to grow, dodge hazards.\n     * Built with Three.js for 3D rendering.\n     */\n\n    async function run(mode) {\n        const config = window.gameConfig;\n        const gameWorld = document.getElementById('game-world');\n\n        // Audio context\n        let audioCtx = null;\n        const audioBuffers = {};\n\n        function initAudio() {\n            if (!audioCtx) {\n                audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n            }\n        }\n\n        async function loadAudio(id) {\n            try {\n                const asset = lib.getAsset(id);\n                if (!asset || asset.type !== 'audio') return;\n                const response = await fetch(asset.url);\n                const arrayBuffer = await response.arrayBuffer();\n                audioBuffers[id] = await audioCtx.decodeAudioData(arrayBuffer);\n            } catch (e) {\n                lib.log('Audio load failed: ' + id);\n            }\n        }\n\n        function playSound(id, volume = 0.5, loop = false) {\n            if (!audioCtx || !audioBuffers[id]) return null;\n            const source = audioCtx.createBufferSource();\n            const gain = audioCtx.createGain();\n            source.buffer = audioBuffers[id];\n            source.loop = loop;\n            gain.gain.value = volume;\n            source.connect(gain);\n            gain.connect(audioCtx.destination);\n            source.start(0);\n            return { source, gain };\n        }\n\n        // Show game parameters\n        lib.showGameParameters({\n            name: 'Wormhole Swarm',\n            params: {\n                'Ship Color': { key: 'ship.primaryColor', type: 'color' },\n                'Trail Color': { key: 'ship.trailColor', type: 'color' },\n                'Starting Ships': { key: 'swarm.startSize', type: 'slider', min: 1, max: 10, step: 1 },\n                'Clone Z': { key: 'swarm.cloneZ', type: 'slider', min: 0, max: 6, step: 0.1 },\n                'Follow Distance': { key: 'swarm.followDistance', type: 'slider', min: 0, max: 6, step: 0.1 },\n                'Swarm Spread': { key: 'swarm.spread', type: 'slider', min: 0.0, max: 1.0, step: 0.05 },\n                'Tunnel Color': { key: 'tunnel.color', type: 'color' },\n                'Energy Color': { key: 'tunnel.energyColor', type: 'color' },\n                'Starting Speed': { key: 'gameplay.startSpeed', type: 'slider', min: 0.5, max: 1.5, step: 0.1 },\n                'Difficulty Scale': { key: 'gameplay.difficultyScale', type: 'slider', min: 0.5, max: 2.0, step: 0.1 },\n                'Collectible Freq': { key: 'gameplay.collectibleFreq', type: 'slider', min: 0.5, max: 2.0, step: 0.1 },\n                'Camera Distance': { key: 'gameplay.cameraZ', type: 'slider', min: 4, max: 16, step: 0.5 },\n                'Lead Ship Size': { key: 'models.leadShipScale', type: 'slider', min: 0.05, max: 0.5, step: 0.025 },\n                'Virus Size': { key: 'models.virusScale', type: 'slider', min: 0.1, max: 4.0, step: 0.05 },\n                'Health Pickup Size': { key: 'models.healthPickupScale', type: 'slider', min: 0.001, max: 0.010, step: 0.001 },\n                'Cyberarac Size': { key: 'models.cyberaracScale', type: 'slider', min: 0.0001, max: 0.0030, step: 0.0001 },\n                'Cleaver Drone Size': { key: 'models.cleaverDroneScale', type: 'slider', min: 0.05, max: 1.0, step: 0.05 }\n            }\n        });\n\n        // Three.js setup\n        const scene = new THREE.Scene();\n        const camera = new THREE.PerspectiveCamera(75, gameWorld.clientWidth / gameWorld.clientHeight, 0.1, 200);\n        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });\n        renderer.setSize(gameWorld.clientWidth, gameWorld.clientHeight);\n        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n        renderer.setClearColor(0x050510);\n        gameWorld.insertBefore(renderer.domElement, gameWorld.firstChild);\n\n        window.addEventListener('resize', () => {\n            const w = gameWorld.clientWidth;\n            const h = gameWorld.clientHeight;\n            camera.aspect = w / h;\n            camera.updateProjectionMatrix();\n            renderer.setSize(w, h);\n        });\n\n        // Lighting\n        const ambientLight = new THREE.AmbientLight(0x334466, 0.7);\n        scene.add(ambientLight);\n        const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);\n        dirLight.position.set(2, 8, 10);\n        scene.add(dirLight);\n        const cyanPoint = new THREE.PointLight(0x00ffff, 0.4, 30);\n        cyanPoint.position.set(0, 2, 10);\n        scene.add(cyanPoint);\n\n        // Constants\n        const TUNNEL_RADIUS = 8;\n        const TUNNEL_SEGMENT_LENGTH = 40;\n        const SHIP_SIZE = 0.6;\n        const PLAYER_Z = 2.0;\n        const INTERACTION_END_Z = 3;\n        const FADE_START_Z = 3;\n        const FADE_END_Z = 3.5;\n        const REMOVE_Z = 5;\n\n        function prepareClonedModelForSpawn(mesh) {\n            mesh.visible = true;\n\n            mesh.traverse(child => {\n                if (child.isMesh && child.material) {\n                    if (Array.isArray(child.material)) {\n                        child.material = child.material.map(mat => {\n                            const cloned = mat.clone();\n                            cloned.transparent = false;\n                            cloned.opacity = 1;\n                            cloned.depthWrite = true;\n                            return cloned;\n                        });\n                    } else {\n                        child.material = child.material.clone();\n                        child.material.transparent = false;\n                        child.material.opacity = 1;\n                        child.material.depthWrite = true;\n                    }\n                    child.visible = true;\n                }\n            });\n        }\n\n        function createCenteredModelWrapper(gltfScene) {\n            const wrapper = new THREE.Group();\n            const model = gltfScene;\n            wrapper.add(model);\n\n            const box = new THREE.Box3().setFromObject(model);\n            const center = new THREE.Vector3();\n            box.getCenter(center);\n\n            model.position.sub(center);\n\n            return wrapper;\n        }\n\n        // ---- TUNNEL ----\n        function createTunnelSegment(zOffset) {\n            const tunnelColor = new THREE.Color(config.tunnel.color);\n            const energyColor = new THREE.Color(config.tunnel.energyColor);\n            const geo = new THREE.CylinderGeometry(TUNNEL_RADIUS, TUNNEL_RADIUS, TUNNEL_SEGMENT_LENGTH, 24, 8, true);\n            const mat = new THREE.MeshBasicMaterial({\n                color: tunnelColor,\n                side: THREE.BackSide,\n                wireframe: false\n            });\n            const mesh = new THREE.Mesh(geo, mat);\n            mesh.rotation.x = Math.PI / 2;\n            mesh.position.z = zOffset - TUNNEL_SEGMENT_LENGTH / 2;\n\n            // Energy lines\n            const lineGeo = new THREE.CylinderGeometry(TUNNEL_RADIUS - 0.05, TUNNEL_RADIUS - 0.05, TUNNEL_SEGMENT_LENGTH, 24, 8, true);\n            const lineMat = new THREE.MeshBasicMaterial({\n                color: energyColor,\n                side: THREE.BackSide,\n                wireframe: true,\n                transparent: true,\n                opacity: 0.15\n            });\n            const lineMesh = new THREE.Mesh(lineGeo, lineMat);\n            lineMesh.rotation.x = Math.PI / 2;\n            lineMesh.position.z = zOffset - TUNNEL_SEGMENT_LENGTH / 2;\n\n            const group = new THREE.Group();\n            group.add(mesh);\n            group.add(lineMesh);\n            group.userData = { zStart: zOffset, zEnd: zOffset - TUNNEL_SEGMENT_LENGTH };\n            return group;\n        }\n\n        // ---- SHIP GEOMETRY ----\n        function createShipGeometry() {\n            const geo = new THREE.ConeGeometry(SHIP_SIZE * 0.4, SHIP_SIZE, 4);\n            geo.rotateX(Math.PI / 2);\n            return geo;\n        }\n\n        const shipGeo = createShipGeometry();\n        let leadShipModel = null;\n\n        // Load health pickup 3D model\n        let healthPickupModel = null;\n        const healthPickupAsset = lib.getAsset('health_pickup_model');\n        if (healthPickupAsset) {\n            const hpLoader = new THREE.GLTFLoader();\n            hpLoader.load(healthPickupAsset.url, (gltf) => {\n                healthPickupModel = gltf.scene;\n                healthPickupModel.scale.setScalar(config.models.healthPickupScale);\n                healthPickupModel.traverse((child) => {\n                    if (child.isMesh && child.material) {\n                        child.material = child.material.clone();\n                        child.material.transparent = false;\n                        child.material.opacity = 1;\n                        child.material.depthWrite = true;\n                    }\n                });\n                lib.log('Health pickup model loaded');\n            });\n        }\n\n        // Load virus enemy 3D model\n        let virusEnemyModel = null;\n        let virusEnemyAnimations = null;\n        const virusEnemyAsset = lib.getAsset('virus_enemy_model');\n        if (virusEnemyAsset) {\n            const vLoader = new THREE.GLTFLoader();\n            vLoader.load(virusEnemyAsset.url, (gltf) => {\n                virusEnemyModel = createCenteredModelWrapper(gltf.scene);\n                virusEnemyModel.scale.setScalar(SHIP_SIZE * config.models.virusScale);\n                // Apply magenta/pink emissive glow to all meshes\n                virusEnemyModel.traverse((child) => {\n                    if (child.isMesh && child.material) {\n                        child.material = child.material.clone();\n                        child.material.transparent = false;\n                        child.material.opacity = 1;\n                        child.material.depthWrite = true;\n                    }\n                });\n                // Store animations if available\n                if (gltf.animations && gltf.animations.length > 0) {\n                    virusEnemyAnimations = gltf.animations;\n                }\n                lib.log('Virus enemy model loaded' + (virusEnemyAnimations ? ' with ' + virusEnemyAnimations.length + ' animations' : ''));\n            });\n        }\n\n        // Load Cyberarac enemy 3D model\n        let cyberaracModel = null;\n        let cyberaracAnimations = null;\n        const cyberaracAsset = lib.getAsset('cyberarac_model');\n        if (cyberaracAsset) {\n            const cLoader = new THREE.GLTFLoader();\n            cLoader.load(cyberaracAsset.url, (gltf) => {\n                cyberaracModel = createCenteredModelWrapper(gltf.scene);\n                cyberaracModel.scale.setScalar(config.models.cyberaracScale);\n                // Apply dark metallic material with subtle emissive\n                cyberaracModel.traverse((child) => {\n                    if (child.isMesh && child.material) {\n                        child.material = child.material.clone();\n                        child.material.transparent = false;\n                        child.material.opacity = 1;\n                        child.material.depthWrite = true;\n                    }\n                });\n                // Store animations if available\n                if (gltf.animations && gltf.animations.length > 0) {\n                    cyberaracAnimations = gltf.animations;\n                }\n                lib.log('Cyberarac model loaded' + (cyberaracAnimations ? ' with ' + cyberaracAnimations.length + ' animations' : ''));\n            });\n        }\n\n        // Load Cleaver Drone enemy 3D model\n        let cleaverDroneModel = null;\n        const cleaverDroneAsset = lib.getAsset('cleaver_drone_model');\n        if (cleaverDroneAsset) {\n            const cdLoader = new THREE.GLTFLoader();\n            cdLoader.load(cleaverDroneAsset.url, (gltf) => {\n                cleaverDroneModel = createCenteredModelWrapper(gltf.scene);\n                cleaverDroneModel.scale.setScalar(config.models.cleaverDroneScale);\n                // Preserve original materials - just clone them on the template\n                cleaverDroneModel.traverse((child) => {\n                    if (child.isMesh && child.material) {\n                        child.material = child.material.clone();\n                        child.material.transparent = false;\n                        child.material.opacity = 1;\n                        child.material.depthWrite = true;\n                    }\n                });\n                lib.log('Cleaver Drone model loaded');\n            });\n        }\n\n        // Load lead ship 3D model\n        const leadShipAsset = lib.getAsset('lead_ship_model');\n        if (leadShipAsset) {\n            const loader = new THREE.GLTFLoader();\n            loader.load(leadShipAsset.url, (gltf) => {\n                leadShipModel = gltf.scene;\n                leadShipModel.scale.setScalar(SHIP_SIZE * config.models.leadShipScale);\n                leadShipModel.rotation.set(0, Math.PI, 0);\n                // Replace lead ship mesh if swarm exists\n                if (swarm.length > 0 && swarm[0].mesh) {\n                    const oldMesh = swarm[0].mesh;\n                    const pos = oldMesh.position.clone();\n                    scene.remove(oldMesh);\n                    const newMesh = leadShipModel.clone();\n                    newMesh.rotation.set(0, Math.PI, 0);\n                    newMesh.position.copy(pos);\n                    scene.add(newMesh);\n                    swarm[0].mesh = newMesh;\n                    swarm[0].isModel = true;\n                }\n            });\n        }\n\n        function createShipMesh(isLead) {\n            if (isLead && leadShipModel) {\n                const mesh = leadShipModel.clone();\n                mesh.scale.setScalar(SHIP_SIZE * config.models.leadShipScale);\n                mesh.rotation.set(0, Math.PI, 0);\n                return mesh;\n            }\n            const color = new THREE.Color(config.ship.primaryColor);\n            const mat = new THREE.MeshLambertMaterial({ color: color, emissive: new THREE.Color(0x004444), flatShading: true });\n            const mesh = new THREE.Mesh(shipGeo, mat);\n            return mesh;\n        }\n\n        // ---- COLLECTIBLES ----\n        function createOrb(pos) {\n            const geo = new THREE.IcosahedronGeometry(0.4, 0);\n            const mat = new THREE.MeshLambertMaterial({ color: 0x00ff88, emissive: new THREE.Color(0x003322), flatShading: true });\n            const mesh = new THREE.Mesh(geo, mat);\n            mesh.position.copy(pos);\n            mesh.userData = { type: 'orb', collected: false };\n            return mesh;\n        }\n\n        function createCluster(pos) {\n            const group = new THREE.Group();\n            for (let i = 0; i < 4; i++) {\n                const geo = new THREE.IcosahedronGeometry(0.25, 0);\n                const mat = new THREE.MeshLambertMaterial({ color: 0xffdd00, emissive: new THREE.Color(0x332200), flatShading: true });\n                const m = new THREE.Mesh(geo, mat);\n                const angle = (i / 4) * Math.PI * 2;\n                m.position.set(Math.cos(angle) * 0.5, Math.sin(angle) * 0.5, 0);\n                group.add(m);\n            }\n            group.position.copy(pos);\n            group.userData = { type: 'cluster', collected: false };\n            return group;\n        }\n\n        // ---- OBSTACLES ----\n        function createSpike(pos, direction) {\n            const geo = new THREE.ConeGeometry(0.5, 2.5, 4);\n            const mat = new THREE.MeshLambertMaterial({ color: 0xff4400, emissive: new THREE.Color(0x331100), flatShading: true });\n            const mesh = new THREE.Mesh(geo, mat);\n            mesh.position.copy(pos);\n            mesh.lookAt(new THREE.Vector3(0, 0, pos.z));\n            mesh.userData = { type: 'spike', radius: 0.8 };\n            return mesh;\n        }\n\n        function createBarrier(pos, rotSpeed) {\n            const geo = new THREE.BoxGeometry(6, 0.4, 0.4);\n            const mat = new THREE.MeshLambertMaterial({ color: 0xff2200, emissive: new THREE.Color(0x331100), flatShading: true });\n            const mesh = new THREE.Mesh(geo, mat);\n            mesh.position.copy(pos);\n            mesh.userData = { type: 'barrier', rotSpeed: rotSpeed, radius: 3.0 };\n            return mesh;\n        }\n\n        // ---- PARTICLES ----\n        class ParticleSystem {\n            constructor() {\n                this.particles = [];\n            }\n            emit(pos, color, count = 10, speed = 3) {\n                for (let i = 0; i < count; i++) {\n                    this.particles.push({\n                        x: pos.x, y: pos.y, z: pos.z,\n                        vx: (Math.random() - 0.5) * speed,\n                        vy: (Math.random() - 0.5) * speed,\n                        vz: (Math.random() - 0.5) * speed,\n                        life: 1.0,\n                        color: color,\n                        size: 0.1 + Math.random() * 0.2\n                    });\n                }\n            }\n            update(dt) {\n                for (let i = this.particles.length - 1; i >= 0; i--) {\n                    const p = this.particles[i];\n                    p.x += p.vx * dt;\n                    p.y += p.vy * dt;\n                    p.z += p.vz * dt;\n                    p.life -= dt * 2;\n                    if (p.life <= 0) this.particles.splice(i, 1);\n                }\n            }\n            render(scene) {\n                // Use simple points - update existing or create\n                // For simplicity, we'll handle this with a points buffer\n            }\n        }\n\n        // Simple particle rendering with sprites\n        const particleMeshes = [];\n        const particlePool = [];\n        const MAX_PARTICLES = 400;\n        const particleGeo = new THREE.SphereGeometry(0.1, 4, 4);\n\n        function getParticleMesh(color) {\n            if (particlePool.length > 0) {\n                const m = particlePool.pop();\n                m.material.color.set(color);\n                m.visible = true;\n                scene.add(m);\n                return m;\n            }\n            if (particleMeshes.length >= MAX_PARTICLES) return null;\n            const mat = new THREE.MeshBasicMaterial({ color: color });\n            const mesh = new THREE.Mesh(particleGeo, mat);\n            particleMeshes.push(mesh);\n            scene.add(mesh);\n            return mesh;\n        }\n\n        const activeParticles = [];\n\n        function emitParticles(pos, color, count = 8, speed = 4) {\n            for (let i = 0; i < count; i++) {\n                const mesh = getParticleMesh(color);\n                if (!mesh) break;\n                mesh.position.set(pos.x, pos.y, pos.z);\n                activeParticles.push({\n                    mesh,\n                    vx: (Math.random() - 0.5) * speed,\n                    vy: (Math.random() - 0.5) * speed,\n                    vz: (Math.random() - 0.5) * speed * 0.5,\n                    life: 1.0\n                });\n            }\n        }\n\n        function updateParticles(dt) {\n            for (let i = activeParticles.length - 1; i >= 0; i--) {\n                const p = activeParticles[i];\n                p.mesh.position.x += p.vx * dt;\n                p.mesh.position.y += p.vy * dt;\n                p.mesh.position.z += p.vz * dt;\n                p.life -= dt * 2.5;\n                p.mesh.scale.setScalar(Math.max(0, p.life));\n                if (p.life <= 0) {\n                    p.mesh.visible = false;\n                    scene.remove(p.mesh);\n                    particlePool.push(p.mesh);\n                    activeParticles.splice(i, 1);\n                }\n            }\n        }\n\n        // ===== GAME STATE =====\n        let gameState = 'start'; // start, playing, gameover\n        let score = 0;\n        let maxSwarmSize = 0;\n        let distance = 0;\n        let currentSpeed = 10;\n        let difficulty = 1;\n        let nextMilestone = 10;\n\n        // Swarm\n        const swarm = [];\n        let leadX = 0, leadY = 0;\n        let targetX = 0, targetY = 0;\n\n        // Lead ship tilt state\n        const LEAD_SHIP_YAW_OFFSET = Math.PI;\n        let currentShipYaw = LEAD_SHIP_YAW_OFFSET;\n        let currentShipPitch = 0;\n        let currentShipRoll = 0;\n\n        // Tunnel segments\n        const tunnelSegments = [];\n        let tunnelZFront = 0;\n\n        // Collectibles and obstacles\n        const collectibles = [];\n        const obstacles = [];\n        const enemies = [];\n        const healthPickups = [];\n        const MAX_ACTIVE_ENEMIES = 3;\n        let lastSpawnZ = 0;\n        let sectionCounter = 0; // tracks spawn calls for section alternation\n\n        // Input\n        let energy = 100;\n        let invincible = false;\n        let invincibleTimer = 0;\n        let inputX = 0, inputY = 0;\n        let joystickActive = false;\n        let joystickStartX = 0, joystickStartY = 0;\n        let joystickTouchId = null;\n\n        // Music\n        let bgMusic = null;\n\n        // Warp streaks\n        const warpStreaks = [];\n        const MAX_STREAKS = 60;\n        const streakGeo = new THREE.BufferGeometry();\n        const streakPositions = new Float32Array(MAX_STREAKS * 6);\n        const streakColors = new Float32Array(MAX_STREAKS * 6);\n        streakGeo.setAttribute('position', new THREE.BufferAttribute(streakPositions, 3));\n        streakGeo.setAttribute('color', new THREE.BufferAttribute(streakColors, 3));\n        const streakMat = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.6 });\n        const streakLines = new THREE.LineSegments(streakGeo, streakMat);\n        scene.add(streakLines);\n\n        // ---- HOLOGRAPHIC GRID FLOOR ----\n        const FLOOR_Y = -4.5;\n        const floorGeo = new THREE.PlaneGeometry(TUNNEL_RADIUS * 2.5, 160, 20, 80);\n        const floorMat = new THREE.ShaderMaterial({\n            transparent: true,\n            depthWrite: false,\n            uniforms: {\n                uTime: { value: 0 },\n                uColor1: { value: new THREE.Color(0x220044) },\n                uColor2: { value: new THREE.Color(0x004466) }\n            },\n            vertexShader: `\n                varying vec2 vUv;\n                varying vec3 vPos;\n                void main() {\n                    vUv = uv;\n                    vPos = position;\n                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n                }\n            `,\n            fragmentShader: `\n                uniform float uTime;\n                uniform vec3 uColor1;\n                uniform vec3 uColor2;\n                varying vec2 vUv;\n                varying vec3 vPos;\n                void main() {\n                    // Grid lines\n                    float gridX = abs(fract(vPos.x * 0.5) - 0.5);\n                    float gridZ = abs(fract((vPos.y + uTime * 2.0) * 0.25) - 0.5);\n                    float lineX = smoothstep(0.02, 0.04, gridX);\n                    float lineZ = smoothstep(0.02, 0.04, gridZ);\n                    float grid = 1.0 - lineX * lineZ;\n                    // Fade at edges\n                    float edgeFade = 1.0 - smoothstep(0.3, 0.5, abs(vUv.x - 0.5) * 2.0);\n                    float depthFade = smoothstep(0.0, 0.3, vUv.y) * (1.0 - smoothstep(0.7, 1.0, vUv.y));\n                    vec3 col = mix(uColor1, uColor2, grid);\n                    float alpha = grid * 0.18 * edgeFade * depthFade;\n                    gl_FragColor = vec4(col, alpha);\n                }\n            `\n        });\n        const floorPlane = new THREE.Mesh(floorGeo, floorMat);\n        floorPlane.rotation.x = -Math.PI / 2;\n        floorPlane.position.set(0, FLOOR_Y, -60);\n        scene.add(floorPlane);\n\n        // ---- SHADOW MARKERS (projection onto floor) ----\n        const MAX_SHADOW_MARKERS = 40;\n        const shadowMarkers = [];\n        const shadowMarkerGeo = new THREE.CircleGeometry(0.5, 16);\n        const shadowMarkerMat = new THREE.MeshBasicMaterial({\n            color: 0x00ffcc,\n            transparent: true,\n            opacity: 0.25,\n            depthWrite: false\n        });\n\n        for (let i = 0; i < MAX_SHADOW_MARKERS; i++) {\n            const m = new THREE.Mesh(shadowMarkerGeo, shadowMarkerMat.clone());\n            m.rotation.x = -Math.PI / 2;\n            m.position.set(0, FLOOR_Y + 0.01, 0);\n            m.visible = false;\n            scene.add(m);\n            shadowMarkers.push(m);\n        }\n\n        // ---- VERTICAL GUIDE LINES (object to shadow) ----\n        const MAX_GUIDE_LINES = 40;\n        const guideLines = [];\n        const guideLineMat = new THREE.LineDashedMaterial({\n            color: 0x66ffff,\n            transparent: true,\n            opacity: 0.30,\n            depthWrite: false,\n            dashSize: 0.4,\n            gapSize: 0.3\n        });\n        for (let i = 0; i < MAX_GUIDE_LINES; i++) {\n            const geo = new THREE.BufferGeometry();\n            const positions = new Float32Array(6);\n            geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n            const line = new THREE.Line(geo, guideLineMat.clone());\n            line.computeLineDistances();\n            line.visible = false;\n            scene.add(line);\n            guideLines.push(line);\n        }\n\n        // ---- DEPTH FOG / BRIGHTNESS + DANGER ZONE ----\n        const DANGER_ZONE_DIST = 6.0; // objects within this 3D distance get full brightness + glow\n\n        function applyDepthTint(obj, z) {\n            // z is world z; player is at PLAYER_Z (~-2). Objects spawn far negative z and move toward positive.\n            const distFromPlayer = Math.abs(z - PLAYER_Z);\n            const maxDist = 60;\n            const t = Math.min(distFromPlayer / maxDist, 1); // 0=near, 1=far\n\n            // Check if object is in danger zone (close enough to interact)\n            const dx = (obj.position ? obj.position.x : 0) - leadX;\n            const dy = (obj.position ? obj.position.y : 0) - leadY;\n            const dz = z - PLAYER_Z;\n            const dist3D = Math.sqrt(dx * dx + dy * dy + dz * dz);\n            const inDangerZone = dist3D < DANGER_ZONE_DIST;\n            const dangerFactor = inDangerZone ? Math.max(0, 1.0 - dist3D / DANGER_ZONE_DIST) : 0;\n\n            // Strong depth effect: near=1.0 bright, far=0.25 dark\n            const baseBrightness = 1.0 - t * 0.75;\n            // Strong purple/blue tint at distance\n            const purpleTint = t * 0.55;\n\n            // Danger zone overrides: full brightness + emissive boost\n            const brightness = inDangerZone ? (baseBrightness + dangerFactor * (1.0 - baseBrightness)) : baseBrightness;\n            const glowBoost = dangerFactor * 0.4;\n\n            const applyToMaterial = (mat) => {\n                if (!mat) return;\n                if (!mat._depthOriginalColor && mat.color) {\n                    mat._depthOriginalColor = mat.color.clone();\n                }\n                if (mat._depthOriginalColor) {\n                    const orig = mat._depthOriginalColor;\n                    mat.color.setRGB(\n                        Math.min(1, orig.r * brightness + purpleTint * 0.3 + glowBoost),\n                        Math.min(1, orig.g * brightness * (1 - purpleTint * 0.7) + glowBoost * 0.5),\n                        Math.min(1, orig.b * brightness + purpleTint * 0.8 + glowBoost * 0.3)\n                    );\n                }\n                // Emissive glow in danger zone\n                if (mat.emissive) {\n                    if (!mat._depthOriginalEmissive) {\n                        mat._depthOriginalEmissive = mat.emissive.clone();\n                    }\n                    const oe = mat._depthOriginalEmissive;\n                    mat.emissive.setRGB(\n                        oe.r + glowBoost * 0.8,\n                        oe.g + glowBoost * 0.6,\n                        oe.b + glowBoost * 0.4\n                    );\n                }\n            };\n\n            if (obj.traverse) {\n                obj.traverse(ch => { if (ch.material) applyToMaterial(ch.material); });\n            } else {\n                applyToMaterial(obj.material);\n            }\n        }\n\n        function updateShadowMarkers() {\n            let idx = 0;\n            const sources = [];\n\n            // Player swarm (lead ship only)\n            if (swarm.length > 0) {\n                sources.push({ x: swarm[0].x, y: swarm[0].y, z: swarm[0].mesh.position.z, size: 0.7, bright: 0.35 });\n            }\n\n            // Collectibles\n            for (let i = 0; i < collectibles.length && sources.length < MAX_SHADOW_MARKERS; i++) {\n                const c = collectibles[i];\n                if (c.position.z < 10 && c.position.z > -80) {\n                    sources.push({ x: c.position.x, y: c.position.y, z: c.position.z, size: 0.3, bright: 0.18 });\n                }\n            }\n\n            // Enemies\n            for (let i = 0; i < enemies.length && sources.length < MAX_SHADOW_MARKERS; i++) {\n                const e = enemies[i];\n                if (e.mesh.position.z < 10 && e.mesh.position.z > -80) {\n                    sources.push({ x: e.mesh.position.x, y: e.mesh.position.y, z: e.mesh.position.z, size: 0.6, bright: 0.25 });\n                }\n            }\n\n            // Health pickups\n            for (let i = 0; i < healthPickups.length && sources.length < MAX_SHADOW_MARKERS; i++) {\n                const h = healthPickups[i];\n                if (h.mesh.position.z < 10 && h.mesh.position.z > -80) {\n                    sources.push({ x: h.mesh.position.x, y: h.mesh.position.y, z: h.mesh.position.z, size: 0.4, bright: 0.2 });\n                }\n            }\n\n            for (let i = 0; i < sources.length && idx < MAX_SHADOW_MARKERS; i++) {\n                const s = sources[i];\n                const marker = shadowMarkers[idx];\n                const height = s.y - FLOOR_Y;\n                const heightFactor = Math.max(0, 1.0 - height * 0.08);\n                const scale = s.size * (0.6 + heightFactor * 0.6);\n                marker.position.set(s.x, FLOOR_Y + 0.01, s.z);\n                marker.scale.setScalar(scale);\n                marker.material.opacity = s.bright * heightFactor;\n                marker.visible = heightFactor > 0.05;\n                idx++;\n            }\n\n            // Hide unused\n            for (let i = idx; i < MAX_SHADOW_MARKERS; i++) {\n                shadowMarkers[i].visible = false;\n            }\n\n            // Guide lines disabled\n            for (let i = 0; i < MAX_GUIDE_LINES; i++) {\n                guideLines[i].visible = false;\n            }\n        }\n\n        function initStreaks() {\n            warpStreaks.length = 0;\n            for (let i = 0; i < MAX_STREAKS; i++) {\n                warpStreaks.push(createStreak());\n            }\n        }\n\n        function createStreak() {\n            const angle = Math.random() * Math.PI * 2;\n            const r = 1 + Math.random() * (TUNNEL_RADIUS - 2);\n            return {\n                x: Math.cos(angle) * r,\n                y: Math.sin(angle) * r,\n                z: -Math.random() * 120,\n                length: 1 + Math.random() * 4,\n                speed: 20 + Math.random() * 40,\n                brightness: 0.3 + Math.random() * 0.7\n            };\n        }\n\n        function updateStreaks(dt, moveSpeed) {\n            for (let i = 0; i < warpStreaks.length; i++) {\n                const s = warpStreaks[i];\n                s.z += (s.speed * dt + moveSpeed);\n                if (s.z > 15) {\n                    warpStreaks[i] = createStreak();\n                    warpStreaks[i].z = -80 - Math.random() * 40;\n                }\n                const idx = i * 6;\n                streakPositions[idx] = s.x;\n                streakPositions[idx + 1] = s.y;\n                streakPositions[idx + 2] = s.z;\n                streakPositions[idx + 3] = s.x;\n                streakPositions[idx + 4] = s.y;\n                streakPositions[idx + 5] = s.z - s.length;\n                const b = s.brightness;\n                streakColors[idx] = b * 0.5; streakColors[idx + 1] = b * 0.7; streakColors[idx + 2] = b;\n                streakColors[idx + 3] = 0; streakColors[idx + 4] = 0; streakColors[idx + 5] = 0;\n            }\n            streakGeo.attributes.position.needsUpdate = true;\n            streakGeo.attributes.color.needsUpdate = true;\n        }\n\n        // ===== INITIALIZATION =====\n        function initGame() {\n            score = 0;\n            distance = 0;\n            maxSwarmSize = 0;\n            currentSpeed = 10 * config.gameplay.startSpeed;\n            difficulty = 1;\n            nextMilestone = 10;\n            leadX = 0;\n            leadY = 0;\n            targetX = 0;\n            targetY = 0;\n            inputX = 0;\n            inputY = 0;\n\n            // Clear scene objects\n            swarm.forEach(s => scene.remove(s.mesh));\n            swarm.length = 0;\n            tunnelSegments.forEach(s => scene.remove(s));\n            tunnelSegments.length = 0;\n            collectibles.forEach(c => scene.remove(c));\n            collectibles.length = 0;\n            obstacles.forEach(o => scene.remove(o));\n            obstacles.length = 0;\n            activeParticles.forEach(p => { p.mesh.visible = false; scene.remove(p.mesh); particlePool.push(p.mesh); });\n            activeParticles.length = 0;\n\n            tunnelZFront = 0;\n            lastSpawnZ = 0;\n            sectionCounter = 0;\n\n            // Clear enemies\n            enemies.forEach(e => scene.remove(e.mesh));\n            enemies.length = 0;\n            // Clear health pickups\n            healthPickups.forEach(h => scene.remove(h.mesh));\n            healthPickups.length = 0;\n\n            // Create initial tunnel\n            for (let i = 0; i < 5; i++) {\n                const seg = createTunnelSegment(tunnelZFront);\n                tunnelSegments.push(seg);\n                scene.add(seg);\n                tunnelZFront -= TUNNEL_SEGMENT_LENGTH;\n            }\n\n            // Create initial swarm\n            const startSize = config.swarm.startSize;\n            for (let i = 0; i < startSize; i++) {\n                const isLead = (i === 0);\n                const mesh = createShipMesh(isLead);\n                mesh.position.set(0, 0, i * config.swarm.followDistance);\n                scene.add(mesh);\n                swarm.push({\n                    mesh,\n                    x: 0, y: 0, z: i * config.swarm.followDistance,\n                    targetX: 0, targetY: 0,\n                    isModel: isLead && !!leadShipModel\n                });\n            }\n            maxSwarmSize = startSize;\n            energy = 100;\n\n            // Init warp streaks\n            initStreaks();\n\n            // Camera\n            camera.position.set(0, 2, config.gameplay.cameraZ);\n            camera.lookAt(0, 0, 2);\n\n            // Spawn initial collectibles\n            for (let z = -20; z > tunnelZFront; z -= 8) {\n                spawnContent(z);\n            }\n            lastSpawnZ = tunnelZFront;\n        }\n\n        function spawnVirusEnemy(z) {\n            if (!virusEnemyModel) return;\n            if (enemies.length >= MAX_ACTIVE_ENEMIES) return;\n\n            const mesh = virusEnemyModel.clone();\n            prepareClonedModelForSpawn(mesh);\n            lib.log('Enemy spawned: virus z=' + z.toFixed(2) + ' opacity=1');\n\n            // Position: random spot in tunnel but not blocking entire path\n            const angle = Math.random() * Math.PI * 2;\n            const r = 1.5 + Math.random() * (TUNNEL_RADIUS - 4);\n            mesh.position.set(\n                Math.cos(angle) * r,\n                Math.sin(angle) * r,\n                z\n            );\n            scene.add(mesh);\n\n            const enemy = {\n                mesh,\n                type: 'virus',\n                damage: 25,\n                radius: 1.2,\n                driftAngle: Math.random() * Math.PI * 2,\n                driftSpeed: 0.5 + Math.random() * 0.8,\n                driftRadius: 0.5 + Math.random() * 1.0,\n                baseX: mesh.position.x,\n                baseY: mesh.position.y,\n                rotSpeed: 0.5 + Math.random() * 1.0,\n                mixer: null,\n                hasAnimation: false,\n                isFading: false\n            };\n\n            // Setup animation if available\n            if (virusEnemyAnimations && virusEnemyAnimations.length > 0) {\n                const mixer = new THREE.AnimationMixer(mesh);\n                // Prefer idle animation\n                let clip = virusEnemyAnimations[0];\n                for (const anim of virusEnemyAnimations) {\n                    if (anim.name.toLowerCase().includes('idle')) {\n                        clip = anim;\n                        break;\n                    }\n                }\n                mixer.clipAction(clip).play();\n                enemy.mixer = mixer;\n                enemy.hasAnimation = true;\n            }\n\n            enemies.push(enemy);\n        }\n\n        function spawnCyberarac(z) {\n            if (!cyberaracModel) return;\n            if (enemies.length >= MAX_ACTIVE_ENEMIES) return;\n\n            const mesh = cyberaracModel.clone();\n            prepareClonedModelForSpawn(mesh);\n            lib.log('Enemy spawned: cyberarac z=' + z.toFixed(2) + ' opacity=1');\n\n            mesh.scale.setScalar(config.models.cyberaracScale);\n            // Position: pick a lane to patrol (left, center, or right area)\n            const laneAngle = (Math.floor(Math.random() * 3) - 1) * (Math.PI / 4); // -45, 0, or 45 degrees\n            const r = 2.0 + Math.random() * 2.0;\n            const startX = Math.cos(laneAngle) * r;\n            const startY = Math.sin(laneAngle) * r;\n            mesh.position.set(startX, startY, z);\n            scene.add(mesh);\n\n            const enemy = {\n                mesh,\n                type: 'cyberarac',\n                damage: 25,\n                radius: 1.1,\n                // Patrol: side-to-side movement\n                patrolCenterX: startX,\n                patrolCenterY: startY,\n                patrolAmplitude: 2.0 + Math.random() * 1.5, // how far it patrols side-to-side\n                patrolSpeed: 0.6 + Math.random() * 0.4, // slow patrol\n                patrolAngle: Math.random() * Math.PI * 2, // starting phase\n                patrolDirection: Math.random() * Math.PI * 2, // direction of patrol line\n                // Bob/hover\n                bobPhase: Math.random() * Math.PI * 2,\n                bobSpeed: 1.5 + Math.random() * 0.5,\n                bobAmplitude: 0.3 + Math.random() * 0.2,\n                // Rotation/banking\n                rotSpeed: 0.3 + Math.random() * 0.3,\n                baseX: startX,\n                baseY: startY,\n                driftAngle: 0,\n                driftSpeed: 0,\n                driftRadius: 0,\n                mixer: null,\n                hasAnimation: false,\n                isFading: false\n            };\n\n            // Setup animation if available\n            if (cyberaracAnimations && cyberaracAnimations.length > 0) {\n                const mixer = new THREE.AnimationMixer(mesh);\n                // Prefer idle animation\n                let clip = cyberaracAnimations[0];\n                for (const anim of cyberaracAnimations) {\n                    if (anim.name.toLowerCase().includes('idle')) {\n                        clip = anim;\n                        break;\n                    }\n                }\n                mixer.clipAction(clip).play();\n                enemy.mixer = mixer;\n                enemy.hasAnimation = true;\n            }\n\n            enemies.push(enemy);\n        }\n\n        function spawnCleaverDrone(z) {\n            if (!cleaverDroneModel) return;\n            if (enemies.length >= MAX_ACTIVE_ENEMIES) return;\n\n            const mesh = cleaverDroneModel.clone();\n            prepareClonedModelForSpawn(mesh);\n            mesh.scale.setScalar(config.models.cleaverDroneScale);\n\n            // Position: random spot in tunnel\n            const angle = Math.random() * Math.PI * 2;\n            const r = 1.5 + Math.random() * (TUNNEL_RADIUS - 4);\n            mesh.position.set(Math.cos(angle) * r, Math.sin(angle) * r, z);\n            scene.add(mesh);\n\n            lib.log('Cleaver Drone spawned z=' + z.toFixed(2));\n\n            enemies.push({\n                mesh,\n                type: 'cleaverDrone',\n                damage: 30,\n                radius: 1.2,\n                baseX: mesh.position.x,\n                baseY: mesh.position.y,\n                driftAngle: 0,\n                driftSpeed: 0,\n                driftRadius: 0,\n                rotSpeed: 0,\n                mixer: null,\n                hasAnimation: false,\n                isFading: false\n            });\n        }\n\n        function spawnContent(z) {\n            const freq = config.gameplay.collectibleFreq;\n            const diff = difficulty * config.gameplay.difficultyScale;\n            sectionCounter++;\n\n            // Determine section type\n            // Enemy section logic:\n            // - After distance 150+: Virus\n            // - After distance 250+: Virus + Cyberarac + Cleaver Drone\n            const canSpawnVirus = distance > 150 && virusEnemyModel;\n            const canSpawnCyberarac = distance > 250 && cyberaracModel;\n            const canSpawnCleaverDrone = distance > 250 && cleaverDroneModel;\n            const canSpawnEnemy = (canSpawnVirus || canSpawnCyberarac || canSpawnCleaverDrone) && enemies.length < MAX_ACTIVE_ENEMIES;\n            const isEnemySection = canSpawnEnemy && Math.random() < 0.13;\n            let isCyberaracSection = false;\n\n            if (isEnemySection) {\n                // Decide which enemy to spawn based on distance thresholds\n                if (canSpawnCleaverDrone && canSpawnCyberarac && canSpawnVirus) {\n                    // After distance 250: Virus 50%, Cyberarac 25%, Cleaver Drone 25%\n                    const roll = Math.random();\n                    if (roll < 0.50) {\n                        spawnVirusEnemy(z - 3);\n                    } else if (roll < 0.75) {\n                        spawnCyberarac(z - 3);\n                        isCyberaracSection = true;\n                    } else {\n                        spawnCleaverDrone(z - 6);\n                    }\n                } else if (canSpawnCyberarac && canSpawnVirus) {\n                    if (Math.random() < 0.50) {\n                        spawnVirusEnemy(z - 3);\n                    } else {\n                        spawnCyberarac(z - 3);\n                        isCyberaracSection = true;\n                    }\n                } else if (canSpawnVirus) {\n                    spawnVirusEnemy(z - 3);\n                }\n\n                // Still spawn a few collectibles around the enemy\n                const numOrbs = Math.floor(1 + Math.random() * 2 * freq);\n                for (let i = 0; i < numOrbs; i++) {\n                    const a = Math.random() * Math.PI * 2;\n                    const r2 = Math.random() * (TUNNEL_RADIUS - 2);\n                    const pos = new THREE.Vector3(\n                        Math.cos(a) * r2,\n                        Math.sin(a) * r2,\n                        z - Math.random() * 8\n                    );\n                    const orb = createOrb(pos);\n                    collectibles.push(orb);\n                    scene.add(orb);\n                }\n                return;\n            }\n\n            // Health pickup section\n            if (healthPickupModel && healthPickups.length < 1 && energy < 90) {\n                let hpChance = 0.08;\n                if (energy < 35) hpChance = 0.22;\n                else if (energy < 60) hpChance = 0.14;\n                if (Math.random() < hpChance) {\n                    spawnHealthPickup(z - 2 - Math.random() * 4);\n                }\n            }\n\n            // Normal section: Collectibles\n            const numOrbs = Math.floor(2 + Math.random() * 3 * freq);\n            for (let i = 0; i < numOrbs; i++) {\n                const angle = Math.random() * Math.PI * 2;\n                const r = Math.random() * (TUNNEL_RADIUS - 2);\n                const pos = new THREE.Vector3(\n                    Math.cos(angle) * r,\n                    Math.sin(angle) * r,\n                    z - Math.random() * 8\n                );\n                const orb = createOrb(pos);\n                collectibles.push(orb);\n                scene.add(orb);\n            }\n\n            // Clusters (rarer)\n            if (Math.random() < 0.2 * freq) {\n                const angle = Math.random() * Math.PI * 2;\n                const r = (TUNNEL_RADIUS - 3) * (0.3 + Math.random() * 0.5);\n                const pos = new THREE.Vector3(\n                    Math.cos(angle) * r,\n                    Math.sin(angle) * r,\n                    z - Math.random() * 5\n                );\n                const cluster = createCluster(pos);\n                collectibles.push(cluster);\n                scene.add(cluster);\n            }\n\n            // Obstacles (based on difficulty) - skip barriers if Cyberarac section\n            if (distance > 100) {\n                const numObs = Math.floor(1 + Math.random() * diff);\n                for (let i = 0; i < numObs; i++) {\n                    const obsType = Math.random();\n                    const angle = Math.random() * Math.PI * 2;\n\n                    // Reduce barrier frequency as speed increases\n                    const barrierChance = Math.max(0.15, 0.5 - (difficulty - 1) * 0.12);\n                    if (obsType < (1 - barrierChance)) {\n                        // Spike from wall\n                        const r = TUNNEL_RADIUS - 1.5;\n                        const pos = new THREE.Vector3(\n                            Math.cos(angle) * r,\n                            Math.sin(angle) * r,\n                            z - Math.random() * 6\n                        );\n                        const spike = createSpike(pos, angle);\n                        obstacles.push(spike);\n                        scene.add(spike);\n                    } else {\n                        // Rotating barrier\n                        const pos = new THREE.Vector3(0, 0, z - Math.random() * 6);\n                        const speed = (0.5 + Math.random()) * (Math.random() < 0.5 ? 1 : -1);\n                        const barrier = createBarrier(pos, speed);\n                        obstacles.push(barrier);\n                        scene.add(barrier);\n                    }\n                }\n            }\n        }\n\n        function spawnHealthPickup(z) {\n            if (!healthPickupModel || healthPickups.length >= 1) return;\n            const mesh = healthPickupModel.clone();\n            const angle = Math.random() * Math.PI * 2;\n            const r = 1.5 + Math.random() * (TUNNEL_RADIUS - 4);\n            mesh.position.set(Math.cos(angle) * r, Math.sin(angle) * r, z);\n            scene.add(mesh);\n            lib.log('Health pickup spawned at z=' + z.toFixed(2) + ' scale=' + mesh.scale.x.toFixed(4));\n            healthPickups.push({ mesh, radius: 1.3 });\n        }\n\n        function addShipToSwarm() {\n            const lastShip = swarm[swarm.length - 1];\n            const mesh = createShipMesh();\n            const offset = config.swarm.followDistance;\n            mesh.position.set(\n                lastShip.x + (Math.random() - 0.5) * config.swarm.spread,\n                lastShip.y + (Math.random() - 0.5) * config.swarm.spread,\n                lastShip.z + offset\n            );\n            scene.add(mesh);\n            swarm.push({\n                mesh,\n                x: mesh.position.x,\n                y: mesh.position.y,\n                z: mesh.position.z,\n                targetX: lastShip.x,\n                targetY: lastShip.y\n            });\n            if (swarm.length > maxSwarmSize) maxSwarmSize = swarm.length;\n\n            // Check milestones\n            if (swarm.length >= nextMilestone) {\n                showMilestone(nextMilestone);\n                playSound('milestone_sfx', 0.6);\n                if (nextMilestone === 10) nextMilestone = 25;\n                else if (nextMilestone === 25) nextMilestone = 50;\n                else if (nextMilestone === 50) nextMilestone = 100;\n                else nextMilestone += 50;\n            }\n        }\n\n        function removeShipFromSwarm() {\n            if (swarm.length <= 0) return;\n            const ship = swarm.pop();\n            emitParticles(ship.mesh.position, 0xff4400, 12, 5);\n            scene.remove(ship.mesh);\n            playSound('ship_destroy_sfx', 0.4);\n\n            if (swarm.length === 0) {\n                gameOver();\n            }\n        }\n\n        function showMilestone(count) {\n            const popup = document.getElementById('milestone-popup');\n            popup.textContent = `SWARM x${count} REACHED`;\n            popup.classList.remove('show');\n            void popup.offsetWidth;\n            popup.classList.add('show');\n            setTimeout(() => popup.classList.remove('show'), 1600);\n        }\n\n        async function submitCurrentScore() {\n            // Submit current score to leaderboard if in active game\n            if (gameState === 'playing' && score > 0) {\n                const currentScore = Math.floor(score);\n                try {\n                    await lib.addPlayerScoreToLeaderboard(currentScore);\n                    lib.log('Score submitted to leaderboard: ' + currentScore);\n                } catch (error) {\n                    lib.log('Failed to submit score to leaderboard: ' + error.message);\n                }\n            }\n        }\n\n        async function gameOver() {\n            gameState = 'gameover';\n            const finalScore = Math.floor(score);\n            document.getElementById('final-score').textContent = finalScore;\n            document.getElementById('final-swarm').textContent = maxSwarmSize;\n            document.getElementById('game-over').classList.add('show');\n            document.getElementById('hud').style.display = 'none';\n\n            document.getElementById('joystick-area').style.display = 'none';\n            playSound('game_over_sfx', 0.6);\n            if (bgMusic) { bgMusic.source.stop(); bgMusic = null; }\n\n            // Submit score to leaderboard\n            try {\n                await lib.addPlayerScoreToLeaderboard(finalScore);\n                lib.log('Score submitted to leaderboard: ' + finalScore);\n            } catch (error) {\n                lib.log('Failed to submit score to leaderboard: ' + error.message);\n                // Game continues to work even if leaderboard submission fails\n            }\n        }\n\n        // ===== INPUT HANDLING =====\n        const joystickArea = document.getElementById('joystick-area');\n\n        joystickArea.addEventListener('pointerdown', (e) => {\n            if (gameState !== 'playing') return;\n            e.preventDefault();\n            joystickActive = true;\n            joystickTouchId = e.pointerId;\n            joystickStartX = e.clientX;\n            joystickStartY = e.clientY;\n            try { joystickArea.setPointerCapture(e.pointerId); } catch (err) {}\n        });\n\n        joystickArea.addEventListener('pointermove', (e) => {\n            if (!joystickActive || e.pointerId !== joystickTouchId) return;\n            e.preventDefault();\n            const dx = e.clientX - joystickStartX;\n            const dy = e.clientY - joystickStartY;\n            const maxR = 80;\n            const dist = Math.sqrt(dx * dx + dy * dy);\n            const deadzone = maxR * 0.1;\n            if (dist < deadzone) {\n                inputX = 0; inputY = 0;\n            } else {\n                const clamped = Math.min(dist, maxR);\n                inputX = (dx / dist) * (clamped / maxR);\n                inputY = -(dy / dist) * (clamped / maxR);\n            }\n            // Move joystick base if dragged too far\n            if (dist > maxR * 1.5) {\n                joystickStartX = e.clientX - (dx / dist) * maxR;\n                joystickStartY = e.clientY - (dy / dist) * maxR;\n            }\n        });\n\n        const endPointer = (e) => {\n            if (e.pointerId === joystickTouchId) {\n                joystickActive = false;\n                joystickTouchId = null;\n                inputX = 0; inputY = 0;\n                try { joystickArea.releasePointerCapture(e.pointerId); } catch (err) {}\n            }\n        };\n\n        joystickArea.addEventListener('pointerup', endPointer);\n        joystickArea.addEventListener('pointercancel', endPointer);\n\n        // Keyboard\n        const keys = {};\n        document.addEventListener('keydown', (e) => {\n            keys[e.key.toLowerCase()] = true;\n            if (['w', 'a', 's', 'd', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright', ' '].includes(e.key.toLowerCase())) {\n                e.preventDefault();\n            }\n        });\n        document.addEventListener('keyup', (e) => {\n            keys[e.key.toLowerCase()] = false;\n        });\n        window.addEventListener('blur', () => {\n            Object.keys(keys).forEach(k => keys[k] = false);\n            inputX = 0; inputY = 0;\n            joystickActive = false;\n        });\n\n        function updateKeyboardInput() {\n            let kx = 0, ky = 0;\n            if (keys['a'] || keys['arrowleft']) kx -= 1;\n            if (keys['d'] || keys['arrowright']) kx += 1;\n            if (keys['w'] || keys['arrowup']) ky += 1;\n            if (keys['s'] || keys['arrowdown']) ky -= 1;\n            if (kx !== 0 || ky !== 0) {\n                inputX = kx;\n                inputY = ky;\n            } else if (!joystickActive) {\n                inputX = 0;\n                inputY = 0;\n            }\n        }\n\n        // ===== UI =====\n        const startScreen = document.getElementById('start-screen');\n        const gameOverScreen = document.getElementById('game-over');\n        const hud = document.getElementById('hud');\n\n        document.getElementById('start-btn').addEventListener('click', () => {\n            initAudio();\n            startGame();\n        });\n\n        document.getElementById('restart-btn').addEventListener('click', async () => {\n            await submitCurrentScore();\n            gameOverScreen.classList.remove('show');\n            startGame();\n        });\n\n        let paused = false;\n        const pauseOverlay = document.getElementById('pause-overlay');\n\n        function togglePause() {\n            if (gameState !== 'playing') return;\n            paused = !paused;\n            document.getElementById('pause-btn').textContent = paused ? '\u25b6' : '\u23f8';\n            pauseOverlay.classList.toggle('show', paused);\n        }\n\n        document.getElementById('pause-btn').addEventListener('click', togglePause);\n\n        document.getElementById('replay-btn').addEventListener('click', async () => {\n            if (gameState !== 'playing') return;\n            await submitCurrentScore();\n            paused = false;\n            document.getElementById('pause-btn').textContent = '\u23f8';\n            pauseOverlay.classList.remove('show');\n            gameOverScreen.classList.remove('show');\n            startGame();\n        });\n\n        document.getElementById('pause-resume-btn').addEventListener('click', () => {\n            if (paused) togglePause();\n        });\n\n        document.getElementById('pause-restart-btn').addEventListener('click', async () => {\n            await submitCurrentScore();\n            paused = false;\n            document.getElementById('pause-btn').textContent = '\u23f8';\n            pauseOverlay.classList.remove('show');\n            startGame();\n        });\n\n        document.addEventListener('keydown', (e) => {\n            if (e.key === 'Escape' || e.key === 'p') togglePause();\n        });\n\n        function startGame() {\n            startScreen.style.display = 'none';\n            gameState = 'playing';\n            initGame();\n            hud.style.display = 'flex';\n\n            document.getElementById('joystick-area').style.display = 'block';\n\n            // Load and play audio\n            loadAllAudio().then(() => {\n                bgMusic = playSound('background_music', 0.3, true);\n            });\n        }\n\n        async function loadAllAudio() {\n            const ids = ['orb_collect_sfx', 'cluster_collect_sfx', 'ship_destroy_sfx', 'milestone_sfx', 'game_over_sfx', 'background_music', 'health_pickup_sfx'];\n            await Promise.all(ids.map(id => loadAudio(id)));\n        }\n\n        function showHealthPopup(worldPos) {\n            const popup = document.createElement('div');\n            popup.textContent = '+25% HEALTH';\n            popup.style.cssText = `position:absolute;font-family:'Orbitron',sans-serif;font-size:26px;font-weight:700;color:#ffffff;text-shadow:0 0 12px rgba(255,255,255,0.9),0 0 24px rgba(200,255,200,0.5);pointer-events:none;z-index:15;transition:transform 1s ease-out,opacity 1s ease-out;`;\n            const vec = worldPos.clone().project(camera);\n            const x = (vec.x * 0.5 + 0.5) * gameWorld.clientWidth;\n            const y = (-vec.y * 0.5 + 0.5) * gameWorld.clientHeight;\n            popup.style.left = x + 'px';\n            popup.style.top = y + 'px';\n            popup.style.transform = 'translate(-50%,-50%)';\n            popup.style.opacity = '1';\n            document.getElementById('ui-overlay').appendChild(popup);\n            requestAnimationFrame(() => {\n                popup.style.transform = 'translate(-50%,-100px)';\n                popup.style.opacity = '0';\n            });\n            setTimeout(() => popup.remove(), 1000);\n        }\n\n        function showPointsPopup(text, worldPos) {\n            const popup = document.createElement('div');\n            popup.textContent = text;\n            popup.style.cssText = `position:absolute;font-family:'Orbitron',sans-serif;font-size:28px;font-weight:700;color:#ffd700;text-shadow:0 0 10px rgba(255,215,0,0.8);pointer-events:none;z-index:15;transition:transform 1s ease-out,opacity 1s ease-out;`;\n            const vec = worldPos.clone().project(camera);\n            const x = (vec.x * 0.5 + 0.5) * gameWorld.clientWidth;\n            const y = (-vec.y * 0.5 + 0.5) * gameWorld.clientHeight;\n            popup.style.left = x + 'px';\n            popup.style.top = y + 'px';\n            popup.style.transform = 'translate(-50%,-50%)';\n            popup.style.opacity = '1';\n            document.getElementById('ui-overlay').appendChild(popup);\n            requestAnimationFrame(() => {\n                popup.style.transform = 'translate(-50%,-100px)';\n                popup.style.opacity = '0';\n            });\n            setTimeout(() => popup.remove(), 1000);\n        }\n\n        function updateHUD() {\n            document.getElementById('hud-score').textContent = Math.floor(score);\n            document.getElementById('hud-swarm').textContent = `\u2b21 ${swarm.length}`;\n            const bar = document.getElementById('energy-bar-fill');\n            bar.style.width = energy + '%';\n            bar.classList.toggle('low', energy <= 30);\n            bar.classList.toggle('mid', energy > 30 && energy <= 60);\n            document.getElementById('energy-percent').textContent = Math.round(energy) + '%';\n        }\n\n        // ===== GAME LOOP =====\n        let lastTime = 0;\n\n        function gameLoop(timestamp) {\n            const dt = Math.min((timestamp - lastTime) / 1000, 0.11);\n            lastTime = timestamp;\n\n            if (mode === 'edit') {\n                updateEdit(dt, timestamp);\n            } else {\n                if (gameState === 'playing' && !paused) {\n                    updateGame(dt, timestamp);\n                }\n            }\n\n            renderer.render(scene, camera);\n            requestAnimationFrame(gameLoop);\n        }\n\n        function updateGame(dt, time) {\n            updateKeyboardInput();\n\n            // Speed increases over time\n            difficulty = 1 + distance * 0.001 * config.gameplay.difficultyScale;\n            currentSpeed = (10 + distance * 0.005) * config.gameplay.startSpeed;\n            const moveSpeed = currentSpeed * dt;\n\n            // Move lead ship target\n            const maxOffset = TUNNEL_RADIUS - 1.5;\n            targetX += inputX * 15 * dt;\n            targetY += inputY * 15 * dt;\n            const dist = Math.sqrt(targetX * targetX + targetY * targetY);\n            if (dist > maxOffset) {\n                targetX = (targetX / dist) * maxOffset;\n                targetY = (targetY / dist) * maxOffset;\n            }\n\n            // Smooth interpolation\n            leadX += (targetX - leadX) * 8 * dt;\n            leadY += (targetY - leadY) * 8 * dt;\n\n            // Update swarm positions\n            if (swarm.length > 0) {\n                swarm[0].x = leadX;\n                swarm[0].y = leadY;\n                swarm[0].z = PLAYER_Z;\n                swarm[0].mesh.position.set(leadX, leadY, PLAYER_Z);\n\n                // Dynamic tilt for imported lead ship model\n                if (swarm[0].isModel) {\n                    const sideAmount = leadX / maxOffset;\n                    const verticalAmount = leadY / maxOffset;\n                    const tYaw = LEAD_SHIP_YAW_OFFSET + sideAmount * 0.3;\n                    const tPitch = -verticalAmount * 0.25;\n                    const tRoll = -inputX * 0.25;\n                    const smooth = 8 * dt;\n                    currentShipYaw += (tYaw - currentShipYaw) * smooth;\n                    currentShipPitch += (tPitch - currentShipPitch) * smooth;\n                    currentShipRoll += (tRoll - currentShipRoll) * smooth;\n                    swarm[0].mesh.rotation.set(currentShipPitch, currentShipYaw, currentShipRoll);\n                }\n\n                for (let i = 1; i < swarm.length; i++) {\n                    const prev = swarm[i - 1];\n                    const curr = swarm[i];\n                    const followDist = config.swarm.followDistance;\n                    const spread = config.swarm.spread;\n\n                    curr.targetX = prev.x + (Math.sin(time * 0.001 + i) * spread);\n                    curr.targetY = prev.y + (Math.cos(time * 0.001 + i * 1.3) * spread);\n\n                    curr.x += (curr.targetX - curr.x) * 5 * dt;\n                    curr.y += (curr.targetY - curr.y) * 5 * dt;\n                    curr.z = (i === 1) ? config.swarm.cloneZ : (config.swarm.cloneZ + (i - 1) * followDist);\n\n                    curr.mesh.position.set(curr.x, curr.y, curr.z);\n                }\n            }\n\n            // Move everything forward (actually move world backward)\n            distance += moveSpeed;\n\n\n            // Move tunnel\n            tunnelSegments.forEach(seg => {\n                seg.position.z += moveSpeed;\n            });\n\n            // Recycle tunnel segments\n            while (tunnelSegments.length > 0 && tunnelSegments[0].position.z > TUNNEL_SEGMENT_LENGTH + 20) {\n                const old = tunnelSegments.shift();\n                scene.remove(old);\n            }\n            const lastZ = tunnelSegments.length > 0 ?\n                tunnelSegments[tunnelSegments.length - 1].position.z - TUNNEL_SEGMENT_LENGTH / 2 - TUNNEL_SEGMENT_LENGTH :\n                tunnelZFront;\n\n            while (tunnelSegments.length < 5) {\n                tunnelZFront -= TUNNEL_SEGMENT_LENGTH;\n                const seg = createTunnelSegment(0);\n                const backZ = tunnelSegments.length > 0 ?\n                    tunnelSegments[tunnelSegments.length - 1].position.z - TUNNEL_SEGMENT_LENGTH :\n                    -TUNNEL_SEGMENT_LENGTH * 2;\n                seg.position.z = backZ;\n                tunnelSegments.push(seg);\n                scene.add(seg);\n            }\n\n            // Move collectibles and check collision\n            for (let i = collectibles.length - 1; i >= 0; i--) {\n                const c = collectibles[i];\n                c.position.z += moveSpeed;\n                // Animate\n                c.rotation.y += dt * 2;\n                applyDepthTint(c, c.position.z);\n                if (c.children) c.children.forEach(ch => ch.rotation && (ch.rotation.y += dt));\n\n                // Fade out when behind player, remove when fully past\n                if (c.position.z > FADE_START_Z) {\n                    const fade = 1 - Math.min(1, Math.max(0, (c.position.z - FADE_START_Z) / (FADE_END_Z - FADE_START_Z)));\n                    c.traverse ? c.traverse(ch => { if (ch.material) { ch.material.transparent = true; ch.material.opacity = fade; } }) : (c.material && (c.material.transparent = true, c.material.opacity = fade));\n                }\n                if (c.position.z > REMOVE_Z) {\n                    scene.remove(c);\n                    collectibles.splice(i, 1);\n                    continue;\n                }\n\n                // Collision with lead ship - skip if behind player\n                const cHasPassed = c.position.z > INTERACTION_END_Z;\n                if (!c.userData.collected && !cHasPassed) {\n                    const dx = c.position.x - leadX;\n                    const dy = c.position.y - leadY;\n                    const dz = c.position.z - PLAYER_Z;\n                    const d = Math.sqrt(dx * dx + dy * dy + dz * dz);\n                    if (d < 1.5) {\n                        c.userData.collected = true;\n                        emitParticles(c.position, c.userData.type === 'orb' ? 0x00ff88 : 0xffdd00, 10, 3);\n                        scene.remove(c);\n                        collectibles.splice(i, 1);\n\n                        if (c.userData.type === 'orb') {\n                            addShipToSwarm();\n                            score += 10;\n                            playSound('orb_collect_sfx', 0.4);\n                            showPointsPopup('+10', c.position);\n                        } else {\n                            for (let j = 0; j < 4; j++) addShipToSwarm();\n                            score += 50;\n                            playSound('cluster_collect_sfx', 0.5);\n                            showPointsPopup('+50', c.position);\n                        }\n                    }\n                }\n            }\n\n            // Update invincibility\n            if (invincible) {\n                invincibleTimer -= dt;\n                if (invincibleTimer <= 0) {\n                    invincible = false;\n                    swarm.forEach(s => {\n                        if (s.mesh.traverse) s.mesh.traverse(c => { if (c.material) { c.material.transparent = false; c.material.opacity = 1; } });\n                        else if (s.mesh.material) { s.mesh.material.transparent = false; s.mesh.material.opacity = 1; }\n                    });\n                } else {\n                    const flash = Math.sin(invincibleTimer * 20) > 0;\n                    swarm.forEach(s => {\n                        if (s.mesh.traverse) s.mesh.traverse(c => { if (c.material) { c.material.transparent = true; c.material.opacity = flash ? 0.3 : 1; } });\n                        else if (s.mesh.material) { s.mesh.material.transparent = true; s.mesh.material.opacity = flash ? 0.3 : 1; }\n                    });\n                }\n            }\n\n            // Move obstacles and check collision\n            for (let i = obstacles.length - 1; i >= 0; i--) {\n                const o = obstacles[i];\n                o.position.z += moveSpeed;\n\n                // Animate barriers\n                if (o.userData.type === 'barrier') {\n                    o.rotation.z += o.userData.rotSpeed * dt;\n                }\n                applyDepthTint(o, o.position.z);\n\n                // Fade out behind player\n                if (o.position.z > FADE_START_Z) {\n                    const fade = 1 - Math.min(1, Math.max(0, (o.position.z - FADE_START_Z) / (FADE_END_Z - FADE_START_Z)));\n                    o.traverse ? o.traverse(ch => { if (ch.material) { ch.material.transparent = true; ch.material.opacity = fade; } }) : (o.material && (o.material.transparent = true, o.material.opacity = fade));\n                }\n                if (o.position.z > REMOVE_Z) {\n                    scene.remove(o);\n                    obstacles.splice(i, 1);\n                    continue;\n                }\n\n                // Collision check with swarm - skip if behind player\n                const oHasPassed = o.position.z > INTERACTION_END_Z;\n                if (!oHasPassed && o.position.z > PLAYER_Z && o.position.z < swarm.length * config.swarm.followDistance + 2) {\n                    for (let s = swarm.length - 1; s >= 0; s--) {\n                        const ship = swarm[s];\n                        let hit = false;\n\n                        if (o.userData.type === 'spike') {\n                            const dx = o.position.x - ship.x;\n                            const dy = o.position.y - ship.y;\n                            const dz = o.position.z - ship.z;\n                            const d = Math.sqrt(dx * dx + dy * dy + dz * dz);\n                            hit = d < o.userData.radius;\n                        } else if (o.userData.type === 'barrier') {\n                            // Barrier is a rotating bar - check if ship is near the bar plane\n                            const dz = Math.abs(o.position.z - ship.z);\n                            if (dz < 0.8) {\n                                const angle = o.rotation.z;\n                                const barDirX = Math.cos(angle);\n                                const barDirY = Math.sin(angle);\n                                const relX = ship.x - o.position.x;\n                                const relY = ship.y - o.position.y;\n                                const proj = Math.abs(relX * barDirX + relY * barDirY);\n                                const perp = Math.abs(-relX * barDirY + relY * barDirX);\n                                hit = proj < o.userData.radius && perp < 0.6;\n                            }\n                        }\n\n                        if (hit && !invincible) {\n                            energy -= 30;\n                            invincible = true;\n                            invincibleTimer = 0.8;\n                            const bar = document.getElementById('energy-bar-fill');\n                            bar.style.boxShadow = '0 0 20px #ff0000';\n                            setTimeout(() => { bar.style.boxShadow = 'none'; }, 300);\n                            emitParticles(new THREE.Vector3(ship.x, ship.y, ship.z), 0xff8800, 30, 8);\n                            emitParticles(new THREE.Vector3(ship.x, ship.y, ship.z), 0xffffff, 15, 12);\n                            playSound('ship_destroy_sfx', 0.6);\n                            if (energy <= 0) {\n                                energy = 0;\n                                gameOver();\n                            }\n                            removeShipFromSwarm();\n                            break;\n                        }\n                    }\n                }\n            }\n\n            // Update health pickups\n            for (let i = healthPickups.length - 1; i >= 0; i--) {\n                const h = healthPickups[i];\n                h.mesh.position.z += moveSpeed;\n                h.mesh.rotation.y += dt * 1.5;\n                applyDepthTint(h.mesh, h.mesh.position.z);\n                // Fade out behind player\n                if (h.mesh.position.z > FADE_START_Z) {\n                    const fade = 1 - Math.min(1, Math.max(0, (h.mesh.position.z - FADE_START_Z) / (FADE_END_Z - FADE_START_Z)));\n                    h.mesh.traverse(ch => { if (ch.material) { ch.material.transparent = true; ch.material.opacity = fade; } });\n                }\n                if (h.mesh.position.z > REMOVE_Z) {\n                    scene.remove(h.mesh);\n                    healthPickups.splice(i, 1);\n                    continue;\n                }\n                // Collision with lead ship - skip if behind player\n                const hHasPassed = h.mesh.position.z > INTERACTION_END_Z;\n                if (hHasPassed) continue;\n                const dx = h.mesh.position.x - leadX;\n                const dy = h.mesh.position.y - leadY;\n                const dz = h.mesh.position.z - PLAYER_Z;\n                const d = Math.sqrt(dx * dx + dy * dy + dz * dz);\n                if (d < h.radius) {\n                    lib.log('Health pickup collected');\n                    energy = Math.min(100, energy + 25);\n                    emitParticles(h.mesh.position, 0xffffff, 15, 4);\n                    playSound('health_pickup_sfx', 0.6);\n                    showHealthPopup(h.mesh.position);\n                    scene.remove(h.mesh);\n                    healthPickups.splice(i, 1);\n                }\n            }\n\n            // Update enemies\n            for (let i = enemies.length - 1; i >= 0; i--) {\n                const e = enemies[i];\n                e.mesh.position.z += moveSpeed;\n\n                if (e.type === 'cyberarac') {\n                    // Cyberarac: slow side-to-side patrol + hover bob\n                    e.patrolAngle += e.patrolSpeed * dt;\n                    e.bobPhase += e.bobSpeed * dt;\n\n                    // Side-to-side patrol along a direction\n                    const patrolOffset = Math.sin(e.patrolAngle) * e.patrolAmplitude;\n                    const patrolDirX = Math.cos(e.patrolDirection);\n                    const patrolDirY = Math.sin(e.patrolDirection);\n                    e.mesh.position.x = e.patrolCenterX + patrolDirX * patrolOffset;\n                    e.mesh.position.y = e.patrolCenterY + patrolDirY * patrolOffset + Math.sin(e.bobPhase) * e.bobAmplitude;\n\n                    // Slow banking/rotation to feel alive and 3D\n                    if (!e.hasAnimation) {\n                        e.mesh.rotation.y += e.rotSpeed * dt;\n                    }\n                    // Bank into patrol direction\n                    const bankAngle = Math.cos(e.patrolAngle) * 0.2;\n                    e.mesh.rotation.z = bankAngle;\n                    e.mesh.rotation.x = Math.sin(e.bobPhase) * 0.1;\n\n                    // Constrain to tunnel\n                    const cx = e.mesh.position.x;\n                    const cy = e.mesh.position.y;\n                    const cd = Math.sqrt(cx * cx + cy * cy);\n                    if (cd > TUNNEL_RADIUS - 2) {\n                        const scale = (TUNNEL_RADIUS - 2) / cd;\n                        e.mesh.position.x *= scale;\n                        e.mesh.position.y *= scale;\n                    }\n                } else if (e.type === 'cleaverDrone') {\n                    // Cleaver Drone: Z movement only + spin\n                    e.mesh.position.x = e.baseX;\n                    e.mesh.position.y = e.baseY;\n                    e.mesh.rotation.z += dt * 1.5;\n                } else {\n                    // Drift motion (small circular) for virus\n                    e.driftAngle += e.driftSpeed * dt;\n                    e.mesh.position.x = e.baseX + Math.cos(e.driftAngle) * e.driftRadius;\n                    e.mesh.position.y = e.baseY + Math.sin(e.driftAngle) * e.driftRadius;\n\n                    // Rotate gently if no animation\n                    if (!e.hasAnimation) {\n                        e.mesh.rotation.y += e.rotSpeed * dt;\n                        e.mesh.rotation.x += e.rotSpeed * 0.3 * dt;\n                    }\n                }\n\n                // Update animation mixer\n                if (e.mixer) {\n                    e.mixer.update(dt);\n                }\n                applyDepthTint(e.mesh, e.mesh.position.z);\n\n                // Fade out behind player, remove if fully past\n                if (e.mesh.position.z > FADE_START_Z) {\n                    if (!e.isFading) {\n                        e.isFading = true;\n                        lib.log('Enemy fading: ' + e.type + ' z=' + e.mesh.position.z.toFixed(2));\n                    }\n                    const fade = 1 - Math.min(1, Math.max(0, (e.mesh.position.z - FADE_START_Z) / (FADE_END_Z - FADE_START_Z)));\n                    e.mesh.traverse(ch => { \n                        if (ch.material) { \n                            if (Array.isArray(ch.material)) {\n                                ch.material.forEach(m => { m.transparent = true; m.opacity = fade; });\n                            } else {\n                                ch.material.transparent = true; \n                                ch.material.opacity = fade; \n                            }\n                        } \n                    });\n                }\n                if (e.mesh.position.z > REMOVE_Z) {\n                    scene.remove(e.mesh);\n                    enemies.splice(i, 1);\n                    continue;\n                }\n\n                // Collision with swarm - skip if behind player\n                const eHasPassed = e.mesh.position.z > INTERACTION_END_Z;\n                if (!eHasPassed && !invincible && e.mesh.position.z > PLAYER_Z - 2 && e.mesh.position.z < swarm.length * config.swarm.followDistance + 2) {\n                    for (let s = swarm.length - 1; s >= 0; s--) {\n                        const ship = swarm[s];\n                        const dx = e.mesh.position.x - ship.x;\n                        const dy = e.mesh.position.y - ship.y;\n                        const dz = e.mesh.position.z - ship.z;\n                        const d = Math.sqrt(dx * dx + dy * dy + dz * dz);\n                        if (d < e.radius) {\n                            // Damage based on enemy type\n                            energy -= e.damage || 25;\n                            invincible = true;\n                            invincibleTimer = 0.8;\n                            const bar = document.getElementById('energy-bar-fill');\n                            let glowColor, particleColor, particleColor2;\n                            if (e.type === 'cyberarac') {\n                                glowColor = '#ff4488';\n                                particleColor = 0xff4488;\n                                particleColor2 = 0xff88aa;\n                            } else if (e.type === 'cleaverDrone') {\n                                glowColor = '#ff8800';\n                                particleColor = 0xff8800;\n                                particleColor2 = 0xffcc00;\n                            } else {\n                                glowColor = '#ff00ff';\n                                particleColor = 0xff00ff;\n                                particleColor2 = 0xff88ff;\n                            }\n                            bar.style.boxShadow = '0 0 20px ' + glowColor;\n                            setTimeout(() => { bar.style.boxShadow = 'none'; }, 300);\n                            emitParticles(new THREE.Vector3(ship.x, ship.y, ship.z), particleColor, 20, 6);\n                            emitParticles(new THREE.Vector3(ship.x, ship.y, ship.z), particleColor2, 10, 10);\n                            playSound('ship_destroy_sfx', 0.5);\n                            if (energy <= 0) {\n                                energy = 0;\n                                gameOver();\n                            }\n                            // Cleaver Drone removes 3 swarm units\n                            if (e.type === 'cleaverDrone') {\n                                for (let r = 0; r < 3; r++) {\n                                    removeShipFromSwarm();\n                                    if (swarm.length === 0) break;\n                                }\n                            } else {\n                                removeShipFromSwarm();\n                            }\n                            break;\n                        }\n                    }\n                }\n            }\n\n            // Spawn new content\n            const spawnThreshold = -80;\n            const farthestZ = collectibles.length > 0 ?\n                Math.min(...collectibles.map(c => c.position.z)) : 0;\n            if (farthestZ > spawnThreshold) {\n                const spawnZ = spawnThreshold - Math.random() * 20;\n                spawnContent(spawnZ);\n            }\n\n            // Also spawn periodically based on distance\n            if (distance - lastSpawnZ > 30) {\n                lastSpawnZ = distance;\n                spawnContent(-60 - Math.random() * 30);\n            }\n\n            updateStreaks(dt, moveSpeed);\n            updateParticles(dt);\n\n            // Update floor plane scroll and shadow markers\n            floorMat.uniforms.uTime.value += dt;\n            floorPlane.position.set(leadX * 0.3, FLOOR_Y, -60);\n            updateShadowMarkers();\n\n            updateHUD();\n\n            // Camera\n            camera.position.set(leadX * 0.3, leadY * 0.3 + 2.0, config.gameplay.cameraZ);\n            camera.lookAt(leadX * 0.5, leadY * 0.5, 0);\n        }\n\n        // ===== EDIT MODE =====\n        function updateEdit(dt, time) {\n            // Show a static scene with swarm preview\n            if (swarm.length === 0) {\n                initGame();\n                gameState = 'edit';\n            }\n\n            // Animate ships gently\n            swarm.forEach((s, i) => {\n                s.mesh.position.x = Math.sin(time * 0.001 + i * 0.5) * 2;\n                s.mesh.position.y = Math.cos(time * 0.001 + i * 0.7) * 2;\n                // Update colors only for non-model ships\n                if (!s.isModel && s.mesh.material) {\n                    s.mesh.material.color.set(config.ship.primaryColor);\n                }\n            });\n\n            // Update tunnel colors\n            tunnelSegments.forEach(seg => {\n                seg.children[0].material.color.set(config.tunnel.color);\n                seg.children[1].material.color.set(config.tunnel.energyColor);\n            });\n\n            collectibles.forEach(c => {\n                c.rotation.y += dt * 2;\n            });\n\n            camera.position.set(0, 2, config.gameplay.cameraZ);\n            camera.lookAt(0, 0, 2);\n\n            floorMat.uniforms.uTime.value += dt;\n            updateShadowMarkers();\n        }\n\n        // ===== START =====\n        // Start screen warp streak background\n        const startBgCanvas = document.getElementById('start-bg-canvas');\n        const bgCtx = startBgCanvas.getContext('2d');\n        const bgStreaks = [];\n        function resizeBgCanvas() {\n            startBgCanvas.width = startBgCanvas.clientWidth;\n            startBgCanvas.height = startBgCanvas.clientHeight;\n        }\n        resizeBgCanvas();\n        for (let i = 0; i < 80; i++) {\n            bgStreaks.push({ x: Math.random() * startBgCanvas.width, y: Math.random() * startBgCanvas.height, len: 20 + Math.random() * 60, speed: 200 + Math.random() * 400, bright: 0.2 + Math.random() * 0.6 });\n        }\n        let bgAnimId = null;\n        let bgLastT = performance.now();\n        function animateBg(t) {\n            const dt = Math.min((t - bgLastT) / 1000, 0.11);\n            bgLastT = t;\n            bgCtx.fillStyle = 'rgba(5,5,16,0.4)';\n            bgCtx.fillRect(0, 0, startBgCanvas.width, startBgCanvas.height);\n            for (const s of bgStreaks) {\n                s.y += s.speed * dt;\n                if (s.y > startBgCanvas.height + s.len) { s.y = -s.len; s.x = Math.random() * startBgCanvas.width; }\n                const grad = bgCtx.createLinearGradient(s.x, s.y - s.len, s.x, s.y);\n                grad.addColorStop(0, 'rgba(0,255,255,0)');\n                grad.addColorStop(1, `rgba(0,200,255,${s.bright})`);\n                bgCtx.strokeStyle = grad;\n                bgCtx.lineWidth = 1.5;\n                bgCtx.beginPath();\n                bgCtx.moveTo(s.x, s.y - s.len);\n                bgCtx.lineTo(s.x, s.y);\n                bgCtx.stroke();\n            }\n            if (startScreen.style.display !== 'none') bgAnimId = requestAnimationFrame(animateBg);\n        }\n        bgAnimId = requestAnimationFrame(animateBg);\n\n        // Leaderboard buttons\n        document.getElementById('start-leaderboard-btn').addEventListener('click', () => { showLeaderboard(); });\n        document.getElementById('pause-leaderboard-btn').addEventListener('click', () => { showLeaderboard(); });\n        document.getElementById('gameover-leaderboard-btn').addEventListener('click', () => { showLeaderboard(); });\n\n        async function showLeaderboard() {\n            try {\n                const response = await lib.getTopNEntriesFromLeaderboard(10);\n                let html = '<div style=\"position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:100;pointer-events:auto;\" id=\"lb-modal\">';\n                html += '<div style=\"font-family:Orbitron,sans-serif;font-size:32px;color:#0ff;margin-bottom:24px;text-shadow:0 0 15px rgba(0,255,255,0.5);\">\ud83c\udfc6 LEADERBOARD</div>';\n                if (response.entries && response.entries.length > 0) {\n                    response.entries.forEach((e, i) => {\n                        html += `<div style=\"font-family:Orbitron,sans-serif;font-size:18px;color:${i<3?'#ffd700':'#ccc'};margin:4px 0;\">${i+1}. ${e.username||'Player'} \u2014 ${Math.floor(e.score)}</div>`;\n                    });\n                } else {\n                    html += '<div style=\"font-family:Inter,sans-serif;font-size:18px;color:#888;\">No scores yet!</div>';\n                }\n                html += '<button class=\"pause-btn\" style=\"margin-top:24px;\" id=\"lb-close-btn\">CLOSE</button></div>';\n                document.getElementById('ui-overlay').insertAdjacentHTML('beforeend', html);\n                document.getElementById('lb-close-btn').addEventListener('click', () => { document.getElementById('lb-modal').remove(); });\n            } catch(e) { lib.log('Leaderboard error: ' + e); }\n        }\n\n        if (mode === 'edit') {\n            startScreen.style.display = 'none';\n            hud.style.display = 'none';\n            gameState = 'edit';\n            initGame();\n        }\n\n        lastTime = performance.now();\n        requestAnimationFrame(gameLoop);\n    }\n    </script>\n</body>\n</html>", "game_config": "{\"ship\":{\"primaryColor\":\"#00ffff\",\"accentColor\":\"#ffffff\",\"trailColor\":\"#88ccff\",\"scale\":1},\"swarm\":{\"startSize\":3,\"followDistance\":0.4,\"spread\":0.1,\"cloneZ\":3.5},\"tunnel\":{\"color\":\"#1a0533\",\"energyColor\":\"#8800ff\",\"fogDensity\":0.5},\"gameplay\":{\"startSpeed\":1,\"difficultyScale\":1,\"collectibleFreq\":1,\"cameraZ\":8},\"models\":{\"leadShipScale\":0.125,\"virusScale\":2,\"healthPickupScale\":0.004,\"cyberaracScale\":0.0016,\"cleaverDroneScale\":0.6}}", "game_design_document": "## Core Game Loop and Mechanics\n\n**Wormhole Swarm** is an endless 3D arcade game where players control a swarm of clone ships traveling through a procedurally generated wormhole tunnel. The core loop focuses on growth, navigation, and survival:\n\n### Moment-to-Moment Gameplay\n- Player controls the lead ship's position within the cylindrical wormhole tunnel\n- The entire swarm follows the lead ship with smooth, organic trailing behavior\n- Ships automatically move forward through the tunnel at constant speed\n- Collectible orbs spawn ahead - collecting them adds new ships to the swarm\n- Obstacles (spikes, barriers, rotating hazards) appear that destroy ships on contact\n- The swarm size dynamically grows and shrinks based on collections and collisions\n\n### Progression and Engagement\n- **Score System**: Points earned for distance traveled and orbs collected (bonus multiplier for larger swarms)\n- **Size Milestones**: Visual and audio feedback when reaching swarm size thresholds (10, 25, 50, 100+ ships)\n- **Difficulty Scaling**: Tunnel speed gradually increases, obstacles become more frequent and complex\n- **Risk/Reward**: Larger swarms score more points but are harder to navigate through tight spaces\n\n### Win/Lose Conditions\n- **Game Over**: When the last ship in the swarm is destroyed\n- **Goal**: Survive as long as possible and achieve the highest score\n- **Replayability**: Procedural generation ensures each run is unique; players chase high scores and size milestones\n\n### What Makes It Compelling\n- **Satisfying Growth**: Watching your single ship multiply into a massive swarm creates a powerful sense of progression\n- **Dynamic Challenge**: The swarm's changing size constantly alters the difficulty - bigger swarms are more powerful but harder to protect\n- **Flow State**: Simple controls combined with escalating speed creates an addictive \"one more try\" arcade experience\n- **Visual Spectacle**: The low-poly aesthetic with particle effects and swarm behavior creates mesmerizing visuals\n\n## Starting Scope\n\nThe first iteration will focus on delivering a polished, playable core experience:\n\n### Implement First (MVP)\n- **3D Wormhole Tunnel**: Cylindrical tunnel with procedurally generated segments, textured walls with a sci-fi aesthetic\n- **Lead Ship Control**: Smooth movement within the tunnel using touch joystick (mobile) or mouse/WASD (desktop)\n- **Basic Swarm Behavior**: 5-10 clone ships that follow the lead ship with trailing/flocking behavior\n- **Two Collectible Types**:\n  - Orbs: Add 1 ship to swarm (green glow)\n  - Clusters: Add 3-5 ships (gold glow, rarer)\n- **Three Obstacle Types**:\n  - Static spikes protruding from tunnel walls\n  - Rotating barriers that sweep across the tunnel\n  - Narrow gaps that require precise navigation\n- **Core Feedback Systems**:\n  - Particle effects when collecting orbs\n  - Ship destruction effects with sound\n  - Score display and swarm count HUD\n  - Milestone celebrations (visual burst + sound at 10, 25, 50 ships)\n- **Simple Difficulty Curve**: Gradual speed increase and obstacle frequency scaling\n- **Game Over Screen**: Final score, swarm size achieved, restart button\n\n### Add Later (Future Iterations)\n- Power-ups (shield, magnet, slow-mo)\n- Multiple tunnel themes/biomes\n- Boss encounters or special challenge sections\n- Leaderboard integration\n- Ship customization options\n- More complex swarm formations\n- Advanced obstacle patterns and combinations\n- Combo system for consecutive collections\n\n## Game Perspective\n\n**3D Third-Person Trailing Camera**\n\nThe game uses a dynamic third-person camera positioned behind and slightly above the lead ship:\n- Camera follows the lead ship smoothly through the tunnel\n- Maintains consistent distance to keep the swarm visible\n- Slight camera lag creates a sense of speed and momentum\n- Field of view adjusted to show upcoming obstacles while keeping swarm in frame\n- The tunnel curves and twists, creating visual variety and spatial challenges\n\n## Game World and Level Design\n\n### Wormhole Tunnel Structure\nThe game takes place inside an infinite cylindrical tunnel with these characteristics:\n\n**Tunnel Geometry**:\n- Cylinder radius: Large enough for comfortable navigation (approximately 8-10 ship widths)\n- Tunnel segments: Generated procedurally in chunks ahead of the player\n- Each segment is 50-100 units long\n- Segments can include straight sections, gentle curves, and spiral twists\n- Walls have a low-poly faceted appearance with glowing energy lines\n\n**Visual Design**:\n- **Walls**: Dark metallic/crystalline low-poly surfaces with animated energy veins\n- **Lighting**: Pulsing ambient light from tunnel walls, bright highlights on collectibles and obstacles\n- **Depth Cues**: Fog effect in the distance, grid lines on tunnel surface, parallax scrolling details\n- **Color Palette**: Deep purples and blues for tunnel, bright greens/golds for collectibles, red/orange for hazards\n\n**Procedural Generation Rules**:\n- **Early Game (0-30 seconds)**: Straight tunnel, sparse obstacles, frequent orbs\n- **Mid Game (30-90 seconds)**: Introduce curves, increase obstacle density, add rotating hazards\n- **Late Game (90+ seconds)**: Tight spirals, complex obstacle patterns, narrow gaps, faster speed\n- **Collectible Placement**: Orbs placed in safe paths between obstacles; clusters placed in riskier positions\n- **Obstacle Patterns**: Start with isolated hazards, progress to coordinated patterns requiring specific movements\n\n### Segment Generation Algorithm\n1. Determine segment type (straight, curve left/right, spiral)\n2. Place 2-4 obstacles based on difficulty level\n3. Place 3-6 orbs in navigable spaces\n4. Occasionally place 1 cluster in a challenging but fair position\n5. Ensure at least one clear path through the segment\n6. Add visual variety (wall patterns, lighting changes)\n\n## Edit Mode Design\n\nEdit mode allows players to customize their experience and create unique visual styles:\n\n### Editable Properties\n\n**Ship Appearance**:\n- **Primary Color**: RGB color picker for main ship hull (default: cyan)\n- **Accent Color**: RGB color picker for ship highlights/engines (default: white)\n- **Trail Color**: RGB color picker for ship trails (default: light blue)\n- **Ship Scale**: Slider 0.5x - 2.0x (default: 1.0x) - affects all ships in swarm\n\n**Swarm Behavior**:\n- **Starting Swarm Size**: Slider 1-10 ships (default: 3)\n- **Follow Distance**: Slider 0.5-2.0 units (default: 1.0) - spacing between ships\n- **Swarm Spread**: Slider 0.0-1.0 (default: 0.3) - how much ships spread out vs. stay tight\n\n**Tunnel Appearance**:\n- **Tunnel Color**: RGB color picker for tunnel walls (default: dark purple)\n- **Energy Line Color**: RGB color picker for glowing veins (default: bright purple)\n- **Fog Density**: Slider 0.0-1.0 (default: 0.5) - visibility distance\n\n**Gameplay Tweaks**:\n- **Starting Speed**: Slider 0.5x - 1.5x (default: 1.0x)\n- **Difficulty Scaling**: Slider 0.5x - 2.0x (default: 1.0x) - how fast difficulty increases\n- **Collectible Frequency**: Slider 0.5x - 2.0x (default: 1.0x)\n\n### Constraints\n- Colors must maintain sufficient contrast for visibility\n- Ship scale cannot make ships larger than 1/4 tunnel radius\n- Starting swarm size capped at 10 to prevent performance issues\n- Speed and difficulty multipliers limited to prevent unplayable extremes\n- All settings have \"Reset to Default\" option\n\n### Edit Mode UI\n- Collapsible panel on the left side in edit mode\n- Organized into sections: Ship, Swarm, Tunnel, Gameplay\n- Live preview of changes as sliders/pickers are adjusted\n- \"Play\" button to test changes immediately\n\n## Control Design\n\n**3D Third-Person Movement Controls**\n\nThe game uses simple, intuitive controls optimized for both mobile and desktop:\n\n### Mobile (Touch)\n- **Single Virtual Joystick**: Appears where player touches in the lower half of screen\n  - Joystick controls lead ship position within the tunnel (X/Y movement relative to tunnel center)\n  - Base moves with finger if dragged beyond max radius\n  - Dead zone: 10% of max radius\n  - Max radius: 80 pixels\n  - Output: Analog X/Y values (-1 to +1)\n- **Movement Mapping**:\n  - Joystick X-axis: Move ship left/right around tunnel circumference\n  - Joystick Y-axis: Move ship up/down (closer/farther from tunnel center)\n- **Auto-Forward**: Ship always moves forward automatically\n- **Pause Button**: Small button in top-right corner\n\n### Desktop (Keyboard + Mouse)\n- **WASD / Arrow Keys**: Move lead ship within tunnel\n  - W/Up: Move away from tunnel center (outward)\n  - S/Down: Move toward tunnel center (inward)\n  - A/Left: Move counterclockwise around tunnel\n  - D/Right: Move clockwise around tunnel\n- **Mouse Movement** (Alternative): Move mouse to position ship\n  - Mouse position relative to screen center maps to ship position in tunnel\n  - Smooth interpolation for natural feel\n  - No pointer lock needed (cursor visible)\n- **Space**: Pause game\n- **Escape**: Return to menu\n\n### Movement Behavior\n- **Smooth Interpolation**: Ship position smoothly interpolates toward target position (no instant snapping)\n- **Speed Scaling**: Movement speed scales with tunnel speed to maintain consistent feel\n- **Boundary Clamping**: Ship cannot move outside tunnel radius\n- **Swarm Following**: Clone ships follow lead ship with slight delay and organic trailing\n  - Each ship follows the position of the ship ahead of it\n  - Slight randomness in follow distance creates organic swarm feel\n  - Ships avoid overlapping each other\n\n### Input Handling\n- Clear all input states on window blur\n- Prevent default browser behavior for used keys\n- Track touch IDs to prevent interference between UI and joystick\n- Normalize all input to -1 to +1 range for consistent behavior\n\n## Art Style and Visual Theme\n\n**Low-Poly Sci-Fi Aesthetic**\n\nThe game embraces a clean, geometric low-poly art style with vibrant neon accents:\n\n### Overall Style\n- **Geometry**: Faceted low-poly models with visible edges (no smooth shading)\n- **Color Palette**: Deep space colors (purples, blues, blacks) contrasted with bright neon highlights (cyan, magenta, green, gold)\n- **Lighting**: Dramatic rim lighting, glowing materials, particle effects\n- **Mood**: Futuristic, energetic, arcade-inspired\n\n### Asset Specifications\n\n**Ships** (Code-Rendered with Three.js):\n- Simple geometric shapes (tetrahedron or wedge-based)\n- 20-30 polygons per ship\n- Glowing engine trails (particle system)\n- Pulsing emission on edges\n- Size: ~1 unit (relative to tunnel scale)\n\n**Tunnel** (Code-Rendered):\n- Cylindrical mesh with faceted surface (32-48 segments around circumference)\n- Animated glowing lines/veins running along surface\n- Subtle texture variation using vertex colors\n- Fog shader for depth\n\n**Collectibles** (Code-Rendered):\n- **Orbs**: Icosphere geometry (low-poly sphere), glowing green, floating particle ring\n- **Clusters**: Multiple small orbs orbiting a center point, golden glow\n- Gentle rotation and bob animation\n- Size: 0.5-0.7 units\n\n**Obstacles** (Code-Rendered):\n- **Spikes**: Sharp pyramidal shapes protruding from walls, red/orange glow\n- **Barriers**: Rotating rectangular planes with hazard stripes, red edges\n- **Gaps**: Sections where tunnel narrows with warning indicators\n- Consistent red/orange danger color scheme\n\n**Effects** (Particle Systems):\n- **Collection Effect**: Burst of colored particles matching collectible type\n- **Destruction Effect**: Explosion of ship-colored fragments\n- **Ship Trails**: Persistent glowing trails behind each ship\n- **Milestone Effect**: Ring-shaped particle burst expanding from swarm\n- **Ambient Particles**: Floating energy motes in tunnel\n\n### Visual Consistency\n- All elements use the same low-poly aesthetic (no mixing with realistic or high-poly assets)\n- Consistent edge highlighting across all objects\n- Unified color temperature (cool blues/purples with warm accent colors)\n- Particle effects use additive blending for glow\n- All geometry uses flat shading to emphasize faceted look\n\n### Technical Rendering\n- Use Three.js for 3D rendering\n- MeshBasicMaterial with emissive properties for glowing elements\n- Simple directional lighting + ambient light\n- Post-processing bloom effect for glow enhancement (if performance allows)\n- Particle systems using Points or Sprites\n- Efficient instancing for swarm ships (all use same geometry)", "persistent_storage_config": {"leaderboard": {"sort_ascending": false}}, "game_metadata": {"game_id": "01KT6GYXBW4Q6NTTTVNMHR85ZJ", "title": "Wormhole Swarm", "description": "Guide your clone ship swarm through an endless wormhole! Collect glowing orbs to multiply your fleet while dodging deadly obstacles.", "thumbnail_url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_102494898897174822368/1780538878166_01KT866A6PA0VTR4WNA3ZQ4HYC.ChatGPT Image Jun 4, 2026, 02_07_32 PM.png", "created_at": 1780483222, "updated_at": 1780611872158.7258, "art_style": null}, "task_plan": null}