{"asset_map": {"player_character_idle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/player_character_idle_abf1b138-e2c0-4824-aa0a-df0c1cbb3214.webp", "type": "image", "prompt": "Generate a full-body, front-view Minecraft-style humanoid character for a top-down game. Blocky cubic head, rectangular torso, thin rectangular limbs. Wearing a blue shirt and brown pants. Top-down view with a slight isometric tilt, standing idle facing forward. Clean pixel-perfect edges, flat shading, bright colors, transparent background.", "description": "Minecraft-style blocky humanoid character, blue shirt, top-down isometric tilt view", "subtype": "character", "aspect_ratio": [621, 1341], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "ai_enemy_red": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ai_enemy_red_d169c8b0-92b6-47ee-8a80-81c2fd498f70.webp", "type": "image", "prompt": "Generate a full-body Minecraft-style humanoid character identical in structure to image 1, but change the shirt color to crimson red (#DC143C) and pants to dark gray. Top-down view with slight isometric tilt, idle pose, transparent background. Clean blocky aesthetic.", "description": "Enemy character variant with a red shirt and aggressive color scheme", "subtype": "character", "aspect_ratio": [578, 1247], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "ai_enemy_purple": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ai_enemy_purple_1786a488-2f03-445f-8592-9e48da7402b5.webp", "type": "image", "prompt": "Generate a full-body Minecraft-style humanoid character identical in structure to image 1, but change the shirt color to purple (#9370DB) and pants to dark gray. Top-down view with slight isometric tilt, idle pose, transparent background.", "description": "Enemy character variant with a purple shirt", "subtype": "character", "aspect_ratio": [578, 1245], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "ai_enemy_orange": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ai_enemy_orange_e2a93188-e403-455d-be85-74c48b3ba4a0.webp", "type": "image", "prompt": "Generate a full-body Minecraft-style humanoid character identical in structure to image 1, but change the shirt color to orange (#FF8C00) and pants to dark gray. Top-down view with slight isometric tilt, idle pose, transparent background.", "description": "Enemy character variant with an orange shirt", "subtype": "character", "aspect_ratio": [578, 1247], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "ai_enemy_green": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ai_enemy_green_5b6f7ec3-b7ab-400e-a0c1-e2adf7a5bff6.webp", "type": "image", "prompt": "Generate a full-body Minecraft-style humanoid character identical in structure to image 1, but change the shirt color to dark green (#228B22) and pants to dark gray. Top-down view with slight isometric tilt, idle pose, transparent background.", "description": "Enemy character variant with a forest green shirt", "subtype": "character", "aspect_ratio": [579, 1243], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "weapon_wood": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/weapon_wood_bc63f641-921e-4830-8fdd-88bde91b0ba6.webp", "type": "image", "prompt": "Generate a simple blocky wooden sword from a top-down perspective. Rectangular blade, dark brown wood texture (#8B4513) with light brown highlights. Vertical orientation pointing up, simple cross-guard, Minecraft aesthetic, transparent background.", "description": "Starting wooden blade, rectangular blocky design", "subtype": "object", "aspect_ratio": [433, 1274], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "weapon_stone": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/weapon_stone_4be57b1f-62d6-421b-beda-f08bebb6bb22.webp", "type": "image", "prompt": "Generate a blocky stone sword identical in shape to image 1. Color is stone gray (#808080) with a subtle white glow outline. Minecraft aesthetic, vertical orientation, transparent background.", "description": "Mid-tier stone blade with a subtle white glow", "subtype": "object", "aspect_ratio": [336, 1160], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "weapon_gold": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/weapon_gold_8df63dd9-aa1d-4d8b-9fbd-bfa75ef509f2.webp", "type": "image", "prompt": "Generate a blocky golden sword identical in shape to image 1. Bright gold color (#FFD700) with a yellow aura and small sparkles. Polished appearance, vertical orientation, transparent background.", "description": "High-tier golden blade with yellow sparkles", "subtype": "object", "aspect_ratio": [628, 1248], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "weapon_diamond": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/weapon_diamond_dcad3462-d8d4-4c98-be72-f34405dcf213.webp", "type": "image", "prompt": "Generate a blocky diamond crystal sword identical in shape to image 1. Cyan-blue color (#00CED1) with intense cyan glow and white sparkles. Crystalline facets, vertical orientation, transparent background.", "description": "Ultimate diamond blade with cyan glow and sparkles", "subtype": "object", "aspect_ratio": [424, 1248], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "grass_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/grass_block_e9e78ae9-2522-4519-b6ca-b7a1b3f3eb36.webp", "type": "image", "prompt": "Generate a 3D grass block from a top-down perspective. Minecraft-style green cube (#7FD957) with darker green pixelated specks on the top face. Flat shading, clean edges, transparent background.", "description": "Simple destructible grass block object", "subtype": "ground", "aspect_ratio": [893, 966], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "wooden_crate": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wooden_crate_61614d89-cadf-4447-9dfc-37a39bf0ef62.webp", "type": "image", "prompt": "Generate a wooden crate block from a top-down perspective. Brown cube (#8B4513) with pixelated plank lines and wooden slats visible on the top face. Minecraft style, transparent background.", "description": "Destructible wooden crate with plank texture", "subtype": "object", "aspect_ratio": [879, 951], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tree_stump": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/tree_stump_cb21e63b-c5c1-4fc0-af64-d9944a74c854.webp", "type": "image", "prompt": "Generate a blocky tree stump from a top-down perspective. Cylindrical shape, dark brown bark edges, light brown concentric rings on the top face. Minecraft aesthetic, transparent background.", "description": "Durable tree stump with ring texture", "subtype": "object", "aspect_ratio": [717, 813], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "stone_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/stone_block_a3069933-f646-4e86-af3e-e7d234d63005.webp", "type": "image", "prompt": "Generate a stone cube block from a top-down perspective. Gray color (#808080) with cracks and rough pixelated texture. Blocky Minecraft aesthetic, transparent background.", "description": "Durable stone block with cracks", "subtype": "object", "aspect_ratio": [769, 786], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "gold_ore": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/gold_ore_471e7367-b0ff-44fd-a034-779ac1f5ea9d.webp", "type": "image", "prompt": "Generate a gold ore block from a top-down perspective. Dark gray stone base with bright golden (#FFD700) pixelated veins and sparkles embedded in it. Yellow glow around the gold parts, transparent background.", "description": "High-value gold ore stone block", "subtype": "object", "aspect_ratio": [793, 799], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "diamond_ore": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/diamond_ore_580d34cc-3c05-437e-9af3-06ac9cdc20c0.webp", "type": "image", "prompt": "Generate a diamond ore block from a top-down perspective. Dark gray stone base with bright cyan-blue (#00CED1) crystalline veins. Intense cyan glow and white sparkles, Minecraft aesthetic, transparent background.", "description": "Premium diamond ore stone block", "subtype": "object", "aspect_ratio": [600, 571], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "blade_soul_blue": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/blade_soul_blue_4f776673-bbe0-4a92-81df-97e26b814439.webp", "type": "image", "prompt": "Generate a glowing blue magical orb (#4169E1) with a white core and soft blue aura. Smooth, clean circular shape, 2D game asset, transparent background.", "description": "Common blue glowing collectible orb", "subtype": "effect", "aspect_ratio": [591, 581], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "blade_soul_green": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/blade_soul_green_dc0fdd1e-f437-406b-858c-9ecb97abbad0.webp", "type": "image", "prompt": "Generate a glowing green magical orb (#32CD32) with a white core and soft green aura. Match the style and shape of image 1, transparent background.", "description": "Medium value green glowing orb", "subtype": "effect", "aspect_ratio": [1007, 1001], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "blade_soul_silver": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/blade_soul_silver_74976c42-676a-45ea-9d73-4839780ee833.webp", "type": "image", "prompt": "Generate a glowing silver/white magical orb (#C0C0C0) with a white core and soft silver aura. Match the style of image 1, slightly larger, transparent background.", "description": "Silver glowing orb for stone drops", "subtype": "effect", "aspect_ratio": [906, 901], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "blade_soul_gold": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/blade_soul_gold_17e97cfa-2ae9-4bda-b5cb-4ecec3da7b0f.webp", "type": "image", "prompt": "Generate a glowing golden magical orb (#FFD700) with a yellow core and intense golden aura. Match the style of image 1, premium appearance with extra sparkles, transparent background.", "description": "High value golden glowing orb", "subtype": "effect", "aspect_ratio": [879, 881], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "blade_soul_diamond": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/blade_soul_diamond_6f3c9095-8e7a-4bb1-a1bb-b4e25822336f.webp", "type": "image", "prompt": "Generate a glowing cyan-blue magical orb (#00CED1) with a white core and intense crystalline aura with sparkles. Match the style of image 1, largest size, transparent background.", "description": "Premium cyan glowing orb with sparkles", "subtype": "effect", "aspect_ratio": [906, 911], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "blade_soul_enemy": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/blade_soul_enemy_03db5f70-2ed3-4774-a120-57fbb041cea0.webp", "type": "image", "prompt": "Generate a glowing red-orange magical orb (#FF4500) with a fiery orange core and aura. Match the style of image 1, transparent background.", "description": "Red-orange orb dropped by enemies", "subtype": "effect", "aspect_ratio": [1018, 1002], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "particle_wood": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/particle_wood_2a361c4f-2810-41ee-919f-392bfa9bf723.webp", "type": "image", "prompt": "Generate a small, square-shaped brown wood chip particle (#8B4513). Flat shading, tiny pixel art style, transparent background.", "description": "Small wood chip particle", "subtype": "effect", "aspect_ratio": [556, 556], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "particle_stone": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/particle_stone_09bb8a71-219b-4c97-a07a-cd180e911e33.webp", "type": "image", "prompt": "Generate a small, square-shaped gray stone chip particle (#808080). Flat shading, tiny pixel art style, transparent background.", "description": "Small stone chip particle", "subtype": "effect", "aspect_ratio": [466, 423], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "particle_gold": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/particle_gold_532b7380-7355-4ef8-ab8e-f3e3720cc112.webp", "type": "image", "prompt": "Generate a small, square-shaped golden sparkle particle (#FFD700) with a slight glow. Flat shading, transparent background.", "description": "Small gold sparkle particle", "subtype": "effect", "aspect_ratio": [666, 681], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "particle_diamond": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/particle_diamond_530dc4f7-d6c9-4b30-bc9a-1b90348c344c.webp", "type": "image", "prompt": "Generate a small, square-shaped cyan crystal sparkle particle (#00CED1) with a bright glow. Flat shading, transparent background.", "description": "Small cyan crystal sparkle particle", "subtype": "effect", "aspect_ratio": [489, 536], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "shield_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/shield_icon_fd220dc4-b748-4f30-82a6-8983d3a818f4.webp", "type": "image", "prompt": "Generate a blocky silver shield icon (#C0C0C0) with a blue emblem in the center. Minecraft-style, thick dark outline, flat shading, transparent background.", "description": "Silver shield icon for mobile UI", "subtype": "ui", "aspect_ratio": [825, 934], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "background_ground": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/background_ground_bb84216a-f481-4fc1-8f3f-b89b8a0250ac.webp", "type": "image", "prompt": "Generate a seamless horizontally and vertically tiling ground texture. Minecraft style grass and dirt pattern, bright green (#7FD957) with brown patches. Top-down view, flat shading, pixelated texture, no visible seams.", "description": "Seamlessly tiling grass and dirt floor texture", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "tree_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/tree_block_ffb7330c-8523-4301-be1e-892bd63246d8.webp", "type": "image", "prompt": "Generate a Minecraft-style blocky tree, top-down isometric view. The tree should consist of simple cubic brown trunk segments with a cluster of bright green leaf blocks on top. Bright, colorful pixel art style, clean edges, game-ready asset. Match the blocky aesthetic of 'player_character_idle'. Full body illustration with a transparent background.", "description": "A Minecraft-style blocky tree object for a voxel-based sandbox game environment.", "subtype": "object", "aspect_ratio": [662, 908], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "mushroom_red": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mushroom_red_756f9181-a166-41a7-a6c9-83cdd6118236.webp", "type": "image", "prompt": "Generate a Minecraft-style blocky red mushroom with white spots. The design should be a cute, low-poly cubic representation with a thick red cap and a small white stem. Top-down isometric view, clean pixel art texture, bright and colorful. Game-ready asset with a transparent background, matching the style of tree_block.", "description": "A blocky, Minecraft-style red mushroom with white spots for the sandbox environment.", "subtype": "object", "aspect_ratio": [571, 703], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "flower_yellow": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/flower_yellow_d251e0c9-7edd-4025-92eb-973561cf4f10.webp", "type": "image", "prompt": "Generate a Minecraft-style blocky yellow flower, top-down isometric view. Simple bright yellow petals with a green stem, cute pixel art style suitable for a blocky sandbox game. Small decorative plant, clean edges, transparent background, high resolution, game-ready asset.", "description": "A decorative Minecraft-style yellow flower for the game environment.", "subtype": "object", "aspect_ratio": [478, 605], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "flower_red": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/flower_red_dec7b310-a2ee-40b6-bb62-b3c69cbeb595.webp", "type": "image", "prompt": "Generate a Minecraft-style blocky red flower poppy, top-down isometric view. Simple bright red petals with a green stem. Cute pixel art style for a block sandbox game, high-quality clean edges, professional game asset. Transparent background.", "description": "A decorative Minecraft-style red poppy flower for the game world environment.", "subtype": "object", "aspect_ratio": [830, 1054], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "background_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/background_music_ef6dd907-6106-4f49-bce4-01b52e081043.mp3", "type": "audio", "prompt": "A cheerful and bouncy sandbox game background track, medium tempo (100-110 BPM), joyful and lighthearted vibe. Incorporate playful marimba melodies, warm pluck synths, a gentle walking bassline, and soft acoustic percussion. Keep the arrangement airy and fun without becoming frantic or overwhelming. Positive, major-key atmosphere suitable for creative exploration and casual play. High-quality production, melodic and loopable.", "description": "Upbeat, cheerful, and bouncy medium-tempo background music for a joyful sandbox experience.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 60.02938775510204, "loop": true, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>My 99-Meter Blade</title>\n    <link href=\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;800&family=Nunito:wght@400;600;700&display=swap\" rel=\"stylesheet\">\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n    <style>\n        body, html {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            background: #1a1a2e;\n        }\n\n        #game-world {\n            position: relative;\n            width: 720px;\n            height: 1280px;\n            overflow: hidden;\n        }\n\n        #game-canvas {\n            display: block;\n            width: 100%;\n            height: 100%;\n        }\n\n        .ui-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: 10;\n        }\n\n        .ui-overlay > * {\n            pointer-events: auto;\n        }\n\n        .hud-top {\n            position: absolute;\n            top: max(16px, env(safe-area-inset-top));\n            left: 16px;\n            right: 16px;\n            display: flex;\n            justify-content: space-between;\n            align-items: flex-start;\n            gap: 10px;\n            pointer-events: none;\n        }\n\n        .hud-item {\n            font-family: 'Poppins', sans-serif;\n            font-size: 21px;\n            font-weight: 700;\n            color: #fff;\n            text-shadow: 0 2px 8px rgba(0,0,0,0.55);\n            background: linear-gradient(135deg, rgba(26,34,58,0.92) 0%, rgba(18,24,44,0.96) 100%);\n            padding: 12px 18px;\n            min-width: 140px;\n            text-align: center;\n            border-radius: 14px;\n            border: 2px solid rgba(255,255,255,0.18);\n            backdrop-filter: blur(10px);\n            box-shadow: 0 6px 18px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.14);\n        }\n\n        .hud-timer {\n            position: absolute;\n            top: max(14px, env(safe-area-inset-top));\n            left: 50%;\n            transform: translateX(-50%);\n            font-family: 'Poppins', sans-serif;\n            font-size: 32px;\n            font-weight: 800;\n            color: #fff;\n            text-shadow: 0 2px 10px rgba(0,0,0,0.5);\n            background: linear-gradient(135deg, rgba(46,46,78,0.95) 0%, rgba(24,24,48,0.98) 100%);\n            padding: 14px 28px;\n            border-radius: 18px;\n            border: 2px solid rgba(255,215,0,0.35);\n            backdrop-filter: blur(12px);\n            box-shadow: 0 8px 24px rgba(0,0,0,0.42), inset 0 1px 0 rgba(255,255,255,0.1);\n            min-width: 156px;\n            text-align: center;\n        }\n\n        .hud-timer.urgent {\n            color: #FF4444;\n            border-color: #FF4444;\n            animation: pulse-urgent 0.5s infinite alternate;\n        }\n\n        @keyframes pulse-urgent {\n            from { transform: translateX(-50%) scale(1); }\n            to { transform: translateX(-50%) scale(1.05); }\n        }\n\n        .btn-action {\n            position: absolute;\n            width: 96px;\n            height: 96px;\n            border-radius: 12px;\n            border: 4px solid rgba(255,255,255,0.5);\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-size: 32px;\n            color: #fff;\n            cursor: pointer;\n            user-select: none;\n        }\n\n        .hud-status {\n            position: absolute;\n            bottom: calc(28px + env(safe-area-inset-bottom));\n            left: 50%;\n            transform: translateX(-50%);\n            font-family: 'Poppins', sans-serif;\n            font-size: 18px;\n            font-weight: 700;\n            color: #fff;\n            text-shadow: 0 2px 8px rgba(0,0,0,0.45);\n            background: linear-gradient(135deg, rgba(64,138,210,0.92) 0%, rgba(38,84,156,0.96) 100%);\n            padding: 14px 24px;\n            border-radius: 22px;\n            border: 2px solid rgba(255,255,255,0.25);\n            backdrop-filter: blur(10px);\n            box-shadow: 0 8px 24px rgba(41,107,184,0.45), inset 0 1px 0 rgba(255,255,255,0.2);\n            transition: all 0.25s ease;\n            min-width: 230px;\n            text-align: center;\n        }\n\n        .hud-status.attacking {\n            background: linear-gradient(135deg, rgba(220,80,80,0.9) 0%, rgba(180,50,50,0.95) 100%);\n            border-color: rgba(255,150,150,0.3);\n            box-shadow: 0 4px 20px rgba(220,80,80,0.5), inset 0 1px 0 rgba(255,255,255,0.15);\n        }\n\n        .start-screen, .game-over-screen {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: radial-gradient(circle at 50% 15%, rgba(70,95,150,0.35) 0%, rgba(0,0,0,0.86) 55%, rgba(0,0,0,0.92) 100%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 100;\n            padding: 14px 0 calc(14px + env(safe-area-inset-bottom));\n            box-sizing: border-box;\n        }\n\n        .start-title, .game-over-title {\n            font-family: 'Poppins', sans-serif;\n            font-size: 42px;\n            font-weight: 800;\n            color: #FFD700;\n            text-shadow: 0 5px 25px rgba(255,215,0,0.6), 0 2px 4px rgba(0,0,0,0.5);\n            text-align: center;\n            line-height: 1.3;\n            margin-bottom: 20px;\n            letter-spacing: 1.6px;\n        }\n\n        .start-subtitle {\n            font-family: 'Nunito', sans-serif;\n            font-size: 22px;\n            font-weight: 600;\n            color: #aab;\n            margin-bottom: 40px;\n            letter-spacing: 1px;\n        }\n\n        .btn-start, .btn-restart {\n            padding: 20px 60px;\n            font-family: 'Poppins', sans-serif;\n            font-size: 22px;\n            font-weight: 700;\n            color: #fff;\n            background: linear-gradient(135deg, #5c5 0%, #3a3 50%, #282 100%);\n            border: none;\n            border-radius: 30px;\n            cursor: pointer;\n            box-shadow: 0 6px 25px rgba(50,200,50,0.4), inset 0 2px 0 rgba(255,255,255,0.2);\n            text-shadow: 0 2px 4px rgba(0,0,0,0.3);\n            letter-spacing: 1px;\n            animation: btn-bounce 1.5s infinite;\n            transition: transform 0.2s, box-shadow 0.2s;\n        }\n        \n        .btn-start:hover, .btn-restart:hover {\n            transform: scale(1.05);\n            box-shadow: 0 8px 30px rgba(50,200,50,0.5), inset 0 2px 0 rgba(255,255,255,0.2);\n        }\n\n        .btn-restart {\n            background: linear-gradient(135deg, #f75 0%, #e53 50%, #c42 100%);\n            margin-top: 18px;\n            animation: none;\n            box-shadow: 0 10px 32px rgba(230,80,50,0.5), inset 0 2px 0 rgba(255,255,255,0.2);\n            min-width: 280px;\n            min-height: 72px;\n            font-size: 24px;\n        }\n        \n        .btn-restart:hover {\n            box-shadow: 0 8px 30px rgba(230,80,50,0.5), inset 0 2px 0 rgba(255,255,255,0.2);\n        }\n\n        @keyframes btn-bounce {\n            0%, 100% { transform: scale(1); }\n            50% { transform: scale(1.05); }\n        }\n\n        .game-over-screen {\n            animation: fadeInUp 0.6s ease-out;\n        }\n\n        @keyframes fadeInUp {\n            from {\n                opacity: 0;\n                transform: translateY(30px);\n            }\n            to {\n                opacity: 1;\n                transform: translateY(0);\n            }\n        }\n\n        .game-over-title {\n            animation: titlePop 0.5s ease-out 0.2s both;\n        }\n\n        @keyframes titlePop {\n            from {\n                opacity: 0;\n                transform: scale(0.8);\n            }\n            to {\n                opacity: 1;\n                transform: scale(1);\n            }\n        }\n\n        .game-over-stats {\n            display: flex;\n            flex-direction: column;\n            gap: 12px;\n            margin-bottom: 16px;\n            width: 92%;\n            max-width: 580px;\n        }\n\n        .game-over-stats .stat-item {\n            animation: statSlideIn 0.4s ease-out both;\n        }\n\n        .game-over-stats .stat-item:nth-child(1) { animation-delay: 0.3s; }\n        .game-over-stats .stat-item:nth-child(2) { animation-delay: 0.4s; }\n        .game-over-stats .stat-item:nth-child(3) { animation-delay: 0.5s; }\n        .game-over-stats .stat-item:nth-child(4) { animation-delay: 0.6s; }\n\n        @keyframes statSlideIn {\n            from {\n                opacity: 0;\n                transform: translateX(-20px);\n            }\n            to {\n                opacity: 1;\n                transform: translateX(0);\n            }\n        }\n\n        .coins-earned {\n            animation: coinsPopIn 0.5s ease-out 0.7s both;\n        }\n\n        @keyframes coinsPopIn {\n            from {\n                opacity: 0;\n                transform: scale(0.5);\n            }\n            50% {\n                transform: scale(1.1);\n            }\n            to {\n                opacity: 1;\n                transform: scale(1);\n            }\n        }\n\n        .leaderboard-container {\n            animation: fadeInUp 0.5s ease-out 0.9s both;\n        }\n\n        .cumulative-stats {\n            animation: fadeInUp 0.5s ease-out 1.0s both;\n        }\n\n        .btn-restart {\n            animation: buttonSlideUp 0.5s ease-out 1.1s both;\n        }\n\n        @keyframes buttonSlideUp {\n            from {\n                opacity: 0;\n                transform: translateY(20px);\n            }\n            to {\n                opacity: 1;\n                transform: translateY(0);\n            }\n        }\n\n        .stat-item {\n            font-family: 'Poppins', sans-serif;\n            font-size: 22px;\n            font-weight: 600;\n            color: #ccd;\n            text-align: center;\n            background: linear-gradient(135deg, rgba(36,38,65,0.96) 0%, rgba(22,26,48,0.98) 100%);\n            padding: 16px 24px;\n            border-radius: 18px;\n            border: 2px solid rgba(255,255,255,0.18);\n            backdrop-filter: blur(12px);\n            box-shadow: 0 8px 30px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.12);\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n        }\n\n        .stat-item .stat-label {\n            font-size: 22px;\n            font-weight: 600;\n            color: #aab;\n        }\n\n        .stat-item .stat-value {\n            font-family: 'Poppins', sans-serif;\n            font-size: 34px;\n            font-weight: 800;\n            color: #FFD700;\n            text-shadow: 0 4px 18px rgba(255,215,0,0.7), 0 0 35px rgba(255,215,0,0.5);\n            animation: stat-pulse 1.5s ease-in-out infinite;\n        }\n\n        .stat-item.rank-1 .stat-value {\n            color: #FFD700;\n            text-shadow: 0 3px 20px rgba(255,215,0,0.8), 0 0 40px rgba(255,215,0,0.6);\n            animation: rank1-glow 1s ease-in-out infinite;\n        }\n\n        .stat-item.rank-2 .stat-value {\n            color: #C0C0C0;\n            text-shadow: 0 3px 15px rgba(192,192,192,0.6), 0 0 30px rgba(192,192,192,0.4);\n        }\n\n        .stat-item.rank-3 .stat-value {\n            color: #CD7F32;\n            text-shadow: 0 3px 15px rgba(205,127,50,0.6), 0 0 30px rgba(205,127,50,0.4);\n        }\n\n        @keyframes stat-pulse {\n            0%, 100% { transform: scale(1); }\n            50% { transform: scale(1.05); }\n        }\n\n        @keyframes rank1-glow {\n            0%, 100% { \n                text-shadow: 0 3px 20px rgba(255,215,0,0.8), 0 0 40px rgba(255,215,0,0.6);\n                transform: scale(1);\n            }\n            50% { \n                text-shadow: 0 4px 30px rgba(255,215,0,1), 0 0 60px rgba(255,215,0,0.8);\n                transform: scale(1.08);\n            }\n        }\n\n        .floating-text {\n            position: absolute;\n            font-family: 'Poppins', sans-serif;\n            font-size: 20px;\n            font-weight: 700;\n            color: #FFD700;\n            text-shadow: 0 2px 8px rgba(0,0,0,0.8), 0 0 18px rgba(255,215,0,0.5);\n            pointer-events: none;\n            z-index: 20;\n            animation: floatUp 1s ease-out forwards;\n            white-space: nowrap;\n        }\n\n        .floating-text.large {\n            font-size: 28px;\n            color: #FF4500;\n            text-shadow: 0 2px 8px rgba(0,0,0,0.75), 0 0 20px rgba(255,69,0,0.5);\n        }\n\n        .floating-text.diamond {\n            font-size: 26px;\n            color: #00FFFF;\n            text-shadow: 0 2px 8px rgba(0,0,0,0.75), 0 0 20px rgba(0,255,255,0.5);\n        }\n\n        .floating-text.gold {\n            font-size: 24px;\n            color: #FFD700;\n            text-shadow: 0 2px 8px rgba(0,0,0,0.75), 0 0 20px rgba(255,215,0,0.5);\n        }\n\n        .floating-text.small {\n            font-size: 16px;\n            font-weight: 600;\n            color: #CCCC88;\n            opacity: 0.75;\n            text-shadow: 0 1px 3px rgba(0,0,0,0.5);\n        }\n\n        @keyframes floatUp {\n            0% {\n                opacity: 1;\n                transform: translateY(0) scale(1);\n            }\n            20% {\n                transform: translateY(-10px) scale(1.2);\n            }\n            100% {\n                opacity: 0;\n                transform: translateY(-60px) scale(0.8);\n            }\n        }\n\n        .edit-panel {\n            position: absolute;\n            top: 80px;\n            left: 16px;\n            background: rgba(0,0,0,0.85);\n            border: 2px solid #555;\n            border-radius: 8px;\n            padding: 12px;\n            z-index: 50;\n            max-height: 400px;\n            overflow-y: auto;\n        }\n\n        .edit-panel-title {\n            font-family: 'Poppins', sans-serif;\n            font-size: 16px;\n            font-weight: 700;\n            color: #FFD700;\n            margin-bottom: 12px;\n            letter-spacing: 1px;\n        }\n\n        .edit-obj-btn {\n            width: 50px;\n            height: 50px;\n            margin: 4px;\n            border: 2px solid #666;\n            border-radius: 4px;\n            cursor: pointer;\n            display: inline-block;\n        }\n\n        .edit-obj-btn.selected {\n            border-color: #FFD700;\n            background: rgba(255,215,0,0.3);\n        }\n\n        /* Character Selection UI */\n        .character-select-screen {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: linear-gradient(135deg, rgba(20,20,40,0.78) 0%, rgba(10,10,30,0.84) 100%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            z-index: 100;\n            overflow-y: auto;\n            padding: max(24px, env(safe-area-inset-top)) 0 calc(28px + env(safe-area-inset-bottom));\n            box-sizing: border-box;\n        }\n\n        .char-topbar {\n            width: 100%;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n            padding: 0 16px;\n            margin-bottom: 16px;\n            box-sizing: border-box;\n            position: relative;\n        }\n\n        .char-select-title {\n            font-family: 'Poppins', sans-serif;\n            font-size: 36px;\n            font-weight: 800;\n            color: #FFD700;\n            text-shadow: 0 4px 20px rgba(255,215,0,0.5);\n            margin: 0;\n        }\n\n        .char-coins-topbar {\n            font-family: 'Poppins', sans-serif;\n            font-size: 22px;\n            font-weight: 700;\n            color: #FFD700;\n            background: linear-gradient(135deg, rgba(0,0,0,0.6) 0%, rgba(30,30,50,0.7) 100%);\n            padding: 10px 18px;\n            border-radius: 12px;\n            border: 2px solid rgba(255,215,0,0.35);\n            box-shadow: 0 4px 12px rgba(0,0,0,0.3);\n            position: absolute;\n            right: 16px;\n            top: 50%;\n            transform: translateY(-50%);\n        }\n\n        .char-coins-display {\n            font-family: 'Nunito', sans-serif;\n            font-size: 28px;\n            font-weight: 700;\n            color: #FFD700;\n            background: linear-gradient(135deg, rgba(0,0,0,0.48) 0%, rgba(20,20,40,0.55) 100%);\n            padding: 12px 26px;\n            border-radius: 16px;\n            margin-bottom: 24px;\n            border: 1px solid rgba(255,215,0,0.25);\n        }\n\n        .char-3d-container {\n            width: calc(100% - 36px);\n            max-width: 480px;\n            height: 320px;\n            position: relative;\n            margin: 0 auto 18px;\n            border-radius: 20px;\n            overflow: hidden;\n            background: linear-gradient(135deg, rgba(34,34,56,0.85) 0%, rgba(20,20,40,0.92) 100%);\n            border: 2px solid rgba(255,255,255,0.14);\n            box-shadow: 0 10px 30px rgba(0,0,0,0.4);\n        }\n\n        .char-3d-canvas {\n            width: 100%;\n            height: 100%;\n        }\n\n        .char-nav-btn {\n            position: absolute;\n            top: 50%;\n            transform: translateY(-50%);\n            width: 50px;\n            height: 72px;\n            background: rgba(0,0,0,0.45);\n            border: 1.5px solid rgba(255,255,255,0.18);\n            border-radius: 10px;\n            color: #fff;\n            font-size: 30px;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: all 0.2s ease;\n            z-index: 10;\n        }\n\n        .char-nav-btn:hover {\n            background: rgba(255,215,0,0.25);\n            border-color: #FFD700;\n        }\n\n        .char-nav-btn.left { left: 8px; }\n        .char-nav-btn.right { right: 8px; }\n\n        .char-indicator {\n            display: flex;\n            justify-content: center;\n            gap: 12px;\n            margin: 16px 0 20px;\n        }\n\n        .char-dot {\n            width: 10px;\n            height: 10px;\n            border-radius: 50%;\n            background: rgba(255,255,255,0.25);\n            transition: all 0.2s ease;\n        }\n\n        .char-dot.active {\n            background: #FFD700;\n            transform: scale(1.2);\n        }\n\n        .char-dot.owned {\n            border: 1.5px solid #5c5;\n        }\n\n        .char-info-panel {\n            background: linear-gradient(135deg, rgba(42,44,66,0.9) 0%, rgba(30,32,54,0.94) 100%);\n            border: 1.5px solid rgba(255,255,255,0.12);\n            border-radius: 16px;\n            padding: 20px 24px;\n            margin: 0 16px 20px;\n            text-align: center;\n            max-width: 420px;\n            width: calc(100% - 32px);\n        }\n\n        .char-name {\n            font-family: 'Poppins', sans-serif;\n            font-size: 22px;\n            font-weight: 700;\n            color: #fff;\n            margin-bottom: 8px;\n        }\n\n        .char-price {\n            font-family: 'Nunito', sans-serif;\n            font-size: 28px;\n            font-weight: 600;\n            color: #FFD700;\n            margin-bottom: 14px;\n        }\n\n        .char-price.owned {\n            color: #5c5;\n        }\n\n        .char-passive {\n            font-family: 'Nunito', sans-serif;\n            font-size: 24px;\n            color: #bbc;\n            line-height: 1.45;\n        }\n\n        .btn-select-char {\n            min-width: 280px;\n            min-height: 64px;\n            padding: 16px 32px;\n            font-family: 'Poppins', sans-serif;\n            font-size: 26px;\n            font-weight: 700;\n            color: #fff;\n            background: linear-gradient(135deg, #5c5 0%, #3a3 50%, #282 100%);\n            border: none;\n            border-radius: 18px;\n            cursor: pointer;\n            margin-top: 12px;\n            box-shadow: 0 6px 18px rgba(50,200,50,0.38);\n            position: relative;\n            overflow: hidden;\n            animation: btn-pulse-glow 1.5s ease-in-out infinite;\n            transition: transform 0.2s, box-shadow 0.3s;\n        }\n        \n        .btn-select-char::before {\n            content: '';\n            position: absolute;\n            top: -50%;\n            left: -50%;\n            width: 200%;\n            height: 200%;\n            background: linear-gradient(\n                45deg,\n                transparent 30%,\n                rgba(255, 255, 255, 0.3) 50%,\n                transparent 70%\n            );\n            transform: rotate(45deg);\n            animation: btn-shine 2s linear infinite;\n        }\n        \n        .btn-select-char::after {\n            content: '';\n            position: absolute;\n            inset: 0;\n            border-radius: 18px;\n            padding: 3px;\n            background: linear-gradient(45deg, #5c5, #FFD700, #5c5, #FFD700);\n            -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);\n            -webkit-mask-composite: xor;\n            mask-composite: exclude;\n            opacity: 0;\n            animation: btn-border-glow 1.5s ease-in-out infinite;\n        }\n        \n        @keyframes btn-pulse-glow {\n            0%, 100% {\n                transform: scale(1);\n                box-shadow: 0 6px 18px rgba(50,200,50,0.38), \n                           0 0 0 0 rgba(85,204,85,0.7);\n            }\n            50% {\n                transform: scale(1.08);\n                box-shadow: 0 8px 30px rgba(50,200,50,0.6), \n                           0 0 30px 10px rgba(85,204,85,0.4);\n            }\n        }\n        \n        @keyframes btn-shine {\n            0% {\n                transform: translateX(-100%) translateY(-100%) rotate(45deg);\n            }\n            100% {\n                transform: translateX(100%) translateY(100%) rotate(45deg);\n            }\n        }\n        \n        @keyframes btn-border-glow {\n            0%, 100% {\n                opacity: 0.3;\n            }\n            50% {\n                opacity: 0.8;\n            }\n        }\n        \n        .btn-select-char:hover {\n            transform: scale(1.1);\n            box-shadow: 0 10px 40px rgba(50,200,50,0.7), \n                       0 0 40px 15px rgba(85,204,85,0.5);\n        }\n        \n        .btn-select-char:active {\n            transform: scale(1.05);\n        }\n\n        .btn-select-char:disabled {\n            background: linear-gradient(135deg, #666 0%, #444 100%);\n            cursor: not-allowed;\n            box-shadow: none;\n            animation: none;\n        }\n        \n        .btn-select-char:disabled::before {\n            animation: none;\n            opacity: 0;\n        }\n        \n        .btn-select-char:disabled::after {\n            animation: none;\n            opacity: 0;\n        }\n\n        .btn-select-char.buy {\n            background: linear-gradient(135deg, #f90 0%, #d70 50%, #b50 100%);\n            box-shadow: 0 5px 16px rgba(255,150,0,0.35);\n        }\n\n        .btn-back {\n            padding: 10px 24px;\n            font-family: 'Poppins', sans-serif;\n            font-size: 16px;\n            font-weight: 600;\n            color: #fff;\n            background: rgba(255,255,255,0.08);\n            border: 1.5px solid rgba(255,255,255,0.18);\n            border-radius: 16px;\n            cursor: pointer;\n            margin-top: 8px;\n        }\n\n        .btn-leaderboard {\n            min-width: 200px;\n            padding: 14px 28px;\n            font-family: 'Poppins', sans-serif;\n            font-size: 20px;\n            font-weight: 700;\n            color: #FFD700;\n            background: linear-gradient(135deg, rgba(255,215,0,0.15) 0%, rgba(255,165,0,0.15) 100%);\n            border: 2px solid rgba(255,215,0,0.4);\n            border-radius: 16px;\n            cursor: pointer;\n            margin-top: 40px;\n            transition: all 0.2s ease;\n        }\n\n        .btn-leaderboard:hover {\n            background: linear-gradient(135deg, rgba(255,215,0,0.25) 0%, rgba(255,165,0,0.25) 100%);\n            border-color: rgba(255,215,0,0.6);\n            transform: scale(1.03);\n        }\n\n        .leaderboard-screen {\n            position: absolute;\n            top: 0; left: 0;\n            width: 100%; height: 100%;\n            background: linear-gradient(135deg, rgba(20,20,40,0.96) 0%, rgba(10,10,30,0.98) 100%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            z-index: 100;\n            padding: max(20px, env(safe-area-inset-top)) 16px calc(20px + env(safe-area-inset-bottom));\n            box-sizing: border-box;\n        }\n\n        .leaderboard-header {\n            width: 100%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            position: relative;\n            margin-bottom: 20px;\n        }\n\n        .btn-back-leaderboard {\n            position: absolute;\n            left: 0;\n            padding: 10px 18px;\n            font-family: 'Poppins', sans-serif;\n            font-size: 16px;\n            font-weight: 600;\n            color: #fff;\n            background: rgba(255,255,255,0.1);\n            border: 1.5px solid rgba(255,255,255,0.2);\n            border-radius: 12px;\n            cursor: pointer;\n            transition: all 0.2s ease;\n        }\n\n        .btn-back-leaderboard:hover {\n            background: rgba(255,255,255,0.2);\n        }\n\n        .leaderboard-screen-title {\n            font-family: 'Poppins', sans-serif;\n            font-size: 32px;\n            font-weight: 800;\n            color: #FFD700;\n            text-shadow: 0 3px 15px rgba(255,215,0,0.5);\n        }\n\n        .leaderboard-loading {\n            font-family: 'Nunito', sans-serif;\n            font-size: 22px;\n            color: #aab;\n            margin-top: 100px;\n        }\n\n        .leaderboard-list {\n            width: 100%;\n            max-width: 500px;\n            max-height: calc(100% - 180px);\n            overflow-y: auto;\n            display: flex;\n            flex-direction: column;\n            gap: 10px;\n            padding: 10px;\n        }\n\n        .leaderboard-empty {\n            font-family: 'Nunito', sans-serif;\n            font-size: 20px;\n            color: #888;\n            text-align: center;\n            margin-top: 60px;\n        }\n\n        .leaderboard-user-rank {\n            font-family: 'Poppins', sans-serif;\n            font-size: 20px;\n            color: #fff;\n            background: linear-gradient(135deg, rgba(255,215,0,0.2) 0%, rgba(255,165,0,0.2) 100%);\n            padding: 12px 24px;\n            border-radius: 14px;\n            border: 2px solid rgba(255,215,0,0.35);\n            margin-bottom: 16px;\n        }\n\n        .leaderboard-user-rank .user-rank-value {\n            font-weight: 700;\n            color: #FFD700;\n            text-shadow: 0 2px 10px rgba(255,215,0,0.5);\n        }\n\n        /* Mode Selection Styles */\n        .mode-select-container {\n            width: calc(100% - 32px);\n            max-width: 480px;\n            margin: 12px auto 20px;\n            text-align: center;\n        }\n\n        .mode-select-title {\n            font-family: 'Poppins', sans-serif;\n            font-size: 24px;\n            font-weight: 700;\n            color: #fff;\n            margin-bottom: 10px;\n            text-shadow: 0 2px 8px rgba(0,0,0,0.4);\n        }\n\n        .mode-buttons {\n            display: flex;\n            gap: 12px;\n            justify-content: center;\n        }\n\n        .mode-btn {\n            flex: 1;\n            max-width: 240px;\n            padding: 16px 12px;\n            background: linear-gradient(135deg, rgba(50,50,70,0.9) 0%, rgba(30,30,50,0.95) 100%);\n            border: 2px solid rgba(255,255,255,0.15);\n            border-radius: 14px;\n            cursor: pointer;\n            transition: all 0.25s ease;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 4px;\n        }\n\n        .mode-btn:hover {\n            background: linear-gradient(135deg, rgba(60,60,85,0.95) 0%, rgba(40,40,60,0.98) 100%);\n            border-color: rgba(255,255,255,0.25);\n            transform: translateY(-2px);\n        }\n\n        .mode-btn.active {\n            background: linear-gradient(135deg, rgba(255,215,0,0.25) 0%, rgba(255,165,0,0.2) 100%);\n            border-color: rgba(255,215,0,0.6);\n            box-shadow: 0 4px 20px rgba(255,215,0,0.3);\n        }\n\n        .mode-btn .mode-icon {\n            font-size: 36px;\n        }\n\n        .mode-btn .mode-name {\n            font-family: 'Poppins', sans-serif;\n            font-size: 26px;\n            font-weight: 700;\n            color: #fff;\n        }\n\n        .mode-btn.active .mode-name {\n            color: #FFD700;\n        }\n\n        .mode-btn .mode-desc {\n            font-family: 'Nunito', sans-serif;\n            font-size: 18px;\n            color: #aab;\n            line-height: 1.3;\n            margin-top: 4px;\n        }\n\n        .mode-btn.active .mode-desc {\n            color: #ddc;\n        }\n\n        .coins-earned {\n            font-family: 'Poppins', sans-serif;\n            font-size: 26px;\n            font-weight: 800;\n            color: #FFD700;\n            margin: 14px 0;\n            text-shadow: 0 4px 18px rgba(255,215,0,0.8), 0 0 35px rgba(255,215,0,0.5);\n            background: linear-gradient(135deg, rgba(255,215,0,0.18) 0%, rgba(255,165,0,0.18) 100%);\n            padding: 14px 28px;\n            border-radius: 18px;\n            border: 3px solid rgba(255,215,0,0.45);\n            animation: coins-shine 2s ease-in-out infinite;\n        }\n\n        @keyframes coins-shine {\n            0%, 100% { \n                box-shadow: 0 6px 30px rgba(255,215,0,0.4), inset 0 2px 0 rgba(255,255,255,0.2);\n            }\n            50% { \n                box-shadow: 0 8px 40px rgba(255,215,0,0.6), inset 0 2px 0 rgba(255,255,255,0.3);\n            }\n        }\n\n        .leaderboard-container {\n            width: 92%;\n            max-width: 480px;\n            margin: 12px auto;\n            background: linear-gradient(135deg, rgba(36,38,65,0.94) 0%, rgba(22,24,46,0.96) 100%);\n            border: 2px solid rgba(255,255,255,0.15);\n            border-radius: 18px;\n            padding: 14px;\n            backdrop-filter: blur(12px);\n            box-shadow: 0 6px 24px rgba(0,0,0,0.45);\n            max-height: 260px;\n            overflow-y: auto;\n        }\n\n        .leaderboard-title {\n            font-family: 'Poppins', sans-serif;\n            font-size: 22px;\n            font-weight: 700;\n            color: #FFD700;\n            text-align: center;\n            margin-bottom: 12px;\n            text-shadow: 0 3px 12px rgba(255,215,0,0.5);\n        }\n\n        .leaderboard-entry {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            padding: 10px 14px;\n            margin: 6px 0;\n            background: linear-gradient(135deg, rgba(44,44,68,0.75) 0%, rgba(32,32,54,0.85) 100%);\n            border-radius: 12px;\n            border: 1.5px solid rgba(255,255,255,0.1);\n            font-family: 'Nunito', sans-serif;\n            font-size: 18px;\n            color: #eee;\n        }\n\n        .leaderboard-entry.user {\n            border: 2px solid #FFD700;\n            background: linear-gradient(135deg, rgba(255,215,0,0.15) 0%, rgba(255,165,0,0.15) 100%);\n        }\n\n        .leaderboard-entry.rank-1 {\n            border-color: #FFD700;\n            background: linear-gradient(135deg, rgba(255,215,0,0.2) 0%, rgba(255,165,0,0.2) 100%);\n        }\n\n        .leaderboard-entry.rank-2 {\n            border-color: #C0C0C0;\n            background: linear-gradient(135deg, rgba(192,192,192,0.15) 0%, rgba(160,160,160,0.15) 100%);\n        }\n\n        .leaderboard-entry.rank-3 {\n            border-color: #CD7F32;\n            background: linear-gradient(135deg, rgba(205,127,50,0.15) 0%, rgba(180,100,40,0.15) 100%);\n        }\n\n        .leaderboard-rank {\n            font-weight: 700;\n            font-size: 20px;\n            min-width: 40px;\n            text-align: center;\n        }\n\n        .leaderboard-rank.rank-1 { color: #FFD700; }\n        .leaderboard-rank.rank-2 { color: #C0C0C0; }\n        .leaderboard-rank.rank-3 { color: #CD7F32; }\n\n        .leaderboard-player {\n            flex: 1;\n            margin: 0 12px;\n            font-weight: 600;\n            font-size: 18px;\n            overflow: hidden;\n            text-overflow: ellipsis;\n            white-space: nowrap;\n        }\n\n        .leaderboard-score {\n            font-weight: 700;\n            font-size: 20px;\n            color: #FFD700;\n            text-shadow: 0 2px 8px rgba(255,215,0,0.45);\n        }\n\n        .cumulative-stats {\n            width: 92%;\n            max-width: 480px;\n            margin: 12px auto;\n            padding: 14px 20px;\n            background: linear-gradient(135deg, rgba(52,128,192,0.22) 0%, rgba(34,82,152,0.26) 100%);\n            border: 2px solid rgba(100,150,255,0.35);\n            border-radius: 16px;\n            text-align: center;\n            font-family: 'Poppins', sans-serif;\n        }\n\n        .cumulative-title {\n            font-size: 20px;\n            font-weight: 600;\n            color: #bbc;\n            margin-bottom: 6px;\n        }\n\n        .cumulative-value {\n            font-size: 32px;\n            font-weight: 800;\n            color: #4AAFFF;\n            text-shadow: 0 3px 12px rgba(74,175,255,0.55);\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"game-canvas\"></canvas>\n        <div class=\"ui-overlay\" id=\"ui-overlay\"></div>\n    </div>\n\n    <script>\n    /*\n     * MY 99-METER BLADE - TRUE 3D VERSION\n     * A Minecraft-style 3D arena game using Three.js\n     */\n    async function run(mode) {\n        const container = document.getElementById('game-world');\n        const canvas = document.getElementById('game-canvas');\n        const uiOverlay = document.getElementById('ui-overlay');\n        \n        // === THREE.JS SETUP ===\n        const scene = new THREE.Scene();\n        \n        // Beautiful gradient sky\n        scene.background = new THREE.Color(0x87CEEB);\n        scene.fog = new THREE.Fog(0x9BC4E2, 40, 100);\n        \n        // Add sky dome with gradient\n        const skyGeo = new THREE.SphereGeometry(400, 32, 32);\n        const skyMat = new THREE.ShaderMaterial({\n            uniforms: {\n                topColor: { value: new THREE.Color(0x0077BE) },\n                bottomColor: { value: new THREE.Color(0x87CEEB) },\n                offset: { value: 20 },\n                exponent: { value: 0.6 }\n            },\n            vertexShader: `\n                varying vec3 vWorldPosition;\n                void main() {\n                    vec4 worldPosition = modelMatrix * vec4(position, 1.0);\n                    vWorldPosition = worldPosition.xyz;\n                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n                }\n            `,\n            fragmentShader: `\n                uniform vec3 topColor;\n                uniform vec3 bottomColor;\n                uniform float offset;\n                uniform float exponent;\n                varying vec3 vWorldPosition;\n                void main() {\n                    float h = normalize(vWorldPosition + offset).y;\n                    gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);\n                }\n            `,\n            side: THREE.BackSide\n        });\n        const sky = new THREE.Mesh(skyGeo, skyMat);\n        scene.add(sky);\n        \n\n        \n        const camera = new THREE.PerspectiveCamera(60, 720 / 1280, 0.1, 200);\n        camera.position.set(0, 35, 25);\n        camera.lookAt(0, 0, 0);\n        \n        const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });\n        renderer.setSize(720, 1280);\n        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); // Limit pixel ratio for performance\n        renderer.shadowMap.enabled = true;\n        renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n        \n        // === LIGHTING ===\n        const ambientLight = new THREE.AmbientLight(0xB4D7FF, 0.55);\n        scene.add(ambientLight);\n        \n        // Hemisphere light for better outdoor feel\n        const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x5B8731, 0.4);\n        scene.add(hemiLight);\n        \n        const sunLight = new THREE.DirectionalLight(0xFFFAE6, 0.9);\n        sunLight.position.set(30, 50, 30);\n        sunLight.castShadow = true;\n        sunLight.shadow.mapSize.width = 1024;\n        sunLight.shadow.mapSize.height = 1024;\n        sunLight.shadow.camera.near = 1;\n        sunLight.shadow.camera.far = 300;\n        sunLight.shadow.camera.left = -100;\n        sunLight.shadow.camera.right = 100;\n        sunLight.shadow.camera.top = 100;\n        sunLight.shadow.camera.bottom = -100;\n        scene.add(sunLight);\n        \n        // === CONFIG ===\n        const config = window.gameConfig;\n        \n        // === CHARACTER DEFINITIONS ===\n        const CHARACTER_DEFS = {\n            newbie: {\n                name: 'Newbie',\n                price: 0,\n                emoji: '\ud83e\uddd1',\n                colors: { shirt: 0x4488CC, skin: 0xE8C39E },\n                passive: 'A balanced character with no special abilities',\n                passiveShort: 'Balanced'\n            },\n            bomber: {\n                name: 'Bomber',\n                price: 200,\n                emoji: '\ud83d\udca3',\n                colors: { shirt: 0xCC2222, skin: 0xE8C39E, tnt: 0x8B0000 },\n                passive: 'Every 5th attack triggers an explosion with +2 tile radius',\n                passiveShort: 'Explode every 5 hits'\n            },\n            bounty: {\n                name: 'Bounty',\n                price: 200,\n                emoji: '\ud83e\udd20',\n                colors: { shirt: 0x4A3728, skin: 0xE8C39E, hat: 0xFFD700, cape: 0x2A1A0A },\n                passive: 'Earn +5 bonus coins for each kill',\n                passiveShort: '+5 coins per kill'\n            },\n            miner: {\n                name: 'Miner',\n                price: 500,\n                emoji: '\u26cf\ufe0f',\n                colors: { shirt: 0x8B4513, skin: 0xD4A574, hat: 0xFFCC00, lamp: 0xFFFF88 },\n                passive: 'Gain 2x blade length from stone objects',\n                passiveShort: '2x stone bonus'\n            },\n            slider: {\n                name: 'Slider',\n                price: 500,\n                emoji: '\ud83e\uddca',\n                colors: { shirt: 0x88CCFF, skin: 0xE8C39E, ice: 0xAAEEFF },\n                passive: 'Speed increases up to +100% after 5s of continuous movement; fades in 1s when stopped',\n                passiveShort: 'Speed boost'\n            },\n            guardian: {\n                name: 'Guardian',\n                price: 500,\n                emoji: '\ud83d\udee1\ufe0f',\n                colors: { shirt: 0x555566, skin: 0xE8C39E, shield: 0x4466AA, armor: 0x666677 },\n                passive: '180\u00b0 block angle; successful blocks stun for +1s and grant +200% speed for 2s',\n                passiveShort: '180\u00b0 block + stun + speed'\n            },\n            reckless: {\n                name: 'Reckless',\n                price: 500,\n                emoji: '\ud83d\ude08',\n                colors: { shirt: 0x882222, skin: 0xCCAAAA, scarf: 0xAA2222 },\n                passive: 'Cannot block; always attacks even when stationary; +20% attack speed',\n                passiveShort: 'No block, +20% atk speed'\n            },\n            knight: {\n                name: 'Knight',\n                price: 1000,\n                emoji: '\ud83c\udff0',\n                colors: { shirt: 0x4466AA, skin: 0xE8C39E, armor: 0x6688CC, crystal: 0x44DDFF },\n                passive: 'Starts each life with a shield that blocks one frontal attack',\n                passiveShort: '1 shield per life'\n            },\n            ninja: {\n                name: 'Ninja',\n                price: 1000,\n                emoji: '\ud83d\udc64',\n                colors: { shirt: 0x2A1A3A, skin: 0xE8C39E, scarf: 0x4A2A5A },\n                passive: 'Absorb 35% blade length on kills; lose 35% on death',\n                passiveShort: '35% absorb'\n            }\n        };\n\n        // === AUDIO ===\n        let audioCtx;\n        let bgMusicSource = null;\n        let bgMusicBuffer = null;\n        let bgMusicGain = null;\n        \n        function getAudioCtx() {\n            if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n            return audioCtx;\n        }\n        \n        // Load background music\n        async function loadBackgroundMusic() {\n            try {\n                const musicAsset = lib.getAsset('background_music');\n                if (!musicAsset) return;\n                \n                const ctx = getAudioCtx();\n                const response = await fetch(musicAsset.url);\n                const arrayBuffer = await response.arrayBuffer();\n                bgMusicBuffer = await ctx.decodeAudioData(arrayBuffer);\n            } catch(e) {\n                lib.log('Failed to load background music: ' + e.message);\n            }\n        }\n        \n        // Play background music\n        function playBackgroundMusic() {\n            if (!bgMusicBuffer) return;\n            try {\n                const ctx = getAudioCtx();\n                if (ctx.state === 'suspended') ctx.resume();\n                \n                // If already playing, keep it playing for continuous seamless audio\n                if (bgMusicSource) {\n                    return;\n                }\n                \n                // Create new source\n                bgMusicSource = ctx.createBufferSource();\n                bgMusicSource.buffer = bgMusicBuffer;\n                bgMusicSource.loop = true;\n                \n                // Create gain node for volume control\n                if (!bgMusicGain) {\n                    bgMusicGain = ctx.createGain();\n                    bgMusicGain.connect(ctx.destination);\n                }\n                bgMusicGain.gain.value = 0.35; // Background music at 35% volume\n                \n                bgMusicSource.connect(bgMusicGain);\n                bgMusicSource.start(0);\n            } catch(e) {\n                lib.log('Failed to play background music: ' + e.message);\n            }\n        }\n        \n        // Stop background music\n        function stopBackgroundMusic() {\n            if (bgMusicSource) {\n                try {\n                    bgMusicSource.stop();\n                } catch(e) {}\n                bgMusicSource = null;\n            }\n        }\n        \n        function playSound(freq, duration, type = 'square', volume = 0.15, delay = 0) {\n            try {\n                const ctx = getAudioCtx();\n                const osc = ctx.createOscillator();\n                const gain = ctx.createGain();\n                osc.type = type;\n                osc.frequency.value = freq;\n                const startTime = ctx.currentTime + delay;\n                gain.gain.setValueAtTime(volume, startTime);\n                gain.gain.exponentialRampToValueAtTime(0.001, startTime + duration);\n                osc.connect(gain);\n                gain.connect(ctx.destination);\n                osc.start(startTime);\n                osc.stop(startTime + duration);\n            } catch(e) {}\n        }\n        \n        // Sweep sound effect (rising or falling pitch)\n        function playSweep(startFreq, endFreq, duration, type = 'sine', volume = 0.1) {\n            try {\n                const ctx = getAudioCtx();\n                const osc = ctx.createOscillator();\n                const gain = ctx.createGain();\n                osc.type = type;\n                osc.frequency.setValueAtTime(startFreq, ctx.currentTime);\n                osc.frequency.exponentialRampToValueAtTime(endFreq, ctx.currentTime + duration);\n                gain.gain.setValueAtTime(volume, ctx.currentTime);\n                gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);\n                osc.connect(gain);\n                gain.connect(ctx.destination);\n                osc.start();\n                osc.stop(ctx.currentTime + duration);\n            } catch(e) {}\n        }\n        \n        // Noise burst for impacts\n        function playNoise(duration, volume = 0.1) {\n            try {\n                const ctx = getAudioCtx();\n                const bufferSize = Math.floor(ctx.sampleRate * duration);\n                const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);\n                const data = buffer.getChannelData(0);\n                for (let i = 0; i < bufferSize; i++) {\n                    data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i/bufferSize, 2);\n                }\n                const source = ctx.createBufferSource();\n                source.buffer = buffer;\n                const gain = ctx.createGain();\n                gain.gain.value = volume;\n                source.connect(gain);\n                gain.connect(ctx.destination);\n                source.start();\n            } catch(e) {}\n        }\n        \n        // Material-specific hit sounds\n        function playHitSound(material = 'default') {\n            if (material === 'wood' || material === 'crate') {\n                // Wood thunk - lower, softer\n                playSound(120 + Math.random()*40, 0.08, 'triangle', 0.12);\n                playNoise(0.05, 0.06);\n            } else if (material === 'stone') {\n                // Stone crack - sharp, crunchy\n                playSound(180 + Math.random()*60, 0.06, 'square', 0.08);\n                playSound(90 + Math.random()*30, 0.1, 'sawtooth', 0.06);\n                playNoise(0.08, 0.1);\n            } else if (material === 'gold') {\n                // Gold ring - metallic, bright\n                playSound(800 + Math.random()*200, 0.15, 'sine', 0.1);\n                playSound(1200 + Math.random()*300, 0.1, 'sine', 0.06);\n            } else if (material === 'diamond') {\n                // Diamond chime - crystalline, high\n                playSound(1400 + Math.random()*400, 0.2, 'sine', 0.08);\n                playSound(2000 + Math.random()*500, 0.15, 'sine', 0.05);\n                playSound(1800, 0.25, 'sine', 0.04, 0.05);\n            } else if (material === 'grass' || material === 'flower') {\n                // Soft rustling\n                playNoise(0.06, 0.04);\n                playSound(300 + Math.random()*100, 0.05, 'sine', 0.03);\n            } else if (material === 'mushroom') {\n                // Squishy pop\n                playSound(200 + Math.random()*80, 0.1, 'sine', 0.1);\n                playSweep(400, 150, 0.08, 'sine', 0.06);\n            } else if (material === 'tree') {\n                // Wood crunch\n                playSound(100 + Math.random()*50, 0.12, 'triangle', 0.12);\n                playNoise(0.1, 0.08);\n                playSound(80, 0.15, 'sawtooth', 0.05);\n            } else {\n                // Default hit\n                playSound(200 + Math.random()*100, 0.1, 'square', 0.1);\n            }\n        }\n        \n        // Swing/slash sound\n        function playSwingSound(length) {\n            const pitchMod = Math.max(0.5, 1 - length / 100);\n            playSweep(300 * pitchMod, 150 * pitchMod, 0.12, 'sawtooth', 0.06);\n            playNoise(0.08, 0.04);\n        }\n        \n        // Destroy/break sounds by material\n        function playDestroySound(material) {\n            if (material === 'wood' || material.includes('crate')) {\n                // Wood splinter\n                playSound(150, 0.1, 'triangle', 0.15);\n                playNoise(0.15, 0.12);\n                playSound(100 + Math.random()*50, 0.08, 'square', 0.06, 0.03);\n                playSound(80, 0.12, 'sawtooth', 0.05, 0.05);\n            } else if (material.includes('stone')) {\n                // Stone crumble\n                playNoise(0.2, 0.15);\n                playSound(100 + Math.random()*40, 0.15, 'sawtooth', 0.1);\n                playSound(60, 0.2, 'square', 0.08);\n            } else if (material === 'gold') {\n                // Gold treasure burst\n                playSound(600, 0.1, 'sine', 0.12);\n                playSound(900, 0.15, 'sine', 0.1, 0.03);\n                playSound(1200, 0.2, 'sine', 0.08, 0.06);\n                playSweep(800, 1600, 0.2, 'sine', 0.06);\n            } else if (material === 'diamond') {\n                // Diamond shatter - magical\n                playSound(1200, 0.1, 'sine', 0.1);\n                playSound(1800, 0.15, 'sine', 0.08, 0.02);\n                playSound(2400, 0.2, 'sine', 0.06, 0.04);\n                playSweep(1000, 2500, 0.25, 'sine', 0.08);\n                playSound(1500, 0.3, 'triangle', 0.05, 0.08);\n            } else if (material.includes('tree')) {\n                // Tree fall\n                playSound(80, 0.2, 'sawtooth', 0.12);\n                playNoise(0.2, 0.1);\n                playSweep(150, 60, 0.25, 'triangle', 0.08);\n            } else if (material.includes('mushroom')) {\n                // Mushroom pop\n                playSweep(500, 200, 0.1, 'sine', 0.12);\n                playNoise(0.08, 0.06);\n            } else if (material.includes('flower') || material === 'grass') {\n                // Soft poof\n                playNoise(0.1, 0.06);\n                playSound(400 + Math.random()*100, 0.08, 'sine', 0.05);\n            } else {\n                // Default destroy\n                playNoise(0.12, 0.1);\n                playSound(200, 0.1, 'square', 0.08);\n            }\n        }\n        \n        // Collect/gain length sound\n        function playCollectSound(amount = 1) {\n            const baseFreq = 500 + Math.min(amount * 100, 400);\n            playSound(baseFreq, 0.1, 'sine', 0.1);\n            playSound(baseFreq * 1.5, 0.12, 'sine', 0.08, 0.03);\n            if (amount >= 1) {\n                playSound(baseFreq * 2, 0.15, 'sine', 0.06, 0.06);\n            }\n        }\n        \n        // Kill enemy sound\n        function playKillSound() {\n            playSound(300, 0.15, 'sawtooth', 0.12);\n            playSound(450, 0.12, 'square', 0.08, 0.02);\n            playSweep(400, 800, 0.2, 'sine', 0.1);\n            playNoise(0.15, 0.08);\n            // Victory chime\n            playSound(600, 0.1, 'sine', 0.08, 0.1);\n            playSound(800, 0.1, 'sine', 0.06, 0.15);\n        }\n        \n        // Block/parry sound\n        function playBlockSound() {\n            playSound(300, 0.2, 'sine', 0.15);\n            playSound(450, 0.15, 'triangle', 0.1);\n            playNoise(0.1, 0.08);\n        }\n        \n        // Weapon clash sound\n        function playClashSound() {\n            playSound(800, 0.12, 'square', 0.15);\n            playSound(600, 0.1, 'sawtooth', 0.1);\n            playSound(1000, 0.08, 'sine', 0.08, 0.02);\n            playNoise(0.1, 0.12);\n        }\n        \n        // Stun sound\n        function playStunSound() {\n            playSweep(600, 200, 0.3, 'sine', 0.12);\n            playSound(150, 0.2, 'square', 0.08);\n        }\n        \n        // Take damage sound\n        function playDamageSound() {\n            playSound(150, 0.15, 'sawtooth', 0.15);\n            playSweep(400, 150, 0.2, 'square', 0.1);\n            playNoise(0.1, 0.1);\n        }\n        \n        // Milestone/level up sound\n        function playMilestoneSound(tier) {\n            const baseFreq = 400 + tier * 100;\n            playSound(baseFreq, 0.1, 'sine', 0.12);\n            playSound(baseFreq * 1.25, 0.12, 'sine', 0.1, 0.08);\n            playSound(baseFreq * 1.5, 0.15, 'sine', 0.08, 0.16);\n            playSound(baseFreq * 2, 0.2, 'sine', 0.06, 0.24);\n            playSweep(baseFreq, baseFreq * 2, 0.4, 'triangle', 0.05);\n        }\n        \n        // Rush mode warning sound\n        function playRushWarning() {\n            playSound(600, 0.2, 'square', 0.12);\n            playSound(800, 0.15, 'square', 0.1, 0.1);\n            playSound(600, 0.2, 'square', 0.12, 0.2);\n        }\n        \n        // Countdown tick sound\n        function playCountdownTick() {\n            playSound(1000, 0.05, 'sine', 0.08);\n        }\n        \n        // === COIN & CHARACTER MANAGEMENT ===\n        \n        // Save character progress to persistent storage\n        async function saveCharacterProgress() {\n            try {\n                const saveData = {\n                    playerCoins: config.playerCoins || 0,\n                    unlockedCharacters: config.unlockedCharacters || ['newbie'],\n                    selectedCharacter: config.selectedCharacter || 'newbie',\n                    cumulativeBladeLength: config.cumulativeBladeLength || 0\n                };\n                await lib.saveUserGameState(saveData);\n                lib.log('Character progress saved successfully');\n            } catch (error) {\n                lib.log(`Failed to save character progress: ${error.message}`);\n            }\n        }\n        \n        // Load character progress from persistent storage\n        async function loadCharacterProgress() {\n            try {\n                const response = await lib.getUserGameState();\n                if (response.state) {\n                    // Restore saved data\n                    config.playerCoins = response.state.playerCoins || 0;\n                    config.unlockedCharacters = response.state.unlockedCharacters || ['newbie'];\n                    config.selectedCharacter = response.state.selectedCharacter || 'newbie';\n                    config.cumulativeBladeLength = response.state.cumulativeBladeLength || 0;\n                    lib.log('Character progress loaded successfully');\n                } else {\n                    // New player - use configured initial coins or default to 0\n                    config.playerCoins = config.initialCoins || 0;\n                    config.unlockedCharacters = ['newbie'];\n                    config.selectedCharacter = 'newbie';\n                    config.cumulativeBladeLength = 0;\n                    lib.log('No saved progress found, using defaults with initial coins: ' + config.playerCoins);\n                }\n            } catch (error) {\n                lib.log(`Failed to load character progress: ${error.message}`);\n                // Use defaults on error\n                config.playerCoins = config.playerCoins || config.initialCoins || 0;\n                config.unlockedCharacters = config.unlockedCharacters || ['newbie'];\n                config.selectedCharacter = config.selectedCharacter || 'newbie';\n                config.cumulativeBladeLength = config.cumulativeBladeLength || 0;\n            }\n        }\n        \n        function getPlayerCoins() {\n            return config.playerCoins || 0;\n        }\n        \n        function addPlayerCoins(amount) {\n            config.playerCoins = getPlayerCoins() + amount;\n            saveCharacterProgress(); // Auto-save when coins change\n            return config.playerCoins;\n        }\n        \n        function getUnlockedCharacters() {\n            return config.unlockedCharacters || ['newbie'];\n        }\n        \n        function isCharacterUnlocked(charId) {\n            return getUnlockedCharacters().includes(charId);\n        }\n        \n        function unlockCharacter(charId) {\n            const unlocked = getUnlockedCharacters();\n            if (!unlocked.includes(charId)) {\n                unlocked.push(charId);\n                config.unlockedCharacters = unlocked;\n                saveCharacterProgress(); // Auto-save when character unlocked\n            }\n        }\n        \n        function purchaseCharacter(charId) {\n            const charDef = CHARACTER_DEFS[charId];\n            if (!charDef) return false;\n            if (isCharacterUnlocked(charId)) return false;\n            if (getPlayerCoins() < charDef.price) return false;\n            \n            config.playerCoins = getPlayerCoins() - charDef.price;\n            unlockCharacter(charId); // This will trigger save\n            return true;\n        }\n        \n        function getSelectedCharacter() {\n            return config.selectedCharacter || 'newbie';\n        }\n        \n        function setSelectedCharacter(charId) {\n            if (isCharacterUnlocked(charId)) {\n                config.selectedCharacter = charId;\n                saveCharacterProgress(); // Auto-save when selection changes\n                return true;\n            }\n            return false;\n        }\n        \n        // Game mode functions\n        function getGameMode() {\n            return config.gameMode || 'standard';\n        }\n        \n        function setGameMode(mode) {\n            config.gameMode = mode;\n        }\n        \n        function isPeaceMode() {\n            return getGameMode() === 'peace';\n        }\n        \n        function getCurrentCharacterDef() {\n            return CHARACTER_DEFS[getSelectedCharacter()] || CHARACTER_DEFS.newbie;\n        }\n        \n        // Game start sound\n        function playGameStartSound() {\n            playSound(400, 0.15, 'sine', 0.1);\n            playSound(500, 0.15, 'sine', 0.1, 0.1);\n            playSound(600, 0.15, 'sine', 0.1, 0.2);\n            playSound(800, 0.25, 'sine', 0.12, 0.3);\n        }\n        \n        // Game over sound\n        function playGameOverSound() {\n            playSound(500, 0.2, 'sine', 0.12);\n            playSound(400, 0.2, 'sine', 0.1, 0.15);\n            playSound(300, 0.3, 'sine', 0.08, 0.3);\n            playSweep(600, 200, 0.5, 'triangle', 0.06);\n        }\n        \n        // Footstep sound\n        let lastFootstepTime = 0;\n        function playFootstep() {\n            const now = Date.now();\n            if (now - lastFootstepTime < 250) return; // Rate limit footsteps\n            lastFootstepTime = now;\n            playSound(80 + Math.random()*40, 0.06, 'triangle', 0.04);\n            playNoise(0.03, 0.02);\n        }\n\n        // === GAME CONSTANTS ===\n        const ARENA_SIZE = 50; // 3D world units\n        const PLAYER_SPEED = 6;\n        const growthMult = config.growthMultiplier || 1;\n\n        // === OBJECT DEFINITIONS ===\n        const OBJ_DEFS = {\n            grass: { color: 0x6AAA40, size: 0.8, hp: 1, lengthGain: 0.1, soulColor: 0x32CD32 },\n            flower_yellow: { color: 0xFFE135, size: 0.5, hp: 1, lengthGain: 0.15, soulColor: 0xFFFF00, model: 'flower' },\n            flower_red: { color: 0xFF4444, size: 0.5, hp: 1, lengthGain: 0.15, soulColor: 0xFF6666, model: 'flower' },\n            mushroom_small: { color: 0xE02020, size: 0.4, hp: 1, lengthGain: 0.15, soulColor: 0xFF6B6B, model: 'mushroom_small' },\n            mushroom: { color: 0xE02020, size: 0.5, hp: 1, lengthGain: 0.2, soulColor: 0xFF6B6B, model: 'mushroom' },\n            mushroom_large: { color: 0xE02020, size: 0.7, hp: 2, lengthGain: 0.35, soulColor: 0xFF6B6B, model: 'mushroom_large' },\n            crate_small: { color: 0xA08040, size: 0.7, hp: 1, lengthGain: 0.2, soulColor: 0x4169E1 },\n            crate: { color: 0xA08040, size: 1.0, hp: 2, lengthGain: 0.3, soulColor: 0x4169E1 },\n            crate_large: { color: 0xA08040, size: 1.4, hp: 3, lengthGain: 0.5, soulColor: 0x4169E1 },\n            stump: { color: 0x6B5344, size: 1.0, hp: 3, lengthGain: 0.5, soulColor: 0x32CD32 },\n            tree_small: { color: 0x228B22, size: 0.8, hp: 2, lengthGain: 0.3, soulColor: 0x32CD32, model: 'tree_small' },\n            tree: { color: 0x228B22, size: 1.2, hp: 4, lengthGain: 0.6, soulColor: 0x32CD32, model: 'tree' },\n            tree_large: { color: 0x228B22, size: 1.6, hp: 6, lengthGain: 1.0, soulColor: 0x32CD32, model: 'tree_large' },\n            stone_small: { color: 0x808080, size: 0.7, hp: 2, lengthGain: 0.4, soulColor: 0xC0C0C0 },\n            stone: { color: 0x808080, size: 1.2, hp: 3, lengthGain: 0.8, soulColor: 0xC0C0C0 },\n            stone_large: { color: 0x808080, size: 1.8, hp: 4, lengthGain: 1.2, soulColor: 0xC0C0C0 },\n            stone_huge: { color: 0x707070, size: 2.5, hp: 5, lengthGain: 1.8, soulColor: 0xC0C0C0 },\n            gold: { color: 0xFFD700, size: 1.1, hp: 3, lengthGain: 2.5, soulColor: 0xFFD700, glow: true },\n            diamond: { color: 0x00CED1, size: 1.1, hp: 4, lengthGain: 4.0, soulColor: 0x00FFFF, glow: true }\n        };\n\n        // Show game parameters\n        lib.showGameParameters({\n            name: 'Game Settings',\n            params: {\n                'Match Duration (s)': { key: 'matchDuration', type: 'slider', min: 60, max: 300, step: 10 },\n                'Starting Length (m)': { key: 'startLength', type: 'slider', min: 1, max: 5, step: 0.5 },\n                'Growth Multiplier': { key: 'growthMultiplier', type: 'slider', min: 0.5, max: 3, step: 0.25 },\n                'AI Enemy Count': { key: 'aiCount', type: 'slider', min: 0, max: 12, step: 1 },\n                'Object Density': { key: 'objectDensity', type: 'slider', min: 0.5, max: 2, step: 0.25 },\n                'Initial Coins': { key: 'initialCoins', type: 'number', min: 0, max: 100000, step: 100 },\n                'AI Miss Chance (%)': { key: 'aiMissChance', type: 'slider', min: 0, max: 80, step: 5 },\n                'Miss Chance Rule': { key: 'aiMissChanceInfo', type: 'string' }\n            }\n        });\n\n        // === 3D MATERIALS ===\n        const materials = {\n            grass: new THREE.MeshLambertMaterial({ color: 0x6AAA40 }),\n            grassTop: new THREE.MeshLambertMaterial({ color: 0x7EC850 }),\n            dirt: new THREE.MeshLambertMaterial({ color: 0x8B5A2B }),\n            crate: new THREE.MeshLambertMaterial({ color: 0xA08040 }),\n            stump: new THREE.MeshLambertMaterial({ color: 0x6B5344 }),\n            stone: new THREE.MeshLambertMaterial({ color: 0x808080 }),\n            gold: new THREE.MeshLambertMaterial({ color: 0xFFD700, emissive: 0x332200 }),\n            diamond: new THREE.MeshLambertMaterial({ color: 0x00CED1, emissive: 0x002222 }),\n            player: new THREE.MeshLambertMaterial({ color: 0x4488CC }),\n            playerSkin: new THREE.MeshLambertMaterial({ color: 0xE8C39E }),\n            enemyRed: new THREE.MeshLambertMaterial({ color: 0xCC3333 }),\n            enemyPurple: new THREE.MeshLambertMaterial({ color: 0x8B008B }),\n            enemyOrange: new THREE.MeshLambertMaterial({ color: 0xFF8C00 }),\n            enemyGreen: new THREE.MeshLambertMaterial({ color: 0x228B22 }),\n            weaponWood: new THREE.MeshLambertMaterial({ color: 0x8B4513 }),\n            weaponStone: new THREE.MeshLambertMaterial({ color: 0x808080 }),\n            weaponGold: new THREE.MeshLambertMaterial({ color: 0xFFD700, emissive: 0x332200 }),\n            weaponDiamond: new THREE.MeshLambertMaterial({ color: 0x00CED1, emissive: 0x003333 })\n        };\n\n        // === CREATE 3D GROUND ===\n        function createGround() {\n            const groundGroup = new THREE.Group();\n            \n            // Main floor with gradient coloring\n            const floorGeo = new THREE.PlaneGeometry(ARENA_SIZE * 2, ARENA_SIZE * 2, 20, 20);\n            const floorMat = new THREE.MeshLambertMaterial({ color: 0x5B8731, vertexColors: true });\n            \n            // Add vertex color variation for natural look\n            const colors = [];\n            const positions = floorGeo.attributes.position;\n            for (let i = 0; i < positions.count; i++) {\n                const x = positions.getX(i);\n                const z = positions.getZ(i);\n                const distFromCenter = Math.sqrt(x*x + z*z) / ARENA_SIZE;\n                // Center is slightly golden/lighter, edges are darker - desaturated earthy green\n                const hue = 0.22 + Math.random() * 0.08; // More yellow-green variation\n                const sat = 0.12 + Math.random() * 0.08; // Much lower saturation - pale, earthy\n                const light = 0.38 + (1 - distFromCenter) * 0.12 + Math.random() * 0.1;\n                const color = new THREE.Color().setHSL(hue, sat, light);\n                colors.push(color.r, color.g, color.b);\n            }\n            floorGeo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));\n            \n            const floor = new THREE.Mesh(floorGeo, floorMat);\n            floor.rotation.x = -Math.PI / 2;\n            floor.position.y = -0.01;\n            floor.receiveShadow = true;\n            groundGroup.add(floor);\n            \n            // Continuous curved dirt paths \u2014 one smooth spline ribbon per direction\n            const pathMat = new THREE.MeshLambertMaterial({ color: 0x8B6914, side: THREE.DoubleSide });\n            const pathDarkMat = new THREE.MeshLambertMaterial({ color: 0x7A5A10, side: THREE.DoubleSide });\n\n            // Build a flat ribbon mesh along a CatmullRom spline\n            function buildRibbon(pts2D, yPos, width, samples) {\n                const curve = new THREE.CatmullRomCurve3(\n                    pts2D.map(p => new THREE.Vector3(p[0], yPos, p[1])),\n                    false, 'catmullrom', 0.5\n                );\n                const verts = [];\n                const inds = [];\n                for (let i = 0; i <= samples; i++) {\n                    const t = i / samples;\n                    const pos = curve.getPoint(t);\n                    const tan = curve.getTangent(t).normalize();\n                    // perpendicular in XZ\n                    const px =  tan.z;\n                    const pz = -tan.x;\n                    verts.push(\n                        pos.x - px * width * 0.5, pos.y, pos.z - pz * width * 0.5,\n                        pos.x + px * width * 0.5, pos.y, pos.z + pz * width * 0.5\n                    );\n                    if (i < samples) {\n                        const b = i * 2;\n                        inds.push(b, b+1, b+2,  b+1, b+3, b+2);\n                    }\n                }\n                const geo = new THREE.BufferGeometry();\n                geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));\n                geo.setIndex(inds);\n                geo.computeVertexNormals();\n                return geo;\n            }\n\n            // Build edge-trim ribbon offset laterally from a spline\n            function buildOffsetRibbon(pts2D, yPos, lateralOffset, trimWidth, samples) {\n                const baseCurve = new THREE.CatmullRomCurve3(\n                    pts2D.map(p => new THREE.Vector3(p[0], yPos, p[1])),\n                    false, 'catmullrom', 0.5\n                );\n                const verts = [];\n                const inds = [];\n                for (let i = 0; i <= samples; i++) {\n                    const t = i / samples;\n                    const pos = baseCurve.getPoint(t);\n                    const tan = baseCurve.getTangent(t).normalize();\n                    const px =  tan.z;\n                    const pz = -tan.x;\n                    const cx = pos.x + px * lateralOffset;\n                    const cz = pos.z + pz * lateralOffset;\n                    verts.push(\n                        cx - px * trimWidth * 0.5, pos.y, cz - pz * trimWidth * 0.5,\n                        cx + px * trimWidth * 0.5, pos.y, cz + pz * trimWidth * 0.5\n                    );\n                    if (i < samples) {\n                        const b = i * 2;\n                        inds.push(b, b+1, b+2,  b+1, b+3, b+2);\n                    }\n                }\n                const geo = new THREE.BufferGeometry();\n                geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));\n                geo.setIndex(inds);\n                geo.computeVertexNormals();\n                return geo;\n            }\n\n            // Each path: control points [x, z], each unique curve shape\n            const pathDefs = [\n                // North  (toward -Z)\n                [[ 0,-7],[ 2.5,-15],[-2,-24],[ 1.5,-34],[ 0,-50]],\n                // South  (toward +Z)\n                [[ 0, 7],[-2.5, 16],[ 2, 25],[-1.5, 36],[ 0, 50]],\n                // East   (toward +X)\n                [[ 7, 0],[15, 2.5],[24,-2],[34, 1.5],[50, 0]],\n                // West   (toward -X)\n                [[-7, 0],[-15,-2.5],[-24, 2],[-34,-1.5],[-50, 0]]\n            ];\n\n            pathDefs.forEach(pts => {\n                // Main road surface\n                const roadMesh = new THREE.Mesh(buildRibbon(pts, 0.015, 3.2, 80), pathMat);\n                roadMesh.receiveShadow = true;\n                groundGroup.add(roadMesh);\n\n                // Dark edge trim, each side\n                [-1, 1].forEach(side => {\n                    const trimMesh = new THREE.Mesh(\n                        buildOffsetRibbon(pts, 0.02, side * 1.75, 0.38, 80),\n                        pathDarkMat\n                    );\n                    trimMesh.receiveShadow = true;\n                    groundGroup.add(trimMesh);\n                });\n            });\n            \n            // Central plaza - single unified cobblestone disc\n            const plazaGeo = new THREE.CylinderGeometry(9.0, 9.0, 0.12, 48);\n            const plazaMat = new THREE.MeshLambertMaterial({ color: 0x707070 });\n            const plaza = new THREE.Mesh(plazaGeo, plazaMat);\n            plaza.position.set(0, 0.04, 0);\n            plaza.receiveShadow = true;\n            groundGroup.add(plaza);\n\n            // Subtle border ring around plaza\n            const plazaRingGeo = new THREE.CylinderGeometry(9.3, 9.3, 0.1, 48);\n            const plazaRingMat = new THREE.MeshLambertMaterial({ color: 0x505050 });\n            const plazaRing = new THREE.Mesh(plazaRingGeo, plazaRingMat);\n            plazaRing.position.set(0, 0.03, 0);\n            plazaRing.receiveShadow = true;\n            groundGroup.add(plazaRing);\n\n            // Inner accent ring\n            const plazaInnerGeo = new THREE.CylinderGeometry(3.5, 3.5, 0.13, 48);\n            const plazaInnerMat = new THREE.MeshLambertMaterial({ color: 0x606060 });\n            const plazaInner = new THREE.Mesh(plazaInnerGeo, plazaInnerMat);\n            plazaInner.position.set(0, 0.045, 0);\n            plazaInner.receiveShadow = true;\n            groundGroup.add(plazaInner);\n            \n            // Scattered pebbles\n            const pebbleMat = new THREE.MeshLambertMaterial({ color: 0x707070 });\n            for (let i = 0; i < 30; i++) {\n                const pebbleGeo = new THREE.BoxGeometry(0.2 + Math.random() * 0.3, 0.1, 0.2 + Math.random() * 0.3);\n                const pebble = new THREE.Mesh(pebbleGeo, pebbleMat);\n                pebble.position.set(\n                    (Math.random() - 0.5) * ARENA_SIZE * 1.8,\n                    0.05,\n                    (Math.random() - 0.5) * ARENA_SIZE * 1.8\n                );\n                pebble.rotation.y = Math.random() * Math.PI;\n                groundGroup.add(pebble);\n            }\n            \n            // Small grass tufts\n            const grassMatD = new THREE.MeshLambertMaterial({ color: 0x4A8A30 });\n            const grassMatL = new THREE.MeshLambertMaterial({ color: 0x6AAA40 });\n            for (let i = 0; i < 60; i++) {\n                const tuftGroup = new THREE.Group();\n                for (let j = 0; j < 3; j++) {\n                    const bladeGeo = new THREE.BoxGeometry(0.05, 0.2 + Math.random() * 0.15, 0.05);\n                    const blade = new THREE.Mesh(bladeGeo, Math.random() > 0.5 ? grassMatD : grassMatL);\n                    blade.position.set((Math.random() - 0.5) * 0.2, 0.1, (Math.random() - 0.5) * 0.2);\n                    blade.rotation.z = (Math.random() - 0.5) * 0.4;\n                    tuftGroup.add(blade);\n                }\n                tuftGroup.position.set(\n                    (Math.random() - 0.5) * ARENA_SIZE * 1.9,\n                    0,\n                    (Math.random() - 0.5) * ARENA_SIZE * 1.9\n                );\n                groundGroup.add(tuftGroup);\n            }\n            \n            // Arena walls - stone brick style\n            const wallBaseMat = new THREE.MeshLambertMaterial({ color: 0x5A5A5A });\n            const wallBrickMat = new THREE.MeshLambertMaterial({ color: 0x6B6B6B });\n            const wallTopMat = new THREE.MeshLambertMaterial({ color: 0x4A4A4A });\n            \n            const wallPositions = [\n                { pos: [-ARENA_SIZE, 0, 0], rot: 0, len: ARENA_SIZE * 2 },\n                { pos: [ARENA_SIZE, 0, 0], rot: 0, len: ARENA_SIZE * 2 },\n                { pos: [0, 0, -ARENA_SIZE], rot: Math.PI/2, len: ARENA_SIZE * 2 },\n                { pos: [0, 0, ARENA_SIZE], rot: Math.PI/2, len: ARENA_SIZE * 2 }\n            ];\n            \n            wallPositions.forEach(w => {\n                const wallGroup = new THREE.Group();\n                \n                // Base wall\n                const baseGeo = new THREE.BoxGeometry(2.5, 2, w.len);\n                const base = new THREE.Mesh(baseGeo, wallBaseMat);\n                base.position.y = 1;\n                base.castShadow = true;\n                base.receiveShadow = true;\n                wallGroup.add(base);\n                \n                // Brick pattern\n                for (let bz = -w.len/2 + 1; bz < w.len/2; bz += 2) {\n                    for (let by = 0; by < 2; by++) {\n                        const brickGeo = new THREE.BoxGeometry(2.6, 0.9, 1.8);\n                        const brick = new THREE.Mesh(brickGeo, wallBrickMat);\n                        brick.position.set(0, 0.5 + by * 1, bz + (by % 2) * 1);\n                        wallGroup.add(brick);\n                    }\n                }\n                \n                // Wall top trim\n                const topGeo = new THREE.BoxGeometry(3, 0.4, w.len + 0.5);\n                const top = new THREE.Mesh(topGeo, wallTopMat);\n                top.position.y = 2.2;\n                top.castShadow = true;\n                wallGroup.add(top);\n                \n                // Crenellations (battlements)\n                for (let bz = -w.len/2 + 2; bz < w.len/2; bz += 4) {\n                    const crenGeo = new THREE.BoxGeometry(3.2, 0.8, 1.5);\n                    const cren = new THREE.Mesh(crenGeo, wallBaseMat);\n                    cren.position.set(0, 2.8, bz);\n                    cren.castShadow = true;\n                    wallGroup.add(cren);\n                }\n                \n                wallGroup.position.set(w.pos[0], w.pos[1], w.pos[2]);\n                wallGroup.rotation.y = w.rot;\n                groundGroup.add(wallGroup);\n            });\n            \n            // Corner towers\n            const towerMat = new THREE.MeshLambertMaterial({ color: 0x5A5A5A });\n            const towerTopMat = new THREE.MeshLambertMaterial({ color: 0x4A4A4A });\n            const corners = [[-1,-1],[1,-1],[-1,1],[1,1]];\n            const flagColors = [0xFF4444, 0x4444FF, 0x44FF44, 0xFFFF44];\n            corners.forEach(([cx, cz], idx) => {\n                const towerGroup = new THREE.Group();\n                \n                // Main tower body\n                const bodyGeo = new THREE.BoxGeometry(4, 6, 4);\n                const body = new THREE.Mesh(bodyGeo, towerMat);\n                body.position.y = 3;\n                body.castShadow = true;\n                towerGroup.add(body);\n                \n                // Tower top platform\n                const platGeo = new THREE.BoxGeometry(5, 0.5, 5);\n                const plat = new THREE.Mesh(platGeo, towerTopMat);\n                plat.position.y = 6.25;\n                plat.castShadow = true;\n                towerGroup.add(plat);\n                \n                // Tower crenellations\n                const crenPositions = [[-1.8, 0], [1.8, 0], [0, -1.8], [0, 1.8]];\n                crenPositions.forEach(([px, pz]) => {\n                    const crenGeo = new THREE.BoxGeometry(1.2, 1, 1.2);\n                    const cren = new THREE.Mesh(crenGeo, towerMat);\n                    cren.position.set(px, 7, pz);\n                    cren.castShadow = true;\n                    towerGroup.add(cren);\n                });\n                \n                // Flag pole\n                const poleGeo = new THREE.BoxGeometry(0.15, 3, 0.15);\n                const poleMat = new THREE.MeshLambertMaterial({ color: 0x4a3728 });\n                const pole = new THREE.Mesh(poleGeo, poleMat);\n                pole.position.y = 8;\n                towerGroup.add(pole);\n                \n                // Flag\n                const flagGeo = new THREE.BoxGeometry(0.05, 1.2, 2);\n                const flagMat = new THREE.MeshLambertMaterial({ color: flagColors[idx] });\n                const flag = new THREE.Mesh(flagGeo, flagMat);\n                flag.position.set(0, 8.8, 1);\n                towerGroup.add(flag);\n                \n                towerGroup.position.set(cx * ARENA_SIZE, 0, cz * ARENA_SIZE);\n                groundGroup.add(towerGroup);\n            });\n            \n            // Wall torches\n            const torchMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 });\n            const flameMat = new THREE.MeshBasicMaterial({ color: 0xFF6600 });\n            for (let i = 0; i < 16; i++) {\n                const torchGroup = new THREE.Group();\n                const stickGeo = new THREE.BoxGeometry(0.15, 0.8, 0.15);\n                const stick = new THREE.Mesh(stickGeo, torchMat);\n                stick.position.y = 2.8;\n                torchGroup.add(stick);\n                \n                const flameGeo = new THREE.BoxGeometry(0.25, 0.4, 0.25);\n                const flame = new THREE.Mesh(flameGeo, flameMat);\n                flame.position.y = 3.3;\n                torchGroup.add(flame);\n                \n                const side = Math.floor(i / 4);\n                const idx = i % 4;\n                const offset = (idx - 1.5) * 20;\n                if (side === 0) torchGroup.position.set(-ARENA_SIZE + 1.5, 0, offset);\n                else if (side === 1) torchGroup.position.set(ARENA_SIZE - 1.5, 0, offset);\n                else if (side === 2) torchGroup.position.set(offset, 0, -ARENA_SIZE + 1.5);\n                else torchGroup.position.set(offset, 0, ARENA_SIZE - 1.5);\n                \n                groundGroup.add(torchGroup);\n            }\n            \n            scene.add(groundGroup);\n        }\n\n        // === CREATE 3D CHARACTER ===\n        function createCharacter(shirtMaterial, isPlayer = false, characterId = null) {\n            const group = new THREE.Group();\n            const charDef = characterId ? CHARACTER_DEFS[characterId] : null;\n            \n            // Override shirt material if character is specified\n            if (charDef && isPlayer) {\n                shirtMaterial = new THREE.MeshLambertMaterial({ color: charDef.colors.shirt });\n            }\n            \n            // Body size variations based on character\n            let bodyScale = 1.0;\n            let bodyWidth = 0.8;\n            let bodyHeight = 1.0;\n            if (characterId === 'miner') {\n                bodyScale = 0.9;\n                bodyWidth = 0.9;\n            } else if (characterId === 'guardian') {\n                bodyScale = 1.15;\n                bodyWidth = 1.0;\n            } else if (characterId === 'knight') {\n                bodyScale = 1.2;\n                bodyWidth = 0.95;\n                bodyHeight = 1.1;\n            } else if (characterId === 'ninja') {\n                bodyScale = 0.85;\n                bodyWidth = 0.7;\n            } else if (characterId === 'slider') {\n                bodyScale = 0.95;\n            }\n            \n            // Skin material\n            const skinMat = charDef && isPlayer ? \n                new THREE.MeshLambertMaterial({ color: charDef.colors.skin }) : \n                materials.playerSkin;\n            \n            // Body\n            const bodyGeo = new THREE.BoxGeometry(bodyWidth, bodyHeight, 0.5);\n            const body = new THREE.Mesh(bodyGeo, shirtMaterial);\n            body.position.y = 1.0;\n            body.castShadow = true;\n            group.add(body);\n            \n            // Character-specific body decorations\n            if (characterId === 'bomber' && charDef) {\n                // TNT pack on back\n                const tntMat = new THREE.MeshLambertMaterial({ color: charDef.colors.tnt || 0x8B0000 });\n                const tntGeo = new THREE.BoxGeometry(0.5, 0.6, 0.3);\n                const tnt = new THREE.Mesh(tntGeo, tntMat);\n                tnt.position.set(0, 1.0, -0.35);\n                group.add(tnt);\n                // TNT label\n                const labelMat = new THREE.MeshLambertMaterial({ color: 0xFFFFFF });\n                const labelGeo = new THREE.BoxGeometry(0.3, 0.2, 0.05);\n                const label = new THREE.Mesh(labelGeo, labelMat);\n                label.position.set(0, 1.0, -0.52);\n                group.add(label);\n            } else if (characterId === 'bounty' && charDef) {\n                // Cape\n                const capeMat = new THREE.MeshLambertMaterial({ color: charDef.colors.cape || 0x2A1A0A });\n                const capeGeo = new THREE.BoxGeometry(0.7, 0.8, 0.1);\n                const cape = new THREE.Mesh(capeGeo, capeMat);\n                cape.position.set(0, 0.8, -0.3);\n                group.add(cape);\n                // Coin bag at waist\n                const bagMat = new THREE.MeshLambertMaterial({ color: 0x8B6914 });\n                const bagGeo = new THREE.BoxGeometry(0.25, 0.2, 0.15);\n                const bag = new THREE.Mesh(bagGeo, bagMat);\n                bag.position.set(0.4, 0.6, 0.15);\n                group.add(bag);\n            } else if (characterId === 'miner' && charDef) {\n                // Backpack\n                const packMat = new THREE.MeshLambertMaterial({ color: 0x5A4A3A });\n                const packGeo = new THREE.BoxGeometry(0.5, 0.5, 0.25);\n                const pack = new THREE.Mesh(packGeo, packMat);\n                pack.position.set(0, 0.9, -0.35);\n                group.add(pack);\n            } else if (characterId === 'guardian' && charDef) {\n                // Large shield on left arm\n                const shieldMat = new THREE.MeshLambertMaterial({ color: charDef.colors.shield || 0x4466AA });\n                const shieldGeo = new THREE.BoxGeometry(0.15, 0.9, 0.7);\n                const shield = new THREE.Mesh(shieldGeo, shieldMat);\n                shield.position.set(-0.55, 1.0, 0.2);\n                group.add(shield);\n                // Shield emblem\n                const emblemMat = new THREE.MeshLambertMaterial({ color: 0xFFD700 });\n                const emblemGeo = new THREE.BoxGeometry(0.18, 0.2, 0.2);\n                const emblem = new THREE.Mesh(emblemGeo, emblemMat);\n                emblem.position.set(-0.6, 1.0, 0.2);\n                group.add(emblem);\n            } else if (characterId === 'reckless' && charDef) {\n                // Torn scarf\n                const scarfMat = new THREE.MeshLambertMaterial({ color: charDef.colors.scarf || 0xAA2222 });\n                const scarfGeo = new THREE.BoxGeometry(0.2, 0.6, 0.1);\n                const scarf = new THREE.Mesh(scarfGeo, scarfMat);\n                scarf.position.set(0.1, 1.4, -0.25);\n                scarf.rotation.z = 0.3;\n                group.add(scarf);\n                // Body crack lines\n                const crackMat = new THREE.MeshBasicMaterial({ color: 0x330000 });\n                for (let i = 0; i < 3; i++) {\n                    const crackGeo = new THREE.BoxGeometry(0.03, 0.3, 0.52);\n                    const crack = new THREE.Mesh(crackGeo, crackMat);\n                    crack.position.set((i - 1) * 0.2, 0.9 + i * 0.1, 0);\n                    crack.rotation.z = (i - 1) * 0.2;\n                    group.add(crack);\n                }\n            } else if (characterId === 'knight' && charDef) {\n                // Crystal shoulder pads\n                const crystalMat = new THREE.MeshLambertMaterial({ color: charDef.colors.crystal || 0x44DDFF, emissive: 0x112233 });\n                const shoulderGeo = new THREE.BoxGeometry(0.35, 0.2, 0.35);\n                const leftShoulder = new THREE.Mesh(shoulderGeo, crystalMat);\n                leftShoulder.position.set(-0.5, 1.4, 0);\n                group.add(leftShoulder);\n                const rightShoulder = new THREE.Mesh(shoulderGeo, crystalMat);\n                rightShoulder.position.set(0.5, 1.4, 0);\n                group.add(rightShoulder);\n                // Armor plates\n                const armorMat = new THREE.MeshLambertMaterial({ color: charDef.colors.armor || 0x6688CC });\n                const chestGeo = new THREE.BoxGeometry(0.75, 0.5, 0.1);\n                const chest = new THREE.Mesh(chestGeo, armorMat);\n                chest.position.set(0, 1.1, 0.28);\n                group.add(chest);\n            } else if (characterId === 'ninja' && charDef) {\n                // Long scarf\n                const scarfMat = new THREE.MeshLambertMaterial({ color: charDef.colors.scarf || 0x4A2A5A });\n                const scarfGeo = new THREE.BoxGeometry(0.15, 0.8, 0.1);\n                const scarf = new THREE.Mesh(scarfGeo, scarfMat);\n                scarf.position.set(0.15, 1.3, -0.3);\n                scarf.rotation.z = -0.2;\n                group.add(scarf);\n                const scarf2 = new THREE.Mesh(scarfGeo, scarfMat);\n                scarf2.position.set(-0.1, 1.2, -0.35);\n                scarf2.rotation.z = 0.3;\n                group.add(scarf2);\n            }\n            \n            // Head\n            const headGeo = new THREE.BoxGeometry(0.6, 0.6, 0.6);\n            const head = new THREE.Mesh(headGeo, skinMat);\n            head.position.y = 1.8;\n            head.castShadow = true;\n            group.add(head);\n            \n            // Character-specific head decorations\n            if (characterId === 'bomber' && charDef) {\n                // Fuse on head\n                const fuseMat = new THREE.MeshLambertMaterial({ color: 0x333333 });\n                const fuseGeo = new THREE.BoxGeometry(0.05, 0.25, 0.05);\n                const fuse = new THREE.Mesh(fuseGeo, fuseMat);\n                fuse.position.set(0, 2.2, 0);\n                group.add(fuse);\n                // Spark\n                const sparkMat = new THREE.MeshBasicMaterial({ color: 0xFF6600 });\n                const sparkGeo = new THREE.BoxGeometry(0.1, 0.1, 0.1);\n                const spark = new THREE.Mesh(sparkGeo, sparkMat);\n                spark.position.set(0, 2.35, 0);\n                group.add(spark);\n            } else if (characterId === 'bounty' && charDef) {\n                // Golden cowboy hat\n                const hatMat = new THREE.MeshLambertMaterial({ color: charDef.colors.hat || 0xFFD700 });\n                const brimGeo = new THREE.BoxGeometry(0.9, 0.08, 0.9);\n                const brim = new THREE.Mesh(brimGeo, hatMat);\n                brim.position.set(0, 2.05, 0);\n                group.add(brim);\n                const topGeo = new THREE.BoxGeometry(0.5, 0.3, 0.5);\n                const top = new THREE.Mesh(topGeo, hatMat);\n                top.position.set(0, 2.25, 0);\n                group.add(top);\n            } else if (characterId === 'miner' && charDef) {\n                // Yellow miner hat\n                const hatMat = new THREE.MeshLambertMaterial({ color: charDef.colors.hat || 0xFFCC00 });\n                const hatGeo = new THREE.BoxGeometry(0.65, 0.25, 0.65);\n                const hat = new THREE.Mesh(hatGeo, hatMat);\n                hat.position.set(0, 2.15, 0);\n                group.add(hat);\n                // Headlamp\n                const lampMat = new THREE.MeshBasicMaterial({ color: charDef.colors.lamp || 0xFFFF88 });\n                const lampGeo = new THREE.BoxGeometry(0.15, 0.12, 0.1);\n                const lamp = new THREE.Mesh(lampGeo, lampMat);\n                lamp.position.set(0, 2.1, 0.35);\n                group.add(lamp);\n            } else if (characterId === 'slider' && charDef) {\n                // Ice helmet\n                const iceMat = new THREE.MeshLambertMaterial({ color: charDef.colors.ice || 0xAAEEFF, transparent: true, opacity: 0.8 });\n                const helmetGeo = new THREE.BoxGeometry(0.7, 0.35, 0.7);\n                const helmet = new THREE.Mesh(helmetGeo, iceMat);\n                helmet.position.set(0, 2.1, 0);\n                group.add(helmet);\n            } else if (characterId === 'guardian' && charDef) {\n                // Wide helmet\n                const helmetMat = new THREE.MeshLambertMaterial({ color: charDef.colors.armor || 0x666677 });\n                const helmetGeo = new THREE.BoxGeometry(0.8, 0.35, 0.7);\n                const helmet = new THREE.Mesh(helmetGeo, helmetMat);\n                helmet.position.set(0, 2.1, 0);\n                group.add(helmet);\n                // Visor\n                const visorMat = new THREE.MeshLambertMaterial({ color: 0x333344 });\n                const visorGeo = new THREE.BoxGeometry(0.5, 0.15, 0.1);\n                const visor = new THREE.Mesh(visorGeo, visorMat);\n                visor.position.set(0, 1.9, 0.35);\n                group.add(visor);\n            } else if (characterId === 'reckless' && charDef) {\n                // Red glowing eyes\n                const eyeGlowMat = new THREE.MeshBasicMaterial({ color: 0xFF0000 });\n                const eyeGlowGeo = new THREE.BoxGeometry(0.12, 0.12, 0.1);\n                const leftGlow = new THREE.Mesh(eyeGlowGeo, eyeGlowMat);\n                leftGlow.position.set(-0.15, 1.85, 0.32);\n                group.add(leftGlow);\n                const rightGlow = new THREE.Mesh(eyeGlowGeo, eyeGlowMat);\n                rightGlow.position.set(0.15, 1.85, 0.32);\n                group.add(rightGlow);\n            } else if (characterId === 'knight' && charDef) {\n                // Knight helmet\n                const helmetMat = new THREE.MeshLambertMaterial({ color: charDef.colors.armor || 0x6688CC });\n                const helmetGeo = new THREE.BoxGeometry(0.7, 0.4, 0.7);\n                const helmet = new THREE.Mesh(helmetGeo, helmetMat);\n                helmet.position.set(0, 2.1, 0);\n                group.add(helmet);\n                // Plume\n                const plumeMat = new THREE.MeshLambertMaterial({ color: 0x2244AA });\n                const plumeGeo = new THREE.BoxGeometry(0.1, 0.4, 0.3);\n                const plume = new THREE.Mesh(plumeGeo, plumeMat);\n                plume.position.set(0, 2.4, -0.1);\n                group.add(plume);\n            } else if (characterId === 'ninja' && charDef) {\n                // Ninja mask (covers lower face)\n                const maskMat = new THREE.MeshLambertMaterial({ color: charDef.colors.shirt });\n                const maskGeo = new THREE.BoxGeometry(0.62, 0.25, 0.62);\n                const mask = new THREE.Mesh(maskGeo, maskMat);\n                mask.position.set(0, 1.65, 0);\n                group.add(mask);\n            }\n            \n            // Eyes (skip for ninja and some helmeted characters)\n            if (characterId !== 'ninja' && characterId !== 'guardian' && characterId !== 'reckless') {\n                const eyeGeo = new THREE.BoxGeometry(0.1, 0.1, 0.1);\n                const eyeMat = new THREE.MeshBasicMaterial({ color: characterId === 'bounty' ? 0xFFD700 : 0x222222 });\n                const leftEye = new THREE.Mesh(eyeGeo, eyeMat);\n                leftEye.position.set(-0.15, 1.85, 0.3);\n                group.add(leftEye);\n                const rightEye = new THREE.Mesh(eyeGeo, eyeMat);\n                rightEye.position.set(0.15, 1.85, 0.3);\n                group.add(rightEye);\n            } else if (characterId === 'ninja') {\n                // Ninja eyes (visible above mask)\n                const eyeGeo = new THREE.BoxGeometry(0.1, 0.08, 0.1);\n                const eyeMat = new THREE.MeshBasicMaterial({ color: 0x222222 });\n                const leftEye = new THREE.Mesh(eyeGeo, eyeMat);\n                leftEye.position.set(-0.15, 1.88, 0.3);\n                group.add(leftEye);\n                const rightEye = new THREE.Mesh(eyeGeo, eyeMat);\n                rightEye.position.set(0.15, 1.88, 0.3);\n                group.add(rightEye);\n            }\n            \n            const armGeo = new THREE.BoxGeometry(0.25, 0.7, 0.25);\n            \n            // Left Arm Pivot\n            const leftArmPivot = new THREE.Group();\n            leftArmPivot.position.set(-0.55 * bodyScale, 1.45, 0);\n            group.add(leftArmPivot);\n            \n            // Left Arm mesh\n            const leftArm = new THREE.Mesh(armGeo, skinMat);\n            leftArm.position.set(0, -0.35, 0);\n            leftArm.castShadow = true;\n            leftArmPivot.add(leftArm);\n            \n            // Right Arm Pivot\n            const rightArmPivot = new THREE.Group();\n            rightArmPivot.position.set(0.55 * bodyScale, 1.45, 0);\n            group.add(rightArmPivot);\n            \n            // Right Arm mesh\n            const rightArm = new THREE.Mesh(armGeo, skinMat);\n            rightArm.position.set(0, -0.35, 0);\n            rightArm.castShadow = true;\n            rightArmPivot.add(rightArm);\n            \n            // Weapon pivot\n            const weaponPivot = new THREE.Group();\n            weaponPivot.position.set(0, -0.7, 0.1);\n            rightArmPivot.add(weaponPivot);\n            \n            // Store references for animation\n            group.userData.leftArmPivot = leftArmPivot;\n            group.userData.rightArmPivot = rightArmPivot;\n            group.userData.weaponPivot = weaponPivot;\n            \n            // Legs with pivots\n            const legGeo = new THREE.BoxGeometry(0.3, 0.6, 0.3);\n            const legMat = new THREE.MeshLambertMaterial({ color: characterId === 'ninja' ? 0x1A0A2A : 0x3a3a5c });\n            \n            // Slider ice skates\n            if (characterId === 'slider' && charDef) {\n                const skateMat = new THREE.MeshLambertMaterial({ color: charDef.colors.ice || 0xAAEEFF });\n                const skateGeo = new THREE.BoxGeometry(0.35, 0.1, 0.5);\n                const leftSkate = new THREE.Mesh(skateGeo, skateMat);\n                leftSkate.position.set(-0.2, 0.05, 0);\n                group.add(leftSkate);\n                const rightSkate = new THREE.Mesh(skateGeo, skateMat);\n                rightSkate.position.set(0.2, 0.05, 0);\n                group.add(rightSkate);\n            }\n            \n            // Left leg pivot\n            const leftLegPivot = new THREE.Group();\n            leftLegPivot.position.set(-0.2, 0.6, 0);\n            group.add(leftLegPivot);\n            const leftLeg = new THREE.Mesh(legGeo, legMat);\n            leftLeg.position.set(0, -0.3, 0);\n            leftLeg.castShadow = true;\n            leftLegPivot.add(leftLeg);\n            \n            // Right leg pivot\n            const rightLegPivot = new THREE.Group();\n            rightLegPivot.position.set(0.2, 0.6, 0);\n            group.add(rightLegPivot);\n            const rightLeg = new THREE.Mesh(legGeo, legMat);\n            rightLeg.position.set(0, -0.3, 0);\n            rightLeg.castShadow = true;\n            rightLegPivot.add(rightLeg);\n            \n            // Store leg references\n            group.userData.leftLegPivot = leftLegPivot;\n            group.userData.rightLegPivot = rightLegPivot;\n            group.userData.walkCycle = 0;\n            group.userData.characterId = characterId;\n            \n            // Apply body scale\n            if (bodyScale !== 1.0) {\n                group.scale.setScalar(bodyScale);\n            }\n            \n            return group;\n        }\n\n        // === CREATE 3D WEAPON ===\n        function createWeapon(length, material) {\n            const group = new THREE.Group();\n            const METER_TO_3D = 0.5;\n            const pixLen = length * METER_TO_3D;\n            const baseWidth = 0.55 + Math.min(length / 50, 2.5);\n            const baseThick = 0.25 + Math.min(length / 120, 0.7);\n            \n            const handleMat = new THREE.MeshLambertMaterial({ color: 0x3a3a3a });\n            const darkMat = new THREE.MeshLambertMaterial({ color: 0x2a2a2a });\n            const spineMat = new THREE.MeshLambertMaterial({ \n                color: material.color.clone().multiplyScalar(0.7)\n            });\n            const edgeMat = new THREE.MeshLambertMaterial({ \n                color: material.color.clone().multiplyScalar(1.4),\n                emissive: material.emissive ? material.emissive.clone() : new THREE.Color(0x000000)\n            });\n            \n            // === \u5927\u5200 BLADE DESIGN ===\n            const bladeLen = pixLen * 0.72;\n            const tipLen = pixLen * 0.28;\n            \n            // Main blade body - wider at tip, narrower at handle (\u5927\u5200\u7279\u5f81)\n            // Use multiple segments for curved profile\n            const segments = 6;\n            const segLen = bladeLen / segments;\n            \n            for (let i = 0; i < segments; i++) {\n                // Width increases toward tip (\u5927\u5200\u7684\u5bbd\u5ea6\u53d8\u5316)\n                const t = i / segments;\n                const segWidth = baseWidth * (0.7 + t * 0.5);\n                const segThick = baseThick * (0.9 + t * 0.2);\n                \n                // Main segment\n                const segGeo = new THREE.BoxGeometry(segWidth, segThick, segLen * 1.1);\n                const seg = new THREE.Mesh(segGeo, material);\n                seg.position.z = 0.1 + segLen * (i + 0.5);\n                // Slight curve upward (\u5200\u8eab\u5f27\u5ea6)\n                seg.position.y = Math.pow(t, 2) * baseThick * 0.3;\n                seg.castShadow = true;\n                group.add(seg);\n                \n                // Spine ridge on top (\u5200\u810a - \u539a\u5b9e\u7684\u5200\u80cc)\n                const spineWidth = segWidth * 0.35;\n                const spineHeight = segThick * 0.8;\n                const spineGeo = new THREE.BoxGeometry(spineWidth, spineHeight, segLen * 1.05);\n                const spine = new THREE.Mesh(spineGeo, spineMat);\n                spine.position.set(0, segThick * 0.5 + spineHeight * 0.4 + Math.pow(t, 2) * baseThick * 0.3, 0.1 + segLen * (i + 0.5));\n                spine.castShadow = true;\n                group.add(spine);\n                \n                // Sharp edge on bottom (\u950b\u5229\u7684\u5200\u5203)\n                const edgeWidth = segWidth * 1.05;\n                const edgeHeight = segThick * 0.2;\n                const edgeGeo = new THREE.BoxGeometry(edgeWidth, edgeHeight, segLen * 1.05);\n                const edge = new THREE.Mesh(edgeGeo, edgeMat);\n                edge.position.set(0, -segThick * 0.45 + Math.pow(t, 2) * baseThick * 0.3, 0.1 + segLen * (i + 0.5));\n                group.add(edge);\n            }\n            \n            // Blade tip section - characteristic \u5927\u5200 curved tip\n            const tipWidth = baseWidth * 1.15;\n            const tipThick = baseThick * 1.0;\n            const tipStartZ = bladeLen + 0.1;\n            \n            // Tip main body (\u5200\u5c16\u4e3b\u4f53)\n            const tipMainGeo = new THREE.BoxGeometry(tipWidth * 0.9, tipThick, tipLen * 0.5);\n            const tipMain = new THREE.Mesh(tipMainGeo, material);\n            tipMain.position.set(0, baseThick * 0.35, tipStartZ + tipLen * 0.25);\n            tipMain.castShadow = true;\n            group.add(tipMain);\n            \n            // Tip spine\n            const tipSpineGeo = new THREE.BoxGeometry(tipWidth * 0.3, tipThick * 0.7, tipLen * 0.45);\n            const tipSpine = new THREE.Mesh(tipSpineGeo, spineMat);\n            tipSpine.position.set(0, tipThick * 0.85 + baseThick * 0.35, tipStartZ + tipLen * 0.22);\n            group.add(tipSpine);\n            \n            // Tip edge\n            const tipEdgeGeo = new THREE.BoxGeometry(tipWidth * 0.95, tipThick * 0.18, tipLen * 0.5);\n            const tipEdge = new THREE.Mesh(tipEdgeGeo, edgeMat);\n            tipEdge.position.set(0, -tipThick * 0.35 + baseThick * 0.35, tipStartZ + tipLen * 0.25);\n            group.add(tipEdge);\n            \n            // Curved tip point - \u5927\u5200\u7684\u5f2f\u66f2\u5c16\u7aef (clip point style)\n            const pointGeo = new THREE.BoxGeometry(tipWidth * 0.55, tipThick * 0.75, tipLen * 0.35);\n            const point = new THREE.Mesh(pointGeo, material);\n            point.position.set(0, baseThick * 0.5, tipStartZ + tipLen * 0.65);\n            point.rotation.x = -0.15; // Slight upward curve\n            point.castShadow = true;\n            group.add(point);\n            \n            // Final sharp point (\u6700\u7ec8\u5c16\u7aef)\n            const sharpGeo = new THREE.BoxGeometry(tipWidth * 0.25, tipThick * 0.45, tipLen * 0.2);\n            const sharp = new THREE.Mesh(sharpGeo, edgeMat);\n            sharp.position.set(0, baseThick * 0.6, tipStartZ + tipLen * 0.88);\n            sharp.rotation.x = -0.2;\n            group.add(sharp);\n            \n            // Back curve detail (\u5200\u80cc\u5f27\u7ebf)\n            if (length >= 8) {\n                const backCurveGeo = new THREE.BoxGeometry(tipWidth * 0.2, tipThick * 0.4, tipLen * 0.6);\n                const backCurve = new THREE.Mesh(backCurveGeo, spineMat);\n                backCurve.position.set(0, tipThick * 0.9 + baseThick * 0.35, tipStartZ + tipLen * 0.4);\n                backCurve.rotation.x = 0.2;\n                group.add(backCurve);\n            }\n            \n            // Blood groove / fuller (\u8840\u69fd\u88c5\u9970)\n            if (length >= 15) {\n                const fullerMat = new THREE.MeshLambertMaterial({ color: material.color.clone().multiplyScalar(0.5) });\n                const fullerGeo = new THREE.BoxGeometry(baseWidth * 0.15, baseThick * 0.08, bladeLen * 0.7);\n                const fuller = new THREE.Mesh(fullerGeo, fullerMat);\n                fuller.position.set(0, baseThick * 0.1, bladeLen * 0.4);\n                group.add(fuller);\n            }\n            \n            // === HANDLE ===\n            const handleLen = 0.5 + Math.min(length / 80, 0.4);\n            const handleThick = 0.12;\n            \n            // Handle core\n            const handleGeo = new THREE.BoxGeometry(handleThick, handleThick, handleLen);\n            const handle = new THREE.Mesh(handleGeo, handleMat);\n            handle.position.z = -handleLen / 2;\n            group.add(handle);\n            \n            // Handle wrapping (\u63e1\u628a\u7f20\u7ed5)\n            const wrapMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 });\n            const wrapCount = Math.floor(handleLen / 0.1);\n            for (let i = 0; i < wrapCount; i++) {\n                const wrapGeo = new THREE.BoxGeometry(0.15, 0.15, 0.05);\n                const wrap = new THREE.Mesh(wrapGeo, wrapMat);\n                wrap.position.z = -0.06 - i * 0.1;\n                wrap.rotation.z = i * 0.3; // Spiral pattern\n                group.add(wrap);\n            }\n            \n            // Guard (\u62a4\u624b) - circular style like Chinese dao\n            const guardRadius = 0.22;\n            const guardGeo = new THREE.BoxGeometry(guardRadius * 2, guardRadius * 2, 0.08);\n            const guard = new THREE.Mesh(guardGeo, darkMat);\n            guard.position.z = 0.03;\n            group.add(guard);\n            \n            // Guard decoration\n            const guardDecorGeo = new THREE.BoxGeometry(guardRadius * 1.6, guardRadius * 1.6, 0.1);\n            const guardDecor = new THREE.Mesh(guardDecorGeo, handleMat);\n            guardDecor.position.z = 0.03;\n            guardDecor.rotation.z = Math.PI / 4;\n            group.add(guardDecor);\n            \n            // Guard ring detail\n            const ringGeo = new THREE.BoxGeometry(guardRadius * 2.2, guardRadius * 0.15, 0.06);\n            const ringH = new THREE.Mesh(ringGeo, darkMat);\n            ringH.position.z = 0.06;\n            group.add(ringH);\n            const ringV = new THREE.Mesh(ringGeo, darkMat);\n            ringV.position.z = 0.06;\n            ringV.rotation.z = Math.PI / 2;\n            group.add(ringV);\n            \n            // Pommel (\u5200\u67c4\u5c3e\u7aef) - decorative end cap\n            const pommelGeo = new THREE.BoxGeometry(0.18, 0.18, 0.1);\n            const pommel = new THREE.Mesh(pommelGeo, darkMat);\n            pommel.position.z = -handleLen - 0.05;\n            group.add(pommel);\n            \n            // Pommel ring\n            const pommelRingGeo = new THREE.BoxGeometry(0.22, 0.22, 0.03);\n            const pommelRing = new THREE.Mesh(pommelRingGeo, handleMat);\n            pommelRing.position.z = -handleLen - 0.08;\n            group.add(pommelRing);\n            \n            // Add glow effect for high-tier weapons\n            if (length >= 50) {\n                const glowIntensity = Math.min((length - 50) / 50, 1);\n                const glowColor = length >= 75 ? 0x00FFFF : 0xFFD700;\n                const glowMat = new THREE.MeshBasicMaterial({ \n                    color: glowColor, \n                    transparent: true, \n                    opacity: 0.12 + glowIntensity * 0.12 \n                });\n                const glowGeo = new THREE.BoxGeometry(baseWidth * 1.8, baseThick * 2.5, pixLen * 0.95);\n                const glow = new THREE.Mesh(glowGeo, glowMat);\n                glow.position.z = pixLen / 2;\n                group.add(glow);\n            }\n            \n            return group;\n        }\n\n        function getWeaponMaterial(length) {\n            if (length >= 75) return materials.weaponDiamond;\n            if (length >= 50) return materials.weaponGold;\n            if (length >= 10) return materials.weaponStone;\n            return materials.weaponWood;\n        }\n\n        // === CREATE 3D BLOCK OBJECT ===\n        function createBlockMesh(type) {\n            const def = OBJ_DEFS[type];\n            const group = new THREE.Group();\n            \n            if (def.model === 'tree_small') {\n                // Small tree\n                const trunkGeo = new THREE.BoxGeometry(0.3, 1.0, 0.3);\n                const trunkMat = new THREE.MeshLambertMaterial({ color: 0x6B4423 });\n                const trunk = new THREE.Mesh(trunkGeo, trunkMat);\n                trunk.position.y = 0.5;\n                trunk.castShadow = true;\n                group.add(trunk);\n                \n                const leavesGeo = new THREE.BoxGeometry(1.0, 0.9, 1.0);\n                const leavesMat = new THREE.MeshLambertMaterial({ color: def.color });\n                const leaves = new THREE.Mesh(leavesGeo, leavesMat);\n                leaves.position.y = 1.3;\n                leaves.castShadow = true;\n                group.add(leaves);\n                \n            } else if (def.model === 'tree') {\n                // Medium tree: trunk + leaf crown\n                const trunkGeo = new THREE.BoxGeometry(0.5, 1.8, 0.5);\n                const trunkMat = new THREE.MeshLambertMaterial({ color: 0x6B4423 });\n                const trunk = new THREE.Mesh(trunkGeo, trunkMat);\n                trunk.position.y = 0.9;\n                trunk.castShadow = true;\n                group.add(trunk);\n                \n                const leavesGeo = new THREE.BoxGeometry(1.6, 1.4, 1.6);\n                const leavesMat = new THREE.MeshLambertMaterial({ color: def.color });\n                const leaves = new THREE.Mesh(leavesGeo, leavesMat);\n                leaves.position.y = 2.3;\n                leaves.castShadow = true;\n                group.add(leaves);\n                \n                // Top leaves\n                const topLeavesGeo = new THREE.BoxGeometry(1.0, 0.6, 1.0);\n                const topLeaves = new THREE.Mesh(topLeavesGeo, leavesMat);\n                topLeaves.position.y = 3.2;\n                topLeaves.castShadow = true;\n                group.add(topLeaves);\n                \n            } else if (def.model === 'tree_large') {\n                // Large tree\n                const trunkGeo = new THREE.BoxGeometry(0.8, 3.0, 0.8);\n                const trunkMat = new THREE.MeshLambertMaterial({ color: 0x6B4423 });\n                const trunk = new THREE.Mesh(trunkGeo, trunkMat);\n                trunk.position.y = 1.5;\n                trunk.castShadow = true;\n                group.add(trunk);\n                \n                const leavesGeo = new THREE.BoxGeometry(2.4, 2.0, 2.4);\n                const leavesMat = new THREE.MeshLambertMaterial({ color: def.color });\n                const leaves = new THREE.Mesh(leavesGeo, leavesMat);\n                leaves.position.y = 3.5;\n                leaves.castShadow = true;\n                group.add(leaves);\n                \n                // Middle leaves\n                const midLeavesGeo = new THREE.BoxGeometry(2.0, 1.2, 2.0);\n                const midLeaves = new THREE.Mesh(midLeavesGeo, leavesMat);\n                midLeaves.position.y = 4.8;\n                midLeaves.castShadow = true;\n                group.add(midLeaves);\n                \n                // Top leaves\n                const topLeavesGeo = new THREE.BoxGeometry(1.2, 0.8, 1.2);\n                const topLeaves = new THREE.Mesh(topLeavesGeo, leavesMat);\n                topLeaves.position.y = 5.6;\n                topLeaves.castShadow = true;\n                group.add(topLeaves);\n                \n            } else if (def.model === 'mushroom_small') {\n                // Small mushroom\n                const stemGeo = new THREE.BoxGeometry(0.1, 0.2, 0.1);\n                const stemMat = new THREE.MeshLambertMaterial({ color: 0xF5F5DC });\n                const stem = new THREE.Mesh(stemGeo, stemMat);\n                stem.position.y = 0.1;\n                stem.castShadow = true;\n                group.add(stem);\n                \n                const capGeo = new THREE.BoxGeometry(0.25, 0.15, 0.25);\n                const capMat = new THREE.MeshLambertMaterial({ color: def.color });\n                const cap = new THREE.Mesh(capGeo, capMat);\n                cap.position.y = 0.25;\n                cap.castShadow = true;\n                group.add(cap);\n                \n            } else if (def.model === 'mushroom') {\n                // Medium Mushroom: stem + cap\n                const stemGeo = new THREE.BoxGeometry(0.15, 0.3, 0.15);\n                const stemMat = new THREE.MeshLambertMaterial({ color: 0xF5F5DC });\n                const stem = new THREE.Mesh(stemGeo, stemMat);\n                stem.position.y = 0.15;\n                stem.castShadow = true;\n                group.add(stem);\n                \n                const capGeo = new THREE.BoxGeometry(0.4, 0.2, 0.4);\n                const capMat = new THREE.MeshLambertMaterial({ color: def.color });\n                const cap = new THREE.Mesh(capGeo, capMat);\n                cap.position.y = 0.35;\n                cap.castShadow = true;\n                group.add(cap);\n                \n                // White spots on cap\n                const spotGeo = new THREE.BoxGeometry(0.08, 0.06, 0.08);\n                const spotMat = new THREE.MeshLambertMaterial({ color: 0xFFFFFF });\n                const spots = [[-0.1, 0.46, 0.1], [0.12, 0.46, -0.05], [-0.05, 0.46, -0.12]];\n                spots.forEach(pos => {\n                    const spot = new THREE.Mesh(spotGeo, spotMat);\n                    spot.position.set(...pos);\n                    group.add(spot);\n                });\n                \n            } else if (def.model === 'mushroom_large') {\n                // Large Mushroom\n                const stemGeo = new THREE.BoxGeometry(0.2, 0.4, 0.2);\n                const stemMat = new THREE.MeshLambertMaterial({ color: 0xF5F5DC });\n                const stem = new THREE.Mesh(stemGeo, stemMat);\n                stem.position.y = 0.2;\n                stem.castShadow = true;\n                group.add(stem);\n                \n                const capGeo = new THREE.BoxGeometry(0.6, 0.25, 0.6);\n                const capMat = new THREE.MeshLambertMaterial({ color: def.color });\n                const cap = new THREE.Mesh(capGeo, capMat);\n                cap.position.y = 0.45;\n                cap.castShadow = true;\n                group.add(cap);\n                \n                // White spots on cap\n                const spotGeo = new THREE.BoxGeometry(0.1, 0.08, 0.1);\n                const spotMat = new THREE.MeshLambertMaterial({ color: 0xFFFFFF });\n                const spots = [[-0.15, 0.56, 0.15], [0.15, 0.56, -0.1], [-0.1, 0.56, -0.15], [0.1, 0.56, 0.15]];\n                spots.forEach(pos => {\n                    const spot = new THREE.Mesh(spotGeo, spotMat);\n                    spot.position.set(...pos);\n                    group.add(spot);\n                });\n                \n            } else if (def.model === 'flower') {\n                // Flower: stem + petals\n                const stemGeo = new THREE.BoxGeometry(0.1, 0.5, 0.1);\n                const stemMat = new THREE.MeshLambertMaterial({ color: 0x228B22 });\n                const stem = new THREE.Mesh(stemGeo, stemMat);\n                stem.position.y = 0.25;\n                stem.castShadow = true;\n                group.add(stem);\n                \n                // Flower center\n                const centerGeo = new THREE.BoxGeometry(0.2, 0.15, 0.2);\n                const centerMat = new THREE.MeshLambertMaterial({ color: 0xFFD700 });\n                const center = new THREE.Mesh(centerGeo, centerMat);\n                center.position.y = 0.55;\n                group.add(center);\n                \n                // Petals\n                const petalGeo = new THREE.BoxGeometry(0.18, 0.12, 0.18);\n                const petalMat = new THREE.MeshLambertMaterial({ color: def.color });\n                const petalPositions = [[0.2, 0.55, 0], [-0.2, 0.55, 0], [0, 0.55, 0.2], [0, 0.55, -0.2]];\n                petalPositions.forEach(pos => {\n                    const petal = new THREE.Mesh(petalGeo, petalMat);\n                    petal.position.set(...pos);\n                    group.add(petal);\n                });\n                \n            } else if (type === 'grass') {\n                // Grass tuft - multiple grass blades\n                const bladeMat = new THREE.MeshLambertMaterial({ color: 0x6AAA40 });\n                const bladeDarkMat = new THREE.MeshLambertMaterial({ color: 0x4A8A30 });\n                for (let i = 0; i < 5; i++) {\n                    const bladeGeo = new THREE.BoxGeometry(0.08, 0.4 + Math.random() * 0.3, 0.08);\n                    const blade = new THREE.Mesh(bladeGeo, i % 2 === 0 ? bladeMat : bladeDarkMat);\n                    blade.position.set((Math.random() - 0.5) * 0.4, 0.25, (Math.random() - 0.5) * 0.4);\n                    blade.rotation.x = (Math.random() - 0.5) * 0.3;\n                    blade.rotation.z = (Math.random() - 0.5) * 0.3;\n                    blade.castShadow = true;\n                    group.add(blade);\n                }\n                // Dirt base\n                const baseGeo = new THREE.BoxGeometry(0.5, 0.15, 0.5);\n                const baseMat = new THREE.MeshLambertMaterial({ color: 0x8B5A2B });\n                const base = new THREE.Mesh(baseGeo, baseMat);\n                base.position.y = 0.075;\n                group.add(base);\n                \n            } else if (type.startsWith('crate')) {\n                // Wooden crate with plank lines\n                const size = def.size;\n                const crateMat = new THREE.MeshLambertMaterial({ color: 0xA08040 });\n                const darkMat = new THREE.MeshLambertMaterial({ color: 0x705020 });\n                \n                // Main crate body\n                const bodyGeo = new THREE.BoxGeometry(size * 0.95, size * 0.95, size * 0.95);\n                const body = new THREE.Mesh(bodyGeo, crateMat);\n                body.position.y = size / 2;\n                body.castShadow = true;\n                group.add(body);\n                \n                // Horizontal plank lines\n                const plankGeo = new THREE.BoxGeometry(size, 0.06, size * 1.01);\n                for (let i = 0; i < 3; i++) {\n                    const plank = new THREE.Mesh(plankGeo, darkMat);\n                    plank.position.y = size * 0.2 + i * size * 0.3;\n                    group.add(plank);\n                }\n                // Vertical edge planks\n                const edgeGeo = new THREE.BoxGeometry(0.08, size, 0.08);\n                const corners = [[-1,-1], [1,-1], [-1,1], [1,1]];\n                corners.forEach(([cx, cz]) => {\n                    const edge = new THREE.Mesh(edgeGeo, darkMat);\n                    edge.position.set(cx * size * 0.45, size / 2, cz * size * 0.45);\n                    group.add(edge);\n                });\n                \n            } else if (type.startsWith('stone')) {\n                // Beautiful rocky stone with layered, natural look\n                const size = def.size;\n                \n                // Rich color palette for natural stone\n                const baseGray = type === 'stone_huge' ? 0x5A5A5A : 0x707070;\n                const stoneMat = new THREE.MeshLambertMaterial({ color: baseGray });\n                const darkStoneMat = new THREE.MeshLambertMaterial({ color: 0x4A4A4A });\n                const lightStoneMat = new THREE.MeshLambertMaterial({ color: 0x8A8A8A });\n                const accentMat = new THREE.MeshLambertMaterial({ color: 0x9A8A7A }); // Warm accent\n                const mossMat = new THREE.MeshLambertMaterial({ color: 0x4A6A3A }); // Green moss\n                const lichMat = new THREE.MeshLambertMaterial({ color: 0x8A9A7A }); // Lichen patches\n                \n                // Main rock body - more organic shape with multiple overlapping boxes\n                const mainGeo = new THREE.BoxGeometry(size * 0.9, size * 0.6, size * 0.85);\n                const main = new THREE.Mesh(mainGeo, stoneMat);\n                main.position.y = size * 0.3;\n                main.rotation.y = Math.random() * 0.8;\n                main.castShadow = true;\n                group.add(main);\n                \n                // Secondary layer for more organic shape\n                const layer2Geo = new THREE.BoxGeometry(size * 0.75, size * 0.55, size * 0.7);\n                const layer2 = new THREE.Mesh(layer2Geo, stoneMat);\n                layer2.position.set(size * 0.1, size * 0.28, size * 0.05);\n                layer2.rotation.y = 0.4;\n                layer2.castShadow = true;\n                group.add(layer2);\n                \n                // Top weathered cap\n                const capGeo = new THREE.BoxGeometry(size * 0.7, size * 0.25, size * 0.65);\n                const cap = new THREE.Mesh(capGeo, lightStoneMat);\n                cap.position.set(-size * 0.05, size * 0.58, 0);\n                cap.rotation.y = -0.2;\n                cap.castShadow = true;\n                group.add(cap);\n                \n                // Protruding rock chunks for rugged look\n                const chunk1Geo = new THREE.BoxGeometry(size * 0.4, size * 0.35, size * 0.5);\n                const chunk1 = new THREE.Mesh(chunk1Geo, darkStoneMat);\n                chunk1.position.set(size * 0.35, size * 0.2, size * 0.25);\n                chunk1.rotation.set(0.1, 0.6, 0.1);\n                chunk1.castShadow = true;\n                group.add(chunk1);\n                \n                const chunk2Geo = new THREE.BoxGeometry(size * 0.35, size * 0.3, size * 0.4);\n                const chunk2 = new THREE.Mesh(chunk2Geo, accentMat);\n                chunk2.position.set(-size * 0.32, size * 0.35, -size * 0.2);\n                chunk2.rotation.y = -0.4;\n                chunk2.castShadow = true;\n                group.add(chunk2);\n                \n                // Small detail rocks at base\n                for (let i = 0; i < 4; i++) {\n                    const detailSize = 0.12 + Math.random() * 0.15;\n                    const detailGeo = new THREE.BoxGeometry(size * detailSize, size * detailSize * 0.7, size * detailSize);\n                    const detailMat = i % 2 === 0 ? darkStoneMat : lightStoneMat;\n                    const detail = new THREE.Mesh(detailGeo, detailMat);\n                    const angle = (i / 4) * Math.PI * 2 + Math.random() * 0.5;\n                    detail.position.set(\n                        Math.cos(angle) * size * 0.5,\n                        size * detailSize * 0.35,\n                        Math.sin(angle) * size * 0.45\n                    );\n                    detail.rotation.y = Math.random() * Math.PI;\n                    detail.castShadow = true;\n                    group.add(detail);\n                }\n                \n                // Crack/crevice details (dark lines)\n                const crackMat = new THREE.MeshLambertMaterial({ color: 0x2A2A2A });\n                for (let i = 0; i < 3; i++) {\n                    const crackGeo = new THREE.BoxGeometry(size * 0.03, size * 0.02, size * (0.2 + Math.random() * 0.3));\n                    const crack = new THREE.Mesh(crackGeo, crackMat);\n                    crack.position.set(\n                        (Math.random() - 0.5) * size * 0.5,\n                        size * 0.62 + Math.random() * 0.1,\n                        (Math.random() - 0.5) * size * 0.4\n                    );\n                    crack.rotation.y = Math.random() * Math.PI;\n                    group.add(crack);\n                }\n                \n                // Moss patches (green accents on shaded side)\n                if (size > 0.8) {\n                    for (let i = 0; i < 2; i++) {\n                        const mossGeo = new THREE.BoxGeometry(size * 0.15, size * 0.04, size * 0.2);\n                        const moss = new THREE.Mesh(mossGeo, mossMat);\n                        moss.position.set(\n                            -size * 0.3 + i * size * 0.15,\n                            size * 0.1 + Math.random() * size * 0.2,\n                            size * 0.35\n                        );\n                        moss.rotation.set(0.3, Math.random() * 0.5, 0);\n                        group.add(moss);\n                    }\n                }\n                \n                // Lichen/mineral patches on top (yellowish-gray spots)\n                if (size > 1.0) {\n                    for (let i = 0; i < 3; i++) {\n                        const lichGeo = new THREE.BoxGeometry(size * 0.1, size * 0.02, size * 0.12);\n                        const lich = new THREE.Mesh(lichGeo, lichMat);\n                        lich.position.set(\n                            (Math.random() - 0.5) * size * 0.4,\n                            size * 0.68,\n                            (Math.random() - 0.5) * size * 0.35\n                        );\n                        lich.rotation.y = Math.random() * Math.PI;\n                        group.add(lich);\n                    }\n                }\n                \n                // Highlight edges (lighter stone on exposed edges)\n                const edgeGeo = new THREE.BoxGeometry(size * 0.08, size * 0.5, size * 0.08);\n                const edgeMat = new THREE.MeshLambertMaterial({ color: 0x9A9A9A });\n                const edgePositions = [\n                    [size * 0.42, size * 0.35, size * 0.38],\n                    [-size * 0.4, size * 0.32, -size * 0.35],\n                    [size * 0.38, size * 0.3, -size * 0.4]\n                ];\n                edgePositions.forEach(pos => {\n                    const edge = new THREE.Mesh(edgeGeo, edgeMat);\n                    edge.position.set(...pos);\n                    edge.rotation.set(Math.random() * 0.2, Math.random() * 0.3, Math.random() * 0.2);\n                    group.add(edge);\n                });\n                \n            } else if (type === 'stump') {\n                // Tree stump with rings\n                const stumpMat = new THREE.MeshLambertMaterial({ color: 0x6B5344 });\n                const ringMat = new THREE.MeshLambertMaterial({ color: 0x8B7355 });\n                const darkRingMat = new THREE.MeshLambertMaterial({ color: 0x4A3728 });\n                \n                // Main stump body (cylindrical shape using boxes)\n                const bodyGeo = new THREE.BoxGeometry(1.0, 0.8, 1.0);\n                const body = new THREE.Mesh(bodyGeo, stumpMat);\n                body.position.y = 0.4;\n                body.castShadow = true;\n                group.add(body);\n                \n                // Rounded edges\n                const edgeGeo = new THREE.BoxGeometry(0.85, 0.82, 0.85);\n                const edge = new THREE.Mesh(edgeGeo, stumpMat);\n                edge.position.y = 0.4;\n                edge.rotation.y = Math.PI / 4;\n                edge.castShadow = true;\n                group.add(edge);\n                \n                // Top surface with rings\n                const topGeo = new THREE.BoxGeometry(0.95, 0.08, 0.95);\n                const top = new THREE.Mesh(topGeo, ringMat);\n                top.position.y = 0.82;\n                group.add(top);\n                \n                // Ring pattern on top\n                const ring1Geo = new THREE.BoxGeometry(0.6, 0.09, 0.6);\n                const ring1 = new THREE.Mesh(ring1Geo, darkRingMat);\n                ring1.position.y = 0.83;\n                group.add(ring1);\n                \n                const ring2Geo = new THREE.BoxGeometry(0.3, 0.1, 0.3);\n                const ring2 = new THREE.Mesh(ring2Geo, ringMat);\n                ring2.position.y = 0.84;\n                group.add(ring2);\n                \n                // Bark texture (vertical lines)\n                const barkGeo = new THREE.BoxGeometry(0.08, 0.7, 0.08);\n                for (let i = 0; i < 6; i++) {\n                    const angle = (i / 6) * Math.PI * 2;\n                    const bark = new THREE.Mesh(barkGeo, darkRingMat);\n                    bark.position.set(Math.cos(angle) * 0.48, 0.4, Math.sin(angle) * 0.48);\n                    group.add(bark);\n                }\n                \n            } else if (type === 'gold') {\n                // Gold treasure chest\n                const size = def.size;\n                const woodMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 });\n                const darkWoodMat = new THREE.MeshLambertMaterial({ color: 0x5D2E0C });\n                const goldMat = new THREE.MeshLambertMaterial({ color: 0xFFD700, emissive: 0x332200 });\n                const goldBrightMat = new THREE.MeshLambertMaterial({ color: 0xFFE55C, emissive: 0x443300 });\n                \n                // Chest body\n                const bodyGeo = new THREE.BoxGeometry(size * 1.0, size * 0.6, size * 0.7);\n                const body = new THREE.Mesh(bodyGeo, woodMat);\n                body.position.y = size * 0.3;\n                body.castShadow = true;\n                group.add(body);\n                \n                // Chest lid (slightly open)\n                const lidGeo = new THREE.BoxGeometry(size * 1.02, size * 0.25, size * 0.72);\n                const lid = new THREE.Mesh(lidGeo, darkWoodMat);\n                lid.position.set(0, size * 0.7, -size * 0.1);\n                lid.rotation.x = -0.3; // Slightly open\n                lid.castShadow = true;\n                group.add(lid);\n                \n                // Gold metal bands on body\n                const bandGeo = new THREE.BoxGeometry(size * 1.05, size * 0.08, size * 0.75);\n                const band1 = new THREE.Mesh(bandGeo, goldMat);\n                band1.position.y = size * 0.15;\n                group.add(band1);\n                const band2 = new THREE.Mesh(bandGeo, goldMat);\n                band2.position.y = size * 0.45;\n                group.add(band2);\n                \n                // Gold corner edges\n                const cornerGeo = new THREE.BoxGeometry(size * 0.1, size * 0.65, size * 0.1);\n                const corners = [[-0.45, -0.32], [0.45, -0.32], [-0.45, 0.32], [0.45, 0.32]];\n                corners.forEach(([cx, cz]) => {\n                    const corner = new THREE.Mesh(cornerGeo, goldMat);\n                    corner.position.set(cx * size, size * 0.32, cz * size);\n                    group.add(corner);\n                });\n                \n                // Gold lock on front\n                const lockGeo = new THREE.BoxGeometry(size * 0.2, size * 0.25, size * 0.08);\n                const lock = new THREE.Mesh(lockGeo, goldBrightMat);\n                lock.position.set(0, size * 0.4, size * 0.38);\n                group.add(lock);\n                \n                // Gold coins spilling out\n                const coinGeo = new THREE.BoxGeometry(0.15, 0.04, 0.15);\n                for (let i = 0; i < 6; i++) {\n                    const coin = new THREE.Mesh(coinGeo, goldBrightMat);\n                    coin.position.set(\n                        (Math.random() - 0.5) * size * 0.6,\n                        size * 0.62 + i * 0.03,\n                        size * 0.2 + Math.random() * 0.2\n                    );\n                    coin.rotation.set(Math.random() * 0.5, Math.random() * Math.PI, Math.random() * 0.3);\n                    group.add(coin);\n                }\n                \n                // Glowing effect (golden aura)\n                const glowGeo = new THREE.BoxGeometry(size * 0.5, size * 0.3, size * 0.3);\n                const glowMat = new THREE.MeshBasicMaterial({ color: 0xFFD700, transparent: true, opacity: 0.3 });\n                const glow = new THREE.Mesh(glowGeo, glowMat);\n                glow.position.y = size * 0.8;\n                group.add(glow);\n                \n            } else if (type === 'diamond') {\n                // Diamond treasure chest (fancier, more valuable)\n                const size = def.size;\n                const woodMat = new THREE.MeshLambertMaterial({ color: 0x4A3728 });\n                const darkWoodMat = new THREE.MeshLambertMaterial({ color: 0x2D1F15 });\n                const diamondMat = new THREE.MeshLambertMaterial({ color: 0x00CED1, emissive: 0x004444 });\n                const diamondBrightMat = new THREE.MeshLambertMaterial({ color: 0x40E0D0, emissive: 0x006666 });\n                const silverMat = new THREE.MeshLambertMaterial({ color: 0xC0C0C0, emissive: 0x222222 });\n                \n                // Chest body (darker, more ornate)\n                const bodyGeo = new THREE.BoxGeometry(size * 1.1, size * 0.65, size * 0.75);\n                const body = new THREE.Mesh(bodyGeo, woodMat);\n                body.position.y = size * 0.32;\n                body.castShadow = true;\n                group.add(body);\n                \n                // Chest lid (more open to show treasure)\n                const lidGeo = new THREE.BoxGeometry(size * 1.12, size * 0.28, size * 0.77);\n                const lid = new THREE.Mesh(lidGeo, darkWoodMat);\n                lid.position.set(0, size * 0.75, -size * 0.15);\n                lid.rotation.x = -0.5; // More open\n                lid.castShadow = true;\n                group.add(lid);\n                \n                // Silver/diamond metal bands\n                const bandGeo = new THREE.BoxGeometry(size * 1.15, size * 0.1, size * 0.8);\n                const band1 = new THREE.Mesh(bandGeo, silverMat);\n                band1.position.y = size * 0.15;\n                group.add(band1);\n                const band2 = new THREE.Mesh(bandGeo, silverMat);\n                band2.position.y = size * 0.5;\n                group.add(band2);\n                \n                // Diamond-encrusted corners\n                const cornerGeo = new THREE.BoxGeometry(size * 0.12, size * 0.7, size * 0.12);\n                const corners = [[-0.5, -0.35], [0.5, -0.35], [-0.5, 0.35], [0.5, 0.35]];\n                corners.forEach(([cx, cz]) => {\n                    const corner = new THREE.Mesh(cornerGeo, diamondMat);\n                    corner.position.set(cx * size, size * 0.35, cz * size);\n                    group.add(corner);\n                });\n                \n                // Diamond lock on front\n                const lockBaseGeo = new THREE.BoxGeometry(size * 0.25, size * 0.3, size * 0.1);\n                const lockBase = new THREE.Mesh(lockBaseGeo, silverMat);\n                lockBase.position.set(0, size * 0.42, size * 0.4);\n                group.add(lockBase);\n                const lockGemGeo = new THREE.BoxGeometry(size * 0.12, size * 0.12, size * 0.06);\n                const lockGem = new THREE.Mesh(lockGemGeo, diamondBrightMat);\n                lockGem.position.set(0, size * 0.42, size * 0.46);\n                group.add(lockGem);\n                \n                // Diamonds spilling out\n                for (let i = 0; i < 5; i++) {\n                    const gemGeo = new THREE.BoxGeometry(0.12, 0.18, 0.12);\n                    const gem = new THREE.Mesh(gemGeo, diamondBrightMat);\n                    gem.position.set(\n                        (Math.random() - 0.5) * size * 0.5,\n                        size * 0.68 + i * 0.04,\n                        size * 0.25 + Math.random() * 0.15\n                    );\n                    gem.rotation.set(Math.random() * 0.5, Math.random() * Math.PI, Math.random() * 0.5);\n                    group.add(gem);\n                }\n                \n                // Large central diamond\n                const bigDiamondGeo = new THREE.BoxGeometry(0.2, 0.28, 0.2);\n                const bigDiamond = new THREE.Mesh(bigDiamondGeo, diamondBrightMat);\n                bigDiamond.position.set(0, size * 0.85, size * 0.15);\n                bigDiamond.rotation.y = Math.PI / 4;\n                group.add(bigDiamond);\n                \n                // Glowing effect (cyan aura)\n                const glowGeo = new THREE.BoxGeometry(size * 0.6, size * 0.4, size * 0.4);\n                const glowMat = new THREE.MeshBasicMaterial({ color: 0x00FFFF, transparent: true, opacity: 0.25 });\n                const glow = new THREE.Mesh(glowGeo, glowMat);\n                glow.position.y = size * 0.9;\n                group.add(glow);\n                \n            } else {\n                // Default block (fallback)\n                const geo = new THREE.BoxGeometry(def.size, def.size, def.size);\n                const mat = new THREE.MeshLambertMaterial({ \n                    color: def.color,\n                    emissive: def.glow ? 0x111111 : 0x000000\n                });\n                const mesh = new THREE.Mesh(geo, mat);\n                mesh.castShadow = true;\n                mesh.receiveShadow = true;\n                mesh.position.y = def.size / 2;\n                group.add(mesh);\n            }\n            \n            return group;\n        }\n\n        // === CREATE SOUL ORB ===\n        function createSoulMesh(color) {\n            const geo = new THREE.SphereGeometry(0.3, 8, 8);\n            const mat = new THREE.MeshBasicMaterial({ \n                color: color,\n                transparent: true,\n                opacity: 0.8\n            });\n            const mesh = new THREE.Mesh(geo, mat);\n            \n            // Glow sprite\n            const spriteMat = new THREE.SpriteMaterial({\n                color: color,\n                transparent: true,\n                opacity: 0.5\n            });\n            const sprite = new THREE.Sprite(spriteMat);\n            sprite.scale.set(1.5, 1.5, 1);\n            mesh.add(sprite);\n            \n            return mesh;\n        }\n        \n        // === FLOATING TEXT SYSTEM (Optimized) ===\n        const floatingTexts = [];\n        let lastFloatTextTime = 0;\n        const FLOAT_TEXT_COOLDOWN = 80; // Minimum ms between small floating texts\n        \n        function spawnFloatingText(x, y, z, text, type = 'normal') {\n            // Throttle small texts to reduce DOM operations\n            const now = Date.now();\n            if (type === 'small' && now - lastFloatTextTime < FLOAT_TEXT_COOLDOWN) return;\n            lastFloatTextTime = now;\n            \n            // Project 3D position to 2D screen\n            const pos3D = new THREE.Vector3(x, y + 1, z);\n            const projected = pos3D.clone().project(camera);\n            \n            const screenX = (projected.x * 0.5 + 0.5) * 720;\n            const screenY = (-projected.y * 0.5 + 0.5) * 1280;\n            \n            // Create floating text element\n            const el = document.createElement('div');\n            el.className = 'floating-text' + (type !== 'normal' ? ' ' + type : '');\n            el.textContent = text;\n            el.style.left = screenX + 'px';\n            el.style.top = screenY + 'px';\n            el.style.transform = 'translate(-50%, -50%)';\n            uiOverlay.appendChild(el);\n            \n            // Remove after animation\n            setTimeout(() => {\n                if (el.parentNode) el.parentNode.removeChild(el);\n            }, 1000);\n        }\n\n        // === 3D PARTICLES (Optimized with pooling) ===\n        const particlePool = [];\n        const particleInactive = [];\n        const MAX_PARTICLES = 100;\n        const particleGeo = new THREE.BoxGeometry(0.15, 0.15, 0.15); // Shared geometry\n        \n        function spawnParticle3D(x, y, z, color) {\n            // Limit total particles\n            if (particlePool.length >= MAX_PARTICLES) return;\n            \n            let mesh;\n            if (particleInactive.length > 0) {\n                // Reuse inactive particle\n                mesh = particleInactive.pop();\n                mesh.material.color.setHex(color);\n                mesh.visible = true;\n            } else {\n                // Create new particle with shared geometry\n                const mat = new THREE.MeshBasicMaterial({ color });\n                mesh = new THREE.Mesh(particleGeo, mat);\n                scene.add(mesh);\n            }\n            \n            mesh.position.set(x, y, z);\n            mesh.userData.vx = (Math.random() - 0.5) * 8;\n            mesh.userData.vy = Math.random() * 5 + 3;\n            mesh.userData.vz = (Math.random() - 0.5) * 8;\n            mesh.userData.life = 1.0;\n            particlePool.push(mesh);\n        }\n        \n        // === NINJA AFTERIMAGE SYSTEM (Optimized with pooling) ===\n        const ninjaAfterimages = [];\n        const ninjaAfterimagePool = [];\n        const MAX_AFTERIMAGES = 8;\n        let lastAfterimageTime = 0;\n        \n        // Pre-create afterimage materials\n        const ghostMat = new THREE.MeshBasicMaterial({ \n            color: 0x4A2A5A, \n            transparent: true, \n            opacity: 0.5 \n        });\n        const afterimageBodyGeo = new THREE.BoxGeometry(0.6, 0.8, 0.4);\n        const afterimageHeadGeo = new THREE.BoxGeometry(0.5, 0.5, 0.5);\n        \n        function spawnNinjaAfterimage(x, z, dir) {\n            if (ninjaAfterimages.length >= MAX_AFTERIMAGES) return;\n            \n            let group;\n            if (ninjaAfterimagePool.length > 0) {\n                group = ninjaAfterimagePool.pop();\n                group.visible = true;\n                // Reset material opacity\n                group.children.forEach(child => {\n                    if (child.material) child.material.opacity = 0.5;\n                });\n            } else {\n                group = new THREE.Group();\n                \n                // Body\n                const body = new THREE.Mesh(afterimageBodyGeo, ghostMat.clone());\n                body.position.y = 1.0;\n                group.add(body);\n                \n                // Head\n                const head = new THREE.Mesh(afterimageHeadGeo, ghostMat.clone());\n                head.position.y = 1.7;\n                group.add(head);\n                \n                scene.add(group);\n            }\n            \n            group.position.set(x, 0, z);\n            group.rotation.y = dir;\n            group.userData.life = 0.4;\n            ninjaAfterimages.push(group);\n        }\n        \n        function updateNinjaAfterimages(dt) {\n            for (let i = ninjaAfterimages.length - 1; i >= 0; i--) {\n                const img = ninjaAfterimages[i];\n                img.userData.life -= dt;\n                \n                // Fade out\n                const opacity = img.userData.life / 0.4 * 0.5;\n                img.children.forEach(child => {\n                    if (child.material) child.material.opacity = opacity;\n                });\n                \n                if (img.userData.life <= 0) {\n                    img.visible = false;\n                    ninjaAfterimagePool.push(img);\n                    ninjaAfterimages.splice(i, 1);\n                }\n            }\n        }\n\n        // === EDIT MODE ===\n        if (mode === 'edit') {\n            createGround();\n            \n            let selectedType = 'crate';\n            let objects3D = [];\n            let objectsData = config.objects ? [...config.objects] : [];\n            \n            // If no objects, generate default layout\n            if (objectsData.length === 0) {\n                const density = config.objectDensity || 1;\n                const types = ['grass','grass','flower_yellow','flower_red','flower_yellow','flower_red','mushroom_small','mushroom','mushroom_large','crate_small','crate','crate_large','stump','stump','tree_small','tree_small','tree','tree','tree_large','stone_small','stone','stone_large','gold','diamond'];\n                const count = Math.floor(120 * density);\n                for (let i = 0; i < count; i++) {\n                    const t = types[Math.floor(Math.random() * types.length)];\n                    objectsData.push({\n                        type: t,\n                        x: (Math.random() - 0.5) * (ARENA_SIZE * 1.6),\n                        z: (Math.random() - 0.5) * (ARENA_SIZE * 1.6)\n                    });\n                }\n                config.objects = objectsData;\n            }\n            \n            // Create 3D meshes for objects\n            function rebuildObjects() {\n                objects3D.forEach(o => scene.remove(o.mesh));\n                objects3D = [];\n                objectsData.forEach((o, idx) => {\n                    const mesh = createBlockMesh(o.type);\n                    mesh.position.set(o.x, 0, o.z);\n                    scene.add(mesh);\n                    objects3D.push({ mesh, data: o, idx });\n                });\n            }\n            rebuildObjects();\n            \n            // Edit panel UI\n            uiOverlay.innerHTML = `\n                <div class=\"edit-panel\">\n                    <div class=\"edit-panel-title\">BLOCKS</div>\n                    <div id=\"edit-palette\"></div>\n                    <div style=\"margin-top:12px;font-family:'Nunito',sans-serif;font-size:15px;color:#aab;line-height:1.6;\">\n                        Tap to place<br>Drag to move<br>Double-tap delete\n                    </div>\n                </div>\n            `;\n            \n            const palette = document.getElementById('edit-palette');\n            const blockColors = {\n                grass: '#6AAA40', flower_yellow: '#FFE135', flower_red: '#FF4444',\n                mushroom_small: '#E02020', mushroom: '#E02020', mushroom_large: '#E02020',\n                crate_small: '#A08040', crate: '#A08040', crate_large: '#A08040',\n                stump: '#6B5344',\n                tree_small: '#228B22', tree: '#228B22', tree_large: '#228B22',\n                stone_small: '#808080', stone: '#808080', stone_large: '#808080', stone_huge: '#505050',\n                gold: '#FFD700', diamond: '#00CED1'\n            };\n            Object.keys(OBJ_DEFS).forEach(type => {\n                const btn = document.createElement('div');\n                btn.className = 'edit-obj-btn' + (type === selectedType ? ' selected' : '');\n                btn.style.background = blockColors[type];\n                btn.onclick = () => {\n                    selectedType = type;\n                    palette.querySelectorAll('.edit-obj-btn').forEach(b => b.classList.remove('selected'));\n                    btn.classList.add('selected');\n                };\n                palette.appendChild(btn);\n            });\n            \n            // Raycaster for 3D picking\n            const raycaster = new THREE.Raycaster();\n            const mouse = new THREE.Vector2();\n            let dragging = null;\n            let lastTap = 0;\n            \n            function getMousePos(e) {\n                const rect = canvas.getBoundingClientRect();\n                mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n                mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;\n            }\n            \n            function get3DPosition(e) {\n                getMousePos(e);\n                raycaster.setFromCamera(mouse, camera);\n                const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);\n                const point = new THREE.Vector3();\n                raycaster.ray.intersectPlane(plane, point);\n                return point;\n            }\n            \n            canvas.addEventListener('pointerdown', (e) => {\n                const pos = get3DPosition(e);\n                if (!pos) return;\n                \n                // Check if clicking existing object\n                for (let i = objects3D.length - 1; i >= 0; i--) {\n                    const obj = objects3D[i];\n                    const dx = pos.x - obj.mesh.position.x;\n                    const dz = pos.z - obj.mesh.position.z;\n                    if (Math.abs(dx) < 1.5 && Math.abs(dz) < 1.5) {\n                        const now = Date.now();\n                        if (now - lastTap < 300) {\n                            // Double tap - delete\n                            scene.remove(obj.mesh);\n                            objectsData.splice(i, 1);\n                            objects3D.splice(i, 1);\n                            config.objects = objectsData;\n                            lastTap = 0;\n                            return;\n                        }\n                        lastTap = now;\n                        dragging = { obj, offsetX: dx, offsetZ: dz };\n                        return;\n                    }\n                }\n                \n                // Place new object\n                const newObj = { type: selectedType, x: pos.x, z: pos.z };\n                objectsData.push(newObj);\n                const mesh = createBlockMesh(selectedType);\n                mesh.position.set(pos.x, 0, pos.z);\n                scene.add(mesh);\n                objects3D.push({ mesh, data: newObj, idx: objectsData.length - 1 });\n                config.objects = objectsData;\n            });\n            \n            canvas.addEventListener('pointermove', (e) => {\n                if (!dragging) return;\n                const pos = get3DPosition(e);\n                if (!pos) return;\n                dragging.obj.mesh.position.x = pos.x - dragging.offsetX;\n                dragging.obj.mesh.position.z = pos.z - dragging.offsetZ;\n                dragging.obj.data.x = dragging.obj.mesh.position.x;\n                dragging.obj.data.z = dragging.obj.mesh.position.z;\n                config.objects = objectsData;\n            });\n            \n            canvas.addEventListener('pointerup', () => { dragging = null; });\n            \n            // Edit render loop\n            function editLoop() {\n                renderer.render(scene, camera);\n                requestAnimationFrame(editLoop);\n            }\n            editLoop();\n            return;\n        }\n\n        // === PLAY MODE ===\n        createGround();\n        \n        // Game state\n        let gameState = 'start';\n        let timeRemaining = config.matchDuration || 120;\n        let playerLength = config.startLength || 2;\n        let killCount = 0;\n        let destroyCount = 0;\n        \n        // 3D Game objects\n        let objects3D = [];\n        let souls3D = [];\n        let enemies3D = [];\n        \n        // === SPATIAL GRID for fast object collision lookup ===\n        // Objects are placed in grid cells; only nearby cells are checked.\n        const _GRID_CELL = 6; // world-unit cell size\n        const _objGrid = new Map();\n        function _gridKey(x, z) { return Math.floor(x / _GRID_CELL) * 2000 + Math.floor(z / _GRID_CELL); }\n        function _gridRebuild() {\n            _objGrid.clear();\n            for (const obj of objects3D) {\n                const k = _gridKey(obj.x, obj.z);\n                if (!_objGrid.has(k)) _objGrid.set(k, []);\n                _objGrid.get(k).push(obj);\n            }\n        }\n        function _gridNearby(x, z, r) {\n            const out = [];\n            const r1 = Math.ceil(r / _GRID_CELL) + 1;\n            const cx = Math.floor(x / _GRID_CELL), cz = Math.floor(z / _GRID_CELL);\n            for (let dx = -r1; dx <= r1; dx++) {\n                for (let dz = -r1; dz <= r1; dz++) {\n                    const cell = _objGrid.get((cx + dx) * 2000 + (cz + dz));\n                    if (cell) for (const o of cell) out.push(o);\n                }\n            }\n            return out;\n        }\n        let groundDebris = []; // Ground marks left by destroyed objects\n        let playerMesh = null;\n        let playerWeapon = null;\n        \n        // Player state - spawn near a random edge\n        const edge = Math.floor(Math.random() * 4); // 0=top, 1=right, 2=bottom, 3=left\n        const edgeDist = 42 + Math.random() * 6; // 42-48 units from center\n        let playerStartX, playerStartZ;\n        \n        if (edge === 0) { // Top edge\n            playerStartX = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n            playerStartZ = -edgeDist;\n        } else if (edge === 1) { // Right edge\n            playerStartX = edgeDist;\n            playerStartZ = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n        } else if (edge === 2) { // Bottom edge\n            playerStartX = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n            playerStartZ = edgeDist;\n        } else { // Left edge\n            playerStartX = -edgeDist;\n            playerStartZ = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n        }\n        \n        const player = {\n            x: playerStartX, z: playerStartZ,\n            dir: 0,\n            swingTimer: 0,\n            isSwinging: false,\n            swingPhase: 0, // 0: idle, 1: windup, 2: slash, 3: recovery\n            swingProgress: 0,\n            hasHitThisSwing: false,\n            defending: false,\n            invincible: 0,\n            walkCycle: 0, // For leg animation\n            knockbackVX: 0, // Knockback velocity X\n            knockbackVZ: 0, // Knockback velocity Z\n            // Respawn state\n            respawning: false,\n            respawnTimer: 0,\n            deathX: 0,\n            deathZ: 0,\n            respawnX: 0,\n            respawnZ: 0,\n            // Character-specific state\n            characterId: getSelectedCharacter(),\n            attackCount: 0, // For bomber\n            bombReady: false,\n            moveTime: 0, // For slider\n            speedBoost: 0,\n            hasShield: false, // For knight\n            guardianSpeedBoost: 0, // For guardian - speed boost after successful block\n            guardianSpeedTimer: 0 // Duration remaining\n        };\n        \n        // Enemy corpse debris system (with limit)\n        const MAX_CORPSES = 15;\n        \n        function createEnemyCorpse(x, z, shirtColor) {\n            // Remove oldest corpse if at limit\n            if (groundDebris.length >= MAX_GROUND_DEBRIS + MAX_CORPSES) {\n                const oldest = groundDebris.shift();\n                scene.remove(oldest);\n            }\n            \n            const group = new THREE.Group();\n            \n            // Fallen body - flattened character remains\n            const bodyMat = new THREE.MeshLambertMaterial({ color: shirtColor });\n            const skinMat = new THREE.MeshLambertMaterial({ color: 0xD8B39E });\n            const darkMat = new THREE.MeshLambertMaterial({ color: 0x2a2a4c });\n            \n            // Flattened torso\n            const torsoGeo = new THREE.BoxGeometry(0.9, 0.1, 0.6);\n            const torso = new THREE.Mesh(torsoGeo, bodyMat);\n            torso.position.set(0, 0.05, 0);\n            torso.receiveShadow = true;\n            group.add(torso);\n            \n            // Head (fallen to side)\n            const headGeo = new THREE.BoxGeometry(0.5, 0.12, 0.5);\n            const head = new THREE.Mesh(headGeo, skinMat);\n            head.position.set(0.6, 0.06, 0.1);\n            head.rotation.z = 0.3;\n            group.add(head);\n            \n            // Closed eyes (X marks)\n            const eyeMat = new THREE.MeshBasicMaterial({ color: 0x222222 });\n            const eyeGeo = new THREE.BoxGeometry(0.08, 0.02, 0.08);\n            const leftEye = new THREE.Mesh(eyeGeo, eyeMat);\n            leftEye.position.set(0.55, 0.13, 0.3);\n            group.add(leftEye);\n            const rightEye = new THREE.Mesh(eyeGeo, eyeMat);\n            rightEye.position.set(0.7, 0.13, 0.3);\n            group.add(rightEye);\n            \n            // Arms sprawled out\n            const armGeo = new THREE.BoxGeometry(0.5, 0.08, 0.2);\n            const leftArm = new THREE.Mesh(armGeo, skinMat);\n            leftArm.position.set(-0.1, 0.04, 0.5);\n            leftArm.rotation.y = 0.4;\n            group.add(leftArm);\n            const rightArm = new THREE.Mesh(armGeo, skinMat);\n            rightArm.position.set(-0.1, 0.04, -0.5);\n            rightArm.rotation.y = -0.3;\n            group.add(rightArm);\n            \n            // Legs\n            const legGeo = new THREE.BoxGeometry(0.5, 0.08, 0.25);\n            const leftLeg = new THREE.Mesh(legGeo, darkMat);\n            leftLeg.position.set(-0.55, 0.04, 0.2);\n            leftLeg.rotation.y = 0.2;\n            group.add(leftLeg);\n            const rightLeg = new THREE.Mesh(legGeo, darkMat);\n            rightLeg.position.set(-0.55, 0.04, -0.2);\n            rightLeg.rotation.y = -0.15;\n            group.add(rightLeg);\n            \n            // Broken weapon pieces nearby\n            const weaponPieceMat = new THREE.MeshLambertMaterial({ color: 0x6B4423 });\n            const piece1Geo = new THREE.BoxGeometry(0.3, 0.06, 0.1);\n            const piece1 = new THREE.Mesh(piece1Geo, weaponPieceMat);\n            piece1.position.set(0.3, 0.03, -0.6);\n            piece1.rotation.y = 0.8;\n            group.add(piece1);\n            const piece2Geo = new THREE.BoxGeometry(0.2, 0.05, 0.08);\n            const piece2 = new THREE.Mesh(piece2Geo, weaponPieceMat);\n            piece2.position.set(-0.4, 0.025, 0.7);\n            piece2.rotation.y = -1.2;\n            group.add(piece2);\n            \n            // Blood/damage marks (dark red patches)\n            const bloodMat = new THREE.MeshLambertMaterial({ color: 0x8B0000 });\n            for (let i = 0; i < 3; i++) {\n                const bloodGeo = new THREE.BoxGeometry(0.15 + Math.random() * 0.1, 0.01, 0.15 + Math.random() * 0.1);\n                const blood = new THREE.Mesh(bloodGeo, bloodMat);\n                blood.position.set((Math.random() - 0.5) * 0.8, 0.01, (Math.random() - 0.5) * 0.6);\n                group.add(blood);\n            }\n            \n            group.position.set(x, 0.12, z);\n            group.rotation.y = Math.random() * Math.PI * 2;\n            scene.add(group);\n            groundDebris.push(group);\n        }\n        \n        // Ground debris/marks system (with limit)\n        const MAX_GROUND_DEBRIS = 50;\n        \n        function createGroundDebris(x, z, type, scale = 1) {\n            // Remove oldest debris if at limit\n            if (groundDebris.length >= MAX_GROUND_DEBRIS) {\n                const oldest = groundDebris.shift();\n                scene.remove(oldest);\n            }\n            \n            const group = new THREE.Group();\n            const def = OBJ_DEFS[type];\n            \n            if (type === 'grass') {\n                // Flattened grass - dark dirt patch with scattered grass bits\n                const baseGeo = new THREE.BoxGeometry(1.0, 0.04, 1.0);\n                const baseMat = new THREE.MeshLambertMaterial({ color: 0x3A2A1B });\n                const base = new THREE.Mesh(baseGeo, baseMat);\n                base.position.y = 0.02;\n                base.receiveShadow = true;\n                group.add(base);\n                // Scattered grass bits (more visible)\n                for (let i = 0; i < 6; i++) {\n                    const bitGeo = new THREE.BoxGeometry(0.15, 0.04, 0.25);\n                    const bitMat = new THREE.MeshLambertMaterial({ color: 0x3A6A20 });\n                    const bit = new THREE.Mesh(bitGeo, bitMat);\n                    bit.position.set((Math.random()-0.5)*0.7, 0.04, (Math.random()-0.5)*0.7);\n                    bit.rotation.y = Math.random() * Math.PI;\n                    bit.castShadow = true;\n                    group.add(bit);\n                }\n            } else if (type.includes('flower')) {\n                // Crushed flower - petal scatter (more vibrant)\n                const baseGeo = new THREE.BoxGeometry(0.7, 0.03, 0.7);\n                const baseMat = new THREE.MeshLambertMaterial({ color: 0x2A4A10 });\n                const base = new THREE.Mesh(baseGeo, baseMat);\n                base.position.y = 0.015;\n                group.add(base);\n                // Scattered petals (larger and more)\n                const petalColor = type === 'flower_yellow' ? 0xFFD700 : 0xFF2222;\n                for (let i = 0; i < 8; i++) {\n                    const petalGeo = new THREE.BoxGeometry(0.18, 0.03, 0.18);\n                    const petalMat = new THREE.MeshLambertMaterial({ color: petalColor });\n                    const petal = new THREE.Mesh(petalGeo, petalMat);\n                    petal.position.set((Math.random()-0.5)*0.8, 0.03, (Math.random()-0.5)*0.8);\n                    petal.rotation.y = Math.random() * Math.PI;\n                    petal.rotation.z = (Math.random()-0.5) * 0.5;\n                    petal.castShadow = true;\n                    group.add(petal);\n                }\n            } else if (type.includes('mushroom')) {\n                // Mushroom remains - cap pieces and stem stub (larger)\n                const size = def.size * 0.8;\n                const stubGeo = new THREE.BoxGeometry(size * 0.5, 0.12, size * 0.5);\n                const stubMat = new THREE.MeshLambertMaterial({ color: 0xE5E5D4 });\n                const stub = new THREE.Mesh(stubGeo, stubMat);\n                stub.position.y = 0.06;\n                stub.castShadow = true;\n                group.add(stub);\n                // Cap fragments (larger and more)\n                for (let i = 0; i < 6; i++) {\n                    const fragGeo = new THREE.BoxGeometry(size * 0.4, 0.08, size * 0.35);\n                    const fragMat = new THREE.MeshLambertMaterial({ color: 0xD01010 });\n                    const frag = new THREE.Mesh(fragGeo, fragMat);\n                    frag.position.set((Math.random()-0.5)*size*1.3, 0.04, (Math.random()-0.5)*size*1.3);\n                    frag.rotation.y = Math.random() * Math.PI;\n                    frag.rotation.z = (Math.random()-0.5) * 0.6;\n                    frag.castShadow = true;\n                    group.add(frag);\n                }\n            } else if (type.includes('crate')) {\n                // Broken crate - wood planks scattered (larger, more visible)\n                const size = def.size * 0.9;\n                for (let i = 0; i < 8; i++) {\n                    const plankGeo = new THREE.BoxGeometry(size * 0.6, 0.08, size * 0.25);\n                    const plankMat = new THREE.MeshLambertMaterial({ color: i % 2 === 0 ? 0x7B5B20 : 0x5A3A10 });\n                    const plank = new THREE.Mesh(plankGeo, plankMat);\n                    plank.position.set((Math.random()-0.5)*size*1.2, 0.04 + i * 0.015, (Math.random()-0.5)*size*1.2);\n                    plank.rotation.y = Math.random() * Math.PI;\n                    plank.rotation.z = (Math.random()-0.5) * 0.5;\n                    plank.castShadow = true;\n                    group.add(plank);\n                }\n            } else if (type.includes('stone')) {\n                // Beautiful rock rubble - varied stone pieces with natural colors\n                const size = def.size * 0.85;\n                \n                // Color palette for debris\n                const stoneColors = [0x5A5A5A, 0x6A6A6A, 0x4A4A4A, 0x7A7A7A, 0x555555];\n                const accentColors = [0x8A7A6A, 0x6A7A6A]; // Warm and mossy accents\n                \n                // Ground dust/gravel base\n                const dustGeo = new THREE.BoxGeometry(size * 1.5, 0.03, size * 1.5);\n                const dustMat = new THREE.MeshLambertMaterial({ color: 0x5A5A5A });\n                const dust = new THREE.Mesh(dustGeo, dustMat);\n                dust.position.y = 0.015;\n                dust.receiveShadow = true;\n                group.add(dust);\n                \n                // Large rubble chunks\n                for (let i = 0; i < 5; i++) {\n                    const rubbleSize = 0.25 + Math.random() * 0.3;\n                    const rubbleGeo = new THREE.BoxGeometry(rubbleSize, rubbleSize * 0.65, rubbleSize * 0.9);\n                    const rubbleMat = new THREE.MeshLambertMaterial({ color: stoneColors[i % stoneColors.length] });\n                    const rubble = new THREE.Mesh(rubbleGeo, rubbleMat);\n                    const angle = (i / 5) * Math.PI * 2 + Math.random() * 0.5;\n                    rubble.position.set(\n                        Math.cos(angle) * size * 0.5,\n                        rubbleSize * 0.32,\n                        Math.sin(angle) * size * 0.45\n                    );\n                    rubble.rotation.set(Math.random() * 0.4, Math.random() * Math.PI, Math.random() * 0.3);\n                    rubble.castShadow = true;\n                    group.add(rubble);\n                }\n                \n                // Medium rubble pieces\n                for (let i = 0; i < 8; i++) {\n                    const rubbleSize = 0.12 + Math.random() * 0.18;\n                    const rubbleGeo = new THREE.BoxGeometry(rubbleSize, rubbleSize * 0.6, rubbleSize);\n                    const rubbleMat = new THREE.MeshLambertMaterial({ \n                        color: i < 6 ? stoneColors[Math.floor(Math.random() * stoneColors.length)] : accentColors[i % 2]\n                    });\n                    const rubble = new THREE.Mesh(rubbleGeo, rubbleMat);\n                    rubble.position.set(\n                        (Math.random() - 0.5) * size * 1.6,\n                        rubbleSize * 0.3,\n                        (Math.random() - 0.5) * size * 1.6\n                    );\n                    rubble.rotation.set(Math.random() * 0.5, Math.random() * Math.PI, Math.random() * 0.4);\n                    rubble.castShadow = true;\n                    group.add(rubble);\n                }\n                \n                // Small gravel/pebbles\n                for (let i = 0; i < 12; i++) {\n                    const pebbleSize = 0.05 + Math.random() * 0.08;\n                    const pebbleGeo = new THREE.BoxGeometry(pebbleSize, pebbleSize * 0.5, pebbleSize);\n                    const pebbleMat = new THREE.MeshLambertMaterial({ color: stoneColors[Math.floor(Math.random() * stoneColors.length)] });\n                    const pebble = new THREE.Mesh(pebbleGeo, pebbleMat);\n                    pebble.position.set(\n                        (Math.random() - 0.5) * size * 1.8,\n                        pebbleSize * 0.25,\n                        (Math.random() - 0.5) * size * 1.8\n                    );\n                    pebble.rotation.y = Math.random() * Math.PI;\n                    group.add(pebble);\n                }\n                \n                // Dust/debris marks (darker patches)\n                for (let i = 0; i < 3; i++) {\n                    const markGeo = new THREE.BoxGeometry(0.15 + Math.random() * 0.1, 0.02, 0.15 + Math.random() * 0.1);\n                    const markMat = new THREE.MeshLambertMaterial({ color: 0x3A3A3A });\n                    const mark = new THREE.Mesh(markGeo, markMat);\n                    mark.position.set((Math.random() - 0.5) * size * 1.2, 0.02, (Math.random() - 0.5) * size * 1.2);\n                    mark.rotation.y = Math.random() * Math.PI;\n                    group.add(mark);\n                }\n            } else if (type === 'stump') {\n                // Tree stump remains - sawdust and bark (scaled to match stump size)\n                const s = scale; // Use the scale parameter\n                const baseGeo = new THREE.BoxGeometry(1.3 * s, 0.05, 1.3 * s);\n                const baseMat = new THREE.MeshLambertMaterial({ color: 0x4B3A1B });\n                const base = new THREE.Mesh(baseGeo, baseMat);\n                base.position.y = 0.025;\n                group.add(base);\n                // Wood chips (scaled to match stump size)\n                const chipCount = Math.max(5, Math.floor(10 * s));\n                for (let i = 0; i < chipCount; i++) {\n                    const chipGeo = new THREE.BoxGeometry(0.2 * s, 0.05, 0.15 * s);\n                    const chipMat = new THREE.MeshLambertMaterial({ color: 0x7B6345 });\n                    const chip = new THREE.Mesh(chipGeo, chipMat);\n                    chip.position.set((Math.random()-0.5)*1.1*s, 0.05, (Math.random()-0.5)*1.1*s);\n                    chip.rotation.set(Math.random()*0.5, Math.random()*Math.PI, Math.random()*0.5);\n                    chip.castShadow = true;\n                    group.add(chip);\n                }\n            } else if (type === 'gold') {\n                // Gold chest debris - broken wood planks and scattered gold coins\n                const woodMat = new THREE.MeshLambertMaterial({ color: 0x6B3A1A });\n                const goldMat = new THREE.MeshLambertMaterial({ color: 0xFFD700, emissive: 0x443300 });\n                \n                // Broken chest planks\n                for (let i = 0; i < 6; i++) {\n                    const plankGeo = new THREE.BoxGeometry(0.35, 0.08, 0.2);\n                    const plank = new THREE.Mesh(plankGeo, woodMat);\n                    plank.position.set((Math.random()-0.5)*1.2, 0.04 + i * 0.01, (Math.random()-0.5)*1.2);\n                    plank.rotation.set(Math.random()*0.3, Math.random()*Math.PI, Math.random()*0.4);\n                    plank.castShadow = true;\n                    group.add(plank);\n                }\n                // Scattered gold coins\n                for (let i = 0; i < 12; i++) {\n                    const coinGeo = new THREE.BoxGeometry(0.14, 0.04, 0.14);\n                    const coin = new THREE.Mesh(coinGeo, goldMat);\n                    coin.position.set((Math.random()-0.5)*1.3, 0.06, (Math.random()-0.5)*1.3);\n                    coin.rotation.set(Math.random()*0.5, Math.random()*Math.PI, Math.random()*0.3);\n                    coin.castShadow = true;\n                    group.add(coin);\n                }\n                // Broken gold band pieces\n                const bandMat = new THREE.MeshLambertMaterial({ color: 0xDAA520 });\n                for (let i = 0; i < 3; i++) {\n                    const bandGeo = new THREE.BoxGeometry(0.25, 0.06, 0.08);\n                    const band = new THREE.Mesh(bandGeo, bandMat);\n                    band.position.set((Math.random()-0.5)*0.9, 0.08, (Math.random()-0.5)*0.9);\n                    band.rotation.y = Math.random() * Math.PI;\n                    group.add(band);\n                }\n            } else if (type === 'diamond') {\n                // Diamond chest debris - broken ornate wood and scattered gems\n                const woodMat = new THREE.MeshLambertMaterial({ color: 0x3A2518 });\n                const silverMat = new THREE.MeshLambertMaterial({ color: 0xA0A0A0 });\n                const diamondMat = new THREE.MeshLambertMaterial({ color: 0x40E0D0, emissive: 0x006666 });\n                \n                // Broken chest planks (darker wood)\n                for (let i = 0; i < 6; i++) {\n                    const plankGeo = new THREE.BoxGeometry(0.4, 0.09, 0.22);\n                    const plank = new THREE.Mesh(plankGeo, woodMat);\n                    plank.position.set((Math.random()-0.5)*1.3, 0.045 + i * 0.01, (Math.random()-0.5)*1.3);\n                    plank.rotation.set(Math.random()*0.3, Math.random()*Math.PI, Math.random()*0.4);\n                    plank.castShadow = true;\n                    group.add(plank);\n                }\n                // Scattered diamonds/gems\n                for (let i = 0; i < 10; i++) {\n                    const gemGeo = new THREE.BoxGeometry(0.12, 0.18, 0.12);\n                    const gem = new THREE.Mesh(gemGeo, diamondMat);\n                    gem.position.set((Math.random()-0.5)*1.4, 0.1, (Math.random()-0.5)*1.4);\n                    gem.rotation.set(Math.random()*0.6, Math.random()*Math.PI, Math.random()*0.5);\n                    gem.castShadow = true;\n                    group.add(gem);\n                }\n                // Broken silver band pieces\n                for (let i = 0; i < 4; i++) {\n                    const bandGeo = new THREE.BoxGeometry(0.28, 0.07, 0.09);\n                    const band = new THREE.Mesh(bandGeo, silverMat);\n                    band.position.set((Math.random()-0.5)*1.0, 0.08, (Math.random()-0.5)*1.0);\n                    band.rotation.y = Math.random() * Math.PI;\n                    group.add(band);\n                }\n            } else {\n                // Default debris - darker colored patch (more visible)\n                const baseGeo = new THREE.BoxGeometry(0.8, 0.04, 0.8);\n                const baseMat = new THREE.MeshLambertMaterial({ color: def.color });\n                const base = new THREE.Mesh(baseGeo, baseMat);\n                base.position.y = 0.02;\n                baseMat.color.multiplyScalar(0.6); // Make darker\n                group.add(base);\n            }\n            \n            group.position.set(x, 0, z);\n            group.rotation.y = Math.random() * Math.PI * 2;\n            scene.add(group);\n            groundDebris.push(group);\n        }\n        \n        // Slash trail effect (optimized with pooling)\n        let slashTrails = [];\n        const slashTrailInactive = [];\n        const MAX_SLASH_TRAILS = 20;\n        const slashTrailGeo = new THREE.PlaneGeometry(1, 0.3); // Shared, scaled per use\n        \n        function createSlashTrail(x, y, z, angle, length, color) {\n            if (slashTrails.length >= MAX_SLASH_TRAILS) return;\n            \n            let trail;\n            if (slashTrailInactive.length > 0) {\n                trail = slashTrailInactive.pop();\n                trail.material.color.setHex(color);\n                trail.material.opacity = 0.8;\n                trail.visible = true;\n            } else {\n                const trailMat = new THREE.MeshBasicMaterial({\n                    color: color,\n                    transparent: true,\n                    opacity: 0.8,\n                    side: THREE.DoubleSide\n                });\n                trail = new THREE.Mesh(slashTrailGeo, trailMat);\n                scene.add(trail);\n            }\n            \n            trail.scale.x = length * 0.8;\n            trail.position.set(x, y, z);\n            trail.rotation.y = angle + Math.PI / 2;\n            trail.rotation.x = Math.PI / 2;\n            trail.userData.life = 0.3;\n            slashTrails.push(trail);\n        }\n        \n        function updateSlashTrails(dt) {\n            for (let i = slashTrails.length - 1; i >= 0; i--) {\n                const trail = slashTrails[i];\n                trail.userData.life -= dt;\n                trail.material.opacity = trail.userData.life / 0.3 * 0.8;\n                trail.scale.y = trail.userData.life / 0.3;\n                if (trail.userData.life <= 0) {\n                    trail.visible = false;\n                    slashTrailInactive.push(trail);\n                    slashTrails.splice(i, 1);\n                }\n            }\n        }\n        \n        // Knight shield visual effect\n        let knightShieldMesh = null;\n        \n        function createKnightShieldEffect() {\n            const group = new THREE.Group();\n            \n            // Outer glowing bubble - hexagonal crystal shield\n            const shieldGeo = new THREE.SphereGeometry(1.4, 6, 4);\n            const shieldMat = new THREE.MeshBasicMaterial({\n                color: 0x44DDFF,\n                transparent: true,\n                opacity: 0.25,\n                side: THREE.DoubleSide\n            });\n            const shieldBubble = new THREE.Mesh(shieldGeo, shieldMat);\n            shieldBubble.position.y = 1.0;\n            group.add(shieldBubble);\n            \n            // Inner energy core\n            const coreGeo = new THREE.SphereGeometry(1.2, 6, 4);\n            const coreMat = new THREE.MeshBasicMaterial({\n                color: 0x88EEFF,\n                transparent: true,\n                opacity: 0.12,\n                side: THREE.DoubleSide\n            });\n            const core = new THREE.Mesh(coreGeo, coreMat);\n            core.position.y = 1.0;\n            group.add(core);\n            \n            // Rotating crystal ring around character\n            const ringGeo = new THREE.TorusGeometry(1.3, 0.05, 4, 6);\n            const ringMat = new THREE.MeshBasicMaterial({\n                color: 0x44DDFF,\n                transparent: true,\n                opacity: 0.6\n            });\n            const ring = new THREE.Mesh(ringGeo, ringMat);\n            ring.position.y = 1.0;\n            ring.rotation.x = Math.PI / 2;\n            group.add(ring);\n            \n            // Floating crystal particles around the shield\n            const crystalMat = new THREE.MeshBasicMaterial({ color: 0x66EEFF });\n            for (let i = 0; i < 6; i++) {\n                const crystalGeo = new THREE.BoxGeometry(0.1, 0.2, 0.1);\n                const crystal = new THREE.Mesh(crystalGeo, crystalMat);\n                const angle = (i / 6) * Math.PI * 2;\n                crystal.position.set(Math.cos(angle) * 1.5, 1.0 + Math.sin(i * 1.5) * 0.3, Math.sin(angle) * 1.5);\n                crystal.rotation.y = angle;\n                crystal.userData.baseAngle = angle;\n                crystal.userData.baseY = 1.0 + Math.sin(i * 1.5) * 0.3;\n                group.add(crystal);\n            }\n            \n            group.userData.ring = ring;\n            group.userData.bubble = shieldBubble;\n            group.userData.core = core;\n            \n            return group;\n        }\n        \n        function updateKnightShieldEffect(dt) {\n            if (!knightShieldMesh) return;\n            \n            if (player.characterId === 'knight' && player.hasShield && !player.respawning) {\n                knightShieldMesh.visible = true;\n                \n                // Position shield at player location\n                knightShieldMesh.position.set(player.x, 0, player.z);\n                \n                // Animate shield\n                const time = Date.now() * 0.001;\n                \n                // Rotate the ring\n                if (knightShieldMesh.userData.ring) {\n                    knightShieldMesh.userData.ring.rotation.z = time * 0.5;\n                }\n                \n                // Pulse the bubble\n                if (knightShieldMesh.userData.bubble) {\n                    const pulse = 1 + Math.sin(time * 3) * 0.05;\n                    knightShieldMesh.userData.bubble.scale.setScalar(pulse);\n                    knightShieldMesh.userData.bubble.material.opacity = 0.2 + Math.sin(time * 2) * 0.08;\n                }\n                \n                // Pulse the core opposite to bubble\n                if (knightShieldMesh.userData.core) {\n                    const corePulse = 1 + Math.sin(time * 3 + Math.PI) * 0.08;\n                    knightShieldMesh.userData.core.scale.setScalar(corePulse);\n                }\n                \n                // Animate floating crystals\n                knightShieldMesh.children.forEach((child, idx) => {\n                    if (child.userData.baseAngle !== undefined) {\n                        const angle = child.userData.baseAngle + time * 0.8;\n                        child.position.x = Math.cos(angle) * 1.5;\n                        child.position.z = Math.sin(angle) * 1.5;\n                        child.position.y = child.userData.baseY + Math.sin(time * 2 + idx) * 0.2;\n                        child.rotation.y = angle + time;\n                        child.rotation.x = Math.sin(time + idx) * 0.5;\n                    }\n                });\n            } else {\n                knightShieldMesh.visible = false;\n            }\n        }\n        \n        // Initialize player 3D model\n        let lastPlayerWeaponLength = -1;\n        const WEAPON_UPDATE_THRESHOLD = 0.5; // Only update weapon visual every 0.5m\n        \n        function initPlayer() {\n            const selectedChar = getSelectedCharacter();\n            playerMesh = createCharacter(materials.player, true, selectedChar);\n            playerMesh.position.set(0, 0, 0);\n            scene.add(playerMesh);\n            \n            // Initialize character-specific state\n            player.characterId = selectedChar;\n            player.attackCount = 0; // For bomber\n            player.bombReady = false;\n            player.moveTime = 0; // For slider\n            player.speedBoost = 0;\n            player.hasShield = (selectedChar === 'knight'); // For knight\n            \n            // Create knight shield visual effect\n            if (selectedChar === 'knight') {\n                if (knightShieldMesh) scene.remove(knightShieldMesh);\n                knightShieldMesh = createKnightShieldEffect();\n                scene.add(knightShieldMesh);\n            }\n            \n            lastPlayerWeaponLength = -1;\n            updatePlayerWeapon();\n        }\n        \n        function updatePlayerWeapon() {\n            // Throttle weapon updates - only if length changed significantly or tier changed\n            const currentTier = getWeaponTier(playerLength);\n            const lastTier = getWeaponTier(lastPlayerWeaponLength);\n            \n            if (lastPlayerWeaponLength >= 0 && \n                Math.abs(playerLength - lastPlayerWeaponLength) < WEAPON_UPDATE_THRESHOLD &&\n                currentTier === lastTier) {\n                return; // Skip update\n            }\n            \n            lastPlayerWeaponLength = playerLength;\n            \n            if (playerWeapon) {\n                playerMesh.userData.weaponPivot.remove(playerWeapon);\n            }\n            playerWeapon = createWeapon(playerLength, getWeaponMaterial(playerLength));\n            // Weapon extends forward from hand, no initial rotation needed\n            playerMesh.userData.weaponPivot.add(playerWeapon);\n        }\n        \n        // Initialize game objects\n        function initGameObjects() {\n            objects3D.forEach(o => scene.remove(o.mesh));\n            objects3D = [];\n            \n            let objectsData = config.objects && config.objects.length > 0 ? [...config.objects] : [];\n            \n            if (objectsData.length === 0) {\n                const density = config.objectDensity || 1;\n                // Central plaza - high value\n                for (let i = 0; i < Math.floor(6 * density); i++) {\n                    objectsData.push({ type: 'gold', x: (Math.random()-0.5)*15, z: (Math.random()-0.5)*15 });\n                }\n                for (let i = 0; i < Math.floor(5 * density); i++) {\n                    objectsData.push({ type: 'diamond', x: (Math.random()-0.5)*10, z: (Math.random()-0.5)*10 });\n                }\n                // Flower fields - easy early game (more flowers)\n                for (let i = 0; i < Math.floor(50 * density); i++) {\n                    const type = Math.random() < 0.5 ? 'flower_yellow' : 'flower_red';\n                    objectsData.push({ type, x: (Math.random()-0.5)*85, z: (Math.random()-0.5)*85 });\n                }\n                // Mushroom patches - varied sizes\n                for (let i = 0; i < Math.floor(18 * density); i++) {\n                    objectsData.push({ type: 'mushroom_small', x: -30 + Math.random()*25, z: -30 + Math.random()*25 });\n                }\n                for (let i = 0; i < Math.floor(14 * density); i++) {\n                    objectsData.push({ type: 'mushroom', x: -30 + Math.random()*25, z: -30 + Math.random()*25 });\n                }\n                for (let i = 0; i < Math.floor(8 * density); i++) {\n                    objectsData.push({ type: 'mushroom_large', x: -30 + Math.random()*25, z: -30 + Math.random()*25 });\n                }\n                for (let i = 0; i < Math.floor(15 * density); i++) {\n                    const mtype = ['mushroom_small', 'mushroom', 'mushroom_large'][Math.floor(Math.random()*3)];\n                    objectsData.push({ type: mtype, x: 15 + Math.random()*20, z: 15 + Math.random()*20 });\n                }\n                // Additional mushroom cluster\n                for (let i = 0; i < Math.floor(12 * density); i++) {\n                    const mtype = ['mushroom_small', 'mushroom', 'mushroom_large'][Math.floor(Math.random()*3)];\n                    objectsData.push({ type: mtype, x: -15 + Math.random()*30, z: -40 + Math.random()*15 });\n                }\n                // Tree forest areas - varied sizes (Main forest - NE)\n                for (let i = 0; i < Math.floor(16 * density); i++) {\n                    objectsData.push({ type: 'tree_small', x: 20 + Math.random()*22, z: -35 + Math.random()*28 });\n                }\n                for (let i = 0; i < Math.floor(14 * density); i++) {\n                    objectsData.push({ type: 'tree', x: 18 + Math.random()*24, z: -33 + Math.random()*26 });\n                }\n                for (let i = 0; i < Math.floor(10 * density); i++) {\n                    objectsData.push({ type: 'tree_large', x: 20 + Math.random()*20, z: -30 + Math.random()*22 });\n                }\n                // Second forest area (SW)\n                for (let i = 0; i < Math.floor(14 * density); i++) {\n                    objectsData.push({ type: 'tree_small', x: -40 + Math.random()*22, z: 8 + Math.random()*28 });\n                }\n                for (let i = 0; i < Math.floor(12 * density); i++) {\n                    objectsData.push({ type: 'tree', x: -38 + Math.random()*20, z: 10 + Math.random()*26 });\n                }\n                for (let i = 0; i < Math.floor(8 * density); i++) {\n                    objectsData.push({ type: 'tree_large', x: -36 + Math.random()*18, z: 12 + Math.random()*22 });\n                }\n                // Third forest area (NW)\n                for (let i = 0; i < Math.floor(12 * density); i++) {\n                    objectsData.push({ type: 'tree_small', x: -38 + Math.random()*20, z: -38 + Math.random()*20 });\n                }\n                for (let i = 0; i < Math.floor(10 * density); i++) {\n                    objectsData.push({ type: 'tree', x: -36 + Math.random()*18, z: -36 + Math.random()*18 });\n                }\n                for (let i = 0; i < Math.floor(6 * density); i++) {\n                    objectsData.push({ type: 'tree_large', x: -34 + Math.random()*16, z: -34 + Math.random()*16 });\n                }\n                // Fourth forest area (SE)\n                for (let i = 0; i < Math.floor(10 * density); i++) {\n                    objectsData.push({ type: 'tree_small', x: 25 + Math.random()*18, z: 20 + Math.random()*20 });\n                }\n                for (let i = 0; i < Math.floor(8 * density); i++) {\n                    objectsData.push({ type: 'tree', x: 27 + Math.random()*16, z: 22 + Math.random()*18 });\n                }\n                for (let i = 0; i < Math.floor(5 * density); i++) {\n                    objectsData.push({ type: 'tree_large', x: 28 + Math.random()*14, z: 24 + Math.random()*16 });\n                }\n                // Crate walls - arranged in wall formations\n                const crateWalls = [\n                    // Long horizontal wall in the west\n                    { startX: -38, startZ: -15, dirX: 0, dirZ: 1.3, count: 12, type: 'crate' },\n                    { startX: -36.5, startZ: -14, dirX: 0, dirZ: 1.3, count: 10, type: 'crate_small' }, // Second layer\n                    \n                    // L-shaped wall in northwest\n                    { startX: -30, startZ: -35, dirX: 1.4, dirZ: 0, count: 8, type: 'crate' },\n                    { startX: -30, startZ: -35, dirX: 0, dirZ: 1.4, count: 6, type: 'crate' },\n                    { startX: -28.5, startZ: -33.5, dirX: 1.4, dirZ: 0, count: 6, type: 'crate_small' },\n                    \n                    // Horizontal wall segment in south\n                    { startX: -20, startZ: 25, dirX: 1.5, dirZ: 0, count: 10, type: 'crate_large' },\n                    { startX: -18, startZ: 26.5, dirX: 1.5, dirZ: 0, count: 8, type: 'crate' },\n                    \n                    // Vertical wall in east\n                    { startX: 20, startZ: -5, dirX: 0, dirZ: 1.3, count: 10, type: 'crate' },\n                    { startX: 21.5, startZ: -4, dirX: 0, dirZ: 1.3, count: 8, type: 'crate_small' },\n                    \n                    // Small fortification near center-east\n                    { startX: 12, startZ: 8, dirX: 1.4, dirZ: 0, count: 5, type: 'crate' },\n                    { startX: 12, startZ: 8, dirX: 0, dirZ: 1.4, count: 4, type: 'crate' },\n                    { startX: 18, startZ: 8, dirX: 0, dirZ: 1.4, count: 4, type: 'crate' },\n                    \n                    // Diagonal-ish barrier southwest\n                    { startX: -35, startZ: 15, dirX: 0.8, dirZ: 1.0, count: 8, type: 'crate_large' },\n                    \n                    // Short walls scattered\n                    { startX: 5, startZ: -25, dirX: 1.3, dirZ: 0, count: 6, type: 'crate' },\n                    { startX: -10, startZ: 35, dirX: 1.4, dirZ: 0, count: 7, type: 'crate' },\n                ];\n                \n                crateWalls.forEach(wall => {\n                    for (let i = 0; i < Math.floor(wall.count * density); i++) {\n                        objectsData.push({\n                            type: wall.type,\n                            x: wall.startX + wall.dirX * i + (Math.random() - 0.5) * 0.2,\n                            z: wall.startZ + wall.dirZ * i + (Math.random() - 0.5) * 0.2\n                        });\n                    }\n                });\n                // No direct stumps - stumps are created when trees are chopped\n                // Stone area - varied sizes including huge stones\n                for (let i = 0; i < Math.floor(12 * density); i++) {\n                    objectsData.push({ type: 'stone_small', x: (Math.random()-0.5)*75, z: 25 + Math.random()*18 });\n                }\n                for (let i = 0; i < Math.floor(14 * density); i++) {\n                    objectsData.push({ type: 'stone', x: (Math.random()-0.5)*75, z: 25 + Math.random()*18 });\n                }\n                for (let i = 0; i < Math.floor(12 * density); i++) {\n                    objectsData.push({ type: 'stone_large', x: (Math.random()-0.5)*75, z: 25 + Math.random()*18 });\n                }\n                for (let i = 0; i < Math.floor(6 * density); i++) {\n                    objectsData.push({ type: 'stone_huge', x: (Math.random()-0.5)*75, z: 25 + Math.random()*18 });\n                }\n                for (let i = 0; i < Math.floor(14 * density); i++) {\n                    const stype = ['stone_small', 'stone', 'stone_large', 'stone_huge'][Math.floor(Math.random()*4)];\n                    objectsData.push({ type: stype, x: (Math.random()-0.5)*75, z: -38 + Math.random()*12 });\n                }\n                // Additional stone cluster with huge stones\n                for (let i = 0; i < Math.floor(10 * density); i++) {\n                    const stype = ['stone', 'stone_large', 'stone_huge'][Math.floor(Math.random()*3)];\n                    objectsData.push({ type: stype, x: 30 + Math.random()*12, z: -10 + Math.random()*20 });\n                }\n                // Grass scattered everywhere (more)\n                for (let i = 0; i < Math.floor(60 * density); i++) {\n                    objectsData.push({ type: 'grass', x: (Math.random()-0.5)*88, z: (Math.random()-0.5)*88 });\n                }\n                // Extra scattered trees throughout the map (many more)\n                for (let i = 0; i < Math.floor(30 * density); i++) {\n                    const ttype = ['tree_small', 'tree', 'tree_large'][Math.floor(Math.random()*3)];\n                    objectsData.push({ type: ttype, x: (Math.random()-0.5)*85, z: (Math.random()-0.5)*85 });\n                }\n                // Scattered variety objects\n                for (let i = 0; i < Math.floor(20 * density); i++) {\n                    const types = ['flower_yellow', 'flower_red', 'mushroom_small', 'mushroom', 'grass'];\n                    const type = types[Math.floor(Math.random()*types.length)];\n                    objectsData.push({ type, x: (Math.random()-0.5)*88, z: (Math.random()-0.5)*88 });\n                }\n            }\n            \n            objectsData.forEach(o => {\n                const def = OBJ_DEFS[o.type];\n                const mesh = createBlockMesh(o.type);\n                mesh.position.set(o.x, 0, o.z);\n                scene.add(mesh);\n                objects3D.push({\n                    mesh,\n                    type: o.type,\n                    hp: def.hp,\n                    maxHp: def.hp,\n                    x: o.x,\n                    z: o.z\n                });\n            });\n        }\n        \n        // Initialize enemies\n        function initEnemies() {\n            enemies3D.forEach(e => scene.remove(e.mesh));\n            enemies3D = [];\n            \n            const count = config.aiCount || 8;\n            const enemyMats = [materials.enemyRed, materials.enemyPurple, materials.enemyOrange, materials.enemyGreen];\n            const enemyColors = [0xCC3333, 0x8B008B, 0xFF8C00, 0x228B22];\n            \n            for (let i = 0; i < count; i++) {\n                // Spawn near map edges\n                const edge = Math.floor(Math.random() * 4); // 0=top, 1=right, 2=bottom, 3=left\n                let spawnX, spawnZ;\n                const edgeDist = 42 + Math.random() * 6; // 42-48 units from center\n                \n                if (edge === 0) { // Top edge\n                    spawnX = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                    spawnZ = -edgeDist;\n                } else if (edge === 1) { // Right edge\n                    spawnX = edgeDist;\n                    spawnZ = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                } else if (edge === 2) { // Bottom edge\n                    spawnX = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                    spawnZ = edgeDist;\n                } else { // Left edge\n                    spawnX = -edgeDist;\n                    spawnZ = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                }\n                \n                const mesh = createCharacter(enemyMats[i % enemyMats.length], false);\n                mesh.position.set(spawnX, 0, spawnZ);\n                scene.add(mesh);\n                \n                const weapon = createWeapon(2 + Math.random() * 3, materials.weaponWood);\n                // Weapon extends forward from hand\n                mesh.userData.weaponPivot.add(weapon);\n                \n                enemies3D.push({\n                    mesh,\n                    weapon,\n                    x: spawnX,\n                    z: spawnZ,\n                    dir: Math.random() * Math.PI * 2,\n                    length: config.startLength || 2, // Same starting length as player\n                    shirtColor: enemyColors[i % enemyColors.length],\n                    swingTimer: Math.random() * 0.6,\n                    isSwinging: false,\n                    swingPhase: 0,\n                    swingProgress: 0,\n                    hasHitThisSwing: false,\n                    hasHitThisSwingPlayer: false,\n                    baseSpeed: PLAYER_SPEED, // Same base speed as player\n                    knockbackVX: 0,\n                    knockbackVZ: 0,\n                    stunTimer: 0,\n                    defending: false, // AI can defend too\n                    invincible: 0, // Invincibility after being hit\n                    walkCycle: 0,\n                    // Respawn state\n                    respawning: false,\n                    respawnTimer: 0,\n                    deathX: 0,\n                    deathZ: 0,\n                    respawnX: 0,\n                    respawnZ: 0,\n                    ai: {\n                        targetX: 0,\n                        targetZ: 0,\n                        retargetTimer: 0,\n                        behavior: Math.random() < 0.4 ? 'chase' : 'gather',\n                        idleTimer: 0 // For triggering defense stance\n                    }\n                });\n            }\n        }\n        \n        // Input handling\n        let joystick = { active: false, startX: 0, startY: 0, dx: 0, dy: 0, touchId: null };\n        let keys = {};\n        \n        canvas.addEventListener('pointerdown', handlePointerDown);\n        canvas.addEventListener('pointermove', handlePointerMove);\n        canvas.addEventListener('pointerup', handlePointerEnd);\n        canvas.addEventListener('pointercancel', handlePointerEnd);\n        canvas.addEventListener('contextmenu', e => e.preventDefault());\n        document.addEventListener('keydown', (e) => { \n            try { if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); } catch(err) {}\n            keys[e.key.toLowerCase()] = true; \n        });\n        document.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; });\n        \n        function getPointerPos(e) {\n            const rect = canvas.getBoundingClientRect();\n            return {\n                x: (e.clientX - rect.left) / rect.width * 720,\n                y: (e.clientY - rect.top) / rect.height * 1280\n            };\n        }\n        \n        function handlePointerDown(e) {\n            try { if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); } catch(err) {}\n            if (gameState === 'start') return;\n            if (!joystick.active) {\n                const pos = getPointerPos(e);\n                if (pos.y > 1280 * 0.3) {\n                    joystick.active = true;\n                    joystick.startX = pos.x;\n                    joystick.startY = pos.y;\n                    joystick.dx = 0;\n                    joystick.dy = 0;\n                    joystick.touchId = e.pointerId;\n                    try { canvas.setPointerCapture(e.pointerId); } catch(err) {}\n                }\n            }\n        }\n        \n        function handlePointerMove(e) {\n            if (joystick.active && e.pointerId === joystick.touchId) {\n                const pos = getPointerPos(e);\n                joystick.dx = pos.x - joystick.startX;\n                joystick.dy = pos.y - joystick.startY;\n                const dist = Math.sqrt(joystick.dx*joystick.dx + joystick.dy*joystick.dy);\n                if (dist > 80) {\n                    joystick.dx = (joystick.dx / dist) * 80;\n                    joystick.dy = (joystick.dy / dist) * 80;\n                }\n            }\n        }\n        \n        function handlePointerEnd(e) {\n            if (e.pointerId === joystick.touchId) {\n                joystick.active = false;\n                joystick.dx = 0;\n                joystick.dy = 0;\n                joystick.touchId = null;\n                try { canvas.releasePointerCapture(e.pointerId); } catch(err) {}\n            }\n        }\n        \n        function getInputDir() {\n            let dx = 0, dz = 0;\n            if (keys['w'] || keys['arrowup']) dz -= 1;\n            if (keys['s'] || keys['arrowdown']) dz += 1;\n            if (keys['a'] || keys['arrowleft']) dx -= 1;\n            if (keys['d'] || keys['arrowright']) dx += 1;\n            if (joystick.active) {\n                const dist = Math.sqrt(joystick.dx*joystick.dx + joystick.dy*joystick.dy);\n                if (dist > 15) {\n                    dx += joystick.dx / 80;\n                    dz += joystick.dy / 80;\n                }\n            }\n            const mag = Math.sqrt(dx*dx + dz*dz);\n            if (mag > 1) { dx /= mag; dz /= mag; }\n            return { dx, dz };\n        }\n        \n        // Spawn soul\n        function spawnSoul(x, z, color, gain) {\n            const mesh = createSoulMesh(color);\n            mesh.position.set(x + (Math.random()-0.5)*2, 1, z + (Math.random()-0.5)*2);\n            scene.add(mesh);\n            souls3D.push({ mesh, gain, life: 8, pulse: 0 });\n        }\n        \n        // Weapon clash detection and knockback\n        function checkWeaponClash(attacker, defender, attackerLen, defenderLen, attackerDir, defenderDir) {\n            // Check if both are in active slash phase\n            if (!attacker.isSwinging || attacker.swingPhase !== 2) return false;\n            if (!defender.isSwinging || defender.swingPhase !== 2) return false;\n            \n            const METER_TO_3D = 0.5;\n            const aLen = attackerLen * METER_TO_3D;\n            const dLen = defenderLen * METER_TO_3D;\n            \n            // Calculate weapon widths for more accurate collision\n            const aWidth = 0.55 + Math.min(attackerLen / 50, 2.5);\n            const dWidth = 0.55 + Math.min(defenderLen / 50, 2.5);\n            \n            // More generous collision distance - accounts for weapon width\n            const clashThreshold = 1.2 + (aWidth + dWidth) * 0.3;\n            \n            // Check if weapon arcs intersect with denser sampling (more detection points)\n            for (let ad = 0.2; ad <= aLen; ad += 0.25) {\n                const apx = attacker.x + Math.sin(attackerDir) * ad;\n                const apz = attacker.z + Math.cos(attackerDir) * ad;\n                \n                for (let dd = 0.2; dd <= dLen; dd += 0.25) {\n                    const dpx = defender.x + Math.sin(defenderDir) * dd;\n                    const dpz = defender.z + Math.cos(defenderDir) * dd;\n                    \n                    const dist = Math.hypot(apx - dpx, apz - dpz);\n                    if (dist < clashThreshold) { // Weapons are close enough to clash\n                        return true;\n                    }\n                }\n            }\n            return false;\n        }\n        \n        function applyKnockback(entity, fromX, fromZ, force) {\n            const dx = entity.x - fromX;\n            const dz = entity.z - fromZ;\n            const dist = Math.hypot(dx, dz);\n            if (dist > 0.01) {\n                // Set knockback velocity instead of instant position change\n                const knockbackSpeed = force * 12; // Convert force to velocity (reduced for slower knockback)\n                entity.knockbackVX = (dx / dist) * knockbackSpeed;\n                entity.knockbackVZ = (dz / dist) * knockbackSpeed;\n            }\n        }\n        \n        // Track clash cooldowns to prevent repeated clashes\n        let playerClashCooldown = 0;\n        const enemyClashCooldowns = new Map();\n        \n        // Check weapon hits (optimized with early distance checks)\n        function checkPlayerHits() {\n            // Match visual scale: 1 meter = 0.5 3D units\n            const METER_TO_3D = 0.5;\n            const weaponLen = playerLength * METER_TO_3D;\n            // Blade width scales with length (matching createWeapon visual)\n            const bladeWidth = 0.55 + Math.min(playerLength / 50, 2.5);\n            const dirX = Math.sin(player.dir);\n            const dirZ = Math.cos(player.dir);\n            // Perpendicular direction for width checks\n            const perpX = Math.cos(player.dir);\n            const perpZ = -Math.sin(player.dir);\n            \n            // Max reach for early culling\n            const maxReach = weaponLen + 2;\n            \n            // Check objects - use spatial grid for culling\n            const _objCandidates = _gridNearby(player.x, player.z, maxReach + 2);\n            for (let ci = _objCandidates.length - 1; ci >= 0; ci--) {\n                const obj = _objCandidates[ci];\n                const i = objects3D.indexOf(obj);\n                if (i < 0) continue;\n                let hit = false;\n                // Check along blade length\n                for (let d = 0.3; d <= weaponLen && !hit; d += 0.4) {\n                    // Width increases toward tip (like the visual blade)\n                    const widthAtD = bladeWidth * (0.7 + (d / weaponLen) * 0.5);\n                    // Check across blade width at this length\n                    for (let w = -widthAtD / 2; w <= widthAtD / 2 && !hit; w += widthAtD / 2) {\n                        const px = player.x + dirX * d + perpX * w;\n                        const pz = player.z + dirZ * d + perpZ * w;\n                        const dist = Math.hypot(px - obj.x, pz - obj.z);\n                        if (dist < OBJ_DEFS[obj.type].size * 0.8) {\n                            hit = true;\n                        }\n                    }\n                }\n                if (hit) {\n                    obj.hp--;\n                    // Play material-specific hit sound\n                    const matType = obj.type.includes('crate') ? 'crate' : \n                                   obj.type.includes('stone') ? 'stone' :\n                                   obj.type.includes('tree') ? 'tree' :\n                                   obj.type.includes('mushroom') ? 'mushroom' :\n                                   obj.type.includes('flower') ? 'flower' :\n                                   obj.type === 'grass' ? 'grass' :\n                                   obj.type === 'gold' ? 'gold' :\n                                   obj.type === 'diamond' ? 'diamond' : 'wood';\n                    playHitSound(matType);\n                    for (let p = 0; p < 3; p++) spawnParticle3D(obj.x, 1, obj.z, OBJ_DEFS[obj.type].color);\n                    \n                    // === CHARACTER PASSIVE: Bomber explosion ===\n                    if (player.characterId === 'bomber' && player.bombReady) {\n                        // Explosion effect - damage objects in radius\n                        player.bombReady = false;\n                        const explosionRadius = 3; // +2 grid = ~3 units\n                        playDestroySound('gold'); // Explosion sound\n                        \n                        // Enhanced visual explosion - multiple waves of particles\n                        // Core explosion - bright yellow/white center\n                        for (let p = 0; p < 15; p++) {\n                            const angle = Math.random() * Math.PI * 2;\n                            const dist = Math.random() * 0.8;\n                            spawnParticle3D(\n                                obj.x + Math.cos(angle) * dist,\n                                1.5 + Math.random() * 1.5,\n                                obj.z + Math.sin(angle) * dist,\n                                Math.random() > 0.3 ? 0xFFFF00 : 0xFFFFFF\n                            );\n                        }\n                        // Inner ring - orange/red fire\n                        for (let p = 0; p < 25; p++) {\n                            const angle = Math.random() * Math.PI * 2;\n                            const dist = 0.5 + Math.random() * 1.5;\n                            const fireColors = [0xFF0000, 0xFF3300, 0xFF6600, 0xFF9900];\n                            spawnParticle3D(\n                                obj.x + Math.cos(angle) * dist,\n                                0.8 + Math.random() * 2.5,\n                                obj.z + Math.sin(angle) * dist,\n                                fireColors[Math.floor(Math.random() * fireColors.length)]\n                            );\n                        }\n                        // Outer ring - orange/smoke\n                        for (let p = 0; p < 20; p++) {\n                            const angle = Math.random() * Math.PI * 2;\n                            const dist = 1.5 + Math.random() * explosionRadius;\n                            const outerColors = [0xFF6600, 0xFFAA00, 0x333333, 0x555555];\n                            spawnParticle3D(\n                                obj.x + Math.cos(angle) * dist,\n                                0.5 + Math.random() * 2,\n                                obj.z + Math.sin(angle) * dist,\n                                outerColors[Math.floor(Math.random() * outerColors.length)]\n                            );\n                        }\n                        // Ground debris flying up\n                        for (let p = 0; p < 10; p++) {\n                            const angle = Math.random() * Math.PI * 2;\n                            const dist = Math.random() * explosionRadius * 0.8;\n                            spawnParticle3D(\n                                obj.x + Math.cos(angle) * dist,\n                                0.2 + Math.random() * 0.5,\n                                obj.z + Math.sin(angle) * dist,\n                                0x8B4513\n                            );\n                        }\n                        \n                        // Explosion shockwave ring effect\n                        const ringGeo = new THREE.RingGeometry(0.1, explosionRadius * 1.2, 32);\n                        const ringMat = new THREE.MeshBasicMaterial({ \n                            color: 0xFF6600, \n                            transparent: true, \n                            opacity: 0.8,\n                            side: THREE.DoubleSide\n                        });\n                        const shockwave = new THREE.Mesh(ringGeo, ringMat);\n                        shockwave.rotation.x = -Math.PI / 2;\n                        shockwave.position.set(obj.x, 0.1, obj.z);\n                        scene.add(shockwave);\n                        \n                        // Animate shockwave expansion and fade\n                        let shockwaveLife = 0.4;\n                        const animateShockwave = () => {\n                            shockwaveLife -= 0.016;\n                            if (shockwaveLife > 0) {\n                                const progress = 1 - (shockwaveLife / 0.4);\n                                shockwave.scale.setScalar(1 + progress * 2);\n                                shockwave.material.opacity = 0.8 * (1 - progress);\n                                requestAnimationFrame(animateShockwave);\n                            } else {\n                                scene.remove(shockwave);\n                            }\n                        };\n                        animateShockwave();\n                        \n                        spawnFloatingText(obj.x, 2.5, obj.z, '\ud83d\udca5\ud83d\udca5 BOOM! \ud83d\udca5\ud83d\udca5', 'large');\n                        \n                        // Damage nearby objects - increased to 3\n                        for (let j = objects3D.length - 1; j >= 0; j--) {\n                            if (j === i) continue;\n                            const nearObj = objects3D[j];\n                            const dist = Math.hypot(nearObj.x - obj.x, nearObj.z - obj.z);\n                            if (dist < explosionRadius) {\n                                nearObj.hp -= 3;\n                                for (let p = 0; p < 8; p++) spawnParticle3D(nearObj.x, 1, nearObj.z, OBJ_DEFS[nearObj.type].color);\n                            }\n                        }\n                        \n                        // Damage nearby enemies\n                        for (let j = enemies3D.length - 1; j >= 0; j--) {\n                            const en = enemies3D[j];\n                            if (en.respawning || en.invincible > 0) continue;\n                            const dist = Math.hypot(en.x - obj.x, en.z - obj.z);\n                            if (dist < explosionRadius) {\n                                const absorbRate = player.characterId === 'ninja' ? 0.35 : 0.25;\n                                const gain = en.length * absorbRate * growthMult;\n                                playerLength = playerLength + gain;\n                                for (let p = 0; p < 12; p++) spawnParticle3D(en.x, 1, en.z, 0xFF4500);\n                                spawnFloatingText(en.x, 2, en.z, '+' + gain.toFixed(1) + 'm', 'large');\n                                createEnemyCorpse(en.x, en.z, en.shirtColor);\n                                respawnEnemy(en);\n                                killCount++;\n                            }\n                        }\n                        updatePlayerWeapon();\n                    }\n                    \n                    // Small length gain on every hit for better feedback\n                    let hitFeedbackGain = 0.08 * growthMult;\n                    \n                    // === CHARACTER PASSIVE: Miner 2x stone bonus ===\n                    if (player.characterId === 'miner' && obj.type.includes('stone')) {\n                        hitFeedbackGain *= 2;\n                    }\n                    \n                    playerLength = playerLength + hitFeedbackGain;\n                    updatePlayerWeapon();\n                    \n                    // Show floating text for hit feedback gain - use 'small' style for gains < 0.1m\n                    const feedbackTextType = hitFeedbackGain < 0.1 ? 'small' : 'normal';\n                    spawnFloatingText(obj.x, 1.5, obj.z, '+' + hitFeedbackGain.toFixed(2) + 'm', feedbackTextType);\n                    \n                    if (obj.hp <= 0) {\n                        const def = OBJ_DEFS[obj.type];\n                        let gain = def.lengthGain * growthMult;\n                        \n                        // === CHARACTER PASSIVE: Miner 2x stone bonus ===\n                        if (player.characterId === 'miner' && obj.type.includes('stone')) {\n                            gain *= 2;\n                            spawnFloatingText(obj.x, 2.2, obj.z, '\u26cf\ufe0f 2x!', 'gold');\n                        }\n                        \n                        // Direct length gain - no souls\n                        playerLength = playerLength + gain;\n                        playCollectSound(gain);\n                        playDestroySound(obj.type);\n                        updatePlayerWeapon();\n                        for (let p = 0; p < 8; p++) spawnParticle3D(obj.x, 1, obj.z, def.color);\n                        \n                        // Spawn floating text showing length gain - different styles based on gain amount\n                        let textType;\n                        if (gain < 0.1) {\n                            textType = 'small';\n                        } else if (obj.type === 'diamond') {\n                            textType = 'diamond';\n                        } else if (obj.type === 'gold') {\n                            textType = 'gold';\n                        } else if (gain >= 1) {\n                            textType = 'large';\n                        } else {\n                            textType = 'normal';\n                        }\n                        spawnFloatingText(obj.x, 1.5, obj.z, '+' + gain.toFixed(1) + 'm', textType);\n                        \n                        scene.remove(obj.mesh);\n                        \n                        // Trees turn into stumps when chopped - scale stump based on tree size\n                        if (obj.type === 'tree_small' || obj.type === 'tree' || obj.type === 'tree_large') {\n                            const stumpDef = OBJ_DEFS['stump'];\n                            const stumpMesh = createBlockMesh('stump');\n                            // Scale stump based on original tree size\n                            const treeSize = OBJ_DEFS[obj.type].size;\n                            const stumpScale = treeSize / 1.8; // Scale down to ~55% of tree size\n                            stumpMesh.scale.set(stumpScale, stumpScale, stumpScale);\n                            stumpMesh.position.set(obj.x, 0, obj.z);\n                            scene.add(stumpMesh);\n                            objects3D[i] = {\n                                mesh: stumpMesh,\n                                type: 'stump',\n                                hp: stumpDef.hp,\n                                maxHp: stumpDef.hp,\n                                x: obj.x,\n                                z: obj.z,\n                                scale: stumpScale\n                            };\n                        } else if (obj.type === 'stone_huge' || obj.type === 'stone_large' || obj.type === 'stone') {\n                            // Stones shrink when chopped\n                            const nextStone = obj.type === 'stone_huge' ? 'stone_large' : \n                                              obj.type === 'stone_large' ? 'stone' : 'stone_small';\n                            const nextDef = OBJ_DEFS[nextStone];\n                            const nextMesh = createBlockMesh(nextStone);\n                            nextMesh.position.set(obj.x, 0, obj.z);\n                            scene.add(nextMesh);\n                            objects3D[i] = {\n                                mesh: nextMesh,\n                                type: nextStone,\n                                hp: nextDef.hp,\n                                maxHp: nextDef.hp,\n                                x: obj.x,\n                                z: obj.z\n                            };\n                            // Spawn some rock particles\n                            for (let p = 0; p < 4; p++) spawnParticle3D(obj.x, 0.5, obj.z, 0x808080);\n                        } else {\n                            // Leave ground debris mark (pass scale for stumps)\n                            createGroundDebris(obj.x, obj.z, obj.type, obj.scale || 1);\n                            objects3D.splice(i, 1);\n                        }\n                        destroyCount++;\n                    }\n                }\n            }\n            \n            // Check enemies - also use blade width (optimized)\n            for (let i = enemies3D.length - 1; i >= 0; i--) {\n                const en = enemies3D[i];\n                \n                // Skip enemies that are respawning or invincible\n                if (en.respawning || en.invincible > 0) continue;\n                \n                // Early distance cull\n                const roughDist = Math.abs(player.x - en.x) + Math.abs(player.z - en.z);\n                if (roughDist > maxReach * 1.5) continue;\n                \n                // Check for weapon clash first\n                if (playerClashCooldown <= 0 && checkWeaponClash(player, en, playerLength, en.length, player.dir, en.dir)) {\n                    // Weapons clashed! Knockback both, no damage\n                    const midX = (player.x + en.x) / 2;\n                    const midZ = (player.z + en.z) / 2;\n                    const knockbackForce = 3;\n                    \n                    applyKnockback(player, midX, midZ, knockbackForce);\n                    applyKnockback(en, midX, midZ, knockbackForce);\n                    \n                    // Visual/audio feedback for clash\n                    playClashSound();\n                    \n                    // Spawn clash particles (sparks)\n                    for (let p = 0; p < 8; p++) {\n                        spawnParticle3D(midX, 1.5, midZ, 0xFFFF00);\n                        spawnParticle3D(midX, 1.2, midZ, 0xFFAA00);\n                    }\n                    \n                    // Show clash text\n                    spawnFloatingText(midX, 2, midZ, '\u2694\ufe0f CLASH!', 'large');\n                    \n                    // Set cooldown to prevent repeated clashes\n                    playerClashCooldown = 0.5;\n                    enemyClashCooldowns.set(en, 0.5);\n                    \n                    continue; // Skip damage check for this enemy\n                }\n                \n                // Check hit with blade width\n                let enemyHit = false;\n                for (let d = 0.3; d <= weaponLen && !enemyHit; d += 0.4) {\n                    const widthAtD = bladeWidth * (0.7 + (d / weaponLen) * 0.5);\n                    for (let w = -widthAtD / 2; w <= widthAtD / 2 && !enemyHit; w += widthAtD / 2) {\n                        const px = player.x + dirX * d + perpX * w;\n                        const pz = player.z + dirZ * d + perpZ * w;\n                        const dist = Math.hypot(px - en.x, pz - en.z);\n                        if (dist < 1.0) {\n                            enemyHit = true;\n                        }\n                    }\n                }\n                \n                if (enemyHit) {\n                    // === PEACE MODE: Knockback only, no kills ===\n                    if (isPeaceMode()) {\n                        // Strong knockback effect\n                        const knockbackForce = 5 + playerLength * 0.1; // Longer weapon = stronger knockback\n                        applyKnockback(en, player.x, player.z, knockbackForce);\n                        \n                        // Visual feedback\n                        for (let p = 0; p < 6; p++) spawnParticle3D(en.x, 1, en.z, 0x88FFFF);\n                        playClashSound();\n                        spawnFloatingText(en.x, 2, en.z, '\ud83d\udca8 Knockback!', 'normal');\n                        \n                        // Brief stun\n                        en.stunTimer = 0.3;\n                        en.isSwinging = false;\n                        en.swingPhase = 0;\n                    } else {\n                        // === STANDARD MODE: Normal kill logic ===\n                        // === CHARACTER PASSIVE: Bomber explosion ===\n                        if (player.characterId === 'bomber' && player.bombReady) {\n                            player.bombReady = false;\n                            const explosionRadius = 3;\n                            playDestroySound('gold');\n                            \n                            // Enhanced visual explosion - multiple waves of particles\n                            // Core explosion - bright yellow/white center\n                            for (let p = 0; p < 15; p++) {\n                                const angle = Math.random() * Math.PI * 2;\n                                const dist = Math.random() * 0.8;\n                                spawnParticle3D(\n                                    en.x + Math.cos(angle) * dist,\n                                    1.5 + Math.random() * 1.5,\n                                    en.z + Math.sin(angle) * dist,\n                                    Math.random() > 0.3 ? 0xFFFF00 : 0xFFFFFF\n                                );\n                            }\n                            // Inner ring - orange/red fire\n                            for (let p = 0; p < 25; p++) {\n                                const angle = Math.random() * Math.PI * 2;\n                                const dist = 0.5 + Math.random() * 1.5;\n                                const fireColors = [0xFF0000, 0xFF3300, 0xFF6600, 0xFF9900];\n                                spawnParticle3D(\n                                    en.x + Math.cos(angle) * dist,\n                                    0.8 + Math.random() * 2.5,\n                                    en.z + Math.sin(angle) * dist,\n                                    fireColors[Math.floor(Math.random() * fireColors.length)]\n                                );\n                            }\n                            // Outer ring - orange/smoke\n                            for (let p = 0; p < 20; p++) {\n                                const angle = Math.random() * Math.PI * 2;\n                                const dist = 1.5 + Math.random() * explosionRadius;\n                                const outerColors = [0xFF6600, 0xFFAA00, 0x333333, 0x555555];\n                                spawnParticle3D(\n                                    en.x + Math.cos(angle) * dist,\n                                    0.5 + Math.random() * 2,\n                                    en.z + Math.sin(angle) * dist,\n                                    outerColors[Math.floor(Math.random() * outerColors.length)]\n                                );\n                            }\n                            // Ground debris flying up\n                            for (let p = 0; p < 10; p++) {\n                                const angle = Math.random() * Math.PI * 2;\n                                const dist = Math.random() * explosionRadius * 0.8;\n                                spawnParticle3D(\n                                    en.x + Math.cos(angle) * dist,\n                                    0.2 + Math.random() * 0.5,\n                                    en.z + Math.sin(angle) * dist,\n                                    0x8B4513\n                                );\n                            }\n                            \n                            // Explosion shockwave ring effect\n                            const ringGeo2 = new THREE.RingGeometry(0.1, explosionRadius * 1.2, 32);\n                            const ringMat2 = new THREE.MeshBasicMaterial({ \n                                color: 0xFF6600, \n                                transparent: true, \n                                opacity: 0.8,\n                                side: THREE.DoubleSide\n                            });\n                            const shockwave2 = new THREE.Mesh(ringGeo2, ringMat2);\n                            shockwave2.rotation.x = -Math.PI / 2;\n                            shockwave2.position.set(en.x, 0.1, en.z);\n                            scene.add(shockwave2);\n                            \n                            // Animate shockwave expansion and fade\n                            let shockwaveLife2 = 0.4;\n                            const animateShockwave2 = () => {\n                                shockwaveLife2 -= 0.016;\n                                if (shockwaveLife2 > 0) {\n                                    const progress = 1 - (shockwaveLife2 / 0.4);\n                                    shockwave2.scale.setScalar(1 + progress * 2);\n                                    shockwave2.material.opacity = 0.8 * (1 - progress);\n                                    requestAnimationFrame(animateShockwave2);\n                                } else {\n                                    scene.remove(shockwave2);\n                                }\n                            };\n                            animateShockwave2();\n                            \n                            spawnFloatingText(en.x, 2.5, en.z, '\ud83d\udca5\ud83d\udca5 BOOM! \ud83d\udca5\ud83d\udca5', 'large');\n                            \n                            // Damage nearby objects - increased to 3\n                            for (let j = objects3D.length - 1; j >= 0; j--) {\n                                const nearObj = objects3D[j];\n                                const dist = Math.hypot(nearObj.x - en.x, nearObj.z - en.z);\n                                if (dist < explosionRadius) {\n                                    nearObj.hp -= 3;\n                                    for (let p = 0; p < 8; p++) spawnParticle3D(nearObj.x, 1, nearObj.z, OBJ_DEFS[nearObj.type].color);\n                                }\n                            }\n                            \n                            for (let j = enemies3D.length - 1; j >= 0; j--) {\n                                if (j === i) continue;\n                                const nearEn = enemies3D[j];\n                                if (nearEn.respawning || nearEn.invincible > 0) continue;\n                                const dist = Math.hypot(nearEn.x - en.x, nearEn.z - en.z);\n                                if (dist < explosionRadius) {\n                                    const absorbRate = player.characterId === 'ninja' ? 0.35 : 0.25;\n                                    const gain = nearEn.length * absorbRate * growthMult;\n                                    playerLength = playerLength + gain;\n                                    for (let p = 0; p < 12; p++) spawnParticle3D(nearEn.x, 1, nearEn.z, 0xFF4500);\n                                    spawnFloatingText(nearEn.x, 2, nearEn.z, '+' + gain.toFixed(1) + 'm', 'large');\n                                    createEnemyCorpse(nearEn.x, nearEn.z, nearEn.shirtColor);\n                                    respawnEnemy(nearEn);\n                                    killCount++;\n                                }\n                            }\n                            updatePlayerWeapon();\n                        }\n                        \n                        // One-hit kill! Take 25% of enemy's length (or 35% for ninja)\n                        const absorbRate = player.characterId === 'ninja' ? 0.35 : 0.25;\n                        const gain = en.length * absorbRate * growthMult;\n                        playerLength = playerLength + gain;\n                        playCollectSound();\n                        updatePlayerWeapon();\n                        for (let p = 0; p < 12; p++) spawnParticle3D(en.x, 1, en.z, 0xFF4500);\n                        \n                        // Spawn floating text for enemy kill\n                        spawnFloatingText(en.x, 2, en.z, '+' + gain.toFixed(1) + 'm', 'large');\n                        \n                        // === CHARACTER PASSIVE: Bounty - show coin notification ===\n                        if (player.characterId === 'bounty') {\n                            spawnFloatingText(en.x, 2.8, en.z, '+5 \ud83d\udcb0 Bounty!', 'gold');\n                        }\n                        \n                        // Leave corpse on ground\n                        createEnemyCorpse(en.x, en.z, en.shirtColor);\n                        \n                        // Respawn enemy at random far location\n                        respawnEnemy(en);\n                        killCount++;\n                        playKillSound();\n                    }\n                }\n            }\n        }\n        \n        // Collision detection helper (optimized with early exit)\n        const _collisionCheckDist = 5; // Max check distance for small objects\n        \n        function getCollisionRadius(type) {\n            const def = OBJ_DEFS[type];\n            if (!def) return 0.5;\n            // Different collision sizes based on object type\n            if (type.includes('tree')) return def.size * 0.4; // Trees have trunk collision\n            if (type.includes('stone')) return def.size * 0.5;\n            if (type.includes('crate')) return def.size * 0.45;\n            if (type === 'stump') return 0.4;\n            if (type === 'gold' || type === 'diamond') return def.size * 0.45;\n            return def.size * 0.3; // Small objects like grass, flowers, mushrooms\n        }\n        \n        function checkObjectCollision(x, z, radius, excludeSmall = true) {\n            const nearby = _gridNearby(x, z, _collisionCheckDist + radius);\n            for (const obj of nearby) {\n                const def = OBJ_DEFS[obj.type];\n                if (excludeSmall && def.size < 0.6) continue;\n                const objRadius = getCollisionRadius(obj.type);\n                const dx = x - obj.x;\n                const dz = z - obj.z;\n                const dist = Math.sqrt(dx * dx + dz * dz);\n                const minDist = radius + objRadius;\n                if (dist < minDist && dist > 0.01) {\n                    return { collision: true, pushX: (dx / dist) * (minDist - dist), pushZ: (dz / dist) * (minDist - dist) };\n                }\n            }\n            return { collision: false, pushX: 0, pushZ: 0 };\n        }\n        \n        function checkEnemyCollision(x, z, radius, selfIndex = -1) {\n            for (let i = 0; i < enemies3D.length; i++) {\n                if (i === selfIndex) continue;\n                const en = enemies3D[i];\n                // Skip enemies that are respawning\n                if (en.respawning) continue;\n                \n                // Early distance check\n                const roughDist = Math.abs(x - en.x) + Math.abs(z - en.z);\n                if (roughDist > 3) continue;\n                \n                const dx = x - en.x;\n                const dz = z - en.z;\n                const dist = Math.sqrt(dx * dx + dz * dz);\n                const minDist = radius + 0.6; // Enemy collision radius\n                \n                if (dist < minDist && dist > 0.01) {\n                    const pushX = (dx / dist) * (minDist - dist);\n                    const pushZ = (dz / dist) * (minDist - dist);\n                    return { collision: true, pushX, pushZ };\n                }\n            }\n            return { collision: false, pushX: 0, pushZ: 0 };\n        }\n        \n        function checkPlayerCollision(x, z, radius) {\n            // Skip if player is respawning\n            if (player.respawning) return { collision: false, pushX: 0, pushZ: 0 };\n            const dx = x - player.x;\n            const dz = z - player.z;\n            const dist = Math.sqrt(dx * dx + dz * dz);\n            const minDist = radius + 0.5; // Player collision radius\n            \n            if (dist < minDist && dist > 0.01) {\n                const pushX = (dx / dist) * (minDist - dist);\n                const pushZ = (dz / dist) * (minDist - dist);\n                return { collision: true, pushX, pushZ };\n            }\n            return { collision: false, pushX: 0, pushZ: 0 };\n        }\n        \n        // Update player\n        function updatePlayer(dt) {\n            // Handle respawn animation\n            if (player.respawning) {\n                updatePlayerRespawn(dt);\n                return; // Skip normal updates while respawning\n            }\n            \n            const input = getInputDir();\n            const isMoving = Math.abs(input.dx) > 0.1 || Math.abs(input.dz) > 0.1;\n            \n            // Auto defense when not moving - immediately cancel attack\n            player.defending = !isMoving;\n            \n            // If player stops moving, immediately cancel any ongoing swing\n            if (player.defending && player.isSwinging) {\n                player.isSwinging = false;\n                player.swingPhase = 0;\n                player.swingProgress = 0;\n                player.swingTimer = 0;\n                // Reset arm rotations will be handled by defense pose below\n            }\n            \n            // Defense pose animation - dual-hand vertical guard in front of chest\n            if (player.defending && !player.isSwinging) {\n                const rightArmPivot = playerMesh.userData.rightArmPivot;\n                const leftArmPivot = playerMesh.userData.leftArmPivot;\n                const weaponPivot = playerMesh.userData.weaponPivot;\n\n                // Both hands move inward and forward to hold the blade in front,\n                // with the blade vertical and tip pointing to the sky.\n                const targetRightArmX = -1.2;\n                const targetRightArmY = -0.4;\n                const targetRightArmZ = 0;\n                const targetLeftArmX = -1.2;\n                const targetLeftArmY = 0.4;\n                const targetLeftArmZ = 0;\n\n                const targetWeaponX = -0.4;\n                const targetWeaponY = 0;\n                const targetWeaponZ = 1.57;\n                const targetWeaponPosX = -0.2;\n                const targetWeaponPosY = -0.7;\n                const targetWeaponPosZ = 0.1;\n\n                const poseSpeed = 10;\n                rightArmPivot.rotation.x += (targetRightArmX - rightArmPivot.rotation.x) * poseSpeed * dt;\n                rightArmPivot.rotation.y += (targetRightArmY - rightArmPivot.rotation.y) * poseSpeed * dt;\n                rightArmPivot.rotation.z += (targetRightArmZ - rightArmPivot.rotation.z) * poseSpeed * dt;\n\n                if (leftArmPivot) {\n                    leftArmPivot.rotation.x += (targetLeftArmX - leftArmPivot.rotation.x) * poseSpeed * dt;\n                    leftArmPivot.rotation.y += (targetLeftArmY - leftArmPivot.rotation.y) * poseSpeed * dt;\n                    leftArmPivot.rotation.z += (targetLeftArmZ - leftArmPivot.rotation.z) * poseSpeed * dt;\n                }\n\n                weaponPivot.rotation.x += (targetWeaponX - weaponPivot.rotation.x) * poseSpeed * dt;\n                weaponPivot.rotation.y += (targetWeaponY - weaponPivot.rotation.y) * poseSpeed * dt;\n                weaponPivot.rotation.z += (targetWeaponZ - weaponPivot.rotation.z) * poseSpeed * dt;\n                weaponPivot.position.x += (targetWeaponPosX - weaponPivot.position.x) * poseSpeed * dt;\n                weaponPivot.position.y += (targetWeaponPosY - weaponPivot.position.y) * poseSpeed * dt;\n                weaponPivot.position.z += (targetWeaponPosZ - weaponPivot.position.z) * poseSpeed * dt;\n            }\n            \n            const lengthPenalty = Math.max(0.5, 1 - playerLength / 200);\n            let speed = PLAYER_SPEED * lengthPenalty;\n            \n            // === CHARACTER PASSIVE: Slider speed boost ===\n            if (player.characterId === 'slider') {\n                if (isMoving) {\n                    player.moveTime += dt;\n                    // Speed gradually increases from 0% to 100% over 5 seconds of continuous movement\n                    player.speedBoost = Math.min(player.moveTime / 5, 1.0);\n                } else {\n                    // Speed boost decays over 1 second when stopped\n                    player.speedBoost = Math.max(0, player.speedBoost - dt);\n                    player.moveTime = Math.max(0, player.moveTime - dt * 5); // Reset moveTime proportionally\n                }\n                speed *= (1 + player.speedBoost);\n            }\n            \n            // === CHARACTER PASSIVE: Guardian speed boost after successful block ===\n            if (player.characterId === 'guardian' && player.guardianSpeedTimer > 0) {\n                player.guardianSpeedTimer -= dt;\n                if (player.guardianSpeedTimer <= 0) {\n                    player.guardianSpeedBoost = 0;\n                    player.guardianSpeedTimer = 0;\n                }\n                speed *= (1 + player.guardianSpeedBoost);\n            }\n            \n            // === CHARACTER PASSIVE: Reckless - no defense ===\n            if (player.characterId === 'reckless') {\n                player.defending = false; // Never defend\n            }\n            \n            // Apply knockback velocity with decay\n            if (Math.abs(player.knockbackVX) > 0.1 || Math.abs(player.knockbackVZ) > 0.1) {\n                player.x += player.knockbackVX * dt;\n                player.z += player.knockbackVZ * dt;\n                // Decay knockback velocity (friction) - slower decay for longer sliding\n                const knockbackDecay = 0.92;\n                player.knockbackVX *= Math.pow(knockbackDecay, dt * 60);\n                player.knockbackVZ *= Math.pow(knockbackDecay, dt * 60);\n            } else {\n                player.knockbackVX = 0;\n                player.knockbackVZ = 0;\n            }\n            \n            // Calculate new position\n            let newX = player.x + input.dx * speed * dt;\n            let newZ = player.z + input.dz * speed * dt;\n            \n            // Check collision with objects\n            const objCol = checkObjectCollision(newX, newZ, 0.5);\n            if (objCol.collision) {\n                newX += objCol.pushX;\n                newZ += objCol.pushZ;\n            }\n            \n            // Check collision with enemies\n            const enCol = checkEnemyCollision(newX, newZ, 0.5);\n            if (enCol.collision) {\n                newX += enCol.pushX;\n                newZ += enCol.pushZ;\n            }\n            \n            player.x = newX;\n            player.z = newZ;\n            \n            // Bounds\n            player.x = Math.max(-ARENA_SIZE + 2, Math.min(ARENA_SIZE - 2, player.x));\n            player.z = Math.max(-ARENA_SIZE + 2, Math.min(ARENA_SIZE - 2, player.z));\n            \n            // Update facing direction when moving\n            if (isMoving) {\n                player.dir = Math.atan2(input.dx, input.dz);\n            }\n            \n            // Update mesh\n            playerMesh.position.set(player.x, 0, player.z);\n            playerMesh.rotation.y = player.dir;\n            \n            // === CHARACTER PASSIVE: Ninja afterimage ===\n            if (player.characterId === 'ninja' && isMoving) {\n                const now = Date.now();\n                if (now - lastAfterimageTime > 100) { // Spawn every 100ms\n                    lastAfterimageTime = now;\n                    spawnNinjaAfterimage(player.x, player.z, player.dir);\n                }\n            }\n            \n            // Leg walking animation\n            const leftLegPivot = playerMesh.userData.leftLegPivot;\n            const rightLegPivot = playerMesh.userData.rightLegPivot;\n            if (isMoving && leftLegPivot && rightLegPivot) {\n                // Advance walk cycle based on movement speed\n                const walkSpeed = 12; // Animation speed\n                player.walkCycle += dt * walkSpeed;\n                \n                // Swing legs back and forth\n                const legSwing = Math.sin(player.walkCycle) * 0.6; // Amplitude of leg swing\n                leftLegPivot.rotation.x = legSwing;\n                rightLegPivot.rotation.x = -legSwing; // Opposite phase\n                \n                // Play footstep sound at leg swing peaks\n                if (Math.abs(legSwing) > 0.55) {\n                    playFootstep();\n                }\n            } else if (leftLegPivot && rightLegPivot) {\n                // Return legs to neutral when not moving\n                leftLegPivot.rotation.x *= 0.85;\n                rightLegPivot.rotation.x *= 0.85;\n            }\n            \n            // Auto swing only when moving (or always if reckless)\n            const shouldSwing = isMoving || player.characterId === 'reckless';\n            let swingCooldown = Math.max(0.08, 0.08 + playerLength / 60);\n            \n            // === CHARACTER PASSIVE: Reckless +20% attack speed ===\n            if (player.characterId === 'reckless') {\n                swingCooldown *= 0.8; // 20% faster = 80% of original cooldown\n            }\n            \n            if (shouldSwing && !player.isSwinging) {\n                player.swingTimer += dt;\n            }\n            \n            // Return arm to neutral when moving but not swinging (exit defense pose)\n            if (shouldSwing && !player.isSwinging) {\n                const rightArmPivot = playerMesh.userData.rightArmPivot;\n                const leftArmPivot = playerMesh.userData.leftArmPivot;\n                const weaponPivot = playerMesh.userData.weaponPivot;\n                const returnSpeed = 10;\n                rightArmPivot.rotation.x += (0 - rightArmPivot.rotation.x) * returnSpeed * dt;\n                rightArmPivot.rotation.y += (0 - rightArmPivot.rotation.y) * returnSpeed * dt;\n                rightArmPivot.rotation.z += (0 - rightArmPivot.rotation.z) * returnSpeed * dt;\n\n                if (leftArmPivot) {\n                    leftArmPivot.rotation.x += (0 - leftArmPivot.rotation.x) * returnSpeed * dt;\n                    leftArmPivot.rotation.y += (0 - leftArmPivot.rotation.y) * returnSpeed * dt;\n                    leftArmPivot.rotation.z += (0 - leftArmPivot.rotation.z) * returnSpeed * dt;\n                }\n\n                weaponPivot.rotation.x += (0 - weaponPivot.rotation.x) * returnSpeed * dt;\n                weaponPivot.rotation.y += (0 - weaponPivot.rotation.y) * returnSpeed * dt;\n                weaponPivot.rotation.z += (0 - weaponPivot.rotation.z) * returnSpeed * dt;\n                weaponPivot.position.x += (0 - weaponPivot.position.x) * returnSpeed * dt;\n                weaponPivot.position.y += (-0.7 - weaponPivot.position.y) * returnSpeed * dt;\n                weaponPivot.position.z += (0.1 - weaponPivot.position.z) * returnSpeed * dt;\n            }\n            \n            // Start swing when moving (or always if reckless)\n            if (!player.isSwinging && player.swingTimer >= swingCooldown && shouldSwing) {\n                player.isSwinging = true;\n                player.swingTimer = 0;\n                player.swingPhase = 1; // Start windup\n                player.swingProgress = 0;\n                player.hasHitThisSwing = false;\n                playSwingSound(playerLength);\n                \n                // === CHARACTER PASSIVE: Bomber swing counter ===\n                if (player.characterId === 'bomber') {\n                    if (player.attackCount >= 5 && !player.bombReady) {\n                        player.bombReady = true;\n                        player.attackCount = 0;\n                    }\n                }\n            }\n            \n            // Animated swing phases - arm raises and slashes down\n            if (player.isSwinging) {\n                const rightArmPivot = playerMesh.userData.rightArmPivot;\n                const leftArmPivot = playerMesh.userData.leftArmPivot;\n                const weaponPivot = playerMesh.userData.weaponPivot;\n                let swingSpeedMult = Math.max(0.4, 1.6 - playerLength / 80); // Longer = slower\n                \n                // === CHARACTER PASSIVE: Reckless +20% attack speed ===\n                if (player.characterId === 'reckless') {\n                    swingSpeedMult *= 1.25; // 20% faster swing animation\n                }\n                \n                player.swingProgress += dt * swingSpeedMult;\n                \n                if (leftArmPivot) {\n                    leftArmPivot.rotation.x += (0 - leftArmPivot.rotation.x) * 15 * dt;\n                    leftArmPivot.rotation.y += (0 - leftArmPivot.rotation.y) * 15 * dt;\n                    leftArmPivot.rotation.z += (0 - leftArmPivot.rotation.z) * 15 * dt;\n                }\n                \n                if (player.swingPhase === 1) {\n                    // Windup phase - raise arm up and back\n                    const windupDuration = 0.7;\n                    const t = Math.min(player.swingProgress / windupDuration, 1);\n                    const easeOut = 1 - Math.pow(1 - t, 2);\n                    \n                    // Arm rotates backward (raising weapon above head)\n                    rightArmPivot.rotation.x = -2.5 * easeOut; // Arm goes up and back\n                    rightArmPivot.rotation.z = -0.3 * easeOut; // Slight outward angle\n                    weaponPivot.rotation.x = 0.5 * easeOut; // Weapon tilts back\n                    \n                    if (t >= 1) {\n                        player.swingPhase = 2;\n                        player.swingProgress = 0;\n                    }\n                } else if (player.swingPhase === 2) {\n                    // Slash phase - arm swings down fast in front\n                    const slashDuration = 0.12;\n                    const t = Math.min(player.swingProgress / slashDuration, 1);\n                    const easeIn = t * t; // Accelerate\n                    \n                    // Arm swings from raised position down to front\n                    rightArmPivot.rotation.x = -2.5 + 3.5 * easeIn; // Swing down and forward\n                    rightArmPivot.rotation.z = -0.3 * (1 - easeIn); // Return to center\n                    weaponPivot.rotation.x = 0.5 - 1.0 * easeIn; // Weapon follows through\n                    \n                    // Create slash trail during swing\n                    if (t > 0.2 && t < 0.9) {\n                        const weaponLen = playerLength * 0.5;\n                        const trailX = player.x + Math.sin(player.dir) * weaponLen * 0.6;\n                        const trailZ = player.z + Math.cos(player.dir) * weaponLen * 0.6;\n                        const trailColor = playerLength >= 75 ? 0x00FFFF : \n                                          playerLength >= 50 ? 0xFFD700 : \n                                          playerLength >= 10 ? 0xCCCCCC : 0xA0522D;\n                        createSlashTrail(trailX, 1.2, trailZ, player.dir, weaponLen, trailColor);\n                    }\n                    \n                    // Check hits at peak of swing\n                    if (t >= 0.8 && !player.hasHitThisSwing) {\n                        checkPlayerHits();\n                        player.hasHitThisSwing = true;\n                    }\n                    \n                    if (t >= 1) {\n                        player.swingPhase = 3;\n                        player.swingProgress = 0;\n                    }\n                } else if (player.swingPhase === 3) {\n                    // Recovery phase - arm returns to idle\n                    const recoveryDuration = 0.15;\n                    const t = Math.min(player.swingProgress / recoveryDuration, 1);\n                    const easeOut = 1 - Math.pow(1 - t, 3);\n                    \n                    // Arm returns to resting position\n                    rightArmPivot.rotation.x = 1.0 * (1 - easeOut);\n                    rightArmPivot.rotation.z = 0;\n                    weaponPivot.rotation.x = -0.5 * (1 - easeOut);\n                    \n                    if (t >= 1) {\n                        player.isSwinging = false;\n                        player.swingPhase = 0;\n                        player.swingProgress = 0;\n                        rightArmPivot.rotation.x = 0;\n                        rightArmPivot.rotation.y = 0;\n                        rightArmPivot.rotation.z = 0;\n                        weaponPivot.rotation.x = 0;\n                        weaponPivot.rotation.y = 0;\n                        weaponPivot.rotation.z = 0;\n                        weaponPivot.position.set(0, -0.7, 0.1);\n                        \n                        // === CHARACTER PASSIVE: Bomber swing counter increment ===\n                        if (player.characterId === 'bomber' && !player.bombReady) {\n                            player.attackCount = (player.attackCount || 0) + 1;\n                        }\n                    }\n                }\n            }\n            \n            if (player.invincible > 0) {\n                player.invincible -= dt;\n                // Flashing invincibility effect\n                playerMesh.visible = Math.floor(Date.now() * 0.01) % 2 === 0;\n            } else {\n                playerMesh.visible = true;\n            }\n        }\n        \n        // Update enemy weapon when length changes significantly\n        function updateEnemyWeapon(en) {\n            if (en.weapon) {\n                en.mesh.userData.weaponPivot.remove(en.weapon);\n            }\n            en.weapon = createWeapon(en.length, getWeaponMaterial(en.length));\n            en.mesh.userData.weaponPivot.add(en.weapon);\n        }\n        \n        // Start enemy death/respawn sequence\n        function startEnemyRespawn(en) {\n            // Save death position for animation\n            en.deathX = en.x;\n            en.deathZ = en.z;\n            \n            // Calculate new spawn position\n            const edge = Math.floor(Math.random() * 4);\n            const edgeDist = 42 + Math.random() * 6;\n            \n            if (edge === 0) {\n                en.respawnX = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                en.respawnZ = -edgeDist;\n            } else if (edge === 1) {\n                en.respawnX = edgeDist;\n                en.respawnZ = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n            } else if (edge === 2) {\n                en.respawnX = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                en.respawnZ = edgeDist;\n            } else {\n                en.respawnX = -edgeDist;\n                en.respawnZ = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n            }\n            \n            // Start respawn animation\n            en.respawning = true;\n            en.respawnTimer = 0;\n            en.isSwinging = false;\n            en.swingPhase = 0;\n            en.knockbackVX = 0;\n            en.knockbackVZ = 0;\n            en.stunTimer = 0;\n            \n            // Reduce length by 25% (victim loses 25%, killer already gained 25%)\n            en.length = Math.max(config.startLength || 2, en.length * 0.75);\n            updateEnemyWeapon(en);\n        }\n        \n        // Update enemy respawn animation\n        function updateEnemyRespawn(en, dt) {\n            const DEATH_DURATION = 0.5;   // Death fall animation\n            const RESPAWN_DURATION = 0.4; // Respawn rise animation\n            const TOTAL_DURATION = DEATH_DURATION + RESPAWN_DURATION;\n            \n            en.respawnTimer += dt;\n            const t = en.respawnTimer;\n            \n            if (t < DEATH_DURATION) {\n                // Death animation - character falls down and fades\n                const deathProgress = t / DEATH_DURATION;\n                const easeIn = deathProgress * deathProgress;\n                \n                // Character falls down (rotate forward)\n                en.mesh.rotation.x = easeIn * Math.PI / 2;\n                // Sink into ground\n                en.mesh.position.y = -easeIn * 0.5;\n                // Fade out (scale down)\n                const fadeScale = 1 - easeIn * 0.4;\n                en.mesh.scale.setScalar(fadeScale);\n                \n                // Keep visible during death animation\n                en.mesh.visible = true;\n            } else if (t < TOTAL_DURATION) {\n                // Teleport to new position at start of respawn phase\n                if (t - dt < DEATH_DURATION) {\n                    // Just crossed into respawn phase - teleport\n                    en.x = en.respawnX;\n                    en.z = en.respawnZ;\n                    en.dir = Math.random() * Math.PI * 2;\n                    en.mesh.position.set(en.x, 0, en.z);\n                    en.mesh.rotation.y = en.dir;\n                    en.mesh.rotation.x = -Math.PI / 4; // Start tilted back\n                    en.mesh.visible = true;\n                    \n                    // Spawn respawn particles at new location\n                    for (let p = 0; p < 6; p++) {\n                        spawnParticle3D(en.x, 1.5, en.z, en.shirtColor);\n                    }\n                }\n                \n                // Respawn animation - character rises up\n                const respawnProgress = (t - DEATH_DURATION) / RESPAWN_DURATION;\n                const easeOut = 1 - Math.pow(1 - respawnProgress, 2);\n                \n                // Rise up and straighten\n                en.mesh.rotation.x = -Math.PI / 4 * (1 - easeOut);\n                en.mesh.position.y = 0;\n                // Scale back to normal\n                const riseScale = 0.6 + easeOut * 0.4;\n                en.mesh.scale.setScalar(riseScale);\n                \n                // Flashing effect\n                en.mesh.visible = Math.floor(t * 8) % 2 === 0;\n            } else {\n                // Respawn complete\n                en.respawning = false;\n                en.invincible = 0.6; // Brief invincibility\n                en.mesh.rotation.x = 0;\n                en.mesh.position.y = 0;\n                en.mesh.scale.setScalar(1);\n                en.mesh.visible = true;\n                \n                // Reset arm rotation\n                const rightArmPivot = en.mesh.userData.rightArmPivot;\n                if (rightArmPivot) {\n                    rightArmPivot.rotation.x = 0;\n                    rightArmPivot.rotation.y = 0;\n                    rightArmPivot.rotation.z = 0;\n                }\n            }\n        }\n        \n        // Legacy respawnEnemy function - now starts animation\n        function respawnEnemy(en) {\n            startEnemyRespawn(en);\n        }\n        \n        // Start player death/respawn sequence\n        function startPlayerRespawn() {\n            // Save death position for animation\n            player.deathX = player.x;\n            player.deathZ = player.z;\n            \n            // Calculate new spawn position\n            const edge = Math.floor(Math.random() * 4);\n            const edgeDist = 42 + Math.random() * 6;\n            \n            if (edge === 0) {\n                player.respawnX = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                player.respawnZ = -edgeDist;\n            } else if (edge === 1) {\n                player.respawnX = edgeDist;\n                player.respawnZ = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n            } else if (edge === 2) {\n                player.respawnX = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                player.respawnZ = edgeDist;\n            } else {\n                player.respawnX = -edgeDist;\n                player.respawnZ = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n            }\n            \n            // Start respawn animation\n            player.respawning = true;\n            player.respawnTimer = 0;\n            player.isSwinging = false;\n            player.swingPhase = 0;\n            player.knockbackVX = 0;\n            player.knockbackVZ = 0;\n            \n            // Reset slider speed boost on death\n            player.moveTime = 0;\n            player.speedBoost = 0;\n            \n            // Reset guardian speed boost on death\n            player.guardianSpeedBoost = 0;\n            player.guardianSpeedTimer = 0;\n            \n            // Reduce length by 25% (victim loses 25%, killer already gained 25%)\n            playerLength = Math.max(config.startLength || 2, playerLength * 0.75);\n            updatePlayerWeapon();\n        }\n        \n        // Update player respawn animation\n        function updatePlayerRespawn(dt) {\n            const DEATH_DURATION = 0.6;   // Death fall animation\n            const RESPAWN_DURATION = 0.5; // Respawn rise animation\n            const TOTAL_DURATION = DEATH_DURATION + RESPAWN_DURATION;\n            \n            player.respawnTimer += dt;\n            const t = player.respawnTimer;\n            \n            if (t < DEATH_DURATION) {\n                // Death animation - character falls down and fades\n                const deathProgress = t / DEATH_DURATION;\n                const easeIn = deathProgress * deathProgress;\n                \n                // Character falls down (rotate forward)\n                playerMesh.rotation.x = easeIn * Math.PI / 2;\n                // Sink into ground\n                playerMesh.position.y = -easeIn * 0.5;\n                // Fade out (scale down slightly)\n                const fadeScale = 1 - easeIn * 0.3;\n                playerMesh.scale.setScalar(fadeScale);\n                \n                // Keep visible during death animation\n                playerMesh.visible = true;\n            } else if (t < TOTAL_DURATION) {\n                // Teleport to new position at start of respawn phase\n                if (t - dt < DEATH_DURATION) {\n                    // Just crossed into respawn phase - teleport\n                    player.x = player.respawnX;\n                    player.z = player.respawnZ;\n                    player.dir = Math.random() * Math.PI * 2;\n                    playerMesh.position.set(player.x, 0, player.z);\n                    playerMesh.rotation.y = player.dir;\n                    playerMesh.rotation.x = -Math.PI / 4; // Start tilted back\n                    playerMesh.visible = true;\n                    \n                    // Spawn respawn particles at new location\n                    for (let p = 0; p < 8; p++) {\n                        spawnParticle3D(player.x, 1.5, player.z, 0x44AAFF);\n                    }\n                    spawnFloatingText(player.x, 2.5, player.z, '\u26a1 RESPAWN!', 'normal');\n                }\n                \n                // Respawn animation - character rises up\n                const respawnProgress = (t - DEATH_DURATION) / RESPAWN_DURATION;\n                const easeOut = 1 - Math.pow(1 - respawnProgress, 2);\n                \n                // Rise up and straighten\n                playerMesh.rotation.x = -Math.PI / 4 * (1 - easeOut);\n                playerMesh.position.y = 0;\n                // Scale back to normal\n                const riseScale = 0.7 + easeOut * 0.3;\n                playerMesh.scale.setScalar(riseScale);\n                \n                // Flashing invincibility effect\n                if (Math.floor(t * 10) % 2 === 0) {\n                    playerMesh.visible = true;\n                } else {\n                    playerMesh.visible = false;\n                }\n            } else {\n                // Respawn complete\n                player.respawning = false;\n                player.invincible = 0.6; // Brief invincibility after respawn\n                playerMesh.rotation.x = 0;\n                playerMesh.position.y = 0;\n                playerMesh.scale.setScalar(1);\n                playerMesh.visible = true;\n                \n                // Reset arm rotations\n                const rightArmPivot = playerMesh.userData.rightArmPivot;\n                const leftArmPivot = playerMesh.userData.leftArmPivot;\n                const weaponPivot = playerMesh.userData.weaponPivot;\n                if (rightArmPivot) {\n                    rightArmPivot.rotation.x = 0;\n                    rightArmPivot.rotation.y = 0;\n                    rightArmPivot.rotation.z = 0;\n                }\n                if (leftArmPivot) {\n                    leftArmPivot.rotation.x = 0;\n                    leftArmPivot.rotation.y = 0;\n                    leftArmPivot.rotation.z = 0;\n                }\n                if (weaponPivot) {\n                    weaponPivot.rotation.x = 0;\n                    weaponPivot.rotation.y = 0;\n                    weaponPivot.rotation.z = 0;\n                    weaponPivot.position.set(0, -0.7, 0.1);\n                }\n            }\n        }\n        \n        // Legacy respawnPlayer function - now just starts the animation\n        function respawnPlayer() {\n            startPlayerRespawn();\n            // === CHARACTER PASSIVE: Knight - restore shield on respawn ===\n            if (player.characterId === 'knight') {\n                player.hasShield = true;\n                // Recreate shield visual if needed\n                if (!knightShieldMesh) {\n                    knightShieldMesh = createKnightShieldEffect();\n                    scene.add(knightShieldMesh);\n                }\n            }\n        }\n        \n        // Track last weapon tier for each enemy\n        function getWeaponTier(length) {\n            if (length >= 75) return 4;\n            if (length >= 50) return 3;\n            if (length >= 10) return 2;\n            return 1;\n        }\n        \n        // Update enemies\n        function updateEnemies(dt) {\n            enemies3D.forEach((en, enIndex) => {\n                // Handle respawn animation\n                if (en.respawning) {\n                    updateEnemyRespawn(en, dt);\n                    return; // Skip normal updates while respawning\n                }\n                \n                // Update invincibility\n                if (en.invincible > 0) {\n                    en.invincible -= dt;\n                    // Flashing invincibility effect\n                    en.mesh.visible = Math.floor(Date.now() * 0.01) % 2 === 0;\n                } else {\n                    en.mesh.visible = true;\n                }\n                \n                // Update stun timer\n                if (en.stunTimer > 0) {\n                    en.stunTimer -= dt;\n                    // Stunned visual - flash yellow and shake\n                    const flashIntensity = Math.sin(Date.now() * 0.02) * 0.5 + 0.5;\n                    en.mesh.position.x = en.x + Math.sin(Date.now() * 0.05) * 0.1;\n                    en.mesh.position.z = en.z + Math.cos(Date.now() * 0.05) * 0.1;\n                    // Skip all actions when stunned\n                    return;\n                }\n                \n                en.ai.retargetTimer -= dt;\n                if (en.ai.retargetTimer <= 0) {\n                    en.ai.retargetTimer = 1.5 + Math.random() * 2;\n                    \n                    // Smarter AI targeting based on behavior and situation\n                    const distToPlayer = Math.hypot(player.x - en.x, player.z - en.z);\n                    const isStronger = en.length > playerLength * 1.2;\n                    const isWeaker = en.length < playerLength * 0.7;\n                    \n                    // Dynamically adjust behavior\n                    if (isStronger && Math.random() < 0.7) {\n                        en.ai.behavior = 'chase'; // Hunt player when stronger\n                    } else if (isWeaker && Math.random() < 0.6) {\n                        en.ai.behavior = 'gather'; // Farm when weaker\n                    }\n                    \n                    if (en.ai.behavior === 'chase' && Math.random() < 0.6) {\n                        en.ai.targetX = player.x + (Math.random()-0.5)*5;\n                        en.ai.targetZ = player.z + (Math.random()-0.5)*5;\n                    } else {\n                        // Prioritize higher value targets\n                        let bestTarget = null, bestScore = -Infinity;\n                        objects3D.forEach(o => {\n                            const d = Math.hypot(o.x - en.x, o.z - en.z);\n                            const def = OBJ_DEFS[o.type];\n                            // Score based on value and distance\n                            const valueScore = def.lengthGain * 10;\n                            const distScore = -d * 0.5;\n                            const score = valueScore + distScore;\n                            if (score > bestScore) { bestScore = score; bestTarget = o; }\n                        });\n                        if (bestTarget) {\n                            en.ai.targetX = bestTarget.x;\n                            en.ai.targetZ = bestTarget.z;\n                        } else {\n                            en.ai.targetX = (Math.random()-0.5) * ARENA_SIZE * 1.5;\n                            en.ai.targetZ = (Math.random()-0.5) * ARENA_SIZE * 1.5;\n                        }\n                    }\n                }\n                \n                const dx = en.ai.targetX - en.x;\n                const dz = en.ai.targetZ - en.z;\n                const dist = Math.sqrt(dx*dx + dz*dz);\n                \n                // AI decides to defend when idle or near dangerous player\n                const distToPlayer = Math.hypot(player.x - en.x, player.z - en.z);\n                const playerIsAttacking = player.isSwinging && player.swingPhase === 2;\n                const shouldDefend = (dist < 0.5 && Math.random() < 0.3) || \n                                    (distToPlayer < playerLength * 0.6 && playerIsAttacking && Math.random() < 0.5);\n                \n                if (shouldDefend && !en.isSwinging) {\n                    en.defending = true;\n                    en.ai.idleTimer += dt;\n                } else {\n                    en.defending = false;\n                    en.ai.idleTimer = 0;\n                }\n                // Apply knockback velocity with decay for enemy\n                if (Math.abs(en.knockbackVX) > 0.1 || Math.abs(en.knockbackVZ) > 0.1) {\n                    en.x += en.knockbackVX * dt;\n                    en.z += en.knockbackVZ * dt;\n                    // Decay knockback velocity - slower decay for longer sliding\n                    const knockbackDecay = 0.92;\n                    en.knockbackVX *= Math.pow(knockbackDecay, dt * 60);\n                    en.knockbackVZ *= Math.pow(knockbackDecay, dt * 60);\n                } else {\n                    en.knockbackVX = 0;\n                    en.knockbackVZ = 0;\n                }\n                \n                // Same movement speed penalty as player based on weapon length\n                const lengthPenalty = Math.max(0.5, 1 - en.length / 200);\n                const currentSpeed = en.baseSpeed * lengthPenalty;\n                \n                // Defense pose animation for AI\n                if (en.defending && !en.isSwinging) {\n                    const rightArmPivot = en.mesh.userData.rightArmPivot;\n                    const leftArmPivot = en.mesh.userData.leftArmPivot;\n                    const weaponPivot = en.mesh.userData.weaponPivot;\n                    const poseSpeed = 10;\n                    \n                    rightArmPivot.rotation.x += (-1.2 - rightArmPivot.rotation.x) * poseSpeed * dt;\n                    rightArmPivot.rotation.y += (-0.4 - rightArmPivot.rotation.y) * poseSpeed * dt;\n                    if (leftArmPivot) {\n                        leftArmPivot.rotation.x += (-1.2 - leftArmPivot.rotation.x) * poseSpeed * dt;\n                        leftArmPivot.rotation.y += (0.4 - leftArmPivot.rotation.y) * poseSpeed * dt;\n                    }\n                    weaponPivot.rotation.x += (-0.4 - weaponPivot.rotation.x) * poseSpeed * dt;\n                    weaponPivot.rotation.z += (1.57 - weaponPivot.rotation.z) * poseSpeed * dt;\n                }\n                \n                if (dist > 0.5 && !en.defending) {\n                    let newX = en.x + (dx / dist) * currentSpeed * dt;\n                    let newZ = en.z + (dz / dist) * currentSpeed * dt;\n                    \n                    // Check collision with objects\n                    const objCol = checkObjectCollision(newX, newZ, 0.6, true);\n                    if (objCol.collision) {\n                        newX += objCol.pushX;\n                        newZ += objCol.pushZ;\n                    }\n                    \n                    // Check collision with other enemies\n                    const idx = enemies3D.indexOf(en);\n                    const enCol = checkEnemyCollision(newX, newZ, 0.6, idx);\n                    if (enCol.collision) {\n                        newX += enCol.pushX;\n                        newZ += enCol.pushZ;\n                    }\n                    \n                    // Check collision with player\n                    const plCol = checkPlayerCollision(newX, newZ, 0.6);\n                    if (plCol.collision) {\n                        newX += plCol.pushX;\n                        newZ += plCol.pushZ;\n                    }\n                    \n                    en.x = newX;\n                    en.z = newZ;\n                    en.dir = Math.atan2(dx, dz);\n                }\n                \n                en.x = Math.max(-ARENA_SIZE + 2, Math.min(ARENA_SIZE - 2, en.x));\n                en.z = Math.max(-ARENA_SIZE + 2, Math.min(ARENA_SIZE - 2, en.z));\n                \n                en.mesh.position.set(en.x, 0, en.z);\n                en.mesh.rotation.y = en.dir;\n                \n                // Enemy leg walking animation\n                const enLeftLeg = en.mesh.userData.leftLegPivot;\n                const enRightLeg = en.mesh.userData.rightLegPivot;\n                if (dist > 0.5 && !en.defending && enLeftLeg && enRightLeg) {\n                    // Walking - animate legs\n                    en.walkCycle += dt * 10;\n                    const legSwing = Math.sin(en.walkCycle) * 0.5;\n                    enLeftLeg.rotation.x = legSwing;\n                    enRightLeg.rotation.x = -legSwing;\n                } else if (enLeftLeg && enRightLeg) {\n                    // Standing still - return to neutral\n                    enLeftLeg.rotation.x *= 0.85;\n                    enRightLeg.rotation.x *= 0.85;\n                }\n                \n                // Same swing cooldown as player based on weapon length\n                const swingCooldown = Math.max(0.08, 0.08 + en.length / 60);\n                \n                // Only auto swing when not defending and moving toward target\n                if (!en.defending && dist > 0.5) {\n                    en.swingTimer += dt;\n                }\n                \n                if (en.swingTimer >= swingCooldown && !en.isSwinging && !en.defending) {\n                    en.isSwinging = true;\n                    en.swingTimer = 0;\n                    en.swingPhase = 1;\n                    en.swingProgress = 0;\n                    en.hasHitThisSwing = false;\n                    en.hasHitThisSwingPlayer = false;\n                }\n                \n                if (en.isSwinging) {\n                    const rightArmPivot = en.mesh.userData.rightArmPivot;\n                    const weaponPivot = en.mesh.userData.weaponPivot;\n                    \n                    \n                    // Same swing timing as player based on weapon length\n                    const swingSpeedMult = Math.max(0.4, 1.6 - en.length / 80);\n                    en.swingProgress += dt * swingSpeedMult;\n                    \n                    if (en.swingPhase === 1) {\n                        // Windup - raise arm (same as player)\n                        const windupDuration = 0.7;\n                        const t = Math.min(en.swingProgress / windupDuration, 1);\n                        const easeOut = 1 - Math.pow(1 - t, 2);\n                        rightArmPivot.rotation.x = -2.5 * easeOut;\n                        rightArmPivot.rotation.z = -0.3 * easeOut;\n                        if (t >= 1) { en.swingPhase = 2; en.swingProgress = 0; }\n                    } else if (en.swingPhase === 2) {\n                        // Slash - arm swings down (same as player)\n                        const slashDuration = 0.12;\n                        const t = Math.min(en.swingProgress / slashDuration, 1);\n                        const easeIn = t * t;\n                        rightArmPivot.rotation.x = -2.5 + 3.5 * easeIn;\n                        rightArmPivot.rotation.z = -0.3 * (1 - easeIn);\n                        \n                        if (t >= 0.8 && !en.hasHitThisSwing) {\n                            enemyAttack(en, enIndex);\n                            en.hasHitThisSwing = true;\n                        }\n                        if (t >= 1) { en.swingPhase = 3; en.swingProgress = 0; }\n                    } else if (en.swingPhase === 3) {\n                        // Recovery - arm returns (same as player)\n                        const recoveryDuration = 0.15;\n                        const t = Math.min(en.swingProgress / recoveryDuration, 1);\n                        const easeOut = 1 - Math.pow(1 - t, 3);\n                        rightArmPivot.rotation.x = 1.0 * (1 - easeOut);\n                        rightArmPivot.rotation.z = 0;\n                        if (t >= 1) {\n                            en.isSwinging = false;\n                            en.swingPhase = 0;\n                            rightArmPivot.rotation.x = 0;\n                            rightArmPivot.rotation.z = 0;\n                        }\n                    }\n                }\n                \n                // Check if weapon visual needs update (when tier changes)\n                if (!en.lastWeaponTier) en.lastWeaponTier = getWeaponTier(en.length);\n                const currentTier = getWeaponTier(en.length);\n                if (currentTier !== en.lastWeaponTier) {\n                    en.lastWeaponTier = currentTier;\n                    updateEnemyWeapon(en);\n                }\n                \n                // Check if enemy hits player (skip if weapons just clashed or player is respawning)\n                const enClashCooldown = enemyClashCooldowns.get(en) || 0;\n                let isActiveHitFrame = false;\n                if (en.hasHitThisSwing && !en.hasHitThisSwingPlayer) {\n                    isActiveHitFrame = true;\n                    en.hasHitThisSwingPlayer = true;\n                }\n                \n                if (isActiveHitFrame && player.invincible <= 0 && enClashCooldown <= 0 && !player.respawning) {\n                    const weaponLen = en.length * 0.5;\n                    const bladeWidth = 0.55 + Math.min(en.length / 50, 2.5);\n                    const eDirX = Math.sin(en.dir);\n                    const eDirZ = Math.cos(en.dir);\n                    const perpX = Math.cos(en.dir);\n                    const perpZ = -Math.sin(en.dir);\n                    \n                    // First, check if AI weapon hits the player's collision circle\n                    let attackHitsPlayer = false;\n                    for (let d = 0.3; d <= weaponLen && !attackHitsPlayer; d += 0.4) {\n                        const widthAtD = bladeWidth * (0.7 + (d / weaponLen) * 0.5);\n                        for (let w = -widthAtD / 2; w <= widthAtD / 2 && !attackHitsPlayer; w += widthAtD / 2) {\n                            const px = en.x + eDirX * d + perpX * w;\n                            const pz = en.z + eDirZ * d + perpZ * w;\n                            if (Math.hypot(px - player.x, pz - player.z) < 1.0) {\n                                attackHitsPlayer = true;\n                            }\n                        }\n                    }\n                    \n                    if (attackHitsPlayer) {\n                        // AI miss chance - dynamic based on cumulative coins\n                        // Base 70%, decreases 5% per 100 coins earned, min 0%\n                        const totalCoins = getPlayerCoins();\n                        const dynamicMissPercent = Math.max(0, 70 - Math.floor(totalCoins / 100) * 5);\n                        const missChance = dynamicMissPercent / 100;\n                        if (Math.random() < missChance) {\n                            // Attack missed! Skip damage\n                            return;\n                        }\n                        \n                        let attackBlocked = false;\n                        \n                        // === CHARACTER PASSIVE: Knight shield ===\n                        if (player.characterId === 'knight' && player.hasShield) {\n                            // Shield blocks one attack\n                            const enemyAngle = Math.atan2(en.x - player.x, en.z - player.z);\n                            let angleDiff = Math.abs(player.dir - enemyAngle);\n                            if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff;\n                            \n                            // Shield only blocks front attacks\n                            if (angleDiff < Math.PI / 2) {\n                                attackBlocked = true;\n                                player.hasShield = false; // Consume shield\n                                \n                                // Shield break visual effect - burst of cyan particles\n                                for (let p = 0; p < 20; p++) {\n                                    const angle = Math.random() * Math.PI * 2;\n                                    const dist = 0.5 + Math.random() * 1.5;\n                                    spawnParticle3D(\n                                        player.x + Math.cos(angle) * dist,\n                                        1.0 + Math.random() * 1.5,\n                                        player.z + Math.sin(angle) * dist,\n                                        Math.random() > 0.5 ? 0x44DDFF : 0x88EEFF\n                                    );\n                                }\n                                \n                                playBlockSound();\n                                for (let p = 0; p < 10; p++) spawnParticle3D(player.x, 1.5, player.z, 0x44DDFF);\n                                spawnFloatingText(player.x, 2.5, player.z, '\ud83d\udee1\ufe0f SHIELD BREAK!', 'diamond');\n                                applyKnockback(en, player.x, player.z, 2);\n                            }\n                        }\n                        \n                        // Check if player is defending and attack is from front\n                        if (!attackBlocked && player.defending) {\n                            const enemyAngle = Math.atan2(en.x - player.x, en.z - player.z);\n                            let angleDiff = Math.abs(player.dir - enemyAngle);\n                            if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff;\n                            \n                            // === CHARACTER PASSIVE: Guardian 180\u00b0 defense ===\n                            const defenseAngle = player.characterId === 'guardian' ? Math.PI / 2 : Math.PI / 4;\n                            \n                            // If attack is from front (within defense cone)\n                            if (angleDiff < defenseAngle) {\n                                attackBlocked = true;\n                                \n                                // === CHARACTER PASSIVE: Guardian extra stun ===\n                                const stunDuration = player.characterId === 'guardian' ? 2.0 : 1.0;\n                                en.stunTimer = stunDuration;\n                                en.isSwinging = false;\n                                en.swingPhase = 0;\n                                en.swingProgress = 0;\n                                \n                                const rightArmPivot = en.mesh.userData.rightArmPivot;\n                                if (rightArmPivot) rightArmPivot.rotation.x = 0;\n                                \n                                playBlockSound();\n                                playStunSound();\n                                \n                                for (let p = 0; p < 6; p++) spawnParticle3D(player.x, 1.5, player.z, 0xFFFFFF);\n                                spawnFloatingText(en.x, 2.5, en.z, '\ud83d\udcab STUNNED!', 'large');\n                                applyKnockback(en, player.x, player.z, 1.5);\n                                \n                                // === CHARACTER PASSIVE: Guardian speed boost after successful block ===\n                                if (player.characterId === 'guardian') {\n                                    player.guardianSpeedBoost = 2.0; // +200% speed\n                                    player.guardianSpeedTimer = 2.0; // 2 seconds duration\n                                    spawnFloatingText(player.x, 2.8, player.z, '\ud83c\udfc3 SPEED BOOST!', 'diamond');\n                                }\n                            }\n                        }\n                        \n                        if (!attackBlocked && en.stunTimer <= 0) {\n                            // Check for weapon clash when player is also swinging\n                            let weaponsClashed = false;\n                            if (player.isSwinging && player.swingPhase === 2 && playerClashCooldown <= 0) {\n                                if (checkWeaponClash(en, player, en.length, playerLength, en.dir, player.dir)) {\n                                    weaponsClashed = true;\n                                    const midX = (player.x + en.x) / 2;\n                                    const midZ = (player.z + en.z) / 2;\n                                    const knockbackForce = 3;\n                                    \n                                    applyKnockback(player, midX, midZ, knockbackForce);\n                                    applyKnockback(en, midX, midZ, knockbackForce);\n                                    \n                                    playClashSound();\n                                    for (let p = 0; p < 8; p++) spawnParticle3D(midX, 1.5, midZ, 0xFFFF00);\n                                    spawnFloatingText(midX, 2, midZ, '\u2694\ufe0f CLASH!', 'large');\n                                    \n                                    playerClashCooldown = 0.5;\n                                    enemyClashCooldowns.set(en, 0.5);\n                                }\n                            }\n                            \n                            if (!weaponsClashed) {\n                                // === PEACE MODE: Knockback only, no kills ===\n                                if (isPeaceMode()) {\n                                    // Strong knockback effect\n                                    const knockbackForce = 5 + en.length * 0.1;\n                                    applyKnockback(player, en.x, en.z, knockbackForce);\n                                    \n                                    // Visual feedback\n                                    for (let p = 0; p < 6; p++) spawnParticle3D(player.x, 1, player.z, 0x88FFFF);\n                                    playClashSound();\n                                    spawnFloatingText(player.x, 2, player.z, '\ud83d\udca8 Knockback!', 'normal');\n                                    \n                                    // Brief invincibility to prevent spam\n                                    player.invincible = 0.5;\n                                } else {\n                                    // === STANDARD MODE: Normal kill logic ===\n                                    // One-hit kill! Enemy takes 25% of player's length (or 35% for ninja)\n                                    const absorbRate = player.characterId === 'ninja' ? 0.35 : 0.25;\n                                    const gain = playerLength * absorbRate;\n                                    en.length += gain;\n                                    updateEnemyWeapon(en);\n                                    \n                                    for (let p = 0; p < 12; p++) spawnParticle3D(player.x, 1, player.z, 0xFF0000);\n                                    playDamageSound();\n                                    \n                                    respawnPlayer();\n                                }\n                            }\n                        }\n                    }\n                }\n            });\n        }\n        \n        function enemyAttack(en, enIndex) {\n            const METER_TO_3D = 0.5;\n            const weaponLen = en.length * METER_TO_3D;\n            // Same blade width calculation as player\n            const bladeWidth = 0.55 + Math.min(en.length / 50, 2.5);\n            const eDirX = Math.sin(en.dir);\n            const eDirZ = Math.cos(en.dir);\n            const perpX = Math.cos(en.dir);\n            const perpZ = -Math.sin(en.dir);\n            \n            // Attack objects - use spatial grid for culling\n            const _atkObjs = _gridNearby(en.x, en.z, weaponLen + 2);\n            for (let ci = _atkObjs.length - 1; ci >= 0; ci--) {\n                const obj = _atkObjs[ci];\n                const i = objects3D.indexOf(obj);\n                if (i < 0) continue;\n                let hit = false;\n                \n                // Check along blade length and width (same as player)\n                for (let d = 0.3; d <= weaponLen && !hit; d += 0.4) {\n                    const widthAtD = bladeWidth * (0.7 + (d / weaponLen) * 0.5);\n                    for (let w = -widthAtD / 2; w <= widthAtD / 2 && !hit; w += widthAtD / 2) {\n                        const px = en.x + eDirX * d + perpX * w;\n                        const pz = en.z + eDirZ * d + perpZ * w;\n                        const dist = Math.hypot(px - obj.x, pz - obj.z);\n                        if (dist < OBJ_DEFS[obj.type].size * 0.8) {\n                            hit = true;\n                        }\n                    }\n                }\n                \n                if (hit) {\n                    obj.hp--;\n                    for (let p = 0; p < 3; p++) spawnParticle3D(obj.x, 1, obj.z, OBJ_DEFS[obj.type].color);\n                    \n                    // Same hit feedback gain as player\n                    const hitFeedbackGain = 0.08 * growthMult;\n                    en.length += hitFeedbackGain;\n                    \n                    if (obj.hp <= 0) {\n                        const def = OBJ_DEFS[obj.type];\n                        // Same length gain as player\n                        const gain = def.lengthGain * growthMult;\n                        en.length += gain;\n                        \n                        for (let p = 0; p < 8; p++) spawnParticle3D(obj.x, 1, obj.z, def.color);\n                        scene.remove(obj.mesh);\n                        \n                        // Trees turn into stumps when chopped by enemies - scale stump based on tree size\n                        if (obj.type === 'tree_small' || obj.type === 'tree' || obj.type === 'tree_large') {\n                            const stumpDef = OBJ_DEFS['stump'];\n                            const stumpMesh = createBlockMesh('stump');\n                            const treeSize = OBJ_DEFS[obj.type].size;\n                            const stumpScale = treeSize / 1.8;\n                            stumpMesh.scale.set(stumpScale, stumpScale, stumpScale);\n                            stumpMesh.position.set(obj.x, 0, obj.z);\n                            scene.add(stumpMesh);\n                            objects3D[i] = {\n                                mesh: stumpMesh,\n                                type: 'stump',\n                                hp: stumpDef.hp,\n                                maxHp: stumpDef.hp,\n                                x: obj.x,\n                                z: obj.z,\n                                scale: stumpScale\n                            };\n                        } else if (obj.type === 'stone_huge' || obj.type === 'stone_large' || obj.type === 'stone') {\n                            const nextStone = obj.type === 'stone_huge' ? 'stone_large' : \n                                              obj.type === 'stone_large' ? 'stone' : 'stone_small';\n                            const nextDef = OBJ_DEFS[nextStone];\n                            const nextMesh = createBlockMesh(nextStone);\n                            nextMesh.position.set(obj.x, 0, obj.z);\n                            scene.add(nextMesh);\n                            objects3D[i] = {\n                                mesh: nextMesh,\n                                type: nextStone,\n                                hp: nextDef.hp,\n                                maxHp: nextDef.hp,\n                                x: obj.x,\n                                z: obj.z\n                            };\n                        } else {\n                            createGroundDebris(obj.x, obj.z, obj.type, obj.scale || 1);\n                            objects3D.splice(i, 1);\n                        }\n                        destroyCount++;\n                    }\n                }\n            }\n            \n            // AI can also attack other AI enemies (PvP)\n            for (let i = enemies3D.length - 1; i >= 0; i--) {\n                if (i === enIndex) continue; // Don't hit self\n                const target = enemies3D[i];\n                if (target.invincible > 0 || target.respawning) continue; // Skip invincible or respawning targets\n                \n                // Check for AI vs AI weapon clash first!\n                const enClashCD = enemyClashCooldowns.get(en) || 0;\n                const targetClashCD = enemyClashCooldowns.get(target) || 0;\n                if (enClashCD <= 0 && targetClashCD <= 0 && checkWeaponClash(en, target, en.length, target.length, en.dir, target.dir)) {\n                    // AI weapons clashed! Knockback both, no damage\n                    const midX = (en.x + target.x) / 2;\n                    const midZ = (en.z + target.z) / 2;\n                    const knockbackForce = 3;\n                    \n                    applyKnockback(en, midX, midZ, knockbackForce);\n                    applyKnockback(target, midX, midZ, knockbackForce);\n                    \n                    // Visual/audio feedback for clash\n                    playClashSound();\n                    \n                    // Spawn clash particles (sparks)\n                    for (let p = 0; p < 8; p++) {\n                        spawnParticle3D(midX, 1.5, midZ, 0xFFFF00);\n                        spawnParticle3D(midX, 1.2, midZ, 0xFFAA00);\n                    }\n                    \n                    // Show clash text\n                    spawnFloatingText(midX, 2, midZ, '\u2694\ufe0f CLASH!', 'large');\n                    \n                    // Set cooldowns to prevent repeated clashes\n                    enemyClashCooldowns.set(en, 0.5);\n                    enemyClashCooldowns.set(target, 0.5);\n                    \n                    return; // Skip damage check after clash\n                }\n                \n                // Check if target is defending from front\n                if (target.defending) {\n                    const targetAngle = Math.atan2(en.x - target.x, en.z - target.z);\n                    let angleDiff = Math.abs(target.dir - targetAngle);\n                    if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff;\n                    \n                    if (angleDiff < Math.PI / 4) {\n                        // Check if attack would hit\n                        let wouldHit = false;\n                        for (let d = 0.3; d <= weaponLen && !wouldHit; d += 0.4) {\n                            const px = en.x + eDirX * d;\n                            const pz = en.z + eDirZ * d;\n                            if (Math.hypot(px - target.x, pz - target.z) < 1.0) wouldHit = true;\n                        }\n                        \n                        if (wouldHit) {\n                            // Blocked! Stun attacker\n                            en.stunTimer = 1.0;\n                            en.isSwinging = false;\n                            en.swingPhase = 0;\n                            const rightArmPivot = en.mesh.userData.rightArmPivot;\n                            if (rightArmPivot) rightArmPivot.rotation.x = 0;\n                            playBlockSound();\n                            applyKnockback(en, target.x, target.z, 1.5);\n                            for (let p = 0; p < 6; p++) spawnParticle3D(target.x, 1.5, target.z, 0xFFFFFF);\n                            return;\n                        }\n                    }\n                }\n                \n                // Check hit\n                let targetHit = false;\n                for (let d = 0.3; d <= weaponLen && !targetHit; d += 0.4) {\n                    const widthAtD = bladeWidth * (0.7 + (d / weaponLen) * 0.5);\n                    for (let w = -widthAtD / 2; w <= widthAtD / 2 && !targetHit; w += widthAtD / 2) {\n                        const px = en.x + eDirX * d + perpX * w;\n                        const pz = en.z + eDirZ * d + perpZ * w;\n                        if (Math.hypot(px - target.x, pz - target.z) < 1.0) targetHit = true;\n                    }\n                }\n                \n                if (targetHit) {\n                    // === PEACE MODE: Knockback only, no kills ===\n                    if (isPeaceMode()) {\n                        // Strong knockback effect\n                        const knockbackForce = 5 + en.length * 0.1;\n                        applyKnockback(target, en.x, en.z, knockbackForce);\n                        \n                        // Visual feedback\n                        for (let p = 0; p < 6; p++) spawnParticle3D(target.x, 1, target.z, 0x88FFFF);\n                        \n                        // Brief invincibility to prevent spam\n                        target.invincible = 0.5;\n                    } else {\n                        // === STANDARD MODE: Normal kill logic ===\n                        // One-hit kill! AI takes 25% of target's length\n                        const gain = target.length * 0.25 * growthMult;\n                        en.length += gain;\n                        for (let p = 0; p < 12; p++) spawnParticle3D(target.x, 1, target.z, 0xFF4500);\n                        createEnemyCorpse(target.x, target.z, target.shirtColor);\n                        \n                        // Respawn target at random far location\n                        respawnEnemy(target);\n                    }\n                }\n            }\n        }\n        \n        // Update souls\n        function updateSouls(dt) {\n            for (let i = souls3D.length - 1; i >= 0; i--) {\n                const s = souls3D[i];\n                s.life -= dt;\n                s.pulse += dt * 4;\n                \n                // Bobbing animation\n                s.mesh.position.y = 1 + Math.sin(s.pulse) * 0.3;\n                s.mesh.rotation.y += dt * 2;\n                \n                // Attract to player\n                const dx = player.x - s.mesh.position.x;\n                const dz = player.z - s.mesh.position.z;\n                const dist = Math.sqrt(dx*dx + dz*dz);\n                \n                if (dist < 5) {\n                    const speed = 15;\n                    s.mesh.position.x += (dx / dist) * speed * dt;\n                    s.mesh.position.z += (dz / dist) * speed * dt;\n                }\n                \n                if (dist < 1) {\n                    playerLength = playerLength + s.gain;\n                    playCollectSound();\n                    scene.remove(s.mesh);\n                    souls3D.splice(i, 1);\n                    updatePlayerWeapon();\n                    continue;\n                }\n                \n                if (s.life <= 0) {\n                    scene.remove(s.mesh);\n                    souls3D.splice(i, 1);\n                }\n            }\n        }\n        \n        // Update particles (optimized with pooling)\n        function updateParticles(dt) {\n            for (let i = particlePool.length - 1; i >= 0; i--) {\n                const p = particlePool[i];\n                p.position.x += p.userData.vx * dt;\n                p.position.y += p.userData.vy * dt;\n                p.position.z += p.userData.vz * dt;\n                p.userData.vy -= 15 * dt;\n                p.userData.life -= dt;\n                \n                if (p.userData.life <= 0 || p.position.y < 0) {\n                    // Return to inactive pool instead of removing\n                    p.visible = false;\n                    particleInactive.push(p);\n                    particlePool.splice(i, 1);\n                }\n            }\n        }\n        \n        // Camera state - only recalculates when blade length changes\n        let cameraTargetY = 24;\n        let cameraTargetZOffset = 16;\n        let currentCameraY = 24;\n        let currentCameraZOffset = 16;\n        let lastCameraBladeLength = -1;\n        \n        // Update camera - only adjust height when blade length changes\n        function updateCamera(dt) {\n            // Only recalculate camera height when blade length actually changes\n            if (lastCameraBladeLength !== playerLength) {\n                lastCameraBladeLength = playerLength;\n                \n                // Base camera settings (for short weapons)\n                const baseCameraY = 24;\n                const baseCameraZOffset = 16;\n                \n                // Calculate weapon's 3D length\n                const METER_TO_3D = 0.5;\n                const weaponLen3D = playerLength * METER_TO_3D;\n                \n                // Start zooming out when weapon exceeds ~20 meters\n                // Only zoom enough to keep blade tip just visible on screen\n                const zoomThreshold = 20;\n                const thresholdLen3D = zoomThreshold * METER_TO_3D;\n                \n                if (playerLength > zoomThreshold) {\n                    // Minimal zoom - just enough to fit the blade edge\n                    const excessLen = weaponLen3D - thresholdLen3D;\n                    const zoomFactor = 1 + (excessLen / thresholdLen3D) * 0.35;\n                    cameraTargetY = baseCameraY * zoomFactor;\n                    cameraTargetZOffset = baseCameraZOffset * zoomFactor;\n                } else {\n                    cameraTargetY = baseCameraY;\n                    cameraTargetZOffset = baseCameraZOffset;\n                }\n            }\n            \n            const heightSpeed = 3; // Slower for height changes\n            \n            currentCameraY += (cameraTargetY - currentCameraY) * heightSpeed * dt;\n            currentCameraZOffset += (cameraTargetZOffset - currentCameraZOffset) * heightSpeed * dt;\n            \n            camera.position.x = player.x;\n            camera.position.y = currentCameraY;\n            camera.position.z = player.z + currentCameraZOffset;\n            \n            camera.lookAt(player.x, 0, player.z);\n            \n            // Adjust fog based on camera height to prevent obstructing the view\n            scene.fog.near = 40 + Math.max(0, currentCameraY - 24) * 2.5;\n            scene.fog.far = 100 + Math.max(0, currentCameraY - 24) * 3.5;\n        }\n        \n        // Final 30 second rush\n        let rushTriggered = false;\n        let lastSecond = -1;\n        function checkRush() {\n            // Play countdown ticks in last 10 seconds\n            const currentSecond = Math.floor(timeRemaining);\n            if (currentSecond !== lastSecond && currentSecond <= 10 && currentSecond > 0) {\n                lastSecond = currentSecond;\n                playCountdownTick();\n            }\n            \n            if (!rushTriggered && timeRemaining <= 30) {\n                rushTriggered = true;\n                playRushWarning();\n                for (let i = 0; i < 5; i++) {\n                    const obj = { type: 'gold', x: (Math.random()-0.5)*12, z: (Math.random()-0.5)*12 };\n                    const mesh = createBlockMesh('gold');\n                    mesh.position.set(obj.x, 0, obj.z);\n                    scene.add(mesh);\n                    objects3D.push({ mesh, type: 'gold', hp: 5, maxHp: 5, x: obj.x, z: obj.z });\n                }\n                for (let i = 0; i < 3; i++) {\n                    const obj = { type: 'diamond', x: (Math.random()-0.5)*8, z: (Math.random()-0.5)*8 };\n                    const mesh = createBlockMesh('diamond');\n                    mesh.position.set(obj.x, 0, obj.z);\n                    scene.add(mesh);\n                    objects3D.push({ mesh, type: 'diamond', hp: 6, maxHp: 6, x: obj.x, z: obj.z });\n                }\n            }\n        }\n        \n        // HUD\n        function buildHUD() {\n            const shieldHTML = player.characterId === 'knight' ? '<div class=\"hud-item\" id=\"hud-shield\">\ud83d\udee1\ufe0f</div>' : '';\n            const modeIndicator = isPeaceMode() ? '<div class=\"hud-item\" id=\"hud-mode\" style=\"background: linear-gradient(135deg, rgba(100,200,150,0.92) 0%, rgba(50,150,100,0.96) 100%); border-color: rgba(100,255,150,0.3);\">\u262e\ufe0f Peace</div>' : '';\n            const killsLabel = isPeaceMode() ? '\ud83d\udca8 0' : '\ud83d\udc80 0';\n            \n            uiOverlay.innerHTML = `\n                <div class=\"hud-top\">\n                    <div class=\"hud-item\" id=\"hud-length\">\u2694\ufe0f 2.0m</div>\n                    ${modeIndicator}\n                    <div class=\"hud-item\" id=\"hud-kills\">${killsLabel}</div>\n                </div>\n                <div class=\"hud-timer\" id=\"hud-timer\">2:00</div>\n                ${shieldHTML}\n                <div class=\"hud-status\" id=\"hud-status\">\ud83d\udee1\ufe0f Blocking</div>\n            `;\n            \n            // Position shield indicator if knight\n            if (player.characterId === 'knight') {\n                const shieldEl = document.getElementById('hud-shield');\n                if (shieldEl) {\n                    shieldEl.style.position = 'absolute';\n                    shieldEl.style.top = '120px';\n                    shieldEl.style.left = '16px';\n                }\n            }\n        }\n        \n        // HUD update throttling\n        let lastHUDUpdate = 0;\n        const HUD_UPDATE_INTERVAL = 100; // Update HUD every 100ms\n        \n        function updateHUD() {\n            const now = Date.now();\n            if (now - lastHUDUpdate < HUD_UPDATE_INTERVAL) return;\n            lastHUDUpdate = now;\n            \n            const lenEl = document.getElementById('hud-length');\n            const killEl = document.getElementById('hud-kills');\n            const timerEl = document.getElementById('hud-timer');\n            const statusEl = document.getElementById('hud-status');\n            const shieldEl = document.getElementById('hud-shield');\n            \n            if (lenEl) lenEl.textContent = '\u2694\ufe0f ' + playerLength.toFixed(1) + 'm';\n            if (killEl) killEl.textContent = (isPeaceMode() ? '\ud83d\udca8 ' : '\ud83d\udc80 ') + killCount;\n            if (timerEl) {\n                const m = Math.floor(timeRemaining / 60);\n                const s = Math.floor(timeRemaining % 60);\n                timerEl.textContent = m + ':' + (s < 10 ? '0' : '') + s;\n                if (timeRemaining <= 30) timerEl.classList.add('urgent');\n            }\n            if (statusEl) {\n                // Reckless character always shows attacking\n                if (player.characterId === 'reckless') {\n                    statusEl.textContent = '\u2694\ufe0f Reckless Attack';\n                    statusEl.classList.add('attacking');\n                } else if (player.defending) {\n                    statusEl.textContent = '\ud83d\udee1\ufe0f Blocking';\n                    statusEl.classList.remove('attacking');\n                } else {\n                    statusEl.textContent = '\u2694\ufe0f Attacking';\n                    statusEl.classList.add('attacking');\n                }\n                \n                // Show slider speed boost\n                if (player.characterId === 'slider' && player.speedBoost > 0) {\n                    const boostPercent = Math.floor(player.speedBoost * 100);\n                    statusEl.textContent += ` \ud83c\udfc3 +${boostPercent}%`;\n                }\n                \n                // Show guardian speed boost\n                if (player.characterId === 'guardian' && player.guardianSpeedTimer > 0) {\n                    const boostPercent = Math.floor(player.guardianSpeedBoost * 100);\n                    statusEl.textContent += ` \ud83c\udfc3 +${boostPercent}%`;\n                }\n                \n                // Show bomber attack count\n                if (player.characterId === 'bomber') {\n                    if (player.bombReady || player.attackCount >= 5) {\n                        statusEl.textContent += ` \ud83d\udca3 Ready!`;\n                    } else {\n                        statusEl.textContent += ` \ud83d\udca3 ${player.attackCount || 0}/5`;\n                    }\n                }\n            }\n            \n            // Update knight shield indicator\n            if (shieldEl) {\n                if (player.hasShield) {\n                    shieldEl.style.opacity = '1';\n                    shieldEl.textContent = '\ud83d\udee1\ufe0f Shield';\n                } else {\n                    shieldEl.style.opacity = '0.4';\n                    shieldEl.textContent = '\ud83d\udc94 Broken';\n                }\n            }\n        }\n        \n        function showStartScreen() {\n            // Preview scene\n            initGameObjects();\n            initEnemies();\n            initPlayer();\n            \n            showCharacterSelect(false);\n            \n            // Animate preview\n            function previewLoop() {\n                if (gameState !== 'start') return;\n                camera.position.x = Math.sin(Date.now() / 3000) * 10;\n                camera.position.z = Math.cos(Date.now() / 3000) * 10 + 25;\n                camera.lookAt(0, 0, 0);\n                renderer.render(scene, camera);\n                requestAnimationFrame(previewLoop);\n            }\n            previewLoop();\n        }\n        \n        // Character Selection Screen with 3D Preview\n        function showCharacterSelect(fromGameOver = false) {\n            const coins = getPlayerCoins();\n            const charList = Object.keys(CHARACTER_DEFS);\n            let currentIndex = charList.indexOf(getSelectedCharacter());\n            if (currentIndex < 0) currentIndex = 0;\n            \n            // 3D preview scene setup\n            let previewScene, previewCamera, previewRenderer, previewChar, previewWeapon;\n            let previewAnimFrame = null;\n            let rotationY = 0;\n            let autoRotate = true;\n            \n            // Swipe handling\n            let touchStartX = 0;\n            let touchCurrentX = 0;\n            let isSwiping = false;\n            \n            function setupPreviewScene() {\n                previewScene = new THREE.Scene();\n                previewScene.background = new THREE.Color(0x1a1a2e);\n                \n                previewCamera = new THREE.PerspectiveCamera(45, 400 / 260, 0.1, 100);\n                previewCamera.position.set(0, 2, 5);\n                previewCamera.lookAt(0, 1, 0);\n                \n                const previewCanvas = document.getElementById('char-preview-canvas');\n                if (!previewCanvas) return;\n                \n                previewRenderer = new THREE.WebGLRenderer({ canvas: previewCanvas, antialias: true });\n                previewRenderer.setSize(400, 260);\n                \n                // Lighting\n                const ambientLight = new THREE.AmbientLight(0xB4D7FF, 0.6);\n                previewScene.add(ambientLight);\n                \n                const frontLight = new THREE.DirectionalLight(0xFFFFFF, 0.8);\n                frontLight.position.set(2, 3, 4);\n                previewScene.add(frontLight);\n                \n                const backLight = new THREE.DirectionalLight(0x4488FF, 0.3);\n                backLight.position.set(-2, 2, -3);\n                previewScene.add(backLight);\n                \n                // Ground platform\n                const platformGeo = new THREE.CylinderGeometry(1.5, 1.8, 0.2, 32);\n                const platformMat = new THREE.MeshLambertMaterial({ color: 0x3a3a5a });\n                const platform = new THREE.Mesh(platformGeo, platformMat);\n                platform.position.set(0.4, -0.1, 0);\n                previewScene.add(platform);\n                \n                // Ring around platform\n                const ringGeo = new THREE.TorusGeometry(1.65, 0.05, 8, 32);\n                const ringMat = new THREE.MeshBasicMaterial({ color: 0xFFD700 });\n                const ring = new THREE.Mesh(ringGeo, ringMat);\n                ring.rotation.x = Math.PI / 2;\n                ring.position.set(0.4, 0.02, 0);\n                previewScene.add(ring);\n                \n                updatePreviewCharacter();\n                animatePreview();\n            }\n            \n            function updatePreviewCharacter() {\n                if (!previewScene) return;\n                \n                // Remove old character\n                if (previewChar) previewScene.remove(previewChar);\n                \n                const charId = charList[currentIndex];\n                const charDef = CHARACTER_DEFS[charId];\n                \n                // Create character with the character-specific appearance\n                const shirtMat = new THREE.MeshLambertMaterial({ color: charDef.colors.shirt });\n                previewChar = createCharacter(shirtMat, true, charId);\n                previewChar.position.set(0.4, 0, 0); // Moved right by 0.4 units to center better\n                previewChar.rotation.y = rotationY;\n                previewScene.add(previewChar);\n            }\n            \n            function animatePreview() {\n                if (!previewRenderer || !previewScene || !previewCamera) return;\n                \n                if (autoRotate && previewChar) {\n                    rotationY += 0.01;\n                    previewChar.rotation.y = rotationY;\n                }\n                \n                // Idle animation - subtle breathing\n                if (previewChar) {\n                    const breathe = Math.sin(Date.now() / 800) * 0.02;\n                    previewChar.scale.y = 1 + breathe;\n                }\n                \n                previewRenderer.render(previewScene, previewCamera);\n                previewAnimFrame = requestAnimationFrame(animatePreview);\n            }\n            \n            function cleanupPreview() {\n                if (previewAnimFrame) {\n                    cancelAnimationFrame(previewAnimFrame);\n                    previewAnimFrame = null;\n                }\n                if (previewRenderer) {\n                    previewRenderer.dispose();\n                    previewRenderer = null;\n                }\n                previewScene = null;\n                previewCamera = null;\n                previewChar = null;\n            }\n            \n            function navigateChar(direction) {\n                currentIndex += direction;\n                if (currentIndex < 0) currentIndex = charList.length - 1;\n                if (currentIndex >= charList.length) currentIndex = 0;\n                rotationY = 0; // Reset rotation on change\n                updatePreviewCharacter();\n                updateCharInfo();\n            }\n            \n            function updateCharInfo() {\n                const charId = charList[currentIndex];\n                const charDef = CHARACTER_DEFS[charId];\n                const isOwned = isCharacterUnlocked(charId);\n                const canAfford = coins >= charDef.price;\n                \n                // Update name\n                const nameEl = document.getElementById('char-info-name');\n                if (nameEl) nameEl.textContent = charDef.emoji + ' ' + charDef.name;\n                \n                // Update passive\n                const passiveEl = document.getElementById('char-info-passive');\n                if (passiveEl) passiveEl.textContent = charDef.passive;\n                \n                // Update dots\n                document.querySelectorAll('.char-dot').forEach((dot, idx) => {\n                    dot.classList.toggle('active', idx === currentIndex);\n                });\n                \n                // Update button\n                const btnContainer = document.getElementById('char-btn-container');\n                if (btnContainer) {\n                    let buttonHTML = '';\n                    if (isOwned) {\n                        buttonHTML = `<button class=\"btn-select-char\" id=\"btn-confirm\">\u2694\ufe0f Start Game</button>`;\n                    } else if (canAfford) {\n                        buttonHTML = `<button class=\"btn-select-char buy\" id=\"btn-buy\">\ud83d\udcb0 Buy for ${charDef.price} Coins</button>`;\n                    } else {\n                        buttonHTML = `<button class=\"btn-select-char\" disabled>\ud83d\udcb0 Not Enough (Need ${charDef.price})</button>`;\n                    }\n                    btnContainer.innerHTML = buttonHTML;\n                    attachButtonHandlers();\n                }\n            }\n            \n            function attachButtonHandlers() {\n                const btnBuy = document.getElementById('btn-buy');\n                if (btnBuy) {\n                    btnBuy.addEventListener('click', () => {\n                        const charId = charList[currentIndex];\n                        if (purchaseCharacter(charId)) {\n                            setSelectedCharacter(charId);\n                            playCollectSound(5);\n                            // Refresh the whole screen to update coins display\n                            cleanupPreview();\n                            showCharacterSelect(fromGameOver);\n                        }\n                    });\n                }\n                \n                const btnConfirm = document.getElementById('btn-confirm');\n                if (btnConfirm) {\n                    const handleStart = () => {\n                        try { if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); } catch(e) {}\n                        setSelectedCharacter(charList[currentIndex]);\n                        cleanupPreview();\n                        if (fromGameOver) {\n                            // Already reset in showGameOverScreen handler\n                        }\n                        if (playerMesh) scene.remove(playerMesh);\n                        initPlayer();\n                        startGame();\n                    };\n                    btnConfirm.addEventListener('click', handleStart);\n                    btnConfirm.addEventListener('touchstart', (e) => {\n                        e.preventDefault();\n                        handleStart();\n                    });\n                }\n            }\n            \n            function renderCharacterSelect() {\n                // Start background music on character select screen\n                playBackgroundMusic();\n                \n                const charId = charList[currentIndex];\n                const charDef = CHARACTER_DEFS[charId];\n                const isOwned = isCharacterUnlocked(charId);\n                const canAfford = coins >= charDef.price;\n                const currentMode = getGameMode();\n                \n                // Create dots for indicator\n                let dotsHTML = '';\n                charList.forEach((id, idx) => {\n                    const owned = isCharacterUnlocked(id);\n                    dotsHTML += `<div class=\"char-dot ${idx === currentIndex ? 'active' : ''} ${owned ? 'owned' : ''}\"></div>`;\n                });\n                \n                let buttonHTML = '';\n                if (isOwned) {\n                    buttonHTML = `<button class=\"btn-select-char\" id=\"btn-confirm\">\u2694\ufe0f Start Game</button>`;\n                } else if (canAfford) {\n                    buttonHTML = `<button class=\"btn-select-char buy\" id=\"btn-buy\">\ud83d\udcb0 Buy for ${charDef.price} Coins</button>`;\n                } else {\n                    buttonHTML = `<button class=\"btn-select-char\" disabled>\ud83d\udcb0 Not Enough (Need ${charDef.price})</button>`;\n                }\n                \n                // Mode selection HTML\n                const modeSelectHTML = `\n                    <div class=\"mode-select-container\">\n                        <div class=\"mode-select-title\">\ud83c\udfae Game Mode</div>\n                        <div class=\"mode-buttons\">\n                            <button class=\"mode-btn ${currentMode === 'standard' ? 'active' : ''}\" id=\"btn-mode-standard\">\n                                <span class=\"mode-icon\">\u2694\ufe0f</span>\n                                <span class=\"mode-name\">Standard</span>\n                                <span class=\"mode-desc\">Kill to absorb length</span>\n                            </button>\n                            <button class=\"mode-btn ${currentMode === 'peace' ? 'active' : ''}\" id=\"btn-mode-peace\">\n                                <span class=\"mode-icon\">\u262e\ufe0f</span>\n                                <span class=\"mode-name\">Peace</span>\n                                <span class=\"mode-desc\">No damage, knockback only</span>\n                            </button>\n                        </div>\n                    </div>\n                `;\n                \n                uiOverlay.innerHTML = `\n                    <div class=\"character-select-screen\">\n                        <div class=\"char-topbar\">\n                            <div class=\"char-select-title\">\ud83d\udde1\ufe0f Select Character</div>\n                            <div class=\"char-coins-topbar\">\ud83d\udcb0 ${coins}</div>\n                        </div>\n                        <div class=\"char-3d-container\" id=\"char-3d-container\">\n                            <canvas class=\"char-3d-canvas\" id=\"char-preview-canvas\"></canvas>\n                            <button class=\"char-nav-btn left\" id=\"btn-prev\">\u25c0</button>\n                            <button class=\"char-nav-btn right\" id=\"btn-next\">\u25b6</button>\n                        </div>\n                        <div class=\"char-indicator\">${dotsHTML}</div>\n                        <div class=\"char-info-panel\">\n                            <div class=\"char-name\" id=\"char-info-name\">${charDef.emoji} ${charDef.name}</div>\n                            <div class=\"char-passive\" id=\"char-info-passive\">${charDef.passive}</div>\n                        </div>\n                        ${modeSelectHTML}\n                        <div id=\"char-btn-container\">${buttonHTML}</div>\n                        <button class=\"btn-leaderboard\" id=\"btn-leaderboard\">\ud83c\udfc6 Leaderboard</button>\n                    </div>\n                `;\n                \n                // Setup 3D preview\n                setTimeout(() => setupPreviewScene(), 50);\n                \n                // Navigation buttons\n                document.getElementById('btn-prev')?.addEventListener('click', () => navigateChar(-1));\n                document.getElementById('btn-next')?.addEventListener('click', () => navigateChar(1));\n                \n                // Swipe handling on 3D container\n                const container = document.getElementById('char-3d-container');\n                if (container) {\n                    container.addEventListener('touchstart', (e) => {\n                        touchStartX = e.touches[0].clientX;\n                        touchCurrentX = touchStartX;\n                        isSwiping = true;\n                        autoRotate = false;\n                    });\n                    \n                    container.addEventListener('touchmove', (e) => {\n                        if (!isSwiping) return;\n                        touchCurrentX = e.touches[0].clientX;\n                        // Manual rotation while dragging\n                        const dx = touchCurrentX - touchStartX;\n                        if (previewChar) {\n                            previewChar.rotation.y = rotationY + dx * 0.01;\n                        }\n                    });\n                    \n                    container.addEventListener('touchend', (e) => {\n                        if (!isSwiping) return;\n                        isSwiping = false;\n                        const dx = touchCurrentX - touchStartX;\n                        \n                        // Threshold for swipe navigation\n                        if (Math.abs(dx) > 50) {\n                            navigateChar(dx > 0 ? -1 : 1);\n                        } else {\n                            // Update rotation from drag\n                            rotationY = previewChar ? previewChar.rotation.y : rotationY;\n                        }\n                        autoRotate = true;\n                    });\n                    \n                    // Mouse drag for desktop\n                    let mouseDown = false;\n                    let mouseStartX = 0;\n                    \n                    container.addEventListener('mousedown', (e) => {\n                        mouseDown = true;\n                        mouseStartX = e.clientX;\n                        autoRotate = false;\n                    });\n                    \n                    container.addEventListener('mousemove', (e) => {\n                        if (!mouseDown) return;\n                        const dx = e.clientX - mouseStartX;\n                        if (previewChar) {\n                            previewChar.rotation.y = rotationY + dx * 0.01;\n                        }\n                    });\n                    \n                    container.addEventListener('mouseup', (e) => {\n                        if (!mouseDown) return;\n                        mouseDown = false;\n                        const dx = e.clientX - mouseStartX;\n                        \n                        if (Math.abs(dx) > 50) {\n                            navigateChar(dx > 0 ? -1 : 1);\n                        } else {\n                            rotationY = previewChar ? previewChar.rotation.y : rotationY;\n                        }\n                        autoRotate = true;\n                    });\n                    \n                    container.addEventListener('mouseleave', () => {\n                        if (mouseDown) {\n                            mouseDown = false;\n                            rotationY = previewChar ? previewChar.rotation.y : rotationY;\n                            autoRotate = true;\n                        }\n                    });\n                }\n                \n                attachButtonHandlers();\n                \n                // Mode selection buttons\n                const btnModeStandard = document.getElementById('btn-mode-standard');\n                const btnModePeace = document.getElementById('btn-mode-peace');\n                \n                if (btnModeStandard) {\n                    btnModeStandard.addEventListener('click', () => {\n                        setGameMode('standard');\n                        document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));\n                        btnModeStandard.classList.add('active');\n                        playSound(600, 0.1, 'sine', 0.1);\n                    });\n                }\n                \n                if (btnModePeace) {\n                    btnModePeace.addEventListener('click', () => {\n                        setGameMode('peace');\n                        document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));\n                        btnModePeace.classList.add('active');\n                        playSound(800, 0.1, 'sine', 0.1);\n                    });\n                }\n                \n                // Leaderboard button\n                const btnLeaderboard = document.getElementById('btn-leaderboard');\n                if (btnLeaderboard) {\n                    btnLeaderboard.addEventListener('click', () => {\n                        showLeaderboardScreen();\n                    });\n                }\n            }\n            \n            renderCharacterSelect();\n        }\n        \n        // Leaderboard Screen\n        async function showLeaderboardScreen() {\n            // Show loading state\n            uiOverlay.innerHTML = `\n                <div class=\"leaderboard-screen\">\n                    <div class=\"leaderboard-header\">\n                        <button class=\"btn-back-leaderboard\" id=\"btn-back\">\u2190 Back</button>\n                        <div class=\"leaderboard-screen-title\">\ud83c\udfc6 Leaderboard</div>\n                    </div>\n                    <div class=\"leaderboard-loading\">Loading...</div>\n                </div>\n            `;\n            \n            // Add back button handler immediately\n            document.getElementById('btn-back')?.addEventListener('click', () => {\n                showCharacterSelect(false);\n            });\n            \n            // Fetch leaderboard data\n            let leaderboardData = null;\n            try {\n                leaderboardData = await lib.getTopNEntriesFromLeaderboard(20);\n            } catch (error) {\n                lib.log('Failed to fetch leaderboard: ' + error.message);\n            }\n            \n            // Build leaderboard entries HTML\n            let entriesHTML = '';\n            if (leaderboardData && leaderboardData.entries && leaderboardData.entries.length > 0) {\n                leaderboardData.entries.forEach((entry, idx) => {\n                    const entryRank = idx + 1;\n                    const entryRankClass = entryRank === 1 ? 'rank-1' : (entryRank === 2 ? 'rank-2' : (entryRank === 3 ? 'rank-3' : ''));\n                    const entryRankEmoji = entryRank === 1 ? '\ud83e\udd47' : (entryRank === 2 ? '\ud83e\udd48' : (entryRank === 3 ? '\ud83e\udd49' : `#${entryRank}`));\n                    const isUser = leaderboardData.userRank === entryRank;\n                    entriesHTML += `\n                        <div class=\"leaderboard-entry ${entryRankClass} ${isUser ? 'user' : ''}\">\n                            <div class=\"leaderboard-rank ${entryRankClass}\">${entryRankEmoji}</div>\n                            <div class=\"leaderboard-player\">${entry.username}</div>\n                            <div class=\"leaderboard-score\">${entry.score}m</div>\n                        </div>\n                    `;\n                });\n            } else {\n                entriesHTML = '<div class=\"leaderboard-empty\">No records yet. Play to set one!</div>';\n            }\n            \n            // User's rank info\n            let userRankHTML = '';\n            if (leaderboardData && leaderboardData.userRank) {\n                userRankHTML = `\n                    <div class=\"leaderboard-user-rank\">\n                        <span>Your Rank: </span>\n                        <span class=\"user-rank-value\">#${leaderboardData.userRank}</span>\n                    </div>\n                `;\n            }\n            \n            uiOverlay.innerHTML = `\n                <div class=\"leaderboard-screen\">\n                    <div class=\"leaderboard-header\">\n                        <button class=\"btn-back-leaderboard\" id=\"btn-back\">\u2190 Back</button>\n                        <div class=\"leaderboard-screen-title\">\ud83c\udfc6 Longest Blade Leaderboard</div>\n                    </div>\n                    ${userRankHTML}\n                    <div class=\"leaderboard-list\">\n                        ${entriesHTML}\n                    </div>\n                </div>\n            `;\n            \n            // Back button handler\n            document.getElementById('btn-back')?.addEventListener('click', () => {\n                showCharacterSelect(false);\n            });\n        }\n        \n        async function showGameOver() {\n            gameState = 'over';\n            \n            // Play game over sound\n            playGameOverSound();\n            \n            // Calculate coins earned (equal to blade length)\n            let coinsEarned = Math.floor(playerLength);\n            \n            // === CHARACTER PASSIVE: Bounty - extra coins per kill ===\n            if (player.characterId === 'bounty') {\n                const bonusCoins = killCount * 5;\n                coinsEarned += bonusCoins;\n            }\n            \n            // Add coins to player\n            addPlayerCoins(coinsEarned);\n            \n            const allLengths = enemies3D.map(e => e.length).concat([playerLength]).sort((a,b) => b-a);\n            const rank = allLengths.indexOf(playerLength) + 1;\n            \n            // Update cumulative blade length\n            const bladeLengthToAdd = Math.floor(playerLength);\n            config.cumulativeBladeLength = (config.cumulativeBladeLength || 0) + bladeLengthToAdd;\n            await saveCharacterProgress();\n            \n            // Submit score to longest blade leaderboard\n            let leaderboardData = null;\n            \n            try {\n                // Submit current blade length to leaderboard\n                leaderboardData = await lib.addPlayerScoreToLeaderboard(bladeLengthToAdd, 10);\n                lib.log('Blade length submitted to leaderboard: ' + bladeLengthToAdd);\n            } catch (error) {\n                lib.log('Failed to submit to leaderboard: ' + error.message);\n            }\n            \n            const stats = {\n                rank: rank,\n                length: playerLength.toFixed(1),\n                kills: killCount,\n                destroys: destroyCount,\n                cumulativeLength: config.cumulativeBladeLength,\n                leaderboardData: leaderboardData\n            };\n            \n            // Show dedicated game over screen\n            showGameOverScreen(coinsEarned, stats);\n        }\n        \n        // Dedicated Game Over Results Screen\n        function showGameOverScreen(earnedCoins, stats) {\n            const rankClass = stats.rank === 1 ? 'rank-1' : (stats.rank === 2 ? 'rank-2' : (stats.rank === 3 ? 'rank-3' : ''));\n            const rankEmoji = stats.rank === 1 ? '\ud83e\udd47' : (stats.rank === 2 ? '\ud83e\udd48' : (stats.rank === 3 ? '\ud83e\udd49' : '\ud83c\udfaf'));\n            \n\n            \n            // Cumulative stats display\n            const cumulativeHTML = `\n                <div class=\"cumulative-stats\">\n                    <div class=\"cumulative-title\">\ud83d\udcca Cumulative Blade Length</div>\n                    <div class=\"cumulative-value\">${stats.cumulativeLength}m</div>\n                </div>\n            `;\n            \n            uiOverlay.innerHTML = `\n                <div class=\"game-over-screen\">\n                    <div class=\"game-over-title\">\ud83c\udfc1 Battle Over</div>\n                    <div class=\"game-over-stats\">\n                        <div class=\"stat-item ${rankClass}\">\n                            <span class=\"stat-label\">${rankEmoji} Rank</span>\n                            <span class=\"stat-value\">#${stats.rank}</span>\n                        </div>\n                        <div class=\"stat-item\">\n                            <span class=\"stat-label\">\u2694\ufe0f Length</span>\n                            <span class=\"stat-value\">${stats.length}m</span>\n                        </div>\n                        <div class=\"stat-item\">\n                            <span class=\"stat-label\">\ud83d\udc80 Kills</span>\n                            <span class=\"stat-value\">${stats.kills}</span>\n                        </div>\n                        <div class=\"stat-item\">\n                            <span class=\"stat-label\">\ud83d\udca5 Destroys</span>\n                            <span class=\"stat-value\">${stats.destroys}</span>\n                        </div>\n                    </div>\n                    <div class=\"coins-earned\">+${earnedCoins} \ud83d\udcb0 Coins Earned!</div>\n                    ${cumulativeHTML}\n                    <button class=\"btn-restart\" id=\"btn-play-again\">\ud83d\udd04 Play Again</button>\n                </div>\n            `;\n            \n            // Handle Play Again button - go to character select\n            const btnPlayAgain = document.getElementById('btn-play-again');\n            if (btnPlayAgain) {\n                const handlePlayAgain = () => {\n                    try { if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); } catch(e) {}\n                    resetGame();\n                    showCharacterSelect(false);\n                };\n                btnPlayAgain.addEventListener('click', handlePlayAgain);\n                btnPlayAgain.addEventListener('touchstart', (e) => {\n                    e.preventDefault();\n                    handlePlayAgain();\n                });\n            }\n        }\n        \n        function resetGame() {\n            playerLength = config.startLength || 2;\n            killCount = 0;\n            destroyCount = 0;\n            timeRemaining = config.matchDuration || 120;\n            rushTriggered = false;\n            \n            // Spawn player near a random edge\n            const edge = Math.floor(Math.random() * 4); // 0=top, 1=right, 2=bottom, 3=left\n            const edgeDist = 42 + Math.random() * 6; // 42-48 units from center\n            \n            if (edge === 0) { // Top edge\n                player.x = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                player.z = -edgeDist;\n            } else if (edge === 1) { // Right edge\n                player.x = edgeDist;\n                player.z = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n            } else if (edge === 2) { // Bottom edge\n                player.x = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n                player.z = edgeDist;\n            } else { // Left edge\n                player.x = -edgeDist;\n                player.z = (Math.random() - 0.5) * ARENA_SIZE * 1.8;\n            }\n            \n            player.invincible = 0;\n            player.respawning = false;\n            \n            // Reset character-specific state\n            player.characterId = getSelectedCharacter();\n            player.attackCount = 0;\n            player.bombReady = false;\n            player.moveTime = 0;\n            player.speedBoost = 0;\n            player.hasShield = (player.characterId === 'knight');\n\n            particlePool.forEach(p => scene.remove(p));\n            particlePool.length = 0;\n            \n            // Clear ground debris\n            groundDebris.forEach(d => scene.remove(d));\n            groundDebris.length = 0;\n            \n            // Clear slash trails\n            slashTrails.forEach(t => scene.remove(t));\n            slashTrails.length = 0;\n            \n            // Clear ninja afterimages\n            ninjaAfterimages.forEach(n => scene.remove(n));\n            ninjaAfterimages.length = 0;\n            \n            if (playerMesh) scene.remove(playerMesh);\n            initPlayer();\n            initGameObjects();\n            initEnemies();\n        }\n        \n        function startGame() {\n            gameState = 'playing';\n            buildHUD();\n            playerMesh.position.set(player.x, 0, player.z);\n            \n            // Play game start sound\n            playGameStartSound();\n            \n            // Start background music\n            playBackgroundMusic();\n        }\n        \n        // Game ending transition state\n        let endingTransition = false;\n        let endingTimer = 0;\n        const ENDING_DURATION = 2.5; // seconds for transition\n        \n        // Show ending transition overlay\n        function showEndingTransition() {\n            endingTransition = true;\n            endingTimer = 0;\n            \n            // Play dramatic sound\n            playSound(800, 0.3, 'sine', 0.15);\n            playSound(600, 0.4, 'sine', 0.12, 0.1);\n            playSound(400, 0.5, 'sine', 0.1, 0.25);\n            \n            // Create transition overlay\n            const overlay = document.createElement('div');\n            overlay.id = 'ending-overlay';\n            overlay.style.cssText = `\n                position: absolute;\n                top: 0; left: 0;\n                width: 100%; height: 100%;\n                display: flex;\n                flex-direction: column;\n                align-items: center;\n                justify-content: center;\n                background: rgba(0,0,0,0);\n                z-index: 50;\n                pointer-events: none;\n                transition: background 1.5s ease;\n            `;\n            \n            const timeUpText = document.createElement('div');\n            timeUpText.style.cssText = `\n                font-family: 'Poppins', sans-serif;\n                font-size: 64px;\n                font-weight: 800;\n                color: #FFD700;\n                text-shadow: 0 4px 30px rgba(255,215,0,0.8), 0 0 60px rgba(255,215,0,0.6);\n                opacity: 0;\n                transform: scale(0.3);\n                transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n            `;\n            timeUpText.textContent = '\u23f0 TIME UP!';\n            overlay.appendChild(timeUpText);\n            \n            const lengthText = document.createElement('div');\n            lengthText.style.cssText = `\n                font-family: 'Poppins', sans-serif;\n                font-size: 36px;\n                font-weight: 700;\n                color: #fff;\n                text-shadow: 0 2px 15px rgba(0,0,0,0.8);\n                opacity: 0;\n                margin-top: 20px;\n                transition: opacity 0.5s ease 0.6s;\n            `;\n            lengthText.textContent = `\u2694\ufe0f Final Length: ${playerLength.toFixed(1)}m`;\n            overlay.appendChild(lengthText);\n            \n            uiOverlay.appendChild(overlay);\n            \n            // Trigger animations after a frame\n            requestAnimationFrame(() => {\n                overlay.style.background = 'rgba(0,0,0,0.7)';\n                timeUpText.style.opacity = '1';\n                timeUpText.style.transform = 'scale(1)';\n                lengthText.style.opacity = '1';\n            });\n            \n            // Spawn celebration particles around player\n            for (let i = 0; i < 30; i++) {\n                setTimeout(() => {\n                    if (playerMesh) {\n                        const angle = Math.random() * Math.PI * 2;\n                        const dist = 1 + Math.random() * 3;\n                        const colors = [0xFFD700, 0xFF6600, 0x00FFFF, 0xFF00FF, 0x00FF00];\n                        spawnParticle3D(\n                            player.x + Math.cos(angle) * dist,\n                            1 + Math.random() * 2,\n                            player.z + Math.sin(angle) * dist,\n                            colors[Math.floor(Math.random() * colors.length)]\n                        );\n                    }\n                }, i * 50);\n            }\n        }\n        \n        // Update ending transition\n        function updateEndingTransition(dt) {\n            endingTimer += dt;\n            \n            // Slow down time effect - gradually slow camera movement\n            const slowFactor = Math.max(0.1, 1 - endingTimer / ENDING_DURATION);\n            \n            // Camera slowly zooms out and up during transition\n            const zoomProgress = Math.min(endingTimer / ENDING_DURATION, 1);\n            const targetY = 35 + zoomProgress * 15;\n            const targetZOffset = 20 + zoomProgress * 10;\n            \n            camera.position.x += (player.x - camera.position.x) * 2 * dt * slowFactor;\n            camera.position.y += (targetY - camera.position.y) * 2 * dt;\n            camera.position.z += (player.z + targetZOffset - camera.position.z) * 2 * dt;\n            camera.lookAt(player.x, 0, player.z);\n            \n            // Fade fog to darker\n            const fogProgress = Math.min(endingTimer / ENDING_DURATION, 1);\n            scene.fog.color.setHSL(0.6, 0.3, 0.3 * (1 - fogProgress * 0.5));\n            \n            // Transition complete\n            if (endingTimer >= ENDING_DURATION) {\n                endingTransition = false;\n                const overlay = document.getElementById('ending-overlay');\n                if (overlay) overlay.remove();\n                showGameOver();\n            }\n        }\n        \n        // Main game loop\n        let lastTime = 0;\n        function gameLoop(timestamp) {\n            const dt = Math.min((timestamp - lastTime) / 1000, 0.11);\n            lastTime = timestamp;\n            \n            // Handle ending transition\n            if (endingTransition) {\n                updateEndingTransition(dt);\n                updateParticles(dt);\n                renderer.render(scene, camera);\n                requestAnimationFrame(gameLoop);\n                return;\n            }\n            \n            if (gameState === 'playing') {\n                timeRemaining -= dt;\n                \n                // Update clash cooldowns\n                if (playerClashCooldown > 0) playerClashCooldown -= dt;\n                enemyClashCooldowns.forEach((cd, en) => {\n                    if (cd > 0) enemyClashCooldowns.set(en, cd - dt);\n                });\n                \n                if (timeRemaining <= 0) {\n                    timeRemaining = 0;\n                    showEndingTransition();\n                } else {\n                    checkRush();\n                    _gridRebuild();\n                    updatePlayer(dt);\n                    updateEnemies(dt);\n                    updateParticles(dt);\n                    updateSlashTrails(dt);\n                    updateNinjaAfterimages(dt);\n                    updateKnightShieldEffect(dt);\n                    updateCamera(dt);\n                    updateHUD();\n                }\n            }\n            \n            // Rush visual effect\n            if (timeRemaining <= 30 && gameState === 'playing') {\n                const rushHue = 0.05 + Math.sin(Date.now()/300) * 0.03;\n                scene.fog.color.setHSL(rushHue, 0.6, 0.6);\n                sunLight.color.setHSL(rushHue, 0.8, 0.7);\n            }\n            \n\n            \n            renderer.render(scene, camera);\n            requestAnimationFrame(gameLoop);\n        }\n        \n        // Initialize\n        loadBackgroundMusic().then(() => {\n            lib.log('Background music loaded');\n        });\n        \n        // Load character progress before starting\n        loadCharacterProgress().then(() => {\n            showStartScreen();\n            requestAnimationFrame(gameLoop);\n        });\n    }\n    </script>\n</body>\n</html>", "game_config": "{\"matchDuration\":120,\"startLength\":5,\"growthMultiplier\":1,\"aiCount\":7,\"objectDensity\":1,\"objects\":[],\"playerCoins\":0,\"unlockedCharacters\":[\"newbie\"],\"selectedCharacter\":\"newbie\",\"initialCoins\":100,\"gameMode\":\"standard\",\"aiMissChance\":70,\"aiMissChanceInfo\":\"Base 70%, -5% per 100 coins\"}", "game_design_document": "## Core Game Loop and Mechanics\n\n**My 99-Meter Blade** is a fast-paced, single-session arcade game where players experience immediate satisfaction through weapon growth and destruction. The core loop is:\n\n1. **Move & Target** - Navigate the blocky arena to find destructible objects and enemies\n2. **Swing & Destroy** - Automatically swing your weapon to smash blocks and hit enemies\n3. **Collect Blade Souls** - Gather glowing souls dropped from destroyed objects\n4. **Weapon Growth** - Watch your weapon visibly extend from 2m to 99m\n5. **Increased Power** - Longer weapons have greater range but slower swing speed\n6. **Defeat Rivals** - Use your extended reach to defeat AI enemies and absorb their length\n7. **Race to 99m** - Compete to achieve the longest weapon before the 120-second timer ends\n\n**Win Condition**: Achieve the longest weapon and highest score (based on length + kills + destruction) by the end of 120 seconds.\n\n**Engagement Drivers**:\n- Immediate visual feedback as weapon grows dramatically\n- Satisfying destruction of colorful block objects\n- Risk/reward of targeting high-value objects vs. safe grinding\n- Strategic positioning to outrange opponents\n- Final 30-second rush when premium resources spawn in center\n\n## Starting Scope\n\n**Phase 1 - Core Prototype** (Implement First):\n\n**Essential Gameplay**:\n- Single 720\u00d71280 arena with fixed camera view (top-down perspective)\n- Player character (blocky humanoid) with basic movement\n- Automatic weapon swinging mechanic (periodic attacks in movement direction)\n- 3-5 types of destructible objects (grass blocks, wooden crates, stone blocks, gold ore, diamond ore)\n- Blade soul collection system with visual feedback\n- Weapon length progression from 2m to 99m with visual scaling\n- 4-6 AI enemies with simple behaviors (random movement, attack nearby objects/players)\n- 120-second countdown timer\n- Basic UI: timer, current length, kill count\n- End screen showing final stats\n\n**Visual Elements**:\n- Minecraft-inspired blocky art style\n- Bright, colorful palette\n- Simple particle effects for destruction\n- Weapon trail effect\n\n**Defer to Later Iterations**:\n- Multiple weapon types and visual transformations\n- Skill power-ups (sprint, magnet, whirlwind, etc.)\n- Complex AI behaviors (flanking, defending, targeting specific resources)\n- TNT explosives and chain reactions\n- Advanced particle systems\n- Sound effects and music\n- Leaderboard/ranking system\n\n## Game Perspective\n\n**Top-Down View** - The game uses a bird's eye perspective showing the entire arena at once. This allows players to:\n- See all threats and opportunities simultaneously\n- Plan movement and positioning strategically\n- Appreciate weapon length growth visually\n- Easily target objects and enemies\n\nThe camera is fixed and shows the complete arena (fitting within the 720\u00d71280 viewport). All game elements are designed to be clearly visible at this scale.\n\n## Game World and Level Design\n\n**Arena Layout** (Single Map - \"Mine Village Square\"):\n\nThe arena is a rectangular blocky environment divided into themed zones:\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502     [Trees & Stumps Zone]       \u2502\n\u2502  \ud83c\udf33  \ud83c\udf33  \ud83e\udeb5  \ud83e\udeb5  \ud83c\udf33  \ud83c\udf33         \u2502\n\u2502                                 \u2502\n\u2502 [Wooden Crate Area]             \u2502\n\u2502  \ud83d\udce6 \ud83d\udce6 \ud83d\udce6 \ud83d\udce6 \ud83d\udce6                 \u2502\n\u2502                                 \u2502\n\u2502      [CENTRAL PLAZA]            \u2502\n\u2502    \ud83d\udc8e \u2b50 \ud83d\udcb0 \u2b50 \ud83d\udc8e              \u2502\n\u2502    \u2b50 \ud83d\udcb0 \ud83d\udcb0 \ud83d\udcb0 \u2b50              \u2502\n\u2502    \ud83d\udc8e \u2b50 \ud83d\udcb0 \u2b50 \ud83d\udc8e              \u2502\n\u2502                                 \u2502\n\u2502         [Stone Mine]            \u2502\n\u2502    \ud83e\udea8 \ud83e\udea8 \u26cf\ufe0f \u26cf\ufe0f \ud83e\udea8 \ud83e\udea8         \u2502\n\u2502                                 \u2502\n\u2502    [Block Wall Perimeter]       \u2502\n\u2502    \ud83e\uddf1 \ud83e\uddf1 \ud83e\uddf1 \ud83e\uddf1 \ud83e\uddf1 \ud83e\uddf1           \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n**Zone Details**:\n\n1. **Central Plaza** (High-Value Zone)\n   - Contains gold ore (\ud83d\udcb0), diamond ore (\ud83d\udc8e), and premium blade souls (\u2b50)\n   - Becomes hotly contested in final 30 seconds when extra resources spawn\n   - Open space encourages combat\n\n2. **Wooden Crate Area** (Safe Grinding Zone)\n   - Dense cluster of easily destroyed crates\n   - Good for early growth\n   - Low risk, moderate reward\n\n3. **Trees & Stumps Zone** (Medium Value)\n   - Tree stumps require multiple hits\n   - Drops moderate blade souls\n   - Scattered layout\n\n4. **Stone Mine** (Durable Resources)\n   - Stone blocks and iron ore\n   - Takes longer to destroy but gives good length\n   - Strategic choice for mid-game\n\n5. **Block Wall Perimeter** (Defensive Structures)\n   - Low destructible walls around edges\n   - Can be used for cover or destroyed for souls\n   - Creates tactical positioning opportunities\n\n**Destructible Objects** (Phase 1 Implementation):\n\n| Object | Hits to Destroy | Length Gain | Visual |\n|--------|----------------|-------------|---------|\n| Grass Block | 1 | +0.1m | Light green cube, scatters particles |\n| Wooden Crate | 1-2 | +0.3m | Brown box, drops blue soul |\n| Tree Stump | 2-3 | +0.5m | Dark brown cylinder, drops green soul |\n| Stone Block | 3-4 | +0.8m | Gray cube, cracks before breaking |\n| Gold Ore | 4-5 | +2.0m | Yellow-gold cube with sparkles, drops golden soul |\n| Diamond Ore | 5-6 | +3.0m | Cyan-blue cube with shine, drops large blue soul |\n\n**Blade Souls** (Collectible Drops):\n- Small blue soul: +0.3m (from crates)\n- Green soul: +0.5m (from stumps)\n- Silver soul: +0.8m (from stone/iron)\n- Golden soul: +2.0m (from gold ore)\n- Large blue soul: +3.0m (from diamond ore)\n- Enemy soul: +20% of defeated enemy's current length (red-orange glow)\n\n**Dynamic Events**:\n- **0:00-0:90** - Normal resource distribution\n- **0:90-1:20** - Final 30 seconds: Central Plaza spawns 2x gold ore, 2x diamond ore, and premium souls\n- **Visual cue**: Screen edges pulse with orange glow during final 30 seconds\n\n## Edit Mode Design\n\nEdit mode allows players to customize the arena layout and game parameters to create their own variations.\n\n**Editable Properties**:\n\n1. **Arena Layout**\n   - Object placement: Drag and drop destructible objects anywhere in the arena\n   - Object types: Select from available object types (grass, crates, stumps, stone, ores)\n   - Object density: Slider to control how many objects spawn in each zone\n   - Zone boundaries: Adjust size and position of themed zones\n\n2. **Game Parameters**\n   - Match duration: 60-300 seconds (default: 120)\n   - Starting weapon length: 1-5 meters (default: 2)\n   - Growth rate multiplier: 0.5x-3x (default: 1x)\n   - AI enemy count: 0-15 (default: 8)\n   - Final rush timing: When premium resources spawn (default: last 30 seconds)\n\n3. **Object Properties** (per object type)\n   - Durability: How many hits to destroy (1-10)\n   - Length reward: How much length gained (0.1m-5m)\n   - Spawn frequency: How commonly it appears (rare/common/abundant)\n\n4. **Visual Customization**\n   - Color palette: Choose from preset themes (Classic/Neon/Pastel/Dark)\n   - Particle intensity: Low/Medium/High\n   - Weapon trail: On/Off\n\n**Constraints**:\n- Minimum 20 destructible objects must be placed\n- At least one high-value object (gold or diamond ore) must exist\n- AI count cannot exceed 15 (performance)\n- Match duration minimum 60 seconds (ensures playability)\n- Growth rate cannot be set below 0.5x (prevents frustrating progression)\n\n**Edit Mode Interface**:\n- Top toolbar: Object palette with icons\n- Side panel: Parameter sliders and toggles\n- Main view: Arena with drag-and-drop placement\n- Bottom: \"Test Play\" button to try changes immediately\n\n## Control Design\n\n**Mobile (Touch) Controls**:\n\n1. **Virtual Joystick** (Left side, bottom half)\n   - Appears where player touches the left half of screen (below y=640)\n   - Analog movement in 8 directions\n   - Base moves with finger if exceeding max radius\n   - Dead zone: 15px radius\n   - Max radius: 80px\n   - Visual: Semi-transparent circle with knob\n\n2. **Defense Button** (Right side, bottom corner)\n   - Fixed position: bottom-right (x=620, y=1180)\n   - Size: 96\u00d796 pixels\n   - Hold to enter defensive stance (blocks frontal attacks)\n   - Visual: Shield icon\n\n3. **Skill Button** (Right side, middle-bottom)\n   - Fixed position: (x=620, y=1060)\n   - Size: 96\u00d796 pixels\n   - Appears only when skill is available\n   - Tap to activate collected skill power-up\n   - Visual: Icon matching current skill\n\n**Desktop (Keyboard/Mouse) Controls**:\n\n1. **Movement**\n   - WASD or Arrow Keys for 8-directional movement\n   - Continuous movement while held\n   - Movement direction determines attack direction\n\n2. **Defense**\n   - Hold Shift or Right Mouse Button for defensive stance\n\n3. **Skill Activation**\n   - Spacebar or Left Mouse Click to use skill (when available)\n\n**Attack Mechanics** (Automatic):\n- Character automatically swings weapon every 0.8-1.5 seconds (speed depends on weapon length)\n- Swing direction follows movement direction\n- If stationary, swings in last movement direction\n- Weapon hitbox extends from character position in swing direction\n- Visual: Weapon sweeps in an arc, leaving trail effect\n\n**Input Handling**:\n- Prevent default browser behavior for all used keys\n- Clear input state on window blur\n- Track touch IDs separately to prevent interference\n- Normalize key codes to lowercase\n\n## Art Style and Visual Theme\n\n**Overall Style**: Minecraft-inspired blocky/voxel aesthetic with bright, saturated colors and a cheerful, arcade-game feel.\n\n**Visual Characteristics**:\n- **Geometric Simplicity**: All objects are composed of cubes, rectangular prisms, and simple shapes\n- **Bright Palette**: Vibrant, saturated colors (grass green, sky blue, golden yellow, diamond cyan)\n- **Flat Shading**: Minimal gradients, solid colors with simple highlights\n- **Chunky Proportions**: Characters and objects have exaggerated, toy-like proportions\n- **Pixel-Perfect Edges**: Clean, crisp edges on all block elements\n\n**Character Design**:\n- Player character: Blocky humanoid (similar to Minecraft Steve)\n  - Head: 1:1 cube\n  - Body: 1:1.5 rectangular prism\n  - Arms/Legs: Thin rectangular prisms\n  - Size: ~40\u00d760 pixels at default zoom\n  - Simple facial features (two square eyes, simple mouth)\n- AI enemies: Same base model with different color schemes (red, purple, orange, green tints)\n\n**Weapon Design** (Phase 1 - Simple Version):\n- Rectangular blade extending from character\n- Width: 8-12 pixels\n- Length: Scales from 20px (2m) to 990px (99m)\n- Color: Starts gray-brown (wood), becomes silver (stone), then gold (at 50m+)\n- Trail effect: Semi-transparent copies fade behind weapon during swing\n- Visual milestones:\n  - 2-10m: Wooden brown\n  - 10-25m: Stone gray with slight glow\n  - 25-50m: Iron silver with white glow\n  - 50-75m: Gold with yellow particles\n  - 75-99m: Diamond cyan with sparkle trail\n\n**Environment Design**:\n- Ground: Textured grid of grass/dirt blocks (green with brown specks)\n- Destructible objects: Distinct colors and simple textures\n  - Grass: Light green (#7FD957)\n  - Wood: Brown (#8B4513)\n  - Stone: Gray (#808080)\n  - Gold: Yellow-gold (#FFD700)\n  - Diamond: Cyan-blue (#00CED1)\n- Background: Solid color or simple gradient (sky blue to light cyan)\n\n**Particle Effects**:\n- Destruction particles: Small colored cubes matching destroyed object\n- Blade souls: Glowing orbs with pulsing animation\n  - Blue souls: #4169E1 with white core\n  - Golden souls: #FFD700 with yellow core\n  - Red souls (from enemies): #FF4500 with orange core\n- Impact effects: White flash + small shockwave ring\n- Trail effects: Semi-transparent weapon copies fading out\n\n**UI Style**:\n- Blocky, pixel-art inspired fonts\n- Solid color backgrounds with thick borders\n- Icons use same blocky aesthetic\n- High contrast for readability\n\n**Asset Generation vs Code Rendering**:\n- **Generate as Assets**: Character sprites (player + AI variants), weapon sprites at key milestones, destructible object sprites, blade soul sprites, particle textures\n- **Render with Code**: UI elements (timer, length counter), grid background, weapon scaling/rotation, particle systems, trail effects, hit flashes\n\n**Viewing Angle**: Top-down (bird's eye view) with slight isometric tilt (optional, can be pure top-down for simplicity)\n\n**Consistency Requirements**:\n- All assets must use the same blocky/voxel style\n- Maintain consistent lighting direction (top-left light source)\n- Use the same color palette across all elements\n- Keep proportions consistent (character ~40\u00d760px, objects 30-50px cubes)", "persistent_storage_config": {"leaderboard": {"sort_ascending": false}}, "game_metadata": {"game_id": "01KSW6V1CKG2TWCDP66WNWD68Y", "title": "My 99-Meter Blade", "description": "Swing your growing weapon to smash blocks and defeat rivals in this chaotic 120-second arena brawl!", "thumbnail_url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_108962118237074986778/1781698124493_01KVAQQPPDK6HW1JA7CBBDE535.99blade.gif", "created_at": 1780137555, "updated_at": 1781700791611.8853, "art_style": null}, "task_plan": null}