{"asset_map": {"vixera_idle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/13c1e07d-5e58-463a-bde2-15fab71896c0.webp", "type": "image", "prompt": "Generate a game-ready side-view sprite of the robot character Vixera in an idle standing pose, facing right, with both feet on the ground. Maintain the anime-inspired art style with the black and white color scheme, fox-like ears, fluffy white tail, black outfit, and gray mechanical hands and boots. The character should be approximately 80-100 pixels tall. Use clean lines, smooth shading, and ensure the mechanical elements (hands, boots) have visible tech details. Transparent background. Side-view perspective suitable for a 2D platformer. Match the style and details of user uploaded image 01KDEBD429YM9C93FDXTH5B2DR.", "description": "A game-ready side-view sprite of the robot character Vixera in an idle standing pose.", "subtype": "character", "aspect_ratio": [522, 724], "compression_suggestion": "medium", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "vixera_running": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/0920f0ad-f8ed-4e41-88a8-32628f91ff98.webp", "type": "image", "prompt": "Generate a game-ready side-view sprite of Vixera in a dynamic running pose, facing right. The character should be mid-stride with one leg forward and one back, arms pumping in running motion, tail flowing behind. Match the exact anime-inspired art style, black and white colors, and proportions of the `vixera_idle` asset. The character should convey speed and energy while maintaining the same 80-100 pixel height. Transparent background. Ensure mechanical hands and boots show motion blur or dynamic positioning. Side-view perspective suitable for a 2D platformer.", "description": "A game-ready side-view sprite of Vixera in a dynamic running pose.", "subtype": "character", "aspect_ratio": [777, 650], "compression_suggestion": "medium", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "vixera_jumping": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/7ae18e41-d9b8-419e-b0e7-82531d1570fa.webp", "type": "image", "prompt": "Generate a game-ready side-view sprite of Vixera in a jumping pose, facing right. The character should be airborne with legs tucked slightly, arms positioned for balance, tail flowing upward. Match the exact anime-inspired art style, black and white colors, and proportions of the `vixera_idle` asset. Maintain the same 80-100 pixel height. Transparent background. The pose should clearly communicate upward motion and weightlessness. Side-view perspective suitable for a 2D platformer.", "description": "A game-ready side-view sprite of Vixera in a jumping pose.", "subtype": "character", "aspect_ratio": [526, 513], "compression_suggestion": "medium", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "vixera_shooting": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/c8051e0b-6a60-45e5-b4e7-af4fc1e62bc9.webp", "type": "image", "prompt": "Generate a game-ready side-view sprite of Vixera in a shooting pose, facing right. The character should be in a dynamic action stance with one arm extended forward (the mechanical hand) as if firing the Fox Buster weapon. The other arm can be positioned for balance. Legs in a stable, slightly crouched combat stance. Match the exact anime-inspired art style, black and white colors, and proportions of the `vixera_idle` asset. Maintain the same 80-100 pixel height. Transparent background. The extended arm should clearly indicate the shooting direction. Side-view perspective suitable for a 2D platformer.", "description": "A game-ready side-view sprite of Vixera in a shooting pose.", "subtype": "character", "aspect_ratio": [686, 645], "compression_suggestion": "medium", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "enemy_robot_idle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/fde4686f-a7ca-44c3-9d64-e236d8795a1b.webp", "type": "image", "prompt": "Generate a game-ready side-view sprite of a hostile enemy robot in an idle/patrol pose, facing right. The robot should have a menacing, mechanical appearance with an anime-inspired art style matching the `vixera_idle` character assets. Color scheme: primarily dark gray and red with glowing orange warning lights or eyes. The design should be clearly robotic (angular, mechanical parts, panel lines) and distinctly different from Vixera - more aggressive and industrial looking. Approximately 70-90 pixels tall. The robot should have a humanoid or bipedal form that can patrol platforms. Transparent background. Side-view perspective for a 2D platformer.", "description": "A game-ready side-view sprite of a hostile enemy robot in an idle/patrol pose.", "subtype": "character", "aspect_ratio": [591, 913], "compression_suggestion": "medium", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "enemy_robot_shooting": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/97e91b24-358d-45da-b9d6-9c600fb8b635.webp", "type": "image", "prompt": "Generate a game-ready side-view sprite of the same enemy robot from `enemy_robot_idle`, now in a shooting pose facing right. The robot should have one arm or weapon system extended forward, clearly indicating it's firing. Maintain the exact same anime-inspired art style, colors (dark gray, red, orange glow), and proportions as `enemy_robot_idle`. Maintain the same 70-90 pixels tall. The pose should be aggressive and clearly communicate hostile intent. Transparent background. Side-view perspective for a 2D platformer.", "description": "A game-ready side-view sprite of the enemy robot in a shooting pose.", "subtype": "character", "aspect_ratio": [1298, 844], "compression_suggestion": "medium", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "platform_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/34404486-029e-4e36-b5b8-f88e5b4892ac.webp", "type": "image", "prompt": "Generate a tileable futuristic metal platform texture for a cyberpunk-themed platformer game. The texture should be 200 pixels wide by 40 pixels tall, designed to be tiled horizontally. Style: industrial sci-fi metal with panel lines, rivets, and tech details. Color scheme: dark metallic gray/blue with glowing cyan edge lights along the top edge. The texture should have a slight gradient (darker at bottom, lighter at top) and include subtle wear/scratches. The top surface should clearly indicate it's a solid walkable platform. Seamless edges for tiling. Matches the anime-inspired cyberpunk aesthetic of the `vixera_idle` character. Side-view perspective.", "description": "A tileable futuristic metal platform texture for a cyberpunk-themed platformer game.", "subtype": "ground", "aspect_ratio": [1536, 1024], "compression_suggestion": "medium", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "background_cityscape": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/63341a46-9c72-48cb-8709-5b2a877b318d.webp", "type": "image", "prompt": "Generate a 9:16 tall image of a cyberpunk city skyline background for a futuristic platformer game, sized 720x1280 pixels (portrait orientation). The scene should depict a nighttime urban landscape with towering skyscrapers, neon signs, and glowing windows. Color palette: deep blues and purples for the night sky, with vibrant cyan, magenta, and yellow neon accents on buildings. Art style: anime-inspired with clean lines and atmospheric depth. The composition should work as a background layer with darker/hazier buildings in the distance and slightly more detailed structures in the mid-ground. Include atmospheric effects like light beams, floating particles, or fog. The overall mood should be futuristic and slightly dystopian but visually exciting. Leave the foreground area (bottom third) relatively clear/dark so gameplay elements stand out. Side-view perspective.", "description": "A cyberpunk city skyline background for a futuristic platformer game.", "subtype": "background", "aspect_ratio": [1024, 1536], "compression_suggestion": "original", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "health_heart_full": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/d335b4bb-dfd9-4bbf-ab77-065892c0dbea.webp", "type": "image", "prompt": "Generate a UI icon of a full health heart for a cyberpunk-themed game. The heart should be 48x48 pixels, with a futuristic/tech aesthetic. Style: glowing cyan heart with a digital/holographic appearance, possibly with circuit-like patterns or tech details. The heart should be clearly filled/solid to indicate full health. Transparent background. Should match the cyan accent color used in the game's UI and neon elements. Clean, polished, game-ready UI asset.", "description": "A UI icon of a full health heart for a cyberpunk-themed game.", "subtype": "ui", "aspect_ratio": [773, 687], "compression_suggestion": "high", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "health_heart_empty": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/80196c06-5fb8-4c72-ac17-94ee202d78a3.webp", "type": "image", "prompt": "Generate a UI icon of an empty health heart for a cyberpunk-themed game. The heart should be 48x48 pixels, matching the exact same style and proportions as `health_heart_full`, but appearing depleted/empty. Style: dark gray outline of a heart with minimal glow, possibly with dimmed circuit patterns. Should clearly communicate 'no health remaining' in this slot. Transparent background. Should visually pair with `health_heart_full` as part of the same UI set. Clean, polished, game-ready UI asset.", "description": "A UI icon of an empty health heart for a cyberpunk-themed game.", "subtype": "ui", "aspect_ratio": [901, 811], "compression_suggestion": "high", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "energy_core": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/b4abff9f-204b-41dc-8674-02b330250459.webp", "type": "image", "prompt": "Generate a collectible energy core pickup item for a cyberpunk platformer game. The item should be approximately 32x32 pixels. Design: a glowing orb or crystal with a bright cyan/blue energy glow, possibly with floating particles or an aura effect. Style: futuristic and valuable-looking, clearly indicating it's a collectible item. The glow should make it stand out against the game environment. Transparent background. Should match the cyberpunk aesthetic and cyan accent colors of the game. Clean, polished, game-ready object.", "description": "A collectible energy core pickup item for a cyberpunk platformer game.", "subtype": "object", "aspect_ratio": [488, 435], "compression_suggestion": "medium", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}, "shoot_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/6c42652f-83d6-40e4-9f2e-e961087c4a7a.mp3", "type": "audio", "prompt": "Short, punchy 'pew' or 'zap' style laser energy blast, bright, energetic, sci-fi, satisfying and responsive.", "description": "Futuristic energy weapon shooting sound effect for the Fox Buster weapon.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": "short", "loop": false, "animation": null}, "enemy_shoot_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/e793d029-c380-49e7-9c00-20626890ac98.mp3", "type": "audio", "prompt": "Futuristic energy weapon shot, slightly deeper and more aggressive than player's shot, sci-fi, threatening.", "description": "Futuristic energy weapon shooting sound effect for enemy robots.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": "short", "loop": false, "animation": null}, "enemy_destroy_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/7c473647-ed7c-45e6-b331-ba11c581e705.mp3", "type": "audio", "prompt": "Robotic explosion and destruction sound, combining mechanical breaking, crunching, metal shattering, sparks, and a small impactful explosion with bass.", "description": "Robotic explosion/destruction sound effect for when enemy robots are defeated.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": "medium", "loop": false, "animation": null}, "player_hit_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/a1d5104f-106c-468d-abf1-0398eb448848.mp3", "type": "audio", "prompt": "Sharp, painful impact sound, metallic clang with a brief electronic glitch or warning beep, alarming and urgent.", "description": "Damage/hit sound effect for when the player takes damage.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": "short", "loop": false, "animation": null}, "jump_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/8eea177a-2118-447d-a82c-19ccd15fff58.mp3", "type": "audio", "prompt": "Light, energetic 'whoosh' or mechanical boost sound for a robotic jump, quick, responsive, satisfying.", "description": "Jump sound effect for the player character.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": "short", "loop": false, "animation": null}, "collect_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/ecac0c28-71a1-4312-b112-539f21d14d05.mp3", "type": "audio", "prompt": "Bright, pleasant, rewarding digital chime or sparkle sound with a futuristic twist, positive and satisfying, slightly musical.", "description": "Collectible pickup sound effect for when the player collects energy cores.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": "short", "loop": false, "animation": null}, "background_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/502fe417-2f88-4ed9-b654-6e0c5c218337.mp3", "type": "audio", "prompt": "Energetic cyberpunk electronic/synthwave music, driving beats, futuristic synth melodies, upbeat tempo (130-150 BPM), intense, exciting, futuristic. Designed to loop seamlessly.", "description": "Energetic, cyberpunk-themed background music track for an action platformer game.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": "long", "loop": true, "animation": null}, "game_over_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/660fc6ba-8409-4ba0-997c-bbaab1a7f347.mp3", "type": "audio", "prompt": "Game over sound effect communicating defeat, descending tone, power-down effect, glitchy electronic elements, sense of finality.", "description": "Game over sound effect for when the player loses all health.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": "medium", "loop": false, "animation": null}, "energy_can": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/140e271d-7abc-4d76-85c0-831dc6be3910.webp", "type": "image", "prompt": "Generate a small, glowing cylindrical energy can, roughly 32x32 pixels, with a bright green color and a futuristic, metallic appearance with glowing effects. It should have a transparent background and be clearly distinguishable from an 'energy core' asset, designed as a game-ready cyberpunk platformer item.", "description": "A collectible energy can/health pack pickup item for a cyberpunk platformer game.", "subtype": "object", "aspect_ratio": [468, 725], "compression_suggestion": "high", "view": null, "outline": null, "image_asset_type": null, "audio_type": null, "edited": null, "edit_instructions": null, "edit_reason": null, "original_url": null, "error": null, "duration_secs": null, "duration_hint": null, "loop": null, "animation": null}}, "workspace": null, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Battledroids</title>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n    <link href=\"https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@400;600;700&display=swap\" rel=\"stylesheet\">\n    \n    <style>\n        /* ============================================\n           CSS RESET & BASE STYLES\n           ============================================ */\n        *, *::before, *::after {\n            box-sizing: border-box;\n            margin: 0;\n            padding: 0;\n        }\n        \n        body, html { \n            margin: 0; \n            padding: 0; \n            width: 100%; \n            height: 100%; \n            overflow: hidden;\n            background: #000;\n            font-family: 'Orbitron', 'Rajdhani', 'Arial', sans-serif;\n            -webkit-font-smoothing: antialiased;\n            -moz-osx-font-smoothing: grayscale;\n        }\n        \n        /* ============================================\n           CSS CUSTOM PROPERTIES\n           ============================================ */\n        :root {\n            /* Colors */\n            --color-primary: #00ffff;\n            --color-primary-glow: rgba(0, 255, 255, 0.5);\n            --color-accent: #ff6600;\n            --color-accent-glow: rgba(255, 102, 0, 0.5);\n            --color-danger: #ff0000;\n            --color-danger-glow: rgba(255, 0, 0, 0.5);\n            --color-hud-bg: rgba(0, 10, 20, 0.85);\n            --color-panel-border: rgba(0, 255, 255, 0.3);\n            --color-text-primary: #ffffff;\n            --color-text-secondary: rgba(255, 255, 255, 0.7);\n            \n            /* Spacing (8px grid) */\n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n            \n            /* Animation Timings */\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --transition-slow: 500ms;\n            \n            /* Easing Functions */\n            --ease-out: cubic-bezier(0.0, 0, 0.2, 1);\n            --ease-bounce: cubic-bezier(0.68, -0.6, 0.32, 1.6);\n            \n            /* Shadows & Effects */\n            --shadow-glow-cyan: 0 0 20px var(--color-primary-glow);\n            --shadow-glow-orange: 0 0 20px var(--color-accent-glow);\n            --shadow-glow-red: 0 0 20px var(--color-danger-glow);\n            --text-shadow-cyan: 0 0 10px var(--color-primary-glow);\n            --text-shadow-white: 0 2px 4px rgba(0, 0, 0, 0.8);\n        }\n        \n        /* ============================================\n           GAME CANVAS\n           ============================================ */\n        #gameCanvas {\n            display: block;\n            width: 100%;\n            height: 100%;\n            touch-action: none;\n            -ms-touch-action: none;\n        }\n        \n        /* ============================================\n           HUD OVERLAY CONTAINER\n           ============================================ */\n        .hud-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 720px;\n            height: 1280px;\n            pointer-events: none;\n            z-index: 10;\n        }\n        \n        .hud-overlay > * {\n            pointer-events: auto;\n        }\n        \n        /* ============================================\n           TOP HUD PANEL\n           ============================================ */\n        .top-hud {\n            position: absolute;\n            top: var(--space-sm);\n            left: var(--space-sm);\n            right: var(--space-sm);\n            background: var(--color-hud-bg);\n            border: 2px solid var(--color-panel-border);\n            border-radius: 8px;\n            padding: var(--space-sm);\n            box-shadow: var(--shadow-glow-cyan);\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            gap: var(--space-sm);\n        }\n        \n        /* Health Display */\n        .health-container {\n            display: flex;\n            align-items: center;\n            gap: var(--space-xs);\n        }\n        \n        .health-icon {\n            width: 40px;\n            height: 40px;\n            transition: transform var(--transition-fast) var(--ease-out);\n        }\n        \n        .health-container.low-health .health-icon {\n            animation: pulse-danger 1s ease-in-out infinite;\n        }\n        \n        /* Score & Wave Display */\n        .stats-container {\n            display: flex;\n            flex-direction: column;\n            align-items: flex-end;\n            gap: 4px;\n        }\n        \n        .score-display {\n            font-family: 'Orbitron', sans-serif;\n            font-weight: 700;\n            font-size: 28px;\n            color: var(--color-primary);\n            text-shadow: var(--text-shadow-cyan);\n            letter-spacing: 1px;\n        }\n        \n        .wave-display {\n            font-family: 'Rajdhani', sans-serif;\n            font-weight: 600;\n            font-size: 18px;\n            color: var(--color-text-secondary);\n            text-shadow: var(--text-shadow-white);\n            letter-spacing: 0.5px;\n        }\n        \n        /* ============================================\n           TOUCH CONTROLS (MOBILE)\n           ============================================ */\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        /* Joystick Zone Indicator */\n        .joystick-zone {\n            position: absolute;\n            bottom: var(--space-md);\n            left: var(--space-md);\n            width: 140px;\n            height: 140px;\n            border: 2px dashed var(--color-panel-border);\n            border-radius: 50%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            opacity: 0.3;\n            transition: opacity var(--transition-normal) var(--ease-out);\n        }\n        \n        .joystick-zone.active {\n            opacity: 0.6;\n            border-style: solid;\n        }\n        \n        .joystick-icon {\n            font-family: 'Rajdhani', sans-serif;\n            font-size: 14px;\n            color: var(--color-text-secondary);\n            text-align: center;\n            line-height: 1.2;\n        }\n        \n        /* Fire Button */\n        .fire-button {\n            position: absolute;\n            bottom: var(--space-lg);\n            right: var(--space-lg);\n            width: 96px;\n            height: 96px;\n            background: var(--color-primary);\n            border: 3px solid var(--color-text-primary);\n            border-radius: 50%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            box-shadow: var(--shadow-glow-cyan);\n            transition: all var(--transition-fast) var(--ease-out);\n            pointer-events: auto;\n            cursor: pointer;\n            user-select: none;\n        }\n        \n        .fire-button:active,\n        .fire-button.pressed {\n            background: var(--color-accent);\n            box-shadow: var(--shadow-glow-orange);\n            transform: scale(0.95);\n        }\n        \n        .fire-button-text {\n            font-family: 'Orbitron', sans-serif;\n            font-weight: 900;\n            font-size: 18px;\n            color: #000;\n            text-transform: uppercase;\n            letter-spacing: 1px;\n        }\n        \n        /* Desktop Control Hints */\n        .desktop-controls {\n            position: absolute;\n            bottom: var(--space-sm);\n            left: 50%;\n            transform: translateX(-50%);\n            background: var(--color-hud-bg);\n            border: 1px solid var(--color-panel-border);\n            border-radius: 8px;\n            padding: var(--space-xs) var(--space-sm);\n            font-family: 'Rajdhani', sans-serif;\n            font-size: 14px;\n            color: var(--color-text-secondary);\n            text-align: center;\n            opacity: 0.7;\n        }\n        \n        .desktop-controls kbd {\n            display: inline-block;\n            padding: 2px 6px;\n            background: rgba(255, 255, 255, 0.1);\n            border: 1px solid var(--color-panel-border);\n            border-radius: 4px;\n            font-family: 'Orbitron', monospace;\n            font-size: 12px;\n            margin: 0 2px;\n        }\n        \n        /* ============================================\n           GAME OVER MODAL\n           ============================================ */\n        .game-over-modal {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.9);\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            opacity: 0;\n            pointer-events: none;\n            transition: opacity var(--transition-slow) var(--ease-out);\n        }\n        \n        .game-over-modal.visible {\n            opacity: 1;\n            pointer-events: auto;\n        }\n        \n        .game-over-content {\n            background: var(--color-hud-bg);\n            border: 3px solid var(--color-danger);\n            border-radius: 16px;\n            padding: var(--space-xl);\n            max-width: 560px;\n            width: calc(100% - var(--space-lg) * 2);\n            box-shadow: var(--shadow-glow-red);\n            text-align: center;\n            transform: scale(0.9);\n            transition: transform var(--transition-slow) var(--ease-bounce);\n        }\n        \n        .game-over-modal.visible .game-over-content {\n            transform: scale(1);\n        }\n        \n        .game-over-title {\n            font-family: 'Orbitron', sans-serif;\n            font-weight: 900;\n            font-size: 56px;\n            color: var(--color-danger);\n            text-shadow: var(--shadow-glow-red);\n            margin-bottom: var(--space-md);\n            letter-spacing: 2px;\n        }\n        \n        .game-over-score {\n            font-family: 'Orbitron', sans-serif;\n            font-weight: 700;\n            font-size: 36px;\n            color: var(--color-primary);\n            text-shadow: var(--text-shadow-cyan);\n            margin-bottom: var(--space-sm);\n        }\n        \n        .game-over-label {\n            font-family: 'Rajdhani', sans-serif;\n            font-size: 18px;\n            color: var(--color-text-secondary);\n            margin-bottom: var(--space-lg);\n        }\n        \n        .restart-button {\n            background: var(--color-primary);\n            border: 3px solid var(--color-text-primary);\n            border-radius: 8px;\n            padding: var(--space-sm) var(--space-xl);\n            font-family: 'Orbitron', sans-serif;\n            font-weight: 700;\n            font-size: 24px;\n            color: #000;\n            text-transform: uppercase;\n            letter-spacing: 2px;\n            cursor: pointer;\n            box-shadow: var(--shadow-glow-cyan);\n            transition: all var(--transition-fast) var(--ease-out);\n            min-width: 200px;\n            min-height: 64px;\n        }\n        \n        .restart-button:hover {\n            background: var(--color-text-primary);\n            transform: translateY(-2px);\n            box-shadow: 0 0 30px var(--color-primary-glow);\n        }\n        \n        .restart-button:active {\n            transform: translateY(0);\n        }\n        \n        /* ============================================\n           ANIMATIONS\n           ============================================ */\n        @keyframes pulse-danger {\n            0%, 100% {\n                transform: scale(1);\n                filter: drop-shadow(0 0 5px var(--color-danger-glow));\n            }\n            50% {\n                transform: scale(1.1);\n                filter: drop-shadow(0 0 15px var(--color-danger));\n            }\n        }\n        \n        @keyframes fadeIn {\n            from {\n                opacity: 0;\n                transform: translateY(-10px);\n            }\n            to {\n                opacity: 1;\n                transform: translateY(0);\n            }\n        }\n        \n        /* ============================================\n           REDUCED MOTION SUPPORT\n           ============================================ */\n        @media (prefers-reduced-motion: reduce) {\n            *,\n            *::before,\n            *::after {\n                animation-duration: 0.01ms !important;\n                animation-iteration-count: 1 !important;\n                transition-duration: 0.01ms !important;\n            }\n            \n            .health-container.low-health .health-icon {\n                animation: none;\n            }\n        }\n        \n        /* ============================================\n           VISIBILITY UTILITIES\n           ============================================ */\n        .hidden {\n            display: none !important;\n        }\n        \n        .mobile-only {\n            display: block;\n        }\n        \n        .desktop-only {\n            display: none;\n        }\n        \n        @media (hover: hover) and (pointer: fine) {\n            .mobile-only {\n                display: none;\n            }\n            .desktop-only {\n                display: block;\n            }\n        }\n    </style>\n</head>\n<body>\n    <canvas id=\"gameCanvas\" width=\"720\" height=\"1280\"></canvas>\n    \n    <!-- HUD Overlay -->\n    <div class=\"hud-overlay\">\n        <!-- Top HUD Panel -->\n        <div class=\"top-hud\">\n            <div class=\"health-container\" id=\"healthContainer\">\n                <!-- Health hearts will be inserted here -->\n            </div>\n            <div class=\"stats-container\">\n                <div class=\"score-display\" id=\"scoreDisplay\">0</div>\n                <div class=\"wave-display\" id=\"waveDisplay\">WAVE 1</div>\n            </div>\n        </div>\n        \n        <!-- Touch Controls (Mobile Only) -->\n        <div class=\"touch-controls mobile-only\">\n            <div class=\"joystick-zone\" id=\"joystickZone\">\n                <div class=\"joystick-icon\">MOVE</div>\n            </div>\n            <div class=\"fire-button\" id=\"fireButton\">\n                <span class=\"fire-button-text\">FIRE</span>\n            </div>\n        </div>\n        \n        <!-- Desktop Controls Hint -->\n        <div class=\"desktop-controls desktop-only\">\n            <kbd>WASD</kbd> or <kbd>Arrows</kbd> Move \u2022 <kbd>Space</kbd> Jump \u2022 <kbd>Click</kbd> or <kbd>X</kbd> Shoot\n        </div>\n        \n        <!-- Game Over Modal -->\n        <div class=\"game-over-modal\" id=\"gameOverModal\">\n            <div class=\"game-over-content\">\n                <div class=\"game-over-title\">GAME OVER</div>\n                <div class=\"game-over-score\" id=\"finalScore\">0</div>\n                <div class=\"game-over-label\">FINAL SCORE</div>\n                <button class=\"restart-button\" id=\"restartButton\">RESTART</button>\n            </div>\n        </div>\n    </div>\n    \n    <script>\n        /* ==================================================\n         * GAME OVERVIEW: Battledroids - A fast-paced platformer where Vixera, a robot hero,\n         * defends the city from waves of evil robots. Jump between platforms, shoot enemies,\n         * collect energy cores, and survive as long as possible!\n         * \n         * GAME STATE SHAPE: window.gameConfig = {\n         *   player: { maxHealth, moveSpeed, jumpPower, shootCooldown },\n         *   enemy: { moveSpeed, shootInterval, health, spawnInterval },\n         *   difficulty: { increaseEnabled, waveInterval },\n         *   visual: { particlesEnabled, screenShakeEnabled }\n         * }\n         * ==================================================\n         */\n\n        const canvas = document.getElementById('gameCanvas');\n        const ctx = canvas.getContext('2d');\n        \n        // UI Elements\n        const healthContainer = document.getElementById('healthContainer');\n        const scoreDisplay = document.getElementById('scoreDisplay');\n        const waveDisplay = document.getElementById('waveDisplay');\n        const gameOverModal = document.getElementById('gameOverModal');\n        const finalScoreDisplay = document.getElementById('finalScore');\n        const restartButton = document.getElementById('restartButton');\n        const fireButton = document.getElementById('fireButton');\n        const joystickZone = document.getElementById('joystickZone');\n        \n        // Asset cache\n        const assetCache = {};\n        const audioBuffers = {};\n        let audioContext;\n        \n        // Game constants\n        const GAME_WIDTH = 720;\n        const GAME_HEIGHT = 1280;\n        const GRAVITY = 0.8;\n        const PLAYER_WIDTH = 60;\n        const PLAYER_HEIGHT = 80;\n        const ENEMY_WIDTH = 60;\n        const ENEMY_HEIGHT = 70;\n        const PROJECTILE_SPEED = 8;\n        const INVINCIBILITY_TIME = 1000;\n        \n        // Platform definitions\n        const PLATFORMS = [\n            { x: 0, y: 1150, width: 720, height: 40 }, // Ground\n            { x: 50, y: 950, width: 280, height: 40 }, // Low left\n            { x: 390, y: 950, width: 280, height: 40 }, // Low right\n            { x: 150, y: 700, width: 200, height: 40 }, // Mid left\n            { x: 370, y: 700, width: 200, height: 40 }, // Mid right\n            { x: 260, y: 450, width: 200, height: 40 }  // High center\n        ];\n        \n        // Game state\n        let gameMode = 'play';\n        let gameState = 'playing'; // playing, gameOver, victory\n        let player = null;\n        let enemies = [];\n        let boss = null;\n        let playerProjectiles = [];\n        let enemyProjectiles = [];\n        let energyCores = [];\n        let particles = [];\n        let score = 0;\n        let health = 3;\n        let lastShootTime = 0;\n        let lastEnemySpawnTime = 0;\n        let gameStartTime = 0;\n        let currentWave = 1;\n        let lastWaveTime = 0;\n        let invincibleUntil = 0;\n        let screenShake = { x: 0, y: 0, intensity: 0 };\n        let bossSpawned = false;\n        let enemiesDefeatedThisWave = 0;\n        \n        // Input state\n        let keys = {};\n        let mousePos = { x: GAME_WIDTH / 2, y: GAME_HEIGHT / 2 };\n        let mouseDown = false;\n        let isTouchDevice = false;\n        let joystick = {\n            active: false,\n            baseX: 0,\n            baseY: 0,\n            knobX: 0,\n            knobY: 0,\n            touchId: null,\n            maxRadius: 60,\n            deadZone: 15\n        };\n        let shootButton = {\n            x: GAME_WIDTH - 100,\n            y: GAME_HEIGHT - 100,\n            radius: 50,\n            pressed: false,\n            touchId: null\n        };\n        \n        // Animation frame tracking\n        let lastFrameTime = Date.now();\n        let animationFrameId = null;\n        \n        /* ==================================================\n         * UI UPDATE FUNCTIONS\n         * ==================================================\n         */\n        \n        function updateHealthDisplay() {\n            healthContainer.innerHTML = '';\n            const maxHealth = window.gameConfig.player.maxHealth;\n            \n            for (let i = 0; i < maxHealth; i++) {\n                const heartImg = document.createElement('img');\n                heartImg.className = 'health-icon';\n                \n                const heartAsset = assetCache[i < health ? 'health_heart_full' : 'health_heart_empty'];\n                if (heartAsset) {\n                    heartImg.src = heartAsset.info.url;\n                }\n                \n                healthContainer.appendChild(heartImg);\n            }\n            \n            // Add low health warning\n            if (health <= 1 && health > 0) {\n                healthContainer.classList.add('low-health');\n            } else {\n                healthContainer.classList.remove('low-health');\n            }\n        }\n        \n        function updateScoreDisplay() {\n            scoreDisplay.textContent = score.toString();\n        }\n        \n        function updateWaveDisplay() {\n            waveDisplay.textContent = `WAVE ${currentWave}`;\n        }\n        \n        function showGameOver() {\n            finalScoreDisplay.textContent = score.toString();\n            gameOverModal.classList.add('visible');\n        }\n        \n        function showVictory() {\n            finalScoreDisplay.textContent = score.toString();\n            gameOverModal.querySelector('.game-over-title').textContent = 'VICTORY!';\n            gameOverModal.querySelector('.game-over-title').style.color = 'var(--color-primary)';\n            gameOverModal.classList.add('visible');\n        }\n        \n        function hideGameOver() {\n            gameOverModal.classList.remove('visible');\n            gameOverModal.querySelector('.game-over-title').textContent = 'GAME OVER';\n            gameOverModal.querySelector('.game-over-title').style.color = 'var(--color-danger)';\n        }\n        \n        function updateJoystickZone() {\n            if (joystick.active) {\n                joystickZone.classList.add('active');\n            } else {\n                joystickZone.classList.remove('active');\n            }\n        }\n        \n        function updateFireButton() {\n            if (shootButton.pressed) {\n                fireButton.classList.add('pressed');\n            } else {\n                fireButton.classList.remove('pressed');\n            }\n        }\n        \n        /* ==================================================\n         * ASSET LOADING\n         * ==================================================\n         */\n        \n        // Preload assets\n        function preloadAssets(callback) {\n            const assetIds = [\n                'vixera_idle', 'vixera_running', 'vixera_jumping', 'vixera_shooting',\n                'enemy_robot_idle', 'enemy_robot_shooting',\n                'platform_texture', 'background_cityscape',\n                'health_heart_full', 'health_heart_empty', 'energy_core', 'energy_can'\n            ];\n            \n            const audioIds = [\n                'shoot_sfx', 'enemy_shoot_sfx', 'enemy_destroy_sfx',\n                'player_hit_sfx', 'jump_sfx', 'collect_sfx',\n                'background_music', 'game_over_sfx'\n            ];\n            \n            let loadedCount = 0;\n            const totalAssets = assetIds.length + audioIds.length;\n            \n            // Initialize audio context\n            audioContext = new (window.AudioContext || window.webkitAudioContext)();\n            \n            // Load images\n            assetIds.forEach(id => {\n                const assetInfo = lib.getAsset(id);\n                if (assetInfo) {\n                    const img = new Image();\n                    img.onload = () => {\n                        assetCache[id] = { img, info: assetInfo };\n                        loadedCount++;\n                        if (loadedCount === totalAssets) callback();\n                    };\n                    img.onerror = () => {\n                        lib.log('Failed to load image: ' + id);\n                        loadedCount++;\n                        if (loadedCount === totalAssets) callback();\n                    };\n                    img.src = assetInfo.url;\n                } else {\n                    loadedCount++;\n                    if (loadedCount === totalAssets) callback();\n                }\n            });\n            \n            // Load audio\n            audioIds.forEach(id => {\n                const assetInfo = lib.getAsset(id);\n                if (assetInfo) {\n                    fetch(assetInfo.url)\n                        .then(response => response.arrayBuffer())\n                        .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))\n                        .then(audioBuffer => {\n                            audioBuffers[id] = audioBuffer;\n                            loadedCount++;\n                            if (loadedCount === totalAssets) callback();\n                        })\n                        .catch(err => {\n                            lib.log('Failed to load audio: ' + id);\n                            loadedCount++;\n                            if (loadedCount === totalAssets) callback();\n                        });\n                } else {\n                    loadedCount++;\n                    if (loadedCount === totalAssets) callback();\n                }\n            });\n        }\n        \n        // Play sound\n        function playSound(id, loop = false) {\n            if (!audioBuffers[id]) return;\n            \n            const source = audioContext.createBufferSource();\n            source.buffer = audioBuffers[id];\n            source.loop = loop;\n            \n            const gainNode = audioContext.createGain();\n            gainNode.gain.value = id === 'background_music' ? 0.3 : 0.5;\n            \n            source.connect(gainNode);\n            gainNode.connect(audioContext.destination);\n            source.start(0);\n            \n            return source;\n        }\n        \n        /* ==================================================\n         * GAME INITIALIZATION\n         * ==================================================\n         */\n        \n        // Initialize player\n        function initPlayer() {\n            player = {\n                x: GAME_WIDTH / 2 - PLAYER_WIDTH / 2,\n                y: PLATFORMS[0].y - PLAYER_HEIGHT - 10,\n                width: PLAYER_WIDTH,\n                height: PLAYER_HEIGHT,\n                vx: 0,\n                vy: 0,\n                onGround: false,\n                facingRight: true,\n                state: 'idle', // idle, running, jumping, shooting\n                shootingTimer: 0\n            };\n        }\n        \n        // Spawn enemy\n        function spawnEnemy() {\n            // Choose random platform (not ground)\n            const platformIndex = Math.floor(Math.random() * (PLATFORMS.length - 1)) + 1;\n            const platform = PLATFORMS[platformIndex];\n            \n            const enemy = {\n                x: platform.x + Math.random() * (platform.width - ENEMY_WIDTH),\n                y: platform.y - ENEMY_HEIGHT,\n                width: ENEMY_WIDTH,\n                height: ENEMY_HEIGHT,\n                vx: window.gameConfig.enemy.moveSpeed * (Math.random() > 0.5 ? 1 : -1),\n                platformIndex: platformIndex,\n                health: window.gameConfig.enemy.health,\n                maxHealth: window.gameConfig.enemy.health,\n                lastShootTime: Date.now(),\n                state: 'idle', // idle, shooting\n                shootingTimer: 0\n            };\n            \n            enemies.push(enemy);\n        }\n        \n        // Spawn boss\n        function spawnBoss() {\n            const platform = PLATFORMS[PLATFORMS.length - 1]; // High center platform\n            \n            boss = {\n                x: platform.x + (platform.width - 100) / 2,\n                y: platform.y - 150,\n                width: 100,\n                height: 120,\n                vx: window.gameConfig.boss.moveSpeed * (Math.random() > 0.5 ? 1 : -1),\n                platformIndex: PLATFORMS.length - 1,\n                health: window.gameConfig.boss.health,\n                maxHealth: window.gameConfig.boss.health,\n                lastShootTime: Date.now(),\n                state: 'idle', // idle, shooting\n                shootingTimer: 0,\n                isBoss: true\n            };\n            \n            bossSpawned = true;\n        }\n        \n        // Spawn energy core\n        function spawnEnergyCore(x, y, isHealing = false) {\n            energyCores.push({\n                x: x,\n                y: y,\n                width: 32,\n                height: 32,\n                vy: -3,\n                lifetime: 5000,\n                spawnTime: Date.now(),\n                isHealing: isHealing\n            });\n        }\n        \n        // Create particle effect\n        function createParticles(x, y, count, color) {\n            if (!window.gameConfig.visual.particlesEnabled) return;\n            \n            for (let i = 0; i < count; i++) {\n                const angle = Math.random() * Math.PI * 2;\n                const speed = 2 + Math.random() * 4;\n                particles.push({\n                    x: x,\n                    y: y,\n                    vx: Math.cos(angle) * speed,\n                    vy: Math.sin(angle) * speed,\n                    size: 2 + Math.random() * 4,\n                    color: color,\n                    life: 1,\n                    decay: 0.02 + Math.random() * 0.02\n                });\n            }\n        }\n        \n        // Screen shake\n        function shakeScreen(intensity) {\n            if (!window.gameConfig.visual.screenShakeEnabled) return;\n            screenShake.intensity = intensity;\n        }\n        \n        // Update screen shake\n        function updateScreenShake() {\n            if (screenShake.intensity > 0) {\n                screenShake.x = (Math.random() - 0.5) * screenShake.intensity;\n                screenShake.y = (Math.random() - 0.5) * screenShake.intensity;\n                screenShake.intensity *= 0.9;\n                if (screenShake.intensity < 0.1) {\n                    screenShake.intensity = 0;\n                    screenShake.x = 0;\n                    screenShake.y = 0;\n                }\n            }\n        }\n        \n        /* ==================================================\n         * COLLISION DETECTION\n         * ==================================================\n         */\n        \n        // Check collision with platforms\n        function checkPlatformCollision(obj) {\n            obj.onGround = false;\n            \n            for (let platform of PLATFORMS) {\n                // Check if falling onto platform\n                if (obj.vy >= 0 &&\n                    obj.y + obj.height <= platform.y + obj.vy &&\n                    obj.y + obj.height + obj.vy >= platform.y &&\n                    obj.x + obj.width > platform.x &&\n                    obj.x < platform.x + platform.width) {\n                    \n                    obj.y = platform.y - obj.height;\n                    obj.vy = 0;\n                    obj.onGround = true;\n                    return platform;\n                }\n            }\n            return null;\n        }\n        \n        // Check if point is on platform\n        function isOnPlatform(x, y) {\n            for (let platform of PLATFORMS) {\n                if (x >= platform.x && x <= platform.x + platform.width &&\n                    Math.abs(y - platform.y) < 5) {\n                    return true;\n                }\n            }\n            return false;\n        }\n        \n        // Rectangle collision\n        function rectCollision(a, b) {\n            return a.x < b.x + b.width &&\n                   a.x + a.width > b.x &&\n                   a.y < b.y + b.height &&\n                   a.y + a.height > b.y;\n        }\n        \n        // Point in circle collision\n        function pointInCircle(px, py, cx, cy, radius) {\n            const dx = px - cx;\n            const dy = py - cy;\n            return dx * dx + dy * dy <= radius * radius;\n        }\n        \n        /* ==================================================\n         * INPUT HANDLING\n         * ==================================================\n         */\n        \n        // Get input vector from joystick or keyboard\n        function getInputVector() {\n            let x = 0;\n            let y = 0;\n            \n            if (isTouchDevice && joystick.active) {\n                const dx = joystick.knobX - joystick.baseX;\n                const dy = joystick.knobY - joystick.baseY;\n                const distance = Math.sqrt(dx * dx + dy * dy);\n                \n                if (distance > joystick.deadZone) {\n                    x = dx / joystick.maxRadius;\n                    y = dy / joystick.maxRadius;\n                    // Clamp to -1, 1\n                    x = Math.max(-1, Math.min(1, x));\n                    y = Math.max(-1, Math.min(1, y));\n                }\n            } else {\n                if (keys['a'] || keys['arrowleft']) x -= 1;\n                if (keys['d'] || keys['arrowright']) x += 1;\n                if (keys['w'] || keys['arrowup']) y -= 1;\n            }\n            \n            return { x, y };\n        }\n        \n        // Check if should jump\n        function shouldJump() {\n            if (isTouchDevice) {\n                const input = getInputVector();\n                return input.y < -0.6; // Upward threshold\n            } else {\n                return keys[' '] || keys['w'] || keys['arrowup'];\n            }\n        }\n        \n        // Check if should shoot\n        function shouldShoot() {\n            if (isTouchDevice) {\n                return shootButton.pressed;\n            } else {\n                return mouseDown || keys['x'];\n            }\n        }\n        \n        // Get aim direction\n        function getAimDirection() {\n            if (isTouchDevice) {\n                // Auto-aim at nearest enemy\n                let nearestEnemy = null;\n                let nearestDist = Infinity;\n                \n                for (let enemy of enemies) {\n                    const dx = (enemy.x + enemy.width / 2) - (player.x + player.width / 2);\n                    const dy = (enemy.y + enemy.height / 2) - (player.y + player.height / 2);\n                    const dist = Math.sqrt(dx * dx + dy * dy);\n                    \n                    if (dist < nearestDist && dist < 500) {\n                        nearestDist = dist;\n                        nearestEnemy = enemy;\n                    }\n                }\n                \n                if (nearestEnemy) {\n                    const dx = (nearestEnemy.x + nearestEnemy.width / 2) - (player.x + player.width / 2);\n                    const dy = (nearestEnemy.y + nearestEnemy.height / 2) - (player.y + player.height / 2);\n                    const dist = Math.sqrt(dx * dx + dy * dy);\n                    return { x: dx / dist, y: dy / dist };\n                } else {\n                    // Shoot in facing direction\n                    return { x: player.facingRight ? 1 : -1, y: 0 };\n                }\n            } else {\n                // Aim toward mouse\n                const dx = mousePos.x - (player.x + player.width / 2);\n                const dy = mousePos.y - (player.y + player.height / 2);\n                const dist = Math.sqrt(dx * dx + dy * dy);\n                return { x: dx / dist, y: dy / dist };\n            }\n        }\n        \n        /* ==================================================\n         * GAME UPDATE LOGIC\n         * ==================================================\n         */\n        \n        // Update player\n        function updatePlayer(deltaTime) {\n            const input = getInputVector();\n            \n            // Horizontal movement\n            player.vx = input.x * window.gameConfig.player.moveSpeed;\n            \n            // Update facing direction\n            if (input.x > 0) player.facingRight = true;\n            if (input.x < 0) player.facingRight = false;\n            \n            // Jumping\n            if (shouldJump() && player.onGround) {\n                player.vy = -window.gameConfig.player.jumpPower;\n                player.onGround = false;\n                playSound('jump_sfx');\n            }\n            \n            // Apply gravity\n            if (!player.onGround) {\n                player.vy += GRAVITY;\n            }\n            \n            // Update position\n            player.x += player.vx;\n            player.y += player.vy;\n            \n            // Clamp to screen bounds\n            if (player.x < 0) player.x = 0;\n            if (player.x + player.width > GAME_WIDTH) player.x = GAME_WIDTH - player.width;\n            \n            // Check platform collision\n            checkPlatformCollision(player);\n            \n            // Prevent falling through bottom\n            if (player.y + player.height > GAME_HEIGHT) {\n                player.y = GAME_HEIGHT - player.height;\n                player.vy = 0;\n                player.onGround = true;\n            }\n            \n            // Shooting\n            const now = Date.now();\n            if (shouldShoot() && now - lastShootTime > window.gameConfig.player.shootCooldown) {\n                const dir = getAimDirection();\n                const startX = player.x + player.width / 2;\n                const startY = player.y + player.height / 2;\n                \n                playerProjectiles.push({\n                    x: startX,\n                    y: startY,\n                    vx: dir.x * PROJECTILE_SPEED,\n                    vy: dir.y * PROJECTILE_SPEED,\n                    radius: 5\n                });\n                \n                lastShootTime = now;\n                player.state = 'shooting';\n                player.shootingTimer = 200;\n                playSound('shoot_sfx');\n            }\n            \n            // Update state\n            if (player.shootingTimer > 0) {\n                player.shootingTimer -= deltaTime * 1000;\n                player.state = 'shooting';\n            } else if (!player.onGround) {\n                player.state = 'jumping';\n            } else if (Math.abs(player.vx) > 0.1) {\n                player.state = 'running';\n            } else {\n                player.state = 'idle';\n            }\n        }\n        \n        // Update enemies\n        function updateEnemies(deltaTime) {\n            const now = Date.now();\n            \n            for (let i = enemies.length - 1; i >= 0; i--) {\n                const enemy = enemies[i];\n                \n                // Patrol movement\n                enemy.x += enemy.vx;\n                \n                // Check platform bounds\n                const platform = PLATFORMS[enemy.platformIndex];\n                if (enemy.x < platform.x || enemy.x + enemy.width > platform.x + platform.width) {\n                    enemy.vx *= -1;\n                    enemy.x = Math.max(platform.x, Math.min(platform.x + platform.width - enemy.width, enemy.x));\n                }\n                \n                // Shooting\n                const shootInterval = window.gameConfig.enemy.shootInterval;\n                if (now - enemy.lastShootTime > shootInterval) {\n                    const dx = (player.x + player.width / 2) - (enemy.x + enemy.width / 2);\n                    const dy = (player.y + player.height / 2) - (enemy.y + enemy.height / 2);\n                    const dist = Math.sqrt(dx * dx + dy * dy);\n                    \n                    enemyProjectiles.push({\n                        x: enemy.x + enemy.width / 2,\n                        y: enemy.y + enemy.height / 2,\n                        vx: (dx / dist) * PROJECTILE_SPEED,\n                        vy: (dy / dist) * PROJECTILE_SPEED,\n                        radius: 5\n                    });\n                    \n                    enemy.lastShootTime = now;\n                    enemy.state = 'shooting';\n                    enemy.shootingTimer = 200;\n                    playSound('enemy_shoot_sfx');\n                }\n                \n                // Update state\n                if (enemy.shootingTimer > 0) {\n                    enemy.shootingTimer -= deltaTime * 1000;\n                } else {\n                    enemy.state = 'idle';\n                }\n            }\n            \n            // Update boss\n            if (boss) {\n                // Patrol movement\n                boss.x += boss.vx;\n                \n                // Check platform bounds\n                const platform = PLATFORMS[boss.platformIndex];\n                if (boss.x < platform.x || boss.x + boss.width > platform.x + platform.width) {\n                    boss.vx *= -1;\n                    boss.x = Math.max(platform.x, Math.min(platform.x + platform.width - boss.width, boss.x));\n                }\n                \n                // Shooting - more aggressive\n                const shootInterval = window.gameConfig.boss.shootInterval / 1.5;\n                if (now - boss.lastShootTime > shootInterval) {\n                    const dx = (player.x + player.width / 2) - (boss.x + boss.width / 2);\n                    const dy = (player.y + player.height / 2) - (boss.y + boss.height / 2);\n                    const dist = Math.sqrt(dx * dx + dy * dy);\n                    \n                    // Boss shoots multiple projectiles\n                    for (let angle = -0.3; angle <= 0.3; angle += 0.3) {\n                        const rotatedDx = dx * Math.cos(angle) - dy * Math.sin(angle);\n                        const rotatedDy = dx * Math.sin(angle) + dy * Math.cos(angle);\n                        const rotatedDist = Math.sqrt(rotatedDx * rotatedDx + rotatedDy * rotatedDy);\n                        \n                        enemyProjectiles.push({\n                            x: boss.x + boss.width / 2,\n                            y: boss.y + boss.height / 2,\n                            vx: (rotatedDx / rotatedDist) * PROJECTILE_SPEED,\n                            vy: (rotatedDy / rotatedDist) * PROJECTILE_SPEED,\n                            radius: 6\n                        });\n                    }\n                    \n                    boss.lastShootTime = now;\n                    boss.state = 'shooting';\n                    boss.shootingTimer = 300;\n                    playSound('enemy_shoot_sfx');\n                }\n                \n                // Update state\n                if (boss.shootingTimer > 0) {\n                    boss.shootingTimer -= deltaTime * 1000;\n                } else {\n                    boss.state = 'idle';\n                }\n            }\n        }\n        \n        // Update projectiles\n        function updateProjectiles() {\n            // Player projectiles\n            for (let i = playerProjectiles.length - 1; i >= 0; i--) {\n                const proj = playerProjectiles[i];\n                proj.x += proj.vx;\n                proj.y += proj.vy;\n                \n                // Remove if off screen\n                if (proj.x < -10 || proj.x > GAME_WIDTH + 10 ||\n                    proj.y < -10 || proj.y > GAME_HEIGHT + 10) {\n                    playerProjectiles.splice(i, 1);\n                    continue;\n                }\n                \n                // Check collision with boss\n                if (boss && proj.x >= boss.x && proj.x <= boss.x + boss.width &&\n                    proj.y >= boss.y && proj.y <= boss.y + boss.height) {\n                    \n                    boss.health--;\n                    playerProjectiles.splice(i, 1);\n                    createParticles(proj.x, proj.y, 12, '#ff6600');\n                    shakeScreen(15);\n                    \n                    if (boss.health <= 0) {\n                        // Boss defeated - VICTORY!\n                        gameState = 'victory';\n                        showVictory();\n                        playSound('enemy_destroy_sfx');\n                    }\n                    continue;\n                }\n                \n                // Check collision with enemies\n                for (let j = enemies.length - 1; j >= 0; j--) {\n                    const enemy = enemies[j];\n                    if (proj.x >= enemy.x && proj.x <= enemy.x + enemy.width &&\n                        proj.y >= enemy.y && proj.y <= enemy.y + enemy.height) {\n                        \n                        enemy.health--;\n                        playerProjectiles.splice(i, 1);\n                        createParticles(proj.x, proj.y, 8, '#ff6600');\n                        \n                        if (enemy.health <= 0) {\n                            // Enemy destroyed\n                            score += 100;\n                            updateScoreDisplay();\n                            enemiesDefeatedThisWave++;\n                            \n                            // 40% chance to drop healing item\n                            const isHealing = Math.random() < 0.4;\n                            spawnEnergyCore(enemy.x + enemy.width / 2, enemy.y + enemy.height / 2, isHealing);\n                            enemies.splice(j, 1);\n                            createParticles(enemy.x + enemy.width / 2, enemy.y + enemy.height / 2, 20, '#ff0000');\n                            shakeScreen(10);\n                            playSound('enemy_destroy_sfx');\n                            \n                            // Check if wave should progress (30 enemies defeated)\n                            if (enemiesDefeatedThisWave >= 30) {\n                                currentWave++;\n                                enemiesDefeatedThisWave = 0;\n                                updateWaveDisplay();\n                                lib.log('Wave ' + currentWave);\n                                \n                                // Spawn boss at wave 5\n                                if (currentWave === 5 && !bossSpawned) {\n                                    spawnBoss();\n                                    lib.log('BOSS SPAWNED!');\n                                }\n                            }\n                        }\n                        break;\n                    }\n                }\n            }\n            \n            // Enemy projectiles\n            const now = Date.now();\n            for (let i = enemyProjectiles.length - 1; i >= 0; i--) {\n                const proj = enemyProjectiles[i];\n                proj.x += proj.vx;\n                proj.y += proj.vy;\n                \n                // Remove if off screen\n                if (proj.x < -10 || proj.x > GAME_WIDTH + 10 ||\n                    proj.y < -10 || proj.y > GAME_HEIGHT + 10) {\n                    enemyProjectiles.splice(i, 1);\n                    continue;\n                }\n                \n                // Check collision with player\n                if (now > invincibleUntil &&\n                    proj.x >= player.x && proj.x <= player.x + player.width &&\n                    proj.y >= player.y && proj.y <= player.y + player.height) {\n                    \n                    health--;\n                    updateHealthDisplay();\n                    invincibleUntil = now + INVINCIBILITY_TIME;\n                    enemyProjectiles.splice(i, 1);\n                    createParticles(proj.x, proj.y, 10, '#00ffff');\n                    shakeScreen(8);\n                    playSound('player_hit_sfx');\n                    \n                    if (health <= 0) {\n                        gameState = 'gameOver';\n                        showGameOver();\n                        playSound('game_over_sfx');\n                    }\n                }\n            }\n        }\n        \n        // Update energy cores\n        function updateEnergyCores() {\n            const now = Date.now();\n            \n            for (let i = energyCores.length - 1; i >= 0; i--) {\n                const core = energyCores[i];\n                \n                // Apply gravity\n                core.vy += GRAVITY * 0.5;\n                core.y += core.vy;\n                \n                // Check platform collision\n                const platform = checkPlatformCollision(core);\n                if (platform) {\n                    core.vy *= -0.5; // Bounce\n                }\n                \n                // Check collection\n                if (rectCollision(player, core)) {\n                    if (core.isHealing) {\n                        // Heal player\n                        if (health < window.gameConfig.player.maxHealth) {\n                            health++;\n                            updateHealthDisplay();\n                            createParticles(core.x + core.width / 2, core.y + core.height / 2, 12, '#00ff00');\n                            playSound('collect_sfx');\n                        }\n                    } else {\n                        // Add points\n                        score += 50;\n                        updateScoreDisplay();\n                        createParticles(core.x + core.width / 2, core.y + core.height / 2, 12, '#00ffff');\n                        playSound('collect_sfx');\n                    }\n                    energyCores.splice(i, 1);\n                    continue;\n                }\n                \n                // Remove if expired\n                if (now - core.spawnTime > core.lifetime) {\n                    energyCores.splice(i, 1);\n                }\n            }\n        }\n        \n        // Update particles\n        function updateParticles() {\n            for (let i = particles.length - 1; i >= 0; i--) {\n                const p = particles[i];\n                p.x += p.vx;\n                p.y += p.vy;\n                p.vy += 0.2; // Gravity\n                p.life -= p.decay;\n                \n                if (p.life <= 0) {\n                    particles.splice(i, 1);\n                }\n            }\n        }\n        \n        // Update wave system (legacy - now handled by enemy defeat counter)\n        function updateWaveSystem() {\n            // Wave progression is now handled in updateProjectiles() when enemies are defeated\n            // This function is kept for compatibility but does nothing\n        }\n        \n        // Spawn enemies\n        function spawnEnemies() {\n            const now = Date.now();\n            const spawnInterval = window.gameConfig.enemy.spawnInterval / Math.sqrt(currentWave);\n            \n            if (now - lastEnemySpawnTime > spawnInterval) {\n                spawnEnemy();\n                lastEnemySpawnTime = now;\n            }\n        }\n        \n        // Check player-enemy collision\n        function checkPlayerEnemyCollision() {\n            const now = Date.now();\n            if (now <= invincibleUntil) return;\n            \n            for (let enemy of enemies) {\n                if (rectCollision(player, enemy)) {\n                    health--;\n                    updateHealthDisplay();\n                    invincibleUntil = now + INVINCIBILITY_TIME;\n                    createParticles(player.x + player.width / 2, player.y + player.height / 2, 10, '#00ffff');\n                    shakeScreen(8);\n                    playSound('player_hit_sfx');\n                    \n                    if (health <= 0) {\n                        gameState = 'gameOver';\n                        showGameOver();\n                        playSound('game_over_sfx');\n                    }\n                    break;\n                }\n            }\n        }\n        \n        /* ==================================================\n         * RENDERING\n         * ==================================================\n         */\n        \n        // Draw image with proper aspect ratio\n        function drawImage(id, x, y, width, height, flipH = false) {\n            const asset = assetCache[id];\n            if (!asset) return;\n            \n            ctx.save();\n            \n            if (flipH) {\n                ctx.translate(x + width, y);\n                ctx.scale(-1, 1);\n                ctx.drawImage(asset.img, 0, 0, width, height);\n            } else {\n                ctx.drawImage(asset.img, x, y, width, height);\n            }\n            \n            ctx.restore();\n        }\n        \n        // Draw background\n        function drawBackground() {\n            drawImage('background_cityscape', 0, 0, GAME_WIDTH, GAME_HEIGHT);\n        }\n        \n        // Draw platforms\n        function drawPlatforms() {\n            const platformTexture = assetCache['platform_texture'];\n            if (!platformTexture) return;\n            \n            for (let platform of PLATFORMS) {\n                // Tile the texture\n                const textureWidth = 200;\n                const numTiles = Math.ceil(platform.width / textureWidth);\n                \n                for (let i = 0; i < numTiles; i++) {\n                    const x = platform.x + i * textureWidth;\n                    const w = Math.min(textureWidth, platform.x + platform.width - x);\n                    ctx.drawImage(platformTexture.img, 0, 0, w, 40, x, platform.y, w, platform.height);\n                }\n            }\n        }\n        \n        // Draw player\n        function drawPlayer() {\n            let spriteId = 'vixera_idle';\n            \n            if (player.state === 'shooting') {\n                spriteId = 'vixera_shooting';\n            } else if (player.state === 'jumping') {\n                spriteId = 'vixera_jumping';\n            } else if (player.state === 'running') {\n                spriteId = 'vixera_running';\n            }\n            \n            // Flash if invincible\n            const now = Date.now();\n            if (now < invincibleUntil && Math.floor(now / 100) % 2 === 0) {\n                ctx.globalAlpha = 0.5;\n            }\n            \n            drawImage(spriteId, player.x, player.y, player.width, player.height, !player.facingRight);\n            \n            ctx.globalAlpha = 1;\n        }\n        \n        // Draw enemies\n        function drawEnemies() {\n            for (let enemy of enemies) {\n                const spriteId = enemy.state === 'shooting' ? 'enemy_robot_shooting' : 'enemy_robot_idle';\n                const flipH = enemy.vx < 0;\n                drawImage(spriteId, enemy.x, enemy.y, enemy.width, enemy.height, flipH);\n                \n                // Health bar\n                const barWidth = enemy.width;\n                const barHeight = 4;\n                const barX = enemy.x;\n                const barY = enemy.y - 10;\n                \n                ctx.fillStyle = '#333';\n                ctx.fillRect(barX, barY, barWidth, barHeight);\n                \n                ctx.fillStyle = '#ff0000';\n                ctx.fillRect(barX, barY, barWidth * (enemy.health / enemy.maxHealth), barHeight);\n            }\n        }\n        \n        // Draw boss\n        function drawBoss() {\n            if (!boss) return;\n            \n            const spriteId = boss.state === 'shooting' ? 'enemy_robot_shooting' : 'enemy_robot_idle';\n            const flipH = boss.vx < 0;\n            \n            // Draw boss with glow effect\n            ctx.shadowBlur = 20;\n            ctx.shadowColor = '#ff6600';\n            drawImage(spriteId, boss.x, boss.y, boss.width, boss.height, flipH);\n            ctx.shadowBlur = 0;\n            \n            // Large health bar\n            const barWidth = boss.width;\n            const barHeight = 8;\n            const barX = boss.x;\n            const barY = boss.y - 20;\n            \n            // Background\n            ctx.fillStyle = '#333';\n            ctx.fillRect(barX, barY, barWidth, barHeight);\n            \n            // Health\n            ctx.fillStyle = '#ff0000';\n            ctx.fillRect(barX, barY, barWidth * (boss.health / boss.maxHealth), barHeight);\n            \n            // Border\n            ctx.strokeStyle = '#ff6600';\n            ctx.lineWidth = 2;\n            ctx.strokeRect(barX, barY, barWidth, barHeight);\n            \n            // Boss label\n            ctx.fillStyle = '#ff6600';\n            ctx.font = 'bold 16px Orbitron';\n            ctx.textAlign = 'center';\n            ctx.fillText('BOSS', boss.x + boss.width / 2, boss.y - 30);\n        }\n        \n        // Draw projectiles\n        function drawProjectiles() {\n            // Player projectiles\n            ctx.fillStyle = '#00ffff';\n            ctx.shadowBlur = 10;\n            ctx.shadowColor = '#00ffff';\n            \n            for (let proj of playerProjectiles) {\n                ctx.beginPath();\n                ctx.arc(proj.x, proj.y, proj.radius, 0, Math.PI * 2);\n                ctx.fill();\n            }\n            \n            // Enemy projectiles\n            ctx.fillStyle = '#ff6600';\n            ctx.shadowColor = '#ff6600';\n            \n            for (let proj of enemyProjectiles) {\n                ctx.beginPath();\n                ctx.arc(proj.x, proj.y, proj.radius, 0, Math.PI * 2);\n                ctx.fill();\n            }\n            \n            ctx.shadowBlur = 0;\n        }\n        \n        // Draw energy cores\n        function drawEnergyCores() {\n            for (let core of energyCores) {\n                const assetId = core.isHealing ? 'energy_can' : 'energy_core';\n                drawImage(assetId, core.x, core.y, core.width, core.height);\n            }\n        }\n        \n        // Draw particles\n        function drawParticles() {\n            for (let p of particles) {\n                ctx.fillStyle = p.color;\n                ctx.globalAlpha = p.life;\n                ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);\n            }\n            ctx.globalAlpha = 1;\n        }\n        \n        // Draw canvas-based joystick (for visual feedback during gameplay)\n        function drawCanvasJoystick() {\n            if (!isTouchDevice || !joystick.active) return;\n            \n            ctx.globalAlpha = 0.4;\n            \n            // Base\n            ctx.fillStyle = '#ffffff';\n            ctx.beginPath();\n            ctx.arc(joystick.baseX, joystick.baseY, joystick.maxRadius, 0, Math.PI * 2);\n            ctx.fill();\n            \n            // Knob\n            ctx.fillStyle = '#00ffff';\n            ctx.beginPath();\n            ctx.arc(joystick.knobX, joystick.knobY, 25, 0, Math.PI * 2);\n            ctx.fill();\n            \n            ctx.globalAlpha = 1;\n        }\n        \n        /* ==================================================\n         * GAME LOOP\n         * ==================================================\n         */\n        \n        // Game loop\n        function gameLoop() {\n            const now = Date.now();\n            let deltaTime = (now - lastFrameTime) / 1000;\n            deltaTime = Math.min(deltaTime, 0.11); // Cap delta time\n            lastFrameTime = now;\n            \n            // Clear canvas\n            ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);\n            \n            // Apply screen shake\n            ctx.save();\n            ctx.translate(screenShake.x, screenShake.y);\n            \n            if (gameState === 'playing') {\n                // Update\n                updatePlayer(deltaTime);\n                updateEnemies(deltaTime);\n                updateProjectiles();\n                updateEnergyCores();\n                updateParticles();\n                updateWaveSystem();\n                spawnEnemies();\n                checkPlayerEnemyCollision();\n                updateScreenShake();\n                \n                // Draw\n                drawBackground();\n                drawPlatforms();\n                drawEnergyCores();\n                drawEnemies();\n                drawBoss();\n                drawPlayer();\n                drawProjectiles();\n                drawParticles();\n                drawCanvasJoystick();\n            } else if (gameState === 'gameOver' || gameState === 'victory') {\n                // Draw final frame\n                drawBackground();\n                drawPlatforms();\n                drawEnergyCores();\n                drawEnemies();\n                drawBoss();\n                drawPlayer();\n                drawProjectiles();\n                drawParticles();\n            }\n            \n            ctx.restore();\n            \n            animationFrameId = requestAnimationFrame(gameLoop);\n        }\n        \n        /* ==================================================\n         * GAME START/RESTART\n         * ==================================================\n         */\n        \n        // Start game\n        function startGame() {\n            // Reset state\n            gameState = 'playing';\n            score = 0;\n            health = window.gameConfig.player.maxHealth;\n            currentWave = 1;\n            lastWaveTime = 0;\n            gameStartTime = Date.now();\n            lastEnemySpawnTime = Date.now();\n            lastShootTime = 0;\n            invincibleUntil = 0;\n            bossSpawned = false;\n            enemiesDefeatedThisWave = 0;\n            \n            enemies = [];\n            playerProjectiles = [];\n            enemyProjectiles = [];\n            energyCores = [];\n            particles = [];\n            boss = null;\n            \n            initPlayer();\n            \n            // Update UI\n            updateHealthDisplay();\n            updateScoreDisplay();\n            updateWaveDisplay();\n            hideGameOver();\n            \n            // Start background music\n            playSound('background_music', true);\n            \n            // Start game loop\n            if (animationFrameId) {\n                cancelAnimationFrame(animationFrameId);\n            }\n            lastFrameTime = Date.now();\n            gameLoop();\n        }\n        \n        /* ==================================================\n         * INPUT SETUP\n         * ==================================================\n         */\n        \n        // Setup input handlers\n        function setupInputHandlers() {\n            // Keyboard\n            window.addEventListener('keydown', (e) => {\n                if (gameMode !== 'play') return;\n                \n                const key = e.key.toLowerCase();\n                keys[key] = true;\n                \n                // Prevent default for game keys\n                if (['w', 'a', 's', 'd', ' ', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright', 'x'].includes(key)) {\n                    e.preventDefault();\n                }\n            });\n            \n            window.addEventListener('keyup', (e) => {\n                const key = e.key.toLowerCase();\n                keys[key] = false;\n            });\n            \n            // Mouse\n            canvas.addEventListener('mousemove', (e) => {\n                if (gameMode !== 'play') return;\n                const rect = canvas.getBoundingClientRect();\n                const scaleX = GAME_WIDTH / rect.width;\n                const scaleY = GAME_HEIGHT / rect.height;\n                mousePos.x = (e.clientX - rect.left) * scaleX;\n                mousePos.y = (e.clientY - rect.top) * scaleY;\n            });\n            \n            canvas.addEventListener('mousedown', (e) => {\n                if (gameMode !== 'play') return;\n                mouseDown = true;\n            });\n            \n            canvas.addEventListener('mouseup', () => {\n                mouseDown = false;\n            });\n            \n            // Touch - Fire Button\n            fireButton.addEventListener('touchstart', (e) => {\n                if (gameMode !== 'play' || gameState !== 'playing') return;\n                e.preventDefault();\n                e.stopPropagation();\n                \n                shootButton.pressed = true;\n                shootButton.touchId = e.changedTouches[0].identifier;\n                updateFireButton();\n            });\n            \n            fireButton.addEventListener('touchend', (e) => {\n                if (gameMode !== 'play') return;\n                e.preventDefault();\n                e.stopPropagation();\n                \n                shootButton.pressed = false;\n                shootButton.touchId = null;\n                updateFireButton();\n            });\n            \n            // Touch - Canvas (for joystick)\n            canvas.addEventListener('touchstart', (e) => {\n                if (gameMode !== 'play') return;\n                e.preventDefault();\n                \n                const rect = canvas.getBoundingClientRect();\n                const scaleX = GAME_WIDTH / rect.width;\n                const scaleY = GAME_HEIGHT / rect.height;\n                \n                for (let touch of e.changedTouches) {\n                    const x = (touch.clientX - rect.left) * scaleX;\n                    const y = (touch.clientY - rect.top) * scaleY;\n                    \n                    // Only handle left side touches for joystick\n                    if (!joystick.active && x < GAME_WIDTH / 2) {\n                        joystick.active = true;\n                        joystick.baseX = x;\n                        joystick.baseY = y;\n                        joystick.knobX = x;\n                        joystick.knobY = y;\n                        joystick.touchId = touch.identifier;\n                        updateJoystickZone();\n                    }\n                }\n            });\n            \n            canvas.addEventListener('touchmove', (e) => {\n                if (gameMode !== 'play') return;\n                e.preventDefault();\n                \n                const rect = canvas.getBoundingClientRect();\n                const scaleX = GAME_WIDTH / rect.width;\n                const scaleY = GAME_HEIGHT / rect.height;\n                \n                for (let touch of e.changedTouches) {\n                    const x = (touch.clientX - rect.left) * scaleX;\n                    const y = (touch.clientY - rect.top) * scaleY;\n                    \n                    // Update joystick\n                    if (joystick.active && touch.identifier === joystick.touchId) {\n                        const dx = x - joystick.baseX;\n                        const dy = y - joystick.baseY;\n                        const distance = Math.sqrt(dx * dx + dy * dy);\n                        \n                        if (distance > joystick.maxRadius) {\n                            // Slide base to keep knob at max radius\n                            const angle = Math.atan2(dy, dx);\n                            joystick.baseX = x - Math.cos(angle) * joystick.maxRadius;\n                            joystick.baseY = y - Math.sin(angle) * joystick.maxRadius;\n                            joystick.knobX = x;\n                            joystick.knobY = y;\n                        } else {\n                            joystick.knobX = x;\n                            joystick.knobY = y;\n                        }\n                    }\n                }\n            });\n            \n            canvas.addEventListener('touchend', (e) => {\n                if (gameMode !== 'play') return;\n                e.preventDefault();\n                \n                for (let touch of e.changedTouches) {\n                    // Release joystick\n                    if (touch.identifier === joystick.touchId) {\n                        joystick.active = false;\n                        joystick.touchId = null;\n                        updateJoystickZone();\n                    }\n                }\n            });\n            \n            // Restart button\n            restartButton.addEventListener('click', () => {\n                if (gameState === 'gameOver') {\n                    startGame();\n                }\n            });\n            \n            // Clear input on blur\n            window.addEventListener('blur', () => {\n                keys = {};\n                mouseDown = false;\n                joystick.active = false;\n                shootButton.pressed = false;\n                updateJoystickZone();\n                updateFireButton();\n            });\n        }\n        \n        /* ==================================================\n         * MAIN RUN FUNCTION\n         * ==================================================\n         */\n        \n        // Main run function\n        function run(mode) {\n            lib.log('run() called. Mode: ' + mode);\n            gameMode = mode;\n            \n            // Detect touch device\n            isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;\n            \n            // Show game parameters\n            lib.showGameParameters({\n                name: 'Game Settings',\n                params: {\n                    'Max Health': {\n                        key: 'gameConfig.player.maxHealth',\n                        type: 'slider',\n                        min: 3,\n                        max: 10,\n                        step: 1,\n                        onChange: (value) => {\n                            window.gameConfig.player.maxHealth = value;\n                            if (gameMode === 'play' && gameState === 'playing') {\n                                health = Math.min(health, value);\n                                updateHealthDisplay();\n                            }\n                        }\n                    },\n                    'Move Speed': {\n                        key: 'gameConfig.player.moveSpeed',\n                        type: 'slider',\n                        min: 3,\n                        max: 8,\n                        step: 0.5,\n                        onChange: (value) => {\n                            window.gameConfig.player.moveSpeed = value;\n                        }\n                    },\n                    'Jump Power': {\n                        key: 'gameConfig.player.jumpPower',\n                        type: 'slider',\n                        min: 10,\n                        max: 25,\n                        step: 1,\n                        onChange: (value) => {\n                            window.gameConfig.player.jumpPower = value;\n                        }\n                    },\n                    'Shoot Cooldown (ms)': {\n                        key: 'gameConfig.player.shootCooldown',\n                        type: 'slider',\n                        min: 200,\n                        max: 1000,\n                        step: 50,\n                        onChange: (value) => {\n                            window.gameConfig.player.shootCooldown = value;\n                        }\n                    },\n                    'Enemy Move Speed': {\n                        key: 'gameConfig.enemy.moveSpeed',\n                        type: 'slider',\n                        min: 1,\n                        max: 5,\n                        step: 0.5,\n                        onChange: (value) => {\n                            window.gameConfig.enemy.moveSpeed = value;\n                        }\n                    },\n                    'Enemy Shoot Interval (ms)': {\n                        key: 'gameConfig.enemy.shootInterval',\n                        type: 'slider',\n                        min: 1000,\n                        max: 5000,\n                        step: 250,\n                        onChange: (value) => {\n                            window.gameConfig.enemy.shootInterval = value;\n                        }\n                    },\n                    'Enemy Health': {\n                        key: 'gameConfig.enemy.health',\n                        type: 'slider',\n                        min: 1,\n                        max: 5,\n                        step: 1,\n                        onChange: (value) => {\n                            window.gameConfig.enemy.health = value;\n                        }\n                    },\n                    'Enemy Spawn Interval (ms)': {\n                        key: 'gameConfig.enemy.spawnInterval',\n                        type: 'slider',\n                        min: 2000,\n                        max: 8000,\n                        step: 500,\n                        onChange: (value) => {\n                            window.gameConfig.enemy.spawnInterval = value;\n                        }\n                    },\n                    'Difficulty Increase': {\n                        key: 'gameConfig.difficulty.increaseEnabled',\n                        type: 'checkbox',\n                        onChange: (value) => {\n                            window.gameConfig.difficulty.increaseEnabled = value;\n                        }\n                    },\n                    'Wave Interval (seconds)': {\n                        key: 'gameConfig.difficulty.waveInterval',\n                        type: 'slider',\n                        min: 20,\n                        max: 60,\n                        step: 5,\n                        onChange: (value) => {\n                            window.gameConfig.difficulty.waveInterval = value;\n                        }\n                    },\n                    'Particles Enabled': {\n                        key: 'gameConfig.visual.particlesEnabled',\n                        type: 'checkbox',\n                        onChange: (value) => {\n                            window.gameConfig.visual.particlesEnabled = value;\n                        }\n                    },\n                    'Screen Shake Enabled': {\n                        key: 'gameConfig.visual.screenShakeEnabled',\n                        type: 'checkbox',\n                        onChange: (value) => {\n                            window.gameConfig.visual.screenShakeEnabled = value;\n                        }\n                    },\n                    'Boss Health': {\n                        key: 'gameConfig.boss.health',\n                        type: 'slider',\n                        min: 10,\n                        max: 50,\n                        step: 5,\n                        onChange: (value) => {\n                            window.gameConfig.boss.health = value;\n                        }\n                    },\n                    'Boss Move Speed': {\n                        key: 'gameConfig.boss.moveSpeed',\n                        type: 'slider',\n                        min: 1,\n                        max: 4,\n                        step: 0.5,\n                        onChange: (value) => {\n                            window.gameConfig.boss.moveSpeed = value;\n                        }\n                    },\n                    'Boss Shoot Interval (ms)': {\n                        key: 'gameConfig.boss.shootInterval',\n                        type: 'slider',\n                        min: 500,\n                        max: 3000,\n                        step: 250,\n                        onChange: (value) => {\n                            window.gameConfig.boss.shootInterval = value;\n                        }\n                    }\n                }\n            });\n            \n            // Preload assets\n            preloadAssets(() => {\n                lib.log('Assets loaded');\n                \n                if (mode === 'play') {\n                    setupInputHandlers();\n                    startGame();\n                } else {\n                    // Edit mode - just show parameters panel\n                    lib.log('Edit mode - adjust parameters in the panel');\n                }\n            });\n        }\n    </script>\n</body>\n</html>", "game_config": "{\"player\":{\"maxHealth\":10,\"moveSpeed\":4,\"jumpPower\":21,\"shootCooldown\":200},\"enemy\":{\"moveSpeed\":2.5,\"shootInterval\":1500,\"health\":3,\"spawnInterval\":3500},\"difficulty\":{\"increaseEnabled\":false,\"waveInterval\":20},\"visual\":{\"particlesEnabled\":true,\"screenShakeEnabled\":true},\"boss\":{\"health\":20,\"moveSpeed\":2,\"shootInterval\":1500}}", "persistent_storage_config": null, "game_metadata": {"game_id": "01KDEB7ATED109377JYA8DMHD5", "title": "Battledroids", "description": "Play as Vixera, a swift robot equipped with a \"fox buster,\" in a thrilling platformer to defend your city from an invasion of hostile droids. Navigate through dynamic, single-screen environments, strategically jumping and shooting to neutralize the enemy threat.\n\n\u2022 Fast-paced combat and agile platforming challenges\n\u2022 A vibrant, futuristic city setting under siege", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/6733f019-0ae3-4fe1-a277-19e5958ac71c.webp", "created_at": 1766787130.0, "updated_at": 1766828087339.4365, "art_style": null}, "chat_history": {"messages": [{"id": "initial-ai-0", "content": {"text": "Hey there! What kind of game do you want to create?", "images": [], "audios": [], "other_files": []}, "role": "astra", "timestamp": 1766786837326.0, "type": "multimodal"}, {"id": "user-01KDEBFVKKBDFD7PGRZWXD8CQP", "content": {"text": "A platforming game, where we play as this character, who is a robot on a quest to save the city from evil robots, she can use her fox buster to shoot at them. game name: Battledroids.", "images": [{"id": "01KDEBD429YM9C93FDXTH5B2DR", "url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_103934270055818335650/1766787027017_01KDEBD429YM9C93FDXTH5B2DR.Vixera.png", "file_name": "Vixera.png", "caption": null, "mime_type": "image/png", "width": null, "height": null}], "audios": [], "other_files": []}, "role": "user", "timestamp": 1766787116659.0, "type": "multimodal"}, {"id": "641efc64-2e0c-414e-90ce-012fc9752967", "content": {"title": "Battledroids", "description": "Play as Vixera, a swift robot equipped with a \"fox buster,\" in a thrilling platformer to defend your city from an invasion of hostile droids. Navigate through dynamic, single-screen environments, strategically jumping and shooting to neutralize the enemy threat.\n\n\u2022 Fast-paced combat and agile platforming challenges\n\u2022 A vibrant, futuristic city setting under siege", "image_urls": ["https://d2oir5eh8rty2e.cloudfront.net/assets/images/mockup/bc97935a-e2bc-4253-81e7-4dfa2a9d0bd5.webp"]}, "role": "astra", "timestamp": 1766787125523.4397, "type": "game_design"}, {"id": "user-01KDEBG8AYVMRVS45QSB65RWGC", "content": {"text": "Ok, create it!", "images": [], "audios": [], "other_files": []}, "role": "user", "timestamp": 1766787129694.0, "type": "multimodal"}, {"id": "da006dd5-44a8-463a-861e-2d060d19cf07", "content": {"text": "I've created **Battledroids** starring your hero Vixera! You'll battle waves of evil robots across a multi-level rooftop arena - jump between platforms, dodge enemy fire, and blast them with your Fox Buster. Use **arrow keys to move**, **spacebar to jump**, and **click to shoot** (or touch controls on mobile). Your goal is to survive as long as possible and rack up the highest score!\n\nIn **Edit mode**, try adjusting **enemy spawn rate** to make battles more intense, or boost Vixera's **jump power** to reach the high platform easier. You can also tweak her **shoot cooldown** for rapid-fire action! Jump in and show those evil robots who's boss! \ud83e\udd8a\u26a1", "images": [], "audios": [], "other_files": []}, "role": "astra", "timestamp": 1766787544658.6738, "type": "multimodal"}, {"id": "user-01KDEHW435WS7F1YBFFJ104JGS", "content": {"text": "Awesome! Add a boss at the end of wave 5, defeating him results in a victory.", "images": [], "audios": [], "other_files": []}, "role": "user", "timestamp": 1766793810021.0, "type": "multimodal"}, {"id": "c36fc6bc-6f7c-4c6e-b169-bbdec8dccb01", "content": {"text": "\ud83c\udfae Your boss battle is ready! At Wave 5, a massive enemy robot will appear on the high platform with devastating multi-shot attacks. Defeat the boss to achieve victory and save the city! You can tweak the boss difficulty in Edit mode to make it even more challenging. \ud83e\udd8a\u26a1", "images": [], "audios": [], "other_files": []}, "role": "astra", "timestamp": 1766793955551.6853, "type": "multimodal"}, {"id": "user-01KDEJ4X3DG97V2XB2ZPKDHG01", "content": {"text": "Give enemies a 40% chance to drop an energy can, which is a healing item.", "images": [], "audios": [], "other_files": []}, "role": "user", "timestamp": 1766794097773.0, "type": "multimodal"}, {"id": "f28b7883-64e9-416c-aa29-bdb68db8cf2d", "content": {"text": "Energy cans are now dropping from defeated enemies with a 40% chance! Collect them to restore your health and stay in the fight longer. \ud83d\udfe2\u26a1", "images": [], "audios": [], "other_files": []}, "role": "astra", "timestamp": 1766794179092.4685, "type": "multimodal"}, {"id": "user-01KDEJS163Y06E4T62341FY1QR", "content": {"text": "Sweet! Now, make it that once every 50 enemies defeated, next wave begins", "images": [], "audios": [], "other_files": []}, "role": "user", "timestamp": 1766794757315.0, "type": "multimodal"}, {"id": "2aa37fae-3cd7-467c-89ff-9fa5bccfb19d", "content": {"text": "** Every 50 enemies you defeat, the next wave begins! The intensity keeps ramping up as you progress through waves, and the boss will appear when you reach Wave 5. \ud83c\udf0a\u26a1", "images": [], "audios": [], "other_files": []}, "role": "astra", "timestamp": 1766794817583.889, "type": "multimodal"}, {"id": "user-01KDFJABJ5153BBAXZH30H2EP1", "content": {"text": "i think i overdid it, change the enemy defeat requirements from 50 to 30", "images": [], "audios": [], "other_files": []}, "role": "user", "timestamp": 1766827830853.0, "type": "multimodal"}, {"id": "b85d18e8-3efc-4b2a-84e1-7bf310f778c5", "content": {"text": "Done! Waves now progress every 30 enemies defeated instead of 50 - the action should feel more fast-paced now! \ud83d\ude80", "images": [], "audios": [], "other_files": []}, "role": "astra", "timestamp": 1766827843484.597, "type": "multimodal"}, {"id": "user-01KDFJJ4ANAJPXDYDXDHZQ6G8G", "content": {"text": "awesome! also, could you not increase the enemy fire rate as the waves go on?", "images": [], "audios": [], "other_files": []}, "role": "user", "timestamp": 1766828085589.0, "type": "multimodal"}, {"id": "d1f580d4-9fb9-477f-832a-ed28f1c5c7f0", "content": {"text": "Done! Enemies will now maintain the same fire rate throughout all waves\u2014no more ramping difficulty from their shots. The challenge will come from having more enemies on screen instead! \ud83e\udd16\u26a1", "images": [], "audios": [], "other_files": []}, "role": "astra", "timestamp": 1766828099402.815, "type": "multimodal"}]}}