{"asset_map": {"bgm_main": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/bgm_main_4572cf5b-7c19-440e-954b-c32bd1af08f5.mp3", "type": "audio", "prompt": "High-energy industrial metal mixed with horror ambiance for a zombie shooter. 150 BPM, aggressive distorted low-tuned guitar riffs, punishing industrial drum machine beats, and dark cinematic synth pads. Features occasional eerie choir stabs and deep sub-bass drops. Relentless and adrenaline-pumping atmosphere similar to DOOM (2016). Structure includes driving main sections and tense build-ups. Instrumental only, dark and intense mood, seamless loop.", "description": "High-energy industrial metal background music for the zombie shooter gameplay.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 90.04408163265306, "loop": true, "animation": null}, "muzzle_flash_radial_flipbook_2x4_256_clean.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_118012379502927147068/1780994359451_01KTNRJG4VDK9J05GED96PED59.muzzle_flash_radial_flipbook_2x4_256_clean.png", "type": "image", "prompt": null, "description": "Muzzle flash radial flipbook (2x4) PNG \u2014 clean frames on transparent background for weapon muzzle effects.", "subtype": null, "aspect_ratio": [1024, 512], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "weapon_icons_atlas.png": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_118012379502927147068/1781196083808_01KTVRYMK0VHMDZZXX0XSRVEQA.weapon_icons_atlas.png", "type": "image", "prompt": null, "description": "Weapon icons sprite atlas containing 7 weapon silhouettes in a horizontal strip: pistol, shotgun, sniper, katana, gatling gun, LMG, assault rifle. Used for weapon selection UI and HUD slots.", "subtype": null, "aspect_ratio": [3584, 512], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "sfx_sprite.mp3": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_118012379502927147068/1781194858311_01KTVQS7T6PMKMBQTCK2WAFRQ5.sfx_sprite.mp3", "type": "audio", "prompt": null, "description": "Audio sprite sheet containing all game sound effects in a single MP3 file: gunshots, reloads, zombie sounds, UI sounds, etc. Referenced by SFX_SPRITE_MAP offsets in the game code.", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "flame.webp": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_118012379502927147068/1782236241413_01KVTRXQG5ADQBSCVN50862EYS.flame.webp", "type": "image", "prompt": null, "description": "A realistic flame texture for flamethrower particles.", "subtype": null, "aspect_ratio": [256, 256], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "weapon_icon_flamethrower.png": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/weapon_icon_flamethrower_png_226d2260-fd3c-4470-9b30-38a459ba553a.webp", "type": "image", "prompt": "Generate a sleek, high-tech flamethrower weapon silhouette icon. Pure white minimalist silhouette on a transparent background. Side-view perspective, clean sharp edges, showing distinct details of a fuel tank, grip, trigger, and nozzle. Tactical shooter HUD style, matching the aesthetic of weapon silhouettes in image 1. Professional game UI asset, high contrast, no internal shading.", "description": "A tactical high-tech flamethrower silhouette icon for the weapon selection UI.", "subtype": "ui", "aspect_ratio": [1413, 408], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "sfx_flamethrower_loop": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/sfx_flamethrower_loop_3043d8a8-5ed6-4fd0-a6d3-1cd6867d5aa0.mp3", "type": "audio", "prompt": "Continuous roaring fire loop sound effect for a tactical flamethrower. Powerful, persistent burning roar with a warm low-end rumble and sharp hissing gas pressure. High-quality, clean sound profile for a first-person shooter. No crackling wood, focus on pressurized liquid fuel ignition. Seamlessly loopable, consistent intensity, no fade out.", "description": "Continuous roaring fire loop for a tactical flamethrower weapon in an FPS context.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 10, "loop": true, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Zombie Last Survivor</title>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n    <link rel=\"preload\" as=\"style\"\n        href=\"https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@400;600;700&display=swap\"\n        onload=\"this.onload=null;this.rel='stylesheet'\">\n    <noscript>\n        <link rel=\"stylesheet\"\n            href=\"https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@400;600;700&display=swap\">\n    </noscript>\n    <style>\n        :root {\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --ease-out: cubic-bezier(0.0, 0, 0.2, 1);\n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n            --cyan: #00d9ff;\n            --dark-bg: rgba(10, 15, 25, 0.85);\n            --red: #ff3333;\n        }\n\n        body,\n        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        html.locale-font-fallback * {\n            font-family: 'Inter', 'Noto Sans', 'Noto Sans Arabic', 'Noto Sans Bengali',\n                'Noto Sans Devanagari', 'Noto Sans JP', 'Noto Sans Tamil',\n                'Noto Sans Telugu', sans-serif !important;\n            letter-spacing: 0 !important;\n        }\n\n        html[dir=\"rtl\"] .upgrade-option-info,\n        html[dir=\"rtl\"] .weapon-select-card,\n        html[dir=\"rtl\"] .stat-row {\n            direction: rtl;\n            text-align: right;\n        }\n        body.perf-pressure-2 .hud-overlay .crosshair::before,\n        body.perf-pressure-2 .hud-overlay .ch-arm,\n        body.perf-pressure-2 .hud-overlay .health-bar-container,\n        body.perf-pressure-2 .hud-overlay .wave-display,\n        body.perf-pressure-2 .hud-overlay .weapon-slot,\n        body.perf-pressure-2 .hud-overlay .reload-btn {\n            box-shadow: none !important;\n        }\n\n        body.perf-pressure-2 .hud-overlay .ammo-display,\n        body.perf-pressure-2 .hud-overlay .score-display,\n        body.perf-pressure-2 .hud-overlay #enemies-display,\n        body.perf-pressure-2 .hud-overlay .health-text {\n            text-shadow: none !important;\n        }\n\n        body.perf-pressure-2 .hud-overlay .combo-rank,\n        body.perf-pressure-2 .hud-overlay .hm-tri {\n            filter: none !important;\n        }\n\n        body.perf-pressure-3 .hud-overlay .combo-rank.rank-S,\n        body.perf-pressure-3 .hud-overlay .combo-rank.rank-SS,\n        body.perf-pressure-3 .hud-overlay .combo-rank.rank-SSS,\n        body.perf-pressure-3 .hud-overlay .ammo-display.low {\n            animation: none !important;\n        }\n\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n            touch-action: none;\n            -ms-touch-action: none;\n        }\n\n        canvas#game-canvas {\n            display: block;\n            width: 100%;\n            height: 100%;\n            touch-action: none;\n            -ms-touch-action: none;\n        }\n\n        /* HUD Overlay */\n        .hud-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: 5;\n            /* Reset to low z-index so pause/game-over screens properly cover and blur the HUD */\n        }\n\n        #game-world:has(.weapon-tutorial-overlay.active) .hud-overlay {\n            z-index: 120 !important;\n            /* Bring HUD to foreground during tutorial so highlighted slots are above spotlight */\n        }\n\n        /* Prevent HUD interactive elements from blocking clicks on menus/overlays */\n        #game-world:has(.pause-overlay.active) .hud-overlay,\n        #game-world:has(.game-over-screen.active) .hud-overlay,\n        #game-world:has(.weapon-select-screen.active) .hud-overlay,\n        #game-world:has(.upgrade-select-screen.active) .hud-overlay {\n            pointer-events: none !important;\n        }\n\n        #game-world:has(.pause-overlay.active) .hud-overlay *,\n        #game-world:has(.game-over-screen.active) .hud-overlay *,\n        #game-world:has(.weapon-select-screen.active) .hud-overlay *,\n        #game-world:has(.upgrade-select-screen.active) .hud-overlay * {\n            pointer-events: none !important;\n        }\n\n        .hud-overlay>* {\n            pointer-events: auto;\n        }\n\n        /* Crosshair - Overwatch style */\n        .crosshair {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            --gap: 5px;\n            --arm: 6px;\n            --thick: 2px;\n            --hit-scale: 1;\n            transform: translate(-50%, -50%) scale(var(--hit-scale));\n            width: 72px;\n            height: 72px;\n            pointer-events: none;\n            color: rgba(255, 255, 255, 0.95);\n            transition: transform 90ms ease-out, color 90ms ease-out;\n        }\n\n        /* Smart Pistol Brackets HUD */\n        .crosshair .sh-bracket-decor {\n            display: none;\n        }\n\n        .crosshair.smart-hud {\n            width: clamp(190px, 31vmin, 255px);\n            height: clamp(190px, 31vmin, 255px);\n            color: rgba(86, 232, 255, 0.72);\n            transition: color 150ms ease, width 0.25s ease, height 0.25s ease;\n            filter: drop-shadow(0 0 5px rgba(0, 217, 255, 0.32));\n        }\n\n        .crosshair.smart-hud.locking {\n            color: rgba(255, 178, 44, 0.85);\n            filter: drop-shadow(0 0 6px rgba(255, 176, 38, 0.42));\n        }\n\n        .crosshair.smart-hud.locked {\n            color: rgba(255, 54, 54, 0.95);\n            filter: drop-shadow(0 0 8px rgba(255, 50, 50, 0.65));\n        }\n\n        .crosshair.smart-hud::before {\n            content: '';\n            display: block;\n            position: absolute;\n            width: 160px;\n            height: 160px;\n            top: 0; bottom: 0; left: 0; right: 0;\n            margin: auto;\n            border-radius: 50%;\n            background:\n                conic-gradient(from 10deg,\n                    transparent 0deg 18deg,\n                    currentColor 18deg 20deg,\n                    transparent 20deg 70deg,\n                    currentColor 70deg 72deg,\n                    transparent 72deg 108deg,\n                    currentColor 108deg 110deg,\n                    transparent 110deg 160deg,\n                    currentColor 160deg 162deg,\n                    transparent 162deg 198deg,\n                    currentColor 198deg 200deg,\n                    transparent 200deg 250deg,\n                    currentColor 250deg 252deg,\n                    transparent 252deg 288deg,\n                    currentColor 288deg 290deg,\n                    transparent 290deg 340deg,\n                    currentColor 340deg 342deg,\n                    transparent 342deg 360deg);\n            -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 1.5px), #000 calc(100% - 1px));\n            mask: radial-gradient(farthest-side, transparent calc(100% - 1.5px), #000 calc(100% - 1px));\n            opacity: 0.42;\n            box-shadow: none;\n            animation: smartHudSweep 6s linear infinite;\n        }\n\n        .crosshair.smart-hud::after {\n            content: '';\n            display: block;\n            position: absolute;\n            width: 100px;\n            height: 100px;\n            top: 0; bottom: 0; left: 0; right: 0;\n            margin: auto;\n            border-radius: 50%;\n            border: 1px dashed currentColor;\n            opacity: 0.25;\n            box-shadow: 0 0 4px currentColor;\n            animation: smartHudSweep 16s linear infinite reverse;\n        }\n\n        .crosshair.smart-hud .ch-top,\n        .crosshair.smart-hud .ch-bottom,\n        .crosshair.smart-hud .ch-left,\n        .crosshair.smart-hud .ch-right {\n            display: block;\n            width: 2px;\n            height: 6px;\n            background: currentColor;\n            border: 0;\n            box-shadow: 0 0 3px currentColor;\n            border-radius: 1px;\n            opacity: 0.75;\n            position: absolute;\n            transition: all 0.2s ease;\n        }\n\n        .crosshair.smart-hud .ch-top {\n            top: calc(50% - 11px);\n            left: 50%;\n            transform: translateX(-50%);\n        }\n\n        .crosshair.smart-hud .ch-bottom {\n            top: calc(50% + 5px);\n            left: 50%;\n            transform: translateX(-50%);\n        }\n\n        .crosshair.smart-hud .ch-left {\n            width: 6px;\n            height: 2px;\n            top: 50%;\n            left: calc(50% - 11px);\n            transform: translateY(-50%);\n        }\n\n        .crosshair.smart-hud .ch-right {\n            width: 6px;\n            height: 2px;\n            top: 50%;\n            left: calc(50% + 5px);\n            transform: translateY(-50%);\n        }\n\n\n        /* Corner bracket frames */\n        .crosshair.smart-hud .sh-bracket-decor {\n            display: block;\n            position: absolute;\n            width: 28px;\n            height: 28px;\n            border: 1.5px solid transparent;\n            opacity: 0.85;\n            transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);\n            pointer-events: none;\n            box-sizing: border-box;\n        }\n\n        .crosshair.smart-hud .sh-bracket-decor.tl {\n            top: 0; left: 0;\n            border-top-color: currentColor;\n            border-left-color: currentColor;\n            border-radius: 4px 0 0 0;\n            transform: translate(0, 0);\n        }\n        .crosshair.smart-hud .sh-bracket-decor.tr {\n            top: 0; right: 0;\n            border-top-color: currentColor;\n            border-right-color: currentColor;\n            border-radius: 0 4px 0 0;\n            transform: translate(0, 0);\n        }\n        .crosshair.smart-hud .sh-bracket-decor.bl {\n            bottom: 0; left: 0;\n            border-bottom-color: currentColor;\n            border-left-color: currentColor;\n            border-radius: 0 0 0 4px;\n            transform: translate(0, 0);\n        }\n        .crosshair.smart-hud .sh-bracket-decor.br {\n            bottom: 0; right: 0;\n            border-bottom-color: currentColor;\n            border-right-color: currentColor;\n            border-radius: 0 0 4px 0;\n            transform: translate(0, 0);\n        }\n\n        /* Inner ticks for corners */\n        .crosshair.smart-hud .sh-bracket-decor::before {\n            content: '';\n            position: absolute;\n            width: 6px;\n            height: 6px;\n            border: 1px solid currentColor;\n            opacity: 0.35;\n        }\n        .crosshair.smart-hud .sh-bracket-decor.tl::before { top: 4px; left: 4px; border-bottom: none; border-right: none; }\n        .crosshair.smart-hud .sh-bracket-decor.tr::before { top: 4px; right: 4px; border-bottom: none; border-left: none; }\n        .crosshair.smart-hud .sh-bracket-decor.bl::before { bottom: 4px; left: 4px; border-top: none; border-right: none; }\n        .crosshair.smart-hud .sh-bracket-decor.br::before { bottom: 4px; right: 4px; border-top: none; border-left: none; }\n\n        /* Lock target focusing transition effects */\n        .crosshair.smart-hud.locking .sh-bracket-decor.tl { transform: translate(6px, 6px); }\n        .crosshair.smart-hud.locking .sh-bracket-decor.tr { transform: translate(-6px, 6px); }\n        .crosshair.smart-hud.locking .sh-bracket-decor.bl { transform: translate(6px, -6px); }\n        .crosshair.smart-hud.locking .sh-bracket-decor.br { transform: translate(-6px, -6px); }\n\n        .crosshair.smart-hud.locked .sh-bracket-decor.tl { transform: translate(3px, 3px) scale(0.98); animation: shLockedPulse 0.4s ease-in-out infinite alternate; }\n        .crosshair.smart-hud.locked .sh-bracket-decor.tr { transform: translate(-3px, 3px) scale(0.98); animation: shLockedPulse 0.4s ease-in-out infinite alternate; }\n        .crosshair.smart-hud.locked .sh-bracket-decor.bl { transform: translate(3px, -3px) scale(0.98); animation: shLockedPulse 0.4s ease-in-out infinite alternate; }\n        .crosshair.smart-hud.locked .sh-bracket-decor.br { transform: translate(-3px, -3px) scale(0.98); animation: shLockedPulse 0.4s ease-in-out infinite alternate; }\n\n        @keyframes shLockedPulse {\n            0% { filter: brightness(0.9) drop-shadow(0 0 2px currentColor); }\n            100% { filter: brightness(1.3) drop-shadow(0 0 6px currentColor); }\n        }\n\n        .smart-lock-link {\n            display: none;\n        }\n\n        .smart-lock-marker {\n            position: absolute;\n            width: var(--marker-size, 34px);\n            height: var(--marker-size, 34px);\n            transform: translate(-50%, -50%);\n            pointer-events: none;\n            color: rgba(83, 235, 255, 0.88);\n            font-family: 'Orbitron', monospace;\n            font-size: 9px;\n            font-weight: 900;\n            line-height: 1;\n            letter-spacing: 0;\n            border: 0;\n            border-radius: 0;\n            opacity: 0.9;\n            filter: drop-shadow(0 0 4px rgba(0, 217, 255, 0.45));\n            background:\n                linear-gradient(currentColor, currentColor) 0 0 / 30% 1px no-repeat,\n                linear-gradient(currentColor, currentColor) 0 0 / 1px 30% no-repeat,\n                linear-gradient(currentColor, currentColor) 100% 0 / 30% 1px no-repeat,\n                linear-gradient(currentColor, currentColor) 100% 0 / 1px 30% no-repeat,\n                linear-gradient(currentColor, currentColor) 0 100% / 30% 1px no-repeat,\n                linear-gradient(currentColor, currentColor) 0 100% / 1px 30% no-repeat,\n                linear-gradient(currentColor, currentColor) 100% 100% / 30% 1px no-repeat,\n                linear-gradient(currentColor, currentColor) 100% 100% / 1px 30% no-repeat;\n            text-shadow: 0 0 7px currentColor;\n            animation: smartLockPulse 0.22s ease-out;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .smart-lock-marker::before {\n            content: '';\n            position: absolute;\n            inset: 19%;\n            border-radius: 50%;\n            border: 1px solid currentColor;\n            opacity: 0.22;\n        }\n\n        .smart-lock-marker::after {\n            content: '';\n            position: absolute;\n            left: 50%;\n            top: calc(var(--marker-size, 34px) * -0.28);\n            width: 1px;\n            height: calc(var(--marker-size, 34px) * 0.22);\n            transform: translateX(-50%);\n            background: currentColor;\n            box-shadow: 0 0 4px currentColor;\n        }\n\n        .smart-lock-marker.partial {\n            color: rgba(255, 188, 58, 0.92);\n            filter: drop-shadow(0 0 5px rgba(255, 176, 38, 0.48));\n        }\n\n        .smart-lock-marker.ready {\n            color: rgba(255, 54, 54, 0.96);\n            filter: drop-shadow(0 0 6px rgba(255, 50, 50, 0.7));\n        }\n\n        .smart-lock-marker.locking {\n            animation: smartLockPulse 0.22s ease-out;\n        }\n\n        .smart-lock-count {\n            position: absolute;\n            top: -16px;\n            left: 50%;\n            transform: translateX(-50%);\n            z-index: 1;\n            padding: 1px 4px;\n            min-width: 18px;\n            border: 1px solid currentColor;\n            background: rgba(0, 0, 0, 0.34);\n            border-radius: 2px;\n            text-align: center;\n            opacity: 0.9;\n        }\n\n        .smart-lock-progress {\n            position: absolute;\n            inset: -4px;\n            border-radius: 50%;\n            background: conic-gradient(currentColor var(--lock-deg, 0deg), rgba(255, 255, 255, 0.08) 0deg);\n            -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 1px));\n            mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 1px));\n            opacity: 0.95;\n            transform: rotate(-90deg);\n        }\n\n        .smart-lock-marker.locking .smart-lock-progress {\n            animation: smartHeadScan 1.1s linear infinite;\n        }\n\n        body.touch-device .crosshair.smart-hud {\n            width: clamp(214px, 66vw, 282px);\n            height: clamp(150px, 34vh, 218px);\n            color: rgba(86, 232, 255, 0.58);\n            filter: drop-shadow(0 0 3px rgba(0, 217, 255, 0.22));\n        }\n\n        body.touch-device .crosshair.smart-hud.locking {\n            color: rgba(255, 185, 58, 0.72);\n            filter: drop-shadow(0 0 4px rgba(255, 176, 38, 0.32));\n        }\n\n        body.touch-device .crosshair.smart-hud.locked {\n            color: rgba(255, 64, 64, 0.78);\n            filter: drop-shadow(0 0 5px rgba(255, 50, 50, 0.45));\n        }\n\n        body.touch-device .crosshair.smart-hud::before {\n            width: 96px;\n            height: 96px;\n            opacity: 0.28;\n            -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 1.2px), #000 calc(100% - 0.8px));\n            mask: radial-gradient(farthest-side, transparent calc(100% - 1.2px), #000 calc(100% - 0.8px));\n        }\n\n        body.touch-device .crosshair.smart-hud::after {\n            width: 60px;\n            height: 60px;\n            opacity: 0.16;\n            box-shadow: 0 0 2px currentColor;\n        }\n\n        body.touch-device .crosshair.smart-hud .ch-top,\n        body.touch-device .crosshair.smart-hud .ch-bottom,\n        body.touch-device .crosshair.smart-hud .ch-left,\n        body.touch-device .crosshair.smart-hud .ch-right {\n            width: 1.5px;\n            height: 4px;\n            box-shadow: 0 0 2px currentColor;\n            opacity: 0.65;\n        }\n\n        body.touch-device .crosshair.smart-hud .ch-top {\n            top: calc(50% - 8px);\n        }\n\n        body.touch-device .crosshair.smart-hud .ch-bottom {\n            top: calc(50% + 4px);\n        }\n\n        body.touch-device .crosshair.smart-hud .ch-left {\n            width: 4px;\n            height: 1.5px;\n            left: calc(50% - 8px);\n        }\n\n        body.touch-device .crosshair.smart-hud .ch-right {\n            width: 4px;\n            height: 1.5px;\n            left: calc(50% + 4px);\n        }\n\n\n        /* Mobile corner brackets */\n        body.touch-device .crosshair.smart-hud .sh-bracket-decor {\n            width: 20px;\n            height: 20px;\n            border-width: 1.2px;\n            opacity: 0.68;\n        }\n\n        /* Mobile focusing effects */\n        body.touch-device .crosshair.smart-hud.locking .sh-bracket-decor.tl { transform: translate(4px, 4px); }\n        body.touch-device .crosshair.smart-hud.locking .sh-bracket-decor.tr { transform: translate(-4px, 4px); }\n        body.touch-device .crosshair.smart-hud.locking .sh-bracket-decor.bl { transform: translate(4px, -4px); }\n        body.touch-device .crosshair.smart-hud.locking .sh-bracket-decor.br { transform: translate(-4px, -4px); }\n\n        body.touch-device .crosshair.smart-hud.locked .sh-bracket-decor.tl { transform: translate(2px, 2px); }\n        body.touch-device .crosshair.smart-hud.locked .sh-bracket-decor.tr { transform: translate(-2px, 2px); }\n        body.touch-device .crosshair.smart-hud.locked .sh-bracket-decor.bl { transform: translate(2px, -2px); }\n        body.touch-device .crosshair.smart-hud.locked .sh-bracket-decor.br { transform: translate(-2px, -2px); }\n        body.touch-device .smart-lock-marker {\n            color: rgba(92, 238, 255, 0.82);\n            font-size: 8px;\n            opacity: 0.86;\n            filter: drop-shadow(0 0 3px rgba(0, 217, 255, 0.38));\n            background:\n                linear-gradient(currentColor, currentColor) 0 0 / 26% 1px no-repeat,\n                linear-gradient(currentColor, currentColor) 0 0 / 1px 26% no-repeat,\n                linear-gradient(currentColor, currentColor) 100% 0 / 26% 1px no-repeat,\n                linear-gradient(currentColor, currentColor) 100% 0 / 1px 26% no-repeat,\n                linear-gradient(currentColor, currentColor) 0 100% / 26% 1px no-repeat,\n                linear-gradient(currentColor, currentColor) 0 100% / 1px 26% no-repeat,\n                linear-gradient(currentColor, currentColor) 100% 100% / 26% 1px no-repeat,\n                linear-gradient(currentColor, currentColor) 100% 100% / 1px 26% no-repeat;\n        }\n\n        body.touch-device .smart-lock-marker.partial {\n            color: rgba(255, 188, 58, 0.92);\n            filter: drop-shadow(0 0 4px rgba(255, 176, 38, 0.45));\n        }\n\n        body.touch-device .smart-lock-marker.ready {\n            color: rgba(255, 54, 54, 0.96);\n            filter: drop-shadow(0 0 5px rgba(255, 50, 50, 0.65));\n        }\n\n        body.touch-device .smart-lock-marker::before {\n            inset: 25%;\n            opacity: 0.16;\n        }\n\n        body.touch-device .smart-lock-marker::after {\n            display: none;\n        }\n\n        body.touch-device .smart-lock-count {\n            top: 50%;\n            left: calc(100% + 5px);\n            transform: translateY(-50%);\n            padding: 1px 3px;\n            min-width: 14px;\n            font-size: 7px;\n            background: rgba(0, 7, 12, 0.42);\n            opacity: 0.78;\n        }\n\n        body.touch-device .smart-lock-marker.single .smart-lock-count {\n            display: none;\n        }\n\n        body.touch-device .smart-lock-progress {\n            inset: -3px;\n            -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 1.5px), #000 calc(100% - 1px));\n            mask: radial-gradient(farthest-side, transparent calc(100% - 1.5px), #000 calc(100% - 1px));\n            opacity: 0.82;\n        }\n\n        body.touch-device #game-world.landscape-mode .crosshair.smart-hud {\n            width: clamp(238px, 50vmin, 318px);\n            height: clamp(126px, 30vmin, 190px);\n        }\n\n\n        @keyframes smartLockingRotate {\n            0% {\n                transform: translate(-50%, -50%) rotate(0deg);\n            }\n\n            100% {\n                transform: translate(-50%, -50%) rotate(360deg);\n            }\n        }\n\n        @keyframes smartHudSweep {\n            0% {\n                transform: rotate(0deg);\n                opacity: 0.48;\n            }\n\n            50% {\n                opacity: 0.76;\n            }\n\n            100% {\n                transform: rotate(360deg);\n                opacity: 0.48;\n            }\n        }\n\n        @keyframes smartHeadScan {\n            0% {\n                filter: brightness(0.8);\n                opacity: 0.5;\n            }\n\n            50% {\n                filter: brightness(1.4);\n                opacity: 1;\n            }\n\n            100% {\n                filter: brightness(0.8);\n                opacity: 0.5;\n            }\n        }\n\n        @keyframes smartLockPulse {\n            0% {\n                transform: translate(-50%, -50%) scale(1.35);\n                opacity: 0;\n            }\n\n            100% {\n                transform: translate(-50%, -50%) scale(1);\n                opacity: 1;\n            }\n        }\n\n        .crosshair::before {\n            content: '';\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            width: 4px;\n            height: 4px;\n            transform: translate(-50%, -50%);\n            background: currentColor;\n            border-radius: 50%;\n            box-shadow: 0 0 2px currentColor;\n        }\n\n        .crosshair::after {\n            display: none;\n        }\n\n        .ch-arm {\n            position: absolute;\n            box-sizing: border-box;\n            background: currentColor;\n            box-shadow: 0 0 2px rgba(0, 0, 0, 0.8), 0 0 4px currentColor;\n            border-radius: 1px;\n        }\n\n        .ch-top,\n        .ch-bottom {\n            width: var(--thick);\n            height: var(--arm);\n            left: 50%;\n            transform: translateX(-50%);\n        }\n\n        .ch-top {\n            top: calc(50% - var(--gap) - var(--arm));\n        }\n\n        .ch-bottom {\n            top: calc(50% + var(--gap));\n        }\n\n        .ch-left,\n        .ch-right {\n            width: var(--arm);\n            height: var(--thick);\n            top: 50%;\n            transform: translateY(-50%);\n        }\n\n        .ch-left {\n            left: calc(50% - var(--gap) - var(--arm));\n        }\n\n        .ch-right {\n            left: calc(50% + var(--gap));\n        }\n\n        /* Crosshair color stays white always */\n\n        /* Health Bar */\n        .health-bar-container {\n            position: absolute;\n            bottom: var(--space-sm);\n            left: 50%;\n            transform: translateX(-50%);\n            width: 300px;\n            height: 28px;\n            background: linear-gradient(180deg, rgba(20, 20, 30, 0.9) 0%, rgba(10, 10, 18, 0.95) 100%);\n            border: 1.5px solid rgba(255, 255, 255, 0.15);\n            border-radius: 6px;\n            overflow: hidden;\n            pointer-events: none;\n            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.05);\n        }\n\n        .health-bar-fill {\n            height: 100%;\n            width: 100%;\n            transform-origin: left;\n            transform: scaleX(1);\n            background: linear-gradient(90deg, #cc0000 0%, #ff2222 40%, #ff4444 100%);\n            transition: transform var(--transition-fast) var(--ease-out);\n            box-shadow: 0 0 12px rgba(255, 0, 0, 0.4), inset 0 -2px 4px rgba(0, 0, 0, 0.3);\n            position: relative;\n        }\n\n        .health-bar-fill::after {\n            content: '';\n            position: absolute;\n            top: 0;\n            left: 0;\n            right: 0;\n            height: 40%;\n            background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, transparent 100%);\n            border-radius: 4px 4px 0 0;\n        }\n\n        .health-text {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            color: white;\n            font-size: 13px;\n            font-weight: 700;\n            font-family: 'Orbitron', monospace;\n            text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8), 0 0 6px rgba(0, 0, 0, 0.5);\n            letter-spacing: 1px;\n        }\n\n        /* Ammo Display */\n        .ammo-display {\n            position: absolute;\n            top: var(--space-sm);\n            right: var(--space-sm);\n            text-align: right;\n            color: rgba(255, 255, 255, 0.9);\n            font-family: 'Orbitron', monospace;\n            font-size: 28px;\n            font-weight: 700;\n            text-shadow: 0 0 6px rgba(255, 255, 255, 0.2);\n            pointer-events: none;\n        }\n\n        .ammo-display.low {\n            color: var(--red);\n            animation: ammoLowPulse 0.46s cubic-bezier(0.2, 0, 0.2, 1) infinite alternate;\n        }\n\n        @keyframes ammoLowPulse {\n            from {\n                opacity: 1;\n                transform: translateX(0) scale(1);\n                text-shadow: 0 0 8px rgba(255, 50, 50, 0.45);\n            }\n\n            to {\n                opacity: 0.58;\n                transform: translateX(-2px) scale(1.07);\n                text-shadow: 0 0 18px rgba(255, 50, 50, 0.85), 0 0 34px rgba(255, 0, 0, 0.35);\n            }\n        }\n\n        .weapon-name {\n            font-size: 13px;\n            opacity: 0.55;\n            margin-bottom: 4px;\n            max-width: 240px;\n            overflow-wrap: anywhere;\n            letter-spacing: 1.5px;\n            font-weight: 600;\n        }\n\n        /* Wave Display */\n        .wave-display {\n            position: absolute;\n            top: var(--space-sm);\n            left: 50%;\n            transform: translateX(-50%);\n            display: inline-flex;\n            align-items: center;\n            gap: 12px;\n            color: rgba(255, 255, 255, 0.9);\n            font-family: 'Orbitron', monospace;\n            font-size: 18px;\n            font-weight: 700;\n            text-shadow: 0 0 8px rgba(0, 217, 255, 0.4);\n            background: linear-gradient(180deg, rgba(10, 15, 30, 0.85) 0%, rgba(5, 8, 18, 0.9) 100%);\n            padding: 8px 20px;\n            border-radius: 8px;\n            border: 1px solid rgba(0, 180, 255, 0.2);\n            pointer-events: none;\n            max-width: min(70vw, 620px);\n            text-align: center;\n            overflow-wrap: anywhere;\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);\n            letter-spacing: 2px;\n            white-space: nowrap;\n        }\n\n        /* Enemies Remaining Display */\n        #enemies-display {\n            color: rgba(255, 255, 255, 0.9);\n            font-family: 'Orbitron', monospace;\n            font-size: 18px;\n            font-weight: 700;\n            letter-spacing: 2px;\n            text-shadow: 0 0 8px rgba(0, 217, 255, 0.4);\n            pointer-events: none;\n            display: inline-flex;\n            align-items: center;\n            vertical-align: middle;\n            line-height: 1;\n        }\n\n        #enemies-display:empty {\n            display: none;\n        }\n\n        /* Score Display */\n        .score-display {\n            position: absolute;\n            top: var(--space-sm);\n            left: var(--space-sm);\n            color: #ffcc00;\n            font-family: 'Orbitron', monospace;\n            font-size: 26px;\n            font-weight: 700;\n            text-shadow: 0 0 8px rgba(255, 204, 0, 0.4);\n            pointer-events: none;\n        }\n\n        .score-label {\n            font-size: 10px;\n            font-weight: 600;\n            opacity: 0.5;\n            display: block;\n            letter-spacing: 3px;\n            margin-bottom: 2px;\n            text-transform: uppercase;\n        }\n\n        /* Combo/Style Rank System */\n        .combo-display {\n            position: absolute;\n            left: var(--space-sm);\n            top: 38%;\n            transform: translateY(-50%);\n            text-align: left;\n            pointer-events: none;\n            opacity: 0;\n            transition: opacity 0.3s ease;\n            min-height: 120px;\n        }\n\n        .combo-display.active {\n            opacity: 1;\n            animation: comboSlideIn 0.42s cubic-bezier(0.13, 1.35, 0.24, 1);\n        }\n\n        @keyframes comboSlideIn {\n            0% {\n                opacity: 0;\n                transform: translateY(-50%) translateX(-20px) scale(0.8);\n            }\n\n            62% {\n                opacity: 1;\n                transform: translateY(-50%) translateX(8px) scale(1.08);\n            }\n\n            100% {\n                opacity: 1;\n                transform: translateY(-50%) translateX(0) scale(1);\n            }\n        }\n\n        .combo-rank {\n            font-family: 'Orbitron', monospace;\n            font-size: 72px;\n            font-weight: 900;\n            letter-spacing: -2px;\n            line-height: 1;\n            background: linear-gradient(180deg, #ffffff 0%, #ffcc00 100%);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n            filter: drop-shadow(0 0 8px rgba(255, 200, 0, 0.5));\n            transition: transform 0.1s ease-out;\n        }\n\n        .combo-rank.pulse {\n            animation: comboPulse 0.24s cubic-bezier(0.12, 1.55, 0.2, 1);\n        }\n\n        @keyframes comboPulse {\n            0% {\n                transform: scale(1.85) rotate(-6deg) translateX(-4px);\n                filter: brightness(1.6);\n            }\n\n            45% {\n                transform: scale(0.92) rotate(2deg) translateX(2px);\n            }\n\n            78% {\n                transform: scale(1.12) rotate(-1deg);\n            }\n\n            100% {\n                transform: scale(1) rotate(0deg);\n                filter: brightness(1);\n            }\n        }\n\n        .combo-rank.rank-D {\n            background: linear-gradient(180deg, #aaaaaa 0%, #666666 100%);\n            -webkit-background-clip: text;\n            background-clip: text;\n            filter: drop-shadow(0 0 4px rgba(150, 150, 150, 0.3));\n        }\n\n        .combo-rank.rank-C {\n            background: linear-gradient(180deg, #ffffff 0%, #88ccff 100%);\n            -webkit-background-clip: text;\n            background-clip: text;\n            filter: drop-shadow(0 0 6px rgba(100, 180, 255, 0.4));\n        }\n\n        .combo-rank.rank-B {\n            background: linear-gradient(180deg, #ffffff 0%, #44ff88 100%);\n            -webkit-background-clip: text;\n            background-clip: text;\n            filter: drop-shadow(0 0 8px rgba(50, 255, 100, 0.5));\n        }\n\n        .combo-rank.rank-A {\n            background: linear-gradient(180deg, #ffffff 0%, #ffcc00 100%);\n            -webkit-background-clip: text;\n            background-clip: text;\n            filter: drop-shadow(0 0 10px rgba(255, 200, 0, 0.6));\n        }\n\n        .combo-rank.rank-S {\n            background: linear-gradient(180deg, #ffffff 0%, #ff6600 100%);\n            -webkit-background-clip: text;\n            background-clip: text;\n            filter: drop-shadow(0 0 12px rgba(255, 100, 0, 0.7));\n            animation: comboS 0.8s ease-in-out infinite alternate;\n        }\n\n        .combo-rank.rank-SS {\n            background: linear-gradient(180deg, #ffffff 0%, #ff2222 100%);\n            -webkit-background-clip: text;\n            background-clip: text;\n            filter: drop-shadow(0 0 14px rgba(255, 30, 30, 0.8));\n            animation: comboSS 0.6s ease-in-out infinite alternate;\n        }\n\n        .combo-rank.rank-SSS {\n            background: linear-gradient(180deg, #ffffff 0%, #ff00ff 50%, #ff2222 100%);\n            -webkit-background-clip: text;\n            background-clip: text;\n            filter: drop-shadow(0 0 18px rgba(255, 0, 200, 0.9));\n            animation: comboSSS 0.08s linear infinite;\n        }\n\n        @keyframes comboS {\n            0% {\n                transform: scale(1) rotate(-0.3deg);\n            }\n\n            100% {\n                transform: scale(1.06) rotate(0.5deg);\n            }\n        }\n\n        @keyframes comboSS {\n            0% {\n                transform: scale(1) translateX(0);\n            }\n\n            100% {\n                transform: scale(1.08) translateX(2px) rotate(-0.8deg);\n            }\n        }\n\n        @keyframes comboSSS {\n            0% {\n                transform: scale(1.05) translate(2px, 1px) rotate(-1deg);\n            }\n\n            20% {\n                transform: scale(1.08) translate(-2px, -2px) rotate(2deg);\n            }\n\n            40% {\n                transform: scale(1.04) translate(1px, -1px) rotate(-2deg);\n            }\n\n            60% {\n                transform: scale(1.09) translate(-1px, 2px) rotate(1deg);\n            }\n\n            80% {\n                transform: scale(1.06) translate(2px, -2px) rotate(-1deg);\n            }\n\n            100% {\n                transform: scale(1.05) translate(1px, 1px) rotate(0deg);\n            }\n        }\n\n        .combo-count {\n            font-family: 'Orbitron', monospace;\n            font-size: 28px;\n            font-weight: 700;\n            color: rgba(255, 255, 255, 0.9);\n            margin-top: 4px;\n            letter-spacing: 1.5px;\n            text-shadow: 0 0 8px rgba(255, 255, 255, 0.4), 0 2px 4px rgba(0, 0, 0, 0.5);\n        }\n\n        .combo-bar-outer {\n            width: 140px;\n            height: 5px;\n            background: rgba(255, 255, 255, 0.1);\n            border-radius: 2px;\n            margin-top: 6px;\n            margin-right: auto;\n            overflow: hidden;\n        }\n\n        .combo-bar-fill {\n            height: 100%;\n            width: 100%;\n            transform-origin: left;\n            transform: scaleX(0);\n            background: linear-gradient(90deg, #ffcc00, #ff6600);\n            border-radius: 2px;\n            box-shadow: 0 0 6px rgba(255, 150, 0, 0.5);\n        }\n\n        .combo-bar-fill.rank-D {\n            background: linear-gradient(90deg, #888, #aaa);\n        }\n\n        .combo-bar-fill.rank-C {\n            background: linear-gradient(90deg, #4488ff, #88ccff);\n        }\n\n        .combo-bar-fill.rank-B {\n            background: linear-gradient(90deg, #22cc66, #44ff88);\n        }\n\n        .combo-bar-fill.rank-A {\n            background: linear-gradient(90deg, #ffaa00, #ffcc00);\n        }\n\n        .combo-bar-fill.rank-S {\n            background: linear-gradient(90deg, #ff4400, #ff8800);\n        }\n\n        .combo-bar-fill.rank-SS {\n            background: linear-gradient(90deg, #cc0000, #ff4444);\n        }\n\n        .combo-bar-fill.rank-SSS {\n            background: linear-gradient(90deg, #ff00aa, #ff44ff);\n        }\n\n        /* Weapon Slots */\n        .weapon-slots {\n            position: absolute;\n            bottom: 52px;\n            left: 50%;\n            transform: translateX(-50%);\n            display: flex;\n            gap: 8px;\n            pointer-events: auto;\n        }\n\n        .weapon-slot {\n            width: 54px;\n            height: 54px;\n            background: linear-gradient(180deg, rgba(15, 20, 35, 0.85) 0%, rgba(8, 10, 20, 0.9) 100%);\n            border: 1.5px solid rgba(255, 255, 255, 0.15);\n            border-radius: 10px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1), border-color 200ms, box-shadow 200ms, background 200ms;\n            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n        }\n\n        .weapon-slot.active {\n            border-color: rgba(0, 200, 255, 0.7);\n            box-shadow: 0 0 12px rgba(0, 200, 255, 0.3), inset 0 0 8px rgba(0, 200, 255, 0.05);\n            background: linear-gradient(180deg, rgba(10, 25, 50, 0.9) 0%, rgba(5, 15, 35, 0.95) 100%);\n            animation: weaponSlotReady 1.35s ease-in-out infinite alternate;\n        }\n\n        .weapon-slot:active {\n            transform: scale(0.84) rotate(-2deg);\n        }\n\n        @keyframes weaponSlotReady {\n            from {\n                box-shadow: 0 0 12px rgba(0, 200, 255, 0.28), inset 0 0 8px rgba(0, 200, 255, 0.05);\n            }\n\n            to {\n                box-shadow: 0 0 22px rgba(0, 200, 255, 0.46), inset 0 0 14px rgba(0, 200, 255, 0.12);\n            }\n        }\n\n        .weapon-slot img,\n        .weapon-slot .weapon-slot-icon {\n            width: 38px;\n            height: 38px;\n            object-fit: contain;\n            filter: brightness(0.85);\n            transition: filter 200ms;\n        }\n\n        .weapon-slot.active img,\n        .weapon-slot.active .weapon-slot-icon {\n            filter: brightness(1) drop-shadow(0 0 3px rgba(0, 200, 255, 0.4));\n        }\n\n        .weapon-slot {\n            position: relative;\n        }\n\n        .weapon-slot .slot-num {\n            display: none;\n            position: absolute;\n            top: 3px;\n            left: 5px;\n            font-size: 11px;\n            font-family: 'Orbitron', monospace;\n            color: rgba(255, 255, 255, 0.65);\n            font-weight: 700;\n            line-height: 1;\n            pointer-events: none;\n            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);\n        }\n\n        .weapon-slots.pc-mode .weapon-slot .slot-num {\n            display: block;\n        }\n\n\n        /* Weapon Viewmodel (3D - canvas overlay hidden) */\n        .weapon-viewmodel {\n            display: none;\n        }\n\n        .muzzle-flash {\n            display: none;\n        }\n\n        /* Start Screen */\n        .start-screen {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: radial-gradient(ellipse at 50% 35%, rgba(10, 20, 40, 0.95) 0%, rgba(2, 4, 10, 0.99) 75%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 100;\n        }\n\n        .start-screen h1 {\n            font-family: 'Orbitron', monospace;\n            font-size: 44px;\n            font-weight: 900;\n            color: #fff;\n            background: linear-gradient(180deg, #ffffff 0%, #80d8ff 50%, #0091ea 100%);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n            filter: drop-shadow(0 0 20px rgba(0, 180, 255, 0.6));\n            margin-bottom: 16px;\n            text-align: center;\n            letter-spacing: 3px;\n            animation: startTitleFloat 3s ease-in-out infinite;\n        }\n\n        @keyframes startTitleFloat {\n\n            0%,\n            100% {\n                transform: translateY(0);\n            }\n\n            50% {\n                transform: translateY(-6px);\n            }\n        }\n\n        .start-screen p {\n            color: rgba(255, 255, 255, 0.5);\n            font-size: 16px;\n            margin-bottom: 52px;\n            text-align: center;\n            padding: 0 32px;\n            letter-spacing: 0.5px;\n            max-width: 400px;\n        }\n\n        .start-btn {\n            width: 260px;\n            height: 64px;\n            background: linear-gradient(135deg, rgba(0, 180, 255, 0.9) 0%, rgba(0, 100, 200, 0.9) 100%);\n            border: 1.5px solid rgba(100, 220, 255, 0.5);\n            border-radius: 12px;\n            color: white;\n            font-family: 'Orbitron', monospace;\n            font-size: 22px;\n            font-weight: 700;\n            cursor: pointer;\n            transition: all 250ms cubic-bezier(0.16, 1, 0.3, 1);\n            letter-spacing: 2px;\n            position: relative;\n            overflow: hidden;\n            box-shadow: 0 4px 20px rgba(0, 150, 255, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.15);\n        }\n\n        .start-btn::before {\n            content: '';\n            position: absolute;\n            top: 0;\n            left: -100%;\n            width: 200%;\n            height: 100%;\n            background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.1) 50%, transparent 100%);\n            animation: btnShimmer 2.5s ease-in-out infinite;\n        }\n\n        @keyframes btnShimmer {\n            0% {\n                transform: translateX(-50%);\n            }\n\n            100% {\n                transform: translateX(50%);\n            }\n        }\n\n        .start-btn:hover {\n            transform: scale(1.04) translateY(-2px);\n            box-shadow: 0 8px 30px rgba(0, 150, 255, 0.4), 0 0 40px rgba(0, 180, 255, 0.15);\n            border-color: rgba(150, 230, 255, 0.7);\n        }\n\n        .start-btn:active {\n            transform: scale(0.96);\n            transition-duration: 100ms;\n        }\n\n        /* Game Over */\n        .game-over-screen {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(5, 7, 15, 0.7);\n            backdrop-filter: blur(12px);\n            -webkit-backdrop-filter: blur(12px);\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 100;\n        }\n\n        .game-over-screen.active {\n            display: flex;\n            animation: gameOverReveal 0.8s cubic-bezier(0.16, 1, 0.3, 1);\n        }\n\n        @keyframes gameOverReveal {\n            0% {\n                opacity: 0;\n            }\n\n            100% {\n                opacity: 1;\n            }\n        }\n\n        .game-over-left {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            width: 100%;\n            box-sizing: border-box;\n        }\n\n        .game-over-right {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            width: 100%;\n            box-sizing: border-box;\n        }\n\n        #restart-btn {\n            margin-top: 10px;\n            margin-bottom: 24px;\n            background: linear-gradient(135deg, #ff3333 0%, #b71c1c 100%);\n            border: 2px solid #ff6666;\n            box-shadow: 0 0 15px rgba(255, 50, 50, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2);\n            animation: retryPulse 1.8s infinite alternate cubic-bezier(0.4, 0, 0.2, 1);\n            transition: all 250ms cubic-bezier(0.16, 1, 0.3, 1);\n        }\n\n        #restart-btn:hover {\n            transform: scale(1.08) translateY(-2px);\n            box-shadow: 0 0 25px rgba(255, 80, 80, 0.9), 0 0 40px rgba(255, 50, 50, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3);\n            border-color: #ff9999;\n            background: linear-gradient(135deg, #ff4444 0%, #d32f2f 100%);\n        }\n\n        #restart-btn:active {\n            transform: scale(0.96) translateY(0);\n            transition-duration: 80ms;\n        }\n\n        @keyframes retryPulse {\n            0% {\n                transform: scale(1);\n                box-shadow: 0 0 15px rgba(255, 50, 50, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15);\n            }\n\n            100% {\n                transform: scale(1.04);\n                box-shadow: 0 0 25px rgba(255, 50, 50, 0.8), 0 0 10px rgba(255, 50, 50, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.25);\n                border-color: #ffa4a4;\n            }\n        }\n\n        .game-over-screen h1 {\n            font-family: 'Orbitron', monospace;\n            font-size: 38px;\n            font-weight: 900;\n            color: #fff;\n            background: linear-gradient(180deg, #ffffff 0%, #ff5252 50%, #b71c1c 100%);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n            filter: drop-shadow(0 0 15px rgba(255, 50, 50, 0.6));\n            margin-bottom: 16px;\n            letter-spacing: 3px;\n            animation: gameOverTitlePulse 2s ease-in-out infinite;\n        }\n\n        @keyframes gameOverTitlePulse {\n\n            0%,\n            100% {\n                filter: drop-shadow(0 0 15px rgba(255, 50, 50, 0.6));\n            }\n\n            50% {\n                filter: drop-shadow(0 0 25px rgba(255, 50, 50, 0.8));\n            }\n        }\n\n        .stats-container {\n            background: linear-gradient(160deg, rgba(20, 15, 25, 0.9) 0%, rgba(10, 8, 15, 0.95) 100%);\n            padding: 12px 36px;\n            border-radius: 16px;\n            border: 1px solid rgba(255, 80, 80, 0.15);\n            margin-bottom: 16px;\n            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.03);\n        }\n\n        .stat-row {\n            display: flex;\n            justify-content: space-between;\n            gap: 48px;\n            color: white;\n            font-size: 16px;\n            margin: 8px 0;\n            opacity: 0;\n            animation: statRowReveal 0.4s ease-out forwards;\n        }\n\n        .stat-row:nth-child(1) {\n            animation-delay: 0.3s;\n        }\n\n        .stat-row:nth-child(2) {\n            animation-delay: 0.45s;\n        }\n\n        .stat-row:nth-child(3) {\n            animation-delay: 0.6s;\n        }\n\n        .stat-row:nth-child(4) {\n            animation-delay: 0.75s;\n        }\n\n        .stat-row:nth-child(5) {\n            animation-delay: 0.9s;\n        }\n\n        @keyframes statRowReveal {\n            0% {\n                opacity: 0;\n                transform: translateX(-10px);\n            }\n\n            100% {\n                opacity: 1;\n                transform: translateX(0);\n            }\n        }\n\n        .stat-row .label {\n            color: rgba(255, 255, 255, 0.5);\n            font-family: 'Inter', sans-serif;\n            font-weight: 400;\n        }\n\n        .stat-row .value {\n            color: var(--cyan);\n            font-family: 'Orbitron', monospace;\n            font-weight: 700;\n            text-shadow: 0 0 6px rgba(0, 200, 255, 0.3);\n        }\n\n        /* Leaderboard */\n        #leaderboard-container {\n            margin-top: 32px;\n            margin-bottom: 32px;\n            width: 100%;\n            max-width: 500px;\n            opacity: 0;\n            animation: leaderboardFadeIn 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.4s forwards;\n        }\n\n        @keyframes leaderboardFadeIn {\n            from {\n                opacity: 0;\n                transform: translateY(20px) scale(0.95);\n            }\n\n            to {\n                opacity: 1;\n                transform: translateY(0) scale(1);\n            }\n        }\n\n        .leaderboard-title {\n            font-family: 'Orbitron', monospace;\n            font-size: 20px;\n            font-weight: 700;\n            color: rgba(255, 255, 255, 0.8);\n            text-align: center;\n            margin-bottom: 16px;\n            letter-spacing: 3px;\n            text-shadow: 0 0 8px rgba(0, 180, 255, 0.3);\n        }\n\n        .leaderboard-table {\n            width: 100%;\n            border-collapse: collapse;\n            background: linear-gradient(160deg, rgba(15, 18, 30, 0.9) 0%, rgba(8, 10, 20, 0.95) 100%);\n            border-radius: 12px;\n            overflow: hidden;\n            border: 1px solid rgba(255, 255, 255, 0.06);\n            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);\n        }\n\n        .leaderboard-table thead {\n            background: rgba(0, 217, 255, 0.1);\n        }\n\n        .leaderboard-table th {\n            font-family: 'Orbitron', monospace;\n            font-size: 12px;\n            color: var(--cyan);\n            text-transform: uppercase;\n            letter-spacing: 1px;\n            padding: 12px 8px;\n            font-weight: 600;\n        }\n\n        .leaderboard-table tbody tr {\n            opacity: 0;\n            animation: leaderboardRowSlide 0.4s ease-out forwards;\n            border-bottom: 1px solid rgba(255, 255, 255, 0.05);\n        }\n\n        .leaderboard-table tbody tr:nth-child(1) {\n            animation-delay: 0.4s;\n        }\n\n        .leaderboard-table tbody tr:nth-child(2) {\n            animation-delay: 0.5s;\n        }\n\n        .leaderboard-table tbody tr:nth-child(3) {\n            animation-delay: 0.6s;\n        }\n\n        .leaderboard-table tbody tr:nth-child(4) {\n            animation-delay: 0.7s;\n        }\n\n        .leaderboard-table tbody tr:nth-child(5) {\n            animation-delay: 0.8s;\n        }\n\n        .leaderboard-table tbody tr:nth-child(6) {\n            animation-delay: 0.9s;\n        }\n\n        .leaderboard-table tbody tr:nth-child(7) {\n            animation-delay: 1.0s;\n        }\n\n        .leaderboard-table tbody tr:nth-child(8) {\n            animation-delay: 1.1s;\n        }\n\n        .leaderboard-table tbody tr:nth-child(9) {\n            animation-delay: 1.2s;\n        }\n\n        .leaderboard-table tbody tr:nth-child(10) {\n            animation-delay: 1.3s;\n        }\n\n        @keyframes leaderboardRowSlide {\n            from {\n                opacity: 0;\n                transform: translateX(-20px);\n            }\n\n            to {\n                opacity: 1;\n                transform: translateX(0);\n            }\n        }\n\n        .leaderboard-table td {\n            padding: 12px 8px;\n            color: rgba(255, 255, 255, 0.8);\n            font-size: 14px;\n        }\n\n        .leaderboard-table .rank-cell {\n            font-family: 'Orbitron', monospace;\n            font-weight: 700;\n            color: rgba(255, 255, 255, 0.6);\n            text-align: center;\n            width: 60px;\n        }\n\n        .leaderboard-table .name-cell {\n            text-align: left;\n            max-width: 200px;\n            overflow: hidden;\n            text-overflow: ellipsis;\n            white-space: nowrap;\n        }\n\n        .leaderboard-table .score-cell {\n            font-family: 'Orbitron', monospace;\n            font-weight: 700;\n            color: var(--cyan);\n            text-align: right;\n            width: 100px;\n        }\n\n        .leaderboard-table tbody tr.current-player {\n            background: linear-gradient(90deg, rgba(255, 204, 0, 0.15), rgba(255, 204, 0, 0.05));\n            border: 2px solid rgba(255, 204, 0, 0.5);\n            box-shadow: 0 0 15px rgba(255, 204, 0, 0.2);\n        }\n\n        .leaderboard-table tbody tr.current-player td {\n            color: #ffcc00;\n            font-weight: 600;\n        }\n\n        .leaderboard-table tbody tr.current-player .rank-cell {\n            color: #ffcc00;\n        }\n\n        .leaderboard-table tbody tr.current-player .score-cell {\n            color: #ffdd44;\n            text-shadow: 0 0 8px rgba(255, 204, 0, 0.4);\n        }\n\n        .game-over-screen {\n            overflow-y: auto;\n            overflow-x: hidden;\n        }\n\n        /* Wave Banner */\n        .wave-banner {\n            position: absolute;\n            top: 40%;\n            left: 50%;\n            transform: translate(-50%, -50%) scale(0.8);\n            font-family: 'Orbitron', monospace;\n            font-size: 52px;\n            font-weight: 900;\n            color: #fff;\n            text-shadow: 0 0 30px rgba(0, 217, 255, 0.8), 0 0 60px rgba(0, 217, 255, 0.4), 0 2px 4px rgba(0, 0, 0, 0.8);\n            opacity: 0;\n            pointer-events: none;\n            transition: none;\n            letter-spacing: 4px;\n        }\n\n        .wave-banner.active {\n            animation: waveBannerReveal 2.15s cubic-bezier(0.13, 1.1, 0.24, 1) forwards;\n        }\n\n        @keyframes waveBannerReveal {\n            0% {\n                opacity: 0;\n                transform: translate(-50%, -50%) scale(1.75) rotate(-2deg);\n                filter: blur(10px);\n                letter-spacing: 12px;\n            }\n\n            12% {\n                opacity: 1;\n                transform: translate(-50%, -50%) scale(0.92) rotate(1deg);\n                filter: blur(0px);\n                letter-spacing: 5px;\n            }\n\n            22% {\n                opacity: 1;\n                transform: translate(-50%, -50%) scale(1.05) rotate(0deg);\n            }\n\n            76% {\n                opacity: 1;\n                transform: translate(-50%, -50%) scale(1.0);\n                filter: blur(0px);\n            }\n\n            100% {\n                opacity: 0;\n                transform: translate(-50%, -50%) scale(0.84) translateY(-18px);\n                filter: blur(5px);\n                letter-spacing: 7px;\n            }\n        }\n\n        /* Touch Controls */\n        .touch-joystick {\n            position: absolute;\n            width: 140px;\n            height: 140px;\n            border-radius: 50%;\n            border: 3px solid rgba(255, 255, 255, 0.3);\n            background: rgba(255, 255, 255, 0.05);\n            display: none;\n            pointer-events: none;\n        }\n\n        /* PC WASD Key Display */\n        .wasd-display {\n            position: absolute;\n            bottom: 160px;\n            left: 32px;\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            gap: 5px;\n            pointer-events: none;\n            user-select: none;\n        }\n\n        .wasd-display.pc-visible {\n            display: flex;\n        }\n\n        .wasd-row {\n            display: flex;\n            gap: 5px;\n        }\n\n        .wasd-key {\n            width: 40px;\n            height: 40px;\n            background: rgba(10, 15, 30, 0.75);\n            border: 1.5px solid rgba(255, 255, 255, 0.25);\n            border-radius: 7px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-family: 'Orbitron', monospace;\n            font-size: 14px;\n            font-weight: 700;\n            color: rgba(255, 255, 255, 0.7);\n            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.08);\n            transition: background 0.1s, color 0.1s, border-color 0.1s;\n        }\n\n        .wasd-key.active {\n            background: rgba(0, 180, 255, 0.25);\n            border-color: rgba(0, 200, 255, 0.7);\n            color: #00d9ff;\n            box-shadow: 0 0 10px rgba(0, 200, 255, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);\n        }\n\n        #game-world.landscape-mode .wasd-display {\n            bottom: 80px;\n            left: 24px;\n        }\n\n        .touch-joystick-thumb {\n            position: absolute;\n            width: 56px;\n            height: 56px;\n            border-radius: 50%;\n            background: rgba(0, 217, 255, 0.5);\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n        }\n\n        /* Shoot Button Container (The base plate) */\n        .shoot-container {\n            position: absolute;\n            width: 140px;\n            height: 140px;\n            border-radius: 50%;\n            border: 3px solid rgba(255, 255, 255, 0.3);\n            background: rgba(255, 255, 255, 0.05);\n            display: none;\n            pointer-events: none;\n            z-index: 20;\n        }\n\n        #game-world.landscape-mode .shoot-container {\n            /* Handled by JS or default positions */\n        }\n\n\n        /* Shoot Button */\n        .shoot-btn {\n            position: absolute;\n            width: 96px;\n            height: 96px;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            background: radial-gradient(circle at center, #8b0000 0%, #520000 70%, #330000 100%);\n            border: 2px solid rgba(180, 40, 40, 0.6);\n            border-radius: 50%;\n            display: none;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            user-select: none;\n            z-index: 21;\n            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4), inset 0 2px 4px rgba(255, 255, 255, 0.1), inset 0 -3px 6px rgba(0, 0, 0, 0.6), 0 0 8px rgba(82, 0, 0, 0.2);\n            touch-action: none;\n            pointer-events: auto;\n            opacity: 0.6;\n            transition: opacity 120ms ease;\n        }\n\n        .shoot-btn.active {\n            background: radial-gradient(circle at center, #e52222 0%, #a00000 70%, #660000 100%);\n            border-color: rgba(220, 80, 80, 0.9);\n            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5), inset 0 1px 3px rgba(255, 255, 255, 0.2), inset 0 -2px 4px rgba(0, 0, 0, 0.6), 0 0 15px rgba(139, 0, 0, 0.5);\n            opacity: 0.92;\n        }\n\n        /* Arrows and Crosshair indicators for Shoot Button */\n        .shoot-arrow {\n            position: absolute;\n            width: 0;\n            height: 0;\n            border-style: solid;\n            opacity: 0.85;\n            transition: transform var(--transition-fast) var(--ease-out);\n            pointer-events: none;\n        }\n\n        .arrow-up {\n            top: 8px;\n            left: 50%;\n            transform: translateX(-50%);\n            border-width: 0 6px 8px 6px;\n            border-color: transparent transparent rgba(255, 255, 255, 0.95) transparent;\n        }\n\n        .arrow-down {\n            bottom: 8px;\n            left: 50%;\n            transform: translateX(-50%);\n            border-width: 8px 6px 0 6px;\n            border-color: rgba(255, 255, 255, 0.95) transparent transparent transparent;\n        }\n\n        .arrow-left {\n            left: 8px;\n            top: 50%;\n            transform: translateY(-50%);\n            border-width: 6px 8px 6px 0;\n            border-color: transparent rgba(255, 255, 255, 0.95) transparent transparent;\n        }\n\n        .arrow-right {\n            right: 8px;\n            top: 50%;\n            transform: translateY(-50%);\n            border-width: 6px 0 6px 8px;\n            border-color: transparent transparent transparent rgba(255, 255, 255, 0.95);\n        }\n\n        .shoot-crosshair {\n            position: absolute;\n            width: 24px;\n            height: 24px;\n            border: 2px solid rgba(255, 255, 255, 0.4);\n            border-radius: 50%;\n            pointer-events: none;\n        }\n\n        .shoot-crosshair::before,\n        .shoot-crosshair::after {\n            content: '';\n            position: absolute;\n            background: rgba(255, 255, 255, 0.6);\n        }\n\n        .shoot-crosshair::before {\n            top: 50%;\n            left: -4px;\n            right: -4px;\n            height: 2px;\n            transform: translateY(-50%);\n        }\n\n        .shoot-crosshair::after {\n            left: 50%;\n            top: -4px;\n            bottom: -4px;\n            width: 2px;\n            transform: translateX(-50%);\n        }\n\n        /* Skill Button */\n        .skill-btn {\n            position: absolute;\n            bottom: 320px;\n            right: 125px;\n            width: 80px;\n            height: 80px;\n            background: rgba(10, 10, 15, 0.5);\n            border: 1.5px solid rgba(0, 217, 255, 0.25);\n            border-radius: 50%;\n            color: rgba(0, 217, 255, 0.7);\n            font-family: 'Orbitron', monospace;\n            font-size: 10px;\n            font-weight: 700;\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            user-select: none;\n            z-index: 22;\n            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);\n            backdrop-filter: blur(4px);\n            touch-action: none;\n            transition: all 150ms ease;\n        }\n\n        .skill-btn .skill-icon {\n            font-size: 22px;\n            margin-bottom: 1px;\n            opacity: 0.8;\n        }\n\n        .skill-btn.active {\n            background: rgba(0, 217, 255, 0.25);\n            border-color: #00d9ff;\n            color: #fff;\n            box-shadow: 0 0 12px rgba(0, 217, 255, 0.4);\n        }\n\n        .skill-btn.cooldown {\n            background: rgba(20, 20, 25, 0.4);\n            border-color: rgba(255, 255, 255, 0.1);\n            color: rgba(255, 255, 255, 0.25);\n            cursor: not-allowed;\n            box-shadow: none;\n        }\n\n        .skill-btn:active:not(.cooldown) {\n            transform: scale(0.92);\n        }\n\n        /* Reload Button */\n        .reload-btn {\n            position: absolute;\n            bottom: 320px;\n            right: 30px;\n            width: 80px;\n            height: 80px;\n            background: rgba(10, 10, 15, 0.5);\n            border: 1.5px solid rgba(255, 170, 0, 0.25);\n            border-radius: 50%;\n            color: rgba(255, 170, 0, 0.7);\n            font-family: 'Orbitron', monospace;\n            font-size: 18px;\n            font-weight: 700;\n            display: none;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            z-index: 22;\n            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);\n            backdrop-filter: blur(4px);\n            transition: all 150ms ease;\n            text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);\n            touch-action: none;\n        }\n\n        .reload-btn:active {\n            background: rgba(255, 170, 0, 0.2);\n            transform: scale(0.86) rotate(-4deg);\n            box-shadow: 0 0 10px rgba(255, 170, 0, 0.35);\n        }\n\n        /* Scope Button (mobile) */\n        .scope-btn {\n            position: absolute;\n            bottom: 320px;\n            right: 220px;\n            width: 80px;\n            height: 80px;\n            background: rgba(10, 10, 15, 0.5);\n            border: 1.5px solid rgba(0, 217, 255, 0.25);\n            border-radius: 50%;\n            color: rgba(0, 217, 255, 0.7);\n            font-family: 'Orbitron', monospace;\n            font-size: 11px;\n            font-weight: 700;\n            display: none;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            flex-direction: column;\n            gap: 1px;\n            user-select: none;\n            z-index: 22;\n            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);\n            backdrop-filter: blur(4px);\n            transition: all 150ms ease;\n            touch-action: none;\n        }\n\n        .scope-btn.scoped {\n            background: rgba(0, 217, 255, 0.2);\n            border-color: #00d9ff;\n            color: #fff;\n            box-shadow: 0 0 12px rgba(0, 217, 255, 0.4);\n            transform: scale(1.03);\n        }\n\n        .scope-btn:active {\n            background: rgba(0, 217, 255, 0.25);\n            transform: scale(0.92);\n        }\n\n        .scope-btn .scope-icon {\n            font-size: 24px;\n            line-height: 1;\n            opacity: 0.8;\n        }\n\n        /* Hit Marker - Overwatch 4-triangle style */\n        .hit-marker {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            width: 40px;\n            height: 40px;\n            opacity: 0;\n            pointer-events: none;\n            transform: translate(-50%, -50%);\n            --hm-color: rgba(255, 255, 255, 0.95);\n        }\n\n        .hit-marker.body {\n            --hm-color: rgba(255, 255, 255, 0.95);\n        }\n\n        .hit-marker.headshot {\n            --hm-color: #ff3344;\n        }\n\n        .hit-marker.kill {\n            --hm-color: #ff3344;\n        }\n\n        .hit-marker::before,\n        .hit-marker::after {\n            display: none !important;\n        }\n\n        .hm-tri {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            width: 3px;\n            height: 11px;\n            background: var(--hm-color);\n            clip-path: polygon(50% 0%, 0% 100%, 100% 100%);\n        }\n\n        /* Each triangle points AWAY from center \u2014 tip outward */\n        .hm-tl {\n            transform: translate(-50%, -50%) rotate(-45deg) translateY(-10px);\n        }\n\n        .hm-tr {\n            transform: translate(-50%, -50%) rotate(45deg) translateY(-10px);\n        }\n\n        .hm-bl {\n            transform: translate(-50%, -50%) rotate(-135deg) translateY(-10px);\n        }\n\n        .hm-br {\n            transform: translate(-50%, -50%) rotate(135deg) translateY(-10px);\n        }\n\n        .hit-marker.body .hm-tri {\n            filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.8));\n        }\n\n        .hit-marker.headshot .hm-tri,\n        .hit-marker.kill .hm-tri {\n            filter: drop-shadow(0 0 3px #ff3344);\n        }\n\n        .hit-marker.pulse-a.active {\n            animation: hm-burst-a 210ms cubic-bezier(0.12, 1.4, 0.22, 1) forwards;\n        }\n\n        .hit-marker.pulse-b.active {\n            animation: hm-burst-b 210ms cubic-bezier(0.12, 1.4, 0.22, 1) forwards;\n        }\n\n        @keyframes hm-burst-a {\n            0% {\n                opacity: 0;\n                transform: translate(-50%, -50%) scale(0.45) rotate(-8deg);\n                filter: brightness(1.8);\n            }\n\n            18% {\n                opacity: 1;\n                transform: translate(-50%, -50%) scale(1.28) rotate(3deg);\n            }\n\n            58% {\n                opacity: 1;\n                transform: translate(-50%, -50%) scale(0.96) rotate(-1deg);\n            }\n\n            100% {\n                opacity: 0;\n                transform: translate(-50%, -50%) scale(1.55) rotate(0deg);\n                filter: brightness(1);\n            }\n        }\n\n        @keyframes hm-burst-b {\n            0% {\n                opacity: 0;\n                transform: translate(-50%, -50%) scale(0.45) rotate(8deg);\n                filter: brightness(1.8);\n            }\n\n            18% {\n                opacity: 1;\n                transform: translate(-50%, -50%) scale(1.28) rotate(-3deg);\n            }\n\n            58% {\n                opacity: 1;\n                transform: translate(-50%, -50%) scale(0.96) rotate(1deg);\n            }\n\n            100% {\n                opacity: 0;\n                transform: translate(-50%, -50%) scale(1.55) rotate(0deg);\n                filter: brightness(1);\n            }\n        }\n\n        /* Directional Damage Indicators */\n        .damage-indicator-container {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: 6;\n        }\n\n        .damage-indicator {\n            position: absolute;\n            pointer-events: none;\n            opacity: 0;\n        }\n\n        .damage-indicator.top {\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 35%;\n            background: linear-gradient(180deg, rgba(255, 0, 0, 0.45) 0%, transparent 100%);\n        }\n\n        .damage-indicator.bottom {\n            bottom: 0;\n            left: 0;\n            width: 100%;\n            height: 35%;\n            background: linear-gradient(0deg, rgba(255, 0, 0, 0.45) 0%, transparent 100%);\n        }\n\n        .damage-indicator.left {\n            top: 0;\n            left: 0;\n            width: 35%;\n            height: 100%;\n            background: linear-gradient(90deg, rgba(255, 0, 0, 0.45) 0%, transparent 100%);\n        }\n\n        .damage-indicator.right {\n            top: 0;\n            right: 0;\n            width: 35%;\n            height: 100%;\n            background: linear-gradient(-90deg, rgba(255, 0, 0, 0.45) 0%, transparent 100%);\n        }\n\n        .damage-indicator.active {\n            animation: damageFlash 0.42s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n        }\n\n        .damage-indicator.active.alt {\n            animation-name: damageFlashAlt;\n        }\n\n        .damage-indicator.soft.top {\n            background: linear-gradient(180deg, rgba(255, 0, 0, 0.18) 0%, transparent 100%);\n        }\n\n        .damage-indicator.soft.bottom {\n            background: linear-gradient(0deg, rgba(255, 0, 0, 0.18) 0%, transparent 100%);\n        }\n\n        .damage-indicator.soft.left {\n            background: linear-gradient(90deg, rgba(255, 0, 0, 0.18) 0%, transparent 100%);\n        }\n\n        .damage-indicator.soft.right {\n            background: linear-gradient(-90deg, rgba(255, 0, 0, 0.18) 0%, transparent 100%);\n        }\n\n        @keyframes damageFlash {\n            0% {\n                opacity: 0;\n                transform: scale(1.08);\n                filter: blur(2px);\n            }\n\n            14% {\n                opacity: 1;\n                transform: scale(1);\n                filter: blur(0);\n            }\n\n            42% {\n                opacity: 0.55;\n            }\n\n            100% {\n                opacity: 0;\n                transform: scale(1.02);\n            }\n        }\n\n        @keyframes damageFlashAlt {\n            0% {\n                opacity: 0;\n                transform: scale(1.08);\n                filter: blur(2px);\n            }\n\n            15% {\n                opacity: 1;\n                transform: scale(1);\n                filter: blur(0);\n            }\n\n            100% {\n                opacity: 0;\n            }\n        }\n\n        /* Loading Screen */\n        .loading-screen {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: radial-gradient(ellipse at 50% 40%, rgba(8, 15, 30, 1) 0%, rgba(2, 4, 10, 1) 80%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 200;\n            transition: opacity 0.6s;\n        }\n\n        .loading-screen.hidden {\n            opacity: 0;\n            pointer-events: none;\n        }\n\n        .loading-title {\n            font-family: 'Orbitron', monospace;\n            font-size: 32px;\n            font-weight: 900;\n            color: #fff;\n            background: linear-gradient(180deg, #ffffff 0%, #80d8ff 60%, #0091ea 100%);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n            filter: drop-shadow(0 0 15px rgba(0, 180, 255, 0.5));\n            margin-bottom: 52px;\n            letter-spacing: 3px;\n        }\n\n        .loading-bar-outer {\n            width: 260px;\n            height: 4px;\n            background: rgba(255, 255, 255, 0.06);\n            border-radius: 2px;\n            overflow: hidden;\n            margin-bottom: 18px;\n        }\n\n        .loading-bar-inner {\n            height: 100%;\n            width: 100%;\n            transform-origin: left;\n            transform: scaleX(0);\n            background: linear-gradient(90deg, #00b0ff, #0091ea, #00b0ff);\n            background-size: 200% 100%;\n            animation: loadingBarGradient 1.5s linear infinite;\n            border-radius: 2px;\n            transition: transform 0.3s;\n            box-shadow: 0 0 8px rgba(0, 150, 255, 0.5);\n        }\n\n        @keyframes loadingBarGradient {\n            0% {\n                background-position: 200% 0;\n            }\n\n            100% {\n                background-position: 0% 0;\n            }\n        }\n\n        .loading-text {\n            color: rgba(255, 255, 255, 0.35);\n            font-size: 12px;\n            font-family: 'Inter', sans-serif;\n            letter-spacing: 2px;\n            text-transform: uppercase;\n        }\n\n        /* Interact Prompt */\n        .interact-prompt {\n            position: absolute;\n            bottom: 200px;\n            left: 50%;\n            transform: translateX(-50%);\n            background: var(--dark-bg);\n            color: white;\n            padding: 12px 24px;\n            border-radius: 8px;\n            border: 1px solid rgba(0, 217, 255, 0.5);\n            font-size: 18px;\n            display: none;\n            pointer-events: none;\n        }\n\n        .reload-indicator {\n            position: absolute;\n            top: 45%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            background: none;\n            color: white;\n            padding: 10px 28px;\n            border-radius: 8px;\n            border: none;\n            font-family: 'Orbitron', monospace;\n            font-size: 16px;\n            font-weight: 600;\n            letter-spacing: 1px;\n            pointer-events: none;\n            display: none;\n            text-align: center;\n            backdrop-filter: none;\n            z-index: 15;\n            box-shadow: none;\n        }\n\n        .reload-indicator.active {\n            display: block;\n            animation: reloadPulse 0.82s cubic-bezier(0.2, 0, 0.2, 1) infinite;\n        }\n\n        @keyframes reloadPulse {\n\n            0%,\n            100% {\n                opacity: 0.9;\n                transform: translate(-50%, -50%) scale(1);\n                box-shadow: none;\n            }\n\n            50% {\n                opacity: 0.62;\n                transform: translate(-50%, -50%) scale(1.045);\n                box-shadow: none;\n            }\n        }\n\n        /* Weapon Select Screen */\n        .weapon-select-screen {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: radial-gradient(ellipse at 50% 40%, rgba(10, 20, 45, 0.97) 0%, rgba(3, 5, 12, 0.99) 80%);\n            display: none;\n            flex-direction: column;\n            align-items: stretch;\n            justify-content: stretch;\n            z-index: 101;\n            overflow: hidden;\n            backdrop-filter: blur(4px);\n            box-sizing: border-box;\n        }\n\n        .weapon-select-screen.active {\n            display: flex;\n            animation: weaponSelectReveal 0.5s cubic-bezier(0.16, 1, 0.3, 1);\n        }\n\n        .weapon-select-left {\n            flex: 1;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            overflow-y: auto;\n            padding: 24px 16px;\n        }\n\n        .weapon-select-right {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            padding: 20px 16px;\n            border-top: 1px solid rgba(255, 255, 255, 0.08);\n            background: rgba(5, 8, 18, 0.5);\n        }\n\n        @keyframes weaponSelectReveal {\n            0% {\n                opacity: 0;\n            }\n\n            100% {\n                opacity: 1;\n            }\n        }\n\n        .weapon-select-title {\n            font-family: 'Orbitron', monospace;\n            font-size: 26px;\n            font-weight: 900;\n            color: #fff;\n            background: linear-gradient(180deg, #ffffff 0%, #80d8ff 60%, #00b0ff 100%);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n            filter: drop-shadow(0 0 10px rgba(0, 180, 255, 0.5));\n            margin-bottom: 8px;\n            max-width: 90%;\n            text-align: center;\n            overflow-wrap: anywhere;\n            letter-spacing: 2px;\n        }\n\n        .weapon-select-subtitle {\n            color: rgba(255, 255, 255, 0.45);\n            font-size: 13px;\n            margin-bottom: 24px;\n            max-width: 90%;\n            text-align: center;\n            overflow-wrap: anywhere;\n            letter-spacing: 2px;\n            text-transform: uppercase;\n        }\n\n        .weapon-select-grid {\n            display: grid;\n            grid-template-columns: repeat(2, 1fr);\n            gap: 12px;\n            padding: 0 16px 40px;\n            max-width: 600px;\n            width: 90%;\n        }\n\n        .weapon-select-card {\n            background: linear-gradient(160deg, rgba(15, 25, 50, 0.95) 0%, rgba(10, 15, 35, 0.98) 100%);\n            border: 1.5px solid rgba(0, 180, 255, 0.2);\n            border-radius: 14px;\n            padding: 16px 12px;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            cursor: pointer;\n            transition: all 250ms cubic-bezier(0.16, 1, 0.3, 1);\n            position: relative;\n            overflow: hidden;\n            opacity: 0;\n            animation: weaponCardEntrance 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n        }\n\n        .weapon-select-card:nth-child(1) {\n            animation-delay: 0.1s;\n        }\n\n        .weapon-select-card:nth-child(2) {\n            animation-delay: 0.15s;\n        }\n\n        .weapon-select-card:nth-child(3) {\n            animation-delay: 0.2s;\n        }\n\n        .weapon-select-card:nth-child(4) {\n            animation-delay: 0.25s;\n        }\n\n        .weapon-select-card:nth-child(5) {\n            animation-delay: 0.3s;\n        }\n\n        .weapon-select-card:nth-child(6) {\n            animation-delay: 0.35s;\n        }\n\n        .weapon-select-card:nth-child(7) {\n            animation-delay: 0.4s;\n        }\n\n        .weapon-select-card:nth-child(8) {\n            animation-delay: 0.45s;\n        }\n\n        @keyframes weaponCardEntrance {\n            0% {\n                opacity: 0;\n                transform: translateY(20px) scale(0.9);\n            }\n\n            100% {\n                opacity: 1;\n                transform: translateY(0) scale(1);\n            }\n        }\n\n        .weapon-select-card::before {\n            content: '';\n            position: absolute;\n            top: -50%;\n            left: -50%;\n            width: 200%;\n            height: 200%;\n            background: conic-gradient(from 0deg, transparent 0%, rgba(0, 180, 255, 0.08) 10%, transparent 20%);\n            animation: weaponCardSpin 6s linear infinite;\n            pointer-events: none;\n            opacity: 0;\n            transition: opacity 300ms;\n        }\n\n        .weapon-select-card:hover::before {\n            opacity: 1;\n        }\n\n        @keyframes weaponCardSpin {\n            100% {\n                transform: rotate(360deg);\n            }\n        }\n\n        .weapon-select-card:active {\n            transform: scale(0.94);\n        }\n\n        .weapon-select-card:hover {\n            border-color: rgba(0, 200, 255, 0.7);\n            box-shadow: 0 6px 30px rgba(0, 180, 255, 0.2), 0 0 40px rgba(0, 180, 255, 0.06);\n            transform: translateY(-3px);\n            background: linear-gradient(160deg, rgba(20, 35, 65, 0.95) 0%, rgba(12, 20, 45, 0.98) 100%);\n        }\n\n        .weapon-select-card img {\n            width: 56px;\n            height: 56px;\n            object-fit: contain;\n            margin-bottom: 8px;\n            filter: drop-shadow(0 0 4px rgba(0, 180, 255, 0.3));\n            transition: all 250ms;\n        }\n\n        .weapon-icon-lazy {\n            display: inline-block;\n            flex: 0 0 auto;\n            border-radius: 8px;\n            background: linear-gradient(100deg, rgba(255, 255, 255, 0.05) 0%, rgba(0, 180, 255, 0.16) 45%, rgba(255, 255, 255, 0.05) 90%);\n            background-size: 220% 100%;\n            opacity: 0.72;\n            animation: weaponIconLazyShimmer 1.1s linear infinite;\n            transition: opacity 220ms ease, filter 220ms ease, transform 220ms ease;\n        }\n\n        .weapon-icon-lazy.is-loaded {\n            border-radius: 0;\n            opacity: 1;\n            animation: weaponIconLoaded 220ms ease-out;\n        }\n\n        @keyframes weaponIconLazyShimmer {\n            0% {\n                background-position: 180% 0;\n            }\n\n            100% {\n                background-position: -40% 0;\n            }\n        }\n\n        @keyframes weaponIconLoaded {\n            0% {\n                opacity: 0;\n                transform: scale(0.94);\n            }\n\n            100% {\n                opacity: 1;\n                transform: scale(1);\n            }\n        }\n\n        .weapon-select-card:hover img {\n            filter: drop-shadow(0 0 8px rgba(0, 200, 255, 0.5)) brightness(1.1);\n            transform: scale(1.08);\n        }\n\n        .weapon-select-card:hover .weapon-icon-lazy.is-loaded {\n            filter: drop-shadow(0 0 8px rgba(0, 200, 255, 0.5)) brightness(1.1);\n            transform: scale(1.08);\n        }\n\n        .weapon-select-card-name {\n            font-family: 'Orbitron', monospace;\n            font-size: 12px;\n            color: rgba(255, 255, 255, 0.9);\n            font-weight: 700;\n            margin-bottom: 4px;\n            text-align: center;\n            overflow-wrap: anywhere;\n            letter-spacing: 0.5px;\n        }\n\n        .weapon-select-card-stats {\n            font-size: 10px;\n            color: rgba(255, 255, 255, 0.5);\n            text-align: center;\n            line-height: 1.5;\n            overflow-wrap: anywhere;\n        }\n\n        /* Upgrade Select Screen - Hades-style */\n        .upgrade-select-screen {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: none;\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 90;\n            pointer-events: none;\n            box-sizing: border-box;\n            opacity: 0;\n            transform: translateZ(0) scale(0.985);\n            backface-visibility: hidden;\n            contain: layout paint style;\n            isolation: isolate;\n            will-change: opacity, transform;\n        }\n\n        .upgrade-select-screen.active {\n            display: flex;\n            opacity: 1;\n            transform: translateZ(0) scale(1);\n            animation: upgradeScreenReveal 0.42s cubic-bezier(0.16, 1, 0.3, 1);\n        }\n\n        @keyframes upgradeScreenReveal {\n            0% {\n                opacity: 0;\n                transform: translateZ(0) scale(1.035);\n            }\n\n            100% {\n                opacity: 1;\n                transform: translateZ(0) scale(1);\n            }\n        }\n\n        .upgrade-select-screen::before {\n            display: none;\n        }\n\n        .upgrade-select-title {\n            font-family: 'Orbitron', monospace;\n            font-size: 28px;\n            font-weight: 900;\n            color: #fff;\n            background: linear-gradient(180deg, #ffffff 0%, #ffd54f 50%, #ff8f00 100%);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n            text-shadow: none;\n            margin-bottom: 6px;\n            letter-spacing: 3px;\n            text-transform: uppercase;\n            max-width: 92%;\n            text-align: center;\n            overflow-wrap: anywhere;\n            position: relative;\n            isolation: isolate;\n            animation: upgradeTitleEntrance 0.72s cubic-bezier(0.12, 1.45, 0.24, 1);\n            transform: translateZ(0);\n            will-change: transform, opacity;\n        }\n\n        .upgrade-select-title::before {\n            content: attr(data-text);\n            position: absolute;\n            inset: 0;\n            color: rgba(255, 178, 40, 0.62);\n            -webkit-text-fill-color: rgba(255, 178, 40, 0.62);\n            background: none;\n            text-align: inherit;\n            overflow-wrap: inherit;\n            pointer-events: none;\n            z-index: -1;\n            filter: blur(10px);\n            opacity: 0.74;\n            transform: translateZ(0) scale(1);\n            animation: upgradeTitleGlow 2.1s ease-in-out infinite;\n            will-change: opacity, transform;\n        }\n\n        @keyframes upgradeTitleGlow {\n\n            0%,\n            100% {\n                opacity: 0.58;\n                transform: translateZ(0) scale(0.995);\n            }\n\n            50% {\n                opacity: 0.9;\n                transform: translateZ(0) scale(1.018);\n            }\n        }\n\n        @keyframes upgradeTitleEntrance {\n            0% {\n                opacity: 0;\n                transform: translateY(-34px) scale(0.72) rotate(-1deg);\n            }\n\n            58% {\n                opacity: 1;\n                transform: translateY(6px) scale(1.08) rotate(0.5deg);\n            }\n\n            100% {\n                opacity: 1;\n                transform: translateY(0) scale(1) rotate(0);\n            }\n        }\n\n        @keyframes titlePulse {\n\n            0%,\n            100% {\n                filter: drop-shadow(0 0 12px rgba(255, 170, 0, 0.5));\n            }\n\n            50% {\n                filter: drop-shadow(0 0 20px rgba(255, 170, 0, 0.7));\n            }\n        }\n\n        .upgrade-select-subtitle {\n            color: rgba(255, 255, 255, 0.45);\n            font-size: 13px;\n            margin-bottom: 32px;\n            font-family: 'Inter', sans-serif;\n            letter-spacing: 3px;\n            text-transform: uppercase;\n            max-width: 90%;\n            text-align: center;\n            overflow-wrap: anywhere;\n            animation: upgradeTitleEntrance 0.8s cubic-bezier(0.16, 1, 0.3, 1) 0.1s both;\n        }\n\n        .upgrade-select-options {\n            display: flex;\n            flex-direction: column;\n            gap: 16px;\n            width: calc(90% + 64px);\n            max-width: 504px;\n            padding: 24px 32px;\n            margin: -24px -32px;\n            box-sizing: border-box;\n            perspective: 800px;\n            contain: layout paint style;\n        }\n\n        .upgrade-option-card {\n            --upgrade-selected-border: rgba(255, 220, 80, 1);\n            --upgrade-selected-glow: 0 0 52px rgba(255, 190, 50, 0.55), 0 0 96px rgba(255, 160, 0, 0.18);\n            pointer-events: auto;\n            background: linear-gradient(145deg, rgba(25, 20, 45, 0.95) 0%, rgba(15, 12, 30, 0.98) 100%);\n            border: 2px solid rgba(255, 190, 60, 0.25);\n            border-radius: 18px;\n            padding: 20px 22px;\n            display: flex;\n            align-items: center;\n            gap: 18px;\n            cursor: pointer;\n            transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1), border-color 180ms ease;\n            position: relative;\n            overflow: hidden;\n            opacity: 0;\n            transform: translate3d(0, 30px, 0) scale(0.92);\n            animation: upgradeCardEntrance 0.58s cubic-bezier(0.12, 1.5, 0.24, 1) forwards;\n            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.04);\n            backface-visibility: hidden;\n            contain: layout paint style;\n            will-change: opacity, transform;\n        }\n\n        .upgrade-option-card>* {\n            position: relative;\n            z-index: 1;\n        }\n\n        .upgrade-option-card:nth-child(1) {\n            animation-delay: 0.15s;\n        }\n\n        .upgrade-option-card:nth-child(2) {\n            animation-delay: 0.30s;\n        }\n\n        .upgrade-option-card:nth-child(3) {\n            animation-delay: 0.45s;\n        }\n\n        @keyframes upgradeCardEntrance {\n            0% {\n                opacity: 0;\n                transform: translate3d(-10px, 46px, 0) scale(0.86) rotateX(18deg);\n            }\n\n            58% {\n                opacity: 1;\n                transform: translate3d(2px, -7px, 0) scale(1.045) rotateX(-3deg);\n            }\n\n            82% {\n                opacity: 1;\n                transform: translate3d(0, 2px, 0) scale(0.99);\n            }\n\n            100% {\n                opacity: 1;\n                transform: translate3d(0, 0, 0) scale(1) rotateX(0);\n            }\n        }\n\n        .upgrade-option-card::before {\n            content: '';\n            position: absolute;\n            top: 0;\n            left: -100%;\n            right: 0;\n            bottom: 0;\n            width: 200%;\n            background: linear-gradient(90deg, transparent 0%, rgba(255, 200, 60, 0.06) 25%, rgba(255, 200, 60, 0.12) 50%, rgba(255, 200, 60, 0.06) 75%, transparent 100%);\n            animation: cardShimmer 3s ease-in-out infinite;\n            pointer-events: none;\n            z-index: 0;\n            transform: translate3d(-50%, 0, 0);\n            will-change: transform;\n        }\n\n        @keyframes cardShimmer {\n            0% {\n                transform: translate3d(-50%, 0, 0);\n            }\n\n            100% {\n                transform: translate3d(50%, 0, 0);\n            }\n        }\n\n        .upgrade-option-card::after {\n            content: '';\n            position: absolute;\n            top: -1px;\n            left: -1px;\n            right: -1px;\n            bottom: -1px;\n            border-radius: 18px;\n            background: linear-gradient(135deg, rgba(255, 190, 60, 0.3) 0%, transparent 40%, transparent 60%, rgba(255, 190, 60, 0.15) 100%);\n            opacity: 0;\n            transform: translateZ(0) scale(1);\n            transition: opacity 220ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);\n            pointer-events: none;\n            z-index: 0;\n            will-change: opacity, transform;\n        }\n\n        .upgrade-option-card:hover::after,\n        .upgrade-option-card:active::after {\n            opacity: 1;\n            transform: translateZ(0) scale(1.018);\n        }\n\n        .upgrade-option-card:active {\n            transform: translateZ(0) scale(0.96) !important;\n            transition-duration: 100ms;\n        }\n\n        .upgrade-option-card:hover {\n            border-color: rgba(255, 190, 60, 0.7);\n            box-shadow: 0 8px 40px rgba(255, 170, 0, 0.2), 0 0 60px rgba(255, 170, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.08);\n            transform: translate3d(0, -4px, 0) scale(1.02);\n            background: linear-gradient(145deg, rgba(35, 25, 55, 0.95) 0%, rgba(20, 15, 40, 0.98) 100%);\n        }\n\n        .upgrade-option-card.is-selected {\n            animation: none;\n            opacity: 1;\n            border-color: var(--upgrade-selected-border);\n            box-shadow: var(--upgrade-selected-glow), inset 0 1px 0 rgba(255, 255, 255, 0.10);\n            transform: translateZ(0) scale(1.08);\n        }\n\n        .upgrade-option-card.is-selected::after {\n            opacity: 1;\n            transform: translateZ(0) scale(1.025);\n        }\n\n        .upgrade-option-card.is-dismissing {\n            animation: none;\n            opacity: 0;\n            transform: translateZ(0) scale(0.88);\n            transition: transform 300ms cubic-bezier(0.4, 0, 1, 1), opacity 300ms cubic-bezier(0.4, 0, 1, 1);\n        }\n\n        .upgrade-option-card.weapon-card {\n            border-color: rgba(0, 180, 255, 0.3);\n            background: linear-gradient(145deg, rgba(12, 25, 55, 0.95) 0%, rgba(8, 15, 40, 0.98) 100%);\n        }\n\n        .upgrade-option-card.weapon-card::before {\n            background: linear-gradient(90deg, transparent 0%, rgba(0, 180, 255, 0.06) 25%, rgba(0, 180, 255, 0.12) 50%, rgba(0, 180, 255, 0.06) 75%, transparent 100%);\n        }\n\n        .upgrade-option-card.weapon-card::after {\n            background: linear-gradient(135deg, rgba(0, 180, 255, 0.3) 0%, transparent 40%, transparent 60%, rgba(0, 180, 255, 0.15) 100%);\n        }\n\n        .upgrade-option-card.weapon-card:hover {\n            border-color: rgba(0, 200, 255, 0.8);\n            box-shadow: 0 8px 40px rgba(0, 180, 255, 0.2), 0 0 60px rgba(0, 180, 255, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.08);\n            background: linear-gradient(145deg, rgba(15, 30, 65, 0.95) 0%, rgba(10, 18, 50, 0.98) 100%);\n        }\n\n        .upgrade-option-icon {\n            width: 58px;\n            height: 58px;\n            background: radial-gradient(circle at center, rgba(255, 190, 60, 0.15) 0%, rgba(255, 190, 60, 0.03) 70%);\n            border: 1.5px solid rgba(255, 190, 60, 0.3);\n            border-radius: 14px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-size: 26px;\n            flex-shrink: 0;\n            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), inset 0 0 20px rgba(255, 190, 60, 0.05);\n            position: relative;\n            overflow: hidden;\n            transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1), border-color 180ms ease, box-shadow 180ms ease;\n            transform: translateZ(0);\n            will-change: transform;\n        }\n\n        .upgrade-option-card:hover .upgrade-option-icon {\n            border-color: rgba(255, 190, 60, 0.6);\n            box-shadow: 0 4px 20px rgba(255, 170, 0, 0.25), inset 0 0 30px rgba(255, 190, 60, 0.1);\n            transform: scale(1.05);\n        }\n\n        .upgrade-option-icon::before {\n            content: '';\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            width: 80%;\n            height: 80%;\n            transform: translate(-50%, -50%);\n            border-radius: 50%;\n            background: radial-gradient(circle, rgba(255, 200, 60, 0.2) 0%, transparent 70%);\n            animation: iconPulse 2s ease-in-out infinite;\n        }\n\n        @keyframes iconPulse {\n\n            0%,\n            100% {\n                opacity: 0.5;\n                transform: translate(-50%, -50%) scale(0.8);\n            }\n\n            50% {\n                opacity: 1;\n                transform: translate(-50%, -50%) scale(1.2);\n            }\n        }\n\n        .weapon-card .upgrade-option-icon {\n            background: radial-gradient(circle at center, rgba(0, 180, 255, 0.15) 0%, rgba(0, 180, 255, 0.03) 70%);\n            border-color: rgba(0, 180, 255, 0.35);\n            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), inset 0 0 20px rgba(0, 180, 255, 0.05);\n        }\n\n        .weapon-card .upgrade-option-icon::before {\n            background: radial-gradient(circle, rgba(0, 180, 255, 0.2) 0%, transparent 70%);\n        }\n\n        .weapon-card:hover .upgrade-option-icon {\n            border-color: rgba(0, 200, 255, 0.7);\n            box-shadow: 0 4px 20px rgba(0, 180, 255, 0.3), inset 0 0 30px rgba(0, 180, 255, 0.1);\n        }\n\n        .upgrade-option-icon img {\n            width: 40px;\n            height: 40px;\n            object-fit: contain;\n            filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.3));\n            transition: filter 280ms;\n            position: relative;\n            z-index: 1;\n        }\n\n        .upgrade-option-card:hover .upgrade-option-icon img {\n            filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.5)) brightness(1.1);\n        }\n\n        .upgrade-option-info {\n            flex: 1;\n            min-width: 0;\n        }\n\n        .upgrade-option-name {\n            font-family: 'Orbitron', monospace;\n            font-size: 14px;\n            color: #ffd54f;\n            font-weight: 700;\n            margin-bottom: 6px;\n            letter-spacing: 0.8px;\n            overflow-wrap: anywhere;\n            text-shadow: 0 0 8px rgba(255, 200, 60, 0.3);\n            transition: color 180ms ease, text-shadow 180ms ease;\n        }\n\n        .upgrade-option-card:hover .upgrade-option-name {\n            color: #ffe082;\n            text-shadow: 0 0 12px rgba(255, 200, 60, 0.5);\n        }\n\n        .weapon-card .upgrade-option-name {\n            color: #80d8ff;\n            text-shadow: 0 0 8px rgba(0, 180, 255, 0.3);\n        }\n\n        .weapon-card:hover .upgrade-option-name {\n            color: #b3e5fc;\n            text-shadow: 0 0 12px rgba(0, 180, 255, 0.5);\n        }\n\n        .upgrade-option-card.rarity-weapon .rarity-badge {\n            color: #80d8ff;\n            border-color: rgba(0, 180, 255, 0.6);\n            background: rgba(0, 180, 255, 0.10);\n        }\n\n        /* === RARITY TIERS === */\n        .upgrade-option-card.rarity-bronze {\n            border-color: rgba(60, 175, 105, 0.42);\n            background: linear-gradient(145deg, rgba(13, 38, 26, 0.95) 0%, rgba(8, 22, 16, 0.98) 100%);\n        }\n\n        .upgrade-option-card.rarity-bronze::before {\n            background: linear-gradient(90deg, transparent 0%, rgba(60, 175, 105, 0.06) 25%, rgba(60, 175, 105, 0.12) 50%, rgba(60, 175, 105, 0.06) 75%, transparent 100%);\n        }\n\n        .upgrade-option-card.rarity-bronze::after {\n            background: linear-gradient(135deg, rgba(60, 175, 105, 0.28) 0%, transparent 40%, transparent 60%, rgba(40, 130, 80, 0.16) 100%);\n        }\n\n        .upgrade-option-card.rarity-bronze:hover {\n            border-color: rgba(90, 215, 135, 0.72);\n            box-shadow: 0 6px 30px rgba(60, 175, 105, 0.22), 0 0 40px rgba(60, 175, 105, 0.10);\n            background: linear-gradient(145deg, rgba(16, 48, 32, 0.95) 0%, rgba(10, 28, 19, 0.98) 100%);\n        }\n\n        .upgrade-option-card.rarity-bronze .upgrade-option-name {\n            color: #66d28d;\n            text-shadow: 0 0 6px rgba(60, 175, 105, 0.42);\n        }\n\n        .upgrade-option-card.rarity-bronze:hover .upgrade-option-name {\n            color: #8df0b2;\n            text-shadow: 0 0 10px rgba(90, 215, 135, 0.62);\n        }\n\n        .upgrade-option-card.rarity-bronze .upgrade-option-icon {\n            background: radial-gradient(circle at center, rgba(60, 175, 105, 0.14) 0%, rgba(60, 175, 105, 0.03) 70%);\n            border-color: rgba(60, 175, 105, 0.34);\n        }\n\n        .upgrade-option-card.rarity-bronze .rarity-badge {\n            color: #66d28d;\n            border-color: rgba(60, 175, 105, 0.55);\n            background: rgba(60, 175, 105, 0.09);\n        }\n\n        .upgrade-option-card.rarity-epic {\n            border-color: rgba(160, 80, 255, 0.45);\n            background: linear-gradient(145deg, rgba(28, 16, 48, 0.96) 0%, rgba(16, 8, 32, 0.98) 100%);\n        }\n\n        .upgrade-option-card.rarity-epic::before {\n            background: linear-gradient(90deg, transparent 0%, rgba(160, 80, 255, 0.07) 25%, rgba(160, 80, 255, 0.14) 50%, rgba(160, 80, 255, 0.07) 75%, transparent 100%);\n        }\n\n        .upgrade-option-card.rarity-epic::after {\n            background: linear-gradient(135deg, rgba(160, 80, 255, 0.35) 0%, transparent 40%, transparent 60%, rgba(160, 80, 255, 0.18) 100%);\n        }\n\n        .upgrade-option-card.rarity-epic:hover {\n            border-color: rgba(180, 100, 255, 0.8);\n            box-shadow: 0 8px 40px rgba(160, 80, 255, 0.3), 0 0 60px rgba(160, 80, 255, 0.12);\n            background: linear-gradient(145deg, rgba(35, 20, 58, 0.96) 0%, rgba(20, 10, 40, 0.98) 100%);\n        }\n\n        .upgrade-option-card.rarity-epic .upgrade-option-name {\n            color: #c77dff;\n            text-shadow: 0 0 8px rgba(160, 80, 255, 0.5);\n        }\n\n        .upgrade-option-card.rarity-epic:hover .upgrade-option-name {\n            color: #d9a0ff;\n            text-shadow: 0 0 14px rgba(180, 100, 255, 0.7);\n        }\n\n        .upgrade-option-card.rarity-epic .upgrade-option-icon {\n            background: radial-gradient(circle at center, rgba(160, 80, 255, 0.15) 0%, rgba(160, 80, 255, 0.03) 70%);\n            border-color: rgba(160, 80, 255, 0.4);\n            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), inset 0 0 20px rgba(160, 80, 255, 0.08);\n        }\n\n        .upgrade-option-card.rarity-epic .rarity-badge {\n            color: #c77dff;\n            border-color: rgba(160, 80, 255, 0.6);\n            background: rgba(160, 80, 255, 0.1);\n        }\n\n        .upgrade-option-card.rarity-legendary {\n            border-color: rgba(255, 200, 50, 0.5);\n            background: linear-gradient(145deg, rgba(40, 30, 12, 0.96) 0%, rgba(22, 16, 6, 0.98) 100%);\n            animation: upgradeCardEntrance 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n            box-shadow: 0 4px 22px rgba(255, 200, 50, 0.18), 0 0 42px rgba(255, 180, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.06);\n        }\n\n        @keyframes legendaryGlow {\n\n            0%,\n            100% {\n                opacity: 0.48;\n                transform: translateZ(0) scale(1);\n            }\n\n            50% {\n                opacity: 0.82;\n                transform: translateZ(0) scale(1.018);\n            }\n        }\n\n        .upgrade-option-card.rarity-legendary::before {\n            background: linear-gradient(90deg, transparent 0%, rgba(255, 200, 50, 0.08) 25%, rgba(255, 200, 50, 0.16) 50%, rgba(255, 200, 50, 0.08) 75%, transparent 100%);\n        }\n\n        .upgrade-option-card.rarity-legendary::after {\n            background: linear-gradient(135deg, rgba(255, 200, 50, 0.4) 0%, transparent 35%, transparent 65%, rgba(255, 200, 50, 0.2) 100%);\n            opacity: 0.6;\n            animation: legendaryGlow 2.5s ease-in-out infinite;\n        }\n\n        .upgrade-option-card.rarity-legendary:hover {\n            border-color: rgba(255, 220, 80, 0.9);\n            box-shadow: 0 10px 50px rgba(255, 200, 50, 0.4), 0 0 80px rgba(255, 180, 0, 0.15);\n            background: linear-gradient(145deg, rgba(50, 38, 14, 0.96) 0%, rgba(28, 20, 8, 0.98) 100%);\n        }\n\n        .upgrade-option-card.rarity-legendary .upgrade-option-name {\n            color: #ffd740;\n            text-shadow: 0 0 10px rgba(255, 200, 50, 0.6);\n            letter-spacing: 1.2px;\n        }\n\n        .upgrade-option-card.rarity-legendary:hover .upgrade-option-name {\n            color: #ffe566;\n            text-shadow: 0 0 16px rgba(255, 220, 80, 0.8);\n        }\n\n        .upgrade-option-card.rarity-legendary .upgrade-option-icon {\n            background: radial-gradient(circle at center, rgba(255, 200, 50, 0.18) 0%, rgba(255, 200, 50, 0.04) 70%);\n            border-color: rgba(255, 200, 50, 0.5);\n            box-shadow: 0 4px 20px rgba(255, 180, 0, 0.2), inset 0 0 24px rgba(255, 200, 50, 0.1);\n        }\n\n        .upgrade-option-card.rarity-legendary .rarity-badge {\n            color: #ffd740;\n            border-color: rgba(255, 200, 50, 0.7);\n            background: rgba(255, 200, 50, 0.12);\n            text-shadow: 0 0 6px rgba(255, 200, 50, 0.5);\n        }\n\n        .rarity-badge {\n            position: absolute;\n            top: 8px;\n            right: 8px;\n            font-family: 'Orbitron', monospace;\n            font-size: 9px;\n            font-weight: 700;\n            letter-spacing: 1.5px;\n            padding: 3px 8px;\n            border-radius: 4px;\n            border: 1px solid rgba(255, 255, 255, 0.2);\n            background: rgba(255, 255, 255, 0.05);\n            color: rgba(255, 255, 255, 0.6);\n            pointer-events: none;\n            text-transform: uppercase;\n        }\n\n        .upgrade-option-desc {\n            font-size: 12px;\n            color: rgba(255, 255, 255, 0.55);\n            line-height: 1.5;\n            font-family: 'Inter', sans-serif;\n            overflow-wrap: anywhere;\n            letter-spacing: 0.2px;\n            transition: color 280ms;\n        }\n\n        .upgrade-option-card:hover .upgrade-option-desc {\n            color: rgba(255, 255, 255, 0.75);\n        }\n\n        .upgrade-countdown {\n            font-family: 'Orbitron', monospace;\n            font-size: 52px;\n            font-weight: 900;\n            color: rgba(255, 200, 60, 0.95);\n            text-shadow: 0 0 24px rgba(255, 170, 0, 0.7), 0 0 60px rgba(255, 130, 0, 0.3);\n            margin-bottom: 12px;\n            pointer-events: none;\n            animation: countdownPulse 1s ease-in-out infinite;\n            transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), color 0.3s;\n            transform: translateZ(0);\n            contain: paint style;\n            will-change: transform;\n        }\n\n        .upgrade-countdown-value {\n            display: block;\n            transform: translateZ(0);\n            will-change: transform;\n        }\n\n        .upgrade-countdown-value.countdown-bump {\n            animation: countdownBump 180ms cubic-bezier(0.12, 1.4, 0.22, 1);\n        }\n\n        .upgrade-countdown.urgent {\n            color: rgba(255, 80, 60, 0.95);\n            text-shadow: 0 0 24px rgba(255, 50, 30, 0.8), 0 0 60px rgba(255, 30, 0, 0.4);\n        }\n\n        @keyframes countdownPulse {\n\n            0%,\n            100% {\n                transform: translateZ(0) scale(1) rotate(0deg);\n            }\n\n            38% {\n                transform: translateZ(0) scale(1.16) rotate(-1.5deg);\n            }\n\n            62% {\n                transform: translateZ(0) scale(0.96) rotate(1deg);\n            }\n        }\n\n        @keyframes countdownBump {\n            0% {\n                transform: translateZ(0) scale(1);\n            }\n\n            45% {\n                transform: translateZ(0) scale(1.3);\n            }\n\n            100% {\n                transform: translateZ(0) scale(1);\n            }\n        }\n\n        .upgrade-select-options {\n            pointer-events: auto;\n        }\n\n        /* New Weapon Showcase Overlay */\n        .weapon-showcase-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: radial-gradient(ellipse at 50% 50%, rgba(0, 20, 40, 0.92) 0%, rgba(0, 0, 0, 0.96) 80%);\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 95;\n            pointer-events: none;\n        }\n\n        .weapon-showcase-overlay.active {\n            display: flex;\n        }\n\n        @keyframes showcaseReveal {\n            0% {\n                opacity: 0;\n            }\n\n            100% {\n                opacity: 1;\n            }\n        }\n\n        .weapon-showcase-overlay.fade-out {\n            opacity: 0;\n            transition: opacity 0.3s ease;\n        }\n\n        @keyframes showcaseFadeOut {\n            0% {\n                opacity: 1;\n            }\n\n            100% {\n                opacity: 0;\n            }\n        }\n\n        .showcase-label {\n            font-family: 'Orbitron', monospace;\n            font-size: 16px;\n            font-weight: 600;\n            color: rgba(0, 200, 255, 0.7);\n            letter-spacing: 4px;\n            text-transform: uppercase;\n            margin-bottom: 16px;\n            opacity: 0;\n            animation: showcaseLabelIn 0.5s ease-out 0.2s forwards;\n        }\n\n        @keyframes showcaseLabelIn {\n            0% {\n                opacity: 0;\n                transform: translateY(10px);\n            }\n\n            100% {\n                opacity: 1;\n                transform: translateY(0);\n            }\n        }\n\n        .showcase-weapon-name {\n            font-family: 'Orbitron', monospace;\n            font-size: 42px;\n            font-weight: 900;\n            color: #fff;\n            background: linear-gradient(180deg, #ffffff 0%, #80d8ff 50%, #0091ea 100%);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            background-clip: text;\n            filter: drop-shadow(0 0 20px rgba(0, 180, 255, 0.7));\n            letter-spacing: 3px;\n            margin-bottom: 32px;\n            opacity: 0;\n            animation: showcaseNameIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.3s forwards;\n            text-align: center;\n            max-width: 90%;\n        }\n\n        @keyframes showcaseNameIn {\n            0% {\n                opacity: 0;\n                transform: scale(0.62) translateY(26px) rotate(-1deg);\n            }\n\n            60% {\n                opacity: 1;\n                transform: scale(1.08) translateY(-4px) rotate(0.5deg);\n            }\n\n            100% {\n                opacity: 1;\n                transform: scale(1) translateY(0) rotate(0);\n            }\n        }\n\n        .showcase-weapon-icon {\n            width: 180px;\n            height: 180px;\n            object-fit: contain;\n            filter: drop-shadow(0 0 30px rgba(0, 200, 255, 0.6)) drop-shadow(0 0 60px rgba(0, 150, 255, 0.3));\n            opacity: 0;\n            animation: showcaseIconIn 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.15s forwards;\n        }\n\n        @keyframes showcaseIconIn {\n            0% {\n                opacity: 0;\n                transform: scale(0.38) rotate(-16deg);\n                filter: brightness(2.1);\n            }\n\n            48% {\n                opacity: 1;\n                transform: scale(1.18) rotate(4deg);\n            }\n\n            72% {\n                transform: scale(0.95) rotate(-1.5deg);\n            }\n\n            100% {\n                opacity: 1;\n                transform: scale(1) rotate(0deg);\n                filter: brightness(1);\n            }\n        }\n\n        .showcase-weapon-stats {\n            font-family: 'Inter', sans-serif;\n            font-size: 14px;\n            color: rgba(255, 255, 255, 0.6);\n            letter-spacing: 1px;\n            opacity: 0;\n            animation: showcaseLabelIn 0.5s ease-out 0.5s forwards;\n            text-align: center;\n            max-width: 80%;\n        }\n\n        /* Pause Button */\n        .pause-btn {\n            position: absolute;\n            top: 70px;\n            right: var(--space-sm);\n            width: 44px;\n            height: 44px;\n            background: var(--dark-bg);\n            border: 1.5px solid rgba(255, 255, 255, 0.2);\n            border-radius: 10px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            pointer-events: auto;\n            transition: all 200ms ease;\n        }\n\n        .pause-btn:active {\n            transform: scale(0.9);\n            background: rgba(0, 217, 255, 0.2);\n        }\n\n        .pause-btn svg {\n            width: 20px;\n            height: 20px;\n            fill: rgba(255, 255, 255, 0.8);\n        }\n\n        /* Pause Menu Overlay */\n        .pause-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.88);\n            backdrop-filter: blur(6px);\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 150;\n            touch-action: auto;\n        }\n\n        .pause-overlay.active {\n            display: flex;\n        }\n\n        .pause-menu-title {\n            font-family: 'Orbitron', monospace;\n            font-size: 32px;\n            font-weight: 900;\n            color: #fff;\n            margin-bottom: 40px;\n            letter-spacing: 3px;\n            text-shadow: 0 0 12px rgba(0, 217, 255, 0.5);\n        }\n\n        .pause-menu-items {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 14px;\n            width: 280px;\n        }\n\n        .pause-menu-item {\n            width: 100%;\n            height: 54px;\n            background: linear-gradient(135deg, rgba(20, 30, 50, 0.9) 0%, rgba(10, 15, 30, 0.95) 100%);\n            border: 1.5px solid rgba(0, 180, 255, 0.25);\n            border-radius: 12px;\n            color: white;\n            font-family: 'Orbitron', monospace;\n            font-size: 16px;\n            font-weight: 600;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: all 200ms ease;\n            letter-spacing: 1px;\n        }\n\n        .pause-menu-item:hover,\n        .pause-menu-item:active {\n            border-color: rgba(0, 200, 255, 0.7);\n            background: linear-gradient(135deg, rgba(0, 40, 80, 0.9) 0%, rgba(0, 20, 50, 0.95) 100%);\n            box-shadow: 0 0 20px rgba(0, 180, 255, 0.2);\n            transform: translateY(-2px);\n        }\n\n        .pause-menu-item:active {\n            transform: scale(0.96);\n        }\n\n        /* Pause Sub Panels */\n        .pause-sub-panel {\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            width: 320px;\n            max-width: 90%;\n        }\n\n        .pause-sub-panel.active {\n            display: flex;\n        }\n\n        .pause-sub-title {\n            font-family: 'Orbitron', monospace;\n            font-size: 22px;\n            font-weight: 700;\n            color: var(--cyan);\n            margin-bottom: 28px;\n            letter-spacing: 2px;\n        }\n\n        .volume-row {\n            width: 100%;\n            margin-bottom: 20px;\n        }\n\n        .volume-label {\n            font-family: 'Inter', sans-serif;\n            font-size: 14px;\n            color: rgba(255, 255, 255, 0.7);\n            margin-bottom: 8px;\n            display: flex;\n            justify-content: space-between;\n        }\n\n        .volume-label span:last-child {\n            color: var(--cyan);\n            font-weight: 600;\n        }\n\n        .volume-slider {\n            -webkit-appearance: none;\n            appearance: none;\n            width: 100%;\n            height: 6px;\n            background: rgba(255, 255, 255, 0.1);\n            border-radius: 3px;\n            outline: none;\n            touch-action: auto;\n        }\n\n        .volume-slider::-webkit-slider-thumb {\n            -webkit-appearance: none;\n            appearance: none;\n            width: 22px;\n            height: 22px;\n            border-radius: 50%;\n            background: var(--cyan);\n            cursor: pointer;\n            box-shadow: 0 0 8px rgba(0, 217, 255, 0.5);\n        }\n\n        .volume-slider::-moz-range-thumb {\n            width: 22px;\n            height: 22px;\n            border-radius: 50%;\n            background: var(--cyan);\n            cursor: pointer;\n            border: none;\n        }\n\n        .pause-back-btn {\n            margin-top: 24px;\n            width: 160px;\n            height: 44px;\n            background: rgba(255, 255, 255, 0.05);\n            border: 1px solid rgba(255, 255, 255, 0.2);\n            border-radius: 8px;\n            color: rgba(255, 255, 255, 0.7);\n            font-family: 'Orbitron', monospace;\n            font-size: 14px;\n            cursor: pointer;\n            transition: all 200ms ease;\n        }\n\n        .pause-back-btn:hover,\n        .pause-back-btn:active {\n            border-color: rgba(255, 255, 255, 0.5);\n            color: white;\n        }\n\n        /* Language Panel */\n        .language-grid {\n            display: grid;\n            grid-template-columns: repeat(2, 1fr);\n            gap: 10px;\n            width: 100%;\n            max-height: 400px;\n            overflow-y: auto;\n        }\n\n        .language-item {\n            padding: 12px 8px;\n            background: rgba(20, 30, 50, 0.9);\n            border: 1.5px solid rgba(255, 255, 255, 0.1);\n            border-radius: 10px;\n            color: rgba(255, 255, 255, 0.8);\n            font-size: 14px;\n            text-align: center;\n            cursor: pointer;\n            transition: all 200ms ease;\n        }\n\n        .language-item:hover,\n        .language-item:active {\n            border-color: var(--cyan);\n            background: rgba(0, 40, 80, 0.8);\n        }\n\n        .language-item.current {\n            border-color: var(--cyan);\n            background: rgba(0, 60, 100, 0.6);\n            color: var(--cyan);\n            font-weight: 600;\n        }\n\n        /* Orientation Toggle */\n        .orientation-toggle {\n            display: flex;\n            flex-direction: row;\n            gap: 16px;\n            margin-top: 0;\n            padding-bottom: 0;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .orientation-btn {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            gap: 10px;\n            padding: 20px 32px;\n            width: 140px;\n            height: 100px;\n            background: rgba(15, 20, 35, 0.85);\n            border: 2px solid rgba(255, 255, 255, 0.15);\n            border-radius: 16px;\n            color: rgba(255, 255, 255, 0.6);\n            font-family: 'Inter', sans-serif;\n            font-size: 14px;\n            font-weight: 600;\n            cursor: pointer;\n            transition: all 200ms ease;\n        }\n\n        .orientation-btn svg {\n            width: 36px;\n            height: 36px;\n        }\n\n        .orientation-btn.active {\n            border-color: rgba(0, 200, 255, 0.7);\n            color: var(--cyan);\n            background: rgba(0, 30, 60, 0.8);\n            box-shadow: 0 0 16px rgba(0, 200, 255, 0.3);\n        }\n\n        .orientation-btn:active {\n            transform: scale(0.94);\n        }\n\n        /* Landscape weapon select layout: left/right split */\n        #game-world.landscape-mode .weapon-select-screen {\n            flex-direction: row !important;\n        }\n\n        #game-world.landscape-mode .weapon-select-left {\n            flex: 1;\n            padding: 24px 16px;\n        }\n\n        #game-world.landscape-mode .weapon-select-right {\n            width: 200px;\n            border-top: none;\n            border-left: 1px solid rgba(255, 255, 255, 0.08);\n            padding: 24px 16px;\n        }\n\n        #game-world.landscape-mode .weapon-select-right .orientation-toggle {\n            flex-direction: column;\n            margin-top: 0;\n            padding-bottom: 0;\n        }\n\n        .pause-orientation-toggle {\n            gap: 10px;\n            margin: 8px 0 18px;\n        }\n\n        .pause-orientation-toggle .orientation-btn {\n            width: 120px;\n            height: 82px;\n            padding: 12px 18px;\n            border-radius: 12px;\n            font-size: 12px;\n        }\n\n        .pause-orientation-toggle .orientation-btn svg {\n            width: 30px;\n            height: 30px;\n        }\n\n        /* Landscape mode rotation (manual, no system orientation dependency) */\n        #game-world.landscape-mode {\n            position: fixed;\n            left: 50%;\n            top: 50%;\n            width: 100vh;\n            height: 100vw;\n            transform: translate(-50%, -50%) rotate(90deg);\n            transform-origin: center center;\n            margin: 0;\n        }\n\n        /* ===== LANDSCAPE SUPPORT ===== */\n        /* Landscape has much more horizontal space \u2014 spread HUD elements to edges,\n           use larger fonts, add generous padding, and let the center stay clear for gameplay. */\n\n        #game-world.landscape-mode .health-bar-container {\n            bottom: 20px;\n            left: 50%;\n            transform: translateX(-50%);\n            width: 320px;\n            height: 32px;\n            border-radius: 8px;\n        }\n\n        #game-world.landscape-mode .health-text {\n            font-size: 14px;\n        }\n\n        #game-world.landscape-mode .weapon-slots {\n            bottom: 60px;\n            left: 50%;\n            transform: translateX(-50%);\n            gap: 10px;\n        }\n\n        #game-world.landscape-mode .weapon-slot {\n            width: 58px;\n            height: 58px;\n            border-radius: 12px;\n        }\n\n        #game-world.landscape-mode .weapon-slot img {\n            width: 40px;\n            height: 40px;\n        }\n\n        #game-world.landscape-mode .shoot-btn {\n            width: 100px;\n            height: 100px;\n        }\n\n        #game-world.landscape-mode .skill-btn {\n            bottom: 240px;\n            right: 120px;\n            width: 72px;\n            height: 72px;\n            border-radius: 50%;\n        }\n\n        #game-world.landscape-mode .reload-btn {\n            bottom: 240px;\n            right: 35px;\n            width: 72px;\n            height: 72px;\n            font-size: 14px;\n            border-radius: 50%;\n        }\n\n        #game-world.landscape-mode .scope-btn {\n            bottom: 240px;\n            right: 205px;\n            width: 72px;\n            height: 72px;\n            border-radius: 50%;\n        }\n\n        #game-world.landscape-mode .wave-display {\n            top: 14px;\n            font-size: 16px;\n            padding: 10px 32px;\n            border-radius: 10px;\n            gap: 14px;\n        }\n\n        #game-world.landscape-mode #enemies-display {\n            font-size: 16px;\n        }\n\n        #game-world.landscape-mode .ammo-display {\n            top: 14px;\n            right: 20px;\n            font-size: 26px;\n        }\n\n        #game-world.landscape-mode .ammo-display .weapon-name {\n            font-size: 12px;\n            letter-spacing: 2px;\n        }\n\n        #game-world.landscape-mode .score-display {\n            top: 14px;\n            left: 20px;\n            font-size: 24px;\n        }\n\n        #game-world.landscape-mode .score-display .score-label {\n            font-size: 11px;\n            letter-spacing: 3px;\n        }\n\n        #game-world.landscape-mode .combo-display {\n            left: 20px;\n            top: 35%;\n        }\n\n        #game-world.landscape-mode .combo-rank {\n            font-size: 56px;\n        }\n\n        #game-world.landscape-mode .combo-count {\n            font-size: 22px;\n        }\n\n        #game-world.landscape-mode .combo-bar-outer {\n            width: 160px;\n        }\n\n        #game-world.landscape-mode .pause-btn {\n            top: 14px;\n            right: 180px;\n            width: 42px;\n            height: 42px;\n            border-radius: 12px;\n        }\n\n        #game-world.landscape-mode .start-screen h1 {\n            font-size: 42px;\n        }\n\n        #game-world.landscape-mode .start-screen p {\n            font-size: 15px;\n            margin-bottom: 40px;\n            max-width: 500px;\n        }\n\n        #game-world.landscape-mode .start-btn {\n            width: 240px;\n            height: 58px;\n            font-size: 20px;\n        }\n\n        #game-world.landscape-mode .game-over-screen {\n            flex-direction: row !important;\n            align-items: stretch;\n            justify-content: space-between;\n            padding: 24px 32px;\n            box-sizing: border-box;\n            overflow-y: hidden;\n        }\n\n        #game-world.landscape-mode .game-over-left {\n            flex: 1;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            padding-right: 24px;\n            box-sizing: border-box;\n        }\n\n        #game-world.landscape-mode .game-over-right {\n            flex: 1.2;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            border-left: 1px solid rgba(255, 255, 255, 0.08);\n            padding-left: 24px;\n            box-sizing: border-box;\n            overflow-y: auto;\n            max-height: 100%;\n        }\n\n        #game-world.landscape-mode .game-over-screen h1 {\n            font-size: 28px;\n            margin-bottom: 12px;\n            text-align: center;\n        }\n\n        #game-world.landscape-mode .stats-container {\n            padding: 10px 40px;\n            border-radius: 14px;\n            margin-bottom: 12px;\n        }\n\n        #game-world.landscape-mode .stat-row {\n            font-size: 15px;\n            margin: 4px 0;\n            gap: 48px;\n        }\n\n        #game-world.landscape-mode .weapon-select-screen {\n            padding-top: 20px;\n        }\n\n        #game-world.landscape-mode .weapon-select-title {\n            font-size: 24px;\n            margin-bottom: 6px;\n        }\n\n        #game-world.landscape-mode .weapon-select-subtitle {\n            font-size: 12px;\n            margin-bottom: 20px;\n        }\n\n        #game-world.landscape-mode .weapon-select-grid {\n            grid-template-columns: repeat(4, 1fr);\n            gap: 14px;\n            max-width: 85%;\n            padding: 0 24px 24px;\n        }\n\n        #game-world.landscape-mode .weapon-select-card {\n            padding: 16px 12px;\n            border-radius: 16px;\n        }\n\n        #game-world.landscape-mode .weapon-select-card img {\n            width: 48px;\n            height: 48px;\n            margin-bottom: 10px;\n        }\n\n        #game-world.landscape-mode .weapon-select-card-name {\n            font-size: 11px;\n            margin-bottom: 4px;\n        }\n\n        #game-world.landscape-mode .weapon-select-card-stats {\n            font-size: 10px;\n        }\n\n        #game-world.landscape-mode .upgrade-select-screen {\n            justify-content: center;\n            align-items: center;\n            padding: 12px 24px;\n        }\n\n        #game-world.landscape-mode .upgrade-select-title {\n            font-size: 20px;\n            margin-bottom: 2px;\n        }\n\n        #game-world.landscape-mode .upgrade-countdown {\n            font-size: 32px;\n            margin-bottom: 2px;\n        }\n\n        #game-world.landscape-mode .upgrade-select-subtitle {\n            margin-bottom: 12px;\n            font-size: 11px;\n        }\n\n        #game-world.landscape-mode .upgrade-select-options {\n            flex-direction: row;\n            gap: 20px;\n            max-width: calc(90% + 64px);\n            width: calc(100% + 64px);\n            align-items: stretch;\n            justify-content: center;\n        }\n\n        #game-world.landscape-mode .upgrade-option-card {\n            flex-direction: column;\n            text-align: center;\n            padding: 24px 16px 20px;\n            flex: 1 1 0;\n            min-width: 0;\n            max-width: 240px;\n            width: 240px;\n            min-height: 250px;\n            border-radius: 18px;\n            gap: 16px;\n            justify-content: flex-start;\n            align-items: center;\n            border-width: 2px;\n            background: linear-gradient(170deg, rgba(30, 22, 50, 0.96) 0%, rgba(15, 10, 30, 0.98) 60%, rgba(10, 8, 22, 0.99) 100%);\n            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.05), inset 0 -2px 8px rgba(0, 0, 0, 0.3);\n            transform: translateY(0) scale(1);\n        }\n\n        #game-world.landscape-mode .upgrade-option-card:hover {\n            transform: translateY(-6px) scale(1.03);\n            box-shadow: 0 16px 48px rgba(255, 170, 0, 0.25), 0 0 80px rgba(255, 170, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.1);\n        }\n\n        #game-world.landscape-mode .upgrade-option-card.weapon-card:hover {\n            box-shadow: 0 16px 48px rgba(0, 180, 255, 0.25), 0 0 80px rgba(0, 180, 255, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.1);\n        }\n\n        #game-world.landscape-mode .upgrade-option-icon {\n            width: 68px;\n            height: 68px;\n            border-radius: 16px;\n            border-width: 2px;\n            flex-shrink: 0;\n        }\n\n        #game-world.landscape-mode .upgrade-option-icon img {\n            width: 46px;\n            height: 46px;\n        }\n\n        #game-world.landscape-mode .upgrade-option-info {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 8px;\n            flex: 1;\n        }\n\n        #game-world.landscape-mode .upgrade-option-name {\n            font-size: 15px;\n            letter-spacing: 1px;\n            margin-bottom: 2px;\n        }\n\n        #game-world.landscape-mode .upgrade-option-desc {\n            font-size: 12px;\n            line-height: 1.5;\n            max-width: 190px;\n        }\n\n        #game-world.landscape-mode .wave-banner {\n            font-size: 44px;\n            top: 40%;\n        }\n\n        #game-world.landscape-mode .weapon-showcase-overlay .showcase-weapon-name {\n            font-size: 36px;\n        }\n\n        #game-world.landscape-mode .weapon-showcase-overlay .showcase-weapon-icon {\n            width: 140px;\n            height: 140px;\n        }\n\n        #game-world.landscape-mode .weapon-showcase-overlay .showcase-label {\n            font-size: 18px;\n            letter-spacing: 5px;\n        }\n\n        #game-world.landscape-mode .weapon-showcase-overlay .showcase-weapon-stats {\n            font-size: 15px;\n        }\n\n        #game-world.landscape-mode .loading-title {\n            font-size: 30px;\n            margin-bottom: 44px;\n        }\n\n        #game-world.landscape-mode .loading-bar-outer {\n            width: 320px;\n        }\n\n        #game-world.landscape-mode #leaderboard-container {\n            max-width: 480px;\n        }\n\n        #game-world.landscape-mode .leaderboard-table td {\n            padding: 10px 10px;\n            font-size: 13px;\n        }\n\n        #game-world.landscape-mode .leaderboard-table th {\n            padding: 10px 10px;\n            font-size: 11px;\n        }\n\n        #game-world.landscape-mode .reload-indicator {\n            font-size: 18px;\n            padding: 12px 32px;\n            border-radius: 10px;\n        }\n\n        #game-world.landscape-mode .interact-prompt {\n            font-size: 16px;\n            padding: 14px 28px;\n            border-radius: 10px;\n        }\n\n        /* Weapon Tutorial Overlay - Canvas spotlight, no blur, no HUD obstruction */\n        .weapon-tutorial-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            display: none;\n            z-index: 105;\n            pointer-events: none;\n            opacity: 0;\n            transition: opacity 300ms ease;\n        }\n\n        .weapon-tutorial-overlay.active {\n            display: block;\n            opacity: 1;\n        }\n\n        .weapon-tutorial-overlay canvas {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n        }\n\n        /* Highlighting weapon slots during tutorial */\n        .weapon-slot.tutorial-highlight {\n            position: relative;\n            z-index: 110 !important;\n            border-color: var(--cyan) !important;\n            box-shadow: 0 0 25px var(--cyan), 0 0 10px rgba(0, 217, 255, 0.4) !important;\n            animation: slotPulse 1.2s infinite alternate;\n        }\n\n        .weapon-slot.tutorial-highlight::after {\n            content: '\ud83d\udc47';\n            position: absolute;\n            top: -48px;\n            left: 50%;\n            transform: translateX(-50%);\n            font-size: 32px;\n            animation: tutorialHandBounce 0.6s ease-in-out infinite alternate;\n            filter: drop-shadow(0 0 6px rgba(0, 217, 255, 0.9));\n            pointer-events: none;\n            z-index: 120;\n        }\n\n        @keyframes tutorialHandBounce {\n            0% {\n                transform: translate(-50%, 0);\n            }\n\n            100% {\n                transform: translate(-50%, 10px);\n            }\n        }\n\n        @keyframes slotPulse {\n            0% {\n                transform: scale(1);\n                box-shadow: 0 0 15px rgba(0, 217, 255, 0.4) !important;\n            }\n\n            100% {\n                transform: scale(1.08);\n                box-shadow: 0 0 30px var(--cyan), 0 0 15px rgba(0, 217, 255, 0.6) !important;\n            }\n        }\n    </style>\n</head>\n\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"game-canvas\"></canvas>\n        <div class=\"hud-overlay\" id=\"hud\">\n            <div id=\"smart-lock-container\"\n                style=\"position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2;\"></div>\n            <div class=\"crosshair\" id=\"crosshair\">\n                <span class=\"ch-arm ch-top\"></span>\n                <span class=\"ch-arm ch-bottom\"></span>\n                <span class=\"ch-arm ch-left\"></span>\n                <span class=\"ch-arm ch-right\"></span>\n                <!-- Smart Pistol decorators -->\n                <div class=\"sh-bracket-decor tl\"></div>\n                <div class=\"sh-bracket-decor tr\"></div>\n                <div class=\"sh-bracket-decor bl\"></div>\n                <div class=\"sh-bracket-decor br\"></div>\n            </div>\n            <div class=\"hit-marker\" id=\"hit-marker\">\n                <span class=\"hm-tri hm-tl\"></span>\n                <span class=\"hm-tri hm-tr\"></span>\n                <span class=\"hm-tri hm-bl\"></span>\n                <span class=\"hm-tri hm-br\"></span>\n            </div>\n            <div class=\"damage-indicator-container\">\n                <div class=\"damage-indicator top\" id=\"damage-ind-top\"></div>\n                <div class=\"damage-indicator bottom\" id=\"damage-ind-bottom\"></div>\n                <div class=\"damage-indicator left\" id=\"damage-ind-left\"></div>\n                <div class=\"damage-indicator right\" id=\"damage-ind-right\"></div>\n            </div>\n\n            <div class=\"health-bar-container\">\n                <div class=\"health-bar-fill\" id=\"health-fill\"></div>\n                <span class=\"health-text\" id=\"health-text\">100</span>\n            </div>\n\n            <div class=\"ammo-display\" id=\"ammo-display\">\n                <div class=\"weapon-name\" id=\"weapon-name\" data-i18n=\"weapons.PISTOL\">PISTOL</div>\n                <span id=\"ammo-text\">\u221e</span>\n                <div id=\"flame-heat-bar\" style=\"display:none; width: 140px; height: 10px; background: rgba(255,255,255,0.15); border: 1.5px solid rgba(255,255,255,0.3); border-radius: 5px; overflow: hidden; margin-top: 6px; float: right; clear: both; box-shadow: 0 0 10px rgba(0,0,0,0.5);\">\n                    <div id=\"flame-heat-fill\" style=\"width: 0%; height: 100%; background: linear-gradient(90deg, #ff9900, #ff0000); box-shadow: 0 0 8px #ff3300; transition: width 0.05s ease-out, background-color 0.2s;\"></div>\n                </div>\n            </div>\n\n            <div class=\"wave-display\" id=\"wave-display-container\">\n                <div style=\"display:inline-flex;align-items:center;gap:10px;\">\n                    <span id=\"wave-display\" data-i18n=\"hud.waveInitial\">WAVE 1</span>\n                </div>\n                <div style=\"display:inline-flex;align-items:center;justify-content:center;min-width:60px;\">\n                    <span id=\"enemies-display\"></span>\n                </div>\n            </div>\n            <div class=\"score-display\" id=\"score-display\">\n                <span class=\"score-label\" data-i18n=\"hud.score\">SCORE</span>\n                <span id=\"score-value\">0</span>\n            </div>\n\n            <div class=\"weapon-slots\" id=\"weapon-slots\"></div>\n\n            <div class=\"weapon-viewmodel\" id=\"weapon-viewmodel\">\n                <img id=\"viewmodel-img\" src=\"\" alt=\"\">\n            </div>\n            <div class=\"muzzle-flash\" id=\"muzzle-flash\">\n                <img id=\"flash-img\" src=\"\" alt=\"\">\n            </div>\n\n            <div class=\"combo-display\" id=\"combo-display\">\n                <div class=\"combo-rank rank-D\" id=\"combo-rank\">D</div>\n                <div class=\"combo-count\" id=\"combo-count\">0 HITS</div>\n                <div class=\"combo-bar-outer\">\n                    <div class=\"combo-bar-fill\" id=\"combo-bar-fill\"></div>\n                </div>\n            </div>\n            <div class=\"wave-banner\" id=\"wave-banner\" data-i18n=\"hud.waveInitial\">WAVE 1</div>\n            <div class=\"interact-prompt\" id=\"interact-prompt\" data-i18n=\"hud.interact\">Press E to interact</div>\n            <div class=\"reload-indicator\" id=\"reload-indicator\"></div>\n\n            <div class=\"pause-btn\" id=\"pause-btn\"><svg viewBox=\"0 0 24 24\">\n                    <rect x=\"6\" y=\"4\" width=\"4\" height=\"16\" />\n                    <rect x=\"14\" y=\"4\" width=\"4\" height=\"16\" />\n                </svg></div>\n\n            <div class=\"reload-btn\" id=\"reload-btn\" data-i18n=\"hud.reloadButton\">R</div>\n            <div class=\"scope-btn\" id=\"scope-btn\"><span class=\"scope-icon\">\ud83d\udd2d</span><span\n                    data-i18n=\"hud.scopeZoom\">x4</span></div>\n            <div class=\"shoot-container\" id=\"shoot-container\">\n                <div class=\"shoot-btn\" id=\"shoot-btn\">\n                    <span class=\"shoot-arrow arrow-up\"></span>\n                    <span class=\"shoot-arrow arrow-down\"></span>\n                    <span class=\"shoot-arrow arrow-left\"></span>\n                    <span class=\"shoot-arrow arrow-right\"></span>\n                    <span class=\"shoot-crosshair\"></span>\n                </div>\n            </div>\n            <div class=\"skill-btn\" id=\"skill-btn\" style=\"display:none;\"><span class=\"skill-icon\">\u23f3</span><span\n                    class=\"skill-label\">SLOW [Q]</span></div>\n\n            <div class=\"touch-joystick\" id=\"joystick-left\">\n                <div class=\"touch-joystick-thumb\" id=\"joystick-left-thumb\"></div>\n            </div>\n            <div class=\"touch-joystick\" id=\"joystick-right\">\n                <div class=\"touch-joystick-thumb\" id=\"joystick-right-thumb\"></div>\n            </div>\n\n            <div class=\"wasd-display\" id=\"wasd-display\">\n                <div class=\"wasd-row\">\n                    <div class=\"wasd-key\" id=\"wasd-w\">W</div>\n                </div>\n                <div class=\"wasd-row\">\n                    <div class=\"wasd-key\" id=\"wasd-a\">A</div>\n                    <div class=\"wasd-key\" id=\"wasd-s\">S</div>\n                    <div class=\"wasd-key\" id=\"wasd-d\">D</div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"loading-screen\" id=\"loading-screen\"\n            style=\"display:flex;flex-direction:column;align-items:center;justify-content:center;position:absolute;top:0;left:0;width:100%;height:100%;background:rgb(2,4,10);z-index:200;\">\n            <div class=\"loading-title\" data-i18n=\"app.titleUpper\"\n                style=\"font-family:monospace;font-size:24px;font-weight:900;color:#80d8ff;letter-spacing:3px;margin-bottom:48px;\">\n                ZOMBIE LAST SURVIVOR</div>\n            <div class=\"loading-bar-outer\"\n                style=\"width:260px;height:4px;background:rgba(255,255,255,0.06);border-radius:2px;overflow:hidden;margin-bottom:18px;\">\n                <div class=\"loading-bar-inner\" id=\"loading-bar\"\n                    style=\"height:100%;width:100%;transform:scaleX(0);transform-origin:left;background:#00b0ff;border-radius:2px;\">\n                </div>\n            </div>\n            <div class=\"loading-text\" id=\"loading-text\" data-i18n=\"loading.assets\"\n                style=\"color:rgba(255,255,255,0.35);font-size:12px;font-family:monospace;letter-spacing:2px;text-transform:uppercase;\">\n                Loading assets...</div>\n        </div>\n\n        <div class=\"start-screen\" id=\"start-screen\" style=\"display:none\">\n            <h1 data-i18n-html=\"app.titleBreak\">ZOMBIE<br>LAST SURVIVOR</h1>\n            <p data-i18n=\"start.subtitle\">Defend the bunker against endless zombie hordes</p>\n            <button class=\"start-btn\" id=\"start-btn\" data-i18n=\"start.play\">PLAY</button>\n        </div>\n\n        <div class=\"game-over-screen\" id=\"game-over-screen\">\n            <div class=\"game-over-left\">\n                <h1 data-i18n=\"gameOver.title\">BUNKER BREACHED</h1>\n                <div class=\"stats-container\">\n                    <div class=\"stat-row\"><span class=\"label\" data-i18n=\"stats.wave\">Wave</span><span class=\"value\"\n                            id=\"final-wave\">1</span></div>\n                    <div class=\"stat-row\"><span class=\"label\" data-i18n=\"stats.kills\">Kills</span><span class=\"value\"\n                            id=\"final-kills\">0</span></div>\n                    <div class=\"stat-row\"><span class=\"label\" data-i18n=\"stats.headshots\">Headshots</span><span\n                            class=\"value\" id=\"final-headshots\">0</span></div>\n                    <div class=\"stat-row\"><span class=\"label\" data-i18n=\"stats.score\">Score</span><span class=\"value\"\n                            id=\"final-score\">0</span></div>\n                    <div class=\"stat-row\"><span class=\"label\" data-i18n=\"stats.accuracy\">Accuracy</span><span\n                            class=\"value\" id=\"final-accuracy\">0%</span></div>\n                </div>\n                <button class=\"start-btn\" id=\"restart-btn\" data-i18n=\"gameOver.retry\">RETRY</button>\n            </div>\n            <div class=\"game-over-right\">\n                <div id=\"leaderboard-container\"></div>\n            </div>\n        </div>\n\n        <div class=\"weapon-select-screen\" id=\"weapon-select-screen\">\n            <div class=\"weapon-select-left\">\n                <div class=\"weapon-select-title\" data-i18n=\"weaponSelect.title\">CHOOSE WEAPON</div>\n                <div class=\"weapon-select-subtitle\" data-i18n=\"weaponSelect.subtitle\">Select your starting weapon</div>\n                <div class=\"weapon-select-grid\" id=\"weapon-select-grid\"></div>\n            </div>\n            <div class=\"weapon-select-right\">\n                <div class=\"orientation-toggle\" id=\"orientation-toggle\">\n                    <div class=\"orientation-btn active\" id=\"orient-portrait\" data-orient=\"portrait\">\n                        <svg width=\"28\" height=\"40\" viewBox=\"0 0 20 28\">\n                            <rect x=\"1\" y=\"1\" width=\"18\" height=\"26\" rx=\"3\" fill=\"none\" stroke=\"currentColor\"\n                                stroke-width=\"2\" />\n                            <circle cx=\"10\" cy=\"24\" r=\"1.5\" fill=\"currentColor\" />\n                        </svg>\n                        <span data-i18n=\"pause.portrait\">Portrait</span>\n                    </div>\n                    <div class=\"orientation-btn\" id=\"orient-landscape\" data-orient=\"landscape\">\n                        <svg width=\"40\" height=\"28\" viewBox=\"0 0 28 20\">\n                            <rect x=\"1\" y=\"1\" width=\"26\" height=\"18\" rx=\"3\" fill=\"none\" stroke=\"currentColor\"\n                                stroke-width=\"2\" />\n                            <circle cx=\"24\" cy=\"10\" r=\"1.5\" fill=\"currentColor\" />\n                        </svg>\n                        <span data-i18n=\"pause.landscape\">Landscape</span>\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"weapon-showcase-overlay\" id=\"weapon-showcase-overlay\">\n            <div class=\"showcase-label\" id=\"showcase-label\">NEW WEAPON</div>\n            <img class=\"showcase-weapon-icon\" id=\"showcase-weapon-icon\" src=\"\" alt=\"\">\n            <div class=\"showcase-weapon-name\" id=\"showcase-weapon-name\">SHOTGUN</div>\n            <div class=\"showcase-weapon-stats\" id=\"showcase-weapon-stats\"></div>\n        </div>\n\n        <div class=\"upgrade-select-screen\" id=\"upgrade-select-screen\">\n            <div class=\"upgrade-select-title\" id=\"upgrade-select-title\" data-i18n=\"upgrade.waveCompleteInitial\">WAVE\n                COMPLETE!</div>\n            <div class=\"upgrade-countdown\" id=\"upgrade-countdown\"><span class=\"upgrade-countdown-value\"\n                    id=\"upgrade-countdown-value\">5</span></div>\n            <div class=\"upgrade-select-subtitle\" data-i18n=\"upgrade.choose\">Choose an upgrade</div>\n            <div class=\"upgrade-select-options\" id=\"upgrade-select-options\"></div>\n        </div>\n\n        <div class=\"weapon-tutorial-overlay\" id=\"weapon-tutorial-overlay\"></div>\n\n        <div class=\"pause-overlay\" id=\"pause-overlay\">\n            <div class=\"pause-menu-items\" id=\"pause-main-menu\">\n                <div class=\"pause-menu-title\" id=\"pause-title\" data-i18n=\"pause.title\">PAUSED</div>\n                <div class=\"pause-menu-item\" id=\"pause-continue\" data-i18n=\"pause.continue\">CONTINUE</div>\n                <div class=\"pause-menu-item\" id=\"pause-restart-btn\" data-i18n=\"pause.restart\">RESTART</div>\n                <div class=\"pause-menu-item\" id=\"pause-settings\" data-i18n=\"pause.settings\">SETTINGS</div>\n                <div class=\"pause-menu-item\" id=\"pause-language\" data-i18n=\"pause.language\">LANGUAGE</div>\n            </div>\n            <div class=\"pause-sub-panel\" id=\"pause-settings-panel\">\n                <div class=\"pause-sub-title\" id=\"settings-title\" data-i18n=\"pause.settings\">SETTINGS</div>\n                <div class=\"volume-row\">\n                    <div class=\"volume-label\"><span id=\"vol-bgm-label\" data-i18n=\"pause.bgm\">Background Music</span><span\n                            id=\"vol-bgm-value\">100%</span></div>\n                    <input type=\"range\" class=\"volume-slider\" id=\"vol-bgm\" min=\"0\" max=\"100\" value=\"100\">\n                </div>\n                <div class=\"volume-row\">\n                    <div class=\"volume-label\"><span id=\"vol-weapon-label\" data-i18n=\"pause.weapon\">Weapon SFX</span><span\n                            id=\"vol-weapon-value\">100%</span></div>\n                    <input type=\"range\" class=\"volume-slider\" id=\"vol-weapon\" min=\"0\" max=\"100\" value=\"100\">\n                </div>\n                <div class=\"volume-row\">\n                    <div class=\"volume-label\"><span id=\"vol-monster-label\" data-i18n=\"pause.monster\">Monster SFX</span><span\n                            id=\"vol-monster-value\">100%</span></div>\n                    <input type=\"range\" class=\"volume-slider\" id=\"vol-monster\" min=\"0\" max=\"100\" value=\"100\">\n                </div>\n                <div class=\"volume-row\">\n                    <div class=\"volume-label\"><span id=\"vol-ui-label\" data-i18n=\"pause.ui\">UI SFX</span><span id=\"vol-ui-value\">100%</span>\n                    </div>\n                    <input type=\"range\" class=\"volume-slider\" id=\"vol-ui\" min=\"0\" max=\"100\" value=\"100\">\n                </div>\n                <div class=\"volume-row\">\n                    <div class=\"volume-label\"><span id=\"visual-intensity-label\" data-i18n=\"pause.visual\">Visual Intensity</span><span\n                            id=\"visual-intensity-value\">Normal</span></div>\n                    <div class=\"orientation-toggle pause-orientation-toggle visual-intensity-toggle\"\n                        id=\"visual-intensity-toggle\">\n                        <div class=\"orientation-btn active\" id=\"visual-normal\" data-visual=\"normal\"><span data-i18n=\"pause.normal\">Normal</span>\n                        </div>\n                        <div class=\"orientation-btn\" id=\"visual-soft\" data-visual=\"soft\"><span data-i18n=\"pause.soft\">Soft</span></div>\n                    </div>\n                </div>\n                <div class=\"orientation-toggle pause-orientation-toggle\" id=\"pause-orientation-toggle\">\n                    <div class=\"orientation-btn active\" id=\"pause-orient-portrait\" data-orient=\"portrait\">\n                        <svg width=\"28\" height=\"40\" viewBox=\"0 0 20 28\">\n                            <rect x=\"1\" y=\"1\" width=\"18\" height=\"26\" rx=\"3\" fill=\"none\" stroke=\"currentColor\"\n                                stroke-width=\"2\" />\n                            <circle cx=\"10\" cy=\"24\" r=\"1.5\" fill=\"currentColor\" />\n                        </svg>\n                        <span data-i18n=\"pause.portrait\">Portrait</span>\n                    </div>\n                    <div class=\"orientation-btn\" id=\"pause-orient-landscape\" data-orient=\"landscape\">\n                        <svg width=\"40\" height=\"28\" viewBox=\"0 0 28 20\">\n                            <rect x=\"1\" y=\"1\" width=\"26\" height=\"18\" rx=\"3\" fill=\"none\" stroke=\"currentColor\"\n                                stroke-width=\"2\" />\n                            <circle cx=\"24\" cy=\"10\" r=\"1.5\" fill=\"currentColor\" />\n                        </svg>\n                        <span data-i18n=\"pause.landscape\">Landscape</span>\n                    </div>\n                </div>\n                <button class=\"pause-back-btn\" id=\"settings-back\" data-i18n=\"pause.back\">\u2190 BACK</button>\n            </div>\n            <div class=\"pause-sub-panel\" id=\"pause-language-panel\">\n                <div class=\"pause-sub-title\" id=\"language-title\" data-i18n=\"pause.language\">LANGUAGE</div>\n                <div class=\"language-grid\" id=\"language-grid\"></div>\n                <button class=\"pause-back-btn\" id=\"language-back\" data-i18n=\"pause.back\">\u2190 BACK</button>\n            </div>\n        </div>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n    <script>\n        /*\n         * LAST STAND BUNKER\n         * Architecture: ECS (Entity Component System) with unidirectional data flow\n         * Performance: Web Worker for AI, Object Pool for entities\n         * Renderer: Three.js r128\n         */\n\n        // ===== WEB WORKER (Blob URL) =====\n        const workerCode = `\n        // Zombie AI Worker - handles pathfinding & AI decisions off main thread\n        const MAX_ZOMBIES = 220;\n\n        // Shared zombie state buffers\n        let zombiePositions = new Float32Array(MAX_ZOMBIES * 3);\n        let zombieVelocities = new Float32Array(MAX_ZOMBIES * 3);\n        let zombieStates = new Int32Array(MAX_ZOMBIES); // 0=inactive, 1=moving, 2=attacking\n        let zombieSpeeds = new Float32Array(MAX_ZOMBIES);\n        let activeCount = 0;\n\n        self.onmessage = function(e) {\n            const { type, data } = e.data;\n\n            switch(type) {\n                case 'update': {\n                    const { playerPos, playerX, playerZ, dt, zombies, ids, packed, generation } = data;\n                    const px = playerPos ? playerPos.x : playerX;\n                    const pz = playerPos ? playerPos.z : playerZ;\n                    activeCount = ids ? (data.count || ids.length) : zombies.length;\n\n                    const outIds = ids ? (data.outIds || new Int32Array(MAX_ZOMBIES)) : null;\n                    const out = ids ? (data.out || new Float32Array(MAX_ZOMBIES * 5)) : null;\n                    const results = ids ? null : [];\n                    for (let i = 0; i < activeCount; i++) {\n                        const id = ids ? ids[i] : zombies[i].id;\n                        const zx = ids ? packed[i * 3] : zombies[i].x;\n                        const zz = ids ? packed[i * 3 + 1] : zombies[i].z;\n                        const speed = ids ? packed[i * 3 + 2] : zombies[i].speed;\n                        const dx = px - zx;\n                        const dz = pz - zz;\n                        const dist = Math.sqrt(dx * dx + dz * dz);\n\n                        let state = 1; // moving\n                        let vx = 0, vz = 0;\n                        let angle = Math.atan2(dx, dz);\n\n                        if (dist > 1.5) {\n                            const ndx = dx / dist;\n                            const ndz = dz / dist;\n                            vx = ndx * speed * dt;\n                            vz = ndz * speed * dt;\n                        } else {\n                            state = 2; // attacking\n                        }\n\n                        if (ids) {\n                            outIds[i] = id;\n                            const j = i * 5;\n                            out[j] = vx;\n                            out[j + 1] = vz;\n                            out[j + 2] = state;\n                            out[j + 3] = angle;\n                            out[j + 4] = dist;\n                        } else {\n                            results.push({ id, vx, vz, state, angle, dist });\n                        }\n                    }\n\n                    if (ids) self.postMessage({ type: 'aiResultPacked', generation, count: activeCount, ids: outIds, data: out, inputIds: ids, inputPacked: packed }, [outIds.buffer, out.buffer, ids.buffer, packed.buffer]);\n                    else self.postMessage({ type: 'aiResult', generation, data: results });\n                    break;\n                }\n            }\n        };\n    `;\n\n        let aiWorker = null;\n        function createAIWorker() {\n            const blob = new Blob([workerCode], { type: 'application/javascript' });\n            const url = URL.createObjectURL(blob);\n            const worker = new Worker(url);\n            worker._blobUrl = url;\n            return worker;\n        }\n        function disposeAIWorker(worker) {\n            if (!worker) return;\n            try { worker.terminate(); } catch (e) { }\n            if (worker._blobUrl) {\n                try { URL.revokeObjectURL(worker._blobUrl); } catch (e) { }\n                worker._blobUrl = null;\n            }\n        }\n        try {\n            aiWorker = createAIWorker();\n        } catch (e) {\n            // Fallback: AI runs on main thread\n        }\n\n        // ===== ECS CORE =====\n        class World {\n            constructor() {\n                this.entities = new Map();\n                this.components = new Map(); // componentName -> Map<entityId, data>\n                this.systems = [];\n                this.nextId = 0;\n                this.events = []; // unidirectional event queue\n            }\n\n            createEntity() {\n                const id = this.nextId++;\n                this.entities.set(id, true);\n                return id;\n            }\n\n            removeEntity(id) {\n                this.entities.delete(id);\n                for (const store of this.components.values()) {\n                    store.delete(id);\n                }\n            }\n\n            addComponent(entityId, name, data) {\n                if (!this.components.has(name)) {\n                    this.components.set(name, new Map());\n                }\n                this.components.get(name).set(entityId, data);\n            }\n\n            getComponent(entityId, name) {\n                const store = this.components.get(name);\n                return store ? store.get(entityId) : undefined;\n            }\n\n            getStore(name) {\n                return this.components.get(name);\n            }\n\n            hasComponent(entityId, name) {\n                const store = this.components.get(name);\n                return store ? store.has(entityId) : false;\n            }\n\n            removeComponent(entityId, name) {\n                const store = this.components.get(name);\n                if (store) store.delete(entityId);\n            }\n\n            query(...componentNames) {\n                if (componentNames.length === 0) return Array.from(this.entities.keys());\n                let baseStore = null;\n                for (const name of componentNames) {\n                    const store = this.components.get(name);\n                    if (!store) return [];\n                    if (!baseStore || store.size < baseStore.size) baseStore = store;\n                }\n                const results = [];\n                for (const [id] of baseStore) {\n                    let hasAll = true;\n                    for (const name of componentNames) {\n                        if (!this.hasComponent(id, name)) { hasAll = false; break; }\n                    }\n                    if (hasAll) results.push(id);\n                }\n                return results;\n            }\n\n            emit(event) { this.events.push(event); }\n            flush() { this.events.length = 0; }\n\n            addSystem(system) { this.systems.push(system); }\n\n            update(dt, context) {\n                const systems = this.systems;\n                for (let i = 0; i < systems.length; i++) {\n                    systems[i](this, dt, context);\n                }\n                this.flush();\n            }\n        }\n\n        // ===== OBJECT POOL =====\n        class ObjectPool {\n            constructor(factory, reset, initialSize = 20, options = null) {\n                this.factory = factory;\n                this.reset = reset;\n                this.dispose = options && typeof options.dispose === 'function' ? options.dispose : null;\n                this.maxIdle = options && Number.isFinite(options.maxIdle) ? Math.max(0, options.maxIdle) : Infinity;\n                this.pool = [];\n                for (let i = 0; i < initialSize; i++) {\n                    this.pool.push(factory());\n                }\n            }\n\n            acquire() {\n                let obj = this.pool.pop();\n                if (!obj) obj = this.factory();\n                return obj;\n            }\n\n            warm(count = 1) {\n                for (let i = 0; i < count && this.pool.length < this.maxIdle; i++) this.pool.push(this.factory());\n            }\n\n            release(obj) {\n                this.reset(obj);\n                if (this.pool.length < this.maxIdle) this.pool.push(obj);\n                else this.disposeObject(obj);\n            }\n\n            disposeObject(obj) {\n                if (!this.dispose) return;\n                try { this.dispose(obj); } catch (e) { }\n            }\n\n            cull(maxIdle = this.maxIdle) {\n                const limit = Number.isFinite(maxIdle) ? Math.max(0, maxIdle) : this.pool.length;\n                while (this.pool.length > limit) this.disposeObject(this.pool.pop());\n            }\n\n            get size() { return this.pool.length; }\n        }\n\n        let threeFallbackLoadPromise = null;\n        function loadThreeFallback() {\n            if (threeFallbackLoadPromise) return threeFallbackLoadPromise;\n            threeFallbackLoadPromise = new Promise((resolve) => {\n                let settled = false;\n                const finish = () => {\n                    if (settled) return;\n                    settled = true;\n                    resolve();\n                };\n                const script = document.createElement('script');\n                script.src = 'https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js';\n                script.onload = finish;\n                script.onerror = finish;\n                document.head.appendChild(script);\n                setTimeout(finish, 4000);\n            });\n            return threeFallbackLoadPromise;\n        }\n\n        async function run(mode) {\n            let threeWaitMs = 0;\n            while (typeof THREE === 'undefined') {\n                if (threeWaitMs === 0) await loadThreeFallback();\n                await new Promise(resolve => setTimeout(resolve, 50));\n                threeWaitMs += 50;\n            }\n            // Force vertex lighting (MeshLambertMaterial) globally instead of pixel lighting (MeshPhongMaterial)\n            // to improve performance and avoid specular artifacts on low-poly assets.\n            const PerformanceLambertMaterial = THREE.MeshLambertMaterial;\n            function PerformancePhongMaterial(parameters) {\n                const lambertParameters = parameters ? { ...parameters } : parameters;\n                if (lambertParameters) {\n                    delete lambertParameters.specular;\n                    delete lambertParameters.shininess;\n                    delete lambertParameters.flatShading;\n                }\n                return new PerformanceLambertMaterial(lambertParameters);\n            }\n            PerformancePhongMaterial.prototype = PerformanceLambertMaterial.prototype;\n            Object.setPrototypeOf(PerformancePhongMaterial, PerformanceLambertMaterial);\n            THREE.MeshPhongMaterial = PerformancePhongMaterial;\n\n            const config = window.gameConfig;\n            const gameWorld = document.getElementById('game-world');\n            const canvas = document.getElementById('game-canvas');\n            const TEXT = {\n                app: {\n                    title: 'Zombie Last Survivor',\n                    titleUpper: 'ZOMBIE LAST SURVIVOR',\n                    titleBreak: 'ZOMBIE<br>LAST SURVIVOR'\n                },\n                tutorial: {\n                    title: '\u3010TUTORIAL\u3011NEW WEAPON',\n                    desc: 'You have acquired a new weapon! Tap the highlighted icon below or press [2] to switch to it and continue.'\n                },\n                loading: {\n                    assets: 'Loading assets...',\n                    assetsProgress: 'Loading assets... {pct}%'\n                },\n                start: {\n                    subtitle: 'Defend the bunker against endless zombie hordes',\n                    play: 'PLAY'\n                },\n                hud: {\n                    wave: 'WAVE {wave}',\n                    waveBoss: 'WAVE {wave} - BOSS: {boss}',\n                    waveInitial: 'WAVE 1',\n                    score: 'SCORE',\n                    interact: 'Press E to interact',\n                    fire: 'FIRE',\n                    reloadButton: 'R',\n                    scopeZoom: 'x4',\n                    reloadSuffix: '{weapon} RELOAD',\n                    seconds: '{value}s'\n                },\n                gameOver: {\n                    title: 'BUNKER BREACHED',\n                    retry: 'RETRY'\n                },\n                stats: {\n                    wave: 'Wave',\n                    kills: 'Kills',\n                    headshots: 'Headshots',\n                    score: 'Score',\n                    accuracy: 'Accuracy'\n                },\n                weaponSelect: {\n                    title: 'CHOOSE WEAPON',\n                    subtitle: 'Select your starting weapon'\n                },\n                upgrade: {\n                    waveCompleteInitial: 'WAVE COMPLETE!',\n                    waveComplete: 'WAVE {wave} COMPLETE!',\n                    choose: 'Choose an upgrade',\n                    lifestealName: 'LIFESTEAL+',\n                    lifestealDesc: 'Heal on hit {from}% -> {to}%',\n                    damageName: '{weapon} DMG+',\n                    damageDesc: 'Damage {from} -> {to}',\n                    reloadName: '{weapon} RELOAD+',\n                    reloadDesc: 'Reload {from}s -> {to}s',\n                    magazineName: '{weapon} MAG+',\n                    magazineDesc: 'Magazine {from} -> {to}',\n                    pierceName: '{weapon} PIERCE+',\n                    pierceDesc: 'Penetration {from} -> {to}',\n                    fireRateName: '{weapon} FIRERATE+',\n                    fireRateDesc: 'Interval {from}ms -> {to}ms',\n                    speedName: '{weapon} SPEED+',\n                    rangeName: '{weapon} RANGE+',\n                    rangeDesc: 'Range {from}m -> {to}m',\n                    newWeaponName: 'NEW WEAPON: {weapon}',\n                    slowTimeName: '\u3010SKILL\u3011TIME DILATION',\n                    slowTimeDesc: 'World slows down by 100x, player unaffected. Lasts 5s.',\n                    flameRangeName: '[Flamethrower] Fuel Compression',\n                    flameRangeDesc: 'Increase range by +40% (speed +15%) and direct damage by +20% (Level {level})',\n                    flameStickyName: '[Flamethrower] Sticky Fire',\n                    flameStickyDesc: 'Increase burning duration by +2s and tick damage by +50% (Level {level})',\n                    flameSpreadName: '[Flamethrower] Wide Nozzle',\n                    flameSpreadDesc: 'Increase flame size and collision area by +35% (Level {level})',\n                    flameEmberName: '[Flamethrower] Ember Resurgence',\n                    flameEmberDesc: 'Burning enemies have a 30% chance to ignite adjacent enemies',\n                    flameLavaName: '[Flamethrower] High-Temp Lava',\n                    flameLavaDesc: 'Flames ignore enemy armor entirely and melt 15% max armor per second'\n                },\n                weaponStats: {\n                    melee: 'MELEE',\n                    auto: 'AUTO',\n                    semi: 'SEMI',\n                    damage: 'DMG:{value}',\n                    range: 'Range:{value}m',\n                    magazine: 'Mag:{value}'\n                },\n                edit: {\n                    title: 'EDIT MODE',\n                    line1: 'Use the settings panel to configure game parameters.',\n                    line2: 'Adjust zombie difficulty, starting wave, weapon settings, and lighting.',\n                    architecture: 'Architecture: ECS + Web Worker + Object Pool'\n                },\n                params: {\n                    name: 'Last Survivor',\n                    zombieHealth: 'Zombie Health',\n                    zombieSpeed: 'Zombie Speed',\n                    startingWave: 'Starting Wave',\n                    headshotMultiplier: 'Headshot Multiplier',\n                    katanaSpeed: 'Katana Speed',\n                    lighting: 'Lighting',\n                    coolWhite: 'Cool White',\n                    warm: 'Warm',\n                    emergencyRed: 'Emergency Red',\n                    nightVision: 'Night Vision'\n                },\n                weapons: {\n                    PISTOL: 'SMART PISTOL',\n                    SHOTGUN: 'SHOTGUN',\n                    SNIPER: 'SNIPER',\n                    KATANA: 'KATANA',\n                    GATLING: 'GATLING',\n                    LMG: 'LMG',\n                    'AUTO RIFLE': 'AUTO RIFLE',\n                    FLAMETHROWER: 'FLAMETHROWER'\n                },\n                zombies: {\n                    Walker: 'Walker',\n                    Runner: 'Runner',\n                    Tank: 'Tank',\n                    Crawler: 'Crawler',\n                    Berserker: 'Berserker',\n                    Armored: 'Armored',\n                    Swarm: 'Swarm',\n                    Bloater: 'Bloater',\n                    Stalker: 'Stalker',\n                    Screamer: 'Screamer',\n                    BRUTE: 'BRUTE',\n                    CHARGER: 'CHARGER',\n                    BEHEMOTH: 'BEHEMOTH',\n                    INFERNAL: 'INFERNAL',\n                    OVERLORD: 'OVERLORD'\n                }\n            };\n            const TEXTS = {\n                en: TEXT,\n                zh: {\n                    app: { title: '\u50f5\u5c38:\u6700\u540e\u7684\u5e78\u5b58\u8005', titleUpper: '\u50f5\u5c38:\u6700\u540e\u7684\u5e78\u5b58\u8005', titleBreak: '\u50f5\u5c38<br>\u6700\u540e\u7684\u5e78\u5b58\u8005' },\n                    tutorial: { title: '\u3010\u65b0\u624b\u6559\u5b66\u3011\u89e3\u9501\u65b0\u6b66\u5668', desc: '\u4f60\u83b7\u5f97\u4e86\u7b2c\u4e00\u4ef6\u65b0\u6b66\u5668\uff01\u70b9\u51fb\u4e0b\u65b9\u9ad8\u4eae\u7684\u6b66\u5668\u56fe\u6807\uff0c\u6216\u6309\u952e\u76d8\u4e0a\u7684\u6570\u5b57\u952e [2] \u5373\u53ef\u5207\u6362\u5e76\u7ee7\u7eed\u6e38\u620f\uff01' },\n                    loading: { assets: '\u6b63\u5728\u52a0\u8f7d\u8d44\u6e90...', assetsProgress: '\u6b63\u5728\u52a0\u8f7d\u8d44\u6e90... {pct}%' },\n                    start: { subtitle: '\u4fdd\u536b\u63a9\u4f53\uff0c\u62b5\u5fa1\u65e0\u5c3d\u50f5\u5c38\u6f6e', play: '\u5f00\u59cb\u6e38\u620f' },\n                    hud: { wave: '\u7b2c {wave} \u6ce2', waveBoss: '\u7b2c {wave} \u6ce2 - BOSS: {boss}', waveInitial: '\u7b2c 1 \u6ce2', score: '\u5206\u6570', interact: '\u6309 E \u4e92\u52a8', fire: '\u5c04\u51fb', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} \u6362\u5f39\u4e2d', seconds: '{value}\u79d2' },\n                    gameOver: { title: '\u63a9\u4f53\u5931\u5b88', retry: '\u518d\u6765\u4e00\u5c40' },\n                    stats: { wave: '\u6ce2\u6570', kills: '\u51fb\u6740', headshots: '\u7206\u5934', score: '\u5f97\u5206', accuracy: '\u547d\u4e2d\u7387' },\n                    weaponSelect: { title: '\u9009\u62e9\u6b66\u5668', subtitle: '\u9009\u62e9\u4f60\u7684\u521d\u59cb\u6b66\u5668' },\n                    upgrade: {\n                        waveCompleteInitial: '\u672c\u6ce2\u5b8c\u6210!',\n                        waveComplete: '\u7b2c {wave} \u6ce2\u5b8c\u6210!',\n                        choose: '\u9009\u62e9\u4e00\u4e2a\u5347\u7ea7',\n                        lifestealName: '\u751f\u547d\u6c72\u53d6+',\n                        lifestealDesc: '\u547d\u4e2d\u56de\u8840 {from}% -> {to}%',\n                        damageName: '{weapon} \u4f24\u5bb3+',\n                        damageDesc: '\u4f24\u5bb3 {from} -> {to}',\n                        reloadName: '{weapon} \u6362\u5f39+',\n                        reloadDesc: '\u6362\u5f39 {from}\u79d2 -> {to}\u79d2',\n                        magazineName: '{weapon} \u5f39\u5323+',\n                        magazineDesc: '\u5f39\u5323 {from} -> {to}',\n                        pierceName: '{weapon} \u7a7f\u900f+',\n                        pierceDesc: '\u7a7f\u900f {from} -> {to}',\n                        fireRateName: '{weapon} \u5c04\u901f+',\n                        fireRateDesc: '\u95f4\u9694 {from}ms -> {to}ms',\n                        speedName: '{weapon} \u901f\u5ea6+',\n                        rangeName: '{weapon} \u5c04\u7a0b+',\n                        rangeDesc: '\u5c04\u7a0b {from}m -> {to}m',\n                        newWeaponName: '\u65b0\u6b66\u5668: {weapon}',\n                        slowTimeName: '\u3010\u6280\u80fd\u3011\u65f6\u95f4\u9759\u6b62',\n                        slowTimeDesc: '\u4e16\u754c\u65f6\u95f4\u653e\u6162100\u500d\uff0c\u73a9\u5bb6\u4e0d\u53d7\u5f71\u54cd\uff0c\u6301\u7eed5\u79d2',\n                        flameRangeName: '\u3010\u706b\u7130\u55b7\u5c04\u5668\u3011\u71c3\u6599\u538b\u7f29',\n                        flameRangeDesc: '\u5c04\u7a0b\u63d0\u534740%\uff08\u55b7\u5c04\u901f\u5ea6+15%\uff09\uff0c\u4e14\u76f4\u63a5\u4f24\u5bb3\u63d0\u534720%\uff08\u5f53\u524d\u7b49\u7ea7 {level}\uff09',\n                        flameStickyName: '\u3010\u706b\u7130\u55b7\u5c04\u5668\u3011\u7c98\u6027\u70c8\u7130',\n                        flameStickyDesc: '\u71c3\u70e7\u6301\u7eed\u65f6\u95f4\u589e\u52a02\u79d2\uff0c\u6bcf\u8df3\u4f24\u5bb3\u589e\u52a050%\uff08\u5f53\u524d\u7b49\u7ea7 {level}\uff09',\n                        flameSpreadName: '\u3010\u706b\u7130\u55b7\u5c04\u5668\u3011\u5bbd\u4f53\u55b7\u5934',\n                        flameSpreadDesc: '\u706b\u7130\u5c3a\u5bf8\u4e0e\u78b0\u649e\u5224\u5b9a\u8303\u56f4\u589e\u52a035%\uff08\u5f53\u524d\u7b49\u7ea7 {level}\uff09',\n                        flameEmberName: '\u3010\u706b\u7130\u55b7\u5c04\u5668\u3011\u4f59\u70ec\u590d\u71c3',\n                        flameEmberDesc: '\u71c3\u70e7\u72b6\u6001\u4e0b\u7684\u654c\u4eba\u5728\u53d7\u5230\u4f24\u5bb3\u65f6\uff0c\u670930%\u6982\u7387\u5f15\u71c3\u5468\u56f4\u7684\u654c\u4eba',\n                        flameLavaName: '\u3010\u706b\u7130\u55b7\u5c04\u5668\u3011\u9ad8\u6e29\u7194\u5ca9',\n                        flameLavaDesc: '\u706b\u7130\u5b8c\u5168\u65e0\u89c6\u654c\u4eba\u62a4\u7532\uff0c\u4e14\u6bcf\u79d2\u7194\u6bc1\u654c\u4eba15%\u7684\u6700\u5927\u62a4\u7532'\n                    },\n                    weaponStats: { melee: '\u8fd1\u6218', auto: '\u5168\u81ea\u52a8', semi: '\u534a\u81ea\u52a8', damage: '\u4f24\u5bb3:{value}', range: '\u5c04\u7a0b:{value}m', magazine: '\u5f39\u5323:{value}' },\n                    edit: { title: '\u7f16\u8f91\u6a21\u5f0f', line1: '\u4f7f\u7528\u8bbe\u7f6e\u9762\u677f\u914d\u7f6e\u6e38\u620f\u53c2\u6570\u3002', line2: '\u8c03\u6574\u50f5\u5c38\u96be\u5ea6\u3001\u8d77\u59cb\u6ce2\u6570\u3001\u6b66\u5668\u548c\u706f\u5149\u3002', architecture: '\u67b6\u6784: ECS + Web Worker + Object Pool' },\n                    params: { name: '\u6700\u540e\u7684\u5e78\u5b58\u8005', zombieHealth: '\u50f5\u5c38\u751f\u547d', zombieSpeed: '\u50f5\u5c38\u901f\u5ea6', startingWave: '\u8d77\u59cb\u6ce2\u6570', headshotMultiplier: '\u7206\u5934\u500d\u7387', katanaSpeed: '\u5200\u901f', lighting: '\u706f\u5149', coolWhite: '\u51b7\u767d', warm: '\u6696\u8272', emergencyRed: '\u7d27\u6025\u7ea2', nightVision: '\u591c\u89c6' },\n                    weapons: { PISTOL: '\u667a\u80fd\u624b\u67aa', SHOTGUN: '\u9730\u5f39\u67aa', SNIPER: '\u72d9\u51fb\u67aa', KATANA: '\u6b66\u58eb\u5200', GATLING: '\u52a0\u7279\u6797', LMG: '\u8f7b\u673a\u67aa', 'AUTO RIFLE': '\u7a81\u51fb\u6b65\u67aa', FLAMETHROWER: '\u706b\u7130\u55b7\u5c04\u5668' },\n                    zombies: { Walker: '\u884c\u8005', Runner: '\u5954\u8dd1\u8005', Tank: '\u5766\u514b', Crawler: '\u722c\u884c\u8005', Berserker: '\u72c2\u66b4\u8005', Armored: '\u91cd\u7532', Swarm: '\u8702\u7fa4', Bloater: '\u81a8\u80c0\u8005', Stalker: '\u6f5c\u4f0f\u8005', Screamer: '\u5c16\u53eb\u8005', BRUTE: '\u86ee\u517d', CHARGER: '\u51b2\u950b\u8005', BEHEMOTH: '\u5de8\u517d', INFERNAL: '\u70bc\u72f1', OVERLORD: '\u9738\u4e3b' }\n                },\n                id: {\n                    app: { title: 'Penyintas Terakhir', titleUpper: 'PENYINTAS TERAKHIR', titleBreak: 'PENYINTAS<br>TERAKHIR' },\n                    tutorial: { title: '\u3010PANDUAN\u3011SENJATA BARU', desc: 'Kamu mendapatkan senjata baru! Ketuk ikon yang disorot di bawah atau tekan [2] untuk beralih dan melanjutkan.' },\n                    loading: { assets: 'Memuat aset...', assetsProgress: 'Memuat aset... {pct}%' },\n                    start: { subtitle: 'Pertahankan bunker dari gerombolan zombi tanpa akhir', play: 'MAIN' },\n                    hud: { wave: 'GELOMBANG {wave}', waveBoss: 'GELOMBANG {wave} - BOS: {boss}', waveInitial: 'GELOMBANG 1', score: 'SKOR', interact: 'Tekan E untuk berinteraksi', fire: 'TEMBAK', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} ISI ULANG', seconds: '{value} dtk' },\n                    gameOver: { title: 'BUNKER JATUH', retry: 'COBA LAGI' },\n                    stats: { wave: 'Gelombang', kills: 'Tewas', headshots: 'Tembakan kepala', score: 'Skor', accuracy: 'Akurasi' },\n                    weaponSelect: { title: 'PILIH SENJATA', subtitle: 'Pilih senjata awalmu' },\n                    upgrade: { waveCompleteInitial: 'GELOMBANG SELESAI!', waveComplete: 'GELOMBANG {wave} SELESAI!', choose: 'Pilih peningkatan', lifestealName: 'SERAP NYAWA+', lifestealDesc: 'Pulih saat mengenai {from}% -> {to}%', damageName: '{weapon} DMG+', damageDesc: 'Kerusakan {from} -> {to}', reloadName: '{weapon} ISI ULANG+', reloadDesc: 'Isi ulang {from}d -> {to}d', magazineName: '{weapon} MAG+', magazineDesc: 'Magasin {from} -> {to}', pierceName: '{weapon} TEMBUS+', pierceDesc: 'Tembusan {from} -> {to}', fireRateName: '{weapon} LAJU TEMBAK+', fireRateDesc: 'Jeda {from}md -> {to}md', speedName: '{weapon} CEPAT+', rangeName: '{weapon} JARAK+', rangeDesc: 'Jarak {from}m -> {to}m', newWeaponName: 'SENJATA BARU: {weapon}', slowTimeName: '\u3010SKILL\u3011DILASI WAKTU', slowTimeDesc: 'Dunia melambat 100x, pemain tidak terpengaruh. Durasi 5 detik.', flameRangeName: '[Pelontar Api] Kompresi Bahan Bakar', flameRangeDesc: 'Meningkatkan jangkauan sebesar +40% (kecepatan +15%) dan kerusakan langsung sebesar +20% (Level {level})', flameStickyName: '[Pelontar Api] Api Lengket', flameStickyDesc: 'Meningkatkan durasi terbakar sebesar +2 detik dan kerusakan per detik sebesar +50% (Level {level})', flameSpreadName: '[Pelontar Api] Nozel Lebar', flameSpreadDesc: 'Meningkatkan ukuran api dan area tabrakan sebesar +35% (Level {level})', flameEmberName: '[Pelontar Api] Kebangkitan Bara', flameEmberDesc: 'Musuh yang terbakar memiliki peluang 30% untuk membakar musuh di sekitarnya', flameLavaName: '[Pelontar Api] Lava Suhu Tinggi', flameLavaDesc: 'Api mengabaikan pelindung musuh sepenuhnya dan melelehkan 15% pelindung maksimum per detik', flameRangeName: '[Pelontar Api] Kompresi Bahan Bakar', flameRangeDesc: 'Meningkatkan jangkauan sebesar +40% (kecepatan +15%) dan kerusakan langsung sebesar +20% (Level {level})', flameStickyName: '[Pelontar Api] Api Lengket', flameStickyDesc: 'Meningkatkan durasi terbakar sebesar +2 detik dan kerusakan per detik sebesar +50% (Level {level})', flameSpreadName: '[Pelontar Api] Nozel Lebar', flameSpreadDesc: 'Meningkatkan ukuran api dan area tabrakan sebesar +35% (Level {level})', flameEmberName: '[Pelontar Api] Kebangkitan Bara', flameEmberDesc: 'Musuh yang terbakar memiliki peluang 30% untuk membakar musuh di sekitarnya', flameLavaName: '[Pelontar Api] Lava Suhu Tinggi', flameLavaDesc: 'Api mengabaikan pelindung musuh sepenuhnya dan melelehkan 15% pelindung maksimum per detik' },\n                    weaponStats: { melee: 'JARAK DEKAT', auto: 'OTOMATIS', semi: 'SEMI', damage: 'DMG:{value}', range: 'Jarak:{value}m', magazine: 'Mag:{value}' },\n                    edit: { title: 'MODE EDIT', line1: 'Gunakan panel pengaturan untuk mengubah parameter game.', line2: 'Atur kesulitan zombi, gelombang awal, senjata, dan pencahayaan.', architecture: 'Arsitektur: ECS + Web Worker + Object Pool' },\n                    params: { name: 'Penyintas Terakhir', zombieHealth: 'Nyawa Zombi', zombieSpeed: 'Kecepatan Zombi', startingWave: 'Gelombang Awal', headshotMultiplier: 'Pengali Headshot', katanaSpeed: 'Kecepatan Katana', lighting: 'Pencahayaan', coolWhite: 'Putih Dingin', warm: 'Hangat', emergencyRed: 'Merah Darurat', nightVision: 'Penglihatan Malam' },\n                    weapons: { PISTOL: 'PISTOL PINTAR', SHOTGUN: 'SHOTGUN', SNIPER: 'SNIPER', KATANA: 'KATANA', GATLING: 'GATLING', LMG: 'LMG', 'AUTO RIFLE': 'SENAPAN OTOMATIS', FLAMETHROWER: 'PELONTAR API' },\n                    zombies: { Walker: 'Pejalan', Runner: 'Pelari', Tank: 'Tank', Crawler: 'Perayap', Berserker: 'Pengamuk', Armored: 'Berzirah', Swarm: 'Kawanan', Bloater: 'Pembengkak', Stalker: 'Pengintai', Screamer: 'Penjerit', BRUTE: 'RAKSASA', CHARGER: 'PENYERBU', BEHEMOTH: 'BEHEMOTH', INFERNAL: 'NERAKA', OVERLORD: 'PENGUASA' }\n                },\n                ru: {\n                    app: { title: '\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u0432\u044b\u0436\u0438\u0432\u0448\u0438\u0439', titleUpper: '\u041f\u041e\u0421\u041b\u0415\u0414\u041d\u0418\u0419 \u0412\u042b\u0416\u0418\u0412\u0428\u0418\u0419', titleBreak: '\u041f\u041e\u0421\u041b\u0415\u0414\u041d\u0418\u0419<br>\u0412\u042b\u0416\u0418\u0412\u0428\u0418\u0419' },\n                    tutorial: { title: '\u3010\u041e\u0411\u0423\u0427\u0415\u041d\u0418\u0415\u3011\u041d\u041e\u0412\u041e\u0415 \u041e\u0420\u0423\u0416\u0418\u0415', desc: '\u0412\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 \u043d\u043e\u0432\u043e\u0435 \u043e\u0440\u0443\u0436\u0438\u0435! \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0435\u043d\u043d\u0443\u044e \u0438\u043a\u043e\u043d\u043a\u0443 \u0432\u043d\u0438\u0437\u0443 \u0438\u043b\u0438 \u043a\u043b\u0430\u0432\u0438\u0448\u0443 [2], \u0447\u0442\u043e\u0431\u044b \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0438 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c.' },\n                    loading: { assets: '\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432...', assetsProgress: '\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432... {pct}%' },\n                    start: { subtitle: '\u041e\u0431\u043e\u0440\u043e\u043d\u044f\u0439\u0442\u0435 \u0431\u0443\u043d\u043a\u0435\u0440 \u043e\u0442 \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u044b\u0445 \u043e\u0440\u0434 \u0437\u043e\u043c\u0431\u0438', play: '\u0418\u0413\u0420\u0410\u0422\u042c' },\n                    hud: { wave: '\u0412\u041e\u041b\u041d\u0410 {wave}', waveBoss: '\u0412\u041e\u041b\u041d\u0410 {wave} - \u0411\u041e\u0421\u0421: {boss}', waveInitial: '\u0412\u041e\u041b\u041d\u0410 1', score: '\u0421\u0427\u0401\u0422', interact: '\u041d\u0430\u0436\u043c\u0438\u0442\u0435 E \u0434\u043b\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f', fire: '\u041e\u0413\u041e\u041d\u042c', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon}: \u041f\u0415\u0420\u0415\u0417\u0410\u0420\u042f\u0414\u041a\u0410', seconds: '{value} \u0441' },\n                    gameOver: { title: '\u0411\u0423\u041d\u041a\u0415\u0420 \u041f\u0420\u041e\u0420\u0412\u0410\u041d', retry: '\u0415\u0429\u0401 \u0420\u0410\u0417' },\n                    stats: { wave: '\u0412\u043e\u043b\u043d\u0430', kills: '\u0423\u0431\u0438\u0439\u0441\u0442\u0432\u0430', headshots: '\u0412 \u0433\u043e\u043b\u043e\u0432\u0443', score: '\u0421\u0447\u0451\u0442', accuracy: '\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c' },\n                    weaponSelect: { title: '\u0412\u042b\u0411\u0415\u0420\u0418\u0422\u0415 \u041e\u0420\u0423\u0416\u0418\u0415', subtitle: '\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0440\u0442\u043e\u0432\u043e\u0435 \u043e\u0440\u0443\u0436\u0438\u0435' },\n                    upgrade: { waveCompleteInitial: '\u0412\u041e\u041b\u041d\u0410 \u041f\u0420\u041e\u0419\u0414\u0415\u041d\u0410!', waveComplete: '\u0412\u041e\u041b\u041d\u0410 {wave} \u041f\u0420\u041e\u0419\u0414\u0415\u041d\u0410!', choose: '\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u0435', lifestealName: '\u0412\u0410\u041c\u041f\u0418\u0420\u0418\u0417\u041c+', lifestealDesc: '\u041b\u0435\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u0438 \u043f\u043e\u043f\u0430\u0434\u0430\u043d\u0438\u0438 {from}% -> {to}%', damageName: '{weapon} \u0423\u0420\u041e\u041d+', damageDesc: '\u0423\u0440\u043e\u043d {from} -> {to}', reloadName: '{weapon} \u041f\u0415\u0420\u0415\u0417\u0410\u0420\u042f\u0414\u041a\u0410+', reloadDesc: '\u041f\u0435\u0440\u0435\u0437\u0430\u0440\u044f\u0434\u043a\u0430 {from}\u0441 -> {to}\u0441', magazineName: '{weapon} \u041c\u0410\u0413\u0410\u0417\u0418\u041d+', magazineDesc: '\u041c\u0430\u0433\u0430\u0437\u0438\u043d {from} -> {to}', pierceName: '{weapon} \u041f\u0420\u041e\u0411\u0418\u0422\u0418\u0415+', pierceDesc: '\u041f\u0440\u043e\u0431\u0438\u0442\u0438\u0435 {from} -> {to}', fireRateName: '{weapon} \u0421\u041a\u041e\u0420\u041e\u0421\u0422\u0420\u0415\u041b\u042c\u041d\u041e\u0421\u0422\u042c+', fireRateDesc: '\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b {from}\u043c\u0441 -> {to}\u043c\u0441', speedName: '{weapon} \u0421\u041a\u041e\u0420\u041e\u0421\u0422\u042c+', rangeName: '{weapon} \u0414\u0410\u041b\u042c\u041d\u041e\u0421\u0422\u042c+', rangeDesc: '\u0414\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c {from}\u043c -> {to}\u043c', newWeaponName: '\u041d\u041e\u0412\u041e\u0415 \u041e\u0420\u0423\u0416\u0418\u0415: {weapon}', slowTimeName: '\u3010\u041d\u0410\u0412\u042b\u041a\u3011\u0417\u0410\u041c\u0415\u0414\u041b\u0415\u041d\u0418\u0415 \u0412\u0420\u0415\u041c\u0415\u041d\u0418', slowTimeDesc: '\u041c\u0438\u0440 \u0437\u0430\u043c\u0435\u0434\u043b\u044f\u0435\u0442\u0441\u044f \u0432 100 \u0440\u0430\u0437, \u0438\u0433\u0440\u043e\u043a \u043d\u0435 \u0437\u0430\u0442\u0440\u043e\u043d\u0443\u0442. \u0414\u043b\u0438\u0442\u0441\u044f 5 \u0441\u0435\u043a.', flameRangeName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u0421\u0436\u0430\u0442\u0438\u0435 \u0442\u043e\u043f\u043b\u0438\u0432\u0430', flameRangeDesc: '\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0434\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043d\u0430 +40% (\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c +15%) \u0438 \u043f\u0440\u044f\u043c\u043e\u0439 \u0443\u0440\u043e\u043d \u043d\u0430 +20% (\u0423\u0440\u043e\u0432\u0435\u043d\u044c {level})', flameStickyName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u041b\u0438\u043f\u043a\u0438\u0439 \u043e\u0433\u043e\u043d\u044c', flameStickyDesc: '\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0433\u043e\u0440\u0435\u043d\u0438\u044f \u043d\u0430 +2 \u0441\u0435\u043a \u0438 \u043f\u0435\u0440\u0438\u043e\u0434\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0443\u0440\u043e\u043d \u043d\u0430 +50% (\u0423\u0440\u043e\u0432\u0435\u043d\u044c {level})', flameSpreadName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u0428\u0438\u0440\u043e\u043a\u043e\u0435 \u0441\u043e\u043f\u043b\u043e', flameSpreadDesc: '\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0440\u0430\u0437\u043c\u0435\u0440 \u043f\u043b\u0430\u043c\u0435\u043d\u0438 \u0438 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0441\u0442\u043e\u043b\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u044f \u043d\u0430 +35% (\u0423\u0440\u043e\u0432\u0435\u043d\u044c {level})', flameEmberName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u0412\u043e\u0437\u0440\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u0443\u0433\u043b\u0435\u0439', flameEmberDesc: '\u0413\u043e\u0440\u044f\u0449\u0438\u0435 \u0432\u0440\u0430\u0433\u0438 \u0441 \u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e\u0441\u0442\u044c\u044e 30% \u043f\u043e\u0434\u0436\u0438\u0433\u0430\u044e\u0442 \u0441\u043e\u0441\u0435\u0434\u043d\u0438\u0445 \u0432\u0440\u0430\u0433\u043e\u0432', flameLavaName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u0412\u044b\u0441\u043e\u043a\u043e\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u043d\u0430\u044f \u043b\u0430\u0432\u0430', flameLavaDesc: '\u041f\u043b\u0430\u043c\u044f \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0440\u043e\u043d\u044e \u0432\u0440\u0430\u0433\u043e\u0432 \u0438 \u0440\u0430\u0441\u043f\u043b\u0430\u0432\u043b\u044f\u0435\u0442 15% \u043c\u0430\u043a\u0441. \u0431\u0440\u043e\u043d\u0438 \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443', flameRangeName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u0421\u0436\u0430\u0442\u0438\u0435 \u0442\u043e\u043f\u043b\u0438\u0432\u0430', flameRangeDesc: '\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0434\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043d\u0430 +40% (\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c +15%) \u0438 \u043f\u0440\u044f\u043c\u043e\u0439 \u0443\u0440\u043e\u043d \u043d\u0430 +20% (\u0423\u0440\u043e\u0432\u0435\u043d\u044c {level})', flameStickyName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u041b\u0438\u043f\u043a\u0438\u0439 \u043e\u0433\u043e\u043d\u044c', flameStickyDesc: '\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0433\u043e\u0440\u0435\u043d\u0438\u044f \u043d\u0430 +2 \u0441\u0435\u043a \u0438 \u043f\u0435\u0440\u0438\u043e\u0434\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0443\u0440\u043e\u043d \u043d\u0430 +50% (\u0423\u0440\u043e\u0432\u0435\u043d\u044c {level})', flameSpreadName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u0428\u0438\u0440\u043e\u043a\u043e\u0435 \u0441\u043e\u043f\u043b\u043e', flameSpreadDesc: '\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0440\u0430\u0437\u043c\u0435\u0440 \u043f\u043b\u0430\u043c\u0435\u043d\u0438 \u0438 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0441\u0442\u043e\u043b\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u044f \u043d\u0430 +35% (\u0423\u0440\u043e\u0432\u0435\u043d\u044c {level})', flameEmberName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u0412\u043e\u0437\u0440\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u0443\u0433\u043b\u0435\u0439', flameEmberDesc: '\u0413\u043e\u0440\u044f\u0449\u0438\u0435 \u0432\u0440\u0430\u0433\u0438 \u0441 \u0432\u0435\u0440\u043e\u044f\u0442\u043d\u043e\u0441\u0442\u044c\u044e 30% \u043f\u043e\u0434\u0436\u0438\u0433\u0430\u044e\u0442 \u0441\u043e\u0441\u0435\u0434\u043d\u0438\u0445 \u0432\u0440\u0430\u0433\u043e\u0432', flameLavaName: '[\u041e\u0433\u043d\u0435\u043c\u0435\u0442] \u0412\u044b\u0441\u043e\u043a\u043e\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u043d\u0430\u044f \u043b\u0430\u0432\u0430', flameLavaDesc: '\u041f\u043b\u0430\u043c\u044f \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0440\u043e\u043d\u044e \u0432\u0440\u0430\u0433\u043e\u0432 \u0438 \u0440\u0430\u0441\u043f\u043b\u0430\u0432\u043b\u044f\u0435\u0442 15% \u043c\u0430\u043a\u0441. \u0431\u0440\u043e\u043d\u0438 \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443' },\n                    weaponStats: { melee: '\u0411\u041b\u0418\u0416\u041d\u0418\u0419 \u0411\u041e\u0419', auto: '\u0410\u0412\u0422\u041e', semi: '\u041e\u0414\u0418\u041d\u041e\u0427\u041d\u042b\u0419', damage: '\u0423\u0440\u043e\u043d:{value}', range: '\u0414\u0430\u043b\u044c\u043d.:{value}\u043c', magazine: '\u041c\u0430\u0433.:{value}' },\n                    edit: { title: '\u0420\u0415\u0416\u0418\u041c \u0420\u0415\u0414\u0410\u041a\u0422\u041e\u0420\u0410', line1: '\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0430\u043d\u0435\u043b\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a, \u0447\u0442\u043e\u0431\u044b \u043c\u0435\u043d\u044f\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0438\u0433\u0440\u044b.', line2: '\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u043b\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0437\u043e\u043c\u0431\u0438, \u0441\u0442\u0430\u0440\u0442\u043e\u0432\u0443\u044e \u0432\u043e\u043b\u043d\u0443, \u043e\u0440\u0443\u0436\u0438\u0435 \u0438 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435.', architecture: '\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430: ECS + Web Worker + Object Pool' },\n                    params: { name: '\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u0432\u044b\u0436\u0438\u0432\u0448\u0438\u0439', zombieHealth: '\u0417\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0437\u043e\u043c\u0431\u0438', zombieSpeed: '\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u0437\u043e\u043c\u0431\u0438', startingWave: '\u0421\u0442\u0430\u0440\u0442\u043e\u0432\u0430\u044f \u0432\u043e\u043b\u043d\u0430', headshotMultiplier: '\u041c\u043d\u043e\u0436\u0438\u0442\u0435\u043b\u044c \u0432\u044b\u0441\u0442\u0440\u0435\u043b\u0430 \u0432 \u0433\u043e\u043b\u043e\u0432\u0443', katanaSpeed: '\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043a\u0430\u0442\u0430\u043d\u044b', lighting: '\u041e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435', coolWhite: '\u0425\u043e\u043b\u043e\u0434\u043d\u044b\u0439 \u0431\u0435\u043b\u044b\u0439', warm: '\u0422\u0451\u043f\u043b\u044b\u0439', emergencyRed: '\u0410\u0432\u0430\u0440\u0438\u0439\u043d\u044b\u0439 \u043a\u0440\u0430\u0441\u043d\u044b\u0439', nightVision: '\u041d\u043e\u0447\u043d\u043e\u0435 \u0432\u0438\u0434\u0435\u043d\u0438\u0435' },\n                    weapons: { PISTOL: '\u0423\u041c\u041d\u042b\u0419 \u041f\u0418\u0421\u0422\u041e\u041b\u0415\u0422', SHOTGUN: '\u0414\u0420\u041e\u0411\u041e\u0412\u0418\u041a', SNIPER: '\u0421\u041d\u0410\u0419\u041f\u0415\u0420\u041a\u0410', KATANA: '\u041a\u0410\u0422\u0410\u041d\u0410', GATLING: '\u0413\u0410\u0422\u041b\u0418\u041d\u0413', LMG: '\u0420\u0423\u0427\u041d\u041e\u0419 \u041f\u0423\u041b\u0415\u041c\u0401\u0422', 'AUTO RIFLE': '\u0410\u0412\u0422\u041e\u041c\u0410\u0422', FLAMETHROWER: '\u041e\u0413\u041d\u0415\u041c\u0415\u0422' },\n                    zombies: { Walker: '\u0425\u043e\u0434\u043e\u043a', Runner: '\u0411\u0435\u0433\u0443\u043d', Tank: '\u0422\u0430\u043d\u043a', Crawler: '\u041f\u043e\u043b\u0437\u0443\u043d', Berserker: '\u0411\u0435\u0440\u0441\u0435\u0440\u043a', Armored: '\u0411\u0440\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439', Swarm: '\u0420\u043e\u0439', Bloater: '\u0420\u0430\u0437\u0434\u0443\u0432\u0448\u0438\u0439\u0441\u044f', Stalker: '\u0421\u0442\u0430\u043b\u043a\u0435\u0440', Screamer: '\u041a\u0440\u0438\u043a\u0443\u043d', BRUTE: '\u0413\u0420\u041e\u041c\u0418\u041b\u0410', CHARGER: '\u0422\u0410\u0420\u0410\u041d', BEHEMOTH: '\u0418\u0421\u041f\u041e\u041b\u0418\u041d', INFERNAL: '\u0418\u041d\u0424\u0415\u0420\u041d\u0410\u041b', OVERLORD: '\u0412\u041b\u0410\u0414\u042b\u041a\u0410' }\n                },\n                ur: {\n                    app: { title: '\u0622\u062e\u0631\u06cc \u0632\u0646\u062f\u06c1 \u0628\u0686\u0646\u06d2 \u0648\u0627\u0644\u0627', titleUpper: '\u0622\u062e\u0631\u06cc \u0632\u0646\u062f\u06c1 \u0628\u0686\u0646\u06d2 \u0648\u0627\u0644\u0627', titleBreak: '\u0622\u062e\u0631\u06cc<br>\u0632\u0646\u062f\u06c1 \u0628\u0686\u0646\u06d2 \u0648\u0627\u0644\u0627' },\n                    tutorial: { title: '\u3010\u0631\u06c1\u0646\u0645\u0627\u0626\u06cc\u3011\u0646\u06cc\u0627 \u06c1\u062a\u06be\u06cc\u0627\u0631', desc: '\u0622\u067e \u0646\u06d2 \u0646\u06cc\u0627 \u06c1\u062a\u06be\u06cc\u0627\u0631 \u062d\u0627\u0635\u0644 \u06a9\u0631 \u0644\u06cc\u0627 \u06c1\u06d2! \u0633\u0648\u0626\u0686 \u06a9\u0631\u0646\u06d2 \u0627\u0648\u0631 \u062c\u0627\u0631\u06cc \u0631\u06a9\u06be\u0646\u06d2 \u06a9\u06d2 \u0644\u06cc\u06d2 \u0646\u06cc\u0686\u06d2 \u0646\u0645\u0627\u06cc\u0627\u06ba \u06a9\u0631\u062f\u06c1 \u0622\u0626\u06cc\u06a9\u0646 \u067e\u0631 \u0679\u06cc\u067e \u06a9\u0631\u06cc\u06ba \u06cc\u0627 [2] \u062f\u0628\u0627\u0626\u06cc\u06ba\u06d4' },\n                    loading: { assets: '\u0627\u062b\u0627\u062b\u06d2 \u0644\u0648\u0688 \u06c1\u0648 \u0631\u06c1\u06d2 \u06c1\u06cc\u06ba...', assetsProgress: '\u0627\u062b\u0627\u062b\u06d2 \u0644\u0648\u0688 \u06c1\u0648 \u0631\u06c1\u06d2 \u06c1\u06cc\u06ba... {pct}%' },\n                    start: { subtitle: '\u0628\u0646\u06a9\u0631 \u06a9\u0648 \u0644\u0627\u0645\u062a\u0646\u0627\u06c1\u06cc \u0632\u0648\u0645\u0628\u06cc \u0644\u0634\u06a9\u0631\u0648\u06ba \u0633\u06d2 \u0628\u0686\u0627\u0626\u06cc\u06ba', play: '\u06a9\u06be\u06cc\u0644\u06cc\u06ba' },\n                    hud: { wave: '\u0644\u06c1\u0631 {wave}', waveBoss: '\u0644\u06c1\u0631 {wave} - \u0628\u0627\u0633: {boss}', waveInitial: '\u0644\u06c1\u0631 1', score: '\u0627\u0633\u06a9\u0648\u0631', interact: '\u062a\u0639\u0627\u0645\u0644 \u06a9\u06d2 \u0644\u06cc\u06d2 E \u062f\u0628\u0627\u0626\u06cc\u06ba', fire: '\u0641\u0627\u0626\u0631', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} \u0631\u06cc \u0644\u0648\u0688', seconds: '{value} \u062b' },\n                    gameOver: { title: '\u0628\u0646\u06a9\u0631 \u0679\u0648\u0679 \u06af\u06cc\u0627', retry: '\u062f\u0648\u0628\u0627\u0631\u06c1' },\n                    stats: { wave: '\u0644\u06c1\u0631', kills: '\u06c1\u0644\u0627\u06a9\u062a\u06cc\u06ba', headshots: '\u06c1\u06cc\u0688 \u0634\u0627\u0679\u0633', score: '\u0627\u0633\u06a9\u0648\u0631', accuracy: '\u062f\u0631\u0633\u062a\u06cc' },\n                    weaponSelect: { title: '\u06c1\u062a\u06be\u06cc\u0627\u0631 \u0686\u0646\u06cc\u06ba', subtitle: '\u0627\u067e\u0646\u0627 \u0627\u0628\u062a\u062f\u0627\u0626\u06cc \u06c1\u062a\u06be\u06cc\u0627\u0631 \u0645\u0646\u062a\u062e\u0628 \u06a9\u0631\u06cc\u06ba' },\n                    upgrade: { waveCompleteInitial: '\u0644\u06c1\u0631 \u0645\u06a9\u0645\u0644!', waveComplete: '\u0644\u06c1\u0631 {wave} \u0645\u06a9\u0645\u0644!', choose: '\u0627\u06cc\u06a9 \u0627\u067e \u06af\u0631\u06cc\u0688 \u0686\u0646\u06cc\u06ba', lifestealName: '\u062c\u0627\u0646 \u0686\u0648\u0631\u06cc+', lifestealDesc: '\u06c1\u0679 \u067e\u0631 \u0634\u0641\u0627 {from}% -> {to}%', damageName: '{weapon} \u0646\u0642\u0635\u0627\u0646+', damageDesc: '\u0646\u0642\u0635\u0627\u0646 {from} -> {to}', reloadName: '{weapon} \u0631\u06cc \u0644\u0648\u0688+', reloadDesc: '\u0631\u06cc \u0644\u0648\u0688 {from}\u062b -> {to}\u062b', magazineName: '{weapon} \u0645\u06cc\u06af\u0632\u06cc\u0646+', magazineDesc: '\u0645\u06cc\u06af\u0632\u06cc\u0646 {from} -> {to}', pierceName: '{weapon} \u0646\u0641\u0648\u0630+', pierceDesc: '\u0646\u0641\u0648\u0630 {from} -> {to}', fireRateName: '{weapon} \u0641\u0627\u0626\u0631 \u0631\u06cc\u0679+', fireRateDesc: '\u0648\u0642\u0641\u06c1 {from}\u0645\u0644\u06cc \u062b -> {to}\u0645\u0644\u06cc \u062b', speedName: '{weapon} \u0631\u0641\u062a\u0627\u0631+', rangeName: '{weapon} \u0631\u06cc\u0646\u062c+', rangeDesc: '\u0631\u06cc\u0646\u062c {from}\u0645 -> {to}\u0645', newWeaponName: '\u0646\u06cc\u0627 \u06c1\u062a\u06be\u06cc\u0627\u0631: {weapon}', slowTimeName: '\u3010\u0645\u06c1\u0627\u0631\u062a\u3011\u0648\u0642\u062a \u06a9\u0627 \u067e\u06be\u06cc\u0644\u0627\u0624', slowTimeDesc: '\u062f\u0646\u06cc\u0627 100 \u06af\u0646\u0627 \u062f\u06be\u06cc\u0645\u06cc \u06c1\u0648 \u062c\u0627\u062a\u06cc \u06c1\u06d2\u060c \u06a9\u06be\u0644\u0627\u0691\u06cc \u0645\u062a\u0627\u062b\u0631 \u0646\u06c1\u06cc\u06ba \u06c1\u0648\u062a\u0627\u06d4 \u062f\u0648\u0631\u0627\u0646\u06cc\u06c1 5 \u0633\u06cc\u06a9\u0646\u0688\u06d4', flameRangeName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u0627\u06cc\u0646\u062f\u06be\u0646 \u06a9\u0627 \u062f\u0628\u0627\u0624', flameRangeDesc: '\u0631\u06cc\u0646\u062c \u0645\u06cc\u06ba +40% (\u0631\u0641\u062a\u0627\u0631 +15%) \u0627\u0648\u0631 \u0628\u0631\u0627\u06c1 \u0631\u0627\u0633\u062a \u0646\u0642\u0635\u0627\u0646 \u0645\u06cc\u06ba +20% \u0627\u0636\u0627\u0641\u06c1 \u06a9\u0631\u06cc\u06ba (\u0644\u06cc\u0648\u0644 {level})', flameStickyName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u0686\u067e\u0686\u067e\u06cc \u0622\u06af', flameStickyDesc: '\u062c\u0644\u0646\u06d2 \u06a9\u06d2 \u062f\u0648\u0631\u0627\u0646\u06cc\u06d2 \u0645\u06cc\u06ba +2 \u0633\u06cc\u06a9\u0646\u0688 \u0627\u0648\u0631 \u06c1\u0631 \u0679\u06a9 \u06a9\u06d2 \u0646\u0642\u0635\u0627\u0646 \u0645\u06cc\u06ba +50% \u0627\u0636\u0627\u0641\u06c1 \u06a9\u0631\u06cc\u06ba (\u0644\u06cc\u0648\u0644 {level})', flameSpreadName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u0686\u0648\u0691\u0627 \u0646\u0648\u0632\u0644', flameSpreadDesc: '\u0634\u0639\u0644\u06d2 \u06a9\u06d2 \u0633\u0627\u0626\u0632 \u0627\u0648\u0631 \u062a\u0635\u0627\u062f\u0645 \u06a9\u06d2 \u0639\u0644\u0627\u0642\u06d2 \u0645\u06cc\u06ba +35% \u0627\u0636\u0627\u0641\u06c1 \u06a9\u0631\u06cc\u06ba (\u0644\u06cc\u0648\u0644 {level})', flameEmberName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u0686\u0646\u06af\u0627\u0631\u06cc \u06a9\u0627 \u062f\u0648\u0628\u0627\u0631\u06c1 \u0627\u0628\u06be\u0631\u0646\u0627', flameEmberDesc: '\u062c\u0644\u062a\u06d2 \u06c1\u0648\u0626\u06d2 \u062f\u0634\u0645\u0646\u0648\u06ba \u06a9\u06d2 \u067e\u0627\u0633 \u0627\u067e\u0646\u06d2 \u0642\u0631\u06cc\u0628\u06cc \u062f\u0634\u0645\u0646\u0648\u06ba \u06a9\u0648 \u062c\u0644\u0627\u0646\u06d2 \u06a9\u0627 30% \u0645\u0648\u0642\u0639 \u06c1\u0648\u062a\u0627 \u06c1\u06d2', flameLavaName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u062a\u06cc\u0632 \u062f\u0631\u062c\u06c1 \u062d\u0631\u0627\u0631\u062a \u0648\u0627\u0644\u0627 \u0644\u0627\u0648\u0627', flameLavaDesc: '\u0634\u0639\u0644\u06d2 \u062f\u0634\u0645\u0646 \u06a9\u06d2 \u0632\u0631\u06c1 \u06a9\u0648 \u0645\u06a9\u0645\u0644 \u0637\u0648\u0631 \u067e\u0631 \u0646\u0638\u0631 \u0627\u0646\u062f\u0627\u0632 \u06a9\u0631\u062a\u06d2 \u06c1\u06cc\u06ba \u0627\u0648\u0631 \u06c1\u0631 \u0633\u06cc\u06a9\u0646\u0688 \u0645\u06cc\u06ba 15% \u0632\u06cc\u0627\u062f\u06c1 \u0633\u06d2 \u0632\u06cc\u0627\u062f\u06c1 \u0632\u0631\u06c1 \u067e\u06af\u06be\u0644\u0627\u062a\u06d2 \u06c1\u06cc\u06ba', flameRangeName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u0627\u06cc\u0646\u062f\u06be\u0646 \u06a9\u0627 \u062f\u0628\u0627\u0624', flameRangeDesc: '\u0631\u06cc\u0646\u062c \u0645\u06cc\u06ba +40% (\u0631\u0641\u062a\u0627\u0631 +15%) \u0627\u0648\u0631 \u0628\u0631\u0627\u06c1 \u0631\u0627\u0633\u062a \u0646\u0642\u0635\u0627\u0646 \u0645\u06cc\u06ba +20% \u0627\u0636\u0627\u0641\u06c1 \u06a9\u0631\u06cc\u06ba (\u0644\u06cc\u0648\u0644 {level})', flameStickyName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u0686\u067e\u0686\u067e\u06cc \u0622\u06af', flameStickyDesc: '\u062c\u0644\u0646\u06d2 \u06a9\u06d2 \u062f\u0648\u0631\u0627\u0646\u06cc\u06d2 \u0645\u06cc\u06ba +2 \u0633\u06cc\u06a9\u0646\u0688 \u0627\u0648\u0631 \u06c1\u0631 \u0679\u06a9 \u06a9\u06d2 \u0646\u0642\u0635\u0627\u0646 \u0645\u06cc\u06ba +50% \u0627\u0636\u0627\u0641\u06c1 \u06a9\u0631\u06cc\u06ba (\u0644\u06cc\u0648\u0644 {level})', flameSpreadName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u0686\u0648\u0691\u0627 \u0646\u0648\u0632\u0644', flameSpreadDesc: '\u0634\u0639\u0644\u06d2 \u06a9\u06d2 \u0633\u0627\u0626\u0632 \u0627\u0648\u0631 \u062a\u0635\u0627\u062f\u0645 \u06a9\u06d2 \u0639\u0644\u0627\u0642\u06d2 \u0645\u06cc\u06ba +35% \u0627\u0636\u0627\u0641\u06c1 \u06a9\u0631\u06cc\u06ba (\u0644\u06cc\u0648\u0644 {level})', flameEmberName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u0686\u0646\u06af\u0627\u0631\u06cc \u06a9\u0627 \u062f\u0648\u0628\u0627\u0631\u06c1 \u0627\u0628\u06be\u0631\u0646\u0627', flameEmberDesc: '\u062c\u0644\u062a\u06d2 \u06c1\u0648\u0626\u06d2 \u062f\u0634\u0645\u0646\u0648\u06ba \u06a9\u06d2 \u067e\u0627\u0633 \u0627\u067e\u0646\u06d2 \u0642\u0631\u06cc\u0628\u06cc \u062f\u0634\u0645\u0646\u0648\u06ba \u06a9\u0648 \u062c\u0644\u0627\u0646\u06d2 \u06a9\u0627 30% \u0645\u0648\u0642\u0639 \u06c1\u0648\u062a\u0627 \u06c1\u06d2', flameLavaName: '[\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627] \u062a\u06cc\u0632 \u062f\u0631\u062c\u06c1 \u062d\u0631\u0627\u0631\u062a \u0648\u0627\u0644\u0627 \u0644\u0627\u0648\u0627', flameLavaDesc: '\u0634\u0639\u0644\u06d2 \u062f\u0634\u0645\u0646 \u06a9\u06d2 \u0632\u0631\u06c1 \u06a9\u0648 \u0645\u06a9\u0645\u0644 \u0637\u0648\u0631 \u067e\u0631 \u0646\u0638\u0631 \u0627\u0646\u062f\u0627\u0632 \u06a9\u0631\u062a\u06d2 \u06c1\u06cc\u06ba \u0627\u0648\u0631 \u06c1\u0631 \u0633\u06cc\u06a9\u0646\u0688 \u0645\u06cc\u06ba 15% \u0632\u06cc\u0627\u062f\u06c1 \u0633\u06d2 \u0632\u06cc\u0627\u062f\u06c1 \u0632\u0631\u06c1 \u067e\u06af\u06be\u0644\u0627\u062a\u06d2 \u06c1\u06cc\u06ba' },\n                    weaponStats: { melee: '\u0642\u0631\u06cc\u0628\u06cc', auto: '\u062e\u0648\u062f\u06a9\u0627\u0631', semi: '\u0646\u06cc\u0645 \u062e\u0648\u062f\u06a9\u0627\u0631', damage: '\u0646\u0642\u0635\u0627\u0646:{value}', range: '\u0631\u06cc\u0646\u062c:{value}\u0645', magazine: '\u0645\u06cc\u06af\u0632\u06cc\u0646:{value}' },\n                    edit: { title: '\u062a\u0631\u0645\u06cc\u0645 \u0645\u0648\u0688', line1: '\u06af\u06cc\u0645 \u06a9\u06d2 \u067e\u06cc\u0631\u0627\u0645\u06cc\u0679\u0631 \u0628\u062f\u0644\u0646\u06d2 \u06a9\u06d2 \u0644\u06cc\u06d2 \u0633\u06cc\u0679\u0646\u06af\u0632 \u067e\u06cc\u0646\u0644 \u0627\u0633\u062a\u0639\u0645\u0627\u0644 \u06a9\u0631\u06cc\u06ba\u06d4', line2: '\u0632\u0648\u0645\u0628\u06cc \u06a9\u06cc \u0645\u0634\u06a9\u0644\u060c \u0627\u0628\u062a\u062f\u0627\u0626\u06cc \u0644\u06c1\u0631\u060c \u06c1\u062a\u06be\u06cc\u0627\u0631 \u0627\u0648\u0631 \u0631\u0648\u0634\u0646\u06cc \u0627\u06cc\u0688\u062c\u0633\u0679 \u06a9\u0631\u06cc\u06ba\u06d4', architecture: '\u0622\u0631\u06a9\u06cc\u0679\u06cc\u06a9\u0686\u0631: ECS + Web Worker + Object Pool' },\n                    params: { name: '\u0622\u062e\u0631\u06cc \u0632\u0646\u062f\u06c1 \u0628\u0686\u0646\u06d2 \u0648\u0627\u0644\u0627', zombieHealth: '\u0632\u0648\u0645\u0628\u06cc \u0635\u062d\u062a', zombieSpeed: '\u0632\u0648\u0645\u0628\u06cc \u0631\u0641\u062a\u0627\u0631', startingWave: '\u0627\u0628\u062a\u062f\u0627\u0626\u06cc \u0644\u06c1\u0631', headshotMultiplier: '\u06c1\u06cc\u0688 \u0634\u0627\u0679 \u0636\u0631\u0628', katanaSpeed: '\u06a9\u0679\u0627\u0646\u0627 \u0631\u0641\u062a\u0627\u0631', lighting: '\u0631\u0648\u0634\u0646\u06cc', coolWhite: '\u0679\u06be\u0646\u0688\u06cc \u0633\u0641\u06cc\u062f', warm: '\u06af\u0631\u0645', emergencyRed: '\u06c1\u0646\u06af\u0627\u0645\u06cc \u0633\u0631\u062e', nightVision: '\u0646\u0627\u0626\u0679 \u0648\u0698\u0646' },\n                    weapons: { PISTOL: '\u0627\u0633\u0645\u0627\u0631\u0679 \u067e\u0633\u062a\u0648\u0644', SHOTGUN: '\u0634\u0627\u0679 \u06af\u0646', SNIPER: '\u0627\u0633\u0646\u0627\u0626\u067e\u0631', KATANA: '\u06a9\u0679\u0627\u0646\u0627', GATLING: '\u06af\u06cc\u0679\u0644\u0646\u06af', LMG: '\u0627\u06cc\u0644 \u0627\u06cc\u0645 \u062c\u06cc', 'AUTO RIFLE': '\u062e\u0648\u062f\u06a9\u0627\u0631 \u0631\u0627\u0626\u0641\u0644', FLAMETHROWER: '\u0634\u0639\u0644\u06c1 \u067e\u06be\u06cc\u0646\u06a9\u0646\u06d2 \u0648\u0627\u0644\u0627' },\n                    zombies: { Walker: '\u0686\u0644\u0646\u06d2 \u0648\u0627\u0644\u0627', Runner: '\u062f\u0648\u0691\u0646\u06d2 \u0648\u0627\u0644\u0627', Tank: '\u0679\u06cc\u0646\u06a9', Crawler: '\u0631\u06cc\u0646\u06af\u0646\u06d2 \u0648\u0627\u0644\u0627', Berserker: '\u0628\u067e\u06be\u0631\u0627 \u06c1\u0648\u0627', Armored: '\u0632\u0631\u06c1 \u067e\u0648\u0634', Swarm: '\u062c\u06be\u0646\u0688', Bloater: '\u067e\u06be\u0648\u0644\u0627 \u06c1\u0648\u0627', Stalker: '\u062a\u0639\u0627\u0642\u0628 \u06a9\u0631\u0646\u06d2 \u0648\u0627\u0644\u0627', Screamer: '\u0686\u06cc\u062e\u0646\u06d2 \u0648\u0627\u0644\u0627', BRUTE: '\u0648\u062d\u0634\u06cc', CHARGER: '\u062d\u0645\u0644\u06c1 \u0622\u0648\u0631', BEHEMOTH: '\u062f\u06cc\u0648\u06c1\u06cc\u06a9\u0644', INFERNAL: '\u062c\u06c1\u0646\u0645\u06cc', OVERLORD: '\u062d\u0627\u06a9\u0645' }\n                },\n                bn: {\n                    app: { title: '\u09b6\u09c7\u09b7 \u09ac\u09c7\u0981\u099a\u09c7 \u09a5\u09be\u0995\u09be', titleUpper: '\u09b6\u09c7\u09b7 \u09ac\u09c7\u0981\u099a\u09c7 \u09a5\u09be\u0995\u09be', titleBreak: '\u09b6\u09c7\u09b7<br>\u09ac\u09c7\u0981\u099a\u09c7 \u09a5\u09be\u0995\u09be' },\n                    tutorial: { title: '\u3010\u099f\u09bf\u0989\u099f\u09cb\u09b0\u09bf\u09af\u09bc\u09be\u09b2\u3011\u09a8\u09a4\u09c1\u09a8 \u0985\u09b8\u09cd\u09a4\u09cd\u09b0', desc: '\u0986\u09aa\u09a8\u09bf \u098f\u0995\u099f\u09bf \u09a8\u09a4\u09c1\u09a8 \u0985\u09b8\u09cd\u09a4\u09cd\u09b0 \u09aa\u09c7\u09af\u09bc\u09c7\u099b\u09c7\u09a8! \u098f\u099f\u09bf \u09aa\u09b0\u09bf\u09ac\u09b0\u09cd\u09a4\u09a8 \u0995\u09b0\u09a4\u09c7 \u098f\u09ac\u0982 \u0996\u09c7\u09b2\u09be \u099a\u09be\u09b2\u09bf\u09af\u09bc\u09c7 \u09af\u09c7\u09a4\u09c7 \u09a8\u09bf\u099a\u09c7\u09b0 \u09b9\u09be\u0987\u09b2\u09be\u0987\u099f \u0995\u09b0\u09be \u0986\u0987\u0995\u0928\u0947 \u0986\u09b2\u09a4\u09cb \u099a\u09be\u09aa\u09c1\u09a8 \u09ac\u09be [2] \u099f\u09bf\u09aa\u09c1\u09a8\u0964' },\n                    loading: { assets: '\u0985\u09cd\u09af\u09be\u09b8\u09c7\u099f \u09b2\u09cb\u09a1 \u09b9\u099a\u09cd\u099b\u09c7...', assetsProgress: '\u0985\u09cd\u09af\u09be\u09b8\u09c7\u099f \u09b2\u09cb\u09a1 \u09b9\u099a\u09cd\u099b\u09c7... {pct}%' },\n                    start: { subtitle: '\u0985\u09a8\u09cd\u09a4\u09b9\u09c0\u09a8 \u099c\u09ae\u09cd\u09ac\u09bf \u09a6\u09b2\u09c7\u09b0 \u09b9\u09be\u09a4 \u09a5\u09c7\u0995\u09c7 \u09ac\u09be\u0999\u09cd\u0995\u09be\u09b0 \u09b0\u0995\u09cd\u09b7\u09be \u0995\u09b0\u09c1\u09a8', play: '\u0996\u09c7\u09b2\u09c1\u09a8' },\n                    hud: { wave: '\u0993\u09af\u09bc\u09c7\u09ad {wave}', waveBoss: '\u0993\u09af\u09bc\u09c7\u09ad {wave} - \u09ac\u09b8: {boss}', waveInitial: '\u0993\u09af\u09bc\u09c7\u09ad 1', score: '\u09b8\u09cd\u0995\u09cb\u09b0', interact: '\u0987\u09a8\u09cd\u099f\u09be\u09b0\u0985\u09cd\u09af\u09be\u0995\u09cd\u099f \u0995\u09b0\u09a4\u09c7 E \u099a\u09be\u09aa\u09c1\u09a8', fire: '\u0997\u09c1\u09b2\u09bf', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} \u09b0\u09bf\u09b2\u09cb\u09a1', seconds: '{value}\u09b8\u09c7' },\n                    gameOver: { title: '\u09ac\u09be\u0999\u09cd\u0995\u09be\u09b0 \u09ad\u09c7\u0999\u09c7 \u09aa\u09a1\u09bc\u09c7\u099b\u09c7', retry: '\u0986\u09ac\u09be\u09b0 \u099a\u09c7\u09b7\u09cd\u099f\u09be' },\n                    stats: { wave: '\u0993\u09af\u09bc\u09c7\u09ad', kills: '\u09b9\u09a4\u09cd\u09af\u09be', headshots: '\u09b9\u09c7\u09a1\u09b6\u099f', score: '\u09b8\u09cd\u0995\u09cb\u09b0', accuracy: '\u09a8\u09bf\u09b0\u09cd\u09ad\u09c1\u09b2\u09a4\u09be' },\n                    weaponSelect: { title: '\u0985\u09b8\u09cd\u09a4\u09cd\u09b0 \u09ac\u09be\u099b\u09c1\u09a8', subtitle: '\u09b6\u09c1\u09b0\u09c1\u09b0 \u0985\u09b8\u09cd\u09a4\u09cd\u09b0 \u09a8\u09bf\u09b0\u09cd\u09ac\u09be\u099a\u09a8 \u0995\u09b0\u09c1\u09a8' },\n                    upgrade: { waveCompleteInitial: '\u0993\u09af\u09bc\u09c7\u09ad \u09b6\u09c7\u09b7!', waveComplete: '\u0993\u09af\u09bc\u09c7\u09ad {wave} \u09b6\u09c7\u09b7!', choose: '\u098f\u0995\u099f\u09bf \u0986\u09aa\u0997\u09cd\u09b0\u09c7\u09a1 \u09ac\u09be\u099b\u09c1\u09a8', lifestealName: '\u099c\u09c0\u09ac\u09a8\u09b6\u09cb\u09b7\u09a3+', lifestealDesc: '\u0986\u0998\u09be\u09a4\u09c7 \u09b8\u09c7\u09b0\u09c7 \u0993\u09a0\u09be {from}% -> {to}%', damageName: '{weapon} \u0995\u09cd\u09b7\u09a4\u09bf+', damageDesc: '\u0995\u09cd\u09b7\u09a4\u09bf {from} -> {to}', reloadName: '{weapon} \u09b0\u09bf\u09b2\u09cb\u09a1+', reloadDesc: '\u09b0\u09bf\u09b2\u09cb\u09a1 {from}\u09b8\u09c7 -> {to}\u09b8\u09c7', magazineName: '{weapon} \u09ae\u09cd\u09af\u09be\u0997+', magazineDesc: '\u09ae\u09cd\u09af\u09be\u0997\u09be\u099c\u09bf\u09a8 {from} -> {to}', pierceName: '{weapon} \u09ad\u09c7\u09a6+', pierceDesc: '\u09ad\u09c7\u09a6\u09a8 {from} -> {to}', fireRateName: '{weapon} \u09ab\u09be\u09af\u09bc\u09be\u09b0 \u09b0\u09c7\u099f+', fireRateDesc: '\u09ac\u09bf\u09b0\u09a4\u09bf {from}\u09ae\u09bf\u09b8\u09c7 -> {to}\u09ae\u09bf\u09b8\u09c7', speedName: '{weapon} \u0997\u09a4\u09bf+', rangeName: '{weapon} \u09aa\u09be\u09b2\u09cd\u09b2\u09be+', rangeDesc: '\u09aa\u09be\u09b2\u09cd\u09b2\u09be {from}\u09ae\u09bf -> {to}\u09ae\u09bf', newWeaponName: '\u09a8\u09a4\u09c1\u09a8 \u0985\u09b8\u09cd\u09a4\u09cd\u09b0: {weapon}', slowTimeName: '\u3010\u09a6\u0995\u09cd\u09b7\u09a4\u09be\u3011\u09b8\u09ae\u09af\u09bc \u09a7\u09c0\u09b0 \u0995\u09b0\u09be', slowTimeDesc: '\u09ac\u09bf\u09b6\u09cd\u09ac\u09c7\u09b0 \u09b8\u09ae\u09af\u09bc \u09e7\u09e6\u09e6 \u0997\u09c1\u09a3 \u09a7\u09c0\u09b0 \u09b9\u09af\u09bc, \u0996\u09c7\u09b2\u09cb\u09af\u09bc\u09be\u09a1\u09bc \u0985\u09aa\u09cd\u09b0\u09ad\u09be\u09ac\u09bf\u09a4 \u09a5\u09be\u0995\u09c7\u0964 \u09b8\u09cd\u09a5\u09be\u09af\u09bc\u09bf\u09a4\u09cd\u09ac \u09eb \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1\u0964', flameRangeName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u099c\u09cd\u09ac\u09be\u09b2\u09be\u09a8\u09c0 \u09b8\u0982\u0995\u09cb\u099a\u09a8', flameRangeDesc: '\u09aa\u09be\u09b2\u09cd\u09b2\u09be +\u09ea\u09e6% (\u0997\u09a4\u09bf +\u09e7\u09eb%) \u098f\u09ac\u0982 \u09b8\u09b0\u09be\u09b8\u09b0\u09bf \u0995\u09cd\u09b7\u09a4\u09bf +\u09e8\u09e6% \u09ac\u09c3\u09a6\u09cd\u09a7\u09bf \u0995\u09b0\u09c7 (\u09b2\u09c7\u09ad\u09c7\u09b2 {level})', flameStickyName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u0986\u09a0\u09be\u09b2\u09cb \u0986\u0997\u09c1\u09a8', flameStickyDesc: '\u09aa\u09cb\u09a1\u09bc\u09be\u09a8\u09cb\u09b0 \u09b8\u09ae\u09af\u09bc\u0995\u09be\u09b2 +\u09e8 \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1 \u098f\u09ac\u0982 \u09aa\u09cd\u09b0\u09a4\u09bf \u099f\u09bf\u0995 \u0995\u09cd\u09b7\u09a4\u09bf +\u09eb\u09e6% \u09ac\u09c3\u09a6\u09cd\u09a7\u09bf \u0995\u09b0\u09c7 (\u09b2\u09c7\u09ad\u09c7\u09b2 {level})', flameSpreadName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u09aa\u09cd\u09b0\u09b6\u09b8\u09cd\u09a4 \u0985\u0997\u09cd\u09b0\u09ad\u09be\u0997', flameSpreadDesc: '\u0986\u0997\u09c1\u09a8\u09c7\u09b0 \u0986\u0995\u09be\u09b0 \u098f\u09ac\u0982 \u09b8\u0982\u0998\u09b0\u09cd\u09b7\u09c7\u09b0 \u0995\u09cd\u09b7\u09c7\u09a4\u09cd\u09b0 +\u09e9\u09eb% \u09ac\u09c3\u09a6\u09cd\u09a7\u09bf \u0995\u09b0\u09c7 (\u09b2\u09c7\u09ad\u09c7\u09b2 {level})', flameEmberName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u09b8\u09cd\u09ab\u09c1\u09b2\u09bf\u0999\u09cd\u0997\u09c7\u09b0 \u09aa\u09c1\u09a8\u09b0\u09c1\u09a4\u09cd\u09a5\u09be\u09a8', flameEmberDesc: '\u099c\u09cd\u09ac\u09b2\u09a8\u09cd\u09a4 \u09b6\u09a4\u09cd\u09b0\u09c1\u09a6\u09c7\u09b0 \u0986\u09b6\u09c7\u09aa\u09be\u09b6\u09c7\u09b0 \u09b6\u09a4\u09cd\u09b0\u09c1\u09a6\u09c7\u09b0 \u099c\u09cd\u09ac\u09be\u09b2\u09be\u09a8\u09cb\u09b0 \u09e9\u09e6% \u09b8\u09ae\u09cd\u09ad\u09be\u09ac\u09a8\u09be \u09a5\u09be\u0995\u09c7', flameLavaName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u0989\u099a\u09cd\u099a-\u09a4\u09be\u09aa\u09ae\u09be\u09a4\u09cd\u09b0\u09be\u09b0 \u09b2\u09be\u09ad\u09be', flameLavaDesc: '\u09b6\u09bf\u0996\u09be \u09b6\u09a4\u09cd\u09b0\u09c1\u09b0 \u09ac\u09b0\u09cd\u09ae \u09b8\u09ae\u09cd\u09aa\u09c2\u09b0\u09cd\u09a3\u09b0\u09c2\u09aa\u09c7 \u0989\u09aa\u09c7\u0995\u09cd\u09b7\u09be \u0995\u09b0\u09c7 \u098f\u09ac\u0982 \u09aa\u09cd\u09b0\u09a4\u09bf \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1\u09c7 \u09e7\u09eb% \u09b8\u09b0\u09cd\u09ac\u09cb\u099a\u09cd\u099a \u09ac\u09b0\u09cd\u09ae \u0997\u09b2\u09bf\u09af\u09bc\u09c7 \u09a6\u09c7\u09af\u09bc', flameRangeName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u099c\u09cd\u09ac\u09be\u09b2\u09be\u09a8\u09c0 \u09b8\u0982\u0995\u09cb\u099a\u09a8', flameRangeDesc: '\u09aa\u09be\u09b2\u09cd\u09b2\u09be +\u09ea\u09e6% (\u0997\u09a4\u09bf +\u09e7\u09eb%) \u098f\u09ac\u0982 \u09b8\u09b0\u09be\u09b8\u09b0\u09bf \u0995\u09cd\u09b7\u09a4\u09bf +\u09e8\u09e6% \u09ac\u09c3\u09a6\u09cd\u09a7\u09bf \u0995\u09b0\u09c7 (\u09b2\u09c7\u09ad\u09c7\u09b2 {level})', flameStickyName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u0986\u09a0\u09be\u09b2\u09cb \u0986\u0997\u09c1\u09a8', flameStickyDesc: '\u09aa\u09cb\u09a1\u09bc\u09be\u09a8\u09cb\u09b0 \u09b8\u09ae\u09af\u09bc\u0995\u09be\u09b2 +\u09e8 \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1 \u098f\u09ac\u0982 \u09aa\u09cd\u09b0\u09a4\u09bf \u099f\u09bf\u0995 \u0995\u09cd\u09b7\u09a4\u09bf +\u09eb\u09e6% \u09ac\u09c3\u09a6\u09cd\u09a7\u09bf \u0995\u09b0\u09c7 (\u09b2\u09c7\u09ad\u09c7\u09b2 {level})', flameSpreadName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u09aa\u09cd\u09b0\u09b6\u09b8\u09cd\u09a4 \u0985\u0997\u09cd\u09b0\u09ad\u09be\u0997', flameSpreadDesc: '\u0986\u0997\u09c1\u09a8\u09c7\u09b0 \u0986\u0995\u09be\u09b0 \u098f\u09ac\u0982 \u09b8\u0982\u0998\u09b0\u09cd\u09b7\u09c7\u09b0 \u0995\u09cd\u09b7\u09c7\u09a4\u09cd\u09b0 +\u09e9\u09eb% \u09ac\u09c3\u09a6\u09cd\u09a7\u09bf \u0995\u09b0\u09c7 (\u09b2\u09c7\u09ad\u09c7\u09b2 {level})', flameEmberName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u09b8\u09cd\u09ab\u09c1\u09b2\u09bf\u0999\u09cd\u0997\u09c7\u09b0 \u09aa\u09c1\u09a8\u09b0\u09c1\u09a4\u09cd\u09a5\u09be\u09a8', flameEmberDesc: '\u099c\u09cd\u09ac\u09b2\u09a8\u09cd\u09a4 \u09b6\u09a4\u09cd\u09b0\u09c1\u09a6\u09c7\u09b0 \u0986\u09b6\u09c7\u09aa\u09be\u09b6\u09c7\u09b0 \u09b6\u09a4\u09cd\u09b0\u09c1\u09a6\u09c7\u09b0 \u099c\u09cd\u09ac\u09be\u09b2\u09be\u09a8\u09cb\u09b0 \u09e9\u09e6% \u09b8\u09ae\u09cd\u09ad\u09be\u09ac\u09a8\u09be \u09a5\u09be\u0995\u09c7', flameLavaName: '[\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0] \u0989\u099a\u09cd\u099a-\u09a4\u09be\u09aa\u09ae\u09be\u09a4\u09cd\u09b0\u09be\u09b0 \u09b2\u09be\u09ad\u09be', flameLavaDesc: '\u09b6\u09bf\u0996\u09be \u09b6\u09a4\u09cd\u09b0\u09c1\u09b0 \u09ac\u09b0\u09cd\u09ae \u09b8\u09ae\u09cd\u09aa\u09c2\u09b0\u09cd\u09a3\u09b0\u09c2\u09aa\u09c7 \u0989\u09aa\u09c7\u0995\u09cd\u09b7\u09be \u0995\u09b0\u09c7 \u098f\u09ac\u0982 \u09aa\u09cd\u09b0\u09a4\u09bf \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1\u09c7 \u09e7\u09eb% \u09b8\u09b0\u09cd\u09ac\u09cb\u099a\u09cd\u099a \u09ac\u09b0\u09cd\u09ae \u0997\u09b2\u09bf\u09af\u09bc\u09c7 \u09a6\u09c7\u09af\u09bc' },\n                    weaponStats: { melee: '\u09a8\u09bf\u0995\u099f \u09af\u09c1\u09a6\u09cd\u09a7', auto: '\u0985\u099f\u09cb', semi: '\u09b8\u09c7\u09ae\u09bf', damage: '\u0995\u09cd\u09b7\u09a4\u09bf:{value}', range: '\u09aa\u09be\u09b2\u09cd\u09b2\u09be:{value}\u09ae\u09bf', magazine: '\u09ae\u09cd\u09af\u09be\u0997:{value}' },\n                    edit: { title: '\u098f\u09a1\u09bf\u099f \u09ae\u09cb\u09a1', line1: '\u0997\u09c7\u09ae\u09c7\u09b0 \u09ae\u09be\u09a8 \u09ac\u09a6\u09b2\u09be\u09a4\u09c7 \u09b8\u09c7\u099f\u09bf\u0982\u09b8 \u09aa\u09cd\u09af\u09be\u09a8\u09c7\u09b2 \u09ac\u09cd\u09af\u09ac\u09b9\u09be\u09b0 \u0995\u09b0\u09c1\u09a8\u0964', line2: '\u099c\u09ae\u09cd\u09ac\u09bf\u09b0 \u0995\u09a0\u09bf\u09a8\u09a4\u09be, \u09b6\u09c1\u09b0\u09c1\u09b0 \u0993\u09af\u09bc\u09c7\u09ad, \u0985\u09b8\u09cd\u09a4\u09cd\u09b0 \u0993 \u0986\u09b2\u09cb \u09b8\u09be\u09ae\u099e\u09cd\u099c\u09b8\u09cd\u09af \u0995\u09b0\u09c1\u09a8\u0964', architecture: '\u0986\u09b0\u09cd\u0995\u09bf\u099f\u09c7\u0995\u099a\u09be\u09b0: ECS + Web Worker + Object Pool' },\n                    params: { name: '\u09b6\u09c7\u09b7 \u09ac\u09c7\u0981\u099a\u09c7 \u09a5\u09be\u0995\u09be', zombieHealth: '\u099c\u09ae\u09cd\u09ac\u09bf\u09b0 \u09b8\u09cd\u09ac\u09be\u09b8\u09cd\u09a5\u09cd\u09af', zombieSpeed: '\u099c\u09ae\u09cd\u09ac\u09bf\u09b0 \u0997\u09a4\u09bf', startingWave: '\u09b6\u09c1\u09b0\u09c1\u09b0 \u0993\u09af\u09bc\u09c7\u09ad', headshotMultiplier: '\u09b9\u09c7\u09a1\u09b6\u099f \u0997\u09c1\u09a3\u0995', katanaSpeed: '\u0995\u09be\u09a4\u09be\u09a8\u09be\u09b0 \u0997\u09a4\u09bf', lighting: '\u0986\u09b2\u09cb', coolWhite: '\u09a0\u09be\u09a8\u09cd\u09a1\u09be \u09b8\u09be\u09a6\u09be', warm: '\u0989\u09b7\u09cd\u09a3', emergencyRed: '\u099c\u09b0\u09c1\u09b0\u09bf \u09b2\u09be\u09b2', nightVision: '\u09a8\u09be\u0987\u099f \u09ad\u09bf\u09b6\u09a8' },\n                    weapons: { PISTOL: '\u09b8\u09cd\u09ae\u09be\u09b0\u09cd\u099f \u09aa\u09bf\u09b8\u09cd\u09a4\u09b2', SHOTGUN: '\u09b6\u099f\u0997\u09be\u09a8', SNIPER: '\u09b8\u09cd\u09a8\u09be\u0987\u09aa\u09be\u09b0', KATANA: '\u0995\u09be\u09a4\u09be\u09a8\u09be', GATLING: '\u0997\u09cd\u09af\u09be\u099f\u09b2\u09bf\u0982', LMG: '\u098f\u09b2\u098f\u09ae\u099c\u09bf', 'AUTO RIFLE': '\u09b8\u09cd\u09ac\u09af\u09bc\u0982\u0995\u09cd\u09b0\u09bf\u09af\u09bc \u09b0\u09be\u0987\u09ab\u09c7\u09b2', FLAMETHROWER: '\u09ab\u09cd\u09b2\u09c7\u09ae\u09a5\u09cd\u09b0\u0993\u09af\u09bc\u09be\u09b0' },\n                    zombies: { Walker: '\u09b9\u09be\u0981\u099f\u09c1\u0995', Runner: '\u09a6\u09cc\u09a1\u09bc\u09ac\u09bf\u09a6', Tank: '\u099f\u09cd\u09af\u09be\u0999\u09cd\u0995', Crawler: '\u09b9\u09be\u09ae\u09be\u0997\u09c1\u09a1\u09bc\u09bf', Berserker: '\u0989\u09a8\u09cd\u09ae\u09a4\u09cd\u09a4', Armored: '\u09ac\u09b0\u09cd\u09ae\u09a7\u09be\u09b0\u09c0', Swarm: '\u099d\u09be\u0981\u0995', Bloater: '\u09ab\u09cb\u09b2\u09be', Stalker: '\u09aa\u09bf\u099b\u09c1 \u09a8\u09c7\u0993\u09af\u09bc\u09be', Screamer: '\u099a\u09bf\u09ce\u0995\u09be\u09b0\u09bf', BRUTE: '\u09a6\u09be\u09a8\u09ac', CHARGER: '\u09a7\u09be\u09ac\u0995', BEHEMOTH: '\u09ae\u09b9\u09be\u09a6\u09be\u09a8\u09ac', INFERNAL: '\u09a8\u09be\u09b0\u0995\u09c0\u09af\u09bc', OVERLORD: '\u0985\u09a7\u09bf\u09aa\u09a4\u09bf' }\n                },\n                sw: {\n                    app: { title: 'Mnusurika wa Mwisho', titleUpper: 'MNUSURIKA WA MWISHO', titleBreak: 'MNUSURIKA<br>WA MWISHO' },\n                    tutorial: { title: '\u3010MAFUNZO\u3011SILAHA MPYA', desc: 'Umepata silaha mpya! Gusa aikoni iliyoangaziwa hapa chini au ubonyeze [2] ili kubadilisha na kuendelea.' },\n                    loading: { assets: 'Inapakia rasilimali...', assetsProgress: 'Inapakia rasilimali... {pct}%' },\n                    start: { subtitle: 'Linda bunker dhidi ya makundi yasiyoisha ya zombi', play: 'CHEZA' },\n                    hud: { wave: 'WIMBI {wave}', waveBoss: 'WIMBI {wave} - BOSI: {boss}', waveInitial: 'WIMBI 1', score: 'ALAMA', interact: 'Bonyeza E kuingiliana', fire: 'FYATUA', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: 'PAKIA {weapon} UPYA', seconds: '{value}s' },\n                    gameOver: { title: 'BUNKER IMEVAMIWA', retry: 'JARIBU TENA' },\n                    stats: { wave: 'Wimbi', kills: 'Mauaji', headshots: 'Vichwa', score: 'Alama', accuracy: 'Usahihi' },\n                    weaponSelect: { title: 'CHAGUA SILAHA', subtitle: 'Chagua silaha ya kuanzia' },\n                    upgrade: { waveCompleteInitial: 'WIMBI LIMEISHA!', waveComplete: 'WIMBI {wave} LIMEISHA!', choose: 'Chagua uboreshaji', lifestealName: 'VUTA UHAI+', lifestealDesc: 'Pona unapogonga {from}% -> {to}%', damageName: '{weapon} NGUVU+', damageDesc: 'Uharibifu {from} -> {to}', reloadName: '{weapon} PAKIA+', reloadDesc: 'Kupakia {from}s -> {to}s', magazineName: '{weapon} MAG+', magazineDesc: 'Magazini {from} -> {to}', pierceName: '{weapon} PENYA+', pierceDesc: 'Kupenya {from} -> {to}', fireRateName: '{weapon} KASI YA RISASI+', fireRateDesc: 'Muda kati {from}ms -> {to}ms', speedName: '{weapon} KASI+', rangeName: '{weapon} UMBALI+', rangeDesc: 'Umbali {from}m -> {to}m', newWeaponName: 'SILAHA MPYA: {weapon}', slowTimeName: '\u3010UJUANJA\u3011KUPUNGUZA MUDA', slowTimeDesc: 'Ulimwengu unapungua mara 100, mchezaji haathiriwi. Huchukua sekunde 5.', flameRangeName: '[Rushasha Moto] Mgandamizo wa Mafuta', flameRangeDesc: 'Ongeza umbali kwa +40% (kasi +15%) na uharibifu wa moja kwa moja kwa +20% (Kiwango {level})', flameStickyName: '[Rushasha Moto] Moto Unatao', flameStickyDesc: 'Ongeza muda wa kuungua kwa sekunde +2 na uharibifu kwa +50% (Kiwango {level})', flameSpreadName: '[Rushasha Moto] Nozo Pana', flameSpreadDesc: 'Ongeza ukubwa wa mwali na eneo la mgongano kwa +35% (Kiwango {level})', flameEmberName: '[Rushasha Moto] Kufufuka kwa Makaa', flameEmberDesc: 'Maadui wanaoungua wana nafasi ya 30% ya kuwasha maadui wa karibu', flameLavaName: '[Rushasha Moto] Lava ya Joto la Juu', flameLavaDesc: 'Miali ya moto inapuuza ngao ya adui kabisa na kuyeyusha 15% ya ngao ya juu kwa sekunde', flameRangeName: '[Rushasha Moto] Mgandamizo wa Mafuta', flameRangeDesc: 'Ongeza umbali kwa +40% (kasi +15%) na uharibifu wa moja kwa moja kwa +20% (Kiwango {level})', flameStickyName: '[Rushasha Moto] Moto Unatao', flameStickyDesc: 'Ongeza muda wa kuungua kwa sekunde +2 na uharibifu kwa +50% (Kiwango {level})', flameSpreadName: '[Rushasha Moto] Nozo Pana', flameSpreadDesc: 'Ongeza ukubwa wa mwali na eneo la mgongano kwa +35% (Kiwango {level})', flameEmberName: '[Rushasha Moto] Kufufuka kwa Makaa', flameEmberDesc: 'Maadui wanaoungua wana nafasi ya 30% ya kuwasha maadui wa karibu', flameLavaName: '[Rushasha Moto] Lava ya Joto la Juu', flameLavaDesc: 'Miali ya moto inapuuza ngao ya adui kabisa na kuyeyusha 15% ya ngao ya juu kwa sekunde' },\n                    weaponStats: { melee: 'KARIBU', auto: 'OTO', semi: 'SEMI', damage: 'NGV:{value}', range: 'Umbali:{value}m', magazine: 'Mag:{value}' },\n                    edit: { title: 'HALI YA KUHARIRI', line1: 'Tumia paneli ya mipangilio kubadili vigezo vya mchezo.', line2: 'Badili ugumu wa zombi, wimbi la kuanzia, silaha na mwanga.', architecture: 'Muundo: ECS + Web Worker + Object Pool' },\n                    params: { name: 'Mnusurika wa Mwisho', zombieHealth: 'Afya ya Zombi', zombieSpeed: 'Kasi ya Zombi', startingWave: 'Wimbi la Kuanza', headshotMultiplier: 'Kizidishi cha Kichwa', katanaSpeed: 'Kasi ya Katana', lighting: 'Mwanga', coolWhite: 'Nyeupe Baridi', warm: 'Joto', emergencyRed: 'Nyekundu ya Dharura', nightVision: 'Kuona Usiku' },\n                    weapons: { PISTOL: 'BASTOLA YA KIJANJA', SHOTGUN: 'SHOTGANI', SNIPER: 'SNIPA', KATANA: 'KATANA', GATLING: 'GATLING', LMG: 'LMG', 'AUTO RIFLE': 'BUNDUKI OTO', FLAMETHROWER: 'RUSHASHA MOTO' },\n                    zombies: { Walker: 'Mtembeaji', Runner: 'Mkimbiaji', Tank: 'Tank', Crawler: 'Mtambaji', Berserker: 'Mwendawazimu', Armored: 'Mwenye Ngao', Swarm: 'Kundi', Bloater: 'Aliyevimba', Stalker: 'Mfuatiliaji', Screamer: 'Mpiga Kelele', BRUTE: 'DUBWANA', CHARGER: 'MVAMIZI', BEHEMOTH: 'JITU', INFERNAL: 'WA KUZIMU', OVERLORD: 'BWANA MKUU' }\n                },\n                de: {\n                    app: { title: 'Letzter \u00dcberlebender', titleUpper: 'LETZTER \u00dcBERLEBENDER', titleBreak: 'LETZTER<br>\u00dcBERLEBENDER' },\n                    tutorial: { title: '\u3010TUTORIAL\u3011NEUE WAFFE', desc: 'Du hast eine neue Waffe erhalten! Tippe unten auf das hervorgehobene Symbol oder dr\u00fccke [2], um zu wechseln und fortzufahren.' },\n                    loading: { assets: 'Lade Inhalte...', assetsProgress: 'Lade Inhalte... {pct}%' },\n                    start: { subtitle: 'Verteidige den Bunker gegen endlose Zombiehorden', play: 'SPIELEN' },\n                    hud: { wave: 'WELLE {wave}', waveBoss: 'WELLE {wave} - BOSS: {boss}', waveInitial: 'WELLE 1', score: 'PUNKTE', interact: 'Dr\u00fccke E zum Interagieren', fire: 'FEUER', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} NACHLADEN', seconds: '{value} s' },\n                    gameOver: { title: 'BUNKER DURCHBROCHEN', retry: 'NEU STARTEN' },\n                    stats: { wave: 'Welle', kills: 'Kills', headshots: 'Kopfsch\u00fcsse', score: 'Punkte', accuracy: 'Trefferquote' },\n                    weaponSelect: { title: 'WAFFE W\u00c4HLEN', subtitle: 'W\u00e4hle deine Startwaffe' },\n                    upgrade: { waveCompleteInitial: 'WELLE GESCHAFFT!', waveComplete: 'WELLE {wave} GESCHAFFT!', choose: 'W\u00e4hle ein Upgrade', lifestealName: 'LEBENSRAUB+', lifestealDesc: 'Heilung bei Treffer {from}% -> {to}%', damageName: '{weapon} SCHADEN+', damageDesc: 'Schaden {from} -> {to}', reloadName: '{weapon} NACHLADEN+', reloadDesc: 'Nachladen {from}s -> {to}s', magazineName: '{weapon} MAGAZIN+', magazineDesc: 'Magazin {from} -> {to}', pierceName: '{weapon} DURCHSCHLAG+', pierceDesc: 'Durchschlag {from} -> {to}', fireRateName: '{weapon} FEUERRATE+', fireRateDesc: 'Intervall {from}ms -> {to}ms', speedName: '{weapon} TEMPO+', rangeName: '{weapon} REICHWEITE+', rangeDesc: 'Reichweite {from}m -> {to}m', newWeaponName: 'NEUE WAFFE: {weapon}', slowTimeName: '\u3010SKILL\u3011ZEITDEHNUNG', slowTimeDesc: 'Die Welt verlangsamt sich um das 100-Fache, der Spieler ist unbetroffen. Dauert 5 Sek.', flameRangeName: '[Flammenwerfer] Kraftstoffkompression', flameRangeDesc: 'Erh\u00f6ht Reichweite um +40% (Geschwindigkeit +15%) und Direktschaden um +20% (Stufe {level})', flameStickyName: '[Flammenwerfer] Klebriges Feuer', flameStickyDesc: 'Erh\u00f6ht Branddauer um +2s und Schaden pro Sekunde um +50% (Stufe {level})', flameSpreadName: '[Flammenwerfer] Breite D\u00fcse', flameSpreadDesc: 'Erh\u00f6ht Flammengr\u00f6\u00dfe und Kollisionsbereich um +35% (Stufe {level})', flameEmberName: '[Flammenwerfer] Glut-Wiederaufflammen', flameEmberDesc: 'Brennende Feinde haben eine Wahrscheinlichkeit von 30%, benachbarte Feinde zu entz\u00fcnden', flameLavaName: '[Flammenwerfer] Hochtemperaturlava', flameLavaDesc: 'Flammen ignorieren die R\u00fcstung der Feinde vollst\u00e4ndig und schmelzen 15% der maximalen R\u00fcstung pro Sekunde', flameRangeName: '[Flammenwerfer] Kraftstoffkompression', flameRangeDesc: 'Erh\u00f6ht Reichweite um +40% (Geschwindigkeit +15%) und Direktschaden um +20% (Stufe {level})', flameStickyName: '[Flammenwerfer] Klebriges Feuer', flameStickyDesc: 'Erh\u00f6ht Branddauer um +2s und Schaden pro Sekunde um +50% (Stufe {level})', flameSpreadName: '[Flammenwerfer] Breite D\u00fcse', flameSpreadDesc: 'Erh\u00f6ht Flammengr\u00f6\u00dfe und Kollisionsbereich um +35% (Stufe {level})', flameEmberName: '[Flammenwerfer] Glut-Wiederaufflammen', flameEmberDesc: 'Brennende Feinde haben eine Wahrscheinlichkeit von 30%, benachbarte Feinde zu entz\u00fcnden', flameLavaName: '[Flammenwerfer] Hochtemperaturlava', flameLavaDesc: 'Flammen ignorieren die R\u00fcstung der Feinde vollst\u00e4ndig und schmelzen 15% der maximalen R\u00fcstung pro Sekunde' },\n                    weaponStats: { melee: 'NAHKAMPF', auto: 'AUTO', semi: 'HALB', damage: 'SCH:{value}', range: 'Reichw.:{value}m', magazine: 'Mag:{value}' },\n                    edit: { title: 'EDIT-MODUS', line1: 'Nutze das Einstellungsfeld, um Spielparameter anzupassen.', line2: 'Passe Zombie-Schwierigkeit, Startwelle, Waffen und Licht an.', architecture: 'Architektur: ECS + Web Worker + Object Pool' },\n                    params: { name: 'Letzter \u00dcberlebender', zombieHealth: 'Zombie-Leben', zombieSpeed: 'Zombie-Tempo', startingWave: 'Startwelle', headshotMultiplier: 'Kopfschuss-Multiplikator', katanaSpeed: 'Katana-Tempo', lighting: 'Beleuchtung', coolWhite: 'Kaltes Wei\u00df', warm: 'Warm', emergencyRed: 'Notfallrot', nightVision: 'Nachtsicht' },\n                    weapons: { PISTOL: 'SMART-PISTOLE', SHOTGUN: 'SCHROTFLINTE', SNIPER: 'PR\u00c4ZISIONSGEWEHR', KATANA: 'KATANA', GATLING: 'GATLING', LMG: 'LMG', 'AUTO RIFLE': 'AUTOGEWEHR', FLAMETHROWER: 'FLAMMENWERFER' },\n                    zombies: { Walker: 'Schlurfer', Runner: 'Renner', Tank: 'Tank', Crawler: 'Kriecher', Berserker: 'Berserker', Armored: 'Gepanzerter', Swarm: 'Schwarm', Bloater: 'Aufgedunsener', Stalker: 'Stalker', Screamer: 'Schreier', BRUTE: 'BRUTALO', CHARGER: 'ST\u00dcRMER', BEHEMOTH: 'KOLOSS', INFERNAL: 'INFERNALER', OVERLORD: 'OBERHERR' }\n                },\n                tr: {\n                    app: { title: 'Son Hayatta Kalan', titleUpper: 'SON HAYATTA KALAN', titleBreak: 'SON<br>HAYATTA KALAN' },\n                    tutorial: { title: '\u3010E\u011e\u0130T\u0130M\u3011YEN\u0130 S\u0130LAH', desc: 'Yeni bir silah kazand\u0131n! Ge\u00e7i\u015f yapmak ve devam etmek i\u00e7in a\u015fa\u011f\u0131daki vurgulanan simgeye dokun veya [2]\\'ye bas.' },\n                    loading: { assets: 'Varl\u0131klar y\u00fckleniyor...', assetsProgress: 'Varl\u0131klar y\u00fckleniyor... {pct}%' },\n                    start: { subtitle: 'S\u0131\u011f\u0131na\u011f\u0131 bitmeyen zombi s\u00fcr\u00fclerine kar\u015f\u0131 savun', play: 'OYNA' },\n                    hud: { wave: 'DALGA {wave}', waveBoss: 'DALGA {wave} - PATRON: {boss}', waveInitial: 'DALGA 1', score: 'SKOR', interact: 'E ile etkile\u015f', fire: 'ATE\u015e', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} DOLDUR', seconds: '{value} sn' },\n                    gameOver: { title: 'SI\u011eINAK D\u00dc\u015eT\u00dc', retry: 'TEKRAR DENE' },\n                    stats: { wave: 'Dalga', kills: '\u00d6ld\u00fcrme', headshots: 'Kafa vuru\u015fu', score: 'Skor', accuracy: '\u0130sabet' },\n                    weaponSelect: { title: 'S\u0130LAH SE\u00c7', subtitle: 'Ba\u015flang\u0131\u00e7 silah\u0131n\u0131 se\u00e7' },\n                    upgrade: { waveCompleteInitial: 'DALGA B\u0130TT\u0130!', waveComplete: 'DALGA {wave} B\u0130TT\u0130!', choose: 'Bir y\u00fckseltme se\u00e7', lifestealName: 'CAN \u00c7ALMA+', lifestealDesc: 'Vuru\u015fta iyile\u015fme {from}% -> {to}%', damageName: '{weapon} HASAR+', damageDesc: 'Hasar {from} -> {to}', reloadName: '{weapon} DOLDURMA+', reloadDesc: 'Doldurma {from}sn -> {to}sn', magazineName: '{weapon} \u015eARJ\u00d6R+', magazineDesc: '\u015earj\u00f6r {from} -> {to}', pierceName: '{weapon} DELME+', pierceDesc: 'Delme {from} -> {to}', fireRateName: '{weapon} ATI\u015e HIZI+', fireRateDesc: 'Aral\u0131k {from}ms -> {to}ms', speedName: '{weapon} HIZ+', rangeName: '{weapon} MENZ\u0130L+', rangeDesc: 'Menzil {from}m -> {to}m', newWeaponName: 'YEN\u0130 S\u0130LAH: {weapon}', slowTimeName: '\u3010YETENEK\u3011ZAMAN YAVA\u015eLATMA', slowTimeDesc: 'D\u00fcnya 100 kat yava\u015flar, oyuncu etkilenmez. 5 saniye s\u00fcrer.', flameRangeName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] Yak\u0131t S\u0131k\u0131\u015ft\u0131rma', flameRangeDesc: 'Menzili +%40 (h\u0131z +%15) ve do\u011frudan hasar\u0131 +%20 art\u0131r\u0131r (Seviye {level})', flameStickyName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] Yap\u0131\u015fkan Ate\u015f', flameStickyDesc: 'Yanma s\u00fcresini +2 sn ve saniye ba\u015f\u0131na hasar\u0131 +%50 art\u0131r\u0131r (Seviye {level})', flameSpreadName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] Geni\u015f A\u011f\u0131zl\u0131 Nozul', flameSpreadDesc: 'Alev boyutunu ve \u00e7arp\u0131\u015fma alan\u0131n\u0131 +%35 art\u0131r\u0131r (Seviye {level})', flameEmberName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] K\u00f6z Yenilenmesi', flameEmberDesc: 'Yanan d\u00fc\u015fmanlar\u0131n yak\u0131ndaki d\u00fc\u015fmanlar\u0131 tutu\u015fturma \u015fans\u0131 %30\\'dur', flameLavaName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] Y\u00fcksek S\u0131cakl\u0131kl\u0131 Lav', flameLavaDesc: 'Alevler d\u00fc\u015fman z\u0131rh\u0131n\u0131 tamamen yok sayar ve saniyede %15 maksimum z\u0131rh\u0131 eritir', flameRangeName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] Yak\u0131t S\u0131k\u0131\u015ft\u0131rma', flameRangeDesc: 'Menzili +%40 (h\u0131z +%15) ve do\u011frudan hasar\u0131 +%20 art\u0131r\u0131r (Seviye {level})', flameStickyName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] Yap\u0131\u015fkan Ate\u015f', flameStickyDesc: 'Yanma s\u00fcresini +2 sn ve saniye ba\u015f\u0131na hasar\u0131 +%50 art\u0131r\u0131r (Seviye {level})', flameSpreadName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] Geni\u015f A\u011f\u0131zl\u0131 Nozul', flameSpreadDesc: 'Alev boyutunu ve \u00e7arp\u0131\u015fma alan\u0131n\u0131 +%35 art\u0131r\u0131r (Seviye {level})', flameEmberName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] K\u00f6z Yenilenmesi', flameEmberDesc: 'Yanan d\u00fc\u015fmanlar\u0131n yak\u0131ndaki d\u00fc\u015fmanlar\u0131 tutu\u015fturma \u015fans\u0131 %30\\'dur', flameLavaName: '[Alev P\u00fcsk\u00fcrt\u00fcc\u00fc] Y\u00fcksek S\u0131cakl\u0131kl\u0131 Lav', flameLavaDesc: 'Alevler d\u00fc\u015fman z\u0131rh\u0131n\u0131 tamamen yok sayar ve saniyede %15 maksimum z\u0131rh\u0131 eritir' },\n                    weaponStats: { melee: 'YAKIN', auto: 'OTO', semi: 'YARI', damage: 'HSR:{value}', range: 'Menzil:{value}m', magazine: '\u015earj\u00f6r:{value}' },\n                    edit: { title: 'D\u00dcZENLEME MODU', line1: 'Oyun parametrelerini ayarlamak i\u00e7in ayarlar panelini kullan.', line2: 'Zombi zorlu\u011funu, ba\u015flang\u0131\u00e7 dalgas\u0131n\u0131, silahlar\u0131 ve \u0131\u015f\u0131\u011f\u0131 ayarla.', architecture: 'Mimari: ECS + Web Worker + Object Pool' },\n                    params: { name: 'Son Hayatta Kalan', zombieHealth: 'Zombi Can\u0131', zombieSpeed: 'Zombi H\u0131z\u0131', startingWave: 'Ba\u015flang\u0131\u00e7 Dalgas\u0131', headshotMultiplier: 'Kafa Vuru\u015fu \u00c7arpan\u0131', katanaSpeed: 'Katana H\u0131z\u0131', lighting: 'I\u015f\u0131k', coolWhite: 'So\u011fuk Beyaz', warm: 'S\u0131cak', emergencyRed: 'Acil K\u0131rm\u0131z\u0131', nightVision: 'Gece G\u00f6r\u00fc\u015f\u00fc' },\n                    weapons: { PISTOL: 'AKILLI TABANCA', SHOTGUN: 'POMPALI', SNIPER: 'KESK\u0130N N\u0130\u015eANCI', KATANA: 'KATANA', GATLING: 'GATLING', LMG: 'HAF\u0130F MAK\u0130NEL\u0130', 'AUTO RIFLE': 'OTOMAT\u0130K T\u00dcFEK', FLAMETHROWER: 'ALEV P\u00dcSK\u00dcRT\u00dcC\u00dc' },\n                    zombies: { Walker: 'Y\u00fcr\u00fcyen', Runner: 'Ko\u015fucu', Tank: 'Tank', Crawler: 'S\u00fcr\u00fcnen', Berserker: 'Berserker', Armored: 'Z\u0131rhl\u0131', Swarm: 'S\u00fcr\u00fc', Bloater: '\u015ei\u015fkin', Stalker: 'Takip\u00e7i', Screamer: '\u00c7\u0131\u011fl\u0131k\u00e7\u0131', BRUTE: 'YABAN\u0130', CHARGER: 'SALDIRGAN', BEHEMOTH: 'DEV', INFERNAL: 'CEHENNEML\u0130K', OVERLORD: 'EFEND\u0130' }\n                },\n                ja: {\n                    app: { title: '\u6700\u5f8c\u306e\u751f\u5b58\u8005', titleUpper: '\u6700\u5f8c\u306e\u751f\u5b58\u8005', titleBreak: '\u6700\u5f8c\u306e<br>\u751f\u5b58\u8005' },\n                    tutorial: { title: '\u3010\u30c1\u30e5\u30fc\u30c8\u30ea\u30a2\u30eb\u3011\u65b0\u3057\u3044\u6b66\u5668', desc: '\u65b0\u3057\u3044\u6b66\u5668\u3092\u7372\u5f97\u3057\u307e\u3057\u305f\uff01\u4e0b\u306e\u4e0b\u306e\u30cf\u30a4\u30e9\u30a4\u30c8\u3055\u308c\u305f\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3059\u308b\u304b\u3001[2]\u30ad\u30fc\u3092\u62bc\u3057\u3066\u5207\u308a\u66ff\u3048\u3066\u7d9a\u3051\u3066\u304f\u3060\u3055\u3044\u3002' },\n                    loading: { assets: '\u30a2\u30bb\u30c3\u30c8\u3092\u8aad\u307f\u8fbc\u307f\u4e2d...', assetsProgress: '\u30a2\u30bb\u30c3\u30c8\u3092\u8aad\u307f\u8fbc\u307f\u4e2d... {pct}%' },\n                    start: { subtitle: '\u7d42\u308f\u308a\u306a\u304d\u30be\u30f3\u30d3\u306e\u7fa4\u308c\u304b\u3089\u30d0\u30f3\u30ab\u30fc\u3092\u5b88\u308c', play: '\u30d7\u30ec\u30a4' },\n                    hud: { wave: '\u30a6\u30a7\u30fc\u30d6 {wave}', waveBoss: '\u30a6\u30a7\u30fc\u30d6 {wave} - \u30dc\u30b9: {boss}', waveInitial: '\u30a6\u30a7\u30fc\u30d6 1', score: '\u30b9\u30b3\u30a2', interact: 'E\u3067\u8abf\u3079\u308b', fire: '\u6483\u3064', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} \u30ea\u30ed\u30fc\u30c9', seconds: '{value}\u79d2' },\n                    gameOver: { title: '\u30d0\u30f3\u30ab\u30fc\u9665\u843d', retry: '\u518d\u6311\u6226' },\n                    stats: { wave: '\u30a6\u30a7\u30fc\u30d6', kills: '\u6483\u7834', headshots: '\u30d8\u30c3\u30c9\u30b7\u30e7\u30c3\u30c8', score: '\u30b9\u30b3\u30a2', accuracy: '\u547d\u4e2d\u7387' },\n                    weaponSelect: { title: '\u6b66\u5668\u3092\u9078\u629e', subtitle: '\u6700\u521d\u306e\u6b66\u5668\u3092\u9078\u3093\u3067\u304f\u3060\u3055\u3044' },\n                    upgrade: { waveCompleteInitial: '\u30a6\u30a7\u30fc\u30d6\u5b8c\u4e86!', waveComplete: '\u30a6\u30a7\u30fc\u30d6 {wave} \u5b8c\u4e86!', choose: '\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3092\u9078\u629e', lifestealName: '\u5438\u8840+', lifestealDesc: '\u547d\u4e2d\u6642\u56de\u5fa9 {from}% -> {to}%', damageName: '{weapon} \u5a01\u529b+', damageDesc: '\u5a01\u529b {from} -> {to}', reloadName: '{weapon} \u30ea\u30ed\u30fc\u30c9+', reloadDesc: '\u30ea\u30ed\u30fc\u30c9 {from}\u79d2 -> {to}\u79d2', magazineName: '{weapon} \u5f3e\u5009+', magazineDesc: '\u5f3e\u5009 {from} -> {to}', pierceName: '{weapon} \u8cab\u901a+', pierceDesc: '\u8cab\u901a {from} -> {to}', fireRateName: '{weapon} \u9023\u5c04+', fireRateDesc: '\u9593\u9694 {from}\u30df\u30ea\u79d2 -> {to}\u30df\u30ea\u79d2', speedName: '{weapon} \u901f\u5ea6+', rangeName: '{weapon} \u5c04\u7a0b+', rangeDesc: '\u5c04\u7a0b {from}m -> {to}m', newWeaponName: '\u65b0\u6b66\u5668: {weapon}', slowTimeName: '\u3010\u30b9\u30ad\u30eb\u3011\u6642\u9593\u6e1b\u901f', slowTimeDesc: '\u4e16\u754c\u306e\u6642\u9593\u304c100\u500d\u9045\u304f\u306a\u308a\u307e\u3059\u304c\u3001\u30d7\u30ec\u30a4\u30e4\u30fc\u306f\u5f71\u97ff\u3092\u53d7\u3051\u307e\u305b\u3093\u30025\u79d2\u9593\u6301\u7d9a\u3002', flameRangeName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u71c3\u6599\u5727\u7e2e', flameRangeDesc: '\u5c04\u7a0b\u304c40%\u5411\u4e0a\uff08\u653e\u5c04\u901f\u5ea6+15%\uff09\u3001\u76f4\u63a5\u30c0\u30e1\u30fc\u30b8\u304c20%\u5897\u52a0\u3057\u307e\u3059\uff08\u73fe\u5728\u306e\u30ec\u30d9\u30eb {level}\uff09', flameStickyName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u7c98\u6027\u71b1\u708e', flameStickyDesc: '\u71c3\u713c\u6642\u9593\u304c2\u79d2\u5897\u52a0\u3057\u3001\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u304c50%\u5897\u52a0\u3057\u307e\u3059\uff08\u73fe\u5728\u306e\u30ec\u30d9\u30eb {level}\uff09', flameSpreadName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u30ef\u30a4\u30c9\u30ce\u30ba\u30eb', flameSpreadDesc: '\u708e\u306e\u30b5\u30a4\u30ba\u3068\u885d\u7a81\u5224\u5b9a\u7bc4\u56f2\u304c35%\u5897\u52a0\u3057\u307e\u3059\uff08\u73fe\u5728\u306e\u30ec\u30d9\u30eb {level}\uff09', flameEmberName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u6b8b\u308a\u706b\u306e\u518d\u71c3', flameEmberDesc: '\u708e\u4e0a\u72b6\u614b\u306e\u6575\u304c\u30c0\u30e1\u30fc\u30b8\u3092\u53d7\u3051\u305f\u969b\u300130%\u306e\u78ba\u7387\u3067\u5468\u56f2\u306e\u6575\u306b\u5f15\u706b\u3057\u307e\u3059', flameLavaName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u9ad8\u6e29\u6eb6\u5ca9', flameLavaDesc: '\u708e\u304c\u6575\u306e\u88c5\u7532\u3092\u5b8c\u5168\u306b\u7121\u8996\u3057\u3001\u6bce\u79d2\u6700\u5927\u88c5\u7532\u306e15%\u3092\u878d\u89e3\u3057\u307e\u3059', flameRangeName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u71c3\u6599\u5727\u7e2e', flameRangeDesc: '\u5c04\u7a0b\u304c40%\u5411\u4e0a\uff08\u653e\u5c04\u901f\u5ea6+15%\uff09\u3001\u76f4\u63a5\u30c0\u30e1\u30fc\u30b8\u304c20%\u5897\u52a0\u3057\u307e\u3059\uff08\u73fe\u5728\u306e\u30ec\u30d9\u30eb {level}\uff09', flameStickyName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u7c98\u6027\u71b1\u708e', flameStickyDesc: '\u71c3\u713c\u6642\u9593\u304c2\u79d2\u5897\u52a0\u3057\u3001\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u304c50%\u5897\u52a0\u3057\u307e\u3059\uff08\u73fe\u5728\u306e\u30ec\u30d9\u30eb {level}\uff09', flameSpreadName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u30ef\u30a4\u30c9\u30ce\u30ba\u30eb', flameSpreadDesc: '\u708e\u306e\u30b5\u30a4\u30ba\u3068\u885d\u7a81\u5224\u5b9a\u7bc4\u56f2\u304c35%\u5897\u52a0\u3057\u307e\u3059\uff08\u73fe\u5728\u306e\u30ec\u30d9\u30eb {level}\uff09', flameEmberName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u6b8b\u308a\u706b\u306e\u518d\u71c3', flameEmberDesc: '\u708e\u4e0a\u72b6\u614b\u306e\u6575\u304c\u30c0\u30e1\u30fc\u30b8\u3092\u53d7\u3051\u305f\u969b\u300130%\u306e\u78ba\u7387\u3067\u5468\u56f2\u306e\u6575\u306b\u5f15\u706b\u3057\u307e\u3059', flameLavaName: '\u3010\u706b\u708e\u653e\u5c04\u5668\u3011\u9ad8\u6e29\u6eb6\u5ca9', flameLavaDesc: '\u708e\u304c\u6575\u306e\u88c5\u7532\u3092\u5b8c\u5168\u306b\u7121\u8996\u3057\u3001\u6bce\u79d2\u6700\u5927\u88c5\u7532\u306e15%\u3092\u878d\u89e3\u3057\u307e\u3059' },\n                    weaponStats: { melee: '\u8fd1\u63a5', auto: '\u30aa\u30fc\u30c8', semi: '\u30bb\u30df', damage: '\u5a01\u529b:{value}', range: '\u5c04\u7a0b:{value}m', magazine: '\u5f3e\u5009:{value}' },\n                    edit: { title: '\u7de8\u96c6\u30e2\u30fc\u30c9', line1: '\u8a2d\u5b9a\u30d1\u30cd\u30eb\u3067\u30b2\u30fc\u30e0\u306e\u30d1\u30e9\u30e1\u30fc\u30bf\u3092\u8abf\u6574\u3067\u304d\u307e\u3059\u3002', line2: '\u30be\u30f3\u30d3\u306e\u5f37\u3055\u3001\u958b\u59cb\u30a6\u30a7\u30fc\u30d6\u3001\u6b66\u5668\u3001\u7167\u660e\u3092\u5909\u66f4\u3067\u304d\u307e\u3059\u3002', architecture: '\u69cb\u6210: ECS + Web Worker + Object Pool' },\n                    params: { name: '\u6700\u5f8c\u306e\u751f\u5b58\u8005', zombieHealth: '\u30be\u30f3\u30d3\u4f53\u529b', zombieSpeed: '\u30be\u30f3\u30d3\u901f\u5ea6', startingWave: '\u958b\u59cb\u30a6\u30a7\u30fc\u30d6', headshotMultiplier: '\u30d8\u30c3\u30c9\u30b7\u30e7\u30c3\u30c8\u500d\u7387', katanaSpeed: '\u5200\u306e\u901f\u5ea6', lighting: '\u7167\u660e', coolWhite: '\u30af\u30fc\u30eb\u30db\u30ef\u30a4\u30c8', warm: '\u30a6\u30a9\u30fc\u30e0', emergencyRed: '\u975e\u5e38\u706f\u30ec\u30c3\u30c9', nightVision: '\u30ca\u30a4\u30c8\u30d3\u30b8\u30e7\u30f3' },\n                    weapons: { PISTOL: '\u30b9\u30de\u30fc\u30c8\u30d4\u30b9\u30c8\u30eb', SHOTGUN: '\u30b7\u30e7\u30c3\u30c8\u30ac\u30f3', SNIPER: '\u30b9\u30ca\u30a4\u30d1\u30fc', KATANA: '\u5200', GATLING: '\u30ac\u30c8\u30ea\u30f3\u30b0', LMG: 'LMG', 'AUTO RIFLE': '\u81ea\u52d5\u5c0f\u9283', FLAMETHROWER: '\u706b\u708e\u653e\u5c04\u5668' },\n                    zombies: { Walker: '\u30a6\u30a9\u30fc\u30ab\u30fc', Runner: '\u30e9\u30f3\u30ca\u30fc', Tank: '\u30bf\u30f3\u30af', Crawler: '\u30af\u30ed\u30fc\u30e9\u30fc', Berserker: '\u30d0\u30fc\u30b5\u30fc\u30ab\u30fc', Armored: '\u30a2\u30fc\u30de\u30fc\u30c9', Swarm: '\u30b9\u30a6\u30a9\u30fc\u30e0', Bloater: '\u30d6\u30ed\u30a6\u30bf\u30fc', Stalker: '\u30b9\u30c8\u30fc\u30ab\u30fc', Screamer: '\u30b9\u30af\u30ea\u30fc\u30de\u30fc', BRUTE: '\u30d6\u30eb\u30fc\u30c8', CHARGER: '\u30c1\u30e3\u30fc\u30b8\u30e3\u30fc', BEHEMOTH: '\u30d9\u30d2\u30fc\u30e2\u30b9', INFERNAL: '\u30a4\u30f3\u30d5\u30a1\u30fc\u30ca\u30eb', OVERLORD: '\u30aa\u30fc\u30d0\u30fc\u30ed\u30fc\u30c9' }\n                },\n                te: {\n                    app: { title: '\u0c1a\u0c3f\u0c35\u0c30\u0c3f \u0c2c\u0c24\u0c3f\u0c15\u0c3f\u0c28 \u0c35\u0c3e\u0c21\u0c41', titleUpper: '\u0c1a\u0c3f\u0c35\u0c30\u0c3f \u0c2c\u0c24\u0c3f\u0c15\u0c3f\u0c28 \u0c35\u0c3e\u0c21\u0c41', titleBreak: '\u0c1a\u0c3f\u0c35\u0c30\u0c3f<br>\u0c2c\u0c24\u0c3f\u0c15\u0c3f\u0c28 \u0c35\u0c3e\u0c21\u0c41' },\n                    tutorial: { title: '\u3010\u0c1f\u0c4d\u0c2f\u0c41\u0c1f\u0c4b\u0c30\u0c3f\u0c2f\u0c32\u0c4d\u3011\u0c15\u0c4a\u0c24\u0c4d\u0c24 \u0c06\u0c2f\u0c41\u0c27\u0c02', desc: '\u0c2e\u0c40\u0c30\u0c41 \u0c15\u0c4a\u0c24\u0c4d\u0c24 \u0c06\u0c2f\u0c41\u0c27\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c2a\u0c4a\u0c02\u0c26\u0c3e\u0c30\u0c41! \u0c2e\u0c3e\u0c30\u0c21\u0c3e\u0c28\u0c3f\u0c15\u0c3f \u0c2e\u0c30\u0c3f\u0c2f\u0c41 \u0c15\u0c4a\u0c28\u0c38\u0c3e\u0c17\u0c3f\u0c02\u0c1a\u0c21\u0c3e\u0c28\u0c3f\u0c15\u0c3f \u0c15\u0c4d\u0c30\u0c3f\u0c02\u0c26\u0c3f \u0c39\u0c48\u0c32\u0c48\u0c1f\u0c4d \u0c1a\u0c47\u0c38\u0c3f\u0c28 \u0c1a\u0c3f\u0c39\u0c4d\u0c28\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c28\u0c4a\u0c15\u0c4d\u0c15\u0c02\u0c21\u0c3f \u0c32\u0c47\u0c26\u0c3e [2] \u0c28\u0c4a\u0c15\u0c4d\u0c15\u0c02\u0c21\u0c3f.' },\n                    loading: { assets: '\u0c05\u0c38\u0c46\u0c1f\u0c4d\u0c32\u0c41 \u0c32\u0c4b\u0c21\u0c4d \u0c05\u0c35\u0c41\u0c24\u0c41\u0c28\u0c4d\u0c28\u0c3e\u0c2f\u0c3f...', assetsProgress: '\u0c05\u0c38\u0c46\u0c1f\u0c4d\u0c32\u0c41 \u0c32\u0c4b\u0c21\u0c4d \u0c05\u0c35\u0c41\u0c24\u0c41\u0c28\u0c4d\u0c28\u0c3e\u0c2f\u0c3f... {pct}%' },\n                    start: { subtitle: '\u0c05\u0c02\u0c24\u0c41\u0c32\u0c47\u0c28\u0c3f \u0c1c\u0c3e\u0c02\u0c2c\u0c40 \u0c17\u0c41\u0c02\u0c2a\u0c41\u0c32 \u0c28\u0c41\u0c02\u0c1a\u0c3f \u0c2c\u0c02\u0c15\u0c30\u0c4d\u200c\u0c28\u0c41 \u0c15\u0c3e\u0c2a\u0c3e\u0c21\u0c41', play: '\u0c06\u0c21\u0c41' },\n                    hud: { wave: '\u0c35\u0c47\u0c35\u0c4d {wave}', waveBoss: '\u0c35\u0c47\u0c35\u0c4d {wave} - \u0c2c\u0c3e\u0c38\u0c4d: {boss}', waveInitial: '\u0c35\u0c47\u0c35\u0c4d 1', score: '\u0c38\u0c4d\u0c15\u0c4b\u0c30\u0c4d', interact: '\u0c1a\u0c30\u0c4d\u0c2f \u0c15\u0c4b\u0c38\u0c02 E \u0c28\u0c4a\u0c15\u0c4d\u0c15\u0c02\u0c21\u0c3f', fire: '\u0c15\u0c3e\u0c32\u0c4d\u0c1a\u0c41', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} \u0c30\u0c40\u0c32\u0c4b\u0c21\u0c4d', seconds: '{value}\u0c38\u0c46' },\n                    gameOver: { title: '\u0c2c\u0c02\u0c15\u0c30\u0c4d \u0c1a\u0c40\u0c32\u0c3f\u0c2a\u0c4b\u0c2f\u0c3f\u0c02\u0c26\u0c3f', retry: '\u0c2e\u0c33\u0c4d\u0c32\u0c40 \u0c2a\u0c4d\u0c30\u0c2f\u0c24\u0c4d\u0c28\u0c3f\u0c02\u0c1a\u0c41' },\n                    stats: { wave: '\u0c35\u0c47\u0c35\u0c4d', kills: '\u0c39\u0c24\u0c3e\u0c32\u0c41', headshots: '\u0c39\u0c46\u0c21\u0c4d\u200c\u0c37\u0c3e\u0c1f\u0c4d\u0c32\u0c41', score: '\u0c38\u0c4d\u0c15\u0c4b\u0c30\u0c4d', accuracy: '\u0c15\u0c1a\u0c4d\u0c1a\u0c3f\u0c24\u0c24\u0c4d\u0c35\u0c02' },\n                    weaponSelect: { title: '\u0c06\u0c2f\u0c41\u0c27\u0c02 \u0c0e\u0c02\u0c1a\u0c41\u0c15\u0c4b\u0c02\u0c21\u0c3f', subtitle: '\u0c2a\u0c4d\u0c30\u0c3e\u0c30\u0c02\u0c2d \u0c06\u0c2f\u0c41\u0c27\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c0e\u0c02\u0c1a\u0c41\u0c15\u0c4b\u0c02\u0c21\u0c3f' },\n                    upgrade: { waveCompleteInitial: '\u0c35\u0c47\u0c35\u0c4d \u0c2a\u0c42\u0c30\u0c4d\u0c24\u0c48\u0c02\u0c26\u0c3f!', waveComplete: '\u0c35\u0c47\u0c35\u0c4d {wave} \u0c2a\u0c42\u0c30\u0c4d\u0c24\u0c48\u0c02\u0c26\u0c3f!', choose: '\u0c12\u0c15 \u0c05\u0c2a\u0c4d\\u200c\u0c17\u0c4d\u0c30\u0c47\u0688 \u0c0e\u0c02\u0c1a\u0c41\u0c15\u0c4b\u0c02\u0c21\u0c3f', lifestealName: '\u0c2a\u0c4d\u0c30\u0c3e\u0c23 \u0c26\u0c4b\u0c2a\u0c3f\u0c21\u0c3f+', lifestealDesc: '\u0c24\u0c3e\u0c15\u0c3f\u0c28\u0c2a\u0c4d\u0c2a\u0c41\u0c21\u0c41 \u0c1a\u0c3f\u0c15\u0c3f\u0c24\u0c4d\u0c38 {from}% -> {to}%', damageName: '{weapon} \u0c28\u0c37\u0c4d\u0c1f\u0c02+', damageDesc: '\u0c28\u0c37\u0c4d\u0c1f\u0c02 {from} -> {to}', reloadName: '{weapon} \u0c30\u0c40\u0c32\u0c4b\u0c21\u0c4d+', reloadDesc: '\u0c30\u0c40\u0c32\u0c4b\u0c21\u0c4d {from}\u0c38\u0c46 -> {to}\u0c38\u0c46', magazineName: '{weapon} \u0c2e\u0c3e\u0c17\u0c4d+', magazineDesc: '\u0c2e\u0c4d\u0c2f\u0c3e\u0c17\u0c1c\u0c48\u589e {from} -> {to}', pierceName: '{weapon} \u0c1a\u0c40\u0c32\u0c4d\u0c1a\u0c41+', pierceDesc: '\u0c1a\u0c40\u0c32\u0c4d\u0c1a\u0c47 \u0c36\u0c15\u0c4d\u0c24\u0c3f {from} -> {to}', fireRateName: '{weapon} \u0c2b\u0c48\u0c30\u0c4d \u0c30\u0c47\u0c1f\u0c4d+', fireRateDesc: '\u0c35\u0c3f\u0c30\u0c3e\u0c2e\u0c02 {from}\u0c2e\u0c3f.\u0c38\u0c46 -> {to}\u0c2e\u0c3f.\u0c38\u0c46', speedName: '{weapon} \u0c35\u0c47\u0c17\u0c02+', rangeName: '{weapon} \u0c2a\u0c30\u0c3f\u0c27\u0c3f+', rangeDesc: '\u0c2a\u0c30\u0c3f\u0c27\u0c3f {from}\u0c2e\u0c40 -> {to}\u0c2e\u0c40', newWeaponName: '\u0c15\u0c4a\u0c24\u0c4d\u0c24 \u0c06\u0c2f\u0c41\u0c27\u0c02: {weapon}', slowTimeName: '\u3010\u0c28\u0c48\u0c2a\u0c41\u0c23\u0c4d\u0c2f\u0c02\u3011\u0c38\u0c2e\u0c2f\u0c02 \u0c28\u0c46\u0c2e\u0c4d\u0c2e\u0c26\u0c3f\u0c02\u0c1a\u0c21\u0c02', slowTimeDesc: '\u0c2a\u0c4d\u0c30\u0c2a\u0c02\u0c1a\u0c02 100 \u0c30\u0c46\u0c1f\u0c4d\u0c32\u0c41 \u0c28\u0c46\u0c2e\u0c4d\u0c2e\u0c26\u0c3f\u0c38\u0c4d\u0c24\u0c41\u0c02\u0c26\u0c3f, \u0c06\u0c1f\u0c17\u0c3e\u0c21\u0c3f\u0c15\u0c3f \u0c0e\u0c1f\u0c41\u0c35\u0c02\u0c1f\u0c3f \u0c2a\u0c4d\u0c30\u0c2d\u0c3e\u0c35\u0c02 \u0c09\u0c02\u0c21\u0c26\u0c41. 5 \u0c38\u0c46\u0c15\u0c28\u0c4d\u0c32\u0c41 \u0c09\u0c02\u0c1f\u0c41\u0c02\u0c26\u0c3f\u0964', flameRangeName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c07\u0c02\u0c27\u0c28 \u0c15\u0c41\u0c26\u0c3f\u0c02\u0c2a\u0c41', flameRangeDesc: '\u0c2a\u0c30\u0c3f\u0c27\u0c3f\u0c28\u0c3f +40% (\u0c35\u0c47\u0c17\u0c02 +15%) \u0c2e\u0c30\u0c3f\u0c2f\u0c41 \u0c2a\u0c4d\u0c30\u0c24\u0c4d\u0c2f\u0c15\u0c4d\u0c37 \u0c28\u0c37\u0c4d\u0c1f\u0c3e\u0c28\u0c4d\u0c28\u0c3f +20% \u0c2a\u0c46\u0c02\u0c1a\u0c41\u0c24\u0c41\u0c02\u0c26\u0c3f (\u0c38\u0c4d\u0c25\u0c3e\u0c2f\u0c3f {level})', flameStickyName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c1c\u0c3f\u0c17\u0c1f \u0c2e\u0c02\u0c1f', flameStickyDesc: '\u0c2e\u0c02\u0c21\u0c47 \u0c35\u0c4d\u0c2f\u0c35\u0c27\u0c3f\u0c28\u0c3f +2\u0c38\u0c46 \u0c2e\u0c30\u0c3f\u0c2f\u0c41 \u0c2a\u0c4d\u0c30\u0c24\u0c3f \u0c38\u0c46\u0c15\u0c28\u0c41 \u0c28\u0c37\u0c4d\u0c1f\u0c3e\u0c28\u0c4d\u0c28\u0c3f +50% \u0c2a\u0c46\u0c02\u0c1a\u0c41\u0c24\u0c41\u0c02\u0c26\u0c3f (\u0c38\u0c4d\u0c25\u0c3e\u0c2f\u0c3f {level})', flameSpreadName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c35\u0c46\u0c21\u0c32\u0c4d\u0c2a\u0c3e\u0c1f\u0c3f \u0c28\u0c4b\u0c1c\u0c3f\u0c32\u0c4d', flameSpreadDesc: '\u0c2e\u0c02\u0c1f \u0c2a\u0c30\u0c3f\u0c2e\u0c3e\u0c23\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c2e\u0c30\u0c3f\u0c2f\u0c41 \u0c18\u0c30\u0c4d\u0c37\u0c23 \u0c2a\u0c4d\u0c30\u0c3e\u0c02\u0c24\u0c3e\u0c28\u0c4d\u0c28\u0c3f +35% \u0c2a\u0c46\u0c02\u0c1a\u0c41\u0c24\u0c41\u0c02\u0c26\u0c3f (\u0c38\u0c4d\u0c25\u0c3e\u0c2f\u0c3f {level})', flameEmberName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c28\u0c3f\u0c2a\u0c4d\u0c2a\u0c41\u0c15\u0c23\u0c3e\u0c32 \u0c2a\u0c41\u0c28\u0c30\u0c41\u0c1c\u0c4d\u0c1c\u0c40\u0c35\u0c28\u0c02', flameEmberDesc: '\u0c2e\u0c02\u0c21\u0c41\u0c24\u0c41\u0c28\u0c4d\u0c28 \u0c36\u0c24\u0c4d\u0c30\u0c41\u0c35\u0c41\u0c32\u0c41 \u0c2a\u0c4a\u0c30\u0c41\u0c17\u0c41\u0c28 \u0c09\u0c28\u0c4d\u0c28 \u0c36\u0c24\u0c4d\u0c30\u0c41\u0c35\u0c41\u0c32\u0c28\u0c41 \u0c2e\u0c02\u0c21\u0c3f\u0c02\u0c1a\u0c21\u0c3e\u0c28\u0c3f\u0c15\u0c3f 30% \u0c05\u0c35\u0c15\u0c3e\u0c36\u0c02 \u0c09\u0c02\u0c26\u0c3f', flameLavaName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c05\u0c27\u0c3f\u0c15-\u0c09\u0c37\u0c4d\u0c23\u0c4b\u0c17\u0c4d\u0c30\u0c24 \u0c32\u0c3e\u0c35\u0c3e', flameLavaDesc: '\u0c2e\u0c02\u0c1f\u0c32\u0c41 \u0c36\u0c24\u0c4d\u0c30\u0c41\u0c35\u0c41 \u0c15\u0c35\u0c1a\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c2a\u0c42\u0c30\u0c4d\u0c24\u0c3f\u0c17\u0c3e \u0c35\u0c3f\u0c38\u0c4d\u0c2e\u0c30\u0c3f\u0c38\u0c4d\u0c24\u0c3e\u0c2f\u0c3f \u0c2e\u0c30\u0c3f\u0c2f\u0c41 \u0c38\u0c46\u0c15\u0c28\u0c41\u0c15\u0c41 15% \u0c17\u0c30\u0c3f\u0c37\u0c4d\u0c1f \u0c15\u0c35\u0c1a\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c15\u0c30\u0c3f\u0c17\u0c3f\u0c38\u0c4d\u0c24\u0c3e\u0c2f\u0c3f', flameRangeName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c07\u0c02\u0c27\u0c28 \u0c15\u0c41\u0c26\u0c3f\u0c02\u0c2a\u0c41', flameRangeDesc: '\u0c2a\u0c30\u0c3f\u0c27\u0c3f\u0c28\u0c3f +40% (\u0c35\u0c47\u0c17\u0c02 +15%) \u0c2e\u0c30\u0c3f\u0c2f\u0c41 \u0c2a\u0c4d\u0c30\u0c24\u0c4d\u0c2f\u0c15\u0c4d\u0c37 \u0c28\u0c37\u0c4d\u0c1f\u0c3e\u0c28\u0c4d\u0c28\u0c3f +20% \u0c2a\u0c46\u0c02\u0c1a\u0c41\u0c24\u0c41\u0c02\u0c26\u0c3f (\u0c38\u0c4d\u0c25\u0c3e\u0c2f\u0c3f {level})', flameStickyName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c1c\u0c3f\u0c17\u0c1f \u0c2e\u0c02\u0c1f', flameStickyDesc: '\u0c2e\u0c02\u0c21\u0c47 \u0c35\u0c4d\u0c2f\u0c35\u0c27\u0c3f\u0c28\u0c3f +2\u0c38\u0c46 \u0c2e\u0c30\u0c3f\u0c2f\u0c41 \u0c2a\u0c4d\u0c30\u0c24\u0c3f \u0c38\u0c46\u0c15\u0c28\u0c41 \u0c28\u0c37\u0c4d\u0c1f\u0c3e\u0c28\u0c4d\u0c28\u0c3f +50% \u0c2a\u0c46\u0c02\u0c1a\u0c41\u0c24\u0c41\u0c02\u0c26\u0c3f (\u0c38\u0c4d\u0c25\u0c3e\u0c2f\u0c3f {level})', flameSpreadName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c35\u0c46\u0c21\u0c32\u0c4d\u0c2a\u0c3e\u0c1f\u0c3f \u0c28\u0c4b\u0c1c\u0c3f\u0c32\u0c4d', flameSpreadDesc: '\u0c2e\u0c02\u0c1f \u0c2a\u0c30\u0c3f\u0c2e\u0c3e\u0c23\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c2e\u0c30\u0c3f\u0c2f\u0c41 \u0c18\u0c30\u0c4d\u0c37\u0c23 \u0c2a\u0c4d\u0c30\u0c3e\u0c02\u0c24\u0c3e\u0c28\u0c4d\u0c28\u0c3f +35% \u0c2a\u0c46\u0c02\u0c1a\u0c41\u0c24\u0c41\u0c02\u0c26\u0c3f (\u0c38\u0c4d\u0c25\u0c3e\u0c2f\u0c3f {level})', flameEmberName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c28\u0c3f\u0c2a\u0c4d\u0c2a\u0c41\u0c15\u0c23\u0c3e\u0c32 \u0c2a\u0c41\u0c28\u0c30\u0c41\u0c1c\u0c4d\u0c1c\u0c40\u0c35\u0c28\u0c02', flameEmberDesc: '\u0c2e\u0c02\u0c21\u0c41\u0c24\u0c41\u0c28\u0c4d\u0c28 \u0c36\u0c24\u0c4d\u0c30\u0c41\u0c35\u0c41\u0c32\u0c41 \u0c2a\u0c4a\u0c30\u0c41\u0c17\u0c41\u0c28 \u0c09\u0c28\u0c4d\u0c28 \u0c36\u0c24\u0c4d\u0c30\u0c41\u0c35\u0c41\u0c32\u0c28\u0c41 \u0c2e\u0c02\u0c21\u0c3f\u0c02\u0c1a\u0c21\u0c3e\u0c28\u0c3f\u0c15\u0c3f 30% \u0c05\u0c35\u0c15\u0c3e\u0c36\u0c02 \u0c09\u0c02\u0c26\u0c3f', flameLavaName: '[\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d] \u0c05\u0c27\u0c3f\u0c15-\u0c09\u0c37\u0c4d\u0c23\u0c4b\u0c17\u0c4d\u0c30\u0c24 \u0c32\u0c3e\u0c35\u0c3e', flameLavaDesc: '\u0c2e\u0c02\u0c1f\u0c32\u0c41 \u0c36\u0c24\u0c4d\u0c30\u0c41\u0c35\u0c41 \u0c15\u0c35\u0c1a\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c2a\u0c42\u0c30\u0c4d\u0c24\u0c3f\u0c17\u0c3e \u0c35\u0c3f\u0c38\u0c4d\u0c2e\u0c30\u0c3f\u0c38\u0c4d\u0c24\u0c3e\u0c2f\u0c3f \u0c2e\u0c30\u0c3f\u0c2f\u0c41 \u0c38\u0c46\u0c15\u0c28\u0c41\u0c15\u0c41 15% \u0c17\u0c30\u0c3f\u0c37\u0c4d\u0c1f \u0c15\u0c35\u0c1a\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c15\u0c30\u0c3f\u0c17\u0c3f\u0c38\u0c4d\u0c24\u0c3e\u0c2f\u0c3f' },\n                    weaponStats: { melee: '\u0c38\u0c2e\u0c40\u0c2a', auto: '\u0c06\u0c1f\u0c4b', semi: '\u0c38\u0c46\u0c2e\u0c40', damage: '\u0c28\u0c37\u0c4d\u0c1f\u0c02:{value}', range: '\u0c2a\u0c30\u0c3f\u0c27\u0c3f:{value}\u0c2e\u0c40', magazine: '\u0c2e\u0c3e\u0c17\u0c4d:{value}' },\n                    edit: { title: '\u0c0e\u0c21\u0c3f\u0c1f\u0c4d \u0c2e\u0c4b\u0c21\u0c4d', line1: '\u0c17\u0c47\u0c2e\u0c4d \u0c2a\u0c30\u0c3e\u0c2e\u0c3f\u0c24\u0c41\u0c32\u0c28\u0c41 \u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c21\u0c3e\u0c28\u0c3f\u0c15\u0c3f \u0c38\u0c46\u0c1f\u0c4d\u0c1f\u0c3f\u0c02\u0c17\u0c4d\u0c38\u0c4d \u0c2a\u0c4d\u0c2f\u0c3e\u0c28\u0c46\u0c32\u0c4d \u0c35\u0c3e\u0c21\u0c02\u0c21\u0c3f.', line2: '\u0c1c\u0c3e\u0c02\u0c2c\u0c40 \u0c15\u0c37\u0c4d\u0c1f\u0c02, \u0c2a\u0c4d\u0c30\u0c3e\u0c30\u0c02\u0c2d \u0c35\u0c47\u0c35\u0c4d, \u0c06\u0c2f\u0c41\u0c27\u0c3e\u0c32\u0c41, \u0c32\u0c48\u0c1f\u0c3f\u0c02\u0c17\u0c4d \u0c38\u0c30\u0c4d\u0c26\u0c02\u0c21\u0c3f.', architecture: '\u0c28\u0c3f\u0c30\u0c4d\u0c2e\u0c3e\u0c23\u0c02: ECS + Web Worker + Object Pool' },\n                    params: { name: '\u0c1a\u0c3f\u0c35\u0c30\u0c3f \u0c2c\u0c24\u0c3f\u0c15\u0c3f\u0c28 \u0c35\u0c3e\u0c21\u0c41', zombieHealth: '\u0c1c\u0c3e\u0c02\u0c2c\u0c40 \u0c06\u0c30\u0c4b\u0c17\u0c4d\u0c2f\u0c02', zombieSpeed: '\u0c1c\u0c3e\u0c02\u0c2c\u0c40 \u0c35\u0c47\u0c17\u0c02', startingWave: '\u0c2a\u0c4d\u0c30\u0c3e\u0c30\u0c02\u0c2d \u0c35\u0c47\u0c35\u0c4d', headshotMultiplier: '\u0c39\u0c46\u0c21\u0c4d\u200c\u0c37\u0c3e\u0c1f\u0c4d \u0c17\u0c41\u0c23\u0c15\u0c02', katanaSpeed: '\u0c15\u0c1f\u0c3e\u0c28\u0c3e \u0c35\u0c47\u0c17\u0c02', lighting: '\u0c32\u0c48\u0c1f\u0c3f\u0c02\u0c17\u0c4d', coolWhite: '\u0c1a\u0c32\u0c4d\u0c32\u0c28\u0c3f \u0c24\u0c46\u0c32\u0c41\u0c2a\u0c41', warm: '\u0c35\u0c46\u0c1a\u0c4d\u0c1a\u0c17\u0c3e', emergencyRed: '\u0c05\u0c24\u0c4d\u0c2f\u0c35\u0c38\u0c30 \u0c0e\u0c30\u0c41\u0c2a\u0c41', nightVision: '\u0c28\u0c48\u0c1f\u0c4d \u0c35\u0c3f\u0c1c\u0c28\u0c4d' },\n                    weapons: { PISTOL: '\u0c38\u0c4d\u0c2e\u0c3e\u0c30\u0c4d\u0c1f\u0c4d \u0c2a\u0c3f\u0c38\u0c4d\u0c1f\u0c32\u0c4d', SHOTGUN: '\u0c37\u0c3e\u0c1f\u0c4d\u200c\u0c17\u0c28\u0c4d', SNIPER: '\u0c38\u0c4d\u0c28\u0c48\u0c2a\u0c30\u0c4d', KATANA: '\u0c15\u0c1f\u0c3e\u0c28\u0c3e', GATLING: '\u0c17\u0c3e\u0c1f\u0c4d\u0c32\u0c3f\u0c02\u0c17\u0c4d', LMG: '\u0c0e\u0c32\u0c4d\u200c\u0c0e\u0c2e\u0c4d\u200c\u0c1c\u0c40', 'AUTO RIFLE': '\u0c06\u0c1f\u0c4b \u0c30\u0c48\u0c2b\u0c3f\u0c32\u0c4d', FLAMETHROWER: '\u0c2b\u0c4d\u0c32\u0c47\u0c2e\u0c4d\u200c\u0c24\u0c4d\u0c30\u0c4b\u0c35\u0c30\u0c4d' },\n                    zombies: { Walker: '\u0c28\u0c21\u0c3f\u0c1a\u0c47\u0c35\u0c3e\u0c21\u0c41', Runner: '\u0c2a\u0c30\u0c41\u0c17\u0c46\u0c24\u0c4d\u0c24\u0c47\u0c35\u0c3e\u0c21\u0c41', Tank: '\u0c1f\u0c4d\u0c2f\u0c3e\u0c02\u0c15\u0c4d', Crawler: '\u0c2a\u0c3e\u0c15\u0c47\u0c35\u0c3e\u0c21\u0c41', Berserker: '\u0c09\u0c28\u0c4d\u0c2e\u0c3e\u0c26\u0c3f', Armored: '\u0c15\u0c35\u0c1a\u0c27\u0c3e\u0c30\u0c3f', Swarm: '\u0c17\u0c41\u0c02\u0c2a\u0c41', Bloater: '\u0c09\u0c2c\u0c4d\u0c2c\u0c3f\u0c28\u0c35\u0c3e\u0c21\u0c41', Stalker: '\u0c35\u0c47\u0c1f\u0c17\u0c3e\u0c21\u0c41', Screamer: '\u0c05\u0c30\u0c1a\u0c47\u0c35\u0c3e\u0c21\u0c41', BRUTE: '\u0c15\u0c4d\u0c30\u0c42\u0c30\u0c41\u0c21\u0c41', CHARGER: '\u0c26\u0c42\u0c38\u0c41\u0c15\u0c4a\u0c1a\u0c4d\u0c1a\u0c47\u0c35\u0c3e\u0c21\u0c41', BEHEMOTH: '\u0c2e\u0c39\u0c3e\u0c26\u0c46\u0c2f\u0c4d\u0c2f\u0c02', INFERNAL: '\u0c28\u0c30\u0c15\u0c2a\u0c41', OVERLORD: '\u0c05\u0c27\u0c3f\u0c2a\u0c24\u0c3f' }\n                },\n                vi: {\n                    app: { title: 'Ng\u01b0\u1eddi S\u1ed1ng S\u00f3t Cu\u1ed1i C\u00f9ng', titleUpper: 'NG\u01af\u1edcI S\u1ed0NG S\u00d3T CU\u1ed0I C\u00d9NG', titleBreak: 'NG\u01af\u1edcI S\u1ed0NG S\u00d3T<br>CU\u1ed0I C\u00d9NG' },\n                    tutorial: { title: '\u3010H\u01af\u1edaNG D\u1eaaN\u3011V\u0168 KH\u00cd M\u1edaI', desc: 'B\u1ea1n \u0111\u00e3 nh\u1eadn \u0111\u01b0\u1ee3c v\u0169 kh\u00ed m\u1edbi! H\u00e3y ch\u1ea1m v\u00e0o bi\u1ec3u t\u01b0\u1ee3ng n\u1ed5i b\u1eadt b\u00ean d\u01b0\u1edbi ho\u1eb7c nh\u1ea5n ph\u00edm [2] \u0111\u1ec3 chuy\u1ec3n v\u0169 kh\u00ed v\u00e0 ti\u1ebfp t\u1ee5c.' },\n                    loading: { assets: '\u0110ang t\u1ea3i t\u00e0i nguy\u00ean...', assetsProgress: '\u0110ang t\u1ea3i t\u00e0i nguy\u00ean... {pct}%' },\n                    start: { subtitle: 'B\u1ea3o v\u1ec7 h\u1ea7m tr\u00fa \u1ea9n tr\u01b0\u1edbc b\u1ea7y zombie b\u1ea5t t\u1eadn', play: 'CH\u01a0I' },\n                    hud: { wave: '\u0110\u1ee2T {wave}', waveBoss: '\u0110\u1ee2T {wave} - TR\u00d9M: {boss}', waveInitial: '\u0110\u1ee2T 1', score: '\u0110I\u1ec2M', interact: 'Nh\u1ea5n E \u0111\u1ec3 t\u01b0\u01a1ng t\u00e1c', fire: 'B\u1eaeN', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: 'N\u1ea0P {weapon}', seconds: '{value} gi\u00e2y' },\n                    gameOver: { title: 'H\u1ea6M \u0110\u00c3 TH\u1ea4T TH\u1ee6', retry: 'TH\u1eec L\u1ea0I' },\n                    stats: { wave: '\u0110\u1ee3t', kills: 'H\u1ea1 g\u1ee5c', headshots: 'B\u1eafn \u0111\u1ea7u', score: '\u0110i\u1ec3m', accuracy: 'Ch\u00ednh x\u00e1c' },\n                    weaponSelect: { title: 'CH\u1eccN V\u0168 KH\u00cd', subtitle: 'Ch\u1ecdn v\u0169 kh\u00ed kh\u1edfi \u0111\u1ea7u' },\n                    upgrade: { waveCompleteInitial: '\u0110\u1ee2T \u0110\u00c3 XONG!', waveComplete: '\u0110\u1ee2T {wave} \u0110\u00c3 XONG!', choose: 'Ch\u1ecdn m\u1ed9t n\u00e2ng c\u1ea5p', lifestealName: 'H\u00daT M\u00c1U+', lifestealDesc: 'H\u1ed3i m\u00e1u khi tr\u00fang {from}% -> {to}%', damageName: '{weapon} S\u00c1T TH\u01af\u01a0NG+', damageDesc: 'S\u00e1t th\u01b0\u01a1ng {from} -> {to}', reloadName: '{weapon} N\u1ea0P \u0110\u1ea0N+', reloadDesc: 'N\u1ea1p \u0111\u1ea1n {from}gi\u00e2y -> {to}gi\u00e2y', magazineName: '{weapon} B\u0102NG \u0110\u1ea0N+', magazineDesc: 'B\u0103ng \u0111\u1ea1n {from} -> {to}', pierceName: '{weapon} XUY\u00caN+', pierceDesc: 'Xuy\u00ean ph\u00e1 {from} -> {to}', fireRateName: '{weapon} T\u1ed0C B\u1eaeN+', fireRateDesc: 'Kho\u1ea3ng b\u1eafn {from}ms -> {to}ms', speedName: '{weapon} T\u1ed0C \u0110\u1ed8+', rangeName: '{weapon} T\u1ea6M+', rangeDesc: 'T\u1ea7m b\u1eafn {from}m -> {to}m', newWeaponName: 'V\u0168 KH\u00cd M\u1edaI: {weapon}', slowTimeName: '\u3010K\u1ef8 N\u0102NG\u3011NG\u01afNG \u0110\u1eccNG TH\u1edcI GIAN', slowTimeDesc: 'Th\u1ebf gi\u1edbi ch\u1eadm l\u1ea1i 100 l\u1ea7n, ng\u01b0\u1eddi ch\u01a1i kh\u00f4ng b\u1ecb \u1ea3nh h\u01b0\u1edfng. K\u00e9o d\u00e0i 5 gi\u00e2y.', flameRangeName: '[S\u00fang Phun L\u1eeda] N\u00e9n Nhi\u00ean Li\u1ec7u', flameRangeDesc: 'T\u0103ng t\u1ea7m b\u1eafn +40% (t\u1ed1c \u0111\u1ed9 phun +15%) v\u00e0 s\u00e1t th\u01b0\u01a1ng tr\u1ef1c ti\u1ebfp +20% (C\u1ea5p {level})', flameStickyName: '[S\u00fang Phun L\u1eeda] L\u1eeda D\u00ednh', flameStickyDesc: 'T\u0103ng th\u1eddi gian ch\u00e1y th\u00eam +2 gi\u00e2y v\u00e0 s\u00e1t th\u01b0\u01a1ng m\u1ed7i gi\u00e2y th\u00eam +50% (C\u1ea5p {level})', flameSpreadName: '[S\u00fang Phun L\u1eeda] V\u00f2i Phun R\u1ed9ng', flameSpreadDesc: 'T\u0103ng k\u00edch th\u01b0\u1edbc ng\u1ecdn l\u1eeda v\u00e0 v\u00f9ng va ch\u1ea1m th\u00eam +35% (C\u1ea5p {level})', flameEmberName: '[S\u00fang Phun L\u1eeda] T\u00e0n L\u1eeda B\u00f9ng Ph\u00e1t', flameEmberDesc: 'K\u1ebb \u0111\u1ecbch \u0111ang ch\u00e1y c\u00f3 30% c\u01a1 h\u1ed9i thi\u00eau r\u1ee5i k\u1ebb \u0111\u1ecbch xung quanh', flameLavaName: '[S\u00fang Phun L\u1eeda] Dung Nham Cao \u0110\u1ed9', flameLavaDesc: 'Ng\u1ecdn l\u1eeda b\u1ecf qua ho\u00e0n to\u00e0n gi\u00e1p c\u1ee7a k\u1ebb \u0111\u1ecbch v\u00e0 nung ch\u1ea3y 15% gi\u00e1p t\u1ed1i \u0111a m\u1ed7i gi\u00e2y', flameRangeName: '[S\u00fang Phun L\u1eeda] N\u00e9n Nhi\u00ean Li\u1ec7u', flameRangeDesc: 'T\u0103ng t\u1ea7m b\u1eafn +40% (t\u1ed1c \u0111\u1ed9 phun +15%) v\u00e0 s\u00e1t th\u01b0\u01a1ng tr\u1ef1c ti\u1ebfp +20% (C\u1ea5p {level})', flameStickyName: '[S\u00fang Phun L\u1eeda] L\u1eeda D\u00ednh', flameStickyDesc: 'T\u0103ng th\u1eddi gian ch\u00e1y th\u00eam +2 gi\u00e2y v\u00e0 s\u00e1t th\u01b0\u01a1ng m\u1ed7i gi\u00e2y th\u00eam +50% (C\u1ea5p {level})', flameSpreadName: '[S\u00fang Phun L\u1eeda] V\u00f2i Phun R\u1ed9ng', flameSpreadDesc: 'T\u0103ng k\u00edch th\u01b0\u1edbc ng\u1ecdn l\u1eeda v\u00e0 v\u00f9ng va ch\u1ea1m th\u00eam +35% (C\u1ea5p {level})', flameEmberName: '[S\u00fang Phun L\u1eeda] T\u00e0n L\u1eeda B\u00f9ng Ph\u00e1t', flameEmberDesc: 'K\u1ebb \u0111\u1ecbch \u0111ang ch\u00e1y c\u00f3 30% c\u01a1 h\u1ed9i thi\u00eau r\u1ee5i k\u1ebb \u0111\u1ecbch xung quanh', flameLavaName: '[S\u00fang Phun L\u1eeda] Dung Nham Cao \u0110\u1ed9', flameLavaDesc: 'Ng\u1ecdn l\u1eeda b\u1ecf qua ho\u00e0n to\u00e0n gi\u00e1p c\u1ee7a k\u1ebb \u0111\u1ecbch v\u00e0 nung ch\u1ea3y 15% gi\u00e1p t\u1ed1i \u0111a m\u1ed7i gi\u00e2y' },\n                    weaponStats: { melee: 'C\u1eacN CHI\u1ebeN', auto: 'T\u1ef0 \u0110\u1ed8NG', semi: 'B\u00c1N T\u1ef0 \u0110\u1ed8NG', damage: 'ST:{value}', range: 'T\u1ea7m:{value}m', magazine: 'B\u0103ng:{value}' },\n                    edit: { title: 'CH\u1ebe \u0110\u1ed8 S\u1eecA', line1: 'D\u00f9ng b\u1ea3ng c\u00e0i \u0111\u1eb7t \u0111\u1ec3 c\u1ea5u h\u00ecnh th\u00f4ng s\u1ed1 tr\u00f2 ch\u01a1i.', line2: '\u0110i\u1ec1u ch\u1ec9nh \u0111\u1ed9 kh\u00f3 zombie, \u0111\u1ee3t b\u1eaft \u0111\u1ea7u, v\u0169 kh\u00ed v\u00e0 \u00e1nh s\u00e1ng.', architecture: 'Ki\u1ebfn tr\u00fac: ECS + Web Worker + Object Pool' },\n                    params: { name: 'Ng\u01b0\u1eddi S\u1ed1ng S\u00f3t Cu\u1ed1i C\u00f9ng', zombieHealth: 'M\u00e1u Zombie', zombieSpeed: 'T\u1ed1c \u0110\u1ed9 Zombie', startingWave: '\u0110\u1ee3t B\u1eaft \u0110\u1ea7u', headshotMultiplier: 'H\u1ec7 S\u1ed1 B\u1eafn \u0110\u1ea7u', katanaSpeed: 'T\u1ed1c \u0110\u1ed9 Katana', lighting: '\u00c1nh S\u00e1ng', coolWhite: 'Tr\u1eafng L\u1ea1nh', warm: '\u1ea4m', emergencyRed: '\u0110\u1ecf Kh\u1ea9n C\u1ea5p', nightVision: 'Nh\u00ecn \u0110\u00eam' },\n                    weapons: { PISTOL: 'S\u00daNG L\u1ee4C TH\u00d4NG MINH', SHOTGUN: 'S\u00daNG S\u0102N', SNIPER: 'S\u00daNG B\u1eaeN T\u1ec8A', KATANA: 'KATANA', GATLING: 'GATLING', LMG: 'LMG', 'AUTO RIFLE': 'S\u00daNG TR\u01af\u1edcNG T\u1ef0 \u0110\u1ed8NG', FLAMETHROWER: 'S\u00daNG PHUN L\u1eecA' },\n                    zombies: { Walker: 'K\u1ebb L\u00ea B\u01b0\u1edbc', Runner: 'K\u1ebb Ch\u1ea1y', Tank: 'Tank', Crawler: 'K\u1ebb B\u00f2', Berserker: 'Cu\u1ed3ng B\u1ea1o', Armored: 'B\u1ecdc Gi\u00e1p', Swarm: 'B\u1ea7y \u0110\u00e0n', Bloater: 'Ph\u00ecnh To', Stalker: 'K\u1ebb R\u00ecnh R\u1eadp', Screamer: 'K\u1ebb G\u00e0o Th\u00e9t', BRUTE: 'QU\u00c1I L\u1ef0C', CHARGER: 'K\u1eba LAO T\u1edaI', BEHEMOTH: 'KH\u1ed4NG L\u1ed2', INFERNAL: 'H\u1eceA NG\u1ee4C', OVERLORD: 'CH\u00daA T\u1ec2' }\n                },\n                mr: {\n                    app: { title: '\u0936\u0947\u0935\u091f\u091a\u093e \u0935\u093e\u091a\u0932\u0947\u0932\u093e', titleUpper: '\u0936\u0947\u0935\u091f\u091a\u093e \u0935\u093e\u091a\u0932\u0947\u0932\u093e', titleBreak: '\u0936\u0947\u0935\u091f\u091a\u093e<br>\u0935\u093e\u091a\u0932\u0947\u0932\u093e' },\n                    tutorial: { title: '\u3010\u092e\u093e\u0930\u094d\u0917\u0926\u0930\u094d\u0936\u0928\u3011\u0928\u0935\u0940\u0928 \u0936\u0938\u094d\u0924\u094d\u0930', desc: '\u0924\u0941\u092e\u094d\u0939\u093e\u0932\u093e \u0928\u0935\u0940\u0928 \u0936\u0938\u094d\u0924\u094d\u0930 \u092e\u093f\u0933\u093e\u0932\u0947 \u0906\u0939\u0947! \u0924\u094d\u092f\u093e\u0935\u0930 \u091c\u093e\u0923\u094d\u092f\u093e\u0938\u093e\u0920\u0940 \u0906\u0923\u093f \u0938\u0941\u0930\u0942 \u0920\u0947\u0935\u0923\u094d\u092f\u093e\u0938\u093e\u0920\u0940 \u0916\u093e\u0932\u0940\u0932 \u0939\u093e\u092f\u0932\u093e\u0907\u091f \u0915\u0947\u0932\u0947\u0932\u094d\u092f\u093e \u091a\u093f\u0928\u094d\u0939\u093e\u0935\u0930 \u091f\u0945\u092a \u0915\u0930\u093e \u0915\u093f\u0902\u0935\u093e [2] \u0926\u093e\u092c\u093e.' },\n                    loading: { assets: '\u0938\u093e\u0927\u0928\u0947 \u0932\u094b\u0921 \u0939\u094b\u0924 \u0906\u0939\u0947\u0924...', assetsProgress: '\u0938\u093e\u0927\u0928\u0947 \u0932\u094b\u0921 \u0939\u094b\u0924 \u0906\u0939\u0947\u0924... {pct}%' },\n                    start: { subtitle: '\u0905\u0916\u0902\u0921 \u091d\u0949\u092e\u094d\u092c\u0940 \u091f\u094b\u0933\u094d\u092f\u093e\u0902\u092a\u093e\u0938\u0942\u0928 \u092c\u0902\u0915\u0930 \u0935\u093e\u091a\u0935\u093e', play: '\u0916\u0947\u0933\u093e' },\n                    hud: { wave: '\u0935\u0947\u0935\u094d\u0939 {wave}', waveBoss: '\u0935\u0947\u0935\u094d\u0939 {wave} - \u092c\u0949\u0938: {boss}', waveInitial: '\u0935\u0947\u0935\u094d\u0939 1', score: '\u0938\u094d\u0915\u094b\u0905\u0930', interact: '\u0915\u094d\u0930\u093f\u092f\u093e \u0915\u0930\u0923\u094d\u092f\u093e\u0938\u093e\u0920\u0940 E \u0926\u093e\u092c\u093e', fire: '\u0917\u094b\u0933\u0940 \u091d\u093e\u0921\u093e', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} \u0930\u0940\u0932\u094b\u0921', seconds: '{value}\u0938\u0947' },\n                    gameOver: { title: '\u092c\u0902\u0915\u0930 \u092a\u0921\u0932\u093e', retry: '\u092a\u0941\u0928\u094d\u0939\u093e \u092a\u094d\u0930\u092f\u0924\u094d\u0928' },\n                    stats: { wave: '\u0935\u0947\u0935\u094d\u0939', kills: '\u0939\u0924\u094d\u092f\u0947', headshots: '\u0939\u0947\u0921\u0936\u0949\u091f', score: '\u0938\u094d\u0915\u094b\u0905\u0930', accuracy: '\u0905\u091a\u0942\u0915\u0924\u093e' },\n                    weaponSelect: { title: '\u0936\u0938\u094d\u0924\u094d\u0930 \u0928\u093f\u0935\u0921\u093e', subtitle: '\u0924\u0941\u092e\u091a\u0947 \u0938\u0941\u0930\u0941\u0935\u093e\u0924\u0940\u091a\u0947 \u0936\u0938\u094d\u0924\u094d\u0930 \u0928\u093f\u0935\u0921\u093e' },\n                    upgrade: { waveCompleteInitial: '\u0935\u0947\u0935\u094d\u0939 \u092a\u0942\u0930\u094d\u0923!', waveComplete: '\u0935\u0947\u0935\u094d\u0939 {wave} \u092a\u0942\u0930\u094d\u0923!', choose: '\u090f\u0915 \u0905\u092a\u0917\u094d\u0930\u0947\u0921 \u0928\u093f\u0935\u0921\u093e', lifestealName: '\u091c\u0940\u0935\u0928\u0936\u094b\u0937\u0923+', lifestealDesc: '\u0939\u093f\u091f\u0935\u0930 \u0909\u092a\u091a\u093e\u0930 {from}% -> {to}%', damageName: '{weapon} \u0928\u0941\u0915\u0938\u093e\u0928+', damageDesc: '\u0928\u0941\u0915\u0938\u093e\u0928 {from} -> {to}', reloadName: '{weapon} \u0930\u0940\u0932\u094b\u0921+', reloadDesc: '\u0930\u0940\u0932\u094b\u0921 {from}\u0938\u0947 -> {to}\u0938\u0947', magazineName: '{weapon} \u092e\u0945\u0917+', magazineDesc: '\u092e\u0945\u0917\u091d\u0940\u0928 {from} -> {to}', pierceName: '{weapon} \u092d\u0947\u0926\u0928+', pierceDesc: '\u092d\u0947\u0926\u0928 {from} -> {to}', fireRateName: '{weapon} \u092b\u093e\u092f\u0930 \u0930\u0947\u091f+', fireRateDesc: '\u0905\u0902\u0924\u0930 {from}\u092e\u093f.\u0938\u0947 -> {to}\u092e\u093f.\u0938\u0947', speedName: '{weapon} \u0935\u0947\u0917+', rangeName: '{weapon} \u092a\u0932\u094d\u0932\u093e+', rangeDesc: '\u092a\u0932\u094d\u0932\u093e {from}\u092e\u0940 -> {to}\u092e\u0940', newWeaponName: '\u0928\u0935\u0947 \u0936\u0938\u094d\u0924\u094d\u0930: {weapon}', slowTimeName: '\u3010\u0915\u094c\u0936\u0932\u094d\u092f\u3011\u0935\u0947\u0933 \u0915\u092e\u0940 \u0915\u0930\u0923\u0947', slowTimeDesc: '\u091c\u0917 \u0967\u0966\u0966 \u092a\u091f\u0940\u0928\u0947 \u0938\u0902\u0925 \u0939\u094b\u0924\u0947, \u0916\u0947\u0933\u093e\u0921\u0942 \u0905\u092a\u094d\u0930\u092d\u093e\u0935\u093f\u0924 \u0930\u093e\u0939\u0924\u094b. \u0915\u093e\u0932\u093e\u0935\u0927\u0940 \u096b \u0938\u0947\u0915\u0902\u0926\u0964', flameRangeName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u0907\u0902\u0927\u0928 \u0915\u0949\u092e\u094d\u092a\u094d\u0930\u0947\u0936\u0928', flameRangeDesc: '\u0915\u0915\u094d\u0937\u093e +40% (\u0935\u0947\u0917 +15%) \u0906\u0923\u093f \u0925\u0947\u091f \u0928\u0941\u0915\u0938\u093e\u0928 +20% \u0928\u0947 \u0935\u093e\u0922\u0935\u093e (\u092a\u093e\u0924\u0933\u0940 {level})', flameStickyName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u091a\u093f\u0915\u091f \u0906\u0917', flameStickyDesc: '\u091c\u0933\u0923\u094d\u092f\u093e\u091a\u093e \u0915\u093e\u0932\u093e\u0935\u0927\u0940 +2 \u0938\u0947\u0915\u0902\u0926\u093e\u0928\u0947 \u0906\u0923\u093f \u092a\u094d\u0930\u0924\u093f \u0938\u0947\u0915\u0902\u0926 \u0928\u0941\u0915\u0938\u093e\u0928 +50% \u0928\u0947 \u0935\u093e\u0922\u0935\u093e (\u092a\u093e\u0924\u0933\u0940 {level})', flameSpreadName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u0930\u0941\u0902\u0926 \u0928\u094b\u091c\u0932', flameSpreadDesc: '\u091c\u094d\u0935\u093e\u0932\u0947\u091a\u093e \u0906\u0915\u093e\u0930 \u0906\u0923\u093f \u091f\u0915\u094d\u0915\u0930 \u0915\u094d\u0937\u0947\u0924\u094d\u0930 +35% \u0928\u0947 \u0935\u093e\u0922\u0935\u093e (\u092a\u093e\u0924\u0933\u0940 {level})', flameEmberName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u0915\u094b\u0933\u0936\u093e\u091a\u0947 \u092a\u0941\u0928\u0930\u0941\u091c\u094d\u091c\u0940\u0935\u0928', flameEmberDesc: '\u091c\u0933\u0923\u093e\u0931\u094d\u092f\u093e \u0936\u0924\u094d\u0930\u0942\u0902\u0915\u0921\u0947 \u0936\u0947\u091c\u093e\u0930\u091a\u094d\u092f\u093e \u0936\u0924\u094d\u0930\u0942\u0902\u0928\u093e \u092a\u0947\u091f\u0935\u0923\u094d\u092f\u093e\u091a\u0940 30% \u0936\u0915\u094d\u092f\u0924\u093e \u0905\u0938\u0924\u0947', flameLavaName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u0909\u091a\u094d\u091a-\u0924\u093e\u092a\u092e\u093e\u0928 \u0932\u093e\u0935\u093e', flameLavaDesc: '\u091c\u094d\u0935\u093e\u0932\u093e \u0936\u0924\u094d\u0930\u0942\u091a\u094d\u092f\u093e \u091a\u093f\u0932\u0916\u0924\u093e\u0915\u0921\u0947 \u092a\u0942\u0930\u094d\u0923\u092a\u0923\u0947 \u0926\u0941\u0930\u094d\u0932\u0915\u094d\u0937 \u0915\u0930\u0924\u093e\u0924 \u0906\u0923\u093f \u092a\u094d\u0930\u0924\u093f \u0938\u0947\u0915\u0902\u0926 15% \u0915\u092e\u093e\u0932 \u091a\u093f\u0932\u0916\u0924 \u0935\u093f\u0924\u0933\u0935\u0924\u093e\u0924', flameRangeName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u0907\u0902\u0927\u0928 \u0915\u0949\u092e\u094d\u092a\u094d\u0930\u0947\u0936\u0928', flameRangeDesc: '\u0915\u0915\u094d\u0937\u093e +40% (\u0935\u0947\u0917 +15%) \u0906\u0923\u093f \u0925\u0947\u091f \u0928\u0941\u0915\u0938\u093e\u0928 +20% \u0928\u0947 \u0935\u093e\u0922\u0935\u093e (\u092a\u093e\u0924\u0933\u0940 {level})', flameStickyName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u091a\u093f\u0915\u091f \u0906\u0917', flameStickyDesc: '\u091c\u0933\u0923\u094d\u092f\u093e\u091a\u093e \u0915\u093e\u0932\u093e\u0935\u0927\u0940 +2 \u0938\u0947\u0915\u0902\u0926\u093e\u0928\u0947 \u0906\u0923\u093f \u092a\u094d\u0930\u0924\u093f \u0938\u0947\u0915\u0902\u0926 \u0928\u0941\u0915\u0938\u093e\u0928 +50% \u0928\u0947 \u0935\u093e\u0922\u0935\u093e (\u092a\u093e\u0924\u0933\u0940 {level})', flameSpreadName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u0930\u0941\u0902\u0926 \u0928\u094b\u091c\u0932', flameSpreadDesc: '\u091c\u094d\u0935\u093e\u0932\u0947\u091a\u093e \u0906\u0915\u093e\u0930 \u0906\u0923\u093f \u091f\u0915\u094d\u0915\u0930 \u0915\u094d\u0937\u0947\u0924\u094d\u0930 +35% \u0928\u0947 \u0935\u093e\u0922\u0935\u093e (\u092a\u093e\u0924\u0933\u0940 {level})', flameEmberName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u0915\u094b\u0933\u0936\u093e\u091a\u0947 \u092a\u0941\u0928\u0930\u0941\u091c\u094d\u091c\u0940\u0935\u0928', flameEmberDesc: '\u091c\u0933\u0923\u093e\u0931\u094d\u092f\u093e \u0936\u0924\u094d\u0930\u0942\u0902\u0915\u0921\u0947 \u0936\u0947\u091c\u093e\u0930\u091a\u094d\u092f\u093e \u0936\u0924\u094d\u0930\u0942\u0902\u0928\u093e \u092a\u0947\u091f\u0935\u0923\u094d\u092f\u093e\u091a\u0940 30% \u0936\u0915\u094d\u092f\u0924\u093e \u0905\u0938\u0924\u0947', flameLavaName: '[\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930] \u0909\u091a\u094d\u091a-\u0924\u093e\u092a\u092e\u093e\u0928 \u0932\u093e\u0935\u093e', flameLavaDesc: '\u091c\u094d\u0935\u093e\u0932\u093e \u0936\u0924\u094d\u0930\u0942\u091a\u094d\u092f\u093e \u091a\u093f\u0932\u0916\u0924\u093e\u0915\u0921\u0947 \u092a\u0942\u0930\u094d\u0923\u092a\u0923\u0947 \u0926\u0941\u0930\u094d\u0932\u0915\u094d\u0937 \u0915\u0930\u0924\u093e\u0924 \u0906\u0923\u093f \u092a\u094d\u0930\u0924\u093f \u0938\u0947\u0915\u0902\u0926 15% \u0915\u092e\u093e\u0932 \u091a\u093f\u0932\u0916\u0924 \u0935\u093f\u0924\u0933\u0935\u0924\u093e\u0924' },\n                    weaponStats: { melee: '\u091c\u0935\u0933\u0942\u0928', auto: '\u0911\u091f\u094b', semi: '\u0938\u0947\u092e\u0940', damage: '\u0928\u0941\u0915\u0938\u093e\u0928:{value}', range: '\u092a\u0932\u094d\u0932\u093e:{value}\u092e\u0940', magazine: '\u092e\u0945\u0917:{value}' },\n                    edit: { title: '\u090f\u0921\u093f\u091f \u092e\u094b\u0921', line1: '\u0917\u0947\u092e\u091a\u0947 \u092a\u0945\u0930\u093e\u092e\u0940\u091f\u0930 \u092c\u0926\u0932\u0923\u094d\u092f\u093e\u0938\u093e\u0920\u0940 \u0938\u0947\u091f\u093f\u0902\u0917\u094d\u091c \u092a\u0945\u0928\u0947\u0932 \u0935\u093e\u092a\u0930\u093e.', line2: '\u091d\u0949\u092e\u094d\u092c\u0940 \u0915\u0920\u0940\u0923\u0924\u093e, \u0938\u0941\u0930\u0941\u0935\u093e\u0924\u0940\u091a\u0940 \u0935\u0947\u0935\u094d\u0939, \u0936\u0938\u094d\u0924\u094d\u0930\u0947 \u0906\u0923\u093f \u092a\u094d\u0930\u0915\u093e\u0936 \u092c\u0926\u0932\u093e.', architecture: '\u0930\u091a\u0928\u093e: ECS + Web Worker + Object Pool' },\n                    params: { name: '\u0936\u0947\u0935\u091f\u091a\u093e \u0935\u093e\u091a\u0932\u0947\u0932\u093e', zombieHealth: '\u091d\u0949\u092e\u094d\u092c\u0940 \u0906\u0930\u094b\u0917\u094d\u092f', zombieSpeed: '\u091d\u0949\u092e\u094d\u092c\u0940 \u0935\u0947\u0917', startingWave: '\u0938\u0941\u0930\u0941\u0935\u093e\u0924\u0940\u091a\u0940 \u0935\u0947\u0935\u094d\u0939', headshotMultiplier: '\u0939\u0947\u0921\u0936\u0949\u091f \u0917\u0941\u0923\u0915', katanaSpeed: '\u0915\u091f\u093e\u0928\u093e \u0935\u0947\u0917', lighting: '\u092a\u094d\u0930\u0915\u093e\u0936', coolWhite: '\u0925\u0902\u0921 \u092a\u093e\u0902\u0922\u0930\u093e', warm: '\u0909\u092c\u0926\u093e\u0930', emergencyRed: '\u0906\u092a\u0924\u094d\u0915\u093e\u0932\u0940\u0928 \u0932\u093e\u0932', nightVision: '\u0930\u093e\u0924\u094d\u0930\u0926\u0943\u0937\u094d\u091f\u0940' },\n                    weapons: { PISTOL: '\u0938\u094d\u092e\u093e\u0930\u094d\u091f \u092a\u093f\u0938\u094d\u0924\u0942\u0932', SHOTGUN: '\u0936\u0949\u091f\u0917\u0928', SNIPER: '\u0938\u094d\u0928\u093e\u092f\u092a\u0930', KATANA: '\u0915\u091f\u093e\u0928\u093e', GATLING: '\u0917\u0945\u091f\u0932\u093f\u0902\u0917', LMG: '\u090f\u0932\u090f\u092e\u091c\u0940', 'AUTO RIFLE': '\u0938\u094d\u0935\u092f\u0902\u091a\u0932\u093f\u0924 \u0930\u093e\u092f\u092b\u0932', FLAMETHROWER: '\u092b\u094d\u0932\u0947\u092e\u0925\u094d\u0930\u094b\u0935\u0930' },\n                    zombies: { Walker: '\u091a\u093e\u0932\u0923\u093e\u0930\u093e', Runner: '\u0927\u093e\u0935\u0923\u093e\u0930\u093e', Tank: '\u091f\u0901\u0915', Crawler: '\u0930\u093e\u0902\u0917\u0923\u093e\u0930\u093e', Berserker: '\u0909\u0928\u094d\u092e\u0924\u094d\u0924', Armored: '\u0915\u0935\u091a\u0927\u093e\u0930\u0940', Swarm: '\u091d\u0941\u0902\u0921', Bloater: '\u092b\u0941\u0917\u0932\u0947\u0932\u093e', Stalker: '\u092a\u093e\u0920\u0932\u093e\u0917\u0940', Screamer: '\u0915\u093f\u0902\u091a\u093e\u0933\u0923\u093e\u0930\u093e', BRUTE: '\u0930\u093e\u0928\u091f\u0940', CHARGER: '\u0927\u0921\u0915\u0923\u093e\u0930\u093e', BEHEMOTH: '\u092e\u0939\u093e\u0915\u093e\u092f', INFERNAL: '\u0928\u0930\u0915\u0940', OVERLORD: '\u0905\u0927\u093f\u092a\u0924\u0940' }\n                },\n                ta: {\n                    app: { title: '\u0b95\u0b9f\u0bc8\u0b9a\u0bbf \u0b89\u0baf\u0bbf\u0bb0\u0bcd\u0bb5\u0bbe\u0bb4\u0bcd\u0bb5\u0bcb\u0bb0\u0bcd', titleUpper: '\u0b95\u0b9f\u0bc8\u0b9a\u0bbf \u0b89\u0baf\u0bbf\u0bb0\u0bcd\u0bb5\u0bbe\u0bb4\u0bcd\u0bb5\u0bcb\u0bb0\u0bcd', titleBreak: '\u0b95\u0b9f\u0bc8\u0b9a\u0bbf<br>\u0b89\u0baf\u0bbf\u0bb0\u0bcd\u0bb5\u0bbe\u0bb4\u0bcd\u0bb5\u0bcb\u0bb0\u0bcd' },\n                    tutorial: { title: '\u3010\u0baa\u0baf\u0bbf\u0bb1\u0bcd\u0b9a\u0bbf\u3011\u0baa\u0bc1\u0ba4\u0bbf\u0baf \u0b86\u0baf\u0bc1\u0ba4\u0bae\u0bcd', desc: '\u0baa\u0bc1\u0ba4\u0bbf\u0baf \u0b86\u0baf\u0bc1\u0ba4\u0ba4\u0bcd\u0ba4\u0bc8 \u0baa\u0bc6\u0bb1\u0bcd\u0bb1\u0bc1\u0bb3\u0bcd\u0bb3\u0bc0\u0bb0\u0bcd\u0b95\u0bb3\u0bcd! \u0bae\u0bbe\u0bb1\u0bb5\u0bc1\u0bae\u0bcd \u0ba4\u0bca\u0b9f\u0bb0\u0bb5\u0bc1\u0bae\u0bcd \u0b95\u0bc0\u0bb4\u0bc7 \u0b9a\u0bbf\u0bb1\u0baa\u0bcd\u0baa\u0bbf\u0ba4\u0bcd\u0ba4\u0bc1\u0b95\u0bcd \u0b95\u0bbe\u0b9f\u0bcd\u0b9f\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f\u0bc1\u0bb3\u0bcd\u0bb3 \u0b90\u0b95\u0bbe\u0ba9\u0bc8\u0ba4\u0bcd \u0ba4\u0b9f\u0bcd\u0b9f\u0bb5\u0bc1\u0bae\u0bcd \u0b85\u0bb2\u0bcd\u0bb2\u0ba4\u0bc1 [2] \u0b85\u0bb4\u0bc1\u0ba4\u0bcd\u0ba4\u0bb5\u0bc1\u0bae\u0bcd.' },\n                    loading: { assets: '\u0bb5\u0bb3\u0b99\u0bcd\u0b95\u0bb3\u0bcd \u0b8f\u0bb1\u0bcd\u0bb1\u0baa\u0bcd\u0baa\u0b9f\u0bc1\u0b95\u0bbf\u0ba9\u0bcd\u0bb1\u0ba9...', assetsProgress: '\u0bb5\u0bb3\u0b99\u0bcd\u0b95\u0bb3\u0bcd \u0b8f\u0bb1\u0bcd\u0bb1\u0baa\u0bcd\u0baa\u0b9f\u0bc1\u0b95\u0bbf\u0ba9\u0bcd\u0bb1\u0ba9... {pct}%' },\n                    start: { subtitle: '\u0bae\u0bc1\u0b9f\u0bbf\u0bb5\u0bbf\u0bb2\u0bcd\u0bb2\u0bbe \u0b9a\u0bcb\u0bae\u0bcd\u0baa\u0bbf \u0b95\u0bc2\u0b9f\u0bcd\u0b9f\u0b99\u0bcd\u0b95\u0bb3\u0bbf\u0b9f\u0bae\u0bbf\u0bb0\u0bc1\u0ba8\u0bcd\u0ba4\u0bc1 \u0baa\u0ba4\u0bc1\u0b99\u0bcd\u0b95\u0bc1\u0b95\u0bcd \u0b95\u0bcb\u0b9f\u0bcd\u0b9f\u0bc8\u0baf\u0bc8 \u0b95\u0bbe\u0baa\u0bcd\u0baa\u0bbe\u0bb1\u0bcd\u0bb1\u0bc1', play: '\u0bb5\u0bbf\u0bb3\u0bc8\u0baf\u0bbe\u0b9f\u0bc1' },\n                    hud: { wave: '\u0b85\u0bb2\u0bc8 {wave}', waveBoss: '\u0b85\u0bb2\u0bc8 {wave} - \u0baa\u0bbe\u0bb8\u0bcd: {boss}', waveInitial: '\u0b85\u0bb2\u0bc8 1', score: '\u0bae\u0ba4\u0bbf\u0baa\u0bcd\u0baa\u0bc6\u0ba3\u0bcd', interact: '\u0b9a\u0bc6\u0baf\u0bb2\u0bcd\u0baa\u0b9f E \u0b85\u0bb4\u0bc1\u0ba4\u0bcd\u0ba4\u0bb5\u0bc1\u0bae\u0bcd', fire: '\u0b9a\u0bc1\u0b9f\u0bc1', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: '{weapon} \u0bb0\u0bc0\u0bb2\u0bcb\u0b9f\u0bc1', seconds: '{value}\u0bb5\u0bbf' },\n                    gameOver: { title: '\u0baa\u0ba4\u0bc1\u0b99\u0bcd\u0b95\u0bc1\u0b95\u0bcd \u0b95\u0bcb\u0b9f\u0bcd\u0b9f\u0bc8 \u0b89\u0b9f\u0bc8\u0ba8\u0bcd\u0ba4\u0ba4\u0bc1', retry: '\u0bae\u0bc0\u0ba3\u0bcd\u0b9f\u0bc1\u0bae\u0bcd \u0bae\u0bc1\u0baf\u0bb1\u0bcd\u0b9a\u0bbf' },\n                    stats: { wave: '\u0b85\u0bb2\u0bc8', kills: '\u0b95\u0bca\u0bb2\u0bc8\u0b95\u0bb3\u0bcd', headshots: '\u0ba4\u0bb2\u0bc8\u0b9a\u0bcd \u0b9a\u0bc1\u0b9f\u0bcd\u0b9f\u0bc1\u0b95\u0bb3\u0bcd', score: '\u0bae\u0ba4\u0bbf\u0baa\u0bcd\u0baa\u0bc6\u0ba3\u0bcd', accuracy: '\u0ba4\u0bc1\u0bb2\u0bcd\u0bb2\u0bbf\u0baf\u0bae\u0bcd' },\n                    weaponSelect: { title: '\u0b86\u0baf\u0bc1\u0ba4\u0bae\u0bcd \u0ba4\u0bc7\u0bb0\u0bcd\u0ba8\u0bcd\u0ba4\u0bc6\u0b9f\u0bc1', subtitle: '\u0ba4\u0bca\u0b9f\u0b95\u0bcd\u0b95 \u0b86\u0baf\u0bc1\u0ba4\u0ba4\u0bcd\u0ba4\u0bc8\u0ba4\u0bcd \u0ba4\u0bc7\u0bb0\u0bcd\u0ba8\u0bcd\u0ba4\u0bc6\u0b9f\u0bc1' },\n                    upgrade: { waveCompleteInitial: '\u0b85\u0bb2\u0bc8 \u0bae\u0bc1\u0b9f\u0bbf\u0ba8\u0bcd\u0ba4\u0ba4\u0bc1!', waveComplete: '\u0b85\u0bb2\u0bc8 {wave} \u0bae\u0bc1\u0b9f\u0bbf\u0ba8\u0bcd\u0ba4\u0ba4\u0bc1!', choose: '\u0b92\u0bb0\u0bc1 \u0bae\u0bc7\u0bae\u0bcd\u0baa\u0bbe\u0b9f\u0bcd\u0b9f\u0bc8\u0ba4\u0bcd \u0ba4\u0bc7\u0bb0\u0bcd\u0ba8\u0bcd\u0ba4\u0bc6\u0b9f\u0bc1', lifestealName: '\u0b89\u0baf\u0bbf\u0bb0\u0bcd \u0b89\u0bb1\u0bbf\u0b9e\u0bcd\u0b9a\u0bb2\u0bcd+', lifestealDesc: '\u0ba4\u0bbe\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd\u0baa\u0bcb\u0ba4\u0bc1 \u0b95\u0bc1\u0ba3\u0bae\u0b9f\u0bc8\u0ba4\u0bb2\u0bcd {from}% -> {to}%', damageName: '{weapon} \u0b9a\u0bc7\u0ba4\u0bae\u0bcd+', damageDesc: '\u0b9a\u0bc7\u0ba4\u0bae\u0bcd {from} -> {to}', reloadName: '{weapon} \u0bb0\u0bc0\u0bb2\u0bcb\u0b9f\u0bc1+', reloadDesc: '\u0bb0\u0bc0\u0bb2\u0bcb\u0b9f\u0bc1 {from}\u0bb5\u0bbf -> {to}\u0bb5\u0bbf', magazineName: '{weapon} \u0bae\u0bc7\u0b95\u0bcd+', magazineDesc: '\u0bae\u0bc7\u0b95\u0b9a\u0bbf\u0ba9\u0bcd {from} -> {to}', pierceName: '{weapon} \u0b8a\u0b9f\u0bc1\u0bb0\u0bc1\u0bb5\u0bb2\u0bcd+', pierceDesc: '\u0b8a\u0b9f\u0bc1\u0bb0\u0bc1\u0bb5\u0bb2\u0bcd {from} -> {to}', fireRateName: '{weapon} \u0b9a\u0bc1\u0b9f\u0bc1\u0bae\u0bcd \u0bb5\u0bc7\u0b95\u0bae\u0bcd+', fireRateDesc: '\u0b87\u0b9f\u0bc8\u0bb5\u0bc6\u0bb3\u0bbf {from}\u0bae\u0bbf.\u0bb5\u0bbf -> {to}\u0bae\u0bbf.\u0bb5\u0bbf', speedName: '{weapon} \u0bb5\u0bc7\u0b95\u0bae\u0bcd+', rangeName: '{weapon} \u0ba4\u0bc2\u0bb0\u0bae\u0bcd+', rangeDesc: '\u0ba4\u0bc2\u0bb0\u0bae\u0bcd {from}\u0bae\u0bc0 -> {to}\u0bae\u0bc0', newWeaponName: '\u0baa\u0bc1\u0ba4\u0bbf\u0baf \u0b86\u0baf\u0bc1\u0ba4\u0bae\u0bcd: {weapon}', slowTimeName: '\u3010\u0ba4\u0bbf\u0bb1\u0ba9\u0bcd\u3011\u0ba8\u0bc7\u0bb0\u0ba4\u0bcd\u0ba4\u0bc8 \u0bae\u0bc6\u0ba4\u0bc1\u0bb5\u0bbe\u0b95\u0bcd\u0b95\u0bc1\u0ba4\u0bb2\u0bcd', slowTimeDesc: '\u0b89\u0bb2\u0b95\u0bae\u0bcd 100 \u0bae\u0b9f\u0b99\u0bcd\u0b95\u0bc1 \u0bae\u0bc6\u0ba4\u0bc1\u0bb5\u0bbe\u0b95\u0bbf\u0bb1\u0ba4\u0bc1, \u0bb5\u0bc0\u0bb0\u0bb0\u0bcd \u0baa\u0bbe\u0ba4\u0bbf\u0b95\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0b9f\u0bc1\u0bb5\u0ba4\u0bbf\u0bb2\u0bcd\u0bb2\u0bc8. 5 \u0bb5\u0bbf\u0ba8\u0bbe\u0b9f\u0bbf\u0b95\u0bb3\u0bcd \u0ba8\u0bc0\u0b9f\u0bbf\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd.', flameRangeName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0b8e\u0bb0\u0bbf\u0baa\u0bca\u0bb0\u0bc1\u0bb3\u0bcd \u0b9a\u0bc1\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bae\u0bcd', flameRangeDesc: '\u0bb5\u0bb0\u0bae\u0bcd\u0baa\u0bc8 +40% (\u0bb5\u0bc7\u0b95\u0bae\u0bcd +15%) \u0bae\u0bb1\u0bcd\u0bb1\u0bc1\u0bae\u0bcd \u0ba8\u0bc7\u0bb0\u0b9f\u0bbf \u0b9a\u0bc7\u0ba4\u0ba4\u0bcd\u0ba4\u0bc8 +20% \u0b85\u0ba4\u0bbf\u0b95\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd (\u0ba8\u0bbf\u0bb2\u0bc8 {level})', flameStickyName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0b92\u0b9f\u0bcd\u0b9f\u0bc1\u0bae\u0bcd \u0ba8\u0bc6\u0bb0\u0bc1\u0baa\u0bcd\u0baa\u0bc1', flameStickyDesc: '\u0b8e\u0bb0\u0bbf\u0baf\u0bc1\u0bae\u0bcd \u0ba8\u0bc7\u0bb0\u0ba4\u0bcd\u0ba4\u0bc8 +2 \u0bb5\u0bbf\u0ba9\u0bbe\u0b9f\u0bbf\u0b95\u0bb3\u0bc1\u0bae\u0bcd \u0b9a\u0bc7\u0ba4\u0ba4\u0bcd\u0ba4\u0bc8 +50% \u0b85\u0ba4\u0bbf\u0b95\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd (\u0ba8\u0bbf\u0bb2\u0bc8 {level})', flameSpreadName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0b85\u0b95\u0bb2\u0bae\u0bbe\u0ba9 \u0bae\u0bc1\u0ba9\u0bc8', flameSpreadDesc: '\u0ba8\u0bc6\u0c30\u0c41\u0baa\u0bcd\u0baa\u0bbf\u0ba9\u0bcd \u0b85\u0bb3\u0bb5\u0bc1 \u0bae\u0bb1\u0bcd\u0bb1\u0bc1\u0bae\u0bcd \u0bae\u0bcb\u0ba4\u0bb2\u0bcd \u0baa\u0bb0\u0baa\u0bcd\u0baa\u0bc8 +35% \u0b85\u0ba4\u0bbf\u0b95\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd (\u0ba8\u0bbf\u0bb2\u0bc8 {level})', flameEmberName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0ba4\u0ba3\u0bb2\u0bcd \u0bae\u0bc0\u0bb3\u0bc1\u0baf\u0bbf\u0bb0\u0bcd\u0baa\u0bcd\u0baa\u0bc1', flameEmberDesc: '\u0b8e\u0bb0\u0bbf\u0baf\u0bc1\u0bae\u0bcd \u0b8e\u0ba4\u0bbf\u0bb0\u0bbf\u0b95\u0bb3\u0bcd \u0ba4\u0b99\u0bcd\u0b95\u0bb3\u0bc1\u0b95\u0bcd\u0b95\u0bc1 \u0b85\u0bb0\u0bc1\u0b95\u0bbf\u0bb2\u0bc1\u0bb3\u0bcd\u0bb3 \u0b8e\u0ba4\u0bbf\u0bb0\u0bbf\u0b95\u0bb3\u0bc8 \u0b8e\u0bb0\u0bbf\u0b95\u0bcd\u0b95 30% \u0bb5\u0bbe\u0baf\u0bcd\u0baa\u0bcd\u0baa\u0bc1 \u0b89\u0bb3\u0bcd\u0bb3\u0ba4\u0bc1', flameLavaName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0b85\u0ba4\u0bbf\u0b95 \u0bb5\u0bc6\u0baa\u0bcd\u0baa \u0bb2\u0bbe\u0bb5\u0bbe', flameLavaDesc: '\u0ba8\u0bc6\u0bb0\u0bc1\u0baa\u0bcd\u0baa\u0bc1 \u0b8e\u0ba4\u0bbf\u0bb0\u0bbf\u0baf\u0bbf\u0ba9\u0bcd \u0b95\u0bb5\u0b9a\u0ba4\u0bcd\u0ba4\u0bc8 \u0bae\u0bc1\u0bb1\u0bcd\u0bb1\u0bbf\u0bb2\u0bc1\u0bae\u0bcd \u0baa\u0bc1\u0bb1\u0b95\u0bcd\u0b95\u0ba3\u0bbf\u0ba4\u0bcd\u0ba4\u0bc1, \u0bb5\u0bbf\u0ba9\u0bbe\u0b9f\u0bbf\u0b95\u0bcd\u0b95\u0bc1 15% \u0b85\u0ba4\u0bbf\u0b95\u0baa\u0b9f\u0bcd\u0b9a \u0b95\u0bb5\u0b9a\u0ba4\u0bcd\u0ba4\u0bc8 \u0b89\u0bb0\u0bc1\u0b95\u0bc1\u0bae\u0bcd', flameRangeName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0b8e\u0bb0\u0bbf\u0baa\u0bca\u0bb0\u0bc1\u0bb3\u0bcd \u0b9a\u0bc1\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bae\u0bcd', flameRangeDesc: '\u0bb5\u0bb0\u0bae\u0bcd\u0baa\u0bc8 +40% (\u0bb5\u0bc7\u0b95\u0bae\u0bcd +15%) \u0bae\u0bb1\u0bcd\u0bb1\u0bc1\u0bae\u0bcd \u0ba8\u0bc7\u0bb0\u0b9f\u0bbf \u0b9a\u0bc7\u0ba4\u0ba4\u0bcd\u0ba4\u0bc8 +20% \u0b85\u0ba4\u0bbf\u0b95\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd (\u0ba8\u0bbf\u0bb2\u0bc8 {level})', flameStickyName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0b92\u0b9f\u0bcd\u0b9f\u0bc1\u0bae\u0bcd \u0ba8\u0bc6\u0bb0\u0bc1\u0baa\u0bcd\u0baa\u0bc1', flameStickyDesc: '\u0b8e\u0bb0\u0bbf\u0baf\u0bc1\u0bae\u0bcd \u0ba8\u0bc7\u0bb0\u0ba4\u0bcd\u0ba4\u0bc8 +2 \u0bb5\u0bbf\u0ba9\u0bbe\u0b9f\u0bbf\u0b95\u0bb3\u0bc1\u0bae\u0bcd \u0b9a\u0bc7\u0ba4\u0ba4\u0bcd\u0ba4\u0bc8 +50% \u0b85\u0ba4\u0bbf\u0b95\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd (\u0ba8\u0bbf\u0bb2\u0bc8 {level})', flameSpreadName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0b85\u0b95\u0bb2\u0bae\u0bbe\u0ba9 \u0bae\u0bc1\u0ba9\u0bc8', flameSpreadDesc: '\u0ba8\u0bc6\u0c30\u0c41\u0baa\u0bcd\u0baa\u0bbf\u0ba9\u0bcd \u0b85\u0bb3\u0bb5\u0bc1 \u0bae\u0bb1\u0bcd\u0bb1\u0bc1\u0bae\u0bcd \u0bae\u0bcb\u0ba4\u0bb2\u0bcd \u0baa\u0bb0\u0baa\u0bcd\u0baa\u0bc8 +35% \u0b85\u0ba4\u0bbf\u0b95\u0bb0\u0bbf\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd (\u0ba8\u0bbf\u0bb2\u0bc8 {level})', flameEmberName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0ba4\u0ba3\u0bb2\u0bcd \u0bae\u0bc0\u0bb3\u0bc1\u0baf\u0bbf\u0bb0\u0bcd\u0baa\u0bcd\u0baa\u0bc1', flameEmberDesc: '\u0b8e\u0bb0\u0bbf\u0baf\u0bc1\u0bae\u0bcd \u0b8e\u0ba4\u0bbf\u0bb0\u0bbf\u0b95\u0bb3\u0bcd \u0ba4\u0b99\u0bcd\u0b95\u0bb3\u0bc1\u0b95\u0bcd\u0b95\u0bc1 \u0b85\u0bb0\u0bc1\u0b95\u0bbf\u0bb2\u0bc1\u0bb3\u0bcd\u0bb3 \u0b8e\u0ba4\u0bbf\u0bb0\u0bbf\u0b95\u0bb3\u0bc8 \u0b8e\u0bb0\u0bbf\u0b95\u0bcd\u0b95 30% \u0bb5\u0bbe\u0baf\u0bcd\u0baa\u0bcd\u0baa\u0bc1 \u0b89\u0bb3\u0bcd\u0bb3\u0ba4\u0bc1', flameLavaName: '[\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u10e0\u10dd\u0bb5\u0bb0\u0bcd] \u0b85\u0ba4\u0bbf\u0b95 \u0bb5\u0bc6\u0baa\u0bcd\u0baa \u0bb2\u0bbe\u0bb5\u0bbe', flameLavaDesc: '\u0ba8\u0bc6\u0bb0\u0bc1\u0baa\u0bcd\u0baa\u0bc1 \u0b8e\u0ba4\u0bbf\u0bb0\u0bbf\u0baf\u0bbf\u0ba9\u0bcd \u0b95\u0bb5\u0b9a\u0ba4\u0bcd\u0ba4\u0bc8 \u0bae\u0bc1\u0bb1\u0bcd\u0bb1\u0bbf\u0bb2\u0bc1\u0bae\u0bcd \u0baa\u0bc1\u0bb1\u0b95\u0bcd\u0b95\u0ba3\u0bbf\u0ba4\u0bcd\u0ba4\u0bc1, \u0bb5\u0bbf\u0ba9\u0bbe\u0b9f\u0bbf\u0b95\u0bcd\u0b95\u0bc1 15% \u0b85\u0ba4\u0bbf\u0b95\u0baa\u0b9f\u0bcd\u0b9a \u0b95\u0bb5\u0b9a\u0ba4\u0bcd\u0ba4\u0bc8 \u0b89\u0bb0\u0bc1\u0b95\u0bc1\u0bae\u0bcd' },\n                    weaponStats: { melee: '\u0ba8\u0bc6\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0baa\u0bcd \u0baa\u0bcb\u0bb0\u0bcd', auto: '\u0b86\u0b9f\u0bcd\u0b9f\u0bcb', semi: '\u0b9a\u0bc6\u0bae\u0bbf', damage: '\u0b9a\u0bc7\u0ba4\u0bae\u0bcd:{value}', range: '\u0ba4\u0bc2\u0bb0\u0bae\u0bcd:{value}\u0bae\u0bc0', magazine: '\u0bae\u0bc7\u0b95\u0bcd:{value}' },\n                    edit: { title: '\u0ba4\u0bbf\u0bb0\u0bc1\u0ba4\u0bcd\u0ba4\u0bc1 \u0bae\u0bc1\u0bb1\u0bc8', line1: '\u0bb5\u0bbf\u0bb3\u0bc8\u0baf\u0bbe\u0b9f\u0bcd\u0b9f\u0bc1 \u0b85\u0bb3\u0bb5\u0bc1\u0b95\u0bb3\u0bc8 \u0b85\u0bae\u0bc8\u0b95\u0bcd\u0b95 \u0b85\u0bae\u0bc8\u0baa\u0bcd\u0baa\u0bc1\u0baa\u0bcd \u0baa\u0bb2\u0b95\u0bc8\u0baf\u0bc8\u0baa\u0bcd \u0baa\u0baf\u0ba9\u0bcd\u0baa\u0b9f\u0bc1\u0ba4\u0bcd\u0ba4\u0bb5\u0bc1\u0bae\u0bcd.', line2: '\u0b9a\u0bcb\u0bae\u0bcd\u0baa\u0bbf \u0b95\u0b9f\u0bbf\u0ba9\u0bae\u0bcd, \u0ba4\u0bca\u0b9f\u0b95\u0bcd\u0b95 \u0b85\u0bb2\u0bc8, \u0b86\u0baf\u0bc1\u0ba4\u0b99\u0bcd\u0b95\u0bb3\u0bcd, \u0b92\u0bb3\u0bbf \u0b86\u0b95\u0bbf\u0baf\u0bb5\u0bb1\u0bcd\u0bb1\u0bc8 \u0bae\u0bbe\u0bb1\u0bcd\u0bb1\u0bb5\u0bc1\u0bae\u0bcd.', architecture: '\u0b95\u0b9f\u0bcd\u0b9f\u0bae\u0bc8\u0baa\u0bcd\u0baa\u0bc1: ECS + Web Worker + Object Pool' },\n                    params: { name: '\u0b95\u0b9f\u0bc8\u0b9a\u0bbf \u0b89\u0baf\u0bbf\u0bb0\u0bcd\u0bb5\u0bbe\u0bb4\u0bcd\u0bb5\u0bcb\u0bb0\u0bcd', zombieHealth: '\u0b9a\u0bcb\u0bae\u0bcd\u0baa\u0bbf \u0b86\u0bb0\u0bcb\u0b95\u0bcd\u0b95\u0bbf\u0baf\u0bae\u0bcd', zombieSpeed: '\u0b9a\u0bcb\u0bae\u0bcd\u0baa\u0bbf \u0bb5\u0bc7\u0b95\u0bae\u0bcd', startingWave: '\u0ba4\u0bca\u0b9f\u0b95\u0bcd\u0b95 \u0b85\u0bb2\u0bc8', headshotMultiplier: '\u0ba4\u0bb2\u0bc8\u0b9a\u0bcd \u0b9a\u0bc1\u0b9f\u0bc1 \u0baa\u0bc6\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bbf', katanaSpeed: '\u0b95\u0b9f\u0bcd\u0b9f\u0bbe\u0ba9\u0bbe \u0bb5\u0bc7\u0b95\u0bae\u0bcd', lighting: '\u0b92\u0bb3\u0bbf', coolWhite: '\u0b95\u0bc1\u0bb3\u0bbf\u0bb0\u0bcd \u0bb5\u0bc6\u0bb3\u0bcd\u0bb3\u0bc8', warm: '\u0bb5\u0bc6\u0baa\u0bcd\u0baa\u0bae\u0bcd', emergencyRed: '\u0b85\u0bb5\u0b9a\u0bb0 \u0b9a\u0bbf\u0bb5\u0baa\u0bcd\u0baa\u0bc1', nightVision: '\u0b87\u0bb0\u0bb5\u0bc1 \u0baa\u0bbe\u0bb0\u0bcd\u0bb5\u0bc8' },\n                    weapons: { PISTOL: '\u0bb8\u0bcd\u0bae\u0bbe\u0bb0\u0bcd\u0b9f\u0bcd \u0b95\u0bc8\u0ba4\u0bcd\u0ba4\u0bc1\u0baa\u0bcd\u0baa\u0bbe\u0b95\u0bcd\u0b95\u0bbf', SHOTGUN: '\u0bb7\u0bbe\u0b9f\u0bcd\u0b95\u0ba9\u0bcd', SNIPER: '\u0bb8\u0bcd\u0ba8\u0bc8\u0baa\u0bcd\u0baa\u0bb0\u0bcd', KATANA: '\u0b95\u0b9f\u0bcd\u0b9f\u0bbe\u0ba9\u0bbe', GATLING: '\u0b95\u0bc7\u0b9f\u0bcd\u0bb2\u0bbf\u0b99\u0bcd', LMG: 'LMG', 'AUTO RIFLE': '\u0ba4\u0bbe\u0ba9\u0bbf\u0baf\u0b99\u0bcd\u0b95\u0bbf \u0bb0\u0bc8\u0b83\u0baa\u0bbf\u0bb3\u0bcd', FLAMETHROWER: '\u0b83\u0baa\u0bbf\u0bb3\u0bc7\u0bae\u0bcd\u0ba4\u0bcd\u0bb0\u0bcb\u0bb5\u0bb0\u0bcd' },\n                    zombies: { Walker: '\u0ba8\u0b9f\u0baa\u0bcd\u0baa\u0bb5\u0ba9\u0bcd', Runner: '\u0b93\u0b9f\u0bc1\u0baa\u0bb5\u0ba9\u0bcd', Tank: '\u0b9f\u0bc7\u0b99\u0bcd\u0b95\u0bcd', Crawler: '\u0ba4\u0bb5\u0bb4\u0bcd\u0baa\u0bb5\u0ba9\u0bcd', Berserker: '\u0bb5\u0bc6\u0bb1\u0bbf\u0baf\u0ba9\u0bcd', Armored: '\u0b95\u0bb5\u0b9a\u0bae\u0bbf\u0b9f\u0bcd\u0b9f\u0bb5\u0ba9\u0bcd', Swarm: '\u0b95\u0bc2\u0b9f\u0bcd\u0b9f\u0bae\u0bcd', Bloater: '\u0bb5\u0bc0\u0b99\u0bcd\u0b95\u0bbf\u0baf\u0bb5\u0ba9\u0bcd', Stalker: '\u0baa\u0bbf\u0ba9\u0bcd\u0ba4\u0bca\u0b9f\u0bb0\u0bcd\u0baa\u0bb5\u0ba9\u0bcd', Screamer: '\u0b95\u0ba4\u0bcd\u0ba4\u0bc1\u0baa\u0bb5\u0ba9\u0bcd', BRUTE: '\u0bae\u0bc1\u0bb0\u0b9f\u0bcd\u0b9f\u0ba9\u0bcd', CHARGER: '\u0baa\u0bbe\u0baf\u0bcd\u0bb5\u0bb5\u0ba9\u0bcd', BEHEMOTH: '\u0baa\u0bc6\u0bb0\u0bc1\u0b99\u0bcd\u0b95\u0bbe\u0baf\u0ba9\u0bcd', INFERNAL: '\u0ba8\u0bb0\u0b95\u0ba9\u0bcd', OVERLORD: '\u0bae\u0bc7\u0bb2\u0bbe\u0ba4\u0bbf\u0baa\u0ba4\u0bbf' }\n                },\n                it: {\n                    app: { title: 'Ultimo Sopravvissuto', titleUpper: 'ULTIMO SOPRAVVISSUTO', titleBreak: 'ULTIMO<br>SOPRAVVISSUTO' },\n                    tutorial: { title: '\u3010TUTORIAL\u3011NUOVA ARMA', desc: 'Hai ottenuto una nuova arma! Tocca l\\'icona evidenziata in basso o premi [2] per selezionarla e continuare.' },\n                    loading: { assets: 'Caricamento risorse...', assetsProgress: 'Caricamento risorse... {pct}%' },\n                    start: { subtitle: 'Difendi il bunker da orde di zombie senza fine', play: 'GIOCA' },\n                    hud: { wave: 'ONDATA {wave}', waveBoss: 'ONDATA {wave} - BOSS: {boss}', waveInitial: 'ONDATA 1', score: 'PUNTEGGIO', interact: 'Premi E per interagire', fire: 'FUOCO', reloadButton: 'R', scopeZoom: 'x4', reloadSuffix: 'RICARICA {weapon}', seconds: '{value}s' },\n                    gameOver: { title: 'BUNKER VIOLATO', retry: 'RIPROVA' },\n                    stats: { wave: 'Ondata', kills: 'Uccisioni', headshots: 'Colpi alla testa', score: 'Punteggio', accuracy: 'Precisione' },\n                    weaponSelect: { title: 'SCEGLI ARMA', subtitle: 'Scegli la tua arma iniziale' },\n                    upgrade: { waveCompleteInitial: 'ONDATA COMPLETATA!', waveComplete: 'ONDATA {wave} COMPLETATA!', choose: 'Scegli un potenziamento', lifestealName: 'RUBA-VITA+', lifestealDesc: 'Cura a ogni colpo {from}% -> {to}%', damageName: '{weapon} DANNO+', damageDesc: 'Danno {from} -> {to}', reloadName: '{weapon} RICARICA+', reloadDesc: 'Ricarica {from}s -> {to}s', magazineName: '{weapon} CARICATORE+', magazineDesc: 'Caricatore {from} -> {to}', pierceName: '{weapon} PERFORAZIONE+', pierceDesc: 'Perforazione {from} -> {to}', fireRateName: '{weapon} CADENZA+', fireRateDesc: 'Intervallo {from}ms -> {to}ms', speedName: '{weapon} VELOCIT\u00c0+', rangeName: '{weapon} PORTATA+', rangeDesc: 'Portata {from}m -> {to}m', newWeaponName: 'NUOVA ARMA: {weapon}', slowTimeName: '\u3010ABILIT\u00c0\u3011DILATAZIONE TEMPORALE', slowTimeDesc: 'Il mondo rallenta di 100 volte, il giocatore non \u00e8 influenzato. Dura 5s.', flameRangeName: '[Lanciafiamme] Compressione Carburante', flameRangeDesc: 'Aumenta la portata del +40% (velocit\u00e0 +15%) e il danno diretto del +20% (Livello {level})', flameStickyName: '[Lanciafiamme] Fuoco Appiccicoso', flameStickyDesc: 'Aumenta la durata della combustione di +2s e il danno nel tempo del +50% (Livello {level})', flameSpreadName: '[Lanciafiamme] Ugello Ampio', flameSpreadDesc: 'Aumenta la dimensione della fiamma e l\\'area di collisione del +35% (Livello {level})', flameEmberName: '[Lanciafiamme] Rinascita della Brace', flameEmberDesc: 'I nemici in fiamme hanno il 30% di probabilit\u00e0 di incendiare i nemici adiacenti', flameLavaName: '[Lanciafiamme] Lava ad Alta Temperatura', flameLavaDesc: 'Le fiamme ignorano completamente l\\'armatura nemica e sciolgono il 15% dell\\'armatura massima al secondo', flameRangeName: '[Lanciafiamme] Compressione Carburante', flameRangeDesc: 'Aumenta la portata del +40% (velocit\u00e0 +15%) e il danno diretto del +20% (Livello {level})', flameStickyName: '[Lanciafiamme] Fuoco Appiccicoso', flameStickyDesc: 'Aumenta la durata della combustione di +2s e il danno nel tempo del +50% (Livello {level})', flameSpreadName: '[Lanciafiamme] Ugello Ampio', flameSpreadDesc: 'Aumenta la dimensione della fiamma e l\\'area di collisione del +35% (Livello {level})', flameEmberName: '[Lanciafiamme] Rinascita della Brace', flameEmberDesc: 'I nemici in fiamme hanno il 30% di probabilit\u00e0 di incendiare i nemici adiacenti', flameLavaName: '[Lanciafiamme] Lava ad Alta Temperatura', flameLavaDesc: 'Le fiamme ignorano completamente l\\'armatura nemica e sciolgono il 15% dell\\'armatura massima al secondo' },\n                    weaponStats: { melee: 'MISCHIA', auto: 'AUTO', semi: 'SEMI', damage: 'DAN:{value}', range: 'Portata:{value}m', magazine: 'Caric.:{value}' },\n                    edit: { title: 'MODALIT\u00c0 MODIFICA', line1: 'Usa il pannello impostazioni per configurare i parametri di gioco.', line2: 'Regola difficolt\u00e0 zombie, ondata iniziale, armi e illuminazione.', architecture: 'Architettura: ECS + Web Worker + Object Pool' },\n                    params: { name: 'Ultimo Sopravvissuto', zombieHealth: 'Salute Zombie', zombieSpeed: 'Velocit\u00e0 Zombie', startingWave: 'Ondata Iniziale', headshotMultiplier: 'Moltiplicatore Colpo alla Testa', katanaSpeed: 'Velocit\u00e0 Katana', lighting: 'Illuminazione', coolWhite: 'Bianco Freddo', warm: 'Calda', emergencyRed: 'Rosso Emergenza', nightVision: 'Visione Notturna' },\n                    weapons: { PISTOL: 'PISTOLA INTELLIGENTE', SHOTGUN: 'FUCILE A POMPA', SNIPER: 'FUCILE DI PRECISIONE', KATANA: 'KATANA', GATLING: 'GATLING', LMG: 'LMG', 'AUTO RIFLE': 'FUCILE AUTO', FLAMETHROWER: 'LANCIAFIAMME' },\n                    zombies: { Walker: 'Camminatore', Runner: 'Corridore', Tank: 'Tank', Crawler: 'Strisciante', Berserker: 'Berserker', Armored: 'Corazzato', Swarm: 'Sciame', Bloater: 'Gonfio', Stalker: 'Predatore', Screamer: 'Urlatore', BRUTE: 'BRUTO', CHARGER: 'ARIETE', BEHEMOTH: 'COLOSSO', INFERNAL: 'INFERNALE', OVERLORD: 'SIGNORE' }\n                }\n            };\n            const extraTranslations = {\n                en: {\n                    hud: { hits: 'HITS' },\n                    gameOver: {\n                        normal: \"You did well, but it wasn't enough...\",\n                        top10: \"You were so close to being first, try again!\",\n                        first: \"You are the ultimate victor, congratulations on getting first place!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010PASSIVE\u3011AUTO-FIRE',\n                        autoFireDesc: 'Auto-fires when aiming at zombies, auto-slashes when zombies are in range. Weapon damage is reduced by 20%; Smart Pistol is excluded.',\n                        smartPistolName: '\u3010PISTOL UPGRADE\u3011SMART PISTOL',\n                        smartPistolDesc: 'Pistol upgrade: Lock on and target enemy heads within brackets. Fired bullets track and curve towards marked heads!'\n                    },\n                    pause: { title: 'PAUSED', continue: 'CONTINUE', restart: 'RESTART', settings: 'SETTINGS', language: 'LANGUAGE', back: '\u2190 BACK', bgm: 'Background Music', weapon: 'Weapon SFX', monster: 'Monster SFX', ui: 'UI SFX', visual: 'Visual Intensity', normal: 'Normal', soft: 'Soft', portrait: 'Portrait', landscape: 'Landscape' }\n                },\n                zh: {\n                    hud: { hits: '\u8fde\u51fb' },\n                    gameOver: {\n                        normal: \"\u4f60\u505a\u7684\u5f88\u597d\uff0c\u4f46\u662f\u8fd8\u4e0d\u591f...\",\n                        top10: \"\u4f60\u5dee\u4e00\u70b9\u5c31\u662f\u7b2c\u4e00\u4e86,\u518d\u8bd5\u8bd5\",\n                        first: \"\u4f60\u662f\u6700\u7ec8\u7684\u80dc\u5229\u8005\uff0c\u606d\u559c\u4f60\u83b7\u5f97\u4e86\u7b2c\u4e00\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010\u88ab\u52a8\u3011\u81ea\u52a8\u5f00\u706b',\n                        autoFireDesc: '\u51c6\u661f\u5bf9\u51c6\u50f5\u5c38\u65f6\u81ea\u52a8\u5c04\u51fb\uff0c\u5200\u5728\u653b\u51fb\u8303\u56f4\u5185\u81ea\u52a8\u6325\u780d\u3002\u4f46\u4f24\u5bb3\u6c38\u4e45\u964d\u4f4e20%\uff1b\u667a\u6167\u624b\u67aa\u4e0d\u53d7\u5f71\u54cd\u3002',\n                        smartPistolName: '\u3010\u624b\u67aa\u5347\u7ea7\u3011\u667a\u6167\u624b\u67aa',\n                        smartPistolDesc: '\u624b\u67aa\u5347\u7ea7\uff1a\u83b7\u5f97\u6218\u672f\u8ffd\u8e2a\u9501\u3002\u81ea\u52a8\u9501\u5b9a\u9501\u5b9a\u6846\u5185\u7684\u654c\u4eba\u5934\u90e8\uff0c\u5b50\u5f39\u8ffd\u8e2a\u7206\u5934\uff01'\n                    },\n                    pause: { title: '\u6682\u505c', continue: '\u7ee7\u7eed', restart: '\u91cd\u65b0\u5f00\u59cb', settings: '\u8bbe\u7f6e', language: '\u8bed\u8a00', back: '\u2190 \u8fd4\u56de', bgm: '\u80cc\u666f\u97f3\u4e50', weapon: '\u6b66\u5668\u97f3\u6548', monster: '\u602a\u7269\u97f3\u6548', ui: 'UI\u97f3\u6548', visual: '\u89c6\u89c9\u5f3a\u5ea6', normal: '\u6807\u51c6', soft: '\u67d4\u548c', portrait: '\u7ad6\u5c4f', landscape: '\u6a2a\u5c4f' }\n                },\n                ja: {\n                    hud: { hits: '\u30b3\u30f3\u30dc' },\n                    gameOver: {\n                        normal: \"\u5065\u95d8\u3057\u307e\u3057\u305f\u304c\u3001\u307e\u3060\u53ca\u3073\u307e\u305b\u3093\u3067\u3057\u305f...\",\n                        top10: \"1\u4f4d\u307e\u3067\u3042\u3068\u4e00\u6b69\u3067\u3057\u305f\uff01\u3082\u3046\u4e00\u5ea6\u6311\u6226\u3057\u307e\u3057\u3087\u3046\uff01\",\n                        first: \"\u3042\u306a\u305f\u304c\u7a76\u6975 of \u8987\u8005\u3067\u3059\uff011\u4f4d\u7372\u5f97\u304a\u3081\u3067\u3068\u3046\u3054\u3056\u3044\u307e\u3059\uff01\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010\u30d1\u30c3\u30b7\u30d6\u3011\u81ea\u52d5\u5c04\u6483',\n                        autoFireDesc: '\u7167\u6e96\u67a0\u5185\u306e\u6575\u3092\u81ea\u52d5\u5c04\u6483\u3057\u3001\u7bc4\u56f2\u5185\u306b\u5165\u308b\u3068\u81ea\u52d5\u3067\u65ac\u6483\u3057\u307e\u3059\u3002\u6b66\u5668\u5a01\u529b\u304c20%\u6e1b\u5c11\u3057\u307e\u3059\uff08\u30b9\u30de\u30fc\u30c8\u30d4\u30b9\u30c8\u30eb\u3092\u9664\u304f\uff09\u3002',\n                        smartPistolName: '\u3010\u30d4\u30b9\u30c8\u30eb\u5f37\u5316\u3011\u30b9\u30de\u30fc\u30c8\u30d4\u30b9\u30c8\u30eb',\n                        smartPistolDesc: '\u30d4\u30b9\u30c8\u30eb\u5f37\u5316\uff1a\u30bf\u30af\u30c6\u30a3\u30ab\u30eb\u30ed\u30c3\u30af\u3092\u7372\u5f97\u3002\u7167\u6e96\u67a0\u5185\u306e\u6575\u306e\u982d\u90e8\u3092\u81ea\u52d5\u30ed\u30c3\u30af\u3057\u3001\u5f3e\u4e38\u304c\u8ffd\u5c3e\u3057\u3066\u30d8\u30c3\u30c9\u30b7\u30e7\u30c3\u30c8\u3057\u307e\u3059\uff01'\n                    },\n                    pause: {\n                        title: '\u4e00\u6642\u505c\u6b62', continue: '\u7d9a\u3051\u308b', restart: '\u3084\u308a\u76f4\u3059', settings: '\u8a2d\u5b9a', language: '\u8a00\u8a9e', back: '\u2190 \u623b\u308b', bgm: 'BGM', weapon: '\u6b66\u5668\u52b9\u679c\u97f3', monster: '\u30e2\u30f3\u30b9\u30bf\u30fc\u52b9\u679c\u97f3', ui: 'UI\u52b9\u679c\u97f3', visual: '\u753b\u8cea\u8a2d\u5b9a', normal: '\u6a19\u6e96', soft: '\u8efd\u91cf', portrait: '\u7e26\u753b\u9762', landscape: '\u6a2a\u753b\u9762'\n                    }\n                },\n                id: {\n                    hud: { hits: 'HITS' },\n                    gameOver: {\n                        normal: \"Kamu sudah berusaha dengan baik, tapi itu belum cukup...\",\n                        top10: \"Kamu hampir menjadi nomor satu, coba lagi!\",\n                        first: \"Kamu adalah pemenang sejati, selamat atas peringkat pertama!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010PASIF\u3011TEMBAK OTOMATIS',\n                        autoFireDesc: 'Menembak otomatis saat membidik zombi, menebas otomatis saat zombi dalam jangkauan. Kerusakan senjata berkurang 20%; Pistol Pintar dikecualikan.',\n                        smartPistolName: '\u3010PENINGKATAN PISTOL\u3011PISTOL PINTAR',\n                        smartPistolDesc: 'Peningkat pistol: Mengunci dan membidik kepala musuh dalam tanda kurung. Peluru yang ditembakkan melacak dan melengkung ke arah kepala yang ditandai!'\n                    },\n                    pause: {\n                        title: 'JEDA', continue: 'LANJUT', restart: 'MULAI ULANG', settings: 'PENGATURAN', language: 'BAHASA', back: '\u2190 KEMBALI', bgm: 'Musik Latar', weapon: 'SFX Senjata', monster: 'SFX Monster', ui: 'SFX UI', visual: 'Intensitas Visual', normal: 'Normal', soft: 'Lembut', portrait: 'Potret', landscape: 'Lanskap'\n                    }\n                },\n                ru: {\n                    hud: { hits: '\u0423\u0414\u0410\u0420\u041e\u0412' },\n                    gameOver: {\n                        normal: \"\u0412\u044b \u043e\u0442\u043b\u0438\u0447\u043d\u043e \u0441\u043f\u0440\u0430\u0432\u0438\u043b\u0438\u0441\u044c, \u043d\u043e \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e...\",\n                        top10: \"\u0412\u044b \u0431\u044b\u043b\u0438 \u0442\u0430\u043a \u0431\u043b\u0438\u0437\u043a\u0438 \u043a \u043f\u0435\u0440\u0432\u043e\u043c\u0443 \u043c\u0435\u0441\u0442\u0443, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437!\",\n                        first: \"\u0412\u044b \u2014 \u0430\u0431\u0441\u043e\u043b\u044e\u0442\u043d\u044b\u0439 \u043f\u043e\u0431\u0435\u0434\u0438\u0442\u0435\u043b\u044c, \u043f\u043e\u0437\u0434\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0441 \u043f\u0435\u0440\u0432\u044b\u043c \u043c\u0435\u0441\u0442\u043e\u043c!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010\u041f\u0410\u0421\u0421\u0418\u0412\u041d\u042b\u0419\u3011\u0410\u0412\u0422\u041e\u041e\u0413\u041e\u041d\u042c',\n                        autoFireDesc: '\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u0442\u0440\u0435\u043b\u044f\u0435\u0442 \u043f\u0440\u0438 \u043f\u0440\u0438\u0446\u0435\u043b\u0438\u0432\u0430\u043d\u0438\u0438 \u0432 \u0437\u043e\u043c\u0431\u0438, \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0431\u044c\u0435\u0442 \u043a\u0430\u0442\u0430\u043d\u043e\u0439 \u0432\u0431\u043b\u0438\u0437\u0438. \u0423\u0440\u043e\u043d \u043e\u0440\u0443\u0436\u0438\u044f \u0441\u043d\u0438\u0436\u0435\u043d \u043d\u0430 20%, \u043a\u0440\u043e\u043c\u0435 \u0423\u043c\u043d\u043e\u0433\u043e \u043f\u0438\u0441\u0442\u043e\u043b\u0435\u0442\u0430.',\n                        smartPistolName: '\u3010\u0423\u041b\u0423\u0427\u0428\u0415\u041d\u0418\u0415 \u041f\u0418\u0421\u0422\u041e\u041b\u0415\u0422\u0410\u3011\u0423\u041c\u041d\u042b\u0419 \u041f\u0418\u0421\u0422\u041e\u041b\u0415\u0422',\n                        smartPistolDesc: '\u0423\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u0435 \u043f\u0438\u0441\u0442\u043e\u043b\u0435\u0442\u0430: \u0437\u0430\u0445\u0432\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0433\u043e\u043b\u043e\u0432\u044b \u0432\u0440\u0430\u0433\u043e\u0432 \u0432 \u0440\u0430\u043c\u043a\u0443. \u0412\u044b\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0435 \u043f\u0443\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043d\u0430\u0432\u043e\u0434\u044f\u0442\u0441\u044f \u043d\u0430 \u043e\u0442\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0435 \u0433\u043e\u043b\u043e\u0432\u044b!'\n                    },\n                    pause: {\n                        title: '\u041f\u0410\u0423\u0417\u0410', continue: '\u041f\u0420\u041e\u0414\u041e\u041b\u0416\u0418\u0422\u042c', restart: '\u0417\u0410\u041d\u041e\u0412\u041e', settings: '\u041d\u0410\u0421\u0422\u0420\u041e\u0419\u041a\u0418', language: '\u042f\u0417\u042b\u041a', back: '\u2190 \u041d\u0410\u0417\u0410\u0414', bgm: '\u0424\u043e\u043d\u043e\u0432\u0430\u044f \u043c\u0443\u0437\u044b\u043a\u0430', weapon: '\u0417\u0432\u0443\u043a\u0438 \u043e\u0440\u0443\u0436\u0438\u044f', monster: '\u0417\u0432\u0443\u043a\u0438 \u043c\u043e\u043d\u0441\u0442\u0440\u043e\u0432', ui: '\u0417\u0432\u0443\u043a\u0438 UI', visual: '\u0418\u043d\u0442\u0435\u043d\u0441\u0438\u0432\u043d\u043e\u0441\u0442\u044c \u044d\u0444\u0444\u0435\u043a\u0442\u043e\u0432', normal: '\u041e\u0431\u044b\u0447\u043d\u0430\u044f', soft: '\u041c\u044f\u0433\u043a\u0430\u044f', portrait: '\u041f\u043e\u0440\u0442\u0440\u0435\u0442', landscape: '\u0410\u043b\u044c\u0431\u043e\u043c\u043d\u0430\u044f'\n                    }\n                },\n                de: {\n                    hud: { hits: 'TREFFER' },\n                    gameOver: {\n                        normal: \"Du hast dich gut geschlagen, aber das war leider nicht genug...\",\n                        top10: \"Du warst so nah dran, Erster zu sein, versuch es noch mal!\",\n                        first: \"Du bist der ultimative Sieger, herzlichen Gl\u00fcckwunsch zum ersten Platz!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010PASSIV\u3011AUTO-FEUER',\n                        autoFireDesc: 'Schie\u00dft automatisch, wenn auf Zombies gezielt wird, und schl\u00e4gt automatisch zu, wenn sie in Reichweite sind. Waffenschaden ist um 20% reduziert, au\u00dfer bei der Smart-Pistole.',\n                        smartPistolName: '\u3010PISTOLEN-UPGRADE\u3011SMART-PISTOLE',\n                        smartPistolDesc: 'Pistolen-Upgrade: Erfasst und visiert die K\u00f6pfe von Feinden in der Klammer an. Abgefeuerte Kugeln verfolgen markierte K\u00f6pfe!'\n                    },\n                    pause: {\n                        title: 'PAUSE', continue: 'WEITER', restart: 'NEU STARTEN', settings: 'EINSTELLUNGEN', language: 'SPRACHE', back: '\u2190 ZUR\u00dcCK', bgm: 'Hintergrundmusik', weapon: 'Waffen-SFX', monster: 'Monster-SFX', ui: 'UI-SFX', visual: 'Visuelle Intensit\u00e4t', normal: 'Normal', soft: 'Weich', portrait: 'Hochformat', landscape: 'Querformat'\n                    }\n                },\n                tr: {\n                    hud: { hits: 'H\u0130T' },\n                    gameOver: {\n                        normal: \"\u0130yi m\u00fccadele ettin ama bu kadar\u0131 yetmedi...\",\n                        top10: \"Birinci olmaya \u00e7ok yakla\u015ft\u0131n, tekrar dene!\",\n                        first: \"E\u015fsiz kazanan sensin, birincili\u011fin i\u00e7in tebrikler!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010PAS\u0130F\u3011OTOMAT\u0130K ATI\u015e',\n                        autoFireDesc: 'Zombileri hedeflerken otomatik ate\u015f eder, yak\u0131ndayken otomatik keser. Silah hasar\u0131 %20 azal\u0131r; Ak\u0131ll\u0131 Tabanca hari\u00e7tir.',\n                        smartPistolName: '\u3010TABANCA Y\u00dcKSELTMES\u0130\u3011AKILLI TABANCA',\n                        smartPistolDesc: 'Tabanca y\u00fckseltmesi: Parantez i\u00e7indeki zombi kafalar\u0131n\u0131 kilitler ve hedefler. At\u0131lan mermiler i\u015faretli kafalara do\u011fru k\u0131vr\u0131larak y\u00f6nelir!'\n                    },\n                    pause: {\n                        title: 'DURDURULDU', continue: 'DEVAM', restart: 'YEN\u0130DEN', settings: 'AYARLAR', language: 'D\u0130L', back: '\u2190 GER\u0130', bgm: 'Arka Plan M\u00fczi\u011fi', weapon: 'Silah Sesleri', monster: 'Canavar Sesleri', ui: 'UI Sesleri', visual: 'G\u00f6rsel Yo\u011funluk', normal: 'Normal', soft: 'Yumu\u015fak', portrait: 'Dikey', landscape: 'Yatay'\n                    }\n                },\n                it: {\n                    hud: { hits: 'COLPI' },\n                    gameOver: {\n                        normal: \"Hai combattuto bene, ma purtroppo non \u00e8 bastato...\",\n                        top10: \"Sei andato vicinissimo al primo posto, riprova!\",\n                        first: \"Sei il vincitore supremo, congratulazioni per il primo posto!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010PASSIVO\u3011FUOCO AUTOMATICO',\n                        autoFireDesc: 'Spara automaticamente quando miri ai zombie, fende automaticamente quando sono a portata. Il danno dell\\'arma \u00e8 ridotto del 20%; esclusa la Pistola Intelligente.',\n                        smartPistolName: '\u3010POTENZIAMENTO PISTOLA\u3011PISTOLA INTELLIGENTE',\n                        smartPistolDesc: 'Potenziamento pistola: Aggancia le teste dei nemici tra le parentesi. I proiettili sparati tracciano e curvano verso le teste contrassegnate!'\n                    },\n                    pause: {\n                        title: 'PAUSA', continue: 'CONTINUA', restart: 'RICOMINCIA', settings: 'IMPOSTAZIONI', language: 'LINGUA', back: '\u2190 INDIETRO', bgm: 'Musica di sfondo', weapon: 'SFX Armi', monster: 'SFX Mostri', ui: 'SFX UI', visual: 'Intensit\u00e0 Visiva', normal: 'Normale', soft: 'Morbida', portrait: 'Verticale', landscape: 'Orizzontale'\n                    }\n                },\n                vi: {\n                    hud: { hits: 'LI\u00caN HO\u00c0N' },\n                    gameOver: {\n                        normal: \"B\u1ea1n \u0111\u00e3 chi\u1ebfn \u0111\u1ea5u r\u1ea5t t\u1ed1t, nh\u01b0ng v\u1eabn ch\u01b0a \u0111\u1ee7...\",\n                        top10: \"B\u1ea1n su\u00fdt ch\u00fat n\u1eefa l\u00e0 \u0111\u1ee9ng \u0111\u1ea7u r\u1ed3i, th\u1eed l\u1ea1i xem sao!\",\n                        first: \"B\u1ea1n l\u00e0 ng\u01b0\u1eddi chi\u1ebfn th\u1eafng cu\u1ed1i c\u00f9ng, ch\u00fac m\u1eebng b\u1ea1n \u0111\u1ea1t h\u1ea1ng nh\u1ea5t!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010B\u1eca \u0110\u1ed8NG\u3011T\u1ef0 \u0110\u1ed8NG B\u1eaeN',\n                        autoFireDesc: 'T\u1ef1 \u0111\u1ed9ng b\u1eafn khi nh\u1eafm v\u00e0o zombie, t\u1ef1 \u0111\u1ed9ng ch\u00e9m khi ch\u00fang \u1edf trong t\u1ea7m c\u1eadn chi\u1ebfn. S\u00e1t th\u01b0\u01a1ng v\u0169 kh\u00ed gi\u1ea3m 20%; S\u00fang l\u1ee5c th\u00f4ng minh \u0111\u01b0\u1ee3c lo\u1ea1i tr\u1eeb.',\n                        smartPistolName: '\u3010N\u00c2NG C\u1ea4P S\u00daNG L\u1ee4C\u3011S\u00daNG L\u1ee4C TH\u00d4NG MINH',\n                        smartPistolDesc: 'N\u00e2ng c\u1ea5p s\u00fang l\u1ee5c: T\u1ef1 \u0111\u1ed9ng kh\u00f3a v\u00e0 nh\u1eafm v\u00e0o \u0111\u1ea7u k\u1ebb \u0111\u1ecbch trong khung ng\u1eafm. \u0110\u1ea1n b\u1eafn ra s\u1ebd t\u1ef1 \u0111\u1ed9ng t\u00ecm m\u1ee5c ti\u00eau t\u00ecm \u0111\u1ea7u k\u1ebb \u0111\u1ecbch!'\n                    },\n                    pause: {\n                        title: 'T\u1ea0M D\u1eeaNG', continue: 'TI\u1ebeP T\u1ee4C', restart: 'CH\u01a0I L\u1ea0I', settings: 'C\u00c0I \u0110\u1eb6T', language: 'NG\u00d4N NG\u1eee', back: '\u2190 QUAY L\u1ea0I', bgm: 'Nh\u1ea1c n\u1ec1n', weapon: '\u00c2m thanh v\u0169 kh\u00ed', monster: '\u00c2m thanh qu\u00e1i v\u1eadt', ui: '\u00c2m thanh UI', visual: 'C\u01b0\u1eddng \u0111\u1ed9 h\u00ecnh \u1ea3nh', normal: 'B\u00ecnh th\u01b0\u1eddng', soft: 'D\u1ecbu m\u1eaft', portrait: 'D\u1ecdc', landscape: 'Ngang'\n                    }\n                },\n                ur: {\n                    hud: { hits: '\u06c1\u091f\u094d\u0938' },\n                    gameOver: {\n                        normal: \"\u0622\u067e \u0646\u06d2 \u0628\u06c1\u062a \u0627\u0686\u06be\u0627 \u06a9\u06be\u06cc\u0644 \u067e\u06cc\u0634 \u06a9\u06cc\u0627\u060c \u0644\u06cc\u06a9\u0646 \u06cc\u06c1 \u06a9\u0627\u0641\u06cc \u0646\u06c1\u06cc\u06ba \u062a\u06be\u0627...\",\n                        top10: \"\u0622\u067e \u067e\u06c1\u0644\u06d2 \u0646\u0645\u0628\u0631 \u067e\u0631 \u0622\u0646\u06d2 \u06a9\u06d2 \u0628\u0627\u0644\u06a9\u0644 \u0642\u0631\u06cc\u0628 \u062a\u06be\u06d2\u060c \u062f\u0648\u0628\u0627\u0631\u06c1 \u06a9\u0648\u0634\u0634 \u06a9\u0631\u06cc\u06ba!\",\n                        first: \"\u0622\u067e \u06c1\u06cc \u062d\u062a\u0645\u06cc \u0641\u0627\u062a\u062d \u06c1\u06cc\u06ba\u060c \u067e\u06c1\u0644\u0627 \u0645\u0642\u0627\u0645 \u062d\u0627\u0635\u0644 \u06a9\u0631\u0646\u06d2 \u067e\u0631 \u0628\u06c1\u062a \u0628\u06c1\u062a \u0645\u0628\u0627\u0631\u06a9 \u06c1\u0648!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010\u063a\u06cc\u0631 \u0641\u0639\u0627\u0644\u3011\u0622\u0679\u0648 \u0641\u0627\u0626\u0631',\n                        autoFireDesc: '\u0632\u0648\u0645\u0628\u06cc\u0648\u06ba \u067e\u0631 \u0646\u0634\u0627\u0646\u06c1 \u0644\u06af\u0627\u062a\u06d2 \u0648\u0642\u062a \u0622\u0679\u0648 \u0641\u0627\u0626\u0631\u060c \u0631\u06cc\u0646\u062c \u0645\u06cc\u06ba \u06c1\u0648\u0646\u06d2 \u067e\u0631 \u0622\u0679\u0648 \u0633\u0644\u06cc\u0634\u06d4 \u06c1\u062a\u06be\u06cc\u0627\u0631 \u06a9\u0627 \u0646\u0642\u0635\u0627\u0646 %20 \u062a\u06a9 \u06a9\u0645 \u06c1\u0648 \u06af\u06cc\u0627\u061b \u0627\u0633\u0645\u0627\u0631\u0679 \u067e\u0633\u062a\u0648\u0644 \u0634\u0627\u0645\u0644 \u0646\u06c1\u06cc\u06ba \u06c1\u06d2\u06d4',\n                        smartPistolName: '\u3010\u067e\u0633\u062a\u0648\u0644 \u0627\u067e \u06af\u0631\u06cc\u0688\u3011\u0627\u0633\u0645\u0627\u0631\u0679 \u067e\u0633\u062a\u0648\u0644',\n                        smartPistolDesc: '\u067e\u0633\u062a\u0648\u0644 \u0627\u067e \u06af\u0631\u06cc\u0688: \u0627\u0633\u06a9\u0648\u0627\u0626\u0631 \u0628\u0631\u06cc\u06a9\u0679\u0633 \u0645\u06cc\u06ba \u062f\u0634\u0645\u0646\u0648\u06ba \u06a9\u06d2 \u0633\u0631\u0648\u06ba \u067e\u0631 \u0644\u0627\u06a9 \u0622\u0646 \u06a9\u0631\u06cc\u06ba\u06d4 \u0641\u0627\u0626\u0631 \u06a9\u06cc \u06af\u0626\u06cc \u06af\u0648\u0644\u06cc\u0627\u06ba \u0646\u0634\u0627\u0646 \u0632\u062f\u06c1 \u0633\u0631\u0648\u06ba \u06a9\u06cc \u0637\u0631\u0641 \u0645\u0691\u062a\u06cc \u06c1\u06cc\u06ba!'\n                    },\n                    pause: {\n                        title: '\u0645\u0648\u0642\u0648\u0641', continue: '\u062c\u0627\u0631\u06cc \u0631\u06a9\u06be\u06cc\u06ba', restart: '\u062f\u0648\u0628\u0627\u0631\u06c1 \u0634\u0631\u0648\u0639', settings: '\u062a\u0631\u062a\u06cc\u0628\u0627\u062a', language: '\u0632\u0628\u0627\u0646', back: '\u2190 \u0648\u0627\u067e\u0633', bgm: '\u067e\u0633 \u0645\u0646\u0638\u0631 \u0645\u0648\u0633\u06cc\u0642\u06cc', weapon: '\u06c1\u062a\u06be\u06cc\u0627\u0631 \u0622\u0648\u0627\u0632', monster: '\u0639\u0641\u0631\u06cc\u062a \u0622\u0648\u0627\u0632', ui: 'UI \u0622\u0648\u0627\u0632', visual: '\u0628\u0635\u0631\u06cc \u0634\u062f\u062a', normal: '\u0639\u0627\u0645', soft: '\u0646\u0631\u0645', portrait: '\u067e\u0648\u0631\u0679\u0631\u06cc\u0679', landscape: '\u0644\u06cc\u0646\u0688 \u0627\u0633\u06a9\u06cc\u067e'\n                    }\n                },\n                bn: {\n                    hud: { hits: '\u0986\u0998\u09be\u09a4' },\n                    gameOver: {\n                        normal: \"\u0986\u09aa\u09a8\u09bf \u0985\u09a8\u09c7\u0995 \u099a\u09c7\u09b7\u09cd\u099f\u09be \u0995\u09b0\u09c7\u099b\u09c7\u09a8, \u09a4\u09ac\u09c7 \u09a4\u09be \u09af\u09a5\u09c7\u09b7\u09cd\u099f \u099b\u09bf\u09b2 \u09a8\u09be...\",\n                        top10: \"\u0986\u09aa\u09a8\u09bf \u09aa\u09cd\u09b0\u09a5\u09ae \u09b9\u0993\u09df\u09be\u09b0 \u0996\u09c1\u09ac \u0995\u09be\u099b\u09be\u0995\u09be\u099b\u09bf \u099b\u09bf\u09b2\u09c7\u09a8, \u0986\u09ac\u09be\u09b0 \u099a\u09c7\u09b7\u09cd\u099f\u09be \u0995\u09b0\u09c1\u09a8!\",\n                        first: \"\u0986\u09aa\u09a8\u09bf\u0987 \u099a\u09dc\u09ae \u09ac\u09bf\u099c\u09df\u09c0, \u09aa\u09cd\u09b0\u09a5\u09ae \u09b8\u09cd\u09a5\u09be\u09a8 \u0985\u09a7\u09bf\u0995\u09be\u09b0 \u0995\u09b0\u09be\u09b0 \u099c\u09a8\u09cd\u09af \u0985\u09ad\u09bf\u09a8\u09a8\u09cd\u09a6\u09a8!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010\u09aa\u09cd\u09af\u09be\u09b8\u09bf\u09ad\u3011\u0985\u099f\u09cb-\u09ab\u09be\u09af\u09bc\u09be\u09b0',\n                        autoFireDesc: '\u099c\u09ae\u09cd\u09ac\u09bf\u09a6\u09c7\u09b0 \u09b2\u0995\u09cd\u09b7\u09cd\u09af \u0995\u09b0\u09be\u09b0 \u09b8\u09ae\u09af\u09bc \u0985\u099f\u09cb-\u09ab\u09be\u09af\u09bc\u09be\u09b0 \u098f\u09ac\u0982 \u099c\u09ae\u09cd\u09ac\u09bf\u09b0\u09be \u09aa\u09be\u09b2\u09cd\u09b2\u09be\u09b0 \u09ae\u09a7\u09cd\u09af\u09c7 \u09a5\u09be\u0995\u09b2\u09c7 \u0985\u099f\u09cb-\u09b8\u09cd\u09b2\u09cd\u09af\u09be\u09b6\u0964 \u0985\u09b8\u09cd\u09a4\u09cd\u09b0\u09c7\u09b0 \u0995\u09cd\u09b7\u09a4\u09bf \u09e8\u09e6% \u09b9\u09cd\u09b0\u09be\u09b8 \u09aa\u09be\u09ac\u09c7; \u09b8\u09cd\u09ae\u09be\u09b0\u09cd\u099f \u09aa\u09bf\u09b8\u09cd\u09a4\u09b2\u099f\u09bf \u09ac\u09cd\u09af\u09a4\u09bf\u0995\u09cd\u09b0\u09ae\u0964',\n                        smartPistolName: '\u3010\u09aa\u09bf\u09b8\u09cd\u09a4\u09b2 \u0986\u09aa\u0997\u09cd\u09b0\u09c7\u09a1\u3011\u09b8\u09cd\u09ae\u093e\u0930\u094d\u091f \u09aa\u09bf\u09b8\u09cd\u09a4\u09b2',\n                        smartPistolDesc: '\u09aa\u09bf\u09b8\u09cd\u09a4\u09b2 \u0986\u09aa\u0997\u09cd\u09b0\u09c7\u09a1: \u09ac\u09cd\u09b0\u09cd\u09af\u09be\u0995\u09c7\u099f\u09c7\u09b0 \u09ae\u09a7\u09cd\u09af\u09c7 \u09a5\u09be\u0995\u09be \u09b6\u09a4\u09cd\u09b0\u09c1\u09a6\u09c7\u09b0 \u09ae\u09be\u09a5\u09be\u09af\u09bc \u09b2\u0995\u09cd\u09b7\u09cd\u09af \u09b8\u09cd\u09a5\u09bf\u09b0 \u0995\u09b0\u09c1\u09a8\u0964 \u09ab\u09be\u09af\u09bc\u09be\u09b0 \u0995\u09b0\u09be \u0997\u09c1\u09b2\u09bf\u0997\u09c1\u09b2\u09bf \u099a\u09bf\u09b9\u09cd\u09a8\u09bf\u09a4 \u09ae\u09be\u09a5\u09be\u09b0 \u09a6\u09bf\u0995\u09c7 \u09b2\u0995\u09cd\u09b7\u09cd\u09af \u0995\u09b0\u09c7 \u09a7\u09be\u09ac\u09bf\u09a4 \u09b9\u09ac\u09c7!'\n                    },\n                    pause: {\n                        title: '\u09ac\u09bf\u09b0\u09a4\u09bf', continue: '\u099a\u09be\u09b2\u09bf\u09af\u09bc\u09c7 \u09af\u09be\u0993', restart: '\u0986\u09ac\u09be\u09b0 \u09b6\u09c1\u09b0\u09c1', settings: '\u09b8\u09c7\u099f\u09bf\u0982\u09b8', language: '\u09ad\u09be\u09b7\u09be', back: '\u2190 \u09ab\u09bf\u09b0\u09c7 \u09af\u09be\u0993', bgm: '\u09ac\u09cd\u09af\u09be\u0995\u0997\u09cd\u09b0\u09be\u0989\u09a8\u09cd\u09a1 \u09ae\u09bf\u0989\u099c\u09bf\u0995', weapon: '\u0985\u09b8\u09cd\u09a4\u09cd\u09b0 \u09b6\u09ac\u09cd\u09a6', monster: '\u09a6\u09be\u09a8\u09ac \u09b6\u09ac\u09cd\u09a6', ui: 'UI \u09b6\u09ac\u09cd\u09a6', visual: '\u09ad\u09bf\u099c\u09cd\u09af\u09c1\u09af\u09bc\u09be\u09b2 \u09a4\u09c0\u09ac\u09cd\u09b0\u09a4\u09be', normal: '\u09b8\u09be\u09a7\u09be\u09b0\u09a3', soft: '\u09a8\u09b0\u09ae', portrait: '\u09b2\u09ae\u09cd\u09ac\u09be\u09b2\u09ae\u09cd\u09ac\u09bf', landscape: '\u0986\u09dc\u09be\u0986\u09dc\u09bf'\n                    }\n                },\n                sw: {\n                    hud: { hits: 'HITS' },\n                    gameOver: {\n                        normal: \"Ulifanya vizuri sana, lakini haitoshi...\",\n                        top10: \"Ulikuwa karibu sana kuwa wa kwanza, jaribu tena!\",\n                        first: \"Wewe ndiye mshindi mkuu, hongera kwa ushindi wa kwanza!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010KALE\u3011KUFYATUA OTO',\n                        autoFireDesc: 'Hufyatua kiotomatiki unapofungia zombi, na kukata kiotomatiki wanapokuwa karibu. Uharibifu wa silaha unapungua kwa 20%; Bastola ya Kijanja haijumuishwi.',\n                        smartPistolName: '\u3010UBORESHAJI WA BASTOLA\u3011BASTOLA YA KIJANJA',\n                        smartPistolDesc: 'Uboreshaji wa bastola: Hufunga na kulenga vichwa vya maadui kwenye mabano. Risasi zinazofyatuliwa hufuata na kujipinda kuelekea kichwa kilicholengwa!'\n                    },\n                    pause: {\n                        title: 'SIMAMA', continue: 'ENDELEA', restart: 'ANZA UPYA', settings: 'MIPANGILIO', language: 'LUGHA', back: '\u2190 RUDI', bgm: 'Muziki wa Mandhari', weapon: 'Sauti za Silaha', monster: 'Sauti za Zombi', ui: 'Sauti za UI', visual: 'Ukali wa Visual', normal: 'Kawaida', soft: 'Laini', portrait: 'Wima', landscape: 'Mlalo'\n                    }\n                },\n                te: {\n                    hud: { hits: '\u0c39\u0c3f\u0c1f\u0c4d\u0c38\u0c4d' },\n                    gameOver: {\n                        normal: \"\u0c2e\u0c40\u0c30\u0c41 \u0c2c\u0c3e\u0c17\u0c3e\u0c28\u0c47 \u0c06\u0c21\u0c3e\u0c30\u0c41, \u0c15\u0c3e\u0c28\u0c40 \u0c05\u0c26\u0c3f \u0c38\u0c30\u0c3f\u0c2a\u0c4b\u0c32\u0c47\u0c26\u0c41...\",\n                        top10: \"\u0c2e\u0c40\u0c30\u0c41 \u0c2e\u0c4a\u0c26\u0c1f\u0c3f \u0c38\u0c4d\u0c25\u0c3e\u0c28\u0c3e\u0c28\u0c3f\u0c15\u0c3f \u0c1a\u0c3e\u0c32\u0c3e \u0c26\u0c17\u0c4d\u0c17\u0c30\u0c17\u0c3e \u0c35\u0c1a\u0c4d\u0c1a\u0c3e\u0c30\u0c41, \u0c2e\u0c33\u0c4d\u0c32\u0c40 \u0c2a\u0c4d\u0c30\u0c2f\u0c24\u0c4d\u0c28\u0c3f\u0c02\u0c1a\u0c02\u0c21\u0c3f!\",\n                        first: \"\u0c2e\u0c40\u0c30\u0c47 \u0c05\u0c02\u0c24\u0c3f\u0c2e \u0c35\u0c3f\u0c1c\u0c47\u0c24, \u0c2e\u0c4a\u0c26\u0c1f\u0c3f \u0c38\u0c4d\u0c25\u0c3e\u0c28\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c38\u0c3e\u0c27\u0c3f\u0c02\u0c1a\u0c3f\u0c28\u0c02\u0c26\u0c41\u0c15\u0c41 \u0c05\u0c2d\u0c3f\u0c28\u0c02\u0c26\u0c28\u0c32\u0c41!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010\u0c28\u0c3f\u0c37\u0c4d\u0c15\u0c4d\u0c30\u0c3f\u0c2f\u0c3e\u0c24\u0c4d\u0c2e\u0c15\u3011\u0c06\u0c1f\u0c4b-\u0c2b\u0c48\u0c30\u0c4d',\n                        autoFireDesc: '\u0c1c\u0c3e\u0c02\u0c2c\u0c40\u0c32\u0c28\u0c41 \u0c17\u0c41\u0c30\u0c3f \u0c1a\u0c42\u0c38\u0c47\u0c1f\u0c2a\u0c4d\u0c2a\u0c41\u0c21\u0c41 \u0c06\u0c1f\u0c4b\u0c2e\u0c47\u0c1f\u0c3f\u0c15\u0c4d \u0c15\u0c3e\u0c32\u0c4d\u0c2a\u0c41\u0c32\u0c41, \u0c2a\u0c30\u0c3f\u0c27\u0c3f\u0c32\u0c4b\u0c15\u0c3f \u0c35\u0c38\u0c4d\u0c24\u0c47 \u0c06\u0c1f\u0c4b\u0c2e\u0c47\u0c1f\u0c3f\u0c15\u0c4d \u0c26\u0c3e\u0c21\u0c3f. \u0c06\u0c2f\u0c41\u0c27 \u0c28\u0c37\u0c4d\u0c1f\u0c02 20% \u0c24\u0c17\u0c4d\u0c17\u0c41\u0c24\u0c41\u0c02\u0c26\u0c3f; \u0c38\u0c4d\u0c2e\u0c3e\u0c30\u0c4d\u0c1f\u0c4d \u0c2a\u0c3f\u0c38\u0c4d\u0c1f\u0c32\u0c4d\u200c\u0c15\u0c41 \u0c2e\u0c3f\u0c28\u0c39\u0c3e\u0c2f\u0c3f\u0c02\u0c2a\u0c41.',\n                        smartPistolName: '\u3010\u0c2a\u0c3f\u0c38\u0c4d\u0c1f\u0c32\u0c4d \u0c05\u0c2a\u0c4d\u200c\u0c17\u0c4d\u0c30\u0c47\u0c21\u0c4d\u3011\u0c38\u0c4d\u0c2e\u0c3e\u0c30\u0c4d\u0c1f\u0c4d \u0c2a\u0c3f\u0c38\u0c4d\u0c1f\u0c32\u0c4d',\n                        smartPistolDesc: '\u0c2a\u0c3f\u0c38\u0c4d\u0c1f\u0c32\u0c4d \u0c05\u0c2a\u0c4d\u200c\u0c17\u0acd\u0ab0\u0ac7\u0c21\u0c4d: \u0c2c\u0c4d\u0c30\u0c3e\u0c15\u0c46\u0c1f\u0c4d\u0c32 \u0c2a\u0c30\u0c3f\u0c27\u0c3f\u0c32\u0c4b\u0c28\u0c3f \u0c36\u0c24\u0c4d\u0c30\u0c41\u0c35\u0c41\u0c32 \u0c24\u0c32\u0c32\u0c2a\u0c48 \u0c32\u0c3e\u0c15\u0c4d \u0c06\u0c28\u0c4d \u0c1a\u0c47\u0c2f\u0c02\u0c21\u0c3f. \u0c2a\u0c47\u0c32\u0c4d\u0c1a\u0c3f\u0c28 \u0c2c\u0c41\u0c32\u0c4d\u0c32\u0c46\u0c1f\u0c4d\u0c32\u0c41 \u0c24\u0c32\u0c32 \u0c35\u0c48\u0c2a\u0c41\u0c28\u0c15\u0c41 \u0c26\u0c42\u0c38\u0c41\u0c15\u0c41\u0c2a\u0c4b\u0c24\u0c3e\u0c2f\u0c3f!'\n                    },\n                    pause: {\n                        title: '\u0c2a\u0c3e\u0c1c\u0c4d', continue: '\u0c15\u0c4a\u0c28\u0c38\u0c3e\u0c17\u0c3f\u0c02\u0c1a\u0c41', restart: '\u0c2e\u0c33\u0c4d\u0c32\u0c40 \u0c2a\u0c4d\u0c30\u0c3e\u0c30\u0c02\u0c2d\u0c3f\u0c02\u0c1a\u0c41', settings: '\u0c38\u0c46\u0c1f\u0c4d\u0c1f\u0c3f\u0c02\u0c17\u0c41\u0c32\u0c41', language: '\u0c2d\u0c3e\u0c37', back: '\u2190 \u0c35\u0c46\u0c28\u0c41\u0c15\u0c15\u0c41', bgm: '\u0c28\u0c47\u0c2a\u0c25\u0c4d\u0c2f \u0c38\u0c02\u0c17\u0c40\u0c24\u0c02', weapon: '\u0c06\u0c2f\u0c41\u0c27 \u0c36\u0c2c\u0c4d\u0c26\u0c3e\u0c32\u0c41', monster: '\u0c1c\u0c3e\u0c02\u0c2c\u0c40 \u0c36\u0c2c\u0c4d\u0c26\u0c3e\u0c32\u0c41', ui: 'UI \u0c36\u0c2c\u0c4d\u0c26\u0c3e\u0c32\u0c41', visual: '\u0c26\u0c43\u0c36\u0c4d\u0c2f \u0c24\u0c40\u0c35\u0c4d\u0c30\u0c24', normal: '\u0c38\u0c3e\u0c27\u0c3e\u0c30\u0c23\u0c02', soft: '\u0c2e\u0c43\u0c26\u0c41\u0c35\u0c48\u0c28', portrait: '\u0c28\u0c3f\u0c32\u0c41\u0c35\u0c41', landscape: '\u0c05\u0c21\u0c4d\u0c21\u0c02\u0c17\u0c3e'\n                    }\n                },\n                mr: {\n                    hud: { hits: '\u0939\u093f\u091f\u094d\u0938' },\n                    gameOver: {\n                        normal: \"\u0924\u0941\u092e\u094d\u0939\u0940 \u091a\u093e\u0902\u0917\u0932\u0940 \u091d\u0941\u0902\u091c \u0926\u093f\u0932\u0940, \u092a\u0923 \u0924\u0947 \u092a\u0941\u0930\u0947\u0938\u0947 \u0928\u0935\u094d\u0939\u0924\u0947...\",\n                        top10: \"\u0924\u0941\u092e\u094d\u0939\u0940 \u092a\u0939\u093f\u0932\u094d\u092f\u093e \u0915\u094d\u0930\u092e\u093e\u0902\u0915\u093e\u091a\u094d\u092f\u093e \u0905\u0917\u0926\u0940 \u091c\u0935\u0933 \u0939\u094b\u0924\u093e, \u092a\u0941\u0928\u094d\u0939\u093e \u092a\u094d\u0930\u092f\u0924\u094d\u0928 \u0915\u0930\u093e...\",\n                        first: \"\u0924\u0941\u092e\u094d\u0939\u0940\u091a \u0905\u0902\u0924\u093f\u092e \u0935\u093f\u091c\u0947\u0924\u0947 \u0906\u0939\u093e\u0924, \u092a\u094d\u0930\u0925\u092e \u0915\u094d\u0930\u092e\u093e\u0902\u0915 \u092e\u093f\u0933\u0935\u0932\u094d\u092f\u093e\u092c\u0926\u094d\u0926\u0932 \u0905\u092d\u093f\u0928\u0902\u0926\u0928!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010\u092a\u0945\u0938\u093f\u0935\u094d\u0939\u3011\u0911\u091f\u094b-\u092b\u093e\u092f\u0930',\n                        autoFireDesc: '\u091d\u0949\u092e\u094d\u092c\u0940\u0902\u0928\u093e \u0932\u0915\u094d\u0937\u094d\u092f \u0915\u0930\u0924\u093e\u0928\u093e \u0911\u091f\u094b-\u092b\u093e\u092f\u0930 \u0906\u0923\u093f \u0924\u0947 \u091c\u0935\u0933 \u0905\u0938\u0924\u093e\u0928\u093e \u0911\u091f\u094b-\u0938\u0945\u0932\u0936. \u0936\u0938\u094d\u0924\u094d\u0930\u093e\u091a\u0947 \u0928\u0941\u0915\u0938\u093e\u0928 \u0968\u0966% \u0928\u0947 \u0915\u092e\u0940 \u0939\u094b\u0908\u0932; \u0938\u094d\u092e\u093e\u0930\u094d\u091f \u092a\u093f\u0938\u094d\u0924\u0942\u0932 \u0935\u0917\u0933\u0924\u093e.',\n                        smartPistolName: '\u3010\u092a\u093f\u0938\u094d\u0924\u0942\u0932 \u0905\u092a\u0917\u094d\u0930\u0947\u0921\u3011\u0938\u094d\u092e\u093e\u0930\u094d\u091f \u092a\u093f\u0938\u094d\u0924\u0942\u0932',\n                        smartPistolDesc: '\u092a\u093f\u0938\u094d\u0924\u0942\u0932 \u0905\u092a\u0917\u094d\u0930\u0947\u0921: \u0915\u0902\u0938\u093e\u0924\u0940\u0932 \u0936\u0924\u094d\u0930\u0942\u0902\u091a\u094d\u092f\u093e \u0921\u094b\u0915\u094d\u092f\u093e\u0935\u0930 \u0932\u0949\u0915 \u0911\u0928 \u0915\u0930\u093e. \u0917\u094b\u0933\u094d\u092f\u093e \u091d\u093e\u0921\u0932\u094d\u092f\u093e\u0928\u0902\u0924\u0930 \u0924\u094d\u092f\u093e \u0921\u094b\u0915\u094d\u092f\u093e\u0902\u091a\u094d\u092f\u093e \u0926\u0947\u0936\u093e\u0928\u0947 \u092e\u093e\u0930\u094d\u0917 \u092c\u0926\u0932\u0942\u0928 \u0927\u093e\u0935\u0924\u0940\u0932!'\n                    },\n                    pause: {\n                        title: '\u0925\u093e\u0902\u092c\u0935\u093e', continue: '\u0938\u0941\u0930\u0942 \u0920\u0947\u0935\u093e', restart: '\u092a\u0941\u0928\u094d\u0939\u093e \u0938\u0941\u0930\u0942 \u0915\u0930\u093e', settings: '\u0938\u0947\u091f\u093f\u0902\u0917\u094d\u091c', language: '\u092d\u093e\u0937\u093e', back: '\u2190 \u092e\u093e\u0917\u0947', bgm: '\u092a\u093e\u0930\u094d\u0936\u094d\u0935\u0938\u0902\u0917\u0940\u0924', weapon: '\u0936\u0938\u094d\u0924\u094d\u0930 \u0906\u0935\u093e\u091c', monster: '\u091d\u0949\u092e\u094d\u092c\u0940 \u0906\u0935\u093e\u091c', ui: 'UI \u0906\u0935\u093e\u091c', visual: '\u0935\u094d\u0939\u093f\u091c\u094d\u092f\u0941\u0905\u0932 \u0924\u0940\u0935\u094d\u0930\u0924\u093e', normal: '\u0938\u093e\u092e\u093e\u0928\u094d\u092f', soft: '\u092e\u090a', portrait: '\u092a\u094b\u0930\u094d\u091f\u094d\u0930\u0947\u091f', landscape: '\u0932\u0901\u0921\u0938\u094d\u0915\u0947\u092a'\n                    }\n                },\n                ta: {\n                    hud: { hits: '\u0bb9\u0bbf\u0b9f\u0bcd\u0bb8\u0bcd' },\n                    gameOver: {\n                        normal: \"\u0ba8\u0bc0\u0b99\u0bcd\u0b95\u0bb3\u0bcd \u0ba8\u0ba9\u0bcd\u0bb1\u0bbe\u0b95 \u0bb5\u0bbf\u0bb3\u0bc8\u0baf\u0bbe\u0b9f\u0bbf\u0ba9\u0bc0\u0bb0\u0bcd\u0b95\u0bb3\u0bcd, \u0b86\u0ba9\u0bbe\u0bb2\u0bcd \u0b85\u0ba4\u0bc1 \u0baa\u0bcb\u0ba4\u0bbe\u0ba4\u0bc1...\",\n                        top10: \"\u0ba8\u0bc0\u0b99\u0bcd\u0b95\u0bb3\u0bcd \u0bae\u0bc1\u0ba4\u0bb2\u0bbf\u0b9f\u0ba4\u0bcd\u0ba4\u0bbf\u0bb1\u0bcd\u0b95\u0bc1 \u0bae\u0bbf\u0b95 \u0b85\u0bb0\u0bc1\u0b95\u0bbf\u0bb2\u0bcd \u0bb5\u0ba8\u0bcd\u0ba4\u0bc0\u0bb0\u0bcd\u0b95\u0bb3\u0bcd, \u0bae\u0bc0\u0ba3\u0bcd\u0b9f\u0bc1\u0bae\u0bcd \u0bae\u0bc1\u0baf\u0bb1\u0bcd\u0b9a\u0bbf\u0b95\u0bcd\u0b95\u0bb5\u0bc1\u0bae\u0bcd!\",\n                        first: \"\u0ba8\u0bc0\u0b99\u0bcd\u0b95\u0bb3\u0bc7 \u0b87\u0bb1\u0bc1\u0ba4\u0bbf \u0bb5\u0bc6\u0bb1\u0bcd\u0bb1\u0bbf\u0baf\u0bbe\u0bb3\u0bb0\u0bcd, \u0bae\u0bc1\u0ba4\u0644\u06cc\u0b9f\u0bae\u0bcd \u0baa\u0bbf\u0b9f\u0bbf\u0ba4\u0bcd\u0ba4\u0ba4\u0bb1\u0bcd\u0b95\u0bc1 \u0bb5\u0bbe\u0bb4\u0bcd\u0ba4\u0bcd\u0ba4\u0bc1\u0b95\u0bb3\u0bcd!\"\n                    },\n                    upgrade: {\n                        autoFireName: '\u3010\u0b9a\u0bc6\u0baf\u0bb2\u0bb1\u0bcd\u0bb1\u3011\u0b86\u0b9f\u0bcd\u0b9f\u0bcb-\u0b83\u0baa\u0baf\u0bb0\u0bcd',\n                        autoFireDesc: '\u0b9a\u0bcb\u0bae\u0bcd\u0baa\u0bbf\u0b95\u0bb3\u0bc8 \u0b87\u0bb2\u0b95\u0bcd\u0b95\u0bc1 \u0bb5\u0bc8\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd\u0baa\u0bcb\u0ba4\u0bc1 \u0ba4\u0bbe\u0ba9\u0bbe\u0b95\u0bb5\u0bc7 \u0b9a\u0bc1\u0b9f\u0bc1\u0bae\u0bcd, \u0b8e\u0bb2\u0bcd\u0bb2\u0bc8\u0b95\u0bcd\u0b95\u0bc1\u0bb3\u0bcd \u0bb5\u0bb0\u0bc1\u0bae\u0bcd\u0baa\u0bcb\u0ba4\u0bc1 \u0ba4\u0bbe\u0ba9\u0bbe\u0b95\u0bb5\u0bc7 \u0bb5\u0bc6\u0b9f\u0bcd\u0b9f\u0bc1\u0bae\u0bcd. \u0b86\u0baf\u0bc1\u0ba4 \u0b9a\u0bc7\u0ba4\u0bae\u0bcd 20% \u0b95\u0bc1\u0bb1\u0bc8\u0baf\u0bc1\u0bae\u0bcd; \u0bb8\u0bcd\u0bae\u0bbe\u0bb0\u0bcd\u0b9f\u0bcd \u0b95\u0bc8\u0ba4\u0bcd\u0ba4\u0bc1\u0baa\u0bcd\u0baa\u0bbe\u0b95\u0bcd\u0b95\u0bbf \u0ba4\u0bb5\u0bbf\u0bb0\u0bcd\u0ba4\u0bcd\u0ba4\u0bc1.',\n                        smartPistolName: '\u3010\u0b95\u0bc8\u0ba4\u0bcd\u0ba4\u0bc1\u0baa\u0bcd\u0baa\u0bbe\u0b95\u0bcd\u0b95\u0bbf \u0bae\u0bc7\u0bae\u0bcd\u0baa\u0bbe\u0b9f\u0bc1\u3011\u0bb8\u0bcd\u0bae\u0bbe\u0bb0\u0bcd\u0b9f\u0bcd \u0b95\u0bc8\u0ba4\u0bcd\u0ba4\u0bc1\u0baa\u0bcd\u0baa\u0bbe\u0b95\u0bcd\u0b95\u0bbf',\n                        smartPistolDesc: '\u0b95\u0bc8\u0ba4\u0bcd\u0ba4\u0bc1\u0baa\u0bcd\u0baa\u0bbe\u0b95\u0bcd\u0b95\u0bbf \u0bae\u0bc7\u0bae\u0bcd\u0baa\u0bbe\u0b9f\u0bc1: \u0b85\u0b9f\u0bc8\u0baa\u0bcd\u0baa\u0bc1\u0b95\u0bcd\u0b95\u0bc1\u0bb1\u0bbf\u0b95\u0bcd\u0b95\u0bc1\u0bb3\u0bcd \u0b87\u0bb0\u0bc1\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd \u0b8e\u0ba4\u0bbf\u0bb0\u0bbf\u0b95\u0bb3\u0bbf\u0ba9\u0bcd \u0ba4\u0bb2\u0bc8\u0b95\u0bb3\u0bc8 \u0b87\u0bb2\u0b95\u0bcd\u0b95\u0bc1 \u0bb5\u0bc8\u0b95\u0bcd\u0b95\u0bc1\u0bae\u0bcd. \u0b9a\u0bc1\u0b9f\u0baa\u0bcd\u0baa\u0b9f\u0bc1\u0bae\u0bcd \u0ba4\u0bcb\u0637\u0629\u0b95\u0bcd\u0b95\u0bb3\u0bcd \u0b95\u0bc1\u0bb1\u0bbf\u0bb5\u0bc8\u0b95\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0b9f\u0bcd\u0b9f \u0ba4\u0bb2\u0bc8\u0b95\u0bb3\u0bc8 \u0ba8\u0bcb\u0b95\u0bcd\u0b95\u0bbf \u0bb5\u0bb3\u0bc8\u0ba8\u0bcd\u0ba4\u0bc1 \u0b9a\u0bc6\u0bb2\u0bcd\u0bb2\u0bc1\u0bae\u0bcd!'\n                    },\n                    pause: {\n                        title: '\u0b87\u0b9f\u0bc8\u0ba8\u0bbf\u0bb1\u0bc1\u0ba4\u0bcd\u0ba4\u0bae\u0bcd', continue: '\u0ba4\u0bca\u0b9f\u0bb0\u0bcd', restart: '\u0bae\u0bc0\u0ba3\u0bcd\u0b9f\u0bc1\u0bae\u0bcd \u0ba4\u0bca\u0b9f\u0b99\u0bcd\u0b95\u0bc1', settings: '\u0b85\u0bae\u0bc8\u0baa\u0bcd\u0baa\u0bc1\u0b95\u0bb3\u0bcd', language: '\u0bae\u0bca\u0bb4\u0bbf', back: '\u0b85\u0bae\u0bc8\u0baa\u0bcd\u0baa\u0bc1\u0b95\u0bb3\u0bcd', bgm: '\u0baa\u0bbf\u0ba9\u0bcd\u0ba9\u0ba3\u0bbf \u0b87\u0b9a\u0bc8', weapon: '\u0b86\u0baf\u0bc1\u0ba4 \u0b92\u0bb2\u0bbf\u0b95\u0bb3\u0bcd', monster: '\u0b9c\u0bbe\u0bae\u0bcd\u0baa\u0bbf \u0b92\u0bb2\u0bbf\u0b95\u0bb3\u0bcd', ui: 'UI \u0b92\u0bb2\u0bbf\u0b95\u0bb3\u0bcd', visual: '\u0b95\u0bbe\u0b9f\u0bcd\u0b9a\u0bbf \u0ba4\u0bc0\u0bb5\u0bbf\u0bb0\u0bae\u0bcd', normal: '\u0b9a\u0bbe\u0ba4\u0bbe\u0bb0\u0ba3', soft: '\u0bae\u0bc6\u0ba9\u0bcd\u0bae\u0bc8\u0baf\u0bbe\u0ba9', portrait: '\u0b9a\u0bc6\u0b99\u0bcd\u0b95\u0bc1\u0ba4\u0bcd\u0ba4\u0bc1', landscape: '\u0b95\u0bbf\u0b9f\u0bc8\u0bae\u0b9f\u0bcd\u0b9f'\n                    }\n                }\n            };\n            function deepMerge(target, source) {\n                for (const key in source) {\n                    if (source[key] && typeof source[key] === 'object') {\n                        if (!target[key]) target[key] = {};\n                        deepMerge(target[key], source[key]);\n                    } else {\n                        target[key] = source[key];\n                    }\n                }\n            }\n            deepMerge(TEXT, extraTranslations.en);\n            for (const loc in extraTranslations) {\n                if (TEXTS[loc]) {\n                    deepMerge(TEXTS[loc], extraTranslations[loc]);\n                }\n            }\n            const LOCALE_ALIASES = { in: 'id' };\n            const NON_LATIN_LOCALES = new Set(['ur', 'bn', 'ja', 'te', 'mr', 'ta', 'zh']);\n            function normalizeLocale(locale) {\n                if (!locale || typeof locale !== 'string') return null;\n                const lower = locale.toLowerCase();\n                const exact = LOCALE_ALIASES[lower] || lower;\n                if (TEXTS[exact]) return exact;\n                const primary = exact.split('-')[0];\n                return TEXTS[LOCALE_ALIASES[primary] || primary] ? (LOCALE_ALIASES[primary] || primary) : null;\n            }\n            function detectLocale() {\n                const candidates = Array.isArray(navigator.languages) && navigator.languages.length\n                    ? navigator.languages\n                    : [navigator.language || navigator.userLanguage || 'en'];\n                for (const locale of candidates) {\n                    const normalized = normalizeLocale(locale);\n                    if (normalized) return normalized;\n                }\n                return 'en';\n            }\n            let currentLocale = detectLocale();\n            let screenOrientationMode = 'portrait';\n            let visualIntensityMode = 'normal';\n            let runtimePerfVisualScale = 1;\n            let sniperScoped = false;\n            let currentText = TEXTS[currentLocale] || TEXT;\n\n            // --- Auto-Fire and Smart Pistol States ---\n            let hasAutoFirePassive = false;\n            let hasSmartPistol = true;\n            let smartPistolLockSpeedMultiplier = 1.0;\n            const SMART_PISTOL_LOCK_CAPACITY = 220;\n            const SMART_PISTOL_QUEUE_CAPACITY = SMART_PISTOL_LOCK_CAPACITY * 3;\n            const SMART_QUEUE_FULL_LOCK = 1;\n            const SMART_QUEUE_FINAL_SHOT = 2;\n            const smartPistolTargets = new Int16Array(SMART_PISTOL_LOCK_CAPACITY);\n            let smartPistolTargetCount = 0;\n            let smartPistolLockCount = 0;\n            const smartLockSlot = new Int16Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockProgress = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockRequiredLocks = new Uint8Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockScreenX = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockScreenY = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockMarkerSize = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockScore = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockCenterNorm = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockDist = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockLostTime = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartLockIndexBySlot = new Int16Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateSlot = new Int16Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateRequiredLocks = new Uint8Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateOrder = new Int16Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartSelectedCandidateIndex = new Int16Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartReadyLockOrder = new Int16Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartActiveSlotFlags = new Uint8Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateScore = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateScreenX = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateScreenY = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateMarkerSize = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateCenterNorm = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateDist = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartCandidateLockRate = new Float32Array(SMART_PISTOL_LOCK_CAPACITY);\n            const smartPistolQueueSlot = new Int16Array(SMART_PISTOL_QUEUE_CAPACITY);\n            const smartPistolQueueEntityId = new Int32Array(SMART_PISTOL_QUEUE_CAPACITY);\n            const smartPistolQueueRequiredLocks = new Uint8Array(SMART_PISTOL_QUEUE_CAPACITY);\n            const smartPistolQueueFlags = new Uint8Array(SMART_PISTOL_QUEUE_CAPACITY);\n            const smartPistolShotScratch = { slot: -1, entityId: -1, fullLock: false, requiredLocks: 1, finalShot: false };\n            smartLockIndexBySlot.fill(-1);\n            smartSelectedCandidateIndex.fill(-1);\n            smartPistolQueueSlot.fill(-1);\n            smartPistolQueueEntityId.fill(-1);\n            let smartPistolQueueHead = 0;\n            let smartPistolQueueCount = 0;\n            let lastSmartPistolLockTime = 0;\n            const smartMarkerCache = new Map();\n            // Pre-allocated pool for Smart Pistol HUD markers; avoids createElement/remove churn.\n            // Initialized lazily on first updateSmartPistolLocks call.\n            let smartMarkerPool = null;\n            let smartContainerEl = null; // cached getElementById reference\n            let lastLockDiameter = 0;\n            let lastSmartHudState = '';\n            let cachedGameWidth = 0;\n            let cachedGameHeight = 0;\n            let cachedIsLandscape = false;\n            const SMART_PISTOL_MAX_TARGETS = 6;\n            const SMART_PISTOL_RANGE = 30;\n            const SMART_PISTOL_LOST_GRACE = 0.22;\n            const SMART_PISTOL_DECAY_RATE = 2.6;\n            const SMART_PISTOL_MIN_LOCK_RATE = 0.42;\n            const SMART_PISTOL_CENTER_LOCK_RATE = 1.65;\n            const SMART_PISTOL_DISTANCE_LOCK_RATE = 0.75;\n            const SMART_PISTOL_SHOT_INTERVAL = 0.095;\n            const SMART_PISTOL_BULLET_SPEED = 42;\n            const SMART_PISTOL_BULLET_SIZE = 0.046;\n            const SMART_PISTOL_BULLET_LENGTH = 0.62;\n            const SMART_PISTOL_TRACER_COLOR = 0x55f6ff;\n            const SMART_PISTOL_INITIAL_STRAIGHT_TIME = 0.09;\n            const SMART_PISTOL_MIN_STRAIGHT_TIME = 0.012;\n            const SMART_PISTOL_MUZZLE_CLEARANCE = 0.34;\n            const SMART_PISTOL_MAX_STRAIGHT_DISTANCE = 2.7;\n            const SMART_PISTOL_CLOSE_STEER_RADIUS = 5.0;\n            const SMART_PISTOL_CLOSE_STEER_BOOST = 3.8;\n            const SMART_PISTOL_STEER_RATE = 7.2;\n\n            // --- Slow Time Skill Variables ---\n            let hasSlowTimeSkill = false;\n            let slowTimeActive = false;\n            let slowTimeDuration = 5.0;\n            let slowTimeTimer = 0;\n            let slowTimeCooldown = 15.0;\n            let slowTimeCooldownTimer = 0;\n\n            // --- Weapon Tutorial Variables ---\n            let weaponTutorialActive = false;\n            let weaponTutorialShown = false;\n            let tutorialSpotlightScale = 6.0;\n            let tutorialSpotlightCompleteTimer = -1;\n            let tutorialHighlightActive = false;\n\n            function getVisualIntensityScale() {\n                const budgetScale = runtimePerfVisualScale || 1;\n                return (visualIntensityMode === 'soft' ? 0.55 : 1) * budgetScale;\n            }\n\n            function getVisualFlashScale() {\n                const budgetScale = runtimePerfVisualScale || 1;\n                return (visualIntensityMode === 'soft' ? 0.42 : 1) * (0.72 + budgetScale * 0.28);\n            }\n\n            function getCameraShakeIntensityScale() {\n                return (visualIntensityMode === 'soft' ? 0.24 : 0.82) * (0.7 + (runtimePerfVisualScale || 1) * 0.3);\n            }\n\n            function getCameraShakeDurationScale() {\n                return (visualIntensityMode === 'soft' ? 0.55 : 0.82) * (0.82 + (runtimePerfVisualScale || 1) * 0.18);\n            }\n\n            function getCameraShakeMaxScale() {\n                return (visualIntensityMode === 'soft' ? 0.22 : 0.68) * (0.7 + (runtimePerfVisualScale || 1) * 0.3);\n            }\n\n            function getVisualShakeFollowSpeed() {\n                return (visualIntensityMode === 'soft' ? 9 : 16) * (0.85 + (runtimePerfVisualScale || 1) * 0.15);\n            }\n\n            function applyCurrentLocaleToDocument() {\n                document.documentElement.lang = currentLocale;\n                document.documentElement.dir = currentLocale === 'ur' ? 'rtl' : 'ltr';\n                if (NON_LATIN_LOCALES.has(currentLocale)) document.documentElement.classList.add('locale-font-fallback');\n                else document.documentElement.classList.remove('locale-font-fallback');\n                currentText = TEXTS[currentLocale] || TEXT;\n                document.title = t('app.title');\n                applyStaticText();\n            }\n\n            function clampVolumeValue(value) {\n                const n = Number(value);\n                if (!Number.isFinite(n)) return null;\n                return Math.max(0, Math.min(1, n));\n            }\n\n            function updateVolumeSliderUIFromState() {\n                const sliderMap = [\n                    { id: 'vol-bgm', valueId: 'vol-bgm-value', key: 'bgm' },\n                    { id: 'vol-weapon', valueId: 'vol-weapon-value', key: 'weapon' },\n                    { id: 'vol-monster', valueId: 'vol-monster-value', key: 'monster' },\n                    { id: 'vol-ui', valueId: 'vol-ui-value', key: 'ui' }\n                ];\n                sliderMap.forEach(({ id, valueId, key }) => {\n                    const slider = document.getElementById(id);\n                    const valueEl = document.getElementById(valueId);\n                    if (!slider || !valueEl) return;\n                    const percent = Math.round((volumeMultipliers[key] || 0) * 100);\n                    slider.value = percent;\n                    valueEl.textContent = percent + '%';\n                });\n            }\n\n            async function saveUserPreferences() {\n                try {\n                    await lib.saveUserGameState({\n                        locale: currentLocale,\n                        orientation: screenOrientationMode,\n                        visualIntensity: visualIntensityMode,\n                        volumes: {\n                            bgm: volumeMultipliers.bgm,\n                            weapon: volumeMultipliers.weapon,\n                            monster: volumeMultipliers.monster,\n                            ui: volumeMultipliers.ui\n                        }\n                    });\n                } catch (error) {\n                    lib.log(`Failed to save user preferences: ${error.message}`);\n                }\n            }\n\n            async function loadUserPreferences() {\n                try {\n                    const response = await lib.getUserGameState();\n                    const state = response && response.state;\n                    if (!state || typeof state !== 'object') return;\n\n                    if (state.locale) {\n                        const normalizedLocale = normalizeLocale(state.locale);\n                        if (normalizedLocale) currentLocale = normalizedLocale;\n                    }\n                    if (state.orientation === 'landscape' || state.orientation === 'portrait') {\n                        screenOrientationMode = state.orientation;\n                    }\n                    if (state.visualIntensity === 'soft' || state.visualIntensity === 'normal') {\n                        visualIntensityMode = state.visualIntensity;\n                    }\n\n                    if (state.volumes && typeof state.volumes === 'object') {\n                        ['bgm', 'weapon', 'monster', 'ui'].forEach((key) => {\n                            const v = clampVolumeValue(state.volumes[key]);\n                            if (v !== null) volumeMultipliers[key] = v;\n                        });\n                        updateLoopedSoundVolumes();\n                        updateVolumeSliderUIFromState();\n                    }\n                } catch (error) {\n                    lib.log(`Failed to load user preferences: ${error.message}`);\n                }\n            }\n\n            function t(key, vars = {}) {\n                const value = key.split('.').reduce((node, part) => node && node[part], currentText)\n                    || key.split('.').reduce((node, part) => node && node[part], TEXT);\n                const template = typeof value === 'string' ? value : key;\n                return template.replace(/\\{(\\w+)\\}/g, (_, name) => vars[name] !== undefined ? String(vars[name]) : '');\n            }\n            function applyStaticText(root = document) {\n                root.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); });\n                root.querySelectorAll('[data-i18n-html]').forEach(el => { el.innerHTML = t(el.dataset.i18nHtml); });\n            }\n            applyCurrentLocaleToDocument();\n\n            // ===== ASSET LOADING =====\n            const imageCache = {};\n            const imageLoadPromises = Object.create(null);\n            const audioBuffers = {};\n            const audioLoadPromises = Object.create(null);\n            let audioCtx;\n            let masterCompressor;\n            let audioWarmPromise = null;\n            let audioWarmStarted = false;\n            let audioPrimed = false;\n            const loopedSoundHandles = Object.create(null);\n            const pendingLoopSounds = Object.create(null);\n            const activeSoundHandles = Object.create(null);\n\n            function initAudio() {\n                if (!audioCtx) {\n                    audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n                    masterCompressor = audioCtx.createDynamicsCompressor();\n                    masterCompressor.threshold.value = -10;\n                    masterCompressor.knee.value = 40;\n                    masterCompressor.ratio.value = 12;\n                    masterCompressor.attack.value = 0.003;\n                    masterCompressor.release.value = 0.25;\n                    masterCompressor.connect(audioCtx.destination);\n                }\n                return audioCtx;\n            }\n\n            function resumeAudioFromGesture() {\n                try {\n                    const ctx = initAudio();\n                    if (ctx.state === 'suspended') ctx.resume();\n                } catch (e) { }\n            }\n            document.addEventListener('pointerdown', resumeAudioFromGesture, { capture: true, passive: true });\n            document.addEventListener('touchstart', resumeAudioFromGesture, { capture: true, passive: true });\n            document.addEventListener('keydown', resumeAudioFromGesture, { capture: true });\n\n            // ===== SOUND CATEGORIES & VOLUME SYSTEM =====\n            const soundCategories = {\n                'bgm_main': 'bgm', 'ambient_bunker_sound': 'bgm',\n                'gunshot_sound': 'weapon', 'shotgun_blast_sound': 'weapon', 'sniper_shot_sound': 'weapon',\n                'reload_sound': 'weapon', 'pistol_reload_sound': 'weapon', 'shotgun_reload_sound': 'weapon',\n                'sniper_reload_sound': 'weapon', 'gatling_reload_sound': 'weapon', 'lmg_reload_sound': 'weapon',\n                'ar_reload_sound': 'weapon', 'katana_slash_sound': 'weapon', 'gatling_fire_sound': 'weapon', 'flamethrower_loop': 'weapon', 'sfx_flamethrower_loop': 'weapon',\n                'zombie_groan': 'monster', 'zombie_groan_2': 'monster', 'zombie_groan_3': 'monster', 'zombie_groan_4': 'monster',\n                'zombie_death_sound': 'monster', 'zombie_death_2': 'monster', 'zombie_death_3': 'monster', 'zombie_death_4': 'monster',\n                'wave_complete_sound': 'ui', 'weapon_showcase_sound': 'ui', 'low_ammo_warning': 'ui', 'headshot_ding_sound': 'ui'\n            };\n            const volumeMultipliers = { bgm: 1, weapon: 1, monster: 1, ui: 1 };\n            const DEFAULT_VOICE_LIMIT = 6;\n            const VOICE_STEAL_FADE = 0.006;\n            const SOUND_VOICE_LIMITS = {\n                gunshot_sound: 5,\n                gatling_fire_sound: 5,\n                shotgun_blast_sound: 3,\n                sniper_shot_sound: 2,\n                katana_slash_sound: 3,\n                low_ammo_warning: 1,\n                headshot_ding_sound: 2,\n                wave_complete_sound: 1,\n                weapon_showcase_sound: 1,\n                zombie_groan: 2,\n                zombie_groan_2: 2,\n                zombie_groan_3: 2,\n                zombie_groan_4: 2,\n                zombie_death_sound: 3,\n                zombie_death_2: 3,\n                zombie_death_3: 3,\n                zombie_death_4: 3\n            };\n            const SFX_SPRITE_ID = 'sfx_sprite.mp3';\n            const FLAMETHROWER_LOOP_SOUND_ID = 'sfx_flamethrower_loop';\n            const SFX_SPRITE_MAP = {\n                gunshot_sound: [0, 3],\n                shotgun_blast_sound: [3.12, 2.27],\n                sniper_shot_sound: [5.51, 1.25],\n                reload_sound: [6.88, 2.31],\n                pistol_reload_sound: [9.31, 1.77],\n                shotgun_reload_sound: [11.2, 2.63],\n                sniper_reload_sound: [13.95, 2.31],\n                gatling_reload_sound: [16.38, 3.65],\n                lmg_reload_sound: [20.15, 3.26],\n                ar_reload_sound: [23.53, 2.08],\n                zombie_groan: [25.73, 3],\n                zombie_groan_2: [28.85, 1.98],\n                zombie_groan_3: [30.95, 2.3],\n                zombie_groan_4: [33.37, 2.08],\n                zombie_death_sound: [35.57, 3.92],\n                zombie_death_2: [39.61, 1.79],\n                zombie_death_3: [41.52, 2.29],\n                zombie_death_4: [43.93, 1.39],\n                wave_complete_sound: [45.44, 3],\n                weapon_showcase_sound: [48.56, 2.27],\n                low_ammo_warning: [50.95, 1.14],\n                katana_slash_sound: [52.21, 1.01],\n                gatling_fire_sound: [53.34, 2],\n                headshot_ding_sound: [55.46, 0.94]\n            };\n\n            function getCategoryVolume(id) {\n                const cat = soundCategories[id];\n                return cat ? (volumeMultipliers[cat] !== undefined ? volumeMultipliers[cat] : 1) : 1;\n            }\n\n            function getSoundVoiceLimit(id) {\n                return SOUND_VOICE_LIMITS[id] || DEFAULT_VOICE_LIMIT;\n            }\n\n            function rampGain(gain, value, fade = 0) {\n                if (!gain) return;\n                try {\n                    const ctx = audioCtx || initAudio();\n                    const now = ctx.currentTime;\n                    gain.gain.cancelScheduledValues(now);\n                    gain.gain.setValueAtTime(gain.gain.value, now);\n                    if (fade > 0) gain.gain.linearRampToValueAtTime(value, now + fade);\n                    else gain.gain.setValueAtTime(value, now);\n                } catch (e) {\n                    try { gain.gain.value = value; } catch (err) { }\n                }\n            }\n\n            function unregisterSoundHandle(handle) {\n                if (!handle || !handle._soundId) return;\n                const list = activeSoundHandles[handle._soundId];\n                if (!list) return;\n                const idx = list.indexOf(handle);\n                if (idx >= 0) list.splice(idx, 1);\n                if (list.length === 0) delete activeSoundHandles[handle._soundId];\n            }\n\n            function stopSoundHandle(handle, fade = VOICE_STEAL_FADE) {\n                if (!handle || handle.stopped) return;\n                handle.stopped = true;\n                unregisterSoundHandle(handle);\n                if (handle._loopKey && loopedSoundHandles[handle._loopKey] === handle) delete loopedSoundHandles[handle._loopKey];\n                try {\n                    const ctx = audioCtx || handle.ctx || initAudio();\n                    const now = ctx.currentTime;\n                    if (handle.gain && fade > 0) {\n                        rampGain(handle.gain, 0, fade);\n                        handle.source.stop(now + fade + 0.005);\n                    } else {\n                        handle.source.stop(0);\n                    }\n                } catch (e) { }\n            }\n\n            function enforceVoiceLimit(id, limit) {\n                if (!limit || limit < 1) return;\n                const list = activeSoundHandles[id];\n                if (!list) return;\n                for (let i = list.length - 1; i >= 0; i--) {\n                    if (!list[i] || list[i].stopped) list.splice(i, 1);\n                }\n                while (list.length >= limit) stopSoundHandle(list[0], VOICE_STEAL_FADE);\n            }\n\n            function updateLoopedSoundVolumes() {\n                for (const id in loopedSoundHandles) {\n                    const handle = loopedSoundHandles[id];\n                    if (handle && !handle.stopped && handle.gain) {\n                        const catMult = getCategoryVolume(id);\n                        const baseVol = handle._baseVolume !== undefined ? handle._baseVolume : handle.gain.gain.value;\n                        handle.gain.gain.value = baseVol * catMult;\n                    }\n                }\n            }\n\n            function playFlamethrowerCoolingSound(duration) {\n                try {\n                    const ctx = initAudio();\n                    if (!ctx) return null;\n                    if (ctx.state === 'suspended') ctx.resume();\n\n                    const now = ctx.currentTime;\n                    const volumeScale = getCategoryVolume('reload_sound') * 1.15; // slightly boost for rich presence\n\n                    // --- 1. Mechanical Valve Release Thump ---\n                    // Deep physical thump of heavy valve opening\n                    const thumpOsc = ctx.createOscillator();\n                    const thumpGain = ctx.createGain();\n                    const thumpFilter = ctx.createBiquadFilter();\n                    thumpOsc.type = 'sine';\n                    thumpOsc.frequency.setValueAtTime(95, now);\n                    thumpOsc.frequency.exponentialRampToValueAtTime(25, now + 0.18);\n                    \n                    thumpFilter.type = 'lowpass';\n                    thumpFilter.frequency.setValueAtTime(140, now);\n\n                    thumpGain.gain.setValueAtTime(0, now);\n                    thumpGain.gain.linearRampToValueAtTime(0.85 * volumeScale, now + 0.005);\n                    thumpGain.gain.exponentialRampToValueAtTime(0.001, now + 0.22);\n\n                    thumpOsc.connect(thumpFilter);\n                    thumpFilter.connect(thumpGain);\n                    thumpGain.connect(masterCompressor || ctx.destination);\n                    thumpOsc.start(now);\n                    thumpOsc.stop(now + 0.35);\n\n                    // --- 2. Metal Valve Metal-on-Metal Clank ---\n                    // Sharp metallic \"clink/snap\" of the valve latch popping\n                    const clankOsc = ctx.createOscillator();\n                    const clankGain = ctx.createGain();\n                    const clankFilter = ctx.createBiquadFilter();\n                    clankOsc.type = 'triangle';\n                    clankOsc.frequency.setValueAtTime(1200, now);\n                    clankOsc.frequency.exponentialRampToValueAtTime(450, now + 0.08);\n\n                    clankFilter.type = 'bandpass';\n                    clankFilter.frequency.setValueAtTime(1100, now);\n                    clankFilter.Q.setValueAtTime(5.0, now);\n\n                    clankGain.gain.setValueAtTime(0, now);\n                    clankGain.gain.linearRampToValueAtTime(0.42 * volumeScale, now + 0.002);\n                    clankGain.gain.exponentialRampToValueAtTime(0.001, now + 0.09);\n\n                    clankOsc.connect(clankFilter);\n                    clankFilter.connect(clankGain);\n                    clankGain.connect(masterCompressor || ctx.destination);\n                    clankOsc.start(now);\n                    clankOsc.stop(now + 0.15);\n\n                    // --- 3. Unified High-Pressure Gas Decompression (White Noise) ---\n                    const bufferSize = ctx.sampleRate * duration;\n                    const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);\n                    const data = buffer.getChannelData(0);\n                    for (let i = 0; i < bufferSize; i++) {\n                        data[i] = Math.random() * 2 - 1;\n                    }\n\n                    const noise = ctx.createBufferSource();\n                    noise.buffer = buffer;\n\n                    // Jet Hiss: sharp, escaping high-pressure steam (highpass)\n                    const jetFilter = ctx.createBiquadFilter();\n                    jetFilter.type = 'highpass';\n                    jetFilter.frequency.setValueAtTime(3200, now);\n                    jetFilter.frequency.exponentialRampToValueAtTime(1400, now + duration);\n\n                    // Cavity Rumbling: low-frequency echo inside the expansion nozzle\n                    const cavityFilter = ctx.createBiquadFilter();\n                    cavityFilter.type = 'bandpass';\n                    cavityFilter.frequency.setValueAtTime(620, now);\n                    cavityFilter.frequency.exponentialRampToValueAtTime(280, now + duration);\n                    cavityFilter.Q.setValueAtTime(1.8, now);\n\n                    // Parallel Noise Paths\n                    const jetGain = ctx.createGain();\n                    const cavityGain = ctx.createGain();\n\n                    // Jet envelope: sudden explosive burst, then gradual fade\n                    jetGain.gain.setValueAtTime(0, now);\n                    jetGain.gain.linearRampToValueAtTime(0.95 * volumeScale, now + 0.015);\n                    jetGain.gain.exponentialRampToValueAtTime(0.38 * volumeScale, now + 0.18);\n                    jetGain.gain.linearRampToValueAtTime(0.06 * volumeScale, now + duration * 0.55);\n                    jetGain.gain.exponentialRampToValueAtTime(0.001, now + duration);\n\n                    // Cavity envelope: follows the main gas expansion\n                    cavityGain.gain.setValueAtTime(0, now);\n                    cavityGain.gain.linearRampToValueAtTime(0.55 * volumeScale, now + 0.04);\n                    cavityGain.gain.exponentialRampToValueAtTime(0.24 * volumeScale, now + 0.28);\n                    cavityGain.gain.linearRampToValueAtTime(0.04 * volumeScale, now + duration * 0.65);\n                    cavityGain.gain.exponentialRampToValueAtTime(0.001, now + duration);\n\n                    noise.connect(jetFilter);\n                    jetFilter.connect(jetGain);\n                    jetGain.connect(masterCompressor || ctx.destination);\n\n                    noise.connect(cavityFilter);\n                    cavityFilter.connect(cavityGain);\n                    cavityGain.connect(masterCompressor || ctx.destination);\n\n                    noise.start(now);\n                    noise.stop(now + duration + 0.15);\n\n                    return { source: noise, gain: jetGain, stopped: false };\n                } catch (e) {\n                    return null;\n                }\n            }\n\n            function playSound(id, volume = 0.5, loop = false, options = null) {\n                try {\n                    options = options || {};\n                    const ctx = initAudio();\n                    if (ctx.state === 'suspended') ctx.resume();\n                    if (id === 'flamethrower_cooling') {\n                        const duration = (options && options.duration) || 2.2;\n                        const handle = playFlamethrowerCoolingSound(duration);\n                        if (handle) {\n                            handle._soundId = id;\n                            handle.ctx = ctx;\n                            handle._baseVolume = volume;\n                            handle.source.onended = () => {\n                                handle.stopped = true;\n                                unregisterSoundHandle(handle);\n                            };\n                            (activeSoundHandles[id] || (activeSoundHandles[id] = [])).push(handle);\n                            return handle;\n                        }\n                    }\n                    const spriteWindow = (!loop || options.spriteLoop) ? SFX_SPRITE_MAP[id] : null;\n                    const bufferId = spriteWindow ? SFX_SPRITE_ID : id;\n                    const buffer = audioBuffers[bufferId];\n                    if (!buffer) {\n                        if (loop) pendingLoopSounds[bufferId] = { id, volume, options };\n                        ensureAudioLoaded(bufferId);\n                        return null;\n                    }\n                    const catMult = getCategoryVolume(id);\n                    const finalVolume = volume * catMult;\n                    if (loop && loopedSoundHandles[id] && !loopedSoundHandles[id].stopped) {\n                        loopedSoundHandles[id]._baseVolume = volume;\n                        rampGain(loopedSoundHandles[id].gain, finalVolume, options.fadeIn || 0.012);\n                        return loopedSoundHandles[id];\n                    }\n                    if (finalVolume <= 0.001 && !loop) return null;\n                    if (!loop) enforceVoiceLimit(id, options.maxVoices || getSoundVoiceLimit(id));\n                    const source = ctx.createBufferSource();\n                    source.buffer = buffer;\n                    const gain = ctx.createGain();\n                    const fadeIn = options.fadeIn !== undefined ? options.fadeIn : (spriteWindow ? 0.002 : 0);\n                    gain.gain.value = fadeIn > 0 ? 0 : finalVolume;\n                    source.connect(gain);\n                    gain.connect(masterCompressor);\n                    source.loop = loop;\n                    if (loop && spriteWindow) {\n                        source.loopStart = spriteWindow[0];\n                        source.loopEnd = spriteWindow[0] + spriteWindow[1];\n                    }\n                    const handle = { source, gain, stopped: false, ctx, _baseVolume: volume, _soundId: id, _loopKey: loop ? id : null };\n                    source.onended = () => {\n                        handle.stopped = true;\n                        unregisterSoundHandle(handle);\n                        if (loop && loopedSoundHandles[id] === handle) delete loopedSoundHandles[id];\n                    };\n                    if (loop) {\n                        loopedSoundHandles[id] = handle;\n                    } else {\n                        (activeSoundHandles[id] || (activeSoundHandles[id] = [])).push(handle);\n                    }\n                    const startAt = ctx.currentTime;\n                    if (spriteWindow && loop) source.start(startAt, spriteWindow[0]);\n                    else if (spriteWindow) source.start(startAt, spriteWindow[0], spriteWindow[1]);\n                    else source.start(startAt, options.offset || 0);\n                    if (fadeIn > 0) rampGain(gain, finalVolume, fadeIn);\n                    return handle;\n                } catch (e) { return null; }\n            }\n\n            // Exclusive one-shot: fade the previous instance instead of hard-cutting it.\n            const exclusiveSoundHandles = {};\n            function playSoundExclusive(id, volume = 0.5, fade = VOICE_STEAL_FADE, options = null) {\n                const prev = exclusiveSoundHandles[id];\n                if (prev && !prev.stopped) {\n                    stopSoundHandle(prev, fade);\n                }\n                const handle = playSound(id, volume, false, options);\n                if (handle) exclusiveSoundHandles[id] = handle;\n                return handle;\n            }\n\n            function stopLoopedSound(id) {\n                delete pendingLoopSounds[id];\n                const handle = loopedSoundHandles[id];\n                if (!handle || handle.stopped) return;\n                stopSoundHandle(handle, 0.08);\n            }\n\n            const failedAssets = { images: new Set(), audio: new Set() };\n            let retryTimer = null;\n            const MAX_RETRIES = 5;\n            const RETRY_DELAYS = [3000, 6000, 15000, 30000, 60000];\n            let retryAttempt = 0;\n\n            async function loadAudio(id) {\n                if (audioBuffers[id]) return audioBuffers[id];\n                if (audioLoadPromises[id]) return audioLoadPromises[id];\n                audioLoadPromises[id] = (async () => {\n                    try {\n                        const ctx = initAudio();\n                        const asset = lib.getAsset(id);\n                        if (!asset || asset.type !== 'audio') return null;\n                        const response = await fetch(asset.url);\n                        if (!response.ok) throw new Error('HTTP ' + response.status);\n                        const arrayBuffer = await response.arrayBuffer();\n                        audioBuffers[id] = await ctx.decodeAudioData(arrayBuffer);\n                        failedAssets.audio.delete(id);\n                        if (pendingLoopSounds[id]) {\n                            const pending = pendingLoopSounds[id];\n                            delete pendingLoopSounds[id];\n                            playSound(pending.id || id, pending.volume, true, pending.options || null);\n                        }\n                        return audioBuffers[id];\n                    } catch (e) {\n                        lib.log('Audio load failed: ' + id);\n                        failedAssets.audio.add(id);\n                        scheduleAssetRetry();\n                        return null;\n                    } finally {\n                        delete audioLoadPromises[id];\n                    }\n                })();\n                return audioLoadPromises[id];\n            }\n\n            function ensureAudioLoaded(id) {\n                if (!id || audioBuffers[id] || audioLoadPromises[id]) return;\n                loadAudio(id);\n            }\n\n            function warmCoreAudio(forceResume = false) {\n                // Assets may finish preloading while the browser still blocks playback.\n                // A later trusted weapon-selection gesture must always get a chance to\n                // resume the context, even when the buffers are already warm.\n                if (forceResume && audioCtx && audioCtx.state === 'suspended') {\n                    try { audioCtx.resume(); } catch (e) { }\n                }\n                if (audioPrimed && audioBuffers[SFX_SPRITE_ID]) return audioWarmPromise || Promise.resolve(audioBuffers[SFX_SPRITE_ID]);\n                if (!audioWarmPromise) {\n                    audioWarmStarted = true;\n                    audioWarmPromise = (async () => {\n                        try {\n                            const ctx = initAudio();\n                            if (forceResume && ctx.state === 'suspended') {\n                                try { await ctx.resume(); } catch (e) { }\n                            }\n                            const buffer = await loadAudio(SFX_SPRITE_ID);\n                            audioPrimed = !!buffer;\n                            return buffer;\n                        } finally {\n                            if (!audioPrimed) audioWarmPromise = null;\n                        }\n                    })();\n                } else if (forceResume && audioCtx && audioCtx.state === 'suspended') {\n                    try { audioCtx.resume(); } catch (e) { }\n                }\n                return audioWarmPromise;\n            }\n\n            function scheduleIdleTask(fn, timeout = 1200) {\n                if ('requestIdleCallback' in window) {\n                    window.requestIdleCallback(fn, { timeout });\n                } else {\n                    setTimeout(fn, 0);\n                }\n            }\n\n            function scheduleAssetRetry() {\n                if (retryTimer) return;\n                if (retryAttempt >= MAX_RETRIES) return;\n                if (failedAssets.audio.size === 0 && failedAssets.images.size === 0) return;\n                const delay = RETRY_DELAYS[Math.min(retryAttempt, RETRY_DELAYS.length - 1)];\n                retryTimer = setTimeout(async () => {\n                    retryTimer = null;\n                    retryAttempt++;\n                    const audioToRetry = [...failedAssets.audio];\n                    const imagesToRetry = [...failedAssets.images];\n                    for (const id of audioToRetry) await loadAudio(id);\n                    for (const id of imagesToRetry) await loadImageWithRetry(id);\n                    if (failedAssets.audio.size > 0 || failedAssets.images.size > 0) scheduleAssetRetry();\n                }, delay);\n            }\n\n            const IMAGE_LOAD_TIMEOUT_MS = 3500;\n            function loadImage(id) {\n                if (!id || imageCache[id]) return Promise.resolve(imageCache[id]);\n                if (imageLoadPromises[id]) return imageLoadPromises[id];\n                imageLoadPromises[id] = new Promise((resolve) => {\n                    const asset = lib.getAsset(id);\n                    if (!asset || asset.type !== 'image') { resolve(); return; }\n                    const img = new Image();\n                    img.crossOrigin = 'anonymous';\n                    let settled = false;\n                    const finish = (ok) => {\n                        if (settled) return;\n                        settled = true;\n                        clearTimeout(timer);\n                        if (ok) {\n                            imageCache[id] = img;\n                            failedAssets.images.delete(id);\n                            handleImageAssetReady(id);\n                        } else {\n                            failedAssets.images.add(id);\n                            scheduleAssetRetry();\n                        }\n                        resolve(imageCache[id] || null);\n                    };\n                    const timer = setTimeout(() => finish(false), IMAGE_LOAD_TIMEOUT_MS);\n                    img.onload = () => finish(true);\n                    img.onerror = () => finish(false);\n                    img.src = asset.url;\n                }).finally(() => {\n                    delete imageLoadPromises[id];\n                });\n                return imageLoadPromises[id];\n            }\n\n            function loadImageWithRetry(id) {\n                return new Promise((resolve) => {\n                    const asset = lib.getAsset(id);\n                    if (!asset || asset.type !== 'image') { resolve(); return; }\n                    const img = new Image();\n                    img.crossOrigin = 'anonymous';\n                    let settled = false;\n                    const finish = (ok) => {\n                        if (settled) return;\n                        settled = true;\n                        clearTimeout(timer);\n                        if (ok) {\n                            imageCache[id] = img;\n                            failedAssets.images.delete(id);\n                            handleImageAssetReady(id);\n                        }\n                        resolve();\n                    };\n                    const timer = setTimeout(() => finish(false), IMAGE_LOAD_TIMEOUT_MS);\n                    img.onload = () => finish(true);\n                    img.onerror = () => finish(false);\n                    img.src = asset.url + (asset.url.includes('?') ? '&' : '?') + '_r=' + Date.now();\n                });\n            }\n\n            const MUZZLE_FLASH_ATLAS_ID = 'muzzle_flash_radial_flipbook_2x4_256_clean.png';\n            const MUZZLE_FLASH_FALLBACK_ID = null;\n            const MUZZLE_FLASH_ATLAS_COLS = 4;\n            const MUZZLE_FLASH_ATLAS_ROWS = 2;\n            const MUZZLE_FLASH_ATLAS_FRAMES = MUZZLE_FLASH_ATLAS_COLS * MUZZLE_FLASH_ATLAS_ROWS;\n            const MUZZLE_FLASH_FRAME_RATE = 96;\n            const MUZZLE_FLASH_LIGHT_SAFE_RATE = 0.075;\n\n            // ===== WEAPON ICON SPRITE SHEET =====\n            const WEAPON_ICON_ATLAS_ID = 'weapon_icons_atlas.png';\n            const WEAPON_ICON_SPRITE_COLS = 7;\n            const WEAPON_ICON_SPRITE_SIZE = 512; // each atlas cell is 512x512 for crisp upgrade/showcase scaling\n            // Map weapon index to sprite column\n            const weaponIconSpriteIndex = {\n                'weapon_icon_pistol': 0,\n                'weapon_icon_shotgun': 1,\n                'weapon_icon_sniper': 2,\n                'weapon_icon_katana': 3,\n                'weapon_icon_gatling': 4,\n                'weapon_icon_lmg': 5,\n                'weapon_icon_assault_rifle': 6\n            };\n\n            function getWeaponIconAtlasUrl() {\n                const atlasImg = imageCache[WEAPON_ICON_ATLAS_ID];\n                return atlasImg ? atlasImg.src : '';\n            }\n\n            function applyWeaponIconSprite(el, iconId = el.dataset.icon, size = Number(el.dataset.size) || 38) {\n                if (iconId === 'weapon_icon_flamethrower') {\n                    const customAsset = lib.getAsset('weapon_icon_flamethrower.png');\n                    if (customAsset && customAsset.url) {\n                        el.style.background = `url('${customAsset.url}') no-repeat center center`;\n                        el.style.backgroundSize = 'contain';\n                        el.classList.add('is-loaded');\n                        return true;\n                    }\n                    const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" width=\"${size}\" height=\"${size}\">\n                        <rect width=\"64\" height=\"64\" rx=\"12\" fill=\"#1b1c1e\" opacity=\"0.8\"/>\n                        <rect x=\"20\" y=\"32\" width=\"12\" height=\"24\" rx=\"4\" fill=\"#3e4a3f\" stroke=\"#5f7560\" stroke-width=\"2\"/>\n                        <path d=\"M20 36h12\" stroke=\"#252d26\" stroke-width=\"2\"/>\n                        <path d=\"M23 32h6v-3h-6z\" fill=\"#7d8f7e\"/>\n                        <circle cx=\"34\" cy=\"44\" r=\"5\" fill=\"#c07030\" stroke=\"#ffd080\" stroke-width=\"1\"/>\n                        <line x1=\"34\" y1=\"44\" x2=\"36\" y2=\"41\" stroke=\"#fff\" stroke-width=\"1.5\"/>\n                        <path d=\"M26 56 Q36 58 38 48 T48 24\" fill=\"none\" stroke=\"#2d3033\" stroke-width=\"3\" stroke-linecap=\"round\"/>\n                        <path d=\"M42 22 l10 -10\" stroke=\"#778088\" stroke-width=\"3.5\" stroke-linecap=\"round\"/>\n                        <circle cx=\"53\" cy=\"11\" r=\"2\" fill=\"#ff4500\"/>\n                        <defs>\n                            <radialGradient id=\"flameGrad\" cx=\"50%\" cy=\"50%\" r=\"50%\">\n                                <stop offset=\"0%\" stop-color=\"#ffffff\"/>\n                                <stop offset=\"30%\" stop-color=\"#ffcc00\"/>\n                                <stop offset=\"70%\" stop-color=\"#ff3300\"/>\n                                <stop offset=\"100%\" stop-color=\"#ff3300\" stop-opacity=\"0\"/>\n                            </radialGradient>\n                        </defs>\n                        <path d=\"M48 24 C44 14, 52 8, 56 4 C60 10, 62 16, 56 22 Z\" fill=\"url(#flameGrad)\"/>\n                        <path d=\"M50 20 C48 16, 51 12, 54 9 C56 12, 57 15, 54 18 Z\" fill=\"#ffdd66\" opacity=\"0.9\"/>\n                    </svg>`;\n                    const dataUri = `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;\n                    el.style.background = `url('${dataUri}') no-repeat center center`;\n                    el.style.backgroundSize = 'contain';\n                    el.classList.add('is-loaded');\n                    return true;\n                }\n                const atlasUrl = getWeaponIconAtlasUrl();\n                const col = weaponIconSpriteIndex[iconId];\n                if (atlasUrl && col !== undefined) {\n                    el.style.background = `url('${atlasUrl}') no-repeat`;\n                    el.style.backgroundSize = `${WEAPON_ICON_SPRITE_COLS * size}px ${size}px`;\n                    el.style.backgroundPosition = `-${col * size}px 0`;\n                    el.classList.add('is-loaded');\n                    return true;\n                }\n                el.classList.remove('is-loaded');\n                return false;\n            }\n\n            function getWeaponIconElement(iconId, size = 38) {\n                const el = document.createElement('div');\n                el.className = 'weapon-icon-lazy';\n                el.dataset.icon = iconId;\n                el.dataset.size = String(size);\n                el.style.width = `${size}px`;\n                el.style.height = `${size}px`;\n                applyWeaponIconSprite(el, iconId, size);\n                return el;\n            }\n\n            function refreshWeaponIconUI() {\n                document.querySelectorAll('.weapon-icon-lazy').forEach(el => applyWeaponIconSprite(el));\n            }\n            let refreshMuzzleFlashTexture = () => { };\n            let weaponSelectOpen = false;\n            function handleImageAssetReady(id) {\n                if (id === WEAPON_ICON_ATLAS_ID) refreshWeaponIconUI();\n                else if (id === MUZZLE_FLASH_ATLAS_ID) refreshMuzzleFlashTexture();\n            }\n            function ensureWeaponIconAtlasLoaded() {\n                return loadImage(WEAPON_ICON_ATLAS_ID);\n            }\n\n            function expSmoothingFactor(dt, speed) {\n                return 1 - Math.exp(-Math.max(0, dt) * speed);\n            }\n\n            function getRapidFireComfort(ws) {\n                const interval = Math.max(0.016, Math.min(0.12, (ws && ws.fireRate) || 0.12));\n                return 0.48 + 0.52 * Math.sqrt(interval / 0.12);\n            }\n\n            const WDS_RAPID_COMFORT = 0;\n            const WDS_RECOIL_IMPULSE = 1;\n            const WDS_PITCH_DEBT_CAP = 2;\n            const WDS_YAW_DEBT_CAP = 3;\n            const WDS_YAW_COMFORT_SCALE = 4;\n            const WDS_VISUAL_KICK_BASE = 5;\n            const WDS_GUN_KICK_STRENGTH_BASE = 6;\n            const WDS_GUN_KICK_TARGET_ADD_BASE = 7;\n            const WDS_RECOIL_KICK_SCALE = 8;\n            const WDS_SPREAD_HEAT_ADD = 9;\n            const WDS_MUZZLE_LIGHT_SMOOTH = 10;\n            const WDS_MUZZLE_FLASH_DURATION = 11;\n            const WDS_MUZZLE_FLASH_CLOCK_RATE = 12;\n            const WDS_RAPID_AUTO_MUZZLE_LIGHT = 13;\n            const WDS_FLASH_STRENGTH_BASE = 14;\n            const WDS_MUZZLE_BASE_SCALE = 15;\n            const WDS_MUZZLE_LIGHT_PEAK_BASE = 16;\n            const WDS_FIRE_VOLUME = 17;\n            const WDS_RECOIL_PITCH = 18;\n            const WDS_RECOIL_YAW = 19;\n            const WDS_RECOIL_HEAT_SCALE = 20;\n            const WDS_LOW_AMMO_THRESHOLD = 21;\n            const WDS_STRIDE = 22;\n            let weaponDerivedStats = null;\n\n            function weaponDerivedOffset(index) {\n                return index * WDS_STRIDE;\n            }\n\n            function initWeaponDerivedStats(count) {\n                weaponDerivedStats = new Float32Array(count * WDS_STRIDE);\n            }\n\n            function updateWeaponDerivedStats(index, ws) {\n                if (!ws || !weaponDerivedStats) return;\n                const o = weaponDerivedOffset(index);\n                const comfort = getRapidFireComfort(ws);\n                const recoilKick = ws.recoilKick || 1;\n                const muzzleScale = ws.muzzleScale || 1;\n                const muzzleSpriteScale = ws.muzzleSpriteScale || muzzleScale;\n                let fireVolume = 0.5;\n                if (ws.name === 'PISTOL') fireVolume = 0.38;\n                else if (ws.name === 'SHOTGUN') fireVolume = 0.62;\n                else if (ws.name === 'SNIPER') fireVolume = 0.7;\n                else if (ws.name === 'GATLING') fireVolume = 0.35;\n                else if (ws.name === 'AUTO RIFLE') fireVolume = 0.48;\n                weaponDerivedStats[o + WDS_RAPID_COMFORT] = comfort;\n                weaponDerivedStats[o + WDS_RECOIL_IMPULSE] = Math.max(0.18, Math.min(0.9, 0.54 * comfort * Math.sqrt(recoilKick)));\n                weaponDerivedStats[o + WDS_PITCH_DEBT_CAP] = 0.12 + Math.min(0.08, Math.max(0, (ws.fireRate || 0.12) - 0.08));\n                weaponDerivedStats[o + WDS_YAW_DEBT_CAP] = 0.045 + 0.035 * comfort;\n                weaponDerivedStats[o + WDS_YAW_COMFORT_SCALE] = 0.62 + comfort * 0.38;\n                weaponDerivedStats[o + WDS_VISUAL_KICK_BASE] = recoilKick * (0.46 + comfort * 0.38);\n                weaponDerivedStats[o + WDS_GUN_KICK_STRENGTH_BASE] = recoilKick * comfort;\n                weaponDerivedStats[o + WDS_GUN_KICK_TARGET_ADD_BASE] = recoilKick * 0.18 * comfort;\n                weaponDerivedStats[o + WDS_RECOIL_KICK_SCALE] = 0.72 + Math.min(0.72, recoilKick * (0.34 + comfort * 0.22));\n                weaponDerivedStats[o + WDS_SPREAD_HEAT_ADD] = (ws.spreadFire || 0.006) * getFireRateScale(ws, ws.spreadReference || 0.12);\n                weaponDerivedStats[o + WDS_MUZZLE_LIGHT_SMOOTH] = shouldSmoothMuzzleLight(ws) ? 1 : 0;\n                weaponDerivedStats[o + WDS_MUZZLE_FLASH_DURATION] = getMuzzleFlashDuration(ws);\n                weaponDerivedStats[o + WDS_MUZZLE_FLASH_CLOCK_RATE] = getMuzzleFlashClockRate(ws);\n                weaponDerivedStats[o + WDS_RAPID_AUTO_MUZZLE_LIGHT] = ws.isAuto && (ws.fireRate || 0.1) <= 0.07 ? 1 : 0;\n                weaponDerivedStats[o + WDS_FLASH_STRENGTH_BASE] = Math.min(1.15, 0.78 + muzzleScale * 0.16) * comfort;\n                weaponDerivedStats[o + WDS_MUZZLE_BASE_SCALE] = muzzleSpriteScale * (0.86 + comfort * 0.14);\n                weaponDerivedStats[o + WDS_MUZZLE_LIGHT_PEAK_BASE] = 1.45 * muzzleScale * comfort;\n                weaponDerivedStats[o + WDS_FIRE_VOLUME] = fireVolume;\n                weaponDerivedStats[o + WDS_RECOIL_PITCH] = ws.recoilPitch !== undefined ? ws.recoilPitch : 0.02;\n                weaponDerivedStats[o + WDS_RECOIL_YAW] = ws.recoilYaw !== undefined ? ws.recoilYaw : 0.012;\n                weaponDerivedStats[o + WDS_RECOIL_HEAT_SCALE] = ws.recoilHeatScale !== undefined ? ws.recoilHeatScale : 10;\n                weaponDerivedStats[o + WDS_LOW_AMMO_THRESHOLD] = ws.magSize === Infinity ? Infinity : Math.ceil(ws.magSize * 0.25);\n            }\n\n            function getFireRateScale(ws, reference = 0.12) {\n                const interval = Math.max(0.016, Math.min(reference, (ws && ws.fireRate) || reference));\n                const minScale = (ws && ws.spreadMinFireScale !== undefined) ? ws.spreadMinFireScale : 0.42;\n                return Math.max(minScale, Math.min(1, Math.sqrt(interval / reference)));\n            }\n\n            function getWeaponSpreadMax(ws) {\n                return (ws && ws.spreadMax !== undefined) ? ws.spreadMax : 0.12;\n            }\n\n            function clampWeaponSpreadHeat(ws, heat) {\n                return Math.max(0, Math.min(getWeaponSpreadMax(ws), heat || 0));\n            }\n\n            function getScopeSpreadMultiplier(ws) {\n                return sniperScoped ? ((ws && ws.scopeSpreadMult !== undefined) ? ws.scopeSpreadMult : 0.15) : 1;\n            }\n\n            function getShotSpread(ws, wComp, moving) {\n                const heat = clampWeaponSpreadHeat(ws, wComp.spreadHeat || 0);\n                const moveSpread = moving ? (ws.spreadMove || 0) : 0;\n                return (ws.spread + heat + moveSpread) * getScopeSpreadMultiplier(ws);\n            }\n\n            function shouldSmoothMuzzleLight(ws) {\n                if (!ws || ws.isMelee) return false;\n                const threshold = ws.muzzleLightSafeRate !== undefined ? ws.muzzleLightSafeRate : MUZZLE_FLASH_LIGHT_SAFE_RATE;\n                return (ws.fireRate || 0.1) <= threshold;\n            }\n\n            function getMuzzleFlashDuration(ws) {\n                return Math.min(0.09, Math.max(0.045, (ws.fireRate || 0.1) * 0.82));\n            }\n\n            function getMuzzleFlashClockRate(ws) {\n                return MUZZLE_FLASH_FRAME_RATE * ((ws.fireRate || 0.1) <= 0.07 ? 1.35 : 1);\n            }\n\n            function getCrosshairTargetGap(ws, wComp, moving) {\n                if (!ws || ws.isMelee) return 7;\n                const scopedMult = getScopeSpreadMultiplier(ws);\n                const heat = clampWeaponSpreadHeat(ws, wComp.spreadHeat || 0);\n                const baseSpread = ((ws.spread || 0) * scopedMult);\n                const moveSpread = (moving ? (ws.spreadMove || 0) : 0) * scopedMult;\n                const heatSpread = heat * scopedMult;\n                const heatRatio = getWeaponSpreadMax(ws) > 0 ? heat / getWeaponSpreadMax(ws) : 0;\n                let minGap = 7;\n                let maxGap = 31;\n                let baseWeight = 0.22;\n                let spreadScale = ws.crosshairSpreadScale || 52;\n                let heatGapMax = ws.crosshairHeatGap || 7;\n                let kickScale = ws.crosshairKickScale || 4;\n\n                if (ws.name === 'SNIPER') {\n                    minGap = sniperScoped ? 3.5 : 4.5;\n                    maxGap = sniperScoped ? 12 : 18;\n                    baseWeight = sniperScoped ? 0 : 0.08;\n                    spreadScale = 50;\n                    heatGapMax = 5;\n                    kickScale = 3.2;\n                } else if (ws.name === 'PISTOL') {\n                    minGap = 6.5;\n                    maxGap = 24;\n                    baseWeight = 0.12;\n                    kickScale = 3.6;\n                } else if (ws.name === 'AUTO RIFLE') {\n                    minGap = 6.5;\n                    maxGap = 27;\n                    baseWeight = 0.16;\n                    kickScale = 3.8;\n                } else if (ws.name === 'LMG') {\n                    minGap = 7.5;\n                    maxGap = 31;\n                    baseWeight = 0.42;\n                } else if (ws.name === 'GATLING') {\n                    minGap = 8;\n                    maxGap = 32;\n                    baseWeight = 0.5;\n                    heatGapMax = 8;\n                    kickScale = 3.8;\n                } else if (ws.name === 'SHOTGUN') {\n                    minGap = 10;\n                    maxGap = 36;\n                    baseWeight = 1;\n                    spreadScale = 54;\n                    heatGapMax = 5;\n                    kickScale = 3.4;\n                }\n\n                const visualSpread = baseSpread * baseWeight + heatSpread * 0.9 + moveSpread * 1.1;\n                const coneGap = Math.sqrt(Math.max(0, visualSpread)) * spreadScale;\n                const heatGap = heatRatio * scopedMult * heatGapMax;\n                const fireBloomGap = Math.min(5, (wComp.visualKick || 0) * kickScale);\n                return Math.max(minGap, Math.min(maxGap, minGap + coneGap + heatGap + fireBloomGap));\n            }\n\n            function getWeaponSoundMinInterval(ws) {\n                if (!ws || ws.isMelee) return 0;\n                return ws.soundMinInterval !== undefined ? ws.soundMinInterval : Math.min(0.055, Math.max(0, (ws.fireRate || 0.1) * 0.55));\n            }\n\n            function getWeaponShellMinInterval(ws) {\n                if (!ws || ws.isMelee) return Infinity;\n                return ws.shellMinInterval !== undefined ? ws.shellMinInterval : Math.min(0.05, Math.max(0.018, (ws.fireRate || 0.1) * 0.45));\n            }\n\n            function getWeaponFireVoiceLimit(ws, minInterval) {\n                if (ws && ws.isAuto) {\n                    const window = Math.max(0.18, (ws.fireRate || minInterval || 0.08) * 2.2);\n                    return Math.max(4, Math.min(8, Math.ceil(window / Math.max(0.016, ws.fireRate || minInterval || 0.08)) + 2));\n                }\n                if (ws && ws.name === 'SHOTGUN') return 2;\n                if (minInterval <= 0.06) return 4;\n                return 3;\n            }\n\n            function shouldSpawnWeaponShell(ws, wComp) {\n                const now = performance.now() / 1000;\n                const minInterval = getWeaponShellMinInterval(ws);\n                if (wComp.lastShellAt && now - wComp.lastShellAt < minInterval) return false;\n                wComp.lastShellAt = now;\n                return true;\n            }\n\n            function playWeaponFireSound(ws, volume) {\n                const minInterval = getWeaponSoundMinInterval(ws);\n                const rateScale = getFireRateScale(ws, 0.12);\n                const finalVolume = volume * (0.72 + rateScale * 0.28);\n                return playSound(ws.sound, finalVolume, false, {\n                    fadeIn: 0,\n                    maxVoices: getWeaponFireVoiceLimit(ws, minInterval)\n                });\n            }\n\n            // Loading screen\n            const loadingScreen = document.getElementById('loading-screen');\n            const loadingBar = document.getElementById('loading-bar');\n            const loadingTextEl = document.getElementById('loading-text');\n            const totalAssets = 1;\n            let loadedCount = 0;\n\n            function updateLoading() {\n                loadedCount++;\n                const pct = Math.round((loadedCount / totalAssets) * 100);\n                if (pct <= 100) {\n                    loadingBar.style.transform = `scaleX(${pct / 100})`;\n                    loadingTextEl.textContent = t('loading.assetsProgress', { pct });\n                }\n            }\n\n            let initialLoadingHidden = false;\n            function hideInitialLoading() {\n                if (initialLoadingHidden) return;\n                initialLoadingHidden = true;\n                loadingScreen.classList.add('hidden');\n                setTimeout(() => { loadingScreen.style.display = 'none'; }, 600);\n            }\n\n            // Load preferences first so locale is correct when start screen paints\n            await loadUserPreferences();\n            applyCurrentLocaleToDocument();\n            updateLoading();\n\n            ensureWeaponIconAtlasLoaded().then(() => {\n                loadImage('weapon_icon_flamethrower.png').then(() => refreshWeaponIconUI());\n                refreshWeaponIconUI();\n            });\n            scheduleIdleTask(() => {\n                loadImage(MUZZLE_FLASH_ATLAS_ID).then(() => refreshMuzzleFlashTexture());\n                warmCoreAudio(false);\n                ensureAudioLoaded(FLAMETHROWER_LOOP_SOUND_ID);\n            }, 900);\n\n            // ===== SHOW GAME PARAMETERS =====\n            lib.showGameParameters({\n                name: t('params.name'),\n                params: {\n                    [t('params.zombieHealth')]: { key: 'zombieHealthMultiplier', type: 'slider', min: 0.5, max: 3, step: 0.1 },\n                    [t('params.zombieSpeed')]: { key: 'zombieSpeedMultiplier', type: 'slider', min: 0.5, max: 2, step: 0.1 },\n                    [t('params.startingWave')]: { key: 'startingWave', type: 'slider', min: 1, max: 10, step: 1 },\n                    [t('params.headshotMultiplier')]: { key: 'headshotMultiplier', type: 'slider', min: 1, max: 5, step: 0.5 },\n                    [t('params.katanaSpeed')]: { key: 'katanaSpeed', type: 'slider', min: 0.5, max: 3, step: 0.1 },\n                    [t('params.lighting')]: {\n                        key: 'lightingColor', type: 'dropdown', options: [\n                            { label: t('params.coolWhite'), value: 'cool' },\n                            { label: t('params.warm'), value: 'warm' },\n                            { label: t('params.emergencyRed'), value: 'red' },\n                            { label: t('params.nightVision'), value: 'green' }\n                        ]\n                    }\n                }\n            });\n\n            if (mode === 'edit') {\n                setupEditMode();\n                await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));\n                hideInitialLoading();\n                return;\n            }\n\n            // ===== PLAY MODE (ECS Architecture) =====\n            setupPlayMode();\n\n            function setupEditMode() {\n                const startScreen = document.getElementById('start-screen');\n                let editModeRafId = 0;\n                let editModeDisposed = false;\n                startScreen.innerHTML = `\n                <h1 style=\"font-family:'Orbitron';font-size:36px;color:var(--cyan);margin-bottom:24px;\">${t('edit.title')}</h1>\n                <p style=\"color:rgba(255,255,255,0.7);font-size:18px;padding:0 32px;text-align:center;\">\n                    ${t('edit.line1')}<br><br>\n                    ${t('edit.line2')}<br><br>\n                    <span style=\"color:var(--cyan);font-size:14px;\">${t('edit.architecture')}</span>\n                </p>\n            `;\n                startScreen.style.display = 'flex';\n                document.getElementById('hud').style.display = 'none';\n\n                const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });\n                renderer.setSize(canvas.parentElement.clientWidth, canvas.parentElement.clientHeight);\n                renderer.setClearColor(0x0e1118);\n\n                const scene = new THREE.Scene();\n                const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 100);\n                camera.position.set(0, 5, 15);\n                camera.lookAt(0, 1, 0);\n\n                buildBunkerScene(scene);\n\n                const onEditResize = () => {\n                    const w = canvas.parentElement.clientWidth;\n                    const h = canvas.parentElement.clientHeight;\n                    renderer.setSize(w, h);\n                    camera.aspect = w / h;\n                    camera.updateProjectionMatrix();\n                    renderEditFrame();\n                };\n\n                function disposeEditMode() {\n                    if (editModeDisposed) return;\n                    editModeDisposed = true;\n                    if (editModeRafId) cancelAnimationFrame(editModeRafId);\n                    window.removeEventListener('resize', onEditResize);\n                    window.removeEventListener('pagehide', disposeEditMode);\n                    renderer.dispose();\n                }\n\n                function renderEditFrame() {\n                    if (editModeDisposed) return;\n                    renderer.render(scene, camera);\n                    editModeRafId = 0;\n                }\n\n                window.addEventListener('resize', onEditResize);\n                window.addEventListener('pagehide', disposeEditMode);\n                editModeRafId = requestAnimationFrame(renderEditFrame);\n            }\n\n            function setupPlayMode() {\n                // ===== THREE.JS SETUP =====\n                const detectedTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;\n                document.body.classList.toggle('touch-device', detectedTouchDevice);\n                const perfProfile = (() => {\n                    const cores = navigator.hardwareConcurrency || 4;\n                    const memory = navigator.deviceMemory || 4;\n                    const lowEnd = cores <= 4 || memory <= 3;\n                    return {\n                        lowEnd,\n                        useLambert: lowEnd || detectedTouchDevice,\n                        pixelRatio: 1,\n                        antialias: true,\n                        precision: detectedTouchDevice ? 'mediump' : 'highp',\n                        targetFrameMs: detectedTouchDevice ? 1000 / 60 : 0,\n                        idleFrameMs: lowEnd ? 160 : 96,\n                        maxMistEffects: detectedTouchDevice ? 8 : (lowEnd ? 10 : 14),\n                        maxBloodFlows: detectedTouchDevice ? 32 : (lowEnd ? 46 : 70),\n                        maxBulletHoles: detectedTouchDevice ? 30 : (lowEnd ? 42 : 60),\n                        maxSeveredParts: detectedTouchDevice ? 6 : (lowEnd ? 8 : 12),\n                        maxCorpses: detectedTouchDevice ? 8 : (lowEnd ? 12 : 18)\n                    };\n                })();\n\n                const renderer = new THREE.WebGLRenderer({\n                    canvas,\n                    antialias: perfProfile.antialias,\n                    powerPreference: 'high-performance',\n                    precision: perfProfile.precision,\n                    preserveDrawingBuffer: false,\n                    stencil: false,\n                    depth: true,\n                    desynchronized: true\n                });\n                renderer.debug.checkShaderErrors = false;\n                window.__lastSurvivorPerfProfile = perfProfile;\n                renderer.setPixelRatio(perfProfile.pixelRatio);\n                renderer.setSize(gameWorld.clientWidth, gameWorld.clientHeight, false);\n                renderer.setClearColor(0x0e1118);\n                renderer.toneMapping = THREE.ACESFilmicToneMapping;\n                renderer.toneMappingExposure = 1.1;\n                renderer.shadowMap.enabled = false;\n\n                const scene = new THREE.Scene();\n                scene.fog = new THREE.Fog(0x0e1118, 16, 42);\n\n                function isLandscapeMode() {\n                    return screenOrientationMode === 'landscape' || gameWorld.classList.contains('landscape-mode');\n                }\n\n                function isRotatedLandscapeMode() {\n                    return screenOrientationMode === 'landscape' || gameWorld.classList.contains('landscape-mode');\n                }\n\n                function getBaseCameraFov() {\n                    if (isLandscapeMode()) {\n                        // Landscape: horizontal FOV 90, vertical FOV 59\n                        // Three.js uses vertical FOV; with typical landscape aspect ~16:9 rotated (9:16 \u2192 ~1.78),\n                        // we use vertical FOV 59 directly.\n                        return 59;\n                    }\n                    return 75;\n                }\n\n                function getBaseWeaponFov() {\n                    return 65;\n                }\n\n                const camera = new THREE.PerspectiveCamera(getBaseCameraFov(), gameWorld.clientWidth / gameWorld.clientHeight, 0.1, 100);\n                camera.position.set(0, 1.7, 10);\n\n                cachedGameWidth = gameWorld.clientWidth;\n                cachedGameHeight = gameWorld.clientHeight;\n                cachedIsLandscape = isLandscapeMode();\n\n                function resizeRendererToDisplay() {\n                    const w = gameWorld.clientWidth;\n                    const h = gameWorld.clientHeight;\n                    renderer.setSize(w, h, false);\n                    camera.aspect = w / h;\n                    camera.updateProjectionMatrix();\n                    cachedGameWidth = w;\n                    cachedGameHeight = h;\n                    cachedIsLandscape = isLandscapeMode();\n                    return { w, h };\n                }\n                window.addEventListener('resize', resizeRendererToDisplay);\n\n                function updateOrientationButtonLabels(root = document) {\n                    root.querySelectorAll('.orientation-btn[data-orient=\"portrait\"] span').forEach((el) => {\n                        el.textContent = t('pause.portrait');\n                    });\n                    root.querySelectorAll('.orientation-btn[data-orient=\"landscape\"] span').forEach((el) => {\n                        el.textContent = t('pause.landscape');\n                    });\n                }\n\n                function syncOrientationButtons() {\n                    updateOrientationButtonLabels();\n                    document.querySelectorAll('.orientation-btn[data-orient]').forEach((btn) => {\n                        btn.classList.toggle('active', btn.dataset.orient === screenOrientationMode);\n                    });\n                }\n\n                function requestScreenOrientationLock(mode) {\n                    // Intentionally disabled: we use manual CSS rotation for landscape/portrait.\n                    // This avoids relying on system orientation lock support.\n                    return;\n                }\n\n                function applyScreenOrientation(mode, options = {}) {\n                    const nextMode = mode === 'landscape' ? 'landscape' : 'portrait';\n                    screenOrientationMode = nextMode;\n                    gameWorld.classList.toggle('landscape-mode', nextMode === 'landscape');\n                    if (typeof cancelActiveTouchControls === 'function') cancelActiveTouchControls();\n                    syncOrientationButtons();\n                    // Manual orientation only (CSS rotation) \u2014 do not rely on system orientation lock.\n                    scheduleRuntimeTimeout(() => {\n                        resizeRendererToDisplay();\n                        camera.fov = getBaseCameraFov();\n                        camera.updateProjectionMatrix();\n                        touchLayoutDirty = true;\n                        joystickLeftDirty = true;\n                        if (typeof syncWeaponViewportForOrientation === 'function') syncWeaponViewportForOrientation();\n                    }, options.delay || 50);\n                    if (options.save !== false) saveUserPreferences();\n                }\n\n                function bindOrientationButtons() {\n                    syncOrientationButtons();\n                    document.querySelectorAll('.orientation-btn[data-orient]').forEach((btn) => {\n                        btn.onclick = () => applyScreenOrientation(btn.dataset.orient || 'portrait');\n                    });\n                }\n\n                buildBunkerScene(scene);\n\n                const perfBudget = {\n                    level: perfProfile.lowEnd ? 1 : 0,\n                    smoothedFrameMs: perfProfile.lowEnd ? 18 : 16.7,\n                    sampleMs: 0,\n                    sampleFrames: 0,\n                    longFrames: 0,\n                    lastFrameMs: 16.7,\n                    zombies: 0,\n                    effects: 0\n                };\n                let perfFrameTick = 0;\n                let perfDomPressureLevel = -1;\n\n                function getPerfPressureLevel() {\n                    return perfBudget.level || 0;\n                }\n\n                function applyPerfPressureClass() {\n                    const level = getPerfPressureLevel();\n                    if (level === perfDomPressureLevel) return;\n                    perfDomPressureLevel = level;\n                    document.body.classList.toggle('perf-pressure-1', level >= 1);\n                    document.body.classList.toggle('perf-pressure-2', level >= 2);\n                    document.body.classList.toggle('perf-pressure-3', level >= 3);\n                }\n\n                function getPerfAdjustedVisualScale() {\n                    const level = getPerfPressureLevel();\n                    return level >= 3 ? 0.55 : (level >= 2 ? 0.72 : (level >= 1 ? 0.88 : 1));\n                }\n                runtimePerfVisualScale = getPerfAdjustedVisualScale();\n                applyPerfPressureClass();\n\n                function shouldRunBudgetedFrame(key, interval) {\n                    interval = interval || 1;\n                    if (interval <= 1) return true;\n                    const frame = perfFrameTick || 0;\n                    let hash = 0;\n                    for (let i = 0; i < key.length; i++) hash = ((hash << 5) - hash + key.charCodeAt(i)) | 0;\n                    return ((frame + Math.abs(hash)) % interval) === 0;\n                }\n\n                // Integer-key variant: avoids string allocation + char-by-char hashing in hot loops.\n                // intKey must be a non-negative integer unique to the call-site (e.g. slot + offset).\n                function shouldRunBudgetedFrameInt(intKey, interval) {\n                    if (interval <= 1) return true;\n                    return (((perfFrameTick || 0) + intKey) % interval) === 0;\n                }\n\n                function getFrameSkipInterval(base, heavy) {\n                    const level = getPerfPressureLevel();\n                    if (level <= 0) return 1;\n                    return Math.max(1, base + Math.max(0, level - 1) + (heavy ? 1 : 0));\n                }\n\n                function getActiveEffectLoad() {\n                    return activeProjectileSlots.length + activeBloodFlowSlots.length + bloodMistRuntime.activeSlots.length +\n                        severedRuntime.activeSlots.length + corpseRuntime.activeSlots.length + activeShellSlots.length +\n                        activeImpactSparks.length + bulletHoles.length + zombieDeathEffectQueue.length;\n                }\n\n                function getEffectDistanceSq(position) {\n                    if (!position) return Infinity;\n                    const playerPos = world.getComponent(playerEntity, 'Position');\n                    if (!playerPos) return Infinity;\n                    const dx = (position.x || 0) - playerPos.x;\n                    const dy = (position.y || 0) - (playerPos.y || 0);\n                    const dz = (position.z || 0) - playerPos.z;\n                    return dx * dx + dy * dy + dz * dz;\n                }\n\n                function shouldSpawnBudgetedEffect(kind, priority = 1, position = null) {\n                    const level = getPerfPressureLevel();\n                    if (level <= 0) return true;\n                    const load = getActiveEffectLoad();\n                    const hardLimit = level >= 3 ? 82 : (level >= 2 ? 112 : 146);\n                    const softLimit = level >= 3 ? 58 : (level >= 2 ? 82 : 112);\n                    const distSq = getEffectDistanceSq(position);\n                    const near = distSq <= 36;\n                    const mid = distSq <= 196;\n                    const effectivePriority = priority + (near ? 2 : mid ? 1 : 0);\n                    if (load >= hardLimit && effectivePriority < 3) return false;\n                    if (load < softLimit) return true;\n                    const frame = (gameState.frameId || perfFrameTick || 0) + kind.length;\n                    if (effectivePriority >= 3) return true;\n                    if (near) return true;\n                    if (effectivePriority >= 2) return level < 3 || (frame & 1) === 0;\n                    const stride = level >= 3 ? 5 : (level >= 2 ? 3 : 2);\n                    return (frame % stride) === 0;\n                }\n\n                 function makePerfMaterial(kind, options) {\n                    const useLambert = perfProfile.useLambert || perfProfile.lowEnd;\n                    if (!useLambert) return new THREE.MeshPhongMaterial(options);\n                    const matParams = { color: options.color };\n                    if (options.transparent !== undefined) matParams.transparent = options.transparent;\n                    if (options.opacity !== undefined) matParams.opacity = options.opacity;\n                    if (options.side !== undefined) matParams.side = options.side;\n                    if (kind === 'basic') return new THREE.MeshBasicMaterial(matParams);\n                    return new THREE.MeshLambertMaterial(matParams);\n                }\n\n                function updatePerformanceBudget(frameMs) {\n                    if (!gameState.started || gameState.over || gamePaused || gameState.upgradeScreenOpen) return;\n                    if (!Number.isFinite(frameMs) || frameMs <= 0) return;\n                    const clamped = Math.min(frameMs, 120);\n                    perfBudget.lastFrameMs = clamped;\n                    perfBudget.smoothedFrameMs += (clamped - perfBudget.smoothedFrameMs) * 0.08;\n                    perfBudget.sampleMs += clamped;\n                    perfBudget.sampleFrames++;\n                    if (clamped > 33) perfBudget.longFrames++;\n                    if (perfBudget.sampleMs < 2200 && perfBudget.sampleFrames < 150) return;\n\n                    const avg = perfBudget.sampleMs / Math.max(1, perfBudget.sampleFrames);\n                    const longRatio = perfBudget.longFrames / Math.max(1, perfBudget.sampleFrames);\n                    const activeZombies = zombieRuntime.activeSlots.length;\n                    const activeEffects = getActiveEffectLoad();\n                    let targetLevel = perfProfile.lowEnd ? 1 : 0;\n                    if (avg > 30 || perfBudget.smoothedFrameMs > 30 || longRatio > 0.18 || activeZombies > 170 || activeEffects > 115) targetLevel = Math.max(targetLevel, 3);\n                    else if (avg > 24 || perfBudget.smoothedFrameMs > 24 || longRatio > 0.08 || activeZombies > 125 || activeEffects > 85) targetLevel = Math.max(targetLevel, 2);\n                    else if (avg > 18.5 || perfBudget.smoothedFrameMs > 19 || activeZombies > 80 || activeEffects > 55) targetLevel = Math.max(targetLevel, 1);\n\n                    if (targetLevel > perfBudget.level) perfBudget.level = targetLevel;\n                    else if (targetLevel < perfBudget.level && avg < 17.5 && perfBudget.smoothedFrameMs < 18 && longRatio < 0.03 && activeZombies < 70) perfBudget.level--;\n\n                    runtimePerfVisualScale = getPerfAdjustedVisualScale();\n                    applyPerfPressureClass();\n                    perfBudget.zombies = activeZombies;\n                    perfBudget.effects = activeEffects;\n                    perfBudget.sampleMs = 0;\n                    perfBudget.sampleFrames = 0;\n                    perfBudget.longFrames = 0;\n                    perfFrameTick = 0;\n                }\n\n                function attachToScene(obj) {\n                    if (obj && !obj.parent) scene.add(obj);\n                }\n\n                function detachFromScene(obj) {\n                    if (obj && obj.parent === scene) scene.remove(obj);\n                }\n\n                function disposePooledThreeObject(obj) {\n                    if (!obj) return;\n                    detachFromScene(obj);\n                    const materials = new Set();\n                    const collectMaterial = (mat) => {\n                        if (!mat) return;\n                        if (Array.isArray(mat)) {\n                            for (let i = 0; i < mat.length; i++) collectMaterial(mat[i]);\n                            return;\n                        }\n                        materials.add(mat);\n                    };\n                    obj.traverse(child => collectMaterial(child.material));\n                    const owned = obj.userData && obj.userData.materials;\n                    if (Array.isArray(owned)) {\n                        for (let i = 0; i < owned.length; i++) collectMaterial(owned[i].material || owned[i]);\n                    } else if (owned) {\n                        for (const key in owned) collectMaterial(owned[key]);\n                    }\n                    materials.forEach(mat => {\n                        try { if (mat && typeof mat.dispose === 'function') mat.dispose(); } catch (e) { }\n                    });\n                }\n\n                // ===== ECS WORLD INSTANCE =====\n                const world = new World();\n\n                // ===== ZOMBIE MESH OBJECT POOL =====\n                // Roblox-style rounded box geometry helper\n                function createRoundedBox(width, height, depth, radius, segments) {\n                    segments = segments || 2;\n                    radius = Math.min(radius, width / 2, height / 2, depth / 2);\n                    const geometry = new THREE.BoxGeometry(width, height, depth, segments, segments, segments);\n                    const positions = geometry.attributes.position;\n                    const hw = width / 2 - radius;\n                    const hh = height / 2 - radius;\n                    const hd = depth / 2 - radius;\n                    for (let i = 0; i < positions.count; i++) {\n                        const x = positions.getX(i);\n                        const y = positions.getY(i);\n                        const z = positions.getZ(i);\n                        const innerX = Math.max(0, Math.abs(x) - hw);\n                        const innerY = Math.max(0, Math.abs(y) - hh);\n                        const innerZ = Math.max(0, Math.abs(z) - hd);\n                        const len = Math.sqrt(innerX * innerX + innerY * innerY + innerZ * innerZ);\n                        if (len > 0.0001) {\n                            const scale = radius / len;\n                            const signX = Math.sign(x) || 1;\n                            const signY = Math.sign(y) || 1;\n                            const signZ = Math.sign(z) || 1;\n                            positions.setXYZ(i,\n                                signX * (hw + innerX * scale),\n                                signY * (hh + innerY * scale),\n                                signZ * (hd + innerZ * scale)\n                            );\n                        }\n                    }\n                    positions.needsUpdate = true;\n                    geometry.computeVertexNormals();\n                    return geometry;\n                }\n\n                const zombieGeo = {\n                    torso: createRoundedBox(0.55, 1.0, 0.32, 0.04, 2),\n                    belly: createRoundedBox(0.62, 0.5, 0.38, 0.04, 2),\n                    belt: createRoundedBox(0.56, 0.06, 0.08, 0.02, 2),\n                    skull: createRoundedBox(0.38, 0.38, 0.38, 0.05, 2),\n                    jaw: createRoundedBox(0.28, 0.08, 0.2, 0.025, 2),\n                    teethUpper: new THREE.BoxGeometry(0.2, 0.03, 0.04),\n                    teethLower: new THREE.BoxGeometry(0.18, 0.03, 0.04),\n                    headWound: new THREE.BoxGeometry(0.1, 0.08, 0.03),\n                    eye: createRoundedBox(0.08, 0.06, 0.04, 0.015, 2),\n                    arm: createRoundedBox(0.16, 0.74, 0.16, 0.035, 2),\n                    hand: createRoundedBox(0.12, 0.14, 0.08, 0.025, 2),\n                    leg: createRoundedBox(0.2, 0.64, 0.2, 0.035, 2),\n                    foot: createRoundedBox(0.2, 0.08, 0.28, 0.025, 2),\n                    upperArm: createRoundedBox(0.16, 0.4, 0.16, 0.03, 2),\n                    forearm: createRoundedBox(0.14, 0.35, 0.14, 0.025, 2),\n                    thigh: createRoundedBox(0.2, 0.32, 0.2, 0.03, 2),\n                    shin: createRoundedBox(0.17, 0.3, 0.17, 0.025, 2),\n                    neckStump: createRoundedBox(0.24, 0.08, 0.24, 0.025, 2),\n                    armStump: createRoundedBox(0.16, 0.1, 0.16, 0.025, 2),\n                    legStump: createRoundedBox(0.18, 0.08, 0.18, 0.025, 2)\n                };\n                const zombieSharedMat = {\n                    cloth: makePerfMaterial('lambert', { color: 0x2a2a2a, specular: 0x111111, shininess: 5 }),\n                    bone: makePerfMaterial('lambert', { color: 0xccbb99, specular: 0x444444, shininess: 30 }),\n                    blood: makePerfMaterial('lambert', { color: 0x661111, specular: 0x330000, shininess: 20 }),\n                    belt: makePerfMaterial('lambert', { color: 0x2a1a0a }),\n                    eye: new THREE.MeshBasicMaterial({ color: 0xff2222 }),\n                    stump: makePerfMaterial('lambert', { color: 0x8a2020 })\n                };\n                const visiblePartNames = ['head', 'leftArm', 'rightArm', 'leftLeg', 'rightLeg', 'body', 'belly'];\n                const facePartNames = ['jaw', 'teethUpper', 'teethLower', 'headWound', 'leftEye', 'rightEye'];\n                const zombieSkinColorParts = ['head', 'leftArm', 'rightArm'];\n                const zombieDarkSkinColorParts = ['leftHand', 'rightHand', 'leftFoot', 'rightFoot'];\n                const zombieStumpColorParts = ['neckStump', 'lArmStump', 'rArmStump', 'lLegStump', 'rLegStump'];\n                const childPartParents = {\n                    leftHand: 'leftArm',\n                    rightHand: 'rightArm',\n                    leftFoot: 'leftLeg',\n                    rightFoot: 'rightLeg',\n                    belt: 'body'\n                };\n                const childPartNames = Object.keys(childPartParents);\n                const animatedPartNames = ['body', 'head', 'leftArm', 'rightArm', 'leftLeg', 'rightLeg', 'jaw'];\n                const corpseRelaxPartNames = ['head', 'leftLeg', 'rightLeg', 'body'];\n                const stumpByPart = {\n                    head: 'neckStump',\n                    leftArm: 'lArmStump',\n                    rightArm: 'rArmStump',\n                    leftLeg: 'lLegStump',\n                    rightLeg: 'rLegStump'\n                };\n                const MAX_ACTIVE_ZOMBIES = 200;\n                const ZOMBIE_POOL_SIZE = 220;\n                // Bigger initial pool prevents the synchronous createZombieMesh spike when\n                // a multi-kill burst tries to acquire more proxies than the pool currently holds.\n                const ZOMBIE_PROXY_INITIAL_POOL_SIZE = perfProfile.lowEnd ? 28 : 36;\n                const ZOMBIE_PROXY_WARM_TARGET = perfProfile.lowEnd ? 56 : 72;\n                const ZOMBIE_PROXY_WARM_LOW_WATER = perfProfile.lowEnd ? 10 : 14;\n                const ZOMBIE_PROXY_WARM_BATCH = 1;\n                const ZOMBIE_PART_HEAD = 1 << 0;\n                const ZOMBIE_PART_BODY = 1 << 1;\n                const ZOMBIE_PART_BELLY = 1 << 2;\n                const ZOMBIE_PART_LEFT_ARM = 1 << 3;\n                const ZOMBIE_PART_RIGHT_ARM = 1 << 4;\n                const ZOMBIE_PART_LEFT_LEG = 1 << 5;\n                const ZOMBIE_PART_RIGHT_LEG = 1 << 6;\n                const zombieZeroMatrix = new THREE.Matrix4().makeScale(0, 0, 0);\n                const zombieInstanceScratchColor = new THREE.Color();\n                const zombieInstanceParts = [\n                    'body', 'belt', 'head', 'jaw', 'teethUpper', 'teethLower', 'headWound', 'leftEye', 'rightEye',\n                    'leftArm', 'rightArm', 'leftHand', 'rightHand', 'leftLeg', 'rightLeg', 'leftFoot', 'rightFoot',\n                    'neckStump', 'lArmStump', 'rArmStump', 'lLegStump', 'rLegStump', 'belly'\n                ];\n                const zombieInstancePartIndex = Object.create(null);\n                for (let i = 0; i < zombieInstanceParts.length; i++) zombieInstancePartIndex[zombieInstanceParts[i]] = i;\n                const zombieInstanceScale = new THREE.Vector3(1, 1, 1);\n                const zombieInstanceDirtyMatrixFlags = new Uint8Array(zombieInstanceParts.length);\n                const zombieInstanceDirtyColorFlags = new Uint8Array(zombieInstanceParts.length);\n                const zombieInstanceDirtyMatrixParts = [];\n                const zombieInstanceDirtyColorParts = [];\n                const zombieInstancePartVisible = Object.create(null);\n                for (let i = 0; i < zombieInstanceParts.length; i++) zombieInstancePartVisible[zombieInstanceParts[i]] = new Uint8Array(ZOMBIE_POOL_SIZE);\n\n                const zombieRuntime = {\n                    capacity: ZOMBIE_POOL_SIZE,\n                    activeSlots: [],\n                    freeSlots: [],\n                    slotByEntity: new Map(),\n                    activeIndex: new Int16Array(ZOMBIE_POOL_SIZE),\n                    entityId: new Int32Array(ZOMBIE_POOL_SIZE),\n                    x: new Float32Array(ZOMBIE_POOL_SIZE),\n                    y: new Float32Array(ZOMBIE_POOL_SIZE),\n                    z: new Float32Array(ZOMBIE_POOL_SIZE),\n                    currentHp: new Float32Array(ZOMBIE_POOL_SIZE),\n                    maxHp: new Float32Array(ZOMBIE_POOL_SIZE),\n                    deadPending: new Uint8Array(ZOMBIE_POOL_SIZE),\n                    radius: new Float32Array(ZOMBIE_POOL_SIZE),\n                    invMass: new Float32Array(ZOMBIE_POOL_SIZE),\n                    scale: new Float32Array(ZOMBIE_POOL_SIZE),\n                    speed: new Float32Array(ZOMBIE_POOL_SIZE),\n                    baseSpeed: new Float32Array(ZOMBIE_POOL_SIZE),\n                    tempSpeedMult: new Float32Array(ZOMBIE_POOL_SIZE),\n                    mobilityMult: new Float32Array(ZOMBIE_POOL_SIZE),\n                    attackTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    attackInterval: new Float32Array(ZOMBIE_POOL_SIZE),\n                    animTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    state: new Int8Array(ZOMBIE_POOL_SIZE),\n                    yaw: new Float32Array(ZOMBIE_POOL_SIZE),\n                    damageMult: new Float32Array(ZOMBIE_POOL_SIZE),\n                    baseDamageMult: new Float32Array(ZOMBIE_POOL_SIZE),\n                    armor: new Float32Array(ZOMBIE_POOL_SIZE),\n                    maxArmor: new Float32Array(ZOMBIE_POOL_SIZE),\n                    speedBuffTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    burstTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    burstCooldown: new Float32Array(ZOMBIE_POOL_SIZE),\n                    strafePhase: new Float32Array(ZOMBIE_POOL_SIZE),\n                    screamTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    bossAbilityCooldown: new Float32Array(ZOMBIE_POOL_SIZE),\n                    dashTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    burnTick: new Float32Array(ZOMBIE_POOL_SIZE),\n                    burningTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    burningTickTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    burningDamage: new Float32Array(ZOMBIE_POOL_SIZE),\n                    summonCooldown: new Float32Array(ZOMBIE_POOL_SIZE),\n                    summonsLeft: new Int16Array(ZOMBIE_POOL_SIZE),\n                    isBoss: new Uint8Array(ZOMBIE_POOL_SIZE),\n                    rageTriggered: new Uint8Array(ZOMBIE_POOL_SIZE),\n                    instanceSlot: new Int16Array(ZOMBIE_POOL_SIZE),\n                    bodyMask: new Uint8Array(ZOMBIE_POOL_SIZE),\n                    partHeadHp: new Float32Array(ZOMBIE_POOL_SIZE),\n                    partLeftArmHp: new Float32Array(ZOMBIE_POOL_SIZE),\n                    partRightArmHp: new Float32Array(ZOMBIE_POOL_SIZE),\n                    partLeftLegHp: new Float32Array(ZOMBIE_POOL_SIZE),\n                    partRightLegHp: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactDuration: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactPitch: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactRoll: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactYaw: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactMoveX: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactMoveZ: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactSlowTimer: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactSlow: new Float32Array(ZOMBIE_POOL_SIZE),\n                    impactPart: new Uint8Array(ZOMBIE_POOL_SIZE),\n                    meshRef: new Array(ZOMBIE_POOL_SIZE),\n                    typeName: new Array(ZOMBIE_POOL_SIZE),\n                    positionFacade: new Array(ZOMBIE_POOL_SIZE),\n                    healthFacade: new Array(ZOMBIE_POOL_SIZE),\n                    colliderFacade: new Array(ZOMBIE_POOL_SIZE),\n                    zombieFacade: new Array(ZOMBIE_POOL_SIZE),\n                    bodyPartsFacade: new Array(ZOMBIE_POOL_SIZE),\n                    bodyPartHealthFacade: new Array(ZOMBIE_POOL_SIZE),\n                    isBurningColorActive: new Uint8Array(ZOMBIE_POOL_SIZE)\n                };\n                const zombieRenderLastFrame = new Uint32Array(ZOMBIE_POOL_SIZE);\n                const zombieRenderForce = new Uint8Array(ZOMBIE_POOL_SIZE);\n                const zombieRenderInView = new Uint8Array(ZOMBIE_POOL_SIZE);\n                const aiFallbackLastUpdateFrame = new Uint32Array(ZOMBIE_POOL_SIZE);\n                zombieRuntime.activeIndex.fill(-1);\n                zombieRuntime.instanceSlot.fill(-1);\n                for (let i = ZOMBIE_POOL_SIZE - 1; i >= 0; i--) zombieRuntime.freeSlots.push(i);\n                const zombieCleanupIds = [];\n\n                function resetZombieImpactSlot(slot) {\n                    zombieRuntime.impactTimer[slot] = 0;\n                    zombieRuntime.impactDuration[slot] = 0;\n                    zombieRuntime.impactPitch[slot] = 0;\n                    zombieRuntime.impactRoll[slot] = 0;\n                    zombieRuntime.impactYaw[slot] = 0;\n                    zombieRuntime.impactMoveX[slot] = 0;\n                    zombieRuntime.impactMoveZ[slot] = 0;\n                    zombieRuntime.impactSlowTimer[slot] = 0;\n                    zombieRuntime.impactSlow[slot] = 0;\n                    zombieRuntime.impactPart[slot] = ZOMBIE_PART_BODY;\n                }\n\n                function getZombieRuntimeSlot(entityId) {\n                    const slot = zombieRuntime.slotByEntity.get(entityId);\n                    return slot === undefined ? -1 : slot;\n                }\n\n                function acquireZombieRuntimeSlot(entityId) {\n                    const slot = zombieRuntime.freeSlots.pop();\n                    if (slot === undefined) return -1;\n                    zombieRuntime.slotByEntity.set(entityId, slot);\n                    zombieRuntime.entityId[slot] = entityId;\n                    zombieRuntime.activeIndex[slot] = zombieRuntime.activeSlots.length;\n                    zombieRuntime.activeSlots.push(slot);\n                    zombieRuntime.deadPending[slot] = 0;\n                    zombieRuntime.tempSpeedMult[slot] = 1;\n                    zombieRuntime.mobilityMult[slot] = 1;\n                    zombieRuntime.state[slot] = 1;\n                    zombieRuntime.attackInterval[slot] = 1;\n                    zombieRuntime.speedBuffTimer[slot] = 0;\n                    zombieRuntime.burstTimer[slot] = 0;\n                    zombieRuntime.burstCooldown[slot] = 0;\n                    zombieRuntime.strafePhase[slot] = 0;\n                    zombieRuntime.screamTimer[slot] = 0;\n                    zombieRuntime.bossAbilityCooldown[slot] = 0;\n                    zombieRuntime.dashTimer[slot] = 0;\n                    zombieRuntime.burnTick[slot] = 0;\n                    zombieRuntime.burningTimer[slot] = 0;\n                    zombieRuntime.burningTickTimer[slot] = 0;\n                    zombieRuntime.burningDamage[slot] = 0;\n                    zombieRuntime.isBurningColorActive[slot] = 0;\n                    flameContactNextFxAt[slot] = 0;\n                    zombieRuntime.summonCooldown[slot] = 0;\n                    zombieRuntime.summonsLeft[slot] = 0;\n                    zombieRuntime.rageTriggered[slot] = 0;\n                    zombieRuntime.bodyMask[slot] = ZOMBIE_PART_HEAD | ZOMBIE_PART_BODY | ZOMBIE_PART_LEFT_ARM | ZOMBIE_PART_RIGHT_ARM | ZOMBIE_PART_LEFT_LEG | ZOMBIE_PART_RIGHT_LEG;\n                    zombieRenderLastFrame[slot] = 0;\n                    zombieRenderForce[slot] = 1;\n                    zombieRenderInView[slot] = 0;\n                    aiFallbackLastUpdateFrame[slot] = 0;\n                    resetZombieImpactSlot(slot);\n                    return slot;\n                }\n\n                function releaseZombieRuntimeSlot(entityId) {\n                    const slot = getZombieRuntimeSlot(entityId);\n                    if (slot < 0) return;\n                    const idx = zombieRuntime.activeIndex[slot];\n                    if (idx >= 0) {\n                        const last = zombieRuntime.activeSlots.pop();\n                        if (idx < zombieRuntime.activeSlots.length) {\n                            zombieRuntime.activeSlots[idx] = last;\n                            zombieRuntime.activeIndex[last] = idx;\n                        }\n                    }\n                    zombieRuntime.activeIndex[slot] = -1;\n                    zombieRuntime.slotByEntity.delete(entityId);\n                    zombieRuntime.entityId[slot] = -1;\n                    zombieRuntime.currentHp[slot] = 0;\n                    zombieRuntime.maxHp[slot] = 0;\n                    zombieRuntime.deadPending[slot] = 0;\n                    zombieRuntime.radius[slot] = 0;\n                    zombieRuntime.invMass[slot] = 0;\n                    zombieRuntime.scale[slot] = 0;\n                    zombieRuntime.speed[slot] = 0;\n                    zombieRuntime.baseSpeed[slot] = 0;\n                    zombieRuntime.tempSpeedMult[slot] = 0;\n                    zombieRuntime.mobilityMult[slot] = 0;\n                    zombieRuntime.attackTimer[slot] = 0;\n                    zombieRuntime.attackInterval[slot] = 0;\n                    zombieRuntime.animTimer[slot] = 0;\n                    zombieRuntime.state[slot] = 0;\n                    zombieRuntime.yaw[slot] = 0;\n                    zombieRuntime.damageMult[slot] = 0;\n                    zombieRuntime.baseDamageMult[slot] = 0;\n                    zombieRuntime.armor[slot] = 0;\n                    zombieRuntime.maxArmor[slot] = 0;\n                    zombieRuntime.speedBuffTimer[slot] = 0;\n                    zombieRuntime.burstTimer[slot] = 0;\n                    zombieRuntime.burstCooldown[slot] = 0;\n                    zombieRuntime.strafePhase[slot] = 0;\n                    zombieRuntime.screamTimer[slot] = 0;\n                    zombieRuntime.bossAbilityCooldown[slot] = 0;\n                    zombieRuntime.dashTimer[slot] = 0;\n                    zombieRuntime.burnTick[slot] = 0;\n                    zombieRuntime.burningTimer[slot] = 0;\n                    zombieRuntime.burningTickTimer[slot] = 0;\n                    zombieRuntime.burningDamage[slot] = 0;\n                    zombieRuntime.isBurningColorActive[slot] = 0;\n                    flameContactNextFxAt[slot] = 0;\n                    zombieRuntime.summonCooldown[slot] = 0;\n                    zombieRuntime.summonsLeft[slot] = 0;\n                    zombieRuntime.isBoss[slot] = 0;\n                    zombieRuntime.rageTriggered[slot] = 0;\n                    zombieRuntime.instanceSlot[slot] = -1;\n                    zombieRuntime.bodyMask[slot] = 0;\n                    zombieRuntime.meshRef[slot] = null;\n                    zombieRuntime.typeName[slot] = null;\n                    zombieRuntime.positionFacade[slot] = null;\n                    zombieRuntime.healthFacade[slot] = null;\n                    zombieRuntime.colliderFacade[slot] = null;\n                    zombieRuntime.zombieFacade[slot] = null;\n                    zombieRuntime.bodyPartsFacade[slot] = null;\n                    zombieRuntime.bodyPartHealthFacade[slot] = null;\n                    zombieRenderLastFrame[slot] = 0;\n                    zombieRenderForce[slot] = 0;\n                    zombieRenderInView[slot] = 0;\n                    aiFallbackLastUpdateFrame[slot] = 0;\n                    resetZombieImpactSlot(slot);\n                    zombieRuntime.freeSlots.push(slot);\n                }\n\n                function defineSoAProp(target, name, array, slot) {\n                    Object.defineProperty(target, name, {\n                        get() { return array[slot]; },\n                        set(value) { array[slot] = value; },\n                        enumerable: true\n                    });\n                }\n\n                function defineSoABoolProp(target, name, array, slot, bit) {\n                    Object.defineProperty(target, name, {\n                        get() { return (array[slot] & bit) !== 0; },\n                        set(value) {\n                            if (value) array[slot] |= bit;\n                            else array[slot] &= ~bit;\n                        },\n                        enumerable: true\n                    });\n                }\n\n                function createZombiePositionFacade(slot, x, y, z) {\n                    zombieRuntime.x[slot] = x;\n                    zombieRuntime.y[slot] = y;\n                    zombieRuntime.z[slot] = z;\n                    const facade = {};\n                    defineSoAProp(facade, 'x', zombieRuntime.x, slot);\n                    defineSoAProp(facade, 'y', zombieRuntime.y, slot);\n                    defineSoAProp(facade, 'z', zombieRuntime.z, slot);\n                    zombieRuntime.positionFacade[slot] = facade;\n                    return facade;\n                }\n\n                function createZombieHealthFacade(slot, current, max) {\n                    zombieRuntime.currentHp[slot] = current;\n                    zombieRuntime.maxHp[slot] = max;\n                    zombieRuntime.deadPending[slot] = 0;\n                    const facade = {};\n                    defineSoAProp(facade, 'current', zombieRuntime.currentHp, slot);\n                    defineSoAProp(facade, 'max', zombieRuntime.maxHp, slot);\n                    Object.defineProperty(facade, 'deadPending', {\n                        get() { return zombieRuntime.deadPending[slot] !== 0; },\n                        set(value) { zombieRuntime.deadPending[slot] = value ? 1 : 0; },\n                        enumerable: true\n                    });\n                    zombieRuntime.healthFacade[slot] = facade;\n                    return facade;\n                }\n\n                function createZombieColliderFacade(slot, collider) {\n                    zombieRuntime.radius[slot] = collider.radius;\n                    zombieRuntime.invMass[slot] = collider.invMass;\n                    const facade = {};\n                    defineSoAProp(facade, 'radius', zombieRuntime.radius, slot);\n                    defineSoAProp(facade, 'invMass', zombieRuntime.invMass, slot);\n                    zombieRuntime.colliderFacade[slot] = facade;\n                    return facade;\n                }\n\n                function createZombieBodyPartsFacade(slot, bodyParts) {\n                    let mask = ZOMBIE_PART_HEAD | ZOMBIE_PART_BODY | ZOMBIE_PART_LEFT_ARM | ZOMBIE_PART_RIGHT_ARM | ZOMBIE_PART_LEFT_LEG | ZOMBIE_PART_RIGHT_LEG;\n                    if (bodyParts.belly) mask |= ZOMBIE_PART_BELLY;\n                    zombieRuntime.bodyMask[slot] = mask;\n                    zombieRuntime.partHeadHp[slot] = bodyParts.partHealth.head;\n                    zombieRuntime.partLeftArmHp[slot] = bodyParts.partHealth.leftArm;\n                    zombieRuntime.partRightArmHp[slot] = bodyParts.partHealth.rightArm;\n                    zombieRuntime.partLeftLegHp[slot] = bodyParts.partHealth.leftLeg;\n                    zombieRuntime.partRightLegHp[slot] = bodyParts.partHealth.rightLeg;\n                    const partHealth = {};\n                    defineSoAProp(partHealth, 'head', zombieRuntime.partHeadHp, slot);\n                    defineSoAProp(partHealth, 'leftArm', zombieRuntime.partLeftArmHp, slot);\n                    defineSoAProp(partHealth, 'rightArm', zombieRuntime.partRightArmHp, slot);\n                    defineSoAProp(partHealth, 'leftLeg', zombieRuntime.partLeftLegHp, slot);\n                    defineSoAProp(partHealth, 'rightLeg', zombieRuntime.partRightLegHp, slot);\n                    const facade = { partHealth };\n                    defineSoABoolProp(facade, 'head', zombieRuntime.bodyMask, slot, ZOMBIE_PART_HEAD);\n                    defineSoABoolProp(facade, 'body', zombieRuntime.bodyMask, slot, ZOMBIE_PART_BODY);\n                    defineSoABoolProp(facade, 'belly', zombieRuntime.bodyMask, slot, ZOMBIE_PART_BELLY);\n                    defineSoABoolProp(facade, 'leftArm', zombieRuntime.bodyMask, slot, ZOMBIE_PART_LEFT_ARM);\n                    defineSoABoolProp(facade, 'rightArm', zombieRuntime.bodyMask, slot, ZOMBIE_PART_RIGHT_ARM);\n                    defineSoABoolProp(facade, 'leftLeg', zombieRuntime.bodyMask, slot, ZOMBIE_PART_LEFT_LEG);\n                    defineSoABoolProp(facade, 'rightLeg', zombieRuntime.bodyMask, slot, ZOMBIE_PART_RIGHT_LEG);\n                    zombieRuntime.bodyPartHealthFacade[slot] = partHealth;\n                    zombieRuntime.bodyPartsFacade[slot] = facade;\n                    return facade;\n                }\n\n                function createZombieFacade(slot, data) {\n                    zombieRuntime.speed[slot] = data.speed;\n                    zombieRuntime.baseSpeed[slot] = data.baseSpeed;\n                    zombieRuntime.scale[slot] = data.scale || 1;\n                    zombieRuntime.tempSpeedMult[slot] = data.tempSpeedMult || 1;\n                    zombieRuntime.mobilityMult[slot] = data.mobilityMult || 1;\n                    zombieRuntime.attackTimer[slot] = data.attackTimer || 0;\n                    zombieRuntime.attackInterval[slot] = data.attackInterval || getZombieAttackInterval(data.typeName, !!data.isBoss);\n                    zombieRuntime.animTimer[slot] = data.animTimer || 0;\n                    zombieRuntime.state[slot] = data.state || 1;\n                    zombieRuntime.yaw[slot] = data.yaw || 0;\n                    zombieRuntime.damageMult[slot] = data.damageMult || 1;\n                    zombieRuntime.baseDamageMult[slot] = data.baseDamageMult || 1;\n                    zombieRuntime.armor[slot] = data.armor || 0;\n                    zombieRuntime.maxArmor[slot] = data.maxArmor || 0;\n                    zombieRuntime.isBoss[slot] = data.isBoss ? 1 : 0;\n                    zombieRuntime.instanceSlot[slot] = data.instanceSlot;\n                    zombieRuntime.meshRef[slot] = data.meshRef || null;\n                    zombieRuntime.typeName[slot] = data.typeName;\n                    const facade = {\n                        idlePhase: data.idlePhase || 0,\n                        typeName: data.typeName\n                    };\n                    zombieRuntime.summonsLeft[slot] = data.summonsLeft || 0;\n                    defineSoAProp(facade, 'speed', zombieRuntime.speed, slot);\n                    defineSoAProp(facade, 'baseSpeed', zombieRuntime.baseSpeed, slot);\n                    defineSoAProp(facade, 'scale', zombieRuntime.scale, slot);\n                    defineSoAProp(facade, 'tempSpeedMult', zombieRuntime.tempSpeedMult, slot);\n                    defineSoAProp(facade, 'mobilityMult', zombieRuntime.mobilityMult, slot);\n                    defineSoAProp(facade, 'attackTimer', zombieRuntime.attackTimer, slot);\n                    defineSoAProp(facade, 'attackInterval', zombieRuntime.attackInterval, slot);\n                    defineSoAProp(facade, 'animTimer', zombieRuntime.animTimer, slot);\n                    defineSoAProp(facade, 'state', zombieRuntime.state, slot);\n                    defineSoAProp(facade, 'yaw', zombieRuntime.yaw, slot);\n                    defineSoAProp(facade, 'damageMult', zombieRuntime.damageMult, slot);\n                    defineSoAProp(facade, 'baseDamageMult', zombieRuntime.baseDamageMult, slot);\n                    defineSoAProp(facade, 'armor', zombieRuntime.armor, slot);\n                    defineSoAProp(facade, 'maxArmor', zombieRuntime.maxArmor, slot);\n                    defineSoAProp(facade, 'speedBuffTimer', zombieRuntime.speedBuffTimer, slot);\n                    defineSoAProp(facade, 'burstTimer', zombieRuntime.burstTimer, slot);\n                    defineSoAProp(facade, 'burstCooldown', zombieRuntime.burstCooldown, slot);\n                    defineSoAProp(facade, 'strafePhase', zombieRuntime.strafePhase, slot);\n                    defineSoAProp(facade, 'screamTimer', zombieRuntime.screamTimer, slot);\n                    defineSoAProp(facade, 'bossAbilityCooldown', zombieRuntime.bossAbilityCooldown, slot);\n                    defineSoAProp(facade, 'dashTimer', zombieRuntime.dashTimer, slot);\n                    defineSoAProp(facade, 'burnTick', zombieRuntime.burnTick, slot);\n                    defineSoAProp(facade, 'burningTimer', zombieRuntime.burningTimer, slot);\n                    defineSoAProp(facade, 'burningTickTimer', zombieRuntime.burningTickTimer, slot);\n                    defineSoAProp(facade, 'burningDamage', zombieRuntime.burningDamage, slot);\n                    defineSoAProp(facade, 'summonCooldown', zombieRuntime.summonCooldown, slot);\n                    defineSoAProp(facade, 'summonsLeft', zombieRuntime.summonsLeft, slot);\n                    Object.defineProperty(facade, 'isBoss', {\n                        get() { return zombieRuntime.isBoss[slot] !== 0; },\n                        set(value) { zombieRuntime.isBoss[slot] = value ? 1 : 0; },\n                        enumerable: true\n                    });\n                    Object.defineProperty(facade, 'rageTriggered', {\n                        get() { return zombieRuntime.rageTriggered[slot] !== 0; },\n                        set(value) { zombieRuntime.rageTriggered[slot] = value ? 1 : 0; },\n                        enumerable: true\n                    });\n                    Object.defineProperty(facade, 'instanceSlot', {\n                        get() { return zombieRuntime.instanceSlot[slot]; },\n                        set(value) { zombieRuntime.instanceSlot[slot] = value; },\n                        enumerable: true\n                    });\n                    Object.defineProperty(facade, 'meshRef', {\n                        get() { return zombieRuntime.meshRef[slot]; },\n                        set(value) { zombieRuntime.meshRef[slot] = value; },\n                        enumerable: true\n                    });\n                    zombieRuntime.zombieFacade[slot] = facade;\n                    return facade;\n                }\n\n                function ensureWhiteVertexColors(geometry) {\n                    if (!geometry || geometry.attributes.color) return geometry;\n                    const colorCount = geometry.attributes.position.count * 3;\n                    const colors = new Float32Array(colorCount);\n                    colors.fill(1);\n                    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n                    return geometry;\n                }\n\n                function cacheZombieParts(mesh) {\n                    const parts = {\n                        body: null, belt: null, belly: null, head: null, jaw: null, teethUpper: null, teethLower: null, headWound: null,\n                        leftEye: null, rightEye: null,\n                        leftArm: null, rightArm: null, leftHand: null, rightHand: null,\n                        leftLeg: null, rightLeg: null, leftFoot: null, rightFoot: null,\n                        neckStump: null, lArmStump: null, rArmStump: null, lLegStump: null, rLegStump: null\n                    };\n                    mesh.traverse(child => {\n                        if (Object.prototype.hasOwnProperty.call(parts, child.name)) parts[child.name] = child;\n                    });\n                    mesh.userData.parts = parts;\n                    return parts;\n                }\n\n                function registerInstancePart(root, key, obj) {\n                    if (!root.userData.instanceParts) root.userData.instanceParts = {};\n                    root.userData.instanceParts[key] = obj;\n                    return obj;\n                }\n\n                function setProxyMeshRenderable(root, renderable) {\n                    root.userData.proxyRenderable = renderable;\n                    if (renderable) attachToScene(root);\n                    else detachFromScene(root);\n                    root.visible = renderable;\n                    root.traverse(child => {\n                        if (child === root) return;\n                        child.visible = renderable && child.userData.logicalVisible !== false;\n                    });\n                }\n\n                function setProxyPartLogicalVisible(root, part, visible) {\n                    if (!part) return;\n                    part.userData.logicalVisible = visible;\n                    part.visible = root.userData.proxyRenderable !== false && visible;\n                }\n\n                function syncZombieBodyParts(mesh, bodyParts) {\n                    if (!mesh) return;\n                    const parts = mesh.userData.parts || cacheZombieParts(mesh);\n                    for (let i = 0; i < visiblePartNames.length; i++) {\n                        const name = visiblePartNames[i];\n                        if (!parts[name]) continue;\n                        const visible = name === 'belly'\n                            ? !!bodyParts && zombiePartAlive(bodyParts, 'belly') && zombiePartAlive(bodyParts, 'body')\n                            : !bodyParts || zombiePartAlive(bodyParts, name);\n                        setProxyPartLogicalVisible(mesh, parts[name], visible);\n                    }\n                    for (let i = 0; i < facePartNames.length; i++) {\n                        const name = facePartNames[i];\n                        if (parts[name]) {\n                            const visible = !bodyParts || zombiePartAlive(bodyParts, 'head');\n                            setProxyPartLogicalVisible(mesh, parts[name], visible);\n                        }\n                    }\n                    for (const name in childPartParents) {\n                        if (parts[name]) {\n                            const parent = childPartParents[name];\n                            const visible = !bodyParts || zombiePartAlive(bodyParts, parent);\n                            setProxyPartLogicalVisible(mesh, parts[name], visible);\n                        }\n                    }\n                    for (const partName in stumpByPart) {\n                        const stump = parts[stumpByPart[partName]];\n                        if (!stump) continue;\n                        const visible = !!bodyParts && !zombiePartAlive(bodyParts, partName);\n                        setProxyPartLogicalVisible(mesh, stump, visible);\n                        stump.rotation.set(0, 0, 0);\n                    }\n                }\n\n                function makeInstanceMaterial(baseColor, lit = true) {\n                    const params = { color: baseColor, vertexColors: true };\n                    return lit\n                        ? new THREE.MeshPhongMaterial(Object.assign(params, { specular: 0x222222, shininess: 10 }))\n                        : new THREE.MeshBasicMaterial(params);\n                }\n\n                const zombieGpuPartCode = {\n                    body: 1, belt: 1, belly: 1,\n                    head: 2, teethUpper: 2, headWound: 2, leftEye: 2, rightEye: 2,\n                    jaw: 3, teethLower: 3,\n                    leftArm: 4, leftHand: 4,\n                    rightArm: 5, rightHand: 5,\n                    leftLeg: 6, leftFoot: 6,\n                    rightLeg: 7, rightFoot: 7\n                };\n\n                function configureZombieGpuMaterial(material, partCode) {\n                    const normalPoseChunk = `\n{\n    float nRootY;\n    vec3 nBodyA;\n    vec3 nHeadA;\n    float nJawA;\n    vec2 nLeftArmA;\n    vec2 nRightArmA;\n    float nLeftLegA;\n    float nRightLegA;\n    zombiePose(\n        instanceZombieAnim.x, instanceZombieAnim.y, instanceZombieAnim.w,\n        nRootY, nBodyA, nHeadA, nJawA,\n        nLeftArmA, nRightArmA, nLeftLegA, nRightLegA\n    );\n    vec3 nAngles = vec3(0.0);\n#if ${partCode} == 1\n    nAngles = nBodyA + vec3(instanceZombieImpact.x, instanceZombieImpact.z * 0.55, instanceZombieImpact.y);\n#elif ${partCode} == 2\n    nAngles = nHeadA + vec3(instanceZombieImpact.x * (abs(instanceZombieImpact.w - 1.0) < 0.5 ? 1.05 : 0.38), instanceZombieImpact.z * 0.45, instanceZombieImpact.y * 0.35);\n#elif ${partCode} == 3\n    nAngles = nHeadA + vec3(nJawA + instanceZombieImpact.x * (abs(instanceZombieImpact.w - 1.0) < 0.5 ? 1.05 : 0.38), instanceZombieImpact.z * 0.45, instanceZombieImpact.y * 0.35);\n#elif ${partCode} == 4\n    nAngles = vec3(nLeftArmA.x, 0.0, nLeftArmA.y);\n    if (abs(instanceZombieImpact.w - 8.0) < 0.5) nAngles += vec3(instanceZombieImpact.x * 1.35, 0.0, instanceZombieImpact.y * 1.5);\n#elif ${partCode} == 5\n    nAngles = vec3(nRightArmA.x, 0.0, nRightArmA.y);\n    if (abs(instanceZombieImpact.w - 16.0) < 0.5) nAngles += vec3(instanceZombieImpact.x * 1.35, 0.0, instanceZombieImpact.y * 1.5);\n#elif ${partCode} == 6\n    nAngles.x = nLeftLegA + (abs(instanceZombieImpact.w - 32.0) < 0.5 ? instanceZombieImpact.x * 0.85 : 0.0);\n#elif ${partCode} == 7\n    nAngles.x = nRightLegA + (abs(instanceZombieImpact.w - 64.0) < 0.5 ? instanceZombieImpact.x * 0.85 : 0.0);\n#endif\n    objectNormal = zombieRotateEuler(objectNormal, nAngles);\n}`;\n                    material.onBeforeCompile = (shader) => {\n                        shader.vertexShader = shader.vertexShader\n                            .replace('#include <common>', `#include <common>\nattribute vec4 instanceZombieAnim;\nattribute vec4 instanceZombieImpact;\nattribute vec3 instanceZombiePivot;\nattribute float instanceZombieBurn;\nvarying float vZombieBurn;\nvarying float vZombieBurnPulse;\n\nvec3 zombieRotateX(vec3 p, float a) {\n    float c = cos(a), s = sin(a);\n    return vec3(p.x, c * p.y - s * p.z, s * p.y + c * p.z);\n}\nvec3 zombieRotateY(vec3 p, float a) {\n    float c = cos(a), s = sin(a);\n    return vec3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);\n}\nvec3 zombieRotateZ(vec3 p, float a) {\n    float c = cos(a), s = sin(a);\n    return vec3(c * p.x - s * p.y, s * p.x + c * p.y, p.z);\n}\nvec3 zombieRotateEuler(vec3 p, vec3 a) {\n    return zombieRotateZ(zombieRotateY(zombieRotateX(p, a.x), a.y), a.z);\n}\nvoid zombiePose(\n    float t, float state, float bodyMask,\n    out float rootY, out vec3 bodyA, out vec3 headA, out float jawA,\n    out vec2 leftArmA, out vec2 rightArmA, out float leftLegA, out float rightLegA\n) {\n    rootY = 0.0;\n    bodyA = vec3(0.0);\n    headA = vec3(0.0);\n    jawA = 0.0;\n    leftArmA = vec2(0.0);\n    rightArmA = vec2(0.0);\n    leftLegA = 0.0;\n    rightLegA = 0.0;\n    bool leftLegAlive = mod(floor(bodyMask / 32.0), 2.0) > 0.5;\n    bool rightLegAlive = mod(floor(bodyMask / 64.0), 2.0) > 0.5;\n    bool noLegs = !leftLegAlive && !rightLegAlive;\n    bool oneLeg = leftLegAlive != rightLegAlive;\n    if (noLegs) {\n        rootY = -0.7;\n        bodyA = vec3(0.8, 0.0, sin(t * 0.5) * 0.05);\n        headA = vec3(-0.4, 0.0, sin(t * 0.8) * 0.06);\n        leftArmA = vec2(-1.2 + sin(t) * 0.5, 0.2);\n        rightArmA = vec2(-1.2 + sin(t + 3.14159265) * 0.5, -0.2);\n        jawA = 0.05 + abs(sin(t * 2.0)) * 0.08;\n        return;\n    }\n    if (oneLeg) {\n        rootY = sin(t * 1.5) * 0.06 - 0.15;\n        bodyA = vec3(0.1, 0.0, sin(t * 1.5) * 0.12);\n    }\n    if (state < 1.5) {\n        float slowT = t * 0.7;\n        if (!oneLeg) {\n            rootY = abs(sin(slowT * 1.8)) * 0.06 + sin(slowT * 0.9) * 0.02;\n            bodyA = vec3(\n                0.25 + sin(slowT * 0.8) * 0.08,\n                sin(slowT * 0.4) * 0.06,\n                sin(slowT * 0.6) * 0.12\n            );\n        }\n        headA = vec3(\n            0.2 + sin(slowT * 1.2) * 0.1,\n            sin(slowT * 0.3) * 0.08,\n            0.15 + sin(slowT * 0.5) * 0.12\n        );\n        jawA = 0.08 + abs(sin(slowT * 0.7)) * 0.06;\n        leftArmA = vec2(-1.1 + sin(slowT * 0.6) * 0.2, 0.15 + sin(slowT * 0.4) * 0.05);\n        rightArmA = vec2(-0.8 + sin(slowT * 0.5 + 1.2) * 0.25, -0.2 + sin(slowT * 0.35) * 0.06);\n        leftLegA = sin(slowT) * 0.35;\n        rightLegA = sin(slowT + 3.14159265) * 0.25 + 0.1;\n    } else {\n        float atk = sin(t * 5.0);\n        bodyA = vec3(0.15 + abs(atk) * 0.1, 0.0, sin(t * 3.0) * 0.05);\n        headA = vec3(0.2 + atk * 0.12, 0.0, sin(t * 4.0) * 0.08);\n        jawA = 0.15 + abs(atk) * 0.2;\n        leftArmA = vec2(-1.3 + atk * 0.4, 0.2 + sin(t * 6.0) * 0.1);\n        rightArmA = vec2(-1.3 - atk * 0.4, -0.2 + sin(t * 6.0 + 3.14159265) * 0.1);\n        leftLegA = sin(t * 2.0) * 0.1;\n        rightLegA = sin(t * 2.0 + 3.14159265) * 0.1;\n    }\n}`)\n                            .replace('#include <defaultnormal_vertex>', `${normalPoseChunk}\n#include <defaultnormal_vertex>`)\n                            .replace('#include <project_vertex>', `\nfloat zombieRootY;\nvec3 zombieBodyA;\nvec3 zombieHeadA;\nfloat zombieJawA;\nvec2 zombieLeftArmA;\nvec2 zombieRightArmA;\nfloat zombieLeftLegA;\nfloat zombieRightLegA;\nzombiePose(\n    instanceZombieAnim.x, instanceZombieAnim.y, instanceZombieAnim.w,\n    zombieRootY, zombieBodyA, zombieHeadA, zombieJawA,\n    zombieLeftArmA, zombieRightArmA, zombieLeftLegA, zombieRightLegA\n);\nvec3 zombieAngles = vec3(0.0);\n#if ${partCode} == 1\n    zombieAngles = zombieBodyA;\n#elif ${partCode} == 2\n    zombieAngles = zombieHeadA;\n#elif ${partCode} == 3\n    zombieAngles = zombieHeadA + vec3(zombieJawA, 0.0, 0.0);\n#elif ${partCode} == 4\n    zombieAngles = vec3(zombieLeftArmA.x, 0.0, zombieLeftArmA.y);\n#elif ${partCode} == 5\n    zombieAngles = vec3(zombieRightArmA.x, 0.0, zombieRightArmA.y);\n#elif ${partCode} == 6\n    zombieAngles = vec3(zombieLeftLegA, 0.0, 0.0);\n#elif ${partCode} == 7\n    zombieAngles = vec3(zombieRightLegA, 0.0, 0.0);\n#endif\nfloat zombieImpactPart = instanceZombieImpact.w;\n#if ${partCode} == 1\n    zombieAngles += vec3(instanceZombieImpact.x, instanceZombieImpact.z * 0.55, instanceZombieImpact.y);\n#elif ${partCode} == 2 || ${partCode} == 3\n    float zombieHeadImpact = abs(zombieImpactPart - 1.0) < 0.5 ? 1.05 : 0.38;\n    zombieAngles += vec3(instanceZombieImpact.x * zombieHeadImpact, instanceZombieImpact.z * 0.45, instanceZombieImpact.y * 0.35);\n#elif ${partCode} == 4\n    if (abs(zombieImpactPart - 8.0) < 0.5) zombieAngles += vec3(instanceZombieImpact.x * 1.35, 0.0, instanceZombieImpact.y * 1.5);\n#elif ${partCode} == 5\n    if (abs(zombieImpactPart - 16.0) < 0.5) zombieAngles += vec3(instanceZombieImpact.x * 1.35, 0.0, instanceZombieImpact.y * 1.5);\n#elif ${partCode} == 6\n    if (abs(zombieImpactPart - 32.0) < 0.5) zombieAngles.x += instanceZombieImpact.x * 0.85;\n#elif ${partCode} == 7\n    if (abs(zombieImpactPart - 64.0) < 0.5) zombieAngles.x += instanceZombieImpact.x * 0.85;\n#endif\nvec3 zombieWorldPosition = (instanceMatrix * vec4(transformed, 1.0)).xyz;\nvec3 zombiePivot = instanceZombiePivot + vec3(0.0, zombieRootY, 0.0);\nzombieWorldPosition.y += zombieRootY;\nvec3 zombieLocalDelta = zombieWorldPosition - zombiePivot;\nzombieLocalDelta = zombieRotateY(zombieLocalDelta, -instanceZombieAnim.z);\nzombieLocalDelta = zombieRotateEuler(zombieLocalDelta, zombieAngles);\nzombieLocalDelta = zombieRotateY(zombieLocalDelta, instanceZombieAnim.z);\nvec4 mvPosition = modelViewMatrix * vec4(zombiePivot + zombieLocalDelta, 1.0);\ngl_Position = projectionMatrix * mvPosition;\nvZombieBurn = instanceZombieBurn;\nvZombieBurnPulse = 0.45 + sin(instanceZombieAnim.x * 2.4 + instanceZombiePivot.x * 0.73 + instanceZombiePivot.z * 0.41) * 0.15;\n`);\n                        shader.fragmentShader = shader.fragmentShader\n                            .replace('#include <common>', `#include <common>\nvarying float vZombieBurn;\nvarying float vZombieBurnPulse;`)\n                            .replace('#include <color_fragment>', `#include <color_fragment>\nfloat zombieBurn = clamp(vZombieBurn, 0.0, 1.0);\nvec3 zombieBurnTint = vec3(0.59, 0.047, 0.039) * vZombieBurnPulse;\n#if ${partCode} == 2\n    zombieBurnTint = vec3(1.0, 0.067, 0.0);\n#endif\ndiffuseColor.rgb = mix(diffuseColor.rgb, zombieBurnTint, zombieBurn * 0.88);\n`);\n                    };\n                    material.customProgramCacheKey = () => `zombie-gpu-part-${partCode}`;\n                }\n\n                function createZombieInstanceRenderer(capacityPerSide = ZOMBIE_POOL_SIZE, meshNamePrefix = 'zombie', gpuAnimated = false) {\n                    const specs = {\n                        body: { geo: zombieGeo.torso, mat: makeInstanceMaterial(0xffffff) },\n                        belt: { geo: zombieGeo.belt, mat: makeInstanceMaterial(0xffffff) },\n                        head: { geo: zombieGeo.skull, mat: makeInstanceMaterial(0xffffff) },\n                        jaw: { geo: zombieGeo.jaw, mat: makeInstanceMaterial(0xffffff) },\n                        teethUpper: { geo: zombieGeo.teethUpper, mat: makeInstanceMaterial(0xffffff) },\n                        teethLower: { geo: zombieGeo.teethLower, mat: makeInstanceMaterial(0xffffff) },\n                        headWound: { geo: zombieGeo.headWound, mat: makeInstanceMaterial(0xffffff) },\n                        leftEye: { geo: zombieGeo.eye, mat: makeInstanceMaterial(0xffffff, false) },\n                        rightEye: { geo: zombieGeo.eye, mat: makeInstanceMaterial(0xffffff, false) },\n                        leftArm: { geo: zombieGeo.arm, mat: makeInstanceMaterial(0xffffff) },\n                        rightArm: { geo: zombieGeo.arm, mat: makeInstanceMaterial(0xffffff) },\n                        leftHand: { geo: zombieGeo.hand, mat: makeInstanceMaterial(0xffffff) },\n                        rightHand: { geo: zombieGeo.hand, mat: makeInstanceMaterial(0xffffff) },\n                        leftLeg: { geo: zombieGeo.leg, mat: makeInstanceMaterial(0xffffff) },\n                        rightLeg: { geo: zombieGeo.leg, mat: makeInstanceMaterial(0xffffff) },\n                        leftFoot: { geo: zombieGeo.foot, mat: makeInstanceMaterial(0xffffff) },\n                        rightFoot: { geo: zombieGeo.foot, mat: makeInstanceMaterial(0xffffff) },\n                        neckStump: { geo: zombieGeo.neckStump, mat: makeInstanceMaterial(0xffffff) },\n                        lArmStump: { geo: zombieGeo.armStump, mat: makeInstanceMaterial(0xffffff) },\n                        rArmStump: { geo: zombieGeo.armStump, mat: makeInstanceMaterial(0xffffff) },\n                        lLegStump: { geo: zombieGeo.legStump, mat: makeInstanceMaterial(0xffffff) },\n                        rLegStump: { geo: zombieGeo.legStump, mat: makeInstanceMaterial(0xffffff) },\n                        belly: { geo: zombieGeo.belly, mat: makeInstanceMaterial(0xffffff) }\n                    };\n                    // Symmetric pairs share one InstancedMesh to halve their draw calls (-7 total).\n                    // Left/right instances are interleaved so active slots remain contiguous.\n                    const mirrorPairs = [\n                        ['leftEye','rightEye'], ['leftArm','rightArm'], ['leftHand','rightHand'],\n                        ['leftLeg','rightLeg'], ['leftFoot','rightFoot'],\n                        ['lArmStump','rArmStump'], ['lLegStump','rLegStump']\n                    ];\n                    const mirrorRightKeys = new Set(mirrorPairs.map(p => p[1]));\n                    const slotStride = Object.create(null);\n                    const slotBias = Object.create(null);\n                    const mergedSet = new Set();\n                    const meshes = {};\n                    const initZeroM = new THREE.Matrix4().makeScale(0, 0, 0);\n                    for (const key of zombieInstanceParts) {\n                        if (mirrorRightKeys.has(key)) continue; // handled as right half of a pair\n                        const pair = mirrorPairs.find(p => p[0] === key);\n                        const capacity = pair ? 2 * capacityPerSide : capacityPerSide;\n                        const geometry = gpuAnimated ? specs[key].geo.clone() : specs[key].geo;\n                        ensureWhiteVertexColors(geometry);\n                        const material = specs[key].mat;\n                        if (gpuAnimated) configureZombieGpuMaterial(material, zombieGpuPartCode[key] || 0);\n                        const mesh = new THREE.InstancedMesh(geometry, material, capacity);\n                        mesh.name = `${meshNamePrefix}-${key}`;\n                        mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);\n                        mesh.frustumCulled = false;\n                        // Zero-init all slots so unactivated right-side instances are invisible.\n                        for (let s = 0; s < capacity; s++) mesh.setMatrixAt(s, initZeroM);\n                        mesh.instanceMatrix.needsUpdate = true;\n                        // Prime the instanceColor shader path.\n                        mesh.setColorAt(0, zombieInstanceScratchColor.setHex(0xffffff));\n                        if (mesh.instanceColor) {\n                            mesh.instanceColor.setUsage(THREE.DynamicDrawUsage);\n                            mesh.instanceColor.needsUpdate = true;\n                        }\n                        mesh.material.needsUpdate = true;\n                        mesh.count = 0;\n                        if (gpuAnimated) {\n                            const animValues = new Float32Array(capacity * 4);\n                            const impactValues = new Float32Array(capacity * 4);\n                            const pivotValues = new Float32Array(capacity * 3);\n                            const burnValues = new Float32Array(capacity);\n                            const animAttr = new THREE.InstancedBufferAttribute(animValues, 4);\n                            const impactAttr = new THREE.InstancedBufferAttribute(impactValues, 4);\n                            const pivotAttr = new THREE.InstancedBufferAttribute(pivotValues, 3);\n                            const burnAttr = new THREE.InstancedBufferAttribute(burnValues, 1);\n                            animAttr.setUsage(THREE.DynamicDrawUsage);\n                            impactAttr.setUsage(THREE.DynamicDrawUsage);\n                            pivotAttr.setUsage(THREE.DynamicDrawUsage);\n                            burnAttr.setUsage(THREE.DynamicDrawUsage);\n                            geometry.setAttribute('instanceZombieAnim', animAttr);\n                            geometry.setAttribute('instanceZombieImpact', impactAttr);\n                            geometry.setAttribute('instanceZombiePivot', pivotAttr);\n                            geometry.setAttribute('instanceZombieBurn', burnAttr);\n                            mesh.userData.zombieGpu = {\n                                animValues, impactValues, pivotValues, burnValues,\n                                animAttr, impactAttr, pivotAttr, burnAttr,\n                                dirty: false\n                            };\n                        }\n                        scene.add(mesh);\n                        meshes[key] = mesh;\n                        slotStride[key] = pair ? 2 : 1;\n                        slotBias[key] = 0;\n                        if (pair) {\n                            const rk = pair[1];\n                            meshes[rk] = mesh;\n                            // Interleave left/right slots so the active range stays compact:\n                            // left = slot * 2, right = slot * 2 + 1.\n                            slotStride[rk] = 2;\n                            slotBias[rk] = 1;\n                            mergedSet.add(mesh);\n                        }\n                    }\n                    return { meshes, slotStride, slotBias, mergedSet };\n                }\n\n                const {\n                    meshes: zombieInstanceMeshes,\n                    slotStride: zombiePartSlotStride,\n                    slotBias: zombiePartSlotBias,\n                    mergedSet: zombieMergedMeshSet\n                } = createZombieInstanceRenderer(ZOMBIE_POOL_SIZE, 'zombie', true);\n                const freeZombieInstanceSlots = [];\n                for (let i = ZOMBIE_POOL_SIZE - 1; i >= 0; i--) freeZombieInstanceSlots.push(i);\n                const zombieSlotOccupied = new Uint8Array(ZOMBIE_POOL_SIZE);\n                let zombieMaxActiveSlot = -1;\n                let zombieInstanceCountDirty = true;\n\n                function markZombieInstanceCountDirty() {\n                    zombieInstanceCountDirty = true;\n                }\n\n                function flushZombieInstanceDrawCount() {\n                    if (!zombieInstanceCountDirty) return;\n                    const leftCount = zombieMaxActiveSlot + 1;\n                    const seenMeshes = new Set();\n                    for (let i = 0; i < zombieInstanceParts.length; i++) {\n                        const mesh = zombieInstanceMeshes[zombieInstanceParts[i]];\n                        if (seenMeshes.has(mesh)) continue;\n                        seenMeshes.add(mesh);\n                        mesh.count = zombieMergedMeshSet.has(mesh) ? leftCount * 2 : leftCount;\n                    }\n                    zombieInstanceCountDirty = false;\n                }\n\n                function getZombieInstanceActualSlot(key, slot) {\n                    return slot * (zombiePartSlotStride[key] || 1) + (zombiePartSlotBias[key] || 0);\n                }\n\n                function markZombieInstanceMatrixDirty(key) {\n                    const idx = zombieInstancePartIndex[key];\n                    if (idx === undefined || zombieInstanceDirtyMatrixFlags[idx]) return;\n                    zombieInstanceDirtyMatrixFlags[idx] = 1;\n                    zombieInstanceDirtyMatrixParts.push(key);\n                }\n\n                function markZombieInstanceColorDirty(key) {\n                    const idx = zombieInstancePartIndex[key];\n                    if (idx === undefined || zombieInstanceDirtyColorFlags[idx]) return;\n                    zombieInstanceDirtyColorFlags[idx] = 1;\n                    zombieInstanceDirtyColorParts.push(key);\n                }\n\n                function setZombieInstanceColor(key, slot, hex) {\n                    const mesh = zombieInstanceMeshes[key];\n                    if (!mesh || !mesh.setColorAt) return;\n                    zombieInstanceScratchColor.setHex(hex);\n                    mesh.setColorAt(getZombieInstanceActualSlot(key, slot), zombieInstanceScratchColor);\n                    if (mesh.instanceColor) {\n                        mesh.instanceColor.setUsage(THREE.DynamicDrawUsage);\n                        markZombieInstanceColorDirty(key);\n                    }\n                }\n\n                function clearZombieInstanceSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0) return;\n                    for (let i = 0; i < zombieInstanceParts.length; i++) {\n                        const key = zombieInstanceParts[i];\n                        const mesh = zombieInstanceMeshes[key];\n                        mesh.setMatrixAt(getZombieInstanceActualSlot(key, slot), zombieZeroMatrix);\n                        zombieInstancePartVisible[key][slot] = 0;\n                        markZombieInstanceMatrixDirty(key);\n                    }\n                }\n\n                function markZombieInstanceMatricesDirty() {\n                    for (let i = 0; i < zombieInstanceParts.length; i++) markZombieInstanceMatrixDirty(zombieInstanceParts[i]);\n                }\n\n                function flushZombieInstanceUpdates() {\n                    flushZombieInstanceDrawCount();\n                    if (zombieInstanceDirtyMatrixParts.length) {\n                        for (let i = 0; i < zombieInstanceDirtyMatrixParts.length; i++) {\n                            const key = zombieInstanceDirtyMatrixParts[i];\n                            const mesh = zombieInstanceMeshes[key];\n                            if (mesh && mesh.count > 0) {\n                                mesh.instanceMatrix.updateRange.offset = 0;\n                                mesh.instanceMatrix.updateRange.count = mesh.count * 16;\n                                mesh.instanceMatrix.needsUpdate = true;\n                            }\n                            zombieInstanceDirtyMatrixFlags[zombieInstancePartIndex[key]] = 0;\n                        }\n                        zombieInstanceDirtyMatrixParts.length = 0;\n                    }\n                    if (zombieInstanceDirtyColorParts.length) {\n                        for (let i = 0; i < zombieInstanceDirtyColorParts.length; i++) {\n                            const key = zombieInstanceDirtyColorParts[i];\n                            const mesh = zombieInstanceMeshes[key];\n                            if (mesh && mesh.instanceColor && mesh.count > 0) {\n                                mesh.instanceColor.updateRange.offset = 0;\n                                mesh.instanceColor.updateRange.count = mesh.count * 3;\n                                mesh.instanceColor.needsUpdate = true;\n                            }\n                            zombieInstanceDirtyColorFlags[zombieInstancePartIndex[key]] = 0;\n                        }\n                        zombieInstanceDirtyColorParts.length = 0;\n                    }\n                    const seenGpuMeshes = new Set();\n                    for (let i = 0; i < zombieInstanceParts.length; i++) {\n                        const mesh = zombieInstanceMeshes[zombieInstanceParts[i]];\n                        if (!mesh || seenGpuMeshes.has(mesh)) continue;\n                        seenGpuMeshes.add(mesh);\n                        const gpu = mesh.userData.zombieGpu;\n                        if (!gpu || !gpu.dirty || mesh.count <= 0) continue;\n                        gpu.animAttr.updateRange.offset = 0;\n                        gpu.animAttr.updateRange.count = mesh.count * 4;\n                        gpu.impactAttr.updateRange.offset = 0;\n                        gpu.impactAttr.updateRange.count = mesh.count * 4;\n                        gpu.pivotAttr.updateRange.offset = 0;\n                        gpu.pivotAttr.updateRange.count = mesh.count * 3;\n                        gpu.burnAttr.updateRange.offset = 0;\n                        gpu.burnAttr.updateRange.count = mesh.count;\n                        gpu.animAttr.needsUpdate = true;\n                        gpu.impactAttr.needsUpdate = true;\n                        gpu.pivotAttr.needsUpdate = true;\n                        gpu.burnAttr.needsUpdate = true;\n                        gpu.dirty = false;\n                    }\n                }\n\n                function acquireZombieInstanceSlot() {\n                    if (!freeZombieInstanceSlots.length) return -1;\n                    const slot = freeZombieInstanceSlots.pop();\n                    zombieSlotOccupied[slot] = 1;\n                    if (slot > zombieMaxActiveSlot) {\n                        zombieMaxActiveSlot = slot;\n                        markZombieInstanceCountDirty();\n                        // The newly exposed tail may not have reached the GPU when a prior\n                        // high slot was retired with a smaller update range.\n                        markZombieInstanceMatricesDirty();\n                    }\n                    return slot;\n                }\n\n                function releaseZombieInstanceSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0) return;\n                    clearZombieInstanceSlot(slot);\n                    zombieSlotOccupied[slot] = 0;\n                    if (slot === zombieMaxActiveSlot) {\n                        while (zombieMaxActiveSlot >= 0 && !zombieSlotOccupied[zombieMaxActiveSlot]) zombieMaxActiveSlot--;\n                        markZombieInstanceCountDirty();\n                    }\n                    freeZombieInstanceSlots.push(slot);\n                }\n\n                const zombieRenderScratch = {\n                    pose: {},\n                    rootM: new THREE.Matrix4(),\n                    bodyM: new THREE.Matrix4(),\n                    headM: new THREE.Matrix4(),\n                    jawM: new THREE.Matrix4(),\n                    lArmM: new THREE.Matrix4(),\n                    rArmM: new THREE.Matrix4(),\n                    lLegM: new THREE.Matrix4(),\n                    rLegM: new THREE.Matrix4(),\n                    localM: new THREE.Matrix4(),\n                    outM: new THREE.Matrix4(),\n                    pos: new THREE.Vector3(),\n                    viewDir: new THREE.Vector3(),\n                    quat: new THREE.Quaternion(),\n                    euler: new THREE.Euler()\n                };\n                const zombieGpuFrameData = {\n                    animTimer: 0,\n                    state: 1,\n                    yaw: 0,\n                    bodyMask: 0,\n                    burn: 0,\n                    impactPitch: 0,\n                    impactRoll: 0,\n                    impactYaw: 0,\n                    impactPart: 0\n                };\n\n                function composeZombieMatrix(out, x, y, z, rx = 0, ry = 0, rz = 0) {\n                    const a = Math.cos(rx), b = Math.sin(rx);\n                    const c = Math.cos(ry), d = Math.sin(ry);\n                    const e = Math.cos(rz), f = Math.sin(rz);\n                    const ae = a * e, af = a * f, be = b * e, bf = b * f;\n                    const te = out.elements;\n                    te[0] = c * e; te[4] = -c * f; te[8] = d; te[12] = x;\n                    te[1] = af + be * d; te[5] = ae - bf * d; te[9] = -b * c; te[13] = y;\n                    te[2] = bf - ae * d; te[6] = be + af * d; te[10] = a * c; te[14] = z;\n                    te[3] = 0; te[7] = 0; te[11] = 0; te[15] = 1;\n                    return out;\n                }\n\n                function setZombieGpuPartData(key, actualSlot, pivotX, pivotY, pivotZ) {\n                    const mesh = zombieInstanceMeshes[key];\n                    const gpu = mesh && mesh.userData.zombieGpu;\n                    if (!gpu) return;\n                    const a4 = actualSlot * 4;\n                    const a3 = actualSlot * 3;\n                    gpu.animValues[a4] = zombieGpuFrameData.animTimer;\n                    gpu.animValues[a4 + 1] = zombieGpuFrameData.state;\n                    gpu.animValues[a4 + 2] = zombieGpuFrameData.yaw;\n                    gpu.animValues[a4 + 3] = zombieGpuFrameData.bodyMask;\n                    gpu.impactValues[a4] = zombieGpuFrameData.impactPitch;\n                    gpu.impactValues[a4 + 1] = zombieGpuFrameData.impactRoll;\n                    gpu.impactValues[a4 + 2] = zombieGpuFrameData.impactYaw;\n                    gpu.impactValues[a4 + 3] = zombieGpuFrameData.impactPart;\n                    gpu.pivotValues[a3] = pivotX;\n                    gpu.pivotValues[a3 + 1] = pivotY;\n                    gpu.pivotValues[a3 + 2] = pivotZ;\n                    gpu.burnValues[actualSlot] = zombieGpuFrameData.burn;\n                    gpu.dirty = true;\n                }\n\n                function setZombieChildInstance(\n                    key, slot, visible, parentMatrix,\n                    x, y, z, rx = 0, ry = 0, rz = 0,\n                    sx = 1, sy = 1, sz = 1,\n                    pivotX = zombieGpuFrameData.pivotX || 0,\n                    pivotY = zombieGpuFrameData.pivotY || 0,\n                    pivotZ = zombieGpuFrameData.pivotZ || 0\n                ) {\n                    const actualSlot = getZombieInstanceActualSlot(key, slot);\n                    const partVisible = zombieInstancePartVisible[key];\n                    setZombieGpuPartData(key, actualSlot, pivotX, pivotY, pivotZ);\n                    if (!visible) {\n                        if (partVisible && partVisible[slot] === 0) return;\n                        if (partVisible) partVisible[slot] = 0;\n                        zombieInstanceMeshes[key].setMatrixAt(actualSlot, zombieZeroMatrix);\n                        markZombieInstanceMatrixDirty(key);\n                        return;\n                    }\n                    if (partVisible) partVisible[slot] = 1;\n                    composeZombieMatrix(zombieRenderScratch.localM, x, y, z, rx, ry, rz);\n                    if (sx !== 1 || sy !== 1 || sz !== 1) {\n                        const te = zombieRenderScratch.localM.elements;\n                        te[0] *= sx; te[1] *= sx; te[2] *= sx;\n                        te[4] *= sy; te[5] *= sy; te[6] *= sy;\n                        te[8] *= sz; te[9] *= sz; te[10] *= sz;\n                    }\n                    zombieRenderScratch.outM.multiplyMatrices(parentMatrix, zombieRenderScratch.localM);\n                    zombieInstanceMeshes[key].setMatrixAt(actualSlot, zombieRenderScratch.outM);\n                    markZombieInstanceMatrixDirty(key);\n                }\n\n                function zombiePartBit(name) {\n                    switch (name) {\n                        case 'head': return ZOMBIE_PART_HEAD;\n                        case 'body': return ZOMBIE_PART_BODY;\n                        case 'belly': return ZOMBIE_PART_BELLY;\n                        case 'leftArm': return ZOMBIE_PART_LEFT_ARM;\n                        case 'rightArm': return ZOMBIE_PART_RIGHT_ARM;\n                        case 'leftLeg': return ZOMBIE_PART_LEFT_LEG;\n                        case 'rightLeg': return ZOMBIE_PART_RIGHT_LEG;\n                        default: return 0;\n                    }\n                }\n\n                function zombiePartNameFromBit(bit) {\n                    switch (bit) {\n                        case ZOMBIE_PART_HEAD: return 'head';\n                        case ZOMBIE_PART_BODY: return 'body';\n                        case ZOMBIE_PART_BELLY: return 'belly';\n                        case ZOMBIE_PART_LEFT_ARM: return 'leftArm';\n                        case ZOMBIE_PART_RIGHT_ARM: return 'rightArm';\n                        case ZOMBIE_PART_LEFT_LEG: return 'leftLeg';\n                        case ZOMBIE_PART_RIGHT_LEG: return 'rightLeg';\n                        default: return 'body';\n                    }\n                }\n\n                function zombiePartBloodOffsetY(bit) {\n                    switch (bit) {\n                        case ZOMBIE_PART_HEAD: return 1.95;\n                        case ZOMBIE_PART_LEFT_ARM:\n                        case ZOMBIE_PART_RIGHT_ARM: return 1.05;\n                        case ZOMBIE_PART_LEFT_LEG:\n                        case ZOMBIE_PART_RIGHT_LEG: return 0.43;\n                        default: return 1.0;\n                    }\n                }\n\n                function zombiePartAlive(bodyParts, name) {\n                    if (!bodyParts) return true;\n                    if (typeof bodyParts === 'number') {\n                        const bit = zombiePartBit(name);\n                        return bit === 0 || (bodyParts & bit) !== 0;\n                    }\n                    return bodyParts[name] !== false;\n                }\n\n                function zombieSlotPartAlive(slot, name) {\n                    const bit = zombiePartBit(name);\n                    return bit === 0 || (zombieRuntime.bodyMask[slot] & bit) !== 0;\n                }\n\n                function invalidateZombieHitPoseCache() {\n                    if (typeof bulletPoseCache !== 'undefined') bulletPoseCache.frameId = -1;\n                }\n\n                function clearZombieSlotPart(slot, name) {\n                    const bit = zombiePartBit(name);\n                    if (bit) {\n                        zombieRuntime.bodyMask[slot] &= ~bit;\n                        zombieRenderForce[slot] = 1;\n                        invalidateZombieHitPoseCache();\n                    }\n                }\n\n                function getZombieSlotPartHp(slot, name) {\n                    switch (name) {\n                        case 'head': return zombieRuntime.partHeadHp[slot];\n                        case 'leftArm': return zombieRuntime.partLeftArmHp[slot];\n                        case 'rightArm': return zombieRuntime.partRightArmHp[slot];\n                        case 'leftLeg': return zombieRuntime.partLeftLegHp[slot];\n                        case 'rightLeg': return zombieRuntime.partRightLegHp[slot];\n                        default: return Infinity;\n                    }\n                }\n\n                function damageZombieSlotPart(slot, name, damage) {\n                    switch (name) {\n                        case 'head':\n                            zombieRuntime.partHeadHp[slot] -= damage;\n                            zombieRenderForce[slot] = 1;\n                            invalidateZombieHitPoseCache();\n                            return zombieRuntime.partHeadHp[slot];\n                        case 'leftArm':\n                            zombieRuntime.partLeftArmHp[slot] -= damage;\n                            zombieRenderForce[slot] = 1;\n                            invalidateZombieHitPoseCache();\n                            return zombieRuntime.partLeftArmHp[slot];\n                        case 'rightArm':\n                            zombieRuntime.partRightArmHp[slot] -= damage;\n                            zombieRenderForce[slot] = 1;\n                            invalidateZombieHitPoseCache();\n                            return zombieRuntime.partRightArmHp[slot];\n                        case 'leftLeg':\n                            zombieRuntime.partLeftLegHp[slot] -= damage;\n                            zombieRenderForce[slot] = 1;\n                            invalidateZombieHitPoseCache();\n                            return zombieRuntime.partLeftLegHp[slot];\n                        case 'rightLeg':\n                            zombieRuntime.partRightLegHp[slot] -= damage;\n                            zombieRenderForce[slot] = 1;\n                            invalidateZombieHitPoseCache();\n                            return zombieRuntime.partRightLegHp[slot];\n                        default:\n                            return Infinity;\n                    }\n                }\n\n                function zombieSlotArmsLeft(slot) {\n                    const mask = zombieRuntime.bodyMask[slot];\n                    return ((mask & ZOMBIE_PART_LEFT_ARM) ? 1 : 0) + ((mask & ZOMBIE_PART_RIGHT_ARM) ? 1 : 0);\n                }\n\n                function zombieSlotLegsLeft(slot) {\n                    const mask = zombieRuntime.bodyMask[slot];\n                    return ((mask & ZOMBIE_PART_LEFT_LEG) ? 1 : 0) + ((mask & ZOMBIE_PART_RIGHT_LEG) ? 1 : 0);\n                }\n\n                function zombieTypeHasBelly(typeName) {\n                    return typeName === 'Bloater' || typeName === 'BEHEMOTH';\n                }\n\n                function clampValue(v, min, max) {\n                    return Math.max(min, Math.min(max, v));\n                }\n\n                function getWeaponImpactProfile(weaponIndex) {\n                    return weaponImpactProfiles[weaponIndex] || weaponImpactProfiles[0];\n                }\n\n                function getZombieImpactResistanceForValues(radius, scale, isBoss) {\n                    const radiusResistance = 0.88 + Math.max(0, radius - 0.32) * 0.72;\n                    const scaleResistance = 0.92 + Math.max(0, scale - 1) * 0.28;\n                    const bossResistance = isBoss ? 1.34 : 1;\n                    return clampValue(radiusResistance * scaleResistance * bossResistance, 0.72, 2.25);\n                }\n\n                function getZombieImpactResistance(slot) {\n                    const radius = zombieRuntime.radius[slot] || 0.44;\n                    const scale = getZombieSlotHitScale(slot);\n                    return getZombieImpactResistanceForValues(radius, scale, zombieRuntime.isBoss[slot] !== 0);\n                }\n\n                function getPartImpactMultiplier(part) {\n                    switch (part) {\n                        case 'head': return 1.24;\n                        case 'leftArm':\n                        case 'rightArm': return 1.08;\n                        case 'leftLeg':\n                        case 'rightLeg': return 0.9;\n                        default: return 1;\n                    }\n                }\n\n                function getPartImpactSide(part, dirX, dirZ) {\n                    if (part === 'leftArm' || part === 'leftLeg') return 1;\n                    if (part === 'rightArm' || part === 'rightLeg') return -1;\n                    return (dirX * 12.9898 + dirZ * 78.233) >= 0 ? 1 : -1;\n                }\n\n                function applyZombieImpactPose(slot, pose) {\n                    const timer = zombieRuntime.impactTimer[slot];\n                    const duration = zombieRuntime.impactDuration[slot];\n                    if (timer <= 0 || duration <= 0) return pose;\n                    const elapsed01 = clampValue(1 - timer / duration, 0, 1);\n                    const recover = clampValue(timer / duration, 0, 1);\n                    const pulse = Math.sin(elapsed01 * Math.PI) * Math.min(1, recover * 1.8);\n                    if (pulse <= 0.0001) return pose;\n\n                    const pitch = zombieRuntime.impactPitch[slot] * pulse;\n                    const roll = zombieRuntime.impactRoll[slot] * pulse;\n                    const yaw = zombieRuntime.impactYaw[slot] * pulse;\n                    const partBit = zombieRuntime.impactPart[slot];\n                    pose.bodyRx += pitch;\n                    pose.bodyRz += roll;\n                    pose.bodyRy += yaw * 0.55;\n                    pose.headRx += pitch * (partBit === ZOMBIE_PART_HEAD ? 1.05 : 0.38);\n                    pose.headRz += roll * 0.35;\n                    pose.headRy += yaw * 0.45;\n                    if (partBit === ZOMBIE_PART_LEFT_ARM) {\n                        pose.leftArmRx += pitch * 1.35;\n                        pose.leftArmRz += roll * 1.5;\n                    } else if (partBit === ZOMBIE_PART_RIGHT_ARM) {\n                        pose.rightArmRx += pitch * 1.35;\n                        pose.rightArmRz += roll * 1.5;\n                    } else if (partBit === ZOMBIE_PART_LEFT_LEG) {\n                        pose.leftLegRx += pitch * 0.85;\n                        pose.bodyRz += roll * 0.35;\n                    } else if (partBit === ZOMBIE_PART_RIGHT_LEG) {\n                        pose.rightLegRx += pitch * 0.85;\n                        pose.bodyRz += roll * 0.35;\n                    }\n                    return pose;\n                }\n\n                function applyZombieImpactReaction(slot, weaponIndex, hitPart, hitDir, damage, severed, cutInfo) {\n                    if (slot < 0 || zombieRuntime.deadPending[slot]) return;\n                    const profile = getWeaponImpactProfile(weaponIndex);\n                    const weapon = weaponStates[weaponIndex] || weaponStates[0];\n                    let dirX = hitDir && hitDir.x || 0;\n                    let dirZ = hitDir && hitDir.z || 0;\n                    const len = Math.sqrt(dirX * dirX + dirZ * dirZ);\n                    if (len > 0.0001) {\n                        dirX /= len;\n                        dirZ /= len;\n                    } else {\n                        dirX = Math.sin(zombieRuntime.yaw[slot] || 0);\n                        dirZ = Math.cos(zombieRuntime.yaw[slot] || 0);\n                    }\n\n                    const resistance = getZombieImpactResistance(slot);\n                    const partMult = getPartImpactMultiplier(hitPart);\n                    const damageScale = clampValue(damage / Math.max(12, weapon.damage || damage || 12), 0.65, 1.55);\n                    const severMult = severed ? 1.35 : 1;\n                    const cutPower = cutInfo && cutInfo.power || 1;\n                    const strength = partMult * damageScale * severMult * cutPower * 1.18 / resistance;\n                    const side = getPartImpactSide(hitPart, dirX, dirZ);\n                    const stack = zombieRuntime.impactTimer[slot] > 0 ? (profile.stack || 0.42) : 0;\n\n                    zombieRuntime.impactDuration[slot] = Math.max(zombieRuntime.impactDuration[slot] * stack, profile.duration * 1.08);\n                    zombieRuntime.impactTimer[slot] = Math.max(zombieRuntime.impactTimer[slot] * stack, profile.duration * 1.08);\n                    zombieRuntime.impactPitch[slot] = clampValue(zombieRuntime.impactPitch[slot] * stack - profile.aliveLean * strength, -0.58, 0.16);\n                    zombieRuntime.impactRoll[slot] = clampValue(zombieRuntime.impactRoll[slot] * stack + side * profile.aliveRoll * strength, -0.46, 0.46);\n                    zombieRuntime.impactYaw[slot] = clampValue(zombieRuntime.impactYaw[slot] * stack + side * profile.aliveYaw * strength, -0.28, 0.28);\n                    zombieRuntime.impactPart[slot] = zombiePartBit(hitPart) || ZOMBIE_PART_BODY;\n                    zombieRuntime.impactSlowTimer[slot] = Math.max(zombieRuntime.impactSlowTimer[slot], profile.slowDuration);\n                    zombieRuntime.impactSlow[slot] = Math.max(zombieRuntime.impactSlow[slot] * stack, clampValue(profile.aliveSlow * strength, 0, 0.34));\n\n                    zombieRuntime.impactMoveX[slot] += dirX * profile.aliveImpulse * strength * 1.12;\n                    zombieRuntime.impactMoveZ[slot] += dirZ * profile.aliveImpulse * strength * 1.12;\n                    const moveLen = Math.sqrt(zombieRuntime.impactMoveX[slot] * zombieRuntime.impactMoveX[slot] + zombieRuntime.impactMoveZ[slot] * zombieRuntime.impactMoveZ[slot]);\n                    const maxMoveSpeed = zombieRuntime.isBoss[slot] ? 3.2 : 4.8;\n                    if (moveLen > maxMoveSpeed) {\n                        const scale = maxMoveSpeed / moveLen;\n                        zombieRuntime.impactMoveX[slot] *= scale;\n                        zombieRuntime.impactMoveZ[slot] *= scale;\n                    }\n                    zombieRenderForce[slot] = 1;\n                    invalidateZombieHitPoseCache();\n                }\n\n                const DEATH_SOURCE_NONE = 0;\n                const DEATH_SOURCE_BULLET = 1;\n                const DEATH_SOURCE_KATANA = 2;\n                const FLAME_DEATH_WEAPON_INDEX = 7;\n                const zombieDeathRuntime = {\n                    activeSlots: [],\n                    freeSlots: [],\n                    activeIndex: new Int16Array(ZOMBIE_POOL_SIZE),\n                    active: new Uint8Array(ZOMBIE_POOL_SIZE),\n                    source: new Uint8Array(ZOMBIE_POOL_SIZE),\n                    hitPart: new Uint8Array(ZOMBIE_POOL_SIZE),\n                    weaponIndex: new Int16Array(ZOMBIE_POOL_SIZE),\n                    dirX: new Float32Array(ZOMBIE_POOL_SIZE),\n                    dirY: new Float32Array(ZOMBIE_POOL_SIZE),\n                    dirZ: new Float32Array(ZOMBIE_POOL_SIZE),\n                    hitX: new Float32Array(ZOMBIE_POOL_SIZE),\n                    hitY: new Float32Array(ZOMBIE_POOL_SIZE),\n                    hitZ: new Float32Array(ZOMBIE_POOL_SIZE)\n                };\n                zombieDeathRuntime.activeIndex.fill(-1);\n                for (let i = ZOMBIE_POOL_SIZE - 1; i >= 0; i--) zombieDeathRuntime.freeSlots.push(i);\n                const zombieDeathEffectQueue = [];\n                let zombieDeathEffectReadIndex = 0;\n                // Pool of death-effect literals: avoid per-kill GC when multi-kills (shotgun, gatling)\n                // burst many zombieKilled events in the same frame.\n                const zombieDeathEffectPool = [];\n                function acquireZombieDeathEffect() {\n                    let effect = zombieDeathEffectPool.pop();\n                    if (!effect) {\n                        effect = {\n                            corpse: null,\n                            bodyParts: 0,\n                            animTimer: 0,\n                            state: 0,\n                            zombiePos: { x: 0, y: 0, z: 0 },\n                            playerPos: { x: 0, y: 0, z: 0 },\n                            hasPlayerPos: false,\n                            typeName: '',\n                            zombieScale: 1,\n                            killedBoss: false,\n                            deathSource: 0,\n                            deathPartBit: 0,\n                            deathPartName: '',\n                            impactProfile: null,\n                            deathDir: { x: 0, y: 0, z: 0 },\n                            bulletHitPos: { x: 0, y: 0, z: 0 },\n                            radius: 0,\n                            processed: false,\n                            isBurnt: false\n                        };\n                    } else {\n                        effect.processed = false;\n                        effect.hasPlayerPos = false;\n                        effect.isBurnt = false;\n                    }\n                    return effect;\n                }\n                function releaseZombieDeathEffect(effect) {\n                    if (!effect) return;\n                    effect.corpse = null;\n                    effect.impactProfile = null;\n                    effect.isBurnt = false;\n                    if (zombieDeathEffectPool.length < 32) zombieDeathEffectPool.push(effect);\n                }\n\n                function acquireZombieDeathSlot() {\n                    const slot = zombieDeathRuntime.freeSlots.pop();\n                    if (slot === undefined) return -1;\n                    zombieDeathRuntime.active[slot] = 1;\n                    zombieDeathRuntime.activeIndex[slot] = zombieDeathRuntime.activeSlots.length;\n                    zombieDeathRuntime.activeSlots.push(slot);\n                    zombieDeathRuntime.source[slot] = DEATH_SOURCE_NONE;\n                    zombieDeathRuntime.hitPart[slot] = ZOMBIE_PART_BODY;\n                    zombieDeathRuntime.weaponIndex[slot] = 0;\n                    return slot;\n                }\n\n                function releaseZombieDeathSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0 || !zombieDeathRuntime.active[slot]) return;\n                    zombieDeathRuntime.active[slot] = 0;\n                    const idx = zombieDeathRuntime.activeIndex[slot];\n                    if (idx >= 0) {\n                        const last = zombieDeathRuntime.activeSlots.pop();\n                        if (idx < zombieDeathRuntime.activeSlots.length) {\n                            zombieDeathRuntime.activeSlots[idx] = last;\n                            zombieDeathRuntime.activeIndex[last] = idx;\n                        }\n                        zombieDeathRuntime.activeIndex[slot] = -1;\n                    }\n                    zombieDeathRuntime.freeSlots.push(slot);\n                }\n\n                function resetZombieDeathRuntime() {\n                    for (let i = zombieDeathRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        releaseZombieDeathSlot(zombieDeathRuntime.activeSlots[i]);\n                    }\n                }\n\n                function queueZombieDeathEffect(effect) {\n                    if (!effect) return;\n                    zombieDeathEffectQueue.push(effect);\n                }\n\n                function clearZombieDeathEffectQueue() {\n                    for (let i = zombieDeathEffectReadIndex; i < zombieDeathEffectQueue.length; i++) {\n                        const effect = zombieDeathEffectQueue[i];\n                        if (!effect) continue;\n                        if (effect.corpse) zombieMeshPool.release(effect.corpse);\n                        releaseZombieDeathEffect(effect);\n                    }\n                    zombieDeathEffectReadIndex = 0;\n                    zombieDeathEffectQueue.length = 0;\n                }\n\n                function flushZombieDeathEffects(budget = 1) {\n                    if (zombieDeathEffectReadIndex >= zombieDeathEffectQueue.length) return;\n                    const limit = Math.max(1, budget | 0);\n                    let flushedShakes = 0;\n                    for (let i = 0; i < limit && zombieDeathEffectReadIndex < zombieDeathEffectQueue.length; i++) {\n                        const effect = zombieDeathEffectQueue[zombieDeathEffectReadIndex++];\n                        if (!effect || effect.processed) continue;\n                        effect.processed = true;\n                        const killedBoss = !!effect.killedBoss;\n                        const corpse = effect.corpse || zombieMeshPool.acquire();\n                        if (!corpse) continue;\n                        maybeTopUpZombieProxyPool();\n                        configureZombieProxyMesh(corpse, effect.typeName, killedBoss);\n                        if (effect.isBurnt) {\n                            const mats = corpse.userData.materials;\n                            if (mats) {\n                                if (mats.skin && mats.skin.color) mats.skin.color.setHex(0x1a1a1c);\n                                if (mats.skin && mats.skin.emissive) mats.skin.emissive.setHex(0x3a0800); // faint ember glow\n                                \n                                if (mats.darkSkin && mats.darkSkin.color) mats.darkSkin.color.setHex(0x121213);\n                                if (mats.darkSkin && mats.darkSkin.emissive) mats.darkSkin.emissive.setHex(0x2a0500);\n                                \n                                if (mats.cloth && mats.cloth.color) mats.cloth.color.setHex(0x161618);\n                                if (mats.cloth && mats.cloth.emissive) mats.cloth.emissive.setHex(0x1e0300);\n                                \n                                if (mats.belly) {\n                                    if (mats.belly.color) mats.belly.color.setHex(0x161618);\n                                    if (mats.belly.emissive) mats.belly.emissive.setHex(0x2a0400);\n                                }\n                                if (mats.eye && mats.eye.color) mats.eye.color.setHex(0x000000); // dead eyes\n                                if (mats.eye && mats.eye.emissive) mats.eye.emissive.setHex(0x000000);\n                            }\n                        } else {\n                            // Reset emissive in case this mesh was previously used as a burnt corpse\n                            const mats = corpse.userData.materials;\n                            if (mats) {\n                                if (mats.skin && mats.skin.emissive) mats.skin.emissive.setHex(0x000000);\n                                if (mats.darkSkin && mats.darkSkin.emissive) mats.darkSkin.emissive.setHex(0x000000);\n                                if (mats.cloth && mats.cloth.emissive) mats.cloth.emissive.setHex(0x000000);\n                                if (mats.belly && mats.belly.emissive) mats.belly.emissive.setHex(0x000000);\n                                if (mats.eye && mats.eye.emissive) mats.eye.emissive.setHex(0x000000);\n                            }\n                        }\n                        const corpseParts = prepareZombieCorpse(corpse, effect.bodyParts, effect.animTimer, effect.state, effect.zombiePos, effect.typeName)\n                            || effect.corpseParts\n                            || corpse.userData.parts\n                            || cacheZombieParts(corpse);\n                        const deathPartBit = effect.deathPartBit;\n                        const deathPartName = effect.deathPartName;\n                        const deathSource = effect.deathSource;\n                        const bulletKill = deathSource === DEATH_SOURCE_BULLET;\n                        const katanaKill = deathSource === DEATH_SOURCE_KATANA;\n                        const zombieScale = effect.zombieScale || 1;\n                        const playerPos = effect.hasPlayerPos ? effect.playerPos : null;\n                        const deathDir = effect.deathDir;\n                        const bulletHitPos = effect.bulletHitPos;\n                        const impactProfile = effect.impactProfile;\n                        const isLargeCorpse = effect.isLargeCorpse || zombieScale > 1.35 || effect.typeName === 'BEHEMOTH' || effect.typeName === 'BRUTE';\n                        const ndx = deathDir && Number.isFinite(deathDir.x) ? deathDir.x : 0;\n                        const ndz = deathDir && Number.isFinite(deathDir.z) ? deathDir.z : 0;\n                        const corpsePos = effect.corpsePos || corpse.position;\n\n                        if (bulletKill && deathPartBit === ZOMBIE_PART_HEAD) {\n                            destroyBodyPart(corpse, 'head');\n                            spawnBloodBurst(bulletHitPos, 18);\n                        }\n                        if (bulletKill && deathPartBit !== ZOMBIE_PART_BODY && deathPartBit !== ZOMBIE_PART_HEAD) {\n                            destroyBodyPart(corpse, deathPartName);\n                            spawnSeveredPart(corpse, deathPartName, effect.zombiePos, deathDir);\n                            spawnBloodBurst(bulletHitPos, 14);\n                        }\n                        if (katanaKill && deathPartBit !== ZOMBIE_PART_BODY) {\n                            destroyBodyPart(corpse, deathPartName);\n                            spawnSeveredPart(corpse, deathPartName, effect.zombiePos, deathDir);\n                            const bloodPos = effectScratch.v10.set(corpse.position.x, corpse.position.y + zombiePartBloodOffsetY(deathPartBit), corpse.position.z);\n                            spawnBloodBurst(bloodPos, deathPartBit === ZOMBIE_PART_HEAD ? 22 : 16);\n                        }\n                        corpse.visible = true;\n\n                        let fallNdx = ndx;\n                        let fallNdz = ndz;\n                        const deathDirLen = Math.sqrt(fallNdx * fallNdx + fallNdz * fallNdz);\n                        if (deathDirLen <= 0.0001 && playerPos) {\n                            const knockDirX = corpsePos.x - playerPos.x;\n                            const knockDirZ = corpsePos.z - playerPos.z;\n                            const knockDist = Math.sqrt(knockDirX * knockDirX + knockDirZ * knockDirZ) || 1;\n                            fallNdx = knockDirX / knockDist;\n                            fallNdz = knockDirZ / knockDist;\n                        } else if (deathDirLen > 0.0001) {\n                            fallNdx /= deathDirLen;\n                            fallNdz /= deathDirLen;\n                        }\n\n                        const facingX = Math.sin(corpse.rotation.y);\n                        const facingZ = Math.cos(corpse.rotation.y);\n                        const dot = fallNdx * facingX + fallNdz * facingZ;\n                        const fallDirSign = dot < 0 ? -1 : 1;\n                        const isHeadshotKill = bulletKill && deathPartBit === ZOMBIE_PART_HEAD;\n                        corpse.rotation.x = fallDirSign * (isHeadshotKill ? 0.58 : 0.32);\n\n                        if (corpseParts.leftArm) corpseParts.leftArm.rotation.set(0, 0, 0.3);\n                        if (corpseParts.rightArm) corpseParts.rightArm.rotation.set(0, 0, -0.3);\n                        for (let partIdx = 0; partIdx < corpseRelaxPartNames.length; partIdx++) {\n                            const name = corpseRelaxPartNames[partIdx];\n                            if (corpseParts[name]) corpseParts[name].rotation.set(0, 0, 0);\n                        }\n\n                        const resistance = getZombieImpactResistanceForValues(\n                            effect.radius || Math.max(0.44, zombieScale * 0.44),\n                            zombieScale,\n                            killedBoss\n                        );\n                        const partMult = getPartImpactMultiplier(deathPartName);\n                        const massScale = partMult / resistance;\n                        let knockbackSpeed = impactProfile.deathImpulse * massScale;\n                        let knockbackUp = impactProfile.deathLift * (0.72 + partMult * 0.28) / Math.sqrt(resistance);\n                        let fallSpeed = impactProfile.deathFallSpeed * (isHeadshotKill ? 1.18 : 1);\n                        let fallAccel = impactProfile.deathFallAccel * (isHeadshotKill ? 1.16 : 1);\n                        const spinSpeed = 0;\n                        const spinX = 0;\n                        const extraBlood = impactProfile.deathBlood;\n                        if (isLargeCorpse) {\n                            knockbackUp *= 0.68;\n                            knockbackSpeed *= 0.78;\n                            fallSpeed *= 0.78;\n                            fallAccel *= 0.82;\n                        }\n                        // Only the first shake per flush batch is at full strength; later kills in the\n                        // same flush damp down (otherwise shotgun multi-kills compound into a jolt).\n                        if (flushedShakes === 0) {\n                            triggerCameraShake(impactProfile.cameraShakeIntensity, impactProfile.cameraShakeDuration);\n                        } else if (flushedShakes === 1) {\n                            triggerCameraShake(impactProfile.cameraShakeIntensity * 0.5, impactProfile.cameraShakeDuration * 0.6);\n                        }\n                        flushedShakes++;\n                        const initialFallAngle = isHeadshotKill\n                            ? (isLargeCorpse ? 0.62 : 0.54)\n                            : (isLargeCorpse ? 0.42 : 0.34);\n                        if (extraBlood > 0) {\n                            const corpseBloodPos = effectScratch.v10.set(corpse.position.x, 1.2, corpse.position.z);\n                            spawnBloodBurst(corpseBloodPos, extraBlood);\n                            // Blood floor puddles removed per design decision.\n                        }\n                        registerCorpse(corpse, {\n                            vx: fallNdx * knockbackSpeed,\n                            vz: fallNdz * knockbackSpeed,\n                            vy: knockbackUp,\n                            ndx: fallNdx,\n                            ndz: fallNdz,\n                            fallAngle: initialFallAngle,\n                            fallDirSign,\n                            fallSpeed,\n                            fallAccel,\n                            forceFlatAge: isLargeCorpse ? CORPSE_BOSS_FORCE_FLAT_AGE : CORPSE_FORCE_FLAT_AGE,\n                            spinSpeed,\n                            spinX,\n                            isBurnt: effect.isBurnt\n                        });\n                    }\n                    if (zombieDeathEffectReadIndex > 48 && zombieDeathEffectReadIndex * 2 >= zombieDeathEffectQueue.length) {\n                        for (let i = 0; i < zombieDeathEffectReadIndex; i++) releaseZombieDeathEffect(zombieDeathEffectQueue[i]);\n                        zombieDeathEffectQueue.splice(0, zombieDeathEffectReadIndex);\n                        zombieDeathEffectReadIndex = 0;\n                    } else if (zombieDeathEffectReadIndex >= zombieDeathEffectQueue.length) {\n                        for (let i = 0; i < zombieDeathEffectQueue.length; i++) releaseZombieDeathEffect(zombieDeathEffectQueue[i]);\n                        zombieDeathEffectReadIndex = 0;\n                        zombieDeathEffectQueue.length = 0;\n                    }\n                }\n\n                function markZombieDeathPending(w, entityId) {\n                    const slot = getZombieRuntimeSlot(entityId);\n                    if (slot >= 0) {\n                        if (zombieRuntime.deadPending[slot]) return false;\n                        zombieRuntime.currentHp[slot] = 0;\n                        zombieRuntime.deadPending[slot] = 1;\n                    } else {\n                        const hp = w.getComponent(entityId, 'Health');\n                        if (!hp || hp.deadPending) return false;\n                        hp.current = 0;\n                        hp.deadPending = true;\n                    }\n                    return true;\n                }\n\n                function emitZombieDeath(w, entityId, deathSlot) {\n                    w.emit({ type: 'zombieKilled', entityId, deathSlot });\n                }\n\n                function queueZombieDeath(w, entityId) {\n                    if (!markZombieDeathPending(w, entityId)) return false;\n                    emitZombieDeath(w, entityId, -1);\n                    return true;\n                }\n\n                function queueBulletZombieDeath(w, entityId, dir, hitPart, hitPos, weaponIndex) {\n                    if (!markZombieDeathPending(w, entityId)) return false;\n                    const deathSlot = acquireZombieDeathSlot();\n                    if (deathSlot >= 0) {\n                        zombieDeathRuntime.source[deathSlot] = DEATH_SOURCE_BULLET;\n                        zombieDeathRuntime.hitPart[deathSlot] = zombiePartBit(hitPart) || ZOMBIE_PART_BODY;\n                        zombieDeathRuntime.weaponIndex[deathSlot] = weaponIndex;\n                        zombieDeathRuntime.dirX[deathSlot] = dir.x;\n                        zombieDeathRuntime.dirY[deathSlot] = dir.y;\n                        zombieDeathRuntime.dirZ[deathSlot] = dir.z;\n                        zombieDeathRuntime.hitX[deathSlot] = hitPos.x;\n                        zombieDeathRuntime.hitY[deathSlot] = hitPos.y;\n                        zombieDeathRuntime.hitZ[deathSlot] = hitPos.z;\n                    }\n                    emitZombieDeath(w, entityId, deathSlot);\n                    return true;\n                }\n\n                function queueKatanaZombieDeath(w, entityId, dir, hitPart) {\n                    if (!markZombieDeathPending(w, entityId)) return false;\n                    const deathSlot = acquireZombieDeathSlot();\n                    if (deathSlot >= 0) {\n                        zombieDeathRuntime.source[deathSlot] = DEATH_SOURCE_KATANA;\n                        zombieDeathRuntime.hitPart[deathSlot] = zombiePartBit(hitPart) || ZOMBIE_PART_BODY;\n                        zombieDeathRuntime.weaponIndex[deathSlot] = 3;\n                        zombieDeathRuntime.dirX[deathSlot] = dir.x;\n                        zombieDeathRuntime.dirY[deathSlot] = dir.y;\n                        zombieDeathRuntime.dirZ[deathSlot] = dir.z;\n                    }\n                    emitZombieDeath(w, entityId, deathSlot);\n                    return true;\n                }\n\n                function queueFlameZombieDeath(w, entityId, hitX, hitY, hitZ) {\n                    if (!markZombieDeathPending(w, entityId)) return false;\n                    const deathSlot = acquireZombieDeathSlot();\n                    if (deathSlot >= 0) {\n                        zombieDeathRuntime.source[deathSlot] = DEATH_SOURCE_NONE;\n                        zombieDeathRuntime.hitPart[deathSlot] = ZOMBIE_PART_BODY;\n                        zombieDeathRuntime.weaponIndex[deathSlot] = FLAME_DEATH_WEAPON_INDEX;\n                        zombieDeathRuntime.dirX[deathSlot] = 0;\n                        zombieDeathRuntime.dirY[deathSlot] = 0;\n                        zombieDeathRuntime.dirZ[deathSlot] = 0;\n                        zombieDeathRuntime.hitX[deathSlot] = hitX || 0;\n                        zombieDeathRuntime.hitY[deathSlot] = hitY || 1;\n                        zombieDeathRuntime.hitZ[deathSlot] = hitZ || 0;\n                    }\n                    emitZombieDeath(w, entityId, deathSlot);\n                    return true;\n                }\n\n\n                function computeZombiePose(animTimer, state, bodyParts, pose) {\n                    pose.rootY = 0;\n                    pose.bodyRx = 0; pose.bodyRy = 0; pose.bodyRz = 0;\n                    pose.headRx = 0; pose.headRy = 0; pose.headRz = 0;\n                    pose.jawRx = 0;\n                    pose.leftArmRx = 0; pose.leftArmRz = 0;\n                    pose.rightArmRx = 0; pose.rightArmRz = 0;\n                    pose.leftLegRx = 0; pose.rightLegRx = 0;\n\n                    const t = animTimer;\n                    const noLegs = bodyParts && !zombiePartAlive(bodyParts, 'leftLeg') && !zombiePartAlive(bodyParts, 'rightLeg');\n                    const oneLeg = bodyParts && (!zombiePartAlive(bodyParts, 'leftLeg') || !zombiePartAlive(bodyParts, 'rightLeg')) && !noLegs;\n\n                    if (noLegs) {\n                        pose.rootY = -0.7;\n                        pose.bodyRx = 0.8;\n                        pose.bodyRz = Math.sin(t * 0.5) * 0.05;\n                        pose.headRx = -0.4;\n                        pose.headRz = Math.sin(t * 0.8) * 0.06;\n                        pose.leftArmRx = -1.2 + Math.sin(t) * 0.5;\n                        pose.leftArmRz = 0.2;\n                        pose.rightArmRx = -1.2 + Math.sin(t + Math.PI) * 0.5;\n                        pose.rightArmRz = -0.2;\n                        pose.jawRx = 0.05 + Math.abs(Math.sin(t * 2)) * 0.08;\n                        return pose;\n                    }\n\n                    if (oneLeg) {\n                        pose.rootY = Math.sin(t * 1.5) * 0.06 - 0.15;\n                        pose.bodyRz = Math.sin(t * 1.5) * 0.12;\n                        pose.bodyRx = 0.1;\n                    }\n\n                    if (state === 1) {\n                        const slowT = t * 0.7;\n                        if (!oneLeg) {\n                            pose.rootY = Math.abs(Math.sin(slowT * 1.8)) * 0.06 + Math.sin(slowT * 0.9) * 0.02;\n                            pose.bodyRx = 0.25 + Math.sin(slowT * 0.8) * 0.08;\n                            pose.bodyRz = Math.sin(slowT * 0.6) * 0.12;\n                            pose.bodyRy = Math.sin(slowT * 0.4) * 0.06;\n                        }\n                        pose.headRx = 0.2 + Math.sin(slowT * 1.2) * 0.1;\n                        pose.headRz = 0.15 + Math.sin(slowT * 0.5) * 0.12;\n                        pose.headRy = Math.sin(slowT * 0.3) * 0.08;\n                        pose.jawRx = 0.08 + Math.abs(Math.sin(slowT * 0.7)) * 0.06;\n                        pose.leftArmRx = -1.1 + Math.sin(slowT * 0.6) * 0.2;\n                        pose.leftArmRz = 0.15 + Math.sin(slowT * 0.4) * 0.05;\n                        pose.rightArmRx = -0.8 + Math.sin(slowT * 0.5 + 1.2) * 0.25;\n                        pose.rightArmRz = -0.2 + Math.sin(slowT * 0.35) * 0.06;\n                        pose.leftLegRx = Math.sin(slowT) * 0.35;\n                        pose.rightLegRx = Math.sin(slowT + Math.PI) * 0.25 + 0.1;\n                    } else if (state === 2) {\n                        const atkCycle = Math.sin(t * 5);\n                        pose.rootY = 0;\n                        pose.bodyRx = 0.15 + Math.abs(atkCycle) * 0.1;\n                        pose.bodyRz = Math.sin(t * 3) * 0.05;\n                        pose.headRx = 0.2 + atkCycle * 0.12;\n                        pose.headRz = Math.sin(t * 4) * 0.08;\n                        pose.jawRx = 0.15 + Math.abs(atkCycle) * 0.2;\n                        pose.leftArmRx = -1.3 + atkCycle * 0.4;\n                        pose.leftArmRz = 0.2 + Math.sin(t * 6) * 0.1;\n                        pose.rightArmRx = -1.3 - atkCycle * 0.4;\n                        pose.rightArmRz = -0.2 + Math.sin(t * 6 + Math.PI) * 0.1;\n                        pose.leftLegRx = Math.sin(t * 2) * 0.1;\n                        pose.rightLegRx = Math.sin(t * 2 + Math.PI) * 0.1;\n                    }\n                    return pose;\n                }\n\n                function applyZombiePoseToProxy(mesh, animTimer, state, bodyParts) {\n                    if (!mesh) return;\n                    const wasRenderable = mesh.userData.proxyRenderable !== false;\n                    setProxyMeshRenderable(mesh, true);\n                    animateZombie(mesh, animTimer || 0, state || 1, bodyParts);\n                    setProxyMeshRenderable(mesh, wasRenderable);\n                }\n\n                function applyTypeProportionsToCorpse(corpse, typeName) {\n                    const props = zombieTypeProportions[typeName] || defaultProportions;\n                    const parts = corpse.userData.parts || cacheZombieParts(corpse);\n                    // Scale each part group/mesh to match instanced rendering proportions\n                    if (parts.body) {\n                        const p = props.body;\n                        // Body is inside a group at y=1.15, scale the mesh child\n                        parts.body.traverse(child => {\n                            if (child.isMesh && child.geometry === zombieGeo.torso) child.scale.set(p[0], p[1], p[2]);\n                            if (child.isMesh && child.geometry === zombieGeo.belt) {\n                                child.position.z = 0.18 * p[2];\n                                child.scale.set(p[0], 1, p[2]);\n                            }\n                        });\n                        if (parts.belly) {\n                            const hasBelly = zombieTypeHasBelly(typeName);\n                            setProxyPartLogicalVisible(corpse, parts.belly, hasBelly);\n                            if (hasBelly) {\n                                const bellyScale = typeName === 'BEHEMOTH' ? 1.4 : 1.15;\n                                parts.belly.scale.set(bellyScale * 1.2, bellyScale * 0.95, bellyScale);\n                            }\n                        }\n                        if (parts.body.isMesh) parts.body.scale.set(p[0], p[1], p[2]);\n                    }\n                    if (parts.head) {\n                        const p = props.head;\n                        parts.head.traverse(child => {\n                            if (child.isMesh && child.geometry === zombieGeo.skull) child.scale.set(p[0], p[1], p[2]);\n                        });\n                        if (parts.leftEye) {\n                            parts.leftEye.position.set(-0.08 * p[0], 0.03 * p[1], 0.18 * p[2]);\n                            parts.leftEye.scale.set(p[0], p[1], p[2]);\n                        }\n                        if (parts.rightEye) {\n                            parts.rightEye.position.set(0.08 * p[0], 0.03 * p[1], 0.18 * p[2]);\n                            parts.rightEye.scale.set(p[0], p[1], p[2]);\n                        }\n                        if (parts.headWound) {\n                            parts.headWound.position.set(0.12 * p[0], 0.05 * p[1], 0.12 * p[2]);\n                            parts.headWound.scale.set(p[0], p[1], p[2]);\n                        }\n                        if (parts.head.isMesh) parts.head.scale.set(p[0], p[1], p[2]);\n                    }\n                    // Arms\n                    const aS = props.arm;\n                    const armOffsetX = 0.42 * props.body[0];\n                    if (parts.leftArm) {\n                        parts.leftArm.position.x = -armOffsetX;\n                        parts.leftArm.traverse(child => {\n                            if (child.isMesh && child.geometry === zombieGeo.arm) child.scale.set(aS[0], aS[1], aS[2]);\n                        });\n                    }\n                    if (parts.rightArm) {\n                        parts.rightArm.position.x = armOffsetX;\n                        parts.rightArm.traverse(child => {\n                            if (child.isMesh && child.geometry === zombieGeo.arm) child.scale.set(aS[0], aS[1], aS[2]);\n                        });\n                    }\n                    // Hands\n                    const hdS = props.hand;\n                    if (parts.leftHand) parts.leftHand.scale.set(hdS[0], hdS[1], hdS[2]);\n                    if (parts.rightHand) parts.rightHand.scale.set(hdS[0], hdS[1], hdS[2]);\n                    // Legs\n                    const lS = props.leg;\n                    const legOffsetX = 0.14 * props.body[0];\n                    if (parts.leftLeg) {\n                        parts.leftLeg.position.x = -legOffsetX;\n                        parts.leftLeg.traverse(child => {\n                            if (child.isMesh && child.geometry === zombieGeo.leg) child.scale.set(lS[0], lS[1], lS[2]);\n                        });\n                    }\n                    if (parts.rightLeg) {\n                        parts.rightLeg.position.x = legOffsetX;\n                        parts.rightLeg.traverse(child => {\n                            if (child.isMesh && child.geometry === zombieGeo.leg) child.scale.set(lS[0], lS[1], lS[2]);\n                        });\n                    }\n                    // Feet\n                    const fS = props.foot;\n                    if (parts.leftFoot) parts.leftFoot.scale.set(fS[0], fS[1], fS[2]);\n                    if (parts.rightFoot) parts.rightFoot.scale.set(fS[0], fS[1], fS[2]);\n                    // Jaw\n                    const jS = props.jaw;\n                    if (parts.jaw) {\n                        const hS = props.head;\n                        parts.jaw.position.set(0, -0.18 * hS[1], 0.04 * hS[2]);\n                        parts.jaw.scale.set(jS[0], jS[1], jS[2]);\n                    }\n                    if (parts.teethUpper) {\n                        const hS = props.head;\n                        parts.teethUpper.position.set(0, -0.12 * hS[1], 0.16 * hS[2]);\n                        parts.teethUpper.scale.set(jS[0], 1, jS[2]);\n                    }\n                    if (parts.teethLower) {\n                        parts.teethLower.position.set(0, 0.02, 0.06);\n                        parts.teethLower.scale.set(1, 1, 1);\n                    }\n                    // Also scale and position stump parts to match\n                    if (parts.neckStump) parts.neckStump.scale.set(props.head[0], props.head[1], props.head[2]);\n                    if (parts.lArmStump) { parts.lArmStump.scale.set(props.arm[0], props.arm[1], props.arm[2]); parts.lArmStump.position.x = -armOffsetX; }\n                    if (parts.rArmStump) { parts.rArmStump.scale.set(props.arm[0], props.arm[1], props.arm[2]); parts.rArmStump.position.x = armOffsetX; }\n                    if (parts.lLegStump) { parts.lLegStump.scale.set(props.leg[0], props.leg[1], props.leg[2]); parts.lLegStump.position.x = -legOffsetX; }\n                    if (parts.rLegStump) { parts.rLegStump.scale.set(props.leg[0], props.leg[1], props.leg[2]); parts.rLegStump.position.x = legOffsetX; }\n                }\n\n                function prepareZombieCorpse(corpse, bodyParts, animTimer, state, zombiePos, typeName) {\n                    if (!corpse) return null;\n                    corpse.rotation.order = 'YXZ';\n                    corpse.rotation.set(0, corpse.rotation.y, 0);\n                    // Preserve scale (boss zombies are larger)\n                    if (zombiePos) corpse.position.set(zombiePos.x, zombiePos.y || 0, zombiePos.z);\n                    corpse.userData.proxyRenderable = true;\n                    // Apply per-type body proportions so corpse matches live zombie\n                    if (typeName) applyTypeProportionsToCorpse(corpse, typeName);\n                    syncZombieBodyParts(corpse, bodyParts);\n                    applyZombiePoseToProxy(corpse, animTimer, state, bodyParts);\n                    setProxyMeshRenderable(corpse, true);\n                    return corpse.userData.parts || cacheZombieParts(corpse);\n                }\n\n                function isZombieInRenderView(slot, playerPos, viewDir, horizontalTan) {\n                    const dx = zombieRuntime.x[slot] - playerPos.x;\n                    const dz = zombieRuntime.z[slot] - playerPos.z;\n                    const distSq = dx * dx + dz * dz;\n                    if (distSq < 16) return true;\n                    const forward = dx * viewDir.x + dz * viewDir.z;\n                    const radius = Math.max(0.75, (zombieRuntime.radius[slot] || 0.65) * (zombieRuntime.scale[slot] || 1));\n                    if (forward < -radius - 1.5) return false;\n                    const side = Math.abs(dx * viewDir.z - dz * viewDir.x);\n                    return side <= Math.max(4.5, forward * horizontalTan * 1.45 + radius + 1.5);\n                }\n\n                function shouldRefreshZombieInstance(slot, frameId, playerPos) {\n                    if (zombieRenderForce[slot]) return true;\n                    if (zombieRuntime.isBoss[slot]) return true;\n                    if (zombieRuntime.impactTimer[slot] > 0 || zombieRuntime.impactMoveX[slot] !== 0 || zombieRuntime.impactMoveZ[slot] !== 0) return true;\n\n                    // Only throttle under genuine GPU pressure, and only for distant zombies.\n                    // At pressure < 2 (normal play), every zombie updates every frame \u2014 no visual stutter.\n                    const pressure = getPerfPressureLevel();\n                    if (pressure < 2) return true;\n\n                    const dx = zombieRuntime.x[slot] - playerPos.x;\n                    const dz = zombieRuntime.z[slot] - playerPos.z;\n                    const distSq = dx * dx + dz * dz;\n\n                    // pressure 2: skip every other frame for zombies > 20 m away\n                    // pressure 3: skip every other frame for zombies > 10 m away\n                    const farThreshold = pressure >= 3 ? 100 : 400;\n                    if (distSq <= farThreshold) return true;\n                    return ((frameId + slot) % 2) === 0;\n                }\n\n                function updateZombieInstances(w, ctx) {\n                    const slots = zombieRuntime.activeSlots;\n                    const frameId = ctx.frameId || gameState.frameId || 1;\n                    const playerPos = w.getComponent(playerEntity, 'Position');\n                    camera.getWorldDirection(zombieRenderScratch.viewDir);\n                    zombieRenderScratch.viewDir.y = 0;\n                    if (zombieRenderScratch.viewDir.lengthSq() < 0.0001) zombieRenderScratch.viewDir.set(0, 0, -1);\n                    else zombieRenderScratch.viewDir.normalize();\n                    const horizontalTan = Math.tan(((camera.fov || 75) * Math.PI / 180) * 0.5) * Math.max(1, camera.aspect || 1);\n                    for (let i = 0; i < slots.length; i++) {\n                        const slot = slots[i];\n                        if (zombieRuntime.deadPending[slot]) continue;\n                        const instanceSlot = zombieRuntime.instanceSlot[slot];\n                        if (instanceSlot < 0) continue;\n                        const inView = isZombieInRenderView(slot, playerPos, zombieRenderScratch.viewDir, horizontalTan);\n                        if (!inView) {\n                            if (zombieRenderInView[slot]) {\n                                clearZombieInstanceSlot(instanceSlot);\n                                zombieRenderInView[slot] = 0;\n                            }\n                            continue;\n                        }\n                        if (!zombieRenderInView[slot]) {\n                            zombieRenderInView[slot] = 1;\n                            zombieRenderForce[slot] = 1;\n                        }\n\n                        // Burning color flicker is evaluated in the fragment shader. CPU only\n                        // emits occasional embers and updates one scalar instance attribute.\n                        const isBurning = zombieRuntime.burningTimer[slot] > 0;\n                        if (isBurning) {\n                            if (Math.random() < 0.28) spawnZombieBurnFlicker(slot);\n                        }\n\n                        if (!shouldRefreshZombieInstance(slot, frameId, playerPos)) continue;\n                        zombieRenderLastFrame[slot] = frameId;\n                        zombieRenderForce[slot] = 0;\n                        const bodyParts = zombieRuntime.bodyMask[slot];\n                        const typeName = zombieRuntime.typeName[slot];\n                        const props = zombieTypeProportions[typeName] || defaultProportions;\n                        const yaw = zombieRuntime.yaw[slot] || 0;\n                        const x = zombieRuntime.x[slot];\n                        const z = zombieRuntime.z[slot];\n                        composeZombieMatrix(zombieRenderScratch.rootM, x, 0, z, 0, yaw, 0);\n                        let impactPitch = 0, impactRoll = 0, impactYaw = 0;\n                        const impactTimer = zombieRuntime.impactTimer[slot];\n                        const impactDuration = zombieRuntime.impactDuration[slot];\n                        if (impactTimer > 0 && impactDuration > 0) {\n                            const elapsed01 = clampValue(1 - impactTimer / impactDuration, 0, 1);\n                            const recover = clampValue(impactTimer / impactDuration, 0, 1);\n                            const pulse = Math.sin(elapsed01 * Math.PI) * Math.min(1, recover * 1.8);\n                            impactPitch = zombieRuntime.impactPitch[slot] * pulse;\n                            impactRoll = zombieRuntime.impactRoll[slot] * pulse;\n                            impactYaw = zombieRuntime.impactYaw[slot] * pulse;\n                        }\n                        zombieGpuFrameData.animTimer = zombieRuntime.animTimer[slot] || 0;\n                        zombieGpuFrameData.state = zombieRuntime.state[slot] || 1;\n                        zombieGpuFrameData.yaw = yaw;\n                        zombieGpuFrameData.bodyMask = bodyParts;\n                        zombieGpuFrameData.burn = isBurning ? 1 : 0;\n                        zombieGpuFrameData.impactPitch = impactPitch;\n                        zombieGpuFrameData.impactRoll = impactRoll;\n                        zombieGpuFrameData.impactYaw = impactYaw;\n                        zombieGpuFrameData.impactPart = zombieRuntime.impactPart[slot] || 0;\n\n                        // Parent matrices carry pose/attachment only. Scale stays on each child mesh,\n                        // preventing boss proportions from double-scaling eyes, belts, and hitboxes.\n                        const zScale = zombieRuntime.scale[slot] || 1;\n\n                        const bodyVisible = zombiePartAlive(bodyParts, 'body');\n                        const headVisible = zombiePartAlive(bodyParts, 'head');\n                        const leftArmVisible = zombiePartAlive(bodyParts, 'leftArm');\n                        const rightArmVisible = zombiePartAlive(bodyParts, 'rightArm');\n                        const leftLegVisible = zombiePartAlive(bodyParts, 'leftLeg');\n                        const rightLegVisible = zombiePartAlive(bodyParts, 'rightLeg');\n\n                        // Per-type body part scales\n                        const bS = props.body, hS = props.head, jS = props.jaw;\n                        const aS = props.arm, hdS = props.hand, lS = props.leg, fS = props.foot;\n                        const cosYaw = Math.cos(yaw);\n                        const sinYaw = Math.sin(yaw);\n                        const bodyPivotY = 1.15 * zScale;\n                        const headPivotY = 1.90 * zScale;\n\n                        setZombieChildInstance('body', instanceSlot, bodyVisible, zombieRenderScratch.rootM, 0, bodyPivotY, 0, 0, 0, 0, bS[0] * zScale, bS[1] * zScale, bS[2] * zScale, x, bodyPivotY, z);\n                        composeZombieMatrix(zombieRenderScratch.bodyM, 0, bodyPivotY, 0);\n                        zombieRenderScratch.bodyM.premultiply(zombieRenderScratch.rootM);\n                        setZombieChildInstance('belt', instanceSlot, bodyVisible, zombieRenderScratch.bodyM, 0, -0.42 * bS[1] * zScale, 0.18 * bS[2] * zScale, 0, 0, 0, bS[0] * zScale, zScale, bS[2] * zScale, x, bodyPivotY, z);\n\n                        setZombieChildInstance('head', instanceSlot, headVisible, zombieRenderScratch.rootM, 0, headPivotY, 0, 0, 0, 0, hS[0] * zScale, hS[1] * zScale, hS[2] * zScale, x, headPivotY, z);\n                        composeZombieMatrix(zombieRenderScratch.headM, 0, headPivotY, 0);\n                        zombieRenderScratch.headM.premultiply(zombieRenderScratch.rootM);\n                        setZombieChildInstance('leftEye', instanceSlot, headVisible, zombieRenderScratch.headM, -0.08 * hS[0] * zScale, 0.03 * hS[1] * zScale, 0.18 * hS[2] * zScale, 0, 0, 0, hS[0] * zScale, hS[1] * zScale, hS[2] * zScale, x, headPivotY, z);\n                        setZombieChildInstance('rightEye', instanceSlot, headVisible, zombieRenderScratch.headM, 0.08 * hS[0] * zScale, 0.03 * hS[1] * zScale, 0.18 * hS[2] * zScale, 0, 0, 0, hS[0] * zScale, hS[1] * zScale, hS[2] * zScale, x, headPivotY, z);\n                        setZombieChildInstance('jaw', instanceSlot, headVisible, zombieRenderScratch.headM, 0, -0.18 * hS[1] * zScale, 0.04 * hS[2] * zScale, 0, 0, 0, jS[0] * zScale, jS[1] * zScale, jS[2] * zScale, x, headPivotY, z);\n                        composeZombieMatrix(zombieRenderScratch.jawM, 0, -0.18 * hS[1] * zScale, 0.04 * hS[2] * zScale);\n                        zombieRenderScratch.jawM.premultiply(zombieRenderScratch.headM);\n                        setZombieChildInstance('teethLower', instanceSlot, headVisible, zombieRenderScratch.jawM, 0, 0.02 * jS[1] * zScale, 0.06 * jS[2] * zScale, 0, 0, 0, jS[0] * zScale, zScale, jS[2] * zScale, x, headPivotY, z);\n                        setZombieChildInstance('teethUpper', instanceSlot, headVisible, zombieRenderScratch.headM, 0, -0.12 * hS[1] * zScale, 0.16 * hS[2] * zScale, 0, 0, 0, jS[0] * zScale, zScale, jS[2] * zScale, x, headPivotY, z);\n                        setZombieChildInstance('headWound', instanceSlot, headVisible, zombieRenderScratch.headM, 0.12 * zScale, 0.05 * zScale, 0.12 * zScale, 0, 0, 0, hS[0] * zScale, hS[1] * zScale, hS[2] * zScale, x, headPivotY, z);\n\n                        // Arms - offset attachment point by body width scale\n                        const armOffsetX = 0.42 * bS[0] * zScale;\n                        const leftArmPivotX = x - cosYaw * armOffsetX;\n                        const leftArmPivotZ = z + sinYaw * armOffsetX;\n                        const rightArmPivotX = x + cosYaw * armOffsetX;\n                        const rightArmPivotZ = z - sinYaw * armOffsetX;\n                        const armPivotY = 1.50 * zScale;\n                        composeZombieMatrix(zombieRenderScratch.lArmM, -armOffsetX, armPivotY, 0);\n                        zombieRenderScratch.lArmM.premultiply(zombieRenderScratch.rootM);\n                        setZombieChildInstance('leftArm', instanceSlot, leftArmVisible, zombieRenderScratch.lArmM, 0, -0.36 * zScale, 0, 0, 0, 0, aS[0] * zScale, aS[1] * zScale, aS[2] * zScale, leftArmPivotX, armPivotY, leftArmPivotZ);\n                        setZombieChildInstance('leftHand', instanceSlot, leftArmVisible, zombieRenderScratch.lArmM, 0, -0.74 * aS[1] * zScale, 0, 0, 0, 0, hdS[0] * zScale, hdS[1] * zScale, hdS[2] * zScale, leftArmPivotX, armPivotY, leftArmPivotZ);\n                        composeZombieMatrix(zombieRenderScratch.rArmM, armOffsetX, armPivotY, 0);\n                        zombieRenderScratch.rArmM.premultiply(zombieRenderScratch.rootM);\n                        setZombieChildInstance('rightArm', instanceSlot, rightArmVisible, zombieRenderScratch.rArmM, 0, -0.36 * zScale, 0, 0, 0, 0, aS[0] * zScale, aS[1] * zScale, aS[2] * zScale, rightArmPivotX, armPivotY, rightArmPivotZ);\n                        setZombieChildInstance('rightHand', instanceSlot, rightArmVisible, zombieRenderScratch.rArmM, 0, -0.74 * aS[1] * zScale, 0, 0, 0, 0, hdS[0] * zScale, hdS[1] * zScale, hdS[2] * zScale, rightArmPivotX, armPivotY, rightArmPivotZ);\n\n                        // Legs - offset by body width\n                        const legOffsetX = 0.14 * bS[0] * zScale;\n                        const leftLegPivotX = x - cosYaw * legOffsetX;\n                        const leftLegPivotZ = z + sinYaw * legOffsetX;\n                        const rightLegPivotX = x + cosYaw * legOffsetX;\n                        const rightLegPivotZ = z - sinYaw * legOffsetX;\n                        const legPivotY = 0.67 * zScale;\n                        composeZombieMatrix(zombieRenderScratch.lLegM, -legOffsetX, legPivotY, 0);\n                        zombieRenderScratch.lLegM.premultiply(zombieRenderScratch.rootM);\n                        setZombieChildInstance('leftLeg', instanceSlot, leftLegVisible, zombieRenderScratch.lLegM, 0, -0.32 * zScale, 0, 0, 0, 0, lS[0] * zScale, lS[1] * zScale, lS[2] * zScale, leftLegPivotX, legPivotY, leftLegPivotZ);\n                        setZombieChildInstance('leftFoot', instanceSlot, leftLegVisible, zombieRenderScratch.lLegM, 0, -0.66 * lS[1] * zScale, 0.04 * zScale, 0, 0, 0, fS[0] * zScale, fS[1] * zScale, fS[2] * zScale, leftLegPivotX, legPivotY, leftLegPivotZ);\n                        composeZombieMatrix(zombieRenderScratch.rLegM, legOffsetX, legPivotY, 0);\n                        zombieRenderScratch.rLegM.premultiply(zombieRenderScratch.rootM);\n                        setZombieChildInstance('rightLeg', instanceSlot, rightLegVisible, zombieRenderScratch.rLegM, 0, -0.32 * zScale, 0, 0, 0, 0, lS[0] * zScale, lS[1] * zScale, lS[2] * zScale, rightLegPivotX, legPivotY, rightLegPivotZ);\n                        setZombieChildInstance('rightFoot', instanceSlot, rightLegVisible, zombieRenderScratch.rLegM, 0, -0.66 * lS[1] * zScale, 0.04 * zScale, 0, 0, 0, fS[0] * zScale, fS[1] * zScale, fS[2] * zScale, rightLegPivotX, legPivotY, rightLegPivotZ);\n\n                        setZombieChildInstance('neckStump', instanceSlot, !!bodyParts && !headVisible, zombieRenderScratch.rootM, 0, 1.73 * zScale, 0, 0, 0, 0, hS[0] * zScale, hS[1] * zScale, hS[2] * zScale, x, headPivotY, z);\n                        setZombieChildInstance('lArmStump', instanceSlot, !!bodyParts && !leftArmVisible, zombieRenderScratch.rootM, -armOffsetX, armPivotY, 0, 0, 0, 0, aS[0] * zScale, aS[1] * zScale, aS[2] * zScale, leftArmPivotX, armPivotY, leftArmPivotZ);\n                        setZombieChildInstance('rArmStump', instanceSlot, !!bodyParts && !rightArmVisible, zombieRenderScratch.rootM, armOffsetX, armPivotY, 0, 0, 0, 0, aS[0] * zScale, aS[1] * zScale, aS[2] * zScale, rightArmPivotX, armPivotY, rightArmPivotZ);\n                        setZombieChildInstance('lLegStump', instanceSlot, !!bodyParts && !leftLegVisible, zombieRenderScratch.rootM, -legOffsetX, legPivotY, 0, 0, 0, 0, lS[0] * zScale, lS[1] * zScale, lS[2] * zScale, leftLegPivotX, legPivotY, leftLegPivotZ);\n                        setZombieChildInstance('rLegStump', instanceSlot, !!bodyParts && !rightLegVisible, zombieRenderScratch.rootM, legOffsetX, legPivotY, 0, 0, 0, 0, lS[0] * zScale, lS[1] * zScale, lS[2] * zScale, rightLegPivotX, legPivotY, rightLegPivotZ);\n\n                        // Belly (only for Bloater types)\n                        const hasBelly = (bodyParts & ZOMBIE_PART_BELLY) !== 0;\n                        if (hasBelly && bodyVisible) {\n                            const bellyScale = typeName === 'BEHEMOTH' ? 1.4 : 1.15;\n                            setZombieChildInstance('belly', instanceSlot, true, zombieRenderScratch.bodyM, 0, -0.08 * bS[1] * zScale, 0.22 * bS[2] * zScale, 0, 0, 0, bellyScale * 1.2 * zScale, bellyScale * 0.95 * zScale, bellyScale * zScale, x, bodyPivotY, z);\n                        } else {\n                            setZombieChildInstance('belly', instanceSlot, false, zombieRenderScratch.rootM, 0, 0, 0, 0, 0, 0, 1, 1, 1, x, bodyPivotY, z);\n                        }\n\n                    }\n                }\n\n                const ZOMBIE_GRID_CELL_SIZE = 3.0;\n                const ZOMBIE_GRID_MIN_X = -24;\n                const ZOMBIE_GRID_MIN_Z = -24;\n                const ZOMBIE_GRID_WIDTH = 16;\n                const ZOMBIE_GRID_HEIGHT = 16;\n                const ZOMBIE_GRID_CELL_COUNT = ZOMBIE_GRID_WIDTH * ZOMBIE_GRID_HEIGHT;\n                const ZOMBIE_GRID_INV_CELL_SIZE = 1 / ZOMBIE_GRID_CELL_SIZE;\n                function zombieGridCoord(value, origin, limit) {\n                    const cell = Math.floor((value - origin) * ZOMBIE_GRID_INV_CELL_SIZE);\n                    return cell < 0 ? 0 : (cell >= limit ? limit - 1 : cell);\n                }\n\n                const spatialGrid = {\n                    cellSize: ZOMBIE_GRID_CELL_SIZE,\n                    head: new Int16Array(ZOMBIE_GRID_CELL_COUNT),\n                    next: new Int16Array(ZOMBIE_POOL_SIZE),\n                    init() {\n                        this.head.fill(-1);\n                        this.next.fill(-1);\n                    },\n                    clear() {\n                        this.head.fill(-1);\n                    },\n                    rebuildZombieRuntime(runtime) {\n                        this.clear();\n                        const slots = runtime.activeSlots;\n                        for (let i = 0; i < slots.length; i++) {\n                            const slot = slots[i];\n                            if (runtime.deadPending[slot]) continue;\n                            const cx = zombieGridCoord(runtime.x[slot], ZOMBIE_GRID_MIN_X, ZOMBIE_GRID_WIDTH);\n                            const cz = zombieGridCoord(runtime.z[slot], ZOMBIE_GRID_MIN_Z, ZOMBIE_GRID_HEIGHT);\n                            const cell = cz * ZOMBIE_GRID_WIDTH + cx;\n                            this.next[slot] = this.head[cell];\n                            this.head[cell] = slot;\n                        }\n                    },\n                    querySlotsAABB(minX, maxX, minZ, maxZ, out) {\n                        out.length = 0;\n                        const minCx = zombieGridCoord(minX, ZOMBIE_GRID_MIN_X, ZOMBIE_GRID_WIDTH);\n                        const maxCx = zombieGridCoord(maxX, ZOMBIE_GRID_MIN_X, ZOMBIE_GRID_WIDTH);\n                        const minCz = zombieGridCoord(minZ, ZOMBIE_GRID_MIN_Z, ZOMBIE_GRID_HEIGHT);\n                        const maxCz = zombieGridCoord(maxZ, ZOMBIE_GRID_MIN_Z, ZOMBIE_GRID_HEIGHT);\n                        for (let cx = minCx; cx <= maxCx; cx++) {\n                            for (let cz = minCz; cz <= maxCz; cz++) {\n                                let slot = this.head[cz * ZOMBIE_GRID_WIDTH + cx];\n                                while (slot !== -1) {\n                                    out.push(slot);\n                                    slot = this.next[slot];\n                                }\n                            }\n                        }\n                        return out;\n                    },\n                    queryAABB(minX, maxX, minZ, maxZ, out) {\n                        this.querySlotsAABB(minX, maxX, minZ, maxZ, out);\n                        for (let i = 0; i < out.length; i++) out[i] = zombieRuntime.entityId[out[i]];\n                        return out;\n                    }\n                };\n                spatialGrid.init();\n\n                function getZombieSpatialIndex(ctx, worldRef) {\n                    if (!ctx.spatialGridBuilt) {\n                        spatialGrid.rebuildZombieRuntime(zombieRuntime);\n                        ctx.spatialGridBuilt = true;\n                    }\n                    return spatialGrid;\n                }\n\n                function createZombieMesh() {\n                    const group = new THREE.Group();\n\n                    // Color variation for each zombie\n                    const skinHue = 0x4a5040 + Math.floor(Math.random() * 0x1a1a0a);\n                    const darkSkinHue = 0x3a4030 + Math.floor(Math.random() * 0x101008);\n                    const skinMat = new THREE.MeshPhongMaterial({ color: skinHue, specular: 0x222222, shininess: 10 });\n                    const darkSkinMat = new THREE.MeshPhongMaterial({ color: darkSkinHue, specular: 0x111111, shininess: 5 });\n                    const clothMat = zombieSharedMat.cloth.clone();\n                    const beltMat = zombieSharedMat.belt.clone();\n                    const eyeMat = zombieSharedMat.eye.clone();\n                    const boneMat = zombieSharedMat.bone.clone();\n                    const bloodMat = zombieSharedMat.blood.clone();\n                    const stumpMat = zombieSharedMat.stump.clone();\n                    const bellyMat = new THREE.MeshPhongMaterial({ color: 0x5a7a3a, specular: 0x222222, shininess: 10 });\n\n                    group.userData.materials = {\n                        skin: skinMat,\n                        darkSkin: darkSkinMat,\n                        cloth: clothMat,\n                        belt: beltMat,\n                        eye: eyeMat,\n                        bone: boneMat,\n                        blood: bloodMat,\n                        stump: stumpMat,\n                        belly: bellyMat\n                    };\n\n                    // === BODY (torso) ===\n                    const body = new THREE.Group();\n                    body.name = 'body';\n                    body.position.y = 1.15;\n\n                    const torso = new THREE.Mesh(zombieGeo.torso, clothMat);\n                    registerInstancePart(group, 'body', torso);\n                    body.add(torso);\n\n                    const belt = new THREE.Mesh(zombieGeo.belt, beltMat);\n                    belt.position.set(0, -0.42, 0.18);\n                    belt.name = 'belt';\n                    registerInstancePart(group, 'belt', belt);\n                    body.add(belt);\n\n                    const belly = new THREE.Mesh(zombieGeo.belly, bellyMat);\n                    belly.position.set(0, -0.08, 0.22);\n                    belly.name = 'belly';\n                    registerInstancePart(group, 'belly', belly);\n                    body.add(belly);\n\n                    group.add(body);\n\n                    // === HEAD (detailed) ===\n                    const head = new THREE.Group();\n                    head.name = 'head';\n                    head.position.y = 1.90;\n\n                    // Skull\n                    const skull = new THREE.Mesh(zombieGeo.skull, skinMat);\n                    registerInstancePart(group, 'head', skull);\n                    head.add(skull);\n\n                    // Jaw (animated during attack)\n                    const jaw = new THREE.Mesh(zombieGeo.jaw, darkSkinMat);\n                    jaw.position.set(0, -0.18, 0.04);\n                    jaw.name = 'jaw';\n                    registerInstancePart(group, 'jaw', jaw);\n                    head.add(jaw);\n                    const teethUpper = new THREE.Mesh(zombieGeo.teethUpper, boneMat);\n                    teethUpper.position.set(0, -0.12, 0.16);\n                    registerInstancePart(group, 'teethUpper', teethUpper);\n                    head.add(teethUpper);\n                    const teethLower = new THREE.Mesh(zombieGeo.teethLower, boneMat);\n                    teethLower.position.set(0, 0.02, 0.06);\n                    registerInstancePart(group, 'teethLower', teethLower);\n                    jaw.add(teethLower);\n                    const headWound = new THREE.Mesh(zombieGeo.headWound, bloodMat);\n                    headWound.position.set(0.12, 0.05, 0.12);\n                    registerInstancePart(group, 'headWound', headWound);\n                    head.add(headWound);\n                    group.add(head);\n\n                    // Eyes (glowing red)\n                    const leftEye = new THREE.Mesh(zombieGeo.eye, eyeMat);\n                    leftEye.position.set(-0.08, 0.03, 0.18);\n                    leftEye.name = 'leftEye';\n                    registerInstancePart(group, 'leftEye', leftEye);\n                    head.add(leftEye);\n                    const rightEye = new THREE.Mesh(zombieGeo.eye, eyeMat);\n                    rightEye.position.set(0.08, 0.03, 0.18);\n                    rightEye.name = 'rightEye';\n                    registerInstancePart(group, 'rightEye', rightEye);\n                    head.add(rightEye);\n\n                    // === LEFT ARM (segmented: upper + forearm + hand) ===\n                    const leftArm = new THREE.Group();\n                    leftArm.name = 'leftArm';\n                    leftArm.position.set(-0.42, 1.50, 0);\n\n                    const lArmMesh = new THREE.Mesh(zombieGeo.arm, skinMat);\n                    lArmMesh.position.y = -0.36;\n                    registerInstancePart(group, 'leftArm', lArmMesh);\n                    leftArm.add(lArmMesh);\n                    const lHand = new THREE.Mesh(zombieGeo.hand, darkSkinMat);\n                    lHand.position.y = -0.74;\n                    registerInstancePart(group, 'leftHand', lHand);\n                    leftArm.add(lHand);\n\n                    group.add(leftArm);\n\n                    // === RIGHT ARM (segmented) ===\n                    const rightArm = new THREE.Group();\n                    rightArm.name = 'rightArm';\n                    rightArm.position.set(0.42, 1.50, 0);\n\n                    const rArmMesh = new THREE.Mesh(zombieGeo.arm, skinMat);\n                    rArmMesh.position.y = -0.36;\n                    registerInstancePart(group, 'rightArm', rArmMesh);\n                    rightArm.add(rArmMesh);\n                    const rHand = new THREE.Mesh(zombieGeo.hand, darkSkinMat);\n                    rHand.position.y = -0.74;\n                    registerInstancePart(group, 'rightHand', rHand);\n                    rightArm.add(rHand);\n\n                    group.add(rightArm);\n\n                    // === LEFT LEG (segmented: thigh + shin + foot) ===\n                    const leftLeg = new THREE.Group();\n                    leftLeg.name = 'leftLeg';\n                    leftLeg.position.set(-0.14, 0.67, 0);\n\n                    const lLegMesh = new THREE.Mesh(zombieGeo.leg, clothMat);\n                    lLegMesh.position.y = -0.32;\n                    registerInstancePart(group, 'leftLeg', lLegMesh);\n                    leftLeg.add(lLegMesh);\n                    const lFoot = new THREE.Mesh(zombieGeo.foot, darkSkinMat);\n                    lFoot.position.set(0, -0.66, 0.04);\n                    registerInstancePart(group, 'leftFoot', lFoot);\n                    leftLeg.add(lFoot);\n\n                    group.add(leftLeg);\n\n                    // === RIGHT LEG (segmented) ===\n                    const rightLeg = new THREE.Group();\n                    rightLeg.name = 'rightLeg';\n                    rightLeg.position.set(0.14, 0.67, 0);\n\n                    const rLegMesh = new THREE.Mesh(zombieGeo.leg, clothMat);\n                    rLegMesh.position.y = -0.32;\n                    registerInstancePart(group, 'rightLeg', rLegMesh);\n                    rightLeg.add(rLegMesh);\n                    const rFoot = new THREE.Mesh(zombieGeo.foot, darkSkinMat);\n                    rFoot.position.set(0, -0.66, 0.04);\n                    registerInstancePart(group, 'rightFoot', rFoot);\n                    rightLeg.add(rFoot);\n\n                    group.add(rightLeg);\n\n                    // === BLOOD STUMPS (hidden until limb destroyed) ===\n                    const neckStump = new THREE.Mesh(zombieGeo.neckStump, stumpMat);\n                    neckStump.position.y = 1.73; neckStump.name = 'neckStump'; neckStump.visible = false;\n                    neckStump.userData.logicalVisible = false;\n                    registerInstancePart(group, 'neckStump', neckStump);\n                    group.add(neckStump);\n                    const lArmStump = new THREE.Mesh(zombieGeo.armStump, stumpMat);\n                    lArmStump.position.set(-0.38, 1.50, 0); lArmStump.name = 'lArmStump'; lArmStump.visible = false;\n                    lArmStump.userData.logicalVisible = false;\n                    registerInstancePart(group, 'lArmStump', lArmStump);\n                    group.add(lArmStump);\n                    const rArmStump = new THREE.Mesh(zombieGeo.armStump, stumpMat);\n                    rArmStump.position.set(0.38, 1.50, 0); rArmStump.name = 'rArmStump'; rArmStump.visible = false;\n                    rArmStump.userData.logicalVisible = false;\n                    registerInstancePart(group, 'rArmStump', rArmStump);\n                    group.add(rArmStump);\n                    const lLegStump = new THREE.Mesh(zombieGeo.legStump, stumpMat);\n                    lLegStump.position.set(-0.14, 0.67, 0); lLegStump.name = 'lLegStump'; lLegStump.visible = false;\n                    lLegStump.userData.logicalVisible = false;\n                    registerInstancePart(group, 'lLegStump', lLegStump);\n                    group.add(lLegStump);\n                    const rLegStump = new THREE.Mesh(zombieGeo.legStump, stumpMat);\n                    rLegStump.position.set(0.14, 0.67, 0); rLegStump.name = 'rLegStump'; rLegStump.visible = false;\n                    rLegStump.userData.logicalVisible = false;\n                    registerInstancePart(group, 'rLegStump', rLegStump);\n                    group.add(rLegStump);\n\n                    group.userData.palette = {\n                        skin: skinHue,\n                        darkSkin: darkSkinHue,\n                        cloth: clothMat.color.getHex(),\n                        bone: boneMat.color.getHex(),\n                        stump: stumpMat.color.getHex(),\n                        eye: eyeMat.color.getHex(),\n                        belt: beltMat.color.getHex()\n                    };\n                    group.visible = false;\n                    cacheZombieParts(group);\n                    return group;\n                }\n\n                // ===== ZOMBIE ANIMATION FUNCTION =====\n                function animateZombie(mesh, animTimer, state, bodyParts) {\n                    const parts = mesh.userData.parts || cacheZombieParts(mesh);\n                    const leftArm = parts.leftArm;\n                    const rightArm = parts.rightArm;\n                    const leftLeg = parts.leftLeg;\n                    const rightLeg = parts.rightLeg;\n                    const head = parts.head;\n                    const body = parts.body;\n                    const jaw = parts.jaw;\n\n                    const t = animTimer;\n                    const noLegs = bodyParts && !zombiePartAlive(bodyParts, 'leftLeg') && !zombiePartAlive(bodyParts, 'rightLeg');\n                    const oneLeg = bodyParts && (!zombiePartAlive(bodyParts, 'leftLeg') || !zombiePartAlive(bodyParts, 'rightLeg')) && !noLegs;\n\n                    if (noLegs) {\n                        // Crawling animation - body low, drag with arms\n                        mesh.position.y = -0.7;\n                        if (body) { body.rotation.x = 0.8; body.rotation.z = Math.sin(t * 0.5) * 0.05; }\n                        if (head && head.visible) { head.rotation.x = -0.4; head.rotation.z = Math.sin(t * 0.8) * 0.06; }\n                        if (leftArm && leftArm.visible) {\n                            leftArm.rotation.x = -1.2 + Math.sin(t) * 0.5;\n                            leftArm.rotation.z = 0.2;\n                        }\n                        if (rightArm && rightArm.visible) {\n                            rightArm.rotation.x = -1.2 + Math.sin(t + Math.PI) * 0.5;\n                            rightArm.rotation.z = -0.2;\n                        }\n                        if (jaw) jaw.rotation.x = 0.05 + Math.abs(Math.sin(t * 2)) * 0.08;\n                        return;\n                    }\n\n                    if (oneLeg) {\n                        // Limping - uneven gait\n                        mesh.position.y = Math.sin(t * 1.5) * 0.06 - 0.15;\n                        if (body) { body.rotation.z = Math.sin(t * 1.5) * 0.12; body.rotation.x = 0.1; }\n                    }\n\n                    if (state === 1) {\n                        // ZOMBIE SHAMBLE ANIMATION - lurching, asymmetric, undead gait\n                        const slowT = t * 0.7; // Slower, more deliberate movement\n\n                        // Uneven vertical bob - lurching side to side\n                        if (!oneLeg) mesh.position.y = Math.abs(Math.sin(slowT * 1.8)) * 0.06 + Math.sin(slowT * 0.9) * 0.02;\n\n                        // Body hunched forward, swaying drunkenly\n                        if (body && !oneLeg) {\n                            body.rotation.x = 0.25 + Math.sin(slowT * 0.8) * 0.08; // hunched forward\n                            body.rotation.z = Math.sin(slowT * 0.6) * 0.12; // heavy side sway\n                            body.rotation.y = Math.sin(slowT * 0.4) * 0.06; // twisting\n                        }\n\n                        // Head hangs low, lolling to the side\n                        if (head && head.visible) {\n                            head.rotation.x = 0.2 + Math.sin(slowT * 1.2) * 0.1; // drooping forward\n                            head.rotation.z = 0.15 + Math.sin(slowT * 0.5) * 0.12; // tilted to one side\n                            head.rotation.y = Math.sin(slowT * 0.3) * 0.08;\n                            if (jaw) jaw.rotation.x = 0.08 + Math.abs(Math.sin(slowT * 0.7)) * 0.06; // slack jaw\n                        }\n\n                        // Arms stretched forward like reaching for prey, asymmetric\n                        if (leftArm && leftArm.visible) {\n                            leftArm.rotation.x = -1.1 + Math.sin(slowT * 0.6) * 0.2; // reaching forward\n                            leftArm.rotation.z = 0.15 + Math.sin(slowT * 0.4) * 0.05;\n                        }\n                        if (rightArm && rightArm.visible) {\n                            rightArm.rotation.x = -0.8 + Math.sin(slowT * 0.5 + 1.2) * 0.25; // slightly lower, asymmetric\n                            rightArm.rotation.z = -0.2 + Math.sin(slowT * 0.35) * 0.06;\n                        }\n\n                        // Legs: dragging, stiff, uneven shamble\n                        if (leftLeg && leftLeg.visible) {\n                            leftLeg.rotation.x = Math.sin(slowT) * 0.35; // shorter stride\n                        }\n                        if (rightLeg && rightLeg.visible) {\n                            rightLeg.rotation.x = Math.sin(slowT + Math.PI) * 0.25 + 0.1; // dragging leg, offset\n                        }\n\n                    } else if (state === 2) {\n                        // ATTACK ANIMATION\n                        const atkCycle = Math.sin(t * 5);\n                        mesh.position.y = 0;\n\n                        // Lunge body forward\n                        if (body) {\n                            body.rotation.x = 0.15 + Math.abs(atkCycle) * 0.1;\n                            body.rotation.z = Math.sin(t * 3) * 0.05;\n                        }\n\n                        // Head lunges, jaw opens\n                        if (head && head.visible) {\n                            head.rotation.x = 0.2 + atkCycle * 0.12;\n                            head.rotation.z = Math.sin(t * 4) * 0.08;\n                            if (jaw) jaw.rotation.x = 0.15 + Math.abs(atkCycle) * 0.2;\n                        }\n\n                        // Arms reach & claw\n                        if (leftArm && leftArm.visible) {\n                            leftArm.rotation.x = -1.3 + atkCycle * 0.4;\n                            leftArm.rotation.z = 0.2 + Math.sin(t * 6) * 0.1;\n                        }\n                        if (rightArm && rightArm.visible) {\n                            rightArm.rotation.x = -1.3 - atkCycle * 0.4;\n                            rightArm.rotation.z = -0.2 + Math.sin(t * 6 + Math.PI) * 0.1;\n                        }\n                    }\n                }\n\n                const zombieMeshPool = new ObjectPool(\n                    createZombieMesh,\n                    (mesh) => {\n                        detachFromScene(mesh);\n                        mesh.visible = false; mesh.position.set(0, -100, 0); mesh.rotation.set(0, 0, 0); mesh.scale.set(1, 1, 1);\n                        const parts = mesh.userData.parts || cacheZombieParts(mesh);\n                        for (let i = 0; i < visiblePartNames.length; i++) {\n                            const name = visiblePartNames[i];\n                            if (parts[name]) {\n                                parts[name].visible = true;\n                                parts[name].userData.logicalVisible = true;\n                                parts[name].rotation.set(0, 0, 0);\n                            }\n                        }\n                        for (let i = 0; i < facePartNames.length; i++) {\n                            const name = facePartNames[i];\n                            if (parts[name]) {\n                                parts[name].visible = true;\n                                parts[name].userData.logicalVisible = true;\n                                parts[name].rotation.set(0, 0, 0);\n                            }\n                        }\n                        for (let i = 0; i < childPartNames.length; i++) {\n                            const name = childPartNames[i];\n                            if (parts[name]) {\n                                parts[name].visible = true;\n                                parts[name].userData.logicalVisible = true;\n                                parts[name].rotation.set(0, 0, 0);\n                            }\n                        }\n                        for (let i = 0; i < animatedPartNames.length; i++) {\n                            const name = animatedPartNames[i];\n                            if (parts[name]) parts[name].rotation.set(0, 0, 0);\n                        }\n                        for (const partName in stumpByPart) {\n                            const stump = parts[stumpByPart[partName]];\n                            if (stump) {\n                                stump.visible = false;\n                                stump.userData.logicalVisible = false;\n                                stump.rotation.set(0, 0, 0);\n                            }\n                        }\n                    },\n                    ZOMBIE_PROXY_INITIAL_POOL_SIZE,\n                    { maxIdle: ZOMBIE_PROXY_WARM_TARGET, dispose: disposePooledThreeObject }\n                );\n\n                function warmZombieProxyPool(targetSize = ZOMBIE_PROXY_WARM_TARGET) {\n                    if (zombieMeshPool.size >= targetSize) return;\n                    scheduleIdleTask((deadline) => {\n                        if (gameState.started && (!gameState.waveActive || gameState.upgradeScreenOpen)) {\n                            warmZombieProxyPool(targetSize);\n                            return;\n                        }\n                        let made = 0;\n                        const hasBudget = () => !deadline || deadline.timeRemaining === undefined || deadline.timeRemaining() > 4;\n                        while (zombieMeshPool.size < targetSize && made < ZOMBIE_PROXY_WARM_BATCH && hasBudget()) {\n                            zombieMeshPool.warm(1);\n                            made++;\n                        }\n                        if (zombieMeshPool.size < targetSize) warmZombieProxyPool(targetSize);\n                    }, 2000);\n                }\n                warmZombieProxyPool();\n\n                // Track low-water hits so we don't queue dozens of warm tasks.\n                let zombieProxyLowWaterScheduled = false;\n                function maybeTopUpZombieProxyPool() {\n                    if (zombieProxyLowWaterScheduled) return;\n                    if (zombieMeshPool.size >= ZOMBIE_PROXY_WARM_LOW_WATER) return;\n                    zombieProxyLowWaterScheduled = true;\n                    // Schedule warm to fire as soon as the harness gets an idle slice.\n                    scheduleIdleTask(() => {\n                        zombieProxyLowWaterScheduled = false;\n                        if (zombieMeshPool.size < ZOMBIE_PROXY_WARM_TARGET) warmZombieProxyPool();\n                    }, 0);\n                }\n\n                // ===== BULLET TRACER INSTANCING =====\n                const bulletTracerGeo = new THREE.CylinderGeometry(0.005, 0.002, 0.2, 5);\n                const projectileTracerMeshes = [];\n                const projectileTracerDirty = [];\n                const projectileTracerCountDirty = [];\n                const projectileTracerMaxSlot = [];\n                const projectileTracerZeroMatrix = new THREE.Matrix4().makeScale(0, 0, 0);\n                const projectileTracerMatrix = new THREE.Matrix4();\n                const projectileTracerPosition = new THREE.Vector3();\n                const projectileTracerQuaternion = new THREE.Quaternion();\n                const projectileTracerScale = new THREE.Vector3();\n                const projectileTracerDirection = new THREE.Vector3();\n                const projectileTracerYAxis = new THREE.Vector3(0, 1, 0);\n\n                function createProjectileTracerMaterial(color) {\n                    const mat = new THREE.MeshBasicMaterial({\n                        color,\n                        transparent: true,\n                        opacity: 0.95,\n                        blending: THREE.AdditiveBlending,\n                        depthWrite: false\n                    });\n                    mat.onBeforeCompile = (shader) => {\n                        shader.vertexShader = shader.vertexShader\n                            .replace('#include <common>', '#include <common>\\nvarying float vTracerY;')\n                            .replace('#include <begin_vertex>', '#include <begin_vertex>\\nvTracerY = position.y;');\n                        shader.fragmentShader = shader.fragmentShader\n                            .replace('#include <common>', '#include <common>\\nvarying float vTracerY;')\n                            .replace('#include <dithering_fragment>', `\n                            float tracerT = clamp((vTracerY + 0.1) / 0.2, 0.0, 1.0);\n                            gl_FragColor.rgb = mix(gl_FragColor.rgb, vec3(1.0), pow(tracerT, 2.5));\n                            gl_FragColor.a *= pow(tracerT, 1.5);\n                            #include <dithering_fragment>\n                        `);\n                    };\n                    return mat;\n                }\n\n                function getProjectileTracerMesh(weaponIndex, ws) {\n                    if (weaponIndex < 0) return null;\n                    let mesh = projectileTracerMeshes[weaponIndex];\n                    if (mesh) return mesh;\n                    mesh = new THREE.InstancedMesh(bulletTracerGeo, createProjectileTracerMaterial((ws && ws.bulletColor) || 0xffdd44), MAX_PROJECTILES);\n                    mesh.name = `projectileTracerInstances_${weaponIndex}`;\n                    mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);\n                    mesh.frustumCulled = false;\n                    mesh.count = 0;\n                    for (let i = 0; i < MAX_PROJECTILES; i++) mesh.setMatrixAt(i, projectileTracerZeroMatrix);\n                    mesh.instanceMatrix.needsUpdate = true;\n                    scene.add(mesh);\n                    projectileTracerMeshes[weaponIndex] = mesh;\n                    projectileTracerDirty[weaponIndex] = 0;\n                    projectileTracerCountDirty[weaponIndex] = 0;\n                    projectileTracerMaxSlot[weaponIndex] = -1;\n                    return mesh;\n                }\n\n                function markProjectileTracerDirty(weaponIndex) {\n                    if (weaponIndex < 0) return;\n                    projectileTracerDirty[weaponIndex] = 1;\n                }\n\n                function markProjectileTracerCountDirty(weaponIndex) {\n                    if (weaponIndex < 0) return;\n                    projectileTracerCountDirty[weaponIndex] = 1;\n                }\n\n                function refreshProjectileTracerMaxSlot(weaponIndex) {\n                    if (weaponIndex < 0) return;\n                    let maxSlot = -1;\n                    for (let i = 0; i < activeProjectileSlots.length; i++) {\n                        const slot = activeProjectileSlots[i];\n                        if (projectileActive[slot] && projectileWeaponIndex[slot] === weaponIndex && slot > maxSlot) maxSlot = slot;\n                    }\n                    projectileTracerMaxSlot[weaponIndex] = maxSlot;\n                    markProjectileTracerCountDirty(weaponIndex);\n                }\n\n                function clearProjectileTracerSlot(slot, weaponIndex) {\n                    const mesh = projectileTracerMeshes[weaponIndex];\n                    if (!mesh) return;\n                    mesh.setMatrixAt(slot, projectileTracerZeroMatrix);\n                    markProjectileTracerDirty(weaponIndex);\n                    if (projectileTracerMaxSlot[weaponIndex] === slot) refreshProjectileTracerMaxSlot(weaponIndex);\n                }\n\n                function setProjectileTracerSlot(slot, ws, midX, midY, midZ, dirX, dirY, dirZ, travelDist) {\n                    const weaponIndex = projectileWeaponIndex[slot];\n                    const isSmartTracer = projectileSmartTargetSlot[slot] >= 0;\n                    const mesh = getProjectileTracerMesh(weaponIndex, ws);\n                    if (!mesh) return;\n                    if (mesh.material && mesh.material.color) {\n                        const tracerColor = isSmartTracer ? SMART_PISTOL_TRACER_COLOR : ((ws && ws.bulletColor) || 0xffdd44);\n                        if (mesh.material.color.getHex() !== tracerColor) mesh.material.color.setHex(tracerColor);\n                    }\n                    projectileTracerDirection.set(dirX, dirY, dirZ);\n                    if (projectileTracerDirection.lengthSq() <= 0.000001) projectileTracerDirection.set(0, 0, -1);\n                    else projectileTracerDirection.normalize();\n                    projectileTracerPosition.set(midX, midY, midZ);\n                    projectileTracerQuaternion.setFromUnitVectors(projectileTracerYAxis, projectileTracerDirection);\n                    const tracerRadius = (((ws && ws.bulletSize) || 0.02) / 0.02) * (isSmartTracer ? 0.72 : 0.45);\n                    const tracerLength = isSmartTracer\n                        ? Math.max(((ws && ws.bulletLength) || 0.2), travelDist * 1.85)\n                        : Math.max(((ws && ws.bulletLength) || 0.2), travelDist * 1.35);\n                    projectileTracerScale.set(tracerRadius, tracerLength / 0.2, tracerRadius);\n                    projectileTracerMatrix.compose(projectileTracerPosition, projectileTracerQuaternion, projectileTracerScale);\n                    mesh.setMatrixAt(slot, projectileTracerMatrix);\n                    markProjectileTracerDirty(weaponIndex);\n                    const maxSlot = projectileTracerMaxSlot[weaponIndex] === undefined ? -1 : projectileTracerMaxSlot[weaponIndex];\n                    if (maxSlot < slot) {\n                        projectileTracerMaxSlot[weaponIndex] = slot;\n                        markProjectileTracerCountDirty(weaponIndex);\n                    }\n                }\n\n                function flushProjectileTracerUpdates() {\n                    for (let i = 0; i < projectileTracerMeshes.length; i++) {\n                        const mesh = projectileTracerMeshes[i];\n                        if (!mesh) continue;\n                        if (projectileTracerCountDirty[i]) {\n                            const maxSlot = projectileTracerMaxSlot[i] === undefined ? -1 : projectileTracerMaxSlot[i];\n                            mesh.count = maxSlot + 1;\n                            projectileTracerCountDirty[i] = 0;\n                        }\n                        if (projectileTracerDirty[i]) {\n                            mesh.instanceMatrix.needsUpdate = true;\n                            projectileTracerDirty[i] = 0;\n                        }\n                    }\n                }\n\n                // ===== SHELL CASING SYSTEM =====\n                const MAX_SHELL_CASINGS = 30;\n                const shellCasingSpecs = [\n                    { radiusTop: 0.005, radiusBot: 0.008, length: 0.022, color: 0xc4a030, rimColor: 0xa08020, ejSpeed: 2.5, spinMult: 1.0 }, // Pistol - 9mm brass\n                    { radiusTop: 0.010, radiusBot: 0.012, length: 0.055, color: 0xcc2222, rimColor: 0x881111, ejSpeed: 1.8, spinMult: 0.7 }, // Shotgun - red shell\n                    { radiusTop: 0.005, radiusBot: 0.007, length: 0.052, color: 0xc4a030, rimColor: 0xa08020, ejSpeed: 3.0, spinMult: 1.2 }, // Sniper - long rifle brass\n                    null, // Katana - no casing\n                    { radiusTop: 0.004, radiusBot: 0.006, length: 0.018, color: 0xd4aa30, rimColor: 0xb09020, ejSpeed: 3.5, spinMult: 1.5 }, // Gatling - small brass fast eject\n                    { radiusTop: 0.005, radiusBot: 0.007, length: 0.035, color: 0xc4a030, rimColor: 0xa08020, ejSpeed: 2.8, spinMult: 1.1 }, // LMG - 7.62 brass\n                    { radiusTop: 0.005, radiusBot: 0.006, length: 0.030, color: 0xc4a030, rimColor: 0xa08020, ejSpeed: 2.8, spinMult: 1.0 }  // Assault Rifle - 5.56 brass\n                ];\n                const shellGeometries = shellCasingSpecs.map(spec => {\n                    if (!spec) return null;\n                    return new THREE.CylinderGeometry(spec.radiusTop, spec.radiusBot, spec.length, 6);\n                });\n                const shellRimGeometries = shellCasingSpecs.map(spec => {\n                    if (!spec) return null;\n                    return new THREE.CylinderGeometry(spec.radiusBot * 1.3, spec.radiusBot * 1.3, spec.length * 0.08, 6);\n                });\n                const shellActive = new Uint8Array(MAX_SHELL_CASINGS);\n                const shellActiveIndex = new Int16Array(MAX_SHELL_CASINGS);\n                const shellWeaponIndex = new Int16Array(MAX_SHELL_CASINGS);\n                const shellX = new Float32Array(MAX_SHELL_CASINGS);\n                const shellY = new Float32Array(MAX_SHELL_CASINGS);\n                const shellZ = new Float32Array(MAX_SHELL_CASINGS);\n                const shellRX = new Float32Array(MAX_SHELL_CASINGS);\n                const shellRY = new Float32Array(MAX_SHELL_CASINGS);\n                const shellRZ = new Float32Array(MAX_SHELL_CASINGS);\n                const shellVX = new Float32Array(MAX_SHELL_CASINGS);\n                const shellVY = new Float32Array(MAX_SHELL_CASINGS);\n                const shellVZ = new Float32Array(MAX_SHELL_CASINGS);\n                const shellRVX = new Float32Array(MAX_SHELL_CASINGS);\n                const shellRVY = new Float32Array(MAX_SHELL_CASINGS);\n                const shellRVZ = new Float32Array(MAX_SHELL_CASINGS);\n                const shellAge = new Float32Array(MAX_SHELL_CASINGS);\n                const shellBounceCount = new Uint8Array(MAX_SHELL_CASINGS);\n                const shellSettled = new Uint8Array(MAX_SHELL_CASINGS);\n                const activeShellSlots = [];\n                const freeShellSlots = [];\n                shellActiveIndex.fill(-1);\n                shellWeaponIndex.fill(-1);\n                for (let i = MAX_SHELL_CASINGS - 1; i >= 0; i--) freeShellSlots.push(i);\n\n                const shellBodyInstanceMeshes = [];\n                const shellRimInstanceMeshes = [];\n                const shellInstanceDirty = new Uint8Array(shellCasingSpecs.length);\n                const shellInstanceCountDirty = new Uint8Array(shellCasingSpecs.length);\n                const shellInstanceMaxSlot = new Int16Array(shellCasingSpecs.length);\n                const shellZeroMatrix = new THREE.Matrix4().makeScale(0, 0, 0);\n                const shellMatrix = new THREE.Matrix4();\n                const shellRimMatrix = new THREE.Matrix4();\n                const shellPositionScratch = new THREE.Vector3();\n                const shellQuaternionScratch = new THREE.Quaternion();\n                const shellEulerScratch = new THREE.Euler();\n                const shellScaleScratch = new THREE.Vector3(1, 1, 1);\n                const shellRimOffsetMatrices = shellCasingSpecs.map(spec => spec ? new THREE.Matrix4().makeTranslation(0, -spec.length * 0.46, 0) : null);\n                shellInstanceMaxSlot.fill(-1);\n                for (let i = 0; i < shellCasingSpecs.length; i++) {\n                    const spec = shellCasingSpecs[i];\n                    if (!spec) {\n                        shellBodyInstanceMeshes[i] = null;\n                        shellRimInstanceMeshes[i] = null;\n                        continue;\n                    }\n                    const bodyMat = new THREE.MeshPhongMaterial({ color: spec.color, specular: 0xffdd88, shininess: 80 });\n                    const rimMat = new THREE.MeshPhongMaterial({ color: spec.rimColor, specular: 0xffcc66, shininess: 60 });\n                    const bodyMesh = new THREE.InstancedMesh(shellGeometries[i], bodyMat, MAX_SHELL_CASINGS);\n                    const rimMesh = new THREE.InstancedMesh(shellRimGeometries[i], rimMat, MAX_SHELL_CASINGS);\n                    bodyMesh.name = `shellBodyInstances_${i}`;\n                    rimMesh.name = `shellRimInstances_${i}`;\n                    bodyMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);\n                    rimMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);\n                    bodyMesh.frustumCulled = false;\n                    rimMesh.frustumCulled = false;\n                    bodyMesh.count = 0;\n                    rimMesh.count = 0;\n                    for (let slot = 0; slot < MAX_SHELL_CASINGS; slot++) {\n                        bodyMesh.setMatrixAt(slot, shellZeroMatrix);\n                        rimMesh.setMatrixAt(slot, shellZeroMatrix);\n                    }\n                    bodyMesh.instanceMatrix.needsUpdate = true;\n                    rimMesh.instanceMatrix.needsUpdate = true;\n                    scene.add(bodyMesh);\n                    scene.add(rimMesh);\n                    shellBodyInstanceMeshes[i] = bodyMesh;\n                    shellRimInstanceMeshes[i] = rimMesh;\n                }\n\n                function markShellInstanceDirty(weaponIndex) {\n                    if (weaponIndex < 0) return;\n                    shellInstanceDirty[weaponIndex] = 1;\n                }\n\n                function markShellInstanceCountDirty(weaponIndex) {\n                    if (weaponIndex < 0) return;\n                    shellInstanceCountDirty[weaponIndex] = 1;\n                }\n\n                function refreshShellInstanceMaxSlot(weaponIndex) {\n                    if (weaponIndex < 0) return;\n                    let maxSlot = -1;\n                    for (let i = 0; i < activeShellSlots.length; i++) {\n                        const slot = activeShellSlots[i];\n                        if (shellActive[slot] && shellWeaponIndex[slot] === weaponIndex && slot > maxSlot) maxSlot = slot;\n                    }\n                    shellInstanceMaxSlot[weaponIndex] = maxSlot;\n                    markShellInstanceCountDirty(weaponIndex);\n                }\n\n                function clearShellInstanceMatrix(slot, weaponIndex) {\n                    const bodyMesh = shellBodyInstanceMeshes[weaponIndex];\n                    const rimMesh = shellRimInstanceMeshes[weaponIndex];\n                    if (!bodyMesh || !rimMesh) return;\n                    bodyMesh.setMatrixAt(slot, shellZeroMatrix);\n                    rimMesh.setMatrixAt(slot, shellZeroMatrix);\n                    markShellInstanceDirty(weaponIndex);\n                    if (shellInstanceMaxSlot[weaponIndex] === slot) refreshShellInstanceMaxSlot(weaponIndex);\n                }\n\n                function setShellInstanceMatrix(slot) {\n                    const weaponIndex = shellWeaponIndex[slot];\n                    const bodyMesh = shellBodyInstanceMeshes[weaponIndex];\n                    const rimMesh = shellRimInstanceMeshes[weaponIndex];\n                    if (!bodyMesh || !rimMesh) return;\n                    shellPositionScratch.set(shellX[slot], shellY[slot], shellZ[slot]);\n                    shellEulerScratch.set(shellRX[slot], shellRY[slot], shellRZ[slot]);\n                    shellQuaternionScratch.setFromEuler(shellEulerScratch);\n                    shellMatrix.compose(shellPositionScratch, shellQuaternionScratch, shellScaleScratch);\n                    shellRimMatrix.multiplyMatrices(shellMatrix, shellRimOffsetMatrices[weaponIndex]);\n                    bodyMesh.setMatrixAt(slot, shellMatrix);\n                    rimMesh.setMatrixAt(slot, shellRimMatrix);\n                    markShellInstanceDirty(weaponIndex);\n                    if (shellInstanceMaxSlot[weaponIndex] < slot) {\n                        shellInstanceMaxSlot[weaponIndex] = slot;\n                        markShellInstanceCountDirty(weaponIndex);\n                    }\n                }\n\n                function flushShellInstanceUpdates() {\n                    for (let i = 0; i < shellCasingSpecs.length; i++) {\n                        const bodyMesh = shellBodyInstanceMeshes[i];\n                        const rimMesh = shellRimInstanceMeshes[i];\n                        if (!bodyMesh || !rimMesh) continue;\n                        if (shellInstanceCountDirty[i]) {\n                            const count = shellInstanceMaxSlot[i] + 1;\n                            bodyMesh.count = count;\n                            rimMesh.count = count;\n                            shellInstanceCountDirty[i] = 0;\n                        }\n                        if (shellInstanceDirty[i]) {\n                            bodyMesh.instanceMatrix.needsUpdate = true;\n                            rimMesh.instanceMatrix.needsUpdate = true;\n                            shellInstanceDirty[i] = 0;\n                        }\n                    }\n                }\n\n                function releaseShellCasingSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0 || !shellActive[slot]) return;\n                    shellActive[slot] = 0;\n                    const weaponIndex = shellWeaponIndex[slot];\n                    clearShellInstanceMatrix(slot, weaponIndex);\n                    shellWeaponIndex[slot] = -1;\n                    shellX[slot] = 0; shellY[slot] = 0; shellZ[slot] = 0;\n                    shellRX[slot] = 0; shellRY[slot] = 0; shellRZ[slot] = 0;\n                    shellAge[slot] = 0;\n                    shellSettled[slot] = 0;\n                    shellBounceCount[slot] = 0;\n                    const idx = shellActiveIndex[slot];\n                    if (idx >= 0) {\n                        const last = activeShellSlots.pop();\n                        if (idx < activeShellSlots.length) {\n                            activeShellSlots[idx] = last;\n                            shellActiveIndex[last] = idx;\n                        }\n                        shellActiveIndex[slot] = -1;\n                    }\n                    if (weaponIndex >= 0 && shellInstanceMaxSlot[weaponIndex] === slot) refreshShellInstanceMaxSlot(weaponIndex);\n                    freeShellSlots.push(slot);\n                }\n\n                function acquireShellCasingSlot() {\n                    if (!freeShellSlots.length && activeShellSlots.length) {\n                        let oldestSlot = activeShellSlots[0];\n                        let oldestAge = shellAge[oldestSlot];\n                        for (let i = 1; i < activeShellSlots.length; i++) {\n                            const slot = activeShellSlots[i];\n                            if (shellAge[slot] > oldestAge) {\n                                oldestAge = shellAge[slot];\n                                oldestSlot = slot;\n                            }\n                        }\n                        releaseShellCasingSlot(oldestSlot);\n                    }\n                    const slot = freeShellSlots.pop();\n                    if (slot === undefined) return -1;\n                    shellActive[slot] = 1;\n                    shellActiveIndex[slot] = activeShellSlots.length;\n                    activeShellSlots.push(slot);\n                    return slot;\n                }\n\n                // Per-weapon ejection data in camera-local space: right, up, forward, speed, spin.\n                const SHELL_EJ_RIGHT = 0;\n                const SHELL_EJ_UP = 1;\n                const SHELL_EJ_FWD = 2;\n                const SHELL_EJ_SPEED = 3;\n                const SHELL_EJ_SPIN = 4;\n                const SHELL_EJ_STRIDE = 5;\n                const shellEjectionData = new Float32Array(shellCasingSpecs.length * SHELL_EJ_STRIDE);\n                const shellEjectionActive = new Uint8Array(shellCasingSpecs.length);\n                function setShellEjectionData(weaponIndex, right, up, fwd) {\n                    const spec = shellCasingSpecs[weaponIndex];\n                    if (!spec) return;\n                    const o = weaponIndex * SHELL_EJ_STRIDE;\n                    shellEjectionData[o + SHELL_EJ_RIGHT] = right;\n                    shellEjectionData[o + SHELL_EJ_UP] = up;\n                    shellEjectionData[o + SHELL_EJ_FWD] = fwd;\n                    shellEjectionData[o + SHELL_EJ_SPEED] = spec.ejSpeed;\n                    shellEjectionData[o + SHELL_EJ_SPIN] = spec.spinMult;\n                    shellEjectionActive[weaponIndex] = 1;\n                }\n                setShellEjectionData(0, 0.09, -0.14, -0.37); // Pistol - right side of slide, receiver mid\n                setShellEjectionData(1, 0.12, -0.18, -0.45); // Shotgun - right side of receiver\n                setShellEjectionData(2, 0.13, -0.16, -0.52); // Sniper - bolt action right side\n                setShellEjectionData(4, 0.13, -0.20, -0.44); // Gatling - rapid eject right side\n                setShellEjectionData(5, 0.13, -0.18, -0.48); // LMG - receiver side\n                setShellEjectionData(6, 0.12, -0.16, -0.43); // AR - ejection port right side\n\n                function spawnShellCasing(weaponIndex, playerPos, playerYaw) {\n                    if (!shellEjectionActive[weaponIndex]) return;\n                    const ejOffset = weaponIndex * SHELL_EJ_STRIDE;\n                    const pressure = getPerfPressureLevel();\n                    if ((pressure >= 3 && activeShellSlots.length >= 8) || (pressure >= 2 && activeShellSlots.length >= 14)) return;\n                    if (!shouldSpawnBudgetedEffect('shell', 1, playerPos)) return;\n                    const slot = acquireShellCasingSlot();\n                    if (slot < 0) return;\n                    shellWeaponIndex[slot] = weaponIndex;\n                    // Use camera quaternion to transform ejection port from local to world space\n                    const camRight = effectScratch.v1.set(1, 0, 0).applyQuaternion(camera.quaternion);\n                    const camUp = effectScratch.v2.set(0, 1, 0).applyQuaternion(camera.quaternion);\n                    const camFwd = effectScratch.v3.set(0, 0, -1).applyQuaternion(camera.quaternion);\n                    // Position at the gun model's ejection port\n                    const rightOffset = shellEjectionData[ejOffset + SHELL_EJ_RIGHT];\n                    const upOffset = shellEjectionData[ejOffset + SHELL_EJ_UP];\n                    const fwdOffset = shellEjectionData[ejOffset + SHELL_EJ_FWD];\n                    shellX[slot] = playerPos.x + camRight.x * rightOffset + camUp.x * upOffset + camFwd.x * (-fwdOffset);\n                    shellY[slot] = playerPos.y + camRight.y * rightOffset + camUp.y * upOffset + camFwd.y * (-fwdOffset);\n                    shellZ[slot] = playerPos.z + camRight.z * rightOffset + camUp.z * upOffset + camFwd.z * (-fwdOffset);\n                    shellRX[slot] = 0;\n                    shellRY[slot] = 0;\n                    shellRZ[slot] = 0;\n                    const ejSpeed = shellEjectionData[ejOffset + SHELL_EJ_SPEED] * (0.85 + Math.random() * 0.3);\n                    // Eject rightward and slightly up from the gun\n                    const vx = camRight.x * ejSpeed * 0.7 + camUp.x * 0.8 + (Math.random() - 0.5) * 0.4;\n                    const vy = camRight.y * ejSpeed * 0.7 + camUp.y * 0.8 + 1.2 + Math.random() * 0.5;\n                    const vz = camRight.z * ejSpeed * 0.7 + camUp.z * 0.8 + (Math.random() - 0.5) * 0.4;\n                    const spinMult = shellEjectionData[ejOffset + SHELL_EJ_SPIN];\n                    shellVX[slot] = vx;\n                    shellVY[slot] = vy;\n                    shellVZ[slot] = vz;\n                    shellRVX[slot] = (Math.random() - 0.3) * 25 * spinMult;\n                    shellRVY[slot] = (Math.random() - 0.5) * 15 * spinMult;\n                    shellRVZ[slot] = (Math.random() - 0.5) * 20 * spinMult;\n                    shellAge[slot] = 0;\n                    shellBounceCount[slot] = 0;\n                    shellSettled[slot] = 0;\n                    setShellInstanceMatrix(slot);\n                }\n\n                function updateShellCasings(dt) {\n                    const pressure = getPerfPressureLevel();\n                    const settledKeepAge = pressure >= 3 ? 1.6 : (pressure >= 2 ? 2.4 : 4.5);\n                    for (let i = activeShellSlots.length - 1; i >= 0; i--) {\n                        const slot = activeShellSlots[i];\n                        if (shellWeaponIndex[slot] < 0) {\n                            releaseShellCasingSlot(slot);\n                            continue;\n                        }\n                        let visualDirty = false;\n                        shellAge[slot] += dt;\n                        if (!shellSettled[slot]) {\n                            shellVY[slot] -= 12 * dt;\n                            shellVX[slot] *= 0.995;\n                            shellVZ[slot] *= 0.995;\n                            shellX[slot] += shellVX[slot] * dt;\n                            shellY[slot] += shellVY[slot] * dt;\n                            shellZ[slot] += shellVZ[slot] * dt;\n                            shellRX[slot] += shellRVX[slot] * dt;\n                            shellRY[slot] += shellRVY[slot] * dt;\n                            shellRZ[slot] += shellRVZ[slot] * dt;\n                            visualDirty = true;\n                            if (shellY[slot] <= 0.015) {\n                                shellY[slot] = 0.015;\n                                shellBounceCount[slot]++;\n                                if (shellBounceCount[slot] >= 3 || Math.abs(shellVY[slot]) < 0.5) {\n                                    shellSettled[slot] = 1;\n                                    shellVY[slot] = 0; shellVX[slot] = 0; shellVZ[slot] = 0;\n                                    shellRVX[slot] = 0; shellRVY[slot] = 0; shellRVZ[slot] = 0;\n                                } else {\n                                    shellVY[slot] = -shellVY[slot] * 0.25;\n                                    shellVX[slot] *= 0.45;\n                                    shellVZ[slot] *= 0.45;\n                                    shellRVX[slot] *= 0.35;\n                                    shellRVY[slot] *= 0.35;\n                                    shellRVZ[slot] *= 0.35;\n                                }\n                            }\n                        }\n                        if (shellAge[slot] > (shellSettled[slot] ? settledKeepAge : 4.5)) {\n                            releaseShellCasingSlot(slot);\n                            continue;\n                        }\n                        if (visualDirty) setShellInstanceMatrix(slot);\n                    }\n                }\n\n                // ===== CONTIGUOUS PROJECTILE STORAGE =====\n                // Hot projectile data stays out of ECS Map/component churn while preserving real flight time,\n                // velocity, gravity, penetration, and swept collision.\n                const MAX_PROJECTILES = 160;\n                const MAX_PROJECTILE_HIT_HISTORY = 6;\n                const projectileActive = new Uint8Array(MAX_PROJECTILES);\n                const projectileX = new Float32Array(MAX_PROJECTILES);\n                const projectileY = new Float32Array(MAX_PROJECTILES);\n                const projectileZ = new Float32Array(MAX_PROJECTILES);\n                const projectileVX = new Float32Array(MAX_PROJECTILES);\n                const projectileVY = new Float32Array(MAX_PROJECTILES);\n                const projectileVZ = new Float32Array(MAX_PROJECTILES);\n                const projectileDamage = new Float32Array(MAX_PROJECTILES);\n                const projectileDropoff = new Float32Array(MAX_PROJECTILES);\n                const projectileLifetime = new Float32Array(MAX_PROJECTILES);\n                const projectilePenetration = new Int16Array(MAX_PROJECTILES);\n                const projectileWeaponIndex = new Int16Array(MAX_PROJECTILES);\n                const projectileHitCount = new Uint8Array(MAX_PROJECTILES);\n                const projectileHitEntities = new Int32Array(MAX_PROJECTILES * MAX_PROJECTILE_HIT_HISTORY);\n                const projectileActiveIndex = new Int16Array(MAX_PROJECTILES);\n                const projectileSmartTargetSlot = new Int16Array(MAX_PROJECTILES);\n                const projectileSmartTargetEntityId = new Int32Array(MAX_PROJECTILES);\n                const projectileSmartFullLock = new Uint8Array(MAX_PROJECTILES);\n                const projectileSmartRequiredLocks = new Uint8Array(MAX_PROJECTILES);\n                const projectileSmartFinalShot = new Uint8Array(MAX_PROJECTILES);\n                const projectileSmartStraightTime = new Float32Array(MAX_PROJECTILES);\n                projectileSmartTargetSlot.fill(-1);\n                projectileSmartTargetEntityId.fill(-1);\n                const activeProjectileSlots = [];\n                const freeProjectileSlots = [];\n                projectileHitEntities.fill(-1);\n                projectileActiveIndex.fill(-1);\n                projectileWeaponIndex.fill(-1);\n                for (let i = MAX_PROJECTILES - 1; i >= 0; i--) freeProjectileSlots.push(i);\n\n                function projectileHasHitEntity(slot, entityId) {\n                    const base = slot * MAX_PROJECTILE_HIT_HISTORY;\n                    const count = projectileHitCount[slot];\n                    for (let i = 0; i < count; i++) {\n                        if (projectileHitEntities[base + i] === entityId) return true;\n                    }\n                    return false;\n                }\n\n                function recordProjectileHitEntity(slot, entityId) {\n                    const base = slot * MAX_PROJECTILE_HIT_HISTORY;\n                    let count = projectileHitCount[slot];\n                    if (count < MAX_PROJECTILE_HIT_HISTORY) {\n                        projectileHitEntities[base + count] = entityId;\n                        projectileHitCount[slot] = count + 1;\n                        return;\n                    }\n                    for (let i = 1; i < MAX_PROJECTILE_HIT_HISTORY; i++) {\n                        projectileHitEntities[base + i - 1] = projectileHitEntities[base + i];\n                    }\n                    projectileHitEntities[base + MAX_PROJECTILE_HIT_HISTORY - 1] = entityId;\n                }\n\n                function releaseProjectileSlot(slot) {\n                    if (slot < 0 || !projectileActive[slot]) return;\n                    projectileActive[slot] = 0;\n                    projectileHitCount[slot] = 0;\n                    projectileSmartTargetSlot[slot] = -1;\n                    projectileSmartTargetEntityId[slot] = -1;\n                    projectileSmartFullLock[slot] = 0;\n                    projectileSmartRequiredLocks[slot] = 0;\n                    projectileSmartFinalShot[slot] = 0;\n                    projectileSmartStraightTime[slot] = 0;\n                    const base = slot * MAX_PROJECTILE_HIT_HISTORY;\n                    for (let i = 0; i < MAX_PROJECTILE_HIT_HISTORY; i++) projectileHitEntities[base + i] = -1;\n                    const weaponIndex = projectileWeaponIndex[slot];\n                    clearProjectileTracerSlot(slot, weaponIndex);\n                    const idx = projectileActiveIndex[slot];\n                    if (idx >= 0) {\n                        const last = activeProjectileSlots.pop();\n                        if (idx < activeProjectileSlots.length) {\n                            activeProjectileSlots[idx] = last;\n                            projectileActiveIndex[last] = idx;\n                        }\n                        projectileActiveIndex[slot] = -1;\n                    }\n                    projectileWeaponIndex[slot] = -1;\n                    freeProjectileSlots.push(slot);\n                }\n\n                function acquireProjectileSlot() {\n                    if (!freeProjectileSlots.length && activeProjectileSlots.length) {\n                        releaseProjectileSlot(activeProjectileSlots[0]);\n                    }\n                    const slot = freeProjectileSlots.pop();\n                    if (slot === undefined) return -1;\n                    projectileActive[slot] = 1;\n                    projectileActiveIndex[slot] = activeProjectileSlots.length;\n                    activeProjectileSlots.push(slot);\n                    projectileHitCount[slot] = 0;\n                    return slot;\n                }\n\n                function clearProjectiles() {\n                    for (let i = activeProjectileSlots.length - 1; i >= 0; i--) {\n                        releaseProjectileSlot(activeProjectileSlots[i]);\n                    }\n                }\n\n                function spawnProjectile(pos, dir, ws, weaponIndex, smartTarget = -1) {\n                    const slot = acquireProjectileSlot();\n                    if (slot < 0) return -1;\n                    projectileX[slot] = pos.x;\n                    projectileY[slot] = pos.y;\n                    projectileZ[slot] = pos.z;\n                    projectileVX[slot] = dir.x * ws.bulletSpeed;\n                    projectileVY[slot] = dir.y * ws.bulletSpeed;\n                    projectileVZ[slot] = dir.z * ws.bulletSpeed;\n                    projectileDamage[slot] = ws.damage;\n                    projectilePenetration[slot] = ws.penetration;\n                    projectileDropoff[slot] = ws.damageDropoff;\n                    projectileLifetime[slot] = 3.0;\n                    projectileWeaponIndex[slot] = weaponIndex;\n                    projectileSmartTargetSlot[slot] = smartTarget;\n                    projectileSmartTargetEntityId[slot] = smartTarget >= 0 ? zombieRuntime.entityId[smartTarget] : -1;\n                    projectileSmartFullLock[slot] = 0;\n                    projectileSmartRequiredLocks[slot] = 0;\n                    projectileSmartFinalShot[slot] = 0;\n                    projectileSmartStraightTime[slot] = smartTarget >= 0 ? SMART_PISTOL_INITIAL_STRAIGHT_TIME : 0;\n                    setProjectileTracerSlot(slot, ws, pos.x, pos.y, pos.z, dir.x, dir.y, dir.z, 0);\n                    return slot;\n                }\n\n                // ===== UNIFIED EFFECT MANAGER =====\n                const effectScratch = {\n                    viewDir: new THREE.Vector3(),\n                    v1: new THREE.Vector3(),\n                    v2: new THREE.Vector3(),\n                    v3: new THREE.Vector3(),\n                    v4: new THREE.Vector3(),\n                    v5: new THREE.Vector3(),\n                    v6: new THREE.Vector3(),\n                    v7: new THREE.Vector3(),\n                    v8: new THREE.Vector3(),\n                    v9: new THREE.Vector3(),\n                    v10: new THREE.Vector3(),\n                    v11: new THREE.Vector3(),\n                    v12: new THREE.Vector3(),\n                    q1: new THREE.Quaternion(),\n                    e1: new THREE.Euler(),\n                    wallHit: {},\n                    wallHitCandidate: {},\n                    zombieImpactPoint: { x: 0, z: 0 },\n                    zNormal: new THREE.Vector3(0, 0, 1)\n                };\n                const MAX_MIST_EFFECTS = perfProfile.maxMistEffects;\n                const MAX_BLOOD_FLOWS = perfProfile.maxBloodFlows;\n                const MAX_SEVERED_PARTS = perfProfile.maxSeveredParts;\n                const MAX_CORPSES = perfProfile.maxCorpses;\n                const activeMistEffects = [];\n                const activeSeveredParts = [];\n                const activeCorpses = [];\n                const activeImpactSparks = [];\n                const MAX_CAMERA_SHAKES = 8;\n                const activeCameraShakes = [];\n\n                // Corpse skeletons keep driving the existing ragdoll logic, but their visible\n                // meshes are rendered through one shared InstancedMesh per body part.\n                const {\n                    meshes: corpseInstanceMeshes,\n                    slotStride: corpsePartSlotStride,\n                    slotBias: corpsePartSlotBias,\n                    mergedSet: corpseMergedMeshSet\n                } = createZombieInstanceRenderer(MAX_CORPSES, 'corpse');\n                const corpseInstanceOccupied = new Uint8Array(MAX_CORPSES);\n                const corpseInstanceScratchColor = new THREE.Color();\n                let corpseInstanceMaxSlot = -1;\n                let corpseInstanceCountDirty = true;\n                let corpseInstanceMatrixDirty = false;\n                let corpseInstanceColorDirty = false;\n\n                function getCorpseInstanceActualSlot(key, slot) {\n                    return slot * (corpsePartSlotStride[key] || 1) + (corpsePartSlotBias[key] || 0);\n                }\n\n                function updateCorpseInstanceDrawCount() {\n                    if (!corpseInstanceCountDirty) return;\n                    const count = corpseInstanceMaxSlot + 1;\n                    const seen = new Set();\n                    for (let i = 0; i < zombieInstanceParts.length; i++) {\n                        const mesh = corpseInstanceMeshes[zombieInstanceParts[i]];\n                        if (!mesh || seen.has(mesh)) continue;\n                        seen.add(mesh);\n                        mesh.count = corpseMergedMeshSet.has(mesh) ? count * 2 : count;\n                    }\n                    corpseInstanceCountDirty = false;\n                }\n\n                function syncCorpseInstanceColors(slot, corpse) {\n                    const instanceParts = corpse && corpse.userData && corpse.userData.instanceParts;\n                    for (let i = 0; i < zombieInstanceParts.length; i++) {\n                        const key = zombieInstanceParts[i];\n                        const mesh = corpseInstanceMeshes[key];\n                        const source = instanceParts && instanceParts[key];\n                        const material = source && source.material;\n                        const color = material && material.color;\n                        corpseInstanceScratchColor.setHex(color ? color.getHex() : 0xffffff);\n                        mesh.setColorAt(getCorpseInstanceActualSlot(key, slot), corpseInstanceScratchColor);\n                    }\n                    corpseInstanceColorDirty = true;\n                }\n\n                function syncCorpseInstanceMatrices(slot, corpse) {\n                    if (!corpse) return;\n                    corpse.updateMatrixWorld(true);\n                    const instanceParts = corpse.userData && corpse.userData.instanceParts;\n                    for (let i = 0; i < zombieInstanceParts.length; i++) {\n                        const key = zombieInstanceParts[i];\n                        const source = instanceParts && instanceParts[key];\n                        let visible = !!source && source.visible && source.userData.logicalVisible !== false;\n                        let parent = source && source.parent;\n                        while (visible && parent && parent !== corpse) {\n                            visible = parent.visible && parent.userData.logicalVisible !== false;\n                            parent = parent.parent;\n                        }\n                        corpseInstanceMeshes[key].setMatrixAt(\n                            getCorpseInstanceActualSlot(key, slot),\n                            visible ? source.matrixWorld : zombieZeroMatrix\n                        );\n                    }\n                    corpseInstanceMatrixDirty = true;\n                }\n\n                function acquireCorpseInstanceSlot(slot, corpse) {\n                    corpseInstanceOccupied[slot] = 1;\n                    if (slot > corpseInstanceMaxSlot) {\n                        corpseInstanceMaxSlot = slot;\n                        corpseInstanceCountDirty = true;\n                    }\n                    syncCorpseInstanceColors(slot, corpse);\n                    syncCorpseInstanceMatrices(slot, corpse);\n                }\n\n                function releaseCorpseInstanceSlot(slot) {\n                    if (slot < 0 || !corpseInstanceOccupied[slot]) return;\n                    for (let i = 0; i < zombieInstanceParts.length; i++) {\n                        const key = zombieInstanceParts[i];\n                        corpseInstanceMeshes[key].setMatrixAt(getCorpseInstanceActualSlot(key, slot), zombieZeroMatrix);\n                    }\n                    corpseInstanceOccupied[slot] = 0;\n                    corpseInstanceMatrixDirty = true;\n                    if (slot === corpseInstanceMaxSlot) {\n                        while (corpseInstanceMaxSlot >= 0 && !corpseInstanceOccupied[corpseInstanceMaxSlot]) corpseInstanceMaxSlot--;\n                        corpseInstanceCountDirty = true;\n                    }\n                }\n\n                function flushCorpseInstanceUpdates() {\n                    updateCorpseInstanceDrawCount();\n                    const seen = new Set();\n                    for (let i = 0; i < zombieInstanceParts.length; i++) {\n                        const mesh = corpseInstanceMeshes[zombieInstanceParts[i]];\n                        if (!mesh || seen.has(mesh)) continue;\n                        seen.add(mesh);\n                        if (corpseInstanceMatrixDirty && mesh.count > 0) {\n                            mesh.instanceMatrix.updateRange.offset = 0;\n                            mesh.instanceMatrix.updateRange.count = mesh.count * 16;\n                            mesh.instanceMatrix.needsUpdate = true;\n                        }\n                        if (corpseInstanceColorDirty && mesh.instanceColor && mesh.count > 0) {\n                            mesh.instanceColor.updateRange.offset = 0;\n                            mesh.instanceColor.updateRange.count = mesh.count * 3;\n                            mesh.instanceColor.needsUpdate = true;\n                        }\n                    }\n                    corpseInstanceMatrixDirty = false;\n                    corpseInstanceColorDirty = false;\n                }\n\n                // ===== CONTIGUOUS FLAME PARTICLE STORAGE =====\n                const MAX_FLAME_PARTICLES = perfProfile.lowEnd ? 360 : 680;\n                const FLAME_DIRECT_SLOW = 0.22;\n                const FLAME_DIRECT_SLOW_TIME = 0.38;\n                const flameContactNextFxAt = new Float32Array(ZOMBIE_POOL_SIZE);\n                let nextFlameWallFxAt = 0;\n                const FLAME_STREAM_SLOT_COUNT = perfProfile.lowEnd ? 140 : 240;\n                const FLAME_COSMETIC_SLOT_START = FLAME_STREAM_SLOT_COUNT;\n                const flameActive = new Uint8Array(MAX_FLAME_PARTICLES);\n                const flameActiveIndex = new Int16Array(MAX_FLAME_PARTICLES);\n                const flameExpireAt = new Float32Array(MAX_FLAME_PARTICLES);\n                const activeFlameSlots = [];\n                const freeFlameStreamSlots = [];\n                const freeFlameCosmeticSlots = [];\n                flameActiveIndex.fill(-1);\n                for (let i = FLAME_STREAM_SLOT_COUNT - 1; i >= 0; i--) freeFlameStreamSlots.push(i);\n                for (let i = MAX_FLAME_PARTICLES - 1; i >= FLAME_COSMETIC_SLOT_START; i--) freeFlameCosmeticSlots.push(i);\n\n                function releaseFlameSlot(slot, returnToPool = true) {\n                    if (slot < 0 || !flameActive[slot]) return;\n                    flameActive[slot] = 0;\n                    flameExpireAt[slot] = 0;\n                    const activeIndex = flameActiveIndex[slot];\n                    if (activeIndex >= 0) {\n                        const lastSlot = activeFlameSlots.pop();\n                        if (activeIndex < activeFlameSlots.length) {\n                            activeFlameSlots[activeIndex] = lastSlot;\n                            flameActiveIndex[lastSlot] = activeIndex;\n                        }\n                        flameActiveIndex[slot] = -1;\n                    }\n                    if (returnToPool) {\n                        if (slot < FLAME_COSMETIC_SLOT_START) freeFlameStreamSlots.push(slot);\n                        else freeFlameCosmeticSlots.push(slot);\n                    }\n                }\n\n                function acquireFlameSlot(forWeaponStream) {\n                    const freeSlots = forWeaponStream ? freeFlameStreamSlots : freeFlameCosmeticSlots;\n                    if (freeSlots.length) return freeSlots.pop();\n                    if (!forWeaponStream) return -1;\n\n                    // The dedicated stream partition is deliberately larger than the\n                    // maximum visible jet. This fallback only runs after an extreme stall.\n                    let oldestSlot = -1;\n                    let oldestExpiry = Infinity;\n                    for (let i = 0; i < activeFlameSlots.length; i++) {\n                        const slot = activeFlameSlots[i];\n                        if (slot >= FLAME_COSMETIC_SLOT_START) continue;\n                        if (flameExpireAt[slot] < oldestExpiry) {\n                            oldestExpiry = flameExpireAt[slot];\n                            oldestSlot = slot;\n                        }\n                    }\n                    if (oldestSlot >= 0) releaseFlameSlot(oldestSlot, false);\n                    return oldestSlot;\n                }\n\n                // Create a soft fire tongue texture. Avoid bright uploaded sprites: additive\n                // blending turns white source images into a flat flashbang near the camera.\n                function createFlameTexture() {\n                    const canvas = document.createElement('canvas');\n                    canvas.width = 96;\n                    canvas.height = 96;\n                    const ctx = canvas.getContext('2d');\n                    ctx.clearRect(0, 0, 96, 96);\n                    const grad = ctx.createRadialGradient(48, 50, 2, 48, 50, 44);\n                    grad.addColorStop(0, 'rgba(255, 232, 145, 0.72)');\n                    grad.addColorStop(0.18, 'rgba(255, 160, 34, 0.68)');\n                    grad.addColorStop(0.45, 'rgba(255, 72, 10, 0.42)');\n                    grad.addColorStop(0.72, 'rgba(110, 24, 4, 0.16)');\n                    grad.addColorStop(1, 'rgba(0, 0, 0, 0.0)');\n                    ctx.fillStyle = grad;\n                    ctx.beginPath();\n                    ctx.ellipse(48, 50, 42, 34, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    const inner = ctx.createRadialGradient(44, 44, 0, 44, 44, 28);\n                    inner.addColorStop(0, 'rgba(255, 248, 190, 0.32)');\n                    inner.addColorStop(0.34, 'rgba(255, 190, 60, 0.20)');\n                    inner.addColorStop(1, 'rgba(255, 90, 0, 0)');\n                    ctx.fillStyle = inner;\n                    ctx.beginPath();\n                    ctx.ellipse(44, 44, 28, 20, -0.35, 0, Math.PI * 2);\n                    ctx.fill();\n                    return new THREE.CanvasTexture(canvas);\n                }\n                const flameTexture = createFlameTexture();\n\n                // GPU instanced billboard quads: CPU writes each particle once at spawn;\n                // shader handles expansion, color, alpha, rise, and screen-facing stretch.\n                const flameGeometry = new THREE.InstancedBufferGeometry();\n                const FLAME_BILLBOARD_SEGMENTS = 12;\n                const flameCornerValues = [0, 0.0208333];\n                const flameUvValues = [0.5, 0.5208333];\n                const flameIndices = [];\n                for (let i = 0; i < FLAME_BILLBOARD_SEGMENTS; i++) {\n                    const angle = i / FLAME_BILLBOARD_SEGMENTS * Math.PI * 2;\n                    const x = Math.cos(angle) * 0.4375;\n                    const y = 0.0208333 + Math.sin(angle) * 0.3541667;\n                    flameCornerValues.push(x, y);\n                    flameUvValues.push(0.5 + x, 0.5 + y);\n                    flameIndices.push(0, i + 1, ((i + 1) % FLAME_BILLBOARD_SEGMENTS) + 1);\n                }\n                flameGeometry.setIndex(flameIndices);\n                flameGeometry.setAttribute('corner', new THREE.BufferAttribute(new Float32Array(flameCornerValues), 2));\n                flameGeometry.setAttribute('flameUv', new THREE.BufferAttribute(new Float32Array(flameUvValues), 2));\n\n                // One interleaved instance buffer replaces seven independently uploaded\n                // buffers. Layout: origin+birth, velocity+life, size+kind+octNormal.\n                const FLAME_INSTANCE_STRIDE = 12;\n                const flameInstanceData = new Float32Array(MAX_FLAME_PARTICLES * FLAME_INSTANCE_STRIDE);\n                const flameInstanceBuffer = new THREE.InstancedInterleavedBuffer(flameInstanceData, FLAME_INSTANCE_STRIDE, 1);\n                if (THREE.DynamicDrawUsage) flameInstanceBuffer.setUsage(THREE.DynamicDrawUsage);\n                flameGeometry.setAttribute('instanceOriginBirth', new THREE.InterleavedBufferAttribute(flameInstanceBuffer, 4, 0));\n                flameGeometry.setAttribute('instanceVelocityLife', new THREE.InterleavedBufferAttribute(flameInstanceBuffer, 4, 4));\n                flameGeometry.setAttribute('instanceVisual', new THREE.InterleavedBufferAttribute(flameInstanceBuffer, 4, 8));\n                flameGeometry.instanceCount = MAX_FLAME_PARTICLES;\n                flameGeometry.maxInstancedCount = MAX_FLAME_PARTICLES;\n\n                let flameGpuTime = 0;\n                let flameGpuDirty = false;\n                let flameLightTargetX = 0;\n                let flameLightTargetY = 0;\n                let flameLightTargetZ = 0;\n\n                function writeFlameGpuParticle(i, x, y, z, vx, vy, vz, life, baseSize, kind, initialAge, surfaceNX, surfaceNY, surfaceNZ) {\n                    const visualAge = Math.max(0, initialAge || 0);\n                    const offset = i * FLAME_INSTANCE_STRIDE;\n                    flameInstanceData[offset] = x;\n                    flameInstanceData[offset + 1] = y;\n                    flameInstanceData[offset + 2] = z;\n                    flameInstanceData[offset + 3] = flameGpuTime - visualAge;\n                    flameInstanceData[offset + 4] = vx;\n                    flameInstanceData[offset + 5] = vy;\n                    flameInstanceData[offset + 6] = vz;\n                    flameInstanceData[offset + 7] = life;\n                    flameInstanceData[offset + 8] = baseSize;\n                    flameInstanceData[offset + 9] = kind;\n                    let nx = surfaceNX || 0;\n                    let ny = surfaceNY || 0;\n                    let nz = surfaceNZ || 0;\n                    const normalL1 = Math.abs(nx) + Math.abs(ny) + Math.abs(nz);\n                    if (normalL1 > 0.0001) {\n                        nx /= normalL1;\n                        ny /= normalL1;\n                        nz /= normalL1;\n                        if (nz < 0) {\n                            const oldX = nx;\n                            nx = (1 - Math.abs(ny)) * (oldX < 0 ? -1 : 1);\n                            ny = (1 - Math.abs(oldX)) * (ny < 0 ? -1 : 1);\n                        }\n                    }\n                    flameInstanceData[offset + 10] = nx;\n                    flameInstanceData[offset + 11] = ny;\n                    flameGpuDirty = true;\n                }\n\n                function flushFlameGpuUpdates() {\n                    if (!flameGpuDirty) return;\n                    flameInstanceBuffer.updateRange.offset = 0;\n                    flameInstanceBuffer.updateRange.count = flameInstanceData.length;\n                    flameInstanceBuffer.needsUpdate = true;\n                    flameGpuDirty = false;\n                }\n\n                function resetFlameRuntime() {\n                    activeFlameSlots.length = 0;\n                    freeFlameStreamSlots.length = 0;\n                    freeFlameCosmeticSlots.length = 0;\n                    flameActive.fill(0);\n                    flameActiveIndex.fill(-1);\n                    flameExpireAt.fill(0);\n                    for (let i = 0; i < MAX_FLAME_PARTICLES; i++) {\n                        const offset = i * FLAME_INSTANCE_STRIDE;\n                        flameInstanceData[offset + 3] = 0;\n                        flameInstanceData[offset + 7] = 0;\n                    }\n                    for (let i = FLAME_STREAM_SLOT_COUNT - 1; i >= 0; i--) freeFlameStreamSlots.push(i);\n                    for (let i = MAX_FLAME_PARTICLES - 1; i >= FLAME_COSMETIC_SLOT_START; i--) freeFlameCosmeticSlots.push(i);\n                    flameGpuTime = 0;\n                    flameLightTargetX = 0;\n                    flameLightTargetY = 0;\n                    flameLightTargetZ = 0;\n                    flameGpuDirty = true;\n                }\n\n                function seedFlameParticle(i, originX, originY, originZ, vx, vy, vz, life, damage, baseSize, kind, initialAge, surfaceNX, surfaceNY, surfaceNZ) {\n                    const age = Math.max(0, Math.min(initialAge || 0, Math.max(0, life - 0.02)));\n                    if (!flameActive[i]) {\n                        flameActive[i] = 1;\n                        flameActiveIndex[i] = activeFlameSlots.length;\n                        activeFlameSlots.push(i);\n                    }\n                    flameExpireAt[i] = flameGpuTime + Math.max(0, life - age);\n                    writeFlameGpuParticle(\n                        i, originX, originY, originZ, vx, vy, vz,\n                        life, baseSize, kind, age,\n                        surfaceNX, surfaceNY, surfaceNZ\n                    );\n                }\n\n                const flameShaderMaterial = new THREE.ShaderMaterial({\n                    uniforms: {\n                        pointTexture: { value: flameTexture },\n                        uTime: { value: 0 }\n                    },\n                    vertexShader: `\n                        attribute vec2 corner;\n                        attribute vec2 flameUv;\n                        attribute vec4 instanceOriginBirth;\n                        attribute vec4 instanceVelocityLife;\n                        attribute vec4 instanceVisual;\n                        uniform float uTime;\n                        varying vec2 vUv;\n                        varying vec3 vColor;\n                        varying float vOpacity;\n                        varying float vKind;\n                        vec3 decodeOctNormal(vec2 encoded) {\n                            vec3 normal = vec3(encoded.xy, 1.0 - abs(encoded.x) - abs(encoded.y));\n                            if (normal.z < 0.0) {\n                                normal.xy = (1.0 - abs(normal.yx)) * sign(normal.xy);\n                            }\n                            return normalize(normal);\n                        }\n                        void main() {\n                            vec3 instanceOrigin = instanceOriginBirth.xyz;\n                            float instanceBirth = instanceOriginBirth.w;\n                            vec3 instanceVelocity = instanceVelocityLife.xyz;\n                            float instanceLife = instanceVelocityLife.w;\n                            float instanceBaseSize = instanceVisual.x;\n                            float instanceKind = instanceVisual.y;\n                            float age = uTime - instanceBirth;\n                            float alive = step(0.0, age) * step(age, instanceLife);\n                            float lifeSafe = max(instanceLife, 0.0001);\n                            float t = clamp(age / lifeSafe, 0.0, 1.0);\n                            float surfaceMask = step(1.5, instanceKind);\n                            float dragCurve = age > 0.001 ? (1.0 - exp(-2.8 * age)) / (2.8 * age) : 1.0;\n                            float heatRise = (instanceKind > 0.5 ? 0.58 : 1.05) * age * age;\n                            vec3 streamPos = instanceOrigin + instanceVelocity * (age * dragCurve) + vec3(0.0, heatRise, 0.0);\n\n                            vec3 surfaceNormal = surfaceMask > 0.5\n                                ? decodeOctNormal(instanceVisual.zw)\n                                : vec3(0.0, 1.0, 0.0);\n                            vec3 tangentVelocity = instanceVelocity - surfaceNormal * dot(instanceVelocity, surfaceNormal);\n                            vec3 surfaceUp = vec3(0.0, 1.0, 0.0) - surfaceNormal * surfaceNormal.y;\n                            float surfaceTravel = age * dragCurve;\n                            vec3 surfacePos = instanceOrigin\n                                + tangentVelocity * surfaceTravel\n                                + surfaceUp * (0.86 * age * age)\n                                + surfaceNormal * (0.012 + 0.018 * t);\n                            vec3 animatedPos = mix(streamPos, surfacePos, surfaceMask);\n\n                            float turbulence = smoothstep(0.05, 0.85, t);\n                            vec3 streamTurbulence = vec3(\n                                sin(age * 18.0 + instanceBirth * 23.0 + instanceOrigin.z * 3.7) * 0.10,\n                                0.0,\n                                cos(age * 15.0 + instanceBirth * 19.0 + instanceOrigin.x * 4.1) * 0.08\n                            ) * turbulence;\n                            vec3 surfaceTurbulence = streamTurbulence\n                                - surfaceNormal * dot(streamTurbulence, surfaceNormal);\n                            animatedPos += mix(streamTurbulence, surfaceTurbulence * 0.68, surfaceMask);\n\n                            vec3 hot = vec3(1.0, 0.78, 0.24);\n                            vec3 mid = vec3(1.0, 0.28, 0.045);\n                            vec3 ember = vec3(0.34, 0.08, 0.025);\n                            vec3 smoke = vec3(0.16, 0.14, 0.13);\n                            vColor = mix(mix(hot, mid, smoothstep(0.12, 0.58, t)), ember, smoothstep(0.58, 0.92, t));\n                            float smokeMask = 1.0 - step(0.5, instanceKind);\n                            vColor = mix(vColor, smoke, smokeMask * smoothstep(0.38, 1.0, t));\n\n                            float fadeIn = 1.0;\n                            float fadeOut = 1.0 - smoothstep(0.20, 1.0, t);\n                            float opacityScale = mix(0.34, 0.82, step(0.5, instanceKind));\n                            opacityScale = mix(opacityScale, 0.64, surfaceMask);\n                            vOpacity = alive * fadeIn * fadeOut * opacityScale;\n                            vKind = instanceKind;\n                            vUv = flameUv;\n\n                            vec4 mvCenter = modelViewMatrix * vec4(animatedPos, 1.0);\n                            vec3 safeVelocity = instanceVelocity;\n                            float velocityLen = length(safeVelocity);\n                            safeVelocity = velocityLen > 0.0001 ? safeVelocity / velocityLen : vec3(0.0, 1.0, 0.0);\n                            vec3 viewVelocity3 = (modelViewMatrix * vec4(safeVelocity, 0.0)).xyz;\n                            vec2 along = normalize(viewVelocity3.xy + vec2(0.0001, 0.0001));\n                            vec2 across = vec2(-along.y, along.x);\n                            float streamExpand = mix(0.32, instanceKind > 0.5 ? 2.45 : 1.25, smoothstep(0.03, 1.0, t));\n                            float surfaceExpand = mix(0.54, 2.10, smoothstep(0.02, 0.88, t));\n                            float expand = mix(streamExpand, surfaceExpand, surfaceMask);\n                            float width = instanceBaseSize * expand;\n                            float streamStretch = instanceKind > 0.5 ? mix(1.75, 2.80, smoothstep(0.10, 0.85, t)) : 1.25;\n                            float surfaceStretch = mix(1.35, 2.25, smoothstep(0.04, 0.78, t));\n                            float stretch = mix(streamStretch, surfaceStretch, surfaceMask);\n                            vec2 viewOffset = across * corner.x * width + along * corner.y * width * stretch;\n                            mvCenter.xy += viewOffset * alive;\n                            gl_Position = projectionMatrix * mvCenter;\n                        }\n                    `,\n                    fragmentShader: `\n                        uniform sampler2D pointTexture;\n                        varying vec2 vUv;\n                        varying vec3 vColor;\n                        varying float vOpacity;\n                        varying float vKind;\n                        void main() {\n                            vec4 tex = texture2D(pointTexture, vUv);\n                            float alpha = tex.a * vOpacity;\n                            if (alpha <= 0.003) discard;\n                            vec3 color = vColor * mix(vec3(0.65), max(tex.rgb, vec3(0.12)), clamp(0.55 + vKind * 0.25, 0.0, 1.0));\n                            gl_FragColor = vec4(color, alpha);\n                        }\n                    `,\n                    transparent: true,\n                    depthWrite: false,\n                    blending: THREE.NormalBlending,\n                    side: THREE.DoubleSide,\n                    precision: 'mediump'\n                });\n\n                const flameMesh = new THREE.Mesh(flameGeometry, flameShaderMaterial);\n                flameMesh.frustumCulled = false;\n                scene.add(flameMesh);\n\n                function getFlameSurfaceFrame(nx, ny, nz, inX, inY, inZ, phase, out) {\n                    let nLen = Math.sqrt(nx * nx + ny * ny + nz * nz);\n                    if (nLen < 0.0001) { nx = 0; ny = 1; nz = 0; nLen = 1; }\n                    nx /= nLen; ny /= nLen; nz /= nLen;\n\n                    let iLen = Math.sqrt(inX * inX + inY * inY + inZ * inZ);\n                    if (iLen < 0.0001) { inX = -nx; inY = -ny; inZ = -nz; iLen = 1; }\n                    inX /= iLen; inY /= iLen; inZ /= iLen;\n\n                    let tx = inX - nx * (inX * nx + inY * ny + inZ * nz);\n                    let ty = inY - ny * (inX * nx + inY * ny + inZ * nz);\n                    let tz = inZ - nz * (inX * nx + inY * ny + inZ * nz);\n                    let tLen = Math.sqrt(tx * tx + ty * ty + tz * tz);\n                    if (tLen < 0.08) {\n                        if (Math.abs(ny) < 0.82) {\n                            tx = -nz; ty = 0; tz = nx;\n                        } else {\n                            tx = 1; ty = 0; tz = 0;\n                        }\n                        tLen = Math.sqrt(tx * tx + ty * ty + tz * tz) || 1;\n                    }\n                    tx /= tLen; ty /= tLen; tz /= tLen;\n\n                    let sx = ny * tz - nz * ty;\n                    let sy = nz * tx - nx * tz;\n                    let sz = nx * ty - ny * tx;\n                    const angle = phase || 0;\n                    const ca = Math.cos(angle);\n                    const sa = Math.sin(angle);\n                    out.nx = nx; out.ny = ny; out.nz = nz;\n                    out.tx = tx * ca + sx * sa;\n                    out.ty = ty * ca + sy * sa;\n                    out.tz = tz * ca + sz * sa;\n                    out.sx = ny * out.tz - nz * out.ty;\n                    out.sy = nz * out.tx - nx * out.tz;\n                    out.sz = nx * out.ty - ny * out.tx;\n                    return out;\n                }\n\n                const flameSurfaceFrame = {};\n\n                function spawnFlameSurfaceBurst(x, y, z, nx, ny, nz, inX, inY, inZ, intensity) {\n                    intensity = Math.max(0.4, intensity || 1);\n                    const budgetPoint = effectScratch.v8.set(x, y, z);\n                    const pressure = getPerfPressureLevel();\n                    if ((pressure >= 3 && Math.random() < 0.35) || !shouldSpawnBudgetedEffect('flame-impact', intensity > 0.9 ? 2 : 1, budgetPoint)) return;\n                    const frame = getFlameSurfaceFrame(nx, ny, nz, inX, inY, inZ, 0, flameSurfaceFrame);\n                    const inputLength = Math.sqrt(inX * inX + inY * inY + inZ * inZ) || 1;\n                    const incidence = Math.min(1, Math.abs((inX * frame.nx + inY * frame.ny + inZ * frame.nz) / inputLength));\n                    const fanSpan = 0.9 + incidence * (Math.PI * 1.45 - 0.9);\n                    const count = perfProfile.lowEnd ? 3 : (pressure >= 2 ? 3 : 5);\n                    for (let p = 0; p < count; p++) {\n                        const slot = acquireFlameSlot(false);\n                        if (slot < 0) break;\n                        const fan = count <= 1 ? 0 : (p / (count - 1) - 0.5) * fanSpan;\n                        const jitter = (Math.random() - 0.5) * 0.28;\n                        const angle = fan + jitter;\n                        const ca = Math.cos(angle);\n                        const sa = Math.sin(angle);\n                        const flowX = frame.tx * ca + frame.sx * sa;\n                        const flowY = frame.ty * ca + frame.sy * sa;\n                        const flowZ = frame.tz * ca + frame.sz * sa;\n                        const flowSpeed = (0.72 + Math.random() * 1.45) * intensity;\n                        const jitterT = (Math.random() - 0.5) * 0.14;\n                        const jitterS = (Math.random() - 0.5) * 0.14;\n                        seedFlameParticle(\n                            slot,\n                            x + frame.nx * 0.03 + frame.tx * jitterT + frame.sx * jitterS,\n                            y + frame.ny * 0.03 + frame.ty * jitterT + frame.sy * jitterS,\n                            z + frame.nz * 0.03 + frame.tz * jitterT + frame.sz * jitterS,\n                            flowX * flowSpeed,\n                            flowY * flowSpeed,\n                            flowZ * flowSpeed,\n                            0.36 + Math.random() * 0.28,\n                            0,\n                            0.14 + Math.random() * 0.11,\n                            2.0,\n                            Math.random() * 0.045,\n                            frame.nx, frame.ny, frame.nz\n                        );\n                    }\n                }\n\n                const runtimeTimers = new Set();\n                let hitMarkerTimer = null;\n                let damageOverlayTimer = null;\n                let hitMarkerPulseFlip = false;\n                const MAX_MIST_POINTS = 24;\n                const CORPSE_FLOOR_Y = 0.05;\n                const CORPSE_GRAVITY = 18;\n                const CORPSE_DRAG_BASE = 0.88;\n                const CORPSE_FLAT_ANGLE = Math.PI / 2;\n                const CORPSE_MIN_LEAN_ANGLE = 0.78;\n                const CORPSE_FORCE_LEAN_AGE = 0.38;\n                const CORPSE_FORCE_FLAT_AGE = 1.75;\n                const CORPSE_BOSS_FORCE_FLAT_AGE = 1.45;\n                const CORPSE_HARD_LAND_GRACE = 0.65;\n                const corpseRuntime = {\n                    activeSlots: activeCorpses,\n                    freeSlots: [],\n                    activeIndex: new Int16Array(MAX_CORPSES),\n                    mesh: new Array(MAX_CORPSES),\n                    vx: new Float32Array(MAX_CORPSES),\n                    vy: new Float32Array(MAX_CORPSES),\n                    vz: new Float32Array(MAX_CORPSES),\n                    rx: new Float32Array(MAX_CORPSES),\n                    ry: new Float32Array(MAX_CORPSES),\n                    rz: new Float32Array(MAX_CORPSES),\n                    rvx: new Float32Array(MAX_CORPSES),\n                    rvy: new Float32Array(MAX_CORPSES),\n                    rvz: new Float32Array(MAX_CORPSES),\n                    // Limb joint rotations and velocities\n                    jointHeadX: new Float32Array(MAX_CORPSES),\n                    jointHeadY: new Float32Array(MAX_CORPSES),\n                    jointHeadZ: new Float32Array(MAX_CORPSES),\n                    jointHeadVX: new Float32Array(MAX_CORPSES),\n                    jointHeadVY: new Float32Array(MAX_CORPSES),\n                    jointHeadVZ: new Float32Array(MAX_CORPSES),\n\n                    jointLArmX: new Float32Array(MAX_CORPSES),\n                    jointLArmZ: new Float32Array(MAX_CORPSES),\n                    jointLArmVX: new Float32Array(MAX_CORPSES),\n                    jointLArmVZ: new Float32Array(MAX_CORPSES),\n\n                    jointRArmX: new Float32Array(MAX_CORPSES),\n                    jointRArmZ: new Float32Array(MAX_CORPSES),\n                    jointRArmVX: new Float32Array(MAX_CORPSES),\n                    jointRArmVZ: new Float32Array(MAX_CORPSES),\n\n                    jointLLegX: new Float32Array(MAX_CORPSES),\n                    jointLLegVX: new Float32Array(MAX_CORPSES),\n                    jointRLegX: new Float32Array(MAX_CORPSES),\n                    jointRLegVX: new Float32Array(MAX_CORPSES),\n\n                    ndx: new Float32Array(MAX_CORPSES),\n                    ndz: new Float32Array(MAX_CORPSES),\n                    fallAngle: new Float32Array(MAX_CORPSES),\n                    fallDirSign: new Float32Array(MAX_CORPSES), // 1 for falling back, -1 for forward\n                    fallSpeed: new Float32Array(MAX_CORPSES),\n                    fallAccel: new Float32Array(MAX_CORPSES),\n                    forceFlatAge: new Float32Array(MAX_CORPSES),\n                    age: new Float32Array(MAX_CORPSES),\n                    sinkAge: new Float32Array(MAX_CORPSES),\n                    bleedTimer: new Float32Array(MAX_CORPSES),\n                    spinSpeed: new Float32Array(MAX_CORPSES),\n                    spinX: new Float32Array(MAX_CORPSES),\n                    active: new Uint8Array(MAX_CORPSES),\n                    landed: new Uint8Array(MAX_CORPSES),\n                    bounceCount: new Uint8Array(MAX_CORPSES),\n                    bleedBursts: new Uint8Array(MAX_CORPSES),\n                    isBurnt: new Uint8Array(MAX_CORPSES)\n                };\n                corpseRuntime.activeIndex.fill(-1);\n                for (let i = MAX_CORPSES - 1; i >= 0; i--) {\n                    corpseRuntime.freeSlots.push(i);\n                    corpseRuntime.isBurnt[i] = 0;\n                }\n                const cameraShakeRuntime = {\n                    activeSlots: activeCameraShakes,\n                    freeSlots: [],\n                    activeIndex: new Int16Array(MAX_CAMERA_SHAKES),\n                    elapsed: new Float32Array(MAX_CAMERA_SHAKES),\n                    duration: new Float32Array(MAX_CAMERA_SHAKES),\n                    intensity: new Float32Array(MAX_CAMERA_SHAKES),\n                    active: new Uint8Array(MAX_CAMERA_SHAKES)\n                };\n                cameraShakeRuntime.activeIndex.fill(-1);\n                for (let i = MAX_CAMERA_SHAKES - 1; i >= 0; i--) cameraShakeRuntime.freeSlots.push(i);\n                let cameraShakeTargetX = 0;\n                let cameraShakeTargetY = 0;\n                let cameraShakeOffsetX = 0;\n                let cameraShakeOffsetY = 0;\n                let cameraShakePhase = 0;\n                let cameraShakeNoiseX = 0;\n                let cameraShakeNoiseY = 0;\n\n                function findWeakestCameraShakeSlot() {\n                    let weakestSlot = -1;\n                    let weakestRemaining = Infinity;\n                    for (let i = 0; i < cameraShakeRuntime.activeSlots.length; i++) {\n                        const slot = cameraShakeRuntime.activeSlots[i];\n                        const remaining = cameraShakeRuntime.duration[slot] - cameraShakeRuntime.elapsed[slot];\n                        if (remaining < weakestRemaining) {\n                            weakestRemaining = remaining;\n                            weakestSlot = slot;\n                        }\n                    }\n                    return weakestSlot;\n                }\n\n                function releaseCameraShakeSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0 || !cameraShakeRuntime.active[slot]) return;\n                    cameraShakeRuntime.active[slot] = 0;\n                    const idx = cameraShakeRuntime.activeIndex[slot];\n                    if (idx >= 0) {\n                        const last = cameraShakeRuntime.activeSlots.pop();\n                        if (idx < cameraShakeRuntime.activeSlots.length) {\n                            cameraShakeRuntime.activeSlots[idx] = last;\n                            cameraShakeRuntime.activeIndex[last] = idx;\n                        }\n                        cameraShakeRuntime.activeIndex[slot] = -1;\n                    }\n                    cameraShakeRuntime.freeSlots.push(slot);\n                }\n\n                function triggerCameraShake(intensity, duration) {\n                    intensity = Math.max(0, Number(intensity) || 0) * getCameraShakeIntensityScale();\n                    duration = Math.max(0.001, (Number(duration) || 0.001) * getCameraShakeDurationScale());\n                    if (intensity <= 0.0005) return;\n                    if (!cameraShakeRuntime.freeSlots.length) {\n                        const weakestSlot = findWeakestCameraShakeSlot();\n                        if (weakestSlot >= 0) releaseCameraShakeSlot(weakestSlot);\n                    }\n                    const slot = cameraShakeRuntime.freeSlots.pop();\n                    if (slot === undefined) return;\n                    cameraShakeRuntime.active[slot] = 1;\n                    cameraShakeRuntime.activeIndex[slot] = cameraShakeRuntime.activeSlots.length;\n                    cameraShakeRuntime.activeSlots.push(slot);\n                    cameraShakeRuntime.elapsed[slot] = 0;\n                    cameraShakeRuntime.duration[slot] = duration;\n                    cameraShakeRuntime.intensity[slot] = intensity;\n                }\n\n                function updateCameraShakeRuntime(dt) {\n                    if (cameraShakeRuntime.activeSlots.length === 0) {\n                        cameraShakeTargetX = 0;\n                        cameraShakeTargetY = 0;\n                        const settle = expSmoothingFactor(dt, 18);\n                        cameraShakeOffsetX += (0 - cameraShakeOffsetX) * settle;\n                        cameraShakeOffsetY += (0 - cameraShakeOffsetY) * settle;\n                        cameraShakeNoiseX += (0 - cameraShakeNoiseX) * settle;\n                        cameraShakeNoiseY += (0 - cameraShakeNoiseY) * settle;\n                        if (Math.abs(cameraShakeOffsetX) < 0.0002) cameraShakeOffsetX = 0;\n                        if (Math.abs(cameraShakeOffsetY) < 0.0002) cameraShakeOffsetY = 0;\n                        return;\n                    }\n                    const step = Math.min(dt, 0.05);\n                    let strengthSum = 0;\n                    const activeCount = Math.max(1, cameraShakeRuntime.activeSlots.length);\n                    const stackDamping = 1 / Math.sqrt(activeCount);\n                    for (let i = cameraShakeRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        const slot = cameraShakeRuntime.activeSlots[i];\n                        cameraShakeRuntime.elapsed[slot] += step;\n                        const t = cameraShakeRuntime.elapsed[slot] / cameraShakeRuntime.duration[slot];\n                        const decay = 1 - smooth01(t);\n                        if (decay <= 0) {\n                            releaseCameraShakeSlot(slot);\n                            continue;\n                        }\n                        const strength = cameraShakeRuntime.intensity[slot] * decay * stackDamping;\n                        strengthSum += strength;\n                    }\n                    cameraShakePhase += step * (visualIntensityMode === 'soft' ? 7.5 : 11.5);\n                    const noiseFollow = expSmoothingFactor(dt, visualIntensityMode === 'soft' ? 5 : 7.5);\n                    cameraShakeNoiseX += ((Math.random() - 0.5) - cameraShakeNoiseX) * noiseFollow;\n                    cameraShakeNoiseY += ((Math.random() - 0.5) - cameraShakeNoiseY) * noiseFollow;\n                    const offsetX = (Math.sin(cameraShakePhase * 1.15) * 0.58 + cameraShakeNoiseX * 0.42) * strengthSum;\n                    const offsetY = (Math.sin(cameraShakePhase * 1.53 + 1.7) * 0.50 + cameraShakeNoiseY * 0.38) * strengthSum;\n                    const maxShakeX = 0.065 * getCameraShakeMaxScale();\n                    const maxShakeY = 0.052 * getCameraShakeMaxScale();\n                    cameraShakeTargetX = Math.max(-maxShakeX, Math.min(maxShakeX, offsetX));\n                    cameraShakeTargetY = Math.max(-maxShakeY, Math.min(maxShakeY, offsetY));\n                    const follow = expSmoothingFactor(dt, getVisualShakeFollowSpeed());\n                    cameraShakeOffsetX += (cameraShakeTargetX - cameraShakeOffsetX) * follow;\n                    cameraShakeOffsetY += (cameraShakeTargetY - cameraShakeOffsetY) * follow;\n                }\n\n                function resetCameraShakeRuntime() {\n                    for (let i = cameraShakeRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        releaseCameraShakeSlot(cameraShakeRuntime.activeSlots[i]);\n                    }\n                    cameraShakeTargetX = 0;\n                    cameraShakeTargetY = 0;\n                    cameraShakeOffsetX = 0;\n                    cameraShakeOffsetY = 0;\n                    cameraShakeNoiseX = 0;\n                    cameraShakeNoiseY = 0;\n                }\n\n                function createBloodFlowTexture() {\n                    const size = 128;\n                    const texCanvas = document.createElement('canvas');\n                    texCanvas.width = size;\n                    texCanvas.height = size;\n                    const ctx = texCanvas.getContext('2d');\n                    ctx.clearRect(0, 0, size, size);\n\n                    function softBlob(cx, cy, rx, ry, rot, alpha) {\n                        ctx.save();\n                        ctx.translate(cx, cy);\n                        ctx.rotate(rot);\n                        ctx.scale(rx, ry);\n                        const g = ctx.createRadialGradient(0, 0, 0.08, 0, 0, 1);\n                        g.addColorStop(0, `rgba(255,255,255,${alpha})`);\n                        g.addColorStop(0.55, `rgba(255,255,255,${alpha * 0.74})`);\n                        g.addColorStop(0.82, `rgba(255,255,255,${alpha * 0.28})`);\n                        g.addColorStop(1, 'rgba(255,255,255,0)');\n                        ctx.fillStyle = g;\n                        ctx.beginPath();\n                        ctx.arc(0, 0, 1, 0, Math.PI * 2);\n                        ctx.fill();\n                        ctx.restore();\n                    }\n\n                    function hardCore(cx, cy, rx, ry, rot, alpha) {\n                        ctx.save();\n                        ctx.translate(cx, cy);\n                        ctx.rotate(rot);\n                        ctx.scale(rx, ry);\n                        ctx.beginPath();\n                        for (let i = 0; i < 22; i++) {\n                            const a = (i / 22) * Math.PI * 2;\n                            const wobble = 1 + Math.sin(i * 2.13) * 0.11 + Math.cos(i * 1.71) * 0.07;\n                            const x = Math.cos(a) * wobble;\n                            const y = Math.sin(a) * wobble;\n                            if (i === 0) ctx.moveTo(x, y);\n                            else ctx.lineTo(x, y);\n                        }\n                        ctx.closePath();\n                        ctx.fillStyle = `rgba(255,255,255,${alpha})`;\n                        ctx.fill();\n                        ctx.restore();\n                    }\n\n                    ctx.globalCompositeOperation = 'source-over';\n                    softBlob(57, 65, 42, 24, 0.08, 0.78);\n                    softBlob(75, 60, 31, 18, -0.22, 0.58);\n                    softBlob(43, 72, 28, 17, 0.36, 0.42);\n                    softBlob(90, 55, 35, 9, -0.12, 0.38);\n                    softBlob(104, 51, 24, 6, -0.08, 0.24);\n                    hardCore(58, 66, 27, 15, 0.06, 0.20);\n\n                    const texture = new THREE.CanvasTexture(texCanvas);\n                    texture.minFilter = THREE.LinearFilter;\n                    texture.magFilter = THREE.LinearFilter;\n                    texture.generateMipmaps = false;\n                    return texture;\n                }\n\n                const bloodFlowGeo = new THREE.PlaneGeometry(1, 1);\n                const bloodFlowAlphaValues = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowAlphaAttr = new THREE.InstancedBufferAttribute(bloodFlowAlphaValues, 1);\n                bloodFlowGeo.setAttribute('instanceAlpha', bloodFlowAlphaAttr);\n                const bloodFlowMat = new THREE.MeshBasicMaterial({\n                    map: createBloodFlowTexture(),\n                    color: 0xffffff,\n                    vertexColors: true,\n                    transparent: true,\n                    opacity: 1.0,\n                    alphaTest: 0.02,\n                    depthWrite: false,\n                    side: THREE.DoubleSide,\n                    polygonOffset: true,\n                    polygonOffsetFactor: -4,\n                    polygonOffsetUnits: -4\n                });\n                bloodFlowMat.onBeforeCompile = (shader) => {\n                    shader.vertexShader = shader.vertexShader\n                        .replace('#include <common>', '#include <common>\\nattribute float instanceAlpha;\\nvarying float vInstanceAlpha;')\n                        .replace('#include <begin_vertex>', '#include <begin_vertex>\\nvInstanceAlpha = instanceAlpha;');\n                    shader.fragmentShader = shader.fragmentShader\n                        .replace('#include <common>', '#include <common>\\nvarying float vInstanceAlpha;')\n                        .replace('#include <premultiplied_alpha_fragment>', 'gl_FragColor.a *= vInstanceAlpha;\\n#include <premultiplied_alpha_fragment>');\n                };\n                const bloodFlowInstanceMesh = new THREE.InstancedMesh(bloodFlowGeo, bloodFlowMat, MAX_BLOOD_FLOWS);\n                bloodFlowInstanceMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);\n                bloodFlowInstanceMesh.frustumCulled = false;\n                bloodFlowInstanceMesh.count = 0;\n                const bloodFlowZeroMatrix = new THREE.Matrix4().makeScale(0, 0, 0);\n                const bloodFlowMatrix = new THREE.Matrix4();\n                const bloodFlowPos = new THREE.Vector3();\n                const bloodFlowQuat = new THREE.Quaternion();\n                const bloodFlowEuler = new THREE.Euler();\n                const bloodFlowScale = new THREE.Vector3();\n                const bloodFlowColor = new THREE.Color();\n                const bloodFlowSlotActive = new Uint8Array(MAX_BLOOD_FLOWS);\n                const bloodFlowX = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowZ = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowDirX = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowDirZ = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowRoll = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowAge = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowDuration = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowBaseScaleX = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowBaseScaleY = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowTargetScaleX = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowTargetScaleY = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowSpreadSpeed = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowSlideDistance = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowOpacity = new Float32Array(MAX_BLOOD_FLOWS);\n                const bloodFlowSettled = new Uint8Array(MAX_BLOOD_FLOWS);\n                const bloodFlowActiveIndex = new Int16Array(MAX_BLOOD_FLOWS);\n                const activeBloodFlowSlots = [];\n                const freeBloodFlowSlots = [];\n                let bloodFlowMaxActiveSlot = -1;\n                let bloodFlowMatrixDirty = false;\n                let bloodFlowColorDirty = false;\n                let bloodFlowAlphaDirty = false;\n                let bloodFlowCountDirty = true;\n                for (let i = 0; i < MAX_BLOOD_FLOWS; i++) {\n                    bloodFlowInstanceMesh.setMatrixAt(i, bloodFlowZeroMatrix);\n                    bloodFlowInstanceMesh.setColorAt(i, bloodFlowColor.setRGB(1.0, 0.025, 0.012));\n                    bloodFlowAlphaValues[i] = 0;\n                    freeBloodFlowSlots.push(MAX_BLOOD_FLOWS - 1 - i);\n                }\n                bloodFlowInstanceMesh.instanceMatrix.needsUpdate = true;\n                if (bloodFlowInstanceMesh.instanceColor) bloodFlowInstanceMesh.instanceColor.needsUpdate = true;\n                bloodFlowAlphaAttr.needsUpdate = true;\n                bloodFlowActiveIndex.fill(-1);\n                scene.add(bloodFlowInstanceMesh);\n\n                function scheduleRuntimeTimeout(fn, delay) {\n                    const id = setTimeout(() => {\n                        runtimeTimers.delete(id);\n                        fn();\n                    }, delay);\n                    runtimeTimers.add(id);\n                    return id;\n                }\n\n                function restartClassNextFrame(el, className) {\n                    if (!el) return;\n                    el.classList.remove(className);\n                    requestAnimationFrame(() => {\n                        if (el.isConnected) el.classList.add(className);\n                    });\n                }\n\n                function restartActiveClassNextFrame(el) {\n                    restartClassNextFrame(el, 'active');\n                }\n\n                function clearRuntimeTimeout(id) {\n                    if (!id) return;\n                    clearTimeout(id);\n                    runtimeTimers.delete(id);\n                }\n\n                function clearRuntimeTimers() {\n                    for (const id of runtimeTimers) clearTimeout(id);\n                    runtimeTimers.clear();\n                    hitMarkerTimer = null;\n                    damageOverlayTimer = null;\n                }\n\n                const MAX_BLOOD_MIST_PARTICLES = MAX_MIST_EFFECTS * MAX_MIST_POINTS;\n                const bloodMistGeo = new THREE.BufferGeometry();\n                const bloodMistPosition = new Float32Array(MAX_BLOOD_MIST_PARTICLES * 3);\n                const bloodMistOrigin = new Float32Array(MAX_BLOOD_MIST_PARTICLES * 3);\n                const bloodMistVelocity = new Float32Array(MAX_BLOOD_MIST_PARTICLES * 3);\n                const bloodMistBirth = new Float32Array(MAX_BLOOD_MIST_PARTICLES);\n                const bloodMistLife = new Float32Array(MAX_BLOOD_MIST_PARTICLES);\n                const bloodMistBaseSize = new Float32Array(MAX_BLOOD_MIST_PARTICLES);\n                const bloodMistOriginAttr = new THREE.BufferAttribute(bloodMistOrigin, 3);\n                const bloodMistVelocityAttr = new THREE.BufferAttribute(bloodMistVelocity, 3);\n                const bloodMistBirthAttr = new THREE.BufferAttribute(bloodMistBirth, 1);\n                const bloodMistLifeAttr = new THREE.BufferAttribute(bloodMistLife, 1);\n                const bloodMistBaseSizeAttr = new THREE.BufferAttribute(bloodMistBaseSize, 1);\n                bloodMistOriginAttr.setUsage(THREE.DynamicDrawUsage);\n                bloodMistVelocityAttr.setUsage(THREE.DynamicDrawUsage);\n                bloodMistBirthAttr.setUsage(THREE.DynamicDrawUsage);\n                bloodMistLifeAttr.setUsage(THREE.DynamicDrawUsage);\n                bloodMistBaseSizeAttr.setUsage(THREE.DynamicDrawUsage);\n                bloodMistGeo.setAttribute('position', new THREE.BufferAttribute(bloodMistPosition, 3));\n                bloodMistGeo.setAttribute('mistOrigin', bloodMistOriginAttr);\n                bloodMistGeo.setAttribute('mistVelocity', bloodMistVelocityAttr);\n                bloodMistGeo.setAttribute('mistBirth', bloodMistBirthAttr);\n                bloodMistGeo.setAttribute('mistLife', bloodMistLifeAttr);\n                bloodMistGeo.setAttribute('mistBaseSize', bloodMistBaseSizeAttr);\n                bloodMistGeo.setDrawRange(0, MAX_BLOOD_MIST_PARTICLES);\n                let bloodMistTime = 0;\n                let bloodMistGpuDirty = false;\n                const bloodMistMat = new THREE.PointsMaterial({\n                    color: 0xffffff,\n                    size: 1,\n                    transparent: true,\n                    opacity: 1,\n                    depthWrite: false,\n                    alphaTest: 0.005,\n                    sizeAttenuation: true\n                });\n                bloodMistMat.onBeforeCompile = (shader) => {\n                    shader.uniforms.uBloodMistTime = { value: 0 };\n                    bloodMistMat.userData.shader = shader;\n                    shader.vertexShader = shader.vertexShader\n                        .replace('#include <common>', `#include <common>\nattribute vec3 mistOrigin;\nattribute vec3 mistVelocity;\nattribute float mistBirth;\nattribute float mistLife;\nattribute float mistBaseSize;\nuniform float uBloodMistTime;\nvarying float vMistOpacity;\nvarying float vMistT;`)\n                        .replace('#include <begin_vertex>', `\nfloat mistAge = uBloodMistTime - mistBirth;\nfloat mistAlive = step(0.0, mistAge) * step(mistAge, mistLife);\nvMistT = clamp(mistAge / max(mistLife, 0.0001), 0.0, 1.0);\nfloat drag = (1.0 - exp(-3.8 * mistAge)) / 3.8;\nvec3 transformed = mistOrigin + mistVelocity * drag + vec3(0.0, -1.3 * mistAge * mistAge, 0.0);\nvMistOpacity = mistAlive * sin(vMistT * 3.14159) * 0.95;\n`)\n                        .replace('gl_PointSize = size;', 'gl_PointSize = mistBaseSize * (1.0 + vMistT * 9.5) * mistAlive;');\n                    shader.fragmentShader = shader.fragmentShader\n                        .replace('#include <common>', `#include <common>\nvarying float vMistOpacity;\nvarying float vMistT;`)\n                        .replace('#include <premultiplied_alpha_fragment>', `\nvec2 mistUv = gl_PointCoord - vec2(0.5);\nfloat angle = atan(mistUv.y, mistUv.x);\nfloat noise = sin(angle * 5.0) * 0.12 + cos(angle * 3.0 + vMistT * 6.28) * 0.08 + sin(angle * 9.0) * 0.05;\nfloat mistRadius = length(mistUv) + noise * (1.0 - vMistT * 0.5);\nfloat mistSoftEdge = 1.0 - smoothstep(0.12, 0.5, mistRadius);\nfloat mistCore = 1.0 - smoothstep(0.0, 0.32, mistRadius);\nvec3 freshColor = vec3(0.85, 0.02, 0.04);\nvec3 driedColor = vec3(0.24, 0.01, 0.012);\nvec3 bloodColor = mix(freshColor, driedColor, vMistT);\ngl_FragColor.rgb = bloodColor * (0.58 + mistCore * 0.65);\ngl_FragColor.a = vMistOpacity * mistSoftEdge;\nif (gl_FragColor.a < 0.01) discard;\n#include <premultiplied_alpha_fragment>`);\n                };\n                bloodMistMat.customProgramCacheKey = () => 'blood-mist-gpu-pool-v2';\n                const bloodMistPoints = new THREE.Points(bloodMistGeo, bloodMistMat);\n                bloodMistPoints.frustumCulled = false;\n                scene.add(bloodMistPoints);\n                const bloodMistRuntime = {\n                    activeSlots: activeMistEffects,\n                    freeSlots: [],\n                    activeIndex: new Int16Array(MAX_MIST_EFFECTS),\n                    age: new Float32Array(MAX_MIST_EFFECTS),\n                    count: new Uint8Array(MAX_MIST_EFFECTS),\n                    active: new Uint8Array(MAX_MIST_EFFECTS)\n                };\n                bloodMistRuntime.activeIndex.fill(-1);\n                for (let i = MAX_MIST_EFFECTS - 1; i >= 0; i--) bloodMistRuntime.freeSlots.push(i);\n\n                function removeArrayItemFast(list, item) {\n                    const idx = list.indexOf(item);\n                    if (idx < 0) return false;\n                    const last = list.pop();\n                    if (idx < list.length) list[idx] = last;\n                    return true;\n                }\n\n                function trimBloodFlows(reserve = 1) {\n                    const pressure = getPerfPressureLevel();\n                    const dynamicLimit = pressure >= 3 ? Math.max(12, Math.floor(MAX_BLOOD_FLOWS * 0.42))\n                        : (pressure >= 2 ? Math.max(18, Math.floor(MAX_BLOOD_FLOWS * 0.62)) : MAX_BLOOD_FLOWS);\n                    while (activeBloodFlowSlots.length + reserve > dynamicLimit) {\n                        let oldestSlot = -1;\n                        let oldestAge = -Infinity;\n                        for (let i = 0; i < activeBloodFlowSlots.length; i++) {\n                            const slot = activeBloodFlowSlots[i];\n                            if (bloodFlowAge[slot] > oldestAge) {\n                                oldestAge = bloodFlowAge[slot];\n                                oldestSlot = slot;\n                            }\n                        }\n                        if (oldestSlot < 0) break;\n                        releaseBloodFlowSlot(oldestSlot);\n                    }\n                }\n\n                function acquireBloodFlowSlot() {\n                    trimBloodFlows(1);\n                    const slot = freeBloodFlowSlots.pop();\n                    if (slot === undefined) return -1;\n                    bloodFlowSlotActive[slot] = 1;\n                    bloodFlowActiveIndex[slot] = activeBloodFlowSlots.length;\n                    activeBloodFlowSlots.push(slot);\n                    if (slot > bloodFlowMaxActiveSlot) {\n                        bloodFlowMaxActiveSlot = slot;\n                        bloodFlowCountDirty = true;\n                    }\n                    return slot;\n                }\n\n                function releaseBloodFlowSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0 || !bloodFlowSlotActive[slot]) return;\n                    bloodFlowSlotActive[slot] = 0;\n                    bloodFlowInstanceMesh.setMatrixAt(slot, bloodFlowZeroMatrix);\n                    bloodFlowAlphaValues[slot] = 0;\n                    bloodFlowOpacity[slot] = 0;\n                    bloodFlowAge[slot] = 0;\n                    bloodFlowSettled[slot] = 0;\n                    bloodFlowMatrixDirty = true;\n                    bloodFlowAlphaDirty = true;\n                    if (slot === bloodFlowMaxActiveSlot) {\n                        while (bloodFlowMaxActiveSlot >= 0 && !bloodFlowSlotActive[bloodFlowMaxActiveSlot]) bloodFlowMaxActiveSlot--;\n                        bloodFlowCountDirty = true;\n                    }\n                    const idx = bloodFlowActiveIndex[slot];\n                    if (idx >= 0) {\n                        const last = activeBloodFlowSlots.pop();\n                        if (idx < activeBloodFlowSlots.length) {\n                            activeBloodFlowSlots[idx] = last;\n                            bloodFlowActiveIndex[last] = idx;\n                        }\n                        bloodFlowActiveIndex[slot] = -1;\n                    }\n                    freeBloodFlowSlots.push(slot);\n                }\n\n                function setBloodFlowColor(slot, intensity) {\n                    const r = Math.max(0.72, Math.min(1, intensity));\n                    bloodFlowInstanceMesh.setColorAt(slot, bloodFlowColor.setRGB(r, r * 0.025, r * 0.012));\n                    bloodFlowColorDirty = true;\n                }\n\n                function setBloodFlowTransform(slot, x, z, roll, sx, sy) {\n                    bloodFlowPos.set(x, 0.014, z);\n                    bloodFlowScale.set(sx, sy, 1);\n                    bloodFlowQuat.setFromEuler(bloodFlowEuler.set(-Math.PI / 2, 0, roll));\n                    bloodFlowMatrix.compose(bloodFlowPos, bloodFlowQuat, bloodFlowScale);\n                    bloodFlowInstanceMesh.setMatrixAt(slot, bloodFlowMatrix);\n                    bloodFlowMatrixDirty = true;\n                }\n\n                function flushBloodFlowInstances() {\n                    if (bloodFlowCountDirty) {\n                        bloodFlowInstanceMesh.count = bloodFlowMaxActiveSlot + 1;\n                        bloodFlowCountDirty = false;\n                    }\n                    if (bloodFlowMatrixDirty) {\n                        bloodFlowInstanceMesh.instanceMatrix.needsUpdate = true;\n                        bloodFlowMatrixDirty = false;\n                    }\n                    if (bloodFlowColorDirty && bloodFlowInstanceMesh.instanceColor) {\n                        bloodFlowInstanceMesh.instanceColor.needsUpdate = true;\n                        bloodFlowColorDirty = false;\n                    }\n                    if (bloodFlowAlphaDirty) {\n                        bloodFlowAlphaAttr.needsUpdate = true;\n                        bloodFlowAlphaDirty = false;\n                    }\n                }\n\n                function resetBloodFlowInstances() {\n                    freeBloodFlowSlots.length = 0;\n                    activeBloodFlowSlots.length = 0;\n                    bloodFlowMaxActiveSlot = -1;\n                    for (let i = 0; i < MAX_BLOOD_FLOWS; i++) {\n                        bloodFlowSlotActive[i] = 0;\n                        bloodFlowActiveIndex[i] = -1;\n                        bloodFlowOpacity[i] = 0;\n                        bloodFlowAge[i] = 0;\n                        bloodFlowSettled[i] = 0;\n                        bloodFlowInstanceMesh.setMatrixAt(i, bloodFlowZeroMatrix);\n                        bloodFlowAlphaValues[i] = 0;\n                        freeBloodFlowSlots.push(MAX_BLOOD_FLOWS - 1 - i);\n                    }\n                    bloodFlowInstanceMesh.count = 0;\n                    bloodFlowMatrixDirty = true;\n                    bloodFlowAlphaDirty = true;\n                    bloodFlowColorDirty = false;\n                    bloodFlowCountDirty = false;\n                }\n\n                function findOldestBloodMistSlot() {\n                    let oldestSlot = -1;\n                    let oldestAge = -Infinity;\n                    for (let i = 0; i < bloodMistRuntime.activeSlots.length; i++) {\n                        const slot = bloodMistRuntime.activeSlots[i];\n                        if (bloodMistRuntime.age[slot] > oldestAge) {\n                            oldestAge = bloodMistRuntime.age[slot];\n                            oldestSlot = slot;\n                        }\n                    }\n                    return oldestSlot;\n                }\n\n                function releaseBloodMistSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0 || !bloodMistRuntime.active[slot]) return;\n                    const start = slot * MAX_MIST_POINTS;\n                    for (let i = 0; i < MAX_MIST_POINTS; i++) bloodMistLife[start + i] = 0;\n                    bloodMistGpuDirty = true;\n                    bloodMistRuntime.active[slot] = 0;\n                    const idx = bloodMistRuntime.activeIndex[slot];\n                    if (idx >= 0) {\n                        const last = bloodMistRuntime.activeSlots.pop();\n                        if (idx < bloodMistRuntime.activeSlots.length) {\n                            bloodMistRuntime.activeSlots[idx] = last;\n                            bloodMistRuntime.activeIndex[last] = idx;\n                        }\n                        bloodMistRuntime.activeIndex[slot] = -1;\n                    }\n                    bloodMistRuntime.freeSlots.push(slot);\n                }\n\n                function acquireBloodMistSlot() {\n                    if (!bloodMistRuntime.freeSlots.length) {\n                        const oldestSlot = findOldestBloodMistSlot();\n                        if (oldestSlot >= 0) releaseBloodMistSlot(oldestSlot);\n                    }\n                    const slot = bloodMistRuntime.freeSlots.pop();\n                    if (slot === undefined) return -1;\n                    bloodMistRuntime.active[slot] = 1;\n                    bloodMistRuntime.activeIndex[slot] = bloodMistRuntime.activeSlots.length;\n                    bloodMistRuntime.activeSlots.push(slot);\n                    bloodMistRuntime.age[slot] = 0;\n                    return slot;\n                }\n\n                function updateBloodMistRuntime(dt) {\n                    const step = Math.min(dt, 0.05);\n                    bloodMistTime += step;\n                    const mistShader = bloodMistMat.userData.shader;\n                    if (mistShader) mistShader.uniforms.uBloodMistTime.value = bloodMistTime;\n                    if (bloodMistGpuDirty) {\n                        // Several bursts can touch disjoint slots in one frame. Uploading the\n                        // complete compact pool avoids losing earlier dirty ranges.\n                        bloodMistOriginAttr.updateRange.offset = 0;\n                        bloodMistOriginAttr.updateRange.count = bloodMistOrigin.length;\n                        bloodMistVelocityAttr.updateRange.offset = 0;\n                        bloodMistVelocityAttr.updateRange.count = bloodMistVelocity.length;\n                        bloodMistBirthAttr.updateRange.offset = 0;\n                        bloodMistBirthAttr.updateRange.count = bloodMistBirth.length;\n                        bloodMistLifeAttr.updateRange.offset = 0;\n                        bloodMistLifeAttr.updateRange.count = bloodMistLife.length;\n                        bloodMistBaseSizeAttr.updateRange.offset = 0;\n                        bloodMistBaseSizeAttr.updateRange.count = bloodMistBaseSize.length;\n                        bloodMistOriginAttr.needsUpdate = true;\n                        bloodMistVelocityAttr.needsUpdate = true;\n                        bloodMistBirthAttr.needsUpdate = true;\n                        bloodMistLifeAttr.needsUpdate = true;\n                        bloodMistBaseSizeAttr.needsUpdate = true;\n                        bloodMistGpuDirty = false;\n                    }\n                    for (let i = bloodMistRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        const slot = bloodMistRuntime.activeSlots[i];\n                        bloodMistRuntime.age[slot] += step;\n                        if (bloodMistRuntime.age[slot] > 0.8) releaseBloodMistSlot(slot);\n                    }\n                }\n\n                function resetBloodMistRuntime() {\n                    for (let i = bloodMistRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        releaseBloodMistSlot(bloodMistRuntime.activeSlots[i]);\n                    }\n                    bloodMistTime = 0;\n                    const mistShader = bloodMistMat.userData.shader;\n                    if (mistShader) mistShader.uniforms.uBloodMistTime.value = 0;\n                }\n\n                function spawnBloodFlow(position, amount = 1, dirX = 0, dirZ = 0) {\n                    const pressure = getPerfPressureLevel();\n                    if (pressure >= 3 && activeBloodFlowSlots.length > Math.max(10, Math.floor(MAX_BLOOD_FLOWS * 0.36)) && Math.random() < 0.35) return;\n                    if (!shouldSpawnBudgetedEffect('blood-flow', amount >= 1.2 ? 2 : 1, position)) return;\n                    const flowScale = pressure >= 3 ? 0.45 : (pressure >= 2 ? 0.65 : (pressure >= 1 ? 0.82 : 1));\n                    const flowCount = Math.min(5, Math.max(1, Math.ceil(amount * flowScale)));\n                    const dirLenSq = dirX * dirX + dirZ * dirZ;\n                    const baseAngle = dirLenSq > 0.001 ? Math.atan2(dirZ, dirX) : Math.random() * Math.PI * 2;\n                    for (let i = 0; i < flowCount; i++) {\n                        const slot = acquireBloodFlowSlot();\n                        if (slot < 0) break;\n                        const angle = baseAngle + (Math.random() - 0.5) * (i === 0 ? 0.42 : 1.25);\n                        const side = (Math.random() - 0.5) * (0.14 + amount * 0.03);\n                        const forward = (i === 0 ? 0.08 : Math.random() * 0.24);\n                        bloodFlowX[slot] = position.x + Math.cos(angle) * forward + Math.cos(angle + Math.PI * 0.5) * side;\n                        bloodFlowZ[slot] = position.z + Math.sin(angle) * forward + Math.sin(angle + Math.PI * 0.5) * side;\n                        bloodFlowDirX[slot] = Math.cos(angle);\n                        bloodFlowDirZ[slot] = Math.sin(angle);\n                        bloodFlowRoll[slot] = angle + Math.PI * 0.5;\n                        bloodFlowAge[slot] = 0;\n                        bloodFlowDuration[slot] = 11 + Math.random() * 5;\n                        bloodFlowBaseScaleX[slot] = 0.14 + Math.random() * 0.07;\n                        bloodFlowBaseScaleY[slot] = 0.20 + Math.random() * 0.08;\n                        bloodFlowTargetScaleX[slot] = 0.62 + amount * 0.18 + Math.random() * 0.28;\n                        bloodFlowTargetScaleY[slot] = 0.46 + amount * 0.24 + Math.random() * 0.22;\n                        bloodFlowSpreadSpeed[slot] = 0.55 + Math.random() * 0.28;\n                        bloodFlowSlideDistance[slot] = 0.16 + amount * 0.08 + Math.random() * 0.18;\n                        bloodFlowOpacity[slot] = 0.86;\n                        bloodFlowSettled[slot] = 0;\n                        bloodFlowAlphaValues[slot] = bloodFlowOpacity[slot];\n                        setBloodFlowColor(slot, 0.78 + Math.random() * 0.18);\n                        setBloodFlowTransform(slot, bloodFlowX[slot], bloodFlowZ[slot], bloodFlowRoll[slot], bloodFlowBaseScaleX[slot], bloodFlowBaseScaleY[slot]);\n                    }\n                }\n\n                function updateBloodFlows(dt) {\n                    if (activeBloodFlowSlots.length === 0) return;\n                    const pressure = getPerfPressureLevel();\n                    const alphaInterval = pressure >= 3 ? 3 : (pressure >= 2 ? 2 : 1);\n                    const shouldUpdateAlpha = shouldRunBudgetedFrame('blood-flow-alpha', alphaInterval);\n                    for (let i = activeBloodFlowSlots.length - 1; i >= 0; i--) {\n                        const slot = activeBloodFlowSlots[i];\n                        bloodFlowAge[slot] += dt;\n                        if (!bloodFlowSettled[slot]) {\n                            const grow = 1 - Math.pow(1 - Math.min(1, bloodFlowAge[slot] * bloodFlowSpreadSpeed[slot]), 3);\n                            const sx = bloodFlowBaseScaleX[slot] + (bloodFlowTargetScaleX[slot] - bloodFlowBaseScaleX[slot]) * grow;\n                            const sy = bloodFlowBaseScaleY[slot] + (bloodFlowTargetScaleY[slot] - bloodFlowBaseScaleY[slot]) * grow;\n                            const slide = grow * bloodFlowSlideDistance[slot];\n                            const x = bloodFlowX[slot] + bloodFlowDirX[slot] * slide;\n                            const z = bloodFlowZ[slot] + bloodFlowDirZ[slot] * slide;\n                            setBloodFlowTransform(slot, x, z, bloodFlowRoll[slot], sx, sy);\n                            if (grow >= 0.995 || bloodFlowAge[slot] > 1.35) bloodFlowSettled[slot] = 1;\n                        }\n                        if (bloodFlowAge[slot] > bloodFlowDuration[slot]) {\n                            bloodFlowOpacity[slot] -= dt * 0.08;\n                        }\n                        if (shouldUpdateAlpha || bloodFlowOpacity[slot] <= 0) {\n                            bloodFlowAlphaValues[slot] = Math.max(0, bloodFlowOpacity[slot]);\n                            bloodFlowAlphaDirty = true;\n                        }\n                        if (bloodFlowOpacity[slot] <= 0) releaseBloodFlowSlot(slot);\n                    }\n                }\n\n                function findOldestCorpseSlot() {\n                    let oldestSlot = -1;\n                    let oldestAge = -Infinity;\n                    for (let i = 0; i < corpseRuntime.activeSlots.length; i++) {\n                        const slot = corpseRuntime.activeSlots[i];\n                        if (corpseRuntime.age[slot] > oldestAge) {\n                            oldestAge = corpseRuntime.age[slot];\n                            oldestSlot = slot;\n                        }\n                    }\n                    return oldestSlot;\n                }\n\n                function releaseCorpseSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0 || !corpseRuntime.active[slot]) return;\n                    const corpse = corpseRuntime.mesh[slot];\n                    releaseCorpseInstanceSlot(slot);\n                    if (corpse) zombieMeshPool.release(corpse);\n                    corpseRuntime.mesh[slot] = null;\n                    corpseRuntime.active[slot] = 0;\n                    corpseRuntime.landed[slot] = 0;\n                    corpseRuntime.isBurnt[slot] = 0;\n                    const idx = corpseRuntime.activeIndex[slot];\n                    if (idx >= 0) {\n                        const last = corpseRuntime.activeSlots.pop();\n                        if (idx < corpseRuntime.activeSlots.length) {\n                            corpseRuntime.activeSlots[idx] = last;\n                            corpseRuntime.activeIndex[last] = idx;\n                        }\n                        corpseRuntime.activeIndex[slot] = -1;\n                    }\n                    corpseRuntime.freeSlots.push(slot);\n                }\n\n                function acquireCorpseSlot() {\n                    if (MAX_CORPSES <= 0) return -1;\n                    if (!corpseRuntime.freeSlots.length) {\n                        const oldestSlot = findOldestCorpseSlot();\n                        if (oldestSlot >= 0) releaseCorpseSlot(oldestSlot);\n                    }\n                    const slot = corpseRuntime.freeSlots.pop();\n                    if (slot === undefined) return -1;\n                    corpseRuntime.active[slot] = 1;\n                    corpseRuntime.activeIndex[slot] = corpseRuntime.activeSlots.length;\n                    corpseRuntime.activeSlots.push(slot);\n                    corpseRuntime.age[slot] = 0;\n                    corpseRuntime.sinkAge[slot] = 0;\n                    corpseRuntime.bleedTimer[slot] = 0;\n                    corpseRuntime.bleedBursts[slot] = 0;\n                    corpseRuntime.bounceCount[slot] = 0;\n                    corpseRuntime.landed[slot] = 0;\n                    return slot;\n                }\n\n                function setCorpseLanded(slot, flowAmount = 1.2) {\n                    if (corpseRuntime.landed[slot]) return;\n                    const corpse = corpseRuntime.mesh[slot];\n                    if (!corpse) return;\n                    corpseRuntime.landed[slot] = 1;\n                    corpseRuntime.vx[slot] = 0;\n                    corpseRuntime.vy[slot] = 0;\n                    corpseRuntime.vz[slot] = 0;\n                    corpseRuntime.spinSpeed[slot] = 0;\n                    corpseRuntime.spinX[slot] = 0;\n\n                    // Flatten body rotations so it lies completely flat on the floor\n                    corpseRuntime.rx[slot] = corpseRuntime.fallDirSign[slot] * CORPSE_FLAT_ANGLE;\n                    corpseRuntime.rz[slot] = 0;\n                    corpse.rotation.set(corpseRuntime.rx[slot], corpseRuntime.ry[slot], corpseRuntime.rz[slot]);\n                    corpse.position.y = CORPSE_FLOOR_Y;\n\n                    // Settle joints flat on the floor\n                    const parts = corpse.userData.parts || cacheZombieParts(corpse);\n                    if (parts.head && parts.head.visible) {\n                        corpseRuntime.jointHeadX[slot] = 0.15;\n                        corpseRuntime.jointHeadY[slot] = (Math.random() - 0.5) * 0.4;\n                        corpseRuntime.jointHeadZ[slot] = (Math.random() - 0.5) * 0.2;\n                        parts.head.rotation.set(corpseRuntime.jointHeadX[slot], corpseRuntime.jointHeadY[slot], corpseRuntime.jointHeadZ[slot]);\n                    }\n                    if (parts.leftArm && parts.leftArm.visible) {\n                        corpseRuntime.jointLArmX[slot] = -0.15;\n                        corpseRuntime.jointLArmZ[slot] = 1.45;\n                        parts.leftArm.rotation.set(corpseRuntime.jointLArmX[slot], 0, corpseRuntime.jointLArmZ[slot]);\n                    }\n                    if (parts.rightArm && parts.rightArm.visible) {\n                        corpseRuntime.jointRArmX[slot] = -0.15;\n                        corpseRuntime.jointRArmZ[slot] = -1.45;\n                        parts.rightArm.rotation.set(corpseRuntime.jointRArmX[slot], 0, corpseRuntime.jointRArmZ[slot]);\n                    }\n                    if (parts.leftLeg && parts.leftLeg.visible) {\n                        corpseRuntime.jointLLegX[slot] = 0;\n                        parts.leftLeg.rotation.set(0, 0, 0);\n                    }\n                    if (parts.rightLeg && parts.rightLeg.visible) {\n                        corpseRuntime.jointRLegX[slot] = 0;\n                        parts.rightLeg.rotation.set(0, 0, 0);\n                    }\n\n                    // Blood floor puddles removed; no spawnBloodFlow on corpse land.\n                    syncCorpseInstanceMatrices(slot, corpse);\n                }\n\n                function updateCorpseRuntime(dt) {\n                    if (corpseRuntime.activeSlots.length === 0) return;\n                    const step = Math.min(dt, 0.05);\n                    const pressure = getPerfPressureLevel();\n                    const bleedMax = pressure >= 3 ? 1 : (pressure >= 2 ? 2 : (pressure >= 1 ? 3 : 5));\n                    const bleedAgeLimit = pressure >= 2 ? 2.6 : 4.8;\n                    const sinkStartAge = pressure >= 3 ? 5.2 : (pressure >= 2 ? 6.2 : 8.0);\n                    for (let i = corpseRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        const slot = corpseRuntime.activeSlots[i];\n                        const corpse = corpseRuntime.mesh[slot];\n                        if (!corpse) {\n                            releaseCorpseSlot(slot);\n                            continue;\n                        }\n\n                        corpseRuntime.age[slot] += step;\n                        const age = corpseRuntime.age[slot];\n\n                        if (!corpseRuntime.landed[slot]) {\n                            if (pressure >= 2 && age > corpseRuntime.forceFlatAge[slot] + 0.12 && corpse.position.y <= CORPSE_FLOOR_Y + 0.08) {\n                                setCorpseLanded(slot, pressure >= 3 ? 0.55 : 0.9);\n                                continue;\n                            }\n                            corpseRuntime.vy[slot] -= CORPSE_GRAVITY * step;\n                            const dragFactor = Math.pow(CORPSE_DRAG_BASE, step / 0.025);\n                            corpseRuntime.vx[slot] *= dragFactor;\n                            corpseRuntime.vz[slot] *= dragFactor;\n                            corpse.position.x += corpseRuntime.vx[slot] * step;\n                            corpse.position.z += corpseRuntime.vz[slot] * step;\n                            corpse.position.y += corpseRuntime.vy[slot] * step;\n\n                            // Apply angular velocities (ragdoll tumble/spins)\n                            const rotDrag = Math.pow(0.95, step / 0.025);\n                            corpseRuntime.rvx[slot] *= rotDrag;\n                            corpseRuntime.rvy[slot] *= rotDrag;\n                            corpseRuntime.rvz[slot] *= rotDrag;\n\n                            corpseRuntime.rx[slot] += corpseRuntime.rvx[slot] * step;\n                            corpseRuntime.ry[slot] += corpseRuntime.rvy[slot] * step;\n                            corpseRuntime.rz[slot] += corpseRuntime.rvz[slot] * step;\n\n                            // Joint alignment torque: make the body gradually face flat on landing\n                            const targetRx = corpseRuntime.fallDirSign[slot] * CORPSE_FLAT_ANGLE;\n                            const targetRz = 0;\n                            const alignSpeed = step * (1.8 + age * 1.6);\n                            corpseRuntime.rx[slot] += (targetRx - corpseRuntime.rx[slot]) * Math.min(1, alignSpeed);\n                            corpseRuntime.rz[slot] += (targetRz - corpseRuntime.rz[slot]) * Math.min(1, alignSpeed * 0.8);\n\n                            corpse.rotation.set(corpseRuntime.rx[slot], corpseRuntime.ry[slot], corpseRuntime.rz[slot]);\n\n                            // === Joint Sag / Limp Dangling Simulation ===\n                            const parts = corpse.userData.parts || cacheZombieParts(corpse);\n\n                            // Head joint slumping\n                            corpseRuntime.jointHeadVX[slot] += (-3.0 * corpseRuntime.jointHeadX[slot] - 8.0 * corpseRuntime.jointHeadVX[slot]) * step;\n                            corpseRuntime.jointHeadVY[slot] += (-3.0 * corpseRuntime.jointHeadY[slot] - 8.0 * corpseRuntime.jointHeadVY[slot]) * step;\n                            corpseRuntime.jointHeadVZ[slot] += (-3.0 * corpseRuntime.jointHeadZ[slot] - 8.0 * corpseRuntime.jointHeadVZ[slot]) * step;\n                            corpseRuntime.jointHeadX[slot] += corpseRuntime.jointHeadVX[slot] * step;\n                            corpseRuntime.jointHeadY[slot] += corpseRuntime.jointHeadVY[slot] * step;\n                            corpseRuntime.jointHeadZ[slot] += corpseRuntime.jointHeadVZ[slot] * step;\n                            // Joint angle limits\n                            corpseRuntime.jointHeadX[slot] = Math.max(-0.6, Math.min(0.6, corpseRuntime.jointHeadX[slot]));\n                            corpseRuntime.jointHeadY[slot] = Math.max(-0.5, Math.min(0.5, corpseRuntime.jointHeadY[slot]));\n                            corpseRuntime.jointHeadZ[slot] = Math.max(-0.4, Math.min(0.4, corpseRuntime.jointHeadZ[slot]));\n                            if (parts.head && parts.head.visible) {\n                                parts.head.rotation.set(corpseRuntime.jointHeadX[slot], corpseRuntime.jointHeadY[slot], corpseRuntime.jointHeadZ[slot]);\n                            }\n\n                            // Left Arm joint slumping\n                            corpseRuntime.jointLArmVX[slot] += (-2.5 * (corpseRuntime.jointLArmX[slot] - 0.2) - 6.0 * corpseRuntime.jointLArmVX[slot]) * step;\n                            corpseRuntime.jointLArmVZ[slot] += (-2.5 * (corpseRuntime.jointLArmZ[slot] - 0.6) - 6.0 * corpseRuntime.jointLArmVZ[slot]) * step;\n                            corpseRuntime.jointLArmX[slot] += corpseRuntime.jointLArmVX[slot] * step;\n                            corpseRuntime.jointLArmZ[slot] += corpseRuntime.jointLArmVZ[slot] * step;\n                            corpseRuntime.jointLArmX[slot] = Math.max(-1.8, Math.min(0.5, corpseRuntime.jointLArmX[slot]));\n                            corpseRuntime.jointLArmZ[slot] = Math.max(-0.2, Math.min(1.8, corpseRuntime.jointLArmZ[slot]));\n                            if (parts.leftArm && parts.leftArm.visible) {\n                                parts.leftArm.rotation.set(corpseRuntime.jointLArmX[slot], 0, corpseRuntime.jointLArmZ[slot]);\n                            }\n\n                            // Right Arm joint slumping\n                            corpseRuntime.jointRArmVX[slot] += (-2.5 * (corpseRuntime.jointRArmX[slot] - 0.2) - 6.0 * corpseRuntime.jointRArmVX[slot]) * step;\n                            corpseRuntime.jointRArmVZ[slot] += (-2.5 * (corpseRuntime.jointRArmZ[slot] + 0.6) - 6.0 * corpseRuntime.jointRArmVZ[slot]) * step;\n                            corpseRuntime.jointRArmX[slot] += corpseRuntime.jointRArmVX[slot] * step;\n                            corpseRuntime.jointRArmZ[slot] += corpseRuntime.jointRArmVZ[slot] * step;\n                            corpseRuntime.jointRArmX[slot] = Math.max(-1.8, Math.min(0.5, corpseRuntime.jointRArmX[slot]));\n                            corpseRuntime.jointRArmZ[slot] = Math.max(-1.8, Math.min(0.2, corpseRuntime.jointRArmZ[slot]));\n                            if (parts.rightArm && parts.rightArm.visible) {\n                                parts.rightArm.rotation.set(corpseRuntime.jointRArmX[slot], 0, corpseRuntime.jointRArmZ[slot]);\n                            }\n\n                            // Left Leg joint slumping\n                            corpseRuntime.jointLLegVX[slot] += (-4.0 * corpseRuntime.jointLLegX[slot] - 8.0 * corpseRuntime.jointLLegVX[slot]) * step;\n                            corpseRuntime.jointLLegX[slot] += corpseRuntime.jointLLegVX[slot] * step;\n                            corpseRuntime.jointLLegX[slot] = Math.max(-0.5, Math.min(0.8, corpseRuntime.jointLLegX[slot]));\n                            if (parts.leftLeg && parts.leftLeg.visible) {\n                                parts.leftLeg.rotation.set(corpseRuntime.jointLLegX[slot], 0, 0);\n                            }\n\n                            // Right Leg joint slumping\n                            corpseRuntime.jointRLegVX[slot] += (-4.0 * corpseRuntime.jointRLegX[slot] - 8.0 * corpseRuntime.jointRLegVX[slot]) * step;\n                            corpseRuntime.jointRLegX[slot] += corpseRuntime.jointRLegVX[slot] * step;\n                            corpseRuntime.jointRLegX[slot] = Math.max(-0.5, Math.min(0.8, corpseRuntime.jointRLegX[slot]));\n                            if (parts.rightLeg && parts.rightLeg.visible) {\n                                parts.rightLeg.rotation.set(corpseRuntime.jointRLegX[slot], 0, 0);\n                            }\n\n                            if (corpse.position.y <= CORPSE_FLOOR_Y && corpseRuntime.vy[slot] < 0) {\n                                corpse.position.y = CORPSE_FLOOR_Y;\n                                corpseRuntime.bounceCount[slot]++;\n                                if (corpseRuntime.bounceCount[slot] === 1 && Math.abs(corpseRuntime.vy[slot]) > 3) {\n                                    corpseRuntime.vy[slot] = Math.abs(corpseRuntime.vy[slot]) * 0.35;\n                                    corpseRuntime.vx[slot] *= 0.5;\n                                    corpseRuntime.vz[slot] *= 0.5;\n                                } else {\n                                    corpseRuntime.vy[slot] = 0;\n                                    corpseRuntime.vx[slot] = 0;\n                                    corpseRuntime.vz[slot] = 0;\n                                }\n                            }\n\n                            if (\n                                (corpseRuntime.fallAngle[slot] >= CORPSE_FLAT_ANGLE && corpseRuntime.bounceCount[slot] >= 1) ||\n                                (age > corpseRuntime.forceFlatAge[slot] && corpse.position.y <= CORPSE_FLOOR_Y + 0.02) ||\n                                age > corpseRuntime.forceFlatAge[slot] + CORPSE_HARD_LAND_GRACE\n                            ) {\n                                setCorpseLanded(slot, age > corpseRuntime.forceFlatAge[slot] ? 1.2 : 1.6);\n                            }\n                            if (!corpseRuntime.landed[slot]) syncCorpseInstanceMatrices(slot, corpse);\n                        }\n                        \n                        if (corpseRuntime.isBurnt[slot] && Math.random() < 0.12) {\n                            // Spawn a tiny glowing ember rising from the corpse!\n                            const cPos = corpse.position;\n                            const rx = cPos.x + (Math.random() - 0.5) * 0.5;\n                            const rz = cPos.z + (Math.random() - 0.5) * 0.5;\n                            const fi = acquireFlameSlot(false);\n                            if (fi >= 0) {\n                                seedFlameParticle(\n                                    fi,\n                                    rx,\n                                    cPos.y + 0.1 + Math.random() * 0.5,\n                                    rz,\n                                    (Math.random() - 0.5) * 0.2,\n                                    0.35 + Math.random() * 0.45, // rise velocity\n                                    (Math.random() - 0.5) * 0.2,\n                                    0.42 + Math.random() * 0.28, // life\n                                    0, // damage\n                                    0.06 + Math.random() * 0.05, // size\n                                    0.70, // kind (smoke/ember)\n                                    Math.random() * 0.04\n                                );\n                            }\n                        }\n\n                        if (corpseRuntime.landed[slot] && age < bleedAgeLimit && corpseRuntime.bleedBursts[slot] < bleedMax) {\n                            corpseRuntime.bleedTimer[slot] += step;\n                            if (corpseRuntime.bleedTimer[slot] >= (pressure >= 2 ? 0.95 : 0.65)) {\n                                corpseRuntime.bleedTimer[slot] = 0;\n                                corpseRuntime.bleedBursts[slot]++;\n                        // Blood floor puddles removed; no periodic bleed flows.\n                            }\n                        }\n\n                        if (corpseRuntime.landed[slot] && age > sinkStartAge) {\n                            corpseRuntime.sinkAge[slot] += step;\n                            corpse.position.y -= 0.5 * step;\n                            if (corpseRuntime.sinkAge[slot] > 2.4) {\n                                releaseCorpseSlot(slot);\n                            } else {\n                                syncCorpseInstanceMatrices(slot, corpse);\n                            }\n                        }\n                    }\n                }\n\n                function registerCorpse(corpse, options) {\n                    const slot = acquireCorpseSlot();\n                    if (slot < 0) {\n                        zombieMeshPool.release(corpse);\n                        return -1;\n                    }\n                    corpseRuntime.mesh[slot] = corpse;\n                    corpseRuntime.isBurnt[slot] = options.isBurnt ? 1 : 0;\n                    corpseRuntime.vx[slot] = options.vx || 0;\n                    corpseRuntime.vy[slot] = options.vy || 0;\n                    corpseRuntime.vz[slot] = options.vz || 0;\n                    corpseRuntime.ndx[slot] = options.ndx || 0;\n                    corpseRuntime.ndz[slot] = options.ndz || 0;\n                    corpseRuntime.fallAngle[slot] = options.fallAngle || 0.8;\n                    corpseRuntime.fallDirSign[slot] = options.fallDirSign !== undefined ? options.fallDirSign : -1;\n                    corpseRuntime.fallSpeed[slot] = options.fallSpeed || 0.6;\n                    corpseRuntime.fallAccel[slot] = options.fallAccel || 1.6;\n                    corpseRuntime.forceFlatAge[slot] = options.forceFlatAge || CORPSE_FORCE_FLAT_AGE;\n                    corpseRuntime.spinSpeed[slot] = options.spinSpeed || 0;\n                    corpseRuntime.spinX[slot] = options.spinX || 0;\n\n                    // Ragdoll Initial State\n                    corpseRuntime.rx[slot] = corpse.rotation.x;\n                    corpseRuntime.ry[slot] = corpse.rotation.y;\n                    corpseRuntime.rz[slot] = corpse.rotation.z;\n\n                    // Toned down spin and tumbling forces upon death to keep the physical feedback realistic and satisfying\n                    corpseRuntime.rvx[slot] = (Math.random() - 0.5) * 1.5 + corpseRuntime.fallDirSign[slot] * corpseRuntime.fallSpeed[slot] * 1.1;\n                    corpseRuntime.rvy[slot] = (Math.random() - 0.5) * 2.5;\n                    corpseRuntime.rvz[slot] = (Math.random() - 0.5) * 1.5;\n\n                    // Initialize joint angles with random limp offsets\n                    corpseRuntime.jointHeadX[slot] = (Math.random() - 0.5) * 0.4;\n                    corpseRuntime.jointHeadY[slot] = (Math.random() - 0.5) * 0.3;\n                    corpseRuntime.jointHeadZ[slot] = (Math.random() - 0.5) * 0.3;\n                    corpseRuntime.jointHeadVX[slot] = (Math.random() - 0.5) * 4;\n                    corpseRuntime.jointHeadVY[slot] = (Math.random() - 0.5) * 4;\n                    corpseRuntime.jointHeadVZ[slot] = (Math.random() - 0.5) * 4;\n\n                    corpseRuntime.jointLArmX[slot] = -0.5 + (Math.random() - 0.5) * 0.5;\n                    corpseRuntime.jointLArmZ[slot] = 0.3 + (Math.random() - 0.5) * 0.3;\n                    corpseRuntime.jointLArmVX[slot] = (Math.random() - 0.5) * 6;\n                    corpseRuntime.jointLArmVZ[slot] = (Math.random() - 0.5) * 6;\n\n                    corpseRuntime.jointRArmX[slot] = -0.5 + (Math.random() - 0.5) * 0.5;\n                    corpseRuntime.jointRArmZ[slot] = -0.3 + (Math.random() - 0.5) * 0.3;\n                    corpseRuntime.jointRArmVX[slot] = (Math.random() - 0.5) * 6;\n                    corpseRuntime.jointRArmVZ[slot] = (Math.random() - 0.5) * 6;\n\n                    corpseRuntime.jointLLegX[slot] = (Math.random() - 0.5) * 0.3;\n                    corpseRuntime.jointLLegVX[slot] = (Math.random() - 0.5) * 4;\n                    corpseRuntime.jointRLegX[slot] = (Math.random() - 0.5) * 0.3;\n                    corpseRuntime.jointRLegVX[slot] = (Math.random() - 0.5) * 4;\n\n                    // Keep the articulated corpse as a detached logical skeleton. Its final\n                    // matrices are copied into the shared corpse instance renderer.\n                    detachFromScene(corpse);\n                    corpse.visible = true;\n                    acquireCorpseInstanceSlot(slot, corpse);\n                    return slot;\n                }\n\n                function resetCorpseRuntime() {\n                    for (let i = corpseRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        releaseCorpseSlot(corpseRuntime.activeSlots[i]);\n                    }\n                }\n\n                function insertHitSorted(hits, record) {\n                    let i = hits.length;\n                    while (i > 0 && hits[i - 1].t > record.t) {\n                        hits[i] = hits[i - 1];\n                        i--;\n                    }\n                    hits[i] = record;\n                }\n\n                function makeSeveredGroup(kind) {\n                    const g = new THREE.Group();\n                    const mats = {\n                        skin: new THREE.MeshPhongMaterial({ color: 0x4a5040, transparent: true, opacity: 1.0 }),\n                        darkSkin: new THREE.MeshPhongMaterial({ color: 0x3a4030, transparent: true, opacity: 1.0 }),\n                        cloth: new THREE.MeshPhongMaterial({ color: 0x2a2a2a, transparent: true, opacity: 1.0 }),\n                        bone: new THREE.MeshPhongMaterial({ color: 0xccbb99, transparent: true, opacity: 1.0 }),\n                        stump: new THREE.MeshPhongMaterial({ color: 0x8a2020, transparent: true, opacity: 1.0 }),\n                        eye: new THREE.MeshBasicMaterial({ color: 0xff2222, transparent: true, opacity: 1.0 })\n                    };\n                    g.userData.materials = mats;\n                    if (kind === 'head') {\n                        g.add(new THREE.Mesh(zombieGeo.skull, mats.skin));\n                        const jaw = new THREE.Mesh(zombieGeo.jaw, mats.darkSkin);\n                        jaw.position.set(0, -0.18, 0.04);\n                        g.add(jaw);\n                        const teethUpper = new THREE.Mesh(zombieGeo.teethUpper, mats.bone);\n                        teethUpper.position.set(0, -0.12, 0.16);\n                        g.add(teethUpper);\n                        const teethLower = new THREE.Mesh(zombieGeo.teethLower, mats.bone);\n                        teethLower.position.set(0, 0.02, 0.06);\n                        jaw.add(teethLower);\n                        const headWound = new THREE.Mesh(zombieGeo.headWound, mats.stump);\n                        headWound.position.set(0.12, 0.05, 0.12);\n                        g.add(headWound);\n                        const lEye = new THREE.Mesh(zombieGeo.eye, mats.eye);\n                        lEye.position.set(-0.08, 0.03, 0.18);\n                        g.add(lEye);\n                        const rEye = new THREE.Mesh(zombieGeo.eye, mats.eye);\n                        rEye.position.set(0.08, 0.03, 0.18);\n                        g.add(rEye);\n                        const stump = new THREE.Mesh(zombieGeo.neckStump, mats.stump);\n                        stump.position.y = -0.22;\n                        g.add(stump);\n                        const bone = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 0.06, 6), mats.bone);\n                        bone.position.y = -0.25;\n                        g.add(bone);\n                    } else if (kind === 'arm') {\n                        const upper = new THREE.Mesh(zombieGeo.arm, mats.skin);\n                        upper.position.y = -0.36;\n                        g.add(upper);\n                        const hand = new THREE.Mesh(zombieGeo.hand, mats.darkSkin);\n                        hand.position.y = -0.74;\n                        g.add(hand);\n                        const stump = new THREE.Mesh(zombieGeo.armStump, mats.stump);\n                        stump.position.y = 0.02;\n                        g.add(stump);\n                    } else {\n                        const thigh = new THREE.Mesh(zombieGeo.leg, mats.cloth);\n                        thigh.position.y = -0.32;\n                        g.add(thigh);\n                        const foot = new THREE.Mesh(zombieGeo.foot, mats.darkSkin);\n                        foot.position.set(0, -0.66, 0.04);\n                        g.add(foot);\n                        const stump = new THREE.Mesh(zombieGeo.legStump, mats.stump);\n                        stump.position.y = 0.02;\n                        g.add(stump);\n                    }\n                    const cap = new THREE.Mesh(new THREE.BoxGeometry(kind === 'head' ? 0.28 : 0.18, 0.04, kind === 'head' ? 0.28 : 0.18), mats.stump);\n                    cap.position.y = kind === 'head' ? -0.23 : 0.03;\n                    g.add(cap);\n                    for (let i = 0; i < 2; i++) {\n                        const splinter = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.09, 0.02), mats.bone);\n                        splinter.position.set((i - 0.5) * 0.06, cap.position.y - 0.02, 0.015);\n                        splinter.rotation.z = (i - 0.5) * 0.45;\n                        g.add(splinter);\n                    }\n                    g.visible = false;\n                    return g;\n                }\n\n                function applySeveredPalette(severedGroup, zombieMesh) {\n                    const palette = zombieMesh.userData.palette;\n                    const mats = severedGroup.userData.materials;\n                    if (!palette || !mats) return;\n                    mats.skin.color.setHex(palette.skin);\n                    mats.darkSkin.color.setHex(palette.darkSkin);\n                    mats.cloth.color.setHex(palette.cloth);\n                    mats.bone.color.setHex(palette.bone);\n                    mats.stump.color.setHex(palette.stump);\n                    mats.eye.color.setHex(palette.eye);\n                    for (const key in mats) {\n                        if (typeof mats[key].opacity === 'number') mats[key].opacity = 1.0;\n                    }\n                }\n\n                const severedPools = {\n                    head: new ObjectPool(() => makeSeveredGroup('head'), (g) => { detachFromScene(g); g.visible = false; g.position.set(0, -100, 0); g.rotation.set(0, 0, 0); g.scale.set(1, 1, 1); }, 4),\n                    arm: new ObjectPool(() => makeSeveredGroup('arm'), (g) => { detachFromScene(g); g.visible = false; g.position.set(0, -100, 0); g.rotation.set(0, 0, 0); g.scale.set(1, 1, 1); }, 8),\n                    leg: new ObjectPool(() => makeSeveredGroup('leg'), (g) => { detachFromScene(g); g.visible = false; g.position.set(0, -100, 0); g.rotation.set(0, 0, 0); g.scale.set(1, 1, 1); }, 8)\n                };\n                const SEVERED_KIND_HEAD = 0;\n                const SEVERED_KIND_ARM = 1;\n                const SEVERED_KIND_LEG = 2;\n                const severedRuntime = {\n                    activeSlots: activeSeveredParts,\n                    freeSlots: [],\n                    activeIndex: new Int16Array(MAX_SEVERED_PARTS),\n                    group: new Array(MAX_SEVERED_PARTS),\n                    kind: new Uint8Array(MAX_SEVERED_PARTS),\n                    vx: new Float32Array(MAX_SEVERED_PARTS),\n                    vy: new Float32Array(MAX_SEVERED_PARTS),\n                    vz: new Float32Array(MAX_SEVERED_PARTS),\n                    rvx: new Float32Array(MAX_SEVERED_PARTS),\n                    rvy: new Float32Array(MAX_SEVERED_PARTS),\n                    rvz: new Float32Array(MAX_SEVERED_PARTS),\n                    age: new Float32Array(MAX_SEVERED_PARTS),\n                    bloodTrailTimer: new Float32Array(MAX_SEVERED_PARTS),\n                    bounceCount: new Uint8Array(MAX_SEVERED_PARTS),\n                    active: new Uint8Array(MAX_SEVERED_PARTS)\n                };\n                severedRuntime.activeIndex.fill(-1);\n                for (let i = MAX_SEVERED_PARTS - 1; i >= 0; i--) severedRuntime.freeSlots.push(i);\n\n                function severedPoolForKind(kind) {\n                    return kind === SEVERED_KIND_HEAD ? severedPools.head\n                        : kind === SEVERED_KIND_ARM ? severedPools.arm\n                            : severedPools.leg;\n                }\n\n                function findOldestSeveredSlot() {\n                    let oldestSlot = -1;\n                    let oldestAge = -Infinity;\n                    for (let i = 0; i < severedRuntime.activeSlots.length; i++) {\n                        const slot = severedRuntime.activeSlots[i];\n                        if (severedRuntime.age[slot] > oldestAge) {\n                            oldestAge = severedRuntime.age[slot];\n                            oldestSlot = slot;\n                        }\n                    }\n                    return oldestSlot;\n                }\n\n                function releaseSeveredSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0 || !severedRuntime.active[slot]) return;\n                    const group = severedRuntime.group[slot];\n                    if (group) severedPoolForKind(severedRuntime.kind[slot]).release(group);\n                    severedRuntime.group[slot] = null;\n                    severedRuntime.active[slot] = 0;\n                    const idx = severedRuntime.activeIndex[slot];\n                    if (idx >= 0) {\n                        const last = severedRuntime.activeSlots.pop();\n                        if (idx < severedRuntime.activeSlots.length) {\n                            severedRuntime.activeSlots[idx] = last;\n                            severedRuntime.activeIndex[last] = idx;\n                        }\n                        severedRuntime.activeIndex[slot] = -1;\n                    }\n                    severedRuntime.freeSlots.push(slot);\n                }\n\n                function acquireSeveredSlot() {\n                    if (!severedRuntime.freeSlots.length) {\n                        const oldestSlot = findOldestSeveredSlot();\n                        if (oldestSlot >= 0) releaseSeveredSlot(oldestSlot);\n                    }\n                    const slot = severedRuntime.freeSlots.pop();\n                    if (slot === undefined) return -1;\n                    severedRuntime.active[slot] = 1;\n                    severedRuntime.activeIndex[slot] = severedRuntime.activeSlots.length;\n                    severedRuntime.activeSlots.push(slot);\n                    severedRuntime.age[slot] = 0;\n                    severedRuntime.bloodTrailTimer[slot] = 0;\n                    severedRuntime.bounceCount[slot] = 0;\n                    return slot;\n                }\n\n                function updateSeveredRuntime(dt) {\n                    if (severedRuntime.activeSlots.length === 0) return;\n                    const step = Math.min(dt, 0.05);\n                    const pressure = getPerfPressureLevel();\n                    const trailInterval = pressure >= 2 ? 0.32 : 0.16;\n                    const maxAge = pressure >= 3 ? 2.8 : (pressure >= 2 ? 3.8 : 5.2);\n                    for (let i = severedRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        const slot = severedRuntime.activeSlots[i];\n                        const group = severedRuntime.group[slot];\n                        if (!group) {\n                            releaseSeveredSlot(slot);\n                            continue;\n                        }\n                        severedRuntime.age[slot] += step;\n                        severedRuntime.vy[slot] -= 13 * step;\n                        const drag = Math.pow(0.995, step / 0.025);\n                        severedRuntime.vx[slot] *= drag;\n                        severedRuntime.vz[slot] *= drag;\n                        group.position.x += severedRuntime.vx[slot] * step;\n                        group.position.y += severedRuntime.vy[slot] * step;\n                        group.position.z += severedRuntime.vz[slot] * step;\n                        group.rotation.x += severedRuntime.rvx[slot] * step;\n                        group.rotation.y += severedRuntime.rvy[slot] * step;\n                        group.rotation.z += severedRuntime.rvz[slot] * step;\n\n                        if (group.position.y <= 0.15) {\n                            group.position.y = 0.15;\n                            severedRuntime.vy[slot] = -severedRuntime.vy[slot] * 0.2;\n                            severedRuntime.vx[slot] *= 0.5;\n                            severedRuntime.vz[slot] *= 0.5;\n                            severedRuntime.rvx[slot] *= 0.3;\n                            severedRuntime.rvy[slot] *= 0.3;\n                            severedRuntime.rvz[slot] *= 0.3;\n                            severedRuntime.bounceCount[slot]++;\n                            if (severedRuntime.bounceCount[slot] >= 3) {\n                                severedRuntime.vy[slot] = 0;\n                                severedRuntime.rvx[slot] = 0;\n                                severedRuntime.rvy[slot] = 0;\n                                severedRuntime.rvz[slot] = 0;\n                            }\n                        }\n\n                        severedRuntime.bloodTrailTimer[slot] += step;\n                        if (severedRuntime.bloodTrailTimer[slot] > trailInterval && severedRuntime.age[slot] < (pressure >= 2 ? 0.55 : 1.0)) {\n                            severedRuntime.bloodTrailTimer[slot] = 0;\n                            spawnBloodBurst(group.position, 1);\n                        }\n\n                        if (severedRuntime.age[slot] > maxAge) releaseSeveredSlot(slot);\n                    }\n                }\n\n                function resetSeveredRuntime() {\n                    for (let i = severedRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        releaseSeveredSlot(severedRuntime.activeSlots[i]);\n                    }\n                }\n\n                // ===== BULLET HOLE DECAL OBJECT POOL =====\n                const MAX_BULLET_HOLES = perfProfile.maxBulletHoles;\n                const bulletHoles = []; // Track active holes for limit management\n                let bulletHoleSerial = 0;\n                const bulletHoleZeroMatrix = new THREE.Matrix4().makeScale(0, 0, 0);\n                const bulletHoleMatrix = new THREE.Matrix4();\n                const bulletHoleQuat = new THREE.Quaternion();\n                const bulletHoleRollQuat = new THREE.Quaternion();\n                const bulletHoleScale = new THREE.Vector3();\n                const bulletHoleRecords = [];\n                const freeBulletHoleSlots = [];\n                const bulletHoleSlotActive = new Uint8Array(MAX_BULLET_HOLES);\n                let bulletHoleMaxActiveSlot = -1;\n                let bulletHoleInstancesDirty = false;\n                let bulletHoleCountDirty = true;\n\n                function createBulletHoleTexture() {\n                    const texCanvas = document.createElement('canvas');\n                    texCanvas.width = 64;\n                    texCanvas.height = 64;\n                    const ctx = texCanvas.getContext('2d');\n                    ctx.clearRect(0, 0, 64, 64);\n                    const burn = ctx.createRadialGradient(32, 32, 2, 32, 32, 30);\n                    burn.addColorStop(0, 'rgba(0,0,0,0.95)');\n                    burn.addColorStop(0.32, 'rgba(10,10,8,0.82)');\n                    burn.addColorStop(0.58, 'rgba(52,45,28,0.42)');\n                    burn.addColorStop(1, 'rgba(0,0,0,0)');\n                    ctx.fillStyle = burn;\n                    ctx.fillRect(0, 0, 64, 64);\n                    ctx.strokeStyle = 'rgba(20,20,18,0.72)';\n                    ctx.lineWidth = 2;\n                    for (let i = 0; i < 7; i++) {\n                        const a = (i / 7) * Math.PI * 2 + 0.23;\n                        const r0 = 9 + (i % 3) * 2;\n                        const r1 = 19 + (i % 4) * 4;\n                        ctx.beginPath();\n                        ctx.moveTo(32 + Math.cos(a) * r0, 32 + Math.sin(a) * r0);\n                        ctx.lineTo(32 + Math.cos(a + 0.18) * r1, 32 + Math.sin(a + 0.18) * r1);\n                        ctx.stroke();\n                    }\n                    const texture = new THREE.CanvasTexture(texCanvas);\n                    texture.minFilter = THREE.LinearFilter;\n                    texture.magFilter = THREE.LinearFilter;\n                    texture.generateMipmaps = false;\n                    return texture;\n                }\n\n                const bulletHoleInstanceMesh = new THREE.InstancedMesh(\n                    new THREE.PlaneGeometry(0.22, 0.22),\n                    new THREE.MeshBasicMaterial({\n                        map: createBulletHoleTexture(),\n                        transparent: true,\n                        depthWrite: false,\n                        alphaTest: 0.08,\n                        polygonOffset: true,\n                        polygonOffsetFactor: -4,\n                        polygonOffsetUnits: -4\n                    }),\n                    MAX_BULLET_HOLES\n                );\n                bulletHoleInstanceMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);\n                bulletHoleInstanceMesh.frustumCulled = false;\n                bulletHoleInstanceMesh.count = 0;\n                for (let i = 0; i < MAX_BULLET_HOLES; i++) {\n                    bulletHoleInstanceMesh.setMatrixAt(i, bulletHoleZeroMatrix);\n                    bulletHoleRecords[i] = { slot: i, serial: 0 };\n                    freeBulletHoleSlots.push(MAX_BULLET_HOLES - 1 - i);\n                }\n                bulletHoleInstanceMesh.instanceMatrix.needsUpdate = true;\n                scene.add(bulletHoleInstanceMesh);\n\n                function releaseBulletHoleRecord(record) {\n                    if (!record || record.serial === 0) return;\n                    removeArrayItemFast(bulletHoles, record);\n                    record.serial = 0;\n                    bulletHoleInstanceMesh.setMatrixAt(record.slot, bulletHoleZeroMatrix);\n                    bulletHoleSlotActive[record.slot] = 0;\n                    if (record.slot === bulletHoleMaxActiveSlot) {\n                        while (bulletHoleMaxActiveSlot >= 0 && !bulletHoleSlotActive[bulletHoleMaxActiveSlot]) bulletHoleMaxActiveSlot--;\n                        bulletHoleCountDirty = true;\n                    }\n                    bulletHoleInstancesDirty = true;\n                    freeBulletHoleSlots.push(record.slot);\n                }\n\n                function trimBulletHoles(reserve = 1) {\n                    const pressure = getPerfPressureLevel();\n                    const dynamicLimit = pressure >= 3 ? Math.max(12, Math.floor(MAX_BULLET_HOLES * 0.45))\n                        : (pressure >= 2 ? Math.max(18, Math.floor(MAX_BULLET_HOLES * 0.68)) : MAX_BULLET_HOLES);\n                    while (bulletHoles.length + reserve > dynamicLimit) {\n                        let oldest = null;\n                        let oldestSerial = Infinity;\n                        for (let i = 0; i < bulletHoles.length; i++) {\n                            const hole = bulletHoles[i];\n                            const serial = hole ? hole.serial : 0;\n                            if (serial < oldestSerial) {\n                                oldestSerial = serial;\n                                oldest = hole;\n                            }\n                        }\n                        if (!oldest) break;\n                        releaseBulletHoleRecord(oldest);\n                    }\n                }\n\n                function acquireBulletHoleRecord() {\n                    trimBulletHoles(1);\n                    const slot = freeBulletHoleSlots.pop();\n                    if (slot === undefined) return null;\n                    const record = bulletHoleRecords[slot];\n                    record.serial = ++bulletHoleSerial;\n                    bulletHoleSlotActive[slot] = 1;\n                    if (slot > bulletHoleMaxActiveSlot) {\n                        bulletHoleMaxActiveSlot = slot;\n                        bulletHoleCountDirty = true;\n                    }\n                    bulletHoles.push(record);\n                    return record;\n                }\n\n                function placeBulletHoleRecord(record, position, surfaceNormal, roll, sx, sy) {\n                    if (!record) return;\n                    bulletHoleQuat.setFromUnitVectors(effectScratch.zNormal, surfaceNormal);\n                    bulletHoleRollQuat.setFromAxisAngle(effectScratch.zNormal, roll);\n                    bulletHoleQuat.multiply(bulletHoleRollQuat);\n                    bulletHoleScale.set(sx, sy, 1);\n                    bulletHoleMatrix.compose(position, bulletHoleQuat, bulletHoleScale);\n                    bulletHoleInstanceMesh.setMatrixAt(record.slot, bulletHoleMatrix);\n                    bulletHoleInstancesDirty = true;\n                }\n\n                function flushBulletHoleInstances() {\n                    if (bulletHoleCountDirty) {\n                        bulletHoleInstanceMesh.count = bulletHoleMaxActiveSlot + 1;\n                        bulletHoleCountDirty = false;\n                    }\n                    if (bulletHoleInstancesDirty) {\n                        bulletHoleInstanceMesh.instanceMatrix.needsUpdate = true;\n                        bulletHoleInstancesDirty = false;\n                    }\n                }\n\n                function createImpactSparkMesh() {\n                    const group = new THREE.Group();\n                    const materials = [];\n                    const core = new THREE.Mesh(\n                        new THREE.SphereGeometry(0.055, 8, 6),\n                        new THREE.MeshBasicMaterial({ color: 0xffcc66, transparent: true, opacity: 0.95 })\n                    );\n                    materials.push({ material: core.material, opacity: 0.95 });\n                    group.add(core);\n                    const streakMat = new THREE.LineBasicMaterial({ color: 0xffeeaa, transparent: true, opacity: 0.8 });\n                    for (let i = 0; i < 5; i++) {\n                        const angle = (i / 5) * Math.PI * 2;\n                        const geo = new THREE.BufferGeometry().setFromPoints([\n                            new THREE.Vector3(0, 0, 0),\n                            new THREE.Vector3(Math.cos(angle) * 0.16, Math.sin(angle) * 0.16, 0)\n                        ]);\n                        const line = new THREE.Line(geo, streakMat.clone());\n                        materials.push({ material: line.material, opacity: 0.8 });\n                        group.add(line);\n                    }\n                    group.userData.materials = materials;\n                    group.visible = false;\n                    return group;\n                }\n\n                const impactSparkPool = new ObjectPool(\n                    createImpactSparkMesh,\n                    (mesh) => { detachFromScene(mesh); mesh.visible = false; mesh.position.set(0, -100, 0); },\n                    24\n                );\n                const MAX_IMPACT_SPARKS = 24;\n                const impactSparkRuntime = {\n                    activeSlots: activeImpactSparks,\n                    freeSlots: [],\n                    activeIndex: new Int16Array(MAX_IMPACT_SPARKS),\n                    mesh: new Array(MAX_IMPACT_SPARKS),\n                    ttl: new Float32Array(MAX_IMPACT_SPARKS),\n                    active: new Uint8Array(MAX_IMPACT_SPARKS)\n                };\n                impactSparkRuntime.activeIndex.fill(-1);\n                for (let i = MAX_IMPACT_SPARKS - 1; i >= 0; i--) impactSparkRuntime.freeSlots.push(i);\n\n                function findOldestImpactSparkSlot() {\n                    let oldestSlot = -1;\n                    let lowestTtl = Infinity;\n                    for (let i = 0; i < impactSparkRuntime.activeSlots.length; i++) {\n                        const slot = impactSparkRuntime.activeSlots[i];\n                        if (impactSparkRuntime.ttl[slot] < lowestTtl) {\n                            lowestTtl = impactSparkRuntime.ttl[slot];\n                            oldestSlot = slot;\n                        }\n                    }\n                    return oldestSlot;\n                }\n\n                function releaseImpactSparkSlot(slot) {\n                    if (slot === undefined || slot === null || slot < 0 || !impactSparkRuntime.active[slot]) return;\n                    const mesh = impactSparkRuntime.mesh[slot];\n                    if (mesh) impactSparkPool.release(mesh);\n                    impactSparkRuntime.mesh[slot] = null;\n                    impactSparkRuntime.active[slot] = 0;\n                    const idx = impactSparkRuntime.activeIndex[slot];\n                    if (idx >= 0) {\n                        const last = impactSparkRuntime.activeSlots.pop();\n                        if (idx < impactSparkRuntime.activeSlots.length) {\n                            impactSparkRuntime.activeSlots[idx] = last;\n                            impactSparkRuntime.activeIndex[last] = idx;\n                        }\n                        impactSparkRuntime.activeIndex[slot] = -1;\n                    }\n                    impactSparkRuntime.freeSlots.push(slot);\n                }\n\n                function acquireImpactSparkSlot() {\n                    if (!impactSparkRuntime.freeSlots.length) {\n                        const oldestSlot = findOldestImpactSparkSlot();\n                        if (oldestSlot >= 0) releaseImpactSparkSlot(oldestSlot);\n                    }\n                    const slot = impactSparkRuntime.freeSlots.pop();\n                    if (slot === undefined) return -1;\n                    impactSparkRuntime.active[slot] = 1;\n                    impactSparkRuntime.activeIndex[slot] = impactSparkRuntime.activeSlots.length;\n                    impactSparkRuntime.activeSlots.push(slot);\n                    impactSparkRuntime.ttl[slot] = 0.09;\n                    return slot;\n                }\n\n                function updateImpactSparkRuntime(dt) {\n                    if (impactSparkRuntime.activeSlots.length === 0) return;\n                    const step = Math.min(dt, 0.05);\n                    const interval = getFrameSkipInterval(1, impactSparkRuntime.activeSlots.length > 8);\n                    const animateThisFrame = shouldRunBudgetedFrame('impact-sparks', interval);\n                    for (let i = impactSparkRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        const slot = impactSparkRuntime.activeSlots[i];\n                        const spark = impactSparkRuntime.mesh[slot];\n                        if (!spark) {\n                            releaseImpactSparkSlot(slot);\n                            continue;\n                        }\n                        impactSparkRuntime.ttl[slot] -= step;\n                        if (animateThisFrame) {\n                            spark.scale.multiplyScalar(Math.pow(0.94, interval));\n                            const materials = spark.userData.materials || [];\n                            for (let j = 0; j < materials.length; j++) materials[j].material.opacity *= Math.pow(0.86, interval);\n                        }\n                        if (impactSparkRuntime.ttl[slot] <= 0) releaseImpactSparkSlot(slot);\n                    }\n                }\n\n                function resetImpactSparkRuntime() {\n                    for (let i = impactSparkRuntime.activeSlots.length - 1; i >= 0; i--) {\n                        releaseImpactSparkSlot(impactSparkRuntime.activeSlots[i]);\n                    }\n                }\n\n                // ===== WALL COLLIDERS (for bullet collision) =====\n                const wallColliders = [];\n                function buildWallColliders() {\n                    // Floor\n                    wallColliders.push({ type: 'plane', normal: new THREE.Vector3(0, 1, 0), d: 0, name: 'floor' });\n                    // Ceiling\n                    wallColliders.push({ type: 'plane', normal: new THREE.Vector3(0, -1, 0), d: -5.5, name: 'ceiling' });\n                    // Left wall\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(-11.25, 0, -19.5), max: new THREE.Vector3(-10.75, 5.5, 20), name: 'left_wall' });\n                    // Right wall\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(10.75, 0, -19.5), max: new THREE.Vector3(11.25, 5.5, 20), name: 'right_wall' });\n                    // Back wall\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(-11, 0, 19.75), max: new THREE.Vector3(11, 5.5, 20.25), name: 'back_wall' });\n                    // Pillars\n                    for (let px = -8; px <= 8; px += 4) {\n                        if (px === 0) continue; // center pillar removed\n                        wallColliders.push({ type: 'aabb', min: new THREE.Vector3(px - 0.15, 0, 4.85), max: new THREE.Vector3(px + 0.15, 5.5, 5.15), name: 'pillar_' + px });\n                    }\n                    // Crates\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(-8.5, 0, 11.7), max: new THREE.Vector3(-7.5, 0.6, 12.3), name: 'crate_left' });\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(7.5, 0, 11.7), max: new THREE.Vector3(8.5, 0.6, 12.3), name: 'crate_right' });\n                    // Front wall sections (3 archways at x=-7, 0, 7, each 4 units wide)\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(-11, 0, -12.25), max: new THREE.Vector3(-9, 5.5, -11.75), name: 'front_far_left' });\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(-5, 0, -12.25), max: new THREE.Vector3(-2, 5.5, -11.75), name: 'front_mid_left' });\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(2, 0, -12.25), max: new THREE.Vector3(5, 5.5, -11.75), name: 'front_mid_right' });\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(9, 0, -12.25), max: new THREE.Vector3(11, 5.5, -11.75), name: 'front_far_right' });\n                    // Corridor walls\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(-9.15, 0, -20), max: new THREE.Vector3(-8.85, 5.5, -12), name: 'lcorr_l' });\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(-5.15, 0, -20), max: new THREE.Vector3(-4.85, 5.5, -12), name: 'lcorr_r' });\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(-2.15, 0, -20), max: new THREE.Vector3(-1.85, 5.5, -12), name: 'ccorr_l' });\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(1.85, 0, -20), max: new THREE.Vector3(2.15, 5.5, -12), name: 'ccorr_r' });\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(4.85, 0, -20), max: new THREE.Vector3(5.15, 5.5, -12), name: 'rcorr_l' });\n                    wallColliders.push({ type: 'aabb', min: new THREE.Vector3(8.85, 0, -20), max: new THREE.Vector3(9.15, 5.5, -12), name: 'rcorr_r' });\n                }\n                buildWallColliders();\n\n                // Ray-AABB intersection helper\n                function rayAABBIntersect(origin, dir, aabb) {\n                    let tmin = (aabb.min.x - origin.x) / (dir.x || 1e-10);\n                    let tmax = (aabb.max.x - origin.x) / (dir.x || 1e-10);\n                    if (tmin > tmax) { let t = tmin; tmin = tmax; tmax = t; }\n                    let tymin = (aabb.min.y - origin.y) / (dir.y || 1e-10);\n                    let tymax = (aabb.max.y - origin.y) / (dir.y || 1e-10);\n                    if (tymin > tymax) { let t = tymin; tymin = tymax; tymax = t; }\n                    if (tmin > tymax || tymin > tmax) return null;\n                    if (tymin > tmin) tmin = tymin;\n                    if (tymax < tmax) tmax = tymax;\n                    let tzmin = (aabb.min.z - origin.z) / (dir.z || 1e-10);\n                    let tzmax = (aabb.max.z - origin.z) / (dir.z || 1e-10);\n                    if (tzmin > tzmax) { let t = tzmin; tzmin = tzmax; tzmax = t; }\n                    if (tmin > tzmax || tzmin > tmax) return null;\n                    if (tzmin > tmin) tmin = tzmin;\n                    if (tzmax < tmax) tmax = tzmax;\n                    if (tmin < 0 && tmax < 0) return null;\n                    const t = tmin >= 0 ? tmin : tmax;\n                    if (t < 0) return null;\n                    // Calculate hit normal (which face was hit)\n                    const hitPoint = effectScratch.v6.set(origin.x + dir.x * t, origin.y + dir.y * t, origin.z + dir.z * t);\n                    const normal = effectScratch.v7.set(0, 0, 1);\n                    const eps = 0.01;\n                    if (Math.abs(hitPoint.x - aabb.min.x) < eps) normal.set(-1, 0, 0);\n                    else if (Math.abs(hitPoint.x - aabb.max.x) < eps) normal.set(1, 0, 0);\n                    else if (Math.abs(hitPoint.y - aabb.min.y) < eps) normal.set(0, -1, 0);\n                    else if (Math.abs(hitPoint.y - aabb.max.y) < eps) normal.set(0, 1, 0);\n                    else if (Math.abs(hitPoint.z - aabb.min.z) < eps) normal.set(0, 0, -1);\n                    else if (Math.abs(hitPoint.z - aabb.max.z) < eps) normal.set(0, 0, 1);\n                    const hit = effectScratch.wallHitCandidate;\n                    hit.t = t;\n                    hit.px = hitPoint.x;\n                    hit.py = hitPoint.y;\n                    hit.pz = hitPoint.z;\n                    hit.nx = normal.x;\n                    hit.ny = normal.y;\n                    hit.nz = normal.z;\n                    return hit;\n                }\n\n                // Find nearest wall hit along a segment (origin + dir * maxDist)\n                function getNearestWallHit(origin, dir, maxDist) {\n                    let hasNearest = false;\n                    const nearest = effectScratch.wallHit;\n                    for (let i = 0; i < wallColliders.length; i++) {\n                        const wall = wallColliders[i];\n                        if (wall.type === 'aabb') {\n                            const hit = rayAABBIntersect(origin, dir, wall);\n                            if (hit && hit.t >= 0.005 && hit.t <= maxDist) {\n                                if (!hasNearest || hit.t < nearest.distance) {\n                                    hasNearest = true;\n                                    nearest.px = hit.px;\n                                    nearest.py = hit.py;\n                                    nearest.pz = hit.pz;\n                                    nearest.nx = hit.nx;\n                                    nearest.ny = hit.ny;\n                                    nearest.nz = hit.nz;\n                                    nearest.distance = hit.t;\n                                }\n                            }\n                        } else if (wall.type === 'plane') {\n                            const dot = dir.dot(wall.normal);\n                            if (Math.abs(dot) <= 0.00001) continue;\n                            const t = -(wall.normal.dot(origin) + wall.d) / dot;\n                            if (t >= 0.005 && t <= maxDist) {\n                                const hitPoint = effectScratch.v6.set(origin.x + dir.x * t, origin.y + dir.y * t, origin.z + dir.z * t);\n                                // Keep plane hits inside bunker footprint only\n                                if (hitPoint.x > -11 && hitPoint.x < 11 && hitPoint.z > -20 && hitPoint.z < 15) {\n                                    if (!hasNearest || t < nearest.distance) {\n                                        hasNearest = true;\n                                        nearest.px = hitPoint.x;\n                                        nearest.py = hitPoint.y;\n                                        nearest.pz = hitPoint.z;\n                                        nearest.nx = wall.normal.x;\n                                        nearest.ny = wall.normal.y;\n                                        nearest.nz = wall.normal.z;\n                                        nearest.distance = t;\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    return hasNearest ? nearest : null;\n                }\n\n                // Spawn a bullet hole decal at a given position and normal\n                function spawnBulletHole(position, normal) {\n                    const record = acquireBulletHoleRecord();\n                    if (!record) return;\n                    const surfaceNormal = effectScratch.v4.copy(normal).normalize();\n                    const point = effectScratch.v5.copy(position).addScaledVector(surfaceNormal, 0.025);\n                    const s = 0.7 + Math.random() * 0.6;\n                    placeBulletHoleRecord(record, point, surfaceNormal, Math.random() * Math.PI * 2, s, s);\n                }\n\n                function spawnBulletMark(position, normal, bulletDir) {\n                    const pressure = getPerfPressureLevel();\n                    if (pressure >= 3 && bulletHoles.length >= Math.max(12, Math.floor(MAX_BULLET_HOLES * 0.42)) && Math.random() < 0.45) return;\n                    if (!shouldSpawnBudgetedEffect('bullet-mark', 1, position)) return;\n                    const record = acquireBulletHoleRecord();\n                    if (!record) return;\n                    const surfaceNormal = effectScratch.v4.copy(normal).normalize();\n                    const incoming = effectScratch.v5.copy(bulletDir).normalize();\n                    const point = effectScratch.v6.copy(position).addScaledVector(surfaceNormal, 0.025);\n\n                    const tangent = effectScratch.v3.copy(incoming).addScaledVector(surfaceNormal, -incoming.dot(surfaceNormal));\n                    const grazing = 1 - Math.abs(incoming.dot(surfaceNormal));\n                    const stretch = 1 + grazing * 3.8;\n                    let roll = Math.random() * Math.PI * 2;\n                    if (tangent.lengthSq() > 0.00001) {\n                        tangent.normalize();\n                        const localTangent = effectScratch.v2.copy(tangent).applyQuaternion(effectScratch.q1.copy(bulletHoleQuat.setFromUnitVectors(effectScratch.zNormal, surfaceNormal)).invert());\n                        roll = Math.atan2(localTangent.y, localTangent.x) + (Math.random() - 0.5) * 0.2;\n                    }\n\n                    const s = 0.65 + Math.random() * 0.45;\n                    placeBulletHoleRecord(record, point, surfaceNormal, roll, s * stretch, s * (0.85 + grazing * 0.25));\n                }\n\n                function spawnImpactSpark(position, normal, color = 0xffcc66, scale = 1) {\n                    const pressure = getPerfPressureLevel();\n                    if ((pressure >= 3 && activeImpactSparks.length >= 6) || (pressure >= 2 && activeImpactSparks.length >= 12)) return;\n                    if (!shouldSpawnBudgetedEffect('impact-spark', scale >= 0.85 ? 2 : 1, position)) return;\n                    const slot = acquireImpactSparkSlot();\n                    if (slot < 0) return;\n                    const spark = impactSparkPool.acquire();\n                    impactSparkRuntime.mesh[slot] = spark;\n                    attachToScene(spark);\n                    spark.visible = true;\n                    const surfaceNormal = effectScratch.v4.copy(normal).normalize();\n                    spark.position.copy(position).addScaledVector(surfaceNormal, 0.04);\n                    spark.quaternion.setFromUnitVectors(effectScratch.zNormal, surfaceNormal);\n                    spark.scale.setScalar(scale);\n                    const materials = spark.userData.materials || [];\n                    for (let i = 0; i < materials.length; i++) {\n                        const entry = materials[i];\n                        entry.material.color.setHex(color);\n                        entry.material.opacity = entry.opacity;\n                    }\n                }\n\n                // ===== PLAYER ENTITY =====\n                const playerEntity = world.createEntity();\n                world.addComponent(playerEntity, 'Position', { x: 0, y: 1.7, z: 10 });\n                world.addComponent(playerEntity, 'Player', { yaw: 0, pitch: 0 });\n                world.addComponent(playerEntity, 'Health', { current: 100, max: 100 });\n                world.addComponent(playerEntity, 'Weapon', {\n                    index: 0, fireTimer: 0, isReloading: false, reloadTimer: 0,\n                    spreadHeat: 0, visualKick: 0, reloadStage: 0,\n                    recoilPitchDebt: 0, recoilYawDebt: 0, lastShellAt: 0\n                });\n\n                // ===== GAME STATE (Unidirectional data store) =====\n                const gameState = {\n                    started: false, over: false, score: 0, kills: 0,\n                    shotsFired: 0, shotsHit: 0, headshots: 0,\n                    currentWave: config.startingWave || 1,\n                    waveActive: false, waveCooldown: 0,\n                    waveEndPending: false, waveEndDelay: 0,\n                    zombiesRemaining: 0, zombiesSpawned: 0, zombiesToSpawn: 0, spawnTimer: 0,\n                    upgradeScreenOpen: false,\n                    bossToSpawn: null, bossSpawned: false\n                };\n                const roguelikeState = {\n                    ownedWeapons: [0], // indices of owned weapons\n                    upgradeHistory: [] // track applied upgrades\n                };\n                const flamethrowerUpgrades = {\n                    rangeBoost: 0,\n                    stickyFire: 0,\n                    wideNozzle: 0,\n                    fireSpread: 0,\n                    armorMelting: 0\n                };\n                const uiState = {\n                    health: null,\n                    healthText: null,\n                    score: null,\n                    weaponKey: '',\n                    lowAmmo: null,\n                    activeWeapon: -1,\n                    crosshairGap: null,\n                    hitScale: null,\n                    hitMarkerKind: null,\n                    hitMarkerFrame: -1\n                };\n\n                // Lifesteal state (percentage of damage healed)\n                const LIFESTEAL_UPGRADE_STEP = 0.12;\n                const LIFESTEAL_UPGRADE_CAP = 0.48;\n                const LIFESTEAL_UPGRADE_WEIGHT = 2;\n                const LIFESTEAL_FIRST_OFFER_CHANCE = 0.45;\n                const LIFESTEAL_REPEAT_OFFER_CHANCE = 0.25;\n                const AUTO_FIRE_UPGRADE_WEIGHT = 2;\n                const AUTO_FIRE_OFFER_CHANCE = LIFESTEAL_FIRST_OFFER_CHANCE;\n                let lifestealPercent = 0;\n\n                // ===== COMBO / STYLE RANK SYSTEM (DMC/DOOM inspired) =====\n                const comboState = {\n                    count: 0,\n                    timer: 0,        // time since last kill\n                    decayTime: 3.0,  // seconds before combo resets\n                    rank: 0,         // 0=none, 1=D, 2=C, 3=B, 4=A, 5=S, 6=SS, 7=SSS\n                    displayActive: false,\n                    pulsePending: false\n                };\n                const COMBO_RANKS = ['', 'D', 'C', 'B', 'A', 'S', 'SS', 'SSS'];\n                const COMBO_THRESHOLDS = [0, 3, 8, 15, 25, 40, 60, 85];\n                const COMBO_DECAY_TIMES = [5.0, 5.0, 4.5, 4.0, 3.5, 3.2, 3.0, 2.5];\n                const comboDisplayEl = document.getElementById('combo-display');\n                const comboRankEl = document.getElementById('combo-rank');\n                const comboCountEl = document.getElementById('combo-count');\n                const comboBarFillEl = document.getElementById('combo-bar-fill');\n                let lastComboCount = -1;\n                let lastComboRank = -1;\n                let lastComboActive = false;\n\n                function getComboRank(count) {\n                    for (let i = COMBO_THRESHOLDS.length - 1; i >= 0; i--) {\n                        if (count >= COMBO_THRESHOLDS[i]) return i;\n                    }\n                    return 0;\n                }\n\n                function registerComboKill(isHeadshot) {\n                    const bonus = isHeadshot ? 2 : 1;\n                    comboState.count += bonus;\n                    comboState.timer = 0;\n                    const newRank = getComboRank(comboState.count);\n                    if (newRank > comboState.rank) comboState.pulsePending = true;\n                    comboState.rank = newRank;\n                    comboState.displayActive = true;\n                    updateComboDisplay();\n                }\n\n                function updateComboSystem(dt) {\n                    if (comboState.count === 0) return;\n                    // Freeze combo during upgrade screen, wave-end handoff, or wave cooldown.\n                    if (gameState.upgradeScreenOpen || gameState.waveEndPending || !gameState.waveActive) return;\n                    comboState.timer += dt;\n                    const decayTime = COMBO_DECAY_TIMES[comboState.rank] || 3.0;\n                    if (comboState.timer >= decayTime) {\n                        comboState.count = 0;\n                        comboState.timer = 0;\n                        comboState.rank = 0;\n                        comboState.displayActive = false;\n                        updateComboDisplay();\n                        return;\n                    }\n                    // Update bar fill based on remaining time\n                    const remaining = 1 - (comboState.timer / decayTime);\n                    comboBarFillEl.style.transform = `scaleX(${Math.max(0, remaining)})`;\n                }\n\n                function updateComboDisplay() {\n                    const active = comboState.displayActive;\n                    if (lastComboActive !== active) {\n                        lastComboActive = active;\n                        comboDisplayEl.classList.toggle('active', active);\n                    }\n                    if (!active) return;\n\n                    const rankStr = COMBO_RANKS[comboState.rank] || 'D';\n                    if (lastComboRank !== comboState.rank) {\n                        lastComboRank = comboState.rank;\n                        comboRankEl.textContent = rankStr;\n                        comboRankEl.className = 'combo-rank rank-' + rankStr;\n                        comboBarFillEl.className = 'combo-bar-fill rank-' + rankStr;\n                    }\n\n                    if (comboState.pulsePending) {\n                        comboState.pulsePending = false;\n                        restartClassNextFrame(comboRankEl, 'pulse');\n                    }\n\n                    if (lastComboCount !== comboState.count) {\n                        lastComboCount = comboState.count;\n                        comboCountEl.textContent = comboState.count + ' ' + t('hud.hits');\n                    }\n\n                    const decayTime = COMBO_DECAY_TIMES[comboState.rank] || 3.0;\n                    const remaining = 1 - (comboState.timer / decayTime);\n                    const wantedScale = `scaleX(${Math.max(0, remaining).toFixed(3)})`;\n                    if (comboBarFillEl.style.transform !== wantedScale) {\n                        comboBarFillEl.style.transform = wantedScale;\n                    }\n                }\n\n                function resetComboSystem() {\n                    comboState.count = 0;\n                    comboState.timer = 0;\n                    comboState.rank = 0;\n                    comboState.displayActive = false;\n                    comboState.pulsePending = false;\n                    updateComboDisplay();\n                }\n\n                function applyLifesteal(damage) {\n                    if (lifestealPercent <= 0) return;\n                    const hp = world.getComponent(playerEntity, 'Health');\n                    if (!hp) return;\n                    const heal = damage * lifestealPercent;\n                    hp.current = Math.min(hp.max, hp.current + heal);\n                }\n\n                function createLifestealUpgradeOption() {\n                    const nextValue = Math.min(LIFESTEAL_UPGRADE_CAP, lifestealPercent + LIFESTEAL_UPGRADE_STEP);\n                    return {\n                        type: 'upgrade',\n                        key: 'lifesteal',\n                        name: t('upgrade.lifestealName'),\n                        desc: t('upgrade.lifestealDesc', {\n                            from: Math.round(lifestealPercent * 100),\n                            to: Math.round(nextValue * 100)\n                        }),\n                        icon: null,\n                        apply: () => { lifestealPercent = Math.min(LIFESTEAL_UPGRADE_CAP, lifestealPercent + LIFESTEAL_UPGRADE_STEP); }\n                    };\n                }\n\n                // Weapon state (mutable runtime data)\n                // penetration: how many objects bullet can pass through (0=stops on first hit)\n                // bulletSpeed: units per second the projectile travels\n                // damageDropoff: damage multiplier after each penetration\n                const weaponStates = [\n                    { name: 'PISTOL', ammo: Infinity, maxAmmo: Infinity, magSize: 12, mag: 12, damage: 25, fireRate: 0.3, reloadTime: 1.2, sound: 'gunshot_sound', reloadSound: 'pistol_reload_sound', spread: 0.014, pellets: 1, icon: 'weapon_icon_pistol', viewmodel: 'pistol_viewmodel', bulletSpeed: 90, penetration: 1, damageDropoff: 0.4, bulletSize: 0.03, bulletLength: 0.28, bulletColor: 0xffdd44, isAuto: false, isMelee: false, recoilPitch: 0.018, recoilYaw: 0.012, recoilKick: 0.9, recoilReturn: 7.5, spreadFire: 0.008, spreadMax: 0.065, spreadMove: 0.012, spreadRecover: 5.4, spreadMinFireScale: 0.42, muzzleScale: 0.85, soundMinInterval: 0.045, shellMinInterval: 0.045 },\n                    { name: 'SHOTGUN', ammo: Infinity, maxAmmo: Infinity, magSize: 6, mag: 6, damage: 18, fireRate: 0.8, reloadTime: 2.0, sound: 'shotgun_blast_sound', reloadSound: 'shotgun_reload_sound', spread: 0.095, pellets: 8, icon: 'weapon_icon_shotgun', viewmodel: 'shotgun_viewmodel', bulletSpeed: 65, penetration: 0, damageDropoff: 0, bulletSize: 0.025, bulletLength: 0.20, bulletColor: 0xffaa22, isAuto: false, isMelee: false, recoilPitch: 0.052, recoilYaw: 0.030, recoilKick: 1.75, recoilReturn: 5.0, spreadFire: 0.014, spreadMax: 0.055, spreadMove: 0.025, spreadRecover: 3.6, spreadMinFireScale: 0.48, muzzleScale: 1.55, soundMinInterval: 0.11, shellMinInterval: 0.12 },\n                    { name: 'SNIPER', ammo: Infinity, maxAmmo: Infinity, magSize: 5, mag: 5, damage: 120, fireRate: 1.5, reloadTime: 2.5, sound: 'sniper_shot_sound', reloadSound: 'sniper_reload_sound', spread: 0.003, pellets: 1, icon: 'weapon_icon_sniper', viewmodel: 'sniper_rifle_viewmodel', bulletSpeed: 160, penetration: 3, damageDropoff: 0.25, bulletSize: 0.02, bulletLength: 0.48, bulletColor: 0x88ccff, isAuto: false, isMelee: false, recoilPitch: 0.075, recoilYaw: 0.018, recoilKick: 2.15, recoilReturn: 4.0, spreadFire: 0.018, spreadMax: 0.045, spreadMove: 0.020, spreadRecover: 2.8, spreadMinFireScale: 0.55, scopeSpreadMult: 0.08, muzzleScale: 1.25, soundMinInterval: 0.14, shellMinInterval: 0.16 },\n                    { name: 'KATANA', ammo: Infinity, maxAmmo: Infinity, magSize: Infinity, mag: Infinity, damage: 70, fireRate: 0.35, reloadTime: 0, sound: 'katana_slash_sound', reloadSound: null, spread: 0, pellets: 1, icon: 'weapon_icon_katana', viewmodel: null, bulletSpeed: 0, penetration: 3, damageDropoff: 0, bulletSize: 0, bulletLength: 0, bulletColor: 0, isAuto: true, isMelee: true, meleeRange: 3.5 },\n                    { name: 'GATLING', ammo: Infinity, maxAmmo: Infinity, magSize: 150, mag: 150, damage: 15, fireRate: 0.055, reloadTime: 4.0, sound: 'gatling_fire_sound', reloadSound: 'gatling_reload_sound', spread: 0.052, pellets: 1, icon: 'weapon_icon_gatling', viewmodel: null, bulletSpeed: 80, penetration: 0, damageDropoff: 0, bulletSize: 0.025, bulletLength: 0.30, bulletColor: 0xff8844, isAuto: true, isMelee: false, recoilPitch: 0.010, recoilYaw: 0.020, recoilKick: 0.62, recoilReturn: 8.8, spreadFire: 0.0035, spreadMax: 0.085, spreadMove: 0.020, spreadRecover: 6.2, spreadMinFireScale: 0.34, muzzleScale: 1.05, muzzleSpriteScale: 1.45, soundMinInterval: 0.05, shellMinInterval: 0.035 },\n                    { name: 'LMG', ammo: Infinity, maxAmmo: Infinity, magSize: 80, mag: 80, damage: 22, fireRate: 0.1, reloadTime: 3.0, sound: 'gunshot_sound', reloadSound: 'lmg_reload_sound', spread: 0.034, pellets: 1, icon: 'weapon_icon_lmg', viewmodel: null, bulletSpeed: 85, penetration: 1, damageDropoff: 0.3, bulletSize: 0.025, bulletLength: 0.34, bulletColor: 0xffcc44, isAuto: true, isMelee: false, recoilPitch: 0.016, recoilYaw: 0.018, recoilKick: 0.82, recoilReturn: 7.2, spreadFire: 0.0052, spreadMax: 0.075, spreadMove: 0.018, spreadRecover: 5.3, spreadMinFireScale: 0.38, muzzleScale: 1.0, soundMinInterval: 0.055, shellMinInterval: 0.04 },\n                    { name: 'AUTO RIFLE', ammo: Infinity, maxAmmo: Infinity, magSize: 30, mag: 30, damage: 28, fireRate: 0.12, reloadTime: 2.0, sound: 'gunshot_sound', reloadSound: 'ar_reload_sound', spread: 0.022, pellets: 1, icon: 'weapon_icon_assault_rifle', viewmodel: null, bulletSpeed: 95, penetration: 1, damageDropoff: 0.25, bulletSize: 0.02, bulletLength: 0.36, bulletColor: 0xffdd44, isAuto: true, isMelee: false, recoilPitch: 0.020, recoilYaw: 0.014, recoilKick: 0.95, recoilReturn: 7.8, spreadFire: 0.0064, spreadMax: 0.070, spreadMove: 0.014, spreadRecover: 5.8, spreadMinFireScale: 0.40, muzzleScale: 0.95, soundMinInterval: 0.052, shellMinInterval: 0.04 },\n                    { name: 'FLAMETHROWER', ammo: Infinity, maxAmmo: Infinity, magSize: 200, mag: 200, damage: 5, fireRate: 0.04, reloadTime: 2.2, sound: 'sfx_flamethrower_loop', reloadSound: 'flamethrower_cooling', spread: 0.075, pellets: 1, icon: 'weapon_icon_flamethrower', viewmodel: null, bulletSpeed: 18, penetration: 0, damageDropoff: 0, bulletSize: 0, bulletLength: 0, bulletColor: 0, isAuto: true, isMelee: false, recoilPitch: 0.003, recoilYaw: 0.005, recoilKick: 0.12, recoilReturn: 9.5, spreadFire: 0.0015, spreadMax: 0.095, spreadMove: 0.015, spreadRecover: 5.5, spreadMinFireScale: 0.50, muzzleScale: 0.0, soundMinInterval: 0.08, shellMinInterval: 9999 }\n                ];\n                const weaponImpactProfiles = [\n                    { duration: 0.22, aliveLean: 0.11, aliveRoll: 0.07, aliveYaw: 0.035, aliveImpulse: 1.1, aliveSlow: 0.08, slowDuration: 0.16, deathImpulse: 7.0, deathLift: 2.3, deathFallSpeed: 0.66, deathFallAccel: 1.12, deathBlood: 5, cameraShakeIntensity: 0.025, cameraShakeDuration: 0.2 },\n                    { duration: 0.28, aliveLean: 0.22, aliveRoll: 0.16, aliveYaw: 0.08, aliveImpulse: 3.2, aliveSlow: 0.24, slowDuration: 0.24, deathImpulse: 20.0, deathLift: 6.1, deathFallSpeed: 1.02, deathFallAccel: 1.42, deathBlood: 20, cameraShakeIntensity: 0.08, cameraShakeDuration: 0.5 },\n                    { duration: 0.24, aliveLean: 0.18, aliveRoll: 0.08, aliveYaw: 0.05, aliveImpulse: 2.4, aliveSlow: 0.18, slowDuration: 0.18, deathImpulse: 13.0, deathLift: 4.5, deathFallSpeed: 0.92, deathFallAccel: 1.3, deathBlood: 14, cameraShakeIntensity: 0.055, cameraShakeDuration: 0.35 },\n                    { duration: 0.30, aliveLean: 0.24, aliveRoll: 0.20, aliveYaw: 0.12, aliveImpulse: 2.0, aliveSlow: 0.16, slowDuration: 0.22, deathImpulse: 6.0, deathLift: 3.2, deathFallSpeed: 0.82, deathFallAccel: 1.16, deathBlood: 18, cameraShakeIntensity: 0.04, cameraShakeDuration: 0.3 },\n                    { duration: 0.16, aliveLean: 0.075, aliveRoll: 0.065, aliveYaw: 0.03, aliveImpulse: 0.85, aliveSlow: 0.045, slowDuration: 0.12, deathImpulse: 9.2, deathLift: 1.6, deathFallSpeed: 0.84, deathFallAccel: 1.22, deathBlood: 10, cameraShakeIntensity: 0.06, cameraShakeDuration: 0.4, stack: 0.58 },\n                    { duration: 0.18, aliveLean: 0.095, aliveRoll: 0.075, aliveYaw: 0.035, aliveImpulse: 1.05, aliveSlow: 0.07, slowDuration: 0.14, deathImpulse: 10.4, deathLift: 2.5, deathFallSpeed: 0.82, deathFallAccel: 1.18, deathBlood: 8, cameraShakeIntensity: 0.045, cameraShakeDuration: 0.3, stack: 0.68 },\n                    { duration: 0.20, aliveLean: 0.12, aliveRoll: 0.085, aliveYaw: 0.04, aliveImpulse: 1.35, aliveSlow: 0.09, slowDuration: 0.15, deathImpulse: 10.8, deathLift: 2.8, deathFallSpeed: 0.82, deathFallAccel: 1.2, deathBlood: 8, cameraShakeIntensity: 0.045, cameraShakeDuration: 0.3, stack: 0.72 },\n                    { duration: 0.12, aliveLean: 0.02, aliveRoll: 0.01, aliveYaw: 0.01, aliveImpulse: 0.25, aliveSlow: 0.45, slowDuration: 1.50, deathImpulse: 2.2, deathLift: 0.8, deathFallSpeed: 0.45, deathFallAccel: 1.0, deathBlood: 1, cameraShakeIntensity: 0.01, cameraShakeDuration: 0.1 }\n                ];\n                const initialWeaponState = weaponStates.map(ws => ({\n                    ammo: ws.ammo,\n                    mag: ws.mag,\n                    magSize: ws.magSize\n                }));\n                const originalWeaponStats = weaponStates.map(ws => ({\n                    damage: ws.damage, fireRate: ws.fireRate, reloadTime: ws.reloadTime,\n                    magSize: ws.magSize, penetration: ws.penetration, meleeRange: ws.meleeRange,\n                    ammo: ws.ammo, maxAmmo: ws.maxAmmo, mag: ws.mag, spread: ws.spread\n                }));\n                initWeaponDerivedStats(weaponStates.length);\n                weaponStates.forEach((ws, i) => updateWeaponDerivedStats(i, ws));\n\n                // ===== ZOMBIE TYPES (10 regular + 5 boss) =====\n                const zombieTypes = [\n                    { id: 0, name: 'Walker', healthMult: 1.0, speedMult: 0.8, damageMult: 1.0, scaleMult: 1.0, skinColor: 0x4a5040, clothColor: 0x2a2a2a, eyeColor: 0xff2222, minWave: 1 },\n                    { id: 1, name: 'Runner', healthMult: 0.6, speedMult: 1.6, damageMult: 0.8, scaleMult: 0.9, skinColor: 0x5a5550, clothColor: 0x1a2a1a, eyeColor: 0xffaa00, minWave: 2 },\n                    { id: 2, name: 'Tank', healthMult: 2.5, speedMult: 0.5, damageMult: 1.5, scaleMult: 1.3, skinColor: 0x3a4a30, clothColor: 0x3a3a2a, eyeColor: 0xff0000, minWave: 3 },\n                    { id: 3, name: 'Crawler', healthMult: 0.7, speedMult: 1.3, damageMult: 0.7, scaleMult: 0.85, skinColor: 0x5a4a40, clothColor: 0x2a1a1a, eyeColor: 0xff4444, minWave: 4 },\n                    { id: 4, name: 'Berserker', healthMult: 1.3, speedMult: 1.5, damageMult: 1.8, scaleMult: 1.1, skinColor: 0x6a3030, clothColor: 0x2a0a0a, eyeColor: 0xff0044, minWave: 5 },\n                    { id: 5, name: 'Armored', healthMult: 3.0, speedMult: 0.55, damageMult: 1.3, scaleMult: 1.25, skinColor: 0x3a3a4a, clothColor: 0x4a4a4a, eyeColor: 0x4444ff, minWave: 7 },\n                    { id: 6, name: 'Swarm', healthMult: 0.35, speedMult: 2.0, damageMult: 0.4, scaleMult: 0.7, skinColor: 0x5a5a40, clothColor: 0x1a1a0a, eyeColor: 0xffff00, minWave: 6 },\n                    { id: 7, name: 'Bloater', healthMult: 2.0, speedMult: 0.4, damageMult: 2.2, scaleMult: 1.4, skinColor: 0x4a6a30, clothColor: 0x2a3a1a, eyeColor: 0x00ff44, minWave: 8 },\n                    { id: 8, name: 'Stalker', healthMult: 1.1, speedMult: 1.4, damageMult: 1.4, scaleMult: 1.0, skinColor: 0x3a3040, clothColor: 0x1a1a2a, eyeColor: 0xaa00ff, minWave: 9 },\n                    { id: 9, name: 'Screamer', healthMult: 0.6, speedMult: 1.7, damageMult: 1.0, scaleMult: 0.85, skinColor: 0x6a5a50, clothColor: 0x3a2a2a, eyeColor: 0xff8800, minWave: 10 }\n                ];\n                const bossTypes = [\n                    { id: 0, name: 'BRUTE', healthMult: 15, speedMult: 0.55, damageMult: 3.0, scaleMult: 1.9, skinColor: 0x2a4020, clothColor: 0x1a2a0a, eyeColor: 0xff0000, wave: 5 },\n                    { id: 1, name: 'CHARGER', healthMult: 12, speedMult: 1.7, damageMult: 4.0, scaleMult: 1.6, skinColor: 0x6a2020, clothColor: 0x3a0a0a, eyeColor: 0xff4400, wave: 10 },\n                    { id: 2, name: 'BEHEMOTH', healthMult: 25, speedMult: 0.35, damageMult: 5.0, scaleMult: 2.2, skinColor: 0x3a3a3a, clothColor: 0x2a2a2a, eyeColor: 0x4444ff, wave: 15 },\n                    { id: 3, name: 'INFERNAL', healthMult: 20, speedMult: 0.9, damageMult: 4.5, scaleMult: 1.8, skinColor: 0x5a2010, clothColor: 0x4a1a0a, eyeColor: 0xff8800, wave: 20 },\n                    { id: 4, name: 'OVERLORD', healthMult: 40, speedMult: 0.7, damageMult: 6.0, scaleMult: 2.5, skinColor: 0x1a0a2a, clothColor: 0x0a0a1a, eyeColor: 0xaa00ff, wave: 25 }\n                ];\n\n                // Per-type body proportions [scaleX, scaleY, scaleZ] for each body part\n                const zombieTypeProportions = {\n                    'Walker': { body: [1, 1, 1], head: [1, 1, 1], jaw: [1, 1, 1], arm: [1, 1, 1], hand: [1, 1, 1], leg: [1, 1, 1], foot: [1, 1, 1] },\n                    'Runner': { body: [0.72, 0.92, 0.68], head: [0.85, 0.88, 0.9], jaw: [0.9, 1, 1], arm: [0.7, 1.25, 0.7], hand: [0.8, 1.1, 0.8], leg: [0.72, 1.25, 0.72], foot: [0.85, 1, 0.95] },\n                    'Tank': { body: [1.45, 1.25, 1.15], head: [1.35, 1.15, 1.25], jaw: [1.35, 1.25, 1.2], arm: [1.8, 1.15, 1.8], hand: [1.6, 1.4, 1.6], leg: [1.55, 1.08, 1.55], foot: [1.45, 1.25, 1.35] },\n                    'Crawler': { body: [1.15, 0.75, 1.35], head: [1.08, 1, 1.08], jaw: [1, 1, 1], arm: [1.3, 1.35, 1.3], hand: [1.35, 1.25, 1.35], leg: [0.85, 0.75, 0.85], foot: [1, 0.9, 1] },\n                    'Berserker': { body: [1.4, 1.12, 1.3], head: [1.2, 1.12, 1.12], jaw: [1.25, 1.35, 1.15], arm: [1.7, 1.2, 1.7], hand: [1.6, 1.5, 1.6], leg: [1.2, 1.02, 1.2], foot: [1.15, 1.05, 1.15] },\n                    'Swarm': { body: [1, 1, 1], head: [1.35, 1.35, 1.35], jaw: [1.25, 1.2, 1.25], arm: [0.88, 1, 0.88], hand: [1, 1, 1], leg: [1, 1, 1], foot: [1, 1, 1] },\n                    'Armored': { body: [1.55, 1.18, 1.65], head: [1.12, 1.05, 1.12], jaw: [1, 1, 1], arm: [1.45, 1.08, 1.45], hand: [1.35, 1.25, 1.35], leg: [1.4, 1.08, 1.4], foot: [1.25, 1.15, 1.25] },\n                    'Bloater': { body: [1.25, 1.15, 1.1], head: [1.05, 0.95, 1.05], jaw: [1.12, 1, 1.12], arm: [1.12, 0.82, 1.12], hand: [1.25, 1.12, 1.25], leg: [1.3, 0.78, 1.3], foot: [1.35, 1, 1.35] },\n                    'Stalker': { body: [0.78, 1.18, 0.72], head: [0.82, 1.3, 1.18], jaw: [0.88, 1.12, 1.25], arm: [0.75, 1.25, 0.75], hand: [0.88, 1.12, 0.88], leg: [0.78, 1.2, 0.78], foot: [0.88, 1, 1.12] },\n                    'Screamer': { body: [0.82, 1.02, 0.78], head: [1.12, 1.08, 1.02], jaw: [1.5, 1.6, 1.4], arm: [0.78, 1.05, 0.78], hand: [0.88, 1, 0.88], leg: [0.82, 1.05, 0.82], foot: [0.88, 1, 0.88] },\n                    'BRUTE': { body: [1.55, 1.22, 1.45], head: [1.35, 1.22, 1.35], jaw: [1.35, 1.35, 1.35], arm: [1.85, 1.28, 1.85], hand: [1.75, 1.55, 1.75], leg: [1.55, 1.12, 1.55], foot: [1.45, 1.25, 1.45] },\n                    'CHARGER': { body: [1.12, 1.02, 1.28], head: [1.02, 1.02, 1.18], jaw: [1.12, 1.12, 1.22], arm: [1.12, 1.12, 1.12], hand: [1.12, 1.12, 1.12], leg: [1.12, 1.35, 1.12], foot: [1.12, 1, 1.25] },\n                    'BEHEMOTH': { body: [1.45, 1.3, 1.25], head: [1.45, 1.22, 1.45], jaw: [1.35, 1.25, 1.35], arm: [1.75, 1.18, 1.75], hand: [1.65, 1.45, 1.65], leg: [1.65, 0.88, 1.65], foot: [1.55, 1.25, 1.55] },\n                    'INFERNAL': { body: [1.22, 1.12, 1.12], head: [1.12, 1.12, 1.12], jaw: [1.12, 1.22, 1.12], arm: [1.25, 1.18, 1.25], hand: [1.12, 1.12, 1.12], leg: [1.12, 1.12, 1.12], foot: [1.12, 1, 1.12] },\n                    'OVERLORD': { body: [1.35, 1.38, 1.22], head: [1.55, 1.45, 1.55], jaw: [1.45, 1.35, 1.45], arm: [1.22, 1.42, 1.22], hand: [1.35, 1.35, 1.35], leg: [1.12, 1.38, 1.12], foot: [1.12, 1, 1.12] }\n                };\n                const defaultProportions = { body: [1, 1, 1], head: [1, 1, 1], jaw: [1, 1, 1], arm: [1, 1, 1], hand: [1, 1, 1], leg: [1, 1, 1], foot: [1, 1, 1] };\n\n                const ZOMBIE_SEPARATION_SKIN = 0.035;\n                const ZOMBIE_SEPARATION_STRENGTH = 0.72;\n                const ZOMBIE_SEPARATION_MAX_RADIUS = 1.75;\n                const ZOMBIE_WORLD_MIN_X = -10.45;\n                const ZOMBIE_WORLD_MAX_X = 10.45;\n                const ZOMBIE_WORLD_MIN_Z = -18.6;\n                const ZOMBIE_WORLD_MAX_Z = 18.6;\n                const zombieStaticBlockers = [\n                    { x: -8, z: 5, hw: 0.15, hd: 0.15 },\n                    { x: -4, z: 5, hw: 0.15, hd: 0.15 },\n                    { x: 4, z: 5, hw: 0.15, hd: 0.15 },\n                    { x: 8, z: 5, hw: 0.15, hd: 0.15 },\n                    { x: -8, z: 12, hw: 0.5, hd: 0.3 },\n                    { x: 8, z: 12, hw: 0.5, hd: 0.3 }\n                ];\n\n                function createZombieCollider(typeName, scale, isBoss) {\n                    const props = zombieTypeProportions[typeName] || defaultProportions;\n                    const body = props.body || defaultProportions.body;\n                    const leg = props.leg || defaultProportions.leg;\n                    const width = Math.max(body[0] || 1, body[2] || 1, (leg[0] || 1) * 0.72);\n                    let radius = 0.44 * scale * width;\n                    if (zombieTypeHasBelly(typeName)) {\n                        radius = Math.max(radius, scale * (typeName === 'BEHEMOTH' ? 0.72 : 0.64));\n                    }\n                    radius = Math.max(0.32, Math.min(ZOMBIE_SEPARATION_MAX_RADIUS, radius));\n                    const mass = Math.max(0.18, radius * radius * (isBoss ? 4.0 : 1.0));\n                    return { radius, invMass: 1 / mass };\n                }\n\n                function resolveZombieStaticPoint(out, x, z, radius) {\n                    out.x = Math.max(ZOMBIE_WORLD_MIN_X + radius, Math.min(ZOMBIE_WORLD_MAX_X - radius, x));\n                    out.z = Math.max(ZOMBIE_WORLD_MIN_Z + radius, Math.min(ZOMBIE_WORLD_MAX_Z - radius, z));\n\n                    for (let i = 0; i < zombieStaticBlockers.length; i++) {\n                        const col = zombieStaticBlockers[i];\n                        const dx = out.x - col.x;\n                        const dz = out.z - col.z;\n                        const hw = col.hw + radius;\n                        const hd = col.hd + radius;\n                        if (Math.abs(dx) >= hw || Math.abs(dz) >= hd) continue;\n                        const overlapX = hw - Math.abs(dx);\n                        const overlapZ = hd - Math.abs(dz);\n                        if (overlapX < overlapZ) out.x += (dx < 0 ? -overlapX : overlapX);\n                        else out.z += (dz < 0 ? -overlapZ : overlapZ);\n                        out.x = Math.max(ZOMBIE_WORLD_MIN_X + radius, Math.min(ZOMBIE_WORLD_MAX_X - radius, out.x));\n                        out.z = Math.max(ZOMBIE_WORLD_MIN_Z + radius, Math.min(ZOMBIE_WORLD_MAX_Z - radius, out.z));\n                    }\n                    return out;\n                }\n\n                function resolveZombieStaticBlockers(pos, radius) {\n                    return resolveZombieStaticPoint(pos, pos.x, pos.z, radius);\n                }\n\n                function clearZombieSeparationCells(soa) {\n                    soa.head.fill(-1);\n                }\n\n                function rebuildZombieSeparationCells(soa, count) {\n                    clearZombieSeparationCells(soa);\n                    for (let i = 0; i < count; i++) {\n                        const slot = soa.slots[i];\n                        const cx = zombieGridCoord(zombieRuntime.x[slot], ZOMBIE_GRID_MIN_X, ZOMBIE_GRID_WIDTH);\n                        const cz = zombieGridCoord(zombieRuntime.z[slot], ZOMBIE_GRID_MIN_Z, ZOMBIE_GRID_HEIGHT);\n                        const cell = cz * ZOMBIE_GRID_WIDTH + cx;\n                        soa.next[i] = soa.head[cell];\n                        soa.head[cell] = i;\n                    }\n                }\n\n                function queryZombieSeparationCells(soa, minX, maxX, minZ, maxZ, out) {\n                    out.length = 0;\n                    const minCx = zombieGridCoord(minX, ZOMBIE_GRID_MIN_X, ZOMBIE_GRID_WIDTH);\n                    const maxCx = zombieGridCoord(maxX, ZOMBIE_GRID_MIN_X, ZOMBIE_GRID_WIDTH);\n                    const minCz = zombieGridCoord(minZ, ZOMBIE_GRID_MIN_Z, ZOMBIE_GRID_HEIGHT);\n                    const maxCz = zombieGridCoord(maxZ, ZOMBIE_GRID_MIN_Z, ZOMBIE_GRID_HEIGHT);\n                    for (let cx = minCx; cx <= maxCx; cx++) {\n                        for (let cz = minCz; cz <= maxCz; cz++) {\n                            let node = soa.head[cz * ZOMBIE_GRID_WIDTH + cx];\n                            while (node !== -1) {\n                                out.push(node);\n                                node = soa.next[node];\n                            }\n                        }\n                    }\n                    return out;\n                }\n\n                function pickZombieTypeForWave(wave) {\n                    let totalWeight = 0;\n                    let lastAvailable = null;\n                    for (let i = 0; i < zombieTypes.length; i++) {\n                        const t = zombieTypes[i];\n                        if (wave < t.minWave) continue;\n                        lastAvailable = t;\n                        const freshness = Math.max(0, wave - t.minWave);\n                        totalWeight += 1 + Math.min(freshness * 0.3, 2);\n                    }\n                    if (!lastAvailable) return zombieTypes[0];\n                    let r = Math.random() * totalWeight;\n                    for (let i = 0; i < zombieTypes.length; i++) {\n                        const t = zombieTypes[i];\n                        if (wave < t.minWave) continue;\n                        const freshness = Math.max(0, wave - t.minWave);\n                        r -= 1 + Math.min(freshness * 0.3, 2);\n                        if (r <= 0) return t;\n                    }\n                    return lastAvailable;\n                }\n\n                function getBossForWave(wave) {\n                    // Bosses appear every 5 waves\n                    if (wave % 5 !== 0) return null;\n                    const bossIndex = Math.min(Math.floor(wave / 5) - 1, bossTypes.length - 1);\n                    if (bossIndex < 0) return null;\n                    return bossTypes[bossIndex];\n                }\n\n                function getZombieTypeDefinition(typeName) {\n                    return zombieTypes.find(t => t.name === typeName) || bossTypes.find(t => t.name === typeName) || zombieTypes[0];\n                }\n\n                function configureZombieProxyMesh(mesh, typeName, isBoss = false) {\n                    if (!mesh) return null;\n                    const zType = getZombieTypeDefinition(typeName);\n                    const scale = zType.scaleMult || 1;\n                    mesh.scale.set(scale, scale, scale);\n                    mesh.userData.typeName = zType.name;\n\n                    const pSkin = zType.skinColor || 0x4a5040;\n                    const pDark = (pSkin & 0xfefefe) >> 1;\n                    const pCloth = zType.clothColor || 0x2a2a2a;\n                    const pEye = zType.eyeColor || 0xff2222;\n                    const pBelt = isBoss ? 0x4a3a1a : 0x2a1a0a;\n                    const pBelly = zType.name === 'Bloater' ? 0x5a7a3a : (zType.name === 'BEHEMOTH' ? 0x3a4a3a : pSkin);\n\n                    mesh.userData.palette = {\n                        skin: pSkin,\n                        darkSkin: pDark,\n                        cloth: pCloth,\n                        bone: 0xccbb99,\n                        stump: 0x8a2020,\n                        eye: pEye,\n                        belt: pBelt,\n                        belly: pBelly\n                    };\n\n                    const mats = mesh.userData.materials;\n                    if (mats) {\n                        mats.skin.color.setHex(pSkin);\n                        mats.darkSkin.color.setHex(pDark);\n                        mats.cloth.color.setHex(pCloth);\n                        mats.eye.color.setHex(pEye);\n                        mats.belt.color.setHex(pBelt);\n                        if (mats.belly) mats.belly.color.setHex(pBelly);\n                    }\n                    return mesh;\n                }\n\n                function acquireZombieProxyForSlot(slot) {\n                    if (slot < 0) return null;\n                    let mesh = zombieRuntime.meshRef[slot];\n                    if (!mesh) {\n                        mesh = zombieMeshPool.acquire();\n                        zombieRuntime.meshRef[slot] = mesh;\n                        if (zombieRuntime.zombieFacade[slot]) zombieRuntime.zombieFacade[slot].meshRef = mesh;\n                        maybeTopUpZombieProxyPool();\n                    }\n                    configureZombieProxyMesh(mesh, zombieRuntime.typeName[slot], zombieRuntime.isBoss[slot] !== 0);\n                    setProxyMeshRenderable(mesh, false);\n                    syncZombieBodyParts(mesh, zombieRuntime.bodyPartsFacade[slot]);\n                    return mesh;\n                }\n\n                function getWaveZombieCount(wave) {\n                    const base = 10 + wave * 3 + Math.pow(wave, 1.6) * 1.0;\n                    return Math.min(Math.floor(base), MAX_ACTIVE_ZOMBIES);\n                }\n\n                function getWaveSpawnInterval(wave) {\n                    return Math.max(0.12, 0.55 - wave * 0.024);\n                }\n\n                // Input state (unidirectional: raw input -> input state -> systems)\n                const input = { keys: {}, yawDelta: 0, pitchDelta: 0, firing: false, holdingFire: false, touchHoldingFire: false, reload: false, switchTo: -1, backpackToggle: false };\n                const PLAYER_RADIUS = 0.35;\n                const playerStaticColliders = [\n                    // Pillars\n                    { x: -8, z: 5, hw: 0.15 + PLAYER_RADIUS, hd: 0.15 + PLAYER_RADIUS },\n                    { x: -4, z: 5, hw: 0.15 + PLAYER_RADIUS, hd: 0.15 + PLAYER_RADIUS },\n                    { x: 4, z: 5, hw: 0.15 + PLAYER_RADIUS, hd: 0.15 + PLAYER_RADIUS },\n                    { x: 8, z: 5, hw: 0.15 + PLAYER_RADIUS, hd: 0.15 + PLAYER_RADIUS },\n                    // Crates\n                    { x: -8, z: 12, hw: 0.5 + PLAYER_RADIUS, hd: 0.3 + PLAYER_RADIUS },\n                    { x: 8, z: 12, hw: 0.5 + PLAYER_RADIUS, hd: 0.3 + PLAYER_RADIUS },\n                    // Front wall sections (solid parts between archways)\n                    { x: -10, z: -12, hw: 1.0 + PLAYER_RADIUS, hd: 0.25 + PLAYER_RADIUS },\n                    { x: -3.5, z: -12, hw: 1.5 + PLAYER_RADIUS, hd: 0.25 + PLAYER_RADIUS },\n                    { x: 3.5, z: -12, hw: 1.5 + PLAYER_RADIUS, hd: 0.25 + PLAYER_RADIUS },\n                    { x: 10, z: -12, hw: 1.0 + PLAYER_RADIUS, hd: 0.25 + PLAYER_RADIUS },\n                    // Corridor walls\n                    { x: -9, z: -16, hw: 0.15 + PLAYER_RADIUS, hd: 4.0 + PLAYER_RADIUS },\n                    { x: -5, z: -16, hw: 0.15 + PLAYER_RADIUS, hd: 4.0 + PLAYER_RADIUS },\n                    { x: -2, z: -16, hw: 0.15 + PLAYER_RADIUS, hd: 4.0 + PLAYER_RADIUS },\n                    { x: 2, z: -16, hw: 0.15 + PLAYER_RADIUS, hd: 4.0 + PLAYER_RADIUS },\n                    { x: 5, z: -16, hw: 0.15 + PLAYER_RADIUS, hd: 4.0 + PLAYER_RADIUS },\n                    { x: 9, z: -16, hw: 0.15 + PLAYER_RADIUS, hd: 4.0 + PLAYER_RADIUS },\n                    // Side walls\n                    { x: -11, z: 0, hw: 0.25 + PLAYER_RADIUS, hd: 20 },\n                    { x: 11, z: 0, hw: 0.25 + PLAYER_RADIUS, hd: 20 },\n                    // Back wall\n                    { x: 0, z: 20, hw: 11, hd: 0.25 + PLAYER_RADIUS }\n                ];\n                const inputScratch = {\n                    fwd: new THREE.Vector3(),\n                    right: new THREE.Vector3(),\n                    yawQuat: new THREE.Quaternion(),\n                    yawEuler: new THREE.Euler()\n                };\n                let pointerLocked = false;\n                let isTouchDevice = detectedTouchDevice;\n\n                // WASD display for PC\n                const wasdDisplay = document.getElementById('wasd-display');\n                const wasdKeys = {\n                    w: document.getElementById('wasd-w'),\n                    a: document.getElementById('wasd-a'),\n                    s: document.getElementById('wasd-s'),\n                    d: document.getElementById('wasd-d')\n                };\n                let wasdStateMask = -1;\n                if (!detectedTouchDevice) {\n                    wasdDisplay.classList.add('pc-visible');\n                }\n                function updateWasdDisplay() {\n                    if (detectedTouchDevice || !gameState.started || gameState.over) return;\n                    const mask =\n                        ((input.keys['w'] || input.keys['arrowup']) ? 1 : 0) |\n                        ((input.keys['a'] || input.keys['arrowleft']) ? 2 : 0) |\n                        ((input.keys['s'] || input.keys['arrowdown']) ? 4 : 0) |\n                        ((input.keys['d'] || input.keys['arrowright']) ? 8 : 0);\n                    if (mask === wasdStateMask) return;\n                    wasdStateMask = mask;\n                    wasdKeys.w.classList.toggle('active', !!(mask & 1));\n                    wasdKeys.a.classList.toggle('active', !!(mask & 2));\n                    wasdKeys.s.classList.toggle('active', !!(mask & 4));\n                    wasdKeys.d.classList.toggle('active', !!(mask & 8));\n                }\n                let scopeZoomFov = 25;\n                let scopeOverlay = null;\n\n                // Create scope overlay\n                (function createScopeOverlay() {\n                    scopeOverlay = document.createElement('div');\n                    scopeOverlay.id = 'scope-overlay';\n                    scopeOverlay.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:1;';\n                    scopeOverlay.innerHTML = `\n                    <div style=\"position:absolute;top:0;left:0;width:100%;height:100%;background:radial-gradient(circle at center, transparent 35%, rgba(0,0,0,0.85) 65%);\"></div>\n                    <div style=\"position:absolute;top:50%;left:0;right:0;height:1px;background:rgba(0,200,255,0.6);\"></div>\n                    <div style=\"position:absolute;left:50%;top:0;bottom:0;width:1px;background:rgba(0,200,255,0.6);\"></div>\n                    <div style=\"position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:8px;height:8px;border:1px solid rgba(0,200,255,0.8);border-radius:50%;\"></div>\n                `;\n                    const hud = document.getElementById('hud');\n                    hud.insertBefore(scopeOverlay, hud.firstChild);\n                })();\n                let leftTouch = null, rightTouch = null;\n                let shootJoystickVisible = false;\n                let shootJoystickDirty = true;\n                const shootJoystickVisual = { baseX: 0, baseY: 0, dx: 0, dy: 0 };\n                const leftJoystickBase = { x: 0, y: 0 };\n                const rightJoystickBase = { x: 0, y: 0 };\n                const leftJoystickValue = { x: 0, y: 0 };\n                const rightJoystickValue = { x: 0, y: 0 };\n                let touchFiring = false;\n\n                // Web Worker AI results\n                let pendingAIResults = null;\n                let pendingAIResultsPacked = null;\n                let aiRequestPending = false;\n                let aiGeneration = 0;\n                let aiRequestStartedAt = 0;\n                let aiWorkerFailureCount = 0;\n                let aiFallbackSliceCursor = 0;\n                let aiInputIdsBuffer = new Int32Array(ZOMBIE_POOL_SIZE);\n                let aiInputPackedBuffer = new Float32Array(ZOMBIE_POOL_SIZE * 3);\n                let aiOutputIdsBuffer = new Int32Array(ZOMBIE_POOL_SIZE);\n                let aiOutputDataBuffer = new Float32Array(ZOMBIE_POOL_SIZE * 5);\n                const aiResultPackedView = { count: 0, ids: null, data: null, generation: 0 };\n                function renewAITransferBuffers() {\n                    aiInputIdsBuffer = new Int32Array(ZOMBIE_POOL_SIZE);\n                    aiInputPackedBuffer = new Float32Array(ZOMBIE_POOL_SIZE * 3);\n                    aiOutputIdsBuffer = new Int32Array(ZOMBIE_POOL_SIZE);\n                    aiOutputDataBuffer = new Float32Array(ZOMBIE_POOL_SIZE * 5);\n                }\n                function resetAIWorkerAfterFailure() {\n                    aiWorkerFailureCount++;\n                    perfBudget.level = Math.max(perfBudget.level || 0, aiWorkerFailureCount > 1 ? 3 : 2);\n                    runtimePerfVisualScale = getPerfAdjustedVisualScale();\n                    applyPerfPressureClass();\n                    pendingAIResults = null;\n                    pendingAIResultsPacked = null;\n                    aiRequestPending = false;\n                    aiRequestStartedAt = 0;\n                    renewAITransferBuffers();\n                    if (aiWorker) {\n                        disposeAIWorker(aiWorker);\n                        aiWorker = null;\n                    }\n                    try {\n                        aiWorker = createAIWorker();\n                        attachAIWorkerHandlers();\n                    } catch (e) {\n                        aiWorker = null;\n                    }\n                }\n                function attachAIWorkerHandlers() {\n                    if (!aiWorker) return;\n                    aiWorker.onmessage = (e) => {\n                        if (e.data.type === 'aiResultPacked') {\n                            aiInputIdsBuffer = e.data.inputIds;\n                            aiInputPackedBuffer = e.data.inputPacked;\n                            aiOutputIdsBuffer = e.data.ids;\n                            aiOutputDataBuffer = e.data.data;\n                            aiRequestPending = false;\n                            aiRequestStartedAt = 0;\n                            if (e.data.generation !== aiGeneration) return;\n                            aiWorkerFailureCount = Math.max(0, aiWorkerFailureCount - 1);\n                            aiResultPackedView.count = e.data.count || e.data.ids.length;\n                            aiResultPackedView.ids = e.data.ids;\n                            aiResultPackedView.data = e.data.data;\n                            aiResultPackedView.generation = e.data.generation;\n                            pendingAIResultsPacked = aiResultPackedView;\n                        } else if (e.data.type === 'aiResult') {\n                            if (e.data.generation !== aiGeneration) {\n                                aiRequestPending = false;\n                                aiRequestStartedAt = 0;\n                                return;\n                            }\n                            pendingAIResults = e.data.data;\n                            aiWorkerFailureCount = Math.max(0, aiWorkerFailureCount - 1);\n                            aiRequestPending = false;\n                            aiRequestStartedAt = 0;\n                        }\n                    };\n                    aiWorker.onerror = () => resetAIWorkerAfterFailure();\n                }\n                attachAIWorkerHandlers();\n\n                // ===== ECS SYSTEMS =====\n\n                // System 1: Input -> Player state\n                function inputSystem(w, dt) {\n                    const player = w.getComponent(playerEntity, 'Player');\n                    const pos = w.getComponent(playerEntity, 'Position');\n\n                    const wComp = w.getComponent(playerEntity, 'Weapon');\n                    if (wComp) {\n                        if (input.pitchDelta < 0 && wComp.recoilPitchDebt > 0) {\n                            wComp.recoilPitchDebt = Math.max(0, wComp.recoilPitchDebt + input.pitchDelta);\n                        }\n                        if (wComp.recoilYawDebt > 0 && input.yawDelta < 0) {\n                            wComp.recoilYawDebt = Math.max(0, wComp.recoilYawDebt + input.yawDelta);\n                        } else if (wComp.recoilYawDebt < 0 && input.yawDelta > 0) {\n                            wComp.recoilYawDebt = Math.min(0, wComp.recoilYawDebt + input.yawDelta);\n                        }\n                    }\n\n                    player.yaw += input.yawDelta;\n                    player.pitch += input.pitchDelta;\n                    player.pitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, player.pitch));\n                    input.yawDelta = 0; input.pitchDelta = 0;\n\n\n                    // Movement\n                    inputScratch.yawEuler.set(0, player.yaw, 0);\n                    inputScratch.yawQuat.setFromEuler(inputScratch.yawEuler);\n                    const fwd = inputScratch.fwd.set(0, 0, -1).applyQuaternion(inputScratch.yawQuat);\n                    const rgt = inputScratch.right.set(1, 0, 0).applyQuaternion(inputScratch.yawQuat);\n                    let mx = 0, mz = 0;\n                    if (input.keys['w'] || input.keys['arrowup']) mz += 1;\n                    if (input.keys['s'] || input.keys['arrowdown']) mz -= 1;\n                    if (input.keys['a'] || input.keys['arrowleft']) mx -= 1;\n                    if (input.keys['d'] || input.keys['arrowright']) mx += 1;\n                    if (isTouchDevice) { mx += leftJoystickValue.x; mz -= leftJoystickValue.y; }\n\n                    const spd = (input.keys['shift'] ? 7.5 : 5) * dt;\n                    pos.x += fwd.x * mz * spd + rgt.x * mx * spd;\n                    pos.z += fwd.z * mz * spd + rgt.z * mx * spd;\n                    pos.x = Math.max(-10.4, Math.min(10.4, pos.x));\n                    pos.z = Math.max(-11.5, Math.min(19.4, pos.z));\n                    // Player vs pillar/crate collision\n                    for (let i = 0; i < playerStaticColliders.length; i++) {\n                        const col = playerStaticColliders[i];\n                        const dx = pos.x - col.x;\n                        const dz = pos.z - col.z;\n                        if (Math.abs(dx) < col.hw && Math.abs(dz) < col.hd) {\n                            const overlapX = col.hw - Math.abs(dx);\n                            const overlapZ = col.hd - Math.abs(dz);\n                            if (overlapX < overlapZ) pos.x += Math.sign(dx) * overlapX;\n                            else pos.z += Math.sign(dz) * overlapZ;\n                        }\n                    }\n                }\n\n                let hitStopTimer = 0;\n\n                function getKatanaCutInfo(slashCount, fwdDir, slashRight) {\n                    const up = effectScratch.v8.set(0, 1, 0).applyQuaternion(camera.quaternion).normalize();\n                    const travel = effectScratch.v9;\n                    if (slashCount === 1) {\n                        travel.copy(slashRight).multiplyScalar(-0.72).addScaledVector(up, -0.70).normalize();\n                    } else if (slashCount === 2) {\n                        travel.copy(slashRight).normalize();\n                    } else {\n                        travel.copy(up).multiplyScalar(-1).normalize();\n                    }\n                    const screenX = travel.dot(slashRight);\n                    const screenY = travel.dot(up);\n                    return {\n                        combo: slashCount,\n                        travelX: travel.x,\n                        travelY: travel.y,\n                        travelZ: travel.z,\n                        angle: Math.atan2(screenY, screenX),\n                        power: slashCount === 0 ? 1.35 : (slashCount === 1 ? 1.08 : 1.0)\n                    };\n                }\n\n                function getZombieLocalCutInfo(baseCutInfo, zombieYaw) {\n                    const localRight = effectScratch.v8.set(1, 0, 0)\n                        .applyEuler(effectScratch.e1.set(0, zombieYaw, 0))\n                        .normalize();\n                    const localAngle = Math.atan2(\n                        baseCutInfo.travelY,\n                        baseCutInfo.travelX * localRight.x + baseCutInfo.travelY * localRight.y + baseCutInfo.travelZ * localRight.z\n                    );\n                    return {\n                        combo: baseCutInfo.combo,\n                        travelX: baseCutInfo.travelX,\n                        travelY: baseCutInfo.travelY,\n                        travelZ: baseCutInfo.travelZ,\n                        angle: localAngle,\n                        power: baseCutInfo.power\n                    };\n                }\n\n                function selectKatanaBodyPartByBladeLineForValues(zombieX, zombieY, zombieZ, zombieYaw, bodyParts, cutInfo, fwdDir, slashRight, scale, props, pose) {\n                    if (!bodyParts || !cutInfo) return null;\n                    const bladeUp = effectScratch.v8.set(0, 1, 0).applyQuaternion(camera.quaternion).normalize();\n                    const dirX = Math.cos(cutInfo.angle || 0);\n                    const dirY = Math.sin(cutInfo.angle || 0);\n                    let best = null;\n\n                    for (let hbIdx = 0; hbIdx < bodyPartHitboxes.length; hbIdx++) {\n                        const hb = bodyPartHitboxes[hbIdx];\n                        if (!zombiePartAlive(bodyParts, hb.name)) continue;\n                        const center = getHitboxWorldCenterForValues(effectScratch.v10, zombieX, zombieY, zombieZ, zombieYaw, hb, scale, props, bodyParts, pose);\n                        const rel = effectScratch.v11.copy(center).sub(camera.position);\n                        const forwardDepth = rel.dot(fwdDir);\n                        if (forwardDepth < 0.2) continue;\n\n                        const sx = rel.dot(slashRight);\n                        const sy = rel.dot(bladeUp);\n                        const along = sx * dirX + sy * dirY;\n                        const across = Math.abs(sx * -dirY + sy * dirX);\n                        const metrics = getHitboxMetricsForValues(hb, scale, props, bodyParts, pose, hitboxScratch);\n                        const reach = Math.max(metrics.halfX, metrics.halfY) + (hb.name === 'body' ? 0.18 : 0.34);\n                        if (Math.abs(along) > 1.45 || across > reach) continue;\n\n                        const limbBias = hb.name === 'body' ? 0.22 : -0.16;\n                        const headBias = hb.name === 'head' && cutInfo.combo === 0 ? -0.12 : 0;\n                        const score = across + Math.abs(forwardDepth - 2.1) * 0.08 + limbBias + headBias;\n                        if (!best || score < best.score) best = { part: hb.name, score };\n                    }\n\n                    return best ? best.part : null;\n                }\n\n                function selectKatanaBodyPartByBladeLine(zombiePos, zombieYaw, bodyParts, cutInfo, fwdDir, slashRight, zc, pose) {\n                    return selectKatanaBodyPartByBladeLineForValues(\n                        zombiePos.x,\n                        zombiePos.y || 0,\n                        zombiePos.z,\n                        zombieYaw,\n                        bodyParts,\n                        cutInfo,\n                        fwdDir,\n                        slashRight,\n                        getZombieHitScale(zc),\n                        getZombieHitProps(zc),\n                        pose\n                    );\n                }\n\n                function performKatanaHit(w, ws, slashCount, ctx) {\n                    playSoundExclusive(ws.sound, slashCount === 0 ? 0.82 : 0.6);\n\n                    const player = w.getComponent(playerEntity, 'Player');\n                    const pos = w.getComponent(playerEntity, 'Position');\n                    camera.position.set(pos.x, pos.y, pos.z);\n                    camera.rotation.order = 'YXZ'; camera.rotation.y = player.yaw; camera.rotation.x = player.pitch;\n\n                    const fwdDir = effectScratch.v1.set(0, 0, -1).applyQuaternion(camera.quaternion).normalize();\n                    const slashRight = effectScratch.v2.set(1, 0, 0).applyQuaternion(camera.quaternion).normalize();\n                    const katanaSlashDir = effectScratch.v3.copy(slashCount === 0 ? fwdDir : slashRight);\n                    if (slashCount !== 0) katanaSlashDir.multiplyScalar(slashCount === 1 ? -1 : 1);\n                    const katanaCutInfo = getKatanaCutInfo(slashCount, fwdDir, slashRight);\n\n                    const grid = getZombieSpatialIndex(ctx, w);\n                    const katanaQueryPad = 1.4;\n                    const zombieSlots = grid.querySlotsAABB(\n                        pos.x - ws.meleeRange - katanaQueryPad,\n                        pos.x + ws.meleeRange + katanaQueryPad,\n                        pos.z - ws.meleeRange - katanaQueryPad,\n                        pos.z + ws.meleeRange + katanaQueryPad,\n                        ctx.katanaCandidates || (ctx.katanaCandidates = [])\n                    );\n                    let hitCount = 0;\n\n                    for (let i = 0; i < zombieSlots.length; i++) {\n                        const zSlot = zombieSlots[i];\n                        if (zombieRuntime.deadPending[zSlot]) continue;\n                        const zombieX = zombieRuntime.x[zSlot];\n                        const zombieY = zombieRuntime.y[zSlot] || 0;\n                        const zombieZ = zombieRuntime.z[zSlot];\n                        const bodyParts = zombieRuntime.bodyMask[zSlot];\n                        const hitPose = applyZombieImpactPose(zSlot, computeZombiePose(zombieRuntime.animTimer[zSlot] || 0, zombieRuntime.state[zSlot] || 1, bodyParts, ctx.katanaHitPose || (ctx.katanaHitPose = {})));\n                        const zombieScale = getZombieSlotHitScale(zSlot);\n                        const zombieProps = getZombieSlotHitProps(zSlot);\n                        const dx = zombieX - pos.x;\n                        const dy = (zombieY + getZombieHitRootY(hitPose) + 1.0 * zombieScale) - pos.y;\n                        const dz = zombieZ - pos.z;\n                        const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);\n                        if (dist > ws.meleeRange + Math.max(0.45, getZombieHitReachRadiusForValues(zombieScale, zombieProps, bodyParts, hitPose) * 0.38)) continue;\n\n                        const toZombie = effectScratch.v4.set(dx, dy, dz).normalize();\n                        const dot = fwdDir.dot(toZombie);\n                        if (dot < 0.45) continue; // ~63 degree cone\n\n                        // Blade-line test determines which segment is crossed by the katana arc.\n                        const zombieYaw = zombieRuntime.yaw[zSlot] || 0;\n                        const zombieCutInfo = getZombieLocalCutInfo(katanaCutInfo, zombieYaw);\n                        const rayOrigin = effectScratch.v5.set(pos.x, pos.y, pos.z);\n\n                        let hitPart = 'body';\n                        let bestT = Infinity;\n\n                        if (bodyParts) {\n                            for (let hbIdx = 0; hbIdx < bodyPartHitboxes.length; hbIdx++) {\n                                const hb = bodyPartHitboxes[hbIdx];\n                                if (!zombiePartAlive(bodyParts, hb.name)) continue;\n                                const t = checkBodyPartHitRayForValues(rayOrigin, fwdDir, dist + 1.5, zombieX, zombieY, zombieZ, zombieYaw, hb, zombieScale, zombieProps, bodyParts, hitPose);\n                                if (t !== null && t < bestT) {\n                                    // Prefer limbs over body for katana\n                                    if (hb.name !== 'body' || bestT === Infinity) {\n                                        bestT = t;\n                                        hitPart = hb.name;\n                                    }\n                                }\n                            }\n                        }\n                        const bladeLinePart = selectKatanaBodyPartByBladeLineForValues(zombieX, zombieY, zombieZ, zombieYaw, bodyParts, zombieCutInfo, fwdDir, slashRight, zombieScale, zombieProps, hitPose);\n                        if (bladeLinePart && (hitPart === 'body' || bladeLinePart !== 'body')) hitPart = bladeLinePart;\n\n                        // Katana deals devastating slicing damage - blades cut clean through\n                        const katanaPartDmg = ws.damage * 3.0; // 210 - one-shots all limbs\n                        let dmgMult = 1.0;\n                        if (hitPart === 'head') dmgMult = (config.headshotMultiplier || 2);\n\n                        let finalDmg = ws.damage;\n                        if (hasAutoFirePassive) finalDmg *= 0.8;\n                        const hitResult = applyZombieArmorReductionBySlot(zSlot, finalDmg * dmgMult);\n                        zombieRuntime.currentHp[zSlot] -= hitResult.damage;\n\n                        gameState.shotsHit++;\n                        hitCount++;\n                        ctx.hitZombie = true;\n                        applyLifesteal(hitResult.damage);\n                        if (hitPart === 'head') {\n                            ctx.headshot = true;\n                            gameState.headshots++;\n                        }\n\n                        // Hit position for blood\n                        const hitPartDef = bodyPartHitboxes.find(h => h.name === hitPart);\n                        const hitPos = hitPartDef\n                            ? getHitboxWorldCenterForValues(effectScratch.v6, zombieX, zombieY, zombieZ, zombieYaw, hitPartDef, zombieScale, zombieProps, bodyParts, hitPose)\n                            : effectScratch.v6.set(zombieX, zombieY + getZombieHitRootY(hitPose) + 1.0 * zombieScale, zombieZ);\n\n                        // Apply part damage and dismemberment\n                        if (hitPart !== 'body') {\n                            const partHp = damageZombieSlotPart(zSlot, hitPart, katanaPartDmg);\n                            if (partHp <= 0 && zombieSlotPartAlive(zSlot, hitPart)) {\n                                // === SEVER THE BODY PART ===\n                                clearZombieSlotPart(zSlot, hitPart);\n                                const zombieMesh = acquireZombieProxyForSlot(zSlot);\n                                syncZombieBodyParts(zombieMesh, zombieRuntime.bodyPartsFacade[zSlot]);\n\n                                // Spawn flying severed part\n                                if (zombieRuntime.currentHp[zSlot] > 0) {\n                                    const zp = zombieRuntime.positionFacade[zSlot];\n                                    applyZombieImpactReaction(zSlot, 3, hitPart, katanaSlashDir, hitResult.damage, true, zombieCutInfo);\n                                    if (zombieMesh) spawnSeveredPart(zombieMesh, hitPart, zp, katanaSlashDir, zombieCutInfo);\n                                }\n\n                                // Massive blood spray for severing\n                                if (zombieRuntime.currentHp[zSlot] > 0) spawnBloodBurst(hitPos, slashCount === 0 ? 24 : 16);\n\n                                // Head = instant kill\n                                if (hitPart === 'head') {\n                                    zombieRuntime.currentHp[zSlot] = 0;\n                                } else if (hitPart === 'leftLeg' || hitPart === 'rightLeg') {\n                                    const legsLeft = zombieSlotLegsLeft(zSlot);\n                                    zombieRuntime.speed[zSlot] = zombieRuntime.baseSpeed[zSlot] * (legsLeft === 0 ? 0.15 : 0.5);\n                                }\n                            } else {\n                                // Part not severed yet - still spray blood\n                                if (zombieRuntime.currentHp[zSlot] > 0) applyZombieImpactReaction(zSlot, 3, hitPart, katanaSlashDir, hitResult.damage, false, zombieCutInfo);\n                                spawnBloodBurst(hitPos, slashCount === 0 ? 12 : 8);\n                            }\n                        } else {\n                            // Body hit\n                            if (zombieRuntime.currentHp[zSlot] > 0) applyZombieImpactReaction(zSlot, 3, hitPart, katanaSlashDir, hitResult.damage, false, zombieCutInfo);\n                            spawnBloodBurst(hitPos, slashCount === 0 ? 12 : 8);\n                        }\n\n                        if (zombieRuntime.currentHp[zSlot] <= 0) {\n                            ctx.killConfirm = true;\n                            queueKatanaZombieDeath(w, zombieRuntime.entityId[zSlot], katanaSlashDir, hitPart);\n                        }\n                        if (hitCount >= 4) break; // Katana cleaves up to 4 zombies\n                    }\n\n                    if (hitCount > 0) {\n                        hitStopTimer = Math.max(hitStopTimer, slashCount === 0 ? 0.13 : 0.075);\n                        weaponAnim.viewKickTimer = slashCount === 0 ? 0.28 : 0.18;\n                        weaponAnim.viewKickCombo = slashCount;\n                        weaponAnim.viewKickStrength = slashCount === 0 ? 1.35 : 1.0;\n                    }\n\n                    ctx.weaponChanged = true;\n                }\n\n                function applyFlamethrowerJetHit(w, ctx, origin, direction, ws, pressureScale) {\n                    const baseRange = ws.bulletSpeed * 0.62 * (0.4 + 0.6 * pressureScale);\n                    const wallHit = getNearestWallHit(origin, direction, baseRange);\n                    const maxRange = wallHit ? Math.max(0.15, wallHit.distance) : baseRange;\n                    const lightDistance = Math.min(maxRange * 0.48, 5.4);\n                    flameLightTargetX = origin.x + direction.x * lightDistance;\n                    flameLightTargetY = origin.y + direction.y * lightDistance + 0.18;\n                    flameLightTargetZ = origin.z + direction.z * lightDistance;\n                    const maxRadius = 0.92 + flamethrowerUpgrades.wideNozzle * 0.34;\n                    const endX = origin.x + direction.x * maxRange;\n                    const endZ = origin.z + direction.z * maxRange;\n                    const grid = getZombieSpatialIndex(ctx, w);\n                    const candidates = ctx.flameCandidates || (ctx.flameCandidates = []);\n                    grid.querySlotsAABB(\n                        Math.min(origin.x, endX) - maxRadius,\n                        Math.max(origin.x, endX) + maxRadius,\n                        Math.min(origin.z, endZ) - maxRadius,\n                        Math.max(origin.z, endZ) + maxRadius,\n                        candidates\n                    );\n\n                    for (let i = 0; i < candidates.length; i++) {\n                        const zSlot = candidates[i];\n                        if (zombieRuntime.deadPending[zSlot]) continue;\n                        const relX = zombieRuntime.x[zSlot] - origin.x;\n                        const relY = (zombieRuntime.y[zSlot] || 0) + (zombieRuntime.scale[zSlot] || 1) - origin.y;\n                        const relZ = zombieRuntime.z[zSlot] - origin.z;\n                        const along = relX * direction.x + relY * direction.y + relZ * direction.z;\n                        if (along < -0.15 || along > maxRange + 0.35) continue;\n                        const distSq = relX * relX + relY * relY + relZ * relZ;\n                        const perpSq = Math.max(0, distSq - along * along);\n                        const progress = clamp01(along / Math.max(0.001, maxRange));\n                        const coneRadius = 0.20 + progress * maxRadius + (zombieRuntime.radius[zSlot] || 0.44);\n                        if (perpSq > coneRadius * coneRadius) continue;\n\n                        let finalDmg = ws.damage;\n                        if (!flamethrowerUpgrades.armorMelting) {\n                            finalDmg = applyZombieArmorReductionBySlot(zSlot, finalDmg, 0.48).damage;\n                        }\n                        zombieRuntime.currentHp[zSlot] -= finalDmg;\n                        ctx.hitZombie = true;\n                        applyLifesteal(finalDmg);\n                        zombieRuntime.impactSlowTimer[zSlot] = Math.max(zombieRuntime.impactSlowTimer[zSlot], FLAME_DIRECT_SLOW_TIME);\n                        zombieRuntime.impactSlow[zSlot] = Math.max(zombieRuntime.impactSlow[zSlot], FLAME_DIRECT_SLOW);\n                        zombieRuntime.burningTimer[zSlot] = 4.0 + flamethrowerUpgrades.stickyFire * 2.0;\n                        zombieRuntime.burningDamage[zSlot] = 12.0 * Math.pow(1.5, flamethrowerUpgrades.stickyFire);\n\n                        if (flameGpuTime >= flameContactNextFxAt[zSlot]) {\n                            const nearestY = origin.y + direction.y * along;\n                            const horizontalLen = Math.sqrt(relX * relX + relZ * relZ) || 1;\n                            const nx = -relX / horizontalLen;\n                            const nz = -relZ / horizontalLen;\n                            spawnFlameSurfaceBurst(\n                                zombieRuntime.x[zSlot] + nx * (zombieRuntime.radius[zSlot] || 0.44) * 0.48,\n                                nearestY,\n                                zombieRuntime.z[zSlot] + nz * (zombieRuntime.radius[zSlot] || 0.44) * 0.48,\n                                nx, 0.25, nz,\n                                direction.x, direction.y, direction.z,\n                                0.92\n                            );\n                            flameContactNextFxAt[zSlot] = flameGpuTime + (perfProfile.lowEnd ? 0.14 : 0.075);\n                        }\n\n                        if (zombieRuntime.currentHp[zSlot] <= 0) {\n                            queueFlameZombieDeath(w, zombieRuntime.entityId[zSlot], zombieRuntime.x[zSlot], zombieRuntime.y[zSlot] + 1, zombieRuntime.z[zSlot]);\n                        }\n                    }\n\n                    if (wallHit && flameGpuTime >= nextFlameWallFxAt) {\n                        spawnFlameSurfaceBurst(\n                            wallHit.px, wallHit.py, wallHit.pz,\n                            wallHit.nx, wallHit.ny, wallHit.nz,\n                            direction.x, direction.y, direction.z,\n                            1.15\n                        );\n                        nextFlameWallFxAt = flameGpuTime + (perfProfile.lowEnd ? 0.08 : 0.045);\n                    }\n                    return maxRange;\n                }\n\n                // System 2: Weapon logic\n                function weaponSystem(w, dt, ctx) {\n                    const wComp = w.getComponent(playerEntity, 'Weapon');\n                    const ws = weaponStates[wComp.index];\n                    const pos = w.getComponent(playerEntity, 'Position');\n                    const player = w.getComponent(playerEntity, 'Player');\n                    if (wComp.fireTimer > 0) wComp.fireTimer -= dt;\n                    if (wComp.katanaInputBuffer > 0) wComp.katanaInputBuffer = Math.max(0, wComp.katanaInputBuffer - dt);\n                    const currentWs = weaponStates[wComp.index];\n                    const heatRecover = currentWs.spreadRecover || 4;\n                    wComp.spreadHeat = clampWeaponSpreadHeat(currentWs, (wComp.spreadHeat || 0) - dt * heatRecover * 0.012);\n                    wComp.visualKick = Math.max(0, (wComp.visualKick || 0) - dt * 7.5);\n                    const playerForRecoil = w.getComponent(playerEntity, 'Player');\n                    const returnSpeed = currentWs.recoilReturn || 6;\n                    if (!currentWs.isMelee && playerForRecoil) {\n                        const pitchDebt = wComp.recoilPitchDebt || 0;\n                        if (pitchDebt > 0.00001) {\n                            const pitchBack = Math.min(pitchDebt, dt * returnSpeed * 0.030);\n                            playerForRecoil.pitch -= pitchBack;\n                            wComp.recoilPitchDebt = pitchDebt - pitchBack;\n                        }\n                        const yawDebt = wComp.recoilYawDebt || 0;\n                        if (Math.abs(yawDebt) > 0.00001) {\n                            const yawBack = Math.sign(yawDebt) * Math.min(Math.abs(yawDebt), dt * returnSpeed * 0.018);\n                            playerForRecoil.yaw -= yawBack;\n                            wComp.recoilYawDebt = yawDebt - yawBack;\n                        }\n                        playerForRecoil.pitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, playerForRecoil.pitch));\n                    }\n                    if (wComp.pendingKatanaHit) {\n                        wComp.pendingKatanaHit.timer -= dt;\n                        if (wComp.pendingKatanaHit.timer <= 0) {\n                            const pending = wComp.pendingKatanaHit;\n                            wComp.pendingKatanaHit = null;\n                            performKatanaHit(w, weaponStates[3], pending.combo, ctx);\n                        }\n                    }\n\n                    // Switch (only allow owned weapons) - switching cancels reload\n                    if (input.switchTo >= 0 && input.switchTo < weaponStates.length) {\n                        if (weaponTutorialActive && input.switchTo !== roguelikeState.ownedWeapons[1]) {\n                            input.switchTo = -1;\n                        }\n                        if (input.switchTo >= 0 && input.switchTo !== wComp.index && roguelikeState.ownedWeapons.includes(input.switchTo)) {\n                            const oldWs = weaponStates[wComp.index];\n                            if (wComp.isReloading) {\n                                wComp.isReloading = false;\n                                wComp.reloadTimer = 0;\n                                wComp.reloadStage = 0;\n                            }\n                            if (oldWs && oldWs.isMelee) clearKatanaRuntime(wComp);\n                            if (oldWs && oldWs.name === 'FLAMETHROWER') {\n                                stopLoopedSound(FLAMETHROWER_LOOP_SOUND_ID);\n                                wComp.flameSoundTimer = 0;\n                            }\n                            wComp.index = input.switchTo;\n                            if (wComp.index === 7) ensureAudioLoaded(FLAMETHROWER_LOOP_SOUND_ID);\n                            wComp.recoilPitchDebt = 0;\n                            wComp.recoilYawDebt = 0;\n                            wComp.spreadHeat = 0;\n                            wComp.lastShellAt = 0;\n                            input.firing = false;\n                            input.holdingFire = false;\n                            input.touchHoldingFire = false;\n                            ctx.weaponChanged = true;\n                            toggleSniperScope(false);\n\n                            // Dismiss tutorial if switching to the new weapon\n                            if (input.switchTo === roguelikeState.ownedWeapons[1]) {\n                                dismissWeaponTutorial();\n                            }\n                        }\n                        input.switchTo = -1;\n                    }\n\n                    // Reload\n                    if (input.reload && !wComp.isReloading) {\n                        const ws = weaponStates[wComp.index];\n                        if (!ws.isMelee && ws.mag < ws.magSize && (ws.ammo > 0 || ws.ammo === Infinity)) {\n                            wComp.isReloading = true; wComp.reloadTimer = ws.reloadTime;\n                            wComp.reloadStage = 0;\n                            playSoundExclusive(ws.reloadSound || 'reload_sound', 0.55, VOICE_STEAL_FADE, { duration: ws.reloadTime });\n                            ctx.weaponChanged = true;\n                        }\n                        input.reload = false;\n                    }\n                    if (wComp.isReloading) {\n                        const ws = weaponStates[wComp.index];\n                        wComp.reloadTimer -= dt;\n                        if (wComp.index === 7) {\n                            wComp.flameHeat = Math.max(0, ws.magSize * (wComp.reloadTimer / ws.reloadTime));\n                            ws.mag = Math.max(0, Math.ceil(ws.magSize - wComp.flameHeat));\n                        }\n                        const reloadProgress = 1 - (wComp.reloadTimer / ws.reloadTime);\n                        if (wComp.reloadStage === 0 && reloadProgress >= 0.42) {\n                            wComp.reloadStage = 1;\n                            // mid-reload mechanical sound (only for weapons with multi-stage reload)\n                            if (ws.name === 'SHOTGUN' || ws.name === 'LMG') {\n                                playSoundExclusive(ws.reloadSound || 'reload_sound', 0.32);\n                            }\n                        }\n                        if (wComp.reloadStage === 1 && reloadProgress >= 0.78) {\n                            wComp.reloadStage = 2;\n                            if (ws.name === 'SNIPER' || ws.name === 'LMG') {\n                                playSoundExclusive(ws.reloadSound || 'reload_sound', 0.28);\n                            }\n                        }\n                        if (wComp.reloadTimer <= 0) {\n                            wComp.isReloading = false;\n                            wComp.reloadStage = 0;\n                            if (wComp.index === 7) {\n                                wComp.flameHeat = 0;\n                                ws.mag = ws.magSize;\n                            } else if (ws.ammo === Infinity) { ws.mag = ws.magSize; }\n                            else { const n = Math.min(ws.magSize - ws.mag, ws.ammo); ws.mag += n; ws.ammo -= n; }\n                            ctx.weaponChanged = true;\n                        }\n                    }\n\n                    // Auto-Fire Passive Skill Check (raycast to zombie or automatic katana swing)\n                    if (hasAutoFirePassive && !gameState.upgradeScreenOpen && !weaponSelectOpen && !weaponTutorialActive && !gamePaused) {\n                        const smartPistolActive = wComp.index === 0 && hasSmartPistol;\n                        if (ws.isMelee && wComp.fireTimer <= 0) {\n                            const grid = getZombieSpatialIndex(ctx, w);\n                            const zombieSlots = grid.querySlotsAABB(\n                                pos.x - ws.meleeRange, pos.x + ws.meleeRange,\n                                pos.z - ws.meleeRange, pos.z + ws.meleeRange,\n                                ctx.autoMeleeCandidates || (ctx.katanaCandidates = [])\n                            );\n                            let foundMeleeZombie = false;\n                            for (let i = 0; i < zombieSlots.length; i++) {\n                                const zSlot = zombieSlots[i];\n                                if (zombieRuntime.deadPending[zSlot]) continue;\n                                const dx = zombieRuntime.x[zSlot] - pos.x;\n                                const dz = zombieRuntime.z[zSlot] - pos.z;\n                                const dist = Math.sqrt(dx * dx + dz * dz);\n                                if (dist <= ws.meleeRange) {\n                                    foundMeleeZombie = true;\n                                    break;\n                                }\n                            }\n                            if (foundMeleeZombie) {\n                                input.firing = true;\n                            }\n                        } else if (!smartPistolActive && !ws.isMelee && !wComp.isReloading && ws.mag > 0 && wComp.fireTimer <= 0) {\n                            // Raycast forward from player view to see if pointing at any zombie\n                            const fwdDir = effectScratch.v1.set(0, 0, -1).applyQuaternion(camera.quaternion).normalize();\n                            const grid = getZombieSpatialIndex(ctx, w);\n                            // Increase check range from 25 to 50 to cover entire hallway and avoid first-zombie auto-fire delay\n                            const zombieSlots = grid.querySlotsAABB(\n                                pos.x - 50, pos.x + 50, pos.z - 50, pos.z + 50,\n                                ctx.autoFireCandidates || (ctx.bulletCandidates = [])\n                            );\n                            let foundZombie = false;\n                            for (let i = 0; i < zombieSlots.length; i++) {\n                                const zSlot = zombieSlots[i];\n                                if (zombieRuntime.deadPending[zSlot]) continue;\n                                const zScale = getZombieSlotHitScale(zSlot);\n                                const zProps = getZombieSlotHitProps(zSlot);\n                                const bodyParts = zombieRuntime.bodyMask[zSlot];\n                                const hitPose = getBulletCachedZombiePose(zSlot, ctx);\n                                const t = checkZombieSilhouetteHitRayForValues(\n                                    pos, fwdDir, 50,\n                                    zombieRuntime.x[zSlot], zombieRuntime.y[zSlot] || 0, zombieRuntime.z[zSlot],\n                                    zombieRuntime.yaw[zSlot] || 0, zScale, zProps, bodyParts, hitPose\n                                );\n                                if (t !== null) {\n                                    foundZombie = true;\n                                    break;\n                                }\n                            }\n                            if (foundZombie) {\n                                // Use input.firing (one-shot) instead of holdingFire to avoid\n                                // sticky-state bug where auto weapons fire indefinitely.\n                                input.firing = true;\n                            }\n                        }\n                    }\n\n                    // Fire - support auto weapons (hold to fire) and semi-auto (tap)\n                    const shouldFire = (gameState.upgradeScreenOpen || weaponSelectOpen) ? false : (ws.isAuto ? (input.holdingFire || input.firing) : input.firing);\n\n                    // Natural flamethrower cooling (even when unselected)\n                    const flamethrowerWs = weaponStates[7];\n                    if (flamethrowerWs) {\n                        const isFiringNow = wComp.index === 7 && shouldFire && wComp.fireTimer <= 0 && !wComp.isReloading;\n                        if (wComp.index !== 7 || (!wComp.isReloading && !isFiringNow)) {\n                            wComp.flameHeat = Math.max(0, (wComp.flameHeat || 0) - dt * 35); // Cools by 35 units per second\n                            flamethrowerWs.mag = Math.max(0, Math.ceil(flamethrowerWs.magSize - wComp.flameHeat));\n                        }\n                    }\n                    if (ws.isMelee && shouldFire && wComp.fireTimer > 0 && !wComp.isReloading) {\n                        wComp.katanaInputBuffer = 0.45 * KATANA_ANIM_SLOWMO;\n                    }\n                    const bufferedKatanaFire = ws.isMelee && wComp.katanaInputBuffer > 0 && wComp.fireTimer <= 0;\n\t                    const smartQueuedFire = !ws.isMelee && wComp.index === 0 && hasSmartPistol && hasSmartPistolQueuedShot();\n                    if ((shouldFire || bufferedKatanaFire || smartQueuedFire) && wComp.fireTimer <= 0 && !wComp.isReloading) {\n                        const fireTimerCarry = Math.min(0, wComp.fireTimer || 0);\n                        if (ws.isMelee) {\n                            // ===== MELEE ATTACK (Katana) - Dismemberment System =====\n                            const nextKatanaCombo = ((weaponAnim.slashCount || 0) + 1) % 3;\n                            const slashDuration = (nextKatanaCombo === 0 ? 0.62 : 0.38) * KATANA_ANIM_SLOWMO;\n                            const impactProgress = nextKatanaCombo === 0 ? 0.70 : (nextKatanaCombo === 1 ? 0.50 : 0.47);\n                            wComp.fireTimer = slashDuration;\n                            wComp.pendingKatanaHit = { timer: slashDuration * impactProgress, combo: nextKatanaCombo };\n                            wComp.katanaInputBuffer = 0;\n                            ctx.fired = true;\n                            gameState.shotsFired++;\n                            ctx.weaponChanged = true;\n\n                        } else if (ws.mag <= 0) {\n                            input.reload = true;\n                        } else {\n                            // ===== RANGED FIRE =====\n                            if (wComp.index === 0 && hasSmartPistol) {\n\t                                if (!hasSmartPistolQueuedShot()) {\n                                    consumeSmartPistolLocks(ws.mag);\n                                }\n\n                                const smartShot = getNextSmartPistolQueuedShot();\n                                if (smartShot) {\n                                    ws.mag--;\n                                    gameState.shotsFired++;\n                                    wComp.fireTimer = SMART_PISTOL_SHOT_INTERVAL;\n\n                                    const localMuzzle = setWeaponMuzzleLocal(wComp.index, weaponFireScratch.localMuzzle);\n                                    const worldMuzzle = weaponFireScratch.worldMuzzle.copy(localMuzzle).applyQuaternion(camera.quaternion).add(pos);\n                                    const targetPos = getSmartPistolTargetPoint(smartShot.slot, effectScratch.v6);\n                                    const muzzleToTarget = targetPos.distanceTo(worldMuzzle);\n                                    const shootDir = effectScratch.v5;\n                                    if (muzzleToTarget < 2.2) {\n                                        // At point-blank range the view ray can put the muzzle beside or\n                                        // beyond the target. Aim the first segment directly at the lock.\n                                        shootDir.copy(targetPos).sub(worldMuzzle).normalize();\n                                    } else {\n                                        shootDir.set(0, 0, -1).applyQuaternion(camera.quaternion).normalize();\n                                    }\n                                    const smartWs = getSmartPistolProjectileProfile(ws);\n                                    const projectileSlot = spawnProjectile(worldMuzzle, shootDir, smartWs, wComp.index, smartShot.slot);\n                                    if (projectileSlot >= 0) {\n                                        const straightDistance = Math.max(\n                                            SMART_PISTOL_MUZZLE_CLEARANCE,\n                                            Math.min(SMART_PISTOL_MAX_STRAIGHT_DISTANCE, muzzleToTarget * 0.16)\n                                        );\n                                        projectileSmartStraightTime[projectileSlot] = muzzleToTarget < 2.2\n                                            ? 0\n                                            : Math.min(\n                                                SMART_PISTOL_INITIAL_STRAIGHT_TIME,\n                                                Math.max(SMART_PISTOL_MIN_STRAIGHT_TIME, straightDistance / Math.max(1, smartWs.bulletSpeed || SMART_PISTOL_BULLET_SPEED))\n                                            );\n                                        projectileSmartFullLock[projectileSlot] = smartShot.fullLock ? 1 : 0;\n                                        projectileSmartRequiredLocks[projectileSlot] = Math.max(1, smartShot.requiredLocks || 1);\n                                        projectileSmartFinalShot[projectileSlot] = smartShot.finalShot ? 1 : 0;\n                                        if (smartShot.fullLock) {\n                                            const headMult = config.headshotMultiplier || 2;\n                                            const effectiveHp = zombieRuntime.currentHp[smartShot.slot] + Math.max(0, zombieRuntime.armor[smartShot.slot] || 0) * 0.45;\n                                            const damageDivisor = smartShot.finalShot ? headMult : 1;\n                                            projectileDamage[projectileSlot] = Math.max(\n                                                projectileDamage[projectileSlot],\n                                                (effectiveHp / projectileSmartRequiredLocks[projectileSlot] / damageDivisor) * 1.12\n                                            );\n                                        }\n                                    }\n\n                                    playWeaponFireSound(ws, 0.38);\n                                    ctx.fired = true;\n                                    ctx.muzzleFlashShots = (ctx.muzzleFlashShots || 0) + 1;\n                                    ctx.muzzleFlashWeaponIndex = wComp.index;\n                                    ctx.muzzleFlashSeed = Math.random();\n                                    wComp.visualKick = 1.0;\n                                    weaponAnim.gunKickTimer = 0.12;\n                                    ctx.weaponChanged = true;\n                                } else {\n                                    wComp.fireTimer = Math.min(0.08, ws.fireRate);\n                                }\n                            } else {\n                                if (wComp.index === 7) {\n                                    wComp.flameHeat = (wComp.flameHeat || 0) + 3.2; // 3.2 heat per shot (overheats in ~1.2s of continuous fire)\n                                    ws.mag = Math.max(0, Math.ceil(ws.magSize - wComp.flameHeat));\n                                } else {\n                                    ws.mag--;\n                                }\n                                gameState.shotsFired++;\n                                if (ws.isAuto) {\n                                    const carry = Math.max(-ws.fireRate * 0.85, fireTimerCarry);\n                                    wComp.fireTimer = Math.max(0.001, ws.fireRate + carry);\n                                } else {\n                                    wComp.fireTimer = ws.fireRate;\n                                }\n                                const derivedOffset = weaponDerivedOffset(wComp.index);\n                                const gunVolume = weaponDerivedStats[derivedOffset + WDS_FIRE_VOLUME];\n                                if (ws.name !== 'FLAMETHROWER') {\n                                    playWeaponFireSound(ws, gunVolume);\n                                    ctx.muzzleFlashShots = (ctx.muzzleFlashShots || 0) + 1;\n                                    ctx.muzzleFlashWeaponIndex = wComp.index;\n                                    ctx.muzzleFlashSeed = Math.random();\n                                }\n                                ctx.fired = true;\n\n                                // Spawn bullet entities with real ballistics\n                                const player = w.getComponent(playerEntity, 'Player');\n                                const pos = w.getComponent(playerEntity, 'Position');\n                                camera.position.set(pos.x, pos.y, pos.z);\n                                camera.rotation.order = 'YXZ'; camera.rotation.y = player.yaw; camera.rotation.x = player.pitch;\n                                const moving = input.keys['w'] || input.keys['s'] || input.keys['a'] || input.keys['d'] ||\n                                    input.keys['arrowup'] || input.keys['arrowdown'] || input.keys['arrowleft'] || input.keys['arrowright'] ||\n                                    Math.abs(leftJoystickValue.x) > 0.1 || Math.abs(leftJoystickValue.y) > 0.1;\n                                const shotSpread = getShotSpread(ws, wComp, moving);\n\n                                // Calculate Muzzle Position in World Space\n                                const localMuzzle = setWeaponMuzzleLocal(wComp.index, weaponFireScratch.localMuzzle);\n                                const worldMuzzle = weaponFireScratch.worldMuzzle.copy(localMuzzle).applyQuaternion(camera.quaternion).add(pos);\n\n                                // Find weapon target point (where crosshair aims)\n                                const camDir = weaponFireScratch.camDir.set(0, 0, -1).applyQuaternion(camera.quaternion).normalize();\n                                const wallHit = getNearestWallHit(pos, camDir, 100);\n                                const targetPoint = weaponFireScratch.targetPoint;\n                                if (wallHit) {\n                                    targetPoint.set(wallHit.px, wallHit.py, wallHit.pz);\n                                } else {\n                                    targetPoint.copy(pos).addScaledVector(camDir, 100);\n                                }\n                                const baseDir = weaponFireScratch.baseDir.copy(targetPoint).sub(worldMuzzle).normalize();\n                                const rightVec = weaponFireScratch.rightVec.set(1, 0, 0).applyQuaternion(camera.quaternion);\n                                const upVec = weaponFireScratch.upVec.set(0, 1, 0).applyQuaternion(camera.quaternion);\n\n                                if (ws.name === 'FLAMETHROWER') {\n                                    // Spawn flame particles!\n                                    let spawned = 0;\n                                    const numToSpawn = perfProfile.lowEnd ? 3 : 5;\n                                    const baseSpeed = ws.bulletSpeed; // 18 + Fuel Compression level increments\n                                    const baseSpread = 0.040 + (flamethrowerUpgrades.wideNozzle * 0.030);\n\n                                    // Dynamic fuel pressure decays as heat builds up to simulate pressure loss\n                                    const heatRatio = Math.min(1.0, Math.max(0.0, (wComp.flameHeat || 0) / (ws.magSize || 100)));\n                                    const pressure = 1.0 - 0.65 * heatRatio; // Decays from 1.0 (cold) to 0.35 (hot)\n                                    const flameRange = applyFlamethrowerJetHit(w, ctx, worldMuzzle, baseDir, ws, pressure);\n\n                                    while (spawned < numToSpawn) {\n                                        const i = acquireFlameSlot(true);\n                                        if (i < 0) break;\n                                        const dir = effectScratch.v5.copy(baseDir);\n                                        const lane = spawned / Math.max(1, numToSpawn - 1);\n                                        const spreadScale = 0.55 + lane * 0.65;\n                                        dir.addScaledVector(rightVec, (Math.random() - 0.5) * baseSpread * spreadScale);\n                                        dir.addScaledVector(upVec, (Math.random() - 0.5) * baseSpread * spreadScale);\n                                        dir.normalize();\n\n                                        const speed = baseSpeed * pressure * (1.05 + Math.random() * 0.22);\n                                        let life = (0.46 + Math.random() * 0.24) * (0.4 + 0.6 * pressure);\n                                        life = Math.min(life, flameRange / Math.max(0.001, speed) + 0.06);\n                                        // Distribute initialAge over the entire fireRate interval to fill the gaps between ticks\n                                        const initialAge = (lane + (Math.random() - 0.5) * 0.08) * ws.fireRate * pressure;\n                                        seedFlameParticle(\n                                            i,\n                                            worldMuzzle.x, worldMuzzle.y, worldMuzzle.z,\n                                            dir.x * speed, dir.y * speed, dir.z * speed,\n                                            life,\n                                            0,\n                                            (0.28 + Math.random() * 0.18) * (0.3 + 0.7 * pressure),\n                                            1.0,\n                                            initialAge\n                                        );\n                                        spawned++;\n                                    }\n                                } else {\n                                    for (let p = 0; p < ws.pellets; p++) {\n                                        const spreadDir = weaponFireScratch.spreadDir.copy(baseDir);\n                                        if (shotSpread > 0) {\n                                            spreadDir.addScaledVector(rightVec, (Math.random() - 0.5) * shotSpread);\n                                            spreadDir.addScaledVector(upVec, (Math.random() - 0.5) * shotSpread);\n                                            spreadDir.normalize();\n                                        }\n\n                                        spawnProjectile(worldMuzzle, spreadDir, ws, wComp.index);\n                                    }\n                                }\n                            }\n\n                            if (ctx.fired) {\n                                // Spawn shell casing (pure VFX, rate-limited for ultra-high fire rates)\n                                if (shouldSpawnWeaponShell(ws, wComp)) spawnShellCasing(wComp.index, pos, player.yaw);\n\n                                // Recoil\n                                const pl = w.getComponent(playerEntity, 'Player');\n                                const derivedOffset = weaponDerivedOffset(wComp.index);\n                                const comfort = weaponDerivedStats[derivedOffset + WDS_RAPID_COMFORT];\n                                const recoilRamp = 1 + Math.min(0.85, (wComp.spreadHeat || 0) * weaponDerivedStats[derivedOffset + WDS_RECOIL_HEAT_SCALE]);\n                                const pitchKick = weaponDerivedStats[derivedOffset + WDS_RECOIL_PITCH] * recoilRamp * comfort;\n                                const yawKick = (Math.random() - 0.5) * weaponDerivedStats[derivedOffset + WDS_RECOIL_YAW] * recoilRamp * weaponDerivedStats[derivedOffset + WDS_YAW_COMFORT_SCALE];\n\n                                const oldPitchDebt = wComp.recoilPitchDebt || 0;\n                                const pitchDebtCap = weaponDerivedStats[derivedOffset + WDS_PITCH_DEBT_CAP];\n                                const newPitchDebt = Math.min(pitchDebtCap, oldPitchDebt + pitchKick);\n                                const appliedPitch = newPitchDebt - oldPitchDebt;\n\n                                const oldYawDebt = wComp.recoilYawDebt || 0;\n                                const yawDebtCap = weaponDerivedStats[derivedOffset + WDS_YAW_DEBT_CAP];\n                                const newYawDebt = Math.max(-yawDebtCap, Math.min(yawDebtCap, oldYawDebt + yawKick * 0.72));\n                                const appliedYaw = newYawDebt - oldYawDebt;\n\n                                pl.pitch += appliedPitch;\n                                pl.yaw += appliedYaw;\n                                pl.pitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, pl.pitch));\n\n                                wComp.recoilPitchDebt = newPitchDebt;\n                                wComp.recoilYawDebt = newYawDebt;\n\n                                wComp.spreadHeat = clampWeaponSpreadHeat(ws, (wComp.spreadHeat || 0) + weaponDerivedStats[derivedOffset + WDS_SPREAD_HEAT_ADD]);\n                                const visualScale = getVisualIntensityScale();\n                                wComp.visualKick = Math.min(1.35, Math.max(wComp.visualKick || 0, weaponDerivedStats[derivedOffset + WDS_VISUAL_KICK_BASE] * (0.72 + visualScale * 0.28)));\n                                weaponAnim.gunKickDuration = 0.10;\n                                weaponAnim.gunKickTimer = Math.max(weaponAnim.gunKickTimer || 0, weaponAnim.gunKickDuration);\n                                weaponAnim.gunKickStrength = Math.min(1.45, Math.max(weaponAnim.gunKickStrength || 0, weaponDerivedStats[derivedOffset + WDS_GUN_KICK_STRENGTH_BASE] * (0.78 + visualScale * 0.22)));\n                                weaponAnim.gunKickTarget = Math.min(1.15 * (0.72 + visualScale * 0.28), (weaponAnim.gunKickTarget || 0) + weaponDerivedStats[derivedOffset + WDS_GUN_KICK_TARGET_ADD_BASE] * visualScale);\n                                weaponAnim.recoilKickScale = weaponDerivedStats[derivedOffset + WDS_RECOIL_KICK_SCALE];\n                                ctx.weaponChanged = true;\n\n                                // Auto-reload when magazine is empty\n                                if (ws.mag <= 0 && !ws.isMelee && ws.magSize !== Infinity && (ws.ammo > 0 || ws.ammo === Infinity)) {\n                                    wComp.isReloading = true;\n                                    wComp.reloadTimer = ws.reloadTime;\n                                    wComp.reloadStage = 0;\n                                    playSoundExclusive(ws.reloadSound || 'reload_sound', 0.55, VOICE_STEAL_FADE, { duration: ws.reloadTime });\n                                }\n                            }\n                        }\n                    }\n                    input.firing = false;\n                }\n\n                function getZombieAttackInterval(zombieName, isBoss) {\n                    if (isBoss) {\n                        if (zombieName === 'CHARGER') return 0.65;\n                        if (zombieName === 'OVERLORD') return 0.85;\n                        return 0.9;\n                    }\n                    if (zombieName === 'Runner') return 0.75;\n                    if (zombieName === 'Tank' || zombieName === 'Armored' || zombieName === 'Bloater') return 1.2;\n                    if (zombieName === 'Crawler') return 1.35;\n                    return 1.0;\n                }\n\n                const zombieDamageScratch = { damage: 0, absorbed: 0 };\n\n                function applyZombieArmorReductionBySlot(slot, damage, armorPierce = 0.35) {\n                    const armor = zombieRuntime.armor[slot];\n                    if (armor <= 0) {\n                        zombieDamageScratch.damage = damage;\n                        zombieDamageScratch.absorbed = 0;\n                        return zombieDamageScratch;\n                    }\n                    const minDamage = damage * armorPierce;\n                    const canAbsorb = Math.max(0, damage - minDamage);\n                    const absorbed = Math.min(armor, canAbsorb);\n                    zombieRuntime.armor[slot] = armor - absorbed;\n                    zombieDamageScratch.damage = Math.max(minDamage, damage - absorbed);\n                    zombieDamageScratch.absorbed = absorbed;\n                    return zombieDamageScratch;\n                }\n\n                function isZombieDamageEffective(slot) {\n                    const eid = zombieRuntime.entityId[slot];\n                    const dealerSlot = getZombieRuntimeSlot(gameState.damageDealerId);\n                    if (gameState.damageDealerId === undefined || gameState.damageDealerId === null || dealerSlot < 0 || zombieRuntime.state[dealerSlot] !== 2) {\n                        gameState.damageDealerId = eid;\n                    }\n                    return eid === gameState.damageDealerId;\n                }\n\n                function applyZombieAttackBySlot(w, slot, dt) {\n                    if (zombieRuntime.state[slot] !== 2) return;\n                    zombieRuntime.attackTimer[slot] += dt;\n                    if (zombieRuntime.attackTimer[slot] < zombieRuntime.attackInterval[slot]) return;\n                    zombieRuntime.attackTimer[slot] = 0;\n                    if (!isZombieDamageEffective(slot)) return;\n                    let atkDmg = (10 + gameState.currentWave) * (zombieRuntime.damageMult[slot] || 1);\n                    const armsLeft = zombieSlotArmsLeft(slot);\n                    if (armsLeft === 0) atkDmg *= 0.3;\n                    else if (armsLeft === 1) atkDmg *= 0.6;\n                    w.emit({ type: 'playerDamage', amount: atkDmg, zombieX: zombieRuntime.x[slot], zombieZ: zombieRuntime.z[slot] });\n                }\n\n                function applyZombieSpecialBehaviorBySlot(w, slot, playerPos, dt, ctx) {\n                    const eid = zombieRuntime.entityId[slot];\n                    const typeName = zombieRuntime.typeName[slot];\n                    const hpRatio = zombieRuntime.currentHp[slot] / Math.max(1, zombieRuntime.maxHp[slot]);\n                    let tempSpeedMult = 1;\n\n                    if (zombieRuntime.speedBuffTimer[slot] > 0) {\n                        zombieRuntime.speedBuffTimer[slot] -= dt;\n                        tempSpeedMult *= 1.28;\n                    }\n\n                    if (zombieRuntime.impactSlowTimer[slot] > 0) {\n                        zombieRuntime.impactSlowTimer[slot] = Math.max(0, zombieRuntime.impactSlowTimer[slot] - dt);\n                        tempSpeedMult *= Math.max(0.42, 1 - zombieRuntime.impactSlow[slot]);\n                        if (zombieRuntime.impactSlowTimer[slot] <= 0) zombieRuntime.impactSlow[slot] = 0;\n                    }\n\n                    if (typeName === 'Runner') {\n                        zombieRuntime.burstTimer[slot] = Math.max(0, zombieRuntime.burstTimer[slot] - dt);\n                        if (zombieRuntime.burstTimer[slot] > 0) {\n                            tempSpeedMult *= 2.15;\n                        } else {\n                            zombieRuntime.burstCooldown[slot] -= dt;\n                            if (zombieRuntime.burstCooldown[slot] <= 0) {\n                                zombieRuntime.burstTimer[slot] = 0.45 + Math.random() * 0.25;\n                                zombieRuntime.burstCooldown[slot] = 1.9 + Math.random() * 1.3;\n                            }\n                        }\n                    }\n\n                    if (typeName === 'Berserker') {\n                        if (!zombieRuntime.rageTriggered[slot] && hpRatio <= 0.45) {\n                            zombieRuntime.rageTriggered[slot] = 1;\n                            zombieRuntime.damageMult[slot] = zombieRuntime.baseDamageMult[slot] * 1.9;\n                            zombieRuntime.attackInterval[slot] = Math.max(0.5, zombieRuntime.attackInterval[slot] * 0.72);\n                            const instanceSlot = zombieRuntime.instanceSlot[slot];\n                            if (instanceSlot >= 0) {\n                                setZombieInstanceColor('leftEye', instanceSlot, 0xff0033);\n                                setZombieInstanceColor('rightEye', instanceSlot, 0xff0033);\n                            }\n                            playSound('zombie_groan_3', 0.22);\n                        }\n                        if (zombieRuntime.rageTriggered[slot]) tempSpeedMult *= 1.45;\n                    }\n\n                    if (typeName === 'Stalker') {\n                        zombieRuntime.strafePhase[slot] += dt * 3.4;\n                        const dx = playerPos.x - zombieRuntime.x[slot];\n                        const dz = playerPos.z - zombieRuntime.z[slot];\n                        const dist = Math.sqrt(dx * dx + dz * dz) || 0.0001;\n                        if (dist > 2.2) {\n                            const sideX = -dz / dist;\n                            const sideZ = dx / dist;\n                            const sidestep = Math.sin(zombieRuntime.strafePhase[slot]) * 0.9 * dt;\n                            zombieRuntime.x[slot] += sideX * sidestep;\n                            zombieRuntime.z[slot] += sideZ * sidestep;\n                            ctx.spatialGridBuilt = false;\n                        }\n                    }\n\n                    if (typeName === 'Screamer') {\n                        zombieRuntime.screamTimer[slot] -= dt;\n                        if (zombieRuntime.screamTimer[slot] <= 0) {\n                            zombieRuntime.screamTimer[slot] = 6.5 + Math.random() * 2.5;\n                            const grid = getZombieSpatialIndex(ctx, w);\n                            const nearbySlots = grid.querySlotsAABB(\n                                zombieRuntime.x[slot] - 5.5,\n                                zombieRuntime.x[slot] + 5.5,\n                                zombieRuntime.z[slot] - 5.5,\n                                zombieRuntime.z[slot] + 5.5,\n                                ctx.screamerTargets || (ctx.screamerTargets = [])\n                            );\n                            for (let i = 0; i < nearbySlots.length; i++) {\n                                const targetSlot = nearbySlots[i];\n                                if (targetSlot === slot || zombieRuntime.deadPending[targetSlot]) continue;\n                                zombieRuntime.speedBuffTimer[targetSlot] = Math.max(zombieRuntime.speedBuffTimer[targetSlot], 3.2);\n                            }\n                            playSound('zombie_groan_4', 0.18 + Math.random() * 0.1);\n                        }\n                    }\n\n                    if (zombieRuntime.isBoss[slot]) {\n                        const dx = playerPos.x - zombieRuntime.x[slot];\n                        const dz = playerPos.z - zombieRuntime.z[slot];\n                        // Use squared distance to avoid Math.sqrt in per-boss-per-frame checks.\n                        // Thresholds: BRUTE 3.8\u00b2=14.44, CHARGER 3\u00b2=9, BEHEMOTH 4.6\u00b2=21.16, INFERNAL 2.9\u00b2=8.41\n                        const distSq = dx * dx + dz * dz;\n\n                        if (typeName === 'BRUTE') {\n                            zombieRuntime.bossAbilityCooldown[slot] -= dt;\n                            if (distSq < 14.44 && zombieRuntime.bossAbilityCooldown[slot] <= 0) {\n                                zombieRuntime.bossAbilityCooldown[slot] = 4.2;\n                                if (isZombieDamageEffective(slot)) {\n                                    w.emit({ type: 'playerDamage', amount: 26 + gameState.currentWave * 0.6, zombieX: zombieRuntime.x[slot], zombieZ: zombieRuntime.z[slot] });\n                                }\n                                playSound('zombie_groan_3', 0.34);\n                            }\n                        } else if (typeName === 'CHARGER') {\n                            zombieRuntime.dashTimer[slot] = Math.max(0, zombieRuntime.dashTimer[slot] - dt);\n                            if (zombieRuntime.dashTimer[slot] > 0) {\n                                tempSpeedMult *= 3.1;\n                            } else {\n                                zombieRuntime.bossAbilityCooldown[slot] -= dt;\n                                if (distSq > 9 && zombieRuntime.bossAbilityCooldown[slot] <= 0) {\n                                    zombieRuntime.dashTimer[slot] = 0.65;\n                                    zombieRuntime.bossAbilityCooldown[slot] = 4.5;\n                                    playSound('zombie_groan_3', 0.3);\n                                }\n                            }\n                        } else if (typeName === 'BEHEMOTH') {\n                            zombieRuntime.bossAbilityCooldown[slot] -= dt;\n                            if (zombieRuntime.bossAbilityCooldown[slot] <= 0) {\n                                zombieRuntime.bossAbilityCooldown[slot] = 5.5;\n                                zombieRuntime.armor[slot] = Math.min(zombieRuntime.maxArmor[slot], zombieRuntime.armor[slot] + 55);\n                                if (distSq < 21.16) {\n                                    if (isZombieDamageEffective(slot)) {\n                                        w.emit({ type: 'playerDamage', amount: 20 + gameState.currentWave * 0.5, zombieX: zombieRuntime.x[slot], zombieZ: zombieRuntime.z[slot] });\n                                    }\n                                }\n                                playSound('zombie_groan_2', 0.28);\n                            }\n                            tempSpeedMult *= 0.92;\n                        } else if (typeName === 'INFERNAL') {\n                            zombieRuntime.burnTick[slot] += dt;\n                            if (distSq < 8.41 && zombieRuntime.burnTick[slot] >= 0.6) {\n                                zombieRuntime.burnTick[slot] = 0;\n                                if (isZombieDamageEffective(slot)) {\n                                    w.emit({ type: 'playerDamage', amount: 7 + gameState.currentWave * 0.35, zombieX: zombieRuntime.x[slot], zombieZ: zombieRuntime.z[slot] });\n                                }\n                            }\n                        } else if (typeName === 'OVERLORD') {\n                            zombieRuntime.summonCooldown[slot] -= dt;\n                            if (zombieRuntime.summonsLeft[slot] > 0 && zombieRuntime.summonCooldown[slot] <= 0) {\n                                zombieRuntime.summonCooldown[slot] = 8.5;\n                                const summonCount = Math.min(2, zombieRuntime.summonsLeft[slot]);\n                                zombieRuntime.summonsLeft[slot] -= summonCount;\n                                gameState.zombiesRemaining += summonCount;\n                                gameState.zombiesToSpawn += summonCount;\n                                for (let i = 0; i < summonCount; i++) {\n                                    const summonType = pickZombieTypeForWave(Math.max(1, gameState.currentWave - 2));\n                                    spawnZombie(summonType);\n                                }\n                                playSound('zombie_groan', 0.35);\n                            }\n                        }\n                    }\n\n                    zombieRuntime.tempSpeedMult[slot] = tempSpeedMult;\n                    zombieRuntime.speed[slot] = zombieRuntime.baseSpeed[slot] * zombieRuntime.mobilityMult[slot] * tempSpeedMult;\n                }\n\n                function updateZombieImpactMotion(dt, ctx) {\n                    const activeSlots = zombieRuntime.activeSlots;\n                    let movedAny = false;\n                    const step = Math.min(dt, 0.05);\n                    for (let i = 0; i < activeSlots.length; i++) {\n                        const slot = activeSlots[i];\n                        if (zombieRuntime.deadPending[slot]) continue;\n\n                        if (zombieRuntime.impactTimer[slot] > 0) {\n                            zombieRuntime.impactTimer[slot] = Math.max(0, zombieRuntime.impactTimer[slot] - step);\n                            if (zombieRuntime.impactTimer[slot] <= 0) {\n                                zombieRuntime.impactDuration[slot] = 0;\n                                zombieRuntime.impactPitch[slot] = 0;\n                                zombieRuntime.impactRoll[slot] = 0;\n                                zombieRuntime.impactYaw[slot] = 0;\n                                zombieRuntime.impactPart[slot] = ZOMBIE_PART_BODY;\n                            }\n                        }\n\n                        const moveX = zombieRuntime.impactMoveX[slot];\n                        const moveZ = zombieRuntime.impactMoveZ[slot];\n                        if (moveX !== 0 || moveZ !== 0) {\n                            zombieRuntime.x[slot] += moveX * step;\n                            zombieRuntime.z[slot] += moveZ * step;\n                            resolveZombieStaticPoint(effectScratch.zombieImpactPoint, zombieRuntime.x[slot], zombieRuntime.z[slot], zombieRuntime.radius[slot] || 0.44);\n                            zombieRuntime.x[slot] = effectScratch.zombieImpactPoint.x;\n                            zombieRuntime.z[slot] = effectScratch.zombieImpactPoint.z;\n                            const damping = Math.pow(0.22, step / 0.12);\n                            zombieRuntime.impactMoveX[slot] = Math.abs(moveX) < 0.03 ? 0 : moveX * damping;\n                            zombieRuntime.impactMoveZ[slot] = Math.abs(moveZ) < 0.03 ? 0 : moveZ * damping;\n                            movedAny = true;\n                        }\n                    }\n                    if (movedAny) ctx.spatialGridBuilt = false;\n                }\n\n                // System 3: Zombie AI (Web Worker + fallback)\n                function zombieAISystem(w, dt, ctx) {\n                    const scaledDt = slowTimeActive ? dt * 0.01 : dt;\n                    const activeSlots = zombieRuntime.activeSlots;\n                    const pos = w.getComponent(playerEntity, 'Position');\n                    if (aiWorker && aiRequestPending && performance.now() - aiRequestStartedAt > 500) {\n                        resetAIWorkerAfterFailure();\n                    }\n\n                    // Cache pressure once per system tick \u2014 it never changes mid-loop.\n                    const pressure = getPerfPressureLevel();\n                    for (let i = 0; i < activeSlots.length; i++) {\n                        const slot = activeSlots[i];\n                        if (zombieRuntime.deadPending[slot]) continue;\n                        zombieRuntime.animTimer[slot] += scaledDt * zombieRuntime.speed[slot] * 3;\n                        if (pressure > 0 && !zombieRuntime.isBoss[slot]) {\n                            const dx = zombieRuntime.x[slot] - pos.x;\n                            const dz = zombieRuntime.z[slot] - pos.z;\n                            const distSq = dx * dx + dz * dz;\n                            const interval = distSq > 120 ? (pressure >= 3 ? 5 : 3) : (distSq > 48 ? (pressure >= 3 ? 3 : 2) : 1);\n                            // Use integer key (slot + 1000) to avoid string alloc + char-hash in hot loop.\n                            if (interval > 1 && !shouldRunBudgetedFrameInt(slot + 1000, interval)) continue;\n                            applyZombieSpecialBehaviorBySlot(w, slot, pos, scaledDt * interval, ctx);\n                        } else {\n                            applyZombieSpecialBehaviorBySlot(w, slot, pos, scaledDt, ctx);\n                        }\n                    }\n\n                    // Apply results\n                    if (pendingAIResultsPacked) {\n                        let movedAny = false;\n                        const count = pendingAIResultsPacked.count;\n                        const ids = pendingAIResultsPacked.ids;\n                        const data = pendingAIResultsPacked.data;\n                        for (let i = 0; i < count; i++) {\n                            const id = ids[i];\n                            const slot = getZombieRuntimeSlot(id);\n                            if (slot < 0 || zombieRuntime.deadPending[slot]) continue;\n                            const j = i * 5;\n                            const state = data[j + 2];\n                            zombieRuntime.x[slot] += data[j] * (slowTimeActive ? 0.01 : 1.0);\n                            zombieRuntime.z[slot] += data[j + 1] * (slowTimeActive ? 0.01 : 1.0);\n\n                            // Keep minimum distance from player (prevent clipping and ensure close-range hits)\n                            const p_dx = zombieRuntime.x[slot] - pos.x;\n                            const p_dz = zombieRuntime.z[slot] - pos.z;\n                            const p_dist = Math.sqrt(p_dx * p_dx + p_dz * p_dz);\n                            const min_player_dist = 1.35;\n                            if (p_dist < min_player_dist) {\n                                const p_ndx = p_dist > 0.001 ? p_dx / p_dist : Math.sin(zombieRuntime.yaw[slot]);\n                                const p_ndz = p_dist > 0.001 ? p_dz / p_dist : Math.cos(zombieRuntime.yaw[slot]);\n                                zombieRuntime.x[slot] = pos.x + p_ndx * min_player_dist;\n                                zombieRuntime.z[slot] = pos.z + p_ndz * min_player_dist;\n                            }\n\n                            if (data[j] || data[j + 1]) movedAny = true;\n                            zombieRuntime.state[slot] = state;\n                            const targetYaw = data[j + 3];\n                            const currentYaw = zombieRuntime.yaw[slot];\n                            let diff = targetYaw - currentYaw;\n                            diff = Math.atan2(Math.sin(diff), Math.cos(diff));\n                            zombieRuntime.yaw[slot] = currentYaw + diff * expSmoothingFactor(scaledDt, 10);\n                            applyZombieAttackBySlot(w, slot, scaledDt);\n                        }\n                        if (movedAny) ctx.spatialGridBuilt = false;\n                        pendingAIResultsPacked = null;\n                    } else if (pendingAIResults) {\n                        let movedAny = false;\n                        for (let i = 0; i < pendingAIResults.length; i++) {\n                            const r = pendingAIResults[i];\n                            const slot = getZombieRuntimeSlot(r.id);\n                            if (slot < 0 || zombieRuntime.deadPending[slot]) continue;\n                            zombieRuntime.x[slot] += r.vx * (slowTimeActive ? 0.01 : 1.0);\n                            zombieRuntime.z[slot] += r.vz * (slowTimeActive ? 0.01 : 1.0);\n\n                            // Keep minimum distance from player\n                            const p_dx = zombieRuntime.x[slot] - pos.x;\n                            const p_dz = zombieRuntime.z[slot] - pos.z;\n                            const p_dist = Math.sqrt(p_dx * p_dx + p_dz * p_dz);\n                            const min_player_dist = 1.35;\n                            if (p_dist < min_player_dist) {\n                                const p_ndx = p_dist > 0.001 ? p_dx / p_dist : Math.sin(zombieRuntime.yaw[slot]);\n                                const p_ndz = p_dist > 0.001 ? p_dz / p_dist : Math.cos(zombieRuntime.yaw[slot]);\n                                zombieRuntime.x[slot] = pos.x + p_ndx * min_player_dist;\n                                zombieRuntime.z[slot] = pos.z + p_ndz * min_player_dist;\n                            }\n\n                            if (r.vx || r.vz) movedAny = true;\n                            zombieRuntime.state[slot] = r.state;\n                            const targetYaw = r.angle;\n                            const currentYaw = zombieRuntime.yaw[slot];\n                            let diff = targetYaw - currentYaw;\n                            diff = Math.atan2(Math.sin(diff), Math.cos(diff));\n                            zombieRuntime.yaw[slot] = currentYaw + diff * expSmoothingFactor(scaledDt, 10);\n                            applyZombieAttackBySlot(w, slot, scaledDt);\n                        }\n                        if (movedAny) ctx.spatialGridBuilt = false;\n                        pendingAIResults = null;\n                    } else if (!aiWorker) {\n                        let movedAny = false;\n                        // Main-thread fallback\n                        const pressure = Math.max(getPerfPressureLevel(), 2);\n                        const frameId = ctx.frameId || gameState.frameId || 1;\n                        const count = activeSlots.length;\n                        const sliceDivisor = pressure >= 3 ? 3 : 2;\n                        const sliceSize = Math.max(24, Math.ceil(count / sliceDivisor));\n                        if (aiFallbackSliceCursor >= count) aiFallbackSliceCursor = 0;\n                        const startIndex = aiFallbackSliceCursor;\n                        aiFallbackSliceCursor = (aiFallbackSliceCursor + sliceSize) % Math.max(1, count);\n                        for (let n = 0; n < count; n++) {\n                            const i = (startIndex + n) % count;\n                            const slot = activeSlots[i];\n                            if (zombieRuntime.deadPending[slot]) continue;\n                            const dx = pos.x - zombieRuntime.x[slot], dz = pos.z - zombieRuntime.z[slot];\n                            const distSq = dx * dx + dz * dz;\n                            const dist = Math.sqrt(distSq) || 0.0001;\n                            const urgent = distSq < 7.5 || zombieRuntime.isBoss[slot] || zombieRuntime.impactTimer[slot] > 0;\n                            const lastUpdateFrame = aiFallbackLastUpdateFrame[slot] || 0;\n                            if (!urgent && n >= sliceSize) {\n                                const staleFrames = frameId - lastUpdateFrame;\n                                const staleLimit = distSq > 130 ? (pressure >= 3 ? 10 : 7) : (pressure >= 3 ? 6 : 4);\n                                if (staleFrames < staleLimit) continue;\n                            }\n                            zombieRuntime.state[slot] = dist > 1.5 ? 1 : 2;\n                            if (dist > 1.5) {\n                                const elapsedFrames = lastUpdateFrame ? Math.max(1, Math.min(8, frameId - lastUpdateFrame)) : 1;\n                                const moveDt = scaledDt * (urgent ? 1 : elapsedFrames);\n                                zombieRuntime.x[slot] += (dx / dist) * zombieRuntime.speed[slot] * moveDt;\n                                zombieRuntime.z[slot] += (dz / dist) * zombieRuntime.speed[slot] * moveDt;\n                                movedAny = true;\n                            } else {\n                                applyZombieAttackBySlot(w, slot, scaledDt);\n                            }\n\n                            // Keep minimum distance from player\n                            const p_dx = zombieRuntime.x[slot] - pos.x;\n                            const p_dz = zombieRuntime.z[slot] - pos.z;\n                            const p_dist = Math.sqrt(p_dx * p_dx + p_dz * p_dz);\n                            const min_player_dist = 1.35;\n                            if (p_dist < min_player_dist) {\n                                const p_ndx = p_dist > 0.001 ? p_dx / p_dist : Math.sin(zombieRuntime.yaw[slot]);\n                                const p_ndz = p_dist > 0.001 ? p_dz / p_dist : Math.cos(zombieRuntime.yaw[slot]);\n                                zombieRuntime.x[slot] = pos.x + p_ndx * min_player_dist;\n                                zombieRuntime.z[slot] = pos.z + p_ndz * min_player_dist;\n                                movedAny = true;\n                            }\n\n                            const targetYaw = Math.atan2(dx, dz);\n                            const currentYaw = zombieRuntime.yaw[slot];\n                            let diff = targetYaw - currentYaw;\n                            diff = Math.atan2(Math.sin(diff), Math.cos(diff));\n                            zombieRuntime.yaw[slot] = currentYaw + diff * expSmoothingFactor(scaledDt, 10);\n                            aiFallbackLastUpdateFrame[slot] = frameId;\n                        }\n                        if (movedAny) ctx.spatialGridBuilt = false;\n                    }\n\n                    updateZombieImpactMotion(scaledDt, ctx);\n\n                    // Dispatch next worker request after consuming returned buffers.\n                    if (aiWorker && !aiRequestPending && activeSlots.length > 0) {\n                        let activeAIInputCount = 0;\n                        for (let i = 0; i < activeSlots.length; i++) {\n                            const slot = activeSlots[i];\n                            if (zombieRuntime.deadPending[slot]) continue;\n                            const eid = zombieRuntime.entityId[slot];\n                            aiInputIdsBuffer[activeAIInputCount] = eid;\n                            aiInputPackedBuffer[activeAIInputCount * 3] = zombieRuntime.x[slot];\n                            aiInputPackedBuffer[activeAIInputCount * 3 + 1] = zombieRuntime.z[slot];\n                            aiInputPackedBuffer[activeAIInputCount * 3 + 2] = zombieRuntime.speed[slot];\n                            activeAIInputCount++;\n                        }\n                        if (activeAIInputCount === 0) return;\n                        aiWorker.postMessage({\n                            type: 'update',\n                            data: {\n                                playerX: pos.x,\n                                playerZ: pos.z,\n                                dt: scaledDt,\n                                generation: aiGeneration,\n                                count: activeAIInputCount,\n                                ids: aiInputIdsBuffer,\n                                packed: aiInputPackedBuffer,\n                                outIds: aiOutputIdsBuffer,\n                                out: aiOutputDataBuffer\n                            }\n                        }, [aiInputIdsBuffer.buffer, aiInputPackedBuffer.buffer, aiOutputIdsBuffer.buffer, aiOutputDataBuffer.buffer]);\n                        aiRequestPending = true;\n                        aiRequestStartedAt = performance.now();\n                    }\n                }\n\n                // System 3.2: local crowd solver keeps the horde from collapsing into one stack.\n                function zombieSeparationSystem(w, dt, ctx) {\n                    const activeSlots = zombieRuntime.activeSlots;\n                    if (activeSlots.length < 2) return;\n                    const pressure = getPerfPressureLevel();\n                    if (activeSlots.length > 80 || pressure >= 1) {\n                        const interval = pressure >= 3 ? 3 : 2;\n                        if (!shouldRunBudgetedFrame('zombie-separation', interval)) return;\n                    }\n                    const pos = w.getComponent(playerEntity, 'Position');\n                    const capacity = activeSlots.length;\n                    if (!ctx.zombieSeparationSoa || ctx.zombieSeparationSoa.ids.length < capacity) {\n                        ctx.zombieSeparationSoa = {\n                            ids: new Int32Array(capacity),\n                            slots: new Int16Array(capacity),\n                            head: new Int16Array(ZOMBIE_GRID_CELL_COUNT),\n                            next: new Int16Array(capacity),\n                            candidates: [],\n                            point: { x: 0, z: 0 }\n                        };\n                        ctx.zombieSeparationSoa.head.fill(-1);\n                        ctx.zombieSeparationSoa.next.fill(-1);\n                    }\n                    const soa = ctx.zombieSeparationSoa;\n                    let count = 0;\n                    for (let i = 0; i < activeSlots.length; i++) {\n                        const slot = activeSlots[i];\n                        if (zombieRuntime.deadPending[slot]) continue;\n                        soa.slots[count] = slot;\n                        soa.ids[count] = zombieRuntime.entityId[slot];\n                        count++;\n                    }\n                    if (count < 2) return;\n\n                    const candidates = soa.candidates;\n                    const maxStep = Math.min(0.16, 0.045 + dt * 2.2);\n                    let movedAny = false;\n\n                    const iterations = (activeSlots.length > 120 || pressure >= 2) ? 1 : 2;\n                    for (let iteration = 0; iteration < iterations; iteration++) {\n                        rebuildZombieSeparationCells(soa, count);\n                        let iterationMoved = false;\n\n                        for (let i = 0; i < count; i++) {\n                            const slotA = soa.slots[i];\n                            const radiusA = zombieRuntime.radius[slotA];\n                            const queryRadius = radiusA + ZOMBIE_SEPARATION_MAX_RADIUS + ZOMBIE_SEPARATION_SKIN;\n                            queryZombieSeparationCells(\n                                soa,\n                                zombieRuntime.x[slotA] - queryRadius,\n                                zombieRuntime.x[slotA] + queryRadius,\n                                zombieRuntime.z[slotA] - queryRadius,\n                                zombieRuntime.z[slotA] + queryRadius,\n                                candidates\n                            );\n\n                            for (let c = 0; c < candidates.length; c++) {\n                                const j = candidates[c];\n                                if (j <= i) continue;\n                                const slotB = soa.slots[j];\n                                let dx = zombieRuntime.x[slotB] - zombieRuntime.x[slotA];\n                                let dz = zombieRuntime.z[slotB] - zombieRuntime.z[slotA];\n                                let distSq = dx * dx + dz * dz;\n                                if (distSq < 0.000001) {\n                                    const seed = ((soa.ids[i] * 73856093) ^ (soa.ids[j] * 19349663)) >>> 0;\n                                    const angle = (seed % 6283) * 0.001;\n                                    dx = Math.cos(angle) * 0.001;\n                                    dz = Math.sin(angle) * 0.001;\n                                    distSq = dx * dx + dz * dz;\n                                }\n                                const desired = radiusA + zombieRuntime.radius[slotB] + ZOMBIE_SEPARATION_SKIN;\n                                if (distSq >= desired * desired) continue;\n\n                                const dist = Math.sqrt(distSq);\n                                const nx = dx / dist;\n                                const nz = dz / dist;\n                                const push = Math.min((desired - dist) * ZOMBIE_SEPARATION_STRENGTH, maxStep);\n                                const invMassA = zombieRuntime.invMass[slotA];\n                                const invMassB = zombieRuntime.invMass[slotB];\n                                const totalInvMass = invMassA + invMassB;\n                                if (totalInvMass <= 0) continue;\n                                const pushA = push * (invMassA / totalInvMass);\n                                const pushB = push * (invMassB / totalInvMass);\n                                resolveZombieStaticPoint(soa.point, zombieRuntime.x[slotA] - nx * pushA, zombieRuntime.z[slotA] - nz * pushA, radiusA);\n                                zombieRuntime.x[slotA] = soa.point.x;\n                                zombieRuntime.z[slotA] = soa.point.z;\n                                resolveZombieStaticPoint(soa.point, zombieRuntime.x[slotB] + nx * pushB, zombieRuntime.z[slotB] + nz * pushB, zombieRuntime.radius[slotB]);\n                                zombieRuntime.x[slotB] = soa.point.x;\n                                zombieRuntime.z[slotB] = soa.point.z;\n                                iterationMoved = true;\n                            }\n                        }\n\n                        if (!iterationMoved) {\n                            break;\n                        }\n                        movedAny = true;\n                    }\n\n                    if (!movedAny) return;\n                    for (let i = 0; i < count; i++) {\n                        const slot = soa.slots[i];\n                        resolveZombieStaticPoint(soa.point, zombieRuntime.x[slot], zombieRuntime.z[slot], zombieRuntime.radius[slot]);\n                        zombieRuntime.x[slot] = soa.point.x;\n                        zombieRuntime.z[slot] = soa.point.z;\n\n                        // Keep minimum distance from player (prevent clipping and ensure close-range hits)\n                        const p_dx = zombieRuntime.x[slot] - pos.x;\n                        const p_dz = zombieRuntime.z[slot] - pos.z;\n                        const p_dist = Math.sqrt(p_dx * p_dx + p_dz * p_dz);\n                        const min_player_dist = 1.35;\n                        if (p_dist < min_player_dist) {\n                            const p_ndx = p_dist > 0.001 ? p_dx / p_dist : Math.sin(zombieRuntime.yaw[slot]);\n                            const p_ndz = p_dist > 0.001 ? p_dz / p_dist : Math.cos(zombieRuntime.yaw[slot]);\n                            zombieRuntime.x[slot] = pos.x + p_ndx * min_player_dist;\n                            zombieRuntime.z[slot] = pos.z + p_ndz * min_player_dist;\n                        }\n                    }\n                    clearZombieSeparationCells(soa);\n                    ctx.spatialGridBuilt = false;\n                }\n\n                // ===== BODY PART HITBOX DEFINITIONS (relative to zombie position) =====\n                // Each hitbox: { name, offsetY, offsetX, offsetZ, halfW, halfH, halfD }\n                const bodyPartHitboxes = [\n                    { name: 'head', offsetY: 1.95, offsetX: 0, offsetZ: 0, halfW: 0.2, halfH: 0.2, halfD: 0.2, dmgMult: 3.0 },\n                    // Slightly raise/shrink torso hitbox so low leg shots are not stolen by body hit detection\n                    { name: 'body', offsetY: 1.23, offsetX: 0, offsetZ: 0, halfW: 0.3, halfH: 0.5, halfD: 0.2, dmgMult: 1.0 },\n                    { name: 'leftArm', offsetY: 1.05, offsetX: -0.5, offsetZ: 0, halfW: 0.12, halfH: 0.45, halfD: 0.12, dmgMult: 0.9 },\n                    { name: 'rightArm', offsetY: 1.05, offsetX: 0.5, offsetZ: 0, halfW: 0.12, halfH: 0.45, halfD: 0.12, dmgMult: 0.9 },\n                    { name: 'leftLeg', offsetY: 0.43, offsetX: -0.15, offsetZ: 0, halfW: 0.14, halfH: 0.28, halfD: 0.14, dmgMult: 0.95 },\n                    { name: 'rightLeg', offsetY: 0.43, offsetX: 0.15, offsetZ: 0, halfW: 0.14, halfH: 0.28, halfD: 0.14, dmgMult: 0.95 }\n                ];\n                const hitboxScratch = { centerX: 0, centerY: 0, centerZ: 0, halfX: 0, halfY: 0, halfZ: 0 };\n                const bulletPoseCache = {\n                    frameId: -1,\n                    count: 0,\n                    stamp: new Uint32Array(ZOMBIE_POOL_SIZE),\n                    poseIndex: new Int16Array(ZOMBIE_POOL_SIZE),\n                    poses: []\n                };\n                bulletPoseCache.poseIndex.fill(-1);\n\n                function getZombieHitScale(zc) {\n                    return (zc && zc.scale) || 1;\n                }\n\n                function getZombieSlotHitScale(slot) {\n                    return zombieRuntime.scale[slot] || 1;\n                }\n\n                function getZombieHitProps(zc) {\n                    return (zc && zombieTypeProportions[zc.typeName]) || defaultProportions;\n                }\n\n                function getZombieSlotHitProps(slot) {\n                    return zombieTypeProportions[zombieRuntime.typeName[slot]] || defaultProportions;\n                }\n\n                function getZombieHitRootY(pose) {\n                    return pose && typeof pose.rootY === 'number' ? pose.rootY : 0;\n                }\n\n                function getHitboxPartProps(hitbox, props) {\n                    if (hitbox.name === 'head') return props.head;\n                    if (hitbox.name === 'body') return props.body;\n                    if (hitbox.name === 'leftArm' || hitbox.name === 'rightArm') return props.arm;\n                    if (hitbox.name === 'leftLeg' || hitbox.name === 'rightLeg') return props.leg;\n                    return defaultProportions.body;\n                }\n\n                function expandHitboxForPitch(out, rx) {\n                    const absCos = Math.abs(Math.cos(rx));\n                    const absSin = Math.abs(Math.sin(rx));\n                    const halfY = out.halfY;\n                    const halfZ = out.halfZ;\n                    out.halfY = halfY * absCos + halfZ * absSin;\n                    out.halfZ = halfY * absSin + halfZ * absCos;\n                }\n\n                function expandHitboxForRoll(out, rz) {\n                    const absCos = Math.abs(Math.cos(rz));\n                    const absSin = Math.abs(Math.sin(rz));\n                    const halfX = out.halfX;\n                    const halfY = out.halfY;\n                    out.halfX = halfX * absCos + halfY * absSin;\n                    out.halfY = halfX * absSin + halfY * absCos;\n                }\n\n                function getHitboxMetricsForValues(hitbox, scale, props, bodyParts, pose, out) {\n                    const partProps = getHitboxPartProps(hitbox, props);\n                    const isArm = hitbox.name === 'leftArm' || hitbox.name === 'rightArm';\n                    const isLeg = hitbox.name === 'leftLeg' || hitbox.name === 'rightLeg';\n                    let partRx = 0;\n                    let partRz = 0;\n                    const visualPad = hitbox.name === 'body' ? 1.18 : (hitbox.name === 'head' ? 1.22 : 1.34);\n                    const rootY = getZombieHitRootY(pose);\n                    if (hitbox.name === 'head') {\n                        partRx = pose && pose.headRx || 0;\n                        partRz = pose && pose.headRz || 0;\n                        out.centerX = 0;\n                        out.centerY = rootY + 1.90 * scale;\n                        out.centerZ = 0;\n                    } else if (hitbox.name === 'body') {\n                        partRx = pose && pose.bodyRx || 0;\n                        partRz = pose && pose.bodyRz || 0;\n                        out.centerX = 0;\n                        out.centerY = rootY + 1.15 * scale;\n                        out.centerZ = 0;\n                    } else if (isArm) {\n                        const side = hitbox.name === 'leftArm' ? -1 : 1;\n                        const rx = hitbox.name === 'leftArm' ? (pose && pose.leftArmRx || 0) : (pose && pose.rightArmRx || 0);\n                        const rz = hitbox.name === 'leftArm' ? (pose && pose.leftArmRz || 0) : (pose && pose.rightArmRz || 0);\n                        partRx = rx;\n                        partRz = rz;\n                        const localY = -0.36 * scale;\n                        const cosX = Math.cos(rx);\n                        const sinX = Math.sin(rx);\n                        const cosZ = Math.cos(rz);\n                        const sinZ = Math.sin(rz);\n                        const y1 = localY * cosX;\n                        const z1 = localY * sinX;\n                        out.centerX = side * 0.42 * props.body[0] * scale - y1 * sinZ;\n                        out.centerY = rootY + 1.50 * scale + y1 * cosZ;\n                        out.centerZ = z1;\n                    } else if (isLeg) {\n                        const side = hitbox.name === 'leftLeg' ? -1 : 1;\n                        const rx = hitbox.name === 'leftLeg' ? (pose && pose.leftLegRx || 0) : (pose && pose.rightLegRx || 0);\n                        partRx = rx;\n                        const localY = -0.32 * scale;\n                        out.centerX = side * 0.14 * props.body[0] * scale;\n                        out.centerY = rootY + 0.67 * scale + localY * Math.cos(rx);\n                        out.centerZ = localY * Math.sin(rx);\n                    } else {\n                        out.centerX = hitbox.offsetX * scale;\n                        out.centerY = rootY + hitbox.offsetY * scale;\n                        out.centerZ = hitbox.offsetZ * scale;\n                    }\n                    out.halfX = hitbox.halfW * scale * (partProps[0] || 1) * visualPad;\n                    out.halfY = hitbox.halfH * scale * (partProps[1] || 1) * visualPad;\n                    out.halfZ = hitbox.halfD * scale * (partProps[2] || 1) * visualPad;\n\n                    if (hitbox.name === 'body' && bodyParts && zombiePartAlive(bodyParts, 'belly')) {\n                        out.halfX = Math.max(out.halfX, 0.46 * scale * props.body[0]);\n                        out.halfY = Math.max(out.halfY, 0.58 * scale * props.body[1]);\n                        out.halfZ = Math.max(out.halfZ, 0.36 * scale * props.body[2]);\n                    }\n                    if (isArm) {\n                        out.halfX = Math.max(out.halfX, 0.16 * scale * props.arm[0]);\n                        out.halfY = Math.max(out.halfY, 0.46 * scale * props.arm[1]);\n                        out.halfZ = Math.max(out.halfZ, 0.16 * scale * props.arm[2]);\n                    } else if (isLeg) {\n                        out.halfX = Math.max(out.halfX, 0.17 * scale * props.leg[0]);\n                        out.halfY = Math.max(out.halfY, 0.34 * scale * props.leg[1]);\n                        out.halfZ = Math.max(out.halfZ, 0.17 * scale * props.leg[2]);\n                    }\n                    if (partRx) expandHitboxForPitch(out, partRx);\n                    if (partRz) expandHitboxForRoll(out, partRz);\n                    return out;\n                }\n\n                function getHitboxMetrics(hitbox, zc, bodyParts, pose, out) {\n                    return getHitboxMetricsForValues(hitbox, getZombieHitScale(zc), getZombieHitProps(zc), bodyParts, pose, out);\n                }\n\n                function getHitboxMetricsForSlot(hitbox, slot, bodyParts, pose, out) {\n                    return getHitboxMetricsForValues(hitbox, getZombieSlotHitScale(slot), getZombieSlotHitProps(slot), bodyParts, pose, out);\n                }\n\n                function getHitboxWorldCenterForValues(out, zombieX, zombieY, zombieZ, zombieYaw, hitbox, scale, props, bodyParts, pose) {\n                    const m = getHitboxMetricsForValues(hitbox, scale, props, bodyParts, pose, hitboxScratch);\n                    const c = Math.cos(zombieYaw);\n                    const s = Math.sin(zombieYaw);\n                    out.set(\n                        zombieX + m.centerX * c + m.centerZ * s,\n                        zombieY + m.centerY,\n                        zombieZ - m.centerX * s + m.centerZ * c\n                    );\n                    return out;\n                }\n\n                function getHitboxWorldCenter(out, zombiePos, zombieYaw, hitbox, zc, bodyParts, pose) {\n                    return getHitboxWorldCenterForValues(\n                        out,\n                        zombiePos.x,\n                        zombiePos.y || 0,\n                        zombiePos.z,\n                        zombieYaw,\n                        hitbox,\n                        getZombieHitScale(zc),\n                        getZombieHitProps(zc),\n                        bodyParts,\n                        pose\n                    );\n                }\n\n                function getZombieHitReachRadiusForValues(scale, props, bodyParts, pose) {\n                    const noLegs = bodyParts && !zombiePartAlive(bodyParts, 'leftLeg') && !zombiePartAlive(bodyParts, 'rightLeg');\n                    const horizontalReach = Math.max(\n                        0.78 * props.body[0],\n                        0.55 * props.body[0] + 0.24 * props.arm[0],\n                        0.32 * props.head[0],\n                        0.22 * props.body[0] + 0.22 * props.leg[0]\n                    );\n                    const verticalReach = (noLegs ? 0.95 : 1.32) + Math.abs(getZombieHitRootY(pose)) * 0.25;\n                    return Math.max(1.12, scale * Math.max(horizontalReach, verticalReach));\n                }\n\n                function getZombieHitReachRadius(zc, bodyParts, pose) {\n                    return getZombieHitReachRadiusForValues(getZombieHitScale(zc), getZombieHitProps(zc), bodyParts, pose);\n                }\n\n                function rayVsLocalAabb(localOriginX, localOriginY, localOriginZ, localDirX, localDirY, localDirZ, minX, maxX, minY, maxY, minZ, maxZ, travelDist) {\n                    let tmin = (minX - localOriginX) / (localDirX || 1e-10);\n                    let tmax = (maxX - localOriginX) / (localDirX || 1e-10);\n                    if (tmin > tmax) { let t = tmin; tmin = tmax; tmax = t; }\n\n                    let tymin = (minY - localOriginY) / (localDirY || 1e-10);\n                    let tymax = (maxY - localOriginY) / (localDirY || 1e-10);\n                    if (tymin > tymax) { let t = tymin; tymin = tymax; tymax = t; }\n\n                    if (tmin > tymax || tymin > tmax) return null;\n                    if (tymin > tmin) tmin = tymin;\n                    if (tymax < tmax) tmax = tymax;\n\n                    let tzmin = (minZ - localOriginZ) / (localDirZ || 1e-10);\n                    let tzmax = (maxZ - localOriginZ) / (localDirZ || 1e-10);\n                    if (tzmin > tzmax) { let t = tzmin; tzmin = tzmax; tzmax = t; }\n\n                    if (tmin > tzmax || tzmin > tmax) return null;\n                    if (tzmin > tmin) tmin = tzmin;\n                    if (tzmax < tmax) tmax = tzmax;\n\n                    if (tmin < 0 && tmax < 0) return null;\n                    const t = tmin >= 0 ? tmin : tmax;\n                    return t >= 0 && t <= travelDist ? t : null;\n                }\n\n                function checkBodyPartHitRayForValues(prevPos, bulletDir, travelDist, zombieX, zombieY, zombieZ, zombieYaw, hitbox, scale, props, bodyParts, pose) {\n                    const m = getHitboxMetricsForValues(hitbox, scale, props, bodyParts, pose, hitboxScratch);\n                    // Transform ray start and direction to zombie local space\n                    const startX = prevPos.x - zombieX;\n                    const startZ = prevPos.z - zombieZ;\n                    const startY = prevPos.y - zombieY;\n\n                    const cosY = Math.cos(zombieYaw);\n                    const sinY = Math.sin(zombieYaw);\n\n                    const localOriginX = startX * cosY - startZ * sinY;\n                    const localOriginZ = startX * sinY + startZ * cosY;\n                    const localOriginY = startY;\n\n                    const localDirX = bulletDir.x * cosY - bulletDir.z * sinY;\n                    const localDirZ = bulletDir.x * sinY + bulletDir.z * cosY;\n                    const localDirY = bulletDir.y;\n\n                    // Ray vs AABB intersection in local space\n                    return rayVsLocalAabb(\n                        localOriginX, localOriginY, localOriginZ,\n                        localDirX, localDirY, localDirZ,\n                        m.centerX - m.halfX, m.centerX + m.halfX,\n                        m.centerY - m.halfY, m.centerY + m.halfY,\n                        m.centerZ - m.halfZ, m.centerZ + m.halfZ,\n                        travelDist\n                    );\n                }\n\n                // Helper: Check bullet ray vs zombie body part (using rotated local space)\n                function checkBodyPartHitRay(prevPos, bulletDir, travelDist, zombiePos, zombieYaw, hitbox, zc, bodyParts, pose) {\n                    return checkBodyPartHitRayForValues(\n                        prevPos,\n                        bulletDir,\n                        travelDist,\n                        zombiePos.x,\n                        zombiePos.y || 0,\n                        zombiePos.z,\n                        zombieYaw,\n                        hitbox,\n                        getZombieHitScale(zc),\n                        getZombieHitProps(zc),\n                        bodyParts,\n                        pose\n                    );\n                }\n\n                function checkZombieSilhouetteHitRayForValues(prevPos, bulletDir, travelDist, zombieX, zombieY, zombieZ, zombieYaw, scale, props, bodyParts, pose) {\n                    if (bodyParts && !zombiePartAlive(bodyParts, 'body')) return null;\n                    const centerY = getZombieHitRootY(pose) + 1.05 * scale;\n                    const halfX = 0.48 * scale * props.body[0];\n                    const halfY = 0.9 * scale * props.body[1];\n                    const halfZ = 0.34 * scale * props.body[2];\n                    const startX = prevPos.x - zombieX;\n                    const startZ = prevPos.z - zombieZ;\n                    const startY = prevPos.y - zombieY;\n                    const cosY = Math.cos(zombieYaw);\n                    const sinY = Math.sin(zombieYaw);\n                    const localOriginX = startX * cosY - startZ * sinY;\n                    const localOriginZ = startX * sinY + startZ * cosY;\n                    const localOriginY = startY;\n                    const localDirX = bulletDir.x * cosY - bulletDir.z * sinY;\n                    const localDirZ = bulletDir.x * sinY + bulletDir.z * cosY;\n                    const localDirY = bulletDir.y;\n                    return rayVsLocalAabb(\n                        localOriginX, localOriginY, localOriginZ,\n                        localDirX, localDirY, localDirZ,\n                        -halfX, halfX,\n                        centerY - halfY, centerY + halfY,\n                        -halfZ, halfZ,\n                        travelDist\n                    );\n                }\n\n                function checkZombieSilhouetteHitRay(prevPos, bulletDir, travelDist, zombiePos, zombieYaw, zc, bodyParts, pose) {\n                    return checkZombieSilhouetteHitRayForValues(\n                        prevPos,\n                        bulletDir,\n                        travelDist,\n                        zombiePos.x,\n                        zombiePos.y || 0,\n                        zombiePos.z,\n                        zombieYaw,\n                        getZombieHitScale(zc),\n                        getZombieHitProps(zc),\n                        bodyParts,\n                        pose\n                    );\n                }\n\n                function canBulletReachZombieForValues(prevPos, bulletDir, travelDist, zombieX, zombieY, zombieZ, scale, props, bodyParts, pose) {\n                    const reachRadius = getZombieHitReachRadiusForValues(scale, props, bodyParts, pose);\n                    const centerX = zombieX;\n                    const centerY = zombieY + getZombieHitRootY(pose) + 1.05 * scale;\n                    const centerZ = zombieZ;\n                    const relX = centerX - prevPos.x;\n                    const relY = centerY - prevPos.y;\n                    const relZ = centerZ - prevPos.z;\n                    const along = relX * bulletDir.x + relY * bulletDir.y + relZ * bulletDir.z;\n                    if (along < -reachRadius || along > travelDist + reachRadius) return false;\n                    const distSq = relX * relX + relY * relY + relZ * relZ;\n                    const perpSq = distSq - along * along;\n                    return perpSq <= reachRadius * reachRadius;\n                }\n\n                function canBulletReachZombie(prevPos, bulletDir, travelDist, zombiePos, zc, bodyParts, pose) {\n                    return canBulletReachZombieForValues(\n                        prevPos,\n                        bulletDir,\n                        travelDist,\n                        zombiePos.x,\n                        zombiePos.y || 0,\n                        zombiePos.z,\n                        getZombieHitScale(zc),\n                        getZombieHitProps(zc),\n                        bodyParts,\n                        pose\n                    );\n                }\n\n                function canBulletReachZombieCheap(prevPos, bulletDir, travelDist, zombieX, zombieY, zombieZ, scale) {\n                    const reachRadius = 1.1 * scale;\n                    const centerY = zombieY + 1.05 * scale;\n                    const relX = zombieX - prevPos.x;\n                    const relY = centerY - prevPos.y;\n                    const relZ = zombieZ - prevPos.z;\n                    const along = relX * bulletDir.x + relY * bulletDir.y + relZ * bulletDir.z;\n                    if (along < -reachRadius || along > travelDist + reachRadius) return false;\n                    const distSq = relX * relX + relY * relY + relZ * relZ;\n                    const perpSq = distSq - along * along;\n                    return perpSq <= reachRadius * reachRadius;\n                }\n\n                function shouldUseReducedBulletHitboxes(ws, zSlot, zombieX, zombieZ) {\n                    if (zombieRuntime.isBoss[zSlot] || zombieRuntime.impactTimer[zSlot] > 0) return false;\n                    if (ws && (ws.name === 'SHOTGUN' || ws.name === 'SNIPER' || ws.isMelee)) return false;\n                    const pressure = getPerfPressureLevel();\n                    const playerPos = world.getComponent(playerEntity, 'Position');\n                    const dx = zombieX - playerPos.x;\n                    const dz = zombieZ - playerPos.z;\n                    const distSq = dx * dx + dz * dz;\n                    return (pressure >= 2 && distSq > 72) || (pressure >= 3 && distSq > 34);\n                }\n\n                function getBulletCachedZombiePose(slot, ctx) {\n                    const frameId = ctx.frameId || gameState.frameId || 0;\n                    if (bulletPoseCache.frameId !== frameId) {\n                        bulletPoseCache.frameId = frameId;\n                        bulletPoseCache.count = 0;\n                    }\n                    if (bulletPoseCache.stamp[slot] === frameId) {\n                        const cachedIndex = bulletPoseCache.poseIndex[slot];\n                        if (cachedIndex >= 0) return bulletPoseCache.poses[cachedIndex];\n                    }\n                    const cacheIndex = bulletPoseCache.count++;\n                    const pose = bulletPoseCache.poses[cacheIndex] || (bulletPoseCache.poses[cacheIndex] = {});\n                    applyZombieImpactPose(slot, computeZombiePose(zombieRuntime.animTimer[slot] || 0, zombieRuntime.state[slot] || 1, zombieRuntime.bodyMask[slot], pose));\n                    bulletPoseCache.stamp[slot] = frameId;\n                    bulletPoseCache.poseIndex[slot] = cacheIndex;\n                    return pose;\n                }\n\n                // Helper: Destroy a body part visually\n                function destroyBodyPart(zombieMesh, partName, cutInfo = null) {\n                    const parts = zombieMesh.userData.parts || cacheZombieParts(zombieMesh);\n                    if (parts[partName]) {\n                        parts[partName].visible = false;\n                        parts[partName].userData.logicalVisible = false;\n                    }\n                    if (partName === 'head') {\n                        if (parts.leftEye) { parts.leftEye.visible = false; parts.leftEye.userData.logicalVisible = false; }\n                        if (parts.rightEye) { parts.rightEye.visible = false; parts.rightEye.userData.logicalVisible = false; }\n                    }\n                    const stumpName = stumpByPart[partName];\n                    if (stumpName && parts[stumpName]) {\n                        parts[stumpName].visible = true;\n                        parts[stumpName].userData.logicalVisible = true;\n                        parts[stumpName].rotation.set(0, 0, 0);\n                    }\n                }\n\n                function spawnBulletShatter(partName, zombiePos, zombieYaw, hitPos, bulletDir, weaponIndex) {\n                    const weapon = weaponStates[weaponIndex] || {};\n                    const heavy = partName === 'head' || weapon.name === 'SHOTGUN' || weapon.name === 'SNIPER';\n                    spawnBloodBurst(hitPos, heavy ? 16 : 9);\n                }\n\n                // Helper: Spawn blood mist at hit location.\n                function spawnBloodBurst(position, count) {\n                    const pressure = getPerfPressureLevel();\n                    if (pressure >= 3 && bloodMistRuntime.activeSlots.length >= Math.max(2, Math.floor(MAX_MIST_EFFECTS * 0.55))) return;\n                    if (pressure >= 2 && Math.random() < 0.18) return;\n                    if (!shouldSpawnBudgetedEffect('blood-mist', count >= 12 ? 2 : 1, position)) return;\n                    count = Math.min(count, 18);\n                    const mistScale = pressure >= 3 ? 0.45 : (pressure >= 2 ? 0.65 : (pressure >= 1 ? 0.82 : 1));\n                    const mistCount = Math.min(24, Math.max(3, Math.floor(count * 1.15 * mistScale)));\n                    if (mistCount <= 0) return;\n                    const slot = acquireBloodMistSlot();\n                    if (slot < 0) return;\n                    bloodMistRuntime.count[slot] = mistCount;\n                    const start = slot * MAX_MIST_POINTS;\n                    // PointsMaterial applies perspective size attenuation after our shader\n                    // replacement, so this value is in world-space rather than pixels.\n                    const baseSize = 0.032 + Math.random() * 0.028;\n                    for (let i = 0; i < mistCount; i++) {\n                        const particle = start + i;\n                        const k = particle * 3;\n                        bloodMistOrigin[k] = position.x + (Math.random() - 0.5) * 0.15;\n                        bloodMistOrigin[k + 1] = position.y + (Math.random() - 0.5) * 0.15;\n                        bloodMistOrigin[k + 2] = position.z + (Math.random() - 0.5) * 0.15;\n                        bloodMistVelocity[k] = (Math.random() - 0.5) * 2.5;\n                        bloodMistVelocity[k + 1] = (Math.random() - 0.1) * 1.5;\n                        bloodMistVelocity[k + 2] = (Math.random() - 0.5) * 2.5;\n                        bloodMistBirth[particle] = bloodMistTime;\n                        bloodMistLife[particle] = 0.55 + Math.random() * 0.22;\n                        bloodMistBaseSize[particle] = baseSize * (0.82 + Math.random() * 0.36);\n                    }\n                    for (let i = mistCount; i < MAX_MIST_POINTS; i++) bloodMistLife[start + i] = 0;\n                    bloodMistGpuDirty = true;\n                }\n\n                // Helper: Spawn a severed body part that flies off with realistic physics\n                function spawnSeveredPart(zombieMesh, partName, zombiePos, slashDir, cutInfo = null) {\n                    const pressure = getPerfPressureLevel();\n                    if (pressure >= 3 && activeSeveredParts.length >= Math.max(2, Math.floor(MAX_SEVERED_PARTS * 0.5))) return;\n                    if (!shouldSpawnBudgetedEffect('severed-part', partName === 'head' ? 3 : 2, zombiePos)) return;\n                    let partY = 0;\n                    let partX = 0;\n                    let poolKey = 'leg';\n\n                    switch (partName) {\n                        case 'head': {\n                            poolKey = 'head';\n                            partY = 1.90;\n                            break;\n                        }\n                        case 'leftArm':\n                        case 'rightArm': {\n                            poolKey = 'arm';\n                            partY = 1.50;\n                            partX = partName === 'leftArm' ? -0.42 : 0.42;\n                            break;\n                        }\n                        case 'leftLeg':\n                        case 'rightLeg': {\n                            poolKey = 'leg';\n                            partY = 0.67;\n                            partX = partName === 'leftLeg' ? -0.14 : 0.14;\n                            break;\n                        }\n                        default: return;\n                    }\n\n                    const runtimeSlot = acquireSeveredSlot();\n                    if (runtimeSlot < 0) return;\n                    const severedGroup = severedPools[poolKey].acquire();\n                    severedRuntime.group[runtimeSlot] = severedGroup;\n                    severedRuntime.kind[runtimeSlot] = poolKey === 'head' ? SEVERED_KIND_HEAD : (poolKey === 'arm' ? SEVERED_KIND_ARM : SEVERED_KIND_LEG);\n                    // Scale severed part to match zombie's type proportions\n                    const zombieScale = zombieMesh.scale.x || 1;\n                    const typeName = zombieMesh.userData.typeName;\n                    const typeProps = zombieTypeProportions[typeName] || defaultProportions;\n                    let pScaleX = zombieScale;\n                    let pScaleY = zombieScale;\n                    let pScaleZ = zombieScale;\n                    if (poolKey === 'head') { pScaleX *= typeProps.head[0]; pScaleY *= typeProps.head[1]; pScaleZ *= typeProps.head[2]; }\n                    else if (poolKey === 'arm') { pScaleX *= typeProps.arm[0]; pScaleY *= typeProps.arm[1]; pScaleZ *= typeProps.arm[2]; }\n                    else { pScaleX *= typeProps.leg[0]; pScaleY *= typeProps.leg[1]; pScaleZ *= typeProps.leg[2]; }\n                    severedGroup.scale.set(pScaleX, pScaleY, pScaleZ);\n                    attachToScene(severedGroup);\n                    severedGroup.visible = true;\n                    applySeveredPalette(severedGroup, zombieMesh);\n\n                    // Position at zombie world location\n                    const zombieY = zombiePos.y || 0;\n                    severedGroup.position.set(\n                        zombiePos.x + partX,\n                        zombieY + partY,\n                        zombiePos.z\n                    );\n                    severedGroup.rotation.y = zombieMesh.rotation.y;\n                    const launchSpeed = 3.5 + Math.random() * 4.5;\n                    severedRuntime.vx[runtimeSlot] = slashDir.x * launchSpeed + (Math.random() - 0.5) * 2.5;\n                    severedRuntime.vy[runtimeSlot] = 2.0 + Math.random() * 3.0;\n                    severedRuntime.vz[runtimeSlot] = slashDir.z * launchSpeed + (Math.random() - 0.5) * 2.5;\n                    severedRuntime.rvx[runtimeSlot] = (Math.random() - 0.5) * 14;\n                    severedRuntime.rvy[runtimeSlot] = (Math.random() - 0.5) * 8;\n                    severedRuntime.rvz[runtimeSlot] = (Math.random() - 0.5) * 14;\n                }\n\n                // System 3.5: Projectile physics (real ballistics + penetration + bullet holes + dismemberment)\n                function bulletSystem(w, dt, ctx) {\n                    if (weaponTutorialActive) return;\n                    // Scale time if slow time skill is active\n                    const scaledDt = slowTimeActive ? dt * 0.01 : dt;\n                    if (activeProjectileSlots.length === 0) return;\n                    const grid = getZombieSpatialIndex(ctx, w);\n\n                    for (let activeIdx = activeProjectileSlots.length - 1; activeIdx >= 0; activeIdx--) {\n                        const slot = activeProjectileSlots[activeIdx];\n                        if (!projectileActive[slot]) continue;\n\n                        projectileLifetime[slot] -= scaledDt;\n                        if (projectileLifetime[slot] <= 0) {\n                            releaseProjectileSlot(slot);\n                            continue;\n                        }\n\n                        // Homing bullet logic for Smart Pistol\n                        const targetSlot = projectileSmartTargetSlot[slot];\n                        if (targetSlot >= 0 && !isSmartProjectileTargetValid(slot)) {\n                            releaseProjectileSlot(slot);\n                            continue;\n                        }\n                        if (targetSlot >= 0) {\n                            const age = 3.0 - projectileLifetime[slot];\n                            const straightTime = Math.max(0, projectileSmartStraightTime[slot]);\n                            if (age > straightTime) {\n                                const targetPos = getSmartPistolTargetPoint(targetSlot, effectScratch.v4);\n                                const currentPos = effectScratch.v5.set(projectileX[slot], projectileY[slot], projectileZ[slot]);\n                                const toTarget = effectScratch.v6.copy(targetPos).sub(currentPos);\n                                const distToTarget = toTarget.length();\n                                if (distToTarget > 0.05) {\n                                    toTarget.normalize();\n                                    const currentVel = effectScratch.v7.set(projectileVX[slot], projectileVY[slot], projectileVZ[slot]);\n                                    const speed = currentVel.length();\n                                    const closeBoost = 1 + clamp01((SMART_PISTOL_CLOSE_STEER_RADIUS - distToTarget) / SMART_PISTOL_CLOSE_STEER_RADIUS) * SMART_PISTOL_CLOSE_STEER_BOOST;\n                                    const steerForce = Math.min(0.96, scaledDt * SMART_PISTOL_STEER_RATE * closeBoost);\n                                    if (distToTarget < Math.max(0.65, speed * scaledDt * 2.2)) {\n                                        currentVel.copy(toTarget).multiplyScalar(speed);\n                                    } else {\n                                        currentVel.lerp(toTarget.multiplyScalar(speed), steerForce).normalize().multiplyScalar(speed);\n                                    }\n                                    projectileVX[slot] = currentVel.x;\n                                    projectileVY[slot] = currentVel.y;\n                                    projectileVZ[slot] = currentVel.z;\n                                }\n                            }\n                        }\n\n                        const prevX = projectileX[slot];\n                        const prevY = projectileY[slot];\n                        const prevZ = projectileZ[slot];\n                        projectileX[slot] += projectileVX[slot] * scaledDt;\n                        projectileY[slot] += projectileVY[slot] * scaledDt;\n                        projectileZ[slot] += projectileVZ[slot] * scaledDt;\n\n                        // Apply slight gravity (only noticeable on slow bullets like shotgun).\n                        projectileVY[slot] -= 2.0 * scaledDt;\n\n                        const ws = weaponStates[projectileWeaponIndex[slot]];\n                        const frameTravelX = projectileX[slot] - prevX;\n                        const frameTravelY = projectileY[slot] - prevY;\n                        const frameTravelZ = projectileZ[slot] - prevZ;\n                        const travelDist = Math.sqrt(frameTravelX * frameTravelX + frameTravelY * frameTravelY + frameTravelZ * frameTravelZ);\n\n                        // Use actual segment traveled this frame for contact-accurate hit detection.\n                        const prevPos = effectScratch.v3.set(prevX, prevY, prevZ);\n                        const frameTravel = effectScratch.v1.set(\n                            projectileX[slot] - prevX,\n                            projectileY[slot] - prevY,\n                            projectileZ[slot] - prevZ\n                        );\n                        if (travelDist <= 0.00001) continue;\n                        const bulletDir = effectScratch.v2.copy(frameTravel).divideScalar(travelDist);\n                        setProjectileTracerSlot(\n                            slot,\n                            ws,\n                            (prevX + projectileX[slot]) * 0.5,\n                            (prevY + projectileY[slot]) * 0.5,\n                            (prevZ + projectileZ[slot]) * 0.5,\n                            bulletDir.x,\n                            bulletDir.y,\n                            bulletDir.z,\n                            travelDist\n                        );\n                        let bulletDead = false;\n                        const hits = ctx.bulletHits || (ctx.bulletHits = []);\n                        const hitRecords = ctx.bulletHitRecords || (ctx.bulletHitRecords = []);\n                        hits.length = 0;\n                        const bulletQueryPad = 3.2;\n                        const zombieSlots = grid.querySlotsAABB(\n                            Math.min(prevX, projectileX[slot]) - bulletQueryPad,\n                            Math.max(prevX, projectileX[slot]) + bulletQueryPad,\n                            Math.min(prevZ, projectileZ[slot]) - bulletQueryPad,\n                            Math.max(prevZ, projectileZ[slot]) + bulletQueryPad,\n                            ctx.bulletCandidates || (ctx.bulletCandidates = [])\n                        );\n\n                        // Check zombie hits\n                        for (let i = 0; i < zombieSlots.length; i++) {\n                            const zSlot = zombieSlots[i];\n                            if (zombieRuntime.deadPending[zSlot]) continue;\n                            if (projectileSmartTargetSlot[slot] >= 0 && zSlot !== projectileSmartTargetSlot[slot]) continue;\n                            const zid = zombieRuntime.entityId[zSlot];\n                            if (projectileHasHitEntity(slot, zid)) continue;\n                            if (zombieRuntime.instanceSlot[zSlot] < 0) continue;\n                            const zombieX = zombieRuntime.x[zSlot];\n                            const zombieY = zombieRuntime.y[zSlot] || 0;\n                            const zombieZ = zombieRuntime.z[zSlot];\n                            const bodyParts = zombieRuntime.bodyMask[zSlot];\n                            const zombieScale = getZombieSlotHitScale(zSlot);\n                            const isSmartTargetCandidate = projectileSmartTargetSlot[slot] === zSlot;\n                            // At point-blank range the muzzle can begin inside or just beyond the\n                            // target's broad phase. Keep the locked target flowing to the swept\n                            // lock-point fallback below instead of rejecting it early.\n                            if (!isSmartTargetCandidate &&\n                                !canBulletReachZombieCheap(prevPos, bulletDir, travelDist, zombieX, zombieY, zombieZ, zombieScale)) continue;\n                            const hitPose = getBulletCachedZombiePose(zSlot, ctx);\n                            const zombieProps = getZombieSlotHitProps(zSlot);\n                            if (!isSmartTargetCandidate &&\n                                !canBulletReachZombieForValues(prevPos, bulletDir, travelDist, zombieX, zombieY, zombieZ, zombieScale, zombieProps, bodyParts, hitPose)) continue;\n                            const zombieYaw = zombieRuntime.yaw[zSlot] || 0;\n                            const reducedHitboxes = shouldUseReducedBulletHitboxes(ws, zSlot, zombieX, zombieZ);\n\n                            let bodyT = Infinity;\n                            let bodyPart = null;\n                            let bodyDmgMult = 1;\n                            let partT = Infinity;\n                            let partName = null;\n                            let partDmgMult = 1;\n\n                            for (let hbIdx = 0; hbIdx < bodyPartHitboxes.length; hbIdx++) {\n                                const hb = bodyPartHitboxes[hbIdx];\n                                if (reducedHitboxes && hb.name !== 'body' && hb.name !== 'head') continue;\n                                if (bodyParts && !zombiePartAlive(bodyParts, hb.name)) continue;\n                                const t = checkBodyPartHitRayForValues(prevPos, bulletDir, travelDist, zombieX, zombieY, zombieZ, zombieYaw, hb, zombieScale, zombieProps, bodyParts, hitPose);\n                                if (t !== null) {\n                                    if (hb.name === 'body') {\n                                        if (t < bodyT) {\n                                            bodyT = t;\n                                            bodyPart = hb.name;\n                                            bodyDmgMult = hb.dmgMult;\n                                        }\n                                    } else if (t < partT) {\n                                        partT = t;\n                                        partName = hb.name;\n                                        partDmgMult = hb.dmgMult;\n                                    }\n                                }\n                            }\n\n                            const selectedPart = partName || bodyPart;\n                            let fallbackT = selectedPart ? null : checkZombieSilhouetteHitRayForValues(prevPos, bulletDir, travelDist, zombieX, zombieY, zombieZ, zombieYaw, zombieScale, zombieProps, bodyParts, hitPose);\n                            if (!selectedPart && fallbackT === null && projectileSmartTargetSlot[slot] === zSlot) {\n                                // The lock point moves with the zombie while the projectile advances in\n                                // discrete frames. Capture a swept sphere around that point so low FPS or\n                                // point-blank shots cannot tunnel through the target between frames.\n                                const targetPoint = getSmartPistolTargetPoint(zSlot, effectScratch.v4);\n                                const relX = targetPoint.x - prevX;\n                                const relY = targetPoint.y - prevY;\n                                const relZ = targetPoint.z - prevZ;\n                                const closestT = Math.max(0, Math.min(travelDist, relX * bulletDir.x + relY * bulletDir.y + relZ * bulletDir.z));\n                                const closestX = prevX + bulletDir.x * closestT;\n                                const closestY = prevY + bulletDir.y * closestT;\n                                const closestZ = prevZ + bulletDir.z * closestT;\n                                const missX = targetPoint.x - closestX;\n                                const missY = targetPoint.y - closestY;\n                                const missZ = targetPoint.z - closestZ;\n                                const captureRadius = Math.max(0.26, 0.32 * zombieScale) + Math.min(0.16, travelDist * 0.08);\n                                if (missX * missX + missY * missY + missZ * missZ <= captureRadius * captureRadius) {\n                                    fallbackT = closestT;\n                                }\n                            }\n                            if (selectedPart || fallbackT !== null) {\n                                const record = hitRecords[hits.length] || (hitRecords[hits.length] = {});\n                                record.type = 'zombie';\n                                record.t = selectedPart ? (partName ? partT : bodyT) : fallbackT;\n                                record.zid = zid;\n                                record.zSlot = zSlot;\n                                record.part = selectedPart || 'body';\n                                record.dmgMult = selectedPart ? (partName ? partDmgMult : bodyDmgMult) : 1;\n                                insertHitSorted(hits, record);\n                            }\n                        }\n\n                        // Check wall collisions\n                        const wallHit = getNearestWallHit(prevPos, bulletDir, travelDist);\n                        if (wallHit) {\n                            const record = hitRecords[hits.length] || (hitRecords[hits.length] = {});\n                            record.type = 'wall';\n                            record.t = wallHit.distance;\n                            record.px = wallHit.px;\n                            record.py = wallHit.py;\n                            record.pz = wallHit.pz;\n                            record.nx = wallHit.nx;\n                            record.ny = wallHit.ny;\n                            record.nz = wallHit.nz;\n                            insertHitSorted(hits, record);\n                        }\n\n                        for (let hitIdx = 0; hitIdx < hits.length; hitIdx++) {\n                            const hit = hits[hitIdx];\n                            if (hit.type === 'zombie') {\n                                const { zid, zSlot } = hit;\n                                if (projectileHasHitEntity(slot, zid)) continue; // ignore if already hit in a previous step\n                                if (zSlot < 0 || zombieRuntime.deadPending[zSlot]) continue;\n                                const isSmartProjectile = projectileSmartTargetSlot[slot] >= 0 && projectileSmartTargetSlot[slot] === zSlot;\n                                const smartFullHeadshot = isSmartProjectile && projectileSmartFullLock[slot] && projectileSmartFinalShot[slot] && zombieSlotPartAlive(zSlot, 'head');\n                                const part = smartFullHeadshot ? 'head' : (isSmartProjectile ? 'body' : hit.part);\n                                const dmgMult = smartFullHeadshot || isSmartProjectile ? 1 : hit.dmgMult;\n\n                                gameState.shotsHit++;\n                                let dmg = projectileDamage[slot] * dmgMult;\n                                if (hasAutoFirePassive && !isSmartProjectile) {\n                                    dmg *= 0.8;\n                                }\n                                if (part === 'head') dmg *= (config.headshotMultiplier || 2);\n\n                                const smartArmorPierce = projectileSmartTargetSlot[slot] >= 0\n                                    ? (projectileSmartFullLock[slot] ? 0.92 : 0.58)\n                                    : 0.35;\n                                const hitResult = applyZombieArmorReductionBySlot(zSlot, dmg, smartArmorPierce);\n\n                                zombieRuntime.currentHp[zSlot] -= hitResult.damage;\n                                ctx.hitZombie = true;\n                                applyLifesteal(hitResult.damage);\n                                if (part === 'head' || hitResult.damage > 55) {\n                                    hitStopTimer = Math.max(hitStopTimer, part === 'head' ? 0.035 : 0.018);\n                                }\n                                if (part === 'head') {\n                                    ctx.headshot = true;\n                                    gameState.headshots++;\n                                }\n\n                                const hitPos = effectScratch.v8.set(\n                                    prevPos.x + bulletDir.x * hit.t,\n                                    prevPos.y + bulletDir.y * hit.t,\n                                    prevPos.z + bulletDir.z * hit.t\n                                );\n                                const bulletLaunchDir = effectScratch.v7.set(projectileVX[slot], projectileVY[slot], projectileVZ[slot]).normalize();\n                                spawnBloodBurst(hitPos, part === 'head' ? 8 : 4);\n                                spawnImpactSpark(hitPos, effectScratch.v9.set(-bulletDir.x, -bulletDir.y, -bulletDir.z), part === 'head' ? 0xffdd44 : 0xff3333, part === 'head' ? 0.9 : 0.65);\n\n                                // Apply part damage\n                                const bodyParts = zombieRuntime.bodyMask[zSlot];\n                                let severedPart = false;\n                                if (part !== 'body') {\n                                    const partHp = damageZombieSlotPart(zSlot, part, dmg);\n                                    if (partHp <= 0 && zombieSlotPartAlive(zSlot, part)) {\n                                        severedPart = true;\n                                        clearZombieSlotPart(zSlot, part);\n                                        const zombieMesh = acquireZombieProxyForSlot(zSlot);\n                                        syncZombieBodyParts(zombieMesh, zombieRuntime.bodyPartsFacade[zSlot]);\n                                        // Spawn flying severed part (bullet direction as launch dir)\n                                        if (zombieMesh) {\n                                            const zp = zombieRuntime.positionFacade[zSlot];\n                                            spawnSeveredPart(zombieMesh, part, zp, bulletLaunchDir);\n                                            spawnBulletShatter(part, zp, zombieRuntime.yaw[zSlot] || 0, hitPos, bulletLaunchDir, projectileWeaponIndex[slot]);\n                                        }\n                                        spawnBloodBurst(hitPos, 12);\n                                        if (part === 'head') {\n                                            zombieRuntime.currentHp[zSlot] = 0;\n                                        } else if (part === 'leftLeg' || part === 'rightLeg') {\n                                            const legsLeft = zombieSlotLegsLeft(zSlot);\n                                            zombieRuntime.speed[zSlot] = zombieRuntime.baseSpeed[zSlot] * (legsLeft === 0 ? 0.15 : 0.5);\n                                        }\n                                    } else if (dmg > 22 || part === 'head') {\n                                        const zp = zombieRuntime.positionFacade[zSlot];\n                                        spawnBulletShatter(part, zp, zombieRuntime.yaw[zSlot] || 0, hitPos, bulletLaunchDir, projectileWeaponIndex[slot]);\n                                    }\n                                } else if (part === 'body' && (dmg > 24 || (weaponStates[projectileWeaponIndex[slot]] && weaponStates[projectileWeaponIndex[slot]].name === 'SHOTGUN'))) {\n                                    const zp = zombieRuntime.positionFacade[zSlot];\n                                    spawnBulletShatter(part, zp, zombieRuntime.yaw[zSlot] || 0, hitPos, bulletLaunchDir, projectileWeaponIndex[slot]);\n                                }\n\n                                if (zombieRuntime.currentHp[zSlot] > 0) {\n                                    applyZombieImpactReaction(zSlot, projectileWeaponIndex[slot], part, bulletLaunchDir, hitResult.damage, severedPart, null);\n                                }\n\n                                if (zombieRuntime.currentHp[zSlot] <= 0) {\n                                    ctx.killConfirm = true;\n                                    queueBulletZombieDeath(w, zid, bulletLaunchDir, part, hitPos, projectileWeaponIndex[slot]);\n                                }\n                                recordProjectileHitEntity(slot, zid);\n\n                                if (projectilePenetration[slot] <= 0) {\n                                    bulletDead = true;\n                                    projectileX[slot] = hitPos.x;\n                                    projectileY[slot] = hitPos.y;\n                                    projectileZ[slot] = hitPos.z; // stop bullet visually at hit location\n                                    break;\n                                } else {\n                                    projectilePenetration[slot]--;\n                                    projectileDamage[slot] *= (1.0 - projectileDropoff[slot]);\n                                    projectileVX[slot] *= 0.8;\n                                    projectileVY[slot] *= 0.8;\n                                    projectileVZ[slot] *= 0.8;\n                                }\n                            } else if (hit.type === 'wall') {\n                                const point = effectScratch.v8.set(hit.px, hit.py, hit.pz);\n                                const normal = effectScratch.v9.set(hit.nx, hit.ny, hit.nz);\n                                spawnBulletMark(point, normal, bulletDir);\n                                spawnImpactSpark(point, normal, 0xffcc66, 0.8);\n                                if (projectilePenetration[slot] <= 0) {\n                                    bulletDead = true;\n                                    projectileX[slot] = point.x;\n                                    projectileY[slot] = point.y;\n                                    projectileZ[slot] = point.z;\n                                    break;\n                                } else {\n                                    projectilePenetration[slot]--;\n                                    projectileDamage[slot] *= (1.0 - projectileDropoff[slot]);\n                                    projectileVX[slot] *= 0.6;\n                                    projectileVY[slot] *= 0.6;\n                                    projectileVZ[slot] *= 0.6;\n                                }\n                            }\n                        }\n\n                        // Out of bounds check\n                        if (projectileX[slot] < -15 || projectileX[slot] > 15 || projectileY[slot] < -2 || projectileY[slot] > 6 || projectileZ[slot] < -25 || projectileZ[slot] > 20) {\n                            releaseProjectileSlot(slot);\n                        }\n                    }\n                }\n\n                // System 3.6: Flamethrower fire stream system (particles updates + collisions + tick DoTs + armor melting)\n                function flameSystem(w, dt, ctx) {\n                    if (gamePaused || gameState.upgradeScreenOpen) return;\n\n                    const scaledDt = slowTimeActive ? dt * 0.01 : dt;\n                    flameGpuTime += scaledDt;\n                    flameShaderMaterial.uniforms.uTime.value = flameGpuTime;\n\n                    // Reclaim only slots that were actually used. Visual motion and expiry\n                    // remain GPU-driven; this list exists solely for O(1) slot allocation.\n                    for (let i = activeFlameSlots.length - 1; i >= 0; i--) {\n                        const slot = activeFlameSlots[i];\n                        if (flameExpireAt[slot] <= flameGpuTime) releaseFlameSlot(slot);\n                    }\n\n                    // DoT ticking loop for burning zombies\n                    const activeZombies = zombieRuntime.activeSlots;\n                    for (let z = 0; z < activeZombies.length; z++) {\n                        const zSlot = activeZombies[z];\n                        if (zombieRuntime.deadPending[zSlot]) continue;\n\n                        if (zombieRuntime.burningTimer[zSlot] > 0) {\n                            // \u6838\u5fc3\u4fee\u590d\uff1a\u91c7\u7528\u7ebf\u6027\u65f6\u95f4\u79ef\u5206\u7ed3\u7b97\u4f24\u5bb3\n                            // \u4f24\u5bb3\u8ba1\u7b97\u4e0d\u518d\u4f9d\u8d56\u56fa\u5b9a\u9891\u7387\u7684\u201cTick\u201d\uff0c\u800c\u662f\u76f4\u63a5\u4e0e scaledDt \u6302\u94a9\n                            // \u786e\u4fdd\u4e86\u5373\u4f7f\u5e27\u7387\u6ce2\u52a8\u6216\u51fa\u73b0\u5361\u987f\uff0c\u603b DPS \u4f9d\u7136\u4e25\u683c\u6052\u5b9a (dps * dt)\n                            const dps = zombieRuntime.burningDamage[zSlot];\n                            const frameBurnDmg = dps * scaledDt;\n                            \n                            zombieRuntime.currentHp[zSlot] -= frameBurnDmg;\n\n                            zombieRuntime.burningTimer[zSlot] -= scaledDt;\n                            zombieRuntime.burningTickTimer[zSlot] += scaledDt;\n\n                            if (flamethrowerUpgrades.armorMelting) {\n                                const maxArm = zombieRuntime.maxArmor[zSlot];\n                                if (maxArm > 0) {\n                                    zombieRuntime.armor[zSlot] = Math.max(0, zombieRuntime.armor[zSlot] - maxArm * 0.15 * scaledDt);\n                                }\n                            }\n\n\n                            // Gameplay tick (0.25s) for spread and check\n                            if (zombieRuntime.burningTickTimer[zSlot] >= 0.25) {\n                                zombieRuntime.burningTickTimer[zSlot] -= 0.25;\n\n                                if (zombieRuntime.currentHp[zSlot] <= 0) {\n                                    queueFlameZombieDeath(w, zombieRuntime.entityId[zSlot], zombieRuntime.x[zSlot], zombieRuntime.y[zSlot] + 1, zombieRuntime.z[zSlot]);\n                                    continue;\n                                }\n\n                                if (flamethrowerUpgrades.fireSpread && Math.random() < 0.30) {\n                                    const spreadRadius = 2.5;\n                                    const grid = getZombieSpatialIndex(ctx, w);\n                                    const candidates = ctx.flameCandidates || (ctx.flameCandidates = []);\n                                    grid.querySlotsAABB(\n                                        zombieRuntime.x[zSlot] - spreadRadius, zombieRuntime.x[zSlot] + spreadRadius,\n                                        zombieRuntime.z[zSlot] - spreadRadius, zombieRuntime.z[zSlot] + spreadRadius,\n                                        candidates\n                                    );\n\n                                    for (let n = 0; n < candidates.length; n++) {\n                                        const nSlot = candidates[n];\n                                        if (nSlot === zSlot || zombieRuntime.deadPending[nSlot]) continue;\n\n                                        const ndx = zombieRuntime.x[nSlot] - zombieRuntime.x[zSlot];\n                                        const ndz = zombieRuntime.z[nSlot] - zombieRuntime.z[zSlot];\n                                        const ndistSq = ndx * ndx + ndz * ndz;\n                                        if (ndistSq <= spreadRadius * spreadRadius) {\n                                            zombieRuntime.burningTimer[nSlot] = zombieRuntime.burningTimer[zSlot];\n                                            zombieRuntime.burningDamage[nSlot] = zombieRuntime.burningDamage[zSlot];\n                                            zombieRuntime.impactSlowTimer[nSlot] = Math.max(zombieRuntime.impactSlowTimer[nSlot], FLAME_DIRECT_SLOW_TIME);\n                                            zombieRuntime.impactSlow[nSlot] = Math.max(zombieRuntime.impactSlow[nSlot], FLAME_DIRECT_SLOW);\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    // Update Flamethrower fire lights\n                    const activeCount = activeFlameSlots.length;\n                    const wComp = world.getComponent(playerEntity, 'Weapon');\n                    if (wComp.index === 7 && ctx.fired) {\n                        // Keep the loop alive across the short gaps between automatic fire ticks.\n                        // This also covers the auto-fire passive, whose input flag is consumed\n                        // earlier in the same frame by weaponSystem.\n                        wComp.flameSoundTimer = 0.16;\n                    } else {\n                        wComp.flameSoundTimer = Math.max(0, (wComp.flameSoundTimer || 0) - scaledDt);\n                    }\n                    const isFiringFlamethrower = !gamePaused && !gameState.upgradeScreenOpen && !gameState.over &&\n                        wComp.index === 7 && (input.holdingFire || (wComp.flameSoundTimer || 0) > 0) && !wComp.isReloading;\n\n                    if (activeCount > 0) {\n                        // The old average used stale CPU spawn positions while the particles\n                        // themselves moved on the GPU. Tracking the analytic jet midpoint is\n                        // both cheaper and visually follows the actual flame front.\n                        flamethrowerLight.position.set(flameLightTargetX, flameLightTargetY, flameLightTargetZ);\n                        // Flicker light intensity for realistic fire warmth\n                        const flicker = 0.85 + Math.sin(performance.now() * 0.015) * 0.15;\n                        const targetIntensity = Math.min(2.5, 0.45 + activeCount * 0.03) * flicker;\n                        flamethrowerLight.intensity += (targetIntensity - flamethrowerLight.intensity) * expSmoothingFactor(scaledDt, 12);\n                    } else {\n                        flamethrowerLight.intensity += (0 - flamethrowerLight.intensity) * expSmoothingFactor(scaledDt, 12);\n                    }\n\n                    if (isFiringFlamethrower) {\n                        // Play looping flamethrower fire sound\n                        if (!loopedSoundHandles[FLAMETHROWER_LOOP_SOUND_ID] || loopedSoundHandles[FLAMETHROWER_LOOP_SOUND_ID].stopped) {\n                            playSound(FLAMETHROWER_LOOP_SOUND_ID, 1.35, true, { fadeIn: 0.08 });\n                        }\n                        // Place a bright light at the muzzle of the flamethrower\n                        const muzzlePos = world.getComponent(playerEntity, 'Position');\n                        const fDir = effectScratch.v1.set(0, 0, -1).applyQuaternion(camera.quaternion);\n                        flamethrowerMuzzleLight.position.set(\n                            muzzlePos.x + fDir.x * 0.85,\n                            muzzlePos.y + fDir.y * 0.85,\n                            muzzlePos.z + fDir.z * 0.85\n                        );\n                        const flicker = 0.82 + Math.cos(performance.now() * 0.024) * 0.18;\n                        const targetMuzzleIntensity = (1.45 + Math.random() * 0.4) * flicker;\n                        flamethrowerMuzzleLight.intensity += (targetMuzzleIntensity - flamethrowerMuzzleLight.intensity) * expSmoothingFactor(scaledDt, 24);\n                    } else {\n                        // Stop flamethrower loop sound when not firing\n                        stopLoopedSound(FLAMETHROWER_LOOP_SOUND_ID);\n                        flamethrowerMuzzleLight.intensity += (0 - flamethrowerMuzzleLight.intensity) * expSmoothingFactor(scaledDt, 16);\n                    }\n                }\n\n                function spawnCosmeticFireSpark(x, z) {\n                    const i = acquireFlameSlot(false);\n                    if (i < 0) return;\n                    seedFlameParticle(\n                        i,\n                        x + (Math.random() - 0.5) * 0.3,\n                        0.5 + Math.random() * 0.8,\n                        z + (Math.random() - 0.5) * 0.3,\n                        (Math.random() - 0.5) * 0.5,\n                        0.5 + Math.random() * 1.0,\n                        (Math.random() - 0.5) * 0.5,\n                        0.36 + Math.random() * 0.24,\n                        0,\n                        0.13 + Math.random() * 0.09,\n                        0.68,\n                        0\n                    );\n                }\n\n                function spawnZombieBurnFlicker(zSlot) {\n                    const i = acquireFlameSlot(false);\n                    if (i < 0) return;\n                    const zScale = zombieRuntime.scale[zSlot] || 1;\n                    const angle = Math.random() * Math.PI * 2;\n                    const radius = (zombieRuntime.radius[zSlot] || 0.45) * (0.35 + Math.random() * 0.45);\n                    const px = zombieRuntime.x[zSlot] + Math.cos(angle) * radius;\n                    const py = (zombieRuntime.y[zSlot] || 0) + (0.40 + Math.random() * 1.12) * zScale;\n                    const pz = zombieRuntime.z[zSlot] + Math.sin(angle) * radius;\n                    seedFlameParticle(\n                        i,\n                        px, py, pz,\n                        (Math.random() - 0.5) * 0.75,\n                        0.85 + Math.random() * 1.35,\n                        (Math.random() - 0.5) * 0.75,\n                        0.52 + Math.random() * 0.32,\n                        0,\n                        0.18 + Math.random() * 0.12,\n                        0.70,\n                        Math.random() * 0.04\n                    );\n                }\n\n                // System 4: Wave spawning\n                function waveSystem(w, dt, ctx) {\n                    const scaledDt = slowTimeActive ? dt * 0.01 : dt;\n                    if (!gameState.waveActive) {\n                        if (gameState.upgradeScreenOpen) return;\n                        gameState.waveCooldown -= scaledDt;\n                        if (gameState.waveCooldown <= 0) startWave();\n                        return;\n                    }\n                    if (gameState.zombiesSpawned < gameState.zombiesToSpawn) {\n                        gameState.spawnTimer += scaledDt;\n                        const spawnInterval = getWaveSpawnInterval(gameState.currentWave);\n                        if (gameState.spawnTimer >= spawnInterval) {\n                            gameState.spawnTimer = 0;\n                            // Spawn boss at ~40% through the wave\n                            const boss = gameState.bossToSpawn;\n                            if (boss && !gameState.bossSpawned && gameState.zombiesSpawned >= Math.floor(gameState.zombiesToSpawn * 0.4)) {\n                                spawnZombie(boss);\n                                gameState.bossSpawned = true;\n                            } else {\n                                spawnZombie();\n                            }\n                            ctx.spatialGridBuilt = false;\n                        }\n                    }\n                    if (gameState.zombiesRemaining <= 0 && gameState.zombiesSpawned >= gameState.zombiesToSpawn) {\n                        if (!gameState.waveEndPending) {\n                            gameState.waveEndPending = true;\n                            gameState.waveEndDelay = 0.12;\n                            return;\n                        }\n                        gameState.waveEndDelay -= scaledDt;\n                        if (gameState.waveEndDelay <= 0) endWave();\n                    }\n                }\n\n                // System 5: Event processing (unidirectional)\n                function eventSystem(w, dt, ctx) {\n                    for (let eventIndex = 0; eventIndex < w.events.length; eventIndex++) {\n                        const event = w.events[eventIndex];\n                        switch (event.type) {\n                            case 'zombieKilled': {\n                                const eventDeathSlot = event.deathSlot;\n                                const eventEntityId = typeof event.entityId === 'number'\n                                    ? event.entityId\n                                    : (typeof event.slot === 'number' && event.slot >= 0 ? zombieRuntime.entityId[event.slot] : -1);\n                                const zc = eventEntityId >= 0 ? w.getComponent(eventEntityId, 'Zombie') : null;\n                                if (!zc) {\n                                    releaseZombieDeathSlot(eventDeathSlot);\n                                    break;\n                                }\n                                const zSlot = getZombieRuntimeSlot(eventEntityId);\n                                const killedBoss = zSlot >= 0 ? zombieRuntime.isBoss[zSlot] !== 0 : !!zc.isBoss;\n                                {\n                                    ctx.killConfirm = true;\n                                    // Coalesce death-sound voices: only the first 2 kills in a frame play a fresh\n                                    // zombie_death_*; further multi-kills share to avoid voice-steal churn.\n                                    if ((ctx.deathSoundsThisFrame || 0) < 2) {\n                                        const deathVariants = ['zombie_death_sound', 'zombie_death_2', 'zombie_death_3', 'zombie_death_4'];\n                                        const deathSound = deathVariants[Math.floor(Math.random() * deathVariants.length)];\n                                        playSound(deathSound, 0.35 + Math.random() * 0.15);\n                                        ctx.deathSoundsThisFrame = (ctx.deathSoundsThisFrame || 0) + 1;\n                                    }\n                                    releaseZombieInstanceSlot(zc.instanceSlot);\n                                    zc.instanceSlot = -1;\n\n                                    // Katana deaths now stay controlled: no full-body procedural slicing.\n\n                                    const zombiePos = w.getComponent(eventEntityId, 'Position');\n                                    const zombieScale = zc.scale || 1;\n                                    const deathSlot = eventDeathSlot;\n                                    const hasDeathSlot = deathSlot >= 0 && zombieDeathRuntime.active[deathSlot];\n                                    const deathSource = hasDeathSlot ? zombieDeathRuntime.source[deathSlot] : DEATH_SOURCE_NONE;\n                                    const deathPartBit = hasDeathSlot ? zombieDeathRuntime.hitPart[deathSlot] : ZOMBIE_PART_BODY;\n                                    const deathPartName = zombiePartNameFromBit(deathPartBit);\n                                    const killerWeaponIndex = hasDeathSlot ? zombieDeathRuntime.weaponIndex[deathSlot] : 0;\n                                    const impactProfile = getWeaponImpactProfile(killerWeaponIndex);\n\n                                    // Reuse pooled effect literal \u2014 eliminates per-kill object churn during bursts.\n                                    const effect = acquireZombieDeathEffect();\n                                    effect.corpse = zc.meshRef;\n                                    effect.bodyParts = zSlot >= 0 ? zombieRuntime.bodyMask[zSlot] : 0;\n                                    effect.isBurnt = (zSlot >= 0 && zombieRuntime.burningTimer[zSlot] > 0) || (killerWeaponIndex === 7);\n                                    effect.animTimer = zc.animTimer;\n                                    effect.state = zc.state;\n                                    effect.zombiePos.x = zombiePos ? zombiePos.x : 0;\n                                    effect.zombiePos.y = zombiePos ? (zombiePos.y || 0) : 0;\n                                    effect.zombiePos.z = zombiePos ? zombiePos.z : 0;\n                                    const ppos = w.getComponent(playerEntity, 'Position');\n                                    if (ppos) {\n                                        effect.playerPos.x = ppos.x;\n                                        effect.playerPos.y = ppos.y || 0;\n                                        effect.playerPos.z = ppos.z;\n                                        effect.hasPlayerPos = true;\n                                    } else {\n                                        effect.hasPlayerPos = false;\n                                    }\n                                    effect.typeName = zc.typeName;\n                                    effect.zombieScale = zombieScale;\n                                    effect.killedBoss = killedBoss;\n                                    effect.deathSource = deathSource;\n                                    effect.deathPartBit = deathPartBit;\n                                    effect.deathPartName = deathPartName;\n                                    effect.impactProfile = impactProfile;\n                                    if (hasDeathSlot) {\n                                        effect.deathDir.x = zombieDeathRuntime.dirX[deathSlot];\n                                        effect.deathDir.y = zombieDeathRuntime.dirY[deathSlot];\n                                        effect.deathDir.z = zombieDeathRuntime.dirZ[deathSlot];\n                                        effect.bulletHitPos.x = zombieDeathRuntime.hitX[deathSlot];\n                                        effect.bulletHitPos.y = zombieDeathRuntime.hitY[deathSlot];\n                                        effect.bulletHitPos.z = zombieDeathRuntime.hitZ[deathSlot];\n                                    } else {\n                                        effect.deathDir.x = 0; effect.deathDir.y = 0; effect.deathDir.z = 0;\n                                        effect.bulletHitPos.x = zombiePos ? zombiePos.x : 0;\n                                        effect.bulletHitPos.y = zombiePos ? (zombiePos.y || 0) + 1.0 : 1.0;\n                                        effect.bulletHitPos.z = zombiePos ? zombiePos.z : 0;\n                                    }\n                                    effect.radius = zSlot >= 0 ? zombieRuntime.radius[zSlot] : Math.max(0.44, zombieScale * 0.44);\n                                    queueZombieDeathEffect(effect);\n                                    zc.meshRef = null;\n                                }\n                                releaseZombieRuntimeSlot(eventEntityId);\n                                w.removeEntity(eventEntityId);\n                                ctx.spatialGridBuilt = false;\n                                const comboMultiplier = 1 + comboState.rank * 0.25;\n                                const killBonus = Math.round((killedBoss ? 500 + gameState.currentWave * 50 : 50 + gameState.currentWave * 10) * comboMultiplier);\n                                gameState.kills++; gameState.score += killBonus;\n                                gameState.zombiesRemaining--; ctx.hitZombie = true;\n                                const deathWasHeadshot = eventDeathSlot >= 0 && zombieDeathRuntime.active[eventDeathSlot] && zombieDeathRuntime.hitPart[eventDeathSlot] === ZOMBIE_PART_HEAD;\n                                registerComboKill(deathWasHeadshot);\n                                releaseZombieDeathSlot(eventDeathSlot);\n                                break;\n                            }\n                            case 'playerDamage': {\n                                const hp = w.getComponent(playerEntity, 'Health');\n                                let finalAmount = event.amount;\n                                if (gameState.currentWave <= 5) {\n                                    finalAmount = Math.min(1, finalAmount);\n                                } else if (gameState.currentWave <= 10) {\n                                    finalAmount = Math.min(2, finalAmount);\n                                }\n\n                                // Check if 3 seconds have passed since last damage (for waves 1 to 10)\n                                const now = performance.now() / 1000;\n                                if (gameState.currentWave <= 10) {\n                                    if (gameState.lastPlayerDamageTime && now - gameState.lastPlayerDamageTime < 3.0) {\n                                        break;\n                                    }\n                                    gameState.lastPlayerDamageTime = now;\n                                }\n                                hp.current -= finalAmount;\n                                ctx.playerHit = true;\n\n                                if (typeof event.zombieX === 'number' && typeof event.zombieZ === 'number') {\n                                    const pos = w.getComponent(playerEntity, 'Position');\n                                    const player = w.getComponent(playerEntity, 'Player');\n                                    const dx = event.zombieX - pos.x;\n                                    const dz = event.zombieZ - pos.z;\n\n                                    // Calculate accurate relative direction using player forward and right vectors\n                                    inputScratch.yawEuler.set(0, player.yaw, 0);\n                                    inputScratch.yawQuat.setFromEuler(inputScratch.yawEuler);\n                                    const fwd = inputScratch.fwd.set(0, 0, -1).applyQuaternion(inputScratch.yawQuat);\n                                    const rgt = inputScratch.right.set(1, 0, 0).applyQuaternion(inputScratch.yawQuat);\n\n                                    const fwdProj = dx * fwd.x + dz * fwd.z;\n                                    const rgtProj = dx * rgt.x + dz * rgt.z;\n\n                                    const relAngle = Math.atan2(rgtProj, fwdProj);\n\n                                    if (relAngle >= -Math.PI / 4 && relAngle < Math.PI / 4) {\n                                        ctx.playerHitDir = 'top';\n                                    } else if (relAngle >= Math.PI / 4 && relAngle < 3 * Math.PI / 4) {\n                                        ctx.playerHitDir = 'right';\n                                    } else if (relAngle >= -3 * Math.PI / 4 && relAngle < -Math.PI / 4) {\n                                        ctx.playerHitDir = 'left';\n                                    } else {\n                                        ctx.playerHitDir = 'bottom';\n                                    }\n                                } else {\n                                    ctx.playerHitDir = 'bottom';\n                                }\n\n                                if (hp.current <= 0) { hp.current = 0; endGame(); }\n                                break;\n                            }\n                        }\n                    }\n                }\n\n                // System 6: Render sync (ECS -> Three.js & DOM)\n                function renderSyncSystem(w, dt, ctx) {\n                    const pos = w.getComponent(playerEntity, 'Position');\n                    const player = w.getComponent(playerEntity, 'Player');\n                    const hp = w.getComponent(playerEntity, 'Health');\n                    const wComp = w.getComponent(playerEntity, 'Weapon');\n                    camera.position.set(pos.x, pos.y, pos.z);\n                    camera.rotation.order = 'YXZ'; camera.rotation.y = player.yaw; camera.rotation.x = player.pitch;\n                    camera.rotation.z = 0;\n\n                    // Ensure camera matrices are updated before projecting 3D positions to HUD screen space\n                    camera.updateMatrixWorld(true);\n\n                    updateSmartPistolLocks(w, ctx, dt);\n                    let targetFov = sniperScoped ? scopeZoomFov : getBaseCameraFov();\n                    const visualScale = getVisualIntensityScale();\n                    if (weaponAnim.viewKickTimer > 0) {\n                        const kickDuration = weaponAnim.viewKickCombo === 0 ? 0.28 : 0.18;\n                        const k = weaponAnim.viewKickTimer / kickDuration;\n                        const pulse = Math.sin(clamp01(k) * Math.PI) * weaponAnim.viewKickStrength * visualScale;\n                        camera.rotation.z += (weaponAnim.viewKickCombo === 1 ? -0.018 : weaponAnim.viewKickCombo === 2 ? 0.014 : 0) * pulse;\n                        camera.rotation.x += (weaponAnim.viewKickCombo === 0 ? -0.018 : -0.006) * pulse;\n                        targetFov -= (weaponAnim.viewKickCombo === 0 ? 2.2 : 1.0) * pulse;\n                        weaponAnim.viewKickTimer = Math.max(0, weaponAnim.viewKickTimer - dt);\n                    }\n                    weaponAnim.gunKickTarget = Math.max(0, (weaponAnim.gunKickTarget || 0) - dt * 7.5);\n                    weaponAnim.gunKickAmount += ((weaponAnim.gunKickTarget || 0) - (weaponAnim.gunKickAmount || 0)) * expSmoothingFactor(dt, 24);\n                    if (weaponAnim.gunKickTimer > 0) weaponAnim.gunKickTimer = Math.max(0, weaponAnim.gunKickTimer - dt);\n                    if (weaponAnim.gunKickAmount > 0.001) {\n                        const kick = Math.min(1.2, weaponAnim.gunKickAmount) * Math.min(1.2, weaponAnim.gunKickStrength || 1);\n                        camera.rotation.x -= 0.0028 * kick;\n                        camera.rotation.z += Math.sin((gameState.frameId || 0) * 0.71) * 0.00065 * kick;\n                        targetFov += 0.22 * kick;\n                    }\n                    // Hit FOV punch (Overwatch style)\n                    if (weaponAnim.hitFovPunch > 0) {\n                        targetFov -= weaponAnim.hitFovPunch * 1.2 * visualScale;\n                        weaponAnim.hitFovPunch = Math.max(0, weaponAnim.hitFovPunch - dt * 18);\n                    }\n                    if (Math.abs(camera.fov - targetFov) > 0.01) {\n                        camera.fov += (targetFov - camera.fov) * Math.min(1, dt * 22);\n                        camera.updateProjectionMatrix();\n                    }\n                    const healthValue = Math.max(0, Math.min(100, Math.round(hp.current * 10) / 10));\n                    const healthLabel = Math.ceil(hp.current);\n                    if (uiState.health !== healthValue) {\n                        uiState.health = healthValue;\n                        healthFill.style.transform = `scaleX(${healthValue / 100})`;\n                    }\n                    if (uiState.healthText !== healthLabel) {\n                        uiState.healthText = healthLabel;\n                        healthText.textContent = healthLabel;\n                    }\n                    if (uiState.score !== gameState.score) {\n                        uiState.score = gameState.score;\n                        scoreValue.textContent = gameState.score;\n                    }\n                    // Update enemies remaining\n                    if (enemiesDisplay) {\n                        const rem = Math.max(0, gameState.zombiesRemaining);\n                        const newEnemyText = `\u2620 ${rem}`;\n                        if (enemiesDisplay.textContent !== newEnemyText) enemiesDisplay.textContent = newEnemyText;\n                    }\n                    if (ctx.weaponChanged || wComp.isReloading || (wComp.index === 7 && wComp.flameHeat > 0)) { updateWeaponDisplay(); updateScopeBtnVisibility(); }\n                    if (ctx.hitZombie) {\n                        queueHitMarker(ctx.killConfirm ? 'kill' : (ctx.headshot ? 'headshot' : 'body'), ctx.frameId);\n                    }\n                    if (ctx.fired) {\n                        weaponAnim.hitFovPunch = Math.max(weaponAnim.hitFovPunch || 0, 1.2 * visualScale);\n                    }\n                    if (ctx.playerHit) {\n                        const dir = ctx.playerHitDir || 'bottom';\n                        const el = damageIndicators[dir];\n                        if (el) {\n                            el.classList.remove('active', 'soft');\n                            damageIndicatorAlt = !damageIndicatorAlt;\n                            el.classList.toggle('alt', damageIndicatorAlt);\n                            el.classList.add('active');\n                            if (visualIntensityMode === 'soft') el.classList.add('soft');\n                        }\n                    }\n\n                    // Update 3D weapon viewmodel\n                    const isMoving = input.keys['w'] || input.keys['s'] || input.keys['a'] || input.keys['d'] ||\n                        input.keys['arrowup'] || input.keys['arrowdown'] || input.keys['arrowleft'] || input.keys['arrowright'] ||\n                        Math.abs(leftJoystickValue.x) > 0.1 || Math.abs(leftJoystickValue.y) > 0.1;\n                    // Adaptive flush budget: light at pressure \u22652 to avoid amplifying load,\n                    // but bump slightly when the queue is backing up so corpses don't lag visibly.\n                    const deathQueueDepth = zombieDeathEffectQueue.length - zombieDeathEffectReadIndex;\n                    const flushPressure = getPerfPressureLevel();\n                    let deathBudget = flushPressure >= 2 ? 1 : 2;\n                    if (deathQueueDepth >= 14 && flushPressure < 3) deathBudget = flushPressure >= 2 ? 2 : 3;\n                    flushZombieDeathEffects(deathBudget);\n                    const ws = weaponStates[wComp.index];\n                    const targetGap = getCrosshairTargetGap(ws, wComp, isMoving);\n                    const gapSpeed = targetGap > weaponAnim.crosshairGap ? 30 : 12;\n                    weaponAnim.crosshairGap += (targetGap - weaponAnim.crosshairGap) * Math.min(1, dt * gapSpeed);\n                    weaponAnim.crosshairHitPulse = Math.max(0, (weaponAnim.crosshairHitPulse || 0) - dt * 8.5);\n                    const crosshairGap = weaponAnim.crosshairGap.toFixed(2);\n                    const hitScale = (1 + weaponAnim.crosshairHitPulse * 0.16).toFixed(3);\n                    if (uiState.crosshairGap !== crosshairGap) {\n                        uiState.crosshairGap = crosshairGap;\n                        crosshair.style.setProperty('--gap', `${crosshairGap}px`);\n                    }\n                    if (uiState.hitScale !== hitScale) {\n                        uiState.hitScale = hitScale;\n                        crosshair.style.setProperty('--hit-scale', hitScale);\n                    }\n                    updateZombieInstances(w, ctx);\n                    updateWeapon3D(dt, ctx.fired, wComp.index, isMoving, ctx);\n                }\n\n                // Register systems in execution order (data flows: input -> weapon -> projectiles -> wave -> AI -> separation -> events -> render)\n                world.addSystem(inputSystem);\n                world.addSystem(weaponSystem);\n                world.addSystem(bulletSystem);\n                world.addSystem(flameSystem);\n                world.addSystem(waveSystem);\n                world.addSystem(zombieAISystem);\n                world.addSystem(zombieSeparationSystem);\n                world.addSystem(eventSystem);\n                world.addSystem(renderSyncSystem);\n\n                // ===== ZOMBIE SPAWNING (uses Object Pool) =====\n                // Scratch literals reused across calls to avoid per-spawn GC pressure during wave ramps.\n                const spawnZombieScratchCorridors = [{ x: -7, z: -15 }, { x: 0, z: -15 }, { x: 7, z: -15 }];\n                const spawnZombieScratchPartHealth = { head: 0, leftArm: 0, rightArm: 0, leftLeg: 0, rightLeg: 0 };\n                const spawnZombieScratchBodyParts = {\n                    head: true, body: true, belly: false,\n                    leftArm: true, rightArm: true,\n                    leftLeg: true, rightLeg: true,\n                    partHealth: spawnZombieScratchPartHealth\n                };\n                const spawnZombieScratchData = {\n                    speed: 0, attackTimer: 0, animTimer: 0,\n                    state: 1, yaw: 0, meshRef: null, baseSpeed: 0, scale: 1,\n                    idlePhase: 0, instanceSlot: -1,\n                    typeName: '', isBoss: false, damageMult: 1,\n                    baseDamageMult: 1,\n                    armor: 0, maxArmor: 0, summonsLeft: 0\n                };\n                function spawnZombie(typeOverride = null) {\n                    const wave = gameState.currentWave;\n                    const zType = typeOverride || pickZombieTypeForWave(wave);\n                    const isBoss = typeOverride && typeOverride.wave !== undefined;\n\n                    const c = spawnZombieScratchCorridors[Math.floor(Math.random() * spawnZombieScratchCorridors.length)];\n                    const instanceSlot = acquireZombieInstanceSlot();\n                    if (instanceSlot < 0) {\n                        return;\n                    }\n                    const sx = c.x + (Math.random() - 0.5) * 2;\n\n                    // Scale zombie mesh based on type\n                    const scale = zType.scaleMult || 1.0;\n\n                    const eid = world.createEntity();\n                    // Wave scaling: zombies get slightly tougher each wave\n                    const waveHpScale = 1 + (wave - 1) * 0.08;\n                    const baseHp = 60 * zType.healthMult * waveHpScale * (config.zombieHealthMultiplier || 1);\n                    const partHpMult = zType.healthMult * waveHpScale * (config.zombieHealthMultiplier || 1);\n                    spawnZombieScratchPartHealth.head = 30 * partHpMult;\n                    spawnZombieScratchPartHealth.leftArm = 18 * partHpMult;\n                    spawnZombieScratchPartHealth.rightArm = 18 * partHpMult;\n                    spawnZombieScratchPartHealth.leftLeg = 24 * partHpMult;\n                    spawnZombieScratchPartHealth.rightLeg = 24 * partHpMult;\n                    spawnZombieScratchBodyParts.belly = zombieTypeHasBelly(zType.name);\n                    const bodyParts = spawnZombieScratchBodyParts;\n                    // Speed with slight wave scaling\n                    const waveSpeedScale = 1 + (wave - 1) * 0.02;\n                    const speed = (1.5 + Math.random() * 0.5) * zType.speedMult * waveSpeedScale * (config.zombieSpeedMultiplier || 1);\n\n                    let armor = 0;\n                    let maxArmor = 0;\n                    if (zType.name === 'Armored') {\n                        armor = maxArmor = baseHp * 0.8;\n                    } else if (zType.name === 'BEHEMOTH') {\n                        armor = maxArmor = baseHp * 0.5;\n                    }\n\n                    let summonsLeft = 0;\n                    if (zType.name === 'OVERLORD') summonsLeft = 8 + Math.floor(wave / 2);\n\n                    const runtimeSlot = acquireZombieRuntimeSlot(eid);\n                    if (runtimeSlot < 0) {\n                        world.removeEntity(eid);\n                        releaseZombieInstanceSlot(instanceSlot);\n                        return;\n                    }\n                    const zombieData = spawnZombieScratchData;\n                    zombieData.speed = speed;\n                    zombieData.attackTimer = 0;\n                    zombieData.animTimer = Math.random() * Math.PI * 2;\n                    zombieData.state = 1;\n                    zombieData.yaw = 0;\n                    zombieData.meshRef = null;\n                    zombieData.baseSpeed = speed;\n                    zombieData.scale = scale;\n                    zombieData.idlePhase = Math.random() * Math.PI * 2;\n                    zombieData.instanceSlot = instanceSlot;\n                    zombieData.typeName = zType.name;\n                    zombieData.isBoss = !!isBoss;\n                    zombieData.damageMult = zType.damageMult || 1;\n                    zombieData.baseDamageMult = zType.damageMult || 1;\n                    zombieData.armor = armor;\n                    zombieData.maxArmor = maxArmor;\n                    zombieData.summonsLeft = summonsLeft;\n\n                    const positionFacade = createZombiePositionFacade(runtimeSlot, sx, 0, c.z);\n                    const healthFacade = createZombieHealthFacade(runtimeSlot, baseHp, baseHp);\n                    const colliderFacade = createZombieColliderFacade(runtimeSlot, createZombieCollider(zType.name, scale, isBoss));\n                    const zombieFacade = createZombieFacade(runtimeSlot, zombieData);\n                    const bodyPartsFacade = createZombieBodyPartsFacade(runtimeSlot, bodyParts);\n                    world.addComponent(eid, 'Position', positionFacade);\n                    world.addComponent(eid, 'Health', healthFacade);\n                    world.addComponent(eid, 'Collider', colliderFacade);\n                    world.addComponent(eid, 'Zombie', zombieFacade);\n                    world.addComponent(eid, 'BodyParts', bodyPartsFacade);\n\n                    // Apply type-specific colors\n                    if (instanceSlot >= 0) {\n                        const skinColor = zType.skinColor || 0x4a5040;\n                        const darkSkin = (skinColor & 0xfefefe) >> 1; // Darker variant\n                        const clothColor = zType.clothColor || 0x2a2a2a;\n                        const eyeColor = zType.eyeColor || 0xff2222;\n                        for (let i = 0; i < zombieSkinColorParts.length; i++) setZombieInstanceColor(zombieSkinColorParts[i], instanceSlot, skinColor);\n                        for (let i = 0; i < zombieDarkSkinColorParts.length; i++) setZombieInstanceColor(zombieDarkSkinColorParts[i], instanceSlot, darkSkin);\n                        setZombieInstanceColor('jaw', instanceSlot, darkSkin);\n                        setZombieInstanceColor('teethUpper', instanceSlot, 0xccbb99);\n                        setZombieInstanceColor('teethLower', instanceSlot, 0xccbb99);\n                        setZombieInstanceColor('headWound', instanceSlot, 0x661111);\n                        setZombieInstanceColor('leftEye', instanceSlot, eyeColor);\n                        setZombieInstanceColor('rightEye', instanceSlot, eyeColor);\n                        setZombieInstanceColor('body', instanceSlot, clothColor);\n                        setZombieInstanceColor('belt', instanceSlot, isBoss ? 0x4a3a1a : 0x2a1a0a);\n                        setZombieInstanceColor('leftLeg', instanceSlot, clothColor);\n                        setZombieInstanceColor('rightLeg', instanceSlot, clothColor);\n                        for (let i = 0; i < zombieStumpColorParts.length; i++) setZombieInstanceColor(zombieStumpColorParts[i], instanceSlot, 0x8a2020);\n                        // Belly color (greenish skin for bloater)\n                        if (zombieTypeHasBelly(zType.name)) {\n                            const bellyColor = zType.name === 'Bloater' ? 0x5a7a3a : 0x3a4a3a;\n                            setZombieInstanceColor('belly', instanceSlot, bellyColor);\n                        }\n                    }\n                    gameState.zombiesSpawned++;\n                    if (Math.random() < (isBoss ? 0.9 : 0.3)) {\n                        const groanVariants = ['zombie_groan', 'zombie_groan_2', 'zombie_groan_3', 'zombie_groan_4'];\n                        const groan = groanVariants[Math.floor(Math.random() * groanVariants.length)];\n                        scheduleRuntimeTimeout(() => playSound(groan, isBoss ? 0.5 : (0.15 + Math.random() * 0.12)), Math.random() * 500);\n                    }\n                }\n\n                // ===== WAVE MANAGEMENT =====\n                function startWave() {\n                    gameState.waveActive = true;\n                    // Ensure the proxy pool is at full warm-target before the wave starts so that\n                    // multi-kill bursts never trigger a synchronous createZombieMesh allocation.\n                    if (zombieMeshPool.size < ZOMBIE_PROXY_WARM_TARGET) warmZombieProxyPool();\n                    const wave = gameState.currentWave;\n                    const boss = getBossForWave(wave);\n                    const baseCount = getWaveZombieCount(wave);\n                    // Boss waves add the boss + slightly fewer regular zombies\n                    gameState.zombiesToSpawn = boss ? baseCount + 1 : baseCount;\n                    gameState.bossToSpawn = boss;\n                    gameState.bossSpawned = false;\n                    gameState.zombiesSpawned = 0;\n                    gameState.zombiesRemaining = gameState.zombiesToSpawn;\n                    gameState.spawnTimer = 0;\n                    gameState.waveEndPending = false;\n                    gameState.waveEndDelay = 0;\n                    const bannerText = boss\n                        ? t('hud.waveBoss', { wave, boss: getZombieDisplayName(boss.name) })\n                        : t('hud.wave', { wave });\n                    waveBanner.textContent = bannerText;\n                    restartActiveClassNextFrame(waveBanner);\n                    waveDisplay.textContent = t('hud.wave', { wave });\n                    if (enemiesDisplay) enemiesDisplay.textContent = '';\n                }\n\n                function endWave() {\n                    gameState.waveActive = false; gameState.waveCooldown = 999; // Paused until upgrade chosen\n                    clearSmartPistolRuntime(true);\n                    playSound('wave_complete_sound', 0.6);\n                    gameState.score += gameState.currentWave * 100;\n                    // Refill ammo for all owned weapons\n                    roguelikeState.ownedWeapons.forEach(wi => {\n                        const ws = weaponStates[wi];\n                        if (ws.magSize !== Infinity) ws.mag = ws.magSize;\n                        if (ws.ammo !== Infinity) ws.ammo = ws.maxAmmo;\n                    });\n                    gameState.currentWave++;\n                    // Show upgrade selection screen after 0.5s delay\n                    scheduleRuntimeTimeout(() => showUpgradeScreen(), 500);\n                }\n\n                function updateGameOverTitle(rank) {\n                    const gameOverTitles = {\n                        en: {\n                            normal: \"You did well, but it wasn't enough...\",\n                            top10: \"You were so close to being first, try again!\",\n                            first: \"You are the ultimate victor, congratulations on getting first place!\"\n                        },\n                        zh: {\n                            normal: \"\u4f60\u505a\u7684\u5f88\u597d\uff0c\u4f46\u662f\u8fd8\u4e0d\u591f...\",\n                            top10: \"\u4f60\u5dee\u4e00\u70b9\u5c31\u662f\u7b2c\u4e00\u4e86,\u518d\u8bd5\u8bd5\",\n                            first: \"\u4f60\u662f\u6700\u7ec8\u7684\u80dc\u5229\u8005\uff0c\u606d\u559c\u4f60\u83b7\u5f97\u4e86\u7b2c\u4e00\"\n                        },\n                        id: {\n                            normal: \"Kamu sudah berusaha dengan baik, tapi itu belum cukup...\",\n                            top10: \"Kamu hampir menjadi nomor satu, coba lagi!\",\n                            first: \"Kamu adalah pemenang sejati, selamat atas peringkat pertama!\"\n                        },\n                        ru: {\n                            normal: \"\u0412\u044b \u043e\u0442\u043b\u0438\u0447\u043d\u043e \u0441\u043f\u0440\u0430\u0432\u0438\u043b\u0438\u0441\u044c, \u043d\u043e \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e...\",\n                            top10: \"\u0412\u044b \u0431\u044b\u043b\u0438 \u0442\u0430\u043a \u0431\u043b\u0438\u0437\u043a\u0438 \u043a \u043f\u0435\u0440\u0432\u043e\u043c\u0443 \u043c\u0435\u0441\u0442\u0443, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437!\",\n                            first: \"\u0412\u044b \u2014 \u0430\u0431\u0441\u043e\u043b\u044e\u0442\u043d\u044b\u0439 \u043f\u043e\u0431\u0435\u0434\u0438\u0442\u0435\u043b\u044c, \u043f\u043e\u0437\u0434\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u0441 \u043f\u0435\u0440\u0432\u044b\u043c \u043c\u0435\u0441\u0442\u043e\u043c!\"\n                        },\n                        ur: {\n                            normal: \"\u0622\u067e \u0646\u06d2 \u0628\u06c1\u062a \u0627\u0686\u06be\u0627 \u06a9\u06be\u06cc\u0644 \u067e\u06cc\u0634 \u06a9\u06cc\u0627\u060c \u0644\u06cc\u06a9\u0646 \u06cc\u06c1 \u06a9\u0627\u0641\u06cc \u0646\u06c1\u06cc\u06ba \u062a\u06be\u0627...\",\n                            top10: \"\u0622\u067e \u067e\u06c1\u0644\u06d2 \u0646\u0645\u0628\u0631 \u067e\u0631 \u0622\u0646\u06d2 \u06a9\u06d2 \u0628\u0627\u0644\u06a9\u0644 \u0642\u0631\u06cc\u0628 \u062a\u06be\u06d2\u060c \u062f\u0648\u0628\u0627\u0631\u06c1 \u06a9\u0648\u0634\u0634 \u06a9\u0631\u06cc\u06ba!\",\n                            first: \"\u0622\u067e \u06c1\u06cc \u062d\u062a\u0645\u06cc \u0641\u0627\u062a\u062d \u06c1\u06cc\u06ba\u060c \u067e\u06c1\u0644\u0627 \u0645\u0642\u0627\u0645 \u062d\u0627\u0635\u0644 \u06a9\u0631\u0646\u06d2 \u067e\u0631 \u0628\u06c1\u062a \u0628\u06c1\u062a \u0645\u0628\u0627\u0631\u06a9 \u06c1\u0648!\"\n                        },\n                        bn: {\n                            normal: \"\u0986\u09aa\u09a8\u09bf \u0985\u09a8\u09c7\u0995 \u099a\u09c7\u09b7\u09cd\u099f\u09be \u0995\u09b0\u09c7\u099b\u09c7\u09a8, \u09a4\u09ac\u09c7 \u09a4\u09be \u09af\u09a5\u09c7\u09b7\u09cd\u099f \u099b\u09bf\u09b2 \u09a8\u09be...\",\n                            top10: \"\u0986\u09aa\u09a8\u09bf \u09aa\u09cd\u09b0\u09a5\u09ae \u09b9\u0993\u09df\u09be\u09b0 \u0996\u09c1\u09ac \u0995\u09be\u099b\u09be\u0995\u09be\u099b\u09bf \u099b\u09bf\u09b2\u09c7\u09a8, \u0986\u09ac\u09be\u09b0 \u099a\u09c7\u09b7\u09cd\u099f\u09be \u0995\u09b0\u09c1\u09a8!\",\n                            first: \"\u0986\u09aa\u09a8\u09bf\u0987 \u099a\u09dc\u09ae \u09ac\u09bf\u099c\u09df\u09c0, \u09aa\u09cd\u09b0\u09a5\u09ae \u09b8\u09cd\u09a5\u09be\u09a8 \u0985\u09a7\u09bf\u0995\u09be\u09b0 \u0995\u09b0\u09be\u09b0 \u099c\u09a8\u09cd\u09af \u0985\u09ad\u09bf\u09a8\u09a8\u09cd\u09a6\u09a8!\"\n                        },\n                        sw: {\n                            normal: \"Ulifanya vizuri sana, lakini haitoshi...\",\n                            top10: \"Ulikuwa karibu sana kuwa wa kwanza, jaribu tena!\",\n                            first: \"Wewe ndiye mshindi mkuu, hongera kwa ushindi wa kwanza!\"\n                        },\n                        de: {\n                            normal: \"Du hast dich gut geschlagen, aber das war leider nicht genug...\",\n                            top10: \"Du warst so nah dran, Erster zu sein, versuch es noch mal!\",\n                            first: \"Du bist der ultimative Sieger, herzlichen Gl\u00fcckwunsch zum ersten Platz!\"\n                        },\n                        tr: {\n                            normal: \"\u0130yi m\u00fccadele ettin ama bu kadar\u0131 yetmedi...\",\n                            top10: \"Birinci olmaya \u00e7ok yakla\u015ft\u0131n, tekrar dene!\",\n                            first: \"E\u015fsiz kazanan sensin, birincili\u011fin i\u00e7in tebrikler!\"\n                        },\n                        ja: {\n                            normal: \"\u5065\u95d8\u3057\u307e\u3057\u305f\u304c\u3001\u307e\u3060\u53ca\u3073\u307e\u305b\u3093\u3067\u3057\u305f...\",\n                            top10: \"1\u4f4d\u307e\u3067\u3042\u3068\u4e00\u6b69\u3067\u3057\u305f\uff01\u3082\u3046\u4e00\u5ea6\u6311\u6226\u3057\u307e\u3057\u3087\u3046\uff01\",\n                            first: \"\u3042\u306a\u305f\u304c\u7a76\u6975\u306e\u8987\u8005\u3067\u3059\uff011\u4f4d\u7372\u5f97\u304a\u3081\u3067\u3068\u3046\u3054\u3056\u3044\u307e\u3059\uff01\"\n                        },\n                        te: {\n                            normal: \"\u0c2e\u0c40\u0c30\u0c41 \u0c2c\u0c3e\u0c17\u0c3e\u0c28\u0c47 \u0c06\u0c21\u0c3e\u0c30\u0c41, \u0c15\u0c3e\u0c28\u0c40 \u0c05\u0c26\u0c3f \u0c38\u0c30\u0c3f\u0c2a\u0c4b\u0c32\u0c47\u0c26\u0c41...\",\n                            top10: \"\u0c2e\u0c40\u0c30\u0c41 \u0c2e\u0c4a\u0c26\u0c1f\u0c3f \u0c38\u0c4d\u0c25\u0c3e\u0c28\u0c3e\u0c28\u0c3f\u0c15\u0c3f \u0c1a\u0c3e\u0c32\u0c3e \u0c26\u0c17\u0c4d\u0c17\u0c30\u0c17\u0c3e \u0c35\u0c1a\u0c4d\u0c1a\u0c3e\u0c30\u0c41, \u0c2e\u0c33\u0c4d\u0c32\u0c40 \u0c2a\u0c4d\u0c30\u0c2f\u0c24\u0c4d\u0c28\u0c3f\u0c02\u0c1a\u0c02\u0c21\u0c3f!\",\n                            first: \"\u0c2e\u0c40\u0c30\u0c47 \u0c05\u0c02\u0c24\u0c3f\u0c2e \u0c35\u0c3f\u0c1c\u0c47\u0c24, \u0c2e\u0c4a\u0c26\u0c1f\u0c3f \u0c38\u0c4d\u0c25\u0c3e\u0c28\u0c3e\u0c28\u0c4d\u0c28\u0c3f \u0c38\u0c3e\u0c27\u0c3f\u0c02\u0c1a\u0c3f\u0c28\u0c02\u0c26\u0c41\u0c15\u0c41 \u0c05\u0c2d\u0bbf\u0ba9\u0c02\u0c26\u0c28\u0c32\u0c41!\"\n                        },\n                        vi: {\n                            normal: \"B\u1ea1n \u0111\u00e3 chi\u1ebfn \u0111\u1ea5u r\u1ea5t t\u1ed1t, nh\u01b0ng v\u1eabn ch\u01b0a \u0111\u1ee7...\",\n                            top10: \"B\u1ea1n su\u00fdt ch\u00fat n\u1eefa l\u00e0 \u0111\u1ee9ng \u0111\u1ea7u r\u1ed3i, th\u1eed l\u1ea1i xem sao!\",\n                            first: \"B\u1ea1n l\u00e0 ng\u01b0\u1eddi chi\u1ebfn th\u1eafng cu\u1ed1i c\u00f9ng, ch\u00fac m\u1eebng b\u1ea1n \u0111\u1ea1t h\u1ea1ng nh\u1ea5t!\"\n                        },\n                        mr: {\n                            normal: \"\u0924\u0941\u092e\u094d\u0939\u0940 \u091a\u093e\u0902\u0917\u0932\u0940 \u091d\u0941\u0902\u091c \u0926\u093f\u0932\u0940, \u092a\u0923 \u0924\u0947 \u092a\u0941\u0930\u0947\u0938\u0947 \u0928\u0935\u094d\u0939\u0924\u0947...\",\n                            top10: \"\u0924\u0941\u092e\u094d\u0939\u0940 \u092a\u0939\u093f\u0932\u094d\u092f\u093e \u0915\u094d\u0930\u092e\u093e\u0902\u0915\u093e\u091a\u094d\u092f\u093e \u0905\u0917\u0926\u0940 \u091c\u0935\u0933 \u0939\u094b\u0924\u093e, \u092a\u0941\u0928\u094d\u0939\u093e \u092a\u094d\u0930\u092f\u0924\u094d\u0928 \u0915\u0930\u093e!\",\n                            first: \"\u0924\u0941\u092e\u094d\u0939\u0940\u091a \u0905\u0902\u0924\u093f\u092e \u0935\u093f\u091c\u0947\u0924\u0947 \u0906\u0939\u093e\u0924, \u092a\u094d\u0930\u0925\u092e \u0915\u094d\u0930\u092e\u093e\u0902\u0915 \u092e\u093f\u0933\u0935\u0932\u094d\u092f\u093e\u092c\u0926\u094d\u0926\u0932 \u0905\u092d\u093f\u0928\u0902\u0926\u0928!\"\n                        },\n                        ta: {\n                            normal: \"\u0ba8\u0bc0\u0b99\u0bcd\u0b95\u0bb3\u0bcd \u0ba8\u0ba9\u0bcd\u0bb1\u0bbe\u0b95 \u0bb5\u0bbf\u0bb3\u0bc8\u0baf\u0bbe\u0b9f\u0bbf\u0ba9\u0bc0\u0bb0\u0bcd\u0b95\u0bb3\u0bcd, \u0b86\u0ba9\u0bbe\u0bb2\u0bcd \u0b85\u0ba4\u0bc1 \u0baa\u0bcb\u0ba4\u0bbe\u0ba4\u0bc1...\",\n                            top10: \"\u0ba8\u0bc0\u0b99\u0bcd\u0b95\u0bb3\u0bcd \u0bae\u0bc1\u0ba4\u0bb2\u0bbf\u0b9f\u0ba4\u0bcd\u0ba4\u0bbf\u0bb1\u0bcd\u0b95\u0bc1 \u0bae\u0bbf\u0b95 \u0b85\u0bb0\u0bc1\u0b95\u0bbf\u0bb2\u0bcd \u0bb5\u0ba8\u0bcd\u0ba4\u0bc0\u0bb0\u0bcd\u0b95\u0bb3\u0bcd, \u0bae\u0bc0\u0ba3\u0bcd\u0b9f\u0bc1\u0bae\u0bcd \u0bae\u0bc1\u0baf\u0bb1\u0bcd\u0b9a\u0bbf\u0b95\u0bcd\u0b95\u0bb5\u0bc1\u0bae\u0bcd!\",\n                            first: \"\u0ba8\u0bc0\u0b99\u0bcd\u0b95\u0bb3\u0bc7 \u0b87\u0bb1\u0bc1\u0ba4\u0bbf \u0bb5\u0bc6\u0bb1\u0bcd\u0bb1\u0bbf\u0baf\u0bbe\u0bb3\u0bb0\u0bcd, \u0bae\u0bc1\u0ba4\u0bb2\u0bbf\u0b9f\u0bae\u0bcd \u0baa\u0bbf\u0b9f\u0bbf\u0ba4\u0bcd\u0ba4\u0ba4\u0bb1\u0bcd\u0b95\u0bc1 \u0bb5\u0bbe\u0bb4\u0bcd\u0ba4\u0bcd\u0ba4\u0bc1\u0b95\u0bb3\u0bcd!\"\n                        },\n                        it: {\n                            normal: \"Hai combattuto bene, ma purtroppo non \u00e8 bastato...\",\n                            top10: \"Sei andato vicinissimo al primo posto, riprova!\",\n                            first: \"Sei il vincitore supremo, congratulazioni per il primo posto!\"\n                        }\n                    };\n\n                    const localTitles = gameOverTitles[currentLocale] || gameOverTitles.en;\n                    const titleEl = document.querySelector('#game-over-screen h1');\n                    if (titleEl) {\n                        if (rank === 1) {\n                            titleEl.textContent = localTitles.first;\n                        } else if (rank >= 2 && rank <= 10) {\n                            titleEl.textContent = localTitles.top10;\n                        } else {\n                            titleEl.textContent = localTitles.normal;\n                        }\n                    }\n                }\n\n                function endGame() {\n                    gameState.over = true; document.exitPointerLock();\n                    stopLoopedSound('ambient_bunker_sound');\n                    stopLoopedSound('bgm_main');\n                    stopLoopedSound(FLAMETHROWER_LOOP_SOUND_ID);\n                    document.getElementById('final-wave').textContent = gameState.currentWave;\n                    document.getElementById('final-kills').textContent = gameState.kills;\n                    document.getElementById('final-headshots').textContent = gameState.headshots;\n                    document.getElementById('final-score').textContent = gameState.score;\n                    const acc = gameState.shotsFired > 0 ? Math.round((gameState.shotsHit / gameState.shotsFired) * 100) : 0;\n                    document.getElementById('final-accuracy').textContent = acc + '%';\n\n                    updateGameOverTitle(-1);\n\n                    // Submit score to leaderboard and display results\n                    submitScoreAndShowLeaderboard(gameState.score);\n\n                    gameOverScreen.classList.add('active');\n                }\n\n                async function submitScoreAndShowLeaderboard(score) {\n                    const leaderboardContainer = document.getElementById('leaderboard-container');\n                    leaderboardContainer.innerHTML = '<div class=\"leaderboard-title\">Loading leaderboard...</div>';\n\n                    try {\n                        // Submit score and get leaderboard entries\n                        const response = await lib.addPlayerScoreToLeaderboard(score, 10);\n\n                        if (response && response.entries) {\n                            displayLeaderboard(response.entries, response.userRank, score);\n                        } else {\n                            leaderboardContainer.innerHTML = '<div class=\"leaderboard-title\">Leaderboard unavailable</div>';\n                        }\n                    } catch (error) {\n                        lib.log('Failed to load leaderboard: ' + error.message);\n                        // Show leaderboard anyway without submission\n                        try {\n                            const response = await lib.getTopNEntriesFromLeaderboard(10);\n                            if (response && response.entries) {\n                                displayLeaderboard(response.entries, response.userRank, score);\n                            } else {\n                                leaderboardContainer.innerHTML = '<div class=\"leaderboard-title\">Leaderboard unavailable</div>';\n                            }\n                        } catch (fallbackError) {\n                            lib.log('Failed to fetch leaderboard: ' + fallbackError.message);\n                            leaderboardContainer.innerHTML = '<div class=\"leaderboard-title\">Leaderboard unavailable</div>';\n                        }\n                    }\n                }\n\n                function displayLeaderboard(entries, userRank, currentScore) {\n                    const leaderboardContainer = document.getElementById('leaderboard-container');\n\n                    if (!entries) entries = [];\n\n                    let displayEntries = [];\n                    for (let i = 0; i < entries.length; i++) {\n                        displayEntries.push({\n                            username: entries[i].username,\n                            score: entries[i].score,\n                            isCurrent: false\n                        });\n                    }\n\n                    let calculatedRank = 1;\n                    for (let i = 0; i < displayEntries.length; i++) {\n                        if (currentScore >= displayEntries[i].score) {\n                            break;\n                        }\n                        calculatedRank++;\n                    }\n\n                    let foundMatch = false;\n                    if (calculatedRank <= displayEntries.length && displayEntries[calculatedRank - 1].score === currentScore) {\n                        foundMatch = true;\n                    }\n\n                    if (!foundMatch && calculatedRank <= 10) {\n                        displayEntries.splice(calculatedRank - 1, 0, {\n                            username: 'You (Current)',\n                            score: currentScore,\n                            isCurrent: true\n                        });\n                        if (displayEntries.length > 10) {\n                            displayEntries.pop();\n                        }\n                    } else if (foundMatch) {\n                        displayEntries[calculatedRank - 1].isCurrent = true;\n                        displayEntries[calculatedRank - 1].username = 'You (Current)';\n                    }\n\n                    const finalRank = (typeof userRank === 'number' && userRank > 0) ? userRank : calculatedRank;\n                    updateGameOverTitle(finalRank);\n\n                    if (displayEntries.length === 0) {\n                        leaderboardContainer.innerHTML = '<div class=\"leaderboard-title\">No scores yet</div>';\n                        return;\n                    }\n\n                    let html = '<div class=\"leaderboard-title\">LEADERBOARD</div>';\n                    html += '<table class=\"leaderboard-table\">';\n                    html += '<thead><tr>';\n                    html += '<th class=\"rank-cell\">RANK</th>';\n                    html += '<th class=\"name-cell\">PLAYER</th>';\n                    html += '<th class=\"score-cell\">SCORE</th>';\n                    html += '</tr></thead><tbody>';\n\n                    displayEntries.forEach((entry, index) => {\n                        const rank = index + 1;\n                        const isCurrentPlayer = entry.isCurrent;\n                        const rowClass = isCurrentPlayer ? 'current-player' : '';\n                        const username = entry.username || 'Player';\n                        const displayName = username.length > 20 ? username.substring(0, 20) + '...' : username;\n\n                        html += `<tr class=\"${rowClass}\">`;\n                        html += `<td class=\"rank-cell\">#${rank}</td>`;\n                        html += `<td class=\"name-cell\">${escapeHtml(displayName)}</td>`;\n                        html += `<td class=\"score-cell\">${entry.score.toLocaleString()}</td>`;\n                        html += '</tr>';\n                    });\n\n                    if (!foundMatch && calculatedRank > 10) {\n                        html += `<tr class=\"current-player\" style=\"border-top: 2px dashed rgba(255, 204, 0, 0.4);\">`;\n                        html += `<td class=\"rank-cell\">#99+</td>`;\n                        html += `<td class=\"name-cell\">You (Current)</td>`;\n                        html += `<td class=\"score-cell\">${currentScore.toLocaleString()}</td>`;\n                        html += '</tr>';\n                    }\n\n                    html += '</tbody></table>';\n                    leaderboardContainer.innerHTML = html;\n                }\n\n                function escapeHtml(text) {\n                    const div = document.createElement('div');\n                    div.textContent = text;\n                    return div.innerHTML;\n                }\n\n                function resetWeaponRuntime() {\n                    weaponStates.forEach((ws, i) => {\n                        ws.ammo = initialWeaponState[i].ammo;\n                        ws.mag = initialWeaponState[i].mag;\n                    });\n                    const wComp = world.getComponent(playerEntity, 'Weapon');\n                    Object.assign(wComp, {\n                        index: 0,\n                        fireTimer: 0,\n                        isReloading: false,\n                        reloadTimer: 0,\n                        spreadHeat: 0,\n                        visualKick: 0,\n                        reloadStage: 0,\n                        recoilPitchDebt: 0,\n                        recoilYawDebt: 0,\n                        lastShellAt: 0,\n                        katanaInputBuffer: 0,\n                        pendingKatanaHit: null,\n                        flameHeat: 0\n                    });\n                }\n\n                function clearRuntimeEntitiesAndEffects() {\n                    world.flush();\n                    clearRuntimeTimers();\n                    clearProjectiles();\n                    resetFlameRuntime();\n                    zombieCleanupIds.length = 0;\n                    for (let i = 0; i < zombieRuntime.activeSlots.length; i++) {\n                        zombieCleanupIds.push(zombieRuntime.entityId[zombieRuntime.activeSlots[i]]);\n                    }\n                    for (const id of zombieCleanupIds) {\n                        const zombie = world.getComponent(id, 'Zombie');\n                        if (zombie) releaseZombieInstanceSlot(zombie.instanceSlot);\n                        if (zombie && zombie.meshRef) zombieMeshPool.release(zombie.meshRef);\n                        releaseZombieRuntimeSlot(id);\n                        world.removeEntity(id);\n                    }\n                    while (bulletHoles.length) releaseBulletHoleRecord(bulletHoles[bulletHoles.length - 1]);\n                    resetImpactSparkRuntime();\n                    resetBloodMistRuntime();\n                    resetSeveredRuntime();\n                    resetCorpseRuntime();\n                    resetCameraShakeRuntime();\n                    resetZombieDeathRuntime();\n                    resetBloodFlowInstances();\n                    while (activeShellSlots.length > 0) releaseShellCasingSlot(activeShellSlots[activeShellSlots.length - 1]);\n                    pendingAIResults = null;\n                    pendingAIResultsPacked = null;\n                    aiFallbackSliceCursor = 0;\n                    zombieRuntime.activeIndex.fill(-1);\n                    zombieRuntime.instanceSlot.fill(-1);\n                    zombieRenderLastFrame.fill(0);\n                    zombieRenderForce.fill(0);\n                    zombieRenderInView.fill(0);\n                    aiFallbackLastUpdateFrame.fill(0);\n                    clearZombieDeathEffectQueue();\n                }\n\n                function resetAIWorkerRuntime() {\n                    aiGeneration++;\n                    pendingAIResults = null;\n                    pendingAIResultsPacked = null;\n                    aiWorkerFailureCount = 0;\n                    aiFallbackSliceCursor = 0;\n                    aiFallbackLastUpdateFrame.fill(0);\n                    const waitForTransferredBuffers = aiWorker && aiRequestPending;\n                    if (waitForTransferredBuffers) return;\n                    aiRequestPending = false;\n                    if (aiWorker) return;\n                    try {\n                        aiWorker = createAIWorker();\n                        attachAIWorkerHandlers();\n                    } catch (e) {\n                        aiWorker = null;\n                    }\n                }\n\n                function resetPlayerRuntime() {\n                    const pos = world.getComponent(playerEntity, 'Position');\n                    const player = world.getComponent(playerEntity, 'Player');\n                    const hp = world.getComponent(playerEntity, 'Health');\n                    pos.x = 0; pos.y = 1.7; pos.z = 10;\n                    player.yaw = 0; player.pitch = 0;\n                    hp.current = 100; hp.max = 100;\n                    cancelActiveTouchControls();\n                    input.reload = false;\n                    input.switchTo = -1;\n                    input.backpackToggle = false;\n                    input.yawDelta = 0;\n                    input.pitchDelta = 0;\n                    for (const key in input.keys) input.keys[key] = false;\n                    wasdStateMask = -1;\n                    rightJoystickValue.x = 0;\n                    rightJoystickValue.y = 0;\n                    joystickLeftVisible = false;\n                    joystickLeftDirty = true;\n                    joystickLeftVisual.dx = 0;\n                    joystickLeftVisual.dy = 0;\n                    shootJoystickVisible = false;\n                    shootJoystickDirty = true;\n                    shootJoystickVisual.dx = 0;\n                    shootJoystickVisual.dy = 0;\n                    if (!isTouchDevice) {\n                        joystickLeft.style.display = 'none';\n                        shootContainer.style.display = 'none';\n                    }\n                    joystickRight.style.display = 'none';\n                }\n\n                function resetGameStateRuntime() {\n                    perfBudget.level = perfProfile.lowEnd ? 1 : 0;\n                    perfBudget.smoothedFrameMs = perfProfile.lowEnd ? 18 : 16.7;\n                    perfBudget.sampleMs = 0;\n                    perfBudget.sampleFrames = 0;\n                    perfBudget.longFrames = 0;\n                    perfBudget.lastFrameMs = 16.7;\n                    perfBudget.zombies = 0;\n                    perfBudget.effects = 0;\n                    runtimePerfVisualScale = getPerfAdjustedVisualScale();\n                    applyPerfPressureClass();\n                    Object.assign(gameState, {\n                        started: false,\n                        over: false,\n                        score: 0,\n                        kills: 0,\n                        shotsFired: 0,\n                        shotsHit: 0,\n                        headshots: 0,\n                        currentWave: config.startingWave || 1,\n                        waveActive: false,\n                        waveCooldown: 0,\n                        waveEndPending: false,\n                        waveEndDelay: 0,\n                        zombiesRemaining: 0,\n                        zombiesSpawned: 0,\n                        zombiesToSpawn: 0,\n                        spawnTimer: 0,\n                        frameId: 0,\n                        upgradeScreenOpen: false,\n                        upgradeSelecting: false,\n                        bossToSpawn: null,\n                        bossSpawned: false,\n                        damageDealerId: null,\n                        lastPlayerDamageTime: 0\n                    });\n                    uiState.health = null;\n                    uiState.healthText = null;\n                    uiState.score = null;\n                    uiState.weaponKey = '';\n                    uiState.lowAmmo = null;\n                    uiState.activeWeapon = -1;\n                    uiState.crosshairGap = null;\n                    uiState.hitScale = null;\n                    if (typeof frameCtx !== 'undefined') frameCtx.spatialGridBuilt = false;\n                    hitStopTimer = 0;\n                    if (typeof flamethrowerLight !== 'undefined') flamethrowerLight.intensity = 0;\n                    if (typeof flamethrowerMuzzleLight !== 'undefined') flamethrowerMuzzleLight.intensity = 0;\n                }\n\n                function resetWeaponViewmodelRuntime() {\n                    weaponAnim.recoilAmount = 0;\n                    weaponAnim.recoilTarget = 0;\n                    weaponAnim.switchAnim = 0;\n                    weaponAnim.currentModel = 0;\n                    weaponAnim.muzzleFlashEnergy = 0;\n                    weaponAnim.muzzleFlashFrameCounter = 0;\n                    weaponAnim.gunKickTimer = 0;\n                    weaponAnim.gunKickAmount = 0;\n                    weaponAnim.gunKickTarget = 0;\n                    weaponAnim.gunKickStrength = 0;\n                    weaponAnim.fireJolt = 0;\n                    weaponAnim.fireJoltDir = 1;\n                    weaponAnim.viewKickTimer = 0;\n                    weaponAnim.slashCount = 0;\n                    weaponAnim.slashDuration = 0.38 * KATANA_ANIM_SLOWMO;\n                    weaponAnim.comboHoldTimer = 0;\n                    weaponAnim.comboRecoverTimer = 0;\n                    muzzleFlash3D.visible = false;\n                    muzzleFlashLightTimer = 0;\n                    muzzleFlashLightPeak = 0;\n                    muzzleFlashLight.intensity = 0;\n                    katanaTrail.visible = false;\n                    katanaTrail.material.opacity = 0;\n                    setActiveWeaponModel(0);\n                }\n\n                function clearKatanaRuntime(wComp = null) {\n                    if (wComp) {\n                        wComp.katanaInputBuffer = 0;\n                        wComp.pendingKatanaHit = null;\n                    }\n                    weaponAnim.slashCount = 0;\n                    weaponAnim.slashDuration = 0.38 * KATANA_ANIM_SLOWMO;\n                    weaponAnim.comboHoldTimer = 0;\n                    weaponAnim.comboRecoverTimer = 0;\n                    weaponAnim.viewKickTimer = 0;\n                    weaponAnim.recoilAmount = 0;\n                    weaponAnim.recoilTarget = 0;\n                    weaponAnim.fireJolt = 0;\n                    katanaTrail.visible = false;\n                    katanaTrail.material.opacity = 0;\n                }\n\n                function restartGame() {\n                    stopLoopedSound('ambient_bunker_sound');\n                    stopLoopedSound('bgm_main');\n                    clearUpgradeCountdown();\n                    currentUpgradeOptions = [];\n                    clearRuntimeEntitiesAndEffects();\n                    resetAIWorkerRuntime();\n                    resetPlayerRuntime();\n                    resetWeaponStatsToOriginal();\n                    resetWeaponRuntime();\n                    resetGameStateRuntime();\n                    resetWeaponViewmodelRuntime();\n\n                    toggleSniperScope(false);\n\n                    weaponTutorialActive = false;\n                    weaponTutorialShown = false;\n                    tutorialHighlightActive = false;\n                    tutorialSpotlightCompleteTimer = -1;\n                    const tutOverlay = document.getElementById('weapon-tutorial-overlay');\n                    if (tutOverlay) tutOverlay.classList.remove('active');\n\n                    for (let k in damageIndicators) {\n                        const ind = damageIndicators[k];\n                        if (ind) ind.classList.remove('active', 'soft');\n                    }\n                    hitMarker.classList.remove('active', 'body', 'headshot', 'kill', 'pulse-a', 'pulse-b');\n                    crosshair.classList.remove('hit', 'body', 'headshot', 'kill');\n                    waveBanner.classList.remove('active');\n                    gameOverScreen.classList.remove('active');\n                    upgradeSelectScreen.classList.remove('active');\n                    startScreen.style.display = 'none';\n                    gameState.upgradeScreenOpen = false;\n                    // Reset roguelike state and show weapon selection\n                    roguelikeState.ownedWeapons = [0];\n                    roguelikeState.upgradeHistory = [];\n                    flamethrowerUpgrades.rangeBoost = 0;\n                    flamethrowerUpgrades.stickyFire = 0;\n                    flamethrowerUpgrades.wideNozzle = 0;\n                    flamethrowerUpgrades.fireSpread = 0;\n                    flamethrowerUpgrades.armorMelting = 0;\n                    lifestealPercent = 0;\n                    hasAutoFirePassive = false;\n                    hasSmartPistol = true;\n                    smartPistolLockSpeedMultiplier = 1.0;\n                    clearSmartPistolRuntime(true);\n                    hasSlowTimeSkill = false;\n                    if (skillBtn) skillBtn.style.display = 'none';\n                    if (!detectedTouchDevice) {\n                        wasdDisplay.classList.add('pc-visible');\n                    }\n                    resetComboSystem();\n                    updateWeaponSlotsVisibility();\n                    updateWeaponDisplay();\n                    updateScopeBtnVisibility();\n                    showWeaponSelectScreen();\n                }\n\n                // ===== UI ELEMENTS =====\n                const hud = document.getElementById('hud');\n                const startScreen = document.getElementById('start-screen');\n                const gameOverScreen = document.getElementById('game-over-screen');\n                const healthFill = document.getElementById('health-fill');\n                const healthText = document.getElementById('health-text');\n                const ammoDisplay = document.getElementById('ammo-display');\n                const ammoText = document.getElementById('ammo-text');\n                const weaponNameEl = document.getElementById('weapon-name');\n                const waveDisplay = document.getElementById('wave-display');\n                const scoreValue = document.getElementById('score-value');\n                const enemiesDisplay = document.getElementById('enemies-display');\n                const waveBanner = document.getElementById('wave-banner');\n                const crosshair = document.getElementById('crosshair');\n                const hitMarker = document.getElementById('hit-marker');\n                const damageIndicators = {\n                    top: document.getElementById('damage-ind-top'),\n                    bottom: document.getElementById('damage-ind-bottom'),\n                    left: document.getElementById('damage-ind-left'),\n                    right: document.getElementById('damage-ind-right')\n                };\n                let damageIndicatorAlt = false;\n                const weaponSlotsEl = document.getElementById('weapon-slots');\n                if (!isTouchDevice) weaponSlotsEl.classList.add('pc-mode');\n                const reloadBtn = document.getElementById('reload-btn');\n                const reloadIndicator = document.getElementById('reload-indicator');\n                const joystickLeft = document.getElementById('joystick-left');\n                const joystickRight = document.getElementById('joystick-right');\n                const joystickLeftThumb = document.getElementById('joystick-left-thumb');\n                const joystickRightThumb = document.getElementById('joystick-right-thumb');\n\n\n                // ===== 3D WEAPON VIEWMODEL SYSTEM =====\n                // Separate scene/camera for weapon rendering (prevents clipping with world)\n                const weaponScene = new THREE.Scene();\n                const weaponCamera = new THREE.PerspectiveCamera(getBaseWeaponFov(), gameWorld.clientWidth / gameWorld.clientHeight, 0.005, 10);\n                weaponScene.add(weaponCamera);\n\n                const weaponAmbient = new THREE.AmbientLight(0xffffff, 0.5);\n                weaponScene.add(weaponAmbient);\n                const weaponDirLight = new THREE.DirectionalLight(0xffffff, 1.1);\n                weaponDirLight.position.set(1, 2, 1);\n                weaponScene.add(weaponDirLight);\n                // Rim light from top-left for metallic edge highlights\n                const weaponRimLight = new THREE.DirectionalLight(0xaac8ff, 0.35);\n                weaponRimLight.position.set(-2, 3, -1);\n                weaponScene.add(weaponRimLight);\n                // Subtle fill light from below to soften shadows\n                const weaponFillLight = new THREE.DirectionalLight(0x334466, 0.25);\n                weaponFillLight.position.set(0, -1, 2);\n                if (!perfProfile.lowEnd) weaponScene.add(weaponFillLight);\n\n                // Weapon holder (child of weapon camera)\n                const weaponHolder = new THREE.Group();\n                weaponCamera.add(weaponHolder);\n\n                function syncWeaponViewportForOrientation() {\n                    weaponCamera.aspect = camera.aspect;\n                    weaponCamera.fov = getBaseWeaponFov();\n                    weaponCamera.updateProjectionMatrix();\n                    weaponHolder.scale.setScalar(1);\n                    weaponHolder.position.set(0, 0, 0);\n                }\n\n                const katanaTrail = new THREE.Line(\n                    new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]),\n                    new THREE.LineBasicMaterial({ color: 0xdff7ff, transparent: true, opacity: 0, linewidth: 2 })\n                );\n                katanaTrail.frustumCulled = false;\n                weaponCamera.add(katanaTrail);\n\n                function mergeStaticMeshesByMaterial(parent, meshes, namePrefix) {\n                    const byMaterial = new Map();\n                    for (let i = 0; i < meshes.length; i++) {\n                        const mesh = meshes[i];\n                        if (!mesh || !mesh.geometry || Array.isArray(mesh.material)) continue;\n                        let list = byMaterial.get(mesh.material);\n                        if (!list) {\n                            list = [];\n                            byMaterial.set(mesh.material, list);\n                        }\n                        list.push(mesh);\n                    }\n\n                    let batchIndex = 0;\n                    for (const [material, list] of byMaterial) {\n                        if (list.length < 2) continue;\n                        const prepared = [];\n                        for (let i = 0; i < list.length; i++) {\n                            const mesh = list[i];\n                            mesh.updateMatrix();\n                            const geometry = mesh.geometry.index ? mesh.geometry.toNonIndexed() : mesh.geometry.clone();\n                            geometry.applyMatrix4(mesh.matrix);\n                            prepared.push(geometry);\n                        }\n                        const firstAttributes = Object.keys(prepared[0].attributes);\n                        const mergedGeometry = new THREE.BufferGeometry();\n                        for (let attrIndex = 0; attrIndex < firstAttributes.length; attrIndex++) {\n                            const attributeName = firstAttributes[attrIndex];\n                            if (!prepared.every(geometry => geometry.attributes[attributeName])) continue;\n                            const itemSize = prepared[0].attributes[attributeName].itemSize;\n                            let valueCount = 0;\n                            for (let i = 0; i < prepared.length; i++) valueCount += prepared[i].attributes[attributeName].array.length;\n                            const mergedValues = new Float32Array(valueCount);\n                            let writeOffset = 0;\n                            for (let i = 0; i < prepared.length; i++) {\n                                const values = prepared[i].attributes[attributeName].array;\n                                mergedValues.set(values, writeOffset);\n                                writeOffset += values.length;\n                            }\n                            mergedGeometry.setAttribute(attributeName, new THREE.BufferAttribute(mergedValues, itemSize));\n                        }\n                        mergedGeometry.computeBoundingBox();\n                        mergedGeometry.computeBoundingSphere();\n                        const mergedMesh = new THREE.Mesh(mergedGeometry, material);\n                        mergedMesh.name = `${namePrefix}-${batchIndex++}`;\n                        parent.add(mergedMesh);\n                        for (let i = 0; i < list.length; i++) parent.remove(list[i]);\n                    }\n                }\n\n                                function buildPistol3D() {\n                    const group = new THREE.Group();\n                    \n                    // Shared lightweight Phong/Basic materials to minimize state changes and avoid PBR fragment overhead\n                    const metalMat = new THREE.MeshPhongMaterial({ color: 0x1d1e22, specular: 0x333335, shininess: 24 });\n                    const gripMat = new THREE.MeshPhongMaterial({ color: 0x0f1011, specular: 0x111111, shininess: 8 });\n                    const detailMat = new THREE.MeshPhongMaterial({ color: 0x757a82, specular: 0x555a60, shininess: 36 });\n                    const smartFrameMat = new THREE.MeshPhongMaterial({ color: 0x111315, specular: 0x2d3138, shininess: 20 });\n                    const smartArmorMat = new THREE.MeshPhongMaterial({ color: 0xbfc2c8, specular: 0x666b73, shininess: 32 }); // White-silver sci-fi shroud\n                    const smartOrangeMat = new THREE.MeshPhongMaterial({ color: 0xff6600, specular: 0xaa4400, shininess: 20, emissive: 0xff3b00, emissiveIntensity: 1.0 });\n                    const smartLensMat = new THREE.MeshPhongMaterial({ color: 0x050b10, specular: 0x00f3ff, shininess: 120 });\n                    const smartDarkMat = new THREE.MeshPhongMaterial({ color: 0x090b0d, specular: 0x111111, shininess: 10 });\n                    const smartTanMat = new THREE.MeshPhongMaterial({ color: 0x9ea1a8, specular: 0x50555d, shininess: 24 });\n                    const smartGlowMat = new THREE.MeshBasicMaterial({ color: 0x00f3ff, transparent: true, opacity: 0.85 });\n                    const smartHotGlowMat = new THREE.MeshBasicMaterial({ color: 0xff3311, transparent: true, opacity: 0.88 });\n\n                    const smartAnimatedParts = [];\n                    function markSmartAnimated(part, deployOffset, deployRot, deployScale = 0.35) {\n                        part.userData.smartAnimated = true;\n                        part.userData.smartBasePos = part.position.clone();\n                        part.userData.smartBaseRot = part.rotation.clone();\n                        part.userData.smartBaseScale = part.scale.clone();\n                        part.userData.smartDeployOffset = deployOffset || new THREE.Vector3();\n                        part.userData.smartDeployRot = deployRot || new THREE.Euler();\n                        part.userData.smartDeployScale = deployScale;\n                        smartAnimatedParts.push(part);\n                        return part;\n                    }\n\n                    // Slide - Faceted and layered for a premium angular look\n                    const slide = new THREE.Mesh(new THREE.BoxGeometry(0.046, 0.038, 0.24), metalMat);\n                    slide.name = 'pistolSlide';\n                    slide.position.set(0, 0.034, -0.035);\n                    group.add(slide);\n\n                    // Slide bevel to create an angular top profile\n                    const slideBevel = new THREE.Mesh(new THREE.BoxGeometry(0.034, 0.010, 0.238), detailMat);\n                    slideBevel.position.set(0, 0.050, -0.035);\n                    group.add(slideBevel);\n\n                    const slideNose = new THREE.Mesh(new THREE.BoxGeometry(0.042, 0.032, 0.046), detailMat);\n                    slideNose.position.set(0, 0.031, -0.168);\n                    slideNose.rotation.x = -0.16;\n                    group.add(slideNose);\n\n                    const rearBlock = new THREE.Mesh(new THREE.BoxGeometry(0.050, 0.052, 0.056), detailMat);\n                    rearBlock.position.set(0, 0.026, 0.088);\n                    group.add(rearBlock);\n\n                    // Barrel and muzzle\n                    const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.009, 0.010, 0.10, 12), metalMat);\n                    barrel.name = 'pistolBarrel';\n                    barrel.rotation.x = Math.PI / 2;\n                    barrel.position.set(0, 0.025, -0.14);\n                    group.add(barrel);\n\n                    const muzzle = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.012, 0.022, 12), detailMat);\n                    muzzle.name = 'pistolMuzzle';\n                    muzzle.rotation.x = Math.PI / 2;\n                    muzzle.position.set(0, 0.025, -0.198);\n                    group.add(muzzle);\n\n                    // Frame\n                    const frame = new THREE.Mesh(new THREE.BoxGeometry(0.042, 0.036, 0.164), detailMat);\n                    frame.position.set(0, -0.006, -0.006);\n                    group.add(frame);\n\n                    const underRail = new THREE.Mesh(new THREE.BoxGeometry(0.038, 0.009, 0.140), smartDarkMat);\n                    underRail.position.set(0, -0.031, -0.05);\n                    group.add(underRail);\n\n                    // Trigger guard\n                    const guard = new THREE.Mesh(new THREE.BoxGeometry(0.034, 0.014, 0.062), metalMat);\n                    guard.position.set(0, -0.042, -0.012);\n                    group.add(guard);\n\n                    // Trigger\n                    const trigger = new THREE.Mesh(new THREE.BoxGeometry(0.008, 0.026, 0.009), smartOrangeMat); // Glowing trigger!\n                    trigger.name = 'pistolTrigger';\n                    trigger.position.set(0, -0.031, 0.002);\n                    group.add(trigger);\n\n                    // Grip\n                    const grip = new THREE.Mesh(new THREE.BoxGeometry(0.038, 0.090, 0.044), gripMat);\n                    grip.position.set(0, -0.060, 0.042);\n                    grip.rotation.x = 0.24;\n                    group.add(grip);\n\n                    // Side panels with embedded neon design accents\n                    const gripPanelL = new THREE.Mesh(new THREE.BoxGeometry(0.004, 0.062, 0.030), smartDarkMat);\n                    gripPanelL.position.set(-0.021, -0.060, 0.041);\n                    gripPanelL.rotation.x = 0.24;\n                    group.add(gripPanelL);\n                    \n                    const gripPanelR = new THREE.Mesh(new THREE.BoxGeometry(0.004, 0.062, 0.030), smartDarkMat);\n                    gripPanelR.position.set(0.021, -0.060, 0.041);\n                    gripPanelR.rotation.x = 0.24;\n                    group.add(gripPanelR);\n\n                    // Glowing grip stripes for visual depth\n                    const gripGlowL = new THREE.Mesh(new THREE.BoxGeometry(0.001, 0.048, 0.005), smartOrangeMat);\n                    gripGlowL.position.set(-0.024, -0.060, 0.041);\n                    gripGlowL.rotation.x = 0.24;\n                    group.add(gripGlowL);\n\n                    const gripGlowR = new THREE.Mesh(new THREE.BoxGeometry(0.001, 0.048, 0.005), smartOrangeMat);\n                    gripGlowR.position.set(0.024, -0.060, 0.041);\n                    gripGlowR.rotation.x = 0.24;\n                    group.add(gripGlowR);\n\n                    // Magazine base\n                    const magBase = new THREE.Mesh(new THREE.BoxGeometry(0.036, 0.014, 0.038), metalMat);\n                    magBase.position.set(0, -0.110, 0.044);\n                    group.add(magBase);\n\n                    // Sights\n                    const rearSight = new THREE.Mesh(new THREE.BoxGeometry(0.030, 0.010, 0.008), metalMat);\n                    rearSight.position.set(0, 0.056, 0.070);\n                    group.add(rearSight);\n\n                    const frontSight = new THREE.Mesh(new THREE.BoxGeometry(0.008, 0.012, 0.006), metalMat);\n                    frontSight.position.set(0, 0.052, -0.130);\n                    group.add(frontSight);\n\n                    // These pieces never animate independently. Baking their local transforms and\n                    // batching by material preserves the silhouette while cutting viewmodel draws.\n                    mergeStaticMeshesByMaterial(group, [\n                        slideBevel, slideNose, rearBlock, frame, underRail, guard,\n                        grip, gripPanelL, gripPanelR, gripGlowL, gripGlowR,\n                        magBase, rearSight, frontSight\n                    ], 'pistol-static');\n\n                    // --- Smart Kit Plating (Visual upgrade group) ---\n                    const smartKit = new THREE.Group();\n                    smartKit.name = 'smartPistolKit';\n                    smartKit.visible = false;\n\n                    // Futuristic top armor plate\n                    const topShroud = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.058, 0.020, 0.190), smartArmorMat), new THREE.Vector3(0, -0.020, 0.052), new THREE.Euler(-0.18, 0, 0), 0.42);\n                    topShroud.position.set(0, 0.064, -0.046);\n                    smartKit.add(topShroud);\n\n                    const topSpine = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.024, 0.011, 0.204), smartDarkMat), new THREE.Vector3(0, -0.012, 0.042), new THREE.Euler(-0.12, 0, 0), 0.35);\n                    topSpine.position.set(0, 0.078, -0.042);\n                    smartKit.add(topSpine);\n\n                    const topStripe = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.038, 0.004, 0.090), smartOrangeMat), new THREE.Vector3(0, -0.012, 0.032), new THREE.Euler(-0.12, 0, 0), 0.3);\n                    topStripe.position.set(0, 0.090, -0.054);\n                    smartKit.add(topStripe);\n\n                    const noseArmor = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.054, 0.038, 0.048), smartTanMat), new THREE.Vector3(0, -0.015, 0.056), new THREE.Euler(-0.22, 0, 0), 0.36);\n                    noseArmor.position.set(0, 0.034, -0.162);\n                    noseArmor.rotation.x = -0.08;\n                    smartKit.add(noseArmor);\n\n                    const frontSensorBlock = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.042, 0.028, 0.042), smartFrameMat), new THREE.Vector3(0, -0.020, 0.038), new THREE.Euler(0.18, 0, 0), 0.32);\n                    frontSensorBlock.position.set(0, 0.014, -0.178);\n                    smartKit.add(frontSensorBlock);\n\n                    // Targeting lens (Glass + glow elements)\n                    const sensorLens = markSmartAnimated(new THREE.Mesh(new THREE.CylinderGeometry(0.011, 0.011, 0.010, 16), smartLensMat), new THREE.Vector3(0, -0.011, 0.029), new THREE.Euler(0.16, 0, 0), 0.25);\n                    sensorLens.rotation.x = Math.PI / 2;\n                    sensorLens.position.set(0, 0.014, -0.205);\n                    smartKit.add(sensorLens);\n\n                    const lensGlow = markSmartAnimated(new THREE.Mesh(new THREE.CylinderGeometry(0.013, 0.013, 0.003, 16), smartGlowMat.clone()), new THREE.Vector3(0, -0.011, 0.028), new THREE.Euler(0.16, 0, 0), 0.2);\n                    lensGlow.rotation.x = Math.PI / 2;\n                    lensGlow.position.set(0, 0.014, -0.210);\n                    lensGlow.name = 'smartPistolGlow';\n                    smartKit.add(lensGlow);\n\n                    // Layered side rails\n                    const leftRail = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.009, 0.019, 0.164), smartFrameMat), new THREE.Vector3(0.020, -0.010, 0.036), new THREE.Euler(0, 0.1, 0), 0.3);\n                    leftRail.position.set(-0.033, 0.009, -0.042);\n                    smartKit.add(leftRail);\n                    \n                    const rightRail = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.009, 0.019, 0.164), smartFrameMat), new THREE.Vector3(-0.020, -0.010, 0.036), new THREE.Euler(0, -0.1, 0), 0.3);\n                    rightRail.position.set(0.033, 0.009, -0.042);\n                    smartKit.add(rightRail);\n\n                    const sideModuleL = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.012, 0.046, 0.072), smartArmorMat), new THREE.Vector3(0.026, -0.003, 0.021), new THREE.Euler(0, 0.16, 0), 0.34);\n                    sideModuleL.position.set(-0.041, 0.025, -0.061);\n                    smartKit.add(sideModuleL);\n                    \n                    const sideModuleR = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.012, 0.046, 0.072), smartArmorMat), new THREE.Vector3(-0.026, -0.003, 0.021), new THREE.Euler(0, -0.16, 0), 0.34);\n                    sideModuleR.position.set(0.041, 0.025, -0.061);\n                    smartKit.add(sideModuleR);\n\n                    // Holographic HUD/counter panels\n                    const counterPanelL = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.003, 0.022, 0.065), smartGlowMat.clone()), new THREE.Vector3(0.018, -0.003, 0.016), new THREE.Euler(0, 0.12, 0), 0.25);\n                    counterPanelL.position.set(-0.048, 0.034, -0.011);\n                    counterPanelL.name = 'smartPistolCounter';\n                    smartKit.add(counterPanelL);\n                    \n                    const counterPanelR = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.003, 0.022, 0.065), smartGlowMat.clone()), new THREE.Vector3(-0.018, -0.003, 0.016), new THREE.Euler(0, -0.12, 0), 0.25);\n                    counterPanelR.position.set(0.048, 0.034, -0.011);\n                    counterPanelR.name = 'smartPistolCounter';\n                    smartKit.add(counterPanelR);\n\n                    // Back module\n                    const rearChip = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.038, 0.018, 0.026), smartFrameMat), new THREE.Vector3(0, -0.015, 0.024), new THREE.Euler(-0.15, 0, 0), 0.3);\n                    rearChip.position.set(0, 0.072, 0.064);\n                    smartKit.add(rearChip);\n\n                    const rearGlow = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.026, 0.003, 0.016), smartHotGlowMat), new THREE.Vector3(0, -0.010, 0.016), new THREE.Euler(-0.1, 0, 0), 0.25);\n                    rearGlow.position.set(0, 0.082, 0.064);\n                    rearGlow.name = 'smartPistolGlow';\n                    smartKit.add(rearGlow);\n\n                    // Side cooling vents\n                    for (let i = 0; i < 4; i++) {\n                        const ventL = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.004, 0.018, 0.006), smartDarkMat), new THREE.Vector3(0.016, -0.005, 0.014), new THREE.Euler(0, 0.1, 0), 0.3);\n                        ventL.position.set(-0.027, 0.052, 0.038 - i * 0.021);\n                        smartKit.add(ventL);\n                        \n                        const ventR = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.004, 0.018, 0.006), smartDarkMat), new THREE.Vector3(-0.016, -0.005, 0.014), new THREE.Euler(0, -0.1, 0), 0.3);\n                        ventR.position.set(0.027, 0.052, 0.038 - i * 0.021);\n                        smartKit.add(ventR);\n                    }\n\n                    // Bottom components\n                    const underScanner = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.036, 0.016, 0.090), smartFrameMat), new THREE.Vector3(0, 0.015, 0.028), new THREE.Euler(0.2, 0, 0), 0.32);\n                    underScanner.name = 'smartPistolScanner';\n                    underScanner.position.set(0, -0.038, -0.090);\n                    smartKit.add(underScanner);\n\n                    const underLaser = markSmartAnimated(new THREE.Mesh(new THREE.CylinderGeometry(0.010, 0.010, 0.095, 12), smartDarkMat), new THREE.Vector3(0, 0.013, 0.027), new THREE.Euler(0.2, 0, 0), 0.28);\n                    underLaser.rotation.x = Math.PI / 2;\n                    underLaser.position.set(0, -0.050, -0.108);\n                    smartKit.add(underLaser);\n\n                    const underGlow = markSmartAnimated(new THREE.Mesh(new THREE.BoxGeometry(0.024, 0.003, 0.058), smartGlowMat.clone()), new THREE.Vector3(0, 0.011, 0.022), new THREE.Euler(0.16, 0, 0), 0.22);\n                    underGlow.position.set(0, -0.050, -0.101);\n                    underGlow.name = 'smartPistolGlow';\n                    smartKit.add(underGlow);\n\n                    group.add(smartKit);\n                    group.userData.smartKit = smartKit;\n                    group.userData.smartDeploy = 0;\n                    group.userData.smartAnimatedParts = smartAnimatedParts;\n                    group.userData.smartSlide = slide;\n                    group.userData.smartBarrel = barrel;\n                    group.userData.smartMuzzle = muzzle;\n                    group.userData.smartTrigger = trigger;\n                    group.userData.smartScanner = underScanner;\n                    group.userData.smartGlowParts = [];\n                    group.userData.smartCounterParts = [];\n                    \n                    smartKit.traverse(child => {\n                        if (child.name === 'smartPistolGlow') group.userData.smartGlowParts.push(child);\n                        if (child.name === 'smartPistolCounter') group.userData.smartCounterParts.push(child);\n                    });\n                    \n                    for (let i = 0; i < smartAnimatedParts.length; i++) {\n                        const part = smartAnimatedParts[i];\n                        part.userData.smartBasePos = part.position.clone();\n                        part.userData.smartBaseRot = part.rotation.clone();\n                        part.userData.smartBaseScale = part.scale.clone();\n                    }\n\n                    return group;\n                }\n\n                // --- Build 3D Shotgun ---\n                function buildShotgun3D() {\n                    const group = new THREE.Group();\n                    const metalMat = new THREE.MeshPhongMaterial({ color: 0x202022, specular: 0x333335, shininess: 25 });\n                    const woodMat = new THREE.MeshPhongMaterial({ color: 0x422614, specular: 0x22110a, shininess: 15 });\n                    const pumpMat = new THREE.MeshPhongMaterial({ color: 0x151515, specular: 0x111111, shininess: 10 });\n\n                    // Main barrel\n                    const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.018, 0.018, 0.55, 8), metalMat);\n                    barrel.rotation.x = Math.PI / 2;\n                    barrel.position.set(0, 0.02, -0.15);\n                    group.add(barrel);\n\n                    // Second barrel (under barrel tube magazine)\n                    const magTube = new THREE.Mesh(new THREE.CylinderGeometry(0.014, 0.014, 0.4, 8), metalMat);\n                    magTube.rotation.x = Math.PI / 2;\n                    magTube.position.set(0, -0.01, -0.1);\n                    group.add(magTube);\n\n                    // Receiver\n                    const receiver = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.06, 0.14), metalMat);\n                    receiver.position.set(0, 0.01, 0.12);\n                    group.add(receiver);\n\n                    // Pump/Forend\n                    const pump = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.05, 0.1), pumpMat);\n                    pump.position.set(0, 0.0, -0.08);\n                    group.add(pump);\n\n                    // Stock\n                    const stock = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.06, 0.2), woodMat);\n                    stock.position.set(0, -0.005, 0.28);\n                    stock.rotation.x = -0.05;\n                    group.add(stock);\n\n                    // Stock buttpad\n                    const buttpad = new THREE.Mesh(new THREE.BoxGeometry(0.042, 0.065, 0.015), pumpMat);\n                    buttpad.position.set(0, -0.008, 0.385);\n                    group.add(buttpad);\n\n                    // Trigger guard\n                    const guard = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.012, 0.06), metalMat);\n                    guard.position.set(0, -0.03, 0.1);\n                    group.add(guard);\n\n                    // Trigger\n                    const trigger = new THREE.Mesh(new THREE.BoxGeometry(0.008, 0.02, 0.008), metalMat);\n                    trigger.position.set(0, -0.02, 0.1);\n                    group.add(trigger);\n\n                    // Barrel tip\n                    const tip = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.018, 0.02, 8), metalMat);\n                    tip.rotation.x = Math.PI / 2;\n                    tip.position.set(0, 0.02, -0.43);\n                    group.add(tip);\n\n                    return group;\n                }\n\n                // --- Build 3D Sniper Rifle ---\n                function buildSniper3D() {\n                    const group = new THREE.Group();\n                    const metalMat = new THREE.MeshPhongMaterial({ color: 0x1e1e20, specular: 0x2a2a30, shininess: 25 });\n                    const bodyMat = new THREE.MeshPhongMaterial({ color: 0x242426, specular: 0x222222, shininess: 15 });\n                    const scopeMat = new THREE.MeshPhongMaterial({ color: 0x18181a, specular: 0x333333, shininess: 40 });\n                    const stockMat = new THREE.MeshPhongMaterial({ color: 0x1c1c1e, specular: 0x111111, shininess: 10 });\n                    const lensMat = new THREE.MeshPhongMaterial({ color: 0x1a2a44, specular: 0x66aaff, shininess: 300 });\n\n                    // Long barrel\n                    const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.014, 0.7, 8), metalMat);\n                    barrel.rotation.x = Math.PI / 2;\n                    barrel.position.set(0, 0.015, -0.2);\n                    group.add(barrel);\n\n                    // Muzzle brake\n                    const muzzle = new THREE.Mesh(new THREE.CylinderGeometry(0.018, 0.014, 0.04, 8), metalMat);\n                    muzzle.rotation.x = Math.PI / 2;\n                    muzzle.position.set(0, 0.015, -0.56);\n                    group.add(muzzle);\n\n                    // Receiver body\n                    const body = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.055, 0.22), bodyMat);\n                    body.position.set(0, 0.01, 0.12);\n                    group.add(body);\n\n                    // Scope mount rail\n                    const rail = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.01, 0.15), metalMat);\n                    rail.position.set(0, 0.045, 0.08);\n                    group.add(rail);\n\n                    // Scope body\n                    const scopeBody = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.18, 12), scopeMat);\n                    scopeBody.rotation.x = Math.PI / 2;\n                    scopeBody.position.set(0, 0.07, 0.06);\n                    group.add(scopeBody);\n\n                    // Scope front lens\n                    const scopeFront = new THREE.Mesh(new THREE.CylinderGeometry(0.022, 0.02, 0.015, 12), lensMat);\n                    scopeFront.rotation.x = Math.PI / 2;\n                    scopeFront.position.set(0, 0.07, -0.03);\n                    group.add(scopeFront);\n\n                    // Scope rear lens\n                    const scopeRear = new THREE.Mesh(new THREE.CylinderGeometry(0.018, 0.02, 0.015, 12), scopeMat);\n                    scopeRear.rotation.x = Math.PI / 2;\n                    scopeRear.position.set(0, 0.07, 0.15);\n                    group.add(scopeRear);\n\n                    // Magazine\n                    const mag = new THREE.Mesh(new THREE.BoxGeometry(0.025, 0.08, 0.06), bodyMat);\n                    mag.position.set(0, -0.04, 0.08);\n                    group.add(mag);\n\n                    // Trigger guard\n                    const guard = new THREE.Mesh(new THREE.BoxGeometry(0.025, 0.012, 0.055), metalMat);\n                    guard.position.set(0, -0.03, 0.15);\n                    group.add(guard);\n\n                    // Trigger\n                    const trigger = new THREE.Mesh(new THREE.BoxGeometry(0.008, 0.02, 0.008), metalMat);\n                    trigger.position.set(0, -0.02, 0.15);\n                    group.add(trigger);\n\n                    // Stock\n                    const stock = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.055, 0.22), stockMat);\n                    stock.position.set(0, 0.0, 0.33);\n                    stock.rotation.x = -0.03;\n                    group.add(stock);\n\n                    // Cheek rest\n                    const cheek = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.02, 0.1), stockMat);\n                    cheek.position.set(0, 0.035, 0.3);\n                    group.add(cheek);\n\n                    // Bipod legs (folded)\n                    const leg1 = new THREE.Mesh(new THREE.CylinderGeometry(0.005, 0.005, 0.1, 6), stockMat);\n                    leg1.position.set(-0.025, -0.02, -0.1);\n                    leg1.rotation.x = 0.3;\n                    group.add(leg1);\n                    const leg2 = new THREE.Mesh(new THREE.CylinderGeometry(0.005, 0.005, 0.1, 6), stockMat);\n                    leg2.position.set(0.025, -0.02, -0.1);\n                    leg2.rotation.x = 0.3;\n                    group.add(leg2);\n\n                    return group;\n                }\n\n                // --- 3D Muzzle Flash (texture-based billboard sprite) ---\n                function configureMuzzleFlashTexture(texture) {\n                    texture.needsUpdate = true;\n                    texture.wrapS = THREE.RepeatWrapping;\n                    texture.wrapT = THREE.RepeatWrapping;\n                    texture.repeat.set(1 / MUZZLE_FLASH_ATLAS_COLS, 1 / MUZZLE_FLASH_ATLAS_ROWS);\n                    texture.offset.set(0, 1 - 1 / MUZZLE_FLASH_ATLAS_ROWS);\n                    return texture;\n                }\n\n                function createMuzzleFlashTextureFromCache() {\n                    const flashImg = imageCache[MUZZLE_FLASH_ATLAS_ID];\n                    return flashImg ? configureMuzzleFlashTexture(new THREE.Texture(flashImg)) : null;\n                }\n\n                const muzzleFlashMaterials = [];\n\n                function initializeMuzzleFlashMaterials(baseTexture) {\n                    if (!baseTexture || muzzleFlashMaterials.length > 0) return;\n                    for (let i = 0; i < MUZZLE_FLASH_ATLAS_FRAMES; i++) {\n                        const tex = baseTexture.clone();\n                        const col = i % MUZZLE_FLASH_ATLAS_COLS;\n                        const row = Math.floor(i / MUZZLE_FLASH_ATLAS_COLS);\n                        tex.offset.x = col / MUZZLE_FLASH_ATLAS_COLS;\n                        tex.offset.y = 1 - (row + 1) / MUZZLE_FLASH_ATLAS_ROWS;\n                        tex.needsUpdate = true;\n\n                        const mat = new THREE.SpriteMaterial({\n                            map: tex,\n                            color: 0xffffff,\n                            transparent: true,\n                            opacity: 0,\n                            blending: THREE.AdditiveBlending,\n                            depthWrite: false\n                        });\n                        muzzleFlashMaterials.push(mat);\n                    }\n                }\n\n                function buildMuzzleFlash3D() {\n                    const group = new THREE.Group();\n                    const flashTexture = createMuzzleFlashTextureFromCache();\n                    const isAtlas = !!flashTexture;\n                    if (isAtlas) {\n                        initializeMuzzleFlashMaterials(flashTexture);\n                    }\n                    const flashMat = muzzleFlashMaterials[0] || new THREE.SpriteMaterial({\n                        map: flashTexture,\n                        color: 0xffffff,\n                        transparent: true,\n                        opacity: 0,\n                        blending: THREE.AdditiveBlending,\n                        depthWrite: false\n                    });\n                    const sprite = new THREE.Sprite(flashMat);\n                    sprite.scale.set(0.18, 0.18, 1);\n                    group.add(sprite);\n                    group.userData.sprite = sprite;\n                    group.userData.flashMat = flashMat;\n                    group.userData.flashTexture = flashTexture;\n                    group.userData.isAtlas = isAtlas;\n                    group.userData.frameIndex = 0;\n                    group.userData.frameClock = 0;\n                    group.userData.intensity = 0;\n                    group.userData.baseScale = 1;\n                    group.visible = false;\n                    return group;\n                }\n\n                // --- Muzzle Flash Dynamic Light ---\n                const muzzleFlashLight = new THREE.PointLight(0xffaa33, 0, 8);\n                muzzleFlashLight.decay = 2;\n                scene.add(muzzleFlashLight);\n                let muzzleFlashLightTimer = 0;\n                let muzzleFlashLightPeak = 0;\n\n                // --- Flamethrower Fire Dynamic Lights ---\n                const flamethrowerMuzzleLight = new THREE.PointLight(0xff4400, 0, 10);\n                flamethrowerMuzzleLight.decay = 1.8;\n                scene.add(flamethrowerMuzzleLight);\n\n                const flamethrowerLight = new THREE.PointLight(0xffaa22, 0, 18);\n                flamethrowerLight.decay = 1.2;\n                scene.add(flamethrowerLight);\n\n                // --- Build 3D Katana ---\n                function buildKatana3D() {\n                    const group = new THREE.Group();\n                    const bladeMat = new THREE.MeshPhongMaterial({ color: 0xb0b5bc, specular: 0xeeeeee, shininess: 180, flatShading: true });\n                    const edgeMat = new THREE.MeshPhongMaterial({ color: 0xd0d5d8, specular: 0xffffff, shininess: 250, flatShading: true });\n                    const handleMat = new THREE.MeshPhongMaterial({ color: 0x120806, specular: 0x110a08, shininess: 12 });\n                    const wrapMat = new THREE.MeshPhongMaterial({ color: 0x1e0e0a, specular: 0x0a0604, shininess: 8 });\n                    const guardMat = new THREE.MeshPhongMaterial({ color: 0x4a4a4a, specular: 0x333333, shininess: 40 });\n\n                    function makeBladeGeometry(edgeBandOnly) {\n                        const sections = 12;\n                        const len = 0.68;\n                        const verts = [];\n                        const idx = [];\n                        const add = (x, y, z) => { verts.push(x, y, z); return verts.length / 3 - 1; };\n                        const rings = [];\n\n                        for (let i = 0; i <= sections; i++) {\n                            const t = i / sections;\n                            const z = -0.025 - len * t;\n                            const sori = -0.010 * Math.sin(t * Math.PI);\n                            const width = 0.034 * (1 - t) + 0.010 * t;\n                            const thickness = 0.006 * (1 - t) + 0.002 * t;\n                            const spineY = sori + width * 0.50;\n                            const edgeY = sori - width * 0.50;\n                            const edgeShoulderY = edgeY + width * 0.28;\n\n                            if (edgeBandOnly) {\n                                rings.push([\n                                    add(-thickness * 0.38, edgeY, z),\n                                    add(thickness * 0.38, edgeY, z),\n                                    add(-thickness * 0.18, edgeShoulderY, z),\n                                    add(thickness * 0.18, edgeShoulderY, z)\n                                ]);\n                            } else {\n                                rings.push([\n                                    add(-thickness, spineY, z),\n                                    add(thickness, spineY, z),\n                                    add(-thickness * 0.35, edgeShoulderY, z),\n                                    add(thickness * 0.35, edgeShoulderY, z)\n                                ]);\n                            }\n                        }\n\n                        for (let i = 0; i < sections; i++) {\n                            const a = rings[i], b = rings[i + 1];\n                            idx.push(a[0], b[0], a[2], a[2], b[0], b[2]);\n                            idx.push(a[1], a[3], b[1], a[3], b[3], b[1]);\n                            idx.push(a[0], a[1], b[0], a[1], b[1], b[0]);\n                            idx.push(a[2], b[2], a[3], a[3], b[2], b[3]);\n                        }\n\n                        const tip = add(0, 0.004, -0.735);\n                        const last = rings[sections];\n                        idx.push(last[0], tip, last[2], last[1], last[3], tip, last[0], last[1], tip, last[2], tip, last[3]);\n\n                        const geo = new THREE.BufferGeometry();\n                        geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));\n                        geo.setIndex(idx);\n                        geo.computeVertexNormals();\n                        return geo;\n                    }\n\n                    const blade = new THREE.Mesh(makeBladeGeometry(false), bladeMat);\n                    group.add(blade);\n\n                    const edge = new THREE.Mesh(makeBladeGeometry(true), edgeMat);\n                    group.add(edge);\n\n                    const hamonPoints = [];\n                    for (let i = 0; i <= 18; i++) {\n                        const t = i / 18;\n                        const z = -0.05 - 0.61 * t;\n                        const sori = -0.010 * Math.sin(t * Math.PI);\n                        const width = 0.034 * (1 - t) + 0.010 * t;\n                        const wave = Math.sin(t * Math.PI * 8) * 0.0018;\n                        hamonPoints.push(new THREE.Vector3(0.0042, sori - width * 0.22 + wave, z));\n                    }\n                    const hamon = new THREE.Line(\n                        new THREE.BufferGeometry().setFromPoints(hamonPoints),\n                        new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.55 })\n                    );\n                    group.add(hamon);\n\n                    const spinePoints = [];\n                    for (let i = 0; i <= 12; i++) {\n                        const t = i / 12;\n                        const z = -0.035 - 0.64 * t;\n                        const sori = -0.010 * Math.sin(t * Math.PI);\n                        const width = 0.034 * (1 - t) + 0.010 * t;\n                        spinePoints.push(new THREE.Vector3(-0.0044, sori + width * 0.50, z));\n                    }\n                    const spine = new THREE.Line(\n                        new THREE.BufferGeometry().setFromPoints(spinePoints),\n                        new THREE.LineBasicMaterial({ color: 0x6f7478, transparent: true, opacity: 0.65 })\n                    );\n                    group.add(spine);\n\n                    const kissaki = new THREE.Line(\n                        new THREE.BufferGeometry().setFromPoints([\n                            new THREE.Vector3(0.0045, -0.010, -0.645),\n                            new THREE.Vector3(0.0045, -0.002, -0.695),\n                            new THREE.Vector3(0.0045, 0.004, -0.735)\n                        ]),\n                        new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 })\n                    );\n                    group.add(kissaki);\n\n                    // Guard (tsuba)\n                    const guard = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.08, 0.008), guardMat);\n                    guard.position.set(0, 0, -0.02);\n                    group.add(guard);\n\n                    // Handle (tsuka)\n                    const handle = new THREE.Mesh(new THREE.CylinderGeometry(0.014, 0.012, 0.22, 8), handleMat);\n                    handle.rotation.x = Math.PI / 2;\n                    handle.position.set(0, 0, 0.10);\n                    group.add(handle);\n\n                    // Handle wrapping\n                    for (let i = 0; i < 8; i++) {\n                        const wrap = new THREE.Mesh(new THREE.BoxGeometry(0.024, 0.024, 0.012), wrapMat);\n                        wrap.position.set(0, 0, 0.02 + i * 0.022);\n                        wrap.rotation.z = i % 2 === 0 ? 0.2 : -0.2;\n                        group.add(wrap);\n                    }\n\n                    // Pommel (kashira)\n                    const pommel = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.022, 0.016), guardMat);\n                    pommel.position.set(0, 0, 0.22);\n                    group.add(pommel);\n\n                    // Put the model origin in the player's grip so swings rotate like a two-handed blade.\n                    const gripPivotZ = 0.12;\n                    group.children.forEach(child => { child.position.z -= gripPivotZ; });\n\n                    return group;\n                }\n\n                // --- Build 3D Gatling Gun ---\n                function buildGatling3D() {\n                    const group = new THREE.Group();\n                    const metalMat = new THREE.MeshPhongMaterial({ color: 0x222224, specular: 0x333333, shininess: 30 });\n                    const barrelMat = new THREE.MeshPhongMaterial({ color: 0x2a2a2c, specular: 0x44444a, shininess: 40 });\n                    const gripMat = new THREE.MeshPhongMaterial({ color: 0x1a1a1c, specular: 0x111111, shininess: 10 });\n\n                    // Barrel cluster (6 barrels)\n                    const barrelGroup = new THREE.Group();\n                    barrelGroup.name = 'barrelCluster';\n                    for (let i = 0; i < 6; i++) {\n                        const angle = (i / 6) * Math.PI * 2;\n                        const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.45, 6), barrelMat);\n                        barrel.rotation.x = Math.PI / 2;\n                        barrel.position.set(Math.cos(angle) * 0.035, Math.sin(angle) * 0.035, -0.15);\n                        barrelGroup.add(barrel);\n                    }\n                    // Central hub\n                    const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.08, 12), metalMat);\n                    hub.rotation.x = Math.PI / 2;\n                    hub.position.set(0, 0, 0.05);\n                    barrelGroup.add(hub);\n                    // Front ring\n                    const frontRing = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.055, 0.02, 12), metalMat);\n                    frontRing.rotation.x = Math.PI / 2;\n                    frontRing.position.set(0, 0, -0.37);\n                    barrelGroup.add(frontRing);\n                    group.add(barrelGroup);\n\n                    // Body/receiver\n                    const body = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.09, 0.18), metalMat);\n                    body.position.set(0, 0, 0.15);\n                    group.add(body);\n\n                    // Ammo drum\n                    const drum = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.1, 12), gripMat);\n                    drum.position.set(0, -0.08, 0.15);\n                    group.add(drum);\n\n                    // Handle\n                    const handleVert = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.1, 0.03), gripMat);\n                    handleVert.position.set(0, -0.05, 0.28);\n                    group.add(handleVert);\n\n                    // Rear grip\n                    const rearGrip = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.08, 0.04), gripMat);\n                    rearGrip.position.set(0, -0.06, 0.32);\n                    rearGrip.rotation.x = 0.3;\n                    group.add(rearGrip);\n\n                    return group;\n                }\n\n                // --- Build 3D LMG ---\n                function buildLMG3D() {\n                    const group = new THREE.Group();\n                    const metalMat = new THREE.MeshPhongMaterial({ color: 0x202022, specular: 0x333338, shininess: 25 });\n                    const bodyMat = new THREE.MeshPhongMaterial({ color: 0x252528, specular: 0x222222, shininess: 15 });\n                    const stockMat = new THREE.MeshPhongMaterial({ color: 0x18181a, specular: 0x111111, shininess: 10 });\n                    const ammoBoxMat = new THREE.MeshPhongMaterial({ color: 0x3a4a2a, specular: 0x111111, shininess: 5 });\n\n                    // Barrel\n                    const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.014, 0.014, 0.45, 8), metalMat);\n                    barrel.rotation.x = Math.PI / 2;\n                    barrel.position.set(0, 0.015, -0.15);\n                    group.add(barrel);\n\n                    // Barrel shroud/heat guard\n                    const shroud = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.04, 0.3), bodyMat);\n                    shroud.position.set(0, 0.01, -0.05);\n                    group.add(shroud);\n\n                    // Receiver\n                    const receiver = new THREE.Mesh(new THREE.BoxGeometry(0.055, 0.06, 0.2), metalMat);\n                    receiver.position.set(0, 0.01, 0.15);\n                    group.add(receiver);\n\n                    // Feed tray cover\n                    const cover = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.02, 0.12), metalMat);\n                    cover.position.set(0, 0.04, 0.12);\n                    group.add(cover);\n\n                    // Ammo box (belt-fed)\n                    const ammoBox = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.08, 0.08), ammoBoxMat);\n                    ammoBox.position.set(0.04, -0.04, 0.12);\n                    group.add(ammoBox);\n\n                    // Stock\n                    const stock = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.05, 0.18), stockMat);\n                    stock.position.set(0, 0.0, 0.32);\n                    stock.rotation.x = -0.04;\n                    group.add(stock);\n\n                    // Pistol grip\n                    const grip = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.08, 0.035), stockMat);\n                    grip.position.set(0, -0.05, 0.22);\n                    grip.rotation.x = 0.25;\n                    group.add(grip);\n\n                    // Bipod legs (folded)\n                    const leg1 = new THREE.Mesh(new THREE.CylinderGeometry(0.005, 0.005, 0.12, 6), stockMat);\n                    leg1.position.set(-0.03, -0.03, -0.12);\n                    leg1.rotation.x = 0.4;\n                    group.add(leg1);\n                    const leg2 = new THREE.Mesh(new THREE.CylinderGeometry(0.005, 0.005, 0.12, 6), stockMat);\n                    leg2.position.set(0.03, -0.03, -0.12);\n                    leg2.rotation.x = 0.4;\n                    group.add(leg2);\n\n                    // Carry handle\n                    const handle = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.015, 0.08), metalMat);\n                    handle.position.set(0, 0.045, 0.04);\n                    group.add(handle);\n\n                    return group;\n                }\n\n                // --- Build 3D Assault Rifle ---\n                function buildAssaultRifle3D() {\n                    const group = new THREE.Group();\n                    const metalMat = new THREE.MeshPhongMaterial({ color: 0x1c1c1e, specular: 0x2a2a30, shininess: 20 });\n                    const bodyMat = new THREE.MeshPhongMaterial({ color: 0x222224, specular: 0x222222, shininess: 15 });\n                    const stockMat = new THREE.MeshPhongMaterial({ color: 0x161618, specular: 0x111111, shininess: 10 });\n                    const railMat = new THREE.MeshPhongMaterial({ color: 0x28282a, specular: 0x333338, shininess: 25 });\n                    const lensMat = new THREE.MeshPhongMaterial({ color: 0x0e1a30, specular: 0x55aaff, shininess: 280 });\n\n                    // Barrel\n                    const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.35, 8), metalMat);\n                    barrel.rotation.x = Math.PI / 2;\n                    barrel.position.set(0, 0.015, -0.12);\n                    group.add(barrel);\n\n                    // Muzzle device\n                    const muzzle = new THREE.Mesh(new THREE.CylinderGeometry(0.016, 0.012, 0.04, 8), metalMat);\n                    muzzle.rotation.x = Math.PI / 2;\n                    muzzle.position.set(0, 0.015, -0.31);\n                    group.add(muzzle);\n\n                    // Handguard/rail system\n                    const rail = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.04, 0.2), railMat);\n                    rail.position.set(0, 0.01, -0.05);\n                    group.add(rail);\n\n                    // Top rail\n                    const topRail = new THREE.Mesh(new THREE.BoxGeometry(0.025, 0.008, 0.32), railMat);\n                    topRail.position.set(0, 0.04, 0.02);\n                    group.add(topRail);\n\n                    // Upper receiver\n                    const upper = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.05, 0.12), bodyMat);\n                    upper.position.set(0, 0.015, 0.12);\n                    group.add(upper);\n\n                    // Lower receiver\n                    const lower = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.04, 0.1), bodyMat);\n                    lower.position.set(0, -0.015, 0.13);\n                    group.add(lower);\n\n                    // Magazine\n                    const mag = new THREE.Mesh(new THREE.BoxGeometry(0.025, 0.1, 0.04), bodyMat);\n                    mag.position.set(0, -0.06, 0.12);\n                    mag.rotation.x = -0.1;\n                    group.add(mag);\n\n                    // Pistol grip\n                    const grip = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.08, 0.035), stockMat);\n                    grip.position.set(0, -0.055, 0.18);\n                    grip.rotation.x = 0.2;\n                    group.add(grip);\n\n                    // Trigger guard\n                    const guard = new THREE.Mesh(new THREE.BoxGeometry(0.025, 0.012, 0.05), metalMat);\n                    guard.position.set(0, -0.025, 0.16);\n                    group.add(guard);\n\n                    // Buffer tube\n                    const buffer = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, 0.12, 8), metalMat);\n                    buffer.rotation.x = Math.PI / 2;\n                    buffer.position.set(0, 0.01, 0.24);\n                    group.add(buffer);\n\n                    // Collapsible stock\n                    const stock = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.05, 0.1), stockMat);\n                    stock.position.set(0, 0.005, 0.32);\n                    group.add(stock);\n\n                    // Buttpad\n                    const buttpad = new THREE.Mesh(new THREE.BoxGeometry(0.037, 0.055, 0.01), stockMat);\n                    buttpad.position.set(0, 0.005, 0.37);\n                    group.add(buttpad);\n\n                    // Red dot sight\n                    const sightBase = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.02, 0.03), metalMat);\n                    sightBase.position.set(0, 0.055, 0.06);\n                    group.add(sightBase);\n                    const sightLens = new THREE.Mesh(new THREE.CylinderGeometry(0.01, 0.01, 0.015, 8), lensMat);\n                    sightLens.rotation.x = Math.PI / 2;\n                    sightLens.position.set(0, 0.055, 0.045);\n                    group.add(sightLens);\n\n                    return group;\n                }\n\n                // --- Build 3D Flamethrower (The Boring Company Not-A-Flamethrower style) ---\n                function buildFlamethrower3D() {\n                    const group = new THREE.Group();\n                    \n                    // Materials\n                    const whiteMat = new THREE.MeshPhongMaterial({ color: 0xf5f6f8, specular: 0x444444, shininess: 35 }); // White top shell\n                    const blackMat = new THREE.MeshPhongMaterial({ color: 0x18191b, specular: 0x1e2022, shininess: 15 }); // Black lower receiver, grip, stock, canister\n                    const detailMat = new THREE.MeshPhongMaterial({ color: 0x5a5e65, specular: 0x3c3f44, shininess: 25 }); // Metal parts\n                    const bronzeMat = new THREE.MeshPhongMaterial({ color: 0xa87c4b, specular: 0x6e4d28, shininess: 45 }); // Brass dials & valve\n                    const copperMat = new THREE.MeshPhongMaterial({ color: 0xbd6a3f, specular: 0x824422, shininess: 40 }); // Copper accents\n                    const pilotGlowMat = new THREE.MeshBasicMaterial({ color: 0xff5b00 }); // Pilot light glow\n                    const nozzleMat = new THREE.MeshPhongMaterial({ color: 0x18191b, specular: 0x1e2022, shininess: 15 }); // Black nozzle tip - glows with heat separately\n\n                    // 1. Upper Receiver Cover (White shell)\n                    const upperReceiver = new THREE.Mesh(new THREE.BoxGeometry(0.046, 0.052, 0.36), whiteMat);\n                    upperReceiver.position.set(0, 0.012, 0.06);\n                    group.add(upperReceiver);\n\n                    // 2. Lower Receiver / Handguard (Black)\n                    const lowerReceiver = new THREE.Mesh(new THREE.BoxGeometry(0.048, 0.034, 0.32), blackMat);\n                    lowerReceiver.position.set(0, -0.024, -0.02);\n                    group.add(lowerReceiver);\n\n                    // 3. Pistol Grip (Black)\n                    const grip = new THREE.Mesh(new THREE.BoxGeometry(0.024, 0.08, 0.035), blackMat);\n                    grip.position.set(0, -0.076, 0.12);\n                    grip.rotation.x = 0.22;\n                    group.add(grip);\n\n                    // 4. Skeleton stock (Black structure at the back)\n                    // Upper truss bar\n                    const stockTop = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.012, 0.14), blackMat);\n                    stockTop.position.set(0, -0.01, 0.28);\n                    stockTop.rotation.x = -0.12;\n                    group.add(stockTop);\n\n                    // Lower truss bar\n                    const stockBottom = new THREE.Mesh(new THREE.BoxGeometry(0.016, 0.012, 0.14), blackMat);\n                    stockBottom.position.set(0, -0.068, 0.26);\n                    stockBottom.rotation.x = 0.28;\n                    group.add(stockBottom);\n\n                    // Butt plate\n                    const buttPlate = new THREE.Mesh(new THREE.BoxGeometry(0.024, 0.085, 0.018), blackMat);\n                    buttPlate.position.set(0, -0.042, 0.34);\n                    group.add(buttPlate);\n\n                    // 5. Gas Canister / Tank (Black, mounted horizontally on top)\n                    const tank = new THREE.Mesh(new THREE.CylinderGeometry(0.022, 0.022, 0.16, 8), blackMat);\n                    tank.rotation.x = Math.PI / 2;\n                    tank.position.set(0, 0.052, 0.04);\n                    group.add(tank);\n\n                    // Rounded caps for the horizontal cylinder\n                    const tankCapFront = new THREE.Mesh(new THREE.SphereGeometry(0.022, 8, 8), blackMat);\n                    tankCapFront.position.set(0, 0.052, -0.04);\n                    group.add(tankCapFront);\n\n                    const tankCapBack = new THREE.Mesh(new THREE.SphereGeometry(0.022, 8, 8), blackMat);\n                    tankCapBack.position.set(0, 0.052, 0.12);\n                    group.add(tankCapBack);\n\n                    // Valve connection (No hose running down)\n                    const valve = new THREE.Mesh(new THREE.CylinderGeometry(0.008, 0.008, 0.016, 6), bronzeMat);\n                    valve.position.set(0.012, 0.052, 0.13);\n                    group.add(valve);\n\n                    // 6. Front Handguard Shroud (Black, encloses barrel base)\n                    const handguardExt = new THREE.Mesh(new THREE.BoxGeometry(0.044, 0.040, 0.14), blackMat);\n                    handguardExt.position.set(0, 0.01, -0.15);\n                    group.add(handguardExt);\n\n                    // 7. Thin Torch Pipe / Barrel (Steel color, extends forward)\n                    const torchPipe = new THREE.Mesh(new THREE.CylinderGeometry(0.008, 0.008, 0.24, 8), detailMat);\n                    torchPipe.rotation.x = Math.PI / 2;\n                    torchPipe.position.set(0, 0.01, -0.26);\n                    group.add(torchPipe);\n\n                    // 8. Copper accents / rails on receiver side (The Boring Company decal area)\n                    const decalStrip = new THREE.Mesh(new THREE.BoxGeometry(0.048, 0.015, 0.14), copperMat);\n                    decalStrip.position.set(0, 0.012, 0.04);\n                    group.add(decalStrip);\n\n                    // 9. Flared Burner Nozzle Cup (Black - glows red-hot as heat builds up)\n                    const nozzle = new THREE.Mesh(new THREE.CylinderGeometry(0.022, 0.012, 0.08, 8), nozzleMat);\n                    nozzle.rotation.x = Math.PI / 2;\n                    nozzle.position.set(0, 0.01, -0.42);\n                    group.add(nozzle);\n\n                    // 10. Igniter Wire and Pilot glow tip (runs under the torch pipe to nozzle)\n                    const igniter = new THREE.Mesh(new THREE.CylinderGeometry(0.003, 0.003, 0.14, 6), detailMat);\n                    igniter.rotation.x = Math.PI / 2;\n                    igniter.position.set(0, -0.012, -0.35);\n                    group.add(igniter);\n\n                    const glowTip = new THREE.Mesh(new THREE.SphereGeometry(0.005, 6, 6), pilotGlowMat);\n                    glowTip.position.set(0, -0.012, -0.422);\n                    group.add(glowTip);\n\n                    // Register materials for heat glow effect\n                    group.userData.barrelMat = detailMat;\n                    group.userData.nozzleMat = nozzleMat;\n                    group.userData.heatColor = new THREE.Color();\n                    group.userData.coolingParts = [\n                        { mesh: tank, role: 'tank' },\n                        { mesh: tankCapFront, role: 'tank' },\n                        { mesh: tankCapBack, role: 'tank' },\n                        { mesh: valve, role: 'valve' },\n                        { mesh: nozzle, role: 'nozzle' },\n                        { mesh: glowTip, role: 'pilot' }\n                    ];\n                    for (let i = 0; i < group.userData.coolingParts.length; i++) {\n                        const record = group.userData.coolingParts[i];\n                        record.mesh.userData.flamethrowerCoolingPart = true;\n                        record.position = record.mesh.position.clone();\n                        record.rotation = record.mesh.rotation.clone();\n                        record.scale = record.mesh.scale.clone();\n                    }\n                    return group;\n                }\n\n                // Create weapon models\n                const weaponModels = [buildPistol3D(), buildShotgun3D(), buildSniper3D(), buildKatana3D(), buildGatling3D(), buildLMG3D(), buildAssaultRifle3D(), buildFlamethrower3D()];\n                weaponModels[0].scale.setScalar(0.60); // Pistol scaled down\n                const muzzleFlash3D = buildMuzzleFlash3D();\n                refreshMuzzleFlashTexture = () => {\n                    if (!muzzleFlash3D || muzzleFlash3D.userData.isAtlas) return;\n                    const flashTexture = createMuzzleFlashTextureFromCache();\n                    if (!flashTexture) return;\n                    initializeMuzzleFlashMaterials(flashTexture);\n                    const flashMat = muzzleFlashMaterials[0];\n                    if (!flashMat) return;\n                    const sprite = muzzleFlash3D.userData.sprite;\n                    if (sprite) {\n                        sprite.material = flashMat;\n                    }\n                    muzzleFlash3D.userData.flashMat = flashMat;\n                    muzzleFlash3D.userData.flashTexture = flashTexture;\n                    muzzleFlash3D.userData.isAtlas = true;\n                    setMuzzleFlashAtlasFrame(0);\n                };\n\n                // Position weapons in viewmodel space \u2014 shifted right & far enough back so full model is visible\n                const weaponPositions = [\n                    { pos: new THREE.Vector3(0.06, -0.18, -0.40), rot: new THREE.Euler(0, 0, 0) },    // Pistol\n                    { pos: new THREE.Vector3(0.08, -0.22, -0.55), rot: new THREE.Euler(0, 0, 0) },    // Shotgun\n                    { pos: new THREE.Vector3(0.08, -0.22, -0.65), rot: new THREE.Euler(0, 0, 0) },    // Sniper\n                    { pos: new THREE.Vector3(0.08, -0.24, -0.48), rot: new THREE.Euler(0, 0, -0.08) }, // Katana\n                    { pos: new THREE.Vector3(0.08, -0.24, -0.55), rot: new THREE.Euler(0, 0, 0) },    // Gatling\n                    { pos: new THREE.Vector3(0.08, -0.22, -0.60), rot: new THREE.Euler(0, 0, 0) },    // LMG\n                    { pos: new THREE.Vector3(0.08, -0.22, -0.55), rot: new THREE.Euler(0, 0, 0) },    // Assault Rifle\n                    { pos: new THREE.Vector3(0.08, -0.22, -0.55), rot: new THREE.Euler(0, 0, 0) }     // Flamethrower\n                ];\n\n                // Muzzle offsets are LOCAL to each weapon group origin, matching actual barrel tip geometry.\n                // Pistol is scaled 0.65 \u2192 stand-off muzzle tip z \u2248 -0.266*0.65 = -0.173; y = 0.025*0.65 \u2248 0.016\n                // Shotgun barrel tip mesh: pos.z=-0.43, half-len=0.01 \u2192 tip z=-0.44; y=0.02\n                // Sniper muzzle brake: pos.z=-0.56, half-len=0.02 \u2192 tip z=-0.58; y=0.015\n                // Gatling barrels: pos.z=-0.15, half-len=0.225 \u2192 tips z=-0.375; all cluster at y=0\n                // LMG barrel: pos.z=-0.15, half-len=0.225 \u2192 tip z=-0.375; y=0.015\n                // AR muzzle device: pos.z=-0.31, half-len=0.02 \u2192 tip z=-0.33; y=0.015\n                const muzzleOffsets = [\n                    new THREE.Vector3(0, 0.015, -0.125),  // Pistol (\u00d70.60 scale \u2014 compact muzzle tip)\n                    new THREE.Vector3(0, 0.020, -0.440),  // Shotgun (barrel tip mesh end)\n                    new THREE.Vector3(0, 0.015, -0.580),  // Sniper (muzzle brake tip)\n                    new THREE.Vector3(0, 0.000, -0.550),  // Katana blade tip\n                    new THREE.Vector3(0, 0.000, -0.375),  // Gatling (6-barrel cluster tips)\n                    new THREE.Vector3(0, 0.015, -0.375),  // LMG (barrel tip)\n                    new THREE.Vector3(0, 0.015, -0.330),   // Assault Rifle (muzzle device tip)\n                    new THREE.Vector3(0, 0.010, -0.460)    // Flamethrower (nozzle tip)\n                ];\n                const weaponFireScratch = {\n                    localMuzzle: new THREE.Vector3(),\n                    worldMuzzle: new THREE.Vector3(),\n                    camDir: new THREE.Vector3(),\n                    targetPoint: new THREE.Vector3(),\n                    baseDir: new THREE.Vector3(),\n                    spreadDir: new THREE.Vector3(),\n                    rightVec: new THREE.Vector3(),\n                    upVec: new THREE.Vector3()\n                };\n                const WEAPON_MUZZLE_STRIDE = 3;\n                const weaponMuzzleLocalData = new Float32Array(weaponModels.length * WEAPON_MUZZLE_STRIDE);\n                const weaponMuzzleViewData = new Float32Array(weaponModels.length * WEAPON_MUZZLE_STRIDE);\n                for (let i = 0; i < weaponModels.length; i++) {\n                    const o = i * WEAPON_MUZZLE_STRIDE;\n                    const pos = weaponPositions[i].pos;\n                    const offset = muzzleOffsets[i];\n                    weaponMuzzleLocalData[o] = pos.x + offset.x;\n                    weaponMuzzleLocalData[o + 1] = pos.y + offset.y;\n                    weaponMuzzleLocalData[o + 2] = pos.z + offset.z;\n                    weaponFireScratch.localMuzzle.copy(offset).applyEuler(weaponPositions[i].rot).add(pos);\n                    weaponMuzzleViewData[o] = weaponFireScratch.localMuzzle.x;\n                    weaponMuzzleViewData[o + 1] = weaponFireScratch.localMuzzle.y;\n                    weaponMuzzleViewData[o + 2] = weaponFireScratch.localMuzzle.z;\n                }\n\n                function setWeaponMuzzleLocal(weaponIndex, out) {\n                    const o = weaponIndex * WEAPON_MUZZLE_STRIDE;\n                    return out.set(weaponMuzzleLocalData[o], weaponMuzzleLocalData[o + 1], weaponMuzzleLocalData[o + 2]);\n                }\n\n                function setWeaponMuzzleView(weaponIndex, out) {\n                    const o = weaponIndex * WEAPON_MUZZLE_STRIDE;\n                    return out.set(weaponMuzzleViewData[o], weaponMuzzleViewData[o + 1], weaponMuzzleViewData[o + 2]);\n                }\n\n                const KATANA_ANIM_SLOWMO = config.katanaSpeed || 1;\n                const KATANA_COMBO_RETURN_DELAY = 1.0;\n\n                function freezeWeaponStaticParts(model) {\n                    model.traverse(child => {\n                        if (\n                            child === model ||\n                            child.name === 'barrelCluster' ||\n                            child.name === 'smartPistolKit' ||\n                            child.name === 'smartPistolGlow' ||\n                            child.name === 'smartPistolCounter' ||\n                            child.name === 'smartPistolScanner' ||\n                            child.name === 'pistolSlide' ||\n                            child.name === 'pistolBarrel' ||\n                            child.name === 'pistolMuzzle' ||\n                            child.name === 'pistolTrigger' ||\n                            child.userData.flamethrowerCoolingPart ||\n                            child.userData.smartAnimated\n                        ) return;\n                        child.updateMatrix();\n                        child.matrixAutoUpdate = false;\n                    });\n                }\n\n                function updateSmartPistolModelVisuals(dt) {\n                    const pistolModel = weaponModels[0];\n                    if (!pistolModel) return;\n                    const kit = pistolModel.userData.smartKit;\n                    if (!kit) return;\n                    const targetDeploy = hasSmartPistol ? 1 : 0;\n                    const deployRate = hasSmartPistol ? 5.6 : 8.5;\n                    const deploy = (pistolModel.userData.smartDeploy || 0) + (targetDeploy - (pistolModel.userData.smartDeploy || 0)) * expSmoothingFactor(dt, deployRate);\n                    pistolModel.userData.smartDeploy = deploy;\n                    kit.visible = deploy > 0.012 || hasSmartPistol;\n                    if (!kit.visible) return;\n\n                    const deployEase = smooth01(deploy);\n                    const storedEase = 1 - deployEase;\n                    const animatedParts = pistolModel.userData.smartAnimatedParts || [];\n                    for (let i = 0; i < animatedParts.length; i++) {\n                        const part = animatedParts[i];\n                        const data = part.userData;\n                        const basePos = data.smartBasePos;\n                        const baseRot = data.smartBaseRot;\n                        const baseScale = data.smartBaseScale;\n                        const deployOffset = data.smartDeployOffset;\n                        const deployRot = data.smartDeployRot;\n                        const deployScale = data.smartDeployScale === undefined ? 0.35 : data.smartDeployScale;\n                        if (!basePos || !baseRot || !baseScale) continue;\n                        part.visible = deploy > 0.025;\n                        part.position.set(\n                            basePos.x + deployOffset.x * storedEase,\n                            basePos.y + deployOffset.y * storedEase,\n                            basePos.z + deployOffset.z * storedEase\n                        );\n                        part.rotation.set(\n                            baseRot.x + deployRot.x * storedEase,\n                            baseRot.y + deployRot.y * storedEase,\n                            baseRot.z + deployRot.z * storedEase\n                        );\n                        const s = deployScale + (1 - deployScale) * deployEase;\n                        part.scale.set(baseScale.x * s, baseScale.y * s, baseScale.z * s);\n                    }\n\n\t                    let readyLocks = 0;\n\t                    for (let i = 0; i < smartPistolLockCount; i++) {\n\t                        if (smartLockProgress[i] >= smartLockRequiredLocks[i] - 0.001) readyLocks++;\n\t                    }\n\t                    const partialLocks = smartPistolLockCount;\n                    const lockEnergy = Math.min(1, readyLocks * 0.34 + partialLocks * 0.11);\n                    const fireJolt = weaponAnim.fireJolt || 0;\n                    const pulse = 0.5 + 0.5 * Math.sin(weaponAnim.swayTime * (6.6 + lockEnergy * 3.5));\n                    const scan = 0.5 + 0.5 * Math.sin(weaponAnim.swayTime * (12.0 + lockEnergy * 7.0));\n                    const targetScale = (1 + pulse * 0.08 + lockEnergy * 0.06) * (0.72 + deployEase * 0.28);\n                    const glowParts = pistolModel.userData.smartGlowParts || [];\n                    for (let i = 0; i < glowParts.length; i++) {\n                        const glow = glowParts[i];\n                        glow.scale.setScalar(targetScale + i * 0.015);\n                        if (glow.material) glow.material.opacity = deployEase * (0.34 + pulse * 0.28 + scan * 0.12 + lockEnergy * 0.22);\n                    }\n\n                    const counterParts = pistolModel.userData.smartCounterParts || [];\n                    const pistolWs = weaponStates[0];\n                    const ammoRatio = pistolWs && pistolWs.magSize ? clamp01(pistolWs.mag / pistolWs.magSize) : 1;\n                    for (let i = 0; i < counterParts.length; i++) {\n                        const counter = counterParts[i];\n                        const baseScale = counter.userData.smartBaseScale || counter.scale;\n                        const counterDeployScale = 0.25 + deployEase * 0.75;\n                        counter.scale.set(\n                            baseScale.x * counterDeployScale,\n                            baseScale.y * counterDeployScale,\n                            Math.max(0.18, baseScale.z * (0.25 + ammoRatio * 0.75)) * counterDeployScale\n                        );\n                        if (counter.material) counter.material.opacity = deployEase * (0.38 + ammoRatio * 0.38 + lockEnergy * 0.18);\n                    }\n\n                    const slide = pistolModel.userData.smartSlide;\n                    if (slide) {\n                        slide.position.z = -0.035 + fireJolt * 0.020 * (0.65 + deployEase * 0.35);\n                        slide.position.y = 0.034 + lockEnergy * 0.002 * pulse;\n                    }\n                    const muzzle = pistolModel.userData.smartMuzzle;\n                    if (muzzle) muzzle.scale.setScalar(1 + fireJolt * 0.045 + lockEnergy * 0.018);\n                    const trigger = pistolModel.userData.smartTrigger;\n                    if (trigger) trigger.rotation.x = -fireJolt * 0.34;\n                    const scanner = pistolModel.userData.smartScanner;\n                    if (scanner) scanner.rotation.z = Math.sin(weaponAnim.swayTime * 8.5) * 0.035 * deployEase;\n\n                    kit.rotation.x = -fireJolt * 0.022;\n                    kit.rotation.z = Math.sin(weaponAnim.swayTime * 2.7) * 0.006 + lockEnergy * 0.01 * Math.sin(weaponAnim.swayTime * 13.0);\n                }\n\n                function updateFlamethrowerCoolingModel(wComp, ws) {\n                    const model = weaponModels[7];\n                    if (!model || !model.userData.coolingParts) return;\n                    const isCooling = wComp.index === 7 && wComp.isReloading;\n                    const progress = isCooling ? clamp01(1 - wComp.reloadTimer / Math.max(0.001, ws.reloadTime)) : 0;\n                    const smoothProgress = smooth01(progress);\n                    const open = isCooling ? Math.sin(Math.PI * smoothProgress) : 0;\n                    const ventPulse = isCooling ? (0.5 + 0.5 * Math.sin(progress * Math.PI * 10)) * open : 0;\n                    const parts = model.userData.coolingParts;\n                    for (let i = 0; i < parts.length; i++) {\n                        const record = parts[i];\n                        const part = record.mesh;\n                        part.position.copy(record.position);\n                        part.rotation.copy(record.rotation);\n                        part.scale.copy(record.scale);\n                        if (record.role === 'tank') {\n                            part.position.y -= open * 0.026;\n                            part.position.z += open * 0.022;\n                        } else if (record.role === 'valve') {\n                            part.position.y -= open * 0.026;\n                            part.position.z += open * 0.022;\n                            part.rotation.y += smoothProgress * Math.PI * 5;\n                        } else if (record.role === 'nozzle') {\n                            part.position.z -= open * 0.030;\n                            const flare = 1 + ventPulse * 0.09;\n                            part.scale.x *= flare;\n                            part.scale.z *= flare;\n                        } else if (record.role === 'pilot') {\n                            part.position.z -= open * 0.030;\n                            part.scale.multiplyScalar(1 + ventPulse * 0.42);\n                        }\n                    }\n                    model.userData.coolingProgress = progress;\n                    model.userData.coolingVentPulse = ventPulse;\n                }\n\n                function setActiveWeaponModel(index) {\n                    for (let i = 0; i < weaponModels.length; i++) {\n                        const model = weaponModels[i];\n                        if (i === index) {\n                            if (model.parent !== weaponHolder) weaponHolder.add(model);\n                            model.visible = true;\n                        } else {\n                            model.visible = false;\n                            if (model.parent === weaponHolder) weaponHolder.remove(model);\n                        }\n                    }\n                }\n\n                weaponModels.forEach((m, i) => {\n                    m.position.copy(weaponPositions[i].pos);\n                    m.rotation.copy(weaponPositions[i].rot);\n                    m.userData.baseRot = weaponPositions[i].rot.clone();\n                    freezeWeaponStaticParts(m);\n                });\n                setActiveWeaponModel(0);\n                weaponHolder.add(muzzleFlash3D);\n\n                // Weapon animation state\n                const weaponAnim = {\n                    recoilAmount: 0,\n                    recoilTarget: 0,\n                    recoilRecovery: 8,\n                    recoilMaxZ: 0.04,\n                    recoilMaxRot: 0.06,\n                    swayTime: 0,\n                    bobAmount: 0,\n                    switchAnim: 0,\n                    switching: false,\n                    currentModel: 0,\n                    muzzleFlashEnergy: 0,\n                    muzzleFlashFrameCounter: 0,\n                    slashDuration: 0.38 * KATANA_ANIM_SLOWMO,\n                    comboHoldTimer: 0,\n                    comboRecoverTimer: 0,\n                    comboRecoverDuration: 0.34 * KATANA_ANIM_SLOWMO,\n                    viewKickTimer: 0,\n                    viewKickCombo: 0,\n                    viewKickStrength: 0,\n                    gunKickTimer: 0,\n                    gunKickDuration: 0.11,\n                    gunKickStrength: 0,\n                    gunKickAmount: 0,\n                    gunKickTarget: 0,\n                    recoilKickScale: 1,\n                    fireJolt: 0,\n                    fireJoltDir: 1,\n                    crosshairGap: 8,\n                    crosshairHitPulse: 0,\n                    hitFovPunch: 0\n                };\n\n                const katanaPoseScratch = {\n                    blade: new THREE.Vector3(),\n                    edge: new THREE.Vector3(),\n                    zAxis: new THREE.Vector3(),\n                    yAxis: new THREE.Vector3(),\n                    xAxis: new THREE.Vector3(),\n                    matrix: new THREE.Matrix4(),\n                    qa: new THREE.Quaternion(),\n                    qb: new THREE.Quaternion(),\n                    q: new THREE.Quaternion(),\n                    quat: new THREE.Quaternion(),\n                    pos: new THREE.Vector3()\n                };\n\n                const katanaRestPose = {\n                    pos: new THREE.Vector3(-0.05, -0.36, -0.43),\n                    blade: new THREE.Vector3(0.22, 0.42, -0.88),\n                    edge: new THREE.Vector3(0, -1, 0)\n                };\n                const katanaDiagonalStart = {\n                    pos: new THREE.Vector3(0.18, 0.18, -0.45),\n                    blade: new THREE.Vector3(0.50, 0.70, -0.52),\n                    edge: new THREE.Vector3(0.25, -0.95, 0)\n                };\n                const katanaDiagonalImpact = {\n                    pos: new THREE.Vector3(-0.22, -0.24, -0.52),\n                    blade: new THREE.Vector3(-0.64, -0.48, -0.60),\n                    edge: new THREE.Vector3(-0.25, -0.95, 0)\n                };\n                const katanaDiagonalSettle = {\n                    pos: new THREE.Vector3(-0.18, -0.22, -0.41),\n                    blade: new THREE.Vector3(-0.38, -0.18, -0.91),\n                    edge: new THREE.Vector3(-0.10, -0.99, 0)\n                };\n                const katanaRightSlashStart = {\n                    pos: new THREE.Vector3(-0.20, -0.08, -0.44),\n                    blade: new THREE.Vector3(-0.78, 0.08, -0.62),\n                    edge: new THREE.Vector3(1, 0, 0)\n                };\n                const katanaRightSlashEnd = {\n                    pos: new THREE.Vector3(0.14, -0.12, -0.43),\n                    blade: new THREE.Vector3(0.72, 0.08, -0.68),\n                    edge: new THREE.Vector3(1, 0, 0)\n                };\n                const katanaRightSlashImpact = {\n                    pos: new THREE.Vector3(0.12, -0.10, -0.50),\n                    blade: new THREE.Vector3(0.88, 0.01, -0.47),\n                    edge: new THREE.Vector3(1, 0, 0)\n                };\n                const katanaRightSlashSettle = {\n                    pos: new THREE.Vector3(0.18, -0.15, -0.41),\n                    blade: new THREE.Vector3(0.58, -0.08, -0.81),\n                    edge: new THREE.Vector3(1, 0, 0)\n                };\n                const katanaLeftSlashReady = {\n                    pos: new THREE.Vector3(0.14, -0.12, -0.43),\n                    blade: new THREE.Vector3(0.72, 0.08, -0.68),\n                    edge: new THREE.Vector3(-1, 0, 0)\n                };\n                const katanaLeftSlashEnd = {\n                    pos: new THREE.Vector3(-0.16, -0.12, -0.43),\n                    blade: new THREE.Vector3(-0.72, 0.14, -0.68),\n                    edge: new THREE.Vector3(-1, 0, 0)\n                };\n                const katanaLeftSlashImpact = {\n                    pos: new THREE.Vector3(-0.14, -0.10, -0.50),\n                    blade: new THREE.Vector3(-0.88, 0.01, -0.47),\n                    edge: new THREE.Vector3(-1, 0, 0)\n                };\n                const katanaLeftSlashSettle = {\n                    pos: new THREE.Vector3(-0.19, -0.15, -0.41),\n                    blade: new THREE.Vector3(-0.58, -0.08, -0.81),\n                    edge: new THREE.Vector3(-1, 0, 0)\n                };\n                const katanaOverheadLiftPose = {\n                    pos: new THREE.Vector3(-0.08, 0.12, -0.45),\n                    blade: new THREE.Vector3(-0.18, 0.70, -0.69),\n                    edge: new THREE.Vector3(0, -1, 0)\n                };\n                const katanaOverheadBridgePose = {\n                    pos: new THREE.Vector3(0.10, 0.02, -0.44),\n                    blade: new THREE.Vector3(0.42, 0.54, -0.73),\n                    edge: new THREE.Vector3(0, -1, 0)\n                };\n                const katanaOverheadPose = {\n                    pos: new THREE.Vector3(0.00, 0.25, -0.43),\n                    blade: new THREE.Vector3(0.00, 0.98, -0.20),\n                    edge: new THREE.Vector3(0, -1, 0)\n                };\n                const katanaDownCutPose = {\n                    pos: new THREE.Vector3(0.00, -0.22, -0.42),\n                    blade: new THREE.Vector3(0.00, -0.36, -0.93),\n                    edge: new THREE.Vector3(0, -1, 0)\n                };\n                const katanaDownImpactPose = {\n                    pos: new THREE.Vector3(0.00, -0.25, -0.51),\n                    blade: new THREE.Vector3(0.00, -0.68, -0.73),\n                    edge: new THREE.Vector3(0, -1, 0)\n                };\n                const katanaDownSettlePose = {\n                    pos: new THREE.Vector3(0.00, -0.18, -0.40),\n                    blade: new THREE.Vector3(0.00, -0.24, -0.97),\n                    edge: new THREE.Vector3(0, -1, 0)\n                };\n\n                function clamp01(v) {\n                    return Math.max(0, Math.min(1, v));\n                }\n\n                function smooth01(v) {\n                    const x = clamp01(v);\n                    return x * x * (3 - 2 * x);\n                }\n\n                function easeInCubic(v) {\n                    const x = clamp01(v);\n                    return x * x * x;\n                }\n\n                function easeOutCubic(v) {\n                    const x = 1 - clamp01(v);\n                    return 1 - x * x * x;\n                }\n\n                function easeOutQuint(v) {\n                    const x = 1 - clamp01(v);\n                    return 1 - x * x * x * x * x;\n                }\n\n                function katanaImpactPulse(combo, progress) {\n                    const center = combo === 1 ? 0.50 : (combo === 2 ? 0.47 : 0.70);\n                    const width = combo === 0 ? 0.085 : 0.055;\n                    const x = 1 - Math.abs(progress - center) / width;\n                    if (x <= 0) return 0;\n                    return Math.sin(clamp01(x) * Math.PI);\n                }\n\n                const katanaTrailPointSets = {\n                    0: [\n                        new THREE.Vector3(0.02, 0.42, -0.68),\n                        new THREE.Vector3(0.00, 0.18, -0.72),\n                        new THREE.Vector3(0.00, -0.08, -0.72),\n                        new THREE.Vector3(0.00, -0.36, -0.66)\n                    ],\n                    1: [\n                        new THREE.Vector3(0.34, 0.28, -0.70),\n                        new THREE.Vector3(0.14, 0.08, -0.74),\n                        new THREE.Vector3(-0.10, -0.10, -0.72),\n                        new THREE.Vector3(-0.34, -0.28, -0.68)\n                    ],\n                    2: [\n                        new THREE.Vector3(-0.40, 0.02, -0.68),\n                        new THREE.Vector3(-0.14, 0.08, -0.72),\n                        new THREE.Vector3(0.14, 0.06, -0.72),\n                        new THREE.Vector3(0.42, 0.00, -0.68)\n                    ]\n                };\n\n                function setKatanaTrailPoints(points) {\n                    const attr = katanaTrail.geometry.attributes.position;\n                    const arr = attr.array;\n                    for (let i = 0; i < 4; i++) {\n                        const p = points[i];\n                        const j = i * 3;\n                        arr[j] = p.x;\n                        arr[j + 1] = p.y;\n                        arr[j + 2] = p.z;\n                    }\n                    attr.needsUpdate = true;\n                }\n\n                function updateKatanaTrail(combo, progress, impact) {\n                    const show = combo === 1\n                        ? (progress > 0.34 && progress < 0.68)\n                        : combo === 2\n                            ? (progress > 0.18 && progress < 0.66)\n                            : (progress > 0.58 && progress < 0.86);\n                    katanaTrail.visible = show;\n                    katanaTrail.material.opacity = show ? (0.24 + impact * 0.42) : 0;\n                    if (!show) return;\n                    setKatanaTrailPoints(katanaTrailPointSets[combo] || katanaTrailPointSets[0]);\n                }\n\n                function katanaQuaternionFromBladeEdge(bladeDir, edgeDir, target) {\n                    const s = katanaPoseScratch;\n                    s.blade.copy(bladeDir).normalize();\n                    s.zAxis.copy(s.blade).multiplyScalar(-1).normalize();\n                    s.edge.copy(edgeDir).normalize();\n                    s.yAxis.copy(s.edge).multiplyScalar(-1);\n                    s.yAxis.addScaledVector(s.zAxis, -s.yAxis.dot(s.zAxis)).normalize();\n                    s.xAxis.crossVectors(s.yAxis, s.zAxis).normalize();\n                    s.matrix.makeBasis(s.xAxis, s.yAxis, s.zAxis);\n                    target.setFromRotationMatrix(s.matrix);\n                    return target;\n                }\n\n                function mixKatanaPose(a, b, amount, out, easing) {\n                    const s = katanaPoseScratch;\n                    const k = easing ? easing(amount) : smooth01(amount);\n                    out.pos.copy(a.pos).lerp(b.pos, k);\n                    katanaQuaternionFromBladeEdge(a.blade, a.edge, s.qa);\n                    katanaQuaternionFromBladeEdge(b.blade, b.edge, s.qb);\n                    out.quat.copy(s.qa).slerp(s.qb, k);\n                    return out;\n                }\n\n                function getKatanaPose(combo, progress, out) {\n                    if (combo === 1) {\n                        if (progress < 0.38) return mixKatanaPose(katanaRestPose, katanaDiagonalStart, progress / 0.38, out, easeInCubic);\n                        if (progress < 0.50) return mixKatanaPose(katanaDiagonalStart, katanaDiagonalImpact, (progress - 0.38) / 0.12, out, easeOutQuint);\n                        if (progress < 0.66) return mixKatanaPose(katanaDiagonalImpact, katanaDiagonalSettle, (progress - 0.50) / 0.16, out, easeOutCubic);\n                        return mixKatanaPose(katanaDiagonalSettle, katanaDiagonalSettle, (progress - 0.66) / 0.34, out, smooth01);\n                    }\n                    if (combo === 2) {\n                        if (progress < 0.16) return mixKatanaPose(katanaDiagonalSettle, katanaRightSlashStart, progress / 0.16, out, easeOutCubic);\n                        if (progress < 0.47) return mixKatanaPose(katanaRightSlashStart, katanaRightSlashImpact, (progress - 0.16) / 0.31, out, easeOutQuint);\n                        if (progress < 0.61) return mixKatanaPose(katanaRightSlashImpact, katanaRightSlashSettle, (progress - 0.47) / 0.14, out, easeOutCubic);\n                        return mixKatanaPose(katanaRightSlashSettle, katanaRightSlashSettle, (progress - 0.61) / 0.39, out, smooth01);\n                    }\n                    if (progress < 0.18) return mixKatanaPose(katanaRightSlashSettle, katanaOverheadBridgePose, progress / 0.18, out, easeOutCubic);\n                    if (progress < 0.34) return mixKatanaPose(katanaOverheadBridgePose, katanaOverheadLiftPose, (progress - 0.18) / 0.16, out, easeOutCubic);\n                    if (progress < 0.52) return mixKatanaPose(katanaOverheadLiftPose, katanaOverheadPose, (progress - 0.34) / 0.18, out, easeInCubic);\n                    if (progress < 0.58) return mixKatanaPose(katanaOverheadPose, katanaOverheadPose, (progress - 0.52) / 0.06, out, smooth01);\n                    if (progress < 0.70) return mixKatanaPose(katanaOverheadPose, katanaDownImpactPose, (progress - 0.58) / 0.12, out, easeOutQuint);\n                    if (progress < 0.84) return mixKatanaPose(katanaDownImpactPose, katanaDownSettlePose, (progress - 0.70) / 0.14, out, easeOutCubic);\n                    return mixKatanaPose(katanaDownSettlePose, katanaRestPose, (progress - 0.84) / 0.16, out, smooth01);\n                }\n\n                // Resize handler for weapon camera\n                window.addEventListener('resize', () => {\n                    syncWeaponViewportForOrientation();\n                });\n\n                function setMuzzleFlashAtlasFrame(frameIndex) {\n                    if (!muzzleFlashMaterials.length) return;\n                    const frame = ((frameIndex % MUZZLE_FLASH_ATLAS_FRAMES) + MUZZLE_FLASH_ATLAS_FRAMES) % MUZZLE_FLASH_ATLAS_FRAMES;\n                    const flashSprite = muzzleFlash3D.userData.sprite;\n                    if (flashSprite) {\n                        const prevMat = flashSprite.material;\n                        const nextMat = muzzleFlashMaterials[frame];\n                        if (prevMat !== nextMat) {\n                            if (prevMat) {\n                                nextMat.opacity = prevMat.opacity;\n                                nextMat.rotation = prevMat.rotation;\n                            }\n                            flashSprite.material = nextMat;\n                        }\n                    }\n                }\n\n                function triggerMuzzleFlash(ws, weaponIndex, derivedOffset, shots = 1, seed = undefined) {\n                    if (!muzzleFlash3D.userData.isAtlas) return;\n\n                    const visualScale = getVisualIntensityScale();\n                    const flashScale = getVisualFlashScale();\n                    const smoothLight = weaponDerivedStats[derivedOffset + WDS_MUZZLE_LIGHT_SMOOTH] > 0;\n                    const wasFlashVisible = muzzleFlash3D.visible && (weaponAnim.muzzleFlashEnergy || 0) > 0;\n\n                    // Set energy envelope and min frame lifetime\n                    weaponAnim.muzzleFlashEnergy = 1.0;\n                    weaponAnim.muzzleFlashFrameCounter = 2;\n\n                    muzzleFlash3D.visible = true;\n                    setWeaponMuzzleView(weaponIndex, muzzleFlash3D.position);\n\n                    const flashStrength = weaponDerivedStats[derivedOffset + WDS_FLASH_STRENGTH_BASE] * (0.72 + flashScale * 0.28);\n                    const rng = seed !== undefined ? seed : Math.random();\n\n                    if (!wasFlashVisible) {\n                        muzzleFlash3D.userData.frameClock = rng * 1.5;\n                        muzzleFlash3D.userData.frameIndex = Math.floor(muzzleFlash3D.userData.frameClock);\n                    } else {\n                        muzzleFlash3D.userData.frameClock += 0.65 + rng * 0.45;\n                        muzzleFlash3D.userData.frameIndex = Math.floor(muzzleFlash3D.userData.frameClock);\n                    }\n                    muzzleFlash3D.userData.intensity = Math.max(muzzleFlash3D.userData.intensity || 0, flashStrength);\n                    muzzleFlash3D.userData.baseScale = weaponDerivedStats[derivedOffset + WDS_MUZZLE_BASE_SCALE] * (0.82 + rng * 0.28);\n                    \n                    setMuzzleFlashAtlasFrame(muzzleFlash3D.userData.frameIndex);\n                    \n                    const scale = muzzleFlash3D.userData.baseScale;\n                    muzzleFlash3D.scale.set(scale, scale, scale);\n\n                    const flashSprite = muzzleFlash3D.userData.sprite;\n                    if (flashSprite && flashSprite.material) {\n                        flashSprite.material.rotation = rng * Math.PI * 2;\n                        const opacityCap = visualIntensityMode === 'soft' ? 0.38 : 0.72;\n                        flashSprite.material.opacity = Math.min(opacityCap, (0.42 + flashStrength * 0.28) * (0.7 + flashScale * 0.3));\n                    }\n\n                    const pressure = getPerfPressureLevel();\n                    if (pressure < 3 && !(pressure >= 2 && weaponDerivedStats[derivedOffset + WDS_RAPID_AUTO_MUZZLE_LIGHT] > 0)) {\n                        muzzleFlashLightTimer = Math.max(muzzleFlashLightTimer, smoothLight ? 0.12 : 0.055);\n                        const lightBudgetScale = pressure >= 2 ? 0.35 : (pressure >= 1 ? 0.65 : 1);\n                        const lightPeak = weaponDerivedStats[derivedOffset + WDS_MUZZLE_LIGHT_PEAK_BASE] * visualScale * lightBudgetScale * (smoothLight ? 0.45 : 1);\n                        muzzleFlashLightPeak = smoothLight\n                            ? Math.max(muzzleFlashLightPeak * 0.92, lightPeak)\n                            : Math.max(muzzleFlashLightPeak, lightPeak);\n                        const pos = world.getComponent(playerEntity, 'Position');\n                        const fDir = effectScratch.v1.set(0, 0, -1).applyQuaternion(camera.quaternion);\n                        muzzleFlashLight.position.set(\n                            pos.x + fDir.x * 0.8,\n                            pos.y + fDir.y * 0.8,\n                            pos.z + fDir.z * 0.8\n                        );\n                        muzzleFlashLight.intensity = Math.max(muzzleFlashLight.intensity, muzzleFlashLightPeak * 0.75 * visualScale);\n                        muzzleFlashLight.color.setHex(ws.name === 'SNIPER' ? 0xccddff : 0xffaa33);\n                    } else {\n                        muzzleFlashLightTimer = 0;\n                        muzzleFlashLightPeak = 0;\n                        muzzleFlashLight.intensity = 0;\n                    }\n                }\n\n                function updateWeapon3D(dt, fired, weaponIndex, isMoving, ctx = null) {\n                    // Weapon switch animation\n                    if (weaponIndex !== weaponAnim.currentModel) {\n                        const previousWs = weaponStates[weaponAnim.currentModel];\n                        if (previousWs && previousWs.isMelee) clearKatanaRuntime();\n                        setActiveWeaponModel(weaponIndex);\n                        weaponAnim.currentModel = weaponIndex;\n                        weaponAnim.switchAnim = 1.0;\n                        if (weaponStates[weaponIndex].isMelee) {\n                            weaponAnim.recoilAmount = 0;\n                            weaponAnim.recoilTarget = 0;\n                            weaponAnim.slashCount = 0;\n                            weaponAnim.slashDuration = 0.38 * KATANA_ANIM_SLOWMO;\n                            weaponAnim.comboHoldTimer = 0;\n                            weaponAnim.comboRecoverTimer = 0;\n                        }\n                    }\n\n                    // Switch animation (lower then raise)\n                    if (weaponAnim.switchAnim > 0) {\n                        weaponAnim.switchAnim -= dt * 4;\n                        if (weaponAnim.switchAnim < 0) weaponAnim.switchAnim = 0;\n                    }\n                    const switchOffset = Math.sin(weaponAnim.switchAnim * Math.PI) * 0.15;\n\n                    const ws = weaponStates[weaponIndex];\n                    const derivedOffset = weaponDerivedOffset(weaponIndex);\n\n                    // Recoil / Melee swing\n                    const muzzleShots = (ctx && ctx.muzzleFlashShots) || 0;\n                    if (ws.isMelee && fired) {\n                        weaponAnim.recoilAmount = 1.0;\n                        weaponAnim.recoilTarget = 1.0;\n                        weaponAnim.slashCount = (weaponAnim.slashCount || 0) + 1;\n                        const combo = (weaponAnim.slashCount || 1) % 3;\n                        weaponAnim.slashDuration = (combo === 0 ? 0.62 : 0.38) * KATANA_ANIM_SLOWMO;\n                        weaponAnim.comboHoldTimer = combo === 0 ? 0 : KATANA_COMBO_RETURN_DELAY;\n                        weaponAnim.comboRecoverTimer = 0;\n                    } else if (!ws.isMelee && muzzleShots > 0) {\n                        const recoilImpulse = weaponDerivedStats[derivedOffset + WDS_RECOIL_IMPULSE];\n                        weaponAnim.recoilTarget = Math.min(1.0, Math.max(weaponAnim.recoilTarget, weaponAnim.recoilAmount) + recoilImpulse * muzzleShots);\n                        weaponAnim.fireJolt = Math.min(1.25, Math.max(weaponAnim.fireJolt || 0, 0.42 + recoilImpulse * 0.75));\n                        weaponAnim.fireJoltDir = Math.random() < 0.5 ? -1 : 1;\n                        triggerMuzzleFlash(ws, weaponIndex, derivedOffset, muzzleShots, ctx.muzzleFlashSeed);\n                    }\n\n                    // Muzzle flash decay driven by energy envelope\n                    if ((weaponAnim.muzzleFlashEnergy || 0) > 0) {\n                        if (weaponAnim.muzzleFlashFrameCounter > 0) {\n                            weaponAnim.muzzleFlashFrameCounter--;\n                        }\n                        const duration = weaponDerivedStats[derivedOffset + WDS_MUZZLE_FLASH_DURATION];\n                        const decayRate = 1.0 / (duration || 0.05);\n                        let nextEnergy = weaponAnim.muzzleFlashEnergy - dt * decayRate;\n                        if (weaponAnim.muzzleFlashFrameCounter > 0) {\n                            nextEnergy = Math.max(0.55, nextEnergy);\n                        }\n                        weaponAnim.muzzleFlashEnergy = Math.max(0, nextEnergy);\n\n                        const fade = smooth01(weaponAnim.muzzleFlashEnergy);\n                        const clockRate = weaponDerivedStats[derivedOffset + WDS_MUZZLE_FLASH_CLOCK_RATE];\n                        muzzleFlash3D.userData.frameClock = (muzzleFlash3D.userData.frameClock || 0) + dt * clockRate;\n                        const frameIndex = Math.min(MUZZLE_FLASH_ATLAS_FRAMES - 1, Math.floor(muzzleFlash3D.userData.frameClock) % MUZZLE_FLASH_ATLAS_FRAMES);\n                        setMuzzleFlashAtlasFrame(frameIndex);\n\n                        const flashSprite = muzzleFlash3D.userData.sprite;\n                        if (flashSprite && flashSprite.material) {\n                            const opacityCap = visualIntensityMode === 'soft' ? 0.38 : 0.72;\n                            flashSprite.material.opacity = Math.min(opacityCap, (muzzleFlash3D.userData.intensity || 0.7) * fade);\n                        }\n                        const s = (muzzleFlash3D.userData.baseScale || 1) * (0.92 + (1 - fade) * 0.18);\n                        muzzleFlash3D.scale.set(s, s, s);\n\n                        if (weaponAnim.muzzleFlashEnergy <= 0) {\n                            muzzleFlash3D.visible = false;\n                            muzzleFlash3D.userData.intensity = 0;\n                        }\n                    }\n\n                    // Muzzle flash dynamic light\n                    if (muzzleFlashLightTimer > 0) {\n                        muzzleFlashLightTimer -= dt;\n                        const lightDuration = weaponDerivedStats[derivedOffset + WDS_MUZZLE_LIGHT_SMOOTH] > 0 ? 0.12 : 0.055;\n                        const lightFade = smooth01(Math.max(0, muzzleFlashLightTimer / lightDuration));\n                        const targetLight = Math.min(visualIntensityMode === 'soft' ? 0.72 : 2.05, muzzleFlashLightPeak * lightFade);\n                        muzzleFlashLight.intensity += (targetLight - muzzleFlashLight.intensity) * expSmoothingFactor(dt, 28);\n                        muzzleFlashLightPeak = Math.max(0, muzzleFlashLightPeak - dt * 6);\n                        if (muzzleFlashLightTimer <= 0) {\n                            muzzleFlashLight.intensity = 0;\n                            muzzleFlashLightPeak = 0;\n                        }\n                    }\n\n                    // Recover recoil\n                    if (ws.isMelee) {\n                        const recSpeed = 1.0 / (weaponAnim.slashDuration || ws.fireRate);\n                        weaponAnim.recoilAmount -= dt * recSpeed;\n                        if (weaponAnim.recoilAmount < 0) weaponAnim.recoilAmount = 0;\n                    } else {\n                        weaponAnim.recoilTarget = Math.max(0, weaponAnim.recoilTarget - dt * (weaponAnim.recoilRecovery * 0.82));\n                        weaponAnim.recoilAmount += (weaponAnim.recoilTarget - weaponAnim.recoilAmount) * expSmoothingFactor(dt, 22);\n                        if (weaponAnim.recoilAmount < 0.001 && weaponAnim.recoilTarget <= 0) weaponAnim.recoilAmount = 0;\n                    }\n                    weaponAnim.fireJolt = Math.max(0, (weaponAnim.fireJolt || 0) - dt * 9.5);\n\n                    // Weapon sway\n                    weaponAnim.swayTime += dt;\n                    const swayX = Math.sin(weaponAnim.swayTime * 1.7) * 0.0032 + Math.sin(weaponAnim.swayTime * 3.1) * 0.0012;\n                    const swayY = Math.cos(weaponAnim.swayTime * 2.1) * 0.0018;\n\n                    // Bob when moving\n                    if (isMoving) {\n                        weaponAnim.bobAmount += dt * 8;\n                    } else {\n                        weaponAnim.bobAmount += dt * 2;\n                    }\n                    const bobX = Math.sin(weaponAnim.bobAmount) * (isMoving ? 0.011 : 0.0024);\n                    const bobY = Math.abs(Math.cos(weaponAnim.bobAmount)) * (isMoving ? 0.008 : 0.0014);\n                    updateSmartPistolModelVisuals(dt);\n                    const currentWeaponComponent = world.getComponent(playerEntity, 'Weapon');\n                    if (weaponIndex === 7) updateFlamethrowerCoolingModel(currentWeaponComponent, ws);\n\n                    // Gatling barrel spin\n                    if (weaponIndex === 4) {\n                        const gatlingModel = weaponModels[4];\n                        const barrelCluster = gatlingModel.children.find(c => c.name === 'barrelCluster');\n                        if (barrelCluster) {\n                            if (input.holdingFire) {\n                                barrelCluster.rotation.z += dt * 30; // Fast spin when firing\n                            } else {\n                                barrelCluster.rotation.z += dt * 2; // Slow idle spin\n                            }\n                        }\n                    }\n\n                    // Apply to active weapon\n                    const activeModel = weaponModels[weaponIndex];\n                    const basePos = weaponPositions[weaponIndex].pos;\n\n                    if (ws.isMelee) {\n                        // Katana combo: right horizontal, left horizontal, overhead downward, then reset.\n                        const t = weaponAnim.recoilAmount; // 1\u21920 over fireRate duration\n                        const combo = weaponAnim.slashCount % 3;\n                        const poseOut = katanaPoseScratch;\n\n                        if (t > 0) {\n                            const progress = 1 - t;\n                            const impact = katanaImpactPulse(combo, progress);\n                            const hitX = combo === 1 ? -0.03 : (combo === 2 ? 0.025 : 0);\n                            const hitY = combo === 0 ? -0.055 : (combo === 1 ? -0.035 : 0.008);\n                            const hitZ = combo === 0 ? 0.075 : 0.045;\n                            getKatanaPose(combo, progress, poseOut);\n                            updateKatanaTrail(combo, progress, impact);\n                            activeModel.position.set(\n                                poseOut.pos.x + swayX + bobX + hitX * impact,\n                                poseOut.pos.y + swayY + bobY - switchOffset + hitY * impact,\n                                poseOut.pos.z - hitZ * impact\n                            );\n                            activeModel.quaternion.copy(poseOut.quat);\n                        } else {\n                            katanaTrail.material.opacity = 0;\n                            katanaTrail.visible = false;\n                            if (weaponAnim.comboHoldTimer > 0) {\n                                weaponAnim.comboHoldTimer = Math.max(0, weaponAnim.comboHoldTimer - dt);\n                                if (weaponAnim.comboHoldTimer === 0) weaponAnim.comboRecoverTimer = weaponAnim.comboRecoverDuration;\n                            }\n                            const heldPose = combo === 1 ? katanaDiagonalSettle : (combo === 2 ? katanaRightSlashSettle : katanaRestPose);\n                            if (weaponAnim.comboRecoverTimer > 0) {\n                                weaponAnim.comboRecoverTimer = Math.max(0, weaponAnim.comboRecoverTimer - dt);\n                                const recoverProgress = 1 - (weaponAnim.comboRecoverTimer / weaponAnim.comboRecoverDuration);\n                                mixKatanaPose(heldPose, katanaRestPose, recoverProgress, poseOut, smooth01);\n                                if (weaponAnim.comboRecoverTimer === 0) weaponAnim.slashCount = 0;\n                            } else {\n                                const pose = weaponAnim.comboHoldTimer > 0 ? heldPose : katanaRestPose;\n                                poseOut.pos.copy(pose.pos);\n                                katanaQuaternionFromBladeEdge(pose.blade, pose.edge, poseOut.quat);\n                            }\n                            activeModel.position.set(\n                                poseOut.pos.x + swayX + bobX,\n                                poseOut.pos.y + swayY + bobY - switchOffset,\n                                poseOut.pos.z\n                            );\n                            activeModel.quaternion.copy(poseOut.quat);\n                        }\n                    } else {\n                        const recoilScale = weaponAnim.recoilKickScale || 1;\n                        const visualKick = world.getComponent(playerEntity, 'Weapon').visualKick || 0;\n                        const baseRot = activeModel.userData.baseRot || weaponPositions[weaponIndex].rot;\n                        const fireJolt = weaponAnim.fireJolt || 0;\n                        const fireJoltDir = weaponAnim.fireJoltDir || 1;\n                        const recoilLift = weaponAnim.recoilAmount * 0.014 * recoilScale + visualKick * 0.0025 + fireJolt * 0.004;\n                        const joltX = fireJolt * 0.011 * fireJoltDir;\n                        const joltZ = fireJolt * 0.022;\n                        const joltRoll = fireJolt * 0.028 * fireJoltDir;\n\n                        // Per-weapon reload animation offsets\n                        let rlOX = 0, rlOY = 0, rlOZ = 0, rlRX = 0, rlRY = 0, rlRZ = 0;\n                        const rlWComp = world.getComponent(playerEntity, 'Weapon');\n                        if (rlWComp.isReloading) {\n                            const rp = 1 - (rlWComp.reloadTimer / ws.reloadTime);\n                            const rs = (v) => { const x = Math.max(0, Math.min(1, v)); return x * x * (3 - 2 * x); };\n                            switch (weaponIndex) {\n                                case 0: { // Pistol: tilt down, mag drop/insert, slide rack\n                                    if (rp < 0.2) { const t = rs(rp / 0.2); rlRX = -0.35 * t; rlOY = -0.05 * t; rlRZ = 0.08 * t; }\n                                    else if (rp < 0.45) { const t = rs((rp - 0.2) / 0.25); rlRX = -0.35; rlOY = -0.05 + 0.01 * t; rlRZ = 0.08 - 0.04 * Math.sin(t * Math.PI); rlOZ = 0.01 * Math.sin(t * Math.PI); }\n                                    else if (rp < 0.72) { const t = rs((rp - 0.45) / 0.27); rlRX = -0.35 + 0.12 * t; rlOY = -0.04 - 0.025 * Math.sin(t * Math.PI); rlRZ = 0.08 * (1 - t); rlOZ = 0.035 * Math.sin(t * Math.PI); }\n                                    else { const t = rs((rp - 0.72) / 0.28); rlRX = -0.23 * (1 - t); rlOY = -0.04 * (1 - t); }\n                                    break;\n                                }\n                                case 1: { // Shotgun: pump back, shell insert, pump forward\n                                    if (rp < 0.15) { const t = rs(rp / 0.15); rlRX = -0.12 * t; rlOY = -0.03 * t; rlRZ = 0.06 * t; }\n                                    else if (rp < 0.4) { const t = rs((rp - 0.15) / 0.25); rlOZ = 0.06 * t; rlRX = -0.12; rlOY = -0.03; rlRZ = 0.06; }\n                                    else if (rp < 0.6) { const t = rs((rp - 0.4) / 0.2); rlOZ = 0.06 - 0.09 * t; rlRX = -0.12 + 0.04 * t; rlOY = -0.03 - 0.01 * Math.sin(t * Math.PI); rlRZ = 0.06; }\n                                    else if (rp < 0.82) { const t = rs((rp - 0.6) / 0.22); rlOZ = -0.03 + 0.03 * t; rlRX = -0.08 * (1 - t); rlOY = -0.03 * (1 - t * 0.7); rlRZ = 0.06 * (1 - t); }\n                                    else { const t = rs((rp - 0.82) / 0.18); rlOY = -0.01 * (1 - t); }\n                                    break;\n                                }\n                                case 2: { // Sniper: bolt lift, bolt back, bolt forward, bolt down\n                                    if (rp < 0.18) { const t = rs(rp / 0.18); rlRX = -0.08 * t; rlRZ = 0.15 * t; rlOY = -0.02 * t; }\n                                    else if (rp < 0.38) { const t = rs((rp - 0.18) / 0.2); rlRX = -0.08; rlRZ = 0.15; rlOZ = 0.07 * t; rlOY = -0.02; }\n                                    else if (rp < 0.58) { const t = rs((rp - 0.38) / 0.2); rlRX = -0.08; rlRZ = 0.15; rlOZ = 0.07 - 0.09 * t; rlOY = -0.02 - 0.01 * Math.sin(t * Math.PI); }\n                                    else if (rp < 0.78) { const t = rs((rp - 0.58) / 0.2); rlRX = -0.08 * (1 - t); rlRZ = 0.15 * (1 - t); rlOZ = -0.02 * (1 - t); rlOY = -0.02 * (1 - t); }\n                                    else { const t = rs((rp - 0.78) / 0.22); rlOY = 0; }\n                                    break;\n                                }\n                                case 4: { // Gatling: tilt up, drum detach/attach, return\n                                    if (rp < 0.2) { const t = rs(rp / 0.2); rlRX = 0.15 * t; rlOY = -0.06 * t; rlRZ = -0.1 * t; }\n                                    else if (rp < 0.55) { const t = rs((rp - 0.2) / 0.35); rlRX = 0.15; rlOY = -0.06 + 0.02 * Math.sin(t * Math.PI); rlRZ = -0.1; rlOZ = 0.02 * Math.sin(t * Math.PI * 2); }\n                                    else if (rp < 0.82) { const t = rs((rp - 0.55) / 0.27); rlRX = 0.15 * (1 - t); rlOY = -0.06 * (1 - t); rlRZ = -0.1 * (1 - t); }\n                                    else { const t = rs((rp - 0.82) / 0.18); rlOY = 0; }\n                                    break;\n                                }\n                                case 5: { // LMG: tilt, open feed tray, belt insert, close\n                                    if (rp < 0.15) { const t = rs(rp / 0.15); rlRX = -0.1 * t; rlOY = -0.04 * t; rlRZ = 0.05 * t; }\n                                    else if (rp < 0.35) { const t = rs((rp - 0.15) / 0.2); rlRX = -0.1 + 0.18 * t; rlOY = -0.04; rlRZ = 0.05; }\n                                    else if (rp < 0.6) { const t = rs((rp - 0.35) / 0.25); rlRX = 0.08; rlOY = -0.04 + 0.015 * Math.sin(t * Math.PI); rlRZ = 0.05; rlOZ = 0.02 * Math.sin(t * Math.PI); }\n                                    else if (rp < 0.82) { const t = rs((rp - 0.6) / 0.22); rlRX = 0.08 - 0.18 * t; rlOY = -0.04 * (1 - t * 0.5); rlRZ = 0.05 * (1 - t); }\n                                    else { const t = rs((rp - 0.82) / 0.18); rlRX = -0.1 * (1 - t); rlOY = -0.02 * (1 - t); }\n                                    break;\n                                }\n                                case 6: { // AR: tilt, mag release/insert, charging handle\n                                    if (rp < 0.18) { const t = rs(rp / 0.18); rlRX = -0.22 * t; rlOY = -0.04 * t; rlRZ = 0.1 * t; }\n                                    else if (rp < 0.4) { const t = rs((rp - 0.18) / 0.22); rlRX = -0.22; rlOY = -0.04 + 0.015 * Math.sin(t * Math.PI); rlRZ = 0.1; rlOZ = 0.01 * Math.sin(t * Math.PI); }\n                                    else if (rp < 0.62) { const t = rs((rp - 0.4) / 0.22); rlRX = -0.22 + 0.06 * t; rlOY = -0.04 - 0.02 * Math.sin(t * Math.PI); rlRZ = 0.1 - 0.04 * t; rlOZ = 0.035 * Math.sin(t * Math.PI); }\n                                    else if (rp < 0.82) { const t = rs((rp - 0.62) / 0.2); rlRX = -0.16 * (1 - t); rlOY = -0.04 * (1 - t); rlRZ = 0.06 * (1 - t); }\n                                    else { const t = rs((rp - 0.82) / 0.18); rlOY = 0; }\n                                    break;\n                                }\n                                case 7: { // Flamethrower: vent, unlock tank valve, cool, reseat\n                                    if (rp < 0.18) {\n                                        const t = rs(rp / 0.18);\n                                        rlRX = -0.18 * t;\n                                        rlOY = -0.035 * t;\n                                        rlRZ = 0.12 * t;\n                                    } else if (rp < 0.48) {\n                                        const t = rs((rp - 0.18) / 0.30);\n                                        rlRX = -0.18 + 0.05 * t;\n                                        rlOY = -0.035 - 0.012 * Math.sin(t * Math.PI);\n                                        rlRZ = 0.12;\n                                        rlOZ = 0.025 * Math.sin(t * Math.PI);\n                                    } else if (rp < 0.78) {\n                                        const t = rs((rp - 0.48) / 0.30);\n                                        rlRX = -0.13;\n                                        rlOY = -0.035 + 0.008 * Math.sin(t * Math.PI * 2);\n                                        rlRZ = 0.12 - 0.05 * t;\n                                        rlOZ = 0.018 * (1 - t);\n                                    } else {\n                                        const t = rs((rp - 0.78) / 0.22);\n                                        rlRX = -0.13 * (1 - t);\n                                        rlOY = -0.035 * (1 - t);\n                                        rlRZ = 0.07 * (1 - t);\n                                    }\n                                    break;\n                                }\n                            }\n                        }\n\n                        if (weaponIndex === 7) {\n                            const ftModel = weaponModels[7];\n                            if (ftModel && ftModel.userData.barrelMat) {\n                                const heatRatio = clamp01((rlWComp.flameHeat || 0) / (ws.magSize || 100));\n                                const coolingPulse = rlWComp.isReloading\n                                    ? 0.48 + (ftModel.userData.coolingVentPulse || 0) * 0.52\n                                    : 0;\n                                const emissiveColor = ftModel.userData.heatColor.setRGB(\n                                    heatRatio * 2.5 * (1 - coolingPulse * 0.72),\n                                    heatRatio * 0.35 + coolingPulse * 0.38,\n                                    coolingPulse * 1.8\n                                );\n                                ftModel.userData.barrelMat.emissive.copy(emissiveColor);\n                                ftModel.userData.barrelMat.emissiveIntensity = heatRatio * 3.0 + coolingPulse * 1.5;\n                                if (ftModel.userData.nozzleMat) {\n                                    ftModel.userData.nozzleMat.emissive.copy(emissiveColor);\n                                    ftModel.userData.nozzleMat.emissiveIntensity = heatRatio * 4.0 + coolingPulse * 2.4;\n                                }\n                            }\n                        }\n\n                        activeModel.position.set(\n                            basePos.x + swayX + bobX + rlOX + joltX,\n                            basePos.y + swayY + bobY - switchOffset + recoilLift + rlOY,\n                            basePos.z + weaponAnim.recoilAmount * weaponAnim.recoilMaxZ * recoilScale + rlOZ + joltZ\n                        );\n                        activeModel.rotation.x = baseRot.x + weaponAnim.recoilAmount * weaponAnim.recoilMaxRot * recoilScale + fireJolt * 0.035 + rlRX;\n                        activeModel.rotation.y = baseRot.y + rlRY;\n                        activeModel.rotation.z = baseRot.z + Math.sin(weaponAnim.recoilAmount * Math.PI) * 0.014 * recoilScale + joltRoll + rlRZ;\n                        if (muzzleFlash3D.visible) {\n                            const mOffset = effectScratch.v12.copy(muzzleOffsets[weaponIndex]).applyQuaternion(activeModel.quaternion);\n                            muzzleFlash3D.position.copy(activeModel.position).add(mOffset);\n                        }\n                    }\n                }\n\n                weaponStates.forEach((ws, i) => {\n                    const slot = document.createElement('div');\n                    slot.className = 'weapon-slot' + (i === 0 ? ' active' : '');\n                    slot.style.position = 'relative';\n                    slot.innerHTML = `<span class=\"slot-num\">${i + 1}</span>`;\n                    const iconEl = getWeaponIconElement(ws.icon, 38);\n                    iconEl.style.filter = 'brightness(0.85)';\n                    iconEl.classList && iconEl.classList.add('weapon-slot-icon');\n                    slot.appendChild(iconEl);\n                    slot.addEventListener('click', () => { input.switchTo = i; });\n                    slot.addEventListener('touchstart', (e) => { if (e.cancelable) e.preventDefault(); e.stopPropagation(); input.switchTo = i; }, { passive: false });\n                    weaponSlotsEl.appendChild(slot);\n                });\n                const weaponSlotEls = Array.from(weaponSlotsEl.children);\n                updateWeaponSlotsVisibility();\n                updateWeaponDisplay();\n\n                const shootContainer = document.getElementById('shoot-container');\n                const shootBtn = document.getElementById('shoot-btn');\n                const skillBtn = document.getElementById('skill-btn');\n\n                function triggerSlowTimeSkill() {\n                    if (!hasSlowTimeSkill || slowTimeActive || slowTimeCooldownTimer > 0 || !gameState.started || gameState.over || gamePaused || gameState.upgradeScreenOpen) return;\n                    slowTimeActive = true;\n                    slowTimeTimer = slowTimeDuration;\n                    playSound('wave_complete_sound', 0.85);\n                    if (skillBtn) {\n                        skillBtn.classList.add('active');\n                        skillBtn.querySelector('.skill-label').textContent = 'ACTIVE';\n                    }\n                }\n\n                if (skillBtn) {\n                    skillBtn.style.display = hasSlowTimeSkill ? 'flex' : 'none';\n                    skillBtn.addEventListener('click', (e) => {\n                        e.stopPropagation();\n                        triggerSlowTimeSkill();\n                    });\n                    skillBtn.addEventListener('touchstart', (e) => {\n                        if (e.cancelable) e.preventDefault();\n                        e.stopPropagation();\n                        triggerSlowTimeSkill();\n                    }, { passive: false });\n                }\n\n                if (isTouchDevice) {\n                    reloadBtn.style.display = 'flex';\n                    shootBtn.style.display = 'flex';\n\n                    reloadBtn.addEventListener('touchstart', (e) => { if (e.cancelable) e.preventDefault(); e.stopPropagation(); input.reload = true; }, { passive: false });\n\n                    let shootTouchId = null;\n\n                    shootBtn.addEventListener('touchstart', (e) => {\n                        if (e.cancelable) e.preventDefault();\n                        e.stopPropagation();\n                        if (gameState.upgradeScreenOpen || weaponSelectOpen || gamePaused) return;\n\n                        input.firing = true;\n                        input.holdingFire = true;\n                        shootBtn.classList.add('active');\n\n                        const touch = e.changedTouches[0];\n                        shootTouchId = touch.identifier;\n                        const pShoot = touchToGamePoint(touch);\n\n                        shootJoystickVisible = true;\n                        shootJoystickDirty = true;\n                        shootJoystickVisual.baseX = pShoot.x;\n                        shootJoystickVisual.baseY = pShoot.y;\n                        shootJoystickVisual.dx = 0;\n                        shootJoystickVisual.dy = 0;\n\n                        if (rightTouch === null) {\n                            rightTouch = touch.identifier;\n                            rightJoystickBase.x = pShoot.x;\n                            rightJoystickBase.y = pShoot.y;\n                            touchStarts[touch.identifier] = { x: pShoot.x, y: pShoot.y, time: performance.now(), moved: false, maxMoveSq: 0, canTapFire: false };\n                        }\n                    }, { passive: false });\n\n                    shootBtn.addEventListener('touchmove', (e) => {\n                        if (e.cancelable) e.preventDefault();\n                        e.stopPropagation();\n                        if (!gameState.started || gameState.over || gamePaused) return;\n                        for (let touch of e.changedTouches) {\n                            if (touch.identifier === shootTouchId) {\n                                const pShoot = touchToGamePoint(touch);\n                                let dx = pShoot.x - shootJoystickVisual.baseX;\n                                let dy = pShoot.y - shootJoystickVisual.baseY;\n                                const dist = Math.sqrt(dx * dx + dy * dy), maxR = 60;\n                                if (dist > maxR) {\n                                    shootJoystickVisual.baseX = pShoot.x - (dx / dist) * maxR;\n                                    shootJoystickVisual.baseY = pShoot.y - (dy / dist) * maxR;\n                                    dx = pShoot.x - shootJoystickVisual.baseX;\n                                    dy = pShoot.y - shootJoystickVisual.baseY;\n                                }\n                                shootJoystickVisual.dx = dx;\n                                shootJoystickVisual.dy = dy;\n                                shootJoystickDirty = true;\n                            }\n                            if (touch.identifier === rightTouch) {\n                                const p = touchToGamePoint(touch);\n                                touchMoveState.rightX = p.x;\n                                touchMoveState.rightY = p.y;\n                                touchMoveState.rightDirty = true;\n                                if (touchStarts[touch.identifier]) markTouchMoved(touchStarts[touch.identifier], p.x, p.y);\n                            }\n                        }\n                    }, { passive: false });\n\n                    const endShootTouch = (e) => {\n                        e.stopPropagation();\n                        for (let touch of e.changedTouches) {\n                            if (touch.identifier === shootTouchId) {\n                                input.holdingFire = false;\n                                shootBtn.classList.remove('active');\n                                shootTouchId = null;\n\n                                shootJoystickVisible = false;\n                                shootJoystickDirty = true;\n                                shootJoystickVisual.dx = 0;\n                                shootJoystickVisual.dy = 0;\n                            }\n                            if (touch.identifier === rightTouch) {\n                                rightTouch = null;\n                                touchMoveState.rightDirty = false;\n                                if (touchStarts[touch.identifier]) delete touchStarts[touch.identifier];\n                            }\n                        }\n                    };\n\n                    shootBtn.addEventListener('touchend', endShootTouch, { passive: true });\n                    shootBtn.addEventListener('touchcancel', endShootTouch, { passive: true });\n                }\n\n                const scopeBtn = document.getElementById('scope-btn');\n                if (isTouchDevice) {\n                    scopeBtn.addEventListener('touchstart', (e) => {\n                        if (e.cancelable) e.preventDefault();\n                        e.stopPropagation();\n                        const wComp = world.getComponent(playerEntity, 'Weapon');\n                        const ws = weaponStates[wComp.index];\n                        if (ws.name === 'SNIPER') {\n                            toggleSniperScope(!sniperScoped);\n                            scopeBtn.classList.toggle('scoped', sniperScoped);\n                        }\n                    }, { passive: false });\n                }\n\n                function updateScopeBtnVisibility() {\n                    if (!isTouchDevice) return;\n                    const wComp = world.getComponent(playerEntity, 'Weapon');\n                    const ws = weaponStates[wComp.index];\n                    scopeBtn.style.display = ws.name === 'SNIPER' ? 'flex' : 'none';\n                    scopeBtn.classList.toggle('scoped', sniperScoped);\n                }\n\n                function updateWeaponDisplay() {\n                    const wComp = world.getComponent(playerEntity, 'Weapon');\n                    const ws = weaponStates[wComp.index];\n                    const displayName = getWeaponDisplayName(ws);\n                    \n                    let weaponName;\n                    if (wComp.isReloading) {\n                        if (wComp.index === 7) {\n                            weaponName = currentLocale === 'zh' ? '\u706b\u7130\u55b7\u5c04\u5668 \u51b7\u5374\u4e2d' : 'FLAMETHROWER COOLING';\n                        } else {\n                            weaponName = t('hud.reloadSuffix', { weapon: displayName });\n                        }\n                    } else {\n                        weaponName = displayName;\n                    }\n\n                    const ammoLabel = ws.isMelee\n                        ? '\u221e'\n                        : wComp.isReloading\n                            ? t('hud.seconds', { value: Math.max(0, Math.ceil(wComp.reloadTimer * 10) / 10) })\n                            : (ws.mag === Infinity ? '\u221e' : (ws.ammo === Infinity ? (ws.mag + ' / \u221e') : (ws.mag + ' / ' + ws.ammo)));\n                    const lowAmmo = !ws.isMelee && ws.mag <= Math.ceil(ws.magSize * 0.25) && ws.ammo !== Infinity;\n                    const weaponKey = `${wComp.index}|${weaponName}|${ammoLabel}`;\n\n                    if (uiState.weaponKey !== weaponKey) {\n                        uiState.weaponKey = weaponKey;\n                        weaponNameEl.textContent = weaponName;\n                        ammoText.textContent = ammoLabel;\n                    }\n                    if (uiState.lowAmmo !== lowAmmo) {\n                        uiState.lowAmmo = lowAmmo;\n                        ammoDisplay.classList.toggle('low', lowAmmo);\n                    }\n                    if (uiState.activeWeapon !== wComp.index) {\n                        uiState.activeWeapon = wComp.index;\n                        weaponSlotEls.forEach((s, i) => s.classList.toggle('active', i === wComp.index));\n                    }\n\n                    // Flamethrower Heat Bar UI toggle and fill update\n                    const heatBar = document.getElementById('flame-heat-bar');\n                    if (wComp.index === 7) {\n                        ammoText.style.display = 'none';\n                        if (heatBar) {\n                            heatBar.style.display = 'block';\n                            const heatPercent = clamp01((wComp.flameHeat || 0) / (ws.magSize || 100)) * 100;\n                            const fill = document.getElementById('flame-heat-fill');\n                            if (fill) {\n                                fill.style.width = `${heatPercent}%`;\n                                if (wComp.isReloading) {\n                                    fill.style.background = 'linear-gradient(90deg, #00d9ff, #0088ff)'; // Blue cooling color!\n                                    fill.style.boxShadow = '0 0 8px #00d9ff';\n                                } else {\n                                    fill.style.background = 'linear-gradient(90deg, #ff9900, #ff0000)';\n                                    fill.style.boxShadow = '0 0 8px #ff3300';\n                                }\n                            }\n                        }\n                    } else {\n                        ammoText.style.display = '';\n                        if (heatBar) heatBar.style.display = 'none';\n                    }\n\n                    // Center reload indicator with caching to prevent style invalidation during normal gameplay\n                    if (wComp.isReloading) {\n                        const remaining = Math.max(0, Math.ceil(wComp.reloadTimer * 10) / 10);\n                        let text;\n                        if (wComp.index === 7) {\n                            text = (currentLocale === 'zh' ? '\u706b\u7130\u55b7\u5c04\u5668 \u51b7\u5374\u4e2d' : 'FLAMETHROWER COOLING') + '  ' + remaining.toFixed(1) + 's';\n                        } else {\n                            text = t('hud.reloadSuffix', { weapon: displayName }) + '  ' + remaining.toFixed(1) + 's';\n                        }\n                        if (reloadIndicator.textContent !== text) {\n                            reloadIndicator.textContent = text;\n                        }\n                        if (!uiState.isReloadIndicatorActive) {\n                            uiState.isReloadIndicatorActive = true;\n                            reloadIndicator.classList.add('active');\n                        }\n                    } else {\n                        if (uiState.isReloadIndicatorActive) {\n                            uiState.isReloadIndicatorActive = false;\n                            reloadIndicator.classList.remove('active');\n                        }\n                    }\n                }\n\n                function showHitMarker(kind = 'body') {\n                    clearRuntimeTimeout(hitMarkerTimer);\n                    hitMarkerPulseFlip = !hitMarkerPulseFlip;\n                    hitMarker.classList.remove('body', 'headshot', 'kill', 'pulse-a', 'pulse-b');\n                    crosshair.classList.remove('body', 'headshot', 'kill', 'hit');\n                    hitMarker.classList.add(kind, hitMarkerPulseFlip ? 'pulse-a' : 'pulse-b');\n                    hitMarker.classList.add('active');\n                    weaponAnim.crosshairHitPulse = kind === 'headshot' ? 1.05 : (kind === 'kill' ? 1.25 : 0.78);\n                    if ((kind === 'headshot' || kind === 'kill') && !weaponAnim._dingPlayedFrame) {\n                        weaponAnim._dingPlayedFrame = gameState.frameId;\n                        playSound('headshot_ding_sound', 0.7);\n                    }\n                    hitMarkerTimer = scheduleRuntimeTimeout(() => {\n                        hitMarkerTimer = null;\n                        hitMarker.classList.remove('active', 'body', 'headshot', 'kill', 'pulse-a', 'pulse-b');\n                    }, kind === 'kill' ? 210 : 150);\n                }\n\n                function hitMarkerPriority(kind) {\n                    return kind === 'kill' ? 3 : (kind === 'headshot' ? 2 : 1);\n                }\n\n                function queueHitMarker(kind, frameId) {\n                    if (uiState.hitMarkerFrame === frameId) {\n                        if (hitMarkerPriority(kind) <= hitMarkerPriority(uiState.hitMarkerKind)) return;\n                    }\n                    uiState.hitMarkerFrame = frameId;\n                    uiState.hitMarkerKind = kind;\n                    showHitMarker(kind);\n                }\n\n                // ===== INPUT HANDLERS =====\n                document.addEventListener('keydown', (e) => {\n                    if (gameState.over || !gameState.started) return;\n                    if (gamePaused) return;\n\n                    // Key 'Q' or 'F' triggers slow time skill\n                    if (e.key.toLowerCase() === 'q' || e.key.toLowerCase() === 'f') {\n                        e.preventDefault();\n                        triggerSlowTimeSkill();\n                        return;\n                    }\n\n                    // Handle upgrade selection with keyboard (1/2/3)\n                    if (gameState.upgradeScreenOpen && e.key >= '1' && e.key <= '3') {\n                        e.preventDefault();\n                        if (gameState.upgradeSelecting) return;\n                        const idx = parseInt(e.key) - 1;\n                        handleUpgradeCardSelect(idx);\n                        return;\n                    }\n                    input.keys[e.key.toLowerCase()] = true; e.preventDefault();\n                    if (e.key >= '1' && e.key <= '9') {\n                        const keyNum = parseInt(e.key);\n                        const ownershipIdx = keyNum - 1;\n                        if (ownershipIdx < roguelikeState.ownedWeapons.length) {\n                            input.switchTo = roguelikeState.ownedWeapons[ownershipIdx];\n                        }\n                    }\n                    if (e.key.toLowerCase() === 'r') input.reload = true;\n\n                });\n                document.addEventListener('keyup', (e) => { input.keys[e.key.toLowerCase()] = false; });\n                window.addEventListener('blur', () => {\n                    for (let k in input.keys) input.keys[k] = false;\n                    if (typeof cancelActiveTouchControls === 'function') cancelActiveTouchControls();\n                });\n\n                function safeRequestPointerLock(el) {\n                    if (!el || typeof el.requestPointerLock !== 'function') return;\n                    try {\n                        const promise = el.requestPointerLock();\n                        if (promise && typeof promise.catch === 'function') {\n                            promise.catch((err) => {\n                                lib.log('Pointer lock request rejected: ' + err.message);\n                            });\n                        }\n                    } catch (err) {\n                        lib.log('Pointer lock request failed: ' + err.message);\n                    }\n                }\n\n                canvas.addEventListener('click', () => { if (gameState.started && !gameState.over && !gameState.upgradeScreenOpen && !pointerLocked && !isTouchDevice) safeRequestPointerLock(canvas); });\n                document.addEventListener('pointerlockchange', () => { pointerLocked = document.pointerLockElement === canvas; });\n                document.addEventListener('mousemove', (e) => { if (!pointerLocked || !gameState.started || gameState.over) return; const sens = sniperScoped ? 0.0006 : 0.002; input.yawDelta -= e.movementX * sens; input.pitchDelta -= e.movementY * sens; });\n                document.addEventListener('mousedown', (e) => {\n                    if (gameState.started && !gameState.over && !gameState.upgradeScreenOpen && !gamePaused && pointerLocked) {\n                        if (!weaponSelectOpen) {\n                            if (e.button === 0) { input.firing = true; input.holdingFire = true; }\n                        }\n                        if (e.button === 2) { toggleSniperScope(true); }\n                    }\n                });\n                document.addEventListener('mouseup', (e) => {\n                    if (e.button === 0) { input.holdingFire = false; }\n                    if (e.button === 2) { toggleSniperScope(false); }\n                });\n                canvas.addEventListener('contextmenu', (e) => e.preventDefault());\n\n                function toggleSniperScope(on) {\n                    const wComp = world.getComponent(playerEntity, 'Weapon');\n                    const ws = weaponStates[wComp.index];\n                    const sb = document.getElementById('scope-btn');\n                    if (ws.name !== 'SNIPER') { sniperScoped = false; scopeOverlay.style.display = 'none'; crosshair.style.display = ''; weaponHolder.visible = true; if (sb) { sb.style.display = 'none'; sb.classList.remove('scoped'); } return; }\n                    sniperScoped = on;\n                    if (on) {\n                        scopeOverlay.style.display = 'block';\n                        crosshair.style.display = 'none';\n                        weaponHolder.visible = false;\n                    } else {\n                        scopeOverlay.style.display = 'none';\n                        crosshair.style.display = '';\n                        weaponHolder.visible = true;\n                    }\n                    if (sb) sb.classList.toggle('scoped', sniperScoped);\n                }\n\n                function playSmartPistolLockSound(isFullLock) {\n                try {\n                    const ctx = initAudio();\n                    if (!ctx) return;\n                    if (ctx.state === 'suspended') ctx.resume();\n\n                    const now = ctx.currentTime;\n                    // \u63d0\u9ad8\u57fa\u7840\u97f3\u91cf\u4ee5\u89e3\u51b3\u7528\u6237\u53cd\u9988\u542c\u4e0d\u89c1\u7684\u95ee\u9898\n                    const volume = (isFullLock ? 0.68 : 0.45) * getCategoryVolume('headshot_ding_sound');\n                    if (volume <= 0.001) return;\n\n                    const playBeep = (freq, startTime, duration) => {\n                        const osc = ctx.createOscillator();\n                        const gain = ctx.createGain();\n                        \n                        osc.type = 'triangle';\n                        osc.frequency.setValueAtTime(freq, startTime);\n                        \n                        gain.gain.setValueAtTime(0, startTime);\n                        // \u589e\u52a0\u521d\u59cb\u589e\u76ca\u659c\u7387\uff0c\u4f7f\u58f0\u97f3\u66f4\u6709\u7a7f\u900f\u529b\n                        gain.gain.linearRampToValueAtTime(volume * 0.7, startTime + 0.006);\n                        gain.gain.exponentialRampToValueAtTime(0.0001, startTime + duration);\n                        \n                        osc.connect(gain);\n                        if (masterCompressor) gain.connect(masterCompressor);\n                        else gain.connect(ctx.destination);\n                        \n                        osc.start(startTime);\n                        osc.stop(startTime + duration + 0.01);\n                    };\n\n                    if (isFullLock) {\n                        playBeep(1650, now, 0.05);\n                        playBeep(2100, now + 0.065, 0.06);\n                    } else {\n                        playBeep(1800, now, 0.05);\n                    }\n                } catch (e) {\n                    // \u540c\u6837\u63d0\u9ad8\u56de\u9000\u97f3\u6548\u7684\u97f3\u91cf\n                    playSound('headshot_ding_sound', isFullLock ? 0.6 : 0.4);\n                }\n            }\n\n\t            function clearSmartPistolRuntime(clearHud = true) {\n\t                    smartPistolTargetCount = 0;\n\t                    smartPistolLockCount = 0;\n\t                    smartPistolQueueHead = 0;\n\t                    smartPistolQueueCount = 0;\n\t                    smartLockIndexBySlot.fill(-1);\n\t                    smartSelectedCandidateIndex.fill(-1);\n\t                    smartActiveSlotFlags.fill(0);\n\t                    lastSmartPistolLockTime = 0;\n                    if (!clearHud) return;\n                    const smartContainer = smartContainerEl || document.getElementById('smart-lock-container');\n                    const crosshairEl = document.getElementById('crosshair');\n                    if (smartContainer) {\n                        if (smartMarkerPool) {\n                            // Pool is active: return all cached markers to pool instead of clearing innerHTML.\n                            for (const [, m] of smartMarkerCache.entries()) {\n                                m.style.display = 'none';\n                                smartMarkerPool.push(m);\n                            }\n                        } else {\n                            smartContainer.innerHTML = '';\n                        }\n                    }\n                    if (crosshairEl) {\n                        crosshairEl.classList.remove('smart-hud', 'locking', 'locked');\n                        crosshairEl.style.width = '';\n                        crosshairEl.style.height = '';\n                    }\n                    smartMarkerCache.clear();\n                    lastLockDiameter = 0;\n                    lastSmartHudState = '';\n                }\n\n                function isSmartPistolTargetAlive(slot) {\n                    return slot >= 0 &&\n                        !zombieRuntime.deadPending[slot] &&\n                        zombieRuntime.instanceSlot[slot] >= 0 &&\n                        zombieRuntime.currentHp[slot] > 0;\n                }\n\n                function isSmartProjectileTargetValid(projectileSlot) {\n                    const targetSlot = projectileSmartTargetSlot[projectileSlot];\n                    return isSmartPistolTargetAlive(targetSlot) &&\n                        zombieRuntime.entityId[targetSlot] === projectileSmartTargetEntityId[projectileSlot];\n                }\n\n                function getSmartPistolRequiredLocks(slot) {\n                    const typeName = zombieRuntime.typeName[slot];\n                    if (zombieRuntime.isBoss[slot]) {\n                        if (typeName === 'BRUTE') return 6;\n                        if (typeName === 'CHARGER') return 8;\n                        if (typeName === 'BEHEMOTH') return 10;\n                        if (typeName === 'INFERNAL') return 10;\n                        if (typeName === 'OVERLORD') return 12;\n                        return 6; // fallback boss\n                    }\n                    if (typeName === 'Walker' || typeName === 'Crawler' || typeName === 'Swarm') return 1;\n                    if (typeName === 'Runner' || typeName === 'Screamer') return 2;\n                    if (typeName === 'Stalker' || typeName === 'Berserker') return 3;\n                    if (typeName === 'Tank' || typeName === 'Bloater') return 4;\n                    if (typeName === 'Armored') return 5;\n                    return 1; // fallback regular\n                }\n\n                function getSmartPistolTargetPoint(slot, out) {\n                    const zScale = getZombieSlotHitScale(slot);\n                    const targetY = zombieRuntime.y[slot] + (zombieSlotPartAlive(slot, 'head') ? 1.92 : 1.18) * zScale;\n                    return out.set(zombieRuntime.x[slot], targetY, zombieRuntime.z[slot]);\n                }\n\n                function isSmartPistolMobileHud() {\n                    return detectedTouchDevice;\n                }\n\n                function getSmartPistolLockRadiusPx() {\n                    const shortSide = Math.min(cachedGameWidth || 1, cachedGameHeight || 1);\n                    if (isSmartPistolMobileHud()) {\n                        const mobileRadius = shortSide * (cachedIsLandscape ? 0.38 : 0.37);\n                        return Math.max(126, Math.min(232, mobileRadius));\n                    }\n                    return Math.max(145, Math.min(330, shortSide * 0.34));\n                }\n\n                function getSmartPistolHeadMarkerSizePx(slot, screenHeight, dist) {\n                    const zScale = getZombieSlotHitScale(slot);\n                    const targetY = zombieRuntime.y[slot] + (zombieSlotPartAlive(slot, 'head') ? 1.92 : 1.18) * zScale;\n                    const headHalfHeight = (zombieSlotPartAlive(slot, 'head') ? 0.28 : 0.34) * zScale;\n                    const headTop = effectScratch.v8.set(zombieRuntime.x[slot], targetY + headHalfHeight, zombieRuntime.z[slot]);\n                    const headBottom = effectScratch.v9.set(zombieRuntime.x[slot], targetY - headHalfHeight, zombieRuntime.z[slot]);\n                    headTop.project(camera);\n                    headBottom.project(camera);\n\n                    const topY = (-(headTop.y * 0.5) + 0.5) * screenHeight;\n                    const bottomY = (-(headBottom.y * 0.5) + 0.5) * screenHeight;\n                    if (isSmartPistolMobileHud()) {\n                        const projectedSize = Math.abs(bottomY - topY) * 1.18;\n                        const distanceFallback = 34 / Math.max(0.75, dist * 0.13);\n                        const maxSize = cachedIsLandscape ? 42 : 44;\n                        return Math.max(22, Math.min(maxSize, Math.max(projectedSize, distanceFallback)));\n                    }\n                    const projectedSize = Math.abs(bottomY - topY) * 1.45;\n                    const distanceFallback = 42 / Math.max(0.7, dist * 0.12);\n                    return Math.max(24, Math.min(58, Math.max(projectedSize, distanceFallback)));\n                }\n\n\t                function hasSmartPistolLineOfSight(origin, targetPoint) {\n\t                    const toTarget = effectScratch.v12.copy(targetPoint).sub(origin);\n\t                    const dist = toTarget.length();\n\t                    if (dist <= 0.2) return true;\n\t                    toTarget.divideScalar(dist);\n\t                    return !getNearestWallHit(origin, toTarget, Math.max(0.05, dist - 0.35));\n\t                }\n\n\t                function writeSmartPistolLock(index, slot, progress, requiredLocks, screenX, screenY, markerSize, score, centerNorm, dist, lostTime) {\n\t                    smartLockSlot[index] = slot;\n\t                    smartLockProgress[index] = progress;\n\t                    smartLockRequiredLocks[index] = requiredLocks;\n\t                    smartLockScreenX[index] = screenX;\n\t                    smartLockScreenY[index] = screenY;\n\t                    smartLockMarkerSize[index] = markerSize;\n\t                    smartLockScore[index] = score;\n\t                    smartLockCenterNorm[index] = centerNorm;\n\t                    smartLockDist[index] = dist;\n\t                    smartLockLostTime[index] = lostTime;\n\t                }\n\n\t                function moveSmartPistolLock(from, to) {\n\t                    if (from === to) return;\n\t                    smartLockSlot[to] = smartLockSlot[from];\n\t                    smartLockProgress[to] = smartLockProgress[from];\n\t                    smartLockRequiredLocks[to] = smartLockRequiredLocks[from];\n\t                    smartLockScreenX[to] = smartLockScreenX[from];\n\t                    smartLockScreenY[to] = smartLockScreenY[from];\n\t                    smartLockMarkerSize[to] = smartLockMarkerSize[from];\n\t                    smartLockScore[to] = smartLockScore[from];\n\t                    smartLockCenterNorm[to] = smartLockCenterNorm[from];\n\t                    smartLockDist[to] = smartLockDist[from];\n\t                    smartLockLostTime[to] = smartLockLostTime[from];\n\t                }\n\n\t                function swapSmartPistolLocks(a, b) {\n\t                    const slot = smartLockSlot[a];\n\t                    const progress = smartLockProgress[a];\n\t                    const requiredLocks = smartLockRequiredLocks[a];\n\t                    const screenX = smartLockScreenX[a];\n\t                    const screenY = smartLockScreenY[a];\n\t                    const markerSize = smartLockMarkerSize[a];\n\t                    const score = smartLockScore[a];\n\t                    const centerNorm = smartLockCenterNorm[a];\n\t                    const dist = smartLockDist[a];\n\t                    const lostTime = smartLockLostTime[a];\n\t                    moveSmartPistolLock(b, a);\n\t                    smartLockSlot[b] = slot;\n\t                    smartLockProgress[b] = progress;\n\t                    smartLockRequiredLocks[b] = requiredLocks;\n\t                    smartLockScreenX[b] = screenX;\n\t                    smartLockScreenY[b] = screenY;\n\t                    smartLockMarkerSize[b] = markerSize;\n\t                    smartLockScore[b] = score;\n\t                    smartLockCenterNorm[b] = centerNorm;\n\t                    smartLockDist[b] = dist;\n\t                    smartLockLostTime[b] = lostTime;\n\t                }\n\n\t                function smartLockComesBefore(a, b) {\n\t                    const aFull = smartLockProgress[a] >= smartLockRequiredLocks[a] - 0.001 ? 1 : 0;\n\t                    const bFull = smartLockProgress[b] >= smartLockRequiredLocks[b] - 0.001 ? 1 : 0;\n\t                    if (aFull !== bFull) return aFull > bFull;\n\t                    return smartLockScore[a] < smartLockScore[b];\n\t                }\n\n\t                function sortSmartPistolLocks() {\n\t                    for (let i = 1; i < smartPistolLockCount; i++) {\n\t                        let j = i;\n\t                        while (j > 0 && smartLockComesBefore(j, j - 1)) {\n\t                            swapSmartPistolLocks(j, j - 1);\n\t                            j--;\n\t                        }\n\t                    }\n\t                }\n\n\t                function sortSmartCandidateOrder(count) {\n\t                    for (let i = 1; i < count; i++) {\n\t                        const key = smartCandidateOrder[i];\n\t                        const keyScore = smartCandidateScore[key];\n\t                        let j = i - 1;\n\t                        while (j >= 0 && smartCandidateScore[smartCandidateOrder[j]] > keyScore) {\n\t                            smartCandidateOrder[j + 1] = smartCandidateOrder[j];\n\t                            j--;\n\t                        }\n\t                        smartCandidateOrder[j + 1] = key;\n\t                    }\n\t                }\n\n\t                function smartReadyLockComesBefore(a, b) {\n\t                    const aFull = smartLockProgress[a] >= smartLockRequiredLocks[a] - 0.001 ? 1 : 0;\n\t                    const bFull = smartLockProgress[b] >= smartLockRequiredLocks[b] - 0.001 ? 1 : 0;\n\t                    if (aFull !== bFull) return aFull > bFull;\n\t                    const aLocks = Math.floor(smartLockProgress[a] + 0.0001);\n\t                    const bLocks = Math.floor(smartLockProgress[b] + 0.0001);\n\t                    if (aLocks !== bLocks) return aLocks > bLocks;\n\t                    return smartLockScore[a] < smartLockScore[b];\n\t                }\n\n\t                function sortSmartReadyLockOrder(count) {\n\t                    for (let i = 1; i < count; i++) {\n\t                        const key = smartReadyLockOrder[i];\n\t                        let j = i - 1;\n\t                        while (j >= 0 && smartReadyLockComesBefore(key, smartReadyLockOrder[j])) {\n\t                            smartReadyLockOrder[j + 1] = smartReadyLockOrder[j];\n\t                            j--;\n\t                        }\n\t                        smartReadyLockOrder[j + 1] = key;\n\t                    }\n\t                }\n\n\t                function refreshSmartPistolTargetList() {\n\t                    smartPistolTargetCount = 0;\n\t                    for (let i = 0; i < smartPistolLockCount; i++) {\n\t                        const slot = smartLockSlot[i];\n\t                        if (isSmartPistolTargetAlive(slot) && Math.floor(smartLockProgress[i] + 0.0001) > 0) {\n\t                            smartPistolTargets[smartPistolTargetCount++] = slot;\n\t                        }\n\t                    }\n\t                }\n\n\t                function consumeSmartPistolLocks(maxShots) {\n\t                    smartPistolQueueHead = 0;\n\t                    smartPistolQueueCount = 0;\n\t                    if (maxShots <= 0) return;\n\n\t                    let readyCount = 0;\n\t                    for (let i = 0; i < smartPistolLockCount; i++) {\n\t                        const slot = smartLockSlot[i];\n\t                        if (isSmartPistolTargetAlive(slot) && Math.floor(smartLockProgress[i] + 0.0001) > 0) {\n\t                            smartReadyLockOrder[readyCount++] = i;\n\t                        }\n\t                    }\n\t                    sortSmartReadyLockOrder(readyCount);\n\n\t                    let shotsLeft = maxShots;\n\t                    for (let i = 0; i < readyCount && shotsLeft > 0; i++) {\n\t                        const lockIndex = smartReadyLockOrder[i];\n\t                        const acquiredLocks = Math.min(smartLockRequiredLocks[lockIndex], Math.floor(smartLockProgress[lockIndex] + 0.0001));\n\t                        const shots = Math.min(acquiredLocks, shotsLeft);\n\t                        if (shots <= 0) continue;\n\t                        const fullLock = smartLockProgress[lockIndex] >= smartLockRequiredLocks[lockIndex] - 0.001;\n\t                        for (let s = 0; s < shots && smartPistolQueueCount < SMART_PISTOL_QUEUE_CAPACITY; s++) {\n\t                            const queueIndex = smartPistolQueueCount++;\n\t                            smartPistolQueueSlot[queueIndex] = smartLockSlot[lockIndex];\n\t                            smartPistolQueueEntityId[queueIndex] = zombieRuntime.entityId[smartLockSlot[lockIndex]];\n\t                            smartPistolQueueRequiredLocks[queueIndex] = smartLockRequiredLocks[lockIndex];\n\t                            smartPistolQueueFlags[queueIndex] = (fullLock ? SMART_QUEUE_FULL_LOCK : 0) | (fullLock && s === shots - 1 ? SMART_QUEUE_FINAL_SHOT : 0);\n\t                        }\n\t                        smartLockProgress[lockIndex] = Math.max(0, smartLockProgress[lockIndex] - shots);\n\t                        smartLockLostTime[lockIndex] = 0;\n\t                        shotsLeft -= shots;\n\t                    }\n\n\t                    smartLockIndexBySlot.fill(-1);\n\t                    let writeIndex = 0;\n\t                    for (let i = 0; i < smartPistolLockCount; i++) {\n\t                        const slot = smartLockSlot[i];\n\t                        if (isSmartPistolTargetAlive(slot) && smartLockProgress[i] > 0.04) {\n\t                            moveSmartPistolLock(i, writeIndex);\n\t                            smartLockIndexBySlot[slot] = writeIndex;\n\t                            writeIndex++;\n\t                        }\n\t                    }\n\t                    smartPistolLockCount = writeIndex;\n\t                    refreshSmartPistolTargetList();\n\t                }\n\n\t                function hasSmartPistolQueuedShot() {\n\t                    return smartPistolQueueHead < smartPistolQueueCount;\n\t                }\n\n\t                function getNextSmartPistolQueuedShot() {\n\t                    while (smartPistolQueueHead < smartPistolQueueCount) {\n\t                        const queueIndex = smartPistolQueueHead++;\n\t                        const slot = smartPistolQueueSlot[queueIndex];\n\t                        const entityId = smartPistolQueueEntityId[queueIndex];\n\t                        if (isSmartPistolTargetAlive(slot) && zombieRuntime.entityId[slot] === entityId) {\n\t                            const flags = smartPistolQueueFlags[queueIndex];\n\t                            smartPistolShotScratch.slot = slot;\n\t                            smartPistolShotScratch.entityId = entityId;\n\t                            smartPistolShotScratch.fullLock = (flags & SMART_QUEUE_FULL_LOCK) !== 0;\n\t                            smartPistolShotScratch.requiredLocks = smartPistolQueueRequiredLocks[queueIndex] || 1;\n\t                            smartPistolShotScratch.finalShot = (flags & SMART_QUEUE_FINAL_SHOT) !== 0;\n\t                            return smartPistolShotScratch;\n\t                        }\n\t                    }\n\t                    smartPistolQueueHead = 0;\n\t                    smartPistolQueueCount = 0;\n\t                    return null;\n\t                }\n\n                function getSmartPistolProjectileProfile(ws) {\n                    const profile = weaponFireScratch.smartPistolWs || (weaponFireScratch.smartPistolWs = {});\n                    Object.assign(profile, ws);\n                    profile.bulletSpeed = SMART_PISTOL_BULLET_SPEED;\n                    profile.bulletSize = SMART_PISTOL_BULLET_SIZE;\n                    profile.bulletLength = SMART_PISTOL_BULLET_LENGTH;\n                    profile.bulletColor = SMART_PISTOL_TRACER_COLOR;\n                    // A homing shot owns exactly one locked target. Letting it penetrate\n                    // would leave it alive after that target is recorded as hit, producing\n                    // stray wall impacts or orbiting projectiles.\n                    profile.penetration = 0;\n                    profile.damageDropoff = 0;\n                    return profile;\n                }\n\n\t                function updateSmartPistolLocks(w, ctx, dt = 0) {\n\t                    // Cache the container element reference to avoid repeated getElementById calls.\n\t                    const smartContainer = smartContainerEl || (smartContainerEl = document.getElementById('smart-lock-container'));\n\t                    const crosshairEl = document.getElementById('crosshair');\n\t                    if (!smartContainer || !crosshairEl) return;\n\t                    // Lazily pre-allocate the marker DOM pool on first call.\n\t                    if (!smartMarkerPool) {\n\t                        smartMarkerPool = [];\n\t                        for (let _p = 0; _p < 48; _p++) {\n\t                            const _pe = document.createElement('div');\n\t                            _pe.innerHTML = '<span class=\"smart-lock-count\"></span><span class=\"smart-lock-progress\"></span>';\n\t                            _pe._countEl = _pe.querySelector('.smart-lock-count');\n\t                            _pe.style.display = 'none';\n\t                            smartContainer.appendChild(_pe);\n\t                            smartMarkerPool.push(_pe);\n\t                        }\n\t                    }\n\n\t                    const wComp = w.getComponent(playerEntity, 'Weapon');\n\t                    if (!wComp) return;\n\t                    const ws = weaponStates[wComp.index];\n\n\t                    if (wComp.index !== 0 || !hasSmartPistol || gameState.over || !gameState.started || gamePaused || gameState.upgradeScreenOpen || wComp.isReloading) {\n\t                        clearSmartPistolRuntime(true);\n\t                        return;\n\t                    }\n\n\t                    if (!crosshairEl.classList.contains('smart-hud')) {\n\t                        crosshairEl.classList.add('smart-hud');\n\t                    }\n\n\t                    const pos = w.getComponent(playerEntity, 'Position');\n\t                    const maxTargets = Math.min(SMART_PISTOL_MAX_TARGETS, ws.mag);\n\n\t                    if (maxTargets <= 0) {\n\t                        clearSmartPistolRuntime(true);\n\t                        return;\n\t                    }\n\n\t                    const grid = getZombieSpatialIndex(ctx, w);\n\t                    const queryRange = SMART_PISTOL_RANGE + 4;\n\t                    const zombieSlots = grid.querySlotsAABB(pos.x - queryRange, pos.x + queryRange, pos.z - queryRange, pos.z + queryRange, ctx.bulletCandidates || (ctx.bulletCandidates = []));\n\t                    let validCandidateCount = 0;\n\n\t                    camera.getWorldDirection(effectScratch.viewDir);\n\t                    if (effectScratch.viewDir.lengthSq() < 0.0001) effectScratch.viewDir.set(0, 0, -1);\n\t                    else effectScratch.viewDir.normalize();\n\n\t                    const screenWidth = cachedGameWidth;\n\t                    const screenHeight = cachedGameHeight;\n\t                    const lockRadius = getSmartPistolLockRadiusPx();\n\t                    const centerX = screenWidth / 2;\n\t                    const centerY = screenHeight / 2;\n\n\t                    const lockDiameter = 2 * lockRadius;\n\t                    if (lockDiameter !== lastLockDiameter) {\n\t                        lastLockDiameter = lockDiameter;\n\t                        crosshairEl.style.width = `${lockDiameter}px`;\n\t                        crosshairEl.style.height = `${lockDiameter}px`;\n\t                    }\n\n\t                    for (let i = 0; i < zombieSlots.length; i++) {\n\t                        const zSlot = zombieSlots[i];\n\t                        if (!isSmartPistolTargetAlive(zSlot)) continue;\n\t                        const zx = zombieRuntime.x[zSlot];\n\t                        const zz = zombieRuntime.z[zSlot];\n\n\t                        const dx = zx - pos.x;\n\t                        const dz = zz - pos.z;\n\t                        const dist = Math.sqrt(dx * dx + dz * dz);\n\t                        if (dist > SMART_PISTOL_RANGE) continue;\n\n\t                        const targetPoint = getSmartPistolTargetPoint(zSlot, effectScratch.v10);\n\t                        const rel = effectScratch.v11.copy(targetPoint).sub(camera.position);\n\t                        if (rel.dot(effectScratch.viewDir) < 0.1) continue;\n\t                        if (!hasSmartPistolLineOfSight(camera.position, targetPoint)) continue;\n\n\t                        targetPoint.project(camera);\n\n\t                        const screenX = (targetPoint.x * 0.5 + 0.5) * screenWidth;\n\t                        const screenY = (-(targetPoint.y * 0.5) + 0.5) * screenHeight;\n\n\t                        const diffX = screenX - centerX;\n\t                        const diffY = screenY - centerY;\n\t                        const distFromCenter = Math.sqrt(diffX * diffX + diffY * diffY);\n\t                        const centerNorm = distFromCenter / lockRadius;\n\n\t                        if (centerNorm > 1) continue;\n\n\t                        const centerQuality = 1 - centerNorm;\n\t                        const distanceQuality = 1 - clamp01((dist - 6) / Math.max(1, SMART_PISTOL_RANGE - 6));\n\t                        const lockRate = (SMART_PISTOL_MIN_LOCK_RATE +\n\t                            centerQuality * SMART_PISTOL_CENTER_LOCK_RATE +\n\t                            distanceQuality * SMART_PISTOL_DISTANCE_LOCK_RATE) * smartPistolLockSpeedMultiplier;\n\t                        const requiredLocks = getSmartPistolRequiredLocks(zSlot);\n\t                        const markerSize = getSmartPistolHeadMarkerSizePx(zSlot, screenHeight, dist);\n\t                        if (validCandidateCount >= SMART_PISTOL_LOCK_CAPACITY) continue;\n\t                        const candidateIndex = validCandidateCount++;\n\t                        smartCandidateSlot[candidateIndex] = zSlot;\n\t                        smartCandidateRequiredLocks[candidateIndex] = requiredLocks;\n\t                        smartCandidateScore[candidateIndex] = centerNorm * 1.1 + (dist / SMART_PISTOL_RANGE) * 0.35 + requiredLocks * 0.04;\n\t                        smartCandidateScreenX[candidateIndex] = screenX;\n\t                        smartCandidateScreenY[candidateIndex] = screenY;\n\t                        smartCandidateMarkerSize[candidateIndex] = markerSize;\n\t                        smartCandidateCenterNorm[candidateIndex] = centerNorm;\n\t                        smartCandidateDist[candidateIndex] = dist;\n\t                        smartCandidateLockRate[candidateIndex] = lockRate;\n\t                        smartCandidateOrder[candidateIndex] = candidateIndex;\n\t                    }\n\n\t                    sortSmartCandidateOrder(validCandidateCount);\n\t                    const selectedCount = Math.min(maxTargets, validCandidateCount);\n\t                    const currentTime = performance.now();\n\t                    smartSelectedCandidateIndex.fill(-1);\n\t                    for (let i = 0; i < selectedCount; i++) {\n\t                        const candidateIndex = smartCandidateOrder[i];\n\t                        smartSelectedCandidateIndex[smartCandidateSlot[candidateIndex]] = candidateIndex;\n\t                    }\n\n\t                    smartLockIndexBySlot.fill(-1);\n\t                    let lockWriteIndex = 0;\n\t                    for (let i = 0; i < smartPistolLockCount; i++) {\n\t                        const slot = smartLockSlot[i];\n\t                        if (!isSmartPistolTargetAlive(slot)) continue;\n\t                        const candidateIndex = smartSelectedCandidateIndex[slot];\n\t                        if (candidateIndex >= 0) {\n\t                            const oldWholeLocks = Math.floor(smartLockProgress[i] + 0.0001);\n\t                            const requiredLocks = smartCandidateRequiredLocks[candidateIndex];\n\t                            const progress = Math.min(requiredLocks, smartLockProgress[i] + smartCandidateLockRate[candidateIndex] * dt);\n\t                            const newWholeLocks = Math.floor(progress + 0.0001);\n\t                            if (newWholeLocks > oldWholeLocks && currentTime - lastSmartPistolLockTime > 45) {\n\t                                lastSmartPistolLockTime = currentTime;\n\t                                playSmartPistolLockSound(newWholeLocks >= requiredLocks);\n\t                            }\n\t                            writeSmartPistolLock(\n\t                                lockWriteIndex,\n\t                                slot,\n\t                                progress,\n\t                                requiredLocks,\n\t                                smartCandidateScreenX[candidateIndex],\n\t                                smartCandidateScreenY[candidateIndex],\n\t                                smartCandidateMarkerSize[candidateIndex],\n\t                                smartCandidateScore[candidateIndex],\n\t                                smartCandidateCenterNorm[candidateIndex],\n\t                                smartCandidateDist[candidateIndex],\n\t                                0\n\t                            );\n\t                            smartLockIndexBySlot[slot] = lockWriteIndex;\n\t                            lockWriteIndex++;\n\t                        } else {\n\t                            const lostTime = smartLockLostTime[i] + dt;\n\t                            let progress = smartLockProgress[i];\n\t                            if (lostTime > SMART_PISTOL_LOST_GRACE) {\n\t                                progress = Math.max(0, progress - SMART_PISTOL_DECAY_RATE * dt);\n\t                            }\n\t                            if (progress > 0.04 && lostTime < 0.95) {\n\t                                writeSmartPistolLock(\n\t                                    lockWriteIndex,\n\t                                    slot,\n\t                                    progress,\n\t                                    smartLockRequiredLocks[i],\n\t                                    smartLockScreenX[i],\n\t                                    smartLockScreenY[i],\n\t                                    smartLockMarkerSize[i],\n\t                                    smartLockScore[i],\n\t                                    smartLockCenterNorm[i],\n\t                                    smartLockDist[i],\n\t                                    lostTime\n\t                                );\n\t                                smartLockIndexBySlot[slot] = lockWriteIndex;\n\t                                lockWriteIndex++;\n\t                            }\n\t                        }\n\t                    }\n\t                    smartPistolLockCount = lockWriteIndex;\n\n\t                    for (let i = 0; i < selectedCount && smartPistolLockCount < SMART_PISTOL_LOCK_CAPACITY; i++) {\n\t                        const candidateIndex = smartCandidateOrder[i];\n\t                        const slot = smartCandidateSlot[candidateIndex];\n\t                        if (smartLockIndexBySlot[slot] >= 0) continue;\n\t                        writeSmartPistolLock(\n\t                            smartPistolLockCount,\n\t                            slot,\n\t                            Math.min(smartCandidateRequiredLocks[candidateIndex], smartCandidateLockRate[candidateIndex] * dt),\n\t                            smartCandidateRequiredLocks[candidateIndex],\n\t                            smartCandidateScreenX[candidateIndex],\n\t                            smartCandidateScreenY[candidateIndex],\n\t                            smartCandidateMarkerSize[candidateIndex],\n\t                            smartCandidateScore[candidateIndex],\n\t                            smartCandidateCenterNorm[candidateIndex],\n\t                            smartCandidateDist[candidateIndex],\n\t                            0\n\t                        );\n\t                        smartLockIndexBySlot[slot] = smartPistolLockCount;\n\t                        smartPistolLockCount++;\n\t                    }\n\n\t                    sortSmartPistolLocks();\n\t                    refreshSmartPistolTargetList();\n\n\t                    let hasFullLock = false;\n\t                    let hasPartialLock = false;\n\t                    for (let i = 0; i < smartPistolLockCount; i++) {\n\t                        const progress = smartLockProgress[i];\n\t                        const requiredLocks = smartLockRequiredLocks[i];\n\t                        if (progress >= requiredLocks - 0.001) hasFullLock = true;\n\t                        if (progress >= 1 || progress / requiredLocks > 0.55) hasPartialLock = true;\n\t                    }\n\n\t                    const wantedState = hasFullLock ? 'locked' : (hasPartialLock ? 'locking' : 'searching');\n\t                    if (lastSmartHudState !== wantedState) {\n\t                        crosshairEl.classList.remove('locked', 'locking');\n\t                        if (wantedState !== 'searching') {\n\t                            crosshairEl.classList.add(wantedState);\n\t                        }\n\t                        lastSmartHudState = wantedState;\n\t                    }\n\n\t                    smartActiveSlotFlags.fill(0);\n\t                    for (let i = 0; i < smartPistolLockCount; i++) {\n\t                        const slot = smartLockSlot[i];\n\t                        smartActiveSlotFlags[slot] = 1;\n\t                        const slotStr = String(slot);\n\t                        const requiredLocks = smartLockRequiredLocks[i];\n\t                        const acquiredLocks = Math.min(requiredLocks, Math.floor(smartLockProgress[i] + 0.0001));\n\t                        const progressRatio = clamp01(smartLockProgress[i] / Math.max(1, requiredLocks));\n\t                        const stateCls = progressRatio >= 0.999 ? 'ready' : (acquiredLocks > 0 || progressRatio > 0.55 ? 'partial' : 'locking');\n\t                        const lockCountCls = requiredLocks <= 1 ? 'single' : 'multi';\n\t                        const minMarker = isSmartPistolMobileHud() ? 22 : 24;\n\t                        const maxMarker = isSmartPistolMobileHud() ? (cachedIsLandscape ? 42 : 44) : 58;\n\t                        const markerSize = Math.max(minMarker, Math.min(maxMarker, smartLockMarkerSize[i] || 34));\n\t                        const lockDeg = (progressRatio * 360).toFixed(1);\n\n\t                        let marker = smartMarkerCache.get(slotStr);\n\t                        if (!marker) {\n\t                            // Acquire from pool (zero allocation); fall back to createElement if pool exhausted.\n\t                            if (smartMarkerPool && smartMarkerPool.length > 0) {\n\t                                marker = smartMarkerPool.pop();\n\t                                marker.style.display = '';\n\t                            } else {\n\t                                marker = document.createElement('div');\n\t                                marker.innerHTML = `<span class=\"smart-lock-count\"></span><span class=\"smart-lock-progress\"></span>`;\n\t                                marker._countEl = marker.querySelector('.smart-lock-count');\n\t                                smartContainer.appendChild(marker);\n\t                            }\n\t                            marker.setAttribute('data-slot', slotStr);\n\t                            smartMarkerCache.set(slotStr, marker);\n\t                        }\n\n\t                        const wantedClass = `smart-lock-marker ${stateCls} ${lockCountCls}`;\n\t                        if (marker.className !== wantedClass) {\n\t                            marker.className = wantedClass;\n\t                        }\n\n\t                        const wantedLeft = `${smartLockScreenX[i].toFixed(1)}px`;\n\t                        if (marker.style.left !== wantedLeft) {\n\t                            marker.style.left = wantedLeft;\n\t                        }\n\n\t                        const wantedTop = `${smartLockScreenY[i].toFixed(1)}px`;\n\t                        if (marker.style.top !== wantedTop) {\n\t                            marker.style.top = wantedTop;\n\t                        }\n\n\t                        const wantedSize = `${markerSize.toFixed(1)}px`;\n\t                        if (marker.style.getPropertyValue('--marker-size') !== wantedSize) {\n\t                            marker.style.setProperty('--marker-size', wantedSize);\n\t                        }\n\n\t                        const wantedDeg = `${lockDeg}deg`;\n\t                        if (marker.style.getPropertyValue('--lock-deg') !== wantedDeg) {\n\t                            marker.style.setProperty('--lock-deg', wantedDeg);\n\t                        }\n\n\t                        const countText = `${acquiredLocks}/${requiredLocks}`;\n\t                        const countEl = marker._countEl;\n\t                        if (countEl && countEl.textContent !== countText) {\n\t                            countEl.textContent = countText;\n\t                        }\n\t                    }\n\n\t                    for (const [slotStr, marker] of smartMarkerCache.entries()) {\n\t                        if (!smartActiveSlotFlags[Number(slotStr)]) {\n\t                            // Return to pool instead of removing from DOM \u2014 avoids layout reflow.\n\t                            marker.style.display = 'none';\n\t                            smartMarkerPool.push(marker);\n\t                            smartMarkerCache.delete(slotStr);\n\t                        }\n\t                    }\n\t                }\n\n                // Touch - Left joystick for movement, right swipe for look, tap right to shoot\n                const touchStarts = Object.create(null);\n                let touchLayoutDirty = true;\n                let joystickLeftVisible = false;\n                let joystickLeftDirty = false;\n                const joystickLeftVisual = { baseX: 0, baseY: 0, dx: 0, dy: 0 };\n                const touchMoveState = {\n                    leftDirty: false,\n                    leftX: 0,\n                    leftY: 0,\n                    rightDirty: false,\n                    rightX: 0,\n                    rightY: 0\n                };\n                const touchIgnoreRects = {\n                    weapon: { left: 0, top: 0, right: 0, bottom: 0 },\n                    reload: { left: 0, top: 0, right: 0, bottom: 0 },\n                    shoot: { left: 0, top: 0, right: 0, bottom: 0 },\n                    scope: { left: 0, top: 0, right: 0, bottom: 0 }\n                };\n                const touchGameRect = { left: 0, top: 0, right: 0, bottom: 0 };\n                const TAP_FIRE_MAX_MS = 240;\n                const TAP_FIRE_MAX_MOVE_SQ = 12 * 12;\n                function touchToGamePoint(touch) {\n                    if (touchLayoutDirty) refreshTouchLayoutCache();\n                    const rect = touchGameRect;\n                    if (isRotatedLandscapeMode()) {\n                        return {\n                            x: touch.clientY - rect.top,\n                            y: rect.right - touch.clientX\n                        };\n                    }\n                    return {\n                        x: touch.clientX - rect.left,\n                        y: touch.clientY - rect.top\n                    };\n                }\n                function touchToVisualPoint(touch) {\n                    return touchToGamePoint(touch);\n                }\n                function getVisualTouchSize() {\n                    return { w: gameWorld.clientWidth, h: gameWorld.clientHeight };\n                }\n                function visualDeltaFromGameDelta(dx, dy) {\n                    return { x: dx, y: dy };\n                }\n                function visualPointCanTapFire(x, y) {\n                    const { w, h } = getVisualTouchSize();\n                    const leftZoneWidth = 1.2 / 3;\n                    const leftZoneTopRatio = 0.68;\n                    if (x < w * leftZoneWidth && y > h * leftZoneTopRatio) return false;\n                    return x > w * (isLandscapeMode() ? 0.48 : 0.42);\n                }\n                function markTouchMoved(start, x, y) {\n                    const dx = x - start.x;\n                    const dy = y - start.y;\n                    const moveSq = dx * dx + dy * dy;\n                    start.maxMoveSq = Math.max(start.maxMoveSq || 0, moveSq);\n                    if (moveSq > TAP_FIRE_MAX_MOVE_SQ) start.moved = true;\n                }\n                function cancelActiveTouchControls() {\n                    leftTouch = null;\n                    rightTouch = null;\n                    input.firing = false;\n                    input.holdingFire = false;\n                    input.touchHoldingFire = false;\n                    for (const id in touchStarts) delete touchStarts[id];\n                    touchMoveState.leftDirty = false;\n                    touchMoveState.rightDirty = false;\n                    leftJoystickValue.x = 0;\n                    leftJoystickValue.y = 0;\n                    joystickLeftVisible = false;\n                    joystickLeftDirty = true;\n                    shootJoystickVisible = false;\n                    shootJoystickDirty = true;\n                    shootBtn.classList.remove('active');\n                }\n                function copyRectToCache(src, dst) {\n                    dst.left = src.left;\n                    dst.top = src.top;\n                    dst.right = src.right;\n                    dst.bottom = src.bottom;\n                }\n                function refreshTouchLayoutCache() {\n                    copyRectToCache(gameWorld.getBoundingClientRect(), touchGameRect);\n                    copyRectToCache(weaponSlotsEl.getBoundingClientRect(), touchIgnoreRects.weapon);\n                    copyRectToCache(reloadBtn.getBoundingClientRect(), touchIgnoreRects.reload);\n                    const shootBtn = document.getElementById('shoot-btn');\n                    if (shootBtn) copyRectToCache(shootBtn.getBoundingClientRect(), touchIgnoreRects.shoot);\n                    const scopeBtn = document.getElementById('scope-btn');\n                    if (scopeBtn) copyRectToCache(scopeBtn.getBoundingClientRect(), touchIgnoreRects.scope);\n\n                    touchLayoutDirty = false;\n                }\n                function pointInCachedRect(x, y, rect) {\n                    return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;\n                }\n                function isTouchOnHud(x, y) {\n                    if (touchLayoutDirty) refreshTouchLayoutCache();\n                    return pointInCachedRect(x, y, touchIgnoreRects.weapon)\n                        || pointInCachedRect(x, y, touchIgnoreRects.reload)\n                        || pointInCachedRect(x, y, touchIgnoreRects.shoot)\n                        || pointInCachedRect(x, y, touchIgnoreRects.scope);\n                }\n                function showLeftJoystick(x, y) {\n                    joystickLeftVisible = true;\n                    joystickLeftDirty = true;\n                    joystickLeftVisual.baseX = x;\n                    joystickLeftVisual.baseY = y;\n                    joystickLeftVisual.dx = 0;\n                    joystickLeftVisual.dy = 0;\n                }\n                function hideLeftJoystick() {\n                    leftTouch = null;\n                    touchMoveState.leftDirty = false;\n                    leftJoystickValue.x = 0;\n                    leftJoystickValue.y = 0;\n                    joystickLeftVisible = false;\n                    joystickLeftDirty = true;\n                    joystickLeftVisual.dx = 0;\n                    joystickLeftVisual.dy = 0;\n                }\n                function updateLeftJoystickVisual(dx, dy) {\n                    joystickLeftVisual.dx = dx;\n                    joystickLeftVisual.dy = dy;\n                    joystickLeftDirty = true;\n                }\n                function processPendingTouchInput() {\n                    if (touchMoveState.leftDirty && leftTouch !== null) {\n                        touchMoveState.leftDirty = false;\n                        let dx = touchMoveState.leftX - leftJoystickBase.x;\n                        let dy = touchMoveState.leftY - leftJoystickBase.y;\n                        const dist = Math.sqrt(dx * dx + dy * dy), maxR = 60;\n                        if (dist > maxR) {\n                            leftJoystickBase.x = touchMoveState.leftX - (dx / dist) * maxR;\n                            leftJoystickBase.y = touchMoveState.leftY - (dy / dist) * maxR;\n                            dx = touchMoveState.leftX - leftJoystickBase.x;\n                            dy = touchMoveState.leftY - leftJoystickBase.y;\n                            joystickLeftVisual.baseX = leftJoystickBase.x;\n                            joystickLeftVisual.baseY = leftJoystickBase.y;\n                        }\n                        const visualMove = visualDeltaFromGameDelta(dx, dy);\n                        leftJoystickValue.x = visualMove.x / maxR;\n                        leftJoystickValue.y = visualMove.y / maxR;\n                        updateLeftJoystickVisual(dx, dy);\n                    }\n                    if (touchMoveState.rightDirty && rightTouch !== null) {\n                        touchMoveState.rightDirty = false;\n                        const dx = touchMoveState.rightX - rightJoystickBase.x;\n                        const dy = touchMoveState.rightY - rightJoystickBase.y;\n                        const visualLook = visualDeltaFromGameDelta(dx, dy);\n                        const sensX = sniperScoped ? 0.001 : 0.004;\n                        const sensY = sniperScoped ? 0.00075 : 0.003;\n                        input.yawDelta -= visualLook.x * sensX;\n                        input.pitchDelta -= visualLook.y * sensY;\n                        rightJoystickBase.x = touchMoveState.rightX;\n                        rightJoystickBase.y = touchMoveState.rightY;\n                    }\n                }\n                function flushTouchVisuals() {\n                    if (!isTouchDevice) {\n                        joystickLeft.style.display = 'none';\n                        shootContainer.style.display = 'none';\n                        return;\n                    }\n\n                    const isLandscape = isLandscapeMode();\n\n                    // Left Joystick\n                    if (joystickLeftDirty) {\n                        joystickLeftDirty = false;\n                        joystickLeft.style.display = 'block';\n                        if (!joystickLeftVisible) {\n                            joystickLeft.style.left = isLandscape ? '110px' : '120px';\n                            joystickLeft.style.bottom = isLandscape ? '80px' : '150px';\n                            joystickLeft.style.top = 'auto';\n                            joystickLeftThumb.style.transform = 'translate(-50%, -50%)';\n                            joystickLeftThumb.style.background = 'rgba(255,255,255,0.2)';\n                        } else {\n                            joystickLeft.style.left = (joystickLeftVisual.baseX - 70) + 'px';\n                            joystickLeft.style.top = (joystickLeftVisual.baseY - 70) + 'px';\n                            joystickLeft.style.bottom = 'auto';\n                            joystickLeftThumb.style.transform = `translate(calc(-50% + ${joystickLeftVisual.dx}px), calc(-50% + ${joystickLeftVisual.dy}px))`;\n                            joystickLeftThumb.style.background = 'rgba(0, 217, 255, 0.5)';\n                        }\n                    }\n\n                    // Right Shoot Joystick (Floating)\n                    if (shootJoystickDirty) {\n                        shootJoystickDirty = false;\n                        shootContainer.style.display = 'block';\n                        shootBtn.style.display = 'flex';\n                        if (!shootJoystickVisible) {\n                            shootContainer.style.right = isLandscape ? '110px' : '120px';\n                            shootContainer.style.bottom = isLandscape ? '80px' : '150px';\n                            shootContainer.style.left = 'auto';\n                            shootContainer.style.top = 'auto';\n                            shootBtn.style.transform = 'translate(-50%, -50%)';\n                            shootContainer.style.border = '3px solid rgba(255,255,255,0.3)';\n                            shootContainer.style.background = 'rgba(255,255,255,0.05)';\n                        } else {\n                            shootContainer.style.left = (shootJoystickVisual.baseX - 70) + 'px';\n                            shootContainer.style.top = (shootJoystickVisual.baseY - 70) + 'px';\n                            shootContainer.style.right = 'auto';\n                            shootContainer.style.bottom = 'auto';\n                            shootBtn.style.transform = `translate(calc(-50% + ${shootJoystickVisual.dx}px), calc(-50% + ${shootJoystickVisual.dy}px))`;\n                            shootContainer.style.border = '3px solid rgba(255, 255, 255, 0.45)';\n                            shootContainer.style.background = 'rgba(255, 255, 255, 0.1)';\n                        }\n                    }\n                }\n                window.addEventListener('resize', () => { touchLayoutDirty = true; });\n                gameWorld.addEventListener('touchstart', (e) => {\n                    if (!gameState.started || gameState.over || gamePaused) return;\n                    if (e.cancelable) e.preventDefault();\n                    if (gameState.upgradeScreenOpen || weaponSelectOpen) return;\n                    if (touchLayoutDirty) refreshTouchLayoutCache();\n                    for (let touch of e.changedTouches) {\n                        if (isTouchOnHud(touch.clientX, touch.clientY)) continue;\n                        const p = touchToGamePoint(touch);\n                        const visual = touchToVisualPoint(touch);\n                        const x = p.x, y = p.y;\n                        const visualSize = getVisualTouchSize();\n                        const isLandscape = isLandscapeMode();\n                        const leftZoneWidth = 1.2 / 3;\n                        const leftZoneTopRatio = 0.68;\n                        if (visual.x < visualSize.w * leftZoneWidth && visual.y > visualSize.h * leftZoneTopRatio && leftTouch === null) {\n                            leftTouch = touch.identifier;\n                            leftJoystickBase.x = x;\n                            leftJoystickBase.y = y;\n                            showLeftJoystick(x, y);\n                        } else {\n                            // Any non-joystick touch: track for swipe/tap\n                            touchStarts[touch.identifier] = {\n                                x,\n                                y,\n                                time: performance.now(),\n                                moved: false,\n                                maxMoveSq: 0,\n                                canTapFire: visualPointCanTapFire(visual.x, visual.y)\n                            };\n                            if (rightTouch === null) {\n                                rightTouch = touch.identifier;\n                                rightJoystickBase.x = x;\n                                rightJoystickBase.y = y;\n                            }\n                            // Katana special: screen touch triggers continuous attack like auto weapon\n                            const wComp = world.getComponent(playerEntity, 'Weapon');\n                            if (weaponStates[wComp.index].isMelee) {\n                                input.firing = true;\n                                input.holdingFire = true;\n                            }\n                        }\n                    }\n                }, { passive: false });\n                gameWorld.addEventListener('touchmove', (e) => {\n                    if (!gameState.started || gameState.over) return;\n                    for (let touch of e.changedTouches) {\n                        const p = touchToGamePoint(touch);\n                        const x = p.x, y = p.y;\n                        if (touch.identifier === leftTouch) {\n                            touchMoveState.leftX = x;\n                            touchMoveState.leftY = y;\n                            touchMoveState.leftDirty = true;\n                        } else if (touchStarts[touch.identifier]) {\n                            // Swipe to look\n                            if (touch.identifier === rightTouch) {\n                                touchMoveState.rightX = x;\n                                touchMoveState.rightY = y;\n                                touchMoveState.rightDirty = true;\n                            }\n                            markTouchMoved(touchStarts[touch.identifier], x, y);\n                        }\n                    }\n                }, { passive: true });\n                gameWorld.addEventListener('touchend', (e) => {\n                    for (let touch of e.changedTouches) {\n                        if (touch.identifier === leftTouch) {\n                            hideLeftJoystick();\n                        }\n                        else if (touchStarts[touch.identifier]) {\n                            const start = touchStarts[touch.identifier];\n                            // Tap to shoot (short duration and didn't move much)\n                            if (start.canTapFire && !start.moved && (start.maxMoveSq || 0) <= TAP_FIRE_MAX_MOVE_SQ && (performance.now() - start.time < TAP_FIRE_MAX_MS)) {\n                                if (!gameState.upgradeScreenOpen && !weaponSelectOpen) {\n                                    input.firing = true;\n                                }\n                            }\n                            delete touchStarts[touch.identifier];\n                            if (touch.identifier === rightTouch) {\n                                rightTouch = null;\n                                touchMoveState.rightDirty = false;\n                                // Katana special: stop continuous attack when screen touch ends\n                                const wComp = world.getComponent(playerEntity, 'Weapon');\n                                if (weaponStates[wComp.index].isMelee) {\n                                    input.holdingFire = false;\n                                }\n                            }\n                        }\n                    }\n                }, { passive: false });\n\n                applyScreenOrientation(screenOrientationMode, { save: false, lock: false, delay: 0 });\n\n                // ===== ROGUELIKE SYSTEM: Weapon Select + Upgrades =====\n                const weaponSelectScreen = document.getElementById('weapon-select-screen');\n                const weaponSelectGrid = document.getElementById('weapon-select-grid');\n                const upgradeSelectScreen = document.getElementById('upgrade-select-screen');\n                const upgradeSelectTitle = document.getElementById('upgrade-select-title');\n                const upgradeSelectOptions = document.getElementById('upgrade-select-options');\n\n                function updateWeaponSlotsVisibility() {\n                    weaponSlotEls.forEach((slot, i) => {\n                        const owned = roguelikeState.ownedWeapons.includes(i);\n                        slot.style.display = owned ? '' : 'none';\n                        const numEl = slot.querySelector('.slot-num');\n                        if (numEl) {\n                            if (owned) {\n                                const ownershipIndex = roguelikeState.ownedWeapons.indexOf(i);\n                                slot.style.order = ownershipIndex;\n                                numEl.textContent = ownershipIndex === 0 ? '1' : (ownershipIndex + 1);\n                            } else {\n                                numEl.textContent = '';\n                                slot.style.order = '';\n                            }\n                        }\n                    });\n                }\n\n                function resetWeaponStatsToOriginal() {\n                    weaponStates.forEach((ws, i) => {\n                        const orig = originalWeaponStats[i];\n                        ws.damage = orig.damage;\n                        ws.fireRate = orig.fireRate;\n                        ws.reloadTime = orig.reloadTime;\n                        ws.magSize = orig.magSize;\n                        ws.penetration = orig.penetration;\n                        ws.meleeRange = orig.meleeRange;\n                        ws.ammo = orig.ammo;\n                        ws.maxAmmo = orig.maxAmmo;\n                        ws.mag = orig.mag;\n                        ws.spread = orig.spread;\n                        updateWeaponDerivedStats(i, ws);\n                    });\n                }\n\n                function formatWeaponMode(ws) {\n                    return ws.isMelee ? t('weaponStats.melee') : (ws.isAuto ? t('weaponStats.auto') : t('weaponStats.semi'));\n                }\n\n                function getWeaponDisplayName(wsOrName) {\n                    const name = typeof wsOrName === 'string' ? wsOrName : wsOrName.name;\n                    return t(`weapons.${name}`);\n                }\n\n                function getZombieDisplayName(name) {\n                    return t(`zombies.${name}`);\n                }\n\n                function formatWeaponStats(ws) {\n                    const damage = t('weaponStats.damage', { value: ws.damage });\n                    if (ws.isMelee) {\n                        return `${damage} | ${t('weaponStats.range', { value: ws.meleeRange })}`;\n                    }\n                    return `${damage} | ${formatWeaponMode(ws)} | ${t('weaponStats.magazine', { value: ws.magSize === Infinity ? '\u221e' : ws.magSize })}`;\n                }\n\n                function showWeaponSelectScreen() {\n                    cancelActiveTouchControls();\n                    weaponSelectGrid.innerHTML = '';\n                    weaponStates.forEach((ws, i) => {\n                        const card = document.createElement('div');\n                        card.className = 'weapon-select-card';\n                        const statsStr = formatWeaponStats(ws);\n                        const iconEl = getWeaponIconElement(ws.icon, 56);\n                        iconEl.style.marginBottom = '8px';\n                        iconEl.style.filter = 'drop-shadow(0 0 4px rgba(0,180,255,0.3))';\n                        card.innerHTML = `\n                        <div class=\"weapon-select-card-name\">${getWeaponDisplayName(ws)}</div>\n                        <div class=\"weapon-select-card-stats\">${statsStr}</div>\n                    `;\n                        card.insertBefore(iconEl, card.firstChild);\n                        card.addEventListener('click', () => selectStartingWeapon(i));\n                        card.addEventListener('touchstart', (e) => { e.stopPropagation(); });\n                        weaponSelectGrid.appendChild(card);\n                    });\n                    weaponSelectOpen = true;\n                    weaponSelectScreen.classList.add('active');\n                    setupOrientationToggle();\n                }\n\n                function setupOrientationToggle() {\n                    bindOrientationButtons();\n                }\n\n                function selectStartingWeapon(index) {\n                    warmCoreAudio(true);\n                    if (index === 7) ensureAudioLoaded(FLAMETHROWER_LOOP_SOUND_ID);\n                    roguelikeState.ownedWeapons = [index];\n                    roguelikeState.upgradeHistory = [];\n                    flamethrowerUpgrades.rangeBoost = 0;\n                    flamethrowerUpgrades.stickyFire = 0;\n                    flamethrowerUpgrades.wideNozzle = 0;\n                    flamethrowerUpgrades.fireSpread = 0;\n                    flamethrowerUpgrades.armorMelting = 0;\n                    weaponSelectOpen = false;\n                    weaponSelectScreen.classList.remove('active');\n                    input.firing = false;\n                    input.holdingFire = false;\n                    input.touchHoldingFire = false;\n                    // Set player weapon\n                    const wComp = world.getComponent(playerEntity, 'Weapon');\n                    wComp.index = index;\n                    setActiveWeaponModel(index);\n                    weaponAnim.currentModel = index;\n                    updateWeaponSlotsVisibility();\n                    updateWeaponDisplay();\n                    updateScopeBtnVisibility();\n                    // Start game\n                    gameState.started = true;\n                    startWave();\n                    playSound('ambient_bunker_sound', 0.12, true);\n                    playSound('bgm_main', 0.55, true);\n                    if (!isTouchDevice) safeRequestPointerLock(canvas);\n                    // Force joystick to render on first frame\n                    joystickLeftDirty = true;\n                }\n\n                const RARITY_WEAPON = 'weapon';\n                const RARITY_BRONZE = 'bronze';\n                const RARITY_EPIC = 'epic';\n                const RARITY_LEGENDARY = 'legendary';\n                const rarityLabels = {\n                    en: { weapon: 'NEW WEAPON', bronze: 'BRONZE', epic: 'EPIC', legendary: 'LEGENDARY' },\n                    zh: { weapon: '\u65b0\u6b66\u5668', bronze: '\u9752\u94dc', epic: '\u53f2\u8bd7', legendary: '\u4f20\u5947' },\n                    ja: { weapon: '\u65b0\u6b66\u5668', bronze: '\u30d6\u30ed\u30f3\u30ba', epic: '\u30a8\u30d4\u30c3\u30af', legendary: '\u30ec\u30b8\u30a7\u30f3\u30c9' },\n                    de: { weapon: 'NEUE WAFFE', bronze: 'BRONZE', epic: 'EPISCH', legendary: 'LEGEND\u00c4R' },\n                    ru: { weapon: '\u041d\u041e\u0412\u041e\u0415 \u041e\u0420\u0423\u0416\u0418\u0415', bronze: '\u0411\u0420\u041e\u041d\u0417\u0410', epic: '\u042d\u041f\u0418\u0427\u0415\u0421\u041a\u0418\u0419', legendary: '\u041b\u0415\u0413\u0415\u041d\u0414\u0410\u0420\u041d\u042b\u0419' },\n                    tr: { weapon: 'YEN\u0130 S\u0130LAH', bronze: 'BRONZ', epic: 'EP\u0130K', legendary: 'EFSANE' },\n                    it: { weapon: 'NUOVA ARMA', bronze: 'BRONZO', epic: 'EPICO', legendary: 'LEGGENDARIO' },\n                    vi: { weapon: 'V\u0168 KH\u00cd M\u1edaI', bronze: '\u0110\u1ed2NG', epic: 'S\u1eec THI', legendary: 'HUY\u1ec0N THO\u1ea0I' },\n                    id: { weapon: 'SENJATA BARU', bronze: 'PERUNGGU', epic: 'EPIK', legendary: 'LEGENDARIS' },\n                    ur: { weapon: '\u0646\u06cc\u0627 \u06c1\u062a\u06be\u06cc\u0627\u0631', bronze: '\u06a9\u0627\u0646\u0633\u06cc', epic: '\u0627\u06cc\u067e\u06a9', legendary: '\u0644\u06cc\u062c\u0646\u0688\u0631\u06cc' },\n                    bn: { weapon: '\u09a8\u09a4\u09c1\u09a8 \u0985\u09b8\u09cd\u09a4\u09cd\u09b0', bronze: '\u09ac\u09cd\u09b0\u09cb\u099e\u09cd\u099c', epic: '\u098f\u09aa\u09bf\u0995', legendary: '\u09b2\u09c7\u099c\u09c7\u09a8\u09cd\u09a1\u09be\u09b0\u09bf' },\n                    sw: { weapon: 'SILAHA MPYA', bronze: 'SHABA', epic: 'EPIKI', legendary: 'HADITHI' },\n                    te: { weapon: '\u0c15\u0c4a\u0c24\u0c4d\u0c24 \u0c06\u0c2f\u0c41\u0c27\u0c02', bronze: '\u0c15\u0c3e\u0c02\u0c38\u0c4d\u0c2f\u0c02', epic: '\u0c0e\u0c2a\u0c3f\u0c15\u0c4d', legendary: '\u0c32\u0c46\u0c1c\u0c46\u0c02\u0c21\u0c30\u0c40' },\n                    mr: { weapon: '\u0928\u0935\u0947 \u0936\u0938\u094d\u0924\u094d\u0930', bronze: '\u0915\u093e\u0902\u0938\u094d\u092f', epic: '\u090f\u092a\u093f\u0915', legendary: '\u0932\u0947\u091c\u0947\u0902\u0921\u0930\u0940' },\n                    ta: { weapon: '\u0baa\u0bc1\u0ba4\u0bbf\u0baf \u0b86\u0baf\u0bc1\u0ba4\u0bae\u0bcd', bronze: '\u0bb5\u0bc6\u0ba3\u0bcd\u0b95\u0bb2\u0bae\u0bcd', epic: '\u0b8e\u0baa\u0bbf\u0b95\u0bcd', legendary: '\u0bb2\u0bc6\u0b9c\u0ba3\u0bcd\u0b9f\u0bb0\u0bbf' }\n                };\n                function getRarityLabel(rarity) {\n                    const labels = rarityLabels[currentLocale] || rarityLabels.en;\n                    return labels[rarity] || rarityLabels.en[rarity] || rarity;\n                }\n\n                function assignRarity(opt) {\n                    if (opt.type === 'weapon') { opt.rarity = RARITY_WEAPON; return opt; }\n                    if (opt.key === 'slowtime' || opt.key === 'smartpistol') { opt.rarity = RARITY_LEGENDARY; return opt; }\n                    if (opt.category === 'flame_ember' || opt.category === 'flame_lava') { opt.rarity = RARITY_LEGENDARY; return opt; }\n                    if (opt.key === 'lifesteal') { opt.rarity = RARITY_EPIC; return opt; }\n                    if (opt.key === 'autofire') { opt.rarity = RARITY_EPIC; return opt; }\n                    if (opt.category === 'pierce' || opt.category === 'firerate') { opt.rarity = RARITY_EPIC; return opt; }\n                    if (opt.category === 'flame_sticky' || opt.category === 'flame_spread') { opt.rarity = RARITY_EPIC; return opt; }\n                    if (opt.category === 'damage' || opt.category === 'range' || opt.category === 'flame_range') {\n                        opt.rarity = Math.random() < 0.35 ? RARITY_EPIC : RARITY_BRONZE;\n                        return opt;\n                    }\n                    opt.rarity = RARITY_BRONZE;\n                    return opt;\n                }\n\n                function generateUpgradeOptions() {\n                    const pool = [];\n                    // Add upgrades for owned ranged weapons\n                    roguelikeState.ownedWeapons.forEach(wi => {\n                        const ws = weaponStates[wi];\n                        if (ws.isMelee) return;\n                        if (wi === 7) {\n                            const nameDmg = currentLocale === 'zh' ? '\u3010\u706b\u7130\u55b7\u5c04\u5668\u3011\u4f24\u5bb3+' : '\u3010FLAMETHROWER\u3011DMG+';\n                            const descDmg = currentLocale === 'zh' ? '\u57fa\u7840\u4f24\u5bb3\u7531 ' + Math.round(ws.damage) + ' \u63d0\u5347\u81f3 ' + Math.round(ws.damage * 1.25) : 'Increase base damage ' + Math.round(ws.damage) + ' -> ' + Math.round(ws.damage * 1.25);\n                            \n                            const nameRange = currentLocale === 'zh' ? '\u3010\u706b\u7130\u55b7\u5c04\u5668\u3011\u706b\u7130\u8ddd\u79bb+' : '\u3010FLAMETHROWER\u3011RANGE+';\n                            const descRange = currentLocale === 'zh' ? '\u706b\u7130\u55b7\u5c04\u8ddd\u79bb\u7531 ' + (ws.bulletSpeed * 0.65).toFixed(1) + '\u7c73 \u63d0\u5347\u81f3 ' + (ws.bulletSpeed * 1.3 * 0.65).toFixed(1) + '\u7c73' : 'Increase flame distance ' + (ws.bulletSpeed * 0.65).toFixed(1) + 'm -> ' + (ws.bulletSpeed * 1.3 * 0.65).toFixed(1) + 'm';\n                            \n                            const nameHeat = currentLocale === 'zh' ? '\u3010\u706b\u7130\u55b7\u5c04\u5668\u3011\u70ed\u91cf\u5bb9\u9650+' : '\u3010FLAMETHROWER\u3011HEAT CAPACITY+';\n                            const descHeat = currentLocale === 'zh' ? '\u6700\u5927\u8fc7\u70ed\u9608\u503c\u7531 ' + ws.magSize + ' \u63d0\u5347\u81f3 ' + Math.ceil(ws.magSize * 1.35) : 'Increase max heat limit ' + ws.magSize + ' -> ' + Math.ceil(ws.magSize * 1.35);\n                            \n                            const nameCool = currentLocale === 'zh' ? '\u3010\u706b\u7130\u55b7\u5c04\u5668\u3011\u51b7\u5374\u901f\u5ea6+' : '\u3010FLAMETHROWER\u3011COOLING+';\n                            const descCool = currentLocale === 'zh' ? '\u8fc7\u70ed\u540e\u6240\u9700\u7684\u51b7\u5374\u65f6\u95f4\u7531 ' + ws.reloadTime.toFixed(2) + '\u79d2 \u51cf\u5c11\u81f3 ' + (ws.reloadTime * 0.72).toFixed(2) + '\u79d2' : 'Reduce cooling time ' + ws.reloadTime.toFixed(2) + 's -> ' + (ws.reloadTime * 0.72).toFixed(2) + 's';\n\n                            pool.push(\n                                { type: 'upgrade', category: 'damage', weapon: 7, name: nameDmg, desc: descDmg, icon: ws.icon, apply: () => { ws.damage = Math.round(ws.damage * 1.25); } },\n                                { type: 'upgrade', category: 'firerate', weapon: 7, name: nameRange, desc: descRange, icon: ws.icon, apply: () => { ws.bulletSpeed = Math.round(ws.bulletSpeed * 1.3); } },\n                                { type: 'upgrade', category: 'magazine', weapon: 7, name: nameHeat, desc: descHeat, icon: ws.icon, apply: () => { ws.magSize = Math.ceil(ws.magSize * 1.35); ws.mag = ws.magSize; const wComp = world.getComponent(playerEntity, 'Weapon'); if (wComp) wComp.flameHeat = 0; } },\n                                { type: 'upgrade', category: 'reload', weapon: 7, name: nameCool, desc: descCool, icon: ws.icon, apply: () => { ws.reloadTime *= 0.72; } }\n                            );\n                            return;\n                        }\n                        if (wi === 0) {\n                            const currentMult = smartPistolLockSpeedMultiplier;\n                            const nextMult = smartPistolLockSpeedMultiplier + 0.40;\n                            const nameLock = currentLocale === 'zh' ? '\u3010\u667a\u80fd\u624b\u67aa\u3011\u9501\u5b9a\u901f\u5ea6+' : '\u3010SMART PISTOL\u3011LOCK SPEED+';\n                            const descLock = currentLocale === 'zh'\n                                ? '\u9501\u5b9a\u901f\u5ea6\u7531 ' + Math.round(currentMult * 100) + '% \u63d0\u5347\u81f3 ' + Math.round(nextMult * 100) + '%'\n                                : 'Increase locking speed ' + Math.round(currentMult * 100) + '% -> ' + Math.round(nextMult * 100) + '%';\n                            \n                            pool.push(\n                                { type: 'upgrade', category: 'damage', weapon: 0, name: nameLock, desc: descLock, icon: ws.icon, apply: () => { smartPistolLockSpeedMultiplier += 0.40; } },\n                                { type: 'upgrade', category: 'reload', weapon: wi, name: t('upgrade.reloadName', { weapon: getWeaponDisplayName(ws) }), desc: t('upgrade.reloadDesc', { from: ws.reloadTime.toFixed(2), to: (ws.reloadTime * 0.75).toFixed(2) }), icon: ws.icon, apply: () => { ws.reloadTime *= 0.75; } },\n                                { type: 'upgrade', category: 'magazine', weapon: wi, name: t('upgrade.magazineName', { weapon: getWeaponDisplayName(ws) }), desc: t('upgrade.magazineDesc', { from: ws.magSize === Infinity ? '\u221e' : ws.magSize, to: ws.magSize === Infinity ? '\u221e' : Math.ceil(ws.magSize * 1.5) }), icon: ws.icon, apply: () => { if (ws.magSize !== Infinity) { ws.magSize = Math.ceil(ws.magSize * 1.5); ws.mag = ws.magSize; if (ws.maxAmmo !== Infinity) { ws.maxAmmo = Math.ceil(ws.maxAmmo * 1.3); ws.ammo = ws.maxAmmo; } updateWeaponDerivedStats(wi, ws); } } },\n                                { type: 'upgrade', category: 'firerate', weapon: wi, name: t('upgrade.fireRateName', { weapon: getWeaponDisplayName(ws) }), desc: t('upgrade.fireRateDesc', { from: (ws.fireRate * 1000).toFixed(0), to: (ws.fireRate * 0.8 * 1000).toFixed(0) }), icon: ws.icon, apply: () => { ws.fireRate *= 0.8; updateWeaponDerivedStats(wi, ws); } }\n                            );\n                            return;\n                        }\n                        pool.push(\n                            { type: 'upgrade', category: 'damage', weapon: wi, name: t('upgrade.damageName', { weapon: getWeaponDisplayName(ws) }), desc: t('upgrade.damageDesc', { from: Math.round(ws.damage), to: Math.round(ws.damage * 1.2) }), icon: ws.icon, apply: () => { ws.damage = Math.round(ws.damage * 1.2); } },\n                            { type: 'upgrade', category: 'reload', weapon: wi, name: t('upgrade.reloadName', { weapon: getWeaponDisplayName(ws) }), desc: t('upgrade.reloadDesc', { from: ws.reloadTime.toFixed(2), to: (ws.reloadTime * 0.75).toFixed(2) }), icon: ws.icon, apply: () => { ws.reloadTime *= 0.75; } },\n                            { type: 'upgrade', category: 'magazine', weapon: wi, name: t('upgrade.magazineName', { weapon: getWeaponDisplayName(ws) }), desc: t('upgrade.magazineDesc', { from: ws.magSize === Infinity ? '\u221e' : ws.magSize, to: ws.magSize === Infinity ? '\u221e' : Math.ceil(ws.magSize * 1.5) }), icon: ws.icon, apply: () => { if (ws.magSize !== Infinity) { ws.magSize = Math.ceil(ws.magSize * 1.5); ws.mag = ws.magSize; if (ws.maxAmmo !== Infinity) { ws.maxAmmo = Math.ceil(ws.maxAmmo * 1.3); ws.ammo = ws.maxAmmo; } updateWeaponDerivedStats(wi, ws); } } },\n                            { type: 'upgrade', category: 'pierce', weapon: wi, name: t('upgrade.pierceName', { weapon: getWeaponDisplayName(ws) }), desc: t('upgrade.pierceDesc', { from: ws.penetration, to: ws.penetration + 1 }), icon: ws.icon, apply: () => { ws.penetration += 1; } },\n                            { type: 'upgrade', category: 'firerate', weapon: wi, name: t('upgrade.fireRateName', { weapon: getWeaponDisplayName(ws) }), desc: t('upgrade.fireRateDesc', { from: (ws.fireRate * 1000).toFixed(0), to: (ws.fireRate * 0.8 * 1000).toFixed(0) }), icon: ws.icon, apply: () => { ws.fireRate *= 0.8; updateWeaponDerivedStats(wi, ws); } }\n                        );\n                    });\n                    const lifestealOfferChance = lifestealPercent > 0 ? LIFESTEAL_REPEAT_OFFER_CHANCE : LIFESTEAL_FIRST_OFFER_CHANCE;\n                    let lifestealOption = null;\n                    if (lifestealPercent < LIFESTEAL_UPGRADE_CAP && Math.random() < lifestealOfferChance) {\n                        lifestealOption = createLifestealUpgradeOption();\n                        for (let i = 0; i < LIFESTEAL_UPGRADE_WEIGHT; i++) pool.push(lifestealOption);\n                    }\n\n                    // Add Auto-Fire passive upgrade\n                    let autoFireOption = null;\n                    if (!hasAutoFirePassive && Math.random() < AUTO_FIRE_OFFER_CHANCE) {\n                        autoFireOption = {\n                            type: 'upgrade',\n                            key: 'autofire',\n                            name: t('upgrade.autoFireName'),\n                            desc: t('upgrade.autoFireDesc'),\n                            icon: null,\n                            apply: () => {\n                                hasAutoFirePassive = true;\n                            }\n                        };\n                        for (let i = 0; i < AUTO_FIRE_UPGRADE_WEIGHT; i++) pool.push(autoFireOption);\n                    }\n\n\n                    // Katana upgrades (if owned)\n                    if (roguelikeState.ownedWeapons.includes(3)) {\n                        const ks = weaponStates[3];\n                        pool.push(\n                            { type: 'upgrade', category: 'damage', weapon: 3, name: t('upgrade.damageName', { weapon: getWeaponDisplayName(ks) }), desc: t('upgrade.damageDesc', { from: Math.round(ks.damage), to: Math.round(ks.damage * 1.3) }), icon: 'weapon_icon_katana', apply: () => { ks.damage = Math.round(ks.damage * 1.3); } },\n                            { type: 'upgrade', category: 'firerate', weapon: 3, name: t('upgrade.speedName', { weapon: getWeaponDisplayName(ks) }), desc: t('upgrade.fireRateDesc', { from: (ks.fireRate * 1000).toFixed(0), to: (ks.fireRate * 0.8 * 1000).toFixed(0) }), icon: 'weapon_icon_katana', apply: () => { ks.fireRate *= 0.8; updateWeaponDerivedStats(3, ks); } },\n                            { type: 'upgrade', category: 'range', weapon: 3, name: t('upgrade.rangeName', { weapon: getWeaponDisplayName(ks) }), desc: t('upgrade.rangeDesc', { from: ks.meleeRange.toFixed(1), to: (ks.meleeRange + 0.5).toFixed(1) }), icon: 'weapon_icon_katana', apply: () => { ks.meleeRange += 0.5; } }\n                        );\n                    }\n\n                    // Add \"Slow Time Skill\" upgrade option if they don't have it\n                    if (!hasSlowTimeSkill) {\n                        pool.push({\n                            type: 'upgrade',\n                            key: 'slowtime',\n                            name: t('upgrade.slowTimeName'),\n                            desc: t('upgrade.slowTimeDesc'),\n                            icon: null,\n                            apply: () => {\n                                hasSlowTimeSkill = true;\n                                const btn = document.getElementById('skill-btn');\n                                if (btn) btn.style.display = 'flex';\n                            }\n                        });\n                    }\n\n                    // New weapon options (if not all owned) - Only offered from wave 6 onwards (when entering Wave 6, currentWave is 6)\n                    const unowned = [];\n                    for (let i = 0; i < weaponStates.length; i++) {\n                        if (!roguelikeState.ownedWeapons.includes(i)) unowned.push(i);\n                    }\n                    if (unowned.length > 0) {\n                        // Add up to 2 random weapon options\n                        const weaponCount = Math.min(2, unowned.length);\n                        for (let w = 0; w < weaponCount; w++) {\n                            const pickIdx = w + Math.floor(Math.random() * (unowned.length - w));\n                            const newWi = unowned[pickIdx];\n                            unowned[pickIdx] = unowned[w];\n                            unowned[w] = newWi;\n                            const nws = weaponStates[newWi];\n                            pool.push({\n                                type: 'weapon', weapon: newWi,\n                                name: t('upgrade.newWeaponName', { weapon: getWeaponDisplayName(nws) }),\n                                desc: formatWeaponStats(nws),\n                                icon: nws.icon,\n                                apply: () => { roguelikeState.ownedWeapons.push(newWi); updateWeaponSlotsVisibility(); }\n                            });\n                        }\n                    }\n                    // Shuffle pool\n                    for (let i = pool.length - 1; i > 0; i--) {\n                        const j = Math.floor(Math.random() * (i + 1));\n                        [pool[i], pool[j]] = [pool[j], pool[i]];\n                    }\n                    // Ensure variety: try to include 1 weapon option if available\n                    const options = [];\n                    const optionKeys = [];\n                    const optionKey = (o) => o.key || `${o.type}:${o.weapon}:${o.name}`;\n                    const pushUniqueOption = (opt) => {\n                        if (!opt || options.length >= 3) return;\n                        const key = optionKey(opt);\n                        for (let i = 0; i < optionKeys.length; i++) {\n                            if (optionKeys[i] === key) return;\n                        }\n                        optionKeys.push(key);\n                        options.push(opt);\n                    };\n\n                    // If a rare passive passes its offer roll, surface it instead of letting a large pool bury it.\n                    if (lifestealOption) {\n                        pushUniqueOption(lifestealOption);\n                    }\n                    if (autoFireOption) {\n                        pushUniqueOption(autoFireOption);\n                    }\n\n                    let preferSingleWeapon = false;\n                    if (Math.random() < 0.65) {\n                        for (let i = 0; i < pool.length; i++) {\n                            if (pool[i].type === 'weapon') {\n                                pushUniqueOption(pool[i]);\n                                preferSingleWeapon = options.length > 0;\n                                break;\n                            }\n                        }\n                    }\n                    for (let i = 0; i < pool.length && options.length < 3; i++) {\n                        if (preferSingleWeapon && pool[i].type === 'weapon') continue;\n                        pushUniqueOption(pool[i]);\n                    }\n                    for (let i = 0; i < options.length; i++) assignRarity(options[i]);\n                    return options;\n                }\n\n                let currentUpgradeOptions = [];\n                let upgradeCountdownInterval = null;\n                let upgradeCountdownValue = 5;\n                const upgradeOptionCards = [];\n\n                function createUpgradeOptionCard(idx) {\n                    const card = document.createElement('div');\n                    card.dataset.index = String(idx);\n                    const keyHint = document.createElement('span');\n                    keyHint.style.cssText = \"position:absolute;top:6px;left:10px;font-family:'Orbitron',monospace;font-size:11px;color:rgba(255,255,255,0.4);pointer-events:none;\";\n                    const rarityBadge = document.createElement('span');\n                    rarityBadge.className = 'rarity-badge';\n                    const icon = document.createElement('div');\n                    icon.className = 'upgrade-option-icon';\n                    const iconSprite = document.createElement('div');\n                    const info = document.createElement('div');\n                    info.className = 'upgrade-option-info';\n                    const name = document.createElement('div');\n                    name.className = 'upgrade-option-name';\n                    const desc = document.createElement('div');\n                    desc.className = 'upgrade-option-desc';\n                    info.append(name, desc);\n                    card.append(keyHint, rarityBadge, icon, info);\n                    card.addEventListener('click', () => handleUpgradeCardSelect(idx));\n                    card.addEventListener('touchstart', (e) => { e.stopPropagation(); });\n                    upgradeSelectOptions.appendChild(card);\n                    return { card, keyHint, rarityBadge, icon, iconSprite, name, desc };\n                }\n\n                function ensureUpgradeOptionCards() {\n                    if (upgradeOptionCards.length) return;\n                    upgradeSelectOptions.textContent = '';\n                    for (let i = 0; i < 3; i++) upgradeOptionCards.push(createUpgradeOptionCard(i));\n                }\n\n                function resetUpgradeCardFeedback(ref) {\n                    const { card } = ref;\n                    card.classList.remove('is-selected', 'is-dismissing');\n                    card.style.removeProperty('--upgrade-selected-border');\n                    card.style.removeProperty('--upgrade-selected-glow');\n                    card.style.transition = '';\n                    card.style.transform = '';\n                    card.style.opacity = '';\n                    card.style.borderColor = '';\n                    card.style.boxShadow = '';\n                }\n\n                function setUpgradeCardIcon(ref, opt) {\n                    ref.icon.textContent = '';\n                    if (ref.iconSprite && ref.iconSprite.parentNode) ref.iconSprite.remove();\n                    if (opt.icon) {\n                        ref.iconSprite = getWeaponIconElement(opt.icon, 40);\n                        ref.iconSprite.style.filter = 'drop-shadow(0 0 6px rgba(255,255,255,0.3))';\n                        if (ref.iconSprite.parentNode !== ref.icon) ref.icon.appendChild(ref.iconSprite);\n                    } else {\n                        ref.icon.textContent = opt.type === 'weapon' ? '[]' : '^';\n                    }\n                }\n\n                function renderUpgradeOptionCards(options) {\n                    ensureUpgradeOptionCards();\n                    for (let i = 0; i < upgradeOptionCards.length; i++) {\n                        const ref = upgradeOptionCards[i];\n                        const opt = options[i];\n                        if (!opt) {\n                            ref.card.style.display = 'none';\n                            continue;\n                        }\n                        resetUpgradeCardFeedback(ref);\n                        const rarityClass = opt.rarity ? ` rarity-${opt.rarity}` : '';\n                        ref.card.className = 'upgrade-option-card' + (opt.type === 'weapon' ? ' weapon-card' : '') + rarityClass;\n                        ref.card.style.display = '';\n                        ref.keyHint.textContent = String(i + 1);\n                        ref.keyHint.style.display = isTouchDevice ? 'none' : '';\n                        ref.rarityBadge.textContent = opt.rarity ? getRarityLabel(opt.rarity) : '';\n                        ref.rarityBadge.style.display = opt.rarity ? '' : 'none';\n                        setUpgradeCardIcon(ref, opt);\n                        ref.name.textContent = opt.name;\n                        ref.desc.textContent = opt.desc;\n                    }\n                }\n\n                function handleUpgradeCardSelect(idx) {\n                    const opt = currentUpgradeOptions[idx];\n                    const ref = upgradeOptionCards[idx];\n                    if (!opt || !ref || gameState.upgradeSelecting) return;\n                    gameState.upgradeSelecting = true;\n                    clearUpgradeCountdown();\n                    const rarityColors = {\n                        weapon: { border: 'rgba(0,200,255,1)', shadow: '0 0 44px rgba(0,180,255,0.5)' },\n                        bronze: { border: 'rgba(90,215,135,1)', shadow: '0 0 40px rgba(60,175,105,0.5)' },\n                        epic: { border: 'rgba(180,100,255,1)', shadow: '0 0 50px rgba(160,80,255,0.6), 0 0 100px rgba(160,80,255,0.2)' },\n                        legendary: { border: 'rgba(255,220,80,1)', shadow: '0 0 60px rgba(255,200,50,0.7), 0 0 120px rgba(255,180,0,0.3)' }\n                    };\n                    const rc = rarityColors[opt.rarity] || rarityColors.bronze;\n                    const card = ref.card;\n                    card.style.setProperty('--upgrade-selected-border', rc.border);\n                    card.style.setProperty('--upgrade-selected-glow', rc.shadow);\n                    card.classList.add('is-selected');\n                    if (opt.rarity === RARITY_LEGENDARY) playSound('weapon_showcase_sound', 0.65);\n                    else if (opt.rarity === RARITY_EPIC) playSound('wave_complete_sound', 0.35);\n                    else playSound('headshot_ding_sound', 0.4);\n                    scheduleRuntimeTimeout(() => selectUpgradeByIndex(idx), 180);\n                }\n\n                function getUpgradeAutoSelectScore(opt) {\n                    if (opt.key === 'lifesteal') return 100;\n                    if (opt.category === 'damage') return 80;\n                    if (opt.category === 'firerate') return 70;\n                    if (opt.category === 'magazine') return 60;\n                    if (opt.category === 'pierce') return 50;\n                    if (opt.category === 'reload') return 40;\n                    if (opt.category === 'range') return 55;\n                    if (opt.type === 'weapon') return 30;\n                    return 35;\n                }\n\n                function autoSelectBestUpgrade() {\n                    if (!gameState.upgradeScreenOpen || currentUpgradeOptions.length === 0 || gameState.upgradeSelecting) return;\n                    gameState.upgradeSelecting = true;\n                    let best = currentUpgradeOptions[0];\n                    let bestScore = getUpgradeAutoSelectScore(best);\n                    let bestIdx = 0;\n                    for (let i = 1; i < currentUpgradeOptions.length; i++) {\n                        const score = getUpgradeAutoSelectScore(currentUpgradeOptions[i]);\n                        if (score > bestScore) {\n                            bestScore = score;\n                            best = currentUpgradeOptions[i];\n                            bestIdx = i;\n                        }\n                    }\n\n                    ensureUpgradeOptionCards();\n                    const selectedCard = upgradeOptionCards[bestIdx] && upgradeOptionCards[bestIdx].card;\n                    if (selectedCard) {\n                        selectedCard.classList.add('is-selected');\n                        scheduleRuntimeTimeout(() => {\n                            selectedCard.classList.add('is-dismissing');\n                            scheduleRuntimeTimeout(() => {\n                                best.apply();\n                                roguelikeState.upgradeHistory.push(best.name);\n                                hideUpgradeScreen();\n                                if (best.type === 'weapon') showWeaponShowcase(best.weapon);\n                            }, 310);\n                        }, 200);\n                    } else {\n                        best.apply();\n                        roguelikeState.upgradeHistory.push(best.name);\n                        hideUpgradeScreen();\n                    }\n                }\n\n                function selectUpgradeByIndex(idx) {\n                    if (!gameState.upgradeScreenOpen || !currentUpgradeOptions[idx]) return;\n                    const opt = currentUpgradeOptions[idx];\n                    const isNewWeapon = opt.type === 'weapon';\n                    opt.apply();\n                    roguelikeState.upgradeHistory.push(opt.name);\n                    if (isNewWeapon) {\n                        hideUpgradeScreen();\n                        showWeaponShowcase(opt.weapon);\n                    } else {\n                        hideUpgradeScreen();\n                    }\n                }\n\n                function clearUpgradeCountdown() {\n                    if (upgradeCountdownInterval) {\n                        clearInterval(upgradeCountdownInterval);\n                        upgradeCountdownInterval = null;\n                    }\n                }\n\n                function updateCountdownDisplay() {\n                    const el = document.getElementById('upgrade-countdown');\n                    if (!el) return;\n                    const valueEl = document.getElementById('upgrade-countdown-value') || el;\n                    valueEl.textContent = upgradeCountdownValue;\n                    el.classList.toggle('urgent', upgradeCountdownValue <= 2);\n                    restartClassNextFrame(valueEl, 'countdown-bump');\n                }\n\n                function showUpgradeScreen() {\n                    cancelActiveTouchControls();\n                    gameState.upgradeScreenOpen = true;\n                    gameState.upgradeSelecting = false;\n                    const titleText = t('upgrade.waveComplete', { wave: gameState.currentWave - 1 });\n                    upgradeSelectTitle.textContent = titleText;\n                    upgradeSelectTitle.dataset.text = titleText;\n                    const options = generateUpgradeOptions();\n                    currentUpgradeOptions = options;\n                    renderUpgradeOptionCards(options);\n\n                    // Setup countdown\n                    upgradeCountdownValue = 10;\n                    updateCountdownDisplay();\n                    clearUpgradeCountdown();\n                    upgradeCountdownInterval = setInterval(() => {\n                        upgradeCountdownValue--;\n                        updateCountdownDisplay();\n                        if (upgradeCountdownValue <= 0) {\n                            clearUpgradeCountdown();\n                            autoSelectBestUpgrade();\n                        }\n                    }, 1000);\n                    restartActiveClassNextFrame(upgradeSelectScreen);\n                    if (pointerLocked) document.exitPointerLock();\n                }\n\n                function drawTutorialSpotlight(slotEl, scale = 1.0) {\n                    // Disabled per user request - only the slot highlight (\ud83d\udc47 small hand pointer) is shown.\n                }\n\n                function checkAndTriggerWeaponTutorial() {\n                    // Trigger tutorial only if we have exactly 2 weapons and haven't shown it yet\n                    if (roguelikeState.ownedWeapons.length === 2 && !weaponTutorialShown) {\n                        weaponTutorialShown = true;\n                        weaponTutorialActive = true;\n                        tutorialHighlightActive = true;\n                        tutorialSpotlightScale = 40.0;\n\n                        // Pointer lock maintained so player can keep aiming\n\n                        // Find the second weapon slot element (newly acquired weapon)\n                        const newWeaponIdx = roguelikeState.ownedWeapons[1];\n                        const slotEl = weaponSlotEls[newWeaponIdx];\n\n                        // Show the tutorial overlay with canvas spotlight\n                        const overlay = document.getElementById('weapon-tutorial-overlay');\n                        if (overlay) {\n                            overlay.classList.add('active');\n                            // Draw spotlight after a frame so layout is settled\n                            requestAnimationFrame(() => {\n                                drawTutorialSpotlight(slotEl, tutorialSpotlightScale);\n                            });\n                        }\n\n                        if (slotEl) {\n                            slotEl.classList.add('tutorial-highlight');\n                        }\n                    }\n                }\n\n                function dismissWeaponTutorial() {\n                    if (!tutorialHighlightActive) return;\n                    tutorialHighlightActive = false;\n                    weaponTutorialActive = false;\n                    const overlay = document.getElementById('weapon-tutorial-overlay');\n                    if (overlay) {\n                        overlay.classList.remove('active');\n                        // Clear the canvas\n                        const cvs = overlay.querySelector('canvas');\n                        if (cvs) {\n                            const ctx2d = cvs.getContext('2d');\n                            ctx2d.clearRect(0, 0, cvs.width, cvs.height);\n                        }\n                    }\n                    const newWeaponIdx = roguelikeState.ownedWeapons[1];\n                    const slotEl = weaponSlotEls[newWeaponIdx];\n                    if (slotEl) {\n                        slotEl.classList.remove('tutorial-highlight');\n                    }\n                    playSound('wave_complete_sound', 0.45);\n\n                    // Pointer lock and wave state already active\n                }\n\n                function showWeaponShowcase(weaponIndex, callback) {\n                    const ws = weaponStates[weaponIndex];\n                    const overlay = document.getElementById('weapon-showcase-overlay');\n                    const label = document.getElementById('showcase-label');\n                    const icon = document.getElementById('showcase-weapon-icon');\n                    const nameEl = document.getElementById('showcase-weapon-name');\n                    const statsEl = document.getElementById('showcase-weapon-stats');\n                    label.textContent = t('upgrade.newWeaponName', { weapon: '' }).replace(':', '').trim();\n                    nameEl.textContent = getWeaponDisplayName(ws);\n                    statsEl.textContent = formatWeaponStats(ws);\n                    let spriteDiv = overlay.querySelector('.showcase-sprite-icon');\n                    if (spriteDiv) {\n                        spriteDiv.remove();\n                    }\n                    icon.style.display = 'none';\n                    spriteDiv = getWeaponIconElement(ws.icon, 180);\n                    spriteDiv.classList.add('showcase-sprite-icon');\n                    spriteDiv.style.opacity = '0';\n                    spriteDiv.style.animation = 'showcaseIconIn 0.7s cubic-bezier(0.16,1,0.3,1) 0.15s forwards';\n                    spriteDiv.style.filter = 'drop-shadow(0 0 30px rgba(0,200,255,0.6)) drop-shadow(0 0 60px rgba(0,150,255,0.3))';\n                    icon.parentNode.insertBefore(spriteDiv, icon);\n                    overlay.classList.remove('fade-out');\n                    overlay.classList.add('active');\n                    // Play weapon acquisition sound\n                    playSound('weapon_showcase_sound', 0.85);\n                    // Auto dismiss after 1.8s, then auto-switch to new weapon\n                    scheduleRuntimeTimeout(() => {\n                        overlay.classList.add('fade-out');\n                        scheduleRuntimeTimeout(() => {\n                            overlay.classList.remove('active', 'fade-out');\n                            // Keep current weapon \u2014 do NOT auto-switch to newly acquired weapon\n                            updateWeaponSlotsVisibility();\n                            updateWeaponDisplay();\n                            updateScopeBtnVisibility();\n                            if (callback) callback();\n\n                            // Check if we should trigger weapon tutorial\n                            checkAndTriggerWeaponTutorial();\n                        }, 500);\n                    }, 1800);\n                }\n\n                function hideUpgradeScreen() {\n                    cancelActiveTouchControls();\n                    clearUpgradeCountdown();\n                    currentUpgradeOptions = [];\n                    gameState.upgradeScreenOpen = false;\n                    upgradeSelectScreen.classList.remove('active');\n                    updateWeaponDisplay();\n                    updateWeaponSlotsVisibility();\n                    // Resume game - start next wave after short delay\n                    gameState.waveCooldown = 1.5;\n                    if (!isTouchDevice) safeRequestPointerLock(canvas);\n                }\n\n                // ===== RESTART =====\n                document.getElementById('restart-btn').addEventListener('click', restartGame);\n\n                let backpackOpen = false;\n\n                function checkBackpackToggle() { }\n\n                // ===== PAUSE SYSTEM =====\n                let gamePaused = false;\n                const pauseOverlay = document.getElementById('pause-overlay');\n                const pauseMainMenu = document.getElementById('pause-main-menu');\n                const pauseSettingsPanel = document.getElementById('pause-settings-panel');\n                const pauseLanguagePanel = document.getElementById('pause-language-panel');\n\n                const languageNames = {\n                    en: 'English', zh: '\u4e2d\u6587', id: 'Bahasa Indonesia', ru: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439', ur: '\u0627\u0631\u062f\u0648',\n                    bn: '\u09ac\u09be\u0982\u09b2\u09be', sw: 'Kiswahili', de: 'Deutsch', tr: 'T\u00fcrk\u00e7e', ja: '\u65e5\u672c\u8a9e',\n                    te: '\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41', vi: 'Ti\u1ebfng Vi\u1ec7t', mr: '\u092e\u0930\u093e\u0920\u0940', ta: '\u0ba4\u0bae\u0bbf\u0bb4\u0bcd', it: 'Italiano'\n                };\n\n                function getPauseText(key) {\n                    const lookup = key === 'paused' ? 'title' : key;\n                    return t('pause.' + lookup);\n                }\n\n                function updateVisualIntensityUI() {\n                    const normalBtn = document.getElementById('visual-normal');\n                    const softBtn = document.getElementById('visual-soft');\n                    const valueEl = document.getElementById('visual-intensity-value');\n                    if (!normalBtn || !softBtn || !valueEl) return;\n                    normalBtn.classList.toggle('active', visualIntensityMode !== 'soft');\n                    softBtn.classList.toggle('active', visualIntensityMode === 'soft');\n                    valueEl.textContent = visualIntensityMode === 'soft' ? getPauseText('soft') : getPauseText('normal');\n                }\n\n                function setVisualIntensityMode(mode) {\n                    visualIntensityMode = mode === 'soft' ? 'soft' : 'normal';\n                    updateVisualIntensityUI();\n                    saveUserPreferences();\n                }\n\n                function bindVisualIntensityButtons() {\n                    const normalBtn = document.getElementById('visual-normal');\n                    const softBtn = document.getElementById('visual-soft');\n                    if (normalBtn && !normalBtn.dataset.bound) {\n                        normalBtn.dataset.bound = '1';\n                        normalBtn.addEventListener('click', () => setVisualIntensityMode('normal'));\n                        normalBtn.addEventListener('touchstart', (e) => { if (e.cancelable) e.preventDefault(); setVisualIntensityMode('normal'); }, { passive: false });\n                    }\n                    if (softBtn && !softBtn.dataset.bound) {\n                        softBtn.dataset.bound = '1';\n                        softBtn.addEventListener('click', () => setVisualIntensityMode('soft'));\n                        softBtn.addEventListener('touchstart', (e) => { if (e.cancelable) e.preventDefault(); setVisualIntensityMode('soft'); }, { passive: false });\n                    }\n                    updateVisualIntensityUI();\n                }\n\n                function updatePauseMenuText() {\n                    document.getElementById('pause-title').textContent = getPauseText('paused');\n                    document.getElementById('pause-continue').textContent = getPauseText('continue');\n                    document.getElementById('pause-restart-btn').textContent = getPauseText('restart');\n                    document.getElementById('pause-settings').textContent = getPauseText('settings');\n                    document.getElementById('pause-language').textContent = getPauseText('language');\n                    document.getElementById('settings-title').textContent = getPauseText('settings');\n                    document.getElementById('settings-back').textContent = getPauseText('back');\n                    document.getElementById('language-title').textContent = getPauseText('language');\n                    document.getElementById('language-back').textContent = getPauseText('back');\n                    document.getElementById('vol-bgm-label').textContent = getPauseText('bgm');\n                    document.getElementById('vol-weapon-label').textContent = getPauseText('weapon');\n                    document.getElementById('vol-monster-label').textContent = getPauseText('monster');\n                    document.getElementById('vol-ui-label').textContent = getPauseText('ui');\n                    document.getElementById('visual-intensity-label').textContent = getPauseText('visual');\n                    document.getElementById('visual-normal').querySelector('span').textContent = getPauseText('normal');\n                    document.getElementById('visual-soft').querySelector('span').textContent = getPauseText('soft');\n                    updateVisualIntensityUI();\n                    updateOrientationButtonLabels();\n                }\n\n                function showPausePanel(panel) {\n                    pauseMainMenu.style.display = 'none';\n                    pauseSettingsPanel.classList.remove('active');\n                    pauseLanguagePanel.classList.remove('active');\n                    if (panel === 'main') pauseMainMenu.style.display = 'flex';\n                    else if (panel === 'settings') {\n                        bindOrientationButtons();\n                        bindVisualIntensityButtons();\n                        pauseSettingsPanel.classList.add('active');\n                    }\n                    else if (panel === 'language') pauseLanguagePanel.classList.add('active');\n                }\n\n                function pauseGame() {\n                    if (gamePaused || gameState.over || !gameState.started) return;\n                    gamePaused = true;\n                    cancelActiveTouchControls();\n                    pauseOverlay.classList.add('active');\n                    showPausePanel('main');\n                    updatePauseMenuText();\n                    if (pointerLocked) document.exitPointerLock();\n                }\n\n                function resumeGame() {\n                    if (!gamePaused) return;\n                    gamePaused = false;\n                    cancelActiveTouchControls();\n                    pauseOverlay.classList.remove('active');\n                    if (!isTouchDevice && !gameState.upgradeScreenOpen) safeRequestPointerLock(canvas);\n                }\n\n                function buildLanguageGrid() {\n                    const grid = document.getElementById('language-grid');\n                    grid.innerHTML = '';\n                    const availableLocales = Object.keys(languageNames);\n                    availableLocales.forEach(loc => {\n                        const item = document.createElement('div');\n                        item.className = 'language-item' + (loc === currentLocale ? ' current' : '');\n                        item.textContent = languageNames[loc];\n                        item.addEventListener('click', () => switchLocale(loc));\n                        grid.appendChild(item);\n                    });\n                }\n\n                function switchLocale(newLocale) {\n                    const normalizedLocale = normalizeLocale(newLocale);\n                    if (!normalizedLocale) return;\n                    if (normalizedLocale === currentLocale) return;\n                    currentLocale = normalizedLocale;\n                    applyCurrentLocaleToDocument();\n                    updatePauseMenuText();\n                    buildLanguageGrid();\n                    // Update dynamic HUD text\n                    waveDisplay.textContent = t('hud.wave', { wave: gameState.currentWave });\n                    document.querySelector('.score-label').textContent = t('hud.score');\n                    updateWeaponDisplay();\n                    saveUserPreferences();\n                }\n\n                // Pause button handler\n                pauseOverlay.addEventListener('touchstart', (e) => { e.stopPropagation(); }, { passive: true });\n                pauseOverlay.addEventListener('touchmove', (e) => { e.stopPropagation(); }, { passive: true });\n                document.getElementById('pause-btn').addEventListener('click', (e) => {\n                    e.stopPropagation();\n                    if (gamePaused) resumeGame();\n                    else pauseGame();\n                });\n                document.getElementById('pause-btn').addEventListener('touchstart', (e) => {\n                    if (e.cancelable) e.preventDefault();\n                    e.stopPropagation();\n                    if (gamePaused) resumeGame();\n                    else pauseGame();\n                }, { passive: false });\n\n                // Pause menu item handlers\n                document.getElementById('pause-continue').addEventListener('click', resumeGame);\n                document.getElementById('pause-restart-btn').addEventListener('click', () => {\n                    resumeGame();\n                    restartGame();\n                });\n                document.getElementById('pause-settings').addEventListener('click', () => showPausePanel('settings'));\n                document.getElementById('pause-language').addEventListener('click', () => {\n                    buildLanguageGrid();\n                    showPausePanel('language');\n                });\n                document.getElementById('settings-back').addEventListener('click', () => showPausePanel('main'));\n                document.getElementById('language-back').addEventListener('click', () => showPausePanel('main'));\n\n                // Volume sliders\n                function setupVolumeSlider(sliderId, valueId, category) {\n                    const slider = document.getElementById(sliderId);\n                    const valueEl = document.getElementById(valueId);\n                    slider.value = Math.round(volumeMultipliers[category] * 100);\n                    valueEl.textContent = slider.value + '%';\n                    slider.addEventListener('input', () => {\n                        volumeMultipliers[category] = parseInt(slider.value) / 100;\n                        valueEl.textContent = slider.value + '%';\n                        updateLoopedSoundVolumes();\n                        saveUserPreferences();\n                    });\n                }\n                setupVolumeSlider('vol-bgm', 'vol-bgm-value', 'bgm');\n                setupVolumeSlider('vol-weapon', 'vol-weapon-value', 'weapon');\n                setupVolumeSlider('vol-monster', 'vol-monster-value', 'monster');\n                setupVolumeSlider('vol-ui', 'vol-ui-value', 'ui');\n\n                // ESC key to pause/unpause\n                document.addEventListener('keydown', (e) => {\n                    if (e.key === 'Escape') {\n                        e.preventDefault();\n                        if (gamePaused) resumeGame();\n                        else if (gameState.started && !gameState.over && !gameState.upgradeScreenOpen) pauseGame();\n                    }\n                });\n\n                // ===== GAME LOOP (drives ECS world update) =====\n                let lastTime = 0;\n                let lastIdleRenderTime = 0;\n                let lastActiveRenderTime = 0;\n                let lastFrameTimestamp = 0;\n                const frameCtx = {\n                    fired: false,\n                    muzzleFlashShots: 0,\n                    muzzleFlashWeaponIndex: -1,\n                    muzzleFlashSeed: 0,\n                    hitZombie: false,\n                    headshot: false,\n                    killConfirm: false,\n                    playerHit: false,\n                    weaponChanged: false,\n                    frameId: 0,\n                    spatialGridBuilt: false,\n                    katanaCandidates: [],\n                    bulletCandidates: [],\n                    bulletHits: [],\n                    bulletHitRecords: []\n                };\n                function renderFrame(ctx = null) {\n                    flushZombieInstanceUpdates();\n                    flushCorpseInstanceUpdates();\n                    flushProjectileTracerUpdates();\n                    flushFlameGpuUpdates();\n                    flushShellInstanceUpdates();\n                    flushBulletHoleInstances();\n                    flushBloodFlowInstances();\n                    flushTouchVisuals();\n                    const shakeX = cameraShakeOffsetX;\n                    const shakeY = cameraShakeOffsetY;\n                    if (shakeX !== 0 || shakeY !== 0) {\n                        camera.position.x += shakeX;\n                        camera.position.y += shakeY;\n                    }\n                    renderer.render(scene, camera);\n                    if (shakeX !== 0 || shakeY !== 0) {\n                        camera.position.x -= shakeX;\n                        camera.position.y -= shakeY;\n                    }\n                    if (weaponHolder.visible) {\n                        renderer.autoClear = false;\n                        renderer.clearDepth();\n                        renderer.render(weaponScene, weaponCamera);\n                        renderer.autoClear = true;\n                    }\n                }\n                function updateVisualEffectsOnly(dt) {\n                    const effDt = slowTimeActive ? dt * 0.01 : dt;\n                    updateBloodFlows(effDt);\n                    updateShellCasings(effDt);\n                    updateImpactSparkRuntime(effDt);\n                    updateBloodMistRuntime(effDt);\n                    updateSeveredRuntime(effDt);\n                    updateCorpseRuntime(effDt);\n                    updateCameraShakeRuntime(dt);\n                    updateComboSystem(dt);\n                }\n                function resetFrameCtx(ctx) {\n                    ctx.fired = false;\n                    ctx.muzzleFlashShots = 0;\n                    ctx.muzzleFlashWeaponIndex = -1;\n                    ctx.muzzleFlashSeed = 0;\n                    ctx.hitZombie = false;\n                    ctx.headshot = false;\n                    ctx.killConfirm = false;\n                    ctx.playerHit = false;\n                    ctx.weaponChanged = false;\n                    ctx.frameId = (gameState.frameId = (gameState.frameId || 0) + 1);\n                    weaponAnim._dingPlayedFrame = 0;\n                    return ctx;\n                }\n                function gameLoop(timestamp) {\n                    // Browsers usually throttle rAF when hidden, but bail explicitly to\n                    // skip the whole frame's work (perf budget sampling, sim, render).\n                    if (typeof document !== 'undefined' && document.hidden) {\n                        lastFrameTimestamp = timestamp;\n                        lastTime = timestamp;\n                        requestAnimationFrame(gameLoop);\n                        return;\n                    }\n                    perfFrameTick++;\n                    if (lastFrameTimestamp > 0) updatePerformanceBudget(timestamp - lastFrameTimestamp);\n                    lastFrameTimestamp = timestamp;\n                    if (perfProfile.targetFrameMs > 0) {\n                        if (lastActiveRenderTime === 0) lastActiveRenderTime = timestamp - perfProfile.targetFrameMs;\n                        if (timestamp - lastActiveRenderTime < perfProfile.targetFrameMs - 0.5) {\n                            requestAnimationFrame(gameLoop);\n                            return;\n                        }\n                        lastActiveRenderTime += perfProfile.targetFrameMs;\n                        if (timestamp - lastActiveRenderTime > perfProfile.targetFrameMs * 2) {\n                            lastActiveRenderTime = timestamp;\n                        }\n                    }\n                    const dt = Math.min((timestamp - lastTime) / 1000, 0.11);\n                    lastTime = timestamp;\n\n                    if (!gameState.started || gameState.over) {\n                        if (timestamp - lastIdleRenderTime >= perfProfile.idleFrameMs) {\n                            lastIdleRenderTime = timestamp;\n                            renderFrame(null);\n                        }\n                        requestAnimationFrame(gameLoop);\n                        return;\n                    }\n\n                    // Pause check - freeze game logic, only render static scene\n                    if (gamePaused) {\n                        if (timestamp - lastIdleRenderTime >= perfProfile.idleFrameMs) {\n                            lastIdleRenderTime = timestamp;\n                            renderFrame(null);\n                        }\n                        requestAnimationFrame(gameLoop);\n                        return;\n                    }\n\n                    // Check backpack toggle\n                    checkBackpackToggle();\n                    if (backpackOpen) {\n                        updateVisualEffectsOnly(dt);\n                        if (timestamp - lastIdleRenderTime >= perfProfile.idleFrameMs) {\n                            lastIdleRenderTime = timestamp;\n                            renderFrame(null);\n                        }\n                        requestAnimationFrame(gameLoop);\n                        return;\n                    }\n\n                    // Upgrade / weapon select modals fully obscure the gameplay view \u2014\n                    // keep visual effects ticking but render at idle pace to save GPU/CPU.\n                    if (gameState.upgradeScreenOpen || weaponSelectOpen) {\n                        updateVisualEffectsOnly(dt);\n                        if (timestamp - lastIdleRenderTime >= perfProfile.idleFrameMs) {\n                            lastIdleRenderTime = timestamp;\n                            renderFrame(null);\n                        }\n                        requestAnimationFrame(gameLoop);\n                        return;\n                    }\n\n                    resetFrameCtx(frameCtx);\n\n                    // Single ECS update tick - unidirectional data flow through systems\n                    processPendingTouchInput();\n                    updateWasdDisplay();\n\n                    // Update Slow Time Skill Timer/Cooldown\n                    if (slowTimeActive) {\n                        slowTimeTimer -= dt;\n                        if (slowTimeTimer <= 0) {\n                            slowTimeActive = false;\n                            slowTimeCooldownTimer = slowTimeCooldown;\n                            const skillBtn = document.getElementById('skill-btn');\n                            if (skillBtn) {\n                                skillBtn.classList.remove('active');\n                                skillBtn.classList.add('cooldown');\n                            }\n                        }\n                    } else if (slowTimeCooldownTimer > 0) {\n                        slowTimeCooldownTimer -= dt;\n                        const skillBtn = document.getElementById('skill-btn');\n                        if (skillBtn) {\n                            skillBtn.querySelector('.skill-label').textContent = Math.ceil(slowTimeCooldownTimer) + 's';\n                        }\n                        if (slowTimeCooldownTimer <= 0) {\n                            slowTimeCooldownTimer = 0;\n                            if (skillBtn) {\n                                skillBtn.classList.remove('cooldown');\n                                skillBtn.querySelector('.skill-label').textContent = isTouchDevice ? 'SLOW' : 'SLOW [Q]';\n                            }\n                        }\n                    }\n\n                    const simDt = (hitStopTimer > 0) ? 0 : dt;\n\n                    if (hitStopTimer > 0) hitStopTimer = Math.max(0, hitStopTimer - dt);\n\n                    if (weaponTutorialActive) {\n                        const slotEl = weaponSlotEls[roguelikeState.ownedWeapons[1]];\n                        if (slotEl) {\n                            tutorialSpotlightScale += (1.0 - tutorialSpotlightScale) * expSmoothingFactor(dt, 3.5);\n                            if (Math.abs(tutorialSpotlightScale - 1.0) < 0.01) {\n                                tutorialSpotlightScale = 1.0;\n                                // Close the spotlight but keep the \ud83d\udc47 small hand and do not block inputs!\n                                weaponTutorialActive = false;\n                                const overlay = document.getElementById('weapon-tutorial-overlay');\n                                if (overlay) {\n                                    overlay.classList.remove('active');\n                                    const cvs = overlay.querySelector('canvas');\n                                    if (cvs) {\n                                        const ctx2d = cvs.getContext('2d');\n                                        ctx2d.clearRect(0, 0, cvs.width, cvs.height);\n                                    }\n                                }\n                            } else {\n                                drawTutorialSpotlight(slotEl, tutorialSpotlightScale);\n                            }\n                        }\n                    }\n\n                    // In order to slow down only zombies and bullets but keep player responsive,\n                    // we'll pass scaled dt selectively or handle inside the systems.\n                    // Let's pass the unscaled simDt and inside each world.update/systems we will scale them.\n                    world.update(simDt, frameCtx);\n                    updateVisualEffectsOnly(dt);\n\n                    renderFrame(frameCtx);\n\n                    requestAnimationFrame(gameLoop);\n                }\n                startScreen.style.display = 'none';\n                showWeaponSelectScreen();\n                requestAnimationFrame(() => requestAnimationFrame(hideInitialLoading));\n                requestAnimationFrame(gameLoop);\n            }\n\n            // ===== BUILD BUNKER SCENE =====\n            function buildBunkerScene(scene) {\n                const config = window.gameConfig;\n                const scenePerfProfile = window.__lastSurvivorPerfProfile || null;\n                const simpleLighting = !!(scenePerfProfile && scenePerfProfile.lowEnd);\n                const useLambert = !!(scenePerfProfile && (scenePerfProfile.useLambert || scenePerfProfile.lowEnd));\n\n                // Lighting based on config\n                let ambientColor = 0x2a3040;\n                let lightColor = 0xb8c5d6;\n                const lightSetting = config.lightingColor || 'cool';\n                switch (lightSetting) {\n                    case 'warm': ambientColor = 0x3a2820; lightColor = 0xffaa66; break;\n                    case 'red': ambientColor = 0x2a1010; lightColor = 0xff4444; break;\n                    case 'green': ambientColor = 0x102a10; lightColor = 0x44ff44; break;\n                }\n\n                const ambient = new THREE.AmbientLight(ambientColor, simpleLighting ? 0.52 : 0.6);\n                scene.add(ambient);\n\n                const dirLight = new THREE.DirectionalLight(lightColor, simpleLighting ? 0.72 : 0.8);\n                dirLight.position.set(0, 8, 5);\n                scene.add(dirLight);\n\n                // Point lights for atmosphere\n                if (!simpleLighting) {\n                    const light1 = new THREE.PointLight(0xff6b35, 0.5, 10);\n                    light1.position.set(-7, 3, -10);\n                    scene.add(light1);\n\n                    const light2 = new THREE.PointLight(0xff6b35, 0.5, 10);\n                    light2.position.set(7, 3, -10);\n                    scene.add(light2);\n\n                    const light3 = new THREE.PointLight(lightColor, 0.4, 15);\n                    light3.position.set(0, 3.5, 5);\n                    scene.add(light3);\n                }\n\n                // Floor\n                // Grid baked into floor canvas texture \u2014 zero z-fighting, saves one draw call.\n                // UV mapping (Three.js flipY default): u=(wx+11)/22, canvasY=(20-wz)/40*H\n                function createFloorGridTexture() {\n                    const W = 512, H = 1024;\n                    const cv = document.createElement('canvas');\n                    cv.width = W; cv.height = H;\n                    const c = cv.getContext('2d');\n                    c.fillStyle = '#2d2d2d';\n                    c.fillRect(0, 0, W, H);\n                    c.strokeStyle = useLambert ? '#788492' : '#5f6976';\n                    c.lineWidth = useLambert ? 1.5 : 1.2;\n                    // Vertical lines (constant world x)\n                    for (let wx = -10; wx <= 10; wx += 2) {\n                        const cx = (wx + 11) / 22 * W;\n                        c.beginPath(); c.moveTo(cx, 0); c.lineTo(cx, H); c.stroke();\n                    }\n                    // Horizontal lines (constant world z)\n                    for (let wz = -19; wz <= 15; wz += 2) {\n                        const cy = (20 - wz) / 40 * H;\n                        c.beginPath(); c.moveTo(0, cy); c.lineTo(W, cy); c.stroke();\n                    }\n                    const tex = new THREE.CanvasTexture(cv);\n                    tex.wrapS = THREE.ClampToEdgeWrapping;\n                    tex.wrapT = THREE.ClampToEdgeWrapping;\n                    tex.anisotropy = 4; // Reduces grid aliasing at oblique camera angles\n                    return tex;\n                }\n                const sceneMaterialCtor = (useLambert || simpleLighting) ? THREE.MeshLambertMaterial : THREE.MeshPhongMaterial;\n                const floorGeo = new THREE.PlaneGeometry(22, 40);\n                const floorMat = new sceneMaterialCtor({ color: 0xffffff, map: createFloorGridTexture(), side: THREE.DoubleSide });\n                const floor = new THREE.Mesh(floorGeo, floorMat);\n                floor.rotation.x = -Math.PI / 2;\n                floor.position.set(0, 0, 0);\n                scene.add(floor);\n\n                // Ceiling\n                const ceilGeo = new THREE.PlaneGeometry(22, 40);\n                const ceilMat = new sceneMaterialCtor({ color: 0x3a3a3a, side: THREE.DoubleSide });\n                const ceiling = new THREE.Mesh(ceilGeo, ceilMat);\n                ceiling.rotation.x = Math.PI / 2;\n                ceiling.position.set(0, 5.5, 0);\n                scene.add(ceiling);\n\n                // Static bunker geometry: all BoxGeometry meshes are merged into ONE InstancedMesh\n                // with per-instance color via setColorAt(), cutting 6 draw calls down to 1.\n                const pipeMat = new sceneMaterialCtor({ color: 0x6a6a6a });\n                const staticBoxGeo = new THREE.BoxGeometry(1, 1, 1);\n                const staticPipeGeo = new THREE.CylinderGeometry(1, 1, 1, 6);\n                const staticMatrix = new THREE.Matrix4();\n                const staticPosition = new THREE.Vector3();\n                const staticScale = new THREE.Vector3();\n                const staticQuat = new THREE.Quaternion();\n                const staticEuler = new THREE.Euler();\n                const staticBoxAccum = []; // { x,y,z,sx,sy,sz,rx,ry,rz, color }\n\n                function addStaticBoxInstances(name, colorHex, boxes) {\n                    for (let i = 0; i < boxes.length; i++) {\n                        staticBoxAccum.push(Object.assign({ color: colorHex }, boxes[i]));\n                    }\n                }\n\n                function addStaticPipeInstances(name, material, pipes) {\n                    if (!pipes.length) return null;\n                    const mesh = new THREE.InstancedMesh(staticPipeGeo, material, pipes.length);\n                    mesh.name = name;\n                    for (let i = 0; i < pipes.length; i++) {\n                        const p = pipes[i];\n                        staticPosition.set(p.x, p.y, p.z);\n                        staticEuler.set(p.rx || 0, p.ry || 0, p.rz || 0);\n                        staticQuat.setFromEuler(staticEuler);\n                        staticScale.set(p.radius, p.length, p.radius);\n                        staticMatrix.compose(staticPosition, staticQuat, staticScale);\n                        mesh.setMatrixAt(i, staticMatrix);\n                    }\n                    mesh.instanceMatrix.needsUpdate = true;\n                    if (mesh.computeBoundingSphere) mesh.computeBoundingSphere();\n                    scene.add(mesh);\n                    return mesh;\n                }\n\n                addStaticBoxInstances('bunkerWallInstances', 0x4a4a4a, [\n                    { x: -11, y: 2.75, z: 0, sx: 0.5, sy: 5.5, sz: 40 },\n                    { x: 11, y: 2.75, z: 0, sx: 0.5, sy: 5.5, sz: 40 },\n                    { x: 0, y: 2.75, z: 20, sx: 22, sy: 5.5, sz: 0.5 }\n                ]);\n\n                addStaticBoxInstances('bunkerFrontWallInstances', 0x3a3a3a, [\n                    { x: -10, y: 2.75, z: -12, sx: 2, sy: 5.5, sz: 0.5 },\n                    { x: -3.5, y: 2.75, z: -12, sx: 3, sy: 5.5, sz: 0.5 },\n                    { x: 3.5, y: 2.75, z: -12, sx: 3, sy: 5.5, sz: 0.5 },\n                    { x: 10, y: 2.75, z: -12, sx: 2, sy: 5.5, sz: 0.5 }\n                ]);\n\n\n                addStaticBoxInstances('bunkerCorridorWallInstances', 0x353535, [\n                    { x: -9, y: 2.75, z: -16, sx: 0.3, sy: 5.5, sz: 8 },\n                    { x: -5, y: 2.75, z: -16, sx: 0.3, sy: 5.5, sz: 8 },\n                    { x: -2, y: 2.75, z: -16, sx: 0.3, sy: 5.5, sz: 8 },\n                    { x: 2, y: 2.75, z: -16, sx: 0.3, sy: 5.5, sz: 8 },\n                    { x: 5, y: 2.75, z: -16, sx: 0.3, sy: 5.5, sz: 8 },\n                    { x: 9, y: 2.75, z: -16, sx: 0.3, sy: 5.5, sz: 8 }\n                ]);\n\n                const beamBoxes = [];\n                for (let x = -8; x <= 8; x += 4) {\n                    if (x !== 0) beamBoxes.push({ x, y: 2.75, z: 5, sx: 0.3, sy: 5.5, sz: 0.3 });\n                }\n                addStaticBoxInstances('bunkerBeamInstances', 0x5a5a5a, beamBoxes);\n\n                const pipeInstances = [];\n                for (let z = -8; z <= 12; z += 5) {\n                    pipeInstances.push({ x: 0, y: 5.3, z, radius: 0.08, length: 22, rz: Math.PI / 2 });\n                }\n                addStaticPipeInstances('bunkerPipeInstances', pipeMat, pipeInstances);\n\n                addStaticBoxInstances('bunkerCrateInstances', 0x4a5a3a, [\n                    { x: -8, y: 0.3, z: 12, sx: 1, sy: 0.6, sz: 0.6 },\n                    { x: 8, y: 0.3, z: 12, sx: 1, sy: 0.6, sz: 0.6 },\n                    { x: -5, y: 0.3, z: 17, sx: 1, sy: 0.6, sz: 0.6 },\n                    { x: 5, y: 0.3, z: 17, sx: 1, sy: 0.6, sz: 0.6 }\n                ]);\n\n                // Emit all box instances as a single InstancedMesh with per-instance color.\n                if (staticBoxAccum.length > 0) {\n                    const boxMat = new sceneMaterialCtor({ color: 0xffffff });\n                    const allBoxMesh = new THREE.InstancedMesh(staticBoxGeo, boxMat, staticBoxAccum.length);\n                    allBoxMesh.name = 'bunkerStaticBoxInstances';\n                    const boxColor = new THREE.Color();\n                    for (let i = 0; i < staticBoxAccum.length; i++) {\n                        const b = staticBoxAccum[i];\n                        staticPosition.set(b.x, b.y, b.z);\n                        staticEuler.set(b.rx || 0, b.ry || 0, b.rz || 0);\n                        staticQuat.setFromEuler(staticEuler);\n                        staticScale.set(b.sx, b.sy, b.sz);\n                        staticMatrix.compose(staticPosition, staticQuat, staticScale);\n                        allBoxMesh.setMatrixAt(i, staticMatrix);\n                        allBoxMesh.setColorAt(i, boxColor.setHex(b.color));\n                    }\n                    allBoxMesh.instanceMatrix.needsUpdate = true;\n                    if (allBoxMesh.instanceColor) allBoxMesh.instanceColor.needsUpdate = true;\n                    scene.add(allBoxMesh);\n                }\n\n\n                scene.traverse(obj => {\n                    if (!obj || obj === scene) return;\n                    obj.updateMatrix();\n                    obj.matrixAutoUpdate = false;\n                });\n            }\n        }\n    </script>\n</body>\n\n</html>\n", "game_config": "{\"zombieHealthMultiplier\":0.8,\"zombieSpeedMultiplier\":0.9,\"startingWave\":1,\"headshotMultiplier\":2,\"lightingColor\":\"cool\",\"katanaSpeed\":1}", "game_design_document": "## Core Game Loop and Mechanics\n\n**Last Stand Bunker** combines first-person shooter mechanics with wave-based survival gameplay. Players defend a multi-room underground bunker from increasingly difficult zombie waves.\n\n### Moment-to-Moment Gameplay\n- Players move through a 3D bunker environment in first-person perspective\n- Zombies spawn from designated entry points and advance toward the player's position\n- Players aim and shoot using various weapons to eliminate zombies before they reach critical areas\n- Between waves, players can purchase new weapons, upgrade existing ones, and place barricades\n- Managing ammunition is crucial - players must balance firepower with conservation\n\n### Progression and Engagement\n- Each wave increases in difficulty with more zombies and tougher enemy types\n- Players earn currency by killing zombies, which can be spent on:\n  - New weapons (unlocked progressively)\n  - Weapon upgrades (damage, fire rate, magazine size)\n  - Barricades to slow zombie advancement\n  - Ammunition resupply\n- Score multipliers reward accuracy and consecutive kills\n- The challenge comes from resource management, positioning, and weapon selection\n\n### Win/Lose Conditions\n- **Lose Condition:** Zombies breach the bunker's core room (where the player respawns)\n- **Win Condition:** Survive as many waves as possible - this is an endless survival mode with high score tracking\n- After losing, players see their final wave number, kill count, and accuracy statistics\n\n### What Makes It Compelling\n- Satisfying gunplay with distinct weapon feels (sniper precision vs shotgun spread vs rocket explosions)\n- Tactical decisions about positioning and resource allocation\n- Escalating tension as waves become more challenging\n- Replayability through trying different weapon loadouts and strategies\n\n## Starting Scope\n\nFor the first iteration, we'll implement a **highly polished but simplified version** that captures the core FPS defense experience:\n\n### Phase 1 - Initial Implementation (Start Here)\n**Bunker Layout:**\n- Single large rectangular room with simple geometry\n- 2-3 entry corridors where zombies spawn\n- Clear sightlines for shooting\n\n**Weapons (3 types to start):**\n1. **Pistol** - Starting weapon, unlimited ammo, moderate damage\n2. **Shotgun** - Close range, high damage, limited ammo\n3. **Sniper Rifle** - Long range, very high damage, slow fire rate, limited ammo\n\n**Zombie Types:**\n- Single basic zombie type that walks toward the player\n- Simple health system (multiple hits to kill)\n\n**Core Systems:**\n- First-person movement and mouse look with pointer lock\n- Weapon switching (number keys 1-3)\n- Aiming and shooting mechanics\n- Hit detection and damage\n- Wave system (5 waves to start, increasing zombie count)\n- Simple currency system (earn points per kill)\n- Ammunition management (pickup ammo between waves)\n- Basic UI showing: health, ammo, current wave, score\n\n**Visual Style:**\n- Low-poly 3D art style for performance\n- Simple but atmospheric lighting\n- Clear visual feedback for hits and kills\n\n### Phase 2 - Future Enhancements (Later Iterations)\n- Additional weapons (machine gun, rocket launcher, flamethrower)\n- Multiple zombie types (fast runners, tank zombies, exploding zombies)\n- Multi-room bunker with strategic chokepoints\n- Placeable barricades and traps\n- Weapon upgrade system\n- More sophisticated AI (zombies break through doors, climb obstacles)\n- Power-ups and special abilities\n- Leaderboard integration\n\n## Game Perspective\n\n**3D First-Person** perspective is essential for this game concept.\n\n### Camera Implementation\n- Standard FPS camera with mouse look (pointer lock on desktop)\n- Camera positioned at player eye height (~1.7 units above ground)\n- Smooth mouse sensitivity for aiming\n- Weapon model visible in lower right of screen\n- Crosshair centered on screen for aiming\n\n### Mobile Adaptation\n- Left virtual joystick for movement (WASD equivalent)\n- Right virtual joystick for camera look and aiming\n- Tap right side of screen to shoot\n- Weapon switch buttons in bottom right corner\n- Auto-aim assistance to compensate for touch controls (subtle magnetism toward zombie heads)\n\n## Game World and Level Design\n\n### Bunker Layout (Phase 1)\nThe initial bunker is a single defensive room designed for clear gameplay:\n\n**Main Defense Chamber:**\n- Rectangular room approximately 20 units wide \u00d7 30 units deep \u00d7 4 units high\n- Player starts in the back third of the room (the \"safe zone\")\n- Three entry corridors at the far end where zombies spawn:\n  - Left corridor (45\u00b0 angle)\n  - Center corridor (straight ahead)\n  - Right corridor (45\u00b0 angle)\n- Each corridor is 3 units wide \u00d7 10 units long\n\n**Environmental Details:**\n- Concrete walls with industrial bunker aesthetic\n- Metal support beams and pipes for visual interest\n- Dim overhead lighting with emergency lights near corridors\n- Ammo crates positioned near the back wall (interact to refill)\n- Weapon rack on side wall for weapon purchases\n\n**Spawn System:**\n- Zombies spawn at the far end of corridors (outside player's view)\n- They walk down corridors and enter the main chamber\n- Spawn rate increases with each wave\n- Multiple zombies can be active simultaneously\n\n**Spatial Strategy:**\n- Long sightlines favor sniper rifle\n- Corridor chokepoints favor shotgun\n- Player must rotate attention between three entry points\n- Backing up too far means less reaction time\n\n### Future Level Expansion (Phase 2+)\n- Multiple connected rooms with doors\n- Stairways to upper/lower bunker levels\n- Narrow corridors vs open chambers for varied combat\n- Destructible barricades that slow zombies\n- Ventilation shafts where special zombies emerge\n\n## Edit Mode Design\n\nEdit mode allows players to customize their defensive setup and game parameters before starting a wave.\n\n### Editable Properties\n\n**Weapon Loadout:**\n- Choose starting weapon (pistol, shotgun, sniper)\n- Set starting ammunition counts for each weapon\n- Toggle weapon availability (enable/disable specific weapons)\n\n**Difficulty Settings:**\n- Starting wave number (1-10)\n- Zombie health multiplier (0.5x - 3x)\n- Zombie speed multiplier (0.5x - 2x)\n- Starting currency amount (0 - 5000)\n\n**Visual Customization:**\n- Bunker lighting color (warm, cool, emergency red, night vision green)\n- Crosshair style (dot, cross, circle)\n- UI color scheme\n\n**Gameplay Modifiers:**\n- Headshot damage multiplier (1x - 5x)\n- Ammo scarcity (abundant, normal, scarce)\n- Enable/disable friendly fire (for future multiplayer)\n\n### Edit Mode Interface\n- Accessed from main menu before starting game\n- Clean panel-based UI with sliders and toggles\n- \"Reset to Default\" button for each section\n- \"Save Preset\" option to store favorite configurations\n- Preview window showing current bunker appearance\n\n### Constraints\n- Weapon damage values have reasonable ranges (can't one-shot everything)\n- Zombie counts per wave have maximums to prevent performance issues\n- Starting currency capped to prevent trivializing early waves\n- At least one weapon must be enabled\n\n## Control Design\n\n**Last Stand Bunker** is a 3D first-person shooter requiring precise aiming and quick weapon switching.\n\n### Desktop Controls (Keyboard + Mouse)\n\n**Movement:**\n- WASD or Arrow Keys - Move forward/left/back/right\n- Shift - Sprint (1.5x movement speed)\n- Space - Jump (small hop, mainly for navigating obstacles)\n\n**Camera & Aiming:**\n- Mouse Movement - Look around (pointer lock enabled during gameplay)\n- Pointer lock activates when clicking into game area\n- Pointer lock releases when pressing Escape or opening menus\n\n**Combat:**\n- Left Mouse Button - Fire weapon\n- Right Mouse Button - Aim down sights (zoom for sniper, tighter spread for shotgun)\n- R - Reload current weapon\n- 1, 2, 3, 4, 5 - Switch to weapon slot (pistol, shotgun, sniper, etc.)\n- Mouse Wheel - Cycle through available weapons\n\n**Interaction:**\n- E - Interact (pick up ammo, open weapon shop, place barricades)\n\n**UI:**\n- Tab - View scoreboard/stats\n- Escape - Pause menu\n- M - Toggle map (future feature)\n\n**Input Handling:**\n- Prevent default browser behavior for all game keys\n- Clear all input states on window blur\n- Convert keycodes to lowercase before storing state\n\n### Mobile Controls (Touch)\n\n**Movement:**\n- Left virtual joystick - Move in any direction\n  - Appears where player touches left half of screen (below UI)\n  - Base position set at touch start\n  - Max radius: 60 pixels\n  - Dead zone: 10 pixels\n  - **Base repositioning:** If finger moves beyond max radius, slide the base so finger stays at max radius from base\n  - Analog output: -1 to +1 for X and Y axes\n\n**Camera & Aiming:**\n- Right virtual joystick - Look around and aim\n  - Appears where player touches right half of screen (below UI)\n  - Same joystick mechanics as movement\n  - Controls camera rotation\n  - Includes subtle auto-aim assistance (15\u00b0 cone, slight pull toward zombie heads)\n\n**Combat:**\n- Tap anywhere on right half (outside joystick radius) - Fire weapon\n- Or: Auto-fire when aiming at enemies (toggle in settings)\n- Reload button - Bottom right corner (96\u00d796px)\n- Weapon switch buttons - Bottom right, above reload (64\u00d764px each)\n  - Show weapon icons\n  - Tap to switch\n\n**Interaction:**\n- Interact button appears contextually when near interactive objects (ammo crates, shop)\n- Positioned in center-bottom of screen (128\u00d796px)\n\n**UI:**\n- Pause button - Top right corner (64\u00d764px)\n- Settings button - Top right, below pause\n\n**Touch Handling:**\n- Track touch IDs to prevent interference between joysticks\n- Left touches (ID tracking) control movement only\n- Right touches control camera/shooting only\n- UI buttons have priority over joystick creation\n- Release pointer lock when showing pause/shop menus\n\n### Weapon-Specific Controls\n\n**Pistol:**\n- Semi-automatic (one shot per click)\n- No aim down sights\n- Fast weapon switch time\n\n**Shotgun:**\n- Pump action (slight delay between shots)\n- Aim down sights tightens spread by 30%\n- Slower weapon switch time\n\n**Sniper Rifle:**\n- Bolt action (longer delay between shots)\n- Aim down sights provides 2x zoom\n- Slowest weapon switch time\n- Scope sway when not holding breath (future: hold Shift to steady)\n\n### Control Feedback\n- Crosshair changes color when hovering over enemy (red tint)\n- Weapon recoil and camera kick\n- Hit markers appear on successful hits\n- Ammo counter flashes red when low (<25%)\n- Reload prevents firing\n\n## Art Style and Visual Theme\n\n**Last Stand Bunker** uses a **low-poly 3D art style** with a **dark, atmospheric military bunker aesthetic**. This style balances visual appeal with performance requirements for browser-based 3D gaming.\n\n### Overall Visual Direction\n- **Low-poly 3D models** with flat shading and minimal textures\n- **Dark, moody atmosphere** with strategic lighting\n- **Military/industrial bunker theme** - concrete, metal, emergency lighting\n- **Desaturated color palette** with pops of color for important elements\n- **Clear visual hierarchy** - enemies and interactive objects stand out\n\n### Color Palette\n**Environment:**\n- Concrete walls: Dark gray (#3a3a3a) to medium gray (#6b6b6b)\n- Metal elements: Gunmetal (#4a5568) with rust accents (#8b4513)\n- Floors: Darker gray (#2d2d2d) with subtle grid lines\n\n**Lighting:**\n- Primary: Dim cool white overhead lights (#b8c5d6)\n- Accent: Warm emergency lights near corridors (#ff6b35)\n- Ambient: Dark blue-gray (#1a1f2e) for shadows\n\n**Gameplay Elements:**\n- Zombies: Sickly green-gray skin (#7a8a6f), dark clothing\n- Player weapons: Metallic gray with black grips\n- Muzzle flash: Bright yellow-orange (#ffaa00)\n- Blood effects: Dark crimson (#8b0000)\n- UI elements: Bright cyan (#00d9ff) for readability\n\n### Asset Style Guidelines\n\n**3D Models:**\n- Low polygon count (zombies: 500-800 tris, weapons: 300-500 tris)\n- Flat or simple gradient shading\n- Hard edges and angular forms\n- No complex textures - use vertex colors or simple color blocks\n\n**Weapons:**\n- Recognizable silhouettes (clear weapon identification)\n- Metallic gray base with black accents\n- Visible mechanical details (trigger, magazine, scope)\n- Consistent scale relative to viewport\n\n**Zombies:**\n- Humanoid form with exaggerated proportions (longer arms, hunched posture)\n- Tattered clothing in dark colors\n- Visible wounds/decay (darker patches, exposed areas)\n- Glowing eyes for visibility in dark areas (#ff3333)\n- Distinct silhouette from different angles\n\n**Environment:**\n- Modular wall/floor pieces for bunker construction\n- Industrial details: pipes, vents, support beams, electrical boxes\n- Minimal decorative elements - focus on functional geometry\n- Strategic light sources (overhead fixtures, emergency strips)\n\n### Lighting Strategy\n- **Dim ambient lighting** to create tension\n- **Directional overhead lights** casting shadows\n- **Emergency lights** near zombie spawn corridors (red/orange glow)\n- **Muzzle flash** provides brief illumination during combat\n- **Zombie eyes glow** subtly to aid visibility\n\n### Visual Effects\n- **Muzzle flash:** Bright sprite at weapon barrel, 2-3 frames\n- **Blood splatter:** Small particle burst on zombie hit (dark red droplets)\n- **Death effect:** Zombie ragdoll or simple collapse\n- **Hit feedback:** Brief red outline flash on damaged zombie\n- **Bullet tracers:** Thin yellow lines for rapid-fire weapons (optional)\n\n### UI Visual Style\n- **Minimalist HUD** with semi-transparent dark panels\n- **Bright cyan text** (#00d9ff) for high contrast\n- **Weapon icon** in bottom right (simple silhouette)\n- **Ammo counter** with large, readable numbers\n- **Health bar** in bottom left (red fill, gray background)\n- **Wave indicator** at top center\n- **Crosshair:** Simple white cross or dot, changes to red when on target\n\n### Camera and Perspective\n- **FOV:** 75-80 degrees for comfortable FPS view\n- **Weapon viewmodel:** Positioned in lower right, slightly tilted\n- **Head bob:** Subtle movement during walking (can be toggled off)\n- **Recoil:** Camera kick upward on firing, returns smoothly\n\n### Performance Considerations\n- Use simple geometry to maintain 60 FPS target\n- Limit active zombies on screen (10-15 maximum)\n- Particle effects are minimal and short-lived\n- No complex shaders or post-processing effects\n- Static lighting (no real-time shadows for performance)\n\n### Consistency Requirements\n- All assets share the same low-poly aesthetic\n- Lighting angles consistent across all models\n- Color palette maintained throughout\n- Scale relationships preserved (zombie height ~1.8 units, player eye height ~1.7 units)", "persistent_storage_config": {"leaderboard": {"sort_ascending": false}}, "game_metadata": {"game_id": "01KTG9SHNB65HM9RSEPV2ZDKAF", "title": "zombie survivor", "description": "Survive endless zombie hordes with devastating weaponry\u2014sniper rifles, shotguns, katana, and gatling guns. Master limb dismemberment, visceral gore, and roguelike upgrades. How long can you endure?", "thumbnail_url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_118012379502927147068/1782386283815_01KVZ80N97FKEWSZY76KRRXQ7Z.687b9e62-b9a7-4dea-bb86-5e91ebd2c487.png", "created_at": 1780811093, "updated_at": 1782693044883.485, "art_style": null, "is_remix": true, "base_game_id": "01KTC2C7W11XJ4919KPZE77BVF"}, "task_plan": null}