{"asset_map": {"blob_texture_glossy": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/blob_texture_glossy_325fb34b-bc3c-49b3-bdca-6f567bf50bf7.webp", "type": "image", "prompt": "Generate a seamless 1:1 tileable texture for a glossy, reflective spherical surface. The texture should look like smooth, shiny, slightly translucent red jelly or slime. Include subtle highlights and soft reflections to suggest a 3D rounded form. Use a slight gradient from a lighter top to a darker bottom. Cartoonish, clean, and appealing style, high resolution, professional game asset.", "description": "A glossy, reflective red jelly texture for the player's 3D blob character.", "subtype": "other", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "slime_texture_glow": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/slime_texture_glow_800e624e-6f03-4631-94bd-c163e596c61a.webp", "type": "image", "prompt": "Generate a seamless, tileable soft glowing orb texture. The design should be a radial gradient from a bright white center to a soft, semi-transparent edge. Ethereal, soft, and glowing appearance suitable for a game collectible. White and neutral tones so it can be tinted, high resolution, clean edges.", "description": "A neutral glowing radial texture used for the collectible slime orbs.", "subtype": "other", "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "ui_button_play": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ui_button_play_357180e8-e69f-444f-ad0d-1868fc86e7da.webp", "type": "image", "prompt": "Generate a wide rectangular UI button with heavily rounded corners. Vibrant neon-green color with a glossy, 3D appearance. Include a subtle gradient from light green at the top to a slightly darker green at the bottom. Add a soft inner glow and a subtle drop shadow. No text, clean polished surface, modern cartoonish UI style, transparent background.", "description": "A glossy neon-green 3D button for the main menu.", "subtype": "ui", "aspect_ratio": [1253, 545], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "grid_floor_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/grid_floor_texture_322b446e-c8ab-4e4c-a6a6-91eb6a46145e.webp", "type": "image", "prompt": "Generate a seamless, tileable 1024x1024 texture for a game floor. Futuristic digital aesthetic featuring a dark blue-gray background with glowing cyan and electric blue grid lines forming perfect squares. The lines should have a subtle bloom/glow effect. Include a very subtle radial gradient that is slightly lighter in the center to add depth. Professional quality, clean lines, no seams.", "description": "A futuristic neon cyan grid texture for the arena floor.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "background_music_upbeat": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_105667223504694108789/1780159990038_01KSWWVJ8PGBD5XWSVM03MWXVN.Aurelion Sol, the Star Forger  Login Screen - League of Legends.mp3", "type": "audio", "prompt": null, "description": "Aurelion Sol theme - epic, celestial background music for menu and gameplay", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "opus_url": null, "animation": null}, "heavy_splat_sfx": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_105667223504694108789/1780075915222_01KSTCNSYPC90KRZ9ND6NTRZ01.Minecraft ITEM PLOP Sound Effect (Item PickupDrop).mp3", "type": "audio", "prompt": null, "description": "Minecraft item plop sound effect for heavy splat/gulp when a blob absorbs another player", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "opus_url": null, "animation": null}, "earth_texture": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_105667223504694108789/1780061268964_01KSSYPTZ4SXY6WH8WGS3G8H9X.image.png", "type": "image", "prompt": null, "description": "Custom Earth texture for spherical mapping on 3D blob characters - shows realistic Earth surface with continents, oceans, and cloud patterns", "subtype": null, "aspect_ratio": [720, 360], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "moon_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/moon_texture_2330dc5a-c22b-4a54-b4f9-2f14053a6f13.webp", "type": "image", "prompt": "Generate a 2:1 aspect ratio seamless equirectangular moon texture for spherical UV mapping. Stylized cartoonish 3D aesthetic. The texture features a slate grey base with varied shades, multiple visible craters of varying sizes with shadowed rims, and rocky dusty surface details. Include dark flat maria areas. Matte finish, high quality, designed to wrap seamlessly around a sphere without visible seams at the edges.", "description": "A seamless equirectangular moon texture for spherical mapping on a 3D character.", "subtype": "other", "aspect_ratio": [1536, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "mars_texture": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_105667223504694108789/1780063550229_01KST0WERNGCAB4R90S99K5KQZ.image.png", "type": "image", "prompt": null, "description": "Custom Mars planet texture for spherical UV mapping on 3D blob characters - user uploaded replacement", "subtype": null, "aspect_ratio": [318, 159], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "jupiter_texture": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_105667223504694108789/1780061141677_01KSSYJYND631TWK9V7128MTZ7.image.png", "type": "image", "prompt": null, "description": "Custom Jupiter gas giant texture for spherical UV mapping on 3D blob characters, featuring atmospheric bands and Great Red Spot.", "subtype": null, "aspect_ratio": [1024, 512], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "sun_texture": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_105667223504694108789/1780063613330_01KST0YCCJX16DSNGNF6RMYF1M.image.png", "type": "image", "prompt": null, "description": "Custom Sun texture replacement - a vibrant fiery solar surface with orange and yellow tones", "subtype": null, "aspect_ratio": [318, 159], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "galaxy_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/galaxy_texture_f444a024-6b5a-4662-adf9-7ba398fd505d.webp", "type": "image", "prompt": "Generate a seamless equirectangular 2:1 aspect ratio texture for spherical mapping. The image features a vibrant, mystical galaxy and supernova nebula in a stylized cartoonish 3D aesthetic. Deep purple and black space background filled with swirling nebula clouds in shades of purple, blue, and pink. Scattered bright stars of various sizes with glowing effects. Sparkly, cosmic, and magical appearance. The left and right edges must loop perfectly for seamless spherical wrapping.", "description": "A seamless equirectangular nebula texture for a 3D cosmic blob character.", "subtype": "other", "aspect_ratio": [1536, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "game_thumbnail": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_105667223504694108789/1780016523810_01KSRM1AH2VSQ12C24T2JJXV9B.image.png", "type": "image", "prompt": null, "description": "Game thumbnail showing \"Eat the Planets Simulator\" title with Earth and Sun cosmetic visuals", "subtype": null, "aspect_ratio": [683, 1024], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "custom_thumbnail_upload": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_105667223504694108789/1780016523810_01KSRM1AH2VSQ12C24T2JJXV9B.image.png", "type": "image", "prompt": null, "description": "Custom game thumbnail image for Eat the Planets Simulator showing the game title with Earth (Level 1) and Sun (Level 100) visuals on a space-themed background with colorful slimes", "subtype": null, "aspect_ratio": [683, 1024], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Eat Blobs Simulator</title>\n    <link href=\"https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&family=Inter:wght@400;600&display=swap\" rel=\"stylesheet\">\n    <style>\n        :root {\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --ease-out: cubic-bezier(0.0, 0, 0.2, 1);\n            --ease-bounce: cubic-bezier(0.68, -0.6, 0.32, 1.6);\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n        }\n\n        body, html {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            -ms-touch-action: none;\n            -webkit-touch-callout: none;\n            -webkit-user-select: none;\n            user-select: none;\n            font-family: 'Inter', sans-serif;\n            background: #0a0a1a;\n        }\n\n        * {\n            -webkit-tap-highlight-color: transparent;\n        }\n\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n        }\n\n        #three-canvas {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n        }\n\n        .ui-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: 10;\n        }\n\n        .ui-overlay > * {\n            pointer-events: auto;\n        }\n\n        /* Main Menu */\n        #main-menu {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            background: rgba(10, 10, 30, 0.85);\n            z-index: 100;\n            pointer-events: auto;\n        }\n\n        #main-menu.hidden {\n            display: none;\n        }\n\n        .game-title {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 52px;\n            font-weight: 700;\n            text-align: center;\n            margin-bottom: 40px;\n            text-shadow: 0 4px 20px rgba(0,0,0,0.5);\n        }\n\n        .title-eat { color: #ff8c00; }\n        .title-blobs { color: #00e5ff; }\n        .title-sim { color: #ffeb3b; }\n\n        .play-btn {\n            width: 280px;\n            height: 96px;\n            min-height: 96px;\n            border: none;\n            border-radius: 16px;\n            touch-action: manipulation;\n            -webkit-tap-highlight-color: transparent;\n            background: linear-gradient(180deg, #76ff03, #4caf50);\n            color: white;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 40px;\n            font-weight: 700;\n            cursor: pointer;\n            box-shadow: 0 6px 20px rgba(118, 255, 3, 0.4);\n            transition: transform var(--transition-fast) var(--ease-out);\n            animation: pulse-btn 2s infinite;\n        }\n\n        .play-btn:active {\n            transform: scale(0.95);\n        }\n\n        .play-btn:focus-visible,\n        .respawn-btn:focus-visible {\n            outline: 3px solid #fff;\n            outline-offset: 4px;\n        }\n\n        @keyframes pulse-btn {\n            0%, 100% { transform: scale(1); }\n            50% { transform: scale(1.05); }\n        }\n\n        /* Skin Selector */\n        .skin-selector {\n            margin-top: 40px;\n            text-align: center;\n        }\n\n        .skin-selector-title {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 24px;\n            color: #00e5ff;\n            margin-bottom: 16px;\n            text-shadow: 0 2px 10px rgba(0, 229, 255, 0.5);\n        }\n\n        .skin-carousel {\n            display: flex;\n            gap: 16px;\n            justify-content: center;\n            flex-wrap: wrap;\n            max-width: 500px;\n            padding: 10px;\n        }\n\n        .skin-option {\n            width: 80px;\n            height: 80px;\n            min-width: 80px;\n            min-height: 80px;\n            border-radius: 50%;\n            cursor: pointer;\n            position: relative;\n            transition: transform 0.2s ease, box-shadow 0.2s ease;\n            border: 3px solid rgba(255, 255, 255, 0.2);\n            overflow: hidden;\n            background: #1a1a3e;\n            touch-action: manipulation;\n            -webkit-tap-highlight-color: transparent;\n        }\n\n        .skin-option:hover {\n            transform: scale(1.1);\n            border-color: rgba(255, 255, 255, 0.5);\n        }\n\n        .skin-option.selected {\n            transform: scale(1.15);\n            border-color: #00e5ff;\n            box-shadow: 0 0 20px rgba(0, 229, 255, 0.6), 0 0 40px rgba(0, 229, 255, 0.3);\n        }\n\n        .skin-option canvas {\n            width: 100%;\n            height: 100%;\n            border-radius: 50%;\n        }\n\n        .skin-option .skin-name {\n            position: absolute;\n            bottom: -24px;\n            left: 50%;\n            transform: translateX(-50%);\n            font-family: 'Inter', sans-serif;\n            font-size: 11px;\n            color: #aaa;\n            white-space: nowrap;\n            opacity: 0;\n            transition: opacity 0.2s ease;\n        }\n\n        .skin-option:hover .skin-name,\n        .skin-option.selected .skin-name {\n            opacity: 1;\n            color: #fff;\n        }\n\n        .selected-skin-display {\n            margin-top: 30px;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 18px;\n            color: #ffeb3b;\n        }\n\n        /* HUD */\n        #hud {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            display: none;\n        }\n\n        #hud.visible {\n            display: block;\n        }\n\n        .hud-mass {\n            position: absolute;\n            bottom: 40px;\n            left: 50%;\n            transform: translateX(-50%);\n            background: rgba(0, 0, 0, 0.65);\n            padding: 14px 36px;\n            border-radius: 30px;\n            color: #fff;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 30px;\n            font-weight: 600;\n            border: 2px solid rgba(0, 229, 255, 0.5);\n            backdrop-filter: blur(4px);\n        }\n\n        .hud-skulls {\n            position: absolute;\n            top: 16px;\n            left: 16px;\n            background: rgba(0, 0, 0, 0.65);\n            padding: 10px 20px;\n            border-radius: 20px;\n            color: #ffeb3b;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 26px;\n            font-weight: 600;\n            backdrop-filter: blur(4px);\n            border: 1px solid rgba(255, 235, 59, 0.2);\n        }\n\n        .hud-leaderboard {\n            position: absolute;\n            top: 16px;\n            right: 16px;\n            background: rgba(0, 0, 0, 0.65);\n            padding: 14px 18px;\n            border-radius: 12px;\n            color: #fff;\n            font-family: 'Inter', sans-serif;\n            font-size: 20px;\n            min-width: 170px;\n            backdrop-filter: blur(4px);\n            border: 1px solid rgba(0, 229, 255, 0.2);\n        }\n\n        .hud-leaderboard h3 {\n            margin: 0 0 8px 0;\n            font-size: 22px;\n            color: #00e5ff;\n            font-family: 'Fredoka', sans-serif;\n        }\n\n        .lb-entry {\n            margin: 5px 0;\n            font-size: 18px;\n        }\n\n        /* Death Screen */\n        #death-screen {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: rgba(0,0,0,0.7);\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 50;\n            pointer-events: auto;\n        }\n\n        #death-screen.visible {\n            display: flex;\n        }\n\n        .death-text {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 48px;\n            color: #ff5252;\n            margin-bottom: 24px;\n            text-shadow: 0 2px 12px rgba(255, 82, 82, 0.5);\n        }\n\n        .death-info {\n            font-family: 'Inter', sans-serif;\n            font-size: 26px;\n            color: #fff;\n            margin-bottom: 48px;\n            opacity: 0.9;\n        }\n\n        .respawn-btn {\n            width: 280px;\n            height: 96px;\n            min-height: 96px;\n            border: none;\n            border-radius: 16px;\n            touch-action: manipulation;\n            -webkit-tap-highlight-color: transparent;\n            background: linear-gradient(180deg, #00e5ff, #0097a7);\n            color: white;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 32px;\n            font-weight: 600;\n            cursor: pointer;\n            box-shadow: 0 6px 20px rgba(0, 229, 255, 0.3);\n            transition: transform var(--transition-fast) var(--ease-out);\n        }\n\n        .respawn-btn:active {\n            transform: scale(0.95);\n        }\n\n        /* Joystick */\n        #joystick-area {\n            position: absolute;\n            bottom: 0;\n            left: 0;\n            width: 60%;\n            height: 45%;\n            pointer-events: auto;\n            touch-action: none;\n            -webkit-touch-callout: none;\n            -webkit-user-select: none;\n            user-select: none;\n        }\n\n        .joystick-base {\n            position: absolute;\n            width: 160px;\n            height: 160px;\n            border-radius: 50%;\n            background: rgba(255,255,255,0.15);\n            border: 4px solid rgba(255,255,255,0.4);\n            display: none;\n            touch-action: none;\n        }\n\n        .joystick-knob {\n            position: absolute;\n            width: 70px;\n            height: 70px;\n            border-radius: 50%;\n            background: rgba(255,255,255,0.6);\n            box-shadow: 0 0 15px rgba(0, 229, 255, 0.5);\n            display: none;\n            touch-action: none;\n        }\n\n        /* Edit mode info */\n        .edit-info {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            background: rgba(0,0,0,0.7);\n            padding: 20px 30px;\n            border-radius: 12px;\n            color: #fff;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 24px;\n            text-align: center;\n        }\n\n        /* Boot Loading Screen */\n        #boot-screen {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: #0a0a1a;\n            z-index: 9999;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            pointer-events: all;\n            transition: opacity 1s ease-out;\n        }\n\n        #boot-screen.fade-out {\n            opacity: 0;\n            pointer-events: none;\n        }\n\n        #boot-screen.hidden {\n            display: none;\n        }\n\n        .boot-background {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n        }\n\n        .boot-background canvas {\n            width: 100%;\n            height: 100%;\n            object-fit: cover;\n            filter: saturate(2.5) contrast(1.4) brightness(1.1);\n        }\n\n        .boot-content {\n            position: relative;\n            z-index: 2;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 30px;\n        }\n\n        .orbit-wheel {\n            width: 150px;\n            height: 150px;\n            position: relative;\n            animation: spin-wheel 3s linear infinite;\n        }\n\n        @keyframes spin-wheel {\n            from { transform: rotate(0deg); }\n            to { transform: rotate(360deg); }\n        }\n\n        .orbit-node {\n            position: absolute;\n            font-size: 26px;\n            transform-origin: center;\n            animation: counter-spin 1.5s linear infinite;\n            text-shadow: 0 0 12px rgba(0, 229, 255, 0.8);\n        }\n\n        @keyframes counter-spin {\n            from { transform: rotate(0deg); }\n            to { transform: rotate(-360deg); }\n        }\n\n        .boot-text {\n            font-family: 'Comic Sans MS', 'Comic Sans', cursive;\n            font-size: 24px;\n            color: #ffff00;\n            text-shadow: \n                3px 3px 0 #ff0000,\n                -2px -2px 0 #00ff00,\n                2px -2px 0 #0000ff,\n                -2px 2px 0 #ff00ff,\n                0 0 20px rgba(255, 255, 0, 0.8);\n            text-align: center;\n            letter-spacing: 2px;\n            animation: text-shake 0.1s infinite;\n            max-width: 90%;\n        }\n\n        @keyframes text-shake {\n            0%, 100% { transform: translate(0, 0) rotate(0deg); }\n            25% { transform: translate(1px, 1px) rotate(0.5deg); }\n            50% { transform: translate(-1px, -1px) rotate(-0.5deg); }\n            75% { transform: translate(1px, -1px) rotate(0.5deg); }\n        }\n\n        .boot-progress {\n            width: 280px;\n            height: 20px;\n            background: rgba(0, 0, 0, 0.5);\n            border: 3px solid #ff6600;\n            border-radius: 10px;\n            overflow: hidden;\n            box-shadow: 0 0 15px rgba(255, 102, 0, 0.5);\n        }\n\n        .boot-progress-bar {\n            height: 100%;\n            width: 0%;\n            background: linear-gradient(90deg, #ff0000, #ff6600, #ffff00, #00ff00);\n            background-size: 200% 100%;\n            animation: progress-rainbow 1s linear infinite;\n            transition: width 0.3s ease-out;\n        }\n\n        @keyframes progress-rainbow {\n            0% { background-position: 0% 50%; }\n            100% { background-position: 200% 50%; }\n        }\n\n        /* Disable main menu during boot */\n        #main-menu.boot-locked {\n            pointer-events: none !important;\n        }\n\n        #main-menu.boot-locked * {\n            pointer-events: none !important;\n        }\n\n        /* Minimap */\n        .hud-minimap {\n            position: absolute;\n            bottom: 120px;\n            right: 16px;\n            width: 140px;\n            height: 140px;\n            border-radius: 50%;\n            background: rgba(10, 15, 40, 0.7);\n            border: 3px solid #00e5ff;\n            box-shadow: 0 0 20px rgba(0, 229, 255, 0.5), \n                        0 0 40px rgba(0, 229, 255, 0.2),\n                        inset 0 0 30px rgba(0, 229, 255, 0.15);\n            overflow: hidden;\n        }\n\n        .minimap-canvas {\n            width: 100%;\n            height: 100%;\n            border-radius: 50%;\n        }\n\n        .minimap-label {\n            position: absolute;\n            top: -24px;\n            left: 50%;\n            transform: translateX(-50%);\n            font-family: 'Fredoka', sans-serif;\n            font-size: 12px;\n            color: #00e5ff;\n            text-transform: uppercase;\n            letter-spacing: 1px;\n            white-space: nowrap;\n        }\n\n        /* === COMBO / RANK / FEEDBACK HUD === */\n        .hud-combo {\n            position: absolute;\n            bottom: 112px;\n            left: 50%;\n            transform: translateX(-50%);\n            font-family: 'Fredoka', sans-serif;\n            font-size: 28px;\n            color: #ffeb3b;\n            text-shadow: 0 0 14px rgba(255, 235, 59, 0.9);\n            opacity: 0;\n            transition: opacity 180ms ease-out;\n            white-space: nowrap;\n        }\n\n        .hud-combo.active { opacity: 1; }\n\n        .hud-rank {\n            position: absolute;\n            top: 74px;\n            left: 16px;\n            background: rgba(0, 0, 0, 0.65);\n            padding: 8px 18px;\n            border-radius: 20px;\n            color: #00e5ff;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 22px;\n            border: 1px solid rgba(0, 229, 255, 0.25);\n            backdrop-filter: blur(4px);\n        }\n\n        .kill-feed {\n            position: absolute;\n            top: 150px;\n            left: 50%;\n            transform: translateX(-50%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 6px;\n            pointer-events: none;\n        }\n\n        .kill-feed-item {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 22px;\n            color: #fff;\n            background: rgba(0, 0, 0, 0.55);\n            padding: 6px 16px;\n            border-radius: 14px;\n            border: 1px solid rgba(255, 255, 255, 0.15);\n            animation: feed-in 300ms var(--ease-bounce);\n            white-space: nowrap;\n        }\n\n        @keyframes feed-in {\n            from { transform: translateY(-14px) scale(0.85); opacity: 0; }\n            to { transform: none; opacity: 1; }\n        }\n\n        .float-text {\n            position: absolute;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 30px;\n            font-weight: 700;\n            color: #fff;\n            text-shadow: 0 2px 8px rgba(0, 0, 0, 0.85);\n            pointer-events: none;\n            transform: translate(-50%, -50%);\n            animation: float-up 900ms ease-out forwards;\n            white-space: nowrap;\n        }\n\n        @keyframes float-up {\n            0% { opacity: 1; transform: translate(-50%, -50%) scale(0.8); }\n            25% { opacity: 1; transform: translate(-50%, -70%) scale(1.15); }\n            100% { opacity: 0; transform: translate(-50%, -150%) scale(1.3); }\n        }\n\n        /* Dash button */\n        .dash-btn {\n            position: absolute;\n            bottom: 300px;\n            right: 26px;\n            width: 120px;\n            height: 120px;\n            border-radius: 50%;\n            border: 4px solid rgba(255, 255, 255, 0.5);\n            background: radial-gradient(circle at 35% 30%, #9c6bff, #311b92);\n            color: #fff;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 22px;\n            font-weight: 700;\n            pointer-events: auto;\n            touch-action: manipulation;\n            -webkit-tap-highlight-color: transparent;\n            cursor: pointer;\n            box-shadow: 0 0 26px rgba(124, 77, 255, 0.6);\n            overflow: hidden;\n            transition: transform var(--transition-fast) var(--ease-out);\n        }\n\n        .dash-btn:active { transform: scale(0.94); }\n        .dash-btn.cooling { filter: grayscale(0.65) brightness(0.65); }\n\n        .dash-cool-fill {\n            position: absolute;\n            left: 0;\n            bottom: 0;\n            width: 100%;\n            height: 0%;\n            background: rgba(0, 0, 0, 0.45);\n            pointer-events: none;\n        }\n\n        .danger-vignette {\n            position: absolute;\n            top: 0; left: 0; right: 0; bottom: 0;\n            pointer-events: none;\n            opacity: 0;\n            background: radial-gradient(ellipse at center, transparent 45%, rgba(255, 0, 45, 0.6) 100%);\n            transition: opacity 180ms linear;\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"three-canvas\"></canvas>\n        \n        <!-- Boot Loading Screen Shield -->\n        <div id=\"boot-screen\">\n            <div class=\"boot-background\">\n                <canvas id=\"boot-canvas\"></canvas>\n            </div>\n            <div class=\"boot-content\">\n                <div class=\"orbit-wheel\" id=\"orbit-wheel\">\n                    <!-- Cosmic orbit nodes generated by JS -->\n                </div>\n                <div class=\"boot-text\" id=\"boot-text\">CALIBRATING PLANETARY WEIGHTS... DO NOT PANIC</div>\n                <div class=\"boot-progress\">\n                    <div class=\"boot-progress-bar\" id=\"boot-progress-bar\"></div>\n                </div>\n            </div>\n        </div>\n        \n        <div class=\"ui-overlay\">\n            <div id=\"main-menu\">\n                <div class=\"game-title\">\n                    <span class=\"title-eat\">Eat </span><span class=\"title-blobs\">the Planets </span><span class=\"title-sim\">Simulator</span>\n                </div>\n                <button class=\"play-btn\" id=\"play-btn\">PLAY</button>\n                \n                <div class=\"skin-selector\">\n                    <div class=\"skin-selector-title\">\ud83c\udf0d Choose Your Planet \ud83c\udf0d</div>\n                    <div class=\"skin-carousel\" id=\"skin-carousel\">\n                        <!-- Skin options will be generated by JavaScript -->\n                    </div>\n                    <div class=\"selected-skin-display\" id=\"selected-skin-display\">Selected: Earth</div>\n                </div>\n            </div>\n\n            <div id=\"hud\">\n                <div class=\"hud-skulls\" id=\"skulls-display\">\ud83d\udc80 0</div>\n                <div class=\"hud-leaderboard\" id=\"leaderboard\">\n                    <h3>Top Blobs</h3>\n                    <div id=\"lb-entries\"></div>\n                </div>\n                <div class=\"hud-mass\" id=\"mass-display\">MASS: 10</div>\n                <div class=\"hud-minimap\" id=\"minimap-container\">\n                    <span class=\"minimap-label\">RADAR</span>\n                    <canvas id=\"minimap-canvas\" width=\"140\" height=\"140\"></canvas>\n                </div>\n                <div class=\"hud-rank\" id=\"rank-display\">RANK #1</div>\n                <div class=\"hud-combo\" id=\"combo-display\"></div>\n                <div class=\"kill-feed\" id=\"kill-feed\"></div>\n                <button class=\"dash-btn\" id=\"dash-btn\">DASH<div class=\"dash-cool-fill\" id=\"dash-cool-fill\"></div></button>\n                <div class=\"danger-vignette\" id=\"danger-vignette\"></div>\n                <div id=\"float-layer\" style=\"position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;\"></div>\n            </div>\n\n            <div id=\"death-screen\">\n                <div class=\"death-text\">YOU WERE EATEN!</div>\n                <div class=\"death-info\" id=\"death-info\">You earned 5 Skulls</div>\n                <button class=\"respawn-btn\" id=\"respawn-btn\">RESPAWN</button>\n            </div>\n\n            <div id=\"joystick-area\">\n                <div class=\"joystick-base\" id=\"joystick-base\"></div>\n                <div class=\"joystick-knob\" id=\"joystick-knob\"></div>\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     * EAT BLOBS SIMULATOR\n     * A 3D incremental eating game where players grow their blob by consuming\n     * slimes scattered across an arena. Features dynamic camera zoom, AI blobs,\n     * and permanent skull currency.\n     */\n    async function run(mode) {\n        const config = window.gameConfig;\n        const gameWorld = document.getElementById('game-world');\n        const canvas = document.getElementById('three-canvas');\n        \n        // === BOOT LOADING SCREEN SYSTEM ===\n        const bootScreen = document.getElementById('boot-screen');\n        const bootCanvas = document.getElementById('boot-canvas');\n        const bootText = document.getElementById('boot-text');\n        const bootProgressBar = document.getElementById('boot-progress-bar');\n        const orbitWheel = document.getElementById('orbit-wheel');\n        const mainMenuEl = document.getElementById('main-menu');\n        \n        // Lock main menu during boot\n        if (mainMenuEl) mainMenuEl.classList.add('boot-locked');\n        \n        // Boot screen state\n        let bootComplete = false;\n        let assetsLoaded = false;\n        const BOOT_DURATION = 5000; // 5 seconds mandatory\n        const bootStartTime = performance.now();\n        \n        // Boot loading messages\n        const bootMessages = [\n            'CALIBRATING PLANETARY WEIGHTS... DO NOT PANIC',\n            'FEEDING THE AI BOTS...',\n            'WARMING UP THE VACUUM CLEANERS...',\n            'INFLATING THE PLANETS...',\n            'READY TO YEET!'\n        ];\n        let currentMessageIndex = 0;\n        \n        // Generate orbiting celestial nodes\n        function generateOrbitWheel() {\n            if (!orbitWheel) return;\n            orbitWheel.innerHTML = '';\n            const celestialIcons = ['\ud83c\udf0d', '\ud83e\ude90', '\ud83c\udf19', '\u2604\ufe0f', '\u2600\ufe0f', '\u2b50', '\ud83c\udf0c', '\u2728'];\n            const numNodes = celestialIcons.length;\n            for (let i = 0; i < numNodes; i++) {\n                const node = document.createElement('div');\n                node.className = 'orbit-node';\n                node.textContent = celestialIcons[i];\n                const angle = (i / numNodes) * Math.PI * 2;\n                const radius = 55;\n                const x = 75 + Math.cos(angle) * radius - 14;\n                const y = 75 + Math.sin(angle) * radius - 14;\n                node.style.left = x + 'px';\n                node.style.top = y + 'px';\n                orbitWheel.appendChild(node);\n            }\n        }\n        generateOrbitWheel();\n        \n        // Draw cosmic showdown: Earth and Jupiter facing off\n        function drawBootBackground() {\n            if (!bootCanvas) return;\n            const ctx = bootCanvas.getContext('2d');\n            bootCanvas.width = 720;\n            bootCanvas.height = 1280;\n            \n            // Deep space background with noise\n            const gradient = ctx.createLinearGradient(0, 0, 720, 1280);\n            gradient.addColorStop(0, '#1a0530');\n            gradient.addColorStop(0.5, '#2a1050');\n            gradient.addColorStop(1, '#0a0520');\n            ctx.fillStyle = gradient;\n            ctx.fillRect(0, 0, 720, 1280);\n            \n            // Add noise/grain for deep space nebula effect\n            for (let i = 0; i < 5000; i++) {\n                ctx.fillStyle = `rgba(${Math.random() * 255}, ${Math.random() * 100}, 0, ${Math.random() * 0.15})`;\n                ctx.fillRect(Math.random() * 720, Math.random() * 1280, 2, 2);\n            }\n            \n            // Draw Earth (left side)\n            const earthX = 180, earthY = 500, earthR = 140;\n            \n            // Earth glow\n            const earthGlow = ctx.createRadialGradient(earthX, earthY, earthR * 0.8, earthX, earthY, earthR * 1.5);\n            earthGlow.addColorStop(0, 'rgba(0, 150, 255, 0.4)');\n            earthGlow.addColorStop(1, 'transparent');\n            ctx.fillStyle = earthGlow;\n            ctx.beginPath();\n            ctx.arc(earthX, earthY, earthR * 1.5, 0, Math.PI * 2);\n            ctx.fill();\n            \n            // Earth body\n            const earthGrad = ctx.createRadialGradient(earthX - 40, earthY - 40, 0, earthX, earthY, earthR);\n            earthGrad.addColorStop(0, '#4488ff');\n            earthGrad.addColorStop(0.5, '#2266aa');\n            earthGrad.addColorStop(1, '#113366');\n            ctx.fillStyle = earthGrad;\n            ctx.beginPath();\n            ctx.arc(earthX, earthY, earthR, 0, Math.PI * 2);\n            ctx.fill();\n            \n            // Earth continents (simplified)\n            ctx.fillStyle = '#33aa55';\n            ctx.beginPath();\n            ctx.ellipse(earthX - 30, earthY - 20, 40, 30, 0.3, 0, Math.PI * 2);\n            ctx.fill();\n            ctx.beginPath();\n            ctx.ellipse(earthX + 40, earthY + 30, 25, 40, -0.2, 0, Math.PI * 2);\n            ctx.fill();\n            \n            // Earth original determined cosmic hero face (looking right towards Jupiter)\n            // Glowing cyan eye visors\n            ctx.fillStyle = '#00ffff';\n            ctx.shadowColor = '#00e5ff';\n            ctx.shadowBlur = 16;\n            ctx.beginPath();\n            ctx.ellipse(earthX - 25, earthY - 15, 20, 12, 0.15, 0, Math.PI * 2);\n            ctx.fill();\n            ctx.beginPath();\n            ctx.ellipse(earthX + 32, earthY - 15, 20, 12, -0.15, 0, Math.PI * 2);\n            ctx.fill();\n            // Sharp heroic pupils\n            ctx.fillStyle = '#ffffff';\n            ctx.shadowBlur = 0;\n            ctx.beginPath();\n            ctx.arc(earthX - 18, earthY - 15, 6, 0, Math.PI * 2);\n            ctx.fill();\n            ctx.beginPath();\n            ctx.arc(earthX + 39, earthY - 15, 6, 0, Math.PI * 2);\n            ctx.fill();\n            // Confident battle grin\n            ctx.strokeStyle = '#ffffff';\n            ctx.lineWidth = 5;\n            ctx.lineCap = 'round';\n            ctx.beginPath();\n            ctx.arc(earthX + 8, earthY + 35, 26, 0.2, Math.PI - 0.4);\n            ctx.stroke();\n            \n            // Draw Jupiter (right side)\n            const jupX = 540, jupY = 500, jupR = 160;\n            \n            // Jupiter glow\n            const jupGlow = ctx.createRadialGradient(jupX, jupY, jupR * 0.8, jupX, jupY, jupR * 1.5);\n            jupGlow.addColorStop(0, 'rgba(255, 150, 50, 0.4)');\n            jupGlow.addColorStop(1, 'transparent');\n            ctx.fillStyle = jupGlow;\n            ctx.beginPath();\n            ctx.arc(jupX, jupY, jupR * 1.5, 0, Math.PI * 2);\n            ctx.fill();\n            \n            // Jupiter body\n            const jupGrad = ctx.createRadialGradient(jupX + 40, jupY - 40, 0, jupX, jupY, jupR);\n            jupGrad.addColorStop(0, '#ffcc88');\n            jupGrad.addColorStop(0.5, '#ddaa66');\n            jupGrad.addColorStop(1, '#996633');\n            ctx.fillStyle = jupGrad;\n            ctx.beginPath();\n            ctx.arc(jupX, jupY, jupR, 0, Math.PI * 2);\n            ctx.fill();\n            \n            // Jupiter bands\n            ctx.save();\n            ctx.beginPath();\n            ctx.arc(jupX, jupY, jupR, 0, Math.PI * 2);\n            ctx.clip();\n            const bandColors = ['#cc9955', '#aa7744', '#ddbb77', '#bb8855', '#cc9966'];\n            for (let i = 0; i < 8; i++) {\n                ctx.fillStyle = bandColors[i % bandColors.length];\n                ctx.fillRect(jupX - jupR, jupY - jupR + i * 40, jupR * 2, 20);\n            }\n            // Great Red Spot\n            ctx.fillStyle = '#cc4422';\n            ctx.beginPath();\n            ctx.ellipse(jupX + 50, jupY + 40, 35, 20, 0, 0, Math.PI * 2);\n            ctx.fill();\n            ctx.restore();\n            \n            // Jupiter original cosmic titan face (looking left towards Earth)\n            // Glowing fiery amber titan eyes\n            ctx.fillStyle = '#ffaa00';\n            ctx.shadowColor = '#ff6600';\n            ctx.shadowBlur = 18;\n            ctx.beginPath();\n            ctx.ellipse(jupX - 35, earthY - 15, 22, 14, -0.15, 0, Math.PI * 2);\n            ctx.fill();\n            ctx.beginPath();\n            ctx.ellipse(jupX + 25, earthY - 15, 22, 14, 0.15, 0, Math.PI * 2);\n            ctx.fill();\n            // Intense titan pupils\n            ctx.fillStyle = '#fff4cc';\n            ctx.shadowBlur = 0;\n            ctx.beginPath();\n            ctx.arc(jupX - 42, earthY - 15, 7, 0, Math.PI * 2);\n            ctx.fill();\n            ctx.beginPath();\n            ctx.arc(jupX + 18, earthY - 15, 7, 0, Math.PI * 2);\n            ctx.fill();\n            // Mighty cosmic smile\n            ctx.strokeStyle = '#ffffff';\n            ctx.lineWidth = 6;\n            ctx.lineCap = 'round';\n            ctx.beginPath();\n            ctx.arc(jupX - 10, earthY + 38, 30, 0.3, Math.PI - 0.2);\n            ctx.stroke();\n            \n            // \"VS\" text between them with fire effect\n            ctx.font = 'bold 60px Impact, sans-serif';\n            ctx.fillStyle = '#ff6600';\n            ctx.strokeStyle = '#ffff00';\n            ctx.lineWidth = 3;\n            ctx.textAlign = 'center';\n            ctx.fillText('VS', 360, 520);\n            ctx.strokeText('VS', 360, 520);\n            \n            // Lightning bolts between them\n            ctx.strokeStyle = '#ffff00';\n            ctx.lineWidth = 4;\n            ctx.shadowColor = '#ffff00';\n            ctx.shadowBlur = 15;\n            for (let bolt = 0; bolt < 3; bolt++) {\n                ctx.beginPath();\n                let x = 300 + bolt * 30;\n                let y = 400 + bolt * 50;\n                ctx.moveTo(x, y);\n                for (let i = 0; i < 5; i++) {\n                    x += 15 + Math.random() * 10;\n                    y += (Math.random() - 0.5) * 30;\n                    ctx.lineTo(x, y);\n                }\n                ctx.stroke();\n            }\n            ctx.shadowBlur = 0;\n        }\n        drawBootBackground();\n        \n        // Update boot progress and messages\n        function updateBootProgress() {\n            const elapsed = performance.now() - bootStartTime;\n            const progress = Math.min(elapsed / BOOT_DURATION, 1);\n            \n            if (bootProgressBar) {\n                bootProgressBar.style.width = (progress * 100) + '%';\n            }\n            \n            // Update message based on progress\n            const messageIndex = Math.min(Math.floor(progress * bootMessages.length), bootMessages.length - 1);\n            if (messageIndex !== currentMessageIndex && bootText) {\n                currentMessageIndex = messageIndex;\n                bootText.textContent = bootMessages[messageIndex];\n            }\n            \n            return progress >= 1;\n        }\n        \n        // Complete boot sequence\n        function completeBootSequence() {\n            if (bootComplete) return;\n            bootComplete = true;\n            \n            // Start fade out\n            if (bootScreen) {\n                bootScreen.classList.add('fade-out');\n            }\n            \n            // Unlock main menu after fade completes\n            setTimeout(() => {\n                if (bootScreen) bootScreen.classList.add('hidden');\n                if (mainMenuEl) mainMenuEl.classList.remove('boot-locked');\n            }, 1000);\n        }\n        \n        // Boot update loop\n        function bootLoop() {\n            if (bootComplete) return;\n            \n            const timerComplete = updateBootProgress();\n            \n            if (timerComplete && assetsLoaded) {\n                completeBootSequence();\n            } else {\n                requestAnimationFrame(bootLoop);\n            }\n        }\n        \n        // Start boot loop (only in play mode)\n        if (mode === 'play' && bootScreen) {\n            requestAnimationFrame(bootLoop);\n        } else if (bootScreen) {\n            // In edit mode, skip boot screen\n            bootScreen.classList.add('hidden');\n            if (mainMenuEl) mainMenuEl.classList.remove('boot-locked');\n        }\n\n        // Audio context\n        let audioCtx = null;\n        const audioBuffers = {};\n        let musicSource = null;\n        let musicGain = null;\n        let sfxGain = null;\n\n        function initAudio() {\n            if (audioCtx) return;\n            audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n            musicGain = audioCtx.createGain();\n            musicGain.gain.value = (config.audio?.musicVolume ?? 50) / 100;\n            musicGain.connect(audioCtx.destination);\n            sfxGain = audioCtx.createGain();\n            sfxGain.gain.value = (config.audio?.sfxVolume ?? 80) / 100;\n            sfxGain.connect(audioCtx.destination);\n        }\n\n        async function loadAudio(id) {\n            try {\n                const asset = lib.getAsset(id);\n                if (!asset || asset.type !== 'audio') return;\n                const response = await fetch(asset.url);\n                const arrayBuffer = await response.arrayBuffer();\n                if (!audioCtx) initAudio();\n                audioBuffers[id] = await audioCtx.decodeAudioData(arrayBuffer);\n            } catch (e) {\n                lib.log('Audio load error: ' + id + ' ' + e.message);\n            }\n        }\n\n        function playSound(id, loop = false) {\n            if (!audioCtx || !audioBuffers[id]) return null;\n            if (audioCtx.state === 'suspended') audioCtx.resume();\n            const source = audioCtx.createBufferSource();\n            source.buffer = audioBuffers[id];\n            source.loop = loop;\n            source.connect(loop ? musicGain : sfxGain);\n            source.start(0);\n            return source;\n        }\n\n        // Load images\n        const imageCache = {};\n        function loadImage(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                img.onload = () => { imageCache[id] = img; resolve(); };\n                img.onerror = () => resolve();\n                img.src = asset.url;\n            });\n        }\n\n        // Preload assets\n        await Promise.all([\n            loadAudio('background_music_upbeat'),\n            loadAudio('heavy_splat_sfx'),\n            loadImage('grid_floor_texture'),\n            loadImage('blob_texture_glossy'),\n            loadImage('slime_texture_glow'),\n            // Cosmic skin textures\n            loadImage('earth_texture'),\n            loadImage('moon_texture'),\n            loadImage('mars_texture'),\n            loadImage('jupiter_texture'),\n            loadImage('sun_texture'),\n            loadImage('galaxy_texture')\n        ]);\n        \n        // Mark assets as loaded for boot sequence\n        assetsLoaded = true;\n\n        // Show game parameters\n        lib.showGameParameters({\n            name: 'Eat Blobs Simulator',\n            params: {\n                'Arena Size': { key: 'arena.size', type: 'slider', min: 100, max: 500, step: 50 },\n                'Slime Count': { key: 'slimes.count', type: 'slider', min: 100, max: 2000, step: 100 },\n                'Slime Size': { key: 'slimes.sizeMultiplier', type: 'slider', min: 0.5, max: 2, step: 0.1 },\n                'Starting Mass': { key: 'player.startingMass', type: 'slider', min: 5, max: 50, step: 5 },\n                'Growth Rate': { key: 'player.growthRate', type: 'slider', min: 0.5, max: 3, step: 0.25 },\n                'Move Speed': { key: 'player.speedMultiplier', type: 'slider', min: 0.5, max: 2, step: 0.1 },\n                'Blob Color': { key: 'player.color', type: 'color' },\n                'Bot Count': { key: 'bots.count', type: 'slider', min: 5, max: 20, step: 1 },\n                'Camera Angle': { key: 'camera.angle', type: 'slider', min: 30, max: 75, step: 5 },\n                'Music Volume': { key: 'audio.musicVolume', type: 'slider', min: 0, max: 100, step: 5 },\n                'SFX Volume': { key: 'audio.sfxVolume', type: 'slider', min: 0, max: 100, step: 5 }\n            }\n        });\n\n        if (mode === 'edit') {\n            // Show edit mode info\n            const editDiv = document.createElement('div');\n            editDiv.className = 'edit-info';\n            editDiv.innerHTML = 'Edit Mode<br><span style=\"font-size:18px;color:#aaa;\">Use the settings panel to adjust game parameters</span>';\n            document.querySelector('.ui-overlay').appendChild(editDiv);\n            document.getElementById('main-menu').classList.add('hidden');\n\n            // Render a preview scene\n            setupThreeScene();\n            createArenaFloor();\n            spawnSlimes();\n            \n            function editLoop() {\n                renderer.render(scene, camera);\n                requestAnimationFrame(editLoop);\n            }\n            editLoop();\n            return;\n        }\n\n        // === PLAY MODE ===\n        let gameState = 'menu'; // menu, playing, dying, dead\n        let playerMass = config.player.startingMass;\n        let playerSkulls = 0;\n        let playerPos = { x: 0, z: 0 };\n        let playerVel = { x: 0, z: 0 };\n        let slimes = [];\n        let aiBots = [];\n        let inputDir = { x: 0, z: 0 };\n        let keys = {};\n\n        // === SESSION STATS / COMBO / DASH / GAME FEEL ===\n        let sessionKills = 0;\n        let sessionPeakMass = 0;\n        let sessionStartTime = performance.now();\n        let comboCount = 0;\n        let comboTimer = 0;\n        const COMBO_WINDOW = 2.2;\n        let shakeIntensity = 0;\n        let dashTimer = 0;\n        let dashCooldown = 0;\n        const DASH_DURATION = 0.35;\n        const DASH_COOLDOWN = 3.0;\n        const DASH_SPEED_MULT = 2.6;\n        const DASH_MASS_COST = 0.03;\n        \n        // Physics-based rolling system\n        let playerVelocity = { x: 0, z: 0 };\n        let playerRotation = { x: 0, z: 0 }; // Cumulative rotation angles\n        const FRICTION = 4.0; // Deceleration rate\n        const ACCELERATION = 12.0; // How fast player accelerates\n        \n        // Spawn invincibility system\n        let spawnInvincible = false;\n        let spawnInvincibilityTimer = 0;\n        const SPAWN_INVINCIBILITY_DURATION = 4; // 4 seconds\n        let shieldMesh = null;\n        \n        // Movement direction arrow indicator\n        let directionArrow = null;\n        let arrowVisible = false;\n        \n        // Death animation system\n        let deathAnimation = {\n            active: false,\n            killerBot: null,\n            killerName: '',\n            progress: 0,\n            duration: 1.5, // 1.5 seconds to be fully devoured\n            startPos: { x: 0, z: 0 },\n            startRadius: 1,\n            earnedSkulls: 0\n        };\n        \n        // Player skin selection\n        let selectedPlayerSkin = 'earth';\n        const PLAYER_SKINS = [\n            { id: 'earth', name: 'Earth', emoji: '\ud83c\udf0d' },\n            { id: 'moon', name: 'Moon', emoji: '\ud83c\udf19' },\n            { id: 'mars', name: 'Mars', emoji: '\ud83d\udd34' },\n            { id: 'jupiter', name: 'Jupiter', emoji: '\ud83e\ude90' },\n            { id: 'sun', name: 'Sun', emoji: '\u2600\ufe0f' },\n            { id: 'galaxy', name: 'Galaxy', emoji: '\ud83c\udf0c' },\n            { id: 'blackhole', name: 'Black Hole', emoji: '\u26ab' }\n        ];\n\n        // Load player skulls\n        try {\n            const saved = await lib.getUserGameState();\n            if (saved && saved.state) {\n                playerSkulls = saved.state.skulls || 0;\n            }\n        } catch(e) {}\n\n        // Three.js setup\n        let scene, camera, renderer, playerMesh, arenaFloor;\n        let slimeMeshes = [];\n        let botMeshes = [];\n\n        function setupThreeScene() {\n            scene = new THREE.Scene();\n            // No solid background - we'll use galaxy skybox\n            scene.fog = new THREE.Fog(0x0a0a2e, 50, 250);\n\n            camera = new THREE.PerspectiveCamera(60, gameWorld.clientWidth / gameWorld.clientHeight, 0.1, 500);\n            \n            renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });\n            renderer.setSize(gameWorld.clientWidth, gameWorld.clientHeight);\n            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n\n            // Lighting\n            const ambient = new THREE.AmbientLight(0x404060, 0.6);\n            scene.add(ambient);\n            const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);\n            dirLight.position.set(20, 40, 20);\n            scene.add(dirLight);\n            const pointLight = new THREE.PointLight(0x00e5ff, 0.5, 100);\n            pointLight.position.set(0, 20, 0);\n            scene.add(pointLight);\n        }\n\n        setupThreeScene();\n\n        // === EATING PHYSICS & VFX SYSTEM ===\n        \n        // Particle system for splatter effects\n        const particles = [];\n        const MAX_PARTICLES = 300;\n        const particleGeo = new THREE.SphereGeometry(0.15, 8, 8);\n        \n        function createSplatterParticles(x, y, z, color, count = 8) {\n            for (let i = 0; i < count; i++) {\n                if (particles.length >= MAX_PARTICLES) {\n                    // Reuse oldest particle\n                    const p = particles.shift();\n                    scene.remove(p.mesh);\n                }\n                \n                const mat = new THREE.MeshPhongMaterial({\n                    color: color,\n                    emissive: new THREE.Color(color).multiplyScalar(0.3),\n                    transparent: true,\n                    opacity: 1\n                });\n                const mesh = new THREE.Mesh(particleGeo, mat);\n                mesh.position.set(x, y, z);\n                \n                // Random outward velocity\n                const angle = Math.random() * Math.PI * 2;\n                const upAngle = Math.random() * Math.PI * 0.4;\n                const speed = 3 + Math.random() * 5;\n                \n                const particle = {\n                    mesh,\n                    vx: Math.cos(angle) * Math.cos(upAngle) * speed,\n                    vy: Math.sin(upAngle) * speed + 2,\n                    vz: Math.sin(angle) * Math.cos(upAngle) * speed,\n                    life: 1.0,\n                    decay: 1.5 + Math.random() * 0.5,\n                    scale: 0.8 + Math.random() * 0.4\n                };\n                \n                mesh.scale.set(particle.scale, particle.scale, particle.scale);\n                scene.add(mesh);\n                particles.push(particle);\n            }\n        }\n        \n        // Epic kill burst effect for enemy elimination\n        function createKillBurstEffect(x, y, z, color, victimRadius) {\n            const burstCount = Math.min(40, 20 + Math.floor(victimRadius * 3));\n            \n            // Main radial burst - particles shoot outward in all directions\n            for (let i = 0; i < burstCount; i++) {\n                if (particles.length >= MAX_PARTICLES) {\n                    const p = particles.shift();\n                    scene.remove(p.mesh);\n                }\n                \n                // Evenly distributed radial pattern\n                const angleH = (i / burstCount) * Math.PI * 2;\n                const angleV = (Math.random() * 0.6 - 0.1) * Math.PI; // -0.1 to 0.5 (mostly outward and up)\n                \n                // Varied particle sizes for visual interest\n                const sizeVariation = 0.5 + Math.random() * 1.5;\n                const mat = new THREE.MeshPhongMaterial({\n                    color: color,\n                    emissive: new THREE.Color(color).multiplyScalar(0.5),\n                    transparent: true,\n                    opacity: 1\n                });\n                const mesh = new THREE.Mesh(particleGeo, mat);\n                mesh.position.set(x, y, z);\n                \n                // High speed burst\n                const speed = 8 + Math.random() * 10;\n                const particle = {\n                    mesh,\n                    vx: Math.cos(angleH) * Math.cos(angleV) * speed,\n                    vy: Math.sin(angleV) * speed + 3,\n                    vz: Math.sin(angleH) * Math.cos(angleV) * speed,\n                    life: 1.2,\n                    decay: 1.2 + Math.random() * 0.4,\n                    scale: sizeVariation\n                };\n                \n                mesh.scale.set(particle.scale, particle.scale, particle.scale);\n                scene.add(mesh);\n                particles.push(particle);\n            }\n            \n            // Add expanding shockwave ring particles\n            const ringCount = 16;\n            for (let i = 0; i < ringCount; i++) {\n                if (particles.length >= MAX_PARTICLES) break;\n                \n                const angle = (i / ringCount) * Math.PI * 2;\n                const mat = new THREE.MeshPhongMaterial({\n                    color: 0xffffff,\n                    emissive: new THREE.Color(color).multiplyScalar(0.8),\n                    transparent: true,\n                    opacity: 1\n                });\n                const mesh = new THREE.Mesh(particleGeo, mat);\n                mesh.position.set(x, y * 0.5, z);\n                \n                // Horizontal ring expansion\n                const ringSpeed = 12 + Math.random() * 4;\n                const particle = {\n                    mesh,\n                    vx: Math.cos(angle) * ringSpeed,\n                    vy: 0.5,\n                    vz: Math.sin(angle) * ringSpeed,\n                    life: 0.8,\n                    decay: 2.5,\n                    scale: 1.2\n                };\n                \n                mesh.scale.set(particle.scale, particle.scale, particle.scale);\n                scene.add(mesh);\n                particles.push(particle);\n            }\n            \n            // Add some slower \"debris\" particles for trailing effect\n            const debrisCount = Math.floor(burstCount * 0.3);\n            for (let i = 0; i < debrisCount; i++) {\n                if (particles.length >= MAX_PARTICLES) break;\n                \n                const angle = Math.random() * Math.PI * 2;\n                const upAngle = Math.random() * Math.PI * 0.5;\n                const mat = new THREE.MeshPhongMaterial({\n                    color: color,\n                    emissive: new THREE.Color(color).multiplyScalar(0.2),\n                    transparent: true,\n                    opacity: 0.9\n                });\n                const mesh = new THREE.Mesh(particleGeo, mat);\n                mesh.position.set(x, y, z);\n                \n                const speed = 2 + Math.random() * 4;\n                const particle = {\n                    mesh,\n                    vx: Math.cos(angle) * Math.cos(upAngle) * speed,\n                    vy: Math.sin(upAngle) * speed + 1,\n                    vz: Math.sin(angle) * Math.cos(upAngle) * speed,\n                    life: 1.5,\n                    decay: 1.0 + Math.random() * 0.3,\n                    scale: 0.6 + Math.random() * 0.6\n                };\n                \n                mesh.scale.set(particle.scale, particle.scale, particle.scale);\n                scene.add(mesh);\n                particles.push(particle);\n            }\n        }\n        \n        function updateParticles(dt) {\n            for (let i = particles.length - 1; i >= 0; i--) {\n                const p = particles[i];\n                p.life -= p.decay * dt;\n                \n                if (p.life <= 0) {\n                    scene.remove(p.mesh);\n                    particles.splice(i, 1);\n                    continue;\n                }\n                \n                // Physics\n                p.vy -= 15 * dt; // Gravity\n                p.mesh.position.x += p.vx * dt;\n                p.mesh.position.y += p.vy * dt;\n                p.mesh.position.z += p.vz * dt;\n                \n                // Bounce off ground\n                if (p.mesh.position.y < 0.1) {\n                    p.mesh.position.y = 0.1;\n                    p.vy *= -0.3;\n                    p.vx *= 0.7;\n                    p.vz *= 0.7;\n                }\n                \n                // Fade and shrink\n                p.mesh.material.opacity = p.life;\n                const s = p.scale * p.life;\n                p.mesh.scale.set(s, s, s);\n            }\n        }\n        \n        // Jiggle/wobble system for player blob\n        let playerJiggle = { scaleX: 1, scaleY: 1, scaleZ: 1, velX: 0, velY: 0, velZ: 0 };\n        const JIGGLE_STIFFNESS = 80;\n        const JIGGLE_DAMPING = 5;\n        \n        function triggerJiggle(intensity = 0.15) {\n            // Random directional jiggle\n            playerJiggle.velX += (Math.random() - 0.5) * intensity * 20;\n            playerJiggle.velY += intensity * 15;\n            playerJiggle.velZ += (Math.random() - 0.5) * intensity * 20;\n        }\n        \n        function updateJiggle(dt) {\n            // Spring physics for each axis\n            const dx = 1 - playerJiggle.scaleX;\n            const dy = 1 - playerJiggle.scaleY;\n            const dz = 1 - playerJiggle.scaleZ;\n            \n            playerJiggle.velX += dx * JIGGLE_STIFFNESS * dt;\n            playerJiggle.velY += dy * JIGGLE_STIFFNESS * dt;\n            playerJiggle.velZ += dz * JIGGLE_STIFFNESS * dt;\n            \n            playerJiggle.velX *= Math.exp(-JIGGLE_DAMPING * dt);\n            playerJiggle.velY *= Math.exp(-JIGGLE_DAMPING * dt);\n            playerJiggle.velZ *= Math.exp(-JIGGLE_DAMPING * dt);\n            \n            playerJiggle.scaleX += playerJiggle.velX * dt;\n            playerJiggle.scaleY += playerJiggle.velY * dt;\n            playerJiggle.scaleZ += playerJiggle.velZ * dt;\n            \n            // Volume preservation (approximate)\n            const avgScale = (playerJiggle.scaleX + playerJiggle.scaleY + playerJiggle.scaleZ) / 3;\n            if (avgScale > 0.5) {\n                playerJiggle.scaleX /= avgScale;\n                playerJiggle.scaleY /= avgScale;\n                playerJiggle.scaleZ /= avgScale;\n            }\n        }\n        \n        // Magnetic attraction state for slimes being sucked in\n        const attractedSlimes = new Map(); // slimeIndex -> { progress, startPos, startScale }\n        const ATTRACTION_RADIUS = 2.5; // Distance at which attraction starts\n        const ATTRACTION_SPEED = 4; // How fast slimes get sucked in\n        \n        // Bot deformation when being eaten\n        const botDeformations = new Map(); // botIndex -> { progress, startPos }\n\n        // === X-RAY TRANSPARENCY & OCCLUSION SYSTEM ===\n        const XRAY_FADE_IN_SPEED = 5.0;    // Fade to ghost in 0.2s (1/0.2 = 5)\n        const XRAY_FADE_OUT_SPEED = 3.33;  // Fade back solid in 0.3s (1/0.3 = 3.33)\n        const GHOST_OPACITY = 0.28;        // 25-30% opacity in ghost mode\n        const CORE_SCALE = 0.12;           // Size of glowing core relative to planet\n        const OCCLUSION_ANGLE_THRESHOLD = 0.15; // Radians - how close to center triggers occlusion\n        const OCCLUSION_SCREEN_COVERAGE = 0.25; // 25% screen coverage triggers transparency\n        \n        // Transparency state tracking\n        const xrayState = {\n            player: { \n                current: 1.0, \n                target: 1.0, \n                core: null,\n                originalEmissive: null \n            },\n            bots: new Map() // botIndex -> { current, target, core, originalEmissive }\n        };\n        \n        // Create glowing core mesh for transparent planets (The Cursed Core!)\n        function createXRayCore(bodyType) {\n            // Determine core color based on planet type\n            let coreColor;\n            switch(bodyType) {\n                case 'earth': coreColor = 0x00ffff; break;   // Cyan\n                case 'moon': coreColor = 0xaabbff; break;    // Pale blue\n                case 'mars': coreColor = 0xff4400; break;    // Orange-red\n                case 'jupiter': coreColor = 0xffaa00; break; // Golden\n                case 'sun': coreColor = 0xffff00; break;     // Bright yellow\n                case 'galaxy': coreColor = 0xff00ff; break;  // Magenta\n                case 'blackhole': coreColor = 0x8800ff; break; // Purple\n                default: coreColor = 0x00ffff;\n            }\n            \n            // Inner bright core\n            const coreGeo = new THREE.SphereGeometry(1, 16, 16);\n            const coreMat = new THREE.MeshBasicMaterial({\n                color: coreColor,\n                transparent: true,\n                opacity: 0\n            });\n            const core = new THREE.Mesh(coreGeo, coreMat);\n            \n            // Outer glow shell\n            const glowGeo = new THREE.SphereGeometry(1.8, 16, 16);\n            const glowMat = new THREE.MeshBasicMaterial({\n                color: coreColor,\n                transparent: true,\n                opacity: 0,\n                side: THREE.BackSide\n            });\n            const glow = new THREE.Mesh(glowGeo, glowMat);\n            core.add(glow);\n            \n            // Pulsing ring around core\n            const ringGeo = new THREE.TorusGeometry(1.2, 0.15, 8, 24);\n            const ringMat = new THREE.MeshBasicMaterial({\n                color: coreColor,\n                transparent: true,\n                opacity: 0\n            });\n            const ring = new THREE.Mesh(ringGeo, ringMat);\n            ring.rotation.x = Math.PI / 2;\n            core.add(ring);\n            \n            core.userData.glowMesh = glow;\n            core.userData.ringMesh = ring;\n            core.userData.coreColor = coreColor;\n            \n            return core;\n        }\n        \n        // Check line-of-sight occlusion between camera and targets\n        function checkXRayOcclusion(camPos, playerPos, playerRadius, bots, timestamp) {\n            const results = {\n                playerShouldGhost: false,\n                ghostBots: new Set()\n            };\n            \n            // Camera forward direction (toward player generally)\n            const camToPlayerX = playerPos.x - camPos.x;\n            const camToPlayerY = playerRadius - camPos.y;\n            const camToPlayerZ = playerPos.z - camPos.z;\n            const camToPlayerDist = Math.sqrt(camToPlayerX**2 + camToPlayerY**2 + camToPlayerZ**2);\n            \n            // Normalize camera-to-player direction\n            const camDirX = camToPlayerX / camToPlayerDist;\n            const camDirY = camToPlayerY / camToPlayerDist;\n            const camDirZ = camToPlayerZ / camToPlayerDist;\n            \n            // === CONTEXT A: Self-Obstruction Check ===\n            // Does player's own planet block view of enemies they're chasing?\n            // Check if any bot is directly behind player from camera's perspective\n            for (let i = 0; i < bots.length; i++) {\n                const bot = bots[i];\n                const botRadius = getRadius(bot.mass);\n                \n                // Direction from camera to bot\n                const camToBotX = bot.x - camPos.x;\n                const camToBotY = botRadius - camPos.y;\n                const camToBotZ = bot.z - camPos.z;\n                const camToBotDist = Math.sqrt(camToBotX**2 + camToBotY**2 + camToBotZ**2);\n                \n                // Is bot beyond player from camera's view?\n                if (camToBotDist > camToPlayerDist) {\n                    // Check if bot is roughly in same direction as player (occluded by player)\n                    const dotProduct = (camDirX * camToBotX + camDirY * camToBotY + camDirZ * camToBotZ) / camToBotDist;\n                    const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct)));\n                    \n                    // Calculate player's angular size from camera\n                    const playerAngularSize = Math.atan2(playerRadius, camToPlayerDist);\n                    \n                    // If bot is within player's visual cone AND player is large enough\n                    if (angle < playerAngularSize * 1.3 && playerRadius > 4) {\n                        results.playerShouldGhost = true;\n                        break;\n                    }\n                }\n            }\n            \n            // === CONTEXT B: Enemy Obstruction Check ===\n            // Does a massive enemy bot block the camera's view?\n            for (let i = 0; i < bots.length; i++) {\n                const bot = bots[i];\n                const botRadius = getRadius(bot.mass);\n                \n                // Direction from camera to bot\n                const camToBotX = bot.x - camPos.x;\n                const camToBotY = botRadius - camPos.y;\n                const camToBotZ = bot.z - camPos.z;\n                const camToBotDist = Math.sqrt(camToBotX**2 + camToBotY**2 + camToBotZ**2);\n                \n                // Is bot between camera and player?\n                if (camToBotDist < camToPlayerDist * 0.95) {\n                    // Check if bot is in camera's main view direction\n                    const dotProduct = (camDirX * camToBotX + camDirY * camToBotY + camDirZ * camToBotZ) / camToBotDist;\n                    \n                    // Calculate bot's angular size (how much of FOV it covers)\n                    const botAngularSize = Math.atan2(botRadius, camToBotDist);\n                    const screenCoverage = botAngularSize / (Math.PI / 4); // Relative to ~90\u00b0 FOV\n                    \n                    // If bot covers significant portion of screen OR is very close\n                    if (screenCoverage > OCCLUSION_SCREEN_COVERAGE || \n                        (camToBotDist < botRadius * 4 && botRadius > 3 && dotProduct > 0.7)) {\n                        results.ghostBots.add(i);\n                    }\n                }\n                \n                // Also check if bot is super close to camera regardless of direction\n                if (camToBotDist < botRadius * 2.5 && botRadius > 5) {\n                    results.ghostBots.add(i);\n                }\n            }\n            \n            return results;\n        }\n        \n        // Update transparency for a planet mesh with smooth lerping\n        function updateXRayTransparency(mesh, state, isGhosting, dt, bodyType, timestamp) {\n            // Set target opacity\n            state.target = isGhosting ? GHOST_OPACITY : 1.0;\n            \n            // Determine lerp speed based on direction\n            const speed = state.target < state.current ? XRAY_FADE_IN_SPEED : XRAY_FADE_OUT_SPEED;\n            \n            // Smooth lerp toward target\n            const diff = state.target - state.current;\n            state.current += diff * speed * dt;\n            \n            // Snap if very close\n            if (Math.abs(state.current - state.target) < 0.01) {\n                state.current = state.target;\n            }\n            \n            // Determine ghost level (0 = solid, 1 = full ghost)\n            const ghostLevel = 1 - (state.current - GHOST_OPACITY) / (1 - GHOST_OPACITY);\n            const isInGhostMode = state.current < 0.9;\n            \n            // Apply transparency to main mesh material\n            if (mesh.material) {\n                mesh.material.transparent = true;\n                mesh.material.opacity = state.current;\n                mesh.material.needsUpdate = true;\n                \n                // Glassy neon shell effect - increase emissive when ghosting\n                if (isInGhostMode) {\n                    // Store original emissive if not saved\n                    if (state.originalEmissive === null) {\n                        state.originalEmissive = mesh.material.emissiveIntensity || 0;\n                    }\n                    // Boost emissive for ghostly glow\n                    mesh.material.emissiveIntensity = state.originalEmissive + ghostLevel * 0.6;\n                } else if (state.originalEmissive !== null) {\n                    // Restore original emissive\n                    mesh.material.emissiveIntensity = state.originalEmissive;\n                }\n            }\n            \n            // Apply to children (atmosphere, rings, bands) - but not the core\n            mesh.children.forEach(child => {\n                if (child !== state.core && child.material) {\n                    child.material.transparent = true;\n                    // Children fade faster than main body\n                    child.material.opacity = Math.min(\n                        child.material.userData?.originalOpacity || child.material.opacity,\n                        state.current * 0.7\n                    );\n                    \n                    // Store original opacity for restoration\n                    if (!child.material.userData) child.material.userData = {};\n                    if (child.material.userData.originalOpacity === undefined) {\n                        child.material.userData.originalOpacity = child.material.opacity;\n                    }\n                }\n            });\n            \n            // === THE CURSED CORE VISUAL ===\n            // Create core when entering ghost mode\n            if (isInGhostMode && !state.core) {\n                state.core = createXRayCore(bodyType);\n                mesh.add(state.core);\n            }\n            \n            // Update core visibility and animation\n            if (state.core) {\n                // Core opacity inversely proportional to main opacity\n                const coreOpacity = Math.max(0, ghostLevel * 1.2);\n                \n                // Main core\n                state.core.material.opacity = Math.min(1, coreOpacity);\n                \n                // Glow shell\n                if (state.core.userData.glowMesh) {\n                    state.core.userData.glowMesh.material.opacity = coreOpacity * 0.4;\n                }\n                \n                // Pulsing ring\n                if (state.core.userData.ringMesh) {\n                    state.core.userData.ringMesh.material.opacity = coreOpacity * 0.6;\n                    state.core.userData.ringMesh.rotation.z = timestamp * 0.003;\n                }\n                \n                // Pulsing scale effect\n                const pulse = 1 + Math.sin(timestamp * 0.008) * 0.2;\n                const coreSize = CORE_SCALE * pulse;\n                state.core.scale.set(coreSize, coreSize, coreSize);\n                \n                // Remove core when fully solid\n                if (state.current >= 0.99) {\n                    mesh.remove(state.core);\n                    state.core = null;\n                }\n            }\n        }\n        \n        // Main X-Ray system update function\n        function updateXRaySystem(dt, timestamp) {\n            if (!playerMesh || gameState !== 'playing') return;\n            \n            const camPos = {\n                x: cameraState.currentX,\n                y: cameraState.currentY,\n                z: cameraState.currentZ\n            };\n            const pRadius = getRadius(playerMass);\n            \n            // Check occlusion\n            const occlusion = checkXRayOcclusion(camPos, playerPos, pRadius, aiBots, timestamp);\n            \n            // Update player transparency\n            updateXRayTransparency(\n                playerMesh, \n                xrayState.player, \n                occlusion.playerShouldGhost, \n                dt, \n                selectedPlayerSkin,\n                timestamp\n            );\n            \n            // Update bot transparencies\n            for (let i = 0; i < aiBots.length; i++) {\n                const bot = aiBots[i];\n                \n                // Initialize state if needed\n                if (!xrayState.bots.has(i)) {\n                    xrayState.bots.set(i, {\n                        current: 1.0,\n                        target: 1.0,\n                        core: null,\n                        originalEmissive: null\n                    });\n                }\n                \n                const botState = xrayState.bots.get(i);\n                const shouldGhost = occlusion.ghostBots.has(i);\n                \n                updateXRayTransparency(\n                    bot.mesh,\n                    botState,\n                    shouldGhost,\n                    dt,\n                    bot.skinType,\n                    timestamp\n                );\n            }\n        }\n        \n        // Reset X-Ray state for a bot (called when bot respawns)\n        function resetBotXRayState(botIndex) {\n            if (xrayState.bots.has(botIndex)) {\n                const state = xrayState.bots.get(botIndex);\n                if (state.core) {\n                    // Core will be on old mesh, don't need to remove\n                    state.core = null;\n                }\n                state.current = 1.0;\n                state.target = 1.0;\n                state.originalEmissive = null;\n            }\n        }\n\n        function createArenaFloor() {\n            const arenaSize = config.arena.size;\n            const geo = new THREE.PlaneGeometry(arenaSize, arenaSize, 1, 1);\n            \n            let mat;\n            if (imageCache['grid_floor_texture']) {\n                const tex = new THREE.Texture(imageCache['grid_floor_texture']);\n                tex.needsUpdate = true;\n                tex.wrapS = THREE.RepeatWrapping;\n                tex.wrapT = THREE.RepeatWrapping;\n                tex.repeat.set(arenaSize / 20, arenaSize / 20);\n                mat = new THREE.MeshStandardMaterial({ map: tex, roughness: 0.8 });\n            } else {\n                mat = new THREE.MeshStandardMaterial({ color: 0x1a1a3e, roughness: 0.9 });\n            }\n            \n            arenaFloor = new THREE.Mesh(geo, mat);\n            arenaFloor.rotation.x = -Math.PI / 2;\n            arenaFloor.position.y = -0.5;\n            scene.add(arenaFloor);\n\n            // Grid lines\n            const gridHelper = new THREE.GridHelper(arenaSize, arenaSize / 10, 0x00e5ff, 0x004060);\n            gridHelper.position.y = -0.4;\n            scene.add(gridHelper);\n        }\n\n        createArenaFloor();\n        \n        // === GALAXY SKY BACKGROUND ===\n        function createGalaxySky() {\n            // Create large inverted sphere for galaxy skybox\n            const skyGeo = new THREE.SphereGeometry(400, 32, 32);\n            \n            // Create procedural galaxy/space texture\n            const canvas = document.createElement('canvas');\n            canvas.width = 2048;\n            canvas.height = 1024;\n            const ctx = canvas.getContext('2d');\n            \n            // Deep space gradient\n            const gradient = ctx.createLinearGradient(0, 0, 0, 1024);\n            gradient.addColorStop(0, '#0a0520');\n            gradient.addColorStop(0.3, '#150a3a');\n            gradient.addColorStop(0.5, '#1a0a50');\n            gradient.addColorStop(0.7, '#150a3a');\n            gradient.addColorStop(1, '#0a0520');\n            ctx.fillStyle = gradient;\n            ctx.fillRect(0, 0, 2048, 1024);\n            \n            // Add nebula clouds\n            ctx.globalAlpha = 0.15;\n            for (let i = 0; i < 8; i++) {\n                const x = Math.random() * 2048;\n                const y = Math.random() * 1024;\n                const w = 300 + Math.random() * 500;\n                const h = 150 + Math.random() * 300;\n                \n                const nebulaGrad = ctx.createRadialGradient(x, y, 0, x, y, w);\n                const colors = ['#4a0080', '#0040a0', '#8000a0', '#00206a'];\n                const color = colors[Math.floor(Math.random() * colors.length)];\n                nebulaGrad.addColorStop(0, color);\n                nebulaGrad.addColorStop(0.5, color.replace(/[0-9a-f]{2}$/i, '30'));\n                nebulaGrad.addColorStop(1, 'transparent');\n                ctx.fillStyle = nebulaGrad;\n                ctx.beginPath();\n                ctx.ellipse(x, y, w, h, Math.random() * Math.PI, 0, Math.PI * 2);\n                ctx.fill();\n            }\n            ctx.globalAlpha = 1;\n            \n            // Add thousands of stars\n            for (let i = 0; i < 3000; i++) {\n                const x = Math.random() * 2048;\n                const y = Math.random() * 1024;\n                const size = Math.random() * 2;\n                const brightness = 0.3 + Math.random() * 0.7;\n                \n                ctx.fillStyle = `rgba(255, 255, 255, ${brightness})`;\n                ctx.beginPath();\n                ctx.arc(x, y, size, 0, Math.PI * 2);\n                ctx.fill();\n                \n                // Add glow to brighter stars\n                if (brightness > 0.8 && size > 1) {\n                    ctx.fillStyle = `rgba(255, 255, 255, ${brightness * 0.2})`;\n                    ctx.beginPath();\n                    ctx.arc(x, y, size * 3, 0, Math.PI * 2);\n                    ctx.fill();\n                }\n            }\n            \n            // Add star clusters\n            for (let c = 0; c < 15; c++) {\n                const cx = Math.random() * 2048;\n                const cy = Math.random() * 1024;\n                const count = 30 + Math.floor(Math.random() * 50);\n                \n                for (let s = 0; s < count; s++) {\n                    const angle = Math.random() * Math.PI * 2;\n                    const dist = Math.random() * 80;\n                    const sx = cx + Math.cos(angle) * dist;\n                    const sy = cy + Math.sin(angle) * dist;\n                    \n                    ctx.fillStyle = `rgba(200, 220, 255, ${0.4 + Math.random() * 0.6})`;\n                    ctx.beginPath();\n                    ctx.arc(sx, sy, 0.5 + Math.random(), 0, Math.PI * 2);\n                    ctx.fill();\n                }\n            }\n            \n            // Add Milky Way band\n            ctx.globalAlpha = 0.12;\n            for (let i = 0; i < 5; i++) {\n                const milkyGrad = ctx.createLinearGradient(0, 400 + i * 20, 2048, 600 + i * 20);\n                milkyGrad.addColorStop(0, 'transparent');\n                milkyGrad.addColorStop(0.3, '#d0d8ff');\n                milkyGrad.addColorStop(0.5, '#ffffff');\n                milkyGrad.addColorStop(0.7, '#d0d8ff');\n                milkyGrad.addColorStop(1, 'transparent');\n                ctx.fillStyle = milkyGrad;\n                ctx.fillRect(0, 400 + i * 20, 2048, 40);\n            }\n            ctx.globalAlpha = 1;\n            \n            const skyTexture = new THREE.CanvasTexture(canvas);\n            skyTexture.needsUpdate = true;\n            \n            const skyMat = new THREE.MeshBasicMaterial({\n                map: skyTexture,\n                side: THREE.BackSide, // Render inside of sphere\n                fog: false // Don't apply fog to sky\n            });\n            \n            const skyMesh = new THREE.Mesh(skyGeo, skyMat);\n            scene.add(skyMesh);\n            \n            return skyMesh;\n        }\n        \n        createGalaxySky();\n\n        // === AGAR.IO STYLE MASS SCALING ===\n        // Visual Radius = SquareRoot(Mass) * BaseMultiplier\n        // This creates diminishing returns - eating when tiny = big visual growth\n        // Eating when massive = barely noticeable visual change\n        const BASE_RADIUS_MULTIPLIER = 0.35; // Calibrated so mass 10 \u2248 radius 1.1\n        const GIANT_THRESHOLD = 5000; // Mass at which tiny slimes are ignored\n        const DECAY_THRESHOLD = 500; // Mass at which decay begins\n        const DECAY_RATE = 0.01; // 1% decay\n        const DECAY_INTERVAL = 3; // Every 3 seconds\n        \n        function getRadius(mass) {\n            // Agar.io formula: radius grows with square root of mass\n            // This means doubling your mass only increases radius by ~41%\n            return Math.sqrt(mass) * BASE_RADIUS_MULTIPLIER;\n        }\n\n        function createPlayerMesh() {\n            const radius = getRadius(playerMass);\n            \n            // Create player with selected skin (default: Earth)\n            playerMesh = createCelestialMesh(selectedPlayerSkin, 1);\n            \n            // Scale to actual radius\n            playerMesh.scale.set(radius, radius, radius);\n            playerMesh.position.set(playerPos.x, radius, playerPos.z);\n            playerMesh.userData.baseScale = 1;\n            scene.add(playerMesh);\n        }\n\n        function spawnSlimes() {\n            const count = config.slimes.count;\n            const arenaSize = config.arena.size;\n            const sizeMult = config.slimes.sizeMultiplier;\n            const colors = [0xff4444, 0x44ff44, 0x4444ff, 0xffff44, 0xff44ff, 0x44ffff, 0xff8800];\n            \n            const slimeGeo = new THREE.SphereGeometry(0.5 * sizeMult, 12, 12);\n            \n            for (let i = 0; i < count; i++) {\n                const color = colors[Math.floor(Math.random() * colors.length)];\n                const mat = new THREE.MeshPhongMaterial({\n                    color: color,\n                    emissive: new THREE.Color(color).multiplyScalar(0.4),\n                    shininess: 80,\n                    transparent: true,\n                    opacity: 0.85\n                });\n                const mesh = new THREE.Mesh(slimeGeo, mat);\n                const half = arenaSize / 2 - 5;\n                const x = (Math.random() - 0.5) * 2 * half;\n                const z = (Math.random() - 0.5) * 2 * half;\n                mesh.position.set(x, 0.5 * sizeMult, z);\n                scene.add(mesh);\n                slimeMeshes.push(mesh);\n                slimes.push({ x, z, active: true, idx: i });\n            }\n        }\n\n        function spawnAiBots() {\n            const arenaSize = config.arena.size;\n            const botCount = config.bots?.count ?? 15;\n            const botNames = [\n                'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',\n                'Zeta', 'Omega', 'Nova', 'Blaze', 'Shadow',\n                'Titan', 'Storm', 'Frost', 'Venom', 'Spike',\n                'Chaos', 'Viper', 'Flash', 'Crush', 'Fury'\n            ];\n            \n            for (let i = 0; i < botCount; i++) {\n                // Pre-populated arena: bots spawn with mass 50-100 for a \"lived-in\" world\n                const mass = 50 + Math.random() * 50;\n                const radius = getRadius(mass);\n                \n                // Select cosmic skin for this bot\n                const skinType = selectBotSkin();\n                const mesh = createCelestialMesh(skinType, radius);\n                \n                const half = arenaSize / 2 - 20;\n                const x = (Math.random() - 0.5) * 2 * half;\n                const z = (Math.random() - 0.5) * 2 * half;\n                \n                mesh.position.set(x, radius, z);\n                scene.add(mesh);\n                botMeshes.push(mesh);\n                \n                // Track Sun bots for light distance updates\n                if (skinType === 'sun' && mesh.userData.sunLight) {\n                    botLights.push({ mesh, light: mesh.userData.sunLight });\n                }\n                \n                // Generate thematic name based on skin\n                let botName = botNames[i % botNames.length];\n                if (skinType === 'sun') botName = 'Sol ' + botName;\n                else if (skinType === 'blackhole') botName = 'Void ' + botName;\n                else if (skinType === 'galaxy') botName = 'Nebula ' + botName;\n                else if (skinType === 'jupiter') botName = 'Giant ' + botName;\n                else if (skinType === 'mars') botName = 'Red ' + botName;\n                else if (skinType === 'moon') botName = 'Lunar ' + botName;\n                \n                // Bots start in active foraging/hunting state\n                const state = Math.random() < 0.5 ? 'foraging' : 'hunting';\n                \n                // === PERSONALITY SYSTEM ===\n                // Different bots have different behavioral tendencies\n                const personalities = ['aggressive', 'cautious', 'opportunist', 'territorial', 'nomad'];\n                const personality = personalities[Math.floor(Math.random() * personalities.length)];\n                \n                aiBots.push({\n                    x, z, mass, radius, mesh, \n                    name: botName,\n                    skinType: skinType,\n                    dirX: Math.random() - 0.5,\n                    dirZ: Math.random() - 0.5,\n                    changeTimer: Math.random() * 2, // Staggered behavior changes\n                    aiState: state,\n                    // Enhanced AI properties\n                    currentTarget: null, // { isPlayer, index, x, z, mass }\n                    chaseTimer: 0,\n                    maxChaseTime: 5 + Math.random() * 3, // 5-8 seconds\n                    lastTargetSwitch: 0,\n                    // Rolling physics\n                    prevX: x,\n                    prevZ: z,\n                    rotationX: 0,\n                    rotationZ: 0,\n                    // Black hole disk rotation\n                    diskRotation: 0,\n                    // === ADVANCED FORAGING AI PROPERTIES ===\n                    lastEatTime: performance.now(), // Track last meal for starvation\n                    clusterTarget: null, // { x, z, density, slimeCount }\n                    clusterScanTimer: Math.random() * 2, // Staggered scans\n                    sweepPhase: 0, // Current phase in S-curve (0 to 2\u03c0)\n                    sweepCenter: null, // Center of current sweep area\n                    sweepTimer: 0, // Time spent sweeping\n                    isStarving: false, // Starvation panic state\n                    greedOverride: false, // Currently distracted by food\n                    // === SMARTER AI PROPERTIES ===\n                    personality: personality, // Behavioral tendencies\n                    threatMemory: [], // [{x, z, mass, timestamp}] Recent threats to avoid\n                    lastThreatTime: 0, // When last threatened\n                    opportunityTarget: null, // Target distracted by combat\n                    ambushPosition: null, // Position to wait in ambush\n                    ambushTimer: 0, // Time spent waiting in ambush\n                    combatAwareness: [], // [{x, z, participants, timestamp}] Nearby fights\n                    velocityHistory: [], // [{vx, vz, t}] For momentum tracking\n                    successfulKills: 0, // Track kills for confidence\n                    failedChases: 0, // Track failed chases for learning\n                    territoryCenter: { x: x, z: z }, // Home area for territorial bots\n                    patrolAngle: Math.random() * Math.PI * 2, // Current patrol angle\n                    lastDecisionTime: 0 // Prevent rapid decision changes\n                });\n            }\n        }\n        \n        // Safe-zone spawning: find location away from large bots\n        function findSafeSpawnLocation() {\n            const arenaSize = config.arena.size;\n            const half = arenaSize / 2 - 10;\n            const SAFE_RADIUS = 300; // Must be 300 units away from bots >100 mass\n            const MAX_ATTEMPTS = 50;\n            \n            // Get all large bots (mass > 100)\n            const largeBots = aiBots.filter(b => b.mass > 100);\n            \n            for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n                const testX = (Math.random() - 0.5) * 2 * half;\n                const testZ = (Math.random() - 0.5) * 2 * half;\n                \n                let isSafe = true;\n                for (const bot of largeBots) {\n                    const dx = testX - bot.x;\n                    const dz = testZ - bot.z;\n                    const dist = Math.sqrt(dx * dx + dz * dz);\n                    \n                    // Scale safe radius based on arena size (300 is for 200 arena)\n                    const scaledSafeRadius = SAFE_RADIUS * (arenaSize / 200);\n                    if (dist < scaledSafeRadius) {\n                        isSafe = false;\n                        break;\n                    }\n                }\n                \n                if (isSafe) {\n                    return { x: testX, z: testZ };\n                }\n            }\n            \n            // Fallback: spawn at center if no safe spot found\n            // (but give invincibility anyway)\n            return { x: 0, z: 0 };\n        }\n        \n        // Create glowing shield effect for spawn invincibility\n        function createShieldMesh() {\n            if (shieldMesh) {\n                scene.remove(shieldMesh);\n            }\n            \n            const shieldGeo = new THREE.SphereGeometry(1, 32, 32);\n            const shieldMat = new THREE.MeshPhongMaterial({\n                color: 0xffffff,\n                emissive: 0x88ccff,\n                emissiveIntensity: 0.8,\n                transparent: true,\n                opacity: 0.35,\n                side: THREE.DoubleSide\n            });\n            shieldMesh = new THREE.Mesh(shieldGeo, shieldMat);\n            scene.add(shieldMesh);\n            return shieldMesh;\n        }\n        \n        function updateShieldMesh(playerRadius) {\n            if (shieldMesh && spawnInvincible) {\n                const shieldScale = playerRadius * 1.3;\n                shieldMesh.scale.set(shieldScale, shieldScale, shieldScale);\n                shieldMesh.position.copy(playerMesh.position);\n                \n                // Pulsing effect\n                const pulse = 0.3 + Math.sin(performance.now() * 0.008) * 0.1;\n                shieldMesh.material.opacity = pulse;\n                \n                // Fade out near end of invincibility\n                if (spawnInvincibilityTimer < 1) {\n                    shieldMesh.material.opacity *= spawnInvincibilityTimer;\n                }\n            }\n        }\n        \n        function removeShieldMesh() {\n            if (shieldMesh) {\n                scene.remove(shieldMesh);\n                shieldMesh = null;\n            }\n        }\n        \n        // Create direction arrow indicator\n        function createDirectionArrow() {\n            if (directionArrow) {\n                scene.remove(directionArrow);\n            }\n            \n            // Create arrow group\n            directionArrow = new THREE.Group();\n            \n            // Arrow shaft (cylinder)\n            const shaftGeo = new THREE.CylinderGeometry(0.15, 0.15, 1.2, 8);\n            const arrowMat = new THREE.MeshPhongMaterial({\n                color: 0x00ffff,\n                emissive: 0x00aaaa,\n                emissiveIntensity: 0.8,\n                transparent: true,\n                opacity: 0.9\n            });\n            const shaft = new THREE.Mesh(shaftGeo, arrowMat);\n            shaft.rotation.x = Math.PI / 2; // Point forward\n            shaft.position.z = 0.6;\n            directionArrow.add(shaft);\n            \n            // Arrow head (cone)\n            const headGeo = new THREE.ConeGeometry(0.4, 0.8, 8);\n            const head = new THREE.Mesh(headGeo, arrowMat);\n            head.rotation.x = Math.PI / 2; // Point forward\n            head.position.z = 1.6;\n            directionArrow.add(head);\n            \n            // Glow ring at base\n            const ringGeo = new THREE.TorusGeometry(0.3, 0.08, 8, 16);\n            const ringMat = new THREE.MeshPhongMaterial({\n                color: 0x00ffff,\n                emissive: 0x00ffff,\n                emissiveIntensity: 1,\n                transparent: true,\n                opacity: 0.7\n            });\n            const ring = new THREE.Mesh(ringGeo, ringMat);\n            ring.rotation.x = Math.PI / 2;\n            directionArrow.add(ring);\n            \n            directionArrow.visible = false;\n            scene.add(directionArrow);\n            \n            return directionArrow;\n        }\n        \n        // Update direction arrow position and rotation\n        function updateDirectionArrow(playerRadius, timestamp) {\n            if (!directionArrow) return;\n            \n            // Calculate movement magnitude\n            const velMag = Math.sqrt(playerVelocity.x ** 2 + playerVelocity.z ** 2);\n            \n            if (velMag > 0.5) {\n                // Show arrow when moving\n                directionArrow.visible = true;\n                arrowVisible = true;\n                \n                // Position above player\n                const arrowHeight = playerRadius * 2.2 + 1;\n                directionArrow.position.set(playerPos.x, arrowHeight, playerPos.z);\n                \n                // Scale arrow based on player size\n                const arrowScale = Math.max(0.8, Math.min(2, playerRadius * 0.5));\n                directionArrow.scale.set(arrowScale, arrowScale, arrowScale);\n                \n                // Rotate to point in movement direction\n                const angle = Math.atan2(playerVelocity.x, playerVelocity.z);\n                directionArrow.rotation.y = angle;\n                \n                // Pulsing glow effect\n                const pulse = 0.7 + Math.sin(timestamp * 0.008) * 0.3;\n                directionArrow.children.forEach(child => {\n                    if (child.material) {\n                        child.material.opacity = pulse * 0.9;\n                        child.material.emissiveIntensity = 0.5 + pulse * 0.5;\n                    }\n                });\n                \n                // Slight bobbing animation\n                const bob = Math.sin(timestamp * 0.005) * 0.1;\n                directionArrow.position.y += bob;\n                \n            } else {\n                // Fade out when stationary\n                if (arrowVisible) {\n                    directionArrow.children.forEach(child => {\n                        if (child.material && child.material.opacity > 0.05) {\n                            child.material.opacity *= 0.9;\n                        }\n                    });\n                    \n                    if (directionArrow.children[0]?.material?.opacity < 0.1) {\n                        directionArrow.visible = false;\n                        arrowVisible = false;\n                    }\n                }\n            }\n        }\n        \n        // Bot respawn with ecosystem balance (80% base, 20% mid-tier)\n        function getBotRespawnMass() {\n            if (Math.random() < 0.2) {\n                // 20% chance to respawn as mid-tier threat (50-100 mass)\n                return 50 + Math.random() * 50;\n            } else {\n                // 80% chance to respawn at base mass to build up normally\n                return 10;\n            }\n        }\n\n        // === COSMIC SKIN & MATERIAL SYSTEM ===\n        const CELESTIAL_BODIES = {\n            earth: {\n                name: 'Earth',\n                baseColor: 0x2266aa,\n                emissive: 0x112244,\n                emissiveIntensity: 0.1,\n                shininess: 80,\n                hasAtmosphere: true,\n                atmosphereColor: 0x88ccff,\n                isPlayer: true\n            },\n            moon: {\n                name: 'Moon',\n                baseColor: 0x888888,\n                emissive: 0x222222,\n                emissiveIntensity: 0.05,\n                shininess: 30,\n                hasAtmosphere: false\n            },\n            mars: {\n                name: 'Mars',\n                baseColor: 0xcc4422,\n                emissive: 0x441100,\n                emissiveIntensity: 0.1,\n                shininess: 20,\n                hasAtmosphere: false\n            },\n            jupiter: {\n                name: 'Jupiter',\n                baseColor: 0xddaa77,\n                emissive: 0x332211,\n                emissiveIntensity: 0.1,\n                shininess: 40,\n                hasAtmosphere: false\n            },\n            sun: {\n                name: 'Sun',\n                baseColor: 0xffcc00,\n                emissive: 0xffaa00,\n                emissiveIntensity: 1.0,\n                shininess: 0,\n                hasAtmosphere: false,\n                castsLight: true,\n                lightColor: 0xffdd88,\n                lightIntensity: 0.8,\n                lightDistance: 30\n            },\n            galaxy: {\n                name: 'Galaxy',\n                baseColor: 0x220044,\n                emissive: 0x4400aa,\n                emissiveIntensity: 0.4,\n                shininess: 100,\n                hasAtmosphere: false\n            },\n            blackhole: {\n                name: 'Black Hole',\n                baseColor: 0x000000,\n                emissive: 0x000000,\n                emissiveIntensity: 0,\n                shininess: 0,\n                hasAtmosphere: false,\n                hasAccretionDisk: true,\n                diskColor: 0xff6600,\n                rarity: 0.08 // 8% chance\n            }\n        };\n        \n        const BOT_SKIN_POOL = ['moon', 'mars', 'jupiter', 'sun', 'galaxy', 'blackhole'];\n        \n        // Create procedural canvas texture for celestial bodies\n        function createCelestialTexture(bodyType) {\n            // Check if we have a pre-loaded texture for this body type\n            const textureId = bodyType + '_texture';\n            if (imageCache[textureId]) {\n                const texture = new THREE.Texture(imageCache[textureId]);\n                texture.wrapS = THREE.RepeatWrapping;\n                texture.wrapT = THREE.ClampToEdgeWrapping;\n                texture.needsUpdate = true;\n                return texture;\n            }\n            \n            // Fallback to procedural generation\n            const canvas = document.createElement('canvas');\n            canvas.width = 256;\n            canvas.height = 128;\n            const ctx = canvas.getContext('2d');\n            \n            switch(bodyType) {\n                case 'earth':\n                    // Ocean base\n                    ctx.fillStyle = '#1a5599';\n                    ctx.fillRect(0, 0, 256, 128);\n                    \n                    // Continents\n                    ctx.fillStyle = '#3d7a3d';\n                    // North America\n                    ctx.beginPath();\n                    ctx.ellipse(50, 35, 25, 18, 0.2, 0, Math.PI * 2);\n                    ctx.fill();\n                    // South America\n                    ctx.beginPath();\n                    ctx.ellipse(70, 80, 12, 22, 0.3, 0, Math.PI * 2);\n                    ctx.fill();\n                    // Europe/Africa\n                    ctx.beginPath();\n                    ctx.ellipse(135, 50, 15, 35, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    // Asia\n                    ctx.beginPath();\n                    ctx.ellipse(190, 40, 35, 20, -0.2, 0, Math.PI * 2);\n                    ctx.fill();\n                    // Australia\n                    ctx.beginPath();\n                    ctx.ellipse(210, 90, 12, 10, 0.3, 0, Math.PI * 2);\n                    ctx.fill();\n                    \n                    // Ice caps\n                    ctx.fillStyle = '#ffffff';\n                    ctx.fillRect(0, 0, 256, 8);\n                    ctx.fillRect(0, 120, 256, 8);\n                    \n                    // Cloud wisps\n                    ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';\n                    for (let i = 0; i < 12; i++) {\n                        ctx.beginPath();\n                        ctx.ellipse(Math.random() * 256, Math.random() * 128, 20 + Math.random() * 30, 5 + Math.random() * 8, Math.random() * Math.PI, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    break;\n                    \n                case 'moon':\n                    // Grey base\n                    ctx.fillStyle = '#777777';\n                    ctx.fillRect(0, 0, 256, 128);\n                    \n                    // Craters\n                    for (let i = 0; i < 40; i++) {\n                        const x = Math.random() * 256;\n                        const y = Math.random() * 128;\n                        const r = 3 + Math.random() * 15;\n                        \n                        // Shadow\n                        ctx.fillStyle = '#555555';\n                        ctx.beginPath();\n                        ctx.arc(x + 2, y + 2, r, 0, Math.PI * 2);\n                        ctx.fill();\n                        \n                        // Crater rim highlight\n                        ctx.fillStyle = '#999999';\n                        ctx.beginPath();\n                        ctx.arc(x - 1, y - 1, r * 0.9, 0, Math.PI * 2);\n                        ctx.fill();\n                        \n                        // Crater center\n                        ctx.fillStyle = '#666666';\n                        ctx.beginPath();\n                        ctx.arc(x, y, r * 0.7, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    break;\n                    \n                case 'mars':\n                    // Rusty red base\n                    const marsGrad = ctx.createLinearGradient(0, 0, 256, 128);\n                    marsGrad.addColorStop(0, '#cc5533');\n                    marsGrad.addColorStop(0.5, '#aa3322');\n                    marsGrad.addColorStop(1, '#993322');\n                    ctx.fillStyle = marsGrad;\n                    ctx.fillRect(0, 0, 256, 128);\n                    \n                    // Surface features (valleys, mountains)\n                    ctx.fillStyle = '#884422';\n                    for (let i = 0; i < 20; i++) {\n                        ctx.beginPath();\n                        ctx.ellipse(Math.random() * 256, Math.random() * 128, 10 + Math.random() * 25, 5 + Math.random() * 12, Math.random() * Math.PI, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    \n                    // Dust storms\n                    ctx.fillStyle = 'rgba(200, 150, 100, 0.3)';\n                    for (let i = 0; i < 5; i++) {\n                        ctx.beginPath();\n                        ctx.ellipse(Math.random() * 256, Math.random() * 128, 30 + Math.random() * 40, 10 + Math.random() * 15, Math.random() * Math.PI, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    \n                    // Polar cap\n                    ctx.fillStyle = '#ddccbb';\n                    ctx.fillRect(0, 0, 256, 6);\n                    break;\n                    \n                case 'jupiter':\n                    // Create seamless Jupiter gas giant with rich atmospheric bands\n                    \n                    // Base gradient from pole to equator\n                    const jupiterBase = ctx.createLinearGradient(0, 0, 0, 128);\n                    jupiterBase.addColorStop(0, '#d4a574');\n                    jupiterBase.addColorStop(0.5, '#e8c396');\n                    jupiterBase.addColorStop(1, '#d4a574');\n                    ctx.fillStyle = jupiterBase;\n                    ctx.fillRect(0, 0, 256, 128);\n                    \n                    // Define precise atmospheric bands with varied widths\n                    const jupiterBands = [\n                        { y: 0, h: 8, color: '#c9a67a', dark: true },      // North polar\n                        { y: 8, h: 6, color: '#f5d4a8', dark: false },     // North temperate light\n                        { y: 14, h: 10, color: '#a67c52', dark: true },    // North temperate dark\n                        { y: 24, h: 8, color: '#edd8b5', dark: false },    // North tropical light\n                        { y: 32, h: 12, color: '#8b6542', dark: true },    // North equatorial dark\n                        { y: 44, h: 10, color: '#f7e2c4', dark: false },   // Equatorial zone light\n                        { y: 54, h: 14, color: '#9c7049', dark: true },    // Equatorial band dark\n                        { y: 68, h: 10, color: '#f0d5ab', dark: false },   // South equatorial light\n                        { y: 78, h: 12, color: '#8a6340', dark: true },    // South tropical dark (GRS zone)\n                        { y: 90, h: 8, color: '#e8cfa0', dark: false },    // South temperate light\n                        { y: 98, h: 10, color: '#a07550', dark: true },    // South temperate dark\n                        { y: 108, h: 8, color: '#ddc093', dark: false },   // South polar light\n                        { y: 116, h: 12, color: '#c49a6c', dark: true }    // South polar\n                    ];\n                    \n                    // Draw each band with gradient for depth\n                    for (const band of jupiterBands) {\n                        const bandGrad = ctx.createLinearGradient(0, band.y, 0, band.y + band.h);\n                        const baseColor = band.color;\n                        const lighterColor = band.dark ? \n                            baseColor.replace(/[0-9a-f]{2}$/i, m => Math.min(255, parseInt(m, 16) + 20).toString(16).padStart(2, '0')) :\n                            baseColor;\n                        bandGrad.addColorStop(0, lighterColor);\n                        bandGrad.addColorStop(0.5, baseColor);\n                        bandGrad.addColorStop(1, lighterColor);\n                        ctx.fillStyle = bandGrad;\n                        ctx.fillRect(0, band.y, 256, band.h);\n                    }\n                    \n                    // Add turbulent band edges with wavy patterns\n                    ctx.globalAlpha = 0.4;\n                    for (const band of jupiterBands) {\n                        const edgeY = band.y + band.h;\n                        if (edgeY < 128) {\n                            ctx.beginPath();\n                            ctx.moveTo(0, edgeY);\n                            for (let x = 0; x <= 256; x += 8) {\n                                const waveY = edgeY + Math.sin(x * 0.08 + band.y) * 2 + Math.sin(x * 0.15) * 1.5;\n                                ctx.lineTo(x, waveY);\n                            }\n                            ctx.lineTo(256, edgeY + 4);\n                            ctx.lineTo(0, edgeY + 4);\n                            ctx.closePath();\n                            ctx.fillStyle = band.dark ? '#6b4a2a' : '#f5e0c0';\n                            ctx.fill();\n                        }\n                    }\n                    ctx.globalAlpha = 1;\n                    \n                    // Add swirling cloud patterns throughout\n                    for (let i = 0; i < 25; i++) {\n                        const sx = Math.random() * 256;\n                        const sy = Math.random() * 128;\n                        const sw = 15 + Math.random() * 30;\n                        const sh = 3 + Math.random() * 6;\n                        \n                        ctx.save();\n                        ctx.translate(sx, sy);\n                        ctx.rotate((Math.random() - 0.5) * 0.3);\n                        \n                        const swirlGrad = ctx.createLinearGradient(-sw/2, 0, sw/2, 0);\n                        const swirlColor = Math.random() > 0.5 ? 'rgba(255, 240, 220, 0.3)' : 'rgba(120, 80, 50, 0.25)';\n                        swirlGrad.addColorStop(0, 'transparent');\n                        swirlGrad.addColorStop(0.3, swirlColor);\n                        swirlGrad.addColorStop(0.7, swirlColor);\n                        swirlGrad.addColorStop(1, 'transparent');\n                        \n                        ctx.fillStyle = swirlGrad;\n                        ctx.beginPath();\n                        ctx.ellipse(0, 0, sw, sh, 0, 0, Math.PI * 2);\n                        ctx.fill();\n                        ctx.restore();\n                    }\n                    \n                    // Great Red Spot - iconic storm with swirling detail\n                    const grsX = 180, grsY = 82;\n                    const grsW = 28, grsH = 16;\n                    \n                    // Outer storm glow\n                    const grsGlow = ctx.createRadialGradient(grsX, grsY, 0, grsX, grsY, grsW);\n                    grsGlow.addColorStop(0, 'rgba(180, 60, 30, 0.8)');\n                    grsGlow.addColorStop(0.6, 'rgba(200, 80, 40, 0.6)');\n                    grsGlow.addColorStop(1, 'rgba(160, 70, 50, 0)');\n                    ctx.fillStyle = grsGlow;\n                    ctx.beginPath();\n                    ctx.ellipse(grsX, grsY, grsW + 5, grsH + 3, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    \n                    // Main storm body\n                    const grsMain = ctx.createRadialGradient(grsX - 5, grsY - 3, 0, grsX, grsY, grsW);\n                    grsMain.addColorStop(0, '#e8a080');\n                    grsMain.addColorStop(0.3, '#d86040');\n                    grsMain.addColorStop(0.6, '#c04030');\n                    grsMain.addColorStop(1, '#a03020');\n                    ctx.fillStyle = grsMain;\n                    ctx.beginPath();\n                    ctx.ellipse(grsX, grsY, grsW, grsH, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    \n                    // Inner eye of storm\n                    const grsEye = ctx.createRadialGradient(grsX, grsY, 0, grsX, grsY, grsW * 0.5);\n                    grsEye.addColorStop(0, '#f0b090');\n                    grsEye.addColorStop(0.5, '#e09070');\n                    grsEye.addColorStop(1, 'transparent');\n                    ctx.fillStyle = grsEye;\n                    ctx.beginPath();\n                    ctx.ellipse(grsX, grsY, grsW * 0.5, grsH * 0.5, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    \n                    // Spiral arms in the storm\n                    ctx.strokeStyle = 'rgba(180, 100, 70, 0.5)';\n                    ctx.lineWidth = 2;\n                    for (let arm = 0; arm < 3; arm++) {\n                        ctx.beginPath();\n                        for (let t = 0; t < Math.PI * 1.5; t += 0.1) {\n                            const r = 5 + t * 8;\n                            const angle = t + arm * (Math.PI * 2 / 3);\n                            const px = grsX + Math.cos(angle) * r * 0.8;\n                            const py = grsY + Math.sin(angle) * r * 0.45;\n                            if (t === 0) ctx.moveTo(px, py);\n                            else ctx.lineTo(px, py);\n                        }\n                        ctx.stroke();\n                    }\n                    \n                    // Smaller white storms (anticyclones)\n                    const whiteStorms = [\n                        { x: 60, y: 35, w: 10, h: 6 },\n                        { x: 120, y: 78, w: 8, h: 5 },\n                        { x: 220, y: 50, w: 12, h: 7 }\n                    ];\n                    for (const storm of whiteStorms) {\n                        const wsGrad = ctx.createRadialGradient(storm.x, storm.y, 0, storm.x, storm.y, storm.w);\n                        wsGrad.addColorStop(0, 'rgba(255, 250, 240, 0.9)');\n                        wsGrad.addColorStop(0.5, 'rgba(240, 235, 220, 0.6)');\n                        wsGrad.addColorStop(1, 'rgba(230, 220, 200, 0)');\n                        ctx.fillStyle = wsGrad;\n                        ctx.beginPath();\n                        ctx.ellipse(storm.x, storm.y, storm.w, storm.h, 0, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    \n                    // Add subtle lightning flashes in dark bands\n                    ctx.globalAlpha = 0.15;\n                    for (let i = 0; i < 5; i++) {\n                        const lx = Math.random() * 256;\n                        const ly = 30 + Math.random() * 70;\n                        ctx.fillStyle = '#ffffff';\n                        ctx.beginPath();\n                        ctx.arc(lx, ly, 2 + Math.random() * 3, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    ctx.globalAlpha = 1;\n                    break;\n                    \n                case 'sun':\n                    // Bright yellow-orange gradient\n                    const sunGrad = ctx.createRadialGradient(128, 64, 0, 128, 64, 100);\n                    sunGrad.addColorStop(0, '#ffffcc');\n                    sunGrad.addColorStop(0.3, '#ffdd66');\n                    sunGrad.addColorStop(0.6, '#ffaa22');\n                    sunGrad.addColorStop(1, '#ff6600');\n                    ctx.fillStyle = sunGrad;\n                    ctx.fillRect(0, 0, 256, 128);\n                    \n                    // Solar flares/spots\n                    ctx.fillStyle = '#ff8800';\n                    for (let i = 0; i < 15; i++) {\n                        ctx.beginPath();\n                        ctx.arc(Math.random() * 256, Math.random() * 128, 5 + Math.random() * 15, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    \n                    // Hot spots\n                    ctx.fillStyle = '#ffffaa';\n                    for (let i = 0; i < 8; i++) {\n                        ctx.beginPath();\n                        ctx.arc(Math.random() * 256, Math.random() * 128, 3 + Math.random() * 8, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    break;\n                    \n                case 'galaxy':\n                    // Deep space purple/black\n                    const galaxyGrad = ctx.createLinearGradient(0, 0, 256, 128);\n                    galaxyGrad.addColorStop(0, '#110022');\n                    galaxyGrad.addColorStop(0.5, '#220044');\n                    galaxyGrad.addColorStop(1, '#110033');\n                    ctx.fillStyle = galaxyGrad;\n                    ctx.fillRect(0, 0, 256, 128);\n                    \n                    // Nebula swirls\n                    ctx.fillStyle = 'rgba(100, 0, 150, 0.3)';\n                    for (let i = 0; i < 5; i++) {\n                        ctx.beginPath();\n                        ctx.ellipse(Math.random() * 256, Math.random() * 128, 40 + Math.random() * 50, 20 + Math.random() * 30, Math.random() * Math.PI, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    ctx.fillStyle = 'rgba(0, 100, 200, 0.2)';\n                    for (let i = 0; i < 3; i++) {\n                        ctx.beginPath();\n                        ctx.ellipse(Math.random() * 256, Math.random() * 128, 30 + Math.random() * 40, 15 + Math.random() * 20, Math.random() * Math.PI, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    \n                    // Stars (many small bright dots)\n                    for (let i = 0; i < 200; i++) {\n                        const starColors = ['#ffffff', '#aaccff', '#ffaadd', '#aaffaa', '#ffffaa'];\n                        ctx.fillStyle = starColors[Math.floor(Math.random() * starColors.length)];\n                        ctx.globalAlpha = 0.5 + Math.random() * 0.5;\n                        ctx.beginPath();\n                        ctx.arc(Math.random() * 256, Math.random() * 128, 0.5 + Math.random() * 1.5, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    ctx.globalAlpha = 1;\n                    \n                    // Bright stars with glow\n                    for (let i = 0; i < 10; i++) {\n                        const x = Math.random() * 256;\n                        const y = Math.random() * 128;\n                        ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';\n                        ctx.beginPath();\n                        ctx.arc(x, y, 4, 0, Math.PI * 2);\n                        ctx.fill();\n                        ctx.fillStyle = '#ffffff';\n                        ctx.beginPath();\n                        ctx.arc(x, y, 1.5, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    break;\n                    \n                case 'blackhole':\n                    // Pure black with edge distortion\n                    ctx.fillStyle = '#000000';\n                    ctx.fillRect(0, 0, 256, 128);\n                    \n                    // Gravitational lensing effect at edges\n                    const bhGrad = ctx.createRadialGradient(128, 64, 30, 128, 64, 64);\n                    bhGrad.addColorStop(0, 'rgba(0, 0, 0, 1)');\n                    bhGrad.addColorStop(0.7, 'rgba(20, 0, 40, 0.8)');\n                    bhGrad.addColorStop(1, 'rgba(50, 0, 100, 0.3)');\n                    ctx.fillStyle = bhGrad;\n                    ctx.fillRect(0, 0, 256, 128);\n                    break;\n            }\n            \n            const texture = new THREE.CanvasTexture(canvas);\n            texture.wrapS = THREE.RepeatWrapping;\n            texture.wrapT = THREE.ClampToEdgeWrapping;\n            texture.needsUpdate = true;\n            return texture;\n        }\n        \n        // Create celestial body mesh with proper materials\n        function createCelestialMesh(bodyType, radius = 1) {\n            const body = CELESTIAL_BODIES[bodyType];\n            const texture = createCelestialTexture(bodyType);\n            \n            const geo = new THREE.SphereGeometry(1, 32, 32);\n            const mat = new THREE.MeshPhongMaterial({\n                map: texture,\n                color: 0xffffff,\n                shininess: body.shininess,\n                emissive: new THREE.Color(body.emissive),\n                emissiveIntensity: body.emissiveIntensity,\n                emissiveMap: bodyType === 'sun' ? texture : null\n            });\n            \n            const mesh = new THREE.Mesh(geo, mat);\n            mesh.userData.bodyType = bodyType;\n            mesh.userData.bodyData = body;\n            \n            // Add atmosphere glow for Earth\n            if (body.hasAtmosphere) {\n                const atmosGeo = new THREE.SphereGeometry(1.08, 32, 32);\n                const atmosMat = new THREE.MeshPhongMaterial({\n                    color: body.atmosphereColor,\n                    transparent: true,\n                    opacity: 0.15,\n                    side: THREE.BackSide\n                });\n                const atmosphere = new THREE.Mesh(atmosGeo, atmosMat);\n                mesh.add(atmosphere);\n            }\n            \n            // Add point light for Sun\n            if (body.castsLight) {\n                const sunLight = new THREE.PointLight(body.lightColor, body.lightIntensity, body.lightDistance);\n                sunLight.position.set(0, 0, 0);\n                mesh.add(sunLight);\n                mesh.userData.sunLight = sunLight;\n            }\n            \n            // Add accretion disk for Black Hole\n            if (body.hasAccretionDisk) {\n                const diskGeo = new THREE.RingGeometry(1.3, 2.2, 64);\n                const diskMat = new THREE.MeshBasicMaterial({\n                    color: body.diskColor,\n                    side: THREE.DoubleSide,\n                    transparent: true,\n                    opacity: 0.8\n                });\n                const disk = new THREE.Mesh(diskGeo, diskMat);\n                disk.rotation.x = Math.PI / 2; // Horizontal\n                mesh.add(disk);\n                mesh.userData.accretionDisk = disk;\n                \n                // Inner bright ring\n                const innerDiskGeo = new THREE.RingGeometry(1.1, 1.4, 64);\n                const innerDiskMat = new THREE.MeshBasicMaterial({\n                    color: 0xffaa44,\n                    side: THREE.DoubleSide,\n                    transparent: true,\n                    opacity: 0.9\n                });\n                const innerDisk = new THREE.Mesh(innerDiskGeo, innerDiskMat);\n                innerDisk.rotation.x = Math.PI / 2;\n                mesh.add(innerDisk);\n                mesh.userData.innerDisk = innerDisk;\n            }\n            \n            // Add rolling indicator band (subtle, matches theme)\n            const bandGeo = new THREE.TorusGeometry(0.92, 0.03, 8, 32);\n            const bandColor = bodyType === 'blackhole' ? 0x440066 : \n                             bodyType === 'sun' ? 0xffff00 :\n                             bodyType === 'galaxy' ? 0x8800ff :\n                             new THREE.Color(body.baseColor).multiplyScalar(0.7).getHex();\n            const bandMat = new THREE.MeshBasicMaterial({\n                color: bandColor,\n                transparent: true,\n                opacity: bodyType === 'blackhole' ? 0.5 : 0.3\n            });\n            \n            const band1 = new THREE.Mesh(bandGeo, bandMat);\n            band1.rotation.x = Math.PI / 2;\n            mesh.add(band1);\n            \n            const band2 = new THREE.Mesh(bandGeo, bandMat.clone());\n            band2.rotation.z = Math.PI / 2;\n            mesh.add(band2);\n            \n            mesh.scale.set(radius, radius, radius);\n            \n            return mesh;\n        }\n        \n        // Select random bot skin with rarity consideration\n        function selectBotSkin() {\n            // Check for rare black hole first\n            if (Math.random() < CELESTIAL_BODIES.blackhole.rarity) {\n                return 'blackhole';\n            }\n            // Otherwise pick from regular pool (excluding blackhole)\n            const regularPool = BOT_SKIN_POOL.filter(s => s !== 'blackhole');\n            return regularPool[Math.floor(Math.random() * regularPool.length)];\n        }\n        \n        // Track bot lights for dynamic updates\n        const botLights = [];\n\n        spawnSlimes();\n        spawnAiBots();\n\n        // Input handling\n        window.addEventListener('keydown', (e) => {\n            keys[e.key.toLowerCase()] = true;\n            if (e.key === ' ' || e.key === 'Shift') tryDash();\n            if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(e.key.toLowerCase())) {\n                e.preventDefault();\n            }\n        });\n        window.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; });\n        window.addEventListener('blur', () => { keys = {}; inputDir = {x:0,z:0}; });\n\n        // Touch joystick\n        const joystickArea = document.getElementById('joystick-area');\n        const joystickBase = document.getElementById('joystick-base');\n        const joystickKnob = document.getElementById('joystick-knob');\n        let joystickTouchId = null;\n        let joystickCenter = { x: 0, y: 0 };\n        const JOYSTICK_RADIUS = 70;\n\n        joystickArea.addEventListener('touchstart', (e) => {\n            if (gameState !== 'playing') return;\n            e.preventDefault();\n            const touch = e.changedTouches[0];\n            joystickTouchId = touch.identifier;\n            joystickCenter = { x: touch.clientX, y: touch.clientY };\n            joystickBase.style.display = 'block';\n            joystickKnob.style.display = 'block';\n            joystickBase.style.left = (touch.clientX - 80) + 'px';\n            joystickBase.style.top = (touch.clientY - 80) + 'px';\n            joystickKnob.style.left = (touch.clientX - 35) + 'px';\n            joystickKnob.style.top = (touch.clientY - 35) + 'px';\n        });\n\n        joystickArea.addEventListener('touchmove', (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n            for (const touch of e.changedTouches) {\n                if (touch.identifier === joystickTouchId) {\n                    const dx = touch.clientX - joystickCenter.x;\n                    const dy = touch.clientY - joystickCenter.y;\n                    const dist = Math.sqrt(dx*dx + dy*dy);\n                    const clampDist = Math.min(dist, JOYSTICK_RADIUS);\n                    const angle = Math.atan2(dy, dx);\n                    const knobX = joystickCenter.x + Math.cos(angle) * clampDist;\n                    const knobY = joystickCenter.y + Math.sin(angle) * clampDist;\n                    joystickKnob.style.left = (knobX - 35) + 'px';\n                    joystickKnob.style.top = (knobY - 35) + 'px';\n                    \n                    if (dist > 10) {\n                        inputDir.x = (dx / JOYSTICK_RADIUS);\n                        inputDir.z = (dy / JOYSTICK_RADIUS);\n                        const mag = Math.sqrt(inputDir.x*inputDir.x + inputDir.z*inputDir.z);\n                        if (mag > 1) { inputDir.x /= mag; inputDir.z /= mag; }\n                    } else {\n                        inputDir.x = 0; inputDir.z = 0;\n                    }\n                }\n            }\n        });\n\n        function endJoystick() {\n            joystickTouchId = null;\n            joystickBase.style.display = 'none';\n            joystickKnob.style.display = 'none';\n            inputDir.x = 0; inputDir.z = 0;\n        }\n\n        joystickArea.addEventListener('touchend', (e) => {\n            for (const touch of e.changedTouches) {\n                if (touch.identifier === joystickTouchId) endJoystick();\n            }\n        }, { passive: true });\n        joystickArea.addEventListener('touchcancel', endJoystick, { passive: true });\n\n        // UI refs\n        const mainMenu = document.getElementById('main-menu');\n        const hud = document.getElementById('hud');\n        const massDisplay = document.getElementById('mass-display');\n        const skullsDisplay = document.getElementById('skulls-display');\n        const deathScreen = document.getElementById('death-screen');\n        const deathInfo = document.getElementById('death-info');\n        const lbEntries = document.getElementById('lb-entries');\n        const rankDisplay = document.getElementById('rank-display');\n        const comboDisplay = document.getElementById('combo-display');\n        const killFeedEl = document.getElementById('kill-feed');\n        const dashBtn = document.getElementById('dash-btn');\n        const dashCoolFill = document.getElementById('dash-cool-fill');\n        const dangerVignette = document.getElementById('danger-vignette');\n        const floatLayer = document.getElementById('float-layer');\n        \n        // === SKIN SELECTOR SYSTEM ===\n        const skinCarousel = document.getElementById('skin-carousel');\n        const selectedSkinDisplay = document.getElementById('selected-skin-display');\n        \n        // Generate 2D preview of celestial textures for skin selector\n        function generateSkinPreview(skinId) {\n            const canvas = document.createElement('canvas');\n            canvas.width = 80;\n            canvas.height = 80;\n            const ctx = canvas.getContext('2d');\n            \n            // Draw circular background\n            ctx.beginPath();\n            ctx.arc(40, 40, 38, 0, Math.PI * 2);\n            ctx.fillStyle = '#1a1a3e';\n            ctx.fill();\n            \n            // Draw skin-specific visuals\n            switch(skinId) {\n                case 'earth':\n                    // Ocean base\n                    ctx.beginPath();\n                    ctx.arc(40, 40, 36, 0, Math.PI * 2);\n                    ctx.fillStyle = '#1a5599';\n                    ctx.fill();\n                    // Continents\n                    ctx.fillStyle = '#3d7a3d';\n                    ctx.beginPath();\n                    ctx.ellipse(30, 35, 12, 10, 0.2, 0, Math.PI * 2);\n                    ctx.fill();\n                    ctx.beginPath();\n                    ctx.ellipse(55, 45, 10, 15, 0.3, 0, Math.PI * 2);\n                    ctx.fill();\n                    // Ice caps\n                    ctx.fillStyle = '#ffffff';\n                    ctx.fillRect(10, 5, 60, 6);\n                    ctx.fillRect(10, 69, 60, 6);\n                    // Atmospheric glow\n                    const earthGrad = ctx.createRadialGradient(40, 40, 30, 40, 40, 40);\n                    earthGrad.addColorStop(0, 'transparent');\n                    earthGrad.addColorStop(1, 'rgba(136, 204, 255, 0.3)');\n                    ctx.fillStyle = earthGrad;\n                    ctx.fill();\n                    break;\n                    \n                case 'moon':\n                    ctx.beginPath();\n                    ctx.arc(40, 40, 36, 0, Math.PI * 2);\n                    ctx.fillStyle = '#888888';\n                    ctx.fill();\n                    // Craters\n                    for (let i = 0; i < 8; i++) {\n                        const cx = 20 + Math.random() * 40;\n                        const cy = 20 + Math.random() * 40;\n                        const cr = 3 + Math.random() * 6;\n                        ctx.fillStyle = '#666666';\n                        ctx.beginPath();\n                        ctx.arc(cx, cy, cr, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    break;\n                    \n                case 'mars':\n                    const marsGrad = ctx.createRadialGradient(35, 35, 0, 40, 40, 40);\n                    marsGrad.addColorStop(0, '#dd6644');\n                    marsGrad.addColorStop(1, '#993322');\n                    ctx.beginPath();\n                    ctx.arc(40, 40, 36, 0, Math.PI * 2);\n                    ctx.fillStyle = marsGrad;\n                    ctx.fill();\n                    // Surface features\n                    ctx.fillStyle = '#884422';\n                    ctx.beginPath();\n                    ctx.ellipse(50, 50, 10, 6, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    break;\n                    \n                case 'jupiter':\n                    // Base with gradient\n                    const jupGrad = ctx.createLinearGradient(0, 0, 0, 80);\n                    jupGrad.addColorStop(0, '#d4a574');\n                    jupGrad.addColorStop(0.5, '#e8c396');\n                    jupGrad.addColorStop(1, '#d4a574');\n                    ctx.beginPath();\n                    ctx.arc(40, 40, 36, 0, Math.PI * 2);\n                    ctx.fillStyle = jupGrad;\n                    ctx.fill();\n                    \n                    // Precise atmospheric bands\n                    const previewBands = [\n                        { y: 5, h: 6, color: '#a67c52' },\n                        { y: 14, h: 5, color: '#f5d4a8' },\n                        { y: 22, h: 7, color: '#8b6542' },\n                        { y: 32, h: 6, color: '#f0d5ab' },\n                        { y: 41, h: 8, color: '#9c7049' },\n                        { y: 52, h: 6, color: '#e8cfa0' },\n                        { y: 61, h: 7, color: '#8a6340' },\n                        { y: 71, h: 5, color: '#ddc093' }\n                    ];\n                    for (const band of previewBands) {\n                        ctx.fillStyle = band.color;\n                        ctx.fillRect(5, band.y, 70, band.h);\n                    }\n                    \n                    // Great Red Spot with gradient\n                    const grsGrad = ctx.createRadialGradient(52, 55, 0, 52, 55, 10);\n                    grsGrad.addColorStop(0, '#e8a080');\n                    grsGrad.addColorStop(0.4, '#d86040');\n                    grsGrad.addColorStop(1, '#a03020');\n                    ctx.fillStyle = grsGrad;\n                    ctx.beginPath();\n                    ctx.ellipse(52, 55, 10, 6, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    \n                    // Inner highlight\n                    ctx.fillStyle = 'rgba(240, 180, 150, 0.5)';\n                    ctx.beginPath();\n                    ctx.ellipse(50, 54, 4, 2.5, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    \n                    // Small white storm\n                    ctx.fillStyle = 'rgba(255, 250, 240, 0.7)';\n                    ctx.beginPath();\n                    ctx.ellipse(28, 35, 5, 3, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    \n                    // Clip to circle\n                    ctx.globalCompositeOperation = 'destination-in';\n                    ctx.beginPath();\n                    ctx.arc(40, 40, 36, 0, Math.PI * 2);\n                    ctx.fill();\n                    ctx.globalCompositeOperation = 'source-over';\n                    break;\n                    \n                case 'sun':\n                    const sunGrad = ctx.createRadialGradient(40, 40, 0, 40, 40, 40);\n                    sunGrad.addColorStop(0, '#ffffcc');\n                    sunGrad.addColorStop(0.3, '#ffdd66');\n                    sunGrad.addColorStop(0.7, '#ffaa22');\n                    sunGrad.addColorStop(1, '#ff6600');\n                    ctx.beginPath();\n                    ctx.arc(40, 40, 36, 0, Math.PI * 2);\n                    ctx.fillStyle = sunGrad;\n                    ctx.fill();\n                    // Glow effect\n                    ctx.shadowColor = '#ffaa00';\n                    ctx.shadowBlur = 15;\n                    ctx.beginPath();\n                    ctx.arc(40, 40, 30, 0, Math.PI * 2);\n                    ctx.fillStyle = 'rgba(255, 220, 100, 0.3)';\n                    ctx.fill();\n                    ctx.shadowBlur = 0;\n                    break;\n                    \n                case 'galaxy':\n                    const galaxyGrad = ctx.createRadialGradient(40, 40, 0, 40, 40, 40);\n                    galaxyGrad.addColorStop(0, '#330055');\n                    galaxyGrad.addColorStop(0.5, '#220044');\n                    galaxyGrad.addColorStop(1, '#110022');\n                    ctx.beginPath();\n                    ctx.arc(40, 40, 36, 0, Math.PI * 2);\n                    ctx.fillStyle = galaxyGrad;\n                    ctx.fill();\n                    // Nebula swirls\n                    ctx.fillStyle = 'rgba(100, 0, 180, 0.4)';\n                    ctx.beginPath();\n                    ctx.ellipse(45, 35, 20, 10, 0.5, 0, Math.PI * 2);\n                    ctx.fill();\n                    // Stars\n                    for (let i = 0; i < 30; i++) {\n                        const sx = 10 + Math.random() * 60;\n                        const sy = 10 + Math.random() * 60;\n                        const dist = Math.sqrt((sx - 40) ** 2 + (sy - 40) ** 2);\n                        if (dist < 35) {\n                            ctx.fillStyle = ['#ffffff', '#aaccff', '#ffaadd'][Math.floor(Math.random() * 3)];\n                            ctx.beginPath();\n                            ctx.arc(sx, sy, 0.5 + Math.random() * 1.5, 0, Math.PI * 2);\n                            ctx.fill();\n                        }\n                    }\n                    break;\n                    \n                case 'blackhole':\n                    // Event horizon (black center)\n                    const bhGrad = ctx.createRadialGradient(40, 40, 0, 40, 40, 40);\n                    bhGrad.addColorStop(0, '#000000');\n                    bhGrad.addColorStop(0.6, '#000000');\n                    bhGrad.addColorStop(0.8, '#220044');\n                    bhGrad.addColorStop(1, '#440066');\n                    ctx.beginPath();\n                    ctx.arc(40, 40, 36, 0, Math.PI * 2);\n                    ctx.fillStyle = bhGrad;\n                    ctx.fill();\n                    // Accretion disk\n                    ctx.strokeStyle = '#ff6600';\n                    ctx.lineWidth = 4;\n                    ctx.beginPath();\n                    ctx.ellipse(40, 40, 32, 10, 0, 0, Math.PI * 2);\n                    ctx.stroke();\n                    ctx.strokeStyle = '#ffaa44';\n                    ctx.lineWidth = 2;\n                    ctx.beginPath();\n                    ctx.ellipse(40, 40, 26, 8, 0, 0, Math.PI * 2);\n                    ctx.stroke();\n                    break;\n            }\n            \n            return canvas;\n        }\n        \n        // Initialize skin selector\n        function initSkinSelector() {\n            skinCarousel.innerHTML = '';\n            \n            PLAYER_SKINS.forEach(skin => {\n                const skinOption = document.createElement('div');\n                skinOption.className = 'skin-option' + (skin.id === selectedPlayerSkin ? ' selected' : '');\n                skinOption.dataset.skinId = skin.id;\n                \n                // Add preview canvas\n                const preview = generateSkinPreview(skin.id);\n                skinOption.appendChild(preview);\n                \n                // Add name label\n                const nameLabel = document.createElement('span');\n                nameLabel.className = 'skin-name';\n                nameLabel.textContent = skin.emoji + ' ' + skin.name;\n                skinOption.appendChild(nameLabel);\n                \n                // Click handler\n                skinOption.addEventListener('click', () => selectSkin(skin.id));\n                skinOption.addEventListener('touchend', (e) => {\n                    e.preventDefault();\n                    selectSkin(skin.id);\n                });\n                \n                skinCarousel.appendChild(skinOption);\n            });\n        }\n        \n        // Handle skin selection\n        function selectSkin(skinId) {\n            selectedPlayerSkin = skinId;\n            \n            // Update UI\n            document.querySelectorAll('.skin-option').forEach(opt => {\n                opt.classList.toggle('selected', opt.dataset.skinId === skinId);\n            });\n            \n            const skinData = PLAYER_SKINS.find(s => s.id === skinId);\n            if (skinData) {\n                selectedSkinDisplay.textContent = `Selected: ${skinData.emoji} ${skinData.name}`;\n            }\n        }\n        \n        // Initialize skin selector on load\n        initSkinSelector();\n\n        function updateHUD() {\n            // Show decay indicator when mass is above decay threshold\n            if (playerMass > DECAY_THRESHOLD) {\n                const decayPercent = ((playerMass * DECAY_RATE / DECAY_INTERVAL)).toFixed(1);\n                massDisplay.textContent = `MASS: ${Math.floor(playerMass)} \ud83d\udcc9`;\n                massDisplay.style.color = '#ff9944'; // Orange tint to indicate decay\n            } else {\n                massDisplay.textContent = 'MASS: ' + Math.floor(playerMass);\n                massDisplay.style.color = '#fff';\n            }\n            \n            // Show giant status when above slime-eating threshold\n            if (playerMass >= GIANT_THRESHOLD) {\n                massDisplay.textContent = `\u26a1 GIANT: ${Math.floor(playerMass)} \ud83d\udcc9`;\n                massDisplay.style.color = '#ff4488'; // Pink for giant status\n            }\n            \n            skullsDisplay.textContent = '\ud83d\udc80 ' + playerSkulls;\n        }\n\n        function updateLeaderboard() {\n            const entries = [];\n            entries.push({ name: 'You', mass: playerMass });\n            aiBots.forEach(b => entries.push({ name: b.name, mass: b.mass }));\n            entries.sort((a, b) => b.mass - a.mass);\n            lbEntries.innerHTML = entries.slice(0, 5).map((e, i) =>\n                `<div class=\"lb-entry\"${e.name === 'You' ? ' style=\"color:#76ff03;font-weight:700;\"' : ''}>${i+1}. ${e.name}: ${Math.floor(e.mass)}</div>`\n            ).join('');\n        }\n\n        // === COMBO / JUICE HELPERS ===\n        function getComboMult() {\n            return 1 + Math.min(4, Math.floor(comboCount / 5) * 0.5);\n        }\n\n        function registerEat(n = 1) {\n            comboCount += n;\n            comboTimer = COMBO_WINDOW;\n        }\n\n        function addShake(amount) {\n            shakeIntensity = Math.min(2.5, shakeIntensity + amount);\n        }\n\n        function showFloatText(worldX, worldY, worldZ, text, color) {\n            if (!floatLayer || !camera) return;\n            const v = new THREE.Vector3(worldX, worldY, worldZ).project(camera);\n            if (v.z > 1) return;\n            const el = document.createElement('div');\n            el.className = 'float-text';\n            el.textContent = text;\n            el.style.color = color || '#fff';\n            el.style.left = ((v.x * 0.5 + 0.5) * 100) + '%';\n            el.style.top = ((-v.y * 0.5 + 0.5) * 100) + '%';\n            floatLayer.appendChild(el);\n            setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 950);\n        }\n\n        function addKillFeed(html, color) {\n            if (!killFeedEl) return;\n            const el = document.createElement('div');\n            el.className = 'kill-feed-item';\n            el.innerHTML = html;\n            if (color) el.style.color = color;\n            killFeedEl.appendChild(el);\n            while (killFeedEl.children.length > 4) killFeedEl.removeChild(killFeedEl.firstChild);\n            setTimeout(() => {\n                el.style.transition = 'opacity 400ms linear';\n                el.style.opacity = '0';\n                setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 420);\n            }, 2600);\n        }\n\n        function tryDash() {\n            if (gameState !== 'playing') return;\n            if (dashCooldown > 0 || dashTimer > 0) return;\n            if (playerMass <= config.player.startingMass) return;\n            dashTimer = DASH_DURATION;\n            dashCooldown = DASH_COOLDOWN;\n            playerMass = Math.max(config.player.startingMass, playerMass * (1 - DASH_MASS_COST));\n            const r = getRadius(playerMass);\n            let vm = Math.sqrt(playerVelocity.x ** 2 + playerVelocity.z ** 2);\n            let dx, dz;\n            if (vm > 0.2) {\n                dx = playerVelocity.x / vm;\n                dz = playerVelocity.z / vm;\n            } else {\n                dx = -Math.sin(cameraState.facingAngle);\n                dz = -Math.cos(cameraState.facingAngle);\n            }\n            playerVelocity.x += dx * 9;\n            playerVelocity.z += dz * 9;\n            createSplatterParticles(playerPos.x - dx * r, r * 0.7, playerPos.z - dz * r, 0x9c6bff, 12);\n            addShake(0.25);\n            triggerJiggle(0.2);\n        }\n\n        function updateJuiceHUD() {\n            if (comboDisplay) {\n                if (comboCount >= 5) {\n                    comboDisplay.classList.add('active');\n                    comboDisplay.textContent = `\ud83d\udd25 x${getComboMult().toFixed(1)} COMBO (${comboCount})`;\n                } else {\n                    comboDisplay.classList.remove('active');\n                }\n            }\n            if (dashBtn) {\n                const cooling = dashCooldown > 0;\n                dashBtn.classList.toggle('cooling', cooling);\n                if (dashCoolFill) dashCoolFill.style.height = cooling ? ((dashCooldown / DASH_COOLDOWN) * 100) + '%' : '0%';\n            }\n            if (dangerVignette) {\n                let danger = 0;\n                const pr = getRadius(playerMass);\n                for (const bot of aiBots) {\n                    if (bot.mass > playerMass * 1.1) {\n                        const d = Math.sqrt((bot.x - playerPos.x) ** 2 + (bot.z - playerPos.z) ** 2) - pr - getRadius(bot.mass);\n                        const near = Math.max(0, 1 - d / 25);\n                        if (near > danger) danger = near;\n                    }\n                }\n                if (spawnInvincible) danger *= 0.3;\n                dangerVignette.style.opacity = (danger * 0.85).toFixed(2);\n            }\n            if (rankDisplay) {\n                let rank = 1;\n                for (const bot of aiBots) if (bot.mass > playerMass) rank++;\n                rankDisplay.textContent = 'RANK #' + rank + ' / ' + (aiBots.length + 1);\n            }\n        }\n\n        // === MINIMAP & RADAR SYSTEM ===\n        const minimapCanvas = document.getElementById('minimap-canvas');\n        const minimapCtx = minimapCanvas.getContext('2d');\n        const MINIMAP_SIZE = 140;\n        const MINIMAP_CENTER = MINIMAP_SIZE / 2;\n        \n        // Player movement direction tracking\n        let playerMoveDir = { x: 0, z: 1 }; // Default facing forward\n        let lastPlayerPos = { x: 0, z: 0 };\n        \n        // POI ripple system for high-value areas\n        const poiRipples = [];\n        let poiScanTimer = 0;\n        const POI_SCAN_INTERVAL = 3; // Scan for POIs every 3 seconds\n        \n        function findSlimeClusters() {\n            // Find areas with high slime concentration\n            const gridSize = 20; // Divide arena into grid cells\n            const arenaSize = config.arena.size;\n            const cellSize = arenaSize / gridSize;\n            const densityGrid = {};\n            \n            for (const slime of slimes) {\n                if (!slime.active) continue;\n                const cellX = Math.floor((slime.x + arenaSize/2) / cellSize);\n                const cellZ = Math.floor((slime.z + arenaSize/2) / cellSize);\n                const key = `${cellX},${cellZ}`;\n                densityGrid[key] = (densityGrid[key] || 0) + 1;\n            }\n            \n            // Find cells with high density\n            const clusters = [];\n            for (const [key, count] of Object.entries(densityGrid)) {\n                if (count >= 15) { // Threshold for \"high-value\" cluster\n                    const [cx, cz] = key.split(',').map(Number);\n                    clusters.push({\n                        x: (cx + 0.5) * cellSize - arenaSize/2,\n                        z: (cz + 0.5) * cellSize - arenaSize/2,\n                        intensity: Math.min(count / 25, 1)\n                    });\n                }\n            }\n            return clusters;\n        }\n        \n        function updatePOIs(dt) {\n            poiScanTimer -= dt;\n            if (poiScanTimer <= 0) {\n                poiScanTimer = POI_SCAN_INTERVAL;\n                const clusters = findSlimeClusters();\n                \n                // Add ripples for new clusters (limit to 3 at a time)\n                for (const cluster of clusters.slice(0, 3)) {\n                    // Check if we already have a ripple near this location\n                    const existing = poiRipples.find(r => {\n                        const dx = r.x - cluster.x;\n                        const dz = r.z - cluster.z;\n                        return Math.sqrt(dx*dx + dz*dz) < 20;\n                    });\n                    \n                    if (!existing) {\n                        poiRipples.push({\n                            x: cluster.x,\n                            z: cluster.z,\n                            radius: 0,\n                            maxRadius: 1,\n                            alpha: 0.8,\n                            intensity: cluster.intensity\n                        });\n                    }\n                }\n            }\n            \n            // Update existing ripples\n            for (let i = poiRipples.length - 1; i >= 0; i--) {\n                const ripple = poiRipples[i];\n                ripple.radius += dt * 0.5;\n                ripple.alpha -= dt * 0.25;\n                \n                if (ripple.alpha <= 0 || ripple.radius > ripple.maxRadius) {\n                    poiRipples.splice(i, 1);\n                }\n            }\n        }\n        \n        function calculateMinimapZoom(mass) {\n            // Dynamic zoom: smaller player = tighter radius, larger = wider view\n            // At mass 10, show ~40 units radius. At mass 500+, show ~150 units radius\n            const baseRadius = 40;\n            const maxRadius = Math.min(150, config.arena.size / 2);\n            const massProgress = Math.min(1, (mass - 10) / 490);\n            // Ease-out curve for smooth transition\n            const easedProgress = 1 - Math.pow(1 - massProgress, 2);\n            return baseRadius + (maxRadius - baseRadius) * easedProgress;\n        }\n        \n        function getThreatColor(botMass, playerMass) {\n            // 10% threshold for eating\n            if (botMass > playerMass * 1.1) {\n                return '#ff4444'; // RED - Dangerous (can eat you)\n            } else if (playerMass > botMass * 1.1) {\n                return '#44ff44'; // GREEN - Edible (you can eat them)\n            } else {\n                return '#ffff44'; // YELLOW - Neutral (cannot eat each other)\n            }\n        }\n        \n        function drawMinimap(timestamp) {\n            const ctx = minimapCtx;\n            const arenaSize = config.arena.size;\n            const viewRadius = calculateMinimapZoom(playerMass);\n            const scale = (MINIMAP_SIZE / 2 - 8) / viewRadius; // Pixels per unit, with padding\n            \n            // Clear with circular clip\n            ctx.save();\n            ctx.beginPath();\n            ctx.arc(MINIMAP_CENTER, MINIMAP_CENTER, MINIMAP_SIZE / 2 - 2, 0, Math.PI * 2);\n            ctx.clip();\n            \n            // Background with subtle grid pattern\n            const gradient = ctx.createRadialGradient(\n                MINIMAP_CENTER, MINIMAP_CENTER, 0,\n                MINIMAP_CENTER, MINIMAP_CENTER, MINIMAP_SIZE / 2\n            );\n            gradient.addColorStop(0, 'rgba(20, 30, 60, 0.9)');\n            gradient.addColorStop(1, 'rgba(10, 15, 40, 0.95)');\n            ctx.fillStyle = gradient;\n            ctx.fillRect(0, 0, MINIMAP_SIZE, MINIMAP_SIZE);\n            \n            // Draw arena boundary indicator\n            const arenaBoundaryOnMap = (arenaSize / 2) * scale;\n            ctx.strokeStyle = 'rgba(0, 229, 255, 0.3)';\n            ctx.lineWidth = 1;\n            \n            // Draw grid lines (subtle)\n            ctx.strokeStyle = 'rgba(0, 229, 255, 0.1)';\n            const gridSpacing = 20 * scale; // Grid every 20 units\n            for (let i = -10; i <= 10; i++) {\n                const offset = i * gridSpacing;\n                // Vertical lines\n                ctx.beginPath();\n                ctx.moveTo(MINIMAP_CENTER + offset, 0);\n                ctx.lineTo(MINIMAP_CENTER + offset, MINIMAP_SIZE);\n                ctx.stroke();\n                // Horizontal lines\n                ctx.beginPath();\n                ctx.moveTo(0, MINIMAP_CENTER + offset);\n                ctx.lineTo(MINIMAP_SIZE, MINIMAP_CENTER + offset);\n                ctx.stroke();\n            }\n            \n            // Draw POI ripples (blue pings)\n            for (const ripple of poiRipples) {\n                const relX = (ripple.x - playerPos.x) * scale;\n                const relZ = (ripple.z - playerPos.z) * scale;\n                const mapX = MINIMAP_CENTER + relX;\n                const mapY = MINIMAP_CENTER + relZ;\n                \n                // Only draw if within view\n                if (Math.abs(relX) < MINIMAP_SIZE/2 && Math.abs(relZ) < MINIMAP_SIZE/2) {\n                    const rippleRadius = ripple.radius * 30;\n                    ctx.beginPath();\n                    ctx.arc(mapX, mapY, rippleRadius, 0, Math.PI * 2);\n                    ctx.strokeStyle = `rgba(68, 170, 255, ${ripple.alpha * ripple.intensity})`;\n                    ctx.lineWidth = 2;\n                    ctx.stroke();\n                    \n                    // Inner glow\n                    ctx.beginPath();\n                    ctx.arc(mapX, mapY, rippleRadius * 0.5, 0, Math.PI * 2);\n                    ctx.fillStyle = `rgba(68, 170, 255, ${ripple.alpha * 0.3 * ripple.intensity})`;\n                    ctx.fill();\n                }\n            }\n            \n            // Draw slimes as tiny dots (optional, for density visualization)\n            ctx.fillStyle = 'rgba(255, 255, 100, 0.3)';\n            for (let i = 0; i < slimes.length; i += 5) { // Draw every 5th slime for performance\n                if (!slimes[i].active) continue;\n                const relX = (slimes[i].x - playerPos.x) * scale;\n                const relZ = (slimes[i].z - playerPos.z) * scale;\n                if (Math.abs(relX) < MINIMAP_SIZE/2 && Math.abs(relZ) < MINIMAP_SIZE/2) {\n                    ctx.beginPath();\n                    ctx.arc(MINIMAP_CENTER + relX, MINIMAP_CENTER + relZ, 1.5, 0, Math.PI * 2);\n                    ctx.fill();\n                }\n            }\n            \n            // Draw enemy blips with threat colors\n            for (const bot of aiBots) {\n                const relX = (bot.x - playerPos.x) * scale;\n                const relZ = (bot.z - playerPos.z) * scale;\n                const mapX = MINIMAP_CENTER + relX;\n                const mapY = MINIMAP_CENTER + relZ;\n                \n                // Only draw if within minimap view\n                const distFromCenter = Math.sqrt(relX * relX + relZ * relZ);\n                if (distFromCenter < MINIMAP_SIZE / 2 - 5) {\n                    const threatColor = getThreatColor(bot.mass, playerMass);\n                    const blipSize = Math.max(4, Math.min(8, bot.mass / 30)); // Size based on mass\n                    \n                    // Outer glow\n                    ctx.beginPath();\n                    ctx.arc(mapX, mapY, blipSize + 2, 0, Math.PI * 2);\n                    ctx.fillStyle = threatColor.replace(')', ', 0.3)').replace('rgb', 'rgba');\n                    ctx.fill();\n                    \n                    // Main blip\n                    ctx.beginPath();\n                    ctx.arc(mapX, mapY, blipSize, 0, Math.PI * 2);\n                    ctx.fillStyle = threatColor;\n                    ctx.fill();\n                    \n                    // Inner highlight\n                    ctx.beginPath();\n                    ctx.arc(mapX - blipSize * 0.2, mapY - blipSize * 0.2, blipSize * 0.4, 0, Math.PI * 2);\n                    ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';\n                    ctx.fill();\n                }\n            }\n            \n            // Draw arena boundary (if visible)\n            const halfArena = arenaSize / 2;\n            ctx.strokeStyle = 'rgba(255, 100, 100, 0.5)';\n            ctx.lineWidth = 2;\n            const boundaryPoints = [\n                { x: -halfArena, z: -halfArena },\n                { x: halfArena, z: -halfArena },\n                { x: halfArena, z: halfArena },\n                { x: -halfArena, z: halfArena }\n            ];\n            ctx.beginPath();\n            let firstVisible = true;\n            for (let i = 0; i <= 4; i++) {\n                const point = boundaryPoints[i % 4];\n                const relX = (point.x - playerPos.x) * scale;\n                const relZ = (point.z - playerPos.z) * scale;\n                const mapX = MINIMAP_CENTER + relX;\n                const mapY = MINIMAP_CENTER + relZ;\n                if (firstVisible) {\n                    ctx.moveTo(mapX, mapY);\n                    firstVisible = false;\n                } else {\n                    ctx.lineTo(mapX, mapY);\n                }\n            }\n            ctx.closePath();\n            ctx.stroke();\n            \n            // Draw player arrow in center (pulsing white)\n            const pulse = 0.7 + Math.sin(timestamp * 0.006) * 0.3;\n            \n            // Calculate movement direction\n            const dx = playerPos.x - lastPlayerPos.x;\n            const dz = playerPos.z - lastPlayerPos.z;\n            const moveMag = Math.sqrt(dx * dx + dz * dz);\n            if (moveMag > 0.01) {\n                // Smoothly interpolate direction\n                playerMoveDir.x += (dx / moveMag - playerMoveDir.x) * 0.2;\n                playerMoveDir.z += (dz / moveMag - playerMoveDir.z) * 0.2;\n                // Normalize\n                const dirMag = Math.sqrt(playerMoveDir.x ** 2 + playerMoveDir.z ** 2);\n                if (dirMag > 0) {\n                    playerMoveDir.x /= dirMag;\n                    playerMoveDir.z /= dirMag;\n                }\n            }\n            lastPlayerPos.x = playerPos.x;\n            lastPlayerPos.z = playerPos.z;\n            \n            // Draw player arrow\n            const arrowSize = 10;\n            const angle = Math.atan2(playerMoveDir.z, playerMoveDir.x);\n            \n            ctx.save();\n            ctx.translate(MINIMAP_CENTER, MINIMAP_CENTER);\n            ctx.rotate(angle + Math.PI / 2); // Rotate to point in movement direction\n            \n            // Glow effect\n            ctx.shadowColor = 'rgba(255, 255, 255, 0.8)';\n            ctx.shadowBlur = 8 * pulse;\n            \n            // Arrow shape\n            ctx.beginPath();\n            ctx.moveTo(0, -arrowSize); // Tip\n            ctx.lineTo(-arrowSize * 0.6, arrowSize * 0.5);\n            ctx.lineTo(0, arrowSize * 0.2);\n            ctx.lineTo(arrowSize * 0.6, arrowSize * 0.5);\n            ctx.closePath();\n            \n            ctx.fillStyle = `rgba(255, 255, 255, ${pulse})`;\n            ctx.fill();\n            ctx.strokeStyle = 'rgba(0, 229, 255, 0.8)';\n            ctx.lineWidth = 1.5;\n            ctx.stroke();\n            \n            ctx.restore();\n            \n            // Draw compass indicators at edges\n            ctx.font = '10px Inter';\n            ctx.fillStyle = 'rgba(0, 229, 255, 0.6)';\n            ctx.textAlign = 'center';\n            ctx.fillText('N', MINIMAP_CENTER, 12);\n            ctx.fillText('S', MINIMAP_CENTER, MINIMAP_SIZE - 4);\n            ctx.fillText('W', 8, MINIMAP_CENTER + 4);\n            ctx.fillText('E', MINIMAP_SIZE - 8, MINIMAP_CENTER + 4);\n            \n            ctx.restore();\n            \n            // Draw neon border ring (outside clip)\n            ctx.strokeStyle = '#00e5ff';\n            ctx.lineWidth = 3;\n            ctx.beginPath();\n            ctx.arc(MINIMAP_CENTER, MINIMAP_CENTER, MINIMAP_SIZE / 2 - 1.5, 0, Math.PI * 2);\n            ctx.stroke();\n            \n            // Animated glow ring\n            const glowPulse = 0.3 + Math.sin(timestamp * 0.003) * 0.15;\n            ctx.strokeStyle = `rgba(0, 229, 255, ${glowPulse})`;\n            ctx.lineWidth = 6;\n            ctx.beginPath();\n            ctx.arc(MINIMAP_CENTER, MINIMAP_CENTER, MINIMAP_SIZE / 2 + 1, 0, Math.PI * 2);\n            ctx.stroke();\n        }\n\n        function startGame() {\n            gameState = 'playing';\n            mainMenu.classList.add('hidden');\n            hud.classList.add('visible');\n            deathScreen.classList.remove('visible');\n            playerMass = config.player.startingMass;\n\n            // Reset session stats & juice state\n            sessionKills = 0;\n            sessionPeakMass = playerMass;\n            sessionStartTime = performance.now();\n            comboCount = 0;\n            comboTimer = 0;\n            dashTimer = 0;\n            dashCooldown = 0;\n            shakeIntensity = 0;\n            if (killFeedEl) killFeedEl.innerHTML = '';\n            if (floatLayer) floatLayer.innerHTML = '';\n            if (dangerVignette) dangerVignette.style.opacity = '0';\n            if (comboDisplay) comboDisplay.classList.remove('active');\n            if (dashBtn) dashBtn.classList.remove('cooling');\n            if (dashCoolFill) dashCoolFill.style.height = '0%';\n\n            // Reset death animation state\n            deathAnimation.active = false;\n            deathAnimation.progress = 0;\n            deathAnimation.killerBot = null;\n            \n            // Safe-zone spawning: find location away from large bots\n            const safeSpawn = findSafeSpawnLocation();\n            playerPos.x = safeSpawn.x;\n            playerPos.z = safeSpawn.z;\n            \n            // Activate spawn invincibility\n            spawnInvincible = true;\n            spawnInvincibilityTimer = SPAWN_INVINCIBILITY_DURATION;\n            \n            if (playerMesh) scene.remove(playerMesh);\n            createPlayerMesh();\n            playerMesh.visible = true; // Ensure visible after respawn\n            \n            // Create direction arrow\n            createDirectionArrow();\n            \n            // Create protective shield\n            createShieldMesh();\n            \n            updateHUD();\n\n            // Start music\n            initAudio();\n            if (!musicSource && audioBuffers['background_music_upbeat']) {\n                musicSource = playSound('background_music_upbeat', true);\n            }\n            if (musicGain) musicGain.gain.value = (config.audio?.musicVolume ?? 50) / 100;\n        }\n\n        function respawn() {\n            startGame();\n        }\n\n        if (dashBtn) {\n            dashBtn.addEventListener('click', tryDash);\n            dashBtn.addEventListener('touchstart', (e) => { e.preventDefault(); tryDash(); }, { passive: false });\n        }\n\n        document.getElementById('play-btn').addEventListener('click', startGame);\n        document.getElementById('play-btn').addEventListener('touchend', (e) => { e.preventDefault(); startGame(); });\n        document.getElementById('respawn-btn').addEventListener('click', respawn);\n        document.getElementById('respawn-btn').addEventListener('touchend', (e) => { e.preventDefault(); respawn(); });\n\n        // Resume audio on interaction\n        document.addEventListener('touchstart', () => { if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); }, { once: true });\n        document.addEventListener('click', () => { if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); }, { once: true });\n\n        // Game loop\n        let lastTime = performance.now();\n\n        // === THIRD PERSON CAMERA SYSTEM ===\n        const cameraState = {\n            currentDist: 30,\n            currentHeight: 15,\n            targetDist: 30,\n            targetHeight: 15,\n            currentX: 0,\n            currentY: 40,\n            currentZ: 60,\n            // Camera follows behind player's movement direction\n            facingAngle: 0, // Angle the camera is facing (radians)\n            targetFacingAngle: 0\n        };\n        \n        // Third Person Camera constants\n        const CAMERA_LERP_SPEED = 2.5; // Faster for responsive third person\n        const CAMERA_ROTATION_SPEED = 3.0; // How fast camera rotates to follow direction\n        const CAMERA_HEIGHT_BASE = 8; // Base height above player\n        const CAMERA_HEIGHT_SCALE = 0.4; // Additional height per radius unit\n        const CAMERA_DIST_BASE = 12; // Base distance behind player\n        const CAMERA_DIST_SCALE = 2.5; // Additional distance per radius unit\n        const CAMERA_LOOK_AHEAD = 0.3; // How much to look ahead of player\n        \n        function calculateThirdPersonCamera(radius, mass) {\n            // Calculate distance and height based on player size\n            const dist = CAMERA_DIST_BASE + radius * CAMERA_DIST_SCALE;\n            const height = CAMERA_HEIGHT_BASE + radius * CAMERA_HEIGHT_SCALE;\n            return { dist, height };\n        }\n        \n        \n\n        function gameLoop(timestamp) {\n            let dt = (timestamp - lastTime) / 1000;\n            lastTime = timestamp;\n            if (dt > 0.11) dt = 0.11;\n\n            if (gameState === 'playing') {\n                // Combo decay & dash timers\n                if (comboTimer > 0) {\n                    comboTimer -= dt;\n                    if (comboTimer <= 0) comboCount = 0;\n                }\n                if (dashTimer > 0) dashTimer -= dt;\n                if (dashCooldown > 0) dashCooldown -= dt;\n\n                // Keyboard input (in camera-relative space)\n                let kx = 0, kz = 0;\n                if (keys['w'] || keys['arrowup']) kz -= 1;    // Forward in camera space\n                if (keys['s'] || keys['arrowdown']) kz += 1;  // Backward in camera space\n                if (keys['a'] || keys['arrowleft']) kx -= 1;  // Left in camera space\n                if (keys['d'] || keys['arrowright']) kx += 1; // Right in camera space\n                \n                // Transform keyboard input from camera-relative to world-space\n                // Camera facing angle determines what direction is \"forward\"\n                const camAngle = cameraState.facingAngle;\n                const sinA = Math.sin(camAngle);\n                const cosA = Math.cos(camAngle);\n                \n                // Forward direction (where camera looks): (-sin(angle), -cos(angle))\n                // Right direction: (cos(angle), -sin(angle))\n                // kz: negative = forward, positive = backward\n                // kx: negative = left, positive = right\n                const keyWorldX = kz * sinA + kx * cosA;\n                const keyWorldZ = kz * cosA - kx * sinA;\n                \n                // Transform joystick input from camera-relative to world-space as well\n                const joyWorldX = inputDir.z * sinA + inputDir.x * cosA;\n                const joyWorldZ = inputDir.z * cosA - inputDir.x * sinA;\n                \n                // Combine keyboard and joystick input\n                let moveX = keyWorldX + joyWorldX;\n                let moveZ = keyWorldZ + joyWorldZ;\n                const moveMag = Math.sqrt(moveX * moveX + moveZ * moveZ);\n                if (moveMag > 1) { moveX /= moveMag; moveZ /= moveMag; }\n\n                const maxSpeed = (8 + 2 / (1 + playerMass * 0.01)) * config.player.speedMultiplier * (dashTimer > 0 ? DASH_SPEED_MULT : 1);\n                \n                // Apply acceleration/deceleration for momentum\n                if (moveMag > 0.1) {\n                    // Accelerate toward input direction\n                    playerVelocity.x += moveX * ACCELERATION * dt;\n                    playerVelocity.z += moveZ * ACCELERATION * dt;\n                } else {\n                    // Apply friction when no input (smooth deceleration)\n                    const frictionFactor = Math.exp(-FRICTION * dt);\n                    playerVelocity.x *= frictionFactor;\n                    playerVelocity.z *= frictionFactor;\n                    \n                    // Stop completely if very slow\n                    const velMag = Math.sqrt(playerVelocity.x ** 2 + playerVelocity.z ** 2);\n                    if (velMag < 0.1) {\n                        playerVelocity.x = 0;\n                        playerVelocity.z = 0;\n                    }\n                }\n                \n                // Clamp velocity to max speed\n                const currentVelMag = Math.sqrt(playerVelocity.x ** 2 + playerVelocity.z ** 2);\n                if (currentVelMag > maxSpeed) {\n                    playerVelocity.x = (playerVelocity.x / currentVelMag) * maxSpeed;\n                    playerVelocity.z = (playerVelocity.z / currentVelMag) * maxSpeed;\n                }\n                \n                // Apply velocity to position\n                playerPos.x += playerVelocity.x * dt;\n                playerPos.z += playerVelocity.z * dt;\n\n                // Clamp to arena\n                const half = config.arena.size / 2 - 2;\n                playerPos.x = Math.max(-half, Math.min(half, playerPos.x));\n                playerPos.z = Math.max(-half, Math.min(half, playerPos.z));\n\n                // Update player mesh with jiggle effect\n                const playerRadius = getRadius(playerMass);\n                playerMesh.position.set(playerPos.x, playerRadius * playerJiggle.scaleY, playerPos.z);\n                \n                // === PHYSICS-BASED ROLLING ===\n                // Calculate angular velocity: \u03c9 = v / r (linear speed / radius)\n                // Movement on Z axis -> rotation around X axis\n                // Movement on X axis -> rotation around Z axis (inverted)\n                const linearSpeed = Math.sqrt(playerVelocity.x ** 2 + playerVelocity.z ** 2);\n                if (linearSpeed > 0.05) {\n                    const angularSpeed = linearSpeed / playerRadius;\n                    \n                    // Accumulate rotation based on movement direction\n                    // Moving +Z means rolling forward (positive X rotation)\n                    // Moving +X means rolling right (negative Z rotation)\n                    playerRotation.x += (playerVelocity.z / linearSpeed) * angularSpeed * dt;\n                    playerRotation.z -= (playerVelocity.x / linearSpeed) * angularSpeed * dt;\n                }\n                \n                // Apply rotation using quaternions to avoid gimbal lock\n                const rotationQuatX = new THREE.Quaternion();\n                const rotationQuatZ = new THREE.Quaternion();\n                rotationQuatX.setFromAxisAngle(new THREE.Vector3(1, 0, 0), playerRotation.x);\n                rotationQuatZ.setFromAxisAngle(new THREE.Vector3(0, 0, 1), playerRotation.z);\n                \n                // Combine rotations\n                const finalQuat = new THREE.Quaternion();\n                finalQuat.multiplyQuaternions(rotationQuatX, rotationQuatZ);\n                playerMesh.quaternion.copy(finalQuat);\n                \n                // Smooth growth interpolation\n                const targetScaleRatio = playerRadius;\n                const currentBaseScale = playerMesh.userData.baseScale || targetScaleRatio;\n                const newBaseScale = currentBaseScale + (targetScaleRatio - currentBaseScale) * 5 * dt;\n                playerMesh.userData.baseScale = newBaseScale;\n                \n                // Apply jiggle on top of base scale\n                updateJiggle(dt);\n                playerMesh.scale.set(\n                    newBaseScale * playerJiggle.scaleX,\n                    newBaseScale * playerJiggle.scaleY,\n                    newBaseScale * playerJiggle.scaleZ\n                );\n\n                // === PASSIVE MASS DECAY (Anti-Infinite Growth) ===\n                // Decay only starts above 500 mass, losing 1% every 3 seconds\n                // The larger you are, the more absolute mass you lose\n                if (playerMass > DECAY_THRESHOLD) {\n                    const decayAmount = playerMass * DECAY_RATE * (dt / DECAY_INTERVAL);\n                    playerMass = Math.max(DECAY_THRESHOLD, playerMass - decayAmount);\n                }\n                \n                // === TIERED FOOD IGNORANCE (Culling for Giants) ===\n                // Giants (5000+ mass) cannot eat tiny Tier 1 slimes\n                const canEatTinySlimes = playerMass < GIANT_THRESHOLD;\n                \n                // Eat slimes with magnetic attraction\n                const attractionDist = playerRadius + ATTRACTION_RADIUS;\n                const eatDist = playerRadius;\n                const slimeSizeBase = 0.5 * config.slimes.sizeMultiplier;\n                \n                // === BATCH COLLISION PROCESSING (The Sweep) ===\n                // For massive blobs, collect all slimes at once to prevent lag\n                const useBatchProcessing = playerMass > 200;\n                let batchEatenSlimes = [];\n                let batchMassGain = 0;\n                \n                for (let i = 0; i < slimes.length; i++) {\n                    if (!slimes[i].active) continue;\n                    \n                    // Skip tiny slimes if player is a giant\n                    if (!canEatTinySlimes) {\n                        // Slimes pass harmlessly underneath giant players\n                        attractedSlimes.delete(i);\n                        continue;\n                    }\n                    \n                    const dx = playerPos.x - slimes[i].x;\n                    const dz = playerPos.z - slimes[i].z;\n                    const dist = Math.sqrt(dx*dx + dz*dz);\n                    \n                    // Check if slime is in attraction range\n                    if (dist < attractionDist && dist > eatDist) {\n                        // Start or continue attraction\n                        if (!attractedSlimes.has(i)) {\n                            attractedSlimes.set(i, {\n                                progress: 0,\n                                startX: slimes[i].x,\n                                startZ: slimes[i].z,\n                                originalY: slimeMeshes[i].position.y\n                            });\n                        }\n                        \n                        const attr = attractedSlimes.get(i);\n                        attr.progress += ATTRACTION_SPEED * dt;\n                        \n                        // Ease-in acceleration (gets faster as it approaches)\n                        const t = Math.min(attr.progress, 1);\n                        const easeT = t * t * t; // Cubic ease-in\n                        \n                        // Move toward player\n                        const targetX = playerPos.x;\n                        const targetZ = playerPos.z;\n                        slimes[i].x = attr.startX + (targetX - attr.startX) * easeT;\n                        slimes[i].z = attr.startZ + (targetZ - attr.startZ) * easeT;\n                        \n                        // Stretch toward player (teardrop effect)\n                        const stretchFactor = 1 + easeT * 1.5;\n                        const squishFactor = 1 / Math.sqrt(stretchFactor);\n                        \n                        // Calculate direction to player for stretching\n                        const dirX = dx / (dist + 0.01);\n                        const dirZ = dz / (dist + 0.01);\n                        \n                        slimeMeshes[i].scale.set(\n                            squishFactor * (1 - Math.abs(dirX) * 0.3 * easeT) + Math.abs(dirX) * stretchFactor * 0.3 * easeT,\n                            squishFactor,\n                            squishFactor * (1 - Math.abs(dirZ) * 0.3 * easeT) + Math.abs(dirZ) * stretchFactor * 0.3 * easeT\n                        );\n                        \n                        slimeMeshes[i].position.set(slimes[i].x, attr.originalY * (1 - easeT * 0.5), slimes[i].z);\n                    }\n                    \n                    // Check if slime should be consumed\n                    const currentDx = playerPos.x - slimes[i].x;\n                    const currentDz = playerPos.z - slimes[i].z;\n                    const currentDist = Math.sqrt(currentDx*currentDx + currentDz*currentDz);\n                    \n                    if (currentDist < eatDist || (attractedSlimes.has(i) && attractedSlimes.get(i).progress >= 1)) {\n                        if (useBatchProcessing) {\n                            // Batch mode: collect slime for batch processing\n                            batchEatenSlimes.push(i);\n                            batchMassGain += config.player.growthRate;\n                        } else {\n                            // Individual mode: process immediately\n                            // Get slime color for particles\n                            const slimeColor = slimeMeshes[i].material.color.getHex();\n                            \n                            // Calculate contact point on player surface\n                            const contactX = playerPos.x - (currentDx / (currentDist + 0.01)) * playerRadius;\n                            const contactZ = playerPos.z - (currentDz / (currentDist + 0.01)) * playerRadius;\n                            const contactY = playerRadius;\n                            \n                            // Create splatter particles\n                            createSplatterParticles(contactX, contactY, contactZ, slimeColor, 6);\n                            \n                            // Trigger player jiggle\n                            triggerJiggle(0.1);\n                            \n                            slimes[i].active = false;\n                            slimeMeshes[i].visible = false;\n                            attractedSlimes.delete(i);\n                            registerEat(1);\n                            playerMass += config.player.growthRate * getComboMult();\n                            \n                            // Pop sound removed\n                            \n                            // Respawn slime elsewhere\n                            const slimeIdx = i;\n                            setTimeout(() => {\n                                const h = config.arena.size / 2 - 5;\n                                slimes[slimeIdx].x = (Math.random() - 0.5) * 2 * h;\n                                slimes[slimeIdx].z = (Math.random() - 0.5) * 2 * h;\n                                slimes[slimeIdx].active = true;\n                                slimeMeshes[slimeIdx].position.set(slimes[slimeIdx].x, slimeSizeBase, slimes[slimeIdx].z);\n                                slimeMeshes[slimeIdx].scale.set(1, 1, 1);\n                                slimeMeshes[slimeIdx].visible = true;\n                            }, 1000);\n                        }\n                    }\n                }\n                \n                // === BATCH PROCESSING: Apply all collected slimes at once ===\n                if (useBatchProcessing && batchEatenSlimes.length > 0) {\n                    // Add all mass as single lump sum (prevents +1 mass 50 times lag)\n                    registerEat(batchEatenSlimes.length);\n                    playerMass += batchMassGain * getComboMult();\n                    \n                    // Single jiggle for the whole batch (scaled by count)\n                    triggerJiggle(Math.min(0.3, 0.05 + batchEatenSlimes.length * 0.02));\n                    \n                    // Create a few particles (not one per slime - that would lag)\n                    const particleCount = Math.min(10, batchEatenSlimes.length);\n                    for (let p = 0; p < particleCount; p++) {\n                        const angle = (p / particleCount) * Math.PI * 2;\n                        const px = playerPos.x + Math.cos(angle) * playerRadius * 0.8;\n                        const pz = playerPos.z + Math.sin(angle) * playerRadius * 0.8;\n                        const randomSlimeIdx = batchEatenSlimes[Math.floor(Math.random() * batchEatenSlimes.length)];\n                        const color = slimeMeshes[randomSlimeIdx].material.color.getHex();\n                        createSplatterParticles(px, playerRadius, pz, color, 3);\n                    }\n                    \n                    // Pop sound removed\n                    \n                    // Disable all eaten slimes and schedule respawns\n                    for (const slimeIdx of batchEatenSlimes) {\n                        slimes[slimeIdx].active = false;\n                        slimeMeshes[slimeIdx].visible = false;\n                        attractedSlimes.delete(slimeIdx);\n                        \n                        // Stagger respawns to prevent lag spike\n                        const respawnDelay = 1000 + Math.random() * 500;\n                        setTimeout(() => {\n                            const h = config.arena.size / 2 - 5;\n                            slimes[slimeIdx].x = (Math.random() - 0.5) * 2 * h;\n                            slimes[slimeIdx].z = (Math.random() - 0.5) * 2 * h;\n                            slimes[slimeIdx].active = true;\n                            slimeMeshes[slimeIdx].position.set(slimes[slimeIdx].x, slimeSizeBase, slimes[slimeIdx].z);\n                            slimeMeshes[slimeIdx].scale.set(1, 1, 1);\n                            slimeMeshes[slimeIdx].visible = true;\n                        }, respawnDelay);\n                    }\n                }\n                \n                // Clean up attraction state for slimes that moved out of range\n                for (const [idx, attr] of attractedSlimes) {\n                    if (!slimes[idx].active) {\n                        attractedSlimes.delete(idx);\n                        continue;\n                    }\n                    const dx = playerPos.x - slimes[idx].x;\n                    const dz = playerPos.z - slimes[idx].z;\n                    const dist = Math.sqrt(dx*dx + dz*dz);\n                    if (dist > attractionDist * 1.5) {\n                        // Reset slime position and scale\n                        slimeMeshes[idx].scale.set(1, 1, 1);\n                        attractedSlimes.delete(idx);\n                    }\n                }\n\n                // === ENHANCED AI SYSTEM CONSTANTS ===\n                const AI_DETECTION_RADIUS = 60;\n                const AI_THREAT_MULTIPLIER = 1.3; // Flee if threat is 30% bigger\n                const AI_CHASE_FATIGUE_MIN = 5;\n                const AI_CHASE_FATIGUE_MAX = 8;\n                const AI_REPULSION_RADIUS = 15;\n                const AI_REPULSION_STRENGTH = 3;\n                \n                // === ADVANCED PREDATORY AI CONSTANTS ===\n                const PREDICTION_TIME = 1.5; // Seconds to predict ahead\n                const CORNER_TRAP_THRESHOLD = 30; // Distance from wall to trigger cornering\n                const PINCER_ANGLE_MIN = 0.4; // Minimum angle spread for pincer (radians)\n                const AMBUSH_PATIENCE = 3; // Seconds to wait in ambush\n                const SLIME_SEEK_RADIUS = 25; // Radius to scan for slimes while fleeing\n                \n                // === ADVANCED FORAGING AI CONSTANTS (THE GREEDY ROOMBA) ===\n                const CLUSTER_SCAN_INTERVAL = 2.0; // Scan for clusters every 2 seconds\n                const CLUSTER_SCAN_RADIUS = 50; // How far to scan for slimes\n                const CLUSTER_GRID_SIZE = 10; // Grid cell size for density mapping\n                const CLUSTER_MIN_DENSITY = 8; // Minimum slimes to consider a cluster\n                const GREED_OVERRIDE_DENSITY = 15; // Density that triggers greed override\n                const SWEEP_AMPLITUDE = 8; // Width of S-curve sweep\n                const SWEEP_FREQUENCY = 2.5; // How fast to sweep (oscillations per second)\n                const STARVATION_TIMEOUT = 10; // Seconds without eating to trigger panic\n                const STARVATION_SPEED_BOOST = 1.15; // 15% speed increase when starving\n                const BOT_ATTRACTION_RADIUS = 3.5; // How far bots attract slimes\n                const BOT_ATTRACTION_SPEED = 5; // How fast slimes get sucked to bots\n                \n                // === BORDER AWARENESS CONSTANTS ===\n                const BORDER_AWARENESS_DIST = 25; // Distance at which bots start noticing border\n                const BORDER_DANGER_DIST = 12; // Distance at which border becomes urgent\n                const BORDER_AVOID_STRENGTH = 2.5; // How strongly to steer away from borders\n                const CORNER_AVOID_MULTIPLIER = 1.8; // Extra avoidance in corners\n                \n                // === SMARTER AI CONSTANTS ===\n                const THREAT_MEMORY_DURATION = 8; // Remember threats for 8 seconds\n                const THREAT_AVOIDANCE_RADIUS = 40; // Stay this far from remembered threats\n                const OPPORTUNITY_SCAN_RADIUS = 80; // Scan for distracted targets\n                const AMBUSH_DURATION = 6; // Max seconds to wait in ambush\n                const COMBAT_AWARENESS_RADIUS = 50; // Notice fights within this range\n                const VELOCITY_HISTORY_LENGTH = 10; // Frames of velocity to track\n                const CONFIDENCE_THRESHOLD = 3; // Kills needed for aggressive behavior\n                const LEARNING_CHASE_FAILS = 5; // Failed chases before becoming cautious\n                const TERRITORY_RADIUS = 40; // Size of territorial bot's home area\n                const DECISION_COOLDOWN = 0.5; // Seconds between major decisions\n                \n                // === PERSONALITY MODIFIERS ===\n                const PERSONALITY_TRAITS = {\n                    aggressive: { chaseRange: 1.3, fleeThreshold: 1.5, ambushChance: 0.1, riskTolerance: 0.8 },\n                    cautious: { chaseRange: 0.8, fleeThreshold: 1.15, ambushChance: 0.4, riskTolerance: 0.3 },\n                    opportunist: { chaseRange: 1.0, fleeThreshold: 1.25, ambushChance: 0.6, riskTolerance: 0.5 },\n                    territorial: { chaseRange: 1.1, fleeThreshold: 1.3, ambushChance: 0.3, riskTolerance: 0.6 },\n                    nomad: { chaseRange: 1.0, fleeThreshold: 1.2, ambushChance: 0.2, riskTolerance: 0.5 }\n                };\n                \n                // Track slimes being attracted to each bot (for vacuum visuals)\n                const botAttractedSlimes = new Map(); // botIndex -> Map(slimeIndex -> {progress, startX, startZ})\n                \n                // === COMBAT DETECTION SYSTEM ===\n                // Detect ongoing fights for opportunistic attacks\n                function detectCombatZones() {\n                    const combatZones = [];\n                    \n                    // Check for bots that are very close to each other (fighting)\n                    for (let i = 0; i < aiBots.length; i++) {\n                        for (let j = i + 1; j < aiBots.length; j++) {\n                            const a = aiBots[i];\n                            const b = aiBots[j];\n                            const dx = a.x - b.x;\n                            const dz = a.z - b.z;\n                            const dist = Math.sqrt(dx*dx + dz*dz);\n                            const combinedRadius = getRadius(a.mass) + getRadius(b.mass);\n                            \n                            // If two bots are close enough to be fighting\n                            if (dist < combinedRadius * 2.5) {\n                                combatZones.push({\n                                    x: (a.x + b.x) / 2,\n                                    z: (a.z + b.z) / 2,\n                                    participants: [i, j],\n                                    masses: [a.mass, b.mass],\n                                    timestamp: timestamp\n                                });\n                            }\n                        }\n                    }\n                    \n                    // Check if player is fighting a bot\n                    for (let i = 0; i < aiBots.length; i++) {\n                        const bot = aiBots[i];\n                        const dx = playerPos.x - bot.x;\n                        const dz = playerPos.z - bot.z;\n                        const dist = Math.sqrt(dx*dx + dz*dz);\n                        const combinedRadius = getRadius(playerMass) + getRadius(bot.mass);\n                        \n                        if (dist < combinedRadius * 2.5) {\n                            combatZones.push({\n                                x: (playerPos.x + bot.x) / 2,\n                                z: (playerPos.z + bot.z) / 2,\n                                participants: ['player', i],\n                                masses: [playerMass, bot.mass],\n                                timestamp: timestamp\n                            });\n                        }\n                    }\n                    \n                    return combatZones;\n                }\n                \n                // === ADVANCED VELOCITY PREDICTION ===\n                // Use velocity history for better target prediction\n                function predictTargetPosition(target, predictionTime, velocityHistory) {\n                    if (!velocityHistory || velocityHistory.length < 2) {\n                        // Fallback to simple prediction\n                        return { x: target.x, z: target.z };\n                    }\n                    \n                    // Calculate average velocity from history\n                    let avgVx = 0, avgVz = 0;\n                    for (const v of velocityHistory) {\n                        avgVx += v.vx;\n                        avgVz += v.vz;\n                    }\n                    avgVx /= velocityHistory.length;\n                    avgVz /= velocityHistory.length;\n                    \n                    // Calculate acceleration (change in velocity)\n                    let accelX = 0, accelZ = 0;\n                    if (velocityHistory.length >= 3) {\n                        const recent = velocityHistory.slice(-3);\n                        accelX = (recent[2].vx - recent[0].vx) / 2;\n                        accelZ = (recent[2].vz - recent[0].vz) / 2;\n                    }\n                    \n                    // Predict with velocity and acceleration\n                    const predictedX = target.x + avgVx * predictionTime + 0.5 * accelX * predictionTime * predictionTime;\n                    const predictedZ = target.z + avgVz * predictionTime + 0.5 * accelZ * predictionTime * predictionTime;\n                    \n                    // Clamp to arena\n                    const halfArena = config.arena.size / 2 - 5;\n                    return {\n                        x: Math.max(-halfArena, Math.min(halfArena, predictedX)),\n                        z: Math.max(-halfArena, Math.min(halfArena, predictedZ))\n                    };\n                }\n                \n                // === FIND AMBUSH POSITION ===\n                // Find good ambush spots near slime clusters\n                function findAmbushPosition(botX, botZ, scanRadius) {\n                    const cluster = findSlimeClustersForBot(botX, botZ, scanRadius);\n                    if (!cluster) return null;\n                    \n                    // Position slightly offset from cluster center\n                    const offsetAngle = Math.random() * Math.PI * 2;\n                    const offsetDist = 8 + Math.random() * 5;\n                    return {\n                        x: cluster.x + Math.cos(offsetAngle) * offsetDist,\n                        z: cluster.z + Math.sin(offsetAngle) * offsetDist,\n                        density: cluster.density\n                    };\n                }\n                \n                // Get current combat zones for opportunistic behavior\n                const currentCombatZones = detectCombatZones();\n                \n                // === CLUSTER DETECTION SYSTEM (Density Mapping) ===\n                function findSlimeClustersForBot(botX, botZ, scanRadius) {\n                    const gridCellSize = CLUSTER_GRID_SIZE;\n                    const densityGrid = {};\n                    \n                    // Scan slimes and map to grid\n                    for (let i = 0; i < slimes.length; i++) {\n                        if (!slimes[i].active) continue;\n                        \n                        const dx = slimes[i].x - botX;\n                        const dz = slimes[i].z - botZ;\n                        const dist = Math.sqrt(dx * dx + dz * dz);\n                        \n                        if (dist < scanRadius) {\n                            // Map to grid cell\n                            const cellX = Math.floor(slimes[i].x / gridCellSize);\n                            const cellZ = Math.floor(slimes[i].z / gridCellSize);\n                            const key = `${cellX},${cellZ}`;\n                            \n                            if (!densityGrid[key]) {\n                                densityGrid[key] = {\n                                    count: 0,\n                                    totalX: 0,\n                                    totalZ: 0,\n                                    slimeIndices: []\n                                };\n                            }\n                            densityGrid[key].count++;\n                            densityGrid[key].totalX += slimes[i].x;\n                            densityGrid[key].totalZ += slimes[i].z;\n                            densityGrid[key].slimeIndices.push(i);\n                        }\n                    }\n                    \n                    // Find the densest cluster\n                    let bestCluster = null;\n                    let bestScore = 0;\n                    \n                    for (const [key, cell] of Object.entries(densityGrid)) {\n                        if (cell.count >= CLUSTER_MIN_DENSITY) {\n                            // Calculate cluster center\n                            const centerX = cell.totalX / cell.count;\n                            const centerZ = cell.totalZ / cell.count;\n                            \n                            // Score = density / distance (prioritize nearby dense clusters)\n                            const dx = centerX - botX;\n                            const dz = centerZ - botZ;\n                            const dist = Math.sqrt(dx * dx + dz * dz) + 1;\n                            const score = (cell.count * cell.count) / dist; // Square density for emphasis\n                            \n                            if (score > bestScore) {\n                                bestScore = score;\n                                bestCluster = {\n                                    x: centerX,\n                                    z: centerZ,\n                                    density: cell.count,\n                                    slimeIndices: cell.slimeIndices\n                                };\n                            }\n                        }\n                    }\n                    \n                    return bestCluster;\n                }\n                \n                // AI bot update with TRUE FREE-FOR-ALL behavior and GREEDY ROOMBA logic\n                for (let botIndex = 0; botIndex < aiBots.length; botIndex++) {\n                    const b = aiBots[botIndex];\n                    b.changeTimer -= dt;\n                    \n                    // Initialize bot attraction tracking\n                    if (!botAttractedSlimes.has(botIndex)) {\n                        botAttractedSlimes.set(botIndex, new Map());\n                    }\n                    const thisBotsAttracted = botAttractedSlimes.get(botIndex);\n                    \n                    // === GET PERSONALITY TRAITS ===\n                    const traits = PERSONALITY_TRAITS[b.personality] || PERSONALITY_TRAITS.nomad;\n                    const effectiveDetectionRadius = AI_DETECTION_RADIUS * traits.chaseRange;\n                    const effectiveFleeThreshold = traits.fleeThreshold;\n                    \n                    // === UPDATE VELOCITY HISTORY (for prediction) ===\n                    const currentVx = (b.x - (b.prevX || b.x)) / Math.max(dt, 0.016);\n                    const currentVz = (b.z - (b.prevZ || b.z)) / Math.max(dt, 0.016);\n                    if (!b.velocityHistory) b.velocityHistory = [];\n                    b.velocityHistory.push({ vx: currentVx, vz: currentVz, t: timestamp });\n                    if (b.velocityHistory.length > VELOCITY_HISTORY_LENGTH) {\n                        b.velocityHistory.shift();\n                    }\n                    \n                    // === UPDATE THREAT MEMORY ===\n                    // Clean up old threat memories\n                    if (!b.threatMemory) b.threatMemory = [];\n                    b.threatMemory = b.threatMemory.filter(t => (timestamp - t.timestamp) / 1000 < THREAT_MEMORY_DURATION);\n                    \n                    // === UPDATE COMBAT AWARENESS ===\n                    b.combatAwareness = currentCombatZones.filter(cz => {\n                        const dx = cz.x - b.x;\n                        const dz = cz.z - b.z;\n                        const dist = Math.sqrt(dx*dx + dz*dz);\n                        return dist < COMBAT_AWARENESS_RADIUS && !cz.participants.includes(botIndex);\n                    });\n                    \n                    // === STARVATION PANIC CHECK ===\n                    const timeSinceEat = (timestamp - b.lastEatTime) / 1000;\n                    const wasStarving = b.isStarving;\n                    b.isStarving = timeSinceEat > STARVATION_TIMEOUT;\n                    \n                    // Starvation just triggered - rush to arena center for food\n                    if (b.isStarving && !wasStarving) {\n                        b.aiState = 'starving';\n                        b.clusterTarget = null; // Reset to find new food\n                    }\n                    \n                    // === CLUSTER SCAN (Every 2 seconds) ===\n                    b.clusterScanTimer -= dt;\n                    if (b.clusterScanTimer <= 0) {\n                        b.clusterScanTimer = CLUSTER_SCAN_INTERVAL + Math.random() * 0.5;\n                        b.clusterTarget = findSlimeClustersForBot(b.x, b.z, CLUSTER_SCAN_RADIUS);\n                        \n                        // Opportunist/Cautious bots look for ambush positions\n                        if ((b.personality === 'cautious' || b.personality === 'opportunist') && Math.random() < traits.ambushChance) {\n                            b.ambushPosition = findAmbushPosition(b.x, b.z, CLUSTER_SCAN_RADIUS);\n                        }\n                    }\n                    \n                    // === TERRITORIAL BEHAVIOR ===\n                    // Territorial bots return to their home area periodically\n                    if (b.personality === 'territorial' && b.aiState === 'foraging') {\n                        const distFromHome = Math.sqrt((b.x - b.territoryCenter.x)**2 + (b.z - b.territoryCenter.z)**2);\n                        if (distFromHome > TERRITORY_RADIUS * 1.5) {\n                            // Return home\n                            b.aiState = 'returning';\n                        }\n                    }\n                    \n                    // === SELF-PRESERVATION CHECK (Highest Priority) ===\n                    let nearestThreat = null;\n                    let nearestThreatDist = Infinity;\n                    \n                    // Check player as potential threat (personality affects flee threshold)\n                    if (!spawnInvincible) {\n                        const threatThreshold = b.mass * effectiveFleeThreshold;\n                        if (playerMass > threatThreshold) {\n                            const dx = playerPos.x - b.x;\n                            const dz = playerPos.z - b.z;\n                            const dist = Math.sqrt(dx*dx + dz*dz);\n                            if (dist < effectiveDetectionRadius && dist < nearestThreatDist) {\n                                nearestThreat = { x: playerPos.x, z: playerPos.z, mass: playerMass, isPlayer: true };\n                                nearestThreatDist = dist;\n                            }\n                        }\n                    }\n                    \n                    // Check other bots as potential threats (NO PLAYER BIAS!)\n                    for (let i = 0; i < aiBots.length; i++) {\n                        if (i === botIndex) continue;\n                        const other = aiBots[i];\n                        if (other.mass > b.mass * effectiveFleeThreshold) {\n                            const dx = other.x - b.x;\n                            const dz = other.z - b.z;\n                            const dist = Math.sqrt(dx*dx + dz*dz);\n                            if (dist < effectiveDetectionRadius && dist < nearestThreatDist) {\n                                nearestThreat = { x: other.x, z: other.z, mass: other.mass, botIndex: i };\n                                nearestThreatDist = dist;\n                            }\n                        }\n                    }\n                    \n                    // Also check threat memory - avoid areas where we were recently threatened\n                    for (const memory of b.threatMemory) {\n                        const dx = memory.x - b.x;\n                        const dz = memory.z - b.z;\n                        const dist = Math.sqrt(dx*dx + dz*dz);\n                        const memoryAge = (timestamp - memory.timestamp) / 1000;\n                        const memoryStrength = 1 - (memoryAge / THREAT_MEMORY_DURATION);\n                        \n                        // Remembered threats act as weaker current threats\n                        if (dist < THREAT_AVOIDANCE_RADIUS * memoryStrength && (!nearestThreat || dist < nearestThreatDist * 0.7)) {\n                            // Don't override a real current threat, but influence direction\n                            if (!nearestThreat) {\n                                nearestThreat = { x: memory.x, z: memory.z, mass: memory.mass, isMemory: true };\n                                nearestThreatDist = dist / memoryStrength;\n                            }\n                        }\n                    }\n                    \n                    // If threat detected, FLEE immediately (abandon any chase!)\n                    // === THREAT-AWARE FORAGING (Eating while Fleeing) ===\n                    let isFleeing = false;\n                    if (nearestThreat && nearestThreatDist < AI_DETECTION_RADIUS * 0.6) {\n                        const dx = b.x - nearestThreat.x;\n                        const dz = b.z - nearestThreat.z;\n                        const dist = Math.sqrt(dx*dx + dz*dz) + 0.01;\n                        \n                        // === SAVE TO THREAT MEMORY (for future avoidance) ===\n                        if (!nearestThreat.isMemory) {\n                            b.threatMemory.push({\n                                x: nearestThreat.x,\n                                z: nearestThreat.z,\n                                mass: nearestThreat.mass,\n                                timestamp: timestamp\n                            });\n                            b.lastThreatTime = timestamp;\n                        }\n                        \n                        // Base flee direction\n                        let fleeX = dx / dist;\n                        let fleeZ = dz / dist;\n                        \n                        // === SMART FLEEING: Consider escape routes ===\n                        // Cautious bots plan better escape routes\n                        if (b.personality === 'cautious' || b.personality === 'opportunist') {\n                            // Look for nearby large friendly bots to flee toward (safety in numbers concept)\n                            for (let i = 0; i < aiBots.length; i++) {\n                                if (i === botIndex) continue;\n                                const ally = aiBots[i];\n                                // Find bots similar in size that could deter the threat\n                                if (ally.mass > nearestThreat.mass * 0.9 && ally.mass > b.mass * 0.8) {\n                                    const allyDx = ally.x - b.x;\n                                    const allyDz = ally.z - b.z;\n                                    const allyDist = Math.sqrt(allyDx*allyDx + allyDz*allyDz);\n                                    \n                                    if (allyDist < 50 && allyDist > 5) {\n                                        // Slightly bias flee direction toward ally\n                                        const allyInfluence = 0.2;\n                                        fleeX = fleeX * (1 - allyInfluence) + (allyDx / allyDist) * allyInfluence;\n                                        fleeZ = fleeZ * (1 - allyInfluence) + (allyDz / allyDist) * allyInfluence;\n                                        break;\n                                    }\n                                }\n                            }\n                        }\n                        \n                        // === SMART FLEEING: Scan for slime patches in escape cone ===\n                        let bestSlimeOffset = { x: 0, z: 0 };\n                        let slimeDensityScore = 0;\n                        \n                        // Check slimes within escape cone (\u00b145 degrees from flee direction)\n                        for (const slime of slimes) {\n                            if (!slime.active) continue;\n                            const sdx = slime.x - b.x;\n                            const sdz = slime.z - b.z;\n                            const slimeDist = Math.sqrt(sdx*sdx + sdz*sdz);\n                            \n                            if (slimeDist < SLIME_SEEK_RADIUS && slimeDist > 2) {\n                                // Check if slime is roughly in flee direction\n                                const dotProduct = (sdx * fleeX + sdz * fleeZ) / slimeDist;\n                                if (dotProduct > 0.5) { // Within ~60 degree cone\n                                    // Weight by proximity and alignment\n                                    const weight = dotProduct / (slimeDist + 5);\n                                    bestSlimeOffset.x += sdx * weight;\n                                    bestSlimeOffset.z += sdz * weight;\n                                    slimeDensityScore += weight;\n                                }\n                            }\n                        }\n                        \n                        // Adjust flee direction to pass through slimes (if found)\n                        if (slimeDensityScore > 0.1) {\n                            const offsetMag = Math.sqrt(bestSlimeOffset.x**2 + bestSlimeOffset.z**2) + 0.01;\n                            const slimeInfluence = Math.min(0.4, slimeDensityScore * 0.5);\n                            fleeX = fleeX * (1 - slimeInfluence) + (bestSlimeOffset.x / offsetMag) * slimeInfluence;\n                            fleeZ = fleeZ * (1 - slimeInfluence) + (bestSlimeOffset.z / offsetMag) * slimeInfluence;\n                            \n                            // Renormalize\n                            const newMag = Math.sqrt(fleeX*fleeX + fleeZ*fleeZ) + 0.01;\n                            fleeX /= newMag;\n                            fleeZ /= newMag;\n                        }\n                        \n                        b.dirX = fleeX;\n                        b.dirZ = fleeZ;\n                        b.aiState = 'fleeing';\n                        b.currentTarget = null;\n                        b.chaseTimer = 0;\n                        b.ambushPosition = null; // Cancel any ambush\n                        isFleeing = true;\n                    }\n                    \n                    // === DYNAMIC TARGET PRIORITIZATION (\"Worth It\" Formula) ===\n                    if (!isFleeing) {\n                        let bestTarget = null;\n                        let bestScore = 0;\n                        \n                        // === GREED OVERRIDE CHECK (\"Ooh, Candy!\") ===\n                        // If there's a super dense cluster nearby, override any hunt!\n                        let greedTriggered = false;\n                        if (b.clusterTarget && b.clusterTarget.density >= GREED_OVERRIDE_DENSITY) {\n                            const clusterDx = b.clusterTarget.x - b.x;\n                            const clusterDz = b.clusterTarget.z - b.z;\n                            const clusterDist = Math.sqrt(clusterDx * clusterDx + clusterDz * clusterDz);\n                            \n                            // If cluster is close enough, greed overrides hunting\n                            if (clusterDist < 30) {\n                                greedTriggered = true;\n                                b.greedOverride = true;\n                                b.aiState = 'sweeping';\n                                b.currentTarget = null;\n                                b.chaseTimer = 0;\n                                \n                                // Set sweep center if not already sweeping\n                                if (!b.sweepCenter || b.sweepTimer > 5) {\n                                    b.sweepCenter = { x: b.clusterTarget.x, z: b.clusterTarget.z };\n                                    b.sweepPhase = 0;\n                                    b.sweepTimer = 0;\n                                }\n                            }\n                        }\n                        \n                        // === AMBUSH BEHAVIOR (Cautious/Opportunist) ===\n                        // Wait near slime clusters for prey to come\n                        if (!greedTriggered && b.ambushPosition && b.personality !== 'aggressive') {\n                            const ambushDx = b.ambushPosition.x - b.x;\n                            const ambushDz = b.ambushPosition.z - b.z;\n                            const ambushDist = Math.sqrt(ambushDx * ambushDx + ambushDz * ambushDz);\n                            \n                            if (ambushDist > 3) {\n                                // Move to ambush position\n                                b.dirX = ambushDx / ambushDist;\n                                b.dirZ = ambushDz / ambushDist;\n                                b.aiState = 'ambushing';\n                            } else {\n                                // At ambush position - wait and scan\n                                b.ambushTimer += dt;\n                                b.aiState = 'waiting';\n                                \n                                // Slowly rotate to scan for prey\n                                b.patrolAngle = (b.patrolAngle || 0) + dt * 0.5;\n                                b.dirX = Math.cos(b.patrolAngle) * 0.1;\n                                b.dirZ = Math.sin(b.patrolAngle) * 0.1;\n                                \n                                // Give up ambush after timeout\n                                if (b.ambushTimer > AMBUSH_DURATION) {\n                                    b.ambushPosition = null;\n                                    b.ambushTimer = 0;\n                                }\n                            }\n                        }\n                        \n                        // Only look for prey if not distracted by food or ambushing\n                        if (!greedTriggered && !b.ambushPosition) {\n                            b.greedOverride = false;\n                        \n                        // === OPPORTUNISTIC HUNTING (Third-Party Strikes) ===\n                        // Check for distracted targets fighting others\n                        let opportunityBonus = 0;\n                        for (const combat of b.combatAwareness) {\n                            // Check if any combat participant is edible\n                            for (let p = 0; p < combat.participants.length; p++) {\n                                const participant = combat.participants[p];\n                                const participantMass = combat.masses[p];\n                                \n                                if (participant === 'player') {\n                                    if (b.mass > participantMass * 1.1) {\n                                        // Player is fighting and we can eat them!\n                                        const dx = combat.x - b.x;\n                                        const dz = combat.z - b.z;\n                                        const dist = Math.sqrt(dx*dx + dz*dz);\n                                        // Huge bonus for distracted targets\n                                        const score = (participantMass * 2) / (dist + 1);\n                                        if (score > bestScore) {\n                                            bestScore = score;\n                                            bestTarget = { x: playerPos.x, z: playerPos.z, mass: participantMass, isPlayer: true, dist, isOpportunity: true };\n                                        }\n                                    }\n                                } else if (typeof participant === 'number' && participant !== botIndex) {\n                                    const targetBot = aiBots[participant];\n                                    if (targetBot && b.mass > targetBot.mass * 1.1) {\n                                        const dx = targetBot.x - b.x;\n                                        const dz = targetBot.z - b.z;\n                                        const dist = Math.sqrt(dx*dx + dz*dz);\n                                        // Huge bonus for distracted targets\n                                        const score = (targetBot.mass * 2) / (dist + 1);\n                                        if (score > bestScore) {\n                                            bestScore = score;\n                                            bestTarget = { x: targetBot.x, z: targetBot.z, mass: targetBot.mass, isPlayer: false, botIndex: participant, dist, isOpportunity: true };\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        \n                        // Evaluate player as target (NO BIAS - same formula as bots)\n                        if (!spawnInvincible && b.mass > playerMass * 1.1) {\n                            const dx = playerPos.x - b.x;\n                            const dz = playerPos.z - b.z;\n                            const dist = Math.sqrt(dx*dx + dz*dz);\n                            if (dist < effectiveDetectionRadius) {\n                                // Personality affects scoring\n                                let score = playerMass / (dist + 1); // Worth It formula\n                                \n                                // Aggressive bots get bonus for hunting players\n                                if (b.personality === 'aggressive') score *= 1.3;\n                                // Confident bots (many kills) are more aggressive\n                                if (b.successfulKills >= CONFIDENCE_THRESHOLD) score *= 1.2;\n                                // REVENGE MODE: Bots killed by player within 30 seconds want payback!\n                                if (b.killedByPlayer && (timestamp - b.killedByPlayer) / 1000 < 30) {\n                                    score *= 2.0; // Double priority for revenge!\n                                }\n                                \n                                if (score > bestScore) {\n                                    bestScore = score;\n                                    bestTarget = { x: playerPos.x, z: playerPos.z, mass: playerMass, isPlayer: true, dist };\n                                }\n                            }\n                        }\n                        \n                        // Evaluate ALL other bots as targets (BOT-ON-BOT VIOLENCE!)\n                        for (let i = 0; i < aiBots.length; i++) {\n                            if (i === botIndex) continue;\n                            const other = aiBots[i];\n                            if (b.mass > other.mass * 1.1) { // Can eat them\n                                const dx = other.x - b.x;\n                                const dz = other.z - b.z;\n                                const dist = Math.sqrt(dx*dx + dz*dz);\n                                if (dist < effectiveDetectionRadius) {\n                                    let score = other.mass / (dist + 1); // Same formula!\n                                    \n                                    // Bonus for fleeing targets (easier to catch)\n                                    if (other.aiState === 'fleeing') score *= 1.4;\n                                    // Bonus for starving targets (desperate, less aware)\n                                    if (other.isStarving) score *= 1.3;\n                                    // Territorial bots prioritize intruders\n                                    if (b.personality === 'territorial') {\n                                        const intruderDist = Math.sqrt((other.x - b.territoryCenter.x)**2 + (other.z - b.territoryCenter.z)**2);\n                                        if (intruderDist < TERRITORY_RADIUS) score *= 1.5;\n                                    }\n                                    \n                                    if (score > bestScore) {\n                                        bestScore = score;\n                                        bestTarget = { x: other.x, z: other.z, mass: other.mass, isPlayer: false, botIndex: i, dist };\n                                    }\n                                }\n                            }\n                        }\n                        \n                        // === CHASE FATIGUE (Attention Span) ===\n                        if (bestTarget) {\n                            // Check if this is the same target or new target\n                            const sameTarget = b.currentTarget && \n                                b.currentTarget.isPlayer === bestTarget.isPlayer &&\n                                (bestTarget.isPlayer || b.currentTarget.botIndex === bestTarget.botIndex);\n                            \n                            if (sameTarget) {\n                                b.chaseTimer += dt;\n                                // Personality affects patience\n                                const effectiveMaxChase = b.maxChaseTime * (b.personality === 'aggressive' ? 1.3 : 1);\n                                if (b.chaseTimer > effectiveMaxChase) {\n                                    // Chase fatigue! Give up and forage\n                                    b.aiState = 'foraging';\n                                    b.currentTarget = null;\n                                    b.chaseTimer = 0;\n                                    b.failedChases = (b.failedChases || 0) + 1;\n                                    b.maxChaseTime = AI_CHASE_FATIGUE_MIN + Math.random() * (AI_CHASE_FATIGUE_MAX - AI_CHASE_FATIGUE_MIN);\n                                    bestTarget = null;\n                                    \n                                    // Learning: after many failed chases, become more cautious\n                                    if (b.failedChases >= LEARNING_CHASE_FAILS && b.personality === 'aggressive') {\n                                        b.personality = 'opportunist'; // Learn to be smarter\n                                    }\n                                }\n                            } else {\n                                // New target found (maybe juicier!) - reset timer\n                                b.chaseTimer = 0;\n                                b.currentTarget = bestTarget;\n                            }\n                        } else {\n                            b.chaseTimer = 0;\n                            b.currentTarget = null;\n                        }\n                        \n                        // === HUNTING or FORAGING ===\n                        if (bestTarget) {\n                            // === ADVANCED PREDICTIVE INTERCEPTION ===\n                            // Use velocity history for better prediction\n                            let predictedPos;\n                            \n                            if (bestTarget.isPlayer) {\n                                // Use player's velocity history concept\n                                const predTime = PREDICTION_TIME * Math.min(1.5, bestTarget.dist / 20);\n                                predictedPos = {\n                                    x: bestTarget.x + playerVelocity.x * predTime,\n                                    z: bestTarget.z + playerVelocity.z * predTime\n                                };\n                            } else if (bestTarget.botIndex !== undefined) {\n                                // Use target bot's velocity history\n                                const targetBot = aiBots[bestTarget.botIndex];\n                                predictedPos = predictTargetPosition(\n                                    bestTarget,\n                                    PREDICTION_TIME * Math.min(1.5, bestTarget.dist / 20),\n                                    targetBot.velocityHistory\n                                );\n                            } else {\n                                predictedPos = { x: bestTarget.x, z: bestTarget.z };\n                            }\n                            \n                            let predictedX = predictedPos.x;\n                            let predictedZ = predictedPos.z;\n                            \n                            // Clamp prediction to arena bounds\n                            const predHalf = config.arena.size / 2 - 5;\n                            predictedX = Math.max(-predHalf, Math.min(predHalf, predictedX));\n                            predictedZ = Math.max(-predHalf, Math.min(predHalf, predictedZ));\n                            \n                            // === SPATIAL AWARENESS & TRAPPING (Cornering) ===\n                            const arenaHalf = config.arena.size / 2;\n                            const targetDistToWallX = arenaHalf - Math.abs(bestTarget.x);\n                            const targetDistToWallZ = arenaHalf - Math.abs(bestTarget.z);\n                            const targetNearWall = targetDistToWallX < CORNER_TRAP_THRESHOLD || targetDistToWallZ < CORNER_TRAP_THRESHOLD;\n                            \n                            let interceptX = predictedX;\n                            let interceptZ = predictedZ;\n                            \n                            if (targetNearWall && bestTarget.dist < effectiveDetectionRadius * 0.5) {\n                                // Target is near a wall - try to cut off escape to center\n                                const centerDirX = -bestTarget.x / (Math.abs(bestTarget.x) + 0.01);\n                                const centerDirZ = -bestTarget.z / (Math.abs(bestTarget.z) + 0.01);\n                                \n                                // Position to block the escape route\n                                // Move toward the path between target and center\n                                const blockPointX = bestTarget.x + centerDirX * 15;\n                                const blockPointZ = bestTarget.z + centerDirZ * 15;\n                                \n                                // Blend between direct chase and blocking position\n                                // Aggressive bots are more direct, cautious bots block more\n                                const trapInfluence = b.personality === 'aggressive' ? 0.4 : 0.7;\n                                interceptX = predictedX * (1 - trapInfluence) + blockPointX * trapInfluence;\n                                interceptZ = predictedZ * (1 - trapInfluence) + blockPointZ * trapInfluence;\n                            }\n                            \n                            const dx = interceptX - b.x;\n                            const dz = interceptZ - b.z;\n                            const dist = Math.sqrt(dx*dx + dz*dz) + 0.01;\n                            b.dirX = dx / dist;\n                            b.dirZ = dz / dist;\n                            b.aiState = 'hunting';\n                            b.currentTarget = bestTarget;\n                        } else {\n                            // === CLUSTER-BASED FORAGING (The Greedy Roomba) ===\n                            // Instead of targeting single slimes, go for dense clusters\n                            if (b.aiState !== 'foraging' || b.changeTimer <= 0) {\n                                b.aiState = 'foraging';\n                                \n                                // If we have a cluster target, go to it\n                                if (b.clusterTarget) {\n                                    const dx = b.clusterTarget.x - b.x;\n                                    const dz = b.clusterTarget.z - b.z;\n                                    const dist = Math.sqrt(dx*dx + dz*dz) + 0.01;\n                                    b.dirX = dx / dist;\n                                    b.dirZ = dz / dist;\n                                    \n                                    // If close to cluster, start sweeping\n                                    if (dist < 15) {\n                                        b.aiState = 'sweeping';\n                                        b.sweepCenter = { x: b.clusterTarget.x, z: b.clusterTarget.z };\n                                        b.sweepPhase = 0;\n                                        b.sweepTimer = 0;\n                                    }\n                                } else {\n                                    // No cluster found - wander randomly\n                                    b.dirX = (Math.random() - 0.5) * 2;\n                                    b.dirZ = (Math.random() - 0.5) * 2;\n                                }\n                                b.changeTimer = 2 + Math.random() * 3;\n                            }\n                        }\n                        } // End of !greedTriggered block\n                        \n                        // === S-CURVE SWEEPING MANEUVERS ===\n                        // When in sweeping state, weave back and forth through food field\n                        if (b.aiState === 'sweeping' && b.sweepCenter) {\n                            b.sweepTimer += dt;\n                            b.sweepPhase += SWEEP_FREQUENCY * dt * Math.PI * 2;\n                            \n                            // Calculate serpentine path through cluster\n                            // Base direction toward sweep center\n                            const toCenterX = b.sweepCenter.x - b.x;\n                            const toCenterZ = b.sweepCenter.z - b.z;\n                            const distToCenter = Math.sqrt(toCenterX * toCenterX + toCenterZ * toCenterZ);\n                            \n                            if (distToCenter > 5) {\n                                // Calculate forward direction\n                                const fwdX = toCenterX / distToCenter;\n                                const fwdZ = toCenterZ / distToCenter;\n                                \n                                // Calculate perpendicular for S-curve oscillation\n                                const perpX = -fwdZ;\n                                const perpZ = fwdX;\n                                \n                                // S-curve offset based on phase\n                                const swerve = Math.sin(b.sweepPhase) * SWEEP_AMPLITUDE / distToCenter;\n                                \n                                // Combine forward movement with serpentine swerve\n                                b.dirX = fwdX + perpX * swerve;\n                                b.dirZ = fwdZ + perpZ * swerve;\n                                \n                                // Normalize\n                                const mag = Math.sqrt(b.dirX * b.dirX + b.dirZ * b.dirZ);\n                                if (mag > 0) {\n                                    b.dirX /= mag;\n                                    b.dirZ /= mag;\n                                }\n                            } else {\n                                // At center, do tight circles to vacuum everything\n                                const circleAngle = b.sweepTimer * 3;\n                                b.dirX = Math.cos(circleAngle);\n                                b.dirZ = Math.sin(circleAngle);\n                            }\n                            \n                            // Exit sweep after timeout or if cluster depleted\n                            if (b.sweepTimer > 5 || !b.clusterTarget || b.clusterTarget.density < 3) {\n                                b.aiState = 'foraging';\n                                b.sweepCenter = null;\n                                b.clusterScanTimer = 0; // Force rescan\n                            }\n                        }\n                        \n                        // === STARVATION PANIC MODE ===\n                        // Rush to arena center to find food\n                        if (b.aiState === 'starving') {\n                            // If there's a cluster target, go there frantically\n                            if (b.clusterTarget) {\n                                const dx = b.clusterTarget.x - b.x;\n                                const dz = b.clusterTarget.z - b.z;\n                                const dist = Math.sqrt(dx*dx + dz*dz) + 0.01;\n                                b.dirX = dx / dist;\n                                b.dirZ = dz / dist;\n                            } else {\n                                // Rush toward arena center (usually has more food)\n                                const toCenterDist = Math.sqrt(b.x * b.x + b.z * b.z) + 0.01;\n                                if (toCenterDist > 20) {\n                                    b.dirX = -b.x / toCenterDist;\n                                    b.dirZ = -b.z / toCenterDist;\n                                } else {\n                                    // At center, spiral outward\n                                    const spiralAngle = timestamp * 0.002;\n                                    b.dirX = Math.cos(spiralAngle);\n                                    b.dirZ = Math.sin(spiralAngle);\n                                }\n                            }\n                        }\n                        \n                        // === TERRITORIAL RETURNING BEHAVIOR ===\n                        // Territorial bots return to their home base\n                        if (b.aiState === 'returning' && b.personality === 'territorial') {\n                            const homeX = b.territoryCenter.x;\n                            const homeZ = b.territoryCenter.z;\n                            const dx = homeX - b.x;\n                            const dz = homeZ - b.z;\n                            const dist = Math.sqrt(dx*dx + dz*dz);\n                            \n                            if (dist > 5) {\n                                b.dirX = dx / dist;\n                                b.dirZ = dz / dist;\n                            } else {\n                                // Back home - resume patrolling\n                                b.aiState = 'foraging';\n                                b.patrolAngle = Math.random() * Math.PI * 2;\n                            }\n                        }\n                        \n                        // === NOMAD WANDERING BEHAVIOR ===\n                        // Nomad bots move across the arena systematically\n                        if (b.personality === 'nomad' && b.aiState === 'foraging' && !b.clusterTarget) {\n                            b.patrolAngle = (b.patrolAngle || 0) + dt * 0.3;\n                            const wanderRadius = config.arena.size * 0.3;\n                            const wanderX = Math.cos(b.patrolAngle) * wanderRadius;\n                            const wanderZ = Math.sin(b.patrolAngle * 1.3) * wanderRadius; // Lissajous pattern\n                            \n                            const dx = wanderX - b.x;\n                            const dz = wanderZ - b.z;\n                            const dist = Math.sqrt(dx*dx + dz*dz) + 0.01;\n                            \n                            b.dirX = dx / dist;\n                            b.dirZ = dz / dist;\n                        }\n                        \n                        // === THE PINCER MANEUVER (Advanced Flanking) ===\n                        // If multiple bots chase same target, form a V shape\n                        if (b.currentTarget && b.aiState === 'hunting') {\n                            // Find other hunters of same target\n                            const fellowHunters = [];\n                            for (let i = 0; i < aiBots.length; i++) {\n                                if (i === botIndex) continue;\n                                const other = aiBots[i];\n                                \n                                const sameTarget = other.currentTarget && \n                                    other.currentTarget.isPlayer === b.currentTarget.isPlayer &&\n                                    (b.currentTarget.isPlayer || other.currentTarget.botIndex === b.currentTarget.botIndex);\n                                \n                                if (sameTarget && other.aiState === 'hunting') {\n                                    fellowHunters.push({ bot: other, index: i });\n                                }\n                            }\n                            \n                            if (fellowHunters.length > 0) {\n                                // Calculate target position\n                                const targetX = b.currentTarget.x;\n                                const targetZ = b.currentTarget.z;\n                                \n                                // Calculate this bot's angle to target\n                                const myAngle = Math.atan2(b.z - targetZ, b.x - targetX);\n                                \n                                // For each fellow hunter, check angle spread\n                                for (const fellow of fellowHunters) {\n                                    const otherAngle = Math.atan2(fellow.bot.z - targetZ, fellow.bot.x - targetX);\n                                    let angleDiff = Math.abs(myAngle - otherAngle);\n                                    if (angleDiff > Math.PI) angleDiff = 2 * Math.PI - angleDiff;\n                                    \n                                    // If angle is too small, push apart to form V\n                                    if (angleDiff < PINCER_ANGLE_MIN) {\n                                        // Determine which way to push (clockwise or counter-clockwise)\n                                        const pushDirection = ((myAngle - otherAngle + Math.PI * 3) % (Math.PI * 2)) < Math.PI ? 1 : -1;\n                                        \n                                        // Calculate perpendicular to chase direction\n                                        const perpX = -b.dirZ * pushDirection;\n                                        const perpZ = b.dirX * pushDirection;\n                                        \n                                        // Apply flanking offset\n                                        const flankStrength = (PINCER_ANGLE_MIN - angleDiff) * 2;\n                                        b.dirX += perpX * flankStrength;\n                                        b.dirZ += perpZ * flankStrength;\n                                    }\n                                    \n                                    // Also apply distance-based repulsion\n                                    const dx = b.x - fellow.bot.x;\n                                    const dz = b.z - fellow.bot.z;\n                                    const dist = Math.sqrt(dx*dx + dz*dz);\n                                    \n                                    if (dist < AI_REPULSION_RADIUS && dist > 0.5) {\n                                        const massRatio = Math.min(b.mass, fellow.bot.mass) / Math.max(b.mass, fellow.bot.mass);\n                                        if (massRatio > 0.5) {\n                                            const repulse = (AI_REPULSION_RADIUS - dist) / AI_REPULSION_RADIUS;\n                                            b.dirX += (dx / dist) * repulse * AI_REPULSION_STRENGTH * dt;\n                                            b.dirZ += (dz / dist) * repulse * AI_REPULSION_STRENGTH * dt;\n                                        }\n                                    }\n                                }\n                                \n                                // Normalize final direction\n                                const mag = Math.sqrt(b.dirX*b.dirX + b.dirZ*b.dirZ);\n                                if (mag > 0) {\n                                    b.dirX /= mag;\n                                    b.dirZ /= mag;\n                                }\n                            }\n                        }\n                    }\n                    \n                    // Store previous position for velocity calculation\n                    b.prevX = b.x;\n                    b.prevZ = b.z;\n                    \n                    // === BORDER AWARENESS SYSTEM ===\n                    // Calculate distance to each arena wall\n                    const arenaHalfSize = config.arena.size / 2;\n                    const distToLeft = b.x - (-arenaHalfSize);   // Distance to left wall (-X)\n                    const distToRight = arenaHalfSize - b.x;    // Distance to right wall (+X)\n                    const distToBottom = b.z - (-arenaHalfSize); // Distance to bottom wall (-Z)\n                    const distToTop = arenaHalfSize - b.z;      // Distance to top wall (+Z)\n                    \n                    // Calculate border avoidance steering\n                    let borderAvoidX = 0;\n                    let borderAvoidZ = 0;\n                    let inCorner = false;\n                    \n                    // Check each wall and add steering away from it\n                    if (distToLeft < BORDER_AWARENESS_DIST) {\n                        const urgency = distToLeft < BORDER_DANGER_DIST ? 2.0 : 1.0;\n                        const avoidStrength = (1 - distToLeft / BORDER_AWARENESS_DIST) * BORDER_AVOID_STRENGTH * urgency;\n                        borderAvoidX += avoidStrength; // Steer right (+X)\n                    }\n                    if (distToRight < BORDER_AWARENESS_DIST) {\n                        const urgency = distToRight < BORDER_DANGER_DIST ? 2.0 : 1.0;\n                        const avoidStrength = (1 - distToRight / BORDER_AWARENESS_DIST) * BORDER_AVOID_STRENGTH * urgency;\n                        borderAvoidX -= avoidStrength; // Steer left (-X)\n                    }\n                    if (distToBottom < BORDER_AWARENESS_DIST) {\n                        const urgency = distToBottom < BORDER_DANGER_DIST ? 2.0 : 1.0;\n                        const avoidStrength = (1 - distToBottom / BORDER_AWARENESS_DIST) * BORDER_AVOID_STRENGTH * urgency;\n                        borderAvoidZ += avoidStrength; // Steer up (+Z)\n                    }\n                    if (distToTop < BORDER_AWARENESS_DIST) {\n                        const urgency = distToTop < BORDER_DANGER_DIST ? 2.0 : 1.0;\n                        const avoidStrength = (1 - distToTop / BORDER_AWARENESS_DIST) * BORDER_AVOID_STRENGTH * urgency;\n                        borderAvoidZ -= avoidStrength; // Steer down (-Z)\n                    }\n                    \n                    // Check if in a corner (near two walls) - apply extra avoidance\n                    const nearTwoWalls = (distToLeft < BORDER_AWARENESS_DIST || distToRight < BORDER_AWARENESS_DIST) &&\n                                        (distToBottom < BORDER_AWARENESS_DIST || distToTop < BORDER_AWARENESS_DIST);\n                    if (nearTwoWalls) {\n                        inCorner = true;\n                        borderAvoidX *= CORNER_AVOID_MULTIPLIER;\n                        borderAvoidZ *= CORNER_AVOID_MULTIPLIER;\n                    }\n                    \n                    // Apply border avoidance to direction\n                    if (borderAvoidX !== 0 || borderAvoidZ !== 0) {\n                        // Blend current direction with border avoidance\n                        // Stronger avoidance when very close to border\n                        const minDist = Math.min(distToLeft, distToRight, distToBottom, distToTop);\n                        const avoidWeight = minDist < BORDER_DANGER_DIST ? 0.8 : 0.4;\n                        \n                        b.dirX = b.dirX * (1 - avoidWeight) + borderAvoidX * avoidWeight;\n                        b.dirZ = b.dirZ * (1 - avoidWeight) + borderAvoidZ * avoidWeight;\n                    }\n                    \n                    // === FINAL DIRECTION NORMALIZATION ===\n                    // Ensure the bot always moves at full speed and doesn't get stuck with 0 direction\n                    const finalDirMag = Math.sqrt(b.dirX * b.dirX + b.dirZ * b.dirZ);\n                    if (finalDirMag > 0.001) {\n                        b.dirX /= finalDirMag;\n                        b.dirZ /= finalDirMag;\n                    } else {\n                        // If direction is perfectly 0 (e.g. exactly on top of target), pick a random direction\n                        const randomAngle = Math.random() * Math.PI * 2;\n                        b.dirX = Math.cos(randomAngle);\n                        b.dirZ = Math.sin(randomAngle);\n                    }\n                    \n                    // Movement (speed decreases with size) with starvation boost\n                    let botSpeed = 5 + 2 / (1 + b.mass * 0.01);\n                    if (b.isStarving) {\n                        botSpeed *= STARVATION_SPEED_BOOST; // 15% faster when starving\n                    }\n                    \n                    const prevBotX = b.x;\n                    const prevBotZ = b.z;\n                    b.x += b.dirX * botSpeed * dt;\n                    b.z += b.dirZ * botSpeed * dt;\n                    b.x = Math.max(-half, Math.min(half, b.x));\n                    b.z = Math.max(-half, Math.min(half, b.z));\n                    \n                    // === BOT ROLLING PHYSICS ===\n                    const botDeltaX = b.x - prevBotX;\n                    const botDeltaZ = b.z - prevBotZ;\n                    const botMoveDist = Math.sqrt(botDeltaX ** 2 + botDeltaZ ** 2);\n                    \n                    if (botMoveDist > 0.001) {\n                        const botR = getRadius(b.mass);\n                        const botAngularSpeed = botMoveDist / botR; // \u03c9 = v / r\n                        \n                        // Accumulate rotation\n                        b.rotationX = (b.rotationX || 0) + (botDeltaZ / botMoveDist) * botAngularSpeed;\n                        b.rotationZ = (b.rotationZ || 0) - (botDeltaX / botMoveDist) * botAngularSpeed;\n                    }\n                    \n                    // Update Black Hole accretion disk rotation\n                    if (b.skinType === 'blackhole' && b.mesh.userData.accretionDisk) {\n                        b.diskRotation = (b.diskRotation || 0) + dt * 2;\n                        b.mesh.userData.accretionDisk.rotation.z = b.diskRotation;\n                        if (b.mesh.userData.innerDisk) {\n                            b.mesh.userData.innerDisk.rotation.z = -b.diskRotation * 1.5;\n                        }\n                    }\n                    \n                    // Update Sun light distance based on mass\n                    if (b.skinType === 'sun' && b.mesh.userData.sunLight) {\n                        b.mesh.userData.sunLight.distance = 20 + b.mass * 0.05;\n                        b.mesh.userData.sunLight.intensity = 0.5 + b.mass * 0.003;\n                    }\n                    \n                    // === BOT VACUUM SYSTEM (Magnetic Attraction - The Black Hole Effect) ===\n                    const botR = getRadius(b.mass);\n                    const botAttractionDist = botR + BOT_ATTRACTION_RADIUS;\n                    const botEatDist = botR;\n                    const slimeSizeBase = 0.5 * config.slimes.sizeMultiplier;\n                    let botAteThisFrame = false;\n                    \n                    for (let i = 0; i < slimes.length; i++) {\n                        if (!slimes[i].active) continue;\n                        \n                        const dx = b.x - slimes[i].x;\n                        const dz = b.z - slimes[i].z;\n                        const dist = Math.sqrt(dx * dx + dz * dz);\n                        \n                        // Check if slime is in attraction range (The Vacuum Effect!)\n                        if (dist < botAttractionDist && dist > botEatDist * 0.5) {\n                            // Start or continue attraction\n                            if (!thisBotsAttracted.has(i)) {\n                                thisBotsAttracted.set(i, {\n                                    progress: 0,\n                                    startX: slimes[i].x,\n                                    startZ: slimes[i].z,\n                                    originalY: slimeMeshes[i].position.y\n                                });\n                            }\n                            \n                            const attr = thisBotsAttracted.get(i);\n                            attr.progress += BOT_ATTRACTION_SPEED * dt;\n                            \n                            // Accelerating pull (ease-in cubic)\n                            const t = Math.min(attr.progress, 1);\n                            const easeT = t * t * t;\n                            \n                            // Move slime toward bot\n                            slimes[i].x = attr.startX + (b.x - attr.startX) * easeT;\n                            slimes[i].z = attr.startZ + (b.z - attr.startZ) * easeT;\n                            \n                            // Teardrop stretch toward bot (dramatic vacuum visual!)\n                            const stretchFactor = 1 + easeT * 2.0;\n                            const squishFactor = 1 / Math.sqrt(stretchFactor);\n                            \n                            const dirMag = dist + 0.01;\n                            const ndx = dx / dirMag;\n                            const ndz = dz / dirMag;\n                            \n                            slimeMeshes[i].scale.set(\n                                squishFactor * (1 - Math.abs(ndx) * 0.4 * easeT) + Math.abs(ndx) * stretchFactor * 0.4 * easeT,\n                                squishFactor,\n                                squishFactor * (1 - Math.abs(ndz) * 0.4 * easeT) + Math.abs(ndz) * stretchFactor * 0.4 * easeT\n                            );\n                            \n                            // Rise up and get sucked in\n                            slimeMeshes[i].position.set(slimes[i].x, attr.originalY * (1 + easeT * 0.5), slimes[i].z);\n                        }\n                        \n                        // Check if slime should be consumed\n                        const currentDx = b.x - slimes[i].x;\n                        const currentDz = b.z - slimes[i].z;\n                        const currentDist = Math.sqrt(currentDx * currentDx + currentDz * currentDz);\n                        \n                        if (currentDist < botEatDist || (thisBotsAttracted.has(i) && thisBotsAttracted.get(i).progress >= 1)) {\n                            // Bot eats the slime!\n                            slimes[i].active = false;\n                            slimeMeshes[i].visible = false;\n                            thisBotsAttracted.delete(i);\n                            b.mass += 0.5;\n                            b.lastEatTime = timestamp; // Reset starvation timer\n                            b.isStarving = false;\n                            botAteThisFrame = true;\n                            \n                            // Update cluster density tracking\n                            if (b.clusterTarget) {\n                                b.clusterTarget.density = Math.max(0, b.clusterTarget.density - 1);\n                            }\n                            \n                            // Create small splatter particles (visual feedback)\n                            const slimeColor = slimeMeshes[i].material.color.getHex();\n                            createSplatterParticles(b.x, botR, b.z, slimeColor, 3);\n                            \n                            // Respawn slime elsewhere\n                            const slimeIdx = i;\n                            setTimeout(() => {\n                                const h = config.arena.size / 2 - 5;\n                                slimes[slimeIdx].x = (Math.random() - 0.5) * 2 * h;\n                                slimes[slimeIdx].z = (Math.random() - 0.5) * 2 * h;\n                                slimes[slimeIdx].active = true;\n                                slimeMeshes[slimeIdx].position.set(slimes[slimeIdx].x, slimeSizeBase, slimes[slimeIdx].z);\n                                slimeMeshes[slimeIdx].scale.set(1, 1, 1);\n                                slimeMeshes[slimeIdx].visible = true;\n                            }, 1000);\n                        }\n                    }\n                    \n                    // Clean up attraction state for slimes that moved out of range\n                    for (const [idx] of thisBotsAttracted) {\n                        if (!slimes[idx].active) {\n                            thisBotsAttracted.delete(idx);\n                            continue;\n                        }\n                        const dx = b.x - slimes[idx].x;\n                        const dz = b.z - slimes[idx].z;\n                        const dist = Math.sqrt(dx * dx + dz * dz);\n                        if (dist > botAttractionDist * 1.5) {\n                            slimeMeshes[idx].scale.set(1, 1, 1);\n                            thisBotsAttracted.delete(idx);\n                        }\n                    }\n                    \n                    // === BOT-ON-BOT EATING (True Free-for-All!) ===\n                    for (let i = 0; i < aiBots.length; i++) {\n                        if (i === botIndex) continue;\n                        const other = aiBots[i];\n                        const dx = b.x - other.x;\n                        const dz = b.z - other.z;\n                        const dist = Math.sqrt(dx*dx + dz*dz);\n                        const collisionDist = botR + getRadius(other.mass);\n                        \n                        if (dist < collisionDist && b.mass > other.mass * 1.1) {\n                            // This bot eats the other bot!\n                            const victimColor = other.mesh.material.color ? other.mesh.material.color.getHex() : 0xffffff;\n                            const victimRadius = getRadius(other.mass);\n                            // Epic kill burst effect!\n                            createKillBurstEffect(other.x, victimRadius, other.z, victimColor, victimRadius);\n                            \n                            // Gain mass from eating and reset starvation\n                            b.mass += other.mass * 0.3;\n                            b.lastEatTime = timestamp;\n                            b.isStarving = false;\n                            \n                            // Clear attraction tracking for eaten bot\n                            if (botAttractedSlimes.has(i)) {\n                                botAttractedSlimes.get(i).clear();\n                            }\n                            \n                            // Respawn the eaten bot with new cosmic skin\n                            other.mass = getBotRespawnMass();\n                            other.x = (Math.random() - 0.5) * config.arena.size * 0.6;\n                            other.z = (Math.random() - 0.5) * config.arena.size * 0.6;\n                            other.aiState = 'foraging';\n                            other.currentTarget = null;\n                            other.chaseTimer = 0;\n                            other.lastEatTime = timestamp; // Reset new bot's starvation\n                            other.isStarving = false;\n                            other.clusterTarget = null;\n                            other.sweepCenter = null;\n                            other.sweepTimer = 0;\n                            other.greedOverride = false;\n                            // Reset smarter AI properties\n                            other.threatMemory = [];\n                            other.ambushPosition = null;\n                            other.ambushTimer = 0;\n                            other.velocityHistory = [];\n                            other.territoryCenter = { x: other.x, z: other.z };\n                            other.patrolAngle = Math.random() * Math.PI * 2;\n                            // Assign new personality on respawn\n                            const personalities = ['aggressive', 'cautious', 'opportunist', 'territorial', 'nomad'];\n                            other.personality = personalities[Math.floor(Math.random() * personalities.length)];\n                            botDeformations.delete(i);\n                            resetBotXRayState(i); // Reset X-Ray transparency state\n                            \n                            // Track successful kill for confidence\n                            b.successfulKills = (b.successfulKills || 0) + 1;\n                            \n                            // Assign new cosmic skin\n                            const newSkin = selectBotSkin();\n                            other.skinType = newSkin;\n                            \n                            // Remove old mesh and create new one\n                            scene.remove(other.mesh);\n                            const newOtherR = getRadius(other.mass);\n                            other.mesh = createCelestialMesh(newSkin, newOtherR);\n                            other.mesh.position.set(other.x, newOtherR, other.z);\n                            scene.add(other.mesh);\n                            botMeshes[i] = other.mesh;\n                            \n                            // Update bot name based on new skin\n                            const baseNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta', 'Omega', 'Nova'];\n                            let newName = baseNames[Math.floor(Math.random() * baseNames.length)];\n                            if (newSkin === 'sun') newName = 'Sol ' + newName;\n                            else if (newSkin === 'blackhole') newName = 'Void ' + newName;\n                            else if (newSkin === 'galaxy') newName = 'Nebula ' + newName;\n                            else if (newSkin === 'jupiter') newName = 'Giant ' + newName;\n                            else if (newSkin === 'mars') newName = 'Red ' + newName;\n                            else if (newSkin === 'moon') newName = 'Lunar ' + newName;\n                            other.name = newName;\n                            other.diskRotation = 0;\n                        }\n                    }\n\n                    b.radius = botR;\n                    b.mesh.position.set(b.x, botR, b.z);\n                    \n                    // Apply rolling rotation to bot mesh\n                    const botRotQuatX = new THREE.Quaternion();\n                    const botRotQuatZ = new THREE.Quaternion();\n                    botRotQuatX.setFromAxisAngle(new THREE.Vector3(1, 0, 0), b.rotationX || 0);\n                    botRotQuatZ.setFromAxisAngle(new THREE.Vector3(0, 0, 1), b.rotationZ || 0);\n                    const botFinalQuat = new THREE.Quaternion();\n                    botFinalQuat.multiplyQuaternions(botRotQuatX, botRotQuatZ);\n                    b.mesh.quaternion.copy(botFinalQuat);\n                    \n                    const bScale = botR;\n                    b.mesh.scale.set(bScale, bScale, bScale);\n\n                    // PvP: bot vs player with deformation effects\n                    const pvpDx = playerPos.x - b.x;\n                    const pvpDz = playerPos.z - b.z;\n                    const pvpDist = Math.sqrt(pvpDx*pvpDx + pvpDz*pvpDz);\n                    const collisionDist = playerRadius + botR;\n                    const botIdx = aiBots.indexOf(b);\n                    \n                    // Magnetic attraction for smaller bots\n                    const pvpAttractionDist = playerRadius + botR + 3;\n                    if (playerMass > b.mass * 1.1 && pvpDist < pvpAttractionDist && pvpDist > collisionDist * 0.5) {\n                        // Start deformation\n                        if (!botDeformations.has(botIdx)) {\n                            botDeformations.set(botIdx, { progress: 0, startX: b.x, startZ: b.z });\n                        }\n                        \n                        const def = botDeformations.get(botIdx);\n                        def.progress += dt * 2;\n                        const t = Math.min(def.progress, 1);\n                        \n                        // Pull toward player\n                        const pullStrength = t * t;\n                        b.x += pvpDx * pullStrength * dt * 3;\n                        b.z += pvpDz * pullStrength * dt * 3;\n                        \n                        // Teardrop deformation - stretch toward player\n                        const stretchFactor = 1 + t * 0.8;\n                        const squishFactor = 1 / Math.sqrt(stretchFactor);\n                        \n                        // Direction to player\n                        const dirNorm = Math.sqrt(pvpDx*pvpDx + pvpDz*pvpDz) + 0.01;\n                        const ndx = pvpDx / dirNorm;\n                        const ndz = pvpDz / dirNorm;\n                        \n                        // Apply elongation toward player\n                        const baseScale = botR;\n                        b.mesh.scale.set(\n                            baseScale * (squishFactor + Math.abs(ndx) * (stretchFactor - squishFactor) * t),\n                            baseScale * squishFactor,\n                            baseScale * (squishFactor + Math.abs(ndz) * (stretchFactor - squishFactor) * t)\n                        );\n                    } else if (botDeformations.has(botIdx)) {\n                        // Reset deformation if out of range\n                        const baseScale = botR;\n                        b.mesh.scale.set(baseScale, baseScale, baseScale);\n                        botDeformations.delete(botIdx);\n                    }\n                    \n                    if (pvpDist < collisionDist) {\n                        if (b.mass > playerMass * 1.1 && !spawnInvincible && !deathAnimation.active) {\n                            // Bot eats player - start death animation!\n                            b.lastEatTime = timestamp; // Reset bot's starvation\n                            b.isStarving = false;\n                            \n                            deathAnimation.active = true;\n                            deathAnimation.killerBot = b;\n                            deathAnimation.killerName = b.name;\n                            deathAnimation.progress = 0;\n                            deathAnimation.startPos = { x: playerPos.x, z: playerPos.z };\n                            deathAnimation.startRadius = playerRadius;\n                            deathAnimation.earnedSkulls = Math.floor(playerMass / 10);\n                            \n                            // Initial splatter burst\n                            const contactX = playerPos.x - (pvpDx / pvpDist) * playerRadius;\n                            const contactZ = playerPos.z - (pvpDz / pvpDist) * playerRadius;\n                            createSplatterParticles(contactX, playerRadius, contactZ, 0xff3333, 10);\n                            \n                            // Play heavy splat\n                            if (audioBuffers['heavy_splat_sfx']) {\n                                playSound('heavy_splat_sfx');\n                            }\n                            \n                            // Remove shield\n                            removeShieldMesh();\n                            spawnInvincible = false;\n                            \n                            // Change state to dying\n                            gameState = 'dying';\n                        } else if (playerMass > b.mass * 1.1) {\n                            // Player eats bot - epic effect!\n                            const botColor = b.mesh.material.color ? b.mesh.material.color.getHex() : 0xffffff;\n                            const contactX = b.x;\n                            const contactZ = b.z;\n                            \n                            // EPIC KILL BURST!\n                            createKillBurstEffect(contactX, botR, contactZ, botColor, botR);\n                            \n                            // Strong jiggle\n                            triggerJiggle(0.3);\n                            \n                            const gainedMass = b.mass * 0.3;\n                            const gainedSkulls = Math.floor(b.mass / 10);\n                            playerMass += gainedMass;\n                            playerSkulls += gainedSkulls;\n                            sessionKills++;\n                            registerEat(3);\n                            addShake(0.6 + Math.min(1.2, botR * 0.12));\n                            showFloatText(b.x, botR + 2, b.z, '+' + Math.floor(gainedMass) + ' MASS', '#76ff03');\n                            addKillFeed('\ud83d\udc80 You devoured <b>' + b.name + '</b> &nbsp;+' + gainedSkulls + ' \ud83d\udc80', '#ffeb3b');\n                            lib.saveUserGameState({ skulls: playerSkulls }).catch(() => {});\n                            \n                            // Play heavy splat sound\n                            if (sfxGain) sfxGain.gain.value = (config.audio?.sfxVolume ?? 80) / 100;\n                            if (audioBuffers['heavy_splat_sfx']) {\n                                playSound('heavy_splat_sfx');\n                            }\n                            \n                            // Respawn bot with new cosmic skin\n                            b.mass = getBotRespawnMass();\n                            b.x = (Math.random() - 0.5) * config.arena.size * 0.6;\n                            b.z = (Math.random() - 0.5) * config.arena.size * 0.6;\n                            b.aiState = 'foraging';\n                            b.lastEatTime = timestamp; // Reset new bot's starvation\n                            b.isStarving = false;\n                            b.clusterTarget = null;\n                            b.sweepCenter = null;\n                            b.sweepTimer = 0;\n                            b.greedOverride = false;\n                            b.currentTarget = null;\n                            b.chaseTimer = 0;\n                            // Reset smarter AI properties\n                            b.threatMemory = [];\n                            b.ambushPosition = null;\n                            b.ambushTimer = 0;\n                            b.velocityHistory = [];\n                            b.territoryCenter = { x: b.x, z: b.z };\n                            b.patrolAngle = Math.random() * Math.PI * 2;\n                            b.successfulKills = 0;\n                            b.failedChases = 0;\n                            // Remember the player killed them (revenge mode!)\n                            b.killedByPlayer = timestamp;\n                            // Assign new personality on respawn\n                            const personalities = ['aggressive', 'cautious', 'opportunist', 'territorial', 'nomad'];\n                            b.personality = personalities[Math.floor(Math.random() * personalities.length)];\n                            \n                            // Clear attraction tracking\n                            if (botAttractedSlimes.has(botIdx)) {\n                                botAttractedSlimes.get(botIdx).clear();\n                            }\n                            \n                            botDeformations.delete(botIdx);\n                            resetBotXRayState(botIdx); // Reset X-Ray transparency state\n                            \n                            // Assign new cosmic skin\n                            const newSkin = selectBotSkin();\n                            b.skinType = newSkin;\n                            \n                            // Remove old mesh and create new one\n                            scene.remove(b.mesh);\n                            const newBotR = getRadius(b.mass);\n                            b.mesh = createCelestialMesh(newSkin, newBotR);\n                            b.mesh.position.set(b.x, newBotR, b.z);\n                            scene.add(b.mesh);\n                            botMeshes[botIdx] = b.mesh;\n                            \n                            // Update bot name based on new skin\n                            const baseNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta', 'Omega', 'Nova'];\n                            let newName = baseNames[Math.floor(Math.random() * baseNames.length)];\n                            if (newSkin === 'sun') newName = 'Sol ' + newName;\n                            else if (newSkin === 'blackhole') newName = 'Void ' + newName;\n                            else if (newSkin === 'galaxy') newName = 'Nebula ' + newName;\n                            else if (newSkin === 'jupiter') newName = 'Giant ' + newName;\n                            else if (newSkin === 'mars') newName = 'Red ' + newName;\n                            else if (newSkin === 'moon') newName = 'Lunar ' + newName;\n                            b.name = newName;\n                            b.diskRotation = 0;\n                        }\n                    }\n\n                    // Player-specific reactions now handled in main AI loop above\n                    // Bot behavior toward player is calculated with same \"Worth It\" formula as bot-on-bot\n                }\n\n                // === THIRD PERSON CAMERA SYSTEM ===\n                \n                // Calculate camera distance and height based on player size\n                const camParams = calculateThirdPersonCamera(playerRadius, playerMass);\n                cameraState.targetDist = camParams.dist;\n                cameraState.targetHeight = camParams.height;\n                \n                // Update camera facing angle based on player movement direction\n                const velMag = Math.sqrt(playerVelocity.x ** 2 + playerVelocity.z ** 2);\n                if (velMag > 0.5) {\n                    // Calculate angle from velocity (camera faces opposite direction - behind player)\n                    cameraState.targetFacingAngle = Math.atan2(-playerVelocity.x, -playerVelocity.z);\n                }\n                \n                // Smooth interpolation for distance and height\n                const lerpFactor = 1 - Math.exp(-CAMERA_LERP_SPEED * dt);\n                cameraState.currentDist += (cameraState.targetDist - cameraState.currentDist) * lerpFactor;\n                cameraState.currentHeight += (cameraState.targetHeight - cameraState.currentHeight) * lerpFactor;\n                \n                // Smooth rotation interpolation (handle angle wrapping)\n                let angleDiff = cameraState.targetFacingAngle - cameraState.facingAngle;\n                // Normalize to -PI to PI\n                while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;\n                while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;\n                const rotLerpFactor = 1 - Math.exp(-CAMERA_ROTATION_SPEED * dt);\n                cameraState.facingAngle += angleDiff * rotLerpFactor;\n                \n                // Calculate camera position behind player\n                let camTargetX = playerPos.x + Math.sin(cameraState.facingAngle) * cameraState.currentDist;\n                let camTargetY = playerRadius + cameraState.currentHeight;\n                let camTargetZ = playerPos.z + Math.cos(cameraState.facingAngle) * cameraState.currentDist;\n                \n                // Apply arena collision detection\n                const arenaSize = config.arena.size;\n                const halfArena = arenaSize / 2;\n                const margin = 5;\n                \n                // Clamp camera position to arena bounds\n                camTargetX = Math.max(-halfArena + margin, Math.min(halfArena - margin, camTargetX));\n                camTargetZ = Math.max(-halfArena + margin, Math.min(halfArena - margin, camTargetZ));\n                \n                // Smooth camera position\n                const posLerpFactor = 1 - Math.exp(-4.0 * dt);\n                cameraState.currentX += (camTargetX - cameraState.currentX) * posLerpFactor;\n                cameraState.currentY += (camTargetY - cameraState.currentY) * posLerpFactor;\n                cameraState.currentZ += (camTargetZ - cameraState.currentZ) * posLerpFactor;\n                \n                camera.position.set(cameraState.currentX, cameraState.currentY, cameraState.currentZ);\n\n                // Screen shake\n                if (shakeIntensity > 0.001) {\n                    camera.position.x += (Math.random() - 0.5) * shakeIntensity * 2;\n                    camera.position.y += (Math.random() - 0.5) * shakeIntensity * 2;\n                    camera.position.z += (Math.random() - 0.5) * shakeIntensity * 2;\n                    shakeIntensity *= Math.exp(-6 * dt);\n                } else {\n                    shakeIntensity = 0;\n                }\n                \n                // Look slightly ahead of player in movement direction\n                const lookAheadX = playerPos.x - Math.sin(cameraState.facingAngle) * playerRadius * CAMERA_LOOK_AHEAD;\n                const lookAheadZ = playerPos.z - Math.cos(cameraState.facingAngle) * playerRadius * CAMERA_LOOK_AHEAD;\n                camera.lookAt(lookAheadX, playerRadius * 0.8, lookAheadZ);\n                \n                // Update fog for depth\n                scene.fog.near = 30 + cameraState.currentDist * 0.5;\n                scene.fog.far = 150 + cameraState.currentDist * 2;\n\n                // Update spawn invincibility timer\n                if (spawnInvincible) {\n                    spawnInvincibilityTimer -= dt;\n                    if (spawnInvincibilityTimer <= 0) {\n                        spawnInvincible = false;\n                        removeShieldMesh();\n                    } else {\n                        // Update shield position and effect\n                        updateShieldMesh(playerRadius);\n                    }\n                }\n                \n                // Update direction arrow indicator\n                updateDirectionArrow(playerRadius, timestamp);\n                \n                // === UPDATE X-RAY TRANSPARENCY SYSTEM ===\n                updateXRaySystem(dt, timestamp);\n                \n                if (playerMass > sessionPeakMass) sessionPeakMass = playerMass;\n                updateHUD();\n                updateLeaderboard();\n                updateJuiceHUD();\n                \n                // Update minimap & radar system\n                updatePOIs(dt);\n                drawMinimap(timestamp);\n                \n                // Update particle system\n                updateParticles(dt);\n\n                // Animate slimes (bob)\n                const time = timestamp * 0.002;\n                for (let i = 0; i < slimeMeshes.length; i++) {\n                    if (slimes[i].active) {\n                        slimeMeshes[i].position.y = 0.5 * config.slimes.sizeMultiplier + Math.sin(time + i) * 0.15;\n                    }\n                }\n            } else if (gameState === 'menu') {\n                // Slow camera rotation for menu\n                const time = timestamp * 0.0003;\n                camera.position.set(Math.cos(time) * 60, 40, Math.sin(time) * 60);\n                camera.lookAt(0, 0, 0);\n                \n                // Still update particles in menu\n                updateParticles(dt);\n            } else if (gameState === 'dying') {\n                // Death animation - player getting devoured\n                deathAnimation.progress += dt / deathAnimation.duration;\n                \n                const killer = deathAnimation.killerBot;\n                if (killer && playerMesh) {\n                    // Ease-in-out for smooth devour animation\n                    const t = Math.min(deathAnimation.progress, 1);\n                    const easeT = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;\n                    \n                    // Pull player toward killer's center\n                    const targetX = killer.x;\n                    const targetZ = killer.z;\n                    playerPos.x = deathAnimation.startPos.x + (targetX - deathAnimation.startPos.x) * easeT;\n                    playerPos.z = deathAnimation.startPos.z + (targetZ - deathAnimation.startPos.z) * easeT;\n                    \n                    // Shrink player as they're absorbed\n                    const shrinkScale = Math.max(0.05, 1 - easeT * 1.2);\n                    const startScale = deathAnimation.startRadius / getRadius(config.player.startingMass);\n                    playerMesh.scale.set(\n                        startScale * shrinkScale * (1 + Math.sin(t * 20) * 0.1 * (1 - t)), // Wobble\n                        startScale * shrinkScale * (1 + Math.cos(t * 25) * 0.15 * (1 - t)),\n                        startScale * shrinkScale * (1 + Math.sin(t * 22) * 0.1 * (1 - t))\n                    );\n                    \n                    // Position player mesh (sink into killer)\n                    const sinkAmount = easeT * deathAnimation.startRadius * 0.8;\n                    playerMesh.position.set(playerPos.x, Math.max(0.5, deathAnimation.startRadius - sinkAmount), playerPos.z);\n                    \n                    // Continuous particle spray during devour\n                    if (Math.random() < 0.3) {\n                        createSplatterParticles(\n                            playerPos.x + (Math.random() - 0.5) * 2,\n                            deathAnimation.startRadius * (1 - easeT * 0.7),\n                            playerPos.z + (Math.random() - 0.5) * 2,\n                            0xff3333, 3\n                        );\n                    }\n                    \n                    // Make killer blob bulge/jiggle as it eats\n                    const killerRadius = getRadius(killer.mass);\n                    const bulge = 1 + Math.sin(t * 15) * 0.08 * (1 - t);\n                    killer.mesh.scale.set(killerRadius * bulge, killerRadius / bulge, killerRadius * bulge);\n                    \n                    // Camera stays in third person during death - dramatic close-up\n                    const deathCamDist = 15 + (1 - easeT) * 8;\n                    const deathCamHeight = 10 + deathAnimation.startRadius * 0.3;\n                    camera.position.set(\n                        playerPos.x + Math.sin(cameraState.facingAngle) * deathCamDist,\n                        deathCamHeight,\n                        playerPos.z + Math.cos(cameraState.facingAngle) * deathCamDist\n                    );\n                    camera.lookAt(playerPos.x, deathAnimation.startRadius * 0.5, playerPos.z);\n                }\n                \n                // Update particles during death\n                updateParticles(dt);\n                \n                // Complete death after animation\n                if (deathAnimation.progress >= 1) {\n                    // Final burst of particles\n                    createSplatterParticles(playerPos.x, 1, playerPos.z, 0xff3333, 20);\n                    \n                    // Give mass to killer\n                    if (killer) {\n                        killer.mass += playerMass * 0.3;\n                        const newKillerRadius = getRadius(killer.mass);\n                        killer.mesh.scale.set(newKillerRadius, newKillerRadius, newKillerRadius);\n                    }\n                    \n                    // Hide player mesh\n                    if (playerMesh) {\n                        playerMesh.visible = false;\n                    }\n                    \n                    // Award skulls and show death screen\n                    playerSkulls += deathAnimation.earnedSkulls;\n                    lib.saveUserGameState({ skulls: playerSkulls }).catch(() => {});\n                    \n                    const survivedSec = Math.max(0, (performance.now() - sessionStartTime) / 1000);\n                    deathInfo.innerHTML = `Eaten by <b>${deathAnimation.killerName}</b><br>`\n                        + `+${deathAnimation.earnedSkulls} \ud83d\udc80 &nbsp;\u2022&nbsp; Peak Mass ${Math.floor(sessionPeakMass)}<br>`\n                        + `<span style=\"font-size:22px;opacity:0.85;\">Kills: ${sessionKills} &nbsp;\u2022&nbsp; Survived ${survivedSec.toFixed(0)}s</span>`;\n                    if (dangerVignette) dangerVignette.style.opacity = '0';\n                    if (comboDisplay) comboDisplay.classList.remove('active');\n                    addShake(1.2);\n                    deathScreen.classList.add('visible');\n                    hud.classList.remove('visible');\n                    \n                    deathAnimation.active = false;\n                    gameState = 'dead';\n                }\n            }\n            \n            // Always update particles even outside playing state\n            if (gameState === 'dead') {\n                updateParticles(dt);\n            }\n\n            renderer.render(scene, camera);\n            requestAnimationFrame(gameLoop);\n        }\n\n        // Handle resize\n        window.addEventListener('resize', () => {\n            camera.aspect = gameWorld.clientWidth / gameWorld.clientHeight;\n            camera.updateProjectionMatrix();\n            renderer.setSize(gameWorld.clientWidth, gameWorld.clientHeight);\n        });\n\n        // Initial camera position\n        camera.position.set(0, 40, 60);\n        camera.lookAt(0, 0, 0);\n\n        requestAnimationFrame(gameLoop);\n    }\n    </script>\n</body>\n</html>", "game_config": "{\"arena\":{\"size\":300},\"slimes\":{\"count\":600,\"sizeMultiplier\":1},\"player\":{\"startingMass\":10,\"growthRate\":1,\"speedMultiplier\":1,\"color\":\"#ff3333\"},\"camera\":{\"angle\":55},\"audio\":{\"musicVolume\":50,\"sfxVolume\":80},\"bots\":{\"count\":20}}", "game_design_document": "## Core Game Loop and Mechanics\n\n**The Eating Loop:**\nPlayers control a spherical blob in a 3D arena filled with thousands of tiny, colorful \"Slimes\" (small glowing orbs). The core gameplay revolves around:\n\n1. **Movement & Collection**: Players navigate their blob across the arena floor. Rolling over Slimes causes them to disappear with a satisfying \"pop\" sound, increasing the player's Mass/Size stat.\n\n2. **Growth & Scaling**: As Mass increases, the blob visually grows larger through smooth 3D scaling. The camera automatically zooms out proportionally to maintain proper visibility of surroundings.\n\n3. **PvP Combat**: When two blobs collide, if one is at least 10% larger in mass than the other, the larger blob \"eats\" the smaller one. The eaten blob explodes into collectible \"Skulls\" and respawns at base size (Mass: 10). The victor gains a portion of the defeated blob's mass.\n\n4. **Persistence System**: \n   - Mass/Size is temporary and resets upon death or leaving\n   - \"Skulls\" are permanent currency that persist across sessions\n   - Skulls are used to unlock cosmetic skins in the shop\n\n**Win Conditions**: There is no traditional \"win\" - the game is an endless incremental experience. Success is measured by:\n- Climbing the real-time leaderboard (top 5 largest blobs)\n- Accumulating Skulls for cosmetic unlocks\n- Achieving personal best Mass records\n\n**Engagement Drivers**:\n- Satisfying growth feedback (visual scaling, sound effects, camera zoom)\n- Risk/reward of hunting larger prey vs. safe Slime farming\n- Permanent progression through Skull collection\n- Social competition via leaderboard\n- Cosmetic customization through skins\n\n## Starting Scope\n\n**First Iteration (Minimum Viable Product):**\n\n1. **Core Gameplay**:\n   - Single 3D arena with grid-textured floor\n   - Player blob spawning at Mass: 10\n   - Slime generation system (500-1000 slimes active at once)\n   - Eating mechanics (collision detection for Slimes)\n   - Mass/Size growth with smooth visual scaling\n   - Dynamic camera zoom based on blob size\n   - Basic movement controls (WASD/touch joystick)\n\n2. **Visual Systems**:\n   - Simple 3D sphere rendering for player blob\n   - Colorful glowing Slime orbs\n   - Grid-textured arena floor\n   - Basic lighting\n\n3. **UI Elements**:\n   - Main Menu with \"PLAY\" button\n   - In-game HUD showing current Mass\n   - Simple Skull counter (top-left)\n\n4. **Audio**:\n   - Background music loop\n   - Slime \"pop\" sound effect\n\n**Deferred to Future Iterations:**\n- PvP combat system (eating other players)\n- AI-controlled blobs\n- Leaderboard system\n- Skins shop and cosmetic unlocks\n- Code redemption system\n- Loading screen with progress bar\n- Multiple skin options\n- Skull explosion effects\n- Advanced visual effects (glossy textures, neon glows)\n- Menu background with live arena preview\n\n**Rationale**: The first iteration focuses on the core eating loop and growth mechanics, which are the heart of the gameplay experience. This ensures the fundamental feel is polished and fun before adding competitive and social features.\n\n## Game Perspective\n\n**3D Third-Person (Top-Down Angled)**\n\nThe game uses a dynamic third-person camera positioned above and behind the player's blob at approximately a 45-60 degree angle looking down. This perspective:\n- Provides clear visibility of the arena and nearby Slimes\n- Allows players to see their blob's size relative to others\n- Automatically zooms out as the blob grows to maintain proper framing\n- Follows the player smoothly with slight lag for natural feel\n\nThe camera is not fixed isometric but rather a true 3D camera that can adjust its distance dynamically based on the player's mass.\n\n## Game World and Level Design\n\n**Arena Layout:**\n- **Dimensions**: A large square arena approximately 200x200 units (scales with player growth)\n- **Floor**: Grid-textured ground plane with subtle neon lines forming a square grid pattern\n- **Boundaries**: Invisible walls at arena edges that gently push players back if they try to exit\n- **Lighting**: Ambient lighting from above with slight directional light to give depth to 3D objects\n\n**Slime Distribution:**\n- 500-1000 Slimes active simultaneously\n- Slimes spawn randomly across the arena floor at Y=0\n- Each Slime is a small glowing sphere (radius: 0.5 units) with random vibrant colors (red, blue, green, yellow, pink, cyan, orange)\n- Slimes emit a subtle glow/bloom effect\n- When eaten, new Slimes instantly spawn at random positions to maintain population\n\n**Spawn Points:**\n- Players spawn at random safe positions away from arena edges\n- Spawn position ensures no immediate collision with larger blobs (in future iterations)\n\n**Scale Reference:**\n- Starting blob size (Mass: 10): radius of 1 unit\n- Slimes: radius of 0.5 units\n- Mass-to-radius formula: `radius = 1 + (mass - 10) * 0.05` (grows 0.05 units per mass point)\n- At Mass: 100, blob radius would be ~5.5 units\n- Camera distance formula: `distance = 15 + radius * 2`\n\n## Edit Mode Design\n\n**Editable Properties:**\n\n1. **Arena Settings**:\n   - Arena size (100-500 units)\n   - Grid line color and spacing\n   - Background color/gradient\n\n2. **Slime Settings**:\n   - Slime population (100-2000)\n   - Slime size multiplier (0.5x - 2x)\n   - Slime color palette (preset options: Rainbow, Neon, Pastel, Monochrome)\n   - Slime spawn rate\n\n3. **Player Settings**:\n   - Starting mass (5-50)\n   - Growth rate multiplier (0.5x - 3x)\n   - Movement speed multiplier (0.5x - 2x)\n   - Blob base color (color picker)\n\n4. **Camera Settings**:\n   - Camera angle (30-75 degrees)\n   - Zoom sensitivity (0.5x - 2x)\n   - Camera follow smoothness (0.1 - 1.0)\n\n5. **Audio Settings**:\n   - Music volume (0-100%)\n   - SFX volume (0-100%)\n\n**Constraints:**\n- Arena size minimum ensures playable space\n- Slime population capped to prevent performance issues\n- Growth rate limits prevent instant-win scenarios\n- Movement speed limits maintain game balance\n- Camera angle constrained to maintain 3D perspective visibility\n\n**Edit Mode UI:**\n- Accessible from main menu via \"Settings\" button\n- Organized into collapsible sections (Arena, Slimes, Player, Camera, Audio)\n- Real-time preview of changes where possible\n- \"Reset to Default\" button for each section\n- Changes save automatically to window.gameConfig\n\n## Control Design\n\n**Desktop Controls:**\n- **Movement**: WASD or Arrow Keys to move the blob across the arena\n  - W/Up: Move forward (away from camera)\n  - S/Down: Move backward (toward camera)\n  - A/Left: Move left\n  - D/Right: Move right\n- **Camera**: Mouse movement rotates camera around the blob (optional, can be locked to fixed angle for simplicity in first iteration)\n- **Menu Navigation**: Mouse click for UI buttons\n\n**Mobile Controls:**\n- **Movement**: Virtual joystick on left side of screen\n  - Appears where player touches (avoiding UI areas)\n  - Analog input for smooth directional movement\n  - Joystick base repositions if finger moves beyond max radius\n  - Dead zone in center to prevent drift\n- **Camera**: Fixed angle (no manual camera control on mobile for simplicity)\n- **Menu Navigation**: Tap for UI buttons\n\n**Input Handling:**\n- Movement is relative to camera direction (forward = away from camera view)\n- Normalize diagonal movement to prevent faster diagonal speed\n- Clear all inputs on window blur\n- Prevent default browser behavior for used keys (arrow keys, space)\n- Track touch IDs separately to prevent interference between joystick and UI touches\n\n**Control Scheme Rationale:**\nThis is a casual 3D action game with simple movement mechanics. The top-down angled camera with directional movement (relative to camera) is intuitive and commonly used in similar games. No complex actions like jumping or attacking are needed - movement is the only input, keeping it accessible.\n\n## Art Style and Visual Theme\n\n**Overall Style**: Vibrant, glossy, cartoonish 3D with a modern casual game aesthetic. Think \"mobile-friendly 3D\" with bright colors, smooth shapes, and appealing visual feedback.\n\n**Visual Theme**: Neon-infused playful arena with a futuristic grid aesthetic. The world feels like a digital playground where colorful blobs compete in a glowing arena.\n\n**Color Palette**:\n- **Primary**: Bright neon colors (cyan, magenta, yellow, lime green, hot pink, orange)\n- **Secondary**: Deep purples and blues for UI backgrounds\n- **Accents**: White for text, black for outlines\n- **Arena**: Dark gray/blue grid floor with glowing cyan grid lines\n\n**Asset Specifications**:\n\n1. **Player Blob**:\n   - Smooth 3D sphere with glossy/reflective surface\n   - Default color: Bright red with subtle gradient\n   - Slight emission/glow effect around edges\n   - Should look \"squishy\" and appealing\n   - Size: Dynamically scales based on mass\n\n2. **Slimes**:\n   - Small glowing spheres with vibrant colors\n   - Softer, more translucent appearance than player blob\n   - Emit colored glow matching their hue\n   - Slightly animated (gentle bobbing or pulsing)\n   - Size: Fixed small size (~0.5 unit radius)\n\n3. **Arena Floor**:\n   - Can be rendered with CSS/Canvas: Dark background with glowing grid lines\n   - Grid spacing: 10 units\n   - Grid lines: Thin, glowing cyan/blue lines\n   - Subtle gradient from center (lighter) to edges (darker)\n\n4. **UI Elements**:\n   - Rendered with CSS: Modern, clean interface with rounded corners\n   - Buttons: Glossy with subtle gradients and shadows\n   - Text: Bold, readable fonts with outlines for visibility\n   - HUD elements: Semi-transparent dark backgrounds with bright text\n\n**Rendering Approach**:\n- **3D Elements** (Three.js): Player blob, Slimes, basic arena geometry\n- **2D Elements** (CSS/Canvas): UI, HUD, menu screens, grid floor texture\n- **Effects**: Simple bloom/glow effects for Slimes and blob edges using Three.js post-processing (if performance allows) or CSS filters for UI\n\n**Consistency Notes**:\n- All 3D objects share the same glossy, smooth-shaded aesthetic\n- Lighting should be consistent across all 3D elements\n- UI maintains the same color scheme and rounded, modern style throughout\n- Sound effects should match the playful, satisfying aesthetic (pops, squishes, chimes)", "persistent_storage_config": null, "game_metadata": {"game_id": "01KSQZF83J4FRGTH6E423D5S5Q", "title": "EAT.IO", "description": "Grow your Planet by eating slimes and smaller players in a vibrant 3D arena!!", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_1969f714-2523-430c-a861-ea8c0bb25741.webp", "created_at": 1779994988, "updated_at": 1793629306387.9785, "art_style": null}, "task_plan": null}