{"asset_map": {"grass_block_top": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/grass_block_top_9aa7dee9-3ef6-40cc-bcae-301c934fb7c0.webp", "type": "image", "prompt": "A 32x32 pixel art texture for the top face of a grass block. Use a bright, vibrant green color (#5CB531) with subtle darker green pixel variations to suggest individual grass blades. Clean pixel art style, high-quality game asset. Viewing angle: directly top-down. The texture must be perfectly tileable on all edges to repeat seamlessly in both horizontal and vertical directions.", "description": "32x32 pixel art texture for the top face of a grass block, tileable in both directions.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "grass_block_side": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/grass_block_side_2e661c3d-0527-44f3-9979-2b32e6291712.webp", "type": "image", "prompt": "A 32x32 pixel art texture for the side face of a grass block. Top third is bright green (#5CB531) representing a grass layer, bottom two-thirds is brown dirt (#8B6F47). Clean horizontal division. Style: Clean pixel art matching grass_block_top. Viewing angle: straight-on side view. Seamlessly tileable horizontally.", "description": "32x32 pixel art texture for the side face of a grass block, showing grass on top and dirt below.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "dirt_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/dirt_block_4aa9c870-3233-477d-94ca-b3ef21a96ae9.webp", "type": "image", "prompt": "Generate a 32x32 pixel art dirt block texture. Use a rich palette of brown earthen tones including #8B6F47, #6B5436, #A0826D, and #7A5C3E. The texture should feature varied shades with darker brown spots and lighter tan variations for visual interest. Ensure a seamless tileable pattern that works on all faces of a cube, matching a classic Minecraft-style voxel aesthetic. Professional quality pixel art, solid opaque texture with no transparency.", "description": "A 32x32 pixel art dirt texture for use on all faces of a voxel dirt block, featuring rich brown earthen tones.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "stone_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/stone_block_f2c42f1a-a1a2-4b75-bf7b-ac54ec0f0811.webp", "type": "image", "prompt": "A 32x32 pixel art texture for a stone block. Medium gray color (#7A7A7A) with darker gray (#5A5A5A) and lighter gray (#9A9A9A) pixel variations creating a rocky, natural stone appearance. Clean pixel art with subtle texture. Fully tileable in both directions for seamless 3D cube application.", "description": "32x32 pixel art stone texture for rocky terrain blocks.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "wood_block_top": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wood_block_top_4f7d132a-7526-4950-a003-d6454068951e.webp", "type": "image", "prompt": "A 32x32 pixel art texture for the top face of a wood log. Warm brown color (#8B5A2B) showing tree rings with concentric circles in slightly darker brown (#6B4A1B) radiating from center. Clean pixel art style. Viewing angle: top-down cross-section. Seamlessly tileable in both directions.", "description": "32x32 pixel art texture for the cross-section of a wood log block.", "subtype": "object", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "wood_block_side": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wood_block_side_fe8cec81-4adf-44e3-9311-f72d55a94ffa.webp", "type": "image", "prompt": "A 32x32 pixel art texture for the side face of a wood log. Warm brown color (#8B5A2B) with vertical darker brown (#6B4A1B) lines suggesting bark texture and wood grain. Clean pixel art matching wood_block_top. Side view. Fully tileable horizontally and vertically for continuous trunk appearance.", "description": "32x32 pixel art texture for the bark/side of a wood log block.", "subtype": "object", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "leaves_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/leaves_block_0b883a5d-a5ea-446c-a04d-3a11124c0ba2.webp", "type": "image", "prompt": "A 32x32 pixel art texture for a leaves block. Forest green color (#2D5016) with lighter green (#3D6026) and darker green (#1D4006) pixel variations for a leafy, organic appearance. High-quality clean pixel art. Fully tileable in both directions for seamless tree crowns.", "description": "32x32 pixel art leaf texture for tree canopy blocks.", "subtype": "object", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "hotbar_slot_bg": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/hotbar_slot_bg_2b37a7c1-3450-47ab-8181-9874623ac0c9.webp", "type": "image", "prompt": "A 64x64 pixel art UI element for a hotbar inventory slot. A dark gray (#3A3A3A) rounded square with a 2-3 pixel thick lighter gray (#5A5A5A) border. Inset appearance, clean professional pixel art UI, transparent background outside the slot.", "description": "UI element for the background of an inactive hotbar inventory slot.", "subtype": "ui", "aspect_ratio": [770, 748], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "hotbar_slot_selected": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/hotbar_slot_selected_9c38c440-36f1-4645-998b-694fad8f5703.webp", "type": "image", "prompt": "A 64x64 pixel art UI element for the selected hotbar slot. Match the style and shape of hotbar_slot_bg, but use a bright golden-yellow (#FFD700) border instead of gray. Bright, active appearance. Clean pixel art UI, transparent background outside the slot.", "description": "UI element for the background of the currently active/selected hotbar slot.", "subtype": "ui", "aspect_ratio": [950, 943], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "block_break_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/block_break_sound_06356295-28b3-412c-8d66-0fe16eec765c.mp3", "type": "audio", "prompt": "A short, satisfying block-breaking sound effect. It features a sharp initial crack followed by a quick, gravelly crumble texture. The sound is punchy and organic, resembling the breaking of stone or dry wood. High-quality, clean recording with a natural decay, designed for frequent use in a sandbox building game. No digital clipping or harsh high-frequencies.", "description": "A crisp and crunchy sound effect for breaking voxel blocks, providing tactile feedback.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.86, "loop": false, "animation": null}, "block_place_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/block_place_sound_35ffe15b-2070-4cc3-88f4-df35d91b78b2.mp3", "type": "audio", "prompt": "A very short, solid block-placing sound effect. A firm 'thunk' or heavy 'click' suggesting a heavy object snapping perfectly into a grid. It has a slightly muffled, low-mid frequency focus to distinguish it from the sharper breaking sound. Precise, satisfying, and mechanical. Clean one-shot with no background noise.", "description": "A solid, tactile 'thunk' sound for placing blocks in the world.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2, "loop": false, "animation": null}, "ambient_day_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/ambient_day_music_38068420-cedd-48d2-8e14-cc02f61b0a59.mp3", "type": "audio", "prompt": "A calm, peaceful ambient track for a creative sandbox game. Features gentle felt piano melodies, soft acoustic guitar plucking, and very light, warm string pads in the background. Medium-slow tempo, 80-90 BPM. The mood is hopeful, bright, and unobtrusive, inspired by C418's Minecraft soundtracks. Melodic but minimalist, allowing for deep focus. Seamlessly loopable with a smooth fade-out and fade-in point.", "description": "Peaceful and inspiring acoustic background music for daytime exploration and building.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 150.04734693877552, "loop": true, "animation": null}, "ambient_night_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/ambient_night_music_3a303c74-224d-40b1-ad49-5fd120d3a3ab.mp3", "type": "audio", "prompt": "Atmospheric and mysterious nighttime ambient music. Features soft, evolving synth pads, distant and reverb-heavy piano notes, and subtle crystalline textures. The mood is peaceful yet slightly melancholic and spacious. Very slow tempo, focusing on texture and harmony over rhythm. Minimalist arrangement that evokes a sense of solitude under a starlit voxel sky. Seamlessly loopable.", "description": "Atmospheric and slightly mysterious synth-based music for the nighttime cycle.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 180.03591836734694, "loop": true, "animation": null}, "zombie_groan_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/zombie_groan_sound_2317293b-c2b3-4b3a-a06e-4e2906030537.mp3", "type": "audio", "prompt": "Low, guttural zombie groan sound effect, creepy and unsettling deep raspy moan. Low-fi 8-bit retro game aesthetic, bit-crushed texture, reminiscent of classic voxel sandbox games. Short, punchy, and atmospheric for ambient use, clear of background noise.", "description": "Low-fi, guttural zombie moan for a voxel-based survival game.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.95, "loop": false, "animation": null}, "creeper_hiss_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/creeper_hiss_sound_c51f960e-e617-4755-99f1-7cb7fcc51124.mp3", "type": "audio", "prompt": "A sharp, aggressive hissing sound effect resembling a lit fuse or pressurized steam release. The sound starts at a medium volume and rapidly builds in intensity and frequency over 2 seconds. Lo-fi, 8-bit aesthetic to match a voxel-based sandbox game. It should sound dangerous and urgent, ending abruptly to signify the moment of explosion. No background noise, clean white-noise based fizzing texture.", "description": "A tense, escalating fuse hiss for an exploding enemy, warning the player of danger.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2, "loop": false, "animation": null}, "explosion_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/explosion_sound_08bec177-0dd1-4400-8838-c5e893ef01e5.mp3", "type": "audio", "prompt": "Create a powerful, stylized explosion sound effect for a voxel-based sandbox game. The sound should feature a sharp, high-frequency initial 'pop' or 'crack' followed by a solid, punchy mid-range 'boom' and a brief, crunchy debris-falling tail. It should sound more like an arcade game effect than a realistic military explosion. Clean, impactful, and non-distorted, with a fast decay. No reverb, suitable for immediate feedback when a mob destroys blocks.", "description": "A punchy, arcade-style explosion sound for a creeper detonation, designed to follow the 'creeper_hiss_sound' for high-impact feedback.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.23, "loop": false, "animation": null}, "player_hurt_sound": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1778937515454_01KRRF0KDYFYMRGABW6F0AH519.minecraft_hit_soundmp3converter.mp3", "type": "audio", "prompt": null, "description": "Player damage sound effect - plays when the player takes damage from zombies, fall damage, lava, or other sources", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0, "loop": null, "animation": null}, "dynamite_block": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1773085193390_01KKA1SX5EJ3Q150SCRVR1C8H7.image.png", "type": "image", "prompt": null, "description": "A 32x32 pixel art TNT block texture with red/orange top and bottom sections and \"TNT\" text in the middle, matching classic Minecraft TNT aesthetics for the voxel game world.", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "sand_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/sand_block_76c4f497-44a0-4013-8b78-ba396fe3c45d.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for a sand block, designed to be fully tileable in both horizontal and vertical directions. Use a warm beige and tan color palette (#E0C896, #D4B896, #C9A876). The texture should have a grainy appearance with small speckles to simulate sand particles. Ensure clean, crisp pixels with no anti-aliasing. The style must perfectly match the existing pixel art aesthetic of 'grass_block_top' and 'dirt_block' for a voxel game world.", "description": "A 32x32 pixel art sand texture for use on all faces of a voxel sand block, featuring warm tan tones and a grainy texture.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "water_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/water_block_7727ab69-4a53-4470-8842-5f12b3af0888.webp", "type": "image", "prompt": "Generate a 32x32 pixel art water texture for a voxel game, designed to be fully tileable in both directions. Use a palette of clear ocean blues (#4A90E2, #5BA3F5, #3A7BC8) with lighter blue highlights for wave ripples and surface shimmer. The style must match the existing 'grass_block_top' and 'dirt_block' with clean, crisp pixels and no anti-aliasing. The texture should suggest a translucent liquid appearance suitable for a Minecraft-style aesthetic.", "description": "A 32x32 tileable pixel art water texture for a voxel-style game world, featuring ripples and shimmer.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "loading_pickaxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/loading_pickaxe_634ab1a1-b9bc-4288-8b27-a819fdfb8aef.webp", "type": "image", "prompt": "Generate a professional 64x64 pixel art pickaxe for a voxel game loading screen. The pickaxe features a brown wooden handle and a gray stone-textured head, angled at a 45-degree diagonal orientation. High contrast with a clean, sharp silhouette. Style must match the blocky, 8-bit aesthetic of existing voxel textures like 'stone_block'. Transparent background, centered, full tool visible.", "description": "A pixel art pickaxe tool for a Minecraft-style loading screen animation", "subtype": "ui", "aspect_ratio": [819, 870], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "wooden_pickaxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wooden_pickaxe_ff63131f-7a41-456c-8be5-488e7b3adf94.webp", "type": "image", "prompt": "Generate a 32x32 pixel art wooden pickaxe tool icon for a voxel-style game. The design should feature a classic T-shape head and a sturdy handle, both made of wood with warm brown tones and visible grain. The pickaxe should be oriented diagonally from bottom-left to top-right. Use a clean, polished pixel art style consistent with a Minecraft aesthetic. High resolution, detailed pixels, transparent background.", "description": "A 32x32 pixel art wooden pickaxe tool icon for inventory display.", "subtype": "ui", "aspect_ratio": [851, 890], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "wooden_axe": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777415194244_01KQB371M4V1P9KTA82DHWA5KK.axe.webp", "type": "image", "prompt": null, "description": "Custom wooden axe icon for inventory and UI, matching the Minecraft-style voxel aesthetic.", "subtype": null, "aspect_ratio": [959, 959], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "wooden_sword": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1779729436428_01KSG283RC9MAF9AP0MJN720XV.image.png", "type": "image", "prompt": null, "description": "User-uploaded pixel-art weapon sprite to use as the wooden_sword item texture.", "subtype": null, "aspect_ratio": [160, 160], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "crafting_table_top": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777414994209_01KQB30Y911HGJE8W0KRP6F9VM.craft_block_top.webp", "type": "image", "prompt": null, "description": "Custom crafting table top texture - pixel art grid pattern for the top face of a crafting table block", "subtype": null, "aspect_ratio": [959, 959], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "crafting_table_side": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777415088998_01KQB33TV6071BK8CX2XGC7DPH.craft_block_side.webp", "type": "image", "prompt": null, "description": "User-uploaded texture for the side face of a crafting table block, featuring custom wood planks and tool details", "subtype": null, "aspect_ratio": [959, 959], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "planks_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/planks_block_279366e6-4852-411d-b30f-9c5981c5dc20.webp", "type": "image", "prompt": "Generate a 32x32 pixel art wooden planks texture for a Minecraft-style voxel game. The texture should show horizontal wooden planks with light brown and tan wood colors, visible grain lines, and subtle darker lines between individual planks. Fully tileable and seamless in both horizontal and vertical directions. Polished pixel art style consistent with classic voxel sandbox games.", "description": "A 32x32 pixel art wooden planks texture for a voxel block, tileable in all directions.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "stick_item": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/stick_item_cbbb7de9-37ce-4a48-8760-b273b29e9f0e.webp", "type": "image", "prompt": "Generate a 32x32 pixel art wooden stick item icon for a Minecraft-style voxel game. The stick is oriented diagonally from the bottom-left to the top-right. Use a light brown wood palette with subtle shading for depth. High resolution pixel art, clean lines, professional quality game asset, transparent background.", "description": "A 32x32 pixel art stick item icon for the game inventory.", "subtype": "ui", "aspect_ratio": [770, 800], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "coal_item": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/coal_item_7d47ccde-5b7e-4ff5-86de-1c999afa6cdd.webp", "type": "image", "prompt": "Generate a 32x32 pixel art coal item icon for a Minecraft-style voxel game inventory. The icon should feature a black and dark gray lumpy coal chunk with subtle highlights to define its irregular crystalline surface. Clean, sharp pixel art style, high contrast, transparent background. Match the visual aesthetic of existing items like stick_item.", "description": "A 32x32 pixel art coal item icon for the game inventory.", "subtype": "object", "aspect_ratio": [856, 895], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "torch_block": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777422395031_01KQBA2SMQGTY4WW3WDMH04GKJ.torch.webp", "type": "image", "prompt": null, "description": "Pixel art torch block texture for a voxel game, featuring a wooden stick base and bright burning flame", "subtype": null, "aspect_ratio": [959, 959], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "coal_ore_block": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777418500801_01KQB6BYP1JTDDVWWPG1AHK7Z7.image.png", "type": "image", "prompt": null, "description": "User-uploaded coal ore block texture for the Minecraft-style voxel game. Used as the texture for coal ore blocks that spawn underground in the stone layer.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "stone_pickaxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/stone_pickaxe_14ed2d70-8b98-4a66-893d-3072cad8dac6.webp", "type": "image", "prompt": "Generate a 32x32 pixel art stone pickaxe icon for a Minecraft-style game, transparent background. The pickaxe head is made of medium gray stone with dark gray shading, and the handle is a brown wooden stick. The tool is angled diagonally from top-left (head) to bottom-right (handle). Use a clean, simple silhouette and high contrast pixel art style matching the aesthetic of wooden_pickaxe and stone_block.", "description": "A 32x32 pixel art stone pickaxe tool icon for inventory display.", "subtype": "ui", "aspect_ratio": [931, 1007], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "stone_axe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/stone_axe_c5ab45c4-c025-4a57-9648-4836321ee01a.webp", "type": "image", "prompt": "Generate a 32x32 pixel art stone axe icon for a voxel game inventory. The axe blade is made of medium gray stone with dark gray shading, and the handle is a simple brown wooden stick. The tool is angled diagonally from top-left to bottom-right. Style should perfectly match the existing wooden_axe and stone_block. Transparent background, high contrast, clean silhouette, professional game-ready pixel art.", "description": "A 32x32 pixel art stone axe tool icon for the game inventory, matching the Minecraft-style aesthetic.", "subtype": "ui", "aspect_ratio": [976, 992], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "stone_sword": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/stone_sword_d6e457d7-4ef2-4576-b7fd-c0afee1bcab0.webp", "type": "image", "prompt": "Generate a 32x32 pixel art stone sword icon for a voxel game inventory. The sword features a long, pointed blade made of medium gray stone (#7a7a7a) with dark gray shading (#5a5a5a) for depth, a small stone crossguard, and a brown wooden handle (#8b5a2b). The tool is angled diagonally from top-left to bottom-right. Use a clean, low-resolution pixel art style with a clear silhouette and high contrast, matching the style of wooden_sword and stone_block. Transparent background.", "description": "A 32x32 pixel art stone sword icon for the game inventory, matching the Minecraft voxel aesthetic.", "subtype": "ui", "aspect_ratio": [962, 1022], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "wooden_shovel": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wooden_shovel_de35fc80-69c5-4385-821d-f28d82f6d35d.webp", "type": "image", "prompt": "Generate a 32x32 pixel art wooden shovel icon for a game inventory. The shovel features a wide wooden blade at the top and a handle below. Use light brown oak wood colors with visible grain and subtle shading. Match the artistic style, diagonal orientation, and color palette of image 1 and image 2. Clean pixelated edges, high quality, transparent background.", "description": "A 32x32 pixel art wooden shovel icon for the inventory, matching the Minecraft voxel aesthetic.", "subtype": "ui", "aspect_ratio": [925, 990], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "stone_shovel": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/stone_shovel_521df203-180b-4b37-a609-157d915b7c23.webp", "type": "image", "prompt": "Generate a 32x32 pixel art stone shovel tool icon for a voxel game inventory. The shovel features a gray stone blade at the top, matching the material of stone_pickaxe and stone_axe, and a brown wooden handle matching stick_item. Use a clean pixel art style with subtle shading, viewed at a slight diagonal angle. Transparent background, high resolution pixel art, game-ready UI asset.", "description": "A 32x32 pixel art stone shovel icon for the inventory, matching the existing stone tool set.", "subtype": "ui", "aspect_ratio": [896, 977], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "oak_sign": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777517719262_01KQE4ZVPYHF4HVH0447MKWSWY.image-1.png (16).png", "type": "image", "prompt": null, "description": "Wooden plank texture for oak sign boards - horizontal grain pattern", "subtype": null, "aspect_ratio": [496, 366], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "chest_front": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/chest_front_a1ae73b4-b7cd-480c-9e51-e1671b263898.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for the front face of a Minecraft-style wooden storage chest. The texture should feature dark brown wood planks, a distinct horizontal line for the lid, and a central golden-brass latch lock. Use a clean, voxel-game aesthetic with high-contrast pixels and limited color palette to match the existing blocky style. Ensure the design is centered and fits perfectly within the square frame.", "description": "32x32 pixel art texture for the front face of a voxel chest with a golden latch", "subtype": "object", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "chest_side": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/chest_side_3a6eb84c-a486-4c0c-b2d5-74e979e17825.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for the side face of a Minecraft-style wooden storage chest. The design should feature dark brown wood planks with a horizontal grain, and a slightly lighter wood band near the top to represent the lid line. Ensure there is no latch or lock on this side. The style must match the 'chest_front' asset with clean, blocky voxel game aesthetics and a consistent color palette.", "description": "Side face texture for a wooden storage chest in pixel art style.", "subtype": "object", "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "chest_top": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/chest_top_bc0155f4-551b-4938-89ec-16b0f16261aa.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for the top face of a Minecraft-style wooden storage chest lid. Top-down view showing a dark brown wood planks pattern with subtle wood grain. Use a clean, polished voxel game aesthetic. Match the wood color and artistic style of chest_front and chest_side. High quality pixel art, 32-bit style.", "description": "A 32x32 pixel art texture for the top face of a wooden storage chest lid.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "furnace_front_off": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777545757537_01KQEZQGV150WPGDN56JQMA2FT.furnace_1.png", "type": "image", "prompt": null, "description": "Front face texture of an inactive furnace block with a dark opening", "subtype": null, "aspect_ratio": [495, 495], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "furnace_front_on": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777545817543_01KQEZSBE7RXHKB3042XBA7HQB.furnace.png", "type": "image", "prompt": null, "description": "Front face texture of a lit furnace showing glowing fire opening", "subtype": null, "aspect_ratio": [495, 495], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "furnace_side": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777545871118_01KQEZTZREWNBKT3Y2MWVV5X2D.furnace_3.png", "type": "image", "prompt": null, "description": "32x32 pixel art stone texture for the side and back faces of a furnace block", "subtype": null, "aspect_ratio": [495, 495], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "furnace_top": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777545918564_01KQEZWE340Q4R3MXZVCEW1M45.furnace_2.png", "type": "image", "prompt": null, "description": "Top face texture for a furnace block in pixel art style", "subtype": null, "aspect_ratio": [495, 495], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "bedrock_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/bedrock_block_95300f04-bf24-451c-9f35-0de33eb86874.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture of bedrock for a voxel game. Dark gray and black color palette with irregular, chaotic rocky patterns and ancient cracks. The texture must be perfectly tileable in all directions (horizontal and vertical). Stylized Minecraft-like aesthetic with visible, sharp pixels. The appearance should suggest an impenetrable and indestructible stone material.", "description": "An unbreakable, dark gray/black rocky bedrock texture for the bottom layer of the voxel world.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "glass_block": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1777615231787_01KQH1ZPSBR66Y66T1YQN2WRRM.image (2).webp", "type": "image", "prompt": null, "description": "Custom glass block texture with light blue tint and blue outlines on white background", "subtype": null, "aspect_ratio": [833, 849], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "iron_ore_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/iron_ore_block_77cf92aa-96c3-4dd9-882c-88e420d20250.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for an iron ore block in a voxel game. The base is a rough gray stone, matching the style of 'stone_block'. Scattered throughout are warm beige, tan, and light orange-brown iron ore deposits. The deposits should be distinct, pixelated spots. The texture must be perfectly tileable in all directions and match the classic Minecraft aesthetic. High contrast between the gray stone and the warm tan iron specks.", "description": "32x32 pixel art iron ore texture for voxel blocks with gray stone and tan deposits.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "iron_ingot": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/iron_ingot_bd1fd3be-7865-4653-8565-b6909fedca3b.webp", "type": "image", "prompt": "Generate a 32x32 pixel art iron ingot item icon for a Minecraft-style voxel game. The ingot is a simple elongated rectangular metal bar with beveled edges. Use a silvery-gray metallic color palette with subtle shading and highlights to create a clean, iconic 3D look. Professional quality game asset, transparent background, centered composition.", "description": "A 32x32 pixel art iron ingot item icon for the inventory, featuring a silvery-gray metallic finish.", "subtype": "ui", "aspect_ratio": [854, 737], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "gold_ore_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/gold_ore_block_41f602ef-0c5b-46cc-9c2e-eb37aa586b6e.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for a gold ore block in a voxel game. The texture should feature a dark gray stone background identical in style to 'stone_block', with embedded bright yellow and golden metallic veins and chunks scattered throughout. Use warm yellow-gold (#FFD700) tones for the ore to make it stand out. Ensure it is a fully tileable texture that repeats seamlessly in both directions, matching the aesthetic of 'iron_ore_block'. Professional quality game asset.", "description": "A 32x32 pixel art gold ore block texture with yellow metallic veins in gray stone.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "diamond_ore_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/diamond_ore_block_70296a07-3298-41b4-9fa2-ca72a568386c.webp", "type": "image", "prompt": "Generate a 32x32 pixel art diamond ore texture for a voxel block. The texture should feature a dark gray stone background identical in style to image 1, with embedded bright cyan and light blue (#00FFFF) crystalline deposits scattered throughout. Include small white sparkle highlights on the crystals to make them stand out. The texture must be seamless and tileable in both horizontal and vertical directions, following a clean and sharp pixel art game-ready aesthetic.", "description": "32x32 pixel art diamond ore texture with cyan crystals on a gray stone background, matching the Minecraft aesthetic.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "gold_ingot": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/gold_ingot_520d58b0-0621-4df8-bc77-abd580d60394.webp", "type": "image", "prompt": "Generate a 32x32 pixel art gold ingot item icon for game inventory display. The asset should be a classic gold bar shape with bright yellow-gold metallic coloring (#FFD700), using highlights and shadows to show 3D depth. Ensure it matches the Minecraft-style voxel aesthetic and the metallic shading style of the iron_ingot asset. Clean, polished, transparent background, front-view illustration.", "description": "A 32x32 pixel art gold ingot item icon for the inventory, featuring a bright yellow-gold metallic finish.", "subtype": "ui", "aspect_ratio": [1022, 882], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "diamond_item": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/diamond_item_e99c983e-a902-440d-8ff7-60abd5298a68.webp", "type": "image", "prompt": "Generate a 32x32 pixel art diamond gem item icon for a game inventory. The diamond should have a classic cut gemstone shape with bright cyan and light blue coloring (#00FFFF / #87CEEB). It should feature a faceted appearance with white sparkle highlights to show brilliance, matching the Minecraft-style voxel aesthetic. Clean edges, high contrast, transparent background.", "description": "A 32x32 pixel art diamond gem item icon for the game inventory.", "subtype": "ui", "aspect_ratio": [746, 736], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "iron_pickaxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/iron_pickaxe_59ebed88-b1f7-4fd4-85bb-7388c2955a59.webp", "type": "image", "prompt": "Generate a 32x32 pixel art iron pickaxe tool icon for a voxel game inventory. The pickaxe features a sleek silvery-gray metallic head and a sturdy brown wooden handle. Maintain a clean, polished pixel art style that perfectly matches the aesthetic of image 1 and image 2. Transparent background, high resolution, professional game asset.", "description": "A silvery-gray iron pickaxe icon for the game inventory.", "subtype": "ui", "aspect_ratio": [945, 1023], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "iron_axe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/iron_axe_3694af6b-dba9-426e-8acd-99fff2846925.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of an iron axe for a voxel game inventory. The axe head should be a polished silvery-gray metallic color, and the handle should be a simple wooden brown. Use a clean, sharp pixel art style that matches the aesthetic of 'wooden_axe' and 'stone_axe'. The item should be centered with a transparent background, high resolution pixel art, professional game asset.", "description": "A 32x32 pixel art iron axe tool icon for inventory display, featuring a metallic finish and wooden handle.", "subtype": "ui", "aspect_ratio": [946, 1006], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "iron_sword": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/iron_sword_193d8fa1-6237-4235-aec3-b386c271275e.webp", "type": "image", "prompt": "Generate a 32x32 pixel art iron sword icon for a voxel game inventory. The blade should be a polished silvery-gray metallic iron color, and the hilt/handle should be a simple wooden brown. Use a clean, sharp pixel art style that matches the design of 'wooden_sword' and 'stone_sword'. Side-view diagonal orientation, high resolution pixel art, transparent background.", "description": "A 32x32 pixel art iron sword icon for the game inventory, matching the iron tool set.", "subtype": "ui", "aspect_ratio": [960, 1021], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "iron_shovel": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/iron_shovel_6cc23f89-b688-4903-859a-06deb261edb3.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of an iron shovel for a voxel game inventory. The shovel head should be a polished silvery-gray metallic iron, attached to a simple wooden brown handle. Use a clean, sharp pixel art style that matches the aesthetic of image 1 and image 2. The shovel should be angled diagonally. Transparent background, high resolution pixel art, game-ready asset.", "description": "A 32x32 pixel art iron shovel tool icon for inventory display, matching the iron tool set.", "subtype": "ui", "aspect_ratio": [934, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "gold_pickaxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/gold_pickaxe_0c36f801-ee89-4cf0-8f5e-4dffa5dde4ac.webp", "type": "image", "prompt": "Generate a 32x32 pixel art gold pickaxe tool icon for a Minecraft-style voxel game. The pickaxe head should be a bright yellow-gold metallic color with a wooden brown handle. Use a clean, polished pixel art style consistent with a 32x32 resolution. Transparent background, high resolution, detailed professional game asset.", "description": "A 32x32 pixel art gold pickaxe tool icon for inventory display.", "subtype": "ui", "aspect_ratio": [945, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "gold_axe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/gold_axe_9ff0c751-9e9c-49f2-9a82-41789773a254.webp", "type": "image", "prompt": "Generate a 32x32 pixel art gold axe icon for a voxel game inventory. The axe head should be a bright, shiny yellow-gold metallic color with a brown wooden handle, oriented diagonally. Use a clean, sharp pixel art style matching the aesthetic of classic Minecraft tools. High contrast, professional quality, transparent background.", "description": "A 32x32 pixel art gold axe tool icon for inventory display.", "subtype": "ui", "aspect_ratio": [1005, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "gold_sword": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/gold_sword_70461594-c719-467a-9759-f5038678242a.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of a gold sword for a Minecraft-style voxel game. The blade should be a bright, shimmering yellow-gold metallic color with darker gold shading, and the hilt/handle should be a simple wooden brown. Clean pixel art style with distinct edges, matching the visual aesthetic of iron_sword and stone_sword. Centered on a transparent background, high resolution pixel art.", "description": "A 32x32 pixel art gold sword icon for the game inventory.", "subtype": "ui", "aspect_ratio": [957, 1015], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "gold_shovel": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/gold_shovel_5bcbbc29-236a-444c-91c9-5bd4f9554977.webp", "type": "image", "prompt": "Generate a 32x32 pixel art gold shovel tool icon for a Minecraft-style voxel game. The shovel head should be a bright yellow-gold metallic color with a wooden brown handle. Clean, polished pixel art style matching the aesthetic of iron_shovel and stone_shovel. Front-facing illustration on a transparent background.", "description": "A 32x32 pixel art gold shovel tool icon for inventory display, matching the gold tool set.", "subtype": "ui", "aspect_ratio": [947, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "diamond_pickaxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/diamond_pickaxe_dbc2b251-393b-4c96-8dde-0ef84bd4c57c.webp", "type": "image", "prompt": "Generate a 32x32 pixel art diamond pickaxe tool icon for a Minecraft-style voxel game. The pickaxe head is made of glowing cyan and light blue diamond crystals, attached to a simple brown wooden handle. Maintain a clean, polished pixel art style consistent with existing game tools. Front view, isolated on a transparent background, high resolution pixel art.", "description": "A 32x32 pixel art diamond pickaxe tool icon for inventory display.", "subtype": "ui", "aspect_ratio": [941, 1023], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "diamond_axe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/diamond_axe_3d4f510d-ef35-4402-8b36-9ad7a48d92e9.webp", "type": "image", "prompt": "Generate a 32x32 pixel art diamond axe tool icon for a Minecraft-style voxel game. The axe head should be a vibrant cyan and light blue diamond material with a dark wooden brown handle. Match the clean pixel art style, perspective, and color palette of existing tool assets like 'wooden_axe' and 'iron_axe'. High-quality game-ready sprite on a transparent background.", "description": "A 32x32 pixel art diamond axe tool icon for inventory display.", "subtype": "ui", "aspect_ratio": [960, 1023], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "diamond_sword": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/diamond_sword_0008e5ce-272a-4a96-90cc-7149ec2fc989.webp", "type": "image", "prompt": "Generate a 32x32 pixel art diamond sword icon for a game inventory. The blade should be a vibrant cyan and light blue diamond material, with a simple wooden brown handle and hilt. Clean, polished pixel art style with distinct edges and no anti-aliasing, matching the aesthetic of classic Minecraft tools. High resolution within the pixel art style, isolated on a transparent background.", "description": "A cyan diamond sword icon for the inventory, matching the Minecraft voxel tool set.", "subtype": "ui", "aspect_ratio": [960, 1014], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "diamond_shovel": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/diamond_shovel_8150484a-b5c7-4bbb-9645-30e10f3ceb67.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of a diamond shovel for a voxel game inventory. The shovel head is made of cyan and light blue sparkling diamond material, attached to a simple wooden brown handle. Match the clean pixel art style and perspective of the existing shovel assets. High resolution pixel art, transparent background, centered composition.", "description": "A 32x32 pixel art diamond shovel tool icon for inventory display.", "subtype": "ui", "aspect_ratio": [939, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "raw_beef": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/raw_beef_4106e4df-b1d0-4813-a4bc-67310d40431f.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of a raw beef steak for a voxel game inventory. Use shades of red and pink for the meat with a small white marbling detail. The style must be chunky, blocky, and pixelated to match Minecraft aesthetics. High-quality game asset on a transparent background.", "description": "A 32x32 pixel art raw steak icon for the inventory, matching the Minecraft voxel aesthetic.", "subtype": "ui", "aspect_ratio": [850, 795], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "cooked_beef": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cooked_beef_54d38af3-98a5-4f4b-91c4-f03621a825e9.webp", "type": "image", "prompt": "Generate a 32x32 pixel art cooked beef steak icon for a Minecraft-style voxel game inventory. The steak should have a rich brown, seared coloring to indicate it is cooked, with darker brown shading and lighter highlights. Maintain a blocky, pixelated aesthetic with a clean dark outline. The style must match the existing raw_beef asset but with a cooked appearance. Transparent background, high resolution pixel art, front view.", "description": "A 32x32 pixel art icon of a cooked steak for the game inventory.", "subtype": "ui", "aspect_ratio": [1021, 959], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "apple_item": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/apple_item_381ba91f-0da6-4428-aae7-c1a673dce545.webp", "type": "image", "prompt": "Generate a 32x32 pixel art red apple item icon for a voxel game inventory. The apple is a bright red color with a single small green leaf and a tiny brown stem on top. Use a clean, blocky pixel art style with black outlines to match the Minecraft aesthetic. High resolution pixel art, transparent background.", "description": "A 32x32 pixel art red apple icon for the inventory.", "subtype": "ui", "aspect_ratio": [710, 849], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "bread_item": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/bread_item_88ce483a-f341-488f-a93f-5a722d20833d.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of a golden-brown bread loaf for a Minecraft-style voxel game inventory. The bread should have a rounded top, light brown crust, and a blocky, pixelated aesthetic. Clear, clean edges on a transparent background, matching the style of existing food items like apple and beef.", "description": "A 32x32 pixel art golden-brown bread loaf icon for the inventory.", "subtype": "ui", "aspect_ratio": [966, 943], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "raw_chicken": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/raw_chicken_dc1e928b-3611-4b71-bb11-19121b6becef.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of a raw chicken drumstick. Pale pink and fleshy tones to indicate uncooked poultry. Simple, clean Minecraft-style pixel art for a game inventory. High contrast, sharp edges, no anti-aliasing, transparent background. Style should match the existing raw_beef asset.", "description": "A 32x32 pixel art icon of raw chicken meat for the inventory, matching the existing food item style.", "subtype": "object", "aspect_ratio": [774, 923], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "cooked_chicken": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cooked_chicken_74c685c1-da54-4e62-8b1c-9a9c54ac6062.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of a cooked chicken drumstick for a game inventory. The chicken should have an appetizing golden-brown roasted color with slight highlights, matching the Minecraft voxel aesthetic. Transparent background, high resolution pixel art, consistent in style and shading with image 1.", "description": "A 32x32 pixel art icon of a golden-brown roasted chicken drumstick for the inventory.", "subtype": "object", "aspect_ratio": [915, 995], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "zombie_model": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1778777705184_01KRKPKJQ0FWTTXQTKX9CH2AAH.zombie_compressed.glb", "type": "model_3d", "prompt": null, "description": "Compressed GLB 3D model for zombie mobs with animations", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "steve_model": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1778776677487_01KRKNM73FA0T9Z9EB3YSNAGKW.steve_compressed.glb", "type": "model_3d", "prompt": null, "description": "Compressed GLB 3D model for Steve player character (male) with animations", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "alex_model": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1778777429042_01KRKPB51JJJQFWF64A55MSF3M.alex_compressed.glb", "type": "model_3d", "prompt": null, "description": "Compressed GLB 3D model for Alex player character (female) with animations", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "bucket_empty": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/bucket_empty_f5a68e07-f838-4d3f-8e99-c0e300cc3bc1.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of an empty iron bucket for a voxel game inventory. The bucket is made of metallic silver-gray iron, featuring a classic bucket silhouette with a thin handle on top. Clean pixel art style with dark outlines and simple shading to match the Minecraft aesthetic. Transparent background, front-view, professional game asset quality.", "description": "A 32x32 pixel art empty iron bucket icon for the inventory.", "subtype": "ui", "aspect_ratio": [630, 855], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "bucket_water": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/bucket_water_48481fdd-da79-4aa8-b93a-0f1032d3a822.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of a metallic silver bucket filled with blue water, with the water slightly overflowing at the top. Use a clean, Minecraft-inspired voxel game aesthetic with distinct pixel blocks, sharp outlines, and simple shading. Side-view, transparent background, high resolution pixel art.", "description": "A 32x32 pixel art water bucket icon with blue water inside for the inventory.", "subtype": "ui", "aspect_ratio": [828, 1138], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "lava_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/lava_block_c78a2f5b-39b0-4989-a44a-fe2060a2ff11.webp", "type": "image", "prompt": "Generate a 32x32 pixel art lava texture for a voxel game world, fully tileable in both horizontal and vertical directions. The design should feature a bright orange and red molten liquid with darker red and black spots to simulate a bubbling, flowing appearance. Include vibrant yellow-orange highlights to suggest a glowing, fiery heat. Use a clean pixel art style that matches a classic Minecraft-inspired aesthetic, ensuring no visible seams when repeated.", "description": "A tileable 32x32 pixel art lava texture for voxel terrain, featuring bubbling molten rock and glowing highlights.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "bucket_lava": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/bucket_lava_8dfffdfa-8425-48dd-82dc-e7059f075a9c.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of an iron bucket filled with glowing, molten lava. The bucket should be silvery-gray metal, matching the style of image 1 and image 2. The lava inside should be bright orange and red with yellow highlights to indicate heat. Clean pixel art on a transparent background, Minecraft voxel game aesthetic, high resolution and sharp edges.", "description": "A 32x32 pixel art lava bucket icon for the inventory, showing a metal bucket filled with glowing orange lava.", "subtype": "ui", "aspect_ratio": [707, 968], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "door_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/door_block_3ecfc28f-b173-490e-a5fe-3fe21f76f9e0.webp", "type": "image", "prompt": "Generate a 32x32 pixel art wooden door texture for a Minecraft-style game. The design features warm brown oak wood planks matching the color and style of planks_block, a darker wooden frame around the edges, a small 3x3 light-colored window near the top, and a small gold door handle on the right side. Clean, professional pixel art, front view, flat texture, suitable for a block face.", "description": "32x32 pixel art oak wooden door texture for a voxel block face", "subtype": "object", "aspect_ratio": [550, 929], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "villager_model": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1778932298554_01KRRA1CSTSS9QG1CH6EQA2TQY.villager_compressed.glb", "type": "model_3d", "prompt": null, "description": "Compressed GLB 3D model for villager NPCs with animations - passive mobs that wander around the world", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "villager_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/villager_sound_81630fd9-ae5e-4911-bd44-66a2d999c7a9.mp3", "type": "audio", "prompt": "Create a short, punchy 'hmm' vocalization for a friendly NPC. The sound should have a nasal, mumbly quality with a mid-range pitch. Apply a subtle 8-bit/retro bitcrushed effect to match a voxel sandbox aesthetic. It should sound peaceful and curious, similar to a classic block-game villager grunt. High clarity, no background noise, clean tail.", "description": "A short, retro-style nasal vocalization for a friendly villager NPC, suitable for idle communication.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.68, "loop": false, "animation": null}, "soil_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/soil_block_004ea999-e6dd-4d9e-9b50-83582990f2c5.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for a farmland soil block, designed to be fully tileable in both directions. The texture should feature rich, dark brown tilled earth with visible horizontal furrows and rows for planting. It must match the artistic style and pixel density of the existing dirt_block and grass_block, maintaining a clean Minecraft-style voxel aesthetic with consistent lighting and no visible seams.", "description": "A tileable 32x32 pixel art texture for tilled farmland soil with visible furrows.", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "rice_plant": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/rice_plant_2030d6e4-9d30-4e29-87bc-ff1ae675e1c5.webp", "type": "image", "prompt": "Generate a 32x32 pixel art illustration of a rice plant for a voxel-style game. The asset should feature bright green stalks with distinctive drooping light tan and golden rice grain clusters at the top. Use a clean, blocky Minecraft-style aesthetic. The plant should be centered on a transparent background, high resolution pixel art, professional quality, front-view.", "description": "A 32x32 pixel art rice plant with green stalks and golden grain heads for voxel farming.", "subtype": "object", "aspect_ratio": [787, 858], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "bed_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/bed_block_1e970ed0-2e14-4fa6-8707-5a8fede9bd03.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for a Minecraft-style bed block top view. The design features a vibrant red blanket covering most of the surface, a soft cream-white pillow at the top end, and a thin wooden frame visible at the very edges. Use a limited color palette, clean edges, and distinct pixels to match a classic voxel game aesthetic. The texture should look clear and recognizable as a bed top when applied to a 3D block.", "description": "Top view texture for a red bed block in pixel art style", "subtype": "object", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "villager_hurt_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/villager_hurt_sound_415d7cd7-d9c7-4815-94b9-f0e8a96a80b8.mp3", "type": "audio", "prompt": "A short, soft nasal vocalization of a villager getting hurt, 'hrm' or 'ungh' sound, slightly panicked tone. Low-fidelity retro aesthetic to match a voxel sandbox game. Clean, dry recording with no background noise and minimal reverb. Sudden onset with a quick decay, distinct from a peaceful idle sound. One-shot impact feedback, 0.5 seconds long.", "description": "Short, nasal 'hrm' grunt for a villager NPC taking damage, matching the voxel game aesthetic.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.03, "loop": false, "animation": null}, "flower_block": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1779128422509_01KRY52M3DGQRJZCSJPQQNVYC4.image-1.png (16).png", "type": "image", "prompt": null, "description": "A small, pixel art yellow tulip for billboard use.", "subtype": null, "aspect_ratio": [394, 1049], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "dog_hurt_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/dog_hurt_sound_e26ae42f-95c8-49a3-a78d-bdfaa5f0c3d5.mp3", "type": "audio", "prompt": "A short, high-pitched dog yelp sound effect with a slight retro 8-bit digital crunch. Punchy and immediate attack, resembling damage feedback in a voxel/Minecraft-style game. Quick 'yip' sound with a 0.5s duration, non-realistic, clean arcade aesthetic to match existing low-fi mob sounds. No background noise, no music, single one-shot effect.", "description": "Short, punchy dog yelp sound effect for damage feedback in a voxel-style game.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.81, "loop": false, "animation": null}, "creeper_texture": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1779292501329_01KS31HXAH9Y5T23PF180YZBJM.creeper_skin.png", "type": "image", "prompt": null, "description": "User-uploaded creeper skin spritesheet for the creeper mob texture.", "subtype": null, "aspect_ratio": [64, 32], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tv_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/tv_block_518d21fd-d70a-4ce4-a94f-c724cee1fbd8.webp", "type": "image", "prompt": "Transform the image to match asset 01 precisely: a 32x32 pixel art TV block face with a dark glowing screen, physical knobs, and small antennas on top. Maintain the Minecraft-style voxel aesthetic with clean pixels and vibrant colors.", "description": "A 32x32 pixel art texture for a TV block face, featuring a dark screen and antennas, based on the revised design.", "subtype": "other", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "retro_tv_compressed.glb": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1779409262119_01KS6GX5H79EKCY5YSEDDE0FPJ.retro_tv_compressed.glb", "type": "model_3d", "prompt": null, "description": "Compressed GLB 3D model of a retro TV for the TV block mesh.", "subtype": "glb", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "new_tv_block_image": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1779574134551_01KSBE4NRQH80TAC7N5RPYQF7C.image-1.png (19).png", "type": "image", "prompt": null, "description": "A pixel art TV block texture with a dark screen and antennas.", "subtype": null, "aspect_ratio": [50, 52], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Block Obby</title>\n    <!-- <script src=\"loader.js\"></script> -->\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\" />\n    <link\n      href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=VT323&display=swap\"\n      rel=\"stylesheet\"\n    />\n    <style>\n      :root {\n        --transition-fast: 150ms;\n        --transition-normal: 300ms;\n        --space-xs: 8px;\n        --space-sm: 16px;\n        --space-md: 24px;\n        --ease-out: cubic-bezier(0, 0, 0.2, 1);\n      }\n\n      * {\n        margin: 0;\n        padding: 0;\n        box-sizing: border-box;\n        -webkit-tap-highlight-color: transparent;\n      }\n\n      body,\n      html {\n        width: 100%;\n        height: 100%;\n        overflow: hidden;\n        font-family:\n          \"Inter\",\n          -apple-system,\n          BlinkMacSystemFont,\n          sans-serif;\n        touch-action: none;\n      }\n\n      #game-world {\n        position: relative;\n        width: 100%;\n        height: 100%;\n        background: #87ceeb;\n      }\n\n      #game-canvas {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n      }\n\n      /* Crosshair */\n      .crosshair {\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        pointer-events: none;\n        z-index: 10;\n      }\n\n      .crosshair::before,\n      .crosshair::after {\n        content: \"\";\n        position: absolute;\n        background: white;\n        box-shadow: 0 0 2px rgba(0, 0, 0, 0.8);\n      }\n\n      .crosshair::before {\n        width: 20px;\n        height: 2px;\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n      }\n\n      .crosshair::after {\n        width: 2px;\n        height: 20px;\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n      }\n\n      /* Mining Progress */\n      .mining-progress {\n        position: absolute;\n        top: 45%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        width: 100px;\n        height: 8px;\n        background: rgba(0, 0, 0, 0.5);\n        border-radius: 4px;\n        overflow: hidden;\n        opacity: 0;\n        transition: opacity var(--transition-fast) var(--ease-out);\n        pointer-events: none;\n        z-index: 10;\n      }\n\n      .mining-progress.active {\n        opacity: 1;\n      }\n\n      .mining-progress-bar {\n        height: 100%;\n        background: #4caf50;\n        width: 0%;\n        transition: width 50ms linear;\n      }\n\n      /* Hotbar */\n      .hotbar {\n        position: absolute;\n        bottom: var(--space-sm);\n        left: 50%;\n        transform: translateX(-50%);\n        display: flex;\n        gap: 6px;\n        z-index: 20;\n        padding: 10px 12px;\n        background: rgba(0, 0, 0, 0.6);\n        border-radius: 12px;\n        backdrop-filter: blur(4px);\n        border: 2px solid rgba(255, 255, 255, 0.1);\n      }\n\n      .hotbar-slot {\n        width: 60px;\n        height: 60px;\n        background-size: contain;\n        background-repeat: no-repeat;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n        position: relative;\n        transition:\n          transform var(--transition-fast) var(--ease-out),\n          box-shadow var(--transition-fast) var(--ease-out);\n      }\n\n      .hotbar-slot:active {\n        transform: scale(0.92);\n      }\n\n      .hotbar-slot-icon {\n        width: 40px;\n        height: 40px;\n        image-rendering: pixelated;\n      }\n\n      .hotbar-slot-count {\n        position: absolute;\n        bottom: 4px;\n        right: 4px;\n        font-family: \"VT323\", monospace;\n        font-size: 18px;\n        font-weight: bold;\n        color: white;\n        text-shadow:\n          2px 2px 0 #000,\n          -1px -1px 0 #000;\n      }\n\n      /* Quick Chat */\n      .quick-chat {\n        position: absolute;\n        bottom: 115px;\n        left: 50%;\n        transform: translateX(-50%);\n        display: flex;\n        gap: 8px;\n        z-index: 20;\n        padding: 8px 12px;\n        background: rgba(0, 0, 0, 0.6);\n        border-radius: 12px;\n        backdrop-filter: blur(4px);\n      }\n\n      .quick-chat-btn {\n        width: 56px;\n        height: 56px;\n        font-size: 28px;\n        background: rgba(255, 255, 255, 0.15);\n        border: 2px solid rgba(255, 255, 255, 0.2);\n        border-radius: 8px;\n        cursor: pointer;\n        transition:\n          transform 0.1s,\n          background 0.1s;\n      }\n\n      .chat-toggle-btn {\n        min-width: 96px;\n        height: 56px;\n        padding: 0 14px;\n        font-size: 24px;\n        font-family: \"VT323\", monospace;\n        color: white;\n        position: relative;\n      }\n\n      .chat-notify-dot {\n        position: absolute;\n        top: 6px;\n        right: 6px;\n        width: 10px;\n        height: 10px;\n        border-radius: 50%;\n        background: #ff2e2e;\n        box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.45);\n        display: none;\n        pointer-events: none;\n      }\n\n      .chat-notify-dot.visible {\n        display: block;\n      }\n\n      .quick-chat-btn:active {\n        transform: scale(0.9);\n        background: rgba(255, 255, 255, 0.3);\n      }\n\n      /* Virtual Joysticks */\n      .joystick {\n        position: absolute;\n        pointer-events: none;\n        z-index: 15;\n        opacity: 0.3;\n        transition: opacity var(--transition-fast) var(--ease-out);\n      }\n\n      .joystick.active {\n        opacity: 1;\n      }\n\n      .joystick-base {\n        width: 160px;\n        height: 160px;\n        background: rgba(255, 255, 255, 0.2);\n        border: 3px solid rgba(255, 255, 255, 0.4);\n        border-radius: 50%;\n        position: absolute;\n        transform: translate(-50%, -50%);\n      }\n\n      .joystick-knob {\n        width: 72px;\n        height: 72px;\n        background: rgba(255, 255, 255, 0.7);\n        border-radius: 50%;\n        position: absolute;\n        transform: translate(-50%, -50%);\n        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n      }\n\n      /* Persistent Move Widget */\n      .move-widget {\n        position: absolute;\n        bottom: 50px;\n        left: 30px;\n        width: 220px;\n        height: 220px;\n        z-index: 25;\n        touch-action: none;\n      }\n\n      .move-widget-base {\n        width: 100%;\n        height: 100%;\n        background: rgba(0, 0, 0, 0.4);\n        border: 4px solid rgba(255, 255, 255, 0.4);\n        border-radius: 50%;\n        position: absolute;\n        top: 0;\n        left: 0;\n        box-shadow:\n          0 4px 12px rgba(0, 0, 0, 0.3),\n          inset 0 0 20px rgba(255, 255, 255, 0.1);\n      }\n\n      .move-widget-knob {\n        width: 80px;\n        height: 80px;\n        background: linear-gradient(\n          145deg,\n          rgba(255, 255, 255, 0.9),\n          rgba(200, 200, 200, 0.8)\n        );\n        border-radius: 50%;\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        box-shadow:\n          0 4px 12px rgba(0, 0, 0, 0.4),\n          inset 0 2px 4px rgba(255, 255, 255, 0.5);\n        transition: transform 0.05s ease-out;\n      }\n\n      .move-widget-arrows {\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        width: 100%;\n        height: 100%;\n        pointer-events: none;\n        opacity: 0.4;\n      }\n\n      .move-widget-arrows::before,\n      .move-widget-arrows::after {\n        content: \"\";\n        position: absolute;\n        width: 0;\n        height: 0;\n      }\n\n      .move-widget-arrow {\n        position: absolute;\n        font-size: 20px;\n        color: rgba(255, 255, 255, 0.5);\n        pointer-events: none;\n      }\n\n      .move-widget-arrow.up {\n        top: 8px;\n        left: 50%;\n        transform: translateX(-50%);\n      }\n\n      .move-widget-arrow.down {\n        bottom: 8px;\n        left: 50%;\n        transform: translateX(-50%);\n      }\n\n      .move-widget-arrow.left {\n        left: 8px;\n        top: 50%;\n        transform: translateY(-50%);\n      }\n\n      .move-widget-arrow.right {\n        right: 8px;\n        top: 50%;\n        transform: translateY(-50%);\n      }\n\n      /* Touch Areas */\n      .touch-area {\n        position: absolute;\n        top: 0;\n        bottom: 100px;\n        z-index: 5;\n      }\n\n      .touch-area-left {\n        left: 0;\n        width: 280px;\n        pointer-events: none; /* Movement handled by move widget only */\n      }\n\n      .touch-area-right {\n        right: 0;\n        left: 280px;\n        width: auto;\n      }\n\n      /* Instructions */\n      .instructions {\n        position: absolute;\n        top: calc(var(--space-sm) + 200px);\n        left: 50%;\n        transform: translateX(-50%);\n        background: rgba(0, 0, 0, 0.75);\n        color: white;\n        padding: 12px 20px;\n        border-radius: 12px;\n        font-family: \"VT323\", monospace;\n        font-size: 22px;\n        letter-spacing: 0.5px;\n        z-index: 20;\n        text-align: center;\n        pointer-events: none;\n        transition: opacity var(--transition-normal) var(--ease-out);\n        border: 2px solid rgba(255, 255, 255, 0.15);\n        max-width: 90%;\n        line-height: 1.4;\n      }\n\n      .instructions.hidden {\n        opacity: 0;\n      }\n\n      /* Time indicator */\n      .time-indicator {\n        display: none !important;\n      }\n\n      /* Hide non-essential obby UI */\n      .health-bar,\n      .hunger-bar,\n      .hotbar,\n      #bag-button,\n      .coords-display,\n      .player-count,\n      .worlds-button,\n      .current-world-display,\n      .quick-chat,\n      .version-display,\n      .chat-window {\n        display: none !important;\n      }\n\n      /* Health bar (hearts) */\n      .health-bar {\n        position: absolute;\n        top: var(--space-sm);\n        left: var(--space-sm);\n        display: flex;\n        gap: 2px;\n        z-index: 20;\n        padding: 6px 10px;\n        background: rgba(0, 0, 0, 0.65);\n        border-radius: 10px;\n        border: 2px solid rgba(255, 255, 255, 0.1);\n      }\n\n      .heart {\n        width: 20px;\n        height: 20px;\n        font-size: 25px;\n        line-height: 20px;\n        text-align: center;\n        transition: transform 0.1s ease;\n      }\n\n      .heart.full {\n        color: #e53935;\n        text-shadow: 0 0 6px rgba(229, 57, 53, 0.7);\n      }\n\n      .heart.half {\n        position: relative;\n      }\n\n      .heart.half::before {\n        content: '\u2665';\n        position: absolute;\n        left: 0;\n        width: 50%;\n        overflow: hidden;\n        color: #e53935;\n        text-shadow: 0 0 6px rgba(229, 57, 53, 0.7);\n      }\n\n      .heart.half::after {\n        content: '\u2665';\n        color: #444;\n      }\n\n      .heart.empty {\n        color: #444;\n      }\n\n      .heart.damage {\n        animation: heartShake 0.2s ease;\n      }\n\n      /* Hunger bar */\n      .hunger-bar {\n        position: absolute;\n        top: calc(var(--space-sm) + 45px);\n        left: var(--space-sm);\n        display: flex;\n        gap: 2px;\n        z-index: 20;\n        padding: 6px 10px;\n        background: rgba(0, 0, 0, 0.65);\n        border-radius: 10px;\n        border: 2px solid rgba(255, 255, 255, 0.1);\n      }\n\n      .drumstick {\n        width: 20px;\n        height: 20px;\n        font-size: 18px;\n        line-height: 20px;\n        text-align: center;\n        transition: transform 0.1s ease;\n      }\n\n      .drumstick.full {\n        filter: none;\n        opacity: 1;\n      }\n\n      .drumstick.half {\n        position: relative;\n      }\n\n      .drumstick.half::before {\n        content: '\ud83c\udf56';\n        position: absolute;\n        left: 0;\n        width: 50%;\n        overflow: hidden;\n      }\n\n      .drumstick.half::after {\n        content: '\ud83c\udf56';\n        filter: grayscale(100%);\n        opacity: 0.4;\n      }\n\n      .drumstick.empty {\n        filter: grayscale(100%);\n        opacity: 0.4;\n      }\n\n      .drumstick.hunger {\n        animation: hungerShake 0.3s ease;\n      }\n\n      @keyframes hungerShake {\n        0%, 100% { transform: translateY(0); }\n        25% { transform: translateY(-2px); }\n        75% { transform: translateY(2px); }\n      }\n\n      @keyframes heartShake {\n        0%, 100% { transform: translateX(0); }\n        25% { transform: translateX(-2px); }\n        75% { transform: translateX(2px); }\n      }\n\n      /* Coordinates display */\n      .coords-display {\n        position: absolute;\n        top: calc(var(--space-sm) + 130px);\n        left: var(--space-sm);\n        background: rgba(0, 0, 0, 0.65);\n        color: white;\n        padding: 10px 16px;\n        border-radius: 10px;\n        font-family: \"VT323\", monospace;\n        font-size: 20px;\n        z-index: 20;\n        border: 2px solid rgba(255, 255, 255, 0.1);\n        min-width: 180px;\n        text-align: left;\n      }\n\n      /* Current World display */\n      .current-world-display {\n        position: absolute;\n        top: var(--space-sm);\n        right: var(--space-sm);\n        background: rgba(0, 0, 0, 0.65);\n        color: white;\n        padding: 10px 16px;\n        border-radius: 10px;\n        font-family: \"VT323\", monospace;\n        font-size: 22px;\n        z-index: 20;\n        border: 2px solid rgba(76, 175, 80, 0.3);\n        min-width: 200px;\n        text-align: left;\n      }\n\n\n      /* Edit mode overlay */\n      .edit-overlay {\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        background: rgba(20, 20, 30, 0.92);\n        color: white;\n        padding: 32px 40px;\n        border-radius: 16px;\n        text-align: center;\n        z-index: 100;\n        border: 3px solid rgba(255, 255, 255, 0.15);\n        backdrop-filter: blur(8px);\n        max-width: 85%;\n      }\n\n      .edit-overlay h2 {\n        font-family: \"VT323\", monospace;\n        font-size: 40px;\n        margin-bottom: var(--space-sm);\n        letter-spacing: 1px;\n      }\n\n      .edit-overlay p {\n        font-family: \"Inter\", sans-serif;\n        font-size: 20px;\n        opacity: 0.85;\n        line-height: 1.5;\n        margin-top: 8px;\n      }\n\n      /* Mining progress enhanced */\n      .mining-progress {\n        border: 2px solid rgba(255, 255, 255, 0.3);\n      }\n\n      .mining-progress-bar {\n        background: linear-gradient(90deg, #4caf50, #8bc34a);\n      }\n\n      /* Crosshair enhanced */\n      .crosshair::before,\n      .crosshair::after {\n        border-radius: 1px;\n      }\n\n      /* Full Inventory Modal */\n      .inventory-modal {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        background: rgba(0, 0, 0, 0.85);\n        display: none;\n        align-items: center;\n        justify-content: center;\n        z-index: 180;\n        backdrop-filter: blur(4px);\n      }\n\n      .inventory-modal.active {\n        display: flex;\n      }\n\n      .inventory-container {\n        background: linear-gradient(135deg, rgba(30, 30, 45, 0.98), rgba(20, 20, 35, 0.98));\n        padding: 24px;\n        border-radius: 16px;\n        border: 3px solid rgba(139, 90, 43, 0.6);\n        box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n        max-width: 95%;\n        max-height: 90%;\n        overflow-y: auto;\n      }\n\n      .inventory-title {\n        font-family: \"VT323\", monospace;\n        font-size: 32px;\n        color: white;\n        text-align: center;\n        margin-bottom: 16px;\n        letter-spacing: 2px;\n      }\n\n      .inventory-section {\n        margin-bottom: 16px;\n      }\n\n      .inventory-section-title {\n        font-family: \"VT323\", monospace;\n        font-size: 20px;\n        color: rgba(255, 255, 255, 0.7);\n        margin-bottom: 8px;\n        text-transform: uppercase;\n        letter-spacing: 1px;\n      }\n\n      .inventory-grid {\n        display: grid;\n        grid-template-columns: repeat(9, 1fr);\n        gap: 4px;\n        background: rgba(0, 0, 0, 0.3);\n        padding: 8px;\n        border-radius: 8px;\n        border: 2px solid rgba(255, 255, 255, 0.1);\n      }\n\n      .inventory-slot {\n        width: 52px;\n        height: 52px;\n        background: rgba(60, 60, 60, 0.8);\n        border: 2px solid rgba(100, 100, 100, 0.5);\n        border-radius: 6px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n        position: relative;\n        transition: all 0.15s ease;\n        touch-action: manipulation;\n        -webkit-tap-highlight-color: transparent;\n      }\n\n      .inventory-slot:hover {\n        background: rgba(100, 100, 100, 0.8);\n        border-color: rgba(255, 255, 255, 0.4);\n        transform: scale(1.05);\n      }\n\n      .inventory-slot:active {\n        transform: scale(0.95);\n        background: rgba(120, 120, 120, 0.9);\n      }\n\n      .inventory-slot.selected {\n        background: rgba(255, 215, 0, 0.3);\n        border-color: gold;\n        box-shadow: 0 0 10px rgba(255, 215, 0, 0.5);\n      }\n\n      .inventory-slot.hotbar-highlight {\n        border-color: rgba(76, 175, 80, 0.6);\n      }\n\n      .inventory-slot-icon {\n        width: 36px;\n        height: 36px;\n        image-rendering: pixelated;\n      }\n\n      .inventory-slot-count {\n        position: absolute;\n        bottom: 2px;\n        right: 4px;\n        font-family: \"VT323\", monospace;\n        font-size: 16px;\n        font-weight: bold;\n        color: white;\n        text-shadow: 2px 2px 0 #000, -1px -1px 0 #000;\n      }\n\n      .inventory-close-btn {\n        position: absolute;\n        top: 12px;\n        right: 12px;\n        background: rgba(244, 67, 54, 0.3);\n        color: #f44336;\n        border: 2px solid #f44336;\n        border-radius: 8px;\n        padding: 8px 16px;\n        font-family: \"VT323\", monospace;\n        font-size: 24px;\n        cursor: pointer;\n        transition: all 0.2s;\n      }\n\n      .inventory-close-btn:hover {\n        background: rgba(244, 67, 54, 0.5);\n      }\n\n      .inventory-held-item {\n        position: fixed;\n        top: 30px;\n        right: 80px;\n        pointer-events: none;\n        z-index: 300;\n        width: 60px;\n        height: 60px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        background: rgba(255, 215, 0, 0.2);\n        border: 3px solid gold;\n        border-radius: 8px;\n        box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);\n      }\n\n      .inventory-held-item::before {\n        content: 'Moving';\n        position: absolute;\n        top: -22px;\n        font-family: 'VT323', monospace;\n        color: gold;\n        font-size: 20px;\n        text-shadow: 2px 2px 0 #000;\n      }\n\n      .inventory-held-item img {\n        width: 48px;\n        height: 48px;\n        image-rendering: pixelated;\n      }\n\n      .inventory-held-item .count {\n        position: absolute;\n        bottom: 0;\n        right: 4px;\n        font-family: \"VT323\", monospace;\n        font-size: 20px;\n        font-weight: bold;\n        color: white;\n        text-shadow: 2px 2px 0 #000;\n      }\n\n      .inventory-hint {\n        font-family: \"Inter\", sans-serif;\n        font-size: 14px;\n        color: rgba(255, 255, 255, 0.5);\n        text-align: center;\n        margin-top: 12px;\n      }\n\n      /* Crafting Modal */\n      .crafting-modal {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        background: rgba(0, 0, 0, 0.85);\n        display: none;\n        align-items: center;\n        justify-content: center;\n        z-index: 180;\n        backdrop-filter: blur(4px);\n      }\n\n      .crafting-modal.active {\n        display: flex;\n      }\n\n      .crafting-container {\n        background: linear-gradient(135deg, rgba(30, 30, 45, 0.98), rgba(20, 20, 35, 0.98));\n        padding: 24px;\n        border-radius: 16px;\n        border: 3px solid rgba(139, 90, 43, 0.6);\n        box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n        max-width: 95%;\n        max-height: 90%;\n        overflow-y: auto;\n      }\n\n      .crafting-title {\n        font-family: \"VT323\", monospace;\n        font-size: 32px;\n        color: white;\n        text-align: center;\n        margin-bottom: 16px;\n        letter-spacing: 2px;\n      }\n\n      .crafting-area {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        gap: 20px;\n        margin-bottom: 20px;\n        flex-wrap: wrap;\n      }\n\n      .crafting-grid {\n        display: grid;\n        grid-template-columns: repeat(3, 1fr);\n        gap: 4px;\n        background: rgba(0, 0, 0, 0.3);\n        padding: 8px;\n        border-radius: 8px;\n        border: 2px solid rgba(255, 255, 255, 0.1);\n      }\n\n      .crafting-slot {\n        width: 56px;\n        height: 56px;\n        background: rgba(60, 60, 60, 0.8);\n        border: 2px solid rgba(100, 100, 100, 0.5);\n        border-radius: 6px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n        position: relative;\n        transition: all 0.15s ease;\n        touch-action: manipulation;\n        -webkit-tap-highlight-color: transparent;\n      }\n\n      .crafting-slot:hover {\n        background: rgba(100, 100, 100, 0.8);\n        border-color: rgba(255, 255, 255, 0.4);\n        transform: scale(1.05);\n      }\n\n      .crafting-slot:active {\n        transform: scale(0.95);\n        background: rgba(120, 120, 120, 0.9);\n      }\n\n      .crafting-arrow {\n        font-size: 36px;\n        color: rgba(255, 255, 255, 0.6);\n      }\n\n      .crafting-result {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        gap: 8px;\n      }\n\n      .crafting-result-slot {\n        width: 72px;\n        height: 72px;\n        background: rgba(76, 175, 80, 0.2);\n        border: 3px solid rgba(76, 175, 80, 0.6);\n        border-radius: 8px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n        position: relative;\n        transition: all 0.15s ease;\n      }\n\n      .crafting-result-slot:hover {\n        background: rgba(76, 175, 80, 0.4);\n        transform: scale(1.05);\n      }\n\n      .crafting-result-slot.has-item {\n        box-shadow: 0 0 15px rgba(76, 175, 80, 0.5);\n      }\n\n      .crafting-result-name {\n        font-family: \"VT323\", monospace;\n        font-size: 18px;\n        color: rgba(255, 255, 255, 0.7);\n        text-align: center;\n      }\n\n      .crafting-recipes {\n        background: rgba(0, 0, 0, 0.2);\n        padding: 12px;\n        border-radius: 8px;\n        margin-top: 16px;\n      }\n\n      .crafting-recipes-title {\n        font-family: \"VT323\", monospace;\n        font-size: 20px;\n        color: rgba(255, 255, 255, 0.7);\n        margin-bottom: 8px;\n        text-transform: uppercase;\n        letter-spacing: 1px;\n      }\n\n      .crafting-recipe-list {\n        display: flex;\n        flex-wrap: wrap;\n        gap: 8px;\n        max-height: 120px;\n        overflow-y: auto;\n      }\n\n      .crafting-recipe-hint {\n        background: rgba(255, 255, 255, 0.1);\n        padding: 6px 10px;\n        border-radius: 6px;\n        font-family: \"Inter\", sans-serif;\n        font-size: 12px;\n        color: rgba(255, 255, 255, 0.8);\n        cursor: pointer;\n        transition: all 0.2s;\n      }\n\n      .crafting-recipe-hint:hover {\n        background: rgba(255, 255, 255, 0.2);\n      }\n\n      .crafting-close-btn {\n        position: absolute;\n        top: 12px;\n        right: 12px;\n        background: rgba(244, 67, 54, 0.3);\n        color: #f44336;\n        border: 2px solid #f44336;\n        border-radius: 8px;\n        padding: 8px 16px;\n        font-family: \"VT323\", monospace;\n        font-size: 24px;\n        cursor: pointer;\n        transition: all 0.2s;\n      }\n\n      .crafting-close-btn:hover {\n        background: rgba(244, 67, 54, 0.5);\n      }\n\n      @media (max-width: 520px) {\n        .crafting-slot {\n          width: 52px;\n          height: 52px;\n        }\n\n        .crafting-result-slot {\n          width: 60px;\n          height: 60px;\n        }\n\n        .crafting-container {\n          padding: 14px;\n        }\n\n        .crafting-title {\n          font-size: 26px;\n        }\n\n        .crafting-area {\n          gap: 10px;\n        }\n      }\n\n      @media (max-width: 520px) {\n        .inventory-grid {\n          grid-template-columns: repeat(9, 1fr);\n          gap: 3px;\n          padding: 6px;\n        }\n\n        .inventory-slot {\n          width: 44px;\n          height: 44px;\n          border-radius: 8px;\n        }\n\n        .inventory-slot-icon {\n          width: 32px;\n          height: 32px;\n        }\n\n        .inventory-slot-count {\n          font-size: 14px;\n        }\n\n        .inventory-container {\n          padding: 14px;\n        }\n      }\n\n      /* Extra small screens */\n      @media (max-width: 400px) {\n        .inventory-grid {\n          grid-template-columns: repeat(9, 1fr);\n          gap: 2px;\n          padding: 4px;\n        }\n\n        .inventory-slot {\n          width: 36px;\n          height: 36px;\n        }\n\n        .inventory-slot-icon {\n          width: 26px;\n          height: 26px;\n        }\n      }\n\n      /* Loading Screen */\n      .loading-screen {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        background: linear-gradient(135deg, #8b6f47 0%, #6b5839 100%);\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        justify-content: center;\n        z-index: 1000;\n        transition: opacity 0.5s var(--ease-out);\n      }\n\n      .loading-screen.fade-out {\n        opacity: 0;\n        pointer-events: none;\n      }\n\n      .loading-title {\n        font-family: \"VT323\", monospace;\n        font-size: 56px;\n        color: white;\n        margin-bottom: 40px;\n        text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);\n        letter-spacing: 2px;\n      }\n\n      .loading-bar-container {\n        width: 80%;\n        max-width: 400px;\n        height: 24px;\n        background: rgba(139, 111, 71, 0.7);\n        border-radius: 12px;\n        overflow: hidden;\n        border: 3px solid rgba(255, 255, 255, 0.2);\n        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);\n      }\n\n      .loading-bar-fill {\n        height: 100%;\n        background: white;\n        width: 0%;\n        transition: width 0.3s ease-out;\n        border-radius: 9px;\n        box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);\n      }\n\n      .loading-text {\n        font-family: \"Inter\", sans-serif;\n        font-size: 20px;\n        color: rgba(255, 255, 255, 0.8);\n        margin-top: 20px;\n        letter-spacing: 0.5px;\n      }\n\n      .loading-percentage {\n        font-family: \"VT323\", monospace;\n        font-size: 32px;\n        color: white;\n        margin-top: 16px;\n        text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);\n      }\n\n      .loading-pickaxe {\n        width: 80px;\n        height: 80px;\n        margin-bottom: 20px;\n        image-rendering: pixelated;\n        animation: digging 1.2s ease-in-out infinite;\n        transform-origin: bottom left;\n      }\n\n      @keyframes fadeOut {\n        from {\n          opacity: 1;\n        }\n\n        to {\n          opacity: 0;\n        }\n      }\n\n      @keyframes digging {\n        0% {\n          transform: rotate(-20deg);\n        }\n\n        25% {\n          transform: rotate(15deg);\n        }\n\n        50% {\n          transform: rotate(-20deg);\n        }\n\n        75% {\n          transform: rotate(15deg);\n        }\n\n        100% {\n          transform: rotate(-20deg);\n        }\n      }\n\n      /* Player Count */\n      .player-count {\n        position: absolute;\n        top: calc(var(--space-sm) + 80px);\n        right: calc(var(--space-sm) + 120px);\n        background: rgba(0, 0, 0, 0.65);\n        color: white;\n        padding: 10px 16px;\n        border-radius: 10px;\n        font-family: \"VT323\", monospace;\n        font-size: 26px;\n        z-index: 20;\n        border: 2px solid rgba(255, 255, 255, 0.1);\n        min-width: 150px;\n        text-align: center;\n      }\n\n      /* Worlds Button */\n      .worlds-button {\n        position: absolute;\n        top: calc(var(--space-sm) + 60px);\n        right: var(--space-sm);\n        background: rgba(76, 175, 80, 0.8);\n        color: white;\n        padding: 10px 20px;\n        border-radius: 10px;\n        font-family: \"VT323\", monospace;\n        font-size: 24px;\n        z-index: 20;\n        border: 2px solid rgba(255, 255, 255, 0.2);\n        cursor: pointer;\n        transition: all 0.2s;\n        display: flex;\n        align-items: center;\n        gap: 8px;\n      }\n\n      .worlds-button:hover {\n        background: rgba(76, 175, 80, 1);\n        transform: scale(1.05);\n      }\n\n      .worlds-button:active {\n        transform: scale(0.95);\n      }\n\n      /* Worlds Modal */\n      .worlds-modal {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        background: rgba(0, 0, 0, 0.9);\n        display: none;\n        align-items: center;\n        justify-content: center;\n        z-index: 200;\n        backdrop-filter: blur(8px);\n      }\n\n      .worlds-modal.active {\n        display: flex;\n      }\n\n      .worlds-modal-content {\n        background: linear-gradient(\n          135deg,\n          rgba(30, 30, 45, 0.98),\n          rgba(20, 20, 35, 0.98)\n        );\n        padding: 30px;\n        border-radius: 16px;\n        border: 3px solid rgba(76, 175, 80, 0.5);\n        max-width: 90%;\n        width: 500px;\n        max-height: 80%;\n        overflow-y: auto;\n        box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n      }\n\n      .worlds-modal-header {\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n        margin-bottom: 20px;\n        padding-bottom: 15px;\n        border-bottom: 2px solid rgba(255, 255, 255, 0.1);\n      }\n\n      .worlds-modal-title {\n        font-family: \"VT323\", monospace;\n        font-size: 36px;\n        color: #4caf50;\n        margin: 0;\n      }\n\n      .worlds-modal-close {\n        background: rgba(244, 67, 54, 0.3);\n        color: #f44336;\n        border: 2px solid #f44336;\n        border-radius: 8px;\n        padding: 8px 16px;\n        font-family: \"VT323\", monospace;\n        font-size: 24px;\n        cursor: pointer;\n        transition: all 0.2s;\n      }\n\n      .worlds-modal-close:hover {\n        background: rgba(244, 67, 54, 0.5);\n      }\n\n      .worlds-list {\n        display: flex;\n        flex-direction: column;\n        gap: 12px;\n        margin-bottom: 20px;\n        max-height: 300px;\n        overflow-y: auto;\n        padding-right: 8px;\n      }\n\n      .worlds-list::-webkit-scrollbar {\n        width: 8px;\n      }\n\n      .worlds-list::-webkit-scrollbar-track {\n        background: rgba(255, 255, 255, 0.05);\n        border-radius: 4px;\n      }\n\n      .worlds-list::-webkit-scrollbar-thumb {\n        background: rgba(76, 175, 80, 0.4);\n        border-radius: 4px;\n      }\n\n      .worlds-list::-webkit-scrollbar-thumb:hover {\n        background: rgba(76, 175, 80, 0.6);\n      }\n\n      .world-item {\n        background: rgba(255, 255, 255, 0.05);\n        border: 2px solid rgba(255, 255, 255, 0.1);\n        border-radius: 12px;\n        padding: 15px;\n        cursor: pointer;\n        transition: all 0.2s;\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n      }\n\n      .world-item:hover {\n        background: rgba(76, 175, 80, 0.2);\n        border-color: rgba(76, 175, 80, 0.5);\n        transform: translateX(5px);\n      }\n\n      .world-info {\n        flex: 1;\n      }\n\n      .world-name {\n        font-family: \"VT323\", monospace;\n        font-size: 26px;\n        color: white;\n        margin-bottom: 4px;\n      }\n\n      .world-host {\n        font-family: \"Inter\", sans-serif;\n        font-size: 14px;\n        color: rgba(255, 255, 255, 0.6);\n      }\n\n      .world-stats {\n        text-align: right;\n      }\n\n      .world-players {\n        font-family: \"VT323\", monospace;\n        font-size: 22px;\n        color: #4caf50;\n      }\n\n      .world-ping {\n        font-family: \"Inter\", sans-serif;\n        font-size: 12px;\n        color: rgba(255, 255, 255, 0.5);\n      }\n\n      .host-world-btn {\n        width: 100%;\n        background: linear-gradient(\n          135deg,\n          rgba(33, 150, 243, 0.4),\n          rgba(33, 150, 243, 0.2)\n        );\n        color: #2196f3;\n        border: 2px solid #2196f3;\n        border-radius: 12px;\n        padding: 15px;\n        font-family: \"VT323\", monospace;\n        font-size: 28px;\n        cursor: pointer;\n        transition: all 0.2s;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        gap: 10px;\n      }\n\n      .host-world-btn:hover {\n        background: linear-gradient(\n          135deg,\n          rgba(33, 150, 243, 0.6),\n          rgba(33, 150, 243, 0.4)\n        );\n        transform: scale(1.02);\n      }\n\n      .worlds-section-title {\n        font-family: \"VT323\", monospace;\n        font-size: 22px;\n        color: rgba(255, 255, 255, 0.7);\n        margin-bottom: 12px;\n        margin-top: 20px;\n        text-transform: uppercase;\n        letter-spacing: 2px;\n      }\n\n      .worlds-section-title:first-of-type {\n        margin-top: 0;\n      }\n\n      /* Chat Window */\n      .chat-window {\n        position: absolute;\n        bottom: 114px;\n        right: var(--space-sm);\n        width: 420px;\n        height: 220px;\n        background: rgba(0, 0, 0, 0.75);\n        border-radius: 12px;\n        border: 2px solid rgba(255, 255, 255, 0.1);\n        z-index: 120;\n        display: flex;\n        flex-direction: column;\n        overflow: hidden;\n      }\n\n      .chat-window.hidden {\n        display: none !important;\n      }\n\n      .chat-header {\n        display: none;\n      }\n\n      .chat-room-name {\n        padding: 6px 10px;\n        font-family: \"VT323\", monospace;\n        font-size: 20px;\n        color: rgba(255, 255, 255, 0.85);\n        background: rgba(255, 255, 255, 0.08);\n        border-bottom: 1px solid rgba(255, 255, 255, 0.12);\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n\n      .chat-messages {\n        flex: 1;\n        padding: 8px;\n        overflow-y: auto;\n        font-family: \"Inter\", sans-serif;\n        font-size: 14px;\n        color: white;\n      }\n\n      .chat-input {\n        border-top: 1px solid rgba(255, 255, 255, 0.2);\n        padding: 8px;\n        display: flex;\n        gap: 8px;\n      }\n\n      .chat-input input {\n        flex: 1;\n        background: rgba(255, 255, 255, 0.1);\n        border: none;\n        padding: 6px;\n        color: white;\n        font-family: \"Inter\", sans-serif;\n        border-radius: 4px;\n        box-sizing: border-box;\n      }\n\n      .chat-send-btn {\n        background: rgba(76, 175, 80, 0.3);\n        color: #4CAF50;\n        border: 2px solid #4CAF50;\n        border-radius: 6px;\n        padding: 0 16px;\n        font-size: 20px;\n        cursor: pointer;\n        display: none;\n      }\n\n      .chat-message {\n        margin-bottom: 4px;\n        word-wrap: break-word;\n      }\n\n      .chat-message .username {\n        font-weight: 600;\n        margin-right: 4px;\n      }\n\n      .chat-message.own-message .username {\n        color: #8bc34a;\n      }\n\n      .chat-message .text {\n        color: rgba(255, 255, 255, 0.9);\n      }\n\n      /* Mobile Portrait Mode Adjustments */\n      @media (max-width: 768px) and (orientation: portrait) {\n        .player-count {\n          top: calc(var(--space-sm) + 82px);\n          right: auto;\n          left: var(--space-sm);\n          min-width: 120px;\n          font-size: 22px;\n        }\n\n        .current-world-display {\n          top: var(--space-sm);\n          font-size: 20px;\n          min-width: 180px;\n        }\n\n        .chat-window {\n          position: fixed;\n          top: 10%;\n          left: 5%;\n          right: 5%;\n          bottom: auto;\n          width: 90%;\n          height: 45vh;\n          max-width: none;\n          box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n          border: 3px solid rgba(255, 255, 255, 0.2);\n          background: rgba(20, 20, 30, 0.95);\n          backdrop-filter: blur(10px);\n        }\n\n        .chat-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          padding: 12px 16px;\n          border-bottom: 2px solid rgba(255, 255, 255, 0.1);\n          font-family: \"VT323\", monospace;\n          font-size: 24px;\n          color: white;\n        }\n\n        .chat-room-name {\n          padding: 10px 14px;\n          font-size: 24px;\n          border-bottom: 2px solid rgba(255, 255, 255, 0.1);\n        }\n\n        .chat-close-btn {\n          background: rgba(244, 67, 54, 0.3);\n          color: #f44336;\n          border: 2px solid #f44336;\n          border-radius: 8px;\n          width: 36px;\n          height: 36px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          font-size: 20px;\n          cursor: pointer;\n        }\n\n        .chat-toggle-btn {\n          min-width: 110px;\n          height: 60px;\n          font-size: 26px;\n        }\n\n        .chat-messages {\n          font-size: 16px;\n          padding: 12px;\n        }\n\n        .chat-message {\n          margin-bottom: 8px;\n          line-height: 1.4;\n        }\n\n        .chat-input {\n          padding: 12px;\n          border-top: 2px solid rgba(255, 255, 255, 0.1);\n        }\n\n        .chat-input input {\n          padding: 12px;\n          font-size: 16px;\n          border-radius: 8px;\n        }\n\n        .chat-send-btn {\n          display: block;\n        }\n      }\n    </style>\n  </head>\n  <body>\n    <!-- Loading Screen (visible immediately on page load) -->\n    <div class=\"loading-screen\" id=\"loading-screen\">\n      <div class=\"loading-title\" id=\"loading-title\">MINECRAFT LITE</div>\n      <img\n        class=\"loading-pickaxe\"\n        id=\"loading-pickaxe\"\n        src=\"\"\n        alt=\"Pickaxe\"\n        style=\"display: none\"\n      />\n      <div class=\"loading-bar-container\">\n        <div class=\"loading-bar-fill\" id=\"loading-bar-fill\"></div>\n      </div>\n      <div class=\"loading-percentage\" id=\"loading-percentage\">0%</div>\n      <div class=\"loading-text\" id=\"loading-text\">Loading...</div>\n    </div>\n    <div id=\"game-world\">\n      <canvas id=\"game-canvas\"></canvas>\n    </div>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/DRACOLoader.js\"></script>\n    <script src=\"https://unpkg.com/peerjs@1.4.7/dist/peerjs.min.js\"></script>\n    <script src=\"https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js\"></script>\n    <script src=\"https://www.gstatic.com/firebasejs/9.22.0/firebase-database-compat.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/gh/planktonfun/3d-objects@main/browser-compat.js\"></script>\n    <!-- <script src=\"./src/browser-compat.js\"></script> -->\n    <script>\n      // Multilanguage support\n      const i18nResources = {\n        en: {\n          translation: {\n            gameTitle: 'BLOCK OBBY',\n            loading: 'Loading...',\n            starting: 'Starting...',\n            initializing: 'Initializing...',\n            loadingAssets: 'Loading assets...',\n            loadingPlayerModel: 'Loading player model...',\n            playerModelLoaded: 'Player model loaded!',\n            loadingAlexModel: 'Loading Alex model...',\n            alexModelLoaded: 'Alex model loaded!',\n            loadingZombieModel: 'Loading zombie model...',\n            zombieModelLoaded: 'Zombie model loaded!',\n            loadingVillagerModel: 'Loading villager model...',\n            villagerModelLoaded: 'Villager model loaded!',\n            initializingWorld: 'Initializing infinite world...',\n            worldReady: 'World ready for exploration...',\n            buildingWorld: 'Building world with batched rendering...',\n            infiniteWorldReady: 'Infinite world ready...',\n            settingUpControls: 'Setting up controls...',\n            findingSpawn: 'Finding spawn point...',\n            ready: 'Ready!',\n            complete: 'Complete!',\n            creatingTextures: 'Creating textures...',\n            instructionsTouch:\n              '\u25c0 Move  |  Drag to Aim \u25b6  |  \u2b06\ufe0f Jump  |  \ud83c\udfc1 Reach Checkpoints!',\n            instructionsDesktop:\n              'Click to start \u2022 WASD Move \u2022 Mouse Look \u2022 Space Jump \u2022 Reach all checkpoints!',\n            worlds: 'Worlds',\n            browseWorlds: 'Browse Worlds',\n            yourWorld: 'Your World',\n            availableWorlds: 'Available Worlds',\n            hostWorld: 'Host Your Own World',\n            yourNickname: 'Your Nickname',\n            update: 'Update',\n            worldLabel: 'World',\n            none: 'none',\n            playersOnline: '{{count}} Player Online',\n            playersOnline_plural: '{{count}} Players Online',\n          },\n        },\n        bn: {\n          translation: {\n            gameTitle: '\u09ae\u09be\u0987\u09a8\u0995\u09cd\u09b0\u09be\u09ab\u099f \u09b2\u09be\u0987\u099f',\n            loading: '\u09b2\u09cb\u09a1 \u09b9\u099a\u09cd\u099b\u09c7...',\n            starting: '\u09b6\u09c1\u09b0\u09c1 \u09b9\u099a\u09cd\u099b\u09c7...',\n            initializing: '\u09aa\u09cd\u09b0\u09b8\u09cd\u09a4\u09c1\u09a4 \u09b9\u099a\u09cd\u099b\u09c7...',\n            loadingAssets: '\u09b8\u09ae\u09cd\u09aa\u09a6 \u09b2\u09cb\u09a1 \u09b9\u099a\u09cd\u099b\u09c7...',\n            loadingPlayerModel: '\u0996\u09c7\u09b2\u09cb\u09af\u09bc\u09be\u09a1\u09bc \u09ae\u09a1\u09c7\u09b2 \u09b2\u09cb\u09a1 \u09b9\u099a\u09cd\u099b\u09c7...',\n            playerModelLoaded: '\u0996\u09c7\u09b2\u09cb\u09af\u09bc\u09be\u09a1\u09bc \u09ae\u09a1\u09c7\u09b2 \u09b2\u09cb\u09a1 \u09b9\u09af\u09bc\u09c7\u099b\u09c7!',\n            loadingAlexModel: '\u0985\u09cd\u09af\u09be\u09b2\u09c7\u0995\u09cd\u09b8 \u09ae\u09a1\u09c7\u09b2 \u09b2\u09cb\u09a1 \u09b9\u099a\u09cd\u099b\u09c7...',\n            alexModelLoaded: '\u0985\u09cd\u09af\u09be\u09b2\u09c7\u0995\u09cd\u09b8 \u09ae\u09a1\u09c7\u09b2 \u09b2\u09cb\u09a1 \u09b9\u09af\u09bc\u09c7\u099b\u09c7!',\n            loadingZombieModel: '\u099c\u09ae\u09cd\u09ac\u09bf \u09ae\u09a1\u09c7\u09b2 \u09b2\u09cb\u09a1 \u09b9\u099a\u09cd\u099b\u09c7...',\n            zombieModelLoaded: '\u099c\u09ae\u09cd\u09ac\u09bf \u09ae\u09a1\u09c7\u09b2 \u09b2\u09cb\u09a1 \u09b9\u09af\u09bc\u09c7\u099b\u09c7!',\n            initializingWorld: '\u0985\u09b8\u09c0\u09ae \u09ac\u09bf\u09b6\u09cd\u09ac \u09aa\u09cd\u09b0\u09b8\u09cd\u09a4\u09c1\u09a4 \u09b9\u099a\u09cd\u099b\u09c7...',\n            worldReady: '\u09ac\u09bf\u09b6\u09cd\u09ac \u0985\u09a8\u09cd\u09ac\u09c7\u09b7\u09a3\u09c7\u09b0 \u099c\u09a8\u09cd\u09af \u09aa\u09cd\u09b0\u09b8\u09cd\u09a4\u09c1\u09a4...',\n            buildingWorld: '\u09ac\u09bf\u09b6\u09cd\u09ac \u09a4\u09c8\u09b0\u09bf \u09b9\u099a\u09cd\u099b\u09c7...',\n            infiniteWorldReady: '\u0985\u09b8\u09c0\u09ae \u09ac\u09bf\u09b6\u09cd\u09ac \u09aa\u09cd\u09b0\u09b8\u09cd\u09a4\u09c1\u09a4...',\n            settingUpControls: '\u09a8\u09bf\u09af\u09bc\u09a8\u09cd\u09a4\u09cd\u09b0\u09a3 \u09b8\u09c7\u099f \u0986\u09aa \u09b9\u099a\u09cd\u099b\u09c7...',\n            findingSpawn: '\u09b8\u09cd\u09aa\u09a8 \u09aa\u09af\u09bc\u09c7\u09a8\u09cd\u099f \u0996\u09c1\u0981\u099c\u099b\u09c7...',\n            ready: '\u09aa\u09cd\u09b0\u09b8\u09cd\u09a4\u09c1\u09a4!',\n            complete: '\u09b8\u09ae\u09cd\u09aa\u09c2\u09b0\u09cd\u09a3!',\n            creatingTextures: '\u099f\u09c7\u0995\u09cd\u09b8\u099a\u09be\u09b0 \u09a4\u09c8\u09b0\u09bf \u09b9\u099a\u09cd\u099b\u09c7...',\n            instructionsTouch: '\u25c0 \u09b8\u09b0\u09be\u09a8 | \u099f\u09c7\u09a8\u09c7 \u09a8\u09bf\u09b6\u09be\u09a8\u09be \u25b6 | \u26cf\ufe0f \u0996\u09a8\u09a8 | \ud83d\udce6 \u09b0\u09be\u0996\u09c1\u09a8 | \u2b06\ufe0f \u09b2\u09be\u09ab | \ud83d\udce6 \u0987\u09a8\u09ad\u09c7\u09a8\u09cd\u099f\u09b0\u09bf',\n            instructionsDesktop:\n              '\u09b6\u09c1\u09b0\u09c1 \u0995\u09b0\u09a4\u09c7 \u0995\u09cd\u09b2\u09bf\u0995 \u2022 WASD \u09b8\u09b0\u09be\u09a8 \u2022 \u09ae\u09be\u0989\u09b8 \u09a6\u09c7\u0996\u09c1\u09a8 \u2022 LMB \u0996\u09a8\u09a8 \u2022 RMB \u09b0\u09be\u0996\u09c1\u09a8 \u2022 E \u0987\u09a8\u09ad\u09c7\u09a8\u09cd\u099f\u09b0\u09bf \u2022 M \u09ae\u09c7\u09a8\u09c1',\n            worlds: '\u0993\u09af\u09bc\u09be\u09b0\u09cd\u09b2\u09cd\u09a1\u09b8',\n            browseWorlds: '\u0993\u09af\u09bc\u09be\u09b0\u09cd\u09b2\u09cd\u09a1\u09b8 \u09ac\u09cd\u09b0\u09be\u0989\u099c \u0995\u09b0\u09c1\u09a8',\n            yourWorld: '\u0986\u09aa\u09a8\u09be\u09b0 \u0993\u09af\u09bc\u09be\u09b0\u09cd\u09b2\u09cd\u09a1',\n            availableWorlds: '\u0989\u09aa\u09b2\u09ac\u09cd\u09a7 \u0993\u09af\u09bc\u09be\u09b0\u09cd\u09b2\u09cd\u09a1\u0997\u09c1\u09b2\u09cb',\n            hostWorld: '\u09a8\u09bf\u099c\u09c7\u09b0 \u0993\u09af\u09bc\u09be\u09b0\u09cd\u09b2\u09cd\u09a1 \u09b9\u09cb\u09b8\u09cd\u099f \u0995\u09b0\u09c1\u09a8',\n            yourNickname: '\u0986\u09aa\u09a8\u09be\u09b0 \u09a1\u09be\u0995\u09a8\u09be\u09ae',\n            update: '\u0986\u09aa\u09a1\u09c7\u099f',\n            worldLabel: '\u0993\u09af\u09bc\u09be\u09b0\u09cd\u09b2\u09cd\u09a1',\n            none: '\u0995\u09bf\u099b\u09c1 \u09a8\u09c7\u0987',\n            playersOnline: '{{count}} \u099c\u09a8 \u09aa\u09cd\u09b2\u09c7\u09af\u09bc\u09be\u09b0 \u0985\u09a8\u09b2\u09be\u0987\u09a8',\n            playersOnline_plural: '{{count}} \u099c\u09a8 \u09aa\u09cd\u09b2\u09c7\u09af\u09bc\u09be\u09b0 \u0985\u09a8\u09b2\u09be\u0987\u09a8',\n          },\n        },\n        vi: {\n          translation: {\n            gameTitle: 'BLOCK OBBY',\n            loading: '\u0110ang t\u1ea3i...',\n            starting: '\u0110ang b\u1eaft \u0111\u1ea7u...',\n            initializing: '\u0110ang kh\u1edfi t\u1ea1o...',\n            loadingAssets: '\u0110ang t\u1ea3i t\u00e0i nguy\u00ean...',\n            loadingPlayerModel: '\u0110ang t\u1ea3i m\u00f4 h\u00ecnh ng\u01b0\u1eddi ch\u01a1i...',\n            playerModelLoaded: '\u0110\u00e3 t\u1ea3i m\u00f4 h\u00ecnh ng\u01b0\u1eddi ch\u01a1i!',\n            loadingAlexModel: '\u0110ang t\u1ea3i m\u00f4 h\u00ecnh Alex...',\n            alexModelLoaded: '\u0110\u00e3 t\u1ea3i m\u00f4 h\u00ecnh Alex!',\n            loadingZombieModel: '\u0110ang t\u1ea3i m\u00f4 h\u00ecnh zombie...',\n            zombieModelLoaded: '\u0110\u00e3 t\u1ea3i m\u00f4 h\u00ecnh zombie!',\n            initializingWorld: '\u0110ang kh\u1edfi t\u1ea1o th\u1ebf gi\u1edbi v\u00f4 h\u1ea1n...',\n            worldReady: 'Th\u1ebf gi\u1edbi s\u1eb5n s\u00e0ng kh\u00e1m ph\u00e1...',\n            buildingWorld: '\u0110ang x\u00e2y d\u1ef1ng th\u1ebf gi\u1edbi...',\n            infiniteWorldReady: 'Th\u1ebf gi\u1edbi v\u00f4 h\u1ea1n s\u1eb5n s\u00e0ng...',\n            settingUpControls: '\u0110ang thi\u1ebft l\u1eadp \u0111i\u1ec1u khi\u1ec3n...',\n            findingSpawn: '\u0110ang t\u00ecm \u0111i\u1ec3m xu\u1ea5t hi\u1ec7n...',\n            ready: 'S\u1eb5n s\u00e0ng!',\n            complete: 'Ho\u00e0n th\u00e0nh!',\n            creatingTextures: '\u0110ang t\u1ea1o k\u1ebft c\u1ea5u...',\n            instructionsTouch:\n              '\u25c0 Di chuy\u1ec3n | K\u00e9o \u0111\u1ec3 ng\u1eafm \u25b6 | \u26cf\ufe0f \u0110\u00e0o | \ud83d\udce6 \u0110\u1eb7t | \u2b06\ufe0f Nh\u1ea3y | \ud83d\udce6 T\u00fai \u0111\u1ed3',\n            instructionsDesktop:\n              'Nh\u1ea5p \u0111\u1ec3 b\u1eaft \u0111\u1ea7u \u2022 WASD Di chuy\u1ec3n \u2022 Chu\u1ed9t Nh\u00ecn \u2022 LMB \u0110\u00e0o \u2022 RMB \u0110\u1eb7t \u2022 E T\u00fai \u0111\u1ed3 \u2022 M Menu',\n            worlds: 'Th\u1ebf gi\u1edbi',\n            browseWorlds: 'Duy\u1ec7t th\u1ebf gi\u1edbi',\n            yourWorld: 'Th\u1ebf gi\u1edbi c\u1ee7a b\u1ea1n',\n            availableWorlds: 'Th\u1ebf gi\u1edbi kh\u1ea3 d\u1ee5ng',\n            hostWorld: 'T\u1ea1o th\u1ebf gi\u1edbi c\u1ee7a b\u1ea1n',\n            yourNickname: 'Bi\u1ec7t danh c\u1ee7a b\u1ea1n',\n            update: 'C\u1eadp nh\u1eadt',\n            worldLabel: 'Th\u1ebf gi\u1edbi',\n            none: 'kh\u00f4ng c\u00f3',\n            playersOnline: '{{count}} ng\u01b0\u1eddi ch\u01a1i \u0111ang online',\n            playersOnline_plural: '{{count}} ng\u01b0\u1eddi ch\u01a1i \u0111ang online',\n          },\n        },\n        mr: {\n          translation: {\n            gameTitle: '\u092e\u093e\u0907\u0928\u0915\u094d\u0930\u093e\u092b\u094d\u091f \u0932\u093e\u0907\u091f',\n            loading: '\u0932\u094b\u0921 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            starting: '\u0938\u0941\u0930\u0942 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            initializing: '\u0938\u0941\u0930\u0941\u0935\u093e\u0924 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            loadingAssets: '\u092e\u093e\u0932\u092e\u0924\u094d\u0924\u093e \u0932\u094b\u0921 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            loadingPlayerModel: '\u0916\u0947\u0933\u093e\u0921\u0942 \u092e\u0949\u0921\u0947\u0932 \u0932\u094b\u0921 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            playerModelLoaded: '\u0916\u0947\u0933\u093e\u0921\u0942 \u092e\u0949\u0921\u0947\u0932 \u0932\u094b\u0921 \u091d\u093e\u0932\u0947!',\n            loadingAlexModel: '\u0905\u200d\u0945\u0932\u0947\u0915\u094d\u0938 \u092e\u0949\u0921\u09c7\u09b2 \u0932\u094b\u0921 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            alexModelLoaded: '\u0905\u200d\u0945\u0932\u0947\u0915\u094d\u0938 \u092e\u0949\u0921\u0947\u0932 \u0932\u094b\u0921 \u091d\u093e\u0932\u0947!',\n            loadingZombieModel: '\u091d\u094b\u092e\u094d\u092c\u0940 \u092e\u0949\u0921\u0947\u0932 \u0932\u094b\u0921 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            zombieModelLoaded: '\u091d\u094b\u092e\u094d\u092c\u0940 \u092e\u0949\u0921\u0947\u0932 \u0932\u094b\u0921 \u091d\u093e\u0932\u0947!',\n            initializingWorld: '\u0905\u0928\u0902\u0924 \u091c\u0917 \u0924\u092f\u093e\u0930 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            worldReady: '\u091c\u0917 \u0936\u094b\u0927\u093e\u0938\u093e\u0920\u0940 \u0924\u092f\u093e\u0930...',\n            buildingWorld: '\u091c\u0917 \u0924\u092f\u093e\u0930 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            infiniteWorldReady: '\u0905\u0928\u0902\u0924 \u091c\u0917 \u0924\u092f\u093e\u0930...',\n            settingUpControls: '\u0928\u093f\u092f\u0902\u0924\u094d\u0930\u0923\u0947 \u0938\u0947\u091f \u0939\u094b\u0924 \u0906\u0939\u0947\u0924...',\n            findingSpawn: '\u0938\u094d\u092a\u0949\u0928 \u092a\u0949\u0907\u0902\u091f \u0936\u094b\u0927\u0924 \u0906\u0939\u0947...',\n            ready: '\u0924\u092f\u093e\u0930!',\n            complete: '\u092a\u0942\u0930\u094d\u0923!',\n            creatingTextures: '\u091f\u0947\u0915\u094d\u0938\u094d\u091a\u0930 \u0924\u092f\u093e\u0930 \u0939\u094b\u0924 \u0906\u0939\u0947...',\n            instructionsTouch:\n              '\u25c0 \u0939\u0932\u0935\u093e | \u0921\u094d\u0930\u0945\u0917 \u0915\u0930\u0942\u0928 \u0932\u0915\u094d\u0937\u094d\u092f \u25b6 | \u26cf\ufe0f \u0916\u0923\u093e | \ud83d\udce6 \u0920\u0947\u0935\u093e | \u2b06\ufe0f \u0909\u0921\u0940 | \ud83d\udce6 \u0907\u0928\u094d\u0935\u094d\u0939\u0947\u0902\u091f\u0930\u0940',\n            instructionsDesktop:\n              '\u0938\u0941\u0930\u0942 \u0915\u0930\u0923\u094d\u092f\u093e\u0938\u093e\u0920\u0940 \u0915\u094d\u0932\u093f\u0915 \u2022 WASD \u0939\u0932\u0935\u093e \u2022 \u092e\u093e\u0909\u0938 \u092a\u0939\u093e \u2022 LMB \u0916\u0923\u093e \u2022 RMB \u0920\u0947\u0935\u093e \u2022 E \u0907\u0928\u094d\u0935\u094d\u0939\u0947\u0902\u091f\u0930\u0940 \u2022 M \u092e\u0947\u0928\u0942',\n            worlds: '\u091c\u0917',\n            browseWorlds: '\u091c\u0917 \u092c\u094d\u0930\u093e\u0909\u091d \u0915\u0930\u093e',\n            yourWorld: '\u0924\u0941\u092e\u091a\u0947 \u091c\u0917',\n            availableWorlds: '\u0909\u092a\u0932\u092c\u094d\u0927 \u091c\u0917',\n            hostWorld: '\u0924\u0941\u092e\u091a\u0947 \u091c\u0917 \u0939\u094b\u0938\u094d\u091f \u0915\u0930\u093e',\n            yourNickname: '\u0924\u0941\u092e\u091a\u0947 \u091f\u094b\u092a\u0923\u0928\u093e\u0935',\n            update: '\u0905\u092a\u0921\u0947\u091f',\n            worldLabel: '\u091c\u0917',\n            none: '\u0915\u093e\u0939\u0940 \u0928\u093e\u0939\u0940',\n            playersOnline: '{{count}} \u0916\u0947\u0933\u093e\u0921\u0942 \u0911\u0928\u0932\u093e\u0907\u0928',\n            playersOnline_plural: '{{count}} \u0916\u0947\u0933\u093e\u0921\u0942 \u0911\u0928\u0932\u093e\u0907\u0928',\n          },\n        },\n        ur: {\n          translation: {\n            gameTitle: '\u0645\u0627\u0626\u0646 \u06a9\u0631\u0627\u0641\u0679 \u0644\u0627\u0626\u0679',\n            loading: '\u0644\u0648\u0688 \u06c1\u0648 \u0631\u06c1\u0627 \u06c1\u06d2...',\n            starting: '\u0634\u0631\u0648\u0639 \u06c1\u0648 \u0631\u06c1\u0627 \u06c1\u06d2...',\n            initializing: '\u062a\u06cc\u0627\u0631 \u06c1\u0648 \u0631\u06c1\u0627 \u06c1\u06d2...',\n            loadingAssets: '\u0627\u062b\u0627\u062b\u06d2 \u0644\u0648\u0688 \u06c1\u0648 \u0631\u06c1\u06d2 \u06c1\u06cc\u06ba...',\n            loadingPlayerModel: '\u06a9\u06be\u0644\u0627\u0691\u06cc \u0645\u0627\u0688\u0644 \u0644\u0648\u0688 \u06c1\u0648 \u0631\u06c1\u0627 \u06c1\u06d2...',\n            playerModelLoaded: '\u06a9\u06be\u0644\u0627\u0691\u06cc \u0645\u0627\u0688\u0644 \u0644\u0648\u0688 \u06c1\u0648 \u06af\u06cc\u0627!',\n            loadingAlexModel: '\u0627\u0644\u06cc\u06a9\u0633 \u0645\u0627\u0688\u0644 \u0644\u0648\u0688 \u06c1\u0648 \u0631\u06c1\u0627 \u06c1\u06d2...',\n            alexModelLoaded: '\u0627\u0644\u06cc\u06a9\u0633 \u0645\u0627\u0688\u0644 \u0644\u0648\u0688 \u06c1\u0648 \u06af\u06cc\u0627!',\n            loadingZombieModel: '\u0632\u0648\u0645\u0628\u06cc \u0645\u0627\u0688\u0644 \u0644\u0648\u0688 \u06c1\u0648 \u0631\u06c1\u0627 \u06c1\u06d2...',\n            zombieModelLoaded: '\u0632\u0648\u0645\u0628\u06cc \u0645\u0627\u0688\u0644 \u0644\u0648\u0688 \u06c1\u0648 \u06af\u06cc\u0627!',\n            initializingWorld: '\u0644\u0627\u0645\u062d\u062f\u0648\u062f \u062f\u0646\u06cc\u0627 \u062a\u06cc\u0627\u0631 \u06c1\u0648 \u0631\u06c1\u06cc \u06c1\u06d2...',\n            worldReady: '\u062f\u0646\u06cc\u0627 \u062a\u0644\u0627\u0634 \u06a9\u06d2 \u0644\u06cc\u06d2 \u062a\u06cc\u0627\u0631 \u06c1\u06d2...',\n            buildingWorld: '\u062f\u0646\u06cc\u0627 \u0628\u0646 \u0631\u06c1\u06cc \u06c1\u06d2...',\n            infiniteWorldReady: '\u0644\u0627\u0645\u062d\u062f\u0648\u062f \u062f\u0646\u06cc\u0627 \u062a\u06cc\u0627\u0631 \u06c1\u06d2...',\n            settingUpControls: '\u06a9\u0646\u0679\u0631\u0648\u0644\u0632 \u0633\u06cc\u0679 \u06c1\u0648 \u0631\u06c1\u06d2 \u06c1\u06cc\u06ba...',\n            findingSpawn: '\u0633\u067e\u0648\u0646 \u067e\u0648\u0627\u0626\u0646\u0679 \u062a\u0644\u0627\u0634 \u06c1\u0648 \u0631\u06c1\u0627 \u06c1\u06d2...',\n            ready: '\u062a\u06cc\u0627\u0631!',\n            complete: '\u0645\u06a9\u0645\u0644!',\n            creatingTextures: '\u0679\u06cc\u06a9\u0633\u0686\u0631\u0632 \u0628\u0646 \u0631\u06c1\u06d2 \u06c1\u06cc\u06ba...',\n            instructionsTouch:\n              '\u25c0 \u0686\u0644\u0627\u0626\u06cc\u06ba | \u0646\u0634\u0627\u0646\u06c1 \u06af\u06be\u0633\u06cc\u0679\u06cc\u06ba \u25b6 | \u26cf\ufe0f \u06a9\u06be\u062f\u0627\u0626\u06cc | \ud83d\udce6 \u0631\u06a9\u06be\u06cc\u06ba | \u2b06\ufe0f \u0686\u06be\u0644\u0627\u0646\u06af | \ud83d\udce6 \u0627\u0646\u0648\u06cc\u0646\u0679\u0631\u06cc',\n            instructionsDesktop:\n              '\u0634\u0631\u0648\u0639 \u06a9\u0631\u0646\u06d2 \u06a9\u06d2 \u0644\u06cc\u06d2 \u06a9\u0644\u06a9 \u06a9\u0631\u06cc\u06ba \u2022 WASD \u0686\u0644\u0627\u0626\u06cc\u06ba \u2022 \u0645\u0627\u0624\u0633 \u062f\u06cc\u06a9\u06be\u06cc\u06ba \u2022 LMB \u06a9\u06be\u062f\u0627\u0626\u06cc \u2022 RMB \u0631\u06a9\u06be\u06cc\u06ba \u2022 E \u0627\u0646\u0648\u06cc\u0646\u0679\u0631\u06cc \u2022 M \u0645\u06cc\u0646\u0648',\n            worlds: '\u062f\u0646\u06cc\u0627',\n            browseWorlds: '\u062f\u0646\u06cc\u0627 \u0628\u0631\u0627\u0624\u0632 \u06a9\u0631\u06cc\u06ba',\n            yourWorld: '\u0622\u067e \u06a9\u06cc \u062f\u0646\u06cc\u0627',\n            availableWorlds: '\u062f\u0633\u062a\u06cc\u0627\u0628 \u062f\u0646\u06cc\u0627',\n            hostWorld: '\u0627\u067e\u0646\u06cc \u062f\u0646\u06cc\u0627 \u06c1\u0648\u0633\u0679 \u06a9\u0631\u06cc\u06ba',\n            yourNickname: '\u0622\u067e \u06a9\u0627 \u0639\u0631\u0641\u06cc \u0646\u0627\u0645',\n            update: '\u0627\u067e\u0688\u06cc\u0679',\n            worldLabel: '\u062f\u0646\u06cc\u0627',\n            none: '\u06a9\u0648\u0626\u06cc \u0646\u06c1\u06cc\u06ba',\n            playersOnline: '{{count}} \u06a9\u06be\u0644\u0627\u0691\u06cc \u0622\u0646 \u0644\u0627\u0626\u0646',\n            playersOnline_plural: '{{count}} \u06a9\u06be\u0644\u0627\u0691\u06cc \u0622\u0646 \u0644\u0627\u0626\u0646',\n          },\n        },\n        ar: {\n          translation: {\n            gameTitle: '\u0645\u0627\u064a\u0646 \u0643\u0631\u0627\u0641\u062a \u0644\u0627\u064a\u062a',\n            loading: '\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u062d\u0645\u064a\u0644...',\n            starting: '\u062c\u0627\u0631\u064a \u0627\u0644\u0628\u062f\u0621...',\n            initializing: '\u062c\u0627\u0631\u064a \u0627\u0644\u062a\u0647\u064a\u0626\u0629...',\n            loadingAssets: '\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0648\u0627\u0631\u062f...',\n            loadingPlayerModel: '\u062a\u062d\u0645\u064a\u0644 \u0646\u0645\u0648\u0630\u062c \u0627\u0644\u0644\u0627\u0639\u0628...',\n            playerModelLoaded: '\u062a\u0645 \u062a\u062d\u0645\u064a\u0644 \u0646\u0645\u0648\u0630\u062c \u0627\u0644\u0644\u0627\u0639\u0628!',\n            loadingAlexModel: '\u062a\u062d\u0645\u064a\u0644 \u0646\u0645\u0648\u0630\u062c \u0623\u0644\u064a\u0643\u0633...',\n            alexModelLoaded: '\u062a\u0645 \u062a\u062d\u0645\u064a\u0644 \u0646\u0645\u0648\u0630\u062c \u0623\u0644\u064a\u0643\u0633!',\n            loadingZombieModel: '\u062a\u062d\u0645\u064a\u0644 \u0646\u0645\u0648\u0630\u062c \u0627\u0644\u0632\u0648\u0645\u0628\u064a...',\n            zombieModelLoaded: '\u062a\u0645 \u062a\u062d\u0645\u064a\u0644 \u0646\u0645\u0648\u0630\u062c \u0627\u0644\u0632\u0648\u0645\u0628\u064a!',\n            initializingWorld: '\u062a\u0647\u064a\u0626\u0629 \u0627\u0644\u0639\u0627\u0644\u0645 \u0627\u0644\u0644\u0627\u0646\u0647\u0627\u0626\u064a...',\n            worldReady: '\u0627\u0644\u0639\u0627\u0644\u0645 \u062c\u0627\u0647\u0632 \u0644\u0644\u0627\u0633\u062a\u0643\u0634\u0627\u0641...',\n            buildingWorld: '\u0628\u0646\u0627\u0621 \u0627\u0644\u0639\u0627\u0644\u0645...',\n            infiniteWorldReady: '\u0627\u0644\u0639\u0627\u0644\u0645 \u0627\u0644\u0644\u0627\u0646\u0647\u0627\u0626\u064a \u062c\u0627\u0647\u0632...',\n            settingUpControls: '\u0625\u0639\u062f\u0627\u062f \u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u062a\u062d\u0643\u0645...',\n            findingSpawn: '\u0627\u0644\u0628\u062d\u062b \u0639\u0646 \u0646\u0642\u0637\u0629 \u0627\u0644\u0628\u062f\u0627\u064a\u0629...',\n            ready: '\u062c\u0627\u0647\u0632!',\n            complete: '\u0627\u0643\u062a\u0645\u0644!',\n            creatingTextures: '\u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u0642\u0648\u0627\u0645...',\n            instructionsTouch: '\u25c0 \u062a\u062d\u0631\u0643 | \u0627\u0633\u062d\u0628 \u0644\u0644\u062a\u0635\u0648\u064a\u0628 \u25b6 | \u26cf\ufe0f \u0627\u062d\u0641\u0631 | \ud83d\udce6 \u0636\u0639 | \u2b06\ufe0f \u0627\u0642\u0641\u0632 | \ud83d\udce6 \u0627\u0644\u0645\u062e\u0632\u0648\u0646',\n            instructionsDesktop:\n              '\u0627\u0646\u0642\u0631 \u0644\u0644\u0628\u062f\u0621 \u2022 WASD \u062a\u062d\u0631\u0643 \u2022 \u0627\u0644\u0645\u0627\u0648\u0633 \u0627\u0646\u0638\u0631 \u2022 LMB \u0627\u062d\u0641\u0631 \u2022 RMB \u0636\u0639 \u2022 E \u0627\u0644\u0645\u062e\u0632\u0648\u0646 \u2022 M \u0627\u0644\u0642\u0627\u0626\u0645\u0629',\n            worlds: '\u0627\u0644\u0639\u0648\u0627\u0644\u0645',\n            browseWorlds: '\u062a\u0635\u0641\u062d \u0627\u0644\u0639\u0648\u0627\u0644\u0645',\n            yourWorld: '\u0639\u0627\u0644\u0645\u0643',\n            availableWorlds: '\u0627\u0644\u0639\u0648\u0627\u0644\u0645 \u0627\u0644\u0645\u062a\u0627\u062d\u0629',\n            hostWorld: '\u0627\u0633\u062a\u0636\u0627\u0641\u0629 \u0639\u0627\u0644\u0645\u0643',\n            yourNickname: '\u0627\u0633\u0645\u0643 \u0627\u0644\u0645\u0633\u062a\u0639\u0627\u0631',\n            update: '\u062a\u062d\u062f\u064a\u062b',\n            worldLabel: '\u0627\u0644\u0639\u0627\u0644\u0645',\n            none: '\u0644\u0627 \u064a\u0648\u062c\u062f',\n            playersOnline: '{{count}} \u0644\u0627\u0639\u0628 \u0639\u0644\u0649 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a',\n            playersOnline_plural: '{{count}} \u0644\u0627\u0639\u0628\u064a\u0646 \u0639\u0644\u0649 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a',\n          },\n        },\n      };\n\n      // Initialize custom i18n\n      let currentLanguage = 'en';\n      if (typeof window !== 'undefined' && window.navigator && window.navigator.language) {\n        const lang = window.navigator.language.split('-')[0];\n        if (i18nResources[lang]) {\n          currentLanguage = lang;\n        }\n      }\n\n      // Helper function to get translation\n      function t(key, options) {\n        const dict = i18nResources[currentLanguage]?.translation || i18nResources['en'].translation;\n        let str = dict[key] || i18nResources['en'].translation[key] || key;\n        if (options && options.count !== undefined) {\n          if (options.count !== 1 && dict[key + '_plural']) {\n            str = dict[key + '_plural'];\n          }\n          str = str.replace('{{count}}', options.count);\n        }\n        return str;\n      }\n\n      // Check if current language is RTL\n      function isRTL() {\n        return currentLanguage === 'ar' || currentLanguage === 'ur';\n      }\n\n      const radToDeg = (rad) => rad * (180 / Math.PI);\n      const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n    </script>\n    <script>\n      /*\n       * MINECRAFT LITE\n       * A 3D voxel sandbox game where players mine blocks and build structures.\n       * Features procedural terrain, day/night cycle, and intuitive controls.\n       */\n\n      async function run(mode) {\n        const gameWorld = document.getElementById('game-world');\n        const canvas = document.getElementById('game-canvas');\n\n        // Constants\n        const BLOCK_TYPES = {\n          AIR: 0,\n          GRASS: 1,\n          DIRT: 2,\n          STONE: 3,\n          WOOD: 4,\n          LEAVES: 5,\n          DYNAMITE: 6,\n          SAND: 7,\n          WATER: 8,\n          CRAFTING_TABLE: 9,\n          PLANKS: 10,\n          TORCH: 11,\n          COAL_ORE: 12,\n          OAK_SIGN: 13,\n          CHEST: 14,\n          FURNACE: 15,\n          BEDROCK: 16,\n          GLASS: 17,\n          IRON_ORE: 18,\n          GOLD_ORE: 19,\n          DIAMOND_ORE: 20,\n          WATER_FLOW: 21,\n          LAVA: 22,\n          LAVA_FLOW: 23,\n          DOOR: 24,\n          SOIL: 25,\n          RICE_PLANT: 26,\n          BED: 27,\n          FLOWER: 28,\n          TV: 29,\n        };\n\n        const BLOCK_NAMES = [\n          'Air',\n          'Grass',\n          'Dirt',\n          'Stone',\n          'Wood',\n          'Leaves',\n          'Dynamite',\n          'Sand',\n          'Water',\n          'Crafting Table',\n          'Planks',\n          'Torch',\n          'Coal Ore',\n          'Oak Sign',\n          'Chest',\n          'Furnace',\n          'Bedrock',\n          'Glass',\n          'Iron Ore',\n          'Gold Ore',\n          'Diamond Ore',\n          'Water Flow',\n          'Lava',\n          'Lava Flow',\n          'Door',\n          'Soil',\n          'Rice Plant',\n          'Bed',\n          'Flower',\n          'TV',\n        ];\n\n        // Tool types (using 100+ to distinguish from blocks)\n        const ITEM_TYPES = {\n          WOODEN_PICKAXE: 101,\n          WOODEN_AXE: 102,\n          WOODEN_SWORD: 103,\n          STICK: 104,\n          COAL: 105,\n          STONE_PICKAXE: 106,\n          STONE_AXE: 107,\n          STONE_SWORD: 108,\n          WOODEN_SHOVEL: 109,\n          STONE_SHOVEL: 110,\n          IRON_INGOT: 111,\n          GOLD_INGOT: 112,\n          DIAMOND: 113,\n          IRON_PICKAXE: 114,\n          IRON_AXE: 115,\n          IRON_SWORD: 116,\n          IRON_SHOVEL: 117,\n          GOLD_PICKAXE: 118,\n          GOLD_AXE: 119,\n          GOLD_SWORD: 120,\n          GOLD_SHOVEL: 121,\n          DIAMOND_PICKAXE: 122,\n          DIAMOND_AXE: 123,\n          DIAMOND_SWORD: 124,\n          DIAMOND_SHOVEL: 125,\n          // Food items\n          RAW_BEEF: 126,\n          COOKED_BEEF: 127,\n          APPLE: 128,\n          BREAD: 129,\n          RAW_CHICKEN: 130,\n          COOKED_CHICKEN: 131,\n          BUCKET_EMPTY: 132,\n          BUCKET_WATER: 133,\n          BUCKET_LAVA: 134,\n          ARROW_WEAPON: 135,\n        };\n\n        const ITEM_NAMES = {\n          [ITEM_TYPES.WOODEN_PICKAXE]: 'Wooden Pickaxe',\n          [ITEM_TYPES.WOODEN_AXE]: 'Wooden Axe',\n          [ITEM_TYPES.WOODEN_SWORD]: 'Wooden Sword',\n          [ITEM_TYPES.STICK]: 'Stick',\n          [ITEM_TYPES.COAL]: 'Coal',\n          [ITEM_TYPES.STONE_PICKAXE]: 'Stone Pickaxe',\n          [ITEM_TYPES.STONE_AXE]: 'Stone Axe',\n          [ITEM_TYPES.STONE_SWORD]: 'Stone Sword',\n          [ITEM_TYPES.WOODEN_SHOVEL]: 'Wooden Shovel',\n          [ITEM_TYPES.STONE_SHOVEL]: 'Stone Shovel',\n          [ITEM_TYPES.IRON_INGOT]: 'Iron Ingot',\n          [ITEM_TYPES.GOLD_INGOT]: 'Gold Ingot',\n          [ITEM_TYPES.DIAMOND]: 'Diamond',\n          [ITEM_TYPES.IRON_PICKAXE]: 'Iron Pickaxe',\n          [ITEM_TYPES.IRON_AXE]: 'Iron Axe',\n          [ITEM_TYPES.IRON_SWORD]: 'Iron Sword',\n          [ITEM_TYPES.IRON_SHOVEL]: 'Iron Shovel',\n          [ITEM_TYPES.GOLD_PICKAXE]: 'Gold Pickaxe',\n          [ITEM_TYPES.GOLD_AXE]: 'Gold Axe',\n          [ITEM_TYPES.GOLD_SWORD]: 'Gold Sword',\n          [ITEM_TYPES.GOLD_SHOVEL]: 'Gold Shovel',\n          [ITEM_TYPES.DIAMOND_PICKAXE]: 'Diamond Pickaxe',\n          [ITEM_TYPES.DIAMOND_AXE]: 'Diamond Axe',\n          [ITEM_TYPES.DIAMOND_SWORD]: 'Diamond Sword',\n          [ITEM_TYPES.DIAMOND_SHOVEL]: 'Diamond Shovel',\n          [ITEM_TYPES.RAW_BEEF]: 'Raw Beef',\n          [ITEM_TYPES.COOKED_BEEF]: 'Cooked Beef',\n          [ITEM_TYPES.APPLE]: 'Apple',\n          [ITEM_TYPES.BREAD]: 'Bread',\n          [ITEM_TYPES.RAW_CHICKEN]: 'Raw Chicken',\n          [ITEM_TYPES.COOKED_CHICKEN]: 'Cooked Chicken',\n          [ITEM_TYPES.BUCKET_EMPTY]: 'Bucket',\n          [ITEM_TYPES.BUCKET_WATER]: 'Water Bucket',\n          [ITEM_TYPES.BUCKET_LAVA]: 'Lava Bucket',\n          [ITEM_TYPES.ARROW_WEAPON]: 'Arrow Weapon',\n        };\n\n        // Tool effectiveness: which blocks they mine faster\n        const TOOL_EFFECTIVENESS = {\n          [ITEM_TYPES.WOODEN_PICKAXE]: {\n            blocks: [\n              BLOCK_TYPES.STONE,\n              BLOCK_TYPES.SAND,\n              BLOCK_TYPES.COAL_ORE,\n              BLOCK_TYPES.IRON_ORE,\n              BLOCK_TYPES.GOLD_ORE,\n              BLOCK_TYPES.DIAMOND_ORE,\n            ],\n            speedMultiplier: 2.0,\n          },\n          [ITEM_TYPES.WOODEN_AXE]: {\n            blocks: [BLOCK_TYPES.WOOD, BLOCK_TYPES.LEAVES],\n            speedMultiplier: 2.0,\n          },\n          [ITEM_TYPES.WOODEN_SWORD]: {\n            damageMultiplier: 2.0,\n          },\n          [ITEM_TYPES.STONE_PICKAXE]: {\n            blocks: [\n              BLOCK_TYPES.STONE,\n              BLOCK_TYPES.SAND,\n              BLOCK_TYPES.COAL_ORE,\n              BLOCK_TYPES.IRON_ORE,\n              BLOCK_TYPES.GOLD_ORE,\n              BLOCK_TYPES.DIAMOND_ORE,\n            ],\n            speedMultiplier: 4.0,\n          },\n          [ITEM_TYPES.STONE_AXE]: {\n            blocks: [BLOCK_TYPES.WOOD, BLOCK_TYPES.LEAVES],\n            speedMultiplier: 4.0,\n          },\n          [ITEM_TYPES.STONE_SWORD]: {\n            damageMultiplier: 3.0,\n          },\n          [ITEM_TYPES.WOODEN_SHOVEL]: {\n            blocks: [BLOCK_TYPES.DIRT, BLOCK_TYPES.SAND, BLOCK_TYPES.GRASS],\n            speedMultiplier: 2.0,\n          },\n          [ITEM_TYPES.STONE_SHOVEL]: {\n            blocks: [BLOCK_TYPES.DIRT, BLOCK_TYPES.SAND, BLOCK_TYPES.GRASS],\n            speedMultiplier: 4.0,\n          },\n          [ITEM_TYPES.IRON_PICKAXE]: {\n            blocks: [\n              BLOCK_TYPES.STONE,\n              BLOCK_TYPES.SAND,\n              BLOCK_TYPES.COAL_ORE,\n              BLOCK_TYPES.IRON_ORE,\n              BLOCK_TYPES.GOLD_ORE,\n              BLOCK_TYPES.DIAMOND_ORE,\n            ],\n            speedMultiplier: 6.0,\n          },\n          [ITEM_TYPES.IRON_AXE]: {\n            blocks: [BLOCK_TYPES.WOOD, BLOCK_TYPES.LEAVES, BLOCK_TYPES.PLANKS],\n            speedMultiplier: 6.0,\n          },\n          [ITEM_TYPES.IRON_SWORD]: {\n            damageMultiplier: 4.0,\n          },\n          [ITEM_TYPES.IRON_SHOVEL]: {\n            blocks: [BLOCK_TYPES.DIRT, BLOCK_TYPES.SAND, BLOCK_TYPES.GRASS],\n            speedMultiplier: 6.0,\n          },\n          [ITEM_TYPES.GOLD_PICKAXE]: {\n            blocks: [\n              BLOCK_TYPES.STONE,\n              BLOCK_TYPES.SAND,\n              BLOCK_TYPES.COAL_ORE,\n              BLOCK_TYPES.IRON_ORE,\n              BLOCK_TYPES.GOLD_ORE,\n              BLOCK_TYPES.DIAMOND_ORE,\n            ],\n            speedMultiplier: 10.0,\n          },\n          [ITEM_TYPES.GOLD_AXE]: {\n            blocks: [BLOCK_TYPES.WOOD, BLOCK_TYPES.LEAVES, BLOCK_TYPES.PLANKS],\n            speedMultiplier: 10.0,\n          },\n          [ITEM_TYPES.GOLD_SWORD]: {\n            damageMultiplier: 3.0,\n          },\n          [ITEM_TYPES.GOLD_SHOVEL]: {\n            blocks: [BLOCK_TYPES.DIRT, BLOCK_TYPES.SAND, BLOCK_TYPES.GRASS],\n            speedMultiplier: 10.0,\n          },\n          [ITEM_TYPES.DIAMOND_PICKAXE]: {\n            blocks: [\n              BLOCK_TYPES.STONE,\n              BLOCK_TYPES.SAND,\n              BLOCK_TYPES.COAL_ORE,\n              BLOCK_TYPES.IRON_ORE,\n              BLOCK_TYPES.GOLD_ORE,\n              BLOCK_TYPES.DIAMOND_ORE,\n            ],\n            speedMultiplier: 8.0,\n          },\n          [ITEM_TYPES.DIAMOND_AXE]: {\n            blocks: [BLOCK_TYPES.WOOD, BLOCK_TYPES.LEAVES, BLOCK_TYPES.PLANKS],\n            speedMultiplier: 8.0,\n          },\n          [ITEM_TYPES.DIAMOND_SWORD]: {\n            damageMultiplier: 5.0,\n          },\n          [ITEM_TYPES.DIAMOND_SHOVEL]: {\n            blocks: [BLOCK_TYPES.DIRT, BLOCK_TYPES.SAND, BLOCK_TYPES.GRASS],\n            speedMultiplier: 8.0,\n          },\n        };\n\n        // Check if an item type is a tool\n        function isItem(itemType) {\n          return itemType >= 100;\n        }\n\n        function isTool(itemType) {\n          return TOOL_EFFECTIVENESS[itemType] !== undefined;\n        }\n\n        // Get item name (works for both blocks and tools)\n        function getItemName(itemType) {\n          if (isItem(itemType)) {\n            return ITEM_NAMES[itemType] || 'Unknown Item';\n          }\n          return BLOCK_NAMES[itemType] || 'Unknown';\n        }\n\n        const BLOCK_HARDNESS = {\n          [BLOCK_TYPES.GRASS]: 1.0,\n          [BLOCK_TYPES.DIRT]: 0.8,\n          [BLOCK_TYPES.STONE]: 2.0,\n          [BLOCK_TYPES.WOOD]: 1.5,\n          [BLOCK_TYPES.LEAVES]: 0.3,\n          [BLOCK_TYPES.DYNAMITE]: 0.5,\n          [BLOCK_TYPES.SAND]: 0.6,\n          [BLOCK_TYPES.WATER]: 0.4,\n          [BLOCK_TYPES.CRAFTING_TABLE]: 1.2,\n          [BLOCK_TYPES.PLANKS]: 1.0,\n          [BLOCK_TYPES.TORCH]: 0.1,\n          [BLOCK_TYPES.COAL_ORE]: 2.5,\n          [BLOCK_TYPES.OAK_SIGN]: 0.8,\n          [BLOCK_TYPES.CHEST]: 1.5,\n          [BLOCK_TYPES.FURNACE]: 2.0,\n          [BLOCK_TYPES.BEDROCK]: Infinity,\n          [BLOCK_TYPES.GLASS]: 0.5,\n          [BLOCK_TYPES.IRON_ORE]: 3.0,\n          [BLOCK_TYPES.GOLD_ORE]: 3.0,\n          [BLOCK_TYPES.DIAMOND_ORE]: 3.5,\n          [BLOCK_TYPES.WATER_FLOW]: 0.4,\n          [BLOCK_TYPES.LAVA]: 100,\n          [BLOCK_TYPES.LAVA_FLOW]: 100,\n          [BLOCK_TYPES.DOOR]: 1.0,\n          [BLOCK_TYPES.SOIL]: 0.6,\n          [BLOCK_TYPES.RICE_PLANT]: 0.1,\n          [BLOCK_TYPES.BED]: 0.5,\n          [BLOCK_TYPES.FLOWER]: 0.1,\n          [BLOCK_TYPES.TV]: 1.2,\n        };\n\n        let remixed = null;\n\n        // Initialize config defaults\n        const defaultConfig = {\n          version: '1.0.0',\n          worldHeight: 64, // Height is still limited, but X/Z are infinite\n          treeDensity: 0.02,\n          flowerDensity: 0.015,\n          flowerClusterSize: 6,\n          flowerClusterDensity: 0.18,\n          miningSpeedMultiplier: 1.0,\n          movementSpeedMultiplier: 1.0,\n          hungerRateMultiplier: 1.0,\n          fallDamageThreshold: 3,\n          // Day/Night cycle enabled for obby atmosphere\n          dayNightCycleEnabled: true,\n          dayNightCycleDuration: 180,\n          startTimeOfDay: 'day',\n          zombiesEnabled: true,\n          creepersEnabled: false,\n          chickensEnabled: true,\n          cowsEnabled: true,\n          villagesEnabled: true,\n          forceMobileUI: false,\n          onlineEnabled: true,\n          firebasePrefix: 'sandbox',\n          showWorldsUI: false,\n          lodEnabled: true,\n          lodNearDistance: 40,\n          lodMediumDistance: 80,\n          lodFarDistance: 160,\n          chunkDeletionBufferMs: 1000,\n          inventory: [\n            {\n              type: BLOCK_TYPES.GRASS,\n              count: 64,\n            },\n            {\n              type: BLOCK_TYPES.DIRT,\n              count: 64,\n            },\n            {\n              type: BLOCK_TYPES.STONE,\n              count: 64,\n            },\n            {\n              type: BLOCK_TYPES.WOOD,\n              count: 64,\n            },\n            {\n              type: BLOCK_TYPES.LEAVES,\n              count: 64,\n            },\n            {\n              type: BLOCK_TYPES.DYNAMITE,\n              count: 128,\n            },\n            {\n              type: BLOCK_TYPES.SAND,\n              count: 64,\n            },\n            {\n              type: BLOCK_TYPES.WATER,\n              count: 64,\n            },\n            {\n              type: BLOCK_TYPES.TV,\n              count: 16,\n            },\n            {\n              type: ITEM_TYPES.ARROW_WEAPON,\n              count: 1,\n            },\n          ],\n          worldSeed: Math.floor(Math.random() * 1000000),\n          worldData: null,\n        };\n\n        // Merge with existing config\n        if (!window.gameConfig || Object.keys(window.gameConfig).length === 0) {\n          Object.assign(window.gameConfig, defaultConfig);\n        }\n\n        const config = window.gameConfig;\n\n        // Asset cache\n        const assetCache = {\n          images: {},\n          audio: {},\n        };\n\n        const prefix = 'sandbox';\n        window.roomManager = null;\n        let lobby = null;\n        let currentRoomName = null;\n        let justJoined = false;\n\n        let firebaseApp = null;\n        let firebaseDb = null;\n        let isOnline = false;\n\n        const SYNC_INTERVAL = 50;\n        const CLEANUP_INTERVAL = 5000;\n        const PLAYER_TIMEOUT = 10000;\n        const LERP_FACTOR = 0.15;\n        const MAX_VISIBLE_PLAYERS = 20;\n        const PLAYER_MESH_RANGE = 60; // Only create meshes for players within this range\n        const INPUT_BUFFER_DELAY_MS = 0;\n        const VISIBILITY_UPDATE_INTERVAL = 250; // Update visible players every 250ms\n        let lastVisibilityUpdate = 0;\n        let visiblePlayerIds = new Set();\n\n        let otherPlayers = new Map();\n        let playerTargetPositions = new Map();\n        const mobTargetPositions = new Map();\n        let playerRef = null;\n        let lastSyncTime = 0;\n        let lastCleanupTime = 0;\n        let myPlayerYaw = 0;\n        let myPlayerPitch = 0;\n        let joinedAt = 0; // Timestamp when player joined - used to filter past events\n\n        function generateMobId() {\n          return 'm' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5);\n        }\n\n        function generatePlayerId() {\n          return 'p' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5);\n        }\n\n        let myPlayerId = generatePlayerId();\n        let myPlayerName = 'Player ' + myPlayerId.substr(-4, 4);\n\n        // Persisted nickname helpers (stored in user game state)\n        async function saveNickname(name) {\n          try {\n            const trimmedName = (name || '').trim();\n            if (!trimmedName) {\n              lib.log('saveNickname skipped: empty nickname');\n              return;\n            }\n\n            const response = await lib.getUserGameState();\n            const existingState =\n              response && response.state && typeof response.state === 'object' ? response.state : {};\n            const mergedState = {\n              ...existingState,\n              nickname: trimmedName,\n            };\n\n            await lib.saveUserGameState(mergedState);\n            lib.log('Nickname saved successfully');\n          } catch (error) {\n            lib.log(`Failed to save nickname: ${error.message}`);\n          }\n        }\n\n        async function loadNickname() {\n          try {\n            const response = await lib.getUserGameState();\n            const state = response && response.state;\n\n            if (state && typeof state.nickname === 'string' && state.nickname.trim()) {\n              myPlayerName = state.nickname.trim();\n\n              if (typeof window.refreshNicknameDisplay === 'function') {\n                window.refreshNicknameDisplay();\n              }\n\n              if (isOnline) {\n                syncEvent('nickname', { name: myPlayerName });\n              }\n\n              lib.log('Nickname loaded successfully');\n            } else {\n              lib.log('No saved nickname found');\n            }\n          } catch (error) {\n            lib.log(`Failed to load nickname: ${error.message}`);\n          }\n        }\n\n        function initFirebase() {\n          // Check if online mode is enabled in config\n          if (!config.onlineEnabled) {\n            console.log('Online mode disabled in config - running offline');\n            return false;\n          }\n          if (\n            !window.gameConfig.firebaseConfig.apiKey ||\n            !window.gameConfig.firebaseConfig.databaseURL\n          ) {\n            console.log('Firebase config not configured - running offline');\n            return false;\n          }\n          try {\n            isOnline = true;\n            console.log('Firebase initialized - running online');\n            return true;\n          } catch (e) {\n            console.error('Firebase init error:', e);\n            return false;\n          }\n        }\n\n        function updatePlayerVisibility() {\n          // Throttle visibility updates\n          const now = getTimeStamp();\n          if (now - lastVisibilityUpdate < VISIBILITY_UPDATE_INTERVAL) return;\n          lastVisibilityUpdate = now;\n\n          const playerDistances = [];\n          const meshRangeSq = PLAYER_MESH_RANGE * PLAYER_MESH_RANGE;\n\n          otherPlayers.forEach((data, pid) => {\n            const target = playerTargetPositions.get(pid);\n            if (!target) return;\n\n            const dx = target.x - playerPos.x;\n            const dy = target.y - playerPos.y;\n            const dz = target.z - playerPos.z;\n            const distSq = dx * dx + dy * dy + dz * dz;\n            playerDistances.push({ pid, distSq, data });\n          });\n\n          // Sort by distance (closest first)\n          playerDistances.sort((a, b) => a.distSq - b.distSq);\n\n          // Update visible player set\n          const newVisibleIds = new Set();\n          for (let i = 0; i < Math.min(MAX_VISIBLE_PLAYERS, playerDistances.length); i++) {\n            if (playerDistances[i].distSq <= meshRangeSq) {\n              newVisibleIds.add(playerDistances[i].pid);\n            }\n          }\n\n          // Create meshes for newly visible players\n          for (const { pid, data } of playerDistances) {\n            if (newVisibleIds.has(pid) && !window['playerMesh_' + pid]) {\n              createOtherPlayerMesh(pid, otherPlayers.get(pid));\n            }\n          }\n\n          // Remove meshes for players no longer visible\n          visiblePlayerIds.forEach((pid) => {\n            if (!newVisibleIds.has(pid)) {\n              removeOtherPlayerMesh(pid);\n            }\n          });\n\n          visiblePlayerIds = newVisibleIds;\n        }\n\n        function updateOtherPlayerTarget(pid, data) {\n          const target = playerTargetPositions.get(pid);\n          if (target) {\n            target.x = data.x;\n            target.y = data.y - 0.8;\n            target.z = data.z;\n            target.yaw = data.yaw || 0;\n            target.pitch = data.pitch * -1 || 0;\n          }\n        }\n\n        function renderOtherPlayers() {\n          // Update which players should be visible (throttled)\n          updatePlayerVisibility();\n\n          // Only iterate visible players for smooth updates\n          visiblePlayerIds.forEach((pid) => {\n            const playerGroup = window['playerMesh_' + pid];\n            if (!playerGroup) return;\n\n            playerGroup.visible = true;\n            const target = playerTargetPositions.get(pid);\n            if (!target) return;\n\n            // Store position before lerp to calculate actual movement\n            const lastPos = {\n              x: playerGroup.position.x,\n              y: playerGroup.position.y,\n              z: playerGroup.position.z,\n            };\n\n            playerGroup.position.x += (target.x - playerGroup.position.x) * LERP_FACTOR;\n            playerGroup.position.y += (target.y - playerGroup.position.y) * LERP_FACTOR;\n            playerGroup.position.z += (target.z - playerGroup.position.z) * LERP_FACTOR;\n\n            const dx = playerGroup.position.x - lastPos.x;\n            const dz = playerGroup.position.z - lastPos.z;\n            const movementSpeed = Math.sqrt(dx * dx + dz * dz) / 0.016; // Speed per second\n\n            // Update AnimationMixer if available\n            const mixer = playerGroup.userData.mixer;\n            if (mixer) {\n              // Update mixer with delta time (~16ms at 60fps)\n              mixer.update(0.016);\n\n              // Switch between idle and walk based on movement\n              const isMoving = movementSpeed > 0.5;\n              const currentAction = playerGroup.userData.currentAction;\n              const actions = playerGroup.userData.actions;\n\n              if (isMoving && currentAction !== 'walk') {\n                // Switch to walk animation\n                const walkAction = actions['walk'];\n                const idleAction = actions['idle'];\n                if (walkAction) {\n                  if (idleAction) {\n                    idleAction.fadeOut(0.2);\n                  }\n                  walkAction.reset().fadeIn(0.2).play();\n                  playerGroup.userData.currentAction = 'walk';\n                }\n              } else if (!isMoving && currentAction !== 'idle') {\n                // Switch to idle animation\n                const walkAction = actions['walk'];\n                const idleAction = actions['idle'];\n                if (idleAction) {\n                  if (walkAction) {\n                    walkAction.fadeOut(0.2);\n                  }\n                  idleAction.reset().fadeIn(0.2).play();\n                  playerGroup.userData.currentAction = 'idle';\n                }\n              }\n            } else {\n              // Fallback manual animation for non-GLB models\n              const isMoving = movementSpeed > 0.5;\n              if (isMoving) {\n                playerGroup.userData.animationTime =\n                  (playerGroup.userData.animationTime || 0) + 0.016 * Math.min(movementSpeed, 6);\n\n                const swingAmount = Math.PI / 4;\n                const phase = Math.sin(playerGroup.userData.animationTime * Math.PI);\n\n                if (playerGroup.userData.leftArm) {\n                  playerGroup.userData.leftArm.rotation.x = -phase * swingAmount;\n                }\n                if (playerGroup.userData.rightArm) {\n                  playerGroup.userData.rightArm.rotation.x = phase * swingAmount;\n                }\n                if (playerGroup.userData.leftLeg) {\n                  playerGroup.userData.leftLeg.rotation.x = phase * swingAmount;\n                }\n                if (playerGroup.userData.rightLeg) {\n                  playerGroup.userData.rightLeg.rotation.x = -phase * swingAmount;\n                }\n              }\n            }\n\n            // Body rotation follows movement direction (calculated from velocity)\n            playerGroup.rotation.y += (target.yaw - playerGroup.rotation.y) * LERP_FACTOR;\n\n            // Head rotation handles yaw (looking left/right) using skeleton bone\n            // NOTE: Must happen AFTER mixer.update() so animation doesn't overwrite it!\n            const head = playerGroup.userData.head;\n            if (head) {\n              // Apply pitch to head bone for up/down looking\n              head.rotation.x += (target.pitch - head.rotation.x) * LERP_FACTOR;\n            }\n          });\n        }\n\n        // Chat functions\n        let chatUnsubscribe = null;\n        let chatInputHandler = null;\n        let chatSendHandler = null;\n        let chatReceiveHandler = null;\n\n        // Unified event system - single /events ref for scalability\n        function syncEvent(type, data) {\n          if (!lobby) return;\n          lobby.sendMessage(\n            type,\n            {\n              playerId: myPlayerId,\n              playerName: myPlayerName,\n              type: type,\n              ...data,\n              timestamp: getTimeStamp(),\n            },\n            INPUT_BUFFER_DELAY_MS\n          );\n        }\n\n        function syncChatMessage(text) {\n          if (!text || !text.trim()) return;\n          syncEvent('chat', { text: text.trim() });\n        }\n\n        function syncBlockAction(action, x, y, z, blockType) {\n          syncEvent(action, { x, y, z, blockType });\n        }\n\n        function syncMobSpawn(mobId, type, x, y, z) {\n          if (!lobby) {\n            return;\n          }\n          if (IAmFirstJoinerHost(lobby)) {\n            syncEvent('mob_spawn', { mobId, type, x, y, z });\n          }\n        }\n\n        function syncMobUpdate(mobId, x, y, z, yaw, health, exploding, explodeProgress) {\n          if (!lobby) {\n            return;\n          }\n          if (IAmFirstJoinerHost(lobby)) {\n            syncEvent('mob_update', { mobId, x, y, z, yaw, health, exploding, explodeProgress });\n          }\n        }\n\n        function syncMobHit(mobId, damage) {\n          syncEvent('mob_hit', { mobId, damage });\n        }\n\n        function syncMobRemove(mobId) {\n          if (!lobby) {\n            return;\n          }\n          if (IAmFirstJoinerHost(lobby)) {\n            syncEvent('mob_remove', { mobId });\n          }\n        }\n\n        function syncPlayerSwing() {\n          if (!lobby) return;\n          syncEvent('player_swing', {});\n        }\n\n        let lastEventCleanup = 0;\n        const EVENT_CLEANUP_INTERVAL = 30000;\n\n        function subscribeToEvents() {\n          if (!isOnline || !roomManager) return;\n          function handleEvents(eventType, eventData) {\n            if (eventData.playerId === myPlayerId) return;\n\n            const displayName = eventData.playerName || 'Unknown';\n\n            if (eventType === 'position') {\n              eventData.localReceiveTime = performance.now();\n\n              // Check if gender changed for this player\n              const existingData = otherPlayers.get(eventData.playerId);\n              const genderChanged =\n                existingData && existingData.gender !== eventData.gender && eventData.gender;\n\n              otherPlayers.set(eventData.playerId, eventData);\n              playerTargetPositions.set(eventData.playerId, {\n                x: eventData.x,\n                y: eventData.y,\n                z: eventData.z,\n                yaw: eventData.yaw || 0,\n                pitch: eventData.pitch || 0,\n              });\n              updateOtherPlayerTarget(eventData.playerId, eventData);\n\n              // If gender changed, rebuild the mesh with the correct model\n              if (genderChanged) {\n                removeOtherPlayerMesh(eventData.playerId);\n                createOtherPlayerMesh(eventData.playerId, eventData);\n              }\n            } else if (eventType === 'nickname') {\n              const { name } = eventData;\n              if (name && otherPlayers.has(eventData.playerId)) {\n                const playerData = otherPlayers.get(eventData.playerId);\n                playerData.playerName = name;\n                otherPlayers.set(eventData.playerId, playerData);\n                // Refresh name tag\n                removeOtherPlayerMesh(eventData.playerId);\n                createOtherPlayerMesh(eventData.playerId, playerData);\n              }\n            } else if (eventType === 'chat') {\n              if (typeof addChatMessage === 'function')\n                addChatMessage(displayName, eventData.text, false);\n            } else if (eventType === 'emoji') {\n              // Handle emoji event - show emoji above other player's head\n              const playerGroup = window['playerMesh_' + eventData.playerId];\n              if (playerGroup && window.showPlayerChatBubble) {\n                window.showPlayerChatBubble(playerGroup, quickEmojis[eventData.emojiIndex]);\n              }\n            } else if (eventType === 'player_hit') {\n              // Handle player hit event - show damage indicator above player's head\n              const playerGroup = window['playerMesh_' + eventData.playerId];\n              if (playerGroup && window.showPlayerDamage) {\n                window.showPlayerDamage(playerGroup, eventData.damage);\n              }\n            } else if (eventType === 'mine') {\n              const { x, y, z, blockType } = eventData;\n              if (y >= 0 && y < worldHeight) {\n                const oldType = worldData[x][y][z];\n                if (oldType !== BLOCK_TYPES.AIR) {\n                  worldData[x][y][z] = BLOCK_TYPES.AIR;\n                  updateBlockAt(x, y, z);\n                  modifiedBlocks.set(`${x},${y},${z}`, BLOCK_TYPES.AIR);\n                  createBreakParticles(x + 0.5, y + 0.5, z + 0.5, oldType);\n\n                  // Play sound with volume based on distance\n                  playSoundAtPosition('block_break_sound', x + 0.5, y + 0.5, z + 0.5, 0.6, 40);\n                }\n              }\n            } else if (eventType === 'place') {\n              const { x, y, z, blockType } = eventData;\n              if (y >= 0 && y < worldHeight) {\n                if (worldData[x][y][z] === BLOCK_TYPES.AIR) {\n                  worldData[x][y][z] = blockType;\n                  updateBlockAt(x, y, z);\n                  modifiedBlocks.set(`${x},${y},${z}`, blockType);\n\n                  // Play sound with volume based on distance\n                  playSoundAtPosition('block_place_sound', x + 0.5, y + 0.5, z + 0.5, 0.6, 40);\n                }\n              }\n            } else if (eventType === 'tnt') {\n              const { x, y, z } = eventData;\n              if (y >= 0 && y < worldHeight) {\n                if (worldData[x][y][z] === BLOCK_TYPES.DYNAMITE) {\n                  dynamiteCountdowns.set(`${x},${y},${z}`, {\n                    timeLeft: 3.0,\n                    totalTime: 3.0,\n                    x,\n                    y,\n                    z,\n                  });\n                  // Play TNT hiss with volume based on distance\n                  playSoundAtPosition('creeper_hiss_sound', x + 0.5, y + 0.5, z + 0.5, 0.8, 50);\n                }\n              }\n            } else if (eventType === 'sign_message') {\n              // Handle sign message sync from other players\n              const { x, y, z, message } = eventData;\n              const signKey = `${x},${y},${z}`;\n              signMessages.set(signKey, message);\n\n              // Update the sign text display if mesh exists\n              const signMesh = signMeshes.get(signKey);\n              if (signMesh) {\n                updateSignText(signMesh, message);\n              }\n            } else if (eventType === 'chest_update') {\n              // Handle chest item sync from other players\n              const { x, y, z, slotIndex, item } = eventData;\n              const chestKey = `${x},${y},${z}`;\n\n              // Initialize chest contents if not exists\n              if (!chestContents.has(chestKey)) {\n                const emptyChest = [];\n                for (let i = 0; i < 27; i++) {\n                  emptyChest.push({ type: BLOCK_TYPES.AIR, count: 0 });\n                }\n                chestContents.set(chestKey, emptyChest);\n              }\n\n              // Update the specific slot\n              const chestInv = chestContents.get(chestKey);\n              if (chestInv && slotIndex >= 0 && slotIndex < 27) {\n                chestInv[slotIndex] = { ...item };\n\n                // Update UI if this chest is currently open\n                if (isChestOpen && openChestKey === chestKey) {\n                  updateChestUI();\n                }\n              }\n            } else if (eventType === 'furnace_update') {\n              // Handle furnace item sync from other players\n              const { x, y, z, slotType, item } = eventData;\n              const furnaceKey = `${x},${y},${z}`;\n\n              // Initialize furnace contents if not exists\n              if (!furnaceContents.has(furnaceKey)) {\n                furnaceContents.set(furnaceKey, {\n                  input: { type: BLOCK_TYPES.AIR, count: 0 },\n                  fuel: { type: BLOCK_TYPES.AIR, count: 0 },\n                  output: { type: BLOCK_TYPES.AIR, count: 0 },\n                  progress: 0,\n                  fuelProgress: 0,\n                });\n              }\n\n              // Update the specific slot\n              const furnace = furnaceContents.get(furnaceKey);\n              if (\n                furnace &&\n                (slotType === 'input' || slotType === 'fuel' || slotType === 'output')\n              ) {\n                furnace[slotType] = { ...item };\n\n                // Update UI if this furnace is currently open\n                if (isFurnaceOpen && openFurnaceKey === furnaceKey) {\n                  updateFurnaceUI();\n                }\n              }\n            } else if (eventType === 'world_state') {\n              recieveWorldState(eventData);\n            } else if (eventType === 'mob_spawn') {\n              const { mobId, type, x, y, z, yaw } = eventData;\n              if (typeof createMob === 'function') {\n                const mob = createMob(type, x, y, z);\n                mob.id = mobId;\n                mobs.push(mob);\n                mobTargetPositions.set(mobId, {\n                  x: x,\n                  y: y,\n                  z: z,\n                  yaw: yaw || 0,\n                  health: eventData.health,\n                  exploding: eventData.exploding,\n                  explodeProgress: eventData.explodeProgress,\n                });\n              }\n            } else if (eventType === 'mob_update') {\n              const { mobId, x, y, z, yaw, health, exploding, explodeProgress } = eventData;\n              const target = mobTargetPositions.get(mobId);\n              if (target) {\n                target.x = x;\n                target.y = y;\n                target.z = z;\n                if (yaw !== undefined) target.yaw = yaw;\n                if (health !== undefined) target.health = health;\n                if (exploding !== undefined) target.exploding = exploding;\n                if (explodeProgress !== undefined) target.explodeProgress = explodeProgress;\n              }\n              const mob = mobs.find((m) => m.id === mobId);\n              if (mob) {\n                mob.health = health;\n                mob.exploding = exploding;\n                mob.explodeProgress = explodeProgress;\n              }\n            } else if (eventType === 'mob_remove') {\n              const { mobId } = eventData;\n              mobTargetPositions.delete(mobId);\n              const index = mobs.findIndex((m) => m.id === mobId);\n              if (index !== -1) {\n                if (mobs[index].group) {\n                  scene.remove(mobs[index].group);\n                }\n                mobs.splice(index, 1);\n              }\n            } else if (eventType === 'mob_hit') {\n              const { mobId, damage } = eventData;\n              const index = mobs.findIndex((m) => m.id === mobId);\n              if (index !== -1) {\n                damageMob(mobs[index], damage);\n              }\n            } else if (eventType === 'player_swing') {\n              // Trigger swing animation for the player who sent this event\n              if (typeof window.triggerRemotePlayerSwing === 'function') {\n                window.triggerRemotePlayerSwing(eventData.playerId);\n              }\n            } else if (eventType === 'gender_change') {\n              // Handle gender change from other players - rebuild their mesh with new model\n              const { gender } = eventData;\n              const playerData = otherPlayers.get(eventData.playerId);\n              if (playerData) {\n                playerData.gender = gender;\n                // Remove old mesh and create new one with correct model\n                removeOtherPlayerMesh(eventData.playerId);\n                createOtherPlayerMesh(eventData.playerId, playerData);\n              }\n            } else if (eventType === 'door_toggle') {\n              // Handle door toggle from other players\n              const { x, y, z, isOpen } = eventData;\n              const doorKey = getDoorBaseKey(x, y, z);\n              doorStates.set(doorKey, isOpen);\n\n              // Update the door mesh rotation\n              const doorMesh = doorMeshes.get(doorKey);\n              if (doorMesh) {\n                doorMesh.rotation.y = isOpen ? -Math.PI / 2 : 0;\n              }\n\n              // Play sound with volume based on distance\n              playSoundAtPosition('block_place_sound', x + 0.5, y + 0.5, z + 0.5, 0.4, 30);\n            }\n          }\n\n          window.handleRecievedData = function (event) {\n            const correction = Math.max(0, event.delayTimestamp - getTimeStamp());\n\n            // Handle recieved data from players\n            setTimeout(() => {\n              handleEvents(event.type, event.data);\n            }, correction);\n          };\n        }\n\n        // Audio context\n        let audioContext = null;\n        const audioBuffers = {};\n\n        // Loaded player GLB model\n        let playerModelTemplate = null;\n        let playerModelAnimations = null; // Store animations from GLB\n        // Use asset IDs for models (uploaded assets for bandwidth savings)\n        const STEVE_MODEL_ID = 'steve_model';\n        const ALEX_MODEL_ID = 'alex_model';\n\n        // Player gender/model state\n        let playerGender = 'male'; // 'male' = Steve, 'female' = Alex\n        let alexModelTemplate = null;\n        let alexModelAnimations = null;\n\n        // Loaded zombie GLB model\n        let zombieModelTemplate = null;\n        let zombieModelAnimations = null; // Store animations from GLB\n        const ZOMBIE_MODEL_ID = 'zombie_model';\n\n        // Loaded villager GLB model\n        let villagerModelTemplate = null;\n        let villagerModelAnimations = null;\n        const VILLAGER_MODEL_ID = 'villager_model';\n\n        // Loaded TV GLB model (retro TV)\n        let tvModelTemplate = null;\n        const TV_MODEL_ID = 'retro_tv_compressed.glb';\n\n        // Loading progress tracking\n        let loadingProgress = 0;\n        const loadingScreen = document.getElementById('loading-screen');\n\n        // Make sure loading screen is visible and reset\n        if (loadingScreen) {\n          loadingScreen.classList.remove('fade-out');\n          loadingScreen.style.display = 'flex';\n          loadingScreen.style.opacity = '1';\n          loadingScreen.style.pointerEvents = 'auto';\n        }\n\n        // Initialize loading display immediately with i18n\n        const fillEl = document.getElementById('loading-bar-fill');\n        const percentEl = document.getElementById('loading-percentage');\n        const textEl = document.getElementById('loading-text');\n        const titleEl = document.getElementById('loading-title');\n        if (fillEl) fillEl.style.width = '0%';\n        if (percentEl) percentEl.textContent = '0%';\n        if (textEl) textEl.textContent = t('starting');\n        if (titleEl) titleEl.textContent = t('gameTitle');\n\n        // Apply RTL direction for Arabic/Urdu\n        if (isRTL()) {\n          if (textEl) textEl.style.direction = 'rtl';\n          if (titleEl) titleEl.style.direction = 'rtl';\n        }\n\n        function updateLoadingProgress(progress, textKey) {\n          loadingProgress = progress;\n          const fillEl = document.getElementById('loading-bar-fill');\n          const percentEl = document.getElementById('loading-percentage');\n          const textEl = document.getElementById('loading-text');\n\n          if (fillEl) fillEl.style.width = progress + '%';\n          if (percentEl) percentEl.textContent = Math.round(progress) + '%';\n          if (textEl && textKey) textEl.textContent = t(textKey);\n        }\n\n        function hideLoadingScreen() {\n          loadingScreen.classList.add('fade-out');\n          setTimeout(() => {\n            if (loadingScreen.parentNode) {\n              loadingScreen.parentNode.removeChild(loadingScreen);\n            }\n          }, 500);\n        }\n\n        // Preload assets\n        async function preloadAssets() {\n          // Start loading immediately\n          updateLoadingProgress(5, 'initializing');\n\n          // Load and display pickaxe for loading screen\n          const pickaxeAsset = lib.getAsset('loading_pickaxe');\n          if (pickaxeAsset && pickaxeAsset.url) {\n            const pickaxeImg = document.getElementById('loading-pickaxe');\n            if (pickaxeImg) {\n              pickaxeImg.src = pickaxeAsset.url;\n              pickaxeImg.style.display = 'block';\n            }\n          }\n\n          const imageAssets = [\n            'grass_block_top',\n            'grass_block_side',\n            'dirt_block',\n            'stone_block',\n            'wood_block_top',\n            'wood_block_side',\n            'leaves_block',\n            'dynamite_block',\n            'sand_block',\n            'water_block',\n            'hotbar_slot_bg',\n            'hotbar_slot_selected',\n            'zombie_texture',\n            'creeper_texture',\n            'zombie_head_top',\n            'zombie_head_side',\n            'zombie_head_front',\n            'zombie_torso_top',\n            'zombie_torso_side',\n            'zombie_torso_front',\n            'zombie_arm_top',\n            'zombie_arm_side',\n            'zombie_arm_front',\n            'zombie_arm_bottom',\n            'zombie_leg_top',\n            'zombie_leg_side',\n            'zombie_leg_front',\n            'steve_skin',\n            'wooden_pickaxe',\n            'wooden_axe',\n            'wooden_sword',\n            'crafting_table_top',\n            'crafting_table_side',\n            'planks_block',\n            'stick_item',\n            'coal_item',\n            'torch_block',\n            'coal_ore_block',\n            'iron_ore_block',\n            'iron_ingot',\n            'gold_ore_block',\n            'gold_ingot',\n            'diamond_ore_block',\n            'diamond_item',\n            'stone_pickaxe',\n            'stone_axe',\n            'stone_sword',\n            'wooden_shovel',\n            'stone_shovel',\n            'iron_pickaxe',\n            'iron_axe',\n            'iron_sword',\n            'iron_shovel',\n            'gold_pickaxe',\n            'gold_axe',\n            'gold_sword',\n            'gold_shovel',\n            'diamond_pickaxe',\n            'diamond_axe',\n            'diamond_sword',\n            'diamond_shovel',\n            'oak_sign',\n            'chest_front',\n            'chest_side',\n            'chest_top',\n            'furnace_front_off',\n            'furnace_front_on',\n            'furnace_side',\n            'furnace_top',\n            'bedrock_block',\n            'glass_block',\n            'raw_beef',\n            'cooked_beef',\n            'apple_item',\n            'bread_item',\n            'chicken_texture',\n            'raw_chicken',\n            'cooked_chicken',\n            'cow_texture',\n            'bucket_empty',\n            'bucket_water',\n            'bucket_lava',\n            'lava_block',\n            'door_block',\n            'soil_block',\n            'rice_plant',\n            'bed_block',\n            'flower_block',\n            'tv_block',\n          ];\n\n          const audioAssets = [\n            'block_break_sound',\n            'block_place_sound',\n            'ambient_day_music',\n            'ambient_night_music',\n            'zombie_groan_sound',\n            'creeper_hiss_sound',\n            'explosion_sound',\n            'player_hurt_sound',\n            'villager_sound',\n            'villager_hurt_sound',\n            'dog_hurt_sound',\n          ];\n\n          updateLoadingProgress(10, 'loadingAssets');\n\n          // Load images and models concurrently\n          let loadedCount = 0;\n          const totalImageAssets = imageAssets.length;\n          const totalModelAssets = 5; // Steve, Alex, Zombie, Villager, TV\n          const totalLoadItems = totalImageAssets + totalModelAssets;\n\n          function incrementProgress() {\n            loadedCount++;\n            updateLoadingProgress(10 + (loadedCount / totalLoadItems) * 40, 'loadingAssets');\n          }\n\n          const imagePromises = imageAssets.map((id) => {\n            return new Promise((resolve) => {\n              const assetInfo = lib.getAsset(id);\n              if (assetInfo && assetInfo.url) {\n                const img = new Image();\n                img.crossOrigin = 'anonymous';\n                img.onload = () => {\n                  assetCache.images[id] = img;\n                  incrementProgress();\n                  resolve();\n                };\n                img.onerror = () => {\n                  lib.log(`Failed to load image: ${id}`);\n                  incrementProgress();\n                  resolve();\n                };\n                img.src = assetInfo.url;\n              } else {\n                incrementProgress();\n                resolve();\n              }\n            });\n          });\n\n          function makeGLBLoader() {\n            const dracoLoader = new THREE.DRACOLoader();\n            dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/libs/draco/');\n            const gltfLoader = new THREE.GLTFLoader();\n            gltfLoader.setDRACOLoader(dracoLoader);\n            return gltfLoader;\n          }\n\n          const modelPromises = [\n            // Steve\n            new Promise((resolve) => {\n              try {\n                const asset = lib.getAsset(STEVE_MODEL_ID);\n                if (!asset) { incrementProgress(); resolve(); return; }\n                makeGLBLoader().load(asset.url, (gltf) => {\n                  playerModelTemplate = new THREE.Group();\n                  gltf.scene.rotation.y = Math.PI;\n                  playerModelTemplate.add(gltf.scene);\n                  playerModelAnimations = gltf.animations;\n                  playerModelTemplate.scale.set(0.6, 0.6, 0.6);\n                  playerModelTemplate.traverse((c) => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });\n                  incrementProgress();\n                  resolve();\n                }, null, (e) => { lib.log('Steve load failed: ' + e.message); incrementProgress(); resolve(); });\n              } catch(e) { lib.log('Steve setup error: ' + e.message); incrementProgress(); resolve(); }\n            }),\n            // Alex\n            new Promise((resolve) => {\n              try {\n                const asset = lib.getAsset(ALEX_MODEL_ID);\n                if (!asset) { incrementProgress(); resolve(); return; }\n                makeGLBLoader().load(asset.url, (gltf) => {\n                  alexModelTemplate = new THREE.Group();\n                  gltf.scene.rotation.y = Math.PI;\n                  alexModelTemplate.add(gltf.scene);\n                  alexModelAnimations = gltf.animations;\n                  alexModelTemplate.scale.set(0.6, 0.6, 0.6);\n                  alexModelTemplate.traverse((c) => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });\n                  incrementProgress();\n                  resolve();\n                }, null, (e) => { lib.log('Alex load failed: ' + e.message); incrementProgress(); resolve(); });\n              } catch(e) { lib.log('Alex setup error: ' + e.message); incrementProgress(); resolve(); }\n            }),\n            // Zombie\n            new Promise((resolve) => {\n              try {\n                const asset = lib.getAsset(ZOMBIE_MODEL_ID);\n                if (!asset) { incrementProgress(); resolve(); return; }\n                makeGLBLoader().load(asset.url, (gltf) => {\n                  zombieModelTemplate = new THREE.Group();\n                  zombieModelTemplate.add(gltf.scene);\n                  zombieModelAnimations = gltf.animations;\n                  zombieModelTemplate.scale.set(0.6, 0.6, 0.6);\n                  zombieModelTemplate.traverse((c) => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });\n                  incrementProgress();\n                  resolve();\n                }, null, (e) => { lib.log('Zombie load failed: ' + e.message); incrementProgress(); resolve(); });\n              } catch(e) { lib.log('Zombie setup error: ' + e.message); incrementProgress(); resolve(); }\n            }),\n            // Villager\n            new Promise((resolve) => {\n              try {\n                const asset = lib.getAsset(VILLAGER_MODEL_ID);\n                if (!asset) { incrementProgress(); resolve(); return; }\n                makeGLBLoader().load(asset.url, (gltf) => {\n                  villagerModelTemplate = new THREE.Group();\n                  villagerModelTemplate.add(gltf.scene);\n                  villagerModelAnimations = gltf.animations;\n                  villagerModelTemplate.scale.set(0.6, 0.6, 0.6);\n                  villagerModelTemplate.traverse((c) => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });\n                  incrementProgress();\n                  resolve();\n                }, null, (e) => { lib.log('Villager load failed: ' + e.message); incrementProgress(); resolve(); });\n              } catch(e) { lib.log('Villager setup error: ' + e.message); incrementProgress(); resolve(); }\n            }),\n            // TV\n            new Promise((resolve) => {\n              try {\n                const asset = lib.getAsset(TV_MODEL_ID);\n                if (!asset) { incrementProgress(); resolve(); return; }\n                makeGLBLoader().load(asset.url, (gltf) => {\n                  tvModelTemplate = new THREE.Group();\n                  gltf.scene.rotation.y = -Math.PI/2;\n                  gltf.scene.position.set(-0.1, -0.2, -0.025);\n                  tvModelTemplate.add(gltf.scene);\n                  tvModelTemplate.scale.set(2, 2, 2);\n                  tvModelTemplate.traverse((c) => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; if (c.material) c.material = c.material.clone(); } });\n                  incrementProgress();\n                  resolve();\n                }, null, (e) => { lib.log('TV load failed: ' + e.message); incrementProgress(); resolve(); });\n              } catch(e) { lib.log('TV setup error: ' + e.message); incrementProgress(); resolve(); }\n            }),\n          ];\n\n          await Promise.all([...imagePromises, ...modelPromises]);\n\n          updateLoadingProgress(50, 'loadingAssets');\n\n          // Initialize audio context on user interaction\n          const initAudio = async () => {\n            if (audioContext) return;\n\n            audioContext = new (window.AudioContext || window.webkitAudioContext)();\n\n            await Promise.all(audioAssets.map(async (id) => {\n              const assetInfo = lib.getAsset(id);\n              if (assetInfo && assetInfo.url) {\n                try {\n                  const response = await fetch(assetInfo.url);\n                  const arrayBuffer = await response.arrayBuffer();\n                  audioBuffers[id] = await audioContext.decodeAudioData(arrayBuffer);\n                } catch (e) {\n                  lib.log(`Failed to load audio: ${id}`);\n                }\n              }\n            }));\n          };\n          // Add click listener to init audio\n          const startAudio = async () => {\n            await initAudio();\n            // Start background music in play mode\n            if (mode === 'play' && !currentAmbientMusic) {\n              const musicId = isDay ? 'ambient_day_music' : 'ambient_night_music';\n              currentAmbientMusic = playSound(musicId, true, 0.3);\n            }\n          };\n          document.addEventListener('click', startAudio, {\n            once: true,\n          });\n          document.addEventListener('touchstart', startAudio, {\n            once: true,\n          });\n        }\n\n        function playSound(id, loop = false, volume = 0.5) {\n          if (!audioContext || !audioBuffers[id]) return null;\n\n          const source = audioContext.createBufferSource();\n          const gainNode = audioContext.createGain();\n\n          source.buffer = audioBuffers[id];\n          source.loop = loop;\n          gainNode.gain.value = volume;\n\n          source.connect(gainNode);\n          gainNode.connect(audioContext.destination);\n          source.start(0);\n\n          return {\n            source,\n            gainNode,\n          };\n        }\n\n        // Play sound with volume based on distance from player\n        function playSoundAtPosition(id, x, y, z, baseVolume = 0.5, maxDistance = 30) {\n          if (!audioContext || !audioBuffers[id]) return null;\n\n          // Calculate distance from player\n          const dx = x - playerPos.x;\n          const dy = y - playerPos.y;\n          const dz = z - playerPos.z;\n          const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);\n\n          // Calculate volume based on distance (inverse falloff)\n          // At distance 0: full volume, at maxDistance: near zero\n          let volume = baseVolume * Math.max(0, 1 - distance / maxDistance);\n\n          // Apply slight exponential falloff for more realistic sound\n          volume = volume * volume;\n\n          // Don't play if too quiet\n          if (volume < 0.01) return null;\n\n          return playSound(id, false, volume);\n        }\n\n        // Three.js setup\n        const scene = new THREE.Scene();\n        const camera = new THREE.PerspectiveCamera(\n          75,\n          canvas.clientWidth / canvas.clientHeight,\n          0.1,\n          1000\n        );\n        const renderer = new THREE.WebGLRenderer({\n          canvas,\n          antialias: true,\n        });\n\n        // Debug UI\n        const debugUI = document.createElement('div');\n        debugUI.id = 'debug-ui';\n        debugUI.style.cssText = `\n                    position: absolute;\n                    top: 10px;\n                    left: 10px;\n                    background: rgba(0, 0, 0, 0.7);\n                    color: #00ff00;\n                    font-family: monospace;\n                    font-size: 14px;\n                    padding: 10px;\n                    border-radius: 4px;\n                    pointer-events: none;\n                    display: none;\n                    z-index: 1000;\n                    line-height: 1.6;\n                `;\n        debugUI.innerHTML = 'Loading debug...';\n        gameWorld.appendChild(debugUI);\n\n        let debugEnabled = false;\n\n        function resizeRenderer() {\n          const width = gameWorld.clientWidth;\n          const height = gameWorld.clientHeight;\n          renderer.setSize(width, height);\n          camera.aspect = width / height;\n          camera.updateProjectionMatrix();\n        }\n        resizeRenderer();\n        window.addEventListener('resize', resizeRenderer);\n\n        // Lighting\n        const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);\n        scene.add(ambientLight);\n\n        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);\n        directionalLight.position.set(50, 100, 50);\n        scene.add(directionalLight);\n\n        // Sky\n        scene.background = new THREE.Color(0x87ceeb);\n\n        // Fog - lighter for obby to see platforms clearly\n        const fogColor = new THREE.Color(0x87ceeb);\n        const fogDensity = 0.005; // Light fog so platforms are visible at distance\n        scene.fog = new THREE.FogExp2(fogColor, fogDensity);\n\n        // Skybox with sun and moon\n        const skyboxGroup = new THREE.Group();\n        scene.add(skyboxGroup);\n\n        // Create square sun\n        const sunGeometry = new THREE.PlaneGeometry(8, 8);\n        const sunMaterial = new THREE.MeshBasicMaterial({\n          color: 0xffd700,\n          emissive: 0xffd700,\n          emissiveIntensity: 1.0,\n          side: THREE.DoubleSide,\n          fog: false,\n        });\n        const sun = new THREE.Mesh(sunGeometry, sunMaterial);\n        sun.position.set(0, 0, -100);\n        skyboxGroup.add(sun);\n\n        // Create square moon\n        const moonGeometry = new THREE.PlaneGeometry(6, 6);\n        const moonMaterial = new THREE.MeshBasicMaterial({\n          color: 0xe0e0e0,\n          emissive: 0x808080,\n          emissiveIntensity: 0.8,\n          side: THREE.DoubleSide,\n          fog: false,\n        });\n        const moon = new THREE.Mesh(moonGeometry, moonMaterial);\n        moon.position.set(0, 0, -100);\n        skyboxGroup.add(moon);\n\n        // Add stars for night sky - distributed in a sphere\n        const starsGeometry = new THREE.BufferGeometry();\n        const starPositions = [];\n        const starDistance = 95;\n        // Distance from camera\n        for (let i = 0; i < 300; i++) {\n          // Generate random points on a sphere using spherical coordinates\n          const theta = Math.random() * Math.PI * 2;\n          // Azimuthal angle\n          const phi = Math.acos(2 * Math.random() - 1);\n          // Polar angle\n\n          const x = starDistance * Math.sin(phi) * Math.cos(theta);\n          const y = starDistance * Math.sin(phi) * Math.sin(theta);\n          const z = starDistance * Math.cos(phi);\n          starPositions.push(x, y, z);\n        }\n        starsGeometry.setAttribute(\n          'position',\n          new THREE.BufferAttribute(new Float32Array(starPositions), 3)\n        );\n        const starsMaterial = new THREE.PointsMaterial({\n          color: 0xffffff,\n          size: 1.0,\n          sizeAttenuation: true,\n          fog: false,\n        });\n        const stars = new THREE.Points(starsGeometry, starsMaterial);\n        skyboxGroup.add(stars);\n\n        // ===== INFINITE WORLD SYSTEM =====\n        // World is generated procedurally on-demand using seed + coordinates\n        // No fixed boundaries - explore infinitely in any direction!\n\n        const worldHeight = config.worldHeight || 64; // Height is still limited\n        const TERRAIN_CHUNK_SIZE = 16; // Size of terrain data chunks\n\n        // Terrain data columns - stores generated columns\n        const columnCache = new Map(); // key: \"x,z\" -> Uint8Array\n\n        // Track modified blocks (blocks that differ from generated terrain)\n        const modifiedBlocks = new Map();\n\n        // Track block visibility timestamps for buffered deletion\n        const blockLastSeenTime = new Map(); // key: \"x,y,z\" -> timestamp\n        // key: \"x,y,z\", value: blockType\n\n        // Loaded chunk tracking for cleanup\n        const MAX_LOADED_COLUMNS = 10000; // Maximum terrain columns to keep in memory\n        let lastChunkCleanup = 0;\n        const CHUNK_CLEANUP_INTERVAL = 5000;\n\n        // Simple noise function - deterministic based on coordinates and seed\n        function noise2D(x, z, seed) {\n          const n = Math.sin(x * 12.9898 + z * 78.233 + seed) * 43758.5453;\n          return n - Math.floor(n);\n        }\n\n        function smoothNoise(x, z, seed, scale) {\n          const x0 = Math.floor(x / scale) * scale;\n          const z0 = Math.floor(z / scale) * scale;\n          const x1 = x0 + scale;\n          const z1 = z0 + scale;\n\n          const fx = (x - x0) / scale;\n          const fz = (z - z0) / scale;\n\n          const v00 = noise2D(x0, z0, seed);\n          const v10 = noise2D(x1, z0, seed);\n          const v01 = noise2D(x0, z1, seed);\n          const v11 = noise2D(x1, z1, seed);\n\n          const i0 = v00 * (1 - fx) + v10 * fx;\n          const i1 = v01 * (1 - fx) + v11 * fx;\n\n          return i0 * (1 - fz) + i1 * fz;\n        }\n\n        // Get terrain height at any world coordinate - deterministic\n        function getTerrainHeight(x, z) {\n          const seed = config.worldSeed;\n          const baseHeight = 7;\n          const heightVariation = 5;\n\n          const n1 = smoothNoise(x, z, seed, 16) * heightVariation;\n          const n2 = smoothNoise(x, z, seed + 1000, 8) * (heightVariation / 2);\n          return Math.floor(baseHeight + n1 + n2);\n        }\n\n        // Check if a flower should exist at this position - deterministic\n        // Fewer flowers that spawn in small clusters\n        function hasFlowerAt(x, z) {\n          const seed = config.worldSeed;\n          const clusterDensity = config.flowerClusterDensity || 0.18;\n          const clusterSize = config.flowerClusterSize || 6;\n          const flowerDensity = config.flowerDensity || 0.015;\n\n          // Determine if this area has a flower cluster center\n          const clusterSeed = noise2D(Math.floor(x / clusterSize), Math.floor(z / clusterSize), seed + 9002);\n          if (clusterSeed >= clusterDensity) return false;\n\n          // Within a cluster, only some spots get flowers\n          return noise2D(x, z, seed + 9001) < flowerDensity;\n        }\n\n        // Check if a tree should exist at this position - deterministic\n        function hasTreeAt(x, z) {\n          const seed = config.worldSeed;\n          const treeDensity = config.treeDensity || 0.02;\n          return noise2D(x, z, seed + 5000) < treeDensity;\n        }\n\n        // Get tree trunk height at position - deterministic\n        function getTreeHeight(x, z) {\n          const seed = config.worldSeed;\n          return 4 + Math.floor(noise2D(x, z, seed + 6000) * 3);\n        }\n\n        // Village data cache - villages are deterministic so we can cache them\n        const villageDataCache = new Map(); // key: \"regionX,regionZ\" -> village data or null\n        const VILLAGE_CACHE_MAX_SIZE = 50;\n\n        // Get deterministic village data for a region (with caching)\n        function getVillageData(regionX, regionZ) {\n          if (!config.villagesEnabled) return null;\n\n          const cacheKey = `${regionX},${regionZ}`;\n          if (villageDataCache.has(cacheKey)) {\n            return villageDataCache.get(cacheKey);\n          }\n\n          if (villageDataCache.size >= VILLAGE_CACHE_MAX_SIZE) {\n            const firstKey = villageDataCache.keys().next().value;\n            villageDataCache.delete(firstKey);\n          }\n\n          const seed = config.worldSeed;\n          // Determine if there is a village in this region (1 in 4 chance)\n          const hasVillage = noise2D(regionX * 10, regionZ * 10, seed + 20000) < 0.25;\n\n          if (!hasVillage) {\n            villageDataCache.set(cacheKey, null);\n            return null;\n          }\n\n          // Village center\n          const cx = regionX * 128 + 64 + Math.floor(noise2D(regionX, regionZ, seed + 21000) * 64 - 32);\n          const cz = regionZ * 128 + 64 + Math.floor(noise2D(regionX, regionZ, seed + 22000) * 64 - 32);\n\n          const vData = { cx, cz, radius: 25, structures: [] };\n\n          // Add well in center\n          vData.structures.push({ type: 'well', x: cx, z: cz });\n\n          // Add houses around center\n          for (let i = 0; i < 6; i++) {\n            const angle = noise2D(regionX + i, regionZ, seed + 23000) * Math.PI * 2;\n            const dist = 8 + noise2D(regionX, regionZ + i, seed + 24000) * 12;\n            const hx = Math.floor(cx + Math.cos(angle) * dist);\n            const hz = Math.floor(cz + Math.sin(angle) * dist);\n            vData.structures.push({ type: 'house', x: hx, z: hz, radius: 5 });\n          }\n\n          // Add farms\n          for (let i = 0; i < 3; i++) {\n            const angle = noise2D(regionX, regionZ + i, seed + 25000) * Math.PI * 2;\n            const dist = 15 + noise2D(regionX + i, regionZ, seed + 26000) * 8;\n            const fx = Math.floor(cx + Math.cos(angle) * dist);\n            const fz = Math.floor(cz + Math.sin(angle) * dist);\n            vData.structures.push({ type: 'farm', x: fx, z: fz, radius: 5 });\n          }\n\n          villageDataCache.set(cacheKey, vData);\n          return vData;\n        }\n\n        function getVillageBlock(x, y, z, currentSurfaceHeight) {\n          if (!config.villagesEnabled) return null;\n\n          const regionX = Math.floor(x / 128);\n          const regionZ = Math.floor(z / 128);\n\n          for (let drx = -1; drx <= 1; drx++) {\n            for (let drz = -1; drz <= 1; drz++) {\n              const vData = getVillageData(regionX + drx, regionZ + drz);\n              if (!vData) continue;\n\n              // Early out: skip if block is far from village center (optimization)\n              const distToCenterX = Math.abs(x - vData.cx);\n              const distToCenterZ = Math.abs(z - vData.cz);\n              if (distToCenterX > vData.radius && distToCenterZ > vData.radius) continue;\n\n              for (const struct of vData.structures) {\n                // Early out: skip structures too far from this block\n                const distToStructX = Math.abs(x - struct.x);\n                const distToStructZ = Math.abs(z - struct.z);\n                if (\n                  distToStructX > (struct.radius || 5) + 2 ||\n                  distToStructZ > (struct.radius || 5) + 2\n                )\n                  continue;\n                const baseHeight = getTerrainHeight(struct.x, struct.z);\n\n                if (struct.type === 'well') {\n                  if (Math.abs(x - struct.x) <= 2 && Math.abs(z - struct.z) <= 2) {\n                    if (y < baseHeight) return BLOCK_TYPES.STONE; // foundation\n                    if (y === baseHeight) {\n                      if (Math.abs(x - struct.x) <= 1 && Math.abs(z - struct.z) <= 1) {\n                        if (x === struct.x && z === struct.z) return BLOCK_TYPES.WATER;\n                        return BLOCK_TYPES.STONE;\n                      }\n                      return BLOCK_TYPES.STONE; // larger base\n                    }\n                    if (y === baseHeight + 1 || y === baseHeight + 2) {\n                      if (Math.abs(x - struct.x) === 2 && Math.abs(z - struct.z) === 2)\n                        return BLOCK_TYPES.PLANKS; // pillars\n                      return BLOCK_TYPES.AIR;\n                    }\n                    if (y === baseHeight + 3) {\n                      if (Math.abs(x - struct.x) <= 2 && Math.abs(z - struct.z) <= 2)\n                        return BLOCK_TYPES.WOOD; // roof\n                    }\n                    if (y > baseHeight + 3) {\n                      return BLOCK_TYPES.AIR; // clear space above well\n                    }\n                  }\n                } else if (struct.type === 'house') {\n                  if (\n                    x >= struct.x - 2 &&\n                    x <= struct.x + 2 &&\n                    z >= struct.z - 2 &&\n                    z <= struct.z + 2\n                  ) {\n                    if (y < baseHeight) return BLOCK_TYPES.STONE; // foundation\n                    if (y === baseHeight) return BLOCK_TYPES.PLANKS; // floor\n\n                    if (y > baseHeight && y <= baseHeight + 3) {\n                      const isWall =\n                        x === struct.x - 2 ||\n                        x === struct.x + 2 ||\n                        z === struct.z - 2 ||\n                        z === struct.z + 2;\n                      if (isWall) {\n                        // Door (actual door block)\n                        if (\n                          x === struct.x &&\n                          z === struct.z + 2 &&\n                          (y === baseHeight + 1 || y === baseHeight + 2)\n                        ) {\n                          return BLOCK_TYPES.DOOR;\n                        }\n                        // Windows\n                        if (y === baseHeight + 2 && (x === struct.x || z === struct.z)) {\n                          return BLOCK_TYPES.GLASS;\n                        }\n                        return BLOCK_TYPES.STONE; // walls\n                      }\n                      // Bed in the back corner of the house (away from door)\n                      if (y === baseHeight + 1 && x === struct.x - 1 && z === struct.z - 1) {\n                        return BLOCK_TYPES.BED;\n                      }\n                      // Crate for TV\n                      if (y === baseHeight + 1 && x === struct.x + 1 && z === struct.z - 1) {\n                        return BLOCK_TYPES.WOOD;\n                      }\n                      // TV on top of crate\n                      if (y === baseHeight + 2 && x === struct.x + 1 && z === struct.z - 1) {\n                        return BLOCK_TYPES.TV;\n                      }\n                      return BLOCK_TYPES.AIR; // inside\n                    }\n                    if (y === baseHeight + 4) {\n                      return BLOCK_TYPES.PLANKS; // flat roof\n                    }\n                    if (y > baseHeight + 4) {\n                      return BLOCK_TYPES.AIR; // clear space above house\n                    }\n                  }\n\n                  // Torches outside the house (by the front entrance)\n                  if (y === baseHeight + 1) {\n                    // Torch on left side of door (outside)\n                    if (x === struct.x - 1 && z === struct.z + 3) {\n                      return BLOCK_TYPES.TORCH;\n                    }\n                    // Torch on right side of door (outside)\n                    if (x === struct.x + 1 && z === struct.z + 3) {\n                      return BLOCK_TYPES.TORCH;\n                    }\n                  }\n                } else if (struct.type === 'farm') {\n                  if (\n                    x >= struct.x - 3 &&\n                    x <= struct.x + 3 &&\n                    z >= struct.z - 3 &&\n                    z <= struct.z + 3\n                  ) {\n                    if (y < baseHeight) return BLOCK_TYPES.DIRT;\n                    if (y === baseHeight) {\n                      if (\n                        x === struct.x - 3 ||\n                        x === struct.x + 3 ||\n                        z === struct.z - 3 ||\n                        z === struct.z + 3\n                      ) {\n                        return BLOCK_TYPES.WOOD;\n                      }\n                      if (x === struct.x) return BLOCK_TYPES.WATER;\n                      return BLOCK_TYPES.SOIL;\n                    }\n                    if (y === baseHeight + 1) {\n                      if (\n                        x !== struct.x &&\n                        x > struct.x - 3 &&\n                        x < struct.x + 3 &&\n                        z > struct.z - 3 &&\n                        z < struct.z + 3\n                      ) {\n                        return BLOCK_TYPES.RICE_PLANT;\n                      }\n                    }\n                    if (y > baseHeight + 1) {\n                      return BLOCK_TYPES.AIR; // clear space above farm\n                    }\n                  }\n                }\n              }\n\n              // Check paths (only at surface height and within village radius)\n              if (\n                y === currentSurfaceHeight &&\n                distToCenterX <= vData.radius &&\n                distToCenterZ <= vData.radius\n              ) {\n                for (const struct of vData.structures) {\n                  if (struct.type !== 'well') {\n                    const minX = Math.min(struct.x, vData.cx);\n                    const maxX = Math.max(struct.x, vData.cx);\n                    const minZ = Math.min(struct.z, vData.cz);\n                    const maxZ = Math.max(struct.z, vData.cz);\n\n                    // Thicker paths - only check if within path bounds\n                    if (x >= minX - 1 && x <= maxX + 1 && z >= minZ && z <= maxZ) {\n                      if (Math.abs(x - struct.x) <= 1) {\n                        return BLOCK_TYPES.DIRT; // Dirt path\n                      }\n                    }\n                    if (x >= minX && x <= maxX && z >= minZ - 1 && z <= maxZ + 1) {\n                      if (Math.abs(z - vData.cz) <= 1) {\n                        return BLOCK_TYPES.DIRT; // Dirt path\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n          return null;\n        }\n\n        // ===== OBBY COURSE SYSTEM =====\n        const obbyBlocks = new Map(); // \"x,y,z\" -> blockType\n        const obbyCheckpoints = []; // [{x, y, z}]\n        let currentCheckpointIndex = 0;\n        let obbyGenerated = false;\n        const OBBY_FALL_THRESHOLD = 5; // Y level below which player respawns\n\n        function addObbyRect(cx, cz, w, d, y, blockType) {\n          const x1 = cx - Math.floor(w / 2);\n          const x2 = cx + Math.floor((w - 1) / 2);\n          const z1 = cz - Math.floor(d / 2);\n          const z2 = cz + Math.floor((d - 1) / 2);\n          for (let bx = x1; bx <= x2; bx++) {\n            for (let bz = z1; bz <= z2; bz++) {\n              obbyBlocks.set(`${bx},${y},${bz}`, blockType);\n            }\n          }\n        }\n\n        function addObbyCheckpoint(cx, cz, y) {\n          addObbyRect(cx, cz, 5, 5, y, BLOCK_TYPES.GRASS);\n          // Torch markers on corners\n          obbyBlocks.set(`${cx-2},${y+1},${cz-2}`, BLOCK_TYPES.TORCH);\n          obbyBlocks.set(`${cx+2},${y+1},${cz-2}`, BLOCK_TYPES.TORCH);\n          obbyBlocks.set(`${cx-2},${y+1},${cz+2}`, BLOCK_TYPES.TORCH);\n          obbyBlocks.set(`${cx+2},${y+1},${cz+2}`, BLOCK_TYPES.TORCH);\n          // Gold border to mark checkpoint\n          for (let bx = cx - 2; bx <= cx + 2; bx++) {\n            obbyBlocks.set(`${bx},${y},${cz-2}`, BLOCK_TYPES.PLANKS);\n            obbyBlocks.set(`${bx},${y},${cz+2}`, BLOCK_TYPES.PLANKS);\n          }\n          for (let bz = cz - 2; bz <= cz + 2; bz++) {\n            obbyBlocks.set(`${cx-2},${y},${bz}`, BLOCK_TYPES.PLANKS);\n            obbyBlocks.set(`${cx+2},${y},${bz}`, BLOCK_TYPES.PLANKS);\n          }\n          obbyCheckpoints.push({ x: cx + 0.5, y: y + 1 + PLAYER_HEIGHT, z: cz + 0.5 });\n        }\n\n        function generateObbyLayout() {\n          if (obbyGenerated) return;\n          obbyGenerated = true;\n\n          const STEP2 = 3;\n          const STEP1 = 3;\n\n          let z = 0, y = 25, x = 0;\n\n          // === START PLATFORM (7x7 grass) ===\n          addObbyRect(0, 0, 7, 7, y, BLOCK_TYPES.GRASS);\n          addObbyCheckpoint(0, 0, y); // CP 0 - Start\n          z = 2;\n\n          // === SECTION 1: Easy Forward Jumps (2x2 stone) ===\n          for (let i = 0; i < 8; i++) {\n            z += STEP2;\n            addObbyRect(0, z, 2, 2, y, BLOCK_TYPES.STONE);\n          }\n          z += STEP2;\n          addObbyCheckpoint(0, z, y); // CP 1\n          z += 3;\n\n          // === SECTION 2: Ascending Stairs (3x2 wood, +1 y each) ===\n          for (let i = 0; i < 6; i++) {\n            z += STEP2;\n            y += 1;\n            addObbyRect(0, z, 3, 2, y, BLOCK_TYPES.WOOD);\n          }\n          z += STEP2;\n          addObbyCheckpoint(0, z, y); // CP 2\n          z += 3;\n\n          // === SECTION 3: Single-file bridge (1-wide planks, no gaps) ===\n          for (let i = 0; i < 12; i++) {\n            z += 1;\n            obbyBlocks.set(`${0},${y},${z}`, BLOCK_TYPES.PLANKS);\n          }\n          z += 2;\n          addObbyCheckpoint(0, z, y); // CP 3\n          z += 3;\n\n          // === SECTION 4: Zigzag Jumps (alternate left/right, 2x2) ===\n          for (let i = 0; i < 8; i++) {\n            z += STEP2;\n            x = (i % 2 === 0) ? 2 : -2;\n            addObbyRect(x, z, 2, 2, y, BLOCK_TYPES.STONE);\n          }\n          x = 0;\n          z += STEP2;\n          addObbyRect(0, z, 3, 2, y, BLOCK_TYPES.STONE);\n          z += 3;\n          addObbyCheckpoint(0, z, y); // CP 4\n          z += 3;\n\n          // === SECTION 5: Descending Stairs (3x2 sand, -1 y each) ===\n          for (let i = 0; i < 6; i++) {\n            z += STEP2;\n            y -= 1;\n            addObbyRect(0, z, 3, 2, y, BLOCK_TYPES.SAND);\n          }\n          z += STEP2;\n          addObbyCheckpoint(0, z, y); // CP 5\n          z += 3;\n\n          // === SECTION 6: Single-block precision (1x1 stone) ===\n          for (let i = 0; i < 7; i++) {\n            z += STEP1;\n            const ox = (i % 2 === 0) ? 1 : -1;\n            obbyBlocks.set(`${ox},${y},${z}`, BLOCK_TYPES.STONE);\n          }\n          z += STEP1;\n          addObbyRect(0, z, 2, 2, y, BLOCK_TYPES.PLANKS);\n          z += 3;\n          addObbyCheckpoint(0, z, y); // CP 6\n          z += 3;\n\n          // === SECTION 7: Ascending pillars (2x2 dirt, stagger sides, +1 y each) ===\n          for (let i = 0; i < 8; i++) {\n            z += STEP2;\n            y += 1;\n            const ox = (i % 2 === 0) ? 1 : -1;\n            addObbyRect(ox, z, 2, 2, y, BLOCK_TYPES.DIRT);\n          }\n          z += STEP2;\n          addObbyRect(0, z, 3, 2, y, BLOCK_TYPES.DIRT);\n          z += 3;\n          addObbyCheckpoint(0, z, y); // CP 7\n          z += 3;\n\n          // === SECTION 8: Bridge with regular gaps (plank + gap pattern) ===\n          for (let i = 0; i < 10; i++) {\n            obbyBlocks.set(`${0},${y},${z + 1}`, BLOCK_TYPES.PLANKS);\n            obbyBlocks.set(`${0},${y},${z + 2}`, BLOCK_TYPES.PLANKS);\n            z += STEP2;\n          }\n          z += 2;\n          addObbyCheckpoint(0, z, y); // CP 8\n          z += 3;\n\n          // === SECTION 9: Wave (2x2 glass, alternating up/down by 1) ===\n          for (let i = 0; i < 10; i++) {\n            z += STEP2;\n            y += (i % 2 === 0) ? 1 : -1;\n            addObbyRect(0, z, 2, 2, y, BLOCK_TYPES.GLASS);\n          }\n          z += STEP2;\n          addObbyCheckpoint(0, z, y); // CP 9\n          z += 3;\n\n          // === SECTION 10: Spiral ascent (2x2 wood, +1 y, rotating around center) ===\n          for (let i = 0; i < 12; i++) {\n            z += STEP2;\n            y += 1;\n            const angle = (i / 12) * Math.PI * 2;\n            const ox = Math.round(Math.cos(angle) * 2);\n            addObbyRect(ox, z, 2, 2, y, BLOCK_TYPES.WOOD);\n          }\n          z += STEP2;\n          addObbyRect(0, z, 3, 2, y, BLOCK_TYPES.WOOD);\n          z += 3;\n          addObbyCheckpoint(0, z, y); // CP 10\n          z += 3;\n\n          // === SECTION 11: Diagonal jumps (2x2 stone, alternating diagonals) ===\n          for (let i = 0; i < 8; i++) {\n            z += STEP2;\n            x = (i % 2 === 0) ? 2 : -2;\n            addObbyRect(x, z, 2, 2, y, BLOCK_TYPES.STONE);\n          }\n          x = 0;\n          z += STEP2;\n          addObbyRect(0, z, 3, 2, y, BLOCK_TYPES.STONE);\n          z += 3;\n          addObbyCheckpoint(0, z, y); // CP 11\n          z += 3;\n\n          // === SECTION 12: Wide descending stairs (4x2 planks, -1 y each) ===\n          for (let i = 0; i < 7; i++) {\n            z += STEP2;\n            y -= 1;\n            addObbyRect(0, z, 4, 2, y, BLOCK_TYPES.PLANKS);\n          }\n          z += STEP2;\n          addObbyCheckpoint(0, z, y); // CP 12\n          z += 3;\n\n          // === SECTION 13: Fast run (2x1 dirt platforms, same height) ===\n          for (let i = 0; i < 10; i++) {\n            z += STEP1;\n            const ox = (i % 3 === 0) ? 2 : (i % 3 === 1) ? -2 : 0;\n            addObbyRect(ox, z, 2, 1, y, BLOCK_TYPES.DIRT);\n          }\n          z += STEP2;\n          addObbyRect(0, z, 3, 2, y, BLOCK_TYPES.DIRT);\n          z += 3;\n          addObbyCheckpoint(0, z, y); // CP 13\n          z += 3;\n\n          // === SECTION 14: Rising challenge (2x2 stone, +2 y each) ===\n          for (let i = 0; i < 6; i++) {\n            z += STEP2;\n            y += 2;\n            addObbyRect(0, z, 2, 2, y, BLOCK_TYPES.STONE);\n          }\n          z += STEP2;\n          addObbyCheckpoint(0, z, y); // CP 14\n          z += 3;\n\n          // === SECTION 15: Freefall descent (3x2 sand, -3 y each) ===\n          for (let i = 0; i < 5; i++) {\n            z += STEP2;\n            y -= 3;\n            addObbyRect(0, z, 3, 2, y, BLOCK_TYPES.SAND);\n          }\n          z += STEP2;\n          addObbyCheckpoint(0, z, y); // CP 15\n          z += 3;\n\n          // === SECTION 16: Gauntlet \u2014 single blocks zigzag with small height changes ===\n          for (let i = 0; i < 12; i++) {\n            z += STEP1;\n            const ox = (i % 2 === 0) ? 1 : -1;\n            const dy = (i % 3 === 0) ? 1 : (i % 3 === 1) ? -1 : 0;\n            y += dy;\n            obbyBlocks.set(`${ox},${y},${z}`, BLOCK_TYPES.STONE);\n          }\n          z += STEP2;\n          addObbyRect(0, z, 3, 2, y, BLOCK_TYPES.PLANKS);\n          z += 3;\n          addObbyCheckpoint(0, z, y); // CP 16\n          z += 3;\n\n          // === SECTION 17: Final Climb (2x2 wood, +1 y, straight) ===\n          for (let i = 0; i < 10; i++) {\n            z += STEP2;\n            y += 1;\n            addObbyRect(0, z, 2, 2, y, BLOCK_TYPES.WOOD);\n          }\n          z += STEP2;\n\n          // === VICTORY PLATFORM ===\n          addObbyRect(0, z, 9, 9, y, BLOCK_TYPES.GRASS);\n          for (let py = y + 1; py <= y + 4; py++) {\n            obbyBlocks.set(`${-3},${py},${z - 3}`, BLOCK_TYPES.WOOD);\n            obbyBlocks.set(`${3},${py},${z - 3}`, BLOCK_TYPES.WOOD);\n            obbyBlocks.set(`${-3},${py},${z + 3}`, BLOCK_TYPES.WOOD);\n            obbyBlocks.set(`${3},${py},${z + 3}`, BLOCK_TYPES.WOOD);\n          }\n          obbyBlocks.set(`${-3},${y + 5},${z - 3}`, BLOCK_TYPES.TORCH);\n          obbyBlocks.set(`${3},${y + 5},${z - 3}`, BLOCK_TYPES.TORCH);\n          obbyBlocks.set(`${-3},${y + 5},${z + 3}`, BLOCK_TYPES.TORCH);\n          obbyBlocks.set(`${3},${y + 5},${z + 3}`, BLOCK_TYPES.TORCH);\n          addObbyCheckpoint(0, z, y); // CP 17 - Victory\n\n          lib.log('Obby generated: ' + obbyBlocks.size + ' blocks, ' + obbyCheckpoints.length + ' checkpoints');\n        }\n\n        // Check if player is on/near a checkpoint and activate it\n        function checkObbyCheckpoints() {\n          for (let i = 0; i < obbyCheckpoints.length; i++) {\n            if (i <= currentCheckpointIndex) continue; // Already passed\n            const cp = obbyCheckpoints[i];\n            const dx = playerPos.x - cp.x;\n            const dz = playerPos.z - cp.z;\n            const dy = playerPos.y - cp.y;\n            if (Math.abs(dx) < 3 && Math.abs(dz) < 3 && Math.abs(dy) < 3) {\n              currentCheckpointIndex = i;\n              spawnPoint = new THREE.Vector3(cp.x, cp.y, cp.z);\n              showMessage(`\u2705 Checkpoint ${i}/${obbyCheckpoints.length - 1}`, '#4CAF50', 2000);\n              playSound('block_place_sound', false, 0.6);\n              // Update checkpoint display\n              const cpDisplay = document.getElementById('checkpoint-display');\n              if (cpDisplay) {\n                cpDisplay.textContent = '\ud83c\udfc1 Checkpoint: ' + i + '/' + (obbyCheckpoints.length - 1);\n              }\n              if (i === obbyCheckpoints.length - 1) {\n                showMessage('\ud83c\udfc6 OBBY COMPLETE! Congratulations!', '#FFD700', 5000);\n              }\n            }\n          }\n        }\n\n        // Respawn player at current checkpoint\n        function respawnAtCheckpoint() {\n          const cp = obbyCheckpoints[currentCheckpointIndex];\n          if (cp) {\n            playerPos.set(cp.x, cp.y, cp.z);\n            playerVelocity.set(0, 0, 0);\n            playerYaw = Math.PI;\n            playerPitch = 0;\n            isGrounded = false;\n            wasFalling = false;\n            updateThirdPersonCamera();\n            showMessage('\ud83d\udc80 Fell! Back to checkpoint ' + currentCheckpointIndex, '#FF5722', 1500);\n            playSound('player_hurt_sound', false, 0.4);\n          }\n        }\n\n        // Generate a full column of blocks at once - OBBY MODE\n        function generateColumn(x, z) {\n          const col = new Uint8Array(worldHeight);\n          // Obby mode: only place blocks that are part of the course\n          for (let y = 0; y < worldHeight; y++) {\n            const key = `${x},${y},${z}`;\n            if (obbyBlocks.has(key)) {\n              col[y] = obbyBlocks.get(key);\n            }\n            // Everything else stays AIR (no terrain)\n          }\n          return col;\n        }\n\n        // Get column from cache or generate it\n        function getColumn(x, z) {\n          const key = `${x},${z}`;\n          let col = columnCache.get(key);\n          if (!col) {\n            col = generateColumn(x, z);\n            columnCache.set(key, col);\n          }\n          return col;\n        }\n\n        // Get block at any world coordinate - checks modifications first, then uses cached column\n        function getBlockAt(x, y, z) {\n          if (y < 0 || y >= worldHeight) return BLOCK_TYPES.AIR;\n\n          // Check if block was modified by player\n          const key = `${x},${y},${z}`;\n          if (modifiedBlocks.has(key)) {\n            return modifiedBlocks.get(key);\n          }\n\n          // Get from cached column\n          const col = getColumn(x, z);\n          return col[y];\n        }\n\n        // Set block at any world coordinate\n        function setBlockAt(x, y, z, blockType) {\n          if (y < 0 || y >= worldHeight) return;\n\n          const key = `${x},${y},${z}`;\n          const col = getColumn(x, z);\n          const generatedType = col[y];\n\n          if (blockType === generatedType) {\n            // Block matches generated terrain - remove from modifications\n            modifiedBlocks.delete(key);\n          } else {\n            // Block differs from generated terrain - track modification\n            modifiedBlocks.set(key, blockType);\n          }\n        }\n\n        // WorldData proxy - allows existing code to use worldData[x][y][z] syntax\n        // while actually using the infinite world system\n        const worldData = new Proxy(\n          {},\n          {\n            get: function (target, xStr) {\n              const x = parseInt(xStr);\n              if (isNaN(x)) return undefined;\n\n              return new Proxy(\n                {},\n                {\n                  get: function (target2, yStr) {\n                    const y = parseInt(yStr);\n                    if (isNaN(y)) return undefined;\n\n                    return new Proxy(\n                      {},\n                      {\n                        get: function (target3, zStr) {\n                          const z = parseInt(zStr);\n                          if (isNaN(z)) return undefined;\n                          return getBlockAt(x, y, z);\n                        },\n                        set: function (target3, zStr, value) {\n                          const z = parseInt(zStr);\n                          if (isNaN(z)) return false;\n                          setBlockAt(x, y, z, value);\n                          return true;\n                        },\n                      }\n                    );\n                  },\n                }\n              );\n            },\n          }\n        );\n\n        // For compatibility - these are now unlimited\n        const worldWidth = Infinity;\n        const worldDepth = Infinity;\n\n        // Cleanup old terrain columns to save memory\n        function cleanupTerrainChunks(playerX, playerZ) {\n          if (columnCache.size <= MAX_LOADED_COLUMNS) return;\n\n          const maxDistSq = (config.lodFarDistance + 32) ** 2;\n\n          const columnsToRemove = [];\n          columnCache.forEach((_, key) => {\n            const [x, z] = key.split(',').map(Number);\n            const dx = x - playerX;\n            const dz = z - playerZ;\n            if (dx * dx + dz * dz > maxDistSq) {\n              columnsToRemove.push(key);\n            }\n          });\n\n          columnsToRemove.forEach((key) => columnCache.delete(key));\n        }\n\n        function generateTerrain() {\n          updateLoadingProgress(35, 'initializingWorld');\n          // Generate the obby course layout\n          generateObbyLayout();\n          updateLoadingProgress(55, 'worldReady');\n        }\n\n        // Block textures\n        const blockTextures = {};\n\n        function createBlockTextures() {\n          const textureLoader = new THREE.TextureLoader();\n\n          // Create textures from loaded images\n          const createTexture = (img) => {\n            if (!img) return null;\n            const texture = new THREE.Texture(img);\n            texture.magFilter = THREE.NearestFilter;\n            texture.minFilter = THREE.NearestFilter;\n            texture.needsUpdate = true;\n            return texture;\n          };\n          blockTextures[BLOCK_TYPES.GRASS] = {\n            top: createTexture(assetCache.images['grass_block_top']),\n            side: createTexture(assetCache.images['grass_block_side']),\n            bottom: createTexture(assetCache.images['dirt_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.DIRT] = {\n            all: createTexture(assetCache.images['dirt_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.STONE] = {\n            all: createTexture(assetCache.images['stone_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.WOOD] = {\n            top: createTexture(assetCache.images['wood_block_top']),\n            side: createTexture(assetCache.images['wood_block_side']),\n            bottom: createTexture(assetCache.images['wood_block_top']),\n          };\n\n          blockTextures[BLOCK_TYPES.LEAVES] = {\n            all: createTexture(assetCache.images['leaves_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.DYNAMITE] = {\n            all: createTexture(assetCache.images['dynamite_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.SAND] = {\n            all: createTexture(assetCache.images['sand_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.WATER] = {\n            all: createTexture(assetCache.images['water_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.WATER_FLOW] = {\n            all: createTexture(assetCache.images['water_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.CRAFTING_TABLE] = {\n            top: createTexture(assetCache.images['crafting_table_top']),\n            side: createTexture(assetCache.images['crafting_table_side']),\n            bottom: createTexture(assetCache.images['wood_block_top']),\n          };\n\n          blockTextures[BLOCK_TYPES.PLANKS] = {\n            all: createTexture(assetCache.images['planks_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.TORCH] = {\n            all: createTexture(assetCache.images['torch_block']),\n          };\n\n          // Make torch material transparent\n          if (blockTextures[BLOCK_TYPES.TORCH].all) {\n            blockTextures[BLOCK_TYPES.TORCH].all.transparent = true;\n          }\n\n          blockTextures[BLOCK_TYPES.COAL_ORE] = {\n            all: createTexture(assetCache.images['coal_ore_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.OAK_SIGN] = {\n            all: createTexture(assetCache.images['oak_sign']),\n          };\n\n          // Make sign material transparent\n          if (blockTextures[BLOCK_TYPES.OAK_SIGN].all) {\n            blockTextures[BLOCK_TYPES.OAK_SIGN].all.transparent = true;\n          }\n\n          blockTextures[BLOCK_TYPES.CHEST] = {\n            top: createTexture(assetCache.images['chest_top']),\n            side: createTexture(assetCache.images['chest_side']),\n            bottom: createTexture(assetCache.images['chest_top']),\n            front: createTexture(assetCache.images['chest_front']),\n          };\n\n          blockTextures[BLOCK_TYPES.FURNACE] = {\n            top: createTexture(assetCache.images['furnace_top']),\n            side: createTexture(assetCache.images['furnace_side']),\n            bottom: createTexture(assetCache.images['furnace_top']),\n            front: createTexture(assetCache.images['furnace_front_off']),\n            frontOn: createTexture(assetCache.images['furnace_front_on']),\n          };\n\n          blockTextures[BLOCK_TYPES.BEDROCK] = {\n            all: createTexture(assetCache.images['bedrock_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.GLASS] = {\n            all: createTexture(assetCache.images['glass_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.IRON_ORE] = {\n            all: createTexture(assetCache.images['iron_ore_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.GOLD_ORE] = {\n            all: createTexture(assetCache.images['gold_ore_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.DIAMOND_ORE] = {\n            all: createTexture(assetCache.images['diamond_ore_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.LAVA] = {\n            all: createTexture(assetCache.images['lava_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.LAVA_FLOW] = {\n            all: createTexture(assetCache.images['lava_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.DOOR] = {\n            all: createTexture(assetCache.images['door_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.SOIL] = {\n            all: createTexture(assetCache.images['soil_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.RICE_PLANT] = {\n            all: createTexture(assetCache.images['rice_plant']),\n          };\n\n          blockTextures[BLOCK_TYPES.BED] = {\n            all: createTexture(assetCache.images['bed_block']),\n          };\n\n          // Bed uses custom mesh, make instanced mesh invisible\n          blockTextures[BLOCK_TYPES.BED].useCustomMesh = true;\n\n          blockTextures[BLOCK_TYPES.FLOWER] = {\n            all: createTexture(assetCache.images['flower_block']),\n          };\n\n          blockTextures[BLOCK_TYPES.TV] = {\n            all: createTexture(assetCache.images['tv_block']),\n          };\n\n          // TV uses custom mesh with transparent outer block\n          if (blockTextures[BLOCK_TYPES.TV].all) {\n            blockTextures[BLOCK_TYPES.TV].all.transparent = true;\n          }\n          blockTextures[BLOCK_TYPES.TV].useCustomMesh = true;\n\n          // Make rice plant material transparent\n          if (blockTextures[BLOCK_TYPES.RICE_PLANT].all) {\n            blockTextures[BLOCK_TYPES.RICE_PLANT].all.transparent = true;\n          }\n\n          // Rice plant uses custom sprite mesh, make instanced mesh invisible\n          blockTextures[BLOCK_TYPES.RICE_PLANT].useSpriteMesh = true;\n\n          // Make flower material transparent\n          if (blockTextures[BLOCK_TYPES.FLOWER].all) {\n            blockTextures[BLOCK_TYPES.FLOWER].all.transparent = true;\n          }\n\n          // Flower uses custom sprite mesh, make instanced mesh invisible\n          blockTextures[BLOCK_TYPES.FLOWER].useSpriteMesh = true;\n        }\n\n        // Create block materials\n        function getBlockMaterials(blockType) {\n          const textures = blockTextures[blockType];\n          if (!textures) return null;\n          if (textures.all) {\n            const mat = new THREE.MeshLambertMaterial({\n              map: textures.all,\n              transparent:\n                blockType === BLOCK_TYPES.LEAVES ||\n                blockType === BLOCK_TYPES.WATER ||\n                blockType === BLOCK_TYPES.WATER_FLOW ||\n                blockType === BLOCK_TYPES.LAVA ||\n                blockType === BLOCK_TYPES.LAVA_FLOW ||\n                blockType === BLOCK_TYPES.TORCH ||\n                blockType === BLOCK_TYPES.GLASS ||\n                blockType === BLOCK_TYPES.OAK_SIGN ||\n                blockType === BLOCK_TYPES.DOOR ||\n                blockType === BLOCK_TYPES.RICE_PLANT ||\n                blockType === BLOCK_TYPES.BED ||\n                blockType === BLOCK_TYPES.FLOWER ||\n                blockType === BLOCK_TYPES.TV,\n              alphaTest:\n                blockType === BLOCK_TYPES.LEAVES || blockType === BLOCK_TYPES.GLASS\n                  ? 0\n                  : blockType === BLOCK_TYPES.TORCH ||\n                      blockType === BLOCK_TYPES.OAK_SIGN ||\n                      blockType === BLOCK_TYPES.DOOR ||\n                      blockType === BLOCK_TYPES.RICE_PLANT ||\n                      blockType === BLOCK_TYPES.BED ||\n                      blockType === BLOCK_TYPES.FLOWER ||\n                      blockType === BLOCK_TYPES.TV\n                    ? 0.1\n                    : 0,\n              opacity:\n                blockType === BLOCK_TYPES.WATER || blockType === BLOCK_TYPES.WATER_FLOW\n                  ? 0.45\n                  : blockType === BLOCK_TYPES.LAVA || blockType === BLOCK_TYPES.LAVA_FLOW\n                    ? 0.9\n                    : blockType === BLOCK_TYPES.GLASS\n                      ? 1.0\n                      : blockType === BLOCK_TYPES.TORCH ||\n                          blockType === BLOCK_TYPES.OAK_SIGN ||\n                          blockType === BLOCK_TYPES.DOOR ||\n                          blockType === BLOCK_TYPES.RICE_PLANT ||\n                          blockType === BLOCK_TYPES.BED ||\n                          blockType === BLOCK_TYPES.FLOWER ||\n                          blockType === BLOCK_TYPES.TV\n                        ? 0\n                        : 1.0,\n              emissive:\n                blockType === BLOCK_TYPES.LAVA || blockType === BLOCK_TYPES.LAVA_FLOW\n                  ? new THREE.Color(0xff4400)\n                  : undefined,\n              emissiveIntensity:\n                blockType === BLOCK_TYPES.LAVA || blockType === BLOCK_TYPES.LAVA_FLOW ? 1.0 : 0,\n            });\n            return [mat, mat, mat, mat, mat, mat];\n          }\n\n          // Handle chest with different front texture (make instanced mesh invisible, use custom mesh)\n          if (blockType === BLOCK_TYPES.CHEST && textures.front) {\n            const invisibleMat = new THREE.MeshLambertMaterial({\n              transparent: true,\n              opacity: 0,\n              alphaTest: 0.1,\n            });\n            return [\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n            ];\n          }\n\n          // Handle furnace with different front texture (we make instanced mesh invisible and use custom mesh)\n          if (blockType === BLOCK_TYPES.FURNACE && textures.front) {\n            const invisibleMat = new THREE.MeshLambertMaterial({\n              transparent: true,\n              opacity: 0,\n              alphaTest: 0.1,\n            });\n            return [\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n            ];\n          }\n\n          // Handle rice plant with sprite mesh (make instanced mesh invisible)\n          if (blockType === BLOCK_TYPES.RICE_PLANT && textures.useSpriteMesh) {\n            const invisibleMat = new THREE.MeshLambertMaterial({\n              transparent: true,\n              opacity: 0,\n              alphaTest: 0.1,\n            });\n            return [\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n            ];\n          }\n\n          // Handle bed with custom mesh (make instanced mesh invisible)\n          if (blockType === BLOCK_TYPES.BED && textures.useCustomMesh) {\n            const invisibleMat = new THREE.MeshLambertMaterial({\n              transparent: true,\n              opacity: 0,\n              alphaTest: 0.1,\n            });\n            return [\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n            ];\n          }\n\n          // Handle flower with sprite mesh (make instanced mesh invisible)\n          if (blockType === BLOCK_TYPES.FLOWER && textures.useSpriteMesh) {\n            const invisibleMat = new THREE.MeshLambertMaterial({\n              transparent: true,\n              opacity: 0,\n              alphaTest: 0.1,\n            });\n            return [\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n            ];\n          }\n\n          // Handle TV with custom mesh (make instanced mesh invisible)\n          if (blockType === BLOCK_TYPES.TV && textures.useCustomMesh) {\n            const invisibleMat = new THREE.MeshLambertMaterial({\n              transparent: true,\n              opacity: 0,\n              alphaTest: 0.1,\n            });\n            return [\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n              invisibleMat,\n            ];\n          }\n\n          return [\n            new THREE.MeshLambertMaterial({\n              map: textures.side,\n            }), // right\n            new THREE.MeshLambertMaterial({\n              map: textures.side,\n            }), // left\n            new THREE.MeshLambertMaterial({\n              map: textures.top,\n            }), // top\n            new THREE.MeshLambertMaterial({\n              map: textures.bottom,\n            }), // bottom\n            new THREE.MeshLambertMaterial({\n              map: textures.side,\n            }), // front\n            new THREE.MeshLambertMaterial({\n              map: textures.side,\n            }), // back\n          ];\n        }\n\n        // ===== BATCH BVH LOD RENDERING SYSTEM =====\n        // Optimizations implemented:\n        // 2. Chunk-based BVH - Spatial hierarchy for efficient frustum culling (skip entire chunks)\n        // 3. LOD distance culling - Skip chunks beyond render distance\n        // 4. DDA raycasting - Efficient voxel visibility determination\n\n        const RENDER_DISTANCE = config.renderDistance;\n        const CHUNK_SIZE = 16; // Blocks per chunk dimension\n        const MAX_INSTANCES_PER_TYPE = 50000; // Max instances per block type\n\n        // LOD configuration\n        const LOD_ENABLED = config.lodEnabled !== false;\n        const LOD_NEAR_DISTANCE = config.lodNearDistance || 80; // Full detail\n        const LOD_MEDIUM_DISTANCE = config.lodMediumDistance || 160; // Reduced detail\n        const LOD_FAR_DISTANCE = config.lodFarDistance || 320; // Simplified chunks\n\n        // LOD levels: 0 = full detail, 1 = medium (skip some blocks), 2 = far (chunk mesh only)\n        const LOD_FULL = 0;\n        const LOD_MEDIUM = 1;\n        const LOD_FAR = 2;\n\n        // Chunk-based BVH structure\n        const chunks = new Map(); // key: \"cx,cy,cz\", value: { blocks, dirty, boundingBox }\n        const lodChunkMeshes = new Map(); // key: \"cx,cz\", value: { mesh, lodLevel, lastUpdate }\n        const chunkFrustumCache = new Map(); // Cache frustum test results per frame\n\n        // InstancedMesh for each block type (batched rendering)\n        const instancedMeshes = new Map(); // blockType -> { mesh, matrices, count, blockPositions }\n        const blockMeshes = new Map(); // For raycasting compatibility: \"x,y,z\" -> { blockType, x, y, z }\n\n        // Shared geometry for all blocks\n        const sharedBlockGeometry = new THREE.BoxGeometry(1, 1, 1);\n\n        // Debug state\n        let iteratedBlocks = 0;\n        let visibleMeshes = 0;\n        let visibleChunks = 0;\n        let culledChunks = 0;\n        let lodFullChunks = 0;\n        let lodMediumChunks = 0;\n        let lodFarChunks = 0;\n        let lastFrameTime = performance.now();\n        let fps = 60;\n        let frameCount = 0;\n        let fpsUpdateTime = 0;\n        let renderTime = 0;\n        let batchedDrawCalls = 0;\n\n        // Frustum for culling\n        const frustum = new THREE.Frustum();\n        const frustumMatrix = new THREE.Matrix4();\n\n        // Reusable matrix for instance updates\n        const tempMatrix = new THREE.Matrix4();\n        const tempPosition = new THREE.Vector3();\n        const tempQuaternion = new THREE.Quaternion();\n        const tempScale = new THREE.Vector3(1, 1, 1);\n\n        // Initialize instanced meshes for each block type\n        function initializeInstancedMeshes() {\n          const blockTypes = [\n            BLOCK_TYPES.GRASS,\n            BLOCK_TYPES.DIRT,\n            BLOCK_TYPES.STONE,\n            BLOCK_TYPES.WOOD,\n            BLOCK_TYPES.LEAVES,\n            BLOCK_TYPES.DYNAMITE,\n            BLOCK_TYPES.SAND,\n            BLOCK_TYPES.WATER,\n            BLOCK_TYPES.WATER_FLOW,\n            BLOCK_TYPES.CRAFTING_TABLE,\n            BLOCK_TYPES.PLANKS,\n            BLOCK_TYPES.TORCH,\n            BLOCK_TYPES.COAL_ORE,\n            BLOCK_TYPES.OAK_SIGN,\n            BLOCK_TYPES.CHEST,\n            BLOCK_TYPES.FURNACE,\n            BLOCK_TYPES.BEDROCK,\n            BLOCK_TYPES.GLASS,\n            BLOCK_TYPES.IRON_ORE,\n            BLOCK_TYPES.GOLD_ORE,\n            BLOCK_TYPES.DIAMOND_ORE,\n            BLOCK_TYPES.LAVA,\n            BLOCK_TYPES.LAVA_FLOW,\n            BLOCK_TYPES.DOOR,\n            BLOCK_TYPES.SOIL,\n            BLOCK_TYPES.RICE_PLANT,\n            BLOCK_TYPES.BED,\n            BLOCK_TYPES.FLOWER,\n            BLOCK_TYPES.TV,\n          ];\n\n          for (const blockType of blockTypes) {\n            const materials = getBlockMaterials(blockType);\n            if (!materials) continue;\n\n            const instancedMesh = new THREE.InstancedMesh(\n              sharedBlockGeometry,\n              materials,\n              MAX_INSTANCES_PER_TYPE\n            );\n            instancedMesh.count = 0; // Start with 0 visible instances\n            instancedMesh.frustumCulled = false; // We handle culling ourselves\n            instancedMesh.userData.blockType = blockType;\n\n            instancedMeshes.set(blockType, {\n              mesh: instancedMesh,\n              blockPositions: new Map(), // \"x,y,z\" -> instanceIndex\n              dirtyInstances: new Set(), // Indices that need matrix updates\n              count: 0,\n            });\n\n            scene.add(instancedMesh);\n          }\n        }\n\n        // Get chunk key from world position\n        function getChunkKey(x, y, z) {\n          const cx = Math.floor(x / CHUNK_SIZE);\n          const cy = Math.floor(y / CHUNK_SIZE);\n          const cz = Math.floor(z / CHUNK_SIZE);\n          return `${cx},${cy},${cz}`;\n        }\n\n        // Get or create chunk\n        function getOrCreateChunk(cx, cy, cz) {\n          const key = `${cx},${cy},${cz}`;\n          if (!chunks.has(key)) {\n            const minX = cx * CHUNK_SIZE;\n            const minY = cy * CHUNK_SIZE;\n            const minZ = cz * CHUNK_SIZE;\n            const maxX = minX + CHUNK_SIZE;\n            const maxY = minY + CHUNK_SIZE;\n            const maxZ = minZ + CHUNK_SIZE;\n\n            chunks.set(key, {\n              cx,\n              cy,\n              cz,\n              blocks: new Set(), // \"x,y,z\" keys of blocks in this chunk\n              dirty: true,\n              boundingBox: new THREE.Box3(\n                new THREE.Vector3(minX, minY, minZ),\n                new THREE.Vector3(maxX, maxY, maxZ)\n              ),\n              // BVH node data\n              visible: false,\n              distanceToCamera: Infinity,\n            });\n          }\n          return chunks.get(key);\n        }\n\n        // Torch lighting - create a point light for a torch\n        function addTorchLight(x, y, z) {\n          const key = `${x},${y},${z}`;\n          if (torchLights.has(key)) return; // Already has light\n\n          const light = new THREE.PointLight(0xffaa33, 1.2, 12, 1.5);\n          light.position.set(x + 0.5, y + 0.7, z + 0.5);\n          scene.add(light);\n          torchLights.set(key, light);\n\n          // Also create torch mesh if not already present\n          if (!torchMeshes.has(key)) {\n            const torchMesh = createTorchMesh(x, y, z);\n            scene.add(torchMesh);\n            torchMeshes.set(key, torchMesh);\n          }\n        }\n\n        // Remove torch light\n        function removeTorchLight(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const light = torchLights.get(key);\n          if (light) {\n            scene.remove(light);\n            light.dispose();\n            torchLights.delete(key);\n          }\n\n          // Also remove torch mesh\n          const torchMesh = torchMeshes.get(key);\n          if (torchMesh) {\n            scene.remove(torchMesh);\n            torchMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => mat.dispose());\n                } else {\n                  child.material.dispose();\n                }\n              }\n            });\n            torchMeshes.delete(key);\n          }\n        }\n\n        // Add lava light - creates a glowing orange/red point light for lava blocks\n        function addLavaLight(x, y, z) {\n          const key = `${x},${y},${z}`;\n          if (lavaLights.has(key)) return; // Already has light\n\n          const light = new THREE.PointLight(0xff4400, 2.0, 12, 1.8);\n          light.position.set(x + 0.5, y + 0.6, z + 0.5);\n          scene.add(light);\n          lavaLights.set(key, light);\n        }\n\n        // Remove lava light\n        function removeLavaLight(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const light = lavaLights.get(key);\n          if (light) {\n            scene.remove(light);\n            light.dispose();\n            lavaLights.delete(key);\n          }\n        }\n\n        // Create rice plant mesh - crossed sprite planes (X shape)\n        function createRicePlantMesh(x, y, z) {\n          const plantGroup = new THREE.Group();\n          plantGroup.position.set(x + 0.5, y + 0.5, z + 0.5);\n\n          // Get rice plant texture\n          const plantTexture = blockTextures[BLOCK_TYPES.RICE_PLANT]?.all;\n\n          let plantMaterial;\n          if (plantTexture) {\n            plantMaterial = new THREE.MeshLambertMaterial({\n              map: plantTexture,\n              transparent: true,\n              alphaTest: 0.1,\n              side: THREE.DoubleSide,\n            });\n          } else {\n            // Fallback to green color if texture not available\n            plantMaterial = new THREE.MeshLambertMaterial({\n              color: 0x5cb531,\n              transparent: true,\n              opacity: 0.9,\n              side: THREE.DoubleSide,\n            });\n          }\n\n          // Create two crossed planes (X shape) for the plant sprite\n          const planeGeometry = new THREE.PlaneGeometry(0.9, 0.9);\n\n          // First plane - diagonal\n          const plane1 = new THREE.Mesh(planeGeometry, plantMaterial);\n          plane1.rotation.y = Math.PI / 4; // 45 degrees\n          plantGroup.add(plane1);\n\n          // Second plane - perpendicular to first\n          const plane2 = new THREE.Mesh(planeGeometry, plantMaterial.clone());\n          plane2.rotation.y = -Math.PI / 4; // -45 degrees\n          plantGroup.add(plane2);\n\n          return plantGroup;\n        }\n\n        // Add rice plant mesh at position\n        function addRicePlantMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          if (ricePlantMeshes.has(key)) return; // Already has mesh\n\n          const plantMesh = createRicePlantMesh(x, y, z);\n          scene.add(plantMesh);\n          ricePlantMeshes.set(key, plantMesh);\n        }\n\n        // Remove rice plant mesh at position\n        function removeRicePlantMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const plantMesh = ricePlantMeshes.get(key);\n          if (plantMesh) {\n            scene.remove(plantMesh);\n            plantMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => mat.dispose());\n                } else {\n                  child.material.dispose();\n                }\n              }\n            });\n            ricePlantMeshes.delete(key);\n          }\n        }\n\n        // Create flower mesh - crossed sprite planes (X shape)\n        function createFlowerMesh(x, y, z) {\n          const flowerGroup = new THREE.Group();\n          flowerGroup.position.set(x + 0.5, y + 0.5, z + 0.5);\n\n          // Get flower texture\n          const flowerTexture = blockTextures[BLOCK_TYPES.FLOWER]?.all;\n\n          let flowerMaterial;\n          if (flowerTexture) {\n            flowerMaterial = new THREE.MeshLambertMaterial({\n              map: flowerTexture,\n              transparent: true,\n              alphaTest: 0.1,\n              side: THREE.DoubleSide,\n            });\n          } else {\n            // Fallback to pink color if texture not available\n            flowerMaterial = new THREE.MeshLambertMaterial({\n              color: 0xff69b4,\n              transparent: true,\n              opacity: 0.9,\n              side: THREE.DoubleSide,\n            });\n          }\n\n          // Create two crossed planes (X shape) for the flower sprite\n          const planeGeometry = new THREE.PlaneGeometry(0.8, 0.8);\n\n          const plane1 = new THREE.Mesh(planeGeometry, flowerMaterial);\n          plane1.rotation.y = Math.PI / 4;\n          flowerGroup.add(plane1);\n\n          const plane2 = new THREE.Mesh(planeGeometry, flowerMaterial.clone());\n          plane2.rotation.y = -Math.PI / 4;\n          flowerGroup.add(plane2);\n\n          return flowerGroup;\n        }\n\n        // Add flower mesh at position\n        function addFlowerMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          if (flowerMeshes.has(key)) return;\n\n          const flowerMesh = createFlowerMesh(x, y, z);\n          scene.add(flowerMesh);\n          flowerMeshes.set(key, flowerMesh);\n        }\n\n        // Remove flower mesh at position\n        function removeFlowerMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const flowerMesh = flowerMeshes.get(key);\n          if (flowerMesh) {\n            scene.remove(flowerMesh);\n            flowerMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => mat.dispose());\n                } else {\n                  child.material.dispose();\n                }\n              }\n            });\n            flowerMeshes.delete(key);\n          }\n        }\n\n        // Create TV mesh - transparent glass shell with inner GLB TV model\n        function createTVMesh(x, y, z) {\n          const tvGroup = new THREE.Group();\n          tvGroup.position.set(x + 0.5, y + 0.5, z + 0.5);\n\n          // Outer transparent glass shell\n          const glassMaterial = new THREE.MeshLambertMaterial({\n            color: 0x9ad6ff,\n            transparent: true,\n            opacity: 0.25,\n            emissive: new THREE.Color(0x204060),\n            emissiveIntensity: 0.2,\n          });\n          const glassShell = new THREE.Mesh(new THREE.BoxGeometry(1.0, 1.0, 1.0), glassMaterial);\n          tvGroup.add(glassShell);\n\n          if (tvModelTemplate) {\n            const tvModel = tvModelTemplate.clone();\n            tvModel.position.set(0, -0.1, 0.05);\n            tvModel.traverse((child) => {\n              if (child.isMesh) {\n                child.castShadow = true;\n                child.receiveShadow = true;\n                if (child.material) {\n                  child.material = child.material.clone();\n                }\n              }\n            });\n            tvGroup.add(tvModel);\n          } else {\n            // Fallback to simple inner TV body if GLB is unavailable\n            const bodyMaterial = new THREE.MeshLambertMaterial({\n              color: 0x1c1c1c,\n              emissive: new THREE.Color(0x050505),\n              emissiveIntensity: 0.3,\n            });\n            const body = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.55, 0.45), bodyMaterial);\n            body.position.set(0, 0, 0.08);\n            tvGroup.add(body);\n\n            // Screen plane with tv texture\n            const screenTexture = blockTextures[BLOCK_TYPES.TV]?.all;\n            const screenMaterial = screenTexture\n              ? new THREE.MeshLambertMaterial({\n                  map: screenTexture,\n                  emissive: new THREE.Color(0x0f3a5c),\n                  emissiveIntensity: 0.6,\n                })\n              : new THREE.MeshLambertMaterial({\n                  color: 0x0f3a5c,\n                  emissive: new THREE.Color(0x0f3a5c),\n                  emissiveIntensity: 0.6,\n                });\n            const screen = new THREE.Mesh(new THREE.PlaneGeometry(0.72, 0.42), screenMaterial);\n            screen.position.set(0, 0, 0.325);\n            tvGroup.add(screen);\n\n            // Base stand\n            const standMaterial = new THREE.MeshLambertMaterial({ color: 0x2b2b2b });\n            const stand = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.08, 0.25), standMaterial);\n            stand.position.set(0, -0.32, 0.05);\n            tvGroup.add(stand);\n          }\n\n          return tvGroup;\n        }\n\n        function addTVMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          if (tvMeshes.has(key)) return;\n\n          const tvMesh = createTVMesh(x, y, z);\n          scene.add(tvMesh);\n          tvMeshes.set(key, tvMesh);\n        }\n\n        function removeTVMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const tvMesh = tvMeshes.get(key);\n          if (tvMesh) {\n            scene.remove(tvMesh);\n            tvMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => mat.dispose());\n                } else {\n                  child.material.dispose();\n                }\n              }\n            });\n            tvMeshes.delete(key);\n          }\n        }\n\n        // Create bed mesh - 2x1 bed (2 blocks long, 1 block wide)\n        function createBedMesh(x, y, z) {\n          const bedGroup = new THREE.Group();\n          // Position at the center of the 2-block bed (offset by 0.5 in Z for the second block)\n          bedGroup.position.set(x + 0.5, y + 0.5, z + 1.0);\n\n          // Materials\n          const frameMaterial = new THREE.MeshLambertMaterial({ color: 0x5c3a21 }); // Dark oak wood\n          const frameAccentMaterial = new THREE.MeshLambertMaterial({ color: 0x7a4a2a }); // Lighter wood accent\n          const pillowMaterial = new THREE.MeshLambertMaterial({ color: 0xf5f0e6 }); // Cream white pillow\n          const blanketMaterial = new THREE.MeshLambertMaterial({ color: 0xc41e3a }); // Rich red blanket\n          const blanketFoldMaterial = new THREE.MeshLambertMaterial({ color: 0xa01830 }); // Darker red for folds\n          const sheetMaterial = new THREE.MeshLambertMaterial({ color: 0xfaf8f5 }); // White sheet\n\n          // === BED FRAME BASE ===\n          // Main frame rails (sides)\n          const leftRail = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.12, 1.85), frameMaterial);\n          leftRail.position.set(-0.46, -0.32, 0);\n          bedGroup.add(leftRail);\n\n          const rightRail = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.12, 1.85), frameMaterial);\n          rightRail.position.set(0.46, -0.32, 0);\n          bedGroup.add(rightRail);\n\n          // Cross supports\n          const frontSupport = new THREE.Mesh(\n            new THREE.BoxGeometry(0.84, 0.08, 0.08),\n            frameMaterial\n          );\n          frontSupport.position.set(0, -0.34, 0.85);\n          bedGroup.add(frontSupport);\n\n          const backSupport = new THREE.Mesh(\n            new THREE.BoxGeometry(0.84, 0.08, 0.08),\n            frameMaterial\n          );\n          backSupport.position.set(0, -0.34, -0.85);\n          bedGroup.add(backSupport);\n\n          const middleSupport = new THREE.Mesh(\n            new THREE.BoxGeometry(0.84, 0.08, 0.08),\n            frameMaterial\n          );\n          middleSupport.position.set(0, -0.34, 0);\n          bedGroup.add(middleSupport);\n\n          // === BED LEGS ===\n          const legGeometry = new THREE.BoxGeometry(0.1, 0.18, 0.1);\n\n          const legFL = new THREE.Mesh(legGeometry, frameMaterial);\n          legFL.position.set(-0.42, -0.47, 0.87);\n          bedGroup.add(legFL);\n\n          const legFR = new THREE.Mesh(legGeometry, frameMaterial);\n          legFR.position.set(0.42, -0.47, 0.87);\n          bedGroup.add(legFR);\n\n          const legBL = new THREE.Mesh(legGeometry, frameMaterial);\n          legBL.position.set(-0.42, -0.47, -0.87);\n          bedGroup.add(legBL);\n\n          const legBR = new THREE.Mesh(legGeometry, frameMaterial);\n          legBR.position.set(0.42, -0.47, -0.87);\n          bedGroup.add(legBR);\n\n          // === MATTRESS ===\n          const mattress = new THREE.Mesh(new THREE.BoxGeometry(0.88, 0.18, 1.78), sheetMaterial);\n          mattress.position.y = -0.17;\n          bedGroup.add(mattress);\n\n          // === PILLOW ===\n          // Main pillow - slightly puffy shape\n          const pillow = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.14, 0.28), pillowMaterial);\n          pillow.position.set(0, -0.01, -0.72);\n          bedGroup.add(pillow);\n\n          // Pillow top (slight dome effect)\n          const pillowTop = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.06, 0.22), pillowMaterial);\n          pillowTop.position.set(0, 0.08, -0.72);\n          bedGroup.add(pillowTop);\n\n          // === BLANKET / COMFORTER ===\n          // Main blanket body covering most of bed\n          const blanket = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.12, 1.35), blanketMaterial);\n          blanket.position.set(0, 0.0, 0.15);\n          bedGroup.add(blanket);\n\n          // Blanket fold at top (near pillow)\n          const blanketFold = new THREE.Mesh(\n            new THREE.BoxGeometry(0.88, 0.08, 0.18),\n            blanketFoldMaterial\n          );\n          blanketFold.position.set(0, 0.02, -0.48);\n          bedGroup.add(blanketFold);\n\n          // Sheet visible above blanket fold\n          const sheetTop = new THREE.Mesh(new THREE.BoxGeometry(0.86, 0.04, 0.12), sheetMaterial);\n          sheetTop.position.set(0, 0.0, -0.54);\n          bedGroup.add(sheetTop);\n\n          // Blanket side drapes (hang over edges slightly)\n          const leftDrape = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.1, 1.3), blanketMaterial);\n          leftDrape.position.set(-0.46, -0.08, 0.15);\n          bedGroup.add(leftDrape);\n\n          const rightDrape = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.1, 1.3), blanketMaterial);\n          rightDrape.position.set(0.46, -0.08, 0.15);\n          bedGroup.add(rightDrape);\n\n          // Blanket foot drape\n          const footDrape = new THREE.Mesh(\n            new THREE.BoxGeometry(0.86, 0.08, 0.06),\n            blanketMaterial\n          );\n          footDrape.position.set(0, -0.06, 0.84);\n          bedGroup.add(footDrape);\n\n          // === HEADBOARD ===\n          // Main headboard panel\n          const headboard = new THREE.Mesh(new THREE.BoxGeometry(0.96, 0.45, 0.08), frameMaterial);\n          headboard.position.set(0, 0.12, -0.94);\n          bedGroup.add(headboard);\n\n          // Headboard top trim\n          const headboardTrim = new THREE.Mesh(\n            new THREE.BoxGeometry(1.0, 0.06, 0.1),\n            frameAccentMaterial\n          );\n          headboardTrim.position.set(0, 0.37, -0.94);\n          bedGroup.add(headboardTrim);\n\n          // Headboard decorative posts\n          const leftPost = new THREE.Mesh(\n            new THREE.BoxGeometry(0.1, 0.55, 0.1),\n            frameAccentMaterial\n          );\n          leftPost.position.set(-0.45, 0.15, -0.94);\n          bedGroup.add(leftPost);\n\n          const rightPost = new THREE.Mesh(\n            new THREE.BoxGeometry(0.1, 0.55, 0.1),\n            frameAccentMaterial\n          );\n          rightPost.position.set(0.45, 0.15, -0.94);\n          bedGroup.add(rightPost);\n\n          // Post caps (finials)\n          const capGeometry = new THREE.BoxGeometry(0.12, 0.08, 0.12);\n          const leftCap = new THREE.Mesh(capGeometry, frameMaterial);\n          leftCap.position.set(-0.45, 0.46, -0.94);\n          bedGroup.add(leftCap);\n\n          const rightCap = new THREE.Mesh(capGeometry, frameMaterial);\n          rightCap.position.set(0.45, 0.46, -0.94);\n          bedGroup.add(rightCap);\n\n          // === FOOTBOARD ===\n          // Lower footboard panel\n          const footboard = new THREE.Mesh(new THREE.BoxGeometry(0.96, 0.22, 0.06), frameMaterial);\n          footboard.position.set(0, -0.05, 0.95);\n          bedGroup.add(footboard);\n\n          // Footboard trim\n          const footboardTrim = new THREE.Mesh(\n            new THREE.BoxGeometry(1.0, 0.05, 0.08),\n            frameAccentMaterial\n          );\n          footboardTrim.position.set(0, 0.08, 0.95);\n          bedGroup.add(footboardTrim);\n\n          return bedGroup;\n        }\n\n        // Add bed mesh at position (only add for head block to avoid duplicates)\n        function addBedMesh(x, y, z) {\n          const headKey = getBedHeadKey(x, y, z);\n          if (bedMeshes.has(headKey)) return;\n\n          // Only create mesh from the head block position\n          const headZ = getBedHeadZ(x, y, z);\n          const bedMesh = createBedMesh(x, y, headZ);\n          scene.add(bedMesh);\n          bedMeshes.set(headKey, bedMesh);\n        }\n\n        // Remove bed mesh at position\n        function removeBedMesh(x, y, z) {\n          const headKey = getBedHeadKey(x, y, z);\n          const bedMesh = bedMeshes.get(headKey);\n          if (bedMesh) {\n            scene.remove(bedMesh);\n            bedMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => mat.dispose());\n                } else {\n                  child.material.dispose();\n                }\n              }\n            });\n            bedMeshes.delete(headKey);\n          }\n        }\n\n        function damageMob(mob, damage) {\n          mob.health -= damage;\n\n          // Cows become aggravated (flee) when hit\n          if (mob.type === MOB_TYPES.COW) {\n            mob.isAggravated = true;\n            mob.aggravationTimer = 5; // Flee for 5 seconds\n          }\n\n          // Play villager hurt sound when villager takes damage\n          if (mob.type === MOB_TYPES.VILLAGER) {\n            playSoundAtPosition(\n              'villager_hurt_sound',\n              mob.position.x,\n              mob.position.y,\n              mob.position.z,\n              0.6,\n              18\n            );\n          }\n\n          // Play dog hurt sound when dog takes damage\n          if (mob.type === MOB_TYPES.DOG) {\n            playSoundAtPosition(\n              'dog_hurt_sound',\n              mob.position.x,\n              mob.position.y,\n              mob.position.z,\n              0.6,\n              18\n            );\n          }\n\n          // Flash red - store original colors for each material\n          const originalColors = new Map();\n          mob.group.traverse((mesh) => {\n            if (mesh.isMesh && mesh.material) {\n              // Handle both single material and material arrays\n              if (Array.isArray(mesh.material)) {\n                mesh.material.forEach((mat) => {\n                  if (mat.color && !originalColors.has(mat.uuid)) {\n                    originalColors.set(mat.uuid, mat.color.clone());\n                    mat.color.setHex(0xff0000);\n                  }\n                });\n              } else {\n                if (mesh.material.color && !originalColors.has(mesh.material.uuid)) {\n                  originalColors.set(mesh.material.uuid, mesh.material.color.clone());\n                  mesh.material.color.setHex(0xff0000);\n                }\n              }\n            }\n          });\n\n          setTimeout(() => {\n            if (mob.group) {\n              mob.group.traverse((mesh) => {\n                if (mesh.isMesh && mesh.material) {\n                  if (Array.isArray(mesh.material)) {\n                    mesh.material.forEach((mat) => {\n                      const originalColor = originalColors.get(mat.uuid);\n                      if (originalColor && mat.color) {\n                        mat.color.copy(originalColor);\n                      }\n                    });\n                  } else {\n                    const originalColor = originalColors.get(mesh.material.uuid);\n                    if (originalColor && mesh.material.color) {\n                      mesh.material.color.copy(originalColor);\n                    }\n                  }\n                }\n              });\n            }\n          }, 100);\n\n          if (mob.health <= 0) {\n            // Death particles\n            const color =\n              mob.type === MOB_TYPES.ZOMBIE\n                ? 0x5a7a3f\n                : mob.type === MOB_TYPES.CHICKEN\n                  ? 0xf5f5f5\n                  : mob.type === MOB_TYPES.COW\n                    ? 0x4a3728\n                    : mob.type === MOB_TYPES.VILLAGER\n                      ? 0x8b4513\n                      : 0x0da70d;\n            for (let i = 0; i < 10; i++) {\n              const geometry = new THREE.BoxGeometry(0.15, 0.15, 0.15);\n              const material = new THREE.MeshBasicMaterial({\n                color,\n              });\n              const particle = new THREE.Mesh(geometry, material);\n              particle.position.copy(mob.position);\n              particle.userData.velocity = new THREE.Vector3(\n                (Math.random() - 0.5) * 4,\n                Math.random() * 5,\n                (Math.random() - 0.5) * 4\n              );\n              particle.userData.life = 1;\n              scene.add(particle);\n              particles.push(particle);\n            }\n\n            // Drop food when zombie dies\n            if (mob.type === MOB_TYPES.ZOMBIE) {\n              // 50% chance to drop raw beef\n              if (Math.random() < 0.5) {\n                addToInventory(ITEM_TYPES.RAW_BEEF);\n                showMessage('+1 Raw Beef', '#8BC34A', 1500);\n              }\n            }\n\n            // Drop food when chicken dies\n            if (mob.type === MOB_TYPES.CHICKEN) {\n              // Always drop 1 raw chicken, 50% chance for 2\n              addToInventory(ITEM_TYPES.RAW_CHICKEN);\n              if (Math.random() < 0.5) {\n                addToInventory(ITEM_TYPES.RAW_CHICKEN);\n                showMessage('+2 Raw Chicken', '#8BC34A', 1500);\n              } else {\n                showMessage('+1 Raw Chicken', '#8BC34A', 1500);\n              }\n            }\n\n            // Drop food when cow dies\n            if (mob.type === MOB_TYPES.COW) {\n              // Always drop 1-3 raw beef\n              const beefDrops = 1 + Math.floor(Math.random() * 3);\n              for (let i = 0; i < beefDrops; i++) {\n                addToInventory(ITEM_TYPES.RAW_BEEF);\n              }\n              showMessage(`+${beefDrops} Raw Beef`, '#8BC34A', 1500);\n            }\n          }\n        }\n\n        // Add block to chunk tracking\n        function addBlockToChunk(x, y, z, blockType) {\n          const cx = Math.floor(x / CHUNK_SIZE);\n          const cy = Math.floor(y / CHUNK_SIZE);\n          const cz = Math.floor(z / CHUNK_SIZE);\n          const chunk = getOrCreateChunk(cx, cy, cz);\n          const blockKey = `${x},${y},${z}`;\n          chunk.blocks.add(blockKey);\n          chunk.dirty = true;\n\n          // Add torch light if this is a torch\n          if (blockType === BLOCK_TYPES.TORCH) {\n            addTorchLight(x, y, z);\n          }\n\n          // Add sign mesh if this is a sign\n          if (blockType === BLOCK_TYPES.OAK_SIGN) {\n            addSignMesh(x, y, z);\n          }\n\n          // Add door mesh if this is a door (only on base block)\n          if (blockType === BLOCK_TYPES.DOOR) {\n            addDoorMesh(x, y, z);\n          }\n\n          // Add furnace mesh if this is a furnace\n          if (blockType === BLOCK_TYPES.FURNACE) {\n            addFurnaceMesh(x, y, z);\n          }\n\n          // Add chest mesh if this is a chest\n          if (blockType === BLOCK_TYPES.CHEST) {\n            addChestMesh(x, y, z);\n          }\n\n          // Add lava light if this is a lava block\n          if (blockType === BLOCK_TYPES.LAVA) {\n            addLavaLight(x, y, z);\n          }\n\n          // Add rice plant mesh if this is a rice plant\n          if (blockType === BLOCK_TYPES.RICE_PLANT) {\n            addRicePlantMesh(x, y, z);\n          }\n\n          // Add bed mesh if this is a bed\n          if (blockType === BLOCK_TYPES.BED) {\n            addBedMesh(x, y, z);\n          }\n\n          // Add flower mesh if this is a flower\n          if (blockType === BLOCK_TYPES.FLOWER) {\n            addFlowerMesh(x, y, z);\n          }\n\n          // Add TV mesh if this is a TV\n          if (blockType === BLOCK_TYPES.TV) {\n            addTVMesh(x, y, z);\n          }\n\n          // Mark LOD chunk as needing update\n          const lodKey = `${cx},${cz}`;\n          const lodChunk = lodChunkMeshes.get(lodKey);\n          if (lodChunk) lodChunk.needsUpdate = true;\n        }\n\n        // Remove block from chunk tracking\n        function removeBlockFromChunk(x, y, z) {\n          const cx = Math.floor(x / CHUNK_SIZE);\n          const cy = Math.floor(y / CHUNK_SIZE);\n          const cz = Math.floor(z / CHUNK_SIZE);\n          const key = `${cx},${cy},${cz}`;\n          const chunk = chunks.get(key);\n          if (chunk) {\n            const blockKey = `${x},${y},${z}`;\n            chunk.blocks.delete(blockKey);\n            chunk.dirty = true;\n          }\n\n          // Remove torch light if there was one\n          removeTorchLight(x, y, z);\n\n          // Remove sign mesh if there was one\n          removeSignMesh(x, y, z);\n\n          // Remove door mesh if there was one (base of 2-block door)\n          removeDoorMesh(x, y, z);\n\n          // Remove furnace mesh if there was one\n          removeFurnaceMesh(x, y, z);\n\n          // Remove chest mesh if there was one\n          removeChestMesh(x, y, z);\n\n          // Remove lava light if there was one\n          removeLavaLight(x, y, z);\n\n          // Remove rice plant mesh if there was one\n          removeRicePlantMesh(x, y, z);\n\n          // Remove bed mesh if there was one\n          removeBedMesh(x, y, z);\n\n          // Remove flower mesh if there was one\n          removeFlowerMesh(x, y, z);\n\n          // Remove TV mesh if there was one\n          removeTVMesh(x, y, z);\n\n          // Mark LOD chunk as needing update\n          const lodKey = `${cx},${cz}`;\n          const lodChunk = lodChunkMeshes.get(lodKey);\n          if (lodChunk) lodChunk.needsUpdate = true;\n        }\n\n        // Get dominant block color for a chunk column (for LOD far meshes)\n        // Works with infinite world - samples terrain at chunk corners for efficiency\n        function getChunkDominantColor(cx, cz) {\n          const startX = cx * CHUNK_SIZE;\n          const startZ = cz * CHUNK_SIZE;\n          const endX = startX + CHUNK_SIZE;\n          const endZ = startZ + CHUNK_SIZE;\n\n          const blockCounts = {};\n          let totalBlocks = 0;\n          let avgHeight = 0;\n\n          // Sample every 2 blocks for performance on infinite terrain\n          const step = 2;\n          for (let x = startX; x < endX; x += step) {\n            for (let z = startZ; z < endZ; z += step) {\n              // Use terrain height function directly - much faster than scanning\n              const height = getTerrainHeight(x, z);\n              const blockType = getBlockAt(x, height, z);\n\n              if (blockType && blockType !== BLOCK_TYPES.AIR) {\n                blockCounts[blockType] = (blockCounts[blockType] || 0) + 1;\n                totalBlocks++;\n                avgHeight += height;\n              }\n            }\n          }\n\n          if (totalBlocks === 0) return { color: 0x87ceeb, height: 8 }; // Sky color for empty\n\n          avgHeight /= totalBlocks;\n\n          // Find dominant block type\n          let dominantType = BLOCK_TYPES.GRASS;\n          let maxCount = 0;\n          for (const [type, count] of Object.entries(blockCounts)) {\n            if (count > maxCount) {\n              maxCount = count;\n              dominantType = parseInt(type);\n            }\n          }\n\n          // Map block types to colors\n          const colorMap = {\n            [BLOCK_TYPES.GRASS]: 0x5cb531,\n            [BLOCK_TYPES.DIRT]: 0x8b6f47,\n            [BLOCK_TYPES.STONE]: 0x7a7a7a,\n            [BLOCK_TYPES.WOOD]: 0x8b5a2b,\n            [BLOCK_TYPES.LEAVES]: 0x2d5016,\n            [BLOCK_TYPES.SAND]: 0xe0c896,\n            [BLOCK_TYPES.WATER]: 0x4a90e2,\n            [BLOCK_TYPES.WATER_FLOW]: 0x4a90e2,\n            [BLOCK_TYPES.LAVA]: 0xff4400,\n            [BLOCK_TYPES.LAVA_FLOW]: 0xff4400,\n            [BLOCK_TYPES.DYNAMITE]: 0xff4500,\n            [BLOCK_TYPES.PLANKS]: 0xc4a76b,\n            [BLOCK_TYPES.TORCH]: 0xffa500,\n            [BLOCK_TYPES.GLASS]: 0xc0e8ff,\n          };\n\n          return {\n            color: colorMap[dominantType] || 0x5cb531,\n            height: avgHeight,\n            dominantType,\n          };\n        }\n\n        // Create or update LOD far mesh for a chunk column\n        function createLodFarMesh(cx, cz) {\n          const lodKey = `${cx},${cz}`;\n          let lodData = lodChunkMeshes.get(lodKey);\n\n          const chunkInfo = getChunkDominantColor(cx, cz);\n\n          if (!lodData) {\n            // Create new LOD mesh - a simple box representing the chunk\n            const geometry = new THREE.BoxGeometry(CHUNK_SIZE, chunkInfo.height + 1, CHUNK_SIZE);\n            const material = new THREE.MeshLambertMaterial({\n              color: chunkInfo.color,\n              transparent: true,\n              opacity: 0.9,\n            });\n            const mesh = new THREE.Mesh(geometry, material);\n\n            mesh.position.set(\n              cx * CHUNK_SIZE + CHUNK_SIZE / 2,\n              (chunkInfo.height + 1) / 2,\n              cz * CHUNK_SIZE + CHUNK_SIZE / 2\n            );\n            mesh.visible = false;\n            scene.add(mesh);\n\n            lodData = {\n              mesh,\n              lodLevel: LOD_FAR,\n              needsUpdate: false,\n              lastColor: chunkInfo.color,\n              lastHeight: chunkInfo.height,\n            };\n            lodChunkMeshes.set(lodKey, lodData);\n          } else if (lodData.needsUpdate) {\n            // Update existing mesh\n            lodData.mesh.material.color.setHex(chunkInfo.color);\n            lodData.mesh.scale.y = (chunkInfo.height + 1) / (lodData.lastHeight + 1) || 1;\n            lodData.mesh.position.y = (chunkInfo.height + 1) / 2;\n            lodData.lastColor = chunkInfo.color;\n            lodData.lastHeight = chunkInfo.height;\n            lodData.needsUpdate = false;\n          }\n\n          return lodData;\n        }\n\n        // Get LOD level based on distance\n        function getLodLevel(distance) {\n          if (!LOD_ENABLED) return LOD_FULL;\n          if (distance <= LOD_NEAR_DISTANCE) return LOD_FULL;\n          if (distance <= LOD_MEDIUM_DISTANCE) return LOD_MEDIUM;\n          if (distance <= LOD_FAR_DISTANCE) return LOD_FAR;\n          return -1; // Beyond all LOD levels - don't render\n        }\n\n        // Update frustum for culling\n        function updateFrustum() {\n          camera.updateMatrixWorld();\n          frustumMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);\n          frustum.setFromProjectionMatrix(frustumMatrix);\n        }\n\n        // Test if chunk is visible (BVH frustum culling)\n        function isChunkVisible(chunk) {\n          // Distance-based LOD culling\n          const chunkCenter = new THREE.Vector3(\n            (chunk.cx + 0.5) * CHUNK_SIZE,\n            (chunk.cy + 0.5) * CHUNK_SIZE,\n            (chunk.cz + 0.5) * CHUNK_SIZE\n          );\n          const distance = chunkCenter.distanceTo(camera.position);\n          chunk.distanceToCamera = distance;\n\n          // Skip chunks beyond render distance\n          if (distance > RENDER_DISTANCE * 1.5) {\n            return false;\n          }\n\n          // Frustum test\n          return frustum.intersectsBox(chunk.boundingBox);\n        }\n\n        // Sort chunks by distance for LOD prioritization\n        function sortChunksByDistance(visibleChunksList) {\n          return visibleChunksList.sort((a, b) => a.distanceToCamera - b.distanceToCamera);\n        }\n\n        // Legacy compatibility functions for existing code\n        function getPooledMesh() {\n          // Return a dummy object for compatibility\n          return {\n            position: new THREE.Vector3(),\n            material: [],\n            userData: {},\n          };\n        }\n\n        function returnMeshToPool(mesh) {\n          // No-op for batched rendering\n        }\n\n        // DDA Raycast for line-of-sight visibility check (works with infinite world)\n        function hasLineOfSight(x0, y0, z0, x1, y1, z1) {\n          const dx = x1 - x0;\n          const dy = y1 - y0;\n          const dz = z1 - z0;\n          const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);\n          if (dist < 0.1) return true;\n\n          const stepX = dx / dist;\n          const stepY = dy / dist;\n          const stepZ = dz / dist;\n\n          let cx = Math.floor(x0);\n          let cy = Math.floor(y0);\n          let cz = Math.floor(z0);\n\n          const targetCX = Math.floor(x1);\n          const targetCY = Math.floor(y1);\n          const targetCZ = Math.floor(z1);\n\n          const stepDirX = stepX >= 0 ? 1 : -1;\n          const stepDirY = stepY >= 0 ? 1 : -1;\n          const stepDirZ = stepZ >= 0 ? 1 : -1;\n\n          const nextBoundaryX = stepX >= 0 ? cx + 1 - x0 : x0 - cx;\n          const nextBoundaryY = stepY >= 0 ? cy + 1 - y0 : y0 - cy;\n          const nextBoundaryZ = stepZ >= 0 ? cz + 1 - z0 : z0 - cz;\n\n          let tMaxX = nextBoundaryX / Math.abs(stepX);\n          let tMaxY = nextBoundaryY / Math.abs(stepY);\n          let tMaxZ = nextBoundaryZ / Math.abs(stepZ);\n\n          const tDeltaX = 1 / Math.abs(stepX);\n          const tDeltaY = 1 / Math.abs(stepY);\n          const tDeltaZ = 1 / Math.abs(stepZ);\n\n          const maxSteps = Math.ceil(dist) + 10;\n          let steps = 0;\n\n          while (cy >= 0 && cy < worldHeight && steps < maxSteps) {\n            if (cx === targetCX && cy === targetCY && cz === targetCZ) {\n              return true;\n            }\n\n            if (getBlockAt(cx, cy, cz) !== BLOCK_TYPES.AIR) {\n              return false;\n            }\n\n            if (tMaxX < tMaxY) {\n              if (tMaxX < tMaxZ) {\n                cx += stepDirX;\n                tMaxX += tDeltaX;\n              } else {\n                cz += stepDirZ;\n                tMaxZ += tDeltaZ;\n              }\n            } else {\n              if (tMaxY < tMaxZ) {\n                cy += stepDirY;\n                tMaxY += tDeltaY;\n              } else {\n                cz += stepDirZ;\n                tMaxZ += tDeltaZ;\n              }\n            }\n            steps++;\n          }\n          return true;\n        }\n\n        function buildWorldMesh() {\n          updateLoadingProgress(70, 'buildingWorld');\n\n          // Initialize instanced meshes for batched rendering\n          initializeInstancedMeshes();\n\n          // Clear existing data\n          blockMeshes.clear();\n          chunks.clear();\n\n          updateLoadingProgress(75, 'infiniteWorldReady');\n\n          // No pre-population needed for infinite world\n          // Chunks are created on-demand as player explores\n\n          // Initial render of visible blocks around spawn\n          updateVisibleBlocks();\n        }\n\n        function createOtherPlayerMesh(pid, data) {\n          const playerGroup = new THREE.Group();\n\n          // Determine which model to use based on player's gender\n          const useAlexModel = data.gender === 'female' && alexModelTemplate;\n          const modelTemplate = useAlexModel ? alexModelTemplate : playerModelTemplate;\n          const modelAnimations = useAlexModel ? alexModelAnimations : playerModelAnimations;\n\n          // Store gender for later reference\n          playerGroup.userData.gender = data.gender || 'male';\n\n          // Use the loaded GLB model if available\n          if (modelTemplate) {\n            const playerModel = modelTemplate.clone();\n\n            // Clone materials to allow independent modifications\n            playerModel.traverse((child) => {\n              if (child.isMesh) {\n                child.castShadow = true;\n                child.receiveShadow = true;\n\n                // Clone material to avoid shared state issues\n                if (child.material) {\n                  child.material = child.material.clone();\n                }\n              }\n            });\n\n            // Adjust position so feet are at ground level\n            playerModel.position.y = -0.9;\n\n            playerGroup.add(playerModel);\n\n            // Store reference for animations\n            playerGroup.userData.model = playerModel;\n            playerGroup.userData.lastPos = { x: data.x, y: data.y, z: data.z };\n\n            // Set up AnimationMixer if animations are available\n            if (modelAnimations && modelAnimations.length > 0) {\n              const mixer = new THREE.AnimationMixer(playerModel);\n              playerGroup.userData.mixer = mixer;\n              playerGroup.userData.actions = {};\n              playerGroup.userData.currentAction = null;\n\n              // Filter out head tracks from all animations so we can control head rotation manually\n              let headTrackNames = [];\n              for (const clip of modelAnimations) {\n                const headTracks = clip.tracks.filter((track) => {\n                  const trackName = track.name.toLowerCase();\n                  return trackName.includes('head');\n                });\n                headTrackNames = headTrackNames.concat(headTracks.map((t) => t.name));\n              }\n              // Remove duplicates\n              headTrackNames = [...new Set(headTrackNames)];\n\n              // Create actions for each animation, filtering out head tracks\n              for (const clip of modelAnimations) {\n                const clipName = clip.name.toLowerCase();\n                if (headTrackNames.length > 0) {\n                  // Filter out head tracks from this animation\n                  const filteredTracks = clip.tracks.filter(\n                    (track) => !headTrackNames.includes(track.name)\n                  );\n                  if (filteredTracks.length > 0) {\n                    const filteredClip = new THREE.AnimationClip(\n                      clip.name,\n                      clip.duration,\n                      filteredTracks\n                    );\n                    const action = mixer.clipAction(filteredClip);\n                    playerGroup.userData.actions[clipName] = action;\n                  }\n                } else {\n                  // No head tracks found, use original animation\n                  const action = mixer.clipAction(clip);\n                  playerGroup.userData.actions[clipName] = action;\n                }\n              }\n\n              // Start with idle animation\n              const idleAction = playerGroup.userData.actions['idle'];\n              if (idleAction) {\n                idleAction.play();\n                playerGroup.userData.currentAction = 'idle';\n              }\n            }\n\n            // Try to find head for pitch rotation\n            playerModel.traverse((child) => {\n              const name = child.name.toLowerCase();\n              if (name.includes('head')) {\n                playerGroup.userData.head = child;\n              }\n            });\n          } else {\n            // Fallback to the original Steve skin-based mesh if GLB failed to load\n            function getSubTexture(image, x, y, w, h, fullW, fullH) {\n              const tex = new THREE.Texture(image);\n              tex.repeat.set(w / fullW, h / fullH);\n              tex.offset.set(x / fullW, 1 - (y + h) / fullH);\n              tex.magFilter = THREE.NearestFilter;\n              tex.minFilter = THREE.NearestFilter;\n              tex.needsUpdate = true;\n              return tex;\n            }\n\n            const skin = assetCache.images['steve_skin'];\n\n            const headMaterials = [\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 0, 8, 8, 8, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 16, 8, 8, 8, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 8, 0, 8, 8, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 16, 0, 8, 8, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 24, 8, 8, 8, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 8, 8, 8, 8, 64, 64) }),\n            ];\n\n            const bodyMaterials = [\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 16, 20, 4, 12, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 28, 20, 4, 12, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 20, 16, 8, 4, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 28, 16, 8, 4, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 32, 20, 8, 12, 64, 64) }),\n              new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 20, 20, 8, 12, 64, 64) }),\n            ];\n\n            const pirateScale = 1;\n\n            const headSize = 0.4 * pirateScale;\n            const headGeometry = new THREE.BoxGeometry(headSize, headSize, headSize);\n            const head = new THREE.Mesh(headGeometry, headMaterials);\n            head.position.y = 0.7 * pirateScale;\n            playerGroup.add(head);\n\n            const bodyWidth = 0.5 * pirateScale;\n            const bodyHeight = 0.6 * pirateScale;\n            const bodyDepth = 0.25 * pirateScale;\n            const bodyGeometry = new THREE.BoxGeometry(bodyWidth, bodyHeight, bodyDepth);\n            const body = new THREE.Mesh(bodyGeometry, bodyMaterials);\n            body.position.y = 0.2 * pirateScale;\n            playerGroup.add(body);\n\n            playerGroup.userData.head = head;\n            playerGroup.userData.animationTime = 0;\n            playerGroup.userData.lastPos = { x: data.x, y: data.y, z: data.z };\n          }\n\n          playerGroup.position.set(data.x, data.y, data.z);\n          playerGroup.rotation.y = data.yaw || 0;\n\n          playerGroup.userData.pid = pid;\n          playerGroup.userData.playerName = data.playerName;\n\n          playerTargetPositions.set(pid, {\n            x: data.x,\n            y: data.y,\n            z: data.z,\n            yaw: data.yaw || 0,\n            pitch: data.pitch || 0,\n          });\n\n          if (scene) {\n            scene.add(playerGroup);\n          }\n\n          // Add name tag above player\n          const nameCanvas = document.createElement('canvas');\n          nameCanvas.width = 256;\n          nameCanvas.height = 64;\n          const nameCtx = nameCanvas.getContext('2d');\n          nameCtx.fillStyle = 'rgba(0, 0, 0, 0.6)';\n          nameCtx.fillRect(0, 0, 256, 64);\n          nameCtx.font = 'bold 32px Inter, sans-serif';\n          nameCtx.fillStyle = 'white';\n          nameCtx.textAlign = 'center';\n          nameCtx.textBaseline = 'middle';\n          nameCtx.fillText(data.playerName || 'Player', 128, 32);\n\n          const nameTexture = new THREE.CanvasTexture(nameCanvas);\n          nameTexture.magFilter = THREE.LinearFilter;\n          nameTexture.minFilter = THREE.LinearFilter;\n\n          const nameMaterial = new THREE.SpriteMaterial({\n            map: nameTexture,\n            transparent: true,\n          });\n          const nameSprite = new THREE.Sprite(nameMaterial);\n          nameSprite.scale.set(2, 0.5, 1);\n          nameSprite.position.y = playerModelTemplate ? 1.8 : 1.2;\n          playerGroup.add(nameSprite);\n\n          window['playerMesh_' + pid] = playerGroup;\n        }\n\n        function removeOtherPlayerMesh(pid) {\n          const playerGroup = window['playerMesh_' + pid];\n          if (playerGroup) {\n            // Stop the mixer if it exists\n            if (playerGroup.userData.mixer) {\n              playerGroup.userData.mixer.stopAllAction();\n            }\n            scene.remove(playerGroup);\n            playerGroup.traverse((child) => {\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => {\n                    if (mat.map && typeof mat.map.dispose === 'function') mat.map.dispose();\n                    mat.dispose();\n                  });\n                } else {\n                  if (child.material.map && typeof child.material.map.dispose === 'function')\n                    child.material.map.dispose();\n                  child.material.dispose();\n                }\n              }\n              if (child.geometry) child.geometry.dispose();\n            });\n            delete window['playerMesh_' + pid];\n          }\n          playerTargetPositions.delete(pid);\n        }\n\n        // Trigger arm swing animation for a player (for mining/placing blocks)\n        function triggerPlayerSwingAnimation(playerGroup) {\n          if (!playerGroup || !playerGroup.userData.actions) return;\n\n          // Look for holding-right- animation (for arm swing)\n          const actions = playerGroup.userData.actions;\n          const swingAction =\n            actions['holding-right-'] || actions['holding-right'] || actions['swing'];\n\n          if (swingAction) {\n            // Play swing animation once\n            swingAction.reset();\n            swingAction.setLoop(THREE.LoopOnce, 1);\n            swingAction.clampWhenFinished = true;\n            swingAction.play();\n\n            // Return to previous animation after swing completes\n            const mixer = playerGroup.userData.mixer;\n            if (mixer) {\n              const onFinish = () => {\n                mixer.removeEventListener('finished', onFinish);\n                // Resume idle or walk based on current state\n                const currentAction = playerGroup.userData.currentAction;\n                if (currentAction && actions[currentAction]) {\n                  actions[currentAction].reset().fadeIn(0.1).play();\n                }\n              };\n              mixer.addEventListener('finished', onFinish);\n            }\n          }\n        }\n\n        // Global function to trigger swing animation for remote players\n        window.triggerRemotePlayerSwing = function (pid) {\n          const playerGroup = window['playerMesh_' + pid];\n          if (playerGroup) {\n            triggerPlayerSwingAnimation(playerGroup);\n          }\n        };\n\n        // Toggle player gender between Steve (male) and Alex (female)\n        function togglePlayerGender() {\n          playerGender = playerGender === 'male' ? 'female' : 'male';\n\n          // Rebuild local player model with new gender\n          if (typeof rebuildLocalPlayerModel === 'function') {\n            rebuildLocalPlayerModel();\n          }\n\n          // Update button appearance\n          const genderBtn = document.getElementById('gender-toggle-btn');\n          if (genderBtn) {\n            genderBtn.textContent = playerGender === 'male' ? '\ud83d\udc68' : '\ud83d\udc69';\n            genderBtn.style.background =\n              playerGender === 'male' ? 'rgba(100, 149, 237, 0.3)' : 'rgba(255, 105, 180, 0.3)';\n            genderBtn.style.borderColor =\n              playerGender === 'male' ? 'rgba(100, 149, 237, 0.5)' : 'rgba(255, 105, 180, 0.5)';\n          }\n\n          // Show feedback\n          const modelName = playerGender === 'male' ? 'Steve' : 'Alex';\n          showMessage(\n            `Switched to ${modelName}!`,\n            playerGender === 'male' ? '#6495ED' : '#FF69B4',\n            1500\n          );\n\n          // Play sound\n          playSound('block_place_sound', false, 0.4);\n\n          // Sync gender change to other players\n          if (isOnline) {\n            syncEvent('gender_change', { gender: playerGender });\n          }\n\n          lib.log('Player gender switched to: ' + playerGender);\n        }\n\n        function updateVisibleBlocks() {\n          // Update frustum for BVH culling\n          updateFrustum();\n\n          const nearDistSq = LOD_NEAR_DISTANCE * LOD_NEAR_DISTANCE;\n          const mediumDistSq = LOD_MEDIUM_DISTANCE * LOD_MEDIUM_DISTANCE;\n          const farDistSq = LOD_FAR_DISTANCE * LOD_FAR_DISTANCE;\n\n          // Reset counters\n          iteratedBlocks = 0;\n          visibleChunks = 0;\n          culledChunks = 0;\n          lodFullChunks = 0;\n          lodMediumChunks = 0;\n          lodFarChunks = 0;\n          batchedDrawCalls = 0;\n\n          // Track which blocks should be visible\n          const visibleBlockKeys = new Set();\n          const blocksToAdd = new Map(); // blockType -> array of {x, y, z}\n          const blocksToRemove = new Map(); // blockType -> array of keys\n\n          // Track LOD far chunks to show\n          const farChunksToShow = new Set();\n          const farChunksToHide = new Set();\n\n          // Initialize block arrays for each type\n          instancedMeshes.forEach((data, blockType) => {\n            blocksToAdd.set(blockType, []);\n            blocksToRemove.set(blockType, []);\n          });\n\n          // === BVH CHUNK-BASED FRUSTUM CULLING WITH LOD ===\n          const visibleChunksList = [];\n          const chunkColumnsProcessed = new Set();\n\n          chunks.forEach((chunk, chunkKey) => {\n            // Distance-based early rejection\n            const chunkCenterX = (chunk.cx + 0.5) * CHUNK_SIZE;\n            const chunkCenterY = (chunk.cy + 0.5) * CHUNK_SIZE;\n            const chunkCenterZ = (chunk.cz + 0.5) * CHUNK_SIZE;\n\n            const dx = chunkCenterX - playerPos.x;\n            const dy = chunkCenterY - playerPos.y;\n            const dz = chunkCenterZ - playerPos.z;\n            const distSq = dx * dx + dy * dy + dz * dz;\n            const dist2D = Math.sqrt(dx * dx + dz * dz); // Horizontal distance for LOD\n\n            chunk.distanceToCamera = Math.sqrt(distSq);\n            chunk.lodLevel = getLodLevel(dist2D);\n\n            const lodKey = `${chunk.cx},${chunk.cz}`;\n\n            // Beyond all LOD levels - don't render\n            if (chunk.lodLevel < 0) {\n              chunk.visible = false;\n              culledChunks++;\n              farChunksToHide.add(lodKey);\n              return;\n            }\n\n            // Frustum culling for chunks\n            if (!frustum.intersectsBox(chunk.boundingBox)) {\n              chunk.visible = false;\n              culledChunks++;\n              // Still hide LOD mesh if frustum culled\n              if (chunk.lodLevel === LOD_FAR) {\n                farChunksToHide.add(lodKey);\n              }\n              return;\n            }\n\n            // Handle LOD levels\n            if (chunk.lodLevel === LOD_FAR && LOD_ENABLED) {\n              // Far LOD - use simplified chunk mesh\n              if (!chunkColumnsProcessed.has(lodKey)) {\n                chunkColumnsProcessed.add(lodKey);\n                farChunksToShow.add(lodKey);\n                lodFarChunks++;\n              }\n              chunk.visible = false; // Don't process individual blocks\n              return;\n            } else {\n              // Near/Medium LOD - hide LOD mesh, use detailed blocks\n              farChunksToHide.add(lodKey);\n            }\n\n            chunk.visible = true;\n            visibleChunksList.push(chunk);\n            visibleChunks++;\n\n            if (chunk.lodLevel === LOD_FULL) lodFullChunks++;\n            else if (chunk.lodLevel === LOD_MEDIUM) lodMediumChunks++;\n          });\n\n          // Sort chunks by distance for LOD prioritization\n          visibleChunksList.sort((a, b) => a.distanceToCamera - b.distanceToCamera);\n\n          // === MANAGE LOD FAR MESHES ===\n          farChunksToShow.forEach((lodKey) => {\n            const [cx, cz] = lodKey.split(',').map(Number);\n            const lodData = createLodFarMesh(cx, cz);\n            lodData.mesh.visible = true;\n          });\n\n          farChunksToHide.forEach((lodKey) => {\n            const lodData = lodChunkMeshes.get(lodKey);\n            if (lodData && lodData.mesh) {\n              lodData.mesh.visible = false;\n            }\n          });\n\n          // === RAYCAST VISIBILITY WITHIN VISIBLE CHUNKS ===\n          const cameraDir = new THREE.Vector3();\n          camera.getWorldDirection(cameraDir);\n          const cameraRight = new THREE.Vector3();\n          const cameraUp = new THREE.Vector3();\n          cameraRight.crossVectors(cameraDir, camera.up).normalize();\n          cameraUp.crossVectors(cameraRight, cameraDir).normalize();\n\n          // Adaptive ray count based on LOD\n          const baseRayCount = Math.min(config.rayCountX || 60, 80);\n          const rayCountX = baseRayCount;\n          const rayCountY = Math.floor(baseRayCount * 0.75);\n          const fov = (camera.fov * Math.PI) / 180;\n          const aspect = camera.aspect;\n          const spreadMult = config.raycastSpreadMultiplier || 2;\n          const camPos = camera.position.clone();\n\n          // Use medium distance for raycast (not far - those use LOD meshes)\n          const raycastMaxDist = LOD_ENABLED ? LOD_MEDIUM_DISTANCE : RENDER_DISTANCE;\n          const raycastDistSq = raycastMaxDist * raycastMaxDist;\n\n          // DDA raycast to find visible blocks\n          for (let i = 0; i < rayCountX; i++) {\n            for (let j = 0; j < rayCountY; j++) {\n              const u = (i / (rayCountX - 1) - 0.5) * 2;\n              const v = (j / (rayCountY - 1) - 0.5) * 2;\n\n              const rayDir = new THREE.Vector3()\n                .copy(cameraDir)\n                .add(\n                  cameraRight.clone().multiplyScalar(u * Math.tan(fov / 2) * aspect * spreadMult)\n                )\n                .add(cameraUp.clone().multiplyScalar(v * Math.tan(fov / 2) * spreadMult))\n                .normalize();\n\n              let cx = Math.floor(camPos.x);\n              let cy = Math.floor(camPos.y);\n              let cz = Math.floor(camPos.z);\n\n              // Only check Y bounds (height is still limited)\n              if (cy < 0 || cy >= worldHeight) continue;\n\n              const stepDirX = rayDir.x >= 0 ? 1 : -1;\n              const stepDirY = rayDir.y >= 0 ? 1 : -1;\n              const stepDirZ = rayDir.z >= 0 ? 1 : -1;\n\n              const nextBoundaryX = rayDir.x >= 0 ? cx + 1 - camPos.x : camPos.x - cx;\n              const nextBoundaryY = rayDir.y >= 0 ? cy + 1 - camPos.y : camPos.y - cy;\n              const nextBoundaryZ = rayDir.z >= 0 ? cz + 1 - camPos.z : camPos.z - cz;\n\n              let tMaxX = nextBoundaryX / Math.abs(rayDir.x);\n              let tMaxY = nextBoundaryY / Math.abs(rayDir.y);\n              let tMaxZ = nextBoundaryZ / Math.abs(rayDir.z);\n\n              const tDeltaX = 1 / Math.abs(rayDir.x);\n              const tDeltaY = 1 / Math.abs(rayDir.y);\n              const tDeltaZ = 1 / Math.abs(rayDir.z);\n\n              let steps = 0;\n              const maxSteps = raycastMaxDist + 20;\n              const penetration = config.raycastPenetration || 0;\n              let penetrationCount = 0;\n\n              while (steps < maxSteps) {\n                iteratedBlocks++;\n\n                // Distance check\n                const dx = cx - playerPos.x;\n                const dy = cy - playerPos.y;\n                const dz = cz - playerPos.z;\n                const distSq = dx * dx + dy * dy + dz * dz;\n                if (distSq > raycastDistSq) break;\n\n                // Only check Y bounds for infinite world\n                if (cy >= 0 && cy < worldHeight) {\n                  // BVH optimization: get or create chunk for this position\n                  const chunkKey = getChunkKey(cx, cy, cz);\n                  let chunk = chunks.get(chunkKey);\n                  if (!chunk) {\n                    // Create chunk on-demand for infinite world\n                    const chunkCx = Math.floor(cx / CHUNK_SIZE);\n                    const chunkCy = Math.floor(cy / CHUNK_SIZE);\n                    const chunkCz = Math.floor(cz / CHUNK_SIZE);\n                    chunk = getOrCreateChunk(chunkCx, chunkCy, chunkCz);\n                    chunk.visible = true; // Mark as visible since we're raycasting to it\n                  }\n\n                  const blockType = getBlockAt(cx, cy, cz);\n                  if (chunk && chunk.visible && blockType !== BLOCK_TYPES.AIR) {\n                    const blockKey = `${cx},${cy},${cz}`;\n\n                    // For medium LOD, skip some blocks for performance\n                    const isFullLod = chunk.lodLevel === LOD_FULL;\n                    const skipBlock = !isFullLod && (cx + cy + cz) % 2 === 1;\n\n                    if (!skipBlock) {\n                      visibleBlockKeys.add(blockKey);\n\n                      // Add neighbors for smooth edges (only for full LOD)\n                      if (isFullLod) {\n                        const neighbors = [\n                          [cx - 1, cy, cz],\n                          [cx + 1, cy, cz],\n                          [cx, cy - 1, cz],\n                          [cx, cy + 1, cz],\n                          [cx, cy, cz - 1],\n                          [cx, cy, cz + 1],\n                        ];\n                        for (const [nx, ny, nz] of neighbors) {\n                          if (ny >= 0 && ny < worldHeight) {\n                            if (getBlockAt(nx, ny, nz) !== BLOCK_TYPES.AIR) {\n                              const ndx = nx - playerPos.x;\n                              const ndy = ny - playerPos.y;\n                              const ndz = nz - playerPos.z;\n                              if (ndx * ndx + ndy * ndy + ndz * ndz <= nearDistSq) {\n                                visibleBlockKeys.add(`${nx},${ny},${nz}`);\n                              }\n                            }\n                          }\n                        }\n                      }\n                    }\n\n                    if (\n                      blockType !== BLOCK_TYPES.GLASS &&\n                      blockType !== BLOCK_TYPES.WATER &&\n                      blockType !== BLOCK_TYPES.WATER_FLOW &&\n                      blockType !== BLOCK_TYPES.LAVA &&\n                      blockType !== BLOCK_TYPES.LAVA_FLOW\n                    ) {\n                      if (penetration === 0 || penetrationCount >= penetration) break;\n                      penetrationCount++;\n                    }\n                  }\n                }\n\n                // DDA step\n                if (tMaxX < tMaxY) {\n                  if (tMaxX < tMaxZ) {\n                    cx += stepDirX;\n                    tMaxX += tDeltaX;\n                  } else {\n                    cz += stepDirZ;\n                    tMaxZ += tDeltaZ;\n                  }\n                } else {\n                  if (tMaxY < tMaxZ) {\n                    cy += stepDirY;\n                    tMaxY += tDeltaY;\n                  } else {\n                    cz += stepDirZ;\n                    tMaxZ += tDeltaZ;\n                  }\n                }\n                steps++;\n              }\n            }\n          }\n\n          // === BATCH UPDATE INSTANCED MESHES ===\n          // Find blocks to add (visible but not yet in instance)\n          for (const key of visibleBlockKeys) {\n            if (!blockMeshes.has(key)) {\n              const [x, y, z] = key.split(',').map(Number);\n              const blockType = worldData[x][y][z];\n              if (blockType !== BLOCK_TYPES.AIR) {\n                blocksToAdd.get(blockType)?.push({ x, y, z, key });\n              }\n            }\n          }\n\n          // Find blocks to remove (in instance but no longer visible, with buffer timeout)\n          const currentTime = performance.now();\n          const deletionBuffer = config.chunkDeletionBufferMs || 1000;\n\n          // Update last seen time for visible blocks\n          for (const key of visibleBlockKeys) {\n            blockLastSeenTime.set(key, currentTime);\n          }\n\n          blockMeshes.forEach((blockData, key) => {\n            if (!visibleBlockKeys.has(key)) {\n              const lastSeen = blockLastSeenTime.get(key) || 0;\n              // Only remove if buffer timeout has passed\n              if (currentTime - lastSeen > deletionBuffer) {\n                blocksToRemove.get(blockData.blockType)?.push(key);\n                blockLastSeenTime.delete(key); // Clean up timestamp entry\n              }\n            }\n          });\n\n          // Update each instanced mesh\n          instancedMeshes.forEach((instanceData, blockType) => {\n            const toAdd = blocksToAdd.get(blockType) || [];\n            const toRemove = blocksToRemove.get(blockType) || [];\n\n            // Remove blocks\n            for (const key of toRemove) {\n              const blockData = blockMeshes.get(key);\n              if (blockData && instanceData.blockPositions.has(key)) {\n                const instanceIndex = instanceData.blockPositions.get(key);\n\n                // Swap with last instance to avoid gaps\n                if (instanceData.count > 1 && instanceIndex < instanceData.count - 1) {\n                  let lastBlockKey = null;\n                  instanceData.blockPositions.forEach((idx, k) => {\n                    if (idx === instanceData.count - 1) lastBlockKey = k;\n                  });\n\n                  if (lastBlockKey) {\n                    const lastMatrix = new THREE.Matrix4();\n                    instanceData.mesh.getMatrixAt(instanceData.count - 1, lastMatrix);\n                    instanceData.mesh.setMatrixAt(instanceIndex, lastMatrix);\n                    instanceData.blockPositions.set(lastBlockKey, instanceIndex);\n                  }\n                }\n\n                instanceData.blockPositions.delete(key);\n                instanceData.count--;\n                blockMeshes.delete(key);\n                removeBlockFromChunk(blockData.x, blockData.y, blockData.z);\n              }\n            }\n\n            // Add blocks\n            for (const block of toAdd) {\n              if (instanceData.count < MAX_INSTANCES_PER_TYPE) {\n                const instanceIndex = instanceData.count;\n\n                // Water flow blocks have trapezoid shape (shorter height)\n                if (blockType === BLOCK_TYPES.WATER_FLOW) {\n                  const flowHeight = 0.4; // Flowing water is shorter (trapezoid effect)\n                  tempPosition.set(block.x + 0.5, block.y + flowHeight / 2, block.z + 0.5);\n                  tempScale.set(1, flowHeight, 1);\n                  tempMatrix.compose(tempPosition, tempQuaternion, tempScale);\n                  tempScale.set(1, 1, 1); // Reset scale for other blocks\n                } else {\n                  tempPosition.set(block.x + 0.5, block.y + 0.5, block.z + 0.5);\n                  tempMatrix.compose(tempPosition, tempQuaternion, tempScale);\n                }\n                instanceData.mesh.setMatrixAt(instanceIndex, tempMatrix);\n\n                instanceData.blockPositions.set(block.key, instanceIndex);\n                instanceData.count++;\n\n                blockMeshes.set(block.key, {\n                  blockType,\n                  x: block.x,\n                  y: block.y,\n                  z: block.z,\n                });\n\n                addBlockToChunk(block.x, block.y, block.z, blockType);\n              }\n            }\n\n            // Update instance count and mark for GPU upload\n            instanceData.mesh.count = instanceData.count;\n            if (toAdd.length > 0 || toRemove.length > 0) {\n              instanceData.mesh.instanceMatrix.needsUpdate = true;\n            }\n\n            if (instanceData.count > 0) batchedDrawCalls++;\n          });\n\n          visibleMeshes = visibleBlockKeys.size;\n        }\n\n        function updateBlockAt(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const existingBlock = blockMeshes.get(key);\n\n          // Remove existing block from instanced mesh\n          if (existingBlock) {\n            const instanceData = instancedMeshes.get(existingBlock.blockType);\n            if (instanceData && instanceData.blockPositions.has(key)) {\n              const instanceIndex = instanceData.blockPositions.get(key);\n\n              // Swap with last instance\n              if (instanceData.count > 1 && instanceIndex < instanceData.count - 1) {\n                let lastBlockKey = null;\n                instanceData.blockPositions.forEach((idx, k) => {\n                  if (idx === instanceData.count - 1) lastBlockKey = k;\n                });\n\n                if (lastBlockKey) {\n                  const lastMatrix = new THREE.Matrix4();\n                  instanceData.mesh.getMatrixAt(instanceData.count - 1, lastMatrix);\n                  instanceData.mesh.setMatrixAt(instanceIndex, lastMatrix);\n                  instanceData.blockPositions.set(lastBlockKey, instanceIndex);\n                }\n              }\n\n              instanceData.blockPositions.delete(key);\n              instanceData.count--;\n              instanceData.mesh.count = instanceData.count;\n              instanceData.mesh.instanceMatrix.needsUpdate = true;\n            }\n\n            blockMeshes.delete(key);\n            removeBlockFromChunk(x, y, z);\n          }\n\n          const blockType = getBlockAt(x, y, z);\n          if (blockType === BLOCK_TYPES.AIR) {\n            // Update neighbors - they might need to be visible now\n            const neighbors = [\n              [x - 1, y, z],\n              [x + 1, y, z],\n              [x, y - 1, z],\n              [x, y + 1, z],\n              [x, y, z - 1],\n              [x, y, z + 1],\n            ];\n\n            for (const [nx, ny, nz] of neighbors) {\n              if (ny >= 0 && ny < worldHeight) {\n                const nKey = `${nx},${ny},${nz}`;\n                const neighborType = getBlockAt(nx, ny, nz);\n\n                if (!blockMeshes.has(nKey) && neighborType !== BLOCK_TYPES.AIR) {\n                  // Add neighbor to instanced mesh\n                  const instanceData = instancedMeshes.get(neighborType);\n                  if (instanceData && instanceData.count < MAX_INSTANCES_PER_TYPE) {\n                    const instanceIndex = instanceData.count;\n\n                    // Water flow blocks have trapezoid shape (shorter height)\n                    if (neighborType === BLOCK_TYPES.WATER_FLOW) {\n                      const flowHeight = 0.4;\n                      tempPosition.set(nx + 0.5, ny + flowHeight / 2, nz + 0.5);\n                      tempScale.set(1, flowHeight, 1);\n                      tempMatrix.compose(tempPosition, tempQuaternion, tempScale);\n                      tempScale.set(1, 1, 1);\n                    } else {\n                      tempPosition.set(nx + 0.5, ny + 0.5, nz + 0.5);\n                      tempMatrix.compose(tempPosition, tempQuaternion, tempScale);\n                    }\n                    instanceData.mesh.setMatrixAt(instanceIndex, tempMatrix);\n\n                    instanceData.blockPositions.set(nKey, instanceIndex);\n                    instanceData.count++;\n                    instanceData.mesh.count = instanceData.count;\n                    instanceData.mesh.instanceMatrix.needsUpdate = true;\n\n                    blockMeshes.set(nKey, {\n                      blockType: neighborType,\n                      x: nx,\n                      y: ny,\n                      z: nz,\n                    });\n\n                    addBlockToChunk(nx, ny, nz, neighborType);\n                  }\n                }\n              }\n            }\n            return;\n          }\n\n          // Check if any face is exposed (infinite world - only check Y bounds)\n          const isTransp = (t) =>\n            t === BLOCK_TYPES.AIR ||\n            t === BLOCK_TYPES.WATER ||\n            t === BLOCK_TYPES.WATER_FLOW ||\n            t === BLOCK_TYPES.LAVA ||\n            t === BLOCK_TYPES.LAVA_FLOW ||\n            t === BLOCK_TYPES.LEAVES ||\n            t === BLOCK_TYPES.GLASS ||\n            t === BLOCK_TYPES.RICE_PLANT ||\n            t === BLOCK_TYPES.FLOWER;\n          const hasExposedFace =\n            isTransp(getBlockAt(x - 1, y, z)) ||\n            isTransp(getBlockAt(x + 1, y, z)) ||\n            y === 0 ||\n            isTransp(getBlockAt(x, y - 1, z)) ||\n            y === worldHeight - 1 ||\n            isTransp(getBlockAt(x, y + 1, z)) ||\n            isTransp(getBlockAt(x, y, z - 1)) ||\n            isTransp(getBlockAt(x, y, z + 1));\n\n          if (!hasExposedFace) return;\n\n          // Add block to instanced mesh\n          const instanceData = instancedMeshes.get(blockType);\n          if (instanceData && instanceData.count < MAX_INSTANCES_PER_TYPE) {\n            const instanceIndex = instanceData.count;\n\n            // Water flow blocks have trapezoid shape (shorter height)\n            if (blockType === BLOCK_TYPES.WATER_FLOW) {\n              const flowHeight = 0.4;\n              tempPosition.set(x + 0.5, y + flowHeight / 2, z + 0.5);\n              tempScale.set(1, flowHeight, 1);\n              tempMatrix.compose(tempPosition, tempQuaternion, tempScale);\n              tempScale.set(1, 1, 1);\n            } else {\n              tempPosition.set(x + 0.5, y + 0.5, z + 0.5);\n              tempMatrix.compose(tempPosition, tempQuaternion, tempScale);\n            }\n            instanceData.mesh.setMatrixAt(instanceIndex, tempMatrix);\n\n            instanceData.blockPositions.set(key, instanceIndex);\n            instanceData.count++;\n            instanceData.mesh.count = instanceData.count;\n            instanceData.mesh.instanceMatrix.needsUpdate = true;\n\n            blockMeshes.set(key, {\n              blockType,\n              x,\n              y,\n              z,\n            });\n\n            addBlockToChunk(x, y, z, blockType);\n          }\n        }\n\n        // Player state - spawn at origin for infinite world\n        let playerPos = new THREE.Vector3(0, 20, 0);\n        let playerYaw = 0;\n        let playerPitch = 0;\n        let playerVelocity = new THREE.Vector3(0, 0, 0);\n        const PLAYER_HEIGHT = 1.7;\n        const PLAYER_RADIUS = 0.3;\n        const GRAVITY = -20;\n        const JUMP_VELOCITY = 8;\n        let isGrounded = false;\n        let isInWater = false;\n        let isFirstPerson = false;\n        let spawnPoint = null;\n\n        // Fall damage tracking\n        let fallStartY = 0; // Y position when player started falling\n        let wasFalling = false; // Track if player was falling in previous frame\n        const FALL_DAMAGE_PER_BLOCK = 1; // Damage per block fallen beyond threshold\n\n        // Water physics constants\n        const WATER_GRAVITY = -5; // Reduced gravity in water\n        const WATER_SWIM_SPEED = 0.7; // Multiplier for movement in water\n        const WATER_SINK_SPEED = 2; // How fast player sinks in water\n        const WATER_SWIM_UP_SPEED = 4; // How fast player can swim up\n        const WATER_SPREAD_DISTANCE = Math.max(1, config.waterFlowDistance || 1); // How far water spreads horizontally (like Minecraft)\n        // Store the initial spawn point for respawning\n\n        // Find spawn point - start of obby course\n        function findSpawnPoint() {\n          // Spawn at first checkpoint of the obby\n          if (obbyCheckpoints.length > 0) {\n            const cp = obbyCheckpoints[0];\n            return new THREE.Vector3(cp.x, cp.y, cp.z);\n          }\n          return new THREE.Vector3(0.5, 27, 0.5);\n        }\n\n        // Input state\n        const keys = {};\n        let mouseMovement = {\n          x: 0,\n          y: 0,\n        };\n        let isPointerLocked = false;\n        let isMining = false;\n        let miningProgress = 0;\n        let targetBlock = null;\n        let selectedSlot = 0;\n\n        // Touch controls\n        const isTouchDevice =\n          config.forceMobileUI ||\n          (lib.shouldUseTouchControls ? lib.shouldUseTouchControls() : 'ontouchstart' in window);\n\n        // Mobile lighting limits for performance\n        const MAX_LIGHTS_MOBILE = 6; // Max point lights on mobile\n        const MAX_LIGHTS_DESKTOP = 50; // Max point lights on desktop\n        const MAX_ACTIVE_LIGHTS = isTouchDevice ? MAX_LIGHTS_MOBILE : MAX_LIGHTS_DESKTOP;\n        let lastLightUpdate = 0;\n        const LIGHT_UPDATE_INTERVAL = 250; // Update active lights every 250ms\n        // Throttle vars for updateVisibleBlocks\n        let _lastVBTime = 0;\n        const _lastVBPos = { x: 0, z: 0 };\n\n        let leftJoystick = {\n          active: false,\n          baseX: 0,\n          baseY: 0,\n          knobX: 0,\n          knobY: 0,\n          dx: 0,\n          dy: 0,\n          touchId: null,\n        };\n        let rightJoystick = {\n          active: false,\n          baseX: 0,\n          baseY: 0,\n          knobX: 0,\n          knobY: 0,\n          dx: 0,\n          dy: 0,\n          touchId: null,\n        };\n        const JOYSTICK_MAX_RADIUS = 80;\n        const JOYSTICK_DEAD_ZONE = 15;\n\n        // Inventory - expand to 36 slots (9 hotbar + 27 main inventory)\n        let inventory = JSON.parse(JSON.stringify(config.inventory));\n        // Ensure we have 36 slots total\n        while (inventory.length < 36) {\n          inventory.push({ type: BLOCK_TYPES.AIR, count: 0 });\n        }\n\n        // Additional starting items for the inventory\n        inventory[9] = { type: ITEM_TYPES.BREAD, count: 10 };\n        inventory[10] = { type: BLOCK_TYPES.OAK_SIGN, count: 5 };\n        inventory[11] = { type: ITEM_TYPES.BUCKET_EMPTY, count: 1 };\n        inventory[12] = { type: ITEM_TYPES.BUCKET_LAVA, count: 1 };\n        inventory[13] = { type: ITEM_TYPES.BUCKET_WATER, count: 1 };\n        inventory[14] = { type: BLOCK_TYPES.SOIL, count: 5 };\n        inventory[15] = { type: BLOCK_TYPES.RICE_PLANT, count: 10 };\n        inventory[16] = { type: BLOCK_TYPES.BED, count: 5 };\n        inventory[17] = { type: BLOCK_TYPES.TV, count: 5 };\n        inventory[18] = { type: ITEM_TYPES.ARROW_WEAPON, count: 1 };\n\n        // Inventory UI state\n        let isInventoryOpen = false;\n        let heldItem = null; // { type, count } when dragging\n        let heldItemSourceSlot = -1;\n\n        // Crafting grid (2x2 for inventory, 3x3 for crafting table)\n        let craftingGrid = [\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n        ];\n        let craftingTableGrid = [\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n          { type: BLOCK_TYPES.AIR, count: 0 },\n        ];\n        let isCraftingTableOpen = false;\n        let craftingResult = { type: BLOCK_TYPES.AIR, count: 0 };\n\n        // Crafting recipes - pattern is 3x3 grid, 0 = empty\n        const CRAFTING_RECIPES = [\n          // 1 Wood \u2192 4 Planks\n          {\n            pattern: [\n              [4, 0, 0],\n              [0, 0, 0],\n              [0, 0, 0],\n            ],\n            result: { type: BLOCK_TYPES.PLANKS, count: 4 },\n            name: 'Planks',\n          },\n          // 2 Planks (vertical) \u2192 4 Sticks\n          {\n            pattern: [\n              [10, 0, 0],\n              [10, 0, 0],\n              [0, 0, 0],\n            ],\n            result: { type: ITEM_TYPES.STICK, count: 4 },\n            name: 'Sticks',\n          },\n          // 1 Coal + 1 Stick (vertical) \u2192 4 Torches\n          {\n            pattern: [\n              [105, 0, 0],\n              [104, 0, 0],\n              [0, 0, 0],\n            ],\n            result: { type: BLOCK_TYPES.TORCH, count: 4 },\n            name: 'Torches',\n          },\n          // 4 Dirt in square \u2192 1 Grass\n          {\n            pattern: [\n              [2, 2, 0],\n              [2, 2, 0],\n              [0, 0, 0],\n            ],\n            result: { type: BLOCK_TYPES.GRASS, count: 1 },\n            name: 'Grass Block',\n          },\n          // 8 Stone around center \u2192 1 Dynamite\n          {\n            pattern: [\n              [3, 3, 3],\n              [3, 0, 3],\n              [3, 3, 0],\n            ],\n            result: { type: BLOCK_TYPES.DYNAMITE, count: 1 },\n            name: 'TNT',\n          },\n          // 4 Sand in square \u2192 1 Stone\n          {\n            pattern: [\n              [7, 7, 0],\n              [7, 7, 0],\n              [0, 0, 0],\n            ],\n            result: { type: BLOCK_TYPES.STONE, count: 1 },\n            name: 'Sandstone',\n          },\n          // 2 Wood + 2 Leaves \u2192 4 Dirt (compost)\n          {\n            pattern: [\n              [4, 5, 0],\n              [4, 5, 0],\n              [0, 0, 0],\n            ],\n            result: { type: BLOCK_TYPES.DIRT, count: 4 },\n            name: 'Compost',\n          },\n          // 1 Wood \u2192 4 Leaves (decorative)\n          {\n            pattern: [\n              [4, 0, 0],\n              [0, 0, 0],\n              [0, 0, 0],\n            ],\n            result: { type: BLOCK_TYPES.LEAVES, count: 4 },\n            name: 'Leaf Bundle',\n          },\n          // 4 Leaves \u2192 1 Wood\n          {\n            pattern: [\n              [5, 5, 0],\n              [5, 5, 0],\n              [0, 0, 0],\n            ],\n            result: { type: BLOCK_TYPES.WOOD, count: 1 },\n            name: 'Compressed Wood',\n          },\n          // 9 Dirt \u2192 1 Sand\n          {\n            pattern: [\n              [2, 2, 2],\n              [2, 2, 2],\n              [2, 2, 2],\n            ],\n            result: { type: BLOCK_TYPES.SAND, count: 1 },\n            name: 'Sand',\n          },\n          // Wooden Pickaxe: 3 wood on top, 1 wood in middle\n          {\n            pattern: [\n              [4, 4, 4],\n              [0, 4, 0],\n              [0, 4, 0],\n            ],\n            result: { type: ITEM_TYPES.WOODEN_PICKAXE, count: 1 },\n            name: 'Wooden Pickaxe',\n          },\n          // Wooden Axe: 2 wood top-left, 1 wood below, 1 wood handle\n          {\n            pattern: [\n              [4, 4, 0],\n              [4, 4, 0],\n              [0, 4, 0],\n            ],\n            result: { type: ITEM_TYPES.WOODEN_AXE, count: 1 },\n            name: 'Wooden Axe',\n          },\n          // Wooden Sword: 2 wood vertically + 1 wood handle\n          {\n            pattern: [\n              [0, 4, 0],\n              [0, 4, 0],\n              [0, 4, 0],\n            ],\n            result: { type: ITEM_TYPES.WOODEN_SWORD, count: 1 },\n            name: 'Wooden Sword',\n          },\n          // Crafting Table: 4 wood in a 2x2 square\n          {\n            pattern: [\n              [4, 4, 0],\n              [4, 4, 0],\n              [0, 0, 0],\n            ],\n            result: { type: BLOCK_TYPES.CRAFTING_TABLE, count: 1 },\n            name: 'Crafting Table',\n          },\n          // Stone Pickaxe: 3 stone on top, 1 stick in middle, 1 stick handle\n          {\n            pattern: [\n              [3, 3, 3],\n              [0, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.STONE_PICKAXE, count: 1 },\n            name: 'Stone Pickaxe',\n          },\n          // Stone Axe: 2 stone top-left, 1 stick below, 1 stick handle\n          {\n            pattern: [\n              [3, 3, 0],\n              [3, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.STONE_AXE, count: 1 },\n            name: 'Stone Axe',\n          },\n          // Stone Sword: 2 stone vertically + 1 stick handle\n          {\n            pattern: [\n              [0, 3, 0],\n              [0, 3, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.STONE_SWORD, count: 1 },\n            name: 'Stone Sword',\n          },\n          // Wooden Shovel: 1 wood on top, 2 sticks below\n          {\n            pattern: [\n              [0, 4, 0],\n              [0, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.WOODEN_SHOVEL, count: 1 },\n            name: 'Wooden Shovel',\n          },\n          // Stone Shovel: 1 stone on top, 2 sticks below\n          {\n            pattern: [\n              [0, 3, 0],\n              [0, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.STONE_SHOVEL, count: 1 },\n            name: 'Stone Shovel',\n          },\n          // Oak Sign: 6 planks + 1 stick = 3 signs\n          {\n            pattern: [\n              [10, 10, 10],\n              [10, 10, 10],\n              [0, 104, 0],\n            ],\n            result: { type: BLOCK_TYPES.OAK_SIGN, count: 3 },\n            name: 'Oak Sign',\n          },\n          // Chest: 8 planks in a square with empty center\n          {\n            pattern: [\n              [10, 10, 10],\n              [10, 0, 10],\n              [10, 10, 10],\n            ],\n            result: { type: BLOCK_TYPES.CHEST, count: 1 },\n            name: 'Chest',\n          },\n          // Furnace: 8 stone in a square with empty center\n          {\n            pattern: [\n              [3, 3, 3],\n              [3, 0, 3],\n              [3, 3, 3],\n            ],\n            result: { type: BLOCK_TYPES.FURNACE, count: 1 },\n            name: 'Furnace',\n          },\n          // Iron Pickaxe: 3 iron ingots on top, 2 sticks below\n          {\n            pattern: [\n              [111, 111, 111],\n              [0, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.IRON_PICKAXE, count: 1 },\n            name: 'Iron Pickaxe',\n          },\n          // Iron Axe: 2 iron ingots top-left, 1 stick below, 1 stick handle\n          {\n            pattern: [\n              [111, 111, 0],\n              [111, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.IRON_AXE, count: 1 },\n            name: 'Iron Axe',\n          },\n          // Iron Sword: 2 iron ingots vertically + 1 stick handle\n          {\n            pattern: [\n              [0, 111, 0],\n              [0, 111, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.IRON_SWORD, count: 1 },\n            name: 'Iron Sword',\n          },\n          // Iron Shovel: 1 iron ingot on top, 2 sticks below\n          {\n            pattern: [\n              [0, 111, 0],\n              [0, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.IRON_SHOVEL, count: 1 },\n            name: 'Iron Shovel',\n          },\n          // Gold Pickaxe: 3 gold ingots on top, 2 sticks below\n          {\n            pattern: [\n              [112, 112, 112],\n              [0, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.GOLD_PICKAXE, count: 1 },\n            name: 'Gold Pickaxe',\n          },\n          // Gold Axe: 2 gold ingots top-left, 1 stick below, 1 stick handle\n          {\n            pattern: [\n              [112, 112, 0],\n              [112, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.GOLD_AXE, count: 1 },\n            name: 'Gold Axe',\n          },\n          // Gold Sword: 2 gold ingots vertically + 1 stick handle\n          {\n            pattern: [\n              [0, 112, 0],\n              [0, 112, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.GOLD_SWORD, count: 1 },\n            name: 'Gold Sword',\n          },\n          // Gold Shovel: 1 gold ingot on top, 2 sticks below\n          {\n            pattern: [\n              [0, 112, 0],\n              [0, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.GOLD_SHOVEL, count: 1 },\n            name: 'Gold Shovel',\n          },\n          // Diamond Pickaxe: 3 diamonds on top, 2 sticks below\n          {\n            pattern: [\n              [113, 113, 113],\n              [0, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.DIAMOND_PICKAXE, count: 1 },\n            name: 'Diamond Pickaxe',\n          },\n          // Diamond Axe: 2 diamonds top-left, 1 stick below, 1 stick handle\n          {\n            pattern: [\n              [113, 113, 0],\n              [113, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.DIAMOND_AXE, count: 1 },\n            name: 'Diamond Axe',\n          },\n          // Diamond Sword: 2 diamonds vertically + 1 stick handle\n          {\n            pattern: [\n              [0, 113, 0],\n              [0, 113, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.DIAMOND_SWORD, count: 1 },\n            name: 'Diamond Sword',\n          },\n          // Diamond Shovel: 1 diamond on top, 2 sticks below\n          {\n            pattern: [\n              [0, 113, 0],\n              [0, 104, 0],\n              [0, 104, 0],\n            ],\n            result: { type: ITEM_TYPES.DIAMOND_SHOVEL, count: 1 },\n            name: 'Diamond Shovel',\n          },\n          // Bread: 3 planks in a row (wheat substitute since we don't have wheat)\n          {\n            pattern: [\n              [10, 10, 10],\n              [0, 0, 0],\n              [0, 0, 0],\n            ],\n            result: { type: ITEM_TYPES.BREAD, count: 1 },\n            name: 'Bread',\n          },\n          // Bucket: 3 iron ingots in V shape\n          {\n            pattern: [\n              [111, 0, 111],\n              [0, 111, 0],\n              [0, 0, 0],\n            ],\n            result: { type: ITEM_TYPES.BUCKET_EMPTY, count: 1 },\n            name: 'Bucket',\n          },\n          // Door: 6 planks in 2x3 pattern = 3 doors\n          {\n            pattern: [\n              [10, 10, 0],\n              [10, 10, 0],\n              [10, 10, 0],\n            ],\n            result: { type: BLOCK_TYPES.DOOR, count: 3 },\n            name: 'Door',\n          },\n          // Flowers: 1 rice plant + 1 grass (diagonal) = 2 flowers\n          {\n            pattern: [\n              [26, 0, 0],\n              [0, 1, 0],\n              [0, 0, 0],\n            ],\n            result: { type: BLOCK_TYPES.FLOWER, count: 2 },\n            name: 'Flower',\n          },\n        ];\n\n        // Player health\n        let playerHealth = 20;\n        const maxPlayerHealth = 20;\n        let lastDamageTime = 0;\n        const DAMAGE_COOLDOWN = 1.0;\n\n        // Player hunger system\n        let playerHunger = 20;\n        const maxPlayerHunger = 20;\n        let hungerTimer = 0;\n        const HUNGER_DECREASE_INTERVAL = 4.0; // Base seconds between hunger decrease\n        const HUNGER_MOVEMENT_MULTIPLIER = 2.0; // Hunger decreases faster when moving\n        const HUNGER_MINING_COST = 0.05; // Hunger cost per block mined\n        let lastHungerDamageTime = 0;\n        const HUNGER_DAMAGE_COOLDOWN = 2.0; // Seconds between starvation damage\n        const HUNGER_DAMAGE_AMOUNT = 1; // Damage per starvation tick\n        const HUNGER_REGEN_THRESHOLD = 1; // Hunger level needed for health regen\n        let lastRegenTime = 0;\n        const REGEN_COOLDOWN = 4.0; // Seconds between health regen ticks\n\n        // Food restoration values\n        const FOOD_VALUES = {\n          [ITEM_TYPES.RAW_BEEF]: 3,\n          [ITEM_TYPES.COOKED_BEEF]: 8,\n          [ITEM_TYPES.APPLE]: 4,\n          [ITEM_TYPES.BREAD]: 5,\n          [ITEM_TYPES.RAW_CHICKEN]: 2,\n          [ITEM_TYPES.COOKED_CHICKEN]: 6,\n        };\n\n        // Mobs\n        const MOB_TYPES = {\n          ZOMBIE: 'zombie',\n          CREEPER: 'creeper',\n          CHICKEN: 'chicken',\n          DOG: 'dog',\n          COW: 'cow',\n          VILLAGER: 'villager',\n        };\n\n        const mobs = [];\n        let nextMobSpawn = 0;\n        const MOB_SPAWN_INTERVAL = 10;\n        const MAX_MOBS = 20;\n\n        // Dynamite countdown tracking\n        const dynamiteCountdowns = new Map();\n\n        // Torch lighting system - track point lights at torch positions\n        const torchLights = new Map(); // key: \"x,y,z\" -> THREE.PointLight\n        const torchMeshes = new Map(); // key: \"x,y,z\" -> THREE.Group (torch mesh)\n\n        // Lava lighting system - track point lights at lava positions\n        const lavaLights = new Map(); // key: \"x,y,z\" -> THREE.PointLight\n\n        // Rice plant mesh system - track sprite meshes at plant positions\n        const ricePlantMeshes = new Map(); // key: \"x,y,z\" -> THREE.Group (crossed sprite mesh)\n\n        // Flower mesh system - track sprite meshes at flower positions\n        const flowerMeshes = new Map(); // key: \"x,y,z\" -> THREE.Group (crossed sprite mesh)\n\n        // Bed mesh system - track meshes at bed positions\n        const bedMeshes = new Map(); // key: \"x,y,z\" -> THREE.Group (bed mesh)\n\n        // TV mesh system - track meshes at TV positions\n        const tvMeshes = new Map(); // key: \"x,y,z\" -> THREE.Group (tv mesh)\n\n        // Sign mesh system - track sign meshes at sign positions\n        const signMeshes = new Map(); // key: \"x,y,z\" -> THREE.Group (sign mesh)\n        const signMessages = new Map(); // key: \"x,y,z\" -> string (sign message)\n        const signOrientations = new Map(); // key: \"x,y,z\" -> rotation in radians\n        // key: \"x,y,z\", value: { timeLeft, totalTime }\n\n        // Chest storage system - track chest contents\n        const chestContents = new Map(); // key: \"x,y,z\" -> array of { type, count } (27 slots)\n        let isChestOpen = false;\n        let openChestKey = null; // \"x,y,z\" key of currently open chest\n\n        // Furnace smelting system\n        const furnaceContents = new Map();\n        const furnaceLights = new Map(); // key: \"x,y,z\" -> THREE.PointLight // key: \"x,y,z\" -> { input: {type, count}, fuel: {type, count}, output: {type, count}, progress: 0, fuelProgress: 0 }\n        let isFurnaceOpen = false;\n        let openFurnaceKey = null; // \"x,y,z\" key of currently open furnace\n\n        // Door state tracking system\n        const doorStates = new Map(); // key: \"x,y,z\" -> boolean (true = open, false = closed)\n        const doorMeshes = new Map(); // key: \"x,y,z\" -> THREE.Mesh (custom door mesh)\n        const doorOrientations = new Map(); // key: \"x,y,z\" -> rotation in radians (facing direction)\n\n        // Chest orientation tracking system\n        const chestOrientations = new Map(); // key: \"x,y,z\" -> rotation in radians\n        const chestMeshes = new Map(); // key: \"x,y,z\" -> THREE.Mesh (custom chest mesh)\n\n        // Furnace orientation tracking system\n        const furnaceOrientations = new Map(); // key: \"x,y,z\" -> rotation in radians (0, PI/2, PI, -PI/2)\n        const furnaceMeshes = new Map(); // key: \"x,y,z\" -> THREE.Mesh (custom furnace mesh)\n\n        // Smelting recipes: input type -> output type\n        const SMELTING_RECIPES = {\n          [BLOCK_TYPES.SAND]: BLOCK_TYPES.GLASS, // Sand -> Glass\n          [BLOCK_TYPES.WOOD]: ITEM_TYPES.COAL, // Wood -> Charcoal (coal)\n          [BLOCK_TYPES.COAL_ORE]: ITEM_TYPES.COAL, // Coal ore -> Coal\n          [BLOCK_TYPES.IRON_ORE]: ITEM_TYPES.IRON_INGOT, // Iron ore -> Iron ingot\n          [BLOCK_TYPES.GOLD_ORE]: ITEM_TYPES.GOLD_INGOT, // Gold ore -> Gold ingot\n          [BLOCK_TYPES.STONE]: BLOCK_TYPES.STONE, // Stone -> Smooth stone\n          [ITEM_TYPES.RAW_BEEF]: ITEM_TYPES.COOKED_BEEF, // Raw beef -> Cooked beef\n          [ITEM_TYPES.RAW_CHICKEN]: ITEM_TYPES.COOKED_CHICKEN, // Raw chicken -> Cooked chicken\n        };\n\n        // Fuel burn times (in seconds)\n        const FUEL_BURN_TIMES = {\n          [ITEM_TYPES.COAL]: 80, // Coal burns longest\n          [BLOCK_TYPES.WOOD]: 15, // Wood logs\n          [BLOCK_TYPES.PLANKS]: 15, // Planks\n          [ITEM_TYPES.STICK]: 5, // Sticks burn quickly\n        };\n\n        const SMELT_TIME = 10; // Seconds to smelt one item\n\n        function createTorchMesh(x, y, z) {\n          const torch = new THREE.Group();\n          torch.position.set(x + 0.5, y + 0.25, z + 0.5);\n\n          // Helper to create a box\n          function box(w, h, d, color) {\n            return new THREE.Mesh(\n              new THREE.BoxGeometry(w, h, d),\n              new THREE.MeshLambertMaterial({ color })\n            );\n          }\n\n          // === HANDLE (stacked cubes, Minecraft style) ===\n          const handleColors = [0x5a3a1a, 0x6b4423, 0x7a4f28, 0x8b5a2b];\n\n          for (let i = 0; i < 4; i++) {\n            const segment = box(0.18, 0.18, 0.18, handleColors[i % handleColors.length]);\n            segment.position.y = -0.25 + i * 0.18;\n            torch.add(segment);\n          }\n\n          // top lighter band\n          const topBand = box(0.2, 0.12, 0.2, 0xe6d3a3);\n          topBand.position.y = 0.35;\n          torch.add(topBand);\n\n          // === FLAME (layered cubes, pixel look) ===\n          function flameCube(size, y, color) {\n            const m = new THREE.MeshBasicMaterial({\n              color,\n              transparent: false,\n            });\n            const cube = new THREE.Mesh(new THREE.BoxGeometry(size, size, size), m);\n            cube.position.y = y;\n            return cube;\n          }\n\n          const flameGroup = new THREE.Group();\n\n          flameGroup.add(flameCube(0.22, 0.48, 0xffcc33)); // base yellow\n          // flameGroup.add(flameCube(0.22, 0.60, 0xff9933)); // mid orange\n          // flameGroup.add(flameCube(0.10, 0.72, 0xff3300)); // tip red\n\n          // tiny smoke pixels\n          // flameGroup.add(flameCube(0.05, 0.82, 0x333333));\n          // flameGroup.add(flameCube(0.04, 0.88, 0x555555));\n\n          torch.add(flameGroup);\n\n          // Add slight animation to flame\n          torch.userData.flameOriginalY = flameGroup.position.y;\n          torch.userData.flame = flameGroup;\n          torch.userData.animationTime = Math.random() * Math.PI * 1;\n\n          return torch;\n        }\n\n        // Manage active lights for mobile performance\n        function updateActiveLights() {\n          const now = performance.now();\n          if (now - lastLightUpdate < LIGHT_UPDATE_INTERVAL) return;\n          lastLightUpdate = now;\n\n          // Collect all lights with their distances\n          const allLights = [];\n\n          // Add torch lights\n          torchLights.forEach((light, key) => {\n            const [x, y, z] = key.split(',').map(Number);\n            const dx = x + 0.5 - playerPos.x;\n            const dy = y + 0.5 - playerPos.y;\n            const dz = z + 0.5 - playerPos.z;\n            const distSq = dx * dx + dy * dy + dz * dz;\n            allLights.push({ light, distSq, type: 'torch' });\n          });\n\n          // Add lava lights\n          lavaLights.forEach((light, key) => {\n            const [x, y, z] = key.split(',').map(Number);\n            const dx = x + 0.5 - playerPos.x;\n            const dy = y + 0.5 - playerPos.y;\n            const dz = z + 0.5 - playerPos.z;\n            const distSq = dx * dx + dy * dy + dz * dz;\n            allLights.push({ light, distSq, type: 'lava' });\n          });\n\n          // Add furnace lights\n          furnaceLights.forEach((data, key) => {\n            if (data.light) {\n              const [x, y, z] = key.split(',').map(Number);\n              const dx = x + 0.5 - playerPos.x;\n              const dy = y + 0.5 - playerPos.y;\n              const dz = z + 0.5 - playerPos.z;\n              const distSq = dx * dx + dy * dy + dz * dz;\n              allLights.push({ light: data.light, distSq, type: 'furnace' });\n            }\n          });\n\n          // Sort by distance (closest first)\n          allLights.sort((a, b) => a.distSq - b.distSq);\n\n          // Enable closest lights, disable the rest\n          for (let i = 0; i < allLights.length; i++) {\n            const lightData = allLights[i];\n            const shouldBeVisible = i < MAX_ACTIVE_LIGHTS;\n            lightData.light.visible = shouldBeVisible;\n          }\n        }\n\n        // Update torch mesh animations\n        function updateTorchMeshes(dt) {\n          torchMeshes.forEach((torchGroup) => {\n            if (torchGroup.userData.flame) {\n              torchGroup.userData.animationTime += dt * 3;\n              const flame = torchGroup.userData.flame;\n              // Gentle bobbing animation\n              flame.position.y =\n                torchGroup.userData.flameOriginalY +\n                Math.sin(torchGroup.userData.animationTime) * 0.0125;\n              // Slight scale variation for flickering\n              const flicker = 0.9 + Math.sin(torchGroup.userData.animationTime * 1.5) * 0.0065;\n              flame.scale.set(flicker, flicker, flicker);\n            }\n          });\n        }\n\n        // Create sign mesh at position\n        function createSignMesh(x, y, z, rotation = 0) {\n          const sign = new THREE.Group();\n          sign.position.set(x + 0.5, y + 0.25, z + 0.5);\n\n          // Helper to create a box\n          function box(w, h, d, color) {\n            return new THREE.Mesh(\n              new THREE.BoxGeometry(w, h, d),\n              new THREE.MeshLambertMaterial({ color })\n            );\n          }\n\n          // Sign post (vertical stick)\n          const postColors = [0x5a3a1a, 0x6b4423, 0x7a4f28];\n          for (let i = 0; i < 4; i++) {\n            const segment = box(0.12, 0.12, 0.12, postColors[i % postColors.length]);\n            segment.position.y = -0.3 + i * 0.12;\n            sign.add(segment);\n          }\n\n          // Sign board (flat rectangle) - made of planks\n          const boardWidth = 0.7;\n          const boardHeight = 0.4;\n          const boardDepth = 0.08;\n\n          // Use oak sign texture if available\n          const signTexture = assetCache.images['oak_sign'];\n          let boardMaterial;\n          if (signTexture) {\n            const tex = new THREE.Texture(signTexture);\n            tex.magFilter = THREE.NearestFilter;\n            tex.minFilter = THREE.NearestFilter;\n            tex.needsUpdate = true;\n            boardMaterial = new THREE.MeshLambertMaterial({ map: tex });\n          } else {\n            boardMaterial = new THREE.MeshLambertMaterial({ color: 0xc4a76b });\n          }\n\n          const board = new THREE.Mesh(\n            new THREE.BoxGeometry(boardWidth, boardHeight, boardDepth),\n            boardMaterial\n          );\n          board.position.y = 0.35;\n          sign.add(board);\n\n          // Add wooden frame around the board\n          const frameColor = 0x5a3a1a;\n          const frameThickness = 0.04;\n\n          // Top frame\n          const topFrame = box(\n            boardWidth + frameThickness * 2,\n            frameThickness,\n            boardDepth + 0.02,\n            frameColor\n          );\n          topFrame.position.y = 0.35 + boardHeight / 2 + frameThickness / 2;\n          sign.add(topFrame);\n\n          // Bottom frame\n          const bottomFrame = box(\n            boardWidth + frameThickness * 2,\n            frameThickness,\n            boardDepth + 0.02,\n            frameColor\n          );\n          bottomFrame.position.y = 0.35 - boardHeight / 2 - frameThickness / 2;\n          sign.add(bottomFrame);\n\n          // Left frame\n          const leftFrame = box(frameThickness, boardHeight, boardDepth + 0.02, frameColor);\n          leftFrame.position.set(-boardWidth / 2 - frameThickness / 2, 0.35, 0);\n          sign.add(leftFrame);\n\n          // Right frame\n          const rightFrame = box(frameThickness, boardHeight, boardDepth + 0.02, frameColor);\n          rightFrame.position.set(boardWidth / 2 + frameThickness / 2, 0.35, 0);\n          sign.add(rightFrame);\n\n          // Create text planes for displaying the sign message on both sides\n          const textCanvas = document.createElement('canvas');\n          textCanvas.width = 256;\n          textCanvas.height = 128;\n          const textCtx = textCanvas.getContext('2d');\n\n          // Initial empty text\n          textCtx.fillStyle = 'transparent';\n          textCtx.fillRect(0, 0, 256, 128);\n\n          const textTexture = new THREE.CanvasTexture(textCanvas);\n          textTexture.magFilter = THREE.LinearFilter;\n          textTexture.minFilter = THREE.LinearFilter;\n\n          // Front text plane (facing +Z)\n          const textGeometry = new THREE.PlaneGeometry(boardWidth * 0.85, boardHeight * 0.75);\n          const frontTextMaterial = new THREE.MeshBasicMaterial({\n            map: textTexture,\n            transparent: true,\n            side: THREE.FrontSide,\n          });\n          const frontTextPlane = new THREE.Mesh(textGeometry, frontTextMaterial);\n          frontTextPlane.position.set(0, 0.35, boardDepth / 2 + 0.01); // Slightly in front of board\n          sign.add(frontTextPlane);\n\n          // Back text plane (facing -Z) - uses same texture\n          const backTextMaterial = new THREE.MeshBasicMaterial({\n            map: textTexture,\n            transparent: true,\n            side: THREE.FrontSide,\n          });\n          const backTextPlane = new THREE.Mesh(textGeometry.clone(), backTextMaterial);\n          backTextPlane.position.set(0, 0.35, -boardDepth / 2 - 0.01); // Slightly behind board\n          backTextPlane.rotation.y = Math.PI; // Rotate 180 degrees to face the other way\n          sign.add(backTextPlane);\n\n          // Store references for text updates\n          sign.userData.textCanvas = textCanvas;\n          sign.userData.textCtx = textCtx;\n          sign.userData.textTexture = textTexture;\n          sign.userData.frontTextPlane = frontTextPlane;\n          sign.userData.backTextPlane = backTextPlane;\n\n          // Apply rotation to face player\n          sign.rotation.y = rotation;\n\n          return sign;\n        }\n\n        // Update sign text display\n        function updateSignText(signMesh, message) {\n          if (!signMesh || !signMesh.userData.textCanvas) return;\n\n          const canvas = signMesh.userData.textCanvas;\n          const ctx = signMesh.userData.textCtx;\n          const texture = signMesh.userData.textTexture;\n\n          // Clear canvas\n          ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n          if (!message || message.trim() === '') {\n            texture.needsUpdate = true;\n            return;\n          }\n\n          // Draw text with word wrapping\n          ctx.fillStyle = '#000000'; // Dark text color\n          ctx.font = 'bold 48px \"VT323\", monospace';\n          ctx.textAlign = 'center';\n          ctx.textBaseline = 'top';\n\n          // Word wrap the text\n          const maxWidth = 230;\n          const lineHeight = 28;\n          const lines = [];\n          const words = message.split(' ');\n          let currentLine = '';\n\n          for (const word of words) {\n            const testLine = currentLine ? currentLine + ' ' + word : word;\n            const metrics = ctx.measureText(testLine);\n\n            if (metrics.width > maxWidth && currentLine) {\n              lines.push(currentLine);\n              currentLine = word;\n            } else {\n              currentLine = testLine;\n            }\n          }\n          if (currentLine) {\n            lines.push(currentLine);\n          }\n\n          // Limit to 4 lines max\n          const displayLines = lines.slice(0, 4);\n\n          // Calculate starting Y to center text vertically\n          const totalHeight = displayLines.length * lineHeight;\n          let startY = (canvas.height - totalHeight) / 2;\n\n          // Draw each line\n          for (const line of displayLines) {\n            ctx.fillText(line, canvas.width / 2, startY);\n            startY += lineHeight;\n          }\n\n          texture.needsUpdate = true;\n        }\n\n        // Add sign mesh at position\n        function addSignMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          if (signMeshes.has(key)) return; // Already has mesh\n\n          const rotation = signOrientations.get(key) || 0;\n          const signMesh = createSignMesh(x, y, z, rotation);\n          scene.add(signMesh);\n          signMeshes.set(key, signMesh);\n\n          // Update text if message exists\n          const message = signMessages.get(key);\n          if (message) {\n            updateSignText(signMesh, message);\n          }\n        }\n\n        // Remove sign mesh at position\n        function removeSignMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const signMesh = signMeshes.get(key);\n          if (signMesh) {\n            scene.remove(signMesh);\n            signMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => mat.dispose());\n                } else {\n                  child.material.dispose();\n                }\n              }\n            });\n            signMeshes.delete(key);\n          }\n          // Do not remove stored message here, as chunks can be temporarily unloaded\n        }\n\n        // Show sign input UI when placing a sign\n        function showSignInputUI(x, y, z, onConfirm, onCancel) {\n          // Exit pointer lock and pause game\n          try {\n            if (document.pointerLockElement) {\n              document.exitPointerLock?.();\n            }\n          } catch (e) {\n            lib.log('Error exiting pointer lock: ' + e.message);\n          }\n          isPointerLocked = false;\n          isPaused = true;\n\n          const inputDialog = document.createElement('div');\n          inputDialog.id = 'sign-input-dialog';\n          inputDialog.style.cssText = `\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.85);\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            z-index: 250;\n            backdrop-filter: blur(4px);\n          `;\n          inputDialog.innerHTML = `\n            <div style=\"\n              background: linear-gradient(135deg, rgba(139, 90, 43, 0.95), rgba(90, 58, 26, 0.95));\n              padding: 30px;\n              border-radius: 16px;\n              border: 4px solid rgba(196, 167, 107, 0.8);\n              max-width: 90%;\n              width: 400px;\n              box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n            \">\n              <h3 style=\"\n                font-family: 'VT323', monospace;\n                font-size: 32px;\n                color: #c4a76b;\n                margin: 0 0 20px 0;\n                text-align: center;\n                text-shadow: 2px 2px 0 #000;\n              \">\ud83e\udea7 Edit Sign</h3>\n              <textarea id=\"sign-text-input\" placeholder=\"Enter sign message...\" maxlength=\"100\" rows=\"4\" style=\"\n                width: 100%;\n                padding: 14px 16px;\n                font-family: 'VT323', monospace;\n                font-size: 20px;\n                background: rgba(0, 0, 0, 0.4);\n                border: 3px solid rgba(196, 167, 107, 0.6);\n                border-radius: 10px;\n                color: white;\n                outline: none;\n                margin-bottom: 20px;\n                box-sizing: border-box;\n                resize: none;\n              \"></textarea>\n              <div style=\"display: flex; gap: 12px;\">\n                <button id=\"sign-cancel-btn\" style=\"\n                  flex: 1;\n                  padding: 12px;\n                  font-family: 'VT323', monospace;\n                  font-size: 24px;\n                  background: rgba(244, 67, 54, 0.3);\n                  color: #f44336;\n                  border: 2px solid #f44336;\n                  border-radius: 10px;\n                  cursor: pointer;\n                  transition: all 0.2s;\n                \">Cancel</button>\n                <button id=\"sign-confirm-btn\" style=\"\n                  flex: 1;\n                  padding: 12px;\n                  font-family: 'VT323', monospace;\n                  font-size: 24px;\n                  background: rgba(76, 175, 80, 0.3);\n                  color: #4CAF50;\n                  border: 2px solid #4CAF50;\n                  border-radius: 10px;\n                  cursor: pointer;\n                  transition: all 0.2s;\n                \">Place Sign</button>\n              </div>\n            </div>\n          `;\n          gameWorld.appendChild(inputDialog);\n\n          const textInput = document.getElementById('sign-text-input');\n          textInput.focus();\n\n          // Handle confirm button\n          document.getElementById('sign-confirm-btn').addEventListener('click', () => {\n            const message = textInput.value.trim() || '';\n            inputDialog.remove();\n            isPaused = false;\n            onConfirm(message);\n          });\n\n          // Handle cancel button\n          document.getElementById('sign-cancel-btn').addEventListener('click', () => {\n            inputDialog.remove();\n            isPaused = false;\n            onCancel();\n          });\n\n          // Handle Enter key (Shift+Enter for newline)\n          textInput.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter' && !e.shiftKey) {\n              e.preventDefault();\n              document.getElementById('sign-confirm-btn').click();\n            } else if (e.key === 'Escape') {\n              isPaused = false;\n              inputDialog.remove();\n              onCancel();\n            }\n          });\n\n          // Close on background click\n          inputDialog.addEventListener('click', (e) => {\n            if (e.target === inputDialog) {\n              isPaused = false;\n              inputDialog.remove();\n              onCancel();\n            }\n          });\n        }\n\n        // Show sign view UI when interacting with an existing sign\n        function showSignViewUI(x, y, z) {\n          // Exit pointer lock and pause game\n          try {\n            if (document.pointerLockElement) {\n              document.exitPointerLock?.();\n            }\n          } catch (e) {\n            lib.log('Error exiting pointer lock: ' + e.message);\n          }\n          isPointerLocked = false;\n          isPaused = true;\n\n          const key = `${x},${y},${z}`;\n          const message = signMessages.get(key) || '';\n\n          const viewDialog = document.createElement('div');\n          viewDialog.id = 'sign-view-dialog';\n          viewDialog.style.cssText = `\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.75);\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            z-index: 250;\n            backdrop-filter: blur(4px);\n          `;\n          viewDialog.innerHTML = `\n            <div style=\"\n              background: linear-gradient(135deg, rgba(139, 90, 43, 0.95), rgba(90, 58, 26, 0.95));\n              padding: 30px;\n              border-radius: 16px;\n              border: 4px solid rgba(196, 167, 107, 0.8);\n              max-width: 90%;\n              width: 400px;\n              box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n              text-align: center;\n            \">\n              <h3 style=\"\n                font-family: 'VT323', monospace;\n                font-size: 32px;\n                color: #c4a76b;\n                margin: 0 0 20px 0;\n                text-shadow: 2px 2px 0 #000;\n              \">\ud83e\udea7 Sign</h3>\n              <div style=\"\n                background: rgba(0, 0, 0, 0.4);\n                border: 3px solid rgba(196, 167, 107, 0.6);\n                border-radius: 10px;\n                padding: 20px;\n                min-height: 60px;\n                margin-bottom: 20px;\n              \">\n                <p id=\"sign-message-text\" style=\"\n                  font-family: 'VT323', monospace;\n                  font-size: 24px;\n                  color: white;\n                  margin: 0;\n                  white-space: pre-wrap;\n                  word-break: break-word;\n                \">${message || '(Empty sign)'}</p>\n              </div>\n              <div style=\"display: flex; gap: 12px; justify-content: center;\">\n                <button id=\"sign-update-btn\" style=\"\n                  padding: 12px 30px;\n                  font-family: 'VT323', monospace;\n                  font-size: 24px;\n                  background: rgba(33, 150, 243, 0.3);\n                  color: #2196F3;\n                  border: 2px solid #2196F3;\n                  border-radius: 10px;\n                  cursor: pointer;\n                  transition: all 0.2s;\n                \">\u270f\ufe0f Update</button>\n                <button id=\"sign-close-btn\" style=\"\n                  padding: 12px 30px;\n                  font-family: 'VT323', monospace;\n                  font-size: 24px;\n                  background: rgba(76, 175, 80, 0.3);\n                  color: #4CAF50;\n                  border: 2px solid #4CAF50;\n                  border-radius: 10px;\n                  cursor: pointer;\n                  transition: all 0.2s;\n                \">Close</button>\n              </div>\n            </div>\n          `;\n          gameWorld.appendChild(viewDialog);\n\n          // Handle update button - open edit dialog\n          document.getElementById('sign-update-btn').addEventListener('click', () => {\n            viewDialog.remove();\n            isPaused = false;\n\n            // Open the sign input UI to edit the message\n            showSignInputUI(\n              x,\n              y,\n              z,\n              (newMessage) => {\n                // On confirm - update the sign message\n                const signKey = `${x},${y},${z}`;\n                signMessages.set(signKey, newMessage);\n\n                // Update the sign text display\n                const signMesh = signMeshes.get(signKey);\n                if (signMesh) {\n                  updateSignText(signMesh, newMessage);\n                }\n\n                // Sync to other players\n                if (isOnline) {\n                  syncEvent('sign_message', { x: x, y: y, z: z, message: newMessage });\n                }\n\n                showMessage('Sign updated!', '#4CAF50', 1500);\n              },\n              () => {\n                // On cancel - do nothing\n              }\n            );\n          });\n\n          // Handle close button\n          document.getElementById('sign-close-btn').addEventListener('click', () => {\n            isPaused = false;\n            viewDialog.remove();\n          });\n\n          // Close on background click\n          viewDialog.addEventListener('click', (e) => {\n            if (e.target === viewDialog) {\n              isPaused = false;\n              viewDialog.remove();\n            }\n          });\n\n          // Close on Escape key\n          const escHandler = (e) => {\n            if (e.key === 'Escape') {\n              isPaused = false;\n              viewDialog.remove();\n              document.removeEventListener('keydown', escHandler);\n            }\n          };\n          document.addEventListener('keydown', escHandler);\n        }\n\n        // Door helpers\n        function getDoorBaseY(x, y, z) {\n          if (getBlockAt(x, y - 1, z) === BLOCK_TYPES.DOOR) return y - 1;\n          return y;\n        }\n\n        function getDoorBaseKey(x, y, z) {\n          const baseY = getDoorBaseY(x, y, z);\n          return `${x},${baseY},${z}`;\n        }\n\n        function isDoorTopBlock(x, y, z) {\n          return getBlockAt(x, y - 1, z) === BLOCK_TYPES.DOOR;\n        }\n\n        // Bed helpers - beds are 2 blocks long in the Z direction (head at z, foot at z+1)\n        function getBedHeadZ(x, y, z) {\n          // Check if block at z-1 is a bed (meaning this is the foot)\n          if (getBlockAt(x, y, z - 1) === BLOCK_TYPES.BED) return z - 1;\n          return z; // This is the head\n        }\n\n        function getBedHeadKey(x, y, z) {\n          const headZ = getBedHeadZ(x, y, z);\n          return `${x},${y},${headZ}`;\n        }\n\n        function isBedFootBlock(x, y, z) {\n          return getBlockAt(x, y, z - 1) === BLOCK_TYPES.BED;\n        }\n\n        // Create door mesh at position (uses base block as anchor)\n        function createDoorMesh(x, y, z, isOpen = false, rotation = 0) {\n          const door = new THREE.Group();\n\n          // Position door based on rotation (to align with block face)\n          const rotDeg = radToDeg(rotation);\n          if (rotDeg === 0) {\n            door.position.set(x, y + 1.0, z);\n          } else if (rotDeg === 180 || rotDeg === -180) {\n            door.position.set(x + 1.0, y + 1.0, z + 1.0);\n          } else if (rotDeg === -90 || rotDeg === 270) {\n            door.position.set(x + 1, y + 1.0, z);\n          } else if (rotDeg === 90 || rotDeg === -270) {\n            door.position.set(x, y + 1.0, z + 1);\n          } else {\n            // Default fallback\n            door.position.set(x + 0.5, y + 1.0, z + 0.5);\n          }\n\n          // Door panel - thin box (2 blocks tall)\n          const doorWidth = 0.9;\n          const doorHeight = 1.9;\n          const doorDepth = 0.15;\n\n          // Get door texture\n          const doorImg = assetCache.images['door_block'];\n          let doorMaterial;\n          if (doorImg) {\n            const tex = new THREE.Texture(doorImg);\n            tex.magFilter = THREE.NearestFilter;\n            tex.minFilter = THREE.NearestFilter;\n            tex.needsUpdate = true;\n            doorMaterial = new THREE.MeshLambertMaterial({ map: tex });\n          } else {\n            doorMaterial = new THREE.MeshLambertMaterial({ color: 0x8b5a2b });\n          }\n\n          // Create door panel geometry\n          const doorGeo = new THREE.BoxGeometry(doorWidth, doorHeight, doorDepth);\n          const doorPanel = new THREE.Mesh(doorGeo, doorMaterial);\n\n          // Offset pivot to hinge side (left edge) - position door so it rotates from its left edge\n          doorPanel.position.x = doorWidth / 2;\n\n          door.add(doorPanel);\n\n          // Store reference for animations\n          door.userData.panel = doorPanel;\n          door.userData.isOpen = isOpen;\n          door.userData.baseRotation = rotation;\n\n          // Apply base rotation (facing direction) plus open rotation if needed\n          door.rotation.y = rotation + (isOpen ? -Math.PI / 2 : 0);\n\n          return door;\n        }\n\n        // Add door mesh at position (only once per 2-block door)\n        function addDoorMesh(x, y, z) {\n          const baseKey = getDoorBaseKey(x, y, z);\n          if (doorMeshes.has(baseKey)) return;\n\n          const baseY = getDoorBaseY(x, y, z);\n          const isOpen = doorStates.get(baseKey) || false;\n          const rotation = doorOrientations.get(baseKey) || 0;\n          const doorMesh = createDoorMesh(x, baseY, z, isOpen, rotation);\n          scene.add(doorMesh);\n          doorMeshes.set(baseKey, doorMesh);\n        }\n\n        // Remove door mesh at position\n        function removeDoorMesh(x, y, z) {\n          const baseKey = getDoorBaseKey(x, y, z);\n          const doorMesh = doorMeshes.get(baseKey);\n          if (doorMesh) {\n            scene.remove(doorMesh);\n            doorMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => mat.dispose());\n                } else {\n                  child.material.dispose();\n                }\n              }\n            });\n            doorMeshes.delete(baseKey);\n          }\n        }\n\n        // Create chest mesh at position with rotation\n        function createChestMesh(x, y, z, rotation = 0) {\n          const chest = new THREE.Group();\n          chest.position.set(x + 0.5, y + 0.5, z + 0.5);\n\n          // Get chest textures\n          const topTex = blockTextures[BLOCK_TYPES.CHEST]?.top;\n          const sideTex = blockTextures[BLOCK_TYPES.CHEST]?.side;\n          const frontTex = blockTextures[BLOCK_TYPES.CHEST]?.front;\n\n          // Create materials for each face\n          const materials = [\n            new THREE.MeshLambertMaterial({ map: sideTex }), // right (+X)\n            new THREE.MeshLambertMaterial({ map: sideTex }), // left (-X)\n            new THREE.MeshLambertMaterial({ map: topTex }), // top (+Y)\n            new THREE.MeshLambertMaterial({ map: topTex }), // bottom (-Y)\n            new THREE.MeshLambertMaterial({ map: frontTex }), // front (+Z)\n            new THREE.MeshLambertMaterial({ map: sideTex }), // back (-Z)\n          ];\n\n          const geometry = new THREE.BoxGeometry(1, 1, 1);\n          const mesh = new THREE.Mesh(geometry, materials);\n          chest.add(mesh);\n\n          // Apply rotation\n          chest.rotation.y = rotation;\n\n          // Store references\n          chest.userData.innerMesh = mesh;\n          chest.userData.rotation = rotation;\n\n          return chest;\n        }\n\n        // Add chest mesh at position\n        function addChestMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          if (chestMeshes.has(key)) return;\n\n          const rotation = chestOrientations.get(key) || 0;\n          const chestMesh = createChestMesh(x, y, z, rotation);\n          scene.add(chestMesh);\n          chestMeshes.set(key, chestMesh);\n        }\n\n        // Remove chest mesh at position\n        function removeChestMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const chestMesh = chestMeshes.get(key);\n          if (chestMesh) {\n            scene.remove(chestMesh);\n            chestMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => mat.dispose());\n                } else {\n                  child.material.dispose();\n                }\n              }\n            });\n            chestMeshes.delete(key);\n          }\n        }\n\n        // Create furnace mesh at position with rotation\n        function createFurnaceMesh(x, y, z, rotation = 0) {\n          const furnace = new THREE.Group();\n          furnace.position.set(x + 0.5, y + 0.5, z + 0.5);\n\n          // Get furnace textures\n          const topTex = blockTextures[BLOCK_TYPES.FURNACE]?.top;\n          const sideTex = blockTextures[BLOCK_TYPES.FURNACE]?.side;\n          const frontTex = blockTextures[BLOCK_TYPES.FURNACE]?.front;\n\n          // Create materials for each face\n          const materials = [\n            new THREE.MeshLambertMaterial({ map: sideTex }), // right (+X)\n            new THREE.MeshLambertMaterial({ map: sideTex }), // left (-X)\n            new THREE.MeshLambertMaterial({ map: topTex }), // top (+Y)\n            new THREE.MeshLambertMaterial({ map: topTex }), // bottom (-Y)\n            new THREE.MeshLambertMaterial({ map: frontTex }), // front (+Z)\n            new THREE.MeshLambertMaterial({ map: sideTex }), // back (-Z)\n          ];\n\n          const geometry = new THREE.BoxGeometry(1, 1, 1);\n          const mesh = new THREE.Mesh(geometry, materials);\n          furnace.add(mesh);\n\n          // Apply rotation\n          furnace.rotation.y = rotation;\n\n          // Store references\n          furnace.userData.innerMesh = mesh;\n          furnace.userData.rotation = rotation;\n\n          return furnace;\n        }\n\n        // Add furnace mesh at position\n        function addFurnaceMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          if (furnaceMeshes.has(key)) return;\n\n          const rotation = furnaceOrientations.get(key) || 0;\n          const furnaceMesh = createFurnaceMesh(x, y, z, rotation);\n          scene.add(furnaceMesh);\n          furnaceMeshes.set(key, furnaceMesh);\n        }\n\n        // Remove furnace mesh at position\n        function removeFurnaceMesh(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const furnaceMesh = furnaceMeshes.get(key);\n          if (furnaceMesh) {\n            scene.remove(furnaceMesh);\n            furnaceMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) {\n                  child.material.forEach((mat) => mat.dispose());\n                } else {\n                  child.material.dispose();\n                }\n              }\n            });\n            furnaceMeshes.delete(key);\n          }\n        }\n\n        // Calculate rotation to face player from block position\n        function getRotationToFacePlayer(bx, bz) {\n          const dx = playerPos.x - (bx + 0.5);\n          const dz = playerPos.z - (bz + 0.5);\n\n          // Determine which cardinal direction the player is in\n          if (Math.abs(dx) > Math.abs(dz)) {\n            // Player is more to the east or west\n            return dx > 0 ? Math.PI / 2 : -Math.PI / 2; // Face +X or -X\n          } else {\n            // Player is more to the north or south\n            return dz > 0 ? 0 : Math.PI; // Face +Z or -Z\n          }\n        }\n\n        // Toggle door open/closed state\n        function toggleDoor(x, y, z) {\n          const baseKey = getDoorBaseKey(x, y, z);\n          const currentState = doorStates.get(baseKey) || false;\n          const newState = !currentState;\n          doorStates.set(baseKey, newState);\n\n          // Update door mesh rotation (base rotation + open rotation)\n          const doorMesh = doorMeshes.get(baseKey);\n          if (doorMesh) {\n            const baseRotation = doorMesh.userData.baseRotation || 0;\n\n            doorMesh.rotation.y = baseRotation + (newState ? -Math.PI / 2 : 0);\n          }\n\n          // Play sound\n          playSound('block_place_sound', false, 0.4);\n\n          // Sync to other players\n          if (isOnline) {\n            const [bx, by, bz] = baseKey.split(',').map(Number);\n            syncEvent('door_toggle', { x: bx, y: by, z: bz, isOpen: newState });\n          }\n\n          return newState;\n        }\n\n        // Raycaster\n        const raycaster = new THREE.Raycaster();\n        raycaster.far = 6;\n\n        // Highlight mesh\n        const highlightGeometry = new THREE.BoxGeometry(1.01, 1.01, 1.01);\n        const highlightMaterial = new THREE.MeshBasicMaterial({\n          color: 0xffffff,\n          transparent: true,\n          opacity: 0.3,\n          side: THREE.BackSide,\n        });\n        const highlightMesh = new THREE.Mesh(highlightGeometry, highlightMaterial);\n        highlightMesh.visible = false;\n        scene.add(highlightMesh);\n\n        // First-person player arm (unused in 3rd person)\n        let playerArm = null;\n        let playerArmPivot = null;\n        let armSwingProgress = 0;\n        let armSwingDirection = 0; // 0 = idle, 1 = swinging down, -1 = returning\n        let armSwingSpeed = 8; // Speed of swing animation\n        let isPlaceSwing = false; // Track if it's a place animation\n\n        // === 3RD PERSON CAMERA SYSTEM ===\n        const THIRD_PERSON_DISTANCE = 5;\n        const THIRD_PERSON_HEIGHT = 1.5;\n        let localPlayerMesh = null;\n        let localPlayerMixer = null;\n        let localPlayerActions = {};\n        let localPlayerCurrentAction = null;\n        let localPlayerHead = null;\n\n        function createLocalPlayerModel() {\n          const useAlexModel = playerGender === 'female' && alexModelTemplate;\n          const modelTemplate = useAlexModel ? alexModelTemplate : playerModelTemplate;\n          const modelAnimations = useAlexModel ? alexModelAnimations : playerModelAnimations;\n\n          if (!modelTemplate) return;\n\n          localPlayerMesh = new THREE.Group();\n\n          const playerModel = modelTemplate.clone();\n          playerModel.traverse((child) => {\n            if (child.isMesh) {\n              child.castShadow = true;\n              child.receiveShadow = true;\n              if (child.material) {\n                child.material = child.material.clone();\n              }\n            }\n          });\n\n          playerModel.position.y = -0.9;\n          localPlayerMesh.add(playerModel);\n\n          // Set up animations\n          if (modelAnimations && modelAnimations.length > 0) {\n            localPlayerMixer = new THREE.AnimationMixer(playerModel);\n            localPlayerActions = {};\n\n            // Filter head tracks so we can control head manually\n            let headTrackNames = [];\n            for (const clip of modelAnimations) {\n              const headTracks = clip.tracks.filter(t => t.name.toLowerCase().includes('head'));\n              headTrackNames = headTrackNames.concat(headTracks.map(t => t.name));\n            }\n            headTrackNames = [...new Set(headTrackNames)];\n\n            for (const clip of modelAnimations) {\n              const clipName = clip.name.toLowerCase();\n              if (headTrackNames.length > 0) {\n                const filteredTracks = clip.tracks.filter(t => !headTrackNames.includes(t.name));\n                if (filteredTracks.length > 0) {\n                  const filteredClip = new THREE.AnimationClip(clip.name, clip.duration, filteredTracks);\n                  localPlayerActions[clipName] = localPlayerMixer.clipAction(filteredClip);\n                }\n              } else {\n                localPlayerActions[clipName] = localPlayerMixer.clipAction(clip);\n              }\n            }\n\n            // Start with idle\n            if (localPlayerActions['idle']) {\n              localPlayerActions['idle'].play();\n              localPlayerCurrentAction = 'idle';\n            }\n          }\n\n          // Find head bone for pitch rotation\n          localPlayerHead = null;\n          playerModel.traverse((child) => {\n            if (child.name.toLowerCase().includes('head')) {\n              localPlayerHead = child;\n            }\n          });\n\n          localPlayerMesh.position.set(playerPos.x, playerPos.y - 0.8, playerPos.z);\n          scene.add(localPlayerMesh);\n        }\n\n        function rebuildLocalPlayerModel() {\n          if (localPlayerMesh) {\n            if (localPlayerMixer) localPlayerMixer.stopAllAction();\n            scene.remove(localPlayerMesh);\n            localPlayerMesh.traverse((child) => {\n              if (child.geometry) child.geometry.dispose();\n              if (child.material) {\n                if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());\n                else child.material.dispose();\n              }\n            });\n            localPlayerMesh = null;\n            localPlayerMixer = null;\n            localPlayerActions = {};\n            localPlayerCurrentAction = null;\n            localPlayerHead = null;\n          }\n          createLocalPlayerModel();\n        }\n\n        function updateLocalPlayerModel(dt) {\n          if (!localPlayerMesh) return;\n\n          // Position at player feet (same offset as other players: -0.8 group + -0.9 model = -1.7 = ground)\n          localPlayerMesh.position.set(playerPos.x, playerPos.y - 0.8, playerPos.z);\n\n          // Rotate body to face yaw direction\n          localPlayerMesh.rotation.y = playerYaw;\n\n          // Calculate movement speed for animation switching\n          const movementSpeed = Math.sqrt(playerVelocity.x * playerVelocity.x + playerVelocity.z * playerVelocity.z);\n          const isMoving = movementSpeed > 0.5;\n\n          // Update mixer\n          if (localPlayerMixer) {\n            localPlayerMixer.update(dt);\n\n            if (isMoving && localPlayerCurrentAction !== 'walk') {\n              const walkAction = localPlayerActions['walk'];\n              const idleAction = localPlayerActions['idle'];\n              if (walkAction) {\n                if (idleAction) idleAction.fadeOut(0.2);\n                walkAction.reset().fadeIn(0.2).play();\n                localPlayerCurrentAction = 'walk';\n              }\n            } else if (!isMoving && localPlayerCurrentAction !== 'idle') {\n              const walkAction = localPlayerActions['walk'];\n              const idleAction = localPlayerActions['idle'];\n              if (idleAction) {\n                if (walkAction) walkAction.fadeOut(0.2);\n                idleAction.reset().fadeIn(0.2).play();\n                localPlayerCurrentAction = 'idle';\n              }\n            }\n          }\n\n          // Head pitch (looking up/down)\n          if (localPlayerHead) {\n            localPlayerHead.rotation.x = playerPitch * 0.7; // Slightly damped\n          }\n        }\n\n        function updateCamera() {\n          if (isFirstPerson) {\n            camera.position.copy(playerPos);\n            camera.rotation.order = 'YXZ';\n            camera.rotation.y = playerYaw;\n            camera.rotation.x = playerPitch;\n            if (localPlayerMesh) localPlayerMesh.visible = false;\n            if (playerArmPivot) playerArmPivot.visible = true;\n          } else {\n            updateThirdPersonCamera();\n            if (localPlayerMesh) localPlayerMesh.visible = true;\n            if (playerArmPivot) playerArmPivot.visible = false;\n          }\n        }\n\n        function updateThirdPersonCamera() {\n          // Direction behind the player\n          const behindX = Math.sin(playerYaw) * Math.cos(playerPitch);\n          const behindY = -Math.sin(playerPitch);\n          const behindZ = Math.cos(playerYaw) * Math.cos(playerPitch);\n\n          let finalDist = THIRD_PERSON_DISTANCE;\n\n          // Camera collision - step along the ray from player to desired camera pos\n          for (let d = 0.5; d <= THIRD_PERSON_DISTANCE; d += 0.5) {\n            const checkX = Math.floor(playerPos.x + behindX * d);\n            const checkY = Math.floor(playerPos.y + behindY * d + THIRD_PERSON_HEIGHT * 0.5);\n            const checkZ = Math.floor(playerPos.z + behindZ * d);\n\n            if (checkY >= 0 && checkY < worldHeight) {\n              const bt = getBlockAt(checkX, checkY, checkZ);\n              if (bt !== BLOCK_TYPES.AIR && bt !== BLOCK_TYPES.WATER && bt !== BLOCK_TYPES.WATER_FLOW &&\n                  bt !== BLOCK_TYPES.LAVA && bt !== BLOCK_TYPES.LAVA_FLOW && bt !== BLOCK_TYPES.TORCH &&\n                  bt !== BLOCK_TYPES.FLOWER && bt !== BLOCK_TYPES.RICE_PLANT) {\n                finalDist = Math.max(1.5, d - 0.5);\n                break;\n              }\n            }\n          }\n\n          const camX = playerPos.x + behindX * finalDist;\n          const camY = playerPos.y + behindY * finalDist + THIRD_PERSON_HEIGHT;\n          const camZ = playerPos.z + behindZ * finalDist;\n\n          camera.position.set(camX, camY, camZ);\n          camera.rotation.order = 'YXZ';\n          camera.rotation.y = playerYaw;\n          camera.rotation.x = playerPitch;\n        }\n\n        function createPlayerArm() {\n          // Create arm pivot point (attached to camera)\n          playerArmPivot = new THREE.Group();\n\n          // Get Steve skin for arm texture\n          const skin = assetCache.images['steve_skin'];\n\n          function getSubTexture(image, x, y, w, h, fullW, fullH) {\n            const tex = new THREE.Texture(image);\n            tex.repeat.set(w / fullW, h / fullH);\n            tex.offset.set(x / fullW, 1 - (y + h) / fullH);\n            tex.magFilter = THREE.NearestFilter;\n            tex.minFilter = THREE.NearestFilter;\n            tex.needsUpdate = true;\n            return tex;\n          }\n\n          // Arm materials using Steve skin right arm section\n          let armMaterials;\n          if (skin) {\n            armMaterials = [\n              new THREE.MeshLambertMaterial({\n                map: getSubTexture(skin, 40, 20, 4, 12, 64, 64),\n              }), // right\n              new THREE.MeshLambertMaterial({\n                map: getSubTexture(skin, 48, 20, 4, 12, 64, 64),\n              }), // left\n              new THREE.MeshLambertMaterial({\n                map: getSubTexture(skin, 44, 16, 4, 4, 64, 64),\n              }), // top\n              new THREE.MeshLambertMaterial({\n                map: getSubTexture(skin, 48, 16, 4, 4, 64, 64),\n              }), // bottom\n              new THREE.MeshLambertMaterial({\n                map: getSubTexture(skin, 44, 20, 4, 12, 64, 64),\n              }), // front\n              new THREE.MeshLambertMaterial({\n                map: getSubTexture(skin, 52, 20, 4, 12, 64, 64),\n              }), // back\n            ];\n          } else {\n            // Fallback to skin-colored arm\n            const skinMat = new THREE.MeshLambertMaterial({ color: 0xd4a574 });\n            armMaterials = [skinMat, skinMat, skinMat, skinMat, skinMat, skinMat];\n          }\n\n          // Create arm geometry - longer and positioned for first-person view\n          const armGeometry = new THREE.BoxGeometry(0.25, 0.6, 0.25);\n          armGeometry.translate(0, -0.3, 0); // Pivot from shoulder\n\n          playerArm = new THREE.Mesh(armGeometry, armMaterials);\n          playerArmPivot.add(playerArm);\n\n          // Position arm pivot relative to camera\n          // Bottom-right of screen, extending forward\n          playerArmPivot.position.set(0.4, -0.35, -0.5);\n\n          camera.add(playerArmPivot);\n        }\n\n        function triggerArmSwing(isPlace = false) {\n          if (armSwingDirection === 0) {\n            armSwingDirection = 1;\n            armSwingProgress = 0;\n            isPlaceSwing = isPlace;\n            armSwingSpeed = isPlace ? 12 : 8; // Place is faster\n          }\n          // Trigger swing animation on local 3rd person model\n          if (localPlayerMesh && localPlayerMixer && localPlayerActions) {\n            const swingAction = localPlayerActions['holding-right-'] || localPlayerActions['holding-right'] || localPlayerActions['swing'];\n            if (swingAction) {\n              swingAction.reset();\n              swingAction.setLoop(THREE.LoopOnce, 1);\n              swingAction.clampWhenFinished = true;\n              swingAction.play();\n              const onFinish = () => {\n                localPlayerMixer.removeEventListener('finished', onFinish);\n                if (localPlayerCurrentAction && localPlayerActions[localPlayerCurrentAction]) {\n                  localPlayerActions[localPlayerCurrentAction].reset().fadeIn(0.1).play();\n                }\n              };\n              localPlayerMixer.addEventListener('finished', onFinish);\n            }\n          }\n        }\n\n        function updatePlayerArm(dt) {\n          if (!playerArmPivot) return;\n\n          // Mining continuous swing animation\n          if (isMining && targetBlock) {\n            // Continuous mining swing\n            armSwingProgress += dt * 6;\n            const swing = Math.sin(armSwingProgress * Math.PI) * 0.5;\n            playerArmPivot.rotation.x = -0.3 + swing;\n            playerArmPivot.rotation.z = Math.sin(armSwingProgress * Math.PI * 2) * 0.1;\n          } else if (armSwingDirection !== 0) {\n            // Single swing animation (for placing)\n            armSwingProgress += dt * armSwingSpeed;\n\n            if (armSwingDirection === 1) {\n              // Swing forward/down\n              const swingAngle = Math.min(armSwingProgress, 1) * 0.8;\n              playerArmPivot.rotation.x = -swingAngle;\n\n              if (armSwingProgress >= 1) {\n                armSwingDirection = -1;\n                armSwingProgress = 0;\n              }\n            } else {\n              // Return to idle\n              const returnAngle = (1 - Math.min(armSwingProgress, 1)) * 0.8;\n              playerArmPivot.rotation.x = -returnAngle;\n\n              if (armSwingProgress >= 1) {\n                armSwingDirection = 0;\n                armSwingProgress = 0;\n                playerArmPivot.rotation.x = 0;\n              }\n            }\n          } else {\n            // Idle bob animation\n            const idleBob = Math.sin(performance.now() * 0.002) * 0.02;\n            playerArmPivot.rotation.x = idleBob;\n            playerArmPivot.rotation.z = 0;\n          }\n        }\n\n        // UI Elements\n        function createUI() {\n          // Crosshair\n          const crosshair = document.createElement('div');\n          crosshair.className = 'crosshair';\n          gameWorld.appendChild(crosshair);\n\n          // Mining progress\n          const miningProgressEl = document.createElement('div');\n          miningProgressEl.className = 'mining-progress';\n          miningProgressEl.innerHTML = '<div class=\"mining-progress-bar\"></div>';\n          gameWorld.appendChild(miningProgressEl);\n\n          // Hotbar\n          const hotbar = document.createElement('div');\n          hotbar.className = 'hotbar';\n\n          for (let i = 0; i < 9; i++) {\n            const slot = document.createElement('div');\n            slot.className = 'hotbar-slot';\n            slot.dataset.slot = i;\n\n            const slotBg =\n              assetCache.images[i === selectedSlot ? 'hotbar_slot_selected' : 'hotbar_slot_bg'];\n            if (slotBg) {\n              slot.style.backgroundImage = `url(${slotBg.src})`;\n            } else {\n              slot.style.background =\n                i === selectedSlot ? 'rgba(255,215,0,0.5)' : 'rgba(60,60,60,0.8)';\n              slot.style.border = i === selectedSlot ? '2px solid gold' : '2px solid #666';\n              slot.style.borderRadius = '4px';\n            }\n\n            // Block icon\n            const icon = document.createElement('img');\n            icon.className = 'hotbar-slot-icon';\n            updateSlotIcon(icon, inventory[i].type);\n            slot.appendChild(icon);\n\n            // Count\n            const count = document.createElement('span');\n            count.className = 'hotbar-slot-count';\n            count.textContent = inventory[i].count > 0 ? inventory[i].count : '';\n            slot.appendChild(count);\n\n            slot.addEventListener('click', () => selectSlot(i));\n            slot.addEventListener('touchend', (e) => {\n              e.preventDefault();\n              selectSlot(i);\n            });\n\n            hotbar.appendChild(slot);\n          }\n          gameWorld.appendChild(hotbar);\n\n          window.createRoom = async function (roomName, maxPlayers) {\n            if (!roomName) {\n              console.log('Please enter a room name');\n              return;\n            }\n\n            try {\n              const result = await roomManager.createRoom(roomName, {\n                maxPlayers,\n              });\n              if (result.success) {\n                console.log(`Room \"${roomName}\" created successfully`);\n                listRooms();\n              } else {\n                console.log('Failed to create room');\n              }\n            } catch (error) {\n              console.log('Error creating room: ' + error.message);\n            }\n          };\n\n          window.listRooms = async function () {\n            try {\n              await roomManager.initialize();\n              const rooms = await roomManager.listRooms();\n              const roomListEl = document.getElementById('worlds-list');\n\n              // console.log(rooms);\n\n              let html = '';\n              let roomCount = 0;\n              for (const room of rooms) {\n                const playerCount = room.playerCount;\n                const updateTime = room.updatedTime;\n                if (playerCount > 0 && updateTime < 10) {\n                  roomCount++;\n                  html += `<div class=\"world-item\" data-world=\"${room.name}\">\n                          <div class=\"world-info\">\n                              <div class=\"world-name\">${room.name}</div>\n                          </div>\n                          <div class=\"world-stats\">\n                              <div class=\"world-players\">\ud83d\udc65 ${playerCount}/20  ${updateTime} mins ago</div>\n                          </div>\n                      </div>\n                    </div>`;\n                }\n              }\n              if (roomCount === 0) {\n                roomListEl.innerHTML = '<div style=\"color:white;\">No rooms found</div>';\n                return;\n              }\n              console.log('active rooms found: ', roomCount);\n              if (roomListEl.innerHTML != html) {\n                roomListEl.innerHTML = html;\n              }\n\n              // Remove inactive and empty rooms\n              if (rooms.length >= 100) {\n                console.log('removing empty rooms: ', roomCount);\n                setTimeout(() => removeEmptyRooms(), 3000);\n              }\n            } catch (error) {\n              console.log('Error listing rooms: ' + error.message);\n            }\n          };\n\n          async function cleanRooms() {\n            const rooms = await roomManager.listRooms();\n\n            const emptyRooms = rooms.filter(\n              (room) => room.playerCount === 0 || room.updatedTime >= 10\n            );\n\n            await Promise.all(emptyRooms.map((room) => roomManager.removeEmptyRoom(room.name)));\n\n            console.log(`Removed ${emptyRooms.length} empty room(s)`);\n          }\n\n          window.removeEmptyRooms = async function () {\n            try {\n              await cleanRooms();\n              await listRooms();\n            } catch (error) {\n              console.log('Error removing empty rooms: ' + error.message);\n            }\n          };\n\n          window.joinRoom = async function (roomName) {\n            if (!roomName) {\n              console.log('Please enter a room name');\n              return;\n            }\n\n            try {\n              const result = await roomManager.joinRoom(roomName);\n              if (result.success) {\n                currentRoomName = roomName;\n                currentWorldDisplay.textContent = `\ud83c\udf0d ${t('worldLabel')}: ${roomName}`;\n                const chatRoomNameEl = document.getElementById('chat-room-name');\n                if (chatRoomNameEl) {\n                  chatRoomNameEl.textContent = `Room: ${roomName}`;\n                }\n\n                // Clear chat history when entering a new room\n                const chatMessagesEl = document.getElementById('chat-messages');\n                if (chatMessagesEl) {\n                  chatMessagesEl.innerHTML = '';\n                }\n\n                console.log(`Joined room \"${roomName}\" successfully`);\n\n                lobby = new Lobby(\n                  window.gameConfig.firebaseConfig,\n                  `${config.firebasePrefix}/rooms/${roomName}`,\n                  handleRecievedData,\n                  () => {\n                    resetWorld();\n                    justJoined = true;\n                  }\n                );\n\n                window.lobby = lobby;\n\n                // Broadcast nickname when joining a room\n                if (myPlayerName) {\n                  syncEvent('nickname', { name: myPlayerName });\n                }\n\n                // Teleport player to spawn area when joining a room\n                if (spawnPoint) {\n                  playerPos.copy(spawnPoint);\n                  playerYaw = 0;\n                  playerPitch = 0;\n                  playerVelocity.set(0, 0, 0);\n                  camera.position.copy(playerPos);\n                  camera.rotation.order = 'YXZ';\n                  camera.rotation.y = playerYaw;\n                  camera.rotation.x = playerPitch;\n                }\n              } else {\n                console.log(result.error || 'Failed to join room');\n              }\n            } catch (error) {\n              console.log('Error joining room: ' + error.message);\n            }\n          };\n\n          window.leaveRoom = async function () {\n            if (lobby) {\n              lobby.leaveRoom();\n              lobby = null;\n            }\n\n            if (currentRoomName) {\n              // clear players\n              otherPlayers.forEach((data, pid) => {\n                playerTargetPositions.delete(pid);\n                otherPlayers.delete(pid);\n                removeOtherPlayerMesh(pid);\n              });\n\n              await roomManager.leaveRoom(currentRoomName);\n              currentRoomName = null;\n              currentWorldDisplay.textContent = `\ud83c\udf0d ${t('worldLabel')}: ${t('none')}`;\n              const chatRoomNameEl = document.getElementById('chat-room-name');\n              if (chatRoomNameEl) {\n                chatRoomNameEl.textContent = `Room: ${t('none')}`;\n              }\n              console.log('Left room successfully');\n            }\n          };\n\n          // Quick Chat bar\n          const quickChat = document.createElement('div');\n          quickChat.className = 'quick-chat';\n          window.quickEmojis = ['\ud83d\ude00', '\ud83d\udc4d', '\u26a0\ufe0f', '\ud83c\udfc3', '\ud83d\udcaa'];\n          const quickTexts = ['Nice!', 'Thanks!', 'Danger!', 'Come here!', 'Good job!'];\n          quickEmojis.forEach((emoji, i) => {\n            const btn = document.createElement('button');\n            btn.className = 'quick-chat-btn';\n            btn.textContent = emoji;\n            btn.title = quickTexts[i] + (!isTouchDevice ? ` (Shift+${i + 1})` : '');\n            btn.onclick = () => {\n              // Show emoji above own player\n              window.addChatMessage('You', emoji, true);\n              // Sync emoji to other players\n              if (isOnline) syncEvent('emoji', { emojiIndex: quickEmojis.indexOf(emoji) });\n              if (typeof window.showEmojiFeedback === 'function') window.showEmojiFeedback(emoji);\n            };\n            quickChat.appendChild(btn);\n          });\n\n          // Text chat toggle button (chat hidden by default)\n          const chatToggleBtn = document.createElement('button');\n          chatToggleBtn.className = 'quick-chat-btn chat-toggle-btn';\n          chatToggleBtn.id = 'chat-toggle-btn';\n          chatToggleBtn.title = 'Toggle text chat';\n\n          const chatToggleLabel = document.createElement('span');\n          chatToggleLabel.id = 'chat-toggle-label';\n          chatToggleLabel.textContent = '\ud83d\udcac Chat';\n          chatToggleBtn.appendChild(chatToggleLabel);\n\n          const chatNotifyDot = document.createElement('span');\n          chatNotifyDot.className = 'chat-notify-dot';\n          chatNotifyDot.id = 'chat-notify-dot';\n          chatToggleBtn.appendChild(chatNotifyDot);\n\n          let hasUnreadChat = false;\n          function updateChatToggleUI(isHidden) {\n            if (chatToggleLabel) {\n              chatToggleLabel.textContent = isHidden ? '\ud83d\udcac Chat' : '\u274c Close';\n            }\n            if (chatNotifyDot) {\n              chatNotifyDot.classList.toggle('visible', hasUnreadChat && isHidden);\n            }\n          }\n\n          function toggleTextChatWindow(forceOpen = false) {\n            const chatWindowEl = document.getElementById('chat-window');\n            if (!chatWindowEl) return;\n\n            let isHidden;\n            if (forceOpen) {\n              chatWindowEl.classList.remove('hidden');\n              isHidden = false;\n            } else {\n              isHidden = chatWindowEl.classList.toggle('hidden');\n            }\n\n            if (!isHidden) {\n              hasUnreadChat = false;\n              const messagesEl = document.getElementById('chat-messages');\n              if (messagesEl) {\n                // Ensure latest messages are visible whenever chat is opened\n                requestAnimationFrame(() => {\n                  messagesEl.scrollTop = messagesEl.scrollHeight;\n                });\n              }\n              const inputEl = document.getElementById('chat-input-field');\n              if (inputEl) {\n                setTimeout(() => {\n                  inputEl.focus();\n                  inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length);\n                }, 50);\n              }\n            }\n            updateChatToggleUI(isHidden);\n          }\n\n          chatToggleBtn.onclick = () => {\n            toggleTextChatWindow(false);\n          };\n          quickChat.appendChild(chatToggleBtn);\n\n          // Gender toggle button\n          const genderBtn = document.createElement('button');\n          genderBtn.className = 'quick-chat-btn';\n          genderBtn.id = 'gender-toggle-btn';\n          genderBtn.textContent = '\ud83d\udc68'; // Start with male (Steve)\n          genderBtn.title = 'Switch Gender' + (!isTouchDevice ? ' (G)' : '');\n          genderBtn.style.background = 'rgba(100, 149, 237, 0.3)'; // Cornflower blue tint\n          genderBtn.style.borderColor = 'rgba(100, 149, 237, 0.5)';\n          genderBtn.onclick = () => {\n            togglePlayerGender();\n          };\n          quickChat.appendChild(genderBtn);\n\n          gameWorld.appendChild(quickChat);\n\n          // Text Chat Window (hidden by default, toggled by chat button)\n          const chatWindow = document.createElement('div');\n          chatWindow.className = 'chat-window hidden';\n          chatWindow.id = 'chat-window';\n\n          // Mobile header\n          const chatHeader = document.createElement('div');\n          chatHeader.className = 'chat-header';\n          chatHeader.innerHTML = `\n            <span>\ud83d\udcac Chat</span>\n            <button class=\"chat-close-btn\" id=\"chat-close-modal-btn\">\u2715</button>\n          `;\n          chatWindow.appendChild(chatHeader);\n\n          const chatRoomName = document.createElement('div');\n          chatRoomName.className = 'chat-room-name';\n          chatRoomName.id = 'chat-room-name';\n          chatRoomName.textContent = `Room: ${currentRoomName || t('none')}`;\n\n          const chatMessages = document.createElement('div');\n          chatMessages.className = 'chat-messages';\n          chatMessages.id = 'chat-messages';\n\n          const chatInputWrap = document.createElement('div');\n          chatInputWrap.className = 'chat-input';\n\n          const chatInput = document.createElement('input');\n          chatInput.type = 'text';\n          chatInput.id = 'chat-input-field';\n          chatInput.placeholder = isTouchDevice\n            ? 'Type message...'\n            : 'Type message and press Enter...';\n          chatInput.maxLength = 120;\n\n          const sendBtn = document.createElement('button');\n          sendBtn.textContent = '\u27a4';\n          sendBtn.className = 'chat-send-btn';\n\n          chatInputWrap.appendChild(chatInput);\n          chatInputWrap.appendChild(sendBtn);\n\n          chatWindow.appendChild(chatRoomName);\n          chatWindow.appendChild(chatMessages);\n          chatWindow.appendChild(chatInputWrap);\n          gameWorld.appendChild(chatWindow);\n\n          const closeChatWindow = () => {\n            chatWindow.classList.add('hidden');\n            updateChatToggleUI(true);\n          };\n\n          window.toggleTextChatWindow = toggleTextChatWindow;\n\n          const closeBtn = document.getElementById('chat-close-modal-btn');\n          if (closeBtn) closeBtn.onclick = closeChatWindow;\n\n          const sendChatMessage = () => {\n            const text = chatInput.value.trim();\n            if (!text) return;\n            addChatMessage(myPlayerName || 'You', text, true);\n            if (isOnline && lobby) {\n              syncChatMessage(text);\n            }\n            chatInput.value = '';\n          };\n\n          sendBtn.onclick = sendChatMessage;\n\n          chatInput.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter') {\n              e.preventDefault();\n              sendChatMessage();\n            }\n            e.stopPropagation();\n          });\n\n          chatInput.addEventListener('focus', () => {\n            try {\n              if (document.pointerLockElement) {\n                document.exitPointerLock?.();\n              }\n            } catch (err) {}\n            isPointerLocked = false;\n          });\n\n          // Instructions\n          const instructions = document.createElement('div');\n          instructions.className = 'instructions';\n          instructions.id = 'instructions';\n          instructions.textContent = isTouchDevice\n            ? t('instructionsTouch')\n            : t('instructionsDesktop');\n          // Apply RTL for Arabic/Urdu\n          if (isRTL()) {\n            instructions.style.direction = 'rtl';\n          }\n          gameWorld.appendChild(instructions);\n\n          // Time indicator\n          const timeIndicator = document.createElement('div');\n          timeIndicator.className = 'time-indicator';\n          timeIndicator.id = 'time-indicator';\n          timeIndicator.textContent = '\u2600\ufe0f Day';\n          gameWorld.appendChild(timeIndicator);\n\n          // Health bar (hearts)\n          const healthBar = document.createElement('div');\n          healthBar.className = 'health-bar';\n          healthBar.id = 'health-bar';\n          // Create 10 hearts (each heart = 2 HP, max 20 HP)\n          let heartsHTML = '';\n          for (let i = 0; i < 10; i++) {\n            heartsHTML += `<div class=\"heart full\" data-heart=\"${i}\">\u2665</div>`;\n          }\n          healthBar.innerHTML = heartsHTML;\n          gameWorld.appendChild(healthBar);\n\n          // Hunger bar (drumsticks)\n          const hungerBar = document.createElement('div');\n          hungerBar.className = 'hunger-bar';\n          hungerBar.id = 'hunger-bar';\n          // Create 10 drumsticks (each drumstick = 2 hunger, max 20 hunger)\n          let drumsticksHTML = '';\n          for (let i = 0; i < 10; i++) {\n            drumsticksHTML += `<div class=\"drumstick full\" data-drumstick=\"${i}\">\ud83c\udf56</div>`;\n          }\n          hungerBar.innerHTML = drumsticksHTML;\n          gameWorld.appendChild(hungerBar);\n\n          // Coordinates display\n          const coordsDisplay = document.createElement('div');\n          coordsDisplay.className = 'coords-display';\n          coordsDisplay.id = 'coords-display';\n          coordsDisplay.textContent = 'X: 0 Y: 0 Z: 0';\n          gameWorld.appendChild(coordsDisplay);\n\n          // Checkpoint progress display\n          const checkpointDisplay = document.createElement('div');\n          checkpointDisplay.id = 'checkpoint-display';\n          checkpointDisplay.style.cssText = `\n            position: absolute;\n            top: calc(var(--space-sm) + 90px);\n            left: var(--space-sm);\n            background: rgba(0, 0, 0, 0.65);\n            color: #4CAF50;\n            padding: 10px 16px;\n            border-radius: 10px;\n            font-family: \"VT323\", monospace;\n            font-size: 22px;\n            z-index: 20;\n            border: 2px solid rgba(76, 175, 80, 0.3);\n            min-width: 180px;\n            text-align: left;\n          `;\n          checkpointDisplay.textContent = '\ud83c\udfc1 Checkpoint: 0/' + (obbyCheckpoints.length - 1);\n          gameWorld.appendChild(checkpointDisplay);\n\n          // Version display\n          const versionDisplay = document.createElement('div');\n          versionDisplay.className = 'version-display';\n          versionDisplay.id = 'version-display';\n          versionDisplay.textContent = 'v' + (config.version || '1.0.0');\n          versionDisplay.style.cssText = `\n            position: absolute;\n            bottom: var(--space-sm);\n            right: var(--space-sm);\n            background: rgba(0, 0, 0, 0.5);\n            color: rgba(255, 255, 255, 0.6);\n            padding: 6px 12px;\n            border-radius: 8px;\n            font-family: \"Inter\", sans-serif;\n            font-size: 14px;\n            z-index: 20;\n            pointer-events: none;\n          `;\n          gameWorld.appendChild(versionDisplay);\n\n          // Bag/Inventory button with hint (visible for all devices)\n          const bagButton = document.createElement('button');\n          bagButton.id = 'bag-button';\n          bagButton.innerHTML = '\ud83c\udf92<span style=\"font-size: 14px; margin-left: 4px;\">(E)</span>';\n          bagButton.title = 'Open Inventory (E)';\n          bagButton.style.cssText = `\n            position: absolute;\n            top: calc(var(--space-sm) + 200px);\n            left: var(--space-sm);\n            background: rgba(0, 0, 0, 0.65);\n            color: white;\n            padding: 10px 16px;\n            border-radius: 10px;\n            font-family: \"VT323\", monospace;\n            font-size: 28px;\n            z-index: 20;\n            border: 2px solid rgba(139, 90, 43, 0.6);\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            transition: all 0.2s;\n          `;\n          bagButton.addEventListener('click', () => toggleInventory());\n          bagButton.addEventListener('touchend', (e) => {\n            e.preventDefault();\n            toggleInventory();\n          });\n          bagButton.addEventListener('mouseenter', () => {\n            bagButton.style.background = 'rgba(139, 90, 43, 0.8)';\n            bagButton.style.transform = 'scale(1.05)';\n          });\n          bagButton.addEventListener('mouseleave', () => {\n            bagButton.style.background = 'rgba(0, 0, 0, 0.65)';\n            bagButton.style.transform = 'scale(1)';\n          });\n          gameWorld.appendChild(bagButton);\n\n          // Current World display\n          const currentWorldDisplay = document.createElement('div');\n          currentWorldDisplay.className = 'current-world-display';\n          currentWorldDisplay.id = 'current-world-display';\n          const worldName = config.firebasePrefix || 'My World';\n          currentWorldDisplay.textContent = `\ud83c\udf0d ${t('worldLabel')}: ${t('none')}`;\n          currentWorldDisplay.style.display = config.showWorldsUI ? 'block' : 'none';\n          gameWorld.appendChild(currentWorldDisplay);\n\n          // Player Count\n          const playerCount = document.createElement('div');\n          playerCount.className = 'player-count';\n          playerCount.id = 'player-count';\n          playerCount.textContent = t('playersOnline', { count: 1 });\n          gameWorld.appendChild(playerCount);\n\n          // Worlds Button\n          const worldsButton = document.createElement('button');\n          worldsButton.className = 'worlds-button';\n          worldsButton.id = 'worlds-button';\n          worldsButton.innerHTML = isTouchDevice ? `\ud83c\udf0d ${t('worlds')}` : `\ud83c\udf0d ${t('worlds')} (L)`;\n          worldsButton.title = `${t('browseWorlds')} (L)`;\n          worldsButton.addEventListener('click', () => toggleWorldsModal(true));\n          worldsButton.style.display = config.showWorldsUI ? 'flex' : 'none';\n          gameWorld.appendChild(worldsButton);\n\n          // Worlds Modal\n          const worldsModal = document.createElement('div');\n          worldsModal.className = 'worlds-modal';\n          worldsModal.id = 'worlds-modal';\n\n          // Simulated world list data\n          const simulatedWorlds = [\n            {\n              name: \"Steve's Survival\",\n              host: 'SteveBuilder',\n              players: 12,\n              maxPlayers: 20,\n              ping: 45,\n            },\n            {\n              name: 'Creative Paradise',\n              host: 'BlockMaster99',\n              players: 8,\n              maxPlayers: 15,\n              ping: 32,\n            },\n            {\n              name: 'PvP Arena',\n              host: 'CraftWarrior',\n              players: 24,\n              maxPlayers: 30,\n              ping: 78,\n            },\n            {\n              name: 'Peaceful Build',\n              host: 'ZenBuilder',\n              players: 3,\n              maxPlayers: 10,\n              ping: 22,\n            },\n            {\n              name: 'Mega City Project',\n              host: 'ArchitectPro',\n              players: 18,\n              maxPlayers: 25,\n              ping: 56,\n            },\n            {\n              name: 'Survival Challenge',\n              host: 'HardcoreHero',\n              players: 6,\n              maxPlayers: 12,\n              ping: 41,\n            },\n          ];\n\n          worldsModal.innerHTML = `\n                        <div class=\"worlds-modal-content\">\n                            <div class=\"worlds-modal-header\">\n                                <h2 class=\"worlds-modal-title\">\ud83c\udf0d ${t('browseWorlds')}</h2>\n                                <button class=\"worlds-modal-close\" id=\"worlds-close-btn\">\u2715</button>\n                            </div>\n                            <div class=\"worlds-section-title\">\ud83d\udc64 ${t('yourNickname')}</div>\n                            <div id=\"nickname-row\" style=\"display:flex; align-items:center; justify-content: space-between; gap: 10px; margin-bottom: 12px; background: rgba(255,255,255,0.05); border: 2px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 12px 14px;\">\n                                <div id=\"nickname-display\" style=\"font-family: 'VT323', monospace; font-size: 26px; color: white;\">Player</div>\n                                <button id=\"update-nickname-btn\" style=\"background: rgba(255, 193, 7, 0.2); color: #FFC107; border: 2px solid #FFC107; border-radius: 10px; padding: 8px 14px; font-family: 'VT323', monospace; font-size: 20px; cursor: pointer;\">\u270f\ufe0f ${t('update')}</button>\n                            </div>\n                            <div class=\"worlds-section-title\">\ud83c\udfae ${t('yourWorld')}</div>\n                            <button class=\"host-world-btn\" id=\"host-world-btn\">\n                                \u2795 ${t('hostWorld')}\n                            </button>\n                            <div class=\"worlds-section-title\">\ud83d\udce1 ${t('availableWorlds')}</div>\n                            <div class=\"worlds-list\" id=\"worlds-list\">\n                                ${simulatedWorlds\n                                  .map(\n                                    (world) => `\n                                    <div class=\"world-item\" data-world=\"${world.name}\">\n                                        <div class=\"world-info\">\n                                            <div class=\"world-name\">${world.name}</div>\n                                            <div class=\"world-host\">Hosted by ${world.host}</div>\n                                        </div>\n                                        <div class=\"world-stats\">\n                                            <div class=\"world-players\">\ud83d\udc65 ${world.players}/${world.maxPlayers}</div>\n                                            <div class=\"world-ping\">\ud83c\udfd3 ${world.ping}ms</div>\n                                        </div>\n                                    </div>\n                                `\n                                  )\n                                  .join('')}\n                            </div>\n                        </div>\n                    `;\n          gameWorld.appendChild(worldsModal);\n\n          // Worlds modal event handlers\n          document\n            .getElementById('worlds-close-btn')\n            .addEventListener('click', () => toggleWorldsModal(false));\n\n          const nicknameDisplay = document.getElementById('nickname-display');\n          const updateNicknameBtn = document.getElementById('update-nickname-btn');\n\n          function refreshNicknameDisplay() {\n            if (nicknameDisplay) {\n              nicknameDisplay.textContent = `${myPlayerName || 'Player'}`;\n            }\n          }\n          window.refreshNicknameDisplay = refreshNicknameDisplay;\n\n          function openNicknameDialog() {\n            const inputDialog = document.createElement('div');\n            inputDialog.id = 'nickname-dialog';\n            inputDialog.style.cssText = `\n                            position: absolute;\n                            top: 0;\n                            left: 0;\n                            width: 100%;\n                            height: 100%;\n                            background: rgba(0, 0, 0, 0.9);\n                            display: flex;\n                            align-items: center;\n                            justify-content: center;\n                            z-index: 260;\n                        `;\n            inputDialog.innerHTML = `\n                            <div style=\"\n                                background: linear-gradient(135deg, rgba(30, 30, 45, 0.98), rgba(20, 20, 35, 0.98));\n                                padding: 30px;\n                                border-radius: 16px;\n                                border: 3px solid rgba(255, 193, 7, 0.5);\n                                max-width: 90%;\n                                width: 400px;\n                                box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n                            \">\n                                <h3 style=\"\n                                    font-family: 'VT323', monospace;\n                                    font-size: 32px;\n                                    color: #FFC107;\n                                    margin: 0 0 20px 0;\n                                    text-align: center;\n                                \">\u270f\ufe0f Update Nickname</h3>\n                                <input type=\"text\" id=\"nickname-input\" placeholder=\"Enter nickname...\" maxlength=\"18\" value=\"${myPlayerName || ''}\" style=\"\n                                    width: 100%;\n                                    padding: 14px 16px;\n                                    font-family: 'Inter', sans-serif;\n                                    font-size: 18px;\n                                    background: rgba(255, 255, 255, 0.1);\n                                    border: 2px solid rgba(255, 255, 255, 0.2);\n                                    border-radius: 10px;\n                                    color: white;\n                                    outline: none;\n                                    margin-bottom: 20px;\n                                    box-sizing: border-box;\n                                \">\n                                <div style=\"display: flex; gap: 12px;\">\n                                    <button id=\"nickname-cancel\" style=\"\n                                        flex: 1;\n                                        padding: 12px;\n                                        font-family: 'VT323', monospace;\n                                        font-size: 24px;\n                                        background: rgba(255, 255, 255, 0.1);\n                                        color: white;\n                                        border: 2px solid rgba(255, 255, 255, 0.3);\n                                        border-radius: 10px;\n                                        cursor: pointer;\n                                        transition: all 0.2s;\n                                    \">Cancel</button>\n                                    <button id=\"nickname-confirm\" style=\"\n                                        flex: 1;\n                                        padding: 12px;\n                                        font-family: 'VT323', monospace;\n                                        font-size: 24px;\n                                        background: rgba(255, 193, 7, 0.3);\n                                        color: #FFC107;\n                                        border: 2px solid #FFC107;\n                                        border-radius: 10px;\n                                        cursor: pointer;\n                                        transition: all 0.2s;\n                                    \">Update</button>\n                                </div>\n                            </div>\n                        `;\n            gameWorld.appendChild(inputDialog);\n\n            const nicknameInput = document.getElementById('nickname-input');\n            nicknameInput.focus();\n\n            document.getElementById('nickname-confirm').addEventListener('click', async () => {\n              const newName = nicknameInput.value.trim();\n              if (newName) {\n                myPlayerName = newName;\n                refreshNicknameDisplay();\n                await saveNickname(newName);\n                if (isOnline) {\n                  syncEvent('nickname', { name: newName });\n                }\n                showMessage(`Nickname updated: ${newName}`, '#FFC107', 1500);\n                inputDialog.remove();\n              } else {\n                nicknameInput.style.borderColor = '#F44336';\n                nicknameInput.placeholder = 'Please enter a nickname!';\n              }\n            });\n\n            document.getElementById('nickname-cancel').addEventListener('click', () => {\n              inputDialog.remove();\n            });\n\n            nicknameInput.addEventListener('keydown', (e) => {\n              if (e.key === 'Enter') {\n                document.getElementById('nickname-confirm').click();\n              } else if (e.key === 'Escape') {\n                inputDialog.remove();\n              }\n            });\n\n            inputDialog.addEventListener('click', (e) => {\n              if (e.target === inputDialog) {\n                inputDialog.remove();\n              }\n            });\n          }\n\n          if (updateNicknameBtn) {\n            updateNicknameBtn.addEventListener('click', openNicknameDialog);\n          }\n          refreshNicknameDisplay();\n\n          document.getElementById('host-world-btn').addEventListener('click', () => {\n            // Show world name input dialog\n            const inputDialog = document.createElement('div');\n            inputDialog.id = 'world-name-dialog';\n            inputDialog.style.cssText = `\n                            position: absolute;\n                            top: 0;\n                            left: 0;\n                            width: 100%;\n                            height: 100%;\n                            background: rgba(0, 0, 0, 0.9);\n                            display: flex;\n                            align-items: center;\n                            justify-content: center;\n                            z-index: 250;\n                        `;\n            inputDialog.innerHTML = `\n                            <div style=\"\n                                background: linear-gradient(135deg, rgba(30, 30, 45, 0.98), rgba(20, 20, 35, 0.98));\n                                padding: 30px;\n                                border-radius: 16px;\n                                border: 3px solid rgba(33, 150, 243, 0.5);\n                                max-width: 90%;\n                                width: 400px;\n                                box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n                            \">\n                                <h3 style=\"\n                                    font-family: 'VT323', monospace;\n                                    font-size: 32px;\n                                    color: #2196F3;\n                                    margin: 0 0 20px 0;\n                                    text-align: center;\n                                \">\ud83c\udf0d Name Your World</h3>\n                                <input type=\"text\" id=\"world-name-input\" placeholder=\"Enter world name...\" maxlength=\"30\" style=\"\n                                    width: 100%;\n                                    padding: 14px 16px;\n                                    font-family: 'Inter', sans-serif;\n                                    font-size: 18px;\n                                    background: rgba(255, 255, 255, 0.1);\n                                    border: 2px solid rgba(255, 255, 255, 0.2);\n                                    border-radius: 10px;\n                                    color: white;\n                                    outline: none;\n                                    margin-bottom: 20px;\n                                    box-sizing: border-box;\n                                \">\n                                <div style=\"display: flex; gap: 12px;\">\n                                    <button id=\"world-name-cancel\" style=\"\n                                        flex: 1;\n                                        padding: 12px;\n                                        font-family: 'VT323', monospace;\n                                        font-size: 24px;\n                                        background: rgba(255, 255, 255, 0.1);\n                                        color: white;\n                                        border: 2px solid rgba(255, 255, 255, 0.3);\n                                        border-radius: 10px;\n                                        cursor: pointer;\n                                        transition: all 0.2s;\n                                    \">Cancel</button>\n                                    <button id=\"world-name-confirm\" style=\"\n                                        flex: 1;\n                                        padding: 12px;\n                                        font-family: 'VT323', monospace;\n                                        font-size: 24px;\n                                        background: rgba(33, 150, 243, 0.4);\n                                        color: #2196F3;\n                                        border: 2px solid #2196F3;\n                                        border-radius: 10px;\n                                        cursor: pointer;\n                                        transition: all 0.2s;\n                                    \">Host World</button>\n                                </div>\n                            </div>\n                        `;\n            gameWorld.appendChild(inputDialog);\n\n            const worldNameInput = document.getElementById('world-name-input');\n            worldNameInput.focus();\n\n            // Handle confirm button\n            document.getElementById('world-name-confirm').addEventListener('click', async () => {\n              const worldName = worldNameInput.value.trim();\n              if (worldName) {\n                inputDialog.remove();\n                toggleWorldsModal(false);\n                showMessage(`\ud83c\udf0d \"${worldName}\" Hosting...`, '#2196F3', 3000);\n                await createRoom(worldName, 20);\n                await leaveRoom();\n                await joinRoom(worldName);\n                showMessage(`\ud83c\udf0d \"${worldName}\" Connected!`, '#2196F3', 3000);\n              } else {\n                worldNameInput.style.borderColor = '#F44336';\n                worldNameInput.placeholder = 'Please enter a name!';\n              }\n            });\n\n            // Handle cancel button\n            document.getElementById('world-name-cancel').addEventListener('click', () => {\n              inputDialog.remove();\n            });\n\n            // Handle Enter key\n            worldNameInput.addEventListener('keydown', (e) => {\n              if (e.key === 'Enter') {\n                document.getElementById('world-name-confirm').click();\n              } else if (e.key === 'Escape') {\n                inputDialog.remove();\n              }\n            });\n\n            // Close on background click\n            inputDialog.addEventListener('click', (e) => {\n              if (e.target === inputDialog) {\n                inputDialog.remove();\n              }\n            });\n          });\n\n          // World item click handlers\n          const worldContainer = document.querySelector('#worlds-list');\n          worldContainer.addEventListener('click', async (event) => {\n            // Find the closest .world-item to the click (in case they click the name or stats)\n            const item = event.target.closest('.world-item');\n\n            // If a .world-item was clicked\n            if (item) {\n              const worldName = item.dataset.world;\n\n              toggleWorldsModal(false);\n              showMessage(`\ud83d\udd17 Connecting to \"${worldName}\"...`, '#4CAF50', 2000);\n\n              await leaveRoom();\n              await joinRoom(worldName);\n\n              showMessage(`\ud83d\udd17 Connected to \"${worldName}\"!`, '#4CAF50', 2000);\n            }\n          });\n\n          // Close modal when clicking outside\n          worldsModal.addEventListener('click', (e) => {\n            if (e.target === worldsModal) {\n              toggleWorldsModal(false);\n            }\n          });\n\n          function toggleWorldsModal(show) {\n            const modal = document.getElementById('worlds-modal');\n            if (show) {\n              modal.classList.add('active');\n              isPaused = true;\n              try {\n                if (document.pointerLockElement) {\n                  document.exitPointerLock?.();\n                }\n              } catch (e) {}\n              isPointerLocked = false;\n              listRooms();\n            } else {\n              modal.classList.remove('active');\n              isPaused = false;\n            }\n          }\n          window.toggleWorldsModal = toggleWorldsModal;\n\n          // Chat messages - now shown as bubbles above player heads\n          const chatUsernames = [\n            'Steve',\n            'Alex',\n            'Miner2024',\n            'CraftMaster',\n            'BlockBuilder',\n            'VoxelKing',\n            'DiamondHunter',\n          ];\n\n          const playerChatBubbles = new Map(); // pid -> { text, time, mesh }\n          let playerChatBubble = null; // For local player's chat bubble\n\n          function addChatMessage(username, text, isOwn = false) {\n            const messagesEl = document.getElementById('chat-messages');\n            if (messagesEl) {\n              const row = document.createElement('div');\n              row.className = 'chat-message' + (isOwn ? ' own-message' : '');\n\n              const nameSpan = document.createElement('span');\n              nameSpan.className = 'username';\n              nameSpan.textContent = `${username}:`;\n\n              const textSpan = document.createElement('span');\n              textSpan.className = 'text';\n              textSpan.textContent = text;\n\n              row.appendChild(nameSpan);\n              row.appendChild(textSpan);\n              messagesEl.appendChild(row);\n              messagesEl.scrollTop = messagesEl.scrollHeight;\n            }\n\n            const chatWindowEl = document.getElementById('chat-window');\n            const chatIsHidden = !!(chatWindowEl && chatWindowEl.classList.contains('hidden'));\n            if (!isOwn && chatIsHidden) {\n              hasUnreadChat = true;\n              updateChatToggleUI(true);\n            }\n\n            // Emoji feedback bubble above local player\n            if (isOwn && quickEmojis.includes(text)) {\n              showOwnPlayerChatBubble(text);\n            }\n          }\n\n          function showOwnPlayerChatBubble(emoji) {\n            // Remove old bubble if exists\n            if (playerChatBubble) {\n              scene.remove(playerChatBubble);\n              playerChatBubble = null;\n            }\n\n            // Create canvas for emoji\n            const canvas = document.createElement('canvas');\n            canvas.width = 128;\n            canvas.height = 128;\n            const ctx = canvas.getContext('2d');\n\n            // Draw emoji\n            ctx.font =\n              'bold 96px \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Noto Color Emoji\", sans-serif';\n            ctx.textAlign = 'center';\n            ctx.textBaseline = 'middle';\n            ctx.fillText(emoji, 64, 64);\n\n            const texture = new THREE.CanvasTexture(canvas);\n            texture.magFilter = THREE.LinearFilter;\n            texture.minFilter = THREE.LinearFilter;\n\n            const material = new THREE.SpriteMaterial({\n              map: texture,\n              transparent: true,\n            });\n            const bubble = new THREE.Sprite(material);\n            bubble.scale.set(0.6, 0.6, 1);\n            bubble.position.copy(playerPos);\n            bubble.position.y += 2.5; // Above player's head\n            scene.add(bubble);\n\n            playerChatBubble = bubble;\n            playerChatBubble.userData.time = 3.0; // Show for 3 seconds\n          }\n\n          function showPlayerChatBubble(playerGroup, emoji) {\n            // Remove old bubble\n            if (playerGroup.userData.chatBubble) {\n              playerGroup.remove(playerGroup.userData.chatBubble);\n              playerGroup.userData.chatBubble = null;\n            }\n\n            const canvas = document.createElement('canvas');\n            canvas.width = 128;\n            canvas.height = 128;\n            const ctx = canvas.getContext('2d');\n\n            // Draw emoji\n            ctx.font =\n              'bold 96px \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Noto Color Emoji\", sans-serif';\n            ctx.textAlign = 'center';\n            ctx.textBaseline = 'middle';\n            ctx.fillText(emoji, 64, 64);\n\n            const texture = new THREE.CanvasTexture(canvas);\n            texture.magFilter = THREE.LinearFilter;\n            texture.minFilter = THREE.LinearFilter;\n\n            const material = new THREE.SpriteMaterial({\n              map: texture,\n              transparent: true,\n            });\n            const bubble = new THREE.Sprite(material);\n            bubble.scale.set(0.6, 0.6, 1);\n            bubble.position.y = 1.5;\n            playerGroup.add(bubble);\n            playerGroup.userData.chatBubble = bubble;\n            playerGroup.userData.chatBubbleTime = 3.0; // 3 seconds\n\n            // Add to tracking map so updateChatBubbles can manage it\n            const pid = playerGroup.userData.pid;\n            if (pid) {\n              playerChatBubbles.set(pid, playerGroup);\n            }\n          }\n\n          function showPlayerDamage(playerGroup, damage) {\n            // Create canvas for damage number\n            const canvas = document.createElement('canvas');\n            canvas.width = 128;\n            canvas.height = 64;\n            const ctx = canvas.getContext('2d');\n\n            // Draw background\n            ctx.fillStyle = 'rgba(255, 0, 0, 0.8)';\n            ctx.fillRect(0, 0, 128, 64);\n\n            // Draw damage number\n            ctx.font = 'bold 48px \"VT323\", monospace';\n            ctx.fillStyle = 'white';\n            ctx.textAlign = 'center';\n            ctx.textBaseline = 'middle';\n            ctx.fillText('-' + Math.round(damage), 64, 32);\n\n            const texture = new THREE.CanvasTexture(canvas);\n            texture.magFilter = THREE.LinearFilter;\n            texture.minFilter = THREE.LinearFilter;\n\n            const material = new THREE.SpriteMaterial({\n              map: texture,\n              transparent: true,\n            });\n            const damageSprite = new THREE.Sprite(material);\n            damageSprite.scale.set(1.0, 0.5, 1);\n            damageSprite.position.copy(playerGroup.position);\n            damageSprite.position.y += 2.0;\n            scene.add(damageSprite);\n\n            // Animate upward and fade out\n            damageSprite.userData.velocity = 2.0;\n            damageSprite.userData.life = 1.0;\n            particles.push(damageSprite);\n          }\n\n          function updateChatBubbles(dt) {\n            // Update own player's chat bubble\n            if (playerChatBubble) {\n              playerChatBubble.userData.time -= dt;\n              // Fade out effect\n              playerChatBubble.material.opacity = Math.max(0, playerChatBubble.userData.time / 3.0);\n\n              if (playerChatBubble.userData.time <= 0) {\n                scene.remove(playerChatBubble);\n                playerChatBubble = null;\n              } else {\n                // Keep bubble above player's head\n                playerChatBubble.position.copy(playerPos);\n                playerChatBubble.position.y += 2.5;\n              }\n            }\n\n            // Update other players' chat bubbles\n            const pidsToRemove = [];\n            for (const [pid, group] of playerChatBubbles) {\n              if (group.userData.chatBubble) {\n                group.userData.chatBubbleTime -= dt;\n                // Fade out effect\n                group.userData.chatBubble.material.opacity = Math.max(\n                  0,\n                  group.userData.chatBubbleTime / 3.0\n                );\n\n                if (group.userData.chatBubbleTime <= 0) {\n                  group.remove(group.userData.chatBubble);\n                  group.userData.chatBubble = null;\n                  pidsToRemove.push(pid);\n                }\n              }\n            }\n\n            // Clean up entries from tracking map\n            pidsToRemove.forEach((pid) => playerChatBubbles.delete(pid));\n          }\n\n          window.updateChatBubbles = updateChatBubbles;\n          window.addChatMessage = addChatMessage;\n          window.showPlayerChatBubble = showPlayerChatBubble;\n          window.showPlayerDamage = showPlayerDamage;\n\n          function showEmojiFeedback(emoji) {\n            const feedbackEl = document.createElement('div');\n            feedbackEl.style.cssText = `\n                            position: absolute;\n                            bottom: 170px;\n                            left: 50%;\n                            transform: translateX(-50%);\n                            font-size: 64px;\n                            z-index: 100;\n                            pointer-events: none;\n                            text-shadow: 0 4px 8px rgba(0,0,0,0.5);\n                            transition: all 1.5s ease-out;\n                            opacity: 1;\n                        `;\n            feedbackEl.textContent = emoji;\n            const gameWorld = document.getElementById('game-world');\n            gameWorld.appendChild(feedbackEl);\n\n            // Trigger reflow\n            void feedbackEl.offsetWidth;\n\n            feedbackEl.style.transform = 'translate(-50%, -120px) scale(1.5)';\n            feedbackEl.style.opacity = '0';\n\n            setTimeout(() => {\n              if (feedbackEl.parentNode) feedbackEl.parentNode.removeChild(feedbackEl);\n            }, 1500);\n          }\n          window.showEmojiFeedback = showEmojiFeedback;\n\n          addChatMessage('System', 'Welcome!', false);\n          hasUnreadChat = false;\n          updateChatToggleUI(true);\n\n\n          // Mobile action buttons\n          if (isTouchDevice) {\n            // Persistent Move Widget (always visible joystick)\n            const moveWidget = document.createElement('div');\n            moveWidget.className = 'move-widget';\n            moveWidget.id = 'move-widget';\n            moveWidget.innerHTML = `\n                        <div class=\"move-widget-base\"></div>\n                        <div class=\"move-widget-arrows\">\n                            <span class=\"move-widget-arrow up\">\u25b2</span>\n                            <span class=\"move-widget-arrow down\">\u25bc</span>\n                            <span class=\"move-widget-arrow left\">\u25c0</span>\n                            <span class=\"move-widget-arrow right\">\u25b6</span>\n                        </div>\n                        <div class=\"move-widget-knob\" id=\"move-widget-knob\"></div>\n                    `;\n            gameWorld.appendChild(moveWidget);\n\n            // Setup move widget touch handling\n            setupMoveWidget();\n\n            // Mine button\n            const mineButton = document.createElement('button');\n            mineButton.id = 'mobile-mine-button';\n            mineButton.textContent = '\u26cf\ufe0f';\n            mineButton.style.cssText = `\n                        position: absolute;\n                        bottom: 300px;\n                        right: 150px;\n                        font-size: 56px;\n                        width: 112px;\n                        height: 112px;\n                        background: rgba(244, 67, 54, 0.8);\n                        color: white;\n                        border: 4px solid rgba(255,255,255,0.3);\n                        border-radius: 50%;\n                        cursor: pointer;\n                        z-index: 25;\n                        display: flex;\n                        align-items: center;\n                        justify-content: center;\n                        transition: all 0.15s;\n                        box-shadow: 0 4px 8px rgba(0,0,0,0.3);\n                        user-select: none;\n                        -webkit-user-select: none;\n                        touch-action: manipulation;\n                    `;\n            mineButton.addEventListener('touchstart', (e) => {\n              e.preventDefault();\n              mineButton.style.transform = 'scale(0.9)';\n              mineButton.style.background = 'rgba(244, 67, 54, 1)';\n              if (isArrowWeaponSelected()) {\n                fireArrowWeapon();\n              } else if (!attackMob()) {\n                isMining = true;\n              }\n            });\n            mineButton.addEventListener('touchend', (e) => {\n              e.preventDefault();\n              mineButton.style.transform = 'scale(1)';\n              mineButton.style.background = 'rgba(244, 67, 54, 0.8)';\n              isMining = false;\n              miningProgress = 0;\n            });\n            gameWorld.appendChild(mineButton);\n\n            // Place button\n            const placeButton = document.createElement('button');\n            placeButton.id = 'mobile-place-button';\n            placeButton.style.cssText = `\n                        position: absolute;\n                        bottom: 300px;\n                        right: 20px;\n                        font-size: 56px;\n                        width: 112px;\n                        height: 112px;\n                        background: rgba(76, 175, 80, 0.8);\n                        color: white;\n                        border: 4px solid rgba(255,255,255,0.3);\n                        border-radius: 50%;\n                        cursor: pointer;\n                        z-index: 25;\n                        display: flex;\n                        align-items: center;\n                        justify-content: center;\n                        transition: all 0.15s;\n                        box-shadow: 0 4px 8px rgba(0,0,0,0.3);\n                        user-select: none;\n                        -webkit-user-select: none;\n                        touch-action: manipulation;\n                    `;\n\n            // Create icon element inside place button\n            const placeButtonIcon = document.createElement('img');\n            placeButtonIcon.id = 'mobile-place-button-icon';\n            placeButtonIcon.style.cssText =\n              'width: 64px; height: 64px; image-rendering: pixelated; pointer-events: none;';\n            placeButton.appendChild(placeButtonIcon);\n\n            placeButton.addEventListener('touchstart', (e) => {\n              e.preventDefault();\n              placeButton.style.transform = 'scale(0.9)';\n              placeButton.style.background = 'rgba(76, 175, 80, 1)';\n              placeBlock();\n            });\n            placeButton.addEventListener('touchend', (e) => {\n              e.preventDefault();\n              placeButton.style.transform = 'scale(1)';\n              placeButton.style.background = 'rgba(76, 175, 80, 0.8)';\n            });\n            gameWorld.appendChild(placeButton);\n\n            // Jump button (Lower Right)\n            const jumpButton = document.createElement('button');\n            jumpButton.id = 'mobile-jump-button';\n            jumpButton.textContent = '\u2b06\ufe0f';\n            jumpButton.style.cssText = `\n                        position: absolute;\n                        bottom: 50px;\n                        right: 40px;\n                        font-size: 72px;\n                        width: 140px;\n                        height: 140px;\n                        background: rgba(33, 150, 243, 0.8);\n                        color: white;\n                        border: 4px solid rgba(255,255,255,0.3);\n                        border-radius: 50%;\n                        cursor: pointer;\n                        z-index: 25;\n                        display: flex;\n                        align-items: center;\n                        justify-content: center;\n                        transition: all 0.15s;\n                        box-shadow: 0 6px 12px rgba(0,0,0,0.4);\n                        user-select: none;\n                        -webkit-user-select: none;\n                        touch-action: manipulation;\n                    `;\n            jumpButton.addEventListener('touchstart', (e) => {\n              e.preventDefault();\n              jumpButton.style.transform = 'scale(0.9)';\n              jumpButton.style.background = 'rgba(33, 150, 243, 1)';\n              if (isInWater) {\n                // Swim up when in water\n                playerVelocity.y = WATER_SWIM_UP_SPEED;\n              } else if (isGrounded) {\n                playerVelocity.y = JUMP_VELOCITY;\n                isGrounded = false;\n              }\n            });\n            jumpButton.addEventListener('touchend', (e) => {\n              e.preventDefault();\n              jumpButton.style.transform = 'scale(1)';\n              jumpButton.style.background = 'rgba(33, 150, 243, 0.8)';\n            });\n            gameWorld.appendChild(jumpButton);\n\n            // POV Button (Upper Right)\n            const povButton = document.createElement('button');\n            povButton.id = 'mobile-pov-button';\n            povButton.textContent = '\ud83c\udfa5';\n            povButton.style.cssText = `\n                        position: absolute;\n                        top: var(--space-sm);\n                        right: 80px;\n                        font-size: 32px;\n                        width: 56px;\n                        height: 56px;\n                        background: rgba(0,0,0,0.65);\n                        color: white;\n                        border: 2px solid rgba(255,255,255,0.2);\n                        border-radius: 12px;\n                        cursor: pointer;\n                        z-index: 20;\n                        display: flex;\n                        align-items: center;\n                        justify-content: center;\n                        transition: all 0.2s;\n                    `;\n            povButton.addEventListener('click', togglePOV);\n            povButton.addEventListener('touchend', (e) => {\n              e.preventDefault();\n              togglePOV();\n            });\n            gameWorld.appendChild(povButton);\n\n            // Pause button\n            const pauseButton = document.createElement('button');\n            pauseButton.id = 'mobile-pause-button';\n            pauseButton.textContent = '\u23f8';\n            pauseButton.style.cssText = `\n                        position: absolute;\n                        top: var(--space-sm);\n                        left: 50%;\n                        transform: translateX(-50%);\n                        font-size: 32px;\n                        width: 56px;\n                        height: 56px;\n                        background: rgba(0,0,0,0.65);\n                        color: white;\n                        border: 2px solid rgba(255,255,255,0.2);\n                        border-radius: 50%;\n                        cursor: pointer;\n                        z-index: 20;\n                        display: flex;\n                        align-items: center;\n                        justify-content: center;\n                        transition: all 0.2s;\n                    `;\n            pauseButton.addEventListener('click', togglePauseMenu);\n            pauseButton.addEventListener('touchend', (e) => {\n              e.preventDefault();\n              togglePauseMenu();\n            });\n            gameWorld.appendChild(pauseButton);\n\n            // Inventory button for mobile (now includes crafting)\n            const inventoryButton = document.createElement('button');\n            inventoryButton.id = 'mobile-inventory-button';\n            inventoryButton.textContent = '\ud83c\udf92';\n            inventoryButton.style.cssText = `\n                        position: absolute;\n                        bottom: 500px;\n                        right: 20px;\n                        font-size: 40px;\n                        width: 80px;\n                        height: 80px;\n                        background: rgba(139, 90, 43, 0.8);\n                        color: white;\n                        border: 4px solid rgba(255,255,255,0.3);\n                        border-radius: 50%;\n                        cursor: pointer;\n                        z-index: 25;\n                        display: flex;\n                        align-items: center;\n                        justify-content: center;\n                        transition: all 0.15s;\n                        box-shadow: 0 4px 8px rgba(0,0,0,0.3);\n                        user-select: none;\n                        -webkit-user-select: none;\n                        touch-action: manipulation;\n                    `;\n            inventoryButton.addEventListener('touchstart', (e) => {\n              e.preventDefault();\n              inventoryButton.style.transform = 'scale(0.9)';\n              inventoryButton.style.background = 'rgba(139, 90, 43, 1)';\n            });\n            inventoryButton.addEventListener('touchend', (e) => {\n              e.preventDefault();\n              inventoryButton.style.transform = 'scale(1)';\n              inventoryButton.style.background = 'rgba(139, 90, 43, 0.8)';\n              toggleInventory();\n            });\n            gameWorld.appendChild(inventoryButton);\n          }\n\n          // Pause menu (created but hidden by default)\n          createPauseMenu();\n\n          // Inventory UI with integrated crafting (created but hidden by default)\n          createInventoryUI();\n\n          // Crafting Table UI (3x3 grid, separate modal)\n          createCraftingTableUI();\n\n          // Chest UI (storage)\n          createChestUI();\n\n          // Furnace UI (smelting)\n          createFurnaceUI();\n\n          // Touch areas for joysticks\n          if (isTouchDevice) {\n            // Right touch area for camera look (left side uses move widget only)\n            const rightArea = document.createElement('div');\n            rightArea.className = 'touch-area touch-area-right';\n            rightArea.id = 'touch-right';\n            gameWorld.appendChild(rightArea);\n\n            // Right joystick visual for camera aiming (always visible like move widget)\n            const rightJoystickEl = document.createElement('div');\n            rightJoystickEl.className = 'joystick';\n            rightJoystickEl.id = 'right-joystick';\n            rightJoystickEl.style.cssText = `\n              position: absolute;\n              bottom: 280px;\n              right: 30px;\n              width: 180px;\n              height: 180px;\n              pointer-events: none;\n              z-index: 15;\n              opacity: 0.3;\n            `;\n            rightJoystickEl.innerHTML =\n              '<div class=\"joystick-base\" style=\"width: 100%; height: 100%; background: rgba(0, 0, 0, 0.4); border: 4px solid rgba(255, 255, 255, 0.4); border-radius: 50%; position: absolute; top: 0; left: 0; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), inset 0 0 20px rgba(255, 255, 255, 0.1);\"></div><div class=\"joystick-knob\" style=\"width: 60px; height: 60px; background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(200, 200, 200, 0.8)); border-radius: 50%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), inset 0 2px 4px rgba(255, 255, 255, 0.5); transition: transform 0.05s ease-out;\"></div>';\n            gameWorld.appendChild(rightJoystickEl);\n          }\n        }\n\n        function updateSlotIcon(iconEl, itemType) {\n          const textureMap = {\n            [BLOCK_TYPES.GRASS]: 'grass_block_top',\n            [BLOCK_TYPES.DIRT]: 'dirt_block',\n            [BLOCK_TYPES.STONE]: 'stone_block',\n            [BLOCK_TYPES.WOOD]: 'wood_block_top',\n            [BLOCK_TYPES.LEAVES]: 'leaves_block',\n            [BLOCK_TYPES.DYNAMITE]: 'dynamite_block',\n            [BLOCK_TYPES.SAND]: 'sand_block',\n            [BLOCK_TYPES.WATER]: 'water_block',\n            [BLOCK_TYPES.PLANKS]: 'planks_block',\n            [BLOCK_TYPES.TORCH]: 'torch_block',\n            [ITEM_TYPES.WOODEN_PICKAXE]: 'wooden_pickaxe',\n            [ITEM_TYPES.WOODEN_AXE]: 'wooden_axe',\n            [ITEM_TYPES.WOODEN_SWORD]: 'wooden_sword',\n            [ITEM_TYPES.STICK]: 'stick_item',\n            [ITEM_TYPES.COAL]: 'coal_item',\n            [ITEM_TYPES.STONE_PICKAXE]: 'stone_pickaxe',\n            [ITEM_TYPES.STONE_AXE]: 'stone_axe',\n            [ITEM_TYPES.STONE_SWORD]: 'stone_sword',\n            [ITEM_TYPES.WOODEN_SHOVEL]: 'wooden_shovel',\n            [ITEM_TYPES.STONE_SHOVEL]: 'stone_shovel',\n            [ITEM_TYPES.IRON_PICKAXE]: 'iron_pickaxe',\n            [ITEM_TYPES.IRON_AXE]: 'iron_axe',\n            [ITEM_TYPES.IRON_SWORD]: 'iron_sword',\n            [ITEM_TYPES.IRON_SHOVEL]: 'iron_shovel',\n            [ITEM_TYPES.GOLD_PICKAXE]: 'gold_pickaxe',\n            [ITEM_TYPES.GOLD_AXE]: 'gold_axe',\n            [ITEM_TYPES.GOLD_SWORD]: 'gold_sword',\n            [ITEM_TYPES.GOLD_SHOVEL]: 'gold_shovel',\n            [ITEM_TYPES.DIAMOND_PICKAXE]: 'diamond_pickaxe',\n            [ITEM_TYPES.DIAMOND_AXE]: 'diamond_axe',\n            [ITEM_TYPES.DIAMOND_SWORD]: 'diamond_sword',\n            [ITEM_TYPES.DIAMOND_SHOVEL]: 'diamond_shovel',\n            [ITEM_TYPES.IRON_INGOT]: 'iron_ingot',\n            [ITEM_TYPES.RAW_BEEF]: 'raw_beef',\n            [ITEM_TYPES.COOKED_BEEF]: 'cooked_beef',\n            [ITEM_TYPES.APPLE]: 'apple_item',\n            [ITEM_TYPES.BREAD]: 'bread_item',\n            [ITEM_TYPES.RAW_CHICKEN]: 'raw_chicken',\n            [ITEM_TYPES.COOKED_CHICKEN]: 'cooked_chicken',\n            [ITEM_TYPES.BUCKET_EMPTY]: 'bucket_empty',\n            [ITEM_TYPES.BUCKET_WATER]: 'bucket_water',\n            [ITEM_TYPES.BUCKET_LAVA]: 'bucket_lava',\n            [ITEM_TYPES.ARROW_WEAPON]: 'wooden_sword',\n            [BLOCK_TYPES.CRAFTING_TABLE]: 'crafting_table_top',\n            [BLOCK_TYPES.COAL_ORE]: 'coal_ore_block',\n            [BLOCK_TYPES.IRON_ORE]: 'iron_ore_block',\n            [BLOCK_TYPES.GOLD_ORE]: 'gold_ore_block',\n            [BLOCK_TYPES.DIAMOND_ORE]: 'diamond_ore_block',\n            [ITEM_TYPES.GOLD_INGOT]: 'gold_ingot',\n            [ITEM_TYPES.DIAMOND]: 'diamond_item',\n            [BLOCK_TYPES.OAK_SIGN]: 'oak_sign',\n            [BLOCK_TYPES.CHEST]: 'chest_front',\n            [BLOCK_TYPES.FURNACE]: 'furnace_front_off',\n            [BLOCK_TYPES.BEDROCK]: 'bedrock_block',\n            [BLOCK_TYPES.GLASS]: 'glass_block',\n            [BLOCK_TYPES.DOOR]: 'door_block',\n            [BLOCK_TYPES.SOIL]: 'soil_block',\n            [BLOCK_TYPES.RICE_PLANT]: 'rice_plant',\n            [BLOCK_TYPES.BED]: 'bed_block',\n            [BLOCK_TYPES.FLOWER]: 'flower_block',\n            [BLOCK_TYPES.TV]: 'tv_block',\n          };\n\n          const textureName = textureMap[itemType];\n          if (textureName && assetCache.images[textureName]) {\n            iconEl.src = assetCache.images[textureName].src;\n            iconEl.style.display = 'block';\n          } else {\n            iconEl.style.display = 'none';\n          }\n        }\n\n        function selectSlot(index) {\n          selectedSlot = index;\n          updateHotbarUI();\n        }\n\n        function updateHotbarUI() {\n          const slots = document.querySelectorAll('.hotbar-slot');\n          slots.forEach((slot, i) => {\n            const slotBg =\n              assetCache.images[i === selectedSlot ? 'hotbar_slot_selected' : 'hotbar_slot_bg'];\n            if (slotBg) {\n              slot.style.backgroundImage = `url(${slotBg.src})`;\n            } else {\n              slot.style.background =\n                i === selectedSlot ? 'rgba(255,215,0,0.5)' : 'rgba(60,60,60,0.8)';\n              slot.style.border = i === selectedSlot ? '2px solid gold' : '2px solid #666';\n            }\n\n            const icon = slot.querySelector('.hotbar-slot-icon');\n            const count = slot.querySelector('.hotbar-slot-count');\n\n            updateSlotIcon(icon, inventory[i].type);\n            count.textContent = inventory[i].count > 0 ? inventory[i].count : '';\n          });\n\n          // Update mobile place button icon\n          updateMobilePlaceButtonIcon();\n        }\n\n        function updateMobilePlaceButtonIcon() {\n          if (!isTouchDevice) return;\n\n          const placeButtonIcon = document.getElementById('mobile-place-button-icon');\n          if (!placeButtonIcon) return;\n\n          const selectedItem = inventory[selectedSlot];\n          if (selectedItem && selectedItem.count > 0 && selectedItem.type !== BLOCK_TYPES.AIR) {\n            updateSlotIcon(placeButtonIcon, selectedItem.type);\n          } else {\n            // Default to empty/transparent when no item selected\n            placeButtonIcon.style.display = 'none';\n          }\n        }\n\n        function updateHealthBar() {\n          const healthBar = document.getElementById('health-bar');\n          if (!healthBar) return;\n\n          const hearts = healthBar.querySelectorAll('.heart');\n          const heartsToFill = playerHealth / 2; // Each heart = 2 HP\n\n          hearts.forEach((heart, index) => {\n            heart.classList.remove('full', 'half', 'empty', 'damage');\n\n            if (index < Math.floor(heartsToFill)) {\n              // Full heart\n              heart.classList.add('full');\n              heart.innerHTML = '\u2665';\n            } else if (index < heartsToFill) {\n              // Half heart (when heartsToFill has .5)\n              heart.classList.add('half');\n              heart.innerHTML = '';\n            } else {\n              // Empty heart\n              heart.classList.add('empty');\n              heart.innerHTML = '\u2665';\n            }\n          });\n        }\n\n        function updateHungerBar() {\n          const hungerBar = document.getElementById('hunger-bar');\n          if (!hungerBar) return;\n\n          const drumsticks = hungerBar.querySelectorAll('.drumstick');\n          const drumsticksToFill = playerHunger / 2; // Each drumstick = 2 hunger\n\n          drumsticks.forEach((drumstick, index) => {\n            drumstick.classList.remove('full', 'half', 'empty', 'hunger');\n\n            if (index < Math.floor(drumsticksToFill)) {\n              // Full drumstick\n              drumstick.classList.add('full');\n              drumstick.innerHTML = '\ud83c\udf56';\n            } else if (index < drumsticksToFill) {\n              // Half drumstick\n              drumstick.classList.add('half');\n              drumstick.innerHTML = '';\n            } else {\n              // Empty drumstick\n              drumstick.classList.add('empty');\n              drumstick.innerHTML = '\ud83c\udf56';\n            }\n          });\n\n          // Add shake animation when hunger is low\n          if (playerHunger <= 6) {\n            const randomDrumstick = drumsticks[Math.floor(Math.random() * drumsticks.length)];\n            randomDrumstick.classList.add('hunger');\n          }\n        }\n\n        // Eat food to restore hunger\n        function eatFood() {\n          const slot = inventory[selectedSlot];\n          if (!slot || slot.count <= 0) return false;\n\n          const foodValue = FOOD_VALUES[slot.type];\n          if (!foodValue) return false; // Not a food item\n\n          // Don't eat if hunger is full\n          if (playerHunger >= maxPlayerHunger) {\n            showMessage(\"You're not hungry!\", '#FF9800', 1500);\n            return false;\n          }\n\n          // Consume the food\n          slot.count--;\n          if (slot.count <= 0) {\n            inventory[selectedSlot] = { type: BLOCK_TYPES.AIR, count: 0 };\n          }\n\n          // Restore hunger\n          playerHunger = Math.min(maxPlayerHunger, playerHunger + foodValue);\n          updateHungerBar();\n          updateHotbarUI();\n\n          // Play eating sound\n          playSound('block_place_sound', false, 0.4);\n\n          // Show feedback\n          showMessage(`+${foodValue} \ud83c\udf56`, '#8BC34A', 1000);\n\n          return true;\n        }\n\n        // Update hunger over time\n        function updateHunger(dt, isMoving, currentTime) {\n          if (isCreativeMode) {\n            if (playerHunger !== maxPlayerHunger) {\n              playerHunger = maxPlayerHunger;\n              updateHungerBar();\n            }\n            return;\n          }\n          // Decrease hunger over time\n          hungerTimer += dt;\n\n          // Hunger decreases faster when moving\n          // Apply hunger rate multiplier from config\n          const baseHungerRate = isMoving\n            ? HUNGER_DECREASE_INTERVAL / HUNGER_MOVEMENT_MULTIPLIER\n            : HUNGER_DECREASE_INTERVAL;\n          const hungerRate = baseHungerRate / config.hungerRateMultiplier;\n\n          if (hungerTimer >= hungerRate) {\n            hungerTimer = 0;\n            if (playerHunger > 0) {\n              playerHunger = Math.max(0, playerHunger - 1);\n              updateHungerBar();\n            }\n          }\n\n          // Starvation damage when hunger is 0\n          if (playerHunger <= 0 && currentTime - lastHungerDamageTime >= HUNGER_DAMAGE_COOLDOWN) {\n            lastHungerDamageTime = currentTime;\n            damagePlayer(HUNGER_DAMAGE_AMOUNT, currentTime);\n            showMessage('Starving!', '#F44336', 1000);\n          }\n\n          // Health regeneration when hunger is high\n          if (playerHunger >= HUNGER_REGEN_THRESHOLD && playerHealth < maxPlayerHealth) {\n            if (currentTime - lastRegenTime >= REGEN_COOLDOWN) {\n              lastRegenTime = currentTime;\n              playerHealth = Math.min(maxPlayerHealth, playerHealth + 1);\n              updateHealthBar();\n            }\n          }\n        }\n\n        // ===== INVENTORY SYSTEM (with integrated crafting) =====\n        function createInventoryUI() {\n          // Create inventory modal\n          const inventoryModal = document.createElement('div');\n          inventoryModal.className = 'inventory-modal';\n          inventoryModal.id = 'inventory-modal';\n\n          const container = document.createElement('div');\n          container.className = 'inventory-container';\n          container.style.position = 'relative';\n\n          // Close button\n          const closeBtn = document.createElement('button');\n          closeBtn.className = 'inventory-close-btn';\n          closeBtn.textContent = '\u2715';\n          closeBtn.addEventListener('click', () => toggleInventory());\n          container.appendChild(closeBtn);\n\n          // Title\n          const title = document.createElement('div');\n          title.className = 'inventory-title';\n          title.textContent = '\ud83d\udce6 Inventory';\n          container.appendChild(title);\n\n          // === CRAFTING SECTION (at the top) ===\n          const craftingSection = document.createElement('div');\n          craftingSection.className = 'inventory-section';\n          craftingSection.style.marginBottom = '12px';\n\n          const craftingTitle = document.createElement('div');\n          craftingTitle.className = 'inventory-section-title';\n          craftingTitle.textContent = '\ud83d\udd28 Crafting';\n          craftingSection.appendChild(craftingTitle);\n\n          // Crafting area (grid + arrow + result)\n          const craftingArea = document.createElement('div');\n          craftingArea.className = 'crafting-area';\n          craftingArea.style.cssText =\n            'display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px;';\n\n          // 2x2 crafting grid for inventory\n          const craftingGridEl = document.createElement('div');\n          craftingGridEl.className = 'crafting-grid';\n          craftingGridEl.id = 'crafting-grid';\n          craftingGridEl.style.cssText =\n            'display: grid; grid-template-columns: repeat(2, 1fr); gap: 3px; background: rgba(0,0,0,0.3); padding: 6px; border-radius: 6px; border: 2px solid rgba(255,255,255,0.1);';\n\n          for (let i = 0; i < 4; i++) {\n            const slot = createCraftingSlot(i, false);\n            craftingGridEl.appendChild(slot);\n          }\n          craftingArea.appendChild(craftingGridEl);\n\n          // Arrow\n          const arrow = document.createElement('div');\n          arrow.className = 'crafting-arrow';\n          arrow.textContent = '\u27a1\ufe0f';\n          arrow.style.fontSize = '28px';\n          craftingArea.appendChild(arrow);\n\n          // Result slot\n          const resultArea = document.createElement('div');\n          resultArea.style.cssText =\n            'display: flex; flex-direction: column; align-items: center; gap: 4px;';\n\n          const resultSlot = document.createElement('div');\n          resultSlot.className = 'crafting-result-slot';\n          resultSlot.id = 'crafting-result-slot';\n          resultSlot.style.cssText =\n            'width: 56px; height: 56px; background: rgba(76,175,80,0.2); border: 3px solid rgba(76,175,80,0.6); border-radius: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; position: relative; transition: all 0.15s ease;';\n\n          const resultIcon = document.createElement('img');\n          resultIcon.className = 'inventory-slot-icon';\n          resultIcon.style.cssText = 'width: 40px; height: 40px;';\n          resultSlot.appendChild(resultIcon);\n\n          const resultCount = document.createElement('span');\n          resultCount.className = 'inventory-slot-count';\n          resultSlot.appendChild(resultCount);\n\n          resultSlot.addEventListener('click', () => craftItem(false));\n          resultSlot.addEventListener('touchend', (e) => {\n            e.preventDefault();\n            craftItem(false);\n          });\n\n          resultArea.appendChild(resultSlot);\n\n          const resultName = document.createElement('div');\n          resultName.className = 'crafting-result-name';\n          resultName.id = 'crafting-result-name';\n          resultName.style.cssText =\n            'font-family: \"VT323\", monospace; font-size: 14px; color: rgba(255,255,255,0.7); text-align: center;';\n          resultName.textContent = '';\n          resultArea.appendChild(resultName);\n\n          craftingArea.appendChild(resultArea);\n          craftingSection.appendChild(craftingArea);\n\n          // Action buttons row (Clear Grid button for mobile)\n          const invActionRow = document.createElement('div');\n          invActionRow.style.cssText =\n            'display: flex; justify-content: center; gap: 8px; margin-bottom: 8px;';\n\n          const invClearGridBtn = document.createElement('button');\n          invClearGridBtn.id = 'clear-crafting-grid-btn';\n          invClearGridBtn.style.cssText =\n            'background: rgba(244, 67, 54, 0.3); color: #f44336; border: 2px solid #f44336; border-radius: 8px; padding: 8px 16px; font-family: \"VT323\", monospace; font-size: 18px; cursor: pointer; touch-action: manipulation;';\n          invClearGridBtn.textContent = '\ud83d\uddd1\ufe0f Clear Grid';\n          invClearGridBtn.addEventListener('click', (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n            // Return all items in crafting grid to inventory\n            for (let i = 0; i < 4; i++) {\n              if (craftingGrid[i].count > 0 && craftingGrid[i].type !== BLOCK_TYPES.AIR) {\n                for (let j = 0; j < craftingGrid[i].count; j++) {\n                  addToInventory(craftingGrid[i].type);\n                }\n                craftingGrid[i] = { type: BLOCK_TYPES.AIR, count: 0 };\n              }\n            }\n            updateCraftingUI();\n            checkCraftingRecipe();\n            updateInventoryUI();\n            updateHotbarUI();\n            playSound('block_place_sound', false, 0.3);\n          });\n          invActionRow.appendChild(invClearGridBtn);\n          craftingSection.appendChild(invActionRow);\n\n          // Recipe hints (compact)\n          const recipeHints = document.createElement('div');\n          recipeHints.style.cssText =\n            'display: flex; flex-wrap: wrap; gap: 4px; justify-content: center;';\n          CRAFTING_RECIPES.forEach((recipe) => {\n            const hint = document.createElement('div');\n            hint.style.cssText =\n              'background: rgba(255,255,255,0.1); padding: 3px 6px; border-radius: 4px; font-family: \"Inter\", sans-serif; font-size: 10px; color: rgba(255,255,255,0.7); cursor: pointer; transition: all 0.2s;';\n            hint.textContent = recipe.name;\n            hint.title = getRecipeDescription(recipe);\n            hint.addEventListener('click', () => showRecipePreview(recipe));\n            recipeHints.appendChild(hint);\n          });\n          craftingSection.appendChild(recipeHints);\n\n          container.appendChild(craftingSection);\n\n          // === MAIN INVENTORY SECTION (slots 9-35) ===\n          const mainSection = document.createElement('div');\n          mainSection.className = 'inventory-section';\n\n          const mainTitle = document.createElement('div');\n          mainTitle.className = 'inventory-section-title';\n          mainTitle.textContent = 'Main Inventory';\n          mainSection.appendChild(mainTitle);\n\n          const mainGrid = document.createElement('div');\n          mainGrid.className = 'inventory-grid';\n          mainGrid.id = 'inventory-main-grid';\n\n          // Create 27 main inventory slots (3 rows of 9)\n          for (let i = 9; i < 36; i++) {\n            const slot = createInventorySlot(i);\n            mainGrid.appendChild(slot);\n          }\n          mainSection.appendChild(mainGrid);\n          container.appendChild(mainSection);\n\n          // === HOTBAR SECTION (slots 0-8) ===\n          const hotbarSection = document.createElement('div');\n          hotbarSection.className = 'inventory-section';\n\n          const hotbarTitle = document.createElement('div');\n          hotbarTitle.className = 'inventory-section-title';\n          hotbarTitle.textContent = 'Hotbar';\n          hotbarSection.appendChild(hotbarTitle);\n\n          const hotbarGrid = document.createElement('div');\n          hotbarGrid.className = 'inventory-grid';\n          hotbarGrid.id = 'inventory-hotbar-grid';\n\n          for (let i = 0; i < 9; i++) {\n            const slot = createInventorySlot(i, true);\n            hotbarGrid.appendChild(slot);\n          }\n          hotbarSection.appendChild(hotbarGrid);\n          container.appendChild(hotbarSection);\n\n          // Hint text\n          const hint = document.createElement('div');\n          hint.className = 'inventory-hint';\n          hint.textContent = isTouchDevice\n            ? 'Tap an item to select, then tap destination to move \u2022 Double-tap to quick transfer'\n            : 'Click an item to select, then click destination to move \u2022 Press E to close';\n          container.appendChild(hint);\n\n          inventoryModal.appendChild(container);\n\n          // Close on background click\n          inventoryModal.addEventListener('click', (e) => {\n            if (e.target === inventoryModal) {\n              // If holding an item, drop it back to source\n              if (heldItem && heldItemSourceSlot >= 0) {\n                inventory[heldItemSourceSlot] = { ...heldItem };\n                heldItem = null;\n                heldItemSourceSlot = -1;\n                updateInventoryUI();\n                updateHeldItemDisplay(null);\n              }\n              toggleInventory();\n            }\n          });\n\n          gameWorld.appendChild(inventoryModal);\n\n          // Create held item display element (fixed position, tap-to-move style)\n          const heldItemEl = document.createElement('div');\n          heldItemEl.className = 'inventory-held-item';\n          heldItemEl.id = 'inventory-held-item';\n          heldItemEl.style.display = 'none';\n          heldItemEl.innerHTML = '<img src=\"\" alt=\"\"><span class=\"count\"></span>';\n          gameWorld.appendChild(heldItemEl);\n        }\n\n        function createInventorySlot(index, isHotbar = false) {\n          const slot = document.createElement('div');\n          slot.className = 'inventory-slot' + (isHotbar ? ' hotbar-highlight' : '');\n          slot.dataset.slotIndex = index;\n          slot.dataset.lastTapTime = '0';\n\n          const icon = document.createElement('img');\n          icon.className = 'inventory-slot-icon';\n          slot.appendChild(icon);\n\n          const count = document.createElement('span');\n          count.className = 'inventory-slot-count';\n          slot.appendChild(count);\n\n          // Click handler for picking up / placing items\n          const handleSlotClick = (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n\n            const slotIndex = parseInt(slot.dataset.slotIndex);\n            const slotItem = inventory[slotIndex];\n\n            // Double-tap detection for quick transfer to crafting grid\n            const now = Date.now();\n            const lastTap = parseInt(slot.dataset.lastTapTime || '0');\n            slot.dataset.lastTapTime = now.toString();\n\n            if (isTouchDevice && now - lastTap < 350 && !heldItem) {\n              // Double-tap: Quick transfer to crafting grid\n              if (slotItem.count > 0 && slotItem.type !== BLOCK_TYPES.AIR) {\n                // Find first empty crafting slot\n                for (let i = 0; i < 4; i++) {\n                  if (craftingGrid[i].count === 0 || craftingGrid[i].type === BLOCK_TYPES.AIR) {\n                    craftingGrid[i] = { type: slotItem.type, count: 1 };\n                    slotItem.count--;\n                    if (slotItem.count <= 0) {\n                      inventory[slotIndex] = { type: BLOCK_TYPES.AIR, count: 0 };\n                    }\n                    updateCraftingUI();\n                    checkCraftingRecipe();\n                    updateInventoryUI();\n                    updateHotbarUI();\n                    playSound('block_place_sound', false, 0.3);\n                    return;\n                  }\n                }\n              }\n              return;\n            }\n\n            if (heldItem) {\n              // We're holding an item - try to place it\n              if (slotItem.count === 0 || slotItem.type === BLOCK_TYPES.AIR) {\n                // Empty slot - place held item here\n                inventory[slotIndex] = { ...heldItem };\n                heldItem = null;\n                heldItemSourceSlot = -1;\n              } else if (slotItem.type === heldItem.type && slotItem.count < 64) {\n                // Same type - try to stack\n                const spaceLeft = 64 - slotItem.count;\n                const toMove = Math.min(spaceLeft, heldItem.count);\n                slotItem.count += toMove;\n                heldItem.count -= toMove;\n                if (heldItem.count <= 0) {\n                  heldItem = null;\n                  heldItemSourceSlot = -1;\n                }\n              } else {\n                // Different type - swap items\n                const temp = { ...slotItem };\n                inventory[slotIndex] = { ...heldItem };\n                heldItem = temp;\n                heldItemSourceSlot = slotIndex;\n              }\n            } else {\n              // Not holding anything - pick up from this slot\n              if (slotItem.count > 0 && slotItem.type !== BLOCK_TYPES.AIR) {\n                heldItem = { ...slotItem };\n                heldItemSourceSlot = slotIndex;\n                inventory[slotIndex] = { type: BLOCK_TYPES.AIR, count: 0 };\n              }\n            }\n\n            updateInventoryUI();\n            updateHotbarUI();\n            updateHeldItemDisplay(heldItem);\n          };\n\n          slot.addEventListener('click', handleSlotClick);\n          slot.addEventListener('touchend', handleSlotClick);\n\n          return slot;\n        }\n\n        function updateInventoryUI() {\n          const allSlots = document.querySelectorAll(\n            '#inventory-main-grid .inventory-slot, #inventory-hotbar-grid .inventory-slot'\n          );\n          allSlots.forEach((slot) => {\n            const index = parseInt(slot.dataset.slotIndex);\n            const item = inventory[index];\n            const icon = slot.querySelector('.inventory-slot-icon');\n            const count = slot.querySelector('.inventory-slot-count');\n\n            updateSlotIcon(icon, item.type);\n            count.textContent = item.count > 0 ? item.count : '';\n\n            // Highlight selected hotbar slot\n            if (index < 9 && index === selectedSlot) {\n              slot.classList.add('selected');\n            } else {\n              slot.classList.remove('selected');\n            }\n          });\n        }\n\n        function updateHeldItemDisplay(item) {\n          const heldEl = document.getElementById('inventory-held-item');\n          if (!heldEl) return;\n\n          if (item && item.count > 0 && item.type !== BLOCK_TYPES.AIR) {\n            heldEl.style.display = 'flex';\n            const img = heldEl.querySelector('img');\n            const countEl = heldEl.querySelector('.count');\n            updateSlotIcon(img, item.type);\n            countEl.textContent = item.count > 1 ? item.count : '';\n          } else {\n            heldEl.style.display = 'none';\n          }\n        }\n\n        function toggleInventory() {\n          isInventoryOpen = !isInventoryOpen;\n          const modal = document.getElementById('inventory-modal');\n\n          if (isInventoryOpen) {\n            // Open inventory\n            modal.classList.add('active');\n            updateInventoryUI();\n            updateCraftingUI();\n            checkCraftingRecipe();\n\n            // Exit pointer lock\n            try {\n              if (document.pointerLockElement) {\n                document.exitPointerLock?.();\n              }\n            } catch (e) {\n              lib.log('Error exiting pointer lock: ' + e.message);\n            }\n            isPointerLocked = false;\n          } else {\n            // Close inventory\n            modal.classList.remove('active');\n\n            // Return crafting grid items to inventory (2x2)\n            for (let i = 0; i < 4; i++) {\n              if (craftingGrid[i].count > 0 && craftingGrid[i].type !== BLOCK_TYPES.AIR) {\n                for (let j = 0; j < craftingGrid[i].count; j++) {\n                  addToInventory(craftingGrid[i].type);\n                }\n                craftingGrid[i] = { type: BLOCK_TYPES.AIR, count: 0 };\n              }\n            }\n\n            // If still holding an item, return it to inventory\n            if (heldItem) {\n              if (heldItemSourceSlot >= 0 && heldItemSourceSlot < 36) {\n                // Try to add back to source slot\n                if (\n                  inventory[heldItemSourceSlot].count === 0 ||\n                  inventory[heldItemSourceSlot].type === BLOCK_TYPES.AIR\n                ) {\n                  inventory[heldItemSourceSlot] = { ...heldItem };\n                } else {\n                  // Find first empty slot\n                  for (let i = 0; i < 36; i++) {\n                    if (inventory[i].count === 0 || inventory[i].type === BLOCK_TYPES.AIR) {\n                      inventory[i] = { ...heldItem };\n                      break;\n                    }\n                  }\n                }\n              } else {\n                // Held item was from crafting grid - add to inventory\n                for (let j = 0; j < heldItem.count; j++) {\n                  addToInventory(heldItem.type);\n                }\n              }\n              heldItem = null;\n              heldItemSourceSlot = -1;\n              updateHeldItemDisplay(null);\n            }\n\n            updateHotbarUI();\n          }\n        }\n\n        // Crafting UI is now integrated into the inventory - no separate modal needed\n\n        function createCraftingSlot(index, isTable = false) {\n          const slot = document.createElement('div');\n          slot.className = 'crafting-slot';\n          slot.dataset.craftingIndex = index;\n          slot.dataset.lastTapTime = '0';\n\n          const icon = document.createElement('img');\n          icon.className = 'inventory-slot-icon';\n          slot.appendChild(icon);\n\n          const count = document.createElement('span');\n          count.className = 'inventory-slot-count';\n          slot.appendChild(count);\n\n          const handleSlotClick = (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n\n            const grid = isTable ? craftingTableGrid : craftingGrid;\n            const gridItem = grid[index];\n\n            // Double-tap detection for quick transfer back to inventory\n            const now = Date.now();\n            const lastTap = parseInt(slot.dataset.lastTapTime || '0');\n            slot.dataset.lastTapTime = now.toString();\n\n            if (isTouchDevice && now - lastTap < 350 && !heldItem) {\n              // Double-tap: Quick transfer back to inventory\n              if (gridItem.count > 0 && gridItem.type !== BLOCK_TYPES.AIR) {\n                for (let j = 0; j < gridItem.count; j++) {\n                  addToInventory(gridItem.type);\n                }\n                grid[index] = { type: BLOCK_TYPES.AIR, count: 0 };\n                if (isTable) {\n                  updateCraftingTableUI();\n                  checkCraftingTableRecipe();\n                  updateCraftingTableInventoryUI();\n                } else {\n                  updateCraftingUI();\n                  checkCraftingRecipe();\n                  updateInventoryUI();\n                }\n                updateHotbarUI();\n                playSound('block_place_sound', false, 0.3);\n                return;\n              }\n              return;\n            }\n\n            handleCraftingSlotClick(index, isTable);\n          };\n\n          slot.addEventListener('click', handleSlotClick);\n          slot.addEventListener('touchend', handleSlotClick);\n\n          return slot;\n        }\n\n        function handleCraftingSlotClick(index, isTable = false) {\n          const grid = isTable ? craftingTableGrid : craftingGrid;\n          const gridItem = grid[index];\n\n          if (heldItem) {\n            // Place held item into crafting grid\n            if (gridItem.count === 0 || gridItem.type === BLOCK_TYPES.AIR) {\n              grid[index] = { type: heldItem.type, count: 1 };\n              heldItem.count--;\n              if (heldItem.count <= 0) {\n                heldItem = null;\n                heldItemSourceSlot = -1;\n              }\n            } else if (gridItem.type === heldItem.type && gridItem.count < 64) {\n              gridItem.count++;\n              heldItem.count--;\n              if (heldItem.count <= 0) {\n                heldItem = null;\n                heldItemSourceSlot = -1;\n              }\n            }\n          } else {\n            // Pick up from crafting grid\n            if (gridItem.count > 0 && gridItem.type !== BLOCK_TYPES.AIR) {\n              heldItem = { ...gridItem };\n              heldItemSourceSlot = isTable ? -200 - index : -100 - index; // Different range for table\n              grid[index] = { type: BLOCK_TYPES.AIR, count: 0 };\n            }\n          }\n\n          if (isTable) {\n            updateCraftingTableUI();\n            checkCraftingTableRecipe();\n          } else {\n            updateCraftingUI();\n            checkCraftingRecipe();\n          }\n          updateHeldItemDisplay(heldItem);\n        }\n\n        function getRecipeDescription(recipe) {\n          const ingredients = {};\n          recipe.pattern.flat().forEach((type) => {\n            if (type !== 0) {\n              ingredients[type] = (ingredients[type] || 0) + 1;\n            }\n          });\n\n          const parts = Object.entries(ingredients).map(\n            ([type, count]) => `${count}x ${getItemName(parseInt(type))}`\n          );\n\n          return `${parts.join(' + ')} \u2192 ${recipe.result.count}x ${getItemName(recipe.result.type)}`;\n        }\n\n        function showRecipePreview(recipe) {\n          // Show a brief message about what the recipe requires\n          const desc = getRecipeDescription(recipe);\n          showMessage(desc, '#FFD700', 3000);\n        }\n\n        function checkCraftingRecipe() {\n          // Convert 2x2 crafting grid to pattern (inventory crafting)\n          const currentPattern = [\n            [craftingGrid[0].type, craftingGrid[1].type, 0],\n            [craftingGrid[2].type, craftingGrid[3].type, 0],\n            [0, 0, 0],\n          ];\n\n          // Check each recipe\n          for (const recipe of CRAFTING_RECIPES) {\n            if (matchesRecipe(currentPattern, recipe.pattern)) {\n              craftingResult = { ...recipe.result };\n              updateCraftingResult(recipe.name);\n              return;\n            }\n          }\n\n          // No match\n          craftingResult = { type: BLOCK_TYPES.AIR, count: 0 };\n          updateCraftingResult('');\n        }\n\n        function matchesRecipe(current, pattern) {\n          // Check all 4 positions the pattern could start from (shifted)\n          for (let offsetX = 0; offsetX <= 2; offsetX++) {\n            for (let offsetY = 0; offsetY <= 2; offsetY++) {\n              if (matchesAtOffset(current, pattern, offsetX, offsetY)) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }\n\n        function matchesAtOffset(current, pattern, offsetX, offsetY) {\n          // Find the bounding box of the pattern\n          let minX = 3,\n            maxX = -1,\n            minY = 3,\n            maxY = -1;\n          for (let y = 0; y < 3; y++) {\n            for (let x = 0; x < 3; x++) {\n              if (pattern[y][x] !== 0) {\n                minX = Math.min(minX, x);\n                maxX = Math.max(maxX, x);\n                minY = Math.min(minY, y);\n                maxY = Math.max(maxY, y);\n              }\n            }\n          }\n\n          if (maxX < 0) return false; // Empty pattern\n\n          const patternWidth = maxX - minX + 1;\n          const patternHeight = maxY - minY + 1;\n\n          // Check if pattern fits at this offset\n          for (let y = 0; y < 3; y++) {\n            for (let x = 0; x < 3; x++) {\n              const patY = y - offsetY + minY;\n              const patX = x - offsetX + minX;\n\n              let expectedType = 0;\n              if (patY >= minY && patY <= maxY && patX >= minX && patX <= maxX) {\n                expectedType = pattern[patY][patX];\n              }\n\n              const currentType = current[y][x] || 0;\n\n              if (currentType !== expectedType) {\n                return false;\n              }\n            }\n          }\n\n          return true;\n        }\n\n        function craftItem(fromTable = false) {\n          if (craftingResult.type === BLOCK_TYPES.AIR || craftingResult.count === 0) {\n            return;\n          }\n\n          const grid = fromTable ? craftingTableGrid : craftingGrid;\n          const gridSize = fromTable ? 9 : 4;\n\n          // Add result to inventory\n          let added = false;\n\n          // Find existing stack\n          for (let i = 0; i < inventory.length; i++) {\n            if (\n              inventory[i].type === craftingResult.type &&\n              inventory[i].count + craftingResult.count <= 64\n            ) {\n              inventory[i].count += craftingResult.count;\n              added = true;\n              break;\n            }\n          }\n\n          // Find empty slot\n          if (!added) {\n            for (let i = 0; i < inventory.length; i++) {\n              if (inventory[i].count === 0 || inventory[i].type === BLOCK_TYPES.AIR) {\n                inventory[i] = { type: craftingResult.type, count: craftingResult.count };\n                added = true;\n                break;\n              }\n            }\n          }\n\n          if (!added) {\n            showMessage('Inventory full!', '#F44336', 2000);\n            return;\n          }\n\n          // Consume ingredients (1 from each non-empty slot)\n          for (let i = 0; i < gridSize; i++) {\n            if (grid[i].count > 0 && grid[i].type !== BLOCK_TYPES.AIR) {\n              grid[i].count--;\n              if (grid[i].count <= 0) {\n                grid[i] = { type: BLOCK_TYPES.AIR, count: 0 };\n              }\n            }\n          }\n\n          // Play sound\n          playSound('block_place_sound', false, 0.6);\n\n          // Update UI\n          if (fromTable) {\n            updateCraftingTableUI();\n            checkCraftingTableRecipe();\n            updateCraftingTableInventoryUI();\n          } else {\n            updateCraftingUI();\n            checkCraftingRecipe();\n            updateInventoryUI();\n          }\n          updateHotbarUI();\n\n          // Show feedback\n          showMessage(`Crafted ${getItemName(craftingResult.type)}!`, '#4CAF50', 1500);\n        }\n\n        function updateCraftingUI() {\n          const gridSlots = document.querySelectorAll('#crafting-grid .crafting-slot');\n          gridSlots.forEach((slot, i) => {\n            if (i >= craftingGrid.length) return;\n            const item = craftingGrid[i];\n            const icon = slot.querySelector('.inventory-slot-icon');\n            const count = slot.querySelector('.inventory-slot-count');\n\n            updateSlotIcon(icon, item.type);\n            count.textContent = item.count > 0 ? item.count : '';\n          });\n        }\n\n        function updateCraftingTableUI() {\n          const gridSlots = document.querySelectorAll('#crafting-table-grid .crafting-slot');\n          gridSlots.forEach((slot, i) => {\n            if (i >= craftingTableGrid.length) return;\n            const item = craftingTableGrid[i];\n            const icon = slot.querySelector('.inventory-slot-icon');\n            const count = slot.querySelector('.inventory-slot-count');\n\n            updateSlotIcon(icon, item.type);\n            count.textContent = item.count > 0 ? item.count : '';\n          });\n        }\n\n        function checkCraftingTableRecipe() {\n          // Convert 3x3 crafting table grid to pattern\n          const currentPattern = [\n            [craftingTableGrid[0].type, craftingTableGrid[1].type, craftingTableGrid[2].type],\n            [craftingTableGrid[3].type, craftingTableGrid[4].type, craftingTableGrid[5].type],\n            [craftingTableGrid[6].type, craftingTableGrid[7].type, craftingTableGrid[8].type],\n          ];\n\n          // Check each recipe\n          for (const recipe of CRAFTING_RECIPES) {\n            if (matchesRecipe(currentPattern, recipe.pattern)) {\n              craftingResult = { ...recipe.result };\n              updateCraftingTableResult(recipe.name);\n              return;\n            }\n          }\n\n          // No match\n          craftingResult = { type: BLOCK_TYPES.AIR, count: 0 };\n          updateCraftingTableResult('');\n        }\n\n        function updateCraftingTableResult(recipeName) {\n          const resultSlot = document.getElementById('crafting-table-result-slot');\n          const resultName = document.getElementById('crafting-table-result-name');\n\n          if (resultSlot) {\n            const icon = resultSlot.querySelector('.inventory-slot-icon');\n            const count = resultSlot.querySelector('.inventory-slot-count');\n\n            updateSlotIcon(icon, craftingResult.type);\n            count.textContent = craftingResult.count > 0 ? craftingResult.count : '';\n\n            if (craftingResult.count > 0) {\n              resultSlot.classList.add('has-item');\n            } else {\n              resultSlot.classList.remove('has-item');\n            }\n          }\n\n          if (resultName) {\n            resultName.textContent = recipeName;\n          }\n        }\n\n        function updateCraftingResult(recipeName) {\n          const resultSlot = document.getElementById('crafting-result-slot');\n          const resultName = document.getElementById('crafting-result-name');\n\n          if (resultSlot) {\n            const icon = resultSlot.querySelector('.inventory-slot-icon');\n            const count = resultSlot.querySelector('.inventory-slot-count');\n\n            updateSlotIcon(icon, craftingResult.type);\n            count.textContent = craftingResult.count > 0 ? craftingResult.count : '';\n\n            if (craftingResult.count > 0) {\n              resultSlot.classList.add('has-item');\n            } else {\n              resultSlot.classList.remove('has-item');\n            }\n          }\n\n          if (resultName) {\n            resultName.textContent = recipeName;\n          }\n        }\n\n        // toggleCrafting removed - crafting is now part of inventory\n\n        // Create Crafting Table UI (3x3 grid modal with full inventory)\n        function createCraftingTableUI() {\n          const craftingTableModal = document.createElement('div');\n          craftingTableModal.className = 'inventory-modal';\n          craftingTableModal.id = 'crafting-table-modal';\n\n          const container = document.createElement('div');\n          container.className = 'inventory-container';\n          container.style.position = 'relative';\n\n          // Close button\n          const closeBtn = document.createElement('button');\n          closeBtn.className = 'inventory-close-btn';\n          closeBtn.textContent = '\u2715';\n          closeBtn.addEventListener('click', () => closeCraftingTable());\n          container.appendChild(closeBtn);\n\n          // Title\n          const title = document.createElement('div');\n          title.className = 'inventory-title';\n          title.textContent = '\ud83d\udd28 Crafting Table';\n          container.appendChild(title);\n\n          // === CRAFTING SECTION (3x3 grid) ===\n          const craftingSection = document.createElement('div');\n          craftingSection.className = 'inventory-section';\n          craftingSection.style.marginBottom = '12px';\n\n          const craftingTitle = document.createElement('div');\n          craftingTitle.className = 'inventory-section-title';\n          craftingTitle.textContent = '\ud83d\udd28 Crafting (3x3)';\n          craftingSection.appendChild(craftingTitle);\n\n          // Crafting area (grid + arrow + result)\n          const craftingArea = document.createElement('div');\n          craftingArea.className = 'crafting-area';\n          craftingArea.style.cssText =\n            'display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px;';\n\n          // 3x3 crafting grid\n          const craftingGridEl = document.createElement('div');\n          craftingGridEl.className = 'crafting-grid';\n          craftingGridEl.id = 'crafting-table-grid';\n\n          for (let i = 0; i < 9; i++) {\n            const slot = createCraftingSlot(i, true);\n            craftingGridEl.appendChild(slot);\n          }\n          craftingArea.appendChild(craftingGridEl);\n\n          // Arrow\n          const arrow = document.createElement('div');\n          arrow.className = 'crafting-arrow';\n          arrow.textContent = '\u27a1\ufe0f';\n          arrow.style.fontSize = '28px';\n          craftingArea.appendChild(arrow);\n\n          // Result slot\n          const resultArea = document.createElement('div');\n          resultArea.style.cssText =\n            'display: flex; flex-direction: column; align-items: center; gap: 4px;';\n\n          const resultSlot = document.createElement('div');\n          resultSlot.className = 'crafting-result-slot';\n          resultSlot.id = 'crafting-table-result-slot';\n          resultSlot.style.cssText =\n            'width: 56px; height: 56px; background: rgba(76,175,80,0.2); border: 3px solid rgba(76,175,80,0.6); border-radius: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; position: relative; transition: all 0.15s ease;';\n\n          const resultIcon = document.createElement('img');\n          resultIcon.className = 'inventory-slot-icon';\n          resultIcon.style.cssText = 'width: 40px; height: 40px;';\n          resultSlot.appendChild(resultIcon);\n\n          const resultCount = document.createElement('span');\n          resultCount.className = 'inventory-slot-count';\n          resultSlot.appendChild(resultCount);\n\n          resultSlot.addEventListener('click', () => craftItem(true));\n          resultSlot.addEventListener('touchend', (e) => {\n            e.preventDefault();\n            craftItem(true);\n          });\n\n          resultArea.appendChild(resultSlot);\n\n          const resultName = document.createElement('div');\n          resultName.className = 'crafting-result-name';\n          resultName.id = 'crafting-table-result-name';\n          resultName.style.cssText =\n            'font-family: \"VT323\", monospace; font-size: 14px; color: rgba(255,255,255,0.7); text-align: center;';\n          resultName.textContent = '';\n          resultArea.appendChild(resultName);\n\n          craftingArea.appendChild(resultArea);\n          craftingSection.appendChild(craftingArea);\n\n          // Action buttons row (Clear Grid button for mobile)\n          const tableActionRow = document.createElement('div');\n          tableActionRow.style.cssText =\n            'display: flex; justify-content: center; gap: 8px; margin-bottom: 8px;';\n\n          const tableClearGridBtn = document.createElement('button');\n          tableClearGridBtn.id = 'clear-crafting-table-grid-btn';\n          tableClearGridBtn.style.cssText =\n            'background: rgba(244, 67, 54, 0.3); color: #f44336; border: 2px solid #f44336; border-radius: 8px; padding: 8px 16px; font-family: \"VT323\", monospace; font-size: 18px; cursor: pointer; touch-action: manipulation;';\n          tableClearGridBtn.textContent = '\ud83d\uddd1\ufe0f Clear Grid';\n          tableClearGridBtn.addEventListener('click', (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n            // Return all items in crafting table grid to inventory\n            for (let i = 0; i < 9; i++) {\n              if (craftingTableGrid[i].count > 0 && craftingTableGrid[i].type !== BLOCK_TYPES.AIR) {\n                for (let j = 0; j < craftingTableGrid[i].count; j++) {\n                  addToInventory(craftingTableGrid[i].type);\n                }\n                craftingTableGrid[i] = { type: BLOCK_TYPES.AIR, count: 0 };\n              }\n            }\n            updateCraftingTableUI();\n            checkCraftingTableRecipe();\n            updateCraftingTableInventoryUI();\n            updateHotbarUI();\n            playSound('block_place_sound', false, 0.3);\n          });\n          tableActionRow.appendChild(tableClearGridBtn);\n          craftingSection.appendChild(tableActionRow);\n\n          // Recipe hints (compact)\n          const recipeHints = document.createElement('div');\n          recipeHints.style.cssText =\n            'display: flex; flex-wrap: wrap; gap: 4px; justify-content: center;';\n          CRAFTING_RECIPES.forEach((recipe) => {\n            const hint = document.createElement('div');\n            hint.style.cssText =\n              'background: rgba(255,255,255,0.1); padding: 3px 6px; border-radius: 4px; font-family: \"Inter\", sans-serif; font-size: 10px; color: rgba(255,255,255,0.7); cursor: pointer; transition: all 0.2s;';\n            hint.textContent = recipe.name;\n            hint.title = getRecipeDescription(recipe);\n            hint.addEventListener('click', () => showRecipePreview(recipe));\n            recipeHints.appendChild(hint);\n          });\n          craftingSection.appendChild(recipeHints);\n\n          container.appendChild(craftingSection);\n\n          // === MAIN INVENTORY SECTION (slots 9-35) ===\n          const mainSection = document.createElement('div');\n          mainSection.className = 'inventory-section';\n\n          const mainTitle = document.createElement('div');\n          mainTitle.className = 'inventory-section-title';\n          mainTitle.textContent = 'Main Inventory';\n          mainSection.appendChild(mainTitle);\n\n          const mainGrid = document.createElement('div');\n          mainGrid.className = 'inventory-grid';\n          mainGrid.id = 'crafting-table-inventory-main-grid';\n\n          // Create 27 main inventory slots (3 rows of 9)\n          for (let i = 9; i < 36; i++) {\n            const slot = createCraftingTableInventorySlot(i);\n            mainGrid.appendChild(slot);\n          }\n          mainSection.appendChild(mainGrid);\n          container.appendChild(mainSection);\n\n          // === HOTBAR SECTION (slots 0-8) ===\n          const hotbarSection = document.createElement('div');\n          hotbarSection.className = 'inventory-section';\n\n          const hotbarTitle = document.createElement('div');\n          hotbarTitle.className = 'inventory-section-title';\n          hotbarTitle.textContent = 'Hotbar';\n          hotbarSection.appendChild(hotbarTitle);\n\n          const hotbarGrid = document.createElement('div');\n          hotbarGrid.className = 'inventory-grid';\n          hotbarGrid.id = 'crafting-table-inventory-hotbar-grid';\n\n          for (let i = 0; i < 9; i++) {\n            const slot = createCraftingTableInventorySlot(i, true);\n            hotbarGrid.appendChild(slot);\n          }\n          hotbarSection.appendChild(hotbarGrid);\n          container.appendChild(hotbarSection);\n\n          // Hint text\n          const hint = document.createElement('div');\n          hint.className = 'inventory-hint';\n          hint.textContent = isTouchDevice\n            ? 'Tap an item to select, then tap destination to move \u2022 Double-tap to quick transfer'\n            : 'Click an item to select, then click destination to move \u2022 Press E to close';\n          container.appendChild(hint);\n\n          craftingTableModal.appendChild(container);\n\n          // Close on background click\n          craftingTableModal.addEventListener('click', (e) => {\n            if (e.target === craftingTableModal) {\n              closeCraftingTable();\n            }\n          });\n\n          gameWorld.appendChild(craftingTableModal);\n        }\n\n        // Create inventory slot for crafting table UI\n        function createCraftingTableInventorySlot(index, isHotbar = false) {\n          const slot = document.createElement('div');\n          slot.className = 'inventory-slot' + (isHotbar ? ' hotbar-highlight' : '');\n          slot.dataset.slotIndex = index;\n          slot.dataset.craftingTableSlot = 'true';\n          slot.dataset.lastTapTime = '0';\n\n          const icon = document.createElement('img');\n          icon.className = 'inventory-slot-icon';\n          slot.appendChild(icon);\n\n          const count = document.createElement('span');\n          count.className = 'inventory-slot-count';\n          slot.appendChild(count);\n\n          // Click handler for picking up / placing items\n          const handleSlotClick = (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n\n            const slotIndex = parseInt(slot.dataset.slotIndex);\n            const slotItem = inventory[slotIndex];\n\n            // Double-tap detection for quick transfer to crafting grid\n            const now = Date.now();\n            const lastTap = parseInt(slot.dataset.lastTapTime || '0');\n            slot.dataset.lastTapTime = now.toString();\n\n            if (isTouchDevice && now - lastTap < 350 && !heldItem) {\n              // Double-tap: Quick transfer to crafting table grid\n              if (slotItem.count > 0 && slotItem.type !== BLOCK_TYPES.AIR) {\n                // Find first empty crafting slot in 3x3 grid\n                for (let i = 0; i < 9; i++) {\n                  if (\n                    craftingTableGrid[i].count === 0 ||\n                    craftingTableGrid[i].type === BLOCK_TYPES.AIR\n                  ) {\n                    craftingTableGrid[i] = { type: slotItem.type, count: 1 };\n                    slotItem.count--;\n                    if (slotItem.count <= 0) {\n                      inventory[slotIndex] = { type: BLOCK_TYPES.AIR, count: 0 };\n                    }\n                    updateCraftingTableUI();\n                    checkCraftingTableRecipe();\n                    updateCraftingTableInventoryUI();\n                    updateHotbarUI();\n                    playSound('block_place_sound', false, 0.3);\n                    return;\n                  }\n                }\n              }\n              return;\n            }\n\n            if (heldItem) {\n              // We're holding an item - try to place it\n              if (slotItem.count === 0 || slotItem.type === BLOCK_TYPES.AIR) {\n                // Empty slot - place held item here\n                inventory[slotIndex] = { ...heldItem };\n                heldItem = null;\n                heldItemSourceSlot = -1;\n              } else if (slotItem.type === heldItem.type && slotItem.count < 64) {\n                // Same type - try to stack\n                const spaceLeft = 64 - slotItem.count;\n                const toMove = Math.min(spaceLeft, heldItem.count);\n                slotItem.count += toMove;\n                heldItem.count -= toMove;\n                if (heldItem.count <= 0) {\n                  heldItem = null;\n                  heldItemSourceSlot = -1;\n                }\n              } else {\n                // Different type - swap items\n                const temp = { ...slotItem };\n                inventory[slotIndex] = { ...heldItem };\n                heldItem = temp;\n                heldItemSourceSlot = slotIndex;\n              }\n            } else {\n              // Not holding anything - pick up from this slot\n              if (slotItem.count > 0 && slotItem.type !== BLOCK_TYPES.AIR) {\n                heldItem = { ...slotItem };\n                heldItemSourceSlot = slotIndex;\n                inventory[slotIndex] = { type: BLOCK_TYPES.AIR, count: 0 };\n              }\n            }\n\n            updateCraftingTableInventoryUI();\n            updateHotbarUI();\n            updateHeldItemDisplay(heldItem);\n          };\n\n          slot.addEventListener('click', handleSlotClick);\n          slot.addEventListener('touchend', handleSlotClick);\n\n          return slot;\n        }\n\n        // Update inventory UI in crafting table modal\n        function updateCraftingTableInventoryUI() {\n          const mainGrid = document.getElementById('crafting-table-inventory-main-grid');\n          const hotbarGrid = document.getElementById('crafting-table-inventory-hotbar-grid');\n\n          if (mainGrid) {\n            const slots = mainGrid.querySelectorAll('.inventory-slot');\n            slots.forEach((slot) => {\n              const index = parseInt(slot.dataset.slotIndex);\n              const item = inventory[index];\n              const icon = slot.querySelector('.inventory-slot-icon');\n              const count = slot.querySelector('.inventory-slot-count');\n\n              updateSlotIcon(icon, item.type);\n              count.textContent = item.count > 0 ? item.count : '';\n            });\n          }\n\n          if (hotbarGrid) {\n            const slots = hotbarGrid.querySelectorAll('.inventory-slot');\n            slots.forEach((slot) => {\n              const index = parseInt(slot.dataset.slotIndex);\n              const item = inventory[index];\n              const icon = slot.querySelector('.inventory-slot-icon');\n              const count = slot.querySelector('.inventory-slot-count');\n\n              updateSlotIcon(icon, item.type);\n              count.textContent = item.count > 0 ? item.count : '';\n\n              // Highlight selected hotbar slot\n              if (index === selectedSlot) {\n                slot.classList.add('selected');\n              } else {\n                slot.classList.remove('selected');\n              }\n            });\n          }\n        }\n\n        function openCraftingTable() {\n          isCraftingTableOpen = true;\n          const modal = document.getElementById('crafting-table-modal');\n          if (modal) {\n            modal.classList.add('active');\n            updateCraftingTableUI();\n            updateCraftingTableInventoryUI();\n            checkCraftingTableRecipe();\n          }\n\n          // Exit pointer lock\n          try {\n            if (document.pointerLockElement) {\n              document.exitPointerLock?.();\n            }\n          } catch (e) {\n            lib.log('Error exiting pointer lock: ' + e.message);\n          }\n          isPointerLocked = false;\n        }\n\n        function closeCraftingTable() {\n          isCraftingTableOpen = false;\n          const modal = document.getElementById('crafting-table-modal');\n          if (modal) {\n            modal.classList.remove('active');\n          }\n\n          // Return crafting table items to inventory\n          for (let i = 0; i < 9; i++) {\n            if (craftingTableGrid[i].count > 0 && craftingTableGrid[i].type !== BLOCK_TYPES.AIR) {\n              for (let j = 0; j < craftingTableGrid[i].count; j++) {\n                addToInventory(craftingTableGrid[i].type);\n              }\n              craftingTableGrid[i] = { type: BLOCK_TYPES.AIR, count: 0 };\n            }\n          }\n\n          // If holding an item, return it\n          if (heldItem) {\n            for (let j = 0; j < heldItem.count; j++) {\n              addToInventory(heldItem.type);\n            }\n            heldItem = null;\n            heldItemSourceSlot = -1;\n            updateHeldItemDisplay(null);\n          }\n\n          updateHotbarUI();\n        }\n\n        // ===== CHEST STORAGE SYSTEM =====\n        function createChestUI() {\n          const chestModal = document.createElement('div');\n          chestModal.className = 'inventory-modal';\n          chestModal.id = 'chest-modal';\n\n          const container = document.createElement('div');\n          container.className = 'inventory-container';\n          container.style.position = 'relative';\n\n          // Close button\n          const closeBtn = document.createElement('button');\n          closeBtn.className = 'inventory-close-btn';\n          closeBtn.textContent = '\u2715';\n          closeBtn.addEventListener('click', () => closeChest());\n          container.appendChild(closeBtn);\n\n          // Title\n          const title = document.createElement('div');\n          title.className = 'inventory-title';\n          title.textContent = '\ud83d\udce6 Chest';\n          container.appendChild(title);\n\n          // === CHEST CONTENTS SECTION (27 slots - 3 rows of 9) ===\n          const chestSection = document.createElement('div');\n          chestSection.className = 'inventory-section';\n\n          const chestTitle = document.createElement('div');\n          chestTitle.className = 'inventory-section-title';\n          chestTitle.textContent = 'Chest Contents';\n          chestSection.appendChild(chestTitle);\n\n          const chestGrid = document.createElement('div');\n          chestGrid.className = 'inventory-grid';\n          chestGrid.id = 'chest-contents-grid';\n\n          // Create 27 chest slots (3 rows of 9)\n          for (let i = 0; i < 27; i++) {\n            const slot = createChestSlot(i);\n            chestGrid.appendChild(slot);\n          }\n          chestSection.appendChild(chestGrid);\n          container.appendChild(chestSection);\n\n          // === PLAYER INVENTORY SECTION (slots 9-35) ===\n          const mainSection = document.createElement('div');\n          mainSection.className = 'inventory-section';\n\n          const mainTitle = document.createElement('div');\n          mainTitle.className = 'inventory-section-title';\n          mainTitle.textContent = 'Inventory';\n          mainSection.appendChild(mainTitle);\n\n          const mainGrid = document.createElement('div');\n          mainGrid.className = 'inventory-grid';\n          mainGrid.id = 'chest-inventory-main-grid';\n\n          // Create 27 main inventory slots (3 rows of 9)\n          for (let i = 9; i < 36; i++) {\n            const slot = createChestInventorySlot(i);\n            mainGrid.appendChild(slot);\n          }\n          mainSection.appendChild(mainGrid);\n          container.appendChild(mainSection);\n\n          // === HOTBAR SECTION (slots 0-8) ===\n          const hotbarSection = document.createElement('div');\n          hotbarSection.className = 'inventory-section';\n\n          const hotbarTitle = document.createElement('div');\n          hotbarTitle.className = 'inventory-section-title';\n          hotbarTitle.textContent = 'Hotbar';\n          hotbarSection.appendChild(hotbarTitle);\n\n          const hotbarGrid = document.createElement('div');\n          hotbarGrid.className = 'inventory-grid';\n          hotbarGrid.id = 'chest-inventory-hotbar-grid';\n\n          for (let i = 0; i < 9; i++) {\n            const slot = createChestInventorySlot(i, true);\n            hotbarGrid.appendChild(slot);\n          }\n          hotbarSection.appendChild(hotbarGrid);\n          container.appendChild(hotbarSection);\n\n          // Hint text\n          const hint = document.createElement('div');\n          hint.className = 'inventory-hint';\n          hint.textContent = isTouchDevice\n            ? 'Tap to select item, then tap destination to move'\n            : 'Click to select item, then click destination to move \u2022 Press E to close';\n          container.appendChild(hint);\n\n          chestModal.appendChild(container);\n\n          // Close on background click\n          chestModal.addEventListener('click', (e) => {\n            if (e.target === chestModal) {\n              closeChest();\n            }\n          });\n\n          gameWorld.appendChild(chestModal);\n        }\n\n        function createChestSlot(index) {\n          const slot = document.createElement('div');\n          slot.className = 'inventory-slot';\n          slot.dataset.chestIndex = index;\n          slot.style.background = 'rgba(139, 90, 43, 0.4)'; // Brown tint for chest slots\n\n          const icon = document.createElement('img');\n          icon.className = 'inventory-slot-icon';\n          slot.appendChild(icon);\n\n          const count = document.createElement('span');\n          count.className = 'inventory-slot-count';\n          slot.appendChild(count);\n\n          const handleSlotClick = (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n\n            if (!openChestKey) return;\n            const chestInv = chestContents.get(openChestKey);\n            if (!chestInv) return;\n\n            const chestIndex = parseInt(slot.dataset.chestIndex);\n            const chestItem = chestInv[chestIndex];\n\n            if (heldItem) {\n              // Place held item into chest\n              if (chestItem.count === 0 || chestItem.type === BLOCK_TYPES.AIR) {\n                chestInv[chestIndex] = { ...heldItem };\n                heldItem = null;\n                heldItemSourceSlot = -1;\n              } else if (chestItem.type === heldItem.type && chestItem.count < 64) {\n                const spaceLeft = 64 - chestItem.count;\n                const toMove = Math.min(spaceLeft, heldItem.count);\n                chestItem.count += toMove;\n                heldItem.count -= toMove;\n                if (heldItem.count <= 0) {\n                  heldItem = null;\n                  heldItemSourceSlot = -1;\n                }\n              } else {\n                // Swap items\n                const temp = { ...chestItem };\n                chestInv[chestIndex] = { ...heldItem };\n                heldItem = temp;\n                heldItemSourceSlot = -300 - chestIndex; // Unique range for chest slots\n              }\n            } else {\n              // Pick up from chest\n              if (chestItem.count > 0 && chestItem.type !== BLOCK_TYPES.AIR) {\n                heldItem = { ...chestItem };\n                heldItemSourceSlot = -300 - chestIndex;\n                chestInv[chestIndex] = { type: BLOCK_TYPES.AIR, count: 0 };\n              }\n            }\n\n            // Sync chest update to other players\n            if (isOnline && openChestKey) {\n              const [cx, cy, cz] = openChestKey.split(',').map(Number);\n              syncEvent('chest_update', {\n                x: cx,\n                y: cy,\n                z: cz,\n                slotIndex: chestIndex,\n                item: { ...chestInv[chestIndex] },\n              });\n            }\n\n            updateChestUI();\n            updateChestInventoryUI();\n            updateHeldItemDisplay(heldItem);\n          };\n\n          slot.addEventListener('click', handleSlotClick);\n          slot.addEventListener('touchend', handleSlotClick);\n\n          return slot;\n        }\n\n        function createChestInventorySlot(index, isHotbar = false) {\n          const slot = document.createElement('div');\n          slot.className = 'inventory-slot' + (isHotbar ? ' hotbar-highlight' : '');\n          slot.dataset.slotIndex = index;\n          slot.dataset.chestInventorySlot = 'true';\n\n          const icon = document.createElement('img');\n          icon.className = 'inventory-slot-icon';\n          slot.appendChild(icon);\n\n          const count = document.createElement('span');\n          count.className = 'inventory-slot-count';\n          slot.appendChild(count);\n\n          const handleSlotClick = (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n\n            const slotIndex = parseInt(slot.dataset.slotIndex);\n            const slotItem = inventory[slotIndex];\n\n            if (heldItem) {\n              if (slotItem.count === 0 || slotItem.type === BLOCK_TYPES.AIR) {\n                inventory[slotIndex] = { ...heldItem };\n                heldItem = null;\n                heldItemSourceSlot = -1;\n              } else if (slotItem.type === heldItem.type && slotItem.count < 64) {\n                const spaceLeft = 64 - slotItem.count;\n                const toMove = Math.min(spaceLeft, heldItem.count);\n                slotItem.count += toMove;\n                heldItem.count -= toMove;\n                if (heldItem.count <= 0) {\n                  heldItem = null;\n                  heldItemSourceSlot = -1;\n                }\n              } else {\n                const temp = { ...slotItem };\n                inventory[slotIndex] = { ...heldItem };\n                heldItem = temp;\n                heldItemSourceSlot = slotIndex;\n              }\n            } else {\n              if (slotItem.count > 0 && slotItem.type !== BLOCK_TYPES.AIR) {\n                heldItem = { ...slotItem };\n                heldItemSourceSlot = slotIndex;\n                inventory[slotIndex] = { type: BLOCK_TYPES.AIR, count: 0 };\n              }\n            }\n\n            updateChestUI();\n            updateChestInventoryUI();\n            updateHotbarUI();\n            updateHeldItemDisplay(heldItem);\n          };\n\n          slot.addEventListener('click', handleSlotClick);\n          slot.addEventListener('touchend', handleSlotClick);\n\n          return slot;\n        }\n\n        function updateChestUI() {\n          if (!openChestKey) return;\n          const chestInv = chestContents.get(openChestKey);\n          if (!chestInv) return;\n\n          const chestGrid = document.getElementById('chest-contents-grid');\n          if (chestGrid) {\n            const slots = chestGrid.querySelectorAll('.inventory-slot');\n            slots.forEach((slot) => {\n              const index = parseInt(slot.dataset.chestIndex);\n              const item = chestInv[index];\n              const icon = slot.querySelector('.inventory-slot-icon');\n              const count = slot.querySelector('.inventory-slot-count');\n\n              updateSlotIcon(icon, item.type);\n              count.textContent = item.count > 0 ? item.count : '';\n            });\n          }\n        }\n\n        function updateChestInventoryUI() {\n          const mainGrid = document.getElementById('chest-inventory-main-grid');\n          const hotbarGrid = document.getElementById('chest-inventory-hotbar-grid');\n\n          if (mainGrid) {\n            const slots = mainGrid.querySelectorAll('.inventory-slot');\n            slots.forEach((slot) => {\n              const index = parseInt(slot.dataset.slotIndex);\n              const item = inventory[index];\n              const icon = slot.querySelector('.inventory-slot-icon');\n              const count = slot.querySelector('.inventory-slot-count');\n\n              updateSlotIcon(icon, item.type);\n              count.textContent = item.count > 0 ? item.count : '';\n            });\n          }\n\n          if (hotbarGrid) {\n            const slots = hotbarGrid.querySelectorAll('.inventory-slot');\n            slots.forEach((slot) => {\n              const index = parseInt(slot.dataset.slotIndex);\n              const item = inventory[index];\n              const icon = slot.querySelector('.inventory-slot-icon');\n              const count = slot.querySelector('.inventory-slot-count');\n\n              updateSlotIcon(icon, item.type);\n              count.textContent = item.count > 0 ? item.count : '';\n\n              if (index === selectedSlot) {\n                slot.classList.add('selected');\n              } else {\n                slot.classList.remove('selected');\n              }\n            });\n          }\n        }\n\n        function openChest(x, y, z) {\n          const key = `${x},${y},${z}`;\n\n          // Initialize chest contents if not exists\n          if (!chestContents.has(key)) {\n            const emptyChest = [];\n            for (let i = 0; i < 27; i++) {\n              emptyChest.push({ type: BLOCK_TYPES.AIR, count: 0 });\n            }\n            chestContents.set(key, emptyChest);\n          }\n\n          openChestKey = key;\n          isChestOpen = true;\n\n          const modal = document.getElementById('chest-modal');\n          if (modal) {\n            modal.classList.add('active');\n            updateChestUI();\n            updateChestInventoryUI();\n          }\n\n          // Exit pointer lock and pause\n          try {\n            if (document.pointerLockElement) {\n              document.exitPointerLock?.();\n            }\n          } catch (e) {\n            lib.log('Error exiting pointer lock: ' + e.message);\n          }\n          isPointerLocked = false;\n          isPaused = true;\n\n          playSound('block_place_sound', false, 0.4);\n        }\n\n        function closeChest() {\n          isChestOpen = false;\n          openChestKey = null;\n          isPaused = false;\n\n          const modal = document.getElementById('chest-modal');\n          if (modal) {\n            modal.classList.remove('active');\n          }\n\n          // If holding an item, return it to inventory\n          if (heldItem) {\n            for (let j = 0; j < heldItem.count; j++) {\n              addToInventory(heldItem.type);\n            }\n            heldItem = null;\n            heldItemSourceSlot = -1;\n            updateHeldItemDisplay(null);\n          }\n\n          updateHotbarUI();\n          playSound('block_place_sound', false, 0.3);\n        }\n\n        // ===== FURNACE SMELTING SYSTEM =====\n        function createFurnaceUI() {\n          const furnaceModal = document.createElement('div');\n          furnaceModal.className = 'inventory-modal';\n          furnaceModal.id = 'furnace-modal';\n\n          const container = document.createElement('div');\n          container.className = 'inventory-container';\n          container.style.position = 'relative';\n\n          // Close button\n          const closeBtn = document.createElement('button');\n          closeBtn.className = 'inventory-close-btn';\n          closeBtn.textContent = '\u2715';\n          closeBtn.addEventListener('click', () => closeFurnace());\n          container.appendChild(closeBtn);\n\n          // Title\n          const title = document.createElement('div');\n          title.className = 'inventory-title';\n          title.textContent = '\ud83d\udd25 Furnace';\n          container.appendChild(title);\n\n          // === FURNACE SMELTING SECTION ===\n          const smeltingSection = document.createElement('div');\n          smeltingSection.className = 'inventory-section';\n          smeltingSection.style.marginBottom = '12px';\n\n          const smeltingTitle = document.createElement('div');\n          smeltingTitle.className = 'inventory-section-title';\n          smeltingTitle.textContent = '\ud83d\udd25 Smelting';\n          smeltingSection.appendChild(smeltingTitle);\n\n          // Smelting area (input + fuel -> fire -> output)\n          const smeltingArea = document.createElement('div');\n          smeltingArea.style.cssText =\n            'display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; padding: 16px; background: rgba(0,0,0,0.2); border-radius: 8px;';\n\n          // Input/Fuel column\n          const inputColumn = document.createElement('div');\n          inputColumn.style.cssText =\n            'display: flex; flex-direction: column; align-items: center; gap: 8px;';\n\n          // Input slot (item to smelt)\n          const inputSlot = document.createElement('div');\n          inputSlot.className = 'inventory-slot';\n          inputSlot.id = 'furnace-input-slot';\n          inputSlot.style.cssText =\n            'width: 56px; height: 56px; background: rgba(60,60,60,0.8); border: 2px solid rgba(255,200,100,0.5); border-radius: 6px;';\n          const inputIcon = document.createElement('img');\n          inputIcon.className = 'inventory-slot-icon';\n          inputSlot.appendChild(inputIcon);\n          const inputCount = document.createElement('span');\n          inputCount.className = 'inventory-slot-count';\n          inputSlot.appendChild(inputCount);\n          inputSlot.addEventListener('click', () => handleFurnaceSlotClick('input'));\n          inputSlot.addEventListener('touchend', (e) => {\n            e.preventDefault();\n            handleFurnaceSlotClick('input');\n          });\n\n          const inputLabel = document.createElement('div');\n          inputLabel.style.cssText =\n            'font-family: \"VT323\", monospace; font-size: 14px; color: rgba(255,255,255,0.6);';\n          inputLabel.textContent = 'Input';\n\n          inputColumn.appendChild(inputSlot);\n          inputColumn.appendChild(inputLabel);\n\n          // Fuel slot\n          const fuelSlot = document.createElement('div');\n          fuelSlot.className = 'inventory-slot';\n          fuelSlot.id = 'furnace-fuel-slot';\n          fuelSlot.style.cssText =\n            'width: 56px; height: 56px; background: rgba(60,60,60,0.8); border: 2px solid rgba(255,100,50,0.5); border-radius: 6px;';\n          const fuelIcon = document.createElement('img');\n          fuelIcon.className = 'inventory-slot-icon';\n          fuelSlot.appendChild(fuelIcon);\n          const fuelCount = document.createElement('span');\n          fuelCount.className = 'inventory-slot-count';\n          fuelSlot.appendChild(fuelCount);\n          fuelSlot.addEventListener('click', () => handleFurnaceSlotClick('fuel'));\n          fuelSlot.addEventListener('touchend', (e) => {\n            e.preventDefault();\n            handleFurnaceSlotClick('fuel');\n          });\n\n          const fuelLabel = document.createElement('div');\n          fuelLabel.style.cssText =\n            'font-family: \"VT323\", monospace; font-size: 14px; color: rgba(255,255,255,0.6);';\n          fuelLabel.textContent = 'Fuel';\n\n          inputColumn.appendChild(fuelSlot);\n          inputColumn.appendChild(fuelLabel);\n\n          smeltingArea.appendChild(inputColumn);\n\n          // Fire/Progress indicator\n          const fireColumn = document.createElement('div');\n          fireColumn.style.cssText =\n            'display: flex; flex-direction: column; align-items: center; gap: 4px;';\n\n          const fireIndicator = document.createElement('div');\n          fireIndicator.id = 'furnace-fire-indicator';\n          fireIndicator.style.cssText = 'font-size: 32px; opacity: 0.3;';\n          fireIndicator.textContent = '\ud83d\udd25';\n\n          const progressBar = document.createElement('div');\n          progressBar.style.cssText =\n            'width: 40px; height: 6px; background: rgba(0,0,0,0.3); border-radius: 3px; overflow: hidden;';\n          const progressFill = document.createElement('div');\n          progressFill.id = 'furnace-progress-fill';\n          progressFill.style.cssText =\n            'width: 0%; height: 100%; background: linear-gradient(90deg, #ff6600, #ffcc00); transition: width 0.1s;';\n          progressBar.appendChild(progressFill);\n\n          const arrow = document.createElement('div');\n          arrow.style.cssText = 'font-size: 24px; color: rgba(255,255,255,0.5);';\n          arrow.textContent = '\u27a1\ufe0f';\n\n          fireColumn.appendChild(fireIndicator);\n          fireColumn.appendChild(progressBar);\n          fireColumn.appendChild(arrow);\n\n          smeltingArea.appendChild(fireColumn);\n\n          // Output slot\n          const outputColumn = document.createElement('div');\n          outputColumn.style.cssText =\n            'display: flex; flex-direction: column; align-items: center; gap: 8px;';\n\n          const outputSlot = document.createElement('div');\n          outputSlot.className = 'inventory-slot';\n          outputSlot.id = 'furnace-output-slot';\n          outputSlot.style.cssText =\n            'width: 64px; height: 64px; background: rgba(76,175,80,0.2); border: 3px solid rgba(76,175,80,0.6); border-radius: 6px;';\n          const outputIcon = document.createElement('img');\n          outputIcon.className = 'inventory-slot-icon';\n          outputIcon.style.cssText = 'width: 44px; height: 44px;';\n          outputSlot.appendChild(outputIcon);\n          const outputCount = document.createElement('span');\n          outputCount.className = 'inventory-slot-count';\n          outputSlot.appendChild(outputCount);\n          outputSlot.addEventListener('click', () => handleFurnaceSlotClick('output'));\n          outputSlot.addEventListener('touchend', (e) => {\n            e.preventDefault();\n            handleFurnaceSlotClick('output');\n          });\n\n          const outputLabel = document.createElement('div');\n          outputLabel.style.cssText =\n            'font-family: \"VT323\", monospace; font-size: 14px; color: rgba(255,255,255,0.6);';\n          outputLabel.textContent = 'Output';\n\n          outputColumn.appendChild(outputSlot);\n          outputColumn.appendChild(outputLabel);\n\n          smeltingArea.appendChild(outputColumn);\n\n          smeltingSection.appendChild(smeltingArea);\n\n          // Fuel hints\n          const fuelHints = document.createElement('div');\n          fuelHints.style.cssText =\n            'text-align: center; font-family: \"Inter\", sans-serif; font-size: 12px; color: rgba(255,255,255,0.5);';\n          fuelHints.textContent = 'Fuel: Coal (80s), Wood/Planks (15s), Sticks (5s)';\n          smeltingSection.appendChild(fuelHints);\n\n          container.appendChild(smeltingSection);\n\n          // === PLAYER INVENTORY SECTION (slots 9-35) ===\n          const mainSection = document.createElement('div');\n          mainSection.className = 'inventory-section';\n\n          const mainTitle = document.createElement('div');\n          mainTitle.className = 'inventory-section-title';\n          mainTitle.textContent = 'Inventory';\n          mainSection.appendChild(mainTitle);\n\n          const mainGrid = document.createElement('div');\n          mainGrid.className = 'inventory-grid';\n          mainGrid.id = 'furnace-inventory-main-grid';\n\n          for (let i = 9; i < 36; i++) {\n            const slot = createFurnaceInventorySlot(i);\n            mainGrid.appendChild(slot);\n          }\n          mainSection.appendChild(mainGrid);\n          container.appendChild(mainSection);\n\n          // === HOTBAR SECTION (slots 0-8) ===\n          const hotbarSection = document.createElement('div');\n          hotbarSection.className = 'inventory-section';\n\n          const hotbarTitle = document.createElement('div');\n          hotbarTitle.className = 'inventory-section-title';\n          hotbarTitle.textContent = 'Hotbar';\n          hotbarSection.appendChild(hotbarTitle);\n\n          const hotbarGrid = document.createElement('div');\n          hotbarGrid.className = 'inventory-grid';\n          hotbarGrid.id = 'furnace-inventory-hotbar-grid';\n\n          for (let i = 0; i < 9; i++) {\n            const slot = createFurnaceInventorySlot(i, true);\n            hotbarGrid.appendChild(slot);\n          }\n          hotbarSection.appendChild(hotbarGrid);\n          container.appendChild(hotbarSection);\n\n          // Hint text\n          const hint = document.createElement('div');\n          hint.className = 'inventory-hint';\n          hint.textContent = 'Add fuel and items to smelt \u2022 Smelting continues while open';\n          container.appendChild(hint);\n\n          furnaceModal.appendChild(container);\n\n          // Close on background click\n          furnaceModal.addEventListener('click', (e) => {\n            if (e.target === furnaceModal) {\n              closeFurnace();\n            }\n          });\n\n          gameWorld.appendChild(furnaceModal);\n        }\n\n        function createFurnaceInventorySlot(index, isHotbar = false) {\n          const slot = document.createElement('div');\n          slot.className = 'inventory-slot' + (isHotbar ? ' hotbar-highlight' : '');\n          slot.dataset.slotIndex = index;\n\n          const icon = document.createElement('img');\n          icon.className = 'inventory-slot-icon';\n          slot.appendChild(icon);\n\n          const count = document.createElement('span');\n          count.className = 'inventory-slot-count';\n          slot.appendChild(count);\n\n          const handleSlotClick = (e) => {\n            e.preventDefault();\n            e.stopPropagation();\n\n            const slotIndex = parseInt(slot.dataset.slotIndex);\n            const slotItem = inventory[slotIndex];\n\n            if (heldItem) {\n              if (slotItem.count === 0 || slotItem.type === BLOCK_TYPES.AIR) {\n                inventory[slotIndex] = { ...heldItem };\n                heldItem = null;\n                heldItemSourceSlot = -1;\n              } else if (slotItem.type === heldItem.type && slotItem.count < 64) {\n                const spaceLeft = 64 - slotItem.count;\n                const toMove = Math.min(spaceLeft, heldItem.count);\n                slotItem.count += toMove;\n                heldItem.count -= toMove;\n                if (heldItem.count <= 0) {\n                  heldItem = null;\n                  heldItemSourceSlot = -1;\n                }\n              } else {\n                const temp = { ...slotItem };\n                inventory[slotIndex] = { ...heldItem };\n                heldItem = temp;\n                heldItemSourceSlot = slotIndex;\n              }\n            } else {\n              if (slotItem.count > 0 && slotItem.type !== BLOCK_TYPES.AIR) {\n                heldItem = { ...slotItem };\n                heldItemSourceSlot = slotIndex;\n                inventory[slotIndex] = { type: BLOCK_TYPES.AIR, count: 0 };\n              }\n            }\n\n            updateFurnaceUI();\n            updateFurnaceInventoryUI();\n            updateHotbarUI();\n            updateHeldItemDisplay(heldItem);\n          };\n\n          slot.addEventListener('click', handleSlotClick);\n          slot.addEventListener('touchend', handleSlotClick);\n\n          return slot;\n        }\n\n        function handleFurnaceSlotClick(slotType) {\n          if (!openFurnaceKey) return;\n          let furnace = furnaceContents.get(openFurnaceKey);\n          if (!furnace) return;\n\n          const slotItem = furnace[slotType];\n\n          if (heldItem) {\n            // Placing items\n            if (slotType === 'output') {\n              // Can only take from output, not place\n              return;\n            }\n\n            // Validate fuel slot - only accept fuel items\n            if (slotType === 'fuel' && !FUEL_BURN_TIMES[heldItem.type]) {\n              showMessage('Not a valid fuel!', '#FF9800', 1500);\n              return;\n            }\n\n            // Validate input slot - only accept smeltable items\n            if (slotType === 'input' && !SMELTING_RECIPES[heldItem.type]) {\n              showMessage('Cannot smelt this item!', '#FF9800', 1500);\n              return;\n            }\n\n            if (slotItem.count === 0 || slotItem.type === BLOCK_TYPES.AIR) {\n              furnace[slotType] = { type: heldItem.type, count: 1 };\n              heldItem.count--;\n              if (heldItem.count <= 0) {\n                heldItem = null;\n                heldItemSourceSlot = -1;\n              }\n            } else if (slotItem.type === heldItem.type && slotItem.count < 64) {\n              slotItem.count++;\n              heldItem.count--;\n              if (heldItem.count <= 0) {\n                heldItem = null;\n                heldItemSourceSlot = -1;\n              }\n            }\n          } else {\n            // Picking up items\n            if (slotItem.count > 0 && slotItem.type !== BLOCK_TYPES.AIR) {\n              heldItem = { ...slotItem };\n              heldItemSourceSlot = -400; // Unique range for furnace slots\n              furnace[slotType] = { type: BLOCK_TYPES.AIR, count: 0 };\n            }\n          }\n\n          // Sync furnace update to other players\n          if (isOnline && openFurnaceKey) {\n            const [fx, fy, fz] = openFurnaceKey.split(',').map(Number);\n            syncEvent('furnace_update', {\n              x: fx,\n              y: fy,\n              z: fz,\n              slotType: slotType,\n              item: { ...furnace[slotType] },\n            });\n          }\n\n          updateFurnaceUI();\n          updateHeldItemDisplay(heldItem);\n        }\n\n        function updateFurnaceUI() {\n          if (!openFurnaceKey) return;\n          const furnace = furnaceContents.get(openFurnaceKey);\n          if (!furnace) return;\n\n          // Update input slot\n          const inputSlot = document.getElementById('furnace-input-slot');\n          if (inputSlot) {\n            const icon = inputSlot.querySelector('.inventory-slot-icon');\n            const count = inputSlot.querySelector('.inventory-slot-count');\n            updateSlotIcon(icon, furnace.input.type);\n            count.textContent = furnace.input.count > 0 ? furnace.input.count : '';\n          }\n\n          // Update fuel slot\n          const fuelSlot = document.getElementById('furnace-fuel-slot');\n          if (fuelSlot) {\n            const icon = fuelSlot.querySelector('.inventory-slot-icon');\n            const count = fuelSlot.querySelector('.inventory-slot-count');\n            updateSlotIcon(icon, furnace.fuel.type);\n            count.textContent = furnace.fuel.count > 0 ? furnace.fuel.count : '';\n          }\n\n          // Update output slot\n          const outputSlot = document.getElementById('furnace-output-slot');\n          if (outputSlot) {\n            const icon = outputSlot.querySelector('.inventory-slot-icon');\n            const count = outputSlot.querySelector('.inventory-slot-count');\n            updateSlotIcon(icon, furnace.output.type);\n            count.textContent = furnace.output.count > 0 ? furnace.output.count : '';\n          }\n\n          // Update fire indicator\n          const fireIndicator = document.getElementById('furnace-fire-indicator');\n          if (fireIndicator) {\n            fireIndicator.style.opacity = furnace.fuelProgress > 0 ? '1' : '0.3';\n          }\n\n          // Update progress bar\n          const progressFill = document.getElementById('furnace-progress-fill');\n          if (progressFill) {\n            progressFill.style.width = (furnace.progress / SMELT_TIME) * 100 + '%';\n          }\n        }\n\n        function updateFurnaceInventoryUI() {\n          const mainGrid = document.getElementById('furnace-inventory-main-grid');\n          const hotbarGrid = document.getElementById('furnace-inventory-hotbar-grid');\n\n          if (mainGrid) {\n            const slots = mainGrid.querySelectorAll('.inventory-slot');\n            slots.forEach((slot) => {\n              const index = parseInt(slot.dataset.slotIndex);\n              const item = inventory[index];\n              const icon = slot.querySelector('.inventory-slot-icon');\n              const count = slot.querySelector('.inventory-slot-count');\n              updateSlotIcon(icon, item.type);\n              count.textContent = item.count > 0 ? item.count : '';\n            });\n          }\n\n          if (hotbarGrid) {\n            const slots = hotbarGrid.querySelectorAll('.inventory-slot');\n            slots.forEach((slot) => {\n              const index = parseInt(slot.dataset.slotIndex);\n              const item = inventory[index];\n              const icon = slot.querySelector('.inventory-slot-icon');\n              const count = slot.querySelector('.inventory-slot-count');\n              updateSlotIcon(icon, item.type);\n              count.textContent = item.count > 0 ? item.count : '';\n              if (index === selectedSlot) {\n                slot.classList.add('selected');\n              } else {\n                slot.classList.remove('selected');\n              }\n            });\n          }\n        }\n\n        function openFurnace(x, y, z) {\n          const key = `${x},${y},${z}`;\n\n          // Initialize furnace contents if not exists\n          if (!furnaceContents.has(key)) {\n            furnaceContents.set(key, {\n              input: { type: BLOCK_TYPES.AIR, count: 0 },\n              fuel: { type: BLOCK_TYPES.AIR, count: 0 },\n              output: { type: BLOCK_TYPES.AIR, count: 0 },\n              progress: 0,\n              fuelProgress: 0,\n            });\n          }\n\n          openFurnaceKey = key;\n          isFurnaceOpen = true;\n\n          const modal = document.getElementById('furnace-modal');\n          if (modal) {\n            modal.classList.add('active');\n            updateFurnaceUI();\n            updateFurnaceInventoryUI();\n          }\n\n          // Exit pointer lock and pause\n          try {\n            if (document.pointerLockElement) {\n              document.exitPointerLock?.();\n            }\n          } catch (e) {\n            lib.log('Error exiting pointer lock: ' + e.message);\n          }\n          isPointerLocked = false;\n          isPaused = true;\n\n          playSound('block_place_sound', false, 0.4);\n        }\n\n        function closeFurnace() {\n          isFurnaceOpen = false;\n          openFurnaceKey = null;\n          isPaused = false;\n\n          const modal = document.getElementById('furnace-modal');\n          if (modal) {\n            modal.classList.remove('active');\n          }\n\n          // If holding an item, return it to inventory\n          if (heldItem) {\n            for (let j = 0; j < heldItem.count; j++) {\n              addToInventory(heldItem.type);\n            }\n            heldItem = null;\n            heldItemSourceSlot = -1;\n            updateHeldItemDisplay(null);\n          }\n\n          updateHotbarUI();\n          playSound('block_place_sound', false, 0.3);\n        }\n\n        // Add furnace light and lit front face when smelting\n        function addFurnaceLight(x, y, z) {\n          const key = `${x},${y},${z}`;\n          if (furnaceLights.has(key)) return; // Already has light\n\n          const rotation = furnaceOrientations.get(key) || 0;\n\n          const light = new THREE.PointLight(0xff6600, 1.5, 10, 1.5);\n          let lightOffset = new THREE.Vector3(0, 0, 0.7);\n          lightOffset.applyAxisAngle(new THREE.Vector3(0, 1, 0), rotation);\n          light.position.set(x + 0.5 + lightOffset.x, y + 0.5, z + 0.5 + lightOffset.z);\n          scene.add(light);\n\n          // Create lit furnace front face overlay\n          let frontMesh = null;\n          const frontOnTexture = blockTextures[BLOCK_TYPES.FURNACE]?.frontOn;\n          if (frontOnTexture) {\n            const frontGeo = new THREE.PlaneGeometry(1, 1);\n            const frontMat = new THREE.MeshBasicMaterial({\n              map: frontOnTexture,\n              transparent: false,\n            });\n            frontMesh = new THREE.Mesh(frontGeo, frontMat);\n\n            let meshOffset = new THREE.Vector3(0, 0, 0.501);\n            meshOffset.applyAxisAngle(new THREE.Vector3(0, 1, 0), rotation);\n\n            // Position slightly in front of the furnace block's front face\n            frontMesh.position.set(x + 0.5 + meshOffset.x, y + 0.5, z + 0.5 + meshOffset.z);\n            frontMesh.rotation.y = rotation;\n\n            scene.add(frontMesh);\n          }\n\n          // Create glowing overlay for the furnace block itself\n          const glowGeo = new THREE.BoxGeometry(1.02, 1.02, 1.02);\n          const glowMat = new THREE.MeshBasicMaterial({\n            color: 0xff6600,\n            transparent: true,\n            opacity: 0.15,\n            side: THREE.FrontSide,\n          });\n          const glowMesh = new THREE.Mesh(glowGeo, glowMat);\n          glowMesh.position.set(x + 0.5, y + 0.5, z + 0.5);\n          scene.add(glowMesh);\n\n          furnaceLights.set(key, { light, lastParticleTime: 0, frontMesh, glowMesh, rotation });\n        }\n\n        // Remove furnace light and lit front face\n        function removeFurnaceLight(x, y, z) {\n          const key = `${x},${y},${z}`;\n          const lightData = furnaceLights.get(key);\n          if (lightData) {\n            if (lightData.light) {\n              scene.remove(lightData.light);\n              lightData.light.dispose();\n            }\n            // Remove lit front face mesh\n            if (lightData.frontMesh) {\n              scene.remove(lightData.frontMesh);\n              lightData.frontMesh.geometry.dispose();\n              lightData.frontMesh.material.dispose();\n            }\n            // Remove glow mesh\n            if (lightData.glowMesh) {\n              scene.remove(lightData.glowMesh);\n              lightData.glowMesh.geometry.dispose();\n              lightData.glowMesh.material.dispose();\n            }\n            furnaceLights.delete(key);\n          }\n        }\n\n        // Create smoke and fire particles for active furnaces\n        function createFurnaceParticles(x, y, z, rotation = 0) {\n          // Smoke particles - gray, rise upward\n          if (Math.random() < 0.3) {\n            const smokeGeo = new THREE.BoxGeometry(0.08, 0.08, 0.08);\n            const smokeMat = new THREE.MeshBasicMaterial({\n              color: new THREE.Color(\n                0.3 + Math.random() * 0.2,\n                0.3 + Math.random() * 0.2,\n                0.3 + Math.random() * 0.2\n              ),\n              transparent: true,\n              opacity: 0.6,\n            });\n            const smoke = new THREE.Mesh(smokeGeo, smokeMat);\n            smoke.position.set(\n              x + 0.5 + (Math.random() - 0.5) * 0.3,\n              y + 1.0,\n              z + 0.5 + (Math.random() - 0.5) * 0.3\n            );\n            smoke.userData.velocity = new THREE.Vector3(\n              (Math.random() - 0.5) * 0.3,\n              1.5 + Math.random() * 0.5,\n              (Math.random() - 0.5) * 0.3\n            );\n            smoke.userData.life = 1.5 + Math.random() * 0.5;\n            scene.add(smoke);\n            particles.push(smoke);\n          }\n\n          // Fire/ember particles - orange/yellow, flicker near front\n          if (Math.random() < 0.4) {\n            const fireGeo = new THREE.BoxGeometry(0.06, 0.06, 0.06);\n            const fireMat = new THREE.MeshBasicMaterial({\n              color: new THREE.Color(1, 0.3 + Math.random() * 0.5, 0),\n              transparent: true,\n              opacity: 0.9,\n            });\n            const fire = new THREE.Mesh(fireGeo, fireMat);\n            // Spawn near the front of furnace\n            let offset = new THREE.Vector3(\n              (Math.random() - 0.5) * 0.3,\n              -0.2 + Math.random() * 0.4,\n              0.4 + Math.random() * 0.2\n            );\n            offset.applyAxisAngle(new THREE.Vector3(0, 1, 0), rotation);\n            fire.position.set(x + 0.5 + offset.x, y + 0.5 + offset.y, z + 0.5 + offset.z);\n            fire.userData.velocity = new THREE.Vector3(\n              (Math.random() - 0.5) * 0.5,\n              1.0 + Math.random() * 1.5,\n              (Math.random() - 0.5) * 0.3\n            );\n            fire.userData.life = 0.5 + Math.random() * 0.3;\n            scene.add(fire);\n            particles.push(fire);\n          }\n        }\n\n        // Update furnace visual effects (particles and light flicker)\n        function updateFurnaceVisuals(dt) {\n          furnaceLights.forEach((lightData, key) => {\n            const [x, y, z] = key.split(',').map(Number);\n\n            // Light flicker effect\n            if (lightData.light) {\n              lightData.light.intensity = 1.3 + Math.sin(performance.now() * 0.01) * 0.3;\n            }\n\n            // Glow mesh pulsing effect\n            if (lightData.glowMesh) {\n              const pulse = 0.12 + Math.sin(performance.now() * 0.005) * 0.05;\n              lightData.glowMesh.material.opacity = pulse;\n            }\n\n            // Spawn particles periodically\n            lightData.lastParticleTime = (lightData.lastParticleTime || 0) + dt;\n            if (lightData.lastParticleTime > 0.05) {\n              // Every 50ms\n              createFurnaceParticles(x, y, z, lightData.rotation || 0);\n              lightData.lastParticleTime = 0;\n            }\n          });\n        }\n\n        // Update all active furnaces (smelting logic)\n        function updateFurnaces(dt) {\n          furnaceContents.forEach((furnace, key) => {\n            const inputType = furnace.input.type;\n            const fuelType = furnace.fuel.type;\n            const outputType = furnace.output.type;\n\n            // Check if we can smelt\n            const recipe = SMELTING_RECIPES[inputType];\n            const canSmelt =\n              recipe &&\n              furnace.input.count > 0 &&\n              (outputType === BLOCK_TYPES.AIR ||\n                (outputType === recipe && furnace.output.count < 64));\n\n            // Track if furnace was burning before this update\n            const wasBurning = furnace.fuelProgress > 0;\n\n            // Consume fuel if needed\n            if (canSmelt && furnace.fuelProgress <= 0 && furnace.fuel.count > 0) {\n              const burnTime = FUEL_BURN_TIMES[fuelType];\n              if (burnTime) {\n                furnace.fuelProgress = burnTime;\n                furnace.fuel.count--;\n                if (furnace.fuel.count <= 0) {\n                  furnace.fuel = { type: BLOCK_TYPES.AIR, count: 0 };\n                }\n              }\n            }\n\n            // Smelt if we have fuel burning\n            if (furnace.fuelProgress > 0) {\n              furnace.fuelProgress -= dt;\n\n              if (canSmelt) {\n                furnace.progress += dt;\n\n                // Complete smelting\n                if (furnace.progress >= SMELT_TIME) {\n                  furnace.progress = 0;\n                  furnace.input.count--;\n                  if (furnace.input.count <= 0) {\n                    furnace.input = { type: BLOCK_TYPES.AIR, count: 0 };\n                  }\n\n                  // Add to output\n                  if (furnace.output.type === BLOCK_TYPES.AIR) {\n                    furnace.output = { type: recipe, count: 1 };\n                  } else {\n                    furnace.output.count++;\n                  }\n\n                  // Play sound\n                  playSound('block_place_sound', false, 0.3);\n                }\n              }\n            } else {\n              // No fuel - reset progress\n              furnace.progress = Math.max(0, furnace.progress - dt * 2);\n            }\n\n            // Check if burning state changed - add/remove light\n            const isBurning = furnace.fuelProgress > 0;\n            const [x, y, z] = key.split(',').map(Number);\n\n            if (isBurning && !furnaceLights.has(key)) {\n              // Started burning - add light\n              addFurnaceLight(x, y, z);\n            } else if (!isBurning && furnaceLights.has(key)) {\n              // Stopped burning - remove light\n              removeFurnaceLight(x, y, z);\n            }\n\n            // Update UI if this furnace is open\n            if (openFurnaceKey === key && isFurnaceOpen) {\n              updateFurnaceUI();\n            }\n          });\n        }\n\n        // Pause menu state\n        let isPaused = false;\n        let isCreativeMode = false;\n\n        function togglePOV() {\n          isFirstPerson = !isFirstPerson;\n          updateCamera();\n          const modeName = isFirstPerson ? '1st Person' : '3rd Person';\n          showMessage(`Camera: ${modeName}`, '#2196F3', 1000);\n        }\n\n        function createPauseMenu() {\n          const pauseMenu = document.createElement('div');\n          pauseMenu.id = 'pause-menu';\n          pauseMenu.style.cssText = `\n                    position: absolute;\n                    top: 0;\n                    left: 0;\n                    width: 100%;\n                    height: 100%;\n                    background: rgba(0, 0, 0, 0.85);\n                    display: none;\n                    align-items: center;\n                    justify-content: center;\n                    z-index: 150;\n                    backdrop-filter: blur(4px);\n                `;\n\n          const menuContent = document.createElement('div');\n          menuContent.style.cssText = `\n                    background: rgba(20, 20, 30, 0.95);\n                    padding: 40px 50px;\n                    border-radius: 16px;\n                    border: 3px solid rgba(255, 255, 255, 0.2);\n                    text-align: center;\n                    min-width: 300px;\n                `;\n\n          menuContent.innerHTML = `\n                    <h2 style=\"font-family: 'VT323', monospace; font-size: 48px; color: white; margin-bottom: 30px; letter-spacing: 2px;\">PAUSED</h2>\n                    <div style=\"display: flex; flex-direction: column; gap: 16px;\">\n                        <button id=\"resume-button\" style=\"\n                            font-family: 'VT323', monospace;\n                            font-size: 28px;\n                            padding: 14px 28px;\n                            background: rgba(76, 175, 80, 0.3);\n                            color: #4CAF50;\n                            border: 2px solid #4CAF50;\n                            border-radius: 10px;\n                            cursor: pointer;\n                            transition: all 0.2s;\n                        \">\u25b6 Resume</button>\n                        <button id=\"creative-mode-button\" style=\"\n                            font-family: 'VT323', monospace;\n                            font-size: 28px;\n                            padding: 14px 28px;\n                            background: rgba(156, 39, 176, 0.3);\n                            color: #E1BEE7;\n                            border: 2px solid #9C27B0;\n                            border-radius: 10px;\n                            cursor: pointer;\n                            transition: all 0.2s;\n                        \">\ud83e\uddf1 Creative Mode: OFF</button>\n                        <button id=\"pause-save-button\" style=\"\n                            font-family: 'VT323', monospace;\n                            font-size: 28px;\n                            padding: 14px 28px;\n                            background: rgba(33, 150, 243, 0.3);\n                            color: #2196F3;\n                            border: 2px solid #2196F3;\n                            border-radius: 10px;\n                            cursor: pointer;\n                            transition: all 0.2s;\n                        \">\ud83d\udcbe Save Game</button>\n                        <button id=\"pause-load-button\" style=\"\n                            font-family: 'VT323', monospace;\n                            font-size: 28px;\n                            padding: 14px 28px;\n                            background: rgba(255, 152, 0, 0.3);\n                            color: #FF9800;\n                            border: 2px solid #FF9800;\n                            border-radius: 10px;\n                            cursor: pointer;\n                            transition: all 0.2s;\n                        \">\ud83d\udcc2 Load Game</button>\n                        <button id=\"pause-delete-button\" style=\"\n                            font-family: 'VT323', monospace;\n                            font-size: 24px;\n                            padding: 10px 24px;\n                            background: rgba(244, 67, 54, 0.3);\n                            color: #F44336;\n                            border: 2px solid #F44336;\n                            border-radius: 10px;\n                            cursor: pointer;\n                            transition: all 0.2s;\n                            margin-top: 8px;\n                        \">\ud83d\uddd1\ufe0f Delete Save</button>\n                    </div>\n                    <p style=\"font-family: 'Inter', sans-serif; font-size: 18px; color: rgba(255,255,255,0.6); margin-top: 24px;\">Press M to resume</p>\n                `;\n\n          pauseMenu.appendChild(menuContent);\n          gameWorld.appendChild(pauseMenu);\n\n          // Add hover effects\n          const buttons = menuContent.querySelectorAll('button');\n          buttons.forEach((button) => {\n            button.addEventListener('mouseenter', () => {\n              button.style.transform = 'scale(1.05)';\n              button.style.opacity = '1';\n            });\n            button.addEventListener('mouseleave', () => {\n              button.style.transform = 'scale(1)';\n              button.style.opacity = '0.9';\n            });\n          });\n\n          // Wire up buttons\n          document.getElementById('resume-button').addEventListener('click', togglePauseMenu);\n          document.getElementById('creative-mode-button').addEventListener('click', toggleCreativeMode);\n          document.getElementById('pause-save-button').addEventListener('click', async () => {\n            await saveGame();\n          });\n          document.getElementById('pause-load-button').addEventListener('click', async () => {\n            await loadGame();\n          });\n          document.getElementById('pause-delete-button').addEventListener('click', async () => {\n            await deleteSave();\n          });\n        }\n\n        function updateCreativeModeButton() {\n          const btn = document.getElementById('creative-mode-button');\n          if (!btn) return;\n          if (isCreativeMode) {\n            btn.textContent = '\ud83e\uddf1 Creative Mode: ON';\n            btn.style.background = 'rgba(156, 39, 176, 0.6)';\n            btn.style.color = '#FFFFFF';\n            btn.style.borderColor = '#CE93D8';\n          } else {\n            btn.textContent = '\ud83e\uddf1 Creative Mode: OFF';\n            btn.style.background = 'rgba(156, 39, 176, 0.3)';\n            btn.style.color = '#E1BEE7';\n            btn.style.borderColor = '#9C27B0';\n          }\n        }\n\n        function toggleCreativeMode() {\n          isCreativeMode = !isCreativeMode;\n          updateCreativeModeButton();\n          showMessage(isCreativeMode ? 'Creative Mode Enabled' : 'Creative Mode Disabled', '#CE93D8', 1500);\n        }\n\n        function togglePauseMenu() {\n          isPaused = !isPaused;\n          const pauseMenu = document.getElementById('pause-menu');\n\n          if (isPaused) {\n            // Show menu and exit pointer lock\n            pauseMenu.style.display = 'flex';\n            updateCreativeModeButton();\n            try {\n              if (document.pointerLockElement) {\n                document.exitPointerLock?.();\n              }\n            } catch (e) {\n              lib.log('Error exiting pointer lock: ' + e.message);\n            }\n            isPointerLocked = false;\n          } else {\n            // Hide menu\n            pauseMenu.style.display = 'none';\n          }\n        }\n\n        // Input handlers\n        function setupInputHandlers() {\n          // Keyboard\n          document.addEventListener('keydown', (e) => {\n            if (mode === 'edit') return;\n\n            const activeTag = document.activeElement && document.activeElement.tagName;\n            const isTypingInInput =\n              activeTag === 'INPUT' || activeTag === 'TEXTAREA' || document.activeElement?.isContentEditable;\n            if (isTypingInInput) {\n              // Let chat/input fields receive keystrokes without driving gameplay controls\n              return;\n            }\n\n            // M for pause menu\n            if (e.key.toLowerCase() === 'm') {\n              togglePauseMenu();\n              return;\n            }\n\n            // L for worlds menu (only if worlds UI is enabled)\n            if (e.key.toLowerCase() === 'l' && config.showWorldsUI) {\n              if (typeof window.toggleWorldsModal === 'function') {\n                window.toggleWorldsModal(true);\n              }\n              return;\n            }\n\n            // E for inventory (now includes crafting)\n            if (e.key.toLowerCase() === 'e') {\n              if (isFurnaceOpen) {\n                closeFurnace();\n              } else if (isChestOpen) {\n                closeChest();\n              } else if (isCraftingTableOpen) {\n                closeCraftingTable();\n              } else {\n                toggleInventory();\n              }\n              return;\n            }\n\n            // T for text chat: open and focus input\n            if (e.key.toLowerCase() === 't') {\n              if (typeof window.toggleTextChatWindow === 'function') {\n                window.toggleTextChatWindow(true);\n                const chatInputField = document.getElementById('chat-input-field');\n                if (chatInputField) {\n                  setTimeout(() => {\n                    chatInputField.focus();\n                    chatInputField.setSelectionRange(\n                      chatInputField.value.length,\n                      chatInputField.value.length\n                    );\n                  }, 60);\n                }\n              }\n              e.preventDefault();\n              return;\n            }\n\n            // Don't process other keys if paused or inventory/crafting/chest open\n            if (isPaused || isInventoryOpen || isCraftingTableOpen || isChestOpen) return;\n\n            // Emoji hotkeys: Shift+1 through Shift+6\n            if (e.shiftKey && e.code && e.code.startsWith('Digit')) {\n              const emojiNum = parseInt(e.code.replace('Digit', ''));\n              if (emojiNum >= 1 && emojiNum <= 5) {\n                const emojiHotkeys = ['\ud83d\ude00', '\ud83d\udc4d', '\u26a0\ufe0f', '\ud83c\udfc3', '\ud83d\udcaa'];\n                const emoji = emojiHotkeys[emojiNum - 1];\n                if (typeof window.addChatMessage === 'function') {\n                  window.addChatMessage('You', emoji, true);\n                }\n                if (isOnline)\n                  syncEvent('emoji', {\n                    emojiIndex: quickEmojis.indexOf(emoji),\n                  });\n                if (typeof window.showEmojiFeedback === 'function') window.showEmojiFeedback(emoji);\n                e.preventDefault();\n                return;\n              }\n            }\n\n            // Debug: Shift+O to spawn enemy\n            if (e.shiftKey && e.key.toLowerCase() === 'o') {\n              spawnMob2();\n                return;\n            }\n\n            // F3 for debug UI toggle\n            if (e.key === 'F3') {\n              debugEnabled = !debugEnabled;\n              debugUI.style.display = debugEnabled ? 'block' : 'none';\n              e.preventDefault();\n              return;\n            }\n\n            // G for gender toggle\n            if (\n              e.key.toLowerCase() === 'g' &&\n              !isPaused &&\n              !isInventoryOpen &&\n              !isCraftingTableOpen &&\n              !isChestOpen &&\n              !isFurnaceOpen\n            ) {\n              togglePlayerGender();\n              return;\n            }\n\n            keys[e.key.toLowerCase()] = true;\n\n            // Number keys for hotbar\n            const num = parseInt(e.key);\n            if (num >= 1 && num <= 9) {\n              selectSlot(num - 1);\n            }\n\n            // Space for jump/swim\n            if (e.key === ' ') {\n              if (isInWater) {\n                // Swim up when in water\n                playerVelocity.y = WATER_SWIM_UP_SPEED;\n              } else if (isGrounded) {\n                playerVelocity.y = JUMP_VELOCITY;\n                isGrounded = false;\n              }\n            }\n          });\n\n          document.addEventListener('keyup', (e) => {\n            keys[e.key.toLowerCase()] = false;\n          });\n\n          // Mouse\n          canvas.addEventListener('click', () => {\n            if (mode === 'edit') return;\n            if (isPaused) return;\n            if (!isPointerLocked && !isTouchDevice) {\n              try {\n                const requestPointerLock =\n                  canvas.requestPointerLock || canvas.mozRequestPointerLock;\n                if (requestPointerLock) {\n                  requestPointerLock.call(canvas);\n                }\n              } catch (e) {\n                lib.log('Pointer lock request failed: ' + e.message);\n              }\n            }\n          });\n\n          document.addEventListener(\n            'pointerlockchange',\n            () => {\n              try {\n                isPointerLocked = !!document.pointerLockElement;\n                const instructions = document.getElementById('instructions');\n                if (instructions) {\n                  instructions.classList.toggle('hidden', isPointerLocked);\n                }\n              } catch (e) {\n                lib.log('Pointer lock change error: ' + e.message);\n                isPointerLocked = false;\n              }\n            },\n            false\n          );\n\n          document.addEventListener(\n            'pointerlockerror',\n            () => {\n              lib.log('Pointer lock error - continuing without pointer lock');\n              isPointerLocked = false;\n            },\n            false\n          );\n\n          document.addEventListener('mousemove', (e) => {\n            if (mode === 'edit') return;\n            if (isPaused) return;\n            try {\n              if (isPointerLocked) {\n                const currentLocked = !!document.pointerLockElement;\n                if (currentLocked) {\n                  mouseMovement.x += e.movementX || 0;\n                  mouseMovement.y += e.movementY || 0;\n                } else {\n                  isPointerLocked = false;\n                }\n              }\n            } catch (e) {\n              lib.log('Mouse move error: ' + e.message);\n              isPointerLocked = false;\n            }\n          });\n\n          canvas.addEventListener('mousedown', (e) => {\n            if (mode === 'edit') return;\n            if (isPaused) return;\n            if (!isPointerLocked) return;\n\n            if (e.button === 0) {\n              // Arrow weapon fires with primary action\n              if (isArrowWeaponSelected()) {\n                fireArrowWeapon();\n              } else {\n                // Try to attack mob first\n                if (!attackMob()) {\n                  isMining = true;\n                }\n              }\n            } else if (e.button === 2) {\n              placeBlock();\n            }\n          });\n\n          canvas.addEventListener('mouseup', (e) => {\n            if (e.button === 0) {\n              isMining = false;\n              miningProgress = 0;\n            }\n          });\n\n          canvas.addEventListener('contextmenu', (e) => e.preventDefault());\n\n          // Mouse wheel for hotbar\n          canvas.addEventListener('wheel', (e) => {\n            if (mode === 'edit') return;\n            if (isPaused) return;\n            e.preventDefault();\n            if (e.deltaY > 0) {\n              selectSlot((selectedSlot + 1) % 9);\n            } else {\n              selectSlot((selectedSlot + 8) % 9);\n            }\n          });\n\n          // Touch controls\n          if (isTouchDevice) {\n            setupTouchControls();\n          }\n\n          // Window blur\n          window.addEventListener('blur', () => {\n            Object.keys(keys).forEach((k) => (keys[k] = false));\n            isMining = false;\n            miningProgress = 0;\n            leftJoystick.active = false;\n            rightJoystick.active = false;\n          });\n        }\n\n        function setupTouchControls() {\n          const rightArea = document.getElementById('touch-right');\n\n          // Right side: Camera look control only (movement is handled by move widget)\n          function handleRightTouchStart(e) {\n            // Don't interfere with buttons\n            if (e.target.tagName === 'BUTTON') return;\n\n            e.preventDefault();\n            const touch = e.changedTouches[0];\n\n            if (rightJoystick.active) return;\n\n            rightJoystick.active = true;\n            rightJoystick.touchId = touch.identifier;\n            rightJoystick.baseX = touch.clientX;\n            rightJoystick.baseY = touch.clientY;\n            rightJoystick.lastX = touch.clientX;\n            rightJoystick.lastY = touch.clientY;\n            rightJoystick.dx = 0;\n            rightJoystick.dy = 0;\n\n            updateJoystickVisual(false);\n          }\n\n          function handleTouchMove(e) {\n            for (const touch of e.changedTouches) {\n              // Handle right joystick (camera look)\n              if (rightJoystick.touchId === touch.identifier) {\n                e.preventDefault();\n\n                // Direct drag for smooth camera rotation\n                const dragX = touch.clientX - rightJoystick.lastX;\n                const dragY = touch.clientY - rightJoystick.lastY;\n\n                const sensitivity = 0.008;\n                rightJoystick.dx += dragX * sensitivity;\n                rightJoystick.dy += dragY * sensitivity;\n\n                rightJoystick.lastX = touch.clientX;\n                rightJoystick.lastY = touch.clientY;\n\n                // Update visual feedback\n                const dx = touch.clientX - rightJoystick.baseX;\n                const dy = touch.clientY - rightJoystick.baseY;\n                const dist = Math.sqrt(dx * dx + dy * dy);\n                if (dist > JOYSTICK_MAX_RADIUS) {\n                  const angle = Math.atan2(dy, dx);\n                  rightJoystick.baseX = touch.clientX - Math.cos(angle) * JOYSTICK_MAX_RADIUS;\n                  rightJoystick.baseY = touch.clientY - Math.sin(angle) * JOYSTICK_MAX_RADIUS;\n                }\n                rightJoystick.knobX = touch.clientX;\n                rightJoystick.knobY = touch.clientY;\n\n                updateJoystickVisual(false);\n              }\n            }\n          }\n\n          function handleTouchEnd(e) {\n            for (const touch of e.changedTouches) {\n              if (rightJoystick.touchId === touch.identifier) {\n                rightJoystick.active = false;\n                rightJoystick.touchId = null;\n                rightJoystick.dx = 0;\n                rightJoystick.dy = 0;\n                updateJoystickVisual(false);\n              }\n            }\n          }\n\n          // Only set up right area for camera look\n          if (rightArea) {\n            rightArea.addEventListener('touchstart', handleRightTouchStart, {\n              passive: false,\n            });\n          }\n\n          document.addEventListener('touchmove', handleTouchMove, {\n            passive: false,\n          });\n          document.addEventListener('touchend', handleTouchEnd);\n          document.addEventListener('touchcancel', handleTouchEnd);\n        }\n\n        function updateJoystickVisual(isLeft) {\n          // Only handle right joystick visual (left uses move widget)\n          if (isLeft) return;\n\n          const joystick = rightJoystick;\n          const el = document.getElementById('right-joystick');\n\n          if (!el) return;\n\n          if (joystick.active) {\n            el.classList.add('active');\n            el.style.opacity = '1';\n            el.style.left = joystick.baseX + 'px';\n            el.style.top = joystick.baseY + 'px';\n\n            const knob = el.querySelector('.joystick-knob');\n            if (knob) {\n              // Aim joystick - knob follows touch with clamping\n              const dx = joystick.knobX - joystick.baseX;\n              const dy = joystick.knobY - joystick.baseY;\n              const dist = Math.sqrt(dx * dx + dy * dy);\n              if (dist > JOYSTICK_MAX_RADIUS) {\n                const angle = Math.atan2(dy, dx);\n                knob.style.left = Math.cos(angle) * JOYSTICK_MAX_RADIUS + 'px';\n                knob.style.top = Math.sin(angle) * JOYSTICK_MAX_RADIUS + 'px';\n              } else {\n                knob.style.left = dx + 'px';\n                knob.style.top = dy + 'px';\n              }\n            }\n          } else {\n            el.classList.remove('active');\n            el.style.opacity = '0.3';\n          }\n        }\n\n        // Move Widget (persistent joystick) handling\n        let moveWidgetState = {\n          active: false,\n          touchId: null,\n          dx: 0,\n          dy: 0,\n        };\n        const MOVE_WIDGET_RADIUS = 65;\n        // Max distance knob can move from center\n        const MOVE_WIDGET_DEAD_ZONE = 12;\n\n        function setupMoveWidget() {\n          const widget = document.getElementById('move-widget');\n          const knob = document.getElementById('move-widget-knob');\n\n          if (!widget || !knob) return;\n\n          function handleWidgetTouchStart(e) {\n            e.preventDefault();\n            e.stopPropagation();\n\n            if (moveWidgetState.active) return;\n\n            const touch = e.changedTouches[0];\n            const rect = widget.getBoundingClientRect();\n            const centerX = rect.left + rect.width / 2;\n            const centerY = rect.top + rect.height / 2;\n\n            moveWidgetState.active = true;\n            moveWidgetState.touchId = touch.identifier;\n            moveWidgetState.centerX = centerX;\n            moveWidgetState.centerY = centerY;\n\n            // Update knob position based on touch\n            updateMoveWidgetKnob(touch.clientX, touch.clientY);\n          }\n\n          function handleWidgetTouchMove(e) {\n            if (!moveWidgetState.active) return;\n\n            for (const touch of e.changedTouches) {\n              if (touch.identifier === moveWidgetState.touchId) {\n                e.preventDefault();\n                updateMoveWidgetKnob(touch.clientX, touch.clientY);\n                break;\n              }\n            }\n          }\n\n          function handleWidgetTouchEnd(e) {\n            for (const touch of e.changedTouches) {\n              if (touch.identifier === moveWidgetState.touchId) {\n                moveWidgetState.active = false;\n                moveWidgetState.touchId = null;\n                moveWidgetState.dx = 0;\n                moveWidgetState.dy = 0;\n\n                // Reset knob to center\n                knob.style.left = '50%';\n                knob.style.top = '50%';\n                knob.style.transform = 'translate(-50%, -50%)';\n                break;\n              }\n            }\n          }\n\n          function updateMoveWidgetKnob(touchX, touchY) {\n            let dx = touchX - moveWidgetState.centerX;\n            let dy = touchY - moveWidgetState.centerY;\n            const dist = Math.sqrt(dx * dx + dy * dy);\n\n            // Clamp to max radius\n            if (dist > MOVE_WIDGET_RADIUS) {\n              const angle = Math.atan2(dy, dx);\n              dx = Math.cos(angle) * MOVE_WIDGET_RADIUS;\n              dy = Math.sin(angle) * MOVE_WIDGET_RADIUS;\n            }\n\n            // Update knob visual position\n            const knobX = 50 + (dx / MOVE_WIDGET_RADIUS) * 35;\n            // 35% offset max\n            const knobY = 50 + (dy / MOVE_WIDGET_RADIUS) * 35;\n            knob.style.left = knobX + '%';\n            knob.style.top = knobY + '%';\n\n            // Calculate normalized movement values with dead zone\n            const normalizedDist =\n              Math.max(0, dist - MOVE_WIDGET_DEAD_ZONE) /\n              (MOVE_WIDGET_RADIUS - MOVE_WIDGET_DEAD_ZONE);\n            if (dist > MOVE_WIDGET_DEAD_ZONE) {\n              moveWidgetState.dx = (dx / dist) * normalizedDist;\n              moveWidgetState.dy = (dy / dist) * normalizedDist;\n            } else {\n              moveWidgetState.dx = 0;\n              moveWidgetState.dy = 0;\n            }\n          }\n\n          widget.addEventListener('touchstart', handleWidgetTouchStart, {\n            passive: false,\n          });\n          document.addEventListener('touchmove', handleWidgetTouchMove, {\n            passive: false,\n          });\n          document.addEventListener('touchend', handleWidgetTouchEnd);\n          document.addEventListener('touchcancel', handleWidgetTouchEnd);\n        }\n\n        // Block interaction - DDA raycast for instanced mesh compatibility\n        function getTargetBlock() {\n          // In 3rd person, raycast from player's eye position (not camera)\n          const rayDir = new THREE.Vector3();\n          camera.getWorldDirection(rayDir);\n\n          const camPos = playerPos.clone();\n          const maxDistance = 6;\n\n          // DDA raycast\n          let cx = Math.floor(camPos.x);\n          let cy = Math.floor(camPos.y);\n          let cz = Math.floor(camPos.z);\n\n          const stepDirX = rayDir.x >= 0 ? 1 : -1;\n          const stepDirY = rayDir.y >= 0 ? 1 : -1;\n          const stepDirZ = rayDir.z >= 0 ? 1 : -1;\n\n          const nextBoundaryX = rayDir.x >= 0 ? cx + 1 - camPos.x : camPos.x - cx;\n          const nextBoundaryY = rayDir.y >= 0 ? cy + 1 - camPos.y : camPos.y - cy;\n          const nextBoundaryZ = rayDir.z >= 0 ? cz + 1 - camPos.z : camPos.z - cz;\n\n          let tMaxX = Math.abs(rayDir.x) > 0.0001 ? nextBoundaryX / Math.abs(rayDir.x) : Infinity;\n          let tMaxY = Math.abs(rayDir.y) > 0.0001 ? nextBoundaryY / Math.abs(rayDir.y) : Infinity;\n          let tMaxZ = Math.abs(rayDir.z) > 0.0001 ? nextBoundaryZ / Math.abs(rayDir.z) : Infinity;\n\n          const tDeltaX = Math.abs(rayDir.x) > 0.0001 ? 1 / Math.abs(rayDir.x) : Infinity;\n          const tDeltaY = Math.abs(rayDir.y) > 0.0001 ? 1 / Math.abs(rayDir.y) : Infinity;\n          const tDeltaZ = Math.abs(rayDir.z) > 0.0001 ? 1 / Math.abs(rayDir.z) : Infinity;\n\n          let prevX = cx,\n            prevY = cy,\n            prevZ = cz;\n          let lastFaceNormal = new THREE.Vector3(0, 1, 0);\n          let steps = 0;\n          const maxSteps = Math.ceil(maxDistance * 2);\n\n          while (steps < maxSteps) {\n            // Distance check\n            const dx = cx + 0.5 - camPos.x;\n            const dy = cy + 0.5 - camPos.y;\n            const dz = cz + 0.5 - camPos.z;\n            const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);\n            if (dist > maxDistance) break;\n\n            // Only check Y bounds for infinite world\n            if (cy >= 0 && cy < worldHeight) {\n              const blockType = getBlockAt(cx, cy, cz);\n              if (blockType !== BLOCK_TYPES.AIR) {\n                // Calculate hit point (approximate center of face)\n                const hitPoint = new THREE.Vector3(cx + 0.5, cy + 0.5, cz + 0.5);\n\n                return {\n                  point: hitPoint,\n                  face: { normal: lastFaceNormal.clone() },\n                  x: cx,\n                  y: cy,\n                  z: cz,\n                  blockType: blockType,\n                };\n              }\n            }\n\n            // Store previous position for face normal calculation\n            prevX = cx;\n            prevY = cy;\n            prevZ = cz;\n\n            // DDA step\n            if (tMaxX < tMaxY) {\n              if (tMaxX < tMaxZ) {\n                cx += stepDirX;\n                tMaxX += tDeltaX;\n                lastFaceNormal.set(-stepDirX, 0, 0);\n              } else {\n                cz += stepDirZ;\n                tMaxZ += tDeltaZ;\n                lastFaceNormal.set(0, 0, -stepDirZ);\n              }\n            } else {\n              if (tMaxY < tMaxZ) {\n                cy += stepDirY;\n                tMaxY += tDeltaY;\n                lastFaceNormal.set(0, -stepDirY, 0);\n              } else {\n                cz += stepDirZ;\n                tMaxZ += tDeltaZ;\n                lastFaceNormal.set(0, 0, -stepDirZ);\n              }\n            }\n            steps++;\n          }\n\n          return null;\n        }\n\n        function triggerDynamiteExplosion(x, y, z) {\n          // Remove the dynamite block\n          setBlockAt(x, y, z, BLOCK_TYPES.AIR);\n          updateBlockAt(x, y, z);\n\n          // Track modification\n          const blockKey = `${x},${y},${z}`;\n          modifiedBlocks.set(blockKey, BLOCK_TYPES.AIR);\n\n          // Play explosion sound\n          playSound('explosion_sound', false, 0.7);\n\n          // Damage player if nearby\n          const explosionPos = new THREE.Vector3(x + 0.5, y + 0.5, z + 0.5);\n          const distToPlayer = explosionPos.distanceTo(playerPos);\n          if (distToPlayer < 8) {\n            const damage = Math.ceil((1 - distToPlayer / 8) * 12);\n            damagePlayer(damage, performance.now() / 1000);\n          }\n\n          // Destroy blocks in radius (infinite world - only check Y bounds)\n          const explosionRadius = 4;\n          for (let ex = x - explosionRadius; ex <= x + explosionRadius; ex++) {\n            for (let ey = y - explosionRadius; ey <= y + explosionRadius; ey++) {\n              for (let ez = z - explosionRadius; ez <= z + explosionRadius; ez++) {\n                if (ey >= 0 && ey < worldHeight) {\n                  const dist = Math.sqrt((ex - x) ** 2 + (ey - y) ** 2 + (ez - z) ** 2);\n                  const blockType = getBlockAt(ex, ey, ez);\n                  if (\n                    dist <= explosionRadius &&\n                    blockType !== BLOCK_TYPES.AIR &&\n                    blockType !== BLOCK_TYPES.BEDROCK\n                  ) {\n                    // Chain reaction: trigger other dynamite blocks immediately\n                    if (blockType === BLOCK_TYPES.DYNAMITE) {\n                      const key = `${ex},${ey},${ez}`;\n                      // Remove from countdown if it was there\n                      dynamiteCountdowns.delete(key);\n                      // Trigger immediate explosion\n                      triggerDynamiteExplosion(ex, ey, ez);\n                    } else {\n                      setBlockAt(ex, ey, ez, BLOCK_TYPES.AIR);\n                      updateBlockAt(ex, ey, ez);\n\n                      // Track modification\n                      const modKey = `${ex},${ey},${ez}`;\n                      modifiedBlocks.set(modKey, BLOCK_TYPES.AIR);\n\n                      if (blockType === BLOCK_TYPES.DOOR) {\n                        if (getBlockAt(ex, ey + 1, ez) === BLOCK_TYPES.DOOR) {\n                          setBlockAt(ex, ey + 1, ez, BLOCK_TYPES.AIR);\n                          updateBlockAt(ex, ey + 1, ez);\n                          modifiedBlocks.set(`${ex},${ey + 1},${ez}`, BLOCK_TYPES.AIR);\n                          if (isOnline) syncBlockAction('mine', ex, ey + 1, ez, BLOCK_TYPES.DOOR);\n                        }\n                        if (getBlockAt(ex, ey - 1, ez) === BLOCK_TYPES.DOOR) {\n                          setBlockAt(ex, ey - 1, ez, BLOCK_TYPES.AIR);\n                          updateBlockAt(ex, ey - 1, ez);\n                          modifiedBlocks.set(`${ex},${ey - 1},${ez}`, BLOCK_TYPES.AIR);\n                          if (isOnline) syncBlockAction('mine', ex, ey - 1, ez, BLOCK_TYPES.DOOR);\n                        }\n                      }\n\n                      if (Math.random() < 0.4) {\n                        createBreakParticles(ex + 0.5, ey + 0.5, ez + 0.5, blockType);\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n\n          // Explosion particles\n          for (let i = 0; i < 40; i++) {\n            const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);\n            const material = new THREE.MeshBasicMaterial({\n              color: new THREE.Color(1, Math.random() * 0.6, 0),\n            });\n            const particle = new THREE.Mesh(geometry, material);\n            particle.position.copy(explosionPos);\n            particle.userData.velocity = new THREE.Vector3(\n              (Math.random() - 0.5) * 12,\n              Math.random() * 12,\n              (Math.random() - 0.5) * 12\n            );\n            particle.userData.life = 1.5;\n            scene.add(particle);\n            particles.push(particle);\n          }\n\n          // Damage mobs nearby\n          for (let i = mobs.length - 1; i >= 0; i--) {\n            const mob = mobs[i];\n            const distToMob = explosionPos.distanceTo(mob.position);\n            if (distToMob < 8) {\n              const damage = Math.ceil((1 - distToMob / 8) * 20);\n              damageMob(mob, damage);\n              if (typeof syncMobHit === 'function') {\n                syncMobHit(mob.id, damage);\n              }\n            }\n          }\n        }\n\n        function mineBlock(dt) {\n          if (!targetBlock || targetBlock.blockType === BLOCK_TYPES.AIR) return;\n          // Bedrock is unmineable\n          if (targetBlock.blockType === BLOCK_TYPES.BEDROCK) {\n            return;\n          }\n          // Water is not mineable - use bucket instead\n          if (\n            targetBlock.blockType === BLOCK_TYPES.WATER ||\n            targetBlock.blockType === BLOCK_TYPES.WATER_FLOW\n          ) {\n            return;\n          }\n          // Lava is not mineable - use bucket instead\n          if (\n            targetBlock.blockType === BLOCK_TYPES.LAVA ||\n            targetBlock.blockType === BLOCK_TYPES.LAVA_FLOW\n          ) {\n            return;\n          }\n\n          const hardness = BLOCK_HARDNESS[targetBlock.blockType] || 1;\n          let miningSpeed = config.miningSpeedMultiplier / hardness;\n\n          // Check if holding a tool and apply bonus\n          const heldSlot = inventory[selectedSlot];\n          if (heldSlot && isTool(heldSlot.type)) {\n            const toolEffectiveness = TOOL_EFFECTIVENESS[heldSlot.type];\n            if (\n              toolEffectiveness &&\n              toolEffectiveness.blocks &&\n              toolEffectiveness.blocks.includes(targetBlock.blockType)\n            ) {\n              miningSpeed *= toolEffectiveness.speedMultiplier;\n            }\n          }\n\n          miningProgress += dt * miningSpeed;\n\n          // Update progress bar\n          const progressEl = document.querySelector('.mining-progress');\n          const progressBar = document.querySelector('.mining-progress-bar');\n          if (progressEl && progressBar) {\n            progressEl.classList.add('active');\n            progressBar.style.width = miningProgress * 100 + '%';\n          }\n\n          if (miningProgress >= 1) {\n            // Break block\n            const { x, y, z, blockType } = targetBlock;\n\n            // Special handling for dynamite - start countdown instead of immediate explosion\n            if (blockType === BLOCK_TYPES.DYNAMITE) {\n              const key = `${x},${y},${z}`;\n              dynamiteCountdowns.set(key, {\n                timeLeft: 3.0,\n                totalTime: 3.0,\n                x,\n                y,\n                z,\n              });\n              playSound('creeper_hiss_sound', false, 0.6);\n\n              // Sync TNT activation\n              if (isOnline) {\n                syncBlockAction('tnt', x, y, z, blockType);\n              }\n            } else {\n              // If mining a water source, clear surrounding flow blocks\n              if (blockType === BLOCK_TYPES.WATER) {\n                clearFlowWaterFromSource(x, y, z);\n              }\n              // If mining a lava source, clear surrounding flow blocks\n              if (blockType === BLOCK_TYPES.LAVA) {\n                clearFlowLavaFromSource(x, y, z);\n              }\n\n              setBlockAt(x, y, z, BLOCK_TYPES.AIR);\n              updateBlockAt(x, y, z);\n\n              // Track modification\n              const blockKey = `${x},${y},${z}`;\n              modifiedBlocks.set(blockKey, BLOCK_TYPES.AIR);\n\n              if (blockType === BLOCK_TYPES.DOOR) {\n                if (getBlockAt(x, y + 1, z) === BLOCK_TYPES.DOOR) {\n                  setBlockAt(x, y + 1, z, BLOCK_TYPES.AIR);\n                  updateBlockAt(x, y + 1, z);\n                  modifiedBlocks.set(`${x},${y + 1},${z}`, BLOCK_TYPES.AIR);\n                  if (isOnline) syncBlockAction('mine', x, y + 1, z, BLOCK_TYPES.DOOR);\n                }\n                if (getBlockAt(x, y - 1, z) === BLOCK_TYPES.DOOR) {\n                  setBlockAt(x, y - 1, z, BLOCK_TYPES.AIR);\n                  updateBlockAt(x, y - 1, z);\n                  modifiedBlocks.set(`${x},${y - 1},${z}`, BLOCK_TYPES.AIR);\n                  if (isOnline) syncBlockAction('mine', x, y - 1, z, BLOCK_TYPES.DOOR);\n                }\n              }\n              // Special handling for bed - remove both head and foot blocks\n              if (blockType === BLOCK_TYPES.BED) {\n                // Check for the other half of the bed (z+1 or z-1)\n                if (getBlockAt(x, y, z + 1) === BLOCK_TYPES.BED) {\n                  setBlockAt(x, y, z + 1, BLOCK_TYPES.AIR);\n                  updateBlockAt(x, y, z + 1);\n                  modifiedBlocks.set(`${x},${y},${z + 1}`, BLOCK_TYPES.AIR);\n                  if (isOnline) syncBlockAction('mine', x, y, z + 1, BLOCK_TYPES.BED);\n                }\n                if (getBlockAt(x, y, z - 1) === BLOCK_TYPES.BED) {\n                  setBlockAt(x, y, z - 1, BLOCK_TYPES.AIR);\n                  updateBlockAt(x, y, z - 1);\n                  modifiedBlocks.set(`${x},${y},${z - 1}`, BLOCK_TYPES.AIR);\n                  if (isOnline) syncBlockAction('mine', x, y, z - 1, BLOCK_TYPES.BED);\n                }\n              }\n\n              // After removing a flow block, refresh nearby water to check if it should disappear\n              if (blockType === BLOCK_TYPES.WATER_FLOW) {\n                refreshNearbyFlowingWater(x, y, z);\n              }\n              // Special handling for chest - drop contents to inventory\n              if (blockType === BLOCK_TYPES.CHEST) {\n                const chestKey = `${x},${y},${z}`;\n                const chestInv = chestContents.get(chestKey);\n                if (chestInv && !isCreativeMode) {\n                  // Add all chest contents to player inventory\n                  for (const item of chestInv) {\n                    if (item.count > 0 && item.type !== BLOCK_TYPES.AIR) {\n                      for (let i = 0; i < item.count; i++) {\n                        addToInventory(item.type);\n                      }\n                    }\n                  }\n                }\n                chestContents.delete(chestKey);\n              }\n\n              // Add to inventory\n              // Coal ore drops coal instead of the block itself\n              if (!isCreativeMode) {\n                if (blockType === BLOCK_TYPES.COAL_ORE) {\n                  addToInventory(ITEM_TYPES.COAL);\n                  // Chance for bonus coal (25%)\n                  if (Math.random() < 0.25) {\n                    addToInventory(ITEM_TYPES.COAL);\n                  }\n                } else if (blockType === BLOCK_TYPES.DIAMOND_ORE) {\n                  // Diamond ore drops diamonds directly (no smelting needed)\n                  addToInventory(ITEM_TYPES.DIAMOND);\n                  // Chance for bonus diamond (10%)\n                  if (Math.random() < 0.1) {\n                    addToInventory(ITEM_TYPES.DIAMOND);\n                  }\n                } else if (blockType === BLOCK_TYPES.LEAVES) {\n                  // Leaves have a chance to drop apples (5%)\n                  if (Math.random() < 0.05) {\n                    addToInventory(ITEM_TYPES.APPLE);\n                    showMessage('+1 Apple', '#8BC34A', 1500);\n                  }\n                  // Still drop the leaves block\n                  addToInventory(blockType);\n                } else if (\n                  blockType === BLOCK_TYPES.WATER || blockType === BLOCK_TYPES.WATER_FLOW\n                ) {\n                  // Do not add water blocks to inventory when mined\n                } else {\n                  addToInventory(blockType);\n                }\n              }\n              // Create particles\n              createBreakParticles(x + 0.5, y + 0.5, z + 0.5, blockType);\n            }\n\n            // Sync block break to other players (skip for TNT - it sends 'tnt' action instead)\n            if (isOnline && blockType !== BLOCK_TYPES.DYNAMITE) {\n              syncBlockAction('mine', x, y, z, blockType);\n              syncPlayerSwing(); // Sync swing animation\n            }\n\n            // Play sound\n            playSound('block_break_sound', false, 0.5);\n\n            // Decrease hunger when mining\n            if (playerHunger > 0) {\n              playerHunger = Math.max(0, playerHunger - HUNGER_MINING_COST);\n              updateHungerBar();\n            }\n\n            miningProgress = 0;\n            isMining = false;\n          }\n        }\n\n        function findSafeBedSpawn(x, y, z) {\n          const candidateOffsets = [\n            [0, 1, 0],\n            [1, 1, 0],\n            [-1, 1, 0],\n            [0, 1, 1],\n            [0, 1, -1],\n            [1, 1, 1],\n            [-1, 1, 1],\n            [1, 1, -1],\n            [-1, 1, -1],\n          ];\n\n          for (const [ox, oy, oz] of candidateOffsets) {\n            const sx = x + ox;\n            const sy = y + oy;\n            const sz = z + oz;\n\n            if (\n              sy + 1 < worldHeight &&\n              getBlockAt(sx, sy, sz) === BLOCK_TYPES.AIR &&\n              getBlockAt(sx, sy + 1, sz) === BLOCK_TYPES.AIR\n            ) {\n              return new THREE.Vector3(sx + 0.5, sy + PLAYER_HEIGHT, sz + 0.5);\n            }\n          }\n\n          return new THREE.Vector3(x + 0.5, y + PLAYER_HEIGHT + 1.1, z + 0.5);\n        }\n\n        // Sleeping animation state\n        let isSleeping = false;\n        let sleepAnimationProgress = 0;\n        let sleepBedPosition = null;\n        let preSleepCameraPos = null;\n        let preSleepCameraYaw = 0;\n        let preSleepCameraPitch = 0;\n\n        // Check if there are hostile enemies nearby that would prevent sleeping\n        function hasNearbyEnemies(x, y, z, radius = 8) {\n          const bedPos = new THREE.Vector3(x + 0.5, y + 0.5, z + 0.5);\n\n          for (const mob of mobs) {\n            // Only check hostile mobs (zombies, creepers)\n            if (mob.type === MOB_TYPES.ZOMBIE || mob.type === MOB_TYPES.CREEPER) {\n              const distance = mob.position.distanceTo(bedPos);\n              if (distance <= radius) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }\n\n        function useBed(x, y, z) {\n          // Find the head of the bed for consistent behavior\n          const headZ = getBedHeadZ(x, y, z);\n\n          // Check for nearby enemies first\n          if (hasNearbyEnemies(x, y, headZ)) {\n            showMessage('\ud83d\udc80 You cannot sleep with enemies nearby!', '#F44336', 2500);\n            playSound('player_hurt_sound', false, 0.3);\n            return;\n          }\n\n          const spawn = findSafeBedSpawn(x, y, headZ);\n          spawnPoint = spawn.clone();\n\n          // Start sleeping animation\n          startSleepAnimation(x, y, headZ);\n\n          playSound('block_place_sound', false, 0.4);\n        }\n\n        function startSleepAnimation(x, y, z) {\n          if (isSleeping) return;\n\n          isSleeping = true;\n          sleepAnimationProgress = 0;\n          sleepBedPosition = new THREE.Vector3(x + 0.5, y + 0.4, z + 1.0); // Center of bed\n\n          // Store current camera state\n          preSleepCameraPos = playerPos.clone();\n          preSleepCameraYaw = playerYaw;\n          preSleepCameraPitch = playerPitch;\n\n          // Create sleep overlay\n          const sleepOverlay = document.createElement('div');\n          sleepOverlay.id = 'sleep-overlay';\n          sleepOverlay.style.cssText = `\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0);\n            pointer-events: none;\n            z-index: 100;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            transition: background 0.5s ease;\n          `;\n          sleepOverlay.innerHTML = `\n            <div id=\"sleep-text\" style=\"\n              font-family: 'VT323', monospace;\n              font-size: 48px;\n              color: white;\n              opacity: 0;\n              text-shadow: 2px 2px 4px rgba(0,0,0,0.8);\n              transition: opacity 0.5s ease;\n            \">\ud83d\udca4 Sleeping...</div>\n            <div id=\"sleep-zzz\" style=\"\n              font-size: 64px;\n              opacity: 0;\n              margin-top: 20px;\n              transition: opacity 0.3s ease;\n            \"></div>\n          `;\n          gameWorld.appendChild(sleepOverlay);\n\n          // Animate lying down sequence\n          animateSleeping();\n        }\n\n        function animateSleeping() {\n          const sleepOverlay = document.getElementById('sleep-overlay');\n          const sleepText = document.getElementById('sleep-text');\n          const sleepZzz = document.getElementById('sleep-zzz');\n\n          if (!sleepOverlay || !isSleeping) return;\n\n          sleepAnimationProgress += 0.016; // ~60fps\n\n          // Phase 1: Fade to dark and lower camera (0-1s)\n          if (sleepAnimationProgress < 1.0) {\n            const progress = sleepAnimationProgress / 1.0;\n            sleepOverlay.style.background = `rgba(0, 0, 20, ${progress * 0.85})`;\n\n            // Move camera down to bed level and look up\n            const targetY = sleepBedPosition.y + 0.3;\n            const targetPitch = -Math.PI / 2 + 0.3; // Looking mostly up\n\n            playerPos.x += (sleepBedPosition.x - playerPos.x) * 0.08;\n            playerPos.y += (targetY - playerPos.y) * 0.08;\n            playerPos.z += (sleepBedPosition.z - playerPos.z) * 0.08;\n            playerPitch += (targetPitch - playerPitch) * 0.08;\n\n            camera.position.copy(playerPos);\n            camera.rotation.x = playerPitch;\n          }\n          // Phase 2: Show sleeping text (1-1.5s)\n          else if (sleepAnimationProgress < 1.5) {\n            sleepText.style.opacity = '1';\n          }\n          // Phase 3: Animate Z's (1.5-4s)\n          else if (sleepAnimationProgress < 4.0) {\n            const zzzPhase = Math.floor((sleepAnimationProgress - 1.5) * 2) % 4;\n            const zzzTexts = ['\ud83d\udca4', '\ud83d\udca4\ud83d\udca4', '\ud83d\udca4\ud83d\udca4\ud83d\udca4', '\ud83d\udca4\ud83d\udca4'];\n            sleepZzz.textContent = zzzTexts[zzzPhase];\n            sleepZzz.style.opacity = '1';\n\n            // Gentle camera bob while sleeping\n            const bobAmount = Math.sin(sleepAnimationProgress * 2) * 0.02;\n            camera.position.y = playerPos.y + bobAmount;\n          }\n          // Phase 4: Wake up - fade out and restore (4-5s)\n          else if (sleepAnimationProgress < 5.0) {\n            const wakeProgress = (sleepAnimationProgress - 4.0) / 1.0;\n            sleepOverlay.style.background = `rgba(0, 0, 20, ${0.85 * (1 - wakeProgress)})`;\n            sleepText.style.opacity = `${1 - wakeProgress}`;\n            sleepZzz.style.opacity = `${1 - wakeProgress}`;\n\n            // Return camera to standing position\n            playerPos.x += (preSleepCameraPos.x - playerPos.x) * 0.1;\n            playerPos.y += (preSleepCameraPos.y - playerPos.y) * 0.1;\n            playerPos.z += (preSleepCameraPos.z - playerPos.z) * 0.1;\n            playerPitch += (preSleepCameraPitch - playerPitch) * 0.1;\n            playerYaw += (preSleepCameraYaw - playerYaw) * 0.1;\n\n            camera.position.copy(playerPos);\n            camera.rotation.x = playerPitch;\n            camera.rotation.y = playerYaw;\n          }\n          // Phase 5: End animation\n          else {\n            endSleepAnimation();\n            return;\n          }\n\n          requestAnimationFrame(animateSleeping);\n        }\n\n        function endSleepAnimation() {\n          isSleeping = false;\n          sleepAnimationProgress = 0;\n\n          // Restore player position\n          if (preSleepCameraPos) {\n            playerPos.copy(preSleepCameraPos);\n            playerYaw = preSleepCameraYaw;\n            playerPitch = preSleepCameraPitch;\n            camera.position.copy(playerPos);\n            camera.rotation.y = playerYaw;\n            camera.rotation.x = playerPitch;\n          }\n\n          // Restore health after sleep\n          playerHealth = maxPlayerHealth;\n          updateHealthBar();\n\n          // Remove overlay\n          const sleepOverlay = document.getElementById('sleep-overlay');\n          if (sleepOverlay) {\n            sleepOverlay.remove();\n          }\n\n          showMessage('Spawn point set! \u2600\ufe0f', '#8BC34A', 2000);\n        }\n\n        function placeBlock() {\n          // Check if holding food - eat it instead of placing (or tame dog)\n          const slot = inventory[selectedSlot];\n          if (slot && slot.count > 0 && FOOD_VALUES[slot.type]) {\n            const targetMob = getTargetMob(4);\n            if (targetMob && targetMob.type === MOB_TYPES.DOG) {\n              // Tame the dog with meat\n              if (isMeatItem(slot.type)) {\n                targetMob.isTamed = true;\n                targetMob.followOwnerId = myPlayerId || 'local';\n                slot.count--;\n                if (slot.count <= 0) {\n                  inventory[selectedSlot] = { type: BLOCK_TYPES.AIR, count: 0 };\n                }\n                updateHotbarUI();\n                playSound('block_place_sound', false, 0.4);\n                triggerArmSwing(true);\n                showMessage('\ud83d\udc36 Tamed! The dog will follow you.', '#8BC34A', 2000);\n                return;\n              }\n              showMessage('Dogs only follow if you give them meat!', '#FF9800', 1600);\n              return;\n            }\n            eatFood();\n            return;\n          }\n\n          // Check if holding empty bucket - try to pick up water or lava (3x3 area)\n          if (slot && slot.count > 0 && slot.type === ITEM_TYPES.BUCKET_EMPTY) {\n            if (\n              targetBlock &&\n              (targetBlock.blockType === BLOCK_TYPES.WATER ||\n                targetBlock.blockType === BLOCK_TYPES.WATER_FLOW)\n            ) {\n              const { x, y, z, blockType } = targetBlock;\n\n              // Remove 3x3 area of water blocks centered on target\n              let blocksRemoved = 0;\n              for (let dx = -1; dx <= 1; dx++) {\n                for (let dz = -1; dz <= 1; dz++) {\n                  const nx = x + dx;\n                  const nz = z + dz;\n                  const nBlockType = getBlockAt(nx, y, nz);\n\n                  if (nBlockType === BLOCK_TYPES.WATER || nBlockType === BLOCK_TYPES.WATER_FLOW) {\n                    // If it's a source block, clear its flow blocks first\n                    if (nBlockType === BLOCK_TYPES.WATER) {\n                      clearFlowWaterFromSource(nx, y, nz);\n                    }\n\n                    setBlockAt(nx, y, nz, BLOCK_TYPES.AIR);\n                    updateBlockAt(nx, y, nz);\n                    modifiedBlocks.set(`${nx},${y},${nz}`, BLOCK_TYPES.AIR);\n\n                    if (nBlockType === BLOCK_TYPES.DOOR) {\n                      if (getBlockAt(nx, y + 1, nz) === BLOCK_TYPES.DOOR) {\n                        setBlockAt(nx, y + 1, nz, BLOCK_TYPES.AIR);\n                        updateBlockAt(nx, y + 1, nz);\n                        modifiedBlocks.set(`${nx},${y + 1},${nz}`, BLOCK_TYPES.AIR);\n                        if (isOnline) syncBlockAction('mine', nx, y + 1, nz, BLOCK_TYPES.DOOR);\n                      }\n                      if (getBlockAt(nx, y - 1, nz) === BLOCK_TYPES.DOOR) {\n                        setBlockAt(nx, y - 1, nz, BLOCK_TYPES.AIR);\n                        updateBlockAt(nx, y - 1, nz);\n                        modifiedBlocks.set(`${nx},${y - 1},${nz}`, BLOCK_TYPES.AIR);\n                        if (isOnline) syncBlockAction('mine', nx, y - 1, nz, BLOCK_TYPES.DOOR);\n                      }\n                    }\n\n                    blocksRemoved++;\n\n                    // Sync to other players\n                    if (isOnline) {\n                      syncBlockAction('mine', nx, y, nz, nBlockType);\n                    }\n                  }\n                }\n              }\n\n              // Convert empty bucket to water bucket\n              slot.type = ITEM_TYPES.BUCKET_WATER;\n              updateHotbarUI();\n\n              // Play sound\n              playSound('block_place_sound', false, 0.5);\n              triggerArmSwing(true);\n\n              showMessage(`Filled bucket with water! (${blocksRemoved} blocks)`, '#4a90e2', 1500);\n              return;\n            }\n            // Check for lava pickup (3x3 area)\n            if (\n              targetBlock &&\n              (targetBlock.blockType === BLOCK_TYPES.LAVA ||\n                targetBlock.blockType === BLOCK_TYPES.LAVA_FLOW)\n            ) {\n              const { x, y, z, blockType } = targetBlock;\n\n              // Remove 3x3 area of lava blocks centered on target\n              let blocksRemoved = 0;\n              for (let dx = -1; dx <= 1; dx++) {\n                for (let dz = -1; dz <= 1; dz++) {\n                  const nx = x + dx;\n                  const nz = z + dz;\n                  const nBlockType = getBlockAt(nx, y, nz);\n\n                  if (nBlockType === BLOCK_TYPES.LAVA || nBlockType === BLOCK_TYPES.LAVA_FLOW) {\n                    // If it's a source block, clear its flow blocks first\n                    if (nBlockType === BLOCK_TYPES.LAVA) {\n                      clearFlowLavaFromSource(nx, y, nz);\n                    }\n\n                    setBlockAt(nx, y, nz, BLOCK_TYPES.AIR);\n                    updateBlockAt(nx, y, nz);\n                    modifiedBlocks.set(`${nx},${y},${nz}`, BLOCK_TYPES.AIR);\n\n                    if (nBlockType === BLOCK_TYPES.DOOR) {\n                      if (getBlockAt(nx, y + 1, nz) === BLOCK_TYPES.DOOR) {\n                        setBlockAt(nx, y + 1, nz, BLOCK_TYPES.AIR);\n                        updateBlockAt(nx, y + 1, nz);\n                        modifiedBlocks.set(`${nx},${y + 1},${nz}`, BLOCK_TYPES.AIR);\n                        if (isOnline) syncBlockAction('mine', nx, y + 1, nz, BLOCK_TYPES.DOOR);\n                      }\n                      if (getBlockAt(nx, y - 1, nz) === BLOCK_TYPES.DOOR) {\n                        setBlockAt(nx, y - 1, nz, BLOCK_TYPES.AIR);\n                        updateBlockAt(nx, y - 1, nz);\n                        modifiedBlocks.set(`${nx},${y - 1},${nz}`, BLOCK_TYPES.AIR);\n                        if (isOnline) syncBlockAction('mine', nx, y - 1, nz, BLOCK_TYPES.DOOR);\n                      }\n                    }\n\n                    blocksRemoved++;\n\n                    // Sync to other players\n                    if (isOnline) {\n                      syncBlockAction('mine', nx, y, nz, nBlockType);\n                    }\n                  }\n                }\n              }\n\n              // Convert empty bucket to lava bucket\n              slot.type = ITEM_TYPES.BUCKET_LAVA;\n              updateHotbarUI();\n\n              // Play sound\n              playSound('block_place_sound', false, 0.5);\n              triggerArmSwing(true);\n\n              showMessage(`Filled bucket with lava! (${blocksRemoved} blocks)`, '#FF4500', 1500);\n              return;\n            }\n            return; // Empty bucket can't do anything else\n          }\n\n          // Check if holding water bucket - pour water\n          if (slot && slot.count > 0 && slot.type === ITEM_TYPES.BUCKET_WATER) {\n            if (!targetBlock) return;\n\n            // Calculate placement position based on face normal\n            const normal = targetBlock.face.normal;\n            const px = targetBlock.x + Math.round(normal.x);\n            const py = targetBlock.y + Math.round(normal.y);\n            const pz = targetBlock.z + Math.round(normal.z);\n\n            // Check Y bounds\n            if (py < 0 || py >= worldHeight) return;\n\n            // Check if space is empty\n            if (getBlockAt(px, py, pz) !== BLOCK_TYPES.AIR) return;\n\n            // Check if player is in the way\n            const playerBlockX = Math.floor(playerPos.x);\n            const playerBlockY = Math.floor(playerPos.y);\n            const playerBlockZ = Math.floor(playerPos.z);\n\n            if (\n              px === playerBlockX &&\n              (py === playerBlockY || py === playerBlockY - 1) &&\n              pz === playerBlockZ\n            ) {\n              return;\n            }\n\n            // Place water source block\n            setBlockAt(px, py, pz, BLOCK_TYPES.WATER);\n            updateBlockAt(px, py, pz);\n            modifiedBlocks.set(`${px},${py},${pz}`, BLOCK_TYPES.WATER);\n\n            // Trigger water spreading\n            onWaterPlaced(px, py, pz);\n\n            // Convert water bucket back to empty bucket\n            slot.type = ITEM_TYPES.BUCKET_EMPTY;\n            updateHotbarUI();\n\n            // Play sound\n            playSound('block_place_sound', false, 0.5);\n            triggerArmSwing(true);\n\n            // Sync to other players\n            if (isOnline) {\n              syncBlockAction('place', px, py, pz, BLOCK_TYPES.WATER);\n            }\n\n            showMessage('Poured water!', '#4a90e2', 1500);\n            return;\n          }\n\n          // Check if holding lava bucket - pour lava\n          if (slot && slot.count > 0 && slot.type === ITEM_TYPES.BUCKET_LAVA) {\n            if (!targetBlock) return;\n\n            // Calculate placement position based on face normal\n            const normal = targetBlock.face.normal;\n            const px = targetBlock.x + Math.round(normal.x);\n            const py = targetBlock.y + Math.round(normal.y);\n            const pz = targetBlock.z + Math.round(normal.z);\n\n            // Check Y bounds\n            if (py < 0 || py >= worldHeight) return;\n\n            // Check if space is empty\n            if (getBlockAt(px, py, pz) !== BLOCK_TYPES.AIR) return;\n\n            // Check if player is in the way\n            const playerBlockX = Math.floor(playerPos.x);\n            const playerBlockY = Math.floor(playerPos.y);\n            const playerBlockZ = Math.floor(playerPos.z);\n\n            if (\n              px === playerBlockX &&\n              (py === playerBlockY || py === playerBlockY - 1) &&\n              pz === playerBlockZ\n            ) {\n              return;\n            }\n\n            // Place lava source block\n            setBlockAt(px, py, pz, BLOCK_TYPES.LAVA);\n            updateBlockAt(px, py, pz);\n            modifiedBlocks.set(`${px},${py},${pz}`, BLOCK_TYPES.LAVA);\n\n            // Trigger lava spreading\n            onLavaPlaced(px, py, pz);\n\n            // Convert lava bucket back to empty bucket\n            slot.type = ITEM_TYPES.BUCKET_EMPTY;\n            updateHotbarUI();\n\n            // Play sound\n            playSound('block_place_sound', false, 0.5);\n            triggerArmSwing(true);\n\n            // Sync to other players\n            if (isOnline) {\n              syncBlockAction('place', px, py, pz, BLOCK_TYPES.LAVA);\n            }\n\n            showMessage('Poured lava!', '#FF4500', 1500);\n            return;\n          }\n\n          if (!targetBlock) return;\n\n          // Check if target block is a bed - set spawn point\n          if (targetBlock.blockType === BLOCK_TYPES.BED) {\n            useBed(targetBlock.x, targetBlock.y, targetBlock.z);\n            return;\n          }\n\n          // Check if target block is a crafting table - open crafting UI instead\n          if (targetBlock.blockType === BLOCK_TYPES.CRAFTING_TABLE) {\n            openCraftingTable();\n            return;\n          }\n\n          // Check if target block is a sign - show view UI instead\n          if (targetBlock.blockType === BLOCK_TYPES.OAK_SIGN) {\n            showSignViewUI(targetBlock.x, targetBlock.y, targetBlock.z);\n            return;\n          }\n\n          // Check if target block is a chest - open chest UI instead\n          if (targetBlock.blockType === BLOCK_TYPES.CHEST) {\n            openChest(targetBlock.x, targetBlock.y, targetBlock.z);\n            return;\n          }\n\n          // Check if target block is a furnace - open furnace UI instead\n          if (targetBlock.blockType === BLOCK_TYPES.FURNACE) {\n            openFurnace(targetBlock.x, targetBlock.y, targetBlock.z);\n            return;\n          }\n\n          // Check if target block is a door - toggle open/close instead\n          if (targetBlock.blockType === BLOCK_TYPES.DOOR) {\n            toggleDoor(targetBlock.x, targetBlock.y, targetBlock.z);\n            return;\n          }\n          // slot is already defined at the top for food check\n          if (slot.count <= 0 || slot.type === BLOCK_TYPES.AIR) return;\n\n          // Can't place non-block items\n          if (isItem(slot.type)) return;\n\n          // Calculate placement position based on face normal\n          const normal = targetBlock.face.normal;\n          const px = targetBlock.x + Math.round(normal.x);\n          const py = targetBlock.y + Math.round(normal.y);\n          const pz = targetBlock.z + Math.round(normal.z);\n\n          // Check Y bounds only for infinite world\n          if (py < 0 || py >= worldHeight) return;\n\n          // Check if space is empty\n          if (getBlockAt(px, py, pz) !== BLOCK_TYPES.AIR) return;\n\n          // Check if player is in the way\n          const playerBlockX = Math.floor(playerPos.x);\n          const playerBlockY = Math.floor(playerPos.y);\n          const playerBlockZ = Math.floor(playerPos.z);\n\n          if (\n            px === playerBlockX &&\n            (py === playerBlockY || py === playerBlockY - 1) &&\n            pz === playerBlockZ\n          ) {\n            return;\n          }\n\n          // If placing a sign, show input UI first\n          if (slot.type === BLOCK_TYPES.OAK_SIGN) {\n            // Calculate rotation to face player before showing input UI\n            const signRotation = getRotationToFacePlayer(px, pz);\n\n            showSignInputUI(\n              px,\n              py,\n              pz,\n              (message) => {\n                // On confirm - place the sign with the message\n                setBlockAt(px, py, pz, BLOCK_TYPES.OAK_SIGN);\n                updateBlockAt(px, py, pz);\n\n                // Store the sign message and orientation\n                const signKey = `${px},${py},${pz}`;\n                signMessages.set(signKey, message);\n                signOrientations.set(signKey, signRotation);\n\n                // Update the sign mesh with correct rotation\n                const signMesh = signMeshes.get(signKey);\n                if (signMesh) {\n                  signMesh.rotation.y = signRotation;\n                  updateSignText(signMesh, message);\n                }\n\n                // Track modification\n                const blockKey = `${px},${py},${pz}`;\n                modifiedBlocks.set(blockKey, BLOCK_TYPES.OAK_SIGN);\n\n                // Remove from inventory (skip in creative)\n                if (!isCreativeMode) {\n                  slot.count--;\n                }\n                updateHotbarUI();\n\n                // Play sound\n                playSound('block_place_sound', false, 0.5);\n\n                // Trigger arm swing animation\n                triggerArmSwing(true);\n\n                // Sync to other players\n                if (isOnline) {\n                  syncBlockAction('place', px, py, pz, BLOCK_TYPES.OAK_SIGN);\n                  // Also sync the sign message\n                  syncEvent('sign_message', { x: px, y: py, z: pz, message: message });\n                }\n              },\n              () => {\n                // On cancel - do nothing\n              }\n            );\n            return;\n          }\n\n          // If placing rice plant, only allow on soil\n          if (slot.type === BLOCK_TYPES.RICE_PLANT) {\n            const belowType = getBlockAt(px, py - 1, pz);\n            if (belowType !== BLOCK_TYPES.SOIL) {\n              showMessage('Rice must be planted on soil!', '#8BC34A', 1200);\n              return;\n            }\n          }\n\n          // If placing flower, only allow on grass or soil\n          if (slot.type === BLOCK_TYPES.FLOWER) {\n            const belowType = getBlockAt(px, py - 1, pz);\n            if (belowType !== BLOCK_TYPES.GRASS && belowType !== BLOCK_TYPES.SOIL) {\n              showMessage('Flowers must be placed on grass or soil!', '#8BC34A', 1200);\n              return;\n            }\n          }\n\n          // If placing a bed, need 2 blocks of space in Z direction\n          if (slot.type === BLOCK_TYPES.BED) {\n            if (getBlockAt(px, py, pz + 1) !== BLOCK_TYPES.AIR) {\n              showMessage('Need more space to place bed!', '#FF9800', 1500);\n              return;\n            }\n            // Also check player isn't in the way of the foot block\n            if (\n              px === playerBlockX &&\n              (py === playerBlockY || py === playerBlockY - 1) &&\n              pz + 1 === playerBlockZ\n            ) {\n              return;\n            }\n          }\n\n          // Place block (non-sign)\n          setBlockAt(px, py, pz, slot.type);\n          updateBlockAt(px, py, pz);\n\n          // Track modification\n          const blockKey = `${px},${py},${pz}`;\n          modifiedBlocks.set(blockKey, slot.type);\n\n          // If placing a furnace, set its orientation to face the player\n          if (slot.type === BLOCK_TYPES.FURNACE) {\n            const rotation = getRotationToFacePlayer(px, pz);\n            furnaceOrientations.set(blockKey, rotation);\n            // Update the furnace mesh with the correct rotation\n            const existingMesh = furnaceMeshes.get(blockKey);\n            if (existingMesh) {\n              existingMesh.rotation.y = rotation;\n            }\n          }\n\n          // If placing a chest, set its orientation to face the player\n          if (slot.type === BLOCK_TYPES.CHEST) {\n            const rotation = getRotationToFacePlayer(px, pz);\n            chestOrientations.set(blockKey, rotation);\n            // Update the chest mesh with the correct rotation\n            const existingMesh = chestMeshes.get(blockKey);\n            if (existingMesh) {\n              existingMesh.rotation.y = rotation;\n            }\n          }\n\n          // If placing a bed, also place the foot block (z+1)\n          if (slot.type === BLOCK_TYPES.BED) {\n            setBlockAt(px, py, pz + 1, BLOCK_TYPES.BED);\n            updateBlockAt(px, py, pz + 1);\n            modifiedBlocks.set(`${px},${py},${pz + 1}`, BLOCK_TYPES.BED);\n            if (isOnline) {\n              syncBlockAction('place', px, py, pz + 1, BLOCK_TYPES.BED);\n            }\n          }\n\n          // If placing a door, also place the top block and set orientation to face player\n          if (slot.type === BLOCK_TYPES.DOOR) {\n            if (py + 1 < worldHeight && getBlockAt(px, py + 1, pz) === BLOCK_TYPES.AIR) {\n              setBlockAt(px, py + 1, pz, BLOCK_TYPES.DOOR);\n              updateBlockAt(px, py + 1, pz);\n              modifiedBlocks.set(`${px},${py + 1},${pz}`, BLOCK_TYPES.DOOR);\n\n              // Set door orientation to face player\n              const doorRotation = getRotationToFacePlayer(px, pz);\n              const doorBaseKey = `${px},${py},${pz}`;\n              doorOrientations.set(doorBaseKey, doorRotation);\n\n              // Remove and recreate door mesh with correct rotation and position\n              removeDoorMesh(px, py, pz);\n              addDoorMesh(px, py, pz);\n\n              if (isOnline) {\n                syncBlockAction('place', px, py + 1, pz, BLOCK_TYPES.DOOR);\n              }\n            } else {\n              // Can't place door if top block is blocked\n              setBlockAt(px, py, pz, BLOCK_TYPES.AIR);\n              updateBlockAt(px, py, pz);\n              modifiedBlocks.set(`${px},${py},${pz}`, BLOCK_TYPES.AIR);\n              return;\n            }\n          }\n\n          // If placing water, trigger spreading\n          if (slot.type === BLOCK_TYPES.WATER) {\n            onWaterPlaced(px, py, pz);\n          }\n\n          // Remove from inventory (skip in creative)\n          if (!isCreativeMode) {\n            slot.count--;\n          }\n          updateHotbarUI();\n          // Play sound\n          playSound('block_place_sound', false, 0.5);\n\n          // Trigger arm swing animation\n          triggerArmSwing(true);\n\n          // Sync to other players\n          if (isOnline) {\n            syncBlockAction('place', px, py, pz, slot.type);\n            syncPlayerSwing(); // Sync swing animation\n          }\n        }\n\n        function addToInventory(itemType) {\n          // Tools don't stack (max 1 per slot)\n          const maxStack = isTool(itemType) ? 1 : 64;\n\n          if (!isTool(itemType)) {\n            for (let i = 0; i < inventory.length; i++) {\n              if (inventory[i].type === itemType && inventory[i].count < maxStack) {\n                inventory[i].count++;\n                updateHotbarUI();\n                return;\n              }\n            }\n          }\n\n          // Find empty slot\n          for (let i = 0; i < inventory.length; i++) {\n            if (inventory[i].count === 0 || inventory[i].type === BLOCK_TYPES.AIR) {\n              inventory[i].type = itemType;\n              inventory[i].count = 1;\n              updateHotbarUI();\n              return;\n            }\n          }\n        }\n\n        // Water spreading system - simulates Minecraft water flow with sources + flowing water\n        // Water flows down first, then spreads horizontally up to WATER_SPREAD_DISTANCE blocks\n        const waterUpdateQueue = []; // Queue of water blocks to update\n        let lastWaterUpdate = 0;\n        const WATER_UPDATE_INTERVAL = 120; // Update water every 120ms\n\n        // Lava spreading system - similar to water but slower\n        const lavaUpdateQueue = []; // Queue of lava blocks to update\n        let lastLavaUpdate = 0;\n        const LAVA_UPDATE_INTERVAL = 400; // Lava spreads slower than water (400ms)\n        const LAVA_SPREAD_DISTANCE = Math.max(1, config.waterFlowDistance || 1); // Same spread as water\n        // Schedule a water block for spreading\n        function scheduleWaterSpread(x, y, z, distance = 0, sourceKey = null) {\n          // Don't schedule if already at max distance\n          if (distance > WATER_SPREAD_DISTANCE) return;\n\n          const key = `${x},${y},${z}`;\n          waterUpdateQueue.push({\n            x,\n            y,\n            z,\n            distance,\n            sourceKey,\n            key,\n            time: performance.now() + 120 + distance * 60,\n          });\n        }\n\n        // Process water spread - called in game loop\n        function updateWaterSpread() {\n          const now = performance.now();\n          if (now - lastWaterUpdate < WATER_UPDATE_INTERVAL) return;\n          lastWaterUpdate = now;\n\n          // Process up to 12 water updates per frame to avoid lag\n          let processed = 0;\n          while (waterUpdateQueue.length > 0 && processed < 12) {\n            const item = waterUpdateQueue[0];\n\n            // Check if it's time to process this item\n            if (item.time > now) break;\n\n            waterUpdateQueue.shift();\n            spreadWaterAt(item.x, item.y, item.z, item.distance, item.sourceKey);\n            processed++;\n          }\n\n          // Periodically validate flow blocks near player\n          validateFlowingWaterNearPlayer();\n        }\n\n        // Track last validation time for flow blocks\n        let lastFlowValidation = 0;\n        const FLOW_VALIDATION_INTERVAL = 500; // Check every 500ms\n\n        // Validate that flow blocks near player have a source nearby\n        function validateFlowingWaterNearPlayer() {\n          const now = performance.now();\n          if (now - lastFlowValidation < FLOW_VALIDATION_INTERVAL) return;\n          lastFlowValidation = now;\n\n          // Check flow blocks within a reasonable range of player\n          const checkRange = WATER_SPREAD_DISTANCE + 2;\n          const px = Math.floor(playerPos.x);\n          const py = Math.floor(playerPos.y);\n          const pz = Math.floor(playerPos.z);\n\n          const flowBlocksToRemove = [];\n\n          // Scan area around player for flow blocks\n          for (let x = px - checkRange * 2; x <= px + checkRange * 2; x++) {\n            for (\n              let y = Math.max(0, py - checkRange);\n              y <= Math.min(worldHeight - 1, py + checkRange);\n              y++\n            ) {\n              for (let z = pz - checkRange * 2; z <= pz + checkRange * 2; z++) {\n                const blockType = getBlockAt(x, y, z);\n                if (blockType === BLOCK_TYPES.WATER_FLOW) {\n                  // Check if there's a water source within range\n                  if (!hasWaterSourceNearby(x, y, z)) {\n                    flowBlocksToRemove.push({ x, y, z });\n                  }\n                }\n              }\n            }\n          }\n\n          // Remove orphaned flow blocks (limit per frame to avoid lag)\n          const maxRemovalsPerFrame = 8;\n          for (let i = 0; i < Math.min(maxRemovalsPerFrame, flowBlocksToRemove.length); i++) {\n            const { x, y, z } = flowBlocksToRemove[i];\n            // Double-check it's still a flow block\n            if (getBlockAt(x, y, z) === BLOCK_TYPES.WATER_FLOW) {\n              setBlockAt(x, y, z, BLOCK_TYPES.AIR);\n              updateBlockAt(x, y, z);\n              modifiedBlocks.set(`${x},${y},${z}`, BLOCK_TYPES.AIR);\n              if (isOnline) {\n                syncBlockAction('mine', x, y, z, BLOCK_TYPES.WATER_FLOW);\n              }\n            }\n          }\n        }\n\n        // Check if a flow block has a water source within valid range\n        function hasWaterSourceNearby(fx, fy, fz) {\n          const searchRange = WATER_SPREAD_DISTANCE + 1;\n\n          // Search for a water source block within range\n          for (let x = fx - searchRange; x <= fx + searchRange; x++) {\n            for (let y = fy; y <= Math.min(worldHeight - 1, fy + searchRange); y++) {\n              for (let z = fz - searchRange; z <= fz + searchRange; z++) {\n                if (getBlockAt(x, y, z) === BLOCK_TYPES.WATER) {\n                  // Found a source - verify it could reach this flow block\n                  const dx = Math.abs(x - fx);\n                  const dz = Math.abs(z - fz);\n                  const dy = y - fy; // Source should be at same level or above flow block\n\n                  // Source must be within horizontal spread distance and at or above flow block\n                  if (dx <= WATER_SPREAD_DISTANCE && dz <= WATER_SPREAD_DISTANCE && dy >= 0) {\n                    return true;\n                  }\n                }\n              }\n            }\n          }\n          return false;\n        }\n\n        function spreadWaterAt(x, y, z, distance, sourceKey) {\n          const blockType = getBlockAt(x, y, z);\n          if (blockType !== BLOCK_TYPES.WATER && blockType !== BLOCK_TYPES.WATER_FLOW) return;\n\n          // If this is a flowing block, validate its source still exists\n          if (blockType === BLOCK_TYPES.WATER_FLOW && sourceKey) {\n            const [sx, sy, sz] = sourceKey.split(',').map(Number);\n            if (getBlockAt(sx, sy, sz) !== BLOCK_TYPES.WATER) {\n              setBlockAt(x, y, z, BLOCK_TYPES.AIR);\n              updateBlockAt(x, y, z);\n              modifiedBlocks.set(`${x},${y},${z}`, BLOCK_TYPES.AIR);\n              if (isOnline) {\n                syncBlockAction('mine', x, y, z, BLOCK_TYPES.WATER_FLOW);\n              }\n              return;\n            }\n          }\n\n          // Check for lava contact in all directions and convert to stone\n          const lavaCheckDirs = [\n            [0, -1, 0],\n            [0, 1, 0],\n            [1, 0, 0],\n            [-1, 0, 0],\n            [0, 0, 1],\n            [0, 0, -1],\n          ];\n\n          for (const [dx, dy, dz] of lavaCheckDirs) {\n            const checkX = x + dx;\n            const checkY = y + dy;\n            const checkZ = z + dz;\n\n            if (checkY >= 0 && checkY < worldHeight) {\n              const adjacentBlock = getBlockAt(checkX, checkY, checkZ);\n              if (adjacentBlock === BLOCK_TYPES.LAVA || adjacentBlock === BLOCK_TYPES.LAVA_FLOW) {\n                setBlockAt(checkX, checkY, checkZ, BLOCK_TYPES.STONE);\n                updateBlockAt(checkX, checkY, checkZ);\n                modifiedBlocks.set(`${checkX},${checkY},${checkZ}`, BLOCK_TYPES.STONE);\n                playSound('block_place_sound', false, 0.5);\n                if (isOnline) {\n                  syncBlockAction('place', checkX, checkY, checkZ, BLOCK_TYPES.STONE);\n                }\n              }\n            }\n          }\n\n          // First priority: flow down\n          const belowY = y - 1;\n          if (belowY >= 0) {\n            const belowBlock = getBlockAt(x, belowY, z);\n            if (belowBlock === BLOCK_TYPES.AIR) {\n              // Water flowing down from water or water_flow becomes a source block\n              // (vertical stacking of water orphans creates sources - like Minecraft infinite water)\n              setBlockAt(x, belowY, z, BLOCK_TYPES.WATER);\n              updateBlockAt(x, belowY, z);\n              modifiedBlocks.set(`${x},${belowY},${z}`, BLOCK_TYPES.WATER);\n\n              // Sync to other players\n              if (isOnline) {\n                syncBlockAction('place', x, belowY, z, BLOCK_TYPES.WATER);\n              }\n\n              // Schedule this new water source to spread\n              const newSourceKey = `${x},${belowY},${z}`;\n              scheduleWaterSpread(x, belowY, z, 0, newSourceKey);\n              return; // Water prioritizes falling over spreading\n            }\n          }\n\n          // If at max spread distance, don't spread horizontally\n          if (distance >= WATER_SPREAD_DISTANCE) return;\n\n          // Second priority: spread horizontally (only if can't flow down)\n          const horizontalDirs = [\n            [1, 0], // +X\n            [-1, 0], // -X\n            [0, 1], // +Z\n            [0, -1], // -Z\n          ];\n\n          for (const [dx, dz] of horizontalDirs) {\n            const nx = x + dx;\n            const nz = z + dz;\n\n            const neighborBlock = getBlockAt(nx, y, nz);\n            if (neighborBlock === BLOCK_TYPES.AIR) {\n              // Spread as flow blocks (slaves)\n              setBlockAt(nx, y, nz, BLOCK_TYPES.WATER_FLOW);\n              updateBlockAt(nx, y, nz);\n              modifiedBlocks.set(`${nx},${y},${nz}`, BLOCK_TYPES.WATER_FLOW);\n\n              // Sync to other players\n              if (isOnline) {\n                syncBlockAction('place', nx, y, nz, BLOCK_TYPES.WATER_FLOW);\n              }\n\n              // Schedule this new water to spread with incremented distance\n              scheduleWaterSpread(nx, y, nz, distance + 1, sourceKey);\n            } else if (\n              neighborBlock === BLOCK_TYPES.WATER_FLOW &&\n              sourceKey !== `${nx},${y},${nz}`\n            ) {\n              // Two flow blocks meeting - merge into a source block!\n              setBlockAt(nx, y, nz, BLOCK_TYPES.WATER);\n              updateBlockAt(nx, y, nz);\n              modifiedBlocks.set(`${nx},${y},${nz}`, BLOCK_TYPES.WATER);\n\n              // Sync to other players\n              if (isOnline) {\n                syncBlockAction('place', nx, y, nz, BLOCK_TYPES.WATER);\n              }\n\n              // Schedule as a new source block (distance 0)\n              const newSourceKey = `${nx},${y},${nz}`;\n              scheduleWaterSpread(nx, y, nz, 0, newSourceKey);\n            }\n          }\n        }\n\n        // Called when water is placed by player\n        function onWaterPlaced(x, y, z) {\n          const sourceKey = `${x},${y},${z}`;\n          // Schedule initial water spread\n          scheduleWaterSpread(x, y, z, 0, sourceKey);\n        }\n\n        function refreshNearbyFlowingWater(x, y, z) {\n          const maxDist = WATER_SPREAD_DISTANCE + 1;\n          const sourceKey = `${x},${y},${z}`;\n          for (let fx = x - maxDist; fx <= x + maxDist; fx++) {\n            for (\n              let fy = Math.max(0, y - maxDist);\n              fy <= Math.min(worldHeight - 1, y + maxDist);\n              fy++\n            ) {\n              for (let fz = z - maxDist; fz <= z + maxDist; fz++) {\n                if (getBlockAt(fx, fy, fz) === BLOCK_TYPES.WATER_FLOW) {\n                  scheduleWaterSpread(fx, fy, fz, 0, sourceKey);\n                }\n              }\n            }\n          }\n        }\n\n        // Remove nearby flowing water when a source block is removed\n        function clearFlowWaterFromSource(sx, sy, sz) {\n          const maxDist = WATER_SPREAD_DISTANCE + 1;\n          const sourceKey = `${sx},${sy},${sz}`;\n          for (let x = sx - maxDist; x <= sx + maxDist; x++) {\n            for (\n              let y = Math.max(0, sy - maxDist);\n              y <= Math.min(worldHeight - 1, sy + maxDist);\n              y++\n            ) {\n              for (let z = sz - maxDist; z <= sz + maxDist; z++) {\n                const blockType = getBlockAt(x, y, z);\n                if (blockType === BLOCK_TYPES.WATER_FLOW) {\n                  // Only clear flow blocks that are within horizontal range of the source\n                  const dx = Math.abs(x - sx);\n                  const dz = Math.abs(z - sz);\n                  if (dx <= WATER_SPREAD_DISTANCE && dz <= WATER_SPREAD_DISTANCE) {\n                    setBlockAt(x, y, z, BLOCK_TYPES.AIR);\n                    updateBlockAt(x, y, z);\n                    modifiedBlocks.set(`${x},${y},${z}`, BLOCK_TYPES.AIR);\n                    if (isOnline) {\n                      syncBlockAction('mine', x, y, z, BLOCK_TYPES.WATER_FLOW);\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n\n        // ===== LAVA SPREADING SYSTEM =====\n        // Schedule a lava block for spreading\n        function scheduleLavaSpread(x, y, z, distance = 0, sourceKey = null) {\n          if (distance > LAVA_SPREAD_DISTANCE) return;\n\n          const key = `${x},${y},${z}`;\n          lavaUpdateQueue.push({\n            x,\n            y,\n            z,\n            distance,\n            sourceKey,\n            key,\n            time: performance.now() + 400 + distance * 200, // Lava is slower\n          });\n        }\n\n        // Process lava spread - called in game loop\n        function updateLavaSpread() {\n          const now = performance.now();\n          if (now - lastLavaUpdate < LAVA_UPDATE_INTERVAL) return;\n          lastLavaUpdate = now;\n\n          // Process up to 6 lava updates per frame (slower than water)\n          let processed = 0;\n          while (lavaUpdateQueue.length > 0 && processed < 6) {\n            const item = lavaUpdateQueue[0];\n\n            if (item.time > now) break;\n\n            lavaUpdateQueue.shift();\n            spreadLavaAt(item.x, item.y, item.z, item.distance, item.sourceKey);\n            processed++;\n          }\n\n          // Periodically validate lava flow blocks near player\n          validateFlowingLavaNearPlayer();\n        }\n\n        // Track last validation time for lava flow blocks\n        let lastLavaFlowValidation = 0;\n        const LAVA_FLOW_VALIDATION_INTERVAL = 800; // Check every 800ms (slower than water)\n\n        // Validate that lava flow blocks near player have a source nearby\n        function validateFlowingLavaNearPlayer() {\n          const now = performance.now();\n          if (now - lastLavaFlowValidation < LAVA_FLOW_VALIDATION_INTERVAL) return;\n          lastLavaFlowValidation = now;\n\n          const checkRange = LAVA_SPREAD_DISTANCE + 2;\n          const px = Math.floor(playerPos.x);\n          const py = Math.floor(playerPos.y);\n          const pz = Math.floor(playerPos.z);\n\n          const flowBlocksToRemove = [];\n\n          for (let x = px - checkRange * 2; x <= px + checkRange * 2; x++) {\n            for (\n              let y = Math.max(0, py - checkRange);\n              y <= Math.min(worldHeight - 1, py + checkRange);\n              y++\n            ) {\n              for (let z = pz - checkRange * 2; z <= pz + checkRange * 2; z++) {\n                const blockType = getBlockAt(x, y, z);\n                if (blockType === BLOCK_TYPES.LAVA_FLOW) {\n                  if (!hasLavaSourceNearby(x, y, z)) {\n                    flowBlocksToRemove.push({ x, y, z });\n                  }\n                }\n              }\n            }\n          }\n\n          const maxRemovalsPerFrame = 4;\n          for (let i = 0; i < Math.min(maxRemovalsPerFrame, flowBlocksToRemove.length); i++) {\n            const { x, y, z } = flowBlocksToRemove[i];\n            if (getBlockAt(x, y, z) === BLOCK_TYPES.LAVA_FLOW) {\n              setBlockAt(x, y, z, BLOCK_TYPES.AIR);\n              updateBlockAt(x, y, z);\n              modifiedBlocks.set(`${x},${y},${z}`, BLOCK_TYPES.AIR);\n              if (isOnline) {\n                syncBlockAction('mine', x, y, z, BLOCK_TYPES.LAVA_FLOW);\n              }\n            }\n          }\n        }\n\n        // Check if a lava flow block has a source nearby\n        function hasLavaSourceNearby(fx, fy, fz) {\n          const searchRange = LAVA_SPREAD_DISTANCE + 1;\n\n          for (let x = fx - searchRange; x <= fx + searchRange; x++) {\n            for (let y = fy; y <= Math.min(worldHeight - 1, fy + searchRange); y++) {\n              for (let z = fz - searchRange; z <= fz + searchRange; z++) {\n                if (getBlockAt(x, y, z) === BLOCK_TYPES.LAVA) {\n                  const dx = Math.abs(x - fx);\n                  const dz = Math.abs(z - fz);\n                  const dy = y - fy;\n\n                  if (dx <= LAVA_SPREAD_DISTANCE && dz <= LAVA_SPREAD_DISTANCE && dy >= 0) {\n                    return true;\n                  }\n                }\n              }\n            }\n          }\n          return false;\n        }\n\n        // Spread lava from a source position\n        function spreadLavaAt(x, y, z, distance, sourceKey) {\n          const blockType = getBlockAt(x, y, z);\n          if (blockType !== BLOCK_TYPES.LAVA && blockType !== BLOCK_TYPES.LAVA_FLOW) return;\n\n          // Validate source still exists for flow blocks\n          if (blockType === BLOCK_TYPES.LAVA_FLOW && sourceKey) {\n            const [sx, sy, sz] = sourceKey.split(',').map(Number);\n            if (getBlockAt(sx, sy, sz) !== BLOCK_TYPES.LAVA) {\n              setBlockAt(x, y, z, BLOCK_TYPES.AIR);\n              updateBlockAt(x, y, z);\n              modifiedBlocks.set(`${x},${y},${z}`, BLOCK_TYPES.AIR);\n              if (isOnline) {\n                syncBlockAction('mine', x, y, z, BLOCK_TYPES.LAVA_FLOW);\n              }\n              return;\n            }\n          }\n\n          // First priority: flow down\n          const belowY = y - 1;\n          if (belowY >= 0) {\n            const belowBlock = getBlockAt(x, belowY, z);\n            if (belowBlock === BLOCK_TYPES.AIR) {\n              // Lava flowing down creates source block (like water)\n              setBlockAt(x, belowY, z, BLOCK_TYPES.LAVA);\n              updateBlockAt(x, belowY, z);\n              modifiedBlocks.set(`${x},${belowY},${z}`, BLOCK_TYPES.LAVA);\n\n              if (isOnline) {\n                syncBlockAction('place', x, belowY, z, BLOCK_TYPES.LAVA);\n              }\n\n              const newSourceKey = `${x},${belowY},${z}`;\n              scheduleLavaSpread(x, belowY, z, 0, newSourceKey);\n              return;\n            }\n            // Lava touching water creates stone (cobblestone effect)\n            if (belowBlock === BLOCK_TYPES.WATER || belowBlock === BLOCK_TYPES.WATER_FLOW) {\n              setBlockAt(x, belowY, z, BLOCK_TYPES.STONE);\n              updateBlockAt(x, belowY, z);\n              modifiedBlocks.set(`${x},${belowY},${z}`, BLOCK_TYPES.STONE);\n              playSound('block_place_sound', false, 0.5);\n              if (isOnline) {\n                syncBlockAction('place', x, belowY, z, BLOCK_TYPES.STONE);\n              }\n            }\n          }\n\n          if (distance >= LAVA_SPREAD_DISTANCE) return;\n\n          // Spread horizontally\n          const horizontalDirs = [\n            [1, 0],\n            [-1, 0],\n            [0, 1],\n            [0, -1],\n          ];\n\n          for (const [dx, dz] of horizontalDirs) {\n            const nx = x + dx;\n            const nz = z + dz;\n\n            const neighborBlock = getBlockAt(nx, y, nz);\n            if (neighborBlock === BLOCK_TYPES.AIR) {\n              setBlockAt(nx, y, nz, BLOCK_TYPES.LAVA_FLOW);\n              updateBlockAt(nx, y, nz);\n              modifiedBlocks.set(`${nx},${y},${nz}`, BLOCK_TYPES.LAVA_FLOW);\n\n              if (isOnline) {\n                syncBlockAction('place', nx, y, nz, BLOCK_TYPES.LAVA_FLOW);\n              }\n\n              scheduleLavaSpread(nx, y, nz, distance + 1, sourceKey);\n            } else if (\n              neighborBlock === BLOCK_TYPES.WATER ||\n              neighborBlock === BLOCK_TYPES.WATER_FLOW\n            ) {\n              // Lava touching water creates stone\n              setBlockAt(nx, y, nz, BLOCK_TYPES.STONE);\n              updateBlockAt(nx, y, nz);\n              modifiedBlocks.set(`${nx},${y},${nz}`, BLOCK_TYPES.STONE);\n              playSound('block_place_sound', false, 0.5);\n              if (isOnline) {\n                syncBlockAction('place', nx, y, nz, BLOCK_TYPES.STONE);\n              }\n            } else if (\n              neighborBlock === BLOCK_TYPES.LAVA_FLOW &&\n              sourceKey !== `${nx},${y},${nz}`\n            ) {\n              // Two lava flows meeting - merge into source\n              setBlockAt(nx, y, nz, BLOCK_TYPES.LAVA);\n              updateBlockAt(nx, y, nz);\n              modifiedBlocks.set(`${nx},${y},${nz}`, BLOCK_TYPES.LAVA);\n\n              if (isOnline) {\n                syncBlockAction('place', nx, y, nz, BLOCK_TYPES.LAVA);\n              }\n\n              const newSourceKey = `${nx},${y},${nz}`;\n              scheduleLavaSpread(nx, y, nz, 0, newSourceKey);\n            }\n          }\n        }\n\n        // Called when lava is placed by player\n        function onLavaPlaced(x, y, z) {\n          const sourceKey = `${x},${y},${z}`;\n          scheduleLavaSpread(x, y, z, 0, sourceKey);\n        }\n\n        // Remove nearby flowing lava when a source block is removed\n        function clearFlowLavaFromSource(sx, sy, sz) {\n          const maxDist = LAVA_SPREAD_DISTANCE + 1;\n          for (let x = sx - maxDist; x <= sx + maxDist; x++) {\n            for (\n              let y = Math.max(0, sy - maxDist);\n              y <= Math.min(worldHeight - 1, sy + maxDist);\n              y++\n            ) {\n              for (let z = sz - maxDist; z <= sz + maxDist; z++) {\n                const blockType = getBlockAt(x, y, z);\n                if (blockType === BLOCK_TYPES.LAVA_FLOW) {\n                  const dx = Math.abs(x - sx);\n                  const dz = Math.abs(z - sz);\n                  if (dx <= LAVA_SPREAD_DISTANCE && dz <= LAVA_SPREAD_DISTANCE) {\n                    setBlockAt(x, y, z, BLOCK_TYPES.AIR);\n                    updateBlockAt(x, y, z);\n                    modifiedBlocks.set(`${x},${y},${z}`, BLOCK_TYPES.AIR);\n                    if (isOnline) {\n                      syncBlockAction('mine', x, y, z, BLOCK_TYPES.LAVA_FLOW);\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n\n        // Particles\n        const particles = [];\n\n        // Arrow weapon projectiles\n        const arrows = [];\n        const ARROW_SPEED = 26;\n        const ARROW_GRAVITY = -8;\n        const ARROW_LIFETIME = 3.0;\n        const ARROW_COOLDOWN = 0.35;\n        const ARROW_DAMAGE = 8;\n        let lastArrowShotTime = 0;\n\n        function isArrowWeaponSelected() {\n          const slot = inventory[selectedSlot];\n          return !!slot && slot.type === ITEM_TYPES.ARROW_WEAPON && slot.count > 0;\n        }\n\n        function createArrowProjectile(origin, direction) {\n          const arrowGroup = new THREE.Group();\n\n          const shaft = new THREE.Mesh(\n            new THREE.BoxGeometry(0.04, 0.04, 0.45),\n            new THREE.MeshLambertMaterial({ color: 0x8b5a2b })\n          );\n          shaft.position.z = -0.05;\n          arrowGroup.add(shaft);\n\n          const tip = new THREE.Mesh(\n            new THREE.ConeGeometry(0.06, 0.18, 6),\n            new THREE.MeshLambertMaterial({ color: 0x9e9e9e })\n          );\n          tip.rotation.x = Math.PI / 2;\n          tip.position.z = -0.32;\n          arrowGroup.add(tip);\n\n          const fletch = new THREE.Mesh(\n            new THREE.BoxGeometry(0.12, 0.03, 0.08),\n            new THREE.MeshLambertMaterial({ color: 0xffffff })\n          );\n          fletch.position.z = 0.16;\n          arrowGroup.add(fletch);\n\n          arrowGroup.position.copy(origin);\n          const dir = direction.clone().normalize();\n          arrowGroup.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, -1), dir);\n\n          scene.add(arrowGroup);\n\n          arrows.push({\n            mesh: arrowGroup,\n            position: origin.clone(),\n            velocity: dir.multiplyScalar(ARROW_SPEED),\n            life: ARROW_LIFETIME,\n          });\n        }\n\n        function fireArrowWeapon() {\n          const now = performance.now() / 1000;\n          if (now - lastArrowShotTime < ARROW_COOLDOWN) return false;\n          if (!isArrowWeaponSelected()) return false;\n\n          lastArrowShotTime = now;\n\n          const direction = new THREE.Vector3();\n          camera.getWorldDirection(direction);\n          // Fire from player position in 3rd person\n          const origin = playerPos.clone().add(direction.clone().multiplyScalar(0.9));\n          origin.y -= 0.2;\n\n          createArrowProjectile(origin, direction);\n          playSound('block_place_sound', false, 0.35);\n          triggerArmSwing(true);\n          if (isOnline) syncPlayerSwing();\n          return true;\n        }\n\n        function updateArrows(dt) {\n          for (let i = arrows.length - 1; i >= 0; i--) {\n            const arrow = arrows[i];\n            const prevPos = arrow.position.clone();\n\n            arrow.velocity.y += ARROW_GRAVITY * dt;\n            arrow.position.addScaledVector(arrow.velocity, dt);\n            arrow.life -= dt;\n\n            // Rotate arrow to face travel direction\n            const velDir = arrow.velocity.clone().normalize();\n            arrow.mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, -1), velDir);\n            arrow.mesh.position.copy(arrow.position);\n\n            // Check collision with blocks\n            const bx = Math.floor(arrow.position.x);\n            const by = Math.floor(arrow.position.y);\n            const bz = Math.floor(arrow.position.z);\n\n            let hitBlock = false;\n            if (by < 0 || by >= worldHeight) {\n              hitBlock = true;\n            } else {\n              const hitType = getBlockAt(bx, by, bz);\n              if (\n                hitType !== BLOCK_TYPES.AIR &&\n                hitType !== BLOCK_TYPES.WATER &&\n                hitType !== BLOCK_TYPES.WATER_FLOW &&\n                hitType !== BLOCK_TYPES.LAVA &&\n                hitType !== BLOCK_TYPES.LAVA_FLOW &&\n                hitType !== BLOCK_TYPES.RICE_PLANT &&\n                hitType !== BLOCK_TYPES.FLOWER\n              ) {\n                hitBlock = true;\n              }\n            }\n\n            // Check collision with mobs\n            let hitMob = null;\n            if (!hitBlock) {\n              const segment = arrow.position.clone().sub(prevPos);\n              const segLenSq = Math.max(segment.lengthSq(), 0.0001);\n\n              for (const mob of mobs) {\n                if (!mob || !mob.group || mob.health <= 0) continue;\n\n                const toMob = mob.position.clone().sub(prevPos);\n                const t = Math.max(0, Math.min(1, toMob.dot(segment) / segLenSq));\n                const closest = prevPos.clone().addScaledVector(segment, t);\n                const hitRadius = Math.max(0.35, mob.width * 0.75);\n\n                if (\n                  closest.distanceTo(mob.position) <= hitRadius &&\n                  Math.abs(closest.y - mob.position.y) <= mob.size * 0.6\n                ) {\n                  hitMob = mob;\n                  break;\n                }\n              }\n            }\n\n            if (hitMob) {\n              damageMob(hitMob, ARROW_DAMAGE);\n              if (typeof syncMobHit === 'function') {\n                syncMobHit(hitMob.id, ARROW_DAMAGE);\n              }\n              createBreakParticles(arrow.position.x, arrow.position.y, arrow.position.z, BLOCK_TYPES.STONE);\n              hitBlock = true;\n            }\n\n            if (hitBlock || arrow.life <= 0) {\n              scene.remove(arrow.mesh);\n              arrow.mesh.traverse((child) => {\n                if (child.geometry) child.geometry.dispose();\n                if (child.material) {\n                  if (Array.isArray(child.material)) {\n                    child.material.forEach((m) => m.dispose());\n                  } else {\n                    child.material.dispose();\n                  }\n                }\n              });\n              arrows.splice(i, 1);\n            }\n          }\n        }\n\n        // Mob System\n        function createMob(type, x, y, z) {\n          const mob = {\n            type: type,\n            position: new THREE.Vector3(x, y, z),\n            velocity: new THREE.Vector3(0, 0, 0),\n            health:\n              type === MOB_TYPES.ZOMBIE\n                ? 20\n                : type === MOB_TYPES.CHICKEN\n                  ? 4\n                  : type === MOB_TYPES.DOG\n                    ? 8\n                    : type === MOB_TYPES.COW\n                      ? 10\n                      : type === MOB_TYPES.VILLAGER\n                        ? 20\n                        : 10,\n            maxHealth:\n              type === MOB_TYPES.ZOMBIE\n                ? 20\n                : type === MOB_TYPES.CHICKEN\n                  ? 4\n                  : type === MOB_TYPES.DOG\n                    ? 8\n                    : type === MOB_TYPES.COW\n                      ? 10\n                      : type === MOB_TYPES.VILLAGER\n                        ? 20\n                        : 10,\n            size:\n              type === MOB_TYPES.ZOMBIE\n                ? 1.8\n                : type === MOB_TYPES.CHICKEN\n                  ? 0.7\n                  : type === MOB_TYPES.DOG\n                    ? 0.9\n                    : type === MOB_TYPES.COW\n                      ? 1.4\n                      : type === MOB_TYPES.VILLAGER\n                        ? 1.8\n                        : 1.4,\n            width:\n              type === MOB_TYPES.CHICKEN\n                ? 0.35\n                : type === MOB_TYPES.DOG\n                  ? 0.45\n                  : type === MOB_TYPES.COW\n                    ? 0.6\n                    : type === MOB_TYPES.VILLAGER\n                      ? 0.5\n                      : 0.6,\n            isGrounded: false,\n            targetPlayer:\n              type !== MOB_TYPES.CHICKEN &&\n              type !== MOB_TYPES.DOG &&\n              type !== MOB_TYPES.COW &&\n              type !== MOB_TYPES.VILLAGER,\n            lastSoundTime: 0,\n            exploding: false,\n            explodeProgress: 0,\n            EXPLODE_DISTANCE: 3,\n            EXPLODE_TIME: 1.5,\n            animationTime: 0,\n            leftArm: null,\n            rightArm: null,\n            leftLeg: null,\n            rightLeg: null,\n            isPassive:\n              type === MOB_TYPES.CHICKEN ||\n              type === MOB_TYPES.DOG ||\n              type === MOB_TYPES.COW ||\n              type === MOB_TYPES.VILLAGER,\n          };\n\n          // Create group for multiple meshes\n          mob.group = new THREE.Group();\n\n          if (type === MOB_TYPES.ZOMBIE) {\n            // Zombie - use GLB model if available\n            if (zombieModelTemplate) {\n              const zombieModel = zombieModelTemplate.clone();\n\n\n              zombieModel.traverse((child) => {\n                    if (child.isMesh) {\n                      child.castShadow = true;\n                      child.receiveShadow = true;\n                      // Clone material to avoid shared state issues\n                      if (child.material) {\n                        child.material = child.material.clone();\n                      }\n                    }\n                  });\n\n              // Adjust position so feet are at ground level\n              zombieModel.position.y = -0.9;\n\n              mob.group.add(zombieModel);\n\n              // Store reference for animations\n              mob.model = zombieModel;\n              mob.useGLBAnimation = true;\n\n              // Set up AnimationMixer if animations are available\n              if (zombieModelAnimations && zombieModelAnimations.length > 0) {\n                const mixer = new THREE.AnimationMixer(zombieModel);\n                mob.mixer = mixer;\n                mob.actions = {};\n                mob.currentAction = null;\n\n                // Process animations to separate arms from the rest of the body\n                const holdingClip = zombieModelAnimations.find((a) =>\n                  a.name.toLowerCase().includes('holding-both')\n                );\n\n                let armTrackNames = [];\n                if (holdingClip) {\n                  // Filter tracks to only include arm bones (arm-left, arm-right)\n                  const armTracks = holdingClip.tracks.filter((track) => {\n                    const trackName = track.name.toLowerCase();\n                    return (\n                      trackName.includes('arm-left') ||\n                      trackName.includes('arm-right') ||\n                      trackName.includes('arm_left') ||\n                      trackName.includes('arm_right') ||\n                      trackName.includes('arm')\n                    );\n                  });\n\n                  armTrackNames = armTracks.map((t) => t.name);\n\n                  if (armTracks.length > 0) {\n                    const armsOnlyClip = new THREE.AnimationClip(\n                      'arms-holding',\n                      holdingClip.duration,\n                      armTracks\n                    );\n                    const armsAction = mixer.clipAction(armsOnlyClip);\n                    armsAction.play();\n                    armsAction.setEffectiveWeight(1.0);\n                    mob.armsAction = armsAction;\n                  }\n                }\n\n                // Filter out head tracks from all animations so we can control head rotation manually\n                let headTrackNames = [];\n                for (const clip of zombieModelAnimations) {\n                  const headTracks = clip.tracks.filter((track) => {\n                    const trackName = track.name.toLowerCase();\n                    return trackName.includes('head');\n                  });\n                  headTrackNames = headTrackNames.concat(headTracks.map((t) => t.name));\n                }\n                // Remove duplicates\n                headTrackNames = [...new Set(headTrackNames)];\n\n                // Create actions for each animation, filtering out arm and head tracks from walk/idle\n                for (const clip of zombieModelAnimations) {\n                  const clipName = clip.name.toLowerCase();\n                  if (\n                    (clipName === 'idle' || clipName === 'walk') &&\n                    (armTrackNames.length > 0 || headTrackNames.length > 0)\n                  ) {\n                    const filteredTracks = clip.tracks.filter(\n                      (track) =>\n                        !armTrackNames.includes(track.name) && !headTrackNames.includes(track.name)\n                    );\n                    if (filteredTracks.length > 0) {\n                      const filteredClip = new THREE.AnimationClip(\n                        clip.name,\n                        clip.duration,\n                        filteredTracks\n                      );\n                      const action = mixer.clipAction(filteredClip);\n                      mob.actions[clipName] = action;\n                    }\n                  } else {\n                    const action = mixer.clipAction(clip);\n                    mob.actions[clipName] = action;\n                  }\n                }\n\n                // Start with idle animation\n                const idleAction = mob.actions['idle'];\n                if (idleAction) {\n                  idleAction.play();\n                  mob.currentAction = 'idle';\n                }\n              }\n\n              // Try to find head bone for pitch/yaw rotation (like player)\n              zombieModel.traverse((child) => {\n                const name = child.name.toLowerCase();\n                if (name.includes('head')) {\n                  mob.head = child;\n                }\n              });\n            }\n          } else if (type === MOB_TYPES.CREEPER) {\n            // Creeper - multiple green cubes\n            const textureKey = 'creeper_texture';\n            const skin = assetCache.images[textureKey];\n\n            function getSubTexture(image, x, y, w, h) {\n              const fullW = image.width || 64;\n              const fullH = image.height || 64;\n              const tex = new THREE.Texture(image);\n              tex.repeat.set(w / fullW, h / fullH);\n              tex.offset.set(x / fullW, 1 - (y + h) / fullH);\n              tex.magFilter = THREE.NearestFilter;\n              tex.minFilter = THREE.NearestFilter;\n              tex.needsUpdate = true;\n              return tex;\n            }\n\n            let headMaterials, bodyMaterials, legMaterials;\n            if (skin) {\n              headMaterials = [\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 0, 8, 8, 8) }), // right\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 16, 8, 8, 8) }), // left\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 8, 0, 8, 8) }), // top\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 16, 0, 8, 8) }), // bottom\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 8, 8, 8, 8) }), // front\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 24, 8, 8, 8) }), // back\n              ];\n\n              bodyMaterials = [\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 16, 20, 4, 12) }), // right\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 28, 20, 4, 12) }), // left\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 20, 16, 8, 4) }), // top\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 28, 16, 8, 4) }), // bottom\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 20, 20, 8, 12) }), // front\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 32, 20, 8, 12) }), // back\n              ];\n\n              legMaterials = [\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 0, 20, 4, 6) }), // right\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 8, 20, 4, 6) }), // left\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 4, 16, 4, 4) }), // top\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 8, 16, 4, 4) }), // bottom\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 4, 20, 4, 6) }), // front\n                new THREE.MeshLambertMaterial({ map: getSubTexture(skin, 12, 20, 4, 6) }), // back\n              ];\n            } else {\n              const defaultMat = new THREE.MeshLambertMaterial({ color: 0x0da70d });\n              headMaterials = [defaultMat, defaultMat, defaultMat, defaultMat, defaultMat, defaultMat];\n              bodyMaterials = [defaultMat, defaultMat, defaultMat, defaultMat, defaultMat, defaultMat];\n              legMaterials = [defaultMat, defaultMat, defaultMat, defaultMat, defaultMat, defaultMat];\n            }\n\n            // Head\n            const head = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), headMaterials);\n            head.position.set(0, 0.5, 0);\n            mob.group.add(head);\n\n            // Body (taller than zombie)\n            const body = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.8, 0.4), bodyMaterials);\n            body.position.set(0, -0.1, 0);\n            mob.group.add(body);\n\n            // Front left leg\n            const frontLeftLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.2, 0.4, 0.2),\n              legMaterials\n            );\n            frontLeftLeg.position.set(-0.15, -0.6, 0.15);\n            mob.group.add(frontLeftLeg);\n            mob.frontLeftLeg = frontLeftLeg;\n\n            // Front right leg\n            const frontRightLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.2, 0.4, 0.2),\n              legMaterials\n            );\n            frontRightLeg.position.set(0.15, -0.6, 0.15);\n            mob.group.add(frontRightLeg);\n            mob.frontRightLeg = frontRightLeg;\n\n            // Back left leg\n            const backLeftLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.2, 0.4, 0.2),\n              legMaterials\n            );\n            backLeftLeg.position.set(-0.15, -0.6, -0.15);\n            mob.group.add(backLeftLeg);\n            mob.backLeftLeg = backLeftLeg;\n\n            // Back right leg\n            const backRightLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.2, 0.4, 0.2),\n              legMaterials\n            );\n            backRightLeg.position.set(0.15, -0.6, -0.15);\n            mob.group.add(backRightLeg);\n            mob.backRightLeg = backRightLeg;\n          } else if (type === MOB_TYPES.CHICKEN) {\n            // Chicken - small white bird\n            const textureKey = 'chicken_texture';\n            const texture = assetCache.images[textureKey];\n\n            let bodyMaterial, headMaterial;\n            if (texture) {\n              const tex = new THREE.Texture(texture);\n              tex.magFilter = THREE.NearestFilter;\n              tex.minFilter = THREE.NearestFilter;\n              tex.needsUpdate = true;\n              bodyMaterial = new THREE.MeshLambertMaterial({ map: tex });\n              headMaterial = new THREE.MeshLambertMaterial({ map: tex });\n            } else {\n              bodyMaterial = new THREE.MeshLambertMaterial({ color: 0xf5f5f5 }); // White body\n              headMaterial = new THREE.MeshLambertMaterial({ color: 0xf5f5f5 });\n            }\n\n            // Body (oval-ish box)\n            const body = new THREE.Mesh(\n              new THREE.BoxGeometry(0.35, 0.3, 0.45),\n              bodyMaterial.clone()\n            );\n            body.position.set(0, 0, 0);\n            mob.group.add(body);\n\n            // Head (small cube)\n            const head = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.2, 0.2), headMaterial.clone());\n            head.position.set(0, 0.2, 0.2);\n            mob.group.add(head);\n            mob.head = head;\n\n            // Beak (orange)\n            const beakMaterial = new THREE.MeshLambertMaterial({ color: 0xff8c00 });\n            const beak = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.05, 0.1), beakMaterial);\n            beak.position.set(0, 0.15, 0.35);\n            mob.group.add(beak);\n\n            // Comb/Wattle (red on top of head)\n            const combMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });\n            const comb = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.1, 0.1), combMaterial);\n            comb.position.set(0, 0.35, 0.2);\n            mob.group.add(comb);\n\n            // Wattle (under beak)\n            const wattle = new THREE.Mesh(\n              new THREE.BoxGeometry(0.04, 0.08, 0.04),\n              combMaterial.clone()\n            );\n            wattle.position.set(0, 0.08, 0.32);\n            mob.group.add(wattle);\n\n            // Left leg (orange)\n            const legMaterial = new THREE.MeshLambertMaterial({ color: 0xff8c00 });\n            const leftLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.05, 0.2, 0.05),\n              legMaterial.clone()\n            );\n            leftLeg.position.set(-0.1, -0.25, 0);\n            mob.group.add(leftLeg);\n            mob.leftLeg = leftLeg;\n\n            // Right leg\n            const rightLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.05, 0.2, 0.05),\n              legMaterial.clone()\n            );\n            rightLeg.position.set(0.1, -0.25, 0);\n            mob.group.add(rightLeg);\n            mob.rightLeg = rightLeg;\n\n            // Wing left\n            const wingMaterial = new THREE.MeshLambertMaterial({ color: 0xe8e8e8 });\n            const leftWing = new THREE.Mesh(\n              new THREE.BoxGeometry(0.05, 0.15, 0.25),\n              wingMaterial.clone()\n            );\n            leftWing.position.set(-0.2, 0.05, 0);\n            mob.group.add(leftWing);\n            mob.leftWing = leftWing;\n\n            // Wing right\n            const rightWing = new THREE.Mesh(\n              new THREE.BoxGeometry(0.05, 0.15, 0.25),\n              wingMaterial.clone()\n            );\n            rightWing.position.set(0.2, 0.05, 0);\n            mob.group.add(rightWing);\n            mob.rightWing = rightWing;\n\n            // Tail feathers\n            const tailMaterial = new THREE.MeshLambertMaterial({ color: 0xe0e0e0 });\n            const tail = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.2, 0.1), tailMaterial);\n            tail.position.set(0, 0.1, -0.25);\n            tail.rotation.x = -0.3;\n            mob.group.add(tail);\n\n            // Chicken-specific properties\n            mob.isPassive = true;\n            mob.fleeDistance = 5; // Distance at which chicken flees from player\n            mob.wanderTime = 0;\n            mob.wanderDirection = new THREE.Vector3();\n          } else if (type === MOB_TYPES.DOG) {\n            // Dog - friendly passive mob\n            const furMaterial = new THREE.MeshLambertMaterial({ color: 0xd6c2a3 });\n            const darkFurMaterial = new THREE.MeshLambertMaterial({ color: 0xb89a72 });\n            const noseMaterial = new THREE.MeshLambertMaterial({ color: 0x2b2b2b });\n            const earMaterial = new THREE.MeshLambertMaterial({ color: 0x8b6f47 });\n\n            // Body\n            const body = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.3, 0.7), furMaterial.clone());\n            body.position.set(0, 0.05, 0);\n            mob.group.add(body);\n            mob.body = body;\n\n            // Chest\n            const chest = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.25, 0.3), furMaterial.clone());\n            chest.position.set(0, 0.05, 0.35);\n            mob.group.add(chest);\n            mob.chest = chest;\n            mob.standPose = {\n              bodyY: 0.05,\n              chestY: 0.05,\n              headY: 0.2,\n              tailRotX: 0.8,\n            };\n\n            // Head\n            const head = new THREE.Mesh(new THREE.BoxGeometry(0.28, 0.24, 0.28), furMaterial.clone());\n            head.position.set(0, 0.2, 0.55);\n            mob.group.add(head);\n            mob.head = head;\n\n            // Snout\n            const snout = new THREE.Mesh(\n              new THREE.BoxGeometry(0.18, 0.14, 0.22),\n              darkFurMaterial.clone()\n            );\n            snout.position.set(0, 0.14, 0.72);\n            mob.group.add(snout);\n\n            // Nose\n            const nose = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.06, 0.06), noseMaterial);\n            nose.position.set(0, 0.12, 0.84);\n            mob.group.add(nose);\n\n            // Ears\n            const leftEar = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.16, 0.05), earMaterial.clone());\n            leftEar.position.set(-0.12, 0.3, 0.5);\n            leftEar.rotation.x = -0.3;\n            mob.group.add(leftEar);\n\n            const rightEar = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.16, 0.05), earMaterial.clone());\n            rightEar.position.set(0.12, 0.3, 0.5);\n            rightEar.rotation.x = -0.3;\n            mob.group.add(rightEar);\n\n            // Tail\n            const tail = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.2, 0.06), darkFurMaterial.clone());\n            tail.position.set(0, 0.18, -0.35);\n            tail.rotation.x = 0.8;\n            mob.group.add(tail);\n            mob.tail = tail;\n\n            // Legs\n            const legMaterial = new THREE.MeshLambertMaterial({ color: 0xa7895c });\n            const legGeo = new THREE.BoxGeometry(0.08, 0.25, 0.08);\n\n            const frontLeftLeg = new THREE.Mesh(legGeo, legMaterial.clone());\n            frontLeftLeg.position.set(-0.16, -0.2, 0.25);\n            mob.group.add(frontLeftLeg);\n            mob.frontLeftLeg = frontLeftLeg;\n\n            const frontRightLeg = new THREE.Mesh(legGeo, legMaterial.clone());\n            frontRightLeg.position.set(0.16, -0.2, 0.25);\n            mob.group.add(frontRightLeg);\n            mob.frontRightLeg = frontRightLeg;\n\n            const backLeftLeg = new THREE.Mesh(legGeo, legMaterial.clone());\n            backLeftLeg.position.set(-0.16, -0.2, -0.25);\n            mob.group.add(backLeftLeg);\n            mob.backLeftLeg = backLeftLeg;\n\n            const backRightLeg = new THREE.Mesh(legGeo, legMaterial.clone());\n            backRightLeg.position.set(0.16, -0.2, -0.25);\n            mob.group.add(backRightLeg);\n            mob.backRightLeg = backRightLeg;\n\n            // Dog-specific properties\n            mob.isPassive = true;\n            mob.wanderTime = 0;\n            mob.wanderDirection = new THREE.Vector3();\n            mob.isFriendly = true;\n          } else if (type === MOB_TYPES.COW) {\n            // Cow - larger passive farm animal\n            const textureKey = 'cow_texture';\n            const texture = assetCache.images[textureKey];\n\n            let bodyMaterial, headMaterial;\n            if (texture) {\n              const tex = new THREE.Texture(texture);\n              tex.magFilter = THREE.NearestFilter;\n              tex.minFilter = THREE.NearestFilter;\n              tex.needsUpdate = true;\n              bodyMaterial = new THREE.MeshLambertMaterial({ map: tex });\n              headMaterial = new THREE.MeshLambertMaterial({ map: tex });\n            } else {\n              bodyMaterial = new THREE.MeshLambertMaterial({ color: 0x4a3728 }); // Brown body\n              headMaterial = new THREE.MeshLambertMaterial({ color: 0x4a3728 });\n            }\n\n            const whiteMaterial = new THREE.MeshLambertMaterial({ color: 0xf5f5f5 }); // White patches\n            const pinkMaterial = new THREE.MeshLambertMaterial({ color: 0xffc0cb }); // Pink nose\n\n            // Body (large rectangular box)\n            const body = new THREE.Mesh(\n              new THREE.BoxGeometry(0.6, 0.55, 0.9),\n              bodyMaterial.clone()\n            );\n            body.position.set(0, 0.1, 0);\n            mob.group.add(body);\n\n            // White patch on body\n            const whitePatch = new THREE.Mesh(\n              new THREE.BoxGeometry(0.62, 0.3, 0.5),\n              whiteMaterial.clone()\n            );\n            whitePatch.position.set(0, 0.15, 0.1);\n            mob.group.add(whitePatch);\n\n            // Head (cube)\n            const head = new THREE.Mesh(\n              new THREE.BoxGeometry(0.4, 0.35, 0.35),\n              bodyMaterial.clone()\n            );\n            head.position.set(0, 0.25, 0.55);\n            mob.group.add(head);\n            mob.head = head;\n\n            // Snout/Nose (lighter colored)\n            const snout = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.15, 0.1), pinkMaterial);\n            snout.position.set(0, 0.15, 0.75);\n            mob.group.add(snout);\n\n            // Left horn\n            const hornMaterial = new THREE.MeshLambertMaterial({ color: 0xd4c4a8 });\n            const leftHorn = new THREE.Mesh(\n              new THREE.BoxGeometry(0.06, 0.15, 0.06),\n              hornMaterial.clone()\n            );\n            leftHorn.position.set(-0.12, 0.48, 0.5);\n            leftHorn.rotation.z = 0.3;\n            mob.group.add(leftHorn);\n\n            // Right horn\n            const rightHorn = new THREE.Mesh(\n              new THREE.BoxGeometry(0.06, 0.15, 0.06),\n              hornMaterial.clone()\n            );\n            rightHorn.position.set(0.12, 0.48, 0.5);\n            rightHorn.rotation.z = -0.3;\n            mob.group.add(rightHorn);\n\n            // Ears\n            const earMaterial = new THREE.MeshLambertMaterial({ color: 0x5a4030 });\n            const leftEar = new THREE.Mesh(\n              new THREE.BoxGeometry(0.12, 0.08, 0.05),\n              earMaterial.clone()\n            );\n            leftEar.position.set(-0.22, 0.35, 0.5);\n            mob.group.add(leftEar);\n\n            const rightEar = new THREE.Mesh(\n              new THREE.BoxGeometry(0.12, 0.08, 0.05),\n              earMaterial.clone()\n            );\n            rightEar.position.set(0.22, 0.35, 0.5);\n            mob.group.add(rightEar);\n\n            // Legs (4 legs)\n            const legMaterial = new THREE.MeshLambertMaterial({ color: 0x4a3728 });\n\n            // Front left leg\n            const frontLeftLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.12, 0.35, 0.12),\n              legMaterial.clone()\n            );\n            frontLeftLeg.position.set(-0.2, -0.35, 0.3);\n            mob.group.add(frontLeftLeg);\n            mob.frontLeftLeg = frontLeftLeg;\n\n            // Front right leg\n            const frontRightLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.12, 0.35, 0.12),\n              legMaterial.clone()\n            );\n            frontRightLeg.position.set(0.2, -0.35, 0.3);\n            mob.group.add(frontRightLeg);\n            mob.frontRightLeg = frontRightLeg;\n\n            // Back left leg\n            const backLeftLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.12, 0.35, 0.12),\n              legMaterial.clone()\n            );\n            backLeftLeg.position.set(-0.2, -0.35, -0.3);\n            mob.group.add(backLeftLeg);\n            mob.backLeftLeg = backLeftLeg;\n\n            // Back right leg\n            const backRightLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.12, 0.35, 0.12),\n              legMaterial.clone()\n            );\n            backRightLeg.position.set(0.2, -0.35, -0.3);\n            mob.group.add(backRightLeg);\n            mob.backRightLeg = backRightLeg;\n\n            // Tail\n            const tailMaterial = new THREE.MeshLambertMaterial({ color: 0x4a3728 });\n            const tail = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.25, 0.06), tailMaterial);\n            tail.position.set(0, 0.0, -0.5);\n            tail.rotation.x = 0.5;\n            mob.group.add(tail);\n\n            // Udder (pink, underneath)\n            const udder = new THREE.Mesh(\n              new THREE.BoxGeometry(0.2, 0.1, 0.15),\n              pinkMaterial.clone()\n            );\n            udder.position.set(0, -0.2, -0.15);\n            mob.group.add(udder);\n\n            // Cow-specific properties\n            mob.isPassive = true;\n            mob.fleeDistance = 4; // Distance at which cow flees from player when attacked\n            mob.wanderTime = 0;\n            mob.wanderDirection = new THREE.Vector3();\n            mob.isAggravated = false; // Cows only flee when hit\n          } else if (type === MOB_TYPES.VILLAGER) {\n            // Villager - passive NPC that wanders around\n            if (villagerModelTemplate) {\n              const villagerModel = villagerModelTemplate.clone();\n\n              // Clone materials to allow independent modifications\n               villagerModel.traverse((child) => {\n                    if (child.isMesh) {\n                      child.castShadow = true;\n                      child.receiveShadow = true;\n                      // Clone material to avoid shared state issues\n                      if (child.material) {\n                        child.material = child.material.clone();\n                      }\n                    }\n                  });\n\n              // Adjust position so feet are at ground level\n              villagerModel.position.y = -0.9;\n\n              mob.group.add(villagerModel);\n\n              // Store reference for animations\n              mob.model = villagerModel;\n              mob.useGLBAnimation = true;\n\n              // Set up AnimationMixer if animations are available\n              if (villagerModelAnimations && villagerModelAnimations.length > 0) {\n                const mixer = new THREE.AnimationMixer(villagerModel);\n                mob.mixer = mixer;\n                mob.actions = {};\n                mob.currentAction = null;\n\n                // Filter out arm tracks from all animations so we can control head rotation manually\n                let armTrackNames = [];\n                for (const clip of villagerModelAnimations) {\n                  const headTracks = clip.tracks.filter((track) => {\n                    const trackName = track.name.toLowerCase();\n                    return trackName.includes('arm');\n                  });\n                  armTrackNames = armTrackNames.concat(headTracks.map((t) => t.name));\n                }\n                // Remove duplicates\n                armTrackNames = [...new Set(armTrackNames)];\n\n                // Filter out head tracks from all animations so we can control head rotation manually\n                let headTrackNames = [];\n                for (const clip of villagerModelAnimations) {\n                  const headTracks = clip.tracks.filter((track) => {\n                    const trackName = track.name.toLowerCase();\n                    return trackName.includes('head');\n                  });\n                  headTrackNames = headTrackNames.concat(headTracks.map((t) => t.name));\n                }\n                // Remove duplicates\n                headTrackNames = [...new Set(headTrackNames)];\n\n                // Create actions for each animation, filtering out head tracks from walk/idle\n                for (const clip of villagerModelAnimations) {\n                  const clipName = clip.name.toLowerCase();\n                  if (\n                    (clipName === 'idle' || clipName === 'walk') &&\n                    (armTrackNames.length > 0 || headTrackNames.length > 0)\n                  ) {\n                    const filteredTracks = clip.tracks.filter(\n                      (track) =>\n                        !armTrackNames.includes(track.name) && !headTrackNames.includes(track.name)\n                    );\n                    if (filteredTracks.length > 0) {\n                      const filteredClip = new THREE.AnimationClip(\n                        clip.name,\n                        clip.duration,\n                        filteredTracks\n                      );\n                      const action = mixer.clipAction(filteredClip);\n                      mob.actions[clipName] = action;\n                    }\n                  } else {\n                    const action = mixer.clipAction(clip);\n                    mob.actions[clipName] = action;\n                  }\n                }\n\n                // Start with idle animation\n                const idleAction = mob.actions['idle'];\n                if (idleAction) {\n                  idleAction.play();\n                  mob.currentAction = 'idle';\n                }\n              }\n\n              // Try to find head for looking around\n              villagerModel.traverse((child) => {\n                const name = child.name.toLowerCase();\n                if (name.includes('head')) {\n                  mob.head = child;\n                }\n              });\n            } else {\n              // Fallback to simple box-based villager if GLB not loaded\n              const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0x8b4513 }); // Brown robe\n              const skinMaterial = new THREE.MeshLambertMaterial({ color: 0xd4a574 }); // Skin tone\n\n              // Body (robe)\n              const body = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.7, 0.35), bodyMaterial);\n              body.position.set(0, 0, 0);\n              mob.group.add(body);\n\n              // Head\n              const head = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), skinMaterial);\n              head.position.set(0, 0.55, 0);\n              mob.group.add(head);\n              mob.head = head;\n\n              // Nose (villager signature)\n              const nose = new THREE.Mesh(\n                new THREE.BoxGeometry(0.12, 0.2, 0.15),\n                skinMaterial.clone()\n              );\n              nose.position.set(0, 0.5, 0.25);\n              mob.group.add(nose);\n\n              // Arms\n              const leftArm = new THREE.Mesh(\n                new THREE.BoxGeometry(0.2, 0.5, 0.2),\n                bodyMaterial.clone()\n              );\n              leftArm.position.set(-0.35, 0.1, 0);\n              mob.group.add(leftArm);\n\n              const rightArm = new THREE.Mesh(\n                new THREE.BoxGeometry(0.2, 0.5, 0.2),\n                bodyMaterial.clone()\n              );\n              rightArm.position.set(0.35, 0.1, 0);\n              mob.group.add(rightArm);\n\n              // Legs\n              const leftLeg = new THREE.Mesh(\n                new THREE.BoxGeometry(0.2, 0.5, 0.2),\n                bodyMaterial.clone()\n              );\n              leftLeg.position.set(-0.12, -0.6, 0);\n              mob.group.add(leftLeg);\n              mob.leftLeg = leftLeg;\n\n              const rightLeg = new THREE.Mesh(\n                new THREE.BoxGeometry(0.2, 0.5, 0.2),\n                bodyMaterial.clone()\n              );\n              rightLeg.position.set(0.12, -0.6, 0);\n              mob.group.add(rightLeg);\n              mob.rightLeg = rightLeg;\n            }\n\n            // Villager-specific properties\n            mob.isPassive = true;\n            mob.wanderTime = 0;\n            mob.wanderDirection = new THREE.Vector3();\n            mob.lookAroundTime = 0;\n            mob.stopAndLookChance = 0.15; // 15% chance to stop and look around\n          }\n\n          mob.group.position.copy(mob.position);\n          scene.add(mob.group);\n\n          return mob;\n        }\n\n        function lerpRemoteMobs(dt) {\n          const lerpSpeed = 3;\n          for (const [mobId, target] of mobTargetPositions) {\n            const mob = mobs.find((m) => m.id === mobId);\n            if (!mob || !mob.group) continue;\n\n            const prevPos = mob.position.clone();\n\n            mob.position.x += (target.x - mob.position.x) * lerpSpeed * dt;\n            mob.position.y += (target.y - mob.position.y) * lerpSpeed * dt;\n            mob.position.z += (target.z - mob.position.z) * lerpSpeed * dt;\n            mob.group.position.copy(mob.position);\n\n            if (target.yaw !== undefined) {\n              mob.group.rotation.y += (target.yaw - mob.group.rotation.y) * lerpSpeed * dt;\n            }\n\n            // Sync creeper animation/flash based on remote movement and state\n            const dx = mob.position.x - prevPos.x;\n            const dz = mob.position.z - prevPos.z;\n            const safeDt = Math.max(dt, 0.016);\n            mob.velocity.x = dx / safeDt;\n            mob.velocity.z = dz / safeDt;\n\n            if (target.exploding !== undefined) mob.exploding = target.exploding;\n            if (target.explodeProgress !== undefined) mob.explodeProgress = target.explodeProgress;\n\n            if (mob.type === MOB_TYPES.CREEPER) {\n              animateCreeper(mob, dt);\n\n              if (mob.exploding) {\n                const flash = Math.floor(mob.explodeProgress * 10) % 2;\n                const flashColor = new THREE.Color(flash ? 0xffffff : 0x000000);\n                mob.group.traverse((mesh) => {\n                  if (mesh.isMesh && mesh.material) {\n                    if (Array.isArray(mesh.material)) {\n                      mesh.material.forEach((m) => (m.emissive = flashColor));\n                    } else {\n                      mesh.material.emissive = flashColor;\n                    }\n                  }\n                });\n                mob.group.scale.set(\n                  1 + mob.explodeProgress * 0.3,\n                  1 + mob.explodeProgress * 0.3,\n                  1 + mob.explodeProgress * 0.3\n                );\n              } else {\n                mob.group.scale.set(1, 1, 1);\n              }\n            }\n          }\n        }\n\n        // Helper function to find the closest player to a position\n        function findClosestPlayer(pos) {\n          let closestPlayer = null;\n          let closestDistSq = Infinity;\n\n          // Check main player\n          const dx = pos.x - playerPos.x;\n          const dy = pos.y - playerPos.y;\n          const dz = pos.z - playerPos.z;\n          const mainDistSq = dx * dx + dy * dy + dz * dz;\n          if (mainDistSq < closestDistSq) {\n            closestDistSq = mainDistSq;\n            closestPlayer = { pos: playerPos, distSq: mainDistSq };\n          }\n\n          // Check other players\n          if (typeof otherPlayers !== 'undefined' && Array.isArray(otherPlayers)) {\n            for (const other of otherPlayers) {\n              const odx = pos.x - other.x;\n              const ody = pos.y - other.y;\n              const odz = pos.z - other.z;\n              const otherDistSq = odx * odx + ody * ody + odz * odz;\n              if (otherDistSq < closestDistSq) {\n                closestDistSq = otherDistSq;\n                closestPlayer = {\n                  pos: new THREE.Vector3(other.x, other.y, other.z),\n                  distSq: otherDistSq,\n                  yaw: other.yaw,\n                  pitch: other.pitch,\n                };\n              }\n            }\n          }\n\n          return closestPlayer;\n        }\n\n        function spawnMob() {\n          if (mobs.length >= MAX_MOBS) return;\n          if (isCreativeMode) return;\n          if (isDay) return;\n          // Only spawn mobs at night\n\n          // Check which mob types are enabled\n          const enabledTypes = [];\n          if (config.zombiesEnabled) enabledTypes.push(MOB_TYPES.ZOMBIE);\n          if (config.creepersEnabled) enabledTypes.push(MOB_TYPES.CREEPER);\n\n          if (enabledTypes.length === 0) return;\n          // No mobs enabled\n\n          // Find the closest player to spawn mobs around\n          const spawnPos = findClosestPlayer(playerPos);\n          if (!spawnPos) return;\n\n          // Random position around closest player (infinite world - no bounds check)\n          const angle = Math.random() * Math.PI * 2;\n          const distance = 15 + Math.random() * 20;\n          const x = spawnPos.pos.x + Math.cos(angle) * distance;\n          const z = spawnPos.pos.z + Math.sin(angle) * distance;\n\n          // Find ground\n          const wx = Math.floor(x);\n          const wz = Math.floor(z);\n\n          let groundY = -1;\n          for (let y = worldHeight - 1; y >= 0; y--) {\n            if (getBlockAt(wx, y, wz) !== BLOCK_TYPES.AIR) {\n              groundY = y;\n              break;\n            }\n          }\n\n          if (groundY < 0) return;\n\n          // Find first clear space above ground (need 2 blocks of air for mob to fit)\n          let spawnY = groundY + 1;\n          const mobHeight = 2;\n          // Mobs need 2 blocks of vertical space\n\n          // Check upward for clear space\n          for (let y = groundY + 1; y < worldHeight - mobHeight; y++) {\n            let isClear = true;\n            for (let checkY = y; checkY < y + mobHeight; checkY++) {\n              if (getBlockAt(wx, checkY, wz) !== BLOCK_TYPES.AIR) {\n                isClear = false;\n                break;\n              }\n            }\n            if (isClear) {\n              spawnY = y;\n              break;\n            }\n          }\n\n          // Random mob type from enabled types\n          const type = enabledTypes[Math.floor(Math.random() * enabledTypes.length)];\n          const mob = createMob(type, wx + 0.5, spawnY + 0.5, wz + 0.5);\n          mob.id = crypto.randomUUID();\n          mob.lastSyncTime = 0;\n          mobs.push(mob);\n          if (typeof syncMobSpawn === 'function') {\n            syncMobSpawn(mob.id, type, wx + 0.5, spawnY + 0.5, wz + 0.5);\n          }\n        }\n\n        function spawnMob2() {\n          // Only spawn mobs at night\n\n          // Check which mob types are enabled\n          const enabledTypes = [];\n          // if (config.zombiesEnabled) enabledTypes.push(MOB_TYPES.ZOMBIE);\n          if (config.creepersEnabled) enabledTypes.push(MOB_TYPES.CREEPER);\n\n          if (enabledTypes.length === 0) return;\n          // No mobs enabled\n\n          // Find the closest player to spawn mobs around\n          const spawnPos = findClosestPlayer(playerPos);\n          if (!spawnPos) return;\n\n          // Random position around closest player (infinite world - no bounds check)\n          const angle = Math.random() * Math.PI * 2;\n          const distance = 15 + Math.random() * 20;\n          const x = spawnPos.pos.x + Math.cos(angle) * distance;\n          const z = spawnPos.pos.z + Math.sin(angle) * distance;\n\n          // Find ground\n          const wx = Math.floor(x);\n          const wz = Math.floor(z);\n\n          let groundY = -1;\n          for (let y = worldHeight - 1; y >= 0; y--) {\n            if (getBlockAt(wx, y, wz) !== BLOCK_TYPES.AIR) {\n              groundY = y;\n              break;\n            }\n          }\n\n          if (groundY < 0) return;\n\n          // Find first clear space above ground (need 2 blocks of air for mob to fit)\n          let spawnY = groundY + 1;\n          const mobHeight = 2;\n          // Mobs need 2 blocks of vertical space\n\n          // Check upward for clear space\n          for (let y = groundY + 1; y < worldHeight - mobHeight; y++) {\n            let isClear = true;\n            for (let checkY = y; checkY < y + mobHeight; checkY++) {\n              if (getBlockAt(wx, checkY, wz) !== BLOCK_TYPES.AIR) {\n                isClear = false;\n                break;\n              }\n            }\n            if (isClear) {\n              spawnY = y;\n              break;\n            }\n          }\n\n          // Random mob type from enabled types\n          const type = enabledTypes[Math.floor(Math.random() * enabledTypes.length)];\n          const mob = createMob(type, wx + 0.5, spawnY + 0.5, wz + 0.5);\n          mob.id = crypto.randomUUID();\n          mob.lastSyncTime = 0;\n          mobs.push(mob);\n          if (typeof syncMobSpawn === 'function') {\n            syncMobSpawn(mob.id, type, wx + 0.5, spawnY + 0.5, wz + 0.5);\n          }\n        }\n\n        // Spawn passive mobs (chickens) during the day\n        let nextChickenSpawn = 0;\n        const CHICKEN_SPAWN_INTERVAL = 15;\n        const MAX_CHICKENS = 8;\n\n        // Spawn cows during the day\n        let nextCowSpawn = 0;\n        const COW_SPAWN_INTERVAL = 20;\n        const MAX_COWS = 6;\n\n        // Spawn dogs during the day\n        let nextDogSpawn = 0;\n        const DOG_SPAWN_INTERVAL = 18;\n        const MAX_DOGS = 6;\n\n        // Spawn villagers during the day\n        let nextVillagerSpawn = 0;\n        const VILLAGER_SPAWN_INTERVAL = 10;\n        const MAX_VILLAGERS = 10;\n\n        function spawnChicken() {\n          // Check if chickens are enabled\n          if (config.chickensEnabled === false) return;\n\n          // Count existing chickens\n          const chickenCount = mobs.filter((m) => m.type === MOB_TYPES.CHICKEN).length;\n          if (chickenCount >= MAX_CHICKENS) return;\n          if (!isDay) return; // Only spawn chickens during the day\n\n          // Find the closest player to spawn chickens around\n          const spawnPos = findClosestPlayer(playerPos);\n          if (!spawnPos) return;\n\n          // Random position around closest player\n          const angle = Math.random() * Math.PI * 2;\n          const distance = 10 + Math.random() * 15;\n          const x = spawnPos.pos.x + Math.cos(angle) * distance;\n          const z = spawnPos.pos.z + Math.sin(angle) * distance;\n\n          // Find ground\n          const wx = Math.floor(x);\n          const wz = Math.floor(z);\n\n          let groundY = -1;\n          for (let y = worldHeight - 1; y >= 0; y--) {\n            const blockType = getBlockAt(wx, y, wz);\n            if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {\n              groundY = y;\n              break;\n            }\n          }\n\n          if (groundY < 0) return;\n\n          // Don't spawn in water\n          const groundBlock = getBlockAt(wx, groundY, wz);\n          if (groundBlock === BLOCK_TYPES.WATER) return;\n\n          // Find clear space (chickens only need 1 block)\n          let spawnY = groundY + 1;\n          if (getBlockAt(wx, spawnY, wz) !== BLOCK_TYPES.AIR) return;\n\n          const mob = createMob(MOB_TYPES.CHICKEN, wx + 0.5, spawnY + 0.3, wz + 0.5);\n          mob.id = crypto.randomUUID();\n          mob.lastSyncTime = 0;\n          mobs.push(mob);\n          if (typeof syncMobSpawn === 'function') {\n            syncMobSpawn(mob.id, MOB_TYPES.CHICKEN, wx + 0.5, spawnY + 0.3, wz + 0.5);\n          }\n        }\n\n        function spawnCow() {\n          // Check if cows are enabled\n          if (config.cowsEnabled === false) return;\n\n          // Count existing cows\n          const cowCount = mobs.filter((m) => m.type === MOB_TYPES.COW).length;\n          if (cowCount >= MAX_COWS) return;\n          if (!isDay) return; // Only spawn cows during the day\n\n          // Find the closest player to spawn cows around\n          const spawnPos = findClosestPlayer(playerPos);\n          if (!spawnPos) return;\n\n          // Random position around closest player\n          const angle = Math.random() * Math.PI * 2;\n          const distance = 12 + Math.random() * 18;\n          const x = spawnPos.pos.x + Math.cos(angle) * distance;\n          const z = spawnPos.pos.z + Math.sin(angle) * distance;\n\n          // Find ground\n          const wx = Math.floor(x);\n          const wz = Math.floor(z);\n\n          let groundY = -1;\n          for (let y = worldHeight - 1; y >= 0; y--) {\n            const blockType = getBlockAt(wx, y, wz);\n            if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {\n              groundY = y;\n              break;\n            }\n          }\n\n          if (groundY < 0) return;\n\n          // Don't spawn in water\n          const groundBlock = getBlockAt(wx, groundY, wz);\n          if (groundBlock === BLOCK_TYPES.WATER) return;\n\n          // Find clear space (cows need 2 blocks of vertical space)\n          let spawnY = groundY + 1;\n          if (getBlockAt(wx, spawnY, wz) !== BLOCK_TYPES.AIR) return;\n          if (getBlockAt(wx, spawnY + 1, wz) !== BLOCK_TYPES.AIR) return;\n\n          const mob = createMob(MOB_TYPES.COW, wx + 0.5, spawnY + 0.5, wz + 0.5);\n          mob.id = crypto.randomUUID();\n          mob.lastSyncTime = 0;\n          mobs.push(mob);\n          if (typeof syncMobSpawn === 'function') {\n            syncMobSpawn(mob.id, MOB_TYPES.COW, wx + 0.5, spawnY + 0.5, wz + 0.5);\n          }\n        }\n\n        function spawnDog() {\n          // Check if dogs are enabled\n          if (config.dogsEnabled === false) return;\n\n          // Count existing dogs\n          const dogCount = mobs.filter((m) => m.type === MOB_TYPES.DOG).length;\n          if (dogCount >= MAX_DOGS) return;\n          if (!isDay) return; // Only spawn dogs during the day\n\n          // Find the closest player to spawn dogs around\n          const spawnPos = findClosestPlayer(playerPos);\n          if (!spawnPos) return;\n\n          // Random position around closest player\n          const angle = Math.random() * Math.PI * 2;\n          const distance = 10 + Math.random() * 16;\n          const x = spawnPos.pos.x + Math.cos(angle) * distance;\n          const z = spawnPos.pos.z + Math.sin(angle) * distance;\n\n          // Find ground\n          const wx = Math.floor(x);\n          const wz = Math.floor(z);\n\n          let groundY = -1;\n          for (let y = worldHeight - 1; y >= 0; y--) {\n            const blockType = getBlockAt(wx, y, wz);\n            if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {\n              groundY = y;\n              break;\n            }\n          }\n\n          if (groundY < 0) return;\n\n          // Don't spawn in water\n          const groundBlock = getBlockAt(wx, groundY, wz);\n          if (groundBlock === BLOCK_TYPES.WATER) return;\n\n          // Find clear space (dogs need 2 blocks of vertical space)\n          let spawnY = groundY + 1;\n          if (getBlockAt(wx, spawnY, wz) !== BLOCK_TYPES.AIR) return;\n          if (getBlockAt(wx, spawnY + 1, wz) !== BLOCK_TYPES.AIR) return;\n\n          const mob = createMob(MOB_TYPES.DOG, wx + 0.5, spawnY + 0.4, wz + 0.5);\n          mob.id = crypto.randomUUID();\n          mob.lastSyncTime = 0;\n          mobs.push(mob);\n          if (typeof syncMobSpawn === 'function') {\n            syncMobSpawn(mob.id, MOB_TYPES.DOG, wx + 0.5, spawnY + 0.4, wz + 0.5);\n          }\n        }\n\n        function spawnVillager() {\n          // Count existing villagers\n          const villagerCount = mobs.filter((m) => m.type === MOB_TYPES.VILLAGER).length;\n          if (villagerCount >= 10) return;\n          if (!isDay) return; // Only spawn villagers during the day\n\n          // Find the closest player to spawn villagers around\n          const spawnPos = findClosestPlayer(playerPos);\n          if (!spawnPos) return;\n\n          // Check if there is a village in the player's region\n          const rx = Math.floor(spawnPos.pos.x / 128);\n          const rz = Math.floor(spawnPos.pos.z / 128);\n          let foundVillage = false;\n          let vx = spawnPos.pos.x;\n          let vz = spawnPos.pos.z;\n\n          for (let drx = -1; drx <= 1; drx++) {\n            for (let drz = -1; drz <= 1; drz++) {\n              const vData = getVillageData(rx + drx, rz + drz);\n              if (vData) {\n                foundVillage = true;\n                // Spawn near the village center\n                const angle = Math.random() * Math.PI * 2;\n                const distance = Math.random() * 25;\n                vx = vData.cx + Math.cos(angle) * distance;\n                vz = vData.cz + Math.sin(angle) * distance;\n                break;\n              }\n            }\n            if (foundVillage) break;\n          }\n\n          // Only spawn villagers in villages\n          if (!foundVillage) return;\n\n          // Find ground\n          const wx = Math.floor(vx);\n          const wz = Math.floor(vz);\n\n          let groundY = -1;\n          for (let y = worldHeight - 1; y >= 0; y--) {\n            const blockType = getBlockAt(wx, y, wz);\n            if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {\n              groundY = y;\n              break;\n            }\n          }\n\n          if (groundY < 0) return;\n\n          // Don't spawn in water\n          const groundBlock = getBlockAt(wx, groundY, wz);\n          if (groundBlock === BLOCK_TYPES.WATER) return;\n\n          // Find clear space (villagers need 2 blocks of vertical space)\n          let spawnY = groundY + 1;\n          if (getBlockAt(wx, spawnY, wz) !== BLOCK_TYPES.AIR) return;\n          if (getBlockAt(wx, spawnY + 1, wz) !== BLOCK_TYPES.AIR) return;\n\n          const mob = createMob(MOB_TYPES.VILLAGER, wx + 0.5, spawnY + 0.5, wz + 0.5);\n          mob.id = crypto.randomUUID();\n          mob.lastSyncTime = 0;\n          mobs.push(mob);\n          if (typeof syncMobSpawn === 'function') {\n            syncMobSpawn(mob.id, MOB_TYPES.VILLAGER, wx + 0.5, spawnY + 0.5, wz + 0.5);\n          }\n        }\n\n        function checkMobCollision(pos, mob) {\n          // Check collision with blocks around mob (infinite world - only check Y bounds)\n          const halfWidth = mob.width / 2;\n          const checkPoints = [\n            [pos.x - halfWidth, pos.y, pos.z - halfWidth],\n            [pos.x + halfWidth, pos.y, pos.z - halfWidth],\n            [pos.x - halfWidth, pos.y, pos.z + halfWidth],\n            [pos.x + halfWidth, pos.y, pos.z + halfWidth],\n            [pos.x - halfWidth, pos.y - mob.size * 0.5, pos.z - halfWidth],\n            [pos.x + halfWidth, pos.y - mob.size * 0.5, pos.z - halfWidth],\n            [pos.x - halfWidth, pos.y - mob.size * 0.5, pos.z + halfWidth],\n            [pos.x + halfWidth, pos.y - mob.size * 0.5, pos.z + halfWidth],\n          ];\n\n          for (const [px, py, pz] of checkPoints) {\n            const bx = Math.floor(px);\n            const by = Math.floor(py);\n            const bz = Math.floor(pz);\n\n            if (by >= 0 && by < worldHeight) {\n              const blockType = getBlockAt(bx, by, bz);\n              // Torch blocks, water, lava, and open doors are walkable - skip collision\n              if (blockType === BLOCK_TYPES.DOOR) {\n                const doorKey = getDoorBaseKey(bx, by, bz);\n                if (doorStates.get(doorKey)) {\n                  continue; // Door is open, no collision\n                }\n              }\n              if (\n                blockType !== BLOCK_TYPES.AIR &&\n                blockType !== BLOCK_TYPES.TORCH &&\n                blockType !== BLOCK_TYPES.WATER &&\n                blockType !== BLOCK_TYPES.WATER_FLOW &&\n                blockType !== BLOCK_TYPES.LAVA &&\n                blockType !== BLOCK_TYPES.LAVA_FLOW &&\n                blockType !== BLOCK_TYPES.FLOWER\n              ) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }\n\n        // Check if mob is in water\n        function checkMobInWater(pos, mob) {\n          const halfWidth = mob.width / 2;\n          const checkPoints = [\n            [pos.x, pos.y, pos.z], // Center\n            [pos.x, pos.y - mob.size * 0.25, pos.z], // Lower body\n            [pos.x, pos.y - mob.size * 0.5, pos.z], // Feet\n          ];\n\n          for (const [px, py, pz] of checkPoints) {\n            const bx = Math.floor(px);\n            const by = Math.floor(py);\n            const bz = Math.floor(pz);\n\n            if (by >= 0 && by < worldHeight) {\n              const blockType = getBlockAt(bx, by, bz);\n              if (blockType === BLOCK_TYPES.WATER || blockType === BLOCK_TYPES.WATER_FLOW) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }\n\n        // Check if mob is in lava\n        function checkMobInLava(pos, mob) {\n          const checkPoints = [\n            [pos.x, pos.y, pos.z], // Center\n            [pos.x, pos.y - mob.size * 0.25, pos.z], // Lower body\n            [pos.x, pos.y - mob.size * 0.5, pos.z], // Feet\n          ];\n\n          for (const [px, py, pz] of checkPoints) {\n            const bx = Math.floor(px);\n            const by = Math.floor(py);\n            const bz = Math.floor(pz);\n\n            if (by >= 0 && by < worldHeight) {\n              const blockType = getBlockAt(bx, by, bz);\n              if (blockType === BLOCK_TYPES.LAVA || blockType === BLOCK_TYPES.LAVA_FLOW) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }\n\n        function animateZombie(mob, dt) {\n          if (mob.type !== MOB_TYPES.ZOMBIE) return;\n\n          // Calculate movement speed for animation\n          const movementSpeed = Math.sqrt(mob.velocity.x ** 2 + mob.velocity.z ** 2);\n          const isMoving = movementSpeed > 0.3;\n\n          // Use GLB animations if available\n          if (mob.mixer && mob.actions) {\n            // Update the animation mixer\n            mob.mixer.update(dt);\n\n            // Switch between idle and walk animations based on movement\n            if (isMoving && mob.currentAction !== 'walk') {\n              // Switch to walk animation\n              const walkAction = mob.actions['walk'];\n              const idleAction = mob.actions['idle'];\n              if (walkAction) {\n                if (idleAction && idleAction.isRunning()) {\n                  idleAction.fadeOut(0.2);\n                }\n                walkAction.reset().fadeIn(0.2).play();\n                mob.currentAction = 'walk';\n              }\n            } else if (!isMoving && mob.currentAction !== 'idle') {\n              // Switch to idle animation\n              const walkAction = mob.actions['walk'];\n              const idleAction = mob.actions['idle'];\n              if (idleAction) {\n                if (walkAction && walkAction.isRunning()) {\n                  walkAction.fadeOut(0.2);\n                }\n                idleAction.reset().fadeIn(0.2).play();\n                mob.currentAction = 'idle';\n              }\n            }\n\n            // Update head pitch and yaw to look at closest player (after mixer.update so animation doesn't overwrite)\n            if (mob.head) {\n              const closest = findClosestPlayer(mob.position);\n              if (closest && closest.pos) {\n                // Calculate direction to player\n                const dx = closest.pos.x - mob.position.x;\n                const dy = closest.pos.y - mob.position.y;\n                const dz = closest.pos.z - mob.position.z;\n                const horizontalDist = Math.sqrt(dx * dx + dz * dz);\n\n                // Calculate target yaw (relative to body rotation)\n                const targetYaw = Math.atan2(dx, dz) - mob.group.rotation.y;\n                // Calculate target pitch (looking up/down at player)\n                const targetPitch = -Math.atan2(dy, horizontalDist);\n\n                // Clamp pitch to reasonable range\n                const clampedPitch = Math.max(-Math.PI / 4, Math.min(Math.PI / 4, targetPitch));\n                // Clamp yaw so head doesn't spin around completely\n                const clampedYaw = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, targetYaw));\n\n                // Smoothly interpolate head rotation\n                const lerpFactor = 0.1;\n                mob.head.rotation.x += (clampedPitch - mob.head.rotation.x) * lerpFactor;\n                mob.head.rotation.y += (clampedYaw - mob.head.rotation.y) * lerpFactor;\n              }\n            }\n          } else {\n            // Fallback manual animation for non-GLB zombies\n            const animationSpeed = Math.max(0.5, movementSpeed * 0.8);\n            mob.animationTime += dt * animationSpeed;\n\n            // Arm animation - keep arms stiff and extended forward\n            const armForwardAngle = -Math.PI / 2;\n\n            if (mob.leftArm) {\n              mob.leftArm.rotation.x = armForwardAngle;\n            }\n            if (mob.rightArm) {\n              mob.rightArm.rotation.x = armForwardAngle;\n            }\n\n            // Leg animation - slow oscillation for walking\n            const legSwingAmount = Math.PI / 8;\n            const legPhase = Math.sin(mob.animationTime * Math.PI) * legSwingAmount;\n\n            if (mob.leftLeg) {\n              mob.leftLeg.rotation.x = legPhase;\n            }\n            if (mob.rightLeg) {\n              mob.rightLeg.rotation.x = -legPhase;\n            }\n          }\n        }\n\n        function animateChicken(mob, dt) {\n          if (mob.type !== MOB_TYPES.CHICKEN) return;\n\n          const movementSpeed = Math.sqrt(mob.velocity.x ** 2 + mob.velocity.z ** 2);\n          const animationSpeed = Math.max(1, movementSpeed * 2);\n          mob.animationTime += dt * animationSpeed;\n\n          // Leg animation - fast walking/running\n          const legSwingAmount = Math.PI / 4;\n          const legPhase = Math.sin(mob.animationTime * Math.PI * 2) * legSwingAmount;\n\n          if (mob.leftLeg) {\n            mob.leftLeg.rotation.x = legPhase;\n          }\n          if (mob.rightLeg) {\n            mob.rightLeg.rotation.x = -legPhase;\n          }\n\n          // Wing flap when running fast\n          if (movementSpeed > 2 && mob.leftWing && mob.rightWing) {\n            const flapAmount = Math.PI / 6;\n            const flapPhase = Math.sin(mob.animationTime * Math.PI * 4) * flapAmount;\n            mob.leftWing.rotation.z = flapPhase;\n            mob.rightWing.rotation.z = -flapPhase;\n          } else if (mob.leftWing && mob.rightWing) {\n            // Reset wings when walking slowly\n            mob.leftWing.rotation.z *= 0.9;\n            mob.rightWing.rotation.z *= 0.9;\n          }\n\n          // Head bobbing\n          if (mob.head) {\n            const bobAmount = 0.05;\n            const bobPhase = Math.sin(mob.animationTime * Math.PI * 3) * bobAmount;\n            mob.head.position.y = 0.2 + bobPhase;\n          }\n        }\n\n        function animateCreeper(mob, dt) {\n          if (mob.type !== MOB_TYPES.CREEPER) return;\n\n          const movementSpeed = Math.sqrt(mob.velocity.x ** 2 + mob.velocity.z ** 2);\n          if (movementSpeed < 0.15) {\n            if (mob.frontLeftLeg) mob.frontLeftLeg.rotation.x *= 0.8;\n            if (mob.frontRightLeg) mob.frontRightLeg.rotation.x *= 0.8;\n            if (mob.backLeftLeg) mob.backLeftLeg.rotation.x *= 0.8;\n            if (mob.backRightLeg) mob.backRightLeg.rotation.x *= 0.8;\n            return;\n          }\n\n          const animationSpeed = Math.max(0.6, movementSpeed * 1.2);\n          mob.animationTime = (mob.animationTime || 0) + dt * animationSpeed;\n\n          const legSwingAmount = Math.PI / 8;\n          const legPhase = Math.sin(mob.animationTime * Math.PI * 2) * legSwingAmount;\n\n          if (mob.frontLeftLeg) mob.frontLeftLeg.rotation.x = legPhase;\n          if (mob.frontRightLeg) mob.frontRightLeg.rotation.x = -legPhase;\n          if (mob.backLeftLeg) mob.backLeftLeg.rotation.x = -legPhase;\n          if (mob.backRightLeg) mob.backRightLeg.rotation.x = legPhase;\n        }\n\n        function animateCow(mob, dt) {\n          if (mob.type !== MOB_TYPES.COW) return;\n\n          const movementSpeed = Math.sqrt(mob.velocity.x ** 2 + mob.velocity.z ** 2);\n          const animationSpeed = Math.max(0.5, movementSpeed * 1.5);\n          mob.animationTime += dt * animationSpeed;\n\n          // Leg animation - slower, lumbering walk\n          const legSwingAmount = Math.PI / 6;\n          const legPhase = Math.sin(mob.animationTime * Math.PI * 1.5) * legSwingAmount;\n\n          // Front legs move opposite to back legs\n          if (mob.frontLeftLeg) {\n            mob.frontLeftLeg.rotation.x = legPhase;\n          }\n          if (mob.frontRightLeg) {\n            mob.frontRightLeg.rotation.x = -legPhase;\n          }\n          if (mob.backLeftLeg) {\n            mob.backLeftLeg.rotation.x = -legPhase;\n          }\n          if (mob.backRightLeg) {\n            mob.backRightLeg.rotation.x = legPhase;\n          }\n\n          // Head bobbing - subtle\n          if (mob.head) {\n            const bobAmount = 0.03;\n            const bobPhase = Math.sin(mob.animationTime * Math.PI * 2) * bobAmount;\n            mob.head.position.y = 0.25 + bobPhase;\n          }\n        }\n\n        function animateDog(mob, dt) {\n          if (mob.type !== MOB_TYPES.DOG) return;\n\n          const movementSpeed = Math.sqrt(mob.velocity.x ** 2 + mob.velocity.z ** 2);\n          const isIdle = movementSpeed < 0.15;\n          const animationSpeed = Math.max(0.6, movementSpeed * 2);\n          mob.animationTime += dt * animationSpeed;\n\n          if (isIdle) {\n            // Sitting pose\n            if (mob.body) mob.body.position.y = 0.0;\n            if (mob.chest) mob.chest.position.y = 0.02;\n            if (mob.head) mob.head.position.y = 0.16;\n            if (mob.tail) mob.tail.rotation.x = 0.4;\n\n            // Fold legs slightly forward\n            const sitAngleFront = -Math.PI / 6;\n            const sitAngleBack = Math.PI / 2.8;\n            if (mob.frontLeftLeg) mob.frontLeftLeg.rotation.x = sitAngleFront;\n            if (mob.frontRightLeg) mob.frontRightLeg.rotation.x = sitAngleFront;\n            if (mob.backLeftLeg) mob.backLeftLeg.rotation.x = sitAngleBack;\n            if (mob.backRightLeg) mob.backRightLeg.rotation.x = sitAngleBack;\n\n            // Gentle idle tail wag\n            if (mob.tail) {\n              const wagAmount = 0.2;\n              const wagPhase = Math.sin(mob.animationTime * Math.PI * 2) * wagAmount;\n              mob.tail.rotation.y = wagPhase;\n            }\n          } else {\n            // Standing pose\n            if (mob.standPose) {\n              if (mob.body) mob.body.position.y = mob.standPose.bodyY;\n              if (mob.chest) mob.chest.position.y = mob.standPose.chestY;\n              if (mob.head) mob.head.position.y = mob.standPose.headY;\n              if (mob.tail) mob.tail.rotation.x = mob.standPose.tailRotX;\n            }\n\n            // Leg animation - quick trot\n            const legSwingAmount = Math.PI / 5;\n            const legPhase = Math.sin(mob.animationTime * Math.PI * 2) * legSwingAmount;\n\n            if (mob.frontLeftLeg) {\n              mob.frontLeftLeg.rotation.x = legPhase;\n            }\n            if (mob.frontRightLeg) {\n              mob.frontRightLeg.rotation.x = -legPhase;\n            }\n            if (mob.backLeftLeg) {\n              mob.backLeftLeg.rotation.x = -legPhase;\n            }\n            if (mob.backRightLeg) {\n              mob.backRightLeg.rotation.x = legPhase;\n            }\n\n            // Head bobbing\n            if (mob.head) {\n              const bobAmount = 0.025;\n              const bobPhase = Math.sin(mob.animationTime * Math.PI * 2.4) * bobAmount;\n              mob.head.position.y = 0.2 + bobPhase;\n            }\n\n            // Tail wag\n            if (mob.tail) {\n              const wagAmount = 0.5;\n              const wagPhase = Math.sin(mob.animationTime * Math.PI * 3) * wagAmount;\n              mob.tail.rotation.y = wagPhase;\n            }\n          }\n        }\n\n        function animateVillager(mob, dt) {\n          if (mob.type !== MOB_TYPES.VILLAGER) return;\n\n          // Calculate movement speed for animation\n          const movementSpeed = Math.sqrt(mob.velocity.x ** 2 + mob.velocity.z ** 2);\n          const isMoving = movementSpeed > 0.3;\n\n          // Use GLB animations if available\n          if (mob.mixer && mob.actions) {\n            // Update the animation mixer\n            mob.mixer.update(dt);\n\n            // Switch between idle and walk animations based on movement\n            if (isMoving && mob.currentAction !== 'walk') {\n              // Switch to walk animation\n              const walkAction = mob.actions['walk'];\n              const idleAction = mob.actions['idle'];\n              if (walkAction) {\n                if (idleAction && idleAction.isRunning()) {\n                  idleAction.fadeOut(0.2);\n                }\n                walkAction.reset().fadeIn(0.2).play();\n                mob.currentAction = 'walk';\n              }\n            } else if (!isMoving && mob.currentAction !== 'idle') {\n              // Switch to idle animation\n              const walkAction = mob.actions['walk'];\n              const idleAction = mob.actions['idle'];\n              if (idleAction) {\n                if (walkAction && walkAction.isRunning()) {\n                  walkAction.fadeOut(0.2);\n                }\n                idleAction.reset().fadeIn(0.2).play();\n                mob.currentAction = 'idle';\n              }\n            }\n\n            // Update head pitch and yaw to look at closest player (after mixer.update so animation doesn't overwrite)\n            if (mob.head) {\n              const closest = findClosestPlayer(mob.position);\n              const VILLAGER_LOOK_RANGE = 5; // Villagers notice players within 10 blocks\n\n              if (closest && closest.pos && Math.sqrt(closest.distSq) <= VILLAGER_LOOK_RANGE) {\n                // if (closest && closest.pos) {\n                // Calculate direction to player\n                const dx = closest.pos.x - mob.position.x;\n                const dy = closest.pos.y - mob.position.y;\n                const dz = closest.pos.z - mob.position.z;\n                const horizontalDist = Math.sqrt(dx * dx + dz * dz);\n\n                // Calculate target yaw (relative to body rotation)\n                const targetYaw = Math.atan2(dx, dz) - mob.group.rotation.y;\n                // Calculate target pitch (looking up/down at player)\n                const targetPitch = -Math.atan2(dy, horizontalDist);\n\n                // Clamp pitch to reasonable range\n                const clampedPitch = Math.max(-Math.PI / 4, Math.min(Math.PI / 4, targetPitch));\n                // Clamp yaw so head doesn't spin around completely\n                const clampedYaw = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, targetYaw));\n\n                // Smoothly interpolate head rotation\n                const lerpFactor = 0.1;\n                mob.head.rotation.x += (clampedPitch - mob.head.rotation.x) * lerpFactor;\n                mob.head.rotation.y += (clampedYaw - mob.head.rotation.y) * lerpFactor;\n              } else {\n                // No player nearby and not moving - look around randomly\n                mob.lookAroundTime = (mob.lookAroundTime || 0) + dt;\n                const lookAmount = Math.sin(mob.lookAroundTime * 0.5) * 0.3;\n                mob.head.rotation.y += (lookAmount - mob.head.rotation.y) * 0.1;\n                mob.head.rotation.x += (0 - mob.head.rotation.x) * 0.1; // Reset pitch when looking around\n              }\n            }\n          }\n        }\n\n        function updateMobs(dt, currentTime) {\n          // Spawn mobs in offline mode\n          if (currentTime > nextMobSpawn) {\n            if (roomManager && roomManager.isInRoom()) {\n              if (lobby && IAmFirstJoinerHost(lobby)) {\n                spawnMob();\n              }\n            } else {\n              spawnMob();\n            }\n            nextMobSpawn = currentTime + MOB_SPAWN_INTERVAL;\n          }\n\n          // Spawn chickens during day\n          if (!isCreativeMode && currentTime > nextChickenSpawn) {\n            if (roomManager && roomManager.isInRoom()) {\n              if (lobby && IAmFirstJoinerHost(lobby)) {\n                spawnChicken();\n              }\n            } else {\n              spawnChicken();\n            }\n            nextChickenSpawn = currentTime + CHICKEN_SPAWN_INTERVAL;\n          }\n\n          // Spawn cows during day\n          if (!isCreativeMode && currentTime > nextCowSpawn) {\n            if (roomManager && roomManager.isInRoom()) {\n              if (lobby && IAmFirstJoinerHost(lobby)) {\n                spawnCow();\n              }\n            } else {\n              spawnCow();\n            }\n            nextCowSpawn = currentTime + COW_SPAWN_INTERVAL;\n          }\n\n          // Spawn dogs during day\n          if (!isCreativeMode && currentTime > nextDogSpawn) {\n            if (roomManager && roomManager.isInRoom()) {\n              if (lobby && IAmFirstJoinerHost(lobby)) {\n                spawnDog();\n              }\n            } else {\n              spawnDog();\n            }\n            nextDogSpawn = currentTime + DOG_SPAWN_INTERVAL;\n          }\n\n          // Spawn villagers during day\n          if (!isCreativeMode && currentTime > nextVillagerSpawn) {\n            if (roomManager && roomManager.isInRoom()) {\n              if (lobby && IAmFirstJoinerHost(lobby)) {\n                spawnVillager();\n              }\n            } else {\n              spawnVillager();\n            }\n            nextVillagerSpawn = currentTime + VILLAGER_SPAWN_INTERVAL;\n          }\n\n          // Limit visible mobs to 10 closest (only recalculate periodically)\n          const MAX_VISIBLE_MOBS = 10;\n          if (mobs.length > MAX_VISIBLE_MOBS) {\n            const mobDistances = mobs.map((mob, index) => {\n              const closest = findClosestPlayer(mob.position);\n              const player = closest ? closest.pos : playerPos;\n              const dx = mob.position.x - player.x;\n              const dy = mob.position.y - player.y;\n              const dz = mob.position.z - player.z;\n              return { index, distSq: dx * dx + dy * dy + dz * dz };\n            });\n\n            // Sort by distance and get indices of closest mobs\n            mobDistances.sort((a, b) => a.distSq - b.distSq);\n            const visibleMobIndices = new Set();\n            for (let i = 0; i < Math.min(MAX_VISIBLE_MOBS, mobDistances.length); i++) {\n              visibleMobIndices.add(mobDistances[i].index);\n            }\n\n            // Update mob visibility\n            for (let i = 0; i < mobs.length; i++) {\n              if (mobs[i].group) {\n                mobs[i].group.visible = visibleMobIndices.has(i);\n              }\n            }\n          }\n\n          for (let i = mobs.length - 1; i >= 0; i--) {\n            const mob = mobs[i];\n\n            // Sun damage during daytime\n            if (isDay && mob.type === MOB_TYPES.ZOMBIE) {\n              const SUN_DAMAGE_PER_SECOND = 2;\n              damageMob(mob, SUN_DAMAGE_PER_SECOND * dt);\n\n              // Create burning particles\n              if (Math.random() < 0.3) {\n                const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);\n                const material = new THREE.MeshBasicMaterial({\n                  color: new THREE.Color(1, Math.random() * 0.5, 0),\n                });\n                const particle = new THREE.Mesh(geometry, material);\n                particle.position.copy(mob.position);\n                particle.position.y += mob.size * 0.5;\n                particle.userData.velocity = new THREE.Vector3(\n                  (Math.random() - 0.5) * 0.5,\n                  Math.random() * 2 + 1,\n                  (Math.random() - 0.5) * 0.5\n                );\n                particle.userData.life = 0.5;\n                scene.add(particle);\n                particles.push(particle);\n              }\n            }\n\n            // Creeper explosion logic\n            if (mob.type === MOB_TYPES.CREEPER && !mob.exploding) {\n              const closest = findClosestPlayer(mob.position);\n              const player = closest ? closest.pos : playerPos;\n              const distToPlayer = mob.position.distanceTo(player);\n              if (distToPlayer < mob.EXPLODE_DISTANCE) {\n                mob.exploding = true;\n                mob.explodeProgress = 0;\n                playSound('creeper_hiss_sound', false, 0.6);\n              }\n            }\n\n            if (mob.exploding) {\n              mob.explodeProgress += dt;\n\n              // Flash effect\n              const flash = Math.floor(mob.explodeProgress * 10) % 2;\n              const flashColor = new THREE.Color(flash ? 0xffffff : 0x000000);\n              mob.group.traverse((mesh) => {\n                if (mesh.isMesh && mesh.material) {\n                  if (Array.isArray(mesh.material)) {\n                    mesh.material.forEach((m) => (m.emissive = flashColor));\n                  } else {\n                    mesh.material.emissive = flashColor;\n                  }\n                }\n              });\n              mob.group.scale.set(\n                1 + mob.explodeProgress * 0.3,\n                1 + mob.explodeProgress * 0.3,\n                1 + mob.explodeProgress * 0.3\n              );\n\n              if (mob.explodeProgress >= mob.EXPLODE_TIME) {\n                // Explode!\n                explodeCreeper(mob);\n                scene.remove(mob.group);\n                mobs.splice(i, 1);\n                continue;\n              }\n            }\n\n            // AI - move toward player or villager (zombies can eat villagers)\n            if (!mob.exploding && !mob.isPassive) {\n              let targetPos = null;\n              let targetIsVillager = false;\n              let targetVillager = null;\n\n              // Find closest player\n              const closest = findClosestPlayer(mob.position);\n              const playerDist = closest ? Math.sqrt(closest.distSq) : Infinity;\n\n              // For zombies, also check for nearby villagers to eat\n              if (mob.type === MOB_TYPES.ZOMBIE) {\n                let closestVillagerDist = Infinity;\n                for (const otherMob of mobs) {\n                  if (otherMob.type === MOB_TYPES.VILLAGER && otherMob.health > 0) {\n                    const vdx = otherMob.position.x - mob.position.x;\n                    const vdy = otherMob.position.y - mob.position.y;\n                    const vdz = otherMob.position.z - mob.position.z;\n                    const vDist = Math.sqrt(vdx * vdx + vdy * vdy + vdz * vdz);\n\n                    // Zombies prefer villagers within 15 blocks\n                    if (vDist < closestVillagerDist && vDist < 15) {\n                      closestVillagerDist = vDist;\n                      targetVillager = otherMob;\n                    }\n                  }\n                }\n\n                // Target villager if closer than player or player is far away\n                if (targetVillager && (closestVillagerDist < playerDist || playerDist > 20)) {\n                  targetPos = targetVillager.position;\n                  targetIsVillager = true;\n                }\n              }\n\n              // Default to player if no villager target\n              if (!targetPos) {\n                targetPos = closest ? closest.pos : playerPos;\n              }\n\n              const direction = new THREE.Vector3().subVectors(targetPos, mob.position);\n              direction.y = 0;\n\n              if (direction.length() > 0) {\n                direction.normalize();\n                const speed = mob.type === MOB_TYPES.ZOMBIE ? 2 : 1.5;\n                mob.velocity.x = direction.x * speed;\n                mob.velocity.z = direction.z * speed;\n\n                // Prevent zombies from overlapping by applying separation force\n                if (mob.type === MOB_TYPES.ZOMBIE) {\n                  const separation = new THREE.Vector3(0, 0, 0);\n                  const minDist = 1.1;\n                  for (const other of mobs) {\n                    if (other === mob || other.type !== MOB_TYPES.ZOMBIE) continue;\n                    const dx = mob.position.x - other.position.x;\n                    const dz = mob.position.z - other.position.z;\n                    const distSq = dx * dx + dz * dz;\n                    if (distSq > 0.0001 && distSq < minDist * minDist) {\n                      const dist = Math.sqrt(distSq);\n                      const strength = (minDist - dist) / minDist;\n                      separation.x += (dx / dist) * strength;\n                      separation.z += (dz / dist) * strength;\n                    }\n                  }\n                  if (separation.lengthSq() > 0) {\n                    separation.normalize().multiplyScalar(1.2);\n                    mob.velocity.x += separation.x;\n                    mob.velocity.z += separation.z;\n                  }\n                }\n\n                // Rotate to face target\n                const angle = Math.atan2(direction.x, direction.z);\n                mob.group.rotation.y = angle;\n              }\n\n              // Zombie attacks villager when close enough\n              if (mob.type === MOB_TYPES.ZOMBIE && targetIsVillager && targetVillager) {\n                const distToVillager = mob.position.distanceTo(targetVillager.position);\n                if (distToVillager < 2) {\n                  // Attack villager - use damage cooldown to prevent spam\n                  if (\n                    !mob.lastVillagerAttackTime ||\n                    currentTime - mob.lastVillagerAttackTime >= 1.0\n                  ) {\n                    mob.lastVillagerAttackTime = currentTime;\n                    damageMob(targetVillager, 4); // 4 damage per attack (2 hearts)\n\n                    // Play zombie groan when attacking\n                    playSoundAtPosition(\n                      'zombie_groan_sound',\n                      mob.position.x,\n                      mob.position.y,\n                      mob.position.z,\n                      0.6,\n                      15\n                    );\n                  }\n                }\n              }\n            }\n\n            // Chicken AI - wander and flee\n            if (mob.type === MOB_TYPES.CHICKEN) {\n              const closest = findClosestPlayer(mob.position);\n              const player = closest ? closest.pos : playerPos;\n              const distToPlayer = mob.position.distanceTo(player);\n\n              // Flee from nearby players\n              if (distToPlayer < (mob.fleeDistance || 5)) {\n                const fleeDir = new THREE.Vector3().subVectors(mob.position, player);\n                fleeDir.y = 0;\n                if (fleeDir.length() > 0) {\n                  fleeDir.normalize();\n                  const fleeSpeed = 3.5; // Chickens run fast when scared!\n                  mob.velocity.x = fleeDir.x * fleeSpeed;\n                  mob.velocity.z = fleeDir.z * fleeSpeed;\n                  const angle = Math.atan2(fleeDir.x, fleeDir.z);\n                  mob.group.rotation.y = angle;\n                }\n              } else {\n                // Wander randomly\n                mob.wanderTime = (mob.wanderTime || 0) - dt;\n                if (mob.wanderTime <= 0) {\n                  // Pick new random direction\n                  const wanderAngle = Math.random() * Math.PI * 2;\n                  mob.wanderDirection = new THREE.Vector3(\n                    Math.cos(wanderAngle),\n                    0,\n                    Math.sin(wanderAngle)\n                  );\n                  mob.wanderTime = 2 + Math.random() * 3; // Wander for 2-5 seconds\n                }\n\n                const wanderSpeed = 1.0;\n                mob.velocity.x = mob.wanderDirection.x * wanderSpeed;\n                mob.velocity.z = mob.wanderDirection.z * wanderSpeed;\n\n                // Face movement direction\n                if (mob.wanderDirection.length() > 0) {\n                  const angle = Math.atan2(mob.wanderDirection.x, mob.wanderDirection.z);\n                  mob.group.rotation.y = angle;\n                }\n              }\n\n              // Animate chicken\n              animateChicken(mob, dt);\n            }\n\n            // Cow AI - wander and flee when hit\n            if (mob.type === MOB_TYPES.COW) {\n              const closest = findClosestPlayer(mob.position);\n              const player = closest ? closest.pos : playerPos;\n              const distToPlayer = mob.position.distanceTo(player);\n\n              // Flee if recently attacked (isAggravated)\n              if (mob.isAggravated && distToPlayer < (mob.fleeDistance || 4)) {\n                const fleeDir = new THREE.Vector3().subVectors(mob.position, player);\n                fleeDir.y = 0;\n                if (fleeDir.length() > 0) {\n                  fleeDir.normalize();\n                  const fleeSpeed = 2.5; // Cows run slower than chickens\n                  mob.velocity.x = fleeDir.x * fleeSpeed;\n                  mob.velocity.z = fleeDir.z * fleeSpeed;\n                  const angle = Math.atan2(fleeDir.x, fleeDir.z);\n                  mob.group.rotation.y = angle;\n                }\n                // Reset aggravation after a while\n                mob.aggravationTimer = (mob.aggravationTimer || 3) - dt;\n                if (mob.aggravationTimer <= 0) {\n                  mob.isAggravated = false;\n                }\n              } else {\n                // Wander randomly\n                mob.wanderTime = (mob.wanderTime || 0) - dt;\n                if (mob.wanderTime <= 0) {\n                  // Pick new random direction or stop\n                  if (Math.random() < 0.3) {\n                    // 30% chance to stop and graze\n                    mob.wanderDirection = new THREE.Vector3(0, 0, 0);\n                    mob.wanderTime = 2 + Math.random() * 4; // Stand still for 2-6 seconds\n                  } else {\n                    const wanderAngle = Math.random() * Math.PI * 2;\n                    mob.wanderDirection = new THREE.Vector3(\n                      Math.cos(wanderAngle),\n                      0,\n                      Math.sin(wanderAngle)\n                    );\n                    mob.wanderTime = 3 + Math.random() * 4; // Wander for 3-7 seconds\n                  }\n                }\n\n                const wanderSpeed = 0.8; // Cows walk slowly\n                mob.velocity.x = mob.wanderDirection.x * wanderSpeed;\n                mob.velocity.z = mob.wanderDirection.z * wanderSpeed;\n\n                // Face movement direction\n                if (mob.wanderDirection.length() > 0.1) {\n                  const angle = Math.atan2(mob.wanderDirection.x, mob.wanderDirection.z);\n                  mob.group.rotation.y = angle;\n                }\n              }\n\n              // Animate cow\n              animateCow(mob, dt);\n            }\n\n            // Dog AI - wander unless tamed with meat, then follow the owner\n            if (mob.type === MOB_TYPES.DOG) {\n              const closest = findClosestPlayer(mob.position);\n              const player = closest ? closest.pos : playerPos;\n              const distToPlayer = mob.position.distanceTo(player);\n              const shouldFollow = !!mob.isTamed;\n\n              if (shouldFollow && distToPlayer > 3.5) {\n                // Follow the player only after being tamed with meat\n                const followDir = new THREE.Vector3().subVectors(player, mob.position);\n                followDir.y = 0;\n                if (followDir.length() > 0) {\n                  followDir.normalize();\n                  const followSpeed = 2.2;\n                  mob.velocity.x = followDir.x * followSpeed;\n                  mob.velocity.z = followDir.z * followSpeed;\n                  const angle = Math.atan2(followDir.x, followDir.z);\n                  mob.group.rotation.y = angle;\n                }\n              } else {\n                // Wander when not tamed or close to player\n                mob.wanderTime = (mob.wanderTime || 0) - dt;\n                if (mob.wanderTime <= 0) {\n                  if (Math.random() < 0.4) {\n                    mob.wanderDirection = new THREE.Vector3(0, 0, 0);\n                    mob.wanderTime = 1.5 + Math.random() * 2.5;\n                  } else {\n                    const wanderAngle = Math.random() * Math.PI * 2;\n                    mob.wanderDirection = new THREE.Vector3(\n                      Math.cos(wanderAngle),\n                      0,\n                      Math.sin(wanderAngle)\n                    );\n                    mob.wanderTime = 2 + Math.random() * 3;\n                  }\n                }\n\n                const wanderSpeed = 1.4;\n                mob.velocity.x = mob.wanderDirection.x * wanderSpeed;\n                mob.velocity.z = mob.wanderDirection.z * wanderSpeed;\n\n                if (mob.wanderDirection.length() > 0.1) {\n                  const angle = Math.atan2(mob.wanderDirection.x, mob.wanderDirection.z);\n                  mob.group.rotation.y = angle;\n                }\n              }\n\n              // Animate dog\n              animateDog(mob, dt);\n            }\n\n            // Villager AI - wander peacefully but flee from zombies\n            if (mob.type === MOB_TYPES.VILLAGER) {\n              // Check for nearby zombies to flee from\n              let nearestZombie = null;\n              let nearestZombieDist = Infinity;\n              const VILLAGER_FLEE_DISTANCE = 10; // Villagers detect zombies within 10 blocks\n\n              for (const otherMob of mobs) {\n                if (otherMob.type === MOB_TYPES.ZOMBIE && otherMob.health > 0) {\n                  const zdx = otherMob.position.x - mob.position.x;\n                  const zdy = otherMob.position.y - mob.position.y;\n                  const zdz = otherMob.position.z - mob.position.z;\n                  const zDist = Math.sqrt(zdx * zdx + zdy * zdy + zdz * zdz);\n\n                  if (zDist < nearestZombieDist && zDist < VILLAGER_FLEE_DISTANCE) {\n                    nearestZombieDist = zDist;\n                    nearestZombie = otherMob;\n                  }\n                }\n              }\n\n              // Flee from zombie if one is nearby\n              if (nearestZombie) {\n                const fleeDir = new THREE.Vector3().subVectors(\n                  mob.position,\n                  nearestZombie.position\n                );\n                fleeDir.y = 0;\n                if (fleeDir.length() > 0) {\n                  fleeDir.normalize();\n                  const fleeSpeed = 3.0; // Villagers run fast when scared!\n                  mob.velocity.x = fleeDir.x * fleeSpeed;\n                  mob.velocity.z = fleeDir.z * fleeSpeed;\n                  const angle = Math.atan2(fleeDir.x, fleeDir.z);\n                  mob.group.rotation.y = angle;\n\n                  // Panicked villager sounds occasionally\n                  if (!mob.lastPanicSoundTime || currentTime - mob.lastPanicSoundTime > 2) {\n                    mob.lastPanicSoundTime = currentTime;\n                    playSoundAtPosition(\n                      'villager_hurt_sound',\n                      mob.position.x,\n                      mob.position.y,\n                      mob.position.z,\n                      0.4,\n                      15\n                    );\n                  }\n                }\n              } else {\n                // Wander randomly with occasional stops to look around\n                mob.wanderTime = (mob.wanderTime || 0) - dt;\n                if (mob.wanderTime <= 0) {\n                  // Decide what to do next\n                  if (Math.random() < (mob.stopAndLookChance || 0.15)) {\n                    // Stop and look around\n                    mob.wanderDirection = new THREE.Vector3(0, 0, 0);\n                    mob.wanderTime = 3 + Math.random() * 4; // Stand still for 3-7 seconds\n                  } else {\n                    // Pick new random direction\n                    const wanderAngle = Math.random() * Math.PI * 2;\n                    mob.wanderDirection = new THREE.Vector3(\n                      Math.cos(wanderAngle),\n                      0,\n                      Math.sin(wanderAngle)\n                    );\n                    mob.wanderTime = 4 + Math.random() * 6; // Wander for 4-10 seconds\n                  }\n                }\n\n                const wanderSpeed = 1.2; // Villagers walk at a moderate pace\n                mob.velocity.x = mob.wanderDirection.x * wanderSpeed;\n                mob.velocity.z = mob.wanderDirection.z * wanderSpeed;\n\n                // Face movement direction\n                if (mob.wanderDirection.length() > 0.1) {\n                  const angle = Math.atan2(mob.wanderDirection.x, mob.wanderDirection.z);\n                  mob.group.rotation.y = angle;\n                }\n              }\n\n              // Animate villager\n              animateVillager(mob, dt);\n            }\n\n            // Animate zombie\n            animateZombie(mob, dt);\n\n            // Animate creeper\n            animateCreeper(mob, dt);\n\n            // Zombie attack\n            if (mob.type === MOB_TYPES.ZOMBIE) {\n              const closest = findClosestPlayer(mob.position);\n              const player = closest ? closest.pos : playerPos;\n              const distToPlayer = mob.position.distanceTo(player);\n              if (distToPlayer < 2) {\n                damagePlayer(2, currentTime);\n              }\n            }\n\n            // Play sounds occasionally\n            if (currentTime - mob.lastSoundTime > 5 && timeSyncer.random() < 0.5) {\n              if (mob.type === MOB_TYPES.ZOMBIE) {\n                playSoundAtPosition(\n                  'zombie_groan_sound',\n                  mob.position.x,\n                  mob.position.y,\n                  mob.position.z,\n                  0.5,\n                  20\n                );\n              } else if (mob.type === MOB_TYPES.VILLAGER) {\n                // Play villager sound with volume based on distance\n                playSoundAtPosition(\n                  'villager_sound',\n                  mob.position.x,\n                  mob.position.y,\n                  mob.position.z,\n                  0.5,\n                  20\n                );\n              }\n              mob.lastSoundTime = currentTime;\n            }\n\n            // Collision recovery - if mob is stuck inside a block, push it up\n            if (checkMobCollision(mob.position, mob)) {\n              // Try to push mob upward to get out of blocks\n              const recoveryPos = mob.position.clone();\n              let foundClearSpace = false;\n\n              for (let yOffset = 0.1; yOffset <= 2.0; yOffset += 0.1) {\n                recoveryPos.y = mob.position.y + yOffset;\n                if (!checkMobCollision(recoveryPos, mob)) {\n                  mob.position.copy(recoveryPos);\n                  mob.velocity.y = 0;\n                  foundClearSpace = true;\n                  break;\n                }\n              }\n\n              // If we can't find clear space, remove the mob (it's hopelessly stuck)\n              if (!foundClearSpace) {\n                scene.remove(mob.group);\n                mobs.splice(i, 1);\n                continue;\n              }\n            }\n\n            // Check if mob is in water\n            const mobInWater = checkMobInWater(mob.position, mob);\n            mob.isInWater = mobInWater;\n\n            // Check if mob is in lava - damage them!\n            const mobInLava = checkMobInLava(mob.position, mob);\n            const currentLavaTime = performance.now() / 1000;\n\n            // Initialize burning properties if not exists\n            if (!mob.lastLavaDamageTime) mob.lastLavaDamageTime = 0;\n            if (!mob.burnEndTime) mob.burnEndTime = 0;\n\n            if (mobInLava) {\n              // Actively in lava - set burning end time to 2 seconds from now\n              mob.burnEndTime = currentLavaTime + BURNING_DURATION;\n\n              // Apply damage based on cooldown\n              if (currentLavaTime - mob.lastLavaDamageTime >= LAVA_DAMAGE_COOLDOWN) {\n                mob.lastLavaDamageTime = currentLavaTime;\n                damageMob(mob, LAVA_DAMAGE_AMOUNT);\n              }\n            } else if (currentLavaTime < mob.burnEndTime) {\n              // Not in lava, but still burning from residual effect (2 seconds max)\n              if (currentLavaTime - mob.lastLavaDamageTime >= LAVA_DAMAGE_COOLDOWN) {\n                mob.lastLavaDamageTime = currentLavaTime;\n                damageMob(mob, LAVA_DAMAGE_AMOUNT);\n              }\n\n              // Create burning particles for mobs on fire\n              if (Math.random() < 0.3) {\n                const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);\n                const material = new THREE.MeshBasicMaterial({\n                  color: new THREE.Color(1, Math.random() * 0.5, 0),\n                });\n                const particle = new THREE.Mesh(geometry, material);\n                particle.position.copy(mob.position);\n                particle.position.y += mob.size * 0.3;\n                particle.userData.velocity = new THREE.Vector3(\n                  (Math.random() - 0.5) * 0.5,\n                  Math.random() * 2 + 1,\n                  (Math.random() - 0.5) * 0.5\n                );\n                particle.userData.life = 0.5;\n                scene.add(particle);\n                particles.push(particle);\n              }\n            }\n\n            // Apply gravity (reduced in water for swimming)\n            if (mobInWater) {\n              // In water: reduced gravity, mobs float/swim\n              mob.velocity.y += WATER_GRAVITY * dt;\n\n              // Mobs try to swim up to stay near surface\n              // Check if there's air above water level\n              const headY = mob.position.y + mob.size * 0.3;\n              const headBlockY = Math.floor(headY);\n              const blockAboveHead = getBlockAt(\n                Math.floor(mob.position.x),\n                headBlockY + 1,\n                Math.floor(mob.position.z)\n              );\n              const blockAtHead = getBlockAt(\n                Math.floor(mob.position.x),\n                headBlockY,\n                Math.floor(mob.position.z)\n              );\n\n              // If head is underwater and there's air above, swim up\n              if (\n                (blockAtHead === BLOCK_TYPES.WATER || blockAtHead === BLOCK_TYPES.WATER_FLOW) &&\n                blockAboveHead === BLOCK_TYPES.AIR\n              ) {\n                mob.velocity.y = Math.max(mob.velocity.y, WATER_SWIM_UP_SPEED * 0.6); // Swim up towards surface\n              }\n\n              // Limit sinking speed in water\n              if (mob.velocity.y < -WATER_SINK_SPEED) {\n                mob.velocity.y = -WATER_SINK_SPEED;\n              }\n\n              // Reduce horizontal speed in water\n              mob.velocity.x *= WATER_SWIM_SPEED;\n              mob.velocity.z *= WATER_SWIM_SPEED;\n            } else {\n              // Normal gravity on land\n              mob.velocity.y += GRAVITY * dt;\n            }\n\n            // Update position with collision detection and stair climbing\n            const newPos = mob.position.clone();\n            const STEP_HEIGHT = 1.0;\n            // Max height mobs can step up\n\n            // X movement with collision and step-up\n            newPos.x += mob.velocity.x * dt;\n            if (checkMobCollision(newPos, mob)) {\n              // Try to step up if blocked horizontally\n              const stepUpPos = newPos.clone();\n              stepUpPos.y += STEP_HEIGHT;\n\n              // Check if we can step up onto the block\n              if (!checkMobCollision(stepUpPos, mob) && (mob.isGrounded || mobInWater)) {\n                // Can step up - apply the movement and lift\n                newPos.y += STEP_HEIGHT;\n              } else {\n                // Cannot step up - block movement\n                newPos.x = mob.position.x;\n                mob.velocity.x = 0;\n              }\n            }\n\n            // Z movement with collision and step-up\n            newPos.z += mob.velocity.z * dt;\n            if (checkMobCollision(newPos, mob)) {\n              // Try to step up if blocked horizontally\n              const stepUpPos = newPos.clone();\n              stepUpPos.y += STEP_HEIGHT;\n\n              // Check if we can step up onto the block\n              if (!checkMobCollision(stepUpPos, mob) && (mob.isGrounded || mobInWater)) {\n                // Can step up - apply the movement and lift\n                newPos.y += STEP_HEIGHT;\n              } else {\n                // Cannot step up - block movement\n                newPos.z = mob.position.z;\n                mob.velocity.z = 0;\n              }\n            }\n\n            // Y movement with collision\n            newPos.y += mob.velocity.y * dt;\n            if (checkMobCollision(newPos, mob)) {\n              if (mob.velocity.y < 0) {\n                // Landing on ground - align to top of block\n                const mobFootY = newPos.y - mob.size * 0.5;\n                const blockY = Math.floor(mobFootY);\n                newPos.y = blockY + 1 + mob.size * 0.5;\n                mob.isGrounded = true;\n              }\n              mob.velocity.y = 0;\n            } else {\n              mob.isGrounded = false;\n            }\n\n            mob.position.copy(newPos);\n            mob.group.position.copy(mob.position);\n\n            // sync mob position\n            const mobId = mob.id;\n            syncMobUpdate(\n              mobId,\n              mob.position.x,\n              mob.position.y,\n              mob.position.z,\n              0,\n              mob.health,\n              mob.exploding,\n              mob.explodeProgress\n            );\n\n            // Remove if too far or dead\n            if (mob.health <= 0) {\n              scene.remove(mob.group);\n              mobs.splice(i, 1);\n              syncMobRemove(mobId);\n            }\n            // else if (mob.position.distanceTo(playerPos) > 100) {\n            //   scene.remove(mob.group);\n            //   mobs.splice(i, 1);\n            // }\n          }\n        }\n\n        function explodeCreeper(mob) {\n          playSound('explosion_sound', false, 0.7);\n\n          // Damage player if nearby\n          const closest = findClosestPlayer(mob.position);\n          const player = closest ? closest.pos : playerPos;\n          const distToPlayer = mob.position.distanceTo(player);\n          if (distToPlayer < 6) {\n            const damage = Math.ceil((1 - distToPlayer / 6) * 15);\n            damagePlayer(damage, performance.now() / 1000);\n          }\n\n          // Destroy blocks in radius (infinite world - only check Y bounds)\n          const explosionRadius = 3;\n          const mx = Math.floor(mob.position.x);\n          const my = Math.floor(mob.position.y);\n          const mz = Math.floor(mob.position.z);\n\n          for (let x = mx - explosionRadius; x <= mx + explosionRadius; x++) {\n            for (let y = my - explosionRadius; y <= my + explosionRadius; y++) {\n              for (let z = mz - explosionRadius; z <= mz + explosionRadius; z++) {\n                if (y >= 0 && y < worldHeight) {\n                  const dist = Math.sqrt((x - mx) ** 2 + (y - my) ** 2 + (z - mz) ** 2);\n                  const blockType = getBlockAt(x, y, z);\n                  if (\n                    dist <= explosionRadius &&\n                    blockType !== BLOCK_TYPES.AIR &&\n                    blockType !== BLOCK_TYPES.BEDROCK\n                  ) {\n                    setBlockAt(x, y, z, BLOCK_TYPES.AIR);\n                    updateBlockAt(x, y, z);\n\n                    // Track modification\n                    const blockKey = `${x},${y},${z}`;\n                    modifiedBlocks.set(blockKey, BLOCK_TYPES.AIR);\n\n                    if (blockType === BLOCK_TYPES.DOOR) {\n                      if (getBlockAt(x, y + 1, z) === BLOCK_TYPES.DOOR) {\n                        setBlockAt(x, y + 1, z, BLOCK_TYPES.AIR);\n                        updateBlockAt(x, y + 1, z);\n                        modifiedBlocks.set(`${x},${y + 1},${z}`, BLOCK_TYPES.AIR);\n                        if (isOnline) syncBlockAction('mine', x, y + 1, z, BLOCK_TYPES.DOOR);\n                      }\n                      if (getBlockAt(x, y - 1, z) === BLOCK_TYPES.DOOR) {\n                        setBlockAt(x, y - 1, z, BLOCK_TYPES.AIR);\n                        updateBlockAt(x, y - 1, z);\n                        modifiedBlocks.set(`${x},${y - 1},${z}`, BLOCK_TYPES.AIR);\n                        if (isOnline) syncBlockAction('mine', x, y - 1, z, BLOCK_TYPES.DOOR);\n                      }\n                    }\n\n                    if (Math.random() < 0.3) {\n                      createBreakParticles(x + 0.5, y + 0.5, z + 0.5, blockType);\n                    }\n                  }\n                }\n              }\n            }\n          }\n\n          // Explosion particles\n          for (let i = 0; i < 30; i++) {\n            const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);\n            const material = new THREE.MeshBasicMaterial({\n              color: new THREE.Color(Math.random(), Math.random() * 0.5, 0),\n            });\n            const particle = new THREE.Mesh(geometry, material);\n            particle.position.copy(mob.position);\n            particle.userData.velocity = new THREE.Vector3(\n              (Math.random() - 0.5) * 10,\n              Math.random() * 10,\n              (Math.random() - 0.5) * 10\n            );\n            particle.userData.life = 1.5;\n            scene.add(particle);\n            particles.push(particle);\n          }\n\n          // Damage other mobs nearby\n          for (let i = mobs.length - 1; i >= 0; i--) {\n            const otherMob = mobs[i];\n            if (otherMob === mob) continue;\n            const distToMob = mob.position.distanceTo(otherMob.position);\n            if (distToMob < 6) {\n              const damage = Math.ceil((1 - distToMob / 6) * 15);\n              damageMob(otherMob, damage);\n              if (typeof syncMobHit === 'function') {\n                syncMobHit(otherMob.id, damage);\n              }\n            }\n          }\n        }\n\n        function buildWorldStatePayload() {\n          return {\n            modifiedBlocks: Array.from(modifiedBlocks),\n            signMessages: Array.from(signMessages),\n            signOrientations: Array.from(signOrientations),\n            chestContents: Array.from(chestContents),\n            furnaceContents: Array.from(furnaceContents),\n            doorStates: Array.from(doorStates),\n            doorOrientations: Array.from(doorOrientations),\n            furnaceOrientations: Array.from(furnaceOrientations),\n            chestOrientations: Array.from(chestOrientations),\n          };\n        }\n\n        function sendWorldState() {\n          syncEvent('world_state', buildWorldStatePayload());\n        }\n\n        function recieveWorldState(receivedData, options = {}) {\n          const forceApply = options.forceApply === true;\n          if (!forceApply && !justJoined) return;\n          justJoined = false;\n\n          if (!receivedData || typeof receivedData !== 'object') return;\n\n          // Reset world\n          resetWorld();\n\n          // Update blocks\n          const updatedBlocks = new Map(receivedData.modifiedBlocks || []);\n          updatedBlocks.forEach((e, k) => {\n            const blockType = e;\n            const seg = k.split(',').map((i) => i * 1);\n            const x = seg[0];\n            const y = seg[1];\n            const z = seg[2];\n\n            worldData[x][y][z] = blockType;\n            updateBlockAt(x, y, z);\n            modifiedBlocks.set(`${x},${y},${z}`, blockType);\n          });\n\n          // Update texts\n          const updatedText = new Map(receivedData.signMessages || []);\n          updatedText.forEach((e, k) => {\n            const message = e;\n            const seg = k.split(',').map((i) => i * 1);\n            const x = seg[0];\n            const y = seg[1];\n            const z = seg[2];\n\n            // Handle sign message sync from other players\n            const signKey = `${x},${y},${z}`;\n            signMessages.set(signKey, message);\n\n            // Update the sign text display if mesh exists\n            const signMesh = signMeshes.get(signKey);\n            if (signMesh) {\n              updateSignText(signMesh, message);\n            }\n          });\n\n          // Update sign orientations\n          if (receivedData.signOrientations) {\n            const updatedSignOrientations = new Map(receivedData.signOrientations);\n            updatedSignOrientations.forEach((rotation, k) => {\n              signOrientations.set(k, rotation);\n              // Update sign mesh if it exists\n              const signMesh = signMeshes.get(k);\n              if (signMesh) {\n                signMesh.rotation.y = rotation;\n              }\n            });\n          }\n\n          // Update chest contents\n          if (receivedData.chestContents) {\n            const updatedChests = new Map(receivedData.chestContents);\n            updatedChests.forEach((contents, k) => {\n              chestContents.set(k, contents);\n            });\n          }\n\n          // Update furnace contents\n          if (receivedData.furnaceContents) {\n            const updatedFurnaces = new Map(receivedData.furnaceContents);\n            updatedFurnaces.forEach((contents, k) => {\n              furnaceContents.set(k, contents);\n            });\n          }\n\n          // Update door states\n          if (receivedData.doorStates) {\n            const updatedDoors = new Map(receivedData.doorStates);\n            updatedDoors.forEach((isOpen, k) => {\n              doorStates.set(k, isOpen);\n              // Update door mesh if it exists\n              const doorMesh = doorMeshes.get(k);\n              if (doorMesh) {\n                const baseRotation = doorMesh.userData.baseRotation || 0;\n                doorMesh.rotation.y = baseRotation + (isOpen ? -Math.PI / 2 : 0);\n              }\n            });\n          }\n\n          // Update door orientations\n          if (receivedData.doorOrientations) {\n            const updatedDoorOrients = new Map(receivedData.doorOrientations);\n            updatedDoorOrients.forEach((rotation, k) => {\n              doorOrientations.set(k, rotation);\n              // Update door mesh if it exists\n              const doorMesh = doorMeshes.get(k);\n              if (doorMesh) {\n                doorMesh.userData.baseRotation = rotation;\n                const isOpen = doorStates.get(k) || false;\n                doorMesh.rotation.y = rotation + (isOpen ? -Math.PI / 2 : 0);\n              }\n            });\n          }\n\n          // Update furnace orientations\n          if (receivedData.furnaceOrientations) {\n            const updatedFurnaces = new Map(receivedData.furnaceOrientations);\n            updatedFurnaces.forEach((rotation, k) => {\n              furnaceOrientations.set(k, rotation);\n              // Update furnace mesh if it exists\n              const furnaceMesh = furnaceMeshes.get(k);\n              if (furnaceMesh) {\n                furnaceMesh.rotation.y = rotation;\n              }\n            });\n          }\n\n          // Update chest orientations\n          if (receivedData.chestOrientations) {\n            const updatedChests = new Map(receivedData.chestOrientations);\n            updatedChests.forEach((rotation, k) => {\n              chestOrientations.set(k, rotation);\n              // Update chest mesh if it exists\n              const chestMesh = chestMeshes.get(k);\n              if (chestMesh) {\n                chestMesh.rotation.y = rotation;\n              }\n            });\n          }\n        }\n\n        let lastPlayersCount = 0;\n\n        // Returns the peer ID of the earliest connected player\n        const firstPlayerJoined = function (lobby) {\n          const peerIds = [];\n          for (let userId in lobby.users) {\n            const user = lobby.users[userId];\n\n            if (user.state == 'online') {\n              peerIds.push(user);\n            }\n          }\n\n          // no players connected\n          if (peerIds.length == 0) return null;\n\n          // get the first player in the room\n          peerIds.sort(function (a, b) {\n            return a.timestamp - b.timestamp;\n          });\n\n          return peerIds[0].peerId;\n        };\n\n        // Override IAmActingServer to use our custom getActingServer\n        const IAmRoundRobinHost = function (lobby, timeSyncer) {\n          return IAmActingServer(lobby, timeSyncer);\n        };\n\n        const IAmFirstJoinerHost = function (lobby) {\n          const serverPeerId = firstPlayerJoined(lobby);\n          return lobby.getSelfPeerId() === serverPeerId;\n        };\n\n        // sync world state to new players\n        function sendWorldStateToNewPlayers() {\n          // prevent spam\n          const playersCount = otherPlayers.size + 1;\n          if (lastPlayersCount == playersCount) return;\n          lastPlayersCount = playersCount;\n\n          sendWorldState();\n        }\n\n        // sync global time\n        window.timeSyncer = new TimestampDisplay(1688647930, 5, () => {\n          if (!lobby || !lobby.fbobj) return;\n          if (IAmRoundRobinHost(lobby, timeSyncer)) {\n            lobby.fbobj.updateServerTimestamp(); // update global time\n          }\n        });\n\n        // place global events here\n        window.mobSyncer = new TimestampDisplay(1688647930, 1, () => {\n          if (!lobby || !lobby.fbobj) return;\n          if (IAmFirstJoinerHost(lobby)) {\n            sendWorldStateToNewPlayers();\n          }\n        });\n\n        setInterval(() => {\n          timeSyncer.update();\n          mobSyncer.update();\n        }, 30);\n\n        // Keep connection alive inside a room\n        let isPolling = false;\n\n        setInterval(async () => {\n          if (!roomManager) return;\n          if (!isPolling) {\n            isPolling = true;\n            try {\n              if (\n                roomManager.isInRoom() ||\n                document.getElementById('worlds-modal').classList.contains('active')\n              ) {\n                await listRooms();\n              }\n            } catch (error) {\n              console.error('Error:', error);\n            } finally {\n              isPolling = false;\n            }\n          }\n        }, 1000);\n\n        function damagePlayer(damage, currentTime) {\n          if (isCreativeMode) return;\n          if (currentTime - lastDamageTime < DAMAGE_COOLDOWN) return;\n\n          playerHealth -= damage;\n          lastDamageTime = currentTime;\n          updateHealthBar();\n          playSound('player_hurt_sound', false, 0.5);\n\n          // Emit player hit event for multiplayer\n          if (isOnline) {\n            syncEvent('player_hit', { damage: damage });\n          }\n\n          // Flash screen red\n          const flashEl = document.createElement('div');\n          flashEl.style.cssText = `\n                    position: absolute;\n                    top: 0;\n                    left: 0;\n                    width: 100%;\n                    height: 100%;\n                    background: rgba(255, 0, 0, 0.3);\n                    pointer-events: none;\n                    z-index: 50;\n                    animation: fadeOut 0.3s;\n                `;\n          gameWorld.appendChild(flashEl);\n          setTimeout(() => flashEl.remove(), 300);\n\n          if (playerHealth <= 0) {\n            playerHealth = 0;\n            gameOver();\n          }\n        }\n\n        function gameOver() {\n          // Immediately respawn at spawn point - no menu\n          if (spawnPoint) {\n            playerPos.copy(spawnPoint);\n            playerYaw = 0;\n            playerPitch = 0;\n            playerVelocity.set(0, 0, 0);\n\n            // Update camera (3rd person)\n            updateThirdPersonCamera();\n          }\n\n          // Restore player to full health\n          playerHealth = maxPlayerHealth;\n          updateHealthBar();\n\n          // Restore player hunger to half (penalty for dying)\n          playerHunger = maxPlayerHunger / 2;\n          updateHungerBar();\n\n          // Clear burning debuff\n          burnEndTime = 0;\n          lastLavaDamageTime = 0;\n\n          // Clear any mining state\n          isMining = false;\n          miningProgress = 0;\n\n          // Brief death flash effect\n          const flashEl = document.createElement('div');\n          flashEl.style.cssText = `\n                    position: absolute;\n                    top: 0;\n                    left: 0;\n                    width: 100%;\n                    height: 100%;\n                    background: rgba(0, 0, 0, 0.8);\n                    pointer-events: none;\n                    z-index: 50;\n                    animation: fadeOut 0.5s;\n                `;\n          gameWorld.appendChild(flashEl);\n          setTimeout(() => flashEl.remove(), 500);\n\n          // Show respawn message\n          showMessage('You died! Respawned at spawn.', '#F44336', 2000);\n        }\n\n        function getTargetMob(maxDistance = 4) {\n          // In 3rd person, raycast from player position in look direction\n          const lookDir = new THREE.Vector3();\n          camera.getWorldDirection(lookDir);\n          raycaster.set(playerPos.clone(), lookDir);\n          raycaster.far = maxDistance;\n\n          const mobGroups = mobs.map((mob) => mob.group);\n          const intersects = raycaster.intersectObjects(mobGroups, true);\n\n          if (intersects.length > 0) {\n            let hitMesh = intersects[0].object;\n            let mob = null;\n\n            // Find which mob this mesh belongs to by traversing parents\n            while (hitMesh && hitMesh !== scene) {\n              mob = mobs.find((m) => m.group === hitMesh);\n              if (mob) break;\n              hitMesh = hitMesh.parent;\n            }\n\n            return mob;\n          }\n\n          return null;\n        }\n\n        function isMeatItem(itemType) {\n          return (\n            itemType === ITEM_TYPES.RAW_BEEF ||\n            itemType === ITEM_TYPES.COOKED_BEEF ||\n            itemType === ITEM_TYPES.RAW_CHICKEN ||\n            itemType === ITEM_TYPES.COOKED_CHICKEN\n          );\n        }\n\n        function attackMob() {\n          // Raycast to find mob\n          const mob = getTargetMob(4);\n\n          if (mob) {\n            // Base damage\n            let damage = 5;\n\n            // Check if holding a sword for bonus damage\n            const heldSlot = inventory[selectedSlot];\n            if (heldSlot && isTool(heldSlot.type)) {\n              const toolEffectiveness = TOOL_EFFECTIVENESS[heldSlot.type];\n              if (toolEffectiveness && toolEffectiveness.damageMultiplier) {\n                damage *= toolEffectiveness.damageMultiplier;\n              }\n            }\n\n            damageMob(mob, damage);\n            syncMobHit(mob.id, damage);\n            triggerArmSwing(false); // Swing arm when attacking\n            return true;\n          }\n\n          return false;\n        }\n\n        function createBreakParticles(x, y, z, blockType) {\n          const colors = {\n            [BLOCK_TYPES.GRASS]: 0x5cb531,\n            [BLOCK_TYPES.DIRT]: 0x8b6f47,\n            [BLOCK_TYPES.STONE]: 0x7a7a7a,\n            [BLOCK_TYPES.WOOD]: 0x8b5a2b,\n            [BLOCK_TYPES.LEAVES]: 0x2d5016,\n            [BLOCK_TYPES.DYNAMITE]: 0xff4500,\n            [BLOCK_TYPES.SAND]: 0xe0c896,\n            [BLOCK_TYPES.WATER]: 0x4a90e2,\n            [BLOCK_TYPES.WATER_FLOW]: 0x4a90e2,\n            [BLOCK_TYPES.CRAFTING_TABLE]: 0x8b5a2b,\n            [BLOCK_TYPES.PLANKS]: 0xc4a76b,\n            [BLOCK_TYPES.TORCH]: 0xffa500,\n            [BLOCK_TYPES.COAL_ORE]: 0x3a3a3a,\n            [BLOCK_TYPES.IRON_ORE]: 0xd4b896,\n            [BLOCK_TYPES.GOLD_ORE]: 0xffd700,\n            [BLOCK_TYPES.DIAMOND_ORE]: 0x00ffff,\n            [BLOCK_TYPES.OAK_SIGN]: 0xc4a76b,\n            [BLOCK_TYPES.CHEST]: 0x8b5a2b,\n            [BLOCK_TYPES.FURNACE]: 0x7a7a7a,\n            [BLOCK_TYPES.BEDROCK]: 0x2a2a2a,\n            [BLOCK_TYPES.GLASS]: 0xc0e8ff,\n          };\n\n          const color = colors[blockType] || 0xffffff;\n          const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);\n          const material = new THREE.MeshBasicMaterial({\n            color,\n          });\n\n          for (let i = 0; i < 8; i++) {\n            const particle = new THREE.Mesh(geometry, material);\n            particle.position.set(\n              x + (Math.random() - 0.5) * 0.5,\n              y + (Math.random() - 0.5) * 0.5,\n              z + (Math.random() - 0.5) * 0.5\n            );\n            particle.userData.velocity = new THREE.Vector3(\n              (Math.random() - 0.5) * 3,\n              Math.random() * 3 + 2,\n              (Math.random() - 0.5) * 3\n            );\n            particle.userData.life = 1;\n            scene.add(particle);\n            particles.push(particle);\n          }\n        }\n\n        function updateParticles(dt) {\n          for (let i = particles.length - 1; i >= 0; i--) {\n            const p = particles[i];\n\n            if (typeof p.userData.velocity === 'number') {\n              p.position.y += p.userData.velocity * dt;\n              p.userData.life -= dt;\n            } else {\n              p.userData.velocity.y -= 15 * dt;\n              p.position.add(p.userData.velocity.clone().multiplyScalar(dt));\n              p.userData.life -= dt * 2;\n            }\n\n            p.material.opacity = p.userData.life;\n\n            if (p.userData.life <= 0) {\n              scene.remove(p);\n              particles.splice(i, 1);\n            }\n          }\n        }\n\n        function updateDynamiteCountdowns(dt) {\n          const keysToDelete = [];\n\n          for (const [key, countdown] of dynamiteCountdowns) {\n            countdown.timeLeft -= dt;\n\n            // Visual effect - for instanced meshes, we can create a flashing overlay mesh\n            const { x, y, z } = countdown;\n\n            // Create or update flash effect particle\n            const flashKey = `flash_${key}`;\n            if (!countdown.flashMesh) {\n              // Create a flashing overlay mesh for this TNT\n              const flashGeo = new THREE.BoxGeometry(1.02, 1.02, 1.02);\n              const flashMat = new THREE.MeshBasicMaterial({\n                color: 0xffffff,\n                transparent: true,\n                opacity: 0,\n              });\n              countdown.flashMesh = new THREE.Mesh(flashGeo, flashMat);\n              countdown.flashMesh.position.set(x + 0.5, y + 0.5, z + 0.5);\n              scene.add(countdown.flashMesh);\n            }\n\n            // Update flash effect\n            const flashSpeed = 10 * (1 - countdown.timeLeft / countdown.totalTime);\n            const flash = Math.floor((countdown.timeLeft * flashSpeed) % 2);\n            countdown.flashMesh.material.opacity = flash ? 0.6 : 0;\n\n            if (countdown.timeLeft <= 0) {\n              // Clean up flash mesh\n              if (countdown.flashMesh) {\n                scene.remove(countdown.flashMesh);\n                countdown.flashMesh.geometry.dispose();\n                countdown.flashMesh.material.dispose();\n              }\n              // Trigger explosion\n              triggerDynamiteExplosion(x, y, z);\n              keysToDelete.push(key);\n            }\n          }\n\n          // Clean up expired countdowns\n          keysToDelete.forEach((key) => dynamiteCountdowns.delete(key));\n        }\n\n        // Player physics\n        const updatePlayer = function (dt) {\n          const moveSpeed = 5 * config.movementSpeedMultiplier;\n          const lookSpeed = 0.002;\n\n          // Camera rotation from mouse\n          if (!isTouchDevice && isPointerLocked) {\n            playerYaw -= mouseMovement.x * lookSpeed;\n            playerPitch -= mouseMovement.y * lookSpeed;\n            playerPitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, playerPitch));\n            mouseMovement.x = 0;\n            mouseMovement.y = 0;\n          }\n\n          // Camera rotation from touch (aim crosshair mode - direct drag)\n          if (isTouchDevice && rightJoystick.active) {\n            playerYaw -= rightJoystick.dx;\n            playerPitch -= rightJoystick.dy;\n            playerPitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, playerPitch));\n            // Reset drag delta after applying\n            rightJoystick.dx = 0;\n            rightJoystick.dy = 0;\n          }\n\n          // Movement direction\n          const forward = new THREE.Vector3(-Math.sin(playerYaw), 0, -Math.cos(playerYaw));\n          const right = new THREE.Vector3(Math.cos(playerYaw), 0, -Math.sin(playerYaw));\n\n          let moveDir = new THREE.Vector3(0, 0, 0);\n\n          // Keyboard movement\n          if (keys['w']) moveDir.add(forward);\n          if (keys['s']) moveDir.sub(forward);\n          if (keys['a']) moveDir.sub(right);\n          if (keys['d']) moveDir.add(right);\n\n          // Touch movement (from move widget only - left side dedicated to movement)\n          if (isTouchDevice && moveWidgetState.active) {\n            moveDir.add(forward.clone().multiplyScalar(-moveWidgetState.dy));\n            moveDir.add(right.clone().multiplyScalar(moveWidgetState.dx));\n          }\n\n          if (moveDir.length() > 0) {\n            moveDir.normalize();\n          }\n\n          // Apply horizontal movement (slower in water)\n          const speedMultiplier = isInWater ? WATER_SWIM_SPEED : 1.0;\n          const targetVelX = moveDir.x * moveSpeed * speedMultiplier;\n          const targetVelZ = moveDir.z * moveSpeed * speedMultiplier;\n\n          playerVelocity.x = targetVelX;\n          playerVelocity.z = targetVelZ;\n\n          // Check if player is in water\n          isInWater = checkInWater(playerPos);\n\n          // Water breaks falls - reset fall tracking when entering water\n          if (isInWater && wasFalling) {\n            wasFalling = false;\n            fallStartY = playerPos.y;\n          }\n\n          // Check if player is in lava and apply damage\n          const currentTime = performance.now() / 1000;\n          isInLava = checkInLava(playerPos);\n          if (isInLava) {\n            // Actively in lava - set burning end time to 2 seconds from now\n            burnEndTime = currentTime + BURNING_DURATION;\n\n            // Apply damage based on cooldown\n            if (currentTime - lastLavaDamageTime >= LAVA_DAMAGE_COOLDOWN) {\n              lastLavaDamageTime = currentTime;\n              damagePlayer(LAVA_DAMAGE_AMOUNT, currentTime);\n              showMessage('\ud83d\udd25 Burning!', '#FF4500', 500);\n            }\n          } else if (currentTime < burnEndTime) {\n            // Not in lava, but still burning from residual effect (2 seconds max)\n            // Water extinguishes fire!\n            if (isInWater) {\n              burnEndTime = 0;\n              showMessage('\ud83c\udf0a Fire extinguished!', '#4a90e2', 1000);\n              playSound('block_place_sound', false, 0.4);\n            } else if (currentTime - lastLavaDamageTime >= LAVA_DAMAGE_COOLDOWN) {\n              lastLavaDamageTime = currentTime;\n              damagePlayer(LAVA_DAMAGE_AMOUNT, currentTime);\n              showMessage('\ud83d\udd25 Burning!', '#FF4500', 500);\n            }\n          }\n\n          // Apply gravity (reduced in water)\n          if (isInWater) {\n            // In water: reduced gravity, natural sinking\n            playerVelocity.y += WATER_GRAVITY * dt;\n            // Limit sinking speed in water\n            if (playerVelocity.y < -WATER_SINK_SPEED) {\n              playerVelocity.y = -WATER_SINK_SPEED;\n            }\n          } else {\n            playerVelocity.y += GRAVITY * dt;\n          }\n\n          // Move with collision\n          const newPos = playerPos.clone();\n\n          const PLAYER_STEP_HEIGHT = 1.0;\n\n          // X movement with step-up\n          newPos.x += playerVelocity.x * dt;\n          if (checkCollision(newPos)) {\n            // Try stepping up 1 block\n            const stepUpX = newPos.clone();\n            stepUpX.y += PLAYER_STEP_HEIGHT;\n            if (isGrounded && !checkCollision(stepUpX)) {\n              newPos.y += PLAYER_STEP_HEIGHT;\n            } else {\n              newPos.x = playerPos.x;\n              playerVelocity.x = 0;\n            }\n          }\n\n          // Z movement with step-up\n          newPos.z += playerVelocity.z * dt;\n          if (checkCollision(newPos)) {\n            const stepUpZ = newPos.clone();\n            stepUpZ.y += PLAYER_STEP_HEIGHT;\n            if (isGrounded && !checkCollision(stepUpZ)) {\n              newPos.y += PLAYER_STEP_HEIGHT;\n            } else {\n              newPos.z = playerPos.z;\n              playerVelocity.z = 0;\n            }\n          }\n\n          // Y movement with ground snap\n          newPos.y += playerVelocity.y * dt;\n          if (checkCollision(newPos)) {\n            if (playerVelocity.y < 0) {\n              // Snap feet to top of block\n              const footY = newPos.y - PLAYER_HEIGHT;\n              const blockY = Math.floor(footY);\n              newPos.y = (blockY + 1) + PLAYER_HEIGHT;\n\n              // Fall damage\n              if (wasFalling && !isInWater) {\n                const fallDistance = fallStartY - newPos.y;\n                if (fallDistance > config.fallDamageThreshold) {\n                  const damage = Math.floor(\n                    (fallDistance - config.fallDamageThreshold) * FALL_DAMAGE_PER_BLOCK\n                  );\n                  if (damage > 0) {\n                    damagePlayer(damage, performance.now() / 1000);\n                    if (damage >= 5) {\n                      showMessage('Ouch! Fall damage: ' + damage, '#FF5722', 1500);\n                    }\n                  }\n                }\n              }\n              isGrounded = true;\n              wasFalling = false;\n            } else {\n              // Hit ceiling\n              newPos.y = playerPos.y;\n            }\n            playerVelocity.y = 0;\n          } else {\n            // Player is in the air\n            if (!isGrounded && playerVelocity.y < 0 && !wasFalling) {\n              fallStartY = playerPos.y;\n              wasFalling = true;\n            } else if (isGrounded) {\n              fallStartY = playerPos.y;\n            }\n            isGrounded = false;\n          }\n\n          // Update player position and collision\n          playerPos.copy(newPos);\n\n          // Obby fall detection\n          if (playerPos.y < OBBY_FALL_THRESHOLD) {\n            respawnAtCheckpoint();\n          }\n\n          // Update camera based on POV mode\n          updateCamera();\n        };\n\n        function checkCollision(pos) {\n          // Tighten collision radius slightly to avoid sticking to walls\n          const RADIUS = PLAYER_RADIUS * 0.8;\n          const checkPoints = [\n            [pos.x - RADIUS, pos.y, pos.z - RADIUS],\n            [pos.x + RADIUS, pos.y, pos.z - RADIUS],\n            [pos.x - RADIUS, pos.y, pos.z + RADIUS],\n            [pos.x + RADIUS, pos.y, pos.z + RADIUS],\n            [pos.x - RADIUS, pos.y - PLAYER_HEIGHT, pos.z - RADIUS],\n            [pos.x + RADIUS, pos.y - PLAYER_HEIGHT, pos.z - RADIUS],\n            [pos.x - RADIUS, pos.y - PLAYER_HEIGHT, pos.z + RADIUS],\n            [pos.x + RADIUS, pos.y - PLAYER_HEIGHT, pos.z + RADIUS],\n          ];\n\n          for (const [px, py, pz] of checkPoints) {\n            const bx = Math.floor(px);\n            const by = Math.floor(py);\n            const bz = Math.floor(pz);\n\n            if (by >= 0 && by < worldHeight) {\n              const blockType = getBlockAt(bx, by, bz);\n              // Torch blocks, water, lava, rice plants, and open doors are walkable - skip collision\n              if (blockType === BLOCK_TYPES.DOOR) {\n                const doorKey = getDoorBaseKey(bx, by, bz);\n                if (doorStates.get(doorKey)) {\n                  continue; // Door is open, no collision\n                }\n              }\n              if (\n                blockType !== BLOCK_TYPES.AIR &&\n                blockType !== BLOCK_TYPES.TORCH &&\n                blockType !== BLOCK_TYPES.WATER &&\n                blockType !== BLOCK_TYPES.WATER_FLOW &&\n                blockType !== BLOCK_TYPES.LAVA &&\n                blockType !== BLOCK_TYPES.LAVA_FLOW &&\n                blockType !== BLOCK_TYPES.RICE_PLANT &&\n                blockType !== BLOCK_TYPES.FLOWER &&\n                blockType !== BLOCK_TYPES.FLOWER &&\n                blockType !== BLOCK_TYPES.FLOWER\n              ) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }\n\n        // Check if player is in water\n        function checkInWater(pos) {\n          // Check blocks around player's body\n          const checkPoints = [\n            [pos.x, pos.y, pos.z], // Head level\n            [pos.x, pos.y - PLAYER_HEIGHT / 2, pos.z], // Torso level\n            [pos.x, pos.y - PLAYER_HEIGHT, pos.z], // Feet level\n          ];\n\n          for (const [px, py, pz] of checkPoints) {\n            const bx = Math.floor(px);\n            const by = Math.floor(py);\n            const bz = Math.floor(pz);\n\n            if (by >= 0 && by < worldHeight) {\n              const blockType = getBlockAt(bx, by, bz);\n              if (blockType === BLOCK_TYPES.WATER || blockType === BLOCK_TYPES.WATER_FLOW) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }\n\n        // Check if player is in lava\n        function checkInLava(pos) {\n          const checkPoints = [\n            [pos.x, pos.y, pos.z], // Head level\n            [pos.x, pos.y - PLAYER_HEIGHT / 2, pos.z], // Torso level\n            [pos.x, pos.y - PLAYER_HEIGHT, pos.z], // Feet level\n          ];\n\n          for (const [px, py, pz] of checkPoints) {\n            const bx = Math.floor(px);\n            const by = Math.floor(py);\n            const bz = Math.floor(pz);\n\n            if (by >= 0 && by < worldHeight) {\n              const blockType = getBlockAt(bx, by, bz);\n              if (blockType === BLOCK_TYPES.LAVA || blockType === BLOCK_TYPES.LAVA_FLOW) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }\n\n        // Lava damage tracking\n        let isInLava = false;\n        let lastLavaDamageTime = 0;\n        let burnEndTime = 0; // When the burning effect ends\n        const LAVA_DAMAGE_COOLDOWN = 0.5; // Damage every 0.5 seconds\n        const LAVA_DAMAGE_AMOUNT = 4; // 4 damage per tick (2 hearts)\n        const BURNING_DURATION = 2.0; // Burning lasts 2 seconds after leaving lava\n\n        // Day/Night cycle\n        let gameTime = 0;\n        // Will be initialized based on config\n        let currentAmbientMusic = null;\n        let isDay = true;\n\n        function updateDayNight(dt) {\n          if (!config.dayNightCycleEnabled) return;\n\n          gameTime = Math.floor(getTimeStamp() / 1000);\n          const cycleDuration = config.dayNightCycleDuration;\n          const cycleProgress = (gameTime % cycleDuration) / cycleDuration;\n\n          const wasDay = isDay;\n          isDay = cycleProgress < 0.5;\n\n          // Update lighting\n          const dayIntensity = isDay\n            ? 0.6 + 0.4 * Math.sin(cycleProgress * Math.PI * 2)\n            : 0.2 + 0.2 * Math.sin(cycleProgress * Math.PI * 2);\n\n          ambientLight.intensity = Math.max(0.2, dayIntensity);\n          directionalLight.intensity = isDay ? 0.8 : 0.2;\n\n          // Update sky color\n          const dayColor = new THREE.Color(0x87ceeb);\n          const nightColor = new THREE.Color(0x1a1a3a);\n          const skyColor = isDay ? dayColor : nightColor;\n          scene.background = skyColor;\n          scene.fog.color.copy(skyColor);\n\n          // Update sun and moon positions\n          // Both sun and moon travel in an arc across the sky\n          // Sun arc during day (0 to 0.5 cycle progress)\n          // Moon arc during night (0.5 to 1.0 cycle progress)\n          const skyRadius = 90;\n\n          if (cycleProgress < 0.5) {\n            // Day - sun visible\n            const sunProgress = cycleProgress * 2;\n            // 0 to 1 during day\n            const sunAngle = sunProgress * Math.PI;\n            // 0 to PI (sunrise to sunset)\n\n            // Position sun on an arc from east to west\n            sun.position.x = Math.cos(sunAngle) * skyRadius;\n            // East (-90\u00b0) to West (90\u00b0)\n            sun.position.y = Math.sin(sunAngle) * skyRadius;\n            // Arc from horizon to horizon\n            sun.position.z = -skyRadius;\n\n            // Make sun face the camera\n            sun.lookAt(camera.position);\n\n            sun.visible = true;\n            moon.visible = false;\n            stars.visible = false;\n          } else {\n            // Night - moon visible\n            const moonProgress = (cycleProgress - 0.5) * 2;\n            // 0 to 1 during night\n            const moonAngle = moonProgress * Math.PI;\n\n            // Position moon on an arc from east to west\n            moon.position.x = Math.cos(moonAngle) * skyRadius;\n            moon.position.y = Math.sin(moonAngle) * skyRadius;\n            moon.position.z = skyRadius;\n\n            // Make moon face the camera\n            moon.lookAt(camera.position);\n\n            sun.visible = false;\n            moon.visible = true;\n            stars.visible = true;\n          }\n\n          // Keep skybox centered on camera\n          skyboxGroup.position.copy(camera.position);\n\n          // Update time indicator\n          const timeIndicator = document.getElementById('time-indicator');\n          if (timeIndicator) {\n            timeIndicator.textContent = isDay ? '\u2600\ufe0f Day' : '\ud83c\udf19 Night';\n          }\n\n          // Switch music\n          if (wasDay !== isDay && audioContext) {\n            if (currentAmbientMusic) {\n              const oldMusic = currentAmbientMusic;\n              oldMusic.gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1);\n              setTimeout(() => {\n                if (oldMusic && oldMusic.source) {\n                  try {\n                    oldMusic.source.stop();\n                  } catch (e) {}\n                }\n              }, 1000);\n            }\n\n            const musicId = isDay ? 'ambient_day_music' : 'ambient_night_music';\n            currentAmbientMusic = playSound(musicId, true, 0.3);\n          }\n        }\n\n        // Show feedback message\n        function showMessage(text, color = '#4CAF50', duration = 2000) {\n          const messageEl = document.createElement('div');\n          messageEl.style.cssText = `\n                    position: absolute;\n                    top: 50%;\n                    left: 50%;\n                    transform: translate(-50%, -50%);\n                    background: rgba(0,0,0,0.9);\n                    color: ${color};\n                    padding: 20px 40px;\n                    border-radius: 12px;\n                    font-family: 'VT323', monospace;\n                    font-size: 32px;\n                    z-index: 150;\n                    border: 3px solid ${color};\n                    pointer-events: none;\n                    animation: fadeOut ${duration}ms ease-out;\n                `;\n          messageEl.textContent = text;\n          gameWorld.appendChild(messageEl);\n\n          setTimeout(() => {\n            if (messageEl.parentNode) {\n              messageEl.parentNode.removeChild(messageEl);\n            }\n          }, duration);\n        }\n\n        // Save game function\n        async function saveGame() {\n          try {\n            // Preserve unrelated persisted fields (like nickname)\n            const existingResponse = await lib.getUserGameState();\n            const existingState =\n              existingResponse && existingResponse.state && typeof existingResponse.state === 'object'\n                ? existingResponse.state\n                : {};\n\n            const worldState = buildWorldStatePayload();\n\n            const saveData = {\n              ...existingState,\n              version: 5,\n              // Version 5: world state saved/restored through sendWorldState/recieveWorldState format\n              playerPos: {\n                x: playerPos.x,\n                y: playerPos.y,\n                z: playerPos.z,\n              },\n              playerYaw: playerYaw,\n              playerPitch: playerPitch,\n              playerHealth: playerHealth,\n              playerHunger: playerHunger,\n              playerVelocity: {\n                x: playerVelocity.x,\n                y: playerVelocity.y,\n                z: playerVelocity.z,\n              },\n              inventory: JSON.parse(JSON.stringify(inventory)),\n              selectedSlot: selectedSlot,\n              gameTime: gameTime,\n              worldSeed: config.worldSeed,\n              worldState,\n            };\n\n            await lib.saveUserGameState(saveData);\n            showMessage('\u2713 Game Saved!', '#4CAF50', 2000);\n            lib.log(\n              'Game saved successfully with ' + (worldState.modifiedBlocks?.length || 0) + ' modified blocks'\n            );\n          } catch (error) {\n            showMessage('\u2717 Save Failed!', '#F44336', 3000);\n            lib.log(`Failed to save game: ${error.message}`);\n          }\n        }\n        function resetWorld() {\n          // Clear block data maps\n          blockMeshes.clear();\n          modifiedBlocks.clear();\n          signMessages.clear();\n          signOrientations.clear();\n          chestContents.clear();\n          furnaceContents.clear();\n          doorStates.clear();\n          furnaceOrientations.clear();\n          chestOrientations.clear();\n\n          // Clear chunks\n          chunks.clear();\n\n          // Clear instanced meshes - reset count and internal maps\n          instancedMeshes.forEach((data, blockType) => {\n            data.blockPositions.clear();\n            data.dirtyInstances.clear();\n            data.count = 0;\n            data.mesh.count = 0;\n          });\n\n          // Clear sign meshes from scene\n          signMeshes.forEach((mesh) => {\n            scene.remove(mesh);\n          });\n          signMeshes.clear();\n          signOrientations.clear();\n\n          // Clear door meshes from scene\n          doorMeshes.forEach((mesh) => {\n            scene.remove(mesh);\n          });\n          doorMeshes.clear();\n          doorOrientations.clear();\n\n          // Clear furnace meshes from scene\n          furnaceMeshes.forEach((mesh) => {\n            scene.remove(mesh);\n          });\n          furnaceMeshes.clear();\n\n          // Clear chest meshes from scene\n          chestMeshes.forEach((mesh) => {\n            scene.remove(mesh);\n          });\n          chestMeshes.clear();\n\n          // Clear bed meshes from scene\n          bedMeshes.forEach((mesh) => {\n            scene.remove(mesh);\n          });\n          bedMeshes.clear();\n\n          // Clear TV meshes from scene\n          tvMeshes.forEach((mesh) => {\n            scene.remove(mesh);\n          });\n          tvMeshes.clear();\n\n          // Clear flower meshes from scene\n          flowerMeshes.forEach((mesh) => {\n            scene.remove(mesh);\n          });\n          flowerMeshes.clear();\n\n          // Clear furnace lights and their extra meshes\n          furnaceLights.forEach((data) => {\n            if (data.light) scene.remove(data.light);\n            if (data.frontMesh) scene.remove(data.frontMesh);\n            if (data.glowMesh) scene.remove(data.glowMesh);\n          });\n          furnaceLights.clear();\n\n          // Clear torch lights and meshes\n          torchLights.forEach((light, key) => {\n            const parts = key.split(',').map(Number);\n            removeTorchLight(parts[0], parts[1], parts[2]);\n          });\n          torchLights.clear();\n          torchMeshes.clear();\n\n          // Clear lava lights\n          lavaLights.forEach((light, key) => {\n            const parts = key.split(',').map(Number);\n            removeLavaLight(parts[0], parts[1], parts[2]);\n          });\n          lavaLights.clear();\n\n          // Regenerate terrain\n          generateTerrain();\n          buildWorldMesh();\n        }\n        window.resetWorld = resetWorld;\n\n        // Load game function\n        async function loadGame() {\n          try {\n            const response = await lib.getUserGameState();\n\n            if (!response.state) {\n              showMessage('\u26a0 No saved game!', '#FF9800', 2500);\n              lib.log('No saved game found');\n              return;\n            }\n\n            const saveData = response.state;\n\n            const normalizeEntryArray = (value) => (Array.isArray(value) ? value : []);\n\n            // Regenerate world from seed\n            if (typeof saveData.worldSeed === 'number') {\n              lib.log('Regenerating world from seed: ' + saveData.worldSeed);\n              config.worldSeed = saveData.worldSeed;\n            }\n\n            // Prefer new format (worldState), but support legacy saves\n            let worldStatePayload =\n              saveData.worldState && typeof saveData.worldState === 'object'\n                ? saveData.worldState\n                : null;\n\n            if (!worldStatePayload) {\n              const legacyModifiedBlocks = Array.isArray(saveData.modifiedBlocks)\n                ? saveData.modifiedBlocks\n                    .map((mod) => {\n                      if (Array.isArray(mod) && mod.length === 2) return mod;\n                      if (\n                        mod &&\n                        typeof mod === 'object' &&\n                        typeof mod.x === 'number' &&\n                        typeof mod.y === 'number' &&\n                        typeof mod.z === 'number'\n                      ) {\n                        return [`${mod.x},${mod.y},${mod.z}`, mod.blockType];\n                      }\n                      return null;\n                    })\n                    .filter(Boolean)\n                : [];\n\n              worldStatePayload = {\n                modifiedBlocks: legacyModifiedBlocks,\n                signMessages: normalizeEntryArray(saveData.signMessages),\n                signOrientations: normalizeEntryArray(saveData.signOrientations),\n                chestContents: normalizeEntryArray(saveData.chestContents),\n                furnaceContents: normalizeEntryArray(saveData.furnaceContents),\n                doorStates: normalizeEntryArray(saveData.doorStates),\n                doorOrientations: normalizeEntryArray(saveData.doorOrientations),\n                furnaceOrientations: normalizeEntryArray(saveData.furnaceOrientations),\n                chestOrientations: normalizeEntryArray(saveData.chestOrientations),\n              };\n            }\n\n            // Apply world using the same logic used for room sync\n            recieveWorldState(worldStatePayload, { forceApply: true });\n\n            // Rebuild visible blocks\n            updateVisibleBlocks();\n\n            // Restore player position and rotation\n            if (saveData.playerPos) {\n              playerPos.set(saveData.playerPos.x, saveData.playerPos.y, saveData.playerPos.z);\n            }\n            if (typeof saveData.playerYaw === 'number') playerYaw = saveData.playerYaw;\n            if (typeof saveData.playerPitch === 'number') playerPitch = saveData.playerPitch;\n\n            // Restore player velocity if available\n            if (saveData.playerVelocity) {\n              playerVelocity.set(\n                saveData.playerVelocity.x,\n                saveData.playerVelocity.y,\n                saveData.playerVelocity.z\n              );\n            }\n\n            // Restore player health\n            if (typeof saveData.playerHealth === 'number') {\n              playerHealth = saveData.playerHealth;\n            }\n            updateHealthBar();\n\n            // Restore player hunger\n            if (saveData.playerHunger !== undefined) {\n              playerHunger = saveData.playerHunger;\n            } else {\n              playerHunger = maxPlayerHunger; // Default to full if not saved\n            }\n            updateHungerBar();\n\n            // Restore inventory\n            if (Array.isArray(saveData.inventory)) {\n              inventory = JSON.parse(JSON.stringify(saveData.inventory));\n            }\n            // Ensure we have 36 slots total\n            while (inventory.length < 36) {\n              inventory.push({ type: BLOCK_TYPES.AIR, count: 0 });\n            }\n            selectedSlot = typeof saveData.selectedSlot === 'number' ? saveData.selectedSlot : 0;\n            updateHotbarUI();\n\n            // Restore game time\n            if (saveData.gameTime !== undefined) {\n              gameTime = saveData.gameTime;\n            }\n\n            // Update camera\n            camera.position.copy(playerPos);\n            camera.rotation.order = 'YXZ';\n            camera.rotation.y = playerYaw;\n            camera.rotation.x = playerPitch;\n\n            showMessage('\u2713 Game Loaded!', '#2196F3', 2000);\n            lib.log('Game loaded successfully');\n          } catch (error) {\n            showMessage('\u2717 Load Failed!', '#F44336', 3000);\n            lib.log(`Failed to load game: ${error.message}`);\n          }\n        }\n\n        // Delete save function\n        async function deleteSave() {\n          try {\n            // Show confirmation message\n            const confirmEl = document.createElement('div');\n            confirmEl.style.cssText = `\n                        position: absolute;\n                        top: 50%;\n                        left: 50%;\n                        transform: translate(-50%, -50%);\n                        background: rgba(20, 20, 30, 0.98);\n                        color: white;\n                        padding: 30px 40px;\n                        border-radius: 16px;\n                        text-align: center;\n                        z-index: 200;\n                        border: 3px solid #F44336;\n                        min-width: 350px;\n                    `;\n            confirmEl.innerHTML = `\n                        <h3 style=\"font-family: 'VT323', monospace; font-size: 32px; margin-bottom: 16px; color: #F44336;\">Delete Save?</h3>\n                        <p style=\"font-family: 'Inter', sans-serif; font-size: 18px; margin-bottom: 24px; opacity: 0.9;\">This cannot be undone!</p>\n                        <div style=\"display: flex; gap: 12px; justify-content: center;\">\n                            <button id=\"delete-confirm\" style=\"\n                                font-family: 'VT323', monospace;\n                                font-size: 24px;\n                                padding: 10px 24px;\n                                background: #F44336;\n                                color: white;\n                                border: none;\n                                border-radius: 8px;\n                                cursor: pointer;\n                            \">Delete</button>\n                            <button id=\"delete-cancel\" style=\"\n                                font-family: 'VT323', monospace;\n                                font-size: 24px;\n                                padding: 10px 24px;\n                                background: rgba(255,255,255,0.1);\n                                color: white;\n                                border: 2px solid rgba(255,255,255,0.3);\n                                border-radius: 8px;\n                                cursor: pointer;\n                            \">Cancel</button>\n                        </div>\n                    `;\n            gameWorld.appendChild(confirmEl);\n\n            // Handle button clicks\n            document.getElementById('delete-confirm').addEventListener('click', async () => {\n              confirmEl.remove();\n              try {\n                await lib.deleteUserGameState();\n                showMessage('\u2713 Save Deleted!', '#F44336', 2000);\n                lib.log('Save deleted successfully');\n              } catch (error) {\n                showMessage('\u2717 Delete Failed!', '#F44336', 3000);\n                lib.log(`Failed to delete save: ${error.message}`);\n              }\n            });\n\n            document.getElementById('delete-cancel').addEventListener('click', () => {\n              confirmEl.remove();\n            });\n          } catch (error) {\n            showMessage('\u2717 Error!', '#F44336', 3000);\n            lib.log(`Error showing delete confirmation: ${error.message}`);\n          }\n        }\n\n        // Edit mode\n        function setupEditMode() {\n          // Create edit overlay\n          const overlay = document.createElement('div');\n          overlay.className = 'edit-overlay';\n          overlay.innerHTML = `\n                    <h2>\u2699\ufe0f Edit Mode</h2>\n                    <p>Customize your world settings below</p>\n                    <p>Then switch to Play to explore!</p>\n                `;\n          gameWorld.appendChild(overlay);\n\n          // Show game parameters\n          lib.showGameParameters({\n            name: 'Game Settings',\n            params: {\n              Version: {\n                key: 'version',\n                type: 'string',\n                onChange: (value) => {\n                  config.version = value;\n                  const versionDisplay = document.getElementById('version-display');\n                  if (versionDisplay) {\n                    versionDisplay.textContent = 'v' + value;\n                  }\n                },\n              },\n              'World Seed': {\n                key: 'worldSeed',\n                type: 'number',\n                min: 0,\n                max: 999999,\n                step: 1,\n              },\n              'Tree Density': {\n                key: 'treeDensity',\n                type: 'slider',\n                min: 0,\n                max: 10,\n                step: 0.5,\n                onChange: (value) => {\n                  config.treeDensity = value / 100;\n                },\n              },\n              'Mining Speed': {\n                key: 'miningSpeedMultiplier',\n                type: 'slider',\n                min: 0.5,\n                max: 3,\n                step: 0.25,\n              },\n              'Movement Speed': {\n                key: 'movementSpeedMultiplier',\n                type: 'slider',\n                min: 0.5,\n                max: 2,\n                step: 0.25,\n              },\n              'Hunger Rate': {\n                key: 'hungerRateMultiplier',\n                type: 'slider',\n                min: 0.25,\n                max: 3,\n                step: 0.25,\n              },\n              'Fall Damage Height': {\n                key: 'fallDamageThreshold',\n                type: 'slider',\n                min: 0,\n                max: 10,\n                step: 0.5,\n              },\n              'Day/Night Cycle': {\n                key: 'dayNightCycleEnabled',\n                type: 'checkbox',\n              },\n              'Cycle Duration (sec)': {\n                key: 'dayNightCycleDuration',\n                type: 'slider',\n                min: 60,\n                max: 600,\n                step: 30,\n              },\n              'Start Time': {\n                key: 'startTimeOfDay',\n                type: 'dropdown',\n                options: [\n                  {\n                    label: 'Day',\n                    value: 'day',\n                  },\n                  {\n                    label: 'Night',\n                    value: 'night',\n                  },\n                ],\n              },\n              'Zombies Enabled': {\n                key: 'zombiesEnabled',\n                type: 'checkbox',\n              },\n              'Creepers Enabled': {\n                key: 'creepersEnabled',\n                type: 'checkbox',\n              },\n              'Water Flow Distance': {\n                key: 'waterFlowDistance',\n                type: 'slider',\n                min: 1,\n                max: 12,\n                step: 1,\n                onChange: (value) => {\n                  config.waterFlowDistance = value;\n                },\n              },\n              'Villages Enabled': {\n                key: 'villagesEnabled',\n                type: 'checkbox',\n              },\n              'Chickens Enabled': {\n                key: 'chickensEnabled',\n                type: 'checkbox',\n              },\n              'Cows Enabled': {\n                key: 'cowsEnabled',\n                type: 'checkbox',\n              },\n              'Dogs Enabled': {\n                key: 'dogsEnabled',\n                type: 'checkbox',\n              },\n              'Flower Density': {\n                key: 'flowerDensity',\n                type: 'slider',\n                min: 0,\n                max: 0.08,\n                step: 0.005,\n              },\n              'Flower Cluster Size': {\n                key: 'flowerClusterSize',\n                type: 'slider',\n                min: 3,\n                max: 12,\n                step: 1,\n              },\n              'Flower Cluster Density': {\n                key: 'flowerClusterDensity',\n                type: 'slider',\n                min: 0.05,\n                max: 0.5,\n                step: 0.05,\n              },\n              'LOD Enabled': {\n                key: 'lodEnabled',\n                type: 'checkbox',\n              },\n              'LOD Near Distance': {\n                key: 'lodNearDistance',\n                type: 'slider',\n                min: 30,\n                max: 150,\n                step: 10,\n              },\n              'LOD Medium Distance': {\n                key: 'lodMediumDistance',\n                type: 'slider',\n                min: 80,\n                max: 250,\n                step: 10,\n              },\n              'LOD Far Distance': {\n                key: 'lodFarDistance',\n                type: 'slider',\n                min: 150,\n                max: 500,\n                step: 25,\n              },\n              'Chunk Deletion Buffer (ms)': {\n                key: 'chunkDeletionBufferMs',\n                type: 'slider',\n                min: 0,\n                max: 5000,\n                step: 250,\n              },\n              'Show Worlds UI': {\n                key: 'showWorldsUI',\n                type: 'checkbox',\n                onChange: (value) => {\n                  config.showWorldsUI = value;\n                  const worldsBtn = document.getElementById('worlds-button');\n                  const worldDisplay = document.getElementById('current-world-display');\n                  if (worldsBtn) worldsBtn.style.display = value ? 'flex' : 'none';\n                  if (worldDisplay) worldDisplay.style.display = value ? 'block' : 'none';\n                },\n              },\n              'Online Multiplayer': {\n                key: 'onlineEnabled',\n                type: 'checkbox',\n              },\n              'Firebase Prefix': {\n                key: 'firebasePrefix',\n                type: 'string',\n              },\n            },\n          });\n\n          // Set up camera for overview (in edit mode, show center of world at origin)\n          camera.position.set(0, worldHeight + 10, 20);\n          camera.lookAt(0, 10, 0);\n        }\n\n        // Game loop\n        let lastTime = 0;\n\n        function gameLoop(timestamp) {\n          let dt = (timestamp - lastTime) / 1000;\n          lastTime = timestamp;\n\n          // Clamp delta time\n          dt = Math.min(dt, 0.11);\n\n          if (mode === 'play') {\n            // Always update furnaces so they smelt while UI is open\n            updateFurnaces(dt);\n\n            if (\n              !isPaused &&\n              !isInventoryOpen &&\n              !isCraftingTableOpen &&\n              !isChestOpen &&\n              !isFurnaceOpen &&\n              !isSleeping\n            ) {\n              const currentTime = timestamp / 1000;\n\n              // Update player\n              updatePlayer(dt);\n\n              // Check obby checkpoints\n              checkObbyCheckpoints();\n\n              // Track if player is moving for hunger calculation\n              const isPlayerMoving =\n                Math.abs(playerVelocity.x) > 0.1 || Math.abs(playerVelocity.z) > 0.1;\n\n              // Update hunger system\n              updateHunger(dt, isPlayerMoving, currentTime);\n\n              // Cleanup old terrain chunks periodically\n              if (currentTime - lastChunkCleanup > CHUNK_CLEANUP_INTERVAL / 1000) {\n                cleanupTerrainChunks(playerPos.x, playerPos.z);\n                lastChunkCleanup = currentTime;\n              }\n\n              // Cleanup inactive players\n              if (currentTime * 1000 - lastCleanupTime > CLEANUP_INTERVAL) {\n                const now = performance.now();\n                const pidsToRemove = [];\n                otherPlayers.forEach((data, pid) => {\n                  if (now - (data.localReceiveTime || 0) > PLAYER_TIMEOUT) {\n                    pidsToRemove.push(pid);\n                  }\n                });\n                pidsToRemove.forEach((pid) => {\n                  otherPlayers.delete(pid);\n                  playerTargetPositions.delete(pid);\n                  removeOtherPlayerMesh(pid);\n                  visiblePlayerIds.delete(pid);\n                });\n                lastCleanupTime = currentTime * 1000;\n              }\n\n              // Update coordinates display\n              const coordsDisplay = document.getElementById('coords-display');\n              if (coordsDisplay) {\n                const x = Math.floor(playerPos.x * 10) / 10;\n                const y = Math.floor(playerPos.y * 10) / 10;\n                const z = Math.floor(playerPos.z * 10) / 10;\n                coordsDisplay.textContent = `X: ${x} Y: ${y} Z: ${z}`;\n              }\n\n              // Update player count display\n              const playerCountEl = document.getElementById('player-count');\n              if (playerCountEl) {\n                const onlineCount = otherPlayers.size + 1;\n                playerCountEl.textContent = t('playersOnline', { count: onlineCount });\n              }\n\n              // Update camera matrix before frustum culling\n              camera.updateMatrixWorld();\n\n              // Throttle updateVisibleBlocks: only when player moves >0.8 blocks or every 150ms\n              const _now = performance.now();\n              const _dx = playerPos.x - _lastVBPos.x;\n              const _dz = playerPos.z - _lastVBPos.z;\n              const _movedSq = _dx*_dx + _dz*_dz;\n              if (_movedSq > 0.64 || _now - _lastVBTime > 150) {\n                updateVisibleBlocks();\n                _lastVBPos.x = playerPos.x; _lastVBPos.z = playerPos.z;\n                _lastVBTime = _now;\n              }\n\n              // Update mobs\n              updateMobs(dt, currentTime);\n              lerpRemoteMobs(dt);\n\n              // Update target block\n              targetBlock = getTargetBlock();\n\n              if (targetBlock) {\n                highlightMesh.visible = true;\n                highlightMesh.position.set(\n                  targetBlock.x + 0.5,\n                  targetBlock.y + 0.5,\n                  targetBlock.z + 0.5\n                );\n              } else {\n                highlightMesh.visible = false;\n              }\n\n              // Mining\n              if (isMining && targetBlock) {\n                mineBlock(dt);\n              } else {\n                miningProgress = 0;\n                const progressEl = document.querySelector('.mining-progress');\n                if (progressEl) {\n                  progressEl.classList.remove('active');\n                }\n              }\n\n              // Update particles\n              updateParticles(dt);\n\n              // Update arrow weapon projectiles\n              updateArrows(dt);\n\n              // Update torch mesh animations\n              updateTorchMeshes(dt);\n\n              // Update active lights (limit on mobile for performance)\n              updateActiveLights();\n\n              // Update furnace visual effects (smoke, fire particles, light flicker)\n              updateFurnaceVisuals(dt);\n\n              // Update dynamite countdowns\n              updateDynamiteCountdowns(dt);\n\n              // Update water spread\n              updateWaterSpread();\n\n              // Update lava spread\n              updateLavaSpread();\n\n              // Update day/night\n              updateDayNight(dt);\n\n              // Update local player model (3rd person)\n              updateLocalPlayerModel(dt);\n\n              // Update chat bubbles (for own player and other players)\n              updateChatBubbles(dt);\n\n              // Sync player position with Lobby\n              if (lobby) {\n                lobby.sendMessage(\n                  'position',\n                  {\n                    playerId: myPlayerId,\n                    playerName: myPlayerName,\n                    x: playerPos.x,\n                    y: playerPos.y,\n                    z: playerPos.z,\n                    yaw: playerYaw,\n                    pitch: playerPitch,\n                    gender: playerGender,\n                  },\n                  0\n                );\n                renderOtherPlayers();\n              }\n            } // Close the !isPaused block\n          } // Close the mode === 'play' block\n\n          // Render\n          const renderStart = performance.now();\n          renderer.render(scene, camera);\n          renderTime = performance.now() - renderStart;\n\n          // Update FPS\n          const now = performance.now();\n          frameCount++;\n          if (now - fpsUpdateTime >= 500) {\n            fps = Math.round((frameCount * 1000) / (now - fpsUpdateTime));\n            frameCount = 0;\n            fpsUpdateTime = now;\n          }\n          lastFrameTime = now;\n\n          // Update debug UI\n          if (debugEnabled) {\n            debugUI.innerHTML = `FPS: ${fps}<br>Render: ${renderTime.toFixed(1)}ms<br>Rays: ${iteratedBlocks}<br>Blocks: ${visibleMeshes}<br>Chunks: ${visibleChunks}/${chunks.size}<br>Culled: ${culledChunks}<br>Batches: ${batchedDrawCalls}<br>LOD Full: ${lodFullChunks}<br>LOD Med: ${lodMediumChunks}<br>LOD Far: ${lodFarChunks}<br>Modified: ${modifiedBlocks.size}<br>Remixed: ${remixed}`;\n          }\n\n          requestAnimationFrame(gameLoop);\n        }\n\n        // ===== MAIN INITIALIZATION =====\n        (async function initialize() {\n          const preloading = preloadAssets();\n\n          try {\n            const { gameId } = await lib.getUserGameState();\n            config.firebasePrefix = gameId;\n          } catch (e) {\n            config.firebasePrefix = 'sandbox';\n          }\n\n          await preloading;\n\n          updateLoadingProgress(32, 'creatingTextures');\n          createBlockTextures();\n\n          // Initialize infinite world (no pre-loading needed)\n          generateTerrain();\n\n          buildWorldMesh();\n\n          updateLoadingProgress(90, 'settingUpControls');\n          createUI();\n          setupInputHandlers();\n\n          if (mode === 'edit') {\n            updateLoadingProgress(100, 'ready');\n            setupEditMode();\n          } else {\n            updateLoadingProgress(95, 'findingSpawn');\n            playerPos = findSpawnPoint();\n            spawnPoint = playerPos.clone();\n            playerYaw = Math.PI; // Face toward course (+Z direction)\n            playerPitch = 0;\n            // Store spawn point for respawning\n            camera.position.copy(playerPos);\n\n            // Create 3rd person player model\n            createLocalPlayerModel();\n\n            // Initialize Firebase online\n            initFirebase();\n            if (isOnline) {\n              window.roomManager = new RoomManager(\n                window.gameConfig.firebaseConfig,\n                'rooms',\n                config.firebasePrefix\n              );\n              listRooms();\n              subscribeToEvents();\n            }\n\n            // Load persisted nickname on startup (play mode)\n            await loadNickname();\n\n            // Initialize game time based on start time of day config\n            if (config.startTimeOfDay === 'night') {\n              gameTime = config.dayNightCycleDuration / 2;\n              // Start at night (halfway through cycle)\n              isDay = false;\n            } else {\n              gameTime = 0;\n              // Start at day (beginning of cycle)\n              isDay = true;\n            }\n\n            // Hide instructions after a few seconds\n            setTimeout(() => {\n              const instructions = document.getElementById('instructions');\n              if (instructions && (isPointerLocked || isTouchDevice)) {\n                instructions.classList.add('hidden');\n              }\n            }, 5000);\n          }\n\n          updateLoadingProgress(100, 'complete');\n\n          // Hide loading screen after a short delay\n          setTimeout(() => {\n            hideLoadingScreen();\n          }, 300);\n\n          // Start game loop\n          requestAnimationFrame(gameLoop);\n        })();\n      }\n      window.run = run;\n    </script>\n  </body>\n</html>\n", "game_config": "{\n  \"firebaseConfig\": {\n    \"apiKey\": \"AIzaSyAH2iXY79IOKr-ssC6hV1cfUho-_oKJgCg\",\n    \"authDomain\": \"lime-for-group-1567103664368.firebaseapp.com\",\n    \"databaseURL\": \"wss://lime-for-group-1567103664368.firebaseio.com\",\n    \"projectId\": \"lime-for-group-1567103664368\"\n  },\n  \"version\": \"1.86.4\",\n  \"worldWidth\": 640,\n  \"worldDepth\": 640,\n  \"worldHeight\": 64,\n  \"treeDensity\": 0.02,\n  \"renderDistance\": 40,\n  \"raycastSpreadMultiplier\": 1,\n  \"raycastPenetration\": 1,\n  \"rayCountX\": 24,\n  \"rayCountY\": 24,\n  \"miningSpeedMultiplier\": 1,\n  \"movementSpeedMultiplier\": 1,\n  \"dayNightCycleEnabled\": false,\n  \"dayNightCycleDuration\": 600,\n  \"fallDamageThreshold\": 999,\n  \"villagesEnabled\": false,\n  \"inventory\": [\n    {\n      \"type\": 17,\n      \"count\": 64\n    },\n    {\n      \"count\": 64,\n      \"type\": 3\n    },\n    {\n      \"count\": 64,\n      \"type\": 4\n    },\n    {\n      \"count\": 1,\n      \"type\": 9\n    },\n    {\n      \"count\": 128,\n      \"type\": 6\n    },\n    {\n      \"type\": 15,\n      \"count\": 2\n    },\n    {\n      \"count\": 10,\n      \"type\": 11\n    },\n    {\n      \"type\": 24,\n      \"count\": 5\n    },\n    {\n      \"count\": 1,\n      \"type\": 14\n    },\n    {\n      \"type\": 29,\n      \"count\": 8\n    }\n  ],\n  \"worldSeed\": 12346,\n  \"worldData\": null,\n  \"startTimeOfDay\": \"day\",\n  \"zombiesEnabled\": false,\n  \"villagersEnabled\": true,\n  \"creepersEnabled\": false,\n  \"forceMobileUI\": false,\n  \"onlineEnabled\": true,\n  \"firebasePrefix\": \"01KK8ZR1F8D71E698WDVMWF1Y4\",\n  \"showWorldsUI\": true,\n  \"lodNearDistance\": 40,\n  \"lodMediumDistance\": 80,\n  \"lodFarDistance\": 160,\n  \"lodEnabled\": true,\n  \"chunkDeletionBufferMs\": 1500,\n  \"hungerRateMultiplier\": 0,\n  \"chickensEnabled\": false,\n  \"cowsEnabled\": false,\n  \"waterFlowDistance\": 1,\n  \"dogsEnabled\": false\n}", "game_design_document": "## Core Game Loop and Mechanics\n\n**Block Builder** is a creative voxel sandbox game that captures the essence of block-based building and exploration. The core loop consists of:\n\n1. **Exploration**: Players navigate a procedurally generated voxel world with varied terrain (grass, dirt, stone, trees)\n2. **Resource Gathering**: Players mine blocks by tapping/clicking and holding on them - different blocks take different amounts of time to break\n3. **Inventory Management**: Collected blocks go into a hotbar inventory system\n4. **Building**: Players place blocks from their inventory to create structures\n5. **Progression**: As players explore, they discover new biomes and can create increasingly complex structures\n\n**What Makes It Compelling:**\n- Immediate creative freedom - build anything you imagine\n- Satisfying block-breaking feedback with visual and audio cues\n- Procedural world generation ensures endless exploration\n- Simple mechanics that are easy to learn but offer deep creative potential\n\n**Win/Lose Conditions:**\n- This is a creative sandbox with no traditional win/lose state\n- Success is measured by player creativity and building achievements\n- Players can set their own goals (build a house, create pixel art, explore distant biomes)\n\n## Starting Scope\n\nFor the initial implementation, we'll focus on a **highly polished, playable creative mode** with these core features:\n\n**Must Have (First Iteration):**\n- Fixed-size flat world (64x64 blocks wide, 32 blocks tall) with simple terrain generation (grass layer, dirt, stone underneath)\n- 6 basic block types: Grass, Dirt, Stone, Wood, Leaves, Air\n- First-person 3D perspective with smooth camera controls\n- Mining blocks (tap/click and hold, with progress indicator)\n- Placing blocks from inventory\n- Hotbar inventory system (9 slots visible at bottom)\n- Touch controls: left joystick for movement, right joystick for camera look, tap block to mine/place\n- Desktop controls: WASD movement, mouse look with pointer lock, left-click to mine, right-click to place\n- Simple day/night cycle with ambient lighting changes\n- Block breaking particles and sound effects\n\n**Future Iterations:**\n- Infinite world generation with multiple biomes\n- Crafting system for tools and items\n- More block varieties (ores, building materials, decorative blocks)\n- Survival mode with health, hunger, and enemies\n- Multiplayer capabilities\n- Advanced building features (copy/paste, fill tools)\n\n## Game Perspective\n\n**3D First-Person Perspective**\n\nThe game uses a first-person 3D voxel view where:\n- Camera is positioned at player eye level\n- Players look around with mouse/right joystick\n- Blocks are rendered as 3D cubes in a grid\n- Crosshair in center indicates which block player is targeting\n- View distance shows surrounding terrain clearly\n\nThis perspective is ideal because:\n- It's immersive and puts players \"in\" the world\n- It's the standard for block-building games\n- It works well for both building and exploration\n- Touch controls translate naturally (joystick movement + look)\n\n## Game World and Level Design\n\n**World Structure:**\n\nThe world is a 3D voxel grid (64 blocks wide \u00d7 64 blocks deep \u00d7 32 blocks tall):\n\n- **Terrain Generation**: Simple procedural generation creates varied terrain\n  - Top layer: Grass blocks (y=10-14, varied height using noise)\n  - Middle layers: Dirt blocks (3-5 blocks deep under grass)\n  - Bottom layers: Stone blocks (filling to y=0, bedrock-like base)\n  - Trees: Randomly placed on grass (wood trunk 4-6 blocks tall, leaf crown on top)\n  \n- **Spawn Point**: Player starts at world center (32, highest_grass_block+2, 32) facing a scenic direction\n\n- **World Boundaries**: Invisible walls at world edges prevent falling off\n\n- **Lighting**: \n  - Ambient lighting changes with day/night cycle\n  - Daytime: bright, clear visibility\n  - Nighttime: darker, atmospheric (but still playable)\n  - No complex shadow system in first iteration\n\n**Block Grid System:**\n- Each block occupies exactly 1\u00d71\u00d71 unit in world space\n- Blocks snap to integer coordinates\n- Air blocks are empty spaces (not rendered but tracked)\n\n## Edit Mode Design\n\nIn Edit mode, players can customize:\n\n**World Properties:**\n- **World Size**: Adjust width/depth (16-128 blocks) and height (16-64 blocks)\n- **Terrain Style**: Choose from presets (Flat, Hills, Mountains, Islands)\n- **Tree Density**: Slider for how many trees generate (0-100%)\n\n**Gameplay Properties:**\n- **Mining Speed**: Multiplier for how fast blocks break (0.5x - 3x)\n- **Movement Speed**: Player walk speed multiplier (0.5x - 2x)\n- **Day/Night Cycle**: Toggle on/off, adjust cycle duration (1-10 minutes)\n\n**Block Palette:**\n- Enable/disable specific block types\n- Adjust starting inventory (which blocks and quantities)\n\n**Constraints:**\n- World size limited to prevent performance issues\n- Minimum 16\u00d716\u00d716 to ensure playability\n- Mining speed capped to maintain game feel\n- At least 3 block types must be enabled\n\n## Control Design\n\n**Desktop Controls:**\n- **WASD**: Move forward/left/backward/right\n- **Mouse Movement**: Look around (with pointer lock)\n- **Left Click (Hold)**: Mine block (break block player is looking at)\n- **Right Click**: Place block from selected hotbar slot\n- **Number Keys 1-9**: Select hotbar slot\n- **Mouse Wheel**: Cycle through hotbar slots\n- **ESC**: Release pointer lock / Open menu\n- **Space**: Jump (future iteration)\n\n**Mobile Controls:**\n- **Left Joystick**: Movement (appears where left side of screen is touched)\n  - Analog movement in all directions\n  - Base moves with finger if exceeds max radius\n  - Dead zone in center\n- **Right Joystick**: Camera look (appears where right side of screen is touched)\n  - Analog look control\n  - Base moves with finger if exceeds max radius\n  - Vertical and horizontal rotation\n- **Tap Block**: Mine (tap and hold on block in world) or Place (quick tap on surface)\n- **Hotbar UI**: Tap to select different block types\n- **Menu Button**: Top-left corner button to pause/access settings\n\n**Control Implementation Details:**\n\n1. **Pointer Lock (Desktop)**:\n   - Automatically request pointer lock on first click in game area\n   - Release on ESC or when menu opens\n   - Mouse movement directly controls camera rotation\n   - Sensitivity adjustable in settings\n\n2. **Touch Joysticks (Mobile)**:\n   - Left joystick (movement): appears in bottom-left third of screen\n   - Right joystick (look): appears in bottom-right two-thirds of screen\n   - Each joystick:\n     - Base position = initial touch point\n     - Max radius = 80px\n     - Dead zone = 15px\n     - If finger exceeds max radius, base slides to keep knob at max radius\n   - Track touch IDs to prevent interference\n   - Visual feedback: semi-transparent circles showing base and knob\n\n3. **Block Interaction**:\n   - Raycast from camera center to detect which block player is looking at\n   - Highlight targeted block with outline\n   - Mining: Hold input for duration (1-3 seconds depending on block type)\n     - Show progress bar on block\n     - Play mining sound and particles\n   - Placing: Instant on input release\n     - Place on face of targeted block\n     - Prevent placing inside player\n\n4. **Input State Management**:\n   - Clear all inputs on window blur\n   - Normalize key codes to lowercase\n   - Prevent default browser behavior for game keys\n   - Handle both keyboard and mouse simultaneously on desktop\n\n## Art Style and Visual Theme\n\n**Art Style: Colorful Voxel/Blocky**\n\nThe game embraces a clean, colorful voxel aesthetic that's immediately recognizable and appealing:\n\n- **Visual Style**: Crisp, solid-colored blocks with subtle texture variations\n- **Color Palette**: Vibrant but natural colors (bright grass green, earthy brown dirt, cool gray stone, warm wood tones)\n- **Mood**: Cheerful, creative, and inviting - encourages experimentation\n- **Lighting**: Soft ambient lighting with day/night transitions (warm daylight, cool moonlight)\n\n**Asset Rendering Strategy:**\n\n- **Blocks**: Rendered with code (WebGL/Three.js)\n  - Each block type has a solid color or simple texture\n  - Faces only rendered when adjacent to air (optimization)\n  - Ambient occlusion for depth (subtle darkening in corners)\n  \n- **Textures**: Generated image assets for block faces\n  - Each block type gets a tileable texture (16\u00d716 or 32\u00d732 pixels)\n  - Pixel art style with subtle detail\n  - Applied to cube faces in 3D rendering\n\n- **UI Elements**: Mix of CSS and generated assets\n  - Hotbar: CSS with generated slot backgrounds\n  - Crosshair: Simple CSS\n  - Icons: Generated pixel art for each block type\n\n- **Particles**: Code-rendered (small cubes that match block color)\n\n- **Sky**: Gradient rendered with code (changes with day/night)\n\n**Consistency Requirements:**\n- All block textures share the same pixel art style and resolution\n- Color palette remains consistent across all blocks\n- Viewing angle: Top-down for block face textures (they'll be applied to 3D cubes)\n- All textures need transparent backgrounds where applicable", "persistent_storage_config": {"leaderboard": {"sort_ascending": true}}, "game_metadata": {"game_id": "01KTY3GF4VFG3QVQS7W348GRXD", "title": "Minecraft obby", "description": "Mine resources and build structures in this creative voxel sandbox where every block is yours to shape!", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_128a449c-dd44-4aaa-ad3f-14206971c8aa.webp", "created_at": 1781278692, "updated_at": 1781979315312.7737, "art_style": null, "is_remix": true, "base_game_id": "01KK8ZR1F8D71E698WDVMWF1Y4"}, "task_plan": null}