{"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.0, "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_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/zombie_texture_ed6026d2-5a16-4ac3-95f3-1866a06a11a5.webp", "type": "image", "prompt": "Generate a pixel art texture sheet for a blocky humanoid zombie character, suitable for a Minecraft-style voxel game. The design features decaying green and gray skin tones with darker patches, simple block-like features, dark empty eye sockets, and a menacing expression. Professional 32x32 pixel art style, clean edges, vibrant but undead color palette. The character should be shown in a full-body standing illustration, front-facing, to be used as a texture reference for a cubic mob. Transparent background.", "description": "A 32x32 pixel art texture for a zombie mob with decaying green skin and blocky features.", "subtype": "character", "aspect_ratio": [583, 881], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "creeper_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/creeper_texture_17d2e713-2e7d-41c6-bc26-4168d3e31768.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for a creeper mob character. The design features a bright green base with a darker green mottled camouflage pattern. Include the iconic creeper face: two black square eyes, two black vertical lines for a nose, and a frowning upside-down T-shaped mouth. Use clean, bold pixel art with no shading gradients to match the existing blocky voxel game style. The texture should be game-ready and professional quality.", "description": "A 32x32 pixel art texture for a creeper mob, matching the Minecraft voxel aesthetic.", "subtype": "character", "aspect_ratio": [625, 961], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "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.0, "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://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/player_hurt_sound_1a6480aa-755a-4be4-a069-2220a8821cdc.mp3", "type": "audio", "prompt": "A quick, punchy 'oof' vocal grunt for a player taking damage. Low-fidelity, arcade-style impact sound, non-graphic and cartoonish. Similar to classic voxel sandbox games. Short, sharp delivery with a clean tail, approximately 0.5 seconds long, conveying a sudden hit without being disturbing or realistic.", "description": "A short, arcade-style vocal grunt for player damage, consistent with voxel sandbox aesthetics.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0.6802494331065759, "loop": false, "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}, "zombie_head_top": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774479257670_01KMKK9C26GP3AMDJDEVDZ3XZY.zombie_head_top.png", "type": "image", "prompt": null, "description": "Pixel art texture for the top face of the zombie enemy's head", "subtype": null, "aspect_ratio": null, "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "zombie_head_side": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774479257918_01KMKK9C9Y6FBTJ6V3PBBFETPM.zombie_head_side.png", "type": "image", "prompt": null, "description": "Pixel art texture for the side faces of the zombie enemy's head", "subtype": null, "aspect_ratio": null, "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "zombie_head_front": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774479257796_01KMKK9C64T07SA1W9H6ZHXFY8.zombie_head_front.png", "type": "image", "prompt": null, "description": "Pixel art texture for the front and back faces of the zombie enemy's head", "subtype": null, "aspect_ratio": null, "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "zombie_torso_top": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774481184202_01KMKN45EAN36T3E7VZYHA5672.torso_top.png", "type": "image", "prompt": null, "description": "Pixel art texture for the top face of the zombie enemy's torso/body", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "zombie_torso_side": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774481184302_01KMKN45HE0DMZ1HPQE61ZM6WV.torso_side.png", "type": "image", "prompt": null, "description": "Pixel art texture for the side faces of the zombie enemy's torso/body", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "zombie_torso_front": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774481175303_01KMKN3WR7HQC0PKQY7SMWP355.torso_front.png", "type": "image", "prompt": null, "description": "Pixel art texture for the front and back faces of the zombie enemy's torso/body", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "zombie_arm_top": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774482609707_01KMKPFNHBBMSGP3TGBPYH68PN.arm_top.png", "type": "image", "prompt": null, "description": "Pixel art texture for the top face of the zombie enemy's arms", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "zombie_arm_side": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774482609820_01KMKPFNMWZK9P81RPQETY1W5J.arm_side.png", "type": "image", "prompt": null, "description": "Pixel art texture for the side faces of the zombie enemy's arms", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "zombie_arm_front": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774482609930_01KMKPFNRAX74FMDC9Q2NSSNH3.arm_front.png", "type": "image", "prompt": null, "description": "Pixel art texture for the front and back faces of the zombie enemy's arms", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "zombie_arm_bottom": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774482811546_01KMKPNTMTBYX66S0CHX17049E.arm_bottom.png", "type": "image", "prompt": null, "description": "Pixel art texture for the bottom face of the zombie enemy's arms", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "zombie_leg_top": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774484845503_01KMKRKWXZHQPPP529WPE5V1MX.leg_top.png", "type": "image", "prompt": null, "description": "Pixel art texture for the top face of the zombie enemy's legs", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "zombie_leg_side": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774484845614_01KMKRKX1EY6945HHRSR1DF3Z7.leg_side.png", "type": "image", "prompt": null, "description": "Pixel art texture for the side faces of the zombie enemy's legs", "subtype": null, "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "zombie_leg_front": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1774484845731_01KMKRKX53KDFZC4HFQ1AT1F0A.leg_front.png", "type": "image", "prompt": null, "description": "Pixel art texture for the front and back faces of the zombie enemy's legs", "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}, "steve_skin": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115298327582608514912/1776380993256_01KPC8XQQ8YNBAV3NFBJYTBAVW.asdad.webp", "type": "image", "prompt": null, "description": "Minecraft Steve player skin texture used for rendering other players in multiplayer - complete character skin layout with head, body, arms, and legs in standard Minecraft skin format", "subtype": null, "aspect_ratio": [640, 640], "compression_suggestion": "low", "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://d2oir5eh8rty2e.cloudfront.net/assets/images/wooden_sword_cde88557-27c2-4abf-a4db-84cc9defccf1.webp", "type": "image", "prompt": "Generate a 32x32 pixel art wooden sword icon for a game inventory. The sword features a light brown wooden blade with subtle wood grain, a simple crossguard, and a dark brown handle. Clean pixel art style with sharp outlines, matching the aesthetic of image 1 and image 2. Flat 2D side-view representation, professional game UI quality, transparent background.", "description": "A 32x32 pixel art wooden sword icon for inventory display, matching the Minecraft-style voxel aesthetic.", "subtype": "ui", "aspect_ratio": [892, 949], "compression_suggestion": "medium", "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}, "bread_item": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/bread_item_34bd6ac0-a3e9-4963-9fff-a929f3c4d536.webp", "type": "image", "prompt": "Generate a 32x32 pixel art icon of a golden-brown bread loaf, showing a simple cross-hatch texture on top, isolated on a transparent background. The style must strictly match a Minecraft voxel aesthetic with clean, distinct pixels and a warm color palette. Professional game asset quality, high resolution pixel art.", "description": "A golden-brown pixel art loaf of bread for the inventory, matching the Minecraft voxel style.", "subtype": "object", "aspect_ratio": [816, 724], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "cow_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cow_texture_877bb8a0-3db1-4172-a485-00a441b8058d.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for a cow mob in a voxel game world. The texture should feature a white base with irregular black spots, matching the blocky Minecraft aesthetic. Create a flat texture layout suitable for wrapping onto a 3D cube-based cow model, ensuring a clean, polished, game-ready look on a transparent background.", "description": "A 32x32 pixel art texture for a cow mob featuring a classic black and white spotted pattern.", "subtype": "character", "aspect_ratio": [899, 901], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "pig_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/pig_texture_758faff2-eef8-4824-99f2-462461fe29b2.webp", "type": "image", "prompt": "Generate a 32x32 pixel art texture for a pig mob in a voxel game. Bright pink skin, large snout, and small eyes, matching the Minecraft aesthetic. Provide a flat texture layout or a clean front-view illustration suitable for a blocky 3D model, consistent with the style of cow_texture. High resolution pixel art, clean lines, transparent background.", "description": "A 32x32 pixel art texture for a pig mob, featuring bright pink skin and a snout in a voxel style.", "subtype": "character", "aspect_ratio": [1022, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "zombie_face_user_upload": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/e82193f0-2051-7024-9ca1-ef6cffc2f793/1779197121709_01KS06K55D8HTN5NPD9SNAD0DD.astrocade files.jpg", "type": "image", "prompt": null, "description": "Zombie face front texture from user upload.", "subtype": null, "aspect_ratio": [600, 600], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "astrocade.webp": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/e82193f0-2051-7024-9ca1-ef6cffc2f793/1779197901314_01KS07AYG212YWWP00PW3RG37N.astrocade.webp", "type": "image", "prompt": null, "description": "TNT top texture from user upload.", "subtype": null, "aspect_ratio": [1152, 1152], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tnt_bottom_user_upload": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/e82193f0-2051-7024-9ca1-ef6cffc2f793/1779236989412_01KS1CKTF4112F493MXB547TAQ.astrocade.webp", "type": "image", "prompt": null, "description": "TNT bottom texture from user upload.", "subtype": null, "aspect_ratio": [1200, 1200], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "apple_item": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/apple_item_306ae098-1ca9-4bec-8066-30a949bd9091.webp", "type": "image", "prompt": "Generate a 32x32 pixel art red apple item icon for a Minecraft-style inventory. The apple is red and juicy with a small brown stem and a tiny green leaf on top. Features a distinct blocky/pixelated silhouette, simple cel-shading, and a transparent background. Professional game-ready asset matching the existing voxel item style.", "description": "A 32x32 pixel art apple item icon for the game inventory.", "subtype": "object", "aspect_ratio": [595, 725], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Floor is Lava - Block Builder</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      .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: 280px;\n        left: 30px;\n        width: 180px;\n        height: 180px;\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: 60px;\n        height: 60px;\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        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: 26px;\n        z-index: 20;\n        border: 2px solid rgba(255, 255, 255, 0.1);\n        min-width: 100px;\n        text-align: center;\n        display:none;\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      @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) + 90px);\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          max-height: 95%;\n        }\n\n        .inventory-title {\n          font-size: 26px;\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: var(--space-sm);\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: 94%;\n        height: 200px;\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: 20;\n        display: flex;\n        flex-direction: column;\n        overflow: hidden;\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      }\n\n      .chat-input input {\n        width: 100%;\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-toggle {\n                      position: absolute;\n                      top: -30px;\n                      right: 0;\n                      background: rgba(0,0,0,0.75);\n                      color: white;\n                      border: none;\n                      padding: 4px 8px;\n                      border-radius: 6px;\n                      cursor: pointer;\n                      font-family: \"VT323\", monospace;\n                      font-size: 18px;\n                      border: 1px solid rgba(255,255,255,0.1);\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      .chat-window.hidden .chat-messages,\n      .chat-window.hidden .chat-input {\n        display: none;\n      }\n\n      /* Mobile Portrait Mode Adjustments */\n      @media (max-width: 768px) and (orientation: portrait) {\n        .player-count {\n          top: calc(var(--space-sm) + 40px);\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                          display: none;\n                      }*/\n        .chat-window.mobile-open {\n          display: flex;\n          position: fixed;\n          bottom: 50%;\n          left: 50%;\n          transform: translate(-50%, 50%);\n          width: 85vw;\n          height: 60vh;\n          max-width: 350px;\n          z-index: 100;\n        }\n\n        /*  .chat-toggle {\n                          position: fixed;\n                          bottom: 100px;\n                          right: 16px;\n                          font-size: 16px;\n                          padding: 8px 12px;\n                          z-index: 50;\n                      }\n      */\n        .chat-messages {\n          font-size: 14px;\n          padding: 10px;\n        }\n\n        .chat-input input {\n          padding: 12px;\n          font-size: 16px;\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\">\ud83c\udf0b FLOOR IS LAVA \ud83c\udf0b</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://unpkg.com/peerjs@1.4.7/dist/peerjs.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/gh/planktonfun/3d-objects@main/multiplayer-prod.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=\"./src/browser-compat.js\"></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        };\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        ];\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          BREAD: 126,\n          MEAT: 127,\n          APPLE: 128,\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.BREAD]: 'Bread',\n          [ITEM_TYPES.MEAT]: 'Meat',\n          [ITEM_TYPES.APPLE]: 'Apple',\n        };\n\n        // Tool effectiveness: which blocks they mine faster\n        const TOOL_EFFECTIVENESS = {\n          [ITEM_TYPES.WOODEN_PICKAXE]: {\n            blocks: [BLOCK_TYPES.STONE, BLOCK_TYPES.SAND, BLOCK_TYPES.COAL_ORE, BLOCK_TYPES.IRON_ORE, BLOCK_TYPES.GOLD_ORE, BLOCK_TYPES.DIAMOND_ORE],\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: [BLOCK_TYPES.STONE, BLOCK_TYPES.SAND, BLOCK_TYPES.COAL_ORE, BLOCK_TYPES.IRON_ORE, BLOCK_TYPES.GOLD_ORE, BLOCK_TYPES.DIAMOND_ORE],\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: [BLOCK_TYPES.STONE, BLOCK_TYPES.SAND, BLOCK_TYPES.COAL_ORE, BLOCK_TYPES.IRON_ORE, BLOCK_TYPES.GOLD_ORE, BLOCK_TYPES.DIAMOND_ORE],\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: [BLOCK_TYPES.STONE, BLOCK_TYPES.SAND, BLOCK_TYPES.COAL_ORE, BLOCK_TYPES.IRON_ORE, BLOCK_TYPES.GOLD_ORE, BLOCK_TYPES.DIAMOND_ORE],\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: [BLOCK_TYPES.STONE, BLOCK_TYPES.SAND, BLOCK_TYPES.COAL_ORE, BLOCK_TYPES.IRON_ORE, BLOCK_TYPES.GOLD_ORE, BLOCK_TYPES.DIAMOND_ORE],\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 isTool(itemType) {\n          return itemType >= 100;\n        }\n\n        // Get item name (works for both blocks and tools)\n        function getItemName(itemType) {\n          if (isTool(itemType)) {\n            return ITEM_NAMES[itemType] || 'Unknown Tool';\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        };\n\n        let remixed = null;\n\n        // Initialize config defaults\n        const defaultConfig = {\n          worldHeight: 64, // Height is still limited, but X/Z are infinite\n          treeDensity: 0.02,\n          miningSpeedMultiplier: 1.0,\n          movementSpeedMultiplier: 1.0,\n          dayNightCycleEnabled: true,\n          dayNightCycleDuration: 180,\n          startTimeOfDay: 'day',\n          zombiesEnabled: true,\n          creepersEnabled: false,\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          lavaRiseSpeed: 0.15,\n          lavaStartDelay: 10,\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.AIR,\n              count: 0,\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 myPlayerId = null;\n        let myPlayerName = '';\n        let otherPlayers = new Map();\n        let playerTargetPositions = 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 generatePlayerId() {\n          return 'p' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5);\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 (!window.gameConfig.firebaseConfig.apiKey || !window.gameConfig.firebaseConfig.databaseURL) {\n            console.log('Firebase config not configured - running offline');\n            return false;\n          }\n          try {\n            isOnline = true;\n            myPlayerId = generatePlayerId();\n            myPlayerName = 'Player ' + myPlayerId.substr(-4, 4);\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 - 1.1;\n            target.z = data.z;\n            target.yaw = data.yaw || 0;\n            target.pitch = data.pitch || 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            // Calculate movement speed for animation\n            const lastPos = playerGroup.userData.lastPos || {\n              x: playerGroup.position.x,\n              y: playerGroup.position.y,\n              z: playerGroup.position.z,\n            };\n            const dx = target.x - lastPos.x;\n            const dz = target.z - lastPos.z;\n            const movementSpeed = Math.sqrt(dx * dx + dz * dz);\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            playerGroup.rotation.y += (target.yaw - playerGroup.rotation.y) * LERP_FACTOR;\n\n            const head = playerGroup.userData.head;\n            if (head) {\n              head.rotation.x += (target.pitch - head.rotation.x) * LERP_FACTOR;\n            }\n\n            // Walking animation when moving\n            const isMoving = movementSpeed > 0.01;\n            if (isMoving) {\n              // Update animation time based on movement speed\n              const animSpeed = Math.min(movementSpeed * 8, 6);\n              playerGroup.userData.animationTime =\n                (playerGroup.userData.animationTime || 0) + 0.016 * animSpeed;\n\n              const swingAmount = Math.PI / 4; // 45 degrees max swing\n              const phase = Math.sin(playerGroup.userData.animationTime * Math.PI);\n\n              // Animate arms (opposite to legs, like natural walking)\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\n              // Animate legs\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            // Update last position for next frame\n            playerGroup.userData.lastPos = {\n              x: target.x,\n              y: target.y,\n              z: target.z,\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        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              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            } 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 === 'world_state') {\n              recieveWorldState(eventData);\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        // 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\n        const fillEl = document.getElementById('loading-bar-fill');\n        const percentEl = document.getElementById('loading-percentage');\n        const textEl = document.getElementById('loading-text');\n        if (fillEl) fillEl.style.width = '0%';\n        if (percentEl) percentEl.textContent = '0%';\n        if (textEl) textEl.textContent = 'Starting...';\n\n        function updateLoadingProgress(progress, text) {\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 && text) textEl.textContent = text;\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            'zombie_face_user_upload',\n            'astrocade.webp',\n            'tnt_bottom_user_upload',\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            'bread_item',\n            'cow_texture',\n            'pig_texture',\n            'apple_item',\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          ];\n\n          updateLoadingProgress(10, 'Loading assets...');\n\n          // Load images\n          let loadedImages = 0;\n          const totalAssets = imageAssets.length;\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                  loadedImages++;\n                  updateLoadingProgress(\n                    (loadedImages / totalAssets) * 30,\n                    `Loading assets... (${loadedImages}/${totalAssets})`\n                  );\n                  resolve();\n                };\n                img.onerror = () => {\n                  lib.log(`Failed to load image: ${id}`);\n                  loadedImages++;\n                  updateLoadingProgress(\n                    (loadedImages / totalAssets) * 30,\n                    `Loading assets... (${loadedImages}/${totalAssets})`\n                  );\n                  resolve();\n                };\n                img.src = assetInfo.url;\n              } else {\n                loadedImages++;\n                updateLoadingProgress(\n                  (loadedImages / totalAssets) * 30,\n                  `Loading assets... (${loadedImages}/${totalAssets})`\n                );\n                resolve();\n              }\n            });\n          });\n\n          await Promise.all(imagePromises);\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            for (const id of audioAssets) {\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 - exponential fog that matches sky color and blends with LOD\n        const fogColor = new THREE.Color(0x87ceeb);\n        const fogDensity = config.lodEnabled !== false ? 0.008 : 0.025; // Less dense fog for LOD to see farther\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(40, 40);\n        const sunMaterial = new THREE.MeshBasicMaterial({\n          color: 0xfff0ad,\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, -400);\n        skyboxGroup.add(sun);\n\n        // Create square moon\n        const moonGeometry = new THREE.PlaneGeometry(30, 30);\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, -400);\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        // Track village locations to avoid overlapping\n        const villageLocations = new Map(); // key: \"vx,vz\" -> boolean\n\n        // Get terrain height at any world coordinate - deterministic\n        function getTerrainHeight(x, z) {\n          const BOX_RADIUS = 15;\n          if (x <= -BOX_RADIUS || x >= BOX_RADIUS || z <= -BOX_RADIUS || z >= BOX_RADIUS) {\n            return worldHeight - 1;\n          }\n          return 0;\n        }\n\n        // Generate a full column of blocks at once\n        function generateColumn(x, z) {\n          const col = new Uint8Array(worldHeight);\n          const BOX_RADIUS = 15;\n\n          // Wall check\n          if (x <= -BOX_RADIUS || x >= BOX_RADIUS || z <= -BOX_RADIUS || z >= BOX_RADIUS) {\n            // Walls of the box\n            for (let y = 0; y < worldHeight; y++) {\n              if (y === 0) col[y] = BLOCK_TYPES.BEDROCK;\n              else col[y] = BLOCK_TYPES.STONE;\n            }\n          } else {\n            // Inside the box\n            for (let y = 0; y < worldHeight; y++) {\n              if (y === 0) col[y] = BLOCK_TYPES.BEDROCK;\n              else col[y] = BLOCK_TYPES.AIR;\n            }\n          }\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, 'Initializing infinite world...');\n          // No pre-generation needed - world generates on demand!\n          updateLoadingProgress(55, 'World ready for exploration...');\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            top: createTexture(assetCache.images['astrocade.webp']),\n            side: createTexture(assetCache.images['dynamite_block']),\n            bottom: createTexture(assetCache.images['tnt_bottom_user_upload']),\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.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\n        // Create block materials\n        function getBlockMaterials(blockType) {\n          const textures = blockTextures[blockType];\n          if (!textures) return null;\n\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.TORCH ||\n                blockType === BLOCK_TYPES.GLASS ||\n                blockType === BLOCK_TYPES.OAK_SIGN,\n              alphaTest:\n                blockType === BLOCK_TYPES.LEAVES || blockType === BLOCK_TYPES.GLASS ? 0 : (blockType === BLOCK_TYPES.TORCH || blockType === BLOCK_TYPES.OAK_SIGN) ? 0.1 : 0,\n              opacity:\n                blockType === BLOCK_TYPES.WATER ? 0.7 : blockType === BLOCK_TYPES.GLASS ? 1.0 : 1.0,\n            });\n            return [mat, mat, mat, mat, mat, mat];\n          }\n\n          // Handle chest with different front texture\n          if (blockType === BLOCK_TYPES.CHEST && textures.front) {\n            return [\n              new THREE.MeshLambertMaterial({ map: textures.side }), // right\n              new THREE.MeshLambertMaterial({ map: textures.side }), // left\n              new THREE.MeshLambertMaterial({ map: textures.top }), // top\n              new THREE.MeshLambertMaterial({ map: textures.bottom }), // bottom\n              new THREE.MeshLambertMaterial({ map: textures.front }), // front\n              new THREE.MeshLambertMaterial({ map: textures.side }), // back\n            ];\n          }\n\n          // Handle furnace with different front texture\n          if (blockType === BLOCK_TYPES.FURNACE && textures.front) {\n            return [\n              new THREE.MeshLambertMaterial({ map: textures.side }), // right\n              new THREE.MeshLambertMaterial({ map: textures.side }), // left\n              new THREE.MeshLambertMaterial({ map: textures.top }), // top\n              new THREE.MeshLambertMaterial({ map: textures.bottom }), // bottom\n              new THREE.MeshLambertMaterial({ map: textures.front }), // front\n              new THREE.MeshLambertMaterial({ map: textures.side }), // back\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.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          ];\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 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          // 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          // 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.DYNAMITE]: 0xff4500,\n            [BLOCK_TYPES.PLANKS]: 0xc4a76b,\n            [BLOCK_TYPES.TORCH]: 0xffa500,\n            [BLOCK_TYPES.GLASS]: 0xc0e8ff,\n            [BLOCK_TYPES.BEDROCK]: 0x2a2a2a,\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, 'Building world with batched rendering...');\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, 'Infinite world ready...');\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          function getSubTexture(image, x, y, w, h, fullW, fullH) {\n            const tex = new THREE.Texture(image);\n\n            tex.repeat.set(w / fullW, h / fullH);\n            tex.offset.set(x / fullW, 1 - (y + h) / fullH);\n\n            tex.magFilter = THREE.NearestFilter;\n            tex.minFilter = THREE.NearestFilter;\n            tex.needsUpdate = true;\n\n            return tex;\n          }\n\n          const playerGroup = new THREE.Group();\n          const skin = assetCache.images['steve_skin']; // full skin image\n\n          const headMaterials = [\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 0, 8, 8, 8, 64, 64),\n            }), // left\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 16, 8, 8, 8, 64, 64),\n            }), // right\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 8, 0, 8, 8, 64, 64),\n            }), // top\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 16, 0, 8, 8, 64, 64),\n            }), // bottom\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 24, 8, 8, 8, 64, 64),\n            }), // back\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 8, 8, 8, 8, 64, 64),\n            }), // front\n          ];\n\n          const bodyMaterials = [\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 16, 20, 4, 12, 64, 64),\n            }), // left\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 28, 20, 4, 12, 64, 64),\n            }), // right\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 20, 16, 8, 4, 64, 64),\n            }), // top\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 28, 16, 8, 4, 64, 64),\n            }), // bottom\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 32, 20, 8, 12, 64, 64),\n            }), // back\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 20, 20, 8, 12, 64, 64),\n            }), // front\n          ];\n\n          const rightArmMaterials = [\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 40, 20, 4, 12, 64, 64),\n            }), // left\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 48, 20, 4, 12, 64, 64),\n            }), // right\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, 52, 20, 4, 12, 64, 64),\n            }), // back\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 44, 20, 4, 12, 64, 64),\n            }), // front\n          ];\n\n          const leftArmMaterials = [\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 32, 52, 4, 12, 64, 64),\n            }), // left\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 40, 52, 4, 12, 64, 64),\n            }), // right\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 36, 48, 4, 4, 64, 64),\n            }), // top\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 40, 48, 4, 4, 64, 64),\n            }), // bottom\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 44, 52, 4, 12, 64, 64),\n            }), // back\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 36, 52, 4, 12, 64, 64),\n            }), // front\n          ];\n\n          const rightLegMaterials = [\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 0, 20, 4, 12, 64, 64),\n            }), // left\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 8, 20, 4, 12, 64, 64),\n            }), // right\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 4, 16, 4, 4, 64, 64),\n            }), // top\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 8, 16, 4, 4, 64, 64),\n            }), // bottom\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 12, 20, 4, 12, 64, 64),\n            }), // back\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 4, 20, 4, 12, 64, 64),\n            }), // front\n          ];\n\n          const leftLegMaterials = [\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 16, 52, 4, 12, 64, 64),\n            }), // left\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 24, 52, 4, 12, 64, 64),\n            }), // right\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 20, 48, 4, 4, 64, 64),\n            }), // top\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 24, 48, 4, 4, 64, 64),\n            }), // bottom\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 28, 52, 4, 12, 64, 64),\n            }), // back\n            new THREE.MeshLambertMaterial({\n              map: getSubTexture(skin, 20, 52, 4, 12, 64, 64),\n            }), // front\n          ];\n\n          const pirateScale = 0.7;\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          const armWidth = 0.15 * pirateScale;\n          const armHeight = 0.6 * pirateScale;\n          const armDepth = 0.15 * pirateScale;\n          const armGeometry = new THREE.BoxGeometry(armWidth, armHeight, armDepth);\n          armGeometry.translate(0, -armHeight / 2, 0);\n\n          const leftArm = new THREE.Mesh(armGeometry, leftArmMaterials);\n          leftArm.position.set(-bodyWidth / 2 - armWidth / 2, 0.2 * pirateScale + armHeight / 2, 0);\n          playerGroup.add(leftArm);\n\n          const rightArm = new THREE.Mesh(armGeometry, rightArmMaterials);\n          rightArm.position.set(bodyWidth / 2 + armWidth / 2, 0.2 * pirateScale + armHeight / 2, 0);\n          playerGroup.add(rightArm);\n\n          const legWidth = 0.2 * pirateScale;\n          const legHeight = 0.6 * pirateScale;\n          const legDepth = 0.2 * pirateScale;\n          const legGeometry = new THREE.BoxGeometry(legWidth, legHeight, legDepth);\n          legGeometry.translate(0, -legHeight / 2, 0);\n\n          const leftLeg = new THREE.Mesh(legGeometry, rightLegMaterials);\n          leftLeg.position.set(-bodyWidth / 4, -0.4 * pirateScale + legHeight / 2, 0);\n          playerGroup.add(leftLeg);\n\n          const rightLeg = new THREE.Mesh(legGeometry, leftLegMaterials);\n          rightLeg.position.set(bodyWidth / 4, -0.4 * pirateScale + legHeight / 2, 0);\n          playerGroup.add(rightLeg);\n\n          // Store limb references for walking animation\n          playerGroup.userData.leftArm = leftArm;\n          playerGroup.userData.rightArm = rightArm;\n          playerGroup.userData.leftLeg = leftLeg;\n          playerGroup.userData.rightLeg = rightLeg;\n          playerGroup.userData.animationTime = 0;\n          playerGroup.userData.lastPos = { x: data.x, y: data.y, z: data.z };\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.head = head;\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          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 = 1.2 * pirateScale;\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            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        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 (blockType !== BLOCK_TYPES.GLASS && blockType !== BLOCK_TYPES.WATER) {\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                tempPosition.set(block.x + 0.5, block.y + 0.5, block.z + 0.5);\n                tempMatrix.compose(tempPosition, tempQuaternion, tempScale);\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                    tempPosition.set(nx + 0.5, ny + 0.5, nz + 0.5);\n                    tempMatrix.compose(tempPosition, tempQuaternion, tempScale);\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) => t === BLOCK_TYPES.AIR || t === BLOCK_TYPES.WATER || t === BLOCK_TYPES.LEAVES || t === BLOCK_TYPES.GLASS;\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            tempPosition.set(x + 0.5, y + 0.5, z + 0.5);\n            tempMatrix.compose(tempPosition, tempQuaternion, tempScale);\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 = 12;\n        let isGrounded = false;\n        let coyoteTimeRemaining = 0; // Allows jumping shortly after leaving a platform\n        const COYOTE_TIME = 0.2; // 200ms coyote time\n        let spawnPoint = null;\n        // Store the initial spawn point for respawning\n\n        // Find spawn point - works with infinite world\n        function findSpawnPoint() {\n          // Spawn at origin (0, ?, 0) - find solid ground\n          const cx = 0;\n          const cz = 0;\n\n          for (let y = worldHeight - 1; y >= 0; y--) {\n            if (getBlockAt(cx, y, cz) !== BLOCK_TYPES.AIR) {\n              // Spawn well above the ground to avoid being inside blocks\n              return new THREE.Vector3(cx + 0.5, y + PLAYER_HEIGHT + 2, cz + 0.5);\n            }\n          }\n          return new THREE.Vector3(cx + 0.5, 15, cz + 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        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        // 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        ];\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\n        let playerHunger = 20;\n        const maxPlayerHunger = 20;\n        let lastHungerDecreaseTime = 0;\n        const HUNGER_DECREASE_INTERVAL = 10;\n        // Decrease hunger every 10 seconds (slower than before)\n        const HUNGER_RESTORE_AMOUNT = 4;\n        // Each bread restores 4 hunger (2 drumsticks)\n\n        // Mobs\n        const MOB_TYPES = {\n          ZOMBIE: 'zombie',\n          CREEPER: 'creeper',\n          COW: 'cow',\n          PIG: 'pig',\n        };\n\n        const mobs = [];\n        let nextMobSpawn = 0;\n        const MOB_SPAWN_INTERVAL = 3;\n        const MAX_MOBS = 25;\n\n        // Dynamite countdown tracking\n        const dynamiteCountdowns = new Map();\n\n        // ===== VICTORY GOAL SYSTEM =====\n        const VICTORY_HEIGHT = 60; // Y level where the golden light goal is\n        let victoryAchieved = false;\n        let goldenLightMesh = null;\n        let goldenLightPoint = null;\n        let goldenLightParticleTimer = 0;\n\n        function createGoldenLight() {\n          // Golden beacon column\n          const beaconGeo = new THREE.CylinderGeometry(0.3, 0.3, 3, 8);\n          const beaconMat = new THREE.MeshBasicMaterial({\n            color: 0xffd700,\n            transparent: true,\n            opacity: 0.6,\n          });\n          goldenLightMesh = new THREE.Mesh(beaconGeo, beaconMat);\n          goldenLightMesh.position.set(0, VICTORY_HEIGHT + 1.5, 0);\n          scene.add(goldenLightMesh);\n\n          // Golden point light\n          goldenLightPoint = new THREE.PointLight(0xffd700, 3, 20, 1);\n          goldenLightPoint.position.set(0, VICTORY_HEIGHT + 2, 0);\n          scene.add(goldenLightPoint);\n\n          // Place a platform at the victory point\n          for (let dx = -1; dx <= 1; dx++) {\n            for (let dz = -1; dz <= 1; dz++) {\n              setBlockAt(dx, VICTORY_HEIGHT, dz, BLOCK_TYPES.GLASS);\n              modifiedBlocks.set(`${dx},${VICTORY_HEIGHT},${dz}`, BLOCK_TYPES.GLASS);\n              updateBlockAt(dx, VICTORY_HEIGHT, dz);\n            }\n          }\n        }\n\n        function updateGoldenLight(dt) {\n          if (!goldenLightMesh || victoryAchieved) return;\n\n          // Pulsing glow\n          const pulse = 0.5 + Math.sin(performance.now() * 0.003) * 0.3;\n          goldenLightMesh.material.opacity = pulse;\n          goldenLightMesh.rotation.y += dt * 1.5;\n\n          if (goldenLightPoint) {\n            goldenLightPoint.intensity = 2.5 + Math.sin(performance.now() * 0.004) * 1.5;\n          }\n\n          // Golden particles\n          goldenLightParticleTimer += dt;\n          if (goldenLightParticleTimer > 0.15) {\n            goldenLightParticleTimer = 0;\n            const geo = new THREE.BoxGeometry(0.12, 0.12, 0.12);\n            const mat = new THREE.MeshBasicMaterial({ color: 0xffd700, transparent: true, opacity: 0.9 });\n            const p = new THREE.Mesh(geo, mat);\n            p.position.set(\n              (Math.random() - 0.5) * 2,\n              VICTORY_HEIGHT + Math.random() * 3,\n              (Math.random() - 0.5) * 2\n            );\n            p.userData.velocity = new THREE.Vector3(\n              (Math.random() - 0.5) * 0.5,\n              1 + Math.random(),\n              (Math.random() - 0.5) * 0.5\n            );\n            p.userData.life = 1.5;\n            scene.add(p);\n            particles.push(p);\n          }\n\n          // Check if player reached the goal\n          const dx = playerPos.x;\n          const dz = playerPos.z;\n          const dy = playerPos.y - VICTORY_HEIGHT;\n          if (Math.abs(dx) < 3 && Math.abs(dz) < 3 && dy >= -1 && dy <= 5) {\n            triggerVictory();\n          }\n        }\n\n        function triggerVictory() {\n          victoryAchieved = true;\n          isPaused = true;\n          isMining = false;\n\n          try {\n            if (document.pointerLockElement) document.exitPointerLock?.();\n          } catch (e) {}\n          isPointerLocked = false;\n\n          const survivalSecs = Math.floor(lavaSurvivalTime);\n\n          const victoryScreen = document.createElement('div');\n          victoryScreen.id = 'victory-screen';\n          victoryScreen.style.cssText = `\n            position: absolute; top: 0; left: 0; width: 100%; height: 100%;\n            background: rgba(0, 0, 0, 0.88);\n            display: flex; flex-direction: column;\n            align-items: center; justify-content: center;\n            z-index: 300; backdrop-filter: blur(6px);\n          `;\n          victoryScreen.innerHTML = `\n            <div style=\"text-align: center; padding: 40px;\">\n              <div style=\"font-family: 'VT323', monospace; font-size: 80px; color: #FFD700;\n                text-shadow: 0 0 40px rgba(255,215,0,0.8), 4px 4px 0 #000;\n                margin-bottom: 16px; letter-spacing: 4px;\">\n                \u2728 VICTORY \u2728\n              </div>\n              <div style=\"font-family: 'VT323', monospace; font-size: 40px; color: rgba(255,255,255,0.9);\n                margin-bottom: 10px;\">\n                You Escaped! \ud83c\udfc6\n              </div>\n              <div style=\"font-family: 'VT323', monospace; font-size: 28px; color: #FFD700;\n                margin-bottom: 40px;\">\n                \u23f1\ufe0f Time: <span style=\"color:#fff;\">${survivalSecs}s</span>\n              </div>\n              <button id=\"victory-restart-btn\" style=\"\n                font-family: 'VT323', monospace; font-size: 36px;\n                padding: 16px 50px;\n                background: linear-gradient(135deg, rgba(255,215,0,0.4), rgba(200,160,0,0.4));\n                color: #FFD700; border: 3px solid #FFD700; border-radius: 14px;\n                cursor: pointer; letter-spacing: 2px;\n                box-shadow: 0 0 20px rgba(255,215,0,0.4);\n                transition: all 0.2s;\n              \">\u25b6 PLAY AGAIN</button>\n            </div>\n          `;\n          gameWorld.appendChild(victoryScreen);\n\n          document.getElementById('victory-restart-btn').addEventListener('click', () => {\n            restartGame();\n          });\n        }\n\n        // ===== FLOOR IS LAVA SYSTEM =====\n        let lavaLevel = 0; // Starts at y=0, rises over time\n        const LAVA_RISE_SPEED = 0.15; // Blocks per second the lava rises\n        const LAVA_DAMAGE = 4; // Damage per tick when in lava\n        const LAVA_DAMAGE_INTERVAL = 0.3; // Damage every 0.3 seconds\n        let lastLavaDamageTime = 0;\n        let lavaStarted = false;\n        let lavaStartDelay = 10; // 10 seconds grace period before lava starts rising\n        let lavaElapsedTime = 0;\n        let lavaSurvivalTime = 0; // How long the player has survived\n        let lavaPlaneMesh = null;\n        let lavaLight = null;\n\n        // ===== PARKOUR PLATFORM SYSTEM =====\n        const parkourPlatforms = []; // Array of generated platform positions\n        let lastPlatformGenY = 0; // Track highest generated platform level\n        const PLATFORM_SPACING_Y = 2; // Vertical spacing between platform levels\n        const PLATFORM_SPACING_XZ = 3; // Max horizontal jump distance\n        const PLATFORMS_AHEAD = 15; // How many levels of platforms to generate ahead of lava\n\n        function generateParkourPlatforms() {\n          // Generate platforms from current lava level upward\n          const startY = Math.max(lastPlatformGenY, Math.floor(lavaLevel) + 5);\n          const endY = startY + PLATFORMS_AHEAD * PLATFORM_SPACING_Y;\n\n          // Use last platform center as starting point\n          let prevSize = parkourPlatforms.length > 0 ? parkourPlatforms[parkourPlatforms.length - 1].size : 2;\n          let lastCX = parkourPlatforms.length > 0 ? parkourPlatforms[parkourPlatforms.length - 1].x + prevSize / 2 : Math.floor(playerPos.x);\n          let lastCZ = parkourPlatforms.length > 0 ? parkourPlatforms[parkourPlatforms.length - 1].z + prevSize / 2 : Math.floor(playerPos.z);\n\n          const BOX_RADIUS = 12; // stay away from walls\n\n          for (let y = startY; y < endY; y += PLATFORM_SPACING_Y) {\n            // Skip if already generated at this level\n            if (y <= lastPlatformGenY) continue;\n\n            // Platform size distribution: 40% 1x1, 30% 2x1, 30% 3x1\n            const sizeRoll = Math.random();\n            const size = sizeRoll < 0.4 ? 1 : (sizeRoll < 0.7 ? 2 : 3);\n            \n            // Gap of 2 blocks between closest edges\n            const gap = 2;\n            \n            // Distance between centers\n            const dist = (prevSize / 2) + gap + (size / 2);\n            \n            // Pick orthogonal direction for clear jump paths\n            const dir = Math.floor(Math.random() * 4);\n            let cx = lastCX;\n            let cz = lastCZ;\n            \n            if (dir === 0) cx += dist;\n            else if (dir === 1) cx -= dist;\n            else if (dir === 2) cz += dist;\n            else if (dir === 3) cz -= dist;\n            \n            // Add a slight offset on the other axis so it's not perfectly straight\n            if (dir <= 1) cz += (Math.random() * 2 - 1);\n            else cx += (Math.random() * 2 - 1);\n\n            let px = Math.round(cx - size / 2);\n            let pz = Math.round(cz - size / 2);\n\n            // Clamp and reverse direction if hitting the box walls\n            if (px < -BOX_RADIUS || px + size > BOX_RADIUS || pz < -BOX_RADIUS || pz + size > BOX_RADIUS) {\n                if (dir === 0) px = Math.round(lastCX - dist - size / 2);\n                else if (dir === 1) px = Math.round(lastCX + dist - size / 2);\n                else if (dir === 2) pz = Math.round(lastCZ - dist - size / 2);\n                else if (dir === 3) pz = Math.round(lastCZ + dist - size / 2);\n                \n                px = Math.max(-BOX_RADIUS, Math.min(BOX_RADIUS - size, px));\n                pz = Math.max(-BOX_RADIUS, Math.min(BOX_RADIUS - size, pz));\n            }\n\n            // Place platform blocks\n            for (let dx = 0; dx < size; dx++) {\n              for (let dz = 0; dz < size; dz++) {\n                const bx = px + dx;\n                const bz = pz + dz;\n                \n                if (bx >= -BOX_RADIUS && bx <= BOX_RADIUS && bz >= -BOX_RADIUS && bz <= BOX_RADIUS) {\n                  const blockType = Math.random() < 0.5 ? BLOCK_TYPES.STONE : BLOCK_TYPES.PLANKS;\n                  setBlockAt(bx, y, bz, blockType);\n                  modifiedBlocks.set(`${bx},${y},${bz}`, blockType);\n                  updateBlockAt(bx, y, bz);\n                }\n              }\n            }\n\n            parkourPlatforms.push({ x: px, y, z: pz, size });\n            lastCX = px + size / 2;\n            lastCZ = pz + size / 2;\n            prevSize = size;\n          }\n\n          lastPlatformGenY = endY;\n        }\n\n        // Remove platforms that are below lava\n        function cleanupSubmergedPlatforms() {\n          for (let i = parkourPlatforms.length - 1; i >= 0; i--) {\n            const plat = parkourPlatforms[i];\n            if (plat.y < lavaLevel - 2) {\n              // Remove the blocks\n              for (let dx = 0; dx < plat.size; dx++) {\n                for (let dz = 0; dz < plat.size; dz++) {\n                  const bx = plat.x + dx;\n                  const bz = plat.z + dz;\n                  setBlockAt(bx, plat.y, bz, BLOCK_TYPES.AIR);\n                  modifiedBlocks.set(`${bx},${plat.y},${bz}`, BLOCK_TYPES.AIR);\n                  updateBlockAt(bx, plat.y, bz);\n                }\n              }\n              parkourPlatforms.splice(i, 1);\n            }\n          }\n        }\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        // 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        // 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        // 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        };\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        // 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) {\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(boardWidth + frameThickness * 2, frameThickness, boardDepth + 0.02, frameColor);\n          topFrame.position.y = 0.35 + boardHeight / 2 + frameThickness / 2;\n          sign.add(topFrame);\n\n          // Bottom frame\n          const bottomFrame = box(boardWidth + frameThickness * 2, frameThickness, boardDepth + 0.02, frameColor);\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          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 signMesh = createSignMesh(x, y, z);\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(x, y, 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        // 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\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        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        }\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              if (rooms.length === 0) {\n                roomListEl.innerHTML = '<div>No rooms found</div>';\n                return;\n              }\n\n              let html = '';\n              for (const room of rooms) {\n                const playerCount = await roomManager.getRoomPlayerCount(room.name);\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 class=\"world-host\">Hosted by Steve</div>\n                        </div>\n                        <div class=\"world-stats\">\n                            <div class=\"world-players\">\ud83d\udc65 ${playerCount}/20</div>\n                        </div>\n                    </div>\n                  </div>`;\n              }\n              if (roomListEl.innerHTML != html) {\n                roomListEl.innerHTML = html;\n              }\n            } catch (error) {\n              console.log('Error listing rooms: ' + error.message);\n            }\n          };\n\n          setInterval(async () => {\n            if (document.getElementById('worlds-modal').classList.contains('active')) {\n              listRooms();\n            }\n          }, 1000);\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 World: ' + roomName;\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                    justJoined = true;\n                  }\n                );\n\n                window.lobby = lobby;\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 World: none';\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', '\ud83d\udc4b'];\n          const quickTexts = ['Nice!', 'Thanks!', 'Danger!', 'Come here!', 'Good job!', 'Bye!'];\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          gameWorld.appendChild(quickChat);\n\n          // Instructions\n          const instructions = document.createElement('div');\n          instructions.className = 'instructions';\n          instructions.id = 'instructions';\n          instructions.textContent = isTouchDevice\n            ? '\ud83c\udf0b FLOOR IS LAVA! Parkour UP! \u25c0 Move | Drag Aim \u25b6 | \u2b06\ufe0f Jump'\n            : '\ud83c\udf0b FLOOR IS LAVA! Parkour UP! \u2022 WASD Move \u2022 Mouse Look \u2022 SPACE Jump';\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 = 'health-bar';\n          hungerBar.id = 'hunger-bar';\n          hungerBar.style.cssText = `\n            position: absolute;\n            top: calc(var(--space-sm) + 50px);\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          // Create 10 drumsticks (each drumstick = 2 hunger, max 20 hunger)\n          let drumstickHTML = '';\n          for (let i = 0; i < 10; i++) {\n            drumstickHTML += `<div class=\"drumstick full\" data-drumstick=\"${i}\">\ud83c\udf57</div>`;\n          }\n          hungerBar.innerHTML = drumstickHTML;\n          gameWorld.appendChild(hungerBar);\n\n          // Floor is Lava timer/indicator\n          const lavaTimer = document.createElement('div');\n          lavaTimer.id = 'lava-timer';\n          lavaTimer.style.cssText = `\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -300%);\n            background: rgba(0, 0, 0, 0.8);\n            color: #FF6600;\n            padding: 10px 20px;\n            border-radius: 12px;\n            font-family: \"VT323\", monospace;\n            font-size: 26px;\n            z-index: 20;\n            border: 2px solid rgba(255, 69, 0, 0.6);\n            text-align: center;\n            pointer-events: none;\n            text-shadow: 0 0 10px rgba(255, 69, 0, 0.5);\n            box-shadow: 0 0 15px rgba(255, 69, 0, 0.3);\n          `;\n          lavaTimer.textContent = '\ud83c\udf0b FLOOR IS LAVA';\n          gameWorld.appendChild(lavaTimer);\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          // 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 World: 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 = '1 Player Online';\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 Worlds' : '\ud83c\udf0d Worlds (L)';\n          worldsButton.title = 'Browse Worlds (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 Browse Worlds</h2>\n                                <button class=\"worlds-modal-close\" id=\"worlds-close-btn\">\u2715</button>\n                            </div>\n                            <div class=\"worlds-section-title\">\ud83c\udfae Your World</div>\n                            <button class=\"host-world-btn\" id=\"host-world-btn\">\n                                \u2795 Host Your Own World\n                            </button>\n                            <div class=\"worlds-section-title\">\ud83d\udce1 Available Worlds</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          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            // Show as bubble on player's head\n            if (isOwn) {\n              // Show emoji above own player (create a temporary sprite above camera)\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\n          // Simulate incoming messages only when offline\n          if (!isOnline) {\n            const chatMessageTexts = [\n              'Nice!',\n              'Thanks!',\n              'Danger!',\n              'Come here!',\n              'Good job!',\n              'Bye!',\n            ];\n            let messageIndex = 0;\n            const chatInterval = setInterval(() => {\n              const username = chatUsernames[Math.floor(Math.random() * chatUsernames.length)];\n              const text = chatMessageTexts[messageIndex % chatMessageTexts.length];\n              addChatMessage(username, text, false);\n              messageIndex++;\n            }, 4000);\n          }\n\n          // Mobile action buttons\n          if (isTouchDevice) {\n            // Eat Button (mobile)\n            const eatButton = document.createElement('button');\n            eatButton.id = 'mobile-eat-button';\n            eatButton.textContent = '\ud83c\udf5e';\n            eatButton.style.cssText = `\n                        position: absolute;\n                        bottom: 500px;\n                        right: 110px;\n                        font-size: 40px;\n                        width: 80px;\n                        height: 80px;\n                        background: rgba(255, 215, 0, 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            eatButton.addEventListener('touchstart', (e) => {\n              e.preventDefault();\n              eatButton.style.transform = 'scale(0.9)';\n              eatBread();\n            });\n            eatButton.addEventListener('touchend', (e) => {\n              e.preventDefault();\n              eatButton.style.transform = 'scale(1)';\n            });\n            gameWorld.appendChild(eatButton);\n\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\n            // Place button (right side, bottom)\n            const placeButton = document.createElement('button');\n            placeButton.id = 'mobile-place-button';\n            placeButton.textContent = '\ud83d\udce6';\n            placeButton.style.cssText = `\n                        position: absolute;\n                        bottom: 250px;\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            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 (right side, above place button)\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: 380px;\n                        right: 20px;\n                        font-size: 52px;\n                        width: 104px;\n                        height: 104px;\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 4px 8px rgba(0,0,0,0.3);\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 (isGrounded || coyoteTimeRemaining > 0) {\n                playerVelocity.y = JUMP_VELOCITY;\n                isGrounded = false;\n                coyoteTimeRemaining = 0;\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            // 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 = '\ud83d\udce6';\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            [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            [ITEM_TYPES.BREAD]: 'bread_item',\n            [ITEM_TYPES.MEAT]: 'bread_item', // Fallback to bread icon if meat icon doesn't exist\n            [ITEM_TYPES.APPLE]: 'apple_item',\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\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 drumstickToFill = playerHunger / 2;\n          // Each drumstick = 2 hunger\n\n          drumsticks.forEach((drumstick, index) => {\n            drumstick.classList.remove('full', 'half', 'empty');\n\n            if (index < Math.floor(drumstickToFill)) {\n              // Full drumstick\n              drumstick.classList.add('full');\n              drumstick.innerHTML = '\ud83c\udf57';\n              drumstick.style.opacity = '1';\n            } else if (index < drumstickToFill) {\n              // Half drumstick (when drumstickToFill has .5)\n              drumstick.classList.add('half');\n              drumstick.innerHTML = '\ud83c\udf57';\n              drumstick.style.opacity = '0.5';\n            } else {\n              // Empty drumstick\n              drumstick.classList.add('empty');\n              drumstick.innerHTML = '\ud83c\udf57';\n              drumstick.style.opacity = '0.2';\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 = '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 = '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('#inventory-main-grid .inventory-slot, #inventory-hotbar-grid .inventory-slot');\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 = '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 = '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 (craftingTableGrid[i].count === 0 || craftingTableGrid[i].type === BLOCK_TYPES.AIR) {\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 = '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 = '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 = '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) => { e.preventDefault(); handleFurnaceSlotClick('input'); });\n\n          const inputLabel = document.createElement('div');\n          inputLabel.style.cssText = '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 = '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) => { e.preventDefault(); handleFurnaceSlotClick('fuel'); });\n\n          const fuelLabel = document.createElement('div');\n          fuelLabel.style.cssText = '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 = '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 = '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 = '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 = '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 = '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) => { e.preventDefault(); handleFurnaceSlotClick('output'); });\n\n          const outputLabel = document.createElement('div');\n          outputLabel.style.cssText = '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 = '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          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 light = new THREE.PointLight(0xff6600, 1.5, 10, 1.5);\n          light.position.set(x + 0.5, y + 0.5, z + 1.2);\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            // Position slightly in front of the furnace block's front face\n            frontMesh.position.set(x + 0.5, y + 0.5, z + 1.001);\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 });\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) {\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(0.3 + Math.random() * 0.2, 0.3 + Math.random() * 0.2, 0.3 + Math.random() * 0.2),\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            fire.position.set(\n              x + 0.5 + (Math.random() - 0.5) * 0.3,\n              y + 0.3 + Math.random() * 0.4,\n              z + 0.9 + Math.random() * 0.2\n            );\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) { // Every 50ms\n              createFurnaceParticles(x, y, z);\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 = recipe && furnace.input.count > 0 &&\n              (outputType === BLOCK_TYPES.AIR || (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\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=\"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('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 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            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            // 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            // 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 <= 6) {\n                const emojiHotkeys = ['\ud83d\ude00', '\ud83d\udc4d', '\u26a0\ufe0f', '\ud83c\udfc3', '\ud83d\udcaa', '\ud83d\udc4b'];\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            // Eating hotkey: 'R' to eat bread from selected slot\n            if (e.key.toLowerCase() === 'r') {\n              eatBread();\n              return;\n            }\n\n            // Jump hotkey: 'SPACE' to jump\n            if (e.key === ' ' && (isGrounded || coyoteTimeRemaining > 0)) {\n              playerVelocity.y = JUMP_VELOCITY;\n              isGrounded = false;\n              coyoteTimeRemaining = 0;\n              return;\n            }\n\n            // Debug: Shift+O to spawn enemy\n            if (e.shiftKey && e.key.toLowerCase() === 'o') {\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) {\n                lib.log('Debug: No enemy types enabled');\n                return;\n              }\n\n              // Spawn enemy close to player for testing\n              const angle = Math.random() * Math.PI * 2;\n              const distance = 5 + Math.random() * 5;\n              const spawnX = playerPos.x + Math.cos(angle) * distance;\n              const spawnZ = playerPos.z + Math.sin(angle) * distance;\n\n              const wx = Math.floor(spawnX);\n              const wz = Math.floor(spawnZ);\n\n              if (wx >= 0 && wx < worldWidth && wz >= 0 && wz < worldDepth) {\n                let groundY = -1;\n                for (let y = worldHeight - 1; y >= 0; y--) {\n                  if (worldData[wx][y][wz] !== BLOCK_TYPES.AIR) {\n                    groundY = y;\n                    break;\n                  }\n                }\n\n                if (groundY >= 0) {\n                  const type = enabledTypes[Math.floor(Math.random() * enabledTypes.length)];\n                  // Spawn well above ground to avoid getting stuck\n                  const mob = createMob(type, wx + 0.5, groundY + 2, wz + 0.5);\n                  mobs.push(mob);\n                  lib.log(\n                    'Debug: Spawned ' + type + ' at ' + wx + ', ' + (groundY + 2) + ', ' + wz\n                  );\n                }\n              }\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            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\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 || isInventoryOpen || isCraftingTableOpen || isChestOpen || isFurnaceOpen) 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              // Mining disabled\n              attackMob();\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          // Get ray direction from camera\n          const rayDir = new THREE.Vector3();\n          camera.getWorldDirection(rayDir);\n\n          const camPos = camera.position.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 (dist <= explosionRadius && blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.BEDROCK) {\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 (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 (const mob of mobs) {\n            const mobDist = explosionPos.distanceTo(mob.position);\n            if (mobDist < 8) {\n              const mobDamage = Math.ceil((1 - mobDist / 8) * 20);\n              damageMob(mob, mobDamage);\n            }\n          }\n        }\n\n        function mineBlock(dt) {\n          if (!targetBlock || targetBlock.blockType === BLOCK_TYPES.AIR) return;\n\n          // Bedrock is unmineable\n          if (targetBlock.blockType === BLOCK_TYPES.BEDROCK) {\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              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              // 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) {\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                  chestContents.delete(chestKey);\n                }\n              }\n\n              // Add to inventory\n              // Coal ore drops coal instead of the block itself\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                addToInventory(BLOCK_TYPES.LEAVES);\n                // 10% chance to drop an apple from leaves\n                if (Math.random() < 0.1) {\n                  addToInventory(ITEM_TYPES.APPLE);\n                  showMessage('\ud83c\udf4e Found an Apple!', '#4CAF50', 1000);\n                }\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            }\n\n            // Play sound\n            playSound('block_break_sound', false, 0.5);\n\n            miningProgress = 0;\n            isMining = false;\n          }\n        }\n\n        function eatBread() {\n          const slot = inventory[selectedSlot];\n          if ((slot.type !== ITEM_TYPES.BREAD && slot.type !== ITEM_TYPES.MEAT && slot.type !== ITEM_TYPES.APPLE) || slot.count <= 0) {\n            return false;\n          }\n\n          // Only eat if not at max hunger\n          if (playerHunger >= maxPlayerHunger) {\n            return false;\n          }\n\n          const isMeat = slot.type === ITEM_TYPES.MEAT;\n          const isApple = slot.type === ITEM_TYPES.APPLE;\n          const restoreAmount = isMeat ? 6 : (isApple ? 4 : HUNGER_RESTORE_AMOUNT);\n\n          // Consume food\n          slot.count--;\n          if (slot.count <= 0) {\n            slot.type = BLOCK_TYPES.AIR;\n          }\n\n          // Restore hunger\n          playerHunger = Math.min(maxPlayerHunger, playerHunger + restoreAmount);\n          updateHungerBar();\n          updateHotbarUI();\n\n          // Play eating sound\n          playSound('block_place_sound', false, 0.4);\n\n          // Show feedback\n          let itemName = 'Bread';\n          if (isMeat) itemName = 'Meat';\n          if (isApple) itemName = 'Apple';\n          showMessage('\ud83d\ude0b Ate ' + itemName + '! +' + restoreAmount + ' Hunger', '#FFD700', 1000);\n\n          return true;\n        }\n\n        function placeBlock() {\n          if (!targetBlock) return;\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          const slot = inventory[selectedSlot];\n          if (slot.count <= 0 || slot.type === BLOCK_TYPES.AIR) return;\n\n          // Can't place tools\n          if (isTool(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            showSignInputUI(px, py, 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\n                const signKey = `${px},${py},${pz}`;\n                signMessages.set(signKey, message);\n\n                // Update the sign text display\n                const signMesh = signMeshes.get(signKey);\n                if (signMesh) {\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\n                slot.count--;\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          // 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); // {'1,18,-1' => 0, '0,18,0' => 0, '-1,17,0' => 11}\n\n          // Remove from inventory\n          slot.count--;\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, slot.type);\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          // Find existing slot with same type (only for stackable items)\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        // Particles\n        const particles = [];\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: (type === MOB_TYPES.COW || type === MOB_TYPES.PIG) ? 10 : (type === MOB_TYPES.ZOMBIE ? 20 : 10),\n            maxHealth: (type === MOB_TYPES.COW || type === MOB_TYPES.PIG) ? 10 : (type === MOB_TYPES.ZOMBIE ? 20 : 10),\n            size: type === MOB_TYPES.ZOMBIE ? 1.8 : (type === MOB_TYPES.CREEPER ? 1.4 : 1.2),\n            width: 0.6,\n            isGrounded: false,\n            targetPlayer: (type === MOB_TYPES.ZOMBIE || type === MOB_TYPES.CREEPER),\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          };\n\n          // Create group for multiple meshes\n          mob.group = new THREE.Group();\n\n          if (type === MOB_TYPES.ZOMBIE) {\n            // Zombie - humanoid shape with UV mapped skin\n            const textureKey = 'zombie_texture';\n            const texture = assetCache.images[textureKey];\n\n            let skinTexture;\n            if (texture) {\n              skinTexture = new THREE.Texture(texture);\n              skinTexture.magFilter = THREE.NearestFilter;\n              skinTexture.minFilter = THREE.NearestFilter;\n              skinTexture.needsUpdate = true;\n            }\n\n            // Helper function to create UV mapped geometry\n            function createBodyPartGeometry(width, height, depth, uvMap) {\n              const geometry = new THREE.BoxGeometry(width, height, depth);\n              const uvAttr = geometry.attributes.uv;\n\n              // Apply UV mapping based on the uvMap parameter\n              // uvMap structure: { right, left, top, bottom, front, back }\n              // Each face has [x, y, width, height] in texture coordinates (0-1)\n              const faces = ['right', 'left', 'top', 'bottom', 'front', 'back'];\n\n              for (let i = 0; i < 6; i++) {\n                const faceUVs = uvMap[faces[i]];\n                if (faceUVs) {\n                  const baseIndex = i * 4;\n                  // Set UVs for the 4 vertices of this face (bottom-left, bottom-right, top-right, top-left)\n                  uvAttr.setXY(baseIndex, faceUVs[0], faceUVs[1] + faceUVs[3]);\n                  uvAttr.setXY(baseIndex + 1, faceUVs[0] + faceUVs[2], faceUVs[1] + faceUVs[3]);\n                  uvAttr.setXY(baseIndex + 2, faceUVs[0] + faceUVs[2], faceUVs[1]);\n                  uvAttr.setXY(baseIndex + 3, faceUVs[0], faceUVs[1]);\n                }\n              }\n\n              uvAttr.needsUpdate = true;\n              return geometry;\n            }\n\n            // Texture coordinates in pixels, normalized to 0-1 (64x64 Minecraft skin format)\n            const px = 1 / 64;\n            // 1 pixel\n\n            // Create head with custom textures\n            const headTopImg = assetCache.images['zombie_head_top'];\n            const headSideImg = assetCache.images['zombie_head_side'];\n            const headFrontImg = assetCache.images['zombie_face_user_upload'] || assetCache.images['zombie_head_front'];\n\n            if (headTopImg && headSideImg && headFrontImg) {\n              // Create textures from images\n              const topTex = new THREE.Texture(headTopImg);\n              topTex.magFilter = THREE.NearestFilter;\n              topTex.minFilter = THREE.NearestFilter;\n              topTex.needsUpdate = true;\n\n              const sideTex = new THREE.Texture(headSideImg);\n              sideTex.magFilter = THREE.NearestFilter;\n              sideTex.minFilter = THREE.NearestFilter;\n              sideTex.needsUpdate = true;\n\n              const frontTex = new THREE.Texture(headFrontImg);\n              frontTex.magFilter = THREE.NearestFilter;\n              frontTex.minFilter = THREE.NearestFilter;\n              frontTex.needsUpdate = true;\n\n              // Create materials for each face (right, left, top, bottom, front, back)\n              const headMaterials = [\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // right\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // left\n                new THREE.MeshLambertMaterial({\n                  map: topTex,\n                }), // top\n                new THREE.MeshLambertMaterial({\n                  map: topTex, // Using top for bottom too\n                }), // bottom\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // front\n                new THREE.MeshLambertMaterial({\n                  map: sideTex, // Using side for back\n                }), // back\n              ];\n\n              const headGeo = new THREE.BoxGeometry(0.5, 0.5, 0.5);\n              const head = new THREE.Mesh(headGeo, headMaterials);\n              head.position.set(0, 0.65, 0);\n              mob.group.add(head);\n            } else if (skinTexture) {\n              // Fallback to UV mapped skin texture\n              // Head (8x8x8 cube) - positioned at top of texture\n              const headUV = {\n                right: [0 * px, 8 * px, 8 * px, 8 * px],\n                left: [16 * px, 8 * px, 8 * px, 8 * px],\n                top: [8 * px, 0 * px, 8 * px, 8 * px],\n                bottom: [16 * px, 0 * px, 8 * px, 8 * px],\n                front: [8 * px, 8 * px, 8 * px, 8 * px],\n                back: [24 * px, 8 * px, 8 * px, 8 * px],\n              };\n              const headGeo = createBodyPartGeometry(0.5, 0.5, 0.5, headUV);\n              const head = new THREE.Mesh(\n                headGeo,\n                new THREE.MeshLambertMaterial({\n                  map: skinTexture,\n                })\n              );\n              head.position.set(0, 0.65, 0);\n              mob.group.add(head);\n            } else {\n              // Fallback to solid color\n              const head = new THREE.Mesh(\n                new THREE.BoxGeometry(0.5, 0.5, 0.5),\n                new THREE.MeshLambertMaterial({\n                  color: 0x5a7a3f,\n                })\n              );\n              head.position.set(0, 0.65, 0);\n              mob.group.add(head);\n            }\n\n            // Create torso/body with custom textures or fallback\n            const torsoTopImg = assetCache.images['zombie_torso_top'];\n            const torsoSideImg = assetCache.images['zombie_torso_side'];\n            const torsoFrontImg = assetCache.images['zombie_torso_front'];\n\n            if (torsoTopImg && torsoSideImg && torsoFrontImg) {\n              // Create textures from images\n              const topTex = new THREE.Texture(torsoTopImg);\n              topTex.magFilter = THREE.NearestFilter;\n              topTex.minFilter = THREE.NearestFilter;\n              topTex.needsUpdate = true;\n\n              const sideTex = new THREE.Texture(torsoSideImg);\n              sideTex.magFilter = THREE.NearestFilter;\n              sideTex.minFilter = THREE.NearestFilter;\n              sideTex.needsUpdate = true;\n\n              const frontTex = new THREE.Texture(torsoFrontImg);\n              frontTex.magFilter = THREE.NearestFilter;\n              frontTex.minFilter = THREE.NearestFilter;\n              frontTex.needsUpdate = true;\n\n              // Create materials for each face (right, left, top, bottom, front, back)\n              const torsoMaterials = [\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // right\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // left\n                new THREE.MeshLambertMaterial({\n                  map: topTex,\n                }), // top\n                new THREE.MeshLambertMaterial({\n                  map: topTex,\n                }), // bottom\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // front\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // back\n              ];\n\n              const bodyGeo = new THREE.BoxGeometry(0.5, 0.7, 0.3);\n              const body = new THREE.Mesh(bodyGeo, torsoMaterials);\n              body.position.set(0, 0, 0);\n              mob.group.add(body);\n            } else if (skinTexture) {\n              // Body (8x12x4 cube) - fallback to UV mapped texture\n              const bodyUV = {\n                right: [16 * px, 20 * px, 4 * px, 12 * px],\n                left: [28 * px, 20 * px, 4 * px, 12 * px],\n                top: [20 * px, 16 * px, 8 * px, 4 * px],\n                bottom: [28 * px, 16 * px, 8 * px, 4 * px],\n                front: [20 * px, 20 * px, 8 * px, 12 * px],\n                back: [32 * px, 20 * px, 8 * px, 12 * px],\n              };\n              const bodyGeo = createBodyPartGeometry(0.5, 0.7, 0.3, bodyUV);\n              const body = new THREE.Mesh(\n                bodyGeo,\n                new THREE.MeshLambertMaterial({\n                  map: skinTexture,\n                })\n              );\n              body.position.set(0, 0, 0);\n              mob.group.add(body);\n            } else {\n              // Fallback to solid color\n              const body = new THREE.Mesh(\n                new THREE.BoxGeometry(0.5, 0.7, 0.3),\n                new THREE.MeshLambertMaterial({\n                  color: 0x5a7a3f,\n                })\n              );\n              body.position.set(0, 0, 0);\n              mob.group.add(body);\n            }\n\n            // Create arms with custom textures or fallback\n            const armTopImg = assetCache.images['zombie_arm_top'];\n            const armSideImg = assetCache.images['zombie_arm_side'];\n            const armFrontImg = assetCache.images['zombie_arm_front'];\n            const armBottomImg = assetCache.images['zombie_arm_bottom'];\n\n            if (armTopImg && armSideImg && armFrontImg) {\n              // Create textures from images\n              const topTex = new THREE.Texture(armTopImg);\n              topTex.magFilter = THREE.NearestFilter;\n              topTex.minFilter = THREE.NearestFilter;\n              topTex.needsUpdate = true;\n\n              const sideTex = new THREE.Texture(armSideImg);\n              sideTex.magFilter = THREE.NearestFilter;\n              sideTex.minFilter = THREE.NearestFilter;\n              sideTex.needsUpdate = true;\n\n              const frontTex = new THREE.Texture(armFrontImg);\n              frontTex.magFilter = THREE.NearestFilter;\n              frontTex.minFilter = THREE.NearestFilter;\n              frontTex.needsUpdate = true;\n\n              // Create bottom texture if available, otherwise use top\n              let bottomTex = topTex;\n              if (armBottomImg) {\n                bottomTex = new THREE.Texture(armBottomImg);\n                bottomTex.magFilter = THREE.NearestFilter;\n                bottomTex.minFilter = THREE.NearestFilter;\n                bottomTex.needsUpdate = true;\n              }\n\n              // Create materials for each face (right, left, top, bottom, front, back)\n              const leftArmMaterials = [\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // right\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // left\n                new THREE.MeshLambertMaterial({\n                  map: topTex,\n                }), // top\n                new THREE.MeshLambertMaterial({\n                  map: bottomTex,\n                }), // bottom\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // front\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // back\n              ];\n\n              const leftArmGeo = new THREE.BoxGeometry(0.2, 0.6, 0.2);\n              const leftArm = new THREE.Mesh(leftArmGeo, leftArmMaterials);\n              leftArm.position.set(-0.4, 0.25, 0);\n              mob.group.add(leftArm);\n              mob.leftArm = leftArm;\n\n              const rightArmMaterials = [\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // right\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // left\n                new THREE.MeshLambertMaterial({\n                  map: topTex,\n                }), // top\n                new THREE.MeshLambertMaterial({\n                  map: bottomTex,\n                }), // bottom\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // front\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // back\n              ];\n\n              const rightArmGeo = new THREE.BoxGeometry(0.2, 0.6, 0.2);\n              const rightArm = new THREE.Mesh(rightArmGeo, rightArmMaterials);\n              rightArm.position.set(0.4, 0.25, 0);\n              mob.group.add(rightArm);\n              mob.rightArm = rightArm;\n            } else if (skinTexture) {\n              // Fallback to UV mapped arms\n              // Left arm (4x12x4 cube)\n              const leftArmUV = {\n                right: [32 * px, 52 * px, 4 * px, 12 * px],\n                left: [40 * px, 52 * px, 4 * px, 12 * px],\n                top: [36 * px, 48 * px, 4 * px, 4 * px],\n                bottom: [40 * px, 48 * px, 4 * px, 4 * px],\n                front: [36 * px, 52 * px, 4 * px, 12 * px],\n                back: [44 * px, 52 * px, 4 * px, 12 * px],\n              };\n              const leftArmGeo = createBodyPartGeometry(0.2, 0.6, 0.2, leftArmUV);\n              const leftArm = new THREE.Mesh(\n                leftArmGeo,\n                new THREE.MeshLambertMaterial({\n                  map: skinTexture,\n                })\n              );\n              leftArm.position.set(-0.4, 0.25, 0);\n              mob.group.add(leftArm);\n              mob.leftArm = leftArm;\n\n              // Right arm (4x12x4 cube)\n              const rightArmUV = {\n                right: [40 * px, 20 * px, 4 * px, 12 * px],\n                left: [48 * px, 20 * px, 4 * px, 12 * px],\n                top: [44 * px, 16 * px, 4 * px, 4 * px],\n                bottom: [48 * px, 16 * px, 4 * px, 4 * px],\n                front: [44 * px, 20 * px, 4 * px, 12 * px],\n                back: [52 * px, 20 * px, 4 * px, 12 * px],\n              };\n              const rightArmGeo = createBodyPartGeometry(0.2, 0.6, 0.2, rightArmUV);\n              const rightArm = new THREE.Mesh(\n                rightArmGeo,\n                new THREE.MeshLambertMaterial({\n                  map: skinTexture,\n                })\n              );\n              rightArm.position.set(0.4, 0.25, 0);\n              mob.group.add(rightArm);\n              mob.rightArm = rightArm;\n            }\n\n            // Create legs with custom textures or fallback\n            const legTopImg = assetCache.images['zombie_leg_top'];\n            const legSideImg = assetCache.images['zombie_leg_side'];\n            const legFrontImg = assetCache.images['zombie_leg_front'];\n\n            if (legTopImg && legSideImg && legFrontImg) {\n              // Create textures from images\n              const topTex = new THREE.Texture(legTopImg);\n              topTex.magFilter = THREE.NearestFilter;\n              topTex.minFilter = THREE.NearestFilter;\n              topTex.needsUpdate = true;\n\n              const sideTex = new THREE.Texture(legSideImg);\n              sideTex.magFilter = THREE.NearestFilter;\n              sideTex.minFilter = THREE.NearestFilter;\n              sideTex.needsUpdate = true;\n\n              const frontTex = new THREE.Texture(legFrontImg);\n              frontTex.magFilter = THREE.NearestFilter;\n              frontTex.minFilter = THREE.NearestFilter;\n              frontTex.needsUpdate = true;\n\n              // Create materials for each face (right, left, top, bottom, front, back)\n              const leftLegMaterials = [\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // right\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // left\n                new THREE.MeshLambertMaterial({\n                  map: topTex,\n                }), // top\n                new THREE.MeshLambertMaterial({\n                  map: topTex,\n                }), // bottom\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // front\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // back\n              ];\n\n              const leftLegGeo = new THREE.BoxGeometry(0.2, 0.6, 0.2);\n              const leftLeg = new THREE.Mesh(leftLegGeo, leftLegMaterials);\n              leftLeg.position.set(-0.15, -0.65, 0);\n              mob.group.add(leftLeg);\n              mob.leftLeg = leftLeg;\n\n              const rightLegMaterials = [\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // right\n                new THREE.MeshLambertMaterial({\n                  map: sideTex,\n                }), // left\n                new THREE.MeshLambertMaterial({\n                  map: topTex,\n                }), // top\n                new THREE.MeshLambertMaterial({\n                  map: topTex,\n                }), // bottom\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // front\n                new THREE.MeshLambertMaterial({\n                  map: frontTex,\n                }), // back\n              ];\n\n              const rightLegGeo = new THREE.BoxGeometry(0.2, 0.6, 0.2);\n              const rightLeg = new THREE.Mesh(rightLegGeo, rightLegMaterials);\n              rightLeg.position.set(0.15, -0.65, 0);\n              mob.group.add(rightLeg);\n              mob.rightLeg = rightLeg;\n            } else if (skinTexture) {\n              // Fallback to UV mapped legs\n              // Left leg (4x12x4 cube)\n              const leftLegUV = {\n                right: [16 * px, 52 * px, 4 * px, 12 * px],\n                left: [24 * px, 52 * px, 4 * px, 12 * px],\n                top: [20 * px, 48 * px, 4 * px, 4 * px],\n                bottom: [24 * px, 48 * px, 4 * px, 4 * px],\n                front: [20 * px, 52 * px, 4 * px, 12 * px],\n                back: [28 * px, 52 * px, 4 * px, 12 * px],\n              };\n              const leftLegGeo = createBodyPartGeometry(0.2, 0.6, 0.2, leftLegUV);\n              const leftLeg = new THREE.Mesh(\n                leftLegGeo,\n                new THREE.MeshLambertMaterial({\n                  map: skinTexture,\n                })\n              );\n              leftLeg.position.set(-0.15, -0.65, 0);\n              mob.group.add(leftLeg);\n              mob.leftLeg = leftLeg;\n\n              // Right leg (4x12x4 cube)\n              const rightLegUV = {\n                right: [0 * px, 20 * px, 4 * px, 12 * px],\n                left: [8 * px, 20 * px, 4 * px, 12 * px],\n                top: [4 * px, 16 * px, 4 * px, 4 * px],\n                bottom: [8 * px, 16 * px, 4 * px, 4 * px],\n                front: [4 * px, 20 * px, 4 * px, 12 * px],\n                back: [12 * px, 20 * px, 4 * px, 12 * px],\n              };\n              const rightLegGeo = createBodyPartGeometry(0.2, 0.6, 0.2, rightLegUV);\n              const rightLeg = new THREE.Mesh(\n                rightLegGeo,\n                new THREE.MeshLambertMaterial({\n                  map: skinTexture,\n                })\n              );\n              rightLeg.position.set(0.15, -0.65, 0);\n              mob.group.add(rightLeg);\n              mob.rightLeg = rightLeg;\n            }\n          } else if (type === MOB_TYPES.CREEPER) {\n            // Creeper - multiple green cubes\n            const textureKey = 'creeper_texture';\n            const texture = assetCache.images[textureKey];\n\n            let material;\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              material = new THREE.MeshLambertMaterial({\n                map: tex,\n              });\n            } else {\n              material = new THREE.MeshLambertMaterial({\n                color: 0x0da70d,\n              });\n            }\n\n            // Head\n            const head = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), material.clone());\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), material.clone());\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              material.clone()\n            );\n            frontLeftLeg.position.set(-0.15, -0.6, 0.15);\n            mob.group.add(frontLeftLeg);\n\n            // Front right leg\n            const frontRightLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.2, 0.4, 0.2),\n              material.clone()\n            );\n            frontRightLeg.position.set(0.15, -0.6, 0.15);\n            mob.group.add(frontRightLeg);\n\n            // Back left leg\n            const backLeftLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.2, 0.4, 0.2),\n              material.clone()\n            );\n            backLeftLeg.position.set(-0.15, -0.6, -0.15);\n            mob.group.add(backLeftLeg);\n\n            // Back right leg\n            const backRightLeg = new THREE.Mesh(\n              new THREE.BoxGeometry(0.2, 0.4, 0.2),\n              material.clone()\n            );\n            backRightLeg.position.set(0.15, -0.6, -0.15);\n            mob.group.add(backRightLeg);\n          } else if (type === MOB_TYPES.COW || type === MOB_TYPES.PIG) {\n            const textureKey = type === MOB_TYPES.COW ? 'cow_texture' : 'pig_texture';\n            const texture = assetCache.images[textureKey];\n            let material;\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              material = new THREE.MeshLambertMaterial({ map: tex });\n            } else {\n              material = new THREE.MeshLambertMaterial({ color: type === MOB_TYPES.COW ? 0x4b3621 : 0xffc0cb });\n            }\n\n            // Head\n            const head = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), material);\n            head.position.set(0, 0.2, 0.4);\n            mob.group.add(head);\n\n            // Body\n            const body = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.6, 0.8), material);\n            body.position.set(0, 0, 0);\n            mob.group.add(body);\n\n            // Legs\n            const legGeo = new THREE.BoxGeometry(0.2, 0.4, 0.2);\n            const fl = new THREE.Mesh(legGeo, material); fl.position.set(-0.2, -0.4, 0.25); mob.group.add(fl);\n            const fr = new THREE.Mesh(legGeo, material); fr.position.set(0.2, -0.4, 0.25); mob.group.add(fr);\n            const bl = new THREE.Mesh(legGeo, material); bl.position.set(-0.2, -0.4, -0.25); mob.group.add(bl);\n            const br = new THREE.Mesh(legGeo, material); br.position.set(0.2, -0.4, -0.25); mob.group.add(br);\n            \n            if (type === MOB_TYPES.COW) {\n              const hornGeo = new THREE.BoxGeometry(0.1, 0.2, 0.1);\n              const hornMat = new THREE.MeshLambertMaterial({ color: 0xeeeeee });\n              const lh = new THREE.Mesh(hornGeo, hornMat); lh.position.set(-0.15, 0.45, 0.4); mob.group.add(lh);\n              const rh = new THREE.Mesh(hornGeo, hornMat); rh.position.set(0.15, 0.45, 0.4); mob.group.add(rh);\n            }\n          }\n\n          mob.group.position.copy(mob.position);\n          scene.add(mob.group);\n\n          return mob;\n        }\n\n        function spawnMob() {\n          if (mobs.length >= MAX_MOBS) return;\n          \n          // Passive mobs spawn during day, hostile at night\n          const enabledHostile = [];\n          if (config.zombiesEnabled) enabledHostile.push(MOB_TYPES.ZOMBIE);\n          if (config.creepersEnabled) enabledHostile.push(MOB_TYPES.CREEPER);\n\n          const enabledPassive = [MOB_TYPES.COW, MOB_TYPES.PIG];\n          \n          let enabledTypes = isDay ? enabledPassive : (enabledHostile.length > 0 ? enabledHostile : enabledPassive);\n          if (enabledTypes.length === 0) return;\n\n          // Random position around player (infinite world - no bounds check)\n          const angle = timeSyncer.random() * Math.PI * 2;\n          const distance = 10 + timeSyncer.random() * 20;\n          const x = playerPos.x + Math.cos(angle) * distance;\n          const z = playerPos.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          // Look around player's height to find the actual surface quickly\n          const startSearchY = Math.min(worldHeight - 1, Math.max(0, Math.floor(playerPos.y) + 10));\n          for (let y = startSearchY; 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(timeSyncer.random() * enabledTypes.length)];\n          const mob = createMob(type, wx + 0.5, spawnY + 0.5, wz + 0.5);\n          mobs.push(mob);\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 are walkable - skip collision\n              if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.TORCH) {\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          // Update animation time based on movement speed\n          const movementSpeed = Math.sqrt(mob.velocity.x ** 2 + mob.velocity.z ** 2);\n          const animationSpeed = Math.max(0.5, movementSpeed * 0.8);\n          // Slower animation for legs\n          mob.animationTime += dt * animationSpeed;\n\n          // Arm animation - keep arms stiff and extended forward\n          const armForwardAngle = -Math.PI / 2;\n          // 90 degrees forward (arms extended) - negative for forward rotation\n\n          if (mob.leftArm) {\n            mob.leftArm.rotation.x = armForwardAngle;\n            // Left arm stays forward\n          }\n          if (mob.rightArm) {\n            mob.rightArm.rotation.x = armForwardAngle;\n            // Right arm stays forward\n          }\n\n          // Leg animation - slow oscillation for walking\n          const legSwingAmount = Math.PI / 8;\n          // 22.5 degrees max swing\n          const legPhase = Math.sin(mob.animationTime * Math.PI) * legSwingAmount;\n          // Slower: only PI instead of PI*2\n\n          if (mob.leftLeg) {\n            mob.leftLeg.rotation.x = legPhase;\n            // Left leg swings forward\n          }\n          if (mob.rightLeg) {\n            mob.rightLeg.rotation.x = -legPhase;\n            // Right leg swings backward (opposite)\n          }\n        }\n\n        function updateMobs(dt, currentTime) {\n          // Spawn mobs - only when active in world\n          if (currentTime > nextMobSpawn) {\n            spawnMob();\n            nextMobSpawn = currentTime + MOB_SPAWN_INTERVAL;\n          }\n\n          // Cleanup dead mobs first\n          for (let i = mobs.length - 1; i >= 0; i--) {\n            if (mobs[i].health <= 0) {\n              scene.remove(mobs[i].group);\n              mobs.splice(i, 1);\n            }\n          }\n\n          // Limit visible mobs to 15 closest\n          const MAX_VISIBLE_MOBS = 15;\n          if (mobs.length > MAX_VISIBLE_MOBS) {\n            const mobDistances = mobs.map((mob, index) => {\n              const dx = mob.position.x - playerPos.x;\n              const dy = mob.position.y - playerPos.y;\n              const dz = mob.position.z - playerPos.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 - ONLY for hostile mobs (Zombies and Creepers)\n            if (isDay && (mob.type === MOB_TYPES.ZOMBIE || mob.type === MOB_TYPES.CREEPER)) {\n              const SUN_DAMAGE_PER_SECOND = 2;\n              damageMob(mob, SUN_DAMAGE_PER_SECOND * dt);\n\n              // Create burning particles\n              if (timeSyncer.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, timeSyncer.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                  (timeSyncer.random() - 0.5) * 0.5,\n                  timeSyncer.random() * 2 + 1,\n                  (timeSyncer.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 distToPlayer = mob.position.distanceTo(playerPos);\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.children.forEach((mesh) => {\n                if (mesh.material) {\n                  mesh.material.emissive = flashColor;\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 wander\n            if (!mob.exploding) {\n              if (mob.targetPlayer) {\n                const direction = new THREE.Vector3().subVectors(playerPos, 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                  // Rotate to face player\n                  const angle = Math.atan2(direction.x, direction.z);\n                  mob.group.rotation.y = angle;\n                }\n              } else {\n                // Passive mob wandering\n                if (!mob.wanderDir || timeSyncer.random() < 0.01) {\n                  const angle = timeSyncer.random() * Math.PI * 2;\n                  mob.wanderDir = new THREE.Vector3(Math.cos(angle), 0, Math.sin(angle));\n                }\n                \n                mob.velocity.x = mob.wanderDir.x * 1.0;\n                mob.velocity.z = mob.wanderDir.z * 1.0;\n                \n                const angle = Math.atan2(mob.wanderDir.x, mob.wanderDir.z);\n                mob.group.rotation.y = angle;\n              }\n            }\n\n            // Animate zombie\n            animateZombie(mob, dt);\n\n            // Zombie attack\n            if (mob.type === MOB_TYPES.ZOMBIE) {\n              const distToPlayer = mob.position.distanceTo(playerPos);\n              if (distToPlayer < 2) {\n                damagePlayer(2, currentTime);\n              }\n            }\n\n            // Play sounds occasionally\n            if (currentTime - mob.lastSoundTime > 5 && timeSyncer.random() < 0.01) {\n              if (mob.type === MOB_TYPES.ZOMBIE) {\n                playSound('zombie_groan_sound', false, 0.4);\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            // Gravity\n            mob.velocity.y += GRAVITY * dt;\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) {\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) {\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            // Remove if too far or dead\n            if (mob.health <= 0) {\n              scene.remove(mob.group);\n              mobs.splice(i, 1);\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 distToPlayer = mob.position.distanceTo(playerPos);\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 (dist <= explosionRadius && blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.BEDROCK) {\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 (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\n        function damageMob(mob, damage) {\n          if (damage <= 0) return; // Don't trigger flash for 0 damage\n          mob.health -= damage;\n\n          // Flash red - store original colors for each mesh\n          const originalColors = [];\n          mob.group.children.forEach((mesh) => {\n            if (mesh.material) {\n              // Handle both single material and material arrays\n              if (Array.isArray(mesh.material)) {\n                const colors = [];\n                mesh.material.forEach((mat) => {\n                  if (mat.color) {\n                    colors.push(mat.color.clone());\n                    mat.color.setHex(0xff0000);\n                  }\n                });\n                originalColors.push(colors);\n              } else {\n                if (mesh.material.color) {\n                  originalColors.push(mesh.material.color.clone());\n                  mesh.material.color.setHex(0xff0000);\n                } else {\n                  originalColors.push(null);\n                }\n              }\n            } else {\n              originalColors.push(null);\n            }\n          });\n          setTimeout(() => {\n            if (mob.group && mob.group.children) {\n              mob.group.children.forEach((mesh, idx) => {\n                const originalColor = originalColors[idx];\n                if (originalColor && mesh.material) {\n                  // Handle both single material and material arrays\n                  if (Array.isArray(mesh.material)) {\n                    mesh.material.forEach((mat, matIdx) => {\n                      if (mat.color && Array.isArray(originalColor) && originalColor[matIdx]) {\n                        mat.color.copy(originalColor[matIdx]);\n                      }\n                    });\n                  } else {\n                    if (\n                      mesh.material.color &&\n                      typeof originalColor === 'object' &&\n                      !Array.isArray(originalColor)\n                    ) {\n                      mesh.material.copy(originalColor); // FIXED: Copy back full material properties\n                    }\n                  }\n                }\n              });\n            }\n          }, 150); // Slightly longer visible flash\n\n          if (mob.health <= 0) {\n            // Drop meat if it's an animal\n            if (mob.type === MOB_TYPES.COW || mob.type === MOB_TYPES.PIG) {\n              addToInventory(ITEM_TYPES.MEAT);\n              showMessage('\ud83e\udd69 Collected Meat!', '#4CAF50', 1000);\n            }\n\n            // Death particles\n            let color = 0xffffff;\n            if (mob.type === MOB_TYPES.ZOMBIE) color = 0x5a7a3f;\n            else if (mob.type === MOB_TYPES.CREEPER) color = 0x0da70d;\n            else if (mob.type === MOB_TYPES.PIG) color = 0xffc0cb;\n            else if (mob.type === MOB_TYPES.COW) color = 0x4b3621;\n\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        }\n\n        function sendWorldState() {\n          syncEvent('world_state', {\n            modifiedBlocks: Array.from(modifiedBlocks),\n            signMessages: Array.from(signMessages),\n            chestContents: Array.from(chestContents),\n            furnaceContents: Array.from(furnaceContents)\n          });\n        }\n\n        function recieveWorldState(receivedData) {\n          if (!justJoined) return;\n          justJoined = false;\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 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\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){ return a.timestamp - b.timestamp });\n\n           return peerIds[0].peerId;\n         };\n\n         // Override IAmActingServer to use our custom getActingServer\n         window.IAmActingServer = function(lobby, timeSyncer) {\n           const serverPeerId = firstPlayerJoined(lobby, timeSyncer);\n           return (lobby.getSelfPeerId() === serverPeerId);\n         };\n\n        // place global events here\n        window.timeSyncer = new TimestampDisplay(1688647930, 5, () => {\n          if (!lobby || !lobby.fbobj) return;\n          if (IAmActingServer(lobby, timeSyncer)) {\n            lobby.fbobj.updateServerTimestamp(); // update global time\n\n            // prevent spam\n            const playersCount = otherPlayers.size + 1;\n            if(lastPlayersCount == playersCount) return;\n            lastPlayersCount = playersCount;\n\n            sendWorldState(); // sync world state to new players\n          }\n        });\n\n        setInterval(() => {\n          timeSyncer.update();\n        }, 30);\n\n        function damagePlayer(damage, currentTime) {\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          const survivalSecs = Math.floor(lavaSurvivalTime);\n\n          // Stop the game immediately\n          isPaused = true;\n          isMining = false;\n          miningProgress = 0;\n\n          // Exit pointer lock\n          try {\n            if (document.pointerLockElement) document.exitPointerLock?.();\n          } catch (e) {}\n          isPointerLocked = false;\n\n          // Red flash overlay\n          const flashEl = document.createElement('div');\n          flashEl.style.cssText = `\n            position: absolute; top: 0; left: 0; width: 100%; height: 100%;\n            background: rgba(200, 0, 0, 0.7); pointer-events: none; z-index: 50;\n          `;\n          gameWorld.appendChild(flashEl);\n\n          // Show permanent Game Over screen after brief flash\n          setTimeout(() => {\n            if (flashEl.parentNode) flashEl.remove();\n\n            const gameOverScreen = document.createElement('div');\n            gameOverScreen.id = 'game-over-screen';\n            gameOverScreen.style.cssText = `\n              position: absolute; top: 0; left: 0; width: 100%; height: 100%;\n              background: rgba(0, 0, 0, 0.92);\n              display: flex; flex-direction: column;\n              align-items: center; justify-content: center;\n              z-index: 300; backdrop-filter: blur(6px);\n            `;\n            gameOverScreen.innerHTML = `\n              <div style=\"text-align: center; padding: 40px;\">\n                <div style=\"font-family: 'VT323', monospace; font-size: 88px; color: #FF2222;\n                  text-shadow: 0 0 30px rgba(255,34,34,0.8), 4px 4px 0 #000;\n                  margin-bottom: 10px; letter-spacing: 4px;\">\n                  YOU DIED\n                </div>\n                <div style=\"font-family: 'VT323', monospace; font-size: 36px; color: rgba(255,255,255,0.8);\n                  margin-bottom: 6px;\">\n                  \u2620\ufe0f The lava got you!\n                </div>\n                <div style=\"font-family: 'VT323', monospace; font-size: 30px; color: #FF6600;\n                  margin-bottom: 40px;\">\n                  \ud83c\udf0b Survived: <span style=\"color:#FFD700;\">${survivalSecs}s</span> &nbsp;|&nbsp; Lava Level: <span style=\"color:#FF4500;\">${Math.floor(lavaLevel)}</span>\n                </div>\n                <button id=\"game-over-restart-btn\" style=\"\n                  font-family: 'VT323', monospace; font-size: 40px;\n                  padding: 18px 60px;\n                  background: linear-gradient(135deg, rgba(255,34,34,0.4), rgba(180,0,0,0.4));\n                  color: #FF4444; border: 3px solid #FF4444; border-radius: 14px;\n                  cursor: pointer; letter-spacing: 2px;\n                  box-shadow: 0 0 20px rgba(255,34,34,0.4);\n                  transition: all 0.2s;\n                \">\u25b6 PLAY AGAIN</button>\n              </div>\n            `;\n            gameWorld.appendChild(gameOverScreen);\n\n            // Play Again = restart game\n            document.getElementById('game-over-restart-btn').addEventListener('click', () => {\n              restartGame();\n            });\n            document.getElementById('game-over-restart-btn').addEventListener('mouseenter', (e) => {\n              e.target.style.background = 'linear-gradient(135deg, rgba(255,34,34,0.7), rgba(180,0,0,0.7))';\n              e.target.style.transform = 'scale(1.05)';\n            });\n            document.getElementById('game-over-restart-btn').addEventListener('mouseleave', (e) => {\n              e.target.style.background = 'linear-gradient(135deg, rgba(255,34,34,0.4), rgba(180,0,0,0.4))';\n              e.target.style.transform = 'scale(1)';\n            });\n          }, 600);\n        }\n\n        function attackMob() {\n          // Raycast to find mob\n          raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);\n\n          // Collect all meshes from all mob groups\n          const mobMeshes = [];\n          mobs.forEach((mob) => {\n            mobMeshes.push(...mob.group.children);\n          });\n\n          const intersects = raycaster.intersectObjects(mobMeshes);\n\n          if (intersects.length > 0) {\n            const hitMesh = intersects[0].object;\n            // Find which mob this mesh belongs to\n            const mob = mobs.find((m) => m.group.children.includes(hitMesh));\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              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.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\n          const targetVelX = moveDir.x * moveSpeed;\n          const targetVelZ = moveDir.z * moveSpeed;\n\n          playerVelocity.x = targetVelX;\n          playerVelocity.z = targetVelZ;\n\n          // Apply gravity\n          playerVelocity.y += GRAVITY * dt;\n\n          // Move with collision\n          const newPos = playerPos.clone();\n\n          // X movement\n          newPos.x += playerVelocity.x * dt;\n          if (checkCollision(newPos)) {\n            newPos.x = playerPos.x;\n            playerVelocity.x = 0;\n          }\n\n          // Z movement\n          newPos.z += playerVelocity.z * dt;\n          if (checkCollision(newPos)) {\n            newPos.z = playerPos.z;\n            playerVelocity.z = 0;\n          }\n\n          // Y movement\n          newPos.y += playerVelocity.y * dt;\n          if (checkCollision(newPos)) {\n            if (playerVelocity.y < 0) {\n              isGrounded = true;\n              coyoteTimeRemaining = COYOTE_TIME;\n            }\n            newPos.y = playerPos.y;\n            playerVelocity.y = 0;\n          } else {\n            // Probe directly below to catch standing-still edge cases\n            const probePos = playerPos.clone();\n            probePos.y -= 0.15;\n            if (checkCollision(probePos)) {\n              isGrounded = true;\n              coyoteTimeRemaining = COYOTE_TIME;\n            } else {\n              if (isGrounded) coyoteTimeRemaining = COYOTE_TIME;\n              isGrounded = false;\n              coyoteTimeRemaining = Math.max(0, coyoteTimeRemaining - dt);\n            }\n          }\n\n          playerPos.copy(newPos);\n\n          // Infinite world - no X/Z bounds clamping\n\n          // Prevent falling through world\n          if (playerPos.y < 1) {\n            playerPos.y = 1;\n            playerVelocity.y = 0;\n            isGrounded = true;\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\n        function checkCollision(pos) {\n          // Check collision with blocks around player (infinite world - only check Y bounds)\n          const checkPoints = [\n            [pos.x - PLAYER_RADIUS, pos.y, pos.z - PLAYER_RADIUS],\n            [pos.x + PLAYER_RADIUS, pos.y, pos.z - PLAYER_RADIUS],\n            [pos.x - PLAYER_RADIUS, pos.y, pos.z + PLAYER_RADIUS],\n            [pos.x + PLAYER_RADIUS, pos.y, pos.z + PLAYER_RADIUS],\n            [pos.x - PLAYER_RADIUS, pos.y - PLAYER_HEIGHT, pos.z - PLAYER_RADIUS],\n            [pos.x + PLAYER_RADIUS, pos.y - PLAYER_HEIGHT, pos.z - PLAYER_RADIUS],\n            [pos.x - PLAYER_RADIUS, pos.y - PLAYER_HEIGHT, pos.z + PLAYER_RADIUS],\n            [pos.x + PLAYER_RADIUS, pos.y - PLAYER_HEIGHT, pos.z + PLAYER_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 are walkable - skip collision\n              if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.TORCH) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }\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 = 400;\n\n          if (cycleProgress < 0.5) {\n            // Day - sun visible\n            const sunProgress = cycleProgress * 2;\n            const sunAngle = sunProgress * Math.PI;\n\n            // Position sun on an arc from east to west\n            sun.position.x = Math.cos(sunAngle) * skyRadius;\n            sun.position.y = Math.sin(sunAngle) * skyRadius;\n            sun.position.z = 0; // Move it to the center instead of -100\n\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            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 = 0;\n\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(\n                0,\n                audioContext.currentTime + 1\n              );\n              setTimeout(() => {\n                if (oldMusic && oldMusic.source) {\n                  try { oldMusic.source.stop(); } 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        // ===== FLOOR IS LAVA FUNCTIONS =====\n\n        function createLavaPlane() {\n          // Create a large lava plane that rises\n          const lavaGeometry = new THREE.PlaneGeometry(500, 500);\n          const lavaMaterial = new THREE.MeshBasicMaterial({\n            color: 0xff4500,\n            transparent: true,\n            opacity: 0.85,\n            side: THREE.DoubleSide,\n          });\n          lavaPlaneMesh = new THREE.Mesh(lavaGeometry, lavaMaterial);\n          lavaPlaneMesh.rotation.x = -Math.PI / 2;\n          lavaPlaneMesh.position.y = lavaLevel + 0.5;\n          scene.add(lavaPlaneMesh);\n\n          // Add a warm light from the lava\n          lavaLight = new THREE.PointLight(0xff4500, 2, 50, 1);\n          lavaLight.position.set(0, lavaLevel + 2, 0);\n          scene.add(lavaLight);\n        }\n\n        function updateFloorIsLava(dt, currentTime) {\n          if (!lavaPlaneMesh) return;\n\n          const startDelay = config.lavaStartDelay || lavaStartDelay;\n\n          lavaElapsedTime += dt;\n          lavaSurvivalTime += dt;\n\n          // Grace period before lava starts rising\n          if (!lavaStarted) {\n            const remaining = Math.ceil(startDelay - lavaElapsedTime);\n            const lavaTimerEl = document.getElementById('lava-timer');\n            if (lavaTimerEl) {\n              if (remaining > 0) {\n                lavaTimerEl.textContent = '\ud83c\udf0b LAVA RISING IN: ' + remaining + 's';\n                lavaTimerEl.style.color = '#FFD700';\n              }\n            }\n            if (lavaElapsedTime >= startDelay) {\n              lavaStarted = true;\n              const diffLabels = { easy: '\ud83d\udfe2 EASY', medium: '\ud83d\udfe1 MEDIUM', hard: '\ud83d\udd34 HARD' };\n              const diffLabel = diffLabels[config.difficulty] || '';\n              showMessage('\ud83c\udf0b THE FLOOR IS LAVA! ' + diffLabel + ' RUN!', '#FF4500', 3000);\n            }\n            return;\n          }\n\n          // Rise the lava level continuously at the configured speed (blocks per second)\n          // Derive rise speed from difficulty if set\n          const difficultySpeedMap = { easy: 0.2, medium: 0.6, hard: 1.0 };\n          const riseSpeed = config.difficulty\n            ? (difficultySpeedMap[config.difficulty] || config.lavaRiseSpeed || 0.2)\n            : (config.lavaRiseSpeed || 0.2);\n          lavaLevel += riseSpeed * dt;\n\n          // Generate more platforms ahead and cleanup submerged ones\n          if (lavaLevel + PLATFORMS_AHEAD * PLATFORM_SPACING_Y > lastPlatformGenY) {\n            generateParkourPlatforms();\n          }\n          cleanupSubmergedPlatforms();\n\n          // Update lava plane position\n          lavaPlaneMesh.position.y = lavaLevel + 0.5;\n          lavaPlaneMesh.position.x = playerPos.x;\n          lavaPlaneMesh.position.z = playerPos.z;\n\n          // Animate lava color/opacity for a flowing effect\n          const pulse = 0.75 + Math.sin(performance.now() * 0.003) * 0.1;\n          lavaPlaneMesh.material.opacity = pulse;\n          const colorPulse = 0.8 + Math.sin(performance.now() * 0.005) * 0.2;\n          lavaPlaneMesh.material.color.setRGB(1, 0.2 + colorPulse * 0.15, 0);\n\n          // Update lava light position\n          if (lavaLight) {\n            lavaLight.position.set(playerPos.x, lavaLevel + 3, playerPos.z);\n            lavaLight.intensity = 2 + Math.sin(performance.now() * 0.004) * 0.5;\n          }\n\n          // Update UI\n          const lavaTimerEl = document.getElementById('lava-timer');\n          if (lavaTimerEl) {\n            const survivalSecs = Math.floor(lavaSurvivalTime);\n            lavaTimerEl.textContent = '\ud83c\udf0b LAVA LVL: ' + Math.floor(lavaLevel) + ' | \u23f1\ufe0f ' + survivalSecs + 's';\n            lavaTimerEl.style.color = '#FF6600';\n          }\n\n          // Check if player is in lava (feet position)\n          const playerFeetY = playerPos.y - PLAYER_HEIGHT;\n          if (playerFeetY <= lavaLevel + 0.5) {\n            // Player is in lava! Deal damage\n            if (currentTime - lastLavaDamageTime >= LAVA_DAMAGE_INTERVAL) {\n              damagePlayer(LAVA_DAMAGE, currentTime);\n              lastLavaDamageTime = currentTime;\n\n              // Create lava splash particles\n              for (let i = 0; i < 5; i++) {\n                const geometry = new THREE.BoxGeometry(0.15, 0.15, 0.15);\n                const material = new THREE.MeshBasicMaterial({\n                  color: new THREE.Color(1, Math.random() * 0.4, 0),\n                });\n                const particle = new THREE.Mesh(geometry, material);\n                particle.position.set(\n                  playerPos.x + (Math.random() - 0.5) * 1,\n                  lavaLevel + 0.5,\n                  playerPos.z + (Math.random() - 0.5) * 1\n                );\n                particle.userData.velocity = new THREE.Vector3(\n                  (Math.random() - 0.5) * 3,\n                  Math.random() * 4 + 2,\n                  (Math.random() - 0.5) * 3\n                );\n                particle.userData.life = 0.8;\n                scene.add(particle);\n                particles.push(particle);\n              }\n            }\n\n            // Slow the player down in lava\n            playerVelocity.x *= 0.7;\n            playerVelocity.z *= 0.7;\n          }\n\n          // Create ambient lava particles at the surface near the player\n          if (Math.random() < 0.15) {\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              transparent: true,\n              opacity: 0.9,\n            });\n            const particle = new THREE.Mesh(geometry, material);\n            particle.position.set(\n              playerPos.x + (Math.random() - 0.5) * 20,\n              lavaLevel + 0.5 + Math.random() * 0.3,\n              playerPos.z + (Math.random() - 0.5) * 20\n            );\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 = 1.0 + Math.random() * 0.5;\n            scene.add(particle);\n            particles.push(particle);\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            // Convert modifiedBlocks map to array for JSON serialization\n            const modificationsArray = Array.from(modifiedBlocks.entries()).map(\n              ([key, blockType]) => {\n                const [x, y, z] = key.split(',').map(Number);\n                return {\n                  x,\n                  y,\n                  z,\n                  blockType,\n                };\n              }\n            );\n\n            const saveData = {\n              version: 4,\n              // Increment version for chest contents\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              modifiedBlocks: modificationsArray, // Save block modifications\n              signMessages: Array.from(signMessages.entries()), // Save sign messages\n              chestContents: Array.from(chestContents.entries()), // Save chest contents\n              furnaceContents: Array.from(furnaceContents.entries()), // Save furnace contents\n            };\n\n            await lib.saveUserGameState(saveData);\n            showMessage('\u2713 Game Saved!', '#4CAF50', 2000);\n            lib.log(\n              'Game saved successfully with ' + modificationsArray.length + ' 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\n        function resetWorld() {\n          // Clear block data maps\n          blockMeshes.clear();\n          modifiedBlocks.clear();\n          signMessages.clear();\n          chestContents.clear();\n          furnaceContents.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\n          // Clear chest meshes from scene\n          if (typeof chestMeshes !== 'undefined') {\n            chestMeshes.forEach((mesh) => {\n              scene.remove(mesh);\n            });\n            chestMeshes.clear();\n          }\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          // Regenerate terrain\n          generateTerrain();\n          buildWorldMesh();\n        }\n        window.resetWorld = resetWorld;\n\n        function restartGame() {\n          const gameOverScreen = document.getElementById('game-over-screen');\n          if (gameOverScreen) gameOverScreen.remove();\n          \n          const victoryScreen = document.getElementById('victory-screen');\n          if (victoryScreen) victoryScreen.remove();\n\n          // Reset player\n          playerHealth = maxPlayerHealth;\n          playerHunger = maxPlayerHunger;\n          updateHealthBar();\n          updateHungerBar();\n          \n          playerPos = spawnPoint ? spawnPoint.clone() : findSpawnPoint();\n          playerVelocity.set(0, 0, 0);\n          playerYaw = 0;\n          playerPitch = 0;\n\n          // Reset lava\n          lavaLevel = getTerrainHeight(0, 0) - 3;\n          lavaElapsedTime = 0;\n          lavaSurvivalTime = 0;\n          lavaStarted = false;\n\n          // Sync lava plane & light to the reset lava level immediately\n          if (lavaPlaneMesh) {\n            lavaPlaneMesh.position.set(0, lavaLevel + 0.5, 0);\n            lavaPlaneMesh.material.opacity = 0.85;\n            lavaPlaneMesh.material.color.setHex(0xff4500);\n          }\n          if (lavaLight) {\n            lavaLight.position.set(0, lavaLevel + 2, 0);\n          }\n          \n          // Reset victory state\n          victoryAchieved = false;\n\n          // Clear parkour tracking\n          parkourPlatforms.length = 0;\n          lastPlatformGenY = Math.floor(lavaLevel) + 3;\n\n          // Clear mobs\n          for (let i = mobs.length - 1; i >= 0; i--) {\n            if (mobs[i].group) scene.remove(mobs[i].group);\n          }\n          mobs.length = 0;\n\n          // Reset inventory\n          inventory = JSON.parse(JSON.stringify(config.inventory));\n          while (inventory.length < 36) {\n            inventory.push({ type: BLOCK_TYPES.AIR, count: 0 });\n          }\n          selectedSlot = 0;\n          updateHotbarUI();\n\n          // Reset the world blocks\n          resetWorld();\n          \n          // Re-generate platforms\n          generateParkourPlatforms();\n\n          // Re-place golden light platform blocks\n          for (let dx = -1; dx <= 1; dx++) {\n            for (let dz = -1; dz <= 1; dz++) {\n              setBlockAt(dx, VICTORY_HEIGHT, dz, BLOCK_TYPES.GLASS);\n              modifiedBlocks.set(`${dx},${VICTORY_HEIGHT},${dz}`, BLOCK_TYPES.GLASS);\n              updateBlockAt(dx, VICTORY_HEIGHT, dz);\n            }\n          }\n\n          // Ensure camera is updated immediately so there isn't a flicker\n          camera.position.copy(playerPos);\n          camera.rotation.order = 'YXZ';\n          camera.rotation.y = playerYaw;\n          camera.rotation.x = playerPitch;\n\n          isPaused = false;\n        }\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            // Regenerate world from seed\n            lib.log('Regenerating world from seed: ' + saveData.worldSeed);\n            config.worldSeed = saveData.worldSeed;\n\n            resetWorld();\n\n            // Apply saved modifications\n            if (saveData.modifiedBlocks && Array.isArray(saveData.modifiedBlocks)) {\n              lib.log('Applying ' + saveData.modifiedBlocks.length + ' block modifications');\n              for (const mod of saveData.modifiedBlocks) {\n                const { x, y, z, blockType } = mod;\n                if (\n                  x >= 0 &&\n                  x < worldWidth &&\n                  y >= 0 &&\n                  y < worldHeight &&\n                  z >= 0 &&\n                  z < worldDepth\n                ) {\n                  worldData[x][y][z] = blockType;\n                  const blockKey = `${x},${y},${z}`;\n                  modifiedBlocks.set(blockKey, blockType);\n                }\n              }\n            }\n\n            // Restore sign messages\n            if (saveData.signMessages && Array.isArray(saveData.signMessages)) {\n              lib.log('Restoring ' + saveData.signMessages.length + ' sign messages');\n              for (const [key, message] of saveData.signMessages) {\n                signMessages.set(key, message);\n                // Update text if sign mesh exists\n                const signMesh = signMeshes.get(key);\n                if (signMesh) {\n                  updateSignText(signMesh, message);\n                }\n              }\n            }\n\n            // Restore chest contents\n            if (saveData.chestContents && Array.isArray(saveData.chestContents)) {\n              lib.log('Restoring ' + saveData.chestContents.length + ' chest inventories');\n              for (const [key, contents] of saveData.chestContents) {\n                chestContents.set(key, contents);\n              }\n            }\n\n            // Restore furnace contents\n            if (saveData.furnaceContents && Array.isArray(saveData.furnaceContents)) {\n              lib.log('Restoring ' + saveData.furnaceContents.length + ' furnace inventories');\n              for (const [key, contents] of saveData.furnaceContents) {\n                furnaceContents.set(key, contents);\n              }\n            }\n\n            // Rebuild visible blocks\n            updateVisibleBlocks();\n\n            // Restore player position and rotation\n            playerPos.set(saveData.playerPos.x, saveData.playerPos.y, saveData.playerPos.z);\n            playerYaw = saveData.playerYaw;\n            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            playerHealth = saveData.playerHealth;\n            updateHealthBar();\n\n            // Restore player hunger if available\n            if (saveData.playerHunger !== undefined) {\n              playerHunger = saveData.playerHunger;\n              updateHungerBar();\n            }\n\n            // Restore inventory\n            inventory = JSON.parse(JSON.stringify(saveData.inventory));\n            // Ensure we have 36 slots total\n            while (inventory.length < 36) {\n              inventory.push({ type: BLOCK_TYPES.AIR, count: 0 });\n            }\n            selectedSlot = saveData.selectedSlot;\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              '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              '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              'Force Mobile UI': {\n                key: 'forceMobileUI',\n                type: 'checkbox',\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              'Difficulty': {\n                key: 'difficulty',\n                type: 'dropdown',\n                options: [\n                  { label: '\ud83d\udfe2 Easy (1 block / 5 sec)', value: 'easy' },\n                  { label: '\ud83d\udfe1 Medium (3 blocks / 5 sec)', value: 'medium' },\n                  { label: '\ud83d\udd34 Hard (1 block / sec)', value: 'hard' },\n                ],\n                onChange: (value) => {\n                  config.difficulty = value;\n                  const speedMap = { easy: 0.2, medium: 0.6, hard: 1.0 };\n                  config.lavaRiseSpeed = speedMap[value] || 0.2;\n                },\n              },\n              'Lava Rise Speed': {\n                key: 'lavaRiseSpeed',\n                type: 'slider',\n                min: 0.05,\n                max: 1.0,\n                step: 0.05,\n              },\n              'Lava Start Delay (sec)': {\n                key: 'lavaStartDelay',\n                type: 'slider',\n                min: 5,\n                max: 60,\n                step: 5,\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        // Show start instructions overlay\n        function showStartInstructions() {\n          isPaused = true;\n\n          const overlay = document.createElement('div');\n          overlay.id = 'start-instructions-overlay';\n          overlay.style.cssText = `\n            position: absolute; top: 0; left: 0; width: 100%; height: 100%;\n            background: rgba(0,0,0,0.88);\n            display: flex; flex-direction: column;\n            align-items: center; justify-content: center;\n            z-index: 350; backdrop-filter: blur(6px);\n            overflow-y: auto;\n          `;\n\n          overlay.innerHTML = `\n            <div style=\"text-align:center; padding: 32px 28px; max-width: 520px; width: 92%;\">\n              <div style=\"font-family:'VT323',monospace; font-size:72px; color:#FF4500;\n                text-shadow: 0 0 30px rgba(255,69,0,0.8), 4px 4px 0 #000;\n                line-height:1; margin-bottom:6px; letter-spacing:2px;\">\n                \ud83c\udf0b FLOOR IS LAVA\n              </div>\n              <div style=\"font-family:'VT323',monospace; font-size:28px; color:#FFD700;\n                margin-bottom:24px; text-shadow: 2px 2px 0 #000;\">\n                PARKOUR UP OR BURN! \ud83d\udd25\n              </div>\n\n              <!-- Objective -->\n              <div style=\"background:rgba(255,69,0,0.15); border:2px solid rgba(255,69,0,0.5);\n                border-radius:12px; padding:14px 18px; margin-bottom:18px; text-align:left;\">\n                <div style=\"font-family:'VT323',monospace; font-size:24px; color:#FF6600;\n                  margin-bottom:8px; letter-spacing:1px;\">\ud83c\udfaf OBJECTIVE</div>\n                <div style=\"font-family:'Inter',sans-serif; font-size:15px; color:rgba(255,255,255,0.9); line-height:1.6;\">\n                  Lava rises from below \u2014 <strong style=\"color:#FFD700;\">jump from platform to platform</strong>\n                  and climb as high as possible.<br>\n                  Reach the <strong style=\"color:#FFD700;\">\u2728 golden light</strong> at the top to win!\n                </div>\n              </div>\n\n              <!-- Controls -->\n              <div style=\"background:rgba(255,255,255,0.05); border:2px solid rgba(255,255,255,0.15);\n                border-radius:12px; padding:14px 18px; margin-bottom:18px;\">\n                <div style=\"font-family:'VT323',monospace; font-size:24px; color:#87CEEB;\n                  margin-bottom:10px; letter-spacing:1px;\">\ud83c\udfae CONTROLS</div>\n                <div style=\"display:grid; grid-template-columns:1fr 1fr; gap:8px 16px;\n                  font-family:'Inter',sans-serif; font-size:14px; color:rgba(255,255,255,0.85); text-align:left;\">\n                  ${isTouchDevice ? `\n                  <div>\u2b06\ufe0f <strong>Left Joystick</strong></div><div>Move</div>\n                  <div>\ud83d\udc46 <strong>Right Drag</strong></div><div>Look Around</div>\n                  <div>\u2b06 <strong>Blue Button</strong></div><div>Jump</div>\n                  <div>\ud83d\udce6 <strong>Green Button</strong></div><div>Place Block</div>\n                  <div>\ud83c\udf92 <strong>Bag Button</strong></div><div>Inventory</div>\n                  <div>\u23f8 <strong>Pause Button</strong></div><div>Menu</div>\n                  ` : `\n                  <div>\u2328\ufe0f <strong>WASD</strong></div><div>Move</div>\n                  <div>\ud83d\uddb1\ufe0f <strong>Mouse</strong></div><div>Look Around</div>\n                  <div>\ud83d\udd32 <strong>Space</strong></div><div>Jump</div>\n                  <div>\ud83d\uddb1\ufe0f <strong>Right-Click</strong></div><div>Place Block</div>\n                  <div>\ud83d\udce6 <strong>E</strong></div><div>Inventory</div>\n                  <div>\u23f8 <strong>M</strong></div><div>Pause Menu</div>\n                  `}\n                </div>\n              </div>\n\n              <!-- Tips -->\n              <div style=\"background:rgba(76,175,80,0.1); border:2px solid rgba(76,175,80,0.4);\n                border-radius:12px; padding:12px 18px; margin-bottom:24px; text-align:left;\">\n                <div style=\"font-family:'VT323',monospace; font-size:22px; color:#4CAF50;\n                  margin-bottom:6px;\">\ud83d\udca1 TIPS</div>\n                <ul style=\"font-family:'Inter',sans-serif; font-size:14px; color:rgba(255,255,255,0.85);\n                  line-height:1.7; margin:0; padding-left:18px;\">\n                  <li>Platforms appear ahead \u2014 keep moving <strong>UP</strong>!</li>\n                  <li>Don't let lava touch you \u2014 it deals heavy damage.</li>\n                  <li>Place blocks to bridge gaps or build higher.</li>\n                  <li>Eat food (\ud83c\udf5e R key) to restore health.</li>\n                </ul>\n              </div>\n\n              <button id=\"start-play-btn\" style=\"\n                font-family:'VT323',monospace; font-size:46px;\n                padding: 18px 70px;\n                background: linear-gradient(135deg, rgba(255,69,0,0.5), rgba(200,30,0,0.5));\n                color: #FF6600; border: 3px solid #FF4500; border-radius: 16px;\n                cursor: pointer; letter-spacing: 3px;\n                box-shadow: 0 0 25px rgba(255,69,0,0.5);\n                transition: all 0.2s; width:100%;\n              \">\u25b6 PLAY</button>\n            </div>\n          `;\n\n          gameWorld.appendChild(overlay);\n\n          const playBtn = document.getElementById('start-play-btn');\n          playBtn.addEventListener('mouseenter', () => {\n            playBtn.style.background = 'linear-gradient(135deg, rgba(255,69,0,0.8), rgba(200,30,0,0.8))';\n            playBtn.style.transform = 'scale(1.03)';\n          });\n          playBtn.addEventListener('mouseleave', () => {\n            playBtn.style.background = 'linear-gradient(135deg, rgba(255,69,0,0.5), rgba(200,30,0,0.5))';\n            playBtn.style.transform = 'scale(1)';\n          });\n\n          const dismiss = () => {\n            overlay.remove();\n            isPaused = false;\n            // Try requesting pointer lock on desktop\n            if (!isTouchDevice) {\n              try { canvas.requestPointerLock?.(); } catch(e) {}\n            }\n          };\n\n          playBtn.addEventListener('click', dismiss);\n          playBtn.addEventListener('touchend', (e) => { e.preventDefault(); dismiss(); });\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 (!isPaused && !isInventoryOpen && !isCraftingTableOpen && !isChestOpen && !isFurnaceOpen) {\n              const currentTime = timestamp / 1000;\n\n            // Update player\n            updatePlayer(dt);\n\n            // Update hunger (decrease over time and handle starvation)\n            lastHungerDecreaseTime += dt;\n            if (lastHungerDecreaseTime >= HUNGER_DECREASE_INTERVAL) {\n              if (playerHunger > 0) {\n                playerHunger = Math.max(0, playerHunger - 1);\n                updateHungerBar();\n              } else {\n                // Starve to death: deal 1 heart (2 HP) damage if hunger is 0\n                damagePlayer(2, currentTime);\n                showMessage('You are starving!', '#F44336', 1000);\n              }\n              lastHungerDecreaseTime = 0;\n            }\n\n            // Health regeneration if hunger is full (20)\n            if (playerHunger >= 20 && playerHealth < maxPlayerHealth) {\n                const regenCooldown = 4.0; // Regenerate 1 HP every 4 seconds\n                if (!window.lastRegenTime) window.lastRegenTime = 0;\n                if (currentTime - window.lastRegenTime >= regenCooldown) {\n                    playerHealth = Math.min(maxPlayerHealth, playerHealth + 1);\n                    updateHealthBar();\n                    window.lastRegenTime = currentTime;\n                }\n            }\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            // 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 =\n                onlineCount + ' Player' + (onlineCount > 1 ? 's' : '') + ' Online';\n            }\n\n            // Update camera matrix before frustum culling\n            camera.updateMatrixWorld();\n\n            // Update visible blocks based on player position\n            updateVisibleBlocks();\n\n            // Update mobs\n            updateMobs(dt, currentTime);\n\n            // Update Day/Night only if player is actually in the world (after potential teleport)\n            updateDayNight(dt);\n\n            // ===== FLOOR IS LAVA UPDATE =====\n            updateFloorIsLava(dt, currentTime);\n\n            // Update golden light goal\n            updateGoldenLight(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 torch mesh animations\n            updateTorchMeshes(dt);\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 Day/Night only if player is actually in the world (after potential teleport)\n            updateDayNight(dt);\n\n            // Update coordinates display\n            updatePlayerArm(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                },\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          await preloadAssets();\n\n          try {\n            const { gameId } = await lib.getUserGameState();\n\n            config.firebasePrefix = gameId;\n            remixed = gameId != '01KK8ZR1F8D71E698WDVMWF1Y4';\n          } catch (e) {\n            config.firebasePrefix = 'sandbox';\n            remixed = false;\n          }\n\n          updateLoadingProgress(32, 'Creating textures...');\n          createBlockTextures();\n\n          // Initialize infinite world (no pre-loading needed)\n          generateTerrain();\n\n          buildWorldMesh();\n\n          updateLoadingProgress(90, 'Setting up controls...');\n          createUI();\n          setupInputHandlers();\n\n          if (mode === 'edit') {\n            updateLoadingProgress(100, 'Ready!');\n            setupEditMode();\n          } else {\n            updateLoadingProgress(95, 'Finding spawn point...');\n            playerPos = findSpawnPoint();\n            spawnPoint = playerPos.clone();\n            // Store spawn point for respawning\n            camera.position.copy(playerPos);\n\n            // Create first-person player arm\n            createPlayerArm();\n\n            // Initialize Floor is Lava system\n            lavaLevel = getTerrainHeight(0, 0) - 3; // Start lava a few blocks below terrain\n            createLavaPlane();\n\n            // Generate initial parkour platforms\n            lastPlatformGenY = Math.floor(lavaLevel) + 3;\n            generateParkourPlatforms();\n\n            // Create golden light goal at the top\n            createGoldenLight();\n\n            // Initialize Firebase online\n            initFirebase();\n            if (isOnline) {\n              window.roomManager = new RoomManager(window.gameConfig.firebaseConfig, 'rooms', config.firebasePrefix);\n              listRooms();\n              subscribeToEvents();\n            }\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            // Initialize hunger bar display\n            updateHungerBar();\n\n            // Show start instructions overlay\n          showStartInstructions();\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": "{\"firebaseConfig\":{\"apiKey\":\"AIzaSyCxsNzN3eJyzyKzXxlkbI8ttCXr3iIIHLI\",\"authDomain\":\"trickster-game-sandbox.firebaseapp.com\",\"databaseURL\":\"wss://trickster-game-sandbox-default-rtdb.asia-southeast1.firebasedatabase.app\",\"projectId\":\"trickster-game-sandbox\"},\"worldWidth\":640,\"worldDepth\":640,\"worldHeight\":128,\"treeDensity\":0.02,\"renderDistance\":40,\"raycastSpreadMultiplier\":1,\"raycastPenetration\":1,\"rayCountX\":30,\"rayCountY\":30,\"miningSpeedMultiplier\":0.5,\"movementSpeedMultiplier\":1,\"dayNightCycleEnabled\":true,\"dayNightCycleDuration\":300,\"inventory\":[{\"type\":0,\"count\":0},{\"type\":0,\"count\":0},{\"type\":0,\"count\":0},{\"type\":0,\"count\":0},{\"type\":0,\"count\":0},{\"type\":0,\"count\":0},{\"type\":0,\"count\":0},{\"type\":0,\"count\":0},{\"type\":0,\"count\":0}],\"worldSeed\":1234567,\"worldData\":null,\"startTimeOfDay\":\"day\",\"zombiesEnabled\":false,\"creepersEnabled\":false,\"forceMobileUI\":false,\"onlineEnabled\":true,\"firebasePrefix\":\"minecraft-lite\",\"showWorldsUI\":true,\"lodNearDistance\":40,\"lodMediumDistance\":80,\"lodFarDistance\":160,\"lodEnabled\":false,\"chunkDeletionBufferMs\":2000,\"lavaRiseSpeed\":0.5,\"lavaStartDelay\":10,\"difficulty\":\"easy\"}", "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": "01KV7DVPTZVZQT6TXCGR9H7GBH", "title": "Minecraft Adventure: Floor Is Lava", "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_53bf71d7-8ead-45dd-9064-c2b76a843e39.webp", "created_at": 1781587108.0, "updated_at": 1781839472641.1423, "art_style": null, "is_remix": true, "base_game_id": "01KQP71VX1MDY06EJFBZX4KA86"}, "task_plan": null}