{"asset_map": {"banana_launcher": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/banana_launcher_a94ef34b-6134-46f1-bdf6-00ff9a5c1798.webp", "type": "image", "prompt": "Generate a cartoon-style wooden slingshot game asset. A whole, bright yellow curved banana is sitting in the cradle. Thick, playful wooden arms with visible grain. Side view, 2D game asset, clean lines, polished mobile game aesthetic, transparent background, high resolution.", "description": "Cartoon wooden slingshot with a whole banana at the top of the screen", "subtype": "object", "aspect_ratio": [785, 1125], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "peeled_banana_projectile": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/peeled_banana_projectile_368f7eb1-09f1-4efc-bd3c-6d2a95be009b.webp", "type": "image", "prompt": "Generate a peeled banana projectile, creamy yellow-white color, simple elongated curved shape. Aerodynamic and clean design, smooth cartoon shading, side view, 2D game asset, transparent background, approximately 40x80 pixels style.", "description": "Creamy white peeled banana projectile for launching", "subtype": "object", "aspect_ratio": [988, 427], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "wooden_crate": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wooden_crate_b571f469-6656-4b2e-8769-f8c21278b775.webp", "type": "image", "prompt": "Generate a square wooden crate, 60x60px style. Brown color with visible wood grain, slightly rounded corners, light shading for depth, cartoon style, sturdy look, 2D game asset, transparent background.", "description": "Destructible 60px wooden crate obstacle", "subtype": "object", "aspect_ratio": [770, 758], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "glass_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/glass_block_c63769a9-ef56-4ced-8b54-0b04f6ac6b30.webp", "type": "image", "prompt": "Generate a square semi-transparent glass block, 60x60px style. Light blue color, subtle shine highlights and reflections, smooth edges, fragile look, cartoon mobile game style, 2D game asset, transparent background.", "description": "Fragile semi-transparent glass block obstacle", "subtype": "object", "aspect_ratio": [702, 696], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "wooden_wall_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wooden_wall_texture_ee9d9841-0a93-437f-ae61-3e977fe8bed1.webp", "type": "image", "prompt": "Generate a seamless vertically tiling texture of wooden planks. Brown wood with visible vertical grain, warm tones, subtle cartoon shading, 50px width style, designed to loop perfectly from top to bottom, opaque background.", "description": "Vertically tiling wooden plank texture for side walls", "subtype": "background", "aspect_ratio": [1024, 1536], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "wood_splinter_particle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wood_splinter_particle_cc96ee51-6164-4c61-9475-4b9ba69105d0.webp", "type": "image", "prompt": "Generate a small irregular triangular wood splinter, brown color matching image 1, simple clean cartoon design, 2D game particle, transparent background.", "description": "Small wood shard particle for crate destruction", "subtype": "effect", "aspect_ratio": [433, 451], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "glass_shard_particle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/glass_shard_particle_ce9ac712-a4dd-445b-8c3d-1c5819467955.webp", "type": "image", "prompt": "Generate a small irregular glass shard, light blue semi-transparent, sharp crystalline look, matching the style of image 1, 2D game particle, transparent background.", "description": "Small blue glass shard particle for block destruction", "subtype": "effect", "aspect_ratio": [310, 341], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "impact_star": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/impact_star_2e4f2b30-751c-4ebc-9954-1b4ea94e4b16.webp", "type": "image", "prompt": "Generate a small bright yellow 4-point cartoon star sparkle, glowing edges, playful and energetic style, 2D game asset, transparent background.", "description": "Sparkle effect for wall bounces", "subtype": "effect", "aspect_ratio": [729, 739], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "restart_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/restart_icon_efa61781-c3f3-4d30-a55d-92d6538e29be.webp", "type": "image", "prompt": "Generate a white circular arrow icon pointing counterclockwise, bold clean flat design, 48x48px style, UI button asset, transparent background.", "description": "Circular arrow icon for restarting the game", "subtype": "ui", "aspect_ratio": [674, 659], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "next_arrow_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/next_arrow_icon_72ff0bcf-d965-429f-ad02-2be8ec61a81e.webp", "type": "image", "prompt": "Generate a white right-pointing chevron arrow icon, bold flat UI design, 48x48px style, clean edges, UI button asset, transparent background.", "description": "Right-pointing arrow icon for level progression", "subtype": "ui", "aspect_ratio": [409, 653], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "menu_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/menu_icon_43bed861-2494-4583-b378-746feb19fecc.webp", "type": "image", "prompt": "Generate a white hamburger menu icon with three horizontal lines, evenly spaced, bold flat UI design, 48x48px style, transparent background.", "description": "Hamburger menu icon for UI", "subtype": "ui", "aspect_ratio": [606, 481], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "star_empty": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/star_empty_3b0b163f-711e-489e-af9d-ffb32ff0dccf.webp", "type": "image", "prompt": "Generate an outline of a 5-point star, hollow center, gold border, 40x40px style, clean simple UI design, transparent background.", "description": "Outline of a 5-point star for UI rating", "subtype": "ui", "aspect_ratio": [738, 702], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "star_filled": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/star_filled_7d8205a5-4225-4f1c-8156-590ddb125c75.webp", "type": "image", "prompt": "Generate a solid filled 5-point star, bright gold color with a slight gradient and shine, matching the shape of image 1, 40x40px style, polished UI asset, transparent background.", "description": "Filled 5-point star for UI rating", "subtype": "ui", "aspect_ratio": [976, 923], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "launch_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/launch_sound_5144d9ca-373b-4724-b5be-726e05ca1cd7.mp3", "type": "audio", "prompt": "A short, punchy 'fwip' or 'whoosh' sound effect for a cartoon physics game. The sound should feel like a rubbery or organic object being flung from a slingshot. Quick attack, airy tail, light and energetic, 0.5 seconds duration, high-quality digital recording.", "description": "Short playful 'fwip' sound for launching the banana projectile.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0.98, "loop": false, "animation": null}, "wall_bounce_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/wall_bounce_sound_f18775a2-5a06-4eff-9815-f3c95d115db8.mp3", "type": "audio", "prompt": "Playful cartoon 'boing' impact sound effect. Rubbery, bouncy texture with a slight pitch bend upwards. Short and snappy, suitable for frequent wall collisions in a physics puzzle game. No metallic noise, clean and crisp.", "description": "Rubbery 'boing' sound for when the banana hits a wall.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.61, "loop": false, "animation": null}, "wood_break_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/wood_break_sound_b42b8ca1-6ab6-4eef-92e1-5ee2db47f7ff.mp3", "type": "audio", "prompt": "Satisfying wooden crate destruction sound effect. Crunchy splintering and cracking wood textures. Impactful but cartoonish, not overly realistic. Short decay, conveys a solid object breaking into pieces. Clean audio with no background hiss.", "description": "Crunchy splintering sound for destroying wooden crates.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2, "loop": false, "animation": null}, "glass_shatter_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/glass_shatter_sound_cea1e9be-f968-4125-b711-c9d09270508a.mp3", "type": "audio", "prompt": "Light glass shattering sound effect for a casual game. High-pitched tinkling shards, fragile and crystalline. Satisfying 'clink-shatter' sequence, bright and airy, cartoon style, approximately 0.5 seconds of active sound followed by a quick silence.", "description": "High-pitched tinkling glass break sound effect.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.37, "loop": false, "animation": null}, "victory_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/victory_sound_9fd74fe4-73f4-4af6-b4a8-092c42c47273.mp3", "type": "audio", "prompt": "A cheerful 3-second victory jingle, 90s casual game style. Bright major-key melody featuring xylophone and light brass. Rising notes ending on a triumphant chord. Upbeat, celebratory, and rewarding. High energy, instrumental only.", "description": "Short celebratory jingle for level completion.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 3, "loop": false, "animation": null}, "fail_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/fail_sound_830ee219-09e3-4fe6-ad69-29391912b8a7.mp3", "type": "audio", "prompt": "A short, playful 'fail' sound effect. Descending 3-note melody on a slide whistle or soft synth. Slightly sad but whimsical and sympathetic, not harsh or punishing. 1.5 seconds duration, encouraging a retry.", "description": "Descending sympathetic tone for a game over state.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 3, "loop": false, "animation": null}, "background_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/background_music_e8a92b92-e8b0-4bf6-83a2-0750f15c002d.mp3", "type": "audio", "prompt": "Upbeat and whimsical background music for a casual puzzle game. 130 BPM, C major. Features a bouncy ukulele rhythm, playful xylophone melody, and light woodblock percussion. Fun, breezy, and non-distracting. Seamless loop, bright and happy atmosphere, high-quality instrumental arrangement.", "description": "Cheerful, loopable background track for gameplay.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 30, "loop": true, "animation": null}, "banana_man": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/banana_man_c6e623f3-a6ea-4b46-9eca-69d15ff4e6d8.webp", "type": "image", "prompt": "Generate a full-body, front-facing illustration of a cartoonish upright walking banana character. The character should have a friendly face, arms, and legs. Vibrant bright yellow color, clean bold outlines, high quality game-ready asset with a transparent background. Consistent with a fun, casual mobile game style.", "description": "A friendly, cartoonish banana character with limbs, standing in a front-facing pose.", "subtype": "character", "aspect_ratio": [856, 1351], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "wooden_plank_long": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wooden_plank_long_fc6cfdf1-1a6e-463e-b8b8-20427465354d.webp", "type": "image", "prompt": "Generate a long horizontal wooden plank for a platformer game, sturdy appearance with visible wood grain. Simple polished cartoon style to match the aesthetic of a wooden crate and banana launcher. High quality, clean edges, front-facing view, transparent background.", "description": "A long horizontal wooden plank platform with a sturdy grain texture.", "subtype": "ground", "aspect_ratio": [1248, 197], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "boss_monkey": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/boss_monkey_476bf5b7-ad61-4f66-8fae-ab433f46cb7b.webp", "type": "image", "prompt": "Generate a full-body, front-facing illustration of a mischievous cartoon monkey wearing a small golden crown. The monkey is sitting in a confident, boss-like pose. Use a vibrant, polished cartoon style consistent with a 'banana man' character. High resolution, detailed textures, clean lines, professional game-ready asset with a transparent background.", "description": "A mischievous cartoon monkey boss wearing a crown, serving as the primary antagonist.", "subtype": "character", "aspect_ratio": [767, 1142], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "explosive_banana_projectile": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/explosive_banana_projectile_ec564ae8-2688-4e9c-b799-3239e8c5682d.webp", "type": "image", "prompt": "Generate a high-quality cartoon style peeled banana projectile, glowing with intense bright orange and red energy. Small fiery spark particles emanate from the fruit. The style should perfectly match the creamy white texture of image 1 but with an explosive, volatile aura. Transparent background, front-view, polished game asset.", "description": "A glowing, explosive version of the peeled banana projectile with sparks.", "subtype": "effect", "aspect_ratio": [1248, 734], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "explosion_particle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/explosion_particle_a1371321-417b-44ac-9f8b-ce09aeea8e2e.webp", "type": "image", "prompt": "Generate a bright cartoon-style explosion burst featuring vibrant orange and yellow flames with small, soft dark smoke puffs, high energy visual effect, clean edges, professional game art quality, transparent background.", "description": "A vibrant cartoon explosion effect for when explosive banana projectiles detonate.", "subtype": "effect", "aspect_ratio": [916, 901], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "explosion_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/explosion_sound_c416aa96-a3ad-45e5-9b80-1f6c601ea843.mp3", "type": "audio", "prompt": "A classic, high-quality cartoon explosion sound effect. It should start with a punchy 'ka-boom' impact, followed by a slightly comical, whistling debris-falling tail. The sound should be bouncy and exaggerated rather than realistic, with a clean low-end punch and a playful character. No metallic clanging or realistic gravel sounds; focus on a soft, rounded, yet powerful burst that fits a colorful, physics-based game.", "description": "A loud, satisfying cartoon explosion sound effect for destroying obstacles.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 3.48, "loop": false, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Banana Bounce Bash</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n    <link href=\"https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&family=Poppins:wght@400;600&display=swap\" rel=\"stylesheet\">\n    <style>\n        /* =========================\n           CSS Reset & Base Styles\n           ========================= */\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body, html {\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            -ms-touch-action: none;\n            font-family: 'Poppins', sans-serif;\n        }\n\n        /* =========================\n           Custom Properties\n           ========================= */\n        :root {\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --transition-slow: 500ms;\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            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n            --color-primary: #FFD700;\n            --color-bg: #87CEEB;\n            --color-text: #2C1810;\n            --color-ui-bg: rgba(255, 255, 255, 0.95);\n            --color-button: #FFA500;\n            --color-button-hover: #FF8C00;\n        }\n\n        /* =========================\n           Game Container\n           ========================= */\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n            background: linear-gradient(180deg, #87CEEB 0%, #B0E0E6 100%);\n        }\n\n        #game-canvas {\n            display: block;\n            width: 100%;\n            height: 100%;\n        }\n\n        /* =========================\n           UI Overlay System\n           ========================= */\n        .ui-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            right: 0;\n            bottom: 0;\n            pointer-events: none;\n        }\n\n        .hud, .bottom-controls, .edit-controls {\n            pointer-events: auto;\n        }\n\n        /* =========================\n           HUD Elements\n           ========================= */\n        .hud {\n            position: absolute;\n            top: var(--space-sm);\n            left: var(--space-sm);\n            right: var(--space-sm);\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            pointer-events: none;\n        }\n\n        .hud-item {\n            background: var(--color-ui-bg);\n            padding: var(--space-xs) var(--space-sm);\n            border-radius: 16px;\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n            display: flex;\n            align-items: center;\n            gap: var(--space-xs);\n            font-size: 28px;\n            font-weight: 600;\n            color: var(--color-text);\n        }\n\n        .banana-icon {\n            width: 32px;\n            height: 32px;\n            background: var(--color-primary);\n            border-radius: 50%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        /* =========================\n           Buttons\n           ========================= */\n        .game-button {\n            background: var(--color-button);\n            border: none;\n            border-radius: 16px;\n            padding: var(--space-sm) var(--space-lg);\n            font-size: 28px;\n            font-weight: 600;\n            color: white;\n            cursor: pointer;\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);\n            transition: all var(--transition-fast) var(--ease-out);\n            font-family: 'Fredoka', sans-serif;\n            pointer-events: auto;\n            min-width: 96px;\n            min-height: 96px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .game-button:hover {\n            background: var(--color-button-hover);\n            transform: translateY(-2px);\n            box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);\n        }\n\n        .game-button:active {\n            transform: translateY(0);\n            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n        }\n\n        .game-button:disabled {\n            background: #CCC;\n            cursor: not-allowed;\n            opacity: 0.6;\n        }\n\n        .icon-button {\n            width: 50px;\n            height: 50px;\n            padding: var(--space-xs);\n            background: var(--color-button);\n            border: none;\n            border-radius: 50%;\n            cursor: pointer;\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);\n            transition: all var(--transition-fast) var(--ease-out);\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            pointer-events: auto;\n        }\n\n        .icon-button img {\n            width: 24px;\n            height: 24px;\n            object-fit: contain;\n        }\n\n        .icon-button:hover {\n            background: var(--color-button-hover);\n            transform: scale(1.05);\n        }\n\n        .icon-button:active {\n            transform: scale(0.95);\n        }\n\n        /* =========================\n           Modal Screens\n           ========================= */\n        .modal-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            right: 0;\n            bottom: 0;\n            background: rgba(0, 0, 0, 0.7);\n            display: none;\n            align-items: center;\n            justify-content: center;\n            opacity: 0;\n            pointer-events: none;\n            transition: opacity var(--transition-normal) var(--ease-out);\n            z-index: 1000;\n        }\n\n        .modal-overlay:not(.visible) * {\n            pointer-events: none !important;\n            cursor: default !important;\n        }\n\n        .modal-overlay.visible {\n            display: flex;\n            opacity: 1;\n            pointer-events: auto;\n        }\n\n        .modal-content {\n            background: var(--color-ui-bg);\n            border-radius: 24px;\n            padding: var(--space-xl);\n            text-align: center;\n            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n            max-width: 600px;\n            width: 90%;\n            transform: scale(0.8);\n            transition: transform var(--transition-normal) var(--ease-bounce);\n        }\n        .modal-overlay.visible .modal-content {\n            transform: scale(1);\n        }\n\n        .modal-title {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 56px;\n            font-weight: 700;\n            color: var(--color-text);\n            margin-bottom: var(--space-md);\n        }\n\n        .modal-subtitle {\n            font-size: 28px;\n            color: var(--color-text);\n            margin-bottom: var(--space-lg);\n        }\n\n        .star-display {\n            display: flex;\n            justify-content: center;\n            gap: var(--space-sm);\n            margin: var(--space-lg) 0;\n        }\n\n        .star-display img {\n            width: 64px;\n            height: 64px;\n        }\n\n        .button-group {\n            display: flex;\n            gap: var(--space-md);\n            justify-content: center;\n            margin-top: var(--space-lg);\n        }\n\n        /* =========================\n           Bottom Controls\n           ========================= */\n        .bottom-controls {\n            position: absolute;\n            bottom: var(--space-sm);\n            left: var(--space-sm);\n            right: var(--space-sm);\n            display: flex;\n            justify-content: flex-end;\n            gap: var(--space-md);\n            pointer-events: none;\n        }\n        /* =========================\n           Edit Mode Styles\n           ========================= */\n        .edit-controls {\n            position: absolute;\n            bottom: var(--space-sm);\n            left: var(--space-sm);\n            right: var(--space-sm);\n            background: var(--color-ui-bg);\n            border-radius: 16px;\n            padding: var(--space-sm);\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n            display: flex;\n            flex-direction: column;\n            gap: var(--space-sm);\n        }\n\n        .edit-toolbar {\n            display: flex;\n            gap: var(--space-sm);\n            justify-content: center;\n            flex-wrap: wrap;\n        }\n\n        .tool-button {\n            padding: var(--space-sm) var(--space-md);\n            background: #DDD;\n            border: 2px solid transparent;\n            border-radius: 12px;\n            cursor: pointer;\n            font-size: 20px;\n            transition: all var(--transition-fast) var(--ease-out);\n            min-width: 96px;\n            min-height: 96px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-weight: 600;\n        }\n\n        .tool-button.active {\n            background: var(--color-primary);\n            border-color: #FFA500;\n        }\n\n        .tool-button:hover {\n            transform: scale(1.05);\n        }\n\n        /* =========================\n           Animations\n           ========================= */\n        @keyframes shake {\n            0%, 100% { transform: translateX(0); }\n            25% { transform: translateX(-4px); }\n            75% { transform: translateX(4px); }\n        }\n\n        @keyframes pulse {\n            0%, 100% { transform: scale(1); }\n            50% { transform: scale(1.1); }\n        }\n\n        @keyframes fadeIn {\n            from { opacity: 0; }\n            to { opacity: 1; }\n        }\n\n        /* =========================\n           Media Query - Reduced Motion\n           ========================= */\n        @media (prefers-reduced-motion: reduce) {\n            * {\n                animation: none !important;\n                transition: none !important;\n            }\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"game-canvas\"></canvas>\n        <div class=\"ui-overlay\">\n            <div class=\"hud\">\n                <div class=\"hud-item\">\n                    <div class=\"banana-icon\">\ud83c\udf4c</div>\n                    <span id=\"banana-count\">5</span>\n                </div>\n                <div class=\"hud-item\">\n                    <span id=\"level-indicator\">Level 1/10</span>\n                </div>\n            </div>\n            <div class=\"bottom-controls\" id=\"play-controls\" style=\"display: none;\">\n                <button class=\"icon-button\" id=\"restart-button\">\n                    <img id=\"restart-icon\" src=\"\" alt=\"Restart\">\n                </button>\n            </div>\n            <div class=\"modal-overlay\" id=\"win-modal\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-title\">Level Complete!</div>\n                    <div class=\"star-display\" id=\"star-display\">\n                        <img id=\"star1\" src=\"\" alt=\"Star\">\n                        <img id=\"star2\" src=\"\" alt=\"Star\">\n                        <img id=\"star3\" src=\"\" alt=\"Star\">\n                    </div>\n                    <div class=\"button-group\">\n                        <button class=\"game-button\" id=\"retry-button\">Retry</button>\n                        <button class=\"game-button\" id=\"next-button\">Next Level</button>\n                    </div>\n                </div>\n            </div>\n            <div class=\"modal-overlay\" id=\"lose-modal\">\n                <div class=\"modal-content\">\n                    <div class=\"modal-title\">Out of Bananas!</div>\n                    <div class=\"modal-subtitle\">Try again to clear all obstacles</div>\n                    <div class=\"button-group\">\n                        <button class=\"game-button\" id=\"lose-retry-button\">Retry</button>\n                    </div>\n                </div>\n            </div>\n            <div class=\"edit-controls\" id=\"edit-controls\" style=\"display: none;\">\n                <div class=\"edit-toolbar\">\n                    <button class=\"tool-button\" id=\"select-tool\">Select</button>\n                    <button class=\"tool-button\" id=\"crate-tool\">\ud83e\udeb5 Crate</button>\n                    <button class=\"tool-button\" id=\"glass-tool\">\ud83d\udc8e Glass</button>\n                    <button class=\"tool-button\" id=\"explosive-tool\">\ud83d\udca5 TNT</button>\n                    <button class=\"tool-button\" id=\"portal-tool\">\ud83c\udf00 Portal</button>\n                    <button class=\"tool-button\" id=\"add-level-tool\">\u2795 Level</button>\n                    <button class=\"tool-button\" id=\"delete-tool\">\ud83d\uddd1\ufe0f Delete</button>\n                </div>\n                <div style=\"display: flex; gap: 8px; justify-content: center;\">\n                    <select id=\"level-select\" style=\"font-size: 20px; padding: 8px; border-radius: 8px; min-height: 48px;\">\n                    </select>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <script>\n        /**\n         * A physics-based puzzle game where players launch peeled bananas to destroy obstacles.\n         * Features 5 levels with increasing difficulty, wall bouncing mechanics, and satisfying\n         * destruction physics. Players aim and launch bananas from the top of the screen,\n         * bouncing off walls to strategically clear all obstacles.\n         * \n         * Core Mechanics:\n         * - Drag-based aiming and power control\n         * - Realistic physics with gravity and bouncing\n         * - Multiple obstacle types with destruction effects\n         * - Star rating system based on efficiency\n         * - Full edit mode for level customization\n         */\n        /**\n         * A physics-based puzzle game where players launch peeled bananas to destroy obstacles.\n         * Features 5 levels with increasing difficulty, wall bouncing mechanics, and satisfying\n         * destruction physics. Players aim and launch bananas from the top of the screen,\n         * bouncing off walls to strategically clear all obstacles.\n         * \n         * Core Mechanics:\n         * - Drag-based aiming and power control\n         * - Realistic physics with gravity and bouncing\n         * - Multiple obstacle types with destruction effects\n         * - Star rating system based on efficiency\n         * - Full edit mode for level customization\n         */\n\n        async function run(mode) {\n            // ============================================\n            // CONSTANTS\n            // ============================================\n            const SCREEN_WIDTH = 720;\n            const SCREEN_HEIGHT = 1280;\n            const WALL_WIDTH = 50;\n            const PLAY_AREA_WIDTH = SCREEN_WIDTH - (WALL_WIDTH * 2);\n            const LAUNCHER_ZONE_HEIGHT = 150;\n            const PLAY_ZONE_TOP = 400;\n            const PLAY_ZONE_BOTTOM = 1200;\n            const OBSTACLE_SIZE = 60;\n            const GRID_SIZE = 60;\n\n            // Physics constants from gameConfig\n            let GRAVITY = 800;\n            let WALL_BOUNCE_RETENTION = 0.7;\n            let OBSTACLE_BOUNCE_RETENTION = 0.5;\n\n            const Vec2 = {\n                add: (v1, v2) => ({ x: v1.x + v2.x, y: v1.y + v2.y }),\n                sub: (v1, v2) => ({ x: v1.x - v2.x, y: v1.y - v2.y }),\n                mul: (v, s) => ({ x: v.x * s, y: v.y * s }),\n                dot: (v1, v2) => v1.x * v2.x + v1.y * v2.y,\n                mag: (v) => Math.sqrt(v.x * v.x + v.y * v.y),\n                normalize: (v) => {\n                    const m = Vec2.mag(v);\n                    return m === 0 ? { x: 0, y: 0 } : { x: v.x / m, y: v.y / m };\n                },\n                reflect: (v, n, restitution) => {\n                    const dot = Vec2.dot(v, n);\n                    if (dot > 0) return v; // Already moving away\n                    const reflection = Vec2.sub(v, Vec2.mul(n, 2 * dot));\n                    return Vec2.mul(reflection, restitution);\n                }\n            };\n\n            // ============================================\n            // CONFIG INITIALIZATION\n            // ============================================\n            \n            // Ensure gameConfig exists with default values\n            if (!window.gameConfig) {\n                lib.log('Warning: gameConfig not initialized, using defaults');\n                window.gameConfig = {\n                    levels: [],\n                    settings: {\n                        gravity: 800,\n                        wallBounceRetention: 0.9,\n                        obstacleBounceRetention: 0.5\n                    }\n                };\n            }\n\n            // Cleanup any functions that might have accidentally leaked into gameConfig\n            const cleanConfig = (obj) => {\n                if (!obj || typeof obj !== 'object') return;\n                for (let key in obj) {\n                    if (typeof obj[key] === 'function') {\n                        delete obj[key];\n                    } else if (typeof obj[key] === 'object') {\n                        cleanConfig(obj[key]);\n                    }\n                }\n            };\n            cleanConfig(window.gameConfig);\n\n            // Ensure we have at least 30 levels\n            if (!window.gameConfig.levels) window.gameConfig.levels = [];\n            while (window.gameConfig.levels.length < 30) {\n                window.gameConfig.levels.push({\n                    bananaCount: 5,\n                    obstacles: [],\n                    portals: [],\n                    starThresholds: [1, 2, 3]\n                });\n            }\n\n            // Helper to add obstacle with bounds and type logic\n            function addObstacle(level, x, y, levelIndex) {\n                const PLAY_ZONE_BOTTOM = 1200;\n                const gridX = Math.round((x - WALL_WIDTH) / 60) * 60 + WALL_WIDTH;\n                const gridY = Math.round((y - 500) / 60) * 60 + 500;\n\n                // Bounds check\n                if (gridX < WALL_WIDTH || gridX > SCREEN_WIDTH - WALL_WIDTH - OBSTACLE_SIZE) return false;\n                if (gridY > PLAY_ZONE_BOTTOM - 100) return false;\n                if (gridY < 400) return false;\n\n                // Overlap check: ensure no two obstacles occupy the same grid slot\n                const isOccupied = level.obstacles.some(obs => \n                    Math.abs(obs.x - gridX) < 10 && Math.abs(obs.y - gridY) < 10\n                );\n                if (isOccupied) return false;\n\n                let type = 'crate';\n                const rand = Math.random();\n                const glassChance = 0.2 + (levelIndex / 60);\n                const tntChance = 0.05 + (levelIndex / 100);\n                const splitChance = 0.05 + (levelIndex / 80);\n\n                if (rand < tntChance) type = 'explosive';\n                else if (rand < tntChance + splitChance) type = 'splitting';\n                else if (rand < tntChance + splitChance + glassChance) type = 'glass';\n\n                level.obstacles.push({ x: gridX, y: gridY, type });\n                return true;\n            }\n\n            const CURRENT_LAYOUT_VERSION = 8;\n            for (let i = 4; i < 30; i++) {\n                if (!window.gameConfig.levels[i]) {\n                    window.gameConfig.levels[i] = { bananaCount: 5, obstacles: [], portals: [], starThresholds: [1, 2, 3] };\n                }\n                const level = window.gameConfig.levels[i];\n                \n                if (level.obstacles.length === 0 || level.layoutVersion !== CURRENT_LAYOUT_VERSION) {\n                    const isBossLevel = (i + 1) % 5 === 0;\n                    level.hasBoss = isBossLevel;\n                    level.bananaCount = 7 + Math.floor(i / 2);\n                    level.starThresholds = [\n                        Math.floor(level.bananaCount * 0.3),\n                        Math.floor(level.bananaCount * 0.5),\n                        Math.floor(level.bananaCount * 0.7)\n                    ];\n                    \n                    if (i >= 19) {\n                        level.portals = [{\n                            x1: 110, y1: 500,\n                            x2: 550, y2: 1000\n                        }];\n                    } else {\n                        level.portals = [];\n                    }\n\n                    level.obstacles = [];\n                    const startY = 550;\n                    const gridCols = 10;\n                    const designIndex = i % 5;\n                    \n                    if (isBossLevel) {\n                        // Boss Arena: Advanced Fortress\n                        if (designIndex === 4) { // Level 5, 10, 15...\n                            // Outer Ring (Glass)\n                            for(let a=0; a<Math.PI*2; a+=Math.PI/6) {\n                                const rx = 240, ry = 180;\n                                addObstacle(level, SCREEN_WIDTH/2 + Math.cos(a)*rx, startY + 200 + Math.sin(a)*ry, i);\n                                level.obstacles[level.obstacles.length-1].type = 'glass';\n                            }\n                            // Inner Core (Wood + TNT)\n                            for(let r=0; r<3; r++) {\n                                for(let c=0; c<3; c++) {\n                                    addObstacle(level, SCREEN_WIDTH/2 - 90 + c*60, startY + 140 + r*60, i);\n                                    if (r === 1 && c === 1) level.obstacles[level.obstacles.length-1].type = 'explosive';\n                                }\n                            }\n                        }\n                    } else {\n                        // New Pattern Designs\n                        if (designIndex === 0) { // 'O' or 'C' Enclosure\n                            const centerX = SCREEN_WIDTH / 2;\n                            const centerY = startY + 200;\n                            const isC = i % 2 === 0;\n                            const maxAngle = isC ? Math.PI * 1.5 : Math.PI * 2;\n                            for(let a=0; a<maxAngle; a+=0.4) {\n                                addObstacle(level, centerX + Math.cos(a)*180 - 30, centerY + Math.sin(a)*180 - 30, i);\n                                if (Math.random() < 0.2) level.obstacles[level.obstacles.length-1].type = 'glass';\n                            }\n                            // Prize in middle\n                            addObstacle(level, centerX - 30, centerY - 30, i);\n                            level.obstacles[level.obstacles.length-1].type = 'explosive';\n                        } \n                        else if (designIndex === 1) { // 'V-Funnel' Structure\n                            for(let r=0; r<6; r++) {\n                                // Diagonal left\n                                addObstacle(level, WALL_WIDTH + r*50, startY + r*80, i);\n                                // Diagonal right\n                                addObstacle(level, SCREEN_WIDTH - WALL_WIDTH - 60 - r*50, startY + r*80, i);\n                            }\n                            // Catch platforms below\n                            for(let c=0; c<3; c++) {\n                                addObstacle(level, SCREEN_WIDTH/2 - 90 + c*60, startY + 600, i);\n                            }\n                        }\n                        else if (designIndex === 2) { // 'Defense Shield' (Layered)\n                            // Outer Glass Wall\n                            for(let c=1; c<gridCols-1; c++) {\n                                addObstacle(level, WALL_WIDTH + c*60, startY, i);\n                                level.obstacles[level.obstacles.length-1].type = 'glass';\n                                addObstacle(level, WALL_WIDTH + c*60, startY + 300, i);\n                                level.obstacles[level.obstacles.length-1].type = 'glass';\n                            }\n                            // Inner Wood Core\n                            for(let r=1; r<4; r++) {\n                                for(let c=3; c<7; c++) {\n                                    addObstacle(level, WALL_WIDTH + c*60, startY + r*60, i);\n                                    if (c === 5 && r === 2) level.obstacles[level.obstacles.length-1].type = 'explosive';\n                                }\n                            }\n                        }\n                        else if (designIndex === 3) { // 'Staggered Stairs' (Zig-Zag)\n                            for(let r=0; r<5; r++) {\n                                const isLeft = r % 2 === 0;\n                                const startX = isLeft ? WALL_WIDTH + 60 : SCREEN_WIDTH - WALL_WIDTH - 300;\n                                for(let c=0; c<4; c++) {\n                                    addObstacle(level, startX + c*60, startY + r*150, i);\n                                    if (c === 3 && isLeft) level.obstacles[level.obstacles.length-1].type = 'glass';\n                                    if (c === 0 && !isLeft) level.obstacles[level.obstacles.length-1].type = 'glass';\n                                }\n                            }\n                        }\n                        else { // Hybrid Maze\n                            for(let r=0; r<6; r++) {\n                                for(let c=0; c<gridCols; c++) {\n                                    if ((r + c) % 4 === 0) {\n                                        addObstacle(level, WALL_WIDTH + c*60, startY + r*60, i);\n                                        if (r === 2) level.obstacles[level.obstacles.length-1].type = 'explosive';\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    level.specialObstaclesCount = 2 + Math.floor(i / 10);\n                    level.layoutVersion = CURRENT_LAYOUT_VERSION;\n                }\n            }\n            \n            // Ensure levels array exists\n            if (!window.gameConfig.levels || !Array.isArray(window.gameConfig.levels)) {\n                window.gameConfig.levels = [];\n            }\n            \n            // Ensure settings exist\n            if (!window.gameConfig.settings) {\n                window.gameConfig.settings = {\n                    gravity: 800,\n                    wallBounceRetention: 0.9,\n                    obstacleBounceRetention: 0.5,\n                    selectedLevel: 0\n                };\n            }\n            if (window.gameConfig.settings.selectedLevel === undefined) {\n                window.gameConfig.settings.selectedLevel = 0;\n            }\n\n            // ============================================\n            // GAME PARAMETERS\n            // ============================================\n\n            function setupGameParameters() {\n                // Ensure we have a valid number of levels for the dropdown\n                let totalLevels = 10;\n                if (window.gameConfig.levels && window.gameConfig.levels.length > 0) {\n                    totalLevels = window.gameConfig.levels.length;\n                }\n                \n                const levelOptions = [];\n                for (let i = 0; i < totalLevels; i++) {\n                    levelOptions.push({\n                        label: `Level ${i + 1} (\u7b2c ${i + 1} \u5173)`,\n                        value: i.toString() // Use string for value\n                    });\n                }\n\n                lib.showGameParameters({\n                    name: 'Banana Bounce Bash',\n                    params: {\n                        'Gravity \u91cd\u529b': {\n                            key: 'settings.gravity',\n                            type: 'slider',\n                            min: 600,\n                            max: 1000,\n                            step: 10\n                        },\n                        'Wall Bounce \u5899\u58c1\u53cd\u5f39': {\n                            key: 'settings.wallBounceRetention',\n                            type: 'slider',\n                            min: 0.1,\n                            max: 0.9,\n                            step: 0.05\n                        },\n                        'Obstacle Bounce \u969c\u788d\u7269\u53cd\u5f39': {\n                            key: 'settings.obstacleBounceRetention',\n                            type: 'slider',\n                            min: 0.1,\n                            max: 0.9,\n                            step: 0.05\n                        },\n                        'Level Select \u5173\u5361\u9009\u62e9': {\n                            key: 'settings.selectedLevel',\n                            type: 'dropdown',\n                            options: levelOptions,\n                            onChange: (val) => {\n                                const newLevel = parseInt(val);\n                                if (!isNaN(newLevel)) {\n                                    window.gameConfig.settings.selectedLevel = newLevel;\n                                    initLevel(newLevel);\n                                }\n                            }\n                        }\n                    }\n                });\n            }\n\n\n            // ============================================\n            // CLEANUP & MULTI-RUN PROTECTION\n            // ============================================\n            \n            // Use a unique ID for this run to identify active listeners and loops\n            const runId = Math.random();\n            window.lastBananaRunId = runId;\n\n            // Cleanup any existing background music from previous runs\n            if (window.currentBananaMusic) {\n                try { window.currentBananaMusic.stop(); } catch(e) {}\n                window.currentBananaMusic = null;\n            }\n\n            const gameWorld = document.getElementById('game-world');\n            const canvas = document.getElementById('game-canvas');\n            const ctx = canvas.getContext('2d');\n\n            // Canvas setup and resize handling\n            function resizeCanvas() {\n                canvas.width = gameWorld.clientWidth;\n                canvas.height = gameWorld.clientHeight;\n            }\n            resizeCanvas();\n            window.addEventListener('resize', resizeCanvas);\n\n            // ============================================\n            // ============================================\n            // ASSET PRELOADING\n            // ============================================\n\n            const assetCache = {};\n            const audioContext = new (window.AudioContext || window.webkitAudioContext)();\n            const audioBuffers = {};\n\n\n            async function preloadAssets() {\n                const assetIds = [\n                    'banana_man', 'wooden_plank_long', 'peeled_banana_projectile', 'wooden_crate', 'glass_block',\n                    'wooden_wall_texture', 'wood_splinter_particle', 'glass_shard_particle',\n                    'impact_star', 'restart_icon', 'next_arrow_icon', 'menu_icon',\n                    'star_empty', 'star_filled',\n                    'launch_sound', 'wall_bounce_sound', 'wood_break_sound',\n                    'glass_shatter_sound', 'victory_sound', 'fail_sound', 'background_music',\n                    'boss_monkey', 'explosive_banana_projectile', 'explosion_particle', 'explosion_sound'\n                ];\n\n                const loadPromises = assetIds.map(async (id) => {\n                    const assetInfo = lib.getAsset(id);\n                    if (!assetInfo) {\n                        lib.log(`Asset not found: ${id}`);\n                        return;\n                    }\n\n                    if (assetInfo.type === 'audio') {\n                        try {\n                            const response = await fetch(assetInfo.url);\n                            const arrayBuffer = await response.arrayBuffer();\n                            const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n                            audioBuffers[id] = audioBuffer;\n                        } catch (error) {\n                            lib.log(`Failed to load audio ${id}: ${error}`);\n                        }\n                    } else {\n                        return new Promise((resolve) => {\n                            const img = new Image();\n                            img.onload = () => {\n                                assetCache[id] = img;\n                                resolve();\n                            };\n                            img.onerror = () => {\n                                lib.log(`Failed to load image ${id}`);\n                                resolve(); // Don't reject, just skip\n                            };\n                            img.src = assetInfo.url;\n                        });\n                    }\n                });\n\n                await Promise.all(loadPromises);\n\n                // Set UI icons\n                const restartIcon = document.getElementById('restart-icon');\n                if (restartIcon && assetCache['restart_icon']) restartIcon.src = assetCache['restart_icon'].src;\n\n                const star1 = document.getElementById('star1');\n                const star2 = document.getElementById('star2');\n                const star3 = document.getElementById('star3');\n                if (assetCache['star_empty']) {\n                    if (star1) star1.src = assetCache['star_empty'].src;\n                    if (star2) star2.src = assetCache['star_empty'].src;\n                    if (star3) star3.src = assetCache['star_empty'].src;\n                }\n            }\n\n            // Start preloading but don't strictly await it before showing params if we want it to be fast\n            // However, the game needs them to render correctly.\n            // We'll await it for now.\n            await preloadAssets();\n\n            // Play background music\n            let bgMusicSource = null;\n            function playBackgroundMusic() {\n                if (audioBuffers['background_music'] && !bgMusicSource) {\n                    bgMusicSource = audioContext.createBufferSource();\n                    bgMusicSource.buffer = audioBuffers['background_music'];\n                    bgMusicSource.loop = true;\n                    bgMusicSource.connect(audioContext.destination);\n                    bgMusicSource.start(0);\n                    window.currentBananaMusic = bgMusicSource; // Store for cleanup\n                }\n            }\n\n            function playSound(soundId) {\n                if (audioBuffers[soundId]) {\n                    const source = audioContext.createBufferSource();\n                    source.buffer = audioBuffers[soundId];\n                    source.connect(audioContext.destination);\n                    source.start(0);\n                }\n            }\n\n            // ============================================\n            // GAME STATE\n            // ============================================\n\n            let currentLevel = 0;\n            let bananasRemaining = 5;\n            let obstacles = [];\n            let originalObstacles = []; // Keep track of original layout for boss repair\n            let portals = []; // \u6210\u5bf9\u7684\u4f20\u9001\u95e8: [{x1, y1, x2, y2}]\n            let projectiles = [];\n            let particles = [];\n            let gameState = 'playing'; // 'playing', 'won', 'lost'\n            let activeBananaType = 'normal'; // 'normal', 'explosive', 'split', 'boomerang'\n            let boss = null;\n\n            // Input state\n            let isDragging = false;\n            let dragStartPos = { x: 0, y: 0 };\n            let currentDragPos = { x: 0, y: 0 };\n            let launchAngle = 0;\n            let launchPower = 0;\n\n            // Edit mode state\n            let editMode = mode === 'edit';\n            let selectedObstacle = null;\n            let currentTool = 'select';\n            let isDraggingObstacle = false;\n            let dragOffset = { x: 0, y: 0 };\n            let portalPlacementStep = 0; // 0: none, 1: placing A\n            let tempPortalA = null;\n\n            // ============================================\n            // HELPER FUNCTIONS\n            // ============================================\n\n            function scaleToCanvas(x, y) {\n                const scaleX = canvas.width / SCREEN_WIDTH;\n                const scaleY = canvas.height / SCREEN_HEIGHT;\n                return {\n                    x: x * scaleX,\n                    y: y * scaleY,\n                    scaleX: scaleX,\n                    scaleY: scaleY\n                };\n            }\n\n            function getMousePos(e) {\n                const rect = canvas.getBoundingClientRect();\n                const clientX = e.touches ? e.touches[0].clientX : e.clientX;\n                const clientY = e.touches ? e.touches[0].clientY : e.clientY;\n                \n                const scaleX = SCREEN_WIDTH / canvas.width;\n                const scaleY = SCREEN_HEIGHT / canvas.height;\n                \n                return {\n                    x: (clientX - rect.left) * scaleX,\n                    y: (clientY - rect.top) * scaleY\n                };\n            }\n\n            function drawImage(img, x, y, width, height, rotation = 0, flipH = false) {\n                if (!img) return;\n                \n                const scaled = scaleToCanvas(x, y);\n                const scaledW = width * scaled.scaleX;\n                const scaledH = height * scaled.scaleY;\n                \n                ctx.save();\n                ctx.translate(scaled.x + scaledW / 2, scaled.y + scaledH / 2);\n                if (rotation) ctx.rotate(rotation);\n                if (flipH) ctx.scale(-1, 1);\n                ctx.drawImage(img, -scaledW / 2, -scaledH / 2, scaledW, scaledH);\n                ctx.restore();\n            }\n\n            function clamp(value, min, max) {\n                return Math.min(Math.max(value, min), max);\n            }\n\n            // ============================================\n            // GAME INITIALIZATION\n            // ============================================\n\n            function initLevel(levelIndex) {\n                if (window.gameConfig.settings) {\n                    window.gameConfig.settings.selectedLevel = levelIndex;\n                }\n                if (!window.gameConfig.levels || !window.gameConfig.levels[levelIndex]) {\n                    lib.log(`Level ${levelIndex} not found in gameConfig`);\n                    // Create a fallback level if it doesn't exist\n                    if (!window.gameConfig.levels[levelIndex]) {\n                        window.gameConfig.levels[levelIndex] = {\n                            bananaCount: 5,\n                            obstacles: [],\n                            starThresholds: [1, 2, 3]\n                        };\n                    }\n                }\n\n                window.activeBananaLevel = levelIndex;\n                currentLevel = levelIndex;\n                const levelConfig = window.gameConfig.levels[levelIndex];\n                \n                bananasRemaining = levelConfig.bananaCount || 5;\n                obstacles = JSON.parse(JSON.stringify(levelConfig.obstacles || []));\n                originalObstacles = JSON.parse(JSON.stringify(levelConfig.obstacles || []));\n                portals = JSON.parse(JSON.stringify(levelConfig.portals || []));\n                projectiles = [];\n                particles = [];\n                gameState = 'playing';\n                activeBananaType = 'normal';\n\n                // Initialize Boss\n                if (levelConfig.hasBoss) {\n                    boss = {\n                        x: SCREEN_WIDTH / 2 - 60,\n                        startX: SCREEN_WIDTH / 2 - 60,\n                        moveDir: 1,\n                        y: PLAY_ZONE_BOTTOM - 140,\n                        width: 120,\n                        height: 120,\n                        health: currentLevel >= 14 ? 8 : 6, // More health for moving bosses\n                        maxHealth: currentLevel >= 14 ? 8 : 6,\n                        repairTimer: currentLevel >= 7 ? 2.0 : 3.0 // Faster repair in later levels\n                    };\n                    \n                    // Remove obstacles that overlap with boss\n                    obstacles = obstacles.filter(obs => {\n                        const obsRect = {\n                            left: obs.x,\n                            right: obs.x + OBSTACLE_SIZE,\n                            top: obs.y,\n                            bottom: obs.y + OBSTACLE_SIZE\n                        };\n                        const bossRect = {\n                            left: boss.x,\n                            right: boss.x + boss.width,\n                            top: boss.y,\n                            bottom: boss.y + boss.height\n                        };\n                        return !(obsRect.right > bossRect.left && \n                                 obsRect.left < bossRect.right && \n                                 obsRect.bottom > bossRect.top && \n                                 obsRect.top < bossRect.bottom);\n                    });\n                } else {\n                    boss = null;\n                }\n\n                // Initialize Special Obstacles (Level 3+)\n                if (levelIndex >= 2 && levelConfig.specialObstaclesCount) {\n                    const count = levelConfig.specialObstaclesCount;\n                    const indices = [];\n                    while (indices.length < count && indices.length < obstacles.length) {\n                        const r = Math.floor(Math.random() * obstacles.length);\n                        if (!indices.includes(r)) indices.push(r);\n                    }\n                    indices.forEach(idx => obstacles[idx].isSpecial = true);\n                }\n                \n                // Update UI\n                const bananaCountEl = document.getElementById('banana-count');\n                if (bananaCountEl) bananaCountEl.textContent = bananasRemaining;\n                \n                const levelIndicatorEl = document.getElementById('level-indicator');\n                if (levelIndicatorEl) {\n                    const totalLevels = window.gameConfig.levels ? window.gameConfig.levels.length : 10;\n                    levelIndicatorEl.textContent = `Level ${levelIndex + 1}/${totalLevels}`;\n                }\n                \n                const levelSelect = document.getElementById('level-select');\n                if (levelSelect) {\n                    const totalLevels = window.gameConfig.levels ? window.gameConfig.levels.length : 10;\n                    if (levelSelect.options.length !== totalLevels) {\n                        levelSelect.innerHTML = '';\n                        for (let i = 0; i < totalLevels; i++) {\n                            const opt = document.createElement('option');\n                            opt.value = i;\n                            opt.textContent = `Level ${i + 1}`;\n                            levelSelect.appendChild(opt);\n                        }\n                    }\n                    levelSelect.value = levelIndex;\n                }\n                \n                // Hide modals\n                const winModal = document.getElementById('win-modal');\n                const loseModal = document.getElementById('lose-modal');\n                if (winModal) winModal.classList.remove('visible');\n                if (loseModal) loseModal.classList.remove('visible');\n\n                // Update physics from config\n                const settings = window.gameConfig.settings || {\n                    gravity: 800,\n                    wallBounceRetention: 0.9,\n                    obstacleBounceRetention: 0.5\n                };\n                GRAVITY = settings.gravity;\n                WALL_BOUNCE_RETENTION = settings.wallBounceRetention;\n                OBSTACLE_BOUNCE_RETENTION = settings.obstacleBounceRetention;\n            }\n\n            // ============================================\n            // PHYSICS & COLLISION\n            // ============================================\n\n            function updateProjectiles(deltaTime) {\n                for (let i = projectiles.length - 1; i >= 0; i--) {\n                    const proj = projectiles[i];\n                    \n                    // Apply gravity\n                    proj.vy += GRAVITY * deltaTime;\n                    \n                    // Update position\n                    if (proj.type === 'boomerang') {\n                        if (proj.state === 'down') {\n                            proj.y += proj.vy * deltaTime;\n                            // Check collisions (but don't stop)\n                            for (let j = obstacles.length - 1; j >= 0; j--) {\n                                const obs = obstacles[j];\n                                const col = getCollisionInfo(proj, obs);\n                                if (col.collided) {\n                                    destroyObstacle(j);\n                                }\n                            }\n                            if (boss) {\n                                const col = getCollisionInfo(proj, boss, true);\n                                if (col.collided) {\n                                    boss.health--;\n                                    createImpactParticles(proj.x, proj.y, 5);\n                                    if (boss.health <= 0) boss = null;\n                                }\n                            }\n                            if (proj.y > PLAY_ZONE_BOTTOM) {\n                                proj.state = 'up';\n                                proj.vy = -1200;\n                            }\n                        } else {\n                            proj.y += proj.vy * deltaTime;\n                            if (proj.y <= proj.startY) {\n                                projectiles.splice(i, 1);\n                                bananasRemaining++;\n                                document.getElementById('banana-count').textContent = bananasRemaining;\n                            }\n                        }\n                        continue;\n                    }\n\n                    proj.x += proj.vx * deltaTime;\n                    proj.y += proj.vy * deltaTime;\n\n                    // Portal logic\n                    for (let p of portals) {\n                        const portalSize = 60;\n                        const distA = Math.sqrt((proj.x - (p.x1 + portalSize/2))**2 + (proj.y - (p.y1 + portalSize/2))**2);\n                        if (distA < 30 && !proj.lastPortal) {\n                            proj.x = p.x2 + portalSize/2;\n                            proj.y = p.y2 + portalSize/2;\n                            proj.lastPortal = p;\n                            createImpactParticles(p.x1 + portalSize/2, p.y1 + portalSize/2, 5);\n                            createImpactParticles(p.x2 + portalSize/2, p.y2 + portalSize/2, 5);\n                            playSound('launch_sound');\n                            break;\n                        }\n                        \n                        const distB = Math.sqrt((proj.x - (p.x2 + portalSize/2))**2 + (proj.y - (p.y2 + portalSize/2))**2);\n                        if (distB < 30 && !proj.lastPortal) {\n                            proj.x = p.x1 + portalSize/2;\n                            proj.y = p.y1 + portalSize/2;\n                            proj.lastPortal = p;\n                            createImpactParticles(p.x1 + portalSize/2, p.y1 + portalSize/2, 5);\n                            createImpactParticles(p.x2 + portalSize/2, p.y2 + portalSize/2, 5);\n                            playSound('launch_sound');\n                            break;\n                        }\n\n                        // Reset lastPortal if we moved away\n                        if (proj.lastPortal === p && distA > 40 && distB > 40) {\n                            proj.lastPortal = null;\n                        }\n                    }\n                    \n                    // Update rotation based on velocity\n                    proj.rotation = Math.atan2(proj.vy, proj.vx);\n                    \n                    const velocity = { x: proj.vx, y: proj.vy };\n\n                    // Wall collision\n                    if (proj.x - proj.width / 2 < WALL_WIDTH) {\n                        proj.x = WALL_WIDTH + proj.width / 2;\n                        const reflected = Vec2.reflect(velocity, { x: 1, y: 0 }, WALL_BOUNCE_RETENTION);\n                        proj.vx = reflected.x;\n                        proj.vy = reflected.y;\n                        createImpactParticles(proj.x, proj.y, 3);\n                        playSound('wall_bounce_sound');\n                    } else if (proj.x + proj.width / 2 > SCREEN_WIDTH - WALL_WIDTH) {\n                        proj.x = SCREEN_WIDTH - WALL_WIDTH - proj.width / 2;\n                        const reflected = Vec2.reflect(velocity, { x: -1, y: 0 }, WALL_BOUNCE_RETENTION);\n                        proj.vx = reflected.x;\n                        proj.vy = reflected.y;\n                        createImpactParticles(proj.x, proj.y, 3);\n                        playSound('wall_bounce_sound');\n                    }\n                    \n                    // Obstacle collision\n                    for (let j = obstacles.length - 1; j >= 0; j--) {\n                        const obs = obstacles[j];\n                        const col = getCollisionInfo(proj, obs);\n                        if (col.collided) {\n                            // Apply bounce\n                            const reflected = Vec2.reflect({ x: proj.vx, y: proj.vy }, col.normal, OBSTACLE_BOUNCE_RETENTION);\n                            proj.vx = reflected.x;\n                            proj.vy = reflected.y;\n                            \n                            // Nudge out of collision\n                            proj.x += col.normal.x * (col.depth + 1);\n                            proj.y += col.normal.y * (col.depth + 1);\n\n                            if (proj.type === 'explosive') {\n                                handleExplosion(proj.x, proj.y);\n                                projectiles.splice(i, 1);\n                                break;\n                            } else {\n                                destroyObstacle(j);\n                            }\n                            break;\n                        }\n                    }\n\n                    // Boss collision\n                    if (boss) {\n                        const col = getCollisionInfo(proj, boss, true);\n                        if (col.collided) {\n                            const reflected = Vec2.reflect({ x: proj.vx, y: proj.vy }, col.normal, OBSTACLE_BOUNCE_RETENTION);\n                            proj.vx = reflected.x;\n                            proj.vy = reflected.y;\n                            \n                            proj.x += col.normal.x * (col.depth + 1);\n                            proj.y += col.normal.y * (col.depth + 1);\n                            \n                            boss.health--;\n                            createImpactParticles(proj.x, proj.y, 5);\n                            playSound('impact_star');\n                            if (boss.health <= 0) {\n                                createDestructionParticles(boss.x + boss.width/2, boss.y + boss.height/2, 'wood', 20);\n                                boss = null;\n                            }\n\n                            if (proj.type === 'explosive') {\n                                handleExplosion(proj.x, proj.y);\n                                projectiles.splice(i, 1);\n                            }\n                        }\n                    }\n                    \n                    // Remove if off screen\n                    if (proj.y > SCREEN_HEIGHT + 100 || proj.y < -100) {\n                        projectiles.splice(i, 1);\n                    }\n                }\n            }\n\n            function getCollisionInfo(proj, obj, isBoss = false) {\n                const width = isBoss ? obj.width : (OBSTACLE_SIZE * (obj.scale || 1));\n                const height = isBoss ? obj.height : (OBSTACLE_SIZE * (obj.scale || 1));\n                \n                const rect = {\n                    left: obj.x,\n                    right: obj.x + width,\n                    top: obj.y,\n                    bottom: obj.y + height\n                };\n\n                const radius = proj.width / 2;\n                const closestX = Math.max(rect.left, Math.min(proj.x, rect.right));\n                const closestY = Math.max(rect.top, Math.min(proj.y, rect.bottom));\n\n                const dx = proj.x - closestX;\n                const dy = proj.y - closestY;\n                const distanceSq = dx * dx + dy * dy;\n\n                if (distanceSq < radius * radius) {\n                    const distance = Math.sqrt(distanceSq);\n                    let normal;\n                    \n                    if (distance < 0.001) {\n                        const dl = proj.x - rect.left;\n                        const dr = rect.right - proj.x;\n                        const dt = proj.y - rect.top;\n                        const db = rect.bottom - proj.y;\n                        const min = Math.min(dl, dr, dt, db);\n                        if (min === dl) normal = { x: -1, y: 0 };\n                        else if (min === dr) normal = { x: 1, y: 0 };\n                        else if (min === dt) normal = { x: 0, y: -1 };\n                        else normal = { x: 0, y: 1 };\n                    } else {\n                        normal = { x: dx / distance, y: dy / distance };\n                    }\n                    \n                    return { collided: true, normal: normal, depth: radius - distance };\n                }\n                return { collided: false };\n            }\n\n\n            function handleExplosion(x, y) {\n                playSound('explosion_sound');\n                \n                // Add explosion visual\n                particles.push({\n                    x: x,\n                    y: y,\n                    vx: 0,\n                    vy: 0,\n                    rotation: 0,\n                    rotationSpeed: 0,\n                    life: 0.6,\n                    image: assetCache['explosion_particle'],\n                    size: 300\n                });\n\n                // Clear area (approx 150 radius)\n                const radius = 150; \n                for (let j = obstacles.length - 1; j >= 0; j--) {\n                    const obs = obstacles[j];\n                    if (!obs) continue;\n                    const centerX = obs.x + (OBSTACLE_SIZE * (obs.scale || 1)) / 2;\n                    const centerY = obs.y + (OBSTACLE_SIZE * (obs.scale || 1)) / 2;\n                    const dist = Math.sqrt((centerX - x)**2 + (centerY - y)**2);\n                    \n                    if (dist < radius) {\n                        destroyObstacle(j);\n                    }\n                }\n\n                // Damage boss if in radius\n                if (boss) {\n                    const centerX = boss.x + boss.width / 2;\n                    const centerY = boss.y + boss.height / 2;\n                    const dist = Math.sqrt((centerX - x)**2 + (centerY - y)**2);\n                    if (dist < radius + 50) {\n                        boss.health -= 2;\n                        if (boss.health <= 0) boss = null;\n                    }\n                }\n            }\n\n            function destroyObstacle(index) {\n                const obs = obstacles[index];\n                if (!obs) return;\n\n                const isExplosive = obs.type === 'explosive';\n                const exX = obs.x + OBSTACLE_SIZE / 2;\n                const exY = obs.y + OBSTACLE_SIZE / 2;\n\n                const particleType = obs.type === 'glass' ? 'glass' : 'wood';\n                const particleCount = obs.type === 'glass' ? 10 : 6;\n                \n                if (obs.type === 'boomerang_crate') {\n                    activeBananaType = 'boomerang';\n                }\n\n                if (obs.isSpecial) {\n                    if (currentLevel >= 2) { // Level 3 and above (0-indexed)\n                        activeBananaType = 'split';\n                    } else {\n                        activeBananaType = 'explosive';\n                    }\n                }\n\n                if (obs.type === 'splitting') {\n                    const size = OBSTACLE_SIZE * (obs.scale || 1);\n                    const newScale = (obs.scale || 1) * 0.6;\n                    const offset = size * 0.3;\n                    \n                    obstacles.push({\n                        x: obs.x - offset,\n                        y: obs.y + offset,\n                        type: obs.subType || 'crate',\n                        scale: newScale,\n                        isSpecial: obs.isSpecial\n                    });\n                    obstacles.push({\n                        x: obs.x + offset,\n                        y: obs.y + offset,\n                        type: obs.subType || 'crate',\n                        scale: newScale\n                    });\n                }\n\n                createDestructionParticles(\n                    obs.x + (OBSTACLE_SIZE * (obs.scale || 1)) / 2,\n                    obs.y + (OBSTACLE_SIZE * (obs.scale || 1)) / 2,\n                    particleType,\n                    particleCount\n                );\n                \n                playSound(obs.type === 'glass' ? 'glass_shatter_sound' : 'wood_break_sound');\n                \n                obstacles.splice(index, 1);\n\n                if (isExplosive) {\n                    handleExplosion(exX, exY);\n                }\n            }\n\n            // ============================================\n            // PARTICLES\n            // ============================================\n\n            function createDestructionParticles(x, y, type, count) {\n                const particleImg = type === 'wood' ? \n                    assetCache['wood_splinter_particle'] : \n                    assetCache['glass_shard_particle'];\n                \n                for (let i = 0; i < count; i++) {\n                    const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;\n                    const speed = 100 + Math.random() * 200;\n                    \n                    particles.push({\n                        x: x,\n                        y: y,\n                        vx: Math.cos(angle) * speed,\n                        vy: Math.sin(angle) * speed - 100,\n                        rotation: Math.random() * Math.PI * 2,\n                        rotationSpeed: (Math.random() - 0.5) * 10,\n                        life: 1.0,\n                        image: particleImg,\n                        size: 12\n                    });\n                }\n            }\n\n            function createImpactParticles(x, y, count) {\n                const particleImg = assetCache['impact_star'];\n                \n                for (let i = 0; i < count; i++) {\n                    const angle = Math.random() * Math.PI * 2;\n                    const speed = 50 + Math.random() * 100;\n                    \n                    particles.push({\n                        x: x,\n                        y: y,\n                        vx: Math.cos(angle) * speed,\n                        vy: Math.sin(angle) * speed,\n                        rotation: 0,\n                        rotationSpeed: 0,\n                        life: 0.5,\n                        image: particleImg,\n                        size: 24\n                    });\n                }\n            }\n\n            function updateBoss(deltaTime) {\n                if (!boss || gameState !== 'playing') return;\n\n                // Horizontal movement for Level 15+ (index 14+)\n                if (currentLevel >= 14) {\n                    const moveRange = 200;\n                    const moveSpeed = 120 + (currentLevel - 14) * 15;\n                    boss.x += boss.moveDir * moveSpeed * deltaTime;\n                    \n                    if (boss.x > boss.startX + moveRange) {\n                        boss.x = boss.startX + moveRange;\n                        boss.moveDir = -1;\n                    } else if (boss.x < boss.startX - moveRange) {\n                        boss.x = boss.startX - moveRange;\n                        boss.moveDir = 1;\n                    }\n                }\n\n                boss.repairTimer -= deltaTime;\n                if (boss.repairTimer <= 0) {\n                    boss.repairTimer = 4.0; // Reset timer\n\n                    // Find a missing obstacle to repair\n                    const missing = originalObstacles.filter(orig => {\n                        return !obstacles.some(curr => curr.x === orig.x && curr.y === orig.y);\n                    });\n\n                    if (missing.length > 0) {\n                        const toRepair = missing[Math.floor(Math.random() * missing.length)];\n                        obstacles.push(JSON.parse(JSON.stringify(toRepair)));\n                        // Effect\n                        createImpactParticles(toRepair.x + OBSTACLE_SIZE/2, toRepair.y + OBSTACLE_SIZE/2, 5);\n                    }\n                }\n            }\n\n            function updateParticles(deltaTime) {\n                for (let i = particles.length - 1; i >= 0; i--) {\n                    const p = particles[i];\n                    \n                    p.x += p.vx * deltaTime;\n                    p.y += p.vy * deltaTime;\n                    p.vy += GRAVITY * deltaTime * 0.5;\n                    p.rotation += p.rotationSpeed * deltaTime;\n                    p.life -= deltaTime * 2;\n                    \n                    if (p.life <= 0) {\n                        particles.splice(i, 1);\n                    }\n                }\n            }\n\n            // ============================================\n            // LAUNCH MECHANICS\n            // ============================================\n\n            function launchBanana() {\n                if (bananasRemaining <= 0 || gameState !== 'playing') return;\n                \n                const power = Math.min(launchPower, 1000);\n                const angle = launchAngle;\n                \n                const projectileType = activeBananaType;\n                const assetId = (projectileType === 'explosive') ? 'explosive_banana_projectile' : 'peeled_banana_projectile';\n                const assetInfo = lib.getAsset(assetId);\n                const aspectRatio = (assetInfo && assetInfo.aspect_ratio) ? assetInfo.aspect_ratio : [40, 80];\n                const projWidth = 40;\n                const projHeight = projWidth * (aspectRatio[1] / aspectRatio[0]);\n                \n                if (projectileType === 'boomerang') {\n                    projectiles.push({\n                        x: SCREEN_WIDTH / 2,\n                        y: LAUNCHER_ZONE_HEIGHT,\n                        vx: 0,\n                        vy: 1000,\n                        width: 60,\n                        height: 60,\n                        rotation: Math.PI / 2,\n                        type: 'boomerang',\n                        state: 'down',\n                        startY: LAUNCHER_ZONE_HEIGHT\n                    });\n                } else if (projectileType === 'split') {\n                    // Create 5 projectiles in a fan, make them larger (80px)\n                    const splitWidth = 80;\n                    const splitHeight = splitWidth * (aspectRatio[1] / aspectRatio[0]);\n                    for (let i = -2; i <= 2; i++) {\n                        const fanAngle = angle + (i * 0.15);\n                        projectiles.push({\n                            x: SCREEN_WIDTH / 2,\n                            y: LAUNCHER_ZONE_HEIGHT,\n                            vx: Math.cos(fanAngle) * power,\n                            vy: Math.sin(fanAngle) * power,\n                            width: splitWidth,\n                            height: splitHeight,\n                            rotation: fanAngle,\n                            type: 'normal' // Split parts are normal bananas\n                        });\n                    }\n                } else {\n                    projectiles.push({\n                        x: SCREEN_WIDTH / 2,\n                        y: LAUNCHER_ZONE_HEIGHT,\n                        vx: Math.cos(angle) * power,\n                        vy: Math.sin(angle) * power,\n                        width: projWidth,\n                        height: projHeight,\n                        rotation: angle,\n                        type: projectileType\n                    });\n                }\n                \n                // Consume special banana\n                if (activeBananaType !== 'normal') {\n                    activeBananaType = 'normal';\n                }\n\n                bananasRemaining--;\n                document.getElementById('banana-count').textContent = bananasRemaining;\n                \n                playSound('launch_sound');\n                \n                // Check lose condition\n                if (bananasRemaining === 0 && obstacles.length > 0) {\n                    setTimeout(() => {\n                        if (projectiles.length === 0 && obstacles.length > 0 && gameState === 'playing') {\n                            gameState = 'lost';\n                            showLoseScreen();\n                        }\n                    }, 3000);\n                }\n            }\n\n            function predictTrajectory(startX, startY, vx, vy, steps = 25) {\n                const points = [];\n                let x = startX;\n                let y = startY;\n                let velX = vx;\n                let velY = vy;\n                const dt = 0.1;\n                const projMock = { x, y, width: 40 }; // Simplified width for prediction\n                \n                for (let i = 0; i < steps; i++) {\n                    points.push({ x, y });\n                    \n                    velY += GRAVITY * dt;\n                    x += velX * dt;\n                    y += velY * dt;\n                    projMock.x = x;\n                    projMock.y = y;\n                    \n                    // Wall bounce\n                    if (x < WALL_WIDTH + 20) {\n                        const reflected = Vec2.reflect({ x: velX, y: velY }, { x: 1, y: 0 }, WALL_BOUNCE_RETENTION);\n                        velX = reflected.x;\n                        velY = reflected.y;\n                        x = WALL_WIDTH + 20;\n                    } else if (x > SCREEN_WIDTH - WALL_WIDTH - 20) {\n                        const reflected = Vec2.reflect({ x: velX, y: velY }, { x: -1, y: 0 }, WALL_BOUNCE_RETENTION);\n                        velX = reflected.x;\n                        velY = reflected.y;\n                        x = SCREEN_WIDTH - WALL_WIDTH - 20;\n                    }\n                    \n                    // Obstacle prediction (optional, but keep it simple to walls for performance)\n                    \n                    if (y > SCREEN_HEIGHT) break;\n                }\n                \n                return points;\n            }\n\n            // ============================================\n            // INPUT HANDLING\n            // ============================================\n\n            function handlePointerDown(e) {\n                if (window.lastBananaRunId !== runId) return;\n                \n                // If a modal is open, don't allow gameplay interaction\n                if (document.querySelector('.modal-overlay.visible')) return;\n\n                // Ensure we only handle direct canvas clicks\n                if (e.target !== canvas) return;\n\n                e.preventDefault();\n                const pos = getMousePos(e);\n                \n                if (editMode) {\n                    handleEditPointerDown(pos);\n                } else if (gameState === 'playing') {\n                    isDragging = true;\n                    dragStartPos = pos;\n                    currentDragPos = pos;\n                }\n            }\n\n            function handlePointerMove(e) {\n                if (window.lastBananaRunId !== runId) return;\n                e.preventDefault();\n                const pos = getMousePos(e);\n                \n                if (editMode) {\n                    handleEditPointerMove(pos);\n                } else if (isDragging) {\n                    currentDragPos = pos;\n                    \n                    const dx = currentDragPos.x - dragStartPos.x;\n                    const dy = currentDragPos.y - dragStartPos.y;\n                    \n                    launchAngle = Math.atan2(dy, dx);\n                    launchPower = Math.min(Math.sqrt(dx * dx + dy * dy) * 5, 1000);\n                    \n                    // Clamp angle to wider downward range (almost 180 degrees)\n                    launchAngle = clamp(launchAngle, 0.1, Math.PI - 0.1);\n                }\n            }\n\n            function handlePointerUp(e) {\n                if (window.lastBananaRunId !== runId) return;\n                e.preventDefault();\n                \n                if (editMode) {\n                    handleEditPointerUp();\n                } else if (isDragging) {\n                    isDragging = false;\n                    if (launchPower > 50) {\n                        launchBanana();\n                    }\n                    launchPower = 0;\n                }\n            }\n\n            canvas.addEventListener('mousedown', handlePointerDown);\n            canvas.addEventListener('mousemove', handlePointerMove);\n            canvas.addEventListener('mouseup', handlePointerUp);\n            canvas.addEventListener('touchstart', handlePointerDown, { passive: false });\n            canvas.addEventListener('touchmove', handlePointerMove, { passive: false });\n            canvas.addEventListener('touchend', handlePointerUp);\n\n            // ============================================\n            // EDIT MODE\n            // ============================================\n\n            function handleEditPointerDown(pos) {\n                if (currentTool === 'select') {\n                    selectedObstacle = null;\n                    for (let i = obstacles.length - 1; i >= 0; i--) {\n                        const obs = obstacles[i];\n                        if (pos.x >= obs.x && pos.x <= obs.x + OBSTACLE_SIZE &&\n                            pos.y >= obs.y && pos.y <= obs.y + OBSTACLE_SIZE) {\n                            selectedObstacle = i;\n                            isDraggingObstacle = true;\n                            dragOffset = {\n                                x: pos.x - obs.x,\n                                y: pos.y - obs.y\n                            };\n                            break;\n                        }\n                    }\n                } else if (currentTool === 'delete') {\n                    // Delete portals\n                    for (let i = portals.length - 1; i >= 0; i--) {\n                        const p = portals[i];\n                        const ps = 60;\n                        if ((pos.x >= p.x1 && pos.x <= p.x1 + ps && pos.y >= p.y1 && pos.y <= p.y1 + ps) ||\n                            (pos.x >= p.x2 && pos.x <= p.x2 + ps && pos.y >= p.y2 && pos.y <= p.y2 + ps)) {\n                            portals.splice(i, 1);\n                            window.gameConfig.levels[currentLevel].portals = JSON.parse(JSON.stringify(portals));\n                            return;\n                        }\n                    }\n                    for (let i = obstacles.length - 1; i >= 0; i--) {\n                        const obs = obstacles[i];\n                        if (pos.x >= obs.x && pos.x <= obs.x + OBSTACLE_SIZE &&\n                            pos.y >= obs.y && pos.y <= obs.y + OBSTACLE_SIZE) {\n                            obstacles.splice(i, 1);\n                            window.gameConfig.levels[currentLevel].obstacles = \n                                JSON.parse(JSON.stringify(obstacles));\n                            break;\n                        }\n                    }\n                } else if (currentTool === 'crate' || currentTool === 'glass' || currentTool === 'explosive') {\n                    // Add obstacle\n                    if (pos.y >= PLAY_ZONE_TOP && pos.y <= PLAY_ZONE_BOTTOM) {\n                        const gridX = Math.floor((pos.x - WALL_WIDTH) / GRID_SIZE) * GRID_SIZE + WALL_WIDTH;\n                        const gridY = Math.floor((pos.y - PLAY_ZONE_TOP) / GRID_SIZE) * GRID_SIZE + PLAY_ZONE_TOP;\n                        \n                        // Check if position is free\n                        let canPlace = true;\n                        for (const obs of obstacles) {\n                            if (Math.abs(obs.x - gridX) < OBSTACLE_SIZE &&\n                                Math.abs(obs.y - gridY) < OBSTACLE_SIZE) {\n                                canPlace = false;\n                                break;\n                            }\n                        }\n                        \n                        if (canPlace) {\n                            obstacles.push({\n                                x: gridX,\n                                y: gridY,\n                                type: currentTool\n                            });\n                            window.gameConfig.levels[currentLevel].obstacles = \n                                JSON.parse(JSON.stringify(obstacles));\n                        }\n                    }\n                } else if (currentTool === 'portal') {\n                    if (pos.y >= PLAY_ZONE_TOP && pos.y <= PLAY_ZONE_BOTTOM) {\n                        const gridX = Math.floor((pos.x - WALL_WIDTH) / GRID_SIZE) * GRID_SIZE + WALL_WIDTH;\n                        const gridY = Math.floor((pos.y - PLAY_ZONE_TOP) / GRID_SIZE) * GRID_SIZE + PLAY_ZONE_TOP;\n                        \n                        if (portalPlacementStep === 0) {\n                            tempPortalA = { x: gridX, y: gridY };\n                            portalPlacementStep = 1;\n                        } else {\n                            portals.push({\n                                x1: tempPortalA.x,\n                                y1: tempPortalA.y,\n                                x2: gridX,\n                                y2: gridY\n                            });\n                            window.gameConfig.levels[currentLevel].portals = JSON.parse(JSON.stringify(portals));\n                            portalPlacementStep = 0;\n                            tempPortalA = null;\n                        }\n                    }\n                }\n            }\n\n            function handleEditPointerMove(pos) {\n                if (isDraggingObstacle && selectedObstacle !== null) {\n                    const obs = obstacles[selectedObstacle];\n                    const newX = pos.x - dragOffset.x;\n                    const newY = pos.y - dragOffset.y;\n                    \n                    // Snap to grid\n                    obs.x = Math.floor((newX - WALL_WIDTH) / GRID_SIZE) * GRID_SIZE + WALL_WIDTH;\n                    obs.y = Math.floor((newY - PLAY_ZONE_TOP) / GRID_SIZE) * GRID_SIZE + PLAY_ZONE_TOP;\n                    \n                    // Clamp to play area\n                    obs.x = clamp(obs.x, WALL_WIDTH, SCREEN_WIDTH - WALL_WIDTH - OBSTACLE_SIZE);\n                    obs.y = clamp(obs.y, PLAY_ZONE_TOP, PLAY_ZONE_BOTTOM - OBSTACLE_SIZE);\n                }\n            }\n\n            function handleEditPointerUp() {\n                if (isDraggingObstacle) {\n                    window.gameConfig.levels[currentLevel].obstacles = \n                        JSON.parse(JSON.stringify(obstacles));\n                }\n                isDraggingObstacle = false;\n            }\n\n            // Edit mode UI\n            if (editMode) {\n                document.getElementById('edit-controls').style.display = 'flex';\n                document.getElementById('play-controls').style.display = 'none';\n                \n                document.getElementById('select-tool').onclick = () => {\n                    if (window.lastBananaRunId !== runId) return;\n                    currentTool = 'select';\n                    updateToolButtons();\n                };\n                document.getElementById('crate-tool').onclick = () => {\n                    if (window.lastBananaRunId !== runId) return;\n                    currentTool = 'crate';\n                    updateToolButtons();\n                };\n                document.getElementById('glass-tool').onclick = () => {\n                    if (window.lastBananaRunId !== runId) return;\n                    currentTool = 'glass';\n                    updateToolButtons();\n                };\n                document.getElementById('explosive-tool').onclick = () => {\n                    if (window.lastBananaRunId !== runId) return;\n                    currentTool = 'explosive';\n                    updateToolButtons();\n                };\n                document.getElementById('portal-tool').onclick = () => {\n                    if (window.lastBananaRunId !== runId) return;\n                    currentTool = 'portal';\n                    portalPlacementStep = 0;\n                    updateToolButtons();\n                };\n                document.getElementById('delete-tool').onclick = () => {\n                    if (window.lastBananaRunId !== runId) return;\n                    currentTool = 'delete';\n                    updateToolButtons();\n                };\n                document.getElementById('add-level-tool').onclick = () => {\n                    if (window.lastBananaRunId !== runId) return;\n                    const newLvl = {\n                        bananaCount: 5,\n                        obstacles: [],\n                        portals: [],\n                        starThresholds: [1, 2, 3]\n                    };\n                    window.gameConfig.levels.push(newLvl);\n                    setupGameParameters();\n                    initLevel(window.gameConfig.levels.length - 1);\n                };\n                \n                function updateToolButtons() {\n                    document.querySelectorAll('.tool-button').forEach(btn => {\n                        btn.classList.remove('active');\n                    });\n                    if (currentTool === 'select') {\n                        document.getElementById('select-tool').classList.add('active');\n                    } else if (currentTool === 'crate') {\n                        document.getElementById('crate-tool').classList.add('active');\n                    } else if (currentTool === 'glass') {\n                        document.getElementById('glass-tool').classList.add('active');\n                    } else if (currentTool === 'explosive') {\n                        document.getElementById('explosive-tool').classList.add('active');\n                    } else if (currentTool === 'portal') {\n                        document.getElementById('portal-tool').classList.add('active');\n                    } else if (currentTool === 'delete') {\n                        document.getElementById('delete-tool').classList.add('active');\n                    }\n                }\n                updateToolButtons();\n                \n                document.getElementById('level-select').onchange = (e) => {\n                    if (window.lastBananaRunId !== runId) return;\n                    initLevel(parseInt(e.target.value));\n                };\n            } else {\n                document.getElementById('edit-controls').style.display = 'none';\n                document.getElementById('play-controls').style.display = 'flex';\n                playBackgroundMusic();\n            }\n\n            // ============================================\n            // UI HANDLERS\n            // ============================================\n\n            function showWinScreen() {\n                playSound('victory_sound');\n                \n                // Calculate stars\n                const levelConfig = window.gameConfig.levels[currentLevel] || { starThresholds: [1, 2, 3] };\n                const thresholds = levelConfig.starThresholds || [1, 2, 3];\n                let stars = 1;\n                if (bananasRemaining >= (thresholds[1] || 2)) stars = 2;\n                if (bananasRemaining >= (thresholds[2] || 3)) stars = 3;\n                \n                // Update star display\n                const star1 = document.getElementById('star1');\n                const star2 = document.getElementById('star2');\n                const star3 = document.getElementById('star3');\n                \n                const starFilled = assetCache['star_filled'];\n                const starEmpty = assetCache['star_empty'];\n                \n                if (stars >= 1 && starFilled) star1.src = starFilled.src;\n                if (stars >= 2 && starFilled) star2.src = starFilled.src;\n                if (stars >= 3 && starFilled) star3.src = starFilled.src;\n                \n                if (stars < 2 && starEmpty) star2.src = starEmpty.src;\n                if (stars < 3 && starEmpty) star3.src = starEmpty.src;\n                \n                // Show modal\n                setTimeout(() => {\n                    document.getElementById('win-modal').classList.add('visible');\n                }, 500);\n                \n                // Enable/disable next button\n                const nextBtn = document.getElementById('next-button');\n                const totalLevels = window.gameConfig.levels ? window.gameConfig.levels.length : 10;\n                if (currentLevel >= totalLevels - 1) {\n                    nextBtn.disabled = true;\n                    nextBtn.textContent = 'Complete!';\n                } else {\n                    nextBtn.disabled = false;\n                    nextBtn.textContent = 'Next Level';\n                }\n            }\n\n            function showLoseScreen() {\n                playSound('fail_sound');\n                setTimeout(() => {\n                    document.getElementById('lose-modal').classList.add('visible');\n                }, 500);\n            }\n\n            document.getElementById('restart-button').onclick = (e) => {\n                if (window.lastBananaRunId !== runId) return;\n                e.stopPropagation();\n                initLevel(currentLevel);\n            };\n\n            document.getElementById('retry-button').onclick = (e) => {\n                if (window.lastBananaRunId !== runId) return;\n                e.stopPropagation();\n                initLevel(currentLevel);\n            };\n\n            document.getElementById('lose-retry-button').onclick = (e) => {\n                if (window.lastBananaRunId !== runId) return;\n                e.stopPropagation();\n                initLevel(currentLevel);\n            };\n\n            document.getElementById('next-button').onclick = (e) => {\n                if (window.lastBananaRunId !== runId) return;\n                e.stopPropagation();\n                if (!document.getElementById('win-modal').classList.contains('visible')) return;\n                const totalLevels = window.gameConfig.levels ? window.gameConfig.levels.length : 10;\n                if (currentLevel < totalLevels - 1) {\n                    initLevel(currentLevel + 1);\n                }\n            };\n\n            // ============================================\n            // RENDERING\n            // ============================================\n            function render() {\n                ctx.clearRect(0, 0, canvas.width, canvas.height);\n                \n                // Draw background gradient (already in CSS)\n                \n                // Draw walls\n                if (assetCache['wooden_wall_texture']) {\n                    const wallImg = assetCache['wooden_wall_texture'];\n                    // Left wall\n                    for (let y = 0; y < SCREEN_HEIGHT; y += 100) {\n                        drawImage(wallImg, 0, y, WALL_WIDTH, 100);\n                    }\n                    // Right wall\n                    for (let y = 0; y < SCREEN_HEIGHT; y += 100) {\n                        drawImage(wallImg, SCREEN_WIDTH - WALL_WIDTH, y, WALL_WIDTH, 100);\n                    }\n                }\n                \n                // Draw horizontal plank\n                if (assetCache['wooden_plank_long']) {\n                    const plankImg = assetCache['wooden_plank_long'];\n                    const plankWidth = SCREEN_WIDTH - (WALL_WIDTH * 2);\n                    const plankHeight = 40;\n                    drawImage(plankImg, WALL_WIDTH, LAUNCHER_ZONE_HEIGHT - 10, plankWidth, plankHeight);\n                }\n\n                // Draw portals\n                for (const p of portals) {\n                    const ps = 60;\n                    // Entry A\n                    ctx.save();\n                    const scaledA = scaleToCanvas(p.x1 + ps/2, p.y1 + ps/2);\n                    const gradientA = ctx.createRadialGradient(scaledA.x, scaledA.y, 0, scaledA.x, scaledA.y, 30 * scaledA.scaleX);\n                    gradientA.addColorStop(0, 'rgba(0, 255, 255, 0.8)');\n                    gradientA.addColorStop(1, 'rgba(0, 100, 255, 0.2)');\n                    ctx.fillStyle = gradientA;\n                    ctx.beginPath();\n                    ctx.ellipse(scaledA.x, scaledA.y, 25 * scaledA.scaleX, 35 * scaledA.scaleY, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    ctx.strokeStyle = 'white';\n                    ctx.lineWidth = 2;\n                    ctx.stroke();\n                    ctx.restore();\n\n                    // Exit B\n                    ctx.save();\n                    const scaledB = scaleToCanvas(p.x2 + ps/2, p.y2 + ps/2);\n                    const gradientB = ctx.createRadialGradient(scaledB.x, scaledB.y, 0, scaledB.x, scaledB.y, 30 * scaledB.scaleX);\n                    gradientB.addColorStop(0, 'rgba(255, 165, 0, 0.8)');\n                    gradientB.addColorStop(1, 'rgba(255, 69, 0, 0.2)');\n                    ctx.fillStyle = gradientB;\n                    ctx.beginPath();\n                    ctx.ellipse(scaledB.x, scaledB.y, 25 * scaledB.scaleX, 35 * scaledB.scaleY, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    ctx.strokeStyle = 'white';\n                    ctx.lineWidth = 2;\n                    ctx.stroke();\n                    ctx.restore();\n                }\n\n                // Draw temp portal in edit mode\n                if (editMode && portalPlacementStep === 1 && tempPortalA) {\n                    const ps = 60;\n                    ctx.save();\n                    const scaled = scaleToCanvas(tempPortalA.x + ps/2, tempPortalA.y + ps/2);\n                    ctx.globalAlpha = 0.5;\n                    ctx.fillStyle = 'cyan';\n                    ctx.beginPath();\n                    ctx.ellipse(scaled.x, scaled.y, 25 * scaled.scaleX, 35 * scaled.scaleY, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    ctx.restore();\n                }\n\n                // Draw launcher (Banana Man)\n                if (assetCache['banana_man'] && !editMode) {\n                    const launcherImg = assetCache['banana_man'];\n                    const assetInfo = lib.getAsset('banana_man');\n                    const aspectRatio = (assetInfo && assetInfo.aspect_ratio) ? assetInfo.aspect_ratio : [1, 1.5];\n                    const charHeight = 120;\n                    const charWidth = charHeight * (aspectRatio[0] / aspectRatio[1]);\n                    // Position him on the plank\n                    drawImage(launcherImg, SCREEN_WIDTH / 2 - charWidth / 2, LAUNCHER_ZONE_HEIGHT - charHeight + 10, charWidth, charHeight);\n                }\n                \n                // Draw obstacles\n                for (const obs of obstacles) {\n                    const img = (obs.type === 'splitting' || obs.type === 'crate' || obs.type === 'explosive' || obs.type === 'boomerang_crate') ? \n                        assetCache['wooden_crate'] : \n                        assetCache['glass_block'];\n                    \n                    if (img) {\n                        const size = OBSTACLE_SIZE * (obs.scale || 1);\n                        drawImage(img, obs.x, obs.y, size, size);\n                        \n                        // Draw boomerang indicator\n                        if (obs.type === 'boomerang_crate') {\n                            ctx.save();\n                            const scaled = scaleToCanvas(obs.x + size/2, obs.y + size/2);\n                            ctx.fillStyle = '#00ffcc';\n                            ctx.beginPath();\n                            ctx.arc(scaled.x, scaled.y, 10 * scaled.scaleX, 0, Math.PI * 2);\n                            ctx.fill();\n                            ctx.restore();\n                        }\n\n                        // Draw explosive indicator\n                        if (obs.type === 'explosive') {\n                            ctx.save();\n                            const scaled = scaleToCanvas(obs.x, obs.y);\n                            const scaledSize = size * scaled.scaleX;\n                            ctx.strokeStyle = '#ff4400';\n                            ctx.lineWidth = 4;\n                            ctx.strokeRect(scaled.x, scaled.y, scaledSize, scaledSize);\n                            \n                            // Add a small inner glow\n                            ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';\n                            ctx.fillRect(scaled.x + 5, scaled.y + 5, scaledSize - 10, scaledSize - 10);\n                            ctx.restore();\n                        }\n\n                        // Draw special indicator\n                        if (obs.isSpecial) {\n                            ctx.save();\n                            const scaled = scaleToCanvas(obs.x + size / 2, obs.y + size / 2);\n                            ctx.fillStyle = 'rgba(255, 215, 0, 0.5)';\n                            ctx.beginPath();\n                            ctx.arc(scaled.x, scaled.y, (10 * (obs.scale || 1)) * scaled.scaleX, 0, Math.PI * 2);\n                            ctx.fill();\n                            ctx.restore();\n                        }\n                    }\n                    \n                    // Draw selection in edit mode\n                    if (editMode && selectedObstacle !== null && obstacles[selectedObstacle] === obs) {\n                        const scaled = scaleToCanvas(obs.x, obs.y);\n                        const size = OBSTACLE_SIZE * scaled.scaleX;\n                        ctx.strokeStyle = '#FFD700';\n                        ctx.lineWidth = 3;\n                        ctx.strokeRect(scaled.x, scaled.y, size, size);\n                    }\n                }\n\n                // Draw Boss\n                if (boss && !editMode) {\n                    drawImage(assetCache['boss_monkey'], boss.x, boss.y, boss.width, boss.height);\n                    \n                    // Boss Health Bar\n                    const scaled = scaleToCanvas(boss.x, boss.y - 20);\n                    const barW = boss.width * scaled.scaleX;\n                    const barH = 10 * scaled.scaleY;\n                    ctx.fillStyle = 'rgba(0,0,0,0.5)';\n                    ctx.fillRect(scaled.x, scaled.y, barW, barH);\n                    const maxHealth = boss.maxHealth || 6;\n                    ctx.fillStyle = '#ff0000';\n                    ctx.fillRect(scaled.x, scaled.y, barW * (boss.health / maxHealth), barH);\n                }\n                \n                // Draw projectiles\n                for (const proj of projectiles) {\n                    const assetId = proj.type === 'explosive' ? 'explosive_banana_projectile' : 'peeled_banana_projectile';\n                    if (assetCache[assetId]) {\n                        drawImage(\n                            assetCache[assetId],\n                            proj.x - proj.width / 2,\n                            proj.y - proj.height / 2,\n                            proj.width,\n                            proj.height,\n                            proj.rotation\n                        );\n                    }\n                }\n                \n                // Draw particles\n                for (const p of particles) {\n                    if (p.image) {\n                        ctx.globalAlpha = p.life;\n                        drawImage(p.image, p.x - p.size / 2, p.y - p.size / 2, p.size, p.size, p.rotation);\n                        ctx.globalAlpha = 1;\n                    }\n                }\n                \n                // Draw trajectory prediction\n                if (isDragging && !editMode) {\n                    const vx = Math.cos(launchAngle) * launchPower;\n                    const vy = Math.sin(launchAngle) * launchPower;\n                    const trajectory = predictTrajectory(SCREEN_WIDTH / 2, LAUNCHER_ZONE_HEIGHT, vx, vy);\n                    \n                    for (let i = 0; i < trajectory.length; i++) {\n                        const point = trajectory[i];\n                        const scaled = scaleToCanvas(point.x, point.y);\n                        \n                        let color = 'rgba(255, 255, 255, 0.7)';\n                        if (activeBananaType === 'explosive') color = 'rgba(255, 100, 100, 0.8)';\n                        else if (activeBananaType === 'split') color = 'rgba(100, 255, 100, 0.8)';\n                        else if (activeBananaType === 'boomerang') color = 'rgba(0, 255, 200, 0.9)';\n\n                        ctx.fillStyle = color;\n                        ctx.beginPath();\n                        ctx.arc(scaled.x, scaled.y, 4, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    \n                    // Draw power meter\n                    const powerPercent = Math.min(launchPower / 1000, 1);\n                    const scaled = scaleToCanvas(SCREEN_WIDTH / 2 - 100, 150);\n                    const barWidth = 200 * scaled.scaleX;\n                    const barHeight = 16 * scaled.scaleY;\n                    \n                    ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';\n                    ctx.fillRect(scaled.x, scaled.y, barWidth, barHeight);\n                    \n                    ctx.fillStyle = '#FFD700';\n                    ctx.fillRect(scaled.x, scaled.y, barWidth * powerPercent, barHeight);\n                    \n                    ctx.strokeStyle = 'white';\n                    ctx.lineWidth = 2;\n                    ctx.strokeRect(scaled.x, scaled.y, barWidth, barHeight);\n                }\n                \n                // Draw grid in edit mode\n                if (editMode) {\n                    ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';\n                    ctx.lineWidth = 1;\n                    \n                    for (let x = WALL_WIDTH; x < SCREEN_WIDTH - WALL_WIDTH; x += GRID_SIZE) {\n                        const scaled = scaleToCanvas(x, 0);\n                        ctx.beginPath();\n                        ctx.moveTo(scaled.x, 0);\n                        ctx.lineTo(scaled.x, canvas.height);\n                        ctx.stroke();\n                    }\n                    \n                    for (let y = PLAY_ZONE_TOP; y < PLAY_ZONE_BOTTOM; y += GRID_SIZE) {\n                        const scaled = scaleToCanvas(0, y);\n                        ctx.beginPath();\n                        ctx.moveTo(0, scaled.y);\n                        ctx.lineTo(canvas.width, scaled.y);\n                        ctx.stroke();\n                    }\n                }\n            }\n\n            // ============================================\n            // GAME LOOP\n            // ============================================\n\n            let lastTime = 0;\n            \n            function gameLoop(timestamp) {\n                if (window.lastBananaRunId !== runId) return;\n                \n                const deltaTime = Math.min((timestamp - lastTime) / 1000, 0.11);\n                lastTime = timestamp;\n                \n                if (!editMode) {\n                    // Keep updating physics and particles even in won/lost state \n                    // so projectiles don't \"freeze\" mid-air.\n                    if (gameState === 'playing' || gameState === 'won' || gameState === 'lost') {\n                        updateProjectiles(deltaTime);\n                        updateParticles(deltaTime);\n                    }\n\n                    if (gameState === 'playing') {\n                        updateBoss(deltaTime);\n\n                        // Failsafe: Check if all obstacles are cleared but game is still 'playing'\n                        if (obstacles.length === 0 && (!boss || boss.health <= 0)) {\n                            gameState = 'won';\n                            showWinScreen();\n                        }\n                    }\n                }\n                \n                render();\n                requestAnimationFrame(gameLoop);\n            }\n\n            // Start the game\n            const startLevel = window.activeBananaLevel !== undefined ? window.activeBananaLevel : (window.gameConfig.settings ? window.gameConfig.settings.selectedLevel : 0);\n            initLevel(startLevel);\n            setupGameParameters();\n            requestAnimationFrame(gameLoop);\n        }\n    </script>\n</body>\n</html>", "game_config": "{\n  \"levels\": [\n    {\n      \"obstacles\": [\n        {\n          \"y\": 900,\n          \"type\": \"crate\",\n          \"x\": 330\n        },\n        {\n          \"y\": 840,\n          \"type\": \"crate\",\n          \"x\": 270\n        },\n        {\n          \"y\": 840,\n          \"type\": \"crate\",\n          \"x\": 390\n        },\n        {\n          \"type\": \"crate\",\n          \"x\": 330,\n          \"y\": 780\n        }\n      ],\n      \"bananaCount\": 5,\n      \"starThresholds\": [\n        1,\n        3,\n        4\n      ]\n    },\n    {\n      \"obstacles\": [\n        {\n          \"y\": 900,\n          \"type\": \"crate\",\n          \"x\": 110\n        },\n        {\n          \"y\": 900,\n          \"type\": \"crate\",\n          \"x\": 330\n        },\n        {\n          \"y\": 900,\n          \"type\": \"crate\",\n          \"x\": 550\n        },\n        {\n          \"y\": 960,\n          \"type\": \"explosive\",\n          \"x\": 170\n        },\n        {\n          \"type\": \"explosive\",\n          \"x\": 330,\n          \"y\": 960\n        },\n        {\n          \"type\": \"explosive\",\n          \"x\": 490,\n          \"y\": 960\n        },\n        {\n          \"type\": \"glass\",\n          \"x\": 330,\n          \"y\": 1020\n        }\n      ],\n      \"bananaCount\": 6,\n      \"starThresholds\": [\n        1,\n        3,\n        4\n      ]\n    },\n    {\n      \"obstacles\": [\n        {\n          \"y\": 1040,\n          \"x\": 330,\n          \"type\": \"crate\"\n        },\n        {\n          \"y\": 980,\n          \"x\": 270,\n          \"type\": \"crate\"\n        },\n        {\n          \"x\": 390,\n          \"type\": \"crate\",\n          \"y\": 980\n        },\n        {\n          \"x\": 330,\n          \"type\": \"glass\",\n          \"y\": 920\n        },\n        {\n          \"y\": 860,\n          \"x\": 270,\n          \"type\": \"glass\"\n        },\n        {\n          \"y\": 860,\n          \"type\": \"glass\",\n          \"x\": 390\n        },\n        {\n          \"x\": 330,\n          \"type\": \"crate\",\n          \"y\": 800\n        },\n        {\n          \"type\": \"crate\",\n          \"x\": 270,\n          \"y\": 740\n        },\n        {\n          \"y\": 740,\n          \"x\": 390,\n          \"type\": \"crate\"\n        },\n        {\n          \"y\": 680,\n          \"type\": \"glass\",\n          \"x\": 330\n        },\n        {\n          \"y\": 800,\n          \"x\": 210,\n          \"type\": \"explosive\"\n        },\n        {\n          \"y\": 800,\n          \"type\": \"explosive\",\n          \"x\": 450\n        }\n      ],\n      \"bananaCount\": 7,\n      \"hasBoss\": true,\n      \"starThresholds\": [\n        1,\n        3,\n        4\n      ],\n      \"specialObstaclesCount\": 2\n    },\n    {\n      \"obstacles\": [\n        {\n          \"x\": 110,\n          \"type\": \"splitting\",\n          \"y\": 980\n        },\n        {\n          \"type\": \"glass\",\n          \"y\": 980,\n          \"x\": 170\n        },\n        {\n          \"x\": 330,\n          \"y\": 920,\n          \"type\": \"explosive\"\n        },\n        {\n          \"x\": 330,\n          \"y\": 860,\n          \"type\": \"glass\"\n        },\n        {\n          \"x\": 430,\n          \"type\": \"explosive\",\n          \"y\": 920\n        },\n        {\n          \"x\": 490,\n          \"type\": \"glass\",\n          \"y\": 980\n        },\n        {\n          \"x\": 550,\n          \"y\": 980,\n          \"type\": \"splitting\"\n        },\n        {\n          \"x\": 150,\n          \"y\": 1050,\n          \"type\": \"glass\"\n        },\n        {\n          \"x\": 270,\n          \"type\": \"splitting\",\n          \"y\": 1110\n        },\n        {\n          \"x\": 390,\n          \"type\": \"splitting\",\n          \"y\": 1110\n        },\n        {\n          \"type\": \"glass\",\n          \"y\": 1050,\n          \"x\": 510\n        },\n        {\n          \"type\": \"explosive\",\n          \"y\": 1170,\n          \"x\": 330\n        },\n        {\n          \"y\": 920,\n          \"type\": \"explosive\",\n          \"x\": 210\n        },\n        {\n          \"y\": 1040,\n          \"type\": \"explosive\",\n          \"x\": 450\n        },\n        {\n          \"type\": \"explosive\",\n          \"y\": 1170,\n          \"x\": 150\n        },\n        {\n          \"type\": \"explosive\",\n          \"y\": 1170,\n          \"x\": 510\n        }\n      ],\n      \"bananaCount\": 8\n    }\n  ],\n  \"settings\": {\n    \"gravity\": 750,\n    \"wallBounceRetention\": 0.75,\n    \"obstacleBounceRetention\": 0.4\n  }\n}", "game_design_document": "## Core Game Loop and Mechanics\n\n**Banana Bounce Bash** is a physics-based puzzle game where players launch peeled bananas downward to destroy all obstacles on each level.\n\n### Moment-to-Moment Gameplay\n1. Player aims their banana launcher at the top of the screen\n2. Player adjusts launch angle and power\n3. Player releases to fire a peeled banana projectile\n4. Banana bounces off side walls and destroys obstacles on impact\n5. Banana eventually falls off screen or loses momentum\n6. Player launches another banana (limited supply per level)\n7. Repeat until all obstacles are destroyed or bananas run out\n\n### Progression and Engagement\n- **Win Condition**: Destroy all obstacles in the level with available bananas\n- **Lose Condition**: Run out of bananas before clearing all obstacles\n- **Star Rating**: Earn 1-3 stars based on bananas remaining (more remaining = more stars)\n- **Level Progression**: Complete a level to unlock the next one\n- **Replayability**: Try to beat levels with fewer bananas for higher star ratings\n\n### What Makes It Compelling\n- Satisfying physics-based destruction with realistic bouncing\n- Strategic angle planning to maximize wall bounces\n- Visual feedback with obstacles breaking apart\n- Progressive difficulty that teaches mechanics naturally\n- Quick retry for \"one more try\" engagement\n\n## Starting Scope\n\n### First Implementation (Minimal Viable Game)\n- **5 complete levels** with increasing difficulty\n- **Core physics**: Banana projectile with gravity, wall bouncing, and obstacle destruction\n- **Launch mechanic**: Drag-based aiming with power control\n- **Obstacle types**: Start with 2-3 simple destructible block types (wooden crates, glass blocks)\n- **UI**: Banana counter, level indicator, win/lose screens, restart button\n- **Visual polish**: Particle effects on destruction, smooth animations\n- **Sound**: Launch sound, bounce sound, destruction sounds, background music\n\n### Future Enhancements (Post-Launch)\n- Additional obstacle types (metal blocks requiring multiple hits, explosive barrels)\n- Special banana types (heavy banana, splitting banana, explosive banana)\n- More levels (expand to 10-20 levels)\n- Level editor in Edit mode\n- Combo system for consecutive hits\n- Slow-motion replay of successful shots\n\n## Game Perspective\n\n**Fixed-screen with vertical orientation** - The entire level is visible on one screen with the banana launcher at the top and obstacles arranged below. The camera does not scroll. This perspective works perfectly for the 720\u00d71280 portrait format, allowing players to see the entire puzzle and plan their shots.\n\n## Game World and Level Design\n\n### Screen Layout (720\u00d71280)\n- **Top Zone (0-200px)**: Banana launcher area with aiming indicator\n- **Play Zone (200-1100px)**: Main area where bananas bounce and obstacles exist\n- **Bottom Zone (1100-1280px)**: UI elements (banana count, level number, restart button)\n- **Side Walls**: Located at x=50 and x=670, creating a 620px wide play area with visible wall boundaries\n\n### Level Progression\n\n**Level 1: Tutorial (Easy)**\n- **Obstacles**: 3-4 wooden crates arranged in a simple pyramid\n- **Bananas Available**: 5\n- **Teaching**: Basic aiming and direct hits\n- **Layout**: Single pyramid structure in center, no wall bounces required\n\n**Level 2: Wall Introduction (Easy-Medium)**\n- **Obstacles**: 5-6 crates with some positioned near walls\n- **Bananas Available**: 6\n- **Teaching**: Wall bounces to reach side obstacles\n- **Layout**: Obstacles spread horizontally, requiring at least one wall bounce\n\n**Level 3: Stacked Challenge (Medium)**\n- **Obstacles**: 8-10 crates in multiple layers, mix of wooden crates and glass blocks\n- **Bananas Available**: 7\n- **Teaching**: Multi-bounce shots and targeting weak points\n- **Layout**: Tower structure with glass blocks (1 hit) and wooden crates (1 hit) mixed\n\n**Level 4: Complex Arrangement (Medium-Hard)**\n- **Obstacles**: 12-15 mixed obstacles in asymmetric pattern\n- **Bananas Available**: 8\n- **Teaching**: Strategic shot planning, combo destruction\n- **Layout**: Multiple small structures requiring precise angles\n\n**Level 5: Master Challenge (Hard)**\n- **Obstacles**: 15-18 obstacles in intricate fortress-like arrangement\n- **Bananas Available**: 10\n- **Teaching**: All mechanics combined, optimal path finding\n- **Layout**: Complex multi-layer structure with protected inner obstacles\n\n### Obstacle Types (Starting Scope)\n\n1. **Wooden Crate** (brown, 60\u00d760px)\n   - Health: 1 hit to destroy\n   - Behavior: Breaks into 4-6 wooden splinters\n   \n2. **Glass Block** (light blue/transparent, 60\u00d760px)\n   - Health: 1 hit to destroy\n   - Behavior: Shatters into 8-10 glass shards with tinkling sound\n\n### Physics Properties\n- **Gravity**: Standard downward acceleration (800 px/s\u00b2)\n- **Banana Bounce**: 70% energy retention on wall bounce\n- **Obstacle Collision**: Banana destroys obstacle and continues with 50% velocity\n- **Wall Friction**: Minimal (95% velocity retention on bounce)\n\n## Edit Mode Design\n\n### Editable Properties\n\n**Level Configuration** (per level):\n- **Banana Count**: Number of bananas available (3-15 range)\n- **Obstacle Layout**: Position and type of each obstacle\n  - Add/remove obstacles\n  - Change obstacle type (wooden crate, glass block)\n  - Drag to reposition obstacles (snapped to 60px grid)\n- **Star Thresholds**: Bananas remaining for 1/2/3 stars\n\n**Global Settings**:\n- **Physics Tuning**:\n  - Gravity strength (600-1000 px/s\u00b2)\n  - Wall bounce retention (50-90%)\n  - Obstacle bounce retention (30-70%)\n- **Visual Theme**:\n  - Background color/pattern\n  - Wall color\n  - Particle effect intensity\n\n### Edit Mode UI\n- **Level Selector**: Dropdown to switch between levels 1-5\n- **Obstacle Palette**: Buttons to select obstacle type to place\n- **Grid Overlay**: Visual 60px grid for precise placement\n- **Test Button**: Quick play test of current level\n- **Reset Button**: Restore level to default design\n\n### Constraints\n- Minimum 1 obstacle per level, maximum 25 obstacles\n- Obstacles must be placed within play zone (200-1100px vertical)\n- Obstacles cannot overlap\n- Banana count must be at least 1, maximum 20\n\n## Control Design\n\n### Mobile (Touch) Controls\n\n**Aiming and Launching**:\n1. **Touch and Hold** on the banana launcher area (top 200px)\n2. **Drag Down** to aim - a visual trajectory line appears showing angle and power\n   - Drag distance determines launch power (longer drag = more power)\n   - Drag angle determines launch direction (always downward arc, 45-135\u00b0 range)\n3. **Release** to launch the banana\n4. **Visual Feedback**: \n   - Dotted trajectory line showing predicted path\n   - Power meter showing launch strength\n   - Banana sprite rotates to face launch direction\n\n**UI Interactions**:\n- **Tap** restart button to retry level\n- **Tap** next level button after winning\n- **Tap** menu button to return to level select\n\n### Desktop (Keyboard/Mouse) Controls\n\n**Aiming and Launching**:\n1. **Click and Hold** on the banana launcher area\n2. **Drag Mouse Down** to aim (same as touch)\n3. **Release Mouse** to launch\n4. **Alternative**: Use **Arrow Keys** to adjust angle, **Space** to set power and launch\n\n**UI Interactions**:\n- **Click** buttons for restart, next level, menu\n- **R Key** for quick restart\n- **ESC Key** to pause/menu\n\n### Control Implementation Notes\n- Prevent default browser behavior for touch events to avoid scrolling\n- Show trajectory prediction with 5-7 dots along predicted path\n- Clamp launch angle to reasonable downward range (45-135\u00b0 from horizontal)\n- Maximum launch power capped to prevent bananas from moving too fast\n- Clear input state on window blur\n\n## Art Style and Visual Theme\n\n### Overall Style\n**Colorful Cartoon with Playful Physics** - Bright, cheerful art style with exaggerated proportions and satisfying destruction effects. Think casual mobile game aesthetic with smooth animations and juicy feedback.\n\n### Color Palette\n- **Background**: Soft gradient sky (light blue to white)\n- **Walls**: Solid brown wooden planks with visible grain\n- **UI**: Bright yellow/orange accents matching banana theme\n- **Obstacles**: Natural material colors (brown wood, light blue glass)\n\n### Visual Elements\n\n**Banana Launcher** (top of screen):\n- Large yellow banana (whole) sitting in a wooden slingshot/catapult\n- When aiming, the banana rotates and a peeled banana appears ready to launch\n- Slingshot stretches visually to show power\n\n**Peeled Banana Projectile**:\n- Creamy yellow-white banana without peel\n- Size: ~40\u00d780px\n- Rotates during flight to show motion\n- Leaves subtle motion trail\n\n**Obstacles**:\n- Wooden Crate: Brown with visible wood grain, cartoon style\n- Glass Block: Semi-transparent light blue with shine highlights\n- Size: 60\u00d760px each\n- Slight shadow underneath for depth\n\n**Walls**:\n- Visible wooden plank texture on both sides\n- Width: 50px each\n- Extend full height of play zone\n\n**Particle Effects**:\n- **Destruction**: 6-10 particles matching obstacle material (wood splinters, glass shards)\n- **Wall Bounce**: Small impact stars/sparkles\n- **Launch**: Puff of smoke/dust from launcher\n\n**UI Elements**:\n- Banana counter: Banana icon \u00d7 number remaining\n- Level indicator: \"Level 1/5\" in clear font\n- Star display: 3 star outlines, filled based on performance\n- Buttons: Rounded rectangles with icons (restart arrow, next arrow, menu hamburger)\n\n### Animation Style\n- Smooth easing on all movements\n- Squash and stretch on banana impacts\n- Wobble effect on obstacles when hit\n- Satisfying destruction with particles flying outward\n- Screen shake on large impacts (subtle, 2-3px)\n\n### Asset vs Code Rendering\n\n**Generated Assets**:\n- Banana launcher (whole banana in slingshot)\n- Peeled banana projectile\n- Wooden crate obstacle\n- Glass block obstacle\n- Wall texture\n- Particle sprites (wood splinter, glass shard)\n- UI button icons\n\n**Code Rendering (CSS/Canvas)**:\n- Trajectory prediction line (dotted line)\n- Power meter (simple bar)\n- Background gradient\n- UI containers and text\n- Star rating display\n- Grid overlay in edit mode", "persistent_storage_config": null, "game_metadata": {"game_id": "01KSGVPH8GS2QW07YHW3CFXK3M", "title": "Banana Bounce Bash", "description": "Launch peeled bananas from above to destroy all obstacles below using wall bounces and physics!", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_a59bd8e7-6a54-4820-95cf-892c414dd7e1.webp", "created_at": 1779756146, "updated_at": 1782917268329.6428, "art_style": null}, "task_plan": null}