{"asset_map": {"snd-gulp-2": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780816810122_01KTGF8446485M101RCBTZQ58R.snd-gulp-2.mp3", "type": "audio", "prompt": null, "description": "Consume gulp sound effect variant 2", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-lip-licking-2": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780818177342_01KTGGHV9Y7VH6T8Q6GEGMG53G.snd-lip-licking-2.mp3", "type": "audio", "prompt": null, "description": "Lip smack/licking sound effect \u2014 consume sound variant 1", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "capsule-art": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780838207905_01KTH3N4D1487SE79E3ZF543HV.EatTheWorld-2by3.png", "type": "image", "prompt": null, "description": "Eat the World capsule art - colorful illustration of a blue hole consuming buildings, cars, and objects in a city. Used for loading screen and main menu background.", "subtype": null, "aspect_ratio": [1024, 1536], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "snd-star-pop": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780839058617_01KTH4F35SX8GKRPWB8CP6FZPD.snd-drop-01.mp3", "type": "audio", "prompt": null, "description": "Pop sound effect for when flying star reaches the goal card", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-size-increase": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780928553237_01KTKST88N17ASBDEXX9W0MTKP.size_increase_3.mp3", "type": "audio", "prompt": null, "description": "Size increase sound effect played when the hole levels up and grows larger", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-man-scream-01": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780930096041_01KTKV9AX9N319JD72B6N7B4ZE.man_scream_01.mp3", "type": "audio", "prompt": null, "description": "Man scream reaction sound 01 - played when person starts falling into hole", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-man-scream-02": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780930096157_01KTKV9B0XKGJGFY8JEKFMKBEB.man_scream_02.mp3", "type": "audio", "prompt": null, "description": "Man scream reaction sound 02 - played when person starts falling into hole", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-man-scream-03": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780929940973_01KTKV4KFDRRXNY220QFCXKCMQ.man_scream_03.mp3", "type": "audio", "prompt": null, "description": "Man scream reaction sound 03 - played when person starts falling into hole", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-car-honk-01": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780930558121_01KTKVQE5975PK9NPMZ79XP8BA.car_honk_01.mp3", "type": "audio", "prompt": null, "description": "Car honk reaction sound 01 - played when a car starts falling into the hole", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-car-honk-02": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780930558219_01KTKVQE8B55A0B2H9YXPG325J.car_honk_02.mp3", "type": "audio", "prompt": null, "description": "Car honk reaction sound 02 - played when a car starts falling into the hole", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-leaves-01": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780930856442_01KTKW0HFTRPYY3VZY5TN0D5B7.leaves_01.mp3", "type": "audio", "prompt": null, "description": "Leaves rustling reaction sound - played when a tree starts falling into the hole", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-clank-01": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780930944162_01KTKW3752ZZVB628A6VJZNCBT.clank_01.mp3", "type": "audio", "prompt": null, "description": "Metal clank reaction sound - played when trashcan or lamp post starts falling into the hole", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "snd-wrong": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1781415416958_01KV2A453YRT9PR9E8564PG53J.snd-wrong.mp3", "type": "audio", "prompt": null, "description": "A short, low-pitched rejection sound played when the player tries to eat an object that is too large.", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "win-fanfare-v2.mp3": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1781417134677_01KV2BRJJN3TXZJW4H2YEMWXQA.win-fanfare-v2.mp3", "type": "audio", "prompt": null, "description": "A celebratory victory fanfare played at the end of the game when the player wins.", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "bg-music": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1781803335639_01KVDW2FYQT2KTM1Z3C2TBHCZN.Simple Future Synthpop v2.mp3", "type": "audio", "prompt": null, "description": "Looping background music track - Simple Future Synthpop v2", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "Oceanside -- Chill_Drum and Bass.mp3": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1781870501317_01KVFW47E5SC899RZM87SX33QF.Oceanside -- Chill_Drum and Bass.mp3", "type": "audio", "prompt": null, "description": "Oceanside -- Chill_Drum and Bass.mp3 background music track", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "logo-art": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1782051443556_01KVN8P4V4X455W57BE2V0QXGF.Logo.png", "type": "image", "prompt": null, "description": "Eat the World Logo image with 3D text styling, blue buildings, and yellow and white lettering.", "subtype": null, "aspect_ratio": [1536, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Eat the World</title>\n    <style>\n        :root {\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n        }\n\n        body, html {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            -ms-touch-action: none;\n            background: #1a1a1a;\n            font-family: 'Inter', sans-serif;\n        }\n\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n        }\n\n        #game-canvas {\n            display: block;\n            width: 100%;\n            height: 100%;\n        }\n\n        .joystick-outer {\n            position: absolute;\n            width: 240px;\n            height: 240px;\n            border-radius: 50%;\n            border: 3px solid rgba(255,255,255,0.4);\n            background: rgba(255,255,255,0.08);\n            pointer-events: none;\n            transform: translate(-50%, -50%);\n            display: none;\n            transition: left 0.25s ease-out, top 0.25s ease-out;\n        }\n\n        .joystick-outer.no-transition {\n            transition: none;\n        }\n\n        .joystick-inner {\n            position: absolute;\n            width: 100px;\n            height: 100px;\n            border-radius: 50%;\n            background: rgba(255,255,255,0.6);\n            pointer-events: none;\n            transform: translate(-50%, -50%);\n            left: 50%;\n            top: 50%;\n            transition: left 0.2s ease-out, top 0.2s ease-out;\n        }\n\n        .joystick-inner.no-transition {\n            transition: none;\n        }\n\n        #goal-card {\n            position: absolute;\n            top: 90px;\n            left: 50%;\n            transform: translateX(-50%);\n            background: transparent;\n            border-radius: 0;\n            padding: 0;\n            display: flex;\n            flex-direction: column;\n            gap: 16px;\n            pointer-events: none;\n            width: 88%;\n            max-width: 620px;\n            box-sizing: border-box;\n            box-shadow: none;\n            border: none;\n            z-index: 10;\n        }\n\n        #timer-panel {\n            position: absolute;\n            top: 15px;\n            left: 50%;\n            transform: translateX(-50%);\n            background: rgba(18,18,20,0.95);\n            border-radius: 16px;\n            padding: 6px 20px;\n            display: flex;\n            align-items: center;\n            gap: 10px;\n            pointer-events: none;\n            box-shadow: 0 6px 16px rgba(0,0,0,0.45);\n            border: 1.5px solid rgba(255,255,255,0.12);\n            z-index: 10;\n        }\n\n        #timer-icon {\n            font-size: 26px;\n            line-height: 1;\n        }\n\n        #timer-text {\n            font-size: 26px;\n            font-weight: 900;\n            color: #fff;\n            font-family: 'Inter', sans-serif;\n            letter-spacing: 1px;\n        }\n\n        .progress-bar-wrapper {\n            width: 100%;\n            display: flex;\n            flex-direction: column;\n            gap: 6px;\n        }\n\n        .progress-info-row {\n            display: flex;\n            flex-direction: row;\n            justify-content: space-between;\n            align-items: center;\n            width: 100%;\n            padding: 0 4px;\n            box-sizing: border-box;\n        }\n\n        .progress-bar-bg {\n            width: 100%;\n            height: 34px;\n            background: rgba(0, 0, 0, 0.45);\n            border-radius: 17px;\n            overflow: hidden;\n            position: relative;\n            border: 1.5px solid rgba(255,255,255,0.15);\n            box-shadow: inset 0 1px 3px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3);\n            box-sizing: border-box;\n        }\n\n        .progress-bar-fill {\n            width: 0%;\n            height: 100%;\n            border-radius: 17px;\n            transition: width 0.3s ease;\n        }\n\n        .progress-value-text {\n            font-family: 'Inter', sans-serif;\n            font-size: 22px;\n            font-weight: 900;\n            color: #ffffff;\n            text-shadow: 0 2px 4px rgba(0,0,0,0.8);\n            text-align: right;\n            margin: 0;\n        }\n\n        .progress-label {\n            font-family: 'Inter', sans-serif;\n            font-size: 22px;\n            font-weight: 900;\n            color: rgba(255, 255, 255, 0.95);\n            text-shadow: 0 2px 4px rgba(0,0,0,0.8);\n            letter-spacing: 0.02em;\n            margin: 0;\n            text-transform: none;\n        }\n\n        #goal-bar-fill {\n            background: #3a8fff;\n        }\n\n        #music-toggle-btn {\n            position: absolute;\n            top: 15px;\n            right: 15px;\n            width: 56px;\n            height: 56px;\n            background: transparent;\n            border: none;\n            color: #fff;\n            font-size: 32px;\n            display: none; /* Hide by default during loading screen */\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            z-index: 1000;\n            transition: transform var(--transition-fast);\n            pointer-events: auto;\n            user-select: none;\n            -webkit-tap-highlight-color: transparent;\n            text-shadow: 0 2px 4px rgba(0,0,0,0.6);\n        }\n\n        #music-toggle-btn:active {\n            transform: scale(0.85);\n        }\n\n        #ghost-bar-fill {\n            background: #a8e8ff;\n        }\n\n        #mode-label {\n            position: absolute;\n            top: 16px;\n            left: 16px;\n            color: rgba(255,255,255,0.7);\n            font-size: 18px;\n            font-family: 'Inter', sans-serif;\n            pointer-events: none;\n            text-shadow: 0 1px 3px rgba(0,0,0,0.5);\n        }\n\n        /* \u2500\u2500 Editor Toolbar \u2500\u2500 */\n        #editor-toolbar {\n            position: absolute;\n            top: 16px;\n            left: 50%;\n            transform: translateX(-50%);\n            display: none;\n            flex-direction: column;\n            align-items: flex-start;\n            background: rgba(18,18,20,0.96);\n            border-radius: 14px;\n            padding: 0;\n            z-index: 100;\n            box-shadow: 0 4px 20px rgba(0,0,0,0.55);\n            backdrop-filter: blur(12px);\n            border: 1px solid rgba(255,255,255,0.08);\n            max-width: calc(100vw - 24px);\n            overflow: visible;\n        }\n\n        #editor-toolbar.active {\n            display: flex;\n        }\n\n        #toolbar-content-row {\n            display: flex;\n            flex-direction: row;\n            gap: 12px;\n            align-items: stretch;\n            padding: 14px 16px;\n            width: 100%;\n            box-sizing: border-box;\n        }\n\n        #registry-dropdown {\n            display: none;\n        }\n\n        /* \u2500\u2500 Shared token for all interactive toolbar items \u2500\u2500 */\n        .tb-item {\n            height: 72px;\n            font-family: 'Inter', sans-serif;\n            font-size: 22px;\n            font-weight: 600;\n            letter-spacing: 0.04em;\n            border-radius: 14px;\n            border: 2px solid rgba(255,255,255,0.12);\n            background: rgba(48,48,54,0.92);\n            color: rgba(255,255,255,0.9);\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: background 120ms, border-color 120ms, color 120ms;\n            white-space: nowrap;\n            user-select: none;\n            box-sizing: border-box;\n        }\n\n        .tb-item:active {\n            background: rgba(70,70,80,1);\n        }\n\n        /* \u2500\u2500 Dropdown \u2500\u2500 */\n        .custom-dropdown {\n            position: relative;\n            min-width: 140px;\n            flex: 1;\n        }\n\n        .custom-dropdown-selected {\n            padding: 0 14px;\n            gap: 8px;\n        }\n\n        .custom-dropdown-selected::after {\n            content: '\u25be';\n            font-size: 14px;\n            opacity: 0.55;\n            flex-shrink: 0;\n        }\n\n        .custom-dropdown-options {\n            display: none;\n            position: fixed;\n            background: rgba(24,24,28,0.98);\n            border: 1.5px solid rgba(255,255,255,0.12);\n            border-radius: 10px;\n            z-index: 9999;\n            max-height: 300px;\n            box-shadow: 0 6px 18px rgba(0,0,0,0.5);\n            min-width: 180px;\n            flex-direction: column;\n            overflow: hidden;\n        }\n\n        .custom-dropdown-options.open {\n            display: flex;\n        }\n\n        .dropdown-search-input {\n            width: 100%;\n            box-sizing: border-box;\n            background: rgba(10, 10, 12, 0.9);\n            border: none;\n            border-bottom: 1.5px solid rgba(255, 255, 255, 0.12);\n            padding: 14px 18px;\n            font-size: 18px;\n            font-family: 'Inter', sans-serif;\n            color: #fff;\n            outline: none;\n        }\n\n        .dropdown-search-input::placeholder {\n            color: rgba(255, 255, 255, 0.45);\n        }\n\n        .custom-dropdown-list {\n            overflow-y: auto;\n            max-height: 240px;\n            flex: 1;\n        }\n\n        .custom-dropdown-option {\n            padding: 18px 20px;\n            font-size: 20px;\n            font-family: 'Inter', sans-serif;\n            font-weight: 500;\n            color: rgba(255,255,255,0.88);\n            cursor: pointer;\n            letter-spacing: 0.03em;\n            transition: background 100ms;\n        }\n\n        .custom-dropdown-option:hover,\n        .custom-dropdown-option.active {\n            background: rgba(58,143,255,0.35);\n            color: #fff;\n        }\n\n        /* \u2500\u2500 Buttons \u2500\u2500 */\n        #editor-toolbar button.tb-item {\n            padding: 0 24px;\n            min-width: 72px;\n        }\n\n        #delete-btn.tb-item {\n            min-width: 72px;\n            padding: 0;\n            font-size: 28px;\n            border-color: rgba(255,80,80,0.25);\n        }\n\n        #delete-btn.tb-item:hover {\n            background: rgba(180,40,40,0.5);\n            border-color: rgba(255,80,80,0.55);\n        }\n\n        .tb-item.mode-active {\n            background: rgba(58,143,255,0.75);\n            border-color: rgba(90,165,255,0.85);\n            color: #fff;\n        }\n\n        /* \u2500\u2500 Divider \u2500\u2500 */\n        .tb-divider {\n            width: 1px;\n            align-self: stretch;\n            background: rgba(255,255,255,0.1);\n            margin: 4px 2px;\n            flex-shrink: 0;\n        }\n\n        /* \u2500\u2500 Status hint \u2500\u2500 */\n                /* \u2500\u2500 Custom In-Game Properties Panel \u2500\u2500 */\n        #prop-panel {\n            position: absolute;\n            bottom: 20px;\n            right: 20px;\n            background: rgba(18,18,20,0.96);\n            border: 1px solid rgba(255,255,255,0.08);\n            border-radius: 16px;\n            padding: 24px;\n            color: white;\n            font-family: 'Inter', sans-serif;\n            z-index: 200;\n            display: none;\n            flex-direction: column;\n            gap: 14px;\n            box-shadow: 0 4px 20px rgba(0,0,0,0.55);\n            backdrop-filter: blur(12px);\n            min-width: 280px;\n            pointer-events: auto;\n        }\n\n        #prop-panel.active {\n            display: flex;\n        }\n\n        .prop-row {\n            display: flex;\n            flex-direction: column;\n            gap: 4px;\n        }\n\n        .prop-row label {\n            font-size: 18px;\n            color: rgba(255,255,255,0.7);\n        }\n\n        .prop-row input {\n            width: 100%;\n        }\n\n        .prop-header {\n            font-size: 22px;\n            font-weight: bold;\n            margin-bottom: 8px;\n            border-bottom: 1px solid rgba(255,255,255,0.1);\n            padding-bottom: 12px;\n        }\n\n        /* \u2500\u2500 Editor Mode Tabs (top row) \u2500\u2500 */\n        .editor-mode-tabs {\n            display: flex;\n            gap: 0;\n            padding: 0;\n            border-bottom: 1px solid rgba(255,255,255,0.08);\n        }\n\n        .editor-mode-tabs .tab-btn {\n            padding: 12px 22px;\n            font-family: 'Inter', sans-serif;\n            font-size: 20px;\n            font-weight: 600;\n            border: none;\n            background: transparent;\n            color: rgba(255,255,255,0.45);\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            gap: 6px;\n            transition: background 120ms, color 120ms, border-color 120ms;\n            white-space: nowrap;\n            border-bottom: 3px solid transparent;\n            margin-bottom: -1px;\n        }\n\n        .editor-mode-tabs .tab-btn.active {\n            color: #fff;\n            border-bottom-color: #3a8fff;\n            background: rgba(58,143,255,0.1);\n        }\n\n        .editor-mode-tabs .tab-btn:not(.active):hover {\n            background: rgba(255,255,255,0.05);\n            color: rgba(255,255,255,0.7);\n        }\n\n        /* \u2500\u2500 Terrain Controls Group \u2500\u2500 */\n        #terrain-controls {\n            display: none;\n            flex-direction: row;\n            gap: 12px;\n            align-items: stretch;\n        }\n\n        #terrain-controls.active {\n            display: flex;\n        }\n\n        #object-controls {\n            display: flex;\n            flex-direction: row;\n            gap: 12px;\n            align-items: stretch;\n        }\n\n        #object-controls.hidden {\n            display: none;\n        }\n\n        /* \u2500\u2500 Terrain paint cursor highlight \u2500\u2500 */\n        .terrain-cursor {\n            pointer-events: none;\n        }\n\n        #level-hud {\n            position: absolute;\n            pointer-events: none;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            transform: translate(-50%, -50%);\n            z-index: 10;\n            left: 50%;\n            top: 58%;\n        }\n\n        #level-hud-bar-bg {\n            width: 160px;\n            height: 42px;\n            background: rgba(0,0,0,0.65);\n            border-radius: 21px;\n            overflow: hidden;\n            border: 1.5px solid rgba(255,255,255,0.25);\n            position: relative;\n            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);\n        }\n\n        #level-hud-bar-fill {\n            width: 0%;\n            height: 100%;\n            background: #3a8fff;\n            border-radius: 21px;\n            transition: width 0.3s ease;\n        }\n\n        #level-hud-text {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-family: 'Inter', sans-serif;\n            font-size: 20px;\n            font-weight: 900;\n            color: #fff;\n            text-shadow: 0 0 3px rgba(0,0,0,1), 0 1px 4px rgba(0,0,0,0.9);\n            white-space: nowrap;\n            z-index: 1;\n        }\n\n\n        /* \u2500\u2500 Game State Screens \u2500\u2500 */\n        #screen-loading {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 500;\n            background: #12141a;\n            padding: 0 8%;\n            box-sizing: border-box;\n            gap: 20px;\n        }\n\n        .loading-label {\n            font-family: 'Inter', sans-serif;\n            font-size: clamp(18px, 4vw, 26px);\n            font-weight: 700;\n            letter-spacing: 0.18em;\n            text-transform: uppercase;\n            color: rgba(255, 255, 255, 0.55);\n        }\n\n        .loading-progress-wrap {\n            width: min(92%, 560px);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 14px;\n        }\n\n        .loading-progress-track {\n            width: 100%;\n            height: clamp(22px, 5vw, 34px);\n            border-radius: 999px;\n            background: rgba(255, 255, 255, 0.1);\n            border: 2px solid rgba(255, 255, 255, 0.14);\n            overflow: hidden;\n            box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.35);\n        }\n\n        .loading-progress-fill {\n            width: 0%;\n            height: 100%;\n            border-radius: 999px;\n            background: linear-gradient(90deg, #3a8fff 0%, #5cb8ff 100%);\n            box-shadow: 0 0 18px rgba(58, 143, 255, 0.55);\n        }\n\n        .loading-progress-pct {\n            font-family: 'Inter', sans-serif;\n            font-size: clamp(28px, 7vw, 48px);\n            font-weight: 900;\n            color: #fff;\n            letter-spacing: 0.04em;\n            text-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\n        }\n\n        #screen-tap-start {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            z-index: 550;\n            display: flex;\n            align-items: flex-end;\n            justify-content: center;\n            padding-bottom: 22%;\n            box-sizing: border-box;\n            background: linear-gradient(\n                180deg,\n                rgba(6, 8, 14, 0) 0%,\n                rgba(6, 8, 14, 0.18) 55%,\n                rgba(6, 8, 14, 0.48) 100%\n            );\n            cursor: pointer;\n            user-select: none;\n            -webkit-tap-highlight-color: transparent;\n            pointer-events: auto;\n        }\n\n        #intro-logo {\n            position: absolute;\n            top: 8%;\n            left: 50%;\n            transform: translateX(-50%);\n            width: min(110%, 676px);\n            height: auto;\n            max-height: 32%;\n            object-fit: contain;\n            pointer-events: none;\n            z-index: 551;\n            filter: drop-shadow(0 10px 20px rgba(0,0,0,0.5));\n            animation: logo-float 4s ease-in-out infinite;\n        }\n\n        @keyframes logo-float {\n            0%, 100% {\n                transform: translate(-50%, 0);\n            }\n            50% {\n                transform: translate(-50%, -12px);\n            }\n        }\n\n        #tap-to-start-pill {\n            display: inline-flex;\n            align-items: center;\n            justify-content: center;\n            padding: 17px 44px;\n            border-radius: 999px;\n            background: rgba(255, 255, 255, 0.96);\n            border: 2.5px solid rgba(255, 255, 255, 0.85);\n            box-shadow:\n                0 13px 36px rgba(0, 0, 0, 0.22),\n                0 3px 8px rgba(0, 0, 0, 0.1),\n                inset 0 1px 0 rgba(255, 255, 255, 1);\n            animation: tap-pill-pulse 2.4s ease-in-out infinite;\n            will-change: transform;\n        }\n\n        #tap-to-start-text {\n            font-family: ui-rounded, 'SF Pro Rounded', 'Nunito', 'Varela Round', 'Segoe UI', system-ui, -apple-system, sans-serif;\n            font-size: clamp(26px, 6.8vw, 39px);\n            font-weight: 800;\n            letter-spacing: 0.01em;\n            color: #1c2438;\n            text-transform: none;\n            text-align: center;\n            line-height: 1.1;\n            white-space: nowrap;\n            padding: 0;\n            text-shadow: none;\n        }\n\n        @keyframes tap-pill-pulse {\n            0%, 100% {\n                transform: scale(1);\n                box-shadow:\n                    0 10px 28px rgba(0, 0, 0, 0.22),\n                    0 2px 6px rgba(0, 0, 0, 0.1),\n                    inset 0 1px 0 rgba(255, 255, 255, 1);\n            }\n            50% {\n                transform: scale(1.045);\n                box-shadow:\n                    0 14px 34px rgba(0, 0, 0, 0.26),\n                    0 0 0 5px rgba(255, 255, 255, 0.14),\n                    inset 0 1px 0 rgba(255, 255, 255, 1);\n            }\n        }\n\n        .screen-hidden {\n            display: none !important;\n        }\n\n        /* \u2500\u2500 Victory & Defeat Screen Overlays \u2500\u2500 */\n        #screen-victory, #screen-defeat {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            z-index: 600;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            pointer-events: auto;\n        }\n\n        #victory-overlay, #defeat-overlay {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: rgba(3, 4, 8, 0.85);\n            backdrop-filter: blur(8px);\n            opacity: 0;\n            transition: opacity 0.5s ease;\n        }\n\n        #victory-card, #defeat-card {\n            position: relative;\n            width: 90%;\n            max-width: 580px;\n            height: 85%; /* Uses roughly 85% vertical space */\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: flex-start;\n            gap: 24px; /* Clean breathing room between celebration, leaderboard, and play again button */\n            z-index: 1;\n            transform: scale(0);\n            opacity: 0;\n            overflow: hidden;\n            box-sizing: border-box;\n            padding: 16px 8px;\n        }\n\n        .celebration-section {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            width: 100%;\n            gap: 20px; /* Balanced spacing between title, total time card, and new best badge */\n            margin-bottom: 0px;\n        }\n\n        .victory-header, .defeat-header {\n            text-align: center;\n            margin: 0;\n            font-family: 'Inter', sans-serif;\n            font-weight: 900;\n            color: #fff;\n            text-transform: uppercase;\n            letter-spacing: -1px;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 4px;\n        }\n\n        .header-you-ate, .header-your-ghost {\n            font-size: 36px;\n            font-weight: 900;\n            color: #ffffff;\n            text-shadow: -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000, 0 4px 8px rgba(0,0,0,0.6);\n            opacity: 0;\n            transform: translateY(20px);\n            transition: opacity 0.4s ease, transform 0.4s ease;\n            letter-spacing: 0.05em;\n        }\n\n        .header-the-world {\n            font-size: 66px;\n            color: #FFD700;\n            text-shadow: -4px -4px 0 #000, 4px -4px 0 #000, -4px 4px 0 #000, 4px 4px 0 #000, 0 8px 16px rgba(0,0,0,0.7);\n            margin: 0;\n            opacity: 0;\n            transform: translateY(20px);\n            transition: opacity 0.4s ease 0.15s, transform 0.4s ease 0.15s;\n            font-weight: 950;\n            letter-spacing: -0.02em;\n        }\n\n        .header-ate-the-world {\n            font-size: 60px;\n            color: #a8e8ff;\n            text-shadow: -4px -4px 0 #000, 4px -4px 0 #000, -4px 4px 0 #000, 4px 4px 0 #000, 0 8px 16px rgba(0,0,0,0.7);\n            margin: 0;\n            opacity: 0;\n            transform: translateY(20px);\n            transition: opacity 0.4s ease 0.15s, transform 0.4s ease 0.15s;\n            font-weight: 950;\n            letter-spacing: -0.02em;\n        }\n\n        .victory-middle {\n            background: rgba(11, 14, 20, 0.95);\n            border: 4px solid #FFD700;\n            border-radius: 24px;\n            padding: 18px 24px;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            box-shadow: 0 12px 36px rgba(0,0,0,0.65);\n            width: 82%;\n            box-sizing: border-box;\n            opacity: 0;\n            transform: scale(0.5);\n            transition: opacity 0.4s ease 0.3s, transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.3s;\n        }\n\n        .defeat-middle {\n            background: rgba(11, 14, 20, 0.95);\n            border: 4px solid #00b0ff;\n            border-radius: 24px;\n            padding: 22px 24px;\n            display: flex;\n            flex-direction: row;\n            align-items: center;\n            justify-content: space-around;\n            gap: 12px;\n            box-shadow: 0 12px 36px rgba(0,0,0,0.65);\n            width: 82%;\n            box-sizing: border-box;\n            opacity: 0;\n            transform: scale(0.5);\n            transition: opacity 0.4s ease 0.3s, transform 0.4s ease 0.3s;\n        }\n\n        .total-time-lbl {\n            font-size: 20px;\n            font-weight: 900;\n            color: rgba(255,255,255,0.6);\n            letter-spacing: 0.12em;\n            margin-bottom: 4px;\n            text-transform: uppercase;\n        }\n\n        .flex-row-center {\n            display: flex;\n            flex-direction: row;\n            align-items: center;\n            justify-content: center;\n            gap: 10px;\n        }\n\n        .time-stopwatch {\n            font-size: 48px;\n            line-height: 1;\n        }\n\n        #victory-time-val {\n            font-size: 54px;\n            font-weight: 950;\n            color: #fff;\n            letter-spacing: 1.5px;\n            text-shadow: 0 4px 8px rgba(0,0,0,0.5);\n            line-height: 1;\n        }\n\n        #victory-pb-container {\n            width: 100%;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n            opacity: 0;\n            transform: scale(0.5);\n            transition: opacity 0.4s ease 0.4s, transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.4s;\n        }\n\n        .new-best-banner {\n            background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);\n            border: 2px solid #FFF;\n            border-radius: 40px;\n            padding: 16px 44px;\n            font-size: 32px;\n            font-weight: 900;\n            color: #FFF;\n            text-transform: uppercase;\n            letter-spacing: 1px;\n            text-shadow: 0 2px 4px rgba(0,0,0,0.5);\n            box-shadow: 0 0 20px rgba(255, 215, 0, 0.6), inset 0 0 10px rgba(255, 255, 255, 0.5);\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            gap: 8px;\n            animation: banner-glow 2s infinite ease-in-out;\n        }\n\n        @keyframes banner-glow {\n            0%, 100% {\n                box-shadow: 0 0 20px rgba(255, 215, 0, 0.6), inset 0 0 10px rgba(255, 255, 255, 0.5);\n            }\n            50% {\n                box-shadow: 0 0 35px rgba(255, 215, 0, 0.9), inset 0 0 15px rgba(255, 255, 255, 0.7);\n            }\n        }\n\n        .best-understated-pill {\n            background: rgba(28, 28, 30, 0.85);\n            border: 1.5px solid rgba(255, 255, 255, 0.2);\n            border-radius: 30px;\n            padding: 12px 36px;\n            font-size: 24px;\n            font-weight: 800;\n            color: rgba(255, 255, 255, 0.9);\n            text-transform: uppercase;\n            letter-spacing: 1px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            gap: 6px;\n        }\n\n        .best-understated-pill .best-lbl {\n            color: rgba(255, 255, 255, 0.5);\n            font-size: 18px;\n            font-weight: 700;\n            margin-right: 4px;\n        }\n\n        .defeat-stat-item {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .defeat-stat-lbl {\n            font-size: 18px;\n            font-weight: 800;\n            color: rgba(255,255,255,0.6);\n            letter-spacing: 0.1em;\n            text-transform: uppercase;\n            margin-bottom: 4px;\n        }\n\n        #defeat-player-pct {\n            font-size: 48px;\n            font-weight: 950;\n            color: #fff;\n            letter-spacing: 1px;\n            text-shadow: 0 4px 8px rgba(0,0,0,0.5);\n            line-height: 1;\n        }\n\n        .defeat-stat-sub {\n            font-size: 15px;\n            font-weight: 800;\n            color: rgba(255,255,255,0.4);\n            letter-spacing: 0.05em;\n            margin-top: 2px;\n            text-transform: uppercase;\n        }\n\n        .defeat-stat-divider {\n            width: 2px;\n            height: 76px;\n            background: rgba(255, 255, 255, 0.15);\n            align-self: center;\n        }\n\n        #defeat-time-to-beat {\n            font-size: 48px;\n            font-weight: 950;\n            color: #a8e8ff;\n            letter-spacing: 1.5px;\n            text-shadow: 0 4px 8px rgba(0,0,0,0.5);\n            line-height: 1;\n        }\n\n        #victory-replay-btn, #defeat-replay-btn {\n            width: 85%;\n            max-width: 480px;\n            height: 112px;\n            font-size: 38px;\n            font-weight: 900;\n            letter-spacing: 0.05em;\n            color: #fff;\n            border: none;\n            border-radius: 28px;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            gap: 16px;\n            opacity: 0;\n            transform: translateY(30px);\n            transition: opacity 0.4s ease 0.5s, transform 0.4s ease 0.5s, scale 0.1s ease;\n            outline: none;\n            -webkit-tap-highlight-color: transparent;\n            box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);\n        }\n\n        #victory-replay-btn {\n            background: linear-gradient(180deg, #2ecc71 0%, #27ae60 100%);\n            box-shadow: 0 8px 24px rgba(46, 204, 113, 0.4);\n            animation: pulse-green 3s infinite 1.5s;\n        }\n\n        #defeat-replay-btn {\n            background: linear-gradient(180deg, #00b0ff 0%, #0088cc 100%);\n            box-shadow: 0 8px 24px rgba(0, 176, 255, 0.4);\n            animation: pulse-blue 3s infinite 1.5s;\n        }\n\n        #victory-replay-btn:active, #defeat-replay-btn:active {\n            transform: scale(0.95) translateY(0) !important;\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n        }\n\n        .play-arrow {\n            display: inline-block;\n            font-size: 40px;\n            line-height: 1;\n        }\n\n        @keyframes pulse-blue {\n            0% {\n                transform: scale(1);\n                box-shadow: 0 8px 24px rgba(0,176,255,0.4);\n            }\n            50% {\n                transform: scale(1.04);\n                box-shadow: 0 12px 32px rgba(0,176,255,0.6);\n            }\n            100% {\n                transform: scale(1);\n                box-shadow: 0 8px 24px rgba(0,176,255,0.4);\n            }\n        }\n\n        @keyframes pulse-green {\n            0% {\n                transform: scale(1);\n                box-shadow: 0 8px 24px rgba(46,204,113,0.4);\n            }\n            50% {\n                transform: scale(1.04);\n                box-shadow: 0 12px 32px rgba(46,204,113,0.6);\n            }\n            100% {\n                transform: scale(1);\n                box-shadow: 0 8px 24px rgba(46,204,113,0.4);\n            }\n        }\n\n        .round-leaderboard {\n            width: 100%;\n            background: linear-gradient(180deg, #0e111d 0%, #080a12 100%);\n            border: 2.5px solid #20263f;\n            border-radius: 20px;\n            padding: 14px 16px;\n            box-sizing: border-box;\n            opacity: 0;\n            transform: translateY(16px);\n            transition: opacity 0.35s ease 0.5s, transform 0.35s ease 0.5s;\n            flex: 1 1 auto;\n            display: flex;\n            flex-direction: column;\n            min-height: 380px; /* Expands visually to occupy a major chunk of the card */\n            justify-content: flex-start;\n            box-shadow: 0 12px 32px rgba(0,0,0,0.6);\n        }\n\n        .leaderboard-title {\n            font-size: 22px;\n            font-weight: 900;\n            letter-spacing: 0.05em;\n            color: #ffffff;\n            margin-bottom: 8px;\n            text-align: center;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            gap: 10px;\n            text-transform: uppercase;\n        }\n\n        .leaderboard-content {\n            flex: 1;\n            display: flex;\n            flex-direction: column;\n            justify-content: flex-start;\n            gap: 8px;\n            color: rgba(255,255,255,0.88);\n            font-size: 16px;\n        }\n\n        .leaderboard-footer {\n            font-size: 16px;\n            font-weight: 700;\n            color: rgba(255,255,255,0.6);\n            text-align: center;\n            margin-top: 6px;\n            letter-spacing: 0.02em;\n        }\n\n        .leaderboard-footer .highlight-yellow {\n            color: #FFD700;\n            font-weight: 800;\n        }\n\n        .lb-loading, .lb-empty, .lb-error {\n            height: 100%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            text-align: center;\n            color: rgba(255,255,255,0.78);\n            font-size: 16px;\n            font-weight: 700;\n            flex: 1;\n        }\n\n        .lb-list {\n            display: flex;\n            flex-direction: column;\n            gap: 6px;\n            width: 100%;\n        }\n\n        .lb-row {\n            display: grid;\n            grid-template-columns: 70px 1fr auto;\n            gap: 12px;\n            align-items: center;\n            padding: 12px 20px;\n            border-radius: 12px;\n            background: rgba(255,255,255,0.04);\n            border: 1.5px solid rgba(255,255,255,0.06);\n            line-height: 1;\n            height: 74px;\n            box-sizing: border-box;\n        }\n\n        .lb-rank {\n            font-weight: 900;\n            color: #ffffff;\n            font-size: 28px;\n            text-shadow: 0 1px 2px rgba(0,0,0,0.5);\n        }\n\n        .lb-list .lb-row:nth-child(1) .lb-rank {\n            color: #ffd700; /* Gold */\n        }\n        .lb-list .lb-row:nth-child(2) .lb-rank {\n            color: #e0e0e0; /* Silver */\n        }\n        .lb-list .lb-row:nth-child(3) .lb-rank {\n            color: #cd7f32; /* Bronze */\n        }\n\n        .lb-name {\n            overflow: hidden;\n            text-overflow: ellipsis;\n            white-space: nowrap;\n            font-weight: 800;\n            font-size: 28px;\n            color: #ffffff;\n            text-align: left;\n        }\n\n        .lb-time {\n            font-weight: 900;\n            font-size: 28px;\n            color: #ffd700;\n            text-shadow: 0 1px 2px rgba(0,0,0,0.5);\n        }\n\n        .lb-you {\n            border-color: #ffd700;\n            background: rgba(255, 215, 0, 0.15);\n            box-shadow: 0 0 15px rgba(255, 215, 0, 0.35);\n        }\n\n        .lb-you .lb-name, .lb-you .lb-rank {\n            color: #ffd700;\n            font-weight: 900;\n        }\n\n        .lb-you-ghost {\n            border-color: #a8e8ff;\n            background: rgba(168, 232, 255, 0.15);\n            box-shadow: 0 0 15px rgba(168, 232, 255, 0.35);\n        }\n\n        .lb-you-ghost .lb-name, .lb-you-ghost .lb-rank {\n            color: #a8e8ff;\n            font-weight: 900;\n        }\n\n        .lb-divider {\n            text-align: center;\n            color: rgba(255,255,255,0.4);\n            font-size: 14px;\n            font-weight: 800;\n            margin: 4px 0;\n        }\n\n        #confetti-container {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            pointer-events: none;\n            overflow: visible;\n        }\n\n        .confetti-particle {\n            position: absolute;\n            width: 10px;\n            height: 10px;\n            border-radius: 2px;\n            opacity: 0.9;\n        }\n\n        /* \u2500\u2500 Onboarding Overlay System \u2500\u2500 */\n        .onboarding-overlay {\n            position: absolute;\n            bottom: clamp(68px, 11vh, 100px);\n            left: 50%;\n            top: auto;\n            transform: translateX(-50%) translateY(18px);\n            width: 88%;\n            max-width: 580px;\n            z-index: 1000;\n            pointer-events: none; /* Allows click-through to gameplay */\n            opacity: 0;\n            transition: opacity 0.5s ease, transform 0.5s ease;\n        }\n\n        .onboarding-overlay.active {\n            opacity: 1;\n            transform: translateX(-50%) translateY(0);\n        }\n\n        .onboarding-card {\n            background: rgba(18, 18, 22, 0.95);\n            border: 2px solid #FFD700;\n            border-radius: 20px;\n            box-shadow: 0 12px 36px rgba(0,0,0,0.6);\n            padding: 24px;\n            box-sizing: border-box;\n            text-align: center;\n            color: #ffffff;\n            display: flex;\n            flex-direction: column;\n            gap: 12px;\n        }\n\n        .onboarding-title {\n            font-size: 32px;\n            font-weight: 900;\n            text-transform: uppercase;\n            letter-spacing: 0.05em;\n            color: #FFD700;\n            text-shadow: 0 2px 4px rgba(0,0,0,0.5);\n            margin: 0;\n        }\n\n        .onboarding-body {\n            font-size: 22px;\n            line-height: 1.4;\n            color: rgba(255, 255, 255, 0.95);\n            font-weight: 700;\n            margin: 0;\n            white-space: pre-line;\n        }\n\n        #ghost-offscreen-indicator {\n            position: absolute;\n            left: 0;\n            top: 0;\n            pointer-events: none;\n            z-index: 12;\n            display: none;\n            transform: translate(-100%, -50%);\n            filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.45));\n        }\n\n        #ghost-offscreen-indicator.visible {\n            display: block;\n        }\n\n        .ghost-offscreen-indicator-inner {\n            display: flex;\n            flex-direction: row;\n            align-items: center;\n            gap: 9px;\n            transform-origin: 100% 50%;\n        }\n\n        .ghost-offscreen-arrow {\n            width: 0;\n            height: 0;\n            border-top: 10px solid transparent;\n            border-bottom: 10px solid transparent;\n            border-left: 17px solid #a8e8ff;\n            filter: drop-shadow(-1px 0 0 rgba(0, 0, 0, 0.85));\n            flex-shrink: 0;\n        }\n\n        .ghost-offscreen-label {\n            font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;\n            font-weight: 900;\n            font-size: 20px;\n            letter-spacing: 0.08em;\n            color: #a8e8ff;\n            text-shadow:\n                -1px -1px 0 #000,\n                1px -1px 0 #000,\n                -1px 1px 0 #000,\n                1px 1px 0 #000;\n            white-space: nowrap;\n            flex-shrink: 0;\n            display: inline-block;\n            transform-origin: center center;\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <!-- Loading Screen -->\n        <div id=\"screen-loading\">\n            <div class=\"loading-label\">Loading</div>\n            <div class=\"loading-progress-wrap\">\n                <div class=\"loading-progress-track\">\n                    <div class=\"loading-progress-fill\" id=\"loading-progress-fill\"></div>\n                </div>\n                <div class=\"loading-progress-pct\" id=\"loading-progress-pct\">0%</div>\n            </div>\n        </div>\n\n        <!-- Tap to Start (live world preview) -->\n        <div id=\"screen-tap-start\" class=\"screen-hidden\">\n            <img id=\"intro-logo\" src=\"\" alt=\"Eat the World Logo\">\n            <div id=\"tap-to-start-pill\">\n                <span id=\"tap-to-start-text\">Tap to Start</span>\n            </div>\n        </div>\n\n        <!-- Victory Screen -->\n        <div id=\"screen-victory\" class=\"screen-hidden\">\n            <div id=\"victory-overlay\"></div>\n            <div id=\"victory-card\">\n                <div id=\"confetti-container\"></div>\n                <div class=\"celebration-section\">\n                    <div class=\"victory-header\">\n                        <div class=\"header-you-ate\">YOU ATE</div>\n                        <div class=\"header-the-world\">THE WORLD</div>\n                    </div>\n                    <div class=\"victory-middle\">\n                        <div class=\"total-time-lbl\">TOTAL TIME</div>\n                        <div class=\"flex-row-center\">\n                            <span class=\"time-stopwatch\">\u23f1\ufe0f</span>\n                            <div id=\"victory-time-val\">00:00</div>\n                        </div>\n                    </div>\n                    <div id=\"victory-pb-container\"></div>\n                </div>\n                <div id=\"victory-leaderboard\" class=\"round-leaderboard\">\n                    <div class=\"leaderboard-title\">\ud83c\udfc6 GLOBAL SPEEDRUN LEADERBOARD</div>\n                    <div id=\"victory-leaderboard-content\" class=\"leaderboard-content\">Loading Leaderboard...</div>\n                    <div class=\"leaderboard-footer\">Keep going! You can <span class=\"highlight-yellow\">climb</span> the ranks!</div>\n                </div>\n                <button id=\"victory-replay-btn\">PLAY AGAIN <span class=\"play-arrow\">\u25b6</span></button>\n            </div>\n        </div>\n\n        <!-- Defeat Screen (PB Ghost wins) -->\n        <div id=\"screen-defeat\" class=\"screen-hidden\">\n            <div id=\"defeat-overlay\"></div>\n            <div id=\"defeat-card\">\n                <div class=\"celebration-section\">\n                    <div class=\"defeat-header\">\n                        <div class=\"header-your-ghost\">YOUR GHOST</div>\n                        <div class=\"header-ate-the-world\">ATE THE WORLD</div>\n                    </div>\n                    <div class=\"defeat-middle\">\n                        <div class=\"defeat-stat-item\">\n                            <div class=\"defeat-stat-lbl\">YOU ATE</div>\n                            <div id=\"defeat-player-pct\">0.0%</div>\n                            <div class=\"defeat-stat-sub\">OF THE WORLD</div>\n                        </div>\n                        <div class=\"defeat-stat-divider\"></div>\n                        <div class=\"defeat-stat-item\">\n                            <div class=\"defeat-stat-lbl\">TIME TO BEAT</div>\n                            <div id=\"defeat-time-to-beat\">00:00</div>\n                        </div>\n                    </div>\n                </div>\n                <div id=\"defeat-leaderboard\" class=\"round-leaderboard\">\n                    <div class=\"leaderboard-title\">\ud83c\udfc6 GLOBAL SPEEDRUN LEADERBOARD</div>\n                    <div id=\"defeat-leaderboard-content\" class=\"leaderboard-content\">Loading Leaderboard...</div>\n                    <div class=\"leaderboard-footer\">Keep going! You can <span class=\"highlight-yellow\">climb</span> the ranks!</div>\n                </div>\n                <button id=\"defeat-replay-btn\">TRY AGAIN <span class=\"play-arrow\">\u25b6</span></button>\n            </div>\n        </div>\n\n        <canvas id=\"game-canvas\"></canvas>\n        <button id=\"music-toggle-btn\">\ud83c\udfb5</button>\n        <div class=\"joystick-outer\" id=\"joystick-outer\">\n            <div class=\"joystick-inner\" id=\"joystick-inner\"></div>\n        </div>\n        <div id=\"mode-label\"></div>\n        <div id=\"timer-panel\" style=\"display: none;\">\n            <span id=\"timer-icon\">\ud83d\udd52</span>\n            <span id=\"timer-text\">00:00</span>\n        </div>\n        <div id=\"goal-card\">\n            <div id=\"player-progress\" class=\"progress-bar-wrapper\">\n                <div class=\"progress-bar-bg\">\n                    <div class=\"progress-bar-fill\" id=\"goal-bar-fill\"></div>\n                </div>\n                <div class=\"progress-info-row\">\n                    <span class=\"progress-label\">Your Score</span>\n                    <span id=\"goal-score-text\" class=\"progress-value-text\">0.0%</span>\n                </div>\n            </div>\n            <div id=\"ghost-progress\" class=\"progress-bar-wrapper\" style=\"display: none;\">\n                <div class=\"progress-bar-bg\">\n                    <div class=\"progress-bar-fill\" id=\"ghost-bar-fill\"></div>\n                </div>\n                <div class=\"progress-info-row\">\n                    <span class=\"progress-label\">Ghost Score</span>\n                    <span id=\"ghost-score-text\" class=\"progress-value-text\">0.0%</span>\n                </div>\n            </div>\n        </div>\n        <div id=\"level-hud\">\n            <div id=\"level-hud-bar-bg\"><div id=\"level-hud-bar-fill\"></div><div id=\"level-hud-text\">0 / 10</div></div>\n        </div>\n        <div id=\"ghost-offscreen-indicator\" aria-hidden=\"true\">\n            <div class=\"ghost-offscreen-indicator-inner\">\n                <span class=\"ghost-offscreen-label\">GHOST</span>\n                <div class=\"ghost-offscreen-arrow\"></div>\n            </div>\n        </div>\n        <div id=\"ghost-debug-panel\" style=\"display: none; position: absolute; bottom: 20px; right: 20px; background: rgba(0,0,0,0.85); color: #00ff88; font-family: monospace; font-size: 16px; padding: 12px 18px; border-radius: 8px; border: 1.5px solid #00ff88; z-index: 1000; pointer-events: none; line-height: 1.4;\">\n            <div style=\"font-weight: bold; border-bottom: 1px solid #00ff88; padding-bottom: 4px; margin-bottom: 6px; color: #fff;\">GHOST BOT DEBUG</div>\n            <div>State: <span id=\"gdb-state\" style=\"color: #ffeb3b; font-weight: bold;\">-</span></div>\n            <div>Score: <span id=\"gdb-score\">-</span></div>\n            <div>Player: <span id=\"gdb-player\">-</span></div>\n            <div>Target: <span id=\"gdb-target\">-</span></div>\n        </div>\n        <div id=\"editor-toolbar\">\n            <div class=\"editor-mode-tabs\">\n                <button id=\"toggle-objects-btn\" class=\"tab-btn active\">\ud83e\uddf1 Objects</button>\n                <button id=\"toggle-terrain-btn\" class=\"tab-btn\">\ud83c\udf3f Terrain</button>\n            </div>\n            <div id=\"path-edit-toolbar\" style=\"display:none;\">\n                <div style=\"display:flex;gap:12px;padding:14px 16px;align-items:stretch;width:100%;box-sizing:border-box;\">\n                    <button id=\"path-add-wp-btn\" class=\"tb-item\">\ud83d\udccd Add</button>\n                    <button id=\"path-select-wp-btn\" class=\"tb-item mode-active\">\u270b Select</button>\n                    <div class=\"tb-divider\"></div>\n                    <button id=\"path-delete-wp-btn\" class=\"tb-item\" style=\"border-color:rgba(255,80,80,0.25);\">\ud83d\uddd1</button>\n                    <div class=\"tb-divider\"></div>\n                    <button id=\"path-exit-btn\" class=\"tb-item\" style=\"border-color:rgba(255,80,80,0.25);\">\u2716 Exit</button>\n                </div>\n            </div>\n            <div id=\"toolbar-content-row\">\n                <div id=\"object-controls\">\n                    <select id=\"registry-dropdown\"></select>\n                    <div class=\"custom-dropdown\" id=\"custom-dropdown\">\n                        <div class=\"custom-dropdown-selected tb-item\" id=\"dropdown-selected\">Select Type</div>\n                        <div class=\"custom-dropdown-options\" id=\"dropdown-options\">\n                            <input type=\"text\" class=\"dropdown-search-input\" id=\"object-search-input\" placeholder=\"\ud83d\udd0d Search...\" autocomplete=\"off\">\n                            <div class=\"custom-dropdown-list\" id=\"object-dropdown-list\"></div>\n                        </div>\n                    </div>\n                    <div class=\"tb-divider\"></div>\n                    <button id=\"place-btn\" class=\"tb-item\">\ud83d\udccc</button>\n                    <button id=\"select-btn\" class=\"tb-item\">\u270b</button>\n                    <div class=\"tb-divider\"></div>\n                    <button id=\"delete-btn\" class=\"tb-item\">\ud83d\uddd1</button>\n                    <div class=\"tb-divider\"></div>\n                    <button id=\"undo-btn-objects\" class=\"tb-item\">\u21a9</button>\n                </div>\n                <div id=\"terrain-controls\">\n                    <div class=\"custom-dropdown\" id=\"terrain-dropdown\">\n                        <div class=\"custom-dropdown-selected tb-item\" id=\"terrain-dropdown-selected\">Grass</div>\n                        <div class=\"custom-dropdown-options\" id=\"terrain-dropdown-options\">\n                            <input type=\"text\" class=\"dropdown-search-input\" id=\"terrain-search-input\" placeholder=\"\ud83d\udd0d Search...\" autocomplete=\"off\">\n                            <div class=\"custom-dropdown-list\" id=\"terrain-dropdown-list\"></div>\n                        </div>\n                    </div>\n                    <div class=\"tb-divider\"></div>\n                    <button id=\"terrain-paint-btn\" class=\"tb-item mode-active\">\ud83d\udd8c</button>\n                    <button id=\"terrain-select-btn\" class=\"tb-item\">\u270b</button>\n                    <button id=\"terrain-erase-btn\" class=\"tb-item\">\ud83e\uddf9</button>\n                    <div class=\"tb-divider\"></div>\n                    <button id=\"undo-btn-terrain\" class=\"tb-item\">\u21a9</button>\n                </div>\n            </div>\n        </div>\n        <div id=\"prop-panel\">\n            <div class=\"prop-header\" id=\"prop-title\">Properties</div>\n            <div id=\"prop-empty\" style=\"font-size: 18px; color: rgba(255,255,255,0.5);\">No object selected.</div>\n            <div id=\"prop-content\" style=\"display: none; flex-direction: column; gap: 12px;\">\n                <div class=\"prop-row\">\n                    <label style=\"font-size:14px;color:rgba(255,255,255,0.5);\">Instance: <span id=\"prop-instance-id\">\u2014</span></label>\n                </div>\n                <div class=\"prop-row\">\n                    <label style=\"font-size:14px;color:rgba(255,255,255,0.5);\">Registry Type: <span id=\"prop-registry-type\">\u2014</span></label>\n                </div>\n                <div class=\"prop-row\" style=\"position:relative;\">\n                    <label>Swap Type:</label>\n                    <div id=\"prop-swap-dropdown\" class=\"custom-dropdown\" style=\"margin-top:4px;\">\n                        <div id=\"prop-swap-selected\" class=\"tb-item custom-dropdown-selected\" style=\"height:48px;font-size:16px;padding:0 12px;\">Select Type</div>\n                        <div id=\"prop-swap-options\" class=\"custom-dropdown-options\" style=\"position:absolute;bottom:100%;left:0;margin-bottom:6px;width:100%;max-height:220px;\">\n                            <input type=\"text\" class=\"dropdown-search-input\" id=\"prop-swap-search\" placeholder=\"\ud83d\udd0d Search...\" autocomplete=\"off\">\n                            <div class=\"custom-dropdown-list\" id=\"prop-swap-list\"></div>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"prop-row\">\n                    <label>Position: <span id=\"prop-pos-val\">0, 0</span></label>\n                </div>\n                <div class=\"prop-row\">\n                    <label>Scale: <span id=\"prop-scale-val\">1.0</span></label>\n                    <input type=\"range\" id=\"prop-scale\" min=\"0.2\" max=\"5\" step=\"0.1\" value=\"1\">\n                </div>\n                <div class=\"prop-row\">\n                    <label>Facing: <span id=\"prop-facing-val\">0.0</span></label>\n                    <input type=\"range\" id=\"prop-facing\" min=\"0\" max=\"6.28\" step=\"0.1\" value=\"0\">\n                </div>\n                <div style=\"display:flex;gap:8px;margin-top:8px;\">\n                    <button id=\"prop-copy-btn\" style=\"padding:12px 18px;font-size:18px;font-weight:600;border-radius:10px;border:2px solid rgba(255,255,255,0.12);background:rgba(48,48,54,0.92);color:#fff;cursor:pointer;flex:1;\">\ud83d\udccb Copy</button>\n                    <button id=\"prop-rotate-btn\" style=\"padding:12px 18px;font-size:18px;font-weight:600;border-radius:10px;border:2px solid rgba(255,255,255,0.12);background:rgba(48,48,54,0.92);color:#fff;cursor:pointer;flex:1;\">\u21bb Rotate</button>\n                </div>\n                <div id=\"prop-ai-path-section\" style=\"display:none; border-top:1px solid rgba(255,255,255,0.1); padding-top:12px; margin-top:4px;\">\n                    <label style=\"font-size:16px;font-weight:600;color:rgba(255,255,255,0.8);\">AI Path</label>\n                    <div style=\"font-size:14px;color:rgba(255,255,255,0.5);margin:6px 0;\" id=\"prop-ai-path-info\">4 waypoints (loop)</div>\n                    <div class=\"prop-row\" style=\"margin-bottom: 8px;\">\n                        <label>Initial Next Waypoint ID:</label>\n                        <input type=\"number\" id=\"prop-initial-wp-id\" min=\"0\" step=\"1\" style=\"background:rgba(30,30,34,0.8); border:1px solid rgba(255,255,255,0.2); color:white; padding:6px; border-radius:6px; font-size:16px;\">\n                    </div>\n                    <button id=\"prop-edit-path-btn\" style=\"padding:12px 18px;font-size:18px;font-weight:600;border-radius:10px;border:2px solid rgba(255,255,255,0.12);background:rgba(48,48,54,0.92);color:#fff;cursor:pointer;width:100%;\">\u270f\ufe0f Edit Path</button>\n                </div>\n            </div>\n            <div id=\"prop-terrain-content\" style=\"display: none; flex-direction: column; gap: 12px;\">\n                <div class=\"prop-row\">\n                    <label>Rotation: <span id=\"prop-terrain-rot-val\">0\u00b0</span></label>\n                </div>\n                <button id=\"prop-terrain-rotate-btn\" style=\"padding:14px 20px;font-size:20px;font-weight:600;border-radius:10px;border:2px solid rgba(255,255,255,0.12);background:rgba(48,48,54,0.92);color:#fff;cursor:pointer;\">\u21bb Rotate 90\u00b0</button>\n            </div>\n        </div>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n    <script>\n    // UserDataManager is defined inside run() and uses lib user game state persistence APIs.\n\n    async function run(mode) {\n\n        function formatMMSS(seconds) {\n            const m = Math.floor(seconds / 60);\n            const s = Math.floor(seconds % 60);\n            return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');\n        }\n\n        /** Temporary PB route pipeline tracing \u2014 toggle via config.pbRoutePipelineDebug */\n        function pbRoutePipelineDebug(stage, detail) {\n            if (!config || config.pbRoutePipelineDebug !== true) return;\n            const msg = detail !== undefined ? stage + ' | ' + detail : stage;\n            console.log('[PBRoutePipeline] ' + msg);\n            lib.log('[PBRoutePipeline] ' + msg);\n        }\n\n        function summarizeRoute(route, label) {\n            if (!route) {\n                pbRoutePipelineDebug(label, 'route=null');\n                return null;\n            }\n            const samples = route.samples || [];\n            const first = samples[0] || null;\n            const last = samples[samples.length - 1] || null;\n            const summary = {\n                label,\n                totalTimeSec: route.totalTimeSec,\n                sampleIntervalSec: route.sampleIntervalSec,\n                sampleCount: samples.length,\n                firstSample: first,\n                lastSample: last,\n                fields: Object.keys(route)\n            };\n            pbRoutePipelineDebug(label, JSON.stringify(summary));\n            return summary;\n        }\n\n        function compareRouteStructures(debugRoute, pbRoute) {\n            const debugSummary = summarizeRoute(debugRoute, 'COMPARE debug route');\n            const pbSummary = summarizeRoute(pbRoute, 'COMPARE pb route');\n            if (!debugSummary || !pbSummary) {\n                pbRoutePipelineDebug('COMPARE', 'missing route \u2014 debug=' + !!debugRoute + ' pb=' + !!pbRoute);\n                return;\n            }\n            const fieldDiffs = [];\n            ['totalTimeSec', 'sampleIntervalSec', 'sampleCount'].forEach(key => {\n                if (debugSummary[key] !== pbSummary[key]) {\n                    fieldDiffs.push(key + ': debug=' + debugSummary[key] + ' pb=' + pbSummary[key]);\n                }\n            });\n            if (debugSummary.fields.join(',') !== pbSummary.fields.join(',')) {\n                fieldDiffs.push('fields: debug=[' + debugSummary.fields.join(',') + '] pb=[' + pbSummary.fields.join(',') + ']');\n            }\n            if (fieldDiffs.length) {\n                pbRoutePipelineDebug('COMPARE diffs', fieldDiffs.join('; '));\n            } else {\n                pbRoutePipelineDebug('COMPARE', 'top-level structure matches');\n            }\n        }\n\n        /**\n         * ============================================================\n         * UserDataManager \u2014 Player Progress & Personal Bests (lib user game state)\n         * ============================================================\n         */\n        const UserDataManager = (() => {\n            const DEFAULT_DATA = {\n                personalBestTimeSec: null,\n                personalBestRoute: null,\n                totalRoundsCompleted: 0,\n                completedOnboardings: {}\n            };\n\n            let userData = { ...DEFAULT_DATA };\n            let loaded = false;\n\n            function normalizeRoute(route) {\n                if (!route || typeof route !== 'object' || !Array.isArray(route.samples)) {\n                    pbRoutePipelineDebug('normalizeRoute', 'rejected \u2014 invalid route object');\n                    return null;\n                }\n                const rawCount = route.samples.length;\n                const samples = route.samples\n                    .filter(s => s && typeof s.t === 'number' && typeof s.x === 'number' && typeof s.z === 'number')\n                    .map(s => ({\n                        t: parseFloat(s.t.toFixed(2)),\n                        x: parseFloat(s.x.toFixed(2)),\n                        z: parseFloat(s.z.toFixed(2))\n                    }));\n                if (samples.length < 2) {\n                    pbRoutePipelineDebug('normalizeRoute', 'rejected \u2014 ' + samples.length + ' valid samples from ' + rawCount + ' raw');\n                    return null;\n                }\n                const normalized = {\n                    totalTimeSec: typeof route.totalTimeSec === 'number' ? route.totalTimeSec : samples[samples.length - 1].t,\n                    sampleIntervalSec: typeof route.sampleIntervalSec === 'number' ? route.sampleIntervalSec : 0.4,\n                    samples\n                };\n                pbRoutePipelineDebug('normalizeRoute', 'ok \u2014 ' + samples.length + ' samples, totalTimeSec=' + normalized.totalTimeSec);\n                return normalized;\n            }\n\n            function normalize(state) {\n                if (!state || typeof state !== 'object') return { ...DEFAULT_DATA };\n                return {\n                    personalBestTimeSec:\n                        typeof state.personalBestTimeSec === 'number' && Number.isFinite(state.personalBestTimeSec)\n                            ? state.personalBestTimeSec\n                            : null,\n                    personalBestRoute: normalizeRoute(state.personalBestRoute),\n                    totalRoundsCompleted:\n                        typeof state.totalRoundsCompleted === 'number' && Number.isFinite(state.totalRoundsCompleted)\n                            ? Math.max(0, Math.floor(state.totalRoundsCompleted))\n                            : 0,\n                    completedOnboardings:\n                        state.completedOnboardings && typeof state.completedOnboardings === 'object'\n                            ? state.completedOnboardings\n                            : {}\n                };\n            }\n\n            async function loadUserData() {\n                if (loaded) return userData;\n\n                try {\n                    const res = await lib.getUserGameState();\n                    const stateSource = res && res.state;\n                    pbRoutePipelineDebug('storage READ platform', 'res.state present=' + !!stateSource + ' keys=' + (stateSource ? Object.keys(stateSource).join(',') : 'none'));\n                    userData = normalize(stateSource);\n                    summarizeRoute(userData.personalBestRoute, 'storage LOADED platform route');\n                } catch (e) {\n                    console.warn('[UserDataManager] Platform user state unavailable, using defaults.');\n                    pbRoutePipelineDebug('storage READ platform', 'FAILED \u2014 ' + e.message);\n                    userData = { ...DEFAULT_DATA };\n                }\n\n                loaded = true;\n                return userData;\n            }\n\n            async function saveUserData() {\n                try {\n                    await lib.saveUserGameState(userData);\n                    pbRoutePipelineDebug('storage WRITE platform', 'ok \u2014 pbTime=' + userData.personalBestTimeSec + ' routeSamples=' + (userData.personalBestRoute ? userData.personalBestRoute.samples.length : 0));\n                } catch (e) {\n                    console.warn('[UserDataManager] Platform save failed \u2014 data kept in memory only.');\n                    pbRoutePipelineDebug('storage WRITE platform', 'FAILED \u2014 ' + e.message);\n                }\n            }\n\n            function getData() {\n                return userData;\n            }\n\n            function getPersonalBest() {\n                return userData.personalBestTimeSec;\n            }\n\n            function checkNewBest(timeSec) {\n                const currentBest = userData.personalBestTimeSec;\n                if (currentBest === null || currentBest === undefined) return true;\n                return timeSec < currentBest;\n            }\n\n            async function updatePersonalBest(timeSec, route) {\n                const isBest = checkNewBest(timeSec);\n                pbRoutePipelineDebug('PB qualify', 'timeSec=' + timeSec + ' currentBest=' + userData.personalBestTimeSec + ' isNewBest=' + isBest + ' routeProvided=' + !!route);\n                if (isBest) {\n                    userData.personalBestTimeSec = timeSec;\n                    if (route) {\n                        userData.personalBestRoute = normalizeRoute(route);\n                        summarizeRoute(userData.personalBestRoute, 'PB SAVED to userData');\n                    } else {\n                        pbRoutePipelineDebug('PB qualify', 'new best but route=null \u2014 route NOT updated');\n                    }\n                    await saveUserData();\n                    return true;\n                }\n                pbRoutePipelineDebug('PB qualify', 'not a new best \u2014 route unchanged');\n                return false;\n            }\n\n            function getPersonalBestRoute() {\n                return userData.personalBestRoute;\n            }\n\n            async function incrementRounds() {\n                userData.totalRoundsCompleted += 1;\n                await saveUserData();\n            }\n\n            return {\n                loadUserData,\n                saveUserData,\n                getData,\n                getPersonalBest,\n                getPersonalBestRoute,\n                normalizeRoute,\n                checkNewBest,\n                updatePersonalBest,\n                incrementRounds\n            };\n        })();\n\n        const ModalSystem = (() => {\n            let activeOverlay = null;\n            let appearanceTimeout = null;\n            let durationTimeout = null;\n\n            function show({ title, body, completionFlag = null, onDismiss = null }) {\n                if (completionFlag) {\n                    const data = UserDataManager.getData();\n                    if (data.completedOnboardings && data.completedOnboardings[completionFlag]) {\n                        if (onDismiss) onDismiss();\n                        return;\n                    }\n                }\n\n                clearTimeout(appearanceTimeout);\n                clearTimeout(durationTimeout);\n                if (activeOverlay) {\n                    activeOverlay.remove();\n                    activeOverlay = null;\n                }\n\n                const delay = config.onboardingDelay !== undefined ? config.onboardingDelay : 1.0;\n                const isGhostOnboarding = completionFlag === 'ghost_onboarding';\n                const duration = isGhostOnboarding\n                    ? (config.ghostOnboardingDuration !== undefined ? config.ghostOnboardingDuration : 5.0)\n                    : (config.onboardingDuration !== undefined ? config.onboardingDuration : 3.5);\n\n                appearanceTimeout = setTimeout(() => {\n                    const overlay = document.createElement('div');\n                    overlay.className = 'onboarding-overlay';\n                    \n                    const card = document.createElement('div');\n                    card.className = 'onboarding-card';\n\n                    const titleEl = document.createElement('h2');\n                    titleEl.className = 'onboarding-title';\n                    titleEl.textContent = title;\n                    card.appendChild(titleEl);\n\n                    const bodyEl = document.createElement('p');\n                    bodyEl.className = 'onboarding-body';\n                    bodyEl.textContent = body;\n                    card.appendChild(bodyEl);\n\n                    overlay.appendChild(card);\n                    document.getElementById('game-world').appendChild(overlay);\n\n                    activeOverlay = overlay;\n\n                    // Force a reflow so the browser registers the initial state for transitions\n                    overlay.offsetHeight;\n                    overlay.classList.add('active');\n\n                    durationTimeout = setTimeout(async () => {\n                        overlay.classList.remove('active');\n                        \n                        setTimeout(async () => {\n                            overlay.remove();\n                            if (activeOverlay === overlay) {\n                                activeOverlay = null;\n                            }\n\n                            if (completionFlag) {\n                                const data = UserDataManager.getData();\n                                if (!data.completedOnboardings) {\n                                    data.completedOnboardings = {};\n                                }\n                                data.completedOnboardings[completionFlag] = true;\n                                await UserDataManager.saveUserData();\n                            }\n\n                            if (onDismiss) {\n                                onDismiss();\n                            }\n                        }, 500);\n                    }, duration * 1000);\n\n                }, delay * 1000);\n            }\n\n            function isActive() {\n                return false;\n            }\n\n            return {\n                show,\n                isActive\n            };\n        })();\n\n        /**\n         * RouteRecorder \u2014 captures player path during a run for PB ghost playback.\n         */\n        const RouteRecorder = (() => {\n            let samples = [];\n            let sampleAccumulator = 0;\n            let recording = false;\n\n            function getSampleInterval() {\n                return config.pbRouteSampleInterval !== undefined ? config.pbRouteSampleInterval : 0.4;\n            }\n\n            function reset() {\n                samples = [];\n                sampleAccumulator = 0;\n                recording = false;\n            }\n\n            function startRecording(playerHole) {\n                reset();\n                recording = true;\n                if (playerHole && playerHole.state) {\n                    samples.push({\n                        t: 0,\n                        x: parseFloat(playerHole.state.x.toFixed(2)),\n                        z: parseFloat(playerHole.state.z.toFixed(2))\n                    });\n                }\n                pbRoutePipelineDebug('record START', 'samples=' + samples.length + ' pos=' + (samples[0] ? samples[0].x + ',' + samples[0].z : 'none'));\n            }\n\n            function update(dt, playerHole) {\n                if (!recording || !playerHole) return;\n                const interval = getSampleInterval();\n                sampleAccumulator += dt;\n                const elapsed = (performance.now() - matchStartTime) / 1000;\n                while (sampleAccumulator >= interval) {\n                    sampleAccumulator -= interval;\n                    samples.push({\n                        t: parseFloat(elapsed.toFixed(2)),\n                        x: parseFloat(playerHole.state.x.toFixed(2)),\n                        z: parseFloat(playerHole.state.z.toFixed(2))\n                    });\n                }\n            }\n\n            function buildRouteSnapshot(totalTimeSec, playerHole) {\n                const lastT = parseFloat(totalTimeSec.toFixed(2));\n                if (playerHole && playerHole.state) {\n                    const last = samples[samples.length - 1];\n                    if (!last || last.t < lastT - 0.05) {\n                        samples.push({\n                            t: lastT,\n                            x: parseFloat(playerHole.state.x.toFixed(2)),\n                            z: parseFloat(playerHole.state.z.toFixed(2))\n                        });\n                    }\n                }\n                const snapshot = {\n                    totalTimeSec: lastT,\n                    sampleIntervalSec: getSampleInterval(),\n                    samples: samples.slice()\n                };\n                pbRoutePipelineDebug('record SNAPSHOT', 'samples=' + snapshot.samples.length + ' totalTimeSec=' + snapshot.totalTimeSec + ' last=' + JSON.stringify(snapshot.samples[snapshot.samples.length - 1]));\n                return snapshot;\n            }\n\n            function discard() {\n                reset();\n            }\n\n            return {\n                startRecording,\n                update,\n                buildRouteSnapshot,\n                discard,\n                reset\n            };\n        })();\n\n        const SessionMetrics = (() => {\n            let data = null;\n            let consumeAccumulator = { byCategory: {}, byRegistryId: {}, totalCount: 0, totalPoints: 0 };\n            let lastMilestoneTimeSec = 0;\n\n            function generateUUID() {\n                return 'sess_' + Date.now() + '_' + Math.floor(Math.random() * 1000000);\n            }\n\n            function initSession() {\n                data = {\n                    sessionId: generateUUID(),\n                    startedAt: new Date().toISOString(),\n                    endedAt: null,\n                    rounds: []\n                };\n            }\n\n            function startRound() {\n                if (!data) initSession();\n                if (data.rounds.length > 0) {\n                    const lastRound = data.rounds[data.rounds.length - 1];\n                    if (!lastRound.endedAt) {\n                        endRound({ completed: false, completionPct: (totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0) });\n                    }\n                }\n\n                resetConsumeAccumulator();\n                lastMilestoneTimeSec = 0;\n\n                const newRound = {\n                    startedAt: new Date().toISOString(),\n                    endedAt: null,\n                    completed: false,\n                    completionPct: 0,\n                    totalTimeSec: 0,\n                    playAgainPressed: false,\n                    sizeMilestones: []\n                };\n                data.rounds.push(newRound);\n            }\n\n            function endRound({ completed, completionPct }) {\n                if (!data || data.rounds.length === 0) return;\n                const round = data.rounds[data.rounds.length - 1];\n                if (round.endedAt) return;\n\n                round.endedAt = new Date().toISOString();\n                round.completed = completed;\n                round.completionPct = parseFloat(completionPct.toFixed(1));\n                const startMs = new Date(round.startedAt).getTime();\n                const endMs = new Date(round.endedAt).getTime();\n                round.totalTimeSec = parseFloat(((endMs - startMs) / 1000).toFixed(2));\n            }\n\n            function recordSizeMilestone(size, position) {\n                if (!data || data.rounds.length === 0) return;\n                const round = data.rounds[data.rounds.length - 1];\n                if (round.endedAt) return;\n\n                const currentSec = (performance.now() - matchStartTime) / 1000;\n                const split = currentSec - lastMilestoneTimeSec;\n\n                const milestone = {\n                    size: size,\n                    mapTimeSec: parseFloat(currentSec.toFixed(2)),\n                    splitTimeSec: parseFloat(split.toFixed(2)),\n                    position: { x: parseFloat(position.x.toFixed(2)), z: parseFloat(position.z.toFixed(2)) },\n                    consumedSinceLastMilestone: JSON.parse(JSON.stringify(consumeAccumulator))\n                };\n\n                round.sizeMilestones.push(milestone);\n\n                const ateSummary = Object.entries(consumeAccumulator.byCategory)\n                    .map(([cat, count]) => `${cat}\u00d7${count}`)\n                    .join(' ') || 'nothing';\n\n                console.log(`[EatTheWorld] Size ${size} reached @ ${formatMMSS(currentSec)} | split ${formatMMSS(split)} | ate: ${ateSummary}`);\n\n                resetConsumeAccumulator();\n                lastMilestoneTimeSec = currentSec;\n            }\n\n            function recordConsume(instanceId, points) {\n                const registryId = (config.objectInstances[instanceId] || {}).registryId || 'unknown';\n                const category = getObjectCategory(instanceId) || 'prop';\n\n                consumeAccumulator.byCategory[category] = (consumeAccumulator.byCategory[category] || 0) + 1;\n                consumeAccumulator.byRegistryId[registryId] = (consumeAccumulator.byRegistryId[registryId] || 0) + 1;\n                consumeAccumulator.totalCount++;\n                consumeAccumulator.totalPoints += points;\n            }\n\n            function markPlayAgain() {\n                if (!data || data.rounds.length === 0) return;\n                const round = data.rounds[data.rounds.length - 1];\n                round.playAgainPressed = true;\n            }\n\n            function getData() {\n                return data;\n            }\n\n            function exportJSON() {\n                return JSON.stringify(data, null, 2);\n            }\n\n            function resetConsumeAccumulator() {\n                consumeAccumulator = { byCategory: {}, byRegistryId: {}, totalCount: 0, totalPoints: 0 };\n            }\n\n            async function upload(reason, keepalive = false) {\n                if (!config.metricsEnabled || !config.metricsUrl) return;\n                const payload = {\n                    reason: reason,\n                    gameVersion: config.gameVersion || \"0.01\",\n                    ...data\n                };\n                try {\n                    const options = {\n                        method: 'POST',\n                        body: JSON.stringify(payload)\n                    };\n                    if (keepalive) {\n                        options.keepalive = true;\n                    }\n                    fetch(config.metricsUrl, options)\n                    .then(res => {\n                        console.log(`[EatTheWorld] Metrics uploaded for reason: ${reason}`);\n                    })\n                    .catch(err => {\n                        console.error('[EatTheWorld] Metrics upload failed:', err);\n                    });\n                } catch (e) {\n                    console.error('[EatTheWorld] Metrics upload exception:', e);\n                }\n            }\n\n            return {\n                initSession,\n                startRound,\n                endRound,\n                recordSizeMilestone,\n                recordConsume,\n                markPlayAgain,\n                getData,\n                exportJSON,\n                upload\n            };\n        })();\n\n        const Logger = {\n            log(message) {\n                console.log(`[EatTheWorld] ${message}`);\n            },\n            getSessionMetrics: () => SessionMetrics.getData(),\n            exportSessionMetrics: () => SessionMetrics.exportJSON()\n        };\n\n        window.EatTheWorldMetrics = {\n            get: () => SessionMetrics.getData(),\n            export: () => SessionMetrics.exportJSON()\n        };\n\n        window.testMetricsAtLevel2 = function() {\n            if (config.testMetricsAtLevel2Enabled && gameState.player.level === 2) {\n                SessionMetrics.upload(\"test_level_2\");\n                return true;\n            }\n            return false;\n        };\n\n        if (mode === 'play') {\n            SessionMetrics.initSession();\n\n            window.addEventListener('beforeunload', () => {\n                if (SessionMetrics.getData()) {\n                    const d = SessionMetrics.getData();\n                    d.endedAt = new Date().toISOString();\n                    if (d.rounds.length > 0) {\n                        const lastRound = d.rounds[d.rounds.length - 1];\n                        if (!lastRound.endedAt) {\n                            SessionMetrics.endRound({\n                                completed: false,\n                                completionPct: (totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0)\n                            });\n                        }\n                    }\n                }\n            });\n\n            window.addEventListener('pagehide', () => {\n                if (SessionMetrics.getData()) {\n                    const d = SessionMetrics.getData();\n                    d.endedAt = new Date().toISOString();\n                    if (d.rounds.length > 0) {\n                        const lastRound = d.rounds[d.rounds.length - 1];\n                        if (!lastRound.endedAt) {\n                            SessionMetrics.endRound({\n                                completed: false,\n                                completionPct: (totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0)\n                            });\n                        }\n                    }\n                    SessionMetrics.upload(\"pagehide\", true);\n                }\n            });\n        }\n\n        let isVictoryActive = false;\n        let isDefeatActive = false;\n        let routeRecordingPending = false;\n        let sessionRoundNumber = 1;\n\n        function checkGhostVictoryDefeat() {\n            if (isVictoryActive || isDefeatActive || mode !== 'play' || !ghostBot || !isGhostActiveThisRound()) return;\n            \n            // Only allow the ghost to win if canWin is true (e.g. Personal Best ghost)\n            if (!ghostBot.canWin) return;\n\n            const ledger = PointManager.getHoleScore('bot-1');\n            const ghostPct = totalAvailablePoints > 0 ? (ledger.totalPoints / totalAvailablePoints) * 100 : 0;\n            const playerPct = totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0;\n\n            // Check if Personal Best route finished playback\n            let ghostFinished = false;\n            if (shouldUsePersonalBestGhost()) {\n                const route = resolveGhostPlaybackRoute();\n                if (route && route.samples && route.samples.length > 0) {\n                    const elapsed = (performance.now() - matchStartTime) / 1000;\n                    const lastSample = route.samples[route.samples.length - 1];\n                    if (elapsed >= lastSample.t) {\n                        ghostFinished = true;\n                    }\n                }\n            }\n\n            const victoryPct = getVictoryCompletionPercent();\n            if ((ghostPct >= victoryPct || ghostFinished) && playerPct < victoryPct) {\n                triggerDefeatScreen();\n            }\n        }\n\n        function getLeaderboardDisplayName(entry) {\n            if (!entry) return 'Unknown';\n            const raw = (entry.username || '').trim();\n            if (!raw) return 'Player';\n            return raw;\n        }\n\n        let leaderboardSelfUsername = null;\n\n        function noteLeaderboardSelfIdentity(entries, userRank) {\n            if (!entries || !entries.length) return;\n            const selfByFlag = entries.find(e => e.isCurrentUser && e.username);\n            if (selfByFlag) {\n                leaderboardSelfUsername = selfByFlag.username;\n                return;\n            }\n            if (userRank != null && userRank >= 1 && userRank <= entries.length) {\n                const ranked = entries[userRank - 1];\n                if (ranked && ranked.username && ranked.username !== 'YOU') {\n                    leaderboardSelfUsername = ranked.username;\n                }\n            }\n        }\n\n        function isLeaderboardSelfEntry(entry, currentUsername) {\n            if (!entry) return false;\n            if (entry.isCurrentUser) return true;\n            if (entry.username === 'YOU') return true;\n            const name = currentUsername || leaderboardSelfUsername;\n            return !!(name && entry.username === name);\n        }\n\n        function renderLeaderboardRows(containerEl, entries, options = {}) {\n            const {\n                userRank = null,\n                currentUserTimeSec = null,\n                highlightClass = 'lb-you',\n                forceYouLabel = false,\n                ensureUserScore = null,\n                currentUsername = null\n            } = options;\n\n            if (!containerEl) return;\n\n            let finalEntries = entries ? entries.slice() : [];\n            let resolvedUserRank = userRank;\n\n            if (ensureUserScore !== null && ensureUserScore !== undefined) {\n                finalEntries = finalEntries.filter(e => !isLeaderboardSelfEntry(e, currentUsername));\n                finalEntries.push({ username: 'YOU', score: ensureUserScore, isCurrentUser: true });\n                finalEntries.sort((a, b) => a.score - b.score);\n                const playerIdx = finalEntries.findIndex(e => e.isCurrentUser);\n                if (playerIdx >= 0) {\n                    resolvedUserRank = playerIdx + 1;\n                }\n                lib.log('[Leaderboard] Applied current run score ' + ensureUserScore + ' at rank #' + resolvedUserRank);\n            }\n\n            if (!finalEntries || finalEntries.length === 0) {\n                containerEl.innerHTML = '<div class=\"lb-empty\">No runs yet. Be the first to set a time!</div>';\n                return;\n            }\n\n            const topFive = finalEntries.slice(0, 5);\n            const showYouInTopFive = resolvedUserRank != null && resolvedUserRank <= 5;\n            const topHtml = topFive.map((entry, idx) => {\n                const rank = idx + 1;\n                const name = getLeaderboardDisplayName(entry);\n                const isYou = showYouInTopFive && resolvedUserRank === rank;\n                const rowClass = isYou ? ('lb-row ' + highlightClass) : 'lb-row';\n                const labelName = isYou ? 'YOU' : name;\n                return `<div class=\"${rowClass}\"><span class=\"lb-rank\">#${rank}</span><span class=\"lb-name\">${labelName}</span><span class=\"lb-time\">${formatMMSS(entry.score || 0)}</span></div>`;\n            }).join('');\n\n            containerEl.innerHTML = `<div class=\"lb-list\">${topHtml}</div>`;\n        }\n\n        async function loadAndRenderLeaderboard({\n            containerId,\n            submitScoreSec = null,\n            highlightClass = 'lb-you',\n            fallbackTimeSec = null\n        }) {\n            const contentEl = document.getElementById(containerId);\n            if (!contentEl) return;\n\n            contentEl.innerHTML = '<div class=\"lb-loading\">Loading Leaderboard...</div>';\n\n            let userRank = null;\n            let localTime = fallbackTimeSec;\n\n            try {\n                let entries = [];\n                let currentUsername = leaderboardSelfUsername;\n\n                if (submitScoreSec !== null && submitScoreSec !== undefined) {\n                    const submitRes = await lib.addPlayerScoreToLeaderboard(submitScoreSec, 5);\n                    if (submitRes && submitRes.success) {\n                        userRank = submitRes.userRank;\n                        localTime = submitScoreSec;\n                        if (submitRes.username) {\n                            currentUsername = submitRes.username;\n                            leaderboardSelfUsername = submitRes.username;\n                        }\n                        // Use entries directly from submit response to avoid eventual-consistency lag\n                        if (Array.isArray(submitRes.entries) && submitRes.entries.length > 0) {\n                            entries = submitRes.entries;\n                            noteLeaderboardSelfIdentity(entries, userRank);\n                            if (!currentUsername) currentUsername = leaderboardSelfUsername;\n                        }\n                    }\n                }\n\n                // Only fetch separately if we didn't get entries from submit\n                if (entries.length === 0) {\n                    const topRes = await lib.getTopNEntriesFromLeaderboard(5);\n                    if (topRes && topRes.userRank !== null && topRes.userRank !== undefined) {\n                        userRank = topRes.userRank;\n                    }\n                    entries = topRes && Array.isArray(topRes.entries) ? topRes.entries : [];\n                    noteLeaderboardSelfIdentity(entries, userRank);\n                    if (!currentUsername) currentUsername = leaderboardSelfUsername;\n                }\n\n                renderLeaderboardRows(contentEl, entries, {\n                    userRank,\n                    currentUserTimeSec: localTime,\n                    highlightClass,\n                    forceYouLabel: true,\n                    ensureUserScore: submitScoreSec,\n                    currentUsername\n                });\n            } catch (error) {\n                const pb = UserDataManager.getPersonalBest();\n                const displayTime = localTime !== null && localTime !== undefined ? localTime : pb;\n                if (displayTime !== null && displayTime !== undefined) {\n                    contentEl.innerHTML = `<div class=\"lb-error\">Leaderboard unavailable.<br>Your Best: <strong>${formatMMSS(displayTime)}</strong></div>`;\n                } else {\n                    contentEl.innerHTML = '<div class=\"lb-error\">Leaderboard unavailable right now. Try again soon!</div>';\n                }\n                lib.log('[Leaderboard] Failed to load leaderboard: ' + error.message);\n            }\n        }\n\n        async function triggerDefeatScreen() {\n            if (isDefeatActive || isVictoryActive || mode !== 'play') return;\n            isDefeatActive = true;\n\n            RouteRecorder.discard();\n\n            if (mode === 'play') {\n                SessionMetrics.endRound({\n                    completed: false,\n                    completionPct: totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0\n                });\n                SessionMetrics.upload('ghost_victory');\n            }\n\n            const goalCardEl = document.getElementById('goal-card');\n            const levelHudEl = document.getElementById('level-hud');\n            const timerPanelEl = document.getElementById('timer-panel');\n            if (goalCardEl) goalCardEl.style.display = 'none';\n            if (levelHudEl) levelHudEl.style.display = 'none';\n            if (timerPanelEl) timerPanelEl.style.display = 'none';\n\n            const playerPct = totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0;\n\n            const defeatScreen = document.getElementById('screen-defeat');\n            if (defeatScreen) {\n                defeatScreen.classList.remove('screen-hidden');\n\n                const overlay = document.getElementById('defeat-overlay');\n                const card = document.getElementById('defeat-card');\n                const yourGhost = document.querySelector('.header-your-ghost');\n                const ateTheWorld = document.querySelector('.header-ate-the-world');\n                const middle = document.querySelector('.defeat-middle');\n                const leaderboardBlock = document.getElementById('defeat-leaderboard');\n                const replayBtn = document.getElementById('defeat-replay-btn');\n                const playerPctEl = document.getElementById('defeat-player-pct');\n                const timeToBeatEl = document.getElementById('defeat-time-to-beat');\n\n                if (playerPctEl) playerPctEl.textContent = playerPct.toFixed(1) + '%';\n                \n                if (timeToBeatEl) {\n                    const pbTime = UserDataManager.getPersonalBest();\n                    const ghostRoute = resolveGhostPlaybackRoute();\n                    const timeToBeatSecs = pbTime !== null ? pbTime : (ghostRoute ? ghostRoute.totalTimeSec : 0);\n                    timeToBeatEl.textContent = formatMMSS(timeToBeatSecs);\n                }\n\n                requestAnimationFrame(() => {\n                    if (overlay) overlay.style.opacity = '1';\n                    setTimeout(() => {\n                        if (card) {\n                            card.style.transform = 'scale(1)';\n                            card.style.opacity = '1';\n                            card.style.transition = 'transform 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.6s ease';\n                        }\n                    }, 100);\n                    setTimeout(() => {\n                        if (yourGhost) {\n                            yourGhost.style.opacity = '1';\n                            yourGhost.style.transform = 'translateY(0)';\n                        }\n                        if (ateTheWorld) {\n                            ateTheWorld.style.opacity = '1';\n                            ateTheWorld.style.transform = 'translateY(0)';\n                        }\n                    }, 300);\n                    setTimeout(() => {\n                        if (middle) {\n                            middle.style.opacity = '1';\n                            middle.style.transform = 'scale(1)';\n                        }\n                    }, 500);\n                    setTimeout(() => {\n                        if (leaderboardBlock) {\n                            leaderboardBlock.style.opacity = '1';\n                            leaderboardBlock.style.transform = 'translateY(0)';\n                        }\n                    }, 760);\n                    setTimeout(() => {\n                        if (replayBtn) {\n                            replayBtn.style.opacity = '1';\n                            replayBtn.style.transform = 'translateY(0)';\n                        }\n                    }, 940);\n                });\n            }\n\n            await loadAndRenderLeaderboard({\n                containerId: 'defeat-leaderboard-content',\n                submitScoreSec: null,\n                highlightClass: 'lb-you-ghost',\n                fallbackTimeSec: UserDataManager.getPersonalBest()\n            });\n\n            lib.log('[PBGhost] Personal best ghost reached ' + getVictoryCompletionPercent() + '% \u2014 player defeated at ' + playerPct.toFixed(1) + '%');\n        }\n\n        async function triggerVictoryScreen() {\n            if (isVictoryActive || isDefeatActive || mode !== 'play') return;\n            isVictoryActive = true;\n\n            AudioSystem.play('victory-fanfare');\n\n            if (mode === 'play') {\n                SessionMetrics.endRound({ completed: true, completionPct: 100 });\n                SessionMetrics.upload(\"round_complete\");\n            }\n\n            const goalCardEl = document.getElementById('goal-card');\n            const levelHudEl = document.getElementById('level-hud');\n            const timerPanelEl = document.getElementById('timer-panel');\n            if (goalCardEl) goalCardEl.style.display = 'none';\n            if (levelHudEl) levelHudEl.style.display = 'none';\n            if (timerPanelEl) timerPanelEl.style.display = 'none';\n\n            const elapsedSecs = Math.floor((performance.now() - matchStartTime) / 1000);\n\n            // Integrate UserDataManager\n            let isNewBest = false;\n            let previousBest = null;\n            let routeSnapshot = null;\n            if (mode === 'play') {\n                previousBest = UserDataManager.getPersonalBest();\n                isNewBest = UserDataManager.checkNewBest(elapsedSecs);\n                pbRoutePipelineDebug('victory', 'elapsedSecs=' + elapsedSecs + ' previousBest=' + previousBest + ' isNewBest=' + isNewBest);\n                if (isNewBest) {\n                    routeSnapshot = RouteRecorder.buildRouteSnapshot(elapsedSecs, playerHole);\n                    summarizeRoute(routeSnapshot, 'victory routeSnapshot');\n                }\n                RouteRecorder.discard();\n                await UserDataManager.updatePersonalBest(elapsedSecs, routeSnapshot);\n                await UserDataManager.incrementRounds();\n            }\n\n            const victoryScreen = document.getElementById('screen-victory');\n            if (victoryScreen) {\n                victoryScreen.classList.remove('screen-hidden');\n\n                const overlay = document.getElementById('victory-overlay');\n                const card = document.getElementById('victory-card');\n                const youAte = document.querySelector('.header-you-ate');\n                const theWorld = document.querySelector('.header-the-world');\n                const middle = document.querySelector('.victory-middle');\n                const replayBtn = document.getElementById('victory-replay-btn');\n                const timeVal = document.getElementById('victory-time-val');\n                const pbContainer = document.getElementById('victory-pb-container');\n                const leaderboardBlock = document.getElementById('victory-leaderboard');\n\n                if (timeVal) timeVal.textContent = '00:00';\n                \n                if (pbContainer) {\n                    if (isNewBest) {\n                        pbContainer.innerHTML = '<div class=\"new-best-banner\">\u2728 NEW BEST! \u2728</div>';\n                    } else {\n                        const displayBest = previousBest !== null ? formatMMSS(previousBest) : formatMMSS(elapsedSecs);\n                        pbContainer.innerHTML = `<div class=\"best-understated-pill\"><span class=\"best-lbl\">BEST:</span>${displayBest}</div>`;\n                    }\n                    pbContainer.style.opacity = '0';\n                    pbContainer.style.transform = 'scale(0.5)';\n                }\n\n                requestAnimationFrame(() => {\n                    if (overlay) overlay.style.opacity = '1';\n                    \n                    setTimeout(() => {\n                        if (card) {\n                            card.style.transform = 'scale(1)';\n                            card.style.opacity = '1';\n                            card.style.transition = 'transform 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.6s ease';\n                        }\n                    }, 100);\n\n                    setTimeout(() => {\n                        if (youAte) {\n                            youAte.style.opacity = '1';\n                            youAte.style.transform = 'translateY(0)';\n                        }\n                        if (theWorld) {\n                            theWorld.style.opacity = '1';\n                            theWorld.style.transform = 'translateY(0)';\n                        }\n                    }, 300);\n\n                    if (isNewBest) {\n                        spawnConfetti();\n                    }\n\n                    setTimeout(() => {\n                        if (middle) {\n                            middle.style.opacity = '1';\n                            middle.style.transform = 'scale(1)';\n                        }\n                        if (pbContainer) {\n                            pbContainer.style.opacity = '1';\n                            pbContainer.style.transform = 'scale(1)';\n                        }\n                        \n                        let startCountTime = performance.now();\n                        const duration = 600; \n                        \n                        function updateCount(now) {\n                            const progress = Math.min(1, (now - startCountTime) / duration);\n                            const currentVal = Math.floor(progress * elapsedSecs);\n                            if (timeVal) timeVal.textContent = formatMMSS(currentVal);\n                            if (progress < 1) {\n                                requestAnimationFrame(updateCount);\n                            } else {\n                                if (timeVal) timeVal.textContent = formatMMSS(elapsedSecs);\n                            }\n                        }\n                        requestAnimationFrame(updateCount);\n                    }, 500);\n\n                    setTimeout(() => {\n                        if (leaderboardBlock) {\n                            leaderboardBlock.style.opacity = '1';\n                            leaderboardBlock.style.transform = 'translateY(0)';\n                        }\n                    }, 900);\n\n                    setTimeout(() => {\n                        if (replayBtn) {\n                            replayBtn.style.opacity = '1';\n                            replayBtn.style.transform = 'translateY(0)';\n                        }\n                    }, 1080);\n                });\n            }\n\n            await loadAndRenderLeaderboard({\n                containerId: 'victory-leaderboard-content',\n                submitScoreSec: elapsedSecs,\n                highlightClass: 'lb-you',\n                fallbackTimeSec: elapsedSecs\n            });\n        }\n\n        function spawnConfetti() {\n            const container = document.getElementById('confetti-container');\n            if (!container) return;\n            container.innerHTML = '';\n            const colors = ['#FFC107', '#FF5722', '#E91E63', '#9C27B0', '#3F51B5', '#00BCD4', '#4CAF50', '#8BC34A'];\n            for (let i = 0; i < 40; i++) {\n                const p = document.createElement('div');\n                p.className = 'confetti-particle';\n                p.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];\n                \n                const angle = Math.random() * Math.PI * 2;\n                const dist = 50 + Math.random() * 150;\n                const tx = Math.cos(angle) * dist;\n                const ty = Math.sin(angle) * dist - 80;\n                \n                p.style.left = '50%';\n                p.style.top = '50%';\n                p.style.transform = 'translate(-50%, -50%) scale(0)';\n                container.appendChild(p);\n\n                setTimeout(() => {\n                    p.style.transition = 'transform 1s cubic-bezier(0.1, 0.8, 0.3, 1), opacity 1s ease';\n                    const scale = 0.5 + Math.random() * 1.0;\n                    const rotation = Math.random() * 360;\n                    p.style.transform = `translate(calc(-50% + ${tx}px), calc(-50% + ${ty}px)) scale(${scale}) rotate(${rotation}deg)`;\n                    \n                    setTimeout(() => {\n                        p.style.opacity = '0';\n                    }, 500);\n                }, 50 + Math.random() * 100);\n            }\n        }\n\n        function restartGame() {\n            if (mode === 'play') {\n                SessionMetrics.markPlayAgain();\n                SessionMetrics.upload(\"play_again\");\n            }\n            isVictoryActive = false;\n            isDefeatActive = false;\n            const victoryScreen = document.getElementById('screen-victory');\n            if (victoryScreen) victoryScreen.classList.add('screen-hidden');\n            const defeatScreen = document.getElementById('screen-defeat');\n            if (defeatScreen) defeatScreen.classList.add('screen-hidden');\n\n            matchStartTime = performance.now();\n            lastLevelReachedSeconds = 0;\n            sessionRoundNumber += 1;\n            routeRecordingPending = true;\n            RouteRecorder.reset();\n            pbRoutePipelineDebug('restart', 'sessionRound=' + sessionRoundNumber + ' pbRouteSamples=' + (UserDataManager.getPersonalBestRoute() ? UserDataManager.getPersonalBestRoute().samples.length : 0));\n\n            playerHole.reset(1);\n            PointManager.reset();\n            ensureGhostBotSpawned();\n            resetGhostBot();\n            updateGhostRivalHUD();\n\n            const timerTextEl = document.getElementById('timer-text');\n            if (timerTextEl) timerTextEl.textContent = '00:00';\n\n            const youAte = document.querySelector('.header-you-ate');\n            const theWorld = document.querySelector('.header-the-world');\n            const middle = document.querySelector('.victory-middle');\n            const replayBtn = document.getElementById('victory-replay-btn');\n            const overlay = document.getElementById('victory-overlay');\n            const card = document.getElementById('victory-card');\n            const pbContainer = document.getElementById('victory-pb-container');\n            const victoryLeaderboard = document.getElementById('victory-leaderboard');\n            const victoryLeaderboardContent = document.getElementById('victory-leaderboard-content');\n\n            if (overlay) overlay.style.opacity = '0';\n            if (card) {\n                card.style.transform = 'scale(0)';\n                card.style.opacity = '0';\n            }\n            if (youAte) {\n                youAte.style.opacity = '0';\n                youAte.style.transform = 'translateY(20px)';\n            }\n            if (theWorld) {\n                theWorld.style.opacity = '0';\n                theWorld.style.transform = 'translateY(20px)';\n            }\n            if (middle) {\n                middle.style.opacity = '0';\n                middle.style.transform = 'scale(0.5)';\n            }\n            if (pbContainer) {\n                pbContainer.style.opacity = '0';\n                pbContainer.style.transform = 'scale(0.5)';\n                pbContainer.innerHTML = '';\n            }\n            if (victoryLeaderboard) {\n                victoryLeaderboard.style.opacity = '0';\n                victoryLeaderboard.style.transform = 'translateY(16px)';\n            }\n            if (victoryLeaderboardContent) {\n                victoryLeaderboardContent.innerHTML = 'Loading Leaderboard...';\n            }\n            if (replayBtn) {\n                replayBtn.style.opacity = '0';\n                replayBtn.style.transform = 'translateY(30px)';\n            }\n\n            const defeatOverlay = document.getElementById('defeat-overlay');\n            const defeatCard = document.getElementById('defeat-card');\n            const yourGhost = document.querySelector('.header-your-ghost');\n            const ateTheWorld = document.querySelector('.header-ate-the-world');\n            const defeatMiddle = document.querySelector('.defeat-middle');\n            const defeatLeaderboard = document.getElementById('defeat-leaderboard');\n            const defeatLeaderboardContent = document.getElementById('defeat-leaderboard-content');\n            const defeatReplayBtn = document.getElementById('defeat-replay-btn');\n            if (defeatOverlay) defeatOverlay.style.opacity = '0';\n            if (defeatCard) {\n                defeatCard.style.transform = 'scale(0)';\n                defeatCard.style.opacity = '0';\n            }\n            if (yourGhost) {\n                yourGhost.style.opacity = '0';\n                yourGhost.style.transform = 'translateY(20px)';\n            }\n            if (ateTheWorld) {\n                ateTheWorld.style.opacity = '0';\n                ateTheWorld.style.transform = 'translateY(20px)';\n            }\n            if (defeatMiddle) {\n                defeatMiddle.style.opacity = '0';\n                defeatMiddle.style.transform = 'scale(0.5)';\n            }\n            if (defeatLeaderboard) {\n                defeatLeaderboard.style.opacity = '0';\n                defeatLeaderboard.style.transform = 'translateY(16px)';\n            }\n            if (defeatLeaderboardContent) {\n                defeatLeaderboardContent.innerHTML = 'Loading Leaderboard...';\n            }\n            if (defeatReplayBtn) {\n                defeatReplayBtn.style.opacity = '0';\n                defeatReplayBtn.style.transform = 'translateY(30px)';\n            }\n\n            // Reset consumption state of all object instances\n            const instances = config.objectInstances || {};\n            Object.keys(instances).forEach(id => {\n                if (instances[id]) {\n                    instances[id].isBeingConsumed = false;\n                    instances[id].consumedByPlayer = false;\n                    instances[id].consumedByGhost = false;\n                }\n            });\n\n            ConsumeSystem.clearAll(worldObjects);\n            if (typeof ConsumeSystem.clearGhostConsumePool === 'function') {\n                ConsumeSystem.clearGhostConsumePool();\n            }\n\n            softResetWorld();\n            calculateTotalAvailablePoints();\n\n            // Re-initialize active managers\n            CarManager.init();\n            ObstructionFadeSystem.init();\n            \n            updateLevelHUD();\n            updateGoalHUD();\n            updateGhostRivalHUD();\n            \n            GameStateManager.setState(GAME_STATE_PLAYING);\n            pendingCameraGameplayReset = true;\n            if (mode === 'play') showJoystickAtRest();\n        }\n\n        let matchStartTime = performance.now();\n        let lastLevelReachedSeconds = 0;\n\n        let totalAvailablePoints = 0;\n        let playerConsumedPoints = 0;\n\n        const AudioSystem = (() => {\n            let audioCtx = null;\n            const bufferCache = {};\n            let musicSource = null;\n            let musicGainNode = null;\n            let musicMuted = false;\n\n            const TrackManager = (() => {\n                let playlist = ['bg-music', 'bg-music-oceanside'];\n\n                let trackIndex = 0;\n\n                function setPlaylist(tracks) {\n                    if (Array.isArray(tracks) && tracks.length > 0) {\n                        playlist = tracks;\n                        trackIndex = 0;\n                    }\n                }\n\n                function getCurrentTrackId() {\n                    return playlist[trackIndex];\n                }\n\n                function advanceTrack() {\n                    trackIndex = (trackIndex + 1) % playlist.length;\n                }\n\n                return { setPlaylist, getCurrentTrackId, advanceTrack, getPlaylist: () => playlist };\n            })();\n\n            const soundDefs = [\n                { id: 'bg-music', url: (lib.getAsset('bg-music') || {}).url, volume: 0.075 },\n                { id: 'bg-music-oceanside', url: (lib.getAsset('Oceanside -- Chill_Drum and Bass.mp3') || {}).url, volume: 0.05 },\n                { id: 'victory-fanfare', url: (lib.getAsset('win-fanfare-v2.mp3') || {}).url, volume: 0.5 },\n                { id: 'lip-licking-2', url: (lib.getAsset('snd-lip-licking-2') || {}).url, volume: 0.5 },\n                { id: 'gulp-2', url: (lib.getAsset('snd-gulp-2') || {}).url, volume: 0.5 },\n                { id: 'star-pop', url: (lib.getAsset('snd-star-pop') || {}).url, volume: 0.1 },\n                { id: 'size-increase', url: (lib.getAsset('snd-size-increase') || {}).url, volume: 0.6 },\n                { id: 'snd-man-scream-01', url: (lib.getAsset('snd-man-scream-01') || {}).url, volume: 0.15},\n                { id: 'snd-man-scream-02', url: (lib.getAsset('snd-man-scream-02') || {}).url, volume: 0.15},\n                { id: 'snd-man-scream-03', url: (lib.getAsset('snd-man-scream-03') || {}).url, volume: 0.15 },\n                { id: 'snd-car-honk-01', url: (lib.getAsset('snd-car-honk-01') || {}).url, volume: 0.2 },\n                { id: 'snd-car-honk-02', url: (lib.getAsset('snd-car-honk-02') || {}).url, volume: 0.15},\n                { id: 'snd-leaves-01', url: (lib.getAsset('snd-leaves-01') || {}).url, volume: 0.35 },\n                { id: 'snd-clank-01', url: (lib.getAsset('snd-clank-01') || {}).url, volume: 0.1 },\n                { id: 'snd-wrong', url: (lib.getAsset('snd-wrong') || {}).url, volume: 0.4 }\n            ];\n\n            const soundGroups = {\n                consume: ['lip-licking-2', 'gulp-2']\n            };\n\n            function init() {\n                if (!audioCtx) {\n                    audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n                }\n            }\n\n            function getMusicGain(trackId) {\n                const def = soundDefs.find(s => s.id === trackId);\n                const baseVol = def ? def.volume : 0.075;\n                const globalVol = window.gameConfig.globalVolume !== undefined ? window.gameConfig.globalVolume : 1;\n                return musicMuted ? 0 : (baseVol * globalVol);\n            }\n\n            function unlock() {\n                init();\n                if (audioCtx.state === 'suspended') {\n                    audioCtx.resume();\n                }\n                const buffer = audioCtx.createBuffer(1, 1, 22050);\n                const source = audioCtx.createBufferSource();\n                source.buffer = buffer;\n                source.connect(audioCtx.destination);\n                source.start(0);\n\n                const firstTrack = TrackManager.getCurrentTrackId();\n                if (!musicSource && bufferCache[firstTrack]) {\n                    playCurrentTrack();\n                }\n            }\n\n            function playCurrentTrack() {\n                const id = TrackManager.getCurrentTrackId();\n                if (!audioCtx || !bufferCache[id]) return;\n                if (audioCtx.state === 'suspended') audioCtx.resume();\n                if (musicSource) {\n                    try { musicSource.stop(); } catch(e) {}\n                    musicSource.onended = null;\n                }\n                musicSource = audioCtx.createBufferSource();\n                musicSource.buffer = bufferCache[id];\n                musicSource.loop = false;\n                if (!musicGainNode) {\n                    musicGainNode = audioCtx.createGain();\n                    musicGainNode.connect(audioCtx.destination);\n                }\n                musicGainNode.gain.value = getMusicGain(id);\n                musicSource.connect(musicGainNode);\n                musicSource.onended = () => {\n                    TrackManager.advanceTrack();\n                    playCurrentTrack();\n                };\n                musicSource.start(0);\n            }\n\n            function toggleMusic() {\n                musicMuted = !musicMuted;\n                const btn = document.getElementById('music-toggle-btn');\n                if (btn) {\n                    btn.textContent = musicMuted ? '\ud83d\udd07' : '\ud83c\udfb5';\n                    btn.style.opacity = musicMuted ? '0.5' : '1.0';\n                }\n                if (musicGainNode) {\n                    musicGainNode.gain.setValueAtTime(\n                        getMusicGain(TrackManager.getCurrentTrackId()),\n                        audioCtx.currentTime\n                    );\n                }\n            }\n\n            async function preloadAll(onProgress) {\n                init();\n                const loadable = soundDefs.filter(def => def.url);\n                let loaded = 0;\n                const report = () => {\n                    if (onProgress) onProgress(loaded, loadable.length);\n                };\n                const promises = soundDefs.map(async (def) => {\n                    if (!def.url) return;\n                    try {\n                        const response = await fetch(def.url);\n                        const arrayBuffer = await response.arrayBuffer();\n                        bufferCache[def.id] = await audioCtx.decodeAudioData(arrayBuffer);\n                        loaded++;\n                        report();\n                    } catch (e) {\n                        lib.log('Audio load failed: ' + def.id);\n                    }\n                });\n                await Promise.all(promises);\n            }\n\n            function play(id, volumeOverride) {\n                if (!audioCtx || !bufferCache[id]) return;\n                if (audioCtx.state === 'suspended') audioCtx.resume();\n                const source = audioCtx.createBufferSource();\n                source.buffer = bufferCache[id];\n                const gain = audioCtx.createGain();\n                const def = soundDefs.find(s => s.id === id);\n                const baseVol = volumeOverride !== undefined ? volumeOverride : (def ? def.volume : 1);\n                const globalVol = window.gameConfig.globalVolume !== undefined ? window.gameConfig.globalVolume : 1;\n                gain.gain.value = baseVol * globalVol;\n                source.connect(gain);\n                gain.connect(audioCtx.destination);\n                source.start(0);\n            }\n\n            function playRandom(groupName) {\n                const group = soundGroups[groupName];\n                if (!group || group.length === 0) return;\n                const id = group[Math.floor(Math.random() * group.length)];\n                play(id);\n            }\n\n            return { init, unlock, preloadAll, play, playRandom, toggleMusic, TrackManager };\n        })();\n\n\n        const GAME_STATE_LOADING = 'loading';\n        const GAME_STATE_READY = 'ready';\n        const GAME_STATE_PLAYING = 'playing';\n\n        let pendingCameraIntroTransition = false;\n        let pendingCameraGameplayReset = false;\n\n        const LoadingManager = (() => {\n            const progressFill = document.getElementById('loading-progress-fill');\n            const progressPct = document.getElementById('loading-progress-pct');\n            let displayedProgress = 0;\n            let targetProgress = 0;\n            let animFrame = null;\n\n            function updateUI() {\n                if (progressFill) progressFill.style.width = displayedProgress + '%';\n                if (progressPct) progressPct.textContent = displayedProgress + '%';\n            }\n\n            function animate() {\n                if (displayedProgress < targetProgress) {\n                    const delta = targetProgress - displayedProgress;\n                    const step = Math.max(1, Math.ceil(delta * 0.08));\n                    displayedProgress = Math.min(targetProgress, displayedProgress + step);\n                    updateUI();\n                    animFrame = requestAnimationFrame(animate);\n                } else {\n                    animFrame = null;\n                }\n            }\n\n            function setProgress(pct) {\n                targetProgress = Math.min(100, Math.max(targetProgress, Math.round(pct)));\n                if (!animFrame) animFrame = requestAnimationFrame(animate);\n            }\n\n            return { setProgress, getProgress: () => displayedProgress };\n        })();\n\n        LoadingManager.setProgress(0);\n        let audioPreloadPromise = Promise.resolve();\n\n        const GameStateManager = (() => {\n            let currentState = mode === 'edit' ? GAME_STATE_PLAYING : GAME_STATE_LOADING;\n\n            const screenLoading = document.getElementById('screen-loading');\n            const screenTapStart = document.getElementById('screen-tap-start');\n            const goalCardEl = document.getElementById('goal-card');\n            const levelHudEl = document.getElementById('level-hud');\n            const timerPanelEl = document.getElementById('timer-panel');\n\n            function beginRun() {\n                AudioSystem.unlock();\n                pendingCameraIntroTransition = true;\n                setState(GAME_STATE_PLAYING);\n                if (mode === 'play') showJoystickAtRest();\n            }\n\n            function setState(newState) {\n                const prevState = currentState;\n                currentState = newState;\n\n                screenLoading.classList.add('screen-hidden');\n                if (screenTapStart) screenTapStart.classList.add('screen-hidden');\n\n                goalCardEl.style.display = 'none';\n                levelHudEl.style.display = 'none';\n                if (timerPanelEl) timerPanelEl.style.display = 'none';\n\n                const musicBtn = document.getElementById('music-toggle-btn');\n\n                switch (newState) {\n                    case GAME_STATE_LOADING:\n                        screenLoading.classList.remove('screen-hidden');\n                        if (musicBtn) musicBtn.style.display = 'none';\n                        break;\n                    case GAME_STATE_READY:\n                        if (musicBtn) musicBtn.style.display = 'flex';\n                        if (screenTapStart) screenTapStart.classList.remove('screen-hidden');\n                        break;\n                    case GAME_STATE_PLAYING:\n                        if (musicBtn) musicBtn.style.display = 'flex';\n                        matchStartTime = performance.now();\n                        lastLevelReachedSeconds = 0;\n                        routeRecordingPending = true;\n                        if (mode === 'play') {\n                            SessionMetrics.startRound();\n                            goalCardEl.style.display = '';\n                            levelHudEl.style.display = '';\n                            if (timerPanelEl) timerPanelEl.style.display = 'flex';\n\n                            const ghostActive = isGhostActiveThisRound();\n                            if (!ghostActive) {\n                                ModalSystem.show({\n                                    title: 'EAT THE WORLD',\n                                    body: 'Try to finish as fast as possible.',\n                                    completionFlag: 'first_run_onboarding'\n                                });\n                            } else {\n                                const data = UserDataManager.getData();\n                                if (!data.completedOnboardings || !data.completedOnboardings['ghost_onboarding']) {\n                                    ModalSystem.show({\n                                        title: 'RACE YOUR GHOST',\n                                        body: 'Beat it to set a new record.',\n                                        completionFlag: 'ghost_onboarding'\n                                    });\n                                }\n                            }\n                        }\n                        break;\n                }\n\n                lib.log('Game State: ' + prevState + ' \u2192 ' + newState);\n            }\n\n            function getState() {\n                return currentState;\n            }\n\n            if (screenTapStart) {\n                const handleTapStart = (e) => {\n                    e.preventDefault();\n                    e.stopPropagation();\n                    if (currentState !== GAME_STATE_READY) return;\n                    beginRun();\n                };\n                screenTapStart.addEventListener('click', handleTapStart);\n                screenTapStart.addEventListener('touchstart', handleTapStart, { passive: false });\n            }\n\n            const victoryReplayBtn = document.getElementById('victory-replay-btn');\n            if (victoryReplayBtn) {\n                victoryReplayBtn.addEventListener('click', () => {\n                    AudioSystem.unlock();\n                    restartGame();\n                });\n            }\n\n            const defeatReplayBtn = document.getElementById('defeat-replay-btn');\n            if (defeatReplayBtn) {\n                defeatReplayBtn.addEventListener('click', () => {\n                    AudioSystem.unlock();\n                    restartGame();\n                });\n            }\n\n            setState(currentState);\n\n            return { setState, getState, beginRun };\n        })();\n\n        const gameWorld = document.getElementById('game-world');\n        const canvas = document.getElementById('game-canvas');\n        const modeLabel = document.getElementById('mode-label');\n        const joystickOuter = document.getElementById('joystick-outer');\n        const joystickInner = document.getElementById('joystick-inner');\n\n        const logoAsset = lib.getAsset('logo-art');\n        if (logoAsset && logoAsset.url) {\n            const introLogo = document.getElementById('intro-logo');\n            if (introLogo) {\n                introLogo.src = logoAsset.url;\n            }\n        }\n\n        modeLabel.textContent = mode === 'edit' ? '\u270f\ufe0f Edit Mode' : '';\n\n        const goalCard = document.getElementById('goal-card');\n        const levelHud = document.getElementById('level-hud');\n\n        const config = window.gameConfig;\n        if (config.useFakeLeaderboard === undefined) config.useFakeLeaderboard = true;\n        if (config.fakeLeaderboard === undefined) {\n            config.fakeLeaderboard = [\n                { username: \"Shawn\", score: 72 },\n                { username: \"Alex\", score: 78 },\n                { username: \"Sarah\", score: 81 },\n                { username: \"Judy\", score: 85 },\n                { username: \"Mike\", score: 87 }\n            ];\n        }\n\n        // Fake leaderboard runtime entries\n        let fakeLeaderboardEntries = null;\n        function getFakeLeaderboardEntries() {\n            if (!fakeLeaderboardEntries) {\n                fakeLeaderboardEntries = JSON.parse(JSON.stringify(config.fakeLeaderboard));\n            }\n            return fakeLeaderboardEntries;\n        }\n\n        function fakeAddPlayerScoreToLeaderboard(score, numEntries) {\n            const entries = getFakeLeaderboardEntries();\n            let playerEntry = entries.find(e => e.username === \"YOU\");\n            if (playerEntry) {\n                if (score < playerEntry.score) {\n                    playerEntry.score = score;\n                }\n            } else {\n                entries.push({ username: \"YOU\", score: score });\n            }\n            entries.sort((a, b) => a.score - b.score);\n            const playerRank = entries.findIndex(e => e.username === \"YOU\") + 1;\n            return {\n                success: true,\n                userRank: playerRank,\n                entries: entries.slice(0, numEntries)\n            };\n        }\n\n        function fakeGetTopNEntriesFromLeaderboard(numEntries) {\n            const entries = getFakeLeaderboardEntries();\n            entries.sort((a, b) => a.score - b.score);\n            const playerRank = entries.findIndex(e => e.username === \"YOU\") + 1;\n            return {\n                userRank: playerRank > 0 ? playerRank : null,\n                entries: entries.slice(0, numEntries)\n            };\n        }\n\n        const originalAddPlayerScore = lib.addPlayerScoreToLeaderboard;\n        const originalGetTopEntries = lib.getTopNEntriesFromLeaderboard;\n\n        lib.addPlayerScoreToLeaderboard = async function(score, numEntries) {\n            if (config.useFakeLeaderboard) {\n                return fakeAddPlayerScoreToLeaderboard(score, numEntries);\n            }\n            if (originalAddPlayerScore) {\n                return await originalAddPlayerScore.call(lib, score, numEntries);\n            }\n            return { success: false, error: 'No original API implementation' };\n        };\n\n        lib.getTopNEntriesFromLeaderboard = async function(numEntries) {\n            if (config.useFakeLeaderboard) {\n                return fakeGetTopNEntriesFromLeaderboard(numEntries);\n            }\n            if (originalGetTopEntries) {\n                return await originalGetTopEntries.call(lib, numEntries);\n            }\n            return { entries: [] };\n        };\n\n        if (config.consumeDepthThreshold === undefined) config.consumeDepthThreshold = -3.5;\n        if (config.consumeFallDuration === undefined) config.consumeFallDuration = 0.75;\n        if (config.consumeNearEdgeShift === undefined) config.consumeNearEdgeShift = 0.8;\n        if (config.consumeSimpleProfileLevelThreshold === undefined) config.consumeSimpleProfileLevelThreshold = 2;\n        if (config.showConsumeTargetDebug === undefined) config.showConsumeTargetDebug = false;\n        if (config.enableGhostDebug === undefined) config.enableGhostDebug = false;\n        if (config.ghostPacingThreshold === undefined) config.ghostPacingThreshold = 5.0;\n        if (config.pbRouteSampleInterval === undefined) config.pbRouteSampleInterval = 0.4;\n        if (config.spawnGhostInRound1 === undefined) config.spawnGhostInRound1 = false;\n        if (config.botGhostCanWin === undefined) config.botGhostCanWin = false;\n        if (config.useDebugGhostRoute === undefined) config.useDebugGhostRoute = false;\n        if (config.debugGhostRoute === undefined) config.debugGhostRoute = null;\n        if (config.pbRoutePipelineDebug === undefined) config.pbRoutePipelineDebug = true;\n        if (config.onboardingDelay === undefined) config.onboardingDelay = 1.0;\n        if (config.onboardingDuration === undefined) config.onboardingDuration = 3.5;\n        if (config.ghostOnboardingDuration === undefined) config.ghostOnboardingDuration = 5.0;\n        if (config.ghostType === undefined) {\n            config.ghostType = config.usePersonalBestGhost === false ? 'Bot' : 'PersonalBest';\n        }\n        if (config.victoryCompletionPercent === undefined) config.victoryCompletionPercent = 100;\n        if (config.ghostObjectOpacity === undefined) config.ghostObjectOpacity = 0.45;\n\n        function getVictoryCompletionPercent() {\n            return config.victoryCompletionPercent !== undefined ? config.victoryCompletionPercent : 100;\n        }\n\n        function getGhostObjectOpacity() {\n            return config.ghostObjectOpacity !== undefined ? config.ghostObjectOpacity : 0.45;\n        }\n\n        function getGhostRimOpacity() {\n            return Math.min(1, getGhostObjectOpacity() * (0.55 / 0.45));\n        }\n\n        const QualityProfile = (() => {\n            const MOBILE_MAX_DPR = 1.5;\n            const DESKTOP_MAX_DPR = 2;\n            const MOBILE_SHADOW_SIZE = 512;\n            const DESKTOP_SHADOW_SIZE = 1024;\n\n            let mobile = false;\n\n            function detectMobile() {\n                if (typeof lib.shouldUseTouchControls === 'function') {\n                    try {\n                        return !!lib.shouldUseTouchControls();\n                    } catch (e) { /* host may not implement */ }\n                }\n                return window.matchMedia('(hover: none) and (pointer: coarse)').matches;\n            }\n\n            function disableMeshShadows(rootMesh) {\n                if (!rootMesh) return;\n                rootMesh.traverse(child => {\n                    if (child.isMesh) {\n                        child.castShadow = false;\n                        child.receiveShadow = false;\n                    }\n                });\n            }\n\n            function apply(renderer, dirLight) {\n                mobile = detectMobile();\n                renderer.setPixelRatio(Math.min(window.devicePixelRatio, mobile ? MOBILE_MAX_DPR : DESKTOP_MAX_DPR));\n                renderer.shadowMap.type = mobile ? THREE.PCFShadowMap : THREE.PCFSoftShadowMap;\n                if (dirLight && dirLight.shadow) {\n                    const size = mobile ? MOBILE_SHADOW_SIZE : DESKTOP_SHADOW_SIZE;\n                    dirLight.shadow.mapSize.set(size, size);\n                }\n            }\n\n            function useMobileAtConstruction() {\n                return detectMobile();\n            }\n\n            function isMobile() {\n                return mobile;\n            }\n\n            return { apply, isMobile, useMobileAtConstruction, disableMeshShadows };\n        })();\n\n        function applyGhostWorldObjectMaterialStyle(rootMesh) {\n            const opacity = getGhostObjectOpacity();\n            rootMesh.traverse(child => {\n                if (!child.isMesh || !child.material) return;\n                const mats = Array.isArray(child.material) ? child.material : [child.material];\n                mats.forEach(m => {\n                    m.transparent = true;\n                    m.opacity = opacity;\n                    m.depthWrite = false;\n                    if (m.color) {\n                        m.color.lerp(new THREE.Color(0xa8e8ff), 0.35);\n                    }\n                });\n            });\n            QualityProfile.disableMeshShadows(rootMesh);\n        }\n\n        function applyGhostObjectOpacityToWorld() {\n            if (ghostBot && typeof ghostBot.applyGhostMaterialStyle === 'function') {\n                ghostBot.applyGhostMaterialStyle();\n            }\n            Object.keys(worldObjects).forEach(id => {\n                const mesh = worldObjects[id];\n                if (mesh && mesh.userData && mesh.userData.isGhostResidue) {\n                    applyGhostWorldObjectMaterialStyle(mesh);\n                }\n            });\n            for (let i = 0; i < manifestAnimations.length; i++) {\n                manifestAnimations[i].normalOpacity = getGhostObjectOpacity();\n            }\n        }\n\n        LoadingManager.setProgress(10);\n        await UserDataManager.loadUserData();\n        LoadingManager.setProgress(25);\n        audioPreloadPromise = AudioSystem.preloadAll((loaded, total) => {\n            const audioWeight = total > 0 ? (loaded / total) * 40 : 40;\n            LoadingManager.setProgress(25 + audioWeight);\n        });\n\n        let TERRAIN_CELL_SIZE = config.terrainCellSize || 2;\n        let TERRAIN_HALF_GRID = (config.gridSize || 100) / 2;\n        let TERRAIN_COLS = Math.floor((config.gridSize || 100) / TERRAIN_CELL_SIZE);\n        let TERRAIN_ROWS = TERRAIN_COLS;\n\n        if (config.selectedEditLevel === undefined) config.selectedEditLevel = 0;\n        let selectedEditLevel = config.selectedEditLevel;\n\n        function showSettings() {\n            const lvlIdx = selectedEditLevel;\n            const levelOptions = [];\n            for (let i = 1; i <= 15; i++) {\n                levelOptions.push({ label: 'Level ' + i, value: i - 1 });\n            }\n\n            lib.showGameParameters({\n                name: 'Eat the World Settings',\n                params: {\n                    'Victory Completion %': { key: 'victoryCompletionPercent', type: 'slider', min: 90, max: 100, step: 0.5 },\n                    'Ghost Object Opacity': { key: 'ghostObjectOpacity', type: 'slider', min: 0.1, max: 1, step: 0.05, onChange: () => { applyGhostObjectOpacityToWorld(); } },\n                    'Use Fake Leaderboard': { key: 'useFakeLeaderboard', type: 'checkbox' },\n                    'Ghost Type': {\n                        key: 'ghostType',\n                        type: 'dropdown',\n                        options: [\n                            { label: 'Bot', value: 'Bot' },\n                            { label: 'Personal Best', value: 'PersonalBest' }\n                        ]\n                    },\n                    'Spawn Ghost In Round 1': { key: 'spawnGhostInRound1', type: 'checkbox' },\n                    'Bot Ghost Can Win': { key: 'botGhostCanWin', type: 'checkbox' },\n                    'Use Debug Ghost Route': { key: 'useDebugGhostRoute', type: 'checkbox' },\n                    'Game Version': { key: 'gameVersion', type: 'string' },\n                    'Enable Metrics': { key: 'metricsEnabled', type: 'checkbox' },\n                    'Show Victory Screen after 5 (seconds)': { key: 'showVictoryAfter5', type: 'checkbox' },\n                    'Start at Selected Level': { key: 'startAtSelectedLevel', type: 'checkbox' },\n                    'Test Metrics at Level 2': { key: 'testMetricsAtLevel2Enabled', type: 'checkbox' },\n                    'Show Grid Lines':    { key: 'showGridLines',   type: 'checkbox', onChange: (v) => { config.showGridLines = v; if (gridHelper) gridHelper.visible = v; } },\n                    'Show Grid Groups':   { key: 'showGridGroups',  type: 'checkbox' },\n                    'Show AI Waypoints':  { key: 'showAIWaypoints', type: 'checkbox' },\n\n                    '\u2699 Edit Level':    { key: 'selectedEditLevel', type: 'dropdown', options: levelOptions, onChange: (v) => { selectedEditLevel = v; config.selectedEditLevel = v; showSettings(); } },\n                    'Cam Height':      { key: 'levelCameraHeights.' + lvlIdx,    type: 'slider', min: 5,   max: 80, step: 1 },\n                    'Cam Tilt':        { key: 'levelCameraTilts.' + lvlIdx,      type: 'slider', min: 10,  max: 89, step: 1 },\n                    'Speed Mult':      { key: 'levelSpeedMultipliers.' + lvlIdx, type: 'slider', min: 0.5, max: 6,  step: 0.1 },\n                    'Scale Mult':      { key: 'levelScaleMultipliers.' + lvlIdx, type: 'slider', min: 0.5, max: 8,  step: 0.1 },\n                    'Points to Next':  { key: 'levelThresholds.' + lvlIdx,       type: 'number' },\n\n                    'Hole Speed':                    { key: 'holeSpeed',          type: 'slider', min: 1,   max: 15,  step: 0.5 },\n                    'Camera Rotation':               { key: 'cameraRotation',     type: 'slider', min: 0,   max: 360, step: 1 },\n                    'Initial Cam Height':            { key: 'initialCameraHeight', type: 'slider', min: 5,   max: 120, step: 1 },\n                    'Initial Cam Tilt':              { key: 'initialCameraTilt',   type: 'slider', min: 10,  max: 89,  step: 1 },\n                    'Initial Cam Distance':          { key: 'initialCameraDistance', type: 'slider', min: 0, max: 200, step: 1 },\n                    'Initial Cam Transition (s)':    { key: 'initialCameraTransitionDuration', type: 'slider', min: 0.3, max: 5, step: 0.1 },\n                    'Car Speed':                     { key: 'carSpeed',           type: 'slider', min: 1,   max: 15,  step: 0.5 },\n                    'L1 Fall Duration Mult':         { key: 'consumeTweenByLevel.1.fallDurationMult', type: 'slider', min: 0.1, max: 2.0, step: 0.05 },\n                    'Consume Fall Duration':         { key: 'consumeFallDuration', type: 'slider', min: 0.3, max: 2.0, step: 0.05 },\n                    'Consume Depth Threshold':       { key: 'consumeDepthThreshold', type: 'slider', min: -10,  max: -1,  step: 0.5 },\n                    'Consume Near Edge Shift':       { key: 'consumeNearEdgeShift', type: 'slider', min: 0,    max: 1.0, step: 0.05 },\n                    'Simple Profile Level Max':      { key: 'consumeSimpleProfileLevelThreshold', type: 'slider', min: 1,   max: 15,  step: 1 },\n                    'Show Consume Target Debug':     { key: 'showConsumeTargetDebug', type: 'checkbox', onChange: (v) => { ConsumeTargetDebug.setVisible(v); } },\n                    'Joystick Full Speed Threshold': { key: 'joystickThreshold',  type: 'slider', min: 1,   max: 100, step: 1 },\n                    'Ghost Min Player Dist':         { key: 'ghostBotMinPlayerDistance', type: 'slider', min: 0,   max: 30,  step: 0.5 },\n                    'Enable Ghost Debug':            { key: 'enableGhostDebug', type: 'checkbox', onChange: (v) => { config.enableGhostDebug = v; updateGhostDebugUI(); } },\n                    'Ghost Pacing Threshold':        { key: 'ghostPacingThreshold', type: 'slider', min: 1, max: 20, step: 0.5 },\n                    'PB Route Sample Interval':      { key: 'pbRouteSampleInterval', type: 'slider', min: 0.25, max: 1.0, step: 0.05 },\n                    'Global Volume':                 { key: 'globalVolume',       type: 'slider', min: 0,   max: 1,   step: 0.05 },\n                    'Obstruction Fade':              { key: 'obstructionFadeOpacity', type: 'slider', min: 0, max: 1, step: 0.05 },\n                    'Obstruction View Radius':       { key: 'obstructionViewRadius', type: 'slider', min: 2,   max: 20,  step: 0.5 },\n                    'Obstruction Padding':           { key: 'obstructionPadding',    type: 'slider', min: 0,   max: 10,  step: 0.5 },\n\n                    'Target Score': { key: 'targetScore', type: 'number' },\n\n                    'Base Hole Scale':    { key: 'baseHoleScale',  type: 'slider', min: 0.5, max: 3,  step: 0.1 },\n                    'Base Camera Height': { key: 'cameraHeight',   type: 'slider', min: 3,   max: 50, step: 1 },\n                    'Base Camera Tilt':   { key: 'cameraAngle',    type: 'slider', min: 10,  max: 89, step: 1 },\n\n                    'Onboarding Delay (s)':   { key: 'onboardingDelay', type: 'slider', min: 0, max: 10, step: 0.1 },\n                    'Onboarding Duration (s)': { key: 'onboardingDuration', type: 'slider', min: 1, max: 15, step: 0.5 },\n                    'Ghost Onboarding Duration (s)': { key: 'ghostOnboardingDuration', type: 'slider', min: 1, max: 15, step: 0.5 },\n                    'Grid Size':               { key: 'gridSize',       type: 'slider', min: 50, max: 1000, step: 10 },\n                    'Grid Group Size':         { key: 'gridGroupSize',   type: 'slider', min: 5,  max: 50,  step: 5 },\n                    'Cell Size (grid+terrain)': { key: 'terrainCellSize', type: 'slider', min: 2, max: 20,  step: 1, onChange: (v) => { config.terrainCellSize = v; buildWorld(); TerrainRenderer.rebuildAll(); } },\n\n                    'Ambient Intensity':  { key: 'ambientIntensity', type: 'slider', min: 0, max: 1.5, step: 0.05 },\n                    'Sun Intensity':      { key: 'sunIntensity',     type: 'slider', min: 0, max: 2,   step: 0.05 },\n                    'Sun Height':         { key: 'sunHeight',        type: 'slider', min: 5, max: 50,  step: 1 },\n                    'Sun Offset X':       { key: 'sunOffsetX',       type: 'slider', min: -30, max: 30, step: 1 },\n                    'Sun Offset Z':       { key: 'sunOffsetZ',       type: 'slider', min: -30, max: 30, step: 1 }\n                }\n            });\n        }\n\n        showSettings();\n\n\n        const useMobileQuality = QualityProfile.useMobileAtConstruction();\n        const renderer = new THREE.WebGLRenderer({ canvas, antialias: !useMobileQuality });\n        LoadingManager.setProgress(55);\n        renderer.shadowMap.enabled = true;\n\n        const scene = new THREE.Scene();\n        scene.background = new THREE.Color(0x87ceeb);\n\n        const camera = new THREE.PerspectiveCamera(\n            50,\n            gameWorld.clientWidth / gameWorld.clientHeight,\n            0.1,\n            1000\n        );\n\n        function handleResize() {\n            const w = gameWorld.clientWidth;\n            const h = gameWorld.clientHeight;\n            renderer.setSize(w, h);\n            camera.aspect = w / h;\n            camera.updateProjectionMatrix();\n        }\n        handleResize();\n        window.addEventListener('resize', handleResize);\n\n        const ambientLight = new THREE.AmbientLight(0xeef4ff, config.ambientIntensity !== undefined ? config.ambientIntensity : 0.55);\n        scene.add(ambientLight);\n\n        const hemiLight = new THREE.HemisphereLight(0xffffff, 0xb0a090, 0.35);\n        scene.add(hemiLight);\n\n        const sunHeight = config.sunHeight !== undefined ? config.sunHeight : 25;\n        const sunOffsetX = config.sunOffsetX !== undefined ? config.sunOffsetX : 15;\n        const sunOffsetZ = config.sunOffsetZ !== undefined ? config.sunOffsetZ : 8;\n        const dirLight = new THREE.DirectionalLight(0xfffbe8, config.sunIntensity !== undefined ? config.sunIntensity : 0.85);\n        dirLight.position.set(sunOffsetX, sunHeight, sunOffsetZ);\n        dirLight.castShadow = true;\n        dirLight.shadow.camera.near = 1;\n        dirLight.shadow.camera.far = 80;\n        dirLight.shadow.camera.left = -35;\n        dirLight.shadow.camera.right = 35;\n        dirLight.shadow.camera.top = 35;\n        dirLight.shadow.camera.bottom = -35;\n        dirLight.shadow.bias = -0.001;\n        scene.add(dirLight);\n\n        QualityProfile.apply(renderer, dirLight);\n\n\n        function getRegistryEntry(id) {\n            const registry = config.objectRegistry || [];\n            return registry.find(entry => entry.id === id);\n        }\n\n        function getObjectCategory(instanceId) {\n            const instances = config.objectInstances || {};\n            const inst = instances[instanceId];\n            if (inst && inst.registryId) {\n                const entry = getRegistryEntry(inst.registryId);\n                if (entry && entry.objectCategory) {\n                    return entry.objectCategory;\n                }\n            }\n            return \"prop\";\n        }\n\n        function getRegistryCapability(entry, name) {\n            if (!entry) return false;\n            return !!entry[name];\n        }\n\n        function getModelTypeForInstance(instanceData) {\n            if (instanceData.registryId) {\n                const entry = getRegistryEntry(instanceData.registryId);\n                if (entry && entry.modelType) return entry.modelType;\n            }\n            return 'box';\n        }\n\n\n        const ObjectFactory = (() => {\n\n            function createCone() {\n                const group = new THREE.Group();\n\n                const baseGeo = new THREE.CylinderGeometry(0.18, 0.18, 0.04, 8);\n                const baseMat = new THREE.MeshLambertMaterial({ color: 0x333333 });\n                const base = new THREE.Mesh(baseGeo, baseMat);\n                base.position.y = 0.02;\n                base.castShadow = true;\n                base.receiveShadow = true;\n                group.add(base);\n\n                const coneGeo = new THREE.ConeGeometry(0.12, 0.55, 8);\n                const coneMat = new THREE.MeshLambertMaterial({ color: 0xff6600 });\n                const cone = new THREE.Mesh(coneGeo, coneMat);\n                cone.position.y = 0.04 + 0.275;\n                cone.castShadow = true;\n                cone.receiveShadow = true;\n                group.add(cone);\n\n                const stripeGeo = new THREE.CylinderGeometry(0.09, 0.105, 0.08, 8);\n                const stripeMat = new THREE.MeshLambertMaterial({ color: 0xffffff });\n                const stripe = new THREE.Mesh(stripeGeo, stripeMat);\n                stripe.position.y = 0.28;\n                group.add(stripe);\n\n                return group;\n            }\n\n            function createTrashCan() {\n                const group = new THREE.Group();\n\n                const bodyGeo = new THREE.CylinderGeometry(0.22, 0.20, 0.65, 12);\n                const bodyMat = new THREE.MeshLambertMaterial({ color: 0x555555 });\n                const body = new THREE.Mesh(bodyGeo, bodyMat);\n                body.position.y = 0.325;\n                body.castShadow = true;\n                body.receiveShadow = true;\n                group.add(body);\n\n                const lidGeo = new THREE.CylinderGeometry(0.24, 0.24, 0.06, 12);\n                const lidMat = new THREE.MeshLambertMaterial({ color: 0x777777 });\n                const lid = new THREE.Mesh(lidGeo, lidMat);\n                lid.position.y = 0.68;\n                lid.castShadow = true;\n                group.add(lid);\n\n                const handleGeo = new THREE.BoxGeometry(0.08, 0.05, 0.08);\n                const handleMat = new THREE.MeshLambertMaterial({ color: 0x999999 });\n                const handle = new THREE.Mesh(handleGeo, handleMat);\n                handle.position.y = 0.735;\n                group.add(handle);\n\n                return group;\n            }\n\n            function createTrashBag() {\n                const group = new THREE.Group();\n                const bagMat = new THREE.MeshStandardMaterial({\n                    color: 0x242424,\n                    roughness: 0.18,\n                    metalness: 0.1\n                });\n\n                // Main bag body - a squashed sphere\n                const bodyGeo = new THREE.SphereGeometry(0.22, 16, 16);\n                const body = new THREE.Mesh(bodyGeo, bagMat);\n                body.scale.set(1, 1.25, 0.95);\n                body.position.y = 0.22;\n                body.castShadow = true;\n                body.receiveShadow = true;\n                group.add(body);\n\n                // lumpy additions\n                const lump1Geo = new THREE.SphereGeometry(0.12, 8, 8);\n                const lump1 = new THREE.Mesh(lump1Geo, bagMat);\n                lump1.position.set(0.07, 0.14, 0.07);\n                lump1.scale.set(1, 1.1, 1);\n                lump1.castShadow = true;\n                group.add(lump1);\n\n                const lump2Geo = new THREE.SphereGeometry(0.1, 8, 8);\n                const lump2 = new THREE.Mesh(lump2Geo, bagMat);\n                lump2.position.set(-0.08, 0.18, -0.05);\n                lump2.castShadow = true;\n                group.add(lump2);\n\n                // Tie/knot at the top\n                const knotGeo = new THREE.CylinderGeometry(0.05, 0.07, 0.06, 8);\n                const knot = new THREE.Mesh(knotGeo, bagMat);\n                knot.position.y = 0.46;\n                knot.castShadow = true;\n                group.add(knot);\n\n                // Tie ribbon ears\n                const tieGeo = new THREE.ConeGeometry(0.035, 0.1, 4);\n                const tie1 = new THREE.Mesh(tieGeo, bagMat);\n                tie1.position.set(0.025, 0.5, 0.015);\n                tie1.rotation.z = -0.4;\n                tie1.rotation.x = 0.2;\n                tie1.castShadow = true;\n                group.add(tie1);\n\n                const tie2 = new THREE.Mesh(tieGeo, bagMat);\n                tie2.position.set(-0.025, 0.5, -0.015);\n                tie2.rotation.z = 0.4;\n                tie2.rotation.x = -0.2;\n                tie2.castShadow = true;\n                group.add(tie2);\n\n                return group;\n            }\n\n            function createPhoneBooth() {\n                const group = new THREE.Group();\n\n                const redMat = new THREE.MeshLambertMaterial({ color: 0xd32f2f }); // classic red\n                const darkMat = new THREE.MeshLambertMaterial({ color: 0x222222 }); // base/phone\n                const glassMat = new THREE.MeshLambertMaterial({ color: 0x88ccff, transparent: true, opacity: 0.5 }); // glass windows\n                const shelfMat = new THREE.MeshLambertMaterial({ color: 0xcccccc }); // silver/grey shelf\n\n                // 1. Base Plate\n                const baseGeo = new THREE.BoxGeometry(0.7, 0.05, 0.7);\n                const base = new THREE.Mesh(baseGeo, darkMat);\n                base.position.y = 0.025;\n                base.castShadow = true;\n                base.receiveShadow = true;\n                group.add(base);\n\n                // 2. Corner Pillars (4 pillars)\n                const pillarGeo = new THREE.BoxGeometry(0.06, 1.6, 0.06);\n                const px = 0.32, pz = 0.32;\n                const positions = [\n                    [-px, pz], [px, pz],\n                    [-px, -pz], [px, -pz]\n                ];\n                positions.forEach(([x, z]) => {\n                    const pillar = new THREE.Mesh(pillarGeo, redMat);\n                    pillar.position.set(x, 0.05 + 0.8, z);\n                    pillar.castShadow = true;\n                    pillar.receiveShadow = true;\n                    group.add(pillar);\n                });\n\n                // 3. Walls (Back, Left, Right are solid/glass. Front is wide open)\n                // Back wall (z = -0.32)\n                const backLowerGeo = new THREE.BoxGeometry(0.58, 0.6, 0.04);\n                const backLower = new THREE.Mesh(backLowerGeo, redMat);\n                backLower.position.set(0, 0.05 + 0.3, -0.31);\n                backLower.castShadow = true;\n                group.add(backLower);\n\n                const backGlassGeo = new THREE.BoxGeometry(0.58, 0.9, 0.02);\n                const backGlass = new THREE.Mesh(backGlassGeo, glassMat);\n                backGlass.position.set(0, 0.05 + 0.6 + 0.45, -0.31);\n                group.add(backGlass);\n\n                // Left wall (x = -0.32)\n                const leftLowerGeo = new THREE.BoxGeometry(0.04, 0.6, 0.58);\n                const leftLower = new THREE.Mesh(leftLowerGeo, redMat);\n                leftLower.position.set(-0.31, 0.05 + 0.3, 0);\n                leftLower.castShadow = true;\n                group.add(leftLower);\n\n                const leftGlassGeo = new THREE.BoxGeometry(0.02, 0.9, 0.58);\n                const leftGlass = new THREE.Mesh(leftGlassGeo, glassMat);\n                leftGlass.position.set(-0.31, 0.05 + 0.6 + 0.45, 0);\n                group.add(leftGlass);\n\n                // Right wall (x = 0.32)\n                const rightLowerGeo = new THREE.BoxGeometry(0.04, 0.6, 0.58);\n                const rightLower = new THREE.Mesh(rightLowerGeo, redMat);\n                rightLower.position.set(0.31, 0.05 + 0.3, 0);\n                rightLower.castShadow = true;\n                group.add(rightLower);\n\n                const rightGlassGeo = new THREE.BoxGeometry(0.02, 0.9, 0.58);\n                const rightGlass = new THREE.Mesh(rightGlassGeo, glassMat);\n                rightGlass.position.set(0.31, 0.05 + 0.6 + 0.45, 0);\n                group.add(rightGlass);\n\n                // Front wall has a tiny header transom at the top but is otherwise empty (open)\n                const frontHeaderGeo = new THREE.BoxGeometry(0.58, 0.15, 0.04);\n                const frontHeader = new THREE.Mesh(frontHeaderGeo, redMat);\n                frontHeader.position.set(0, 0.05 + 1.6 - 0.075, 0.31);\n                frontHeader.castShadow = true;\n                group.add(frontHeader);\n\n                // Top Sign plates (TELEPHONE lettering banner area on all sides)\n                const signMat = new THREE.MeshLambertMaterial({ color: 0xffffff });\n                const signBackGeo = new THREE.BoxGeometry(0.58, 0.1, 0.02);\n\n                // Front sign\n                const signFront = new THREE.Mesh(signBackGeo, signMat);\n                signFront.position.set(0, 0.05 + 1.6 - 0.2, 0.32);\n                group.add(signFront);\n                // Left sign\n                const signLeftGeo = new THREE.BoxGeometry(0.02, 0.1, 0.58);\n                const signLeft = new THREE.Mesh(signLeftGeo, signMat);\n                signLeft.position.set(-0.32, 0.05 + 1.6 - 0.2, 0);\n                group.add(signLeft);\n                // Right sign\n                const signRight = new THREE.Mesh(signLeftGeo.clone(), signMat);\n                signRight.position.set(0.32, 0.05 + 1.6 - 0.2, 0);\n                group.add(signRight);\n\n                // 4. Dome/Roof\n                const roofGeo = new THREE.BoxGeometry(0.74, 0.15, 0.74);\n                const roof = new THREE.Mesh(roofGeo, redMat);\n                roof.position.y = 0.05 + 1.6 + 0.075;\n                roof.castShadow = true;\n                group.add(roof);\n\n                const domeGeo = new THREE.BoxGeometry(0.64, 0.1, 0.64);\n                const dome = new THREE.Mesh(domeGeo, redMat);\n                dome.position.y = 0.05 + 1.6 + 0.15 + 0.05;\n                dome.castShadow = true;\n                group.add(dome);\n\n                // 5. Interior Shelf & Phone\n                const shelfGeo = new THREE.BoxGeometry(0.45, 0.04, 0.25);\n                const shelf = new THREE.Mesh(shelfGeo, shelfMat);\n                shelf.position.set(0, 0.8, -0.16);\n                shelf.castShadow = true;\n                group.add(shelf);\n\n                // Telephone box\n                const phoneBodyGeo = new THREE.BoxGeometry(0.12, 0.2, 0.08);\n                const phoneBody = new THREE.Mesh(phoneBodyGeo, darkMat);\n                phoneBody.position.set(0, 0.95, -0.22);\n                phoneBody.castShadow = true;\n                group.add(phoneBody);\n\n                // Telephone handset (vertical classic shape)\n                const phoneHandsetGeo = new THREE.BoxGeometry(0.04, 0.18, 0.04);\n                const phoneHandset = new THREE.Mesh(phoneHandsetGeo, redMat);\n                phoneHandset.position.set(-0.08, 0.95, -0.20);\n                group.add(phoneHandset);\n\n                return group;\n            }\n\n            function createStreetVendorStall() {\n                const group = new THREE.Group();\n\n                const woodMat = new THREE.MeshLambertMaterial({ color: 0x8d6e63 }); // brown wood\n                const cartMat = new THREE.MeshLambertMaterial({ color: 0x00acc1 }); // bright teal cart\n                const wheelMat = new THREE.MeshLambertMaterial({ color: 0x333333 }); // dark grey wheels\n                const supportMat = new THREE.MeshLambertMaterial({ color: 0xdddddd }); // light supports\n                const redMat = new THREE.MeshLambertMaterial({ color: 0xe53935 }); // canopy red\n                const whiteMat = new THREE.MeshLambertMaterial({ color: 0xffffff }); // canopy white\n                const fruitRedMat = new THREE.MeshLambertMaterial({ color: 0xff3333 }); // apples\n                const fruitGreenMat = new THREE.MeshLambertMaterial({ color: 0x4caf50 }); // greens\n\n                // 1. Cart Main Body\n                const bodyGeo = new THREE.BoxGeometry(1.2, 0.55, 0.8);\n                const body = new THREE.Mesh(bodyGeo, cartMat);\n                body.position.y = 0.4;\n                body.castShadow = true;\n                body.receiveShadow = true;\n                group.add(body);\n\n                // 2. Wheels\n                const wheelGeo = new THREE.CylinderGeometry(0.15, 0.15, 0.08, 12);\n                const wheelRot = Math.PI / 2;\n                \n                const wl1 = new THREE.Mesh(wheelGeo, wheelMat);\n                wl1.rotation.x = wheelRot;\n                wl1.position.set(-0.4, 0.15, 0.42);\n                wl1.castShadow = true;\n                group.add(wl1);\n\n                const wl2 = new THREE.Mesh(wheelGeo, wheelMat);\n                wl2.rotation.x = wheelRot;\n                wl2.position.set(0.4, 0.15, 0.42);\n                wl2.castShadow = true;\n                group.add(wl2);\n\n                const wr1 = new THREE.Mesh(wheelGeo, wheelMat);\n                wr1.rotation.x = wheelRot;\n                wr1.position.set(-0.4, 0.15, -0.42);\n                wr1.castShadow = true;\n                group.add(wr1);\n\n                const wr2 = new THREE.Mesh(wheelGeo, wheelMat);\n                wr2.rotation.x = wheelRot;\n                wr2.position.set(0.4, 0.15, -0.42);\n                wr2.castShadow = true;\n                group.add(wr2);\n\n                // Handle bar\n                const handleGeo = new THREE.BoxGeometry(0.06, 0.06, 0.8);\n                const handle = new THREE.Mesh(handleGeo, woodMat);\n                handle.position.set(0.68, 0.55, 0);\n                handle.castShadow = true;\n                group.add(handle);\n\n                const handleStrGeo = new THREE.BoxGeometry(0.15, 0.04, 0.04);\n                const handleStr1 = new THREE.Mesh(handleStrGeo, woodMat);\n                handleStr1.position.set(0.6, 0.55, 0.3);\n                group.add(handleStr1);\n                const handleStr2 = new THREE.Mesh(handleStrGeo, woodMat);\n                handleStr2.position.set(0.6, 0.55, -0.3);\n                group.add(handleStr2);\n\n                // 3. Canopy Supports (4 pillars)\n                const pillarGeo = new THREE.BoxGeometry(0.04, 1.1, 0.04);\n                const offsets = [\n                    [-0.55, 0.35], [-0.55, -0.35],\n                    [0.55, 0.35], [0.55, -0.35]\n                ];\n                offsets.forEach(([px, pz]) => {\n                    const pillar = new THREE.Mesh(pillarGeo, supportMat);\n                    pillar.position.set(px, 1.0, pz);\n                    pillar.castShadow = true;\n                    group.add(pillar);\n                });\n\n                // 4. Canopy (Overhead Striped Roof)\n                const stripeWidth = 1.4;\n                const stripeDepth = 1.1 / 7;\n                const stripeGeo = new THREE.BoxGeometry(stripeWidth, 0.04, stripeDepth);\n                const canopyGroup = new THREE.Group();\n                canopyGroup.position.set(0, 1.5, 0);\n                canopyGroup.rotation.x = 0.12;\n\n                for (let i = 0; i < 7; i++) {\n                    const stripeMat = (i % 2 === 0) ? redMat : whiteMat;\n                    const stripe = new THREE.Mesh(stripeGeo, stripeMat);\n                    stripe.position.set(0, 0, -0.5 + i * stripeDepth + stripeDepth / 2);\n                    stripe.castShadow = true;\n                    canopyGroup.add(stripe);\n                }\n                group.add(canopyGroup);\n\n                // 5. Produce Boxes / Countertop Displays\n                const crateGeo = new THREE.BoxGeometry(0.35, 0.08, 0.45);\n                const crate1 = new THREE.Mesh(crateGeo, woodMat);\n                crate1.position.set(-0.32, 0.72, 0.1);\n                crate1.rotation.y = 0.1;\n                crate1.castShadow = true;\n                group.add(crate1);\n\n                const appleGeo = new THREE.BoxGeometry(0.08, 0.08, 0.08);\n                for (let xOff = -0.1; xOff <= 0.1; xOff += 0.1) {\n                    for (let zOff = -0.15; zOff <= 0.15; zOff += 0.1) {\n                        const apple = new THREE.Mesh(appleGeo, fruitRedMat);\n                        apple.position.set(-0.32 + xOff, 0.76, 0.1 + zOff);\n                        group.add(apple);\n                    }\n                }\n\n                const crate2 = new THREE.Mesh(crateGeo, woodMat);\n                crate2.position.set(0.25, 0.72, -0.1);\n                crate2.rotation.y = -0.15;\n                crate2.castShadow = true;\n                group.add(crate2);\n\n                const lettuceGeo = new THREE.BoxGeometry(0.09, 0.09, 0.09);\n                for (let xOff = -0.1; xOff <= 0.1; xOff += 0.1) {\n                    for (let zOff = -0.15; zOff <= 0.15; zOff += 0.1) {\n                        const lettuce = new THREE.Mesh(lettuceGeo, fruitGreenMat);\n                        lettuce.position.set(0.25 + xOff, 0.76, -0.1 + zOff);\n                        group.add(lettuce);\n                    }\n                }\n\n                const jarGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.16, 8);\n                const jarMat = new THREE.MeshLambertMaterial({ color: 0xffeb3b });\n                const jar = new THREE.Mesh(jarGeo, jarMat);\n                jar.position.set(-0.25, 0.76, -0.25);\n                jar.castShadow = true;\n                group.add(jar);\n\n                return group;\n            }\n\n            function createLampPost() {\n                const group = new THREE.Group();\n\n                const poleMat = new THREE.MeshLambertMaterial({ color: 0x3a3a3a });\n\n                const poleBaseGeo = new THREE.CylinderGeometry(0.08, 0.12, 0.2, 8);\n                const poleBase = new THREE.Mesh(poleBaseGeo, poleMat);\n                poleBase.position.y = 0.1;\n                poleBase.castShadow = true;\n                group.add(poleBase);\n\n                const poleGeo = new THREE.CylinderGeometry(0.05, 0.06, 2.2, 8);\n                const pole = new THREE.Mesh(poleGeo, poleMat);\n                pole.position.y = 0.2 + 1.1;\n                pole.castShadow = true;\n                pole.receiveShadow = true;\n                group.add(pole);\n\n                const armGeo = new THREE.BoxGeometry(0.4, 0.06, 0.06);\n                const armMat = new THREE.MeshLambertMaterial({ color: 0x444444 });\n                const arm = new THREE.Mesh(armGeo, armMat);\n                arm.position.set(0.15, 2.35, 0);\n                arm.castShadow = true;\n                group.add(arm);\n\n                const fixtureGeo = new THREE.BoxGeometry(0.22, 0.08, 0.16);\n                const fixtureMat = new THREE.MeshLambertMaterial({ color: 0xffdd44, emissive: 0x554400 });\n                const fixture = new THREE.Mesh(fixtureGeo, fixtureMat);\n                fixture.position.set(0.3, 2.3, 0);\n                fixture.castShadow = true;\n                group.add(fixture);\n\n                return group;\n            }\n\n            function createBench() {\n                const group = new THREE.Group();\n\n                const woodMat = new THREE.MeshLambertMaterial({ color: 0x8B6914 });\n                const metalMat = new THREE.MeshLambertMaterial({ color: 0x444444 });\n\n                for (let i = 0; i < 3; i++) {\n                    const plankGeo = new THREE.BoxGeometry(1.4, 0.05, 0.12);\n                    const plank = new THREE.Mesh(plankGeo, woodMat);\n                    plank.position.set(0, 0.42, -0.14 + i * 0.14);\n                    plank.castShadow = true;\n                    plank.receiveShadow = true;\n                    group.add(plank);\n                }\n\n                for (let i = 0; i < 2; i++) {\n                    const backGeo = new THREE.BoxGeometry(1.4, 0.05, 0.10);\n                    const back = new THREE.Mesh(backGeo, woodMat);\n                    back.position.set(0, 0.55 + i * 0.14, -0.18);\n                    back.rotation.x = -Math.PI * 0.08;\n                    back.castShadow = true;\n                    group.add(back);\n                }\n\n                const legPositions = [\n                    [-0.55, -0.2], [0.55, -0.2],\n                    [-0.55, 0.12], [0.55, 0.12]\n                ];\n                legPositions.forEach(([lx, lz]) => {\n                    const legGeo = new THREE.BoxGeometry(0.06, 0.42, 0.06);\n                    const leg = new THREE.Mesh(legGeo, metalMat);\n                    leg.position.set(lx, 0.21, lz);\n                    leg.castShadow = true;\n                    group.add(leg);\n                });\n\n                [-0.62, 0.62].forEach(ax => {\n                    const armGeo = new THREE.BoxGeometry(0.06, 0.06, 0.35);\n                    const arm = new THREE.Mesh(armGeo, metalMat);\n                    arm.position.set(ax, 0.52, -0.04);\n                    arm.castShadow = true;\n                    group.add(arm);\n                });\n\n                return group;\n            }\n\n            function createStopSign() {\n                const group = new THREE.Group();\n\n                const poleMat = new THREE.MeshLambertMaterial({ color: 0x666666 });\n\n                const poleGeo = new THREE.CylinderGeometry(0.04, 0.04, 1.8, 8);\n                const pole = new THREE.Mesh(poleGeo, poleMat);\n                pole.position.y = 0.9;\n                pole.castShadow = true;\n                group.add(pole);\n\n                const signShape = new THREE.Shape();\n                const radius = 0.35;\n                for (let i = 0; i < 8; i++) {\n                    const angle = (Math.PI / 8) + (i * Math.PI / 4);\n                    const x = Math.cos(angle) * radius;\n                    const y = Math.sin(angle) * radius;\n                    if (i === 0) signShape.moveTo(x, y);\n                    else signShape.lineTo(x, y);\n                }\n                signShape.closePath();\n\n                const signGeo = new THREE.ExtrudeGeometry(signShape, { depth: 0.04, bevelEnabled: false });\n                const signMat = new THREE.MeshLambertMaterial({ color: 0xcc0000 });\n                const signMesh = new THREE.Mesh(signGeo, signMat);\n                signMesh.rotation.y = Math.PI / 2;\n                signMesh.position.set(0, 1.85, 0);\n                signMesh.castShadow = true;\n                group.add(signMesh);\n\n                const borderShape = new THREE.Shape();\n                const bRadius = 0.39;\n                for (let i = 0; i < 8; i++) {\n                    const angle = (Math.PI / 8) + (i * Math.PI / 4);\n                    const x = Math.cos(angle) * bRadius;\n                    const y = Math.sin(angle) * bRadius;\n                    if (i === 0) borderShape.moveTo(x, y);\n                    else borderShape.lineTo(x, y);\n                }\n                borderShape.closePath();\n\n                const borderGeo = new THREE.ExtrudeGeometry(borderShape, { depth: 0.02, bevelEnabled: false });\n                const borderMat = new THREE.MeshLambertMaterial({ color: 0xffffff });\n                const borderMesh = new THREE.Mesh(borderGeo, borderMat);\n                borderMesh.rotation.y = Math.PI / 2;\n                borderMesh.position.set(-0.01, 1.85, 0);\n                group.add(borderMesh);\n\n                return group;\n            }\n\n            function createDefaultBox(w, h, d, color) {\n                const group = new THREE.Group();\n                const geo = new THREE.BoxGeometry(w, h, d);\n                const mat = new THREE.MeshLambertMaterial({ color: new THREE.Color(color) });\n                const mesh = new THREE.Mesh(geo, mat);\n                mesh.position.y = h / 2;\n                mesh.castShadow = true;\n                mesh.receiveShadow = true;\n                group.add(mesh);\n                return group;\n            }\n\n            function createPersonManA() {\n                const group = new THREE.Group();\n\n                const skinMat = new THREE.MeshLambertMaterial({ color: 0xf5c6a0 });\n                const shirtMat = new THREE.MeshLambertMaterial({ color: 0x3377cc });\n                const pantsMat = new THREE.MeshLambertMaterial({ color: 0x444466 });\n                const shoeMat = new THREE.MeshLambertMaterial({ color: 0x333333 });\n                const hairMat = new THREE.MeshLambertMaterial({ color: 0x4a3020 });\n\n                const leftLeg = new THREE.Group();\n                leftLeg.name = 'leftLeg';\n                const leftLegUpper = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.4, 0.18), pantsMat);\n                leftLegUpper.position.y = 0.2;\n                leftLegUpper.castShadow = true;\n                leftLeg.add(leftLegUpper);\n                const leftShoe = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.1, 0.22), shoeMat);\n                leftShoe.position.y = 0.05;\n                leftShoe.position.z = 0.02;\n                leftShoe.castShadow = true;\n                leftLeg.add(leftShoe);\n                leftLeg.position.set(-0.1, 0, 0);\n                group.add(leftLeg);\n\n                const rightLeg = new THREE.Group();\n                rightLeg.name = 'rightLeg';\n                const rightLegUpper = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.4, 0.18), pantsMat);\n                rightLegUpper.position.y = 0.2;\n                rightLegUpper.castShadow = true;\n                rightLeg.add(rightLegUpper);\n                const rightShoe = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.1, 0.22), shoeMat);\n                rightShoe.position.y = 0.05;\n                rightShoe.position.z = 0.02;\n                rightShoe.castShadow = true;\n                rightLeg.add(rightShoe);\n                rightLeg.position.set(0.1, 0, 0);\n                group.add(rightLeg);\n\n                const torso = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.5, 0.22), shirtMat);\n                torso.position.y = 0.65;\n                torso.castShadow = true;\n                torso.receiveShadow = true;\n                group.add(torso);\n\n                const leftArm = new THREE.Group();\n                leftArm.name = 'leftArm';\n                const leftArmMesh = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.5, 0.14), shirtMat);\n                leftArmMesh.position.y = -0.2;\n                leftArmMesh.castShadow = true;\n                leftArm.add(leftArmMesh);\n                const leftHand = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.12, 0.12), skinMat);\n                leftHand.position.y = -0.48;\n                leftArm.add(leftHand);\n                leftArm.position.set(-0.27, 0.85, 0);\n                group.add(leftArm);\n\n                const rightArm = new THREE.Group();\n                rightArm.name = 'rightArm';\n                const rightArmMesh = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.5, 0.14), shirtMat);\n                rightArmMesh.position.y = -0.2;\n                rightArmMesh.castShadow = true;\n                rightArm.add(rightArmMesh);\n                const rightHand = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.12, 0.12), skinMat);\n                rightHand.position.y = -0.48;\n                rightArm.add(rightHand);\n                rightArm.position.set(0.27, 0.85, 0);\n                group.add(rightArm);\n\n                const head = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.3), skinMat);\n                head.position.y = 1.08;\n                head.castShadow = true;\n                group.add(head);\n\n                const hairTop = new THREE.Mesh(new THREE.BoxGeometry(0.32, 0.1, 0.32), hairMat);\n                hairTop.position.y = 1.28;\n                hairTop.castShadow = true;\n                group.add(hairTop);\n                const hairBack = new THREE.Mesh(new THREE.BoxGeometry(0.32, 0.18, 0.08), hairMat);\n                hairBack.position.set(0, 1.15, -0.14);\n                group.add(hairBack);\n\n                return group;\n            }\n\n            function createFlower() {\n                const group = new THREE.Group();\n\n                const stemMat = new THREE.MeshLambertMaterial({ color: 0x2e7d32 });\n                const petalMat = new THREE.MeshLambertMaterial({ color: 0xff4081, side: THREE.DoubleSide });\n                const centerMat = new THREE.MeshLambertMaterial({ color: 0xffeb3b });\n\n                const stemGeo = new THREE.CylinderGeometry(0.03, 0.05, 0.5, 6);\n                const stem = new THREE.Mesh(stemGeo, stemMat);\n                stem.position.y = 0.25;\n                stem.rotation.z = 0.12;\n                stem.castShadow = true;\n                group.add(stem);\n\n                const headGroup = new THREE.Group();\n                headGroup.position.set(0.06, 0.52, 0);\n                headGroup.rotation.z = -Math.PI * 0.18;\n\n                const numPetals = 6;\n                for (let i = 0; i < numPetals; i++) {\n                    const angle = (i / numPetals) * Math.PI * 2;\n                    const petalGeo = new THREE.BoxGeometry(0.09, 0.18, 0.03);\n                    const petal = new THREE.Mesh(petalGeo, petalMat);\n                    petal.position.set(\n                        Math.cos(angle) * 0.15,\n                        Math.sin(angle) * 0.15,\n                        0\n                    );\n                    petal.rotation.z = angle;\n                    petal.castShadow = true;\n                    headGroup.add(petal);\n                }\n\n                const centerGeo = new THREE.SphereGeometry(0.09, 8, 6);\n                const center = new THREE.Mesh(centerGeo, centerMat);\n                center.position.z = 0.01;\n                center.castShadow = true;\n                headGroup.add(center);\n\n                group.add(headGroup);\n\n                return group;\n            }\n\n            function createLargeTree() {\n                const group = new THREE.Group();\n                const trunkMat = new THREE.MeshLambertMaterial({ color: 0x5c3a1e });\n                const leafMat = new THREE.MeshLambertMaterial({ color: 0x2d8a2d });\n                const leafDarkMat = new THREE.MeshLambertMaterial({ color: 0x1f6b1f });\n\n                const rootGeo = new THREE.BoxGeometry(0.6, 0.3, 0.6);\n                const root = new THREE.Mesh(rootGeo, trunkMat);\n                root.position.y = 0.15;\n                root.castShadow = true;\n                group.add(root);\n\n                const trunkGeo = new THREE.BoxGeometry(0.45, 1.4, 0.45);\n                const trunk = new THREE.Mesh(trunkGeo, trunkMat);\n                trunk.position.y = 0.7;\n                trunk.castShadow = true;\n                trunk.receiveShadow = true;\n                group.add(trunk);\n\n                const c1 = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.7, 1.8), leafMat);\n                c1.position.y = 1.75;\n                c1.castShadow = true; c1.receiveShadow = true;\n                group.add(c1);\n\n                const c2 = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.6, 1.4), leafDarkMat);\n                c2.position.y = 2.35;\n                c2.castShadow = true;\n                group.add(c2);\n\n                const c3 = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.5, 1.0), leafMat);\n                c3.position.y = 2.8;\n                c3.castShadow = true;\n                group.add(c3);\n\n                const cap = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.3, 0.6), leafDarkMat);\n                cap.position.y = 3.15;\n                cap.castShadow = true;\n                group.add(cap);\n\n                const bumps = [{x:0.8,y:1.9,z:0.3},{x:-0.7,y:2.1,z:-0.5},{x:0.3,y:2.5,z:0.7},{x:-0.4,y:2.6,z:-0.6}];\n                bumps.forEach(pos => {\n                    const b = new THREE.Mesh(new THREE.BoxGeometry(0.5,0.45,0.5), Math.random()>0.5?leafMat:leafDarkMat);\n                    b.position.set(pos.x, pos.y, pos.z);\n                    b.castShadow = true;\n                    group.add(b);\n                });\n\n                return group;\n            }\n\n            function createSmallTree() {\n                const group = new THREE.Group();\n\n                const trunkMat = new THREE.MeshLambertMaterial({ color: 0x6b4226 });\n                const leafMat = new THREE.MeshLambertMaterial({ color: 0x2e8b2e });\n\n                const trunkGeo = new THREE.BoxGeometry(0.25, 0.7, 0.25);\n                const trunk = new THREE.Mesh(trunkGeo, trunkMat);\n                trunk.position.y = 0.35;\n                trunk.castShadow = true;\n                trunk.receiveShadow = true;\n                group.add(trunk);\n\n                const canopy1Geo = new THREE.BoxGeometry(0.9, 0.5, 0.9);\n                const canopy1 = new THREE.Mesh(canopy1Geo, leafMat);\n                canopy1.position.y = 0.95;\n                canopy1.castShadow = true;\n                canopy1.receiveShadow = true;\n                group.add(canopy1);\n\n                const canopy2Geo = new THREE.BoxGeometry(0.6, 0.4, 0.6);\n                const canopy2 = new THREE.Mesh(canopy2Geo, leafMat);\n                canopy2.position.y = 1.4;\n                canopy2.castShadow = true;\n                group.add(canopy2);\n\n                return group;\n            }\n\n            function createCarSedanBlue() {\n                const group = new THREE.Group();\n\n                const bodyMat = new THREE.MeshLambertMaterial({ color: 0x2266cc });\n                const darkMat = new THREE.MeshLambertMaterial({ color: 0x222222 });\n                const glassMat = new THREE.MeshLambertMaterial({ color: 0x88ccff, transparent: true, opacity: 0.6 });\n                const lightFrontMat = new THREE.MeshLambertMaterial({ color: 0xffffaa, emissive: 0x333300 });\n                const lightRearMat = new THREE.MeshLambertMaterial({ color: 0xff3333, emissive: 0x330000 });\n\n                const chassisGeo = new THREE.BoxGeometry(1.8, 0.45, 0.85);\n                const chassis = new THREE.Mesh(chassisGeo, bodyMat);\n                chassis.position.y = 0.35;\n                chassis.castShadow = true;\n                chassis.receiveShadow = true;\n                group.add(chassis);\n\n                const cabinGeo = new THREE.BoxGeometry(1.0, 0.38, 0.78);\n                const cabin = new THREE.Mesh(cabinGeo, bodyMat);\n                cabin.position.set(-0.05, 0.74, 0);\n                cabin.castShadow = true;\n                group.add(cabin);\n\n                const windshieldGeo = new THREE.BoxGeometry(0.05, 0.3, 0.7);\n                const windshield = new THREE.Mesh(windshieldGeo, glassMat);\n                windshield.position.set(0.48, 0.72, 0);\n                windshield.rotation.z = -0.2;\n                group.add(windshield);\n\n                const rearWindowGeo = new THREE.BoxGeometry(0.05, 0.28, 0.65);\n                const rearWindow = new THREE.Mesh(rearWindowGeo, glassMat);\n                rearWindow.position.set(-0.52, 0.72, 0);\n                rearWindow.rotation.z = 0.2;\n                group.add(rearWindow);\n\n                const sideWindowGeo = new THREE.BoxGeometry(0.8, 0.22, 0.04);\n                const leftWindow = new THREE.Mesh(sideWindowGeo, glassMat);\n                leftWindow.position.set(-0.05, 0.78, 0.4);\n                group.add(leftWindow);\n                const rightWindow = new THREE.Mesh(sideWindowGeo.clone(), glassMat);\n                rightWindow.position.set(-0.05, 0.78, -0.4);\n                group.add(rightWindow);\n\n                const wheelGeo = new THREE.CylinderGeometry(0.15, 0.15, 0.12, 12);\n                const wheelPositions = [\n                    { x: 0.55, z: 0.42 }, { x: 0.55, z: -0.42 },\n                    { x: -0.55, z: 0.42 }, { x: -0.55, z: -0.42 }\n                ];\n                wheelPositions.forEach(wp => {\n                    const wheel = new THREE.Mesh(wheelGeo, darkMat);\n                    wheel.position.set(wp.x, 0.15, wp.z);\n                    wheel.rotation.x = Math.PI / 2;\n                    wheel.castShadow = true;\n                    group.add(wheel);\n                });\n\n                const bumperGeo = new THREE.BoxGeometry(0.08, 0.18, 0.8);\n                const frontBumper = new THREE.Mesh(bumperGeo, darkMat);\n                frontBumper.position.set(0.92, 0.22, 0);\n                group.add(frontBumper);\n\n                const rearBumper = new THREE.Mesh(bumperGeo.clone(), darkMat);\n                rearBumper.position.set(-0.92, 0.22, 0);\n                group.add(rearBumper);\n\n                const headlightGeo = new THREE.BoxGeometry(0.06, 0.1, 0.18);\n                const hlLeft = new THREE.Mesh(headlightGeo, lightFrontMat);\n                hlLeft.position.set(0.91, 0.38, 0.28);\n                group.add(hlLeft);\n                const hlRight = new THREE.Mesh(headlightGeo.clone(), lightFrontMat);\n                hlRight.position.set(0.91, 0.38, -0.28);\n                group.add(hlRight);\n\n                const taillightGeo = new THREE.BoxGeometry(0.06, 0.1, 0.16);\n                const tlLeft = new THREE.Mesh(taillightGeo, lightRearMat);\n                tlLeft.position.set(-0.91, 0.38, 0.28);\n                group.add(tlLeft);\n                const tlRight = new THREE.Mesh(taillightGeo.clone(), lightRearMat);\n                tlRight.position.set(-0.91, 0.38, -0.28);\n                group.add(tlRight);\n\n                return group;\n            }\n\n            function createCarCompactRed() {\n                const group = new THREE.Group();\n\n                const bodyMat = new THREE.MeshLambertMaterial({ color: 0xd32f2f }); // bright red body\n                const darkMat = new THREE.MeshLambertMaterial({ color: 0x222222 }); // wheels, trim\n                const glassMat = new THREE.MeshLambertMaterial({ color: 0x88ccff, transparent: true, opacity: 0.6 }); // windows\n                const lightFrontMat = new THREE.MeshLambertMaterial({ color: 0xffffaa, emissive: 0x333300 }); // headlights\n                const lightRearMat = new THREE.MeshLambertMaterial({ color: 0xff3333, emissive: 0x330000 }); // taillights\n\n                // 1. Lower Chassis (shorter than sedan, rounder friendly proportions)\n                const chassisGeo = new THREE.BoxGeometry(1.55, 0.48, 0.85);\n                const chassis = new THREE.Mesh(chassisGeo, bodyMat);\n                chassis.position.y = 0.35;\n                chassis.castShadow = true;\n                chassis.receiveShadow = true;\n                group.add(chassis);\n\n                // 2. Cabin (Hatchback style: cabin extends further back)\n                const cabinGeo = new THREE.BoxGeometry(0.95, 0.42, 0.78);\n                const cabin = new THREE.Mesh(cabinGeo, bodyMat);\n                cabin.position.set(-0.1, 0.78, 0); // shifted backwards\n                cabin.castShadow = true;\n                group.add(cabin);\n\n                // 3. Front Windshield (upright)\n                const windshieldGeo = new THREE.BoxGeometry(0.05, 0.35, 0.7);\n                const windshield = new THREE.Mesh(windshieldGeo, glassMat);\n                windshield.position.set(0.38, 0.78, 0);\n                windshield.rotation.z = 0; // kept upright\n                group.add(windshield);\n\n                // 4. Upright Rear Hatch Window\n                const rearHatchGeo = new THREE.BoxGeometry(0.05, 0.35, 0.68);\n                const rearHatch = new THREE.Mesh(rearHatchGeo, glassMat);\n                rearHatch.position.set(-0.58, 0.78, 0);\n                rearHatch.rotation.z = 0; // kept upright\n                group.add(rearHatch);\n\n                // 5. Side Windows\n                const sideWindowFrontGeo = new THREE.BoxGeometry(0.45, 0.24, 0.04);\n                const sideWindowRearGeo = new THREE.BoxGeometry(0.35, 0.24, 0.04);\n\n                // Left side windows\n                const leftWinFront = new THREE.Mesh(sideWindowFrontGeo, glassMat);\n                leftWinFront.position.set(0.12, 0.8, 0.4);\n                group.add(leftWinFront);\n\n                const leftWinRear = new THREE.Mesh(sideWindowRearGeo, glassMat);\n                leftWinRear.position.set(-0.28, 0.8, 0.4);\n                group.add(leftWinRear);\n\n                // Right side windows\n                const rightWinFront = new THREE.Mesh(sideWindowFrontGeo.clone(), glassMat);\n                rightWinFront.position.set(0.12, 0.8, -0.4);\n                group.add(rightWinFront);\n\n                const rightWinRear = new THREE.Mesh(sideWindowRearGeo.clone(), glassMat);\n                rightWinRear.position.set(-0.28, 0.8, -0.4);\n                group.add(rightWinRear);\n\n                // 6. Cute chunky wheels\n                const wheelGeo = new THREE.CylinderGeometry(0.17, 0.17, 0.14, 12);\n                const wheelPositions = [\n                    { x: 0.46, z: 0.42 }, { x: 0.46, z: -0.42 },\n                    { x: -0.46, z: 0.42 }, { x: -0.46, z: -0.42 }\n                ];\n                wheelPositions.forEach(wp => {\n                    const wheel = new THREE.Mesh(wheelGeo, darkMat);\n                    wheel.position.set(wp.x, 0.17, wp.z);\n                    wheel.rotation.x = Math.PI / 2;\n                    wheel.castShadow = true;\n                    group.add(wheel);\n                });\n\n                // 7. Dark Bumpers\n                const bumperGeo = new THREE.BoxGeometry(0.08, 0.2, 0.8);\n                const frontBumper = new THREE.Mesh(bumperGeo, darkMat);\n                frontBumper.position.set(0.79, 0.24, 0);\n                group.add(frontBumper);\n\n                const rearBumper = new THREE.Mesh(bumperGeo.clone(), darkMat);\n                rearBumper.position.set(-0.79, 0.24, 0);\n                group.add(rearBumper);\n\n                // 8. Cute Headlights\n                const headlightGeo = new THREE.BoxGeometry(0.08, 0.14, 0.18);\n                const hlLeft = new THREE.Mesh(headlightGeo, lightFrontMat);\n                hlLeft.position.set(0.78, 0.38, 0.26);\n                group.add(hlLeft);\n                const hlRight = new THREE.Mesh(headlightGeo.clone(), lightFrontMat);\n                hlRight.position.set(0.78, 0.38, -0.26);\n                group.add(hlRight);\n\n                // 9. Taillights\n                const taillightGeo = new THREE.BoxGeometry(0.06, 0.2, 0.1);\n                const tlLeft = new THREE.Mesh(taillightGeo, lightRearMat);\n                tlLeft.position.set(-0.78, 0.45, 0.32);\n                group.add(tlLeft);\n                const tlRight = new THREE.Mesh(taillightGeo.clone(), lightRearMat);\n                tlRight.position.set(-0.78, 0.45, -0.32);\n                group.add(tlRight);\n\n                // 10. Tiny Black Spoiler on Rear Hatch\n                const spoilerGeo = new THREE.BoxGeometry(0.12, 0.04, 0.72);\n                const spoiler = new THREE.Mesh(spoilerGeo, darkMat);\n                spoiler.position.set(-0.54, 0.94, 0);\n                group.add(spoiler);\n\n                return group;\n            }\n\n            function createCarTaxiYellow() {\n                const group = new THREE.Group();\n\n                const bodyMat = new THREE.MeshLambertMaterial({ color: 0xffcc00 }); // bright yellow body\n                const darkMat = new THREE.MeshLambertMaterial({ color: 0x222222 }); // wheels, bumpers, trim\n                const glassMat = new THREE.MeshLambertMaterial({ color: 0x88ccff, transparent: true, opacity: 0.6 }); // windows\n                const lightFrontMat = new THREE.MeshLambertMaterial({ color: 0xffffaa, emissive: 0x333300 }); // headlights\n                const lightRearMat = new THREE.MeshLambertMaterial({ color: 0xff3333, emissive: 0x330000 }); // taillights\n                const whiteMat = new THREE.MeshLambertMaterial({ color: 0xffffff }); // taxi sign background\n\n                // 1. Lower Chassis (compact city-car shape, similar to compact red)\n                const chassisGeo = new THREE.BoxGeometry(1.55, 0.48, 0.85);\n                const chassis = new THREE.Mesh(chassisGeo, bodyMat);\n                chassis.position.y = 0.35;\n                chassis.castShadow = true;\n                chassis.receiveShadow = true;\n                group.add(chassis);\n\n                // 2. Cabin\n                const cabinGeo = new THREE.BoxGeometry(0.95, 0.42, 0.78);\n                const cabin = new THREE.Mesh(cabinGeo, bodyMat);\n                cabin.position.set(-0.1, 0.78, 0);\n                cabin.castShadow = true;\n                group.add(cabin);\n\n                // 3. Front Windshield (upright)\n                const windshieldGeo = new THREE.BoxGeometry(0.05, 0.35, 0.7);\n                const windshield = new THREE.Mesh(windshieldGeo, glassMat);\n                windshield.position.set(0.38, 0.78, 0);\n                windshield.rotation.z = 0; // kept upright\n                group.add(windshield);\n\n                // 4. Upright Rear Window\n                const rearHatchGeo = new THREE.BoxGeometry(0.05, 0.35, 0.68);\n                const rearHatch = new THREE.Mesh(rearHatchGeo, glassMat);\n                rearHatch.position.set(-0.58, 0.78, 0);\n                rearHatch.rotation.z = 0; // kept upright\n                group.add(rearHatch);\n\n                // 5. Side Windows\n                const sideWindowFrontGeo = new THREE.BoxGeometry(0.45, 0.24, 0.04);\n                const sideWindowRearGeo = new THREE.BoxGeometry(0.35, 0.24, 0.04);\n\n                // Left side windows\n                const leftWinFront = new THREE.Mesh(sideWindowFrontGeo, glassMat);\n                leftWinFront.position.set(0.12, 0.8, 0.4);\n                group.add(leftWinFront);\n\n                const leftWinRear = new THREE.Mesh(sideWindowRearGeo, glassMat);\n                leftWinRear.position.set(-0.28, 0.8, 0.4);\n                group.add(leftWinRear);\n\n                // Right side windows\n                const rightWinFront = new THREE.Mesh(sideWindowFrontGeo.clone(), glassMat);\n                rightWinFront.position.set(0.12, 0.8, -0.4);\n                group.add(rightWinFront);\n\n                const rightWinRear = new THREE.Mesh(sideWindowRearGeo.clone(), glassMat);\n                rightWinRear.position.set(-0.28, 0.8, -0.4);\n                group.add(rightWinRear);\n\n                // 6. Wheels\n                const wheelGeo = new THREE.CylinderGeometry(0.17, 0.17, 0.14, 12);\n                const wheelPositions = [\n                    { x: 0.46, z: 0.42 }, { x: 0.46, z: -0.42 },\n                    { x: -0.46, z: 0.42 }, { x: -0.46, z: -0.42 }\n                ];\n                wheelPositions.forEach(wp => {\n                    const wheel = new THREE.Mesh(wheelGeo, darkMat);\n                    wheel.position.set(wp.x, 0.17, wp.z);\n                    wheel.rotation.x = Math.PI / 2;\n                    wheel.castShadow = true;\n                    group.add(wheel);\n                });\n\n                // 7. Bumpers\n                const bumperGeo = new THREE.BoxGeometry(0.08, 0.2, 0.8);\n                const frontBumper = new THREE.Mesh(bumperGeo, darkMat);\n                frontBumper.position.set(0.79, 0.24, 0);\n                group.add(frontBumper);\n\n                const rearBumper = new THREE.Mesh(bumperGeo.clone(), darkMat);\n                rearBumper.position.set(-0.79, 0.24, 0);\n                group.add(rearBumper);\n\n                // 8. Headlights\n                const headlightGeo = new THREE.BoxGeometry(0.08, 0.14, 0.18);\n                const hlLeft = new THREE.Mesh(headlightGeo, lightFrontMat);\n                hlLeft.position.set(0.78, 0.38, 0.26);\n                group.add(hlLeft);\n                const hlRight = new THREE.Mesh(headlightGeo.clone(), lightFrontMat);\n                hlRight.position.set(0.78, 0.38, -0.26);\n                group.add(hlRight);\n\n                // 9. Taillights\n                const taillightGeo = new THREE.BoxGeometry(0.06, 0.2, 0.1);\n                const tlLeft = new THREE.Mesh(taillightGeo, lightRearMat);\n                tlLeft.position.set(-0.78, 0.45, 0.32);\n                group.add(tlLeft);\n                const tlRight = new THREE.Mesh(taillightGeo.clone(), lightRearMat);\n                tlRight.position.set(-0.78, 0.45, -0.32);\n                group.add(tlRight);\n\n                // 10. Roof TAXI sign\n                // Base/sign mesh\n                const signBaseGeo = new THREE.BoxGeometry(0.18, 0.14, 0.38);\n                const signBase = new THREE.Mesh(signBaseGeo, whiteMat);\n                signBase.position.set(-0.1, 1.06, 0);\n                signBase.castShadow = true;\n                group.add(signBase);\n\n                // Tiny black details on the sign sides to represent TAXI text\n                const textDetailGeo = new THREE.BoxGeometry(0.19, 0.08, 0.28);\n                const textDetail = new THREE.Mesh(textDetailGeo, darkMat);\n                textDetail.position.set(-0.1, 1.06, 0);\n                group.add(textDetail);\n\n                return group;\n            }\n\n            function createTruckDeliveryWhite() {\n                const group = new THREE.Group();\n\n                const whiteMat = new THREE.MeshLambertMaterial({ color: 0xffffff }); // white body\n                const darkMat = new THREE.MeshLambertMaterial({ color: 0x333333 }); // bumpers, wheels, trim\n                const grayMat = new THREE.MeshLambertMaterial({ color: 0x777777 }); // chassis base\n                const glassMat = new THREE.MeshLambertMaterial({ color: 0x88ccff, transparent: true, opacity: 0.6 }); // windshield\n                const lightFrontMat = new THREE.MeshLambertMaterial({ color: 0xffffaa, emissive: 0x333300 }); // headlights\n                const lightRearMat = new THREE.MeshLambertMaterial({ color: 0xff3333, emissive: 0x330000 }); // taillights\n\n                // 1. Lower Chassis (flat gray platform)\n                const chassisGeo = new THREE.BoxGeometry(2.3, 0.2, 0.95);\n                const chassis = new THREE.Mesh(chassisGeo, grayMat);\n                chassis.position.y = 0.25;\n                chassis.castShadow = true;\n                chassis.receiveShadow = true;\n                group.add(chassis);\n\n                // 2. Cab (Front part, white box with sloped windshield)\n                const cabGeo = new THREE.BoxGeometry(0.7, 0.7, 0.9);\n                const cab = new THREE.Mesh(cabGeo, whiteMat);\n                cab.position.set(0.75, 0.7, 0);\n                cab.castShadow = true;\n                group.add(cab);\n\n                // 3. Windshield on the cab\n                const windshieldGeo = new THREE.BoxGeometry(0.04, 0.35, 0.8);\n                const windshield = new THREE.Mesh(windshieldGeo, glassMat);\n                windshield.position.set(1.10, 0.75, 0); // front facing windshield\n                group.add(windshield);\n\n                // Side windows on the cab\n                const sideWindowGeo = new THREE.BoxGeometry(0.3, 0.24, 0.04);\n                const leftWindow = new THREE.Mesh(sideWindowGeo, glassMat);\n                leftWindow.position.set(0.75, 0.75, 0.46);\n                group.add(leftWindow);\n                const rightWindow = new THREE.Mesh(sideWindowGeo.clone(), glassMat);\n                rightWindow.position.set(0.75, 0.75, -0.46);\n                group.add(rightWindow);\n\n                // 4. Large Rectangular Cargo Box (Back part, white)\n                const cargoGeo = new THREE.BoxGeometry(1.45, 1.1, 0.95);\n                const cargo = new THREE.Mesh(cargoGeo, whiteMat);\n                cargo.position.set(-0.35, 0.9, 0);\n                cargo.castShadow = true;\n                cargo.receiveShadow = true;\n                group.add(cargo);\n\n                // Subtle details/accents: Dark line separating cab and cargo area\n                const separatorGeo = new THREE.BoxGeometry(0.04, 1.12, 0.97);\n                const separator = new THREE.Mesh(separatorGeo, darkMat);\n                separator.position.set(0.38, 0.9, 0);\n                group.add(separator);\n\n                // 5. Wheels (6 wheels for a heavy delivery truck look)\n                const wheelGeo = new THREE.CylinderGeometry(0.2, 0.2, 0.14, 12);\n                const wheelPositions = [\n                    { x: 0.75, z: 0.48 }, { x: 0.75, z: -0.48 }, // Front pair\n                    { x: -0.2, z: 0.48 }, { x: -0.2, z: -0.48 }, // Rear pair 1\n                    { x: -0.8, z: 0.48 }, { x: -0.8, z: -0.48 }  // Rear pair 2\n                ];\n                wheelPositions.forEach(wp => {\n                    const wheel = new THREE.Mesh(wheelGeo, darkMat);\n                    wheel.position.set(wp.x, 0.2, wp.z);\n                    wheel.rotation.x = Math.PI / 2;\n                    wheel.castShadow = true;\n                    group.add(wheel);\n                });\n\n                // 6. Bumpers\n                const bumperGeo = new THREE.BoxGeometry(0.08, 0.22, 0.95);\n                const frontBumper = new THREE.Mesh(bumperGeo, darkMat);\n                frontBumper.position.set(1.16, 0.25, 0);\n                group.add(frontBumper);\n\n                const rearBumper = new THREE.Mesh(bumperGeo.clone(), darkMat);\n                rearBumper.position.set(-1.16, 0.25, 0);\n                group.add(rearBumper);\n\n                // 7. Headlights\n                const headlightGeo = new THREE.BoxGeometry(0.06, 0.12, 0.16);\n                const hlLeft = new THREE.Mesh(headlightGeo, lightFrontMat);\n                hlLeft.position.set(1.11, 0.45, 0.32);\n                group.add(hlLeft);\n                const hlRight = new THREE.Mesh(headlightGeo.clone(), lightFrontMat);\n                hlRight.position.set(1.11, 0.45, -0.32);\n                group.add(hlRight);\n\n                // 8. Taillights\n                const taillightGeo = new THREE.BoxGeometry(0.06, 0.12, 0.12);\n                const tlLeft = new THREE.Mesh(taillightGeo, lightRearMat);\n                tlLeft.position.set(-1.15, 0.35, 0.35);\n                group.add(tlLeft);\n                const tlRight = new THREE.Mesh(taillightGeo.clone(), lightRearMat);\n                tlRight.position.set(-1.15, 0.35, -0.35);\n                group.add(tlRight);\n\n                return group;\n            }\n\n            function createBank() {\n                const group = new THREE.Group();\n\n                const wCells = 2;\n                const hCells = 6;\n                const width = wCells * TERRAIN_CELL_SIZE * 0.9; // 10.8 units\n                const depth = hCells * TERRAIN_CELL_SIZE * 0.9; // 32.4 units\n                const mainHeight = 8.5; \n                const centerHeight = 9.8; \n\n                // Materials\n                const wallMat = new THREE.MeshStandardMaterial({ color: 0xebd9c0, roughness: 0.6 }); // Warm tan/beige\n                const darkerWallMat = new THREE.MeshStandardMaterial({ color: 0xd7c4ab, roughness: 0.7 }); // Slightly darker tan for contrast\n                const baseMat = new THREE.MeshStandardMaterial({ color: 0x7c6f5a, roughness: 0.8 }); // Darker stone base\n                const glassMat = new THREE.MeshStandardMaterial({ color: 0x0091ea, emissive: 0x001d3d, roughness: 0.1, metalness: 0.9 }); // Rich Blue-tinted glass\n                const frameMat = new THREE.MeshStandardMaterial({ color: 0x211c1a, roughness: 0.5 }); // Dark frame\n                const columnMat = new THREE.MeshStandardMaterial({ color: 0xf5ebd6, roughness: 0.4 }); // Off-white columns\n                const goldMat = new THREE.MeshStandardMaterial({ color: 0xffd700, metalness: 0.9, roughness: 0.1 }); // Gold letters and $\n                const roofMat = new THREE.MeshLambertMaterial({ color: 0x5a5146 }); // Dark flat roof\n                const grassMat = new THREE.MeshLambertMaterial({ color: 0x4caf50 }); // Landscaping green\n                const flowerMat = new THREE.MeshLambertMaterial({ color: 0xe91e63 }); // Pink flowers\n                const trunkMat = new THREE.MeshLambertMaterial({ color: 0x5d4037 }); // Planter trunk\n\n                // --- 1. Left Wing Body ---\n                const leftWingW = width;\n                const leftWingD = (depth - 9.0) / 2; // ~11.7 units\n                const leftWingGeo = new THREE.BoxGeometry(leftWingW, mainHeight, leftWingD);\n                const leftWing = new THREE.Mesh(leftWingGeo, wallMat);\n                leftWing.position.set(0, mainHeight / 2, -depth / 2 + leftWingD / 2);\n                leftWing.castShadow = true;\n                leftWing.receiveShadow = true;\n                group.add(leftWing);\n\n                // --- 2. Right Wing Body ---\n                const rightWing = new THREE.Mesh(leftWingGeo, wallMat);\n                rightWing.position.set(0, mainHeight / 2, depth / 2 - leftWingD / 2);\n                rightWing.castShadow = true;\n                rightWing.receiveShadow = true;\n                group.add(rightWing);\n\n                // --- 3. Center Tower/Entrance Body (Slightly wider/protruding and taller)\n                const centerW = width + 0.4;\n                const centerD = 9.0;\n                const centerTowerGeo = new THREE.BoxGeometry(centerW, centerHeight, centerD);\n                const centerTower = new THREE.Mesh(centerTowerGeo, darkerWallMat);\n                centerTower.position.set(0, centerHeight / 2, 0);\n                centerTower.castShadow = true;\n                centerTower.receiveShadow = true;\n                group.add(centerTower);\n\n                // --- 4. Darker Ground-Floor Base (wraps around side wings and center)\n                const baseHeight = 1.0;\n                const baseLeft = new THREE.Mesh(new THREE.BoxGeometry(leftWingW + 0.1, baseHeight, leftWingD + 0.1), baseMat);\n                baseLeft.position.set(0, baseHeight / 2, -depth / 2 + leftWingD / 2);\n                baseLeft.castShadow = true;\n                baseLeft.receiveShadow = true;\n                group.add(baseLeft);\n\n                const baseRight = new THREE.Mesh(new THREE.BoxGeometry(leftWingW + 0.1, baseHeight, leftWingD + 0.1), baseMat);\n                baseRight.position.set(0, baseHeight / 2, depth / 2 - leftWingD / 2);\n                baseRight.castShadow = true;\n                baseRight.receiveShadow = true;\n                group.add(baseRight);\n\n                const baseCenter = new THREE.Mesh(new THREE.BoxGeometry(centerW + 0.1, baseHeight, centerD + 0.05), baseMat);\n                baseCenter.position.set(0, baseHeight / 2, 0);\n                baseCenter.castShadow = true;\n                baseCenter.receiveShadow = true;\n                group.add(baseCenter);\n\n                // Helper to build a massive horizontal blue glass panel section with clean black frames & grids\n                function createGiantGlassPanel(w, h, cols, rows) {\n                    const panelGroup = new THREE.Group();\n                    \n                    // Main solid blue glass block behind\n                    const glassGeo = new THREE.BoxGeometry(0.12, h - 0.1, w - 0.1);\n                    const glass = new THREE.Mesh(glassGeo, glassMat);\n                    panelGroup.add(glass);\n\n                    // Outer Frame\n                    const frameThickness = 0.08;\n                    const frameDepth = 0.18;\n                    \n                    // Top/Bottom frames\n                    const hFrameGeo = new THREE.BoxGeometry(frameDepth, frameThickness, w);\n                    const topF = new THREE.Mesh(hFrameGeo, frameMat);\n                    topF.position.y = h / 2 - frameThickness / 2;\n                    panelGroup.add(topF);\n\n                    const botF = new THREE.Mesh(hFrameGeo, frameMat);\n                    botF.position.y = -h / 2 + frameThickness / 2;\n                    panelGroup.add(botF);\n\n                    // Left/Right frames\n                    const vFrameGeo = new THREE.BoxGeometry(frameDepth, h, frameThickness);\n                    const leftF = new THREE.Mesh(vFrameGeo, frameMat);\n                    leftF.position.z = -w / 2 + frameThickness / 2;\n                    panelGroup.add(leftF);\n\n                    const rightF = new THREE.Mesh(vFrameGeo, frameMat);\n                    rightF.position.z = w / 2 - frameThickness / 2;\n                    panelGroup.add(rightF);\n\n                    // Horizontal grids\n                    for (let r = 1; r < rows; r++) {\n                        const gridY = -h / 2 + (r / rows) * h;\n                        const g = new THREE.Mesh(new THREE.BoxGeometry(frameDepth - 0.04, 0.04, w - 0.1), frameMat);\n                        g.position.y = gridY;\n                        panelGroup.add(g);\n                    }\n\n                    // Vertical grids\n                    for (let c = 1; c < cols; c++) {\n                        const gridZ = -w / 2 + (c / cols) * w;\n                        const g = new THREE.Mesh(new THREE.BoxGeometry(frameDepth - 0.04, h - 0.1, 0.04), frameMat);\n                        g.position.z = gridZ;\n                        panelGroup.add(g);\n                    }\n\n                    return panelGroup;\n                }\n\n                // --- 5. Add Massive Blue Glass Horizontal Bands (Side Wings Front & Back) ---\n                const wingWindowW = 4.8; // Broad horizontal sections\n                const wingWindowH = 2.8; // Tall window panels\n                const sides = [-width / 2 - 0.04, width / 2 + 0.04];\n\n                sides.forEach(xPos => {\n                    // For both left and right wings\n                    [-1, 1].forEach(sideMult => {\n                        const wingCenterZ = sideMult * (depth / 2 - leftWingD / 2);\n                        // Two vertical floors of massive horizontal bands\n                        for (let floor = 0; floor < 2; floor++) {\n                            const y = 2.5 + floor * 3.4;\n                            const glassPanel = createGiantGlassPanel(wingWindowW, wingWindowH, 3, 2);\n                            glassPanel.position.set(xPos, y, wingCenterZ);\n                            // If it's the back side, rotate to align properly\n                            if (xPos < 0) {\n                                glassPanel.rotation.y = Math.PI;\n                            }\n                            group.add(glassPanel);\n                        }\n                    });\n                });\n\n                // Short ends window sections (z = +/- depth/2)\n                const endSides = [-depth / 2 - 0.04, depth / 2 + 0.04];\n                endSides.forEach(zPos => {\n                    for (let floor = 0; floor < 2; floor++) {\n                        const y = 2.5 + floor * 3.4;\n                        const glassPanel = createGiantGlassPanel(width - 2.0, wingWindowH, 2, 2);\n                        glassPanel.position.set(0, y, zPos);\n                        glassPanel.rotation.y = Math.PI / 2;\n                        group.add(glassPanel);\n                    }\n                });\n\n                // --- 6. Majestic Front Entrance Portico (Centered along +X wide facade) ---\n                const porticoX = centerW / 2 + 1.6;\n                const porticoW = 7.6; // Much larger portico\n                const porticoH = 6.2; // Taller portico matching the grand scale\n\n                // Grand Steps leading up to Portico\n                const step1Geo = new THREE.BoxGeometry(2.4, 0.25, porticoW + 1.2);\n                const step1 = new THREE.Mesh(step1Geo, baseMat);\n                step1.position.set(porticoX - 0.6, 0.125, 0);\n                step1.castShadow = true;\n                step1.receiveShadow = true;\n                group.add(step1);\n\n                const step2Geo = new THREE.BoxGeometry(2.0, 0.25, porticoW + 0.4);\n                const step2 = new THREE.Mesh(step2Geo, baseMat);\n                step2.position.set(porticoX - 0.8, 0.375, 0);\n                step2.castShadow = true;\n                step2.receiveShadow = true;\n                group.add(step2);\n\n                const step3Geo = new THREE.BoxGeometry(1.6, 0.25, porticoW - 0.4);\n                const step3 = new THREE.Mesh(step3Geo, baseMat);\n                step3.position.set(porticoX - 1.0, 0.625, 0);\n                step3.castShadow = true;\n                step3.receiveShadow = true;\n                group.add(step3);\n\n                // Massive Off-White Pillars/Columns (Taller and thicker)\n                const colGeo = new THREE.CylinderGeometry(0.35, 0.35, 4.4, 12);\n                const colX = porticoX - 0.6;\n                const colOffsetsZ = [-2.8, 2.8];\n                colOffsetsZ.forEach(cz => {\n                    const col = new THREE.Mesh(colGeo, columnMat);\n                    col.position.set(colX, 0.75 + 2.2, cz);\n                    col.castShadow = true;\n                    group.add(col);\n                });\n\n                // Deeply Recessed Double Doors behind columns\n                const doorGroup = new THREE.Group();\n                doorGroup.position.set(centerW / 2 - 0.2, 0.75, 0);\n                \n                // Deep alcove frame\n                const doorFrameGeo = new THREE.BoxGeometry(1.2, 4.0, 4.4);\n                const doorFrame = new THREE.Mesh(doorFrameGeo, darkerWallMat);\n                doorFrame.position.y = 2.0;\n                doorFrame.castShadow = true;\n                doorGroup.add(doorFrame);\n\n                // Deep inset backing\n                const backing = new THREE.Mesh(new THREE.BoxGeometry(0.2, 3.8, 4.0), baseMat);\n                backing.position.set(0.52, 1.9, 0);\n                doorGroup.add(backing);\n\n                // Huge glass entry doors\n                const lDoor = new THREE.Mesh(new THREE.BoxGeometry(0.1, 3.2, 1.4), glassMat);\n                lDoor.position.set(0.55, 1.6, -0.75);\n                const rDoor = new THREE.Mesh(new THREE.BoxGeometry(0.1, 3.2, 1.4), glassMat);\n                rDoor.position.set(0.55, 1.6, 0.75);\n                doorGroup.add(lDoor);\n                doorGroup.add(rDoor);\n\n                // Metal door framing details\n                const dFrame1 = new THREE.Mesh(new THREE.BoxGeometry(0.15, 3.4, 0.1), frameMat);\n                dFrame1.position.set(0.56, 1.7, 0);\n                doorGroup.add(dFrame1);\n\n                const dFrame2 = new THREE.Mesh(new THREE.BoxGeometry(0.15, 3.4, 0.1), frameMat);\n                dFrame2.position.set(0.56, 1.7, -1.45);\n                doorGroup.add(dFrame2);\n\n                const dFrame3 = new THREE.Mesh(new THREE.BoxGeometry(0.15, 3.4, 0.1), frameMat);\n                dFrame3.position.set(0.56, 1.7, 1.45);\n                doorGroup.add(dFrame3);\n\n                group.add(doorGroup);\n\n                // Portico Large Triangular Pediment (Cap)\n                const pedimentGroup = new THREE.Group();\n                pedimentGroup.position.set(porticoX - 0.6, 5.15, 0);\n\n                const pedimentGeo = new THREE.BoxGeometry(2.4, 1.0, porticoW + 0.6);\n                const pedMesh = new THREE.Mesh(pedimentGeo, darkerWallMat);\n                pedMesh.position.y = 0.5;\n                pedMesh.castShadow = true;\n                pedimentGroup.add(pedMesh);\n\n                // Huge, prominent Golden Dollar Sign ($) mounted on the Pediment center\n                const dollarGroup = new THREE.Group();\n                dollarGroup.position.set(1.22, 0.5, 0);\n                \n                const sBarGeo = new THREE.BoxGeometry(0.15, 1.0, 0.15);\n                const sBar = new THREE.Mesh(sBarGeo, goldMat);\n                dollarGroup.add(sBar);\n\n                const sCap1Geo = new THREE.BoxGeometry(0.15, 0.15, 0.6);\n                const sCap1 = new THREE.Mesh(sCap1Geo, goldMat);\n                sCap1.position.y = 0.35;\n                dollarGroup.add(sCap1);\n\n                const sCap2 = new THREE.Mesh(sCap1Geo, goldMat);\n                sCap2.position.y = -0.35;\n                dollarGroup.add(sCap2);\n\n                const sMid = new THREE.Mesh(sCap1Geo, goldMat);\n                sMid.position.y = 0;\n                dollarGroup.add(sMid);\n\n                const sLeft = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.4, 0.15), goldMat);\n                sLeft.position.set(0, 0.18, 0.25);\n                dollarGroup.add(sLeft);\n\n                const sRight = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.4, 0.15), goldMat);\n                sRight.position.set(0, -0.18, -0.25);\n                dollarGroup.add(sRight);\n\n                pedimentGroup.add(dollarGroup);\n                group.add(pedimentGroup);\n\n                // --- 7. Large Glass Transom Window on Center Tower above the Portico ---\n                const towerGlassPanel = createGiantGlassPanel(4.8, 2.0, 3, 1);\n                towerGlassPanel.position.set(centerW / 2 + 0.04, 7.4, 0);\n                group.add(towerGlassPanel);\n\n                // --- 8. Bold 3D \"BANK\" Golden Signage at the very top of Center Tower ---\n                // Removed at user's request.\n\n                // --- 9. Flat Roof and Perimeter Parapets ---\n                // Wing roofs\n                const leftRoofSlab = new THREE.Mesh(new THREE.BoxGeometry(leftWingW, 0.25, leftWingD), roofMat);\n                leftRoofSlab.position.set(0, mainHeight, -depth / 2 + leftWingD / 2);\n                group.add(leftRoofSlab);\n\n                const rightRoofSlab = new THREE.Mesh(new THREE.BoxGeometry(leftWingW, 0.25, leftWingD), roofMat);\n                rightRoofSlab.position.set(0, mainHeight, depth / 2 - leftWingD / 2);\n                group.add(rightRoofSlab);\n\n                // Center roof\n                const centerRoofSlab = new THREE.Mesh(new THREE.BoxGeometry(centerW, 0.25, centerD), roofMat);\n                centerRoofSlab.position.set(0, centerHeight, 0);\n                group.add(centerRoofSlab);\n\n                // Elegant side wings parapets\n                const paraW = 0.25;\n                const paraH = 0.5;\n                const wingParaGeoHoriz = new THREE.BoxGeometry(leftWingW, paraH, paraW);\n                const wingParaGeoVert = new THREE.BoxGeometry(paraW, paraH, leftWingD - paraW * 2);\n\n                // Left wing parapet\n                const lp1 = new THREE.Mesh(wingParaGeoHoriz, wallMat);\n                lp1.position.set(0, mainHeight + paraH / 2, -depth / 2 + paraW / 2);\n                group.add(lp1);\n                const lp2 = new THREE.Mesh(wingParaGeoVert, wallMat);\n                lp2.position.set(leftWingW / 2 - paraW / 2, mainHeight + paraH / 2, -depth / 2 + leftWingD / 2);\n                group.add(lp2);\n                const lp3 = new THREE.Mesh(wingParaGeoVert, wallMat);\n                lp3.position.set(-leftWingW / 2 + paraW / 2, mainHeight + paraH / 2, -depth / 2 + leftWingD / 2);\n                group.add(lp3);\n\n                // Right wing parapet\n                const rp1 = new THREE.Mesh(wingParaGeoHoriz, wallMat);\n                rp1.position.set(0, mainHeight + paraH / 2, depth / 2 - paraW / 2);\n                group.add(rp1);\n                const rp2 = new THREE.Mesh(wingParaGeoVert, wallMat);\n                rp2.position.set(leftWingW / 2 - paraW / 2, mainHeight + paraH / 2, depth / 2 - leftWingD / 2);\n                group.add(rp2);\n                const rp3 = new THREE.Mesh(wingParaGeoVert, wallMat);\n                rp3.position.set(-leftWingW / 2 + paraW / 2, mainHeight + paraH / 2, depth / 2 - leftWingD / 2);\n                group.add(rp3);\n\n                // Center tower parapets\n                const centerParaHoriz = new THREE.BoxGeometry(centerW, paraH, paraW);\n                const centerParaVert = new THREE.BoxGeometry(paraW, paraH, centerD - paraW * 2);\n\n                const cp1 = new THREE.Mesh(centerParaHoriz, darkerWallMat);\n                cp1.position.set(0, centerHeight + paraH / 2, centerD / 2 - paraW / 2);\n                group.add(cp1);\n                const cp2 = new THREE.Mesh(centerParaHoriz, darkerWallMat);\n                cp2.position.set(0, centerHeight + paraH / 2, -centerD / 2 + paraW / 2);\n                group.add(cp2);\n                const cp3 = new THREE.Mesh(centerParaVert, darkerWallMat);\n                cp3.position.set(centerW / 2 - paraW / 2, centerHeight + paraH / 2, 0);\n                group.add(cp3);\n                const cp4 = new THREE.Mesh(centerParaVert, darkerWallMat);\n                cp4.position.set(-centerW / 2 + paraW / 2, centerHeight + paraH / 2, 0);\n                group.add(cp4);\n\n                // --- 10. Upgraded Sidewalk & Luxury Landscaping ---\n                // Full landscape grass strip in front of the side wings on +X side\n                const grassStripGeo = new THREE.BoxGeometry(2.0, 0.18, depth - 1.2);\n                const grassStrip = new THREE.Mesh(grassStripGeo, grassMat);\n                grassStrip.position.set(width / 2 + 1.0, 0.09, 0);\n                grassStrip.receiveShadow = true;\n                group.add(grassStrip);\n\n                // Decorative low-poly bushes, beautiful pink flower arrays and miniature decorative columnar trees\n                const bushGeo = new THREE.BoxGeometry(0.8, 0.8, 0.8);\n                const bushMat = new THREE.MeshLambertMaterial({ color: 0x2e7d32 });\n\n                const treeGeo = new THREE.CylinderGeometry(0.1, 0.1, 0.8, 6);\n                const foliageGeo = new THREE.BoxGeometry(1.0, 1.8, 1.0);\n                const foliageMat = new THREE.MeshLambertMaterial({ color: 0x1b5e20 });\n\n                // Place landscaping carefully on the front grass strips (avoiding the central portico steps)\n                for (let z = -depth / 2 + 1.5; z <= depth / 2 - 1.5; z += 3.2) {\n                    // Skip the area directly blocked by the grand portico steps (roughly z = -4.5 to 4.5)\n                    if (Math.abs(z) < 4.2) continue;\n\n                    // Large Decorative columnar tree\n                    const treeTrunk = new THREE.Mesh(treeGeo, trunkMat);\n                    treeTrunk.position.set(width / 2 + 1.2, 0.49, z);\n                    treeTrunk.castShadow = true;\n                    group.add(treeTrunk);\n\n                    const treeFoliage = new THREE.Mesh(foliageGeo, foliageMat);\n                    treeFoliage.position.set(width / 2 + 1.2, 1.6, z);\n                    treeFoliage.castShadow = true;\n                    group.add(treeFoliage);\n\n                    // Round bush nested beside tree\n                    const bush = new THREE.Mesh(bushGeo, bushMat);\n                    bush.position.set(width / 2 + 0.8, 0.49, z + 1.0);\n                    bush.rotation.y = Math.random() * Math.PI;\n                    bush.scale.setScalar(0.75 + Math.random() * 0.4);\n                    bush.castShadow = true;\n                    group.add(bush);\n\n                    // Beautiful flower blooms\n                    const flower = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.3), flowerMat);\n                    flower.position.set(width / 2 + 0.9, 0.55, z - 0.9);\n                    group.add(flower);\n                }\n\n                return group;\n            }\n\n            function createOfficeMedium() {\n                const group = new THREE.Group();\n\n                const wCells = 2;\n                const hCells = 2;\n                const size = wCells * TERRAIN_CELL_SIZE * 0.9; // 10.8 units\n                const totalHeight = 23;\n                const groundHeight = 4.0;\n                const upperHeight = totalHeight - groundHeight; // 19.0\n\n                // Materials\n                const baseMat = new THREE.MeshStandardMaterial({ color: 0x3a302c, roughness: 0.7, metalness: 0.2 }); // Dark bronze/gray base\n                const upperMat = new THREE.MeshStandardMaterial({ color: 0xdfd5c6, roughness: 0.6, metalness: 0.1 }); // Warm tan/beige wall\n                const roofMat = new THREE.MeshLambertMaterial({ color: 0x2a2522 }); // Flat dark roof\n                const glassMat = new THREE.MeshStandardMaterial({ color: 0x0091ea, emissive: 0x001a33, roughness: 0.1, metalness: 0.9 }); // Tall blue glass\n                const frameMat = new THREE.MeshStandardMaterial({ color: 0x211c1a, roughness: 0.5, metalness: 0.4 }); // Dark frame for vertical bands\n                const foliageMat = new THREE.MeshLambertMaterial({ color: 0x388e3c }); // Planter green\n                const trunkMat = new THREE.MeshLambertMaterial({ color: 0x5d4037 }); // Planter trunk\n                const planterMat = new THREE.MeshStandardMaterial({ color: 0x4e423d, roughness: 0.8 }); // Concrete planter\n\n                // 1. Ground Floor (Base)\n                const groundGeo = new THREE.BoxGeometry(size, groundHeight, size);\n                const groundMesh = new THREE.Mesh(groundGeo, baseMat);\n                groundMesh.position.y = groundHeight / 2;\n                groundMesh.castShadow = true;\n                groundMesh.receiveShadow = true;\n                group.add(groundMesh);\n\n                // 2. Darker Ground-Floor Entrance (Recessed Double Doors on Front z = size/2)\n                const entranceGroup = new THREE.Group();\n                entranceGroup.position.set(0, 0, size / 2 + 0.01);\n\n                // Recessed frame\n                const frameGeo = new THREE.BoxGeometry(2.8, 2.6, 0.4);\n                const frameMesh = new THREE.Mesh(frameGeo, frameMat);\n                frameMesh.position.set(0, 1.3, -0.2);\n                frameMesh.castShadow = true;\n                entranceGroup.add(frameMesh);\n\n                // Recessed doors\n                const doorGeo = new THREE.BoxGeometry(1.1, 2.2, 0.1);\n                const leftDoor = new THREE.Mesh(doorGeo, frameMat);\n                leftDoor.position.set(-0.6, 1.1, -0.3);\n                entranceGroup.add(leftDoor);\n\n                const rightDoor = new THREE.Mesh(doorGeo, frameMat);\n                rightDoor.position.set(0.6, 1.1, -0.3);\n                entranceGroup.add(rightDoor);\n\n                const doorGlassGeo = new THREE.BoxGeometry(0.35, 1.6, 0.05);\n                const doorGlassLeft = new THREE.Mesh(doorGlassGeo, glassMat);\n                doorGlassLeft.position.set(-0.6, 1.2, -0.22);\n                entranceGroup.add(doorGlassLeft);\n\n                const doorGlassRight = new THREE.Mesh(doorGlassGeo, glassMat);\n                doorGlassRight.position.set(0.6, 1.2, -0.22);\n                entranceGroup.add(doorGlassRight);\n\n                group.add(entranceGroup);\n\n                // 3. Planters with tiny low-poly trees at front corners\n                const planterGeo = new THREE.BoxGeometry(1.4, 0.6, 1.4);\n                const foliageGeo = new THREE.BoxGeometry(1.0, 1.0, 1.0);\n                const trunkGeo = new THREE.BoxGeometry(0.15, 0.6, 0.15);\n\n                const planterOffsets = [\n                    { x: -size/2 + 1.0, z: size/2 - 0.2 },\n                    { x: size/2 - 1.0, z: size/2 - 0.2 }\n                ];\n\n                planterOffsets.forEach(offset => {\n                    const planter = new THREE.Mesh(planterGeo, planterMat);\n                    planter.position.set(offset.x, 0.3, offset.z);\n                    planter.castShadow = true;\n                    group.add(planter);\n\n                    const trunk = new THREE.Mesh(trunkGeo, trunkMat);\n                    trunk.position.set(offset.x, 0.6 + 0.3, offset.z);\n                    trunk.castShadow = true;\n                    group.add(trunk);\n\n                    const foliage = new THREE.Mesh(foliageGeo, foliageMat);\n                    foliage.position.set(offset.x, 1.2 + 0.3, offset.z);\n                    foliage.castShadow = true;\n                    group.add(foliage);\n                });\n\n                // 4. Upper Floors (Warm beige body)\n                const upperGeo = new THREE.BoxGeometry(size, upperHeight, size);\n                const upperMesh = new THREE.Mesh(upperGeo, upperMat);\n                upperMesh.position.y = groundHeight + upperHeight / 2;\n                upperMesh.castShadow = true;\n                upperMesh.receiveShadow = true;\n                group.add(upperMesh);\n\n                // 5. Tall, narrow windows in strong vertical bands (6 stories total)\n                const floorHeight = upperHeight / 5; // 3.8 per story for 5 upper stories\n                const sides = [\n                    { dir: new THREE.Vector3(0, 0, 1), rot: 0 },\n                    { dir: new THREE.Vector3(1, 0, 0), rot: Math.PI / 2 },\n                    { dir: new THREE.Vector3(0, 0, -1), rot: Math.PI },\n                    { dir: new THREE.Vector3(-1, 0, 0), rot: -Math.PI / 2 }\n                ];\n\n                sides.forEach(side => {\n                    // Create strong vertical bands (columns of narrow windows)\n                    for (let x = -size / 2 + 1.5; x < size / 2 - 1; x += 1.8) {\n                        // For each vertical column, draw tall narrow windows\n                        for (let f = 0; f < 5; f++) {\n                            const y = groundHeight + f * floorHeight + floorHeight / 2;\n\n                            const winGroup = new THREE.Group();\n                            winGroup.position.set(x, y, size / 2 + 0.04);\n\n                            // Tall narrow window frame\n                            const fGeo = new THREE.BoxGeometry(0.9, 2.4, 0.08);\n                            const frame = new THREE.Mesh(fGeo, frameMat);\n                            winGroup.add(frame);\n\n                            // Glass\n                            const gGeo = new THREE.BoxGeometry(0.7, 2.2, 0.06);\n                            const glass = new THREE.Mesh(gGeo, glassMat);\n                            glass.position.z = 0.02;\n                            winGroup.add(glass);\n\n                            winGroup.rotation.y = side.rot;\n                            winGroup.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), side.rot);\n                            group.add(winGroup);\n                        }\n                    }\n                });\n\n                // 6. Flat Roof and details\n                const roofSlabGeo = new THREE.BoxGeometry(size, 0.2, size);\n                const roofSlab = new THREE.Mesh(roofSlabGeo, roofMat);\n                roofSlab.position.y = totalHeight;\n                group.add(roofSlab);\n\n                // Simple parapet\n                const paraW = 0.18;\n                const paraH = 0.5;\n                const paraGeoHorizontal = new THREE.BoxGeometry(size, paraH, paraW);\n                const paraGeoVertical = new THREE.BoxGeometry(paraW, paraH, size - paraW * 2);\n\n                const p1 = new THREE.Mesh(paraGeoHorizontal, upperMat);\n                p1.position.set(0, totalHeight + paraH / 2, size / 2 - paraW / 2);\n                p1.castShadow = true;\n                group.add(p1);\n\n                const p2 = new THREE.Mesh(paraGeoHorizontal, upperMat);\n                p2.position.set(0, totalHeight + paraH / 2, -size / 2 + paraW / 2);\n                p2.castShadow = true;\n                group.add(p2);\n\n                const p3 = new THREE.Mesh(paraGeoVertical, upperMat);\n                p3.position.set(size / 2 - paraW / 2, totalHeight + paraH / 2, 0);\n                p3.castShadow = true;\n                group.add(p3);\n\n                const p4 = new THREE.Mesh(paraGeoVertical, upperMat);\n                p4.position.set(-size / 2 + paraW / 2, totalHeight + paraH / 2, 0);\n                p4.castShadow = true;\n                group.add(p4);\n\n                return group;\n            }\n\n            function createOfficeSmall() {\n                const group = new THREE.Group();\n\n                const wCells = 2;\n                const hCells = 2;\n                const size = wCells * TERRAIN_CELL_SIZE * 0.9; // 10.8 units\n                const totalHeight = 15;\n                const groundHeight = 3.5;\n                const upperHeight = totalHeight - groundHeight; // 11.5\n\n                // Materials\n                const baseMat = new THREE.MeshStandardMaterial({ color: 0x2e3d44, roughness: 0.7, metalness: 0.2 }); // Dark slate gray base\n                const upperMat = new THREE.MeshStandardMaterial({ color: 0xf5f5f5, roughness: 0.5, metalness: 0.1 }); // Whitish upper walls\n                const roofMat = new THREE.MeshLambertMaterial({ color: 0x37474f }); // Dark roof slab\n                const frameMat = new THREE.MeshStandardMaterial({ color: 0x1c282e, roughness: 0.4, metalness: 0.6 }); // Window frame\n                const glassMat = new THREE.MeshStandardMaterial({ color: 0x00b0ff, emissive: 0x002c4c, roughness: 0.1, metalness: 0.9 }); // Shiny blue glass\n                const metalMat = new THREE.MeshStandardMaterial({ color: 0x78909c, roughness: 0.3, metalness: 0.8 }); // Steel door / canopy\n                const grillMat = new THREE.MeshLambertMaterial({ color: 0x455a64 }); // HVAC grill\n\n                // 1. Ground Floor (Base)\n                const groundGeo = new THREE.BoxGeometry(size, groundHeight, size);\n                const groundMesh = new THREE.Mesh(groundGeo, baseMat);\n                groundMesh.position.y = groundHeight / 2;\n                groundMesh.castShadow = true;\n                groundMesh.receiveShadow = true;\n                group.add(groundMesh);\n\n                // 2. Large Centered Metal Door on the Front Side (z = size/2)\n                const doorGroup = new THREE.Group();\n                doorGroup.position.set(0, 0, size / 2 + 0.02);\n\n                // Frame\n                const doorFrameGeo = new THREE.BoxGeometry(2.4, 2.3, 0.1);\n                const doorFrame = new THREE.Mesh(doorFrameGeo, frameMat);\n                doorFrame.position.y = 1.15;\n                doorGroup.add(doorFrame);\n\n                // Double Doors (Metal)\n                const leftDoorGeo = new THREE.BoxGeometry(0.9, 2.1, 0.08);\n                const leftDoor = new THREE.Mesh(leftDoorGeo, metalMat);\n                leftDoor.position.set(-0.5, 1.1, 0.02);\n                doorGroup.add(leftDoor);\n\n                const rightDoorGeo = new THREE.BoxGeometry(0.9, 2.1, 0.08);\n                const rightDoor = new THREE.Mesh(rightDoorGeo, metalMat);\n                rightDoor.position.set(0.5, 1.1, 0.02);\n                doorGroup.add(rightDoor);\n\n                // Glass panels in doors\n                const doorGlassGeo = new THREE.BoxGeometry(0.4, 1.4, 0.05);\n                const doorGlassLeft = new THREE.Mesh(doorGlassGeo, glassMat);\n                doorGlassLeft.position.set(-0.5, 1.2, 0.07);\n                doorGroup.add(doorGlassLeft);\n\n                const doorGlassRight = new THREE.Mesh(doorGlassGeo, glassMat);\n                doorGlassRight.position.set(0.5, 1.2, 0.07);\n                doorGroup.add(doorGlassRight);\n\n                // Centered metal canopy/awning above the door\n                const awningGeo = new THREE.BoxGeometry(3.2, 0.12, 1.2);\n                const awning = new THREE.Mesh(awningGeo, frameMat);\n                awning.position.set(0, 2.35, -0.5);\n                awning.rotation.x = 0.08; // slightly sloped\n                awning.castShadow = true;\n                doorGroup.add(awning);\n\n                group.add(doorGroup);\n\n                // 3. Upper Floors (Whitish body)\n                const upperGeo = new THREE.BoxGeometry(size, upperHeight, size);\n                const upperMesh = new THREE.Mesh(upperGeo, upperMat);\n                upperMesh.position.y = groundHeight + upperHeight / 2;\n                upperMesh.castShadow = true;\n                upperMesh.receiveShadow = true;\n                group.add(upperMesh);\n\n                // 4. Enhanced Office Window Grid on Floors 2+\n                const sides = [\n                    { dir: new THREE.Vector3(0, 0, 1), rot: 0 },\n                    { dir: new THREE.Vector3(1, 0, 0), rot: Math.PI / 2 },\n                    { dir: new THREE.Vector3(0, 0, -1), rot: Math.PI },\n                    { dir: new THREE.Vector3(-1, 0, 0), rot: -Math.PI / 2 }\n                ];\n\n                const floorHeight = 2.8;\n                const numFloors = 4;\n\n                sides.forEach(side => {\n                    for (let f = 0; f < numFloors; f++) {\n                        const y = groundHeight + f * floorHeight + 1.4;\n\n                        // Decorative window sill/ledge running under the row of windows on this face\n                        const sillGeo = new THREE.BoxGeometry(size - 0.4, 0.12, 0.25);\n                        const sill = new THREE.Mesh(sillGeo, frameMat);\n                        sill.position.set(0, y - 0.8, size / 2 + 0.1);\n                        sill.rotation.y = side.rot;\n                        sill.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), side.rot);\n                        sill.castShadow = true;\n                        group.add(sill);\n\n                        // Individual windows\n                        for (let x = -size / 2 + 1.6; x < size / 2 - 1; x += 2.5) {\n                            const winGroup = new THREE.Group();\n                            winGroup.position.set(x, y, size / 2 + 0.05);\n\n                            // Dark Outer Frame\n                            const fGeo = new THREE.BoxGeometry(1.7, 1.4, 0.08);\n                            const frame = new THREE.Mesh(fGeo, frameMat);\n                            winGroup.add(frame);\n\n                            // Shiny Glass\n                            const gGeo = new THREE.BoxGeometry(1.5, 1.2, 0.06);\n                            const glass = new THREE.Mesh(gGeo, glassMat);\n                            glass.position.z = 0.03;\n                            winGroup.add(glass);\n\n                            winGroup.rotation.y = side.rot;\n                            winGroup.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), side.rot);\n                            group.add(winGroup);\n                        }\n                    }\n                });\n\n                // 5. Roof Details (Parapet Wall and Rooftop machinery)\n                const roofSlabGeo = new THREE.BoxGeometry(size, 0.2, size);\n                const roofSlab = new THREE.Mesh(roofSlabGeo, roofMat);\n                roofSlab.position.y = totalHeight;\n                group.add(roofSlab);\n\n                // Parapet/Border wall around roof\n                const paraW = 0.18;\n                const paraH = 0.65;\n                const paraGeoHorizontal = new THREE.BoxGeometry(size, paraH, paraW);\n                const paraGeoVertical = new THREE.BoxGeometry(paraW, paraH, size - paraW * 2);\n\n                // Front & Back parapets\n                const p1 = new THREE.Mesh(paraGeoHorizontal, upperMat);\n                p1.position.set(0, totalHeight + paraH / 2, size / 2 - paraW / 2);\n                p1.castShadow = true;\n                group.add(p1);\n\n                const p2 = new THREE.Mesh(paraGeoHorizontal, upperMat);\n                p2.position.set(0, totalHeight + paraH / 2, -size / 2 + paraW / 2);\n                p2.castShadow = true;\n                group.add(p2);\n\n                // Left & Right parapets\n                const p3 = new THREE.Mesh(paraGeoVertical, upperMat);\n                p3.position.set(size / 2 - paraW / 2, totalHeight + paraH / 2, 0);\n                p3.castShadow = true;\n                group.add(p3);\n\n                const p4 = new THREE.Mesh(paraGeoVertical, upperMat);\n                p4.position.set(-size / 2 + paraW / 2, totalHeight + paraH / 2, 0);\n                p4.castShadow = true;\n                group.add(p4);\n\n                // Staircase / Access Penthouse on Roof\n                const stairsGroup = new THREE.Group();\n                const stairsGeo = new THREE.BoxGeometry(2.5, 2.3, 2.5);\n                const stairsBox = new THREE.Mesh(stairsGeo, upperMat);\n                stairsBox.position.set(-2, totalHeight + 1.15, 2);\n                stairsBox.castShadow = true;\n                group.add(stairsBox);\n\n                // Penthouse Roof\n                const sRoofGeo = new THREE.BoxGeometry(2.8, 0.12, 2.8);\n                const sRoof = new THREE.Mesh(sRoofGeo, frameMat);\n                sRoof.position.set(-2, totalHeight + 2.36, 2);\n                sRoof.castShadow = true;\n                group.add(sRoof);\n\n                // Upgraded HVAC Unit 1\n                const hvac1Group = new THREE.Group();\n                hvac1Group.position.set(2, totalHeight + 0.6, -2);\n                const hvacBody = new THREE.Mesh(new THREE.BoxGeometry(1.6, 1.2, 1.6), grillMat);\n                hvacBody.castShadow = true;\n                hvac1Group.add(hvacBody);\n\n                const fanGeo = new THREE.CylinderGeometry(0.5, 0.5, 0.1, 12);\n                const fan = new THREE.Mesh(fanGeo, metalMat);\n                fan.position.y = 0.62;\n                hvac1Group.add(fan);\n                group.add(hvac1Group);\n\n                // HVAC Unit 2\n                const hvac2Group = hvac1Group.clone();\n                hvac2Group.position.set(2, totalHeight + 0.6, 1);\n                group.add(hvac2Group);\n\n                return group;\n            }\n\n            function createApartmentBuilding() {\n                const group = new THREE.Group();\n\n                const width = 5.4;\n                const depth = 5.4;\n                const groundFloorH = 3.2;\n                const floorH = 2.8;\n                const numUpperFloors = 4;\n                const totalHeight = groundFloorH + numUpperFloors * floorH;\n\n                const facadeMat = new THREE.MeshLambertMaterial({ color: 0xd4944c });\n                const trimMat = new THREE.MeshLambertMaterial({ color: 0xe8dcc8, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 });\n                const roofMat = new THREE.MeshLambertMaterial({ color: 0x7a7a7a });\n                const storefrontMat = new THREE.MeshLambertMaterial({ color: 0x1e2e20 });\n                const windowMat = new THREE.MeshLambertMaterial({ color: 0x3a5a7a });\n                const windowFrameMat = new THREE.MeshLambertMaterial({ color: 0xddd8c8, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 });\n                const awningBlueMat = new THREE.MeshLambertMaterial({ color: 0x4488cc });\n                const awningWhiteMat = new THREE.MeshLambertMaterial({ color: 0xffffff });\n                const chimneyMat = new THREE.MeshLambertMaterial({ color: 0x888888 });\n                const doorMat = new THREE.MeshLambertMaterial({ color: 0x1a1a1a });\n\n                const bodyGeo = new THREE.BoxGeometry(width, totalHeight, depth);\n                const body = new THREE.Mesh(bodyGeo, facadeMat);\n                body.position.y = totalHeight / 2;\n                body.castShadow = true;\n                body.receiveShadow = true;\n                group.add(body);\n\n                const trimW = 0.35;\n                const trimGeo = new THREE.BoxGeometry(trimW, totalHeight + 0.05, trimW);\n                const cornerOffsets = [\n                    [-width / 2 + trimW / 2, -depth / 2 + trimW / 2],\n                    [width / 2 - trimW / 2, -depth / 2 + trimW / 2],\n                    [-width / 2 + trimW / 2, depth / 2 - trimW / 2],\n                    [width / 2 - trimW / 2, depth / 2 - trimW / 2]\n                ];\n                cornerOffsets.forEach(([cx, cz]) => {\n                    const pillar = new THREE.Mesh(trimGeo, trimMat);\n                    pillar.position.set(cx, totalHeight / 2, cz);\n                    pillar.castShadow = true;\n                    group.add(pillar);\n                });\n\n                for (let f = 1; f <= numUpperFloors; f++) {\n                    const bandY = groundFloorH + (f - 1) * floorH;\n                    const bandGeo = new THREE.BoxGeometry(width + 0.08, 0.15, depth + 0.08);\n                    const band = new THREE.Mesh(bandGeo, trimMat);\n                    band.position.y = bandY;\n                    group.add(band);\n                }\n\n                const roofGeo = new THREE.BoxGeometry(width + 0.5, 0.35, depth + 0.5);\n                const roof = new THREE.Mesh(roofGeo, roofMat);\n                roof.position.y = totalHeight + 0.175;\n                roof.castShadow = true;\n                group.add(roof);\n\n                const corniceGeo = new THREE.BoxGeometry(width + 0.25, 0.25, depth + 0.25);\n                const cornice = new THREE.Mesh(corniceGeo, trimMat);\n                cornice.position.y = totalHeight + 0.01;\n                group.add(cornice);\n\n                const chimGeo = new THREE.BoxGeometry(0.8, 1.5, 0.8);\n                const chimney = new THREE.Mesh(chimGeo, chimneyMat);\n                chimney.position.set(width * 0.22, totalHeight + 0.35 + 0.75, -depth * 0.22);\n                chimney.castShadow = true;\n                group.add(chimney);\n                const chimCapGeo = new THREE.BoxGeometry(1.0, 0.15, 1.0);\n                const chimCap = new THREE.Mesh(chimCapGeo, new THREE.MeshLambertMaterial({ color: 0x666666 }));\n                chimCap.position.set(width * 0.22, totalHeight + 0.35 + 1.5 + 0.075, -depth * 0.22);\n                group.add(chimCap);\n\n                const sfHeight = 2.4;\n                const sfGeo = new THREE.BoxGeometry(width - 1.2, sfHeight, 0.1);\n                const sfFront = new THREE.Mesh(sfGeo, storefrontMat);\n                sfFront.position.set(0, sfHeight / 2 + 0.3, depth / 2 + 0.05);\n                group.add(sfFront);\n\n                const doorGeo = new THREE.BoxGeometry(1.0, 2.0, 0.12);\n                const door = new THREE.Mesh(doorGeo, doorMat);\n                door.position.set(0, 1.0, depth / 2 + 0.06);\n                group.add(door);\n\n                const winW = 0.85;\n                const winH = 1.3;\n                const winGeoFront = new THREE.BoxGeometry(winW, winH, 0.1);\n                const winGeoSide = new THREE.BoxGeometry(0.1, winH, winW);\n                const frameW = winW + 0.18;\n                const frameH = winH + 0.18;\n                const frameGeoFront = new THREE.BoxGeometry(frameW, frameH, 0.06);\n                const frameGeoSide = new THREE.BoxGeometry(0.06, frameH, frameW);\n\n                for (let floor = 0; floor < numUpperFloors; floor++) {\n                    const floorY = groundFloorH + floor * floorH + floorH * 0.5;\n\n                    for (let w = -1; w <= 1; w++) {\n                        const wx = w * 1.55;\n                        const frame = new THREE.Mesh(frameGeoFront, windowFrameMat);\n                        frame.position.set(wx, floorY, depth / 2 + 0.03);\n                        group.add(frame);\n                        const win = new THREE.Mesh(winGeoFront, windowMat);\n                        win.position.set(wx, floorY, depth / 2 + 0.06);\n                        group.add(win);\n                    }\n\n\n                }\n\n                const entAwGeo = new THREE.BoxGeometry(1.8, 0.07, 0.7);\n                const entAw = new THREE.Mesh(entAwGeo, awningBlueMat);\n                entAw.position.set(0, groundFloorH * 0.78, depth / 2 + 0.4);\n                entAw.rotation.x = -0.22;\n                entAw.castShadow = true;\n                group.add(entAw);\n                const entStrGeo = new THREE.BoxGeometry(1.8, 0.08, 0.15);\n                const entStr1 = new THREE.Mesh(entStrGeo, awningWhiteMat);\n                entStr1.position.set(0, groundFloorH * 0.78 - 0.01, depth / 2 + 0.2);\n                entStr1.rotation.x = -0.22;\n                group.add(entStr1);\n                const entStr2 = new THREE.Mesh(entStrGeo, awningWhiteMat);\n                entStr2.position.set(0, groundFloorH * 0.78 - 0.01, depth / 2 + 0.6);\n                entStr2.rotation.x = -0.22;\n                group.add(entStr2);\n\n                return group;\n            }\n\n            function createApartmentVariant(facadeColor, numUpperFloors = 4) {\n                const group = new THREE.Group();\n\n                const width = 5.4;\n                const depth = 5.4;\n                const groundFloorH = 3.2;\n                const floorH = 2.8;\n                const totalHeight = groundFloorH + numUpperFloors * floorH;\n\n                const facadeMat = new THREE.MeshLambertMaterial({ color: facadeColor });\n                const trimMat = new THREE.MeshLambertMaterial({ color: 0xe8dcc8, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 });\n                const roofMat = new THREE.MeshLambertMaterial({ color: 0x7a7a7a });\n                const storefrontMat = new THREE.MeshLambertMaterial({ color: 0x1e2e20 });\n                const windowMat = new THREE.MeshLambertMaterial({ color: 0x3a5a7a });\n                const windowFrameMat = new THREE.MeshLambertMaterial({ color: 0xddd8c8, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 });\n                const awningBlueMat = new THREE.MeshLambertMaterial({ color: 0x4488cc });\n                const awningWhiteMat = new THREE.MeshLambertMaterial({ color: 0xffffff });\n                const chimneyMat = new THREE.MeshLambertMaterial({ color: 0x888888 });\n                const doorMat = new THREE.MeshLambertMaterial({ color: 0x1a1a1a });\n\n                // Main body\n                const bodyGeo = new THREE.BoxGeometry(width, totalHeight, depth);\n                const body = new THREE.Mesh(bodyGeo, facadeMat);\n                body.position.y = totalHeight / 2;\n                body.castShadow = true; body.receiveShadow = true;\n                group.add(body);\n\n                // Corner trim pillars\n                const trimW = 0.35;\n                const trimGeo = new THREE.BoxGeometry(trimW, totalHeight + 0.05, trimW);\n                [[-1,-1],[1,-1],[-1,1],[1,1]].forEach(([sx,sz]) => {\n                    const pillar = new THREE.Mesh(trimGeo, trimMat);\n                    pillar.position.set(sx*(width/2 - trimW/2), totalHeight/2, sz*(depth/2 - trimW/2));\n                    pillar.castShadow = true;\n                    group.add(pillar);\n                });\n\n                // Floor separator bands\n                for (let f = 1; f <= numUpperFloors; f++) {\n                    const bandY = groundFloorH + (f-1)*floorH;\n                    const band = new THREE.Mesh(new THREE.BoxGeometry(width+0.08,0.15,depth+0.08), trimMat);\n                    band.position.y = bandY;\n                    group.add(band);\n                }\n\n                // Roof\n                const roof = new THREE.Mesh(new THREE.BoxGeometry(width+0.5,0.35,depth+0.5), roofMat);\n                roof.position.y = totalHeight+0.175; roof.castShadow = true;\n                group.add(roof);\n                const cornice = new THREE.Mesh(new THREE.BoxGeometry(width+0.25,0.25,depth+0.25), trimMat);\n                cornice.position.y = totalHeight+0.01;\n                group.add(cornice);\n\n                // Chimney\n                const chimney = new THREE.Mesh(new THREE.BoxGeometry(0.8,1.5,0.8), chimneyMat);\n                chimney.position.set(width*0.22, totalHeight+0.35+0.75, -depth*0.22);\n                chimney.castShadow = true;\n                group.add(chimney);\n                const chimCap = new THREE.Mesh(new THREE.BoxGeometry(1.0,0.15,1.0), new THREE.MeshLambertMaterial({color:0x666666}));\n                chimCap.position.set(width*0.22, totalHeight+0.35+1.5+0.075, -depth*0.22);\n                group.add(chimCap);\n\n                // Storefront\n                const sfGeo = new THREE.BoxGeometry(width-1.2, 2.4, 0.1);\n                const sf = new THREE.Mesh(sfGeo, storefrontMat);\n                sf.position.set(0, 1.5, depth/2+0.05);\n                group.add(sf);\n                const door = new THREE.Mesh(new THREE.BoxGeometry(1.0,2.0,0.12), doorMat);\n                door.position.set(0, 1.0, depth/2+0.06);\n                group.add(door);\n\n                // Windows on front face\n                const winW = 0.85, winH = 1.3;\n                const winGeo = new THREE.BoxGeometry(winW, winH, 0.1);\n                const frameGeo = new THREE.BoxGeometry(winW+0.18, winH+0.18, 0.06);\n                for (let floor = 0; floor < numUpperFloors; floor++) {\n                    const floorY = groundFloorH + floor*floorH + floorH*0.5;\n                    for (let w = -1; w <= 1; w++) {\n                        const wx = w * 1.55;\n                        const frame = new THREE.Mesh(frameGeo, windowFrameMat);\n                        frame.position.set(wx, floorY, depth/2+0.03);\n                        group.add(frame);\n                        const win = new THREE.Mesh(winGeo, windowMat);\n                        win.position.set(wx, floorY, depth/2+0.06);\n                        group.add(win);\n                    }\n                }\n\n                // Entrance awning\n                const entAw = new THREE.Mesh(new THREE.BoxGeometry(1.8,0.07,0.7), awningBlueMat);\n                entAw.position.set(0, groundFloorH*0.78, depth/2+0.4);\n                entAw.rotation.x = -0.22; entAw.castShadow = true;\n                group.add(entAw);\n                const entStrGeo = new THREE.BoxGeometry(1.8,0.08,0.15);\n                const s1 = new THREE.Mesh(entStrGeo, awningWhiteMat);\n                s1.position.set(0, groundFloorH*0.78-0.01, depth/2+0.2); s1.rotation.x = -0.22;\n                group.add(s1);\n                const s2 = new THREE.Mesh(entStrGeo, awningWhiteMat);\n                s2.position.set(0, groundFloorH*0.78-0.01, depth/2+0.6); s2.rotation.x = -0.22;\n                group.add(s2);\n\n                return group;\n            }\n\n            function createApartmentYellow() { return createApartmentVariant(0xd4b84c, 5); }\n            function createApartmentRed() { return createApartmentVariant(0xc45a4a, 5); }\n            function createApartmentBlue() { return createApartmentVariant(0x6a9abf, 6); }\n            function createApartmentGreen() { return createApartmentVariant(0x6b9a6b, 4); }\n\n            function createBillboardA() {\n                const group = new THREE.Group();\n\n                const poleMat = new THREE.MeshLambertMaterial({ color: 0x444444 });\n                const frameMat = new THREE.MeshLambertMaterial({ color: 0x222222 });\n                const boardMat = new THREE.MeshLambertMaterial({ color: 0xeeeeee });\n\n                // 1. Central Support Pole\n                const poleGeo = new THREE.CylinderGeometry(0.12, 0.12, 4.0, 8);\n                const pole = new THREE.Mesh(poleGeo, poleMat);\n                pole.position.y = 2.0;\n                pole.castShadow = true;\n                pole.receiveShadow = true;\n                group.add(pole);\n\n                // 2. Large Outer Frame\n                const frameGeo = new THREE.BoxGeometry(3.0, 1.8, 0.2);\n                const frame = new THREE.Mesh(frameGeo, frameMat);\n                frame.position.y = 4.0;\n                frame.castShadow = true;\n                group.add(frame);\n\n                // 3. Inner Advertisement Board\n                const boardGeo = new THREE.BoxGeometry(2.8, 1.6, 0.22);\n                const board = new THREE.Mesh(boardGeo, boardMat);\n                board.position.y = 4.0;\n                board.castShadow = true;\n                group.add(board);\n\n                // 4. Low-poly Burger pop-out advertisement (colorful, readable)\n                const burgerGroup = new THREE.Group();\n                burgerGroup.position.set(0, 4.0, 0.12);\n                burgerGroup.scale.set(1.1, 1.1, 1.1);\n\n                const bunColor = 0xcd853f; // orange-brown\n                const pattyColor = 0x4a3b32; // dark brown\n                const cheeseColor = 0xffeb3b; // yellow\n                const lettuceColor = 0x4caf50; // green\n\n                const bunMat = new THREE.MeshLambertMaterial({ color: bunColor });\n                const pattyMat = new THREE.MeshLambertMaterial({ color: pattyColor });\n                const cheeseMat = new THREE.MeshLambertMaterial({ color: cheeseColor });\n                const lettuceMat = new THREE.MeshLambertMaterial({ color: lettuceColor });\n\n                // Bottom Bun\n                const botBunGeo = new THREE.CylinderGeometry(0.35, 0.35, 0.1, 10);\n                const botBun = new THREE.Mesh(botBunGeo, bunMat);\n                botBun.rotation.x = Math.PI / 2;\n                botBun.position.set(-0.5, 0, 0);\n                burgerGroup.add(botBun);\n\n                // Patty\n                const pattyGeo = new THREE.CylinderGeometry(0.38, 0.38, 0.12, 10);\n                const patty = new THREE.Mesh(pattyGeo, pattyMat);\n                patty.rotation.x = Math.PI / 2;\n                patty.position.set(-0.5, 0.08, 0.02);\n                burgerGroup.add(patty);\n\n                // Cheese\n                const cheeseGeo = new THREE.BoxGeometry(0.72, 0.72, 0.04);\n                const cheese = new THREE.Mesh(cheeseGeo, cheeseMat);\n                cheese.rotation.z = Math.PI / 4;\n                cheese.position.set(-0.5, 0.14, 0.03);\n                burgerGroup.add(cheese);\n\n                // Lettuce\n                const lettuceGeo = new THREE.BoxGeometry(0.74, 0.1, 0.74);\n                const lettuce = new THREE.Mesh(lettuceGeo, lettuceMat);\n                lettuce.rotation.y = 0.2;\n                lettuce.position.set(-0.5, 0.18, 0.02);\n                burgerGroup.add(lettuce);\n\n                // Top Bun (Dome)\n                const topBunGeo = new THREE.CylinderGeometry(0.3, 0.35, 0.18, 10);\n                const topBun = new THREE.Mesh(topBunGeo, bunMat);\n                topBun.rotation.x = Math.PI / 2;\n                topBun.position.set(-0.5, 0.28, 0);\n                burgerGroup.add(topBun);\n\n                // Graphic details: simple colorful shapes to decorate the billboard face (e.g. background stripes)\n                const stripeMat1 = new THREE.MeshLambertMaterial({ color: 0xffa726 }); // orange stripes\n                const bgStripe1 = new THREE.Mesh(new THREE.BoxGeometry(1.2, 1.4, 0.02), stripeMat1);\n                bgStripe1.position.set(0.6, 0.0, -0.09);\n                burgerGroup.add(bgStripe1);\n\n                const starMat = new THREE.MeshLambertMaterial({ color: 0xffeb3b });\n                const starGeo = new THREE.BoxGeometry(0.18, 0.18, 0.05);\n                for(let i=0; i<3; i++) {\n                    const star = new THREE.Mesh(starGeo, starMat);\n                    star.position.set(0.5 + i*0.3, 0.4 - i*0.15, 0.02);\n                    star.rotation.z = 0.3 * i;\n                    burgerGroup.add(star);\n                }\n\n                group.add(burgerGroup);\n\n                return group;\n            }\n\n            function createBillboardB() {\n                const group = new THREE.Group();\n\n                const poleMat = new THREE.MeshLambertMaterial({ color: 0x444444 });\n                const frameMat = new THREE.MeshLambertMaterial({ color: 0x222222 });\n                const boardMat = new THREE.MeshLambertMaterial({ color: 0xeeeeee });\n\n                // 1. Central Support Pole\n                const poleGeo = new THREE.CylinderGeometry(0.12, 0.12, 4.0, 8);\n                const pole = new THREE.Mesh(poleGeo, poleMat);\n                pole.position.y = 2.0;\n                pole.castShadow = true;\n                pole.receiveShadow = true;\n                group.add(pole);\n\n                // 2. Large Outer Frame\n                const frameGeo = new THREE.BoxGeometry(3.0, 1.8, 0.2);\n                const frame = new THREE.Mesh(frameGeo, frameMat);\n                frame.position.y = 4.0;\n                frame.castShadow = true;\n                group.add(frame);\n\n                // 3. Inner Advertisement Board\n                const boardGeo = new THREE.BoxGeometry(2.8, 1.6, 0.22);\n                const board = new THREE.Mesh(boardGeo, boardMat);\n                board.position.y = 4.0;\n                board.castShadow = true;\n                group.add(board);\n\n                // 4. Low-poly Soda Cup pop-out advertisement (colorful, readable)\n                const sodaGroup = new THREE.Group();\n                sodaGroup.position.set(0, 4.0, 0.12);\n                sodaGroup.scale.set(1.1, 1.1, 1.1);\n\n                const cupMat = new THREE.MeshLambertMaterial({ color: 0xe53935 }); // vibrant red cup\n                const lidMat = new THREE.MeshLambertMaterial({ color: 0xffffff }); // white lid\n                const strawMat = new THREE.MeshLambertMaterial({ color: 0xffeb3b }); // yellow straw\n                const stripeMat = new THREE.MeshLambertMaterial({ color: 0xffffff }); // white stripe on cup\n\n                // Cup Body (Tapered cylinder)\n                const cupGeo = new THREE.CylinderGeometry(0.28, 0.20, 0.8, 10);\n                const cup = new THREE.Mesh(cupGeo, cupMat);\n                cup.position.set(-0.5, -0.05, 0.05);\n                cup.rotation.x = Math.PI / 18; // slightly tilted for action\n                sodaGroup.add(cup);\n\n                // Cup Stripe\n                const cupStripeGeo = new THREE.CylinderGeometry(0.29, 0.27, 0.15, 10);\n                const cupStripe = new THREE.Mesh(cupStripeGeo, stripeMat);\n                cupStripe.position.set(-0.5, -0.05, 0.05);\n                cupStripe.rotation.x = Math.PI / 18;\n                sodaGroup.add(cupStripe);\n\n                // Cup Lid (flat cylinder)\n                const lidGeo = new THREE.CylinderGeometry(0.31, 0.31, 0.06, 10);\n                const lid = new THREE.Mesh(lidGeo, lidMat);\n                lid.position.set(-0.46, 0.36, 0.05);\n                lid.rotation.x = Math.PI / 18;\n                sodaGroup.add(lid);\n\n                // Straw (thin cylinder)\n                const strawGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.4, 6);\n                const straw = new THREE.Mesh(strawGeo, strawMat);\n                straw.position.set(-0.38, 0.52, 0.06);\n                straw.rotation.z = -0.3; // angled\n                straw.rotation.x = Math.PI / 18;\n                sodaGroup.add(straw);\n\n                // Graphic details: background blue wave\n                const waveMat = new THREE.MeshLambertMaterial({ color: 0x1e88e5 }); // blue wave\n                const bgWave = new THREE.Mesh(new THREE.BoxGeometry(1.3, 1.4, 0.02), waveMat);\n                bgWave.position.set(0.5, 0.0, -0.09);\n                sodaGroup.add(bgWave);\n\n                const bubbleMat = new THREE.MeshLambertMaterial({ color: 0xffffff });\n                const bubbleGeo = new THREE.SphereGeometry(0.06, 6, 6);\n                for(let i=0; i<4; i++) {\n                    const bubble = new THREE.Mesh(bubbleGeo, bubbleMat);\n                    bubble.position.set(0.3 + i*0.25, -0.3 + i*0.2, 0.02);\n                    sodaGroup.add(bubble);\n                }\n\n                group.add(sodaGroup);\n\n                return group;\n            }\n\n            // --- Factory Registry ---\n            // Maps modelType strings to factory functions.\n            // To add a new type, simply add an entry here.\n            const modelFactories = {\n                'bank': createBank,\n                'officeMedium': createOfficeMedium,\n                'officeSmall': createOfficeSmall,\n                'cone': createCone,\n                'trashcan': createTrashCan,\n                'trashbag': createTrashBag,\n                'phonebooth': createPhoneBooth,\n                'streetVendorStall': createStreetVendorStall,\n                'lamppost': createLampPost,\n                'bench': createBench,\n                'stopsign': createStopSign,\n                'personManA': createPersonManA,\n                'smallTree': createSmallTree,\n                'largeTree': createLargeTree,\n                'flower': createFlower,\n                'carSedanBlue': createCarSedanBlue,\n                'carCompactRed': createCarCompactRed,\n                'carTaxiYellow': createCarTaxiYellow,\n                'truckDeliveryWhite': createTruckDeliveryWhite,\n                'apartmentBuilding': createApartmentBuilding,\n                'apartmentYellow': createApartmentYellow,\n                'apartmentRed': createApartmentRed,\n                'apartmentBlue': createApartmentBlue,\n                'apartmentGreen': createApartmentGreen,\n                'billboardBurger': createBillboardA,\n                'billboardSoda': createBillboardB\n            };\n\n            /**\n             * Creates an object model by modelType.\n             * Falls back to a default box if the type has no factory.\n             * @param {string} modelType - The model type key from registry.\n             * @param {object} instanceData - Instance data (w, h, d, color for fallback).\n             * @returns {THREE.Group} The assembled object group.\n             */\n            function create(modelType, instanceData) {\n                const factory = modelFactories[modelType];\n                if (factory) {\n                    return factory();\n                }\n                // Fallback: colored box using instance dimensions\n                const w = instanceData.w || 0.5;\n                const h = instanceData.h || 0.5;\n                const d = instanceData.d || 0.5;\n                const color = instanceData.color || '#888888';\n                return createDefaultBox(w, h, d, color);\n            }\n\n            return { create, createCone, createTrashCan, createTrashBag, createStreetVendorStall, createLampPost, createBench, createStopSign, createFlower, createDefaultBox };\n        })();\n\n\n        // ============================================================\n        // 4. WORLD GENERATION\n        // ============================================================\n        // Holds references to the ground mesh and all spawned objects\n        let groundMesh = null;\n        let gridHelper = null;\n        let worldObjects = {}; // Map of instanceId -> THREE.Group\n        let instanceCounter = 0; // Counter for generating unique instance IDs\n\n        function generateInstanceId() {\n            instanceCounter++;\n            return 'inst_' + String(instanceCounter).padStart(3, '0');\n        }\n\n        function getWorldObjectsArray() {\n            return Object.values(worldObjects);\n        }\n\n        function getWorldMeshesForRaycast() {\n            const meshes = [];\n            Object.values(worldObjects).forEach(obj => {\n                obj.traverse(child => {\n                    if (child.isMesh) meshes.push(child);\n                });\n            });\n            return meshes;\n        }\n\n        function getInstanceGroupFromHit(object) {\n            let current = object;\n            while (current) {\n                if (current.userData && current.userData.instanceId) return current;\n                current = current.parent;\n            }\n            return null;\n        }\n\n        function createGround(gridSize) {\n            const groundGeo = new THREE.PlaneGeometry(gridSize, gridSize);\n            const groundMat = new THREE.MeshLambertMaterial({ color: 0x0c1b2b }); // Ocean bed dark color instead of bright gray\n            groundMesh = new THREE.Mesh(groundGeo, groundMat);\n            groundMesh.rotation.x = -Math.PI / 2;\n            groundMesh.position.y = -5.0; // Pushed deep down to prevent any overlap\n            groundMesh.receiveShadow = true;\n            scene.add(groundMesh);\n\n            const gridDivisions = Math.round(gridSize / (config.terrainCellSize || 2));\n            gridHelper = new THREE.GridHelper(gridSize, gridDivisions, 0x606060, 0x606060);\n            groundMesh.add(gridHelper);\n            gridHelper.rotation.x = Math.PI / 2;\n            gridHelper.position.z = 0.01;\n            gridHelper.visible = config.showGridLines !== false ? config.showGridLines : false;\n        }\n\n        function spawnWorldObjectFromInstance(id, def) {\n            const num = parseInt(id.replace('inst_', ''), 10);\n            if (!isNaN(num) && num > instanceCounter) instanceCounter = num;\n\n            const modelType = getModelTypeForInstance(def);\n            const group = ObjectFactory.create(modelType, def);\n            group.position.set(def.x, 0, def.z);\n            const entry = def.registryId ? getRegistryEntry(def.registryId) : null;\n            const effectiveScale = (def.scale !== undefined && def.scale !== null)\n                ? def.scale\n                : ((entry && entry.defaultScale) ? entry.defaultScale : 1);\n            group.scale.setScalar(effectiveScale);\n            group.rotation.y = def.rotationY !== undefined ? def.rotationY : 0;\n            group.userData.instanceId = id;\n            group.visible = true;\n            scene.add(group);\n            return group;\n        }\n\n        function spawnPlaceholderObjects(instanceMap) {\n            Object.keys(instanceMap).forEach(id => {\n                worldObjects[id] = spawnWorldObjectFromInstance(id, instanceMap[id]);\n            });\n        }\n\n        function disposeWorldObjectGroup(group) {\n            if (!group) return;\n            scene.remove(group);\n            group.traverse(child => {\n                if (!child.isMesh) return;\n                if (child.geometry) child.geometry.dispose();\n                const mats = Array.isArray(child.material) ? child.material : [child.material];\n                mats.forEach(m => { if (m) m.dispose(); });\n            });\n        }\n\n        function resetWorldObjectFromInstance(mesh, id, def) {\n            mesh.position.set(def.x, 0, def.z);\n            const entry = def.registryId ? getRegistryEntry(def.registryId) : null;\n            const effectiveScale = (def.scale !== undefined && def.scale !== null)\n                ? def.scale\n                : ((entry && entry.defaultScale) ? entry.defaultScale : 1);\n            mesh.scale.setScalar(effectiveScale);\n            mesh.rotation.set(0, def.rotationY !== undefined ? def.rotationY : 0, 0);\n            mesh.visible = true;\n            mesh.renderOrder = 0;\n            mesh.userData.instanceId = id;\n            delete mesh.userData.isGhostResidue;\n            delete mesh.userData._originalMaterialSnapshots;\n        }\n\n        function cleanupOrphanInstanceMeshes() {\n            const orphans = [];\n            for (let i = 0; i < scene.children.length; i++) {\n                const child = scene.children[i];\n                if (!child.userData || !child.userData.instanceId) continue;\n                const id = child.userData.instanceId;\n                if (worldObjects[id] !== child) {\n                    orphans.push(child);\n                }\n            }\n            orphans.forEach(mesh => disposeWorldObjectGroup(mesh));\n        }\n\n        function clearManifestVfx() {\n            manifestAnimations.length = 0;\n            manifestRings.forEach(ring => {\n                scene.remove(ring.mesh);\n                if (ring.mesh.geometry) ring.mesh.geometry.dispose();\n                if (ring.mesh.material) ring.mesh.material.dispose();\n            });\n            manifestRings.length = 0;\n        }\n\n        function softResetWorld() {\n            clearManifestVfx();\n            if (typeof clearFloatingTexts === 'function') clearFloatingTexts();\n            if (typeof clearFlyingStars === 'function') clearFlyingStars();\n\n            const instances = config.objectInstances || {};\n            const recreateIds = new Set();\n\n            Object.keys(worldObjects).forEach(id => {\n                const mesh = worldObjects[id];\n                if (mesh && mesh.userData && mesh.userData.isGhostResidue) {\n                    disposeWorldObjectGroup(mesh);\n                    delete worldObjects[id];\n                    recreateIds.add(id);\n                }\n            });\n\n            cleanupOrphanInstanceMeshes();\n\n            Object.keys(instances).forEach(id => {\n                const inst = instances[id];\n                if (!inst) return;\n\n                if (recreateIds.has(id) || !worldObjects[id]) {\n                    if (worldObjects[id]) {\n                        disposeWorldObjectGroup(worldObjects[id]);\n                    }\n                    worldObjects[id] = spawnWorldObjectFromInstance(id, inst);\n                } else {\n                    resetWorldObjectFromInstance(worldObjects[id], id, inst);\n                }\n            });\n\n            Object.keys(worldObjects).forEach(id => {\n                if (!instances[id]) {\n                    disposeWorldObjectGroup(worldObjects[id]);\n                    delete worldObjects[id];\n                }\n            });\n\n            InstanceSpatialIndex.rebuild();\n        }\n\n        const manifestAnimations = [];\n        const manifestRings = [];\n\n        const InstanceSpatialIndex = (() => {\n            const cellToInstances = {};\n            const instanceCell = {};\n\n            function removeFromCell(id) {\n                const key = instanceCell[id];\n                if (!key || !cellToInstances[key]) return;\n                const list = cellToInstances[key];\n                const idx = list.indexOf(id);\n                if (idx !== -1) list.splice(idx, 1);\n                if (list.length === 0) delete cellToInstances[key];\n            }\n\n            function setInstanceCell(id, x, z) {\n                const { col, row } = worldToCell(x, z);\n                const key = cellKey(col, row);\n                if (instanceCell[id] === key) return;\n                removeFromCell(id);\n                if (!cellToInstances[key]) cellToInstances[key] = [];\n                cellToInstances[key].push(id);\n                instanceCell[id] = key;\n            }\n\n            function rebuild() {\n                Object.keys(cellToInstances).forEach(k => delete cellToInstances[k]);\n                Object.keys(instanceCell).forEach(k => delete instanceCell[k]);\n                const instances = config.objectInstances || {};\n                Object.keys(instances).forEach(id => {\n                    const inst = instances[id];\n                    if (!inst) return;\n                    setInstanceCell(id, inst.x, inst.z);\n                });\n            }\n\n            function queryNear(x, z, radius) {\n                const out = [];\n                const seen = {};\n                const { col: centerCol, row: centerRow } = worldToCell(x, z);\n                const cellRadius = Math.ceil(radius / TERRAIN_CELL_SIZE) + 1;\n\n                for (let dc = -cellRadius; dc <= cellRadius; dc++) {\n                    for (let dr = -cellRadius; dr <= cellRadius; dr++) {\n                        const c = centerCol + dc;\n                        const r = centerRow + dr;\n                        if (c < 0 || c >= TERRAIN_COLS || r < 0 || r >= TERRAIN_ROWS) continue;\n                        const list = cellToInstances[cellKey(c, r)];\n                        if (!list) continue;\n                        for (let i = 0; i < list.length; i++) {\n                            const id = list[i];\n                            if (seen[id]) continue;\n                            seen[id] = true;\n                            out.push(id);\n                        }\n                    }\n                }\n                return out;\n            }\n\n            return { rebuild, queryNear, setInstanceCell };\n        })();\n\n        function buildWorld() {\n            if (groundMesh) scene.remove(groundMesh);\n            Object.values(worldObjects).forEach(obj => scene.remove(obj));\n            worldObjects = {};\n            instanceCounter = 0;\n\n            clearManifestVfx();\n\n            const gridSize = config.gridSize || 100;\n            TERRAIN_CELL_SIZE = config.terrainCellSize || 2;\n            TERRAIN_HALF_GRID = gridSize / 2;\n            TERRAIN_COLS = Math.floor(gridSize / TERRAIN_CELL_SIZE);\n            TERRAIN_ROWS = TERRAIN_COLS;\n\n            createGround(gridSize);\n            \n            // Build the object occupancy map on load/reload\n            config.objectCellOccupancy = {};\n            const instances = config.objectInstances || {};\n            Object.keys(instances).forEach(id => {\n                const inst = instances[id];\n                if (!inst) return;\n                const entry = getRegistryEntry(inst.registryId);\n                const wCells = (entry && entry.widthInCells) || 1;\n                const hCells = (entry && entry.heightInCells) || 1;\n                if (wCells > 1 || hCells > 1 || inst.anchorCol !== undefined) {\n                    if (inst.anchorCol === undefined) {\n                        // Calculate anchor from center x, z\n                        const { col, row } = worldToCell(inst.x, inst.z);\n                        inst.anchorCol = col - Math.floor((wCells - 1) / 2);\n                        inst.anchorRow = row - Math.floor((hCells - 1) / 2);\n                    }\n                    inst.isPrimary = true;\n                    const fp = getObjectEffectiveFootprint(inst.registryId, inst.rotationY || 0);\n                    const keys = getFootprintCellKeys(inst.anchorCol, inst.anchorRow, fp.w, fp.h);\n                    keys.forEach(k => {\n                        config.objectCellOccupancy[k] = id;\n                    });\n                } else {\n                    const { col, row } = worldToCell(inst.x, inst.z);\n                    config.objectCellOccupancy[cellKey(col, row)] = id;\n                }\n            });\n\n            spawnPlaceholderObjects(config.objectInstances || {});\n            InstanceSpatialIndex.rebuild();\n        }\n\n        function calculateTotalAvailablePoints() {\n            totalAvailablePoints = 0;\n            const instances = config.objectInstances || {};\n            Object.keys(instances).forEach(id => {\n                const inst = instances[id];\n                normalizeConsumeState(inst);\n                if (inst && inst.registryId && !inst.consumedByPlayer) {\n                    const entry = getRegistryEntry(inst.registryId);\n                    if (entry && entry.pointValue) {\n                        totalAvailablePoints += entry.pointValue;\n                    }\n                }\n            });\n            lib.log('Total Available Points calculated once: ' + totalAvailablePoints);\n        }\n\n        buildWorld();\n        calculateTotalAvailablePoints();\n        LoadingManager.setProgress(85);\n\n\n        const RegionSystem = (() => {\n            let regions = [];\n            let regionMap = {};\n            let debugGroup = null;\n            let isVisible = false;\n\n            function generateRegions() {\n                regions = [];\n                regionMap = {};\n\n                const gridSize = config.gridSize || 100;\n                const groupSize = config.gridGroupSize || 20;\n                const halfGrid = gridSize / 2;\n\n                const numCols = Math.ceil(gridSize / groupSize);\n                const numRows = Math.ceil(gridSize / groupSize);\n\n                for (let row = 0; row < numRows; row++) {\n                    const rowLetter = String.fromCharCode(65 + row);\n                    for (let col = 0; col < numCols; col++) {\n                        const colNum = col + 1;\n                        const regionId = rowLetter + colNum;\n\n                        const minX = -halfGrid + col * groupSize;\n                        const maxX = Math.min(minX + groupSize, halfGrid);\n                        const minZ = -halfGrid + row * groupSize;\n                        const maxZ = Math.min(minZ + groupSize, halfGrid);\n\n                        const centerX = (minX + maxX) / 2;\n                        const centerZ = (minZ + maxZ) / 2;\n\n                        const region = {\n                            id: regionId,\n                            row: row,\n                            col: col,\n                            bounds: { minX, maxX, minZ, maxZ },\n                            center: { x: centerX, z: centerZ }\n                        };\n\n                        regions.push(region);\n                        regionMap[regionId] = region;\n                    }\n                }\n            }\n\n            function createTextSprite(text) {\n                const canvasEl = document.createElement('canvas');\n                canvasEl.width = 128;\n                canvasEl.height = 64;\n                const ctx = canvasEl.getContext('2d');\n\n                ctx.fillStyle = 'rgba(0,0,0,0)';\n                ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);\n\n                ctx.font = 'bold 48px Arial';\n                ctx.textAlign = 'center';\n                ctx.textBaseline = 'middle';\n                ctx.fillStyle = 'rgba(255, 200, 50, 0.85)';\n                ctx.strokeStyle = 'rgba(0, 0, 0, 0.6)';\n                ctx.lineWidth = 3;\n                ctx.strokeText(text, 64, 32);\n                ctx.fillText(text, 64, 32);\n\n                const texture = new THREE.CanvasTexture(canvasEl);\n                texture.needsUpdate = true;\n\n                const spriteMat = new THREE.SpriteMaterial({\n                    map: texture,\n                    transparent: true,\n                    depthTest: false\n                });\n                const sprite = new THREE.Sprite(spriteMat);\n                sprite.scale.set(6, 3, 1);\n                return sprite;\n            }\n\n            function buildDebugVisualization() {\n                if (debugGroup) {\n                    scene.remove(debugGroup);\n                    debugGroup.traverse(child => {\n                        if (child.geometry) child.geometry.dispose();\n                        if (child.material) {\n                            if (child.material.map) child.material.map.dispose();\n                            child.material.dispose();\n                        }\n                    });\n                }\n\n                debugGroup = new THREE.Group();\n                debugGroup.name = 'RegionDebugGroup';\n\n                const gridSize = config.gridSize || 100;\n                const groupSize = config.gridGroupSize || 20;\n                const halfGrid = gridSize / 2;\n                const numCols = Math.ceil(gridSize / groupSize);\n                const numRows = Math.ceil(gridSize / groupSize);\n                const lineY = 0.05;\n\n                const lineMat = new THREE.LineBasicMaterial({\n                    color: 0xffc832,\n                    linewidth: 2\n                });\n\n                for (let col = 0; col <= numCols; col++) {\n                    const x = -halfGrid + col * groupSize;\n                    const clampedX = Math.min(x, halfGrid);\n                    const points = [\n                        new THREE.Vector3(clampedX, lineY, -halfGrid),\n                        new THREE.Vector3(clampedX, lineY, halfGrid)\n                    ];\n                    const geo = new THREE.BufferGeometry().setFromPoints(points);\n                    const line = new THREE.Line(geo, lineMat);\n                    debugGroup.add(line);\n                }\n\n                for (let row = 0; row <= numRows; row++) {\n                    const z = -halfGrid + row * groupSize;\n                    const clampedZ = Math.min(z, halfGrid);\n                    const points = [\n                        new THREE.Vector3(-halfGrid, lineY, clampedZ),\n                        new THREE.Vector3(halfGrid, lineY, clampedZ)\n                    ];\n                    const geo = new THREE.BufferGeometry().setFromPoints(points);\n                    const line = new THREE.Line(geo, lineMat);\n                    debugGroup.add(line);\n                }\n\n                regions.forEach(region => {\n                    const sprite = createTextSprite(region.id);\n                    sprite.position.set(region.center.x, 0.5, region.center.z);\n                    debugGroup.add(sprite);\n                });\n\n                debugGroup.visible = isVisible;\n                scene.add(debugGroup);\n            }\n\n            function setVisible(visible) {\n                isVisible = visible;\n                if (debugGroup) {\n                    debugGroup.visible = visible;\n                }\n            }\n\n            function rebuild() {\n                generateRegions();\n                buildDebugVisualization();\n            }\n\n            function getRegionByPosition(x, z) {\n                for (let i = 0; i < regions.length; i++) {\n                    const b = regions[i].bounds;\n                    if (x >= b.minX && x < b.maxX && z >= b.minZ && z < b.maxZ) {\n                        return regions[i];\n                    }\n                }\n                return null;\n            }\n\n            function getRegionCenter(regionId) {\n                const region = regionMap[regionId];\n                return region ? { x: region.center.x, z: region.center.z } : null;\n            }\n\n            function getRegionBounds(regionId) {\n                const region = regionMap[regionId];\n                return region ? { ...region.bounds } : null;\n            }\n\n            function getAllRegions() {\n                return regions.slice();\n            }\n\n            rebuild();\n            setVisible(config.showGridGroups || false);\n\n            return {\n                getRegionByPosition,\n                getRegionCenter,\n                getRegionBounds,\n                getAllRegions,\n                rebuild,\n                setVisible\n            };\n        })();\n\n\n        const TerrainRegistry = (() => {\n            const registry = config.terrainRegistry || [\n                { id: 'grass', displayName: 'Grass', color: '#4CAF50' }\n            ];\n\n            function createTerrainTile(terrainTypeId, dims) {\n                const entry = registry.find(e => e.id === terrainTypeId);\n                const color = entry ? entry.color : '#888888';\n                const wCells = (dims && dims.widthInCells) || 1;\n                const hCells = (dims && dims.heightInCells) || 1;\n                const tileW = wCells * TERRAIN_CELL_SIZE;\n                const tileH = hCells * TERRAIN_CELL_SIZE;\n\n                if (terrainTypeId === 'wideRoad') {\n                    const group = new THREE.Group();\n                    group.rotation.x = -Math.PI / 2;\n                    group.position.y = 0.10; // Clean physical stacking: road layer\n                    group.userData.isTerrain = true;\n                    group.renderOrder = 1;\n\n                    const surfGeo = new THREE.PlaneGeometry(tileW, tileH);\n                    const surfMat = new THREE.MeshLambertMaterial({ \n                        color: 0x3a3a3a, \n                        side: THREE.DoubleSide\n                    });\n                    const surf = new THREE.Mesh(surfGeo, surfMat);\n                    surf.receiveShadow = true;\n                    group.add(surf);\n\n                    const whiteMat = new THREE.MeshBasicMaterial({ \n                        color: 0xffffff, \n                        side: THREE.DoubleSide\n                    });\n                    const lineWidth = tileH * 0.03;\n\n                    const edgeGeo = new THREE.PlaneGeometry(lineWidth, tileH);\n                    const leftEdge = new THREE.Mesh(edgeGeo, whiteMat);\n                    leftEdge.position.x = -tileW * 0.46;\n                    leftEdge.position.z = 0.005; // Slightly raised locally (world Y)\n                    group.add(leftEdge);\n\n                    const rightEdge = new THREE.Mesh(edgeGeo.clone(), whiteMat);\n                    rightEdge.position.x = tileW * 0.46;\n                    rightEdge.position.z = 0.005;\n                    group.add(rightEdge);\n\n                    const dashLength = tileH * 0.12;\n                    const dashGeo = new THREE.PlaneGeometry(lineWidth, dashLength);\n                    const numDashes = Math.max(3, Math.round(tileH / (TERRAIN_CELL_SIZE * 0.3)));\n                    const spacing = tileH / (numDashes + 1);\n                    for (let i = 1; i <= numDashes; i++) {\n                        const dash = new THREE.Mesh(dashGeo, whiteMat);\n                        dash.position.x = 0;\n                        dash.position.y = -tileH / 2 + i * spacing;\n                        dash.position.z = 0.005;\n                        group.add(dash);\n                    }\n\n                    return group;\n                }\n\n                if (terrainTypeId === 'sidewalk') {\n                    const group = new THREE.Group();\n                    group.rotation.x = -Math.PI / 2;\n                    group.position.y = 0.10; // Clean physical stacking: sidewalk layer (above road)\n                    group.userData.isTerrain = true;\n                    group.renderOrder = 1;\n\n                    const S = TERRAIN_CELL_SIZE;\n\n                    const surfGeo = new THREE.PlaneGeometry(S, S);\n                    const surfMat = new THREE.MeshLambertMaterial({ \n                        color: 0x8a8a8a, \n                        side: THREE.DoubleSide\n                    });\n                    const surf = new THREE.Mesh(surfGeo, surfMat);\n                    surf.receiveShadow = true;\n                    group.add(surf);\n\n                    const borderMat = new THREE.MeshBasicMaterial({ \n                        color: 0x555555, \n                        side: THREE.DoubleSide\n                    });\n                    const lineW = S * 0.025;\n                    const halfS = S / 2;\n\n                    const hGeo = new THREE.PlaneGeometry(S, lineW);\n                    const topLine = new THREE.Mesh(hGeo, borderMat);\n                    topLine.position.set(0, halfS - lineW / 2, 0.005);\n                    group.add(topLine);\n\n                    const bottomLine = new THREE.Mesh(hGeo.clone(), borderMat);\n                    bottomLine.position.set(0, -halfS + lineW / 2, 0.005);\n                    group.add(bottomLine);\n\n                    const vGeo = new THREE.PlaneGeometry(lineW, S);\n                    const leftLine = new THREE.Mesh(vGeo, borderMat);\n                    leftLine.position.set(-halfS + lineW / 2, 0, 0.005);\n                    group.add(leftLine);\n\n                    const rightLine = new THREE.Mesh(vGeo.clone(), borderMat);\n                    rightLine.position.set(halfS - lineW / 2, 0, 0.005);\n                    group.add(rightLine);\n\n                    return group;\n                }\n\n                if (terrainTypeId === 'intersection') {\n                    const group = new THREE.Group();\n                    group.rotation.x = -Math.PI / 2;\n                    group.position.y = 0.10; // Clean physical stacking: road layer\n                    group.userData.isTerrain = true;\n                    group.renderOrder = 1;\n\n                    const S = TERRAIN_CELL_SIZE;\n                    const surfGeo = new THREE.PlaneGeometry(S, S);\n                    const surfMat = new THREE.MeshLambertMaterial({ \n                        color: 0x3a3a3a, \n                        side: THREE.DoubleSide\n                    });\n                    const surf = new THREE.Mesh(surfGeo, surfMat);\n                    surf.receiveShadow = true;\n                    group.add(surf);\n\n                    return group;\n                }\n\n                if (terrainTypeId === 'road') {\n                    const group = new THREE.Group();\n                    group.rotation.x = -Math.PI / 2;\n                    group.position.y = 0.10; // Clean physical stacking: road layer\n                    group.userData.isTerrain = true;\n                    group.renderOrder = 1;\n\n                    const S = TERRAIN_CELL_SIZE;\n\n                    const surfGeo = new THREE.PlaneGeometry(S, S);\n                    const surfMat = new THREE.MeshLambertMaterial({ \n                        color: 0x3a3a3a, \n                        side: THREE.DoubleSide\n                    });\n                    const surf = new THREE.Mesh(surfGeo, surfMat);\n                    surf.receiveShadow = true;\n                    group.add(surf);\n\n                    const whiteMat = new THREE.MeshBasicMaterial({ \n                        color: 0xffffff, \n                        side: THREE.DoubleSide\n                    });\n                    const lineWidth = S * 0.03;\n\n                    const edgeGeo = new THREE.PlaneGeometry(lineWidth, S);\n                    const leftEdge = new THREE.Mesh(edgeGeo, whiteMat);\n                    leftEdge.position.x = -S * 0.4;\n                    leftEdge.position.z = 0.005;\n                    group.add(leftEdge);\n\n                    const rightEdge = new THREE.Mesh(edgeGeo.clone(), whiteMat);\n                    rightEdge.position.x = S * 0.4;\n                    rightEdge.position.z = 0.005;\n                    group.add(rightEdge);\n\n                    const dashLength = S * 0.15;\n                    const dashGeo = new THREE.PlaneGeometry(lineWidth, dashLength);\n                    const spacing = S / 4;\n                    for (let i = -1; i <= 1; i++) {\n                        const dash = new THREE.Mesh(dashGeo, whiteMat);\n                        dash.position.x = 0;\n                        dash.position.y = i * spacing;\n                        dash.position.z = 0.005;\n                        group.add(dash);\n                    }\n\n                    return group;\n                }\n\n                if (terrainTypeId === 'water') {\n                    const geo = new THREE.PlaneGeometry(tileW, tileH);\n                    const mat = new THREE.MeshLambertMaterial({\n                        color: 0x00BCD4,\n                        transparent: true,\n                        opacity: 0.85,\n                        side: THREE.DoubleSide\n                    });\n                    const mesh = new THREE.Mesh(geo, mat);\n                    mesh.rotation.x = -Math.PI / 2;\n                    mesh.position.y = -1.0; // Substantially pushed down below land layer to eliminate z-fighting and tearing\n                    mesh.receiveShadow = true;\n                    mesh.userData.isTerrain = true;\n                    mesh.renderOrder = 2; // Water is rendered after opaque ground, but before hole\n                    return mesh;\n                }\n\n                const geo = new THREE.PlaneGeometry(TERRAIN_CELL_SIZE, TERRAIN_CELL_SIZE);\n                const mat = new THREE.MeshLambertMaterial({\n                    color: new THREE.Color(color),\n                    side: THREE.DoubleSide\n                });\n                const mesh = new THREE.Mesh(geo, mat);\n                mesh.rotation.x = -Math.PI / 2;\n                mesh.position.y = 0.10; // grass is at sidewalk/ground height\n                mesh.receiveShadow = true;\n                mesh.userData.isTerrain = true;\n                mesh.renderOrder = 1;\n                return mesh;\n            }\n\n            function getAll() {\n                return registry;\n            }\n\n            function getById(id) {\n                return registry.find(e => e.id === id);\n            }\n\n            return { createTerrainTile, getAll, getById };\n        })();\n\n\n        function worldToCell(x, z) {\n            const col = Math.floor((x + TERRAIN_HALF_GRID) / TERRAIN_CELL_SIZE);\n            const row = Math.floor((z + TERRAIN_HALF_GRID) / TERRAIN_CELL_SIZE);\n            return {\n                col: Math.max(0, Math.min(TERRAIN_COLS - 1, col)),\n                row: Math.max(0, Math.min(TERRAIN_ROWS - 1, row))\n            };\n        }\n\n        function cellKey(col, row) {\n            return col + '_' + row;\n        }\n\n        function cellToWorld(col, row) {\n            return {\n                x: -TERRAIN_HALF_GRID + col * TERRAIN_CELL_SIZE + TERRAIN_CELL_SIZE / 2,\n                z: -TERRAIN_HALF_GRID + row * TERRAIN_CELL_SIZE + TERRAIN_CELL_SIZE / 2\n            };\n        }\n\n\n        let placementRotation = 0;\n\n\n        function getTerrainRegistryEntry(id) {\n            const registry = config.terrainRegistry || [];\n            return registry.find(e => e.id === id);\n        }\n\n        function getEffectiveFootprint(typeId, rotationRad) {\n            const entry = getTerrainRegistryEntry(typeId);\n            let w = (entry && entry.widthInCells) ? entry.widthInCells : 1;\n            let h = (entry && entry.heightInCells) ? entry.heightInCells : 1;\n            const steps = Math.round((rotationRad || 0) / (Math.PI / 2)) % 4;\n            if (steps % 2 === 1) { const tmp = w; w = h; h = tmp; }\n            return { w, h };\n        }\n\n        function getObjectEffectiveFootprint(registryId, rotationY) {\n            const entry = getRegistryEntry(registryId);\n            let w = (entry && entry.widthInCells) ? entry.widthInCells : 1;\n            let h = (entry && entry.heightInCells) ? entry.heightInCells : 1;\n            const steps = Math.round((rotationY || 0) / (Math.PI / 2)) % 4;\n            if (steps % 2 === 1) { const tmp = w; w = h; h = tmp; }\n            return { w, h };\n        }\n\n        function getObjectFootprintWorldPosition(anchorCol, anchorRow, w, h) {\n            const centerCol = anchorCol + (w - 1) / 2;\n            const centerRow = anchorRow + (h - 1) / 2;\n            return cellToWorld(centerCol, centerRow);\n        }\n\n        function getObjectFootprintCellKeys(anchorCol, anchorRow, w, h) {\n            return getFootprintCellKeys(anchorCol, anchorRow, w, h);\n        }\n\n        function resolvePrimaryObjectInstance(cellKey) {\n            if (!config.objectCellOccupancy) return null;\n            return config.objectCellOccupancy[cellKey] || null;\n        }\n\n        function clearObjectFootprint(instanceId) {\n            if (!config.objectCellOccupancy) return;\n            Object.keys(config.objectCellOccupancy).forEach(k => {\n                if (config.objectCellOccupancy[k] === instanceId) {\n                    delete config.objectCellOccupancy[k];\n                }\n            });\n        }\n\n        function getFootprintCellKeys(anchorCol, anchorRow, w, h) {\n            const keys = [];\n            for (let dc = 0; dc < w; dc++) {\n                for (let dr = 0; dr < h; dr++) {\n                    keys.push(cellKey(anchorCol + dc, anchorRow + dr));\n                }\n            }\n            return keys;\n        }\n\n        function resolvePrimaryCellKey(key) {\n            const cells = config.terrainCells || {};\n            const data = cells[key];\n            if (!data) return null;\n            if (data.occupiedBy) return data.occupiedBy;\n            if (data.terrainType) return key;\n            return null;\n        }\n\n        function getAllKeysForPiece(primaryKey) {\n            const cells = config.terrainCells || {};\n            const primaryData = cells[primaryKey];\n            if (!primaryData || !primaryData.terrainType) return [primaryKey];\n            const fp = getEffectiveFootprint(primaryData.terrainType, primaryData.rotation || 0);\n            const parts = primaryKey.split('_');\n            const anchorCol = parseInt(parts[0], 10);\n            const anchorRow = parseInt(parts[1], 10);\n            return getFootprintCellKeys(anchorCol, anchorRow, fp.w, fp.h);\n        }\n\n        function clearTerrainPiece(primaryKey) {\n            const cells = config.terrainCells || {};\n            const allKeys = getAllKeysForPiece(primaryKey);\n            allKeys.forEach(k => { delete cells[k]; });\n            TerrainRenderer.eraseCell(primaryKey);\n        }\n\n        function canPlaceFootprint(anchorCol, anchorRow, w, h) {\n            return (anchorCol >= 0 && anchorCol + w <= TERRAIN_COLS &&\n                    anchorRow >= 0 && anchorRow + h <= TERRAIN_ROWS);\n        }\n\n\n        const TerrainRenderer = (() => {\n            const meshes = {};\n            const terrainGroup = new THREE.Group();\n            terrainGroup.name = 'TerrainGroup';\n            scene.add(terrainGroup);\n\n            function disposeTile(tile) {\n                if (!tile) return;\n                if (tile.geometry) { tile.geometry.dispose(); if (tile.material) tile.material.dispose(); }\n                else { tile.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); }); }\n            }\n\n            function paintCell(key, cellData) {\n                if (meshes[key]) {\n                    terrainGroup.remove(meshes[key]);\n                    disposeTile(meshes[key]);\n                    delete meshes[key];\n                }\n\n                const fp = getEffectiveFootprint(cellData.terrainType, cellData.rotation || 0);\n\n                const regEntry = getTerrainRegistryEntry(cellData.terrainType);\n                const origW = (regEntry && regEntry.widthInCells) ? regEntry.widthInCells : 1;\n                const origH = (regEntry && regEntry.heightInCells) ? regEntry.heightInCells : 1;\n\n                const tile = TerrainRegistry.createTerrainTile(cellData.terrainType, { widthInCells: origW, heightInCells: origH });\n\n                const parts = key.split('_');\n                const anchorCol = parseInt(parts[0], 10);\n                const anchorRow = parseInt(parts[1], 10);\n                const centerCol = anchorCol + (fp.w - 1) / 2;\n                const centerRow = anchorRow + (fp.h - 1) / 2;\n                const pos = cellToWorld(centerCol, centerRow);\n                tile.position.x = pos.x;\n                tile.position.z = pos.z;\n\n                if (cellData.rotation) {\n                    tile.rotation.z = cellData.rotation;\n                }\n                tile.userData.cellKey = key;\n                terrainGroup.add(tile);\n                meshes[key] = tile;\n            }\n\n            function eraseCell(key) {\n                if (meshes[key]) {\n                    terrainGroup.remove(meshes[key]);\n                    disposeTile(meshes[key]);\n                    delete meshes[key];\n                }\n            }\n\n            function rebuildAll() {\n                Object.keys(meshes).forEach(key => {\n                    terrainGroup.remove(meshes[key]);\n                    disposeTile(meshes[key]);\n                    delete meshes[key];\n                });\n\n                const cells = config.terrainCells || {};\n                Object.keys(cells).forEach(key => {\n                    const data = cells[key];\n                    if (data.occupiedBy) return;\n                    paintCell(key, data);\n                });\n            }\n\n            return { paintCell, eraseCell, rebuildAll };\n        })();\n\n        TerrainRenderer.rebuildAll();\n\n        const DynamicWaterSystem = (() => {\n            let waterMesh = null;\n\n            function update(centerX, centerZ) {\n                if (!waterMesh) {\n                    const S = (config.gridSize || 1000) * 1.2;\n                    const geo = new THREE.PlaneGeometry(S, S);\n                    const mat = new THREE.MeshLambertMaterial({\n                        color: 0x00BCD4,\n                        transparent: true,\n                        opacity: 0.85,\n                        side: THREE.DoubleSide\n                    });\n                    waterMesh = new THREE.Mesh(geo, mat);\n                    waterMesh.rotation.x = -Math.PI / 2;\n                    waterMesh.position.y = -1.0; // Aligned with the deep water terrain tile height to prevent tearing\n                    waterMesh.receiveShadow = true;\n                    waterMesh.renderOrder = 2; // Rendered after standard terrain, but before hole\n                    scene.add(waterMesh);\n                }\n            }\n\n            function clear() {\n                if (waterMesh) {\n                    scene.remove(waterMesh);\n                    if (waterMesh.geometry) waterMesh.geometry.dispose();\n                    if (waterMesh.material) waterMesh.material.dispose();\n                    waterMesh = null;\n                }\n            }\n\n            return { update, clear };\n        })();\n\n\n        const terrainCursor = (() => {\n            let currentW = 1;\n            let currentH = 1;\n            const geo = new THREE.PlaneGeometry(TERRAIN_CELL_SIZE, TERRAIN_CELL_SIZE);\n            const mat = new THREE.MeshBasicMaterial({\n                color: 0x76ff03,\n                transparent: true,\n                opacity: 0.35,\n                side: THREE.DoubleSide,\n                depthWrite: false\n            });\n            const mesh = new THREE.Mesh(geo, mat);\n            mesh.rotation.x = -Math.PI / 2;\n            mesh.position.y = 0.04;\n            mesh.visible = false;\n            mesh.renderOrder = 5;\n            scene.add(mesh);\n\n            function updateSize() {\n                let fp;\n                if (editorTopMode === 'terrain') {\n                    fp = getEffectiveFootprint(selectedTerrainId, placementRotation);\n                } else if (editorTopMode === 'objects' && editorMode === 'place' && selectedRegistryId) {\n                    fp = getObjectEffectiveFootprint(selectedRegistryId, placementRotation);\n                } else {\n                    fp = { w: 1, h: 1 };\n                }\n                if (fp.w !== currentW || fp.h !== currentH) {\n                    currentW = fp.w;\n                    currentH = fp.h;\n                    mesh.geometry.dispose();\n                    mesh.geometry = new THREE.PlaneGeometry(\n                        fp.w * TERRAIN_CELL_SIZE,\n                        fp.h * TERRAIN_CELL_SIZE\n                    );\n                }\n            }\n\n            function show(worldX, worldZ) {\n                updateSize();\n                const { col, row } = worldToCell(worldX, worldZ);\n                const centerCol = col + (currentW - 1) / 2;\n                const centerRow = row + (currentH - 1) / 2;\n                const pos = cellToWorld(centerCol, centerRow);\n                mesh.position.x = pos.x;\n                mesh.position.z = pos.z;\n                mesh.visible = true;\n            }\n\n            function hide() {\n                mesh.visible = false;\n            }\n\n            function setEraseMode(isErase) {\n                mat.color.set(isErase ? 0xff5252 : 0x76ff03);\n            }\n\n            return { show, hide, setEraseMode, updateSize };\n        })();\n\n\n        // ============================================================\n        // HOLE MODULE \u2014 Base Hole, PlayerHole, HoleProfile, HoleManager\n        // ============================================================\n        const HoleModule = (() => {\n            const HOLE_RADIUS = 1;\n            const HOLE_OPENING_Y_LOCAL = 0.16;\n            const HOLE_CYLINDER_DEPTH = 0.05;\n\n            const PROFILE_KEY_LIST = [\n                'holeSpeed', 'baseHoleScale',\n                'levelThresholds', 'levelScaleMultipliers', 'levelSpeedMultipliers',\n                'levelCameraHeights', 'levelCameraTilts',\n                'consumeNearEdgeShift', 'consumeNearEdgeShiftByLevel', 'consumeTweenByLevel',\n                'consumeSimpleProfileLevelThreshold', 'consumeDepthThreshold', 'consumeFallDuration'\n            ];\n\n            const PROFILE_DEFAULTS = {\n                holeSpeed: 5,\n                baseHoleScale: 1.0,\n                levelThresholds: [10, 20, 40, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105],\n                levelScaleMultipliers: [1.0, 2.0, 2.6],\n                levelSpeedMultipliers: [1.0, 1.3, 1.6],\n                levelCameraHeights: [19, 23, 27],\n                levelCameraTilts: [40, 45, 48],\n                consumeNearEdgeShift: 0.8,\n                consumeSimpleProfileLevelThreshold: 2,\n                consumeDepthThreshold: -3.5,\n                consumeFallDuration: 0.75\n            };\n\n            const HoleProfile = {\n                resolve(config, profileKey) {\n                    const profiles = config.holeProfiles || {};\n                    const overrides = profiles[profileKey] || {};\n                    const playerOverrides = profileKey !== 'player' ? (profiles.player || {}) : {};\n                    const result = {};\n\n                    PROFILE_KEY_LIST.forEach(key => {\n                        if (overrides[key] !== undefined) {\n                            result[key] = overrides[key];\n                        } else if (profileKey !== 'player' && playerOverrides[key] !== undefined) {\n                            result[key] = playerOverrides[key];\n                        } else if (config[key] !== undefined) {\n                            result[key] = config[key];\n                        } else if (PROFILE_DEFAULTS[key] !== undefined) {\n                            result[key] = PROFILE_DEFAULTS[key];\n                        }\n                    });\n                    return result;\n                },\n\n                getConsumeTweenParams(profile, level) {\n                    const table = profile.consumeTweenByLevel;\n                    const shiftTable = profile.consumeNearEdgeShiftByLevel;\n\n                    const defaults = {\n                        nearEdgeShift: profile.consumeNearEdgeShift !== undefined ? profile.consumeNearEdgeShift : 0.8,\n                        fallDurationMult: 1.0,\n                        depthThreshold: profile.consumeDepthThreshold !== undefined ? profile.consumeDepthThreshold : -3.5,\n                        shiftTimingPower: 2.0,\n                        attractPower: 1.8\n                    };\n\n                    const result = { ...defaults };\n\n                    if (table && typeof table === 'object') {\n                        const keys = Object.keys(table).map(Number).sort((a, b) => a - b);\n                        if (keys.length > 0) {\n                            const fields = ['nearEdgeShift', 'fallDurationMult', 'depthThreshold', 'shiftTimingPower', 'attractPower'];\n\n                            function getValueAtKey(key, field) {\n                                if (table[key] && table[key][field] !== undefined) {\n                                    return table[key][field];\n                                }\n                                return defaults[field];\n                            }\n\n                            fields.forEach(field => {\n                                if (level <= keys[0]) {\n                                    result[field] = getValueAtKey(keys[0], field);\n                                } else if (level >= keys[keys.length - 1]) {\n                                    result[field] = getValueAtKey(keys[keys.length - 1], field);\n                                } else {\n                                    for (let i = 0; i < keys.length - 1; i++) {\n                                        const low = keys[i];\n                                        const high = keys[i + 1];\n                                        if (level >= low && level <= high) {\n                                            const t = (level - low) / (high - low);\n                                            const valLow = getValueAtKey(low, field);\n                                            const valHigh = getValueAtKey(high, field);\n                                            result[field] = valLow + (valHigh - valLow) * t;\n                                            break;\n                                        }\n                                    }\n                                }\n                            });\n                            return result;\n                        }\n                    }\n\n                    if (shiftTable && typeof shiftTable === 'object') {\n                        const shiftKeys = Object.keys(shiftTable).map(Number).sort((a, b) => a - b);\n                        if (shiftKeys.length > 0) {\n                            if (level <= shiftKeys[0]) {\n                                result.nearEdgeShift = shiftTable[shiftKeys[0]];\n                            } else if (level >= shiftKeys[shiftKeys.length - 1]) {\n                                result.nearEdgeShift = shiftTable[shiftKeys[shiftKeys.length - 1]];\n                            } else {\n                                for (let i = 0; i < shiftKeys.length - 1; i++) {\n                                    const low = shiftKeys[i];\n                                    const high = shiftKeys[i + 1];\n                                    if (level >= low && level <= high) {\n                                        const t = (level - low) / (high - low);\n                                        result.nearEdgeShift = shiftTable[low] + (shiftTable[high] - shiftTable[low]) * t;\n                                        break;\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    return result;\n                }\n            };\n\n            class Hole {\n                constructor({ id, scene, profile, cameraRotationGetter, startLevel = 1, tracksPlayerVictory = false, onLevelUp = null }) {\n                    this.id = id;\n                    this.scene = scene;\n                    this.profile = profile;\n                    this.getCameraRotation = cameraRotationGetter || (() => 0);\n                    this.tracksPlayerVictory = tracksPlayerVictory;\n                    this.onLevelUp = onLevelUp;\n\n                    this.state = {\n                        x: 0,\n                        z: 0,\n                        velX: 0,\n                        velZ: 0,\n                        level: startLevel,\n                        levelScore: 0,\n                        radius: 0\n                    };\n\n                    this.group = new THREE.Group();\n                    scene.add(this.group);\n\n                    this.rimMesh = null;\n                    this.rimMat = null;\n                    this.cylMesh = null;\n                    this.bottomMesh = null;\n                    this.holeShakeTimer = 0;\n                    this.rimFlashTimer = 0;\n                    this.originalRimColor = new THREE.Color(0x3a8fff);\n                    this.flashRimColor = new THREE.Color(0xff3333);\n                    this.lightRimColor = new THREE.Color(0xd0e8ff);\n\n                    this.growthAnim = { active: false, startScale: 1, endScale: 1, elapsed: 0, duration: 0.45 };\n                    this.consumeFeedbackAnim = { active: false, elapsed: 0, duration: 0.45, scaleMult: 1.0, colorLerp: 0.0 };\n                    this.lastDepthVisualRotation = null;\n\n                    this.buildVisual();\n                    this.updateDepthVisual(true);\n\n                    const initialScale = this.getLevelScale(startLevel);\n                    this.group.scale.setScalar(initialScale);\n                    this.state.radius = HOLE_RADIUS * initialScale;\n                }\n\n                getLevelTarget(level) {\n                    const thresholds = this.profile.levelThresholds || PROFILE_DEFAULTS.levelThresholds;\n                    const idx = level - 1;\n                    if (idx < thresholds.length) return thresholds[idx];\n                    return thresholds[thresholds.length - 1] + (idx - (thresholds.length - 1)) * 5;\n                }\n\n                getLevelScale(level) {\n                    const base = this.profile.baseHoleScale || 1.0;\n                    const multipliers = this.profile.levelScaleMultipliers || PROFILE_DEFAULTS.levelScaleMultipliers;\n                    const idx = level - 1;\n                    const mult = (idx < multipliers.length) ? multipliers[idx] : multipliers[multipliers.length - 1];\n                    return base * mult;\n                }\n\n                getHoleVisualScale() {\n                    return this.group.scale.x;\n                }\n\n                getHoleOpeningWorldCenter(out) {\n                    const v = out || new THREE.Vector3();\n                    const scale = this.getHoleVisualScale();\n                    v.set(this.group.position.x, HOLE_OPENING_Y_LOCAL * scale, this.group.position.z);\n                    return v;\n                }\n\n                buildVisual() {\n                    const depth = HOLE_CYLINDER_DEPTH;\n\n                    const maskGeo = new THREE.CircleGeometry(HOLE_RADIUS, 64);\n                    const maskMat = new THREE.MeshBasicMaterial({ color: 0x08183a, depthWrite: false });\n                    const maskMesh = new THREE.Mesh(maskGeo, maskMat);\n                    maskMesh.rotation.x = -Math.PI / 2;\n                    maskMesh.position.y = HOLE_OPENING_Y_LOCAL;\n                    maskMesh.renderOrder = 3;\n                    this.group.add(maskMesh);\n\n                    const cylGeo = new THREE.CylinderGeometry(HOLE_RADIUS, HOLE_RADIUS * 0.90, depth, 64, 1, true);\n                    cylGeo.userData = { originalPositions: cylGeo.attributes.position.clone() };\n                    const cylMat = new THREE.MeshBasicMaterial({ color: 0x0d2a5e, side: THREE.BackSide });\n                    this.cylMesh = new THREE.Mesh(cylGeo, cylMat);\n                    this.cylMesh.position.y = HOLE_OPENING_Y_LOCAL - depth / 2;\n                    this.cylMesh.renderOrder = 3;\n                    this.group.add(this.cylMesh);\n\n                    const bottomGeo = new THREE.CircleGeometry(HOLE_RADIUS * 0.90, 64);\n                    const bottomMat = new THREE.MeshBasicMaterial({ color: 0x020810, side: THREE.DoubleSide });\n                    this.bottomMesh = new THREE.Mesh(bottomGeo, bottomMat);\n                    this.bottomMesh.rotation.x = -Math.PI / 2;\n                    this.bottomMesh.position.y = HOLE_OPENING_Y_LOCAL - depth;\n                    this.bottomMesh.renderOrder = 3;\n                    this.group.add(this.bottomMesh);\n\n                    const rimGeo = new THREE.RingGeometry(HOLE_RADIUS, HOLE_RADIUS * 1.18, 64);\n                    this.rimMat = new THREE.MeshBasicMaterial({ color: 0x3a8fff, side: THREE.DoubleSide });\n                    this.rimMesh = new THREE.Mesh(rimGeo, this.rimMat);\n                    this.rimMesh.rotation.x = -Math.PI / 2;\n                    this.rimMesh.position.y = HOLE_OPENING_Y_LOCAL + 0.01;\n                    this.rimMesh.renderOrder = 4;\n                    this.group.add(this.rimMesh);\n                }\n\n                updateDepthVisual(force) {\n                    if (!this.cylMesh || !this.bottomMesh) return;\n                    const rotDeg = this.getCameraRotation() || 0;\n                    if (!force && this.lastDepthVisualRotation === rotDeg) return;\n                    this.lastDepthVisualRotation = rotDeg;\n\n                    const rotRad = rotDeg * Math.PI / 180;\n                    const dx = Math.sin(rotRad);\n                    const dz = Math.cos(rotRad);\n                    const offsetDist = 0.10 * HOLE_RADIUS;\n                    const offsetX = dx * offsetDist;\n                    const offsetZ = dz * offsetDist;\n\n                    this.bottomMesh.position.x = offsetX;\n                    this.bottomMesh.position.z = offsetZ;\n\n                    const origPos = this.cylMesh.geometry.userData.originalPositions;\n                    const posAttr = this.cylMesh.geometry.attributes.position;\n                    if (origPos && posAttr) {\n                        for (let i = 0; i < posAttr.count; i++) {\n                            const x = origPos.getX(i);\n                            const y = origPos.getY(i);\n                            const z = origPos.getZ(i);\n                            if (y < 0) {\n                                posAttr.setX(i, x + offsetX);\n                                posAttr.setZ(i, z + offsetZ);\n                            } else {\n                                posAttr.setX(i, x);\n                                posAttr.setZ(i, z);\n                            }\n                        }\n                        posAttr.needsUpdate = true;\n                    }\n                }\n\n                getBaseScale() {\n                    if (this.growthAnim.active) {\n                        const t = Math.min(this.growthAnim.elapsed / this.growthAnim.duration, 1);\n                        let eased;\n                        if (t < 0.5) {\n                            eased = (t / 0.5) * 1.15;\n                        } else {\n                            const settleT = (t - 0.5) / 0.5;\n                            eased = 1.15 - 0.15 * settleT;\n                        }\n                        return this.growthAnim.startScale + (this.growthAnim.endScale - this.growthAnim.startScale) * eased;\n                    }\n                    return this.getLevelScale(this.state.level);\n                }\n\n                updateGrowthAnimation(dt) {\n                    if (!this.growthAnim.active) return;\n                    this.growthAnim.elapsed += dt;\n                    if (this.growthAnim.elapsed >= this.growthAnim.duration) {\n                        this.growthAnim.active = false;\n                    }\n                }\n\n                updateConsumeFeedbackAnimation(dt) {\n                    if (!this.consumeFeedbackAnim.active) {\n                        this.consumeFeedbackAnim.scaleMult = 1.0;\n                        this.consumeFeedbackAnim.colorLerp = 0.0;\n                        return;\n                    }\n                    this.consumeFeedbackAnim.elapsed += dt;\n                    const t = this.consumeFeedbackAnim.elapsed / this.consumeFeedbackAnim.duration;\n                    if (t >= 1) {\n                        this.consumeFeedbackAnim.active = false;\n                        this.consumeFeedbackAnim.scaleMult = 1.0;\n                        this.consumeFeedbackAnim.colorLerp = 0.0;\n                    } else {\n                        const easeBack = Math.pow(1 - t, 2);\n                        this.consumeFeedbackAnim.scaleMult = 1.0 + 0.12 * easeBack;\n                        this.consumeFeedbackAnim.colorLerp = easeBack;\n                    }\n                }\n\n                triggerRejectionFeedback() {\n                    this.holeShakeTimer = 0.12;\n                    this.rimFlashTimer = 0.12;\n                    AudioSystem.play('snd-wrong');\n                }\n\n                updateVisual(dt) {\n                    this.updateGrowthAnimation(dt);\n                    this.updateConsumeFeedbackAnimation(dt);\n\n                    const finalScale = this.getBaseScale() * this.consumeFeedbackAnim.scaleMult;\n                    this.group.scale.setScalar(finalScale);\n\n                    let shakeX = 0;\n                    let shakeZ = 0;\n                    if (this.holeShakeTimer > 0) {\n                        this.holeShakeTimer -= dt;\n                        const t = Math.max(0, this.holeShakeTimer / 0.12);\n                        const intensity = 0.25 * t;\n                        shakeX = (Math.random() - 0.5) * intensity;\n                        shakeZ = (Math.random() - 0.5) * intensity;\n                    }\n\n                    if (this.rimFlashTimer > 0 && this.rimMat) {\n                        this.rimFlashTimer -= dt;\n                        const tFlash = Math.max(0, this.rimFlashTimer / 0.12);\n                        this.rimMat.color.lerpColors(this.originalRimColor, this.flashRimColor, tFlash);\n                    } else if (this.consumeFeedbackAnim.active && this.rimMat) {\n                        this.rimMat.color.lerpColors(this.originalRimColor, this.lightRimColor, this.consumeFeedbackAnim.colorLerp);\n                    } else if (this.rimMat) {\n                        this.rimMat.color.copy(this.originalRimColor);\n                    }\n\n                    this.updateDepthVisual();\n                    this.group.position.set(this.state.x + shakeX, 0, this.state.z + shakeZ);\n                }\n\n                getConsumeRadius(factor = 0.85) {\n                    return this.state.radius * factor;\n                }\n\n                checkLevelUp(onLevelUp) {\n                    const p = this.state;\n                    const target = this.getLevelTarget(p.level);\n                    if (p.levelScore >= target) {\n                        const excess = p.levelScore - target;\n                        const nextLevel = p.level + 1;\n                        p.level++;\n                        p.levelScore = excess;\n\n                        const oldScale = this.getLevelScale(p.level - 1);\n                        const newScale = this.getLevelScale(p.level);\n                        this.growthAnim.active = true;\n                        this.growthAnim.startScale = oldScale;\n                        this.growthAnim.endScale = newScale;\n                        this.growthAnim.elapsed = 0;\n                        this.consumeFeedbackAnim.active = true;\n                        this.consumeFeedbackAnim.elapsed = 0;\n                        p.radius = HOLE_RADIUS * newScale;\n\n                        if (onLevelUp) onLevelUp(this, nextLevel);\n                        this.checkLevelUp(onLevelUp);\n                    }\n                }\n\n                addLevelScore(amount, onLevelUp) {\n                    this.state.levelScore += amount;\n                    this.checkLevelUp(onLevelUp);\n                }\n\n                resetFeedbackState() {\n                    this.holeShakeTimer = 0;\n                    this.rimFlashTimer = 0;\n                    this.consumeFeedbackAnim.active = false;\n                    this.consumeFeedbackAnim.elapsed = 0;\n                    this.consumeFeedbackAnim.scaleMult = 1.0;\n                    this.consumeFeedbackAnim.colorLerp = 0.0;\n                    this.growthAnim.active = false;\n                }\n\n                resetState(startLevel = 1) {\n                    this.state.x = 0;\n                    this.state.z = 0;\n                    this.state.velX = 0;\n                    this.state.velZ = 0;\n                    this.state.level = startLevel;\n                    this.state.levelScore = 0;\n                    const scale = this.getLevelScale(startLevel);\n                    this.group.scale.setScalar(scale);\n                    this.state.radius = HOLE_RADIUS * scale;\n                    this.resetFeedbackState();\n                }\n\n                getConsumeFallTargetY(selfOcclusion) {\n                    if (selfOcclusion) {\n                        return (HOLE_OPENING_Y_LOCAL - HOLE_CYLINDER_DEPTH) * this.getHoleVisualScale();\n                    }\n                    return null;\n                }\n            }\n\n            class PlayerHole extends Hole {\n                constructor(opts) {\n                    super(opts);\n                    this.tracksPlayerVictory = true;\n                    this.onLevelUp = opts.onLevelUp || null;\n                    this.waterRejectionCooldownUntil = 0;\n                    this.ACCELERATION = 12;\n                    this.FRICTION = 8;\n                }\n\n                updateFromInput(inputX, inputZ, dt, ctx) {\n                    const { mode, isWorldPositionTraversable, gridHalf } = ctx;\n                    const baseSpeed = this.profile.holeSpeed || 5;\n                    const speedMultipliers = this.profile.levelSpeedMultipliers || [1.0, 1.3, 1.6];\n                    const speedIdx = Math.min(this.state.level - 1, Math.max(0, speedMultipliers.length - 1));\n                    const speed = baseSpeed * speedMultipliers[speedIdx];\n                    const p = this.state;\n\n                    p.velX += (inputX * speed - p.velX) * Math.min(1, this.ACCELERATION * dt);\n                    p.velZ += (inputZ * speed - p.velZ) * Math.min(1, this.ACCELERATION * dt);\n\n                    if (inputX === 0 && inputZ === 0) {\n                        p.velX *= Math.max(0, 1 - this.FRICTION * dt);\n                        p.velZ *= Math.max(0, 1 - this.FRICTION * dt);\n                    }\n\n                    let candidateX = p.x + p.velX * dt;\n                    let candidateZ = p.z + p.velZ * dt;\n                    let blocked = false;\n\n                    if (mode === 'play') {\n                        if (!isWorldPositionTraversable(candidateX, p.z)) {\n                            p.velX = 0;\n                            blocked = true;\n                        } else {\n                            p.x = candidateX;\n                        }\n                        if (!isWorldPositionTraversable(p.x, candidateZ)) {\n                            p.velZ = 0;\n                            blocked = true;\n                        } else {\n                            p.z = candidateZ;\n                        }\n                        if (blocked) {\n                            const now = performance.now();\n                            if (now >= this.waterRejectionCooldownUntil) {\n                                this.triggerRejectionFeedback();\n                                this.waterRejectionCooldownUntil = now + 250;\n                            }\n                        }\n                    } else {\n                        p.x = candidateX;\n                        p.z = candidateZ;\n                    }\n\n                    p.x = Math.max(-gridHalf, Math.min(gridHalf, p.x));\n                    p.z = Math.max(-gridHalf, Math.min(gridHalf, p.z));\n                }\n\n                reset(startLevel) {\n                    this.waterRejectionCooldownUntil = 0;\n                    this.resetState(startLevel || 1);\n                }\n            }\n\n            class BotHole extends Hole {\n                constructor(opts) {\n                    super({ ...opts, tracksPlayerVictory: false });\n                    this.canWin = !!opts.canWin;\n                    this.originalRimColor = new THREE.Color(0xa8e8ff);\n                    this.lightRimColor = new THREE.Color(0xd8f8ff);\n                    this.flashRimColor = new THREE.Color(0xff8888);\n                    this.applyGhostMaterialStyle();\n                    this.createGhostLabel();\n                }\n\n                createGhostLabel() {\n                    const canvasEl = document.createElement('canvas');\n                    canvasEl.width = 256;\n                    canvasEl.height = 128;\n                    const ctx = canvasEl.getContext('2d');\n\n                    ctx.fillStyle = 'rgba(0,0,0,0)';\n                    ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);\n\n                    ctx.font = '900 47px \"Inter\", \"Segoe UI\", system-ui, sans-serif';\n                    ctx.textAlign = 'center';\n                    ctx.textBaseline = 'middle';\n                    ctx.fillStyle = '#a8e8ff';\n                    ctx.strokeStyle = '#000000';\n                    ctx.lineWidth = 5;\n                    ctx.strokeText('GHOST', 128, 64);\n                    ctx.fillText('GHOST', 128, 64);\n\n                    const texture = new THREE.CanvasTexture(canvasEl);\n                    texture.needsUpdate = true;\n\n                    const spriteMat = new THREE.SpriteMaterial({\n                        map: texture,\n                        transparent: true,\n                        depthTest: false\n                    });\n                    this.ghostLabelSprite = new THREE.Sprite(spriteMat);\n                    this.ghostLabelSprite.scale.set(3.12, 1.56, 1);\n                    this.scene.add(this.ghostLabelSprite);\n                }\n\n                applyGhostMaterialStyle() {\n                    const bodyOpacity = getGhostObjectOpacity();\n                    const rimOpacity = getGhostRimOpacity();\n                    if (this.rimMat) {\n                        this.rimMat.color.copy(this.originalRimColor);\n                        this.rimMat.transparent = true;\n                        this.rimMat.opacity = rimOpacity;\n                    }\n                    this.group.traverse(child => {\n                        if (!child.isMesh || !child.material) return;\n                        const mats = Array.isArray(child.material) ? child.material : [child.material];\n                        mats.forEach(m => {\n                            if (m === this.rimMat) return;\n                            m.transparent = true;\n                            m.opacity = child === this.rimMesh ? rimOpacity : bodyOpacity;\n                            if (m.color) {\n                                m.color.lerp(new THREE.Color(0xa8e8ff), 0.35);\n                            }\n                        });\n                    });\n                    QualityProfile.disableMeshShadows(this.group);\n                }\n\n                buildVisual() {\n                    super.buildVisual();\n                    this.applyGhostMaterialStyle();\n                }\n\n                updateVisual(dt) {\n                    super.updateVisual(dt);\n\n                    if (this.ghostLabelSprite) {\n                        const isVisible = this.group.visible;\n                        this.ghostLabelSprite.visible = isVisible;\n\n                        if (isVisible) {\n                            const scale = this.getHoleVisualScale();\n                            const labelScaleFactor = Math.sqrt(scale) * 1.0;\n                            this.ghostLabelSprite.scale.set(3.12 * labelScaleFactor, 1.56 * labelScaleFactor, 1);\n\n                            const offsetHeight = scale * 1.1 + 0.9;\n                            this.ghostLabelSprite.position.set(\n                                this.state.x,\n                                offsetHeight,\n                                this.state.z\n                            );\n                        }\n                    }\n                }\n\n                reset(startLevel) {\n                    this.resetState(startLevel || 1);\n                }\n\n                destroy() {\n                    if (this.ghostLabelSprite) {\n                        this.scene.remove(this.ghostLabelSprite);\n                        if (this.ghostLabelSprite.geometry) this.ghostLabelSprite.geometry.dispose();\n                        if (this.ghostLabelSprite.material) {\n                            if (this.ghostLabelSprite.material.map) this.ghostLabelSprite.material.map.dispose();\n                            this.ghostLabelSprite.material.dispose();\n                        }\n                    }\n                }\n            }\n\n            const HoleManager = {\n                holes: [],\n                playerHole: null,\n                register(hole, isPlayer) {\n                    this.holes.push(hole);\n                    if (isPlayer) this.playerHole = hole;\n                },\n                getPlayerHole() {\n                    return this.playerHole;\n                },\n                updateAllVisuals(dt) {\n                    this.holes.forEach(h => h.updateVisual(dt));\n                }\n            };\n\n            return { HoleProfile, Hole, PlayerHole, BotHole, HoleManager, HOLE_RADIUS, HOLE_OPENING_Y_LOCAL, HOLE_CYLINDER_DEPTH };\n        })();\n\n        const { HoleProfile, PlayerHole, BotHole, HoleManager, HOLE_RADIUS, HOLE_OPENING_Y_LOCAL, HOLE_CYLINDER_DEPTH } = HoleModule;\n\n        const startLevel = (config.startAtSelectedLevel && config.selectedEditLevel !== undefined) ? (parseInt(config.selectedEditLevel, 10) + 1) : 1;\n\n        const playerProfile = HoleProfile.resolve(config, 'player');\n        const playerHole = new PlayerHole({\n            id: 'player',\n            scene,\n            profile: playerProfile,\n            profileKey: 'player',\n            cameraRotationGetter: () => config.cameraRotation || 0,\n            startLevel\n        });\n        HoleManager.register(playerHole, true);\n\n        let ghostBot = null;\n\n        function getGhostSpawnOffset() {\n            return {\n                x: config.ghostBotSpawnOffsetX !== undefined ? config.ghostBotSpawnOffsetX : 14,\n                z: config.ghostBotSpawnOffsetZ !== undefined ? config.ghostBotSpawnOffsetZ : 4\n            };\n        }\n\n        function applyGhostSpawnPosition(hole) {\n            const route = resolveGhostPlaybackRoute();\n            if (shouldUsePersonalBestGhost() && route && route.samples.length > 0) {\n                const first = route.samples[0];\n                hole.state.x = first.x;\n                hole.state.z = first.z;\n                return;\n            }\n            const offset = getGhostSpawnOffset();\n            hole.state.x = playerHole.state.x + offset.x;\n            hole.state.z = playerHole.state.z + offset.z;\n        }\n\n        function getGhostType() {\n            if (config.ghostType === 'Bot' || config.ghostType === 'PersonalBest') {\n                return config.ghostType;\n            }\n            return config.usePersonalBestGhost === false ? 'Bot' : 'PersonalBest';\n        }\n\n        function resolveGhostPlaybackRoute() {\n            if (config.useDebugGhostRoute && config.debugGhostRoute) {\n                const route = UserDataManager.normalizeRoute(config.debugGhostRoute);\n                pbRoutePipelineDebug('resolveRoute', 'source=debugGhostRoute samples=' + (route ? route.samples.length : 0));\n                return route;\n            }\n            const route = UserDataManager.getPersonalBestRoute();\n            pbRoutePipelineDebug('resolveRoute', 'source=personalBestRoute samples=' + (route ? route.samples.length : 0));\n            return route;\n        }\n\n        function hasPersonalBestRoute() {\n            const route = resolveGhostPlaybackRoute();\n            return !!(route && route.samples && route.samples.length >= 2);\n        }\n\n        function shouldUsePersonalBestGhost() {\n            return getGhostType() === 'PersonalBest' && hasPersonalBestRoute();\n        }\n\n        function isBotGhostMode() {\n            return getGhostType() === 'Bot';\n        }\n\n        function ghostCanWinThisRound() {\n            return shouldUsePersonalBestGhost() || (isBotGhostMode() && !!config.botGhostCanWin);\n        }\n\n        function isSessionRoundOne() {\n            return sessionRoundNumber === 1;\n        }\n\n        /** Single gate for ghost bot, ghost UI, ghost residue, and ghost win/loss this round. */\n        function isGhostActiveThisRound() {\n            if (mode !== 'play') return false;\n            const type = getGhostType();\n            if (type === 'PersonalBest') {\n                return UserDataManager.getPersonalBest() !== null;\n            } else if (type === 'Bot') {\n                return !!config.spawnGhostInRound1;\n            }\n            return false;\n        }\n\n        function isGhostBotSpawnEnabled() {\n            return isGhostActiveThisRound();\n        }\n\n        function getActiveGhostBotState() {\n            if (shouldUsePersonalBestGhost()) return 'PB_PLAYBACK';\n            if (typeof GhostBotController !== 'undefined' && GhostBotController.getGhostBotState) {\n                return GhostBotController.getGhostBotState();\n            }\n            return 'COMPETING';\n        }\n\n        function initActiveGhostController() {\n            if (!ghostBot) return;\n            const route = resolveGhostPlaybackRoute();\n            pbRoutePipelineDebug('ghost INIT controller', 'type=' + (shouldUsePersonalBestGhost() ? 'PB_PLAYBACK' : 'BOT') + ' routeAssigned=' + !!(route && route.samples && route.samples.length));\n            summarizeRoute(route, 'ghost INIT route');\n            if (shouldUsePersonalBestGhost()) {\n                PlayerGhostBotController.init(ghostBot, route);\n            } else {\n                GhostBotController.init(ghostBot);\n            }\n        }\n\n        function updateActiveGhostController(dt, ctx) {\n            if (shouldUsePersonalBestGhost()) {\n                PlayerGhostBotController.update(dt, ctx);\n            } else {\n                GhostBotController.update(dt, ctx);\n            }\n        }\n\n        function resetActiveGhostController() {\n            if (shouldUsePersonalBestGhost()) {\n                PlayerGhostBotController.reset();\n            } else {\n                GhostBotController.reset();\n            }\n        }\n\n        function ensureGhostBotSpawned() {\n            if (!isGhostActiveThisRound()) return;\n            if (!ghostBot) spawnGhostBot();\n            initActiveGhostController();\n            pbRoutePipelineDebug('ghost ENSURE spawn', 'round=' + sessionRoundNumber + ' initCalled=true');\n        }\n\n        function spawnGhostBot() {\n            if (ghostBot) return ghostBot;\n\n            const botProfile = HoleProfile.resolve(config, 'bot');\n            ghostBot = new BotHole({\n                id: 'bot-1',\n                scene,\n                profile: botProfile,\n                cameraRotationGetter: () => config.cameraRotation || 0,\n                startLevel: 1,\n                canWin: ghostCanWinThisRound()\n            });\n            ghostBot.group.visible = isGhostActiveThisRound();\n            applyGhostSpawnPosition(ghostBot);\n\n            ghostBot.onLevelUp = function(hole) {\n                if (config.ghostBotDebugLog) {\n                    const ledger = PointManager.getHoleScore(hole.id);\n                    lib.log('[GhostBot] Level ' + hole.state.level + ' | match points: ' + ledger.totalPoints);\n                }\n                updateGhostRivalHUD();\n            };\n\n            HoleManager.register(ghostBot, false);\n            PointManager.registerHole(ghostBot);\n            if (config.ghostBotDebugLog) {\n                if (shouldUsePersonalBestGhost()) {\n                    lib.log('[PBGhost] Spawned personal best playback ghost');\n                } else {\n                    const offset = getGhostSpawnOffset();\n                    lib.log('[GhostBot] Spawned at offset (' + offset.x + ', ' + offset.z + ')');\n                }\n            }\n            return ghostBot;\n        }\n\n        function resetGhostBot() {\n            if (!ghostBot) return;\n            ghostBot.reset(1);\n            ghostBot.canWin = ghostCanWinThisRound();\n            applyGhostSpawnPosition(ghostBot);\n            resetActiveGhostController();\n            updateGhostRivalHUD();\n            ghostBot.group.visible = isGhostActiveThisRound();\n        }\n\n        function updateGhostRivalHUD() {\n            const ghostContainer = document.getElementById('ghost-progress');\n            if (!ghostContainer) return;\n\n            if (!isGhostActiveThisRound() || !ghostBot || mode !== 'play') {\n                ghostContainer.style.display = 'none';\n                return;\n            }\n\n            ghostContainer.style.display = 'flex';\n            const ledger = PointManager.getHoleScore('bot-1');\n            const pct = totalAvailablePoints > 0 ? (ledger.totalPoints / totalAvailablePoints) * 100 : 0;\n            const displayPct = pct.toFixed(1);\n            const ghostLabel = shouldUsePersonalBestGhost() ? 'Your Best' : 'Ghost Score';\n\n            renderProgressBar(ghostContainer, {\n                label: ghostLabel,\n                pct: pct,\n                color: '#a8e8ff',\n                valueText: displayPct + '%'\n            });\n\n            checkGhostVictoryDefeat();\n        }\n\n        const gameState = {\n            player: playerHole.state,\n            score: {\n                current: 0,\n                target: config.targetScore || 500\n            }\n        };\n\n        const goalScoreText = document.getElementById('goal-score-text');\n        const goalBarFill = document.getElementById('goal-bar-fill');\n\n        function handlePlayerLevelUp(hole, nextLevel) {\n            const p = hole.state;\n            const nowSecs = (performance.now() - matchStartTime) / 1000;\n            const duration = nowSecs - lastLevelReachedSeconds;\n            lastLevelReachedSeconds = nowSecs;\n\n            if (mode === 'play') {\n                SessionMetrics.recordSizeMilestone(p.level, { x: p.x, z: p.z });\n                if (typeof window.testMetricsAtLevel2 === 'function') {\n                    window.testMetricsAtLevel2();\n                }\n            } else {\n                Logger.log(`Map ${formatMMSS(nowSecs)} | Reached Level ${nextLevel} in ${formatMMSS(duration)}`);\n            }\n\n            AudioSystem.play('size-increase');\n            lib.log('Level Up! Now level ' + p.level);\n        }\n\n        function checkConqueredVictory() {\n            const pct = totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0;\n            if (pct >= getVictoryCompletionPercent()) {\n                const instances = config.objectInstances || {};\n                let maxLevelRemaining = 0;\n                let hasEdibleObjects = false;\n\n                Object.keys(instances).forEach(id => {\n                    const inst = instances[id];\n                    normalizeConsumeState(inst);\n                    if (inst && !inst.consumedByPlayer) {\n                        const entry = getRegistryEntry(inst.registryId);\n                        if (entry) {\n                            hasEdibleObjects = true;\n                            const reqLevel = entry.requiredHoleLevel || 0;\n                            if (reqLevel > maxLevelRemaining) {\n                                maxLevelRemaining = reqLevel;\n                            }\n                        }\n                    }\n                });\n\n                if (!hasEdibleObjects || maxLevelRemaining <= 3) {\n                    lib.log(\"Conquered victory triggered! Remaining max required hole level: \" + maxLevelRemaining);\n                    triggerVictoryScreen();\n                }\n            }\n        }\n\n        function renderProgressBar(container, { label, pct, color, valueText }) {\n            if (!container) return;\n            const fill = container.querySelector('.progress-bar-fill');\n            const valText = container.querySelector('.progress-value-text');\n            const labelEl = container.querySelector('.progress-label');\n\n            if (labelEl) labelEl.textContent = label;\n            if (valText) valText.textContent = valueText !== undefined ? valueText : (pct.toFixed(1) + '%');\n            if (fill) {\n                fill.style.width = Math.min(100, Math.max(0, pct)) + '%';\n                if (color) {\n                    fill.style.backgroundColor = color;\n                }\n            }\n        }\n\n        function updateGoalHUD() {\n            const pct = totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0;\n            const displayPct = Math.min(100, pct).toFixed(1);\n            \n            const playerContainer = document.getElementById('player-progress');\n            if (playerContainer) {\n                renderProgressBar(playerContainer, {\n                    label: 'Your Score',\n                    pct: pct,\n                    color: '#3a8fff',\n                    valueText: displayPct + '%'\n                });\n            }\n\n            if (pct >= getVictoryCompletionPercent()) {\n                triggerVictoryScreen();\n            } else {\n                checkConqueredVictory();\n            }\n        }\n\n\n        const levelHudText = document.getElementById('level-hud-text');\n        const levelHudBarFill = document.getElementById('level-hud-bar-fill');\n\n        function updateLevelHUD() {\n            const p = playerHole.state;\n            const target = playerHole.getLevelTarget(p.level);\n            levelHudText.textContent = 'Size ' + p.level;\n            const pct = Math.min(100, (p.levelScore / target) * 100);\n            levelHudBarFill.style.width = pct + '%';\n\n            if (levelHud) {\n                if (p.level >= 2) {\n                    levelHud.style.top = 'calc(58% + 80px)';\n                } else {\n                    levelHud.style.top = '58%';\n                }\n            }\n        }\n\n        function updateGhostDebugUI() {\n            const panel = document.getElementById('ghost-debug-panel');\n            if (!panel) return;\n\n            if (mode === 'play' && config.enableGhostDebug && ghostBot && isGhostActiveThisRound()) {\n                panel.style.display = 'block';\n                \n                const stateEl = document.getElementById('gdb-state');\n                const scoreEl = document.getElementById('gdb-score');\n                const playerEl = document.getElementById('gdb-player');\n                const targetEl = document.getElementById('gdb-target');\n\n                const gState = getActiveGhostBotState();\n                if (stateEl) {\n                    stateEl.textContent = gState;\n                    if (gState === 'ROAMING') {\n                        stateEl.style.color = '#ff9800';\n                    } else if (gState === 'PB_PLAYBACK') {\n                        stateEl.style.color = '#00e5ff';\n                    } else {\n                        stateEl.style.color = '#00e5ff';\n                    }\n                }\n\n                const ledger = PointManager.getHoleScore('bot-1');\n                const ghostPct = totalAvailablePoints > 0 ? (ledger.totalPoints / totalAvailablePoints) * 100 : 0;\n                const playerPct = totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0;\n\n                if (scoreEl) scoreEl.textContent = ghostPct.toFixed(1) + '%';\n                if (playerEl) playerEl.textContent = playerPct.toFixed(1) + '%';\n                \n                if (targetEl) {\n                    const targetId = GhostBotController.getTarget();\n                    if (targetId) {\n                        const inst = config.objectInstances[targetId];\n                        targetEl.textContent = targetId + ' (' + (inst ? inst.registryId : 'unknown') + ')';\n                    } else {\n                        targetEl.textContent = 'None / Roaming Spot';\n                    }\n                }\n            } else {\n                panel.style.display = 'none';\n            }\n        }\n\n        function updateLevelBarPosition() {\n            // No longer dynamically positions relative to the 3D hole to prevent distracting floating/bobbing\n        }\n\n        updateLevelHUD();\n\n        playerHole.onLevelUp = handlePlayerLevelUp;\n\n        window.EatTheWorldDebug = window.EatTheWorldDebug || {};\n        window.EatTheWorldDebug.getBotScore = function() {\n            return ghostBot ? PointManager.getHoleScore('bot-1') : null;\n        };\n        window.EatTheWorldDebug.getGhostTarget = function() {\n            if (shouldUsePersonalBestGhost()) return null;\n            return typeof GhostBotController !== 'undefined' ? GhostBotController.getTarget() : null;\n        };\n        window.EatTheWorldDebug.getPersonalBestRoute = function() {\n            return UserDataManager.getPersonalBestRoute();\n        };\n        window.EatTheWorldDebug.getGhostPlaybackRoute = function() {\n            return resolveGhostPlaybackRoute();\n        };\n        window.EatTheWorldDebug.compareGhostRoutes = function() {\n            const debugRoute = config.debugGhostRoute ? UserDataManager.normalizeRoute(config.debugGhostRoute) : null;\n            const pbRoute = UserDataManager.getPersonalBestRoute();\n            compareRouteStructures(debugRoute, pbRoute);\n            return { debugRoute, pbRoute, playbackRoute: resolveGhostPlaybackRoute(), controllerRoute: PlayerGhostBotController.getRoute() };\n        };\n        window.EatTheWorldDebug.getPlaybackControllerRoute = function() {\n            return PlayerGhostBotController.getRoute();\n        };\n        window.EatTheWorldDebug.isGhostActiveThisRound = function() {\n            return isGhostActiveThisRound();\n        };\n        window.EatTheWorldDebug.copyPersonalBestRouteToClipboard = async function() {\n            const route = UserDataManager.getPersonalBestRoute();\n            if (!route) {\n                console.warn('[EatTheWorldDebug] No saved PB route in user data (check lib.getUserGameState / saveUserGameState)');\n                return false;\n            }\n            const json = JSON.stringify(route, null, 2);\n            try {\n                await navigator.clipboard.writeText(json);\n                console.log('[EatTheWorldDebug] PB route copied (' + route.samples.length + ' samples). Paste into config.json as debugGhostRoute');\n                return true;\n            } catch (e) {\n                console.log('[EatTheWorldDebug] Clipboard failed \u2014 copy from console:\\n' + json);\n                return false;\n            }\n        };\n\n        // ============================================================\n        // POINT MANAGER \u2014 per-hole match points and player victory\n        // ============================================================\n        const PointManager = (() => {\n            const scoresByHoleId = {};\n\n            function registerHole(hole) {\n                scoresByHoleId[hole.id] = { totalPoints: 0, consumeCount: 0 };\n            }\n\n            function getPointsForObject(instanceId) {\n                const instances = config.objectInstances || {};\n                const obj = instances[instanceId];\n                if (obj && obj.registryId) {\n                    const entry = getRegistryEntry(obj.registryId);\n                    if (entry && entry.pointValue) return entry.pointValue;\n                }\n                return 10;\n            }\n\n            function awardConsume(hole, instanceId) {\n                const pts = getPointsForObject(instanceId);\n                hole.addLevelScore(pts, hole.onLevelUp || null);\n\n                if (!scoresByHoleId[hole.id]) {\n                    registerHole(hole);\n                }\n                const ledger = scoresByHoleId[hole.id];\n                ledger.totalPoints += pts;\n                ledger.consumeCount += 1;\n\n                if (hole.tracksPlayerVictory) {\n                    playerConsumedPoints += pts;\n                    gameState.score.current = playerConsumedPoints;\n                    if (mode === 'play') {\n                        SessionMetrics.recordConsume(instanceId, pts);\n                    }\n                    updateLevelHUD();\n                    updateGoalHUD();\n                    lib.log('Score: ' + playerConsumedPoints + ' / ' + totalAvailablePoints);\n                } else if (config.ghostBotDebugLog) {\n                    lib.log('[PointManager] ' + hole.id + ' +' + pts + ' pts (total: ' + ledger.totalPoints + ')');\n                }\n                if (!hole.tracksPlayerVictory) {\n                    updateGhostRivalHUD();\n                }\n\n                return pts;\n            }\n\n            function getHoleScore(holeId) {\n                return scoresByHoleId[holeId] || { totalPoints: 0, consumeCount: 0 };\n            }\n\n            function getPlayerConsumedPoints() {\n                return playerConsumedPoints;\n            }\n\n            function reset(holeId) {\n                if (holeId) {\n                    scoresByHoleId[holeId] = { totalPoints: 0, consumeCount: 0 };\n                    if (holeId === 'player') {\n                        playerConsumedPoints = 0;\n                        gameState.score.current = 0;\n                    }\n                } else {\n                    Object.keys(scoresByHoleId).forEach(id => {\n                        scoresByHoleId[id] = { totalPoints: 0, consumeCount: 0 };\n                    });\n                    playerConsumedPoints = 0;\n                    gameState.score.current = 0;\n                }\n            }\n\n            return {\n                registerHole,\n                getPointsForObject,\n                awardConsume,\n                getHoleScore,\n                getPlayerConsumedPoints,\n                reset\n            };\n        })();\n\n        PointManager.registerHole(playerHole);\n\n        const GhostBotController = (() => {\n            let bot = null;\n            let targetInstanceId = null;\n            let roamingPosition = null;\n            let ghostBotState = 'COMPETING'; // 'COMPETING' or 'ROAMING'\n            let retargetCooldown = 0;\n            let thinkDelay = 0;\n\n            function ghostCfg(key, defaultVal) {\n                return config[key] !== undefined ? config[key] : defaultVal;\n            }\n\n            function distSq(x1, z1, x2, z2) {\n                const dx = x1 - x2;\n                const dz = z1 - z2;\n                return dx * dx + dz * dz;\n            }\n\n            function init(ghostBotHole) {\n                bot = ghostBotHole;\n                targetInstanceId = null;\n                roamingPosition = null;\n                ghostBotState = 'COMPETING';\n                retargetCooldown = 0;\n                thinkDelay = ghostCfg('ghostBotThinkDelay', 0.15);\n            }\n\n            function reset() {\n                targetInstanceId = null;\n                roamingPosition = null;\n                ghostBotState = 'COMPETING';\n                retargetCooldown = 0;\n                thinkDelay = ghostCfg('ghostBotThinkDelay', 0.15);\n            }\n\n            function isTargetValid(instanceId, worldObjects) {\n                if (!instanceId) return false;\n                const inst = (config.objectInstances || {})[instanceId];\n                if (!inst) return false;\n                return ConsumeSystem.canConsume(bot, instanceId);\n            }\n\n            function findBestTarget(worldObjects, playerHole) {\n                const searchRadius = ghostCfg('ghostBotSearchRadius', 50);\n                const searchRadiusSq = searchRadius * searchRadius;\n                const playerBuffer = ghostCfg('ghostBotPlayerObjectBuffer', 8);\n                const playerBufferSq = playerBuffer * playerBuffer;\n                const minPlayerDist = ghostCfg('ghostBotMinPlayerDistance', 10);\n                const minPlayerDistSq = minPlayerDist * minPlayerDist;\n\n                const px = playerHole.state.x;\n                const pz = playerHole.state.z;\n                const bx = bot.state.x;\n                const bz = bot.state.z;\n\n                let bestId = null;\n                let bestScore = Infinity;\n                const instances = config.objectInstances || {};\n                const ids = Object.keys(instances);\n\n                for (let i = 0; i < ids.length; i++) {\n                    const instanceId = ids[i];\n                    if (!ConsumeSystem.canConsume(bot, instanceId)) continue;\n\n                    const pos = getInstanceTargetPosition(instanceId, worldObjects);\n                    const ox = pos.x;\n                    const oz = pos.z;\n\n                    if (distSq(ox, oz, px, pz) < minPlayerDistSq) continue;\n\n                    const distBotSq = distSq(ox, oz, bx, bz);\n                    if (distBotSq > searchRadiusSq) continue;\n\n                    let score = distBotSq;\n                    if (distSq(ox, oz, px, pz) < playerBufferSq) {\n                        score += playerBufferSq * 2;\n                    }\n\n                    if (score < bestScore) {\n                        bestScore = score;\n                        bestId = instanceId;\n                    }\n                }\n                return bestId;\n            }\n\n            function findBestRoamingTarget(worldObjects, playerHole) {\n                const searchRadius = ghostCfg('ghostBotSearchRadius', 50);\n                const searchRadiusSq = searchRadius * searchRadius;\n                const bx = bot.state.x;\n                const bz = bot.state.z;\n\n                const instances = config.objectInstances || {};\n                const ids = Object.keys(instances);\n\n                const ineligibleIds = [];\n                const otherIds = [];\n\n                for (let i = 0; i < ids.length; i++) {\n                    const id = ids[i];\n                    const inst = instances[id];\n                    if (!inst) continue;\n                    if (isConsumedBy(bot, inst)) continue;\n\n                    const reg = getRegistryEntry(inst.registryId);\n                    const reqLevel = (reg && reg.requiredHoleLevel) || 1;\n\n                    const pos = getInstanceTargetPosition(id, worldObjects);\n                    const distSqVal = distSq(pos.x, pos.z, bx, bz);\n                    if (distSqVal > searchRadiusSq) continue;\n\n                    if (reqLevel > bot.state.level) {\n                        ineligibleIds.push({ id, distSq: distSqVal });\n                    } else {\n                        otherIds.push({ id, distSq: distSqVal });\n                    }\n                }\n\n                if (ineligibleIds.length > 0) {\n                    ineligibleIds.sort((a, b) => a.distSq - b.distSq);\n                    const idx = Math.floor(Math.random() * Math.min(3, ineligibleIds.length));\n                    return { targetInstanceId: ineligibleIds[idx].id, roamingPosition: null };\n                }\n\n                if (otherIds.length > 0) {\n                    otherIds.sort((a, b) => a.distSq - b.distSq);\n                    const idx = Math.floor(Math.random() * Math.min(5, otherIds.length));\n                    return { targetInstanceId: otherIds[idx].id, roamingPosition: null };\n                }\n\n                const angle = Math.random() * Math.PI * 2;\n                const dist = 10 + Math.random() * 20;\n                const targetX = bx + Math.cos(angle) * dist;\n                const targetZ = bz + Math.sin(angle) * dist;\n                return { targetInstanceId: null, roamingPosition: { x: targetX, z: targetZ } };\n            }\n\n            function updateGhostPacingState(playerHole) {\n                if (!bot) return;\n\n                const ledger = PointManager.getHoleScore(bot.id);\n                const ghostPct = totalAvailablePoints > 0 ? (ledger.totalPoints / totalAvailablePoints) * 100 : 0;\n                const playerPct = totalAvailablePoints > 0 ? (playerConsumedPoints / totalAvailablePoints) * 100 : 0;\n\n                const threshold = config.ghostPacingThreshold !== undefined ? config.ghostPacingThreshold : 5.0;\n\n                if (ghostBotState === 'COMPETING') {\n                    if (ghostPct - playerPct > threshold) {\n                        ghostBotState = 'ROAMING';\n                        targetInstanceId = null;\n                        roamingPosition = null;\n                        retargetCooldown = 0;\n                        lib.log('[GhostPacing] Ghost transitioned to ROAMING mode (Ghost: ' + ghostPct.toFixed(1) + '%, Player: ' + playerPct.toFixed(1) + '%)');\n                    }\n                } else if (ghostBotState === 'ROAMING') {\n                    if (ghostPct - playerPct <= threshold) {\n                        ghostBotState = 'COMPETING';\n                        targetInstanceId = null;\n                        roamingPosition = null;\n                        retargetCooldown = 0;\n                        lib.log('[GhostPacing] Ghost transitioned to COMPETING mode (Ghost: ' + ghostPct.toFixed(1) + '%, Player: ' + playerPct.toFixed(1) + '%)');\n                    }\n                }\n            }\n\n            function applyPlayerRepulsion(playerHole, dt) {\n                const minDist = ghostCfg('ghostBotMinPlayerDistance', 10);\n                const dx = bot.state.x - playerHole.state.x;\n                const dz = bot.state.z - playerHole.state.z;\n                const dist = Math.sqrt(dx * dx + dz * dz);\n                if (dist >= minDist || dist < 0.001) return false;\n\n                const pushSpeed = ghostCfg('ghostBotRepulsionSpeed', 8);\n                const nx = dx / dist;\n                const nz = dz / dist;\n                const push = pushSpeed * dt;\n                bot.state.x += nx * push;\n                bot.state.z += nz * push;\n                return true;\n            }\n\n            function moveToward(targetX, targetZ, dt, ctx, speedScale) {\n                const { isWorldPositionTraversable, gridHalf } = ctx;\n                const dx = targetX - bot.state.x;\n                const dz = targetZ - bot.state.z;\n                const dist = Math.sqrt(dx * dx + dz * dz);\n                if (dist < 0.05) return;\n\n                const arriveRadius = bot.getConsumeRadius() * 0.95;\n                if (dist <= arriveRadius) return;\n\n                const baseSpeed = bot.profile.holeSpeed || 5;\n                const speedMultipliers = bot.profile.levelSpeedMultipliers || [1.0, 1.3, 1.6];\n                const speedIdx = Math.min(bot.state.level - 1, Math.max(0, speedMultipliers.length - 1));\n                const speedMult = ghostCfg('ghostBotSpeedMultiplier', 0.75);\n                const speed = baseSpeed * speedMultipliers[speedIdx] * speedMult * (speedScale || 1);\n\n                const nx = dx / dist;\n                const nz = dz / dist;\n                const moveAmount = Math.min(speed * dt, Math.max(0, dist - arriveRadius));\n\n                const candidateX = bot.state.x + nx * moveAmount;\n                const candidateZ = bot.state.z + nz * moveAmount;\n\n                if (isWorldPositionTraversable(candidateX, bot.state.z)) {\n                    bot.state.x = candidateX;\n                }\n                if (isWorldPositionTraversable(bot.state.x, candidateZ)) {\n                    bot.state.z = candidateZ;\n                }\n\n                bot.state.x = Math.max(-gridHalf, Math.min(gridHalf, bot.state.x));\n                bot.state.z = Math.max(-gridHalf, Math.min(gridHalf, bot.state.z));\n            }\n\n            function applyLeashPull(playerHole, dt) {\n                const maxDist = ghostCfg('ghostBotMaxPlayerDistance', 80);\n                const dx = playerHole.state.x - bot.state.x;\n                const dz = playerHole.state.z - bot.state.z;\n                const dist = Math.sqrt(dx * dx + dz * dz);\n                if (dist <= maxDist) return;\n\n                const pullSpeed = ghostCfg('ghostBotLeashPullSpeed', 4);\n                const nx = dx / dist;\n                const nz = dz / dist;\n                const pull = Math.min(pullSpeed * dt, dist - maxDist);\n                bot.state.x += nx * pull;\n                bot.state.z += nz * pull;\n            }\n\n            function update(dt, ctx) {\n                if (!bot) return;\n                const { playerHole, worldObjects } = ctx;\n\n                updateGhostPacingState(playerHole);\n\n                if (thinkDelay > 0) {\n                    thinkDelay -= dt;\n                    return;\n                }\n\n                retargetCooldown -= dt;\n\n                let hasValidTarget = false;\n                if (ghostBotState === 'ROAMING') {\n                    if (targetInstanceId) {\n                        const inst = (config.objectInstances || {})[targetInstanceId];\n                        if (inst && !isConsumedBy(bot, inst)) {\n                            hasValidTarget = true;\n                        }\n                    } else if (roamingPosition) {\n                        hasValidTarget = true;\n                    }\n                } else {\n                    hasValidTarget = isTargetValid(targetInstanceId, worldObjects);\n                }\n\n                if (retargetCooldown <= 0 || !hasValidTarget) {\n                    if (ghostBotState === 'ROAMING') {\n                        const roam = findBestRoamingTarget(worldObjects, playerHole);\n                        targetInstanceId = roam.targetInstanceId;\n                        roamingPosition = roam.roamingPosition;\n                        retargetCooldown = ghostCfg('ghostBotRetargetInterval', 0.8) * 1.5;\n                    } else {\n                        targetInstanceId = findBestTarget(worldObjects, playerHole);\n                        roamingPosition = null;\n                        retargetCooldown = ghostCfg('ghostBotRetargetInterval', 0.8);\n                    }\n                }\n\n                if (ghostBotState === 'ROAMING') {\n                    let distanceToDest = Infinity;\n                    if (targetInstanceId) {\n                        const pos = getInstanceTargetPosition(targetInstanceId, worldObjects);\n                        distanceToDest = Math.sqrt(distSq(pos.x, pos.z, bot.state.x, bot.state.z));\n                    } else if (roamingPosition) {\n                        distanceToDest = Math.sqrt(distSq(roamingPosition.x, roamingPosition.z, bot.state.x, bot.state.z));\n                    }\n\n                    const arriveRadius = bot.getConsumeRadius() * 1.2;\n                    if (distanceToDest <= Math.max(1.5, arriveRadius)) {\n                        targetInstanceId = null;\n                        roamingPosition = null;\n                        retargetCooldown = 0;\n                    }\n                }\n\n                if (applyPlayerRepulsion(playerHole, dt)) {\n                    if (ghostBotState === 'ROAMING') {\n                        targetInstanceId = null;\n                        roamingPosition = null;\n                        retargetCooldown = 0;\n                    } else {\n                        targetInstanceId = findBestTarget(worldObjects, playerHole);\n                    }\n                }\n\n                applyLeashPull(playerHole, dt);\n\n                if (ghostBotState === 'ROAMING') {\n                    if (targetInstanceId) {\n                        const pos = getInstanceTargetPosition(targetInstanceId, worldObjects);\n                        moveToward(pos.x, pos.z, dt, ctx, 0.85);\n                    } else if (roamingPosition) {\n                        moveToward(roamingPosition.x, roamingPosition.z, dt, ctx, 0.85);\n                    }\n                } else {\n                    if (targetInstanceId && isTargetValid(targetInstanceId, worldObjects)) {\n                        const pos = getInstanceTargetPosition(targetInstanceId, worldObjects);\n                        moveToward(pos.x, pos.z, dt, ctx, 1);\n                    } else {\n                        const dx = playerHole.state.x - bot.state.x;\n                        const dz = playerHole.state.z - bot.state.z;\n                        const dist = Math.sqrt(dx * dx + dz * dz);\n                        const maxDist = ghostCfg('ghostBotMaxPlayerDistance', 80);\n                        if (dist > maxDist * 0.55) {\n                            const anchorX = playerHole.state.x + dx * 0.35;\n                            const anchorZ = playerHole.state.z + dz * 0.35;\n                            moveToward(anchorX, anchorZ, dt, ctx, 0.5);\n                        }\n                    }\n                }\n\n                applyPlayerRepulsion(playerHole, dt);\n            }\n\n            function getTarget() {\n                return targetInstanceId;\n            }\n\n            return {\n                init,\n                update,\n                reset,\n                getTarget,\n                getBot: () => bot,\n                getGhostBotState: () => ghostBotState\n            };\n        })();\n\n        const PlayerGhostBotController = (() => {\n            let bot = null;\n            let route = null;\n            let lastPlaybackLogSec = -1;\n\n            function init(ghostBotHole, savedRoute) {\n                bot = ghostBotHole;\n                route = savedRoute;\n                lastPlaybackLogSec = -1;\n                const sampleCount = route && route.samples ? route.samples.length : 0;\n                pbRoutePipelineDebug('playback INIT', 'route=' + (route ? 'object' : 'null') + ' samples=' + sampleCount);\n                summarizeRoute(route, 'playback INIT route');\n                if (!route || !route.samples || route.samples.length < 2) {\n                    pbRoutePipelineDebug('playback INIT', 'FAIL \u2014 controller has no usable route');\n                }\n            }\n\n            function reset() {\n                if (!bot || !route || !route.samples || !route.samples.length) return;\n                const first = route.samples[0];\n                bot.state.x = first.x;\n                bot.state.z = first.z;\n            }\n\n            function getPositionAtTime(elapsedSec) {\n                const samples = route.samples;\n                if (!samples.length) return { x: bot.state.x, z: bot.state.z };\n                if (elapsedSec <= samples[0].t) return { x: samples[0].x, z: samples[0].z };\n\n                const last = samples[samples.length - 1];\n                if (elapsedSec >= last.t) return { x: last.x, z: last.z };\n\n                for (let i = 0; i < samples.length - 1; i++) {\n                    const a = samples[i];\n                    const b = samples[i + 1];\n                    if (elapsedSec >= a.t && elapsedSec <= b.t) {\n                        const span = b.t - a.t;\n                        const u = span > 0 ? (elapsedSec - a.t) / span : 0;\n                        return {\n                            x: a.x + (b.x - a.x) * u,\n                            z: a.z + (b.z - a.z) * u\n                        };\n                    }\n                }\n                return { x: last.x, z: last.z };\n            }\n\n            function update(dt, ctx) {\n                if (!bot || !route || !route.samples || !route.samples.length) {\n                    if (config.pbRoutePipelineDebug && (!route || !route.samples || !route.samples.length)) {\n                        pbRoutePipelineDebug('playback UPDATE', 'SKIP \u2014 route unusable (route=' + !!route + ' samples=' + (route && route.samples ? route.samples.length : 0) + ')');\n                    }\n                    return;\n                }\n                const elapsed = (performance.now() - matchStartTime) / 1000;\n                const pos = getPositionAtTime(elapsed);\n                const gridHalf = ctx.gridHalf || 50;\n                bot.state.x = Math.max(-gridHalf, Math.min(gridHalf, pos.x));\n                bot.state.z = Math.max(-gridHalf, Math.min(gridHalf, pos.z));\n                const logBucket = Math.floor(elapsed);\n                if (logBucket !== lastPlaybackLogSec && logBucket % 5 === 0) {\n                    lastPlaybackLogSec = logBucket;\n                    pbRoutePipelineDebug('playback UPDATE', 'elapsed=' + elapsed.toFixed(1) + 's pos=' + bot.state.x.toFixed(2) + ',' + bot.state.z.toFixed(2) + ' routeSamples=' + route.samples.length + ' routeEndT=' + route.samples[route.samples.length - 1].t);\n                }\n            }\n\n            return { init, update, reset, getRoute: () => route };\n        })();\n\n        if (mode === 'play' && isGhostActiveThisRound()) {\n            pbRoutePipelineDebug('startup', 'initial ghost spawn round=' + sessionRoundNumber);\n            spawnGhostBot();\n            initActiveGhostController();\n            updateGhostRivalHUD();\n        }\n\n        function spawnManifestRing(x, z, size) {\n            const ringGeo = new THREE.RingGeometry(0.1, 1.2, 32);\n            const ringMat = new THREE.MeshBasicMaterial({\n                color: 0x00e5ff, // Cyan matching the ghost color palette\n                side: THREE.DoubleSide,\n                transparent: true,\n                opacity: 0.8,\n                depthWrite: false\n            });\n            const ringMesh = new THREE.Mesh(ringGeo, ringMat);\n            ringMesh.rotation.x = -Math.PI / 2;\n            ringMesh.position.set(x, 0.12, z); // slightly above ground to prevent z-fighting\n            ringMesh.scale.setScalar(0.1);\n            scene.add(ringMesh);\n\n            manifestRings.push({\n                mesh: ringMesh,\n                elapsed: 0,\n                duration: 0.5,\n                maxScale: size * 1.5\n            });\n        }\n\n        function updateManifestRings(dt) {\n            for (let i = manifestRings.length - 1; i >= 0; i--) {\n                const ring = manifestRings[i];\n                ring.elapsed += dt;\n                const t = Math.min(ring.elapsed / ring.duration, 1);\n                \n                const currentScale = 0.1 + (ring.maxScale - 0.1) * t;\n                ring.mesh.scale.setScalar(currentScale);\n\n                if (ring.mesh.material) {\n                    ring.mesh.material.opacity = 0.8 * (1 - t);\n                }\n\n                if (t >= 1) {\n                    scene.remove(ring.mesh);\n                    ring.mesh.geometry.dispose();\n                    ring.mesh.material.dispose();\n                    manifestRings.splice(i, 1);\n                }\n            }\n        }\n\n        function updateManifestAnimations(dt) {\n            for (let i = manifestAnimations.length - 1; i >= 0; i--) {\n                const anim = manifestAnimations[i];\n                anim.elapsed += dt;\n                const t = Math.min(anim.elapsed / anim.duration, 1);\n\n                anim.mesh.traverse(child => {\n                    if (child.isMesh && child.material) {\n                        const mats = Array.isArray(child.material) ? child.material : [child.material];\n                        mats.forEach(m => {\n                            m.transparent = true;\n                            m.opacity = anim.normalOpacity * t;\n                        });\n                    }\n                });\n\n                if (t >= 1) {\n                    manifestAnimations.splice(i, 1);\n                }\n            }\n        }\n\n        const floatingTexts = [];\n\n        function clearFloatingTexts() {\n            for (let i = floatingTexts.length - 1; i >= 0; i--) {\n                floatingTexts[i].div.remove();\n                floatingTexts.splice(i, 1);\n            }\n        }\n\n        function spawnFloatingText(worldX, worldZ, points) {\n            const div = document.createElement('div');\n            div.textContent = '+' + points;\n            div.style.cssText = 'position:absolute;pointer-events:none;font-family:Inter,sans-serif;font-weight:bold;font-size:38px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,0.7);transition:none;';\n            gameWorld.appendChild(div);\n            floatingTexts.push({ div, elapsed: 0, duration: 1.0, startY: 0.3 });\n        }\n\n        function updateFloatingTexts(dt) {\n            for (let i = floatingTexts.length - 1; i >= 0; i--) {\n                const ft = floatingTexts[i];\n                ft.elapsed += dt;\n                const t = ft.elapsed / ft.duration;\n                if (t >= 1) {\n                    ft.div.remove();\n                    floatingTexts.splice(i, 1);\n                    continue;\n                }\n                const worldY = ft.startY + ft.elapsed * 2.5;\n                const vec = new THREE.Vector3(playerHole.state.x, worldY, playerHole.state.z);\n                vec.project(camera);\n                const sx = (vec.x * 0.5 + 0.5) * gameWorld.clientWidth;\n                const sy = (-vec.y * 0.5 + 0.5) * gameWorld.clientHeight;\n                ft.div.style.left = sx + 'px';\n                ft.div.style.top = sy + 'px';\n                ft.div.style.opacity = 1 - t;\n                ft.div.style.transform = 'translate(-50%,-50%)';\n            }\n        }\n\n\n        const GhostPresence = (() => {\n            const _proj = new THREE.Vector3();\n            const SCREEN_INSET = 52;\n\n            function isGhostOnScreen() {\n                if (!ghostBot || !isGhostActiveThisRound()) return false;\n                const w = gameWorld.clientWidth;\n                const h = gameWorld.clientHeight;\n                if (w <= 0 || h <= 0) return false;\n\n                _proj.set(ghostBot.state.x, 0.4, ghostBot.state.z);\n                _proj.project(camera);\n\n                const sx = (_proj.x * 0.5 + 0.5) * w;\n                const sy = (-_proj.y * 0.5 + 0.5) * h;\n                const inset = SCREEN_INSET;\n                return _proj.z >= -1 && _proj.z <= 1 &&\n                    sx >= inset && sx <= w - inset &&\n                    sy >= inset && sy <= h - inset;\n            }\n\n            function updateGhostVisibility() {\n                if (!ghostBot || !isGhostActiveThisRound()) return;\n                const onScreen = isGhostOnScreen();\n                ghostBot.group.visible = onScreen;\n                if (ghostBot.ghostLabelSprite) {\n                    ghostBot.ghostLabelSprite.visible = onScreen;\n                }\n            }\n\n            return { isGhostOnScreen, updateGhostVisibility };\n        })();\n\n        const GhostOffscreenIndicator = (() => {\n            const _ghostProj = new THREE.Vector3();\n            const EDGE_PADDING = 64;\n            const SCREEN_INSET = 52;\n\n            let el = null;\n            let innerEl = null;\n            let labelEl = null;\n\n            function hide() {\n                if (!el) return;\n                el.classList.remove('visible');\n                el.setAttribute('aria-hidden', 'true');\n            }\n\n            function ensureElements() {\n                if (el) return;\n                el = document.getElementById('ghost-offscreen-indicator');\n                innerEl = el ? el.querySelector('.ghost-offscreen-indicator-inner') : null;\n                labelEl = el ? el.querySelector('.ghost-offscreen-label') : null;\n            }\n\n            function clampToScreenEdge(cx, cy, dx, dy, minX, minY, maxX, maxY) {\n                const tCandidates = [];\n                if (Math.abs(dx) > 1e-6) {\n                    tCandidates.push((minX - cx) / dx, (maxX - cx) / dx);\n                }\n                if (Math.abs(dy) > 1e-6) {\n                    tCandidates.push((minY - cy) / dy, (maxY - cy) / dy);\n                }\n                let bestT = Infinity;\n                for (let i = 0; i < tCandidates.length; i++) {\n                    const t = tCandidates[i];\n                    if (t <= 0) continue;\n                    const px = cx + dx * t;\n                    const py = cy + dy * t;\n                    if (px >= minX - 1 && px <= maxX + 1 && py >= minY - 1 && py <= maxY + 1 && t < bestT) {\n                        bestT = t;\n                    }\n                }\n                if (!isFinite(bestT)) return { x: cx, y: cy };\n                return { x: cx + dx * bestT, y: cy + dy * bestT };\n            }\n\n            function update() {\n                ensureElements();\n                if (!el || !innerEl || !labelEl) return;\n\n                if (mode !== 'play' ||\n                    GameStateManager.getState() !== GAME_STATE_PLAYING ||\n                    isVictoryActive ||\n                    isDefeatActive ||\n                    !isGhostActiveThisRound() ||\n                    !ghostBot) {\n                    hide();\n                    return;\n                }\n\n                const w = gameWorld.clientWidth;\n                const h = gameWorld.clientHeight;\n                if (w <= 0 || h <= 0) {\n                    hide();\n                    return;\n                }\n\n                _ghostProj.set(ghostBot.state.x, 0.4, ghostBot.state.z);\n                _ghostProj.project(camera);\n\n                const sx = (_ghostProj.x * 0.5 + 0.5) * w;\n                const sy = (-_ghostProj.y * 0.5 + 0.5) * h;\n                if (GhostPresence.isGhostOnScreen()) {\n                    hide();\n                    return;\n                }\n\n                const cx = w * 0.5;\n                const cy = h * 0.5;\n                let dx = sx - cx;\n                let dy = sy - cy;\n\n                if (_ghostProj.z > 1 || !isFinite(sx) || !isFinite(sy) || (Math.abs(dx) < 1 && Math.abs(dy) < 1)) {\n                    dx = cx - sx;\n                    dy = cy - sy;\n                }\n                if (Math.abs(dx) < 1e-4 && Math.abs(dy) < 1e-4) {\n                    dx = 0;\n                    dy = -1;\n                }\n\n                const len = Math.hypot(dx, dy) || 1;\n                dx /= len;\n                dy /= len;\n\n                const pad = EDGE_PADDING;\n                const edge = clampToScreenEdge(cx, cy, dx, dy, pad, pad, w - pad, h - pad);\n                const angleDeg = Math.atan2(dy, dx) * (180 / Math.PI);\n                const flipLabel = edge.x < cx;\n\n                el.style.left = edge.x + 'px';\n                el.style.top = edge.y + 'px';\n                innerEl.style.transform = 'rotate(' + angleDeg + 'deg)';\n                labelEl.style.transform = flipLabel ? 'rotate(180deg)' : 'rotate(0deg)';\n                el.classList.add('visible');\n                el.setAttribute('aria-hidden', 'false');\n            }\n\n            return { update, hide };\n        })();\n\n\n        const flyingStars = [];\n        const FLYING_STAR_DURATION = 0.85;\n\n        function clearFlyingStars() {\n            for (let i = flyingStars.length - 1; i >= 0; i--) {\n                flyingStars[i].el.remove();\n                flyingStars.splice(i, 1);\n            }\n        }\n\n        function spawnFlyingStar(worldX, worldY, worldZ) {\n            const startVec = new THREE.Vector3(worldX, worldY, worldZ);\n            startVec.project(camera);\n            const startX = (startVec.x * 0.5 + 0.5) * gameWorld.clientWidth;\n            const startY = (-startVec.y * 0.5 + 0.5) * gameWorld.clientHeight;\n\n            const goalStarEl = document.getElementById('goal-score-text') || document.getElementById('goal-card');\n            const rect = goalStarEl.getBoundingClientRect();\n            const endX = rect.left + rect.width / 2;\n            const endY = rect.top + rect.height / 2;\n            const el = document.createElement('div');\n            el.textContent = '\u2605';\n            el.style.cssText = 'position:absolute;pointer-events:none;font-size:36px;color:#FFD700;text-shadow:0 0 12px rgba(255,215,0,0.9),0 0 24px rgba(255,180,0,0.5);z-index:50;transform:translate(-50%,-50%);will-change:transform,opacity;';\n            gameWorld.appendChild(el);\n\n            const dx = endX - startX;\n            const dy = endY - startY;\n            const sideOffset = (Math.random() > 0.5 ? 1 : -1) * (60 + Math.random() * 80);\n            const ctrlX = startX + dx * 0.3 + sideOffset;\n            const ctrlY = startY + dy * 0.15 - (120 + Math.random() * 60);\n\n            flyingStars.push({\n                el,\n                elapsed: 0,\n                duration: FLYING_STAR_DURATION,\n                startX, startY,\n                ctrlX, ctrlY,\n                endX, endY,\n                startScale: 1.6,\n                endScale: 0.5\n            });\n        }\n\n        function updateFlyingStars(dt) {\n            for (let i = flyingStars.length - 1; i >= 0; i--) {\n                const star = flyingStars[i];\n                star.elapsed += dt;\n                const rawT = Math.min(star.elapsed / star.duration, 1);\n\n                const t = rawT < 0.5\n                    ? 2 * rawT * rawT\n                    : 1 - Math.pow(-2 * rawT + 2, 2) / 2;\n\n                const invT = 1 - t;\n                const x = invT * invT * star.startX + 2 * invT * t * star.ctrlX + t * t * star.endX;\n                const y = invT * invT * star.startY + 2 * invT * t * star.ctrlY + t * t * star.endY;\n\n                const scaleMid = 0.7;\n                let scale;\n                if (t < 0.5) {\n                    scale = star.startScale + (scaleMid - star.startScale) * (t / 0.5);\n                } else {\n                    scale = scaleMid + (star.endScale - scaleMid) * ((t - 0.5) / 0.5);\n                }\n\n                const rotY = t * 720;\n                const rotX = Math.sin(t * Math.PI) * 35;\n\n                star.el.style.left = x + 'px';\n                star.el.style.top = y + 'px';\n                star.el.style.transform = `translate(-50%,-50%) perspective(400px) rotateY(${rotY}deg) rotateX(${rotX}deg) scale(${scale})`;\n\n                let opacity = 1;\n                if (rawT < 0.1) opacity = rawT / 0.1;\n                else if (rawT > 0.85) opacity = (1 - rawT) / 0.15;\n                star.el.style.opacity = opacity;\n\n                if (rawT >= 1) {\n                    star.el.remove();\n                    flyingStars.splice(i, 1);\n                    AudioSystem.play('star-pop');\n                    const goalStarEl = document.getElementById('goal-score-text') || document.getElementById('goal-card');\n                    if (goalStarEl) {\n                        goalStarEl.style.transition = 'transform 0.15s ease-out';\n                        goalStarEl.style.transform = 'scale(1.2)';\n                        setTimeout(() => {\n                            goalStarEl.style.transition = 'transform 0.2s ease-in';\n                            goalStarEl.style.transform = 'scale(1.0)';\n                        }, 150);\n                    }\n                }\n            }\n        }\n\n        // ============================================================\n        // PARALLEL CONSUME STATE \u2014 independent player / ghost flags\n        // ============================================================\n        function normalizeConsumeState(inst) {\n            if (!inst) return;\n            if (inst.consumedByPlayer === undefined) {\n                inst.consumedByPlayer = !!inst.isBeingConsumed;\n            }\n            if (inst.consumedByGhost === undefined) {\n                inst.consumedByGhost = false;\n            }\n        }\n\n        function isConsumedBy(hole, inst) {\n            normalizeConsumeState(inst);\n            return hole.tracksPlayerVictory ? inst.consumedByPlayer : inst.consumedByGhost;\n        }\n\n        function markConsumedBy(hole, inst) {\n            normalizeConsumeState(inst);\n            if (hole.tracksPlayerVictory) {\n                inst.consumedByPlayer = true;\n                inst.isBeingConsumed = true;\n            } else {\n                inst.consumedByGhost = true;\n            }\n        }\n\n        function getInstanceTargetPosition(instanceId, worldObjects) {\n            const inst = (config.objectInstances || {})[instanceId];\n            if (!inst) return { x: 0, z: 0 };\n            const mesh = worldObjects[instanceId];\n            if (mesh && mesh.visible) {\n                return { x: mesh.position.x, z: mesh.position.z };\n            }\n            return { x: inst.x, z: inst.z };\n        }\n\n        // ============================================================\n        // CONSUME SYSTEM \u2014 hole-parameterized eating / overlap\n        // ============================================================\n        const ConsumeSystem = (() => {\n            const consumingObjects = [];\n            const blockedCooldowns = {};\n            const rejectionCooldowns = {};\n            const activeNudges = {};\n            const CONSUME_RENDER_ORDER = 10;\n\n            function computeHorizontalTarget(hole, level, t, objectRadius, cameraRotation) {\n                const profile = hole.profile;\n                const levelThreshold = profile.consumeSimpleProfileLevelThreshold !== undefined ? profile.consumeSimpleProfileLevelThreshold : 2;\n                const useSimpleProfile = level <= levelThreshold;\n                const tweenParams = HoleProfile.getConsumeTweenParams(profile, level);\n                const p = hole.state;\n\n                if (useSimpleProfile) {\n                    return { targetX: p.x, targetZ: p.z, useSimpleProfile: true };\n                }\n\n                const shiftFraction = tweenParams.nearEdgeShift;\n                const shiftTimingPower = tweenParams.shiftTimingPower;\n                const currentHoleRadius = p.radius;\n                const rotRad = (cameraRotation || 0) * Math.PI / 180;\n                const offsetDirX = Math.sin(rotRad);\n                const offsetDirZ = Math.cos(rotRad);\n                const safetyMargin = 0.85;\n                const maxPossibleShift = Math.max(0, currentHoleRadius * safetyMargin - (objectRadius || 0.3));\n                const desiredShift = currentHoleRadius * shiftFraction;\n                const finalMaxShift = Math.min(desiredShift, maxPossibleShift);\n                const shiftT = Math.pow(t, shiftTimingPower);\n                const shiftDistance = finalMaxShift * shiftT;\n\n                return {\n                    targetX: p.x + offsetDirX * shiftDistance,\n                    targetZ: p.z + offsetDirZ * shiftDistance,\n                    useSimpleProfile: false\n                };\n            }\n\n            function canConsume(hole, instanceId) {\n                const instances = config.objectInstances || {};\n                const obj = instances[instanceId];\n                if (!obj) return false;\n                if (isConsumedBy(hole, obj)) return false;\n                if (consumingObjects.some(c => c.instanceId === instanceId && c.holeId === hole.id)) return false;\n\n                // Stop consumption when AI ghost is ROAMING (not PB playback)\n                if (hole.id !== 'player') {\n                    if (isBotGhostMode() && typeof GhostBotController !== 'undefined' && GhostBotController.getGhostBotState && GhostBotController.getGhostBotState() === 'ROAMING') {\n                        return false;\n                    }\n                }\n\n                if (obj.registryId) {\n                    const entry = getRegistryEntry(obj.registryId);\n                    if (entry && entry.requiredHoleLevel > hole.state.level) return false;\n                }\n                return true;\n            }\n\n            function isHoleConsuming(holeId, instanceId) {\n                return consumingObjects.some(c => c.instanceId === instanceId && c.holeId === holeId);\n            }\n\n            function applyGhostConsumeMaterialStyle(rootMesh) {\n                applyGhostWorldObjectMaterialStyle(rootMesh);\n            }\n\n            const GhostConsumeClonePool = (() => {\n                const pools = {};\n                const MAX_PER_TYPE = 12;\n\n                function capturePoolMaterials(root) {\n                    root.traverse(child => {\n                        if (!child.isMesh || !child.material) return;\n                        const mats = Array.isArray(child.material) ? child.material : [child.material];\n                        child.userData._poolGhostMaterials = mats.slice();\n                    });\n                }\n\n                function restorePoolMaterials(mesh) {\n                    mesh.traverse(child => {\n                        if (!child.isMesh || !child.material) return;\n                        const poolMats = child.userData._poolGhostMaterials;\n                        if (!poolMats) return;\n                        const current = Array.isArray(child.material) ? child.material : [child.material];\n                        current.forEach(m => {\n                            if (poolMats.indexOf(m) === -1 && m) m.dispose();\n                        });\n                        child.material = poolMats.length === 1 ? poolMats[0] : poolMats.slice();\n                        child.renderOrder = 0;\n                    });\n                    mesh.renderOrder = 0;\n                }\n\n                function computePoolBaseRadius(group) {\n                    try {\n                        const box = new THREE.Box3().setFromObject(group);\n                        const size = new THREE.Vector3();\n                        box.getSize(size);\n                        return Math.max(size.x, size.z) / 2;\n                    } catch (e) {\n                        return 0.3;\n                    }\n                }\n\n                function createNew(modelType, inst) {\n                    const group = ObjectFactory.create(modelType, inst);\n                    applyGhostConsumeMaterialStyle(group);\n                    capturePoolMaterials(group);\n                    group.userData._poolKey = modelType;\n                    group.userData._pooledGhostConsumeClone = true;\n                    group.userData._poolBaseRadius = computePoolBaseRadius(group);\n                    return group;\n                }\n\n                function acquire(instanceId, worldPos) {\n                    const inst = (config.objectInstances || {})[instanceId];\n                    if (!inst) return null;\n                    const modelType = getModelTypeForInstance(inst);\n                    const entry = inst.registryId ? getRegistryEntry(inst.registryId) : null;\n                    const effectiveScale = (inst.scale !== undefined && inst.scale !== null)\n                        ? inst.scale\n                        : ((entry && entry.defaultScale) ? entry.defaultScale : 1);\n\n                    const pool = pools[modelType];\n                    const group = (pool && pool.length > 0)\n                        ? pool.pop()\n                        : createNew(modelType, inst);\n\n                    restorePoolMaterials(group);\n                    group.position.set(worldPos.x, 0, worldPos.z);\n                    group.scale.setScalar(effectiveScale);\n                    group.rotation.set(0, inst.rotationY !== undefined ? inst.rotationY : 0, 0);\n                    group.visible = true;\n                    group.userData.instanceId = instanceId;\n                    delete group.userData.isGhostResidue;\n                    applyGhostConsumeMaterialStyle(group);\n                    scene.add(group);\n                    return group;\n                }\n\n                function release(mesh) {\n                    if (!mesh || !mesh.userData._pooledGhostConsumeClone) {\n                        disposeWorldObjectGroup(mesh);\n                        return;\n                    }\n                    scene.remove(mesh);\n                    restorePoolMaterials(mesh);\n                    mesh.visible = false;\n                    mesh.position.set(0, 0, 0);\n                    mesh.rotation.set(0, 0, 0);\n                    mesh.scale.setScalar(1);\n                    delete mesh.userData.instanceId;\n\n                    const key = mesh.userData._poolKey || 'default';\n                    if (!pools[key]) pools[key] = [];\n                    if (pools[key].length < MAX_PER_TYPE) {\n                        pools[key].push(mesh);\n                    } else {\n                        disposeWorldObjectGroup(mesh);\n                    }\n                }\n\n                function clear() {\n                    Object.keys(pools).forEach(key => {\n                        pools[key].forEach(mesh => disposeWorldObjectGroup(mesh));\n                        pools[key].length = 0;\n                    });\n                }\n\n                return { acquire, release, clear };\n            })();\n\n            function releaseGhostConsumeMesh(mesh) {\n                GhostConsumeClonePool.release(mesh);\n            }\n\n            function saveOriginalMaterials(mesh) {\n                if (mesh.userData._originalMaterialSnapshots) return;\n                const snapshots = [];\n                mesh.traverse(child => {\n                    if (!child.isMesh || !child.material) return;\n                    snapshots.push({\n                        mesh: child,\n                        material: child.material\n                    });\n                });\n                mesh.userData._originalMaterialSnapshots = snapshots;\n            }\n\n            function releaseConsumeMaterialsAndRestore(mesh) {\n                const snapshots = mesh.userData._originalMaterialSnapshots;\n                if (!snapshots) return;\n\n                mesh.traverse(child => {\n                    if (!child.isMesh || !child.material) return;\n                    const currentMats = Array.isArray(child.material) ? child.material : [child.material];\n                    currentMats.forEach(m => {\n                        let isOriginal = false;\n                        for (let i = 0; i < snapshots.length; i++) {\n                            const snap = snapshots[i];\n                            if (snap.mesh !== child) continue;\n                            const origMats = Array.isArray(snap.material) ? snap.material : [snap.material];\n                            if (origMats.indexOf(m) !== -1) {\n                                isOriginal = true;\n                                break;\n                            }\n                        }\n                        if (!isOriginal && m) m.dispose();\n                    });\n                });\n\n                snapshots.forEach(snap => {\n                    snap.mesh.material = snap.material;\n                    snap.mesh.renderOrder = 0;\n                });\n                delete mesh.userData._originalMaterialSnapshots;\n            }\n\n            function preparePlayerMeshAsGhostResidue(mesh, instanceId, startPos, rotX, rotY, rotZ, startScale) {\n                releaseConsumeMaterialsAndRestore(mesh);\n\n                mesh.position.copy(startPos);\n                mesh.rotation.set(rotX, rotY, rotZ);\n                mesh.scale.copy(startScale);\n                mesh.visible = true;\n                mesh.renderOrder = 0;\n                mesh.userData.instanceId = instanceId;\n                mesh.userData.isGhostResidue = true;\n\n                applyGhostWorldObjectMaterialStyle(mesh);\n            }\n\n            function disposeConsumedMesh(mesh) {\n                if (mesh && mesh.userData._pooledGhostConsumeClone) {\n                    releaseGhostConsumeMesh(mesh);\n                } else {\n                    disposeWorldObjectGroup(mesh);\n                }\n            }\n\n            function createGhostConsumeClone(instanceId, worldPos) {\n                return GhostConsumeClonePool.acquire(instanceId, worldPos);\n            }\n\n            function createGhostWorldResidue(instanceId, worldPos, rotX, rotY, rotZ, startScale, sourceMesh) {\n                if (sourceMesh) {\n                    preparePlayerMeshAsGhostResidue(\n                        sourceMesh,\n                        instanceId,\n                        worldPos,\n                        rotX,\n                        rotY,\n                        rotZ,\n                        startScale\n                    );\n                    return sourceMesh;\n                }\n\n                const inst = (config.objectInstances || {})[instanceId];\n                if (!inst) return null;\n                const modelType = getModelTypeForInstance(inst);\n                const group = ObjectFactory.create(modelType, inst);\n                group.position.set(worldPos.x, 0, worldPos.z);\n                group.rotation.set(rotX || 0, rotY !== undefined ? rotY : (inst.rotationY !== undefined ? inst.rotationY : 0), rotZ || 0);\n                if (startScale) {\n                    group.scale.copy(startScale);\n                } else {\n                    group.scale.setScalar(inst.scale || 1);\n                }\n                applyGhostConsumeMaterialStyle(group);\n                group.userData.instanceId = instanceId;\n                group.userData.isGhostResidue = true;\n                scene.add(group);\n                return group;\n            }\n\n            function pushConsumeEntry(hole, mesh, instanceId, isGhostClone) {\n                const inst = (config.objectInstances || {})[instanceId];\n                const consumeSelfOcclusion = inst && inst.registryId\n                    ? registryNeedsConsumeSelfOcclusion(inst.registryId)\n                    : false;\n\n                let objectRadius = 0.3;\n                if (isGhostClone && mesh.userData._poolBaseRadius !== undefined) {\n                    objectRadius = mesh.userData._poolBaseRadius * mesh.scale.x;\n                } else {\n                    try {\n                        const box = new THREE.Box3().setFromObject(mesh);\n                        const size = new THREE.Vector3();\n                        box.getSize(size);\n                        objectRadius = Math.max(size.x, size.z) / 2;\n                    } catch (e) {\n                        console.warn('[ConsumeSystem] Failed to calculate bounding box:', e);\n                    }\n                }\n\n                const willBecomeResidue = hole.tracksPlayerVictory && !isGhostClone\n                    && isGhostActiveThisRound() && inst && !inst.consumedByGhost;\n                if (willBecomeResidue) {\n                    saveOriginalMaterials(mesh);\n                }\n\n                applyConsumeRenderForObject(mesh, consumeSelfOcclusion);\n\n                const tweenParams = HoleProfile.getConsumeTweenParams(hole.profile, hole.state.level);\n                const baseFallDuration = hole.profile.consumeFallDuration !== undefined ? hole.profile.consumeFallDuration : 0.75;\n                const fallDuration = baseFallDuration * (tweenParams.fallDurationMult !== undefined ? tweenParams.fallDurationMult : 1.0);\n\n                consumingObjects.push({\n                    holeId: hole.id,\n                    hole,\n                    mesh,\n                    instanceId,\n                    isGhostClone: !!isGhostClone,\n                    elapsed: 0,\n                    duration: fallDuration,\n                    startPos: mesh.position.clone(),\n                    startScale: mesh.scale.clone(),\n                    rotX: mesh.rotation.x,\n                    rotY: mesh.rotation.y,\n                    rotZ: mesh.rotation.z,\n                    tumbleSpeedX: (Math.random() - 0.5) * 0.4,\n                    tumbleSpeedY: (Math.random() - 0.5) * 0.8 + 0.4,\n                    tumbleSpeedZ: (Math.random() - 0.5) * 0.4,\n                    targetX: hole.state.x,\n                    targetZ: hole.state.z,\n                    objectRadius,\n                    consumeSelfOcclusion\n                });\n            }\n\n            function startPlayerConsume(hole, mesh, instanceId) {\n                const instances = config.objectInstances || {};\n                const inst = instances[instanceId];\n                if (inst) markConsumedBy(hole, inst);\n\n                if (inst && inst.registryId) {\n                    const entry = getRegistryEntry(inst.registryId);\n                    if (entry && entry.reactionSounds && entry.reactionSounds.length > 0) {\n                        const randomSound = entry.reactionSounds[Math.floor(Math.random() * entry.reactionSounds.length)];\n                        AudioSystem.play(randomSound);\n                    }\n                }\n\n                const willSpawnResidue = isGhostActiveThisRound() && inst && !inst.consumedByGhost;\n                if (!willSpawnResidue) {\n                    CarManager.removeCar(instanceId);\n                }\n\n                // Remove from active world objects so it cannot be interacted with during the consume animation\n                delete worldObjects[instanceId];\n                \n                pushConsumeEntry(hole, mesh, instanceId, false);\n            }\n\n            function finishGhostConsumeInstant(hole, instanceId, worldObjects) {\n                const instances = config.objectInstances || {};\n                const inst = instances[instanceId];\n\n                if (inst && inst.consumedByPlayer) {\n                    CarManager.removeCar(instanceId);\n                    const residueMesh = worldObjects[instanceId];\n                    if (residueMesh) {\n                        delete worldObjects[instanceId];\n                        disposeWorldObjectGroup(residueMesh);\n                    }\n                }\n\n                PointManager.awardConsume(hole, instanceId);\n                if (!hole.tracksPlayerVictory) {\n                    updateGhostRivalHUD();\n                }\n            }\n\n            function startGhostConsume(hole, instanceId, worldObjects) {\n                const instances = config.objectInstances || {};\n                const inst = instances[instanceId];\n                if (inst) markConsumedBy(hole, inst);\n\n                if (!GhostPresence.isGhostOnScreen()) {\n                    finishGhostConsumeInstant(hole, instanceId, worldObjects);\n                    return;\n                }\n\n                if (inst && inst.consumedByPlayer) {\n                    CarManager.removeCar(instanceId);\n                    const residueMesh = worldObjects[instanceId];\n                    if (residueMesh) {\n                        delete worldObjects[instanceId];\n                        pushConsumeEntry(hole, residueMesh, instanceId, true);\n                    }\n                } else {\n                    const worldPos = getInstanceTargetPosition(instanceId, worldObjects);\n                    const clone = createGhostConsumeClone(instanceId, worldPos);\n                    if (!clone) return;\n                    pushConsumeEntry(hole, clone, instanceId, true);\n                }\n            }\n\n            function registryNeedsConsumeSelfOcclusion(registryId) {\n                const entry = getRegistryEntry(registryId);\n                return getRegistryCapability(entry, 'consumeSelfOcclusion');\n            }\n\n            function applyConsumeRenderForObject(rootMesh, selfOcclusion) {\n                rootMesh.renderOrder = CONSUME_RENDER_ORDER;\n                rootMesh.traverse(child => {\n                    if (!child.isMesh || !child.material) return;\n                    child.renderOrder = CONSUME_RENDER_ORDER;\n                    const applyMat = (m) => {\n                        const cloned = m.clone();\n                        if (selfOcclusion) {\n                            cloned.depthTest = true;\n                            cloned.depthWrite = !cloned.transparent;\n                        } else {\n                            cloned.depthTest = false;\n                            cloned.depthWrite = false;\n                        }\n                        return cloned;\n                    };\n                    if (Array.isArray(child.material)) {\n                        child.material = child.material.map(applyMat);\n                    } else {\n                        child.material = applyMat(child.material);\n                    }\n                });\n            }\n\n            function handleBlockedOverlap(hole, instanceId, mesh) {\n                const now = performance.now();\n\n                if (!rejectionCooldowns[instanceId] || now >= rejectionCooldowns[instanceId]) {\n                    rejectionCooldowns[instanceId] = now + 1000;\n                    hole.triggerRejectionFeedback();\n\n                    const inst = config.objectInstances[instanceId];\n                    if (inst) {\n                        const dx = mesh.position.x - hole.state.x;\n                        const dz = mesh.position.z - hole.state.z;\n                        const len = Math.sqrt(dx * dx + dz * dz);\n                        if (len > 0) {\n                            const nx = dx / len;\n                            const nz = dz / len;\n                            const nudgeDist = 0.45;\n                            activeNudges[instanceId] = {\n                                startX: inst.x,\n                                startZ: inst.z,\n                                nudgeX: nx * nudgeDist,\n                                nudgeZ: nz * nudgeDist,\n                                elapsed: 0,\n                                duration: 0.2\n                            };\n                        }\n                    }\n                }\n\n                if (blockedCooldowns[instanceId] && now < blockedCooldowns[instanceId]) {\n                    return;\n                }\n\n                if (getObjectCategory(instanceId) === 'vehicle' && CarManager.isActiveCar(instanceId)) {\n                    const inst = config.objectInstances[instanceId];\n                    if (inst && inst.registryId) {\n                        const entry = getRegistryEntry(inst.registryId);\n                        if (entry && entry.reactionSounds && entry.reactionSounds.length > 0) {\n                            const randomSound = entry.reactionSounds[Math.floor(Math.random() * entry.reactionSounds.length)];\n                            AudioSystem.play(randomSound);\n                        }\n                    }\n                    CarManager.pauseCar(instanceId, 1.5);\n                    blockedCooldowns[instanceId] = now + 3000;\n                }\n            }\n\n            function checkOverlapForHole(hole, worldObjects) {\n                const consumeRadius = hole.getConsumeRadius();\n\n                if (hole.tracksPlayerVictory) {\n                    const holeX = hole.state.x;\n                    const holeZ = hole.state.z;\n                    const consumeRadiusSq = consumeRadius * consumeRadius;\n                    const nearbyIds = InstanceSpatialIndex.queryNear(holeX, holeZ, consumeRadius);\n\n                    for (let i = 0; i < nearbyIds.length; i++) {\n                        const instanceId = nearbyIds[i];\n                        const mesh = worldObjects[instanceId];\n                        if (!mesh || !mesh.visible) continue;\n                        if (mesh.userData.isGhostResidue) continue;\n                        if (isHoleConsuming(hole.id, instanceId)) continue;\n\n                        const dx = mesh.position.x - holeX;\n                        const dz = mesh.position.z - holeZ;\n                        const distSq = dx * dx + dz * dz;\n\n                        if (distSq < consumeRadiusSq) {\n                            if (canConsume(hole, instanceId)) {\n                                startPlayerConsume(hole, mesh, instanceId);\n                            } else {\n                                handleBlockedOverlap(hole, instanceId, mesh);\n                            }\n                        }\n                    }\n                } else {\n                    const instances = config.objectInstances || {};\n                    const holeX = hole.state.x;\n                    const holeZ = hole.state.z;\n                    const consumeRadiusSq = consumeRadius * consumeRadius;\n                    const nearbyIds = InstanceSpatialIndex.queryNear(holeX, holeZ, consumeRadius);\n\n                    for (let i = 0; i < nearbyIds.length; i++) {\n                        const instanceId = nearbyIds[i];\n                        if (!canConsume(hole, instanceId)) continue;\n                        if (isHoleConsuming(hole.id, instanceId)) continue;\n\n                        const pos = getInstanceTargetPosition(instanceId, worldObjects);\n                        const dx = pos.x - holeX;\n                        const dz = pos.z - holeZ;\n                        const distSq = dx * dx + dz * dz;\n\n                        if (distSq < consumeRadiusSq) {\n                            startGhostConsume(hole, instanceId, worldObjects);\n                        }\n                    }\n                }\n            }\n\n            function update(dt, cameraRotation) {\n                for (let i = consumingObjects.length - 1; i >= 0; i--) {\n                    const obj = consumingObjects[i];\n                    const hole = obj.hole;\n                    const currentLevel = hole.state.level;\n                    const profile = hole.profile;\n                    const levelThreshold = profile.consumeSimpleProfileLevelThreshold !== undefined ? profile.consumeSimpleProfileLevelThreshold : 2;\n                    const useSimpleProfile = currentLevel <= levelThreshold;\n                    const tweenParams = HoleProfile.getConsumeTweenParams(profile, currentLevel);\n                    const depthThreshold = useSimpleProfile ? -2.0 : tweenParams.depthThreshold;\n                    const attractPower = useSimpleProfile ? 1.4 : tweenParams.attractPower;\n\n                    obj.elapsed += dt;\n                    const t = Math.min(obj.elapsed / obj.duration, 1);\n                    const easedFall = t * t;\n                    const easedAttract = Math.pow(t, attractPower);\n\n                    const horiz = computeHorizontalTarget(hole, currentLevel, t, obj.objectRadius, cameraRotation);\n                    const targetX = horiz.targetX;\n                    const targetZ = horiz.targetZ;\n\n                    obj.mesh.position.x = obj.startPos.x + (targetX - obj.startPos.x) * easedAttract;\n                    obj.mesh.position.z = obj.startPos.z + (targetZ - obj.startPos.z) * easedAttract;\n\n                    const fallTargetY = obj.consumeSelfOcclusion\n                        ? hole.getConsumeFallTargetY(true)\n                        : depthThreshold;\n                    obj.mesh.position.y = obj.startPos.y + (fallTargetY - obj.startPos.y) * easedFall;\n\n                    if (useSimpleProfile) {\n                        obj.mesh.rotation.x = obj.rotX + obj.tumbleSpeedX * 0.25 * t;\n                        obj.mesh.rotation.y = obj.rotY + obj.tumbleSpeedY * 0.25 * t;\n                        obj.mesh.rotation.z = obj.rotZ + obj.tumbleSpeedZ * 0.25 * t;\n                        const shrinkRate = obj.consumeSelfOcclusion ? 0.95 : 0.9;\n                        const scaleMult = Math.max(0, 1.0 - t * shrinkRate);\n                        obj.mesh.scale.set(\n                            obj.startScale.x * scaleMult,\n                            obj.startScale.y * scaleMult,\n                            obj.startScale.z * scaleMult\n                        );\n                    } else {\n                        obj.mesh.rotation.x = obj.rotX + obj.tumbleSpeedX * t;\n                        obj.mesh.rotation.y = obj.rotY + obj.tumbleSpeedY * t;\n                        obj.mesh.rotation.z = obj.rotZ + obj.tumbleSpeedZ * t;\n                        const shrinkRate = obj.consumeSelfOcclusion ? 0.92 : 0.85;\n                        const scaleMult = Math.max(0, 1.0 - t * shrinkRate);\n                        obj.mesh.scale.set(\n                            obj.startScale.x * scaleMult,\n                            obj.startScale.y * scaleMult,\n                            obj.startScale.z * scaleMult\n                        );\n                    }\n\n                    if (t >= 1) {\n                        if (obj.hole.tracksPlayerVictory) {\n                            AudioSystem.playRandom('consume');\n                        }\n                        const pts = PointManager.awardConsume(obj.hole, obj.instanceId);\n                        if (obj.isGhostClone) {\n                            disposeConsumedMesh(obj.mesh);\n                        } else {\n                            const inst = (config.objectInstances || {})[obj.instanceId];\n                            const spawnResidue = obj.hole.tracksPlayerVictory\n                                && isGhostActiveThisRound() && inst && !inst.consumedByGhost;\n\n                            if (obj.hole.tracksPlayerVictory) {\n                                spawnFloatingText(obj.mesh.position.x, obj.mesh.position.z, pts);\n                                spawnFlyingStar(obj.startPos.x, obj.startPos.y, obj.startPos.z);\n                            }\n\n                            if (spawnResidue) {\n                                const ghostResidue = createGhostWorldResidue(\n                                    obj.instanceId,\n                                    obj.startPos,\n                                    obj.rotX,\n                                    obj.rotY,\n                                    obj.rotZ,\n                                    obj.startScale,\n                                    obj.mesh\n                                );\n                                if (ghostResidue) {\n                                    worldObjects[obj.instanceId] = ghostResidue;\n\n                                    ghostResidue.traverse(child => {\n                                        if (child.isMesh && child.material) {\n                                            const mats = Array.isArray(child.material) ? child.material : [child.material];\n                                            mats.forEach(m => {\n                                                m.transparent = true;\n                                                m.opacity = 0;\n                                            });\n                                        }\n                                    });\n\n                                    manifestAnimations.push({\n                                        mesh: ghostResidue,\n                                        elapsed: 0,\n                                        duration: 0.45,\n                                        normalOpacity: getGhostObjectOpacity()\n                                    });\n\n                                    spawnManifestRing(obj.startPos.x, obj.startPos.z, obj.objectRadius);\n                                } else {\n                                    obj.mesh.visible = false;\n                                }\n                            } else {\n                                obj.mesh.visible = false;\n                            }\n                        }\n                        consumingObjects.splice(i, 1);\n                    }\n                }\n            }\n\n            function updateNudges(dt, worldObjects) {\n                Object.keys(activeNudges).forEach(id => {\n                    const nudge = activeNudges[id];\n                    const mesh = worldObjects[id];\n                    if (!mesh) {\n                        delete activeNudges[id];\n                        return;\n                    }\n                    nudge.elapsed += dt;\n                    const t = Math.min(nudge.elapsed / nudge.duration, 1);\n                    const ease = Math.pow(1 - t, 2);\n                    mesh.position.x = nudge.startX + nudge.nudgeX * ease;\n                    mesh.position.z = nudge.startZ + nudge.nudgeZ * ease;\n                    if (t >= 1) {\n                        mesh.position.x = nudge.startX;\n                        mesh.position.z = nudge.startZ;\n                        delete activeNudges[id];\n                    }\n                });\n            }\n\n            function clearAll(worldObjects) {\n                for (let i = consumingObjects.length - 1; i >= 0; i--) {\n                    const obj = consumingObjects[i];\n                    if (obj.mesh) {\n                        if (obj.isGhostClone) {\n                            releaseGhostConsumeMesh(obj.mesh);\n                        } else {\n                            disposeWorldObjectGroup(obj.mesh);\n                        }\n                    }\n                }\n                consumingObjects.length = 0;\n                Object.keys(blockedCooldowns).forEach(k => delete blockedCooldowns[k]);\n                Object.keys(rejectionCooldowns).forEach(k => delete rejectionCooldowns[k]);\n                Object.keys(activeNudges).forEach(k => {\n                    const mesh = worldObjects[k];\n                    if (mesh && activeNudges[k]) {\n                        mesh.position.x = activeNudges[k].startX;\n                        mesh.position.z = activeNudges[k].startZ;\n                    }\n                    delete activeNudges[k];\n                });\n            }\n\n            return {\n                computeHorizontalTarget,\n                canConsume,\n                checkOverlapForHole,\n                update,\n                updateNudges,\n                clearAll,\n                clearGhostConsumePool: GhostConsumeClonePool.clear,\n                getConsumingObjects: () => consumingObjects\n            };\n        })();\n\n        HoleManager.checkAllOverlaps = function(worldObjects) {\n            this.holes.forEach(hole => ConsumeSystem.checkOverlapForHole(hole, worldObjects));\n        };\n\n        const ConsumeTargetDebug = (() => {\n            const group = new THREE.Group();\n            group.name = 'consumeTargetDebug';\n            group.visible = !!config.showConsumeTargetDebug;\n            scene.add(group);\n\n            const markerMat = (color) => new THREE.MeshBasicMaterial({\n                color, depthTest: false, transparent: true, opacity: 0.9\n            });\n\n            const openingCenterMarker = new THREE.Mesh(new THREE.SphereGeometry(0.08, 12, 12), markerMat(0xffff00));\n            openingCenterMarker.renderOrder = 20;\n            group.add(openingCenterMarker);\n\n            const codeCenterGroundMarker = new THREE.Mesh(new THREE.SphereGeometry(0.06, 12, 12), markerMat(0xffffff));\n            codeCenterGroundMarker.renderOrder = 20;\n            group.add(codeCenterGroundMarker);\n\n            const consumeTargetMarker = new THREE.Mesh(new THREE.SphereGeometry(0.1, 12, 12), markerMat(0xff3333));\n            consumeTargetMarker.renderOrder = 20;\n            group.add(consumeTargetMarker);\n\n            const visualRadiusRing = new THREE.Mesh(\n                new THREE.RingGeometry(0.98, 1.0, 48),\n                new THREE.MeshBasicMaterial({ color: 0x00ff88, side: THREE.DoubleSide, depthTest: false, transparent: true, opacity: 0.85 })\n            );\n            visualRadiusRing.rotation.x = -Math.PI / 2;\n            visualRadiusRing.renderOrder = 19;\n            group.add(visualRadiusRing);\n\n            const logicRadiusRing = new THREE.Mesh(\n                new THREE.RingGeometry(0.98, 1.0, 48),\n                new THREE.MeshBasicMaterial({ color: 0xff8800, side: THREE.DoubleSide, depthTest: false, transparent: true, opacity: 0.7 })\n            );\n            logicRadiusRing.rotation.x = -Math.PI / 2;\n            logicRadiusRing.renderOrder = 19;\n            group.add(logicRadiusRing);\n\n            const openingToGroundLine = new THREE.Line(\n                new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3()]),\n                new THREE.LineBasicMaterial({ color: 0xffff00, depthTest: false, transparent: true, opacity: 0.5 })\n            );\n            openingToGroundLine.renderOrder = 18;\n            group.add(openingToGroundLine);\n\n            const meshMarkerPool = [];\n            const fallPathLines = [];\n\n            function ensureMeshMarkers(count) {\n                while (meshMarkerPool.length < count) {\n                    const m = new THREE.Mesh(\n                        new THREE.SphereGeometry(0.07, 10, 10),\n                        markerMat(0x33ccff)\n                    );\n                    m.renderOrder = 21;\n                    group.add(m);\n                    meshMarkerPool.push(m);\n\n                    const line = new THREE.Line(\n                        new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3()]),\n                        new THREE.LineBasicMaterial({ color: 0xff66aa, depthTest: false, transparent: true, opacity: 0.65 })\n                    );\n                    line.renderOrder = 18;\n                    group.add(line);\n                    fallPathLines.push(line);\n                }\n            }\n\n            function setMarkerScale(mesh, baseGeoRadius, visualRadius) {\n                const s = Math.max(0.04, visualRadius * 0.07) / baseGeoRadius;\n                mesh.scale.setScalar(s);\n            }\n\n            function setRingScale(ring, radius) {\n                ring.scale.set(radius, radius, 1);\n            }\n\n            function update() {\n                if (!group.visible) return;\n\n                const hole = HoleManager.getPlayerHole();\n                if (!hole) return;\n\n                const opening = hole.getHoleOpeningWorldCenter();\n                const visualRadius = HOLE_RADIUS * hole.getHoleVisualScale();\n                const logicRadius = hole.state.radius;\n                const openingY = opening.y;\n                const p = hole.state;\n\n                setMarkerScale(openingCenterMarker, 0.08, visualRadius);\n                setMarkerScale(codeCenterGroundMarker, 0.06, visualRadius);\n                setMarkerScale(consumeTargetMarker, 0.1, visualRadius);\n\n                openingCenterMarker.position.copy(opening);\n                codeCenterGroundMarker.position.set(p.x, 0, p.z);\n\n                const linePos = openingToGroundLine.geometry.attributes.position;\n                linePos.setXYZ(0, opening.x, openingY, opening.z);\n                linePos.setXYZ(1, p.x, 0, p.z);\n                linePos.needsUpdate = true;\n\n                visualRadiusRing.position.set(opening.x, openingY, opening.z);\n                setRingScale(visualRadiusRing, visualRadius);\n\n                logicRadiusRing.position.set(p.x, openingY, p.z);\n                setRingScale(logicRadiusRing, logicRadius);\n\n                let sampleTargetX = p.x;\n                let sampleTargetZ = p.z;\n\n                const consumingObjects = ConsumeSystem.getConsumingObjects();\n                ensureMeshMarkers(consumingObjects.length);\n                for (let i = 0; i < meshMarkerPool.length; i++) {\n                    const marker = meshMarkerPool[i];\n                    const pathLine = fallPathLines[i];\n                    if (i < consumingObjects.length) {\n                        const obj = consumingObjects[i];\n                        const t = Math.min(obj.elapsed / obj.duration, 1);\n                        const horiz = ConsumeSystem.computeHorizontalTarget(hole, p.level, t, obj.objectRadius, config.cameraRotation);\n                        marker.visible = true;\n                        pathLine.visible = true;\n                        setMarkerScale(marker, 0.07, visualRadius);\n                        marker.position.set(obj.mesh.position.x, obj.mesh.position.y, obj.mesh.position.z);\n\n                        const pathPos = pathLine.geometry.attributes.position;\n                        pathPos.setXYZ(0, horiz.targetX, openingY, horiz.targetZ);\n                        pathPos.setXYZ(1, obj.mesh.position.x, obj.mesh.position.y, obj.mesh.position.z);\n                        pathPos.needsUpdate = true;\n\n                        if (i === 0) {\n                            sampleTargetX = horiz.targetX;\n                            sampleTargetZ = horiz.targetZ;\n                        }\n                    } else {\n                        marker.visible = false;\n                        if (pathLine) pathLine.visible = false;\n                    }\n                }\n\n                if (consumingObjects.length === 0) {\n                    const horiz = ConsumeSystem.computeHorizontalTarget(hole, p.level, 1, 0.3, config.cameraRotation);\n                    sampleTargetX = horiz.targetX;\n                    sampleTargetZ = horiz.targetZ;\n                }\n\n                consumeTargetMarker.position.set(sampleTargetX, openingY, sampleTargetZ);\n\n                if (config.showConsumeTargetDebug && !update._loggedMismatch) {\n                    update._loggedMismatch = true;\n                    const tweenParams = HoleProfile.getConsumeTweenParams(hole.profile, p.level);\n                    lib.log('[ConsumeDebug] Yellow=hole opening center | Red=horizontal consume target @ opening Y | Cyan=mesh group origin | Pink line=target-to-mesh Y gap | White=player XZ @ Y=0');\n                    lib.log('[ConsumeDebug] Opening Y=' + openingY.toFixed(3) + ' | Mesh origins start Y\u22480 | Fall terminus Y=' + tweenParams.depthThreshold + ' (below ground, not at opening)');\n                }\n            }\n\n            function setVisible(v) {\n                group.visible = !!v;\n                config.showConsumeTargetDebug = !!v;\n                if (!v) update._loggedMismatch = false;\n            }\n\n            return { update, setVisible, group };\n        })();\n\n        function getRegistryCapability(entry, name) {\n            if (!entry) return false;\n            return !!entry[name];\n        }\n\n\n        const WaypointManager = (() => {\n            let debugGroup = null;\n            let isVisible = false;\n\n            function getWaypoints(instanceId) {\n                const instances = config.objectInstances || {};\n                const inst = instances[instanceId];\n                if (inst && inst.waypoints && inst.waypoints.length >= 2) {\n                    return inst.waypoints;\n                }\n                return null;\n            }\n\n            function getInstancesWithWaypoints() {\n                const instances = config.objectInstances || {};\n                return Object.keys(instances).filter(id => {\n                    const inst = instances[id];\n                    return inst.waypoints && inst.waypoints.length >= 2;\n                });\n            }\n\n            function rebuild() {\n                if (debugGroup) {\n                    scene.remove(debugGroup);\n                    debugGroup.traverse(child => {\n                        if (child.geometry) child.geometry.dispose();\n                        if (child.material) child.material.dispose();\n                    });\n                }\n\n                debugGroup = new THREE.Group();\n                debugGroup.name = 'WaypointDebugGroup';\n\n                const markerGeo = new THREE.SphereGeometry(0.4, 8, 6);\n                const markerMat = new THREE.MeshBasicMaterial({ color: 0xff4444 });\n                const lineMat = new THREE.LineBasicMaterial({ color: 0xff8800, linewidth: 2 });\n\n                const ids = getInstancesWithWaypoints();\n                ids.forEach(id => {\n                    const waypoints = getWaypoints(id);\n                    if (!waypoints) return;\n\n                    const linePoints = [];\n                    waypoints.forEach((wp, idx) => {\n                        const marker = new THREE.Mesh(markerGeo, markerMat);\n                        marker.position.set(wp.x, 0.5, wp.z);\n                        debugGroup.add(marker);\n                        linePoints.push(new THREE.Vector3(wp.x, 0.3, wp.z));\n                    });\n\n                    if (linePoints.length > 0) {\n                        linePoints.push(linePoints[0].clone());\n                    }\n\n                    const lineGeo = new THREE.BufferGeometry().setFromPoints(linePoints);\n                    const line = new THREE.Line(lineGeo, lineMat);\n                    debugGroup.add(line);\n                });\n\n                debugGroup.visible = isVisible;\n                scene.add(debugGroup);\n            }\n\n            function setVisible(visible) {\n                isVisible = visible;\n                if (debugGroup) debugGroup.visible = visible;\n            }\n\n            rebuild();\n\n            return { getWaypoints, getInstancesWithWaypoints, rebuild, setVisible };\n        })();\n\n\n        const CarManager = (() => {\n            const activeCars = {};\n\n            function init() {\n                Object.keys(activeCars).forEach(k => delete activeCars[k]);\n                const ids = WaypointManager.getInstancesWithWaypoints();\n                ids.forEach(id => {\n                    const waypoints = WaypointManager.getWaypoints(id);\n                    if (!waypoints) return;\n\n                    if (getObjectCategory(id) !== 'vehicle') return;\n\n                    const inst = config.objectInstances[id];\n                    let nextIdx = 0;\n\n                    if (inst && inst.initialNextWaypointId !== undefined && inst.initialNextWaypointId !== null && inst.initialNextWaypointId !== '') {\n                        // Convert 1-based node ID (as shown in editor) to 0-based array index\n                        nextIdx = parseInt(inst.initialNextWaypointId, 10) - 1;\n                        if (isNaN(nextIdx) || nextIdx < 0 || nextIdx >= waypoints.length) {\n                            nextIdx = 0;\n                        }\n                    } else {\n                        let closestIdx = 0;\n                        let closestDist = Infinity;\n                        waypoints.forEach((wp, idx) => {\n                            const dx = wp.x - inst.x;\n                            const dz = wp.z - inst.z;\n                            const dist = dx * dx + dz * dz;\n                            if (dist < closestDist) {\n                                closestDist = dist;\n                                closestIdx = idx;\n                            }\n                        });\n                        nextIdx = (closestIdx + 1) % waypoints.length;\n                    }\n\n                    activeCars[id] = {\n                        waypointIndex: nextIdx,\n                        waypoints: waypoints\n                    };\n                });\n\n                lib.log('CarManager: Initialized ' + Object.keys(activeCars).length + ' moving cars');\n            }\n\n            function update(dt) {\n                const speed = config.carSpeed || 4;\n\n                Object.keys(activeCars).forEach(id => {\n                    const car = activeCars[id];\n                    const mesh = worldObjects[id];\n                    if (!mesh || !mesh.visible) return;\n\n                    if (car.pauseTimer && car.pauseTimer > 0) {\n                        car.pauseTimer -= dt;\n                        return;\n                    }\n\n                    const target = car.waypoints[car.waypointIndex];\n                    const currentX = mesh.position.x;\n                    const currentZ = mesh.position.z;\n\n                    const dx = target.x - currentX;\n                    const dz = target.z - currentZ;\n                    const dist = Math.sqrt(dx * dx + dz * dz);\n\n                    const arrivalDist = 0.3;\n\n                    if (dist < arrivalDist) {\n                        car.waypointIndex = (car.waypointIndex + 1) % car.waypoints.length;\n                    } else {\n                        const moveAmount = Math.min(speed * dt, dist);\n                        const nx = dx / dist;\n                        const nz = dz / dist;\n\n                        mesh.position.x += nx * moveAmount;\n                        mesh.position.z += nz * moveAmount;\n\n                        if (config.objectInstances[id]) {\n                            config.objectInstances[id].x = mesh.position.x;\n                            config.objectInstances[id].z = mesh.position.z;\n                        }\n                        InstanceSpatialIndex.setInstanceCell(id, mesh.position.x, mesh.position.z);\n\n                        const targetAngle = Math.atan2(nx, nz) - Math.PI / 2;\n                        mesh.rotation.y = targetAngle;\n                    }\n                });\n            }\n\n            function removeCar(instanceId) {\n                if (activeCars[instanceId]) {\n                    delete activeCars[instanceId];\n                    lib.log('CarManager: Removed car ' + instanceId);\n                }\n            }\n\n            function isActiveCar(instanceId) {\n                return !!activeCars[instanceId];\n            }\n\n            function pauseCar(instanceId, duration) {\n                const car = activeCars[instanceId];\n                if (car) {\n                    car.pauseTimer = duration;\n                    lib.log('CarManager: Paused car ' + instanceId + ' for ' + duration + 's');\n                }\n            }\n\n            init();\n\n            return { init, update, removeCar, isActiveCar, pauseCar };\n        })();\n\n        // ============================================================\n        // OBSTRUCTION FADE SYSTEM\n        // ============================================================\n        const ObstructionFadeSystem = (() => {\n            const eligibleIds = [];\n            const fadeStates = {}; // instanceId -> { current, target }\n            const FADE_SPEED = 3.5; // opacity units per second\n\n            // Pre-allocated vectors to avoid GC pressure\n            const _holePos = new THREE.Vector3();\n            const _toHole = new THREE.Vector3();\n            const _dir = new THREE.Vector3();\n            const _camToObj = new THREE.Vector3();\n            const _objCenter = new THREE.Vector3();\n            const _projPoint = new THREE.Vector3();\n\n            function isGhostResidueMesh(mesh) {\n                return !!(mesh && mesh.userData && mesh.userData.isGhostResidue);\n            }\n\n            function init() {\n                eligibleIds.length = 0;\n                const registry = config.objectRegistry || [];\n                const instances = config.objectInstances || {};\n\n                Object.keys(instances).forEach(id => {\n                    const inst = instances[id];\n                    if (!inst.registryId) return;\n                    const entry = registry.find(e => e.id === inst.registryId);\n                    if (entry && getRegistryCapability(entry, 'supportsObstructionFade')) {\n                        eligibleIds.push(id);\n                        fadeStates[id] = { current: 1, target: 1 };\n                    }\n                });\n            }\n\n            function update(dt) {\n                if (eligibleIds.length === 0) return;\n\n                const fadeOpacity = config.obstructionFadeOpacity !== undefined ? config.obstructionFadeOpacity : 0.25;\n                const padding = config.obstructionPadding !== undefined ? config.obstructionPadding : 3.0;\n\n                const camPos = camera.position;\n                const playerHole = HoleManager.getPlayerHole();\n                const playerRadius = playerHole.state.radius;\n\n                const camToPlayerX = playerHole.state.x - camPos.x;\n                const camToPlayerZ = playerHole.state.z - camPos.z;\n                const camToPlayerLen = Math.sqrt(camToPlayerX * camToPlayerX + camToPlayerZ * camToPlayerZ);\n                const camDirX = camToPlayerX / camToPlayerLen;\n                const camDirZ = camToPlayerZ / camToPlayerLen;\n\n                for (let i = 0; i < eligibleIds.length; i++) {\n                    const id = eligibleIds[i];\n                    const mesh = worldObjects[id];\n                    if (!mesh || !mesh.visible || isGhostResidueMesh(mesh)) continue;\n\n                    const inst = config.objectInstances[id];\n                    normalizeConsumeState(inst);\n                    if (!inst || inst.consumedByPlayer) {\n                        if (fadeStates[id]) fadeStates[id].target = 1;\n                        continue;\n                    }\n\n                    if (!fadeStates[id]) fadeStates[id] = { current: 1, target: 1 };\n\n                    const scale = inst.scale || 1;\n                    const buildingRadius = scale * 3.5; // approximate half-width of building\n\n                    // Hole overlap/near-contact: keep fully visible so consumption reads clearly\n                    const dxPlayer = mesh.position.x - playerHole.state.x;\n                    const dzPlayer = mesh.position.z - playerHole.state.z;\n                    const distToPlayer = Math.sqrt(dxPlayer * dxPlayer + dzPlayer * dzPlayer);\n                    const overlapThreshold = playerRadius + buildingRadius * 0.6;\n\n                    if (distToPlayer < overlapThreshold) {\n                        fadeStates[id].target = 1;\n                        continue;\n                    }\n\n                    // --- Camera \u2192 hole line-of-sight obstruction ---\n                    const camToObjX = mesh.position.x - camPos.x;\n                    const camToObjZ = mesh.position.z - camPos.z;\n                    const camToObjLen = Math.sqrt(camToObjX * camToObjX + camToObjZ * camToObjZ);\n\n                    if (camToObjLen < 1 || camToPlayerLen < 1) {\n                        fadeStates[id].target = 1;\n                        continue;\n                    }\n\n                    // Project building onto the camera\u2192hole ray; only fade if it lies in front of the hole\n                    const projDist = camToObjX * camDirX + camToObjZ * camDirZ;\n                    const holeNearEdgeDist = camToPlayerLen - playerRadius;\n                    if (projDist < 1 || projDist > holeNearEdgeDist + padding * 0.25) {\n                        fadeStates[id].target = 1;\n                        continue;\n                    }\n\n                    // Lateral distance from the camera\u2192hole ray\n                    const perpDistSq = Math.max(0, camToObjLen * camToObjLen - projDist * projDist);\n                    const perpDist = Math.sqrt(perpDistSq);\n\n                    // Corridor half-width scales with depth so it matches the hole's screen footprint\n                    const corridorHalfWidth = (playerRadius + padding) * (projDist / camToPlayerLen) + buildingRadius * 0.5;\n\n                    if (perpDist < corridorHalfWidth) {\n                        fadeStates[id].target = fadeOpacity;\n                    } else {\n                        fadeStates[id].target = 1;\n                    }\n                }\n\n                // Smoothly interpolate and apply opacity\n                for (let i = 0; i < eligibleIds.length; i++) {\n                    const id = eligibleIds[i];\n                    const state = fadeStates[id];\n                    if (!state) continue;\n                    const mesh = worldObjects[id];\n                    if (!mesh || !mesh.visible || isGhostResidueMesh(mesh)) continue;\n\n                    if (Math.abs(state.target - state.current) < 0.005) {\n                        state.current = state.target;\n                    } else {\n                        const step = FADE_SPEED * dt;\n                        if (state.target < state.current) {\n                            state.current = Math.max(state.target, state.current - step);\n                        } else {\n                            state.current = Math.min(state.target, state.current + step);\n                        }\n                    }\n\n                    const isTransparent = state.current < 0.99;\n                    mesh.traverse(child => {\n                        if (child.isMesh && child.material) {\n                            child.material.transparent = isTransparent;\n                            child.material.opacity = state.current;\n                            child.material.depthWrite = !isTransparent;\n                        }\n                    });\n                }\n            }\n\n            init();\n\n            return { update, init };\n        })();\n\n        const keys = {};\n\n        const MOVE_KEYS = ['w', 'a', 's', 'd', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'];\n\n        function onKeyDown(e) {\n            if (mode === 'edit') return;\n            const state = GameStateManager.getState();\n            if (state === GAME_STATE_READY) {\n                if (e.key === ' ' || e.key === 'Enter') {\n                    e.preventDefault();\n                    GameStateManager.beginRun();\n                }\n                return;\n            }\n            if (state !== GAME_STATE_PLAYING) return;\n            AudioSystem.unlock();\n            const k = e.key.toLowerCase();\n            if (MOVE_KEYS.includes(k)) {\n                e.preventDefault();\n                keys[k] = true;\n            }\n        }\n\n        function onKeyUp(e) {\n            keys[e.key.toLowerCase()] = false;\n        }\n\n        function onWindowBlur() {\n            Object.keys(keys).forEach(k => keys[k] = false);\n            resetJoystick();\n        }\n\n        window.addEventListener('keydown', onKeyDown);\n        window.addEventListener('keyup', onKeyUp);\n        window.addEventListener('blur', onWindowBlur);\n\n        function getKeyboardInput() {\n            let inputX = 0;\n            let inputZ = 0;\n\n            if (keys['w'] || keys['arrowup'])    inputZ -= 1;\n            if (keys['s'] || keys['arrowdown'])  inputZ += 1;\n            if (keys['a'] || keys['arrowleft'])  inputX -= 1;\n            if (keys['d'] || keys['arrowright']) inputX += 1;\n\n            const magnitude = Math.sqrt(inputX * inputX + inputZ * inputZ);\n            if (magnitude > 0) {\n                inputX /= magnitude;\n                inputZ /= magnitude;\n            }\n\n            return { x: inputX, z: inputZ };\n        }\n\n\n        const JOYSTICK_MAX_RADIUS = 120;\n        const JOYSTICK_DEAD_ZONE = 15;\n\n        function shouldUseTouchControls() {\n            if (typeof lib.shouldUseTouchControls === 'function') {\n                try {\n                    return !!lib.shouldUseTouchControls();\n                } catch (e) { /* host may not implement */ }\n            }\n            return window.matchMedia('(hover: none) and (pointer: coarse)').matches;\n        }\n\n        let joystickActive = false;\n        let joystickTouchId = null;\n        let joystickBaseX = 0;\n        let joystickBaseY = 0;\n        let joystickDX = 0;\n        let joystickDZ = 0;\n\n        function getJoystickDefaultPosition() {\n            return { x: gameWorld.clientWidth / 2, y: gameWorld.clientHeight - 190 };\n        }\n\n        function showJoystickAtRest() {\n            if (!shouldUseTouchControls()) {\n                joystickOuter.style.display = 'none';\n                return;\n            }\n            const def = getJoystickDefaultPosition();\n            joystickOuter.style.display = 'block';\n            joystickOuter.classList.remove('no-transition');\n            joystickOuter.style.left = def.x + 'px';\n            joystickOuter.style.top = def.y + 'px';\n            joystickInner.classList.remove('no-transition');\n            centerJoystickKnob();\n        }\n\n        function resetJoystick() {\n            joystickActive = false;\n            joystickTouchId = null;\n            joystickDX = 0;\n            joystickDZ = 0;\n            if (!shouldUseTouchControls()) {\n                joystickOuter.style.display = 'none';\n                return;\n            }\n            showJoystickAtRest();\n        }\n\n        const JOYSTICK_EDGE_PADDING = 60;\n        function positionJoystickBase(screenX, screenY) {\n            const minX = JOYSTICK_MAX_RADIUS + JOYSTICK_EDGE_PADDING;\n            const maxX = gameWorld.clientWidth  - JOYSTICK_MAX_RADIUS - JOYSTICK_EDGE_PADDING;\n            const minY = JOYSTICK_MAX_RADIUS + JOYSTICK_EDGE_PADDING;\n            const maxY = gameWorld.clientHeight - JOYSTICK_MAX_RADIUS - JOYSTICK_EDGE_PADDING;\n            joystickBaseX = Math.max(minX, Math.min(maxX, screenX));\n            joystickBaseY = Math.max(minY, Math.min(maxY, screenY));\n            joystickOuter.style.left = joystickBaseX + 'px';\n            joystickOuter.style.top  = joystickBaseY + 'px';\n        }\n\n        function centerJoystickKnob() {\n            joystickInner.style.left = '50%';\n            joystickInner.style.top = '50%';\n        }\n\n        function updateJoystickFromOffset(dx, dy, dist) {\n            joystickInner.classList.add('no-transition');\n            if (dist < JOYSTICK_DEAD_ZONE) {\n                joystickDX = 0;\n                joystickDZ = 0;\n                centerJoystickKnob();\n            } else {\n                joystickDX = dx / JOYSTICK_MAX_RADIUS;\n                joystickDZ = dy / JOYSTICK_MAX_RADIUS;\n                const knobPercentX = 50 + (dx / JOYSTICK_MAX_RADIUS) * 50;\n                const knobPercentY = 50 + (dy / JOYSTICK_MAX_RADIUS) * 50;\n                joystickInner.style.left = knobPercentX + '%';\n                joystickInner.style.top = knobPercentY + '%';\n            }\n        }\n\n        function onTouchStart(e) {\n            if (mode === 'edit') return;\n            if (!shouldUseTouchControls()) return;\n            if (GameStateManager.getState() !== GAME_STATE_PLAYING) return;\n            if (isVictoryActive) return;\n            e.preventDefault();\n            if (joystickActive) return;\n\n            const touch = e.changedTouches[0];\n            joystickTouchId = touch.identifier;\n            joystickActive = true;\n\n            joystickOuter.classList.add('no-transition');\n            joystickInner.classList.add('no-transition');\n            positionJoystickBase(touch.clientX, touch.clientY);\n            joystickOuter.style.display = 'block';\n            centerJoystickKnob();\n            joystickDX = 0;\n            joystickDZ = 0;\n        }\n\n        function onTouchMove(e) {\n            if (mode === 'edit') return;\n            if (GameStateManager.getState() !== GAME_STATE_PLAYING) return;\n            e.preventDefault();\n            if (!joystickActive) return;\n\n            for (let i = 0; i < e.changedTouches.length; i++) {\n                const touch = e.changedTouches[i];\n                if (touch.identifier !== joystickTouchId) continue;\n\n                let dx = touch.clientX - joystickBaseX;\n                let dy = touch.clientY - joystickBaseY;\n                let dist = Math.sqrt(dx * dx + dy * dy);\n\n                if (dist > JOYSTICK_MAX_RADIUS) {\n                    const excess = dist - JOYSTICK_MAX_RADIUS;\n                    const angle = Math.atan2(dy, dx);\n                    positionJoystickBase(\n                        joystickBaseX + Math.cos(angle) * excess,\n                        joystickBaseY + Math.sin(angle) * excess\n                    );\n                    dx = touch.clientX - joystickBaseX;\n                    dy = touch.clientY - joystickBaseY;\n                    dist = JOYSTICK_MAX_RADIUS;\n                }\n\n                updateJoystickFromOffset(dx, dy, dist);\n            }\n        }\n\n        function onTouchEnd(e) {\n            if (!joystickActive) return;\n            for (let i = 0; i < e.changedTouches.length; i++) {\n                if (e.changedTouches[i].identifier === joystickTouchId) {\n                    resetJoystick();\n                    break;\n                }\n            }\n        }\n\n        let mouseJoystickActive = false;\n        let mouseJoystickStartX = 0;\n        let mouseJoystickStartY = 0;\n\n        function onMouseDownJoystick(e) {\n            if (mode === 'edit') return;\n            if (GameStateManager.getState() !== GAME_STATE_PLAYING) return;\n            if (isVictoryActive) return;\n            if (e.button !== 0) return;\n            e.preventDefault();\n            mouseJoystickActive = true;\n            joystickOuter.classList.add('no-transition');\n            joystickInner.classList.add('no-transition');\n            positionJoystickBase(e.clientX, e.clientY);\n            joystickActive = true;\n            joystickDX = 0;\n            joystickDZ = 0;\n            centerJoystickKnob();\n        }\n\n        function onMouseMoveJoystick(e) {\n            if (!mouseJoystickActive) return;\n            let dx = e.clientX - joystickBaseX;\n            let dy = e.clientY - joystickBaseY;\n            let dist = Math.sqrt(dx * dx + dy * dy);\n            if (dist > JOYSTICK_MAX_RADIUS) {\n                const excess = dist - JOYSTICK_MAX_RADIUS;\n                const angle = Math.atan2(dy, dx);\n                positionJoystickBase(\n                    joystickBaseX + Math.cos(angle) * excess,\n                    joystickBaseY + Math.sin(angle) * excess\n                );\n                dx = e.clientX - joystickBaseX;\n                dy = e.clientY - joystickBaseY;\n                dist = JOYSTICK_MAX_RADIUS;\n            }\n            updateJoystickFromOffset(dx, dy, dist);\n        }\n\n        function onMouseUpJoystick(e) {\n            if (!mouseJoystickActive) return;\n            mouseJoystickActive = false;\n            joystickActive = false;\n            joystickDX = 0;\n            joystickDZ = 0;\n            resetJoystick();\n        }\n\n        if (mode === 'play' && shouldUseTouchControls()) {\n            gameWorld.addEventListener('mousedown', onMouseDownJoystick);\n            window.addEventListener('mousemove', onMouseMoveJoystick);\n            window.addEventListener('mouseup', onMouseUpJoystick);\n        }\n\n        if (mode === 'play' && shouldUseTouchControls()) {\n            setTimeout(() => {\n                if (GameStateManager.getState() === GAME_STATE_PLAYING) showJoystickAtRest();\n            }, 50);\n        }\n\n        const stopPropagation = (e) => e.stopPropagation();\n        const screenVictory = document.getElementById('screen-victory');\n        const screenDefeat = document.getElementById('screen-defeat');\n        const musicToggleBtn = document.getElementById('music-toggle-btn');\n        \n        if (musicToggleBtn) {\n            musicToggleBtn.addEventListener('click', (e) => {\n                e.stopPropagation();\n                AudioSystem.unlock();\n                AudioSystem.toggleMusic();\n            });\n            ['touchstart', 'touchmove', 'touchend', 'mousedown', 'mousemove', 'mouseup', 'click'].forEach(evt => {\n                musicToggleBtn.addEventListener(evt, stopPropagation, { passive: evt.startsWith('touch') });\n            });\n        }\n        \n        if (screenVictory) {\n            ['touchstart', 'touchmove', 'touchend', 'touchcancel', 'mousedown', 'mousemove', 'mouseup', 'click'].forEach(evt => {\n                screenVictory.addEventListener(evt, stopPropagation, { passive: evt.startsWith('touch') });\n            });\n        }\n        if (screenDefeat) {\n            ['touchstart', 'touchmove', 'touchend', 'touchcancel', 'mousedown', 'mousemove', 'mouseup', 'click'].forEach(evt => {\n                screenDefeat.addEventListener(evt, stopPropagation, { passive: evt.startsWith('touch') });\n            });\n        }\n\n        gameWorld.addEventListener('touchstart', (e) => {\n            AudioSystem.unlock();\n            if (GameStateManager.getState() === GAME_STATE_READY) {\n                GameStateManager.beginRun();\n                return;\n            }\n            onTouchStart(e);\n        }, { passive: false });\n        gameWorld.addEventListener('touchmove', onTouchMove, { passive: false });\n        gameWorld.addEventListener('touchend', onTouchEnd);\n        gameWorld.addEventListener('touchcancel', onTouchEnd);\n\n\n        const _ecH   = (config.levelCameraHeights && config.levelCameraHeights[0] != null)\n                        ? config.levelCameraHeights[0] : (config.cameraHeight || 19);\n        const _ecTiltDeg = (config.levelCameraTilts && config.levelCameraTilts[0] != null)\n                        ? config.levelCameraTilts[0] : (config.cameraAngle || 40);\n        const _ecTiltRad = _ecTiltDeg * Math.PI / 180;\n        const _ecDist = _ecH / Math.sin(_ecTiltRad);\n        const _ecAzimuth = (config.cameraRotation || 0) * Math.PI / 180;\n\n        const editCamera = {\n            target: new THREE.Vector3(0, 0, 0),\n            distance: _ecDist,\n            azimuth: _ecAzimuth,\n            elevation: _ecTiltRad,\n            minDistance: 5,\n            maxDistance: 400,\n            minElevation: 0.1,\n            maxElevation: Math.PI / 2 - 0.02,\n            panSpeed: 0.5,\n            rotateSpeed: 0.005,\n            zoomFactor: 1.12\n        };\n\n        let selectedObj = null;\n        let dragging = false;\n        let dragStartX = 0;\n        let dragStartZ = 0;\n\n        let editInteraction = 'none';\n        let editPointerDown = false;\n        let editLastX = 0;\n        let editLastY = 0;\n\n        let editTouches = [];\n        let editPinchStartDist = 0;\n        let editPinchStartAngle = 0;\n        let editTwoFingerStarted = false;\n\n        const dragPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);\n        const raycaster = new THREE.Raycaster();\n        const mouseNDC = new THREE.Vector2();\n\n        function screenToNDC(clientX, clientY) {\n            mouseNDC.x = (clientX / gameWorld.clientWidth) * 2 - 1;\n            mouseNDC.y = -(clientY / gameWorld.clientHeight) * 2 + 1;\n        }\n\n        function updateEditCamera() {\n            const { target, distance, azimuth, elevation } = editCamera;\n            const x = target.x + distance * Math.cos(elevation) * Math.sin(azimuth);\n            const y = target.y + distance * Math.sin(elevation);\n            const z = target.z + distance * Math.cos(elevation) * Math.cos(azimuth);\n            camera.position.set(x, y, z);\n            camera.lookAt(target);\n        }\n\n        function initEditCamera() {\n            updateEditCamera();\n        }\n\n        function swapObjectRegistryType(instanceId, newRegistryId, isUndo = false) {\n            const inst = config.objectInstances[instanceId];\n            if (!inst) return;\n\n            const oldRegistryId = inst.registryId;\n            if (oldRegistryId === newRegistryId) return;\n\n            if (!isUndo) {\n                UndoSystem.push({ type: 'object-swap', instanceId: instanceId, oldRegistryId: oldRegistryId });\n            }\n\n            // 1. Clear old footprint in cell occupancy\n            clearObjectFootprint(instanceId);\n\n            // 2. Fetch new registry entry and its defaults\n            const newEntry = getRegistryEntry(newRegistryId);\n            const defaults = getDefaultsForType(newRegistryId);\n\n            // Keep track of preserved properties:\n            const preservedX = inst.x;\n            const preservedZ = inst.z;\n            const preservedScale = inst.scale;\n            const preservedRotationY = inst.rotationY;\n\n            // 3. Update the instance config object with new defaults while preserving keys\n            inst.registryId = newRegistryId;\n            inst.w = defaults.w;\n            inst.h = defaults.h;\n            inst.d = defaults.d;\n            inst.color = defaults.color;\n\n            // 4. Handle grid snapping and footprint calculation for the new type\n            const wCells = (newEntry && newEntry.widthInCells) || 1;\n            const hCells = (newEntry && newEntry.heightInCells) || 1;\n\n            let finalX = preservedX;\n            let finalZ = preservedZ;\n            let anchorCol, anchorRow;\n\n            if (wCells > 1 || hCells > 1) {\n                const { col, row } = worldToCell(finalX, finalZ);\n                anchorCol = col;\n                anchorRow = row;\n                \n                const fp = getObjectEffectiveFootprint(newRegistryId, preservedRotationY || 0);\n                if (canPlaceFootprint(anchorCol, anchorRow, fp.w, fp.h)) {\n                    const snapped = getObjectFootprintWorldPosition(anchorCol, anchorRow, fp.w, fp.h);\n                    finalX = snapped.x;\n                    finalZ = snapped.z;\n                } else {\n                    anchorCol = Math.max(0, Math.min(TERRAIN_COLS - fp.w, anchorCol));\n                    anchorRow = Math.max(0, Math.min(TERRAIN_ROWS - fp.h, anchorRow));\n                    const snapped = getObjectFootprintWorldPosition(anchorCol, anchorRow, fp.w, fp.h);\n                    finalX = snapped.x;\n                    finalZ = snapped.z;\n                }\n                inst.anchorCol = anchorCol;\n                inst.anchorRow = anchorRow;\n                inst.isPrimary = true;\n\n                // Set new cell occupancy\n                const footprintKeys = getObjectFootprintCellKeys(anchorCol, anchorRow, fp.w, fp.h);\n                footprintKeys.forEach(k => {\n                    const existingId = resolvePrimaryObjectInstance(k);\n                    if (existingId && existingId !== instanceId) {\n                        const existingMesh = worldObjects[existingId];\n                        if (existingMesh) scene.remove(existingMesh);\n                        delete worldObjects[existingId];\n                        delete config.objectInstances[existingId];\n                        clearObjectFootprint(existingId);\n                    }\n                });\n\n                footprintKeys.forEach(k => {\n                    config.objectCellOccupancy[k] = instanceId;\n                });\n            } else {\n                const snapped = applySnapToGrid(newRegistryId, finalX, finalZ);\n                finalX = snapped.x;\n                finalZ = snapped.z;\n\n                const { col, row } = worldToCell(finalX, finalZ);\n                anchorCol = col;\n                anchorRow = row;\n                inst.anchorCol = anchorCol;\n                inst.anchorRow = anchorRow;\n\n                const existingId = resolvePrimaryObjectInstance(cellKey(col, row));\n                if (existingId && existingId !== instanceId) {\n                    const existingMesh = worldObjects[existingId];\n                    if (existingMesh) scene.remove(existingMesh);\n                    delete worldObjects[existingId];\n                    delete config.objectInstances[existingId];\n                    clearObjectFootprint(existingId);\n                }\n\n                config.objectCellOccupancy[cellKey(col, row)] = instanceId;\n            }\n\n            inst.x = Math.round(finalX * 100) / 100;\n            inst.z = Math.round(finalZ * 100) / 100;\n\n            // 5. Remove the old Three.js group from the scene\n            const oldGroup = worldObjects[instanceId];\n            if (oldGroup) {\n                scene.remove(oldGroup);\n                oldGroup.traverse(child => {\n                    if (child.isMesh) {\n                        if (child.geometry) child.geometry.dispose();\n                        if (child.material) {\n                            if (Array.isArray(child.material)) {\n                                child.material.forEach(m => m.dispose());\n                            } else {\n                                child.material.dispose();\n                            }\n                        }\n                    }\n                });\n            }\n\n            // 6. Create the new Three.js model\n            const modelType = getModelTypeForInstance(inst);\n            const newGroup = ObjectFactory.create(modelType, inst);\n            newGroup.position.set(inst.x, 0, inst.z);\n            if (inst.scale) newGroup.scale.setScalar(inst.scale);\n            if (inst.rotationY) newGroup.rotation.y = inst.rotationY;\n            newGroup.userData.instanceId = instanceId;\n            scene.add(newGroup);\n            worldObjects[instanceId] = newGroup;\n\n            selectObject(newGroup);\n\n            CarManager.init();\n            ObstructionFadeSystem.init();\n\n            calculateTotalAvailablePoints();\n            updateGoalHUD();\n\n            lib.log('Swapped object instance ' + instanceId + ' from ' + oldRegistryId + ' to ' + newRegistryId);\n        }\n\n        function deselectObject() {\n            if (selectedObj) {\n                selectedObj.traverse(child => {\n                    if (child.isMesh && child.material) {\n                        child.material.emissive = new THREE.Color(0x000000);\n                    }\n                });\n                selectedObj = null;\n                lib.editMenu.close();\n                \n                const propEmpty = document.getElementById('prop-empty');\n                const propContent = document.getElementById('prop-content');\n                const propTitle = document.getElementById('prop-title');\n                if (propTitle) propTitle.textContent = 'Properties';\n                if (propEmpty) propEmpty.style.display = 'block';\n                if (propContent) propContent.style.display = 'none';\n            }\n        }\n\n        function selectObject(group) {\n            deselectObject();\n            selectedObj = group;\n            selectedObj.traverse(child => {\n                if (child.isMesh && child.material) {\n                    child.material.emissive = new THREE.Color(0x444400);\n                }\n            });\n\n            const instanceId = group.userData.instanceId;\n            if (instanceId && config.objectInstances[instanceId]) {\n                const inst = config.objectInstances[instanceId];\n                \n                if (inst.scale === undefined) inst.scale = 1;\n                if (inst.rotationY === undefined) inst.rotationY = 0;\n\n                const regEntryForPath = getRegistryEntry(inst.registryId);\n                const canHavePath = regEntryForPath && (getRegistryCapability(regEntryForPath, 'canHavePath') || regEntryForPath.objectCategory === 'vehicle');\n\n                const editMenuParams = {\n                    'X Position': {\n                        key: 'objectInstances.' + instanceId + '.x',\n                        type: 'number',\n                        onChange: (v) => {\n                            config.objectInstances[instanceId].x = v;\n                            group.position.x = v;\n                        }\n                    },\n                    'Z Position': {\n                        key: 'objectInstances.' + instanceId + '.z',\n                        type: 'number',\n                        onChange: (v) => {\n                            config.objectInstances[instanceId].z = v;\n                            group.position.z = v;\n                        }\n                    },\n                    'Scale': {\n                        key: 'objectInstances.' + instanceId + '.scale',\n                        type: 'slider',\n                        min: 0.2,\n                        max: 5,\n                        step: 0.1,\n                        onChange: (v) => {\n                            config.objectInstances[instanceId].scale = v;\n                            group.scale.setScalar(v);\n                        }\n                    },\n                    'Facing': {\n                        key: 'objectInstances.' + instanceId + '.rotationY',\n                        type: 'slider',\n                        min: 0,\n                        max: 6.28,\n                        step: 0.1,\n                        onChange: (v) => {\n                            config.objectInstances[instanceId].rotationY = v;\n                            group.rotation.y = v;\n                        }\n                    }\n                };\n\n                if (canHavePath) {\n                    editMenuParams['Initial Next Waypoint ID'] = {\n                        key: 'objectInstances.' + instanceId + '.initialNextWaypointId',\n                        type: 'number',\n                        onChange: (v) => {\n                            config.objectInstances[instanceId].initialNextWaypointId = v;\n                            const inputEl = document.getElementById('prop-initial-wp-id');\n                            if (inputEl) inputEl.value = (v === undefined || v === null) ? '' : v;\n                        }\n                    };\n                }\n\n                lib.editMenu.open({\n                    name: inst.registryId || instanceId,\n                    params: editMenuParams,\n                    onDelete: () => {\n                        const deletedData = config.objectInstances[instanceId];\n                        if (deletedData) {\n                            UndoSystem.push({ type: 'object-delete', instanceId: instanceId, instanceData: JSON.parse(JSON.stringify(deletedData)) });\n                        }\n                        scene.remove(group);\n                        delete worldObjects[instanceId];\n                        delete config.objectInstances[instanceId];\n                        clearObjectFootprint(instanceId);\n                        deselectObject();\n                    }\n                });\n\n                const propEmpty = document.getElementById('prop-empty');\n                const propContent = document.getElementById('prop-content');\n                const propTitle = document.getElementById('prop-title');\n                const propScale = document.getElementById('prop-scale');\n                const propScaleVal = document.getElementById('prop-scale-val');\n                const propFacing = document.getElementById('prop-facing');\n                const propFacingVal = document.getElementById('prop-facing-val');\n                \n                if (propTitle) {\n                    const entry = getRegistryEntry(inst.registryId);\n                    const name = entry ? (entry.displayName || entry.id) : (inst.registryId || instanceId);\n                    propTitle.textContent = name;\n                    propEmpty.style.display = 'none';\n                    propContent.style.display = 'flex';\n\n                    const propInstanceId = document.getElementById('prop-instance-id');\n                    const propRegistryType = document.getElementById('prop-registry-type');\n                    const propPosVal = document.getElementById('prop-pos-val');\n                    if (propInstanceId) propInstanceId.textContent = instanceId;\n                    if (propRegistryType) propRegistryType.textContent = inst.registryId || '\u2014';\n                    if (propPosVal) propPosVal.textContent = parseFloat(inst.x).toFixed(1) + ', ' + parseFloat(inst.z).toFixed(1);\n\n                    const propSwapSelected = document.getElementById('prop-swap-selected');\n                    const propSwapList = document.getElementById('prop-swap-list');\n                    if (propSwapSelected && propSwapList) {\n                        propSwapList.innerHTML = '';\n                        const registry = config.objectRegistry || [];\n                        const currentEntry = registry.find(e => e.id === inst.registryId);\n                        propSwapSelected.textContent = currentEntry ? (currentEntry.displayName || currentEntry.id) : (inst.registryId || '\u2014');\n                        registry.forEach(entry => {\n                            const div = document.createElement('div');\n                            div.className = 'custom-dropdown-option';\n                            div.dataset.value = entry.id;\n                            div.textContent = entry.displayName || entry.id;\n                            if (entry.id === inst.registryId) div.classList.add('active');\n                            div.addEventListener('click', (ev) => {\n                                ev.stopPropagation();\n                                if (selectedObj && selectedObj.userData.instanceId) {\n                                    const iid = selectedObj.userData.instanceId;\n                                    swapObjectRegistryType(iid, entry.id);\n                                }\n                                document.getElementById('prop-swap-options').classList.remove('open');\n                            });\n                            propSwapList.appendChild(div);\n                        });\n                    }\n\n                    propScale.value = inst.scale;\n                    propScaleVal.textContent = parseFloat(inst.scale).toFixed(1);\n                    \n                    propFacing.value = inst.rotationY;\n                    propFacingVal.textContent = parseFloat(inst.rotationY).toFixed(1);\n\n                    const aiPathSection = document.getElementById('prop-ai-path-section');\n                    const aiPathInfo = document.getElementById('prop-ai-path-info');\n                    if (aiPathSection) {\n                        const regEntry = getRegistryEntry(inst.registryId);\n                        const canHavePath = regEntry && (getRegistryCapability(regEntry, 'canHavePath') || regEntry.objectCategory === 'vehicle');\n                        aiPathSection.style.display = canHavePath ? 'block' : 'none';\n                        if (inst.waypoints && inst.waypoints.length >= 2) {\n                            aiPathInfo.textContent = inst.waypoints.length + ' waypoints (loop)';\n                        } else {\n                            aiPathInfo.textContent = 'No path defined';\n                        }\n                        const inputEl = document.getElementById('prop-initial-wp-id');\n                        if (inputEl) {\n                            inputEl.value = (inst.initialNextWaypointId !== undefined && inst.initialNextWaypointId !== null) ? inst.initialNextWaypointId : '';\n                        }\n                    }\n                }\n            }\n        }\n\n        function onEditPointerDown(e) {\n            if (mode !== 'edit') return;\n            e.preventDefault();\n\n            editPointerDown = true;\n            editLastX = e.clientX;\n            editLastY = e.clientY;\n\n            if (e.button === 2) {\n                editInteraction = 'rotate';\n                return;\n            }\n\n            if (e.button === 1 || (e.button === 0 && e.shiftKey)) {\n                editInteraction = 'pan';\n                return;\n            }\n\n            if (pathEditActive && e.button === 0) {\n                if (handlePathEditPointerDown(e.clientX, e.clientY)) return;\n            }\n\n            if (e.button === 0) {\n                screenToNDC(e.clientX, e.clientY);\n                raycaster.setFromCamera(mouseNDC, camera);\n\n                if (editorTopMode === 'terrain') {\n                    if (terrainToolMode === 'select') {\n                        const intersection = new THREE.Vector3();\n                        const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                        if (hit) {\n                            selectTerrainCell(intersection.x, intersection.z);\n                        } else {\n                            deselectTerrainCell();\n                        }\n                        editInteraction = 'none';\n                    } else {\n                        const intersection = new THREE.Vector3();\n                        const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                        if (hit) {\n                            paintTerrainAtPosition(intersection.x, intersection.z);\n                        }\n                        editInteraction = 'terrain-paint';\n                    }\n                    return;\n                }\n\n                if (editorMode === 'place' && selectedRegistryId) {\n                    const intersection = new THREE.Vector3();\n                    const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                    if (hit) {\n                        const newId = placeObjectInstance(selectedRegistryId, intersection.x, intersection.z);\n                        selectObject(worldObjects[newId]);\n                        updateEditorStatus();\n                    }\n                    editInteraction = 'none';\n                } else if (editorMode === 'delete') {\n                    const hits = raycaster.intersectObjects(getWorldMeshesForRaycast());\n                    if (hits.length > 0) {\n                        const group = getInstanceGroupFromHit(hits[0].object);\n                        if (group) {\n                            const instanceId = group.userData.instanceId;\n                            const deletedData = config.objectInstances[instanceId];\n                            if (deletedData) {\n                                UndoSystem.push({ type: 'object-delete', instanceId: instanceId, instanceData: JSON.parse(JSON.stringify(deletedData)) });\n                            }\n                            scene.remove(group);\n                            delete worldObjects[instanceId];\n                            delete config.objectInstances[instanceId];\n                            clearObjectFootprint(instanceId);\n                            if (selectedObj === group) deselectObject();\n                        }\n                    }\n                    editInteraction = 'none';\n                } else {\n                    const hits = raycaster.intersectObjects(getWorldMeshesForRaycast());\n                    if (hits.length > 0) {\n                        const group = getInstanceGroupFromHit(hits[0].object);\n                        if (group) {\n                            selectObject(group);\n                            editInteraction = 'drag-object';\n                            dragging = true;\n                            dragStartX = group.position.x;\n                            dragStartZ = group.position.z;\n                            updateEditorStatus();\n                        }\n                    } else {\n                        deselectObject();\n                        editInteraction = 'rotate';\n                        updateEditorStatus();\n                    }\n                }\n            }\n        }\n\n        function onEditPointerMove(e) {\n            if (mode !== 'edit' || !editPointerDown) return;\n\n            const dx = e.clientX - editLastX;\n            const dy = e.clientY - editLastY;\n            editLastX = e.clientX;\n            editLastY = e.clientY;\n\n            if (handlePathEditPointerMove(e.clientX, e.clientY)) return;\n\n            if (editInteraction === 'rotate') {\n                editCamera.azimuth -= dx * editCamera.rotateSpeed;\n                editCamera.elevation += dy * editCamera.rotateSpeed;\n                editCamera.elevation = Math.max(editCamera.minElevation, Math.min(editCamera.maxElevation, editCamera.elevation));\n                updateEditCamera();\n            } else if (editInteraction === 'terrain-paint') {\n                screenToNDC(e.clientX, e.clientY);\n                raycaster.setFromCamera(mouseNDC, camera);\n                const intersection = new THREE.Vector3();\n                const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                if (hit) {\n                    paintTerrainAtPosition(intersection.x, intersection.z);\n                    terrainCursor.show(intersection.x, intersection.z);\n                }\n            } else if (editInteraction === 'pan') {\n                const panScale = editCamera.distance * editCamera.panSpeed * 0.002;\n                const right = new THREE.Vector3();\n                const forward = new THREE.Vector3();\n                camera.getWorldDirection(forward);\n                right.crossVectors(forward, camera.up).normalize();\n                const flatForward = new THREE.Vector3(forward.x, 0, forward.z).normalize();\n\n                editCamera.target.addScaledVector(right, -dx * panScale);\n                editCamera.target.addScaledVector(flatForward, dy * panScale);\n                updateEditCamera();\n            } else if (editInteraction === 'drag-object' && selectedObj && dragging) {\n                screenToNDC(e.clientX, e.clientY);\n                raycaster.setFromCamera(mouseNDC, camera);\n                const intersection = new THREE.Vector3();\n                raycaster.ray.intersectPlane(dragPlane, intersection);\n\n                if (intersection) {\n                    const instanceId = selectedObj.userData.instanceId;\n                    let px = intersection.x;\n                    let pz = intersection.z;\n                    if (instanceId && config.objectInstances[instanceId]) {\n                        const inst = config.objectInstances[instanceId];\n                        const regId = inst.registryId;\n                        if (regId) {\n                            const entry = getRegistryEntry(regId);\n                            const wCells = (entry && entry.widthInCells) || 1;\n                            const hCells = (entry && entry.heightInCells) || 1;\n                            if (wCells > 1 || hCells > 1) {\n                                const { col, row } = worldToCell(px, pz);\n                                const fp = getObjectEffectiveFootprint(regId, inst.rotationY || 0);\n                                if (canPlaceFootprint(col, row, fp.w, fp.h)) {\n                                    const snapped = getObjectFootprintWorldPosition(col, row, fp.w, fp.h);\n                                    px = snapped.x;\n                                    pz = snapped.z;\n                                    inst.anchorCol = col;\n                                    inst.anchorRow = row;\n                                    \n                                    // Update objectCellOccupancy\n                                    clearObjectFootprint(instanceId);\n                                    const keys = getObjectFootprintCellKeys(col, row, fp.w, fp.h);\n                                    keys.forEach(k => {\n                                        config.objectCellOccupancy[k] = instanceId;\n                                    });\n                                }\n                            } else {\n                                const s = applySnapToGrid(regId, px, pz);\n                                px = s.x; pz = s.z;\n                                const { col, row } = worldToCell(px, pz);\n                                inst.anchorCol = col;\n                                inst.anchorRow = row;\n\n                                clearObjectFootprint(instanceId);\n                                config.objectCellOccupancy[cellKey(col, row)] = instanceId;\n                            }\n                        }\n                        selectedObj.position.x = px;\n                        selectedObj.position.z = pz;\n                        inst.x = px;\n                        inst.z = pz;\n                        const propPosVal = document.getElementById('prop-pos-val');\n                        if (propPosVal) propPosVal.textContent = px.toFixed(1) + ', ' + pz.toFixed(1);\n                    } else {\n                        selectedObj.position.x = px;\n                        selectedObj.position.z = pz;\n                    }\n                }\n            }\n        }\n\n        function onEditPointerUp(e) {\n            if (handlePathEditPointerUp()) {\n                editPointerDown = false;\n                return;\n            }\n            if (editInteraction === 'drag-object' && selectedObj && selectedObj.userData.instanceId) {\n                const id = selectedObj.userData.instanceId;\n                const newX = selectedObj.position.x;\n                const newZ = selectedObj.position.z;\n                if (Math.abs(newX - dragStartX) > 0.01 || Math.abs(newZ - dragStartZ) > 0.01) {\n                    UndoSystem.push({ type: 'object-move', instanceId: id, oldX: dragStartX, oldZ: dragStartZ });\n                }\n            }\n            editPointerDown = false;\n            dragging = false;\n            editInteraction = 'none';\n        }\n\n        function onEditWheel(e) {\n            if (mode !== 'edit') return;\n            e.preventDefault();\n            if (e.deltaY > 0) {\n                editCamera.distance = Math.min(editCamera.maxDistance, editCamera.distance * editCamera.zoomFactor);\n            } else {\n                editCamera.distance = Math.max(editCamera.minDistance, editCamera.distance / editCamera.zoomFactor);\n            }\n            updateEditCamera();\n        }\n\n        function onEditContextMenu(e) {\n            if (mode === 'edit') e.preventDefault();\n        }\n\n        function getEditTouchById(id) {\n            return editTouches.find(t => t.id === id);\n        }\n\n        function twoFingerDist() {\n            if (editTouches.length < 2) return 0;\n            const dx = editTouches[1].x - editTouches[0].x;\n            const dy = editTouches[1].y - editTouches[0].y;\n            return Math.sqrt(dx * dx + dy * dy);\n        }\n\n        function twoFingerAngle() {\n            if (editTouches.length < 2) return 0;\n            return Math.atan2(editTouches[1].y - editTouches[0].y, editTouches[1].x - editTouches[0].x);\n        }\n\n        function twoFingerCenter() {\n            if (editTouches.length < 2) return { x: 0, y: 0 };\n            return {\n                x: (editTouches[0].x + editTouches[1].x) / 2,\n                y: (editTouches[0].y + editTouches[1].y) / 2\n            };\n        }\n\n        function onEditTouchStart(e) {\n            if (mode !== 'edit') return;\n            e.preventDefault();\n\n            for (let i = 0; i < e.changedTouches.length; i++) {\n                const t = e.changedTouches[i];\n                editTouches.push({ id: t.identifier, x: t.clientX, y: t.clientY });\n            }\n\n            if (editTouches.length === 2) {\n                editTwoFingerStarted = true;\n                editPinchStartDist = twoFingerDist();\n                editPinchStartAngle = twoFingerAngle();\n                editLastX = twoFingerCenter().x;\n                editLastY = twoFingerCenter().y;\n                dragging = false;\n                editInteraction = 'none';\n            } else if (editTouches.length === 1) {\n                const t = editTouches[0];\n                screenToNDC(t.x, t.y);\n                raycaster.setFromCamera(mouseNDC, camera);\n\n                if (pathEditActive) {\n                    if (handlePathEditPointerDown(t.x, t.y)) {\n                        editLastX = t.x;\n                        editLastY = t.y;\n                        return;\n                    }\n                }\n\n                if (editorTopMode === 'terrain') {\n                    if (terrainToolMode === 'select') {\n                        const intersection = new THREE.Vector3();\n                        const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                        if (hit) {\n                            selectTerrainCell(intersection.x, intersection.z);\n                        } else {\n                            deselectTerrainCell();\n                        }\n                        editInteraction = 'none';\n                    } else {\n                        const intersection = new THREE.Vector3();\n                        const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                        if (hit) {\n                            paintTerrainAtPosition(intersection.x, intersection.z);\n                            terrainCursor.show(intersection.x, intersection.z);\n                        }\n                        editInteraction = 'terrain-paint';\n                    }\n                    editLastX = t.x;\n                    editLastY = t.y;\n                    return;\n                }\n\n                if (editorMode === 'place' && selectedRegistryId) {\n                    const intersection = new THREE.Vector3();\n                    const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                    if (hit) {\n                        const newId = placeObjectInstance(selectedRegistryId, intersection.x, intersection.z);\n                        selectObject(worldObjects[newId]);\n                        updateEditorStatus();\n                    }\n                    editInteraction = 'none';\n                } else if (editorMode === 'delete') {\n                    const hits = raycaster.intersectObjects(getWorldMeshesForRaycast());\n                    if (hits.length > 0) {\n                        const group = getInstanceGroupFromHit(hits[0].object);\n                        if (group) {\n                            const instanceId = group.userData.instanceId;\n                            const deletedData = config.objectInstances[instanceId];\n                            if (deletedData) {\n                                UndoSystem.push({ type: 'object-delete', instanceId: instanceId, instanceData: JSON.parse(JSON.stringify(deletedData)) });\n                            }\n                            scene.remove(group);\n                            delete worldObjects[instanceId];\n                            delete config.objectInstances[instanceId];\n                            clearObjectFootprint(instanceId);\n                            if (selectedObj === group) deselectObject();\n                        }\n                    }\n                    editInteraction = 'none';\n                } else {\n                    const hits = raycaster.intersectObjects(getWorldMeshesForRaycast());\n                    if (hits.length > 0) {\n                        const group = getInstanceGroupFromHit(hits[0].object);\n                        if (group) {\n                            selectObject(group);\n                            editInteraction = 'drag-object';\n                            dragging = true;\n                            dragStartX = group.position.x;\n                            dragStartZ = group.position.z;\n                            updateEditorStatus();\n                        }\n                    } else {\n                        deselectObject();\n                        editInteraction = 'none';\n                        updateEditorStatus();\n                    }\n                }\n                editLastX = t.x;\n                editLastY = t.y;\n            }\n        }\n\n        function onEditTouchMove(e) {\n            if (mode !== 'edit') return;\n            e.preventDefault();\n\n            for (let i = 0; i < e.changedTouches.length; i++) {\n                const t = e.changedTouches[i];\n                const stored = getEditTouchById(t.identifier);\n                if (stored) {\n                    stored.x = t.clientX;\n                    stored.y = t.clientY;\n                }\n            }\n\n            if (editTouches.length === 2 && editTwoFingerStarted) {\n                const currentDist = twoFingerDist();\n                const currentAngle = twoFingerAngle();\n                const center = twoFingerCenter();\n\n                if (editPinchStartDist > 0) {\n                    const ratio = editPinchStartDist / currentDist;\n                    editCamera.distance = Math.max(editCamera.minDistance,\n                        Math.min(editCamera.maxDistance, editCamera.distance * (1 + (ratio - 1) * 0.3)));\n                    editPinchStartDist = currentDist;\n                }\n\n                const angleDelta = currentAngle - editPinchStartAngle;\n                if (Math.abs(angleDelta) > 0.005) {\n                    editCamera.azimuth += angleDelta * 0.8;\n                    editPinchStartAngle = currentAngle;\n                }\n\n                const panDx = center.x - editLastX;\n                const panDy = center.y - editLastY;\n                if (Math.abs(panDx) > 1 || Math.abs(panDy) > 1) {\n                    const panScale = editCamera.distance * editCamera.panSpeed * 0.003;\n                    const right = new THREE.Vector3();\n                    const forward = new THREE.Vector3();\n                    camera.getWorldDirection(forward);\n                    right.crossVectors(forward, camera.up).normalize();\n                    const flatForward = new THREE.Vector3(forward.x, 0, forward.z).normalize();\n                    editCamera.target.addScaledVector(right, -panDx * panScale);\n                    editCamera.target.addScaledVector(flatForward, panDy * panScale);\n                }\n\n                editLastX = center.x;\n                editLastY = center.y;\n                updateEditCamera();\n\n            } else if (editTouches.length === 1) {\n                const t = editTouches[0];\n                const dx = t.x - editLastX;\n                const dy = t.y - editLastY;\n\n                if (editInteraction === 'path-drag-waypoint') {\n                    handlePathEditPointerMove(t.x, t.y);\n                } else if (editInteraction === 'terrain-paint') {\n                    screenToNDC(t.x, t.y);\n                    raycaster.setFromCamera(mouseNDC, camera);\n                    const intersection = new THREE.Vector3();\n                    const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                    if (hit) {\n                        paintTerrainAtPosition(intersection.x, intersection.z);\n                        terrainCursor.show(intersection.x, intersection.z);\n                    }\n                } else if (editInteraction === 'drag-object' && selectedObj && dragging) {\n                    screenToNDC(t.x, t.y);\n                    raycaster.setFromCamera(mouseNDC, camera);\n                    const intersection = new THREE.Vector3();\n                    raycaster.ray.intersectPlane(dragPlane, intersection);\n                    if (intersection) {\n                        const instanceId = selectedObj.userData.instanceId;\n                        let px = intersection.x;\n                        let pz = intersection.z;\n                        if (instanceId && config.objectInstances[instanceId]) {\n                            const inst = config.objectInstances[instanceId];\n                            const regId = inst.registryId;\n                            if (regId) {\n                                const entry = getRegistryEntry(regId);\n                                const wCells = (entry && entry.widthInCells) || 1;\n                                const hCells = (entry && entry.heightInCells) || 1;\n                                if (wCells > 1 || hCells > 1) {\n                                    const { col, row } = worldToCell(px, pz);\n                                    const fp = getObjectEffectiveFootprint(regId, inst.rotationY || 0);\n                                    if (canPlaceFootprint(col, row, fp.w, fp.h)) {\n                                        const snapped = getObjectFootprintWorldPosition(col, row, fp.w, fp.h);\n                                        px = snapped.x;\n                                        pz = snapped.z;\n                                        inst.anchorCol = col;\n                                        inst.anchorRow = row;\n                                        \n                                        clearObjectFootprint(instanceId);\n                                        const keys = getObjectFootprintCellKeys(col, row, fp.w, fp.h);\n                                        keys.forEach(k => {\n                                            config.objectCellOccupancy[k] = instanceId;\n                                        });\n                                    }\n                                } else {\n                                    const s = applySnapToGrid(regId, px, pz);\n                                    px = s.x; pz = s.z;\n                                    const { col, row } = worldToCell(px, pz);\n                                    inst.anchorCol = col;\n                                    inst.anchorRow = row;\n\n                                    clearObjectFootprint(instanceId);\n                                    config.objectCellOccupancy[cellKey(col, row)] = instanceId;\n                                }\n                            }\n                            inst.x = px;\n                            inst.z = pz;\n                        }\n                        selectedObj.position.x = px;\n                        selectedObj.position.z = pz;\n                    }\n                }\n\n                editLastX = t.x;\n                editLastY = t.y;\n            }\n        }\n\n        function onEditTouchEnd(e) {\n            if (mode !== 'edit') return;\n\n            for (let i = 0; i < e.changedTouches.length; i++) {\n                const id = e.changedTouches[i].identifier;\n                editTouches = editTouches.filter(t => t.id !== id);\n            }\n\n            if (editTouches.length < 2) {\n                editTwoFingerStarted = false;\n            }\n            if (editTouches.length === 0) {\n                if (editInteraction === 'drag-object' && selectedObj && selectedObj.userData.instanceId) {\n                    const instId = selectedObj.userData.instanceId;\n                    const newX = selectedObj.position.x;\n                    const newZ = selectedObj.position.z;\n                    if (Math.abs(newX - dragStartX) > 0.01 || Math.abs(newZ - dragStartZ) > 0.01) {\n                        UndoSystem.push({ type: 'object-move', instanceId: instId, oldX: dragStartX, oldZ: dragStartZ });\n                    }\n                }\n                dragging = false;\n                editInteraction = 'none';\n            }\n        }\n\n\n        let editorMode = 'select';\n        let selectedRegistryId = null;\n\n\n        const UndoSystem = (() => {\n            const history = [];\n            const MAX_HISTORY = 100;\n\n            function push(entry) {\n                history.push(entry);\n                if (history.length > MAX_HISTORY) history.shift();\n            }\n\n            function undo() {\n                if (history.length === 0) return;\n                const entry = history.pop();\n\n                switch (entry.type) {\n                    case 'object-place': {\n                        const id = entry.instanceId;\n                        if (worldObjects[id]) {\n                            scene.remove(worldObjects[id]);\n                            delete worldObjects[id];\n                        }\n                        if (config.objectInstances[id]) {\n                            delete config.objectInstances[id];\n                        }\n                        clearObjectFootprint(id);\n                        if (selectedObj && selectedObj.userData.instanceId === id) {\n                            deselectObject();\n                        }\n                        break;\n                    }\n                    case 'object-delete': {\n                        const id = entry.instanceId;\n                        const data = entry.instanceData;\n                        if (!config.objectInstances) config.objectInstances = {};\n                        config.objectInstances[id] = JSON.parse(JSON.stringify(data));\n                        \n                        const entryReg = getRegistryEntry(data.registryId);\n                        const wCells = (entryReg && entryReg.widthInCells) || 1;\n                        const hCells = (entryReg && entryReg.heightInCells) || 1;\n                        if (wCells > 1 || hCells > 1) {\n                            const fp = getObjectEffectiveFootprint(data.registryId, data.rotationY || 0);\n                            const keys = getObjectFootprintCellKeys(data.anchorCol, data.anchorRow, fp.w, fp.h);\n                            keys.forEach(k => {\n                                config.objectCellOccupancy[k] = id;\n                            });\n                        } else {\n                            const { col, row } = worldToCell(data.x, data.z);\n                            config.objectCellOccupancy[cellKey(col, row)] = id;\n                        }\n\n                        const modelType = getModelTypeForInstance(data);\n                        const group = ObjectFactory.create(modelType, data);\n                        group.position.set(data.x, 0, data.z);\n                        if (data.scale) group.scale.setScalar(data.scale);\n                        if (data.rotationY) group.rotation.y = data.rotationY;\n                        group.userData.instanceId = id;\n                        scene.add(group);\n                        worldObjects[id] = group;\n                        break;\n                    }\n                    case 'object-move': {\n                        const id = entry.instanceId;\n                        if (worldObjects[id]) {\n                            worldObjects[id].position.x = entry.oldX;\n                            worldObjects[id].position.z = entry.oldZ;\n                        }\n                        if (config.objectInstances[id]) {\n                            config.objectInstances[id].x = entry.oldX;\n                            config.objectInstances[id].z = entry.oldZ;\n                        }\n                        break;\n                    }\n                    case 'object-rotate': {\n                        const id = entry.instanceId;\n                        if (worldObjects[id]) {\n                            worldObjects[id].rotation.y = entry.oldRotationY;\n                        }\n                        if (config.objectInstances[id]) {\n                            config.objectInstances[id].rotationY = entry.oldRotationY;\n                        }\n                        break;\n                    }\n                    case 'object-scale': {\n                        const id = entry.instanceId;\n                        if (worldObjects[id]) {\n                            worldObjects[id].scale.setScalar(entry.oldScale);\n                        }\n                        if (config.objectInstances[id]) {\n                            config.objectInstances[id].scale = entry.oldScale;\n                        }\n                        break;\n                    }\n                    case 'object-swap': {\n                        const id = entry.instanceId;\n                        const oldRegistryId = entry.oldRegistryId;\n                        if (config.objectInstances[id]) {\n                            swapObjectRegistryType(id, oldRegistryId, true);\n                        }\n                        break;\n                    }\n                    case 'terrain-paint': {\n                        const cells = config.terrainCells || {};\n                        entry.newKeys.forEach(k => {\n                            delete cells[k];\n                            TerrainRenderer.eraseCell(k);\n                        });\n                        if (entry.oldCellData) {\n                            Object.keys(entry.oldCellData).forEach(k => {\n                                cells[k] = entry.oldCellData[k];\n                            });\n                            Object.keys(entry.oldCellData).forEach(k => {\n                                const data = entry.oldCellData[k];\n                                if (data && data.terrainType && !data.occupiedBy) {\n                                    TerrainRenderer.paintCell(k, data);\n                                }\n                            });\n                        }\n                        break;\n                    }\n                    case 'terrain-erase': {\n                        const cells = config.terrainCells || {};\n                        if (entry.oldCellData) {\n                            Object.keys(entry.oldCellData).forEach(k => {\n                                cells[k] = entry.oldCellData[k];\n                            });\n                            Object.keys(entry.oldCellData).forEach(k => {\n                                const data = entry.oldCellData[k];\n                                if (data && data.terrainType && !data.occupiedBy) {\n                                    TerrainRenderer.paintCell(k, data);\n                                }\n                            });\n                        }\n                        break;\n                    }\n                }\n\n                lib.log('Undo: ' + entry.type);\n            }\n\n            function size() {\n                return history.length;\n            }\n\n            return { push, undo, size };\n        })();\n\n        const registryDefaults = {\n            person:              { w: 0.4, h: 1.6, d: 0.4, color: '#ffaa44' },\n            cone:                { w: 0.36, h: 0.6, d: 0.36, color: '#ff6600' },\n            trashcan:            { w: 0.48, h: 0.75, d: 0.48, color: '#555555' },\n            trashbag:            { w: 0.44, h: 0.55, d: 0.44, color: '#242424' },\n            phonebooth:          { w: 0.7, h: 1.8, d: 0.7, color: '#d32f2f' },\n            bench:               { w: 1.5, h: 0.6, d: 0.5, color: '#8B6914' },\n            lamp:                { w: 0.4, h: 2.5, d: 0.4, color: '#3a3a3a' },\n            stopsign:            { w: 0.8, h: 2.2, d: 0.15, color: '#cc0000' },\n            street_vendor_stall: { w: 1.4, h: 1.6, d: 1.1, color: '#00acc1' },\n            billboard_burger:    { w: 3.0, h: 4.9, d: 0.5, color: '#222222' },\n            billboard_soda:      { w: 3.0, h: 4.9, d: 0.5, color: '#222222' },\n            car_sedan_blue:      { w: 1.8, h: 0.75, d: 0.85, color: '#2266cc' },\n            car_compact_red:     { w: 1.55, h: 0.78, d: 0.85, color: '#d32f2f' },\n            car_taxi_yellow:     { w: 1.55, h: 0.78, d: 0.85, color: '#ffcc00' },\n            truck_delivery_white: { w: 2.3, h: 1.3, d: 0.95, color: '#ffffff' }\n        };\n        const defaultFallback = { w: 0.5, h: 0.5, d: 0.5, color: '#888888' };\n\n        function getDefaultsForType(registryId) {\n            return registryDefaults[registryId] || defaultFallback;\n        }\n\n        function applySnapToGrid(registryId, x, z) {\n            const entry = getRegistryEntry(registryId);\n            if (entry && entry.snapToGrid) {\n                const wCells = entry.widthInCells || 1;\n                const hCells = entry.heightInCells || 1;\n                if (wCells > 1 || hCells > 1) {\n                    const { col, row } = worldToCell(x, z);\n                    // Use footprint center\n                    const fp = getObjectEffectiveFootprint(registryId, placementRotation);\n                    return getObjectFootprintWorldPosition(col, row, fp.w, fp.h);\n                } else if (entry.snapMode === 'cellCenter') {\n                    const { col, row } = worldToCell(x, z);\n                    const snapped = cellToWorld(col, row);\n                    return { x: snapped.x, z: snapped.z };\n                }\n            }\n            return { x, z };\n        }\n\n        function placeObjectInstance(registryId, x, z) {\n            const defaults = getDefaultsForType(registryId);\n            const instanceId = generateInstanceId();\n\n            const entry = getRegistryEntry(registryId);\n            const wCells = (entry && entry.widthInCells) || 1;\n            const hCells = (entry && entry.heightInCells) || 1;\n\n            let anchorCol, anchorRow;\n            let rotY = 0;\n\n            if (wCells > 1 || hCells > 1) {\n                const { col, row } = worldToCell(x, z);\n                anchorCol = col;\n                anchorRow = row;\n                rotY = placementRotation;\n\n                const fp = getObjectEffectiveFootprint(registryId, rotY);\n                if (!canPlaceFootprint(anchorCol, anchorRow, fp.w, fp.h)) {\n                    lib.log(\"Cannot place footprint: out of terrain bounds\");\n                    return null;\n                }\n\n                // Footprint center\n                const snapped = getObjectFootprintWorldPosition(anchorCol, anchorRow, fp.w, fp.h);\n                x = snapped.x;\n                z = snapped.z;\n\n                // Clear any objects whose footprint overlaps the new placement\n                const footprintKeys = getObjectFootprintCellKeys(anchorCol, anchorRow, fp.w, fp.h);\n                footprintKeys.forEach(k => {\n                    const existingId = resolvePrimaryObjectInstance(k);\n                    if (existingId && existingId !== instanceId) {\n                        const existingMesh = worldObjects[existingId];\n                        if (existingMesh) scene.remove(existingMesh);\n                        delete worldObjects[existingId];\n                        delete config.objectInstances[existingId];\n                        clearObjectFootprint(existingId);\n                    }\n                });\n\n                // Populate cell occupancy\n                footprintKeys.forEach(k => {\n                    config.objectCellOccupancy[k] = instanceId;\n                });\n\n            } else {\n                const snapped = applySnapToGrid(registryId, x, z);\n                x = snapped.x;\n                z = snapped.z;\n\n                const { col, row } = worldToCell(x, z);\n                anchorCol = col;\n                anchorRow = row;\n\n                // Clear existing at this cell\n                const existingId = resolvePrimaryObjectInstance(cellKey(col, row));\n                if (existingId && existingId !== instanceId) {\n                    const existingMesh = worldObjects[existingId];\n                    if (existingMesh) scene.remove(existingMesh);\n                    delete worldObjects[existingId];\n                    delete config.objectInstances[existingId];\n                    clearObjectFootprint(existingId);\n                }\n\n                config.objectCellOccupancy[cellKey(col, row)] = instanceId;\n            }\n\n            const registryEntry = getRegistryEntry(registryId);\n            const defaultScale = (registryEntry && registryEntry.defaultScale) ? registryEntry.defaultScale : 1;\n\n            const instanceData = {\n                x: Math.round(x * 100) / 100,\n                z: Math.round(z * 100) / 100,\n                scale: defaultScale,\n                rotationY: rotY,\n                w: defaults.w,\n                h: defaults.h,\n                d: defaults.d,\n                color: defaults.color,\n                registryId: registryId,\n                isBeingConsumed: false,\n                consumedByPlayer: false,\n                consumedByGhost: false,\n                anchorCol: anchorCol,\n                anchorRow: anchorRow,\n                isPrimary: true\n            };\n\n            if (!config.objectInstances) config.objectInstances = {};\n            config.objectInstances[instanceId] = instanceData;\n\n            const modelType = getModelTypeForInstance(instanceData);\n            const group = ObjectFactory.create(modelType, instanceData);\n            group.position.set(instanceData.x, 0, instanceData.z);\n            if (instanceData.scale) group.scale.setScalar(instanceData.scale);\n            if (instanceData.rotationY) group.rotation.y = instanceData.rotationY;\n            group.userData.instanceId = instanceId;\n            scene.add(group);\n            worldObjects[instanceId] = group;\n\n            UndoSystem.push({ type: 'object-place', instanceId: instanceId });\n\n            return instanceId;\n        }\n\n        const editorToolbar = document.getElementById('editor-toolbar');\n        const registryDropdown = document.getElementById('registry-dropdown');\n        const customDropdown = document.getElementById('custom-dropdown');\n        const dropdownSelected = document.getElementById('dropdown-selected');\n        const dropdownOptions = document.getElementById('dropdown-options');\n        const objectDropdownList = document.getElementById('object-dropdown-list');\n        const objectSearchInput = document.getElementById('object-search-input');\n        const placeBtn = document.getElementById('place-btn');\n        const selectBtn = document.getElementById('select-btn');\n        const deleteBtn = document.getElementById('delete-btn');\n\n\n        const toggleObjectsBtn = document.getElementById('toggle-objects-btn');\n        const toggleTerrainBtn = document.getElementById('toggle-terrain-btn');\n        const objectControls = document.getElementById('object-controls');\n        const terrainControls = document.getElementById('terrain-controls');\n        const terrainDropdownSelected = document.getElementById('terrain-dropdown-selected');\n        const terrainDropdownOptions = document.getElementById('terrain-dropdown-options');\n        const terrainDropdownList = document.getElementById('terrain-dropdown-list');\n        const terrainSearchInput = document.getElementById('terrain-search-input');\n        const terrainPaintBtn = document.getElementById('terrain-paint-btn');\n        const terrainSelectBtn = document.getElementById('terrain-select-btn');\n        const terrainEraseBtn = document.getElementById('terrain-erase-btn');\n\n        let editorTopMode = 'objects';\n        let terrainToolMode = 'paint';\n        let selectedTerrainId = 'grass';\n\n        function populateRegistryDropdown() {\n            const registry = config.objectRegistry || [];\n            objectDropdownList.innerHTML = '';\n            registry.forEach(entry => {\n                const div = document.createElement('div');\n                div.className = 'custom-dropdown-option';\n                div.dataset.value = entry.id;\n                div.textContent = entry.displayName || entry.id;\n                div.addEventListener('click', (e) => {\n                    e.stopPropagation();\n                    selectedRegistryId = entry.id;\n                    dropdownSelected.textContent = entry.displayName || entry.id;\n                    dropdownOptions.classList.remove('open');\n                    objectDropdownList.querySelectorAll('.custom-dropdown-option').forEach(o => o.classList.remove('active'));\n                    div.classList.add('active');\n                    updateEditorStatus();\n                });\n                objectDropdownList.appendChild(div);\n            });\n            if (registry.length > 0) {\n                selectedRegistryId = registry[0].id;\n                dropdownSelected.textContent = registry[0].displayName || registry[0].id;\n            }\n        }\n\n        if (objectSearchInput) {\n            objectSearchInput.addEventListener('input', (e) => {\n                const filterText = e.target.value.toLowerCase();\n                const options = objectDropdownList.querySelectorAll('.custom-dropdown-option');\n                options.forEach(opt => {\n                    const text = opt.textContent.toLowerCase();\n                    if (text.includes(filterText)) {\n                        opt.style.display = '';\n                    } else {\n                        opt.style.display = 'none';\n                    }\n                });\n            });\n            \n            // Prevent pointerdown, touchstart, and keydown inside search input from leaking to 3D controls\n            objectSearchInput.addEventListener('pointerdown', e => e.stopPropagation());\n            objectSearchInput.addEventListener('touchstart', e => e.stopPropagation());\n            objectSearchInput.addEventListener('keydown', e => e.stopPropagation());\n        }\n\n        function positionDropdownBelow(triggerEl, optionsEl) {\n            const rect = triggerEl.getBoundingClientRect();\n            optionsEl.style.top = (rect.bottom + 6) + 'px';\n            optionsEl.style.left = rect.left + 'px';\n            optionsEl.style.width = rect.width + 'px';\n        }\n\n        dropdownSelected.addEventListener('click', (e) => {\n            e.stopPropagation();\n            const isOpen = dropdownOptions.classList.contains('open');\n            dropdownOptions.classList.toggle('open');\n            if (!isOpen) {\n                positionDropdownBelow(dropdownSelected, dropdownOptions);\n                if (objectSearchInput) {\n                    objectSearchInput.value = '';\n                    objectSearchInput.focus();\n                    objectDropdownList.querySelectorAll('.custom-dropdown-option').forEach(opt => opt.style.display = '');\n                }\n            }\n        });\n\n        document.addEventListener('click', () => {\n            dropdownOptions.classList.remove('open');\n            if (terrainDropdownOptions) terrainDropdownOptions.classList.remove('open');\n            const swapOpts = document.getElementById('prop-swap-options');\n            if (swapOpts) swapOpts.classList.remove('open');\n        });\n\n        function setEditorMode(newMode) {\n            editorMode = newMode;\n            placeBtn.classList.toggle('mode-active', newMode === 'place');\n            selectBtn.classList.toggle('mode-active', newMode === 'select');\n            deleteBtn.classList.toggle('mode-active', newMode === 'delete');\n            updateEditorStatus();\n        }\n\n        function updateEditorStatus() {}\n\n\n        function setEditorTopMode(topMode) {\n            editorTopMode = topMode;\n\n            if (toggleObjectsBtn && toggleTerrainBtn) {\n                toggleObjectsBtn.classList.toggle('active', topMode === 'objects');\n                toggleTerrainBtn.classList.toggle('active', topMode === 'terrain');\n            }\n\n            if (objectControls) {\n                objectControls.classList.toggle('hidden', topMode !== 'objects');\n            }\n            if (terrainControls) {\n                terrainControls.classList.toggle('active', topMode === 'terrain');\n            }\n\n            if (topMode !== 'terrain') {\n                terrainCursor.hide();\n            }\n\n            if (topMode === 'terrain') {\n                deselectObject();\n            }\n\n            updateEditorStatus();\n        }\n\n        function setTerrainToolMode(toolMode) {\n            terrainToolMode = toolMode;\n            if (terrainPaintBtn) terrainPaintBtn.classList.toggle('mode-active', toolMode === 'paint');\n            if (terrainSelectBtn) terrainSelectBtn.classList.toggle('mode-active', toolMode === 'select');\n            if (terrainEraseBtn) terrainEraseBtn.classList.toggle('mode-active', toolMode === 'erase');\n            terrainCursor.setEraseMode(toolMode === 'erase');\n            if (toolMode === 'select') terrainCursor.hide();\n            updateEditorStatus();\n        }\n\n        function paintTerrainAtPosition(worldX, worldZ) {\n            const { col, row } = worldToCell(worldX, worldZ);\n\n            if (!config.terrainCells) config.terrainCells = {};\n\n            if (terrainToolMode === 'paint') {\n                const fp = getEffectiveFootprint(selectedTerrainId, placementRotation);\n                const anchorCol = col;\n                const anchorRow = row;\n\n                if (!canPlaceFootprint(anchorCol, anchorRow, fp.w, fp.h)) return;\n\n                const footprintKeys = getFootprintCellKeys(anchorCol, anchorRow, fp.w, fp.h);\n                const anchorKey = cellKey(anchorCol, anchorRow);\n\n                const oldCellData = {};\n                const piecesToClear = new Set();\n                footprintKeys.forEach(k => {\n                    const primary = resolvePrimaryCellKey(k);\n                    if (primary) piecesToClear.add(primary);\n                });\n                piecesToClear.forEach(pk => {\n                    const allKeys = getAllKeysForPiece(pk);\n                    allKeys.forEach(k => {\n                        if (config.terrainCells[k]) {\n                            oldCellData[k] = JSON.parse(JSON.stringify(config.terrainCells[k]));\n                        }\n                    });\n                });\n\n                piecesToClear.forEach(pk => clearTerrainPiece(pk));\n\n                const cellData = {\n                    terrainType: selectedTerrainId,\n                    rotation: placementRotation,\n                    isPrimary: true\n                };\n                config.terrainCells[anchorKey] = cellData;\n\n                footprintKeys.forEach(k => {\n                    if (k !== anchorKey) {\n                        config.terrainCells[k] = { occupiedBy: anchorKey };\n                    }\n                });\n\n                TerrainRenderer.paintCell(anchorKey, cellData);\n\n                UndoSystem.push({ type: 'terrain-paint', newKeys: footprintKeys.slice(), oldCellData: oldCellData });\n\n            } else if (terrainToolMode === 'erase') {\n                const key = cellKey(col, row);\n                const primaryKey = resolvePrimaryCellKey(key);\n                if (primaryKey) {\n                    const oldCellData = {};\n                    const allKeys = getAllKeysForPiece(primaryKey);\n                    allKeys.forEach(k => {\n                        if (config.terrainCells[k]) {\n                            oldCellData[k] = JSON.parse(JSON.stringify(config.terrainCells[k]));\n                        }\n                    });\n\n                    clearTerrainPiece(primaryKey);\n\n                    UndoSystem.push({ type: 'terrain-erase', oldCellData: oldCellData });\n                }\n            }\n        }\n\n\n        let pathEditActive = false;\n        let pathEditInstanceId = null;\n        let pathEditTool = 'select';\n        let pathEditSelectedIdx = -1;\n        let pathEditDragging = false;\n\n        let pathEditGroup = null;\n        let pathEditMarkers = [];\n\n        const PATH_MARKER_RADIUS = 0.6;\n        const PATH_MARKER_COLOR = 0x00e5ff;\n        const PATH_MARKER_SELECTED_COLOR = 0xffeb3b;\n        const PATH_LINE_COLOR = 0x76ff03;\n\n        function enterPathEditMode(instanceId) {\n            const inst = config.objectInstances[instanceId];\n            if (!inst || !inst.waypoints || inst.waypoints.length < 1) {\n                if (!inst) { lib.log('Cannot enter path edit: invalid instance'); return; }\n                if (!inst.waypoints) inst.waypoints = [];\n            }\n\n            pathEditActive = true;\n            pathEditInstanceId = instanceId;\n            pathEditTool = 'select';\n            pathEditSelectedIdx = -1;\n            pathEditDragging = false;\n\n            document.getElementById('toolbar-content-row').style.display = 'none';\n            document.querySelector('.editor-mode-tabs').style.display = 'none';\n            document.getElementById('path-edit-toolbar').style.display = 'block';\n\n            const mesh = worldObjects[instanceId];\n            if (mesh) {\n                mesh.traverse(child => {\n                    if (child.isMesh && child.material) {\n                        child.material.emissive = new THREE.Color(0x003344);\n                    }\n                });\n            }\n\n            updatePathEditToolButtons();\n\n            rebuildPathEditVisualization();\n\n            lib.log('Entered Path Edit Mode for ' + instanceId);\n        }\n\n        function exitPathEditMode() {\n            if (pathEditInstanceId) {\n                const mesh = worldObjects[pathEditInstanceId];\n                if (mesh) {\n                    mesh.traverse(child => {\n                        if (child.isMesh && child.material) {\n                            child.material.emissive = new THREE.Color(0x000000);\n                        }\n                    });\n                }\n            }\n\n            pathEditActive = false;\n            pathEditInstanceId = null;\n            pathEditSelectedIdx = -1;\n            pathEditDragging = false;\n\n            document.getElementById('path-edit-toolbar').style.display = 'none';\n            document.getElementById('toolbar-content-row').style.display = 'flex';\n            document.querySelector('.editor-mode-tabs').style.display = 'flex';\n\n            clearPathEditVisualization();\n\n            WaypointManager.rebuild();\n\n            lib.log('Exited Path Edit Mode');\n        }\n\n        function rebuildPathEditVisualization() {\n            clearPathEditVisualization();\n\n            if (!pathEditInstanceId) return;\n            const inst = config.objectInstances[pathEditInstanceId];\n            if (!inst || !inst.waypoints || inst.waypoints.length === 0) return;\n\n            pathEditGroup = new THREE.Group();\n            pathEditGroup.name = 'PathEditGroup';\n            pathEditMarkers = [];\n\n            const markerGeo = new THREE.SphereGeometry(PATH_MARKER_RADIUS, 16, 12);\n\n            inst.waypoints.forEach((wp, idx) => {\n                const color = (idx === pathEditSelectedIdx) ? PATH_MARKER_SELECTED_COLOR : PATH_MARKER_COLOR;\n                const markerMat = new THREE.MeshBasicMaterial({ color: color });\n                const marker = new THREE.Mesh(markerGeo, markerMat);\n                marker.position.set(wp.x, 0.6, wp.z);\n                marker.userData.waypointIndex = idx;\n                pathEditGroup.add(marker);\n                pathEditMarkers.push(marker);\n\n                const labelCanvas = document.createElement('canvas');\n                labelCanvas.width = 64;\n                labelCanvas.height = 64;\n                const ctx = labelCanvas.getContext('2d');\n                ctx.font = 'bold 40px Arial';\n                ctx.textAlign = 'center';\n                ctx.textBaseline = 'middle';\n                ctx.fillStyle = '#fff';\n                ctx.strokeStyle = '#000';\n                ctx.lineWidth = 4;\n                ctx.strokeText(String(idx + 1), 32, 32);\n                ctx.fillText(String(idx + 1), 32, 32);\n                const texture = new THREE.CanvasTexture(labelCanvas);\n                const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false });\n                const sprite = new THREE.Sprite(spriteMat);\n                sprite.scale.set(1.2, 1.2, 1);\n                sprite.position.set(wp.x, 1.8, wp.z);\n                pathEditGroup.add(sprite);\n            });\n\n            if (inst.waypoints.length >= 2) {\n                const linePoints = inst.waypoints.map(wp => new THREE.Vector3(wp.x, 0.3, wp.z));\n                linePoints.push(linePoints[0].clone());\n                const lineGeo = new THREE.BufferGeometry().setFromPoints(linePoints);\n                const lineMat = new THREE.LineBasicMaterial({ color: PATH_LINE_COLOR, linewidth: 2 });\n                const line = new THREE.Line(lineGeo, lineMat);\n                pathEditGroup.add(line);\n            }\n\n            scene.add(pathEditGroup);\n        }\n\n        function clearPathEditVisualization() {\n            if (pathEditGroup) {\n                scene.remove(pathEditGroup);\n                pathEditGroup.traverse(child => {\n                    if (child.geometry) child.geometry.dispose();\n                    if (child.material) {\n                        if (child.material.map) child.material.map.dispose();\n                        child.material.dispose();\n                    }\n                });\n                pathEditGroup = null;\n            }\n            pathEditMarkers = [];\n        }\n\n        function updatePathEditToolButtons() {\n            const addBtn = document.getElementById('path-add-wp-btn');\n            const selectBtn = document.getElementById('path-select-wp-btn');\n            if (addBtn) addBtn.classList.toggle('mode-active', pathEditTool === 'add');\n            if (selectBtn) selectBtn.classList.toggle('mode-active', pathEditTool === 'select');\n        }\n\n        function pathEditAddWaypoint(worldX, worldZ) {\n            if (!pathEditInstanceId) return;\n            const inst = config.objectInstances[pathEditInstanceId];\n            if (!inst) return;\n            if (!inst.waypoints) inst.waypoints = [];\n\n            const newWp = { x: Math.round(worldX * 10) / 10, z: Math.round(worldZ * 10) / 10 };\n\n            if (pathEditSelectedIdx >= 0 && pathEditSelectedIdx < inst.waypoints.length) {\n                inst.waypoints.splice(pathEditSelectedIdx + 1, 0, newWp);\n                pathEditSelectedIdx = pathEditSelectedIdx + 1;\n            } else {\n                inst.waypoints.push(newWp);\n                pathEditSelectedIdx = inst.waypoints.length - 1;\n            }\n\n            rebuildPathEditVisualization();\n\n            const aiPathInfo = document.getElementById('prop-ai-path-info');\n            if (aiPathInfo) aiPathInfo.textContent = inst.waypoints.length + ' waypoints (loop)';\n        }\n\n        function pathEditDeleteWaypoint() {\n            if (!pathEditInstanceId || pathEditSelectedIdx < 0) return;\n            const inst = config.objectInstances[pathEditInstanceId];\n            if (!inst || !inst.waypoints) return;\n            if (inst.waypoints.length <= 0) return;\n\n            inst.waypoints.splice(pathEditSelectedIdx, 1);\n            if (pathEditSelectedIdx >= inst.waypoints.length) {\n                pathEditSelectedIdx = inst.waypoints.length - 1;\n            }\n            rebuildPathEditVisualization();\n\n            const aiPathInfo = document.getElementById('prop-ai-path-info');\n            if (aiPathInfo) aiPathInfo.textContent = (inst.waypoints ? inst.waypoints.length : 0) + ' waypoints (loop)';\n        }\n\n        function handlePathEditPointerDown(clientX, clientY) {\n            if (!pathEditActive) return false;\n\n            screenToNDC(clientX, clientY);\n            raycaster.setFromCamera(mouseNDC, camera);\n\n            if (pathEditMarkers.length > 0) {\n                const hits = raycaster.intersectObjects(pathEditMarkers);\n                if (hits.length > 0) {\n                    const hitMarker = hits[0].object;\n                    pathEditSelectedIdx = hitMarker.userData.waypointIndex;\n                    pathEditDragging = true;\n                    rebuildPathEditVisualization();\n                    editInteraction = 'path-drag-waypoint';\n                    return true;\n                }\n            }\n\n            if (pathEditTool === 'add') {\n                const intersection = new THREE.Vector3();\n                const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                if (hit) {\n                    pathEditAddWaypoint(intersection.x, intersection.z);\n                }\n                editInteraction = 'none';\n                return true;\n            }\n\n            pathEditSelectedIdx = -1;\n            rebuildPathEditVisualization();\n            editInteraction = 'none';\n            return true;\n        }\n\n        function handlePathEditPointerMove(clientX, clientY) {\n            if (!pathEditActive || !pathEditDragging || pathEditSelectedIdx < 0) return false;\n            if (editInteraction !== 'path-drag-waypoint') return false;\n\n            screenToNDC(clientX, clientY);\n            raycaster.setFromCamera(mouseNDC, camera);\n            const intersection = new THREE.Vector3();\n            const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n            if (hit) {\n                const inst = config.objectInstances[pathEditInstanceId];\n                if (inst && inst.waypoints && inst.waypoints[pathEditSelectedIdx]) {\n                    inst.waypoints[pathEditSelectedIdx].x = Math.round(intersection.x * 10) / 10;\n                    inst.waypoints[pathEditSelectedIdx].z = Math.round(intersection.z * 10) / 10;\n                    rebuildPathEditVisualization();\n                }\n            }\n            return true;\n        }\n\n        function handlePathEditPointerUp() {\n            if (!pathEditActive) return false;\n            if (editInteraction === 'path-drag-waypoint') {\n                pathEditDragging = false;\n                editInteraction = 'none';\n                return true;\n            }\n            return false;\n        }\n\n        let selectedTerrainCellKey = null;\n\n        function selectTerrainCell(worldX, worldZ) {\n            const { col, row } = worldToCell(worldX, worldZ);\n            const key = cellKey(col, row);\n\n            const primaryKey = resolvePrimaryCellKey(key);\n            if (!primaryKey) {\n                deselectTerrainCell();\n                return;\n            }\n\n            selectedTerrainCellKey = primaryKey;\n            deselectObject();\n\n            const cellData = config.terrainCells[primaryKey];\n            const rotDeg = Math.round((cellData.rotation || 0) * 180 / Math.PI);\n\n            const propTitle = document.getElementById('prop-title');\n            const propEmpty = document.getElementById('prop-empty');\n            const propContent = document.getElementById('prop-content');\n            const propTerrainContent = document.getElementById('prop-terrain-content');\n            const propTerrainRotVal = document.getElementById('prop-terrain-rot-val');\n\n            const entry = getTerrainRegistryEntry(cellData.terrainType);\n            propTitle.textContent = (entry && entry.displayName) ? entry.displayName : 'Terrain Tile';\n            propEmpty.style.display = 'none';\n            propContent.style.display = 'none';\n            propTerrainContent.style.display = 'flex';\n            propTerrainRotVal.textContent = rotDeg + '\u00b0';\n        }\n\n        function deselectTerrainCell() {\n            selectedTerrainCellKey = null;\n            const propEmpty = document.getElementById('prop-empty');\n            const propContent = document.getElementById('prop-content');\n            const propTerrainContent = document.getElementById('prop-terrain-content');\n            const propTitle = document.getElementById('prop-title');\n            if (propTitle) propTitle.textContent = 'Properties';\n            if (propEmpty) propEmpty.style.display = 'block';\n            if (propContent) propContent.style.display = 'none';\n            if (propTerrainContent) propTerrainContent.style.display = 'none';\n        }\n\n        function rotateSelectedTerrainCell() {\n            if (!selectedTerrainCellKey || !config.terrainCells[selectedTerrainCellKey]) return;\n            const cellData = config.terrainCells[selectedTerrainCellKey];\n            if (cellData.occupiedBy) return;\n\n            const currentRot = cellData.rotation || 0;\n            const newRot = (currentRot + Math.PI / 2) % (Math.PI * 2);\n\n            const parts = selectedTerrainCellKey.split('_');\n            const anchorCol = parseInt(parts[0], 10);\n            const anchorRow = parseInt(parts[1], 10);\n\n            const newFp = getEffectiveFootprint(cellData.terrainType, newRot);\n            if (!canPlaceFootprint(anchorCol, anchorRow, newFp.w, newFp.h)) return;\n\n            clearTerrainPiece(selectedTerrainCellKey);\n\n            const newCellData = {\n                terrainType: cellData.terrainType,\n                rotation: newRot,\n                isPrimary: true\n            };\n            config.terrainCells[selectedTerrainCellKey] = newCellData;\n\n            const newKeys = getFootprintCellKeys(anchorCol, anchorRow, newFp.w, newFp.h);\n            newKeys.forEach(k => {\n                if (k !== selectedTerrainCellKey) {\n                    config.terrainCells[k] = { occupiedBy: selectedTerrainCellKey };\n                }\n            });\n\n            TerrainRenderer.paintCell(selectedTerrainCellKey, newCellData);\n\n            const rotDeg = Math.round(newRot * 180 / Math.PI);\n            document.getElementById('prop-terrain-rot-val').textContent = rotDeg + '\u00b0';\n        }\n\n        function populateTerrainDropdown() {\n            if (!terrainDropdownList) return;\n            const types = TerrainRegistry.getAll();\n            terrainDropdownList.innerHTML = '';\n            types.forEach(entry => {\n                const div = document.createElement('div');\n                div.className = 'custom-dropdown-option';\n                div.dataset.value = entry.id;\n                div.textContent = entry.displayName || entry.id;\n                if (entry.id === selectedTerrainId) div.classList.add('active');\n                div.addEventListener('click', (e) => {\n                    e.stopPropagation();\n                    selectedTerrainId = entry.id;\n                    placementRotation = 0;\n                    terrainDropdownSelected.textContent = entry.displayName || entry.id;\n                    terrainDropdownOptions.classList.remove('open');\n                    terrainDropdownList.querySelectorAll('.custom-dropdown-option').forEach(o => o.classList.remove('active'));\n                    div.classList.add('active');\n                    terrainCursor.updateSize();\n                    updateEditorStatus();\n                });\n                terrainDropdownList.appendChild(div);\n            });\n            if (types.length > 0 && !selectedTerrainId) {\n                selectedTerrainId = types[0].id;\n                terrainDropdownSelected.textContent = types[0].displayName || types[0].id;\n            }\n        }\n\n        if (terrainSearchInput) {\n            terrainSearchInput.addEventListener('input', (e) => {\n                const filterText = e.target.value.toLowerCase();\n                const options = terrainDropdownList.querySelectorAll('.custom-dropdown-option');\n                options.forEach(opt => {\n                    const text = opt.textContent.toLowerCase();\n                    if (text.includes(filterText)) {\n                        opt.style.display = '';\n                    } else {\n                        opt.style.display = 'none';\n                    }\n                });\n            });\n            \n            // Prevent pointerdown, touchstart, and keydown inside search input from leaking to 3D controls\n            terrainSearchInput.addEventListener('pointerdown', e => e.stopPropagation());\n            terrainSearchInput.addEventListener('touchstart', e => e.stopPropagation());\n            terrainSearchInput.addEventListener('keydown', e => e.stopPropagation());\n        }\n\n        function initEditorToolbar() {\n            editorToolbar.classList.add('active');\n            populateRegistryDropdown();\n            \n            editorToolbar.addEventListener('pointerdown', e => e.stopPropagation());\n            editorToolbar.addEventListener('touchstart', e => e.stopPropagation());\n            \n            document.getElementById('prop-panel').classList.add('active');\n\n            const propSwapSelected = document.getElementById('prop-swap-selected');\n            const propSwapOptions = document.getElementById('prop-swap-options');\n            const propSwapSearch = document.getElementById('prop-swap-search');\n            if (propSwapSelected && propSwapOptions) {\n                propSwapSelected.addEventListener('click', (e) => {\n                    e.stopPropagation();\n                    const isOpen = propSwapOptions.classList.contains('open');\n                    propSwapOptions.classList.toggle('open');\n                    if (!isOpen && propSwapSearch) {\n                        propSwapSearch.value = '';\n                        propSwapSearch.focus();\n                        const opts = document.getElementById('prop-swap-list').querySelectorAll('.custom-dropdown-option');\n                        opts.forEach(o => o.style.display = '');\n                    }\n                });\n                propSwapSelected.addEventListener('pointerdown', e => e.stopPropagation());\n                propSwapSelected.addEventListener('touchstart', e => e.stopPropagation());\n            }\n            if (propSwapSearch) {\n                propSwapSearch.addEventListener('input', (e) => {\n                    const filterText = e.target.value.toLowerCase();\n                    const opts = document.getElementById('prop-swap-list').querySelectorAll('.custom-dropdown-option');\n                    opts.forEach(opt => {\n                        opt.style.display = opt.textContent.toLowerCase().includes(filterText) ? '' : 'none';\n                    });\n                });\n                propSwapSearch.addEventListener('pointerdown', e => e.stopPropagation());\n                propSwapSearch.addEventListener('touchstart', e => e.stopPropagation());\n                propSwapSearch.addEventListener('keydown', e => e.stopPropagation());\n            }\n\n            const propInitialWpId = document.getElementById('prop-initial-wp-id');\n            if (propInitialWpId) {\n                propInitialWpId.addEventListener('input', (e) => {\n                    if (selectedObj && selectedObj.userData.instanceId) {\n                        const instId = selectedObj.userData.instanceId;\n                        if (config.objectInstances[instId]) {\n                            const val = e.target.value === '' ? undefined : parseInt(e.target.value, 10);\n                            config.objectInstances[instId].initialNextWaypointId = val;\n                        }\n                    }\n                });\n                propInitialWpId.addEventListener('pointerdown', e => e.stopPropagation());\n                propInitialWpId.addEventListener('touchstart', e => e.stopPropagation());\n                propInitialWpId.addEventListener('keydown', e => e.stopPropagation());\n            }\n\n            placeBtn.addEventListener('click', () => setEditorMode('place'));\n            selectBtn.addEventListener('click', () => setEditorMode('select'));\n\n            deleteBtn.addEventListener('click', () => {\n                setEditorMode('delete');\n            });\n\n            const undoBtnObjects = document.getElementById('undo-btn-objects');\n            const undoBtnTerrain = document.getElementById('undo-btn-terrain');\n            if (undoBtnObjects) undoBtnObjects.addEventListener('click', () => UndoSystem.undo());\n            if (undoBtnTerrain) undoBtnTerrain.addEventListener('click', () => UndoSystem.undo());\n\n            const propScale = document.getElementById('prop-scale');\n            const propScaleVal = document.getElementById('prop-scale-val');\n            const propFacing = document.getElementById('prop-facing');\n            const propFacingVal = document.getElementById('prop-facing-val');\n            const propPanel = document.getElementById('prop-panel');\n            \n            if (propPanel) {\n                propPanel.addEventListener('pointerdown', e => e.stopPropagation());\n                propPanel.addEventListener('touchstart', e => e.stopPropagation());\n                propPanel.addEventListener('wheel', e => e.stopPropagation());\n            }\n            \n            if (propScale) {\n                propScale.addEventListener('input', (e) => {\n                    if (selectedObj && selectedObj.userData.instanceId) {\n                        const val = parseFloat(e.target.value);\n                        propScaleVal.textContent = val.toFixed(1);\n                        selectedObj.scale.setScalar(val);\n                        const instId = selectedObj.userData.instanceId;\n                        if (config.objectInstances[instId]) {\n                            config.objectInstances[instId].scale = val;\n                        }\n                    }\n                });\n            }\n            \n            if (propFacing) {\n                propFacing.addEventListener('input', (e) => {\n                    if (selectedObj && selectedObj.userData.instanceId) {\n                        const val = parseFloat(e.target.value);\n                        propFacingVal.textContent = val.toFixed(1);\n                        selectedObj.rotation.y = val;\n                        const instId = selectedObj.userData.instanceId;\n                        if (config.objectInstances[instId]) {\n                            config.objectInstances[instId].rotationY = val;\n                        }\n                    }\n                });\n            }\n\n            setEditorMode('select');\n\n            if (toggleObjectsBtn) {\n                toggleObjectsBtn.addEventListener('click', () => setEditorTopMode('objects'));\n            }\n            if (toggleTerrainBtn) {\n                toggleTerrainBtn.addEventListener('click', () => setEditorTopMode('terrain'));\n            }\n\n            if (terrainPaintBtn) {\n                terrainPaintBtn.addEventListener('click', () => setTerrainToolMode('paint'));\n            }\n            if (terrainSelectBtn) {\n                terrainSelectBtn.addEventListener('click', () => setTerrainToolMode('select'));\n            }\n            if (terrainEraseBtn) {\n                terrainEraseBtn.addEventListener('click', () => setTerrainToolMode('erase'));\n            }\n\n            if (terrainDropdownSelected) {\n                terrainDropdownSelected.addEventListener('click', (e) => {\n                    e.stopPropagation();\n                    const isOpen = terrainDropdownOptions.classList.contains('open');\n                    terrainDropdownOptions.classList.toggle('open');\n                    if (!isOpen) {\n                        positionDropdownBelow(terrainDropdownSelected, terrainDropdownOptions);\n                        if (terrainSearchInput) {\n                            terrainSearchInput.value = '';\n                            terrainSearchInput.focus();\n                            terrainDropdownList.querySelectorAll('.custom-dropdown-option').forEach(opt => opt.style.display = '');\n                        }\n                    }\n                });\n            }\n\n            populateTerrainDropdown();\n\n            const terrainRotateBtn = document.getElementById('prop-terrain-rotate-btn');\n            if (terrainRotateBtn) {\n                terrainRotateBtn.addEventListener('click', () => rotateSelectedTerrainCell());\n            }\n\n            const propCopyBtn = document.getElementById('prop-copy-btn');\n            if (propCopyBtn) {\n                propCopyBtn.addEventListener('click', () => {\n                    if (typeof copySelectedObject === 'function') {\n                        copySelectedObject();\n                    }\n                });\n            }\n\n            const propRotateBtn = document.getElementById('prop-rotate-btn');\n            if (propRotateBtn) {\n                propRotateBtn.addEventListener('click', () => {\n                    rotateSelectedObject();\n                });\n            }\n\n            const editPathBtn = document.getElementById('prop-edit-path-btn');\n            if (editPathBtn) {\n                editPathBtn.addEventListener('click', () => {\n                    if (selectedObj && selectedObj.userData.instanceId) {\n                        enterPathEditMode(selectedObj.userData.instanceId);\n                    }\n                });\n            }\n\n            const pathAddBtn = document.getElementById('path-add-wp-btn');\n            const pathSelectBtn = document.getElementById('path-select-wp-btn');\n            const pathDeleteBtn = document.getElementById('path-delete-wp-btn');\n            const pathExitBtn = document.getElementById('path-exit-btn');\n\n            if (pathAddBtn) {\n                pathAddBtn.addEventListener('click', () => {\n                    pathEditTool = 'add';\n                    updatePathEditToolButtons();\n                });\n            }\n            if (pathSelectBtn) {\n                pathSelectBtn.addEventListener('click', () => {\n                    pathEditTool = 'select';\n                    updatePathEditToolButtons();\n                });\n            }\n            if (pathDeleteBtn) {\n                pathDeleteBtn.addEventListener('click', () => {\n                    pathEditDeleteWaypoint();\n                });\n            }\n            if (pathExitBtn) {\n                pathExitBtn.addEventListener('click', () => {\n                    exitPathEditMode();\n                });\n            }\n\n            const pathEditToolbar = document.getElementById('path-edit-toolbar');\n            if (pathEditToolbar) {\n                pathEditToolbar.addEventListener('pointerdown', e => e.stopPropagation());\n                pathEditToolbar.addEventListener('touchstart', e => e.stopPropagation());\n            }\n        }\n\n        function rotateSelectedObject() {\n            if (!selectedObj || !selectedObj.userData.instanceId) return;\n            const instId = selectedObj.userData.instanceId;\n            const inst = config.objectInstances[instId];\n            if (!inst) return;\n            const oldRot = inst.rotationY || 0;\n            const newRot = (oldRot + Math.PI / 2) % (Math.PI * 2);\n\n            const entry = getRegistryEntry(inst.registryId);\n            const wCells = (entry && entry.widthInCells) || 1;\n            const hCells = (entry && entry.heightInCells) || 1;\n\n            if (wCells > 1 || hCells > 1) {\n                const fp = getObjectEffectiveFootprint(inst.registryId, newRot);\n                if (!canPlaceFootprint(inst.anchorCol, inst.anchorRow, fp.w, fp.h)) {\n                    lib.log(\"Cannot rotate footprint: would go out of terrain bounds\");\n                    return;\n                }\n                clearObjectFootprint(instId);\n                const keys = getObjectFootprintCellKeys(inst.anchorCol, inst.anchorRow, fp.w, fp.h);\n                keys.forEach(k => {\n                    config.objectCellOccupancy[k] = instId;\n                });\n\n                // Update world position for center of rotated footprint if dimensions are rectangular\n                const centerPos = getObjectFootprintWorldPosition(inst.anchorCol, inst.anchorRow, fp.w, fp.h);\n                inst.x = centerPos.x;\n                inst.z = centerPos.z;\n                selectedObj.position.set(inst.x, 0, inst.z);\n            }\n\n            inst.rotationY = newRot;\n            selectedObj.rotation.y = newRot;\n            UndoSystem.push({ type: 'object-rotate', instanceId: instId, oldRotationY: oldRot });\n            const propFacing = document.getElementById('prop-facing');\n            const propFacingVal = document.getElementById('prop-facing-val');\n            if (propFacing) propFacing.value = newRot;\n            if (propFacingVal) propFacingVal.textContent = newRot.toFixed(1);\n            lib.log('Rotated object 90\u00b0: ' + Math.round(newRot * 180 / Math.PI) + '\u00b0');\n        }\n\n        function copySelectedObject() {\n            if (!selectedObj) {\n                lib.log('Copy failed: No object currently selected.');\n                return;\n            }\n            if (!selectedObj.userData.instanceId) {\n                lib.log('Copy failed: Selected object has no instance ID.');\n                return;\n            }\n            \n            const srcId = selectedObj.userData.instanceId;\n            const srcData = config.objectInstances[srcId];\n            if (!srcData) {\n                lib.log('Copy failed: Could not find config data for ' + srcId);\n                return;\n            }\n            \n            const newX = srcData.x + 2;\n            const newZ = srcData.z + 2;\n            const newId = generateInstanceId();\n            const newData = JSON.parse(JSON.stringify(srcData));\n            newData.x = newX;\n            newData.z = newZ;\n            newData.isBeingConsumed = false;\n            newData.consumedByPlayer = false;\n            newData.consumedByGhost = false;\n            if (!config.objectInstances) config.objectInstances = {};\n            config.objectInstances[newId] = newData;\n\n            const modelType = getModelTypeForInstance(newData);\n            const group = ObjectFactory.create(modelType, newData);\n            group.position.set(newData.x, 0, newData.z);\n            if (newData.scale) group.scale.setScalar(newData.scale);\n            if (newData.rotationY) group.rotation.y = newData.rotationY;\n            group.userData.instanceId = newId;\n            scene.add(group);\n            worldObjects[newId] = group;\n\n            UndoSystem.push({ type: 'object-place', instanceId: newId });\n\n            selectObject(group);\n            lib.log('Copied ' + srcId + ' \u2192 ' + newId);\n        }\n\n        if (mode === 'edit') {\n            canvas.addEventListener('pointerdown', onEditPointerDown);\n            canvas.addEventListener('pointermove', onEditPointerMove);\n            canvas.addEventListener('pointerup', onEditPointerUp);\n            canvas.addEventListener('wheel', onEditWheel, { passive: false });\n            canvas.addEventListener('contextmenu', onEditContextMenu);\n\n            window.addEventListener('keydown', (e) => {\n                if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;\n                if ((e.key === 'c' || e.key === 'C') && !e.ctrlKey && !e.metaKey) {\n                    copySelectedObject();\n                }\n            });\n\n            window.addEventListener('keydown', (e) => {\n                if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;\n                if ((e.key === 'z' || e.key === 'Z') && !e.ctrlKey && !e.metaKey) {\n                    UndoSystem.undo();\n                }\n            });\n\n            window.addEventListener('keydown', (e) => {\n                if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;\n                if (e.key === 'r' || e.key === 'R') {\n                    if (editorTopMode === 'terrain' && terrainToolMode === 'paint') {\n                        placementRotation = (placementRotation + Math.PI / 2) % (Math.PI * 2);\n                        terrainCursor.updateSize();\n                        lib.log('Placement rotation: ' + Math.round(placementRotation * 180 / Math.PI) + '\u00b0');\n                    } else if (editorTopMode === 'objects' && editorMode === 'place' && selectedRegistryId) {\n                        placementRotation = (placementRotation + Math.PI / 2) % (Math.PI * 2);\n                        terrainCursor.updateSize();\n                        lib.log('Placement rotation: ' + Math.round(placementRotation * 180 / Math.PI) + '\u00b0');\n                    } else if (editorTopMode === 'terrain' && terrainToolMode === 'select' && selectedTerrainCellKey) {\n                        rotateSelectedTerrainCell();\n                    } else if (selectedObj && selectedObj.userData.instanceId) {\n                        rotateSelectedObject();\n                    }\n                }\n            });\n\n            canvas.addEventListener('pointermove', (e) => {\n                if (editorTopMode === 'terrain' && terrainToolMode !== 'select' && !editPointerDown) {\n                    screenToNDC(e.clientX, e.clientY);\n                    raycaster.setFromCamera(mouseNDC, camera);\n                    const intersection = new THREE.Vector3();\n                    const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                    if (hit) {\n                        terrainCursor.show(intersection.x, intersection.z);\n                    }\n                } else if (editorTopMode === 'objects' && editorMode === 'place' && selectedRegistryId) {\n                    screenToNDC(e.clientX, e.clientY);\n                    raycaster.setFromCamera(mouseNDC, camera);\n                    const intersection = new THREE.Vector3();\n                    const hit = raycaster.ray.intersectPlane(dragPlane, intersection);\n                    if (hit) {\n                        terrainCursor.show(intersection.x, intersection.z);\n                    }\n                } else {\n                    terrainCursor.hide();\n                }\n            });\n\n            gameWorld.addEventListener('touchstart', onEditTouchStart, { passive: false });\n            gameWorld.addEventListener('touchmove', onEditTouchMove, { passive: false });\n            gameWorld.addEventListener('touchend', onEditTouchEnd);\n            gameWorld.addEventListener('touchcancel', onEditTouchEnd);\n\n            initEditCamera();\n\n            initEditorToolbar();\n        }\n\n\n        let currentCameraHeight = null;\n        let currentCameraTilt = null;\n        let currentCameraHorizontalDist = null;\n\n        let cameraIntroTransitionActive = false;\n        let cameraIntroTransitionStart = 0;\n        let cameraIntroTransitionFrom = null;\n        let cameraIntroTransitionTo = null;\n\n        function resolveCameraHorizontalDistance(height, tiltDeg, explicitDist) {\n            if (explicitDist != null && explicitDist > 0) return explicitDist;\n            const tiltRad = Math.max(tiltDeg, 1) * Math.PI / 180;\n            return height / Math.tan(tiltRad);\n        }\n\n        function getLevelCameraParams(levelIdx) {\n            const hole = HoleManager.getPlayerHole();\n            const profile = hole.profile;\n            const heights = profile.levelCameraHeights || [19, 23, 27];\n            const tilts = profile.levelCameraTilts || [40, 45, 48];\n            const height = heights[levelIdx] !== undefined ? heights[levelIdx] : (config.cameraHeight || 19);\n            const tilt = tilts[levelIdx] !== undefined ? tilts[levelIdx] : (config.cameraAngle || 40);\n            return {\n                height,\n                tilt,\n                horizontalDist: resolveCameraHorizontalDistance(height, tilt, 0)\n            };\n        }\n\n        function getInitialCameraParams() {\n            const height = config.initialCameraHeight != null ? config.initialCameraHeight : 38;\n            const tilt = config.initialCameraTilt != null ? config.initialCameraTilt : 32;\n            return {\n                height,\n                tilt,\n                horizontalDist: resolveCameraHorizontalDistance(height, tilt, config.initialCameraDistance)\n            };\n        }\n\n        function easeOutCubic(t) {\n            return 1 - Math.pow(1 - t, 3);\n        }\n\n        function startCameraIntroTransition() {\n            const from = getInitialCameraParams();\n            const to = getLevelCameraParams(0);\n            cameraIntroTransitionFrom = from;\n            cameraIntroTransitionTo = to;\n            currentCameraHeight = from.height;\n            currentCameraTilt = from.tilt;\n            currentCameraHorizontalDist = from.horizontalDist;\n            cameraIntroTransitionActive = true;\n            cameraIntroTransitionStart = performance.now();\n        }\n\n        function resetCameraToGameplayLevel() {\n            const levelIdx = HoleManager.getPlayerHole().state.level - 1;\n            const params = getLevelCameraParams(levelIdx);\n            currentCameraHeight = params.height;\n            currentCameraTilt = params.tilt;\n            currentCameraHorizontalDist = params.horizontalDist;\n            cameraIntroTransitionActive = false;\n            pendingCameraIntroTransition = false;\n        }\n\n        function getCameraTargetPosition() {\n            const hole = HoleManager.getPlayerHole();\n            const p = hole.state;\n            const levelIdx = p.level - 1;\n            const gameState = GameStateManager.getState();\n\n            if (gameState === GAME_STATE_READY || gameState === GAME_STATE_LOADING) {\n                const initial = getInitialCameraParams();\n                currentCameraHeight = initial.height;\n                currentCameraTilt = initial.tilt;\n                currentCameraHorizontalDist = initial.horizontalDist;\n            } else if (pendingCameraGameplayReset) {\n                pendingCameraGameplayReset = false;\n                resetCameraToGameplayLevel();\n            } else if (pendingCameraIntroTransition) {\n                pendingCameraIntroTransition = false;\n                startCameraIntroTransition();\n            } else if (cameraIntroTransitionActive) {\n                const duration = config.initialCameraTransitionDuration != null\n                    ? config.initialCameraTransitionDuration : 1.5;\n                const elapsed = (performance.now() - cameraIntroTransitionStart) / 1000;\n                const t = easeOutCubic(Math.min(1, elapsed / Math.max(0.001, duration)));\n                const from = cameraIntroTransitionFrom;\n                const to = cameraIntroTransitionTo;\n                currentCameraHeight = from.height + (to.height - from.height) * t;\n                currentCameraTilt = from.tilt + (to.tilt - from.tilt) * t;\n                currentCameraHorizontalDist = from.horizontalDist + (to.horizontalDist - from.horizontalDist) * t;\n                if (t >= 1) cameraIntroTransitionActive = false;\n            } else {\n                const gameplay = getLevelCameraParams(levelIdx);\n                if (currentCameraHeight === null) currentCameraHeight = gameplay.height;\n                if (currentCameraTilt === null) currentCameraTilt = gameplay.tilt;\n                currentCameraHeight += (gameplay.height - currentCameraHeight) * 0.05;\n                currentCameraTilt += (gameplay.tilt - currentCameraTilt) * 0.05;\n                currentCameraHorizontalDist = resolveCameraHorizontalDistance(currentCameraHeight, currentCameraTilt, 0);\n            }\n\n            const rotRad = (config.cameraRotation || 0) * Math.PI / 180;\n            return {\n                x: p.x + Math.sin(rotRad) * currentCameraHorizontalDist,\n                y: currentCameraHeight,\n                z: p.z + Math.cos(rotRad) * currentCameraHorizontalDist\n            };\n        }\n\n        function updateCamera() {\n            const target = getCameraTargetPosition();\n            const p = HoleManager.getPlayerHole().state;\n\n            camera.position.set(target.x, target.y, target.z);\n            camera.lookAt(p.x, 0, p.z);\n        }\n\n        function initializeCameraPosition() {\n            const initial = getInitialCameraParams();\n            currentCameraHeight = initial.height;\n            currentCameraTilt = initial.tilt;\n            currentCameraHorizontalDist = initial.horizontalDist;\n            const hole = HoleManager.getPlayerHole();\n            const p = hole.state;\n            const rotRad = (config.cameraRotation || 0) * Math.PI / 180;\n\n            camera.position.set(\n                p.x + Math.sin(rotRad) * initial.horizontalDist,\n                initial.height,\n                p.z + Math.cos(rotRad) * initial.horizontalDist\n            );\n            camera.lookAt(p.x, 0, p.z);\n        }\n\n        if (mode === 'play') {\n            initializeCameraPosition();\n        }\n        updateGoalHUD();\n\n\n        let lastTime = performance.now();\n\n        function isCellTraversable(col, row) {\n            if (col < 0 || col >= TERRAIN_COLS || row < 0 || row >= TERRAIN_ROWS) {\n                return false;\n            }\n            const key = cellKey(col, row);\n            const cells = config.terrainCells || {};\n            let entry = cells[key];\n            if (!entry) {\n                return false; // Implicit water/void\n            }\n            if (entry.occupiedBy) {\n                entry = cells[entry.occupiedBy];\n            }\n            if (!entry || entry.terrainType === 'water') {\n                return false;\n            }\n            return true;\n        }\n\n        function isWorldPositionTraversable(x, z) {\n            const { col, row } = worldToCell(x, z);\n            return isCellTraversable(col, row);\n        }\n\n        function updateLighting() {\n            const p = HoleManager.getPlayerHole().state;\n            const sh = config.sunHeight !== undefined ? config.sunHeight : 25;\n            const sx = config.sunOffsetX !== undefined ? config.sunOffsetX : 15;\n            const sz = config.sunOffsetZ !== undefined ? config.sunOffsetZ : 8;\n            dirLight.position.set(p.x + sx, sh, p.z + sz);\n            dirLight.target.position.set(p.x, 0, p.z);\n            dirLight.target.updateMatrixWorld();\n            dirLight.intensity = config.sunIntensity !== undefined ? config.sunIntensity : 0.85;\n            ambientLight.intensity = config.ambientIntensity !== undefined ? config.ambientIntensity : 0.55;\n        }\n\n        function gameLoop(timestamp) {\n            let dt = (timestamp - lastTime) / 1000;\n            lastTime = timestamp;\n            dt = Math.min(dt, 0.11);\n\n            const gameState = GameStateManager.getState();\n            const isPlaying = mode === 'play' && gameState === GAME_STATE_PLAYING && !isVictoryActive && !isDefeatActive && !ModalSystem.isActive();\n            const isReadyPreview = mode === 'play' && gameState === GAME_STATE_READY && !isVictoryActive && !isDefeatActive;\n\n            if (isPlaying) {\n                if (routeRecordingPending) {\n                    RouteRecorder.startRecording(playerHole);\n                    routeRecordingPending = false;\n                }\n                RouteRecorder.update(dt, playerHole);\n\n                if (config.showVictoryAfter5 && (performance.now() - matchStartTime) > 5000) {\n                    triggerVictoryScreen();\n                }\n\n                // Update timer UI\n                const elapsedSecs = Math.floor((performance.now() - matchStartTime) / 1000);\n                const timerTextEl = document.getElementById('timer-text');\n                if (timerTextEl) {\n                    timerTextEl.textContent = formatMMSS(elapsedSecs);\n                }\n\n                let inputX, inputZ;\n                if (joystickActive) {\n                    const threshold = (config.joystickThreshold || 25) / 100;\n                    const rawMag = Math.sqrt(joystickDX * joystickDX + joystickDZ * joystickDZ);\n                    if (rawMag > 0.01) {\n                        const remapped = Math.min(1, rawMag / threshold);\n                        inputX = (joystickDX / rawMag) * remapped;\n                        inputZ = (joystickDZ / rawMag) * remapped;\n                    } else {\n                        inputX = 0;\n                        inputZ = 0;\n                    }\n                } else {\n                    const kb = getKeyboardInput();\n                    inputX = kb.x;\n                    inputZ = kb.z;\n                }\n\n                if (shouldUseTouchControls()) {\n                    if (!joystickActive && (inputX !== 0 || inputZ !== 0)) {\n                        joystickInner.classList.add('no-transition');\n                        const knobPctX = 50 + inputX * 40;\n                        const knobPctY = 50 + inputZ * 40;\n                        joystickInner.style.left = knobPctX + '%';\n                        joystickInner.style.top = knobPctY + '%';\n                    } else if (!joystickActive) {\n                        joystickInner.classList.remove('no-transition');\n                        centerJoystickKnob();\n                    }\n                }\n\n                const rotRad = (config.cameraRotation || 0) * Math.PI / 180;\n                const worldInputX = inputX * Math.cos(rotRad) + inputZ * Math.sin(rotRad);\n                const worldInputZ = -inputX * Math.sin(rotRad) + inputZ * Math.cos(rotRad);\n\n                playerHole.updateFromInput(worldInputX, worldInputZ, dt, {\n                    mode,\n                    isWorldPositionTraversable,\n                    gridHalf: (config.gridSize || 100) / 2\n                });\n\n                CarManager.update(dt);\n\n                if (ghostBot && isGhostActiveThisRound()) {\n                    updateActiveGhostController(dt, {\n                        playerHole,\n                        worldObjects,\n                        isWorldPositionTraversable,\n                        gridHalf: (config.gridSize || 100) / 2\n                    });\n                    GhostPresence.updateGhostVisibility();\n                    if (GhostPresence.isGhostOnScreen()) {\n                        updateGhostDebugUI();\n                    }\n                    checkGhostVictoryDefeat();\n                }\n\n                HoleManager.checkAllOverlaps(worldObjects);\n                ConsumeSystem.update(dt, config.cameraRotation);\n                ConsumeTargetDebug.update();\n                updateFloatingTexts(dt);\n                updateFlyingStars(dt);\n                if (isGhostActiveThisRound()) {\n                    updateManifestAnimations(dt);\n                    updateManifestRings(dt);\n                }\n\n                ObstructionFadeSystem.update(dt);\n                updateLevelBarPosition();\n\n                ConsumeSystem.updateNudges(dt, worldObjects);\n            } else if (isReadyPreview) {\n                CarManager.update(dt);\n                ObstructionFadeSystem.update(dt);\n            }\n\n            RegionSystem.setVisible(config.showGridGroups || false);\n\n            const showWaypoints = (mode === 'edit') || (config.showAIWaypoints === true);\n            WaypointManager.setVisible(showWaypoints);\n\n            playerHole.updateVisual(dt);\n            if (ghostBot && isGhostActiveThisRound()) {\n                if (GhostPresence.isGhostOnScreen()) {\n                    ghostBot.updateVisual(dt);\n                } else {\n                    ghostBot.group.position.set(ghostBot.state.x, 0, ghostBot.state.z);\n                }\n            }\n\n            if (mode === 'edit') {\n                updateEditCamera();\n                DynamicWaterSystem.update(editCamera.target.x, editCamera.target.z);\n            } else {\n                updateCamera();\n                DynamicWaterSystem.update(playerHole.state.x, playerHole.state.z);\n            }\n            updateLighting();\n            GhostOffscreenIndicator.update();\n\n            renderer.render(scene, camera);\n            requestAnimationFrame(gameLoop);\n        }\n\n        (async function finalizeStartup() {\n            await audioPreloadPromise;\n            LoadingManager.setProgress(100);\n            if (mode !== 'edit') {\n                requestAnimationFrame(() => {\n                    requestAnimationFrame(() => {\n                        GameStateManager.setState(GAME_STATE_READY);\n                    });\n                });\n            }\n        })();\n\n        requestAnimationFrame(gameLoop);\n    }\n    </script>\n</body>\n</html>w", "game_config": "{\"holeSpeed\":6.5,\"cameraHeight\":19,\"cameraAngle\":40,\"initialCameraHeight\":44,\"initialCameraTilt\":44,\"initialCameraDistance\":0,\"initialCameraTransitionDuration\":1.5,\"gridSize\":1000,\"cameraRotation\":51,\"levelScaleMultipliers\":[1,2,2.8,3.6,4.2,4.8,5.4,5.8,6.2,6.6,7,7.3,7.6,7.8,8],\"levelThresholds\":[10,20,40,50,55,60,65,70,75,80,85,90,95,100,105],\"levelSpeedMultipliers\":[1.15,1.5,2,2.2,3,3.3,3.6,3.9,4.2,4.5,4.8,5,5.2,5.4,5.5],\"levelCameraHeights\":[19,23,28,36,43,49,55,60,65,70,75,80,85,90,95],\"levelCameraTilts\":[40,45,48,51,51,51,51,51,51,51,51,51,51,51,51],\"objectRegistry\":[{\"defaultScale\":2,\"id\":\"person\",\"reactionSounds\":[\"snd-man-scream-01\",\"snd-man-scream-02\",\"snd-man-scream-03\"],\"objectCategory\":\"person\",\"modelType\":\"personManA\",\"requiredHoleLevel\":1,\"displayName\":\"Person (Man A)\",\"pointValue\":1},{\"requiredHoleLevel\":1,\"displayName\":\"Traffic Cone\",\"pointValue\":1,\"objectCategory\":\"prop\",\"modelType\":\"cone\",\"id\":\"cone\",\"reactionSounds\":[]},{\"displayName\":\"Trash Can\",\"pointValue\":1,\"requiredHoleLevel\":1,\"objectCategory\":\"prop\",\"modelType\":\"trashcan\",\"reactionSounds\":[\"snd-clank-01\"],\"id\":\"trashcan\",\"defaultScale\":2},{\"id\":\"bench\",\"reactionSounds\":[],\"defaultScale\":2,\"requiredHoleLevel\":1,\"pointValue\":1,\"displayName\":\"Bench\",\"modelType\":\"bench\",\"objectCategory\":\"prop\"},{\"pointValue\":1,\"displayName\":\"Street Lamp\",\"requiredHoleLevel\":1,\"objectCategory\":\"prop\",\"modelType\":\"lamppost\",\"reactionSounds\":[\"snd-clank-01\"],\"id\":\"lamp\",\"defaultScale\":2},{\"modelType\":\"stopsign\",\"objectCategory\":\"prop\",\"displayName\":\"Stop Sign\",\"pointValue\":1,\"requiredHoleLevel\":1,\"defaultScale\":2,\"reactionSounds\":[],\"id\":\"stopsign\",\"consumeSelfOcclusion\":true},{\"objectCategory\":\"vegetation\",\"modelType\":\"smallTree\",\"displayName\":\"Small Tree\",\"pointValue\":2,\"requiredHoleLevel\":2,\"reactionSounds\":[\"snd-leaves-01\"],\"id\":\"small_tree\"},{\"defaultScale\":2,\"reactionSounds\":[\"snd-leaves-01\"],\"id\":\"large_tree\",\"objectCategory\":\"vegetation\",\"modelType\":\"largeTree\",\"pointValue\":3,\"displayName\":\"Large Tree\",\"requiredHoleLevel\":3,\"consumeSelfOcclusion\":true},{\"defaultScale\":2,\"id\":\"flower\",\"reactionSounds\":[],\"objectCategory\":\"vegetation\",\"modelType\":\"flower\",\"requiredHoleLevel\":1,\"pointValue\":1,\"displayName\":\"Flower\"},{\"id\":\"car_sedan_blue\",\"reactionSounds\":[\"snd-car-honk-01\",\"snd-car-honk-02\"],\"defaultScale\":2,\"requiredHoleLevel\":3,\"pointValue\":3,\"displayName\":\"Car (Blue Sedan)\",\"objectCategory\":\"vehicle\",\"consumeSelfOcclusion\":true,\"modelType\":\"carSedanBlue\"},{\"pointValue\":5,\"snapMode\":\"cellCenter\",\"supportsObstructionFade\":true,\"objectCategory\":\"building\",\"consumeSelfOcclusion\":true,\"id\":\"apartment_small_orange\",\"requiredHoleLevel\":6,\"modelType\":\"apartmentBuilding\",\"snapToGrid\":true,\"reactionSounds\":[]},{\"snapMode\":\"cellCenter\",\"supportsObstructionFade\":true,\"objectCategory\":\"building\",\"consumeSelfOcclusion\":true,\"pointValue\":5,\"id\":\"apartment_small_yellow\",\"modelType\":\"apartmentYellow\",\"snapToGrid\":true,\"requiredHoleLevel\":6,\"displayName\":\"Apartment (Yellow)\",\"reactionSounds\":[]},{\"id\":\"apartment_small_red\",\"pointValue\":5,\"objectCategory\":\"building\",\"consumeSelfOcclusion\":true,\"snapMode\":\"cellCenter\",\"supportsObstructionFade\":true,\"reactionSounds\":[],\"displayName\":\"Apartment (Red)\",\"requiredHoleLevel\":6,\"modelType\":\"apartmentRed\",\"snapToGrid\":true},{\"pointValue\":5,\"objectCategory\":\"building\",\"consumeSelfOcclusion\":true,\"snapMode\":\"cellCenter\",\"supportsObstructionFade\":true,\"id\":\"apartment_small_blue\",\"displayName\":\"Apartment (Blue)\",\"requiredHoleLevel\":6,\"modelType\":\"apartmentBlue\",\"snapToGrid\":true,\"reactionSounds\":[]},{\"reactionSounds\":[],\"displayName\":\"Apartment (Green)\",\"requiredHoleLevel\":6,\"modelType\":\"apartmentGreen\",\"snapToGrid\":true,\"id\":\"apartment_small_green\",\"pointValue\":5,\"objectCategory\":\"building\",\"consumeSelfOcclusion\":true,\"snapMode\":\"cellCenter\",\"supportsObstructionFade\":true},{\"requiredHoleLevel\":1,\"displayName\":\"Trash Bag\",\"pointValue\":1,\"modelType\":\"trashbag\",\"objectCategory\":\"building\",\"id\":\"trashbag\",\"reactionSounds\":[]},{\"displayName\":\"Street Vendor Stall\",\"pointValue\":2,\"requiredHoleLevel\":2,\"modelType\":\"streetVendorStall\",\"objectCategory\":\"prop\",\"reactionSounds\":[\"snd-clank-01\"],\"id\":\"street_vendor_stall\",\"consumeSelfOcclusion\":true},{\"requiredHoleLevel\":2,\"displayName\":\"Phone Booth\",\"pointValue\":2,\"modelType\":\"phonebooth\",\"objectCategory\":\"prop\",\"id\":\"phonebooth\",\"reactionSounds\":[\"snd-clank-01\"]},{\"id\":\"billboard_burger\",\"reactionSounds\":[\"snd-clank-01\"],\"defaultScale\":1.5,\"requiredHoleLevel\":3,\"displayName\":\"Billboard (Burger)\",\"pointValue\":3,\"modelType\":\"billboardBurger\",\"objectCategory\":\"prop\",\"consumeSelfOcclusion\":true},{\"reactionSounds\":[\"snd-clank-01\"],\"id\":\"billboard_soda\",\"defaultScale\":1.5,\"pointValue\":3,\"displayName\":\"Billboard (Soda)\",\"requiredHoleLevel\":3,\"modelType\":\"billboardSoda\",\"objectCategory\":\"prop\",\"consumeSelfOcclusion\":true},{\"snapToGrid\":true,\"modelType\":\"officeSmall\",\"requiredHoleLevel\":8,\"displayName\":\"Small Office\",\"reactionSounds\":[\"snd-clank-01\"],\"supportsObstructionFade\":true,\"objectCategory\":\"building\",\"consumeSelfOcclusion\":true,\"pointValue\":10,\"heightInCells\":2,\"id\":\"office_small\"},{\"snapToGrid\":true,\"modelType\":\"officeMedium\",\"requiredHoleLevel\":8,\"displayName\":\"Medium Office\",\"reactionSounds\":[\"snd-clank-01\"],\"supportsObstructionFade\":true,\"objectCategory\":\"building\",\"consumeSelfOcclusion\":true,\"pointValue\":15,\"heightInCells\":2,\"id\":\"office_medium\"},{\"reactionSounds\":[\"snd-clank-01\"],\"snapToGrid\":true,\"modelType\":\"bank\",\"displayName\":\"Bank\",\"requiredHoleLevel\":12,\"heightInCells\":6,\"id\":\"bank\",\"objectCategory\":\"building\",\"supportsObstructionFade\":true,\"pointValue\":35,\"consumeSelfOcclusion\":true},{\"defaultScale\":2,\"reactionSounds\":[\"snd-car-honk-01\",\"snd-car-honk-02\"],\"id\":\"car_compact_red\",\"modelType\":\"carCompactRed\",\"objectCategory\":\"vehicle\",\"consumeSelfOcclusion\":true,\"displayName\":\"Car (Red Compact)\",\"pointValue\":3,\"requiredHoleLevel\":3},{\"id\":\"car_taxi_yellow\",\"reactionSounds\":[\"snd-car-honk-01\",\"snd-car-honk-02\"],\"defaultScale\":2,\"requiredHoleLevel\":3,\"pointValue\":3,\"displayName\":\"Car (Yellow Taxi)\",\"objectCategory\":\"vehicle\",\"consumeSelfOcclusion\":true,\"modelType\":\"carTaxiYellow\"},{\"defaultScale\":2.2,\"reactionSounds\":[\"snd-car-honk-01\",\"snd-car-honk-02\"],\"id\":\"truck_delivery_white\",\"modelType\":\"truckDeliveryWhite\",\"objectCategory\":\"vehicle\",\"consumeSelfOcclusion\":true,\"pointValue\":3,\"displayName\":\"Truck (White Delivery)\",\"requiredHoleLevel\":3}],\"targetScore\":100,\"globalVolume\":0.5,\"gridGroupSize\":20,\"showGridGroups\":false,\"objectInstances\":{\"inst_002\":{\"x\":14.503451958146577,\"z\":7.371347338595982,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"cone\",\"isBeingConsumed\":false,\"scale\":2,\"rotationY\":0},\"inst_003\":{\"x\":4.052833210960781,\"z\":4.3893696910524795,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"person\",\"isBeingConsumed\":false,\"scale\":1.7,\"rotationY\":3.5},\"inst_012\":{\"x\":13.227077516323448,\"z\":15.106748470523574,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"stopsign\",\"isBeingConsumed\":false,\"scale\":1.3,\"rotationY\":0},\"inst_018\":{\"x\":6.569459370897331,\"z\":14.849939795206609,\"w\":0.3,\"h\":2.5,\"d\":0.3,\"color\":\"#444444\",\"registryId\":\"lamp\",\"isBeingConsumed\":false,\"scale\":2,\"rotationY\":4.5},\"inst_019\":{\"x\":-4.757757870232226,\"z\":-11.754312691309039,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_021\":{\"x\":-4.57335828119671,\"z\":-7.963467784927651,\"scale\":1.8,\"rotationY\":1.6,\"w\":1.5,\"h\":0.6,\"d\":0.5,\"color\":\"#8B6914\",\"registryId\":\"bench\",\"isBeingConsumed\":false},\"inst_022\":{\"x\":4.384190715798635,\"z\":-8.31623223864592,\"scale\":1.7,\"rotationY\":0,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_023\":{\"x\":3.0758412826134,\"z\":6.333376059201171,\"scale\":1.8,\"rotationY\":3.2,\"w\":1.5,\"h\":0.6,\"d\":0.5,\"color\":\"#8B6914\",\"registryId\":\"bench\",\"isBeingConsumed\":false},\"inst_024\":{\"x\":-7.122149349906119,\"z\":-8.62513801181562,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_026\":{\"x\":-9.303107772445877,\"z\":6.476364204232151,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_027\":{\"x\":7.238891250064327,\"z\":6.26242414321422,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_034\":{\"x\":8.297911752529915,\"z\":8.217169816039005,\"scale\":1.5,\"rotationY\":1.1,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_035\":{\"x\":9.23,\"z\":5.85,\"scale\":1.6,\"rotationY\":1,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_036\":{\"x\":7.89,\"z\":3.97,\"scale\":1.5,\"rotationY\":0.9,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_037\":{\"x\":5.726203556125935,\"z\":8.2684921592453,\"scale\":1.5,\"rotationY\":1,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_041\":{\"x\":6.41,\"z\":-6.32,\"scale\":1.8,\"rotationY\":0.9,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_042\":{\"x\":8.874755901995984,\"z\":-7.15578060411295,\"scale\":1.7,\"rotationY\":0.7,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_043\":{\"x\":8.602279147203117,\"z\":-9.606864979729725,\"scale\":1.5,\"rotationY\":0.9,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_044\":{\"x\":6.362003094500718,\"z\":-11.540500969270592,\"scale\":1.8,\"rotationY\":0.9,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_045\":{\"x\":-8.494549691272288,\"z\":-11.075632674416513,\"scale\":1.5,\"rotationY\":0.6,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_046\":{\"x\":-6.224875687143829,\"z\":-11.018693112220165,\"scale\":1.5,\"rotationY\":0.5,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_047\":{\"x\":-6.916222964138509,\"z\":-5.97587333929923,\"scale\":1.5,\"rotationY\":0.5,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_048\":{\"x\":-8.631275226303938,\"z\":-12.526894122698026,\"scale\":1.5,\"rotationY\":0.5,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_049\":{\"x\":-10.24458573670779,\"z\":-12.047383135414798,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false,\"anchorCol\":81,\"anchorRow\":81,\"isPrimary\":true},\"inst_050\":{\"x\":6.870579364797656,\"z\":-8.73529400236816,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false},\"inst_051\":{\"x\":-0.34,\"z\":6.55,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false},\"inst_053\":{\"x\":-7.1283038184567715,\"z\":6.339417078744674,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_054\":{\"x\":7.72,\"z\":14.66,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_055\":{\"x\":14.7,\"z\":-12.77,\"scale\":2,\"rotationY\":0,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_056\":{\"x\":7.53,\"z\":-18.47,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_057\":{\"x\":15.69966005722195,\"z\":-18.560668124595146,\"scale\":1.3,\"rotationY\":4.9,\"w\":0.8,\"h\":2.2,\"d\":0.15,\"color\":\"#cc0000\",\"registryId\":\"stopsign\",\"isBeingConsumed\":false},\"inst_058\":{\"x\":-10.28,\"z\":-18.57,\"scale\":2,\"rotationY\":1.6,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_059\":{\"x\":-18.17450302885854,\"z\":7.464422670720893,\"scale\":2,\"rotationY\":3,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_060\":{\"x\":-18.79,\"z\":-12.39,\"scale\":2,\"rotationY\":3.1,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_061\":{\"x\":-34.98,\"z\":19.71,\"scale\":2.4,\"rotationY\":3.2,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#ffcc00\",\"registryId\":\"car_taxi_yellow\",\"isBeingConsumed\":false,\"waypoints\":[{\"x\":-87.6,\"z\":18.6},{\"x\":-87.6,\"z\":23.9},{\"x\":82.1,\"z\":26.2},{\"x\":81.7,\"z\":17.7}],\"anchorCol\":77,\"anchorRow\":86,\"isPrimary\":true},\"inst_063\":{\"x\":-11.426584082319563,\"z\":4.761528862365831,\"scale\":1.5,\"rotationY\":0.5,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_064\":{\"x\":-9.010495615199213,\"z\":4.331663031428812,\"scale\":1.5,\"rotationY\":0.5,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_065\":{\"x\":-10.192332497278224,\"z\":1.7633066507476371,\"scale\":1.5,\"rotationY\":0.5,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_066\":{\"x\":-7.8508520169882505,\"z\":1.8684841741088745,\"scale\":1.5,\"rotationY\":0.5,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_067\":{\"x\":-9.058985066566652,\"z\":-6.142134130788337,\"scale\":1.5,\"rotationY\":0.5,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_068\":{\"x\":-7.686719714168726,\"z\":-4.231808976073665,\"scale\":1.5,\"rotationY\":0.5,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"flower\",\"isBeingConsumed\":false},\"inst_069\":{\"x\":-18.13351867623023,\"z\":14.83078203671137,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"stopsign\",\"isBeingConsumed\":false,\"scale\":1.7,\"rotationY\":1.5},\"inst_090\":{\"x\":55,\"z\":37,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false,\"anchorCol\":92,\"anchorRow\":89,\"isPrimary\":true},\"inst_091\":{\"x\":61,\"z\":43,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false,\"anchorCol\":93,\"anchorRow\":90,\"isPrimary\":true},\"inst_093\":{\"x\":67,\"z\":37,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false},\"inst_095\":{\"x\":49,\"z\":37,\"scale\":1,\"rotationY\":4.71238898038469,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false,\"anchorCol\":91,\"anchorRow\":89,\"isPrimary\":true},\"inst_106\":{\"x\":13,\"z\":67,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_yellow\",\"isBeingConsumed\":false},\"inst_107\":{\"x\":13,\"z\":61,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false},\"inst_125\":{\"x\":61,\"z\":37,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false,\"anchorCol\":93,\"anchorRow\":89,\"isPrimary\":true},\"inst_131\":{\"x\":55,\"z\":43,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false,\"anchorCol\":92,\"anchorRow\":90,\"isPrimary\":true},\"inst_135\":{\"x\":37,\"z\":1,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false},\"inst_136\":{\"x\":43,\"z\":1,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false},\"inst_137\":{\"x\":49,\"z\":1,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_138\":{\"x\":55,\"z\":1,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_139\":{\"x\":61,\"z\":1,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false},\"inst_145\":{\"x\":61,\"z\":-11,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false,\"anchorCol\":93,\"anchorRow\":81,\"isPrimary\":true},\"inst_146\":{\"x\":61,\"z\":-5,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false},\"inst_148\":{\"x\":55,\"z\":-11,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false},\"inst_149\":{\"x\":49,\"z\":-11,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false},\"inst_150\":{\"x\":43,\"z\":-11,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false},\"inst_151\":{\"x\":37,\"z\":-11,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false},\"inst_152\":{\"x\":37,\"z\":-5,\"scale\":1,\"rotationY\":4.71238898038469,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false},\"inst_153\":{\"x\":55,\"z\":-5,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false},\"inst_154\":{\"x\":49,\"z\":-5,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false},\"inst_155\":{\"x\":43,\"z\":-5,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false},\"inst_156\":{\"x\":34.26,\"z\":10.22,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false},\"inst_157\":{\"x\":52.26,\"z\":10.01,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false},\"inst_158\":{\"x\":55.04995630601441,\"z\":9.481827840627403,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_159\":{\"x\":69.65,\"z\":9.72,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false},\"inst_160\":{\"x\":73,\"z\":73.39,\"scale\":1.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false},\"inst_161\":{\"x\":69.62,\"z\":-5.52,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false},\"inst_162\":{\"x\":29.68200864686908,\"z\":14.989192929642932,\"scale\":1.3,\"rotationY\":4.5,\"w\":0.8,\"h\":2.2,\"d\":0.15,\"color\":\"#cc0000\",\"registryId\":\"stopsign\",\"isBeingConsumed\":false},\"inst_163\":{\"x\":40.155500203367076,\"z\":13.548641428127105,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_164\":{\"x\":68.12652127506207,\"z\":14.600537916033822,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_165\":{\"x\":69.36559310477958,\"z\":14.468614810672243,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_166\":{\"x\":73.45,\"z\":-14.4,\"scale\":2,\"rotationY\":0,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_167\":{\"x\":73.60668879859475,\"z\":-12.98640880413301,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_168\":{\"x\":53.92014228416729,\"z\":11.650296955130607,\"scale\":1.8,\"rotationY\":0,\"w\":1.5,\"h\":0.6,\"d\":0.5,\"color\":\"#8B6914\",\"registryId\":\"bench\",\"isBeingConsumed\":false},\"inst_169\":{\"x\":74.47,\"z\":14.7,\"scale\":1.3,\"rotationY\":4.71238898038469,\"w\":0.8,\"h\":2.2,\"d\":0.15,\"color\":\"#cc0000\",\"registryId\":\"stopsign\",\"isBeingConsumed\":false},\"inst_170\":{\"x\":41.95,\"z\":29.9,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_171\":{\"x\":68.12,\"z\":29.37,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_172\":{\"x\":69.29,\"z\":29.05,\"scale\":2,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_173\":{\"x\":73.76,\"z\":71.22,\"scale\":2,\"rotationY\":0,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_174\":{\"x\":73.8,\"z\":69.9,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_175\":{\"x\":55.14223993910391,\"z\":74.67683118083528,\"scale\":1.8,\"rotationY\":0,\"w\":1.5,\"h\":0.6,\"d\":0.5,\"color\":\"#8B6914\",\"registryId\":\"bench\",\"isBeingConsumed\":false},\"inst_176\":{\"x\":53.15,\"z\":74.28,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_177\":{\"x\":40.94,\"z\":29.59,\"scale\":1.3,\"rotationY\":4.71238898038469,\"w\":0.8,\"h\":2.2,\"d\":0.15,\"color\":\"#cc0000\",\"registryId\":\"stopsign\",\"isBeingConsumed\":false},\"inst_178\":{\"x\":32.98354009790104,\"z\":74.5403401889154,\"scale\":1.3,\"rotationY\":3.141592653589793,\"w\":0.8,\"h\":2.2,\"d\":0.15,\"color\":\"#cc0000\",\"registryId\":\"stopsign\",\"isBeingConsumed\":false},\"inst_179\":{\"x\":28.8,\"z\":73.75,\"scale\":1.9,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_180\":{\"x\":29.82,\"z\":73.81,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_181\":{\"x\":81.06307546785314,\"z\":18.422696545616077,\"scale\":2.4,\"rotationY\":3.141592653589793,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#d32f2f\",\"registryId\":\"car_compact_red\",\"isBeingConsumed\":false,\"waypoints\":[{\"x\":82.5,\"z\":17.8},{\"x\":-88,\"z\":19},{\"x\":-88.1,\"z\":24.3},{\"x\":81.7,\"z\":26.1}],\"anchorCol\":96,\"anchorRow\":86,\"isPrimary\":true},\"inst_182\":{\"x\":-41,\"z\":7,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false},\"inst_190\":{\"x\":-47,\"z\":1,\"scale\":1,\"rotationY\":4.71238898038469,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_191\":{\"x\":-41,\"z\":-5,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_192\":{\"x\":-41,\"z\":-11,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_yellow\",\"isBeingConsumed\":false},\"inst_195\":{\"x\":-47,\"z\":7,\"scale\":1,\"rotationY\":4.71238898038469,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false},\"inst_196\":{\"x\":-41,\"z\":1,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false},\"inst_197\":{\"x\":-47,\"z\":-5,\"scale\":1,\"rotationY\":4.71238898038469,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false},\"inst_198\":{\"x\":-47,\"z\":-11,\"scale\":1,\"rotationY\":4.71238898038469,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_yellow\",\"isBeingConsumed\":false},\"inst_203\":{\"x\":-17,\"z\":37,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_204\":{\"x\":-17,\"z\":43,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false,\"anchorCol\":80,\"anchorRow\":90,\"isPrimary\":true},\"inst_205\":{\"x\":-17,\"z\":49,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_yellow\",\"isBeingConsumed\":false,\"anchorCol\":80,\"anchorRow\":91,\"isPrimary\":true},\"inst_206\":{\"x\":-17,\"z\":55,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false,\"anchorCol\":80,\"anchorRow\":92,\"isPrimary\":true},\"inst_207\":{\"x\":-17,\"z\":61,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_208\":{\"x\":-17,\"z\":67,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_232\":{\"x\":-77,\"z\":7,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_233\":{\"x\":-83,\"z\":7,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_234\":{\"x\":-89,\"z\":7,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_255\":{\"x\":-89,\"z\":1,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false,\"anchorCol\":68,\"anchorRow\":83,\"isPrimary\":true},\"inst_256\":{\"x\":-83,\"z\":1,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false,\"anchorCol\":69,\"anchorRow\":83,\"isPrimary\":true},\"inst_257\":{\"x\":-77,\"z\":1,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_orange\",\"isBeingConsumed\":false},\"inst_293\":{\"x\":-10.4,\"z\":38.86,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_294\":{\"x\":-9.58,\"z\":29.93,\"scale\":1.7,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_295\":{\"x\":-9.4,\"z\":31.63,\"scale\":2,\"rotationY\":0,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_296\":{\"x\":-34.87,\"z\":13.29,\"scale\":1.8,\"rotationY\":0,\"w\":3,\"h\":4.9,\"d\":0.5,\"color\":\"#222222\",\"registryId\":\"billboard_soda\",\"isBeingConsumed\":false},\"inst_297\":{\"x\":-33.72,\"z\":15.17,\"scale\":1.4,\"rotationY\":0,\"w\":0.8,\"h\":2.2,\"d\":0.15,\"color\":\"#cc0000\",\"registryId\":\"stopsign\",\"isBeingConsumed\":false},\"inst_298\":{\"x\":-47.19,\"z\":14.02,\"scale\":2.2,\"rotationY\":0,\"w\":1.4,\"h\":1.6,\"d\":1.1,\"color\":\"#00acc1\",\"registryId\":\"street_vendor_stall\",\"isBeingConsumed\":false,\"anchorCol\":75,\"anchorRow\":85,\"isPrimary\":true},\"inst_299\":{\"x\":-47.39544140304476,\"z\":12.665392807171749,\"scale\":1.7,\"rotationY\":0,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false,\"anchorCol\":75,\"anchorRow\":85,\"isPrimary\":true},\"inst_300\":{\"x\":-47.17,\"z\":15.69,\"scale\":1.7,\"rotationY\":3.141592653589793,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_301\":{\"x\":-76.41,\"z\":14.69,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_302\":{\"x\":-75.55,\"z\":14.78,\"scale\":1.6,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_304\":{\"x\":-92.71415070715923,\"z\":15.324159162547147,\"scale\":1.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_305\":{\"x\":-86.75,\"z\":15.32,\"scale\":1.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_306\":{\"x\":-92.66,\"z\":21.37,\"scale\":1.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_307\":{\"x\":-91.24,\"z\":15.38,\"scale\":1.7,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_309\":{\"x\":-79.6,\"z\":10.65,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_310\":{\"x\":-97.72,\"z\":10.42,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_311\":{\"x\":-43.83,\"z\":10.38,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_312\":{\"x\":-55.2,\"z\":14.54,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_313\":{\"x\":-53.96,\"z\":15.1,\"scale\":1.6,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_314\":{\"x\":-21.003539760945444,\"z\":30.919192878870266,\"scale\":1.7,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_315\":{\"x\":-41.60497002977094,\"z\":30.65475866997522,\"scale\":1.8,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false,\"anchorCol\":76,\"anchorRow\":88,\"isPrimary\":true},\"inst_316\":{\"x\":-77.97,\"z\":29.29,\"scale\":1.8,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_317\":{\"x\":-44.51,\"z\":28.41,\"scale\":1,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_318\":{\"x\":-49.358535796772024,\"z\":40.697066838106124,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_319\":{\"x\":-20.601934941771738,\"z\":33.8319165534203,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_320\":{\"x\":-67.92,\"z\":33.86,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_321\":{\"x\":-74.43,\"z\":28.81,\"scale\":1.8,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_322\":{\"x\":-79.55,\"z\":33.71,\"scale\":1.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_323\":{\"x\":-25.423032429362276,\"z\":28.906630975602106,\"scale\":1.6,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false,\"anchorCol\":78,\"anchorRow\":88,\"isPrimary\":true},\"inst_324\":{\"x\":-33.22368950328243,\"z\":28.74732255902704,\"scale\":1.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false,\"anchorCol\":77,\"anchorRow\":88,\"isPrimary\":true},\"inst_325\":{\"x\":-25.535855028375373,\"z\":33.24187037533017,\"scale\":1.6,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_326\":{\"x\":-20.72,\"z\":28.6,\"scale\":1.6,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_327\":{\"x\":-14.61,\"z\":28.52,\"scale\":1.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_328\":{\"x\":-44.99,\"z\":28.59,\"scale\":1.6,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false,\"anchorCol\":75,\"anchorRow\":88,\"isPrimary\":true},\"inst_329\":{\"x\":-56.85,\"z\":28.69,\"scale\":1.8,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_330\":{\"x\":-50.44,\"z\":28.53,\"scale\":1.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_331\":{\"x\":-62.65,\"z\":28.52,\"scale\":1.8,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_332\":{\"x\":-68.71,\"z\":28.73,\"scale\":1.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_333\":{\"x\":-23.41,\"z\":28.37,\"scale\":2,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_334\":{\"x\":-41.13,\"z\":28.5,\"scale\":2,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_335\":{\"x\":-53.64,\"z\":28.54,\"scale\":2,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_336\":{\"x\":-65.77,\"z\":28.51,\"scale\":2,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_337\":{\"x\":-79.38,\"z\":28.64,\"scale\":2,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_339\":{\"x\":-17.01,\"z\":-34.54,\"scale\":2,\"rotationY\":0,\"w\":3,\"h\":4.9,\"d\":0.5,\"color\":\"#222222\",\"registryId\":\"billboard_soda\",\"isBeingConsumed\":false},\"inst_340\":{\"x\":-19.09,\"z\":-34.25,\"scale\":1.8,\"rotationY\":0,\"w\":0.36,\"h\":0.6,\"d\":0.36,\"color\":\"#ff6600\",\"registryId\":\"cone\",\"isBeingConsumed\":false},\"inst_341\":{\"x\":-17.025264989842654,\"z\":-33.425593658076835,\"scale\":1.7,\"rotationY\":0,\"w\":0.36,\"h\":0.6,\"d\":0.36,\"color\":\"#ff6600\",\"registryId\":\"cone\",\"isBeingConsumed\":false,\"anchorCol\":80,\"anchorRow\":77,\"isPrimary\":true},\"inst_342\":{\"x\":-14.98,\"z\":-34.28,\"scale\":1.7,\"rotationY\":0,\"w\":0.36,\"h\":0.6,\"d\":0.36,\"color\":\"#ff6600\",\"registryId\":\"cone\",\"isBeingConsumed\":false},\"inst_343\":{\"x\":-10.69,\"z\":-34.92,\"scale\":1.9,\"rotationY\":0,\"w\":1.4,\"h\":1.6,\"d\":1.1,\"color\":\"#00acc1\",\"registryId\":\"street_vendor_stall\",\"isBeingConsumed\":false},\"inst_344\":{\"x\":-11.00437407915626,\"z\":-32.92538395324805,\"scale\":1.4,\"rotationY\":3.141592653589793,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false,\"anchorCol\":81,\"anchorRow\":77,\"isPrimary\":true},\"inst_345\":{\"x\":-10.945436291352955,\"z\":-36.768512124108234,\"scale\":1.5,\"rotationY\":0,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false,\"anchorCol\":81,\"anchorRow\":77,\"isPrimary\":true},\"inst_347\":{\"x\":-38.491312502542854,\"z\":-18.309563064228705,\"scale\":2,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false,\"anchorCol\":76,\"anchorRow\":80,\"isPrimary\":true},\"inst_348\":{\"x\":-3.11,\"z\":-32.84,\"scale\":1.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_349\":{\"x\":15.25,\"z\":-32.62,\"scale\":1.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_350\":{\"x\":33.47,\"z\":-32.54,\"scale\":1.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_351\":{\"x\":48.95015201347224,\"z\":-32.25554116246157,\"scale\":1.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false,\"anchorCol\":91,\"anchorRow\":77,\"isPrimary\":true},\"inst_352\":{\"x\":68.61,\"z\":-33.07,\"scale\":1.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_353\":{\"x\":87.12,\"z\":-32.73,\"scale\":1.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_354\":{\"x\":14.73,\"z\":-37.06,\"scale\":1.7,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_355\":{\"x\":45.16,\"z\":-38.82,\"scale\":1.8,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_357\":{\"x\":5.75,\"z\":-32.72,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_358\":{\"x\":28.94,\"z\":-32.72,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_359\":{\"x\":75.03,\"z\":-32.8,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_360\":{\"x\":88.84,\"z\":-32.59,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_361\":{\"x\":30.07,\"z\":-19.59,\"scale\":1,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_362\":{\"x\":65.45,\"z\":-14.68,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_363\":{\"x\":45.71,\"z\":-13.92,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_364\":{\"x\":51.14,\"z\":-19.42,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_365\":{\"x\":64.43,\"z\":-13.33,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_366\":{\"x\":63.57,\"z\":-14.41,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_367\":{\"x\":37.73,\"z\":-18.18,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_368\":{\"x\":-33.31,\"z\":-35.16,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":1.5,\"h\":0.6,\"d\":0.5,\"color\":\"#8B6914\",\"registryId\":\"bench\",\"isBeingConsumed\":false},\"inst_369\":{\"x\":-33.16,\"z\":-32.59,\"scale\":1.9,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false,\"anchorCol\":77,\"anchorRow\":77,\"isPrimary\":true},\"inst_370\":{\"x\":-32.41495230100609,\"z\":-8.80960148620423,\"scale\":1.8,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false,\"anchorCol\":77,\"anchorRow\":81,\"isPrimary\":true},\"inst_371\":{\"x\":-40.319059990617205,\"z\":-18.47101842203966,\"scale\":1.5,\"rotationY\":0,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_372\":{\"x\":-37.51,\"z\":-1.54,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_373\":{\"x\":-37.44,\"z\":-14.07,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_374\":{\"x\":-37.91,\"z\":3.28,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_376\":{\"x\":-40.0182132057317,\"z\":-18.243925240933308,\"scale\":1.6,\"rotationY\":3.141592653589793,\"w\":0.7,\"h\":1.8,\"d\":0.7,\"color\":\"#d32f2f\",\"registryId\":\"phonebooth\",\"isBeingConsumed\":false},\"inst_377\":{\"x\":6.997734407667135,\"z\":-32.741357736649604,\"scale\":1.2,\"rotationY\":3.141592653589793,\"w\":0.7,\"h\":1.8,\"d\":0.7,\"color\":\"#d32f2f\",\"registryId\":\"phonebooth\",\"isBeingConsumed\":false},\"inst_378\":{\"x\":52.82,\"z\":18.67,\"scale\":2.4,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false,\"waypoints\":[{\"x\":-88.3,\"z\":19.1},{\"x\":-87.8,\"z\":23.9},{\"x\":82.2,\"z\":26.9},{\"x\":81.4,\"z\":18.7}]},\"inst_379\":{\"x\":65.96030897694673,\"z\":18.60150697923032,\"scale\":2.4,\"rotationY\":3.141592653589793,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#ffcc00\",\"registryId\":\"car_taxi_yellow\",\"isBeingConsumed\":false,\"waypoints\":[{\"x\":-88.3,\"z\":19.1},{\"x\":-87.8,\"z\":23.9},{\"x\":82.2,\"z\":26.9},{\"x\":81.4,\"z\":18.7}],\"anchorCol\":94,\"anchorRow\":86,\"isPrimary\":true},\"inst_380\":{\"x\":-51.78,\"z\":19.35,\"scale\":2.4,\"rotationY\":3.2,\"w\":2.3,\"h\":1.3,\"d\":0.95,\"color\":\"#ffffff\",\"registryId\":\"truck_delivery_white\",\"isBeingConsumed\":false,\"waypoints\":[{\"x\":-87.6,\"z\":18.6},{\"x\":-87.6,\"z\":23.9},{\"x\":82.1,\"z\":26.2},{\"x\":81.7,\"z\":17.7}],\"anchorCol\":74,\"anchorRow\":86,\"isPrimary\":true},\"inst_383\":{\"x\":-49.55387730212887,\"z\":78.75622578021532,\"scale\":2.6,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false,\"waypoints\":[{\"x\":-88.5,\"z\":78.2},{\"x\":-88.2,\"z\":84.4},{\"x\":84.2,\"z\":84.7},{\"x\":85.1,\"z\":-28.1},{\"x\":79.7,\"z\":-29.4},{\"x\":79,\"z\":78}],\"initialNextWaypointId\":1},\"inst_387\":{\"x\":-41.22,\"z\":78.41,\"scale\":2.6,\"rotationY\":3.141592653589793,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#d32f2f\",\"registryId\":\"car_compact_red\",\"isBeingConsumed\":false,\"waypoints\":[{\"x\":-88.5,\"z\":78.2},{\"x\":-88.2,\"z\":84.4},{\"x\":84.2,\"z\":84.7},{\"x\":85.1,\"z\":-28.1},{\"x\":79.7,\"z\":-29.4},{\"x\":79,\"z\":78}],\"initialNextWaypointId\":1,\"anchorCol\":76,\"anchorRow\":96,\"isPrimary\":true},\"inst_388\":{\"x\":-3.0624731976539934,\"z\":85.22581792639508,\"scale\":2.6,\"rotationY\":0,\"w\":2.3,\"h\":1.3,\"d\":0.95,\"color\":\"#ffffff\",\"registryId\":\"truck_delivery_white\",\"isBeingConsumed\":false,\"waypoints\":[{\"x\":-88.5,\"z\":78.2},{\"x\":-88.2,\"z\":84.4},{\"x\":84.2,\"z\":84.7},{\"x\":85.1,\"z\":-28.1},{\"x\":79.7,\"z\":-29.4},{\"x\":79,\"z\":78}],\"initialNextWaypointId\":3,\"anchorCol\":82,\"anchorRow\":97,\"isPrimary\":true},\"inst_389\":{\"x\":-28.560885940691175,\"z\":37.39295962315967,\"scale\":2.4,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false},\"inst_390\":{\"x\":-28.117095823700453,\"z\":42.89524930414916,\"scale\":2.4,\"rotationY\":0,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#d32f2f\",\"registryId\":\"car_compact_red\",\"isBeingConsumed\":false,\"anchorCol\":78,\"anchorRow\":90,\"isPrimary\":true},\"inst_391\":{\"x\":-28.421839770607207,\"z\":49.18770876451608,\"scale\":2.4,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false,\"anchorCol\":78,\"anchorRow\":91,\"isPrimary\":true},\"inst_392\":{\"x\":-28.97,\"z\":55.42,\"scale\":2.4,\"rotationY\":0,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#ffcc00\",\"registryId\":\"car_taxi_yellow\",\"isBeingConsumed\":false,\"anchorCol\":78,\"anchorRow\":92,\"isPrimary\":true},\"inst_393\":{\"x\":-28.946931684786694,\"z\":61.45223599249438,\"scale\":2.4,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false},\"inst_394\":{\"x\":-40.87,\"z\":37.08,\"scale\":2.4,\"rotationY\":3.141592653589793,\"w\":2.3,\"h\":1.3,\"d\":0.95,\"color\":\"#ffffff\",\"registryId\":\"truck_delivery_white\",\"isBeingConsumed\":false,\"anchorCol\":76,\"anchorRow\":89,\"isPrimary\":true},\"inst_395\":{\"x\":-40.775386282746794,\"z\":43.45769312959759,\"scale\":2.4,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false},\"inst_396\":{\"x\":-40.8096041821221,\"z\":49.89536318160005,\"scale\":2.4,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false},\"inst_397\":{\"x\":-40.28,\"z\":67.56,\"scale\":2.4,\"rotationY\":3.141592653589793,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#d32f2f\",\"registryId\":\"car_compact_red\",\"isBeingConsumed\":false,\"anchorCol\":76,\"anchorRow\":94,\"isPrimary\":true},\"inst_398\":{\"x\":-40.61,\"z\":61.08,\"scale\":2.4,\"rotationY\":3.141592653589793,\"w\":2.3,\"h\":1.3,\"d\":0.95,\"color\":\"#ffffff\",\"registryId\":\"truck_delivery_white\",\"isBeingConsumed\":false,\"anchorCol\":76,\"anchorRow\":93,\"isPrimary\":true},\"inst_400\":{\"x\":-48.44010151405732,\"z\":71.60203225887219,\"scale\":2,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false,\"anchorCol\":75,\"anchorRow\":95,\"isPrimary\":true},\"inst_401\":{\"x\":-47.92919173860209,\"z\":32.66173485529,\"scale\":1.9,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"large_tree\",\"isBeingConsumed\":false,\"anchorCol\":75,\"anchorRow\":88,\"isPrimary\":true},\"inst_105\":{\"x\":19,\"z\":67,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false,\"anchorCol\":86,\"anchorRow\":94,\"isPrimary\":true},\"inst_104\":{\"x\":25,\"z\":67,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false},\"inst_103\":{\"x\":25,\"z\":61,\"scale\":1,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_yellow\",\"isBeingConsumed\":false,\"anchorCol\":87,\"anchorRow\":93,\"isPrimary\":true},\"inst_124\":{\"x\":19,\"z\":61,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_red\",\"isBeingConsumed\":false},\"inst_402\":{\"x\":-60.35471150239535,\"z\":-65.86178376333933,\"scale\":2.8,\"rotationY\":4.71238898038469,\"w\":1.4,\"h\":1.6,\"d\":1.1,\"color\":\"#00acc1\",\"registryId\":\"street_vendor_stall\",\"isBeingConsumed\":false},\"inst_403\":{\"x\":-61.084288812017746,\"z\":-48.63476235626414,\"scale\":2.8,\"rotationY\":4.71238898038469,\"w\":1.4,\"h\":1.6,\"d\":1.1,\"color\":\"#00acc1\",\"registryId\":\"street_vendor_stall\",\"isBeingConsumed\":false},\"inst_404\":{\"x\":-44.89496523864857,\"z\":-63.74250745962591,\"scale\":2.8,\"rotationY\":0,\"w\":1.4,\"h\":1.6,\"d\":1.1,\"color\":\"#00acc1\",\"registryId\":\"street_vendor_stall\",\"isBeingConsumed\":false},\"inst_405\":{\"x\":-58.18233642104112,\"z\":-30.23595404198268,\"scale\":2.8,\"rotationY\":5.2,\"w\":1.4,\"h\":1.6,\"d\":1.1,\"color\":\"#00acc1\",\"registryId\":\"street_vendor_stall\",\"isBeingConsumed\":false},\"inst_406\":{\"x\":-61.202707766724046,\"z\":-45.08793005779801,\"scale\":2,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_407\":{\"x\":-61.19622065026206,\"z\":-63.500660407324204,\"scale\":2,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_409\":{\"x\":-42.380951709887114,\"z\":-61.06797322707845,\"scale\":2,\"rotationY\":0,\"w\":0.48,\"h\":0.75,\"d\":0.48,\"color\":\"#555555\",\"registryId\":\"trashcan\",\"isBeingConsumed\":false},\"inst_410\":{\"x\":-58.95754953768518,\"z\":-76.01134627698967,\"scale\":2.4,\"rotationY\":0,\"w\":1.5,\"h\":0.6,\"d\":0.5,\"color\":\"#8B6914\",\"registryId\":\"bench\",\"isBeingConsumed\":false},\"inst_411\":{\"x\":-70.7972614944282,\"z\":-35.34874109377023,\"scale\":2.4,\"rotationY\":1.5707963267948966,\"w\":1.5,\"h\":0.6,\"d\":0.5,\"color\":\"#8B6914\",\"registryId\":\"bench\",\"isBeingConsumed\":false},\"inst_412\":{\"x\":-52.86191005207864,\"z\":-71.26206285395595,\"scale\":2.7,\"rotationY\":1.5707963267948966,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#d32f2f\",\"registryId\":\"car_compact_red\",\"isBeingConsumed\":false,\"anchorCol\":74,\"anchorRow\":71,\"isPrimary\":true},\"inst_413\":{\"x\":-48.73016877450325,\"z\":-71.12468053172395,\"scale\":2.7,\"rotationY\":1.5707963267948966,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false},\"inst_414\":{\"x\":-65.3089271774692,\"z\":-52.77707911742283,\"scale\":2.7,\"rotationY\":3.141592653589793,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#d32f2f\",\"registryId\":\"car_compact_red\",\"isBeingConsumed\":false,\"anchorCol\":72,\"anchorRow\":74,\"isPrimary\":true},\"inst_415\":{\"x\":-64.40092506612417,\"z\":-35.219503962971245,\"scale\":2.7,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false},\"inst_416\":{\"x\":-64.33055029944813,\"z\":-40.326840877496586,\"scale\":2.7,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false},\"inst_417\":{\"x\":-63.52,\"z\":-76.41,\"scale\":2,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false},\"inst_418\":{\"x\":-36.34926416003228,\"z\":-58.480943422664645,\"scale\":2,\"rotationY\":3.141592653589793,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false,\"anchorCol\":77,\"anchorRow\":73,\"isPrimary\":true},\"inst_419\":{\"x\":-54.56837080448464,\"z\":-19.192622202689083,\"scale\":2,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":2.5,\"d\":0.4,\"color\":\"#3a3a3a\",\"registryId\":\"lamp\",\"isBeingConsumed\":false,\"anchorCol\":74,\"anchorRow\":80,\"isPrimary\":true},\"inst_420\":{\"x\":-68.58240775889367,\"z\":-18.917967447643974,\"scale\":2.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_421\":{\"x\":-68.52529257531698,\"z\":-74.935297126329,\"scale\":2.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_422\":{\"x\":-69.05661694841474,\"z\":-30.66898628184598,\"scale\":2.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_423\":{\"x\":-39.55756874639418,\"z\":-75.9152315136844,\"scale\":2.5,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"small_tree\",\"isBeingConsumed\":false},\"inst_424\":{\"x\":-62.91,\"z\":-65.26,\"scale\":2.3,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_425\":{\"x\":-63.18047839181215,\"z\":-48.30798875086239,\"scale\":2.3,\"rotationY\":1.5707963267948966,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_426\":{\"x\":-59.89812013541193,\"z\":-28.8192364905698,\"scale\":2.3,\"rotationY\":2.2,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_427\":{\"x\":-45.1415142203873,\"z\":-65.96872922358307,\"scale\":2.3,\"rotationY\":0,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_428\":{\"x\":-40.62208039393454,\"z\":-52.639608126889314,\"scale\":2.7,\"rotationY\":0,\"w\":1.55,\"h\":0.78,\"d\":0.85,\"color\":\"#d32f2f\",\"registryId\":\"car_compact_red\",\"isBeingConsumed\":false,\"anchorCol\":76,\"anchorRow\":74,\"isPrimary\":true},\"inst_429\":{\"x\":-40.69033995404005,\"z\":-45.27803271077606,\"scale\":2.7,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"car_sedan_blue\",\"isBeingConsumed\":false},\"inst_430\":{\"x\":-45.25934576023813,\"z\":-58.94968963056547,\"scale\":2.3,\"rotationY\":3.141592653589793,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_431\":{\"x\":-57.48857703139866,\"z\":-47.86830344257698,\"scale\":2.3,\"rotationY\":4.71238898038469,\"w\":0.4,\"h\":1.6,\"d\":0.4,\"color\":\"#ffaa44\",\"registryId\":\"person\",\"isBeingConsumed\":false},\"inst_432\":{\"x\":-43.0357316019814,\"z\":-65.66609610609518,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_433\":{\"x\":-42.32259729174192,\"z\":-64.59768714603867,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_435\":{\"x\":-59.6154077044865,\"z\":-63.30109848609174,\"scale\":2,\"rotationY\":0,\"w\":0.44,\"h\":0.55,\"d\":0.44,\"color\":\"#242424\",\"registryId\":\"trashbag\",\"isBeingConsumed\":false},\"inst_437\":{\"x\":34,\"z\":-50,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"office_small\",\"isBeingConsumed\":false,\"anchorCol\":88,\"anchorRow\":74,\"isPrimary\":true},\"inst_438\":{\"x\":52,\"z\":-50,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"office_medium\",\"isBeingConsumed\":false,\"anchorCol\":85,\"anchorRow\":70,\"isPrimary\":true},\"inst_441\":{\"x\":-68,\"z\":52,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"bank\",\"isBeingConsumed\":false,\"anchorCol\":71,\"anchorRow\":89,\"isPrimary\":true},\"inst_442\":{\"x\":16,\"z\":40,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"office_small\",\"isBeingConsumed\":false,\"anchorCol\":85,\"anchorRow\":89,\"isPrimary\":true},\"inst_444\":{\"x\":64,\"z\":64,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"office_medium\",\"isBeingConsumed\":false,\"anchorCol\":93,\"anchorRow\":93,\"isPrimary\":true},\"inst_445\":{\"x\":49,\"z\":43,\"scale\":1,\"rotationY\":4.71238898038469,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false,\"anchorCol\":91,\"anchorRow\":90,\"isPrimary\":true},\"inst_446\":{\"x\":52,\"z\":64,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"office_small\",\"isBeingConsumed\":false,\"anchorCol\":91,\"anchorRow\":93,\"isPrimary\":true},\"inst_447\":{\"x\":67,\"z\":43,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false,\"anchorCol\":94,\"anchorRow\":90,\"isPrimary\":true},\"inst_448\":{\"x\":13,\"z\":-47,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false,\"anchorCol\":85,\"anchorRow\":75,\"isPrimary\":true},\"inst_450\":{\"x\":7,\"z\":-47,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false,\"anchorCol\":84,\"anchorRow\":75,\"isPrimary\":true},\"inst_451\":{\"x\":7,\"z\":-53,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false,\"anchorCol\":84,\"anchorRow\":74,\"isPrimary\":true},\"inst_452\":{\"x\":13,\"z\":-53,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false,\"anchorCol\":85,\"anchorRow\":74,\"isPrimary\":true},\"inst_453\":{\"x\":19,\"z\":-47,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false,\"anchorCol\":86,\"anchorRow\":75,\"isPrimary\":true},\"inst_454\":{\"x\":19,\"z\":-53,\"scale\":1,\"rotationY\":3.141592653589793,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_blue\",\"isBeingConsumed\":false,\"anchorCol\":86,\"anchorRow\":74,\"isPrimary\":true},\"inst_457\":{\"x\":73,\"z\":-47,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false,\"anchorCol\":95,\"anchorRow\":75,\"isPrimary\":true},\"inst_458\":{\"x\":79,\"z\":-47,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false,\"anchorCol\":96,\"anchorRow\":75,\"isPrimary\":true},\"inst_459\":{\"x\":85,\"z\":-47,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"apartment_small_green\",\"isBeingConsumed\":false,\"anchorCol\":97,\"anchorRow\":75,\"isPrimary\":true},\"inst_461\":{\"x\":-8,\"z\":-50,\"scale\":1,\"rotationY\":0,\"w\":0.5,\"h\":0.5,\"d\":0.5,\"color\":\"#888888\",\"registryId\":\"office_small\",\"isBeingConsumed\":false,\"anchorCol\":81,\"anchorRow\":74,\"isPrimary\":true}},\"terrainCellSize\":6,\"terrainRegistry\":[{\"color\":\"#4CAF50\",\"id\":\"grass\",\"displayName\":\"Grass\"},{\"displayName\":\"Road\",\"color\":\"#3a3a3a\",\"id\":\"road\"},{\"displayName\":\"Sidewalk\",\"color\":\"#cccccc\",\"id\":\"sidewalk\"},{\"color\":\"#3a3a3a\",\"id\":\"intersection\",\"displayName\":\"Intersection\"},{\"displayName\":\"Wide Road\",\"color\":\"#3a3a3a\",\"heightInCells\":1,\"widthInCells\":2,\"id\":\"wideRoad\"},{\"displayName\":\"Water\",\"color\":\"#00BCD4\",\"id\":\"water\"}],\"terrainCells\":{\"84_84\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"84_83\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"84_82\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"84_81\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"83_81\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"82_81\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"81_81\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"81_82\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"81_83\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"81_84\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"83_84\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"83_83\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"83_82\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"82_82\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"82_83\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"82_84\":{\"terrainType\":\"grass\",\"rotation\":0,\"isPrimary\":true},\"85_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"83_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"82_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"83_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_85\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"87_85\":{\"occupiedBy\":\"86_85\"},\"86_84\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"87_84\":{\"occupiedBy\":\"86_84\"},\"86_83\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"87_83\":{\"occupiedBy\":\"86_83\"},\"86_80\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"87_80\":{\"occupiedBy\":\"86_80\"},\"86_82\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"87_82\":{\"occupiedBy\":\"86_82\"},\"86_81\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"87_81\":{\"occupiedBy\":\"86_81\"},\"85_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"85_79\":{\"occupiedBy\":\"85_78\"},\"84_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"84_79\":{\"occupiedBy\":\"84_78\"},\"83_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"83_79\":{\"occupiedBy\":\"83_78\"},\"82_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"82_79\":{\"occupiedBy\":\"82_78\"},\"81_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"81_79\":{\"occupiedBy\":\"81_78\"},\"80_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"80_79\":{\"occupiedBy\":\"80_78\"},\"78_80\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"79_80\":{\"occupiedBy\":\"78_80\"},\"78_81\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"79_81\":{\"occupiedBy\":\"78_81\"},\"78_82\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"79_82\":{\"occupiedBy\":\"78_82\"},\"78_83\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"79_83\":{\"occupiedBy\":\"78_83\"},\"78_84\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"79_84\":{\"occupiedBy\":\"78_84\"},\"78_85\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"79_85\":{\"occupiedBy\":\"78_85\"},\"80_86\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"80_87\":{\"occupiedBy\":\"80_86\"},\"81_86\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"81_87\":{\"occupiedBy\":\"81_86\"},\"84_86\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"84_87\":{\"occupiedBy\":\"84_86\"},\"85_86\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"85_87\":{\"occupiedBy\":\"85_86\"},\"86_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"87_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"87_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"86_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"79_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"79_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"79_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"79_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"86_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"87_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"86_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"87_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"88_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"88_87\":{\"occupiedBy\":\"88_86\"},\"90_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"90_87\":{\"occupiedBy\":\"90_86\"},\"91_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"91_87\":{\"occupiedBy\":\"91_86\"},\"92_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"92_87\":{\"occupiedBy\":\"92_86\"},\"93_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"93_87\":{\"occupiedBy\":\"93_86\"},\"94_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"94_87\":{\"occupiedBy\":\"94_86\"},\"95_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"95_87\":{\"occupiedBy\":\"95_86\"},\"96_89\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"97_89\":{\"occupiedBy\":\"96_89\"},\"96_90\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"97_90\":{\"occupiedBy\":\"96_90\"},\"96_91\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"97_91\":{\"occupiedBy\":\"96_91\"},\"96_92\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"97_92\":{\"occupiedBy\":\"96_92\"},\"96_93\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"97_93\":{\"occupiedBy\":\"96_93\"},\"96_94\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"97_94\":{\"occupiedBy\":\"96_94\"},\"96_95\":{\"terrainType\":\"wideRoad\",\"rotation\":3.141592653589793,\"isPrimary\":true},\"97_95\":{\"occupiedBy\":\"96_95\"},\"95_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"95_97\":{\"occupiedBy\":\"95_96\"},\"94_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"94_97\":{\"occupiedBy\":\"94_96\"},\"91_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"91_97\":{\"occupiedBy\":\"91_96\"},\"90_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"90_97\":{\"occupiedBy\":\"90_96\"},\"88_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"88_97\":{\"occupiedBy\":\"88_96\"},\"87_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"87_97\":{\"occupiedBy\":\"87_96\"},\"86_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"86_97\":{\"occupiedBy\":\"86_96\"},\"85_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"85_97\":{\"occupiedBy\":\"85_96\"},\"84_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"84_97\":{\"occupiedBy\":\"84_96\"},\"81_96\":{\"terrainType\":\"wideRoad\",\"rotation\":4.71238898038469,\"isPrimary\":true},\"81_97\":{\"occupiedBy\":\"81_96\"},\"82_95\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"83_95\":{\"occupiedBy\":\"82_95\"},\"80_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"80_97\":{\"occupiedBy\":\"80_96\"},\"77_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"77_97\":{\"occupiedBy\":\"77_96\"},\"76_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"76_97\":{\"occupiedBy\":\"76_96\"},\"79_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"79_97\":{\"occupiedBy\":\"79_96\"},\"75_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"75_97\":{\"occupiedBy\":\"75_96\"},\"74_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"74_97\":{\"occupiedBy\":\"74_96\"},\"73_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"73_97\":{\"occupiedBy\":\"73_96\"},\"68_95\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"69_95\":{\"occupiedBy\":\"68_95\"},\"68_94\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"69_94\":{\"occupiedBy\":\"68_94\"},\"68_93\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"69_93\":{\"occupiedBy\":\"68_93\"},\"68_92\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"69_92\":{\"occupiedBy\":\"68_92\"},\"68_91\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"69_91\":{\"occupiedBy\":\"68_91\"},\"68_90\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"69_90\":{\"occupiedBy\":\"68_90\"},\"68_89\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"69_89\":{\"occupiedBy\":\"68_89\"},\"68_88\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"69_88\":{\"occupiedBy\":\"68_88\"},\"70_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"70_97\":{\"occupiedBy\":\"70_96\"},\"71_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"71_97\":{\"occupiedBy\":\"71_96\"},\"72_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"72_97\":{\"occupiedBy\":\"72_96\"},\"69_96\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"69_97\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"68_97\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"68_96\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"96_96\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"96_97\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"97_97\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"97_96\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"96_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"96_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"97_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"97_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"96_88\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"97_88\":{\"occupiedBy\":\"96_88\"},\"70_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"70_87\":{\"occupiedBy\":\"70_86\"},\"71_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"71_87\":{\"occupiedBy\":\"71_86\"},\"75_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"75_87\":{\"occupiedBy\":\"75_86\"},\"76_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"76_87\":{\"occupiedBy\":\"76_86\"},\"77_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"77_87\":{\"occupiedBy\":\"77_86\"},\"69_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"68_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"68_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"69_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"83_96\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"83_97\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"82_96\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"82_97\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"83_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"82_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"82_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"83_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"89_89\":{\"terrainType\":\"road\",\"rotation\":0,\"isPrimary\":true},\"89_88\":{\"terrainType\":\"road\",\"rotation\":0,\"isPrimary\":true},\"89_90\":{\"terrainType\":\"road\",\"rotation\":0,\"isPrimary\":true},\"89_91\":{\"terrainType\":\"road\",\"rotation\":0,\"isPrimary\":true},\"89_92\":{\"terrainType\":\"road\",\"rotation\":0,\"isPrimary\":true},\"89_93\":{\"terrainType\":\"road\",\"rotation\":0,\"isPrimary\":true},\"89_94\":{\"terrainType\":\"road\",\"rotation\":0,\"isPrimary\":true},\"89_95\":{\"terrainType\":\"road\",\"rotation\":0,\"isPrimary\":true},\"89_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"89_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"89_97\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"89_96\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"84_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"92_97\":{\"occupiedBy\":\"92_96\"},\"93_96\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"93_97\":{\"occupiedBy\":\"93_96\"},\"88_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"96_85\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"97_85\":{\"occupiedBy\":\"96_85\"},\"96_84\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"97_84\":{\"occupiedBy\":\"96_84\"},\"96_83\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"97_83\":{\"occupiedBy\":\"96_83\"},\"96_82\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"97_82\":{\"occupiedBy\":\"96_82\"},\"96_81\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"97_81\":{\"occupiedBy\":\"96_81\"},\"96_80\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"97_80\":{\"occupiedBy\":\"96_80\"},\"95_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"95_79\":{\"occupiedBy\":\"95_78\"},\"94_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"94_79\":{\"occupiedBy\":\"94_78\"},\"93_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"93_79\":{\"occupiedBy\":\"93_78\"},\"92_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"92_79\":{\"occupiedBy\":\"92_78\"},\"91_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"91_79\":{\"occupiedBy\":\"91_78\"},\"90_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"90_79\":{\"occupiedBy\":\"90_78\"},\"89_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"89_79\":{\"occupiedBy\":\"89_78\"},\"88_78\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"88_79\":{\"occupiedBy\":\"88_78\"},\"96_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"96_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"97_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"94_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"97_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"81_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"82_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"69_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"68_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_86\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_87\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_86\":{\"terrainType\":\"wideRoad\",\"rotation\":1.5707963267948966,\"isPrimary\":true},\"74_87\":{\"occupiedBy\":\"74_86\"},\"74_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_79\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_78\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_85\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"73_85\":{\"occupiedBy\":\"72_85\"},\"72_84\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"73_84\":{\"occupiedBy\":\"72_84\"},\"72_82\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"73_82\":{\"occupiedBy\":\"72_82\"},\"72_81\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"73_81\":{\"occupiedBy\":\"72_81\"},\"74_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_86\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_87\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_96\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_97\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"79_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"79_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"82_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"83_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"96_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"97_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_77\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_78\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_79\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_85\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_95\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_96\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_97\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"97_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"96_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"82_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"83_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"79_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"78_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"69_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"68_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_98\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"82_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"83_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"96_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"97_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"78_77\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"79_77\":{\"occupiedBy\":\"78_77\"},\"78_76\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"79_76\":{\"occupiedBy\":\"78_76\"},\"78_75\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"79_75\":{\"occupiedBy\":\"78_75\"},\"78_73\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"79_73\":{\"occupiedBy\":\"78_73\"},\"78_74\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"79_74\":{\"occupiedBy\":\"78_74\"},\"77_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_71\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_72\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"78_89\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_90\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_91\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_92\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_93\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_94\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"77_92\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"77_89\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"77_90\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"77_91\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"77_93\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"77_94\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_94\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_93\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_92\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_91\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_90\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_89\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"79_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"79_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"79_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"79_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"79_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"79_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"78_88\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_95\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_96\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_97\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"88_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"82_88\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"83_88\":{\"occupiedBy\":\"82_88\"},\"82_89\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"83_89\":{\"occupiedBy\":\"82_89\"},\"82_90\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"83_90\":{\"occupiedBy\":\"82_90\"},\"82_91\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"83_91\":{\"occupiedBy\":\"82_91\"},\"82_92\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"83_92\":{\"occupiedBy\":\"82_92\"},\"82_93\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"83_93\":{\"occupiedBy\":\"82_93\"},\"82_94\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"83_94\":{\"occupiedBy\":\"82_94\"},\"81_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"77_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"75_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"75_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_79\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_78\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_77\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_77\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_77\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"75_77\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_77\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_76\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"75_76\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_76\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_76\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_76\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_75\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_75\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_75\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"75_75\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_75\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_74\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"75_74\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_74\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_74\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_74\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_73\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_73\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_73\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"75_73\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_73\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_72\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"76_71\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"75_71\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_72\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_72\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_72\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"75_72\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"74_71\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"73_71\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"72_71\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"71_76\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_72\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_71\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"73_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"74_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_71\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_72\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"82_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"83_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"83_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"96_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"97_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_86\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_87\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"68_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"67_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"69_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"71_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"78_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"79_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_70\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"82_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"83_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"95_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"96_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"97_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"98_73\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"81_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"82_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"84_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_74\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"96_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"97_75\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"88_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"89_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"90_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"94_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"93_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"91_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"92_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_90\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"87_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"86_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"85_93\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_94\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_92\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_91\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"80_89\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"68_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"69_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"70_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"68_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"69_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_84\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_83\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_82\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"75_81\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"72_83\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"73_83\":{\"occupiedBy\":\"72_83\"},\"72_80\":{\"terrainType\":\"wideRoad\",\"rotation\":0,\"isPrimary\":true},\"73_80\":{\"occupiedBy\":\"72_80\"},\"74_80\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"78_71\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"79_71\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"79_72\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"78_72\":{\"terrainType\":\"intersection\",\"rotation\":0,\"isPrimary\":true},\"77_71\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_72\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"76_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true},\"77_88\":{\"terrainType\":\"sidewalk\",\"rotation\":0,\"isPrimary\":true}},\"showGridLines\":false,\"showAIWaypoints\":false,\"carSpeed\":8,\"joystickThreshold\":25,\"baseHoleScale\":1.4,\"ambientIntensity\":0.55,\"sunIntensity\":0.85,\"sunHeight\":25,\"sunOffsetX\":15,\"sunOffsetZ\":8,\"selectedEditLevel\":\"5\",\"victoryCompletionPercent\":95,\"ghostObjectOpacity\":0.2,\"obstructionFadeOpacity\":0.1,\"obstructionViewRadius\":6.5,\"obstructionPadding\":3,\"startAtSelectedLevel\":false,\"objectCellOccupancy\":{\"85_84\":\"inst_002\",\"84_84\":\"inst_037\",\"85_85\":\"inst_012\",\"84_85\":\"inst_054\",\"82_81\":\"inst_046\",\"82_82\":\"inst_068\",\"84_81\":\"inst_050\",\"83_84\":\"inst_051\",\"81_84\":\"inst_064\",\"84_83\":\"inst_036\",\"84_82\":\"inst_042\",\"81_81\":\"inst_049\",\"82_84\":\"inst_053\",\"85_81\":\"inst_055\",\"84_80\":\"inst_056\",\"85_80\":\"inst_057\",\"81_80\":\"inst_058\",\"80_84\":\"inst_059\",\"80_81\":\"inst_060\",\"77_86\":\"inst_061\",\"81_83\":\"inst_065\",\"82_83\":\"inst_066\",\"81_82\":\"inst_067\",\"80_85\":\"inst_069\",\"92_89\":\"inst_090\",\"93_90\":\"inst_091\",\"94_89\":\"inst_093\",\"91_89\":\"inst_095\",\"85_94\":\"inst_106\",\"85_93\":\"inst_107\",\"93_89\":\"inst_125\",\"92_90\":\"inst_131\",\"89_83\":\"inst_135\",\"90_83\":\"inst_136\",\"91_83\":\"inst_137\",\"92_83\":\"inst_138\",\"93_83\":\"inst_139\",\"93_81\":\"inst_145\",\"93_82\":\"inst_146\",\"92_81\":\"inst_148\",\"91_81\":\"inst_149\",\"90_81\":\"inst_363\",\"89_81\":\"inst_151\",\"89_82\":\"inst_152\",\"92_82\":\"inst_153\",\"91_82\":\"inst_154\",\"90_82\":\"inst_155\",\"89_85\":\"inst_156\",\"92_85\":\"inst_168\",\"92_84\":\"inst_158\",\"94_84\":\"inst_159\",\"95_95\":\"inst_173\",\"94_82\":\"inst_161\",\"88_85\":\"inst_162\",\"90_85\":\"inst_163\",\"94_85\":\"inst_165\",\"95_80\":\"inst_166\",\"95_81\":\"inst_167\",\"95_85\":\"inst_169\",\"90_88\":\"inst_177\",\"94_88\":\"inst_172\",\"95_94\":\"inst_174\",\"92_95\":\"inst_176\",\"88_95\":\"inst_180\",\"96_86\":\"inst_181\",\"76_84\":\"inst_182\",\"75_83\":\"inst_190\",\"76_82\":\"inst_191\",\"76_81\":\"inst_192\",\"75_84\":\"inst_195\",\"76_83\":\"inst_196\",\"75_82\":\"inst_197\",\"75_81\":\"inst_198\",\"80_89\":\"inst_203\",\"80_90\":\"inst_204\",\"80_91\":\"inst_205\",\"80_92\":\"inst_206\",\"80_93\":\"inst_207\",\"80_94\":\"inst_208\",\"70_84\":\"inst_232\",\"69_84\":\"inst_233\",\"68_84\":\"inst_234\",\"68_83\":\"inst_255\",\"69_83\":\"inst_256\",\"70_83\":\"inst_257\",\"81_89\":\"inst_293\",\"81_88\":\"inst_295\",\"77_85\":\"inst_297\",\"75_85\":\"inst_300\",\"70_85\":\"inst_309\",\"67_85\":\"inst_310\",\"68_85\":\"inst_307\",\"67_86\":\"inst_306\",\"76_85\":\"inst_311\",\"74_85\":\"inst_313\",\"79_88\":\"inst_333\",\"76_88\":\"inst_334\",\"70_88\":\"inst_337\",\"75_88\":\"inst_401\",\"75_90\":\"inst_318\",\"72_88\":\"inst_336\",\"78_88\":\"inst_323\",\"77_88\":\"inst_324\",\"80_88\":\"inst_327\",\"73_88\":\"inst_329\",\"74_88\":\"inst_335\",\"71_88\":\"inst_332\",\"80_77\":\"inst_342\",\"81_77\":\"inst_345\",\"76_80\":\"inst_376\",\"82_77\":\"inst_348\",\"85_77\":\"inst_354\",\"88_77\":\"inst_358\",\"91_77\":\"inst_351\",\"94_77\":\"inst_352\",\"97_77\":\"inst_353\",\"90_76\":\"inst_355\",\"84_77\":\"inst_377\",\"95_77\":\"inst_359\",\"98_77\":\"inst_360\",\"88_80\":\"inst_361\",\"94_80\":\"inst_362\",\"91_80\":\"inst_364\",\"94_81\":\"inst_365\",\"93_80\":\"inst_366\",\"89_80\":\"inst_367\",\"77_77\":\"inst_369\",\"77_81\":\"inst_370\",\"77_83\":\"inst_374\",\"77_80\":\"inst_373\",\"92_86\":\"inst_378\",\"94_86\":\"inst_379\",\"74_86\":\"inst_380\",\"75_96\":\"inst_383\",\"76_96\":\"inst_387\",\"82_97\":\"inst_388\",\"78_89\":\"inst_389\",\"78_90\":\"inst_390\",\"78_91\":\"inst_391\",\"78_92\":\"inst_392\",\"78_93\":\"inst_393\",\"76_89\":\"inst_394\",\"76_90\":\"inst_395\",\"76_91\":\"inst_396\",\"76_94\":\"inst_397\",\"76_93\":\"inst_398\",\"75_95\":\"inst_400\",\"86_94\":\"inst_105\",\"87_94\":\"inst_104\",\"87_93\":\"inst_103\",\"86_93\":\"inst_124\",\"73_72\":\"inst_435\",\"73_75\":\"inst_431\",\"75_72\":\"inst_427\",\"73_78\":\"inst_426\",\"76_73\":\"inst_409\",\"73_70\":\"inst_410\",\"71_77\":\"inst_411\",\"74_71\":\"inst_412\",\"75_71\":\"inst_413\",\"72_74\":\"inst_414\",\"72_77\":\"inst_415\",\"72_76\":\"inst_416\",\"72_70\":\"inst_417\",\"77_73\":\"inst_418\",\"74_80\":\"inst_419\",\"71_80\":\"inst_420\",\"71_70\":\"inst_421\",\"71_78\":\"inst_422\",\"76_70\":\"inst_423\",\"72_72\":\"inst_424\",\"72_75\":\"inst_425\",\"76_74\":\"inst_428\",\"76_75\":\"inst_429\",\"75_73\":\"inst_430\",\"76_72\":\"inst_433\",\"88_74\":\"inst_437\",\"88_75\":\"inst_437\",\"85_70\":\"inst_438\",\"85_71\":\"inst_438\",\"71_89\":\"inst_441\",\"71_90\":\"inst_441\",\"71_91\":\"inst_441\",\"71_92\":\"inst_441\",\"71_93\":\"inst_441\",\"71_94\":\"inst_441\",\"85_89\":\"inst_442\",\"85_90\":\"inst_442\",\"93_93\":\"inst_444\",\"93_94\":\"inst_444\",\"91_90\":\"inst_445\",\"91_93\":\"inst_446\",\"91_94\":\"inst_446\",\"94_90\":\"inst_447\",\"85_75\":\"inst_448\",\"84_75\":\"inst_450\",\"84_74\":\"inst_451\",\"85_74\":\"inst_452\",\"86_75\":\"inst_453\",\"86_74\":\"inst_454\",\"95_75\":\"inst_457\",\"96_75\":\"inst_458\",\"97_75\":\"inst_459\",\"81_74\":\"inst_461\",\"81_75\":\"inst_461\"},\"showVictoryAfter5\":false,\"metricsEnabled\":true,\"metricsUrl\":\"https://script.google.com/macros/s/AKfycbxB2n1tWzWwjrXi_uMayLl27soy9js4dGkrc8uhSTBrFXaZZBEwTZY7_fBChCT67eZ-Fw/exec\",\"testMetricsAtLevel2Enabled\":false,\"gameVersion\":\"0.021\",\"consumeNearEdgeShift\":0.6,\"consumeNearEdgeShiftByLevel\":{\"1\":0,\"2\":0,\"5\":0.6},\"consumeTweenByLevel\":{\"1\":{\"shiftTimingPower\":1,\"fallDurationMult\":0.5,\"nearEdgeShift\":0,\"attractPower\":1.4,\"depthThreshold\":-2},\"2\":{\"nearEdgeShift\":0},\"5\":{\"nearEdgeShift\":0.6}},\"consumeSimpleProfileLevelThreshold\":2,\"holeProfiles\":{\"player\":{},\"bot\":{}},\"spawnGhostBot\":true,\"ghostBotSpawnOffsetX\":14,\"ghostBotSpawnOffsetZ\":4,\"ghostBotSearchRadius\":50,\"ghostBotRetargetInterval\":0.8,\"ghostBotPlayerObjectBuffer\":8,\"ghostBotMinPlayerDistance\":1,\"ghostBotMaxPlayerDistance\":80,\"ghostBotSpeedMultiplier\":0.75,\"ghostBotThinkDelay\":0.15,\"ghostBotDebugLog\":false,\"enableGhostDebug\":false,\"ghostPacingThreshold\":2,\"ghostType\":\"PersonalBest\",\"spawnGhostInRound1\":false,\"botGhostCanWin\":false,\"pbRouteSampleInterval\":0.4,\"useDebugGhostRoute\":false,\"debugGhostRoute\":{\"totalTimeSec\":94,\"sampleIntervalSec\":0.4,\"samples\":[{\"t\":0,\"x\":0,\"z\":0},{\"t\":0.4,\"x\":-0.04,\"z\":-0.05},{\"t\":0.8,\"x\":-2.1,\"z\":-1.71},{\"t\":1.2,\"x\":-4.99,\"z\":-2.27},{\"t\":1.6,\"x\":-7.75,\"z\":-3.53},{\"t\":1.99,\"x\":-8.4,\"z\":-5.7},{\"t\":2.4,\"x\":-5.85,\"z\":-7.38},{\"t\":2.79,\"x\":-4.38,\"z\":-9.83},{\"t\":3.2,\"x\":-6.05,\"z\":-12.01},{\"t\":3.59,\"x\":-8.77,\"z\":-11.86},{\"t\":4,\"x\":-7.54,\"z\":-13.58},{\"t\":4.38,\"x\":-4.72,\"z\":-13.04},{\"t\":4.8,\"x\":-1.73,\"z\":-12.15},{\"t\":5.19,\"x\":0.91,\"z\":-11.03},{\"t\":5.6,\"x\":3.09,\"z\":-8.88},{\"t\":5.99,\"x\":5.61,\"z\":-7.54},{\"t\":6.39,\"x\":8.5,\"z\":-7.68},{\"t\":6.79,\"x\":9.22,\"z\":-10.8},{\"t\":7.19,\"x\":6.8,\"z\":-13.91},{\"t\":7.59,\"x\":7.67,\"z\":-17.32},{\"t\":7.99,\"x\":10.77,\"z\":-19.16},{\"t\":8.4,\"x\":11.4,\"z\":-15.94},{\"t\":8.79,\"x\":14.68,\"z\":-14.85},{\"t\":9.2,\"x\":15.07,\"z\":-18.37},{\"t\":9.6,\"x\":15.45,\"z\":-22.22},{\"t\":10,\"x\":17.23,\"z\":-25.64},{\"t\":10.39,\"x\":18.31,\"z\":-29.25},{\"t\":10.8,\"x\":17.59,\"z\":-33.09},{\"t\":11.19,\"x\":15.2,\"z\":-35.87},{\"t\":11.6,\"x\":11.51,\"z\":-34.84},{\"t\":11.99,\"x\":7.89,\"z\":-33.48},{\"t\":12.39,\"x\":4.11,\"z\":-32.77},{\"t\":12.79,\"x\":0.25,\"z\":-32.49},{\"t\":13.2,\"x\":-3.73,\"z\":-32.12},{\"t\":13.59,\"x\":-7.55,\"z\":-32.28},{\"t\":14,\"x\":-10.73,\"z\":-34.53},{\"t\":14.4,\"x\":-14.24,\"z\":-36.08},{\"t\":14.81,\"x\":-17.79,\"z\":-34.62},{\"t\":15.19,\"x\":-21.38,\"z\":-33.74},{\"t\":15.6,\"x\":-26.02,\"z\":-33.37},{\"t\":16,\"x\":-30.83,\"z\":-34.24},{\"t\":16.4,\"x\":-33.03,\"z\":-38.82},{\"t\":16.8,\"x\":-35.05,\"z\":-43.66},{\"t\":17.2,\"x\":-38.48,\"z\":-47.53},{\"t\":17.59,\"x\":-40.22,\"z\":-52.32},{\"t\":17.99,\"x\":-42.03,\"z\":-57.1},{\"t\":18.39,\"x\":-40.93,\"z\":-59.39},{\"t\":18.8,\"x\":-37.25,\"z\":-59},{\"t\":19.19,\"x\":-40.17,\"z\":-62.52},{\"t\":19.59,\"x\":-44.96,\"z\":-64.38},{\"t\":19.98,\"x\":-48.11,\"z\":-68.33},{\"t\":20.39,\"x\":-52.05,\"z\":-71.78},{\"t\":20.79,\"x\":-56.65,\"z\":-74.29},{\"t\":21.2,\"x\":-61.71,\"z\":-75.65},{\"t\":21.6,\"x\":-66.7,\"z\":-75.09},{\"t\":22,\"x\":-65.94,\"z\":-71.28},{\"t\":22.4,\"x\":-61.49,\"z\":-68.8},{\"t\":22.79,\"x\":-59.19,\"z\":-64.27},{\"t\":23.19,\"x\":-58.09,\"z\":-59.26},{\"t\":23.6,\"x\":-60.01,\"z\":-54.72},{\"t\":23.99,\"x\":-63.92,\"z\":-51.83},{\"t\":24.39,\"x\":-60.67,\"z\":-48.5},{\"t\":24.79,\"x\":-60.86,\"z\":-44.64},{\"t\":25.19,\"x\":-63.79,\"z\":-40.7},{\"t\":25.59,\"x\":-62.69,\"z\":-35.35},{\"t\":26,\"x\":-62.2,\"z\":-30.93},{\"t\":26.45,\"x\":-67.14,\"z\":-31.98},{\"t\":26.84,\"x\":-70.23,\"z\":-33.64},{\"t\":27.25,\"x\":-71.29,\"z\":-28.08},{\"t\":27.64,\"x\":-70.62,\"z\":-22.8},{\"t\":28.04,\"x\":-69.04,\"z\":-17.35},{\"t\":28.45,\"x\":-67.6,\"z\":-14.12},{\"t\":28.85,\"x\":-63.29,\"z\":-17.59},{\"t\":29.25,\"x\":-58.21,\"z\":-20.04},{\"t\":29.64,\"x\":-52.8,\"z\":-21.55},{\"t\":30.05,\"x\":-47.37,\"z\":-22.24},{\"t\":30.45,\"x\":-41.96,\"z\":-20.64},{\"t\":30.84,\"x\":-38.37,\"z\":-16.44},{\"t\":31.24,\"x\":-35.77,\"z\":-11.6},{\"t\":31.64,\"x\":-30.59,\"z\":-12.18},{\"t\":32.05,\"x\":-25.24,\"z\":-13.96},{\"t\":32.44,\"x\":-19.72,\"z\":-13.28},{\"t\":32.84,\"x\":-14,\"z\":-12.52},{\"t\":33.26,\"x\":-9.42,\"z\":-12.83},{\"t\":33.65,\"x\":-10.58,\"z\":-15.94},{\"t\":34.04,\"x\":-5.67,\"z\":-13.64},{\"t\":34.45,\"x\":-6.25,\"z\":-15.75},{\"t\":34.84,\"x\":-10.49,\"z\":-19.31},{\"t\":35.24,\"x\":-13.96,\"z\":-23.87},{\"t\":35.64,\"x\":-16.85,\"z\":-28.74},{\"t\":36.04,\"x\":-15.4,\"z\":-31.87},{\"t\":36.45,\"x\":-11.8,\"z\":-27.5},{\"t\":36.84,\"x\":-8.78,\"z\":-22.7},{\"t\":37.24,\"x\":-5.55,\"z\":-18.19},{\"t\":37.65,\"x\":-1.11,\"z\":-14.36},{\"t\":38.04,\"x\":2.45,\"z\":-10.2},{\"t\":38.44,\"x\":3.86,\"z\":-4.72},{\"t\":38.85,\"x\":5.27,\"z\":0.97},{\"t\":39.25,\"x\":5.8,\"z\":6.28},{\"t\":39.65,\"x\":0.97,\"z\":5.02},{\"t\":40.05,\"x\":-4.59,\"z\":4.46},{\"t\":40.45,\"x\":-10.25,\"z\":4.52},{\"t\":40.85,\"x\":-15.37,\"z\":5.9},{\"t\":41.24,\"x\":-15.45,\"z\":13.12},{\"t\":41.64,\"x\":-13,\"z\":19.85},{\"t\":42.05,\"x\":-5.35,\"z\":19.57},{\"t\":42.44,\"x\":2.03,\"z\":17.84},{\"t\":42.85,\"x\":9.78,\"z\":15.34},{\"t\":43.24,\"x\":16.27,\"z\":14.68},{\"t\":43.65,\"x\":13.1,\"z\":11.06},{\"t\":44.04,\"x\":7.61,\"z\":16.08},{\"t\":44.45,\"x\":1.93,\"z\":21.72},{\"t\":44.84,\"x\":-3.52,\"z\":27},{\"t\":45.24,\"x\":-9.98,\"z\":31},{\"t\":45.64,\"x\":-17.75,\"z\":30.48},{\"t\":46.06,\"x\":-25.62,\"z\":30.96},{\"t\":46.44,\"x\":-27.35,\"z\":37.47},{\"t\":46.84,\"x\":-26.34,\"z\":44.97},{\"t\":47.24,\"x\":-27.5,\"z\":52.73},{\"t\":47.65,\"x\":-29.81,\"z\":60.31},{\"t\":48.04,\"x\":-35.84,\"z\":64.82},{\"t\":48.44,\"x\":-39.52,\"z\":63.43},{\"t\":48.84,\"x\":-38.24,\"z\":55.91},{\"t\":49.25,\"x\":-39.01,\"z\":48.02},{\"t\":49.64,\"x\":-41.16,\"z\":40.74},{\"t\":50.05,\"x\":-46.55,\"z\":35.7},{\"t\":50.44,\"x\":-44.35,\"z\":31.33},{\"t\":50.84,\"x\":-37.88,\"z\":26.31},{\"t\":51.25,\"x\":-42.59,\"z\":20.13},{\"t\":51.64,\"x\":-49.84,\"z\":16.37},{\"t\":52.05,\"x\":-55.8,\"z\":22.17},{\"t\":52.44,\"x\":-61.55,\"z\":28.17},{\"t\":52.84,\"x\":-69.72,\"z\":29.15},{\"t\":53.24,\"x\":-78.23,\"z\":27.63},{\"t\":53.65,\"x\":-86.92,\"z\":26.31},{\"t\":54.03,\"x\":-90.67,\"z\":20.31},{\"t\":54.44,\"x\":-90.41,\"z\":11.7},{\"t\":54.84,\"x\":-88.57,\"z\":3.56},{\"t\":55.24,\"x\":-81.2,\"z\":2.48},{\"t\":55.65,\"x\":-76.7,\"z\":9.21},{\"t\":56.05,\"x\":-75.39,\"z\":18.04},{\"t\":56.44,\"x\":-73.11,\"z\":26.85},{\"t\":56.84,\"x\":-76.21,\"z\":33.62},{\"t\":57.25,\"x\":-71.94,\"z\":29.94},{\"t\":57.65,\"x\":-63.68,\"z\":25.89},{\"t\":58.04,\"x\":-55.49,\"z\":26.54},{\"t\":58.44,\"x\":-58.76,\"z\":20.44},{\"t\":58.84,\"x\":-53.07,\"z\":25.06},{\"t\":59.26,\"x\":-47.68,\"z\":25.72},{\"t\":59.65,\"x\":-41.7,\"z\":21.61},{\"t\":60.04,\"x\":-34.25,\"z\":16.39},{\"t\":60.44,\"x\":-28.01,\"z\":14.28},{\"t\":60.85,\"x\":-35.63,\"z\":10.07},{\"t\":61.24,\"x\":-42.21,\"z\":4.47},{\"t\":61.65,\"x\":-44.89,\"z\":-4.71},{\"t\":62.04,\"x\":-40.66,\"z\":-9.57},{\"t\":62.44,\"x\":-32.36,\"z\":-5.42},{\"t\":62.84,\"x\":-26.21,\"z\":1.71},{\"t\":63.24,\"x\":-27.48,\"z\":11.39},{\"t\":63.65,\"x\":-24.65,\"z\":21.42},{\"t\":64.04,\"x\":-20.73,\"z\":30.48},{\"t\":64.45,\"x\":-16.42,\"z\":39.94},{\"t\":64.84,\"x\":-15.65,\"z\":49.26},{\"t\":65.25,\"x\":-19.44,\"z\":58.91},{\"t\":65.65,\"x\":-21.35,\"z\":68.48},{\"t\":66.04,\"x\":-13.65,\"z\":73.94},{\"t\":66.44,\"x\":-4.46,\"z\":78.39},{\"t\":66.85,\"x\":4.56,\"z\":75.4},{\"t\":67.25,\"x\":11.1,\"z\":67.8},{\"t\":67.65,\"x\":13.81,\"z\":59.27},{\"t\":68.05,\"x\":21.61,\"z\":63.38},{\"t\":68.44,\"x\":26.44,\"z\":58.54},{\"t\":68.84,\"x\":24.43,\"z\":48.48},{\"t\":69.25,\"x\":20.54,\"z\":39.34},{\"t\":69.63,\"x\":30.36,\"z\":39.43},{\"t\":70.04,\"x\":41.14,\"z\":40.33},{\"t\":70.44,\"x\":52.11,\"z\":41.25},{\"t\":70.85,\"x\":62.97,\"z\":40.6},{\"t\":71.25,\"x\":67.24,\"z\":48.39},{\"t\":71.64,\"x\":66.54,\"z\":58.79},{\"t\":72.05,\"x\":67.66,\"z\":70.06},{\"t\":72.45,\"x\":62.23,\"z\":74.56},{\"t\":72.84,\"x\":54.27,\"z\":67.43},{\"t\":73.25,\"x\":45.66,\"z\":60.76},{\"t\":73.64,\"x\":35.97,\"z\":66.54},{\"t\":74.04,\"x\":31.75,\"z\":66.45},{\"t\":74.45,\"x\":36.73,\"z\":56.83},{\"t\":74.86,\"x\":31.64,\"z\":65.17},{\"t\":75.25,\"x\":28.59,\"z\":75.94},{\"t\":75.65,\"x\":33.89,\"z\":76.87},{\"t\":76.04,\"x\":34.82,\"z\":65.73},{\"t\":76.45,\"x\":34.95,\"z\":53.73},{\"t\":76.85,\"x\":35.34,\"z\":42.14},{\"t\":77.25,\"x\":39.19,\"z\":31.57},{\"t\":77.65,\"x\":38.78,\"z\":21.1},{\"t\":78.05,\"x\":34.27,\"z\":10.91},{\"t\":78.47,\"x\":40.28,\"z\":0.92},{\"t\":78.84,\"x\":45.81,\"z\":-8.11},{\"t\":79.25,\"x\":56.27,\"z\":-3.65},{\"t\":79.64,\"x\":61.17,\"z\":5.52},{\"t\":80.04,\"x\":59.6,\"z\":15.73},{\"t\":80.43,\"x\":68.24,\"z\":11.47},{\"t\":80.84,\"x\":69.14,\"z\":-0.93},{\"t\":81.25,\"x\":65.14,\"z\":-12.83},{\"t\":81.64,\"x\":54.1,\"z\":-13.41},{\"t\":82.05,\"x\":42.02,\"z\":-12.92},{\"t\":82.45,\"x\":35.17,\"z\":-22.94},{\"t\":82.84,\"x\":36.23,\"z\":-33.46},{\"t\":83.25,\"x\":48.34,\"z\":-32.35},{\"t\":83.64,\"x\":60.36,\"z\":-30.69},{\"t\":84.05,\"x\":72.87,\"z\":-32.33},{\"t\":84.44,\"x\":84.13,\"z\":-36.92},{\"t\":84.84,\"x\":85.27,\"z\":-47.58},{\"t\":85.24,\"x\":73.77,\"z\":-49.84},{\"t\":85.65,\"x\":60.98,\"z\":-49.51},{\"t\":86.04,\"x\":48.87,\"z\":-48.59},{\"t\":86.44,\"x\":36.34,\"z\":-47.51},{\"t\":86.84,\"x\":23.53,\"z\":-48.04},{\"t\":87.24,\"x\":10.45,\"z\":-49.01},{\"t\":87.64,\"x\":-2.39,\"z\":-49.99},{\"t\":88.04,\"x\":-15.67,\"z\":-51.11},{\"t\":88.44,\"x\":-26.83,\"z\":-56.53},{\"t\":88.84,\"x\":-35.64,\"z\":-66.2},{\"t\":89.24,\"x\":-45.18,\"z\":-72.37},{\"t\":89.64,\"x\":-45.74,\"z\":-60.54},{\"t\":90.04,\"x\":-43.43,\"z\":-47.65},{\"t\":90.44,\"x\":-42.13,\"z\":-34.83},{\"t\":90.84,\"x\":-41.41,\"z\":-21.75},{\"t\":91.24,\"x\":-41,\"z\":-8.88},{\"t\":91.65,\"x\":-42.56,\"z\":2.31},{\"t\":92.03,\"x\":-54.37,\"z\":2.09},{\"t\":92.45,\"x\":-60.96,\"z\":12.9},{\"t\":92.84,\"x\":-65.69,\"z\":24.63},{\"t\":93.24,\"x\":-63.21,\"z\":37.07},{\"t\":93.65,\"x\":-68.55,\"z\":48.58},{\"t\":94.04,\"x\":-74.4,\"z\":60.03}]},\"useFakeLeaderboard\":false,\"fakeLeaderboard\":[{\"score\":72,\"username\":\"Shawn\"},{\"score\":78,\"username\":\"Alex\"},{\"username\":\"Sarah\",\"score\":81},{\"score\":85,\"username\":\"Judy\"},{\"username\":\"Mike\",\"score\":87}],\"onboardingDelay\":1,\"onboardingDuration\":3.5,\"ghostOnboardingDuration\":4.5}", "game_design_document": "## Core Game Loop and Mechanics\n\nThis is a **foundation prototype** for a Hole.io-inspired growth game. The current phase focuses exclusively on establishing core movement mechanics and the testing environment. Players control a black hole that moves across a flat testing ground populated with placeholder objects.\n\n**Current Phase Gameplay:**\n- Player spawns in the center of a gray-box testing environment\n- Move the hole freely across the map using keyboard or touch controls\n- Camera follows smoothly with a slight top-down angle\n- Explore the environment and test movement responsiveness\n- Placeholder cubes are distributed around the map for scale reference\n\n**Future Phases (Not Implemented Yet):**\n- Object consumption mechanics\n- Scoring system\n- Growth and leveling\n- Visual polish and city environment\n- Sound effects and music\n- Progression systems\n\n## Starting Scope\n\n**Phase 1 (Current Implementation):**\n- Controllable player hole with responsive movement\n- Top-down camera with smooth follow behavior\n- Desktop controls (WASD + Arrow Keys)\n- Mobile controls (dynamic virtual joystick)\n- Gray-box testing environment with grid floor\n- Placeholder objects (4 cube types) distributed around the map\n\n**Explicitly NOT in Phase 1:**\n- Object consumption or collision detection\n- Scoring or points system\n- Growth mechanics or size changes\n- UI elements beyond basic game view\n- Sound effects or music\n- Visual polish or art assets\n- Progression or level systems\n\n## Game Perspective\n\n**3D Third-Person Top-Down** with a slight angle (similar to Hole.io):\n- Camera positioned above and behind the hole\n- Angled downward at approximately 60-70 degrees\n- Camera follows the player hole smoothly with slight lag for natural feel\n- Fixed distance from the hole (no zoom in this phase)\n- Provides clear view of the hole and surrounding objects\n\n## Game World and Level Design\n\n**Testing Environment:**\n- Large flat plane (100x100 units) serving as the ground\n- Subtle grid pattern on the floor for spatial reference\n- Gray color scheme (#808080 floor, #606060 grid lines)\n- No boundaries or walls (infinite feel within the play area)\n\n**Player Hole:**\n- Circular black hole rendered as a 3D cylinder/disc\n- Dark gradient from black center to dark gray edges\n- Starting size: 2 units diameter\n- Spawns at center of map (0, 0, 0)\n- Rendered with slight depth to create hole illusion\n\n**Placeholder Objects Distribution:**\nThe map contains 40-50 placeholder cubes distributed randomly:\n\n1. **Tiny Cube** (0.5 x 0.5 x 0.5 units) - Gray (#888888) - 15 instances\n2. **Small Cube** (1 x 1 x 1 units) - Blue (#4169E1) - 12 instances\n3. **Tall Cube** (0.8 x 2.5 x 0.8 units) - Dark Gray (#505050) - 10 instances\n4. **Wide Cube** (1.5 x 1 x 1.5 units) - Orange (#FF8C00) - 8 instances\n\nObjects are scattered within a 40x40 unit area around the spawn point, with minimum 3 unit spacing between objects. Objects are static and do not move or respond to the hole in this phase.\n\n## Edit Mode Design\n\n**Phase 1 Edit Mode:**\nSince this is a foundation prototype, edit mode is minimal:\n\n**Editable Properties:**\n- `holeSpeed`: Movement speed of the hole (default: 5, range: 1-15)\n- `cameraHeight`: Height of camera above hole (default: 15, range: 8-25)\n- `cameraAngle`: Angle of camera in degrees (default: 65, range: 45-80)\n- `gridSize`: Size of the testing ground (default: 100, range: 50-200)\n\n**Future Edit Mode Features (Not Implemented):**\n- Object placement and distribution\n- Hole size and growth parameters\n- Scoring thresholds\n- Level layouts\n\n## Control Design\n\n**Desktop Controls:**\n- **WASD Keys**: Move hole (W=forward, S=backward, A=left, D=right)\n- **Arrow Keys**: Alternative movement controls\n- Movement is relative to camera view (forward moves away from camera)\n- Smooth acceleration and deceleration for natural feel\n- Prevent default browser behavior for used keys\n\n**Mobile Controls - Dynamic Virtual Joystick:**\n- **Touch anywhere** on screen to create joystick at touch position\n- **Outer ring** appears at initial touch point (semi-transparent white circle, 120px diameter)\n- **Inner knob** follows finger drag (solid white circle, 50px diameter)\n- **Drag direction** controls hole movement direction\n- **Drag distance** controls movement speed (max radius: 60px from center)\n- **Dead zone**: 10px radius treated as no input\n- **Base sliding**: When finger exceeds max radius, joystick base moves to keep knob under finger at max radius\n- **Release touch** removes joystick visuals\n- **New touch** creates new joystick at new position\n- Joystick outputs analog values (-1 to +1) for smooth movement\n- Track touch IDs to prevent multi-touch interference\n\n**Input Handling:**\n- Clear all input states on window blur\n- Normalize key codes to lowercase\n- Smooth interpolation between input and movement\n- Movement speed scales with joystick distance on mobile\n\n## Win/Lose Conditions\n\n**Phase 1:** No win/lose conditions. This is a free exploration prototype for testing movement and controls.\n\n**Future Phases:** Will include score targets, time limits, and growth objectives.\n\n## Technical Architecture\n\n**Code Structure (for clean future expansion):**\n- Modular game class with separate systems:\n  - `InputManager`: Handles keyboard and touch input\n  - `CameraController`: Manages camera follow and positioning\n  - `Player`: Hole movement and state\n  - `World`: Environment and object management\n  - `Renderer`: Three.js rendering setup\n- Use Three.js for 3D rendering\n- Separate game logic from rendering\n- Config-driven parameters for easy tweaking\n- Clear separation between Phase 1 and future features\n\n**Performance Considerations:**\n- Simple geometry for optimal mobile performance\n- Minimal draw calls (static objects can be merged)\n- Efficient touch event handling\n- Smooth 60fps target on mobile devices\n\n## Art Style and Visual Theme\n\n**Minimalist Gray-Box Aesthetic:**\n- Clean, simple geometric shapes\n- Muted gray color palette for environment\n- Bright accent colors for placeholder objects (blue, orange)\n- Flat shading with subtle shadows\n- Grid pattern for spatial reference\n- Focus on clarity and functionality over visual polish\n\n**Lighting:**\n- Single directional light from above-front\n- Ambient light for even illumination\n- Soft shadows for depth perception\n- No dynamic lighting effects\n\n**Visual Hierarchy:**\n- Player hole: Darkest element (black/dark gray gradient)\n- Environment: Medium gray with subtle grid\n- Objects: Bright colors for visibility and testing\n\nThis aesthetic serves the prototype phase and will be replaced with polished city visuals in future phases.", "persistent_storage_config": {"leaderboard": {"sort_ascending": true}}, "game_metadata": {"game_id": "01KTGD1RZF3GX6BKA312DQ0MTZ", "title": "Eat the World", "description": "Start small, eat everything, and beat your best time.", "thumbnail_url": "https://dzmv1mxivz48k.cloudfront.net/images/3891a370-a061-70c0-7fd1-14cc6ef142af/1780838207905_01KTH3N4D1487SE79E3ZF543HV.EatTheWorld-2by3.png", "created_at": 1780814585, "updated_at": 1782051630378.7227, "art_style": null}, "task_plan": null}