{"asset_map": {"mutant_monster": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mutant_monster_21539b07-3177-4dc9-91c9-ffdfe7fc2cd4.webp", "type": "image", "prompt": "Transform the character in image 1 by removing all cybernetic implants, metal parts, and neon elements. Change the style to a realistic organic biological mutant creature. Focus on grotesque textures, muscular definition, exposed flesh, and natural mutations. Maintain the exact menacing pose and framing. Professional quality, high detail, transparent background, front view.", "description": "A realistic organic mutant beast with grotesque biological features and no cybernetics.", "subtype": "character", "aspect_ratio": [832, 1074], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "building_texture_1": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/building_texture_1_3a8e3b0c-4d75-4de0-8b28-d503a215cc84.webp", "type": "image", "prompt": "Transform the skyscraper facade in image 1 by removing all cyberpunk neon and sci-fi elements. Change the style to a realistic modern office building with clear reflective glass windows and clean concrete and steel frames. Ensure the texture remains perfectly seamless and tileable in both horizontal and vertical directions for a professional architectural look.", "description": "A seamless, tileable realistic modern office building facade with reflective glass and steel.", "subtype": "ground", "aspect_ratio": [832, 1248], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "building_texture_2": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/building_texture_2_1dbb3c51-057e-48cb-b252-131dea7b564b.webp", "type": "image", "prompt": "Transform the building texture in image 1 by removing all neon strips and cyberpunk elements. Change the style to a realistic modern commercial or apartment building facade. Include clean concrete and brick textures with standard office windows. Ensure the result is a high-resolution, professional quality texture that is fully tileable and repeats seamlessly in both horizontal and vertical directions.", "description": "Seamless tileable realistic modern commercial building texture with brick and concrete details.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "street_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/street_texture_2d6dd708-e569-402b-b6ea-393e2969297f.webp", "type": "image", "prompt": "Generate a high-resolution, realistic asphalt street texture. The texture must include natural road markings such as double yellow center lines and white lane markings. The style should be contemporary and grounded, removing all futuristic or cyberpunk elements from image 1. Designed to be a fully tileable texture that repeats seamlessly in both horizontal and vertical directions for a top-down or ground-plane view.", "description": "A realistic, seamless asphalt road texture with yellow center lines and white lane markings.", "subtype": "ground", "aspect_ratio": [832, 1248], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "crosshair_normal": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/crosshair_normal_7d5130fe-8542-45cd-a74e-ad5c329ea583.webp", "type": "image", "prompt": "Generate a clean, minimalist sniper crosshair on a transparent background. Thin bright cyan cross lines with a small 4-6 pixel circle in the center. High-tech precise design, 2-3 pixel line thickness, crisp edges with a slight outer glow. Professional HUD element.", "description": "Minimalist cyan sniper crosshair for standard view.", "subtype": "ui", "aspect_ratio": [810, 822], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "crosshair_scope": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/crosshair_scope_9dc3ce38-0e8c-407d-b820-f5d23f929ad9.webp", "type": "image", "prompt": "Generate a detailed military sniper scope reticle on a transparent background. Mil-dot style with a central cross, small dots along the lines for range estimation, and a thin outer circle. Bright neon green color with a subtle night-vision glow. Fine 1-2 pixel lines, high precision, professional military tech style.", "description": "Detailed military mil-dot reticle for zoomed sniper mode.", "subtype": "ui", "aspect_ratio": [863, 876], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "scope_overlay": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/scope_overlay_7e9b5a58-ae54-47fa-afba-f63b10578960.webp", "type": "image", "prompt": "Generate a 1280x720 wide image of a sniper scope overlay. The center is a large, perfectly circular transparent area (60% of the image), surrounded by a thick solid black vignette that fades into the edges. Include subtle lens details like a faint crosshatch pattern and dark gray metallic reflections in the black area. Realistic rifle scope aesthetic.", "description": "Black circular vignette overlay for the sniper scope view.", "subtype": "ui", "aspect_ratio": [1327, 1018], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "muzzle_flash": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/muzzle_flash_48a2c017-989b-4d4c-a471-a5a20fcb64e0.webp", "type": "image", "prompt": "Generate a bright muzzle flash effect on a transparent background. Explosive star-burst shape with a bright white-yellow core and radiating tongues of flame in orange and red. Includes small flying sparks. High energy, realistic weapon fire effect, designed for additive blending.", "description": "Bright energetic muzzle flash effect for the sniper rifle.", "subtype": "effect", "aspect_ratio": [778, 816], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "hit_spark": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/hit_spark_5cc7d67c-acaa-409a-a1e3-a6cf77550453.webp", "type": "image", "prompt": "Generate a bright impact spark burst on a transparent background. Multiple small orange-yellow particles flying outward from a white-hot center point. Energetic metallic impact effect, high-speed motion feel, crisp details.", "description": "Impact spark effect for successful hits on targets.", "subtype": "effect", "aspect_ratio": [1006, 859], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "ui_panel": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ui_panel_87dd2b2d-d75e-4609-bf14-13733bfbf94a.webp", "type": "image", "prompt": "Generate a rectangular UI panel on a transparent background with rounded corners. Frosted glass material with 70% transparency, dark blue-gray color. Features a 2px glowing cyan border, subtle scan-line texture, and small tech-style corner accent marks. Cyberpunk HUD aesthetic, clean and professional.", "description": "Tech-style semi-transparent UI background for the HUD.", "subtype": "ui", "aspect_ratio": [1231, 757], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "health_bar_fill": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/health_bar_fill_88a6d80c-0710-4eb3-9452-2e1f1e0aae44.webp", "type": "image", "prompt": "Generate a 200x20 horizontal health bar fill texture on a transparent background. Bright cyan-blue energy gradient from left to right. Includes subtle horizontal scan lines and a slight outer glow. Futuristic energy bar style, high-tech interface element.", "description": "Glowing energy fill texture for the health bar.", "subtype": "ui", "aspect_ratio": [1230, 225], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "ammo_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ammo_icon_8b95d392-1b00-4201-99b6-b9f7d4df97d5.webp", "type": "image", "prompt": "Generate a clean icon of a single sniper rifle cartridge viewed from the side on a transparent background. Features a polished brass metallic casing with a pointed silver bullet tip. Realistic style with a slight metallic shine, studio lighting, high resolution.", "description": "Realistic sniper bullet icon for the HUD ammo display.", "subtype": "ui", "aspect_ratio": [1215, 246], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "sniper_rifle_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/sniper_rifle_sound_7c811a9a-e379-4622-9231-df8c94300b51.mp3", "type": "audio", "prompt": "Powerful military sniper rifle gunshot, deep heavy high-caliber shot with a strong bass thud, sharp initial mechanical crack followed by a deep atmospheric boom and a realistic urban echo tail, authoritative and satisfying, 24-bit high fidelity.", "description": "A powerful high-caliber sniper rifle shot for impactful combat feedback.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2, "loop": false, "animation": null}, "empty_gun_click": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/empty_gun_click_a5f5dbea-47a9-4f1d-baee-de076dea54ed.mp3", "type": "audio", "prompt": "Dry fire empty gun click, realistic metallic gun mechanism, small hollow mechanical snap, disappointing and light, clean sound with no reverb, indicates out of ammo.", "description": "A metallic click sound when the player tries to fire an empty weapon.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.82, "loop": false, "animation": null}, "reload_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/reload_sound_cb9fe616-020f-41a8-89ce-76239bc83b89.mp3", "type": "audio", "prompt": "Tactical military sniper rifle reload sequence, metallic magazine ejection click, smooth slide insertion, and a heavy clack of the bolt chambering a round, professional and mechanical, clean foley recording.", "description": "A multi-stage mechanical reload sequence for the sniper rifle.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.4, "loop": false, "animation": null}, "monster_hit_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/monster_hit_sound_50d80e9d-6b3f-43c1-a013-eb69d18fac72.mp3", "type": "audio", "prompt": "Heavy wet impact sound, bullet hitting organic flesh and bone, slight squelch mixed with a brief low-pitched creature grunt, satisfying hit confirmation, brutal sci-fi texture.", "description": "Impact sound for bullets hitting the mutant monsters.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.37, "loop": false, "animation": null}, "monster_death_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/monster_death_sound_3ea58be0-98a6-43f0-a626-ea93dc6a50d2.mp3", "type": "audio", "prompt": "Sci-fi mutant creature death cry, deep organic roar fading into a high-tech electronic glitch and power-down hum, satisfying kill confirmation with a 2-second tail fade out.", "description": "Sound effect for a monster being eliminated, blending organic and tech elements.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.7, "loop": false, "animation": null}, "monster_roar": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/monster_roar_55800052-ca07-45ed-bfd8-5cb7dc9ee434.mp3", "type": "audio", "prompt": "Deep threatening monster roar, low-frequency guttural growl with subtle mechanical undertones, echoing through a city street, menacing and atmospheric, partially organic partially synthetic.", "description": "Ambient menacing roar to build atmosphere and tension.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 3.23, "loop": false, "animation": null}, "player_damage_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/player_damage_sound_34cbf8b5-5602-4ea0-bec9-816bb485428d.mp3", "type": "audio", "prompt": "Heavy bass-heavy thud impact, electronic HUD distortion sound, urgent warning tone, alarming and painful, conveys system damage in a sci-fi environment.", "description": "Urgent feedback sound when the player takes damage.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.61, "loop": false, "animation": null}, "wave_complete_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/wave_complete_sound_aef1b506-24f9-40a6-b634-38bc499ec70d.mp3", "type": "audio", "prompt": "Sci-fi UI achievement sound, triumphant rising electronic chime, bright digital sequence with a satisfying resolution, clear and clean success feedback.", "description": "Short triumphant chime for completing a monster wave.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.76, "loop": false, "animation": null}, "game_over_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/game_over_sound_d18df3d3-07cd-486a-af96-853cea8de081.mp3", "type": "audio", "prompt": "System failure game over sound, descending ominous electronic tones, power-down glitch effect with a low-frequency drone, somber and definitive defeat.", "description": "Ominous descending tones for a game over state.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 4, "loop": false, "animation": null}, "victory_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/victory_sound_f14c03b4-6a05-4db5-b1e3-7361141753fd.mp3", "type": "audio", "prompt": "Epic sci-fi victory fanfare, rising heroic synth melody, celebratory digital chimes, powerful and satisfying resolution, cinematic game win theme.", "description": "Triumphant fanfare for completing the game.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 3.58, "loop": false, "animation": null}, "background_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/background_music_281eee53-e38d-4707-b570-d25c843dfaa5.mp3", "type": "audio", "prompt": "Cinematic tactical military action music, realistic orchestral arrangement. Features heavy rhythmic percussion, low brass swells, and driving staccato strings. Focus on a tense, high-stakes atmosphere suitable for a modern warfare setting. Fast-paced 120-140 BPM. No synths, no electronic elements, no cyberpunk aesthetic. Features a clear progression of tension with deep double bass and sharp orchestral hits. Instrumental only.", "description": "Tense tactical military orchestral music for intense combat and stealth gameplay.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 60.02938775510204, "loop": true, "animation": null}, "menu_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/menu_music_b663bcf4-956d-4840-bcb2-a139edb4976d.mp3", "type": "audio", "prompt": "Futuristic cinematic menu music, 90 BPM, ambient synth textures, mysterious and inviting melody, minimal percussion, low intensity, elegant cyberpunk atmosphere, loopable.", "description": "Calm but mysterious music for the title screen and menu.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 60.02938775510204, "loop": true, "animation": null}, "missile_launch_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/missile_launch_sound_335dc526-6f3d-48f1-9122-c2ae3360fee2.mp3", "type": "audio", "prompt": "High-quality military missile launch sound effect. Starts with a sharp, powerful explosive ignition blast followed by a pressurized whooshing rocket propellant sound. Realistic jet engine trail that fades out quickly. Cinematic and punchy, similar to a helicopter-mounted rocket pod or shoulder-fired launcher. Clean, heavy low-end impact, no background noise, one-shot.", "description": "Short, punchy military missile launch with a sharp ignition blast and fading rocket trail.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.430453514739229, "loop": false, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Urban Hunter</title>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n    <link href=\"https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@400;600&family=Press+Start+2P&display=swap\" rel=\"stylesheet\">\n    <style>\n        :root {\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --transition-slow: 500ms;\n            --ease-out: cubic-bezier(0.0, 0, 0.2, 1);\n            --ease-bounce: cubic-bezier(0.68, -0.6, 0.32, 1.6);\n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n            --color-primary: #ffcc00;\n            --color-danger: #e3000b;\n            --color-success: #00852b;\n            --color-panel: rgba(20, 20, 20, 0.92);\n            --color-border: rgba(255, 204, 0, 0.5);\n            --lego-red: #e3000b;\n            --lego-blue: #0055bf;\n            --lego-green: #00852b;\n            --lego-yellow: #ffcc00;\n            --lego-orange: #ff6600;\n            --lego-white: #f4f4f4;\n        }\n\n        *, *::before, *::after {\n            user-select: none !important;\n            -webkit-user-select: none !important;\n            -moz-user-select: none !important;\n            -ms-user-select: none !important;\n            -webkit-touch-callout: none !important;\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#game-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 Elements */\n        .hud-panel {\n            position: absolute;\n            background: var(--color-panel);\n            border: 3px solid var(--color-border);\n            border-radius: 0px;\n            padding: 8px 16px;\n            color: #fff;\n            font-family: 'Press Start 2P', monospace;\n            text-shadow: 2px 2px 0px rgba(0,0,0,0.8);\n            box-shadow: 4px 4px 0px rgba(0,0,0,0.5);\n        }\n\n        #hud-health {\n            bottom: 170px;\n            left: 16px;\n        }\n\n        #hud-ammo {\n            bottom: 16px;\n            right: 16px;\n        }\n\n        #hud-score {\n            top: 16px;\n            left: 16px;\n        }\n\n        #hud-wave {\n            top: 16px;\n            right: 16px;\n        }\n\n        #hud-timer {\n            top: 16px;\n            left: 50%;\n            transform: translateX(-50%);\n            text-align: center;\n        }\n\n        #hud-timer .hud-value {\n            font-size: 22px;\n            color: var(--lego-yellow);\n        }\n\n        #hud-timer .hud-value.urgent {\n            color: var(--lego-red);\n            animation: pulse-urgent 0.5s infinite;\n        }\n\n        @keyframes pulse-urgent {\n            0%, 100% { opacity: 1; }\n            50% { opacity: 0.5; }\n        }\n\n        #hud-civilians {\n            top: 80px;\n            left: 50%;\n            transform: translateX(-50%);\n            text-align: center;\n        }\n\n        #hud-civilians .hud-value {\n            font-size: 14px;\n            color: var(--lego-green);\n        }\n\n        #hud-support {\n            top: 80px;\n            right: 16px;\n        }\n\n        #hud-support .hud-value {\n            font-size: 16px;\n            color: var(--lego-yellow);\n        }\n\n        #hud-support .hud-value.imminent {\n            color: var(--lego-green);\n            animation: pulse-urgent 0.5s infinite;\n        }\n\n        .hud-label {\n            font-size: 9px;\n            color: var(--lego-yellow);\n            margin-bottom: 4px;\n            text-transform: uppercase;\n            letter-spacing: 1px;\n        }\n\n        .hud-value {\n            font-size: 18px;\n            font-weight: 700;\n            color: var(--lego-white);\n        }\n\n        .hud-value.danger {\n            color: var(--lego-red);\n        }\n\n        /* Health bar */\n        .health-bar-bg {\n            width: 160px;\n            height: 16px;\n            background: rgba(255,255,255,0.1);\n            border-radius: 0px;\n            border: 2px solid rgba(255,255,255,0.3);\n            overflow: hidden;\n            margin-top: 4px;\n        }\n\n        .health-bar-fill {\n            height: 100%;\n            background: var(--lego-green);\n            border-radius: 0px;\n            transition: width var(--transition-fast) var(--ease-out);\n        }\n\n        .health-bar-fill.low {\n            background: var(--lego-red);\n        }\n\n        /* Modern Tech Crosshair */\n        #crosshair {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            width: 64px;\n            height: 64px;\n            pointer-events: none;\n            display: none;\n        }\n\n        #tech-crosshair {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            width: 80px;\n            height: 80px;\n            pointer-events: none;\n        }\n\n        /* Helicopter HUD crosshair - green mil style */\n        #tech-crosshair .crosshair-ring {\n            position: absolute;\n            top: 50%; left: 50%;\n            transform: translate(-50%, -50%);\n            width: 44px; height: 44px;\n            border: 1.5px solid rgba(0, 255, 80, 0.85);\n            border-radius: 50%;\n            box-shadow: 0 0 6px rgba(0, 255, 80, 0.4);\n        }\n        #tech-crosshair .crosshair-inner {\n            display: none;\n        }\n        #tech-crosshair .crosshair-dot {\n            position: absolute;\n            top: 50%; left: 50%;\n            transform: translate(-50%, -50%);\n            width: 3px; height: 3px;\n            background: rgba(0, 255, 80, 0.95);\n            border-radius: 50%;\n            box-shadow: 0 0 4px rgba(0,255,80,0.7);\n        }\n        /* Lines: gap in center, extend outward */\n        #tech-crosshair .crosshair-line {\n            width: 100%;\n            height: 3px;\n            background: rgba(30, 40, 30, 0.25);\n        }\n\n        #scope-overlay.active {\n            display: block;\n        }\n\n        /* Mobile controls */\n        .mobile-btn {\n            position: absolute;\n            width: 100px;\n            height: 100px;\n            border-radius: 50%;\n            background: rgba(227, 0, 11, 0.3);\n            border: 4px solid #ffcc00;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-family: 'Press Start 2P', monospace;\n            font-size: 10px;\n            color: #ffcc00;\n            user-select: none;\n            -webkit-user-select: none;\n            transition: transform 0.1s;\n            box-shadow: 0 0 15px rgba(0,0,0,0.5);\n            pointer-events: auto;\n            z-index: 50;\n        }\n\n        .mobile-btn:active {\n            transform: scale(0.9);\n            background: rgba(227, 0, 11, 0.6);\n        }\n\n        #btn-fire {\n            bottom: 120px;\n            right: 40px;\n            width: 130px;\n            height: 130px;\n            font-size: 14px;\n            background: rgba(227, 0, 11, 0.7);\n        }\n\n        #btn-missile {\n            bottom: 280px;\n            right: 40px;\n            width: 90px;\n            height: 90px;\n            background: rgba(0, 85, 191, 0.7);\n        }\n\n        #btn-reload {\n            bottom: 120px;\n            left: 200px;\n            width: 80px;\n            height: 80px;\n            background: rgba(80, 80, 80, 0.7);\n            font-size: 8px;\n        }\n\n        /* Virtual Joystick */\n        #virtual-joystick {\n            position: absolute;\n            bottom: 280px;\n            left: 20px;\n            width: 140px;\n            height: 140px;\n            display: none;\n            z-index: 20;\n            pointer-events: auto;\n        }\n        #joystick-base {\n            position: absolute;\n            width: 140px;\n            height: 140px;\n            border-radius: 50%;\n            background: rgba(0, 255, 80, 0.1);\n            border: 2px solid rgba(0, 255, 80, 0.4);\n            box-shadow: inset 0 0 20px rgba(0, 255, 80, 0.1);\n        }\n        #joystick-knob {\n            position: absolute;\n            width: 50px;\n            height: 50px;\n            border-radius: 50%;\n            background: rgba(0, 255, 80, 0.4);\n            border: 2px solid rgba(0, 255, 80, 0.8);\n            left: 45px;\n            top: 45px;\n            transition: none;\n            box-shadow: 0 0 10px rgba(0, 255, 80, 0.3);\n        }\n        /* Altitude buttons */\n        #btn-alt-up, #btn-alt-down {\n            position: absolute;\n            width: 60px;\n            height: 60px;\n            border-radius: 50%;\n            background: rgba(0, 255, 80, 0.2);\n            border: 2px solid rgba(0, 255, 80, 0.5);\n            display: none;\n            align-items: center;\n            justify-content: center;\n            pointer-events: auto;\n            z-index: 20;\n            user-select: none;\n            -webkit-user-select: none;\n            -moz-user-select: none;\n            -ms-user-select: none;\n            -webkit-touch-callout: none;\n            -webkit-tap-highlight-color: transparent;\n        }\n        #btn-alt-up::after {\n            content: '';\n            width: 0; \n            height: 0; \n            border-left: 10px solid transparent;\n            border-right: 10px solid transparent;\n            border-bottom: 14px solid rgba(0, 255, 80, 0.9);\n            pointer-events: none;\n        }\n        #btn-alt-down::after {\n            content: '';\n            width: 0; \n            height: 0; \n            border-left: 10px solid transparent;\n            border-right: 10px solid transparent;\n            border-top: 14px solid rgba(0, 255, 80, 0.9);\n            pointer-events: none;\n        }\n        #btn-alt-up {\n            bottom: 390px;\n            left: 170px;\n        }\n        #btn-alt-down {\n            bottom: 310px;\n            left: 170px;\n        }\n        #btn-alt-up:active, #btn-alt-down:active {\n            background: rgba(0, 255, 80, 0.5);\n        }\n        /* Speed indicator */\n        #hud-speed {\n            bottom: 170px;\n            left: 16px;\n        }\n        #hud-speed .hud-value {\n            font-size: 14px;\n            color: rgba(0, 255, 80, 0.9);\n        }\n\n        #btn-fire {\n            bottom: 130px;\n            right: 40px;\n            left: auto;\n            width: 110px;\n            height: 110px;\n            font-size: 12px;\n            background: var(--lego-red);\n            border: 3px solid var(--lego-yellow);\n            border-radius: 50%;\n            color: var(--lego-yellow);\n            position: absolute;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            box-shadow: 0 0 15px rgba(227, 0, 11, 0.5);\n            z-index: 30;\n            pointer-events: auto;\n        }\n\n        #btn-fire:active {\n            background: var(--lego-orange);\n            transform: scale(0.95);\n        }\n\n        #btn-missile {\n            bottom: 260px;\n            right: 40px;\n            width: 90px;\n            height: 90px;\n            font-size: 10px;\n            background: var(--lego-blue);\n            border: 3px solid var(--lego-yellow);\n            border-radius: 50%;\n            color: var(--lego-yellow);\n            z-index: 30;\n            pointer-events: auto;\n        }\n\n        #btn-reload {\n            bottom: 130px;\n            right: 170px;\n            width: 90px;\n            height: 90px;\n            font-size: 10px;\n            background: #444;\n            border: 2px solid #888;\n            border-radius: 50%;\n            color: #fff;\n            z-index: 30;\n            pointer-events: auto;\n        }\n\n        /* Game screens */\n        .game-screen {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            background: rgba(0, 5, 15, 0.95);\n            z-index: 100;\n            pointer-events: auto;\n        }\n\n        .game-screen.hidden {\n            display: none;\n        }\n\n        .screen-title {\n            font-family: 'Press Start 2P', monospace;\n            font-size: 28px;\n            font-weight: 900;\n            color: var(--lego-yellow);\n            text-shadow: 4px 4px 0px rgba(0,0,0,0.8), -1px -1px 0 var(--lego-red);\n            margin-bottom: 16px;\n            text-align: center;\n        }\n\n        .screen-subtitle {\n            font-family: 'Press Start 2P', monospace;\n            font-size: 12px;\n            color: var(--lego-white);\n            margin-bottom: 48px;\n            text-align: center;\n            line-height: 1.8;\n        }\n\n        .screen-btn {\n            width: 280px;\n            height: 64px;\n            background: var(--lego-blue);\n            border: 3px solid var(--lego-yellow);\n            border-radius: 0px;\n            color: var(--lego-yellow);\n            font-family: 'Press Start 2P', monospace;\n            font-size: 14px;\n            font-weight: 700;\n            cursor: pointer;\n            transition: all var(--transition-fast) var(--ease-out);\n            margin: 8px 0;\n            box-shadow: 4px 4px 0px rgba(0,0,0,0.6);\n        }\n        .upgrade-title {\n            font-family: 'Press Start 2P', monospace;\n            font-size: 20px;\n            font-weight: 900;\n            color: var(--lego-yellow);\n            text-shadow: 3px 3px 0px rgba(0,0,0,0.8);\n            margin-bottom: 12px;\n        }\n\n        .upgrade-points {\n            font-family: 'Press Start 2P', monospace;\n            font-size: 14px;\n            color: var(--lego-blue);\n            margin-bottom: 20px;\n            text-shadow: 2px 2px 0px rgba(0,0,0,0.5);\n        }\n\n        .upgrade-item {\n            width: 360px;\n            background: rgba(255,255,255,0.05);\n            border: 3px solid var(--lego-blue);\n            border-radius: 0px;\n            padding: 16px 20px;\n            margin: 7px 0;\n            display: flex;\n            flex-direction: column;\n            gap: 8px;\n            box-shadow: 3px 3px 0px rgba(0,0,0,0.4);\n        }\n\n        .upgrade-item-header {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n        }\n\n        .upgrade-item-name {\n            font-family: 'Press Start 2P', monospace;\n            font-size: 11px;\n            color: var(--lego-white);\n        }\n\n        .upgrade-item-level {\n            font-family: 'Press Start 2P', monospace;\n            font-size: 10px;\n            color: var(--lego-orange);\n        }\n\n        .upgrade-item-desc {\n            font-family: 'Press Start 2P', monospace;\n            font-size: 8px;\n            color: rgba(255,255,255,0.7);\n            line-height: 1.6;\n        }\n\n        .upgrade-btn {\n            width: 100%;\n            height: 48px;\n            background: var(--lego-green);\n            border: 2px solid var(--lego-yellow);\n            border-radius: 0px;\n            color: var(--lego-yellow);\n            font-family: 'Press Start 2P', monospace;\n            font-size: 10px;\n            font-weight: 700;\n            cursor: pointer;\n            transition: all var(--transition-fast);\n            margin-top: 6px;\n            box-shadow: 3px 3px 0px rgba(0,0,0,0.5);\n        }\n\n        .upgrade-btn:hover, .upgrade-btn:active {\n            background: var(--lego-blue);\n            transform: translate(2px, 2px);\n            box-shadow: 1px 1px 0px rgba(0,0,0,0.5);\n        }\n\n        .upgrade-btn:disabled {\n            opacity: 0.35;\n            cursor: not-allowed;\n            border-color: rgba(255,255,255,0.2);\n            color: rgba(255,255,255,0.4);\n            background: #333;\n        }\n\n        .upgrade-btn:disabled:hover {\n            background: #333;\n            box-shadow: 3px 3px 0px rgba(0,0,0,0.5);\n            transform: none;\n        }\n\n        .upgrade-start-btn {\n            width: 280px;\n            height: 56px;\n            background: var(--lego-red);\n            border: 3px solid var(--lego-yellow);\n            border-radius: 0px;\n            color: var(--lego-yellow);\n            font-family: 'Press Start 2P', monospace;\n            font-size: 12px;\n            font-weight: 700;\n            cursor: pointer;\n            margin-top: 24px;\n            transition: all var(--transition-fast);\n            box-shadow: 4px 4px 0px rgba(0,0,0,0.6);\n        }\n\n        .upgrade-start-btn:hover, .upgrade-start-btn:active {\n            background: var(--lego-orange);\n            transform: translate(2px, 2px);\n            box-shadow: 2px 2px 0px rgba(0,0,0,0.6);\n        }\n\n        /* Edit mode info */\n        #edit-info {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            font-family: 'Press Start 2P', monospace;\n            font-size: 16px;\n            color: var(--lego-yellow);\n            text-align: center;\n            text-shadow: 3px 3px 0px rgba(0,0,0,0.8);\n            pointer-events: none;\n        }\n\n        #loading-screen {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: #0a0a1a;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 200;\n            transition: opacity 0.5s;\n        }\n        #loading-screen.hidden { opacity: 0; pointer-events: none; }\n        #loading-title {\n            font-family: 'Press Start 2P', monospace;\n            font-size: 28px;\n            color: var(--lego-yellow);\n            text-shadow: 4px 4px 0px rgba(0,0,0,0.8), -1px -1px 0 var(--lego-red);\n            margin-bottom: 40px;\n        }\n        #loading-bar-bg {\n            width: 320px; height: 20px;\n            background: rgba(255,255,255,0.1);\n            border: 3px solid var(--lego-yellow);\n            box-shadow: 4px 4px 0px rgba(0,0,0,0.5);\n        }\n        #loading-bar-fill {\n            height: 100%; width: 0%;\n            background: var(--lego-yellow);\n            transition: width 0.2s;\n        }\n        #loading-text {\n            font-family: 'Press Start 2P', monospace;\n            font-size: 10px;\n            color: rgba(255,255,255,0.7);\n            margin-top: 16px;\n        }\n\n        #muzzle-flash, #damage-flash {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            pointer-events: none;\n            z-index: 200;\n            opacity: 0;\n        }\n        #damage-flash {\n            background: rgba(227, 0, 11, 0.3);\n            z-index: 190;\n        }\n\n        #wave-announce {\n            position: absolute;\n            top: 25%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            font-family: 'Press Start 2P', monospace;\n            font-size: 32px;\n            color: var(--lego-yellow);\n            text-shadow: 4px 4px 0px rgba(0,0,0,0.8);\n            pointer-events: none;\n            z-index: 100;\n            opacity: 0;\n            transition: opacity 0.5s, transform 0.5s;\n        }\n        #wave-announce.show {\n            opacity: 1;\n            transform: translate(-50%, -50%) scale(1.2);\n        }\n\n        #kill-feed {\n            position: absolute;\n            bottom: 240px;\n            left: 50%;\n            transform: translateX(-50%);\n            font-family: 'Press Start 2P', monospace;\n            font-size: 10px;\n            color: #69f0ae;\n            text-shadow: 2px 2px 0px rgba(0,0,0,0.8);\n            pointer-events: none;\n            z-index: 50;\n            opacity: 0;\n            transition: opacity 0.3s;\n            text-align: center;\n            width: 80%;\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"game-canvas\"></canvas>\n        <div id=\"loading-screen\">\n            <div id=\"loading-title\">Urban Hunter</div>\n            <div id=\"loading-bar-bg\"><div id=\"loading-bar-fill\"></div></div>\n            <div id=\"loading-text\">Loading assets...</div>\n        </div>\n        <div class=\"ui-overlay\" id=\"ui-overlay\">\n            <div id=\"scope-overlay\"></div>\n            <div id=\"cockpit-frame\"></div>\n            <img id=\"crosshair\" src=\"\" alt=\"\">\n            <div id=\"tech-crosshair\">\n                <div class=\"crosshair-ring\"></div>\n                <div class=\"crosshair-inner\"></div>\n                <div class=\"crosshair-dot\"></div>\n                <div class=\"crosshair-line top\"></div>\n                <div class=\"crosshair-line bottom\"></div>\n                <div class=\"crosshair-line left\"></div>\n                <div class=\"crosshair-line right\"></div>\n                <div class=\"crosshair-bracket tl\"></div>\n                <div class=\"crosshair-bracket tr\"></div>\n                <div class=\"crosshair-bracket bl\"></div>\n                <div class=\"crosshair-bracket br\"></div>\n            </div>\n            <div id=\"muzzle-flash\"></div>\n            <div id=\"damage-flash\"></div>\n            <div id=\"wave-announce\"></div>\n            <div id=\"kill-feed\"></div>\n\n            <div class=\"hud-panel\" id=\"hud-health\" style=\"display:none;\">\n                <div class=\"hud-label\">Health</div>\n                <div class=\"health-bar-bg\"><div class=\"health-bar-fill\" id=\"health-fill\"></div></div>\n            </div>\n            <div class=\"hud-panel\" id=\"hud-ammo\">\n                <div class=\"hud-label\">Ammo</div>\n                <div class=\"hud-value\" id=\"ammo-value\">5/5</div>\n            </div>\n            <div class=\"hud-panel\" id=\"hud-score\">\n                <div class=\"hud-label\">Score</div>\n                <div class=\"hud-value\" id=\"score-value\">0</div>\n            </div>\n            <div class=\"hud-panel\" id=\"hud-wave\">\n                <div class=\"hud-label\">Level</div>\n                <div class=\"hud-value\" id=\"wave-value\">1</div>\n            </div>\n\n            <div class=\"hud-panel\" id=\"hud-timer\">\n                <div class=\"hud-label\">Monsters Left</div>\n                <div class=\"hud-value\" id=\"timer-value\">0</div>\n            </div>\n\n            <div class=\"hud-panel\" id=\"hud-civilians\">\n                <div class=\"hud-label\">Evacuation Zone</div>\n                <div class=\"hud-value\" id=\"civilians-value\">0 Safe</div>\n            </div>\n\n            <div class=\"hud-panel\" id=\"hud-support\">\n                <div class=\"hud-label\">\ud83e\ude96 Support</div>\n                <div class=\"hud-value\" id=\"support-value\">2:00</div>\n            </div>\n\n            <canvas id=\"radar-canvas\" width=\"200\" height=\"200\" style=\"position:absolute;bottom:16px;left:16px;border-radius:50%;border:2px solid rgba(0,255,80,0.6);background:rgba(0,20,0,0.7);box-shadow:0 0 14px rgba(0,255,80,0.4);\"></canvas>\n\n            <div id=\"virtual-joystick\">\n                <div id=\"joystick-base\"></div>\n                <div id=\"joystick-knob\"></div>\n            </div>\n            <div id=\"btn-alt-up\"></div>\n            <div id=\"btn-alt-down\"></div>\n\n            <div class=\"hud-panel\" id=\"hud-speed\" style=\"display:none;\">\n                <div class=\"hud-label\">Speed</div>\n                <div class=\"hud-value\" id=\"speed-value\">0 km/h</div>\n            </div>\n\n            <div class=\"mobile-btn\" id=\"btn-fire\">FIRE</div>\n            <div class=\"mobile-btn\" id=\"btn-missile\">MISSILE</div>\n            <div class=\"mobile-btn\" id=\"btn-reload\">RELOAD</div>\n            <!-- Game screens -->\n            <div class=\"game-screen\" id=\"start-screen\">\n                <div class=\"screen-title\">Urban Hunter</div>\n                <div class=\"screen-subtitle\">Eliminate all monsters to clear each level!<br>20 levels, increasing difficulty!</div>\n                <button class=\"screen-btn\" id=\"btn-start\">Start Game</button>\n                <button class=\"screen-btn\" id=\"btn-leaderboard\" style=\"background: var(--lego-green); margin-top: 8px;\">Leaderboard</button>\n                <div id=\"controls-panel\" style=\"margin-top: 28px; width: 520px; background: rgba(0,0,0,0.75); border: 4px solid rgba(255,204,0,0.7); padding: 28px 32px; box-sizing: border-box;\">\n                    <div style=\"font-family:'Press Start 2P',monospace; font-size:18px; color:var(--lego-yellow); text-align:center; margin-bottom:20px; letter-spacing:3px; text-shadow:2px 2px 0 rgba(0,0,0,0.8);\">\u2014 OPERATION GUIDE \u2014</div>\n                    <div id=\"controls-pc\" style=\"display:none;\">\n                        <div style=\"display:grid; grid-template-columns:1fr 1fr; gap:14px 24px;\">\n                            <div style=\"font-family:'Press Start 2P',monospace; font-size:15px; color:rgba(255,255,255,0.8); line-height:2.6; text-shadow:1px 1px 0 rgba(0,0,0,0.6);\">\ud83d\uddb1\ufe0f Move Mouse<br>\ud83d\uddb1\ufe0f Left Click<br>\ud83d\uddb1\ufe0f Hold Left<br>\u2328\ufe0f WASD<br>\u2328\ufe0f Q / E<br>\u2328\ufe0f R<br>\u2328\ufe0f F / SPACE</div>\n                            <div style=\"font-family:'Press Start 2P',monospace; font-size:15px; color:#ffffff; line-height:2.6; text-shadow:1px 1px 0 rgba(0,0,0,0.6);\">Aim<br>Shoot<br>Auto-Fire<br>Flight Move<br>Alt Up / Down<br>Reload<br>Missile</div>\n                        </div>\n                    </div>\n                    <div id=\"controls-mobile\" style=\"display:none;\">\n                        <div style=\"display:grid; grid-template-columns:1fr 1fr; gap:14px 24px;\">\n                            <div style=\"font-family:'Press Start 2P',monospace; font-size:15px; color:rgba(255,255,255,0.8); line-height:2.6; text-shadow:1px 1px 0 rgba(0,0,0,0.6);\">\ud83d\udc46 Drag Screen<br>\ud83d\udd79\ufe0f Joystick<br>\ud83d\udd3c\ud83d\udd3d Buttons<br>\ud83d\udd04 RELOAD Button<br>\ud83d\ude80 MISSILE Button</div>\n                            <div style=\"font-family:'Press Start 2P',monospace; font-size:15px; color:#ffffff; line-height:2.6; text-shadow:1px 1px 0 rgba(0,0,0,0.6);\">Aim + Fire<br>Flight Move<br>Alt Up / Down<br>Reload<br>Missile</div>\n                        </div>\n                    </div>\n                    <div style=\"margin-top:20px; font-family:'Press Start 2P',monospace; font-size:14px; color:#69f0ae; text-align:center; line-height:2.4; border-top:3px solid rgba(0,255,80,0.3); padding-top:16px; text-shadow:1px 1px 0 rgba(0,0,0,0.6);\">\ud83d\udca1 Headshots double damage! Protect civilians!<br>\ud83d\ude80 Homing missiles lock-on \u00b7 \ud83e\ude96 Support every 2m</div>\n                </div>\n                <script>\n                (function(){\n                    const m = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);\n                    document.getElementById('controls-pc').style.display = m ? 'none' : 'block';\n                    document.getElementById('controls-mobile').style.display = m ? 'block' : 'none';\n                })();\n                </script>\n            </div>\n\n            <div class=\"game-screen hidden\" id=\"gameover-screen\">\n                <div class=\"screen-title\" style=\"color: var(--color-danger);\">Evacuation Failed</div>\n                <div class=\"screen-subtitle\">The evacuation zone has been destroyed...</div>\n                <div class=\"final-score\" id=\"final-score-lose\">0</div>\n                <button class=\"screen-btn\" id=\"btn-upgrade\">Upgrade Weapons</button>\n                <button class=\"screen-btn\" id=\"btn-retry\" style=\"margin-top:8px;font-size:16px;height:48px;opacity:0.7;\">Skip Upgrade</button>\n                <button class=\"screen-btn\" id=\"btn-gameover-leaderboard\" style=\"background: var(--lego-green); margin-top: 16px;\">View Leaderboard</button>\n            </div>\n\n            <div id=\"upgrade-screen\" class=\"game-screen hidden\" style=\"overflow-y: auto; padding: 40px 0; justify-content: flex-start;\">\n                <div class=\"upgrade-title\">Weapon Upgrade</div>\n                <div class=\"upgrade-points\">Available Points: <span id=\"upgrade-points-value\">0</span></div>\n                <div class=\"upgrade-item\">\n                    <div class=\"upgrade-item-header\">\n                        <span class=\"upgrade-item-name\">\ud83d\udd25 Fire Rate</span>\n                        <span class=\"upgrade-item-level\" id=\"upgrade-firerate-level\">Lv.0</span>\n                    </div>\n                    <div class=\"upgrade-item-desc\" id=\"upgrade-firerate-desc\">Increase firing frequency</div>\n                    <button class=\"upgrade-btn\" id=\"btn-upgrade-firerate\">Upgrade (500pts)</button>\n                </div>\n                <div class=\"upgrade-item\">\n                    <div class=\"upgrade-item-header\">\n                        <span class=\"upgrade-item-name\">\ud83d\udca5 Bullet Damage</span>\n                        <span class=\"upgrade-item-level\" id=\"upgrade-damage-level\">Lv.0</span>\n                    </div>\n                    <div class=\"upgrade-item-desc\" id=\"upgrade-damage-desc\">Increase damage per bullet</div>\n                    <button class=\"upgrade-btn\" id=\"btn-upgrade-damage\">Upgrade (500pts)</button>\n                </div>\n                <div class=\"upgrade-item\">\n                    <div class=\"upgrade-item-header\">\n                        <span class=\"upgrade-item-name\">\ud83d\udce6 Ammo Capacity</span>\n                        <span class=\"upgrade-item-level\" id=\"upgrade-ammo-level\">Lv.0</span>\n                    </div>\n                    <div class=\"upgrade-item-desc\" id=\"upgrade-ammo-desc\">Increase max ammo</div>\n                    <button class=\"upgrade-btn\" id=\"btn-upgrade-ammo\">Upgrade (300pts)</button>\n                </div>\n                <div class=\"upgrade-item\">\n                    <div class=\"upgrade-item-header\">\n                        <span class=\"upgrade-item-name\">\ud83d\udca3 Explosive Rounds</span>\n                        <span class=\"upgrade-item-level\" id=\"upgrade-explosive-level\">Lv.0</span>\n                    </div>\n                    <div class=\"upgrade-item-desc\" id=\"upgrade-explosive-desc\">Bullets have a chance to explode for AoE damage</div>\n                    <button class=\"upgrade-btn\" id=\"btn-upgrade-explosive\">Upgrade (600pts)</button>\n                </div>\n                <div class=\"upgrade-item\">\n                    <div class=\"upgrade-item-header\">\n                        <span class=\"upgrade-item-name\">\ud83d\udc80 Headshot Boost</span>\n                        <span class=\"upgrade-item-level\" id=\"upgrade-giant-level\">Lv.0</span>\n                    </div>\n                    <div class=\"upgrade-item-desc\" id=\"upgrade-giant-desc\">Increase headshot damage multiplier</div>\n                    <button class=\"upgrade-btn\" id=\"btn-upgrade-giant\">Upgrade (700pts)</button>\n                </div>\n                <div class=\"upgrade-item\">\n                    <div class=\"upgrade-item-header\">\n                        <span class=\"upgrade-item-name\">\ud83d\ude80 Homing Missiles</span>\n                        <span class=\"upgrade-item-level\" id=\"upgrade-homing-level\">Lv.0</span>\n                    </div>\n                    <div class=\"upgrade-item-desc\" id=\"upgrade-homing-desc\">Auto-fire homing missiles at monsters</div>\n                    <button class=\"upgrade-btn\" id=\"btn-upgrade-homing\">Upgrade (800pts)</button>\n                </div>\n\n                <button class=\"upgrade-start-btn\" id=\"btn-upgrade-start\">Fight!</button>\n            </div>\n\n            <div class=\"game-screen hidden\" id=\"victory-screen\">\n                <div class=\"screen-title\" style=\"color: var(--color-success);\">Evacuation Success</div>\n                <div class=\"screen-subtitle\">Civilians evacuated safely!</div>\n                <div class=\"final-score\" id=\"final-score-win\">0</div>\n                <button class=\"screen-btn\" id=\"btn-replay\">Play Again</button>\n                <button class=\"screen-btn\" id=\"btn-victory-leaderboard\" style=\"background: var(--lego-green); margin-top: 8px;\">View Leaderboard</button>\n            </div>\n\n            <div id=\"edit-info\" style=\"display:none;\">Edit Mode<br><span style=\"font-size:16px;opacity:0.7;\">Adjust parameters in the sidebar</span></div>\n\n            <!-- Battle Briefing / Prep Phase Overlay -->\n            <div id=\"briefing-overlay\" class=\"game-screen hidden\" style=\"background: rgba(0, 5, 15, 0.92);\">\n                <div style=\"font-family:'Press Start 2P',monospace;font-size:12px;color:var(--lego-yellow);margin-bottom:16px;text-shadow:2px 2px 0 rgba(0,0,0,0.8);\">\u2694\ufe0f BATTLE BRIEFING \u2694\ufe0f</div>\n                <div id=\"briefing-level\" style=\"font-family:'Press Start 2P',monospace;font-size:24px;color:var(--lego-red);margin-bottom:20px;text-shadow:3px 3px 0 rgba(0,0,0,0.8);\">Level 1</div>\n                <div id=\"briefing-info\" style=\"font-family:'Press Start 2P',monospace;font-size:9px;color:var(--lego-white);line-height:2.2;text-align:center;margin-bottom:20px;max-width:360px;\"></div>\n                <div id=\"briefing-tip\" style=\"font-family:'Press Start 2P',monospace;font-size:8px;color:var(--lego-green);line-height:1.8;text-align:center;margin-bottom:24px;max-width:340px;\"></div>\n                <div id=\"briefing-countdown\" style=\"font-family:'Press Start 2P',monospace;font-size:48px;color:var(--lego-yellow);text-shadow:4px 4px 0 rgba(0,0,0,0.8);animation:pulse-urgent 0.5s infinite;\">3</div>\n                <div style=\"font-family:'Press Start 2P',monospace;font-size:10px;color:rgba(255,255,255,0.5);margin-top:12px;\">DEPLOYING IN...</div>\n            </div>\n\n            <div class=\"game-screen hidden\" id=\"leaderboard-screen\">\n                <div class=\"screen-title\" style=\"margin-bottom: 24px;\">\ud83c\udfc6 Leaderboard \ud83c\udfc6</div>\n                <div id=\"leaderboard-content\" style=\"width: 90%; max-width: 600px; max-height: 60vh; overflow-y: auto; margin-bottom: 24px;\">\n                    <div id=\"leaderboard-loading\" style=\"font-family: 'Press Start 2P', monospace; font-size: 12px; color: var(--lego-yellow); text-align: center; padding: 40px;\">Loading...</div>\n                    <div id=\"leaderboard-error\" style=\"font-family: 'Press Start 2P', monospace; font-size: 10px; color: var(--lego-red); text-align: center; padding: 40px; display: none;\">Failed to load leaderboard</div>\n                    <div id=\"leaderboard-list\" style=\"display: none;\"></div>\n                </div>\n                <button class=\"screen-btn\" id=\"btn-leaderboard-back\">Back</button>\n            </div>\n        </div>\n    </div>\n\n    <script>\n    /*\n     * Urban Hunter - Armed Helicopter City Defense Game\n     * A 3D first-person helicopter gunship game where the player defends a city from mutant monsters.\n     * Uses Three.js for 3D rendering with billboard sprites for monsters.\n     */\n    const assetCache = {};\n    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n    const audioBuffers = {};\n\n    function playSound(id, volume = 1.0, loop = false) {\n        const buffer = audioBuffers[id];\n        if (!buffer) return null;\n        const source = audioCtx.createBufferSource();\n        source.buffer = buffer;\n        const gainNode = audioCtx.createGain();\n        gainNode.gain.value = volume * (window.gameConfig.settings.sfxVolume || 1);\n        gainNode.connect(audioCtx.destination);\n        source.connect(gainNode);\n        source.loop = loop;\n        source.start(0);\n        return { source, gainNode };\n    }\n\n    async function preloadAssets(assetIds) {\n        const loadingBar = document.getElementById('loading-bar-fill');\n        const loadingScreen = document.getElementById('loading-screen');\n        let loaded = 0;\n        \n        await Promise.all(assetIds.map(async id => {\n            try {\n                const asset = lib.getAsset(id);\n                if (!asset) return;\n                \n                if (asset.type === 'audio') {\n                    const response = await fetch(asset.url);\n                    const arrayBuffer = await response.arrayBuffer();\n                    audioBuffers[id] = await audioCtx.decodeAudioData(arrayBuffer);\n                } else {\n                    await new Promise(resolve => {\n                        const img = new Image();\n                        img.crossOrigin = 'anonymous';\n                        img.onload = () => {\n                            assetCache[id] = img;\n                            resolve();\n                        };\n                        img.onerror = () => {\n                            console.warn('Failed to load asset:', id);\n                            resolve();\n                        };\n                        img.src = asset.url;\n                    });\n                }\n            } catch (e) {\n                console.error('Error loading asset:', id, e);\n            }\n            loaded++;\n            if (loadingBar) loadingBar.style.width = `${(loaded / assetIds.length) * 100}%`;\n        }));\n        \n        if (loadingScreen) {\n            loadingScreen.classList.add('hidden');\n            setTimeout(() => loadingScreen.style.display = 'none', 500);\n        }\n    }\n    async function run(mode) {\n        const config = window.gameConfig;\n        const canvas = document.getElementById('game-canvas');\n        const gameWorld = document.getElementById('game-world');\n        const assetIds = [\n            'mutant_monster', 'building_texture_1', 'building_texture_2', 'street_texture',\n            'sniper_rifle_sound', 'empty_gun_click', 'reload_sound', 'monster_hit_sound',\n            'monster_death_sound', 'monster_roar', 'player_damage_sound', 'wave_complete_sound',\n            'game_over_sound', 'victory_sound', 'background_music', 'menu_music', 'missile_launch_sound'\n        ];\n        \n        if (mode === 'play') {\n            await preloadAssets(assetIds);\n        }\n\n        const buildingDamages = [];\n\n        // --- Adaptive Quality System ---\n        const perfMonitor = {\n            frames: 0,\n            lastCheck: performance.now(),\n            fps: 60,\n            quality: 'high', // high, medium, low\n            particleMultiplier: 1.0,\n            maxTracers: 30,\n            maxFireTrails: 60,\n            maxDebris: 40,\n            checkInterval: 2000, // check every 2s\n            update() {\n                this.frames++;\n                const now = performance.now();\n                if (now - this.lastCheck >= this.checkInterval) {\n                    this.fps = Math.round(this.frames * 1000 / (now - this.lastCheck));\n                    this.frames = 0;\n                    this.lastCheck = now;\n                    if (this.fps < 24 && this.quality !== 'low') {\n                        this.quality = 'low';\n                        this.particleMultiplier = 0.3;\n                        this.maxTracers = 10;\n                        this.maxFireTrails = 15;\n                        this.maxDebris = 15;\n                        renderer.setPixelRatio(1);\n                    } else if (this.fps < 35 && this.quality === 'high') {\n                        this.quality = 'medium';\n                        this.particleMultiplier = 0.6;\n                        this.maxTracers = 20;\n                        this.maxFireTrails = 35;\n                        this.maxDebris = 25;\n                        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));\n                    } else if (this.fps > 50 && this.quality !== 'high') {\n                        this.quality = 'high';\n                        this.particleMultiplier = 1.0;\n                        this.maxTracers = 30;\n                        this.maxFireTrails = 60;\n                        this.maxDebris = 40;\n                        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n                    }\n                }\n            }\n        };\n\n        // --- Object Pool System ---\n        class ObjectPool {\n            constructor(createFn, resetFn, initialSize = 20) {\n                this.createFn = createFn;\n                this.resetFn = resetFn;\n                this.pool = [];\n                this.active = [];\n                for (let i = 0; i < initialSize; i++) {\n                    const obj = this.createFn();\n                    obj.userData._pooled = true;\n                    obj.visible = false;\n                    this.pool.push(obj);\n                }\n            }\n            get() {\n                let obj;\n                if (this.pool.length > 0) {\n                    obj = this.pool.pop();\n                } else {\n                    obj = this.createFn();\n                    obj.userData._pooled = true;\n                }\n                obj.visible = true;\n                this.active.push(obj);\n                return obj;\n            }\n            release(obj) {\n                const idx = this.active.indexOf(obj);\n                if (idx >= 0) this.active.splice(idx, 1);\n                obj.visible = false;\n                if (this.resetFn) this.resetFn(obj);\n                this.pool.push(obj);\n            }\n            releaseAll() {\n                while (this.active.length > 0) {\n                    this.release(this.active[0]);\n                }\n            }\n        }\n\n        // --- Three.js Setup ---\n        const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });\n        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n        renderer.shadowMap.enabled = false;\n\n        const scene = new THREE.Scene();\n        scene.background = new THREE.Color(0x0a0a1a);\n        scene.fog = new THREE.FogExp2(0x0a0a1a, config.settings.fogDensity);\n\n        const camera = new THREE.PerspectiveCamera(75, gameWorld.clientWidth / gameWorld.clientHeight, 0.1, 1000);\n        camera.position.set(4, 57, 0);\n\n        function resize() {\n            const w = gameWorld.clientWidth;\n            const h = gameWorld.clientHeight;\n            renderer.setSize(w, h);\n            if (camera) {\n                camera.aspect = w / h;\n                camera.updateProjectionMatrix();\n            }\n        }\n        resize();\n        window.addEventListener('resize', resize);\n\n        // Lighting\n        const ambientLight = new THREE.AmbientLight(0x1a1a3a, 0.3);\n        scene.add(ambientLight);\n\n        const moonLight = new THREE.DirectionalLight(0x4466aa, 0.4);\n        moonLight.position.set(50, 100, 30);\n        scene.add(moonLight);\n\n        const fillLight = new THREE.DirectionalLight(0x223344, 0.2);\n        fillLight.position.set(-30, 40, -50);\n        scene.add(fillLight);\n\n        // Helicopter searchlight\n        const searchLight = new THREE.SpotLight(0xffffee, 2.5, 200, Math.PI / 6, 0.5, 1);\n        searchLight.position.copy(camera.position);\n        scene.add(searchLight);\n        scene.add(searchLight.target);\n\n        // Visible searchlight cone (volumetric effect)\n        const coneGeo = new THREE.ConeGeometry(12, 80, 16, 1, true);\n        const coneMat = new THREE.MeshBasicMaterial({ color: 0xffffdd, transparent: true, opacity: 0.04, side: THREE.DoubleSide });\n        const searchCone = new THREE.Mesh(coneGeo, coneMat);\n        searchCone.visible = false;\n        scene.add(searchCone);\n\n        const cityLightColor = new THREE.Color(config.settings.cityLightColor);\n        const pointLight1 = new THREE.PointLight(cityLightColor, 0.5, 100);\n        pointLight1.position.set(30, 20, -50);\n        scene.add(pointLight1);\n\n        // --- Build City ---\n        // Ground\n        const groundGeo = new THREE.PlaneGeometry(400, 400);\n        const groundMat = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.9 });\n        if (assetCache['street_texture']) {\n            const streetTex = new THREE.Texture(assetCache['street_texture']);\n            streetTex.needsUpdate = true;\n            streetTex.wrapS = streetTex.wrapT = THREE.RepeatWrapping;\n            streetTex.repeat.set(8, 8);\n            groundMat.map = streetTex;\n        }\n        const ground = new THREE.Mesh(groundGeo, groundMat);\n        ground.rotation.x = -Math.PI / 2;\n        ground.position.y = 0;\n        scene.add(ground);\n\n        // Buildings\n        const buildingTextures = [];\n        if (assetCache['building_texture_1']) {\n            const t = new THREE.Texture(assetCache['building_texture_1']);\n            t.needsUpdate = true;\n            t.wrapS = t.wrapT = THREE.RepeatWrapping;\n            buildingTextures.push(t);\n        }\n        if (assetCache['building_texture_2']) {\n            const t = new THREE.Texture(assetCache['building_texture_2']);\n            t.needsUpdate = true;\n            t.wrapS = t.wrapT = THREE.RepeatWrapping;\n            buildingTextures.push(t);\n        }\n\n        const buildings = [];\n        function createBuilding(x, z, w, d, h) {\n            const geo = new THREE.BoxGeometry(w, h, d);\n            const matOptions = { color: 0x8899aa, roughness: 0.7, metalness: 0.2 };\n            if (buildingTextures.length > 0) {\n                const tex = buildingTextures[Math.floor(Math.random() * buildingTextures.length)].clone();\n                tex.needsUpdate = true;\n                tex.repeat.set(w / 15, h / 15);\n                matOptions.map = tex;\n            }\n            const mat = new THREE.MeshStandardMaterial(matOptions);\n            const mesh = new THREE.Mesh(geo, mat);\n            mesh.position.set(x, h / 2, z);\n            scene.add(mesh);\n            buildings.push(mesh);\n\n            // Rooftop light\n            const lightGeo = new THREE.BoxGeometry(2, 1, 2);\n            const lightMat = new THREE.MeshBasicMaterial({ color: Math.random() > 0.5 ? 0xcccccc : 0x999999 });\n            const lightMesh = new THREE.Mesh(lightGeo, lightMat);\n            lightMesh.position.set(x, h + 0.5, z);\n            scene.add(lightMesh);\n            \n            mesh.userData.roofLight = lightMesh;\n        }\n\n        // Place buildings around the streets\n        const buildingPositions = [\n            [-60, -60, 20, 20, 35], [-60, -30, 18, 15, 25], [-60, 10, 22, 18, 40],\n            [-60, 50, 16, 20, 22], [-60, 80, 20, 16, 32],\n            [60, -60, 18, 22, 28], [60, -20, 20, 18, 38], [60, 20, 16, 20, 30],\n            [60, 60, 22, 16, 42], [60, 90, 18, 18, 20],\n            [-30, -80, 16, 18, 25], [0, -80, 20, 16, 32], [30, -80, 18, 20, 28],\n            [-30, 80, 20, 16, 22], [30, 80, 16, 18, 35],\n            [100, -40, 25, 25, 45], [-100, -40, 22, 22, 40],\n            [100, 40, 20, 20, 30], [-100, 40, 24, 20, 35],\n            [0, -120, 30, 30, 50], [80, -100, 20, 20, 38], [-80, -100, 22, 22, 32],\n        ];\n        buildingPositions.forEach(b => createBuilding(...b));\n\n        // --- Navigation Grid for A* Pathfinding ---\n        const NAV_CELL_SIZE = 5;\n        const NAV_GRID_SIZE = 80; // 400 / 5 = 80 cells\n        const NAV_OFFSET = 200; // world goes from -200 to 200\n        const navGrid = [];\n        for (let i = 0; i < NAV_GRID_SIZE; i++) {\n            navGrid[i] = [];\n            for (let j = 0; j < NAV_GRID_SIZE; j++) {\n                navGrid[i][j] = 0; // 0 = walkable, 1 = blocked\n            }\n        }\n\n        function buildNavGrid() {\n            for (let i = 0; i < NAV_GRID_SIZE; i++) {\n                for (let j = 0; j < NAV_GRID_SIZE; j++) {\n                    navGrid[i][j] = 0;\n                }\n            }\n            for (const bld of buildings) {\n                const bPos = bld.position;\n                const bGeo = bld.geometry.parameters;\n                const halfW = bGeo.width / 2 + 6; // padding for monster width\n                const halfD = bGeo.depth / 2 + 6;\n                const minI = Math.floor((bPos.x - halfW + NAV_OFFSET) / NAV_CELL_SIZE);\n                const maxI = Math.ceil((bPos.x + halfW + NAV_OFFSET) / NAV_CELL_SIZE);\n                const minJ = Math.floor((bPos.z - halfD + NAV_OFFSET) / NAV_CELL_SIZE);\n                const maxJ = Math.ceil((bPos.z + halfD + NAV_OFFSET) / NAV_CELL_SIZE);\n                for (let i = Math.max(0, minI); i <= Math.min(NAV_GRID_SIZE - 1, maxI); i++) {\n                    for (let j = Math.max(0, minJ); j <= Math.min(NAV_GRID_SIZE - 1, maxJ); j++) {\n                        navGrid[i][j] = 1;\n                    }\n                }\n            }\n        }\n\n        function worldToGrid(x, z) {\n            return [\n                Math.max(0, Math.min(NAV_GRID_SIZE - 1, Math.floor((x + NAV_OFFSET) / NAV_CELL_SIZE))),\n                Math.max(0, Math.min(NAV_GRID_SIZE - 1, Math.floor((z + NAV_OFFSET) / NAV_CELL_SIZE)))\n            ];\n        }\n\n        function gridToWorld(gi, gj) {\n            return [\n                gi * NAV_CELL_SIZE - NAV_OFFSET + NAV_CELL_SIZE / 2,\n                gj * NAV_CELL_SIZE - NAV_OFFSET + NAV_CELL_SIZE / 2\n            ];\n        }\n\n        function findPath(startX, startZ, endX, endZ) {\n            const [si, sj] = worldToGrid(startX, startZ);\n            const [ei, ej] = worldToGrid(endX, endZ);\n\n            // If start is blocked, skip pathfinding\n            if (navGrid[si][sj] === 1) return null;\n\n            // If end is blocked, find nearest walkable cell\n            let targetI = ei, targetJ = ej;\n            if (navGrid[ei][ej] === 1) {\n                let found = false;\n                for (let r = 1; r < 8 && !found; r++) {\n                    for (let di = -r; di <= r && !found; di++) {\n                        for (let dj = -r; dj <= r && !found; dj++) {\n                            const ni = ei + di, nj = ej + dj;\n                            if (ni >= 0 && ni < NAV_GRID_SIZE && nj >= 0 && nj < NAV_GRID_SIZE && navGrid[ni][nj] === 0) {\n                                targetI = ni; targetJ = nj; found = true;\n                            }\n                        }\n                    }\n                }\n                if (!found) return null;\n            }\n\n            // Quick check: if direct line is clear AND short distance, no need for A*\n            if (isDirectPathClear(si, sj, targetI, targetJ) && Math.abs(si - targetI) + Math.abs(sj - targetJ) < 12) return null;\n\n            const openSet = [];\n            const closedSet = new Set();\n            const cameFrom = {};\n            const gScore = {};\n            const fScore = {};\n\n            const startKey = si * NAV_GRID_SIZE + sj;\n            const endKey = targetI * NAV_GRID_SIZE + targetJ;\n\n            gScore[startKey] = 0;\n            fScore[startKey] = heuristic(si, sj, targetI, targetJ);\n            openSet.push({ i: si, j: sj, f: fScore[startKey] });\n\n            const dirs = [[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[-1,1],[1,-1],[1,1]];\n            let iterations = 0;\n            const MAX_ITERATIONS = 1200;\n\n            while (openSet.length > 0 && iterations < MAX_ITERATIONS) {\n                iterations++;\n                // Find lowest fScore\n                let lowestIdx = 0;\n                for (let oi = 1; oi < openSet.length; oi++) {\n                    if (openSet[oi].f < openSet[lowestIdx].f) lowestIdx = oi;\n                }\n                const current = openSet.splice(lowestIdx, 1)[0];\n                const currentKey = current.i * NAV_GRID_SIZE + current.j;\n\n                if (current.i === targetI && current.j === targetJ) {\n                    // Reconstruct path\n                    const path = [];\n                    let key = endKey;\n                    while (key !== undefined && key !== startKey) {\n                        const pi = Math.floor(key / NAV_GRID_SIZE);\n                        const pj = key % NAV_GRID_SIZE;\n                        const [wx, wz] = gridToWorld(pi, pj);\n                        path.unshift({ x: wx, z: wz });\n                        key = cameFrom[key];\n                    }\n                    // Simplify path - remove collinear waypoints\n                    if (path.length > 2) {\n                        const simplified = [path[0]];\n                        for (let pi = 1; pi < path.length - 1; pi++) {\n                            const prev = simplified[simplified.length - 1];\n                            const next = path[pi + 1];\n                            const dx1 = path[pi].x - prev.x;\n                            const dz1 = path[pi].z - prev.z;\n                            const dx2 = next.x - path[pi].x;\n                            const dz2 = next.z - path[pi].z;\n                            if (Math.abs(dx1 * dz2 - dz1 * dx2) > 0.01) {\n                                simplified.push(path[pi]);\n                            }\n                        }\n                        simplified.push(path[path.length - 1]);\n                        return simplified;\n                    }\n                    return path;\n                }\n\n                closedSet.add(currentKey);\n\n                for (const [di, dj] of dirs) {\n                    const ni = current.i + di;\n                    const nj = current.j + dj;\n                    if (ni < 0 || ni >= NAV_GRID_SIZE || nj < 0 || nj >= NAV_GRID_SIZE) continue;\n                    if (navGrid[ni][nj] === 1) continue;\n                    // Prevent diagonal cutting through blocked corners\n                    if (di !== 0 && dj !== 0) {\n                        if (navGrid[current.i + di][current.j] === 1 || navGrid[current.i][current.j + dj] === 1) continue;\n                    }\n\n                    const neighborKey = ni * NAV_GRID_SIZE + nj;\n                    if (closedSet.has(neighborKey)) continue;\n\n                    const moveCost = (di !== 0 && dj !== 0) ? 1.414 : 1;\n                    const tentativeG = gScore[currentKey] + moveCost;\n\n                    if (tentativeG < (gScore[neighborKey] || Infinity)) {\n                        cameFrom[neighborKey] = currentKey;\n                        gScore[neighborKey] = tentativeG;\n                        fScore[neighborKey] = tentativeG + heuristic(ni, nj, targetI, targetJ);\n\n                        if (!openSet.find(n => n.i === ni && n.j === nj)) {\n                            openSet.push({ i: ni, j: nj, f: fScore[neighborKey] });\n                        }\n                    }\n                }\n            }\n\n            return null; // No path found\n        }\n\n        function heuristic(i1, j1, i2, j2) {\n            // Octile distance\n            const dx = Math.abs(i1 - i2);\n            const dz = Math.abs(j1 - j2);\n            return Math.max(dx, dz) + 0.414 * Math.min(dx, dz);\n        }\n\n        // Check if direct line between two grid cells is clear (Bresenham)\n        function isDirectPathClear(i1, j1, i2, j2) {\n            let x = i1, y = j1;\n            const dx = Math.abs(i2 - i1), dy = Math.abs(j2 - j1);\n            const sx = i1 < i2 ? 1 : -1, sy = j1 < j2 ? 1 : -1;\n            let err = dx - dy;\n            while (true) {\n                if (navGrid[x] && navGrid[x][y] === 1) return false;\n                if (x === i2 && y === j2) break;\n                const e2 = 2 * err;\n                if (e2 > -dy) { err -= dy; x += sx; }\n                if (e2 < dx) { err += dx; y += sy; }\n            }\n            return true;\n        }\n\n        buildNavGrid();\n\n        // --- Evacuation Zone ---\n        const EVAC_ZONE_RADIUS = 25;\n        const EVAC_ZONE_POS = new THREE.Vector3(0, 0, 0);\n\n        // Helipad circle on ground\n        const evacGroup = new THREE.Group();\n        evacGroup.position.set(0, 0.1, 0);\n        scene.add(evacGroup);\n\n        // Main landing pad circle\n        const padGeo = new THREE.CircleGeometry(EVAC_ZONE_RADIUS, 48);\n        const padMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.9 });\n        const pad = new THREE.Mesh(padGeo, padMat);\n        pad.rotation.x = -Math.PI / 2;\n        evacGroup.add(pad);\n\n        // Yellow boundary ring\n        const ringGeo = new THREE.RingGeometry(EVAC_ZONE_RADIUS - 1.5, EVAC_ZONE_RADIUS, 48);\n        const ringMat = new THREE.MeshBasicMaterial({ color: 0xffcc00, side: THREE.DoubleSide });\n        const evacRing = new THREE.Mesh(ringGeo, ringMat);\n        evacRing.rotation.x = -Math.PI / 2;\n        evacRing.position.y = 0.02;\n        evacGroup.add(evacRing);\n\n        // Inner circle marker\n        const innerRingGeo = new THREE.RingGeometry(8, 9, 32);\n        const innerRingMat = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide });\n        const innerRing = new THREE.Mesh(innerRingGeo, innerRingMat);\n        innerRing.rotation.x = -Math.PI / 2;\n        innerRing.position.y = 0.03;\n        evacGroup.add(innerRing);\n\n        // \"H\" marking for helipad\n        const hBarGeo1 = new THREE.BoxGeometry(1, 0.05, 8);\n        const hBarMat = new THREE.MeshBasicMaterial({ color: 0xffffff });\n        const hBar1 = new THREE.Mesh(hBarGeo1, hBarMat);\n        hBar1.position.set(-3, 0.04, 0);\n        evacGroup.add(hBar1);\n        const hBar2 = new THREE.Mesh(hBarGeo1, hBarMat);\n        hBar2.position.set(3, 0.04, 0);\n        evacGroup.add(hBar2);\n        const hBarCross = new THREE.Mesh(new THREE.BoxGeometry(7, 0.05, 1.2), hBarMat);\n        hBarCross.position.set(0, 0.04, 0);\n        evacGroup.add(hBarCross);\n\n        // Flashing beacon lights at corners\n        const beaconPositions = [\n            [EVAC_ZONE_RADIUS - 2, 0, 0], [-EVAC_ZONE_RADIUS + 2, 0, 0],\n            [0, 0, EVAC_ZONE_RADIUS - 2], [0, 0, -EVAC_ZONE_RADIUS + 2],\n            [16, 0, 16], [-16, 0, 16], [16, 0, -16], [-16, 0, -16]\n        ];\n        const beaconMeshes = [];\n        beaconPositions.forEach(pos => {\n            const beaconGeo = new THREE.CylinderGeometry(0.3, 0.5, 2, 8);\n            const beaconMat = new THREE.MeshStandardMaterial({ color: 0x444444 });\n            const beacon = new THREE.Mesh(beaconGeo, beaconMat);\n            beacon.position.set(pos[0], 1, pos[2]);\n            evacGroup.add(beacon);\n\n            const lightGeo = new THREE.SphereGeometry(0.5, 8, 8);\n            const lightMat = new THREE.MeshBasicMaterial({ color: 0xffaa00 });\n            const light = new THREE.Mesh(lightGeo, lightMat);\n            light.position.set(pos[0], 2.2, pos[2]);\n            evacGroup.add(light);\n            beaconMeshes.push(light);\n        });\n\n        // Barrier walls (sandbags)\n        for (let i = 0; i < 16; i++) {\n            const angle = (i / 16) * Math.PI * 2;\n            const bx = Math.cos(angle) * (EVAC_ZONE_RADIUS - 3);\n            const bz = Math.sin(angle) * (EVAC_ZONE_RADIUS - 3);\n            const barrierGeo = new THREE.BoxGeometry(4, 1.5, 1.5);\n            const barrierMat = new THREE.MeshStandardMaterial({ color: 0x8b7355, roughness: 0.9 });\n            const barrier = new THREE.Mesh(barrierGeo, barrierMat);\n            barrier.position.set(bx, 0.75, bz);\n            barrier.rotation.y = angle + Math.PI / 2;\n            evacGroup.add(barrier);\n        }\n\n        // Armed Helicopter\n        const heliGroup = new THREE.Group();\n        heliGroup.position.set(0, 90, 0);\n        scene.add(heliGroup);\n\n        // Helicopter body\n        const heliBodyGeo = new THREE.SphereGeometry(3, 16, 16);\n        const heliBodyMat = new THREE.MeshStandardMaterial({ color: 0x2d4a2d, roughness: 0.4, metalness: 0.6 });\n        const heliBody = new THREE.Mesh(heliBodyGeo, heliBodyMat);\n        heliBody.scale.set(1, 2, 1);\n        heliBody.rotation.z = Math.PI / 2;\n        heliGroup.add(heliBody);\n\n        // Cockpit glass\n        const cockpitGeo = new THREE.SphereGeometry(2.5, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2);\n        const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x88ccff, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.6 });\n        const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);\n        cockpit.position.set(5, 1, 0);\n        cockpit.rotation.z = -Math.PI / 4;\n        heliGroup.add(cockpit);\n\n        // Tail boom\n        const tailGeo = new THREE.CylinderGeometry(0.8, 1.5, 10, 8);\n        const tailMat = new THREE.MeshStandardMaterial({ color: 0x2d4a2d, roughness: 0.5, metalness: 0.5 });\n        const tail = new THREE.Mesh(tailGeo, tailMat);\n        tail.rotation.z = Math.PI / 2;\n        tail.position.set(-9, 1, 0);\n        heliGroup.add(tail);\n\n        // Tail fin\n        const finGeo = new THREE.BoxGeometry(2, 3, 0.3);\n        const finMat = new THREE.MeshStandardMaterial({ color: 0x2d4a2d, roughness: 0.5 });\n        const fin = new THREE.Mesh(finGeo, finMat);\n        fin.position.set(-13, 2, 0);\n        heliGroup.add(fin);\n\n        // Main rotor hub\n        const rotorHubGeo = new THREE.CylinderGeometry(0.5, 0.5, 1, 8);\n        const rotorHubMat = new THREE.MeshStandardMaterial({ color: 0x444444 });\n        const rotorHub = new THREE.Mesh(rotorHubGeo, rotorHubMat);\n        rotorHub.position.set(0, 3.5, 0);\n        heliGroup.add(rotorHub);\n\n        // Main rotor blades\n        const rotorGroup = new THREE.Group();\n        rotorGroup.position.set(0, 4, 0);\n        for (let i = 0; i < 4; i++) {\n            const bladeGeo = new THREE.BoxGeometry(14, 0.15, 0.8);\n            const bladeMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.8 });\n            const blade = new THREE.Mesh(bladeGeo, bladeMat);\n            blade.rotation.y = (Math.PI / 2) * i;\n            rotorGroup.add(blade);\n        }\n        heliGroup.add(rotorGroup);\n\n        // Tail rotor\n        const tailRotorGroup = new THREE.Group();\n        tailRotorGroup.position.set(-13, 2, 1);\n        for (let i = 0; i < 2; i++) {\n            const tBladeGeo = new THREE.BoxGeometry(0.15, 3, 0.4);\n            const tBladeMat = new THREE.MeshStandardMaterial({ color: 0x333333 });\n            const tBlade = new THREE.Mesh(tBladeGeo, tBladeMat);\n            tBlade.rotation.z = (Math.PI / 2) * i;\n            tailRotorGroup.add(tBlade);\n        }\n        heliGroup.add(tailRotorGroup);\n\n        // Landing skids\n        const skidMat = new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.7 });\n        for (let side = -1; side <= 1; side += 2) {\n            const skidGeo = new THREE.CylinderGeometry(0.2, 0.2, 10, 8);\n            const skid = new THREE.Mesh(skidGeo, skidMat);\n            skid.rotation.z = Math.PI / 2;\n            skid.position.set(0, -3.5, side * 2.5);\n            heliGroup.add(skid);\n\n            // Skid struts\n            for (let s = -1; s <= 1; s += 2) {\n                const strutGeo = new THREE.CylinderGeometry(0.15, 0.15, 2.5, 6);\n                const strut = new THREE.Mesh(strutGeo, skidMat);\n                strut.position.set(s * 2.5, -2.2, side * 2.5);\n                heliGroup.add(strut);\n            }\n        }\n\n        // Machine gun (mounted under nose)\n        const gunGeo = new THREE.CylinderGeometry(0.3, 0.4, 5, 8);\n        const gunMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.9, roughness: 0.2 });\n        const gun = new THREE.Mesh(gunGeo, gunMat);\n        gun.rotation.z = Math.PI / 2;\n        gun.position.set(6, -2, 0);\n        heliGroup.add(gun);\n\n        // Rocket pods on sides\n        for (let side = -1; side <= 1; side += 2) {\n            const podGeo = new THREE.CylinderGeometry(0.8, 0.8, 4, 8);\n            const podMat = new THREE.MeshStandardMaterial({ color: 0x3a3a3a, metalness: 0.7 });\n            const pod = new THREE.Mesh(podGeo, podMat);\n            pod.rotation.z = Math.PI / 2;\n            pod.position.set(1, -1.5, side * 4.5);\n            heliGroup.add(pod);\n        }\n\n        // Hide helicopter body from cockpit in play mode (camera is inside)\n        if (mode === 'play') {\n            heliBody.visible = false;\n            cockpit.visible = false;\n            gun.visible = false;\n        }\n        for (let i = -5; i <= 5; i++) {\n            if (i === 0) continue;\n            const poleGeo = new THREE.CylinderGeometry(0.2, 0.2, 8);\n            const poleMat = new THREE.MeshStandardMaterial({ color: 0x666666 });\n            const pole = new THREE.Mesh(poleGeo, poleMat);\n            pole.position.set(i * 20, 4, -40 + Math.random() * 80);\n            scene.add(pole);\n\n            const bulbGeo = new THREE.SphereGeometry(0.5);\n            const bulbMat = new THREE.MeshBasicMaterial({ color: 0xffaa44 });\n            const bulb = new THREE.Mesh(bulbGeo, bulbMat);\n            bulb.position.set(pole.position.x, 8.5, pole.position.z);\n            scene.add(bulb);\n        }\n\n        // --- Bullet Tracer System ---\n        const tracers = [];\n        const shellCasings = [];\n        \n        const tracerMat = new THREE.MeshBasicMaterial({ color: 0xffee44, transparent: true, opacity: 1 });\n        const tracerGeo = new THREE.CylinderGeometry(0.09, 0.18, 18, 4);\n        tracerGeo.rotateX(Math.PI / 2);\n\n        // Glow sheath around tracer\n        const tracerGlowMat = new THREE.MeshBasicMaterial({ color: 0xff8800, transparent: true, opacity: 0.35 });\n        const tracerGlowGeo = new THREE.CylinderGeometry(0.28, 0.45, 22, 6);\n        tracerGlowGeo.rotateX(Math.PI / 2);\n\n        // Fire trail particles\n        const fireTrails = [];\n        const fireTrailGeo = new THREE.SphereGeometry(0.18, 4, 4);\n\n        // Object pools for frequently created/destroyed objects\n        const tracerPool = new ObjectPool(\n            () => { const m = new THREE.Mesh(tracerGeo, tracerMat.clone()); scene.add(m); return m; },\n            (m) => { m.material.opacity = 1; },\n            15\n        );\n        const glowPool = new ObjectPool(\n            () => { const m = new THREE.Mesh(tracerGlowGeo, tracerGlowMat.clone()); scene.add(m); return m; },\n            (m) => { m.material.opacity = 0.35; },\n            15\n        );\n        const fireTrailPool = new ObjectPool(\n            () => { const m = new THREE.Mesh(fireTrailGeo, new THREE.MeshBasicMaterial({ color: 0xff8800, transparent: true, opacity: 1 })); scene.add(m); return m; },\n            (m) => { m.material.opacity = 1; m.scale.setScalar(1); },\n            40\n        );\n        const debrisPool = new ObjectPool(\n            () => { const g = new THREE.BoxGeometry(0.5, 0.5, 0.5); const m = new THREE.Mesh(g, new THREE.MeshStandardMaterial({ color: 0x888888 })); scene.add(m); return m; },\n            (m) => { m.scale.setScalar(1); m.rotation.set(0,0,0); },\n            30\n        );\n\n        // Bullet craters\n        const craters = [];\n        const craterGeo = new THREE.CircleGeometry(0.8, 8);\n        craterGeo.rotateX(-Math.PI / 2);\n        const craterMat = new THREE.MeshBasicMaterial({ color: 0x111111, transparent: true, opacity: 0.85 });\n\n        function spawnBuildingDamage(position, normal, building) {\n            // Bullet hole decal\n            const holeGeo = new THREE.CircleGeometry(0.2, 8);\n            const holeMat = new THREE.MeshBasicMaterial({ color: 0x111111, transparent: true, opacity: 0.9 });\n            const hole = new THREE.Mesh(holeGeo, holeMat);\n            hole.position.copy(position).addScaledVector(normal, 0.05);\n            hole.lookAt(position.clone().add(normal));\n            scene.add(hole);\n\n            // Crack decal\n            const crackGeo = new THREE.CircleGeometry(0.5, 8);\n            const crackMat = new THREE.MeshBasicMaterial({ color: 0x222222, transparent: true, opacity: 0.6 });\n            const crack = new THREE.Mesh(crackGeo, crackMat);\n            crack.position.copy(position).addScaledVector(normal, 0.04);\n            crack.lookAt(position.clone().add(normal));\n            scene.add(crack);\n\n            // Debris particles\n            for (let i = 0; i < 6; i++) {\n                const debrisGeo = new THREE.BoxGeometry(0.2 + Math.random() * 0.3, 0.2 + Math.random() * 0.3, 0.2 + Math.random() * 0.2);\n                const debrisMat = new THREE.MeshStandardMaterial({ color: new THREE.Color(0.4 + Math.random() * 0.3, 0.4 + Math.random() * 0.2, 0.35 + Math.random() * 0.2) });\n                const debris = new THREE.Mesh(debrisGeo, debrisMat);\n                debris.position.copy(position);\n                const vel = new THREE.Vector3(\n                    normal.x * 3 + (Math.random() - 0.5) * 5,\n                    normal.y * 3 + Math.random() * 4,\n                    normal.z * 3 + (Math.random() - 0.5) * 5\n                );\n                scene.add(debris);\n                buildingDamages.push({ type: 'debris', mesh: debris, vel, life: 1.2 + Math.random() * 0.5 });\n            }\n\n            // Dust puff\n            for (let i = 0; i < 4; i++) {\n                const dustGeo = new THREE.SphereGeometry(0.3 + Math.random() * 0.4, 4, 4);\n                const dustMat = new THREE.MeshBasicMaterial({ color: 0xaaaaaa, transparent: true, opacity: 0.6 });\n                const dust = new THREE.Mesh(dustGeo, dustMat);\n                dust.position.copy(position);\n                const vel = new THREE.Vector3(\n                    normal.x * 2 + (Math.random() - 0.5) * 3,\n                    Math.random() * 2,\n                    normal.z * 2 + (Math.random() - 0.5) * 3\n                );\n                scene.add(dust);\n                buildingDamages.push({ type: 'dust', mesh: dust, vel, life: 0.8 + Math.random() * 0.4 });\n            }\n\n            // Store decals for cleanup\n            buildingDamages.push({ type: 'decal', mesh: hole, life: 20 });\n            buildingDamages.push({ type: 'decal', mesh: crack, life: 20 });\n            if (buildingDamages.filter(d => d.type === 'decal').length > 60) {\n                const oldDecal = buildingDamages.find(d => d.type === 'decal');\n                if (oldDecal) {\n                    scene.remove(oldDecal.mesh);\n                    buildingDamages.splice(buildingDamages.indexOf(oldDecal), 1);\n                }\n            }\n        }\n\n        function updateBuildingDamages(dt) {\n            for (let i = buildingDamages.length - 1; i >= 0; i--) {\n                const d = buildingDamages[i];\n                d.life -= dt;\n                if (d.type === 'debris') {\n                    d.vel.y -= 12 * dt;\n                    d.mesh.position.addScaledVector(d.vel, dt);\n                    d.mesh.rotation.x += dt * 8;\n                    d.mesh.rotation.z += dt * 6;\n                    if (d.mesh.position.y < 0) d.life = 0;\n                } else if (d.type === 'dust') {\n                    d.mesh.position.addScaledVector(d.vel, dt);\n                    d.mesh.material.opacity *= 0.94;\n                    d.mesh.scale.multiplyScalar(1.03);\n                }\n                if (d.life <= 0) {\n                    scene.remove(d.mesh);\n                    buildingDamages.splice(i, 1);\n                }\n            }\n        }\n\n        function spawnCrater(position, normal) {\n            const crater = new THREE.Mesh(craterGeo, craterMat.clone());\n            crater.position.copy(position);\n            crater.position.y += 0.05;\n            // Scorch ring\n            const ringGeo = new THREE.RingGeometry(0.6, 1.2, 10);\n            ringGeo.rotateX(-Math.PI / 2);\n            const ringMat = new THREE.MeshBasicMaterial({ color: 0x332200, transparent: true, opacity: 0.5, side: THREE.DoubleSide });\n            const ring = new THREE.Mesh(ringGeo, ringMat);\n            ring.position.copy(position);\n            ring.position.y += 0.03;\n            scene.add(crater);\n            scene.add(ring);\n            craters.push({ crater, ring, life: 15 });\n            // Limit craters\n            if (craters.length > 40) {\n                const old = craters.shift();\n                scene.remove(old.crater);\n                scene.remove(old.ring);\n            }\n        }\n        \n        const shellGeo = new THREE.CylinderGeometry(0.03, 0.04, 0.2, 6);\n        const shellMat = new THREE.MeshStandardMaterial({ color: 0xccaa00, metalness: 0.9, roughness: 0.2 });\n\n        function spawnTracer(origin, direction) {\n            // Respect pool limits\n            if (tracers.length >= perfMonitor.maxTracers) return;\n            const mesh = tracerPool.get();\n            mesh.position.copy(origin);\n            mesh.lookAt(origin.clone().add(direction));\n\n            const glowMesh = glowPool.get();\n            glowMesh.position.copy(origin);\n            glowMesh.lookAt(origin.clone().add(direction));\n\n            tracers.push({ mesh, glowMesh, dir: direction.clone().multiplyScalar(340), life: 0.55, trail: true, pooled: true });\n        }\n\n        function spawnShellCasing(origin) {\n            const mesh = new THREE.Mesh(shellGeo, shellMat);\n            mesh.position.copy(origin);\n            const ejectDir = new THREE.Vector3(\n                (Math.random() - 0.3) * 4,\n                Math.random() * 3 + 2,\n                (Math.random() - 0.5) * 4\n            );\n            ejectDir.applyAxisAngle(new THREE.Vector3(0, 1, 0), yaw);\n            scene.add(mesh);\n            shellCasings.push({ mesh, vel: ejectDir, life: 1.5 });\n        }\n\n        // --- Muzzle Fire Particles ---\n        const muzzleParticles = [];\n        const muzzleParticleGeo = new THREE.SphereGeometry(0.15, 4, 4);\n\n        function spawnMuzzleFire(origin, direction) {\n            const count = Math.max(2, Math.round(5 * perfMonitor.particleMultiplier));\n            for (let i = 0; i < count; i++) {\n                const mat = new THREE.MeshBasicMaterial({ \n                    color: new THREE.Color().setHSL(0.1 * Math.random(), 1, 0.5 + Math.random() * 0.5),\n                    transparent: true, opacity: 1 \n                });\n                const mesh = new THREE.Mesh(muzzleParticleGeo, mat);\n                mesh.position.copy(origin);\n                const spread = new THREE.Vector3(\n                    direction.x + (Math.random() - 0.5) * 0.5,\n                    direction.y + (Math.random() - 0.5) * 0.5,\n                    direction.z + (Math.random() - 0.5) * 0.5\n                ).multiplyScalar(15 + Math.random() * 20);\n                mesh.scale.setScalar(0.5 + Math.random() * 1.5);\n                scene.add(mesh);\n                muzzleParticles.push({ mesh, vel: spread, life: 0.15 + Math.random() * 0.15 });\n            }\n        }\n\n        function updateBulletEffects(dt) {\n            // Tracers with fire trails\n            for (let i = tracers.length - 1; i >= 0; i--) {\n                const t = tracers[i];\n                t.life -= dt;\n                t.mesh.position.addScaledVector(t.dir, dt);\n                if (t.glowMesh) t.glowMesh.position.copy(t.mesh.position);\n\n                // Fade tracer near end of life\n                const lifeRatio = t.life / 0.55;\n                t.mesh.material.opacity = Math.min(1, lifeRatio * 2);\n                if (t.glowMesh) t.glowMesh.material.opacity = 0.35 * Math.min(1, lifeRatio * 2);\n\n                // Spawn fire trail particles - giant bullets have much bigger trails\n                if (t.trail && fireTrails.length < perfMonitor.maxFireTrails) {\n                    const isGiant = t.isGiant;\n                    const numP = isGiant ? 4 : (Math.random() < 0.5 ? 2 : 1);\n                    const numParticles = Math.max(1, Math.round(numP * perfMonitor.particleMultiplier));\n                    for (let p = 0; p < numParticles; p++) {\n                        const hue = isGiant ? (0.0 + Math.random() * 0.06) : (0.04 + Math.random() * 0.1);\n                        const fMat = new THREE.MeshBasicMaterial({\n                            color: new THREE.Color().setHSL(hue, 1, isGiant ? 0.5 + Math.random() * 0.4 : 0.45 + Math.random() * 0.4),\n                            transparent: true, opacity: 1\n                        });\n                        const fMesh = new THREE.Mesh(fireTrailGeo, fMat);\n                        const backOffset = -Math.random() * (isGiant ? 10 : 6);\n                        fMesh.position.copy(t.mesh.position).addScaledVector(t.dir.clone().normalize(), backOffset);\n                        fMesh.scale.setScalar(isGiant ? (1.5 + Math.random() * 2.5) : (0.6 + Math.random() * 1.2));\n                        scene.add(fMesh);\n                        fireTrails.push({ mesh: fMesh, life: isGiant ? 0.5 + Math.random() * 0.3 : 0.35 + Math.random() * 0.2 });\n                    }\n                }\n                if (t.life <= 0) {\n                    if (t.pooled) {\n                        tracerPool.release(t.mesh);\n                        if (t.glowMesh) glowPool.release(t.glowMesh);\n                    } else {\n                        scene.remove(t.mesh);\n                        if (t.glowMesh) scene.remove(t.glowMesh);\n                    }\n                    tracers.splice(i, 1);\n                }\n            }\n            // Fire trail fade\n            for (let i = fireTrails.length - 1; i >= 0; i--) {\n                const f = fireTrails[i];\n                f.life -= dt;\n                f.mesh.material.opacity = Math.max(0, f.life / 0.55);\n                f.mesh.scale.multiplyScalar(0.91);\n                if (f.life <= 0) {\n                    scene.remove(f.mesh);\n                    fireTrails.splice(i, 1);\n                }\n            }\n            // Crater fade\n            for (let i = craters.length - 1; i >= 0; i--) {\n                const c = craters[i];\n                c.life -= dt;\n                if (c.life <= 0) {\n                    scene.remove(c.crater);\n                    scene.remove(c.ring);\n                    craters.splice(i, 1);\n                }\n            }\n            // Shell casings\n            for (let i = shellCasings.length - 1; i >= 0; i--) {\n                const s = shellCasings[i];\n                s.life -= dt;\n                s.vel.y -= 15 * dt;\n                s.mesh.position.addScaledVector(s.vel, dt);\n                s.mesh.rotation.x += dt * 10;\n                s.mesh.rotation.z += dt * 8;\n                if (s.life <= 0 || s.mesh.position.y < 0) {\n                    scene.remove(s.mesh);\n                    shellCasings.splice(i, 1);\n                }\n            }\n            // Muzzle fire particles\n            for (let i = muzzleParticles.length - 1; i >= 0; i--) {\n                const p = muzzleParticles[i];\n                p.life -= dt;\n                p.mesh.position.addScaledVector(p.vel, dt);\n                p.mesh.material.opacity = Math.max(0, p.life / 0.3);\n                p.mesh.scale.multiplyScalar(0.92);\n                if (p.life <= 0) {\n                    scene.remove(p.mesh);\n                    muzzleParticles.splice(i, 1);\n                }\n            }\n        }\n\n        // --- Monster System (Multiple Blocky Monster Types) ---\n        const monsters = [];\n        const monsterColor = new THREE.Color(config.settings.monsterColor);\n\n        // Monster type definitions\n        const MONSTER_TYPES = {\n            flyer: { health: 10, speed: 1.8, height: 8, width: 5, points: 120, headPoints: 200, color: 0xffd700 },\n            spiker: { health: 15, speed: 1.5, height: 16, width: 4, points: 150, headPoints: 250, color: 0xcc4444 },\n            swarmer: { health: 8, speed: 2.5, height: 4, width: 2.5, points: 80, headPoints: 150, color: 0x44cc44 },\n            brute: { health: 30, speed: 1.0, height: 14, width: 6, points: 200, headPoints: 300, color: null },\n            demolisher: { health: 60, speed: 0.7, height: 16, width: 9, points: 300, headPoints: 500, color: 0x661133 },\n            titan: { health: 100, speed: 0.4, height: 20, width: 10, points: 500, headPoints: 800, color: 0x664422 },\n            colossus: { health: 200, speed: 0.3, height: 24, width: 14, points: 800, headPoints: 1200, color: 0x443355 },\n            dinosaur: { health: 150, speed: 0.5, height: 22, width: 12, points: 600, headPoints: 1000, color: 0x0a0a0a }\n        };\n\n        // --- Brute Monster (Original Design) ---\n        function createBruteMonster() {\n            const group = new THREE.Group();\n            const MW = 6, MH = 14;\n            const baseMat = new THREE.MeshStandardMaterial({ color: monsterColor, roughness: 0.6, metalness: 0.3 });\n            const darkMat = new THREE.MeshStandardMaterial({ color: monsterColor.clone().multiplyScalar(0.6), roughness: 0.7, metalness: 0.2 });\n            const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff2200 });\n            const teethMat = new THREE.MeshBasicMaterial({ color: 0xffffcc });\n            const hornMat = new THREE.MeshStandardMaterial({ color: 0x332211, roughness: 0.5 });\n\n            const body = new THREE.Mesh(new THREE.BoxGeometry(MW, 5, 4), baseMat);\n            body.position.y = 7; group.add(body);\n            const belly = new THREE.Mesh(new THREE.BoxGeometry(MW * 0.7, 3, 4.2), darkMat);\n            belly.position.y = 6.5; group.add(belly);\n            const head = new THREE.Mesh(new THREE.BoxGeometry(4, 4, 3.5), baseMat);\n            head.position.y = 11.5; group.add(head); group.userData.head = head;\n            const eyeL = new THREE.Mesh(new THREE.BoxGeometry(1, 0.8, 0.5), eyeMat);\n            eyeL.position.set(-1, 12, 1.8); group.add(eyeL);\n            const eyeR = new THREE.Mesh(new THREE.BoxGeometry(1, 0.8, 0.5), eyeMat);\n            eyeR.position.set(1, 12, 1.8); group.add(eyeR);\n            const jaw = new THREE.Mesh(new THREE.BoxGeometry(3, 1, 2), darkMat);\n            jaw.position.set(0, 9.8, 1); group.add(jaw); group.userData.jaw = jaw;\n            for (let i = -1; i <= 1; i++) {\n                const tooth = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.6, 0.4), teethMat);\n                tooth.position.set(i * 0.8, 9.4, 1.8); group.add(tooth);\n            }\n            for (let side = -1; side <= 1; side += 2) {\n                const horn = new THREE.Mesh(new THREE.BoxGeometry(0.8, 3, 0.8), hornMat);\n                horn.position.set(side * 1.8, 14, -0.5); horn.rotation.z = side * 0.3; group.add(horn);\n            }\n            const armGeoL = new THREE.BoxGeometry(2, 7, 2.5); armGeoL.translate(0, -3.5, 0);\n            const armL = new THREE.Mesh(armGeoL, darkMat); armL.position.set(-MW/2-1.5, 9.5, 0); group.add(armL); group.userData.armL = armL;\n            const armGeoR = new THREE.BoxGeometry(2, 7, 2.5); armGeoR.translate(0, -3.5, 0);\n            const armR = new THREE.Mesh(armGeoR, darkMat.clone()); armR.position.set(MW/2+1.5, 9.5, 0); group.add(armR); group.userData.armR = armR;\n            const fistL = new THREE.Mesh(new THREE.BoxGeometry(2.5, 2.5, 2.5), baseMat); fistL.position.set(0, -5.5, 0); armL.add(fistL);\n            const fistR = new THREE.Mesh(new THREE.BoxGeometry(2.5, 2.5, 2.5), baseMat); fistR.position.set(0, -5.5, 0); armR.add(fistR);\n            const legGeoL = new THREE.BoxGeometry(2.2, 5, 2.5); legGeoL.translate(0, -2.5, 0);\n            const legL = new THREE.Mesh(legGeoL, darkMat.clone()); legL.position.set(-1.5, 4.5, 0); group.add(legL); group.userData.legL = legL;\n            const legGeoR = new THREE.BoxGeometry(2.2, 5, 2.5); legGeoR.translate(0, -2.5, 0);\n            const legR = new THREE.Mesh(legGeoR, darkMat.clone()); legR.position.set(1.5, 4.5, 0); group.add(legR); group.userData.legR = legR;\n            for (let i = 0; i < 3; i++) {\n                const spike = new THREE.Mesh(new THREE.BoxGeometry(1, 2+i*0.5, 1), hornMat);\n                spike.position.set(0, 8+i*1.2, -2.5); spike.rotation.x = -0.3; group.add(spike);\n            }\n            return group;\n        }\n\n        // --- Demolisher Monster (Four-armed hulking beast with skull face) ---\n        function createDemolisherMonster() {\n            const group = new THREE.Group();\n            const col = new THREE.Color(0x661133);\n            const baseMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.6, metalness: 0.4 });\n            const darkMat = new THREE.MeshStandardMaterial({ color: col.clone().multiplyScalar(0.4), roughness: 0.7 });\n            const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0044 });\n            const boneMat = new THREE.MeshStandardMaterial({ color: 0xccbbaa, roughness: 0.5, metalness: 0.2 });\n            const lavaMat = new THREE.MeshBasicMaterial({ color: 0xff3300, transparent: true, opacity: 0.8 });\n\n            // Massive torso\n            const body = new THREE.Mesh(new THREE.BoxGeometry(9, 7, 6), baseMat);\n            body.position.y = 9; group.add(body);\n            // Exposed rib cage\n            for (let i = 0; i < 4; i++) {\n                const rib = new THREE.Mesh(new THREE.BoxGeometry(8, 0.4, 0.6), boneMat);\n                rib.position.set(0, 7 + i * 1.5, 3.2); group.add(rib);\n            }\n            // Lava core visible through chest\n            const core = new THREE.Mesh(new THREE.BoxGeometry(4, 3, 1), lavaMat);\n            core.position.set(0, 9, 3.3); group.add(core);\n            // Skull-like head\n            const head = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 4.5), boneMat);\n            head.position.y = 14.5; group.add(head); group.userData.head = head;\n            // Deep eye sockets\n            const socketL = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.5, 1), darkMat);\n            socketL.position.set(-1.2, 15, 2.3); group.add(socketL);\n            const socketR = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.5, 1), darkMat);\n            socketR.position.set(1.2, 15, 2.3); group.add(socketR);\n            // Glowing eyes inside sockets\n            const eyeL = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.8, 0.5), eyeMat);\n            eyeL.position.set(-1.2, 15, 2.5); group.add(eyeL);\n            const eyeR = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.8, 0.5), eyeMat);\n            eyeR.position.set(1.2, 15, 2.5); group.add(eyeR);\n            // Jaw with teeth\n            const jaw = new THREE.Mesh(new THREE.BoxGeometry(4, 2, 3), darkMat);\n            jaw.position.set(0, 12, 1); group.add(jaw); group.userData.jaw = jaw;\n            for (let i = -2; i <= 2; i++) {\n                const tooth = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.2, 0.5), boneMat);\n                tooth.position.set(i * 0.9, 11.2, 2.3); group.add(tooth);\n            }\n            // Four arms! Upper pair (massive)\n            const armGeoUL = new THREE.BoxGeometry(2.5, 8, 2.5); armGeoUL.translate(0, -4, 0);\n            const armUL = new THREE.Mesh(armGeoUL, baseMat); armUL.position.set(-6, 11, 0); group.add(armUL); group.userData.armL = armUL;\n            const armGeoUR = new THREE.BoxGeometry(2.5, 8, 2.5); armGeoUR.translate(0, -4, 0);\n            const armUR = new THREE.Mesh(armGeoUR, baseMat.clone()); armUR.position.set(6, 11, 0); group.add(armUR); group.userData.armR = armUR;\n            // Wrecking fists\n            const fistUL = new THREE.Mesh(new THREE.BoxGeometry(3.5, 3.5, 3.5), darkMat); fistUL.position.set(0, -6, 0); armUL.add(fistUL);\n            const fistUR = new THREE.Mesh(new THREE.BoxGeometry(3.5, 3.5, 3.5), darkMat); fistUR.position.set(0, -6, 0); armUR.add(fistUR);\n            // Lower pair (smaller, clawed)\n            const armGeoLL = new THREE.BoxGeometry(1.8, 6, 1.8); armGeoLL.translate(0, -3, 0);\n            const armLL = new THREE.Mesh(armGeoLL, darkMat.clone()); armLL.position.set(-4.5, 8, 1.5); group.add(armLL);\n            const armGeoLR = new THREE.BoxGeometry(1.8, 6, 1.8); armGeoLR.translate(0, -3, 0);\n            const armLR = new THREE.Mesh(armGeoLR, darkMat.clone()); armLR.position.set(4.5, 8, 1.5); group.add(armLR);\n            // Claws on lower arms\n            for (let side = -1; side <= 1; side += 2) {\n                for (let c = -1; c <= 1; c++) {\n                    const claw = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.5, 0.3), boneMat);\n                    claw.position.set(side * 4.5 + c * 0.5, 2.5, 2.2); claw.rotation.x = 0.3; group.add(claw);\n                }\n            }\n            // Thick legs\n            const legGeoL = new THREE.BoxGeometry(3, 5.5, 3); legGeoL.translate(0, -2.75, 0);\n            const legL = new THREE.Mesh(legGeoL, darkMat.clone()); legL.position.set(-2.5, 5.5, 0); group.add(legL); group.userData.legL = legL;\n            const legGeoR = new THREE.BoxGeometry(3, 5.5, 3); legGeoR.translate(0, -2.75, 0);\n            const legR = new THREE.Mesh(legGeoR, darkMat.clone()); legR.position.set(2.5, 5.5, 0); group.add(legR); group.userData.legR = legR;\n            // Spine protrusions\n            for (let i = 0; i < 5; i++) {\n                const spine = new THREE.Mesh(new THREE.BoxGeometry(1, 2 + i * 0.4, 1), boneMat);\n                spine.position.set(0, 7 + i * 1.8, -3.5); spine.rotation.x = -0.3; group.add(spine);\n            }\n            return group;\n        }\n\n        // --- Titan Monster (Huge, slow, heavily armored) ---\n        function createTitanMonster() {\n            const group = new THREE.Group();\n            const col = new THREE.Color(0x664422);\n            const baseMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.8, metalness: 0.5 });\n            const darkMat = new THREE.MeshStandardMaterial({ color: col.clone().multiplyScalar(0.5), roughness: 0.9 });\n            const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffaa00 });\n            const armorMat = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.3, metalness: 0.8 });\n\n            // Massive body\n            const body = new THREE.Mesh(new THREE.BoxGeometry(10, 8, 7), baseMat);\n            body.position.y = 10; group.add(body);\n            // Armor plates\n            const chestPlate = new THREE.Mesh(new THREE.BoxGeometry(9, 6, 7.2), armorMat);\n            chestPlate.position.y = 10.5; group.add(chestPlate);\n            // Small head on massive body\n            const head = new THREE.Mesh(new THREE.BoxGeometry(5, 4, 4), baseMat);\n            head.position.y = 16; group.add(head); group.userData.head = head;\n            // Helmet armor\n            const helmet = new THREE.Mesh(new THREE.BoxGeometry(5.5, 2, 4.5), armorMat);\n            helmet.position.y = 17; group.add(helmet);\n            // Eyes (small, glowing)\n            const eyeL = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.6, 0.5), eyeMat);\n            eyeL.position.set(-1.2, 15.8, 2.2); group.add(eyeL);\n            const eyeR = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.6, 0.5), eyeMat);\n            eyeR.position.set(1.2, 15.8, 2.2); group.add(eyeR);\n            // Jaw\n            const jaw = new THREE.Mesh(new THREE.BoxGeometry(4, 1.5, 2.5), darkMat);\n            jaw.position.set(0, 13.8, 1.2); group.add(jaw); group.userData.jaw = jaw;\n            // Massive arms\n            const armGeoL = new THREE.BoxGeometry(3.5, 10, 3.5); armGeoL.translate(0, -5, 0);\n            const armL = new THREE.Mesh(armGeoL, baseMat); armL.position.set(-7, 12, 0); group.add(armL); group.userData.armL = armL;\n            const armGeoR = new THREE.BoxGeometry(3.5, 10, 3.5); armGeoR.translate(0, -5, 0);\n            const armR = new THREE.Mesh(armGeoR, baseMat.clone()); armR.position.set(7, 12, 0); group.add(armR); group.userData.armR = armR;\n            // Wrecking ball fists\n            const fistL = new THREE.Mesh(new THREE.BoxGeometry(4, 4, 4), armorMat); fistL.position.set(0, -7, 0); armL.add(fistL);\n            const fistR = new THREE.Mesh(new THREE.BoxGeometry(4, 4, 4), armorMat); fistR.position.set(0, -7, 0); armR.add(fistR);\n            // Thick legs\n            const legGeoL = new THREE.BoxGeometry(3.5, 6, 4); legGeoL.translate(0, -3, 0);\n            const legL = new THREE.Mesh(legGeoL, darkMat.clone()); legL.position.set(-3, 6, 0); group.add(legL); group.userData.legL = legL;\n            const legGeoR = new THREE.BoxGeometry(3.5, 6, 4); legGeoR.translate(0, -3, 0);\n            const legR = new THREE.Mesh(legGeoR, darkMat.clone()); legR.position.set(3, 6, 0); group.add(legR); group.userData.legR = legR;\n            // Shoulder spikes\n            for (let side = -1; side <= 1; side += 2) {\n                for (let i = 0; i < 2; i++) {\n                    const spike = new THREE.Mesh(new THREE.BoxGeometry(1.2, 3+i, 1.2), armorMat);\n                    spike.position.set(side * 5.5, 15 + i * 1.5, -1.5 + i);\n                    spike.rotation.z = side * 0.4; group.add(spike);\n                }\n            }\n            // Back crystal growths\n            for (let i = 0; i < 4; i++) {\n                const crystal = new THREE.Mesh(new THREE.BoxGeometry(1.5, 4 + i * 0.8, 1.5), new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 0.7 }));\n                crystal.position.set((Math.random()-0.5)*4, 12 + i * 1.5, -4);\n                crystal.rotation.z = (Math.random()-0.5) * 0.5; group.add(crystal);\n            }\n            return group;\n        }\n\n        // --- Flyer Monster (Flying blocky bat/dragon) ---\n        function createFlyerMonster() {\n            const group = new THREE.Group();\n            const col = new THREE.Color(0xffd700); // Golden Yellow\n            const baseMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.3, metalness: 0.7 });\n            const darkMat = new THREE.MeshStandardMaterial({ color: col.clone().multiplyScalar(0.6), roughness: 0.4, metalness: 0.6 });\n            const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });\n            const wingMat = new THREE.MeshStandardMaterial({ color: col.clone().multiplyScalar(0.8), roughness: 0.2, metalness: 0.5, side: THREE.DoubleSide });\n            const glowMat = new THREE.MeshBasicMaterial({ color: 0xffcc00, transparent: true, opacity: 0.4 });\n\n            // Streamlined body\n            const body = new THREE.Mesh(new THREE.BoxGeometry(3, 3, 5), baseMat);\n            body.position.y = 0; group.add(body);\n            // Belly glow\n            const bellyGlow = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.5, 4), glowMat);\n            bellyGlow.position.y = -1.5; group.add(bellyGlow);\n            // Head (angular)\n            const head = new THREE.Mesh(new THREE.BoxGeometry(2.5, 2.5, 3), baseMat);\n            head.position.set(0, 0.5, 3.5); group.add(head); group.userData.head = head;\n            // Beak/mouth\n            const jaw = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.8, 2), darkMat);\n            jaw.position.set(0, -0.5, 4.5); group.add(jaw); group.userData.jaw = jaw;\n            // Eyes (big, glowing)\n            const eyeL = new THREE.Mesh(new THREE.BoxGeometry(0.8, 1.0, 0.5), eyeMat);\n            eyeL.position.set(-1, 1, 5); group.add(eyeL);\n            const eyeR = new THREE.Mesh(new THREE.BoxGeometry(0.8, 1.0, 0.5), eyeMat);\n            eyeR.position.set(1, 1, 5); group.add(eyeR);\n            // Horns/crest\n            for (let side = -1; side <= 1; side += 2) {\n                const horn = new THREE.Mesh(new THREE.BoxGeometry(0.5, 2, 0.5), darkMat);\n                horn.position.set(side * 0.8, 2.5, 3); horn.rotation.z = side * 0.3; group.add(horn);\n            }\n            // Wings (large, segmented blocky wings)\n            const wingL = new THREE.Group(); wingL.position.set(-2, 0.5, 0);\n            const wing1L = new THREE.Mesh(new THREE.BoxGeometry(6, 0.4, 3.5), wingMat); wing1L.position.set(-3.5, 0, 0); wingL.add(wing1L);\n            const wing2L = new THREE.Mesh(new THREE.BoxGeometry(4, 0.3, 2.5), wingMat); wing2L.position.set(-6.5, -0.3, 0.5); wingL.add(wing2L);\n            // Wing bones\n            const boneL = new THREE.Mesh(new THREE.BoxGeometry(8, 0.5, 0.5), darkMat); boneL.position.set(-4, 0.3, 0); wingL.add(boneL);\n            group.add(wingL); group.userData.armL = wingL;\n\n            const wingR = new THREE.Group(); wingR.position.set(2, 0.5, 0);\n            const wing1R = new THREE.Mesh(new THREE.BoxGeometry(6, 0.4, 3.5), wingMat); wing1R.position.set(3.5, 0, 0); wingR.add(wing1R);\n            const wing2R = new THREE.Mesh(new THREE.BoxGeometry(4, 0.3, 2.5), wingMat); wing2R.position.set(6.5, -0.3, 0.5); wingR.add(wing2R);\n            const boneR = new THREE.Mesh(new THREE.BoxGeometry(8, 0.5, 0.5), darkMat); boneR.position.set(4, 0.3, 0); wingR.add(boneR);\n            group.add(wingR); group.userData.armR = wingR;\n\n            // Tail\n            const tail = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 4), darkMat);\n            tail.position.set(0, -0.5, -4); group.add(tail);\n            const tailFin = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.3, 1.5), wingMat);\n            tailFin.position.set(0, 0, -5.5); group.add(tailFin);\n            // Claws\n            for (let side = -1; side <= 1; side += 2) {\n                const claw = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.5, 0.6), darkMat);\n                claw.position.set(side * 1, -2.5, 1); group.add(claw);\n            }\n            // No legs for flyer animation\n            group.userData.legL = null;\n            group.userData.legR = null;\n            return group;\n        }\n\n        // --- Spiker Monster (Tall, thin, covered in spikes) ---\n        function createSpikerMonster() {\n            const group = new THREE.Group();\n            const col = new THREE.Color(0xcc4444);\n            const baseMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.5, metalness: 0.4 });\n            const darkMat = new THREE.MeshStandardMaterial({ color: col.clone().multiplyScalar(0.5), roughness: 0.6 });\n            const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0066 });\n            const spikeMat = new THREE.MeshStandardMaterial({ color: 0x220000, roughness: 0.3, metalness: 0.7 });\n\n            // Thin tall body\n            const body = new THREE.Mesh(new THREE.BoxGeometry(3, 7, 2.5), baseMat);\n            body.position.y = 9; group.add(body);\n            // Narrow head (skull-like)\n            const head = new THREE.Mesh(new THREE.BoxGeometry(2.5, 3.5, 2.5), baseMat);\n            head.position.y = 14; group.add(head); group.userData.head = head;\n            // Single cyclopean eye\n            const eye = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.5, 0.5), eyeMat);\n            eye.position.set(0, 14.5, 1.3); group.add(eye);\n            // Jaw\n            const jaw = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 1.5), darkMat);\n            jaw.position.set(0, 12.5, 0.8); group.add(jaw); group.userData.jaw = jaw;\n            // Many spikes all over body\n            for (let i = 0; i < 12; i++) {\n                const len = 1.5 + Math.random() * 2.5;\n                const spikeGeo = new THREE.BoxGeometry(0.5, len, 0.5);\n                const spike = new THREE.Mesh(spikeGeo, spikeMat);\n                const angle = (i / 12) * Math.PI * 2;\n                spike.position.set(Math.cos(angle) * 2.2, 7 + Math.random() * 6, Math.sin(angle) * 1.8);\n                spike.rotation.z = Math.cos(angle) * 0.6;\n                spike.rotation.x = -Math.sin(angle) * 0.6;\n                group.add(spike);\n            }\n            // Crown of spikes on head\n            for (let i = 0; i < 5; i++) {\n                const crownSpike = new THREE.Mesh(new THREE.BoxGeometry(0.4, 2 + Math.random(), 0.4), spikeMat);\n                crownSpike.position.set((i-2)*0.8, 16, -0.5);\n                crownSpike.rotation.z = (i-2) * 0.15; group.add(crownSpike);\n            }\n            // Long thin arms with spike tips\n            const armGeoL = new THREE.BoxGeometry(1.2, 8, 1.2); armGeoL.translate(0, -4, 0);\n            const armL = new THREE.Mesh(armGeoL, darkMat); armL.position.set(-2.5, 11, 0); group.add(armL); group.userData.armL = armL;\n            const spikeArmL = new THREE.Mesh(new THREE.BoxGeometry(0.6, 3, 0.6), spikeMat); spikeArmL.position.set(0, -6, 0); armL.add(spikeArmL);\n            const armGeoR = new THREE.BoxGeometry(1.2, 8, 1.2); armGeoR.translate(0, -4, 0);\n            const armR = new THREE.Mesh(armGeoR, darkMat.clone()); armR.position.set(2.5, 11, 0); group.add(armR); group.userData.armR = armR;\n            const spikeArmR = new THREE.Mesh(new THREE.BoxGeometry(0.6, 3, 0.6), spikeMat); spikeArmR.position.set(0, -6, 0); armR.add(spikeArmR);\n            // Digitigrade legs\n            const legGeoL = new THREE.BoxGeometry(1.5, 5, 1.5); legGeoL.translate(0, -2.5, 0);\n            const legL = new THREE.Mesh(legGeoL, darkMat.clone()); legL.position.set(-1.2, 5.5, 0); group.add(legL); group.userData.legL = legL;\n            const legGeoR = new THREE.BoxGeometry(1.5, 5, 1.5); legGeoR.translate(0, -2.5, 0);\n            const legR = new THREE.Mesh(legGeoR, darkMat.clone()); legR.position.set(1.2, 5.5, 0); group.add(legR); group.userData.legR = legR;\n            // Feet claws\n            for (let side = -1; side <= 1; side += 2) {\n                const claw = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 1.5), spikeMat);\n                claw.position.set(side * 1.2, 0.3, 0.8); group.add(claw);\n            }\n            return group;\n        }\n\n        // --- Swarmer Monster (Small insect-like, fast, appears in groups) ---\n        function createSwarmerMonster() {\n            const group = new THREE.Group();\n            const col = new THREE.Color(0x44cc44);\n            const baseMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.4, metalness: 0.6 });\n            const darkMat = new THREE.MeshStandardMaterial({ color: col.clone().multiplyScalar(0.4), roughness: 0.5 });\n            const eyeMat = new THREE.MeshBasicMaterial({ color: 0x00ff00 });\n            const clawMat = new THREE.MeshStandardMaterial({ color: 0x112211, roughness: 0.3, metalness: 0.7 });\n\n            // Compact insect-like body\n            const body = new THREE.Mesh(new THREE.BoxGeometry(2, 1.5, 3), baseMat);\n            body.position.y = 2; group.add(body);\n            // Abdomen\n            const abdomen = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.3, 2), darkMat);\n            abdomen.position.set(0, 1.8, -2); group.add(abdomen);\n            // Head\n            const head = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.5, 1.5), baseMat);\n            head.position.set(0, 2.5, 2); group.add(head); group.userData.head = head;\n            // Multiple eyes\n            for (let i = -1; i <= 1; i++) {\n                const eye = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.3), eyeMat);\n                eye.position.set(i * 0.5, 2.8, 2.8); group.add(eye);\n            }\n            // Mandibles\n            const jaw = new THREE.Mesh(new THREE.BoxGeometry(1, 0.5, 1), darkMat);\n            jaw.position.set(0, 1.8, 2.5); group.add(jaw); group.userData.jaw = jaw;\n            for (let side = -1; side <= 1; side += 2) {\n                const mandible = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 1.2), clawMat);\n                mandible.position.set(side * 0.6, 1.9, 3); group.add(mandible);\n            }\n            // Six legs (insect-like)\n            const legs = [];\n            for (let side = -1; side <= 1; side += 2) {\n                for (let i = -1; i <= 1; i++) {\n                    const legGeo = new THREE.BoxGeometry(0.3, 2, 0.3); legGeo.translate(0, -1, 0);\n                    const leg = new THREE.Mesh(legGeo, darkMat.clone());\n                    leg.position.set(side * 1.3, 1.5, i * 1);\n                    leg.rotation.z = side * 0.5;\n                    group.add(leg);\n                    legs.push(leg);\n                }\n            }\n            group.userData.legs = legs;\n            group.userData.armL = null; group.userData.armR = null;\n            group.userData.legL = legs[0] || null; group.userData.legR = legs[3] || null;\n            // Tail stinger\n            const stinger = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 1.5), clawMat);\n            stinger.position.set(0, 2.2, -3.2); stinger.rotation.x = 0.3; group.add(stinger);\n            return group;\n        }\n\n        // --- Colossus Monster (Enormous armored beast, appears late game) ---\n        function createColossusMonster() {\n            const group = new THREE.Group();\n            const col = new THREE.Color(0x443355);\n            const baseMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.7, metalness: 0.6 });\n            const darkMat = new THREE.MeshStandardMaterial({ color: col.clone().multiplyScalar(0.4), roughness: 0.8 });\n            const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff00ff });\n            const armorMat = new THREE.MeshStandardMaterial({ color: 0x333344, roughness: 0.2, metalness: 0.9 });\n            const glowMat = new THREE.MeshBasicMaterial({ color: 0xaa00ff, transparent: true, opacity: 0.7 });\n\n            // Enormous body\n            const body = new THREE.Mesh(new THREE.BoxGeometry(12, 10, 9), baseMat);\n            body.position.y = 12; group.add(body);\n            // Heavy armor plates\n            const frontPlate = new THREE.Mesh(new THREE.BoxGeometry(11, 8, 9.2), armorMat);\n            frontPlate.position.y = 12.5; group.add(frontPlate);\n            // Glowing cracks in armor\n            for (let i = 0; i < 5; i++) {\n                const crack = new THREE.Mesh(new THREE.BoxGeometry(0.3, 2 + Math.random() * 3, 9.3), glowMat);\n                crack.position.set((Math.random()-0.5) * 8, 10 + Math.random() * 5, 0); group.add(crack);\n            }\n            // Tiny head relative to body\n            const head = new THREE.Mesh(new THREE.BoxGeometry(5, 4, 5), baseMat);\n            head.position.y = 19; group.add(head); group.userData.head = head;\n            // Heavy brow/helmet\n            const brow = new THREE.Mesh(new THREE.BoxGeometry(6, 2, 5.5), armorMat);\n            brow.position.y = 20; group.add(brow);\n            // Glowing eyes\n            const eyeL = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1, 0.5), eyeMat);\n            eyeL.position.set(-1.5, 18.5, 2.8); group.add(eyeL);\n            const eyeR = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1, 0.5), eyeMat);\n            eyeR.position.set(1.5, 18.5, 2.8); group.add(eyeR);\n            // Jaw\n            const jaw = new THREE.Mesh(new THREE.BoxGeometry(4, 2, 3.5), darkMat);\n            jaw.position.set(0, 16.5, 1.5); group.add(jaw); group.userData.jaw = jaw;\n            // Massive pillar arms\n            const armGeoL = new THREE.BoxGeometry(4, 12, 4); armGeoL.translate(0, -6, 0);\n            const armL = new THREE.Mesh(armGeoL, baseMat); armL.position.set(-9, 14, 0); group.add(armL); group.userData.armL = armL;\n            const armGeoR = new THREE.BoxGeometry(4, 12, 4); armGeoR.translate(0, -6, 0);\n            const armR = new THREE.Mesh(armGeoR, baseMat.clone()); armR.position.set(9, 14, 0); group.add(armR); group.userData.armR = armR;\n            // Hammer fists\n            const fistL = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 5), armorMat); fistL.position.set(0, -8, 0); armL.add(fistL);\n            const fistR = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 5), armorMat); fistR.position.set(0, -8, 0); armR.add(fistR);\n            // Trunk-like legs\n            const legGeoL = new THREE.BoxGeometry(4.5, 7, 5); legGeoL.translate(0, -3.5, 0);\n            const legL = new THREE.Mesh(legGeoL, darkMat.clone()); legL.position.set(-4, 7, 0); group.add(legL); group.userData.legL = legL;\n            const legGeoR = new THREE.BoxGeometry(4.5, 7, 5); legGeoR.translate(0, -3.5, 0);\n            const legR = new THREE.Mesh(legGeoR, darkMat.clone()); legR.position.set(4, 7, 0); group.add(legR); group.userData.legR = legR;\n            // Crystal growths on back\n            for (let i = 0; i < 6; i++) {\n                const crystal = new THREE.Mesh(\n                    new THREE.BoxGeometry(1.5 + Math.random(), 5 + Math.random() * 4, 1.5 + Math.random()),\n                    glowMat.clone()\n                );\n                crystal.position.set((Math.random()-0.5) * 6, 15 + i * 1.5, -5);\n                crystal.rotation.z = (Math.random()-0.5) * 0.4;\n                group.add(crystal);\n            }\n            // Shoulder spikes\n            for (let side = -1; side <= 1; side += 2) {\n                for (let i = 0; i < 3; i++) {\n                    const spike = new THREE.Mesh(new THREE.BoxGeometry(1.5, 3 + i * 1.5, 1.5), armorMat);\n                    spike.position.set(side * 7, 18 + i * 2, -2 + i);\n                    spike.rotation.z = side * 0.3; group.add(spike);\n                }\n            }\n            return group;\n        }\n\n        // --- Dinosaur Monster (Giant black upright T-Rex) ---\n        function createDinosaurMonster() {\n            const group = new THREE.Group();\n            const col = new THREE.Color(0x0a0a0a);\n            const baseMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.8, metalness: 0.3 });\n            const darkMat = new THREE.MeshStandardMaterial({ color: 0x050505, roughness: 0.9, metalness: 0.2 });\n            const bellyMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.7 });\n            const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff2200 });\n            const teethMat = new THREE.MeshBasicMaterial({ color: 0xffffdd });\n            const clawMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.3, metalness: 0.8 });\n            const spineMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.5, metalness: 0.6 });\n\n            // Massive torso (upright stance)\n            const torso = new THREE.Mesh(new THREE.BoxGeometry(8, 9, 6), baseMat);\n            torso.position.y = 11; group.add(torso);\n            // Belly plate (lighter)\n            const belly = new THREE.Mesh(new THREE.BoxGeometry(6, 7, 6.2), bellyMat);\n            belly.position.set(0, 10, 0.5); group.add(belly);\n\n            // Massive head (T-Rex skull)\n            const head = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 7), baseMat);\n            head.position.set(0, 18, 2); group.add(head); group.userData.head = head;\n            // Upper jaw/snout\n            const snout = new THREE.Mesh(new THREE.BoxGeometry(4, 3, 5), baseMat);\n            snout.position.set(0, 17, 5.5); group.add(snout);\n            // Lower jaw (movable)\n            const jaw = new THREE.Mesh(new THREE.BoxGeometry(3.5, 2, 5), darkMat);\n            jaw.position.set(0, 15, 5); group.add(jaw); group.userData.jaw = jaw;\n            // Teeth - upper row\n            for (let i = -2; i <= 2; i++) {\n                const tooth = new THREE.Mesh(new THREE.BoxGeometry(0.5, 1.2, 0.5), teethMat);\n                tooth.position.set(i * 0.8, 15.8, 7.5); group.add(tooth);\n            }\n            // Teeth - lower row\n            for (let i = -2; i <= 2; i++) {\n                const tooth = new THREE.Mesh(new THREE.BoxGeometry(0.4, 1.0, 0.4), teethMat);\n                tooth.position.set(i * 0.8, 15.2, 7.2); group.add(tooth);\n            }\n            // Large fangs\n            const fangL = new THREE.Mesh(new THREE.BoxGeometry(0.6, 2.0, 0.6), teethMat);\n            fangL.position.set(-1.5, 15.5, 7.8); group.add(fangL);\n            const fangR = new THREE.Mesh(new THREE.BoxGeometry(0.6, 2.0, 0.6), teethMat);\n            fangR.position.set(1.5, 15.5, 7.8); group.add(fangR);\n            // Glowing red eyes\n            const eyeL = new THREE.Mesh(new THREE.BoxGeometry(1.2, 1.0, 0.5), eyeMat);\n            eyeL.position.set(-1.5, 19, 5.3); group.add(eyeL);\n            const eyeR = new THREE.Mesh(new THREE.BoxGeometry(1.2, 1.0, 0.5), eyeMat);\n            eyeR.position.set(1.5, 19, 5.3); group.add(eyeR);\n            // Brow ridges\n            const browL = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.6, 1.5), darkMat);\n            browL.position.set(-1.5, 19.8, 4.5); group.add(browL);\n            const browR = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.6, 1.5), darkMat);\n            browR.position.set(1.5, 19.8, 4.5); group.add(browR);\n            // Head crest/horns\n            for (let side = -1; side <= 1; side += 2) {\n                const horn = new THREE.Mesh(new THREE.BoxGeometry(0.8, 2.5, 0.8), clawMat);\n                horn.position.set(side * 2, 20.5, 1); horn.rotation.z = side * 0.2; group.add(horn);\n            }\n\n            // Tiny T-Rex arms\n            const armGeoL = new THREE.BoxGeometry(1.2, 3.5, 1.2); armGeoL.translate(0, -1.75, 0);\n            const armL = new THREE.Mesh(armGeoL, darkMat); armL.position.set(-4.5, 14, 1.5); group.add(armL); group.userData.armL = armL;\n            const armGeoR = new THREE.BoxGeometry(1.2, 3.5, 1.2); armGeoR.translate(0, -1.75, 0);\n            const armR = new THREE.Mesh(armGeoR, darkMat.clone()); armR.position.set(4.5, 14, 1.5); group.add(armR); group.userData.armR = armR;\n            // Tiny claws on arms\n            for (let side = -1; side <= 1; side += 2) {\n                for (let c = 0; c < 2; c++) {\n                    const claw = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.0, 0.3), clawMat);\n                    claw.position.set(side * 4.5 + (c - 0.5) * 0.5, 11, 2); group.add(claw);\n                }\n            }\n\n            // Massive powerful legs\n            const thighGeoL = new THREE.BoxGeometry(3.5, 6, 3.5); thighGeoL.translate(0, -3, 0);\n            const thighL = new THREE.Mesh(thighGeoL, baseMat); thighL.position.set(-3, 7, -0.5); group.add(thighL); group.userData.legL = thighL;\n            const thighGeoR = new THREE.BoxGeometry(3.5, 6, 3.5); thighGeoR.translate(0, -3, 0);\n            const thighR = new THREE.Mesh(thighGeoR, baseMat.clone()); thighR.position.set(3, 7, -0.5); group.add(thighR); group.userData.legR = thighR;\n            // Lower legs / shins\n            const shinL = new THREE.Mesh(new THREE.BoxGeometry(2.5, 4, 2.5), darkMat);\n            shinL.position.set(-3, 1.5, 0.5); group.add(shinL);\n            const shinR = new THREE.Mesh(new THREE.BoxGeometry(2.5, 4, 2.5), darkMat.clone());\n            shinR.position.set(3, 1.5, 0.5); group.add(shinR);\n            // Feet with claws\n            for (let side = -1; side <= 1; side += 2) {\n                const foot = new THREE.Mesh(new THREE.BoxGeometry(3, 1, 4), darkMat.clone());\n                foot.position.set(side * 3, 0.3, 1.5); group.add(foot);\n                // Toe claws\n                for (let t = -1; t <= 1; t++) {\n                    const toeClaw = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 1.5), clawMat);\n                    toeClaw.position.set(side * 3 + t * 0.8, 0.3, 3.5); group.add(toeClaw);\n                }\n            }\n\n            // Thick muscular tail\n            const tail1 = new THREE.Mesh(new THREE.BoxGeometry(4, 3.5, 5), baseMat);\n            tail1.position.set(0, 8, -5); tail1.rotation.x = 0.15; group.add(tail1);\n            const tail2 = new THREE.Mesh(new THREE.BoxGeometry(3, 2.5, 5), darkMat);\n            tail2.position.set(0, 7, -9.5); tail2.rotation.x = 0.25; group.add(tail2);\n            const tail3 = new THREE.Mesh(new THREE.BoxGeometry(2, 1.8, 4), darkMat.clone());\n            tail3.position.set(0, 5.5, -13); tail3.rotation.x = 0.35; group.add(tail3);\n            const tailTip = new THREE.Mesh(new THREE.BoxGeometry(1.2, 1.2, 3), baseMat);\n            tailTip.position.set(0, 4.5, -15.5); tailTip.rotation.x = 0.4; group.add(tailTip);\n\n            // Dorsal spines along back\n            for (let i = 0; i < 8; i++) {\n                const spineHeight = 1.5 + Math.sin(i * 0.5) * 1.0;\n                const spine = new THREE.Mesh(new THREE.BoxGeometry(0.8, spineHeight, 0.8), spineMat);\n                spine.position.set(0, 15 - i * 1.2, -1 - i * 1.5);\n                spine.rotation.x = -0.2;\n                group.add(spine);\n            }\n\n            // Chest scars (glowing red lines)\n            const scarMat = new THREE.MeshBasicMaterial({ color: 0x440000, transparent: true, opacity: 0.6 });\n            for (let i = 0; i < 3; i++) {\n                const scar = new THREE.Mesh(new THREE.BoxGeometry(0.2, 2 + Math.random() * 2, 6.3), scarMat);\n                scar.position.set((i - 1) * 2, 10 + i * 0.5, 0); group.add(scar);\n            }\n\n            return group;\n        }\n\n        function createMonsterByType(type) {\n            switch(type) {\n                case 'demolisher': return createDemolisherMonster();\n                case 'titan': return createTitanMonster();\n                case 'flyer': return createFlyerMonster();\n                case 'spiker': return createSpikerMonster();\n                case 'swarmer': return createSwarmerMonster();\n                case 'colossus': return createColossusMonster();\n                case 'dinosaur': return createDinosaurMonster();\n                default: return createBruteMonster();\n            }\n        }\n\n        let dinosaurSpawnedThisLevel = false;\n\n        function spawnMonster() {\n            // Force dinosaur spawn at start of level 2+\n            let type;\n            if (currentLevel >= 2 && !dinosaurSpawnedThisLevel) {\n                type = 'dinosaur';\n                dinosaurSpawnedThisLevel = true;\n                showKillFeed('\u26a0\ufe0f GIANT DINOSAUR SPOTTED! BEWARE!');\n                playSound('monster_roar', 0.8);\n            } else {\n            // Wave-based type selection with new types unlocking over time\n            let typeRoll = Math.random();\n            if (evacWave >= 12 && typeRoll < 0.12) type = 'colossus';\n            else if (evacWave >= 6 && typeRoll < 0.30) type = 'swarmer';\n            else if (evacWave >= 8 && typeRoll < 0.18) type = 'titan';\n            else if (evacWave >= 2 && typeRoll < 0.10) type = 'dinosaur';\n            else if (typeRoll < 0.30) type = 'brute';\n            else if (typeRoll < 0.50) type = 'demolisher';\n            else if (typeRoll < 0.65) type = 'spiker';\n            else if (typeRoll < 0.80) type = 'flyer';\n            else type = evacWave > 10 ? 'titan' : (Math.random() < 0.5 ? 'demolisher' : 'brute');\n            }\n\n            const typeDef = MONSTER_TYPES[type];\n            const group = createMonsterByType(type);\n\n\n            // Wave-based health scaling: slower scaling as requested (monsters stay around 200 HP)\n            const healthMultiplier = 1 + (evacWave - 1) * 0.2;\n            // Wave-based size scaling: monsters grow larger each wave (base size doubled)\n            const sizeScale = 2 * (1 + (evacWave - 1) * 0.12);\n\n            // Random spawn position on street edges\n            const side = Math.floor(Math.random() * 4);\n            let x, z;\n            if (side === 0) { x = -100 + Math.random() * 200; z = -120; }\n            else if (side === 1) { x = -100 + Math.random() * 200; z = 120; }\n            else if (side === 2) { x = -120; z = -100 + Math.random() * 200; }\n            else { x = 120; z = -100 + Math.random() * 200; }\n\n            // Flyers start at altitude\n            const startY = type === 'flyer' ? 90 : 0;\n            group.position.set(x, startY, z);\n            scene.add(group);\n\n            // Apply wave scaling\n            group.scale.setScalar(sizeScale);\n            const scaledHealth = Math.ceil(typeDef.health * healthMultiplier);\n            const scaledHeight = typeDef.height * sizeScale;\n            const scaledWidth = typeDef.width * sizeScale;\n            // Points also scale with wave\n            const pointsMult = 1 + (evacWave - 1) * 0.3;\n\n            // Pre-compute path immediately on spawn\n            const initialPath = (type !== 'flyer') ? findPath(x, z, EVAC_ZONE_POS.x, EVAC_ZONE_POS.z) : null;\n\n            monsters.push({\n                group,\n                type,\n                health: scaledHealth,\n                maxHealth: scaledHealth,\n                speed: config.settings.monsterSpeed * typeDef.speed * (1 + evacWave * 0.02) * 1.2,\n                alive: true,\n                hitTimer: 0,\n                animTime: Math.random() * Math.PI * 2,\n                attacking: false,\n                attackTimer: 0,\n                walkCycle: 0,\n                flyHeight: startY,\n                monsterHeight: scaledHeight,\n                monsterWidth: scaledWidth,\n                points: Math.ceil(typeDef.points * pointsMult),\n                headPoints: Math.ceil(typeDef.headPoints * pointsMult),\n                path: initialPath,\n                pathIndex: 0,\n                pathRecalcTimer: initialPath ? 2.0 : 0.1,\n                lastTargetPos: EVAC_ZONE_POS.clone()\n            });\n        }\n\n        function removeMonster(index) {\n            const m = monsters[index];\n            m.alive = false;\n            scene.remove(m.group);\n            monsters.splice(index, 1);\n        }\n\n        // --- Pedestrian & Vehicle System ---\n        const pedestrians = [];\n        const vehicles = [];\n        const MAX_PEDESTRIANS = 12;\n        const MAX_VEHICLES = 20;\n        const PEDESTRIAN_SPEED = 4;\n        const VEHICLE_SPEED = 12;\n        const FLEE_RANGE = 40;\n        const DESTROY_RANGE = 5;\n\n        function createBlockyPedestrian() {\n            const group = new THREE.Group();\n            const skinColors = [0xffcc99, 0xbb8855, 0xffe0bd, 0xcc9966];\n            const shirtColors = [0x3366cc, 0xcc3333, 0x33aa33, 0xff9900, 0x9933cc, 0xffff33];\n            const pantsColors = [0x222244, 0x333333, 0x44332b, 0x1a1a3a];\n            const skinColor = skinColors[Math.floor(Math.random() * skinColors.length)];\n            const shirtColor = shirtColors[Math.floor(Math.random() * shirtColors.length)];\n            const pantsColor = pantsColors[Math.floor(Math.random() * pantsColors.length)];\n\n            const skinMat = new THREE.MeshStandardMaterial({ color: skinColor, roughness: 0.8 });\n            const shirtMat = new THREE.MeshStandardMaterial({ color: shirtColor, roughness: 0.7 });\n            const pantsMat = new THREE.MeshStandardMaterial({ color: pantsColor, roughness: 0.7 });\n\n            // Head\n            const headGeo = new THREE.BoxGeometry(0.6, 0.7, 0.6);\n            const head = new THREE.Mesh(headGeo, skinMat);\n            head.position.y = 2.4;\n            group.add(head);\n\n            // Body\n            const bodyGeo = new THREE.BoxGeometry(0.8, 1.0, 0.5);\n            const body = new THREE.Mesh(bodyGeo, shirtMat);\n            body.position.y = 1.7;\n            group.add(body);\n\n            // Legs\n            const legGeoL = new THREE.BoxGeometry(0.3, 0.9, 0.35);\n            legGeoL.translate(0, -0.45, 0);\n            const legL = new THREE.Mesh(legGeoL, pantsMat);\n            legL.position.set(-0.2, 1.2, 0);\n            group.add(legL);\n            group.userData.legL = legL;\n\n            const legGeoR = new THREE.BoxGeometry(0.3, 0.9, 0.35);\n            legGeoR.translate(0, -0.45, 0);\n            const legR = new THREE.Mesh(legGeoR, pantsMat.clone());\n            legR.position.set(0.2, 1.2, 0);\n            group.add(legR);\n            group.userData.legR = legR;\n\n            // Arms\n            const armGeoL = new THREE.BoxGeometry(0.25, 0.8, 0.25);\n            armGeoL.translate(0, -0.4, 0);\n            const armL = new THREE.Mesh(armGeoL, shirtMat.clone());\n            armL.position.set(-0.55, 2.1, 0);\n            group.add(armL);\n            group.userData.armL = armL;\n\n            const armGeoR = new THREE.BoxGeometry(0.25, 0.8, 0.25);\n            armGeoR.translate(0, -0.4, 0);\n            const armR = new THREE.Mesh(armGeoR, shirtMat.clone());\n            armR.position.set(0.55, 2.1, 0);\n            group.add(armR);\n            group.userData.armR = armR;\n\n            return group;\n        }\n\n        function createBlockyVehicle() {\n            const group = new THREE.Group();\n            const wheelMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9 });\n            const glassMat = new THREE.MeshStandardMaterial({ color: 0x88ccff, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.7 });\n            const lightMat = new THREE.MeshBasicMaterial({ color: 0xffffaa });\n            const tailMat = new THREE.MeshBasicMaterial({ color: 0xff2200 });\n\n            const typeRoll = Math.random();\n            let vehicleType;\n            if (typeRoll < 0.30) vehicleType = 'sedan';\n            else if (typeRoll < 0.50) vehicleType = 'taxi';\n            else if (typeRoll < 0.63) vehicleType = 'police';\n            else if (typeRoll < 0.73) vehicleType = 'truck';\n            else if (typeRoll < 0.83) vehicleType = 'bus';\n            else if (typeRoll < 0.91) vehicleType = 'suv';\n            else vehicleType = 'sportscar';\n\n            group.userData.vehicleType = vehicleType;\n\n            function addWheels(bw, bl, bh, axleY, halfW, halfL) {\n                for (let x = -1; x <= 1; x += 2) {\n                    for (let z = -1; z <= 1; z += 2) {\n                        const wGeo = new THREE.BoxGeometry(0.45, 0.7, 0.7);\n                        const w = new THREE.Mesh(wGeo, wheelMat);\n                        w.position.set(x * halfW, axleY, z * halfL);\n                        group.add(w);\n                        // Hubcap\n                        const hGeo = new THREE.BoxGeometry(0.5, 0.45, 0.45);\n                        const hMat = new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.8 });\n                        const h = new THREE.Mesh(hGeo, hMat);\n                        h.position.set(x * halfW, axleY, z * halfL);\n                        group.add(h);\n                    }\n                }\n            }\n\n            function addHeadlights(y, halfZ, halfX) {\n                for (let x = -1; x <= 1; x += 2) {\n                    const lGeo = new THREE.BoxGeometry(0.45, 0.3, 0.18);\n                    const l = new THREE.Mesh(lGeo, lightMat);\n                    l.position.set(x * halfX, y, halfZ);\n                    group.add(l);\n                }\n            }\n\n            function addTaillights(y, halfZ, halfX) {\n                for (let x = -1; x <= 1; x += 2) {\n                    const lGeo = new THREE.BoxGeometry(0.45, 0.25, 0.18);\n                    const l = new THREE.Mesh(lGeo, tailMat);\n                    l.position.set(x * halfX, y, -halfZ);\n                    group.add(l);\n                }\n            }\n\n            if (vehicleType === 'sedan') {\n                const colors = [0xcc2222, 0x2244cc, 0xffffff, 0x222222, 0x44aa44, 0x884400];\n                const col = colors[Math.floor(Math.random() * colors.length)];\n                const bMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.4, metalness: 0.5 });\n                const body = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.1, 4.4), bMat);\n                body.position.y = 0.8; group.add(body);\n                const roof = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.95, 2.4), bMat);\n                roof.position.set(0, 1.78, -0.2); group.add(roof);\n                const wind = new THREE.Mesh(new THREE.BoxGeometry(1.9, 0.75, 0.15), glassMat);\n                wind.position.set(0, 1.75, 1.05); group.add(wind);\n                const rearWind = new THREE.Mesh(new THREE.BoxGeometry(1.9, 0.75, 0.15), glassMat);\n                rearWind.position.set(0, 1.75, -1.4); group.add(rearWind);\n                addWheels(2.4, 4.4, 1.1, 0.38, 1.32, 1.45);\n                addHeadlights(0.8, 2.25, 0.85);\n                addTaillights(0.8, 2.25, 0.85);\n\n            } else if (vehicleType === 'taxi') {\n                const bMat = new THREE.MeshStandardMaterial({ color: 0xffcc00, roughness: 0.4, metalness: 0.4 });\n                const stripeMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.6 });\n                const body = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.1, 4.4), bMat);\n                body.position.y = 0.8; group.add(body);\n                // Checker stripe\n                const stripe = new THREE.Mesh(new THREE.BoxGeometry(2.45, 0.28, 4.45), stripeMat);\n                stripe.position.y = 0.65; group.add(stripe);\n                const roof = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.95, 2.4), bMat);\n                roof.position.set(0, 1.78, -0.2); group.add(roof);\n                // Taxi sign on roof\n                const signMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });\n                const sign = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.35, 1.0), signMat);\n                sign.position.set(0, 2.45, 0); group.add(sign);\n                const wind = new THREE.Mesh(new THREE.BoxGeometry(1.9, 0.75, 0.15), glassMat);\n                wind.position.set(0, 1.75, 1.05); group.add(wind);\n                addWheels(2.4, 4.4, 1.1, 0.38, 1.32, 1.45);\n                addHeadlights(0.8, 2.25, 0.85);\n                addTaillights(0.8, 2.25, 0.85);\n\n            } else if (vehicleType === 'police') {\n                const bMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.4, metalness: 0.5 });\n                const whiteMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.4, metalness: 0.3 });\n                const body = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.1, 4.4), bMat);\n                body.position.y = 0.8; group.add(body);\n                // White door stripe\n                const stripe = new THREE.Mesh(new THREE.BoxGeometry(2.45, 0.5, 2.0), whiteMat);\n                stripe.position.set(0, 0.85, 0.2); group.add(stripe);\n                const roof = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.95, 2.4), bMat);\n                roof.position.set(0, 1.78, -0.2); group.add(roof);\n                // Light bar\n                const barMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.8 });\n                const bar = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.25, 0.6), barMat);\n                bar.position.set(0, 2.42, -0.1); group.add(bar);\n                const redLight = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.22, 0.55), new THREE.MeshBasicMaterial({ color: 0xff0000 }));\n                redLight.position.set(-0.4, 2.48, -0.1); group.add(redLight);\n                const blueLight = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.22, 0.55), new THREE.MeshBasicMaterial({ color: 0x0044ff }));\n                blueLight.position.set(0.4, 2.48, -0.1); group.add(blueLight);\n                group.userData.policeRedLight = redLight;\n                group.userData.policeBlueLight = blueLight;\n                const wind = new THREE.Mesh(new THREE.BoxGeometry(1.9, 0.75, 0.15), glassMat);\n                wind.position.set(0, 1.75, 1.05); group.add(wind);\n                addWheels(2.4, 4.4, 1.1, 0.38, 1.32, 1.45);\n                addHeadlights(0.8, 2.25, 0.85);\n                addTaillights(0.8, 2.25, 0.85);\n\n            } else if (vehicleType === 'truck') {\n                const cabColors = [0xdd3311, 0x226688, 0x44aa22, 0x884422, 0x555555];\n                const col = cabColors[Math.floor(Math.random() * cabColors.length)];\n                const bMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.5, metalness: 0.4 });\n                const cargoMat = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.6 });\n                // Cab\n                const cab = new THREE.Mesh(new THREE.BoxGeometry(2.6, 2.0, 2.4), bMat);\n                cab.position.set(0, 1.3, 2.0); group.add(cab);\n                // Cargo bed\n                const cargo = new THREE.Mesh(new THREE.BoxGeometry(2.5, 1.8, 4.5), cargoMat);\n                cargo.position.set(0, 1.15, -1.5); group.add(cargo);\n                // Cab window\n                const wind = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.8, 0.15), glassMat);\n                wind.position.set(0, 1.8, 3.25); group.add(wind);\n                // Exhaust pipe\n                const exhMat = new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.8 });\n                const exh = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 2.5, 6), exhMat);\n                exh.position.set(1.2, 2.5, 1.0); group.add(exh);\n                // Front bumper\n                const bump = new THREE.Mesh(new THREE.BoxGeometry(2.8, 0.4, 0.4), new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.7 }));\n                bump.position.set(0, 0.7, 3.3); group.add(bump);\n                // Extra wheels (6-wheel truck)\n                for (let x = -1; x <= 1; x += 2) {\n                    for (let z of [-2.8, -1.0, 2.0]) {\n                        const wGeo = new THREE.BoxGeometry(0.5, 0.8, 0.8);\n                        const w = new THREE.Mesh(wGeo, wheelMat);\n                        w.position.set(x * 1.35, 0.45, z);\n                        group.add(w);\n                    }\n                }\n                addHeadlights(1.4, 3.35, 0.95);\n                addTaillights(1.1, 3.8, 0.95);\n\n            } else if (vehicleType === 'bus') {\n                const busColors = [0xdd8800, 0x2255cc, 0xcc3322, 0x228833];\n                const col = busColors[Math.floor(Math.random() * busColors.length)];\n                const bMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.5, metalness: 0.3 });\n                const body = new THREE.Mesh(new THREE.BoxGeometry(3.0, 2.8, 9.0), bMat);\n                body.position.y = 1.6; group.add(body);\n                // Windows row\n                for (let wz = -3; wz <= 3; wz += 1.5) {\n                    for (let wx = -1; wx <= 1; wx += 2) {\n                        const wGeo = new THREE.BoxGeometry(0.15, 0.9, 0.9);\n                        const w = new THREE.Mesh(wGeo, glassMat);\n                        w.position.set(wx * 1.55, 2.0, wz);\n                        group.add(w);\n                    }\n                }\n                // Front windshield\n                const wind = new THREE.Mesh(new THREE.BoxGeometry(2.6, 1.4, 0.15), glassMat);\n                wind.position.set(0, 1.9, 4.58); group.add(wind);\n                // Door\n                const doorMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.5 });\n                const door = new THREE.Mesh(new THREE.BoxGeometry(0.15, 2.2, 1.2), glassMat);\n                door.position.set(1.55, 1.4, 3.0); group.add(door);\n                // Destination sign on front\n                const signMat = new THREE.MeshBasicMaterial({ color: 0xffffff });\n                const sign = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.5, 0.18), signMat);\n                sign.position.set(0, 3.0, 4.58); group.add(sign);\n                // 6 wheels\n                for (let x = -1; x <= 1; x += 2) {\n                    for (let z of [-3.0, 0, 3.0]) {\n                        const wGeo = new THREE.BoxGeometry(0.5, 0.9, 0.9);\n                        const w = new THREE.Mesh(wGeo, wheelMat);\n                        w.position.set(x * 1.55, 0.5, z);\n                        group.add(w);\n                    }\n                }\n                addHeadlights(1.2, 4.6, 1.1);\n                addTaillights(1.2, 4.6, 1.1);\n\n            } else if (vehicleType === 'suv') {\n                const colors = [0x333344, 0x664422, 0x446644, 0x993322, 0xaaaaaa];\n                const col = colors[Math.floor(Math.random() * colors.length)];\n                const bMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.45, metalness: 0.5 });\n                const body = new THREE.Mesh(new THREE.BoxGeometry(2.7, 1.3, 5.0), bMat);\n                body.position.y = 0.95; group.add(body);\n                const roof = new THREE.Mesh(new THREE.BoxGeometry(2.5, 1.15, 3.2), bMat);\n                roof.position.set(0, 2.22, -0.2); group.add(roof);\n                // Roof rack\n                const rackMat = new THREE.MeshStandardMaterial({ color: 0x555555, metalness: 0.8 });\n                const rack = new THREE.Mesh(new THREE.BoxGeometry(2.3, 0.1, 2.8), rackMat);\n                rack.position.set(0, 2.85, -0.2); group.add(rack);\n                // Front grill\n                const grillMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.7 });\n                const grill = new THREE.Mesh(new THREE.BoxGeometry(2.3, 0.6, 0.2), grillMat);\n                grill.position.set(0, 0.8, 2.65); group.add(grill);\n                const wind = new THREE.Mesh(new THREE.BoxGeometry(2.3, 0.9, 0.15), glassMat);\n                wind.position.set(0, 2.1, 1.48); group.add(wind);\n                const rearWind = new THREE.Mesh(new THREE.BoxGeometry(2.3, 0.9, 0.15), glassMat);\n                rearWind.position.set(0, 2.1, -1.88); group.add(rearWind);\n                addWheels(2.7, 5.0, 1.3, 0.5, 1.42, 1.65);\n                addHeadlights(0.95, 2.7, 0.95);\n                addTaillights(0.95, 2.7, 0.95);\n\n            } else { // sportscar\n                const colors = [0xff1100, 0xffaa00, 0x00aaff, 0xffffff, 0x111111];\n                const col = colors[Math.floor(Math.random() * colors.length)];\n                const bMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.25, metalness: 0.7 });\n                const body = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.8, 4.6), bMat);\n                body.position.y = 0.55; group.add(body);\n                // Low sloped roof\n                const roof = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.65, 1.8), bMat);\n                roof.position.set(0, 1.28, -0.1); group.add(roof);\n                // Spoiler\n                const spoilerMat = new THREE.MeshStandardMaterial({ color: col, roughness: 0.3, metalness: 0.8 });\n                const spoiler = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.4, 0.15), spoilerMat);\n                spoiler.position.set(0, 1.3, -2.4); group.add(spoiler);\n                const wind = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.55, 0.15), glassMat);\n                wind.position.set(0, 1.15, 1.05); group.add(wind);\n                // Wide side skirts\n                for (let x = -1; x <= 1; x += 2) {\n                    const skirt = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.22, 4.2), new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.9 }));\n                    skirt.position.set(x * 1.15, 0.3, 0); group.add(skirt);\n                }\n                // Dual exhausts\n                for (let x = -0.4; x <= 0.4; x += 0.8) {\n                    const exh = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.3, 6), new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.9 }));\n                    exh.rotation.x = Math.PI / 2;\n                    exh.position.set(x, 0.35, -2.45); group.add(exh);\n                }\n                addWheels(2.2, 4.6, 0.8, 0.32, 1.18, 1.55);\n                addHeadlights(0.55, 2.38, 0.8);\n                addTaillights(0.55, 2.38, 0.8);\n            }\n\n            return group;\n        }\n\n        // Helper function to check if a position is inside any building\n        function isInsideBuilding(x, z, radius) {\n            for (let b = 0; b < buildings.length; b++) {\n                const bld = buildings[b];\n                const bPos = bld.position;\n                const bGeo = bld.geometry.parameters;\n                const halfW = bGeo.width / 2 + radius;\n                const halfD = bGeo.depth / 2 + radius;\n                if (x > bPos.x - halfW && x < bPos.x + halfW &&\n                    z > bPos.z - halfD && z < bPos.z + halfD) {\n                    return true;\n                }\n            }\n            return false;\n        }\n\n        function spawnPedestrian() {\n            if (pedestrians.length >= MAX_PEDESTRIANS) return;\n            const group = createBlockyPedestrian();\n            // Spawn from map edges\n            let x, z;\n            const side = Math.floor(Math.random() * 4);\n            if (side === 0) { x = -90; z = (Math.random() - 0.5) * 160; }\n            else if (side === 1) { x = 90; z = (Math.random() - 0.5) * 160; }\n            else if (side === 2) { x = (Math.random() - 0.5) * 160; z = -90; }\n            else { x = (Math.random() - 0.5) * 160; z = 90; }\n            \n            group.position.set(x, 0, z);\n            scene.add(group);\n            pedestrians.push({\n                group,\n                dirX: 0,\n                dirZ: 0,\n                speed: PEDESTRIAN_SPEED * (0.7 + Math.random() * 0.6),\n                alive: true,\n                fleeing: false,\n                settled: false,\n                walkCycle: Math.random() * Math.PI * 2,\n                changeTimer: 2 + Math.random() * 4\n            });\n        }\n\n        function spawnVehicle() {\n            if (vehicles.length >= MAX_VEHICLES) return;\n            const group = createBlockyVehicle();\n            const vType = group.userData.vehicleType;\n            const speedMult = vType === 'bus' ? 0.45 : vType === 'truck' ? 0.55 : vType === 'sportscar' ? 1.4 : vType === 'police' ? 1.2 : 0.7 + Math.random() * 0.5;\n            // Spawn from map edges\n            let x, z;\n            const side = Math.floor(Math.random() * 4);\n            if (side === 0) { x = -95; z = (Math.random() - 0.5) * 160; }\n            else if (side === 1) { x = 95; z = (Math.random() - 0.5) * 160; }\n            else if (side === 2) { x = (Math.random() - 0.5) * 160; z = -95; }\n            else { x = (Math.random() - 0.5) * 160; z = 95; }\n            \n            group.position.set(x, 0, z);\n            scene.add(group);\n            vehicles.push({\n                group,\n                dirX: 0, dirZ: 0,\n                speed: VEHICLE_SPEED * speedMult,\n                alive: true,\n                fleeing: false,\n                settled: false,\n                vType,\n                lightTimer: 0\n            });\n        }\n\n        function spawnDestroyEffect(position, isVehicle) {\n            const numParts = isVehicle ? 12 : 8;\n            const color = isVehicle ? 0x888888 : 0xcc9966;\n            for (let i = 0; i < numParts; i++) {\n                const size = isVehicle ? (0.3 + Math.random() * 0.5) : (0.15 + Math.random() * 0.3);\n                const geo = new THREE.BoxGeometry(size, size, size);\n                const mat = new THREE.MeshStandardMaterial({ \n                    color: new THREE.Color(color).offsetHSL(Math.random() * 0.2 - 0.1, 0, Math.random() * 0.3 - 0.15)\n                });\n                const piece = new THREE.Mesh(geo, mat);\n                piece.position.copy(position);\n                piece.position.y += isVehicle ? 0.8 : 1.2;\n                const vel = new THREE.Vector3(\n                    (Math.random() - 0.5) * 10,\n                    Math.random() * 8 + 3,\n                    (Math.random() - 0.5) * 10\n                );\n                scene.add(piece);\n                buildingDamages.push({ type: 'debris', mesh: piece, vel, life: 1.5 + Math.random() * 0.8 });\n            }\n            // Dust cloud\n            for (let i = 0; i < 5; i++) {\n                const dGeo = new THREE.SphereGeometry(0.4 + Math.random() * 0.6, 4, 4);\n                const dMat = new THREE.MeshBasicMaterial({ color: 0xbbbbbb, transparent: true, opacity: 0.7 });\n                const dust = new THREE.Mesh(dGeo, dMat);\n                dust.position.copy(position);\n                dust.position.y += 0.5;\n                const vel = new THREE.Vector3((Math.random() - 0.5) * 4, Math.random() * 3, (Math.random() - 0.5) * 4);\n                scene.add(dust);\n                buildingDamages.push({ type: 'dust', mesh: dust, vel, life: 1.0 + Math.random() * 0.5 });\n            }\n        }\n\n        function removePedestrian(index) {\n            scene.remove(pedestrians[index].group);\n            pedestrians.splice(index, 1);\n        }\n\n        function removeVehicle(index) {\n            scene.remove(vehicles[index].group);\n            vehicles.splice(index, 1);\n        }\n\n        function updatePedestrians(dt) {\n            for (let i = pedestrians.length - 1; i >= 0; i--) {\n                const p = pedestrians[i];\n                if (!p.alive) continue;\n\n                // If settled, never move again (even if monster approaches)\n                if (p.settled) {\n                    // Still check destruction\n                    for (let m = 0; m < monsters.length; m++) {\n                        if (!monsters[m].alive) continue;\n                        const dx = p.group.position.x - monsters[m].group.position.x;\n                        const dz = p.group.position.z - monsters[m].group.position.z;\n                        const dist = Math.sqrt(dx * dx + dz * dz);\n                        if (dist < DESTROY_RANGE) {\n                            p.alive = false;\n                            spawnDestroyEffect(p.group.position, false);\n                            civiliansLost++;\n                            score = Math.max(0, score - 20);\n                            showKillFeed(`Civilian hit! -20 (Lost:${civiliansLost}/${maxCiviliansLost})`);\n                            updateHUD();\n                            removePedestrian(i);\n                            break;\n                        }\n                    }\n                    continue;\n                }\n\n                // Check for nearby monsters - destruction only (no fleeing)\n                for (let m = 0; m < monsters.length; m++) {\n                    if (!monsters[m].alive) continue;\n                    const dx = p.group.position.x - monsters[m].group.position.x;\n                    const dz = p.group.position.z - monsters[m].group.position.z;\n                    const dist = Math.sqrt(dx * dx + dz * dz);\n                    if (dist < DESTROY_RANGE) {\n                        p.alive = false;\n                        spawnDestroyEffect(p.group.position, false);\n                        civiliansLost++;\n                        score = Math.max(0, score - 20);\n                        showKillFeed(`Civilian hit! -20 (Lost:${civiliansLost}/${maxCiviliansLost})`);\n                        updateHUD();\n                        removePedestrian(i);\n                        break;\n                    }\n                }\n                if (!p.alive) continue;\n\n                // Always head toward evacuation zone\n                p.fleeing = false;\n                const toEvacX = EVAC_ZONE_POS.x - p.group.position.x;\n                const toEvacZ = EVAC_ZONE_POS.z - p.group.position.z;\n                const evacDist = Math.sqrt(toEvacX * toEvacX + toEvacZ * toEvacZ);\n                if (evacDist > EVAC_ZONE_RADIUS * 0.6) {\n                    p.dirX = toEvacX / evacDist;\n                    p.dirZ = toEvacZ / evacDist;\n                } else {\n                    // Inside zone - find non-overlapping spot and settle\n                    let canSettle = true;\n                    for (let pi = 0; pi < pedestrians.length; pi++) {\n                        if (pi === i || !pedestrians[pi].alive) continue;\n                        const ox = p.group.position.x - pedestrians[pi].group.position.x;\n                        const oz = p.group.position.z - pedestrians[pi].group.position.z;\n                        if (Math.sqrt(ox*ox + oz*oz) < 1.8) { canSettle = false; break; }\n                    }\n                    if (canSettle) {\n                        for (let vi = 0; vi < vehicles.length; vi++) {\n                            if (!vehicles[vi].alive) continue;\n                            const ox = p.group.position.x - vehicles[vi].group.position.x;\n                            const oz = p.group.position.z - vehicles[vi].group.position.z;\n                            if (Math.sqrt(ox*ox + oz*oz) < 3.5) { canSettle = false; break; }\n                        }\n                    }\n                    if (canSettle) {\n                        p.settled = true;\n                        p.dirX = 0;\n                        p.dirZ = 0;\n                        continue;\n                    } else {\n                        // Move to a slightly offset position\n                        const offsetAngle = Math.random() * Math.PI * 2;\n                        p.dirX = Math.sin(offsetAngle) * 0.5 + toEvacX / evacDist * 0.5;\n                        p.dirZ = Math.cos(offsetAngle) * 0.5 + toEvacZ / evacDist * 0.5;\n                        const len = Math.sqrt(p.dirX*p.dirX + p.dirZ*p.dirZ);\n                        if (len > 0) { p.dirX /= len; p.dirZ /= len; }\n                    }\n                }\n\n                // Move\n                const currentSpeed = p.fleeing ? p.speed * 2.5 : p.speed;\n                let newX = p.group.position.x + p.dirX * currentSpeed * dt;\n                let newZ = p.group.position.z + p.dirZ * currentSpeed * dt;\n\n                // Building collision for pedestrians\n                const pedRadius = 0.6;\n                for (let b = 0; b < buildings.length; b++) {\n                    const bld = buildings[b];\n                    const bPos = bld.position;\n                    const bGeo = bld.geometry.parameters;\n                    const halfW = bGeo.width / 2 + pedRadius;\n                    const halfD = bGeo.depth / 2 + pedRadius;\n                    if (newX > bPos.x - halfW && newX < bPos.x + halfW &&\n                        newZ > bPos.z - halfD && newZ < bPos.z + halfD) {\n                        const dx = newX - bPos.x;\n                        const dz = newZ - bPos.z;\n                        const overlapX = halfW - Math.abs(dx);\n                        const overlapZ = halfD - Math.abs(dz);\n                        if (overlapX < overlapZ) {\n                            newX = bPos.x + Math.sign(dx) * halfW;\n                            p.dirX = Math.sign(dx) * 0.5 + (Math.random() - 0.5) * 0.5;\n                        } else {\n                            newZ = bPos.z + Math.sign(dz) * halfD;\n                            p.dirZ = Math.sign(dz) * 0.5 + (Math.random() - 0.5) * 0.5;\n                        }\n                        const len = Math.sqrt(p.dirX * p.dirX + p.dirZ * p.dirZ);\n                        if (len > 0) { p.dirX /= len; p.dirZ /= len; }\n                    }\n                }\n\n                // Collision with vehicles - push apart\n                for (let vi = 0; vi < vehicles.length; vi++) {\n                    const v = vehicles[vi];\n                    if (!v.alive) continue;\n                    const dx = newX - v.group.position.x;\n                    const dz = newZ - v.group.position.z;\n                    const dist = Math.sqrt(dx * dx + dz * dz);\n                    const minDist = 3.5;\n                    if (dist < minDist && dist > 0) {\n                        // Push pedestrian away from vehicle\n                        const pushX = dx / dist;\n                        const pushZ = dz / dist;\n                        newX += pushX * (minDist - dist) * 0.8;\n                        newZ += pushZ * (minDist - dist) * 0.8;\n                        p.dirX = pushX;\n                        p.dirZ = pushZ;\n                    }\n                }\n\n                // Collision with other pedestrians - push apart\n                for (let pi = 0; pi < pedestrians.length; pi++) {\n                    if (pi === i || !pedestrians[pi].alive) continue;\n                    const op = pedestrians[pi];\n                    const dx = newX - op.group.position.x;\n                    const dz = newZ - op.group.position.z;\n                    const dist = Math.sqrt(dx * dx + dz * dz);\n                    if (dist < 1.5 && dist > 0) {\n                        // Push both apart\n                        const pushX = dx / dist;\n                        const pushZ = dz / dist;\n                        const overlap = (1.5 - dist) * 0.5;\n                        newX += pushX * overlap;\n                        newZ += pushZ * overlap;\n                        op.group.position.x -= pushX * overlap;\n                        op.group.position.z -= pushZ * overlap;\n                    }\n                }\n\n                // Safety net: check if actually stuck in a building (e.g. from spawning)\n                if (!p.settled && isInsideBuilding(newX, newZ, 0)) {\n                    // Force them to walk towards center to escape\n                    const cx = -newX;\n                    const cz = -newZ;\n                    const clen = Math.sqrt(cx*cx + cz*cz);\n                    if (clen > 0) {\n                        p.dirX = cx / clen;\n                        p.dirZ = cz / clen;\n                    }\n                    newX += p.dirX * currentSpeed * dt * 2; // Extra push\n                    newZ += p.dirZ * currentSpeed * dt * 2;\n                }\n\n                p.group.position.x = newX;\n                p.group.position.z = newZ;\n\n                // Boundary - remove them if they run to the edge (they escaped)\n                if (Math.abs(p.group.position.x) > 95 || Math.abs(p.group.position.z) > 95) {\n                    removePedestrian(i);\n                    continue;\n                }\n\n                // Face direction\n                p.group.rotation.y = Math.atan2(p.dirX, p.dirZ);\n\n                // Walk animation\n                const animSpeed = p.fleeing ? 12 : 5;\n                p.walkCycle += dt * animSpeed;\n                const legSwing = Math.sin(p.walkCycle) * (p.fleeing ? 0.8 : 0.4);\n                const armSwing = Math.sin(p.walkCycle) * (p.fleeing ? 0.6 : 0.3);\n                if (p.group.userData.legL) p.group.userData.legL.rotation.x = legSwing;\n                if (p.group.userData.legR) p.group.userData.legR.rotation.x = -legSwing;\n                if (p.group.userData.armL) p.group.userData.armL.rotation.x = -armSwing;\n                if (p.group.userData.armR) p.group.userData.armR.rotation.x = armSwing;\n            }\n        }\n\n        function updateVehicles(dt) {\n            for (let i = vehicles.length - 1; i >= 0; i--) {\n                const v = vehicles[i];\n                if (!v.alive) continue;\n\n                // If settled, never move again (even if monster approaches)\n                if (v.settled) {\n                    // Still check destruction\n                    for (let m = 0; m < monsters.length; m++) {\n                        if (!monsters[m].alive) continue;\n                        const dx = v.group.position.x - monsters[m].group.position.x;\n                        const dz = v.group.position.z - monsters[m].group.position.z;\n                        const dist = Math.sqrt(dx * dx + dz * dz);\n                        if (dist < DESTROY_RANGE + 2) {\n                            v.alive = false;\n                            spawnDestroyEffect(v.group.position, true);\n                            civiliansLost++;\n                            score = Math.max(0, score - 30);\n                            showKillFeed(`Vehicle destroyed! -30 (Lost:${civiliansLost}/${maxCiviliansLost})`);\n                            updateHUD();\n                            removeVehicle(i);\n                            break;\n                        }\n                    }\n                    // Police lights still flash\n                    if (v.alive && v.vType === 'police') {\n                        v.lightTimer = (v.lightTimer || 0) + dt;\n                        const redL = v.group.userData.policeRedLight;\n                        const blueL = v.group.userData.policeBlueLight;\n                        if (redL && blueL) {\n                            const flash = Math.floor(v.lightTimer * 4) % 2 === 0;\n                            redL.material.color.setHex(flash ? 0xff0000 : 0x440000);\n                            blueL.material.color.setHex(flash ? 0x110044 : 0x0044ff);\n                        }\n                    }\n                    continue;\n                }\n\n                // Check for nearby monsters - destruction only (no fleeing)\n                for (let m = 0; m < monsters.length; m++) {\n                    if (!monsters[m].alive) continue;\n                    const dx = v.group.position.x - monsters[m].group.position.x;\n                    const dz = v.group.position.z - monsters[m].group.position.z;\n                    const dist = Math.sqrt(dx * dx + dz * dz);\n                    if (dist < DESTROY_RANGE + 2) {\n                        v.alive = false;\n                        spawnDestroyEffect(v.group.position, true);\n                        civiliansLost++;\n                        score = Math.max(0, score - 30);\n                        showKillFeed(`Vehicle destroyed! -30 (Lost:${civiliansLost}/${maxCiviliansLost})`);\n                        updateHUD();\n                        removeVehicle(i);\n                        break;\n                    }\n                }\n                if (!v.alive) continue;\n\n                // Always head toward evacuation zone\n                v.fleeing = false;\n                const toEvacX = EVAC_ZONE_POS.x - v.group.position.x;\n                const toEvacZ = EVAC_ZONE_POS.z - v.group.position.z;\n                const evacDist = Math.sqrt(toEvacX * toEvacX + toEvacZ * toEvacZ);\n                if (evacDist > EVAC_ZONE_RADIUS * 0.7) {\n                    const targetDirX = toEvacX / evacDist;\n                    const targetDirZ = toEvacZ / evacDist;\n                    v.dirX += (targetDirX - v.dirX) * dt * 1.5;\n                    v.dirZ += (targetDirZ - v.dirZ) * dt * 1.5;\n                    const nLen = Math.sqrt(v.dirX * v.dirX + v.dirZ * v.dirZ);\n                    if (nLen > 0) { v.dirX /= nLen; v.dirZ /= nLen; }\n                } else {\n                    // Inside zone - find non-overlapping spot and settle\n                    let canSettle = true;\n                    for (let vi = 0; vi < vehicles.length; vi++) {\n                        if (vi === i || !vehicles[vi].alive) continue;\n                        const ox = v.group.position.x - vehicles[vi].group.position.x;\n                        const oz = v.group.position.z - vehicles[vi].group.position.z;\n                        if (Math.sqrt(ox*ox + oz*oz) < 6) { canSettle = false; break; }\n                    }\n                    if (canSettle) {\n                        for (let pi = 0; pi < pedestrians.length; pi++) {\n                            if (!pedestrians[pi].alive) continue;\n                            const ox = v.group.position.x - pedestrians[pi].group.position.x;\n                            const oz = v.group.position.z - pedestrians[pi].group.position.z;\n                            if (Math.sqrt(ox*ox + oz*oz) < 3.5) { canSettle = false; break; }\n                        }\n                    }\n                    if (canSettle) {\n                        v.settled = true;\n                        v.dirX = 0;\n                        v.dirZ = 0;\n                        continue;\n                    } else {\n                        // Move to a slightly offset position\n                        const offsetAngle = Math.random() * Math.PI * 2;\n                        v.dirX = Math.sin(offsetAngle) * 0.5 + toEvacX / evacDist * 0.5;\n                        v.dirZ = Math.cos(offsetAngle) * 0.5 + toEvacZ / evacDist * 0.5;\n                        const nLen = Math.sqrt(v.dirX*v.dirX + v.dirZ*v.dirZ);\n                        if (nLen > 0) { v.dirX /= nLen; v.dirZ /= nLen; }\n                    }\n                }\n\n                // Move\n                const currentSpeed = v.fleeing ? v.speed * 1.8 : v.speed;\n                let newX = v.group.position.x + v.dirX * currentSpeed * dt;\n                let newZ = v.group.position.z + v.dirZ * currentSpeed * dt;\n\n                // Building collision for vehicles\n                const vehRadius = 2.5;\n                for (let b = 0; b < buildings.length; b++) {\n                    const bld = buildings[b];\n                    const bPos = bld.position;\n                    const bGeo = bld.geometry.parameters;\n                    const halfW = bGeo.width / 2 + vehRadius;\n                    const halfD = bGeo.depth / 2 + vehRadius;\n                    if (newX > bPos.x - halfW && newX < bPos.x + halfW &&\n                        newZ > bPos.z - halfD && newZ < bPos.z + halfD) {\n                        const dx = newX - bPos.x;\n                        const dz = newZ - bPos.z;\n                        const overlapX = halfW - Math.abs(dx);\n                        const overlapZ = halfD - Math.abs(dz);\n                        if (overlapX < overlapZ) {\n                            newX = bPos.x + Math.sign(dx) * halfW;\n                            v.dirX = Math.sign(dx) * 0.8 + (Math.random() - 0.5) * 0.2;\n                        } else {\n                            newZ = bPos.z + Math.sign(dz) * halfD;\n                            v.dirZ = Math.sign(dz) * 0.8 + (Math.random() - 0.5) * 0.2;\n                        }\n                        const nLen = Math.sqrt(v.dirX * v.dirX + v.dirZ * v.dirZ);\n                        if (nLen > 0) { v.dirX /= nLen; v.dirZ /= nLen; }\n                    }\n                }\n\n                // Collision with other vehicles - push apart\n                for (let vi = 0; vi < vehicles.length; vi++) {\n                    if (vi === i || !vehicles[vi].alive) continue;\n                    const ov = vehicles[vi];\n                    const dx = newX - ov.group.position.x;\n                    const dz = newZ - ov.group.position.z;\n                    const dist = Math.sqrt(dx * dx + dz * dz);\n                    if (dist < 5.5 && dist > 0) {\n                        // Push both vehicles apart\n                        const pushX = dx / dist;\n                        const pushZ = dz / dist;\n                        const overlap = (5.5 - dist) * 0.5;\n                        newX += pushX * overlap;\n                        newZ += pushZ * overlap;\n                        ov.group.position.x -= pushX * overlap;\n                        ov.group.position.z -= pushZ * overlap;\n                        // Steer away\n                        v.dirX = v.dirX * 0.7 + pushX * 0.3;\n                        v.dirZ = v.dirZ * 0.7 + pushZ * 0.3;\n                        const nLen = Math.sqrt(v.dirX*v.dirX + v.dirZ*v.dirZ);\n                        if (nLen > 0) { v.dirX /= nLen; v.dirZ /= nLen; }\n                    }\n                }\n\n                // Collision with pedestrians - push pedestrian aside\n                for (let pi = 0; pi < pedestrians.length; pi++) {\n                    if (!pedestrians[pi].alive) continue;\n                    const pp = pedestrians[pi];\n                    const dx = newX - pp.group.position.x;\n                    const dz = newZ - pp.group.position.z;\n                    const dist = Math.sqrt(dx * dx + dz * dz);\n                    if (dist < 3.5 && dist > 0) {\n                        // Push pedestrian out of the way\n                        const pushX = -dx / dist;\n                        const pushZ = -dz / dist;\n                        pp.group.position.x += pushX * (3.5 - dist);\n                        pp.group.position.z += pushZ * (3.5 - dist);\n                        pp.dirX = pushX;\n                        pp.dirZ = pushZ;\n                    }\n                }\n\n                // Safety net: check if actually stuck in a building\n                if (!v.settled && isInsideBuilding(newX, newZ, 0)) {\n                    // Force them to drive towards center to escape\n                    const cx = -newX;\n                    const cz = -newZ;\n                    const clen = Math.sqrt(cx*cx + cz*cz);\n                    if (clen > 0) {\n                        v.dirX = cx / clen;\n                        v.dirZ = cz / clen;\n                    }\n                    newX += v.dirX * currentSpeed * dt * 2;\n                    newZ += v.dirZ * currentSpeed * dt * 2;\n                }\n\n                v.group.position.x = newX;\n                v.group.position.z = newZ;\n\n                // Police light flash\n                if (v.vType === 'police') {\n                    v.lightTimer = (v.lightTimer || 0) + dt;\n                    const redL = v.group.userData.policeRedLight;\n                    const blueL = v.group.userData.policeBlueLight;\n                    if (redL && blueL) {\n                        const flash = Math.floor(v.lightTimer * 4) % 2 === 0;\n                        redL.material.color.setHex(flash ? 0xff0000 : 0x440000);\n                        blueL.material.color.setHex(flash ? 0x110044 : 0x0044ff);\n                    }\n                }\n\n                // Face direction\n                v.group.rotation.y = Math.atan2(v.dirX, v.dirZ);\n\n                // Boundary - remove them if they run to the edge (they escaped)\n                if (Math.abs(v.group.position.x) > 95 || Math.abs(v.group.position.z) > 95) {\n                    removeVehicle(i);\n                    continue;\n                }\n            }\n        }\n\n        // --- Game State ---\n        // --- Upgrade System ---\n        let totalScore = 0;\n        let upgradePoints = 49000; // Starting bonus points as requested\n        let fireRateLevel = 0;\n        let damageLevel = 0;\n        let ammoCapacityLevel = 0;\n        let explosiveBulletLevel = 0;\n        let giantBulletLevel = 0;\n        let homingMissileLevel = 1; // Start with missiles as requested\n\n        const UPGRADE_MAX_LEVEL = 10;\n        function getFireRateCost() { return Math.floor(500 * Math.pow(1.5, fireRateLevel)); }\n        function getDamageCost() { return Math.floor(500 * Math.pow(1.5, damageLevel)); }\n        function getAmmoCost() { return Math.floor(300 * Math.pow(1.5, ammoCapacityLevel)); }\n        function getShootCooldown() { return Math.max(0.03, 0.08 - fireRateLevel * 0.005); }\n        function getBulletDamage() { return 1 + damageLevel * 0.5; }\n        function getHeadshotDamage() { return getBulletDamage() * (2 + giantBulletLevel * 0.5); }\n        function getMaxAmmo() { return config.settings.ammoCapacity + ammoCapacityLevel * 50; }\n        function getExplosiveCost() { return Math.floor(600 * Math.pow(1.5, explosiveBulletLevel)); }\n        function getGiantCost() { return Math.floor(700 * Math.pow(1.5, giantBulletLevel)); }\n        function getHomingCost() { return Math.floor(800 * Math.pow(1.5, homingMissileLevel)); }\n\n        function getExplosiveChance() { return explosiveBulletLevel * 0.12; }\n        function getGiantDamageBonus() { return giantBulletLevel * 0.5; }\n        function getHomingInterval() { return Math.max(1.5, 5.0 - homingMissileLevel * 0.5); }\n        function getHomingDamage() { return 5 + homingMissileLevel * 3; }\n\n        let gameState = 'menu'; // menu, playing, paused, gameover, victory, upgrading\n        let playerHealth = config.settings.playerHealth;\n        let ammo = getMaxAmmo();\n        let maxAmmo = getMaxAmmo();\n        let score = 0;\n        let currentWave = 0;\n        let totalWaves = config.settings.waves;\n        let monstersToSpawn = 0;\n        let monstersKilledThisWave = 0;\n        let totalMonstersThisWave = 0;\n        let spawnTimer = 0;\n        let isScoped = false;\n        let shootCooldown = 0;\n        let reloading = false;\n        let reloadTimer = 0;\n        let heliRotorAngle = 0;\n        let combo = 0;\n        let comboTimer = 0;\n        let waveTransitioning = false;\n        let yaw = 0;\n        let pitch = -0.2;\n        const keysDown = {};\n        let recoilPitch = 0;\n        let damageFlashTimer = 0;\n        let waveAnnouncerTimer = 0;\n        let noEnemyInViewTimer = 0;\n        let bgMusic = null;\n        let menuMusic = null;\n        let lastTime = 0;\n        let homingMissileTimer = 0;\n        const homingMissiles = [];\n\n        // --- Helicopter Flight Controls ---\n        let heliForwardSpeed = 0;\n        let heliStrafeSpeed = 0;\n        let heliAltitude = 55;\n        let heliAltSpeed = 0;\n        const HELI_MAX_SPEED = 45;\n        const HELI_ACCELERATION = 50;\n        const HELI_BRAKE = 35;\n        const HELI_ALT_MAX_SPEED = 20;\n        const HELI_MIN_ALT = 20;\n        const HELI_MAX_ALT = 120;\n        const HELI_MAP_BOUNDS = 180;\n        let joystickInputX = 0;\n        let joystickInputY = 0;\n        let altInputUp = false;\n        let altInputDown = false;\n\n        // --- Support Sniper System ---\n        const SUPPORT_INTERVAL = 120; // 2 minutes\n        let supportTimer = SUPPORT_INTERVAL;\n        const sniperAllies = [];\n        const SNIPER_ATTACK_POWER = 10;\n        const SNIPER_FIRE_INTERVAL = 20.0; // seconds between shots\n        const SNIPER_RANGE = Infinity;\n\n\n        // --- Evacuation Timer System ---\n        const EVAC_TOTAL_TIME = config.settings.evacTime || 180;\n        let evacTimer = EVAC_TOTAL_TIME;\n        let civiliansLost = 0;\n        let maxCiviliansLost = config.settings.maxCiviliansLost || 15;\n        let monsterSpawnInterval = 5.0;\n        let globalSpawnTimer = 0;\n        let evacWave = 1;\n        let currentLevel = 1;\n        let monstersToKillThisLevel = 0;\n        let monstersKilledThisLevel = 0;\n        let levelComplete = false;\n        let subWave = 0;\n        let totalSubWaves = 3;\n        let monstersThisSubWave = 0;\n        let monstersKilledThisSubWave = 0;\n        let monstersSpawnedThisSubWave = 0;\n        let subWaveTransitioning = false;\n        let subWaveTimer = 0;\n        let subWaveElapsed = 0; // time since sub-wave started\n        let prepPhaseActive = false;\n        let prepPhaseTimer = 0;\n        const PREP_PHASE_DURATION = 3.0; // 3 seconds preparation before monsters spawn\n\n        // --- UI References ---\n        const crosshairEl = document.getElementById('crosshair');\n        const scopeOverlay = document.getElementById('scope-overlay');\n        const healthFill = document.getElementById('health-fill');\n        const ammoValue = document.getElementById('ammo-value');\n        const scoreValue = document.getElementById('score-value');\n        const waveValue = document.getElementById('wave-value');\n        const startScreen = document.getElementById('start-screen');\n        const gameoverScreen = document.getElementById('gameover-screen');\n        const victoryScreen = document.getElementById('victory-screen');\n        const waveAnnounce = document.getElementById('wave-announce');\n        const damageFlash = document.getElementById('damage-flash');\n        const muzzleFlash = document.getElementById('muzzle-flash');\n        const killFeed = document.getElementById('kill-feed');\n        const editInfo = document.getElementById('edit-info');\n        const timerValue = document.getElementById('timer-value');\n        const civiliansValue = document.getElementById('civilians-value');\n        const supportValue = document.getElementById('support-value');\n\n        function updateSupportHUD() {\n            const mins = Math.floor(supportTimer / 60);\n            const secs = Math.floor(supportTimer % 60);\n            const timeStr = `${mins}:${secs.toString().padStart(2, '0')}`;\n            supportValue.textContent = `${timeStr} (${sniperAllies.length}\ud83e\ude96)`;\n            supportValue.classList.toggle('imminent', supportTimer <= 10);\n        }\n\n        // Tech crosshair is CSS-based, no image loading needed\n        // Muzzle flash is now a bright screen flash effect (no image needed)\n\n        // --- Touch detection ---\n        const isMobile = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);\n\n        // --- Show Game Parameters ---\n        lib.showGameParameters({\n            name: 'Urban Hunter Settings',\n            params: {\n                'Evacuation Time (s)': { key: 'settings.evacTime', type: 'slider', min: 120, max: 600, step: 30 },\n                'Max Casualties': { key: 'settings.maxCiviliansLost', type: 'slider', min: 5, max: 30, step: 1 },\n                'Monster Speed': { key: 'settings.monsterSpeed', type: 'slider', min: 0.5, max: 2.0, step: 0.1 },\n                'Player Health': { key: 'settings.playerHealth', type: 'slider', min: 50, max: 200, step: 10 },\n                'Starting Ammo': { key: 'settings.ammoCapacity', type: 'slider', min: 50, max: 500, step: 50 },\n                'City Light Color': { key: 'settings.cityLightColor', type: 'color' },\n                'Fog Density': { key: 'settings.fogDensity', type: 'slider', min: 0, max: 0.01, step: 0.001 },\n                'Monster Color': { key: 'settings.monsterColor', type: 'color' },\n                'SFX Volume': { key: 'settings.sfxVolume', type: 'slider', min: 0, max: 1, step: 0.1 },\n                'Music Volume': { key: 'settings.musicVolume', type: 'slider', min: 0, max: 1, step: 0.1 }\n            }\n        });\n\n        // --- Mode Setup ---\n        if (mode === 'edit') {\n            // Edit mode - show scene, no gameplay\n            document.querySelectorAll('.hud-panel, .mobile-btn, .game-screen, #crosshair, #tech-crosshair').forEach(el => el.style.display = 'none');\n            editInfo.style.display = 'block';\n\n            // Slowly rotate camera for preview\n            let editAngle = 0;\n            function editLoop(timestamp) {\n                const dt = Math.min((timestamp - (lastTime || timestamp)) / 1000, 0.11);\n                lastTime = timestamp;\n                editAngle += dt * 0.2;\n\n                // Update from config\n                scene.fog.density = config.settings.fogDensity;\n                scene.fog.color.set(0x0a0a1a);\n\n                camera.position.set(Math.sin(editAngle) * 80, 70, Math.cos(editAngle) * 80);\n                camera.lookAt(0, 50, 0);\n\n                // Animate rotors in edit mode\n                rotorGroup.rotation.y += dt * 25;\n                tailRotorGroup.rotation.x += dt * 37;\n\n                renderer.render(scene, camera);\n                requestAnimationFrame(editLoop);\n            }\n            requestAnimationFrame(editLoop);\n            return;\n        }\n\n        // --- PLAY MODE ---\n        // Show appropriate UI\n        editInfo.style.display = 'none';\n\n\n        // --- Input Handling ---\n        let pointerLocked = false;\n\n        function requestPointerLock() {\n            if (document.pointerLockElement) return;\n            try {\n                const element = document.body || canvas;\n                if (element.requestPointerLock) {\n                    const promise = element.requestPointerLock();\n                    if (promise && typeof promise.catch === 'function') {\n                        promise.catch(e => console.warn('Pointer lock suppressed:', e.message));\n                    }\n                }\n            } catch(e) {\n                console.warn('Pointer lock error:', e.message);\n            }\n        }\n\n        document.addEventListener('pointerlockchange', () => {\n            pointerLocked = !!document.pointerLockElement;\n        });\n\n        // Mouse movement to control crosshair (works with or without pointer lock)\n        let lastMouseX = null, lastMouseY = null;\n        let targetYaw = 0;\n        let targetPitch = 0;\n\n        document.addEventListener('mousemove', (e) => {\n            if (gameState !== 'playing') return;\n            const sensitivity = isScoped ? 0.0008 : 0.004;\n            if (pointerLocked) {\n                targetYaw -= e.movementX * sensitivity;\n                targetPitch -= e.movementY * sensitivity;\n            } else {\n                if (lastMouseX !== null) {\n                    targetYaw -= (e.clientX - lastMouseX) * sensitivity;\n                    targetPitch -= (e.clientY - lastMouseY) * sensitivity;\n                }\n                lastMouseX = e.clientX;\n                lastMouseY = e.clientY;\n            }\n            targetPitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, targetPitch));\n        });\n\n        document.addEventListener('mouseleave', () => { lastMouseX = null; lastMouseY = null; });\n\n        // Left click to shoot (hold for continuous fire)\n        let mouseHeld = false;\n        document.addEventListener('mousedown', (e) => {\n            if (e.target.closest && (e.target.closest('button') || e.target.closest('.mobile-btn'))) return;\n            if (e.button === 0) {\n                mouseHeld = true;\n                if (gameState === 'playing') {\n                    shoot();\n                    requestPointerLock();\n                } else {\n                    requestPointerLock();\n                }\n            }\n        });\n        document.addEventListener('mouseup', (e) => {\n            if (e.button === 0) mouseHeld = false;\n        });\n\n        // Right click for scope\n        document.addEventListener('contextmenu', (e) => {\n            if (e.target.closest && (e.target.closest('button') || e.target.closest('.mobile-btn'))) return;\n            e.preventDefault();\n            if (gameState === 'playing') {\n                toggleScope();\n            }\n        });\n\n        // Keyboard\n        document.addEventListener('keydown', (e) => {\n            keysDown[e.key] = true;\n            if (e.key === 'r' || e.key === 'R') {\n                if (gameState === 'playing') reload();\n            }\n            if (e.key === 'f' || e.key === 'F' || e.key === ' ') {\n                e.preventDefault();\n                if (gameState === 'playing' && homingMissileLevel > 0 && homingMissileTimer <= 0) {\n                    spawnHomingMissile();\n                    homingMissileTimer = getHomingInterval();\n                }\n            }\n            if (e.key === 'Escape') {\n                if (isScoped) toggleScope();\n            }\n        });\n        let spaceHeld = false;\n        document.addEventListener('keyup', (e) => {\n            keysDown[e.key] = false;\n            if (e.key === ' ') spaceHeld = false;\n        });\n        // Pinch-to-zoom (two-finger gesture)\n        let pinchStartDist = 0;\n        let pinchFov = 75;\n        const MIN_FOV = 20;\n        const MAX_FOV = 75;\n\n        function getPinchDistance(touches) {\n            const dx = touches[0].clientX - touches[1].clientX;\n            const dy = touches[0].clientY - touches[1].clientY;\n            return Math.sqrt(dx * dx + dy * dy);\n        }\n\n        document.addEventListener('touchstart', (e) => {\n            if (e.touches.length === 2) {\n                // Pinch disabled - fixed FOV\n                stopTouchShooting();\n            }\n        }, { passive: false });\n\n        // Touch controls - touch to shoot + drag to aim (sweep fire)\n        let touchStartX = 0, touchStartY = 0;\n        let touchShootInterval = null;\n        let touchShooting = false;\n        let swipeSpeed = 0;\n        const SWIPE_THRESHOLD = 12; // pixels per frame - above this, stop firing\n\n        // Fire button touch handlers\n        const btnFire = document.getElementById('btn-fire');\n        if (btnFire) {\n            btnFire.addEventListener('touchstart', (e) => {\n                e.preventDefault();\n                e.stopPropagation();\n                if (gameState === 'playing') {\n                    shoot();\n                    touchShooting = true;\n                    if (touchShootInterval) clearInterval(touchShootInterval);\n                    touchShootInterval = setInterval(() => {\n                        if (gameState === 'playing') shoot();\n                    }, 100);\n                }\n            }, { passive: false });\n            \n            const stopFire = (e) => {\n                if (e) { e.preventDefault(); e.stopPropagation(); }\n                touchShooting = false;\n                if (touchShootInterval) {\n                    clearInterval(touchShootInterval);\n                    touchShootInterval = null;\n                }\n            };\n            \n            btnFire.addEventListener('touchend', stopFire);\n            btnFire.addEventListener('touchcancel', stopFire);\n            btnFire.addEventListener('mousedown', (e) => {\n                e.stopPropagation();\n                if (gameState === 'playing') {\n                    shoot();\n                    touchShootInterval = setInterval(shoot, 100);\n                }\n            });\n            window.addEventListener('mouseup', () => {\n                if (touchShootInterval) {\n                    clearInterval(touchShootInterval);\n                    touchShootInterval = null;\n                }\n            });\n        }\n\n        // Missile button touch handler\n        const btnMissile = document.getElementById('btn-missile');\n        if (btnMissile) {\n            const launchMissile = (e) => {\n                e.preventDefault();\n                e.stopPropagation();\n                if (gameState === 'playing' && homingMissileLevel > 0 && homingMissileTimer <= 0) {\n                    spawnHomingMissile();\n                    homingMissileTimer = getHomingInterval();\n                }\n            };\n            btnMissile.addEventListener('touchstart', launchMissile, { passive: false });\n            btnMissile.addEventListener('mousedown', launchMissile);\n        }\n\n        // Reload button touch handler\n        const btnReload = document.getElementById('btn-reload');\n        if (btnReload) {\n            const triggerReload = (e) => {\n                e.preventDefault();\n                e.stopPropagation();\n                if (gameState === 'playing') reload();\n            };\n            btnReload.addEventListener('touchstart', triggerReload, { passive: false });\n            btnReload.addEventListener('mousedown', triggerReload);\n        }\n\n        function startTouchShooting() {\n            // No-op, we use the fire button now\n        }\n\n        function stopTouchShooting() {\n            // No-op, we use the fire button now\n        }\n\n        if (isMobile) {\n            document.addEventListener('touchstart', (e) => {\n                if (e.target.closest && (e.target.closest('button') || e.target.closest('.mobile-btn') || e.target.closest('.hud-panel') || e.target.closest('#virtual-joystick') || e.target.closest('#btn-alt-up') || e.target.closest('#btn-alt-down'))) return;\n                if (e.touches.length >= 2) { return; }\n                const touch = e.touches[0];\n                touchStartX = touch.clientX;\n                touchStartY = touch.clientY;\n            });\n\n            document.addEventListener('touchmove', (e) => {\n                if (e.target.closest && (e.target.closest('button') || e.target.closest('.mobile-btn') || e.target.closest('.hud-panel') || e.target.closest('#virtual-joystick') || e.target.closest('#btn-alt-up') || e.target.closest('#btn-alt-down'))) return;\n                if (e.touches.length >= 2) return; // pinch handled separately\n                e.preventDefault();\n                if (gameState !== 'playing') return;\n                const touch = e.touches[0];\n                const dx = touch.clientX - touchStartX;\n                const dy = touch.clientY - touchStartY;\n                touchStartX = touch.clientX;\n                touchStartY = touch.clientY;\n\n                // Track swipe speed for smart safety\n                swipeSpeed = Math.sqrt(dx * dx + dy * dy);\n\n                const sensitivity = isScoped ? 0.0012 : 0.006;\n                targetYaw -= dx * sensitivity;\n                targetPitch -= dy * sensitivity;\n                targetPitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, targetPitch));\n            }, { passive: false });\n\n            document.addEventListener('touchend', (e) => {\n                if (e.target.closest && (e.target.closest('button') || e.target.closest('.mobile-btn') || e.target.closest('.hud-panel') || e.target.closest('#virtual-joystick') || e.target.closest('#btn-alt-up') || e.target.closest('#btn-alt-down'))) return;\n            });\n\n            document.addEventListener('touchcancel', () => { stopTouchShooting(); });\n        }\n\n        // --- Virtual Joystick Handling ---\n        const joystickEl = document.getElementById('virtual-joystick');\n        const joystickKnob = document.getElementById('joystick-knob');\n        const joystickBase = document.getElementById('joystick-base');\n        const btnAltUp = document.getElementById('btn-alt-up');\n        const btnAltDown = document.getElementById('btn-alt-down');\n        const speedHudPanel = document.getElementById('hud-speed');\n\n        if (isMobile) {\n            joystickEl.style.display = 'block';\n            btnAltUp.style.display = 'flex';\n            btnAltDown.style.display = 'flex';\n            if (speedHudPanel) speedHudPanel.style.display = 'block';\n        } else {\n            // Show speed HUD on desktop too\n            if (speedHudPanel) speedHudPanel.style.display = 'block';\n        }\n\n        let joystickActive = false;\n        let joystickTouchId = null;\n        const JOYSTICK_RADIUS = 45; // max displacement of knob\n\n        function handleJoystickStart(e) {\n            e.preventDefault();\n            e.stopPropagation();\n            const touch = e.changedTouches ? e.changedTouches[0] : e;\n            joystickActive = true;\n            joystickTouchId = touch.identifier !== undefined ? touch.identifier : -1;\n            handleJoystickMove(e);\n        }\n\n        function handleJoystickMove(e) {\n            if (!joystickActive) return;\n            e.preventDefault();\n            e.stopPropagation();\n            let touch;\n            if (e.changedTouches) {\n                for (let i = 0; i < e.changedTouches.length; i++) {\n                    if (e.changedTouches[i].identifier === joystickTouchId) {\n                        touch = e.changedTouches[i];\n                        break;\n                    }\n                }\n                if (!touch) return;\n            } else {\n                touch = e;\n            }\n            const rect = joystickBase.getBoundingClientRect();\n            const centerX = rect.left + rect.width / 2;\n            const centerY = rect.top + rect.height / 2;\n            let dx = touch.clientX - centerX;\n            let dy = touch.clientY - centerY;\n            const dist = Math.sqrt(dx * dx + dy * dy);\n            if (dist > JOYSTICK_RADIUS) {\n                dx = (dx / dist) * JOYSTICK_RADIUS;\n                dy = (dy / dist) * JOYSTICK_RADIUS;\n            }\n            joystickInputX = dx / JOYSTICK_RADIUS;\n            joystickInputY = dy / JOYSTICK_RADIUS;\n            joystickKnob.style.left = (45 + dx) + 'px';\n            joystickKnob.style.top = (45 + dy) + 'px';\n        }\n\n        function handleJoystickEnd(e) {\n            if (!joystickActive) return;\n            if (e.changedTouches) {\n                let found = false;\n                for (let i = 0; i < e.changedTouches.length; i++) {\n                    if (e.changedTouches[i].identifier === joystickTouchId) { found = true; break; }\n                }\n                if (!found) return;\n            }\n            joystickActive = false;\n            joystickTouchId = null;\n            joystickInputX = 0;\n            joystickInputY = 0;\n            joystickKnob.style.left = '45px';\n            joystickKnob.style.top = '45px';\n        }\n\n        joystickEl.addEventListener('touchstart', handleJoystickStart, { passive: false });\n        joystickEl.addEventListener('touchmove', handleJoystickMove, { passive: false });\n        joystickEl.addEventListener('touchend', handleJoystickEnd, { passive: false });\n        joystickEl.addEventListener('touchcancel', handleJoystickEnd, { passive: false });\n\n        // Altitude buttons\n        if (btnAltUp) {\n            btnAltUp.addEventListener('touchstart', (e) => { e.preventDefault(); e.stopPropagation(); altInputUp = true; }, { passive: false });\n            btnAltUp.addEventListener('touchend', (e) => { e.preventDefault(); altInputUp = false; }, { passive: false });\n            btnAltUp.addEventListener('touchcancel', () => { altInputUp = false; });\n        }\n        if (btnAltDown) {\n            btnAltDown.addEventListener('touchstart', (e) => { e.preventDefault(); e.stopPropagation(); altInputDown = true; }, { passive: false });\n            btnAltDown.addEventListener('touchend', (e) => { e.preventDefault(); altInputDown = false; }, { passive: false });\n            btnAltDown.addEventListener('touchcancel', () => { altInputDown = false; });\n        }\n\n\n        // Resume audio context on any interaction\n        document.addEventListener('touchstart', () => { if (audioCtx.state === 'suspended') audioCtx.resume(); }, { once: true });\n        document.addEventListener('click', () => { if (audioCtx.state === 'suspended') audioCtx.resume(); }, { once: true });\n\n        // --- Game Actions ---\n        // --- Explosion Effect for Explosive Bullets ---\n        function triggerExplosion(position, radius, damage) {\n            // Massive fireball core\n            const explGeo = new THREE.SphereGeometry(radius * 0.5, 12, 12);\n            const explMat = new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 1 });\n            const explMesh = new THREE.Mesh(explGeo, explMat);\n            explMesh.position.copy(position);\n            scene.add(explMesh);\n            buildingDamages.push({ type: 'dust', mesh: explMesh, vel: new THREE.Vector3(0, 3, 0), life: 0.7 });\n\n            // Inner white-hot core\n            const coreGeo = new THREE.SphereGeometry(radius * 0.25, 8, 8);\n            const coreMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 1 });\n            const coreMesh = new THREE.Mesh(coreGeo, coreMat);\n            coreMesh.position.copy(position);\n            scene.add(coreMesh);\n            buildingDamages.push({ type: 'dust', mesh: coreMesh, vel: new THREE.Vector3(0, 1, 0), life: 0.3 });\n\n            // Multiple expanding shockwave rings\n            for (let r = 0; r < 3; r++) {\n                const ringGeo = new THREE.RingGeometry(radius * 0.2 * r, radius * (0.4 + r * 0.3), 24);\n                const ringMat = new THREE.MeshBasicMaterial({ color: r === 0 ? 0xffffff : 0xffaa00, transparent: true, opacity: 0.8 - r * 0.2, side: THREE.DoubleSide });\n                const ring = new THREE.Mesh(ringGeo, ringMat);\n                ring.position.copy(position);\n                ring.position.y += 1;\n                ring.rotation.x = -Math.PI / 2;\n                scene.add(ring);\n                buildingDamages.push({ type: 'dust', mesh: ring, vel: new THREE.Vector3(0, 0.3, 0), life: 0.5 + r * 0.15 });\n            }\n\n            // Vertical fire column\n            const colGeo = new THREE.CylinderGeometry(radius * 0.15, radius * 0.4, radius * 1.2, 8);\n            const colMat = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.7 });\n            const col = new THREE.Mesh(colGeo, colMat);\n            col.position.copy(position);\n            col.position.y += radius * 0.4;\n            scene.add(col);\n            buildingDamages.push({ type: 'dust', mesh: col, vel: new THREE.Vector3(0, 8, 0), life: 0.6 });\n\n            // Fire/debris particles - more and bigger\n            const fireCount = Math.max(5, Math.round(20 * perfMonitor.particleMultiplier));\n            for (let i = 0; i < fireCount; i++) {\n                const size = 0.5 + Math.random() * 1.2;\n                const pGeo = new THREE.SphereGeometry(size, 4, 4);\n                const pMat = new THREE.MeshBasicMaterial({ color: new THREE.Color().setHSL(0.03 + Math.random() * 0.1, 1, 0.3 + Math.random() * 0.5), transparent: true, opacity: 1 });\n                const p = new THREE.Mesh(pGeo, pMat);\n                p.position.copy(position);\n                const vel = new THREE.Vector3((Math.random()-0.5)*20, Math.random()*15+5, (Math.random()-0.5)*20);\n                scene.add(p);\n                buildingDamages.push({ type: 'debris', mesh: p, vel, life: 1.0 + Math.random() * 0.8 });\n            }\n\n            // Smoke clouds\n            const smokeCount = Math.max(2, Math.round(8 * perfMonitor.particleMultiplier));\n            for (let i = 0; i < smokeCount; i++) {\n                const sGeo = new THREE.SphereGeometry(1 + Math.random() * 2, 6, 6);\n                const sMat = new THREE.MeshBasicMaterial({ color: 0x333333, transparent: true, opacity: 0.6 });\n                const smoke = new THREE.Mesh(sGeo, sMat);\n                smoke.position.copy(position);\n                smoke.position.y += Math.random() * 3;\n                const vel = new THREE.Vector3((Math.random()-0.5)*8, Math.random()*6+3, (Math.random()-0.5)*8);\n                scene.add(smoke);\n                buildingDamages.push({ type: 'dust', mesh: smoke, vel, life: 1.5 + Math.random() * 0.8 });\n            }\n\n            // Damage nearby monsters\n            for (let i = monsters.length - 1; i >= 0; i--) {\n                const m = monsters[i];\n                if (!m.alive) continue;\n                const dist = m.group.position.distanceTo(position);\n                if (dist < radius) {\n                    const dmgFactor = 1 - (dist / radius) * 0.5;\n                    m.health -= damage * dmgFactor;\n                    m.hitTimer = 0.3;\n                    if (m.health <= 0) {\n                        score += m.points;\n                        showKillFeed(`Explosion kill! +${m.points}`);\n                        playSound('monster_death_sound', 0.4);\n                        removeMonster(i);\n                    }\n                }\n            }\n        }\n\n        // --- Homing Missile System ---\n        function spawnHomingMissile() {\n            if (monsters.length === 0) return;\n            // Find nearest monster\n            let nearest = null, nearDist = Infinity;\n            for (const m of monsters) {\n                if (!m.alive) continue;\n                const d = m.group.position.distanceTo(camera.position);\n                if (d < nearDist) { nearDist = d; nearest = m; }\n            }\n            if (!nearest) return;\n\n            // Large visible missile body\n            const missileGroup = new THREE.Group();\n            const missileBodyGeo = new THREE.CylinderGeometry(0.5, 0.25, 4, 8);\n            missileBodyGeo.rotateX(Math.PI / 2);\n            const missileMat = new THREE.MeshStandardMaterial({ color: 0x334466, metalness: 0.8, roughness: 0.3 });\n            const missileBody = new THREE.Mesh(missileBodyGeo, missileMat);\n            missileGroup.add(missileBody);\n\n            // Nose cone\n            const noseGeo = new THREE.ConeGeometry(0.5, 1.5, 8);\n            noseGeo.rotateX(-Math.PI / 2);\n            const noseMat = new THREE.MeshStandardMaterial({ color: 0x224488, metalness: 0.9, roughness: 0.2 });\n            const nose = new THREE.Mesh(noseGeo, noseMat);\n            nose.position.z = -2.5;\n            missileGroup.add(nose);\n\n            // Fins\n            for (let i = 0; i < 4; i++) {\n                const finGeo = new THREE.BoxGeometry(0.1, 1.2, 1);\n                const finMat = new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.7 });\n                const fin = new THREE.Mesh(finGeo, finMat);\n                fin.position.z = 2;\n                fin.rotation.z = (Math.PI / 2) * i;\n                missileGroup.add(fin);\n            }\n\n            // Bright exhaust flame\n            const exhaustGeo = new THREE.ConeGeometry(0.7, 3, 8);\n            exhaustGeo.rotateX(Math.PI / 2);\n            const exhaustMat = new THREE.MeshBasicMaterial({ color: 0x2288ff, transparent: true, opacity: 0.9 });\n            const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);\n            exhaust.position.z = 3.5;\n            missileGroup.add(exhaust);\n\n            // Inner bright blue-white flame\n            const innerFlameGeo = new THREE.ConeGeometry(0.35, 2, 6);\n            innerFlameGeo.rotateX(Math.PI / 2);\n            const innerFlameMat = new THREE.MeshBasicMaterial({ color: 0xaaddff, transparent: true, opacity: 1 });\n            const innerFlame = new THREE.Mesh(innerFlameGeo, innerFlameMat);\n            innerFlame.position.z = 3;\n            missileGroup.add(innerFlame);\n\n            // Blue glow light around missile\n            const glowGeo = new THREE.SphereGeometry(2, 8, 8);\n            const glowMat = new THREE.MeshBasicMaterial({ color: 0x0066ff, transparent: true, opacity: 0.25 });\n            const glow = new THREE.Mesh(glowGeo, glowMat);\n            missileGroup.add(glow);\n\n            missileGroup.position.copy(camera.position);\n            scene.add(missileGroup);\n\n            homingMissiles.push({\n                mesh: missileGroup,\n                target: nearest,\n                speed: 55,\n                life: 6,\n                damage: getHomingDamage(),\n                trailTimer: 0\n            });\n\n            // Launch flash\n            muzzleFlash.style.background = 'radial-gradient(circle at center, rgba(50, 100, 255, 0.5) 0%, rgba(0, 50, 255, 0.2) 40%, transparent 70%)';\n            muzzleFlash.style.opacity = '1';\n            setTimeout(() => { muzzleFlash.style.opacity = '0'; }, 80);\n            playSound('missile_launch_sound', 0.7);\n            showKillFeed('\ud83d\ude80 Homing missile launched!');\n        }\n\n        function updateHomingMissiles(dt) {\n            for (let i = homingMissiles.length - 1; i >= 0; i--) {\n                const m = homingMissiles[i];\n                m.life -= dt;\n\n                // Re-target if target is dead\n                if (!m.target || !m.target.alive) {\n                    let nearest = null, nearDist = Infinity;\n                    for (const mon of monsters) {\n                        if (!mon.alive) continue;\n                        const d = mon.group.position.distanceTo(m.mesh.position);\n                        if (d < nearDist) { nearDist = d; nearest = mon; }\n                    }\n                    m.target = nearest;\n                }\n\n                if (m.target && m.target.alive) {\n                    const dir = m.target.group.position.clone().sub(m.mesh.position).normalize();\n                    m.mesh.position.addScaledVector(dir, m.speed * dt);\n                    m.mesh.lookAt(m.target.group.position);\n\n                    // Dense smoke and fire trail\n                    m.trailTimer = (m.trailTimer || 0) + dt;\n                    if (m.trailTimer > 0.02) {\n                        m.trailTimer = 0;\n                        // Blue fire particle\n                        const tGeo = new THREE.SphereGeometry(0.4 + Math.random() * 0.5, 4, 4);\n                        const tMat = new THREE.MeshBasicMaterial({ color: new THREE.Color().setHSL(0.55 + Math.random() * 0.1, 0.9, 0.4 + Math.random() * 0.4), transparent: true, opacity: 1 });\n                        const trail = new THREE.Mesh(tGeo, tMat);\n                        trail.position.copy(m.mesh.position);\n                        trail.position.x += (Math.random() - 0.5) * 0.5;\n                        trail.position.y += (Math.random() - 0.5) * 0.5;\n                        trail.position.z += (Math.random() - 0.5) * 0.5;\n                        scene.add(trail);\n                        fireTrails.push({ mesh: trail, life: 0.5 });\n                        // Blue-tinted smoke\n                        const sGeo = new THREE.SphereGeometry(0.6 + Math.random() * 0.8, 4, 4);\n                        const sMat = new THREE.MeshBasicMaterial({ color: 0x334466, transparent: true, opacity: 0.5 });\n                        const smoke = new THREE.Mesh(sGeo, sMat);\n                        smoke.position.copy(m.mesh.position);\n                        scene.add(smoke);\n                        fireTrails.push({ mesh: smoke, life: 0.8 });\n                    }\n\n                    // Hit detection\n                    const dist = m.mesh.position.distanceTo(m.target.group.position);\n                    if (dist < 5) {\n                        // Explode on impact\n                        triggerExplosion(m.mesh.position.clone(), 12, m.damage);\n                        scene.remove(m.mesh);\n                        homingMissiles.splice(i, 1);\n                        continue;\n                    }\n                } else {\n                    // No target, fly forward and expire\n                    const fwd = new THREE.Vector3(0, 0, -1).applyQuaternion(m.mesh.quaternion);\n                    m.mesh.position.addScaledVector(fwd, m.speed * dt);\n                }\n\n                if (m.life <= 0) {\n                    scene.remove(m.mesh);\n                    homingMissiles.splice(i, 1);\n                }\n            }\n        }\n\n        // --- Sniper Ally Functions ---\n        function createSniperAlly() {\n            const group = new THREE.Group();\n\n            // Military sniper figure (blocky)\n            const bodyMat = new THREE.MeshStandardMaterial({ color: 0x2d4a2d, roughness: 0.7 });\n            const skinMat = new THREE.MeshStandardMaterial({ color: 0xffcc99, roughness: 0.8 });\n            const helmetMat = new THREE.MeshStandardMaterial({ color: 0x3a5a3a, roughness: 0.6, metalness: 0.3 });\n            const gunMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.8, roughness: 0.2 });\n\n            // Body\n            const body = new THREE.Mesh(new THREE.BoxGeometry(0.8, 1.2, 0.5), bodyMat);\n            body.position.y = 1.8; group.add(body);\n            // Head\n            const head = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.6, 0.6), skinMat);\n            head.position.y = 2.7; group.add(head);\n            // Helmet\n            const helmet = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.4, 0.7), helmetMat);\n            helmet.position.y = 3.0; group.add(helmet);\n            // Sniper rifle (pointing forward along +Z)\n            const rifle = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.15, 2.5), gunMat);\n            rifle.position.set(0.5, 1.8, 0.8); group.add(rifle);\n            group.userData.rifle = rifle;\n            // Scope on rifle\n            const scope = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.25, 0.5), gunMat);\n            scope.position.set(0.5, 1.95, 0.6); group.add(scope);\n            // Legs (crouching)\n            const legMat = new THREE.MeshStandardMaterial({ color: 0x2d4a2d, roughness: 0.7 });\n            const legL = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.8, 0.4), legMat);\n            legL.position.set(-0.2, 0.9, 0.1); group.add(legL);\n            const legR = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.8, 0.4), legMat);\n            legR.position.set(0.2, 0.9, -0.1); group.add(legR);\n\n            // Muzzle flash indicator (at rifle tip)\n            const muzzleGeo = new THREE.SphereGeometry(0.3, 4, 4);\n            const muzzleMat = new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0 });\n            const muzzle = new THREE.Mesh(muzzleGeo, muzzleMat);\n            muzzle.position.set(0.5, 1.8, 2.1);\n            group.add(muzzle);\n            group.userData.muzzle = muzzle;\n\n            // Position inside evacuation zone\n            const angle = Math.random() * Math.PI * 2;\n            const radius = EVAC_ZONE_RADIUS * 0.5 + Math.random() * (EVAC_ZONE_RADIUS * 0.3);\n            const spawnX = EVAC_ZONE_POS.x + Math.cos(angle) * radius;\n            const spawnZ = EVAC_ZONE_POS.z + Math.sin(angle) * radius;\n            group.position.set(spawnX, 0.1, spawnZ);\n\n            scene.add(group);\n\n            return {\n                group,\n                fireTimer: Math.random() * SNIPER_FIRE_INTERVAL,\n                muzzleFlashTimer: 0,\n                target: null\n            };\n        }\n\n        function spawnSupportSniper() {\n            const sniper = createSniperAlly();\n            sniperAllies.push(sniper);\n\n            // Airdrop visual effect - parachute crate falling into evac zone\n            const crateGeo = new THREE.BoxGeometry(1.5, 1.5, 1.5);\n            const crateMat = new THREE.MeshStandardMaterial({ color: 0x556b2f, roughness: 0.8 });\n            const crate = new THREE.Mesh(crateGeo, crateMat);\n            crate.position.set(sniper.group.position.x, 40, sniper.group.position.z);\n            scene.add(crate);\n\n            // Parachute (simple cone)\n            const chuteGeo = new THREE.ConeGeometry(3, 2, 8, 1, true);\n            const chuteMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8, side: THREE.DoubleSide });\n            const chute = new THREE.Mesh(chuteGeo, chuteMat);\n            chute.position.y = 3;\n            crate.add(chute);\n\n            // Animate the drop\n            const dropTarget = 1;\n            const dropSpeed = 20;\n            function animateDrop() {\n                if (crate.position.y > dropTarget) {\n                    crate.position.y -= dropSpeed * 0.016;\n                    crate.rotation.y += 0.02;\n                    requestAnimationFrame(animateDrop);\n                } else {\n                    scene.remove(crate);\n                }\n            }\n            animateDrop();\n\n            // Announce\n            showKillFeed(`\ud83e\ude96 Sniper reinforcement deployed! (Total: ${sniperAllies.length})`);\n            playSound('wave_complete_sound', 0.6);\n        }\n\n        function updateSniperAllies(dt) {\n            for (let i = 0; i < sniperAllies.length; i++) {\n                const sniper = sniperAllies[i];\n\n                // Muzzle flash fade\n                if (sniper.muzzleFlashTimer > 0) {\n                    sniper.muzzleFlashTimer -= dt;\n                    sniper.group.userData.muzzle.material.opacity = sniper.muzzleFlashTimer > 0 ? 1 : 0;\n                }\n\n                // Fire timer\n                sniper.fireTimer -= dt;\n\n                // Find nearest monster (infinite range)\n                let nearest = null, nearDist = Infinity;\n                for (const m of monsters) {\n                    if (!m.alive) continue;\n                    const d = m.group.position.distanceTo(sniper.group.position);\n                    if (d < nearDist) { nearDist = d; nearest = m; }\n                }\n\n                // Laser aiming phase (show red laser line 3 seconds before firing)\n                const LASER_WARMUP = 3.0;\n                if (nearest && sniper.fireTimer <= LASER_WARMUP && sniper.fireTimer > 0) {\n                    // Face target - rotate entire sniper group so rifle points at target\n                    const dir = nearest.group.position.clone().sub(sniper.group.position).normalize();\n                    sniper.group.rotation.y = Math.atan2(dir.x, dir.z);\n\n                    // Show red laser line from rifle muzzle to target\n                    if (!sniper.laserLine) {\n                        const laserMat = new THREE.LineBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.8 });\n                        const laserGeo = new THREE.BufferGeometry();\n                        laserGeo.setAttribute('position', new THREE.Float32BufferAttribute([0,0,0, 0,0,0], 3));\n                        sniper.laserLine = new THREE.Line(laserGeo, laserMat);\n                        scene.add(sniper.laserLine);\n                    }\n                    // Calculate rifle muzzle world position (muzzle is at local 0.5, 1.8, 2.1)\n                    const muzzleLocal = new THREE.Vector3(0.5, 1.8, 2.1);\n                    const muzzleWorld = muzzleLocal.applyMatrix4(sniper.group.matrixWorld);\n                    const targetPos = nearest.group.position.clone();\n                    targetPos.y += nearest.monsterHeight * 0.7;\n                    const positions = sniper.laserLine.geometry.attributes.position.array;\n                    positions[0] = muzzleWorld.x; positions[1] = muzzleWorld.y; positions[2] = muzzleWorld.z;\n                    positions[3] = targetPos.x; positions[4] = targetPos.y; positions[5] = targetPos.z;\n                    sniper.laserLine.geometry.attributes.position.needsUpdate = true;\n                    // Pulse opacity\n                    sniper.laserLine.material.opacity = 0.5 + Math.sin(Date.now() * 0.01) * 0.3;\n                    sniper.laserLine.visible = true;\n                } else {\n                    // Hide laser\n                    if (sniper.laserLine) sniper.laserLine.visible = false;\n                }\n\n                if (sniper.fireTimer <= 0) {\n                    sniper.fireTimer = SNIPER_FIRE_INTERVAL + Math.random() * 1.0;\n\n                    if (nearest) {\n                        // Face target\n                        const dir = nearest.group.position.clone().sub(sniper.group.position).normalize();\n                        sniper.group.rotation.y = Math.atan2(dir.x, dir.z);\n\n                        // Hide laser on fire\n                        if (sniper.laserLine) sniper.laserLine.visible = false;\n\n                        // Deal damage\n                        nearest.health -= SNIPER_ATTACK_POWER;\n                        nearest.hitTimer = 0.2;\n\n                        // Muzzle flash\n                        sniper.muzzleFlashTimer = 0.15;\n                        sniper.group.userData.muzzle.material.opacity = 1;\n\n                        // Tracer from sniper to monster\n                        const origin = sniper.group.position.clone();\n                        origin.y += 1.8;\n                        const tracerDir = dir.clone();\n                        const tMesh = new THREE.Mesh(tracerGeo, tracerMat.clone());\n                        tMesh.position.copy(origin);\n                        tMesh.lookAt(origin.clone().add(tracerDir));\n                        scene.add(tMesh);\n                        const tGlow = new THREE.Mesh(tracerGlowGeo, tracerGlowMat.clone());\n                        tGlow.position.copy(origin);\n                        tGlow.lookAt(origin.clone().add(tracerDir));\n                        scene.add(tGlow);\n                        tracers.push({ mesh: tMesh, glowMesh: tGlow, dir: tracerDir.clone().multiplyScalar(280), life: 0.4, trail: true });\n\n                        // Sound\n                        playSound('sniper_rifle_sound', 0.5);\n\n                        // Kill check\n                        if (nearest.health <= 0) {\n                            score += nearest.points;\n                            showKillFeed(`\ud83e\ude96 Sniper ally killed ${nearest.type}! +${nearest.points}`);\n                            playSound('monster_death_sound', 0.4);\n                            const idx = monsters.indexOf(nearest);\n                            if (idx >= 0) {\n                                removeMonster(idx);\n                                monstersKilledThisLevel++;\n                                monstersKilledThisSubWave++;\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        function shoot() {\n            if (shootCooldown > 0 || reloading) return;\n\n            if (ammo <= 0) {\n                playSound('empty_gun_click', 0.8);\n                reload();\n                return;\n            }\n\n            ammo--;\n            shootCooldown = getShootCooldown();\n\n            playSound('sniper_rifle_sound', 0.7);\n\n            // Bright yellow flash effect\n            muzzleFlash.style.background = 'radial-gradient(circle at center, rgba(255, 220, 0, 0.6) 0%, rgba(255, 160, 0, 0.3) 30%, transparent 60%)';\n            muzzleFlash.style.opacity = '1';\n            setTimeout(() => { muzzleFlash.style.opacity = '0'; }, 50);\n\n            // Spawn bullet tracer, shell casing, and muzzle fire\n            const shootDir = new THREE.Vector3(0, 0, -1);\n            shootDir.applyQuaternion(camera.quaternion);\n            const muzzleOrigin = camera.position.clone().add(shootDir.clone().multiplyScalar(3));\n            muzzleOrigin.y -= 0.5;\n            const tracerMesh = spawnTracer(muzzleOrigin.clone(), shootDir.clone());\n            \n            spawnShellCasing(camera.position.clone());\n            spawnMuzzleFire(muzzleOrigin.clone(), shootDir.clone());\n\n            // Raycast for hit detection\n            const raycaster = new THREE.Raycaster();\n            raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);\n\n            // Check monsters (raycast all meshes in monster groups)\n            const monsterMeshes = [];\n            monsters.filter(m => m.alive).forEach(m => {\n                m.group.traverse(child => {\n                    if (child.isMesh) {\n                        child.userData.monsterRef = m;\n                        monsterMeshes.push(child);\n                    }\n                });\n            });\n            const monsterHits = raycaster.intersectObjects(monsterMeshes);\n\n            // Raycast environment (buildings + ground)\n            const envObjects = [ground, ...buildings];\n            const envHits = raycaster.intersectObjects(envObjects);\n\n            // Determine closest hit - bullet cannot pass through buildings\n            const monsterDist = monsterHits.length > 0 ? monsterHits[0].distance : Infinity;\n            const envDist = envHits.length > 0 ? envHits[0].distance : Infinity;\n\n            if (envDist < monsterDist) {\n                // Bullet hits building/ground first - blocked!\n                const hitPoint = envHits[0].point;\n                const hitNormal = envHits[0].face ? envHits[0].face.normal : new THREE.Vector3(0,1,0);\n                const hitObj = envHits[0].object;\n\n                if (buildings.includes(hitObj)) {\n                    // Building damage effect - bullet hole + debris\n                    spawnBuildingDamage(hitPoint, hitNormal, hitObj);\n                } else {\n                    // Ground crater\n                    spawnCrater(hitPoint, hitNormal);\n                }\n            } else if (monsterHits.length > 0) {\n                const hitObj = monsterHits[0].object;\n                const monster = hitObj.userData.monsterRef;\n                if (monster && monster.alive) {\n                    // Check if headshot (upper portion - head area)\n                    const hitPoint = monsterHits[0].point;\n                    const monsterTop = monster.group.position.y + monster.monsterHeight;\n                    const isHeadshot = hitPoint.y > (monsterTop - 4);\n\n                    // Giant bullet bonus damage\n                    const giantBonus = getGiantDamageBonus();\n\n                    if (isHeadshot) {\n                        monster.health -= getHeadshotDamage();\n                        showHeadshotIndicator(hitPoint);\n                    } else {\n                        monster.health -= getBulletDamage();\n                    }\n\n                    // Explosive bullet chance\n                    if (explosiveBulletLevel > 0 && Math.random() < getExplosiveChance()) {\n                        triggerExplosion(hitPoint.clone(), 12 + explosiveBulletLevel * 2, getBulletDamage() * 2);\n                        showKillFeed('\ud83d\udca5 Explosive triggered!');\n                        // Screen shake effect\n                        const shakeIntensity = 0.02 + explosiveBulletLevel * 0.005;\n                        camera.rotation.x += (Math.random() - 0.5) * shakeIntensity;\n                        camera.rotation.y += (Math.random() - 0.5) * shakeIntensity;\n                        // Full-screen orange flash\n                        damageFlash.style.background = 'rgba(255, 150, 0, 0.4)';\n                        damageFlash.style.opacity = '0.6';\n                        damageFlashTimer = 0.2;\n                    }\n\n                    monster.hitTimer = 0.2;\n                    playSound('monster_hit_sound', 0.6);\n\n                    // Spawn chunk particles on hit (few but large)\n                    const hitPt = monsterHits[0].point;\n                    for (let cp = 0; cp < 3; cp++) {\n                        const size = 0.8 + Math.random() * 1.2;\n                        const chunkGeo = new THREE.BoxGeometry(size, size, size);\n                        const chunkMat = new THREE.MeshStandardMaterial({\n                            color: new THREE.Color(config.settings.monsterColor).offsetHSL(Math.random() * 0.1 - 0.05, 0, Math.random() * 0.3 - 0.15),\n                            roughness: 0.7\n                        });\n                        const chunk = new THREE.Mesh(chunkGeo, chunkMat);\n                        chunk.position.copy(hitPt);\n                        const vel = new THREE.Vector3(\n                            (Math.random() - 0.5) * 12,\n                            Math.random() * 8 + 3,\n                            (Math.random() - 0.5) * 12\n                        );\n                        scene.add(chunk);\n                        buildingDamages.push({ type: 'debris', mesh: chunk, vel, life: 1.5 + Math.random() * 0.5 });\n                    }\n\n                    if (monster.health <= 0) {\n                        // Kill - death animation\n                        combo++;\n                        comboTimer = 3;\n                        const points = (isHeadshot ? monster.headPoints : monster.points) * Math.max(1, combo);\n                        score += points;\n                        totalScore += points;\n\n                        const typeNames = { brute: 'Brute', demolisher: 'Demolisher', titan: 'Titan', flyer: 'Flyer', spiker: 'Spiker', swarmer: 'Swarmer', colossus: 'Colossus', dinosaur: '\ud83e\udd96 Dinosaur' };\n                        const typeName = typeNames[monster.type] || 'Monster';\n                        showKillFeed(isHeadshot ? `Headshot ${typeName}! +${points}` : `Killed ${typeName}! +${points}`);\n                        playSound('monster_death_sound', 0.5);\n\n                        // Quick collapse animation before removal\n                        monster.group.userData.dying = true;\n                        monster.group.scale.y = 0.5;\n                        monster.group.position.y = -2;\n\n                        const idx = monsters.indexOf(monster);\n                        if (idx >= 0) {\n                            removeMonster(idx);\n                            monstersKilledThisLevel++;\n                            monstersKilledThisSubWave++;\n                        }\n                    }\n                }\n            }\n\n            updateHUD();\n        }\n\n        function toggleScope() {\n            // FOV is fixed - scope toggle disabled\n        }\n\n        function reload() {\n            if (reloading || ammo >= maxAmmo) return;\n            reloading = true;\n            reloadTimer = 2.0;\n            playSound('reload_sound', 0.7);\n        }\n\n        function showHeadshotIndicator(worldPos) {\n            // Show \"\u7206\u5934!x2\" floating text at crosshair\n            const indicator = document.createElement('div');\n            const hsMultiplier = (2 + giantBulletLevel * 0.5).toFixed(1);\n            indicator.textContent = `\ud83d\udc80 Headshot! x${hsMultiplier}`;\n            indicator.style.cssText = 'position:absolute;top:42%;left:50%;transform:translate(-50%,-50%);font-family:Orbitron,monospace;font-size:24px;font-weight:900;color:#ff4444;text-shadow:0 0 12px #ff0000,0 0 24px #ff4444;pointer-events:none;z-index:50;opacity:1;transition:all 0.8s ease-out;';\n            document.getElementById('ui-overlay').appendChild(indicator);\n            requestAnimationFrame(() => {\n                indicator.style.opacity = '0';\n                indicator.style.top = '36%';\n                indicator.style.fontSize = '30px';\n            });\n            setTimeout(() => indicator.remove(), 800);\n        }\n\n        function showKillFeed(text) {\n            killFeed.textContent = text;\n            killFeed.style.opacity = '1';\n            setTimeout(() => { killFeed.style.opacity = '0'; }, 1500);\n        }\n\n        function updateHUD() {\n            if (homingMissileLevel > 0) {\n                if (btnMissile) btnMissile.style.display = 'flex';\n                if (homingMissileTimer > 0) {\n                    if (btnMissile) btnMissile.style.opacity = '0.5';\n                } else {\n                    if (btnMissile) btnMissile.style.opacity = '1.0';\n                }\n            } else {\n                if (btnMissile) btnMissile.style.display = 'none';\n            }\n\n            healthFill.style.width = `${(playerHealth / config.settings.playerHealth) * 100}%`;\n            healthFill.classList.toggle('low', playerHealth < config.settings.playerHealth * 0.3);\n            ammoValue.textContent = `${ammo}/${maxAmmo}`;\n            ammoValue.classList.toggle('danger', ammo === 0);\n            scoreValue.textContent = totalScore;\n            waveValue.textContent = `${currentLevel}/20`;\n\n            // Monsters remaining\n            const remaining = Math.max(0, monstersToKillThisLevel - monstersKilledThisLevel);\n            timerValue.textContent = `${remaining}`;\n            timerValue.classList.toggle('urgent', remaining <= 3 && remaining > 0);\n\n            // Civilians count\n            const safeCivilians = pedestrians.filter(p => {\n                const dx = p.group.position.x - EVAC_ZONE_POS.x;\n                const dz = p.group.position.z - EVAC_ZONE_POS.z;\n                return Math.sqrt(dx * dx + dz * dz) < EVAC_ZONE_RADIUS;\n            }).length;\n            const safeVehicles = vehicles.filter(v => {\n                const dx = v.group.position.x - EVAC_ZONE_POS.x;\n                const dz = v.group.position.z - EVAC_ZONE_POS.z;\n                return Math.sqrt(dx * dx + dz * dz) < EVAC_ZONE_RADIUS;\n            }).length;\n            civiliansValue.textContent = `${safeCivilians + safeVehicles} Safe | ${civiliansLost} Lost`;\n            if (civiliansLost > maxCiviliansLost * 0.7) {\n                civiliansValue.style.color = '#ff5252';\n            } else {\n                civiliansValue.style.color = '#69f0ae';\n            }\n        }\n\n        // --- Wave System ---\n        function startWave(waveNum) {\n            currentWave = waveNum;\n            totalMonstersThisWave = config.settings.monstersPerWave;\n            monstersToSpawn = totalMonstersThisWave;\n            monstersKilledThisWave = 0;\n            spawnTimer = 0;\n\n            waveAnnounce.textContent = `Wave ${waveNum}`;\n            waveAnnounce.classList.add('show');\n            waveAnnouncerTimer = 2;\n\n            updateHUD();\n        }\n\n        function checkWaveComplete() {\n            if (waveTransitioning) return;\n            if (monstersKilledThisWave >= totalMonstersThisWave && monsters.length === 0 && monstersToSpawn <= 0) {\n                waveTransitioning = true;\n                if (currentWave >= totalWaves) {\n                    gameState = 'victory';\n                    victoryScreen.classList.remove('hidden');\n                    document.getElementById('final-score-win').textContent = score;\n                    if (bgMusic) { bgMusic.source.stop(); bgMusic = null; }\n                    playSound('victory_sound', 0.8);\n                } else {\n                    playSound('wave_complete_sound', 0.7);\n                    setTimeout(() => { waveTransitioning = false; startWave(currentWave + 1); }, 2000);\n                }\n            }\n        }\n\n        // --- Game Start ---\n        function startGame(isNewGame = true) {\n            gameState = 'playing';\n            startScreen.classList.add('hidden');\n            gameoverScreen.classList.add('hidden');\n            victoryScreen.classList.add('hidden');\n\n            playerHealth = config.settings.playerHealth;\n            maxAmmo = getMaxAmmo();\n            ammo = maxAmmo;\n            totalWaves = config.settings.waves;\n            score = 0;\n            combo = 0;\n            currentWave = 0;\n            yaw = 0;\n            pitch = -0.2;\n            isScoped = false;\n            scopeOverlay.classList.remove('active');\n            camera.fov = 75;\n            camera.updateProjectionMatrix();\n\n            // Reset level state\n            evacTimer = EVAC_TOTAL_TIME;\n            civiliansLost = 0;\n            evacWave = currentLevel;\n            dinosaurSpawnedThisLevel = false;\n            globalSpawnTimer = 0;\n            // Longer spawn intervals early, gradually speeds up\n            monsterSpawnInterval = Math.max(1.2, 5.0 - (currentLevel - 1) * 0.2);\n            homingMissileTimer = 3.0;\n            totalSubWaves = 3 + Math.floor((currentLevel - 1) / 4);\n            subWave = 1;\n            // More monsters per level for longer gameplay\n            monstersThisSubWave = Math.ceil((6 + currentLevel * 5 + Math.floor(currentLevel * currentLevel * 0.25)) / totalSubWaves);\n            monstersToKillThisLevel = monstersThisSubWave * totalSubWaves;\n            monstersKilledThisLevel = 0;\n            monstersKilledThisSubWave = 0;\n            monstersSpawnedThisSubWave = 0;\n            subWaveTransitioning = false;\n            subWaveTimer = 0;\n            subWaveElapsed = 0;\n            levelComplete = false;\n\n            // Activate prep phase (briefing countdown)\n            prepPhaseActive = true;\n            prepPhaseTimer = PREP_PHASE_DURATION;\n            showBriefingOverlay();\n\n            while (homingMissiles.length > 0) { scene.remove(homingMissiles[0].mesh); homingMissiles.shift(); }\n\n            // Reset helicopter speed; only reset position on new game\n            heliForwardSpeed = 0;\n            heliStrafeSpeed = 0;\n            heliAltSpeed = 0;\n            if (isNewGame) {\n                currentLevel = 1;\n                totalScore = 0;\n                upgradePoints = 49000;\n                fireRateLevel = 0;\n                damageLevel = 0;\n                ammoCapacityLevel = 0;\n                explosiveBulletLevel = 0;\n                giantBulletLevel = 0;\n                homingMissileLevel = 1;\n                heliAltitude = 90;\n                heliGroup.position.set(0, 90, 0);\n            }\n\n            // Clear sniper allies - keep existing allies and don't reset timer between levels\n            // (sniperAllies persist across levels, supportTimer continues counting)\n            if (isNewGame) {\n                while (sniperAllies.length > 0) { scene.remove(sniperAllies[0].group); sniperAllies.shift(); }\n                supportTimer = SUPPORT_INTERVAL;\n            }\n\n            // Clear monsters, pedestrians, vehicles\n            while (monsters.length > 0) removeMonster(0);\n            while (pedestrians.length > 0) removePedestrian(0);\n            while (vehicles.length > 0) removeVehicle(0);\n\n            // Spawn initial pedestrians and vehicles\n            for (let i = 0; i < MAX_PEDESTRIANS; i++) spawnPedestrian();\n            for (let i = 0; i < MAX_VEHICLES; i++) spawnVehicle();\n\n            updateHUD();\n\n            if (menuMusic) { menuMusic.source.stop(); menuMusic = null; }\n            bgMusic = playSound('background_music', 0.4, true);\n\n            // Wave announcements hidden per user preference\n        }\n\n        // --- Upgrade Screen Logic ---\n        const upgradeScreen = document.getElementById('upgrade-screen');\n        const upgradePointsValue = document.getElementById('upgrade-points-value');\n\n        function showUpgradeScreen() {\n            gameState = 'upgrading';\n            gameoverScreen.classList.add('hidden');\n            upgradeScreen.classList.remove('hidden');\n            refreshUpgradeUI();\n        }\n\n        function refreshUpgradeUI() {\n            upgradePointsValue.textContent = upgradePoints;\n\n            // Fire rate\n            document.getElementById('upgrade-firerate-level').textContent = `Lv.${fireRateLevel}`;\n            const frCost = getFireRateCost();\n            const frBtn = document.getElementById('btn-upgrade-firerate');\n            if (fireRateLevel >= UPGRADE_MAX_LEVEL) {\n                frBtn.textContent = 'MAX';\n                frBtn.disabled = true;\n            } else {\n                frBtn.textContent = `Upgrade (${frCost}pts)`;\n                frBtn.disabled = upgradePoints < frCost;\n            }\n            document.getElementById('upgrade-firerate-desc').textContent = `Fire interval: ${(getShootCooldown()*1000).toFixed(0)}ms`;\n\n            // Damage\n            document.getElementById('upgrade-damage-level').textContent = `Lv.${damageLevel}`;\n            const dmgCost = getDamageCost();\n            const dmgBtn = document.getElementById('btn-upgrade-damage');\n            if (damageLevel >= UPGRADE_MAX_LEVEL) {\n                dmgBtn.textContent = 'MAX';\n                dmgBtn.disabled = true;\n            } else {\n                dmgBtn.textContent = `Upgrade (${dmgCost}pts)`;\n                dmgBtn.disabled = upgradePoints < dmgCost;\n            }\n            document.getElementById('upgrade-damage-desc').textContent = `Damage: ${getBulletDamage().toFixed(1)} | Headshot: ${getHeadshotDamage().toFixed(1)} (x2)`;\n\n            // Ammo\n            document.getElementById('upgrade-ammo-level').textContent = `Lv.${ammoCapacityLevel}`;\n            const ammoCost = getAmmoCost();\n            const ammoBtn = document.getElementById('btn-upgrade-ammo');\n            if (ammoCapacityLevel >= UPGRADE_MAX_LEVEL) {\n                ammoBtn.textContent = 'MAX';\n                ammoBtn.disabled = true;\n            } else {\n                ammoBtn.textContent = `Upgrade (${ammoCost}pts)`;\n                ammoBtn.disabled = upgradePoints < ammoCost;\n            }\n            document.getElementById('upgrade-ammo-desc').textContent = `Max ammo: ${getMaxAmmo()}`;\n\n            // Explosive\n            document.getElementById('upgrade-explosive-level').textContent = `Lv.${explosiveBulletLevel}`;\n            const expCost = getExplosiveCost();\n            const expBtn = document.getElementById('btn-upgrade-explosive');\n            if (explosiveBulletLevel >= UPGRADE_MAX_LEVEL) { expBtn.textContent = 'MAX'; expBtn.disabled = true; }\n            else { expBtn.textContent = `Upgrade (${expCost}pts)`; expBtn.disabled = upgradePoints < expCost; }\n            document.getElementById('upgrade-explosive-desc').textContent = `Explosion chance: ${Math.floor(getExplosiveChance()*100)}%`;\n\n            // Giant\n            document.getElementById('upgrade-giant-level').textContent = `Lv.${giantBulletLevel}`;\n            const gntCost = getGiantCost();\n            const gntBtn = document.getElementById('btn-upgrade-giant');\n            if (giantBulletLevel >= UPGRADE_MAX_LEVEL) { gntBtn.textContent = 'MAX'; gntBtn.disabled = true; }\n            else { gntBtn.textContent = `Upgrade (${gntCost}pts)`; gntBtn.disabled = upgradePoints < gntCost; }\n            document.getElementById('upgrade-giant-desc').textContent = `Headshot multiplier: x${(2 + giantBulletLevel * 0.5).toFixed(1)}`;\n\n            // Homing\n            document.getElementById('upgrade-homing-level').textContent = `Lv.${homingMissileLevel}`;\n            const hmCost = getHomingCost();\n            const hmBtn = document.getElementById('btn-upgrade-homing');\n            if (homingMissileLevel >= UPGRADE_MAX_LEVEL) { hmBtn.textContent = 'MAX'; hmBtn.disabled = true; }\n            else { hmBtn.textContent = `Upgrade (${hmCost}pts)`; hmBtn.disabled = upgradePoints < hmCost; }\n            document.getElementById('upgrade-homing-desc').textContent = `Interval: ${getHomingInterval().toFixed(1)}s | Damage: ${getHomingDamage()}`;\n\n\n        }\n\n        document.getElementById('btn-upgrade-firerate').addEventListener('click', () => {\n            const cost = getFireRateCost();\n            if (upgradePoints >= cost && fireRateLevel < UPGRADE_MAX_LEVEL) {\n                upgradePoints -= cost;\n                fireRateLevel++;\n                lib.log(`Upgraded Fire Rate to Level ${fireRateLevel}`);\n                refreshUpgradeUI();\n            }\n        });\n\n        document.getElementById('btn-upgrade-damage').addEventListener('click', () => {\n            const cost = getDamageCost();\n            if (upgradePoints >= cost && damageLevel < UPGRADE_MAX_LEVEL) {\n                upgradePoints -= cost;\n                damageLevel++;\n                lib.log(`Upgraded Damage to Level ${damageLevel}`);\n                refreshUpgradeUI();\n            }\n        });\n\n        document.getElementById('btn-upgrade-ammo').addEventListener('click', () => {\n            const cost = getAmmoCost();\n            if (upgradePoints >= cost && ammoCapacityLevel < UPGRADE_MAX_LEVEL) {\n                upgradePoints -= cost;\n                ammoCapacityLevel++;\n                lib.log(`Upgraded Ammo to Level ${ammoCapacityLevel}`);\n                refreshUpgradeUI();\n            }\n        });\n\n        document.getElementById('btn-upgrade-explosive').addEventListener('click', () => {\n            const cost = getExplosiveCost();\n            if (upgradePoints >= cost && explosiveBulletLevel < UPGRADE_MAX_LEVEL) {\n                upgradePoints -= cost;\n                explosiveBulletLevel++;\n                lib.log(`Upgraded Explosive Rounds to Level ${explosiveBulletLevel}`);\n                refreshUpgradeUI();\n            }\n        });\n\n        document.getElementById('btn-upgrade-giant').addEventListener('click', () => {\n            const cost = getGiantCost();\n            if (upgradePoints >= cost && giantBulletLevel < UPGRADE_MAX_LEVEL) {\n                upgradePoints -= cost;\n                giantBulletLevel++;\n                lib.log(`Upgraded Headshot Boost to Level ${giantBulletLevel}`);\n                refreshUpgradeUI();\n            }\n        });\n\n        document.getElementById('btn-upgrade-homing').addEventListener('click', () => {\n            const cost = getHomingCost();\n            if (upgradePoints >= cost && homingMissileLevel < UPGRADE_MAX_LEVEL) {\n                upgradePoints -= cost;\n                homingMissileLevel++;\n                lib.log(`Upgraded Homing Missiles to Level ${homingMissileLevel}`);\n                refreshUpgradeUI();\n            }\n        });\n\n\n        document.getElementById('btn-upgrade-start').addEventListener('click', () => {\n            upgradeScreen.classList.add('hidden');\n            startGame(false);\n            requestPointerLock();\n        });\n\n        // --- Leaderboard Functions ---\n        const leaderboardScreen = document.getElementById('leaderboard-screen');\n        const leaderboardContent = document.getElementById('leaderboard-content');\n        const leaderboardLoading = document.getElementById('leaderboard-loading');\n        const leaderboardError = document.getElementById('leaderboard-error');\n        const leaderboardList = document.getElementById('leaderboard-list');\n        let leaderboardReturnScreen = 'menu'; // Track which screen to return to\n\n        async function submitScoreToLeaderboard(finalScore) {\n            try {\n                const response = await lib.addPlayerScoreToLeaderboard(finalScore, 10);\n                \n                // Handle both object response and direct array\n                let entries = null;\n                let userRank = null;\n                let success = false;\n                \n                if (response && typeof response === 'object') {\n                    if (Array.isArray(response)) {\n                        // Response is just an array\n                        entries = response;\n                        success = entries.length > 0;\n                    } else {\n                        // Response is an object with entries and userRank\n                        entries = response.entries || [];\n                        userRank = response.userRank;\n                        success = response.success !== false; // Default to true if not specified\n                    }\n                }\n                \n                if (success && userRank) {\n                    showKillFeed(`\ud83c\udfc6 Rank #${userRank} on leaderboard!`);\n                    lib.log(`Score ${finalScore} submitted! Rank: #${userRank}`);\n                } else if (success) {\n                    lib.log(`Score ${finalScore} submitted successfully!`);\n                }\n            } catch (error) {\n                lib.log(`Failed to submit score to leaderboard: ${error.message}`);\n                // Game continues normally even if leaderboard fails\n            }\n        }\n\n        async function showLeaderboard(fromScreen = 'menu') {\n            gameState = 'leaderboard';\n            leaderboardReturnScreen = fromScreen;\n            startScreen.classList.add('hidden');\n            gameoverScreen.classList.add('hidden');\n            victoryScreen.classList.add('hidden');\n            leaderboardScreen.classList.remove('hidden');\n            leaderboardLoading.style.display = 'block';\n            leaderboardError.style.display = 'none';\n            leaderboardList.style.display = 'none';\n            leaderboardList.innerHTML = '';\n\n            try {\n                const response = await lib.getTopNEntriesFromLeaderboard(10);\n                leaderboardLoading.style.display = 'none';\n\n                // Handle both array response and object response\n                let entries = [];\n                let userRank = null;\n                \n                if (response) {\n                    if (Array.isArray(response)) {\n                        // Response is just an array\n                        entries = response;\n                    } else if (typeof response === 'object') {\n                        // Response is an object with entries and userRank\n                        entries = response.entries || [];\n                        userRank = response.userRank;\n                    }\n                }\n\n                if (entries && entries.length > 0) {\n                    leaderboardList.style.display = 'block';\n                    \n                    entries.forEach((entry, index) => {\n                        // Validate entry has required fields\n                        if (!entry || typeof entry !== 'object') return;\n                        if (!entry.username || entry.score === undefined) return;\n                        \n                        const rankColors = ['#FFD700', '#C0C0C0', '#CD7F32'];\n                        const rankColor = index < 3 ? rankColors[index] : 'var(--lego-white)';\n                        const isCurrentUser = userRank && (index + 1) === userRank;\n                        \n                        const entryDiv = document.createElement('div');\n                        entryDiv.style.cssText = `\n                            display: flex;\n                            align-items: center;\n                            background: ${isCurrentUser ? 'rgba(255, 204, 0, 0.15)' : 'rgba(255,255,255,0.03)'};\n                            border: 3px solid ${isCurrentUser ? 'var(--lego-yellow)' : 'var(--lego-blue)'};\n                            border-radius: 0px;\n                            padding: 12px 16px;\n                            margin: 8px 0;\n                            box-shadow: 3px 3px 0px rgba(0,0,0,0.5);\n                            transition: all 0.2s;\n                        `;\n                        \n                        // Use profilePicture if available, otherwise show placeholder\n                        const profilePic = entry.profilePicture || 'data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"40\"%3E%3Crect fill=\"%23333\" width=\"40\" height=\"40\"/%3E%3Ctext x=\"50%25\" y=\"50%25\" fill=\"%23fff\" font-size=\"20\" text-anchor=\"middle\" dy=\".3em\"%3E?%3C/text%3E%3C/svg%3E';\n                        \n                        entryDiv.innerHTML = `\n                            <div style=\"font-family: 'Press Start 2P', monospace; font-size: 16px; font-weight: 900; color: ${rankColor}; min-width: 50px; text-shadow: 2px 2px 0px rgba(0,0,0,0.8);\">#${index + 1}</div>\n                            <img src=\"${profilePic}\" alt=\"${entry.username}\" style=\"width: 40px; height: 40px; border-radius: 0px; border: 2px solid var(--lego-yellow); margin: 0 16px; image-rendering: pixelated;\" onerror=\"this.src='data:image/svg+xml,%3Csvg xmlns=\\\\'http://www.w3.org/2000/svg\\\\' width=\\\\'40\\\\' height=\\\\'40\\\\'%3E%3Crect fill=\\\\'%23333\\\\' width=\\\\'40\\\\' height=\\\\'40\\\\'/%3E%3Ctext x=\\\\'50%25\\\\' y=\\\\'50%25\\\\' fill=\\\\'%23fff\\\\' font-size=\\\\'20\\\\' text-anchor=\\\\'middle\\\\' dy=\\\\'.3em\\\\'%3E${entry.username.charAt(0).toUpperCase()}%3C/text%3E%3C/svg%3E'\">\n                            <div style=\"flex: 1; font-family: 'Press Start 2P', monospace; font-size: 11px; color: var(--lego-white); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\">${entry.username}</div>\n                            <div style=\"font-family: 'Press Start 2P', monospace; font-size: 14px; font-weight: 700; color: var(--lego-yellow); margin-left: 16px; text-shadow: 2px 2px 0px rgba(0,0,0,0.8);\">${entry.score}</div>\n                        `;\n                        \n                        leaderboardList.appendChild(entryDiv);\n                    });\n\n                    // Show user's rank if not in top 10\n                    if (userRank && userRank > 10) {\n                        const userRankDiv = document.createElement('div');\n                        userRankDiv.style.cssText = `\n                            font-family: 'Press Start 2P', monospace;\n                            font-size: 10px;\n                            color: var(--lego-yellow);\n                            text-align: center;\n                            margin-top: 24px;\n                            padding: 12px;\n                            background: rgba(255,204,0,0.1);\n                            border: 2px solid var(--lego-yellow);\n                            border-radius: 0px;\n                        `;\n                        userRankDiv.textContent = `Your Rank: #${userRank}`;\n                        leaderboardList.appendChild(userRankDiv);\n                    }\n                } else {\n                    leaderboardError.style.display = 'block';\n                    leaderboardError.textContent = 'No scores yet. Be the first!';\n                }\n            } catch (error) {\n                lib.log(`Failed to fetch leaderboard: ${error.message}`);\n                leaderboardLoading.style.display = 'none';\n                leaderboardError.style.display = 'block';\n                leaderboardError.textContent = error.message.includes('guest') ? 'Login to view leaderboard' : 'Failed to load leaderboard';\n            }\n        }\n\n        function hideLeaderboard() {\n            leaderboardScreen.classList.add('hidden');\n            if (leaderboardReturnScreen === 'victory') {\n                victoryScreen.classList.remove('hidden');\n                gameState = 'victory';\n            } else if (leaderboardReturnScreen === 'gameover') {\n                gameoverScreen.classList.remove('hidden');\n                gameState = 'gameover';\n            } else {\n                startScreen.classList.remove('hidden');\n                gameState = 'menu';\n            }\n        }\n\n        // Button handlers\n        document.getElementById('btn-start').addEventListener('click', () => { startGame(); requestPointerLock(); });\n        document.getElementById('btn-retry').addEventListener('click', () => { startGame(false); requestPointerLock(); });\n        document.getElementById('btn-upgrade').addEventListener('click', () => { showUpgradeScreen(); });\n        document.getElementById('btn-replay').addEventListener('click', () => { startGame(); requestPointerLock(); });\n        document.getElementById('btn-leaderboard').addEventListener('click', () => { showLeaderboard('menu'); });\n        document.getElementById('btn-gameover-leaderboard').addEventListener('click', () => { showLeaderboard('gameover'); });\n        document.getElementById('btn-victory-leaderboard').addEventListener('click', () => { showLeaderboard('victory'); });\n        document.getElementById('btn-leaderboard-back').addEventListener('click', () => { hideLeaderboard(); });\n\n        // --- Battle Briefing System ---\n        const briefingOverlay = document.getElementById('briefing-overlay');\n        const briefingLevel = document.getElementById('briefing-level');\n        const briefingInfo = document.getElementById('briefing-info');\n        const briefingTip = document.getElementById('briefing-tip');\n        const briefingCountdown = document.getElementById('briefing-countdown');\n\n        const BATTLE_TIPS = [\n            \"Aim for the head! Headshots deal x2 damage or more.\",\n            \"Use WASD/Joystick to reposition for better angles.\",\n            \"Upgrade explosive rounds for crowd control.\",\n            \"Homing missiles auto-target the nearest monster.\",\n            \"Flyers dive-bomb civilians \u2014 prioritize them!\",\n            \"Titans are heavily armored. Focus fire on weak points.\",\n            \"Keep moving! A stationary helicopter is an easy target.\",\n            \"Reload early \u2014 don't get caught with an empty magazine.\",\n            \"Upgrade fire rate for faster sustained damage.\",\n            \"Watch the radar for incoming threats.\",\n            \"Support snipers deploy every 2 minutes automatically.\",\n            \"Monsters will smash through buildings blocking their path!\",\n            \"Higher altitude gives better overview but harder to aim.\",\n            \"Colossus monsters appear after Level 12 \u2014 save upgrades!\",\n            \"Protect vehicles \u2014 each destruction counts as a casualty.\"\n        ];\n\n        function getExpectedEnemies(level) {\n            const enemies = ['Brute', 'Demolisher'];\n            if (level >= 2) enemies.push('\ud83e\udd96 Dinosaur');\n            if (level >= 3) enemies.push('Spiker');\n            if (level >= 4) enemies.push('Flyer');\n            if (level >= 6) enemies.push('Swarmer');\n            if (level >= 8) enemies.push('Titan');\n            if (level >= 12) enemies.push('Colossus');\n            return enemies;\n        }\n\n        function showBriefingOverlay() {\n            if (!briefingOverlay) return;\n            briefingOverlay.classList.remove('hidden');\n            briefingLevel.textContent = `LEVEL ${currentLevel}`;\n            \n            const totalMonsters = monstersThisSubWave * totalSubWaves;\n            const expectedEnemies = getExpectedEnemies(currentLevel);\n            briefingInfo.innerHTML = `Hostiles: ~${totalMonsters} targets<br>` +\n                `Sub-waves: ${totalSubWaves}<br>` +\n                `Threat types: ${expectedEnemies.join(', ')}`;\n            \n            const tip = BATTLE_TIPS[Math.floor(Math.random() * BATTLE_TIPS.length)];\n            briefingTip.textContent = `\ud83d\udca1 TIP: ${tip}`;\n            \n            briefingCountdown.textContent = '3';\n        }\n\n        function hideBriefingOverlay() {\n            if (briefingOverlay) briefingOverlay.classList.add('hidden');\n        }\n\n        // Play menu music\n        menuMusic = playSound('menu_music', 0.3, true);\n\n        // --- Main Game Loop ---\n        function gameLoop(timestamp) {\n            const dt = Math.min((timestamp - (lastTime || timestamp)) / 1000, 0.11);\n            lastTime = timestamp;\n\n            if (gameState === 'playing') {\n                // Continuous fire\n                if ((mouseHeld || spaceHeld) && shootCooldown <= 0 && !reloading) {\n                    shoot();\n                }\n\n                // Update cooldowns\n                if (shootCooldown > 0) shootCooldown -= dt;\n                if (comboTimer > 0) { comboTimer -= dt; if (comboTimer <= 0) combo = 0; }\n\n                // Decay swipe speed (smart safety)\n                swipeSpeed *= Math.max(0, 1 - dt * 15);\n\n                // --- Prep Phase Countdown ---\n                if (prepPhaseActive) {\n                    prepPhaseTimer -= dt;\n                    const countNum = Math.ceil(prepPhaseTimer);\n                    if (briefingCountdown) briefingCountdown.textContent = countNum > 0 ? countNum : 'GO!';\n                    if (prepPhaseTimer <= 0) {\n                        prepPhaseActive = false;\n                        hideBriefingOverlay();\n                        showKillFeed('\u2694\ufe0f Monsters incoming! Defend the zone!');\n                    }\n                }\n\n                // Reload\n                if (reloading) {\n                    reloadTimer -= dt;\n                    if (reloadTimer <= 0) {\n                        ammo = maxAmmo;\n                        reloading = false;\n                        updateHUD();\n                    }\n                }\n\n                // Wave announcer\n                if (waveAnnouncerTimer > 0) {\n                    waveAnnouncerTimer -= dt;\n                    if (waveAnnouncerTimer <= 0) waveAnnounce.classList.remove('show');\n                }\n\n                // Damage flash\n                if (damageFlashTimer > 0) {\n                    damageFlashTimer -= dt;\n                    damageFlash.style.opacity = damageFlashTimer > 0 ? '0.4' : '0';\n                }\n\n                // --- Sub-wave completion check ---\n                if (!prepPhaseActive) subWaveElapsed += dt;\n                if (subWaveTransitioning) {\n                    subWaveTimer -= dt;\n                    if (subWaveTimer <= 0) {\n                        subWaveTransitioning = false;\n                        subWave++;\n                        monstersKilledThisSubWave = 0;\n                        monstersSpawnedThisSubWave = 0;\n                        subWaveElapsed = 0;\n                    }\n                }\n                // Trigger next sub-wave if all killed OR 15 seconds elapsed\n                const subWaveDone = monstersKilledThisSubWave >= monstersThisSubWave && monsters.length === 0;\n                const subWaveTimeout = subWaveElapsed >= 15 && monstersSpawnedThisSubWave >= monstersThisSubWave;\n                if (!subWaveTransitioning && !levelComplete && (subWaveDone || subWaveTimeout)) {\n                    if (subWave >= totalSubWaves) {\n                        // All sub-waves done - level complete when all monsters dead\n                        if (monsters.length === 0) {\n                            levelComplete = true;\n                            if (currentLevel >= 20) {\n                                gameState = 'victory';\n                                hideBriefingOverlay();\n                                victoryScreen.classList.remove('hidden');\n                                document.getElementById('final-score-win').textContent = score;\n                                if (bgMusic) { bgMusic.source.stop(); bgMusic = null; }\n                                playSound('victory_sound', 0.8);\n                                // Submit score to leaderboard on victory\n                                submitScoreToLeaderboard(score);\n                            } else {\n                                playSound('wave_complete_sound', 0.7);\n                                lib.log(`Level ${currentLevel} Complete! Adding ${score} to Upgrade Points.`);\n                                upgradePoints += score;\n                                setTimeout(() => {\n                                    currentLevel++;\n                                    showUpgradeScreen();\n                                }, 1500);\n                            }\n                        }\n                    } else {\n                        // Sub-wave complete, brief pause then next sub-wave\n                        subWaveTransitioning = true;\n                        subWaveTimer = 2.5; // breathing room between sub-waves\n                        subWaveElapsed = 0;\n                        playSound('wave_complete_sound', 0.5);\n                        showKillFeed(`\u2705 Sub-wave ${subWave}/${totalSubWaves} cleared! Next wave incoming...`);\n                    }\n                }\n\n                // Timer still counts for urgency (but not during prep phase)\n                if (!prepPhaseActive) evacTimer -= dt;\n\n                // Check lose condition - too many civilians lost\n                if (civiliansLost >= maxCiviliansLost) {\n                    gameState = 'gameover';\n                    lib.log(`Game Over! Adding ${score} to Upgrade Points.`);\n                    upgradePoints += score;\n                    hideBriefingOverlay();\n                    gameoverScreen.classList.remove('hidden');\n                    document.getElementById('final-score-lose').textContent = score;\n                    if (bgMusic) { bgMusic.source.stop(); bgMusic = null; }\n                    playSound('game_over_sound', 0.8);\n                    // Submit score to leaderboard on game over\n                    submitScoreToLeaderboard(score);\n                }\n\n                // Spawn monsters for this sub-wave (only when prep phase is done)\n                const monstersLeftToSpawn = monstersThisSubWave - monstersSpawnedThisSubWave;\n                const maxAlive = Math.min(8 + currentLevel * 2, 30);\n                if (!prepPhaseActive && monstersLeftToSpawn > 0 && monsters.length < maxAlive && !levelComplete && !subWaveTransitioning) {\n                    globalSpawnTimer -= dt;\n                    if (globalSpawnTimer <= 0) {\n                        spawnMonster();\n                        monstersSpawnedThisSubWave++;\n                        globalSpawnTimer = monsterSpawnInterval;\n                        if (Math.random() < 0.3) {\n                            playSound('monster_roar', 0.3);\n                        }\n                    }\n                }\n\n                // Update monsters\n                for (let i = monsters.length - 1; i >= 0; i--) {\n                    const m = monsters[i];\n                    if (!m.alive) continue;\n\n                    const isFlyer = m.type === 'flyer';\n\n                    // Prioritize attacking nearby vehicles and pedestrians\n                    let targetPos = EVAC_ZONE_POS.clone();\n                    let nearestTargetDist = Infinity;\n                    // Check vehicles first (higher priority)\n                    for (let vi = 0; vi < vehicles.length; vi++) {\n                        const v = vehicles[vi];\n                        if (!v.alive) continue;\n                        const dx = v.group.position.x - m.group.position.x;\n                        const dz = v.group.position.z - m.group.position.z;\n                        const d = Math.sqrt(dx * dx + dz * dz);\n                        if (d < 60 && d < nearestTargetDist) {\n                            nearestTargetDist = d;\n                            targetPos = v.group.position.clone();\n                        }\n                    }\n                    // Check pedestrians\n                    for (let pi = 0; pi < pedestrians.length; pi++) {\n                        const p = pedestrians[pi];\n                        if (!p.alive) continue;\n                        const dx = p.group.position.x - m.group.position.x;\n                        const dz = p.group.position.z - m.group.position.z;\n                        const d = Math.sqrt(dx * dx + dz * dz);\n                        if (d < 50 && d < nearestTargetDist) {\n                            nearestTargetDist = d;\n                            targetPos = p.group.position.clone();\n                        }\n                    }\n                    // Pathfinding for ground monsters, direct for flyers\n                    let dir;\n                    if (!isFlyer) {\n                        // Recalculate path periodically\n                        m.pathRecalcTimer -= dt;\n                        const targetChanged = m.lastTargetPos && targetPos.distanceTo(m.lastTargetPos) > 20;\n                        if (!m.path || m.pathRecalcTimer <= 0 || targetChanged || m.pathIndex >= (m.path ? m.path.length : 0)) {\n                            m.path = findPath(m.group.position.x, m.group.position.z, targetPos.x, targetPos.z);\n                            m.pathIndex = 0;\n                            m.pathRecalcTimer = 0.8 + Math.random() * 0.5;\n                            m.lastTargetPos = targetPos.clone();\n                        }\n                        // Follow path waypoints\n                        if (m.path && m.path.length > 0 && m.pathIndex < m.path.length) {\n                            const wp = m.path[m.pathIndex];\n                            const wpDx = wp.x - m.group.position.x;\n                            const wpDz = wp.z - m.group.position.z;\n                            const wpDist = Math.sqrt(wpDx * wpDx + wpDz * wpDz);\n                            if (wpDist < NAV_CELL_SIZE * 0.9) {\n                                m.pathIndex++;\n                            }\n                            if (m.pathIndex < m.path.length) {\n                                const nextWp = m.path[m.pathIndex];\n                                dir = new THREE.Vector3(nextWp.x - m.group.position.x, 0, nextWp.z - m.group.position.z).normalize();\n                            } else {\n                                dir = targetPos.clone().sub(m.group.position).normalize();\n                                dir.y = 0;\n                            }\n                        } else {\n                            // No path needed (direct line clear) or no path found - move directly\n                            dir = targetPos.clone().sub(m.group.position).normalize();\n                            dir.y = 0;\n                        }\n                    } else {\n                        dir = targetPos.clone().sub(m.group.position).normalize();\n                        dir.y = 0;\n                    }\n                    const moveVec = dir.clone().multiplyScalar(m.speed * dt * 4);\n                    const newPos = m.group.position.clone().add(moveVec);\n\n                    if (isFlyer) {\n                        // Flyers avoid buildings horizontally\n                        m.animTime += dt * 2;\n                        m.flyHeight = 30 + Math.sin(m.animTime * 0.5) * 8;\n                        // Check building collision at flight height\n                        const currentFlyY = m.group.position.y;\n                        const flyRadius = m.monsterWidth / 2 + 2;\n                        let flyCollided = false;\n                        for (let b = 0; b < buildings.length; b++) {\n                            const bld = buildings[b];\n                            const bPos = bld.position;\n                            const bGeo = bld.geometry.parameters;\n                            const bHeight = bGeo.height;\n                            // Only collide if flying at building's height\n                            if (currentFlyY < bHeight + 3) {\n                                const halfW = bGeo.width / 2 + flyRadius;\n                                const halfD = bGeo.depth / 2 + flyRadius;\n                                if (newPos.x > bPos.x - halfW && newPos.x < bPos.x + halfW &&\n                                    newPos.z > bPos.z - halfD && newPos.z < bPos.z + halfD) {\n                                    flyCollided = true;\n                                    // Slide around building\n                                    const dx = newPos.x - bPos.x;\n                                    const dz = newPos.z - bPos.z;\n                                    const overlapX = halfW - Math.abs(dx);\n                                    const overlapZ = halfD - Math.abs(dz);\n                                    if (overlapX < overlapZ) {\n                                        newPos.x = bPos.x + Math.sign(dx) * halfW;\n                                    } else {\n                                        newPos.z = bPos.z + Math.sign(dz) * halfD;\n                                    }\n                                }\n                            }\n                        }\n                        m.group.position.x = newPos.x;\n                        m.group.position.z = newPos.z;\n                        // Banking turn\n                        m.group.rotation.z = Math.sin(m.walkCycle) * 0.12;\n                    } else {\n                        // Ground monsters: collision detection with buildings\n                        let collided = false;\n                        let overlappingBuildings = [];\n                        const monsterRadius = m.monsterWidth / 2 + 1;\n                        for (let b = 0; b < buildings.length; b++) {\n                            const bld = buildings[b];\n                            const bPos = bld.position;\n                            const bGeo = bld.geometry.parameters;\n                            const halfW = bGeo.width / 2 + monsterRadius;\n                            const halfD = bGeo.depth / 2 + monsterRadius;\n                            if (newPos.x > bPos.x - halfW && newPos.x < bPos.x + halfW &&\n                                newPos.z > bPos.z - halfD && newPos.z < bPos.z + halfD) {\n                                collided = true;\n                                overlappingBuildings.push(bld);\n                                const dx = newPos.x - bPos.x;\n                                const dz = newPos.z - bPos.z;\n                                const overlapX = halfW - Math.abs(dx);\n                                const overlapZ = halfD - Math.abs(dz);\n                                if (overlapX < overlapZ) {\n                                    newPos.x = bPos.x + Math.sign(dx) * halfW;\n                                } else {\n                                    newPos.z = bPos.z + Math.sign(dz) * halfD;\n                                }\n                            }\n                        }\n                        m.group.position.x = newPos.x;\n                        m.group.position.z = newPos.z;\n\n                        if (m.lastPos) {\n                            const distMoved = m.group.position.distanceTo(m.lastPos);\n                            if (distMoved < m.speed * dt * 1.5) { // intended move is roughly m.speed * dt * 4\n                                m.stuckTimer = (m.stuckTimer || 0) + dt;\n                            } else {\n                                m.stuckTimer = 0;\n                                m.buildingAttackHits = 0;\n                                m.buildingAttackCooldown = 0;\n                                m.attackingBuilding = false;\n                            }\n                        } else {\n                            m.stuckTimer = 0;\n                        }\n                        m.lastPos = m.group.position.clone();\n\n                        // When stuck, quickly attack blocking building 3 times to destroy it\n                        if (m.stuckTimer > 0.5 && overlappingBuildings.length > 0) {\n                            if (!m.attackingBuilding) {\n                                m.attackingBuilding = true;\n                                m.buildingAttackHits = 0;\n                                m.buildingAttackCooldown = 0;\n                                m.targetBuilding = overlappingBuildings[0];\n                            }\n                            m.buildingAttackCooldown = (m.buildingAttackCooldown || 0) - dt;\n                            if (m.buildingAttackCooldown <= 0) {\n                                m.buildingAttackHits = (m.buildingAttackHits || 0) + 1;\n                                m.buildingAttackCooldown = 0.35; // fast 0.35s between attacks\n                                \n                                // Violent attack animation - extremely exaggerated slam\n                                // Arms swing way overhead then crash down\n                                if (m.group.userData.armL) m.group.userData.armL.rotation.x = -2.8;\n                                if (m.group.userData.armR) m.group.userData.armR.rotation.x = -2.8;\n                                // Whole body lunges forward and up\n                                m.group.rotation.x = -0.6;\n                                m.group.position.y += 4;\n                                // Head thrashes\n                                if (m.group.userData.head) m.group.userData.head.rotation.x = -0.8;\n                                if (m.group.userData.jaw) m.group.userData.jaw.rotation.x = 0.7;\n                                setTimeout(() => {\n                                    // SLAM DOWN - arms crash forward\n                                    if (m.group.userData.armL) m.group.userData.armL.rotation.x = 1.8;\n                                    if (m.group.userData.armR) m.group.userData.armR.rotation.x = 1.8;\n                                    m.group.rotation.x = 0.4;\n                                    m.group.position.y -= 4;\n                                    if (m.group.userData.head) m.group.userData.head.rotation.x = 0.5;\n                                    if (m.group.userData.jaw) m.group.userData.jaw.rotation.x = -0.3;\n                                }, 150);\n                                setTimeout(() => {\n                                    // Reset\n                                    if (m.group.userData.armL) m.group.userData.armL.rotation.x = 0;\n                                    if (m.group.userData.armR) m.group.userData.armR.rotation.x = 0;\n                                    m.group.rotation.x = 0;\n                                    if (m.group.userData.head) m.group.userData.head.rotation.x = 0;\n                                    if (m.group.userData.jaw) m.group.userData.jaw.rotation.x = 0;\n                                }, 320);\n                                \n                                // Spawn hit debris on each attack\n                                const bldTarget = m.targetBuilding || overlappingBuildings[0];\n                                if (bldTarget) {\n                                    const hitPos = bldTarget.position.clone();\n                                    hitPos.y = m.group.position.y + m.monsterHeight * 0.5;\n                                    for (let i = 0; i < 8; i++) {\n                                        const size = 0.5 + Math.random() * 1.0;\n                                        const pGeo = new THREE.BoxGeometry(size, size, size);\n                                        const pMat = new THREE.MeshStandardMaterial({ color: new THREE.Color(0.4 + Math.random() * 0.3, 0.4 + Math.random() * 0.2, 0.35 + Math.random() * 0.2) });\n                                        const piece = new THREE.Mesh(pGeo, pMat);\n                                        piece.position.copy(hitPos);\n                                        piece.position.x += (Math.random() - 0.5) * 5;\n                                        piece.position.z += (Math.random() - 0.5) * 5;\n                                        const vel = new THREE.Vector3((Math.random() - 0.5) * 12, Math.random() * 8 + 3, (Math.random() - 0.5) * 12);\n                                        scene.add(piece);\n                                        buildingDamages.push({ type: 'debris', mesh: piece, vel, life: 1.5 + Math.random() });\n                                    }\n                                    playSound('monster_hit_sound', 0.8);\n                                    showKillFeed(`\ud83c\udfe2 Monster attacking building! (${m.buildingAttackHits}/3)`);\n                                }\n                                \n                                // After 3 hits, destroy the building\n                                if (m.buildingAttackHits >= 3) {\n                                    const bldToDestroy = m.targetBuilding || overlappingBuildings[0];\n                                    if (bldToDestroy && buildings.includes(bldToDestroy)) {\n                                        scene.remove(bldToDestroy);\n                                        if (bldToDestroy.userData.roofLight) scene.remove(bldToDestroy.userData.roofLight);\n                                        buildings.splice(buildings.indexOf(bldToDestroy), 1);\n                                        buildNavGrid(); // Rebuild navigation grid after building destroyed\n                                        \n                                        // Spawn collapse effect\n                                        const bPos = bldToDestroy.position;\n                                        const bGeo = bldToDestroy.geometry.parameters;\n                                        for (let i = 0; i < 20; i++) {\n                                            const dGeo = new THREE.SphereGeometry(3 + Math.random() * 4, 6, 6);\n                                            const dMat = new THREE.MeshBasicMaterial({ color: 0x888888, transparent: true, opacity: 0.8 });\n                                            const dust = new THREE.Mesh(dGeo, dMat);\n                                            dust.position.set(\n                                                bPos.x + (Math.random() - 0.5) * bGeo.width,\n                                                Math.random() * bGeo.height * 0.5,\n                                                bPos.z + (Math.random() - 0.5) * bGeo.depth\n                                            );\n                                            const vel = new THREE.Vector3((Math.random() - 0.5) * 10, Math.random() * 5 + 2, (Math.random() - 0.5) * 10);\n                                            scene.add(dust);\n                                            buildingDamages.push({ type: 'dust', mesh: dust, vel, life: 2.0 + Math.random() * 1.5 });\n                                        }\n                                        for (let i = 0; i < 15; i++) {\n                                            const size = 1 + Math.random() * 2;\n                                            const pGeo = new THREE.BoxGeometry(size, size, size);\n                                            const pMat = new THREE.MeshStandardMaterial({ color: 0x666666 });\n                                            const piece = new THREE.Mesh(pGeo, pMat);\n                                            piece.position.set(\n                                                bPos.x + (Math.random() - 0.5) * bGeo.width,\n                                                Math.random() * bGeo.height,\n                                                bPos.z + (Math.random() - 0.5) * bGeo.depth\n                                            );\n                                            const vel = new THREE.Vector3((Math.random() - 0.5) * 15, Math.random() * 10, (Math.random() - 0.5) * 15);\n                                            scene.add(piece);\n                                            buildingDamages.push({ type: 'debris', mesh: piece, vel, life: 2.5 + Math.random() });\n                                        }\n                                        playSound('monster_hit_sound', 1.0);\n                                        showKillFeed('\ud83c\udfe2\ud83d\udca5 Monster smashed through a building!');\n                                    }\n                                    m.buildingAttackHits = 0;\n                                    m.attackingBuilding = false;\n                                    m.stuckTimer = 0;\n                                }\n                            }\n                        } else if (m.stuckTimer > 3.0 && overlappingBuildings.length === 0) {\n                            // No building nearby but stuck - nudge towards center\n                            const toCenter = EVAC_ZONE_POS.clone().sub(m.group.position).normalize();\n                            m.group.position.addScaledVector(toCenter, 5);\n                            m.stuckTimer = 0;\n                        }\n                    }\n\n                    // Face movement direction\n                    const angle = Math.atan2(dir.x, dir.z);\n                    m.group.rotation.y = angle;\n\n                    // Walk/fly animation - slow heavy cycle for ground monsters\n                    const walkSpeed = isFlyer ? (dt * m.speed * 6) : (dt * m.speed * 3);\n                    m.walkCycle += walkSpeed;\n\n                    if (isFlyer) {\n                        // Dive attack logic\n                        if (!m.diveState) m.diveState = 'flying'; // flying, diving, rising\n                        if (!m.diveCooldown) m.diveCooldown = 3 + Math.random() * 4;\n\n                        if (m.diveState === 'flying') {\n                            m.diveCooldown -= dt;\n                            if (m.diveCooldown <= 0) {\n                                // Find nearest target to dive at\n                                let diveTarget = null, diveDist = 80;\n                                for (const v of vehicles) {\n                                    if (!v.alive) continue;\n                                    const d = m.group.position.clone().setY(0).distanceTo(v.group.position.clone().setY(0));\n                                    if (d < diveDist) { diveDist = d; diveTarget = v.group.position.clone(); }\n                                }\n                                for (const p of pedestrians) {\n                                    if (!p.alive) continue;\n                                    const d = m.group.position.clone().setY(0).distanceTo(p.group.position.clone().setY(0));\n                                    if (d < diveDist) { diveDist = d; diveTarget = p.group.position.clone(); }\n                                }\n                                if (diveTarget) {\n                                    m.diveState = 'diving';\n                                    m.diveTargetPos = diveTarget;\n                                    m.diveTargetPos.y = 3; // low altitude for attack\n                                }\n                            }\n                        } else if (m.diveState === 'diving') {\n                            // Swoop down toward target\n                            const targetY = m.diveTargetPos.y;\n                            m.group.position.y -= dt * 30;\n                            if (m.group.position.y <= targetY + 2) {\n                                m.group.position.y = targetY + 2;\n                                m.diveState = 'rising';\n                                m.diveCooldown = 4 + Math.random() * 3;\n                                // Deal damage to nearby targets at impact\n                                for (let vi = vehicles.length - 1; vi >= 0; vi--) {\n                                    const v = vehicles[vi];\n                                    if (!v.alive) continue;\n                                    const d = m.group.position.distanceTo(v.group.position);\n                                    if (d < 8) {\n                                        v.alive = false;\n                                        spawnDestroyEffect(v.group.position, true);\n                                        civiliansLost++;\n                                        score = Math.max(0, score - 30);\n                                        showKillFeed(`Flyer dive-bombed a vehicle! (Lost:${civiliansLost}/${maxCiviliansLost})`);\n                                        updateHUD();\n                                        removeVehicle(vi);\n                                        break;\n                                    }\n                                }\n                                for (let pi = pedestrians.length - 1; pi >= 0; pi--) {\n                                    const p = pedestrians[pi];\n                                    if (!p.alive) continue;\n                                    const d = m.group.position.distanceTo(p.group.position);\n                                    if (d < 6) {\n                                        p.alive = false;\n                                        spawnDestroyEffect(p.group.position, false);\n                                        civiliansLost++;\n                                        score = Math.max(0, score - 20);\n                                        showKillFeed(`Flyer attacked a civilian! (Lost:${civiliansLost}/${maxCiviliansLost})`);\n                                        updateHUD();\n                                        removePedestrian(pi);\n                                        break;\n                                    }\n                                }\n                            }\n                        } else if (m.diveState === 'rising') {\n                            // Rise back up\n                            m.group.position.y += dt * 20;\n                            if (m.group.position.y >= m.flyHeight) {\n                                m.group.position.y = m.flyHeight;\n                                m.diveState = 'flying';\n                            }\n                        }\n\n                        // Only apply normal fly height when in flying state\n                        if (m.diveState === 'flying') {\n                            m.group.position.y = m.flyHeight + Math.sin(m.animTime * 3) * 1.5;\n                        }\n\n                        // Wing flap animation - faster during dive\n                        const flapSpeed = m.diveState === 'diving' ? 4 : 2;\n                        const flapAngle = Math.sin(m.walkCycle * flapSpeed) * 0.6;\n                        if (m.group.userData.armL) m.group.userData.armL.rotation.z = flapAngle;\n                        if (m.group.userData.armR) m.group.userData.armR.rotation.z = -flapAngle;\n                        // Body pitch - nose down during dive\n                        const divePitch = m.diveState === 'diving' ? 0.6 : (m.diveState === 'rising' ? -0.3 : Math.sin(m.animTime) * 0.1);\n                        m.group.rotation.x = divePitch;\n                    } else {\n                        // Heavy lumbering walk: slow swing, big body sway, stomping bob\n                        const legPhase = m.walkCycle * 0.7;\n                        const walkSwing = Math.sin(legPhase) * 0.35;\n                        const armSwing = Math.sin(legPhase + 0.4) * 0.3; // arms lag behind legs\n                        if (m.group.userData.legL) m.group.userData.legL.rotation.x = walkSwing;\n                        if (m.group.userData.legR) m.group.userData.legR.rotation.x = -walkSwing;\n\n                        // Heavy body sway side to side\n                        m.group.rotation.z = Math.sin(legPhase * 0.5) * 0.06;\n                        // Forward lean\n                        m.group.rotation.x = 0.05 + Math.abs(Math.sin(legPhase)) * 0.03;\n                        // Stomping vertical bob - sharp impact feel\n                        const stompPhase = Math.abs(Math.sin(legPhase));\n                        m.group.position.y = stompPhase * stompPhase * 0.8;\n\n                        // Check distance for attack mode\n                        const distToCenter = Math.sqrt(m.group.position.x**2 + m.group.position.z**2);\n                        if (distToCenter < 30 && !m.attacking) {\n                            m.attacking = true;\n                            m.attackTimer = 0;\n                        }\n\n                        // Attack animation (slow heavy slams)\n                        if (m.attacking) {\n                            m.attackTimer += dt * 2.5;\n                            const attackSwing = Math.sin(m.attackTimer) * 1.2;\n                            if (m.group.userData.armL) m.group.userData.armL.rotation.x = attackSwing;\n                            if (m.group.userData.armR) m.group.userData.armR.rotation.x = attackSwing;\n                            if (m.group.userData.jaw) m.group.userData.jaw.rotation.x = Math.abs(Math.sin(m.attackTimer * 1.5)) * 0.5;\n                            if (m.group.userData.head) m.group.userData.head.rotation.z = Math.sin(m.attackTimer * 2) * 0.08;\n                            // Heavy head nod\n                            if (m.group.userData.head) m.group.userData.head.rotation.x = Math.sin(m.attackTimer * 1.2) * 0.15;\n                        } else {\n                            if (m.group.userData.armL) m.group.userData.armL.rotation.x = -armSwing;\n                            if (m.group.userData.armR) m.group.userData.armR.rotation.x = armSwing;\n                            // Idle head bob - slow and heavy\n                            if (m.group.userData.head) m.group.userData.head.rotation.x = Math.sin(m.walkCycle * 0.4) * 0.05;\n                        }\n                    }\n\n                    // Hit flash\n                    if (m.hitTimer > 0) {\n                        m.hitTimer -= dt;\n                        m.group.traverse(child => {\n                            if (child.isMesh && child.material && child.material.emissive !== undefined) {\n                                child.material.emissive.setHex(0xff0000);\n                                child.material.emissiveIntensity = 0.8;\n                            }\n                        });\n                    } else {\n                        m.group.traverse(child => {\n                            if (child.isMesh && child.material && child.material.emissive !== undefined) {\n                                child.material.emissive.setHex(0x000000);\n                                child.material.emissiveIntensity = 0;\n                            }\n                        });\n                    }\n\n                    // Monster attacks evacuation zone\n                    const escapeDist = Math.sqrt(m.group.position.x**2 + m.group.position.z**2);\n                    if (escapeDist < EVAC_ZONE_RADIUS) {\n                        // Monster is in the evacuation zone - attacking civilians!\n                        m.attacking = true;\n                        // Slow down inside zone\n                        m.speed = MONSTER_TYPES[m.type].speed * config.settings.monsterSpeed * 0.4;\n                    }\n                }\n\n                // Animate evacuation zone beacons\n                const beaconFlash = Math.sin(timestamp * 0.005) > 0;\n                beaconMeshes.forEach((beacon, idx) => {\n                    const phase = (timestamp * 0.004 + idx * 0.7) % (Math.PI * 2);\n                    beacon.material.color.setHex(Math.sin(phase) > 0 ? 0xffaa00 : 0x442200);\n                });\n\n                // Update timer HUD every frame\n                updateHUD();\n\n                // Update pedestrians and vehicles\n                updatePedestrians(dt);\n                updateVehicles(dt);\n\n                // No respawning - limited population\n\n                // --- Homing Missile System ---\n                if (homingMissileLevel > 0) {\n                    homingMissileTimer -= dt;\n                    if (homingMissileTimer <= 0 && monsters.length > 0) {\n                        spawnHomingMissile();\n                        homingMissileTimer = getHomingInterval();\n                    }\n                }\n                updateHomingMissiles(dt);\n\n                // --- Support Sniper Timer ---\n                supportTimer -= dt;\n                if (supportTimer <= 0) {\n                    spawnSupportSniper();\n                    supportTimer = SUPPORT_INTERVAL;\n                }\n                updateSniperAllies(dt);\n                updateSupportHUD();\n\n\n                // Update bullet effects (tracers, shells, muzzle fire)\n                updateBulletEffects(dt);\n                updateBuildingDamages(dt);\n\n                // Clamp pitch\n                pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));\n\n                // Smooth camera movement (interpolate toward target)\n                const smoothFactor = isScoped ? 0.12 : 0.18;\n                yaw += (targetYaw - yaw) * smoothFactor;\n                pitch += (targetPitch - pitch) * smoothFactor;\n\n                // --- Helicopter Flight Physics ---\n                // Gather movement input (keyboard + joystick)\n                let inputForward = 0, inputStrafe = 0, inputAlt = 0;\n                if (keysDown['w'] || keysDown['W'] || keysDown['ArrowUp']) inputForward = 1;\n                if (keysDown['s'] || keysDown['S'] || keysDown['ArrowDown']) inputForward = -1;\n                if (keysDown['a'] || keysDown['A'] || keysDown['ArrowLeft']) inputStrafe = -1;\n                if (keysDown['d'] || keysDown['D'] || keysDown['ArrowRight']) inputStrafe = 1;\n                if (keysDown['q'] || keysDown['Q'] || altInputUp) inputAlt = 1;\n                if (keysDown['e'] || keysDown['E'] || altInputDown) inputAlt = -1;\n\n                // Apply joystick input (mobile)\n                if (Math.abs(joystickInputY) > 0.1) inputForward = -joystickInputY;\n                if (Math.abs(joystickInputX) > 0.1) inputStrafe = joystickInputX;\n\n                // Acceleration / deceleration\n                if (inputForward !== 0) {\n                    heliForwardSpeed += inputForward * HELI_ACCELERATION * dt;\n                } else {\n                    if (Math.abs(heliForwardSpeed) > 0.5) {\n                        heliForwardSpeed -= Math.sign(heliForwardSpeed) * HELI_BRAKE * dt;\n                    } else {\n                        heliForwardSpeed = 0;\n                    }\n                }\n                if (inputStrafe !== 0) {\n                    heliStrafeSpeed += inputStrafe * HELI_ACCELERATION * dt;\n                } else {\n                    if (Math.abs(heliStrafeSpeed) > 0.5) {\n                        heliStrafeSpeed -= Math.sign(heliStrafeSpeed) * HELI_BRAKE * dt;\n                    } else {\n                        heliStrafeSpeed = 0;\n                    }\n                }\n\n                // Altitude control\n                if (inputAlt !== 0) {\n                    heliAltSpeed += inputAlt * HELI_ACCELERATION * 0.6 * dt;\n                } else {\n                    if (Math.abs(heliAltSpeed) > 0.5) {\n                        heliAltSpeed -= Math.sign(heliAltSpeed) * HELI_BRAKE * dt;\n                    } else {\n                        heliAltSpeed = 0;\n                    }\n                }\n\n                // Clamp speeds\n                heliForwardSpeed = Math.max(-HELI_MAX_SPEED * 0.6, Math.min(HELI_MAX_SPEED, heliForwardSpeed));\n                heliStrafeSpeed = Math.max(-HELI_MAX_SPEED * 0.8, Math.min(HELI_MAX_SPEED * 0.8, heliStrafeSpeed));\n                heliAltSpeed = Math.max(-HELI_ALT_MAX_SPEED, Math.min(HELI_ALT_MAX_SPEED, heliAltSpeed));\n\n                // Move helicopter relative to yaw direction\n                const moveAngle = yaw;\n                const fwdX = -Math.sin(moveAngle);\n                const fwdZ = -Math.cos(moveAngle);\n                const rightX = Math.cos(moveAngle);\n                const rightZ = -Math.sin(moveAngle);\n\n                heliGroup.position.x += (fwdX * heliForwardSpeed + rightX * heliStrafeSpeed) * dt;\n                heliGroup.position.z += (fwdZ * heliForwardSpeed + rightZ * heliStrafeSpeed) * dt;\n                heliAltitude += heliAltSpeed * dt;\n\n                // Boundary and altitude limits\n                heliGroup.position.x = Math.max(-HELI_MAP_BOUNDS, Math.min(HELI_MAP_BOUNDS, heliGroup.position.x));\n                heliGroup.position.z = Math.max(-HELI_MAP_BOUNDS, Math.min(HELI_MAP_BOUNDS, heliGroup.position.z));\n                heliAltitude = Math.max(HELI_MIN_ALT, Math.min(HELI_MAX_ALT, heliAltitude));\n\n                // Hover bob\n                const hoverOffset = Math.sin(timestamp * 0.001) * 0.3;\n                heliGroup.position.y = heliAltitude + hoverOffset;\n\n                // Helicopter tilt based on movement (nose down when forward, lean into turns)\n                const tiltForward = -(heliForwardSpeed / HELI_MAX_SPEED) * 0.18;\n                const tiltSide = (heliStrafeSpeed / HELI_MAX_SPEED) * 0.14;\n                \n                // Heli group follows the player's look direction\n                heliGroup.rotation.order = 'YXZ';\n                heliGroup.rotation.y = yaw;\n                heliGroup.rotation.x = pitch + tiltForward;\n                heliGroup.rotation.z = tiltSide;\n\n                // Animate helicopter rotors\n                heliRotorAngle += dt * 25;\n                rotorGroup.rotation.y = heliRotorAngle;\n                tailRotorGroup.rotation.x = heliRotorAngle * 1.5;\n\n                // Position camera inside cockpit (pilot seat view)\n                const cockpitOffset = new THREE.Vector3(0, 1.5, 4);\n                cockpitOffset.applyQuaternion(heliGroup.quaternion);\n                \n                camera.position.copy(heliGroup.position).add(cockpitOffset);\n\n                // Update speed HUD\n                const currentSpeed = Math.sqrt(heliForwardSpeed * heliForwardSpeed + heliStrafeSpeed * heliStrafeSpeed);\n                const speedHud = document.getElementById('speed-value');\n                if (speedHud) speedHud.textContent = `${Math.round(currentSpeed * 3.6)} km/h`;\n\n                // Update camera rotation\n                camera.rotation.order = 'YXZ';\n                camera.rotation.y = yaw;\n                camera.rotation.x = pitch;\n\n                // Update searchlight to follow camera\n                searchLight.position.copy(camera.position);\n                const lightDir = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);\n                searchLight.target.position.copy(camera.position).addScaledVector(lightDir, 50);\n                // Update visible cone\n                searchCone.visible = true;\n                searchCone.position.copy(camera.position).addScaledVector(lightDir, 40);\n                searchCone.lookAt(camera.position);\n                searchCone.rotateX(Math.PI / 2);\n            }\n\n            // Draw enemy direction indicators\n            drawEnemyIndicators();\n\n            // Draw radar\n            drawRadar();\n\n            perfMonitor.update();\n            renderer.render(scene, camera);\n            requestAnimationFrame(gameLoop);\n        }\n\n        // --- Enemy Direction Indicators (red triangles on screen edge) ---\n        const indicatorCanvas = document.createElement('canvas');\n        indicatorCanvas.width = 720;\n        indicatorCanvas.height = 1280;\n        indicatorCanvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:15;';\n        document.getElementById('ui-overlay').appendChild(indicatorCanvas);\n        const indCtx = indicatorCanvas.getContext('2d');\n\n        function drawEnemyIndicators() {\n            indCtx.clearRect(0, 0, 720, 1280);\n            if (gameState !== 'playing') return;\n\n            const screenCX = 720 / 2;\n            const screenCY = 1280 / 2;\n            const marginX = 60;\n            const marginY = 80;\n\n            for (let i = 0; i < monsters.length; i++) {\n                const m = monsters[i];\n                if (!m.alive) continue;\n\n                // Project monster position to screen\n                const pos = m.group.position.clone();\n                pos.y += m.monsterHeight * 0.5;\n                const screenPos = pos.clone().project(camera);\n\n                const sx = (screenPos.x * 0.5 + 0.5) * 720;\n                const sy = (-screenPos.y * 0.5 + 0.5) * 1280;\n\n                // Check if on screen (with some margin)\n                const onScreen = screenPos.z < 1 && sx > marginX && sx < 720 - marginX && sy > marginY && sy < 1280 - marginY;\n                if (onScreen) continue; // Don't show indicator for visible enemies\n\n                // Calculate angle from screen center to enemy\n                const dx = sx - screenCX;\n                const dy = sy - screenCY;\n                // If behind camera, flip direction\n                let angle = Math.atan2(dy, dx);\n                if (screenPos.z >= 1) {\n                    angle = Math.atan2(-dy, -dx);\n                }\n\n                // Place indicator on screen edge\n                const edgeX = Math.cos(angle);\n                const edgeY = Math.sin(angle);\n                const maxExtentX = screenCX - marginX;\n                const maxExtentY = screenCY - marginY;\n                const scaleX = Math.abs(edgeX) > 0.001 ? maxExtentX / Math.abs(edgeX) : 9999;\n                const scaleY = Math.abs(edgeY) > 0.001 ? maxExtentY / Math.abs(edgeY) : 9999;\n                const scale = Math.min(scaleX, scaleY);\n                const ix = screenCX + edgeX * scale;\n                const iy = screenCY + edgeY * scale;\n\n                // Draw sharp-tipped polygon pointing toward enemy\n                const triSize = 18;\n                indCtx.save();\n                indCtx.translate(ix, iy);\n                indCtx.rotate(angle);\n                // Sharp arrowhead: very acute tip at front, wide notched tail\n                indCtx.beginPath();\n                indCtx.moveTo(triSize, 0);            // sharp tip (points at enemy)\n                indCtx.lineTo(-triSize * 0.55, -triSize * 0.32); // upper shoulder\n                indCtx.lineTo(-triSize * 0.25, 0);    // center notch (tail indent)\n                indCtx.lineTo(-triSize * 0.55, triSize * 0.32);  // lower shoulder\n                indCtx.closePath();\n                indCtx.fillStyle = 'rgba(255, 30, 30, 0.90)';\n                indCtx.shadowColor = '#ff0000';\n                indCtx.shadowBlur = 10;\n                indCtx.fill();\n                indCtx.strokeStyle = 'rgba(255, 140, 140, 0.95)';\n                indCtx.lineWidth = 1.5;\n                indCtx.stroke();\n                indCtx.restore();\n            }\n        }\n\n        const radarCanvas = document.getElementById('radar-canvas');\n        const radarCtx = radarCanvas.getContext('2d');\n        const RADAR_SIZE = 200;\n        const RADAR_RANGE = 150;\n\n        function drawRadar() {\n            radarCtx.clearRect(0, 0, RADAR_SIZE, RADAR_SIZE);\n            const cx = RADAR_SIZE / 2, cy = RADAR_SIZE / 2;\n            radarCtx.strokeStyle = 'rgba(0,255,80,0.25)';\n            radarCtx.lineWidth = 0.5;\n            for (let r = 20; r < 70; r += 20) {\n                radarCtx.beginPath(); radarCtx.arc(cx, cy, r, 0, Math.PI*2); radarCtx.stroke();\n            }\n            radarCtx.beginPath(); radarCtx.moveTo(cx,0); radarCtx.lineTo(cx,RADAR_SIZE); radarCtx.stroke();\n            radarCtx.beginPath(); radarCtx.moveTo(0,cy); radarCtx.lineTo(RADAR_SIZE,cy); radarCtx.stroke();\n            radarCtx.fillStyle = 'rgba(0,255,80,0.9)';\n            radarCtx.beginPath(); radarCtx.arc(cx, cy, 5, 0, Math.PI*2); radarCtx.fill();\n            // Draw semi-transparent fan sector for heading direction\n            const fanAngle = Math.PI / 4; // 45 degree spread\n            const fanRadius = 60;\n            const headingAngle = -yaw - Math.PI / 2;\n            radarCtx.beginPath();\n            radarCtx.moveTo(cx, cy);\n            radarCtx.arc(cx, cy, fanRadius, headingAngle - fanAngle / 2, headingAngle + fanAngle / 2);\n            radarCtx.closePath();\n            radarCtx.fillStyle = 'rgba(0,255,80,0.13)';\n            radarCtx.fill();\n            radarCtx.strokeStyle = 'rgba(0,255,80,0.45)';\n            radarCtx.lineWidth = 1.2;\n            radarCtx.stroke();\n            const hx = heliGroup.position.x, hz = heliGroup.position.z;\n            for (let i = 0; i < monsters.length; i++) {\n                const m = monsters[i];\n                if (!m.alive) continue;\n                const dx = m.group.position.x - hx;\n                const dz = m.group.position.z - hz;\n                const dist = Math.sqrt(dx*dx + dz*dz);\n                if (dist > RADAR_RANGE) continue;\n                const scale = 60 / RADAR_RANGE;\n                const rx = cx + dx * scale;\n                const ry = cy + dz * scale;\n                // Different colors per type\n                const typeColors = { brute: 'rgba(255,50,50,0.95)', demolisher: 'rgba(180,30,80,0.95)', titan: 'rgba(255,150,0,0.95)', flyer: 'rgba(255,215,0,0.95)', spiker: 'rgba(255,50,150,0.95)', swarmer: 'rgba(50,255,50,0.95)', colossus: 'rgba(180,50,255,0.95)', dinosaur: 'rgba(30,30,30,0.95)' };\n                radarCtx.fillStyle = typeColors[m.type] || 'rgba(255,50,50,0.95)';\n                radarCtx.shadowColor = 'red'; radarCtx.shadowBlur = 4;\n                const dotSize = m.type === 'titan' ? 8 : (m.type === 'flyer' ? 5 : 6);\n                radarCtx.beginPath(); radarCtx.arc(rx, ry, dotSize, 0, Math.PI*2); radarCtx.fill();\n                // Flyer marker: triangle\n                if (m.type === 'flyer') {\n                    radarCtx.strokeStyle = 'rgba(50,150,255,0.9)';\n                    radarCtx.lineWidth = 1.5;\n                    radarCtx.beginPath(); radarCtx.moveTo(rx, ry-7); radarCtx.lineTo(rx-5, ry+4); radarCtx.lineTo(rx+5, ry+4); radarCtx.closePath(); radarCtx.stroke();\n                }\n                radarCtx.shadowBlur = 0;\n            }\n            radarCtx.fillStyle = 'rgba(0,255,80,0.8)';\n            radarCtx.font = '10px Orbitron';\n            radarCtx.textAlign = 'center';\n            radarCtx.fillText('Enemies: ' + monsters.filter(m=>m.alive).length, cx, RADAR_SIZE - 6);\n            // Draw homing missiles on radar as green dots\n            for (let i = 0; i < homingMissiles.length; i++) {\n                const hm = homingMissiles[i];\n                const dx = hm.mesh.position.x - hx;\n                const dz = hm.mesh.position.z - hz;\n                const dist = Math.sqrt(dx*dx + dz*dz);\n                if (dist > RADAR_RANGE) continue;\n                const scale = 60 / RADAR_RANGE;\n                const rx = cx + dx * scale;\n                const ry = cy + dz * scale;\n                radarCtx.fillStyle = 'rgba(0,255,80,1)';\n                radarCtx.shadowColor = '#00ff50'; radarCtx.shadowBlur = 6;\n                radarCtx.beginPath(); radarCtx.arc(rx, ry, 3, 0, Math.PI*2); radarCtx.fill();\n                radarCtx.shadowBlur = 0;\n            }\n            // Draw pedestrians on radar\n            for (let i = 0; i < pedestrians.length; i++) {\n                const p = pedestrians[i];\n                if (!p.alive) continue;\n                const dx = p.group.position.x - hx;\n                const dz = p.group.position.z - hz;\n                const dist = Math.sqrt(dx*dx + dz*dz);\n                if (dist > RADAR_RANGE) continue;\n                const scale = 60 / RADAR_RANGE;\n                const rx = cx + dx * scale;\n                const ry = cy + dz * scale;\n                radarCtx.fillStyle = p.fleeing ? 'rgba(255,255,0,0.8)' : 'rgba(0,200,255,0.7)';\n                radarCtx.beginPath(); radarCtx.arc(rx, ry, 2.5, 0, Math.PI*2); radarCtx.fill();\n            }\n            // Draw vehicles on radar\n            for (let i = 0; i < vehicles.length; i++) {\n                const v = vehicles[i];\n                if (!v.alive) continue;\n                const dx = v.group.position.x - hx;\n                const dz = v.group.position.z - hz;\n                const dist = Math.sqrt(dx*dx + dz*dz);\n                if (dist > RADAR_RANGE) continue;\n                const scale = 60 / RADAR_RANGE;\n                const rx = cx + dx * scale;\n                const ry = cy + dz * scale;\n                radarCtx.fillStyle = v.fleeing ? 'rgba(255,200,0,0.8)' : 'rgba(100,255,100,0.7)';\n                radarCtx.fillRect(rx - 3, ry - 2, 6, 4);\n            }\n        }\n\n        requestAnimationFrame(gameLoop);\n    }\n    </script>\n</body>\n</html>", "game_config": "{\"settings\":{\"waves\":20,\"monstersPerWave\":5,\"monsterSpeed\":0.5,\"playerHealth\":200,\"ammoCapacity\":450,\"cityLightColor\":\"#fff4e0\",\"fogDensity\":0.006,\"monsterColor\":\"#8844aa\",\"sfxVolume\":0.9,\"musicVolume\":0.6,\"evacTime\":300,\"maxCiviliansLost\":15,\"firstWaveMonsterMult\":0.7}}", "game_design_document": "## Core Game Loop and Mechanics\n\n**\u90fd\u5e02\u72e9\u730e\u8005**\u662f\u4e00\u6b3e\u7b2c\u4e00\u4eba\u79f03D\u72d9\u51fb\u5c04\u51fb\u6e38\u620f\uff0c\u73a9\u5bb6\u626e\u6f14\u57ce\u5e02\u5b88\u62a4\u8005\uff0c\u9700\u8981\u5728\u6469\u767b\u90fd\u5e02\u7684\u5236\u9ad8\u70b9\u6d88\u706d\u5165\u4fb5\u7684\u53d8\u5f02\u602a\u7269\u3002\n\n### \u6838\u5fc3\u5faa\u73af\uff1a\n1. **\u89c2\u5bdf\u4e0e\u5b9a\u4f4d** - \u73a9\u5bb6\u4ece\u72d9\u51fb\u70b9\u89c2\u5bdf\u57ce\u5e02\u8857\u9053\uff0c\u5bfb\u627e\u51fa\u73b0\u7684\u602a\u7269\u76ee\u6807\n2. **\u7784\u51c6\u4e0e\u5c04\u51fb** - \u4f7f\u7528\u72d9\u51fb\u955c\u7cbe\u51c6\u7784\u51c6\u602a\u7269\u7684\u5f31\u70b9\uff08\u5934\u90e8\uff09\uff0c\u4e00\u51fb\u6bd9\u547d\n3. **\u8bc4\u5206\u4e0e\u5956\u52b1** - \u6839\u636e\u5c04\u51fb\u7cbe\u51c6\u5ea6\u3001\u901f\u5ea6\u548c\u8fde\u51fb\u83b7\u5f97\u5206\u6570\n4. **\u6ce2\u6b21\u63a8\u8fdb** - \u6bcf\u6ce2\u602a\u7269\u88ab\u6e05\u9664\u540e\uff0c\u8fdb\u5165\u4e0b\u4e00\u6ce2\uff0c\u96be\u5ea6\u9010\u6e10\u589e\u52a0\n\n### \u6e38\u620f\u673a\u5236\uff1a\n- **\u7cbe\u51c6\u5c04\u51fb\u7cfb\u7edf**\uff1a\u73a9\u5bb6\u9700\u8981\u7784\u51c6\u602a\u7269\u7684\u5934\u90e8\u624d\u80fd\u4e00\u51fb\u6bd9\u547d\uff0c\u8eab\u4f53\u5c04\u51fb\u9700\u8981\u591a\u6b21\u547d\u4e2d\n- **\u5f39\u836f\u7ba1\u7406**\uff1a\u72d9\u51fb\u67aa\u5f39\u836f\u6709\u9650\uff0c\u9700\u8981\u8c28\u614e\u5c04\u51fb\uff0c\u6253\u7a7a\u540e\u9700\u8981\u88c5\u586b\n- **\u6ce2\u6b21\u7cfb\u7edf**\uff1a\u602a\u7269\u5206\u6ce2\u6b21\u51fa\u73b0\uff0c\u6bcf\u6ce2\u6570\u91cf\u548c\u79cd\u7c7b\u9012\u589e\n- **\u8fde\u51fb\u5956\u52b1**\uff1a\u8fde\u7eed\u547d\u4e2d\u53ef\u83b7\u5f97\u5206\u6570\u52a0\u6210\n- **\u751f\u547d\u503c\u7cfb\u7edf**\uff1a\u602a\u7269\u63a5\u8fd1\u73a9\u5bb6\u4f4d\u7f6e\u4f1a\u9020\u6210\u4f24\u5bb3\uff0c\u751f\u547d\u503c\u5f52\u96f6\u6e38\u620f\u7ed3\u675f\n\n### \u80dc\u5229\u6761\u4ef6\uff1a\n- \u6210\u529f\u6e05\u9664\u6240\u6709\u6ce2\u6b21\u7684\u602a\u7269\n\n### \u5931\u8d25\u6761\u4ef6\uff1a\n- \u73a9\u5bb6\u751f\u547d\u503c\u964d\u81f3\u96f6\n- \u602a\u7269\u6570\u91cf\u8fc7\u591a\u5360\u9886\u57ce\u5e02\n\n### \u91cd\u73a9\u6027\uff1a\n- \u8ffd\u6c42\u66f4\u9ad8\u5206\u6570\u548c\u66f4\u5feb\u901a\u5173\u65f6\u95f4\n- \u6311\u6218\u66f4\u591a\u6ce2\u6b21\n- \u63d0\u5347\u5c04\u51fb\u7cbe\u51c6\u5ea6\n\n## Starting Scope\n\n\u7b2c\u4e00\u7248\u5b9e\u73b0\u5e94\u8be5\u4e13\u6ce8\u4e8e\u6838\u5fc3\u72d9\u51fb\u4f53\u9a8c\uff0c\u4fdd\u6301\u7b80\u6d01\u4f46\u9ad8\u5ea6\u6253\u78e8\uff1a\n\n### \u5fc5\u987b\u5b9e\u73b0\uff08\u7b2c\u4e00\u7248\uff09\uff1a\n- **3D\u57ce\u5e02\u73af\u5883**\uff1a\u4e00\u4e2a\u56fa\u5b9a\u7684\u72d9\u51fb\u70b9\u4f4d\u7f6e\uff08\u9ad8\u697c\u5929\u53f0\uff09\uff0c\u4fef\u77b0\u57ce\u5e02\u8857\u9053\n- **\u7b2c\u4e00\u4eba\u79f0\u89c6\u89d2**\uff1a\u5e26\u72d9\u51fb\u955c\u7784\u51c6\u529f\u80fd\n- **\u57fa\u7840\u602a\u7269\u7c7b\u578b**\uff1a\u4e00\u79cd\u53d8\u5f02\u602a\u7269\uff0c\u5728\u8857\u9053\u4e0a\u968f\u673a\u751f\u6210\u5e76\u5411\u73a9\u5bb6\u65b9\u5411\u79fb\u52a8\n- **\u5c04\u51fb\u673a\u5236**\uff1a\u70b9\u51fb\u5c04\u51fb\uff0c\u547d\u4e2d\u5224\u5b9a\uff08\u5934\u90e8/\u8eab\u4f53\uff09\uff0c\u540e\u5750\u529b\u6548\u679c\n- **\u6ce2\u6b21\u7cfb\u7edf**\uff1a3-5\u6ce2\u602a\u7269\uff0c\u6bcf\u6ce2\u6570\u91cf\u9012\u589e\n- **UI\u7cfb\u7edf**\uff1a\u5f39\u836f\u8ba1\u6570\u3001\u751f\u547d\u503c\u3001\u5f53\u524d\u6ce2\u6b21\u3001\u5206\u6570\u663e\u793a\n- **\u97f3\u6548**\uff1a\u67aa\u58f0\u3001\u547d\u4e2d\u97f3\u6548\u3001\u602a\u7269\u5486\u54ee\u58f0\n- **\u6e38\u620f\u72b6\u6001**\uff1a\u5f00\u59cb\u754c\u9762\u3001\u6e38\u620f\u8fdb\u884c\u3001\u80dc\u5229/\u5931\u8d25\u754c\u9762\n\n### \u540e\u7eed\u8fed\u4ee3\u53ef\u6dfb\u52a0\uff1a\n- \u591a\u4e2a\u72d9\u51fb\u70b9\u4f4d\u7f6e\u53ef\u9009\u62e9\n- \u66f4\u591a\u602a\u7269\u7c7b\u578b\uff08\u98de\u884c\u602a\u7269\u3001\u88c5\u7532\u602a\u7269\u7b49\uff09\n- \u6b66\u5668\u5347\u7ea7\u7cfb\u7edf\n- \u5929\u6c14\u548c\u663c\u591c\u53d8\u5316\n- \u66f4\u590d\u6742\u7684\u57ce\u5e02\u5730\u56fe\n- \u6162\u52a8\u4f5c\u7279\u6548\n- \u6210\u5c31\u7cfb\u7edf\n\n## Game Perspective\n\n**3D\u7b2c\u4e00\u4eba\u79f0\u89c6\u89d2 (3d-first-person)**\n\n\u73a9\u5bb6\u4ece\u72d9\u51fb\u624b\u7684\u7b2c\u4e00\u4eba\u79f0\u89c6\u89d2\u89c2\u5bdf\u57ce\u5e02\uff0c\u4f7f\u7528\u9f20\u6807\u63a7\u5236\u89c6\u89d2\u548c\u7784\u51c6\u3002\u8fd9\u79cd\u89c6\u89d2\u6700\u9002\u5408\u72d9\u51fb\u5c04\u51fb\u6e38\u620f\uff0c\u80fd\u63d0\u4f9b\u6c89\u6d78\u611f\u548c\u7cbe\u51c6\u7784\u51c6\u4f53\u9a8c\u3002\n\n### \u89c6\u89d2\u7279\u70b9\uff1a\n- \u73a9\u5bb6\u4f4d\u4e8e\u56fa\u5b9a\u7684\u9ad8\u697c\u5929\u53f0\u72d9\u51fb\u70b9\n- \u53ef\u4ee5360\u5ea6\u73af\u89c6\u57ce\u5e02\u73af\u5883\n- \u72d9\u51fb\u955c\u6a21\u5f0f\u4e0b\u89c6\u91ce\u7f29\u5c0f\uff0c\u63d0\u4f9b\u653e\u5927\u7784\u51c6\n- \u4f7f\u7528Three.js\u7684PerspectiveCamera\u5b9e\u73b0\n\n## Game World and Level Design\n\n### \u57ce\u5e02\u73af\u5883\uff1a\n\u6e38\u620f\u573a\u666f\u662f\u4e00\u4e2a\u672a\u6765\u611f\u7684\u6469\u767b\u90fd\u5e02\u591c\u666f\uff1a\n\n- **\u72d9\u51fb\u70b9**\uff1a\u73a9\u5bb6\u7ad9\u5728\u4e00\u680b\u9ad8\u697c\u7684\u5929\u53f0\u8fb9\u7f18\uff08\u9ad8\u5ea6\u7ea650\u7c73\uff09\uff0c\u5468\u56f4\u6709\u62a4\u680f\u548c\u4e00\u4e9b\u57ce\u5e02\u8bbe\u65bd\uff08\u6c34\u7bb1\u3001\u901a\u98ce\u7ba1\u9053\u7b49\uff09\u4f5c\u4e3a\u89c6\u89c9\u5143\u7d20\n- **\u57ce\u5e02\u8857\u9053**\uff1a\u4e0b\u65b9\u662f\u5bbd\u9614\u7684\u57ce\u5e02\u8857\u9053\uff0c\u6709\u591a\u6761\u9053\u8def\u4ea4\u53c9\uff0c\u5f62\u6210\u5341\u5b57\u8def\u53e3\u6216\u5e7f\u573a\n- **\u5efa\u7b51\u7fa4**\uff1a\u5468\u56f4\u73af\u7ed5\u7740\u5404\u79cd\u9ad8\u5ea6\u7684\u73b0\u4ee3\u5efa\u7b51\uff08\u6469\u5929\u5927\u697c\u3001\u5546\u4e1a\u5927\u53a6\uff09\uff0c\u8425\u9020\u90fd\u5e02\u5929\u9645\u7ebf\n- **\u5730\u9762\u7ec6\u8282**\uff1a\u8857\u9053\u4e0a\u6709\u8def\u706f\u3001\u8f66\u8f86\u6b8b\u9ab8\u3001\u8def\u969c\u7b49\u5143\u7d20\uff0c\u589e\u52a0\u771f\u5b9e\u611f\n- **\u7167\u660e**\uff1a\u591c\u666f\u73af\u5883\uff0c\u9713\u8679\u706f\u5149\u3001\u8def\u706f\u548c\u5efa\u7b51\u706f\u5149\u8425\u9020\u8d5b\u535a\u670b\u514b\u6c1b\u56f4\n\n### \u602a\u7269\u751f\u6210\u673a\u5236\uff1a\n- \u602a\u7269\u4ece\u8857\u9053\u8fb9\u7f18\u7684\u591a\u4e2a\u751f\u6210\u70b9\u968f\u673a\u51fa\u73b0\n- \u751f\u6210\u540e\u6cbf\u7740\u8857\u9053\u5411\u73a9\u5bb6\u6240\u5728\u5efa\u7b51\u65b9\u5411\u79fb\u52a8\n- \u79fb\u52a8\u901f\u5ea6\u968f\u6ce2\u6b21\u589e\u52a0\u800c\u52a0\u5feb\n- \u602a\u7269\u5230\u8fbe\u5efa\u7b51\u5e95\u90e8\u4f1a\u5bf9\u73a9\u5bb6\u9020\u6210\u4f24\u5bb3\n\n### \u7a7a\u95f4\u5e03\u5c40\uff1a\n```\n        [\u73a9\u5bb6\u72d9\u51fb\u70b9 - \u9ad8\u697c\u5929\u53f0]\n                 |\n                 | 50\u7c73\u9ad8\u5ea6\n                 |\n        [\u57ce\u5e02\u8857\u9053 - \u602a\u7269\u6d3b\u52a8\u533a\u57df]\n     \u751f\u6210\u70b9A \u2190 \u2192 \u5efa\u7b51\u5e95\u90e8 \u2190 \u2192 \u751f\u6210\u70b9B\n                 \u2191\n            \u751f\u6210\u70b9C/D\n```\n\n### \u8ddd\u79bb\u8bbe\u8ba1\uff1a\n- \u72d9\u51fb\u70b9\u5230\u8857\u9053\u4e2d\u5fc3\uff1a\u7ea650-80\u7c73\n- \u602a\u7269\u751f\u6210\u70b9\u5230\u5efa\u7b51\u5e95\u90e8\uff1a\u7ea6100-150\u7c73\n- \u8fd9\u4e2a\u8ddd\u79bb\u65e2\u80fd\u8ba9\u73a9\u5bb6\u6709\u8db3\u591f\u53cd\u5e94\u65f6\u95f4\uff0c\u53c8\u80fd\u4fdd\u6301\u7d27\u5f20\u611f\n\n## Edit Mode Design\n\nEdit\u6a21\u5f0f\u5141\u8bb8\u73a9\u5bb6\u81ea\u5b9a\u4e49\u6e38\u620f\u96be\u5ea6\u548c\u89c6\u89c9\u6548\u679c\uff1a\n\n### \u53ef\u7f16\u8f91\u5c5e\u6027\uff1a\n\n**\u6e38\u620f\u96be\u5ea6\u8bbe\u7f6e\uff1a**\n- `waves`\uff1a\u6ce2\u6b21\u6570\u91cf\uff083-10\u6ce2\uff09\n- `monstersPerWave`\uff1a\u6bcf\u6ce2\u602a\u7269\u57fa\u7840\u6570\u91cf\uff083-15\u53ea\uff09\n- `monsterSpeed`\uff1a\u602a\u7269\u79fb\u52a8\u901f\u5ea6\u500d\u7387\uff080.5-2.0\uff09\n- `playerHealth`\uff1a\u73a9\u5bb6\u521d\u59cb\u751f\u547d\u503c\uff0850-200\uff09\n- `ammoCapacity`\uff1a\u5f39\u836f\u5bb9\u91cf\uff0810-50\u53d1\uff09\n\n**\u89c6\u89c9\u8bbe\u7f6e\uff1a**\n- `cityLightColor`\uff1a\u57ce\u5e02\u706f\u5149\u4e3b\u8272\u8c03\uff08\u989c\u8272\u9009\u62e9\u5668\uff09\n- `fogDensity`\uff1a\u96fe\u6c14\u6d53\u5ea6\uff080-0.01\uff0c\u5f71\u54cd\u53ef\u89c1\u8ddd\u79bb\uff09\n- `monsterColor`\uff1a\u602a\u7269\u4e3b\u4f53\u989c\u8272\uff08\u989c\u8272\u9009\u62e9\u5668\uff09\n\n**\u97f3\u6548\u8bbe\u7f6e\uff1a**\n- `sfxVolume`\uff1a\u97f3\u6548\u97f3\u91cf\uff080-1\uff09\n- `musicVolume`\uff1a\u80cc\u666f\u97f3\u4e50\u97f3\u91cf\uff080-1\uff09\n\n### \u7ea6\u675f\u6761\u4ef6\uff1a\n- \u6ce2\u6b21\u6570\u91cf\u6700\u5c113\u6ce2\uff0c\u6700\u591a10\u6ce2\uff08\u4fdd\u8bc1\u6e38\u620f\u65f6\u957f\u5408\u7406\uff09\n- \u602a\u7269\u901f\u5ea6\u4e0d\u80fd\u4f4e\u4e8e0.5\u500d\uff08\u592a\u6162\u4f1a\u65e0\u804a\uff09\u6216\u9ad8\u4e8e2\u500d\uff08\u592a\u5feb\u65e0\u6cd5\u7784\u51c6\uff09\n- \u751f\u547d\u503c\u81f3\u5c1150\uff08\u4fdd\u8bc1\u57fa\u672c\u751f\u5b58\u80fd\u529b\uff09\n- \u5f39\u836f\u5bb9\u91cf\u81f3\u5c1110\u53d1\uff08\u4fdd\u8bc1\u57fa\u672c\u6e38\u620f\u6027\uff09\n\n### Edit\u6a21\u5f0f\u754c\u9762\uff1a\n- \u4f7f\u7528\u4fa7\u8fb9\u680f\u6ed1\u5757\u548c\u989c\u8272\u9009\u62e9\u5668\n- \u5b9e\u65f6\u9884\u89c8\u6548\u679c\uff08\u989c\u8272\u53d8\u5316\u7acb\u5373\u53cd\u6620\u5728\u573a\u666f\u4e2d\uff09\n- \"\u91cd\u7f6e\u4e3a\u9ed8\u8ba4\"\u6309\u94ae\u6062\u590d\u521d\u59cb\u8bbe\u7f6e\n- \"\u5f00\u59cb\u6e38\u620f\"\u6309\u94ae\u5e94\u7528\u8bbe\u7f6e\u5e76\u8fdb\u5165Play\u6a21\u5f0f\n\n## Control Design\n\n### \u684c\u9762\u7aef\uff08Desktop\uff09\uff1a\n- **\u9f20\u6807\u79fb\u52a8**\uff1a\u63a7\u5236\u89c6\u89d2/\u7784\u51c6\u65b9\u5411\uff08\u4f7f\u7528Pointer Lock API\uff09\n- **\u9f20\u6807\u5de6\u952e**\uff1a\u5c04\u51fb\n- **\u9f20\u6807\u53f3\u952e**\uff1a\u8fdb\u5165/\u9000\u51fa\u72d9\u51fb\u955c\u6a21\u5f0f\uff08\u653e\u5927\u7784\u51c6\uff09\n- **R\u952e**\uff1a\u624b\u52a8\u88c5\u586b\u5f39\u836f\n- **ESC\u952e**\uff1a\u6682\u505c\u6e38\u620f/\u91ca\u653e\u9f20\u6807\u9501\u5b9a\n\n### \u79fb\u52a8\u7aef\uff08Mobile\uff09\uff1a\n- **\u53f3\u4fa7\u865a\u62df\u6447\u6746**\uff1a\u63a7\u5236\u89c6\u89d2/\u7784\u51c6\u65b9\u5411\uff08\u62d6\u52a8\u6539\u53d8\u89c6\u89d2\uff09\n- **\u5de6\u4e0b\u89d2\u5c04\u51fb\u6309\u94ae**\uff1a\u5927\u578b\u89e6\u6478\u6309\u94ae\uff0c\u70b9\u51fb\u5c04\u51fb\n- **\u53f3\u4e0b\u89d2\u7784\u51c6\u955c\u6309\u94ae**\uff1a\u5207\u6362\u72d9\u51fb\u955c\u6a21\u5f0f\n- **\u5de6\u4e0a\u89d2\u88c5\u586b\u6309\u94ae**\uff1a\u624b\u52a8\u88c5\u586b\u5f39\u836f\n- **\u6682\u505c\u6309\u94ae**\uff1a\u53f3\u4e0a\u89d2\uff0c\u6682\u505c\u6e38\u620f\n\n### \u63a7\u5236\u7ec6\u8282\uff1a\n\n**\u89c6\u89d2\u63a7\u5236\uff1a**\n- \u684c\u9762\u7aef\u4f7f\u7528Pointer Lock\u5b9e\u73b0\u65e0\u9650\u9f20\u6807\u79fb\u52a8\n- \u79fb\u52a8\u7aef\u4f7f\u7528\u89e6\u6478\u62d6\u52a8\uff0c\u7075\u654f\u5ea6\u9002\u4e2d\n- \u5782\u76f4\u89c6\u89d2\u9650\u5236\u5728-60\u00b0\u5230+30\u00b0\u4e4b\u95f4\uff08\u9632\u6b62\u8fc7\u5ea6\u4ef0\u4fef\uff09\n- \u6c34\u5e73\u89c6\u89d2\u53ef360\u00b0\u65cb\u8f6c\n\n**\u72d9\u51fb\u955c\u6a21\u5f0f\uff1a**\n- \u8fdb\u5165\u72d9\u51fb\u955c\u540eFOV\u7f29\u5c0f\uff08\u4ece75\u964d\u81f330\uff09\n- \u663e\u793a\u5341\u5b57\u51c6\u661f\n- \u79fb\u52a8\u7075\u654f\u5ea6\u964d\u4f4e50%\uff08\u66f4\u7cbe\u51c6\u7784\u51c6\uff09\n- \u8fb9\u7f18\u6709\u72d9\u51fb\u955c\u9ed1\u8272\u906e\u7f69\u6548\u679c\n\n**\u5c04\u51fb\u53cd\u9988\uff1a**\n- \u5c04\u51fb\u540e\u6709\u540e\u5750\u529b\u6548\u679c\uff08\u89c6\u89d2\u8f7b\u5fae\u4e0a\u626c\uff09\n- \u5c04\u51fb\u95f4\u9694\u7ea60.8\u79d2\uff08\u9632\u6b62\u8fde\u70b9\uff09\n- \u5f39\u836f\u8017\u5c3d\u65f6\u70b9\u51fb\u5c04\u51fb\u64ad\u653e\u7a7a\u67aa\u97f3\u6548\n\n**\u8f93\u5165\u5904\u7406\uff1a**\n- \u8fdb\u5165\u6e38\u620f\u65f6\u81ea\u52a8\u8bf7\u6c42Pointer Lock\n- \u6253\u5f00\u83dc\u5355/\u6682\u505c\u65f6\u91ca\u653ePointer Lock\n- \u9632\u6b62\u6d4f\u89c8\u5668\u9ed8\u8ba4\u884c\u4e3a\uff08\u53f3\u952e\u83dc\u5355\u7b49\uff09\n- \u7a97\u53e3\u5931\u7126\u65f6\u81ea\u52a8\u6682\u505c\u6e38\u620f\n\n## 3D Graphics Implementation\n\n### \u4f7f\u7528Three.js\u5b9e\u73b03D\u573a\u666f\uff1a\n\n**\u573a\u666f\u8bbe\u7f6e\uff1a**\n- \u4f7f\u7528Three.js WebGL\u6e32\u67d3\u5668\n- PerspectiveCamera\uff08FOV: 75\u00b0\uff0c\u8fd1\u88c1\u526a\u9762: 0.1\uff0c\u8fdc\u88c1\u526a\u9762: 1000\uff09\n- \u73af\u5883\u5149 + \u591a\u4e2a\u70b9\u5149\u6e90\u6a21\u62df\u57ce\u5e02\u706f\u5149\n- \u96fe\u6548\u679c\u589e\u5f3a\u8ddd\u79bb\u611f\u548c\u6c1b\u56f4\n\n**3D\u6a21\u578b\u7b56\u7565\uff1a**\n- **\u5efa\u7b51\u7269**\uff1a\u4f7f\u7528BoxGeometry\u521b\u5efa\u7b80\u5355\u7684\u7acb\u65b9\u4f53\u5efa\u7b51\uff0c\u5e94\u7528\u4e0d\u540c\u6750\u8d28\u548c\u8d34\u56fe\n- **\u602a\u7269**\uff1a\u4f7f\u7528\u751f\u6210\u76842D\u56fe\u50cf\u4f5c\u4e3a\u7cbe\u7075\uff08Sprite\uff09\uff0c\u59cb\u7ec8\u9762\u5411\u6444\u50cf\u673a\uff08Billboard\u6548\u679c\uff09\n- **\u5730\u9762/\u8857\u9053**\uff1aPlaneGeometry + \u7eb9\u7406\u8d34\u56fe\n- **\u88c5\u9970\u5143\u7d20**\uff1a\u7b80\u5355\u51e0\u4f55\u4f53\u7ec4\u5408\uff08\u62a4\u680f\u3001\u8def\u706f\u7b49\uff09\n\n**\u6027\u80fd\u4f18\u5316\uff1a**\n- \u4f7f\u7528InstancedMesh\u6279\u91cf\u6e32\u67d3\u76f8\u4f3c\u5efa\u7b51\n- \u8fdc\u5904\u5efa\u7b51\u4f7f\u7528LOD\uff08\u7ec6\u8282\u5c42\u6b21\uff09\u964d\u4f4e\u590d\u6742\u5ea6\n- \u9650\u5236\u540c\u65f6\u5b58\u5728\u7684\u602a\u7269\u6570\u91cf\uff08\u6700\u591a20\u53ea\uff09\n- \u4f7f\u7528\u5bf9\u8c61\u6c60\u590d\u7528\u602a\u7269\u5bf9\u8c61\n\n**\u7269\u7406\u7cfb\u7edf\uff1a**\n- \u7b80\u5355\u7684\u5c04\u7ebf\u6295\u5c04\u68c0\u6d4b\u547d\u4e2d\uff08Raycaster\uff09\n- \u602a\u7269\u4f7f\u7528\u7b80\u5355\u7684\u76f4\u7ebf\u79fb\u52a8\uff08\u4e0d\u9700\u8981\u590d\u6742\u7269\u7406\u5f15\u64ce\uff09\n- \u78b0\u649e\u68c0\u6d4b\u57fa\u4e8e\u8ddd\u79bb\u8ba1\u7b97\n\n## Art Style and Visual Theme\n\n### \u6574\u4f53\u98ce\u683c\uff1a\n**\u8d5b\u535a\u670b\u514b\u672a\u6765\u90fd\u5e02 + \u9ed1\u6697\u79d1\u5e7b**\n\n\u6e38\u620f\u91c7\u7528\u73b0\u4ee3\u611f\u5f3a\u70c8\u7684\u8d5b\u535a\u670b\u514b\u7f8e\u5b66\uff0c\u7ed3\u5408\u9ed1\u6697\u79d1\u5e7b\u5143\u7d20\uff1a\n\n- **\u8272\u8c03**\uff1a\u4ee5\u6df1\u84dd\u3001\u7d2b\u8272\u3001\u9713\u8679\u7c89\u4e3a\u4e3b\u8272\u8c03\uff0c\u8425\u9020\u591c\u665a\u90fd\u5e02\u6c1b\u56f4\n- **\u5149\u7167**\uff1a\u5f3a\u70c8\u7684\u9713\u8679\u706f\u5149\u5bf9\u6bd4\uff0c\u6697\u90e8\u6df1\u9083\uff0c\u4eae\u90e8\u9c9c\u8273\n- **\u6750\u8d28**\uff1a\u91d1\u5c5e\u3001\u73bb\u7483\u3001\u6df7\u51dd\u571f\u7b49\u73b0\u4ee3\u5efa\u7b51\u6750\u8d28\n- **\u6c1b\u56f4**\uff1a\u795e\u79d8\u3001\u7d27\u5f20\u3001\u79d1\u6280\u611f\n\n### \u89c6\u89c9\u5143\u7d20\uff1a\n\n**\u57ce\u5e02\u5efa\u7b51\uff1a**\n- \u73b0\u4ee3\u6469\u5929\u5927\u697c\uff0c\u7b80\u6d01\u7684\u51e0\u4f55\u9020\u578b\n- \u73bb\u7483\u5e55\u5899\u53cd\u5c04\u9713\u8679\u706f\u5149\n- \u5efa\u7b51\u9876\u90e8\u6709\u53d1\u5149\u7684\u6807\u5fd7\u548c\u5929\u7ebf\n- \u4f7f\u7528\u7a0b\u5e8f\u5316\u751f\u6210\u7684\u7a97\u6237\u706f\u5149\uff08\u90e8\u5206\u7a97\u6237\u4eae\u8d77\uff09\n\n**\u602a\u7269\u8bbe\u8ba1\uff1a**\n- \u53d8\u5f02\u751f\u7269\u5916\u5f62\uff0c\u878d\u5408\u6709\u673a\u548c\u673a\u68b0\u5143\u7d20\n- \u53d1\u5149\u7684\u773c\u775b\u6216\u8eab\u4f53\u90e8\u4f4d\uff08\u4fbf\u4e8e\u8bc6\u522b\uff09\n- \u4f53\u578b\u8f83\u5927\uff0c\u8f6e\u5ed3\u6e05\u6670\n- \u884c\u8d70\u65f6\u6709\u660e\u663e\u7684\u52a8\u753b\u6548\u679c\uff08\u7cbe\u7075\u5e27\u52a8\u753b\u6216\u7b80\u5355\u7684\u4e0a\u4e0b\u6d6e\u52a8\uff09\n\n**UI\u8bbe\u8ba1\uff1a**\n- \u79d1\u6280\u611fHUD\u754c\u9762\n- \u534a\u900f\u660e\u9762\u677f\uff0c\u8fb9\u7f18\u53d1\u5149\n- \u6570\u5b57\u5b57\u4f53\uff0c\u5e26\u626b\u63cf\u7ebf\u6548\u679c\n- \u51c6\u661f\u8bbe\u8ba1\u7b80\u6d01\u4f46\u9192\u76ee\n\n**\u7279\u6548\uff1a**\n- \u67aa\u53e3\u706b\u5149\uff08\u7c92\u5b50\u6548\u679c\uff09\n- \u547d\u4e2d\u706b\u82b1\uff08\u7c92\u5b50\u6548\u679c\uff09\n- \u602a\u7269\u6b7b\u4ea1\u6d88\u6563\u6548\u679c\uff08\u6de1\u51fa + \u7c92\u5b50\uff09\n- \u72d9\u51fb\u955c\u8fb9\u7f18\u6655\u5f71\u6548\u679c\n\n### \u8d44\u6e90\u89c4\u683c\uff1a\n\n**\u56fe\u50cf\u8d44\u6e90\uff1a**\n- \u602a\u7269\u7cbe\u7075\uff1a512x512px\uff0c\u900f\u660e\u80cc\u666fPNG\n- \u5efa\u7b51\u7eb9\u7406\uff1a512x512px\u62161024x1024px\n- UI\u5143\u7d20\uff1a\u9002\u914d720x1280\u5c4f\u5e55\n- \u51c6\u661f\uff1a64x64px\uff0c\u7b80\u5355\u5341\u5b57\u6216\u5706\u70b9\n\n**\u97f3\u9891\u8d44\u6e90\uff1a**\n- \u72d9\u51fb\u67aa\u5c04\u51fb\u97f3\u6548\uff1a\u91cd\u4f4e\u97f3\uff0c\u6709\u529b\u91cf\u611f\n- \u547d\u4e2d\u97f3\u6548\uff1a\u91d1\u5c5e\u649e\u51fb\u6216\u8089\u4f53\u649e\u51fb\u58f0\n- \u602a\u7269\u5486\u54ee\uff1a\u4f4e\u6c89\u3001\u5a01\u80c1\u6027\n- \u80cc\u666f\u97f3\u4e50\uff1a\u7535\u5b50\u97f3\u4e50\uff0c\u8282\u594f\u611f\u5f3a\uff0c\u5faa\u73af\u64ad\u653e\n\n### \u4e00\u81f4\u6027\u8981\u6c42\uff1a\n- \u6240\u6709\u5efa\u7b51\u4f7f\u7528\u76f8\u540c\u7684\u827a\u672f\u98ce\u683c\u548c\u8272\u8c03\n- \u602a\u7269\u8bbe\u8ba1\u4fdd\u6301\u7edf\u4e00\u7684\u53d8\u5f02\u751f\u7269\u7f8e\u5b66\n- UI\u5143\u7d20\u4f7f\u7528\u7edf\u4e00\u7684\u79d1\u6280\u98ce\u683c\n- \u97f3\u6548\u4fdd\u6301\u4e00\u81f4\u7684\u97f3\u8272\u548c\u8d28\u611f", "persistent_storage_config": {"leaderboard": {"sort_ascending": false}}, "game_metadata": {"game_id": "01KW3Q8VGA66C7VWDHNXSCEE6P", "title": "Urban Hunter", "description": "You will pilot a freely maneuverable armed helicopter to shield citizens fleeing from monster pursuit. The creatures will retreat back into the abyss after 20 days.", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_7b630411-460d-4715-8586-cb8a6b2aeb4e.webp", "created_at": 1782536500, "updated_at": 1786012122275.7246, "art_style": null, "is_remix": true, "base_game_id": "01KT3DFP5EBHZPJE5JQXCCHA8T"}, "task_plan": null}