{"asset_map": {"player_character": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/player_character_ac88d671-693b-468b-b334-1f99adf6bdf9.webp", "type": "image", "prompt": "Generate a full-body, low-poly 3D survivor character model for a third-person game. The character is viewed from behind and slightly above in a neutral standing pose. Wearing a dark gray jacket, blue jeans, and brown boots. Stylized realism, clean geometric shapes, muted desaturated colors. Professional game asset, high resolution, transparent background, front-facing but designed for 3D third-person perspective.", "description": "Low-poly 3D survivor character for third-person gameplay", "subtype": "character", "aspect_ratio": [612, 1413], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "zombie_basic": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/zombie_basic_c30ab169-d050-473c-81cf-8b9954500654.webp", "type": "image", "prompt": "Generate a full-body, low-poly 3D zombie character model in a hunched, shambling pose with arms forward. Grayish-green skin, sunken eyes, torn brown and gray clothing. Stylized realism, clean geometric shapes, muted colors. Matches the style of player_character. High resolution, transparent background, front-facing standing illustration.", "description": "Low-poly 3D basic zombie enemy with decayed features", "subtype": "character", "aspect_ratio": [596, 1191], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "pistol_weapon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/pistol_weapon_164e557a-e374-4636-988b-c42e62c809fe.webp", "type": "image", "prompt": "Generate a low-poly 3D semi-automatic pistol from a side-perspective, slightly angled to show top and side. Dark gray and black metal, clean geometric silhouettes (Glock style). Stylized realism, muted metallic colors, game-ready object, transparent background.", "description": "Low-poly 3D semi-automatic pistol weapon model", "subtype": "object", "aspect_ratio": [1045, 855], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "health_pack": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/health_pack_a66b6ea1-3b12-4bde-bd11-c1571dbb8ad3.webp", "type": "image", "prompt": "Generate a low-poly 3D rectangular first aid box. White box with a prominent red cross symbol, slight weathering and dirt. Clean geometric shapes, stylized realism, game-ready icon, transparent background.", "description": "Interactive health pack pickup with red cross icon", "subtype": "object", "aspect_ratio": [848, 842], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "ammo_box": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ammo_box_9c7353d4-e4c1-4153-8145-65b170d12e43.webp", "type": "image", "prompt": "Generate a low-poly 3D military ammunition crate. Olive green with black stenciled lettering, metal corner details. Angular, sturdy geometric shape, stylized realism, game-ready object, transparent background.", "description": "Military-style ammunition box pickup item", "subtype": "object", "aspect_ratio": [1126, 870], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "building_residential": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/building_residential_97c89045-7096-4db9-86f8-4115db62b11d.webp", "type": "image", "prompt": "Generate a seamlessly tileable texture for a residential building exterior. Weathered gray-brown siding and brick, cracked paint, boarded window frames. Muted desaturated colors, stylized realism, overcast lighting, tileable in both directions for 3D modeling.", "description": "Seamlessly tiling texture for residential house exteriors", "subtype": "other", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "building_store": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/building_store_c571c008-ff8b-4a5b-a8c1-2050ee68a03a.webp", "type": "image", "prompt": "Generate a seamlessly tileable texture for a commercial store exterior. Weathered concrete and metal siding, faded signage areas, dark broken windows. Muted blue and gray tones, post-apocalyptic aesthetic, overcast lighting, tileable in both directions.", "description": "Seamlessly tiling texture for commercial store exteriors", "subtype": "other", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "ground_asphalt": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ground_asphalt_e8dcd61e-8424-4018-afda-3ba586b1f896.webp", "type": "image", "prompt": "Generate a fully tileable texture of cracked, weathered dark gray asphalt. Visible potholes, dirt accumulation, and stones. Designed for a top-down/45-degree 3D view. Seamless repetition in both directions, muted desaturated colors, high detail.", "description": "Tiling dark asphalt texture with cracks and debris", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "ground_dirt": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ground_dirt_6626a099-eabb-4445-ac35-60b151ded32a.webp", "type": "image", "prompt": "Generate a fully tileable texture of brown-gray dirt and earth. Sparse patches of dead yellow-green grass and small stones. Seamless repetition in both directions, muted post-apocalyptic colors, stylized realism, designed for 3D terrain.", "description": "Tiling dirt and dead grass ground texture", "subtype": "ground", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "vehicle_abandoned": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/vehicle_abandoned_7a0d31b6-7904-463b-990c-23ca24e6eb61.webp", "type": "image", "prompt": "Generate a low-poly 3D abandoned car model. Rusted metal, broken windows, flat tires, faded blue paint. Viewed from a 45-degree front-side angle. Clean geometric shapes, stylized realism, muted colors, transparent background, game asset.", "description": "Low-poly 3D abandoned car obstacle", "subtype": "object", "aspect_ratio": [1334, 708], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "muzzle_flash": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/muzzle_flash_8c098a5d-f05e-4905-be19-87e25a2e2847.webp", "type": "image", "prompt": "Generate a 2D muzzle flash sprite. Bright yellow-white core radiating into orange sparks, irregular starburst shape, semi-realistic stylized VFX, transparent background.", "description": "VFX sprite for gun muzzle flash", "subtype": "effect", "aspect_ratio": [682, 682], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "blood_particle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/blood_particle_16950c85-37fa-45c0-b6bf-85ce4255c3a6.webp", "type": "image", "prompt": "Generate a 2D blood splatter particle. Dark red irregular droplet splash, simple and readable for combat feedback, stylized realism, transparent background.", "description": "VFX sprite for combat hit feedback", "subtype": "effect", "aspect_ratio": [746, 752], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "pistol_shot_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/pistol_shot_sfx_e3a13080-1910-4415-a8c0-b1cb443b2ba1.mp3", "type": "audio", "prompt": "Sharp, punchy pistol gunshot sound effect. High-impact mechanical crack followed by a subtle, short outdoor echo. Clean, realistic but stylized for arcade satisfaction, no background noise, suitable for rapid-fire playback.", "description": "A sharp and powerful pistol gunshot for the player's primary weapon.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2, "loop": false, "animation": null}, "zombie_groan_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/zombie_groan_sfx_0b5580ab-3f10-47d9-b414-0dd92baaf9c2.mp3", "type": "audio", "prompt": "Guttural zombie groan, low-pitched raspy moan, undead creature vocalization. Eerie and threatening, horror game aesthetic, wet throat textures, inhuman and desolate.", "description": "Guttural and creepy undead vocalization for enemy zombies.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.39, "loop": false, "animation": null}, "pickup_collect_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/pickup_collect_sfx_46495cc6-70d4-483d-8735-dc5300b17bcb.mp3", "type": "audio", "prompt": "Positive arcade-style item pickup sound. Bright tonal chime, high-pitched mechanical click with a subtle musical shimmer. Clear, rewarding, and satisfying feedback for collecting ammo or health.", "description": "A rewarding and bright sound effect for gathering supplies.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0.96, "loop": false, "animation": null}, "player_hurt_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/player_hurt_sfx_010f0385-1902-48a2-8bfe-00c1274ea450.mp3", "type": "audio", "prompt": "Short male grunt of pain, sharp intake of breath, realistic physical impact vocalization. Non-graphic, clear audio cue for taking damage in an action game, punchy and brief.", "description": "A short vocal grunt indicating the player has taken damage.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.6206575963718821, "loop": false, "animation": null}, "reload_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/reload_sfx_18db9b46-ac7a-4597-8235-4672a26112bb.mp3", "type": "audio", "prompt": "Pistol reload sequence: metallic magazine ejection, a solid click of a new magazine insertion, and the crisp sliding rack of the handgun chamber. Tactical, mechanical, and rhythmic.", "description": "Mechanical sounds of a pistol being reloaded.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.86, "loop": false, "animation": null}, "ambient_wind_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/ambient_wind_sfx_c03d2acc-1593-4f40-9c56-085bdd0ca5c7.mp3", "type": "audio", "prompt": "Eerie ambient wind blowing through an abandoned urban environment. Low-frequency rumble, subtle whistling through narrow gaps, desolate and lonely atmosphere. Seamless loop, no birds or city traffic, very quiet and cinematic.", "description": "Atmospheric background wind for the abandoned town setting.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 15, "loop": true, "animation": null}, "ak47_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ak47_texture_19e5b68c-d533-456b-a838-e04733b98454.webp", "type": "image", "prompt": "Generate a professional, high-resolution game asset of an AK-47 assault rifle. Side profile view, realistic and gritty style inspired by Free Fire. Features detailed dark weathered metal and rich wood grain on the stock and handguard. The appearance should be tactical and slightly worn with high contrast. Clean lines suitable for texture mapping, isolated on a transparent background.", "description": "A gritty, tactical AK-47 assault rifle texture for weapon modeling and UI icons.", "subtype": "object", "aspect_ratio": [988, 284], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "shotgun_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/shotgun_texture_3df4a4af-23df-4f0d-adf9-c4d34a24bc6d.webp", "type": "image", "prompt": "Generate a realistic, tactical shotgun texture from a side profile view, designed for mapping onto a 3D model. The style is gritty and weathered with a tactical black finish and visible metallic wear on the edges. High-detail, high-contrast aesthetic similar to 'Free Fire'. Clean professional game asset on a transparent background.", "description": "A gritty, tactical shotgun texture with a weathered black finish for weapon mapping.", "subtype": "object", "aspect_ratio": [967, 157], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "ak47_shot_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/ak47_shot_sfx_753cca8e-9904-409f-be3a-e83a5f382a79.mp3", "type": "audio", "prompt": "High-fidelity, heavy assault rifle gunshot sound effect. Features a powerful, metallic mechanical bolt action click followed by a sharp, aggressive gunpowder explosion. Include a brief, realistic concrete environment reverb tail. The sound should feel weighty, rhythmic, and punchy, suitable for a rapid-fire sequence in a modern combat setting. No high-pitched whistling, just raw mechanical power.", "description": "Powerful, rapid-fire mechanical gunshot sounds for an AK-47 assault rifle.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.48, "loop": false, "animation": null}, "shotgun_shot_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/shotgun_shot_sfx_c1c95a25-8cd0-4098-a7dd-9a41c8876656.mp3", "type": "audio", "prompt": "A heavy, booming shotgun blast sound effect with a powerful low-end impact, followed immediately by a distinct, metallic pump-action mechanical sliding sound. The audio should have a slight indoor reverb consistent with an abandoned town setting. Sharp attack, weighty body, and clean mechanical foley. No background music or extra noise.", "description": "Powerful shotgun blast followed by a mechanical pump-action sound for the heavy weapon.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.77, "loop": false, "animation": null}, "loading_background_image": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/loading_background_image_315e8812-8353-404a-9680-9224c5d2be2f.webp", "type": "image", "prompt": "Generate a 9:16 tall cinematic and atmospheric 3D render of a gritty, post-apocalyptic town at dusk. In the foreground, a rugged survivor matching the style of player_character stands ready with a pistol, looking out over a desolate street. In the distance, silhouettes of zombies resembling zombie_basic are visible through a light toxic fog. Dramatic lighting with orange sunset hues and long shadows. High quality, detailed low-poly 3D aesthetic, professional game-ready splash art.", "description": "A cinematic vertical loading screen featuring a survivor in a post-apocalyptic town at dusk.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "sniper_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/sniper_texture_86315df1-13a2-4afa-83fd-9a5b71c568e2.webp", "type": "image", "prompt": "Generate a high-quality, gritty tactical sniper rifle texture, low-poly 3D style to match 'ak47_texture'. Features a long barrel, high-tech scope, and a weathered metallic black finish. The image should be a flat, clean texture map layout suitable for 3D model wrapping, isolated on a solid background, following the post-apocalyptic survivor aesthetic.", "description": "A gritty, tactical sniper rifle texture with a metallic black finish and high-tech scope for 3D weapon mapping.", "subtype": "object", "aspect_ratio": [1201, 342], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "smg_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/smg_texture_a4fe2c97-2e41-47a9-9a36-4d02916c7db8.webp", "type": "image", "prompt": "Generate a high-quality tactical submachine gun (SMG) texture, compact design with a short barrel and ergonomic grip, matte black finish with gritty and weathered metallic details. The style should match the low-poly 3D aesthetic of existing weapon textures like image 1 and image 2. Isolated on a plain background for clean mapping.", "description": "A tactical submachine gun texture for weapon modeling and UI, matching the existing gritty weapon set.", "subtype": "object", "aspect_ratio": [856, 313], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "lmg_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/lmg_texture_ab80e7ee-690b-4f50-9d6a-1e825bf3898d.webp", "type": "image", "prompt": "Generate a professional, high-detail texture of a heavy light machine gun (LMG). The weapon should feature a box magazine, a heat shield, and a weathered metallic appearance with scratches and tactical grit. Use a low-poly 3D aesthetic to match the style of image 1 and image 2. The gun should be shown in a clean side view, isolated on a white background for easy mapping. High resolution, matte black and steel finishes, realistic wear and tear.", "description": "A gritty, tactical light machine gun (LMG) texture for weapon mapping and UI icons, matching the existing arsenal style.", "subtype": "object", "aspect_ratio": [1226, 363], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "sniper_shot_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/sniper_shot_sfx_395d66df-07b1-4263-8bfe-2cb1c4130b8a.mp3", "type": "audio", "prompt": "Professional high-caliber sniper rifle gunshot sound effect. Features a sharp, explosive crack at the start, followed by a metallic bolt-action mechanical sound, and ending with a long, cinematic echoing tail reflecting off distant buildings. Deep low-end punch, crisp high-frequency reverb, powerful and intimidating. Suitable for a tactical shooter game.", "description": "A high-caliber sniper rifle shot with a heavy mechanical bolt-action sound and a long environmental tail.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 4, "loop": false, "animation": null}, "smg_shot_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/smg_shot_sfx_0fbe5282-5893-4e41-bab2-963b16cde0d6.mp3", "type": "audio", "prompt": "A rapid-fire burst of a submachine gun, crisp mechanical clicking and light metallic clatter. Sharp, high-frequency muzzle pops with a fast cyclic rate. Light but punchy, suitable for a 9mm SMG. No heavy echoes, clear and distinct mechanical action for a modern tactical shooter.", "description": "Rapid, crisp submachine gun burst sound for a fast-firing light firearm.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0.83, "loop": false, "animation": null}, "lmg_shot_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/lmg_shot_sfx_541e8b22-4794-4e6c-87fb-8d004fcbfd26.mp3", "type": "audio", "prompt": "Heavy, sustained rapid-fire gunshot sounds for a light machine gun. The sound should have a deep, thudding mechanical character with a fast fire rate. Each shot should have a powerful low-end punch and a metallic clatter of the belt-fed mechanism. Intense, aggressive, and industrial, suitable for a heavy support weapon in a combat setting. No echoes or long tails, designed for continuous playback.", "description": "Heavy, sustained rapid-fire gunshot sounds for a light machine gun with a deep mechanical tone.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 3, "loop": true, "animation": null}, "headshot_sticker": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/headshot_sticker_6a5fcc02-fee3-4b76-9ca9-2c3fb55a9169.webp", "type": "image", "prompt": "Generate a high-contrast, professional 2D game UI sticker of a scary skeleton skull being shattered by a high-velocity bullet. The skull features glowing red eyes and deep cracks, with cinematic orange and red fire/spark impact effects. Gritty post-apocalyptic art style, vibrant accents, sharp edges, clean vector-like detail but with realistic textures, transparent background.", "description": "A gritty, cinematic 2D UI sticker of a shattered skull with glowing eyes, used for headshot kill feedback.", "subtype": "ui", "aspect_ratio": [930, 912], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "streak_sticker": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/streak_sticker_0873b967-5b9a-485b-84b2-2e583215539b.webp", "type": "image", "prompt": "Generate a cinematic, epic 2D sticker of a skull engulfed in intense golden and orange flames, designed as a modern game UI multi-kill streak icon. Sharp details, aggressive and powerful look with glowing light effects. Professional game UI style, high contrast, vibrant orange and gold accents, transparent background. Match the gritty and high-quality aesthetic of the headshot_sticker.", "description": "A cinematic multi-kill streak icon featuring a flaming skull for game UI feedback.", "subtype": "ui", "aspect_ratio": [994, 987], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "eliminated_sticker": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/eliminated_sticker_19c2c21e-c037-4379-b65c-c0097e0ed3fd.webp", "type": "image", "prompt": "Generate a 2D game UI sticker of a skull and crossbones with a gritty metallic finish. Post-apocalyptic survival style, dark gray and silver metallic tones, sharp edges, professional game icon quality, high contrast, transparent background. Match the tactical and cinematic style of headshot_sticker and streak_sticker.", "description": "A gritty, tactical skull and crossbones sticker for kill confirmation UI.", "subtype": "ui", "aspect_ratio": [943, 980], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>UNDEAD FRONTIER</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n    <style>\n        @keyframes pulse {\n            0%, 100% { opacity: 0.6; transform: scale(0.98); }\n            50% { opacity: 1; transform: scale(1.02); }\n        }\n        /* CSS Reset */\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body, html {\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            -webkit-touch-action: none;\n            font-family: 'Inter', 'Roboto', sans-serif;\n        }\n\n        /* CSS Custom Properties */\n        :root {\n            --primary: #ffcc00;\n            --accent: #ff4444;\n            --bg-dark: rgba(0, 0, 0, 0.7);\n            --transition-fast: 100ms;\n            --transition-normal: 250ms;\n            --transition-slow: 400ms;\n            --ease-out: cubic-bezier(0.19, 1, 0.22, 1);\n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n            --panel-gradient: linear-gradient(180deg, rgba(30,30,30,0.95) 0%, rgba(10,10,10,0.95) 100%);\n            --hud-shadow: 0 4px 15px rgba(0,0,0,0.6);\n        }\n\n        /* Game Container */\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n            background-color: #0a0a0a;\n        }\n\n        #game-canvas {\n            display: block;\n            width: 100%;\n            height: 100%;\n        }\n\n        /* UI Overlay */\n        .ui-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: 10;\n            overflow: hidden;\n        }\n\n        /* Blood Overlay */\n        #blood-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            background: radial-gradient(circle, transparent 40%, rgba(150, 0, 0, 0.4) 100%);\n            opacity: 0;\n            transition: opacity 0.3s;\n            z-index: 4;\n        }\n\n        /* Screen Shake / Flash Effect */\n        #fx-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: 5;\n            transition: background var(--transition-fast);\n        }\n\n        /* HUD */\n        .hud {\n            position: absolute;\n            bottom: var(--space-lg);\n            left: 50%;\n            transform: translateX(-50%);\n            width: 94%;\n            max-width: 800px;\n            display: flex;\n            justify-content: space-between;\n            align-items: flex-end;\n            pointer-events: none;\n        }\n\n        .hud-item {\n            background: var(--panel-gradient);\n            padding: 12px 20px;\n            border-radius: 4px;\n            color: white;\n            font-size: 20px;\n            font-weight: 800;\n            border-left: 4px solid var(--primary);\n            box-shadow: var(--hud-shadow);\n            text-transform: uppercase;\n            letter-spacing: 1px;\n            display: flex;\n            align-items: center;\n            gap: 12px;\n            backdrop-filter: blur(4px);\n        }\n\n        .health-bar-container {\n            width: 280px;\n            position: relative;\n        }\n\n        .health-bar {\n            flex: 1;\n            height: 18px;\n            background: rgba(255, 255, 255, 0.1);\n            border-radius: 2px;\n            overflow: hidden;\n            position: relative;\n        }\n\n        .health-fill {\n            height: 100%;\n            background: linear-gradient(90deg, #ff3333, #ff8800);\n            transition: width var(--transition-normal) var(--ease-out);\n            box-shadow: 0 0 10px rgba(255, 51, 51, 0.5);\n        }\n\n        .ammo-display {\n            font-size: 32px;\n            border-left: none;\n            border-right: 4px solid var(--primary);\n        }\n\n        .wave-indicator {\n            position: absolute;\n            top: 20px;\n            left: 20px;\n            font-size: 24px;\n            z-index: 15;\n            background: linear-gradient(90deg, rgba(255, 204, 0, 0.8), transparent);\n            border-left: none;\n            padding-left: 10px;\n        }\n\n        .score-display {\n            position: absolute;\n            top: 60px; /* Adjusted to be below wave indicator */\n            left: 20px; /* Moved to top-left */\n            font-size: 24px;\n            z-index: 15;\n            background: linear-gradient(90deg, rgba(0, 150, 255, 0.8), transparent); /* Changed to blue gradient */\n            border-left: none;\n            padding-left: 10px; /* Adjusted padding */\n            text-align: left; /* Adjusted text alignment */\n        }\n\n        /* Minimap Styles */\n        #minimap-container {\n            position: absolute;\n            top: 20px;\n            right: 20px;\n            width: 180px;\n            height: 180px;\n            border-radius: 50%;\n            overflow: hidden;\n            background: rgba(0, 0, 0, 0.6);\n            border: 2px solid var(--primary);\n            z-index: 15;\n            pointer-events: none;\n            box-shadow: var(--hud-shadow);\n        }\n\n        #minimap-canvas {\n            width: 100%;\n            height: 100%;\n            display: block;\n        }\n\n        /* Hit Marker */\n        .hit-marker {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            width: 40px;\n            height: 40px;\n            pointer-events: none;\n            display: none;\n        }\n\n        .hit-marker.active {\n            display: block;\n            animation: hit-pulse 0.2s ease-out;\n        }\n\n        @keyframes hit-pulse {\n            0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }\n            50% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; }\n            100% { transform: translate(-50%, -50%) scale(1); opacity: 0; }\n        }\n\n        .hit-marker::before, .hit-marker::after {\n            content: '';\n            position: absolute;\n            background: var(--marker-color, #ff4444);\n            width: 2px;\n            height: 20px;\n            left: 50%;\n            top: 50%;\n        }\n\n        .hit-marker::before { transform: translate(-50%, -50%) rotate(45deg); }\n        .hit-marker::after { transform: translate(-50%, -50%) rotate(-45deg); }\n\n        /* Crosshair */\n        .crosshair {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            width: 32px;\n            height: 32px;\n            pointer-events: none;\n        }\n\n        .crosshair::before,\n        .crosshair::after {\n            content: '';\n            position: absolute;\n            background: rgba(255, 255, 255, 0.8);\n        }\n\n        .crosshair::before {\n            left: 50%;\n            top: 0;\n            width: 2px;\n            height: 100%;\n            transform: translateX(-50%);\n        }\n\n        .crosshair::after {\n            top: 50%;\n            left: 0;\n            height: 2px;\n            width: 100%;\n            transform: translateY(-50%);\n        }\n\n        /* Touch Controls */\n        .touch-controls {\n            position: absolute;\n            bottom: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n        }\n\n        .virtual-joystick {\n            position: absolute;\n            width: 160px;\n            height: 160px;\n            pointer-events: auto;\n            opacity: 0;\n            transition: opacity var(--transition-fast) var(--ease-out);\n        }\n\n        .virtual-joystick.active {\n            opacity: 1;\n        }\n\n        .joystick-base {\n            position: absolute;\n            width: 100%;\n            height: 100%;\n            border-radius: 50%;\n            background: rgba(255, 255, 255, 0.2);\n            border: 3px solid rgba(255, 255, 255, 0.4);\n        }\n\n        .joystick-stick {\n            position: absolute;\n            width: 60px;\n            height: 60px;\n            border-radius: 50%;\n            background: rgba(255, 255, 255, 0.6);\n            border: 3px solid rgba(255, 255, 255, 0.8);\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n        }\n\n        .touch-button {\n            position: absolute;\n            width: 144px; /* 96 * 1.5 */\n            height: 144px; /* 96 * 1.5 */\n            border-radius: 50%;\n            background: rgba(255, 255, 255, 0.3);\n            border: 3px solid rgba(255, 255, 255, 0.5);\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            color: white;\n            font-size: 32px; /* 24 * 1.5 = 36, let's use 32 for better fit */\n            font-weight: bold;\n            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);\n            transition: all var(--transition-fast) var(--ease-out);\n            pointer-events: auto;\n        }\n\n        .touch-button:active {\n            background: rgba(255, 255, 255, 0.5);\n            transform: scale(0.95);\n        }\n\n        .shoot-button {\n            bottom: var(--space-md);\n            right: var(--space-md);\n        }\n\n        .reload-button {\n            bottom: 200px; /* Increased to avoid overlapping the larger shoot button */\n            right: var(--space-md);\n        }\n\n        .jump-button {\n            bottom: 140px;\n            left: var(--space-md);\n            width: 120px; /* 80 * 1.5 */\n            height: 120px; /* 80 * 1.5 */\n            background: rgba(0, 0, 0, 0.3);\n            border: 2px solid rgba(255, 255, 255, 0.4);\n            font-size: 24px; /* 16 * 1.5 */\n        }\n\n        .sprint-button {\n            bottom: 280px;\n            left: var(--space-md);\n            width: 120px;\n            height: 120px;\n            background: rgba(0, 0, 0, 0.3);\n            border: 2px solid rgba(255, 255, 255, 0.4);\n            font-size: 22px;\n        }\n\n        .zoom-button {\n            bottom: 370px; /* Shifted higher to accommodate larger reload and fire buttons */\n            right: var(--space-md);\n            width: 120px; /* 80 * 1.5 */\n            height: 120px; /* 80 * 1.5 */\n            background: rgba(0, 0, 0, 0.3);\n            border: 2px solid rgba(255, 204, 0, 0.6);\n            font-size: 24px; /* 16 * 1.5 */\n        }\n\n        /* Scope Effect */\n        #scope-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            background: radial-gradient(circle, transparent 30%, black 150%);\n            opacity: 0;\n            transition: opacity 0.2s;\n            z-index: 6;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        #scope-overlay.active {\n            opacity: 1;\n        }\n\n        .scope-crosshair {\n            width: 100%;\n            height: 100%;\n            position: relative;\n            display: none;\n        }\n\n        #scope-overlay.active .scope-crosshair {\n            display: block;\n        }\n\n        .scope-line-v {\n            position: absolute;\n            left: 50%;\n            top: 0;\n            width: 2px;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.8);\n            transform: translateX(-50%);\n        }\n\n        .scope-line-h {\n            position: absolute;\n            top: 50%;\n            left: 0;\n            width: 100%;\n            height: 2px;\n            background: rgba(0, 0, 0, 0.8);\n            transform: translateY(-50%);\n        }\n\n        .switch-button {\n            bottom: var(--space-md);\n            left: 50%;\n            transform: translateX(-50%);\n            width: 180px; /* 120 * 1.5 */\n            height: 90px; /* 60 * 1.5 */\n            border-radius: 45px;\n            background: rgba(255, 204, 0, 0.4);\n            border: 2px solid var(--primary);\n            font-size: 24px; /* 16 * 1.5 */\n        }\n\n        /* Death Screen & Store */\n        .death-screen, .store-screen {\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            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            gap: var(--space-lg);\n            z-index: 20;\n            padding: var(--space-md);\n            pointer-events: auto;\n        }\n\n        .death-screen.active, .store-screen.active {\n            display: flex;\n        }\n\n        .store-screen {\n            background: rgba(10, 10, 10, 0.95);\n            justify-content: flex-start;\n            padding-top: var(--space-xl);\n            overflow-y: auto;\n        }\n\n        .store-title {\n            font-size: 48px;\n            color: var(--primary);\n            font-weight: 900;\n            text-transform: uppercase;\n            letter-spacing: 2px;\n            margin-bottom: var(--space-md);\n        }\n\n        .store-sections {\n            width: 100%;\n            gap: var(--space-sm);\n            text-align: center;\n        }\n\n        .death-stat {\n            font-size: 32px;\n            color: white;\n            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);\n        }\n\n        .restart-button {\n            padding: var(--space-md) var(--space-xl);\n            background: linear-gradient(135deg, #ff4444, #cc0000);\n            border: none;\n            border-radius: 8px;\n            color: white;\n            font-size: 32px;\n            font-weight: bold;\n            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);\n            cursor: pointer;\n            transition: all var(--transition-normal) var(--ease-out);\n            pointer-events: auto;\n        }\n\n        .restart-button:hover {\n            transform: scale(1.05);\n            box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);\n        }\n\n        .restart-button:active {\n            transform: scale(0.95);\n        }\n\n        /* Wave Transition */\n        .wave-transition {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            font-size: 64px;\n            color: #ffcc00;\n            font-weight: bold;\n            text-shadow: 4px 4px 8px rgba(0, 0, 0, 1);\n            pointer-events: none;\n            opacity: 0;\n            transition: opacity var(--transition-normal) var(--ease-out);\n        }\n\n        .wave-transition.active {\n            opacity: 1;\n        }\n\n        /* Kill Tags & Combat Feedback */\n        .kill-tag-container {\n            position: absolute;\n            top: 30%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            width: 100%;\n            height: 200px;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            pointer-events: none;\n            z-index: 30;\n        }\n\n        .kill-tag {\n            font-family: 'Arial Black', sans-serif;\n            font-weight: 900;\n            text-transform: uppercase;\n            font-style: italic;\n            white-space: nowrap;\n            opacity: 0;\n            filter: drop-shadow(0 0 15px rgba(0,0,0,0.9));\n            position: absolute;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            gap: 20px;\n        }\n\n        .kill-sticker {\n            height: 100px;\n            width: auto;\n            object-fit: contain;\n            filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.4));\n            animation: sticker-pulse 0.6s ease-in-out infinite alternate;\n        }\n\n        @keyframes sticker-pulse {\n            from { transform: scale(1); filter: brightness(1) drop-shadow(0 0 10px rgba(255,255,255,0.4)); }\n            to { transform: scale(1.1); filter: brightness(1.3) drop-shadow(0 0 20px rgba(255,255,255,0.6)); }\n        }\n\n        .kill-tag.active {\n            animation: epic-entrance 0.6s cubic-bezier(0.17, 0.89, 0.32, 1.28) forwards,\n                       epic-drift 1.5s ease-out forwards;\n        }\n\n        @keyframes epic-entrance {\n            0% { transform: scale(3); opacity: 0; filter: blur(10px) brightness(2); }\n            40% { transform: scale(0.9); opacity: 1; filter: blur(0px) brightness(1.2); }\n            60% { transform: scale(1.1); }\n            80% { transform: scale(1); }\n            100% { transform: scale(1); opacity: 1; }\n        }\n\n        @keyframes epic-drift {\n            0% { margin-top: 0; }\n            100% { margin-top: -40px; opacity: 0; transform: scale(1.2); }\n        }\n\n        .kill-tag.headshot {\n            font-size: 84px;\n            color: #ff0000;\n            -webkit-text-stroke: 2px #fff;\n            text-shadow: 0 0 30px rgba(255, 0, 0, 0.9), 6px 6px 0px #000;\n        }\n\n        .kill-tag.headshot .kill-sticker {\n            height: 180px;\n        }\n\n        .kill-tag.headshot span::after {\n            content: 'HEADSHOT';\n            position: absolute;\n            left: 0;\n            top: 0;\n            color: transparent;\n            -webkit-text-stroke: 4px rgba(255, 255, 255, 0.3);\n            filter: blur(4px);\n            z-index: -1;\n            animation: glisten 1s infinite;\n        }\n\n        @keyframes glisten {\n            0% { clip-path: inset(0 100% 0 0); }\n            100% { clip-path: inset(0 0 0 100%); }\n        }\n\n        .kill-tag.eliminated {\n            font-size: 52px;\n            color: #ffcc00;\n            -webkit-text-stroke: 1px #000;\n            text-shadow: 0 0 15px rgba(255, 204, 0, 0.6), 3px 3px 0px #000;\n        }\n\n        .kill-tag.eliminated .kill-sticker {\n            height: 80px;\n        }\n\n        .kill-tag span {\n            position: relative;\n        }\n\n        .kill-tag.streak {\n            font-size: 64px;\n            color: #ffffff;\n            background: linear-gradient(180deg, #fff 30%, #ffcc00 100%);\n            -webkit-background-clip: text;\n            -webkit-text-fill-color: transparent;\n            -webkit-text-stroke: 1.5px #000;\n            filter: drop-shadow(0 0 15px rgba(255, 100, 0, 0.8));\n        }\n\n        .kill-tag.streak .kill-sticker {\n            height: 140px;\n        }\n\n        .damage-number {\n            position: absolute;\n            color: white;\n            font-weight: 900;\n            font-size: 24px;\n            pointer-events: none;\n            z-index: 20;\n            text-shadow: 2px 2px 0px black;\n            animation: float-up 0.6s ease-out forwards;\n        }\n\n        @keyframes float-up {\n            0% { transform: translateY(0); opacity: 1; }\n            100% { transform: translateY(-100px); opacity: 0; }\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: #1a1a1a;\n            background-size: cover;\n            background-position: center;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: flex-end;\n            padding-bottom: 200px;\n            gap: var(--space-md);\n            z-index: 100;\n        }\n\n        .loading-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.8) 100%);\n            z-index: -1;\n        }\n\n        .loading-text {\n            font-size: 32px;\n            color: var(--primary);\n            font-weight: 900;\n            text-transform: uppercase;\n            letter-spacing: 2px;\n            text-shadow: 0 0 20px rgba(255, 204, 0, 0.5);\n        }\n\n        .loading-bar {\n            width: 80%;\n            max-width: 500px;\n            height: 12px;\n            background: rgba(255, 255, 255, 0.1);\n            border-radius: 6px;\n            overflow: hidden;\n            border: 1px solid rgba(255, 255, 255, 0.2);\n        }\n\n        .loading-fill {\n            height: 100%;\n            background: linear-gradient(90deg, #ffcc00, #ff8800);\n            width: 0%;\n            transition: width var(--transition-fast) linear;\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"game-canvas\"></canvas>\n        \n        <div class=\"ui-overlay\">\n            <div id=\"click-to-play\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 25; pointer-events: auto; font-family: 'Inter', sans-serif;\">\n                <div style=\"font-size: 36px; color: #ffcc00; font-weight: 900; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 10px rgba(255,204,0,0.5);\">Undead Frontier</div>\n                <div style=\"font-size: 18px; color: white; font-weight: bold; margin-bottom: 25px; animation: pulse 1.5s infinite; text-transform: uppercase;\">Click Screen to Lock Mouse & Move</div>\n                <div style=\"background: rgba(255,255,255,0.05); padding: 20px 30px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); max-width: 400px; text-align: center;\">\n                    <div style=\"font-size: 14px; color: #ffcc00; font-weight: bold; margin-bottom: 10px;\">KEYBOARD & MOUSE CONTROLS:</div>\n                    <div style=\"font-size: 13px; color: #ddd; line-height: 1.6;\">\n                        <b style=\"color: white;\">WASD / Arrows:</b> Move Character<br>\n                        <b style=\"color: white;\">Mouse Look:</b> Aim Camera<br>\n                        <b style=\"color: white;\">Left-Click:</b> Fire Weapon<br>\n                        <b style=\"color: white;\">Right-Click (Hold):</b> Aim Down Sights (ADS)<br>\n                        <b style=\"color: white;\">R:</b> Reload Weapon\n                    </div>\n                </div>\n            </div>\n\n            <div id=\"fx-overlay\"></div>\n            <div id=\"blood-overlay\"></div>\n            <div id=\"scope-overlay\">\n                <div class=\"scope-crosshair\">\n                    <div class=\"scope-line-v\"></div>\n                    <div class=\"scope-line-h\"></div>\n                </div>\n            </div>\n            <div class=\"hud-item wave-indicator\">\n                <span id=\"wave-text\">Level 1</span>\n            </div>\n            <div class=\"hud-item score-display\">\n                <span id=\"score-text\">0</span>\n            </div>\n\n            <div id=\"minimap-container\">\n                <canvas id=\"minimap-canvas\"></canvas>\n            </div>\n            <div class=\"hud\">\n                <div class=\"hud-item health-bar-container\">\n                    <div class=\"health-bar\">\n                        <div class=\"health-fill\" id=\"health-fill\"></div>\n                    </div>\n                    <span id=\"health-text\">100</span>\n                </div>\n                <div class=\"hud-item ammo-display\">\n                    <span id=\"ammo-text\">30 / 30</span>\n                </div>\n            </div>\n\n            <!-- Crosshair -->\n            <div class=\"crosshair\"></div>\n            <div class=\"hit-marker\" id=\"hit-marker\"></div>\n\n            <!-- Touch Controls -->\n            <div class=\"touch-controls\" id=\"touch-controls\" style=\"display: none;\">\n                <div class=\"virtual-joystick\" id=\"move-joystick\">\n                    <div class=\"joystick-base\"></div>\n                    <div class=\"joystick-stick\" id=\"move-stick\"></div>\n                </div>\n                <div class=\"virtual-joystick\" id=\"look-joystick\">\n                    <div class=\"joystick-base\"></div>\n                    <div class=\"joystick-stick\" id=\"look-stick\"></div>\n                </div>\n                <div class=\"touch-button shoot-button\" id=\"shoot-button\">FIRE</div>\n                <div class=\"touch-button zoom-button\" id=\"zoom-button\">ADS</div>\n                <div class=\"touch-button reload-button\" id=\"reload-button\">RELOAD</div>\n                <div class=\"touch-button jump-button\" id=\"jump-button\">JUMP</div>\n                <div class=\"touch-button sprint-button\" id=\"sprint-button\">SPRINT</div>\n                <div class=\"touch-button switch-button\" id=\"switch-button\">NEXT GUN</div>\n            </div>\n\n            <!-- Wave Transition -->\n            <div class=\"wave-transition\" id=\"wave-transition\"></div>\n\n            <div id=\"kill-tag-container\" class=\"kill-tag-container\"></div>\n\n            <!-- Death Screen -->\n            <div class=\"death-screen\" id=\"death-screen\">\n                <div class=\"death-title\">YOU DIED</div>\n                <div class=\"death-stats\">\n                    <div class=\"death-stat\" id=\"final-wave\">Level Reached: 1</div>\n                    <div class=\"death-stat\" id=\"final-score\">Final Score: 0</div>\n                    <div class=\"death-stat\" id=\"zombies-killed\">Zombies Killed: 0</div>\n                </div>\n                <button class=\"restart-button\" id=\"restart-button\">RESTART</button>\n            </div>\n\n            <div class=\"store-screen\" id=\"store-screen\">\n                <div class=\"store-title\">Black Market</div>\n                \n                <div class=\"store-sections\">\n                    <div class=\"store-section\">\n                        <div class=\"section-title\">Weapons</div>\n                        <div class=\"store-grid\" id=\"weapon-store-grid\">\n                            <!-- Injected by JS -->\n                        </div>\n                    </div>\n                    \n                    <div class=\"store-section\">\n                        <div class=\"section-title\">Current Weapon Upgrades</div>\n                        <div class=\"store-grid\" id=\"upgrade-store-grid\">\n                            <!-- Injected by JS -->\n                        </div>\n                    </div>\n\n                    <div class=\"store-section\">\n                        <div class=\"section-title\">Character Upgrades</div>\n                        <div class=\"store-grid\" id=\"character-store-grid\">\n                            <div class=\"store-item\">\n                                <div class=\"item-name\">Max Health</div>\n                                <div class=\"item-cost\" id=\"health-upgrade-cost\">Cost: 200</div>\n                                <button class=\"buy-button\" onclick=\"upgradeMaxHealth()\">Upgrade</button>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"store-footer\">\n                    <div class=\"current-score-display\" id=\"store-score\">Score: 0</div>\n                    <button class=\"next-level-button\" id=\"next-level-button\">Next Level</button>\n                </div>\n            </div>\n        </div>\n\n        <!-- Loading Screen -->\n        <div class=\"loading-screen\" id=\"loading-screen\">\n            <div class=\"loading-overlay\"></div>\n            <div class=\"loading-text\">UNDEAD FRONTIER</div>\n            <div class=\"loading-bar\">\n                <div class=\"loading-fill\" id=\"loading-fill\"></div>\n            </div>\n            <div style=\"color: white; font-size: 14px; opacity: 0.7; margin-top: 10px;\">Initializing Environment...</div>\n        </div>\n    </div>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n    <script>\n        /*\n         * UNDEAD FRONTIER - 3D Third-Person Zombie Survival Game\n         * \n         * Core Concept:\n         * Navigate an abandoned town from third-person perspective, scavenging for resources\n         * and fighting off relentless waves of zombies. Survive by strategically using your\n         * pistol and found weapons, managing health and ammo, and outsmarting the undead.\n         * \n         * Key Mechanics:\n         * - Third-person 3D movement and camera control\n         * - Wave-based zombie spawning with difficulty scaling\n         * - Resource management (health, ammo)\n         * - Pickup system for supplies and weapons\n         * - Edit mode for customizing game parameters\n         */\n\n        async function run(mode) {\n            const gameWorld = document.getElementById('game-world');\n            const canvas = document.getElementById('game-canvas');\n            \n            // Initialize Three.js scene\n            const scene = new THREE.Scene();\n            const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);\n            const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });\n            \n            const minimapCanvas = document.getElementById('minimap-canvas');\n            const minimapRenderer = new THREE.WebGLRenderer({ canvas: minimapCanvas, antialias: true, alpha: true });\n            minimapRenderer.setSize(minimapCanvas.clientWidth, minimapCanvas.clientHeight);\n            minimapRenderer.setClearColor(0x000000, 0);\n\n            // Minimap camera\n            const minimapCamera = new THREE.OrthographicCamera(-60, 60, 60, -60, 0.1, 1000);\n            minimapCamera.position.set(0, 100, 0);\n            minimapCamera.lookAt(0, 0, 0);\n            minimapCamera.rotation.z = Math.PI;\n\n            renderer.setSize(canvas.clientWidth, canvas.clientHeight);\n            renderer.shadowMap.enabled = true;\n            renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n            \n            scene.background = new THREE.Color(0x0f0f14);\n            scene.fog = new THREE.FogExp2(0x0f0f14, 0.015);\n            \n            // Lighting\n            const ambientLight = new THREE.AmbientLight(0x111122, 0.5);\n            scene.add(ambientLight);\n            \n            const directionalLight = new THREE.DirectionalLight(0x8899b3, 0.8);\n            directionalLight.position.set(30, 50, 30);\n            directionalLight.castShadow = true;\n            directionalLight.shadow.camera.left = -70;\n            directionalLight.shadow.camera.right = 70;\n            directionalLight.shadow.camera.top = 70;\n            directionalLight.shadow.camera.bottom = -70;\n            directionalLight.shadow.mapSize.width = 2048;\n            directionalLight.shadow.mapSize.height = 2048;\n            directionalLight.shadow.bias = -0.0005;\n            scene.add(directionalLight);\n\n            const hemiLight = new THREE.HemisphereLight(0x1a1a2e, 0x080808, 0.3);\n            scene.add(hemiLight);\n\n            // Game State & Constants\n            const gameState = {\n                currentLevel: window.gameConfig.wave.startingLevel,\n                zombiesKilled: 0,\n                score: 0,\n                gameOver: false,\n                waveInProgress: false,\n                timeBetweenWaves: 0,\n                storeOpen: false,\n                killStreak: 0,\n                killStreakTimeout: null\n            };\n\n            const player = {\n                position: new THREE.Vector3(0, 0, 0),\n                velocity: new THREE.Vector3(0, 0, 0),\n                yVelocity: 0,\n                isGrounded: true,\n                rotation: { y: 0, pitch: 0 },\n                health: window.gameConfig.player.startingHealth,\n                maxHealth: window.gameConfig.player.startingHealth,\n                currentWeaponId: 'pistol',\n                inventory: ['pistol'],\n                weapons: JSON.parse(JSON.stringify(window.gameConfig.weapons)),\n                isReloading: false,\n                reloadTime: 0,\n                shootCooldown: 0,\n                model: null,\n                recoil: 0,\n                screenShake: 0,\n                healthLevel: 0\n            };\n\n            // Initialize ammo\n            Object.keys(player.weapons).forEach(id => {\n                player.weapons[id].currentAmmo = player.weapons[id].maxAmmo;\n            });\n\n            // Input handling\n            const isTouchDevice = lib.shouldUseTouchControls();\n            const input = {\n                keys: {},\n                mouse: { x: 0, y: 0, locked: false, deltaX: 0, deltaY: 0 },\n                zoom: false,\n                touch: {\n                    move: { active: false, x: 0, y: 0, id: null },\n                    look: { active: false, x: 0, y: 0, id: null, lastX: 0, lastY: 0 },\n                    lookDelta: { x: 0, y: 0 },\n                    shoot: false,\n                    zoom: false,\n                    sprint: false\n                }\n            };\n\n            // Helpers & Classes\n            // Helpers & Classes\n            function createCharacterModel(isZombie = false, scale = 1) {\n                const group = new THREE.Group();\n                const skinColor = isZombie ? 0x5a7d33 : 0xf1c27d;\n                const clothingColor = isZombie ? 0x242d24 : 0x1f2430;\n                const primaryColor = isZombie ? 0x111111 : 0xffcc00;\n                \n                // Detailed realistic materials\n                const skinMat = new THREE.MeshStandardMaterial({ \n                    color: skinColor, \n                    roughness: 0.8,\n                    metalness: 0.1 \n                });\n                const clothMat = new THREE.MeshStandardMaterial({ \n                    color: clothingColor, \n                    roughness: 0.85 \n                });\n                const accentMat = new THREE.MeshStandardMaterial({ \n                    color: primaryColor,\n                    roughness: 0.5 \n                });\n                \n                // Torso\n                const torso = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.25, 0.9, 8), clothMat);\n                torso.position.y = 1.35;\n                torso.castShadow = true;\n                torso.receiveShadow = true;\n                torso.name = 'torso';\n                group.add(torso);\n                \n                // Head\n                const head = new THREE.Mesh(new THREE.SphereGeometry(0.2, 12, 12), skinMat);\n                head.position.y = 2.0;\n                head.castShadow = true;\n                head.name = 'head';\n                group.add(head);\n                \n                if (!isZombie) {\n                    // Cool hat for player\n                    const hat = new THREE.Mesh(new THREE.CylinderGeometry(0.21, 0.21, 0.1, 10), accentMat);\n                    hat.position.y = 2.15;\n                    group.add(hat);\n                    \n                    // Shoulder pads for more detail\n                    const padGeo = new THREE.BoxGeometry(0.25, 0.15, 0.25);\n                    const padMat = new THREE.MeshStandardMaterial({ color: 0xffcc00 });\n                    const lPad = new THREE.Mesh(padGeo, padMat);\n                    lPad.position.set(-0.4, 1.7, 0);\n                    group.add(lPad);\n                    const rPad = new THREE.Mesh(padGeo, padMat);\n                    rPad.position.set(0.4, 1.7, 0);\n                    group.add(rPad);\n                } else {\n                    // Red glowing zombie eyes!\n                    const eyeGeo = new THREE.SphereGeometry(0.04, 6, 6);\n                    const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });\n                    const lEye = new THREE.Mesh(eyeGeo, eyeMat);\n                    lEye.position.set(-0.07, 2.05, 0.17);\n                    group.add(lEye);\n                    const rEye = new THREE.Mesh(eyeGeo, eyeMat);\n                    rEye.position.set(0.07, 2.05, 0.17);\n                    group.add(rEye);\n                }\n                \n                // Legs\n                const leftLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.08, 0.9, 8), clothMat);\n                leftLeg.position.set(-0.18, 0.45, 0);\n                leftLeg.castShadow = true;\n                leftLeg.name = 'leg';\n                group.add(leftLeg);\n                \n                const rightLeg = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.08, 0.9, 8), clothMat);\n                rightLeg.position.set(0.18, 0.45, 0);\n                rightLeg.castShadow = true;\n                rightLeg.name = 'leg';\n                group.add(rightLeg);\n\n                // Arms\n                const leftArm = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.07, 0.8, 8), clothMat);\n                leftArm.position.set(-0.45, 1.4, 0);\n                leftArm.castShadow = true;\n                leftArm.name = 'arm';\n                group.add(leftArm);\n                \n                const rightArm = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.07, 0.8, 8), clothMat);\n                rightArm.position.set(0.45, 1.4, 0);\n                rightArm.castShadow = true;\n                rightArm.name = 'arm';\n                group.add(rightArm);\n\n                leftLeg.geometry.translate(0, -0.45, 0);\n                leftLeg.position.y = 0.9;\n                rightLeg.geometry.translate(0, -0.45, 0);\n                rightLeg.position.y = 0.9;\n                leftArm.geometry.translate(0, -0.4, 0);\n                leftArm.position.y = 1.8;\n                rightArm.geometry.translate(0, -0.4, 0);\n                rightArm.position.y = 1.8;\n\n                group.userData.parts = { torso, head, leftLeg, rightLeg, leftArm, rightArm };\n                group.userData.walkPhase = 0;\n                group.scale.set(scale, scale, scale);\n                return group;\n            }\n\n            function updatePlayerModelWeapon() {\n                if (!player.model) return;\n                const parts = player.model.userData.parts;\n                const rightArm = parts.rightArm;\n                \n                // Remove existing weapon if any\n                if (player.model.userData.weapon) {\n                    rightArm.remove(player.model.userData.weapon);\n                }\n\n                const weaponType = player.currentWeaponId;\n                const weaponConfig = window.gameConfig.weapons[weaponType];\n                const weaponTexture = assetCache.textures[weaponConfig.texture] || assetCache.textures['pistol_weapon'];\n                const weaponGroup = new THREE.Group();\n                const body = new THREE.Mesh(\n                    new THREE.BoxGeometry(weaponConfig.size[0] * 1.5, weaponConfig.size[1] * 1.5, weaponConfig.size[2] * 1.5), \n                    new THREE.MeshStandardMaterial({ map: weaponTexture, color: 0xffffff, roughness: 0.5 })\n                );\n                weaponGroup.add(body);\n                weaponGroup.position.set(0.1, -0.4, 0.2);\n                weaponGroup.rotation.x = -Math.PI / 2;\n                rightArm.add(weaponGroup);\n                player.model.userData.weapon = weaponGroup;\n            }\n\n            const assetCache = { images: {}, audio: {}, textures: {} };\n            const audioContext = new (window.AudioContext || window.webkitAudioContext)();\n\n            async function preloadAssets() {\n                const assetIds = [\n                    'player_character', 'zombie_basic', 'pistol_weapon', 'health_pack', 'ammo_box', \n                    'building_residential', 'building_store', 'ground_asphalt', 'ground_dirt',\n                    'vehicle_abandoned', 'muzzle_flash', 'blood_particle', 'pistol_shot_sfx', \n                    'zombie_groan_sfx', 'pickup_collect_sfx', 'player_hurt_sfx', 'reload_sfx', \n                    'ambient_wind_sfx', 'ak47_texture', 'shotgun_texture', 'ak47_shot_sfx', \n                    'shotgun_shot_sfx', 'sniper_texture', 'smg_texture', 'lmg_texture',\n                    'sniper_shot_sfx', 'smg_shot_sfx', 'lmg_shot_sfx', 'headshot_sticker', \n                    'streak_sticker', 'eliminated_sticker'\n                ];\n                \n                // Set loading background image for epic immersive presentation\n                const loadingInfo = lib.getAsset('loading_background_image');\n                if (loadingInfo) {\n                    document.getElementById('loading-screen').style.backgroundImage = `url(${loadingInfo.url})`;\n                }\n\n                const loadingFill = document.getElementById('loading-fill');\n                let loaded = 0;\n                const total = assetIds.length;\n                \n                await Promise.all(assetIds.map(async (id) => {\n                    const assetInfo = lib.getAsset(id);\n                    if (!assetInfo) return;\n                    if (assetInfo.type === 'image') {\n                        const img = new Image();\n                        img.crossOrigin = 'anonymous';\n                        await new Promise(r => {\n                            img.onload = () => {\n                                assetCache.images[id] = img;\n                                const tex = new THREE.Texture(img);\n                                tex.needsUpdate = true;\n                                assetCache.textures[id] = tex;\n                                r();\n                            };\n                            img.src = assetInfo.url;\n                        });\n                    } else if (assetInfo.type === 'audio') {\n                        try {\n                            const res = await fetch(assetInfo.url);\n                            const buf = await audioContext.decodeAudioData(await res.arrayBuffer());\n                            assetCache.audio[id] = buf;\n                        } catch (e) {}\n                    }\n                    loaded++;\n                    loadingFill.style.width = (loaded / total * 100) + '%';\n                }));\n                document.getElementById('loading-screen').style.display = 'none';\n            }\n\n            function playSound(id, vol = 1, loop = false) {\n                if (!assetCache.audio[id]) return null;\n                const src = audioContext.createBufferSource();\n                const gain = audioContext.createGain();\n                src.buffer = assetCache.audio[id];\n                src.loop = loop;\n                gain.gain.value = vol;\n                src.connect(gain).connect(audioContext.destination);\n                src.start(0);\n                return { src, gain };\n            }\n\n            function updateUI() {\n                document.getElementById('health-text').textContent = Math.max(0, Math.floor(player.health));\n                document.getElementById('health-fill').style.width = (player.health / player.maxHealth * 100) + '%';\n                const cur = player.weapons[player.currentWeaponId];\n                document.getElementById('ammo-text').textContent = `${cur.name}: ${cur.currentAmmo} / ${cur.maxAmmo}`;\n                document.getElementById('wave-text').textContent = `Level ${gameState.currentLevel}`;\n                document.getElementById('score-text').textContent = `Score: ${gameState.score}`;\n            }\n\n            function switchWeapon(id) {\n                if (!player.inventory.includes(id) || player.isReloading) return;\n                player.currentWeaponId = id;\n                updatePlayerModelWeapon();\n                updateUI();\n            }\n\n            class Zombie {\n                constructor(position, isGiant = false) {\n                    this.position = position.clone();\n                    this.velocity = new THREE.Vector3();\n                    this.rotation = 0;\n                    this.isGiant = isGiant;\n                    if (this.isGiant) {\n                        this.health = window.gameConfig.giantZombie.baseHealth + (gameState.currentLevel - 1) * 30;\n                        this.damage = window.gameConfig.giantZombie.damage + (gameState.currentLevel - 1) * 5;\n                        this.speed = window.gameConfig.giantZombie.speed;\n                        this.scale = 2.5;\n                    } else {\n                        this.health = window.gameConfig.zombie.baseHealth + (gameState.currentLevel - 1) * 15;\n                        this.damage = window.gameConfig.zombie.damage + (gameState.currentLevel - 1) * 2;\n                        this.speed = window.gameConfig.zombie.speed;\n                        this.scale = 1;\n                    }\n                    this.maxHealth = this.health;\n                    this.state = 0; // IDLE\n                    this.isAlerted = false;\n                    this.attackCooldown = 0;\n                    this.model = createCharacterModel(true, this.scale);\n                    this.model.position.copy(this.position);\n                    scene.add(this.model);\n                }\n                update(dt) {\n                    const dist = this.position.distanceTo(player.position);\n                    if (dist < (this.isGiant ? 4 : 2)) this.state = 2; // ATTACK\n                    else if (dist < window.gameConfig.zombie.detectionRange || this.isAlerted) {\n                        this.state = 1; // CHASE\n                        this.isAlerted = true;\n                    } else this.state = 0;\n\n                    if (this.state === 1) {\n                        const dir = new THREE.Vector3().subVectors(player.position, this.position).normalize();\n                        const s = (this.speed + (gameState.currentLevel * 0.1)) * 5 * dt;\n                        this.velocity.copy(dir).multiplyScalar(s);\n                        this.model.rotation.y = Math.atan2(dir.x, dir.z);\n                    } else if (this.state === 2) {\n                        this.velocity.set(0, 0, 0);\n                        this.attackCooldown -= dt;\n                        if (this.attackCooldown <= 0 && !gameState.gameOver) {\n                            player.health -= this.damage;\n                            playSound('player_hurt_sfx', 0.5);\n                            this.attackCooldown = 1.5;\n                            player.screenShake = 0.5;\n                            document.getElementById('blood-overlay').style.opacity = 1;\n                            setTimeout(() => document.getElementById('blood-overlay').style.opacity = 0, 300);\n                            updateUI();\n                        }\n                    }\n                    this.position.add(this.velocity);\n                    this.model.position.copy(this.position);\n                    animateCharacter(this.model, dt, this.state === 1, true);\n                }\n                takeDamage(amt) {\n                    this.health -= amt;\n                    this.isAlerted = true;\n                    if (this.health <= 0) return true;\n                    return false;\n                }\n                destroy() { scene.remove(this.model); }\n            }\n\n            const zombies = [];\n            const pickups = [];\n            const townProps = new THREE.Group();\n            scene.add(townProps);\n            function spawnWave() {\n                zombies.forEach(z => z.destroy());\n                zombies.length = 0;\n                let count = Math.floor((5 + gameState.currentLevel * 2) * window.gameConfig.wave.zombiesMultiplier);\n                if (gameState.currentLevel % 5 === 0) {\n                    zombies.push(new Zombie(new THREE.Vector3(50, 0, 50), true));\n                    count = Math.max(0, count - 1);\n                }\n                for (let i = 0; i < count; i++) {\n                    const spawn = window.gameConfig.zombieSpawns[i % window.gameConfig.zombieSpawns.length];\n                    zombies.push(new Zombie(new THREE.Vector3(spawn.x + (Math.random()-0.5)*5, 0, spawn.z + (Math.random()-0.5)*5)));\n                }\n                playSound('zombie_groan_sfx', 0.4);\n            }\n\n            const particles = [];\n\n            function createBulletImpact(pos, normal) {\n                // Sparks\n                for (let i = 0; i < 6; i++) {\n                    const pGeo = new THREE.BoxGeometry(0.06, 0.06, 0.06);\n                    const pMat = new THREE.MeshBasicMaterial({ color: 0xffaa00 });\n                    const p = new THREE.Mesh(pGeo, pMat);\n                    p.position.copy(pos);\n                    \n                    const spread = 0.5;\n                    p.userData = {\n                        velocity: normal.clone().add(new THREE.Vector3(\n                            (Math.random() - 0.5) * spread,\n                            (Math.random() - 0.5) * spread,\n                            (Math.random() - 0.5) * spread\n                        )).normalize().multiplyScalar(4 + Math.random() * 4),\n                        gravity: 9.8,\n                        life: 0.3 + Math.random() * 0.3\n                    };\n                    scene.add(p);\n                    particles.push(p);\n                }\n            }\n\n            function createBloodSplatter(pos) {\n                for (let i = 0; i < 8; i++) {\n                    const pGeo = new THREE.BoxGeometry(0.08, 0.08, 0.08);\n                    const pMat = new THREE.MeshBasicMaterial({ color: 0x8a0303 });\n                    const p = new THREE.Mesh(pGeo, pMat);\n                    p.position.copy(pos);\n                    \n                    p.userData = {\n                        velocity: new THREE.Vector3(\n                            (Math.random() - 0.5) * 3,\n                            (Math.random() - 0.2) * 4,\n                            (Math.random() - 0.5) * 3\n                        ),\n                        gravity: 12,\n                        life: 0.4 + Math.random() * 0.4\n                    };\n                    scene.add(p);\n                    particles.push(p);\n                }\n            }\n\n            function spawnMuzzleFlash() {\n                if (!player.model) return;\n                const parts = player.model.userData.parts;\n                const rightArm = parts.rightArm;\n                const weapon = player.model.userData.weapon;\n                if (!weapon) return;\n\n                const flashGeo = new THREE.PlaneGeometry(0.6, 0.6);\n                const flashTex = assetCache.textures['muzzle_flash'];\n                if (!flashTex) return;\n                const flashMat = new THREE.MeshBasicMaterial({ \n                    map: flashTex, \n                    transparent: true, \n                    blending: THREE.AdditiveBlending,\n                    depthWrite: false\n                });\n                const flash = new THREE.Mesh(flashGeo, flashMat);\n                flash.position.set(0, 0, 0.8); // barrel end\n                weapon.add(flash);\n\n                // Flash light\n                const flashLight = new THREE.PointLight(0xffaa44, 2, 8);\n                flashLight.position.set(0, 0, 0.8);\n                weapon.add(flashLight);\n\n                setTimeout(() => {\n                    weapon.remove(flash);\n                    weapon.remove(flashLight);\n                }, 60);\n            }\n\n            function shoot() {\n                const cur = player.weapons[player.currentWeaponId];\n                if (cur.currentAmmo <= 0 || player.isReloading || player.shootCooldown > 0) {\n                    if (cur.currentAmmo <= 0 && !player.isReloading) reload();\n                    return;\n                }\n                cur.currentAmmo--;\n                player.shootCooldown = cur.fireRate;\n                player.recoil = 1.0;\n                player.screenShake = 0.25;\n                player.rotation.pitch += 0.02;\n                \n                // Texture ID mapping\n                const sfxId = cur.texture.replace('_texture', '_shot_sfx');\n                playSound(sfxId, 0.5);\n                updateUI();\n                spawnMuzzleFlash();\n\n                const ray = new THREE.Raycaster();\n                ray.setFromCamera(new THREE.Vector2(0,0), camera);\n                \n                // Raycast against zombies and environment props\n                const targetObjects = [...zombies.map(z => z.model), townProps];\n                const intersects = ray.intersectObjects(targetObjects, true);\n                \n                if (intersects.length > 0) {\n                    const hitObj = intersects[0].object;\n                    let hitZombie = null;\n                    for (let z of zombies) {\n                        let found = false;\n                        z.model.traverse(c => { if (c === hitObj) found = true; });\n                        if (found) { hitZombie = z; break; }\n                    }\n\n                    if (hitZombie) {\n                        const isHead = hitObj.name === 'head';\n                        const damageDealt = cur.damage * (isHead ? 2.5 : 1);\n                        const killed = hitZombie.takeDamage(damageDealt);\n                        \n                        showDamageNumber(damageDealt, intersects[0].point);\n                        createBloodSplatter(intersects[0].point);\n                        \n                        document.getElementById('hit-marker').classList.add('active');\n                        setTimeout(() => document.getElementById('hit-marker').classList.remove('active'), 100);\n\n                        if (killed) {\n                            zombies.splice(zombies.indexOf(hitZombie), 1);\n                            hitZombie.destroy();\n                            gameState.score += 10;\n                            gameState.zombiesKilled++;\n                            showKillTag(isHead ? 'HEADSHOT' : 'ELIMINATED', isHead ? 'headshot' : 'eliminated');\n                            updateUI();\n                        }\n                    } else {\n                        // Environment hit\n                        createBulletImpact(intersects[0].point, intersects[0].face ? intersects[0].face.normal : new THREE.Vector3(0, 1, 0));\n                    }\n                }\n            }\n\n            function reload() {\n                const cur = player.weapons[player.currentWeaponId];\n                if (!player.isReloading && cur.currentAmmo < cur.maxAmmo) {\n                    player.isReloading = true;\n                    player.reloadTime = cur.reloadTime;\n                    playSound('reload_sfx', 0.6);\n                }\n            }\n\n            function animateCharacter(model, dt, moving, isZombie = false) {\n                const parts = model.userData.parts;\n                if (!parts) return;\n                \n                if (moving) model.userData.walkPhase += dt * 12;\n                else model.userData.walkPhase *= 0.9;\n                const p = model.userData.walkPhase;\n                const a = moving ? 0.6 : 0;\n                \n                parts.leftLeg.rotation.x = Math.sin(p) * a;\n                parts.rightLeg.rotation.x = Math.sin(p + Math.PI) * a;\n                \n                if (isZombie) {\n                    // Shambling pose\n                    parts.leftArm.rotation.x = -Math.PI / 2 + Math.sin(p) * 0.1 * a;\n                    parts.rightArm.rotation.x = -Math.PI / 2 + Math.cos(p) * 0.1 * a;\n                } else {\n                    // Holding stance\n                    if (model.userData.weapon) {\n                        parts.rightArm.rotation.x = -Math.PI / 2.2 + Math.sin(p) * 0.1 * a - player.recoil * 0.5 + player.rotation.pitch;\n                        parts.leftArm.rotation.x = -Math.PI / 2.5 + Math.sin(p + Math.PI) * 0.05 * a + player.rotation.pitch;\n                    } else {\n                        parts.leftArm.rotation.x = Math.sin(p + Math.PI) * 0.4 * a;\n                        parts.rightArm.rotation.x = Math.sin(p) * 0.4 * a;\n                    }\n                }\n            }\n\n            function showKillTag(text, type) {\n                const container = document.getElementById('kill-tag-container');\n                const tag = document.createElement('div');\n                tag.className = `kill-tag ${type}`;\n                const sticker = document.createElement('img');\n                sticker.className = 'kill-sticker';\n                sticker.src = lib.getAsset(type === 'headshot' ? 'headshot_sticker' : 'eliminated_sticker').url;\n                const label = document.createElement('span');\n                label.textContent = text;\n                tag.append(sticker, label);\n                container.appendChild(tag);\n                void tag.offsetWidth;\n                tag.classList.add('active');\n                setTimeout(() => tag.remove(), 1500);\n            }\n\n            function showDamageNumber(amount, position) {\n                const el = document.createElement('div');\n                el.className = 'damage-number';\n                el.textContent = Math.floor(amount);\n                document.getElementById('kill-tag-container').appendChild(el);\n\n                const tempV = new THREE.Vector3(position.x, position.y + 1, position.z);\n                tempV.project(camera);\n\n                el.style.left = `${(tempV.x * 0.5 + 0.5) * canvas.clientWidth}px`;\n                el.style.top = `${(-tempV.y * 0.5 + 0.5) * canvas.clientHeight}px`;\n\n                setTimeout(() => el.remove(), 600);\n            }\n\n            function updateStoreUI() {\n                const weaponGrid = document.getElementById('weapon-store-grid');\n                weaponGrid.innerHTML = '';\n                const prices = { smg: 800, ak47: 1200, shotgun: 1500, sniper: 2500, lmg: 4000 };\n                Object.keys(prices).forEach(id => {\n                    const owned = player.inventory.includes(id);\n                    const cost = prices[id];\n                    const item = document.createElement('div');\n                    item.className = 'store-item';\n                    item.innerHTML = `<div>${id.toUpperCase()}</div><div>${owned ? 'OWNED' : 'Cost: '+cost}</div>\n                        <button class=\"buy-button\" ${owned || gameState.score < cost ? 'disabled' : ''} onclick=\"buyWeaponFromStore('${id}')\">BUY</button>`;\n                    weaponGrid.appendChild(item);\n                });\n                document.getElementById('store-score').textContent = `Score: ${gameState.score}`;\n            }\n\n            window.buyWeaponFromStore = (id) => {\n                const cost = { smg: 800, ak47: 1200, shotgun: 1500, sniper: 2500, lmg: 4000 }[id];\n                if (gameState.score >= cost && !player.inventory.includes(id)) {\n                    gameState.score -= cost;\n                    player.inventory.push(id);\n                    switchWeapon(id);\n                    updateStoreUI();\n                }\n            };\n\n            window.upgradeMaxHealth = () => {\n                const cost = Math.floor(250 * Math.pow(1.5, player.healthLevel));\n                if (gameState.score >= cost) {\n                    gameState.score -= cost;\n                    player.healthLevel++;\n                    player.maxHealth += 25;\n                    player.health = player.maxHealth;\n                    updateUI();\n                    updateStoreUI();\n                }\n            };\n\n            // Main Loops\n            await preloadAssets();\n            \n            // Create the player character model inside the 3D scene!\n            player.model = createCharacterModel(false, 1);\n            scene.add(player.model);\n            updatePlayerModelWeapon();\n\n            // Play background spooky looping ambient wind\n            playSound('ambient_wind_sfx', 0.25, true);\n\n            // Setup creator controls\n            lib.showGameParameters({\n                name: 'Undead Frontier Settings',\n                params: {\n                    'Player Max Health': { key: 'player.startingHealth', type: 'number', onChange: (val) => { player.maxHealth = val; player.health = Math.min(player.health, val); updateUI(); } },\n                    'Player Movement Speed': { key: 'player.speedMultiplier', type: 'slider', min: 0.5, max: 3.0, step: 0.1 },\n                    'Zombie Chase Speed': { key: 'zombie.speed', type: 'slider', min: 0.5, max: 4.0, step: 0.1 },\n                    'Zombie Alert Distance': { key: 'zombie.detectionRange', type: 'slider', min: 10, max: 100, step: 5 }\n                }\n            });\n\n            buildTown();\n            spawnWave();\n            updateUI();\n\n            let lastTime = performance.now();\n            function gameLoop(time) {\n                const dt = Math.min((time - lastTime) / 1000, 0.11);\n                lastTime = time;\n\n                if (gameState.gameOver) { \n                    renderer.render(scene, camera); \n                    requestAnimationFrame(gameLoop); \n                    return; \n                }\n\n                // Movement Input\n                let mx = 0, mz = 0;\n                if (isTouchDevice) {\n                    mx = -input.touch.move.x; mz = -input.touch.move.y;\n                } else {\n                    if (input.keys['w'] || input.keys['arrowup']) mz += 1; \n                    if (input.keys['s'] || input.keys['arrowdown']) mz -= 1;\n                    if (input.keys['a'] || input.keys['arrowleft']) mx += 1; \n                    if (input.keys['d'] || input.keys['arrowright']) mx -= 1;\n                }\n                const len = Math.sqrt(mx*mx + mz*mz);\n                if (len > 0) { mx /= len; mz /= len; }\n                \n                if (isTouchDevice) {\n                    player.rotation.y -= input.touch.lookDelta.x * 0.005;\n                    player.rotation.pitch -= input.touch.lookDelta.y * 0.005;\n                    // Fix: Reset lookDelta to prevent infinite spinning camera\n                    input.touch.lookDelta.x = 0;\n                    input.touch.lookDelta.y = 0;\n                } else if (input.mouse.locked) {\n                    player.rotation.y -= input.mouse.deltaX * 0.002;\n                    player.rotation.pitch -= input.mouse.deltaY * 0.002;\n                }\n                input.mouse.deltaX = 0;\n                input.mouse.deltaY = 0;\n                player.rotation.pitch = Math.max(-Math.PI/3, Math.min(Math.PI/3, player.rotation.pitch));\n\n                // Jump & Gravity physics\n                let sprintMod = 1.0;\n                if (input.keys['shift'] || input.touch.sprint) {\n                    sprintMod = 1.5;\n                }\n                \n                if (input.keys[' '] && player.isGrounded) {\n                    player.yVelocity = 12;\n                    player.isGrounded = false;\n                }\n\n                if (!player.isGrounded) {\n                    player.yVelocity -= 32 * dt; // gravity\n                    player.position.y += player.yVelocity * dt;\n                    if (player.position.y <= 0) {\n                        player.position.y = 0;\n                        player.yVelocity = 0;\n                        player.isGrounded = true;\n                    }\n                }\n\n                // Apply velocity and move player\n                const speed = 8 * window.gameConfig.player.speedMultiplier * sprintMod * dt;\n                const forward = new THREE.Vector3(Math.sin(player.rotation.y), 0, Math.cos(player.rotation.y));\n                const right = new THREE.Vector3(Math.sin(player.rotation.y + Math.PI/2), 0, Math.cos(player.rotation.y + Math.PI/2));\n                player.velocity.set(0,0,0).add(forward.multiplyScalar(mz * speed)).add(right.multiplyScalar(mx * speed));\n                player.position.add(player.velocity);\n\n                // Collision Detection and Resolver with Buildings and Obstacles\n                const playerRadius = 0.8;\n                townProps.children.forEach(obj => {\n                    // Check building / vehicle box colliders\n                    if (obj.geometry && obj.geometry.type === 'BoxGeometry') {\n                        const box = new THREE.Box3().setFromObject(obj);\n                        const playerBox = new THREE.Box3(\n                            new THREE.Vector3(player.position.x - playerRadius, 0, player.position.z - playerRadius),\n                            new THREE.Vector3(player.position.x + playerRadius, 5, player.position.z + playerRadius)\n                        );\n                        \n                        if (box.intersectsBox(playerBox)) {\n                            // Resolve collision out of box\n                            const overlapX = Math.min(box.max.x - playerBox.min.x, playerBox.max.x - box.min.x);\n                            const overlapZ = Math.min(box.max.z - playerBox.min.z, playerBox.max.z - box.min.z);\n                            \n                            if (overlapX < overlapZ) {\n                                if (player.position.x < (box.min.x + box.max.x) / 2) {\n                                    player.position.x -= overlapX;\n                                } else {\n                                    player.position.x += overlapX;\n                                }\n                            } else {\n                                if (player.position.z < (box.min.z + box.max.z) / 2) {\n                                    player.position.z -= overlapZ;\n                                } else {\n                                    player.position.z += overlapZ;\n                                }\n                            }\n                        }\n                    }\n                });\n\n                // Update Player Model\n                if (player.model) {\n                    player.model.position.copy(player.position);\n                    player.model.rotation.y = player.rotation.y;\n                    const moving = len > 0;\n                    animateCharacter(player.model, dt, moving);\n\n                    // First-person: Hide head, torso, and legs; show hands/arms and gun\n                    player.model.visible = true;\n                    const parts = player.model.userData.parts;\n                    if (parts) {\n                        parts.head.visible = false;\n                        parts.torso.visible = false;\n                        parts.leftLeg.visible = false;\n                        parts.rightLeg.visible = false;\n                        parts.leftArm.visible = true;\n                        parts.rightArm.visible = true;\n\n                        // Bring the arms slightly inward and forward so they are fully visible in FPS view\n                        parts.rightArm.position.set(0.18, 1.35, 0.35);\n                        parts.leftArm.position.set(-0.18, 1.35, 0.35);\n                    }\n                }\n\n                // Update active sparks & blood particles\n                for (let i = particles.length - 1; i >= 0; i--) {\n                    const p = particles[i];\n                    p.userData.life -= dt;\n                    if (p.userData.life <= 0) {\n                        scene.remove(p);\n                        particles.splice(i, 1);\n                    } else {\n                        p.userData.velocity.y -= p.userData.gravity * dt;\n                        p.position.addScaledVector(p.userData.velocity, dt);\n                    }\n                }\n\n                // Floating pickup animations and collision checks\n                for (let i = pickups.length - 1; i >= 0; i--) {\n                    const p = pickups[i];\n                    p.model.rotation.y += dt * 2.0;\n                    p.model.position.y = 0.5 + Math.sin(time * 0.003) * 0.15;\n\n                    if (player.position.distanceTo(p.position) < 2) {\n                        playSound('pickup_collect_sfx', 0.7);\n                        if (p.type === 'health') {\n                            player.health = Math.min(player.maxHealth, player.health + window.gameConfig.pickup.healthAmount);\n                        } else if (p.type === 'ammo') {\n                            player.weapons[player.currentWeaponId].currentAmmo = Math.min(player.weapons[player.currentWeaponId].maxAmmo, player.weapons[player.currentWeaponId].currentAmmo + window.gameConfig.pickup.ammoAmount);\n                        } else if (p.type.startsWith('weapon_')) {\n                            const weaponId = p.type.replace('weapon_', '');\n                            if (!player.inventory.includes(weaponId)) {\n                                player.inventory.push(weaponId);\n                                switchWeapon(weaponId);\n                            }\n                        }\n                        scene.remove(p.model);\n                        pickups.splice(i, 1);\n                        updateUI();\n                    }\n                }\n                \n                // Camera setup: First-Person (see weapon and hands only)\n                const camHeight = 1.6;\n                const forwardDir = new THREE.Vector3(\n                    Math.sin(player.rotation.y) * Math.cos(player.rotation.pitch),\n                    Math.sin(player.rotation.pitch),\n                    Math.cos(player.rotation.y) * Math.cos(player.rotation.pitch)\n                ).normalize();\n\n                const camPos = player.position.clone().add(new THREE.Vector3(0, camHeight, 0));\n                camera.position.copy(camPos);\n\n                const lookTarget = camPos.clone().add(forwardDir);\n                camera.lookAt(lookTarget);\n\n                // Immersive Camera Screen Shake\n                if (player.screenShake > 0) {\n                    camera.position.x += (Math.random() - 0.5) * player.screenShake;\n                    camera.position.y += (Math.random() - 0.5) * player.screenShake;\n                    camera.position.z += (Math.random() - 0.5) * player.screenShake;\n                    player.screenShake *= 0.9;\n                }\n\n                // ADS Camera Zoom and Viewmodel toggle\n                const isADS = input.keys['mouse2'] || input.touch.zoom;\n                if (isADS) {\n                    camera.fov = 30;\n                    camera.updateProjectionMatrix();\n                    document.getElementById('scope-overlay').classList.add('active');\n                } else {\n                    camera.fov = 75;\n                    camera.updateProjectionMatrix();\n                    document.getElementById('scope-overlay').classList.remove('active');\n                }\n\n                // Handle shooting\n                if (input.keys['mouse0'] || input.touch.shoot) shoot();\n\n                if (player.isReloading) {\n                    player.reloadTime -= dt;\n                    if (player.reloadTime <= 0) {\n                        player.weapons[player.currentWeaponId].currentAmmo = player.weapons[player.currentWeaponId].maxAmmo;\n                        player.isReloading = false;\n                        updateUI();\n                    }\n                }\n                if (player.shootCooldown > 0) player.shootCooldown -= dt;\n                if (player.recoil > 0) player.recoil *= 0.85;\n\n                // Update Zombies\n                zombies.forEach(z => z.update(dt));\n\n                if (gameState.waveInProgress && zombies.length === 0) {\n                    gameState.waveInProgress = false;\n                    gameState.storeOpen = true;\n                    document.getElementById('store-screen').classList.add('active');\n                    updateStoreUI();\n                }\n\n                if (player.health <= 0) {\n                    gameState.gameOver = true;\n                    document.getElementById('death-screen').classList.add('active');\n                }\n\n                // Live-updating Minimap\n                minimapCamera.position.set(player.position.x, 100, player.position.z);\n                minimapCamera.lookAt(player.position.x, 0, player.position.z);\n                minimapRenderer.render(scene, minimapCamera);\n\n                // Render main 3D scene\n                renderer.render(scene, camera);\n                requestAnimationFrame(gameLoop);\n            }\n\n            if (mode === 'play') {\n                gameState.waveInProgress = true;\n                requestAnimationFrame(gameLoop);\n            }\n\n            function buildTown() {\n                // Clear old items from townProps\n                while (townProps.children.length > 0) {\n                    townProps.remove(townProps.children[0]);\n                }\n\n                // 1. Detailed Tiled Asphalt Ground\n                const groundGeo = new THREE.PlaneGeometry(200, 200);\n                const asphaltTex = assetCache.textures['ground_asphalt'].clone();\n                asphaltTex.wrapS = THREE.RepeatWrapping;\n                asphaltTex.wrapT = THREE.RepeatWrapping;\n                asphaltTex.repeat.set(30, 30);\n                asphaltTex.needsUpdate = true;\n                \n                const groundMat = new THREE.MeshStandardMaterial({ \n                    map: asphaltTex, \n                    roughness: 0.85,\n                    metalness: 0.1\n                });\n                const ground = new THREE.Mesh(groundGeo, groundMat);\n                ground.rotation.x = -Math.PI/2;\n                ground.receiveShadow = true;\n                townProps.add(ground);\n\n                // Border dirt/sparse patches\n                const dirtTex = assetCache.textures['ground_dirt'].clone();\n                dirtTex.wrapS = THREE.RepeatWrapping;\n                dirtTex.wrapT = THREE.RepeatWrapping;\n                dirtTex.repeat.set(10, 10);\n                dirtTex.needsUpdate = true;\n                const dirtMat = new THREE.MeshStandardMaterial({ map: dirtTex, roughness: 0.9 });\n                \n                const borderPatches = [\n                    { w: 200, h: 40, x: 0, z: -100 },\n                    { w: 200, h: 40, x: 0, z: 100 },\n                    { w: 40, h: 200, x: -100, z: 0 },\n                    { w: 40, h: 200, x: 100, z: 0 }\n                ];\n                borderPatches.forEach(p => {\n                    const patchGeo = new THREE.PlaneGeometry(p.w, p.h);\n                    const patch = new THREE.Mesh(patchGeo, dirtMat);\n                    patch.rotation.x = -Math.PI/2;\n                    patch.position.set(p.x, 0.01, p.z);\n                    patch.receiveShadow = true;\n                    townProps.add(patch);\n                });\n\n                // 2. Weathered building layouts\n                const buildingPositions = [\n                    { x: -35, z: -35, w: 16, h: 14, d: 16, type: 'residential' },\n                    { x: -15, z: -35, w: 12, h: 12, d: 12, type: 'store' },\n                    { x: 15, z: -35, w: 14, h: 16, d: 14, type: 'residential' },\n                    { x: 35, z: -35, w: 18, h: 15, d: 16, type: 'store' },\n                    { x: -35, z: 35, w: 15, h: 12, d: 15, type: 'store' },\n                    { x: -15, z: 35, w: 14, h: 15, d: 14, type: 'residential' },\n                    { x: 15, z: 35, w: 16, h: 12, d: 16, type: 'store' },\n                    { x: 35, z: 35, w: 14, h: 18, d: 14, type: 'residential' },\n                    { x: -40, z: 0, w: 16, h: 15, d: 18, type: 'residential' },\n                    { x: 40, z: 0, w: 18, h: 16, d: 16, type: 'store' }\n                ];\n\n                buildingPositions.forEach(b => {\n                    const texName = b.type === 'residential' ? 'building_residential' : 'building_store';\n                    const tex = assetCache.textures[texName].clone();\n                    tex.wrapS = THREE.RepeatWrapping;\n                    tex.wrapT = THREE.RepeatWrapping;\n                    tex.repeat.set(b.w / 4, b.h / 4);\n                    tex.needsUpdate = true;\n\n                    const mat = new THREE.MeshStandardMaterial({ \n                        map: tex, \n                        roughness: 0.75, \n                        metalness: 0.15 \n                    });\n                    const geo = new THREE.BoxGeometry(b.w, b.h, b.d);\n                    const mesh = new THREE.Mesh(geo, mat);\n                    mesh.position.set(b.x, b.h / 2, b.z);\n                    mesh.castShadow = true;\n                    mesh.receiveShadow = true;\n                    townProps.add(mesh);\n\n                    // Roof frame details\n                    const roofGeo = new THREE.BoxGeometry(b.w + 0.4, 0.4, b.d + 0.4);\n                    const roofMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.9 });\n                    const roof = new THREE.Mesh(roofGeo, roofMat);\n                    roof.position.set(b.x, b.h + 0.2, b.z);\n                    roof.castShadow = true;\n                    townProps.add(roof);\n\n                    // Door shapes\n                    const doorGeo = new THREE.BoxGeometry(2, 3.5, 0.1);\n                    const doorMat = new THREE.MeshStandardMaterial({ color: 0x3d2314, metalness: 0.2 });\n                    const door = new THREE.Mesh(doorGeo, doorMat);\n                    door.position.set(b.x, 1.75, b.z + b.d / 2 + 0.05);\n                    townProps.add(door);\n                });\n\n                // 3. Abandoned Cars (3D textured boxes using vehicle_abandoned asset)\n                const carPositions = [\n                    { x: -10, z: -10, rot: Math.PI / 4 },\n                    { x: 20, z: 15, rot: -Math.PI / 6 },\n                    { x: -25, z: 20, rot: Math.PI / 2.3 },\n                    { x: 10, z: -25, rot: Math.PI / 1.1 }\n                ];\n                carPositions.forEach(pos => {\n                    const carGroup = new THREE.Group();\n                    const carTex = assetCache.textures['vehicle_abandoned'];\n                    const carMat = new THREE.MeshStandardMaterial({ \n                        map: carTex, \n                        transparent: true, \n                        alphaTest: 0.3,\n                        roughness: 0.6 \n                    });\n                    \n                    const bodyGeo = new THREE.BoxGeometry(2.2, 0.9, 4.5);\n                    const body = new THREE.Mesh(bodyGeo, carMat);\n                    body.position.y = 0.45;\n                    body.castShadow = true;\n                    carGroup.add(body);\n\n                    const cabinGeo = new THREE.BoxGeometry(1.9, 0.7, 2.5);\n                    const cabin = new THREE.Mesh(cabinGeo, carMat);\n                    cabin.position.set(0, 1.25, -0.2);\n                    cabin.castShadow = true;\n                    carGroup.add(cabin);\n\n                    const wheelGeo = new THREE.CylinderGeometry(0.35, 0.35, 0.4, 12);\n                    const wheelMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9 });\n                    const wheelXOffset = 1.1;\n                    const wheelZOffsets = [-1.3, 1.3];\n                    wheelZOffsets.forEach(wz => {\n                        [-1, 1].forEach(wx => {\n                            const w = new THREE.Mesh(wheelGeo, wheelMat);\n                            w.rotation.z = Math.PI / 2;\n                            w.position.set(wx * wheelXOffset, 0.35, wz);\n                            w.castShadow = true;\n                            carGroup.add(w);\n                        });\n                    });\n\n                    carGroup.position.set(pos.x, 0, pos.z);\n                    carGroup.rotation.y = pos.rot;\n                    townProps.add(carGroup);\n                });\n\n                // 4. Glowing Street Lamps with actual dynamic light casting\n                const lampPositions = [\n                    { x: -25, z: -15 },\n                    { x: 25, z: -15 },\n                    { x: -25, z: 15 },\n                    { x: 25, z: 15 }\n                ];\n                lampPositions.forEach(pos => {\n                    const lampGroup = new THREE.Group();\n                    \n                    const postGeo = new THREE.CylinderGeometry(0.12, 0.15, 6, 8);\n                    const postMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.8, roughness: 0.2 });\n                    const post = new THREE.Mesh(postGeo, postMat);\n                    post.position.y = 3;\n                    post.castShadow = true;\n                    lampGroup.add(post);\n\n                    const armGeo = new THREE.CylinderGeometry(0.08, 0.08, 1.5, 8);\n                    const arm = new THREE.Mesh(armGeo, postMat);\n                    arm.rotation.x = Math.PI / 2;\n                    arm.position.set(0, 5.8, 0.6);\n                    lampGroup.add(arm);\n\n                    const globeGeo = new THREE.SphereGeometry(0.3, 12, 12);\n                    const globeMat = new THREE.MeshStandardMaterial({ \n                        color: 0xfff4d6, \n                        emissive: 0xffd880, \n                        emissiveIntensity: 1.5 \n                    });\n                    const globe = new THREE.Mesh(globeGeo, globeMat);\n                    globe.position.set(0, 5.5, 1.2);\n                    lampGroup.add(globe);\n\n                    const light = new THREE.PointLight(0xffddaa, 0.8, 15);\n                    light.position.set(0, 5.3, 1.2);\n                    light.castShadow = true;\n                    light.shadow.bias = -0.002;\n                    lampGroup.add(light);\n\n                    lampGroup.position.set(pos.x, 0, pos.z);\n                    townProps.add(lampGroup);\n                });\n\n                // 5. Sparse Dead Trees for desolation atmosphere\n                const treePositions = [\n                    { x: -10, z: -30 },\n                    { x: 10, z: -30 },\n                    { x: -30, z: 10 },\n                    { x: 30, z: 10 },\n                    { x: 0, z: 45 },\n                    { x: -45, z: -10 }\n                ];\n                treePositions.forEach(pos => {\n                    const treeGroup = new THREE.Group();\n                    \n                    const trunkGeo = new THREE.CylinderGeometry(0.15, 0.4, 5, 8);\n                    const trunkMat = new THREE.MeshStandardMaterial({ color: 0x2e1d11, roughness: 0.9 });\n                    const trunk = new THREE.Mesh(trunkGeo, trunkMat);\n                    trunk.position.y = 2.5;\n                    trunk.castShadow = true;\n                    treeGroup.add(trunk);\n\n                    const branchesCount = 3 + Math.floor(Math.random() * 3);\n                    for (let i = 0; i < branchesCount; i++) {\n                        const bGeo = new THREE.CylinderGeometry(0.08, 0.15, 2, 6);\n                        const bMesh = new THREE.Mesh(bGeo, trunkMat);\n                        bMesh.position.set(\n                            (Math.random() - 0.5) * 0.5,\n                            4 + Math.random() * 1.5,\n                            (Math.random() - 0.5) * 0.5\n                        );\n                        bMesh.rotation.z = (Math.random() - 0.5) * Math.PI / 3;\n                        bMesh.rotation.x = (Math.random() - 0.5) * Math.PI / 3;\n                        bMesh.castShadow = true;\n                        treeGroup.add(bMesh);\n                    }\n\n                    treeGroup.position.set(pos.x, 0, pos.z);\n                    townProps.add(treeGroup);\n                });\n\n                // Rebuild active pickups\n                pickups.forEach(p => scene.remove(p.model));\n                pickups.length = 0;\n\n                window.gameConfig.pickupSpawns.forEach(spawn => {\n                    const pickupType = spawn.type;\n                    let model;\n                    if (pickupType === 'health') {\n                        model = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial({ map: assetCache.textures['health_pack'] }));\n                    } else if (pickupType === 'ammo') {\n                        model = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial({ map: assetCache.textures['ammo_box'] }));\n                    } else if (pickupType.startsWith('weapon_')) {\n                        const weaponId = pickupType.replace('weapon_', '');\n                        const weaponConfig = window.gameConfig.weapons[weaponId];\n                        const tex = assetCache.textures[weaponConfig.texture] || assetCache.textures['pistol_weapon'];\n                        model = new THREE.Mesh(new THREE.BoxGeometry(weaponConfig.size[0]*2, weaponConfig.size[1]*2, weaponConfig.size[2]*2), new THREE.MeshStandardMaterial({ map: tex }));\n                    }\n                    if (model) {\n                        model.position.set(spawn.x, 0.5, spawn.z);\n                        model.userData.type = pickupType;\n                        scene.add(model);\n                        pickups.push({ model, type: pickupType, position: model.position.clone() });\n                    }\n                });\n            }\n\n            window.addEventListener('resize', () => {\n                camera.aspect = canvas.clientWidth / canvas.clientHeight;\n                camera.updateProjectionMatrix();\n                renderer.setSize(canvas.clientWidth, canvas.clientHeight);\n            });\n\n            // Touch events\n            if(isTouchDevice) {\n                document.getElementById('touch-controls').style.display = 'block';\n                const moveJ = document.getElementById('move-joystick'), moveS = document.getElementById('move-stick');\n                canvas.addEventListener('touchstart', e => {\n                    for(let t of e.changedTouches) {\n                        if(t.clientX < window.innerWidth/2) {\n                            input.touch.move.active = true; input.touch.move.id = t.identifier;\n                            moveJ.style.left = (t.clientX - 80)+'px'; moveJ.style.top = (t.clientY - 80)+'px';\n                            moveJ.classList.add('active');\n                        } else {\n                            input.touch.look.active = true; input.touch.look.id = t.identifier;\n                            input.touch.look.lastX = t.clientX; input.touch.look.lastY = t.clientY;\n                        }\n                    }\n                });\n                canvas.addEventListener('touchmove', e => {\n                    for(let t of e.changedTouches) {\n                        if(input.touch.move.active && t.identifier === input.touch.move.id) {\n                            const rect = moveJ.getBoundingClientRect(), cx = rect.left + 80, cy = rect.top + 80;\n                            input.touch.move.x = (t.clientX - cx)/80; input.touch.move.y = (t.clientY - cy)/80;\n                            const dist = Math.min(80, Math.sqrt(input.touch.move.x*input.touch.move.x + input.touch.move.y*input.touch.move.y) * 80);\n                            const angle = Math.atan2(input.touch.move.y, input.touch.move.x);\n                            moveS.style.transform = `translate(-50%, -50%) translate(${Math.cos(angle)*dist}px, ${Math.sin(angle)*dist}px)`;\n                        }\n                        if(input.touch.look.active && t.identifier === input.touch.look.id) {\n                            input.touch.lookDelta.x = t.clientX - input.touch.look.lastX;\n                            input.touch.lookDelta.y = t.clientY - input.touch.look.lastY;\n                            input.touch.look.lastX = t.clientX; input.touch.look.lastY = t.clientY;\n                        }\n                    }\n                });\n                canvas.addEventListener('touchend', e => {\n                    for(let t of e.changedTouches) {\n                        if(t.identifier === input.touch.move.id) { input.touch.move.active = false; moveJ.classList.remove('active'); input.touch.move.x = 0; input.touch.move.y = 0; moveS.style.transform = 'translate(-50%, -50%)'; }\n                        if(t.identifier === input.touch.look.id) { input.touch.look.active = false; input.touch.lookDelta.x = 0; input.touch.lookDelta.y = 0; }\n                    }\n                });\n                document.getElementById('shoot-button').addEventListener('touchstart', (e) => { e.preventDefault(); input.touch.shoot = true; });\n                document.getElementById('shoot-button').addEventListener('touchend', (e) => { e.preventDefault(); input.touch.shoot = false; });\n                document.getElementById('reload-button').addEventListener('touchstart', (e) => { e.preventDefault(); reload(); });\n                document.getElementById('zoom-button').addEventListener('touchstart', (e) => { e.preventDefault(); input.touch.zoom = true; });\n                document.getElementById('zoom-button').addEventListener('touchend', (e) => { e.preventDefault(); input.touch.zoom = false; });\n                \n                // Switch Weapon Button Touch Handler\n                document.getElementById('switch-button').addEventListener('touchstart', (e) => {\n                    e.preventDefault();\n                    const currentIndex = player.inventory.indexOf(player.currentWeaponId);\n                    const nextIndex = (currentIndex + 1) % player.inventory.length;\n                    switchWeapon(player.inventory[nextIndex]);\n                });\n\n                // Sprint Button Touch Handlers\n                document.getElementById('sprint-button').addEventListener('touchstart', (e) => {\n                    e.preventDefault();\n                    input.touch.sprint = true;\n                });\n                document.getElementById('sprint-button').addEventListener('touchend', (e) => {\n                    e.preventDefault();\n                    input.touch.sprint = false;\n                });\n\n                // Jump Button Touch Handler\n                document.getElementById('jump-button').addEventListener('touchstart', (e) => {\n                    e.preventDefault();\n                    if (player.isGrounded) {\n                        player.yVelocity = 12;\n                        player.isGrounded = false;\n                    }\n                });\n            } else {\n                const clickToPlay = document.getElementById('click-to-play');\n                function requestLock() {\n                    audioContext.resume();\n                    if (!input.mouse.locked) {\n                        canvas.requestPointerLock();\n                    }\n                }\n                if (clickToPlay) {\n                    clickToPlay.addEventListener('mousedown', requestLock);\n                    clickToPlay.addEventListener('touchstart', (e) => { e.preventDefault(); requestLock(); });\n                }\n                canvas.addEventListener('mousedown', (e) => {\n                    if (!input.mouse.locked && e.button === 0) requestLock();\n                });\n                document.addEventListener('pointerlockchange', () => {\n                    input.mouse.locked = document.pointerLockElement === canvas;\n                    const overlay = document.getElementById('click-to-play');\n                    if (overlay) {\n                        overlay.style.display = input.mouse.locked ? 'none' : 'flex';\n                    }\n                });\n                document.addEventListener('mousemove', e => {\n                    if (input.mouse.locked) {\n                        input.mouse.deltaX += e.movementX;\n                        input.mouse.deltaY += e.movementY;\n                    }\n                });\n                document.addEventListener('keydown', e => {\n                    input.keys[e.key.toLowerCase()] = true;\n                    if (e.key.toLowerCase() === 'r') reload();\n                });\n                document.addEventListener('keyup', e => {\n                    input.keys[e.key.toLowerCase()] = false;\n                });\n                document.addEventListener('mousedown', e => {\n                    if (!input.mouse.locked) return;\n                    if (e.button === 0) input.keys['mouse0'] = true; // Left click\n                    if (e.button === 2) input.keys['mouse2'] = true; // Right click for zoom\n                });\n                document.addEventListener('mouseup', e => {\n                    if (e.button === 0) input.keys['mouse0'] = false;\n                    if (e.button === 2) input.keys['mouse2'] = false;\n                });\n            }\n            \n            // Touch device safety - remove mouse lock overlay on mobile\n            if (isTouchDevice) {\n                const overlay = document.getElementById('click-to-play');\n                if (overlay) overlay.remove();\n            }\n\n            document.getElementById('next-level-button').addEventListener('click', () => {\n                document.getElementById('store-screen').classList.remove('active');\n                gameState.storeOpen = false;\n                gameState.currentLevel++;\n                gameState.waveInProgress = true;\n                spawnWave();\n            });\n\n            document.getElementById('restart-button').addEventListener('click', () => resetGame());\n\n            function resetGame() {\n                // Reset game state\n                gameState.currentLevel = window.gameConfig.wave.startingLevel;\n                gameState.zombiesKilled = 0;\n                gameState.score = 0;\n                gameState.gameOver = false;\n                gameState.waveInProgress = false;\n                gameState.timeBetweenWaves = 0;\n                gameState.storeOpen = false;\n                gameState.killStreak = 0;\n                gameState.killStreakTimeout = null;\n\n                // Reset player state\n                player.position.set(0, 0, 0);\n                player.velocity.set(0, 0, 0);\n                player.yVelocity = 0;\n                player.isGrounded = true;\n                player.rotation = { y: 0, pitch: 0 };\n                player.health = window.gameConfig.player.startingHealth;\n                player.maxHealth = window.gameConfig.player.startingHealth;\n                player.currentWeaponId = 'pistol';\n                player.inventory = ['pistol'];\n                player.weapons = JSON.parse(JSON.stringify(window.gameConfig.weapons));\n                player.isReloading = false;\n                player.reloadTime = 0;\n                player.shootCooldown = 0;\n                player.recoil = 0;\n                player.screenShake = 0;\n                player.healthLevel = 0;\n\n                // Initialize ammo\n                Object.keys(player.weapons).forEach(id => {\n                    player.weapons[id].currentAmmo = player.weapons[id].maxAmmo;\n                });\n\n                // Clear zombies and pickups from scene\n                zombies.forEach(z => z.destroy());\n                zombies.length = 0;\n                \n                particles.forEach(p => scene.remove(p));\n                particles.length = 0;\n\n                if (player.model) scene.remove(player.model);\n                player.model = createCharacterModel(false, 1);\n                scene.add(player.model);\n                updatePlayerModelWeapon();\n\n                // Hide UI screens\n                document.getElementById('death-screen').classList.remove('active');\n                document.getElementById('store-screen').classList.remove('active');\n\n                // Rebuild town and start new wave\n                buildTown();\n                spawnWave();\n                updateUI();\n                gameState.waveInProgress = true;\n                lastTime = performance.now(); // Reset lastTime for gameLoop\n                requestAnimationFrame(gameLoop);\n            }\n        }\n    </script>\n</body>\n</html>", "game_config": "{\n  \"player\": {\n    \"startingHealth\": 200,\n    \"speedMultiplier\": 1.5,\n    \"startingAmmo\": 50,\n    \"damageMultiplier\": 1\n  },\n  \"zombie\": {\n    \"baseHealth\": 50,\n    \"speed\": 1.8,\n    \"damage\": 10,\n    \"detectionRange\": 45\n  },\n  \"wave\": {\n    \"startingWave\": 1,\n    \"zombiesMultiplier\": 1,\n    \"timeBetweenWaves\": 10,\n    \"startingLevel\": 1\n  },\n  \"pickup\": {\n    \"healthAmount\": 50,\n    \"ammoAmount\": 50,\n    \"respawnEnabled\": true\n  },\n  \"pickupSpawns\": [\n    {\n      \"z\": -25,\n      \"type\": \"health\",\n      \"x\": -25\n    },\n    {\n      \"z\": -25,\n      \"type\": \"health\",\n      \"x\": 25\n    },\n    {\n      \"x\": -25,\n      \"z\": 25,\n      \"type\": \"health\"\n    },\n    {\n      \"z\": 25,\n      \"type\": \"ammo\",\n      \"x\": 25\n    },\n    {\n      \"z\": 10,\n      \"type\": \"weapon_ak47\",\n      \"x\": 10\n    },\n    {\n      \"x\": -10,\n      \"type\": \"weapon_shotgun\",\n      \"z\": -10\n    }\n  ],\n  \"zombieSpawns\": [\n    {\n      \"x\": -50,\n      \"z\": -50\n    },\n    {\n      \"x\": 0,\n      \"z\": -50\n    },\n    {\n      \"x\": 50,\n      \"z\": -50\n    },\n    {\n      \"x\": 50,\n      \"z\": 0\n    },\n    {\n      \"x\": 50,\n      \"z\": 50\n    },\n    {\n      \"x\": 0,\n      \"z\": 50\n    },\n    {\n      \"x\": -50,\n      \"z\": 50\n    },\n    {\n      \"x\": -50,\n      \"z\": 0\n    }\n  ],\n  \"weapons\": {\n    \"ak47\": {\n      \"fireRate\": 0.1,\n      \"texture\": \"ak47_texture\",\n      \"name\": \"AK47\",\n      \"reloadTime\": 2,\n      \"ammo\": 40,\n      \"damage\": 40,\n      \"maxAmmo\": 40,\n      \"size\": [\n        0.1,\n        0.2,\n        0.8\n      ]\n    },\n    \"shotgun\": {\n      \"texture\": \"shotgun_texture\",\n      \"fireRate\": 0.8,\n      \"name\": \"Shotgun\",\n      \"ammo\": 8,\n      \"reloadTime\": 3,\n      \"size\": [\n        0.12,\n        0.2,\n        0.7\n      ],\n      \"maxAmmo\": 8,\n      \"damage\": 120\n    },\n    \"pistol\": {\n      \"texture\": \"pistol_weapon\",\n      \"fireRate\": 0.3,\n      \"name\": \"Pistol\",\n      \"ammo\": 15,\n      \"reloadTime\": 1.5,\n      \"size\": [\n        0.08,\n        0.15,\n        0.4\n      ],\n      \"maxAmmo\": 15,\n      \"damage\": 50\n    },\n    \"sniper\": {\n      \"damage\": 300,\n      \"size\": [\n        0.08,\n        0.2,\n        1.2\n      ],\n      \"maxAmmo\": 10,\n      \"currentAmmo\": 10,\n      \"reloadTime\": 3.5,\n      \"fireRate\": 1.5,\n      \"ammo\": 10,\n      \"name\": \"Sniper Rifle\",\n      \"texture\": \"sniper_texture\"\n    },\n    \"smg\": {\n      \"maxAmmo\": 30,\n      \"currentAmmo\": 30,\n      \"damage\": 25,\n      \"size\": [\n        0.07,\n        0.2,\n        0.5\n      ],\n      \"fireRate\": 0.06,\n      \"reloadTime\": 1.8,\n      \"name\": \"SMG\",\n      \"ammo\": 30,\n      \"texture\": \"smg_texture\"\n    },\n    \"lmg\": {\n      \"texture\": \"lmg_texture\",\n      \"ammo\": 100,\n      \"name\": \"LMG\",\n      \"reloadTime\": 5,\n      \"fireRate\": 0.12,\n      \"size\": [\n        0.12,\n        0.25,\n        0.9\n      ],\n      \"damage\": 45,\n      \"currentAmmo\": 100,\n      \"maxAmmo\": 100\n    }\n  },\n  \"giantZombie\": {\n    \"damage\": 40,\n    \"baseHealth\": 500,\n    \"speed\": 1.2\n  }\n}", "game_design_document": "## Core Game Loop and Mechanics\n\n**Undead Frontier** is a 3D third-person zombie survival game where players must survive increasingly difficult waves of zombies in an abandoned town environment.\n\n### Moment-to-Moment Gameplay\n- Player navigates a 3D town environment from third-person perspective\n- Zombies spawn at the edges of the map and pursue the player when they get close\n- Player shoots zombies with their pistol to eliminate threats\n- Between waves, player scavenges for health packs, ammo boxes, and weapon upgrades scattered around the town\n- Each wave increases in difficulty with more zombies and faster spawn rates\n\n### Progression and Engagement\n- **Wave System**: Game progresses through numbered waves (Wave 1, Wave 2, etc.)\n- **Difficulty Scaling**: Each wave spawns more zombies with slightly increased health\n- **Resource Management**: Players must balance combat with scavenging for limited supplies\n- **Score Tracking**: Points awarded for zombie kills, with combo bonuses for consecutive kills\n- **Survival Challenge**: How many waves can you survive before being overwhelmed?\n\n### Win/Lose Conditions\n- **Lose Condition**: Player health reaches zero (game over, display final wave reached and score)\n- **Win Condition**: Survive as many waves as possible (endless survival mode)\n- **Wave Completion**: Eliminate all zombies in current wave to trigger next wave after brief countdown\n\n### Compelling Gameplay\n- Tension between aggressive combat and defensive scavenging\n- Resource scarcity creates meaningful decisions\n- Increasing difficulty provides escalating challenge\n- Third-person perspective creates immersive atmosphere\n- Simple but satisfying shooting mechanics\n\n## Starting Scope\n\nThe first iteration will focus on delivering a polished, playable core experience:\n\n**MUST HAVE (First Iteration):**\n- Single town environment with 5-8 buildings and open streets\n- Third-person camera following player\n- Basic player movement (WASD + mouse look with pointer lock)\n- Pistol shooting mechanics with recoil and muzzle flash\n- 2-3 zombie types with basic AI (idle, chase, attack states)\n- Wave system (waves 1-5 fully balanced)\n- Health system with visual feedback\n- Ammo system with reload mechanics\n- 3-4 pickup types: health pack, ammo box, better pistol\n- Simple UI: health bar, ammo counter, wave indicator, score\n- Death screen with restart option\n- Sound effects for shooting, zombie groans, pickups\n\n**FUTURE ITERATIONS:**\n- Additional weapon types (shotgun, rifle)\n- More zombie varieties (fast zombies, tank zombies)\n- Larger map with more buildings to explore\n- Barricade/defense building mechanics\n- Power-ups (temporary damage boost, speed boost)\n- More complex AI behaviors\n- Environmental hazards\n- Multiplayer co-op mode\n\n## Game Perspective\n\n**3D Third-Person** perspective with:\n- Camera positioned behind and slightly above the player character\n- Camera follows player smoothly with slight lag for natural feel\n- Mouse controls camera rotation (horizontal and vertical)\n- Camera maintains distance from player (approximately 5-7 units back, 2-3 units up)\n- Camera collision detection to prevent clipping through buildings\n- Crosshair in center of screen for aiming\n- Player character visible on screen (helps with spatial awareness and immersion)\n\n## Game World and Level Design\n\n### Town Layout\nThe abandoned town is a compact, navigable area approximately 100x100 units:\n\n**Buildings (5-8 structures):**\n- 2-3 two-story residential houses (can enter ground floor only)\n- 1 small convenience store\n- 1 gas station with pumps\n- 1-2 small sheds/garages\n- Buildings are solid obstacles (no interior navigation in first iteration)\n- Buildings provide cover and line-of-sight breaks\n\n**Streets and Open Areas:**\n- Main street running through center (north-south)\n- Cross street creating intersection in middle\n- Open plaza area near center (dangerous but good for kiting zombies)\n- Narrow alleyways between some buildings\n- Cracked pavement, abandoned vehicles as obstacles\n\n**Boundaries:**\n- Invisible walls at map edges\n- Visual boundary markers (fences, debris piles)\n- Zombies spawn just outside visible boundary\n\n**Pickup Spawn Locations:**\n- Health packs: Inside building doorways, behind cover\n- Ammo boxes: Near vehicles, in alleyways\n- Weapon upgrades: In center plaza (high risk/reward)\n- 8-10 total pickup spawn points that respawn between waves\n\n**Lighting and Atmosphere:**\n- Overcast/dusk lighting (moody but visible)\n- Streetlights provide pools of light\n- Shadows from buildings create tension\n\n### Zombie Spawn System\n- Zombies spawn at 8 spawn points around map perimeter\n- Spawn points rotate to prevent predictability\n- Wave 1: 5 zombies\n- Wave 2: 8 zombies\n- Wave 3: 12 zombies\n- Wave 4: 16 zombies\n- Wave 5+: 20+ zombies with increasing spawn rate\n\n## Edit Mode Design\n\nPlayers can customize these aspects in Edit mode:\n\n### Editable Properties\n\n**Player Settings:**\n- Starting health (50-200, default: 100)\n- Movement speed multiplier (0.5x-2x, default: 1x)\n- Starting ammo (10-50, default: 30)\n- Damage multiplier (0.5x-3x, default: 1x)\n\n**Zombie Settings:**\n- Zombie health per wave (20-100, default: 50 base + 10 per wave)\n- Zombie speed (0.5x-2x, default: 1x)\n- Zombie damage (5-30, default: 10)\n- Detection range (5-30 units, default: 15)\n\n**Wave Settings:**\n- Starting wave number (1-10, default: 1)\n- Zombies per wave multiplier (0.5x-3x, default: 1x)\n- Time between waves (5-30 seconds, default: 10)\n\n**Pickup Settings:**\n- Health pack heal amount (20-100, default: 50)\n- Ammo box amount (10-50, default: 20)\n- Pickup respawn (on/off, default: on)\n\n**Map Settings:**\n- Time of day (dawn/day/dusk/night)\n- Fog density (0-100%, default: 30%)\n\n### Constraints\n- All multipliers capped to prevent game-breaking values\n- Minimum values ensure game remains playable\n- Wave numbers limited to prevent performance issues\n- Cannot edit during active gameplay (only between games)\n\n## Control Design\n\n**Undead Frontier** uses standard 3D third-person action controls optimized for both mobile and desktop.\n\n### Desktop Controls (Keyboard + Mouse)\n\n**Movement:**\n- WASD or Arrow Keys: Move forward/left/backward/right\n- Movement is relative to camera direction\n- Shift: Sprint (1.5x speed, drains stamina bar slowly)\n\n**Camera/Aiming:**\n- Mouse Movement: Rotate camera (with pointer lock)\n- Camera controls both looking and aiming direction\n- Crosshair always in center of screen\n\n**Combat:**\n- Left Mouse Button: Shoot pistol\n- R Key: Reload weapon\n- Mouse Wheel or Number Keys 1-3: Switch weapons (when multiple acquired)\n\n**Interaction:**\n- E Key: Pick up items (health, ammo, weapons)\n- Auto-pickup when walking over items (with visual/audio feedback)\n\n**UI:**\n- ESC: Pause menu / Release pointer lock\n- Tab: Show wave/score details\n\n### Mobile Controls (Touch)\n\n**Movement:**\n- Left Virtual Joystick: Move character\n  - Appears where left side of screen is touched (bottom 60% of screen, left 40% of width)\n  - Analog movement (\u22121 to +1 on both axes)\n  - Base follows finger if moved beyond max radius\n  - Dead zone: 10% of max radius\n\n**Camera/Aiming:**\n- Right Virtual Joystick: Rotate camera and aim\n  - Appears where right side of screen is touched (bottom 60% of screen, right 60% of width)\n  - Analog rotation (\u22121 to +1 on both axes)\n  - Base follows finger if moved beyond max radius\n  - Dead zone: 10% of max radius\n\n**Combat:**\n- Auto-fire when aiming at zombie within crosshair proximity\n- Dedicated Shoot Button (bottom-right corner): Manual fire\n- Reload Button (right side, middle): Reload weapon\n- Weapon Switch Buttons (top-right): Cycle through weapons\n\n**Interaction:**\n- Auto-pickup when walking near items\n- Pickup Button (bottom-center): Manual pickup if auto-pickup disabled\n\n**UI:**\n- Pause Button (top-left corner): Pause menu\n- Health/Ammo/Wave info always visible (top of screen)\n\n### Control Implementation Details\n\n**Pointer Lock (Desktop):**\n- Automatically request pointer lock when game starts\n- Release pointer lock when pause menu opens\n- Show cursor in menus, hide during gameplay\n- Prevent default browser behavior for used keys (WASD, Space, etc.)\n\n**Touch ID Tracking (Mobile):**\n- Track each touch by unique ID\n- Left joystick claims first touch in left zone\n- Right joystick claims first touch in right zone\n- Buttons claim touches in their hit areas\n- Prevent touch interference between controls\n\n**Input State Management:**\n- Clear all input states on window blur\n- Normalize key codes to lowercase\n- Handle both keydown/keyup and touchstart/touchend\n- Smooth camera rotation with interpolation\n\n**Movement Relative to Camera:**\n- Forward movement goes in camera's forward direction\n- Strafing moves perpendicular to camera direction\n- Character model rotates to face movement direction\n- Smooth rotation interpolation for natural feel\n\n**Aiming System:**\n- Raycast from camera center to determine aim point\n- Crosshair indicates where bullets will hit\n- Bullet spread for pistol (slight randomness)\n- Visual feedback for hits (blood particle, zombie flinch)\n\n### Accessibility Considerations\n- Large touch targets (minimum 96x96 pixels)\n- Visual feedback for all button presses\n- Haptic feedback on mobile (if supported)\n- Adjustable sensitivity in settings (future iteration)\n\n## Art Style and Visual Theme\n\n### Overall Art Direction\n**Low-poly 3D with stylized realism** - A balance between performance and atmosphere:\n- Clean geometric shapes with moderate detail\n- Muted, desaturated color palette emphasizing grays, browns, and sickly greens\n- Subtle texturing to suggest decay and abandonment\n- Consistent lighting creating moody atmosphere without being too dark\n\n### Color Palette and Mood\n**Post-apocalyptic desolation:**\n- **Primary Colors**: Grays (concrete, asphalt), browns (rust, dirt), muted greens (decay)\n- **Accent Colors**: Red (blood, health packs), yellow (ammo boxes), orange (muzzle flash)\n- **Sky**: Overcast gray-blue with hints of orange (perpetual dusk)\n- **Mood**: Tense, lonely, oppressive but not horror-dark\n\n### Asset Sizes and Scale\n**Relative to game units:**\n- Player character: ~2 units tall, ~0.8 units wide\n- Zombies: ~2 units tall (slightly hunched), ~0.8 units wide\n- Buildings: 8-15 units tall, varying widths\n- Vehicles: 4-5 units long, 2 units wide\n- Pickups: 0.5-1 unit cubes/spheres\n- Weapons: Scaled to player hands (~0.3-0.5 units)\n\n### Viewing Angles and Consistency\n**Third-person perspective requirements:**\n- All character models designed to be viewed from behind and slightly above\n- Buildings designed with exterior detail (interiors not visible in first iteration)\n- Ground textures visible from 45-degree angle\n- Props and obstacles designed to read clearly from player's camera height\n\n### Visual Elements by Category\n\n**Characters (Generated Assets):**\n- Player character: Survivor in practical clothing (jacket, jeans, boots)\n- Zombie types: Decayed humans with torn clothing, visible wounds\n- Consistent humanoid proportions across all characters\n- Simple rigging for basic animations\n\n**Environment (Mix of Generated + Code):**\n- Buildings: Generated texture assets applied to code-rendered geometry\n- Ground: Generated tileable asphalt/dirt textures\n- Props (vehicles, debris): Generated 3D assets\n- Sky: Code-rendered gradient or generated skybox texture\n\n**UI Elements (Code-Rendered):**\n- Health bar: Red gradient bar with dark border\n- Ammo counter: White text with drop shadow\n- Crosshair: Simple white cross with slight transparency\n- Wave indicator: Bold white text with dark background\n\n**Effects (Mix):**\n- Muzzle flash: Generated sprite with additive blending\n- Blood splatter: Generated particle textures\n- Dust/debris: Simple code-rendered particles\n- Lighting: Code-rendered point lights and shadows\n\n### Technical Rendering Approach\n- Use Three.js for 3D rendering\n- Simple diffuse materials with basic lighting\n- Minimal post-processing (maybe slight vignette)\n- Optimize for mobile performance (low poly counts, simple shaders)\n- Fog for depth and atmosphere (also hides distant pop-in)\n\n### Consistency Guidelines\nAll generated assets must maintain:\n- Same level of geometric detail (low-poly aesthetic)\n- Consistent color saturation (muted, desaturated)\n- Similar texture resolution and style\n- Matching lighting assumptions (overcast outdoor lighting)\n- Unified art direction (stylized realism, not cartoon or hyper-realistic)", "persistent_storage_config": null, "game_metadata": {"game_id": "01KTM47RN9X87P8NHQRCCP49RF", "title": "BIG  GREAD  HUNTER 2", "description": "Survive waves of zombies in an abandoned town. Scavenge ammo and health while fighting off the undead horde!", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_6ad4be6f-95d0-4aca-8aab-bcc119d99066.webp", "created_at": 1780939523, "updated_at": 1782660108850.5312, "art_style": null}, "task_plan": null}