{"asset_map": {"monster_idle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/monster_idle_ac25d2e5-b539-4d1a-a6ba-f1ed90fa8721.webp", "type": "image", "prompt": "Generate a full-body, 45-degree top-down view of a cute but menacing kaiju monster character. Low-poly voxel art style, chunky geometric shapes. Bright green (#2ecc71) rounded rectangular body, yellow belly (#f1c40f), large head with a wide mouth and triangular white teeth, round red eyes (#e74c3c). Standing pose with a short tapered tail and dark green back spikes. Flat colors, simple shading, clean professional game asset, transparent background, slight drop shadow.", "description": "The main kaiju monster in a standing idle pose, low-poly toy style.", "subtype": "character", "aspect_ratio": [869, 1278], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "monster_walking": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/monster_walking_bc54be1c-7504-47b3-a0e2-b0aee09929c5.webp", "type": "image", "prompt": "Generate the same green kaiju monster from monster_idle in a walking pose. 45-degree top-down view, low-poly toy style. Left leg forward, right leg back, arms swinging, slight forward lean. Maintain exact colors: bright green body, yellow belly, red eyes. Flat shading, simple geometry, transparent background, slight drop shadow.", "description": "The kaiju monster mid-stride in a walking animation frame.", "subtype": "character", "aspect_ratio": [615, 876], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "monster_attacking": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/monster_attacking_b8ca8200-073d-457f-abb5-152f03a7a338.webp", "type": "image", "prompt": "Generate the same green kaiju monster from monster_idle in an aggressive attacking lunge. 45-degree top-down view, low-poly toy style. Mouth opened wide showing teeth, arms reaching forward, lunging posture. Maintain exact colors: bright green body, yellow belly, red eyes. Flat shading, simple geometry, transparent background, slight drop shadow.", "description": "The kaiju monster in an aggressive lunging bite attack pose.", "subtype": "character", "aspect_ratio": [818, 870], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "monster_eating": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/monster_eating_1e2fe437-cc4f-48bb-b133-78c310f04712.webp", "type": "image", "prompt": "Generate the same green kaiju monster from monster_idle in a satisfied eating pose. 45-degree top-down view, low-poly toy style. Head tilted back, mouth wide open, eyes squinting happily, one hand near mouth. Maintain exact colors: bright green body, yellow belly, red eyes. Flat shading, simple geometry, transparent background, slight drop shadow.", "description": "The kaiju monster in a satisfied eating pose with head tilted back.", "subtype": "character", "aspect_ratio": [754, 1124], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "police_car": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/police_car_29a16ecd-4cc6-40ae-b2ef-a5df0ec05adc.webp", "type": "image", "prompt": "Generate a low-poly toy police car, 45-degree top-down view. Simple boxy geometry, black and white color scheme. Four chunky cylinder wheels, dark blue rectangular windshield, small red and blue flashing light bar on roof. Toy-like proportions, flat colors, simple shading, transparent background, slight drop shadow.", "description": "A low-poly toy police car for the enemy forces.", "subtype": "character", "aspect_ratio": [948, 717], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tank": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/tank_a4cdf947-257c-4392-9894-cd7ffd9cc72b.webp", "type": "image", "prompt": "Generate a toy military tank, 45-degree top-down view. Low-poly boxy geometry in military green (#2c3e50). Cylinder turret and barrel, dark gray treads. Chunky, toy-like appearance, flat colors, simple shading, transparent background, slight drop shadow.", "description": "A low-poly toy military tank enemy.", "subtype": "character", "aspect_ratio": [908, 716], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "helicopter": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/helicopter_01f44a3e-7004-4088-821c-275ba1477353.webp", "type": "image", "prompt": "Generate a toy military helicopter, 45-degree top-down view. Low-poly boxy cockpit in military green, cylinder tail boom, large rotor blades on top. Simple skids, chunky toy-like proportions, flat colors, transparent background. Include a soft drop shadow on the ground below to indicate hovering height.", "description": "A low-poly toy military helicopter enemy.", "subtype": "character", "aspect_ratio": [809, 805], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "attack_button_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/attack_button_icon_acf70122-185f-467c-8c6b-676fd34794f8.webp", "type": "image", "prompt": "Flat vector icon of a monster bite or claw attack. Sharp white teeth silhouette inside a circular frame. Bold, thick lines, high contrast red (#e74c3c) and white. Clean professional UI asset, minimal design, transparent background.", "description": "Mobile UI icon for the basic bite/claw attack.", "subtype": "ui", "aspect_ratio": [879, 877], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "special_attack_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/special_attack_icon_229b80e1-de71-4c0a-8b90-5674ca7942b0.webp", "type": "image", "prompt": "Flat vector icon of a powerful explosion or shockwave starburst. Bold geometric shapes, vibrant gold (#f1c40f) and white colors. High impact design, clean lines, professional game UI, transparent background.", "description": "Mobile UI icon for the special explosive attack.", "subtype": "ui", "aspect_ratio": [845, 859], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "mutation_bigger_bite_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mutation_bigger_bite_icon_a4c090fa-bd2d-46bc-9725-1db255c4e772.webp", "type": "image", "prompt": "Flat iconic illustration of a massive open mouth with sharp teeth and expansion arrows. Red and yellow color scheme, bold silhouettes, clean game UI style, transparent background.", "description": "Icon for the 'Bigger Bite' mutation card.", "subtype": "ui", "aspect_ratio": [935, 876], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "mutation_iron_skin_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mutation_iron_skin_icon_87483fba-9655-485c-9260-0ac410ec9b2b.webp", "type": "image", "prompt": "Flat iconic illustration of a shield with metallic armored scale patterns. Silver-gray (#95a5a6) and light blue colors. Bold, protective geometric design, transparent background.", "description": "Icon for the 'Iron Skin' mutation card.", "subtype": "ui", "aspect_ratio": [740, 927], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "mutation_fast_feet_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mutation_fast_feet_icon_4523f293-1228-4557-8d21-188f72ed845d.webp", "type": "image", "prompt": "Flat iconic illustration of a monster foot with motion blur speed lines. Bright green (#2ecc71) and white colors. Dynamic movement symbol, simple geometric shapes, transparent background.", "description": "Icon for the 'Fast Feet' mutation card.", "subtype": "ui", "aspect_ratio": [904, 639], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "mutation_regeneration_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mutation_regeneration_icon_173fb2b7-5d32-4df4-b3e7-9e7d28a240b6.webp", "type": "image", "prompt": "Flat iconic illustration of a heart symbol with a medical plus sign. Vibrant red and white. Clean, healing-themed geometric design, transparent background.", "description": "Icon for the 'Regeneration' mutation card.", "subtype": "ui", "aspect_ratio": [820, 737], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "mutation_city_eater_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mutation_city_eater_icon_3c4edfbd-21c4-4065-b2cf-db4e1c94463c.webp", "type": "image", "prompt": "Flat iconic illustration of a city building with a large bite mark taken out of it. Orange and cyan color scheme. Bold, humorous geometric style, transparent background.", "description": "Icon for the 'City Eater' mutation card.", "subtype": "ui", "aspect_ratio": [650, 921], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "mutation_giant_growth_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mutation_giant_growth_icon_3d7a50b8-f7c5-4fe5-a7cc-d1ec369a26b7.webp", "type": "image", "prompt": "Flat iconic illustration showing a monster silhouette growing larger with upward pointing arrows. Purple (#9b59b6) and yellow colors. Simple, clear power-up symbol, transparent background.", "description": "Icon for the 'Giant Growth' mutation card.", "subtype": "ui", "aspect_ratio": [906, 864], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "mutation_thunder_stomp_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mutation_thunder_stomp_icon_41edefea-b04f-4cab-a011-f52a55a06cbe.webp", "type": "image", "prompt": "Flat iconic illustration of a foot slamming down with circular shockwave rings and lightning. Yellow and electric blue colors. High energy impact design, transparent background.", "description": "Icon for the 'Thunder Stomp' mutation card.", "subtype": "ui", "aspect_ratio": [836, 876], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "mutation_magnetic_hunger_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mutation_magnetic_hunger_icon_d7f53f97-7dfc-41e2-b9f3-8b50c84bfa25.webp", "type": "image", "prompt": "Flat iconic illustration of a horseshoe magnet pulling in bright cyan XP orbs. Magnet colors (red/blue) with cyan accents. Bold, magnetic attraction symbol, transparent background.", "description": "Icon for the 'Magnetic Hunger' mutation card.", "subtype": "ui", "aspect_ratio": [963, 884], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "footstep_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/footstep_sound_63760e2c-4cb1-4f56-b484-c790e07ab122.mp3", "type": "audio", "prompt": "Giant monster footstep sound effect for a playful arcade kaiju game. A massive, heavy stomp starting with a deep booming sub-bass thud (40-60Hz) and a sharp, snappy impact crack. Includes a punchy mid-range arcade 'thump' and a rumbling earth-shaking texture. Slight downward pitch glissando to convey 50 tons of weight. Ends with a short vibrating ground-shake reverb. Powerful and loud but with a toy-like, satisfying arcade aesthetic. High-impact, low-frequency dominance.", "description": "A heavy, earth-shaking, and playful kaiju stomp with a deep bass thud and arcade-style punch.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.48, "loop": false, "animation": null}, "building_hit_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/building_hit_sound_27ffc1f3-2033-439d-9245-065cbc5a5162.mp3", "type": "audio", "prompt": "Arcade-style building impact, crunchy breaking wood and stone, punchy mid-range thwack, satisfying toy-like destruction sound, non-realistic, sharp attack with a brief resonant tail.", "description": "Punchy arcade impact sound for when the monster strikes a building.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0.42, "loop": false, "animation": null}, "building_collapse_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/building_collapse_sound_099167ae-e786-4451-a2c0-f90abfedba33.mp3", "type": "audio", "prompt": "Voxel building collapsing, cascading rubble sounds, cartoony crumbling and falling debris, satisfying multi-stage destruction, arcade game style, medium-long tail, playful physics-based destruction.", "description": "Cascading destruction sound for a building completely crumbling.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.91, "loop": false, "animation": null}, "eating_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/eating_sound_8f61c382-474d-40bc-910a-b91514b5d180.mp3", "type": "audio", "prompt": "Playful monster chomp, 'nom' vocal quality, quick snappy gulp, high-pitched cartoony bite, pleasant and rewarding UI-like sound effect, short and clean.", "description": "A snappy, playful chomping sound for collecting XP orbs.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.86, "loop": false, "animation": null}, "enemy_shoot_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/enemy_shoot_sound_5ca5c641-be03-416b-8ff9-daf9aef671e1.mp3", "type": "audio", "prompt": "Toy tank cannon fire, sharp punchy 'pouf' sound, arcade projectile launch, non-threatening but clear alert sound, plastic toy aesthetic, short snappy duration.", "description": "A toy-like cannon fire sound for enemy tank projectiles.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.2, "loop": false, "animation": null}, "enemy_explosion_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/enemy_explosion_sound_c8908070-247c-4531-96e8-0e5cde5151a0.mp3", "type": "audio", "prompt": "Cartoony vehicle explosion, satisfying 'pop' and 'bang', arcade-style firework burst, low-poly destruction sound, punchy and rewarding, clear high-end sizzle tail.", "description": "A satisfying pop/explosion for destroying enemy vehicles.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.61, "loop": false, "animation": null}, "player_damage_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/player_damage_sound_875730b5-232d-4de0-adbe-e85daef97298.mp3", "type": "audio", "prompt": "Giant monster short grunt, sharp impact oof, cartoony pain vocal, high-contrast attention-grabbing hit sound, non-distressing, arcade feedback.", "description": "A short monster grunt or impact sound when the player takes damage.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.82, "loop": false, "animation": null}, "level_up_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/level_up_sound_619794be-b26b-49c8-bfc0-16330900d4e3.mp3", "type": "audio", "prompt": "A triumphant and energetic 2-second level-up fanfare for a playful 3D kaiju game. Features a bright, ascending 4-note chord progression played on high-quality retro arcade synths with a toy-like, chunky texture. Layered with shimmering metallic chimes and a crisp glockenspiel for a sense of power and growth. The sound builds to a satisfying, punchy crescendo with a clean shimmer tail. Style inspired by modern pixel-art hits like Celeste and Katana Zero\u2014playful, epic, and celebratory. No white noise, no low-quality FM buzzing, high-fidelity production.", "description": "Triumphant arcade-style level-up fanfare for a colorful 3D kaiju game.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.94, "loop": false, "animation": null}, "mutation_select_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/mutation_select_sound_14c25b36-e9f5-49b4-88d9-fa2f655c2fee.mp3", "type": "audio", "prompt": "Positive UI confirmation chime, bright digital click, rewarding select sound, high-pitched satisfying 'ding', arcade menu feedback, clean and modern.", "description": "A bright confirmation chime for selecting a new power-up.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.22, "loop": false, "animation": null}, "game_over_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/game_over_sound_8f53f23f-ad58-4de8-9e20-a272c0ac9dad.mp3", "type": "audio", "prompt": "A short, high-quality game-over defeat sound for a 3D kaiju game. Features a playful descending musical phrase starting with a bright arcade synth leading into a comical 'sad trombone' effect. Include a rubbery cartoon 'boing' and a final light thud impact. Style: Fun, lighthearted, and high-energy like Fall Guys or WarioWare. Retro-modern hybrid synth tones, 120 BPM tempo for the rhythmic descent, major-to-minor shift, clean production with a short shimmer decay. No dark or somber tones, keep it toy-like and entertaining.", "description": "A comical and dramatic defeat fanfare with a descending musical phrase for the game-over screen.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 3.0563265306122447, "loop": false, "animation": null}, "icon_laser": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_laser_446587cc-f926-4522-8efd-ce30297ab90d.webp", "type": "image", "prompt": "Generate a square game UI icon for a 'Laser Breath' ability. The icon features a stylized monster's head silhouette in profile shooting a powerful, glowing red and orange laser beam from its mouth. Use a low-poly toy and cartoon aesthetic with bold outlines and bright colors. Include energy particles and a glowing effect around the beam. Ensure high contrast and simple shapes for readability at small sizes, matching the arcade kaiju style of image 1 and image 2. Transparent background.", "description": "UI icon for the Laser Breath ability featuring a glowing red energy beam.", "subtype": "ui", "aspect_ratio": [897, 602], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "icon_meteor": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_meteor_0c93ec74-5f5d-4005-ba1f-e0943c09a61b.webp", "type": "image", "prompt": "Generate a square game UI icon for a 'Meteor Shower' ability. The icon should feature several stylized, chunky orange and red flaming rocks falling from the sky with bright motion trails. Use a low-poly toy/cartoon style with bold outlines and vibrant colors. The design must be simple, clean, and highly readable at small sizes. Arcade game aesthetic, consistent with a kaiju city destruction theme. Transparent background, high resolution, centered composition.", "description": "UI icon for the Meteor Shower ability, featuring falling flaming rocks in a toy-like arcade style.", "subtype": "ui", "aspect_ratio": [905, 968], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "icon_jump": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_jump_3d5f6089-153f-4e5d-951a-471d75810f4c.webp", "type": "image", "prompt": "Generate a game UI icon for 'Jump Stomp' ability. Features a green monster foot stomping down with yellow and orange radial shockwave rings. Cartoon toy style with bold outlines, vibrant colors, and a clean arcade game aesthetic. Simple and highly readable at small sizes. Transparent background, square composition, matching the style of existing kaiju ability icons.", "description": "A game UI icon for the 'Jump Stomp' ability, showing a monster foot impacting the ground with shockwaves.", "subtype": "ui", "aspect_ratio": [986, 982], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "icon_tail": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_tail_73860c0c-169b-4c30-ae3d-71100af6f535.webp", "type": "image", "prompt": "Generate a game UI icon for a 'Tail Sweep' ability. The icon features a green monster tail performing a circular spinning motion with blue and cyan curved motion swirl effects. Style is low-poly toy/cartoon aesthetic with bold clean outlines, matching the style of image 1. Square composition, simple and highly readable at small sizes, vibrant arcade colors, transparent background.", "description": "UI icon for the Tail Sweep ability showing a spinning monster tail with motion effects.", "subtype": "ui", "aspect_ratio": [803, 801], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "icon_dash": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_dash_121047c5-b992-4bb1-9652-675c8c51650c.webp", "type": "image", "prompt": "Generate a game UI icon for 'Power Dash' ability. The icon features dynamic blue and cyan speed streaks with white highlights indicating fast forward motion. Cartoon toy style with bold clean outlines, vibrant colors, and a polished arcade game aesthetic. Simple composition designed to be highly readable at small sizes. Square format, centered design, transparent background.", "description": "UI icon for the Power Dash ability featuring blue speed lines and motion blur.", "subtype": "ui", "aspect_ratio": [1184, 652], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "icon_growth": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_growth_bd20684b-255f-4809-9c85-54fec7529e03.webp", "type": "image", "prompt": "Generate a game UI icon for 'Growth Spurt' ability. The icon features a vibrant green arrow pointing upwards with magical sparkles, representing size increase and growth. Use a cartoon toy style with thick bold outlines and a colorful arcade aesthetic. Simple, high-contrast, and highly readable at small sizes. Clean professional game asset, transparent background, matching the style of existing mutation icons.", "description": "UI icon for the Growth Spurt ability showing a green upward arrow with sparkles", "subtype": "ui", "aspect_ratio": [890, 769], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "icon_shield": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_shield_30d1093c-3fba-49af-a1e0-d45dff70d94c.webp", "type": "image", "prompt": "Generate a game UI icon for 'Tough Hide' ability featuring a defensive shield with dragon scales or armor patterns. Cartoon toy style with bold outlines and a colorful arcade aesthetic. Blue and silver shield with a protective outer glow. Square composition, high contrast, simple and readable for small mobile screens. Transparent background, professional game asset quality.", "description": "A defensive shield UI icon with a scale pattern for the 'Tough Hide' ability.", "subtype": "ui", "aspect_ratio": [634, 836], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "icon_frenzy": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_frenzy_8c0f4210-b1e3-4c0a-a8b9-153eed0ac923.webp", "type": "image", "prompt": "Generate a game UI icon for 'Frenzy' ability, featuring a sharp yellow and gold electric lightning bolt with energetic sparks. Stylized cartoon and toy-like aesthetic with bold black outlines. Professional game-ready asset, square composition, flat design elements for high readability at small sizes. Bright arcade game colors, transparent background, high resolution.", "description": "A game UI icon for the 'Frenzy' ability featuring a yellow lightning bolt for increased attack speed.", "subtype": "ui", "aspect_ratio": [886, 895], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "icon_stomp": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_stomp_98f84efd-331d-4743-9da4-4115bbd1dde6.webp", "type": "image", "prompt": "Generate a game UI icon for a 'Big Stomp' ability. The icon features a large, dark monster footprint with orange and yellow impact cracks and energy lines radiating outward. Use a low-poly toy and cartoon style with bold black outlines and a colorful arcade aesthetic. High contrast, simple and readable at small sizes. Maintain stylistic consistency with image 1 and image 2.", "description": "A UI icon for the Big Stomp ability featuring a monster footprint with impact cracks.", "subtype": "ui", "aspect_ratio": [844, 929], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "icon_regen": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/icon_regen_f30e48b2-3a36-4018-b878-8fab8b72f8da.webp", "type": "image", "prompt": "Generate a game UI icon for a 'Regeneration' ability. The icon should feature pink and magenta healing sparkles surrounding a green glowing health cross or heart symbol. Visual style is low-poly toy/cartoon with bold outlines and vibrant colors. High contrast, simple shapes, readable at small sizes. Transparent background, professional mobile game UI asset, matching the arcade aesthetic of a kaiju destruction game.", "description": "A cartoon toy-style UI icon representing a health regeneration ability with sparkles and a health symbol.", "subtype": "ui", "aspect_ratio": [783, 743], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "start_screen_bg": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_100411990910946493899/1780234606865_01KSZ40P8HS96EV3WQPT88DNE1.ChatGPT Image May 31, 2026, 09_36_34 AM.png", "type": "image", "prompt": null, "description": "Epic city destruction start screen background featuring Growzilla wreaking havoc with fire and explosions, used as the main menu backdrop.", "subtype": null, "aspect_ratio": [1023, 1537], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "start_button": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/start_button_360b3056-3a21-4d2b-825e-33e29dd773b8.webp", "type": "image", "prompt": "Transform the button in image 1 by decreasing its vertical height to create a wider rectangular shape. Perfectly center the 'START' text vertically and horizontally. Maintain the exact 3D toy-arcade style, lighting, and yellow-to-green stone texture to match the title logo. The final asset should be a clean, game-ready UI element on a transparent background.", "description": "A wider version of the 3D toy-arcade START button with perfectly centered text.", "subtype": "ui", "aspect_ratio": [801, 389], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "start_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/start_music_feeb496f-4092-4b60-881f-0c69c5ea9431.mp3", "type": "audio", "prompt": "Energetic, upbeat arcade-style title theme for 'Mega Muncher: City Rampage'. High-energy electronic music with driving 140 BPM tempo. Features heavy, rhythmic bass drum hits mimicking giant monster footsteps, layered with heroic and playful synthesizer melodies. Mixes 16-bit arcade aesthetics with modern cinematic monster movie power. Includes subtle, integrated kaiju roars within the track. Epic, powerful, and chaotic yet fun energy. Clean loop points for continuous menu playback, no vocals, instrumental.", "description": "Energetic and epic arcade title music with heavy monster stomps and heroic synth melodies.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 45.03510204081633, "loop": true, "animation": null}, "title_logo": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/title_logo_7350d80d-f0ad-49cf-a0c1-b164272e6cc2.webp", "type": "image", "prompt": "Generate a high-quality typographic game logo for 'GROWZILLA'. The text must be strictly horizontal, flat, and straight. Letters are constructed from solid, dry, crumbling stone and chipped bricks. The color is a bright gradient from yellow at the top to vibrant green at the bottom. Absolute dry texture with no slime or liquid effects. 3D arcade-style, bold, epic, and highly readable. Professional game-ready asset, transparent background.", "description": "Bold 3D typographic 'GROWZILLA' logo made of dry yellow-to-green gradient stone and bricks.", "subtype": "ui", "aspect_ratio": [1450, 694], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "heart_pickup_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/heart_pickup_sound_7c96d2cd-49a7-45e7-9dbb-fb93d7d8ec32.mp3", "type": "audio", "prompt": "A bright, uplifting healing sound effect for a toy-like arcade kaiju game. Ascending musical chime, 'ding-ding-ding' pattern, crystalline and sparkly tone like a magical bell or harp. Warm and restorative, featuring a gentle shimmer reverb tail. Musical and melodic, conveying restoration and positivity. Clean, high-quality, 0.5 to 0.8 seconds of actual sound with a natural decay.", "description": "A bright, ascending musical chime for collecting a health pickup heart.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.52, "loop": false, "animation": null}, "growth_pop_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/growth_pop_sound_d9dc2da4-4a68-4144-b5aa-9dd7da283cd0.mp3", "type": "audio", "prompt": "Arcade-style 'pop' sound effect for a kaiju monster growth animation. Features a punchy low-end thud combined with a bright, magical shimmering ping. Short and snappy duration with a quick descending pitch sweep. Playful, toy-like, and energetic aesthetic. Designed for rapid sequential playback, sounding like a satisfying power-up or level-up burst. High-quality, clear, and impactful without being realistic or dark.", "description": "A punchy, satisfying arcade-style pop sound for monster growth animations.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.24, "loop": false, "animation": null}, "tank_explosion_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/tank_explosion_sound_c1dac809-3dfc-457f-9f25-426efb869148.mp3", "type": "audio", "prompt": "Heavy powerful tank explosion sound effect for an arcade kaiju game. Features a deep, earth-shaking low-frequency 'BOOM' (60-200 Hz) combined with a sharp metallic crunch and the sound of heavy clanging armored plates. Includes a satisfying mid-range debris scatter after the initial punchy impact. The tone is weighted, armored, and more substantial than a standard vehicle explosion, with a slight reverberating tail. Low-pitched, cinematic-arcade style, punchy and clean.", "description": "A heavy, bass-heavy metallic explosion for a tank being crushed, featuring deep rumbles and clanging armored debris.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.34, "loop": false, "animation": null}, "helicopter_explosion_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/helicopter_explosion_sound_697817b1-1a6e-43a1-9feb-09f30aaf18b9.mp3", "type": "audio", "prompt": "Arcade-style helicopter explosion sound effect. Features a mid-pitched mechanical crash, the distinct sharp snapping of rotor blades, and shattering glass. Includes a quick high-pitched spinning-down turbine whine followed by light metallic debris scattering. Higher pitch than a tank explosion, satisfying and punchy for a colorful kaiju game, 1-2 seconds duration.", "description": "A light, mechanical crash sound effect with rotor blade snapping for a destroyed helicopter enemy.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.12, "loop": false, "animation": null}, "title_music_epic": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/title_music_epic_da1fea45-802b-497e-a743-f7e12bc7a772.mp3", "type": "audio", "prompt": "Modern arcade title screen music, high-energy synthwave style with a heroic and epic feel. Features rich, thick analog-style synthesizers, driving percussion with punchy bass kicks, and catchy melodic hooks. 120 BPM, C Major. The sound should be 'retrowave' with 80s influence but modern high-fidelity production, avoiding thin 8-bit chiptune bleeps. Grand opening with a rising synth swell leading into an upbeat, loopable anthem suitable for a Kaiju-themed game. Inspired by modern indie arcade hits, warm tones, and energetic rhythmic textures.", "description": "A grand, modern synthwave title theme with heroic melodies and punchy arcade production.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 45.03510204081633, "loop": true, "animation": null}, "battlefield_music_epic": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/battlefield_music_epic_64026e88-da72-438a-82cb-49a91ed3eb03.mp3", "type": "audio", "prompt": "Epic synthwave battle theme for an arcade kaiju game, 140 BPM, high energy. Matches the style of 'title_music_epic' with rich modern synths, retro arcade punch, and triumphant heroic melodies. Features heavy bass-heavy impacts resembling giant monster footsteps on the downbeat. Layered arpeggios, driving synth pads, and punchy electronic drums. Designed for a seamless loop with no silence at the end, maintaining a constant empowering and triumphant atmosphere for continuous city destruction gameplay. High-fidelity production with a mix of cinematic scale and chiptune textures.", "description": "High-energy gameplay battle theme featuring epic synthwave, heroic melodies, and heavy monster-stomp percussion.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 80.01306122448979, "loop": true, "animation": null}, "buff_icon_invincibility": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/buff_icon_invincibility_0efe06a8-8f0b-49a7-b80c-f753b2b080e2.webp", "type": "image", "prompt": "Generate a UI icon of a golden glowing star shape with sparkles for an invincibility buff. Designed in a vibrant, colorful toy-arcade style to match a kaiju city destruction game. The star is bright yellow-gold with white highlights and a soft outer glow. Simple, bold vector-like design that is highly legible at small sizes. Clean edges, professional game asset, transparent background.", "description": "A golden glowing star UI icon for the invincibility power-up buff indicator.", "subtype": "ui", "aspect_ratio": [769, 710], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "buff_icon_speed": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/buff_icon_speed_5a8356bf-4b0c-4dbc-8a8e-bf9a94a58247.webp", "type": "image", "prompt": "Generate a high-quality UI icon for a speed boost power-up. The icon features a bright, vibrant green lightning bolt (#00ff66) with white highlights, subtle glow effects, and dynamic motion lines. Designed in a colorful, low-poly toy-arcade style to match a kaiju city destruction game. Simple bold design, clean edges, professional game asset, transparent background.", "description": "A vibrant green lightning bolt UI icon representing a speed boost power-up in a toy-arcade style.", "subtype": "ui", "aspect_ratio": [826, 771], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Growzilla</title>\n    <style>\n        @import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&display=swap');\n\n        :root {\n            --transition-fast: 150ms;\n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n            --ease-out: cubic-bezier(0.0, 0, 0.2, 1);\n            --ease-bounce: cubic-bezier(0.68, -0.6, 0.32, 1.6);\n        }\n\n        * { margin: 0; padding: 0; box-sizing: border-box; }\n\n        body, html {\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            -ms-touch-action: none;\n            font-family: 'Fredoka', 'Arial', sans-serif;\n            background: #1a1a2e;\n            user-select: none;\n            -webkit-user-select: none;\n        }\n\n        #game-world { position: relative; width: 100%; height: 100%; overflow: hidden; }\n        #game-canvas { display: block; width: 100%; height: 100%; }\n\n        .ui-overlay {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            pointer-events: none;\n            z-index: 10;\n        }\n\n        /* ---------- Top Level Bar ---------- */\n        .level-bar-bg {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%;\n            height: 26px;\n            background: rgba(0, 0, 0, 0.55);\n            overflow: hidden;\n        }\n        .level-bar-fill {\n            height: 100%;\n            width: 0%;\n            background: linear-gradient(90deg, #ffd93d, #ff8c1a);\n            transition: width 0.18s ease-out;\n        }\n        .level-label {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%;\n            height: 26px;\n            line-height: 26px;\n            text-align: center;\n            font-size: 16px;\n            font-weight: 700;\n            letter-spacing: 2px;\n            color: #fff;\n            text-shadow: 1px 1px 0 #000, -1px -1px 0 #000;\n        }\n\n        /* ---------- Ability Status HUD ---------- */\n        .ability-hud {\n            position: absolute;\n            top: 105px;\n            left: var(--space-sm);\n            display: flex;\n            flex-direction: column;\n            gap: 8px;\n            pointer-events: none;\n        }\n        .ability-slot {\n            display: flex;\n            align-items: center;\n            gap: 4px;\n            background: #1e3c64;\n            border: 2px solid #6496ff;\n            border-radius: 8px;\n            padding: 4px 8px 4px 4px;\n            position: relative;\n        }\n        .ability-slot.passive {\n            background: #1e3214;\n            border: 2px solid #8bc34a;\n        }\n        .ability-icon {\n            width: 36px;\n            height: 36px;\n            border-radius: 6px;\n            background: #284c7a;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-size: 22px;\n            overflow: hidden;\n        }\n        .ability-slot.passive .ability-icon {\n            background: #2d4c1e;\n        }\n        .ability-icon img {\n            width: 100%;\n            height: 100%;\n            object-fit: contain;\n        }\n        .ability-level {\n            font-size: 16px;\n            font-weight: 700;\n            color: #ffd93d;\n            text-shadow: 1px 1px 0 #000;\n            min-width: 28px;\n        }\n        .ability-cooldown {\n            position: absolute;\n            bottom: 0;\n            left: 0;\n            height: 3px;\n            background: #4fc3f7;\n            border-radius: 0 0 6px 6px;\n            transition: width 0.1s linear;\n        }\n        .ability-slot.on-cooldown {\n            /* explicitly keep full opacity to avoid empty ruleset warning */\n            opacity: 1;\n        }\n        .ability-slot.on-cooldown .ability-icon {\n            filter: grayscale(50%);\n        }\n\n\n        /* ---------- Mini Map ---------- */\n        .minimap-container {\n            position: absolute;\n            top: 105px;\n            right: var(--space-sm);\n            width: 190px;\n            height: 190px;\n            background: rgba(0, 0, 0, 0.55);\n            border: 3px solid rgba(255, 255, 255, 0.4);\n            border-radius: 50%;\n            overflow: hidden;\n            pointer-events: none;\n            margin: 0;\n        }\n        .minimap-canvas {\n            width: 100%;\n            height: 100%;\n        }\n        .minimap-label {\n            position: absolute;\n            bottom: -20px;\n            left: 50%;\n            transform: translateX(-50%);\n            font-size: 11px;\n            color: rgba(255, 255, 255, 0.6);\n            text-shadow: 1px 1px 0 #000;\n            white-space: nowrap;\n        }\n\n        /* ---------- Score & Timer ---------- */\n        .score-container {\n            position: absolute;\n            top: 32px;\n            right: var(--space-sm);\n            text-align: right;\n        }\n        .score-text {\n            font-size: 34px;\n            font-weight: 700;\n            color: #ffd93d;\n            text-shadow: 3px 3px 0 #000, -1px -1px 0 #000;\n            line-height: 1;\n        }\n        .timer-text {\n            font-size: 18px;\n            font-weight: 600;\n            color: rgba(255, 255, 255, 0.85);\n            text-shadow: 2px 2px 0 #000;\n            margin-top: 2px;\n        }\n\n        /* ---------- Health Bar ---------- */\n        .health-container {\n            position: absolute;\n            top: 70px;\n            left: 50%;\n            transform: translateX(-50%);\n            width: 62%;\n            max-width: 360px;\n            display: flex;\n            align-items: center;\n            gap: var(--space-xs);\n        }\n        .health-icon { font-size: 20px; flex-shrink: 0; }\n        .health-bar-bg {\n            flex: 1;\n            height: 18px;\n            background: rgba(0, 0, 0, 0.55);\n            border-radius: 9px;\n            border: 2px solid rgba(255, 255, 255, 0.85);\n            overflow: hidden;\n        }\n        .health-bar-fill {\n            height: 100%;\n            width: 100%;\n            background: linear-gradient(90deg, #e23c2a 0%, #ff6b3d 55%, #ff9b1a 100%);\n            border-radius: 7px;\n            transition: width 0.2s ease-out;\n            background-size: 22px 100%, 100% 100%;\n        }\n\n        /* ---------- Joystick (full-screen drag) ---------- */\n        .mobile-controls {\n            position: absolute;\n            bottom: 0; left: 0;\n            width: 100%; height: 100%;\n            pointer-events: none;\n        }\n        .joystick-zone {\n            position: absolute;\n            left: 0; bottom: 0;\n            width: 100%; height: 100%;\n            pointer-events: auto;\n        }\n        .joystick-base {\n            position: absolute;\n            width: 120px; height: 120px;\n            background: rgba(255, 255, 255, 0.16);\n            border: 4px solid rgba(255, 255, 255, 0.35);\n            border-radius: 50%;\n            display: none;\n            pointer-events: none;\n        }\n        .joystick-knob {\n            position: absolute;\n            width: 56px; height: 56px;\n            background: rgba(255, 255, 255, 0.45);\n            border: 3px solid #fff;\n            border-radius: 50%;\n            transform: translate(-50%, -50%);\n            display: none;\n            pointer-events: none;\n        }\n\n        /* ---------- Upgrade (Mutation) Screen ---------- */\n        .mutation-screen {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: rgba(0, 0, 0, 0.85);\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 100;\n            pointer-events: auto;\n            padding: var(--space-sm);\n        }\n        .mutation-screen.active { display: flex; }\n        .mutation-title {\n            font-size: 42px;\n            font-weight: 700;\n            color: #ffd93d;\n            text-shadow: 4px 4px 0 #000;\n            margin-bottom: var(--space-lg);\n            text-align: center;\n        }\n        .mutation-cards {\n            display: flex;\n            flex-direction: column;\n            gap: var(--space-md);\n            width: 100%;\n            max-width: 520px;\n        }\n        .mutation-card {\n            width: 100%;\n            padding: var(--space-md) var(--space-lg);\n            background: linear-gradient(180deg, #4a69bd, #3c55a5);\n            border: 5px solid #fff;\n            border-radius: 20px;\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-bounce), background var(--transition-fast) var(--ease-out);\n            display: flex;\n            align-items: center;\n            gap: var(--space-md);\n            box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);\n        }\n        .mutation-card:hover, .mutation-card:active {\n            transform: scale(1.03);\n            background: linear-gradient(180deg, #5a7fd4, #4a69bd);\n        }\n        .mutation-card-icon {\n            width: 88px; height: 88px;\n            background: rgba(255, 255, 255, 0.2);\n            border-radius: 16px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-size: 44px;\n            flex-shrink: 0;\n            overflow: hidden;\n        }\n        .mutation-card-icon img {\n            width: 100%;\n            height: 100%;\n            object-fit: contain;\n        }\n        .mutation-card-text { flex: 1; }\n        .mutation-card-name {\n            font-size: 28px;\n            font-weight: 700;\n            color: #fff;\n            margin-bottom: 6px;\n        }\n        .mutation-card-desc {\n            font-size: 20px;\n            color: rgba(255, 255, 255, 0.92);\n            line-height: 1.35;\n        }\n\n        /* ---------- Game Over ---------- */\n        .game-over-screen {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: rgba(0, 0, 0, 0.9);\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 100;\n            pointer-events: auto;\n            padding: var(--space-sm);\n        }\n        .game-over-screen.active { display: flex; }\n        .game-over-title {\n            font-size: 56px;\n            font-weight: 700;\n            color: #ff6b6b;\n            text-shadow: 4px 4px 0 #000;\n            margin-bottom: var(--space-lg);\n            text-align: center;\n        }\n        .stats-container {\n            background: rgba(255, 255, 255, 0.1);\n            border-radius: 20px;\n            padding: var(--space-md) var(--space-lg);\n            margin-bottom: var(--space-lg);\n            min-width: 420px;\n            border: 3px solid rgba(255, 255, 255, 0.2);\n        }\n        .stat-row {\n            display: flex;\n            justify-content: space-between;\n            gap: var(--space-lg);\n            padding: 12px 0;\n            font-size: 26px;\n            color: #fff;\n            border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n        }\n        .stat-row:last-child { border-bottom: none; }\n        .stat-value { font-weight: 700; color: #ffd93d; }\n        .restart-btn, .start-btn, .home-btn {\n            padding: var(--space-md) var(--space-xl);\n            font-size: 32px;\n            font-weight: 700;\n            color: #fff;\n            border: 5px solid #fff;\n            border-radius: 18px;\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-bounce);\n            font-family: inherit;\n        }\n        .restart-btn { background: linear-gradient(180deg, #2ecc71, #27ae60); }\n        .home-btn { \n            background: linear-gradient(180deg, #3498db, #2980b9); \n            font-size: 24px;\n            padding: var(--space-sm) var(--space-lg);\n        }\n        .home-btn:hover, .home-btn:active { transform: scale(1.05); }\n        .game-over-buttons {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: var(--space-md);\n        }\n        .start-btn { \n            background: linear-gradient(180deg, #ff6b6b, #ee5a5a); \n            margin-bottom: var(--space-md);\n            box-shadow: 0 6px 20px rgba(0,0,0,0.4);\n            animation: btnGlow 1.5s ease-in-out infinite;\n        }\n        .restart-btn:hover, .restart-btn:active, .start-btn:hover, .start-btn:active { transform: scale(1.05); }\n\n        /* ---------- Start Screen ---------- */\n        .start-screen {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: linear-gradient(180deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);\n            display: none;\n            flex-direction: column;\n            align-items: center;\n            justify-content: flex-start;\n            z-index: 100;\n            pointer-events: auto;\n            padding: 0;\n            overflow: hidden;\n        }\n        .start-screen.active { display: flex; }\n        .start-screen.hidden { display: none; }\n        \n        .start-bg {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background-size: cover;\n            background-position: center;\n            z-index: 0;\n        }\n        .start-bg::after {\n            content: '';\n            position: absolute;\n            bottom: 0; left: 0;\n            width: 100%; height: 45%;\n            background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.5) 50%, transparent 100%);\n        }\n        \n        .start-content {\n            position: relative;\n            z-index: 1;\n            width: 100%;\n            height: 100%;\n            padding: var(--space-lg) var(--space-sm);\n        }\n        \n        .title-section {\n            position: absolute;\n            top: 4vh;\n            left: 50%;\n            transform: translateX(-50%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            width: 100%;\n            z-index: 2;\n        }\n        \n        .title-logo {\n            width: 100%;\n            max-width: 598px;\n            height: auto;\n            animation: titleFloat 3s ease-in-out infinite;\n            filter: drop-shadow(0 8px 20px rgba(0,0,0,0.6));\n        }\n        @keyframes titleFloat {\n            0%, 100% { transform: translateY(0) scale(1); }\n            50% { transform: translateY(-8px) scale(1.04); }\n        }\n        \n\n        \n        .game-title {\n            font-size: 46px;\n            font-weight: 700;\n            color: #6fc24a;\n            text-shadow: 4px 4px 0 #000, 0 0 20px rgba(111, 194, 74, 0.4);\n            text-align: center;\n            animation: titlePulse 2s ease-in-out infinite;\n        }\n        @keyframes titlePulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.03); } }\n        .game-subtitle {\n            font-size: 28px;\n            font-weight: 600;\n            color: #ff6b6b;\n            text-shadow: 3px 3px 0 #000;\n            margin-bottom: var(--space-md);\n            text-align: center;\n        }\n        \n        .button-section {\n            position: absolute;\n            bottom: 6vh;\n            left: 50%;\n            transform: translateX(-50%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 16px;\n            width: 100%;\n        }\n        \n        .leaderboard-panel {\n            background: linear-gradient(180deg, #1e3c64, #12223a);\n            border: 4px solid #ffd93d;\n            border-radius: 16px;\n            padding: 16px 20px;\n            max-width: 360px;\n            width: 85%;\n            margin-bottom: 20px;\n            box-shadow: 0 8px 0 #cc8c00, 0 15px 25px rgba(0,0,0,0.6);\n        }\n        .leaderboard-title {\n            font-size: 26px;\n            font-weight: 700;\n            color: #fff;\n            text-align: center;\n            margin-bottom: 14px;\n            text-shadow: 3px 3px 0 #e23c2a;\n            letter-spacing: 2px;\n        }\n        .leaderboard-entries {\n            display: flex;\n            flex-direction: column;\n            gap: 8px;\n            max-height: 240px;\n            overflow-y: auto;\n            scrollbar-width: thin;\n            scrollbar-color: #ffd93d #1e3c64;\n        }\n        .leaderboard-entries::-webkit-scrollbar {\n            width: 6px;\n        }\n        .leaderboard-entries::-webkit-scrollbar-track {\n            background: #1e3c64;\n            border-radius: 3px;\n        }\n        .leaderboard-entries::-webkit-scrollbar-thumb {\n            background: #ffd93d;\n            border-radius: 3px;\n        }\n        .leaderboard-entry {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            font-size: 18px;\n            font-weight: 700;\n            padding: 8px 14px;\n            background: rgba(0,0,0,0.35);\n            border-radius: 10px;\n            border: 2px solid transparent;\n        }\n        .leaderboard-entry.rank-1 {\n            background: rgba(255, 217, 61, 0.25);\n            border-color: #ffd93d;\n            font-size: 22px;\n        }\n        .leaderboard-entry.rank-2 {\n            background: rgba(192, 192, 192, 0.25);\n            border-color: #c0c0c0;\n        }\n        .leaderboard-entry.rank-3 {\n            background: rgba(205, 127, 50, 0.25);\n            border-color: #cd7f32;\n        }\n        .leaderboard-rank {\n            min-width: 40px;\n            text-shadow: 2px 2px 0 #000;\n        }\n        .leaderboard-rank.gold { color: #ffd93d; }\n        .leaderboard-rank.silver { color: #c0c0c0; }\n        .leaderboard-rank.bronze { color: #cd7f32; }\n        .leaderboard-rank.other { color: #ffffff; }\n        .leaderboard-name {\n            color: #ffffff;\n            font-size: 16px;\n            text-shadow: 2px 2px 0 #000;\n            flex: 1;\n            text-align: left;\n            overflow: hidden;\n            text-overflow: ellipsis;\n            white-space: nowrap;\n            margin: 0 8px;\n        }\n        .leaderboard-score {\n            color: #ffd93d;\n            font-size: 18px;\n            text-shadow: 2px 2px 0 #000;\n            letter-spacing: 1px;\n            min-width: 70px;\n            text-align: right;\n        }\n        .leaderboard-empty {\n            font-size: 18px;\n            font-weight: 600;\n            color: rgba(255, 255, 255, 0.9);\n            text-align: center;\n            padding: 12px 0;\n            text-shadow: 2px 2px 0 #000;\n        }\n        \n        .start-btn-img {\n            width: 320px;\n            max-width: 85%;\n            height: auto;\n            cursor: pointer;\n            transition: transform 0.15s var(--ease-bounce), filter 0.15s ease;\n            filter: drop-shadow(0 6px 16px rgba(0,0,0,0.5));\n            animation: btnGlow 1.5s ease-in-out infinite;\n            position: relative;\n        }\n        .start-btn-img:hover, .start-btn-img:active {\n            transform: scale(1.08);\n            filter: drop-shadow(0 8px 24px rgba(255,150,50,0.6));\n        }\n        .start-btn-img.loading-assets {\n            opacity: 0.6;\n            pointer-events: none;\n            animation: none;\n            filter: drop-shadow(0 6px 16px rgba(0,0,0,0.5)) grayscale(0.3);\n        }\n        .start-loading-text {\n            position: absolute;\n            bottom: 6vh;\n            left: 50%;\n            transform: translateX(-50%) translateY(48px);\n            font-size: 16px;\n            font-weight: 600;\n            color: rgba(255, 255, 255, 0.8);\n            text-shadow: 2px 2px 0 #000;\n            white-space: nowrap;\n            pointer-events: none;\n            animation: loadPulse 1.2s ease-in-out infinite;\n        }\n        .start-loading-text.hidden { display: none; }\n        @keyframes loadPulse {\n            0%, 100% { opacity: 0.7; }\n            50% { opacity: 1; }\n        }\n        @keyframes btnGlow {\n            0%, 100% { filter: drop-shadow(0 6px 16px rgba(0,0,0,0.5)) brightness(1); }\n            50% { filter: drop-shadow(0 8px 24px rgba(255,150,50,0.4)) brightness(1.1); }\n        }\n        \n        .monster-preview {\n            position: absolute;\n            bottom: 200px;\n            right: -40px;\n            width: 280px;\n            height: 280px;\n            opacity: 0.15;\n            pointer-events: none;\n            animation: monsterBob 4s ease-in-out infinite;\n        }\n        @keyframes monsterBob {\n            0%, 100% { transform: translateY(0) rotate(-5deg); }\n            50% { transform: translateY(-15px) rotate(5deg); }\n        }\n        \n        .particles-overlay {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            pointer-events: none;\n            overflow: hidden;\n            z-index: 0;\n        }\n        .particle {\n            position: absolute;\n            background: rgba(255, 200, 100, 0.6);\n            border-radius: 50%;\n            animation: particleFall linear infinite;\n        }\n        @keyframes particleFall {\n            0% { transform: translateY(-20px) rotate(0deg); opacity: 0; }\n            10% { opacity: 1; }\n            90% { opacity: 1; }\n            100% { transform: translateY(100vh) rotate(360deg); opacity: 0; }\n        }\n\n        /* ---------- Floating Score Text ---------- */\n        .floating-text {\n            position: absolute;\n            font-size: 24px;\n            font-weight: 700;\n            color: #ffd93d;\n            text-shadow: 2px 2px 0 #000, -1px -1px 0 #000;\n            pointer-events: none;\n            transform: translate(-50%, -50%);\n            animation: floatUp 0.9s ease-out forwards;\n        }\n        @keyframes floatUp {\n            0% { opacity: 1; transform: translate(-50%, -50%) scale(1); }\n            100% { opacity: 0; transform: translate(-50%, -160%) scale(0.85); }\n        }\n\n        /* ---------- Combo Counter ---------- */\n        .combo-text {\n            position: absolute;\n            top: 96px;\n            left: 50%;\n            transform: translateX(-50%);\n            font-size: 42px;\n            font-weight: 700;\n            color: #ff8c1a;\n            text-shadow: 3px 3px 0 #000, -1px -1px 0 #000, 0 0 18px rgba(255, 140, 26, 0.6);\n            pointer-events: none;\n            opacity: 0;\n            white-space: nowrap;\n        }\n\n        /* ---------- Edit Banner ---------- */\n        .edit-mode-banner {\n            position: absolute;\n            top: 60px; left: 50%;\n            transform: translateX(-50%);\n            background: rgba(255, 193, 7, 0.9);\n            color: #000;\n            padding: var(--space-xs) var(--space-md);\n            border-radius: 8px;\n            font-size: 16px;\n            font-weight: 600;\n            display: none;\n        }\n        .edit-mode-banner.active { display: block; }\n\n        /* ---------- Tutorial ---------- */\n        .tutorial-overlay {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            pointer-events: none;\n            z-index: 15;\n            display: none;\n        }\n        .tutorial-overlay.active { display: block; }\n        .tutorial-arrow {\n            position: absolute;\n            top: 22%;\n            left: 50%;\n            transform: translateX(-50%);\n            font-size: 64px;\n            color: #ffd93d;\n            text-shadow: 3px 3px 0 #000, 0 0 20px rgba(255, 217, 61, 0.6);\n            animation: bounceArrow 0.8s ease-in-out infinite;\n        }\n        @keyframes bounceArrow {\n            0%, 100% { transform: translateX(-50%) translateY(0); }\n            50% { transform: translateX(-50%) translateY(-15px); }\n        }\n        .tutorial-text {\n            position: absolute;\n            top: 68%;\n            left: 50%;\n            transform: translateX(-50%);\n            background: rgba(0, 0, 0, 0.85);\n            color: #fff;\n            padding: 24px 44px;\n            border-radius: 20px;\n            font-size: 32px;\n            font-weight: 700;\n            text-align: center;\n            border: 5px solid #ffd93d;\n            white-space: nowrap;\n            animation: pulseGlow 1.5s ease-in-out infinite;\n            max-width: 90%;\n        }\n        @keyframes pulseGlow {\n            0%, 100% { box-shadow: 0 0 15px rgba(255, 217, 61, 0.5); }\n            50% { box-shadow: 0 0 30px rgba(255, 217, 61, 0.9); }\n        }\n\n        /* ---------- Loading Screen ---------- */\n        .loading-screen {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: linear-gradient(180deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 200;\n            pointer-events: none;\n        }\n        .loading-screen.hidden { display: none; }\n        \n        .loading-content {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 30px;\n        }\n        \n        .loading-title {\n            font-size: 32px;\n            font-weight: 700;\n            color: #6fc24a;\n            text-shadow: 3px 3px 0 #000, 0 0 15px rgba(111, 194, 74, 0.4);\n            text-align: center;\n            letter-spacing: 2px;\n        }\n        \n        .dino-container {\n            position: relative;\n            width: 280px;\n            height: 60px;\n        }\n        \n        .dino {\n            position: absolute;\n            bottom: 0;\n            left: 0;\n            width: 56px;\n            height: 56px;\n            transition: left 0.15s linear;\n            image-rendering: pixelated;\n            image-rendering: crisp-edges;\n        }\n        \n        .dino svg {\n            width: 100%;\n            height: 100%;\n            image-rendering: pixelated;\n        }\n        \n        .dino-leg-1, .dino-leg-2 {\n            position: absolute;\n            bottom: 0;\n            left: 0;\n            width: 56px;\n            height: 56px;\n        }\n        \n        .dino-leg-1 { animation: legAnim1 0.1s steps(1) infinite; }\n        .dino-leg-2 { animation: legAnim2 0.1s steps(1) infinite; }\n        \n        @keyframes legAnim1 {\n            0%, 50% { opacity: 1; }\n            50.01%, 100% { opacity: 0; }\n        }\n        @keyframes legAnim2 {\n            0%, 50% { opacity: 0; }\n            50.01%, 100% { opacity: 1; }\n        }\n        \n        .dino-body {\n            animation: dinoRun 0.2s steps(1) infinite;\n        }\n        \n        @keyframes dinoRun {\n            0%, 50% { transform: translateY(0); }\n            50.01%, 100% { transform: translateY(-1px); }\n        }\n        \n        .ground-line {\n            position: absolute;\n            bottom: 0;\n            left: 0;\n            width: 100%;\n            height: 2px;\n            background: linear-gradient(90deg, transparent 0%, #6fc24a 20%, #6fc24a 80%, transparent 100%);\n        }\n        \n        .loading-bar-container {\n            width: 280px;\n            height: 24px;\n            background: rgba(0, 0, 0, 0.5);\n            border: 3px solid #6fc24a;\n            border-radius: 12px;\n            overflow: hidden;\n            box-shadow: 0 0 15px rgba(111, 194, 74, 0.3);\n        }\n        \n        .loading-bar-fill {\n            height: 100%;\n            width: 0%;\n            background: linear-gradient(90deg, #2ecc71, #6fc24a, #a6d65e);\n            border-radius: 9px;\n            transition: width 0.2s ease-out;\n            box-shadow: 0 0 10px rgba(111, 194, 74, 0.5);\n        }\n        \n        .loading-text {\n            font-size: 16px;\n            font-weight: 600;\n            color: rgba(255, 255, 255, 0.8);\n            text-shadow: 2px 2px 0 #000;\n            letter-spacing: 1px;\n        }\n        \n        .loading-percent {\n            font-size: 20px;\n            font-weight: 700;\n            color: #ffd93d;\n            text-shadow: 2px 2px 0 #000;\n        }\n\n        /* ---------- Star Power Active Banner ---------- */\n        .star-power-banner {\n            position: absolute;\n            top: 155px;\n            left: 50%;\n            transform: translateX(-50%);\n            display: none;\n            align-items: center;\n            gap: 10px;\n            background: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.7) 20%, rgba(0,0,0,0.7) 80%, rgba(0,0,0,0) 100%);\n            padding: 6px 28px;\n            border-radius: 40px;\n            pointer-events: none;\n            z-index: 18;\n            white-space: nowrap;\n        }\n        .star-power-banner.active { display: flex; }\n        .star-power-banner-icon {\n            font-size: 28px;\n            animation: starSpin 1.2s linear infinite;\n        }\n        @keyframes starSpin {\n            0% { transform: rotate(0deg) scale(1); }\n            50% { transform: rotate(180deg) scale(1.25); }\n            100% { transform: rotate(360deg) scale(1); }\n        }\n        .star-power-banner-text {\n            font-size: 26px;\n            font-weight: 700;\n            color: #ffd700;\n            text-shadow: 0 0 12px #ffd700, 2px 2px 0 #000, -1px -1px 0 #000;\n            animation: starTextPulse 0.6s ease-in-out infinite;\n            letter-spacing: 2px;\n        }\n        @keyframes starTextPulse {\n            0%, 100% { opacity: 1; transform: scale(1); }\n            50% { opacity: 0.75; transform: scale(1.04); }\n        }\n        .star-power-banner.warning .star-power-banner-text {\n            color: #ff8c00;\n            text-shadow: 0 0 12px #ff6600, 2px 2px 0 #000;\n            animation: starTextWarning 0.25s ease-in-out infinite;\n        }\n        @keyframes starTextWarning {\n            0%, 100% { opacity: 1; }\n            50% { opacity: 0.5; }\n        }\n\n        /* ---------- Star Power Screen Vignette ---------- */\n        .star-power-vignette {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            pointer-events: none;\n            z-index: 9;\n            border-radius: 0;\n            opacity: 0;\n            transition: opacity 0.4s ease;\n            box-shadow: inset 0 0 80px 30px rgba(255, 215, 0, 0.35);\n        }\n        .star-power-vignette.active { opacity: 1; }\n        .star-power-vignette.warning { box-shadow: inset 0 0 80px 30px rgba(255, 100, 0, 0.45); }\n\n        /* Buff Timer HUD removed - star power banner + vignette are the sole indicators */\n\n        /* ---------- Flashes ---------- */\n        .level-up-flash, .damage-flash {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            pointer-events: none;\n            opacity: 0;\n            z-index: 50;\n        }\n        .level-up-flash { background: rgba(255, 215, 0, 0.3); }\n        .damage-flash { background: rgba(255, 0, 0, 0.3); }\n        .level-up-flash.active { animation: flashPulse 0.5s ease-out; }\n        .damage-flash.active { animation: flashPulse 0.2s ease-out; }\n        @keyframes flashPulse { 0% { opacity: 0.55; } 100% { opacity: 0; } }\n\n        /* ---------- In-Game Home Button ---------- */\n        .ingame-home-btn {\n            position: absolute;\n            bottom: 24px;\n            right: 16px;\n            width: 56px;\n            height: 56px;\n            background: rgba(0, 0, 0, 0.55);\n            border: 3px solid rgba(255, 255, 255, 0.5);\n            border-radius: 50%;\n            display: none;\n            align-items: center;\n            justify-content: center;\n            font-size: 28px;\n            cursor: pointer;\n            pointer-events: auto;\n            z-index: 20;\n            transition: transform 0.15s ease, background 0.15s ease;\n        }\n        .ingame-home-btn.active { display: flex; }\n        .ingame-home-btn:hover, .ingame-home-btn:active {\n            transform: scale(1.1);\n            background: rgba(0, 0, 0, 0.75);\n        }\n\n        /* ---------- Quit Confirmation ---------- */\n        .quit-confirm-overlay {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: rgba(0, 0, 0, 0.8);\n            display: none;\n            align-items: center;\n            justify-content: center;\n            z-index: 150;\n            pointer-events: auto;\n        }\n        .quit-confirm-overlay.active { display: flex; }\n        .quit-confirm-box {\n            background: linear-gradient(180deg, #2c3e50, #1a252f);\n            border: 4px solid #ffd93d;\n            border-radius: 20px;\n            padding: 32px 28px;\n            max-width: 380px;\n            width: 85%;\n            text-align: center;\n            box-shadow: 0 8px 30px rgba(0,0,0,0.6);\n        }\n        .quit-confirm-title {\n            font-size: 32px;\n            font-weight: 700;\n            color: #ffd93d;\n            text-shadow: 3px 3px 0 #000;\n            margin-bottom: 12px;\n        }\n        .quit-confirm-text {\n            font-size: 24px;\n            color: rgba(255,255,255,0.9);\n            margin-bottom: 24px;\n            line-height: 1.4;\n        }\n        .quit-confirm-buttons {\n            display: flex;\n            flex-direction: column;\n            gap: 12px;\n        }\n        .quit-confirm-yes, .quit-confirm-no {\n            padding: 14px 24px;\n            font-size: 26px;\n            font-weight: 700;\n            border: 3px solid #fff;\n            border-radius: 12px;\n            cursor: pointer;\n            font-family: inherit;\n            transition: transform 0.15s ease;\n        }\n        .quit-confirm-yes {\n            background: linear-gradient(180deg, #e74c3c, #c0392b);\n            color: #fff;\n        }\n        .quit-confirm-no {\n            background: linear-gradient(180deg, #2ecc71, #27ae60);\n            color: #fff;\n        }\n        .quit-confirm-yes:hover, .quit-confirm-yes:active,\n        .quit-confirm-no:hover, .quit-confirm-no:active { transform: scale(1.05); }\n\n        /* ---------- Boss Progress Indicator ---------- */\n        .boss-progress-bar {\n            position: absolute;\n            top: 32px;\n            left: 50%;\n            transform: translateX(-50%);\n            display: none;\n            align-items: center;\n            gap: 0;\n            padding: 4px 12px;\n            background: rgba(0, 0, 0, 0.55);\n            border-radius: 14px;\n            border: 2px solid rgba(255, 255, 255, 0.3);\n            pointer-events: none;\n            z-index: 12;\n        }\n        .boss-progress-bar.active { display: flex; }\n        .boss-progress-track {\n            display: flex;\n            align-items: center;\n            gap: 0;\n        }\n        .boss-node {\n            width: 18px;\n            height: 18px;\n            border-radius: 4px;\n            transform: rotate(45deg);\n            background: #2a5a2a;\n            border: 2px solid #4a8a4a;\n            transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;\n            flex-shrink: 0;\n        }\n        .boss-node.filled {\n            background: #4ecc4e;\n            border-color: #88ff88;\n            box-shadow: 0 0 6px rgba(78, 204, 78, 0.6);\n        }\n        .boss-node.current {\n            background: #7aee7a;\n            border-color: #bbffbb;\n            box-shadow: 0 0 10px rgba(122, 238, 122, 0.8);\n            animation: nodePulse 1s ease-in-out infinite;\n        }\n        @keyframes nodePulse {\n            0%, 100% { transform: rotate(45deg) scale(1); }\n            50% { transform: rotate(45deg) scale(1.2); }\n        }\n        .boss-connector {\n            width: 14px;\n            height: 3px;\n            background: #2a5a2a;\n            border-radius: 2px;\n            transition: background 0.3s;\n            flex-shrink: 0;\n        }\n        .boss-connector.filled {\n            background: #4ecc4e;\n            box-shadow: 0 0 4px rgba(78, 204, 78, 0.4);\n        }\n        .boss-skull {\n            width: 28px;\n            height: 28px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-size: 20px;\n            background: #4a1a1a;\n            border: 2px solid #cc3333;\n            border-radius: 50%;\n            margin-left: 2px;\n            flex-shrink: 0;\n            transition: box-shadow 0.3s;\n        }\n        .boss-skull.imminent {\n            box-shadow: 0 0 12px rgba(255, 50, 50, 0.8);\n            animation: skullPulse 0.6s ease-in-out infinite;\n        }\n        @keyframes skullPulse {\n            0%, 100% { transform: scale(1); }\n            50% { transform: scale(1.15); }\n        }\n\n        /* ---------- Boss Health Bar ---------- */\n        .boss-health-container {\n            display: none !important;\n        }\n        .boss-name-label {\n            font-size: 18px;\n            font-weight: 700;\n            color: #ff4444;\n            text-shadow: 2px 2px 0 #000, -1px -1px 0 #000;\n            letter-spacing: 2px;\n        }\n        .boss-health-bar-bg {\n            width: 100%;\n            height: 14px;\n            background: rgba(0, 0, 0, 0.7);\n            border-radius: 7px;\n            border: 2px solid #ff4444;\n            overflow: hidden;\n        }\n        .boss-health-bar-fill {\n            height: 100%;\n            width: 100%;\n            background: linear-gradient(90deg, #ff2222 0%, #ff6644 50%, #ffaa22 100%);\n            border-radius: 5px;\n            transition: width 0.15s ease-out;\n        }\n\n        /* ---------- Boss Warning Overlay ---------- */\n        .boss-warning-overlay {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            display: none;\n            align-items: center;\n            justify-content: center;\n            flex-direction: column;\n            gap: 20px;\n            z-index: 90;\n            pointer-events: none;\n            background: rgba(0, 0, 0, 0.5);\n        }\n        .boss-warning-overlay.active { display: flex; }\n        .boss-warning-text {\n            font-size: 64px;\n            font-weight: 700;\n            color: #ff2222;\n            text-shadow: 4px 4px 0 #000, 0 0 30px rgba(255, 0, 0, 0.6);\n            animation: warningPulse 0.4s ease-in-out infinite;\n            letter-spacing: 6px;\n        }\n        @keyframes warningPulse {\n            0%, 100% { transform: scale(1); opacity: 1; }\n            50% { transform: scale(1.08); opacity: 0.7; }\n        }\n        .boss-intro-name {\n            font-size: 32px;\n            font-weight: 700;\n            color: #ffffff;\n            text-shadow: 3px 3px 0 #000, 0 0 20px rgba(255, 255, 255, 0.5);\n            letter-spacing: 4px;\n            opacity: 0;\n            transition: opacity 0.5s ease;\n        }\n        .boss-intro-name.active { opacity: 1; }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"game-canvas\"></canvas>\n        \n        <!-- Loading Screen -->\n        <div class=\"loading-screen\" id=\"loading-screen\">\n            <div class=\"loading-content\">\n                <div class=\"loading-title\">\ud83e\udd96 GROWZILLA</div>\n                \n                <div class=\"dino-container\">\n                    <div class=\"dino\" id=\"walking-dino\">\n                        <!-- Frame 1: Right leg forward, left leg back -->\n                        <svg class=\"dino-leg-1\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\">\n                            <!-- Body -->\n                            <rect fill=\"#6fc24a\" x=\"6\" y=\"3\" width=\"8\" height=\"6\"/>\n                            <!-- Head -->\n                            <rect fill=\"#6fc24a\" x=\"12\" y=\"1\" width=\"6\" height=\"5\"/>\n                            <!-- Eye -->\n                            <rect fill=\"#1a1a1a\" x=\"15\" y=\"2\" width=\"2\" height=\"2\"/>\n                            <!-- Tail -->\n                            <rect fill=\"#6fc24a\" x=\"2\" y=\"4\" width=\"4\" height=\"3\"/>\n                            <rect fill=\"#6fc24a\" x=\"0\" y=\"5\" width=\"2\" height=\"2\"/>\n                            <!-- Arms -->\n                            <rect fill=\"#6fc24a\" x=\"12\" y=\"7\" width=\"2\" height=\"3\"/>\n                            <!-- Right leg (forward) -->\n                            <rect fill=\"#6fc24a\" x=\"11\" y=\"9\" width=\"2\" height=\"4\"/>\n                            <rect fill=\"#6fc24a\" x=\"13\" y=\"12\" width=\"2\" height=\"1\"/>\n                            <!-- Left leg (back, lifted) -->\n                            <rect fill=\"#5aa33d\" x=\"7\" y=\"9\" width=\"2\" height=\"2\"/>\n                            <rect fill=\"#5aa33d\" x=\"5\" y=\"10\" width=\"2\" height=\"2\"/>\n                        </svg>\n                        \n                        <!-- Frame 2: Left leg forward, right leg back -->\n                        <svg class=\"dino-leg-2\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\">\n                            <!-- Body -->\n                            <rect fill=\"#6fc24a\" x=\"6\" y=\"3\" width=\"8\" height=\"6\"/>\n                            <!-- Head -->\n                            <rect fill=\"#6fc24a\" x=\"12\" y=\"1\" width=\"6\" height=\"5\"/>\n                            <!-- Eye -->\n                            <rect fill=\"#1a1a1a\" x=\"15\" y=\"2\" width=\"2\" height=\"2\"/>\n                            <!-- Tail -->\n                            <rect fill=\"#6fc24a\" x=\"2\" y=\"4\" width=\"4\" height=\"3\"/>\n                            <rect fill=\"#6fc24a\" x=\"0\" y=\"5\" width=\"2\" height=\"2\"/>\n                            <!-- Arms -->\n                            <rect fill=\"#6fc24a\" x=\"12\" y=\"7\" width=\"2\" height=\"3\"/>\n                            <!-- Left leg (forward) -->\n                            <rect fill=\"#6fc24a\" x=\"7\" y=\"9\" width=\"2\" height=\"4\"/>\n                            <rect fill=\"#6fc24a\" x=\"9\" y=\"12\" width=\"2\" height=\"1\"/>\n                            <!-- Right leg (back, lifted) -->\n                            <rect fill=\"#5aa33d\" x=\"11\" y=\"9\" width=\"2\" height=\"2\"/>\n                            <rect fill=\"#5aa33d\" x=\"13\" y=\"10\" width=\"2\" height=\"2\"/>\n                        </svg>\n                    </div>\n                    <div class=\"ground-line\"></div>\n                </div>\n                \n                <div class=\"loading-bar-container\">\n                    <div class=\"loading-bar-fill\" id=\"loading-bar-fill\"></div>\n                </div>\n                \n                <div class=\"loading-text\" id=\"loading-text\">Loading assets...</div>\n                <div class=\"loading-percent\" id=\"loading-percent\">0%</div>\n            </div>\n        </div>\n\n        <div class=\"ui-overlay\">\n            <!-- Top Level Bar -->\n            <div class=\"level-bar-bg\"><div class=\"level-bar-fill\" id=\"level-fill\"></div></div>\n            <div class=\"level-label\" id=\"level-label\">LVL 1</div>\n\n            <!-- Ability HUD -->\n            <div class=\"ability-hud\" id=\"ability-hud\"></div>\n\n            <!-- Combo -->\n            <div class=\"combo-text\" id=\"combo-text\"></div>\n\n            <!-- Score & Timer -->\n            <div class=\"score-container\">\n                <div class=\"score-text\" id=\"score-text\">0</div>\n                <div class=\"timer-text\" id=\"timer-text\">0:00</div>\n            </div>\n\n            <!-- Health -->\n            <div class=\"health-container\">\n                <span class=\"health-icon\">&#10084;&#65039;</span>\n                <div class=\"health-bar-bg\"><div class=\"health-bar-fill\" id=\"health-fill\"></div></div>\n            </div>\n\n            <!-- Joystick -->\n            <div class=\"mobile-controls\" id=\"mobile-controls\">\n                <div class=\"joystick-zone\" id=\"joystick-zone\">\n                    <div class=\"joystick-base\" id=\"joystick-base\"></div>\n                    <div class=\"joystick-knob\" id=\"joystick-knob\"></div>\n                </div>\n            </div>\n\n            <!-- Mini Map -->\n\n\n            <div class=\"minimap-container\" id=\"minimap-container\">\n                <canvas class=\"minimap-canvas\" id=\"minimap-canvas\" width=\"190\" height=\"190\"></canvas>\n            </div>\n\n\n            <!-- Boss UI -->\n            <div class=\"boss-progress-bar\" id=\"boss-progress-bar\">\n                <div class=\"boss-progress-track\" id=\"boss-progress-track\"></div>\n                <div class=\"boss-skull\" id=\"boss-skull\">\ud83d\udc80</div>\n            </div>\n\n            <div class=\"boss-health-container\" id=\"boss-health-container\">\n                <div class=\"boss-name-label\" id=\"boss-name-label\">IRON GUARDIAN MK-I</div>\n                <div class=\"boss-health-bar-bg\"><div class=\"boss-health-bar-fill\" id=\"boss-health-fill\"></div></div>\n            </div>\n            <div class=\"boss-warning-overlay\" id=\"boss-warning-overlay\">\n                <div class=\"boss-warning-text\">\u26a0\ufe0f WARNING \u26a0\ufe0f</div>\n                <div class=\"boss-intro-name\" id=\"boss-intro-name\"></div>\n            </div>\n\n            <!-- Star Power Banner -->\n            <div class=\"star-power-banner\" id=\"star-power-banner\">\n                <div class=\"star-power-banner-icon\">\u2b50</div>\n                <div class=\"star-power-banner-text\" id=\"star-power-banner-text\">STAR POWER!</div>\n                <div class=\"star-power-banner-icon\">\u2b50</div>\n            </div>\n\n            <!-- Star Power Vignette -->\n            <div class=\"star-power-vignette\" id=\"star-power-vignette\"></div>\n\n            <div class=\"level-up-flash\" id=\"level-up-flash\"></div>\n            <div class=\"damage-flash\" id=\"damage-flash\"></div>\n            <div class=\"edit-mode-banner\" id=\"edit-banner\">EDIT MODE - Customize your game</div>\n\n            <!-- Tutorial -->\n            <div class=\"ingame-home-btn\" id=\"ingame-home-btn\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"28\" height=\"28\" viewBox=\"0 0 24 24\"><path fill=\"white\" d=\"M4 21v-9.375L2.2 13L1 11.4L12 3l11 8.4l-1.2 1.575l-1.8-1.35V21zm4-6q-.425 0-.712-.288T7 14t.288-.712T8 13t.713.288T9 14t-.288.713T8 15m4 0q-.425 0-.712-.288T11 14t.288-.712T12 13t.713.288T13 14t-.288.713T12 15m4 0q-.425 0-.712-.288T15 14t.288-.712T16 13t.713.288T17 14t-.288.713T16 15\"/></svg></div>\n\n            <div class=\"quit-confirm-overlay\" id=\"quit-confirm-overlay\">\n                <div class=\"quit-confirm-box\">\n                    <div class=\"quit-confirm-title\">End Game?</div>\n                    <div class=\"quit-confirm-text\">Do you want to end the game with your current score?</div>\n                    <div class=\"quit-confirm-buttons\">\n                        <button class=\"quit-confirm-yes\" id=\"quit-confirm-yes\">Yes, End Game</button>\n                        <button class=\"quit-confirm-no\" id=\"quit-confirm-no\">No, Keep Playing</button>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"tutorial-overlay\" id=\"tutorial-overlay\">\n                <div class=\"tutorial-arrow\">\u2b06\ufe0f</div>\n                <div class=\"tutorial-text\">\ud83e\udd96 Walk into the building to SMASH it!</div>\n            </div>\n        </div>\n\n        <!-- Start Screen -->\n        <div class=\"start-screen\" id=\"start-screen\">\n            <div class=\"start-bg\" id=\"start-bg\"></div>\n            <div class=\"particles-overlay\" id=\"particles-overlay\"></div>\n            <div class=\"start-content\">\n                <div class=\"title-section\">\n                    <img class=\"title-logo\" id=\"title-logo\" alt=\"MEGA MUNCHER\">\n                </div>\n                <div class=\"button-section\">\n                    <div class=\"leaderboard-panel\" id=\"leaderboard-panel\">\n                        <div class=\"leaderboard-title\">\ud83c\udfc6 TOP KAIJUS</div>\n                        <div class=\"leaderboard-entries\" id=\"leaderboard-entries\">\n                            <div class=\"leaderboard-empty\">Loading...</div>\n                        </div>\n                    </div>\n                    <img class=\"start-btn-img loading-assets\" id=\"start-btn\" alt=\"START\">\n                    <div class=\"start-loading-text\" id=\"start-loading-text\">\u23f3 Loading game assets...</div>\n                </div>\n            </div>\n        </div>\n\n        <!-- Upgrade Screen -->\n        <div class=\"mutation-screen\" id=\"mutation-screen\">\n            <div class=\"mutation-title\">&#129516; CHOOSE A MUTATION!</div>\n            <div class=\"mutation-cards\" id=\"mutation-cards\"></div>\n        </div>\n\n        <!-- Game Over -->\n        <div class=\"game-over-screen\" id=\"game-over-screen\">\n            <div class=\"game-over-title\">&#128128; GAME OVER</div>\n            <div class=\"stats-container\">\n                <div class=\"stat-row\"><span>Final Score</span><span class=\"stat-value\" id=\"final-score\">0</span></div>\n                <div class=\"stat-row\"><span>Time Survived</span><span class=\"stat-value\" id=\"final-time\">0:00</span></div>\n                <div class=\"stat-row\"><span>Level Reached</span><span class=\"stat-value\" id=\"final-level\">1</span></div>\n                <div class=\"stat-row\"><span>Buildings Crushed</span><span class=\"stat-value\" id=\"final-buildings\">0</span></div>\n                <div class=\"stat-row\"><span>Enemies Crushed</span><span class=\"stat-value\" id=\"final-enemies\">0</span></div>\n                <div class=\"stat-row\" id=\"rank-row\" style=\"display:none;\"><span>\ud83c\udfc6 Global Rank</span><span class=\"stat-value\" id=\"final-rank\">-</span></div>\n            </div>\n            <div class=\"game-over-buttons\">\n                <button class=\"restart-btn\" id=\"restart-btn\">&#128260; TRY AGAIN</button>\n                <button class=\"home-btn\" id=\"home-btn\">\ud83c\udfe0 BACK TO HOME</button>\n            </div>\n        </div>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n    <script>\n    /**\n     * MEGA MUNCHER: CITY RAMPAGE\n     * A blockzilla-style 3D growth rampage.\n     *\n     * Core loop: move the monster (no weapons) -> walking into people, cars,\n     * trees, buildings and ground enemies crushes them automatically -> each\n     * crush grants score + XP + mass -> mass smoothly grows the monster, which\n     * widens its crush footprint and lets it reach flying helicopters -> XP\n     * fills the level bar -> level up lets you pick a mutation.\n     */\n\n    async function run(mode) {\n        const gameWorld = document.getElementById('game-world');\n        const canvas = document.getElementById('game-canvas');\n\n        // Safe wrapper around the Astrocade host API.\n        const lib = window.lib || {\n            getAsset: () => null,\n            log: () => {},\n            showGameParameters: () => {}\n        };\n        window.gameConfig = window.gameConfig || {};\n\n        // ==================== CONSTANTS / TUNING ====================\n        const BLOCK_SIZE = 28;\n        const ROAD_WIDTH = 8;\n        const GRAVITY = 120;\n        const MAX_DELTA = 0.1;\n\n        // Infinite world streaming\n        const BLOCK_PERIOD = BLOCK_SIZE + ROAD_WIDTH;        // 36 \u2014 one block + its road\n        const CHUNK_BLOCKS = 2;\n        const CHUNK_SIZE = CHUNK_BLOCKS * BLOCK_PERIOD;       // 72\n        const CHUNK_RADIUS = 3;     // load chunks within this many of the player's chunk (7x7)\n        const CHUNK_UNLOAD = 4;     // unload beyond this (hysteresis avoids boundary thrash)\n        const CROWD_CULL_DIST = 250; // people/cars beyond this from the player are removed\n\n        const GROWTH_K = 0.11;          // size = STARTING_SIZE + GROWTH_K * sqrt(mass) (lower = slower growth)\n        const CAM_EXP = 0.85;           // camera distance scaling with size (higher = more proportional zoom-out)\n        const CAM_BASE_DIST = 36;       // increased distance to offset lower angle\n        const CAM_BASE_HEIGHT = 25;     // significantly lowered height for a more epic, lower cinematic angle that emphasizes height/scale\n        const HELI_ALT = 32;            // helicopter flight altitude\n        const MONSTER_TOP_UNIT = 16;    // model head-top height at size 1\n        const BODY_HALF_WIDTH = 3.6;    // monster body half width at size 1\n\n        // ==================== CONFIG ====================\n        const defaultConfig = {\n            monster: {\n                primaryColor: '#6fa83a',\n                secondaryColor: '#a6d65e',\n                eyeColor: '#ffe14d',\n                startingSize: 0.3\n            },\n            city: { density: 'normal', propDensity: 'normal' },\n            gameplay: { difficulty: 'normal', xpGainRate: 1 },\n            graphics: {\n                qualityMode: 0, // 0 auto, 1 best ... 5 lowest\n                qualityDowngradeFps: 48,\n                qualityUpgradeFps: 57\n            }\n        };\n        function mergeConfig(base, override) {\n            const out = {};\n            for (const k in base) {\n                if (base[k] && typeof base[k] === 'object' && !Array.isArray(base[k])) {\n                    out[k] = mergeConfig(base[k], (override && override[k]) || {});\n                } else {\n                    out[k] = (override && override[k] !== undefined) ? override[k] : base[k];\n                }\n            }\n            return out;\n        }\n        const config = mergeConfig(defaultConfig, window.gameConfig || {});\n        const STARTING_SIZE = config.monster.startingSize;\n\n        // ==================== ASSET LOADING ====================\n        const assetCache = { images: {}, audio: {} };\n        let audioContext = null;\n        let loadingProgress = 0;\n        let totalAssetsToLoad = 0;\n        let assetsLoaded = 0;\n\n        function getAudioContext() {\n            if (!audioContext) {\n                audioContext = new (window.AudioContext || window.webkitAudioContext)();\n            }\n            return audioContext;\n        }\n        \n        function updateLoadingUI(progress, statusText) {\n            const fillEl = document.getElementById('loading-bar-fill');\n            const textEl = document.getElementById('loading-text');\n            const percentEl = document.getElementById('loading-percent');\n            const dinoEl = document.getElementById('walking-dino');\n            \n            if (fillEl) fillEl.style.width = progress + '%';\n            if (textEl && statusText) textEl.textContent = statusText;\n            if (percentEl) percentEl.textContent = Math.round(progress) + '%';\n            \n            // Move dino along the loading bar (from -10% to 85% of container width)\n            // AND grow from small (0.5x) to large (1.5x) as loading progresses\n            if (dinoEl) {\n                const dinoPos = -10 + (progress * 0.95);\n                dinoEl.style.left = dinoPos + '%';\n                \n                // Scale from 0.5x at 0% to 1.5x at 100%\n                const scale = 0.5 + (progress / 100);\n                dinoEl.style.transform = 'scale(' + scale + ')';\n                dinoEl.style.transformOrigin = 'center bottom'; // Grow from feet up\n            }\n        }\n\n        async function loadAudio(id) {\n            const assetInfo = lib.getAsset(id);\n            if (!assetInfo || assetInfo.type !== 'audio') {\n                assetsLoaded++;\n                return null;\n            }\n            try {\n                const response = await fetch(assetInfo.url);\n                const arrayBuffer = await response.arrayBuffer();\n                const ctx = getAudioContext();\n                const audioBuffer = await ctx.decodeAudioData(arrayBuffer);\n                assetCache.audio[id] = audioBuffer;\n                assetsLoaded++;\n                updateLoadingUI((assetsLoaded / totalAssetsToLoad) * 100, 'Loading ' + id.replace(/_/g, ' ') + '...');\n                return audioBuffer;\n            } catch (e) {\n                lib.log('Failed to load audio ' + id + ': ' + e);\n                assetsLoaded++;\n                return null;\n            }\n        }\n        \n        async function loadImage(id) {\n            const assetInfo = lib.getAsset(id);\n            if (!assetInfo || assetInfo.type === 'audio') {\n                assetsLoaded++;\n                return null;\n            }\n            return new Promise((resolve) => {\n                const img = new Image();\n                img.onload = () => {\n                    assetCache.images[id] = img;\n                    assetsLoaded++;\n                    updateLoadingUI((assetsLoaded / totalAssetsToLoad) * 100, 'Loading ' + id.replace(/_/g, ' ') + '...');\n                    resolve(img);\n                };\n                img.onerror = () => {\n                    lib.log('Failed to load image ' + id);\n                    assetsLoaded++;\n                    updateLoadingUI((assetsLoaded / totalAssetsToLoad) * 100, 'Loading...');\n                    resolve(null);\n                };\n                img.src = assetInfo.url;\n            });\n        }\n\n        // Audio throttling to prevent overlapping sounds\n        const audioThrottle = {};\n        const AUDIO_THROTTLE_WINDOW = 0.08;\n        const AUDIO_MAX_CONCURRENT = 3;\n\n        function playSound(id, volume = 0.5) {\n            const buffer = assetCache.audio[id];\n            if (!buffer) return;\n\n            const now = performance.now() / 1000;\n            if (!audioThrottle[id]) audioThrottle[id] = { lastPlayed: 0, activeCount: 0 };\n            const throttle = audioThrottle[id];\n\n            if (now - throttle.lastPlayed < AUDIO_THROTTLE_WINDOW) return;\n            if (throttle.activeCount >= AUDIO_MAX_CONCURRENT) return;\n\n            throttle.lastPlayed = now;\n            throttle.activeCount++;\n\n            try {\n                const ctx = getAudioContext();\n                if (ctx.state === 'suspended') ctx.resume();\n                const source = ctx.createBufferSource();\n                const gainNode = ctx.createGain();\n                source.buffer = buffer;\n                const concurrencyPenalty = 1 - (throttle.activeCount - 1) * 0.25;\n                gainNode.gain.value = volume * Math.max(0.4, concurrencyPenalty);\n                source.connect(gainNode);\n                gainNode.connect(ctx.destination);\n                source.onended = function() { throttle.activeCount = Math.max(0, throttle.activeCount - 1); };\n                source.start(0);\n                setTimeout(function() { throttle.activeCount = Math.max(0, throttle.activeCount - 1); }, (buffer.duration * 1000) + 100);\n            } catch (e) { /* silent */ }\n        }\n\n        // Track deferred loading state\n        let deferredAssetsReady = false;\n        \n        async function preloadCriticalAssets() {\n            // Only load what's needed for the start screen (3 images)\n            const criticalIds = ['start_screen_bg', 'title_logo', 'start_button'];\n            totalAssetsToLoad = criticalIds.length;\n            assetsLoaded = 0;\n            \n            updateLoadingUI(0, 'Loading...');\n            \n            await Promise.all(criticalIds.map(id => loadImage(id)));\n            \n            updateLoadingUI(100, 'Ready!');\n            await new Promise(r => setTimeout(r, 200));\n        }\n        \n        function loadDeferredAssets() {\n            // All gameplay assets - loaded in background while player is on start screen\n            const audioIds = [\n                'footstep_sound', 'building_hit_sound', 'building_collapse_sound',\n                'eating_sound', 'enemy_shoot_sound', 'enemy_explosion_sound',\n                'player_damage_sound', 'level_up_sound', 'mutation_select_sound',\n                'game_over_sound', 'title_music_epic', 'battlefield_music_epic', 'heart_pickup_sound', 'growth_pop_sound',\n                'tank_explosion_sound', 'helicopter_explosion_sound'\n            ];\n            \n            const imageIds = [\n                'subtitle_logo',\n                'icon_laser', 'icon_meteor', 'icon_jump', 'icon_tail', 'icon_dash',\n                'icon_growth', 'icon_shield', 'icon_frenzy', 'icon_stomp', 'icon_regen',\n                'monster_idle', 'monster_walking', 'monster_attacking', 'monster_eating',\n                'police_car', 'tank', 'helicopter',\n                'attack_button_icon', 'special_attack_icon',\n                'mutation_bigger_bite_icon', 'mutation_iron_skin_icon', 'mutation_fast_feet_icon',\n                'mutation_regeneration_icon', 'mutation_city_eater_icon', 'mutation_giant_growth_icon',\n                'mutation_thunder_stomp_icon', 'mutation_magnetic_hunger_icon',\n                'buff_icon_invincibility', 'buff_icon_speed'\n            ];\n            \n            const allDeferred = [...imageIds, ...audioIds];\n            let loaded = 0;\n            const total = allDeferred.length;\n            \n            // Load in parallel batches of 6 (fast but doesn't overwhelm browser)\n            const batchSize = 6;\n            let idx = 0;\n            \n            function loadNextBatch() {\n                if (idx >= allDeferred.length) {\n                    deferredAssetsReady = true;\n                    onDeferredAssetsReady();\n                    return;\n                }\n                const batch = allDeferred.slice(idx, idx + batchSize);\n                idx += batchSize;\n                \n                Promise.all(batch.map(id => {\n                    if (audioIds.includes(id)) {\n                        return loadAudioSilent(id);\n                    } else {\n                        return loadImageSilent(id);\n                    }\n                })).then(() => {\n                    loaded += batch.length;\n                    updateStartLoadingText(loaded, total);\n                    // Use setTimeout to yield to UI thread\n                    setTimeout(loadNextBatch, 10);\n                });\n            }\n            \n            loadNextBatch();\n        }\n        \n        // Silent loaders (no progress bar UI update - just cache the asset)\n        async function loadAudioSilent(id) {\n            const assetInfo = lib.getAsset(id);\n            if (!assetInfo || assetInfo.type !== 'audio') return null;\n            try {\n                const response = await fetch(assetInfo.url);\n                const arrayBuffer = await response.arrayBuffer();\n                const ctx = getAudioContext();\n                const audioBuffer = await ctx.decodeAudioData(arrayBuffer);\n                assetCache.audio[id] = audioBuffer;\n                return audioBuffer;\n            } catch (e) {\n                lib.log('Failed to load audio ' + id + ': ' + e);\n                return null;\n            }\n        }\n        \n        async function loadImageSilent(id) {\n            const assetInfo = lib.getAsset(id);\n            if (!assetInfo || assetInfo.type === 'audio') return null;\n            return new Promise((resolve) => {\n                const img = new Image();\n                img.onload = () => { assetCache.images[id] = img; resolve(img); };\n                img.onerror = () => { resolve(null); };\n                img.src = assetInfo.url;\n            });\n        }\n        \n        function updateStartLoadingText(loaded, total) {\n            const textEl = document.getElementById('start-loading-text');\n            const pct = Math.round((loaded / total) * 100);\n            if (textEl && !deferredAssetsReady) {\n                textEl.textContent = '\u23f3 Loading game assets... ' + pct + '%';\n            }\n        }\n        \n        function onDeferredAssetsReady() {\n            // Enable start button\n            const btnEl = document.getElementById('start-btn');\n            const textEl = document.getElementById('start-loading-text');\n            if (btnEl) btnEl.classList.remove('loading-assets');\n            if (textEl) textEl.classList.add('hidden');\n            \n            // Now that title music is loaded, try to play it\n            startMenuMusic();\n        }\n        \n        // Background music management\n        let bgMusicSource = null;\n        let bgMusicGain = null;\n        \n        function playBackgroundMusic(id, volume = 0.4, loop = true) {\n            stopBackgroundMusic();\n            const buffer = assetCache.audio[id];\n            if (!buffer) return;\n            try {\n                const ctx = getAudioContext();\n                if (ctx.state === 'suspended') ctx.resume();\n                bgMusicSource = ctx.createBufferSource();\n                bgMusicGain = ctx.createGain();\n                bgMusicSource.buffer = buffer;\n                bgMusicSource.loop = loop;\n                bgMusicGain.gain.value = volume;\n                bgMusicSource.connect(bgMusicGain);\n                bgMusicGain.connect(ctx.destination);\n                bgMusicSource.start(0);\n            } catch (e) { /* silent */ }\n        }\n        \n        function stopBackgroundMusic(fadeTime = 0.5) {\n            if (bgMusicGain && bgMusicSource) {\n                const oldGain = bgMusicGain;\n                const oldSource = bgMusicSource;\n                bgMusicGain = null;\n                bgMusicSource = null;\n                try {\n                    const ctx = getAudioContext();\n                    oldGain.gain.setValueAtTime(oldGain.gain.value, ctx.currentTime);\n                    oldGain.gain.linearRampToValueAtTime(0, ctx.currentTime + fadeTime);\n                    setTimeout(() => {\n                        try { oldSource.stop(); } catch (e) {}\n                        try { oldGain.disconnect(); } catch (e) {}\n                    }, fadeTime * 1000 + 50);\n                } catch (e) {\n                    try { oldSource.stop(); } catch (err) {}\n                }\n            }\n        }\n\n        // ==================== THREE.JS SETUP ====================\n        const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });\n        renderer.shadowMap.enabled = true;\n        renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n        renderer.outputEncoding = THREE.sRGBEncoding;\n        // Using NoToneMapping avoids washing out colors\n        renderer.toneMapping = THREE.NoToneMapping;\n        renderer.sortObjects = false; // Skip per-frame sort \u2014 our scene has minimal transparency overlap\n\n        const QUALITY_LEVEL_COUNT = 5;\n        function clampQualityLevel(level) {\n            const value = Math.floor(Number(level));\n            if (!Number.isFinite(value)) return 0;\n            return Math.max(0, Math.min(QUALITY_LEVEL_COUNT - 1, value));\n        }\n\n        function getQualitySpec(level) {\n            const dpr = window.devicePixelRatio || 1;\n            const levels = [\n                { pixelRatio: Math.min(dpr, 2.0), shadows: true, shadowMap: 1024 },\n                { pixelRatio: Math.min(dpr, 1.5), shadows: true, shadowMap: 1024 },\n                { pixelRatio: Math.min(dpr, 1.15), shadows: true, shadowMap: 512 },\n                { pixelRatio: Math.min(dpr, 1.0), shadows: true, shadowMap: 512 },\n                { pixelRatio: Math.min(dpr, 0.85), shadows: false, shadowMap: 512 }\n            ];\n            return levels[clampQualityLevel(level)];\n        }\n\n        function clampQualityMode(mode) {\n            const value = Math.floor(Number(mode));\n            if (!Number.isFinite(value)) return 0;\n            return Math.max(0, Math.min(QUALITY_LEVEL_COUNT, value));\n        }\n\n        function ensureGraphicsConfig() {\n            if (!window.gameConfig.graphics) window.gameConfig.graphics = {};\n            return window.gameConfig.graphics;\n        }\n\n        let qualityMode = clampQualityMode(config.graphics.qualityMode);\n        let qualityPinned = qualityMode !== 0;\n        let activeQualityLevel = qualityPinned ? clampQualityLevel(qualityMode - 1) : 3;\n        let appliedShadowMapSize = 0;\n        let perfAccum = 0;\n        let perfFrames = 0;\n        let perfCooldown = 0;\n        let perfGoodWindows = 0;\n        let directionalLight = null;\n\n        function resetPerfGovernor() {\n            perfAccum = 0;\n            perfFrames = 0;\n            perfCooldown = 1.5;\n            perfGoodWindows = 0;\n        }\n\n        function applyQualityLevel(level) {\n            activeQualityLevel = clampQualityLevel(level);\n            const quality = getQualitySpec(activeQualityLevel);\n            renderer.setPixelRatio(quality.pixelRatio);\n            renderer.shadowMap.enabled = quality.shadows;\n            if (directionalLight) {\n                directionalLight.castShadow = quality.shadows;\n                if (appliedShadowMapSize !== quality.shadowMap) {\n                    directionalLight.shadow.mapSize.set(quality.shadowMap, quality.shadowMap);\n                    if (directionalLight.shadow.map) {\n                        directionalLight.shadow.map.dispose();\n                        directionalLight.shadow.map = null;\n                    }\n                    appliedShadowMapSize = quality.shadowMap;\n                }\n                directionalLight.shadow.needsUpdate = true;\n            }\n            resizeRenderer();\n        }\n\n        function setQualityMode(modeValue) {\n            qualityMode = clampQualityMode(modeValue);\n            config.graphics.qualityMode = qualityMode;\n            ensureGraphicsConfig().qualityMode = qualityMode;\n            qualityPinned = qualityMode !== 0;\n            applyQualityLevel(qualityPinned ? qualityMode - 1 : activeQualityLevel);\n            resetPerfGovernor();\n        }\n\n        function updateAdaptiveQuality(rawFrameMs) {\n            if (qualityPinned || rawFrameMs <= 0 || rawFrameMs > 250) {\n                resetPerfGovernor();\n                return;\n            }\n\n            const frameSeconds = rawFrameMs / 1000;\n            perfCooldown = Math.max(0, perfCooldown - frameSeconds);\n            perfAccum += rawFrameMs;\n            perfFrames++;\n\n            if (perfAccum < 1200) return;\n\n            const fps = (perfFrames * 1000) / perfAccum;\n            perfAccum = 0;\n            perfFrames = 0;\n\n            if (perfCooldown > 0) return;\n\n            const downgradeFps = Math.max(15, Number(config.graphics.qualityDowngradeFps) || 48);\n            const upgradeFps = Math.max(downgradeFps + 1, Number(config.graphics.qualityUpgradeFps) || 57);\n\n            if (fps < downgradeFps && activeQualityLevel < QUALITY_LEVEL_COUNT - 1) {\n                applyQualityLevel(activeQualityLevel + 1);\n                perfCooldown = 2;\n                perfGoodWindows = 0;\n            } else if (fps > upgradeFps && activeQualityLevel > 0) {\n                perfGoodWindows++;\n                if (perfGoodWindows >= 4) {\n                    applyQualityLevel(activeQualityLevel - 1);\n                    perfCooldown = 3;\n                    perfGoodWindows = 0;\n                }\n            } else {\n                perfGoodWindows = 0;\n            }\n        }\n\n        // ---- procedural canvas texture helper ----\n        function makeCanvasTexture(w, h, draw) {\n            const c = document.createElement('canvas');\n            c.width = w; c.height = h;\n            const ctx = c.getContext('2d');\n            if (!ctx) {\n                lib.log('Warning: Failed to get 2D context for texture');\n                return null;\n            }\n            draw(ctx, w, h);\n            const tex = new THREE.CanvasTexture(c);\n            tex.encoding = THREE.sRGBEncoding;\n            tex.anisotropy = 4;\n            tex.needsUpdate = true; // Ensure texture uploads to GPU immediately\n            return tex;\n        }\n\n        function makeSkyTexture() {\n            return makeCanvasTexture(16, 256, (ctx, w, h) => {\n                const g = ctx.createLinearGradient(0, 0, 0, h);\n                g.addColorStop(0.0, '#1e5090');\n                g.addColorStop(0.35, '#4090d0');\n                g.addColorStop(0.65, '#70c0e8');\n                g.addColorStop(0.85, '#a0ddf0');\n                g.addColorStop(1.0, '#f0e8c0');\n                ctx.fillStyle = g;\n                ctx.fillRect(0, 0, w, h);\n            });\n        }\n\n        const scene = new THREE.Scene();\n        scene.background = makeSkyTexture();\n        scene.fog = new THREE.Fog(0x88c4e8, 300, 600);\n\n        const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 1400);\n\n        function resizeRenderer() {\n            const width = gameWorld.clientWidth;\n            const height = gameWorld.clientHeight;\n            renderer.setSize(width, height);\n            camera.aspect = width / height;\n            // Widen FOV for tall portrait screens so enough city is visible.\n            camera.fov = camera.aspect < 0.75 ? 64 : 55;\n            camera.updateProjectionMatrix();\n        }\n        applyQualityLevel(activeQualityLevel);\n        window.addEventListener('resize', () => applyQualityLevel(activeQualityLevel));\n\n        const hemiLight = new THREE.HemisphereLight(0xffffff, 0x555555, 0.4);\n        scene.add(hemiLight);\n\n        const ambientLight = new THREE.AmbientLight(0xffffff, 0.25);\n        scene.add(ambientLight);\n\n        directionalLight = new THREE.DirectionalLight(0xffffff, 0.85);\n        directionalLight.position.set(70, 130, 50);\n        directionalLight.castShadow = true;\n        directionalLight.shadow.mapSize.width = 1024; // Reduced for performance\n        directionalLight.shadow.mapSize.height = 1024;\n        directionalLight.shadow.camera.near = 10;\n        directionalLight.shadow.camera.far = 400;\n        directionalLight.shadow.camera.left = -180;\n        directionalLight.shadow.camera.right = 180;\n        directionalLight.shadow.camera.top = 180;\n        directionalLight.shadow.camera.bottom = -180;\n        directionalLight.shadow.bias = -0.0004;\n        scene.add(directionalLight);\n        applyQualityLevel(activeQualityLevel);\n        \n        // Light target follows player\n        const lightTarget = new THREE.Object3D();\n        scene.add(lightTarget);\n        directionalLight.target = lightTarget;\n\n        // Cool fill from the opposite side to soften shadowed faces.\n        const fillLight = new THREE.DirectionalLight(0x99ccff, 0.25);\n        fillLight.position.set(-60, 70, -50);\n        scene.add(fillLight);\n\n        // ==================== GAME STATE ====================\n        let gameState = newGameState();\n        function newGameState() {\n            return {\n                running: false,\n                paused: false,\n                score: 0,\n                xp: 0,\n                xpToLevel: 150,\n                level: 1,\n                timeElapsed: 0,\n                buildingsDestroyed: 0,\n                enemiesDefeated: 0,\n                mutations: []\n            };\n        }\n\n        let player = newPlayer();\n        function newPlayer() {\n            return {\n                x: 0, z: 0, y: 0,\n                vx: 0, vz: 0,\n                rotation: Math.PI,\n                health: 100,\n                maxHealth: 100,\n                baseSpeed: 14,\n                speedMul: 1,\n                size: STARTING_SIZE,\n                targetSize: STARTING_SIZE,\n                mass: 0,\n                massGain: 1,\n                crushBonus: 1,\n                damageReduction: 0,\n                xpMultiplier: config.gameplay.xpGainRate,\n                scoreMultiplier: 1,\n                regenRate: 0,\n                walkPhase: 0,\n                gait: 0,\n                bodyDip: 0,\n                upgrades: {},\n                abilityCd: {},\n                cooldownMul: 1,\n                jumping: false,\n                jumpPhase: 'idle',\n                jumpT: 0,\n                jumpDur: 0,\n                jumpPrepDur: 0,\n                jumpRecoverDur: 0,\n                jumpRecoverT: 0,\n                jumpData: null,\n                _prevMaxFromSize: 0,\n                spinning: false,\n                spinT: 0,\n                spinDur: 0,\n                spinData: null,\n                spinStartRot: 0,\n                dashing: false,\n                dashT: 0,\n                dashDur: 0,\n                dashData: null\n            };\n        }\n\n        let cameraShake = { x: 0, y: 0, intensity: 0 };\n        let cameraTarget = { x: 0, z: 0 };\n        let camCurDist = 0, camCurHeight = 0;   // smoothed camera offset (0 = snap to target on first frame)\n        let zoomLevel = 1; // 0 = zoomed in, 1 = zoomed out (set to max zoom out for best view)\n        \n        // Monster hit flash effect\n        let monsterHitFlash = 0; // Timer for hit flash effect\n        const MONSTER_FLASH_DURATION = 0.18; // How long the flash lasts\n        \n        // Stepped growth animation (3 frames with camera zoom)\n        let growthAnimActive = false;\n        let growthAnimTimer = 0;\n        const GROWTH_ANIM_DURATION = 1.4; // Total duration for stepped growth\n        let growthAnimStartSize = 0;\n        let growthAnimTargetSize = 0;\n        let growthCameraZoomOffset = 0; // Camera zoom offset during animation\n        \n        // Growth visual effects\n        let growthAuraMesh = null;\n        let growthAuraInnerMesh = null;\n        let growthShockwaveStage = 0; // Track which shockwaves we've spawned\n        \n        function smoothstep(t) {\n            return t * t * (3 - 2 * t);\n        }\n        \n        function startGrowthAnimation(fromSize, toSize) {\n            growthAnimActive = true;\n            growthAnimTimer = 0;\n            growthAnimStartSize = fromSize;\n            growthAnimTargetSize = toSize;\n            growthShockwaveStage = 0;\n            \n            // Create glowing aura around monster\n            createGrowthAura();\n        }\n        \n        function createGrowthAura() {\n            // Remove existing aura if any\n            removeGrowthAura();\n            \n            // Outer pulsing aura (golden glow)\n            const auraMat = new THREE.MeshBasicMaterial({\n                color: 0xffd700,\n                transparent: true,\n                opacity: 0.0,\n                side: THREE.BackSide\n            });\n            growthAuraMesh = new THREE.Mesh(\n                new THREE.SphereGeometry(1, 16, 12),\n                auraMat\n            );\n            growthAuraMesh.scale.set(20, 25, 20);\n            scene.add(growthAuraMesh);\n            \n            // Inner brighter core aura\n            const innerMat = new THREE.MeshBasicMaterial({\n                color: 0xffffaa,\n                transparent: true,\n                opacity: 0.0,\n                side: THREE.BackSide\n            });\n            growthAuraInnerMesh = new THREE.Mesh(\n                new THREE.SphereGeometry(1, 12, 10),\n                innerMat\n            );\n            growthAuraInnerMesh.scale.set(15, 20, 15);\n            scene.add(growthAuraInnerMesh);\n        }\n        \n        function removeGrowthAura() {\n            if (growthAuraMesh) {\n                scene.remove(growthAuraMesh);\n                if (growthAuraMesh.material) growthAuraMesh.material.dispose();\n                if (growthAuraMesh.geometry) growthAuraMesh.geometry.dispose();\n                growthAuraMesh = null;\n            }\n            if (growthAuraInnerMesh) {\n                scene.remove(growthAuraInnerMesh);\n                if (growthAuraInnerMesh.material) growthAuraInnerMesh.material.dispose();\n                if (growthAuraInnerMesh.geometry) growthAuraInnerMesh.geometry.dispose();\n                growthAuraInnerMesh = null;\n            }\n        }\n        \n        function addGrowthShockwave(intensity) {\n            // Golden expanding ring on the ground\n            const radius = 8 + player.size * 6;\n            const mat = new THREE.MeshBasicMaterial({ \n                color: 0xffd700, \n                transparent: true, \n                opacity: 0.7, \n                side: THREE.DoubleSide \n            });\n            const ring = new THREE.Mesh(new THREE.RingGeometry(2, 4, 32), mat);\n            ring.rotation.x = -Math.PI / 2;\n            ring.position.set(player.x, 0.5, player.z);\n            scene.add(ring);\n            particles.push({ \n                mesh: ring, vx: 0, vy: 0, vz: 0, \n                lifetime: 0.6, \n                isRing: true, \n                ringMax: radius * intensity, \n                ringAge: 0, \n                ringMat: mat \n            });\n            \n            // Secondary white inner ring for extra punch\n            const mat2 = new THREE.MeshBasicMaterial({ \n                color: 0xffffff, \n                transparent: true, \n                opacity: 0.5, \n                side: THREE.DoubleSide \n            });\n            const ring2 = new THREE.Mesh(new THREE.RingGeometry(1, 2.5, 24), mat2);\n            ring2.rotation.x = -Math.PI / 2;\n            ring2.position.set(player.x, 0.6, player.z);\n            scene.add(ring2);\n            particles.push({ \n                mesh: ring2, vx: 0, vy: 0, vz: 0, \n                lifetime: 0.45, \n                isRing: true, \n                ringMax: radius * intensity * 0.7, \n                ringAge: 0, \n                ringMat: mat2 \n            });\n        }\n        \n        function updateGrowthAnimation(dt) {\n            if (!growthAnimActive) {\n                // Smoothly return camera zoom to normal when not animating\n                growthCameraZoomOffset += (0 - growthCameraZoomOffset) * Math.min(1, dt * 4);\n                if (Math.abs(growthCameraZoomOffset) < 0.01) growthCameraZoomOffset = 0;\n                \n                // Fade out aura if it exists\n                if (growthAuraMesh && growthAuraMesh.material.opacity > 0) {\n                    growthAuraMesh.material.opacity -= dt * 3;\n                    growthAuraInnerMesh.material.opacity -= dt * 3;\n                    if (growthAuraMesh.material.opacity <= 0) {\n                        removeGrowthAura();\n                    }\n                }\n                return;\n            }\n            \n            growthAnimTimer += dt;\n            const t = Math.min(1, growthAnimTimer / GROWTH_ANIM_DURATION);\n            \n            const sizeDiff = growthAnimTargetSize - growthAnimStartSize;\n            \n            // Three stepped stages with smooth transitions between them\n            let sizeProgress, cameraZoom, auraIntensity;\n            \n            if (t < 0.2) {\n                // Stage 1: Grow to 33% of new size, camera zooms in to 35%\n                const stageT = t / 0.2;\n                const eased = smoothstep(stageT);\n                sizeProgress = eased * 0.33;\n                cameraZoom = eased * 0.35;\n                auraIntensity = eased * 0.5;\n                \n                // First shockwave at start of growth\n                if (growthShockwaveStage === 0 && stageT > 0.1) {\n                    addGrowthShockwave(0.8);\n                    growthShockwaveStage = 1;\n                    addCameraShake(0.15);\n                    playSound('growth_pop_sound', 0.5);\n                }\n            } else if (t < 0.25) {\n                // Brief hold at stage 1\n                sizeProgress = 0.33;\n                cameraZoom = 0.35;\n                auraIntensity = 0.5 + Math.sin(t * 40) * 0.15; // Pulsing\n            } else if (t < 0.45) {\n                // Stage 2: Grow to 66%, camera zooms in to 65%\n                const stageT = (t - 0.25) / 0.2;\n                const eased = smoothstep(stageT);\n                sizeProgress = 0.33 + eased * 0.33;\n                cameraZoom = 0.35 + eased * 0.3;\n                auraIntensity = 0.5 + eased * 0.25;\n                \n                // Second shockwave\n                if (growthShockwaveStage === 1 && stageT > 0.1) {\n                    addGrowthShockwave(1.0);\n                    growthShockwaveStage = 2;\n                    addCameraShake(0.2);\n                    playSound('growth_pop_sound', 0.6);\n                }\n            } else if (t < 0.5) {\n                // Brief hold at stage 2\n                sizeProgress = 0.66;\n                cameraZoom = 0.65;\n                auraIntensity = 0.75 + Math.sin(t * 50) * 0.15; // Pulsing brighter\n            } else if (t < 0.7) {\n                // Stage 3: Grow to 100%, camera zooms in to max\n                const stageT = (t - 0.5) / 0.2;\n                const eased = smoothstep(stageT);\n                sizeProgress = 0.66 + eased * 0.34;\n                cameraZoom = 0.65 + eased * 0.35;\n                auraIntensity = 0.75 + eased * 0.25;\n                \n                // Final big shockwave\n                if (growthShockwaveStage === 2 && stageT > 0.1) {\n                    addGrowthShockwave(1.4);\n                    growthShockwaveStage = 3;\n                    addCameraShake(0.35);\n                    playSound('growth_pop_sound', 0.75);\n                    // Extra debris burst for the final growth\n                    for (let i = 0; i < 8; i++) {\n                        const angle = (i / 8) * Math.PI * 2;\n                        const dist = player.size * 8;\n                        createDebris(\n                            player.x + Math.cos(angle) * dist,\n                            3 + Math.random() * 5,\n                            player.z + Math.sin(angle) * dist,\n                            0xffd700\n                        );\n                    }\n                }\n            } else if (t < 0.75) {\n                // Brief hold at final size with max zoom\n                sizeProgress = 1;\n                cameraZoom = 1;\n                auraIntensity = 1.0; // Maximum glow\n            } else {\n                // Stage 4: Hold size, camera smoothly zooms back out\n                const stageT = (t - 0.75) / 0.25;\n                const eased = smoothstep(stageT);\n                sizeProgress = 1;\n                cameraZoom = 1 - eased;\n                auraIntensity = 1.0 - eased; // Fade out aura\n            }\n            \n            // Apply stepped size directly (bypassing normal size lerp)\n            player.size = growthAnimStartSize + sizeDiff * sizeProgress;\n            \n            // Update aura position and intensity\n            if (growthAuraMesh) {\n                const auraScale = player.size * 12 + 5;\n                growthAuraMesh.position.set(player.x, player.y + player.size * 8, player.z);\n                growthAuraMesh.scale.set(auraScale, auraScale * 1.3, auraScale);\n                growthAuraMesh.material.opacity = auraIntensity * 0.4;\n                \n                // Pulsing effect\n                const pulse = 1 + Math.sin(growthAnimTimer * 15) * 0.08;\n                growthAuraMesh.scale.multiplyScalar(pulse);\n                \n                // Rotate aura slowly for visual interest\n                growthAuraMesh.rotation.y += dt * 2;\n            }\n            if (growthAuraInnerMesh) {\n                const innerScale = player.size * 9 + 3;\n                growthAuraInnerMesh.position.set(player.x, player.y + player.size * 8, player.z);\n                growthAuraInnerMesh.scale.set(innerScale, innerScale * 1.2, innerScale);\n                growthAuraInnerMesh.material.opacity = auraIntensity * 0.25;\n                \n                // Counter-rotate inner aura\n                growthAuraInnerMesh.rotation.y -= dt * 3;\n            }\n            \n            // Negative offset = zoom in (closer camera)\n            growthCameraZoomOffset = -cameraZoom * 0.35;\n            \n            if (t >= 1) {\n                growthAnimActive = false;\n                player.size = growthAnimTargetSize;\n                player.targetSize = growthAnimTargetSize; // Sync target\n                // Aura will fade out in the non-active branch\n            }\n        }\n\n        // Input\n        const keys = {};\n        const joystick = { active: false, x: 0, y: 0, touchId: null, baseX: 0, baseY: 0 };\n\n        // Pools\n        const buildings = [];\n        const props = [];      // cars + trees\n        const people = [];\n        const enemies = [];\n        const projectiles = [];\n        const particles = [];\n        const squashed = [];   // people mid-flatten animation\n        const meteors = [];    // falling meteors (Meteor Shower power)\n        const activeLasers = []; // persistent laser beams\n        const heartPickups = []; // health pickup items\n        const invincibilityPickups = []; // invincibility pickup items\n        const speedPickups = []; // Kept for compatibility but combined with invincibility\n        \n        // Active buff timers\n        let invincibilityTimer = 0;\n        let speedBoostTimer = 0;\n        const BUFF_DURATION = 10; // 10 seconds for combined star power-up\n\n        let combo = 0;\n        let comboTimer = 0;\n\n        // Tutorial state\n        let tutorialActive = false;\n        let tutorialBuilding = null;\n        let tutorialTimer = 0;\n        \n        // User profile (persistent save state)\n        let userProfile = { tutorialSeen: false };\n\n        // Spawn / difficulty timers\n        let tankSpawnTimer = 0;\n        let heliSpawnTimer = 0;\n        let difficultyTimer = 0;\n        let spawnRateMultiplier = 1;\n        let damageMultiplier = 1;\n\n        const PEOPLE_TARGET = 350; // Large crowds with instanced optimization\n        const CAR_TARGET = 22;\n\n        // ---- Building facade textures (windows) + styles ----\n        function makeFacadeTexture(base, glass, lit) {\n            return makeCanvasTexture(256, 256, (ctx, w, h) => {\n                ctx.fillStyle = base;\n                ctx.fillRect(0, 0, w, h);\n                const cols = 4, rows = 4, cw = w / cols, ch = h / rows;\n                for (let r = 0; r < rows; r++) {\n                    for (let c = 0; c < cols; c++) {\n                        const x = c * cw, y = r * ch;\n                        const mx = cw * 0.2, my = ch * 0.16;\n                        ctx.fillStyle = Math.random() < 0.32 ? lit : glass;\n                        ctx.fillRect(x + mx, y + my, cw - 2 * mx, ch - 2 * my);\n                        ctx.fillStyle = 'rgba(255,255,255,0.10)';\n                        ctx.fillRect(x + mx, y + my, cw - 2 * mx, (ch - 2 * my) * 0.28);\n                        ctx.fillStyle = 'rgba(0,0,0,0.18)';\n                        ctx.fillRect(x + mx, y + ch - my - (ch - 2 * my) * 0.14, cw - 2 * mx, (ch - 2 * my) * 0.14);\n                    }\n                }\n            });\n        }\n\n        const buildingStyleDefs = [\n            { base: '#4a6080', glass: '#2a4060', lit: '#ffdd66', roof: '#3a4a60' },\n            { base: '#e07050', glass: '#804030', lit: '#ffee88', roof: '#aa5040' },\n            { base: '#40a090', glass: '#206050', lit: '#ffee77', roof: '#308070' },\n            { base: '#d06040', glass: '#803828', lit: '#ffdd77', roof: '#a04830' },\n            { base: '#e0c060', glass: '#907030', lit: '#ffffaa', roof: '#b09040' },\n            { base: '#7090b0', glass: '#405060', lit: '#ffee88', roof: '#506880' },\n            { base: '#50a060', glass: '#306040', lit: '#ffee88', roof: '#408050' },\n            { base: '#8060a0', glass: '#503870', lit: '#ffeebb', roof: '#604880' }\n        ];\n        const buildingStyles = buildingStyleDefs.map(s => {\n            const tex = makeFacadeTexture(s.base, s.glass, s.lit);\n            tex.wrapS = THREE.RepeatWrapping;\n            tex.wrapT = THREE.RepeatWrapping;\n            return {\n                facadeMat: new THREE.MeshLambertMaterial({ map: tex }),\n                roofMat: new THREE.MeshLambertMaterial({ color: s.roof }),\n                debrisColor: parseInt(s.base.slice(1), 16)\n            };\n        });\n        const rooftopMaterial = new THREE.MeshLambertMaterial({ color: 0x3a3f46 });\n\n        // ---- Other shared materials ----\n        const carBodyColors = [0xd84b3a, 0x3a6fd8, 0xf1c40f, 0x2ecc71, 0xecf0f1, 0xe67e22, 0x8e44ad, 0x16a085];\n        const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x5a9050 });\n        const treeTrunkMaterial = new THREE.MeshLambertMaterial({ color: 0x8a5530 });\n        const treeCanopyMaterial = new THREE.MeshLambertMaterial({ color: 0x30b050, flatShading: true });\n        const treeCanopyMaterial2 = new THREE.MeshLambertMaterial({ color: 0x20a040, flatShading: true });\n        const tankMaterial = new THREE.MeshLambertMaterial({ color: 0x4a553a, flatShading: true });\n        const tankDarkMaterial = new THREE.MeshLambertMaterial({ color: 0x333b29 });\n        const heliMaterial = new THREE.MeshLambertMaterial({ color: 0x32402c, flatShading: true });\n        const heliGlassMaterial = new THREE.MeshLambertMaterial({ color: 0x223044 });\n        const spotlightMaterial = new THREE.MeshBasicMaterial({ color: 0xff3030, transparent: true, opacity: 0.35, side: THREE.DoubleSide });\n        const enemyGlowMaterial = new THREE.MeshBasicMaterial({ color: 0xff3030, transparent: true, opacity: 0.35, side: THREE.BackSide });\n        const rotorMaterial = new THREE.MeshBasicMaterial({ color: 0x1c1c1c });\n        const carGlassMaterial = new THREE.MeshLambertMaterial({ color: 0x1f2a38 });\n        const wheelMaterial = new THREE.MeshLambertMaterial({ color: 0x1a1a1a });\n        const policeBodyMaterial = new THREE.MeshLambertMaterial({ color: 0x1a3a6e, flatShading: true });\n        const policeWhiteMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });\n        const policeLightRedMaterial = new THREE.MeshBasicMaterial({ color: 0xff2222 });\n        const policeLightBlueMaterial = new THREE.MeshBasicMaterial({ color: 0x2266ff });\n        const projectileMaterial = new THREE.MeshBasicMaterial({ color: 0xff2222 });\n        const laserMaterial = new THREE.MeshBasicMaterial({ color: 0xff4030, transparent: true, opacity: 0.85 });\n        const coneMaterial = new THREE.MeshBasicMaterial({ color: 0xfff1a8, transparent: true, opacity: 0.5, side: THREE.DoubleSide });\n        const meteorMaterial = new THREE.MeshBasicMaterial({ color: 0xff7a2a });\n        const heartMaterial = new THREE.MeshLambertMaterial({ color: 0xff4466, emissive: 0xff2244, emissiveIntensity: 0.3 });\n        const heartGlowMaterial = new THREE.MeshBasicMaterial({ color: 0xff6688, transparent: true, opacity: 0.4, side: THREE.BackSide });\n        const heartBeamMaterial = new THREE.MeshBasicMaterial({ color: 0xff1493, transparent: true, opacity: 0.5, side: THREE.DoubleSide });\n        const heartBeamCoreMaterial = new THREE.MeshBasicMaterial({ color: 0xff69b4, transparent: true, opacity: 0.7 });\n        \n        // Invincibility pickup materials (golden star)\n        const invincibilityMaterial = new THREE.MeshLambertMaterial({ color: 0xffd700, emissive: 0xffaa00, emissiveIntensity: 0.5 });\n        const invincibilityGlowMaterial = new THREE.MeshBasicMaterial({ color: 0xffee88, transparent: true, opacity: 0.5, side: THREE.BackSide });\n        const invincibilityBeamMaterial = new THREE.MeshBasicMaterial({ color: 0xffdd00, transparent: true, opacity: 0.5, side: THREE.DoubleSide });\n        \n        // Speed pickup materials (green lightning)\n        const speedMaterial = new THREE.MeshLambertMaterial({ color: 0x00ff88, emissive: 0x00cc66, emissiveIntensity: 0.4 });\n        const speedGlowMaterial = new THREE.MeshBasicMaterial({ color: 0x88ffaa, transparent: true, opacity: 0.5, side: THREE.BackSide });\n        const speedBeamMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff66, transparent: true, opacity: 0.5, side: THREE.DoubleSide });\n        const skinColors = [0xf2c79b, 0xd9a066, 0x8d5524, 0xffdbac, 0xc68642];\n        const clothColors = [0xe74c3c, 0x3498db, 0x2ecc71, 0xf1c40f, 0x9b59b6, 0xff6b9d, 0xecf0f1, 0xe67e22];\n        const legColors = [0x2c3e50, 0x34495e, 0x4a4a4a, 0x5d4037];\n\n        const persistentSharedGeometries = new Set();\n        function registerSharedGeometry(geo) {\n            persistentSharedGeometries.add(geo);\n            return geo;\n        }\n\n        // Shared geometries for people (massive performance boost)\n        const personTorsoGeo = registerSharedGeometry(new THREE.BoxGeometry(0.55, 0.75, 0.38));\n        const personHeadGeo = registerSharedGeometry(new THREE.BoxGeometry(0.42, 0.42, 0.42));\n        const personLegGeo = registerSharedGeometry(new THREE.BoxGeometry(0.22, 0.8, 0.26));\n        const sharedWheelGeo = registerSharedGeometry(new THREE.CylinderGeometry(0.5, 0.5, 0.5, 6));\n        \n        // Pre-create material cache for people colors\n        const clothMaterialCache = {};\n        const skinMaterialCache = {};\n        const legMaterialCache = {};\n        \n        function getClothMaterial(color) {\n            if (!clothMaterialCache[color]) {\n                clothMaterialCache[color] = new THREE.MeshLambertMaterial({ color });\n                persistentSharedMaterials.add(clothMaterialCache[color]);\n            }\n            return clothMaterialCache[color];\n        }\n        function getSkinMaterial(color) {\n            if (!skinMaterialCache[color]) {\n                skinMaterialCache[color] = new THREE.MeshLambertMaterial({ color });\n                persistentSharedMaterials.add(skinMaterialCache[color]);\n            }\n            return skinMaterialCache[color];\n        }\n        function getLegMaterial(color) {\n            if (!legMaterialCache[color]) {\n                legMaterialCache[color] = new THREE.MeshLambertMaterial({ color });\n                persistentSharedMaterials.add(legMaterialCache[color]);\n            }\n            return legMaterialCache[color];\n        }\n\n        const monsterBodyMaterial = new THREE.MeshLambertMaterial({ color: config.monster.primaryColor, flatShading: true });\n        const monsterBackMaterial = new THREE.MeshLambertMaterial({ color: shadeColor(config.monster.primaryColor, -0.22), flatShading: true });\n        const monsterBellyMaterial = new THREE.MeshLambertMaterial({ color: config.monster.secondaryColor, flatShading: true });\n        const monsterEyeWhiteMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });\n        const monsterPupilMaterial = new THREE.MeshBasicMaterial({ color: 0x141414 });\n        const monsterToothMaterial = new THREE.MeshLambertMaterial({ color: 0xf4efe0 });\n        const monsterClawMaterial = new THREE.MeshLambertMaterial({ color: 0xe9e2cf });\n        const monsterSpikeMaterial = new THREE.MeshLambertMaterial({ color: shadeColor(config.monster.secondaryColor, 0.08), flatShading: true, side: THREE.DoubleSide });\n        const monsterMouthMaterial = new THREE.MeshBasicMaterial({ color: 0x5a1f1f });\n\n        // Lighten/darken a hex/'#rrggbb' color by amount (-1..1).\n        function shadeColor(col, amt) {\n            const c = new THREE.Color(col);\n            if (amt >= 0) { c.r += (1 - c.r) * amt; c.g += (1 - c.g) * amt; c.b += (1 - c.b) * amt; }\n            else { c.r *= (1 + amt); c.g *= (1 + amt); c.b *= (1 + amt); }\n            return c.getHex();\n        }\n\n        // Shared resource tracking (prevents disposing materials still in use).\n        const persistentSharedMaterials = new Set([\n            groundMaterial, treeTrunkMaterial, treeCanopyMaterial, treeCanopyMaterial2,\n            tankMaterial, tankDarkMaterial, heliMaterial, heliGlassMaterial, rotorMaterial,\n            carGlassMaterial, wheelMaterial, policeBodyMaterial, policeWhiteMaterial,\n            policeLightRedMaterial, policeLightBlueMaterial, projectileMaterial, laserMaterial,\n            coneMaterial, meteorMaterial, rooftopMaterial,\n            monsterBodyMaterial, monsterBackMaterial, monsterBellyMaterial,\n            monsterEyeWhiteMaterial, monsterPupilMaterial, monsterToothMaterial,\n            monsterClawMaterial, monsterSpikeMaterial, monsterMouthMaterial\n        ]);\n        buildingStyles.forEach(s => {\n            persistentSharedMaterials.add(s.facadeMat);\n            persistentSharedMaterials.add(s.roofMat);\n        });\n\n        function registerSharedMaterial(mat) {\n            persistentSharedMaterials.add(mat);\n            return mat;\n        }\n\n        function removeFromScene(obj) {\n            if (!obj) return;\n            scene.remove(obj);\n            const seenGeo = new Set();\n            const seenMat = new Set();\n            obj.traverse(node => {\n                if (node.geometry || node.material) {\n                    const geo = node.geometry;\n                    if (geo && !persistentSharedGeometries.has(geo) && !seenGeo.has(geo)) {\n                        seenGeo.add(geo);\n                        geo.dispose();\n                    }\n\n                    const mats = Array.isArray(node.material) ? node.material : [node.material];\n                    mats.forEach(mat => {\n                        if (!mat || persistentSharedMaterials.has(mat) || seenMat.has(mat)) return;\n                        seenMat.add(mat);\n                        if (mat.map && mat.map.isTexture) mat.map.dispose();\n                        mat.dispose();\n                    });\n                }\n            });\n        }\n\n        // Raycaster for camera occlusion checks\n        const occlusionRaycaster = new THREE.Raycaster();\n        function updateCameraOcclusion() {\n            if (!camera || !player || !gameState.running) return;\n\n            const camPos = camera.position;\n            const targetPos = new THREE.Vector3(player.x, player.y + player.size * 5, player.z);\n            const dir = new THREE.Vector3().subVectors(targetPos, camPos);\n            const dist = dir.length();\n            dir.normalize();\n\n            occlusionRaycaster.set(camPos, dir);\n\n            const activeBuildings = buildings.filter(b => !b.dead);\n            const meshesToIntersect = activeBuildings.map(b => b.mesh);\n\n            const intersects = occlusionRaycaster.intersectObjects(meshesToIntersect, true);\n\n            const occludingMeshIds = new Set();\n            intersects.forEach(hit => {\n                let obj = hit.object;\n                while (obj && obj.parent && !meshesToIntersect.includes(obj)) {\n                    obj = obj.parent;\n                }\n                if (obj && hit.distance < dist - 2) {\n                    occludingMeshIds.add(obj.uuid);\n                }\n            });\n\n            activeBuildings.forEach(b => {\n                const isBlocked = occludingMeshIds.has(b.mesh.uuid);\n\n                if (isBlocked) {\n                    if (!b.origMaterials) b.origMaterials = b.mesh.material;\n\n                    if (!b.transMaterials) {\n                        const matsArray = Array.isArray(b.origMaterials) ? b.origMaterials : [b.origMaterials];\n                        const transArray = matsArray.map(mat => {\n                            const cloned = mat.clone();\n                            cloned.transparent = true;\n                            cloned.opacity = 0.22;\n                            return cloned;\n                        });\n                        b.transMaterials = Array.isArray(b.origMaterials) ? transArray : transArray[0];\n                    }\n\n                    b.mesh.material = b.transMaterials;\n\n                    b.mesh.traverse(node => {\n                        if (node.isMesh && node !== b.mesh) {\n                            if (!node.userData.origMaterial) node.userData.origMaterial = node.material;\n                            if (!node.userData.transMaterial) {\n                                const cloned = node.material.clone();\n                                cloned.transparent = true;\n                                cloned.opacity = 0.22;\n                                node.userData.transMaterial = cloned;\n                            }\n                            node.material = node.userData.transMaterial;\n                        }\n                    });\n\n                    if (!b.outline) {\n                        const edges = new THREE.EdgesGeometry(b.mesh.geometry);\n                        const lineMat = new THREE.LineDashedMaterial({\n                            color: 0xffffff,\n                            dashSize: 1.5,\n                            gapSize: 1.2,\n                            transparent: true,\n                            opacity: 0.8\n                        });\n                        b.outline = new THREE.LineSegments(edges, lineMat);\n                        b.outline.computeLineDistances();\n                        b.mesh.add(b.outline);\n                    }\n                    b.outline.visible = true;\n                } else {\n                    if (b.origMaterials) {\n                        b.mesh.material = b.origMaterials;\n                    }\n\n                    b.mesh.traverse(node => {\n                        if (node.isMesh && node !== b.mesh && node.userData.origMaterial) {\n                            node.material = node.userData.origMaterial;\n                        }\n                    });\n\n                    if (b.outline) {\n                        b.outline.visible = false;\n                    }\n                }\n            });\n        }\n        // ==================== UPGRADES (auto-fire abilities + passives) ====================\n        // Abilities (def.fire) auto-fire on cooldown and level up when re-picked.\n        // Passives (def.onPick) apply an incremental effect each time they're chosen.\n        // Icon asset mapping for upgrades\n        const upgradeIcons = {\n            'laser': 'icon_laser',\n            'meteor': 'icon_meteor',\n            'jump': 'icon_jump',\n            'tail': 'icon_tail',\n            'dash': 'icon_dash',\n            'bigger': 'icon_growth',\n            'tough_hide': 'icon_shield',\n            'frenzy': 'icon_frenzy',\n            'big_stomp': 'icon_stomp',\n            'regen': 'icon_regen'\n        };\n        \n        // Fallback emoji icons\n        const upgradeEmojis = {\n            'laser': '\ud83d\udd34',\n            'meteor': '\u2604\ufe0f',\n            'jump': '\ud83e\udd96',\n            'tail': '\ud83c\udf00',\n            'dash': '\ud83d\udca8',\n            'bigger': '\ud83d\udcc8',\n            'tough_hide': '\ud83d\udee1\ufe0f',\n            'frenzy': '\u26a1',\n            'big_stomp': '\ud83d\udc63',\n            'regen': '\u2728'\n        };\n        \n        function getUpgradeIconHtml(id, size = 'normal') {\n            const assetId = upgradeIcons[id];\n            const assetInfo = lib.getAsset(assetId);\n            if (assetInfo && assetInfo.url) {\n                return '<img src=\"' + assetInfo.url + '\" alt=\"' + id + '\">';\n            }\n            return upgradeEmojis[id] || '\u2753';\n        }\n        \n        const upgradeDefs = [\n            { id: 'laser', name: 'Laser Breath', ability: true, maxLevel: 5,\n              desc: 'Sustained beam that fires in your facing direction',\n              cooldown: l => Math.max(2.5, 5.5 - l * 0.5),\n              fire: l => fireLaser(l) },\n            { id: 'meteor', name: 'Meteor Shower', ability: true, maxLevel: 5,\n              desc: 'Rain meteors on the area around you',\n              cooldown: l => Math.max(3.4, 7.5 - l * 0.7),\n              fire: l => fireMeteor(l) },\n            { id: 'jump', name: 'Jump Stomp', ability: true, maxLevel: 5,\n              desc: 'Leap and slam down a huge shockwave',\n              cooldown: l => Math.max(3.5, 6.5 - l * 0.5),\n              fire: l => fireJumpStomp(l) },\n            { id: 'tail', name: 'Tail Sweep', ability: true, maxLevel: 5,\n              desc: 'Sweep and smash everything around you',\n              cooldown: l => Math.max(1.2, 2.8 - l * 0.22),\n              fire: l => fireTailSweep(l) },\n            { id: 'dash', name: 'Power Dash', ability: true, maxLevel: 5,\n              desc: 'Auto-dash forward, crushing everything in your path',\n              cooldown: l => Math.max(1.5, 3.5 - l * 0.35),\n              fire: l => fireDash(l) },\n\n            { id: 'bigger', name: 'Growth Spurt', maxLevel: 5,\n              desc: 'Immediately grow bigger and gain bonus HP',\n              onPick: l => { player.targetSize += 0.12; player.maxHealth *= 1.15; player.health = Math.min(player.maxHealth, player.health + player.maxHealth * 0.15); } },\n            { id: 'tough_hide', name: 'Tough Hide', maxLevel: 5,\n              desc: '+22% max HP, heal, take less damage',\n              onPick: l => { player.maxHealth *= 1.22; player.health = Math.min(player.maxHealth, player.health + player.maxHealth * 0.22); player.damageReduction = Math.min(0.7, player.damageReduction + 0.07); } },\n            { id: 'frenzy', name: 'Frenzy', maxLevel: 4,\n              desc: '-14% cooldown on all powers',\n              onPick: l => { player.cooldownMul *= 0.86; } },\n            { id: 'big_stomp', name: 'Big Stomp', maxLevel: 5,\n              desc: '+18% crush range & power',\n              onPick: l => { player.crushBonus *= 1.18; } },\n            { id: 'regen', name: 'Regeneration', maxLevel: 5,\n              desc: 'Heal over time',\n              onPick: l => { player.regenRate += 2; } }\n        ];\n\n        // ==================== WORLD (infinite chunk streaming) ====================\n        const loadedChunks = new Map();   // \"cx,cz\" -> { ground }\n        let chunkGroundTex = null;        // shared baked tile (tiles seamlessly across chunks)\n        let chunkGroundMat = null;\n        let groundPlane = null;           // big solid plane that follows the player (fills the void)\n        const chunkPlaneGeo = registerSharedGeometry(new THREE.PlaneGeometry(CHUNK_SIZE, CHUNK_SIZE));\n\n        function drawZebra(ctx, x, y, w, h, n, horizontal) {\n            ctx.fillStyle = '#eceae0';\n            if (horizontal) {\n                const sh = h / (n * 2 - 1);\n                for (let k = 0; k < n; k++) ctx.fillRect(x, y + k * 2 * sh, w, sh);\n            } else {\n                const sw = w / (n * 2 - 1);\n                for (let k = 0; k < n; k++) ctx.fillRect(x + k * 2 * sw, y, sw, h);\n            }\n        }\n\n        // One chunk's worth of streets, baked so edge roads are half-width and\n        // join seamlessly with neighbouring chunks (roads sit on block boundaries).\n        function makeChunkGroundTexture() {\n            const px = 1024;\n            const unit = px / CHUNK_SIZE;\n            const toPx = (w) => w * unit;\n            const roadW = ROAD_WIDTH * unit;\n            const rw = roadW / 2;\n            const band = roadW * 0.55;\n\n            return makeCanvasTexture(px, px, (ctx) => {\n                ctx.fillStyle = '#90a880';\n                ctx.fillRect(0, 0, px, px);\n\n                for (let bx = 0; bx < CHUNK_BLOCKS; bx++) {\n                    for (let bz = 0; bz < CHUNK_BLOCKS; bz++) {\n                        const x0 = toPx(bx * BLOCK_PERIOD + ROAD_WIDTH / 2);\n                        const z0 = toPx(bz * BLOCK_PERIOD + ROAD_WIDTH / 2);\n                        const bs = BLOCK_SIZE * unit;\n                        ctx.fillStyle = (bx + bz) % 2 === 0 ? '#85a070' : '#95b080';\n                        ctx.fillRect(x0, z0, bs, bs);\n                    }\n                }\n\n                ctx.fillStyle = '#404858';\n                for (let i = 0; i <= CHUNK_BLOCKS; i++) {\n                    const c = toPx(i * BLOCK_PERIOD);\n                    ctx.fillRect(c - rw, 0, roadW, px);\n                    ctx.fillRect(0, c - rw, px, roadW);\n                }\n\n                ctx.strokeStyle = '#e7c34a';\n                ctx.lineWidth = Math.max(2, unit * 0.45);\n                ctx.setLineDash([unit * 3, unit * 3]);\n                for (let i = 0; i <= CHUNK_BLOCKS; i++) {\n                    const c = toPx(i * BLOCK_PERIOD);\n                    ctx.beginPath(); ctx.moveTo(c, 0); ctx.lineTo(c, px); ctx.stroke();\n                    ctx.beginPath(); ctx.moveTo(0, c); ctx.lineTo(px, c); ctx.stroke();\n                }\n                ctx.setLineDash([]);\n\n                for (let i = 0; i <= CHUNK_BLOCKS; i++) {\n                    for (let j = 0; j <= CHUNK_BLOCKS; j++) {\n                        const cx = toPx(i * BLOCK_PERIOD), cz = toPx(j * BLOCK_PERIOD);\n                        drawZebra(ctx, cx - rw, cz - rw - band, roadW, band, 4, false);\n                        drawZebra(ctx, cx - rw, cz + rw, roadW, band, 4, false);\n                        drawZebra(ctx, cx - rw - band, cz - rw, band, roadW, 4, true);\n                        drawZebra(ctx, cx + rw, cz - rw, band, roadW, 4, true);\n                    }\n                }\n            });\n        }\n\n        function ensureGround() {\n            // Create or recreate ground texture if needed\n            if (!chunkGroundTex || !chunkGroundMat) {\n                chunkGroundTex = makeChunkGroundTexture();\n                if (!chunkGroundTex) {\n                    lib.log('Warning: Failed to create ground texture, using fallback');\n                    chunkGroundMat = new THREE.MeshLambertMaterial({ color: 0x3b414a });\n                } else {\n                    chunkGroundMat = new THREE.MeshLambertMaterial({ map: chunkGroundTex });\n                }\n                persistentSharedMaterials.add(chunkGroundMat);\n            }\n            \n            // Ensure the texture is marked for update (helps with GPU sync issues)\n            if (chunkGroundTex) {\n                chunkGroundTex.needsUpdate = true;\n            }\n            \n            if (!groundPlane) {\n                groundPlane = new THREE.Mesh(new THREE.PlaneGeometry(1600, 1600), groundMaterial);\n                groundPlane.rotation.x = -Math.PI / 2;\n                groundPlane.position.y = -0.3;\n                groundPlane.receiveShadow = true;\n                scene.add(groundPlane);\n            }\n        }\n\n        function chunkKey(cx, cz) { return cx + ',' + cz; }\n\n        function loadChunk(cx, cz) {\n            const key = chunkKey(cx, cz);\n            if (loadedChunks.has(key)) return;\n            const ground = new THREE.Mesh(chunkPlaneGeo, chunkGroundMat);\n            ground.rotation.x = -Math.PI / 2;\n            ground.position.set(cx * CHUNK_SIZE + CHUNK_SIZE / 2, 0, cz * CHUNK_SIZE + CHUNK_SIZE / 2);\n            ground.receiveShadow = true;\n            scene.add(ground);\n            loadedChunks.set(key, { ground });\n\n            const gbx0 = cx * CHUNK_BLOCKS, gbz0 = cz * CHUNK_BLOCKS;\n            for (let bx = 0; bx < CHUNK_BLOCKS; bx++) {\n                for (let bz = 0; bz < CHUNK_BLOCKS; bz++) createBuilding(gbx0 + bx, gbz0 + bz, key);\n            }\n            createTreesInChunk(cx, cz, key);\n            spawnHeartsInChunk(cx, cz, key);\n            spawnPowerupsInChunk(cx, cz, key);\n        }\n\n        function unloadChunk(key) {\n            const c = loadedChunks.get(key);\n            if (!c) return;\n            removeFromScene(c.ground);\n            for (let i = buildings.length - 1; i >= 0; i--) {\n                if (buildings[i].chunkKey === key) { removeFromScene(buildings[i].mesh); buildings.splice(i, 1); }\n            }\n            for (let i = props.length - 1; i >= 0; i--) {\n                if (props[i].chunkKey === key) { removeFromScene(props[i].mesh); props.splice(i, 1); }\n            }\n            for (let i = heartPickups.length - 1; i >= 0; i--) {\n                if (heartPickups[i].chunkKey === key) { removeFromScene(heartPickups[i].mesh); heartPickups.splice(i, 1); }\n            }\n            for (let i = invincibilityPickups.length - 1; i >= 0; i--) {\n                if (invincibilityPickups[i].chunkKey === key) { removeFromScene(invincibilityPickups[i].mesh); invincibilityPickups.splice(i, 1); }\n            }\n            // Speed pickups removed - combined with invincibility star\n            loadedChunks.delete(key);\n        }\n\n        function updateChunks() {\n            ensureGround();\n            groundPlane.position.set(player.x, -0.3, player.z);\n\n            const pcx = Math.floor(player.x / CHUNK_SIZE);\n            const pcz = Math.floor(player.z / CHUNK_SIZE);\n            for (let dz = -CHUNK_RADIUS; dz <= CHUNK_RADIUS; dz++) {\n                for (let dx = -CHUNK_RADIUS; dx <= CHUNK_RADIUS; dx++) loadChunk(pcx + dx, pcz + dz);\n            }\n            loadedChunks.forEach((c, key) => {\n                const parts = key.split(',');\n                if (Math.abs(parseInt(parts[0], 10) - pcx) > CHUNK_UNLOAD ||\n                    Math.abs(parseInt(parts[1], 10) - pcz) > CHUNK_UNLOAD) unloadChunk(key);\n            });\n        }\n\n        // Scale side-face UVs so window rows/cols stay uniform across building sizes.\n        function scaleBuildingUVs(geo, width, height, depth) {\n            const uv = geo.attributes.uv;\n            const ry = Math.max(1, Math.round(height / 16));\n            const rxZ = Math.max(1, Math.round(width / 14));\n            const rxX = Math.max(1, Math.round(depth / 14));\n            const scaleFace = (start, ru) => {\n                for (let k = start; k < start + 4; k++) uv.setXY(k, uv.getX(k) * ru, uv.getY(k) * ry);\n            };\n            scaleFace(0, rxX);   // +X\n            scaleFace(4, rxX);   // -X\n            scaleFace(16, rxZ);  // +Z\n            scaleFace(20, rxZ);  // -Z\n            uv.needsUpdate = true;\n        }\n\n        function createBuilding(gbx, gbz, chunkKeyId) {\n            const baseX = gbx * BLOCK_PERIOD + ROAD_WIDTH / 2;\n            const baseZ = gbz * BLOCK_PERIOD + ROAD_WIDTH / 2;\n\n            const density = config.city.density === 'sparse' ? 1 : config.city.density === 'dense' ? 3 : 2;\n            const count = 1 + Math.floor(Math.random() * density);\n\n            for (let i = 0; i < count; i++) {\n                const type = Math.random();\n                let height, width, depth, xpValue, scoreValue, mass, hp;\n\n                if (type < 0.45) {       // small\n                    height = 9 + Math.random() * 8;\n                    width = 9 + Math.random() * 4;\n                    depth = 9 + Math.random() * 4;\n                    xpValue = 10; scoreValue = 100; mass = 6; hp = 55;\n                } else if (type < 0.82) { // medium\n                    height = 22 + Math.random() * 16;\n                    width = 12 + Math.random() * 6;\n                    depth = 12 + Math.random() * 6;\n                    xpValue = 22; scoreValue = 250; mass = 14; hp = 115;\n                } else {                  // tall\n                    height = 48 + Math.random() * 24;\n                    width = 15 + Math.random() * 6;\n                    depth = 15 + Math.random() * 6;\n                    xpValue = 40; scoreValue = 500; mass = 30; hp = 210;\n                }\n\n                const x = baseX + width / 2 + Math.random() * (BLOCK_SIZE - width);\n                const z = baseZ + depth / 2 + Math.random() * (BLOCK_SIZE - depth);\n                const style = buildingStyles[Math.floor(Math.random() * buildingStyles.length)];\n\n                const geo = new THREE.BoxGeometry(width, height, depth);\n                scaleBuildingUVs(geo, width, height, depth);\n                // [+X, -X, +Y(top), -Y(bottom), +Z, -Z]\n                const mats = [style.facadeMat, style.facadeMat, style.roofMat, style.roofMat, style.facadeMat, style.facadeMat];\n                const mesh = new THREE.Mesh(geo, mats);\n                mesh.position.set(x, height / 2, z);\n                mesh.castShadow = true;\n                mesh.receiveShadow = true;\n\n                // rooftop detail\n                if (Math.random() < 0.6) {\n                    const pw = width * (0.2 + Math.random() * 0.25);\n                    const pd = depth * (0.2 + Math.random() * 0.25);\n                    const ph = 1.5 + Math.random() * 3.5;\n                    const cap = new THREE.Mesh(new THREE.BoxGeometry(pw, ph, pd), rooftopMaterial);\n                    cap.position.set((Math.random() - 0.5) * width * 0.35, height / 2 + ph / 2, (Math.random() - 0.5) * depth * 0.35);\n                    cap.castShadow = true;\n                    mesh.add(cap);\n                }\n\n                scene.add(mesh);\n\n                buildings.push({\n                    mesh, x, z, width, depth, height,\n                    debrisColor: style.debrisColor,\n                    xpValue, scoreValue, mass,\n                    hp, maxHp: hp, hitCd: 0,\n                    tiltDir: Math.random() < 0.5 ? -1 : 1,\n                    tiltDir2: Math.random() < 0.5 ? -1 : 1,\n                    chunkKey: chunkKeyId,\n                    dead: false, crushing: false, crushTimer: 0\n                });\n            }\n        }\n\n        function makeCar(bodyColor) {\n            const g = new THREE.Group();\n            const mat = new THREE.MeshLambertMaterial({ color: bodyColor, flatShading: true });\n            const body = new THREE.Mesh(new THREE.BoxGeometry(4.4, 1.1, 2.1), mat);\n            body.position.y = 0.85; g.add(body);\n            const cabin = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.0, 1.8), mat);\n            cabin.position.set(-0.2, 1.75, 0); g.add(cabin);\n            const glass = new THREE.Mesh(new THREE.BoxGeometry(2.42, 0.8, 1.6), carGlassMaterial);\n            glass.position.set(-0.2, 1.8, 0); g.add(glass);\n            [[1.3, 1.1], [1.3, -1.1], [-1.3, 1.1], [-1.3, -1.1]].forEach(([wx, wz]) => {\n                const wheel = new THREE.Mesh(sharedWheelGeo, wheelMaterial);\n                wheel.rotation.x = Math.PI / 2;\n                wheel.position.set(wx, 0.5, wz);\n                g.add(wheel);\n            });\n            return g;\n        }\n\n        function spawnCar(x, z, onVerticalRoad) {\n            const carColor = carBodyColors[Math.floor(Math.random() * carBodyColors.length)];\n            const car = makeCar(carColor);\n            car.position.set(x, 0, z);\n\n            const speed = 6 + Math.random() * 8;\n            const vx = onVerticalRoad ? 0 : (Math.random() < 0.5 ? -speed : speed);\n            const vz = onVerticalRoad ? (Math.random() < 0.5 ? -speed : speed) : 0;\n            \n            car.rotation.y = Math.atan2(-vz, vx);\n            scene.add(car);\n\n            props.push({\n                mesh: car, x, z, width: 4.4, depth: 2.1,\n                xpValue: 5, scoreValue: 50, mass: 3,\n                type: 'car', dead: false,\n                vx: vx,\n                vz: vz\n            });\n        }\n\n        // Spawn a car on a road near the player (infinite-world friendly).\n        function spawnCarNear() {\n            const vertical = Math.random() < 0.5;\n            const along = (Math.random() - 0.5) * 2 * CROWD_CULL_DIST;\n            const off = (Math.random() < 0.5 ? -1 : 1) * (40 + Math.random() * 150);\n            let x, z;\n            if (vertical) {\n                x = Math.round((player.x + off) / BLOCK_PERIOD) * BLOCK_PERIOD + (Math.random() - 0.5) * 4;\n                z = player.z + along;\n            } else {\n                z = Math.round((player.z + off) / BLOCK_PERIOD) * BLOCK_PERIOD + (Math.random() - 0.5) * 4;\n                x = player.x + along;\n            }\n            spawnCar(x, z, vertical);\n        }\n\n        // Heart pickup creation\n        function createHeartMesh() {\n            const group = new THREE.Group();\n            \n            // Create a simple heart shape using spheres\n            const heartCore = new THREE.Mesh(\n                new THREE.SphereGeometry(1.2, 12, 12),\n                heartMaterial\n            );\n            heartCore.scale.set(1, 0.9, 0.6);\n            heartCore.position.y = 0;\n            group.add(heartCore);\n            \n            // Left bump\n            const leftBump = new THREE.Mesh(\n                new THREE.SphereGeometry(0.7, 10, 10),\n                heartMaterial\n            );\n            leftBump.position.set(-0.5, 0.5, 0);\n            group.add(leftBump);\n            \n            // Right bump\n            const rightBump = new THREE.Mesh(\n                new THREE.SphereGeometry(0.7, 10, 10),\n                heartMaterial\n            );\n            rightBump.position.set(0.5, 0.5, 0);\n            group.add(rightBump);\n            \n            // Bottom point\n            const point = new THREE.Mesh(\n                new THREE.ConeGeometry(0.8, 1.2, 8),\n                heartMaterial\n            );\n            point.position.set(0, -0.9, 0);\n            point.rotation.x = Math.PI;\n            group.add(point);\n            \n            // Glow effect\n            const glow = new THREE.Mesh(\n                new THREE.SphereGeometry(1.8, 12, 12),\n                heartGlowMaterial.clone()\n            );\n            glow.name = 'glow';\n            group.add(glow);\n            \n            // Tall gold vertical beam for visibility at any distance\n            const beamHeight = 120; // Very tall beam\n            const beamOuter = new THREE.Mesh(\n                new THREE.CylinderGeometry(2.5, 2.5, beamHeight, 12, 1, true),\n                heartBeamMaterial.clone()\n            );\n            beamOuter.position.y = beamHeight / 2 - 2;\n            beamOuter.name = 'beamOuter';\n            group.add(beamOuter);\n            \n            // Inner brighter core beam\n            const beamCore = new THREE.Mesh(\n                new THREE.CylinderGeometry(0.8, 0.8, beamHeight, 8, 1, true),\n                heartBeamCoreMaterial.clone()\n            );\n            beamCore.position.y = beamHeight / 2 - 2;\n            beamCore.name = 'beamCore';\n            group.add(beamCore);\n            \n            // Base glow ring on ground (vibrant pink to match the heart)\n            const baseRing = new THREE.Mesh(\n                new THREE.RingGeometry(1.5, 4, 24),\n                new THREE.MeshBasicMaterial({ color: 0xff1493, transparent: true, opacity: 0.4, side: THREE.DoubleSide })\n            );\n            baseRing.rotation.x = -Math.PI / 2;\n            baseRing.position.y = -2.8;\n            baseRing.name = 'baseRing';\n            group.add(baseRing);\n            \n            return group;\n        }\n        \n        function spawnHeartPickup(x, z, chunkKey) {\n            const mesh = createHeartMesh();\n            mesh.position.set(x, 3, z);\n            mesh.castShadow = true;\n            scene.add(mesh);\n            \n            heartPickups.push({\n                mesh,\n                x, z,\n                chunkKey,\n                bobPhase: Math.random() * Math.PI * 2,\n                collected: false\n            });\n        }\n        \n        function spawnHeartsInChunk(cx, cz, chunkKeyId) {\n            // Rare: ~8% chance per chunk to have a heart\n            if (Math.random() > 0.08) return;\n            \n            // Find a valid position not on roads or in buildings\n            for (let attempts = 0; attempts < 8; attempts++) {\n                const x = cx * CHUNK_SIZE + Math.random() * CHUNK_SIZE;\n                const z = cz * CHUNK_SIZE + Math.random() * CHUNK_SIZE;\n                \n                // Check if on road\n                const wrap = (val, period) => ((val % period) + period) % period;\n                const wx = wrap(x, BLOCK_PERIOD);\n                const wz = wrap(z, BLOCK_PERIOD);\n                const halfRoad = ROAD_WIDTH / 2 + 2;\n                const onRoad = wx < halfRoad || wx > BLOCK_PERIOD - halfRoad || wz < halfRoad || wz > BLOCK_PERIOD - halfRoad;\n                \n                if (!onRoad && !checkBuildingCollision(x, z, 2)) {\n                    spawnHeartPickup(x, z, chunkKeyId);\n                    break;\n                }\n            }\n        }\n        \n        // Invincibility pickup (golden star)\n        function createInvincibilityMesh() {\n            const group = new THREE.Group();\n            \n            // Create a star shape using triangular prisms\n            const starPoints = 5;\n            const outerRadius = 1.8;\n            const innerRadius = 0.8;\n            \n            for (let i = 0; i < starPoints; i++) {\n                const angle = (i / starPoints) * Math.PI * 2 - Math.PI / 2;\n                const nextAngle = ((i + 0.5) / starPoints) * Math.PI * 2 - Math.PI / 2;\n                \n                // Outer point\n                const spike = new THREE.Mesh(\n                    new THREE.ConeGeometry(0.6, 1.5, 4),\n                    invincibilityMaterial\n                );\n                spike.position.set(\n                    Math.cos(angle) * outerRadius * 0.7,\n                    0,\n                    Math.sin(angle) * outerRadius * 0.7\n                );\n                spike.rotation.z = -angle - Math.PI / 2;\n                spike.rotation.x = Math.PI / 2;\n                group.add(spike);\n            }\n            \n            // Center sphere\n            const center = new THREE.Mesh(\n                new THREE.SphereGeometry(1, 12, 12),\n                invincibilityMaterial\n            );\n            group.add(center);\n            \n            // Glow effect\n            const glow = new THREE.Mesh(\n                new THREE.SphereGeometry(2.2, 12, 12),\n                invincibilityGlowMaterial.clone()\n            );\n            glow.name = 'glow';\n            group.add(glow);\n            \n            // Tall golden beam\n            const beamHeight = 120;\n            const beamOuter = new THREE.Mesh(\n                new THREE.CylinderGeometry(2.5, 2.5, beamHeight, 12, 1, true),\n                invincibilityBeamMaterial.clone()\n            );\n            beamOuter.position.y = beamHeight / 2 - 2;\n            beamOuter.name = 'beamOuter';\n            group.add(beamOuter);\n            \n            // Base glow ring\n            const baseRing = new THREE.Mesh(\n                new THREE.RingGeometry(1.5, 4, 24),\n                new THREE.MeshBasicMaterial({ color: 0xffd700, transparent: true, opacity: 0.4, side: THREE.DoubleSide })\n            );\n            baseRing.rotation.x = -Math.PI / 2;\n            baseRing.position.y = -2.8;\n            baseRing.name = 'baseRing';\n            group.add(baseRing);\n            \n            return group;\n        }\n        \n        function spawnInvincibilityPickup(x, z, chunkKey, startY = 5.5) {\n            const mesh = createInvincibilityMesh();\n            mesh.position.set(x, startY, z);\n            mesh.castShadow = true;\n            scene.add(mesh);\n            \n            invincibilityPickups.push({\n                mesh,\n                x, z,\n                y: startY,\n                chunkKey,\n                bobPhase: Math.random() * Math.PI * 2,\n                collected: false,\n                falling: startY > 5.5\n            });\n        }\n        \n        // Speed pickup (green lightning bolt)\n        function createSpeedMesh() {\n            const group = new THREE.Group();\n            \n            // Create a lightning bolt shape\n            const boltShape = new THREE.Shape();\n            boltShape.moveTo(0, 1.5);\n            boltShape.lineTo(0.8, 0.3);\n            boltShape.lineTo(0.3, 0.3);\n            boltShape.lineTo(0.6, -1.5);\n            boltShape.lineTo(-0.4, 0);\n            boltShape.lineTo(0.1, 0);\n            boltShape.lineTo(-0.3, 1.5);\n            \n            const extrudeSettings = { depth: 0.6, bevelEnabled: true, bevelThickness: 0.1, bevelSize: 0.1 };\n            const boltGeo = new THREE.ExtrudeGeometry(boltShape, extrudeSettings);\n            const bolt = new THREE.Mesh(boltGeo, speedMaterial);\n            bolt.scale.set(1.2, 1.2, 1.2);\n            bolt.position.z = -0.3;\n            group.add(bolt);\n            \n            // Glow effect\n            const glow = new THREE.Mesh(\n                new THREE.SphereGeometry(2.5, 12, 12),\n                speedGlowMaterial.clone()\n            );\n            glow.name = 'glow';\n            group.add(glow);\n            \n            // Tall green beam\n            const beamHeight = 120;\n            const beamOuter = new THREE.Mesh(\n                new THREE.CylinderGeometry(2.5, 2.5, beamHeight, 12, 1, true),\n                speedBeamMaterial.clone()\n            );\n            beamOuter.position.y = beamHeight / 2 - 2;\n            beamOuter.name = 'beamOuter';\n            group.add(beamOuter);\n            \n            // Base glow ring\n            const baseRing = new THREE.Mesh(\n                new THREE.RingGeometry(1.5, 4, 24),\n                new THREE.MeshBasicMaterial({ color: 0x00ff66, transparent: true, opacity: 0.4, side: THREE.DoubleSide })\n            );\n            baseRing.rotation.x = -Math.PI / 2;\n            baseRing.position.y = -2.8;\n            baseRing.name = 'baseRing';\n            group.add(baseRing);\n            \n            return group;\n        }\n        \n        function spawnSpeedPickup(x, z, chunkKey) {\n            const mesh = createSpeedMesh();\n            mesh.position.set(x, 3, z);\n            mesh.castShadow = true;\n            scene.add(mesh);\n            \n            speedPickups.push({\n                mesh,\n                x, z,\n                chunkKey,\n                bobPhase: Math.random() * Math.PI * 2,\n                collected: false\n            });\n        }\n        \n        function spawnPowerupsInChunk(cx, cz, chunkKeyId) {\n            // Increased from 3% to 18% so stars are much more common and gameplay is easier!\n            if (Math.random() < 0.18) {\n                for (let attempts = 0; attempts < 8; attempts++) {\n                    const x = cx * CHUNK_SIZE + Math.random() * CHUNK_SIZE;\n                    const z = cz * CHUNK_SIZE + Math.random() * CHUNK_SIZE;\n                    \n                    const wrap = (val, period) => ((val % period) + period) % period;\n                    const wx = wrap(x, BLOCK_PERIOD);\n                    const wz = wrap(z, BLOCK_PERIOD);\n                    const halfRoad = ROAD_WIDTH / 2 + 2;\n                    const onRoad = wx < halfRoad || wx > BLOCK_PERIOD - halfRoad || wz < halfRoad || wz > BLOCK_PERIOD - halfRoad;\n                    \n                    if (!onRoad && !checkBuildingCollision(x, z, 2)) {\n                        spawnInvincibilityPickup(x, z, chunkKeyId);\n                        break;\n                    }\n                }\n            }\n        }\n        \n        function updateInvincibilityPickups(dt) {\n            const collectRadius = getCrushRadius() + 3;\n            \n            for (let i = invincibilityPickups.length - 1; i >= 0; i--) {\n                const pickup = invincibilityPickups[i];\n                if (pickup.collected) continue;\n                \n                if (pickup.falling) {\n                    pickup.y -= dt * 65; // Fall speed\n                    if (Math.random() < 0.35) {\n                        // Spawn golden star trails as it falls\n                        createDebris(pickup.x, pickup.y, pickup.z, 0xffd700);\n                    }\n                    if (pickup.y <= 5.5) {\n                        pickup.y = 5.5;\n                        pickup.falling = false;\n                        // Impact effect when landing\n                        addDustRing(pickup.x, pickup.z, 12, 0xffd700);\n                        playSound('growth_pop_sound', 0.5);\n                    }\n                    pickup.mesh.position.y = pickup.y;\n                    pickup.mesh.rotation.y += dt * 6; // Spin fast while falling\n                } else {\n                    // Floating and spinning animation\n                    pickup.bobPhase += dt * 3;\n                    const bobY = 3 + Math.sin(pickup.bobPhase) * 0.8;\n                    pickup.mesh.position.y = bobY;\n                    pickup.mesh.rotation.y += dt * 2;\n                }\n                \n                // Pulsing glow\n                const glowChild = pickup.mesh.children.find(c => c.name === 'glow');\n                if (glowChild && glowChild.material) {\n                    const pulse = 0.4 + Math.sin(pickup.bobPhase * 1.5) * 0.2;\n                    glowChild.material.opacity = pulse;\n                    const scale = 1 + Math.sin(pickup.bobPhase * 2) * 0.15;\n                    glowChild.scale.set(scale, scale, scale);\n                }\n                \n                // Beam effects\n                const beamOuter = pickup.mesh.children.find(c => c.name === 'beamOuter');\n                const baseRing = pickup.mesh.children.find(c => c.name === 'baseRing');\n                if (beamOuter && beamOuter.material) {\n                    const beamPulse = 0.35 + Math.sin(pickup.bobPhase * 2) * 0.2;\n                    beamOuter.material.opacity = beamPulse;\n                }\n                if (baseRing && baseRing.material) {\n                    const ringPulse = 0.3 + Math.sin(pickup.bobPhase * 2) * 0.2;\n                    baseRing.material.opacity = ringPulse;\n                }\n                \n                // Check collection\n                const dist = Math.hypot(pickup.x - player.x, pickup.z - player.z);\n                if (dist < collectRadius) {\n                    collectInvincibility(pickup, i);\n                }\n            }\n        }\n        \n        function collectInvincibility(pickup, index) {\n            pickup.collected = true;\n            \n            // Activate BOTH invincibility AND speed buff (combined star power!)\n            invincibilityTimer = BUFF_DURATION;\n            speedBoostTimer = BUFF_DURATION;\n            \n            // Effects\n            playSound('level_up_sound', 0.5);\n            addFloatingText(pickup.x, 6, pickup.z, '\u2b50 STAR POWER!');\n            addCameraShake(0.25);\n            \n            // Golden particles\n            for (let i = 0; i < 16; i++) {\n                const angle = (i / 16) * Math.PI * 2;\n                const px = pickup.x + Math.cos(angle) * 3;\n                const pz = pickup.z + Math.sin(angle) * 3;\n                createDebris(px, 4, pz, 0xffd700);\n            }\n            addDustRing(pickup.x, pickup.z, 12, 0xffdd00);\n            \n            // Golden flash\n            const flash = document.getElementById('level-up-flash');\n            flash.style.background = 'rgba(255, 215, 0, 0.4)';\n            flash.classList.remove('active');\n            void flash.offsetWidth;\n            flash.classList.add('active');\n            setTimeout(() => { flash.style.background = 'rgba(255, 215, 0, 0.3)'; }, 500);\n            \n            // Remove pickup\n            removeFromScene(pickup.mesh);\n            invincibilityPickups.splice(index, 1);\n        }\n        \n        function updateSpeedPickups(dt) {\n            const collectRadius = getCrushRadius() + 3;\n            \n            for (let i = speedPickups.length - 1; i >= 0; i--) {\n                const pickup = speedPickups[i];\n                if (pickup.collected) continue;\n                \n                // Floating and spinning animation\n                pickup.bobPhase += dt * 4; // Faster spin for speed pickup\n                const bobY = 3 + Math.sin(pickup.bobPhase) * 0.8;\n                pickup.mesh.position.y = bobY;\n                pickup.mesh.rotation.y += dt * 3;\n                \n                // Pulsing glow\n                const glowChild = pickup.mesh.children.find(c => c.name === 'glow');\n                if (glowChild && glowChild.material) {\n                    const pulse = 0.4 + Math.sin(pickup.bobPhase * 1.5) * 0.2;\n                    glowChild.material.opacity = pulse;\n                    const scale = 1 + Math.sin(pickup.bobPhase * 2) * 0.15;\n                    glowChild.scale.set(scale, scale, scale);\n                }\n                \n                // Beam effects\n                const beamOuter = pickup.mesh.children.find(c => c.name === 'beamOuter');\n                const baseRing = pickup.mesh.children.find(c => c.name === 'baseRing');\n                if (beamOuter && beamOuter.material) {\n                    const beamPulse = 0.35 + Math.sin(pickup.bobPhase * 2) * 0.2;\n                    beamOuter.material.opacity = beamPulse;\n                }\n                if (baseRing && baseRing.material) {\n                    const ringPulse = 0.3 + Math.sin(pickup.bobPhase * 2) * 0.2;\n                    baseRing.material.opacity = ringPulse;\n                }\n                \n                // Check collection\n                const dist = Math.hypot(pickup.x - player.x, pickup.z - player.z);\n                if (dist < collectRadius) {\n                    collectSpeed(pickup, i);\n                }\n            }\n        }\n        \n        function collectSpeed(pickup, index) {\n            pickup.collected = true;\n            \n            // Activate speed buff\n            speedBoostTimer = BUFF_DURATION;\n            \n            // Effects\n            playSound('level_up_sound', 0.5);\n            addFloatingText(pickup.x, 6, pickup.z, 'SPEED BOOST!');\n            addCameraShake(0.2);\n            \n            // Green particles\n            for (let i = 0; i < 16; i++) {\n                const angle = (i / 16) * Math.PI * 2;\n                const px = pickup.x + Math.cos(angle) * 3;\n                const pz = pickup.z + Math.sin(angle) * 3;\n                createDebris(px, 4, pz, 0x00ff88);\n            }\n            addDustRing(pickup.x, pickup.z, 12, 0x00ff66);\n            \n            // Green flash\n            const flash = document.getElementById('level-up-flash');\n            flash.style.background = 'rgba(0, 255, 100, 0.4)';\n            flash.classList.remove('active');\n            void flash.offsetWidth;\n            flash.classList.add('active');\n            setTimeout(() => { flash.style.background = 'rgba(255, 215, 0, 0.3)'; }, 500);\n            \n            // Remove pickup\n            removeFromScene(pickup.mesh);\n            speedPickups.splice(index, 1);\n        }\n        \n        function updateBuffTimers(dt) {\n            // Update invincibility timer\n            if (invincibilityTimer > 0) {\n                invincibilityTimer -= dt;\n                if (invincibilityTimer <= 0) {\n                    invincibilityTimer = 0;\n                    // Star power ended effect\n                    addFloatingText(player.x, getMonsterTop() + 5, player.z, 'Star Power Off!');\n                }\n            }\n            \n            // Update speed boost timer (synced with invincibility for star power)\n            if (speedBoostTimer > 0) {\n                speedBoostTimer -= dt;\n                if (speedBoostTimer <= 0) {\n                    speedBoostTimer = 0;\n                    // No separate message - handled by invincibility message\n                }\n            }\n            \n            // Update buff HUD display\n            updateBuffHUD();\n        }\n        \n        function initBuffIcons() { /* buff ring removed - banner/vignette handle display */ }\n        \n        function updateBuffHUD() {\n            const banner = document.getElementById('star-power-banner');\n            const bannerText = document.getElementById('star-power-banner-text');\n            const vignette = document.getElementById('star-power-vignette');\n            \n            const isActive = invincibilityTimer > 0 || speedBoostTimer > 0;\n            const timeRemaining = Math.max(invincibilityTimer, speedBoostTimer);\n            const isWarning = timeRemaining < 3 && isActive;\n\n            // Banner\n            if (banner) {\n                if (isActive) {\n                    banner.classList.add('active');\n                    if (isWarning) {\n                        banner.classList.add('warning');\n                        if (bannerText) bannerText.textContent = 'FADING... ' + Math.ceil(timeRemaining) + 's';\n                    } else {\n                        banner.classList.remove('warning');\n                        if (bannerText) bannerText.textContent = 'STAR POWER!';\n                    }\n                } else {\n                    banner.classList.remove('active', 'warning');\n                }\n            }\n\n            // Vignette\n            if (vignette) {\n                if (isActive) {\n                    vignette.classList.add('active');\n                    if (isWarning) vignette.classList.add('warning');\n                    else vignette.classList.remove('warning');\n                } else {\n                    vignette.classList.remove('active', 'warning');\n                }\n            }\n        }\n        \n        function updateHeartPickups(dt) {\n            const collectRadius = getCrushRadius() + 3;\n            \n            for (let i = heartPickups.length - 1; i >= 0; i--) {\n                const heart = heartPickups[i];\n                if (heart.collected) continue;\n                \n                // Floating animation\n                heart.bobPhase += dt * 3;\n                const bobY = 3 + Math.sin(heart.bobPhase) * 0.8;\n                heart.mesh.position.y = bobY;\n                heart.mesh.rotation.y += dt * 1.5;\n                \n                // Pulsing glow\n                const glowChild = heart.mesh.children.find(c => c.name === 'glow');\n                if (glowChild && glowChild.material) {\n                    const pulse = 0.3 + Math.sin(heart.bobPhase * 1.5) * 0.15;\n                    glowChild.material.opacity = pulse;\n                    const scale = 1 + Math.sin(heart.bobPhase * 2) * 0.1;\n                    glowChild.scale.set(scale, scale, scale);\n                }\n                \n                // Pulsing beam effect\n                const beamOuter = heart.mesh.children.find(c => c.name === 'beamOuter');\n                const beamCore = heart.mesh.children.find(c => c.name === 'beamCore');\n                const baseRing = heart.mesh.children.find(c => c.name === 'baseRing');\n                \n                if (beamOuter && beamOuter.material) {\n                    const beamPulse = 0.35 + Math.sin(heart.bobPhase * 2) * 0.2;\n                    beamOuter.material.opacity = beamPulse;\n                    const beamScale = 1 + Math.sin(heart.bobPhase * 1.5) * 0.15;\n                    beamOuter.scale.set(beamScale, 1, beamScale);\n                }\n                if (beamCore && beamCore.material) {\n                    const corePulse = 0.5 + Math.sin(heart.bobPhase * 3) * 0.3;\n                    beamCore.material.opacity = corePulse;\n                }\n                if (baseRing && baseRing.material) {\n                    const ringPulse = 0.3 + Math.sin(heart.bobPhase * 2) * 0.2;\n                    baseRing.material.opacity = ringPulse;\n                    const ringScale = 1 + Math.sin(heart.bobPhase * 1.8) * 0.2;\n                    baseRing.scale.set(ringScale, ringScale, 1);\n                }\n                \n                // Check collection\n                const dist = Math.hypot(heart.x - player.x, heart.z - player.z);\n                if (dist < collectRadius) {\n                    collectHeart(heart, i);\n                }\n            }\n        }\n        \n        function collectHeart(heart, index) {\n            heart.collected = true;\n            \n            // Heal 25% of max HP\n            const healAmount = player.maxHealth * 0.25;\n            const actualHeal = Math.min(healAmount, player.maxHealth - player.health);\n            player.health = Math.min(player.maxHealth, player.health + healAmount);\n            \n            // Effects\n            playSound('heart_pickup_sound', 0.6);\n            addFloatingText(heart.x, 6, heart.z, '+' + Math.round(actualHeal) + ' HP');\n            addCameraShake(0.15);\n            \n            // Pink healing particles\n            for (let i = 0; i < 12; i++) {\n                const angle = (i / 12) * Math.PI * 2;\n                const px = heart.x + Math.cos(angle) * 2;\n                const pz = heart.z + Math.sin(angle) * 2;\n                createDebris(px, 4, pz, 0xff6688);\n            }\n            addDustRing(heart.x, heart.z, 8, 0xff88aa);\n            \n            // Green heal flash on screen\n            const flash = document.getElementById('level-up-flash');\n            flash.style.background = 'rgba(100, 255, 150, 0.3)';\n            flash.classList.remove('active');\n            void flash.offsetWidth;\n            flash.classList.add('active');\n            setTimeout(() => { flash.style.background = 'rgba(255, 215, 0, 0.3)'; }, 500);\n            \n            // Remove heart\n            removeFromScene(heart.mesh);\n            heartPickups.splice(index, 1);\n        }\n\n        function createTreesInChunk(cx, cz, chunkKeyId) {\n            const n = 1 + Math.floor(Math.random() * 2);\n            for (let i = 0; i < n; i++) {\n                let x, z, valid = false;\n                for (let attempts = 0; attempts < 10; attempts++) {\n                    x = cx * CHUNK_SIZE + Math.random() * CHUNK_SIZE;\n                    z = cz * CHUNK_SIZE + Math.random() * CHUNK_SIZE;\n                    \n                    const wrap = (val, period) => ((val % period) + period) % period;\n                    const wx = wrap(x, BLOCK_PERIOD);\n                    const wz = wrap(z, BLOCK_PERIOD);\n                    const halfRoad = ROAD_WIDTH / 2 + 1; // 1 unit buffer\n                    const onRoad = wx < halfRoad || wx > BLOCK_PERIOD - halfRoad || wz < halfRoad || wz > BLOCK_PERIOD - halfRoad;\n                    \n                    if (!onRoad && !checkBuildingCollision(x, z, 1.5)) {\n                        valid = true;\n                        break;\n                    }\n                }\n                if (!valid) continue;\n\n                const treeGroup = new THREE.Group();\n                const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.85, 4, 5), treeTrunkMaterial);\n                trunk.position.y = 2;\n                const canopy = new THREE.Mesh(new THREE.ConeGeometry(3.2, 5, 6), treeCanopyMaterial);\n                canopy.position.y = 6.5;\n                const canopyTop = new THREE.Mesh(new THREE.ConeGeometry(2.2, 3.6, 6), treeCanopyMaterial2);\n                canopyTop.position.y = 9;\n                treeGroup.add(trunk, canopy, canopyTop);\n                treeGroup.position.set(x, 0, z);\n                treeGroup.rotation.y = Math.random() * Math.PI;\n                scene.add(treeGroup);\n                props.push({ mesh: treeGroup, x, z, width: 3, depth: 3, xpValue: 3, scoreValue: 20, mass: 2, type: 'tree', chunkKey: chunkKeyId, dead: false, vx: 0, vz: 0 });\n            }\n        }\n\n        function makePerson() {\n            const g = new THREE.Group();\n            const cloth = clothColors[Math.floor(Math.random() * clothColors.length)];\n            const skin = skinColors[Math.floor(Math.random() * skinColors.length)];\n            const legCol = legColors[Math.floor(Math.random() * legColors.length)];\n            \n            // Use cached materials and shared geometries for performance\n            const clothMat = getClothMaterial(cloth);\n            const skinMat = getSkinMaterial(skin);\n            const legMat = getLegMaterial(legCol);\n\n            // Optimized smaller people using shared geometries\n            const torso = new THREE.Mesh(personTorsoGeo, clothMat);\n            torso.position.y = 1.2; torso.castShadow = false; // Disable shadow for perf\n            const head = new THREE.Mesh(personHeadGeo, skinMat);\n            head.position.y = 1.7; head.castShadow = false;\n            const legL = new THREE.Mesh(personLegGeo, legMat);\n            legL.position.set(-0.14, 0.45, 0); legL.castShadow = false;\n            const legR = new THREE.Mesh(personLegGeo, legMat);\n            legR.position.set(0.14, 0.45, 0); legR.castShadow = false;\n            g.add(torso); g.add(head); g.add(legL); g.add(legR);\n            return g;\n        }\n\n        function addPersonAt(x, z) {\n            const mesh = makePerson();\n            mesh.position.set(x, 0, z);\n            mesh.matrixAutoUpdate = false; // Manual matrix update for perf\n            mesh.updateMatrix();\n            scene.add(mesh);\n            people.push({\n                mesh, x, z, vx: 0, vz: 0,\n                walkSpeed: 1.2 + Math.random() * 0.8,\n                runSpeed: 6 + Math.random() * 2.5,\n                wanderTimer: Math.random() * 2,\n                phase: Math.random() * Math.PI * 2,\n                fleeing: false, dead: false,\n                xpValue: 4, scoreValue: 20, mass: 1\n            });\n        }\n\n        function spawnPersonNearPlayer() {\n            const a = Math.random() * Math.PI * 2;\n            // Spawn closer when monster is bigger (more people in visible area)\n            const sizeRatio = Math.max(1, player.size / STARTING_SIZE);\n            const minR = 50 + sizeRatio * 10; // Spawn ring moves out with size\n            const maxR = 120 + sizeRatio * 40; // But also extends further\n            const r = minR + Math.random() * (maxR - minR);\n            const px = player.x + Math.cos(a) * r;\n            const pz = player.z + Math.sin(a) * r;\n            if (!checkBuildingCollision(px, pz, 0.5)) {\n                addPersonAt(px, pz);\n            }\n        }\n\n        // Keep a dense crowd + some traffic around the player; cull what falls far behind.\n        // Crowd scales up with monster size so you always feel surrounded\n        function maintainCrowd(dt) {\n            // Dynamic crowd scaling based on monster size - bigger monster = more people to crush!\n            const sizeRatio = Math.max(1, player.size / STARTING_SIZE);\n            const crowdMultiplier = 1 + (sizeRatio - 1) * 0.3; // 30% more people per size doubling\n            const targetPeople = Math.floor(PEOPLE_TARGET * crowdMultiplier);\n            const targetCars = Math.floor(CAR_TARGET * crowdMultiplier);\n            \n            // Expand cull distance with monster size so crowds feel consistent\n            const dynamicCullDist = CROWD_CULL_DIST * (1 + (sizeRatio - 1) * 0.25);\n            const cull2 = dynamicCullDist * dynamicCullDist;\n            \n            for (let i = people.length - 1; i >= 0; i--) {\n                const pp = people[i];\n                if (pp.dead) continue;\n                if ((pp.x - player.x) * (pp.x - player.x) + (pp.z - player.z) * (pp.z - player.z) > cull2) {\n                    removeFromScene(pp.mesh); people.splice(i, 1);\n                }\n            }\n            let cars = 0;\n            for (let i = props.length - 1; i >= 0; i--) {\n                const p = props[i];\n                if (p.type !== 'car' || p.dead) continue;\n                if ((p.x - player.x) * (p.x - player.x) + (p.z - player.z) * (p.z - player.z) > cull2) {\n                    removeFromScene(p.mesh); props.splice(i, 1);\n                } else cars++;\n            }\n            // Spawn more people per frame to fill crowds faster\n            let guard = 0;\n            const spawnRate = Math.min(12, 6 + Math.floor(sizeRatio)); // More spawns at larger size\n            while (people.length < targetPeople && guard++ < spawnRate) spawnPersonNearPlayer();\n            guard = 0;\n            while (cars < targetCars && guard++ < 4) { spawnCarNear(); cars++; }\n        }\n\n        // Dense crowd around a point (uniform disc) \u2014 used for the start cluster.\n        function spawnPeopleAround(cx, cz, n, radius) {\n            for (let i = 0; i < n; i++) {\n                const a = Math.random() * Math.PI * 2;\n                const r = Math.sqrt(Math.random()) * radius;\n                const px = cx + Math.cos(a) * r;\n                const pz = cz + Math.sin(a) * r;\n                if (!checkBuildingCollision(px, pz, 0.5)) {\n                    addPersonAt(px, pz);\n                }\n            }\n        }\n\n        // Spawn a small tutorial building directly in front of the player\n        function spawnTutorialBuilding() {\n            // Place it right in front of starting position (player faces +Z initially)\n            const x = 0;\n            const z = -18; // In front of the monster\n            const width = 8;\n            const depth = 8;\n            const height = 12; // Small building\n            \n            const style = buildingStyles[0];\n            const geo = new THREE.BoxGeometry(width, height, depth);\n            scaleBuildingUVs(geo, width, height, depth);\n            const mats = [style.facadeMat, style.facadeMat, style.roofMat, style.roofMat, style.facadeMat, style.facadeMat];\n            const mesh = new THREE.Mesh(geo, mats);\n            mesh.position.set(x, height / 2, z);\n            mesh.castShadow = true;\n            mesh.receiveShadow = true;\n            scene.add(mesh);\n\n            const building = {\n                mesh, x, z, width, depth, height,\n                debrisColor: style.debrisColor,\n                xpValue: 15, scoreValue: 150, mass: 8,\n                hp: 35, maxHp: 35, hitCd: 0, // Easy to destroy\n                tiltDir: 1, tiltDir2: 1,\n                chunkKey: 'tutorial',\n                dead: false, crushing: false, crushTimer: 0,\n                isTutorial: true // Mark as tutorial building\n            };\n            \n            buildings.push(building);\n            tutorialBuilding = building;\n            return building;\n        }\n\n        function showTutorial() {\n            tutorialActive = true;\n            tutorialTimer = 8; // Auto-hide after 8 seconds\n            document.getElementById('tutorial-overlay').classList.add('active');\n        }\n\n        function hideTutorial(save = true) {\n            tutorialActive = false;\n            tutorialTimer = 0;\n            document.getElementById('tutorial-overlay').classList.remove('active');\n            tutorialBuilding = null;\n            \n            // Save user profile - mark tutorial as seen\n            if (save) {\n                saveUserProfile();\n            }\n        }\n        \n        // Load user profile from persistence\n        async function loadUserProfile() {\n            try {\n                const response = await lib.getUserGameState();\n                if (response && response.state) {\n                    userProfile = response.state;\n                    lib.log('User profile loaded successfully');\n                } else {\n                    // New player - use default profile\n                    userProfile = { tutorialSeen: false };\n                    lib.log('No saved profile found, using defaults');\n                }\n            } catch (error) {\n                lib.log('Failed to load user profile: ' + error.message);\n                // Default to showing tutorial if load fails\n                userProfile = { tutorialSeen: false };\n            }\n        }\n        \n        // Save user profile to persistence\n        async function saveUserProfile() {\n            try {\n                // Mark tutorial as seen\n                userProfile.tutorialSeen = true;\n                await lib.saveUserGameState(userProfile);\n                lib.log('User profile saved successfully');\n            } catch (error) {\n                lib.log('Failed to save user profile: ' + error.message);\n                // Game continues even if save fails\n            }\n        }\n        \n        function updateTutorial(dt) {\n            if (!tutorialActive) return;\n            tutorialTimer -= dt;\n            \n            // Fade out the tutorial as time runs out\n            const overlay = document.getElementById('tutorial-overlay');\n            if (tutorialTimer < 2) {\n                overlay.style.opacity = Math.max(0, tutorialTimer / 2);\n            } else {\n                overlay.style.opacity = 1;\n            }\n            \n            if (tutorialTimer <= 0) {\n                hideTutorial();\n            }\n        }\n\n        function generateCity() {\n            loadedChunks.clear();\n            updateChunks();                                  // streets + buildings around the player\n            spawnPeopleAround(player.x, player.z, 60, 65);    // larger instant dense start crowd (was 36)\n            for (let i = 0; i < 8; i++) maintainCrowd(0);     // fill the rest of the crowd + traffic (was 6)\n        }\n\n        // ==================== MONSTER ====================\n        let monsterGroup = new THREE.Group();\n        let monsterParts = {};\n\n        function createMonster() {\n            monsterGroup = new THREE.Group();\n            monsterParts = {};\n\n            // Add a mesh to a parent group at a local position.\n            const addTo = (parent, w, h, d, x, y, z, mat, shadow) => {\n                const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);\n                m.position.set(x, y, z);\n                if (shadow !== false) m.castShadow = true;\n                parent.add(m);\n                return m;\n            };\n\n            const HIP_Y = 6;\n\n            // ----- Legs (pivot at the hip so the whole leg swings) -----\n            [['legL', -2.3], ['legR', 2.3]].forEach(([key, sx]) => {\n                const leg = new THREE.Group();\n                leg.position.set(sx, HIP_Y, 0);\n                addTo(leg, 2.9, 5.2, 3.2, 0, 3.6 - HIP_Y, -0.2, monsterBodyMaterial);  // thigh/shin\n                addTo(leg, 3.0, 1.2, 5.2, 0, 0.6 - HIP_Y, 0.6, monsterBodyMaterial);   // foot\n                for (let t = -1; t <= 1; t++) {\n                    const claw = new THREE.Mesh(new THREE.ConeGeometry(0.34, 1.0, 5), monsterClawMaterial);\n                    claw.position.set(t * 0.85, 0.45 - HIP_Y, 3.0);\n                    claw.rotation.x = Math.PI / 2;\n                    leg.add(claw);\n                }\n                monsterGroup.add(leg);\n                monsterParts[key] = leg;\n            });\n\n            // ----- Body group (torso + head + arms + tail + fins; bobs/leans while walking) -----\n            const body = new THREE.Group();\n            monsterParts.body = body;\n            monsterGroup.add(body);\n\n            addTo(body, 7.2, 4.2, 5.4, 0, 6.4, -0.2, monsterBodyMaterial);  // hips\n            addTo(body, 8.0, 5.4, 5.8, 0, 10.2, 0.1, monsterBodyMaterial);  // chest\n            addTo(body, 8.2, 2.0, 5.4, 0, 12.6, 0.1, monsterBackMaterial);  // shoulders (darker)\n            for (let i = 0; i < 4; i++) {\n                addTo(body, 4.6 - i * 0.5, 1.5, 1.2, 0, 6.2 + i * 1.9, 2.9, monsterBellyMaterial, false); // belly segments\n            }\n\n            // ----- Arms (pivot at the shoulder) -----\n            [['armL', -1], ['armR', 1]].forEach(([key, s]) => {\n                const arm = new THREE.Group();\n                const sx = s * 4.4, sy = 11.5, sz = 1.0;\n                arm.position.set(sx, sy, sz);\n                const upper = addTo(arm, 1.7, 3.2, 1.7, 0, 10.6 - sy, 1.0 - sz, monsterBodyMaterial);\n                upper.rotation.z = -s * 0.35;\n                const fore = addTo(arm, 1.3, 2.4, 1.3, s * 0.8, 8.6 - sy, 2.2 - sz, monsterBodyMaterial);\n                fore.rotation.x = 0.5;\n                for (let t = -1; t <= 1; t++) {\n                    const claw = new THREE.Mesh(new THREE.ConeGeometry(0.22, 0.9, 5), monsterClawMaterial);\n                    claw.position.set(s * 0.8 + t * 0.4, 7.4 - sy, 3.0 - sz);\n                    claw.rotation.x = Math.PI / 2.2;\n                    arm.add(claw);\n                }\n                body.add(arm);\n                monsterParts[key] = arm;\n            });\n\n            // ----- Neck + head + snout (ride the body) -----\n            addTo(body, 3.4, 2.6, 3.4, 0, 13.6, 1.4, monsterBodyMaterial);  // neck\n            addTo(body, 4.6, 4.0, 4.2, 0, 15.2, 2.2, monsterBodyMaterial);  // head\n            addTo(body, 4.2, 2.4, 3.2, 0, 14.4, 4.6, monsterBodyMaterial);  // snout\n            addTo(body, 4.4, 1.0, 1.2, 0, 16.8, 3.0, monsterBackMaterial);  // brow ridge\n\n            [-1, 1].forEach(s => {\n                const white = new THREE.Mesh(new THREE.SphereGeometry(0.72, 10, 10), monsterEyeWhiteMaterial);\n                white.position.set(s * 1.5, 16.0, 4.0); body.add(white);\n                const pupil = new THREE.Mesh(new THREE.SphereGeometry(0.34, 8, 8), monsterPupilMaterial);\n                pupil.position.set(s * 1.6, 16.0, 4.55); body.add(pupil);\n            });\n\n            addTo(body, 4.0, 0.7, 0.4, 0, 13.6, 5.9, monsterMouthMaterial, false); // mouth interior\n            // Lower jaw (animated for laser breath)\n            const jaw = new THREE.Group();\n            jaw.position.set(0, 13.2, 5.2);\n            const jawMesh = addTo(jaw, 3.8, 0.8, 2.0, 0, 0, 0.8, monsterBodyMaterial);\n            // Teeth on lower jaw\n            for (let t = -1; t <= 1; t++) {\n                const jawTooth = new THREE.Mesh(new THREE.ConeGeometry(0.22, 0.6, 5), monsterToothMaterial);\n                jawTooth.position.set(t * 1.0, 0.5, 1.4);\n                jaw.add(jawTooth);\n            }\n            body.add(jaw);\n            monsterParts.jaw = jaw;\n            for (let t = -1; t <= 1; t++) {\n                const upTooth = new THREE.Mesh(new THREE.ConeGeometry(0.28, 0.9, 5), monsterToothMaterial);\n                upTooth.position.set(t * 1.2, 13.5, 6.0); upTooth.rotation.x = Math.PI; body.add(upTooth);\n                const lowTooth = new THREE.Mesh(new THREE.ConeGeometry(0.24, 0.7, 5), monsterToothMaterial);\n                lowTooth.position.set(t * 1.0 + 0.6, 13.0, 5.9); body.add(lowTooth);\n            }\n            [-1, 1].forEach(s => addTo(body, 0.5, 0.3, 0.3, s * 0.9, 15.2, 6.1, monsterMouthMaterial, false)); // nostrils\n\n            // ----- Tail (pivot at the base so it can sway) -----\n            const tail = new THREE.Group();\n            const TBY = 5.4, TBZ = -3.0;\n            tail.position.set(0, TBY, TBZ);\n            const tailSegs = 6;\n            for (let i = 0; i < tailSegs; i++) {\n                const t = i / (tailSegs - 1);\n                const w = 4.2 - t * 3.4;\n                const segY = (5.4 - Math.sin(t * 2.4) * 1.2 + t * t * 3.2) - TBY;\n                const segZ = (-3.5 - i * 2.7) - TBZ;\n                const seg = addTo(tail, w, w, 3.4, 0, segY, segZ, monsterBodyMaterial);\n                seg.rotation.x = -t * 0.5;\n            }\n            body.add(tail);\n            monsterParts.tail = tail;\n\n            // ----- Dorsal plates (Godzilla fins) along spine + tail (ride the body) -----\n            const plate = (size, y, z, mat) => {\n                const shape = new THREE.Shape();\n                shape.moveTo(-size * 0.55, 0);\n                shape.lineTo(size * 0.55, 0);\n                shape.lineTo(0, size);\n                shape.lineTo(-size * 0.55, 0);\n                const m = new THREE.Mesh(new THREE.ShapeGeometry(shape), mat);\n                m.rotation.y = Math.PI / 2;     // stand up, face sideways\n                m.position.set(0, y, z);\n                m.castShadow = true;\n                body.add(m);\n            };\n            const spine = [\n                [1.4, 13.2, 2.0], [2.2, 13.6, 0.0], [3.0, 13.4, -2.0],\n                [3.2, 12.4, -4.2], [2.8, 11.0, -6.4], [2.3, 9.4, -8.6],\n                [1.8, 8.0, -10.6], [1.3, 7.2, -12.4], [0.9, 7.0, -14.0]\n            ];\n            spine.forEach(p => plate(p[0], p[1], p[2], monsterSpikeMaterial));\n\n            scene.add(monsterGroup);\n            updateMonsterSize();\n        }\n\n        // Walk cycle + footstep-synced stomp. Limb pivots set up in createMonster.\n        function animateMonster(dt, moving) {\n            const p = monsterParts;\n            if (!p.body) return;\n\n            const jumpPhase = player.jumpPhase || 'idle';\n            if (player.jumping || jumpPhase === 'recover') {\n                player.gait += (0 - player.gait) * Math.min(1, dt * 10);\n                player.bodyDip = Math.max(0, player.bodyDip - dt * 7);\n\n                let bodyY = 0;\n                let bodyLean = 0;\n                let legPose = 0;\n                let armPose = 0;\n\n                if (jumpPhase === 'prep') {\n                    const t = smoothstep(Math.min(1, player.jumpT / Math.max(0.001, player.jumpPrepDur)));\n                    bodyY = -1.25 * t;\n                    bodyLean = 0.08 * t;\n                    legPose = 0.42 * t;\n                    armPose = -0.22 * t;\n                } else if (jumpPhase === 'air') {\n                    const jt = Math.min(1, player.jumpT / Math.max(0.001, player.jumpDur));\n                    const hang = Math.sin(jt * Math.PI);\n                    bodyY = 0.35 * hang;\n                    bodyLean = 0.08;\n                    legPose = -0.34 + 0.16 * hang;\n                    armPose = -0.28 - 0.12 * hang;\n                } else if (jumpPhase === 'recover') {\n                    const t = smoothstep(1 - Math.max(0, player.jumpRecoverT) / Math.max(0.001, player.jumpRecoverDur));\n                    bodyY = -1.15 * (1 - t);\n                    bodyLean = 0.06 * (1 - t);\n                    legPose = 0.26 * (1 - t);\n                    armPose = -0.12 * (1 - t);\n                }\n\n                p.body.position.y = bodyY;\n                p.body.rotation.x = bodyLean;\n                p.body.rotation.z = 0;\n                p.legL.rotation.x = legPose;\n                p.legR.rotation.x = legPose;\n                if (p.armL) p.armL.rotation.x = armPose;\n                if (p.armR) p.armR.rotation.x = armPose;\n                if (p.tail && !player.spinning) p.tail.rotation.y = Math.sin(Date.now() * 0.004) * 0.12;\n                return;\n            }\n\n            player.gait += ((moving ? 1 : 0) - player.gait) * Math.min(1, dt * 6);\n            const gait = player.gait;\n            const prevPhase = player.walkPhase;\n            \n            // Check if speed boosted - use running animation\n            const isRunning = speedBoostTimer > 0 && moving;\n            \n            // Walk animation slows down as monster grows - lumbering giant effect\n            // Gradual scaling - animation stays snappy at early levels\n            // Running animation is much faster\n            const sizeRatio = player.size / STARTING_SIZE;\n            const animSpeedScale = 1 / (1 + (sizeRatio - 1) * 0.12); // Very gentle slowdown\n            const baseAnimSpeed = isRunning ? 16 : 8.5; // Running is nearly 2x faster animation\n            const walkAnimSpeed = baseAnimSpeed * Math.max(0.35, animSpeedScale); // Floor at 35% speed\n            if (moving) player.walkPhase += dt * walkAnimSpeed;\n            const ph = player.walkPhase;\n\n            // Legs swing out of phase about the hips.\n            // Bigger monsters have slightly smaller leg swing amplitude (more controlled steps)\n            // Running has larger, more exaggerated leg swings\n            const baseLegSwing = isRunning ? 0.85 : 0.55;\n            const legSwing = baseLegSwing * Math.max(0.7, animSpeedScale);\n            p.legL.rotation.x = Math.sin(ph) * legSwing * gait;\n            p.legR.rotation.x = Math.sin(ph + Math.PI) * legSwing * gait;\n\n            // Arms counter-swing (smaller for big monsters - they lumber more than walk)\n            // Running has bigger arm swings\n            const baseArmSwing = isRunning ? 0.55 : 0.3;\n            const armSwing = baseArmSwing * Math.max(0.6, animSpeedScale);\n            if (p.armL) p.armL.rotation.x = Math.sin(ph + Math.PI) * armSwing * gait;\n            if (p.armR) p.armR.rotation.x = Math.sin(ph) * armSwing * gait;\n\n            // Body bob + lean + roll; dip down on each stomp.\n            // Bigger monsters have heavier, slower body dip recovery\n            // Running has more pronounced body movement\n            const dipRecovery = 6 * Math.max(0.5, animSpeedScale);\n            player.bodyDip = Math.max(0, player.bodyDip - dt * dipRecovery);\n            const bob = (1 - Math.cos(ph * 2)) * 0.5; // 0..1\n            // Bigger monsters bob less but dip more heavily\n            // Running bobs more\n            const bobScale = Math.max(0.5, animSpeedScale);\n            const bobAmount = isRunning ? 1.4 : 0.9;\n            p.body.position.y = bob * bobAmount * gait * bobScale - player.bodyDip;\n            \n            // Running leans forward more\n            const baseLean = isRunning ? 0.12 : 0.04;\n            p.body.rotation.x = baseLean * gait + player.bodyDip * 0.06;\n            \n            // More pronounced roll during running\n            const rollAmount = isRunning ? 0.09 : 0.05;\n            p.body.rotation.z = Math.sin(ph) * rollAmount * gait * bobScale;\n\n            // Tail sways (walk-driven while moving, gentle idle otherwise).\n            // Running tail bounces more dramatically\n            const time = Date.now() * 0.001;\n            const tailSwayAmount = isRunning ? 0.45 : 0.28;\n            p.tail.rotation.y = Math.sin(moving ? ph * 0.6 : time * 1.4) * tailSwayAmount;\n\n            // Footstep + stomp emphasis when a foot crosses its plant point.\n            // Running has more frequent footsteps\n            if (!player.jumping && moving && gait > 0.25) {\n                if (Math.floor(ph / Math.PI) > Math.floor(prevPhase / Math.PI)) {\n                    const stepIndex = Math.floor(ph / Math.PI);\n                    const footSide = (stepIndex % 2 === 0) ? -1 : 1;\n                    doFootstep(footSide);\n                    player.bodyDip = isRunning ? 0.25 : 0.18;\n                }\n            }\n        }\n\n        function getCrushRadius() { return player.crushBonus * (BODY_HALF_WIDTH * player.size + 2.5); }\n        function getMonsterTop() { return MONSTER_TOP_UNIT * player.size; }\n\n        function recalcSize() {\n            // Size now only changes at level up, not from mass\n            player.targetSize = STARTING_SIZE + (gameState.level - 1) * 0.18;\n        }\n\n        function addMass(m) {\n            if (m <= 0) return;\n            player.mass += m * player.massGain;\n            // Size no longer tied to mass - only grows at level up\n            player.growthPulse = Math.min(0.1, player.growthPulse + 0.015);\n        }\n\n        function updateMonsterSize() {\n            monsterGroup.scale.set(player.size, player.size, player.size);\n        }\n\n        function updateMonster(dt) {\n            let moveX = 0, moveZ = 0;\n            if (joystick.active) {\n                moveX = joystick.x;\n                moveZ = joystick.y;\n            } else {\n                if (keys['w'] || keys['arrowup']) moveZ = -1;\n                if (keys['s'] || keys['arrowdown']) moveZ = 1;\n                if (keys['a'] || keys['arrowleft']) moveX = -1;\n                if (keys['d'] || keys['arrowright']) moveX = 1;\n            }\n\n            const len = Math.sqrt(moveX * moveX + moveZ * moveZ);\n            // Movement speed is constant in world units - the camera zoom-out at larger\n            // sizes naturally makes the monster APPEAR slower on screen (lumbering giant feel)\n            // Speed boost multiplies speed by 2.5x\n            const speedBoostMul = speedBoostTimer > 0 ? 2.5 : 1;\n            const speed = player.baseSpeed * player.speedMul * speedBoostMul;\n            let moving = false;\n            const jumpPhase = player.jumpPhase || 'idle';\n            const jumpLocksWalk = player.jumping || jumpPhase === 'recover';\n            const jumpGroundLock = (player.jumping && jumpPhase === 'prep') || jumpPhase === 'recover';\n            const jumpAir = player.jumping && jumpPhase === 'air';\n\n            if (len > 0 && !player.spinning && !player.dashing && !jumpLocksWalk) {\n                moving = true;\n                moveX /= len; moveZ /= len;\n                player.vx = moveX * speed;\n                player.vz = moveZ * speed;\n\n                const targetRot = Math.atan2(moveX, moveZ);\n                let diff = targetRot - player.rotation;\n                if (diff > Math.PI) diff -= Math.PI * 2;\n                if (diff < -Math.PI) diff += Math.PI * 2;\n                player.rotation += diff * Math.min(1, dt * 10);\n            } else if (!player.spinning && !player.dashing) {\n                if (jumpGroundLock) {\n                    const groundBrake = Math.max(0, 1 - dt * 9);\n                    player.vx *= groundBrake;\n                    player.vz *= groundBrake;\n                } else if (jumpAir) {\n                    const airDrag = Math.max(0.96, 1 - dt * 0.35);\n                    player.vx *= airDrag;\n                    player.vz *= airDrag;\n                } else {\n                    player.vx *= 0.85;\n                    player.vz *= 0.85;\n                }\n            } else {\n                if (jumpAir && player.dashing && !player.spinning) {\n                    const airDrag = Math.max(0.96, 1 - dt * 0.35);\n                    player.vx *= airDrag;\n                    player.vz *= airDrag;\n                } else {\n                    // During spin or ground dash, slow down but don't stop completely\n                    player.vx *= 0.92;\n                    player.vz *= 0.92;\n                }\n            }\n\n            player.x += player.vx * dt;\n            player.z += player.vz * dt;\n            // No world bounds \u2014 the map is infinite (chunk-streamed).\n\n            // Jump Stomp: crouch/load, airborne leap, then a short landing recovery.\n            if (player.jumping) {\n                if (player.jumpPhase === 'prep') {\n                    player.jumpT += dt;\n                    player.y = 0;\n                    if (player.jumpT >= player.jumpPrepDur) {\n                        const d = player.jumpData;\n                        player.jumpPhase = 'air';\n                        player.jumpT = 0;\n                        player.vx = d.launchVx || 0;\n                        player.vz = d.launchVz || 0;\n                        addDustRing(player.x, player.z, getCrushRadius() * 0.7, 0xcfc3a8);\n                        addCameraShake(0.18);\n                    }\n                } else {\n                    player.jumpPhase = 'air';\n                    player.jumpT += dt;\n                    const jt = Math.min(1, player.jumpT / player.jumpDur);\n                    // Asymmetric arc: slower ascent (82%), much faster descent (18%) for dramatic slam.\n                    let heightFactor;\n                    if (jt < 0.82) {\n                        const ascendT = jt / 0.82;\n                        heightFactor = Math.sin(ascendT * Math.PI / 2);\n                    } else {\n                        const descendT = (jt - 0.82) / 0.18;\n                        heightFactor = Math.cos(descendT * Math.PI / 2);\n                    }\n                    player.y = heightFactor * player.jumpData.height;\n                    if (player.jumpT >= player.jumpDur) {\n                        player.jumping = false;\n                        player.jumpPhase = 'recover';\n                        player.jumpRecoverT = player.jumpRecoverDur;\n                        player.y = 0;\n                        player.vx *= 0.35;\n                        player.vz *= 0.35;\n                        player.bodyDip = Math.max(player.bodyDip, 0.35);\n                        const d = player.jumpData;\n                        addShockwave(player.x, player.z);\n                        addGroundDamageEffect(player.x, player.z, d.radius);\n                        // Big red impact ring for landing\n                        addDustRing(player.x, player.z, d.radius * 1.4, 0xff3030);\n                        addDustRing(player.x, player.z, d.radius, 0xffe06a);\n                        aoeDamage(player.x, player.z, d.radius, d.dmg);\n                        const landingSizeRatio = Math.max(1, player.size / STARTING_SIZE);\n                        const leapShake = Math.min(0.25, (d.leapDistance || 0) * 0.004);\n                        addCameraShake(Math.min(1.5, 0.75 + (landingSizeRatio - 1) * 0.08 + leapShake));\n                        playSound('building_collapse_sound', 0.6);\n                    }\n                }\n            } else if (player.jumpPhase === 'recover') {\n                player.jumpRecoverT -= dt;\n                player.y = 0;\n                if (player.jumpRecoverT <= 0) {\n                    player.jumpPhase = 'idle';\n                    player.jumpData = null;\n                }\n            } else {\n                player.y = 0;\n            }\n\n            // Regen\n            if (player.regenRate > 0) {\n                player.health = Math.min(player.maxHealth, player.health + player.regenRate * dt);\n            }\n\n            // Update stepped growth animation (handles size changes during level up)\n            updateGrowthAnimation(dt);\n            \n            // Normal smooth growth when not in stepped animation\n            if (!growthAnimActive) {\n                player.size += (player.targetSize - player.size) * Math.min(1, dt * 2.2);\n            }\n\n            // Bigger = tankier; growing also heals.\n            const maxFromSize = 100 * (1 + (player.size - STARTING_SIZE) * 0.45);\n            const baseMul = player.maxHealth / Math.max(1, player._prevMaxFromSize || maxFromSize);\n            if (player._prevMaxFromSize === 0) {\n                player._prevMaxFromSize = maxFromSize;\n            } else if (maxFromSize > player._prevMaxFromSize) {\n                const delta = (maxFromSize - player._prevMaxFromSize) * baseMul;\n                player.maxHealth += delta;\n                player.health = Math.min(player.maxHealth, player.health + delta);\n                player._prevMaxFromSize = maxFromSize;\n            }\n\n            // Apply transform with breathing effect\n            const breath = 1 + Math.sin(Date.now() * 0.003) * 0.02;\n            monsterGroup.position.set(player.x, player.y, player.z);\n            monsterGroup.rotation.y = player.rotation;\n            monsterGroup.scale.set(player.size * breath, player.size, player.size * breath);\n\n            // Update buff visual effects on monster\n            updateMonsterBuffEffects(dt);\n            \n            animateMonster(dt, moving);\n        }\n\n        function doFootstep(footSide) {\n            const cr = getCrushRadius();\n            const forwardX = Math.sin(player.rotation);\n            const forwardZ = Math.cos(player.rotation);\n            const rightX = Math.cos(player.rotation);\n            const rightZ = -Math.sin(player.rotation);\n            const side = footSide || 0;\n            const footSpread = Math.max(1.2, 2.3 * player.size);\n            const fx = player.x + forwardX * cr * 0.34 + rightX * footSpread * side;\n            const fz = player.z + forwardZ * cr * 0.34 + rightZ * footSpread * side;\n            \n            // Bigger monsters create bigger dust rings and heavier shakes\n            const sizeRatio = player.size / STARTING_SIZE;\n            const footDamageRadius = Math.max(2.2, cr * 0.55);\n            addGroundDamageEffect(fx, fz, footDamageRadius, {\n                minCrater: Math.max(0.9, player.size * 2.2),\n                craterScale: 0.34,\n                crackMin: 3,\n                crackMax: speedBoostTimer > 0 ? 6 : 5,\n                crackDensity: 0.45,\n                segmentMin: 1,\n                segmentRand: 2,\n                debrisCount: speedBoostTimer > 0 ? 2 : 1,\n                life: 0.95,\n                fadeTime: 0.45,\n                grow: 0.06,\n                craterOpacity: 0.42,\n                crackOpacity: 0.56,\n                debrisColor: 0x75624d\n            });\n            addDustRing(fx, fz, cr * (1 + sizeRatio * 0.15), 0xcfc3a8);\n            \n            // More screen shake for giant footsteps - that earthquake feel!\n            const shakeIntensity = Math.min(0.45, 0.05 + player.size * 0.025);\n            addCameraShake(shakeIntensity);\n            \n            // Louder footsteps for bigger monsters\n            const volume = Math.min(0.55, 0.2 + sizeRatio * 0.08);\n            playSound('footstep_sound', volume);\n            \n            // Heavier body dip for giant monsters\n            player.bodyDip = Math.max(player.bodyDip, 0.15 + sizeRatio * 0.04);\n            \n            // Speed boost trail effect - green dust when running fast\n            if (speedBoostTimer > 0) {\n                addDustRing(fx, fz, cr * 0.8, 0x00ff88);\n            }\n        }\n        \n        // Monster buff aura meshes\n        let invincibilityAura = null;\n        \n        function updateMonsterBuffEffects(dt) {\n            const time = Date.now() * 0.003;\n            \n            // Invincibility aura (golden shield) - shown for star power\n            if (invincibilityTimer > 0) {\n                if (!invincibilityAura) {\n                    const auraMat = new THREE.MeshBasicMaterial({\n                        color: 0xffd700,\n                        transparent: true,\n                        opacity: 0.3,\n                        side: THREE.DoubleSide\n                    });\n                    invincibilityAura = new THREE.Mesh(\n                        new THREE.SphereGeometry(1, 16, 12),\n                        auraMat\n                    );\n                    scene.add(invincibilityAura);\n                }\n                \n                // Position and scale aura around monster\n                const auraScale = player.size * 14;\n                invincibilityAura.position.set(player.x, player.y + player.size * 8, player.z);\n                invincibilityAura.scale.set(auraScale, auraScale * 1.2, auraScale);\n                \n                // Pulsing effect - more urgent when time is running low\n                const urgency = invincibilityTimer < 3 ? 2 : 1;\n                const pulse = 0.25 + Math.sin(time * 4 * urgency) * 0.15;\n                invincibilityAura.material.opacity = pulse;\n                \n                // Slight rotation\n                invincibilityAura.rotation.y += dt * 1.5;\n                \n                // Flash warning when about to expire\n                if (invincibilityTimer < 3) {\n                    invincibilityAura.material.color.setHex(\n                        Math.sin(time * 8) > 0 ? 0xffd700 : 0xffaa00\n                    );\n                }\n            } else if (invincibilityAura) {\n                scene.remove(invincibilityAura);\n                invincibilityAura.material.dispose();\n                invincibilityAura.geometry.dispose();\n                invincibilityAura = null;\n            }\n            \n            // Speed boost aura removed - star power only shows golden shield\n        }\n\n        // ==================== CRUSH (collision destruction) ====================\n        function updateCrush(dt) {\n            if (player.y > 6) return; // airborne mid Jump Stomp \u2014 the slam handles destruction\n            const cr = getCrushRadius();\n\n            // Building collision with bump-back effect\n            buildings.forEach(b => {\n                if (b.dead) return;\n                if (b.hitCd > 0) b.hitCd -= dt;\n                \n                // Calculate building bounds (rectangle collision)\n                const halfW = b.width / 2 + cr * 0.6;\n                const halfD = b.depth / 2 + cr * 0.6;\n                const bCenterX = b.x;\n                const bCenterZ = b.z;\n                \n                // Check if monster overlaps building\n                const dx = player.x - bCenterX;\n                const dz = player.z - bCenterZ;\n                \n                if (Math.abs(dx) < halfW && Math.abs(dz) < halfD) {\n                    if (player.dashing) {\n                        damageBuilding(b, 99999);\n                        return;\n                    }\n                    // Collision detected! Calculate push-out direction\n                    const overlapX = halfW - Math.abs(dx);\n                    const overlapZ = halfD - Math.abs(dz);\n                    const impactSpeed = Math.hypot(player.vx, player.vz);\n                    \n                    // Push out along the axis with smallest overlap (feels more natural)\n                    const bumpStrength = 1.8 + player.size * 0.5; // Bump force\n                    \n                    if (overlapX < overlapZ) {\n                        // Push out on X axis\n                        const pushDir = dx > 0 ? 1 : -1;\n                        player.x += pushDir * overlapX * bumpStrength * dt * 60;\n                        player.vx = pushDir * Math.abs(player.vx) * 0.3; // Slight bounce velocity\n                    } else {\n                        // Push out on Z axis\n                        const pushDir = dz > 0 ? 1 : -1;\n                        player.z += pushDir * overlapZ * bumpStrength * dt * 60;\n                        player.vz = pushDir * Math.abs(player.vz) * 0.3;\n                    }\n                    \n                    // Deal damage on collision (with cooldown)\n                    if (b.hitCd <= 0) {\n                        damageBuilding(b, 22 + player.size * 18);\n                        b.hitCd = 0.35; // Slightly longer cooldown for bump feel\n                        \n                        // Add wall impact effect - visual feedback on the building wall\n                        addWallImpact(b.x, b.z, b.width, b.depth, b.height, player.x, player.z);\n                        \n                        // Body-to-building hits get heavier with size and speed, but are capped\n                        // and cooldown-gated so they stay cheap and readable.\n                        const speedRatio = Math.min(1.6, impactSpeed / Math.max(1, player.baseSpeed * player.speedMul));\n                        const impactShake = Math.min(0.48, 0.1 + player.size * 0.035 + speedRatio * 0.08);\n                        addCameraShake(impactShake);\n                        \n                        // Brief body dip animation on impact\n                        player.bodyDip = Math.max(player.bodyDip, 0.12);\n                    }\n                }\n            });\n\n            for (let i = props.length - 1; i >= 0; i--) {\n                const p = props[i];\n                if (p.dead) continue;\n                const pr = Math.max(p.width, p.depth) / 2;\n                if (Math.hypot(p.x - player.x, p.z - player.z) < cr + pr) destroyProp(p);\n            }\n\n            for (let i = people.length - 1; i >= 0; i--) {\n                const pp = people[i];\n                if (pp.dead) continue;\n                if (Math.hypot(pp.x - player.x, pp.z - player.z) < cr + 0.9) destroyPerson(pp);\n            }\n\n            for (let i = enemies.length - 1; i >= 0; i--) {\n                const e = enemies[i];\n                if (e.dead) continue;\n                const d = Math.hypot(e.x - player.x, e.z - player.z);\n                if (e.type === 'helicopter') {\n                    if (d < cr + 4 && getMonsterTop() >= e.y - 2) destroyEnemy(e);\n                } else if (d < cr + 4) {\n                    destroyEnemy(e);\n                }\n            }\n            \n            // Boss crush damage (player walks/dashes into boss)\n            if (boss && !boss.dead) {\n                const bossContactR = cr + boss.size * 5;\n                const bDist = Math.hypot(boss.x - player.x, boss.z - player.z);\n                if (bDist < bossContactR && boss.contactCd <= 0) {\n                    const crushDmg = (22 + player.size * 18) * player.crushBonus;\n                    damageBoss(crushDmg);\n                    if (!boss || boss.dead) return;\n                    boss.contactCd = 0.4;\n                    \n                    // Calculate impact point between the two titans\n                    const crushNx = (boss.x - player.x) / bDist;\n                    const crushNz = (boss.z - player.z) / bDist;\n                    const clashX = player.x + crushNx * (bDist * 0.5);\n                    const clashZ = player.z + crushNz * (bDist * 0.5);\n                    const clashY = Math.min(getMonsterTop(), boss.size * 10) * 0.5;\n                    \n                    // Heavy mutual knockback - titans bounce apart!\n                    const crushKnock = 40 + player.size * 10;\n                    player.vx = -crushNx * crushKnock;\n                    player.vz = -crushNz * crushKnock;\n                    player.bodyDip = Math.max(player.bodyDip, 0.3);\n                    if (!boss.dashing && !boss.jumping) {\n                        const bossCrushKnock = 55 + player.size * 12;\n                        boss.kbvx = crushNx * bossCrushKnock;\n                        boss.kbvz = crushNz * bossCrushKnock;\n                    }\n                    \n                    // === VFX: Titan Clash ===\n                    addCameraShake(0.5);\n                    \n                    // Sparks burst at collision point\n                    for (let sp = 0; sp < 12; sp++) {\n                        const sparkAngle = (sp / 12) * Math.PI * 2 + Math.random() * 0.5;\n                        const sparkSpeed = 18 + Math.random() * 25;\n                        const sparkMat = new THREE.MeshBasicMaterial({ color: Math.random() < 0.4 ? 0xffdd44 : 0xffffff, transparent: true });\n                        const spark = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), sparkMat);\n                        spark.position.set(clashX + (Math.random()-0.5)*2, clashY + (Math.random()-0.5)*3, clashZ + (Math.random()-0.5)*2);\n                        scene.add(spark);\n                        particles.push({\n                            mesh: spark,\n                            vx: Math.cos(sparkAngle) * sparkSpeed,\n                            vy: 8 + Math.random() * 15,\n                            vz: Math.sin(sparkAngle) * sparkSpeed,\n                            lifetime: 0.3 + Math.random() * 0.2\n                        });\n                    }\n                    \n                    // Ground shockwave\n                    const shockMat2 = new THREE.MeshBasicMaterial({ color: 0xffcc22, transparent: true, opacity: 0.7, side: THREE.DoubleSide });\n                    const shockRing2 = new THREE.Mesh(new THREE.RingGeometry(1, 2.5, 28), shockMat2);\n                    shockRing2.rotation.x = -Math.PI / 2;\n                    shockRing2.position.set(clashX, 0.5, clashZ);\n                    scene.add(shockRing2);\n                    particles.push({ mesh: shockRing2, vx: 0, vy: 0, vz: 0, lifetime: 0.4, isRing: true, ringMax: 30 + player.size * 6, ringAge: 0, ringMat: shockMat2 });\n                    \n                    // Dust clouds\n                    addDustRing(clashX, clashZ, 15 + player.size * 4, 0xccbb88);\n                    \n                    // Flash at impact\n                    const flashMat2 = new THREE.MeshBasicMaterial({ color: 0xffffee, transparent: true, opacity: 0.85, side: THREE.DoubleSide });\n                    const flashRing2 = new THREE.Mesh(new THREE.RingGeometry(0.4, 1.5, 16), flashMat2);\n                    flashRing2.position.set(clashX, clashY, clashZ);\n                    flashRing2.lookAt(camera.position);\n                    scene.add(flashRing2);\n                    particles.push({ mesh: flashRing2, vx: 0, vy: 0, vz: 0, lifetime: 0.2, isRing: true, ringMax: 12, ringAge: 0, ringMat: flashMat2 });\n                    \n                    playSound('building_hit_sound', 0.55);\n                    playSound('building_collapse_sound', 0.25);\n                }\n            }\n        }\n\n        function grantRewards(obj) {\n            const sc = Math.round((obj.scoreValue || 0) * player.scoreMultiplier);\n            gameState.score += sc;\n            collectXP(obj.xpValue || 0);\n            addMass(obj.mass || 0);\n            return sc;\n        }\n\n        // ==================== DESTRUCTION ====================\n        // Buildings take several bumps: each hit leans + sheds debris, collapse at hp<=0.\n        function damageBuilding(b, dmg) {\n            if (b.dead) return;\n            // Star power = one-shot destruction!\n            if (invincibilityTimer > 0) {\n                b.hp = 0;\n            } else {\n                b.hp -= dmg * player.crushBonus;\n            }\n            const frac = 1 - Math.max(0, b.hp) / b.maxHp;\n            b.mesh.rotation.z = b.tiltDir * frac * 0.14;\n            b.mesh.rotation.x = b.tiltDir2 * frac * 0.09;\n            addCameraShake(0.12);\n            for (let i = 0; i < 2; i++) {\n                createDebris(b.x + (Math.random() - 0.5) * b.width, b.height * (0.3 + Math.random() * 0.5), b.z + (Math.random() - 0.5) * b.depth, b.debrisColor);\n            }\n            playSound('building_hit_sound', 0.22);\n            if (b.hp <= 0) destroyBuilding(b);\n        }\n\n        function destroyBuilding(b) {\n            if (b.dead) return;\n            b.dead = true;\n            b.crushing = true;\n            b.crushTimer = 0.18;\n\n            // Hide tutorial once player destroys any building (they figured it out!)\n            if (tutorialActive) {\n                hideTutorial();\n            }\n\n            const sc = grantRewards(b);\n            gameState.buildingsDestroyed++;\n            bumpCombo();\n\n            const debrisCount = Math.min(6, 3 + Math.floor(b.height / 12));\n            for (let i = 0; i < debrisCount; i++) createDebris(b.x, b.height * 0.4, b.z, b.debrisColor);\n            addDustRing(b.x, b.z, Math.max(b.width, b.depth) * 0.9, 0xc8bca0);\n            addCameraShake(Math.min(0.6, 0.18 + b.height * 0.004));\n            addFloatingText(b.x, b.height, b.z, '+' + sc);\n            playSound('building_collapse_sound', 0.3);\n            playSound('building_hit_sound', 0.2);\n        }\n\n        function destroyProp(p) {\n            if (p.dead) return;\n            p.dead = true;\n            const sc = grantRewards(p);\n            bumpCombo();\n            const col = p.type === 'tree' ? 0x2e7d32 : 0xcccccc;\n            createDebris(p.x, 2, p.z, col);\n            createDebris(p.x, 2, p.z, col);\n            addDustRing(p.x, p.z, 4, 0xd8cdb0);\n            if (p.type === 'car') {\n                addFloatingText(p.x, 3, p.z, '+' + sc);\n                playSound('enemy_explosion_sound', 0.25);\n            }\n            removeFromScene(p.mesh);\n        }\n\n        function bumpCombo() {\n            combo++;\n            // Balanced combo timer: starts at 1.5s and decays slightly as combo increases, but floor it at a playable 0.8s\n            comboTimer = Math.max(0.8, 1.5 - combo * 0.01);\n        }\n\n        function destroyPerson(pp) {\n            if (pp.dead) return;\n            pp.dead = true;\n            grantRewards(pp);\n            bumpCombo();\n            createDebris(pp.x, 1.2, pp.z, 0xd8cdb0);\n            playSound('eating_sound', 0.2);\n            // Hand the mesh to the squash animator (people array drops it next frame).\n            squashed.push({ mesh: pp.mesh, t: 0 });\n        }\n\n        // Flatten squashed people into the ground, then remove; also decays the combo.\n        function updateSquashed(dt) {\n            for (let i = squashed.length - 1; i >= 0; i--) {\n                const s = squashed[i];\n                s.t += dt;\n                const k = Math.min(1, s.t / 0.12);\n                s.mesh.scale.set(1 + 0.5 * k, Math.max(0.08, 1 - k * 0.92), 1 + 0.5 * k);\n                if (s.t >= 0.2) { removeFromScene(s.mesh); squashed.splice(i, 1); }\n            }\n            if (comboTimer > 0) {\n                comboTimer -= dt;\n                if (comboTimer <= 0) combo = 0;\n            }\n        }\n\n        function destroyEnemy(e) {\n            if (e.dead) return;\n            e.dead = true;\n            // Remove helicopter ground spotlight\n            if (e.mesh.userData.spotlight) {\n                removeFromScene(e.mesh.userData.spotlight);\n            }\n            const sc = grantRewards(e);\n            gameState.enemiesDefeated++;\n            bumpCombo();\n            for (let i = 0; i < 6; i++) createDebris(e.x, e.type === 'helicopter' ? e.y : 2, e.z, 0x444038);\n            addDustRing(e.x, e.z, 6, 0xb8ac90);\n            addFloatingText(e.x, e.type === 'helicopter' ? e.y + 2 : 4, e.z, '+' + sc);\n            addCameraShake(0.25);\n            \n            // Play different explosion sounds based on enemy type\n            if (e.type === 'tank') {\n                playSound('tank_explosion_sound', 0.5);\n            } else if (e.type === 'helicopter') {\n                playSound('helicopter_explosion_sound', 0.5);\n            } else {\n                // Police cars and other enemies use generic explosion\n                playSound('enemy_explosion_sound', 0.5);\n            }\n            \n            removeFromScene(e.mesh);\n        }\n\n        // ==================== ABILITIES (auto-fire powers) ====================\n        function updateAbilities(dt) {\n            for (let k = 0; k < upgradeDefs.length; k++) {\n                const def = upgradeDefs[k];\n                if (!def.fire) continue;\n                const lvl = player.upgrades[def.id] || 0;\n                if (lvl <= 0) continue;\n                player.abilityCd[def.id] = (player.abilityCd[def.id] || 0) - dt;\n                if (player.abilityCd[def.id] <= 0) {\n                    def.fire(lvl);\n                    player.abilityCd[def.id] = def.cooldown(lvl) * player.cooldownMul;\n                }\n            }\n        }\n\n        // Damage helpers reuse the existing destruction functions.\n        function aoeDamage(x, z, radius, dmg) {\n            buildings.forEach(b => { if (!b.dead && Math.hypot(b.x - x, b.z - z) < radius + Math.max(b.width, b.depth) / 2) damageBuilding(b, dmg); });\n            for (let i = props.length - 1; i >= 0; i--) { const p = props[i]; if (!p.dead && Math.hypot(p.x - x, p.z - z) < radius) destroyProp(p); }\n            for (let i = people.length - 1; i >= 0; i--) { const pp = people[i]; if (!pp.dead && Math.hypot(pp.x - x, pp.z - z) < radius) destroyPerson(pp); }\n            for (let i = enemies.length - 1; i >= 0; i--) { const e = enemies[i]; if (!e.dead && Math.hypot(e.x - x, e.z - z) < radius) destroyEnemy(e); }\n            damageBossFromAoE(x, z, radius, dmg);\n        }\n        function lineDamage(x, z, dx, dz, length, halfW, dmg) {\n            const test = (ox, oz, r) => {\n                const t = (ox - x) * dx + (oz - z) * dz;\n                if (t < 0 || t > length) return false;\n                return Math.hypot(ox - (x + dx * t), oz - (z + dz * t)) < halfW + r;\n            };\n            buildings.forEach(b => { if (!b.dead && test(b.x, b.z, Math.max(b.width, b.depth) / 2)) damageBuilding(b, dmg); });\n            for (let i = props.length - 1; i >= 0; i--) { const p = props[i]; if (!p.dead && test(p.x, p.z, 2)) destroyProp(p); }\n            for (let i = people.length - 1; i >= 0; i--) { const pp = people[i]; if (!pp.dead && test(pp.x, pp.z, 1)) destroyPerson(pp); }\n            for (let i = enemies.length - 1; i >= 0; i--) { const e = enemies[i]; if (!e.dead && test(e.x, e.z, 3)) destroyEnemy(e); }\n            if (boss && !boss.dead && test(boss.x, boss.z, boss.size * 5)) damageBoss(dmg);\n        }\n        function coneDamage(x, z, dx, dz, range, cosHalf, dmg) {\n            const test = (ox, oz) => {\n                const vx = ox - x, vz = oz - z, d = Math.hypot(vx, vz);\n                if (d > range) return false;\n                if (d < 0.001) return true;\n                return (vx / d) * dx + (vz / d) * dz >= cosHalf;\n            };\n            buildings.forEach(b => { if (!b.dead && test(b.x, b.z)) damageBuilding(b, dmg); });\n            for (let i = props.length - 1; i >= 0; i--) { const p = props[i]; if (!p.dead && test(p.x, p.z)) destroyProp(p); }\n            for (let i = people.length - 1; i >= 0; i--) { const pp = people[i]; if (!pp.dead && test(pp.x, pp.z)) destroyPerson(pp); }\n            for (let i = enemies.length - 1; i >= 0; i--) { const e = enemies[i]; if (!e.dead && test(e.x, e.z)) destroyEnemy(e); }\n            if (boss && !boss.dead && test(boss.x, boss.z)) damageBoss(dmg);\n        }\n\n        function fireLaser(level) {\n            // Limit active lasers for performance\n            if (activeLasers.length >= 2) return;\n            // Create a persistent laser beam\n            const duration = 1.0 + level * 0.6; // Longer duration at higher levels (was 0.8 + level * 0.4)\n            const dps = 220 + level * 100; // Damage per second\n            const range = 100 + level * 35; // Longer range at higher levels (was 100 + level * 25)\n            const beamWidth = 3.5 + level * 0.8;\n            \n            // Create beam mesh (cylinder laid horizontal)\n            const beamMat = laserMaterial.clone();\n            beamMat.transparent = true;\n            const beam = new THREE.Mesh(\n                new THREE.CylinderGeometry(beamWidth, beamWidth * 0.7, 1, 8),\n                beamMat\n            );\n            scene.add(beam);\n            \n            // Glow effect around beam\n            const glowMat = new THREE.MeshBasicMaterial({ \n                color: 0xff6030, \n                transparent: true, \n                opacity: 0.3 \n            });\n            const glow = new THREE.Mesh(\n                new THREE.CylinderGeometry(beamWidth * 1.8, beamWidth * 1.4, 1, 8),\n                glowMat\n            );\n            scene.add(glow);\n            \n            activeLasers.push({\n                beam,\n                glow,\n                beamMat,\n                glowMat,\n                duration,\n                timeLeft: duration,\n                dps,\n                range,\n                level,\n                beamWidth,\n                damageTimer: 0,\n                scorchTimer: 0,\n                currentTarget: null\n            });\n            \n            addCameraShake(0.3);\n            playSound('enemy_shoot_sound', 0.6);\n        }\n        \n        function findClosestEnemy(x, z, range, exclude) {\n            let closest = null;\n            let closestDist = range;\n            for (const e of enemies) {\n                if (e.dead || e === exclude) continue;\n                const dist = Math.hypot(e.x - x, e.z - z);\n                if (dist < closestDist) {\n                    closestDist = dist;\n                    closest = e;\n                }\n            }\n            return closest;\n        }\n        \n        function updateActiveLasers(dt) {\n            // Animate mouth open/close based on active lasers\n            if (monsterParts.jaw) {\n                const targetJawAngle = activeLasers.length > 0 ? -0.6 : 0; // Open when firing\n                monsterParts.jaw.rotation.x += (targetJawAngle - monsterParts.jaw.rotation.x) * Math.min(1, dt * 12);\n            }\n            \n            for (let i = activeLasers.length - 1; i >= 0; i--) {\n                const laser = activeLasers[i];\n                laser.timeLeft -= dt;\n                laser.damageTimer -= dt;\n                \n                // Remove expired laser\n                if (laser.timeLeft <= 0) {\n                    removeFromScene(laser.beam);\n                    removeFromScene(laser.glow);\n                    activeLasers.splice(i, 1);\n                    continue;\n                }\n                \n                // Calculate mouth position in world space\n                // Mouth is at approximately Y=13.6, Z=5.9 in local monster coordinates\n                const mouthLocalY = 14.5 * player.size; // Mouth height scaled by monster size\n                const mouthForwardOffset = 6.5 * player.size; // How far forward the mouth is\n                \n                // Get the direction the monster is facing\n                const faceDirX = Math.sin(player.rotation);\n                const faceDirZ = Math.cos(player.rotation);\n                \n                // Position laser origin at the mouth (includes player.y for jumping)\n                const startX = player.x + faceDirX * mouthForwardOffset;\n                const startZ = player.z + faceDirZ * mouthForwardOffset;\n                const startY = player.y + mouthLocalY; // Includes jump height!\n                \n                // Fire in monster's facing direction only\n                const dx = faceDirX;\n                const dz = faceDirZ;\n                const endX = startX + dx * laser.range;\n                const endZ = startZ + dz * laser.range;\n                const endY = startY; // Beam stays level from mouth\n                \n                // Damage everything in the beam path (enemies, buildings, props, people).\n                // Use the monster's ground center for gameplay so a huge monster's forward\n                // mouth offset does not skip tanks or police cars under the visible beam.\n                if (laser.damageTimer <= 0) {\n                    const dmg = laser.dps * 0.15;\n                    lineDamage(player.x, player.z, dx, dz, laser.range + mouthForwardOffset, laser.beamWidth, dmg);\n                    laser.damageTimer = 0.08;\n                }\n\n                laser.scorchTimer -= dt;\n                if (laser.scorchTimer <= 0) {\n                    const t = 0.18 + Math.random() * 0.72;\n                    const sideJitter = (Math.random() - 0.5) * laser.beamWidth * 1.4;\n                    const sideX = Math.cos(player.rotation);\n                    const sideZ = -Math.sin(player.rotation);\n                    addLaserScorchMark(\n                        startX + dx * laser.range * t + sideX * sideJitter,\n                        startZ + dz * laser.range * t + sideZ * sideJitter,\n                        player.rotation,\n                        laser.beamWidth\n                    );\n                    laser.scorchTimer = 0.13;\n                }\n                 \n                // Update beam visual - stretch from monster to end point\n                const beamDx = endX - startX;\n                const beamDz = endZ - startZ;\n                const beamDy = endY - startY;\n                const length = Math.sqrt(beamDx * beamDx + beamDy * beamDy + beamDz * beamDz);\n                \n                // Position beam at midpoint\n                laser.beam.position.set(\n                    startX + beamDx / 2,\n                    startY + beamDy / 2,\n                    startZ + beamDz / 2\n                );\n                laser.glow.position.copy(laser.beam.position);\n                \n                // Scale beam to correct length\n                laser.beam.scale.set(1, length, 1);\n                laser.glow.scale.set(1, length, 1);\n                \n                // Rotate beam to point in facing direction\n                laser.beam.lookAt(endX, endY, endZ);\n                laser.beam.rotateX(Math.PI / 2);\n                laser.glow.lookAt(endX, endY, endZ);\n                laser.glow.rotateX(Math.PI / 2);\n                \n                // Pulsing visual effect\n                const pulse = 0.85 + Math.sin(Date.now() * 0.025) * 0.15;\n                const fadeIn = Math.min(1, (laser.duration - laser.timeLeft) * 4); // Quick fade in\n                const fadeOut = Math.min(1, laser.timeLeft * 3); // Fade out at end\n                const alpha = pulse * fadeIn * fadeOut;\n                \n                // Color cycle gets faster and more vivid at higher levels\n                const cycleSpeed = 0.002 + laser.level * 0.0015;\n                const baseHue = (Date.now() * cycleSpeed) % 1;\n                laser.beamMat.color.setHSL(baseHue, 1.0, 0.55);\n                laser.glowMat.color.setHSL((baseHue + 0.12) % 1, 1.0, 0.45);\n                \n                laser.beamMat.opacity = alpha * 0.85;\n                laser.glowMat.opacity = alpha * 0.35;\n                \n                // Slight width pulsing\n                const widthPulse = 1 + Math.sin(Date.now() * 0.03) * 0.1;\n                laser.beam.scale.x = widthPulse;\n                laser.beam.scale.z = widthPulse;\n                laser.glow.scale.x = widthPulse;\n                laser.glow.scale.z = widthPulse;\n            }\n        }\n        function fireTailSweep(level) {\n            if (player.spinning) return; // Don't start another spin mid-spin\n            const radius = (21 + level * 10) * Math.max(1, Math.pow(player.size / STARTING_SIZE, 0.45));\n            const dmg = 70 + level * 40; // Damage per hit during spin\n            const spins = 1 + (level - 1) * 0.5; // Spins up to 3 times at level 5\n            const spinDuration = 0.5 + (level - 1) * 0.15; // Slightly longer duration to account for more rotations\n            \n            player.spinning = true;\n            player.spinT = 0;\n            player.spinDur = Math.max(0.3, spinDuration);\n            player.spinStartRot = player.rotation;\n            player.spinData = { radius, dmg, spins, lastDamageAngle: 0, hitTargets: new Set() };\n            \n            addCameraShake(0.25);\n            playSound('building_hit_sound', 0.5);\n        }\n        \n        function updateTailSweepSpin(dt) {\n            if (!player.spinning) return;\n            \n            player.spinT += dt;\n            const progress = Math.min(1, player.spinT / player.spinDur);\n            \n            // Smooth spin with ease-in-out\n            const eased = progress < 0.5 \n                ? 2 * progress * progress \n                : 1 - Math.pow(-2 * progress + 2, 2) / 2;\n            \n            const d = player.spinData;\n            // Full spin angle scaled by the number of spins (e.g. up to 3 full rotations)\n            const totalSpinAngle = d.spins * Math.PI * 2;\n            const spinAngle = eased * totalSpinAngle;\n            player.rotation = player.spinStartRot + spinAngle;\n            \n            // Deal damage in sweeping arc as we spin\n            const currentAngle = spinAngle;\n            const angleStep = Math.PI / 4; // Deal damage every 45 degrees\n            \n            if (currentAngle - d.lastDamageAngle >= angleStep) {\n                d.lastDamageAngle = currentAngle;\n                \n                // Calculate tail tip position\n                const tailDir = player.rotation + Math.PI; // Tail is behind monster\n                const tailX = player.x + Math.sin(tailDir) * d.radius * 0.7;\n                const tailZ = player.z + Math.cos(tailDir) * d.radius * 0.7;\n                \n                // Damage in a cone behind the monster (where tail sweeps)\n                sweepDamage(tailX, tailZ, d.radius * 0.6, d.dmg, d.hitTargets);\n                \n                // Visual effects along the sweep\n                addDustRing(tailX, tailZ, d.radius * 0.4, 0x9be8ff);\n                \n                // Small debris\n                for (let i = 0; i < 2; i++) {\n                    createDebris(tailX + (Math.random() - 0.5) * 8, 3, tailZ + (Math.random() - 0.5) * 8, 0xb8ac90);\n                }\n            }\n            \n            // Animate tail whipping outward during spin\n            if (monsterParts.tail) {\n                const whipIntensity = Math.sin(progress * Math.PI * d.spins); // Dynamic whip matching the spins\n                monsterParts.tail.rotation.y = whipIntensity * 0.8;\n                monsterParts.tail.rotation.x = Math.abs(whipIntensity) * 0.15;\n            }\n            \n            // End spin\n            if (progress >= 1) {\n                player.spinning = false;\n                player.rotation = player.spinStartRot; // Return to original facing (or keep new facing)\n                if (monsterParts.tail) {\n                    monsterParts.tail.rotation.y = 0;\n                    monsterParts.tail.rotation.x = 0;\n                }\n                addCameraShake(0.3);\n                addDustRing(player.x, player.z, d.radius, 0x9be8ff);\n                playSound('building_collapse_sound', 0.35);\n            }\n        }\n        \n        function updateDash(dt) {\n            if (!player.dashing) return;\n            \n            player.dashT += dt;\n            const progress = Math.min(1, player.dashT / player.dashDur);\n            const d = player.dashData;\n            \n            // Ease-out for smooth deceleration at the end\n            const eased = 1 - Math.pow(1 - progress, 2);\n            \n            // Move player along dash path. While airborne from Jump Stomp, dash is\n            // additive so the leap distance and dash distance stack instead of one\n            // replacing the other.\n            if (d.additive) {\n                const deltaEase = eased - (d.lastEase || 0);\n                player.x += d.dx * d.dist * deltaEase;\n                player.z += d.dz * d.dist * deltaEase;\n                d.lastEase = eased;\n            } else {\n                player.x = d.startX + (d.endX - d.startX) * eased;\n                player.z = d.startZ + (d.endZ - d.startZ) * eased;\n            }\n            \n            // Face the dash direction (unless spinning, in which case let the spin control rotation)\n            if (!player.spinning) {\n                player.rotation = Math.atan2(d.dx, d.dz);\n            }\n            \n            // Damage things in path continuously during dash\n            sweepDamage(player.x, player.z, d.width, 99999, d.hitTargets);\n\n            d.afterimageTimer -= dt;\n            if (d.afterimageTimer <= 0) {\n                addDashAfterimage(\n                    player.x - d.dx * player.size * 4,\n                    player.z - d.dz * player.size * 4,\n                    player.rotation,\n                    player.size,\n                    d.width,\n                    d.dx,\n                    d.dz\n                );\n                d.afterimageTimer = 0.055;\n            }\n            \n            // Create trail particles during dash\n            if (Math.random() < 0.7) {\n                const trailX = player.x - d.dx * 5 + (Math.random() - 0.5) * 6;\n                const trailZ = player.z - d.dz * 5 + (Math.random() - 0.5) * 6;\n                addDustRing(trailX, trailZ, 4 + Math.random() * 3, 0x88ddff);\n            }\n            \n            // Speed lines effect - small debris particles\n            if (Math.random() < 0.5) {\n                createDebris(\n                    player.x + (Math.random() - 0.5) * 8,\n                    2 + Math.random() * 4,\n                    player.z + (Math.random() - 0.5) * 8,\n                    0xaaeeff\n                );\n            }\n            \n            // End dash\n            if (progress >= 1) {\n                player.dashing = false;\n                \n                // Final impact burst\n                addDustRing(player.x, player.z, 15, 0x88ddff);\n                addShockwave(player.x, player.z);\n                \n                // Damage at landing spot\n                aoeDamage(player.x, player.z, d.width + 4, d.dmg * 0.5);\n                \n                addCameraShake(0.2);\n                playSound('building_hit_sound', 0.4);\n                \n                // Give slight forward momentum after dash\n                player.vx = d.dx * player.baseSpeed * player.speedMul * 0.5;\n                player.vz = d.dz * player.baseSpeed * player.speedMul * 0.5;\n            }\n        }\n        \n        // Sweep damage that tracks already-hit targets to avoid multi-hitting\n        function sweepDamage(x, z, radius, dmg, hitSet) {\n            buildings.forEach(b => {\n                if (b.dead) return;\n                const key = 'b_' + buildings.indexOf(b);\n                if (hitSet.has(key)) return;\n                if (Math.hypot(b.x - x, b.z - z) < radius + Math.max(b.width, b.depth) / 2) {\n                    damageBuilding(b, dmg);\n                    hitSet.add(key);\n                }\n            });\n            for (let i = props.length - 1; i >= 0; i--) {\n                const p = props[i];\n                if (p.dead) continue;\n                const key = 'p_' + i;\n                if (hitSet.has(key)) continue;\n                if (Math.hypot(p.x - x, p.z - z) < radius) {\n                    destroyProp(p);\n                    hitSet.add(key);\n                }\n            }\n            for (let i = people.length - 1; i >= 0; i--) {\n                const pp = people[i];\n                if (pp.dead) continue;\n                const key = 'pp_' + i;\n                if (hitSet.has(key)) continue;\n                if (Math.hypot(pp.x - x, pp.z - z) < radius) {\n                    destroyPerson(pp);\n                    hitSet.add(key);\n                }\n            }\n            for (let i = enemies.length - 1; i >= 0; i--) {\n                const e = enemies[i];\n                if (e.dead) continue;\n                const key = 'e_' + i;\n                if (hitSet.has(key)) continue;\n                if (Math.hypot(e.x - x, e.z - z) < radius) {\n                    destroyEnemy(e);\n                    hitSet.add(key);\n                }\n            }\n            // Boss damage from sweep\n            if (boss && !boss.dead && !hitSet.has('boss')) {\n                if (Math.hypot(boss.x - x, boss.z - z) < radius + boss.size * 5) {\n                    damageBoss(dmg);\n                    hitSet.add('boss');\n                }\n            }\n        }\n\n        function getMovementInputVector() {\n            let inputX = 0, inputZ = 0;\n            \n            if (joystick.active) {\n                inputX = joystick.x;\n                inputZ = joystick.y;\n            } else {\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            \n            const len = Math.sqrt(inputX * inputX + inputZ * inputZ);\n            return { x: inputX, z: inputZ, len };\n        }\n\n        function fireDash(level) {\n            // Determine dash direction based on player input to prevent random dashes during spin\n            let dx, dz;\n            const input = getMovementInputVector();\n            \n            if (input.len > 0) {\n                // Dash in intended movement direction\n                dx = input.x / input.len;\n                dz = input.z / input.len;\n            } else if (player.jumping && player.jumpData && Math.hypot(player.jumpData.launchVx, player.jumpData.launchVz) > 0.001) {\n                const jumpSpeed = Math.hypot(player.jumpData.launchVx, player.jumpData.launchVz);\n                dx = player.jumpData.launchVx / jumpSpeed;\n                dz = player.jumpData.launchVz / jumpSpeed;\n            } else if (player.spinning) {\n                // Dash in original facing direction before spin started\n                dx = Math.sin(player.spinStartRot);\n                dz = Math.cos(player.spinStartRot);\n            } else {\n                // Dash in current facing direction\n                dx = Math.sin(player.rotation);\n                dz = Math.cos(player.rotation);\n            }\n            \n            // If spinning, update spin return rotation so we face the dash direction when spin ends\n            if (player.spinning) {\n                player.spinStartRot = Math.atan2(dx, dz);\n            }\n            \n            // Distance and speed both scale with level\n            // Level 1 is slower and shorter, higher levels get dramatically faster\n            const dashDist = 28 + level * 12;  // 40 at lvl1, up to 88 at lvl5\n            const dashDmg = 80 + level * 45;\n            const dashWidth = 6 + level * 1.5;\n            const dashDur = Math.max(0.16, 0.30 - level * 0.028); // 0.27s at lvl1, down to 0.16s at lvl5\n            \n            // Store dash data for animation\n            player.dashing = true;\n            player.dashT = 0;\n            player.dashDur = dashDur;\n            player.dashData = {\n                startX: player.x,\n                startZ: player.z,\n                endX: player.x + dx * dashDist,\n                endZ: player.z + dz * dashDist,\n                dx: dx,\n                dz: dz,\n                dist: dashDist,\n                dmg: dashDmg,\n                width: dashWidth,\n                afterimageTimer: 0,\n                lastEase: 0,\n                additive: player.jumping && player.jumpPhase !== 'recover',\n                hitTargets: new Set()\n            };\n            \n            // Visual trail effect at start\n            addDustRing(player.x, player.z, 12, 0x88ddff);\n            \n            // Sound and camera\n            addCameraShake(0.3);\n            playSound('footstep_sound', 0.5);\n        }\n        function fireJumpStomp(level) {\n            if (player.jumping || player.jumpPhase === 'recover') return;\n\n            const input = getMovementInputVector();\n            const currentSpeed = Math.hypot(player.vx, player.vz);\n            let launchDx = 0, launchDz = 0;\n            let hasLeapDirection = false;\n            \n            if (input.len > 0.05) {\n                launchDx = input.x / input.len;\n                launchDz = input.z / input.len;\n                hasLeapDirection = true;\n            } else if (player.dashing && player.dashData) {\n                launchDx = player.dashData.dx;\n                launchDz = player.dashData.dz;\n                hasLeapDirection = true;\n            } else if (currentSpeed > player.baseSpeed * player.speedMul * 0.35) {\n                launchDx = player.vx / currentSpeed;\n                launchDz = player.vz / currentSpeed;\n                hasLeapDirection = true;\n            }\n\n            const speedBoostMul = speedBoostTimer > 0 ? 2.5 : 1;\n            const jumpAirDur = 0.64;\n            const sizeRatio = Math.max(1, player.size / STARTING_SIZE);\n            const leapScale = Math.min(2.3, 1 + (sizeRatio - 1) * 0.16);\n            const walkDistanceDuringJump = player.baseSpeed * player.speedMul * speedBoostMul * jumpAirDur;\n            const leapDistance = hasLeapDirection\n                ? Math.max(24 + level * 6, walkDistanceDuringJump * (1.85 + level * 0.15)) * leapScale\n                : 0;\n            const launchSpeed = leapDistance > 0 ? leapDistance / (jumpAirDur * 0.86) : 0;\n            const launchVx = launchDx * launchSpeed;\n            const launchVz = launchDz * launchSpeed;\n            const heightScale = Math.min(2.8, 1 + (sizeRatio - 1) * 0.45);\n            const jumpHeight = (16 + level * 3) * heightScale;\n\n            player.jumping = true;\n            player.jumpPhase = 'prep';\n            player.jumpT = 0;\n            player.jumpPrepDur = 0.15;\n            player.jumpDur = jumpAirDur;\n            player.jumpRecoverDur = 0.13;\n            player.jumpRecoverT = 0;\n            const jumpRadius = (26 + level * 5) * Math.max(1, Math.pow(player.size / STARTING_SIZE, 0.45));\n            player.jumpData = { height: jumpHeight, radius: jumpRadius, dmg: 220 + level * 80, launchVx, launchVz, leapDistance };\n            player.bodyDip = Math.max(player.bodyDip, 0.22);\n            addCameraShake(0.2);\n            playSound('footstep_sound', 0.4);\n        }\n        function fireMeteor(level) {\n            // Limit active meteors for performance\n            if (meteors.length > 12) return;\n            const count = Math.min(3 + level, 6), spread = 44, impactR = 15 + level * 2, dmg = 160 + level * 60;\n            for (let i = 0; i < count; i++) {\n                const a = Math.random() * Math.PI * 2, r = Math.random() * spread;\n                spawnMeteor(player.x + Math.cos(a) * r, player.z + Math.sin(a) * r, i * 0.12 + Math.random() * 0.2, impactR, dmg);\n            }\n            playSound('enemy_shoot_sound', 0.3);\n        }\n\n        // Meteors: ground telegraph, fall, then explode (AoE).\n        function spawnMeteor(tx, tz, delay, radius, dmg) {\n            const mesh = new THREE.Mesh(new THREE.SphereGeometry(2.4, 8, 8), meteorMaterial.clone());\n            mesh.position.set(tx, 130, tz);\n            mesh.visible = false;\n            scene.add(mesh);\n            addDustRing(tx, tz, radius * 0.7, 0xff7a3a);\n            meteors.push({ mesh, tx, tz, delay, radius, dmg, y: 130 });\n        }\n        function updateMeteors(dt) {\n            for (let i = meteors.length - 1; i >= 0; i--) {\n                const m = meteors[i];\n                if (m.delay > 0) { m.delay -= dt; continue; }\n                m.mesh.visible = true;\n                m.y -= 220 * dt;\n                m.mesh.position.set(m.tx, Math.max(2, m.y), m.tz);\n                m.mesh.rotation.x += dt * 8; m.mesh.rotation.z += dt * 6;\n                if (m.y <= 2) {\n                    addShockwave(m.tx, m.tz);\n                    addGroundDamageEffect(m.tx, m.tz, m.radius, {\n                        minCrater: 3,\n                        craterScale: 0.38,\n                        crackMin: 6,\n                        crackMax: 10,\n                        crackDensity: 0.42,\n                        segmentMin: 1,\n                        segmentRand: 2,\n                        debrisCount: 4,\n                        life: 1.35,\n                        fadeTime: 0.65,\n                        grow: 0.09,\n                        craterOpacity: 0.58,\n                        crackOpacity: 0.7,\n                        debrisColor: 0x6b4a2a\n                    });\n                    addDustRing(m.tx, m.tz, m.radius, 0xffc24a);\n                    aoeDamage(m.tx, m.tz, m.radius, m.dmg);\n                    for (let k = 0; k < 4; k++) createDebris(m.tx + (Math.random() - 0.5) * m.radius, 3, m.tz + (Math.random() - 0.5) * m.radius, 0x6b4a2a);\n                    addCameraShake(0.4);\n                    playSound('enemy_explosion_sound', 0.45);\n                    removeFromScene(m.mesh);\n                    meteors.splice(i, 1);\n                }\n            }\n        }\n\n        // ==================== PEOPLE / PROPS UPDATE ====================\n        function updatePeople(dt) {\n            const cr = getCrushRadius();\n            const fleeRange = cr + 18;\n            const now = Date.now() * 0.012;\n            const skipCollisionDist = 80; // Skip building collision for distant people (perf)\n\n            for (let i = people.length - 1; i >= 0; i--) {\n                const pp = people[i];\n                if (pp.dead) { people.splice(i, 1); continue; }\n\n                const dx = pp.x - player.x, dz = pp.z - player.z;\n                const d = Math.hypot(dx, dz) || 0.001;\n\n                if (d < fleeRange) {\n                    pp.fleeing = true;\n                    pp.vx = (dx / d) * pp.runSpeed;\n                    pp.vz = (dz / d) * pp.runSpeed;\n                } else {\n                    pp.fleeing = false;\n                    pp.wanderTimer -= dt;\n                    if (pp.wanderTimer <= 0) {\n                        const a = Math.random() * Math.PI * 2;\n                        pp.vx = Math.cos(a) * pp.walkSpeed;\n                        pp.vz = Math.sin(a) * pp.walkSpeed;\n                        pp.wanderTimer = 1 + Math.random() * 2.5;\n                    }\n                }\n\n                const newX = pp.x + pp.vx * dt;\n                const newZ = pp.z + pp.vz * dt;\n                \n                // Skip expensive building collision for distant people\n                if (d > skipCollisionDist) {\n                    pp.x = newX;\n                    pp.z = newZ;\n                } else {\n                    const collision = checkBuildingCollision(newX, newZ, 0.5);\n                    if (!collision) {\n                        pp.x = newX;\n                        pp.z = newZ;\n                    } else {\n                        if (!checkBuildingCollision(newX, pp.z, 0.5)) {\n                            pp.x = newX;\n                        } else if (!checkBuildingCollision(pp.x, newZ, 0.5)) {\n                            pp.z = newZ;\n                        } else {\n                            if (!pp.fleeing) pp.wanderTimer = 0;\n                        }\n                    }\n                }\n\n                const bob = Math.abs(Math.sin(now + pp.phase)) * (pp.fleeing ? 0.6 : 0.18);\n                pp.mesh.position.set(pp.x, bob, pp.z);\n                if (Math.abs(pp.vx) + Math.abs(pp.vz) > 0.01) pp.mesh.rotation.y = Math.atan2(pp.vx, pp.vz);\n                pp.mesh.updateMatrix();\n            }\n        }\n\n        function updateProps(dt) {\n            for (let i = props.length - 1; i >= 0; i--) {\n                const p = props[i];\n                if (p.dead) { props.splice(i, 1); continue; }\n                if (p.type === 'car') {\n                    p.x += p.vx * dt;\n                    p.z += p.vz * dt;\n                    p.mesh.position.set(p.x, 0, p.z);\n                    // Update rotation to face movement direction\n                    if (Math.abs(p.vx) > 0.01 || Math.abs(p.vz) > 0.01) {\n                        p.mesh.rotation.y = Math.atan2(-p.vz, p.vx);\n                    }\n                }\n            }\n        }\n\n        function updateDestruction(dt) {\n            for (let i = buildings.length - 1; i >= 0; i--) {\n                const b = buildings[i];\n                if (!b.crushing) continue;\n                b.crushTimer -= dt;\n                const t = Math.max(0, b.crushTimer / 0.18);\n                b.mesh.scale.y = Math.max(0.02, t);\n                b.mesh.position.y = (b.height * Math.max(0.02, t)) / 2;\n                b.mesh.rotation.z = (1 - t) * 0.15 * (i % 2 === 0 ? 1 : -1);\n                if (b.crushTimer <= 0) {\n                    b.crushing = false;\n                    removeFromScene(b.mesh);\n                    buildings.splice(i, 1);\n                }\n            }\n        }\n\n        // ==================== ENEMIES ====================\n        function makePoliceCarMesh() {\n            const g = new THREE.Group();\n            const body = new THREE.Mesh(new THREE.BoxGeometry(4.4, 1.1, 2.1), policeBodyMaterial);\n            body.position.y = 0.85; body.castShadow = true; g.add(body);\n            const cabin = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.0, 1.8), policeWhiteMaterial);\n            cabin.position.set(-0.2, 1.75, 0); cabin.castShadow = true; g.add(cabin);\n            const glass = new THREE.Mesh(new THREE.BoxGeometry(2.42, 0.8, 1.6), carGlassMaterial);\n            glass.position.set(-0.2, 1.8, 0); g.add(glass);\n            // Light bar\n            const lightBar = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.35, 1.8), new THREE.MeshLambertMaterial({ color: 0x222222 }));\n            lightBar.position.set(-0.2, 2.4, 0); g.add(lightBar);\n            const redLight = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.3, 0.5), policeLightRedMaterial);\n            redLight.position.set(-0.2, 2.55, 0.6); redLight.name = 'redLight'; g.add(redLight);\n            const blueLight = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.3, 0.5), policeLightBlueMaterial);\n            blueLight.position.set(-0.2, 2.55, -0.6); blueLight.name = 'blueLight'; g.add(blueLight);\n            // Wheels\n            [[1.3, 1.1], [1.3, -1.1], [-1.3, 1.1], [-1.3, -1.1]].forEach(([wx, wz]) => {\n                const wheel = new THREE.Mesh(sharedWheelGeo, wheelMaterial);\n                wheel.rotation.x = Math.PI / 2;\n                wheel.position.set(wx, 0.5, wz);\n                g.add(wheel);\n            });\n            return g;\n        }\n\n        function spawnEnemy(type, preferDirection) {\n            // Spawn on a ring around the player (infinite world).\n            // If preferDirection is true, bias spawn toward player's MOVEMENT direction\n            let a;\n            if (preferDirection && Math.random() < 0.7) {\n                // 70% chance to spawn in a 140\u00b0 arc ahead of the player's movement\n                // Use movement direction if moving, otherwise use facing direction\n                const moveSpeed = Math.sqrt(player.vx * player.vx + player.vz * player.vz);\n                let movementAngle;\n                if (moveSpeed > 1) {\n                    // Player is moving - use movement direction\n                    movementAngle = Math.atan2(player.vx, player.vz);\n                } else {\n                    // Player is stationary - use facing direction\n                    movementAngle = player.rotation;\n                }\n                const spread = Math.PI * 0.78; // 140 degrees\n                a = movementAngle + (Math.random() - 0.5) * spread;\n            } else {\n                // Otherwise spawn anywhere around the player\n                a = Math.random() * Math.PI * 2;\n            }\n            const r = 95 + Math.random() * 70;\n            let x = player.x + Math.cos(a) * r;\n            let z = player.z + Math.sin(a) * r;\n\n            let mesh, health, speed, xpValue, scoreValue, mass, y;\n            let rotor = null;\n            let lightPhase = Math.random() * Math.PI * 2;\n\n            if (type === 'police') {\n                mesh = makePoliceCarMesh();\n                health = 1; speed = 28; xpValue = 15; scoreValue = 120; mass = 4; y = 0;\n            } else if (type === 'tank') {\n                mesh = new THREE.Group();\n                const hull = new THREE.Mesh(new THREE.BoxGeometry(5, 1.8, 3.2), tankMaterial);\n                hull.position.y = 1.4; hull.castShadow = true; mesh.add(hull);\n                [-1.7, 1.7].forEach(tx => {\n                    const tread = new THREE.Mesh(new THREE.BoxGeometry(5.4, 1.1, 0.9), tankDarkMaterial);\n                    tread.position.set(0, 0.55, tx); tread.castShadow = true; mesh.add(tread);\n                });\n                const turret = new THREE.Mesh(new THREE.BoxGeometry(2.6, 1.3, 2.4), tankMaterial);\n                turret.position.y = 2.6; turret.castShadow = true; mesh.add(turret);\n                const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.28, 3.4, 8), tankDarkMaterial);\n                barrel.position.set(2.0, 2.6, 0); barrel.rotation.z = -Math.PI / 2; mesh.add(barrel);\n                // Glowing outline for visibility\n                const glowMat = enemyGlowMaterial.clone();\n                const glowHull = new THREE.Mesh(new THREE.BoxGeometry(5.8, 2.4, 3.8), glowMat);\n                glowHull.position.y = 1.4; glowHull.name = 'glow'; mesh.add(glowHull);\n                const glowTurret = new THREE.Mesh(new THREE.BoxGeometry(3.2, 1.8, 3.0), glowMat);\n                glowTurret.position.y = 2.6; glowTurret.name = 'glow'; mesh.add(glowTurret);\n                health = 1; speed = 14; xpValue = 28; scoreValue = 250; mass = 10; y = 0;\n                // Ground spotlight for tank visibility\n                const spotlight = new THREE.Mesh(new THREE.CircleGeometry(6, 10), spotlightMaterial.clone());\n                spotlight.rotation.x = -Math.PI / 2;\n                spotlight.position.y = 0.15;\n                spotlight.name = 'spotlight';\n                mesh.add(spotlight);\n            } else { // helicopter\n                mesh = new THREE.Group();\n                const heliBody = new THREE.Mesh(new THREE.BoxGeometry(3, 1.7, 4.2), heliMaterial);\n                heliBody.castShadow = true; mesh.add(heliBody);\n                const cockpit = new THREE.Mesh(new THREE.BoxGeometry(2.6, 1.2, 1.4), heliGlassMaterial);\n                cockpit.position.set(0, 0.2, 2.2); mesh.add(cockpit);\n                const tail = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 4), heliMaterial);\n                tail.position.set(0, 0.4, -3.2); mesh.add(tail);\n                const skidL = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.25, 3.5), tankDarkMaterial);\n                skidL.position.set(-1.1, -1.1, 0.2); mesh.add(skidL);\n                const skidR = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.25, 3.5), tankDarkMaterial);\n                skidR.position.set(1.1, -1.1, 0.2); mesh.add(skidR);\n                const mast = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.8, 6), tankDarkMaterial);\n                mast.position.y = 1.3; mesh.add(mast);\n                rotor = new THREE.Mesh(new THREE.BoxGeometry(9, 0.18, 0.7), rotorMaterial);\n                rotor.position.y = 1.7; mesh.add(rotor);\n                const tailRotor = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.8, 0.2), rotorMaterial);\n                tailRotor.position.set(0.4, 0.5, -5.0); mesh.add(tailRotor);\n                // Glowing outline for visibility\n                const glowMat = enemyGlowMaterial.clone();\n                const glowBody = new THREE.Mesh(new THREE.BoxGeometry(3.8, 2.4, 5.0), glowMat);\n                glowBody.name = 'glow'; mesh.add(glowBody);\n                const glowTail = new THREE.Mesh(new THREE.BoxGeometry(1.0, 1.0, 4.5), glowMat);\n                glowTail.position.set(0, 0.4, -3.2); glowTail.name = 'glow'; mesh.add(glowTail);\n                health = 1; speed = 26; xpValue = 38; scoreValue = 350; mass = 8; y = HELI_ALT;\n                // Ground spotlight for helicopter visibility (will be positioned separately)\n                const spotlight = new THREE.Mesh(new THREE.CircleGeometry(7, 10), spotlightMaterial.clone());\n                spotlight.rotation.x = -Math.PI / 2;\n                spotlight.name = 'heliSpotlight';\n                scene.add(spotlight);\n                mesh.userData.spotlight = spotlight;\n            }\n\n            mesh.position.set(x, y, z);\n            if (mesh.castShadow !== undefined) mesh.castShadow = true;\n            scene.add(mesh);\n\n            enemies.push({\n                mesh, x, z, y, type, rotor,\n                health, maxHealth: health, speed, xpValue, scoreValue, mass,\n                shootTimer: 1.5 + Math.random(), dead: false,\n                strafeDir: Math.random() < 0.5 ? 1 : -1,\n                lightPhase: lightPhase,\n                glowPhase: Math.random() * Math.PI * 2\n            });\n        }\n\n        // Check if a ground position collides with any building\n        function checkBuildingCollision(x, z, radius) {\n            for (let i = 0; i < buildings.length; i++) {\n                const b = buildings[i];\n                if (b.dead) continue;\n                // Quick coarse distance check (skip buildings obviously far away)\n                const cdx = x - b.x, cdz = z - b.z;\n                const maxR = 20 + radius; // Max building half-width is ~15\n                if (cdx > maxR || cdx < -maxR || cdz > maxR || cdz < -maxR) continue;\n                const halfW = b.width / 2 + radius;\n                const halfD = b.depth / 2 + radius;\n                if (Math.abs(cdx) < halfW && Math.abs(cdz) < halfD) {\n                    return b;\n                }\n            }\n            return null;\n        }\n\n        function updateEnemies(dt) {\n            // Despawn distance - enemies beyond this behind the player get removed\n            const ENEMY_DESPAWN_DIST = 180;\n            const ENEMY_DESPAWN_BEHIND_FACTOR = 0.6; // More aggressive despawn if behind movement direction\n            \n            // Calculate player's movement direction for despawn logic\n            const playerMoveSpeed = Math.sqrt(player.vx * player.vx + player.vz * player.vz);\n            const isPlayerMoving = playerMoveSpeed > player.baseSpeed * player.speedMul * 0.3;\n            let moveAngle = player.rotation; // Default to facing\n            if (isPlayerMoving) {\n                moveAngle = Math.atan2(player.vx, player.vz);\n            }\n            const moveDirX = Math.sin(moveAngle);\n            const moveDirZ = Math.cos(moveAngle);\n            \n            for (let i = enemies.length - 1; i >= 0; i--) {\n                const e = enemies[i];\n                if (e.dead) { enemies.splice(i, 1); continue; }\n                \n                // Check if enemy should despawn (too far behind the player)\n                const enemyDx = e.x - player.x;\n                const enemyDz = e.z - player.z;\n                const enemyDist = Math.hypot(enemyDx, enemyDz);\n                \n                if (enemyDist > ENEMY_DESPAWN_DIST) {\n                    // Check if enemy is behind player's movement direction\n                    const dotProduct = (enemyDx * moveDirX + enemyDz * moveDirZ); // Positive = ahead, negative = behind\n                    const isBehind = dotProduct < -enemyDist * 0.3; // Behind if mostly opposite to movement\n                    \n                    // Despawn if too far AND (behind movement OR just very far)\n                    if (isBehind || enemyDist > ENEMY_DESPAWN_DIST * 1.5) {\n                        // Remove spotlight if helicopter\n                        if (e.mesh && e.mesh.userData.spotlight) {\n                            removeFromScene(e.mesh.userData.spotlight);\n                        }\n                        removeFromScene(e.mesh);\n                        enemies.splice(i, 1);\n                        continue;\n                    }\n                }\n\n                const dx = player.x - e.x, dz = player.z - e.z;\n                const dist = Math.hypot(dx, dz) || 0.001;\n                const nx = dx / dist, nz = dz / dist;\n\n                if (e.type === 'police') {\n                    // Police cars chase aggressively and do contact damage\n                    const newX = e.x + nx * e.speed * dt;\n                    const newZ = e.z + nz * e.speed * dt;\n                    \n                    // Check building collision for ground enemies\n                    const collision = checkBuildingCollision(newX, newZ, 2.2);\n                    if (!collision) {\n                        e.x = newX;\n                        e.z = newZ;\n                    } else {\n                        // Try to slide along the building\n                        if (!checkBuildingCollision(newX, e.z, 2.2)) e.x = newX;\n                        else if (!checkBuildingCollision(e.x, newZ, 2.2)) e.z = newZ;\n                    }\n                    \n                    e.mesh.rotation.y = Math.atan2(-nz, nx);\n                    e.mesh.position.set(e.x, e.y, e.z);\n                    // Flashing lights animation\n                    e.lightPhase += dt * 12;\n                    const flashOn = Math.sin(e.lightPhase) > 0;\n                    e.mesh.children.forEach(child => {\n                        if (child.name === 'redLight') child.visible = flashOn;\n                        if (child.name === 'blueLight') child.visible = !flashOn;\n                    });\n                    // Contact damage when close\n                    if (dist < getCrushRadius() + 3) {\n                        dealDamageToPlayer(8 * damageMultiplier);\n                        destroyEnemy(e);\n                        continue;\n                    }\n                } else if (e.type === 'tank') {\n                    let moveX = 0, moveZ = 0;\n                    if (dist > 38) { moveX = nx * e.speed * dt; moveZ = nz * e.speed * dt; }\n                    else if (dist < 26) { moveX = -nx * e.speed * dt; moveZ = -nz * e.speed * dt; }\n                    \n                    if (moveX !== 0 || moveZ !== 0) {\n                        const newX = e.x + moveX;\n                        const newZ = e.z + moveZ;\n                        const collision = checkBuildingCollision(newX, newZ, 2.5);\n                        if (!collision) {\n                            e.x = newX;\n                            e.z = newZ;\n                        } else {\n                            // Try to slide along the building\n                            if (!checkBuildingCollision(newX, e.z, 2.5)) e.x = newX;\n                            else if (!checkBuildingCollision(e.x, newZ, 2.5)) e.z = newZ;\n                        }\n                    }\n                    \n                    e.mesh.rotation.y = Math.atan2(-nz, nx);\n                    e.mesh.position.set(e.x, e.y, e.z);\n                    // Pulsing glow effect\n                    e.glowPhase += dt * 4;\n                    const glowIntensity = 0.25 + Math.sin(e.glowPhase) * 0.2;\n                    e.mesh.children.forEach(child => {\n                        if (child.name === 'glow' && child.material) {\n                            child.material.opacity = glowIntensity;\n                        }\n                    });\n                } else if (e.type === 'helicopter') { // helicopter: approach + strafe\n                    if (dist > 34) { e.x += nx * e.speed * dt; e.z += nz * e.speed * dt; }\n                    else {\n                        e.x += (-nz) * e.strafeDir * e.speed * 0.7 * dt;\n                        e.z += (nx) * e.strafeDir * e.speed * 0.7 * dt;\n                    }\n                    e.y = HELI_ALT + Math.sin(Date.now() * 0.004 + e.x) * 1.5;\n                    e.mesh.rotation.y = Math.atan2(dx, dz);\n                    e.mesh.position.set(e.x, e.y, e.z);\n                    if (e.rotor) e.rotor.rotation.y += dt * 30;\n                    // Pulsing glow effect\n                    e.glowPhase += dt * 5;\n                    const glowIntensity = 0.3 + Math.sin(e.glowPhase) * 0.25;\n                    e.mesh.children.forEach(child => {\n                        if (child.name === 'glow' && child.material) {\n                            child.material.opacity = glowIntensity;\n                        }\n                    });\n                    // Update ground spotlight position for helicopter\n                    if (e.mesh.userData.spotlight) {\n                        e.mesh.userData.spotlight.position.set(e.x, 0.2, e.z);\n                        e.mesh.userData.spotlight.material.opacity = 0.25 + Math.sin(e.glowPhase * 1.2) * 0.15;\n                    }\n                }\n\n                // Only tanks and helicopters shoot projectiles\n                if (e.type === 'tank' || e.type === 'helicopter') {\n                    e.shootTimer -= dt;\n                    if (e.shootTimer <= 0 && dist < 130) {\n                        shootProjectile(e.x, e.y, e.z, nx, nz);\n                        e.shootTimer = (e.type === 'tank' ? 2.2 : 1.6) / spawnRateMultiplier;\n                        playSound('enemy_shoot_sound', 0.25);\n                    }\n                }\n            }\n        }\n\n        function shootProjectile(x, y, z, dirX, dirZ) {\n            const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 8, 8), projectileMaterial);\n            mesh.position.set(x, y, z);\n            scene.add(mesh);\n            projectiles.push({ mesh, x, y, z, vx: dirX * 85, vz: dirZ * 85, lifetime: 5.5 });\n        }\n\n        function updateProjectiles(dt) {\n            const hitR = player.size * 4 + 4;\n            for (let i = projectiles.length - 1; i >= 0; i--) {\n                const p = projectiles[i];\n                p.x += p.vx * dt;\n                p.z += p.vz * dt;\n                p.y += (3 - p.y) * Math.min(1, dt * 1.6); // tracer descends toward the monster\n                p.lifetime -= dt;\n                p.mesh.position.set(p.x, p.y, p.z);\n\n                // Check building collision - projectiles can be blocked by cover!\n                let hitBuilding = false;\n                for (let j = 0; j < buildings.length; j++) {\n                    const b = buildings[j];\n                    if (b.dead) continue;\n                    const halfW = b.width / 2;\n                    const halfD = b.depth / 2;\n                    // Check if projectile is inside building bounds (and below building height)\n                    if (p.x > b.x - halfW && p.x < b.x + halfW &&\n                        p.z > b.z - halfD && p.z < b.z + halfD &&\n                        p.y < b.height) {\n                        hitBuilding = true;\n                        // Damage the building slightly when hit by projectile\n                        damageBuilding(b, 8);\n                        // Create small impact effect\n                        createDebris(p.x, p.y, p.z, b.debrisColor);\n                        break;\n                    }\n                }\n                \n                if (hitBuilding) {\n                    removeFromScene(p.mesh);\n                    projectiles.splice(i, 1);\n                    continue;\n                }\n\n                if (Math.hypot(player.x - p.x, player.z - p.z) < hitR) {\n                    dealDamageToPlayer(10 * damageMultiplier, p.x, p.z);\n                    removeFromScene(p.mesh);\n                    projectiles.splice(i, 1);\n                    continue;\n                }\n                if (p.lifetime <= 0) {\n                    removeFromScene(p.mesh);\n                    projectiles.splice(i, 1);\n                }\n            }\n        }\n\n        function dealDamageToPlayer(damage, hitX, hitZ) {\n            // Check for invincibility\n            if (invincibilityTimer > 0) {\n                // Show blocked effect\n                addFloatingText(hitX || player.x, getMonsterTop(), hitZ || player.z, 'BLOCKED!');\n                addDustRing(player.x, player.z, 8, 0xffd700);\n                playSound('mutation_select_sound', 0.3);\n                return; // No damage taken\n            }\n            \n            player.health -= damage * (1 - player.damageReduction);\n            playSound('player_damage_sound', 0.4);\n            addCameraShake(0.2);\n            \n            // Monster hit flash effect\n            monsterHitFlash = MONSTER_FLASH_DURATION;\n            \n            // Add hit sparks at impact location (or monster center if no location provided)\n            const sparkX = hitX !== undefined ? hitX : player.x;\n            const sparkZ = hitZ !== undefined ? hitZ : player.z;\n            const sparkY = getMonsterTop() * 0.5 + 2;\n            addHitSparks(sparkX, sparkY, sparkZ);\n            \n            const flash = document.getElementById('damage-flash');\n            flash.classList.remove('active');\n            void flash.offsetWidth;\n            flash.classList.add('active');\n            if (player.health <= 0 && gameState.running) { player.health = 0; gameOver(); }\n        }\n        \n        // Add impact sparks when monster gets hit\n        function addHitSparks(x, y, z) {\n            const sparkColor = 0xff4444;\n            for (let i = 0; i < 8; i++) {\n                const angle = (i / 8) * Math.PI * 2 + Math.random() * 0.5;\n                const speed = 15 + Math.random() * 20;\n                const sparkMat = new THREE.MeshBasicMaterial({ color: sparkColor, transparent: true });\n                const spark = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.6, 0.6), sparkMat);\n                spark.position.set(x + (Math.random() - 0.5) * 3, y + (Math.random() - 0.5) * 3, z + (Math.random() - 0.5) * 3);\n                scene.add(spark);\n                particles.push({\n                    mesh: spark,\n                    vx: Math.cos(angle) * speed,\n                    vy: 8 + Math.random() * 12,\n                    vz: Math.sin(angle) * speed,\n                    lifetime: 0.3 + Math.random() * 0.2\n                });\n            }\n            // Add a brief flash ring at impact\n            const ringMat = new THREE.MeshBasicMaterial({ color: 0xff6666, transparent: true, opacity: 0.7, side: THREE.DoubleSide });\n            const ring = new THREE.Mesh(new THREE.RingGeometry(0.5, 2, 16), ringMat);\n            ring.position.set(x, y, z);\n            ring.lookAt(camera.position);\n            scene.add(ring);\n            particles.push({ mesh: ring, vx: 0, vy: 0, vz: 0, lifetime: 0.25, isRing: true, ringMax: 8, ringAge: 0, ringMat: ringMat });\n        }\n        \n        // Update monster material colors for hit flash\n        function updateMonsterHitFlash(dt) {\n            if (monsterHitFlash > 0) {\n                monsterHitFlash -= dt;\n                const flashIntensity = Math.min(1, monsterHitFlash / MONSTER_FLASH_DURATION * 2); // Quick fade\n                \n                // Tint monster materials toward red/white\n                const tintR = 1;\n                const tintG = 1 - flashIntensity * 0.7; // Reduce green\n                const tintB = 1 - flashIntensity * 0.7; // Reduce blue\n                \n                // Apply tint by adjusting emissive color (adds brightness)\n                const emissiveIntensity = flashIntensity * 0.6;\n                monsterBodyMaterial.emissive = new THREE.Color(emissiveIntensity, emissiveIntensity * 0.2, emissiveIntensity * 0.2);\n                monsterBackMaterial.emissive = new THREE.Color(emissiveIntensity, emissiveIntensity * 0.2, emissiveIntensity * 0.2);\n                monsterBellyMaterial.emissive = new THREE.Color(emissiveIntensity * 0.8, emissiveIntensity * 0.15, emissiveIntensity * 0.15);\n            } else {\n                // Reset emissive to black (no glow)\n                monsterBodyMaterial.emissive = new THREE.Color(0, 0, 0);\n                monsterBackMaterial.emissive = new THREE.Color(0, 0, 0);\n                monsterBellyMaterial.emissive = new THREE.Color(0, 0, 0);\n            }\n        }\n\n        // ==================== BOSS SYSTEM ====================\n        let boss = null; // Active boss object\n        let bossIntro = { active: false, phase: '', timer: 0, savedCamPos: null, landX: 0, landZ: 0 };\n        const bossProjectiles = []; // Boss-specific projectiles (fists, missiles)\n\n        const BOSS_NAMES = ['IRON GUARDIAN MK-I', 'IRON GUARDIAN MK-II', 'IRON GUARDIAN MK-III', 'IRON GUARDIAN MK-IV', 'IRON GUARDIAN MK-V', 'IRON GUARDIAN MK-VI'];\n        const BOSS_COLORS = [\n            { body: 0xdddddd, chest: 0x2266cc, accent: 0xcc2222, belt: 0xddaa00, dark: 0x888888 },\n            { body: 0x223366, chest: 0xeeeeee, accent: 0xddaa00, belt: 0xaaaacc, dark: 0x111833 },\n            { body: 0x1a1a1a, chest: 0xcc2222, accent: 0xcccccc, belt: 0x666666, dark: 0x0a0a0a },\n            { body: 0x225533, chest: 0xdddd22, accent: 0xffffff, belt: 0x88aa44, dark: 0x113311 },\n            { body: 0x442266, chest: 0xff6600, accent: 0xddaa00, belt: 0x884488, dark: 0x221133 },\n            { body: 0x882222, chest: 0x111111, accent: 0xffd700, belt: 0x661111, dark: 0x441111 }\n        ];\n\n        function getBossTier() {\n            // Level 10 = tier 1, level 15 = tier 2, etc.\n            if (gameState.level < 10) return 0;\n            return Math.floor((gameState.level - 10) / 5) + 1;\n        }\n\n        function shouldSpawnBoss() {\n            // Spawn at level 10, 15, 20, 25, 30, 35...\n            if (gameState.level < 10) return false;\n            if ((gameState.level - 10) % 5 !== 0) return false;\n            // Don't spawn if one is already alive (wait rule)\n            if (boss && !boss.dead) return false;\n            return true;\n        }\n\n        function createBossMesh(tier) {\n            const group = new THREE.Group();\n            const wrapper = new THREE.Group();\n            wrapper.position.y = 4.6;\n            group.add(wrapper);\n            const scheme = BOSS_COLORS[Math.min(tier - 1, BOSS_COLORS.length - 1)];\n            \n            const bodyMat = new THREE.MeshLambertMaterial({ color: scheme.body, flatShading: true });\n            const chestMat = new THREE.MeshLambertMaterial({ color: scheme.chest, flatShading: true });\n            const accentMat = new THREE.MeshLambertMaterial({ color: scheme.accent, flatShading: true });\n            const beltMat = new THREE.MeshLambertMaterial({ color: scheme.belt, flatShading: true });\n            const darkMat = new THREE.MeshLambertMaterial({ color: scheme.dark, flatShading: true });\n            const visorMat = new THREE.MeshBasicMaterial({ color: 0x88ccff });\n            const thrusterMat = new THREE.MeshBasicMaterial({ color: 0xff4400 });\n            \n            // Store materials for cleanup\n            group.userData.materials = [bodyMat, chestMat, accentMat, beltMat, darkMat, visorMat, thrusterMat];\n            \n            const parts = {};\n            \n            // === LEGS ===\n            ['legL', 'legR'].forEach((key, idx) => {\n                const side = idx === 0 ? -1 : 1;\n                const leg = new THREE.Group();\n                leg.position.set(side * 2.2, 5, 0);\n                // Thigh\n                const thigh = new THREE.Mesh(new THREE.BoxGeometry(2.2, 4.5, 2.2), bodyMat);\n                thigh.position.set(0, -2.5, 0); thigh.castShadow = true; leg.add(thigh);\n                // Knee guard\n                const knee = new THREE.Mesh(new THREE.BoxGeometry(2.6, 1.2, 2.6), darkMat);\n                knee.position.set(0, -4.5, 0); knee.castShadow = true; leg.add(knee);\n                // Shin\n                const shin = new THREE.Mesh(new THREE.BoxGeometry(2.0, 4.0, 2.0), bodyMat);\n                shin.position.set(0, -6.5, 0); shin.castShadow = true; leg.add(shin);\n                // Foot\n                const foot = new THREE.Mesh(new THREE.BoxGeometry(2.8, 1.2, 3.8), accentMat);\n                foot.position.set(0, -9.0, 0.5); foot.castShadow = true; leg.add(foot);\n                wrapper.add(leg);\n                parts[key] = leg;\n            });\n            \n            // === TORSO ===\n            const torso = new THREE.Group();\n            // Waist/belt\n            const waist = new THREE.Mesh(new THREE.BoxGeometry(5, 2, 3), beltMat);\n            waist.position.set(0, 6, 0); waist.castShadow = true; torso.add(waist);\n            // Chest (blue)\n            const chest = new THREE.Mesh(new THREE.BoxGeometry(6.5, 4.5, 3.5), chestMat);\n            chest.position.set(0, 9.5, 0); chest.castShadow = true; torso.add(chest);\n            // Chest vents (yellow stripes)\n            for (let i = 0; i < 3; i++) {\n                const vent = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.4, 0.2), beltMat);\n                vent.position.set(0, 8.5 + i * 1.0, 1.8); torso.add(vent);\n            }\n            // Upper body armor\n            const shoulders = new THREE.Mesh(new THREE.BoxGeometry(7.5, 2, 3.2), bodyMat);\n            shoulders.position.set(0, 12, 0); shoulders.castShadow = true; torso.add(shoulders);\n            wrapper.add(torso);\n            parts.torso = torso;\n            \n            // === ARMS ===\n            ['armL', 'armR'].forEach((key, idx) => {\n                const side = idx === 0 ? -1 : 1;\n                const arm = new THREE.Group();\n                arm.position.set(side * 4.5, 11.5, 0);\n                // Shoulder pad\n                const shoulderPad = new THREE.Mesh(new THREE.BoxGeometry(3, 2, 2.5), bodyMat);\n                shoulderPad.position.set(side * 0.5, 0.5, 0); shoulderPad.castShadow = true; arm.add(shoulderPad);\n                // Upper arm\n                const upper = new THREE.Mesh(new THREE.BoxGeometry(2, 3.5, 2), bodyMat);\n                upper.position.set(0, -2.5, 0); upper.castShadow = true; arm.add(upper);\n                // Forearm\n                const fore = new THREE.Mesh(new THREE.BoxGeometry(2.2, 3.5, 2.2), darkMat);\n                fore.position.set(0, -5.5, 0); fore.castShadow = true; arm.add(fore);\n                // Fist (big blocky)\n                const fist = new THREE.Mesh(new THREE.BoxGeometry(2.5, 2.5, 2.5), accentMat);\n                fist.position.set(0, -8, 0); fist.castShadow = true; fist.name = 'fist'; arm.add(fist);\n                wrapper.add(arm);\n                parts[key] = arm;\n            });\n            \n            // === HEAD ===\n            const head = new THREE.Group();\n            head.position.set(0, 13.5, 0);\n            // Main head\n            const helmet = new THREE.Mesh(new THREE.BoxGeometry(3.5, 3, 3), bodyMat);\n            helmet.position.set(0, 1.5, 0); helmet.castShadow = true; head.add(helmet);\n            // Visor\n            const visor = new THREE.Mesh(new THREE.BoxGeometry(2.8, 1.2, 0.5), visorMat);\n            visor.position.set(0, 1.2, 1.6); head.add(visor);\n            // Chin guard\n            const chin = new THREE.Mesh(new THREE.BoxGeometry(2, 0.8, 1.8), darkMat);\n            chin.position.set(0, 0.2, 0.5); chin.castShadow = true; head.add(chin);\n            // V-fin antenna\n            [-1, 1].forEach(s => {\n                const fin = new THREE.Mesh(new THREE.BoxGeometry(0.3, 2.5, 0.5), beltMat);\n                fin.position.set(s * 1.2, 3.2, 0.5);\n                fin.rotation.z = -s * 0.4;\n                head.add(fin);\n            });\n            // Red forehead gem\n            const gem = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.6, 0.3), accentMat);\n            gem.position.set(0, 2.8, 1.5); head.add(gem);\n            wrapper.add(head);\n            parts.head = head;\n            \n            // === BACKPACK / THRUSTERS ===\n            const backpack = new THREE.Group();\n            const pack = new THREE.Mesh(new THREE.BoxGeometry(4, 4.5, 2.5), darkMat);\n            pack.position.set(0, 9.5, -2.5); pack.castShadow = true; backpack.add(pack);\n            // Thruster nozzles\n            [-1, 1].forEach(s => {\n                const nozzle = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.9, 1.5, 8), darkMat);\n                nozzle.position.set(s * 1.2, 7.5, -3.2); backpack.add(nozzle);\n                const flame = new THREE.Mesh(new THREE.ConeGeometry(0.7, 2.5, 8), thrusterMat);\n                flame.position.set(s * 1.2, 5.8, -3.2); flame.rotation.x = Math.PI;\n                flame.name = 'flame'; backpack.add(flame);\n            });\n            wrapper.add(backpack);\n            parts.backpack = backpack;\n            \n            // Shield mesh (hidden by default, shown during shield phase)\n            const shieldMat = new THREE.MeshBasicMaterial({ color: 0x44aaff, transparent: true, opacity: 0, side: THREE.DoubleSide });\n            const shieldMesh = new THREE.Mesh(new THREE.SphereGeometry(1, 16, 12), shieldMat);\n            shieldMesh.scale.set(12, 15, 12);\n            shieldMesh.visible = false;\n            wrapper.add(shieldMesh);\n            parts.shield = shieldMesh;\n            parts.shieldMat = shieldMat;\n            group.userData.materials.push(shieldMat);\n            \n            group.userData.parts = parts;\n            return group;\n        }\n\n        function spawnBoss() {\n            const tier = getBossTier();\n            if (tier <= 0) return;\n            \n            // Landing position: in front of player\n            const angle = player.rotation; // In front of monster\n            const landDist = 40 + player.size * 5;\n            const landX = player.x + Math.sin(angle) * landDist;\n            const landZ = player.z + Math.cos(angle) * landDist;\n            \n            const mesh = createBossMesh(tier);\n            const bossScale = player.size * 0.95; // Nearly same size as player\n            mesh.scale.set(bossScale, bossScale, bossScale);\n            mesh.position.set(landX, 200, landZ); // Start high in sky\n            scene.add(mesh);\n            \n            let baseHp = (1200 + tier * 600) * 6;\n            if (tier === 1) baseHp *= 0.384; // Reduce first boss HP by ~62% total (0.48 * 0.8)\n            if (tier === 2) baseHp *= 0.56; // Reduce second boss HP by 44% total (0.7 * 0.8)\n            const hpScale = 1 + (gameState.level - 10) * 0.08;\n            \n            boss = {\n                mesh, x: landX, z: landZ, y: 200,\n                tier,\n                health: baseHp * hpScale,\n                maxHealth: baseHp * hpScale,\n                speed: 20 + tier * 4, // Much faster - aggressively chases player\n                size: bossScale,\n                rotation: 0,\n                dead: false,\n                kbvx: 0, kbvz: 0, // Knockback velocities\n                // Ability cooldowns (short initial cooldowns so boss attacks quickly)\n                dashCd: 1.0, punchCd: 2.5, jumpCd: 4, swordCd: 3, missileCd: 6, shieldCd: 12,\n                // States\n                dashing: false, dashT: 0, dashDur: 0.4, dashDx: 0, dashDz: 0, dashSpeed: 0,\n                jumping: false, jumpT: 0, jumpDur: 0.8, jumpY: 0,\n                jumpStartX: 0, jumpStartZ: 0, jumpTargetX: 0, jumpTargetZ: 0,\n                shielded: false, shieldT: 0, shieldDur: 3,\n                swordActive: false, swordT: 0, swordDur: 0.6,\n                // Animation\n                walkPhase: 0,\n                hitFlash: 0,\n                // Damage tracking\n                contactCd: 0,\n                attackMul: (tier <= 2 ? 0.32 : 1.0) // Tiers 1 & 2 deal 44% less damage\n            };\n            \n            // Start intro cinematic\n            startBossIntro(landX, landZ, tier);\n        }\n\n        function startBossIntro(landX, landZ, tier) {\n            bossIntro.active = true;\n            bossIntro.phase = 'warning';\n            bossIntro.timer = 0;\n            bossIntro.landX = landX;\n            bossIntro.landZ = landZ;\n            bossIntro.tier = tier;\n            \n            // Pause everything during intro\n            gameState.paused = true;\n            \n            // Show warning overlay\n            document.getElementById('boss-warning-overlay').classList.add('active');\n            document.getElementById('boss-intro-name').textContent = BOSS_NAMES[Math.min(tier - 1, BOSS_NAMES.length - 1)];\n            document.getElementById('boss-intro-name').classList.remove('active');\n            \n            addCameraShake(0.3);\n            playSound('player_damage_sound', 0.6);\n        }\n\n        function updateBossIntro(dt) {\n            if (!bossIntro.active) return;\n            bossIntro.timer += dt;\n            \n            const phase = bossIntro.phase;\n            \n            if (phase === 'warning') {\n                // Show WARNING for 1.2 seconds\n                if (bossIntro.timer >= 1.2) {\n                    bossIntro.phase = 'descend';\n                    bossIntro.timer = 0;\n                    // Show boss name\n                    document.getElementById('boss-intro-name').classList.add('active');\n                }\n                // Camera stays on player, slight shake\n                if (Math.random() < 0.3) addCameraShake(0.1);\n                updateCamera(dt);\n            } else if (phase === 'descend') {\n                // Boss flies down over 2 seconds\n                const t = Math.min(1, bossIntro.timer / 2.0);\n                const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic\n                \n                if (boss) {\n                    boss.y = 200 * (1 - eased);\n                    boss.mesh.position.y = boss.y;\n                    \n                    // Face toward player\n                    const dx = player.x - boss.x;\n                    const dz = player.z - boss.z;\n                    boss.rotation = Math.atan2(dx, dz);\n                    boss.mesh.rotation.y = boss.rotation;\n                    \n                    // Thruster flames grow during descent\n                    const flames = [];\n                    boss.mesh.traverse(child => { if (child.name === 'flame') flames.push(child); });\n                    flames.forEach(f => {\n                        const flameScale = 1 + (1 - t) * 3;\n                        f.scale.set(flameScale, flameScale, flameScale);\n                        f.visible = t < 0.9;\n                    });\n                }\n                \n                // Camera looks at landing zone\n                const camTargetX = (player.x + bossIntro.landX) / 2;\n                const camTargetZ = (player.z + bossIntro.landZ) / 2;\n                cameraTarget.x += (camTargetX - cameraTarget.x) * Math.min(1, dt * 3);\n                cameraTarget.z += (camTargetZ - cameraTarget.z) * Math.min(1, dt * 3);\n                updateCamera(dt);\n                \n                if (bossIntro.timer >= 2.0) {\n                    bossIntro.phase = 'land';\n                    bossIntro.timer = 0;\n                    // Landing impact!\n                    if (boss) {\n                        boss.y = 0;\n                        boss.mesh.position.y = 0;\n                    }\n                    addCameraShake(0.8);\n                    addShockwave(bossIntro.landX, bossIntro.landZ);\n                    addDustRing(bossIntro.landX, bossIntro.landZ, 30, 0xaaaaaa);\n                    playSound('building_collapse_sound', 0.7);\n                    // Damage nearby buildings on landing\n                    aoeDamage(bossIntro.landX, bossIntro.landZ, 20, 100);\n                }\n            } else if (phase === 'land') {\n                // Brief pause after landing (0.8s)\n                updateCamera(dt);\n                if (bossIntro.timer >= 0.8) {\n                    // End intro\n                    bossIntro.active = false;\n                    gameState.paused = false;\n                    document.getElementById('boss-warning-overlay').classList.remove('active');\n                    document.getElementById('boss-intro-name').classList.remove('active');\n                    // Create 3D health bar above boss head\n                    createBossHealthBar3D();\n                    updateBossHealthUI();\n                }\n            }\n        }\n\n        function updateBoss(dt) {\n            if (!boss || boss.dead) return;\n            \n            // Contact damage cooldown\n            if (boss.contactCd > 0) boss.contactCd -= dt;\n            \n            // Apply knockback movement and friction decay\n            if (Math.hypot(boss.kbvx || 0, boss.kbvz || 0) > 0.1) {\n                boss.x += (boss.kbvx || 0) * dt;\n                boss.z += (boss.kbvz || 0) * dt;\n                boss.kbvx = (boss.kbvx || 0) * Math.max(0, 1 - dt * 8);\n                boss.kbvz = (boss.kbvz || 0) * Math.max(0, 1 - dt * 8);\n                \n                // Destroy buildings in knockback path\n                bossDestroyBuildings();\n            }\n            \n            // Hit flash\n            if (boss.hitFlash > 0) {\n                boss.hitFlash -= dt;\n                const flashInt = Math.min(1, boss.hitFlash / 0.15);\n                boss.mesh.traverse(child => {\n                    if (child.isMesh && child.material && child.material.emissive) {\n                        child.material.emissive.setRGB(flashInt * 0.5, flashInt * 0.1, flashInt * 0.1);\n                    }\n                });\n            }\n            \n            // Shield phase visual\n            const parts = boss.mesh.userData.parts;\n            if (boss.shielded) {\n                boss.shieldT -= dt;\n                if (parts.shield) {\n                    parts.shield.visible = true;\n                    const pulse = 0.35 + Math.sin(Date.now() * 0.01) * 0.1;\n                    parts.shieldMat.opacity = pulse;\n                    parts.shield.rotation.y += dt * 2;\n                }\n                if (boss.shieldT <= 0) {\n                    boss.shielded = false;\n                    if (parts.shield) parts.shield.visible = false;\n                }\n            }\n            \n            // Boss AI\n            bossAI(dt);\n            \n            // Update boss dash\n            if (boss.dashing) {\n                boss.dashT += dt;\n                const progress = Math.min(1, boss.dashT / boss.dashDur);\n                boss.x += boss.dashDx * boss.dashSpeed * dt;\n                boss.z += boss.dashDz * boss.dashSpeed * dt;\n                \n                // Destroy buildings in path\n                bossDestroyBuildings();\n                \n                // Damage player on contact during dash\n                const distToPlayer = Math.hypot(boss.x - player.x, boss.z - player.z);\n                if (distToPlayer < getCrushRadius() + boss.size * 5 && boss.contactCd <= 0) {\n                    dealDamageToPlayer(25 * damageMultiplier * boss.attackMul, boss.x, boss.z);\n                    boss.contactCd = 0.8;\n                    addCameraShake(0.4);\n                }\n                \n                // Trail effect\n                if (Math.random() < 0.6) addDustRing(boss.x, boss.z, 8, 0xffaa44);\n                \n                if (progress >= 1) boss.dashing = false;\n            }\n            \n            // Update boss jump\n            if (boss.jumping) {\n                boss.jumpT += dt;\n                const progress = Math.min(1, boss.jumpT / boss.jumpDur);\n                \n                // Interpolate XZ position towards target\n                boss.x = boss.jumpStartX + (boss.jumpTargetX - boss.jumpStartX) * progress;\n                boss.z = boss.jumpStartZ + (boss.jumpTargetZ - boss.jumpStartZ) * progress;\n                \n                // Parabolic arc for Y\n                if (progress < 0.6) {\n                    boss.jumpY = Math.sin((progress / 0.6) * Math.PI / 2) * 50;\n                } else {\n                    boss.jumpY = Math.cos(((progress - 0.6) / 0.4) * Math.PI / 2) * 50;\n                }\n                boss.y = boss.jumpY;\n                \n                if (progress >= 1) {\n                    boss.jumping = false;\n                    boss.y = 0;\n                    boss.jumpY = 0;\n                    // Landing AoE\n                    const jumpRadius = 20 + boss.tier * 5;\n                    aoeDamage(boss.x, boss.z, jumpRadius, 35 + boss.tier * 10);\n                    // Damage player\n                    if (Math.hypot(boss.x - player.x, boss.z - player.z) < jumpRadius) {\n                        dealDamageToPlayer(35 * damageMultiplier * boss.attackMul, boss.x, boss.z);\n                    }\n                    addShockwave(boss.x, boss.z);\n                    addDustRing(boss.x, boss.z, jumpRadius, 0xff6644);\n                    addCameraShake(0.6);\n                    playSound('building_collapse_sound', 0.6);\n                }\n            }\n            \n            // Sword sweep\n            if (boss.swordActive) {\n                boss.swordT += dt;\n                if (boss.swordT >= boss.swordDur) {\n                    boss.swordActive = false;\n                }\n                // Damage in arc in front of boss\n                const swordRange = boss.size * 15;\n                const faceDx = Math.sin(boss.rotation);\n                const faceDz = Math.cos(boss.rotation);\n                coneDamage(boss.x, boss.z, faceDx, faceDz, swordRange, 0.5, 20 * dt * 5);\n                if (Math.hypot(boss.x - player.x, boss.z - player.z) < swordRange) {\n                    const px = player.x - boss.x, pz = player.z - boss.z;\n                    const pd = Math.hypot(px, pz) || 1;\n                    const dot = (px / pd) * faceDx + (pz / pd) * faceDz;\n                    if (dot > 0.5 && boss.contactCd <= 0) {\n                        dealDamageToPlayer(30 * damageMultiplier * boss.attackMul, boss.x, boss.z);\n                        boss.contactCd = 0.5;\n                    }\n                }\n            }\n            \n            // Walk animation\n            const moving = !boss.dashing && !boss.jumping;\n            if (moving && parts) {\n                boss.walkPhase += dt * 5;\n                if (parts.legL) parts.legL.rotation.x = Math.sin(boss.walkPhase) * 0.3;\n                if (parts.legR) parts.legR.rotation.x = Math.sin(boss.walkPhase + Math.PI) * 0.3;\n                if (parts.armL) parts.armL.rotation.x = Math.sin(boss.walkPhase + Math.PI) * 0.2;\n                if (parts.armR) parts.armR.rotation.x = Math.sin(boss.walkPhase) * 0.2;\n            }\n            \n            // Update position\n            boss.mesh.position.set(boss.x, boss.y, boss.z);\n            boss.mesh.rotation.y = boss.rotation;\n            \n            // Scale to match player size\n            const targetScale = player.size * 0.95;\n            boss.size += (targetScale - boss.size) * Math.min(1, dt * 2);\n            boss.mesh.scale.set(boss.size, boss.size, boss.size);\n            \n            // Contact damage when player walks into boss\n            const distToPlayer = Math.hypot(boss.x - player.x, boss.z - player.z);\n            const contactRange = getCrushRadius() + boss.size * 5;\n            if (distToPlayer < contactRange && boss.contactCd <= 0 && !boss.dashing && !boss.jumping) {\n                // TITAN CLASH! Both giants bounce off each other hard\n                damageBoss(15 + player.crushBonus * 10);\n                if (!boss || boss.dead) return;\n                boss.contactCd = 0.4;\n                \n                // Calculate collision normal and impact point\n                const bumpNx = (boss.x - player.x) / distToPlayer;\n                const bumpNz = (boss.z - player.z) / distToPlayer;\n                const impactX = player.x + bumpNx * (distToPlayer * 0.5);\n                const impactZ = player.z + bumpNz * (distToPlayer * 0.5);\n                const impactY = Math.min(getMonsterTop(), boss.size * 10) * 0.6;\n                \n                // Heavy mutual knockback (both entities get physical velocity-based knockbacks!)\n                const knockStrength = 45 + player.size * 12;\n                player.vx = -bumpNx * knockStrength;\n                player.vz = -bumpNz * knockStrength;\n                player.bodyDip = Math.max(player.bodyDip, 0.35);\n                \n                const bossKnockStrength = 60 + player.size * 15;\n                boss.kbvx = bumpNx * bossKnockStrength;\n                boss.kbvz = bumpNz * bossKnockStrength;\n                \n                // === VFX: Titan Clash Impact ===\n                // 1. Heavy camera shake\n                addCameraShake(0.55);\n                \n                // 2. Sparks flying from impact point\n                for (let sp = 0; sp < 14; sp++) {\n                    const sparkAngle = (sp / 14) * Math.PI * 2 + Math.random() * 0.4;\n                    const sparkSpeed = 20 + Math.random() * 30;\n                    const sparkMat = new THREE.MeshBasicMaterial({ color: Math.random() < 0.5 ? 0xffdd44 : 0xffffff, transparent: true });\n                    const spark = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), sparkMat);\n                    spark.position.set(impactX + (Math.random()-0.5)*3, impactY + (Math.random()-0.5)*3, impactZ + (Math.random()-0.5)*3);\n                    scene.add(spark);\n                    particles.push({\n                        mesh: spark,\n                        vx: Math.cos(sparkAngle) * sparkSpeed + (Math.random()-0.5)*10,\n                        vy: 10 + Math.random() * 18,\n                        vz: Math.sin(sparkAngle) * sparkSpeed + (Math.random()-0.5)*10,\n                        lifetime: 0.35 + Math.random() * 0.25\n                    });\n                }\n                \n                // 3. Ground shockwave ring at impact\n                const shockMat = new THREE.MeshBasicMaterial({ color: 0xffaa33, transparent: true, opacity: 0.8, side: THREE.DoubleSide });\n                const shockRing = new THREE.Mesh(new THREE.RingGeometry(1, 3, 32), shockMat);\n                shockRing.rotation.x = -Math.PI / 2;\n                shockRing.position.set(impactX, 0.5, impactZ);\n                scene.add(shockRing);\n                particles.push({ mesh: shockRing, vx: 0, vy: 0, vz: 0, lifetime: 0.4, isRing: true, ringMax: 35 + player.size * 8, ringAge: 0, ringMat: shockMat });\n                \n                // 4. Dust cloud burst\n                addDustRing(impactX, impactZ, 18 + player.size * 5, 0xddcc88);\n                addDustRing(impactX, impactZ, 12, 0xffeebb);\n                \n                // 5. Vertical impact flash ring (faces camera)\n                const flashMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.9, side: THREE.DoubleSide });\n                const flashRing = new THREE.Mesh(new THREE.RingGeometry(0.5, 2, 20), flashMat);\n                flashRing.position.set(impactX, impactY, impactZ);\n                flashRing.lookAt(camera.position);\n                scene.add(flashRing);\n                particles.push({ mesh: flashRing, vx: 0, vy: 0, vz: 0, lifetime: 0.25, isRing: true, ringMax: 14, ringAge: 0, ringMat: flashMat });\n                \n                // Sound\n                playSound('building_hit_sound', 0.6);\n                playSound('building_collapse_sound', 0.3);\n            }\n            \n            updateBossHealthUI();\n        }\n\n        function bossAI(dt) {\n            if (!boss || boss.dead || boss.dashing || boss.jumping || boss.swordActive) return;\n            \n            const dx = player.x - boss.x;\n            const dz = player.z - boss.z;\n            const dist = Math.hypot(dx, dz) || 1;\n            const nx = dx / dist;\n            const nz = dz / dist;\n            \n            // Face player\n            const targetRot = Math.atan2(dx, dz);\n            let rotDiff = targetRot - boss.rotation;\n            if (rotDiff > Math.PI) rotDiff -= Math.PI * 2;\n            if (rotDiff < -Math.PI) rotDiff += Math.PI * 2;\n            boss.rotation += rotDiff * Math.min(1, dt * 6); // Turn faster\n            \n            // Move toward player aggressively (keep at close range)\n            const idealDist = 12 + boss.size * 2;\n            if (dist > idealDist + 5) {\n                boss.x += nx * boss.speed * dt;\n                boss.z += nz * boss.speed * dt;\n                bossDestroyBuildings();\n            } else if (dist < idealDist - 5) {\n                boss.x -= nx * boss.speed * 0.4 * dt;\n                boss.z -= nz * boss.speed * 0.4 * dt;\n            }\n            \n            // Cooldown updates\n            boss.dashCd -= dt;\n            boss.punchCd -= dt;\n            boss.jumpCd -= dt;\n            boss.swordCd -= dt;\n            boss.missileCd -= dt;\n            boss.shieldCd -= dt;\n            \n            // Remove 'else if' block - check all abilities independently so high tiers don't lock out low tiers\n            \n            // Tier 6+: Shield Phase (when low HP)\n            if (boss.tier >= 6 && boss.shieldCd <= 0 && !boss.shielded && boss.health < boss.maxHealth * 0.4) {\n                bossShieldPhase();\n                boss.shieldCd = 20;\n                return; // Stop other attacks when shielding\n            }\n            \n            // Tier 1+: Dash\n            if (boss.tier >= 1 && boss.dashCd <= 0 && dist < 120 && dist > 15) {\n                bossDash(nx, nz);\n                boss.dashCd = boss.tier === 1 ? (2 + Math.random() * 1.5) : (4 + Math.random() * 2); // Faster cooldown if it's the only ability\n                return;\n            }\n            \n            // Tier 3+: Jump Smash\n            if (boss.tier >= 3 && boss.jumpCd <= 0 && dist < 80) {\n                bossJumpSmash(dx, dz, dist);\n                boss.jumpCd = 5 + Math.random() * 3;\n                return;\n            }\n            \n            // Tier 4+: Beam Sword\n            if (boss.tier >= 4 && boss.swordCd <= 0 && dist < 25) {\n                bossSwordSweep();\n                boss.swordCd = 4 + Math.random() * 2;\n                return;\n            }\n            \n            // Tier 1+: Rocket Punch\n            if (boss.tier >= 1 && boss.punchCd <= 0 && dist < 90 && dist > 20) {\n                bossRocketPunch(nx, nz);\n                boss.punchCd = 3.5 + Math.random() * 2;\n                return;\n            }\n            \n            // Tier 5+: Missile Barrage\n            if (boss.tier >= 5 && boss.missileCd <= 0 && dist < 120) {\n                bossMissileBarrage();\n                boss.missileCd = 7 + Math.random() * 3;\n            }\n        }\n\n        function bossDash(nx, nz) {\n            boss.dashing = true;\n            boss.dashT = 0;\n            boss.dashDur = 0.45; // slightly longer\n            boss.dashDx = nx;\n            boss.dashDz = nz;\n            boss.dashSpeed = 120 + boss.tier * 15; // Much faster dash\n            playSound('footstep_sound', 0.6);\n            addDustRing(boss.x, boss.z, 15, 0xff8844);\n        }\n\n        function bossRocketPunch(nx, nz) {\n            // Launch actual fist mesh that homes toward the player until hit\n            const scheme = BOSS_COLORS[Math.min(boss.tier - 1, BOSS_COLORS.length - 1)];\n            const fistGroup = new THREE.Group();\n            \n            // Main fist block\n            const fistMat = new THREE.MeshLambertMaterial({ color: scheme.accent, flatShading: true });\n            const fistCore = new THREE.Mesh(new THREE.BoxGeometry(3.5, 3.5, 4), fistMat);\n            fistGroup.add(fistCore);\n            \n            // Knuckle bumps\n            for (let i = -1; i <= 1; i++) {\n                const knuckle = new THREE.Mesh(new THREE.BoxGeometry(1, 1.2, 1), fistMat);\n                knuckle.position.set(i * 1.1, 1.5, 2);\n                fistGroup.add(knuckle);\n            }\n            \n            // Wrist/forearm stub\n            const armMat = new THREE.MeshLambertMaterial({ color: scheme.dark, flatShading: true });\n            const wrist = new THREE.Mesh(new THREE.BoxGeometry(2.8, 2.8, 3), armMat);\n            wrist.position.set(0, 0, -3);\n            fistGroup.add(wrist);\n            \n            // Thruster flame on back of fist (rocket effect)\n            const flameMat = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.8 });\n            const flame = new THREE.Mesh(new THREE.ConeGeometry(1.5, 4, 8), flameMat);\n            flame.position.set(0, 0, -5.5);\n            flame.rotation.x = Math.PI / 2;\n            flame.name = 'rocketFlame';\n            fistGroup.add(flame);\n            \n            // Scale fist to boss size\n            const fistScale = boss.size * 0.6;\n            fistGroup.scale.set(fistScale, fistScale, fistScale);\n            \n            const startX = boss.x + nx * boss.size * 6;\n            const startZ = boss.z + nz * boss.size * 6;\n            fistGroup.position.set(startX, boss.size * 12, startZ);\n            scene.add(fistGroup);\n            \n            bossProjectiles.push({\n                mesh: fistGroup, type: 'fist',\n                x: startX, z: startZ, y: boss.size * 12,\n                vx: nx * 45, vz: nz * 45,\n                damage: (20 + boss.tier * 5) * boss.attackMul,\n                lifetime: 8, size: 3 * fistScale,\n                homing: true, speed: 45,\n                indestructible: true,\n                flameMat: flameMat\n            });\n            \n            playSound('enemy_shoot_sound', 0.5);\n            addCameraShake(0.15);\n        }\n\n        function bossJumpSmash(dx, dz, dist) {\n            boss.jumping = true;\n            boss.jumpT = 0;\n            boss.jumpDur = 0.8;\n            \n            boss.jumpStartX = boss.x;\n            boss.jumpStartZ = boss.z;\n            \n            // Jump toward player, predicting a bit of movement\n            const jumpDist = Math.min(dist * 0.95, 65);\n            boss.jumpTargetX = boss.x + (dx / dist) * jumpDist;\n            boss.jumpTargetZ = boss.z + (dz / dist) * jumpDist;\n            \n            playSound('footstep_sound', 0.6);\n        }\n\n        function bossSwordSweep() {\n            boss.swordActive = true;\n            boss.swordT = 0;\n            boss.swordDur = 0.6;\n            // Visual: energy blade\n            const bladeMat = new THREE.MeshBasicMaterial({ color: 0x44ffff, transparent: true, opacity: 0.7, side: THREE.DoubleSide });\n            const blade = new THREE.Mesh(new THREE.BoxGeometry(2, 1, boss.size * 14), bladeMat);\n            const faceDx = Math.sin(boss.rotation);\n            const faceDz = Math.cos(boss.rotation);\n            blade.position.set(boss.x + faceDx * boss.size * 7, boss.size * 12, boss.z + faceDz * boss.size * 7);\n            blade.rotation.y = boss.rotation;\n            scene.add(blade);\n            addFlash(blade, 0.6, 2);\n            playSound('building_hit_sound', 0.5);\n            addCameraShake(0.25);\n        }\n\n        function bossMissileBarrage() {\n            const count = 4 + Math.min(boss.tier - 4, 3); // 4-7 missiles\n            for (let i = 0; i < count; i++) {\n                const angle = (i / count) * Math.PI * 2;\n                const missileMat = new THREE.MeshLambertMaterial({ color: 0x444444 });\n                const missile = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.4, 2, 6), missileMat);\n                const startX = boss.x + Math.cos(angle) * 5;\n                const startZ = boss.z + Math.sin(angle) * 5;\n                missile.position.set(startX, boss.size * 15 + i * 2, startZ);\n                scene.add(missile);\n                \n                bossProjectiles.push({\n                    mesh: missile, type: 'missile',\n                    x: startX, z: startZ, y: boss.size * 15 + i * 2,\n                    vx: 0, vz: 0, // Homing - calculated each frame\n                    damage: (12 + boss.tier * 3) * boss.attackMul,\n                    lifetime: 4, size: 2, delay: i * 0.15,\n                    homing: true, speed: 40 + boss.tier * 5\n                });\n            }\n            playSound('enemy_shoot_sound', 0.6);\n        }\n\n        function bossShieldPhase() {\n            boss.shielded = true;\n            boss.shieldT = boss.shieldDur;\n            playSound('mutation_select_sound', 0.5);\n            addFloatingText(boss.x, boss.size * 21, boss.z, 'SHIELD!');\n            addDustRing(boss.x, boss.z, 15, 0x44aaff);\n        }\n\n        function bossDestroyBuildings() {\n            if (!boss) return;\n            const crushR = boss.size * 5;\n            for (let i = 0; i < buildings.length; i++) {\n                const b = buildings[i];\n                if (b.dead) continue;\n                const halfW = b.width / 2 + crushR;\n                const halfD = b.depth / 2 + crushR;\n                if (Math.abs(boss.x - b.x) < halfW && Math.abs(boss.z - b.z) < halfD) {\n                    damageBuilding(b, 200); // Boss one-shots most buildings\n                }\n            }\n        }\n\n        function damageBoss(dmg) {\n            if (!boss || boss.dead) return;\n            if (boss.shielded) {\n                // Blocked by shield\n                addFloatingText(boss.x, boss.size * 21, boss.z, 'BLOCKED!');\n                addDustRing(boss.x, boss.z, 8, 0x44aaff);\n                return;\n            }\n            \n            // Star power = massive damage\n            const finalDmg = invincibilityTimer > 0 ? dmg * 3 : dmg;\n            boss.health -= finalDmg;\n            boss.hitFlash = 0.15;\n            \n            // Add automatic directional knockback away from the player based on damage!\n            if (!boss.jumping && !boss.dashing) {\n                const dx = boss.x - player.x;\n                const dz = boss.z - player.z;\n                const dist = Math.hypot(dx, dz) || 1;\n                const kStrength = 18 + (finalDmg * 0.14); // Scales with damage\n                boss.kbvx = (boss.kbvx || 0) + (dx / dist) * kStrength;\n                boss.kbvz = (boss.kbvz || 0) + (dz / dist) * kStrength;\n            }\n            \n            addCameraShake(0.1);\n            \n            if (boss.health <= 0) {\n                boss.health = 0;\n                destroyBoss();\n            }\n        }\n\n        // Boss destruction animation state\n        let bossDeathAnim = { active: false, timer: 0, phase: 0, x: 0, z: 0, size: 0, tier: 0, mesh: null, xpReward: 0, scoreReward: 0 };\n        const BOSS_DEATH_DURATION = 3.2; // Total animation time\n\n        function destroyBoss() {\n            if (!boss || boss.dead) return;\n            boss.dead = true;\n            \n            // Drop a star fall from the sky immediately upon defeat! It lands exactly around the final explosion.\n            spawnInvincibilityPickup(boss.x, boss.z, 'boss', 140);\n            \n            // Clean up boss projectiles immediately\n            bossProjectiles.forEach(p => removeFromScene(p.mesh));\n            bossProjectiles.length = 0;\n            \n            // Remove 3D health bar\n            if (boss.healthBar3D) {\n                removeFromScene(boss.healthBar3D.group);\n                if (boss.healthBar3D.labelTex) boss.healthBar3D.labelTex.dispose();\n                boss.healthBar3D = null;\n            }\n            \n            // Calculate rewards but DON'T grant yet\n            const xpReward = (gameState.xpToLevel * 2.5 + boss.tier * 500) * 0.3;\n            const scoreReward = 5000 + boss.tier * 3000;\n            \n            // Start death animation\n            bossDeathAnim = {\n                active: true,\n                timer: 0,\n                phase: 0,\n                x: boss.x,\n                z: boss.z,\n                y: boss.y,\n                size: boss.size,\n                tier: boss.tier,\n                mesh: boss.mesh,\n                xpReward: xpReward,\n                scoreReward: scoreReward,\n                nextExplosion: 0,\n                shakeBuildup: 0\n            };\n            \n            // Initial hit reaction\n            addCameraShake(0.4);\n            playSound('building_hit_sound', 0.6);\n        }\n\n        function updateBossDeathAnim(dt) {\n            if (!bossDeathAnim.active) return;\n            const anim = bossDeathAnim;\n            anim.timer += dt;\n            const t = anim.timer / BOSS_DEATH_DURATION;\n            \n            // Phase 0: Stagger & sparking (0-1.2s) - boss stumbles, small explosions pop off\n            if (anim.timer < 1.2) {\n                // Rapid high-frequency shudder/vibration instead of bouncing sideways\n                if (anim.mesh) {\n                    const shudder = (Math.random() - 0.5) * 0.45 * anim.size;\n                    anim.mesh.position.set(anim.x + shudder, anim.y, anim.z + shudder);\n                    anim.mesh.rotation.z = (Math.random() - 0.5) * 0.04;\n                    anim.mesh.rotation.x = (Math.random() - 0.5) * 0.04;\n                }\n                \n                // Random small explosions on the body\n                anim.nextExplosion -= dt;\n                if (anim.nextExplosion <= 0) {\n                    anim.nextExplosion = 0.12 + Math.random() * 0.1;\n                    const ox = anim.x + (Math.random() - 0.5) * anim.size * 12;\n                    const oz = anim.z + (Math.random() - 0.5) * anim.size * 12;\n                    const oy = anim.size * (4 + Math.random() * 14);\n                    \n                    // Orange/yellow flash\n                    const flashMat = new THREE.MeshBasicMaterial({ color: Math.random() < 0.5 ? 0xff6622 : 0xffdd44, transparent: true, opacity: 0.9, side: THREE.DoubleSide });\n                    const flash = new THREE.Mesh(new THREE.SphereGeometry(1.5 + Math.random() * 2, 8, 8), flashMat);\n                    flash.position.set(ox, oy, oz);\n                    scene.add(flash);\n                    particles.push({ mesh: flash, vx: 0, vy: 2, vz: 0, lifetime: 0.25, isRing: true, ringMax: 6, ringAge: 0, ringMat: flashMat });\n                    \n                    // Sparks\n                    for (let s = 0; s < 4; s++) {\n                        createDebris(ox, oy, oz, Math.random() < 0.5 ? 0xffaa22 : 0xdddddd);\n                    }\n                    \n                    // Escalating shake\n                    anim.shakeBuildup = Math.min(0.5, anim.shakeBuildup + 0.05);\n                    addCameraShake(anim.shakeBuildup);\n                    \n                    if (Math.random() < 0.4) playSound('enemy_explosion_sound', 0.3);\n                }\n                \n                // Emissive flash on boss\n                if (anim.mesh) {\n                    const flashInt = Math.abs(Math.sin(anim.timer * 20)) * 0.4;\n                    anim.mesh.traverse(child => {\n                        if (child.isMesh && child.material && child.material.emissive) {\n                            child.material.emissive.setRGB(flashInt, flashInt * 0.3, 0);\n                        }\n                    });\n                }\n            }\n            // Phase 1: Big chain explosions (1.2-2.2s) - larger blasts, parts fly off\n            else if (anim.timer < 2.2) {\n                if (anim.phase < 1) {\n                    anim.phase = 1;\n                    anim.nextExplosion = 0;\n                    playSound('building_collapse_sound', 0.7);\n                    addCameraShake(0.6);\n                }\n                \n                if (anim.mesh) {\n                    // Shudder even faster, swell up, no bouncing side to side\n                    const shudder = (Math.random() - 0.5) * 0.65 * anim.size;\n                    anim.mesh.position.set(anim.x + shudder, anim.y, anim.z + shudder);\n                    anim.mesh.rotation.z = (Math.random() - 0.5) * 0.06;\n                    anim.mesh.rotation.x = (Math.random() - 0.5) * 0.06;\n                    \n                    // Scale continuous swell (swelling before final explosion)\n                    const swell = 1.0 + (anim.timer - 1.2) * 0.25;\n                    anim.mesh.scale.set(anim.size * swell, anim.size * swell, anim.size * swell);\n                }\n                \n                // Bigger explosions\n                anim.nextExplosion -= dt;\n                if (anim.nextExplosion <= 0) {\n                    anim.nextExplosion = 0.08 + Math.random() * 0.06;\n                    const angle = Math.random() * Math.PI * 2;\n                    const dist = anim.size * (4 + Math.random() * 8);\n                    const ox = anim.x + Math.cos(angle) * dist;\n                    const oz = anim.z + Math.sin(angle) * dist;\n                    const oy = anim.size * (2 + Math.random() * 16);\n                    \n                    // Large fire blast\n                    const blastMat = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.85, side: THREE.DoubleSide });\n                    const blast = new THREE.Mesh(new THREE.SphereGeometry(3 + Math.random() * 3, 10, 10), blastMat);\n                    blast.position.set(ox, oy, oz);\n                    scene.add(blast);\n                    particles.push({ mesh: blast, vx: 0, vy: 3, vz: 0, lifetime: 0.35, isRing: true, ringMax: 10, ringAge: 0, ringMat: blastMat });\n                    \n                    // Heavy debris (metal chunks)\n                    for (let s = 0; s < 6; s++) {\n                        const dAngle = Math.random() * Math.PI * 2;\n                        const speed = 20 + Math.random() * 35;\n                        const debMat = new THREE.MeshLambertMaterial({ color: Math.random() < 0.3 ? 0x666666 : 0xdddddd, transparent: true });\n                        const deb = new THREE.Mesh(debrisGeo, debMat);\n                        const debSize = 1.5 + Math.random() * 2.5;\n                        deb.scale.set(debSize, debSize, debSize);\n                        deb.position.set(ox, oy, oz);\n                        scene.add(deb);\n                        particles.push({\n                            mesh: deb,\n                            vx: Math.cos(dAngle) * speed,\n                            vy: 15 + Math.random() * 25,\n                            vz: Math.sin(dAngle) * speed,\n                            lifetime: 1.5 + Math.random() * 0.5\n                        });\n                    }\n                    \n                    addCameraShake(0.4);\n                    if (Math.random() < 0.5) playSound('enemy_explosion_sound', 0.4);\n                }\n                \n                // Ground shockwaves\n                if (Math.random() < dt * 8) {\n                    addDustRing(anim.x + (Math.random() - 0.5) * 20, anim.z + (Math.random() - 0.5) * 20, 15 + Math.random() * 10, 0xff8844);\n                }\n            }\n            // Phase 2: Final massive explosion (2.2-2.6s) - white flash, everything goes boom\n            else if (anim.timer < 2.6) {\n                if (anim.phase < 2) {\n                    anim.phase = 2;\n                    \n                    // Remove boss mesh with final flash\n                    if (anim.mesh) {\n                        removeFromScene(anim.mesh);\n                        anim.mesh = null;\n                    }\n                    \n                    // MASSIVE final explosion\n                    addCameraShake(1.2);\n                    playSound('building_collapse_sound', 0.9);\n                    playSound('enemy_explosion_sound', 0.8);\n                    playSound('tank_explosion_sound', 0.7);\n                    \n                    // Giant shockwave\n                    const shockMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.9, side: THREE.DoubleSide });\n                    const shockRing = new THREE.Mesh(new THREE.RingGeometry(2, 5, 32), shockMat);\n                    shockRing.rotation.x = -Math.PI / 2;\n                    shockRing.position.set(anim.x, 1, anim.z);\n                    scene.add(shockRing);\n                    particles.push({ mesh: shockRing, vx: 0, vy: 0, vz: 0, lifetime: 0.7, isRing: true, ringMax: 80, ringAge: 0, ringMat: shockMat });\n                    \n                    // Second orange shockwave\n                    const shockMat2 = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.8, side: THREE.DoubleSide });\n                    const shockRing2 = new THREE.Mesh(new THREE.RingGeometry(3, 6, 32), shockMat2);\n                    shockRing2.rotation.x = -Math.PI / 2;\n                    shockRing2.position.set(anim.x, 0.5, anim.z);\n                    scene.add(shockRing2);\n                    particles.push({ mesh: shockRing2, vx: 0, vy: 0, vz: 0, lifetime: 0.6, isRing: true, ringMax: 60, ringAge: 0, ringMat: shockMat2 });\n                    \n                    // White screen flash\n                    const flash = document.getElementById('level-up-flash');\n                    flash.style.background = 'rgba(255, 255, 255, 0.6)';\n                    flash.classList.remove('active');\n                    void flash.offsetWidth;\n                    flash.classList.add('active');\n                    setTimeout(() => { flash.style.background = 'rgba(255, 215, 0, 0.3)'; }, 600);\n                    \n                    // Massive debris cloud\n                    for (let i = 0; i < 30; i++) {\n                        const angle = (i / 30) * Math.PI * 2;\n                        const dist = anim.size * 3 + Math.random() * 15;\n                        const speed = 30 + Math.random() * 50;\n                        const debMat = new THREE.MeshLambertMaterial({ \n                            color: [0xdddddd, 0x888888, 0xff6600, 0xffdd44, 0x444444][Math.floor(Math.random() * 5)], \n                            transparent: true \n                        });\n                        const deb = new THREE.Mesh(debrisGeo, debMat);\n                        const debSize = 1 + Math.random() * 3;\n                        deb.scale.set(debSize, debSize, debSize);\n                        deb.position.set(anim.x + Math.cos(angle) * dist * 0.3, anim.size * 8 + Math.random() * 10, anim.z + Math.sin(angle) * dist * 0.3);\n                        scene.add(deb);\n                        particles.push({\n                            mesh: deb,\n                            vx: Math.cos(angle) * speed,\n                            vy: 20 + Math.random() * 30,\n                            vz: Math.sin(angle) * speed,\n                            lifetime: 2 + Math.random() * 1\n                        });\n                    }\n                    \n                    // Multiple dust rings\n                    addDustRing(anim.x, anim.z, 50, 0xffaa44);\n                    addDustRing(anim.x, anim.z, 35, 0xffffff);\n                    addDustRing(anim.x, anim.z, 25, 0xff4400);\n                    \n                    // Destroy nearby buildings from the blast\n                    aoeDamage(anim.x, anim.z, 30, 300);\n                }\n            }\n            // Phase 3: Settle & reward (2.6-3.2s) - dust settles, score appears, XP granted\n            else if (anim.timer >= BOSS_DEATH_DURATION) {\n                // Animation complete - grant rewards NOW\n                anim.active = false;\n                \n                gameState.score += anim.scoreReward;\n                gameState.enemiesDefeated++;\n                addFloatingText(anim.x, anim.size * 23, anim.z, '+' + anim.scoreReward + ' BOSS DEFEATED!');\n                \n                // Automatically register global score to leaderboard upon boss defeat\n                submitScoreToLeaderboard(gameState.score);\n                \n                // Spawn heart at boss location\n                spawnHeartPickup(anim.x, anim.z, 'boss');\n                \n                // Unpause THEN grant XP (so level-up mutation screen works correctly)\n                gameState.paused = false;\n                boss = null;\n                \n                // Grant XP after unpause - this may trigger level up + mutation screen\n                collectXP(anim.xpReward);\n                checkLevel();\n            }\n            \n            // Keep camera focused on explosion during animation\n            if (anim.active) {\n                const camLerpSpeed = 3;\n                cameraTarget.x += (anim.x - cameraTarget.x) * Math.min(1, dt * camLerpSpeed);\n                cameraTarget.z += (anim.z - cameraTarget.z) * Math.min(1, dt * camLerpSpeed);\n                updateCamera(dt);\n            }\n        }\n\n        function updateBossProjectiles(dt) {\n            const hitR = player.size * 4 + 4;\n            \n            for (let i = bossProjectiles.length - 1; i >= 0; i--) {\n                const p = bossProjectiles[i];\n                \n                // Delay before launching (for missiles)\n                if (p.delay && p.delay > 0) {\n                    p.delay -= dt;\n                    continue;\n                }\n                \n                // Homing behavior (fists and missiles both home)\n                if (p.homing) {\n                    const dx = player.x - p.x;\n                    const dz = player.z - p.z;\n                    const targetY = player.y + player.size * 6;\n                    const dy = targetY - p.y;\n                    const dist = Math.hypot(dx, dz) || 1;\n                    \n                    // Smooth homing - steer toward player\n                    const steerStrength = p.type === 'fist' ? 4 : 3;\n                    p.vx += (dx / dist) * p.speed * dt * steerStrength;\n                    p.vz += (dz / dist) * p.speed * dt * steerStrength;\n                    \n                    // Cap speed\n                    const spd = Math.hypot(p.vx, p.vz);\n                    if (spd > p.speed) { p.vx = (p.vx / spd) * p.speed; p.vz = (p.vz / spd) * p.speed; }\n                    \n                    // Smoothly adjust Y\n                    p.y += dy * dt * 2;\n                    \n                    // Rotate fist to face movement direction\n                    if (p.type === 'fist') {\n                        const moveAngle = Math.atan2(p.vx, p.vz);\n                        p.mesh.rotation.y = moveAngle;\n                        // Pulsing rocket flame\n                        if (p.flameMat) {\n                            p.flameMat.opacity = 0.6 + Math.sin(Date.now() * 0.02) * 0.3;\n                        }\n                        // Trailing smoke/fire particles\n                        if (Math.random() < 0.4) {\n                            createDebris(p.x - (p.vx / spd) * 3, p.y, p.z - (p.vz / spd) * 3, 0xff6600);\n                        }\n                    } else {\n                        p.mesh.rotation.x += dt * 5;\n                        p.mesh.rotation.z += dt * 3;\n                    }\n                } else {\n                    p.mesh.rotation.x += dt * 5;\n                    p.mesh.rotation.z += dt * 3;\n                }\n                \n                p.x += p.vx * dt;\n                p.z += p.vz * dt;\n                p.lifetime -= dt;\n                p.mesh.position.set(p.x, p.y, p.z);\n                \n                // Hit player\n                if (Math.hypot(player.x - p.x, player.z - p.z) < hitR && Math.abs(player.y + player.size * 5 - p.y) < hitR) {\n                    dealDamageToPlayer(p.damage * damageMultiplier, p.x, p.z);\n                    // Explosion effect\n                    addDustRing(p.x, p.z, 10, 0xff6644);\n                    addCameraShake(0.3);\n                    playSound('enemy_explosion_sound', 0.5);\n                    // Big fist explosion\n                    if (p.type === 'fist') {\n                        for (let k = 0; k < 6; k++) createDebris(p.x + (Math.random()-0.5)*6, p.y, p.z + (Math.random()-0.5)*6, 0xdddddd);\n                    }\n                    removeFromScene(p.mesh);\n                    bossProjectiles.splice(i, 1);\n                    continue;\n                }\n                \n                // Timeout (fists last longer since they chase)\n                if (p.lifetime <= 0) {\n                    removeFromScene(p.mesh);\n                    bossProjectiles.splice(i, 1);\n                }\n            }\n        }\n\n        function createBossHealthBar3D() {\n            if (!boss || boss.healthBar3D) return;\n            const group = new THREE.Group();\n            \n            // Background bar (dark)\n            const bgMat = new THREE.MeshBasicMaterial({ color: 0x222222, transparent: true, opacity: 0.85 });\n            const bgMesh = new THREE.Mesh(new THREE.BoxGeometry(14, 1.2, 0.3), bgMat);\n            group.add(bgMesh);\n            \n            // Border\n            const borderMat = new THREE.MeshBasicMaterial({ color: 0xff4444, transparent: true, opacity: 0.9 });\n            const borderMesh = new THREE.Mesh(new THREE.BoxGeometry(14.6, 1.6, 0.2), borderMat);\n            borderMesh.position.z = -0.1;\n            group.add(borderMesh);\n            \n            // Fill bar (red-orange gradient approximation)\n            const fillMat = new THREE.MeshBasicMaterial({ color: 0xff4422 });\n            const fillMesh = new THREE.Mesh(new THREE.BoxGeometry(13.6, 0.9, 0.4), fillMat);\n            fillMesh.position.z = 0.05;\n            group.add(fillMesh);\n            \n            // Name label using canvas texture\n            const labelCanvas = document.createElement('canvas');\n            labelCanvas.width = 512; labelCanvas.height = 64;\n            const ctx = labelCanvas.getContext('2d');\n            ctx.fillStyle = '#ff4444';\n            ctx.font = 'bold 38px Fredoka, Arial';\n            ctx.textAlign = 'center';\n            ctx.textBaseline = 'middle';\n            const bossName = BOSS_NAMES[Math.min(boss.tier - 1, BOSS_NAMES.length - 1)];\n            ctx.fillText(bossName, 256, 32);\n            const labelTex = new THREE.CanvasTexture(labelCanvas);\n            const labelMat = new THREE.SpriteMaterial({ map: labelTex, transparent: true });\n            const labelSprite = new THREE.Sprite(labelMat);\n            labelSprite.scale.set(12, 1.5, 1);\n            labelSprite.position.y = 1.8;\n            group.add(labelSprite);\n            \n            scene.add(group);\n            boss.healthBar3D = { group, fillMesh, bgMat, borderMat, fillMat, labelMat, labelTex };\n        }\n        \n        function updateBossHealthUI() {\n            if (!boss || boss.dead) return;\n            \n            if (!boss.healthBar3D) return;\n            const hb = boss.healthBar3D;\n            const percent = Math.max(0, boss.health / boss.maxHealth);\n            \n            // Scale fill bar width based on HP\n            hb.fillMesh.scale.x = percent;\n            hb.fillMesh.position.x = -(1 - percent) * 6.8; // Keep left-aligned\n            \n            // Color shifts from green->yellow->red based on HP\n            if (percent > 0.5) hb.fillMat.color.setHex(0xff6622);\n            else if (percent > 0.25) hb.fillMat.color.setHex(0xff3300);\n            else hb.fillMat.color.setHex(0xcc0000);\n            \n            // Position above boss head\n            const headHeight = boss.size * 22;\n            hb.group.position.set(boss.x, boss.y + headHeight, boss.z);\n            \n            // Always face camera\n            hb.group.lookAt(camera.position);\n        }\n\n        // Hook boss into AoE/line/cone/sweep damage functions\n        function damageBossFromAoE(x, z, radius, dmg) {\n            if (!boss || boss.dead) return;\n            if (Math.hypot(boss.x - x, boss.z - z) < radius + boss.size * 5) {\n                damageBoss(dmg);\n            }\n        }\n\n        // ==================== XP / LEVEL ====================\n        function collectXP(amount) {\n            if (amount <= 0) return;\n            gameState.xp += amount * player.xpMultiplier;\n        }\n\n        function checkLevel() {\n            if (!gameState.paused && !bossDeathAnim.active && gameState.xp >= gameState.xpToLevel) {\n                gameState.xp -= gameState.xpToLevel;\n                levelUp();\n            }\n        }\n\n        function levelUp() {\n            gameState.level++;\n            gameState.xpToLevel = Math.floor(180 * Math.pow(1.3, gameState.level - 1));\n            playSound('level_up_sound', 0.6);\n            \n            // Monster grows at each level up with stepped animation!\n            const oldSize = player.size;\n            recalcSize(); // Updates player.targetSize\n            startGrowthAnimation(oldSize, player.targetSize);\n            \n            // Subtle speed increase with each level\n            player.speedMul *= 1.05; // 5% faster per level\n\n            const flash = document.getElementById('level-up-flash');\n            flash.classList.remove('active');\n            void flash.offsetWidth;\n            flash.classList.add('active');\n            addCameraShake(0.4);\n\n            showMutationScreen();\n        }\n\n        function showMutationScreen() {\n            const pool = upgradeDefs.filter(def => (player.upgrades[def.id] || 0) < def.maxLevel);\n            if (pool.length === 0) {\n                // Everything maxed \u2014 give a bonus heal instead of an empty screen.\n                player.health = Math.min(player.maxHealth, player.health + player.maxHealth * 0.3);\n                return;\n            }\n            gameState.paused = true;\n\n            const choices = [];\n            const tmp = pool.slice();\n            \n            // Deterministic early upgrades: guarantee Jump (level 1) and Dash (level 2)\n            // Keep showing them until the player picks them (only for first level of each)\n            const jumpDef = upgradeDefs.find(d => d.id === 'jump');\n            const dashDef = upgradeDefs.find(d => d.id === 'dash');\n            const hasJump = (player.upgrades['jump'] || 0) >= 1;\n            const hasDash = (player.upgrades['dash'] || 0) >= 1;\n            \n            // Force Jump into choices if player hasn't picked it yet (first priority)\n            if (!hasJump && jumpDef && (player.upgrades['jump'] || 0) < jumpDef.maxLevel) {\n                choices.push(jumpDef);\n                const idx = tmp.findIndex(d => d.id === 'jump');\n                if (idx >= 0) tmp.splice(idx, 1);\n            }\n            \n            // Force Dash into choices if player hasn't picked it yet (second priority)\n            if (!hasDash && dashDef && (player.upgrades['dash'] || 0) < dashDef.maxLevel) {\n                if (!choices.find(d => d.id === 'dash')) {\n                    choices.push(dashDef);\n                    const idx = tmp.findIndex(d => d.id === 'dash');\n                    if (idx >= 0) tmp.splice(idx, 1);\n                }\n            }\n            \n            // Remove already-chosen defs from tmp\n            choices.forEach(c => {\n                const idx = tmp.findIndex(d => d.id === c.id);\n                if (idx >= 0) tmp.splice(idx, 1);\n            });\n            \n            // Fill remaining slots randomly\n            for (let i = choices.length; i < 3 && tmp.length > 0; i++) {\n                choices.push(tmp.splice(Math.floor(Math.random() * tmp.length), 1)[0]);\n            }\n\n            const cardsContainer = document.getElementById('mutation-cards');\n            cardsContainer.innerHTML = '';\n            choices.forEach(def => {\n                const lvl = player.upgrades[def.id] || 0;\n                const badge = lvl === 0 ? 'NEW!' : 'Lv ' + (lvl + 1);\n                const card = document.createElement('div');\n                card.className = 'mutation-card';\n                card.innerHTML =\n                    '<div class=\"mutation-card-icon\">' + getUpgradeIconHtml(def.id) + '</div>' +\n                    '<div class=\"mutation-card-text\">' +\n                        '<div class=\"mutation-card-name\">' + def.name + ' <span style=\"color:#ffd93d\">' + badge + '</span></div>' +\n                        '<div class=\"mutation-card-desc\">' + def.desc + '</div>' +\n                    '</div>';\n                const choose = (e) => { if (e) e.preventDefault(); selectUpgrade(def); };\n                card.addEventListener('click', choose);\n                card.addEventListener('touchend', choose);\n                cardsContainer.appendChild(card);\n            });\n            document.getElementById('mutation-screen').classList.add('active');\n        }\n\n        function selectUpgrade(def) {\n            playSound('mutation_select_sound', 0.5);\n            const lvl = (player.upgrades[def.id] || 0) + 1;\n            player.upgrades[def.id] = lvl;\n            if (def.onPick) def.onPick(lvl);\n            if (def.fire && lvl === 1) player.abilityCd[def.id] = def.cooldown(1) * 0.4; // fire soon after unlock\n            gameState.mutations.push(def.id);\n            document.getElementById('mutation-screen').classList.remove('active');\n            gameState.paused = false;\n            \n            // Check if boss should spawn at this level (after mutation chosen)\n            if (shouldSpawnBoss()) {\n                setTimeout(() => { if (gameState.running && !gameState.paused) spawnBoss(); }, 800);\n            }\n            \n            checkLevel(); // handle a queued level-up\n        }\n\n        // ==================== PARTICLES / VFX ====================\n        // Reusable debris geometry and material cache for performance\n        const debrisGeo = new THREE.BoxGeometry(1, 1, 1);\n        persistentSharedGeometries.add(debrisGeo);\n        const vfxCircleGeo = new THREE.CircleGeometry(1, 18);\n        persistentSharedGeometries.add(vfxCircleGeo);\n        const debrisMaterialCache = {};\n        function getDebrisMaterial(color) {\n            if (!debrisMaterialCache[color]) {\n                debrisMaterialCache[color] = new THREE.MeshLambertMaterial({ color, transparent: true });\n                persistentSharedMaterials.add(debrisMaterialCache[color]);\n            }\n            return debrisMaterialCache[color];\n        }\n        \n        function createDebris(x, y, z, color) {\n            // Limit debris creation when there are already many particles\n            if (particles.length > 60) return;\n            \n            const size = 1 + Math.random() * 1.5;\n            const mesh = new THREE.Mesh(debrisGeo, getDebrisMaterial(color));\n            mesh.scale.set(size, size, size);\n            mesh.position.set(x + (Math.random() - 0.5) * 5, y, z + (Math.random() - 0.5) * 5);\n            mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);\n            scene.add(mesh);\n            particles.push({\n                mesh,\n                vx: (Math.random() - 0.5) * 30,\n                vy: 12 + Math.random() * 22,\n                vz: (Math.random() - 0.5) * 30,\n                lifetime: 1.2 + Math.random() * 0.5\n            });\n        }\n\n        function addDustRing(x, z, maxScale, color) {\n            const mat = new THREE.MeshBasicMaterial({ color: color || 0xcfc3a8, transparent: true, opacity: 0.6, side: THREE.DoubleSide });\n            const mesh = new THREE.Mesh(new THREE.RingGeometry(0.6, 1.4, 24), mat);\n            mesh.rotation.x = -Math.PI / 2;\n            mesh.position.set(x, 0.4, z);\n            scene.add(mesh);\n            particles.push({ mesh, vx: 0, vy: 0, vz: 0, lifetime: 0.45, isRing: true, ringMax: Math.max(4, maxScale), ringAge: 0, ringMat: mat });\n        }\n\n        function addShockwave(x, z) {\n            const mat = new THREE.MeshBasicMaterial({ color: 0xffee66, transparent: true, opacity: 0.8, side: THREE.DoubleSide });\n            const mesh = new THREE.Mesh(new THREE.RingGeometry(1, 3, 32), mat);\n            mesh.rotation.x = -Math.PI / 2;\n            mesh.position.set(x, 0.5, z);\n            scene.add(mesh);\n            particles.push({ mesh, vx: 0, vy: 0, vz: 0, lifetime: 0.5, isRing: true, ringMax: 60, ringAge: 0, ringMat: mat });\n        }\n\n        function addGroundDamageEffect(x, z, radius, opts) {\n            if (particles.length > 70) return;\n\n            const options = opts || {};\n            const minCrater = options.minCrater !== undefined ? options.minCrater : 5;\n            const craterScale = options.craterScale !== undefined ? options.craterScale : 0.28;\n            const crackMin = options.crackMin !== undefined ? options.crackMin : 8;\n            const crackMax = options.crackMax !== undefined ? options.crackMax : 14;\n            const crackDensity = options.crackDensity !== undefined ? options.crackDensity : 0.28;\n            const segmentMin = options.segmentMin !== undefined ? options.segmentMin : 2;\n            const segmentRand = options.segmentRand !== undefined ? options.segmentRand : 2;\n            const debrisCount = options.debrisCount !== undefined\n                ? options.debrisCount\n                : Math.min(12, Math.max(7, Math.floor(radius * 0.22)));\n            const life = options.life !== undefined ? options.life : 1.9;\n            const fadeTime = options.fadeTime !== undefined ? options.fadeTime : 0.8;\n            const grow = options.grow !== undefined ? options.grow : 0.12;\n            const craterOpacity = options.craterOpacity !== undefined ? options.craterOpacity : 0.62;\n            const crackOpacity = options.crackOpacity !== undefined ? options.crackOpacity : 0.78;\n            const debrisColor = options.debrisColor !== undefined ? options.debrisColor : 0x7a654f;\n\n            const group = new THREE.Group();\n            group.position.set(x, 0, z);\n            const fadeMats = [];\n\n            const craterRadius = Math.max(minCrater, radius * craterScale);\n            const craterMat = new THREE.MeshBasicMaterial({\n                color: 0x2f2924,\n                transparent: true,\n                opacity: craterOpacity,\n                depthWrite: false\n            });\n            fadeMats.push({ mat: craterMat, opacity: craterMat.opacity });\n            const crater = new THREE.Mesh(new THREE.CylinderGeometry(craterRadius, craterRadius * 1.08, 0.08, 32), craterMat);\n            crater.position.y = 0.06;\n            group.add(crater);\n\n            const crackCount = Math.min(crackMax, Math.max(crackMin, Math.floor(radius * crackDensity)));\n            for (let i = 0; i < crackCount; i++) {\n                const baseAngle = (i / crackCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.22;\n                let dist = craterRadius * (0.55 + Math.random() * 0.2);\n                const segments = segmentMin + Math.floor(Math.random() * segmentRand);\n\n                for (let s = 0; s < segments; s++) {\n                    const segLen = radius * (0.14 + Math.random() * 0.11) * (1 - s * 0.12);\n                    const segWidth = Math.max(0.22, radius * 0.028 * (1 - s * 0.18));\n                    const angle = baseAngle + (Math.random() - 0.5) * (0.22 + s * 0.1);\n                    dist += segLen * 0.5;\n\n                    const crackMat = new THREE.MeshBasicMaterial({\n                        color: 0x1f1b18,\n                        transparent: true,\n                        opacity: crackOpacity,\n                        depthWrite: false\n                    });\n                    fadeMats.push({ mat: crackMat, opacity: crackMat.opacity });\n\n                    const crack = new THREE.Mesh(new THREE.BoxGeometry(segLen, 0.08, segWidth), crackMat);\n                    crack.position.set(Math.cos(angle) * dist, 0.12, Math.sin(angle) * dist);\n                    crack.rotation.y = -angle;\n                    group.add(crack);\n\n                    dist += segLen * 0.5 + radius * (0.025 + Math.random() * 0.025);\n                }\n            }\n\n            for (let i = 0; i < debrisCount; i++) {\n                const a = Math.random() * Math.PI * 2;\n                const r = craterRadius * 0.4 + Math.random() * radius * 0.45;\n                createDebris(x + Math.cos(a) * r, 1.4 + Math.random() * 1.4, z + Math.sin(a) * r, debrisColor);\n            }\n\n            scene.add(group);\n            particles.push({\n                mesh: group,\n                lifetime: life,\n                maxLife: life,\n                isGroundDamage: true,\n                fadeMats,\n                grow,\n                fadeTime\n            });\n        }\n\n        function addDashAfterimage(x, z, rotation, size, width, dx, dz) {\n            if (particles.length > 76) return;\n\n            const group = new THREE.Group();\n            group.position.set(x, 0.08, z);\n            group.rotation.y = rotation;\n            const fadeMats = [];\n\n            const addGhostEllipse = (rx, rz, y, color, opacity) => {\n                const mat = new THREE.MeshBasicMaterial({\n                    color,\n                    transparent: true,\n                    opacity,\n                    side: THREE.DoubleSide,\n                    depthWrite: false\n                });\n                fadeMats.push({ mat, opacity });\n                const mesh = new THREE.Mesh(vfxCircleGeo, mat);\n                mesh.rotation.x = -Math.PI / 2;\n                mesh.position.y = y;\n                mesh.scale.set(rx, rz, 1);\n                group.add(mesh);\n            };\n\n            const bodyWidth = Math.max(2.2, width * 0.55 + size * 1.8);\n            const bodyLength = Math.max(5.5, width * 1.45 + size * 4);\n            addGhostEllipse(bodyWidth, bodyLength, 0.08, 0x89ecff, 0.26);\n            addGhostEllipse(bodyWidth * 0.48, bodyLength * 0.62, 0.1, 0xe8fbff, 0.16);\n\n            scene.add(group);\n            particles.push({\n                mesh: group,\n                lifetime: 0.24,\n                maxLife: 0.24,\n                isDashAfterimage: true,\n                fadeMats,\n                grow: 0.28,\n                vx: -dx * 10,\n                vz: -dz * 10\n            });\n        }\n\n        function addLaserScorchMark(x, z, rotation, beamWidth) {\n            if (particles.length > 76) return;\n\n            const group = new THREE.Group();\n            group.position.set(x, 0.1, z);\n            group.rotation.y = rotation + (Math.random() - 0.5) * 0.45;\n            const fadeMats = [];\n\n            const addScorchLayer = (rx, rz, y, color, opacity) => {\n                const mat = new THREE.MeshBasicMaterial({\n                    color,\n                    transparent: true,\n                    opacity,\n                    side: THREE.DoubleSide,\n                    depthWrite: false\n                });\n                fadeMats.push({ mat, opacity });\n                const mesh = new THREE.Mesh(vfxCircleGeo, mat);\n                mesh.rotation.x = -Math.PI / 2;\n                mesh.position.y = y;\n                mesh.scale.set(rx, rz, 1);\n                group.add(mesh);\n            };\n\n            const markWidth = Math.max(1.4, beamWidth * 0.45);\n            const markLength = Math.max(3, beamWidth * (0.95 + Math.random() * 0.45));\n            addScorchLayer(markWidth, markLength, 0.04, 0x15100e, 0.55);\n            addScorchLayer(markWidth * 0.45, markLength * 0.45, 0.06, 0xff5a22, 0.24);\n\n            if (Math.random() < 0.35) {\n                createDebris(x + (Math.random() - 0.5) * markWidth, 1.2, z + (Math.random() - 0.5) * markWidth, 0xff6a2a);\n            }\n\n            scene.add(group);\n            particles.push({\n                mesh: group,\n                lifetime: 0.85,\n                maxLife: 0.85,\n                isLaserScorch: true,\n                fadeMats,\n                fadeTime: 0.5,\n                grow: 0.08\n            });\n        }\n\n        // Wall impact effect when monster bumps into buildings\n        function addWallImpact(bx, bz, bWidth, bDepth, bHeight, px, pz) {\n            // Determine which wall was hit based on player position relative to building\n            const dx = px - bx;\n            const dz = pz - bz;\n            \n            let impactX, impactZ, rotY;\n            const halfW = bWidth / 2;\n            const halfD = bDepth / 2;\n            \n            // Find the closest wall and position impact there\n            if (Math.abs(dx) / halfW > Math.abs(dz) / halfD) {\n                // Hit X-facing wall (east/west)\n                impactX = bx + (dx > 0 ? halfW + 0.1 : -halfW - 0.1);\n                impactZ = Math.max(bz - halfD * 0.8, Math.min(bz + halfD * 0.8, pz));\n                rotY = Math.PI / 2; // Face outward from wall\n            } else {\n                // Hit Z-facing wall (north/south)\n                impactZ = bz + (dz > 0 ? halfD + 0.1 : -halfD - 0.1);\n                impactX = Math.max(bx - halfW * 0.8, Math.min(bx + halfW * 0.8, px));\n                rotY = 0; // Face outward from wall\n            }\n            \n            // Impact height - around monster's midsection, clamped to building height\n            const impactY = Math.min(bHeight * 0.5, getMonsterTop() * 0.55 + 2);\n            \n            // Create the impact ring (oriented vertically on wall) - RED and LARGER\n            const mat = new THREE.MeshBasicMaterial({ \n                color: 0xff3030, \n                transparent: true, \n                opacity: 0.85, \n                side: THREE.DoubleSide \n            });\n            const mesh = new THREE.Mesh(new THREE.RingGeometry(0.6, 1.4, 20), mat);\n            mesh.position.set(impactX, impactY, impactZ);\n            mesh.rotation.y = rotY;\n            scene.add(mesh);\n            \n            // Scale based on monster size for satisfying feedback - larger than ground impacts\n            const impactScale = 10 + player.size * 4;\n            particles.push({ \n                mesh, vx: 0, vy: 0, vz: 0, \n                lifetime: 0.35, \n                isRing: true, \n                ringMax: impactScale, \n                ringAge: 0, \n                ringMat: mat \n            });\n        }\n\n        // Transient effect mesh (beam, cone, etc.) that fades out and optionally grows.\n        function addFlash(mesh, life, grow) {\n            mesh.material.transparent = true;\n            particles.push({ mesh, isFlash: true, grow: grow || 0, maxLife: life, lifetime: life });\n        }\n\n        function updateParticles(dt) {\n            for (let i = particles.length - 1; i >= 0; i--) {\n                const p = particles[i];\n                p.lifetime -= dt;\n\n                if (p.isRing) {\n                    p.ringAge += dt;\n                    const t = Math.min(1, p.ringAge / 0.45);\n                    const scale = 1 + t * p.ringMax;\n                    p.mesh.scale.set(scale, scale, 1);\n                    p.ringMat.opacity = 0.6 * (1 - t);\n                } else if (p.isGroundDamage) {\n                    const t = 1 - Math.max(0, p.lifetime) / p.maxLife;\n                    const scale = 1 + t * p.grow;\n                    p.mesh.scale.set(scale, 1, scale);\n                    const fade = Math.min(1, Math.max(0, p.lifetime / p.fadeTime));\n                    p.fadeMats.forEach(entry => { entry.mat.opacity = entry.opacity * fade; });\n                } else if (p.isDashAfterimage) {\n                    p.mesh.position.x += p.vx * dt;\n                    p.mesh.position.z += p.vz * dt;\n                    const t = 1 - Math.max(0, p.lifetime) / p.maxLife;\n                    const scale = 1 + t * p.grow;\n                    p.mesh.scale.set(scale, 1, scale);\n                    const fade = Math.max(0, p.lifetime / p.maxLife);\n                    p.fadeMats.forEach(entry => { entry.mat.opacity = entry.opacity * fade; });\n                } else if (p.isLaserScorch) {\n                    const t = 1 - Math.max(0, p.lifetime) / p.maxLife;\n                    const scale = 1 + t * p.grow;\n                    p.mesh.scale.set(scale, 1, scale);\n                    const fade = Math.min(1, Math.max(0, p.lifetime / p.fadeTime));\n                    p.fadeMats.forEach(entry => { entry.mat.opacity = entry.opacity * fade; });\n                } else if (p.isFlash) {\n                    if (p.grow) { const sc = 1 + p.grow * dt; p.mesh.scale.multiplyScalar(sc); }\n                    p.mesh.material.opacity = Math.max(0, p.lifetime / p.maxLife);\n                } else {\n                    p.mesh.position.x += p.vx * dt;\n                    p.mesh.position.y += p.vy * dt;\n                    p.mesh.position.z += p.vz * dt;\n                    p.vy -= GRAVITY * dt;\n                    p.mesh.rotation.x += dt * 5;\n                    p.mesh.rotation.z += dt * 3;\n                    if (p.mesh.position.y < 0) {\n                        p.mesh.position.y = 0;\n                        p.vy = -p.vy * 0.3;\n                        p.vx *= 0.8; p.vz *= 0.8;\n                    }\n                    const opacity = Math.min(1, p.lifetime);\n                    p.mesh.material.opacity = opacity;\n                    p.mesh.material.transparent = true;\n                }\n\n                if (p.lifetime <= 0) {\n                    removeFromScene(p.mesh);\n                    particles.splice(i, 1);\n                }\n            }\n            while (particles.length > 80) {\n                removeFromScene(particles[0].mesh);\n                particles.shift();\n            }\n        }\n\n        // ==================== CAMERA ====================\n        function updateCamera(dt) {\n            let targetX = player.x;\n            let targetZ = player.z;\n            let zoomFactor = 1.0;\n\n            if (bossDeathAnim.active) {\n                targetX = bossDeathAnim.x;\n                targetZ = bossDeathAnim.z;\n                zoomFactor = 0.72; // slight zoom in (closer camera) during boss destruction scene\n            }\n\n            cameraTarget.x += (targetX - cameraTarget.x) * Math.min(1, dt * 4);\n            cameraTarget.z += (targetZ - cameraTarget.z) * Math.min(1, dt * 4);\n\n            if (cameraShake.intensity > 0) {\n                cameraShake.x = (Math.random() - 0.5) * cameraShake.intensity * 10;\n                cameraShake.y = (Math.random() - 0.5) * cameraShake.intensity * 10;\n                cameraShake.intensity -= dt * 3;\n                if (cameraShake.intensity < 0) cameraShake.intensity = 0;\n            } else {\n                cameraShake.x = 0; cameraShake.y = 0;\n            }\n\n            // Sub-linear zoom-out: distance grows like size^0.55 while the monster\n            // grows like size, so the monster's absolute on-screen size keeps increasing.\n            // User zoom slider adds additional zoom multiplier (0.6x to 1.8x)\n            // Growth animation adds temporary zoom offset\n            const zoomMul = Math.max(0.4, (0.6 + zoomLevel * 1.2) + growthCameraZoomOffset) * zoomFactor;\n            const s = Math.min(player.size, 16);\n            const targetDist = CAM_BASE_DIST * Math.pow(s, CAM_EXP) * zoomMul;\n            const targetHeight = CAM_BASE_HEIGHT * Math.pow(s, CAM_EXP) * zoomMul;\n            if (camCurDist === 0) { camCurDist = targetDist; camCurHeight = targetHeight; }\n            camCurDist += (targetDist - camCurDist) * Math.min(1, dt * 3);\n            camCurHeight += (targetHeight - camCurHeight) * Math.min(1, dt * 3);\n\n            // Scale fog with the camera distance so the monster/nearby stays crisp at any zoom,\n            // while the edge of the loaded chunks always fades out into haze.\n            const camToTarget = Math.hypot(camCurDist, camCurHeight);\n            scene.fog.near = Math.max(50, camToTarget * 0.85);\n            scene.fog.far = Math.max(200, camToTarget + CHUNK_RADIUS * CHUNK_SIZE + 120);\n\n            camera.position.set(\n                cameraTarget.x + cameraShake.x,\n                camCurHeight + cameraShake.y,\n                cameraTarget.z + camCurDist\n            );\n            camera.lookAt(cameraTarget.x, 0, cameraTarget.z);\n            \n            // CRITICAL: Move lights to follow the player so shadows work far from origin\n            lightTarget.position.set(player.x, 0, player.z);\n            directionalLight.position.set(player.x + 70, 130, player.z + 50);\n            fillLight.position.set(player.x - 60, 70, player.z - 50);\n        }\n\n        function addCameraShake(intensity) {\n            cameraShake.intensity = Math.max(cameraShake.intensity, intensity);\n        }\n\n        // ==================== MINI MAP ====================\n        const minimapCanvas = document.getElementById('minimap-canvas');\n        const minimapCtx = minimapCanvas.getContext('2d');\n        const MINIMAP_RANGE = 180; // World units visible from center to edge\n        let minimapFrameCounter = 0; // Throttle minimap to every 3rd frame\n        const MINIMAP_SIZE = 190;\n        const MINIMAP_CENTER = MINIMAP_SIZE / 2;\n\n        function updateMiniMap() {\n            const ctx = minimapCtx;\n            const scale = MINIMAP_CENTER / MINIMAP_RANGE;\n\n            // Clear with dark background\n            ctx.fillStyle = 'rgba(20, 25, 35, 0.9)';\n            ctx.beginPath();\n            ctx.arc(MINIMAP_CENTER, MINIMAP_CENTER, MINIMAP_CENTER, 0, Math.PI * 2);\n            ctx.fill();\n\n            // Draw range rings\n            ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';\n            ctx.lineWidth = 1;\n            [0.33, 0.66, 1].forEach(r => {\n                ctx.beginPath();\n                ctx.arc(MINIMAP_CENTER, MINIMAP_CENTER, MINIMAP_CENTER * r, 0, Math.PI * 2);\n                ctx.stroke();\n            });\n\n            // Draw buildings (gray blocks)\n            ctx.fillStyle = 'rgba(80, 85, 95, 0.6)';\n            buildings.forEach(b => {\n                if (b.dead) return;\n                const dx = b.x - player.x;\n                const dz = b.z - player.z;\n                const dist = Math.sqrt(dx * dx + dz * dz);\n                if (dist > MINIMAP_RANGE * 1.2) return;\n                \n                const mx = MINIMAP_CENTER + dx * scale;\n                const mz = MINIMAP_CENTER + dz * scale;\n                const w = Math.max(2, b.width * scale * 0.8);\n                const d = Math.max(2, b.depth * scale * 0.8);\n                ctx.fillRect(mx - w/2, mz - d/2, w, d);\n            });\n\n            // Draw enemies with different colors\n            enemies.forEach(e => {\n                if (e.dead) return;\n                const dx = e.x - player.x;\n                const dz = e.z - player.z;\n                const dist = Math.sqrt(dx * dx + dz * dz);\n                if (dist > MINIMAP_RANGE * 1.5) return;\n                \n                const mx = MINIMAP_CENTER + dx * scale;\n                const mz = MINIMAP_CENTER + dz * scale;\n                \n                // Color by enemy type (red = fires projectiles)\n                if (e.type === 'police') {\n                    ctx.fillStyle = '#3498db'; // Blue (no projectiles)\n                    ctx.beginPath();\n                    ctx.arc(mx, mz, 4, 0, Math.PI * 2);\n                    ctx.fill();\n                } else if (e.type === 'tank') {\n                    ctx.fillStyle = '#e74c3c'; // Red (fires projectiles)\n                    ctx.fillRect(mx - 4, mz - 4, 8, 8);\n                } else if (e.type === 'helicopter') {\n                    ctx.fillStyle = '#e74c3c'; // Red (fires projectiles)\n                    // Triangle for helicopters\n                    ctx.beginPath();\n                    ctx.moveTo(mx, mz - 5);\n                    ctx.lineTo(mx - 4, mz + 4);\n                    ctx.lineTo(mx + 4, mz + 4);\n                    ctx.closePath();\n                    ctx.fill();\n                }\n                \n                // Pulsing effect for enemies outside main view\n                if (dist > 60) {\n                    const pulse = 0.5 + Math.sin(Date.now() * 0.006) * 0.5;\n                    ctx.strokeStyle = ctx.fillStyle;\n                    ctx.globalAlpha = pulse * 0.6;\n                    ctx.lineWidth = 2;\n                    ctx.beginPath();\n                    ctx.arc(mx, mz, 7, 0, Math.PI * 2);\n                    ctx.stroke();\n                    ctx.globalAlpha = 1;\n                }\n            });\n\n            // Draw boss (large pulsing orange/red indicator)\n            if (boss && !boss.dead) {\n                const dx = boss.x - player.x;\n                const dz = boss.z - player.z;\n                const mx = MINIMAP_CENTER + dx * scale;\n                const mz = MINIMAP_CENTER + dz * scale;\n                \n                // Large pulsing diamond shape\n                const pulse = 0.7 + Math.sin(Date.now() * 0.008) * 0.3;\n                ctx.fillStyle = '#ff4400';\n                ctx.globalAlpha = pulse;\n                ctx.beginPath();\n                const bossSize = 10;\n                ctx.moveTo(mx, mz - bossSize);\n                ctx.lineTo(mx + bossSize * 0.7, mz);\n                ctx.lineTo(mx, mz + bossSize);\n                ctx.lineTo(mx - bossSize * 0.7, mz);\n                ctx.closePath();\n                ctx.fill();\n                \n                // Outer ring\n                ctx.strokeStyle = '#ff8800';\n                ctx.lineWidth = 3;\n                ctx.beginPath();\n                ctx.arc(mx, mz, bossSize + 4, 0, Math.PI * 2);\n                ctx.stroke();\n                ctx.globalAlpha = 1;\n            }\n\n            // Draw heart pickups (red hearts with gold ring)\n            heartPickups.forEach(heart => {\n                if (heart.collected) return;\n                const dx = heart.x - player.x;\n                const dz = heart.z - player.z;\n                const dist = Math.sqrt(dx * dx + dz * dz);\n                if (dist > MINIMAP_RANGE * 1.5) return;\n                \n                const mx = MINIMAP_CENTER + dx * scale;\n                const mz = MINIMAP_CENTER + dz * scale;\n                \n                // Draw red heart icon (small heart shape)\n                ctx.fillStyle = '#ff4466';\n                ctx.beginPath();\n                ctx.arc(mx - 2, mz - 1, 3, 0, Math.PI * 2);\n                ctx.arc(mx + 2, mz - 1, 3, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.beginPath();\n                ctx.moveTo(mx - 5, mz);\n                ctx.lineTo(mx, mz + 5);\n                ctx.lineTo(mx + 5, mz);\n                ctx.fill();\n                \n                // Pulsing gold ring for visibility\n                const pulse = 0.5 + Math.sin(Date.now() * 0.008) * 0.5;\n                ctx.strokeStyle = '#ffd700';\n                ctx.globalAlpha = pulse * 0.7;\n                ctx.lineWidth = 2;\n                ctx.beginPath();\n                ctx.arc(mx, mz, 8, 0, Math.PI * 2);\n                ctx.stroke();\n                ctx.globalAlpha = 1;\n            });\n\n            // Draw star power-ups (golden stars - combined invincibility + speed)\n            invincibilityPickups.forEach(pickup => {\n                if (pickup.collected) return;\n                const dx = pickup.x - player.x;\n                const dz = pickup.z - player.z;\n                const dist = Math.sqrt(dx * dx + dz * dz);\n                if (dist > MINIMAP_RANGE * 1.5) return;\n                \n                const mx = MINIMAP_CENTER + dx * scale;\n                const mz = MINIMAP_CENTER + dz * scale;\n                \n                // Draw yellow star shape\n                ctx.fillStyle = '#ffd700';\n                ctx.beginPath();\n                for (let i = 0; i < 5; i++) {\n                    const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;\n                    const outerR = 6;\n                    const innerR = 3;\n                    ctx.lineTo(mx + Math.cos(angle) * outerR, mz + Math.sin(angle) * outerR);\n                    const innerAngle = angle + Math.PI / 5;\n                    ctx.lineTo(mx + Math.cos(innerAngle) * innerR, mz + Math.sin(innerAngle) * innerR);\n                }\n                ctx.closePath();\n                ctx.fill();\n                \n                // Pulsing yellow ring\n                const pulse = 0.5 + Math.sin(Date.now() * 0.01) * 0.5;\n                ctx.strokeStyle = '#ffff00';\n                ctx.globalAlpha = pulse * 0.8;\n                ctx.lineWidth = 2;\n                ctx.beginPath();\n                ctx.arc(mx, mz, 10, 0, Math.PI * 2);\n                ctx.stroke();\n                ctx.globalAlpha = 1;\n            });\n\n            // Draw player (monster) at center with direction indicator\n            const dirX = Math.sin(player.rotation);\n            const dirZ = Math.cos(player.rotation);\n            \n            // Monster body (green circle sized by monster size)\n            const monsterRadius = Math.min(12, 5 + player.size * 3);\n            ctx.fillStyle = config.monster.primaryColor;\n            ctx.beginPath();\n            ctx.arc(MINIMAP_CENTER, MINIMAP_CENTER, monsterRadius, 0, Math.PI * 2);\n            ctx.fill();\n            \n            // White border\n            ctx.strokeStyle = '#ffffff';\n            ctx.lineWidth = 2;\n            ctx.stroke();\n            \n            // Direction indicator (arrow/triangle pointing forward)\n            ctx.fillStyle = '#ffffff';\n            ctx.beginPath();\n            const arrowLen = monsterRadius + 8;\n            const arrowX = MINIMAP_CENTER + dirX * arrowLen;\n            const arrowZ = MINIMAP_CENTER + dirZ * arrowLen;\n            const perpX = -dirZ * 4;\n            const perpZ = dirX * 4;\n            ctx.moveTo(arrowX, arrowZ);\n            ctx.lineTo(MINIMAP_CENTER + dirX * (monsterRadius + 2) + perpX, MINIMAP_CENTER + dirZ * (monsterRadius + 2) + perpZ);\n            ctx.lineTo(MINIMAP_CENTER + dirX * (monsterRadius + 2) - perpX, MINIMAP_CENTER + dirZ * (monsterRadius + 2) - perpZ);\n            ctx.closePath();\n            ctx.fill();\n\n            // Clip to circle\n            ctx.globalCompositeOperation = 'destination-in';\n            ctx.beginPath();\n            ctx.arc(MINIMAP_CENTER, MINIMAP_CENTER, MINIMAP_CENTER - 1, 0, Math.PI * 2);\n            ctx.fill();\n            ctx.globalCompositeOperation = 'source-over';\n        }\n\n        // ==================== UI ====================\n        let bossProgressLastLevel = -1; // Cache to skip unnecessary DOM rebuilds\n        function updateBossProgressBar() {\n            // Only rebuild if level changed\n            if (gameState.level === bossProgressLastLevel) return;\n            bossProgressLastLevel = gameState.level;\n            \n            const bar = document.getElementById('boss-progress-bar');\n            const track = document.getElementById('boss-progress-track');\n            const skull = document.getElementById('boss-skull');\n            if (!bar || !track) return;\n\n            // Bosses spawn at levels 10, 15, 20, 25...\n            // Before level 10: progress toward level 10 (first boss)\n            // After a boss: progress toward next boss (every 5 levels)\n            let startLevel, endLevel;\n            if (gameState.level < 10) {\n                startLevel = 1;\n                endLevel = 10;\n            } else {\n                // Find current boss cycle\n                const lastBossLevel = 10 + Math.floor((gameState.level - 10) / 5) * 5;\n                if (gameState.level === lastBossLevel && boss && !boss.dead) {\n                    // Currently fighting a boss - hide progress\n                    bar.classList.remove('active');\n                    return;\n                }\n                startLevel = lastBossLevel;\n                endLevel = lastBossLevel + 5;\n                if (gameState.level >= endLevel) {\n                    startLevel = endLevel;\n                    endLevel = startLevel + 5;\n                }\n            }\n\n            bar.classList.add('active');\n\n            const totalSteps = endLevel - startLevel;\n            const nodeCount = totalSteps; // One node per level\n            const currentProgress = gameState.level - startLevel;\n            const filledNodes = currentProgress;\n\n            // Build track HTML\n            let html = '';\n            for (let i = 0; i < nodeCount; i++) {\n                if (i > 0) {\n                    const connFilled = i <= filledNodes ? ' filled' : '';\n                    html += '<div class=\"boss-connector' + connFilled + '\"></div>';\n                }\n                let nodeClass = 'boss-node';\n                if (i < filledNodes) nodeClass += ' filled';\n                else if (i === filledNodes) nodeClass += ' current';\n                html += '<div class=\"' + nodeClass + '\"></div>';\n            }\n            // Final connector to skull\n            const finalConnFilled = filledNodes >= nodeCount ? ' filled' : '';\n            html += '<div class=\"boss-connector' + finalConnFilled + '\"></div>';\n            track.innerHTML = html;\n\n            // Skull pulses when close to boss\n            skull.classList.toggle('imminent', filledNodes >= nodeCount - 1);\n        }\n\n        // Cache DOM references for frequently updated UI (avoid getElementById every frame)\n        const uiCache = {\n            healthFill: document.getElementById('health-fill'),\n            levelFill: document.getElementById('level-fill'),\n            levelLabel: document.getElementById('level-label'),\n            scoreText: document.getElementById('score-text'),\n            timerText: document.getElementById('timer-text'),\n            comboText: document.getElementById('combo-text')\n        };\n        let lastScoreStr = '', lastTimerStr = '', lastLevelStr = '';\n        \n        function updateUI() {\n            uiCache.healthFill.style.width = ((player.health / player.maxHealth) * 100) + '%';\n            uiCache.levelFill.style.width = ((gameState.xp / gameState.xpToLevel) * 100) + '%';\n            \n            const lvlStr = 'LVL ' + gameState.level;\n            if (lvlStr !== lastLevelStr) { lastLevelStr = lvlStr; uiCache.levelLabel.textContent = lvlStr; }\n            \n            const scoreStr = gameState.score.toLocaleString();\n            if (scoreStr !== lastScoreStr) { lastScoreStr = scoreStr; uiCache.scoreText.textContent = scoreStr; }\n\n            const minutes = Math.floor(gameState.timeElapsed / 60);\n            const seconds = Math.floor(gameState.timeElapsed % 60);\n            const timerStr = minutes + ':' + seconds.toString().padStart(2, '0');\n            if (timerStr !== lastTimerStr) { lastTimerStr = timerStr; uiCache.timerText.textContent = timerStr; }\n\n            // Update ability HUD\n            updateAbilityHUD();\n            updateBossProgressBar();\n\n            if (combo >= 3 && comboTimer > 0) {\n                uiCache.comboText.textContent = combo + 'x COMBO!';\n                uiCache.comboText.style.opacity = Math.min(1, comboTimer * 1.6);\n                uiCache.comboText.style.transform = 'translateX(-50%) scale(' + (1 + Math.min(combo, 40) * 0.012) + ')';\n            } else {\n                uiCache.comboText.style.opacity = 0;\n            }\n        }\n        \n        // Ability HUD: only rebuild DOM when upgrade set changes; just update cooldown bars each frame\n        let abilityHudKey = '';\n        const abilityHudSlots = []; // cached slot DOM refs\n        \n        function updateAbilityHUD() {\n            const hudEl = document.getElementById('ability-hud');\n            if (!hudEl) return;\n            \n            // Build a fast key from upgrade ids+levels to detect structure changes\n            let key = '';\n            for (const def of upgradeDefs) {\n                const lvl = player.upgrades[def.id] || 0;\n                if (lvl > 0) key += def.id + lvl + ',';\n            }\n            \n            // Full rebuild only if upgrades changed\n            if (key !== abilityHudKey) {\n                abilityHudKey = key;\n                abilityHudSlots.length = 0;\n                let html = '';\n                for (const def of upgradeDefs) {\n                    const lvl = player.upgrades[def.id] || 0;\n                    if (lvl <= 0) continue;\n                    const isAbility = !!def.fire;\n                    const slotClass = isAbility ? 'ability-slot' : 'ability-slot passive';\n                    html += '<div class=\"' + slotClass + '\" title=\"' + def.name + '\" data-aid=\"' + def.id + '\">' +\n                        '<div class=\"ability-icon\">' + getUpgradeIconHtml(def.id) + '</div>' +\n                        '<span class=\"ability-level\">x' + lvl + '</span>' +\n                        (isAbility ? '<div class=\"ability-cooldown\"></div>' : '') +\n                    '</div>';\n                }\n                hudEl.innerHTML = html;\n                // Cache references to cooldown bar elements\n                for (const def of upgradeDefs) {\n                    const lvl = player.upgrades[def.id] || 0;\n                    if (lvl <= 0 || !def.fire) continue;\n                    const slot = hudEl.querySelector('[data-aid=\"' + def.id + '\"]');\n                    if (slot) {\n                        abilityHudSlots.push({ id: def.id, slot, bar: slot.querySelector('.ability-cooldown') });\n                    }\n                }\n            }\n            \n            // Fast per-frame update: just cooldown bar widths and on-cooldown class\n            for (const cached of abilityHudSlots) {\n                const def = upgradeDefs.find(d => d.id === cached.id);\n                if (!def) continue;\n                const lvl = player.upgrades[cached.id] || 0;\n                const cd = player.abilityCd[cached.id] || 0;\n                const maxCd = def.cooldown(lvl) * player.cooldownMul;\n                const onCooldown = cd > 0;\n                const pct = onCooldown ? (1 - cd / maxCd) * 100 : 100;\n                if (cached.bar) cached.bar.style.width = pct + '%';\n                if (onCooldown) cached.slot.classList.add('on-cooldown');\n                else cached.slot.classList.remove('on-cooldown');\n            }\n        }\n\n        function addFloatingText(x, y, z, text) {\n            const screenPos = worldToScreen(x, y, z);\n            if (!screenPos) return;\n            const elem = document.createElement('div');\n            elem.className = 'floating-text';\n            elem.textContent = text;\n            elem.style.left = screenPos.x + 'px';\n            elem.style.top = screenPos.y + 'px';\n            document.querySelector('.ui-overlay').appendChild(elem);\n            setTimeout(() => elem.remove(), 900);\n        }\n\n        function worldToScreen(x, y, z) {\n            const vec = new THREE.Vector3(x, y, z);\n            vec.project(camera);\n            return {\n                x: (vec.x * 0.5 + 0.5) * gameWorld.clientWidth,\n                y: (-vec.y * 0.5 + 0.5) * gameWorld.clientHeight\n            };\n        }\n\n        // ==================== LEADERBOARD ====================\n        async function submitScoreToLeaderboard(score) {\n            try {\n                const response = await lib.addPlayerScoreToLeaderboard(score, 10);\n                if (response.success) {\n                    lib.log('Score submitted successfully! Rank: ' + response.userRank);\n                    // Display the player's rank on the game over screen\n                    const rankRow = document.getElementById('rank-row');\n                    const rankEl = document.getElementById('final-rank');\n                    if (rankRow && rankEl && response.userRank) {\n                        rankEl.textContent = '#' + response.userRank;\n                        rankRow.style.display = 'flex';\n                    }\n                    return response.userRank;\n                }\n            } catch (error) {\n                lib.log('Failed to submit score to leaderboard: ' + error.message);\n                // Game continues - leaderboard failure doesn't affect gameplay\n            }\n            return null;\n        }\n        \n        async function fetchAndDisplayLeaderboard() {\n            const entriesEl = document.getElementById('leaderboard-entries');\n            if (!entriesEl) return;\n            \n            try {\n                const response = await lib.getTopNEntriesFromLeaderboard(10);\n                \n                if (!response.entries || response.entries.length === 0) {\n                    entriesEl.innerHTML = '<div class=\"leaderboard-empty\">No scores yet - be the first!</div>';\n                    return;\n                }\n                \n                // Display top 10 entries (scrollable after 5)\n                let html = '';\n                response.entries.forEach((entry, index) => {\n                    const rank = index + 1;\n                    let rankClass = 'other';\n                    let rowClass = 'leaderboard-entry';\n                    \n                    if (rank === 1) { rankClass = 'gold'; rowClass += ' rank-1'; }\n                    else if (rank === 2) { rankClass = 'silver'; rowClass += ' rank-2'; }\n                    else if (rank === 3) { rankClass = 'bronze'; rowClass += ' rank-3'; }\n                    \n                    const formattedScore = entry.score.toLocaleString();\n                    \n                    const displayName = entry.username || 'Player';\n                    const truncatedName = displayName.length > 12 ? displayName.substring(0, 10) + '...' : displayName;\n                    \n                    html += '<div class=\"' + rowClass + '\">' +\n                        '<span class=\"leaderboard-rank ' + rankClass + '\">#' + rank + '</span>' +\n                        '<span class=\"leaderboard-name\">' + truncatedName + '</span>' +\n                        '<span class=\"leaderboard-score\">' + formattedScore + '</span>' +\n                    '</div>';\n                });\n                \n                entriesEl.innerHTML = html;\n            } catch (error) {\n                lib.log('Failed to fetch leaderboard: ' + error.message);\n                entriesEl.innerHTML = '<div class=\"leaderboard-empty\">Failed to load leaderboard</div>';\n            }\n        }\n\n        // ==================== GAME OVER / RESTART ====================\n        async function gameOver() {\n            gameState.running = false;\n            document.getElementById('ingame-home-btn').classList.remove('active');\n            stopBackgroundMusic(0.8); // Fade out battlefield music\n            playSound('game_over_sound', 0.6);\n            \n            // Hide rank row initially while we fetch\n            const rankRow = document.getElementById('rank-row');\n            if (rankRow) rankRow.style.display = 'none';\n            \n            // Submit score to leaderboard and wait for updated rank\n            await submitScoreToLeaderboard(gameState.score);\n            \n            document.getElementById('final-score').textContent = gameState.score.toLocaleString();\n            document.getElementById('final-level').textContent = gameState.level;\n            document.getElementById('final-buildings').textContent = gameState.buildingsDestroyed;\n            document.getElementById('final-enemies').textContent = gameState.enemiesDefeated;\n            const minutes = Math.floor(gameState.timeElapsed / 60);\n            const seconds = Math.floor(gameState.timeElapsed % 60);\n            document.getElementById('final-time').textContent = minutes + ':' + seconds.toString().padStart(2, '0');\n            document.getElementById('game-over-screen').classList.add('active');\n        }\n\n        function clearScene() {\n            // Hide tutorial if active without marking as seen\n            hideTutorial(false);\n            \n            // Clean up buff auras\n            if (invincibilityAura) {\n                scene.remove(invincibilityAura);\n                invincibilityAura.material.dispose();\n                invincibilityAura.geometry.dispose();\n                invincibilityAura = null;\n            }\n            \n            // Clean up boss\n            if (boss && boss.mesh) {\n                removeFromScene(boss.mesh);\n                boss = null;\n            }\n            bossProjectiles.forEach(p => removeFromScene(p.mesh));\n            bossProjectiles.length = 0;\n            bossIntro.active = false;\n            if (boss && boss.healthBar3D) {\n                removeFromScene(boss.healthBar3D.group);\n                if (boss.healthBar3D.labelTex) boss.healthBar3D.labelTex.dispose();\n                boss.healthBar3D = null;\n            }\n            document.getElementById('boss-warning-overlay').classList.remove('active');\n            \n            buildings.forEach(b => removeFromScene(b.mesh));\n            props.forEach(p => removeFromScene(p.mesh));\n            people.forEach(pp => removeFromScene(pp.mesh));\n            // Clean up enemy spotlights before removing enemies\n            enemies.forEach(e => {\n                if (e.mesh && e.mesh.userData.spotlight) {\n                    removeFromScene(e.mesh.userData.spotlight);\n                }\n                removeFromScene(e.mesh);\n            });\n            projectiles.forEach(p => removeFromScene(p.mesh));\n            particles.forEach(p => removeFromScene(p.mesh));\n            squashed.forEach(s => removeFromScene(s.mesh));\n            meteors.forEach(m => removeFromScene(m.mesh));\n            activeLasers.forEach(l => { removeFromScene(l.beam); removeFromScene(l.glow); });\n            heartPickups.forEach(h => removeFromScene(h.mesh));\n            invincibilityPickups.forEach(p => removeFromScene(p.mesh));\n            speedPickups.forEach(p => removeFromScene(p.mesh));\n            loadedChunks.forEach(c => removeFromScene(c.ground));\n            removeFromScene(monsterGroup);\n            \n            // Reset ground plane so it gets recreated fresh (don't dispose - material is shared)\n            if (groundPlane) {\n                scene.remove(groundPlane);\n                groundPlane = null;\n            }\n            \n            buildings.length = 0; props.length = 0; people.length = 0;\n            enemies.length = 0; projectiles.length = 0; particles.length = 0;\n            squashed.length = 0; meteors.length = 0; activeLasers.length = 0; heartPickups.length = 0;\n            invincibilityPickups.length = 0;\n            invincibilityTimer = 0; speedBoostTimer = 0;\n            loadedChunks.clear();\n        }\n\n        function applyDifficulty() {\n            if (config.gameplay.difficulty === 'relaxed') { spawnRateMultiplier = 0.8; damageMultiplier = 0.8; }\n            else if (config.gameplay.difficulty === 'intense') { spawnRateMultiplier = 1.2; damageMultiplier = 1.2; }\n            else { spawnRateMultiplier = 1; damageMultiplier = 1; }\n        }\n\n        function startGame() {\n            document.getElementById('ingame-home-btn').classList.add('active');\n            document.getElementById('quit-confirm-overlay').classList.remove('active');\n            clearScene();\n            gameState = newGameState();\n            gameState.running = true;\n            player = newPlayer();\n            cameraTarget = { x: 0, z: 0 };\n            camCurDist = 0; camCurHeight = 0;\n            combo = 0; comboTimer = 0;\n            tankSpawnTimer = 6;\n            heliSpawnTimer = 18;\n            difficultyTimer = 0;\n            // Reset UI caches\n            bossProgressLastLevel = -1;\n            abilityHudKey = '';\n            abilityHudSlots.length = 0;\n            lastScoreStr = ''; lastTimerStr = ''; lastLevelStr = '';\n            applyDifficulty();\n            generateCity();\n            createMonster();\n            \n            // Only show tutorial for first-time players\n            if (!userProfile.tutorialSeen) {\n                // Spawn tutorial building and show tutorial\n                spawnTutorialBuilding();\n                // Place a star pickup just past the tutorial building so player learns what it does\n                spawnInvincibilityPickup(0, -28, 'tutorial');\n                showTutorial();\n            } else {\n                // For returning players, still spawn the star in front of the monster!\n                spawnInvincibilityPickup(0, -28, 'start');\n            }\n            \n            document.getElementById('game-over-screen').classList.remove('active');\n            document.getElementById('start-screen').classList.remove('active');\n            document.getElementById('mutation-screen').classList.remove('active');\n        }\n\n        // ==================== SPAWNING / DIFFICULTY ====================\n        function updateSpawning(dt) {\n            let policeCount = 0, tankCount = 0, heliCount = 0;\n            for (let i = 0; i < enemies.length; i++) {\n                const e = enemies[i];\n                if (e.dead) continue;\n                if (e.type === 'police') policeCount++;\n                else if (e.type === 'tank') tankCount++;\n                else if (e.type === 'helicopter') heliCount++;\n            }\n\n            // Check if player is moving fast (trying to escape)\n            const playerSpeed = Math.sqrt(player.vx * player.vx + player.vz * player.vz);\n            const isMovingFast = playerSpeed > player.baseSpeed * player.speedMul * 0.3; // Lower threshold for more responsive spawning\n\n            // Level-based scaling - more enemies as you level up!\n            const levelBonus = Math.floor(gameState.level / 2); // +1 enemy cap every 2 levels\n            const lateGameBonus = gameState.level >= 10 ? Math.floor((gameState.level - 10) / 3) : 0; // Extra scaling after level 10\n\n            // Police spawn early and often - they're the first responders!\n            // Base 6 + time bonus + level bonus\n            const maxPolice = 6 + Math.floor(gameState.timeElapsed / 40) + levelBonus;\n            const policeSpawnRate = 0.8 + (gameState.level * 0.05); // Spawn faster at higher levels\n            if (policeCount < maxPolice && Math.random() < dt * policeSpawnRate * spawnRateMultiplier) {\n                spawnEnemy('police', isMovingFast);\n            }\n\n            // Tanks only appear at level 5+\n            if (gameState.level >= 5) {\n                tankSpawnTimer -= dt;\n                // Max tanks scales with level: 2 at level 5, up to 8+ at high levels\n                const maxTanks = 2 + Math.floor((gameState.level - 5) / 2) + lateGameBonus;\n                const tankSpawnInterval = Math.max(2.5, 6 - (gameState.level - 5) * 0.3); // Spawn faster at higher levels\n                if (tankSpawnTimer <= 0 && tankCount < maxTanks) {\n                    spawnEnemy('tank', isMovingFast);\n                    tankSpawnTimer = tankSpawnInterval / spawnRateMultiplier;\n                }\n            }\n\n            // Helicopters only appear at level 5+\n            if (gameState.level >= 5) {\n                heliSpawnTimer -= dt;\n                // Max helis scales with level: 1 at level 5, up to 6+ at high levels\n                const maxHelis = 1 + Math.floor((gameState.level - 5) / 3) + lateGameBonus;\n                const heliSpawnInterval = Math.max(4, 9 - (gameState.level - 5) * 0.4); // Spawn faster at higher levels\n                if (heliSpawnTimer <= 0 && heliCount < maxHelis) {\n                    spawnEnemy('helicopter', isMovingFast);\n                    heliSpawnTimer = heliSpawnInterval / spawnRateMultiplier;\n                }\n            }\n\n            difficultyTimer += dt;\n            if (difficultyTimer >= 30) {\n                difficultyTimer = 0;\n                spawnRateMultiplier *= 1.1;\n            }\n            if (gameState.timeElapsed > 60) {\n                damageMultiplier = (config.gameplay.difficulty === 'intense' ? 1.2 : 1) + (gameState.timeElapsed - 60) * 0.0025;\n            }\n\n            // Keep a dense crowd + traffic around the player as they roam.\n            maintainCrowd(dt);\n        }\n\n        // ==================== INPUT ====================\n        function setupInput() {\n            window.addEventListener('keydown', (e) => {\n                const key = e.key.toLowerCase();\n                keys[key] = true;\n                if (['w', 'a', 's', 'd', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(key)) e.preventDefault();\n            });\n            window.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; });\n            window.addEventListener('blur', () => {\n                Object.keys(keys).forEach(k => keys[k] = false);\n                joystick.active = false;\n            });\n\n            const joystickZone = document.getElementById('joystick-zone');\n            const joystickBase = document.getElementById('joystick-base');\n            const joystickKnob = document.getElementById('joystick-knob');\n\n            const startJoystick = (clientX, clientY, id) => {\n                joystick.touchId = id;\n                joystick.active = true;\n                joystick.baseX = clientX;\n                joystick.baseY = clientY;\n                joystick.x = 0; joystick.y = 0;\n                joystickBase.style.display = 'block';\n                joystickBase.style.left = (clientX - 60) + 'px';\n                joystickBase.style.top = (clientY - 60) + 'px';\n                joystickKnob.style.display = 'block';\n                joystickKnob.style.left = clientX + 'px';\n                joystickKnob.style.top = clientY + 'px';\n            };\n\n            const moveJoystick = (clientX, clientY) => {\n                let dx = clientX - joystick.baseX;\n                let dy = clientY - joystick.baseY;\n                const dist = Math.sqrt(dx * dx + dy * dy);\n                const maxRadius = 50;\n                if (dist > maxRadius) {\n                    const excess = dist - maxRadius;\n                    joystick.baseX += (dx / dist) * excess;\n                    joystick.baseY += (dy / dist) * excess;\n                    joystickBase.style.left = (joystick.baseX - 60) + 'px';\n                    joystickBase.style.top = (joystick.baseY - 60) + 'px';\n                    dx = clientX - joystick.baseX;\n                    dy = clientY - joystick.baseY;\n                }\n                joystick.x = dx / maxRadius;\n                joystick.y = dy / maxRadius; // screen-down (+y) maps to world +z (toward camera)\n                joystickKnob.style.left = clientX + 'px';\n                joystickKnob.style.top = clientY + 'px';\n            };\n\n            const endJoystick = () => {\n                joystick.active = false;\n                joystick.touchId = null;\n                joystick.x = 0; joystick.y = 0;\n                setTimeout(() => { joystickBase.style.display = 'none'; joystickKnob.style.display = 'none'; }, 100);\n            };\n\n            // Touch\n            joystickZone.addEventListener('touchstart', (e) => {\n                e.preventDefault();\n                if (audioContext && audioContext.state === 'suspended') audioContext.resume();\n                const touch = e.changedTouches[0];\n                startJoystick(touch.clientX, touch.clientY, touch.identifier);\n            }, { passive: false });\n\n            window.addEventListener('touchmove', (e) => {\n                for (const touch of e.changedTouches) {\n                    if (touch.identifier === joystick.touchId) {\n                        e.preventDefault();\n                        moveJoystick(touch.clientX, touch.clientY);\n                    }\n                }\n            }, { passive: false });\n\n            window.addEventListener('touchend', (e) => {\n                for (const touch of e.changedTouches) {\n                    if (touch.identifier === joystick.touchId) endJoystick();\n                }\n            });\n\n            // Mouse drag (desktop)\n            joystickZone.addEventListener('mousedown', (e) => {\n                if (audioContext && audioContext.state === 'suspended') audioContext.resume();\n                startJoystick(e.clientX, e.clientY, 'mouse');\n            });\n            window.addEventListener('mousemove', (e) => {\n                if (joystick.active && joystick.touchId === 'mouse') moveJoystick(e.clientX, e.clientY);\n            });\n            window.addEventListener('mouseup', () => {\n                if (joystick.touchId === 'mouse') endJoystick();\n            });\n\n            // Zoom slider removed - now fixed at optimal zoom level\n        }\n\n\n        // ==================== EDIT MODE ====================\n        function setupEditMode() {\n            document.getElementById('edit-banner').classList.add('active');\n            document.getElementById('start-screen').classList.add('hidden');\n            generateCity();\n            createMonster();\n            updateMonster(0.0001); // place the monster mesh\n            camera.position.set(0, 120, 170);\n            camera.lookAt(0, 0, 0);\n        }\n\n        // ==================== START SCREEN SETUP ====================\n        function setupStartScreen() {\n            // Load background image\n            const bgAsset = lib.getAsset('start_screen_bg');\n            if (bgAsset && bgAsset.url) {\n                document.getElementById('start-bg').style.backgroundImage = 'url(' + bgAsset.url + ')';\n            }\n            \n            // Load title logo\n            const titleAsset = lib.getAsset('title_logo');\n            const titleEl = document.getElementById('title-logo');\n            if (titleAsset && titleAsset.url) {\n                titleEl.src = titleAsset.url;\n            } else {\n                // Fallback to text\n                titleEl.style.display = 'none';\n                const fallbackTitle = document.createElement('div');\n                fallbackTitle.className = 'game-title';\n                fallbackTitle.innerHTML = '\ud83e\udd96 MEGA MUNCHER';\n                titleEl.parentNode.insertBefore(fallbackTitle, titleEl);\n            }\n            \n            // Load start button\n            const btnAsset = lib.getAsset('start_button');\n            const btnEl = document.getElementById('start-btn');\n            if (btnAsset && btnAsset.url) {\n                btnEl.src = btnAsset.url;\n            } else {\n                // Fallback to styled button\n                const fallbackBtn = document.createElement('button');\n                fallbackBtn.className = 'start-btn';\n                fallbackBtn.id = 'start-btn';\n                fallbackBtn.textContent = 'START';\n                btnEl.parentNode.replaceChild(fallbackBtn, btnEl);\n            }\n            \n            // Initialize buff icons\n            initBuffIcons();\n            \n            // Fetch and display leaderboard\n            fetchAndDisplayLeaderboard();\n            \n            // Create floating particles\n            createStartScreenParticles();\n        }\n        \n        function createStartScreenParticles() {\n            const overlay = document.getElementById('particles-overlay');\n            if (!overlay) return;\n            \n            const colors = ['rgba(255,200,100,0.6)', 'rgba(255,150,50,0.5)', 'rgba(255,100,50,0.4)', 'rgba(255,220,150,0.5)'];\n            \n            for (let i = 0; i < 25; i++) {\n                const particle = document.createElement('div');\n                particle.className = 'particle';\n                const size = 3 + Math.random() * 6;\n                particle.style.width = size + 'px';\n                particle.style.height = size + 'px';\n                particle.style.left = (Math.random() * 100) + '%';\n                particle.style.top = (-20 + Math.random() * 20) + 'px';\n                particle.style.background = colors[Math.floor(Math.random() * colors.length)];\n                particle.style.animationDuration = (8 + Math.random() * 12) + 's';\n                particle.style.animationDelay = (Math.random() * 8) + 's';\n                overlay.appendChild(particle);\n            }\n        }\n        \n        function startMenuMusic() {\n            if (assetCache.audio['title_music_epic']) {\n                playBackgroundMusic('title_music_epic', 0.45, true);\n            }\n        }\n\n        // ==================== MAIN ====================\n        // Show loading screen and load ONLY critical start-screen assets (fast!)\n        document.getElementById('loading-screen').classList.remove('hidden');\n        document.getElementById('start-screen').classList.remove('active');\n        \n        await preloadCriticalAssets();\n        \n        // Load user profile early (to check if tutorial should be shown later)\n        await loadUserProfile();\n        \n        // Hide loading screen, show start screen immediately\n        document.getElementById('loading-screen').classList.add('hidden');\n        document.getElementById('start-screen').classList.add('active');\n        \n        // Load all gameplay assets in background while player views start screen\n        loadDeferredAssets();\n\n        lib.showGameParameters({\n            name: 'Mega Muncher Settings',\n            params: {\n                'Graphics Quality (0=Auto,1=Best..5=Lowest)': {\n                    key: 'graphics.qualityMode',\n                    type: 'slider',\n                    min: 0,\n                    max: 5,\n                    step: 1,\n                    onChange: (val) => setQualityMode(val)\n                },\n                'Auto Downgrade Below FPS': {\n                    key: 'graphics.qualityDowngradeFps',\n                    type: 'slider',\n                    min: 30,\n                    max: 60,\n                    step: 1,\n                    onChange: (val) => {\n                        config.graphics.qualityDowngradeFps = val;\n                        ensureGraphicsConfig().qualityDowngradeFps = val;\n                    }\n                },\n                'Auto Upgrade Above FPS': {\n                    key: 'graphics.qualityUpgradeFps',\n                    type: 'slider',\n                    min: 35,\n                    max: 60,\n                    step: 1,\n                    onChange: (val) => {\n                        config.graphics.qualityUpgradeFps = val;\n                        ensureGraphicsConfig().qualityUpgradeFps = val;\n                    }\n                },\n                'Monster Color': { key: 'monster.primaryColor', type: 'color' },\n                'Belly Color': { key: 'monster.secondaryColor', type: 'color' },\n                'City Density': {\n                    key: 'city.density', type: 'dropdown',\n                    options: [\n                        { label: 'Sparse', value: 'sparse' },\n                        { label: 'Normal', value: 'normal' },\n                        { label: 'Dense', value: 'dense' }\n                    ]\n                },\n                'Difficulty': {\n                    key: 'gameplay.difficulty', type: 'dropdown',\n                    options: [\n                        { label: 'Relaxed', value: 'relaxed' },\n                        { label: 'Normal', value: 'normal' },\n                        { label: 'Intense', value: 'intense' }\n                    ]\n                }\n            }\n        });\n\n        if (mode === 'edit') {\n            setupStartScreen();\n            setupEditMode();\n            let lastTime = performance.now();\n            function editLoop(timestamp) {\n                const rawFrameMs = timestamp - lastTime;\n                lastTime = timestamp;\n                updateAdaptiveQuality(rawFrameMs);\n                const angle = timestamp * 0.0001;\n                camera.position.x = Math.sin(angle) * 175;\n                camera.position.z = Math.cos(angle) * 175;\n                camera.position.y = 120;\n                camera.lookAt(0, 0, 0);\n                enemies.forEach(e => { if (e.rotor) e.rotor.rotation.y += 0.3; });\n                renderer.render(scene, camera);\n                requestAnimationFrame(editLoop);\n            }\n            requestAnimationFrame(editLoop);\n        } else {\n            setupInput();\n            setupStartScreen();\n            generateCity();\n            createMonster();\n            cameraTarget = { x: 0, z: 0 };\n            applyDifficulty();\n            \n            // Initialize camera position before first render to prevent black screen\n            camCurDist = CAM_BASE_DIST * Math.pow(STARTING_SIZE, CAM_EXP);\n            camCurHeight = CAM_BASE_HEIGHT * Math.pow(STARTING_SIZE, CAM_EXP);\n            camera.position.set(0, camCurHeight, camCurDist);\n            camera.lookAt(0, 0, 0);\n            \n            // Ensure lights are positioned correctly for initial render\n            lightTarget.position.set(0, 0, 0);\n            directionalLight.position.set(70, 130, 50);\n            fillLight.position.set(-60, 70, -50);\n\n            // Start menu music when start screen is clicked (music may still be loading)\n            document.getElementById('start-screen').addEventListener('click', () => {\n                if (audioContext && audioContext.state === 'suspended') {\n                    audioContext.resume().then(() => startMenuMusic());\n                } else {\n                    startMenuMusic();\n                }\n            }, { once: true });\n\n            document.getElementById('start-btn').addEventListener('click', () => {\n                // Block start if assets are still loading\n                if (!deferredAssetsReady) return;\n                if (audioContext && audioContext.state === 'suspended') audioContext.resume();\n                stopBackgroundMusic(0.5);\n                startGame();\n                // Start battlefield music after a brief delay\n                setTimeout(() => {\n                    if (assetCache.audio['battlefield_music_epic']) {\n                        playBackgroundMusic('battlefield_music_epic', 0.4, true);\n                    }\n                }, 600);\n            });\n            document.getElementById('restart-btn').addEventListener('click', () => {\n                startGame();\n                // Start battlefield music\n                setTimeout(() => {\n                    if (assetCache.audio['battlefield_music_epic']) {\n                        playBackgroundMusic('battlefield_music_epic', 0.4, true);\n                    }\n                }, 300);\n            });\n            \n            // In-game home button (quit early)\n            const ingameHomeBtn = document.getElementById('ingame-home-btn');\n            const quitOverlay = document.getElementById('quit-confirm-overlay');\n            const quitYes = document.getElementById('quit-confirm-yes');\n            const quitNo = document.getElementById('quit-confirm-no');\n            \n            ingameHomeBtn.addEventListener('click', () => {\n                if (gameState.running) {\n                    gameState.paused = true;\n                    quitOverlay.classList.add('active');\n                }\n            });\n            quitNo.addEventListener('click', () => {\n                quitOverlay.classList.remove('active');\n                gameState.paused = false;\n            });\n            quitYes.addEventListener('click', () => {\n                quitOverlay.classList.remove('active');\n                gameState.paused = false;\n                gameOver();\n            });\n\n            document.getElementById('home-btn').addEventListener('click', () => {\n                // Return to start screen\n                document.getElementById('game-over-screen').classList.remove('active');\n                document.getElementById('start-screen').classList.add('active');\n                // Refresh leaderboard\n                fetchAndDisplayLeaderboard();\n                // Start menu music again\n                startMenuMusic();\n            });\n\n            let lastTime = performance.now();\n            function gameLoop(timestamp) {\n                const rawFrameMs = timestamp - lastTime;\n                const dt = Math.min(rawFrameMs / 1000, MAX_DELTA);\n                lastTime = timestamp;\n                updateAdaptiveQuality(rawFrameMs);\n\n                // Implement time-slow instead of time-stop during the boss destruction scene!\n                const activeDt = bossDeathAnim.active ? dt * 0.20 : dt;\n\n                if (gameState.running && !gameState.paused) {\n                    gameState.timeElapsed += activeDt;\n                    updateTutorial(activeDt);\n                    updateMonster(activeDt);\n                    updateChunks();\n                    updateCrush(activeDt);\n                    updateAbilities(activeDt);\n                    updateTailSweepSpin(activeDt);\n                    updateDash(activeDt);\n                    updateActiveLasers(activeDt);\n                    updateMeteors(activeDt);\n                    updatePeople(activeDt);\n                    updateProps(activeDt);\n                    updateEnemies(activeDt);\n                    updateProjectiles(activeDt);\n                    updateBoss(activeDt);\n                    updateBossDeathAnim(dt); // Run death animation sequence on normal unscaled time so it pops off with maximum speed and satisfaction!\n                    updateBossProjectiles(activeDt);\n                    updateDestruction(activeDt);\n                    updateSquashed(activeDt);\n                    updateParticles(activeDt);\n                    updateSpawning(activeDt);\n                    updateHeartPickups(activeDt);\n                    updateInvincibilityPickups(activeDt);\n                    updateBuffTimers(activeDt);\n                    updateCamera(dt); // Camera maintains smooth tracking\n                    updateCameraOcclusion();\n                    updateMonsterHitFlash(activeDt);\n                    checkLevel();\n                } else {\n                    updateBossIntro(dt);\n                    updateBossDeathAnim(dt);\n                    updateDestruction(dt);\n                    updateMonsterHitFlash(dt);\n                    updateSquashed(dt);\n                    updateParticles(dt);\n                }\n\n                updateUI();\n                minimapFrameCounter++;\n                if (minimapFrameCounter >= 3) { minimapFrameCounter = 0; updateMiniMap(); }\n                renderer.render(scene, camera);\n                requestAnimationFrame(gameLoop);\n            }\n            requestAnimationFrame(gameLoop);\n        }\n    }\n    </script>\n</body>\n</html>\n", "game_config": "{\"monster\":{\"primaryColor\":\"#2ecc71\",\"secondaryColor\":\"#f1c40f\",\"eyeColor\":\"#e74c3c\",\"startingSize\":0.39},\"city\":{\"density\":\"normal\",\"propDensity\":\"normal\",\"colorPalette\":\"vibrant\"},\"gameplay\":{\"difficulty\":\"normal\",\"xpGainRate\":1},\"graphics\":{\"qualityMode\":5,\"qualityDowngradeFps\":48,\"qualityUpgradeFps\":57}}", "game_design_document": "## Core Game Loop and Mechanics\n\n**Mega Muncher: City Rampage** is a 3D top-down action roguelite where players control a giant monster wreaking havoc on a destructible voxel city. The core loop is designed to be immediately satisfying and progressively chaotic:\n\n### Moment-to-Moment Gameplay\n1. **Movement & Destruction**: Player navigates through a compact 3D city, automatically causing destruction through collision and deliberate attacks\n2. **Combat**: Fight waves of increasingly aggressive military forces (police cars, tanks, helicopters)\n3. **Resource Collection**: Destroyed objects drop XP orbs that fly toward the player\n4. **Progression**: Fill XP bar \u2192 Level up \u2192 Choose 1 of 3 random mutations \u2192 Grow stronger\n5. **Escalation**: Enemies become more numerous and dangerous; player becomes more powerful\n6. **Survival**: Continue until health depletes, then restart with a new run\n\n### What Makes It Compelling\n- **Instant Gratification**: First 10 seconds deliver satisfying destruction with buildings crumbling, debris flying, and score numbers popping\n- **Power Fantasy**: Player feels genuinely powerful as a giant monster, with scale reinforced through camera, size comparison, and destruction impact\n- **Strategic Depth**: Mutation choices create meaningful build variety and replayability\n- **Escalating Chaos**: Difficulty ramps smoothly from relaxing destruction to frantic combat survival\n- **Juicy Feedback**: Every action triggers screen shake, particles, sound effects, and visual feedback\n\n### Win/Lose Conditions\n- **No traditional \"win\"**: This is an endless survival roguelite\n- **Success Metrics**: High score, time survived, level reached, buildings destroyed, enemies defeated\n- **Loss Condition**: Health reaches zero from enemy attacks\n- **Restart Flow**: Quick restart button on game over screen encourages \"one more run\" mentality\n\n### Engagement Drivers\n- **Roguelite Variety**: Each run feels different due to random mutation offerings\n- **Score Chasing**: Leaderboard-style final stats encourage beating personal bests\n- **Build Experimentation**: Players want to try different mutation combinations\n- **Satisfying Destruction**: Core destruction mechanics are inherently fun and stress-relieving\n- **Progressive Mastery**: Players learn enemy patterns, optimal mutation synergies, and efficient destruction routes\n\n## Starting Scope\n\nThe first iteration must deliver a **complete, polished, playable experience** while remaining technically feasible. Prioritize core fun over feature completeness.\n\n### Must Have (First Iteration)\n1. **3D City Scene**\n   - Small procedurally generated city grid (approximately 10x10 blocks)\n   - 3 building height tiers: small (1-2 stories), medium (3-5 stories), tall (6-8 stories)\n   - Simple roads in grid pattern\n   - Scattered destructible props: cars, trees, street lamps\n   - Colorful low-poly/voxel aesthetic with simple Three.js geometry\n\n2. **Player Monster**\n   - Chunky, cute kaiju character (simple geometric shapes)\n   - Smooth WASD/arrow key movement\n   - Basic attack (bite/stomp) on spacebar/click\n   - Clear visual feedback for taking damage\n   - Growth animation when leveling up\n\n3. **Destruction System**\n   - Buildings have health values (small: 2-3 hits, medium: 4-6 hits, tall: 8-12 hits)\n   - Props break in 1-2 hits\n   - Simple particle effects on destruction (debris chunks, dust clouds)\n   - Objects drop XP orbs when destroyed\n   - Score popups on destruction\n\n4. **Enemy System**\n   - **Police Cars**: Chase player, deal contact damage, low health\n   - **Tanks**: Stay at range, shoot slow projectiles, medium health\n   - Progressive spawning (start with police, add tanks after 30 seconds)\n   - Simple AI: police chase, tanks maintain distance and shoot\n\n5. **XP and Leveling**\n   - XP bar fills from collecting orbs\n   - Level up every 100 XP (scales slightly each level)\n   - Pause game on level up\n   - Show 3 random mutation cards\n   - Player selects one, game resumes\n\n6. **Mutation System (6-8 mutations)**\n   - **Bigger Bite**: +50% attack damage and range\n   - **Iron Skin**: -30% damage taken\n   - **Fast Feet**: +40% movement speed\n   - **Regeneration**: Heal 1 HP every 3 seconds\n   - **City Eater**: +50% XP from buildings\n   - **Giant Growth**: +20% size, +25% max health, +15% damage\n   - **Thunder Stomp**: Attack creates small shockwave damaging nearby enemies/buildings\n   - **Magnetic Hunger**: XP orbs pulled from 2x distance\n\n7. **UI Elements**\n   - Health bar (top left)\n   - XP bar (bottom center)\n   - Score (top right)\n   - Timer (top center)\n   - Level indicator\n   - Mutation selection screen with clear card design\n\n8. **Camera System**\n   - Third-person top-down angled perspective\n   - Smooth follow with slight lag\n   - Camera shake on impacts (buildings, attacks, damage)\n   - Maintains good view of surrounding area\n\n9. **Game Feel Polish**\n   - Screen shake on all major impacts\n   - Hit flashes on damaged objects\n   - Particle effects for destruction\n   - Floating score numbers\n   - XP orbs fly toward player with smooth animation\n   - Monster grows slightly with each level\n   - Impact sounds for key actions\n\n10. **Mobile Controls**\n    - Virtual joystick (left side, appears on touch)\n    - Large attack button (bottom right)\n    - Touch-friendly mutation selection\n    - Responsive UI scaling\n\n11. **Game Over Screen**\n    - Final score, time survived, level reached\n    - Buildings destroyed count\n    - Enemies defeated count\n    - Large restart button\n\n### Should Have (Second Iteration)\n- **Helicopters**: Flying enemies that circle and shoot\n- **Special Attack System**: Meter fills from destruction, unleashes powerful area attack\n- **More Mutations**: Expand to 12-15 total options (Fire Breath, Toxic Breath, Meteor Tail, Roar Blast, etc.)\n- **Building Variety**: Different building colors, shapes, and visual styles\n- **Enhanced Particles**: More sophisticated debris, explosion effects, dust trails\n- **Sound Effects**: Footsteps, destruction, attacks, level up, game over\n- **Difficulty Scaling**: More sophisticated enemy spawn rates and damage scaling\n- **Combo System**: Bonus score for destroying multiple objects quickly\n\n### Could Have (Future Iterations)\n- **Boss Enemies**: Giant mech or military commander every few minutes\n- **More Enemy Types**: Missile trucks, fighter jets, anti-kaiju robots\n- **Advanced Mutations**: Demolition Aura, Rage Mode, Armored Scales\n- **City Biomes**: Different city themes (downtown, suburbs, industrial)\n- **Daily Challenges**: Special modifiers or goals\n- **Unlockable Monsters**: Different kaiju with unique abilities\n- **Background Music**: Dynamic soundtrack that intensifies with gameplay\n- **Persistent Progression**: Unlock new mutations or starting bonuses between runs\n\n## Game Perspective\n\n**3D Third-Person Top-Down Angled View**\n\nThis perspective is optimal for the game because:\n- **Spatial Awareness**: Players can see surrounding buildings, enemies, and threats clearly\n- **Scale Communication**: The angled view emphasizes the monster's size relative to the city\n- **Action Clarity**: Top-down angle makes it easy to aim attacks and navigate dense city environments\n- **Destruction Visibility**: Players can see buildings collapse and debris scatter in satisfying ways\n- **Combat Readability**: Enemy positions and projectiles are clearly visible\n- **Mobile Friendly**: Works well on portrait orientation without requiring complex camera controls\n\n### Camera Specifications\n- **Position**: Elevated behind and above the monster at approximately 45-60 degree angle\n- **Distance**: Far enough to show 3-4 building blocks in each direction around the monster\n- **Follow Behavior**: Smooth lerp follow with slight lag (0.1-0.2 second delay) for natural feel\n- **Height**: Adjusts slightly as monster grows to maintain consistent framing\n- **Shake**: Subtle shake on impacts (0.5-2 units depending on impact size)\n- **Constraints**: Camera never goes below ground plane or inside buildings\n\n## Game World and Level Design\n\n### City Generation System\n\nThe city is procedurally generated at game start to create a dense, destructible playground. The generation creates a compact urban environment that ensures players always have something nearby to destroy.\n\n#### City Layout (10x10 Grid)\n- **Grid Structure**: 10x10 block grid with roads between blocks\n- **Block Size**: Each city block is approximately 20x20 units\n- **Road Width**: 8 units wide, creating clear pathways\n- **Total Play Area**: Approximately 280x280 units (including roads)\n- **Boundaries**: Invisible walls at city edges (or endless city wrapping for future iteration)\n\n#### Building Distribution\nEach city block contains 1-4 buildings with spacing between them:\n\n**Small Buildings (40% of buildings)**\n- Height: 8-15 units (1-2 stories)\n- Footprint: 8x8 to 12x12 units\n- Health: 50 HP (2-3 hits to destroy)\n- XP Value: 10 XP\n- Score Value: 100 points\n- Visual: Simple box geometry with 2-4 window rows\n\n**Medium Buildings (40% of buildings)**\n- Height: 20-35 units (3-5 stories)\n- Footprint: 12x12 to 18x18 units\n- Health: 120 HP (4-6 hits to destroy)\n- XP Value: 25 XP\n- Score Value: 250 points\n- Visual: Box geometry with more window detail, possibly slight taper\n\n**Tall Buildings (20% of buildings)**\n- Height: 45-60 units (6-8 stories)\n- Footprint: 15x15 to 20x20 units\n- Health: 250 HP (8-12 hits to destroy)\n- XP Value: 50 XP\n- Score Value: 500 points\n- Visual: Skyscraper-style with multiple window sections, antenna on top\n\n#### Building Colors\nUse a vibrant, toy-like color palette:\n- Primary colors: Bright blue, red, yellow, green, orange, purple\n- Secondary colors: Teal, pink, lime, magenta\n- Each building gets random primary color with slightly darker shade for windows\n- Roofs are slightly lighter shade of building color\n\n#### Props and Details\n\n**Cars (Scattered on Roads)**\n- Count: 30-40 cars total\n- Size: 4x2x2 units\n- Health: 20 HP (1 hit to destroy)\n- XP Value: 5 XP\n- Score Value: 50 points\n- Colors: Random bright colors\n- Behavior: Static (no driving AI in first iteration)\n\n**Trees (Scattered in Blocks)**\n- Count: 20-30 trees\n- Size: 3x3 base, 8-12 units tall\n- Health: 10 HP (1 hit to destroy)\n- XP Value: 2 XP\n- Score Value: 20 points\n- Visual: Simple cylinder trunk with sphere or cone canopy\n\n**Street Lamps (Along Roads)**\n- Count: 40-50 lamps\n- Size: 1x1 base, 6 units tall\n- Health: 5 HP (1 hit to destroy)\n- XP Value: 1 XP\n- Score Value: 10 points\n- Visual: Thin cylinder with small sphere on top\n\n#### Destruction Physics\n\nBuildings use a simplified destruction system optimized for performance:\n\n**Destruction Stages**\n1. **Intact**: Full color, no damage\n2. **Damaged (50-75% HP)**: Slight color darkening, small cracks (texture or geometry detail)\n3. **Heavily Damaged (25-50% HP)**: Darker color, visible damage, slight tilt\n4. **Critical (0-25% HP)**: Very dark, smoking particles, pronounced tilt\n5. **Destroyed (0 HP)**: Building collapses into debris particles, removed from scene\n\n**Debris System**\n- When destroyed, buildings spawn 8-15 debris chunks\n- Debris are smaller boxes (2-4 units) in building's color\n- Debris have simple physics: initial velocity outward and upward, then fall with gravity\n- Debris fade out after 2-3 seconds\n- Debris are pooled and reused for performance\n\n**Particle Effects**\n- Dust clouds on impact (gray/white particles)\n- Debris chunks on destruction (colored particles matching building)\n- Small impact rings on ground for stomps\n- All particles use simple billboard sprites or point sprites for performance\n\n### Monster Starting Position\n- Spawns at city center or random edge\n- Starts at size 1.0 (approximately 12 units tall, 8 units wide)\n- Clearly larger than cars, smaller than medium buildings initially\n\n### Enemy Spawn System\n\n**Police Cars**\n- Spawn at city edges\n- Spawn rate: 1 every 5 seconds (first 60 seconds)\n- Max active: 6 police cars\n- Speed: 80% of player base speed\n- Chase behavior: Move directly toward player\n- Attack: Contact damage (5 HP per second of contact)\n- Health: 30 HP\n- XP Value: 15 XP\n- Score Value: 100 points\n\n**Tanks**\n- Spawn at city edges after 30 seconds\n- Spawn rate: 1 every 10 seconds (after 30 seconds)\n- Max active: 4 tanks\n- Speed: 50% of player base speed\n- Behavior: Maintain 30-40 unit distance, shoot every 2 seconds\n- Projectile: Slow-moving sphere (speed: 60 units/sec), 15 damage\n- Health: 80 HP\n- XP Value: 30 XP\n- Score Value: 250 points\n\n**Difficulty Scaling**\n- Every 30 seconds, spawn rates increase by 10%\n- Every 60 seconds, enemy damage increases by 10%\n- After 90 seconds, max active enemies increases by 1\n- After 120 seconds, helicopters begin spawning (second iteration)\n\n## Edit Mode Design\n\nEdit mode allows players to customize the visual appearance and some gameplay parameters of their monster and the city environment. The goal is to provide meaningful customization without breaking game balance.\n\n### Editable Properties\n\n#### Monster Customization\n**Body Colors**\n- Primary color (main body)\n- Secondary color (belly/accents)\n- Eye color\n- Default: Green body, yellow belly, red eyes\n- Constraint: Colors must have sufficient contrast for visibility\n\n**Monster Type**\n- Preset body shapes: Classic Kaiju, Dinosaur, Lizard, Blob Monster\n- Each type uses different geometry but same hitboxes and mechanics\n- Default: Classic Kaiju\n\n**Starting Size Multiplier**\n- Range: 0.8x to 1.2x\n- Affects visual size only, not gameplay stats\n- Default: 1.0x\n\n#### City Customization\n**Building Color Palette**\n- Choose from preset palettes: Vibrant (default), Pastel, Neon, Monochrome, Sunset\n- Affects all building colors in the city\n- Default: Vibrant\n\n**City Density**\n- Options: Sparse (fewer buildings), Normal (default), Dense (more buildings)\n- Affects building count per block\n- Sparse: 1-2 buildings per block\n- Normal: 1-4 buildings per block\n- Dense: 2-5 buildings per block\n- Default: Normal\n\n**Prop Density**\n- Options: Minimal, Normal (default), Maximum\n- Affects car, tree, and lamp counts\n- Default: Normal\n\n#### Gameplay Parameters (Subtle Adjustments)\n**Starting Difficulty**\n- Options: Relaxed, Normal (default), Intense\n- Relaxed: Enemy spawn rate -20%, enemy damage -20%\n- Normal: Standard values\n- Intense: Enemy spawn rate +20%, enemy damage +20%\n- Default: Normal\n\n**XP Gain Rate**\n- Range: 0.8x to 1.2x\n- Affects how quickly player levels up\n- Default: 1.0x\n\n### Edit Mode UI\n- Accessible from main menu before starting game\n- Simple tabbed interface: Monster, City, Gameplay\n- Each property has clear label and preview\n- \"Reset to Default\" button for each section\n- \"Apply Changes\" button to save and start game\n- Changes persist in window.gameConfig\n\n### Constraints\n- No editing of mutation effects or core mechanics\n- No editing of enemy types or behaviors\n- No editing of building health values\n- Color choices must maintain game readability\n- Size multipliers kept within narrow range to preserve game balance\n- All edits are cosmetic or minor difficulty adjustments\n\n## Control Design\n\n### Desktop Controls\n\n**Movement**\n- WASD or Arrow Keys: Move monster in 8 directions\n- Movement is relative to world space (not camera-relative)\n- W/Up: Move north\n- S/Down: Move south\n- A/Left: Move west\n- D/Right: Move east\n- Diagonal movement when two keys pressed\n- Base speed: 50 units per second\n- Smooth acceleration/deceleration (0.1 second ramp)\n\n**Attack**\n- Spacebar or Left Mouse Click: Primary attack (bite/stomp)\n- Attack has 0.5 second cooldown\n- Attack range: 15 units in front of monster\n- Attack hitbox: 120-degree cone in facing direction\n- Attack deals 25 base damage\n- Attack triggers animation and particle effects\n\n**Facing Direction**\n- Monster faces movement direction\n- When stationary, maintains last facing direction\n- Smooth rotation (0.2 second lerp to new direction)\n\n**Special Attack (Second Iteration)**\n- E Key or Right Mouse Click: Special attack when meter full\n- Meter fills from destruction and combat\n- Special attack has large area effect\n\n**Input Handling**\n- Prevent default browser behavior for used keys (WASD, arrows, spacebar)\n- Clear all input states on window blur\n- Store key states in lowercase for consistency\n- Support simultaneous key presses for diagonal movement\n\n### Mobile Controls\n\n**Virtual Joystick (Movement)**\n- Position: Left side of screen, bottom 40% of screen height\n- Appears where player first touches in left zone\n- Base position: Touch start location\n- Max radius: 60 pixels\n- Dead zone: 10 pixels (treated as zero input)\n- Knob visual: Semi-transparent circle (40px diameter)\n- Base visual: Semi-transparent circle (80px diameter)\n- **Dynamic repositioning**: If finger moves beyond max radius, slide base position so knob stays at max radius under finger\n- Output: Normalized vector (-1 to 1 on X and Y axes)\n- Visual feedback: Knob position shows current input direction\n- Joystick fades out 0.5 seconds after touch release\n\n**Attack Button**\n- Position: Bottom right corner\n- Size: 96x96 pixels (thumb-friendly)\n- Visual: Circular button with bite/claw icon\n- Feedback: Scale down slightly on press, particle effect on activation\n- Cooldown visual: Radial fill showing cooldown progress\n\n**Special Attack Button (Second Iteration)**\n- Position: Above attack button\n- Size: 96x96 pixels\n- Visual: Circular button with special attack icon\n- Only visible when meter is full\n- Glows or pulses when available\n\n**Touch Handling**\n- Track touch IDs to prevent interference between joystick and buttons\n- Left half of screen (excluding UI) = joystick zone\n- Right side buttons = attack zone\n- Prevent touches from affecting both zones simultaneously\n- Support multi-touch (move + attack simultaneously)\n\n**UI Considerations**\n- All interactive elements minimum 96x96 pixels\n- Buttons positioned in bottom corners (thumb-friendly)\n- Health/XP bars at top (out of thumb reach)\n- Mutation selection cards large and centered\n- Adequate spacing between interactive elements (16px minimum)\n\n### Input Responsiveness\n- Zero input lag for movement and attacks\n- Smooth interpolation for visual feedback\n- Clear visual/audio feedback for all actions\n- Attack cooldown clearly communicated\n- Movement feels tight and responsive\n- No accidental inputs from UI overlap\n\n## Art Style and Visual Theme\n\n### Overall Aesthetic\n**Colorful Low-Poly Toy City**\n\nThe game uses a vibrant, playful, toy-like aesthetic that makes destruction feel fun rather than disturbing. Think of a child's toy city being playfully demolished - colorful, chunky, and satisfying to break.\n\n**Key Visual Principles:**\n- Bright, saturated colors throughout\n- Simple geometric shapes (boxes, cylinders, cones, spheres)\n- No realistic textures - flat colors with simple shading\n- Chunky, readable silhouettes\n- High contrast between elements\n- Exaggerated proportions for clarity and charm\n\n### Color Palette\n\n**Primary Palette (Buildings)**\n- Bright Blue (#3498db)\n- Vibrant Red (#e74c3c)\n- Sunny Yellow (#f1c40f)\n- Fresh Green (#2ecc71)\n- Bold Orange (#e67e22)\n- Royal Purple (#9b59b6)\n\n**Secondary Palette (Accents)**\n- Teal (#1abc9c)\n- Hot Pink (#ff6b9d)\n- Lime (#a8e063)\n- Magenta (#c44569)\n\n**Neutral Palette**\n- Roads: Dark gray (#34495e)\n- Road lines: White (#ffffff)\n- Sidewalks: Light gray (#95a5a6)\n- Ground: Muted green (#7f8c8d)\n\n**Effect Palette**\n- Dust/smoke: Light gray with transparency\n- Debris: Matches source object color\n- XP orbs: Bright cyan (#00d4ff) with glow\n- Health: Bright red (#ff0000)\n- Damage flash: White (#ffffff)\n\n### Monster Design\n\n**Visual Characteristics:**\n- **Size**: Starts at ~12 units tall (roughly 1.5x car height, 0.5x small building height)\n- **Proportions**: Chunky body, large head, thick limbs, big mouth\n- **Shape Language**: Rounded rectangles and cylinders for friendly feel\n- **Color**: Bright green primary, yellow belly/accents, red eyes\n- **Style**: Low-poly geometric shapes, no smooth curves\n\n**Body Structure (Simple Geometry):**\n- Body: Rounded box (main mass)\n- Head: Larger rounded box with slight taper\n- Mouth: Dark cavity with simple teeth (white triangles)\n- Eyes: Two spheres (red) with black pupils\n- Arms: Thick cylinders with sphere hands\n- Legs: Thick cylinders with box feet\n- Tail: Tapered cylinder (optional)\n- Spikes/scales: Small triangular protrusions along back (optional)\n\n**Animation States:**\n- Idle: Subtle breathing motion (scale pulse)\n- Walking: Simple walk cycle (legs alternate, body bob)\n- Attacking: Lunge forward, mouth opens wide\n- Taking damage: Flash white, slight recoil\n- Eating: Mouth opens, object shrinks into mouth\n- Level up: Grow animation (scale increase), glow effect\n\n**Growth Progression:**\n- Each level up: +5% size increase\n- Max growth: 2.0x starting size (after ~15 levels)\n- Growth affects visual size and attack range proportionally\n- Maintains same proportions throughout growth\n\n### Building Design\n\n**Visual Style:**\n- Simple box geometry (no complex shapes in first iteration)\n- Flat colored walls\n- Windows: Darker rectangles in grid pattern\n- Roof: Slightly lighter shade of building color\n- No detailed textures - solid colors only\n\n**Size Variations:**\n- Small: 1-2 window rows, simple box\n- Medium: 3-5 window rows, possible slight taper\n- Tall: 6-8 window rows, antenna or spire on top\n\n**Damage States:**\n- Intact: Full brightness\n- Damaged: 20% darker, small cracks (simple line geometry)\n- Heavy damage: 40% darker, visible tilt (3-5 degrees)\n- Critical: 60% darker, pronounced tilt (8-10 degrees), smoke particles\n- Destroyed: Explodes into colored debris chunks\n\n### Props and Environment\n\n**Cars:**\n- Simple box body (4x2x2 units)\n- Cylinder wheels (4 wheels)\n- Bright random colors\n- Simple windshield (darker rectangle)\n- No detailed features\n\n**Trees:**\n- Cylinder trunk (brown #8b4513)\n- Sphere or cone canopy (green #27ae60)\n- Simple, iconic tree shape\n\n**Street Lamps:**\n- Thin cylinder pole (gray #7f8c8d)\n- Small sphere light (yellow #f1c40f with glow)\n\n**Roads:**\n- Flat plane (dark gray)\n- White dashed center line\n- Simple, clean appearance\n\n### Enemy Design\n\n**Police Cars:**\n- Similar to civilian cars but with distinct features\n- Color: Black and white pattern\n- Flashing light on top (red/blue sphere with blink animation)\n- Slightly larger than civilian cars\n\n**Tanks:**\n- Box body (green #2c3e50)\n- Cylinder turret\n- Cylinder barrel\n- Box treads\n- Chunky, toy-like military vehicle\n\n**Helicopters (Second Iteration):**\n- Box body\n- Cylinder tail boom\n- Thin cylinder rotor blades (spinning)\n- Simple, iconic helicopter shape\n\n### Particle Effects\n\n**Destruction Debris:**\n- Small boxes (2-4 units) in source object's color\n- Initial velocity: Outward and upward\n- Gravity: Falls to ground\n- Fade out: 2-3 seconds\n- Count: 8-15 per destroyed object\n\n**Dust Clouds:**\n- Billboard sprites or point sprites\n- Color: Light gray with transparency\n- Size: 4-8 units\n- Expand and fade over 1 second\n- Spawn on impacts and destruction\n\n**Impact Rings:**\n- Expanding circle on ground\n- Color: White with transparency\n- Expands from 0 to 20 units over 0.3 seconds\n- Fades out as it expands\n\n**XP Orbs:**\n- Small spheres (1 unit diameter)\n- Color: Bright cyan with glow effect\n- Float slightly above ground\n- Animate toward player when in range (30 units)\n- Smooth lerp movement\n\n**Damage Flash:**\n- Entire object flashes white for 0.1 seconds\n- Achieved by temporarily setting material to white emissive\n\n**Level Up Effect:**\n- Expanding ring around monster\n- Color: Gold/yellow with glow\n- Particles burst upward from monster\n- Camera pulse effect\n\n### Lighting\n\n**Scene Lighting:**\n- Ambient light: Soft white (#ffffff, intensity 0.6)\n- Directional light: Warm white (#fffacd, intensity 0.8)\n- Direction: From above and slightly to side (45-degree angle)\n- Shadows: Simple shadow map for monster and buildings (optional for performance)\n\n**Dynamic Lights (Optional):**\n- Explosions: Brief orange point light\n- XP orbs: Subtle cyan glow\n- Level up: Brief gold point light\n\n### UI Style\n\n**Visual Design:**\n- Bold, chunky fonts (sans-serif)\n- Bright colors matching game palette\n- Simple geometric shapes for UI elements\n- High contrast for readability\n- Slight drop shadows for depth\n\n**Health Bar:**\n- Position: Top left\n- Style: Thick red bar with dark border\n- Background: Dark gray\n- Size: 200x30 pixels\n\n**XP Bar:**\n- Position: Bottom center\n- Style: Thick cyan bar with dark border\n- Background: Dark gray\n- Size: 300x20 pixels\n- Shows level number on left side\n\n**Score Display:**\n- Position: Top right\n- Style: Large white numbers with dark outline\n- Font size: 32px\n\n**Mutation Cards:**\n- Size: 250x350 pixels each\n- Background: Bright color matching mutation theme\n- Border: Thick white border\n- Icon: Simple geometric icon at top\n- Title: Bold text\n- Description: Smaller text below\n\n### Asset Generation vs Code Rendering\n\n**Generate as Assets:**\n- Monster character (all animation states)\n- Enemy vehicles (police car, tank, helicopter)\n- UI icons (attack button, special attack button, mutation icons)\n- Particle textures (dust, debris sprites if using textured particles)\n\n**Render with Code (Three.js Geometry):**\n- Buildings (simple BoxGeometry with colored materials)\n- Roads (PlaneGeometry)\n- Trees (CylinderGeometry + SphereGeometry)\n- Street lamps (CylinderGeometry + SphereGeometry)\n- Civilian cars (BoxGeometry + CylinderGeometry)\n- Debris particles (BoxGeometry or PointsMaterial)\n- XP orbs (SphereGeometry with emissive material)\n- UI bars and backgrounds (HTML/CSS)\n\n### Consistency Requirements\n\nAll generated assets must:\n- Use the same low-poly, toy-like style\n- Match the bright, saturated color palette\n- Have consistent viewing angle (slight top-down angle for consistency)\n- Use flat colors without realistic textures\n- Have clear, readable silhouettes\n- Work on transparent backgrounds where appropriate\n- Be optimized for web (reasonable file sizes)", "persistent_storage_config": {"leaderboard": {"sort_ascending": false}}, "game_metadata": {"game_id": "01KSPH30DNZ4GPGAKCC1973BX1", "title": "Growzilla", "description": "Smash, eat, and evolve! Control a giant kaiju destroying a toy city, battling military forces, and choosing wild mutations in this chaotic 3D roguelite rampage.", "thumbnail_url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_100411990910946493899/1780234230805_01KSZ3N70N8X5JQKF6JECGF77S.Growzilla Thumbnail.png", "created_at": 1779948427, "updated_at": 1781994049435.4973, "art_style": null}, "task_plan": null}