{"asset_map": {"portrait_23": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/portrait_23_fbea39ee-6ec5-44e9-b802-ae047f21ee1b.webp", "type": "image", "prompt": "Generate an elegant portrait of a woman in vintage-inspired lace or silk clothing. Soft romantic lighting, classic beauty with a modern touch, professional studio portrait.", "description": "Elegant portrait in vintage lace or silk", "subtype": "background", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_01_new": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781268620866_01KTXY49J2RDVE400D792YYBJA.Screenshot 2026-06-12 at 8.46.53\u202fPM.png", "type": "image", "prompt": null, "description": "A portrait of a beautiful woman with long dark hair wearing a black crop top and earphones.", "subtype": null, "aspect_ratio": [716, 1112], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_31": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781268740405_01KTXY7Y9N72QAQA6R0G9M1X5C.Screenshot 2026-06-12 at 8.45.03\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long wavy brown hair wearing a black top.", "subtype": null, "aspect_ratio": [910, 1107], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_32": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781268740559_01KTXY7YEF8ZHFE90Y4X9GPZ9V.Screenshot 2026-06-12 at 8.45.44\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long brown hair in front of a building in Paris.", "subtype": null, "aspect_ratio": [888, 1089], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_33": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781268740582_01KTXY7YF6XW4G3M0XSBWS7142.Screenshot 2026-06-12 at 8.46.22\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long dark hair wearing a beige tank top.", "subtype": null, "aspect_ratio": [699, 1088], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_34": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269004307_01KTXYG00K208BCX78ZVEBE9S8.Screenshot 2026-06-12 at 8.55.03\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with short silver-blonde hair wearing a white tank top and a black choker.", "subtype": null, "aspect_ratio": [782, 1088], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_35": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269004605_01KTXYG09XBVKT913MXBHRSKMQ.Screenshot 2026-06-12 at 8.54.22\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long dark hair wearing a black tube top.", "subtype": null, "aspect_ratio": [728, 1053], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_36": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269004593_01KTXYG09HNVCP1KN6JVZZTW85.Screenshot 2026-06-12 at 8.53.29\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long dark hair wearing a brown cardigan and a white top, outdoors at sunset.", "subtype": null, "aspect_ratio": [661, 1078], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 8.47.28\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269386946_01KTXYVNP26G7ANPJW3GWWKM3D.Screenshot 2026-06-12 at 8.47.28\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long wavy brown hair wearing a floral dress in a grocery store aisle.", "subtype": null, "aspect_ratio": [659, 1068], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 8.57.32\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269390199_01KTXYVRVQBECKHE5TKX888YGZ.Screenshot 2026-06-12 at 8.57.32\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling blonde woman with a ponytail wearing a grey hoodie.", "subtype": null, "aspect_ratio": [938, 1076], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 8.58.01\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269387152_01KTXYVNW7RABVJSFSSYMC0WZ0.Screenshot 2026-06-12 at 8.58.01\u202fPM.png", "type": "image", "prompt": null, "description": "Close-up portrait of a smiling blonde woman with blue eyes wearing a white tank top.", "subtype": null, "aspect_ratio": [928, 1087], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_37": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269543986_01KTXZ0F1JDY80RCKTP9PW88Q1.Screenshot 2026-06-12 at 8.59.02\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling woman with wavy brown hair in a yellow floral dress, outdoors with a city background.", "subtype": null, "aspect_ratio": [667, 1077], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_38": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269544749_01KTXZ0FSDP61QE3Y2297G9RCV.Screenshot 2026-06-12 at 9.00.28\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with curly dark hair tied up, wearing a brown halter dress.", "subtype": null, "aspect_ratio": [664, 1083], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "portrait_39": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269544936_01KTXZ0FZ8WP2NCG4Z9RFJYKRB.Screenshot 2026-06-12 at 9.01.07\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with short dark hair wearing a maroon dress and multiple layered necklaces.", "subtype": null, "aspect_ratio": [665, 1084], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.01.46\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269702965_01KTXZ5A92WTE8WG9DRWWVXSEG.Screenshot 2026-06-12 at 9.01.46\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling young woman with glasses wearing a beige dress.", "subtype": null, "aspect_ratio": [679, 1071], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.02.19\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269703101_01KTXZ5ADX64GV9FEHMBF1S0GD.Screenshot 2026-06-12 at 9.02.19\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with brown hair drinking a green beverage in a cafe.", "subtype": null, "aspect_ratio": [668, 1075], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.06.46\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781269703034_01KTXZ5ABT10W8KAMD2B6G6J79.Screenshot 2026-06-12 at 9.06.46\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long dark wavy hair in a black lace-up dress, crouching in a white room.", "subtype": null, "aspect_ratio": [748, 1345], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.08.35\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781270291106_01KTXZQ8N24W69VHCF6RWBF639.Screenshot 2026-06-12 at 9.08.35\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long dark braids standing on a pier at sunset.", "subtype": null, "aspect_ratio": [922, 1257], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.09.43\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781270291139_01KTXZQ8P3XQNZ3FKR93C9B5S2.Screenshot 2026-06-12 at 9.09.43\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long brown hair wearing a light blue dress.", "subtype": null, "aspect_ratio": [752, 1249], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.10.31\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781270294144_01KTXZQBM01KDV1X4BXVD7S1WJ.Screenshot 2026-06-12 at 9.10.31\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling woman with long brown hair wearing a beige crop top and shorts.", "subtype": null, "aspect_ratio": [775, 1252], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.12.08\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781270586251_01KTY008WB8CZYEP7GGTJ5JG11.Screenshot 2026-06-12 at 9.12.08\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman looking into a mirror.", "subtype": null, "aspect_ratio": [895, 1233], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.13.00\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781270586410_01KTY00919HRPCYXJSFEJA2X1A.Screenshot 2026-06-12 at 9.13.00\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling woman with long dark hair in a park at sunset.", "subtype": null, "aspect_ratio": [745, 1244], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.13.22\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781270586334_01KTY008YYJ3D3A50TTBCMHHG7.Screenshot 2026-06-12 at 9.13.22\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long dark hair wearing a white tank top near a window.", "subtype": null, "aspect_ratio": [742, 1231], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.14.05\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781270708559_01KTY040AFBB52Y13NZX8H2CJ4.Screenshot 2026-06-12 at 9.14.05\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling woman with wavy dark hair by a river.", "subtype": null, "aspect_ratio": [757, 1230], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.15.08\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781270708493_01KTY0408DTYBN99S0VBFXKRBG.Screenshot 2026-06-12 at 9.15.08\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman with dark hair wearing hoop earrings.", "subtype": null, "aspect_ratio": [889, 1246], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.15.45\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781270708347_01KTY0403VBBS48ZA5C4VV9D3J.Screenshot 2026-06-12 at 9.15.45\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling woman with long brown hair in a floral top.", "subtype": null, "aspect_ratio": [758, 1254], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.16.13\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271061440_01KTY0ERY0KTFDWCASEQ260KCF.Screenshot 2026-06-12 at 9.16.13\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling woman in a yellow floral top outdoors.", "subtype": null, "aspect_ratio": [757, 1251], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.16.44\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271061227_01KTY0ERQB699WY5FQFMGVPM3S.Screenshot 2026-06-12 at 9.16.44\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling woman in an olive green top with layered necklaces.", "subtype": null, "aspect_ratio": [746, 1244], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.17.10\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271064446_01KTY0EVVTMY3HZ3RSG3MGQJ5B.Screenshot 2026-06-12 at 9.17.10\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman in a yellow sari.", "subtype": null, "aspect_ratio": [762, 1244], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.18.15\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271153108_01KTY0HJEM8M63KD7GK9SADCAD.Screenshot 2026-06-12 at 9.18.15\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long dark hair wearing a green off-the-shoulder top.", "subtype": null, "aspect_ratio": [741, 1247], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.19.48\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271153128_01KTY0HJF8M22VWHWFMSAEETXE.Screenshot 2026-06-12 at 9.19.48\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling young woman with dark hair and a braid, wearing a light blue and white outfit.", "subtype": null, "aspect_ratio": [918, 1225], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.20.49\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271153181_01KTY0HJGX5P1Q4MC95EFVEF2T.Screenshot 2026-06-12 at 9.20.49\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with long dark hair wearing a grey turtleneck sweater.", "subtype": null, "aspect_ratio": [758, 1246], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.22.10\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271238747_01KTY0M62VP1ERXTB9QVCYK92E.Screenshot 2026-06-12 at 9.22.10\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman with a ponytail in a cozy knit sweater.", "subtype": null, "aspect_ratio": [907, 1360], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.22.35\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271238728_01KTY0M6287P00KVBF196FK5KC.Screenshot 2026-06-12 at 9.22.35\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a smiling woman with long brown hair outdoors at sunset.", "subtype": null, "aspect_ratio": [890, 1353], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.24.43\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271238722_01KTY0M622RE2H0DAAGB44N4S4.Screenshot 2026-06-12 at 9.24.43\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman in a black swimsuit sitting by a pool.", "subtype": null, "aspect_ratio": [904, 1234], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.26.55\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271335766_01KTY0Q4T3ER2NPWGM19414AKB.Screenshot 2026-06-12 at 9.26.55\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman waving in front of a cafe.", "subtype": null, "aspect_ratio": [763, 1238], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.27.20\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271335751_01KTY0Q4T70R95SPNJA7R70M8H.Screenshot 2026-06-12 at 9.27.20\u202fPM.png", "type": "image", "prompt": null, "description": "Close-up portrait of a beautiful woman with long dark hair.", "subtype": null, "aspect_ratio": [761, 1246], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-12 at 9.29.31\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781271335745_01KTY0Q4T1G48X709VANFJN6H2.Screenshot 2026-06-12 at 9.29.31\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a woman with flowers in her hair.", "subtype": null, "aspect_ratio": [768, 1245], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "background_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/background_music_175c7e4c-910e-4845-8877-a8a0d207a671.mp3", "type": "audio", "prompt": "A sweet, relaxing, and romantic background track featuring a soft acoustic guitar and a gentle piano melody. The atmosphere is calm, intimate, and serene, perfect for a cozy puzzle game. Tempo is slow and steady, approximately 70-80 BPM. Clean acoustic arrangement with light reverb. The composition should have a seamless loop point, avoiding sudden changes or harsh transitions. Instrumental only, no lyrics, high-quality studio recording feel.", "description": "A calming and romantic acoustic melody for the game's background ambiance.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 30, "loop": true, "animation": null}, "Screenshot 2026-06-13 at 1.33.21\u202fAM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781285722016_01KTYEE5X0NXWBJK36P7KDEX85.Screenshot 2026-06-13 at 1.33.21\u202fAM.png", "type": "image", "prompt": null, "description": "New portrait of a woman in a red bikini on a beach.", "subtype": null, "aspect_ratio": [1056, 1209], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.30.11\u202fAM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781285722096_01KTYEE5ZGJ3PD10MYSCBHSKDB.Screenshot 2026-06-13 at 1.30.11\u202fAM.png", "type": "image", "prompt": null, "description": "New portrait of a woman with her hair up in a blue swimsuit.", "subtype": null, "aspect_ratio": [593, 1092], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.27.51\u202fAM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781285725190_01KTYEE906NVM7KBBT0NQTC924.Screenshot 2026-06-13 at 1.27.51\u202fAM.png", "type": "image", "prompt": null, "description": "New portrait of a woman in a white lace top and flower crown.", "subtype": null, "aspect_ratio": [674, 1079], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.25.24\u202fAM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781286121376_01KTYETBX00BAVVDWS7TT96DT5.Screenshot 2026-06-13 at 1.25.24\u202fAM.png", "type": "image", "prompt": null, "description": "Portrait of a woman in a white bikini on a beach.", "subtype": null, "aspect_ratio": [669, 1094], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.24.43\u202fAM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781286124245_01KTYETEPNXW9YT0FHA9YN5R5C.Screenshot 2026-06-13 at 1.24.43\u202fAM.png", "type": "image", "prompt": null, "description": "Portrait of a woman in a striped bikini on a beach.", "subtype": null, "aspect_ratio": [873, 1079], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.23.20\u202fAM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781286121438_01KTYETBYY4K9XK1RA2Z67ADE5.Screenshot 2026-06-13 at 1.23.20\u202fAM.png", "type": "image", "prompt": null, "description": "Portrait of a woman in a white halter top.", "subtype": null, "aspect_ratio": [659, 1088], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.44.52\u202fAM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781286328438_01KTYF0P3PVZQGPBP63CQWA24J.Screenshot 2026-06-13 at 1.44.52\u202fAM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman with long brown hair wearing a black top with gold embroidery.", "subtype": null, "aspect_ratio": [669, 1089], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.43.19\u202fAM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781286481317_01KTYF5BCKJXTXQ9ECG9PVQP8Z.Screenshot 2026-06-13 at 1.43.19\u202fAM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman with wavy brown hair wearing a white lace dress.", "subtype": null, "aspect_ratio": [641, 1070], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.50.26\u202fAM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781286648084_01KTYFAE8MYVH75C5VKS3VYTAC.Screenshot 2026-06-13 at 1.50.26\u202fAM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman sitting on a sofa wearing a floral dress.", "subtype": null, "aspect_ratio": [861, 1209], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 12.48.24\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781326133374_01KTZMZE3YS3G0CTSPZH16GS1Y.Screenshot 2026-06-13 at 12.48.24\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman with blonde hair in a ponytail wearing a cow-print bikini.", "subtype": null, "aspect_ratio": [623, 1093], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 12.51.40\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781326369338_01KTZN6MHTFBWNNR3G5K7YGFPX.Screenshot 2026-06-13 at 12.51.40\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman in a floral bikini on a beach.", "subtype": null, "aspect_ratio": [894, 1101], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.00.48\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781327068456_01KTZNVZ98AZEQ1DKBN7AVWGB0.Screenshot 2026-06-13 at 1.00.48\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful blonde woman in a white dress.", "subtype": null, "aspect_ratio": [790, 1091], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "Screenshot 2026-06-13 at 1.03.15\u202fPM.png": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_115305163807261402099/1781327113178_01KTZNXAYT3BXTCZQ1BBBAV98H.Screenshot 2026-06-13 at 1.03.15\u202fPM.png", "type": "image", "prompt": null, "description": "Portrait of a beautiful woman with an elaborate hairstyle and golden jewelry.", "subtype": null, "aspect_ratio": [804, 1083], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Dream Girl Puzzle</title>\n    <style>\n        :root {\n            --primary-color: #8B3A8B;\n            --secondary-color: #D4AF37;\n            --bg-dark: #0a0015;\n            --bg-medium: #1a0033;\n            --text-light: #ffffff;\n            --text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);\n            \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            \n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n        }\n\n        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fredoka:wght@600;700&display=swap');\n\n        * {\n            box-sizing: border-box;\n            -webkit-tap-highlight-color: transparent;\n        }\n\n        body, html {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            -ms-touch-action: none;\n            font-family: 'Inter', sans-serif;\n        }\n\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n            background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-medium) 100%);\n        }\n\n        #game-canvas {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            touch-action: none;\n        }\n\n        /* UI Overlay */\n        .ui-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: 10;\n        }\n\n        .ui-overlay > * {\n            pointer-events: auto;\n        }\n\n        /* Top Bar */\n        .top-bar {\n            position: absolute;\n            top: 0;\n            left: 0;\n            right: 0;\n            height: 80px;\n            background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%);\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n            padding: 0 var(--space-sm);\n            z-index: 20;\n        }\n\n        .btn {\n            min-width: 96px;\n            min-height: 64px;\n            padding: var(--space-sm) var(--space-md);\n            background: var(--primary-color);\n            color: var(--text-light);\n            border: none;\n            border-radius: 12px;\n            font-size: 24px;\n            font-weight: 600;\n            cursor: pointer;\n            transition: all var(--transition-fast) var(--ease-out);\n            text-shadow: var(--text-shadow);\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);\n        }\n\n        .btn:hover {\n            transform: translateY(-2px);\n            box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5);\n        }\n\n        .btn:active {\n            transform: translateY(0);\n            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n        }\n\n        .btn-icon {\n            min-width: 64px;\n            min-height: 64px;\n            padding: var(--space-sm);\n            font-size: 32px;\n            line-height: 1;\n        }\n\n        .title-text {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 32px;\n            font-weight: 700;\n            color: var(--text-light);\n            text-shadow: var(--text-shadow);\n        }\n\n        .subtitle-text {\n            font-size: 20px;\n            color: rgba(255, 255, 255, 0.8);\n            text-shadow: var(--text-shadow);\n        }\n\n        /* Selection Screen */\n        .selection-screen {\n            position: absolute;\n            top: 80px;\n            left: 0;\n            right: 0;\n            bottom: 0;\n            overflow-y: auto;\n            padding: var(--space-md);\n        }\n\n        .progress-header {\n            text-align: center;\n            margin-bottom: var(--space-lg);\n        }\n\n        .puzzle-grid {\n            display: grid;\n            grid-template-columns: repeat(3, 1fr);\n            gap: var(--space-sm);\n            max-width: 680px;\n            margin: 0 auto;\n        }\n\n        .puzzle-card {\n            aspect-ratio: 1;\n            position: relative;\n            border-radius: 12px;\n            overflow: hidden;\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-out);\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);\n        }\n\n        .puzzle-card:active {\n            transform: scale(0.95);\n        }\n\n        .puzzle-card img {\n            width: 100%;\n            height: 100%;\n            object-fit: cover;\n        }\n\n        .puzzle-card.locked {\n            opacity: 0.5;\n        }\n\n        .puzzle-card.locked::after {\n            content: '\ud83d\udd12';\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            font-size: 48px;\n            text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8);\n        }\n\n        .puzzle-card.current {\n            outline: 4px solid var(--secondary-color);\n            outline-offset: -4px;\n        }\n\n        /* Completion Modal */\n        .completion-modal {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            background: rgba(0, 0, 0, 0.95);\n            border-radius: 24px;\n            padding: var(--space-xl);\n            text-align: center;\n            z-index: 100;\n            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);\n            min-width: 320px;\n        }\n\n        .completion-modal h2 {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 48px;\n            color: var(--secondary-color);\n            margin: 0 0 var(--space-md);\n            text-shadow: 0 0 20px var(--secondary-color);\n        }\n\n        .completion-modal p {\n            font-size: 24px;\n            color: var(--text-light);\n            margin: 0 0 var(--space-lg);\n        }\n\n        /* Hidden class */\n        .hidden {\n            display: none !important;\n        }\n\n        /* Particle effects */\n        @keyframes sparkle {\n            0%, 100% { opacity: 0; transform: scale(0) rotate(0deg); }\n            50% { opacity: 1; transform: scale(1) rotate(180deg); }\n        }\n\n        .sparkle {\n            position: absolute;\n            width: 8px;\n            height: 8px;\n            background: var(--secondary-color);\n            border-radius: 50%;\n            pointer-events: none;\n            animation: sparkle 1s ease-out forwards;\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"game-canvas\"></canvas>\n        \n        <!-- UI Overlay -->\n        <div class=\"ui-overlay\">\n            <!-- Top Bar -->\n            <div class=\"top-bar\">\n                <button id=\"back-btn\" class=\"btn btn-icon hidden\">\u2190</button>\n                <div>\n                    <div id=\"title\" class=\"title-text\">Dream Girl Puzzle</div>\n                    <div id=\"subtitle\" class=\"subtitle-text\"></div>\n                </div>\n                <div style=\"display: flex; gap: 8px;\">\n                    <button id=\"music-btn\" class=\"btn btn-icon\">\ud83c\udfb5</button>\n                    <button id=\"preview-btn\" class=\"btn btn-icon hidden\">\ud83d\udc41</button>\n                </div>\n            </div>\n\n            <!-- Selection Screen -->\n            <div id=\"selection-screen\" class=\"selection-screen hidden\">\n                <div class=\"progress-header\">\n                    <h2 class=\"title-text\" id=\"progress-text\">Puzzles Completed</h2>\n                </div>\n                <div id=\"puzzle-grid\" class=\"puzzle-grid\"></div>\n            </div>\n\n            <!-- Completion Modal -->\n            <div id=\"completion-modal\" class=\"completion-modal hidden\">\n                <h2>\ud83c\udf89 Complete! \ud83c\udf89</h2>\n                <p id=\"completion-text\">Puzzle completed!</p>\n                <button id=\"continue-btn\" class=\"btn\">Continue</button>\n            </div>\n        </div>\n    </div>\n\n    <script>\n        /*\n         * PUZZLE QUEENS - Relaxing Jigsaw Puzzle Game\n         * \n         * A sophisticated puzzle game featuring 30 beautiful portraits.\n         * Players solve puzzles sequentially to unlock new challenges.\n         * Features drag-and-drop mechanics with smart snapping and\n         * smooth animations for a polished experience.\n         */\n\n        async function run(mode) {\n            const gameWorld = document.getElementById('game-world');\n            const canvas = document.getElementById('game-canvas');\n            const ctx = canvas.getContext('2d');\n\n            // UI Elements\n            const backBtn = document.getElementById('back-btn');\n            const previewBtn = document.getElementById('preview-btn');\n            const musicBtn = document.getElementById('music-btn');\n            const selectionScreen = document.getElementById('selection-screen');\n            const puzzleGrid = document.getElementById('puzzle-grid');\n            const completionModal = document.getElementById('completion-modal');\n            const continueBtn = document.getElementById('continue-btn');\n            const titleEl = document.getElementById('title');\n            const subtitleEl = document.getElementById('subtitle');\n            const progressText = document.getElementById('progress-text');\n            const completionText = document.getElementById('completion-text');\n\n            // Initialize canvas\n            function resizeCanvas() {\n                canvas.width = gameWorld.clientWidth;\n                canvas.height = gameWorld.clientHeight;\n            }\n            resizeCanvas();\n            window.addEventListener('resize', resizeCanvas);\n\n            // Asset cache\n            const assetCache = {};\n            const audioContext = new (window.AudioContext || window.webkitAudioContext)();\n            let bgMusicBuffer = null;\n            let bgMusicSource = null;\n            let isMusicPlaying = false;\n\n            // Preload all assets\n            async function preloadAssets() {\n                const assetIds = window.gameConfig.puzzles.map(p => p.assetId || p.asset_id);\n                \n                // Load images\n                const loadPromises = assetIds.map(id => {\n                    return new Promise((resolve, reject) => {\n                        const assetInfo = lib.getAsset(id);\n                        if (!assetInfo) {\n                            resolve(null);\n                            return;\n                        }\n\n                        const img = new Image();\n                        img.onload = () => {\n                            assetCache[id] = {\n                                image: img,\n                                info: assetInfo\n                            };\n                            resolve(img);\n                        };\n                        img.onerror = () => resolve(null);\n                        img.src = assetInfo.url;\n                    });\n                });\n\n                // Load background music\n                const musicPromise = (async () => {\n                    const musicInfo = lib.getAsset('background_music');\n                    if (musicInfo) {\n                        try {\n                            const response = await fetch(musicInfo.url);\n                            const arrayBuffer = await response.arrayBuffer();\n                            bgMusicBuffer = await audioContext.decodeAudioData(arrayBuffer);\n                        } catch (e) {\n                            lib.log('Error loading background music: ' + e.message);\n                        }\n                    }\n                })();\n\n                await Promise.all([...loadPromises, musicPromise]);\n            }\n\n            function toggleMusic() {\n                if (!bgMusicBuffer) return;\n\n                if (audioContext.state === 'suspended') {\n                    audioContext.resume();\n                }\n\n                if (isMusicPlaying) {\n                    if (bgMusicSource) {\n                        bgMusicSource.stop();\n                        bgMusicSource = null;\n                    }\n                    isMusicPlaying = false;\n                    musicBtn.textContent = '\ud83d\udd07';\n                } else {\n                    bgMusicSource = audioContext.createBufferSource();\n                    bgMusicSource.buffer = bgMusicBuffer;\n                    bgMusicSource.loop = true;\n                    \n                    const gainNode = audioContext.createGain();\n                    gainNode.gain.value = 0.4;\n                    \n                    bgMusicSource.connect(gainNode);\n                    gainNode.connect(audioContext.destination);\n                    \n                    bgMusicSource.start(0);\n                    isMusicPlaying = true;\n                    musicBtn.textContent = '\ud83c\udfb5';\n                }\n            }\n\n            // Load user progress\n            // State structure: { completedPuzzles: [indices], finishedPuzzles: [indices] }\n            // - completedPuzzles: Array of puzzle indices the player can access\n            // - finishedPuzzles: Array of puzzle indices the player has fully solved\n            async function loadUserProgress() {\n                try {\n                    const result = await lib.getUserGameState();\n                    if (result && result.state) {\n                        const state = result.state;\n                        // Ensure all required fields exist and are arrays\n                        if (!state.completedPuzzles) state.completedPuzzles = [0];\n                        if (!state.finishedPuzzles) state.finishedPuzzles = [];\n                        if (!state.currentPuzzle) state.currentPuzzle = 0;\n                        return state;\n                    }\n                } catch (e) {\n                    lib.log('Error loading user progress: ' + e.message);\n                }\n                // Default state: Only first puzzle (index 0) is unlocked\n                return { currentPuzzle: 0, completedPuzzles: [0], finishedPuzzles: [] };\n            }\n\n            // Save user progress to persistent storage\n            // Saves player-specific progress that persists across sessions\n            async function saveUserProgress(progress) {\n                try {\n                    await lib.saveUserGameState(progress);\n                    lib.log(`Progress saved: ${progress.finishedPuzzles.length} puzzles completed`);\n                } catch (e) {\n                    lib.log('Error saving user progress: ' + e.message);\n                }\n            }\n\n            // Puzzle state\n            let currentScreen = 'selection'; // 'selection' or 'puzzle'\n            let currentPuzzleIndex = 0;\n            let userProgress = { currentPuzzle: 0, completedPuzzles: [0], finishedPuzzles: [] };\n            let puzzlePieces = [];\n            let draggedPiece = null;\n            let dragOffset = { x: 0, y: 0 };\n            let completedPieces = new Set();\n            let isPreviewMode = false;\n            let isVictoryPreview = false;\n\n            // Puzzle layout state (updated per puzzle)\n            let puzzleArea = { x: 0, y: 0, width: 0, height: 0 };\n\n            // Initialize game\n            await preloadAssets();\n            userProgress = await loadUserProgress();\n\n            // Show game parameters\n            lib.showGameParameters({\n                name: 'Puzzle Settings',\n                params: {\n                    'Grid Size': {\n                        key: 'settings.gridSize',\n                        type: 'slider',\n                        min: 2,\n                        max: 6,\n                        step: 1\n                    },\n                    'Snap Tolerance': {\n                        key: 'settings.snapTolerance',\n                        type: 'slider',\n                        min: 20,\n                        max: 80,\n                        step: 5\n                    },\n                    'Sequential Unlock': {\n                        key: 'settings.sequentialUnlock',\n                        type: 'checkbox'\n                    },\n                    'Background Color': {\n                        key: 'settings.backgroundColor',\n                        type: 'color'\n                    }\n                }\n            });\n\n            // Generate jigsaw piece shape\n            function generatePieceShape(col, row, gridSize, pieceWidth, pieceHeight) {\n                const path = new Path2D();\n                const tabSize = Math.min(pieceWidth, pieceHeight) * 0.2;\n                \n                // Start at top-left\n                path.moveTo(0, 0);\n                \n                // Top edge (with tab/blank)\n                if (row > 0) {\n                    const hasTab = (col + row) % 2 === 0;\n                    path.lineTo(pieceWidth * 0.3, 0);\n                    if (hasTab) {\n                        path.bezierCurveTo(\n                            pieceWidth * 0.35, -tabSize,\n                            pieceWidth * 0.65, -tabSize,\n                            pieceWidth * 0.7, 0\n                        );\n                    } else {\n                        path.bezierCurveTo(\n                            pieceWidth * 0.35, tabSize,\n                            pieceWidth * 0.65, tabSize,\n                            pieceWidth * 0.7, 0\n                        );\n                    }\n                }\n                path.lineTo(pieceWidth, 0);\n                \n                // Right edge (with tab/blank)\n                if (col < gridSize - 1) {\n                    const hasTab = (col + row + 1) % 2 === 0;\n                    path.lineTo(pieceWidth, pieceHeight * 0.3);\n                    if (hasTab) {\n                        path.bezierCurveTo(\n                            pieceWidth + tabSize, pieceHeight * 0.35,\n                            pieceWidth + tabSize, pieceHeight * 0.65,\n                            pieceWidth, pieceHeight * 0.7\n                        );\n                    } else {\n                        path.bezierCurveTo(\n                            pieceWidth - tabSize, pieceHeight * 0.35,\n                            pieceWidth - tabSize, pieceHeight * 0.65,\n                            pieceWidth, pieceHeight * 0.7\n                        );\n                    }\n                }\n                path.lineTo(pieceWidth, pieceHeight);\n                \n                // Bottom edge (with tab/blank)\n                if (row < gridSize - 1) {\n                    const hasTab = (col + row + 2) % 2 === 0;\n                    path.lineTo(pieceWidth * 0.7, pieceHeight);\n                    if (hasTab) {\n                        path.bezierCurveTo(\n                            pieceWidth * 0.65, pieceHeight + tabSize,\n                            pieceWidth * 0.35, pieceHeight + tabSize,\n                            pieceWidth * 0.3, pieceHeight\n                        );\n                    } else {\n                        path.bezierCurveTo(\n                            pieceWidth * 0.65, pieceHeight - tabSize,\n                            pieceWidth * 0.35, pieceHeight - tabSize,\n                            pieceWidth * 0.3, pieceHeight\n                        );\n                    }\n                }\n                path.lineTo(0, pieceHeight);\n                \n                // Left edge (with tab/blank)\n                if (col > 0) {\n                    const hasTab = (col + row + 3) % 2 === 0;\n                    path.lineTo(0, pieceHeight * 0.7);\n                    if (hasTab) {\n                        path.bezierCurveTo(\n                            -tabSize, pieceHeight * 0.65,\n                            -tabSize, pieceHeight * 0.35,\n                            0, pieceHeight * 0.3\n                        );\n                    } else {\n                        path.bezierCurveTo(\n                            tabSize, pieceHeight * 0.65,\n                            tabSize, pieceHeight * 0.35,\n                            0, pieceHeight * 0.3\n                        );\n                    }\n                }\n                path.lineTo(0, 0);\n                path.closePath();\n                \n                return path;\n            }\n\n            // Initialize puzzle pieces\n            function initializePuzzle(puzzleIndex) {\n                currentPuzzleIndex = puzzleIndex;\n                puzzlePieces = [];\n                completedPieces.clear();\n                \n                const isAlreadyFinished = userProgress.finishedPuzzles && userProgress.finishedPuzzles.includes(puzzleIndex);\n                isVictoryPreview = isAlreadyFinished;\n                \n                const puzzle = window.gameConfig.puzzles[puzzleIndex];\n                const assetId = puzzle.assetId || puzzle.asset_id;\n                const asset = assetCache[assetId];\n                if (!asset) return;\n\n                // Calculate dynamic puzzle area based on aspect ratio\n                const aspectRatio = asset.info.aspect_ratio || [1, 1];\n                const imgAspect = aspectRatio[0] / aspectRatio[1];\n                \n                const maxWidth = canvas.width * 0.85;\n                const maxHeight = canvas.height * 0.65;\n                \n                if (imgAspect > maxWidth / maxHeight) {\n                    puzzleArea.width = maxWidth;\n                    puzzleArea.height = maxWidth / imgAspect;\n                } else {\n                    puzzleArea.height = maxHeight;\n                    puzzleArea.width = maxHeight * imgAspect;\n                }\n                \n                puzzleArea.x = (canvas.width - puzzleArea.width) / 2;\n                puzzleArea.y = (canvas.height - puzzleArea.height) / 2 + 20;\n                \n                const gridSize = window.gameConfig.settings.gridSize;\n                const pieceWidth = puzzleArea.width / gridSize;\n                const pieceHeight = puzzleArea.height / gridSize;\n                \n                // Create pieces\n                for (let row = 0; row < gridSize; row++) {\n                    for (let col = 0; col < gridSize; col++) {\n                        const piece = {\n                            col,\n                            row,\n                            correctX: puzzleArea.x + col * pieceWidth,\n                            correctY: puzzleArea.y + row * pieceHeight,\n                            currentX: 0,\n                            currentY: 0,\n                            width: pieceWidth,\n                            height: pieceHeight,\n                            shape: generatePieceShape(col, row, gridSize, pieceWidth, pieceHeight),\n                            locked: isAlreadyFinished,\n                            glowing: false\n                        };\n                        \n                        if (isAlreadyFinished) {\n                            piece.currentX = piece.correctX;\n                            piece.currentY = piece.correctY;\n                            completedPieces.add(piece);\n                        } else {\n                            // Random scatter position around the puzzle area\n                            const angle = Math.random() * Math.PI * 2;\n                            const distance = 150 + Math.random() * 200;\n                            piece.currentX = piece.correctX + Math.cos(angle) * distance;\n                            piece.currentY = piece.correctY + Math.sin(angle) * distance;\n                            \n                            // Keep within bounds\n                            piece.currentX = Math.max(pieceWidth * 0.5, Math.min(canvas.width - pieceWidth * 1.5, piece.currentX));\n                            piece.currentY = Math.max(120, Math.min(canvas.height - pieceHeight - 80, piece.currentY));\n                        }\n                        \n                        puzzlePieces.push(piece);\n                    }\n                }\n            }\n\n            // Draw puzzle background\n            function drawPuzzleBackground() {\n                const puzzle = window.gameConfig.puzzles[currentPuzzleIndex];\n                const assetId = puzzle.assetId || puzzle.asset_id;\n                const asset = assetCache[assetId];\n                if (!asset) return;\n\n                ctx.save();\n                ctx.globalAlpha = (isPreviewMode || isVictoryPreview) ? 1.0 : 0.3;\n                \n                // Draw complete image as reference\n                const img = asset.image;\n                ctx.drawImage(img, puzzleArea.x, puzzleArea.y, puzzleArea.width, puzzleArea.height);\n                \n                ctx.restore();\n                \n                // Draw grid lines\n                if (!isPreviewMode && !isVictoryPreview) {\n                    ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';\n                    ctx.lineWidth = 1;\n                    const gridSize = window.gameConfig.settings.gridSize;\n                    const pieceWidth = puzzleArea.width / gridSize;\n                    const pieceHeight = puzzleArea.height / gridSize;\n                    \n                    for (let i = 1; i < gridSize; i++) {\n                        // Vertical lines\n                        ctx.beginPath();\n                        ctx.moveTo(puzzleArea.x + i * pieceWidth, puzzleArea.y);\n                        ctx.lineTo(puzzleArea.x + i * pieceWidth, puzzleArea.y + puzzleArea.height);\n                        ctx.stroke();\n                        \n                        // Horizontal lines\n                        ctx.beginPath();\n                        ctx.moveTo(puzzleArea.x, puzzleArea.y + i * pieceHeight);\n                        ctx.lineTo(puzzleArea.x + puzzleArea.width, puzzleArea.y + i * pieceHeight);\n                        ctx.stroke();\n                    }\n                }\n            }\n\n            // Draw a puzzle piece\n            function drawPiece(piece, isDragging = false) {\n                const puzzle = window.gameConfig.puzzles[currentPuzzleIndex];\n                const assetId = puzzle.assetId || puzzle.asset_id;\n                const asset = assetCache[assetId];\n                if (!asset) return;\n\n                ctx.save();\n                \n                // Position\n                ctx.translate(piece.currentX, piece.currentY);\n                \n                // Glow effect when near correct position\n                if (piece.glowing && !piece.locked) {\n                    ctx.shadowColor = '#ffffff';\n                    ctx.shadowBlur = 20;\n                }\n                \n                // Clip to piece shape\n                ctx.clip(piece.shape);\n                \n                // Draw portion of image\n                const img = asset.image;\n                const gridSize = window.gameConfig.settings.gridSize;\n                const srcPieceWidth = img.width / gridSize;\n                const srcPieceHeight = img.height / gridSize;\n                \n                ctx.drawImage(\n                    img,\n                    piece.col * srcPieceWidth,\n                    piece.row * srcPieceHeight,\n                    srcPieceWidth,\n                    srcPieceHeight,\n                    0,\n                    0,\n                    piece.width,\n                    piece.height\n                );\n                \n                // Piece border\n                ctx.strokeStyle = piece.locked ? 'rgba(212, 175, 55, 0.8)' : 'rgba(255, 255, 255, 0.6)';\n                ctx.lineWidth = 2;\n                ctx.stroke(piece.shape);\n                \n                ctx.restore();\n                \n                // Elevation shadow for dragging\n                if (isDragging) {\n                    ctx.save();\n                    ctx.translate(piece.currentX, piece.currentY);\n                    ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';\n                    ctx.fill(piece.shape);\n                    ctx.restore();\n                }\n            }\n\n            // Check if point is inside piece\n            function isPointInPiece(x, y, piece) {\n                ctx.save();\n                ctx.translate(piece.currentX, piece.currentY);\n                const result = ctx.isPointInPath(piece.shape, x, y);\n                ctx.restore();\n                return result;\n            }\n\n            // Get piece at position\n            function getPieceAt(x, y) {\n                // Check from top to bottom (reverse order)\n                for (let i = puzzlePieces.length - 1; i >= 0; i--) {\n                    const piece = puzzlePieces[i];\n                    if (!piece.locked && isPointInPiece(x, y, piece)) {\n                        return piece;\n                    }\n                }\n                return null;\n            }\n\n            // Check if piece is near correct position\n            function checkSnapPosition(piece) {\n                const dx = piece.currentX - piece.correctX;\n                const dy = piece.currentY - piece.correctY;\n                const distance = Math.sqrt(dx * dx + dy * dy);\n                const tolerance = window.gameConfig.settings.snapTolerance;\n                \n                piece.glowing = distance < tolerance && !piece.locked;\n                \n                return distance < tolerance;\n            }\n\n            // Snap piece to correct position\n            function snapPiece(piece) {\n                piece.currentX = piece.correctX;\n                piece.currentY = piece.correctY;\n                piece.locked = true;\n                piece.glowing = false;\n                completedPieces.add(piece);\n                \n                // Create sparkle effect\n                createSparkles(piece.correctX + piece.width / 2, piece.correctY + piece.height / 2);\n                \n                // Check if puzzle is complete\n                if (completedPieces.size === puzzlePieces.length) {\n                    isVictoryPreview = true;\n                    setTimeout(() => {\n                        isVictoryPreview = false;\n                        showCompletionModal();\n                    }, 3000);\n                }\n            }\n\n            // Create sparkle particles\n            function createSparkles(x, y) {\n                const overlay = document.querySelector('.ui-overlay');\n                for (let i = 0; i < 8; i++) {\n                    const sparkle = document.createElement('div');\n                    sparkle.className = 'sparkle';\n                    sparkle.style.left = x + 'px';\n                    sparkle.style.top = y + 'px';\n                    overlay.appendChild(sparkle);\n                    \n                    setTimeout(() => sparkle.remove(), 1000);\n                }\n            }\n\n            // Show completion modal and update player progress\n            // When a puzzle is completed:\n            // 1. Add to finishedPuzzles array (tracks solved puzzles)\n            // 2. Unlock next puzzle by adding to completedPuzzles array (tracks accessible puzzles)\n            // 3. Save state to persistent storage via lib.saveUserGameState()\n            function showCompletionModal() {\n                // Track finished puzzles\n                if (!userProgress.finishedPuzzles) userProgress.finishedPuzzles = [];\n                if (!userProgress.finishedPuzzles.includes(currentPuzzleIndex)) {\n                    userProgress.finishedPuzzles.push(currentPuzzleIndex);\n                }\n\n                const completed = userProgress.finishedPuzzles.length;\n                const total = window.gameConfig.puzzles.length;\n                completionText.textContent = `Puzzle ${currentPuzzleIndex + 1} completed! (${completed}/${total})`;\n                completionModal.classList.remove('hidden');\n                \n                // Unlock next puzzle for sequential mode\n                if (window.gameConfig.settings.sequentialUnlock && currentPuzzleIndex < total - 1) {\n                    if (!userProgress.completedPuzzles.includes(currentPuzzleIndex + 1)) {\n                        userProgress.completedPuzzles.push(currentPuzzleIndex + 1);\n                        userProgress.currentPuzzle = currentPuzzleIndex + 1;\n                    }\n                }\n                \n                // Save progress immediately after completion\n                saveUserProgress(userProgress);\n            }\n\n            // Touch/Mouse event handling\n            function getEventPosition(e) {\n                const rect = canvas.getBoundingClientRect();\n                const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;\n                const y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;\n                return { x, y };\n            }\n\n            function handleStart(e) {\n                if (currentScreen !== 'puzzle' || isVictoryPreview) return;\n                \n                const pos = getEventPosition(e);\n                const piece = getPieceAt(pos.x, pos.y);\n                \n                if (piece) {\n                    draggedPiece = piece;\n                    dragOffset.x = pos.x - piece.currentX;\n                    dragOffset.y = pos.y - piece.currentY;\n                    \n                    // Move piece to end of array (top of draw order)\n                    puzzlePieces = puzzlePieces.filter(p => p !== piece);\n                    puzzlePieces.push(piece);\n                }\n            }\n\n            function handleMove(e) {\n                if (!draggedPiece) return;\n                \n                const pos = getEventPosition(e);\n                draggedPiece.currentX = pos.x - dragOffset.x;\n                draggedPiece.currentY = pos.y - dragOffset.y;\n                \n                checkSnapPosition(draggedPiece);\n            }\n\n            function handleEnd(e) {\n                if (!draggedPiece) return;\n                \n                if (checkSnapPosition(draggedPiece)) {\n                    snapPiece(draggedPiece);\n                }\n                \n                draggedPiece = null;\n            }\n\n            // Event listeners\n            canvas.addEventListener('mousedown', handleStart);\n            canvas.addEventListener('touchstart', handleStart);\n            canvas.addEventListener('mousemove', handleMove);\n            canvas.addEventListener('touchmove', handleMove);\n            canvas.addEventListener('mouseup', handleEnd);\n            canvas.addEventListener('touchend', handleEnd);\n            canvas.addEventListener('mouseleave', handleEnd);\n\n            // Preview button\n            previewBtn.addEventListener('mousedown', () => { isPreviewMode = true; });\n            previewBtn.addEventListener('touchstart', () => { \n                if (!isMusicPlaying && bgMusicBuffer) toggleMusic();\n                isPreviewMode = true; \n            });\n            previewBtn.addEventListener('mouseup', () => { isPreviewMode = false; });\n            previewBtn.addEventListener('touchend', () => { isPreviewMode = false; });\n            previewBtn.addEventListener('mouseleave', () => { isPreviewMode = false; });\n\n            // Music button\n            musicBtn.addEventListener('click', () => {\n                toggleMusic();\n            });\n\n            // Start music on first interaction if not already playing\n            window.addEventListener('click', () => {\n                if (!isMusicPlaying && bgMusicBuffer) {\n                    toggleMusic();\n                }\n            }, { once: true });\n            window.addEventListener('touchstart', () => {\n                if (!isMusicPlaying && bgMusicBuffer) {\n                    toggleMusic();\n                }\n            }, { once: true });\n\n            // Back button\n            backBtn.addEventListener('click', () => {\n                showSelectionScreen();\n            });\n\n            // Continue button\n            continueBtn.addEventListener('click', () => {\n                completionModal.classList.add('hidden');\n                showSelectionScreen();\n            });\n\n            // Show selection screen\n            function showSelectionScreen() {\n                currentScreen = 'selection';\n                selectionScreen.classList.remove('hidden');\n                backBtn.classList.add('hidden');\n                previewBtn.classList.add('hidden');\n                titleEl.textContent = 'Dream Girl Puzzle';\n                subtitleEl.textContent = '';\n                \n                // Update progress\n                const completed = (userProgress.finishedPuzzles || []).length;\n                const total = window.gameConfig.puzzles.length;\n                progressText.textContent = `Puzzles Completed: ${completed}/${total}`;\n                \n                // Clear and rebuild puzzle grid\n                puzzleGrid.innerHTML = '';\n                \n                window.gameConfig.puzzles.forEach((puzzle, index) => {\n                    const card = document.createElement('div');\n                    card.className = 'puzzle-card';\n                    \n                    const isUnlocked = userProgress.completedPuzzles.includes(index) || \n                                     !window.gameConfig.settings.sequentialUnlock;\n                    const isCurrent = index === userProgress.currentPuzzle;\n                    \n                    if (!isUnlocked) {\n                        card.classList.add('locked');\n                    }\n                    \n                    if (isCurrent) {\n                        card.classList.add('current');\n                    }\n                    \n                    const assetId = puzzle.assetId || puzzle.asset_id;\n                    const asset = assetCache[assetId];\n                    if (asset && isUnlocked) {\n                        const img = document.createElement('img');\n                        img.src = asset.image.src;\n                        card.appendChild(img);\n                    }\n                    \n                    if (isUnlocked) {\n                        card.addEventListener('click', () => {\n                            showPuzzleScreen(index);\n                        });\n                    }\n                    \n                    puzzleGrid.appendChild(card);\n                });\n            }\n\n            // Show puzzle screen\n            function showPuzzleScreen(puzzleIndex) {\n                currentScreen = 'puzzle';\n                selectionScreen.classList.add('hidden');\n                backBtn.classList.remove('hidden');\n                previewBtn.classList.remove('hidden');\n                titleEl.textContent = 'Dream Girl Puzzle';\n                const total = window.gameConfig.puzzles.length;\n                subtitleEl.textContent = `Puzzle ${puzzleIndex + 1}/${total}`;\n                \n                initializePuzzle(puzzleIndex);\n            }\n\n            // Game loop\n            let lastTime = 0;\n            function gameLoop(timestamp) {\n                const deltaTime = Math.min((timestamp - lastTime) / 1000, 0.11);\n                lastTime = timestamp;\n\n                // Clear canvas\n                ctx.fillStyle = window.gameConfig.settings.backgroundColor;\n                ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n                if (currentScreen === 'puzzle') {\n                    // Draw puzzle background\n                    drawPuzzleBackground();\n                    \n                    if (!isVictoryPreview) {\n                        // Draw all pieces\n                        puzzlePieces.forEach(piece => {\n                            if (piece !== draggedPiece) {\n                                drawPiece(piece);\n                            }\n                        });\n                        \n                        // Draw dragged piece on top\n                        if (draggedPiece) {\n                            drawPiece(draggedPiece, true);\n                        }\n                        \n                        // Draw piece counter\n                        ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';\n                        ctx.fillRect(10, canvas.height - 60, 180, 50);\n                        ctx.fillStyle = '#ffffff';\n                        ctx.font = '20px Inter';\n                        ctx.textAlign = 'center';\n                        ctx.fillText(\n                            `${completedPieces.size}/${puzzlePieces.length} pieces`,\n                            100,\n                            canvas.height - 30\n                        );\n                    }\n                }\n\n                requestAnimationFrame(gameLoop);\n            }\n\n            // Start game\n            if (mode === 'edit') {\n                // Edit mode: Show selection screen with all puzzles unlocked\n                userProgress.completedPuzzles = Array.from({length: window.gameConfig.puzzles.length}, (_, i) => i);\n                userProgress.finishedPuzzles = [];\n                showSelectionScreen();\n            } else {\n                // Play mode: Load user progress\n                showSelectionScreen();\n            }\n\n            requestAnimationFrame(gameLoop);\n        }\n    </script>\n</body>\n</html>", "game_config": "{\n  \"puzzles\": [\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.03.15\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.00.48\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 12.51.40\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.24.43\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.43.19\\u202fAM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.20.49\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.13.00\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.12.08\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_32\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.16.13\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_23\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 8.47.28\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_38\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.16.44\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_35\"\n    },\n    {\n      \"asset_id\": \"portrait_36\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.50.26\\u202fAM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.15.08\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 8.57.32\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_34\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.44.52\\u202fAM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.15.45\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.18.15\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.17.10\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_01_new\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.22.35\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.10.31\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.06.46\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_39\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.14.05\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.25.24\\u202fAM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 8.58.01\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.09.43\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.19.48\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.27.51\\u202fAM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.27.20\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.30.11\\u202fAM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.26.55\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_33\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.01.46\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.08.35\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.13.22\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.02.19\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_37\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.23.20\\u202fAM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.22.10\\u202fPM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.33.21\\u202fAM.png\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-13 at 1.24.43\\u202fAM.png\"\n    },\n    {\n      \"asset_id\": \"portrait_31\"\n    },\n    {\n      \"asset_id\": \"Screenshot 2026-06-12 at 9.29.31\\u202fPM.png\"\n    }\n  ],\n  \"settings\": {\n    \"gridSize\": 3,\n    \"snapTolerance\": 50,\n    \"sequentialUnlock\": true,\n    \"backgroundColor\": \"#f9d3e0\"\n  }\n}", "game_design_document": "## Core Game Loop and Mechanics\n\n**Puzzle Queens** is a relaxing jigsaw puzzle game where players solve puzzles one at a time to unlock a collection of 30 beautiful portrait images.\n\n### Main Gameplay Flow:\n1. Player starts with the first puzzle unlocked\n2. Puzzle pieces are scattered randomly around the play area\n3. Player drags and drops pieces to assemble the complete image\n4. When pieces are placed close to their correct position, they snap into place\n5. Upon completing a puzzle, a celebration animation plays and the next puzzle unlocks\n6. Player progresses through all 30 puzzles sequentially\n\n### Puzzle Mechanics:\n- **Piece Count**: Start with 3x3 grid (9 pieces) for early puzzles, gradually increase to 4x4 (16 pieces) for later puzzles to add gentle difficulty progression\n- **Snap Tolerance**: Generous snap radius (40-50 pixels) to make placement forgiving and relaxing\n- **Visual Feedback**: Pieces glow or highlight when near correct position\n- **Piece Movement**: Smooth drag-and-drop with touch and mouse support\n- **Completed Pieces**: Lock in place once correctly positioned\n- **Background Reference**: Faded complete image visible in background to guide placement\n\n### Progression System:\n- Puzzles unlock sequentially (must complete puzzle N to unlock puzzle N+1)\n- Progress is saved automatically\n- Players can replay any previously completed puzzle\n- Completion percentage displayed (e.g., \"12/30 Completed\")\n\n### Win Condition:\n- Complete all pieces of current puzzle to unlock the next one\n- Ultimate goal: Complete all 30 puzzles\n\n## Starting Scope\n\n**First Implementation Should Include:**\n- 30 unique portrait images of attractive women in various styles (glamour, casual, artistic)\n- Puzzle selection screen showing locked/unlocked status\n- Single puzzle gameplay with 3x3 grid (9 pieces)\n- Drag-and-drop piece movement with snap-to-grid\n- Completion detection and celebration\n- Progress saving (which puzzles are unlocked)\n- Simple UI: back button, progress counter, puzzle preview\n\n**Leave for Future Iterations:**\n- Variable difficulty (4x4, 5x5 grids)\n- Hint system\n- Timer/scoring system\n- Piece rotation mechanic\n- Sound effects and background music\n\n## Game Perspective\n\n**fixed-screen** - Each puzzle occupies a single screen with no scrolling. The puzzle area is centered with pieces scattered around it.\n\n## Game World and Level Design\n\n### Puzzle Selection Screen:\n- Grid layout showing all 30 puzzle thumbnails (5 columns \u00d7 6 rows)\n- Locked puzzles show a lock icon overlay\n- Unlocked puzzles show thumbnail preview\n- Current puzzle highlighted\n- Progress indicator at top: \"Puzzles Completed: X/30\"\n- Tap any unlocked puzzle to play it\n\n### Puzzle Play Screen:\n- **Puzzle Area**: Central region (approximately 500\u00d7500px) where the completed puzzle forms\n- **Piece Scatter Area**: Surrounding space where unplaced pieces are randomly positioned\n- **Background**: Faded version of complete image visible in puzzle area as guide\n- **UI Elements**:\n  - Top bar: Back button (return to selection), puzzle number (e.g., \"Puzzle 5/30\")\n  - Preview button: Shows complete image temporarily when held\n  - Progress indicator: \"X/9 pieces placed\"\n\n### Visual Layout:\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 [\u2190] Puzzle 5/30    [\ud83d\udc41]     \u2502 \u2190 Top bar\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502                             \u2502\n\u2502  [piece]  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510      \u2502\n\u2502           \u2502         \u2502       \u2502\n\u2502           \u2502 PUZZLE  \u2502 [pc]  \u2502\n\u2502  [piece]  \u2502  AREA   \u2502       \u2502\n\u2502           \u2502         \u2502       \u2502\n\u2502     [pc]  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518       \u2502\n\u2502                             \u2502\n\u2502        [piece]    [piece]   \u2502\n\u2502                             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Edit Mode Design\n\nPlayers can customize these aspects in Edit mode:\n\n### Editable Properties:\n1. **Puzzle Images**: Replace any of the 30 puzzle images with custom uploads\n   - Constraint: Images should be portrait-oriented or square, minimum 600\u00d7600px recommended\n   \n2. **Difficulty Settings**:\n   - Piece count per puzzle (3\u00d73, 4\u00d74, or 5\u00d75)\n   - Snap tolerance (easy/medium/hard)\n   - Constraint: Minimum 2\u00d72, maximum 6\u00d76 grid\n\n3. **Visual Theme**:\n   - Background color/pattern\n   - Piece border style and color\n   - UI color scheme\n   \n4. **Progression**:\n   - Toggle sequential unlock (on/off) - if off, all puzzles available from start\n   - Reset progress button\n\n### Edit Mode UI:\n- Image upload slots for each of 30 puzzles\n- Sliders for difficulty settings\n- Color pickers for theme customization\n- \"Reset All Progress\" button with confirmation\n- \"Preview\" button to test current puzzle\n\n## Control Design\n\n**Mobile (Touch) Controls:**\n- **Drag Piece**: Touch and hold a piece, drag to move it\n- **Drop Piece**: Release touch to drop piece (snaps if close to correct position)\n- **Preview Image**: Touch and hold the eye icon button to see complete image\n- **Navigate**: Tap back button to return to selection screen\n- **Select Puzzle**: Tap any unlocked puzzle thumbnail\n\n**Desktop (Mouse/Keyboard) Controls:**\n- **Drag Piece**: Click and hold left mouse button on piece, drag to move\n- **Drop Piece**: Release mouse button to drop (snaps if close to correct position)\n- **Preview Image**: Hold mouse button on eye icon or press and hold SPACE key\n- **Navigate**: Click back button or press ESC to return to selection\n- **Select Puzzle**: Click any unlocked puzzle thumbnail\n\n**Implementation Notes:**\n- Track touch/mouse ID to prevent multi-touch interference\n- Bring dragged piece to front (highest z-index)\n- Show visual feedback when piece is in snap range (glow effect)\n- Smooth animation when piece snaps into place\n- Prevent dragging pieces that are already correctly placed\n\n## Art Style and Visual Theme\n\n### Overall Aesthetic:\n- **Modern and Clean**: Minimalist UI with elegant typography\n- **Sophisticated**: Mature, tasteful presentation\n- **Polished**: Smooth animations and professional visual design\n\n### Color Palette:\n- **Primary**: Deep purple/magenta (#8B3A8B) for accents and highlights\n- **Secondary**: Soft gold (#D4AF37) for completion effects\n- **Background**: Dark gradient (deep navy to black) for sophistication\n- **UI Elements**: White text with subtle shadows for readability\n- **Piece Borders**: Subtle white/light gray outlines\n\n### Visual Elements:\n- **Puzzle Pieces**: Realistic jigsaw piece shapes with interlocking tabs and blanks\n- **Locked Puzzles**: Semi-transparent with lock icon overlay\n- **Completion Effect**: Golden sparkle/shimmer animation\n- **Piece Glow**: Soft white glow when near correct position\n- **Background Reference**: 30% opacity version of complete image\n\n### Asset Rendering Strategy:\n- **Puzzle Images**: Generated assets (30 unique portraits)\n- **UI Elements**: CSS/Canvas rendering (buttons, borders, progress bars)\n- **Puzzle Pieces**: Canvas rendering with clipping paths for jigsaw shapes\n- **Lock Icons**: CSS/SVG for crisp scaling\n- **Effects**: Canvas particles for celebration animation\n\n### Portrait Image Style:\nAll 30 puzzle images should feature:\n- Attractive women in various poses and settings\n- Professional photography quality\n- Diverse styles: glamour shots, casual portraits, artistic photography\n- Appropriate clothing (tasteful, non-explicit)\n- Good lighting and composition\n- Portrait or square orientation\n- High resolution (at least 800\u00d7800px)", "persistent_storage_config": null, "game_metadata": {"game_id": "01KTXJZWHW8FMYQ8D5PP8XEXN4", "title": "Dream Girl Puzzle", "description": "Puzzle out your Dream Gril.", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_974fce71-e86c-4fce-8758-93a8acead251.webp", "created_at": 1781257019, "updated_at": 1781508584242.7693, "art_style": null}, "task_plan": null}