diff --git a/.gitignore b/.gitignore index 17656f6..d3c971a 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .venv/ +node_modules/ +assets/games/two-rivers/images/characters/*-source.png output/ .org-timestamps/ backups/ diff --git a/assets/games/two-rivers/audio/ambience/birds.mp3 b/assets/games/two-rivers/audio/ambience/birds.mp3 new file mode 100644 index 0000000..769e621 Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/birds.mp3 differ diff --git a/assets/games/two-rivers/audio/ambience/birds.webm b/assets/games/two-rivers/audio/ambience/birds.webm new file mode 100644 index 0000000..0e0b913 Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/birds.webm differ diff --git a/assets/games/two-rivers/audio/ambience/crowd.mp3 b/assets/games/two-rivers/audio/ambience/crowd.mp3 new file mode 100644 index 0000000..49fbee8 Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/crowd.mp3 differ diff --git a/assets/games/two-rivers/audio/ambience/crowd.webm b/assets/games/two-rivers/audio/ambience/crowd.webm new file mode 100644 index 0000000..66758b3 Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/crowd.webm differ diff --git a/assets/games/two-rivers/audio/ambience/garden-insects.mp3 b/assets/games/two-rivers/audio/ambience/garden-insects.mp3 new file mode 100644 index 0000000..769e621 Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/garden-insects.mp3 differ diff --git a/assets/games/two-rivers/audio/ambience/garden-insects.webm b/assets/games/two-rivers/audio/ambience/garden-insects.webm new file mode 100644 index 0000000..e0e682f Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/garden-insects.webm differ diff --git a/assets/games/two-rivers/audio/ambience/library-room.mp3 b/assets/games/two-rivers/audio/ambience/library-room.mp3 new file mode 100644 index 0000000..39e481e Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/library-room.mp3 differ diff --git a/assets/games/two-rivers/audio/ambience/library-room.webm b/assets/games/two-rivers/audio/ambience/library-room.webm new file mode 100644 index 0000000..906ca04 Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/library-room.webm differ diff --git a/assets/games/two-rivers/audio/ambience/rain.mp3 b/assets/games/two-rivers/audio/ambience/rain.mp3 new file mode 100644 index 0000000..b35bb5d Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/rain.mp3 differ diff --git a/assets/games/two-rivers/audio/ambience/rain.webm b/assets/games/two-rivers/audio/ambience/rain.webm new file mode 100644 index 0000000..9125e14 Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/rain.webm differ diff --git a/assets/games/two-rivers/audio/ambience/water.mp3 b/assets/games/two-rivers/audio/ambience/water.mp3 new file mode 100644 index 0000000..769e621 Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/water.mp3 differ diff --git a/assets/games/two-rivers/audio/ambience/water.webm b/assets/games/two-rivers/audio/ambience/water.webm new file mode 100644 index 0000000..88e120d Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/water.webm differ diff --git a/assets/games/two-rivers/audio/ambience/wind.mp3 b/assets/games/two-rivers/audio/ambience/wind.mp3 new file mode 100644 index 0000000..39e481e Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/wind.mp3 differ diff --git a/assets/games/two-rivers/audio/ambience/wind.webm b/assets/games/two-rivers/audio/ambience/wind.webm new file mode 100644 index 0000000..6ef67aa Binary files /dev/null and b/assets/games/two-rivers/audio/ambience/wind.webm differ diff --git a/assets/games/two-rivers/audio/music/ending.mp3 b/assets/games/two-rivers/audio/music/ending.mp3 new file mode 100644 index 0000000..09e0af6 Binary files /dev/null and b/assets/games/two-rivers/audio/music/ending.mp3 differ diff --git a/assets/games/two-rivers/audio/music/ending.webm b/assets/games/two-rivers/audio/music/ending.webm new file mode 100644 index 0000000..e0cf5a8 Binary files /dev/null and b/assets/games/two-rivers/audio/music/ending.webm differ diff --git a/assets/games/two-rivers/audio/music/festival.mp3 b/assets/games/two-rivers/audio/music/festival.mp3 new file mode 100644 index 0000000..09e0af6 Binary files /dev/null and b/assets/games/two-rivers/audio/music/festival.mp3 differ diff --git a/assets/games/two-rivers/audio/music/festival.webm b/assets/games/two-rivers/audio/music/festival.webm new file mode 100644 index 0000000..81f1de3 Binary files /dev/null and b/assets/games/two-rivers/audio/music/festival.webm differ diff --git a/assets/games/two-rivers/audio/music/garden.mp3 b/assets/games/two-rivers/audio/music/garden.mp3 new file mode 100644 index 0000000..769e621 Binary files /dev/null and b/assets/games/two-rivers/audio/music/garden.mp3 differ diff --git a/assets/games/two-rivers/audio/music/garden.webm b/assets/games/two-rivers/audio/music/garden.webm new file mode 100644 index 0000000..036dba4 Binary files /dev/null and b/assets/games/two-rivers/audio/music/garden.webm differ diff --git a/assets/games/two-rivers/audio/music/marketplace.mp3 b/assets/games/two-rivers/audio/music/marketplace.mp3 new file mode 100644 index 0000000..49fbee8 Binary files /dev/null and b/assets/games/two-rivers/audio/music/marketplace.mp3 differ diff --git a/assets/games/two-rivers/audio/music/marketplace.webm b/assets/games/two-rivers/audio/music/marketplace.webm new file mode 100644 index 0000000..ebfe6d6 Binary files /dev/null and b/assets/games/two-rivers/audio/music/marketplace.webm differ diff --git a/assets/games/two-rivers/audio/music/morning.mp3 b/assets/games/two-rivers/audio/music/morning.mp3 new file mode 100644 index 0000000..49fbee8 Binary files /dev/null and b/assets/games/two-rivers/audio/music/morning.mp3 differ diff --git a/assets/games/two-rivers/audio/music/morning.webm b/assets/games/two-rivers/audio/music/morning.webm new file mode 100644 index 0000000..2355798 Binary files /dev/null and b/assets/games/two-rivers/audio/music/morning.webm differ diff --git a/assets/games/two-rivers/audio/music/night.mp3 b/assets/games/two-rivers/audio/music/night.mp3 new file mode 100644 index 0000000..b35bb5d Binary files /dev/null and b/assets/games/two-rivers/audio/music/night.mp3 differ diff --git a/assets/games/two-rivers/audio/music/night.webm b/assets/games/two-rivers/audio/music/night.webm new file mode 100644 index 0000000..c3a8997 Binary files /dev/null and b/assets/games/two-rivers/audio/music/night.webm differ diff --git a/assets/games/two-rivers/audio/music/palace.mp3 b/assets/games/two-rivers/audio/music/palace.mp3 new file mode 100644 index 0000000..09e0af6 Binary files /dev/null and b/assets/games/two-rivers/audio/music/palace.mp3 differ diff --git a/assets/games/two-rivers/audio/music/palace.webm b/assets/games/two-rivers/audio/music/palace.webm new file mode 100644 index 0000000..175d66a Binary files /dev/null and b/assets/games/two-rivers/audio/music/palace.webm differ diff --git a/assets/games/two-rivers/audio/music/romantic.mp3 b/assets/games/two-rivers/audio/music/romantic.mp3 new file mode 100644 index 0000000..769e621 Binary files /dev/null and b/assets/games/two-rivers/audio/music/romantic.mp3 differ diff --git a/assets/games/two-rivers/audio/music/romantic.webm b/assets/games/two-rivers/audio/music/romantic.webm new file mode 100644 index 0000000..8f735a2 Binary files /dev/null and b/assets/games/two-rivers/audio/music/romantic.webm differ diff --git a/assets/games/two-rivers/audio/music/sad.mp3 b/assets/games/two-rivers/audio/music/sad.mp3 new file mode 100644 index 0000000..39e481e Binary files /dev/null and b/assets/games/two-rivers/audio/music/sad.mp3 differ diff --git a/assets/games/two-rivers/audio/music/sad.webm b/assets/games/two-rivers/audio/music/sad.webm new file mode 100644 index 0000000..5bda610 Binary files /dev/null and b/assets/games/two-rivers/audio/music/sad.webm differ diff --git a/assets/games/two-rivers/audio/music/victory.mp3 b/assets/games/two-rivers/audio/music/victory.mp3 new file mode 100644 index 0000000..09e0af6 Binary files /dev/null and b/assets/games/two-rivers/audio/music/victory.mp3 differ diff --git a/assets/games/two-rivers/audio/music/victory.webm b/assets/games/two-rivers/audio/music/victory.webm new file mode 100644 index 0000000..33cb03a Binary files /dev/null and b/assets/games/two-rivers/audio/music/victory.webm differ diff --git a/assets/games/two-rivers/audio/sfx/archery.mp3 b/assets/games/two-rivers/audio/sfx/archery.mp3 new file mode 100644 index 0000000..9cf340c Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/archery.mp3 differ diff --git a/assets/games/two-rivers/audio/sfx/archery.webm b/assets/games/two-rivers/audio/sfx/archery.webm new file mode 100644 index 0000000..54ce22d Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/archery.webm differ diff --git a/assets/games/two-rivers/audio/sfx/cooking.mp3 b/assets/games/two-rivers/audio/sfx/cooking.mp3 new file mode 100644 index 0000000..1ef623f Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/cooking.mp3 differ diff --git a/assets/games/two-rivers/audio/sfx/cooking.webm b/assets/games/two-rivers/audio/sfx/cooking.webm new file mode 100644 index 0000000..1c3430f Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/cooking.webm differ diff --git a/assets/games/two-rivers/audio/sfx/dance.mp3 b/assets/games/two-rivers/audio/sfx/dance.mp3 new file mode 100644 index 0000000..82ea00a Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/dance.mp3 differ diff --git a/assets/games/two-rivers/audio/sfx/dance.webm b/assets/games/two-rivers/audio/sfx/dance.webm new file mode 100644 index 0000000..04d85f3 Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/dance.webm differ diff --git a/assets/games/two-rivers/audio/sfx/footstep.mp3 b/assets/games/two-rivers/audio/sfx/footstep.mp3 new file mode 100644 index 0000000..82ea00a Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/footstep.mp3 differ diff --git a/assets/games/two-rivers/audio/sfx/footstep.webm b/assets/games/two-rivers/audio/sfx/footstep.webm new file mode 100644 index 0000000..adf08db Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/footstep.webm differ diff --git a/assets/games/two-rivers/audio/sfx/page-turn.mp3 b/assets/games/two-rivers/audio/sfx/page-turn.mp3 new file mode 100644 index 0000000..f719dbd Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/page-turn.mp3 differ diff --git a/assets/games/two-rivers/audio/sfx/page-turn.webm b/assets/games/two-rivers/audio/sfx/page-turn.webm new file mode 100644 index 0000000..e9c663b Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/page-turn.webm differ diff --git a/assets/games/two-rivers/audio/sfx/pickup.mp3 b/assets/games/two-rivers/audio/sfx/pickup.mp3 new file mode 100644 index 0000000..103dc02 Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/pickup.mp3 differ diff --git a/assets/games/two-rivers/audio/sfx/pickup.webm b/assets/games/two-rivers/audio/sfx/pickup.webm new file mode 100644 index 0000000..f6e9849 Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/pickup.webm differ diff --git a/assets/games/two-rivers/audio/sfx/puzzle.mp3 b/assets/games/two-rivers/audio/sfx/puzzle.mp3 new file mode 100644 index 0000000..c8156b0 Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/puzzle.mp3 differ diff --git a/assets/games/two-rivers/audio/sfx/puzzle.webm b/assets/games/two-rivers/audio/sfx/puzzle.webm new file mode 100644 index 0000000..e8c780f Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/puzzle.webm differ diff --git a/assets/games/two-rivers/audio/sfx/ui.mp3 b/assets/games/two-rivers/audio/sfx/ui.mp3 new file mode 100644 index 0000000..7bd4e30 Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/ui.mp3 differ diff --git a/assets/games/two-rivers/audio/sfx/ui.webm b/assets/games/two-rivers/audio/sfx/ui.webm new file mode 100644 index 0000000..936f4d9 Binary files /dev/null and b/assets/games/two-rivers/audio/sfx/ui.webm differ diff --git a/assets/games/two-rivers/data/achievements.json b/assets/games/two-rivers/data/achievements.json new file mode 100644 index 0000000..379e809 --- /dev/null +++ b/assets/games/two-rivers/data/achievements.json @@ -0,0 +1,14 @@ +{ + "first_spark": "Make Lima laugh in the palace courtyard.", + "lantern_reader": "Solve the library cipher.", + "true_aim": "Score four or more bullseyes.", + "kitchen_conspiracy": "Complete the recipe without a mistake.", + "two_left_feet": "Finish the dance, graceful or otherwise.", + "garland_for_all": "Complete A Garland for Everyone.", + "letters_home": "Complete The Bookseller's Letters.", + "thoughtful_gift": "Give Lima a gift.", + "open_book": "Reach Deep Trust.", + "royal_favourite": "Reach Deep Family Approval.", + "moonrise": "Complete the story.", + "every_current": "Unlock all four endings." +} diff --git a/assets/games/two-rivers/data/assets.json b/assets/games/two-rivers/data/assets.json new file mode 100644 index 0000000..ab256a8 --- /dev/null +++ b/assets/games/two-rivers/data/assets.json @@ -0,0 +1,71 @@ +{ + "version": 1, + "images": [ + {"id": "courtyard", "type": "background", "url": "/assets/games/two-rivers/images/backgrounds/courtyard.webp", "prompt": "prompts/backgrounds.md#palace-courtyard"}, + {"id": "library", "type": "background", "url": "/assets/games/two-rivers/images/backgrounds/library.webp", "prompt": "prompts/backgrounds.md#royal-library"}, + {"id": "garden", "type": "background", "url": "/assets/games/two-rivers/images/backgrounds/garden.webp", "prompt": "prompts/backgrounds.md#moon-garden"}, + {"id": "market", "type": "background", "url": "/assets/games/two-rivers/images/backgrounds/market.webp", "prompt": "prompts/backgrounds.md#old-marketplace"}, + {"id": "pavilion", "type": "background", "url": "/assets/games/two-rivers/images/backgrounds/pavilion.webp", "prompt": "prompts/backgrounds.md#riverside-pavilion"}, + {"id": "z", "type": "character", "url": "/assets/games/two-rivers/images/characters/z.webp", "prompt": "prompts/characters.md#z"}, + {"id": "lima", "type": "character", "url": "/assets/games/two-rivers/images/characters/lima.webp", "prompt": "prompts/characters.md#lima"}, + {"id": "anwara", "type": "npc", "url": "/assets/games/two-rivers/images/characters/anwara.webp", "prompt": "prompts/characters/supporting-cast.md#keeper-anwara"}, + {"id": "saira", "type": "npc", "url": "/assets/games/two-rivers/images/characters/saira.webp", "prompt": "prompts/characters/supporting-cast.md#saira"}, + {"id": "nadia", "type": "npc", "url": "/assets/games/two-rivers/images/characters/nadia.webp", "prompt": "prompts/characters/supporting-cast.md#nadia"}, + {"id": "rayhan", "type": "npc", "url": "/assets/games/two-rivers/images/characters/rayhan.webp", "prompt": "prompts/characters/supporting-cast.md#rayhan"}, + {"id": "mira", "type": "npc", "url": "/assets/games/two-rivers/images/characters/mira.webp", "prompt": "prompts/characters/supporting-cast.md#mira"}, + {"id": "ishan", "type": "npc", "url": "/assets/games/two-rivers/images/characters/ishan.webp", "prompt": "prompts/characters/supporting-cast.md#ishan"}, + {"id": "inkstone", "type": "item", "url": "/assets/games/two-rivers/images/items/inkstone.webp", "prompt": "prompts/items/inkstone.md"}, + {"id": "brass_compass", "type": "item", "url": "/assets/games/two-rivers/images/items/brass_compass.webp", "prompt": "prompts/items/brass-compass.md"}, + {"id": "poetry_book", "type": "item", "url": "/assets/games/two-rivers/images/items/poetry_book.webp", "prompt": "prompts/items/poetry-book.md"}, + {"id": "embroidered_ribbon", "type": "item", "url": "/assets/games/two-rivers/images/items/embroidered_ribbon.webp", "prompt": "prompts/items/embroidered-ribbon.md"}, + {"id": "spiced_tea", "type": "item", "url": "/assets/games/two-rivers/images/items/spiced_tea.webp", "prompt": "prompts/items/spiced-tea.md"}, + {"id": "archery_charm", "type": "item", "url": "/assets/games/two-rivers/images/items/archery_charm.webp", "prompt": "prompts/items/archery-charm.md"}, + {"id": "folio_ribbon", "type": "item", "url": "/assets/games/two-rivers/images/items/folio_ribbon.webp", "prompt": "prompts/items/folio-ribbon.md"}, + {"id": "lantern_rubbing", "type": "item", "url": "/assets/games/two-rivers/images/items/lantern_rubbing.webp", "prompt": "prompts/items/lantern-rubbing.md"}, + {"id": "anwara_note", "type": "item", "url": "/assets/games/two-rivers/images/items/anwara_note.webp", "prompt": "prompts/items/anwara-note.md"}, + {"id": "founders_record", "type": "item", "url": "/assets/games/two-rivers/images/items/founders_record.webp", "prompt": "prompts/items/founders-record.md"}, + {"id": "jasmine", "type": "item", "url": "/assets/games/two-rivers/images/items/jasmine.webp", "prompt": "prompts/items/jasmine.md"}, + {"id": "tuberose", "type": "item", "url": "/assets/games/two-rivers/images/items/tuberose.webp", "prompt": "prompts/items/tuberose.md"}, + {"id": "marigold", "type": "item", "url": "/assets/games/two-rivers/images/items/marigold.webp", "prompt": "prompts/items/marigold.md"}, + {"id": "rose", "type": "item", "url": "/assets/games/two-rivers/images/items/rose.webp", "prompt": "prompts/items/rose.md"}, + {"id": "bakul", "type": "item", "url": "/assets/games/two-rivers/images/items/bakul.webp", "prompt": "prompts/items/bakul.md"}, + {"id": "letter_1", "type": "item", "url": "/assets/games/two-rivers/images/items/letter_1.webp", "prompt": "prompts/items/letter-1.md"}, + {"id": "letter_2", "type": "item", "url": "/assets/games/two-rivers/images/items/letter_2.webp", "prompt": "prompts/items/letter-2.md"}, + {"id": "letter_3", "type": "item", "url": "/assets/games/two-rivers/images/items/letter_3.webp", "prompt": "prompts/items/letter-3.md"}, + {"id": "ending-scholar", "type": "cg", "url": "/assets/games/two-rivers/images/cg/scholar.webp", "prompt": "prompts/cg.md#scholars-promise"}, + {"id": "ending-laughter", "type": "cg", "url": "/assets/games/two-rivers/images/cg/laughter.webp", "prompt": "prompts/cg.md#laughter-after-midnight"}, + {"id": "ending-road", "type": "cg", "url": "/assets/games/two-rivers/images/cg/road.webp", "prompt": "prompts/cg.md#road-beyond-palace"}, + {"id": "ending-families", "type": "cg", "url": "/assets/games/two-rivers/images/cg/families.webp", "prompt": "prompts/cg.md#two-families"}, + {"id": "garland", "type": "cg", "url": "/assets/games/two-rivers/images/cg/garland.webp", "prompt": "prompts/cg.md#garland"}, + {"id": "cg-library", "type": "cg", "url": "/assets/games/two-rivers/images/cg/library.webp", "prompt": "prompts/cg.md#library"} + ], + "audio": [ + {"id": "morning", "type": "music", "url": "/assets/games/two-rivers/audio/music/morning.webm", "urls": ["/assets/games/two-rivers/audio/music/morning.webm", "/assets/games/two-rivers/audio/music/morning.mp3"], "source": "/assets/audio/princess-lima/village-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + {"id": "palace", "type": "music", "url": "/assets/games/two-rivers/audio/music/palace.webm", "urls": ["/assets/games/two-rivers/audio/music/palace.webm", "/assets/games/two-rivers/audio/music/palace.mp3"], "source": "/assets/audio/princess-lima/victory-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + {"id": "garden", "type": "music", "url": "/assets/games/two-rivers/audio/music/garden.webm", "urls": ["/assets/games/two-rivers/audio/music/garden.webm", "/assets/games/two-rivers/audio/music/garden.mp3"], "source": "/assets/audio/princess-lima/forest-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + {"id": "marketplace", "type": "music", "url": "/assets/games/two-rivers/audio/music/marketplace.webm", "urls": ["/assets/games/two-rivers/audio/music/marketplace.webm", "/assets/games/two-rivers/audio/music/marketplace.mp3"], "source": "/assets/audio/princess-lima/village-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + {"id": "festival", "type": "music", "url": "/assets/games/two-rivers/audio/music/festival.webm", "urls": ["/assets/games/two-rivers/audio/music/festival.webm", "/assets/games/two-rivers/audio/music/festival.mp3"], "source": "/assets/audio/princess-lima/victory-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + {"id": "romantic", "type": "music", "url": "/assets/games/two-rivers/audio/music/romantic.webm", "urls": ["/assets/games/two-rivers/audio/music/romantic.webm", "/assets/games/two-rivers/audio/music/romantic.mp3"], "source": "/assets/audio/princess-lima/forest-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + {"id": "sad", "type": "music", "url": "/assets/games/two-rivers/audio/music/sad.webm", "urls": ["/assets/games/two-rivers/audio/music/sad.webm", "/assets/games/two-rivers/audio/music/sad.mp3"], "source": "/assets/audio/princess-lima/mountain-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + {"id": "night", "type": "music", "url": "/assets/games/two-rivers/audio/music/night.webm", "urls": ["/assets/games/two-rivers/audio/music/night.webm", "/assets/games/two-rivers/audio/music/night.mp3"], "source": "/assets/audio/princess-lima/fortress-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + {"id": "victory", "type": "music", "url": "/assets/games/two-rivers/audio/music/victory.webm", "urls": ["/assets/games/two-rivers/audio/music/victory.webm", "/assets/games/two-rivers/audio/music/victory.mp3"], "source": "/assets/audio/princess-lima/victory-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + {"id": "ending", "type": "music", "url": "/assets/games/two-rivers/audio/music/ending.webm", "urls": ["/assets/games/two-rivers/audio/music/ending.webm", "/assets/games/two-rivers/audio/music/ending.mp3"], "source": "/assets/audio/princess-lima/victory-theme.wav", "licence": "Original site-owned instrumental; local derivative encodes"}, + + {"id": "birds", "type": "ambience", "url": "/assets/games/two-rivers/audio/ambience/birds.webm", "urls": ["/assets/games/two-rivers/audio/ambience/birds.webm", "/assets/games/two-rivers/audio/ambience/birds.mp3"], "source": "/assets/audio/princess-lima/forest-theme.wav", "licence": "Original site-owned ambience derivative"}, + {"id": "water", "type": "ambience", "url": "/assets/games/two-rivers/audio/ambience/water.webm", "urls": ["/assets/games/two-rivers/audio/ambience/water.webm", "/assets/games/two-rivers/audio/ambience/water.mp3"], "source": "/assets/audio/princess-lima/forest-theme.wav", "licence": "Original site-owned ambience derivative"}, + {"id": "wind", "type": "ambience", "url": "/assets/games/two-rivers/audio/ambience/wind.webm", "urls": ["/assets/games/two-rivers/audio/ambience/wind.webm", "/assets/games/two-rivers/audio/ambience/wind.mp3"], "source": "/assets/audio/princess-lima/mountain-theme.wav", "licence": "Original site-owned ambience derivative"}, + {"id": "crowd", "type": "ambience", "url": "/assets/games/two-rivers/audio/ambience/crowd.webm", "urls": ["/assets/games/two-rivers/audio/ambience/crowd.webm", "/assets/games/two-rivers/audio/ambience/crowd.mp3"], "source": "/assets/audio/princess-lima/village-theme.wav", "licence": "Original site-owned ambience derivative"}, + {"id": "garden-insects", "type": "ambience", "url": "/assets/games/two-rivers/audio/ambience/garden-insects.webm", "urls": ["/assets/games/two-rivers/audio/ambience/garden-insects.webm", "/assets/games/two-rivers/audio/ambience/garden-insects.mp3"], "source": "/assets/audio/princess-lima/forest-theme.wav", "licence": "Original site-owned ambience derivative"}, + {"id": "library", "type": "ambience", "url": "/assets/games/two-rivers/audio/ambience/library-room.webm", "urls": ["/assets/games/two-rivers/audio/ambience/library-room.webm", "/assets/games/two-rivers/audio/ambience/library-room.mp3"], "source": "/assets/audio/princess-lima/mountain-theme.wav", "licence": "Original site-owned ambience derivative"}, + {"id": "rain", "type": "ambience", "url": "/assets/games/two-rivers/audio/ambience/rain.webm", "urls": ["/assets/games/two-rivers/audio/ambience/rain.webm", "/assets/games/two-rivers/audio/ambience/rain.mp3"], "source": "/assets/audio/princess-lima/fortress-theme.wav", "licence": "Original site-owned ambience derivative"}, + + {"id": "ui", "type": "sfx", "url": "/assets/games/two-rivers/audio/sfx/ui.webm", "urls": ["/assets/games/two-rivers/audio/sfx/ui.webm", "/assets/games/two-rivers/audio/sfx/ui.mp3"], "source": "/assets/audio/princess-lima/quest.wav", "licence": "Original site-owned sound effect derivative"}, + {"id": "pickup", "type": "sfx", "url": "/assets/games/two-rivers/audio/sfx/pickup.webm", "urls": ["/assets/games/two-rivers/audio/sfx/pickup.webm", "/assets/games/two-rivers/audio/sfx/pickup.mp3"], "source": "/assets/audio/princess-lima/pickup.wav", "licence": "Original site-owned sound effect derivative"}, + {"id": "puzzle", "type": "sfx", "url": "/assets/games/two-rivers/audio/sfx/puzzle.webm", "urls": ["/assets/games/two-rivers/audio/sfx/puzzle.webm", "/assets/games/two-rivers/audio/sfx/puzzle.mp3"], "source": "/assets/audio/princess-lima/puzzle.wav", "licence": "Original site-owned sound effect derivative"}, + {"id": "archery", "type": "sfx", "url": "/assets/games/two-rivers/audio/sfx/archery.webm", "urls": ["/assets/games/two-rivers/audio/sfx/archery.webm", "/assets/games/two-rivers/audio/sfx/archery.mp3"], "source": "/assets/audio/princess-lima/attack.wav", "licence": "Original site-owned sound effect derivative"}, + {"id": "cooking", "type": "sfx", "url": "/assets/games/two-rivers/audio/sfx/cooking.webm", "urls": ["/assets/games/two-rivers/audio/sfx/cooking.webm", "/assets/games/two-rivers/audio/sfx/cooking.mp3"], "source": "/assets/audio/princess-lima/hit.wav", "licence": "Original site-owned sound effect derivative"}, + {"id": "dance", "type": "sfx", "url": "/assets/games/two-rivers/audio/sfx/dance.webm", "urls": ["/assets/games/two-rivers/audio/sfx/dance.webm", "/assets/games/two-rivers/audio/sfx/dance.mp3"], "source": "/assets/audio/princess-lima/step.wav", "licence": "Original site-owned sound effect derivative"}, + {"id": "footstep", "type": "sfx", "url": "/assets/games/two-rivers/audio/sfx/footstep.webm", "urls": ["/assets/games/two-rivers/audio/sfx/footstep.webm", "/assets/games/two-rivers/audio/sfx/footstep.mp3"], "source": "/assets/audio/princess-lima/step.wav", "licence": "Original site-owned sound effect derivative"}, + {"id": "page-turn", "type": "sfx", "url": "/assets/games/two-rivers/audio/sfx/page-turn.webm", "urls": ["/assets/games/two-rivers/audio/sfx/page-turn.webm", "/assets/games/two-rivers/audio/sfx/page-turn.mp3"], "source": "/assets/audio/princess-lima/door.wav", "licence": "Original site-owned sound effect derivative"} + ] +} diff --git a/assets/games/two-rivers/data/characters.json b/assets/games/two-rivers/data/characters.json new file mode 100644 index 0000000..1bc5915 --- /dev/null +++ b/assets/games/two-rivers/data/characters.json @@ -0,0 +1,22 @@ +{ + "z": { + "name": "Z", + "age": 27, + "pronouns": "he/him", + "role": "A Pakistani visiting scholar and amateur archer", + "expressions": ["warm", "wry", "thoughtful", "flustered", "determined", "tender"] + }, + "lima": { + "name": "Princess Lima", + "age": 26, + "pronouns": "she/her", + "role": "A Bengali princess and curator of the Festival of Two Rivers", + "expressions": ["warm", "playful", "regal", "curious", "vulnerable", "tender", "triumphant"] + }, + "anwara": {"name": "Keeper Anwara", "age": 58, "role": "Senior keeper of the royal archive"}, + "saira": {"name": "Saira", "age": 49, "role": "Lima's aunt and chair of the festival council"}, + "nadia": {"name": "Nadia", "age": 34, "role": "A bookseller and collector of letters"}, + "rayhan": {"name": "Rayhan", "age": 31, "role": "The festival's cheerful head cook"}, + "mira": {"name": "Mira", "age": 29, "role": "A gardener and garland artist"}, + "ishan": {"name": "Ishan", "age": 45, "role": "Master of ceremonies"} +} diff --git a/assets/games/two-rivers/data/dialogue/chapters.json b/assets/games/two-rivers/data/dialogue/chapters.json new file mode 100644 index 0000000..366f80f --- /dev/null +++ b/assets/games/two-rivers/data/dialogue/chapters.json @@ -0,0 +1,96 @@ +{ + "delegation_walk": { + "start": "one", + "nodes": { + "one": {"speaker": "lima", "expression": "curious", "text": "Before the council turns you into a visiting title with polished shoes, walk with me. The courtyard makes more sense from its edges. Those blue banners were dyed by a family near the eastern canal; the gold ones came with your delegation. I wanted the colours to meet without pretending they had always been the same.", "next": "two"}, + "two": {"speaker": "z", "expression": "thoughtful", "text": "At home, people sometimes speak of hospitality as though it means erasing the seam. My mother says a good host lets you see where the cloth was joined, because that is where the work and affection live. I think she would approve of your banners—and ask who climbed the ladder to hang them.", "next": "three"}, + "three": {"speaker": "lima", "expression": "playful", "text": "I climbed the first ladder. Ishan discovered me, delivered a speech about royal ankles, and assigned three guards to the remaining banners. Being a princess is frequently the art of doing something useful before protocol notices. Being a scholar, I assume, is doing something reckless and adding footnotes afterward.", "next": "four"}, + "four": {"speaker": "z", "expression": "wry", "text": "Only the finest footnotes. Mine would record that the curator had excellent balance, questionable respect for ceremonial ladders, and a smile capable of making a witness forget the relevant safety regulation.", "choices": [ + {"id": "flirt", "text": "Admit that the smile is distracting.", "effects": [{"type": "relationship", "key": "romance", "amount": 4}, {"type": "relationship", "key": "humour", "amount": 2}], "next": "five"}, + {"id": "candour", "text": "Say you admire how she credits the makers.", "effects": [{"type": "relationship", "key": "respect", "amount": 4}, {"type": "relationship", "key": "trust", "amount": 2}], "next": "five"}, + {"id": "adventure", "text": "Offer to hold the next ladder.", "effects": [{"type": "relationship", "key": "adventure", "amount": 4}, {"type": "relationship", "key": "comfort", "amount": 2}], "next": "five"} + ]}, + "five": {"speaker": "lima", "expression": "warm", "text": "That answer may earn you a second circuit of the courtyard. Look there: cooks testing clay ovens, gardeners threading garlands, copyists checking name cards. The court calls this a royal festival because the palace pays for the pavilion. I call it a city festival because the city supplies every reason anyone comes.", "next": "six"}, + "six": {"speaker": "z", "expression": "thoughtful", "text": "Then the exhibition should name hands, not only patrons. In Lahore I studied a travel album praised for a governor’s taste. The margins named the paper maker, the binder, and the woman who repaired it after a flood. Those small names changed the whole object. Suddenly history had a crowded room behind it.", "next": "seven"}, + "seven": {"speaker": "lima", "expression": "vulnerable", "text": "That is what I am trying to build here, though some relatives prefer an uncluttered family legend. I can argue with them in committee. What frightens me is becoming fluent in their language and forgetting why I began. If you see me polishing away a difficult truth, will you tell me before I become impossible?", "next": "eight"}, + "eight": {"speaker": "z", "expression": "tender", "text": "I will tell you privately, with evidence, tea, and an escape route from the committee. In return, if I start treating people like an interesting archive instead of people, you may confiscate my notebook. Temporarily. I have grown attached to it.", "next": "nine"}, + "nine": {"speaker": "lima", "expression": "playful", "text": "Agreed. Though I reserve the right to ransom the notebook for another dangerously flattering footnote. Come—the library bell is about to sound. Whatever waits inside, I am glad the delegation sent the scholar who notices seams.", "effects": [{"type": "relationship", "key": "comfort", "amount": 3}, {"type": "relationship", "key": "trust", "amount": 3}, {"type": "flag", "id": "delegation_understanding"}]} + } + }, + "library_search": { + "start": "one", + "nodes": { + "one": {"speaker": "anwara", "expression": "thoughtful", "text": "Search as conservators, not hunters. A hunter asks what was taken. A conservator asks what changed around the absence. The velvet beneath the folio is uncreased, the cabinet lock is unmarked, and the humidity paper was replaced at dawn by someone who understood why it mattered.", "next": "two"}, + "two": {"speaker": "lima", "expression": "determined", "text": "So the person had access, knowledge, and enough patience to move the folio without bruising the binding. That narrows the suspects to an uncomfortable number of people I trust. I dislike mysteries that begin by making suspicion feel intelligent.", "next": "three"}, + "three": {"speaker": "z", "expression": "thoughtful", "text": "Suspicion is only one tool. The cabinet smells faintly of vetiver and rain, but the room is dry. The folio may have been wrapped in a cooling cloth before it moved. Whoever did this was protecting it even while disobeying you.", "next": "four"}, + "four": {"speaker": "anwara", "expression": "warm", "text": "Good. The final leaf was repaired with starch paste after the monsoon of my apprenticeship. Heat makes that repair tighten. Festival lamps, a crowded pavilion, and an hour of speeches would be a poor climate for it. Write that in your notebook before romance makes you careless, young scholar.", "next": "five"}, + "five": {"speaker": "lima", "expression": "playful", "text": "Keeper, we have known each other for less than an afternoon. At least allow him time to become careless on his own merits.", "choices": [ + {"id": "professional", "text": "Promise professional concentration, mostly.", "effects": [{"type": "relationship", "key": "humour", "amount": 3}, {"type": "relationship", "key": "romance", "amount": 2}], "next": "six"}, + {"id": "honest", "text": "Say that care and attraction can coexist.", "effects": [{"type": "relationship", "key": "trust", "amount": 3}, {"type": "relationship", "key": "romance", "amount": 3}], "next": "six"}, + {"id": "folio", "text": "Return gently to the evidence.", "effects": [{"type": "relationship", "key": "respect", "amount": 4}], "next": "six"} + ]}, + "six": {"speaker": "z", "expression": "determined", "text": "The replacement humidity paper is cut from a ledger page. Its watermark matches the festival accounts on the western desk. Someone wanted us to notice the paper but not immediately. There are pinpricks along one edge—five, two, seven, one—too deliberate for ordinary filing.", "next": "seven"}, + "seven": {"speaker": "lima", "expression": "curious", "text": "Shelf numbers. Five is river histories, two is household accounts, seven is poetry, one is gardening. Anwara taught me that route when I was twelve. She called it the path by which a palace remembers it is fed, watered, criticised, and loved.", "next": "eight"}, + "eight": {"speaker": "anwara", "expression": "vulnerable", "text": "I taught many children many routes. Some lessons are remembered only when an emergency gives them teeth. Follow the shelves. Read the marginal names. And if the trail asks you to choose between a perfect ceremony and an honest history, understand that the choice was present long before the folio vanished.", "next": "nine"}, + "nine": {"speaker": "z", "expression": "thoughtful", "text": "This is not a thief’s escape path. It is a curator’s argument arranged as evidence. We will follow it without disturbing the repair, and we will keep separate notes about what we know, what we infer, and what we merely fear.", "next": "ten"}, + "ten": {"speaker": "lima", "expression": "warm", "text": "Separate notes, shared tea, no accusations without proof. You are remarkably calming for a man who arrived carrying a bow case and a reputation for inconvenient questions. Stay close. The poetry shelf has a rolling ladder, and Ishan has already expressed his views on my ankles.", "effects": [{"type": "relationship", "key": "respect", "amount": 3}, {"type": "relationship", "key": "comfort", "amount": 3}, {"type": "flag", "id": "search_complete"}]} + } + }, + "garden_walk": { + "start": "one", + "nodes": { + "one": {"speaker": "mira", "expression": "warm", "text": "The moon garden is not planted for a single country or a single season. Jasmine opens beside tuberose; bakul waits for warmer air; marigolds carry daylight into evening. A garland works because each flower keeps its own scent while accepting the same thread.", "next": "two"}, + "two": {"speaker": "lima", "expression": "thoughtful", "text": "My grandmother used that image in speeches. She usually left out the gardeners who chose which flowers could survive beside one another. Metaphors become very tidy when the labour is removed.", "next": "three"}, + "three": {"speaker": "z", "expression": "warm", "text": "Then let us keep the labour in view. My aunt grows jasmine in pots she moves twice a day to chase shade. She would laugh at any poet who called fragrance effortless. She would also ask why I had not yet offered to carry your basket.", "next": "four"}, + "four": {"speaker": "lima", "expression": "playful", "text": "Because you suspected I would make you earn the privilege. Correctly. Tell me something true while we walk—something your official biography omitted. Visiting scholars are always reduced to institutions, awards, and one respectable hobby.", "choices": [ + {"id": "failure", "text": "Describe the tournament where every arrow missed.", "effects": [{"type": "relationship", "key": "trust", "amount": 4}, {"type": "relationship", "key": "humour", "amount": 2}], "next": "five"}, + {"id": "family", "text": "Talk about loud family breakfasts.", "effects": [{"type": "relationship", "key": "comfort", "amount": 4}, {"type": "relationship", "key": "familyApproval", "amount": 2}], "next": "five"}, + {"id": "desire", "text": "Admit you wanted this journey to change you.", "effects": [{"type": "relationship", "key": "adventure", "amount": 3}, {"type": "relationship", "key": "romance", "amount": 3}], "next": "five"} + ]}, + "five": {"speaker": "z", "expression": "vulnerable", "text": "The polished version makes me sound certain. I am not. I came because I was tired of being praised for questions that never required me to risk an answer. I wanted to meet people who could disagree with my work and still invite me to dinner afterward.", "next": "six"}, + "six": {"speaker": "lima", "expression": "tender", "text": "That sounds less like uncertainty than hunger. I know it. Everyone praises my exhibitions for balance, which sometimes means I have placed every difficult fact where no guest must look at it for long. This folio may be asking whether I want approval more than truth.", "next": "seven"}, + "seven": {"speaker": "z", "expression": "thoughtful", "text": "Truth without care can become another performance of power. Care without truth becomes decoration. Perhaps the work is to refuse both shortcuts. We can protect the page, credit the founders, and let the court feel the discomfort it has postponed.", "next": "eight"}, + "eight": {"speaker": "lima", "expression": "warm", "text": "You say 'we' very easily. I notice that I like it. I also notice you have carried my basket for half the path without asking again, which is either considerate or a cunning strategy to stand close enough that our shoulders keep touching.", "next": "nine"}, + "nine": {"speaker": "z", "expression": "wry", "text": "A serious researcher does not discard a successful method merely because the subject has identified it. For the record, the shoulder contact appears mutually reproducible.", "next": "ten"}, + "ten": {"speaker": "lima", "expression": "playful", "text": "Peer review can wait until after the archery trial. If you impress me, I may permit a larger sample. If you miss every target, I will still be impressed by the honesty of your earlier confession. Either way, the garden seems determined to keep us on the same path.", "effects": [{"type": "relationship", "key": "comfort", "amount": 3}, {"type": "relationship", "key": "romance", "amount": 3}, {"type": "flag", "id": "garden_walk_complete"}]} + } + }, + "market_founders": { + "start": "one", + "nodes": { + "one": {"speaker": "nadia", "expression": "determined", "text": "People imagine censorship as a locked door. More often it is a filing choice. Put a cook’s letter under household accounts, a gardener’s poem under seasonal instructions, a traveller’s sketch under customs records, and soon no scholar thinks to gather them. Nothing is destroyed. Everything is made lonely.", "next": "two"}, + "two": {"speaker": "z", "expression": "thoughtful", "text": "The folio gathers those lonely records. Its border motifs match the letters: the cook’s copper ladle, the gardener’s five-petal seal, the boatwoman’s knotted rope. The royal dedication was added later in a different hand. The festival began below the line where official histories usually stop reading.", "next": "three"}, + "three": {"speaker": "lima", "expression": "vulnerable", "text": "My family did support it eventually. That matters, but it is not the same as founding it. I can already hear the council offering a tasteful plaque to the unnamed citizens while keeping the royal story at the centre. Gratitude can be used to make theft sound gracious.", "next": "four"}, + "four": {"speaker": "rayhan", "expression": "warm", "text": "Then do not serve history as garnish. My kitchen still cooks the founders’ river rice: mustard from one bank, dried fruit brought by travellers from the west, fish or aubergine depending on the household. No single pot owns the dish. The recipe survives because people keep arguing lovingly over it.", "next": "five"}, + "five": {"speaker": "z", "expression": "warm", "text": "That argument sounds familiar. At family tables, authenticity is often whoever speaks loudest until an aunt produces a stained notebook. Perhaps the public presentation should work like that notebook: specific names, specific work, no grand claim that smooths away difference.", "choices": [ + {"id": "public", "text": "Recommend naming every known founder publicly.", "effects": [{"type": "relationship", "key": "respect", "amount": 4}, {"type": "relationship", "key": "familyApproval", "amount": 1}], "next": "six"}, + {"id": "community", "text": "Invite descendants and guilds to tell the story.", "effects": [{"type": "relationship", "key": "trust", "amount": 3}, {"type": "relationship", "key": "familyApproval", "amount": 3}], "next": "six"}, + {"id": "protect", "text": "Put preservation before spectacle.", "effects": [{"type": "relationship", "key": "comfort", "amount": 3}, {"type": "relationship", "key": "respect", "amount": 3}], "next": "six"} + ]}, + "six": {"speaker": "nadia", "expression": "curious", "text": "All three can coexist if the palace gives up being the only narrator. Display a facsimile, keep the fragile page in controlled light, and place the letters beside present-day voices. Let visitors see how evidence, memory, and living practice correct one another.", "next": "seven"}, + "seven": {"speaker": "lima", "expression": "determined", "text": "The council will say there is no time. I will remind them that urgency was created by announcing an unveiling before conservation approval. I signed that schedule. I can admit the mistake in the same voice I use to correct it.", "next": "eight"}, + "eight": {"speaker": "z", "expression": "tender", "text": "I will stand beside you, but I will not turn your admission into my heroic interruption. You discovered the conflict, listened to the keepers, and changed the exhibition. Partnership should make courage easier without stealing its name.", "next": "nine"}, + "nine": {"speaker": "lima", "expression": "tender", "text": "Be careful. That is the sort of sentence a woman remembers while pretending to examine a nineteenth-century spice ledger. Stand beside me tonight and I may ask you to remain there after the speeches, when partnership has fewer witnesses and considerably less formal clothing.", "next": "ten"}, + "ten": {"speaker": "rayhan", "expression": "playful", "text": "I heard nothing. I am only a cook standing three steps away with excellent hearing. Come test the founders’ dish. Cooperation under pressure reveals character, and if the two of you keep looking at each other like that, at least the rice deserves supervision.", "effects": [{"type": "relationship", "key": "trust", "amount": 3}, {"type": "relationship", "key": "romance", "amount": 4}, {"type": "flag", "id": "founders_plan"}]} + } + }, + "pavilion_waiting": { + "start": "one", + "nodes": { + "one": {"speaker": "ishan", "expression": "determined", "text": "The pavilion is full. Guild representatives are seated along the river side, the court delegation faces the stage, and three rival newspapers have discovered the phrase 'missing royal treasure.' I can delay the opening twelve minutes with music and perhaps five more by complimenting the ambassador’s shawl.", "next": "two"}, + "two": {"speaker": "saira", "expression": "thoughtful", "text": "Use ten. We should not behave as though truth is an emergency to be hidden behind entertainment. Lima, the council is divided, but no one disputes that the original cannot endure the lamps. The remaining question is whether we name the concealment and the mistaken history ourselves.", "next": "three"}, + "three": {"speaker": "lima", "expression": "determined", "text": "We do. Anwara protected the folio through an improper act prompted by our improper schedule. I will not punish her publicly to preserve my authority. We will show the conservation images, read the founders’ names, and invite the market guilds to correct the record as research continues.", "next": "four"}, + "four": {"speaker": "z", "expression": "thoughtful", "text": "The evidence supports that account. I can explain the repair, the mismatched dedication, and the letters without claiming certainty where gaps remain. The most persuasive thing we can do is let the audience see the method, including the point where our first assumptions failed.", "next": "five"}, + "five": {"speaker": "saira", "expression": "warm", "text": "You have both chosen a difficult path with unusual grace. Z, a visiting scholar can leave after making trouble and call it principle. You have stayed for the repair. That distinction matters to families, even modern ones who pretend not to conduct interviews over tea.", "choices": [ + {"id": "families", "text": "Say that staying for the repair is the point.", "effects": [{"type": "relationship", "key": "familyApproval", "amount": 5}, {"type": "relationship", "key": "comfort", "amount": 2}], "next": "six"}, + {"id": "lima", "text": "Say that Lima made staying an easy choice.", "effects": [{"type": "relationship", "key": "romance", "amount": 5}, {"type": "relationship", "key": "trust", "amount": 2}], "next": "six"}, + {"id": "work", "text": "Commit to the continuing research.", "effects": [{"type": "relationship", "key": "respect", "amount": 5}, {"type": "relationship", "key": "adventure", "amount": 2}], "next": "six"} + ]}, + "six": {"speaker": "lima", "expression": "flustered", "text": "Aunt Saira, if this is an interview, you might have warned me before allowing him to answer so well. I have a speech to deliver and would prefer not to do it while visibly imagining future tea tables.", "next": "seven"}, + "seven": {"speaker": "saira", "expression": "playful", "text": "A curator should be able to manage competing interpretations. Besides, the entire market has managed to notice what you two believe remains subtle. Do not worry—the newspapers are still occupied with the missing-treasure headline.", "next": "eight"}, + "eight": {"speaker": "z", "expression": "tender", "text": "After the speech, may I have the first dance? This is partly strategic. If I stumble publicly, the scandal may distract everyone from the archive. If I do not, I would like one memory tonight that belongs to us before it becomes an exhibition note.", "next": "nine"}, + "nine": {"speaker": "lima", "expression": "tender", "text": "You may have the first dance and the private memory. You may even have the truth: I have been saving that dance since the garden, when you called our shoulders a reproducible result. Do not make me regret rewarding such terrible scholarship.", "next": "ten"}, + "ten": {"speaker": "ishan", "expression": "warm", "text": "Your twelve minutes have become two. The river lamps are lit, the facsimile is in place, and the musicians are waiting. Whatever changes after tonight, the story will no longer belong to a single house. Take your positions—and, for all our sakes, save the kissing until after the council vote.", "effects": [{"type": "relationship", "key": "familyApproval", "amount": 3}, {"type": "relationship", "key": "romance", "amount": 3}, {"type": "flag", "id": "pavilion_ready"}]} + } + } +} diff --git a/assets/games/two-rivers/data/dialogue/scenes.json b/assets/games/two-rivers/data/dialogue/scenes.json new file mode 100644 index 0000000..bf68025 --- /dev/null +++ b/assets/games/two-rivers/data/dialogue/scenes.json @@ -0,0 +1,199 @@ +{ + "courtyard_preparations": { + "start": "banners", + "nodes": { + "banners": {"speaker": "lima", "expression": "curious", "text": "The indigo banners belong on the riverward arches, but three ministers have moved them because blue is apparently insufficiently regal. This is what curating means: defending colour choices against men who own ceremonial hats.", "next": "z_notices"}, + "z_notices": {"speaker": "z", "expression": "wry", "text": "In Lahore, a committee once spent two meetings deciding whether a catalogue ribbon was blue or philosophically teal. I survived by bringing tea and agreeing with the eldest librarian.", "choices": [ + {"text": "Offer to help restore Lima's original arrangement.", "next": "help", "effects": [{"type": "relationship", "key": "respect", "amount": 3}, {"type": "relationship", "key": "comfort", "amount": 2}]}, + {"text": "Suggest hiding the ministers' hats until they surrender.", "next": "hats", "effects": [{"type": "relationship", "key": "humour", "amount": 4}]}, + {"text": "Admit that the curator makes indigo look dangerously persuasive.", "next": "indigo", "effects": [{"type": "relationship", "key": "romance", "amount": 4}]} + ]}, + "help": {"speaker": "lima", "expression": "warm", "text": "You take the river side; I will take the palace side. If anyone objects, look scholarly and mention preservation standards. People become wonderfully obedient when afraid of footnotes.", "next": "work"}, + "hats": {"speaker": "lima", "expression": "playful", "text": "Tempting. Yet Aunt Saira says diplomacy means allowing people to keep one dignity each. We shall permit the hats and reclaim the arches.", "next": "work"}, + "indigo": {"speaker": "lima", "expression": "playful", "text": "That line was either shameless or culturally observant. I will decide after you carry the heavier banner stand.", "next": "work"}, + "work": {"speaker": "z", "expression": "warm", "text": "They work shoulder to shoulder while musicians tune beyond the gate. Lima explains how each banner carries a different river poem; Z notices she quotes the boatwoman's verse before the king's.", "next": "founders"}, + "founders": {"speaker": "lima", "expression": "thoughtful", "text": "My catalogue begins with rulers because the archive begins with rulers. But festivals are made by people who cook, mend boats, grow flowers, and stay awake arranging banners. I wish the records remembered them as clearly as the songs do.", "effects": [{"type": "relationship", "key": "trust", "amount": 3}, {"type": "discoverGift", "id": "poetry_book"}]} + } + }, + "ishan_protocol": { + "start": "warning", + "nodes": { + "warning": {"speaker": "ishan", "text": "Master Z, a word of survival. At sunset you will be introduced to the council. Bow to Saira, nod to the ministers, and do not call the royal genealogist's speech 'surprisingly brief.' He considers that an insult.", "next": "reply"}, + "reply": {"speaker": "z", "expression": "wry", "text": "How does he feel about 'mercifully finite'?", "choices": [ + {"text": "Promise impeccable diplomatic restraint.", "next": "restraint", "effects": [{"type": "relationship", "key": "familyApproval", "amount": 3}]}, + {"text": "Ask which protocol Lima ignores when nobody watches.", "next": "lima", "effects": [{"type": "relationship", "key": "humour", "amount": 2}, {"type": "relationship", "key": "romance", "amount": 2}]} + ]}, + "restraint": {"speaker": "ishan", "text": "Excellent. You have the solemn face for it. Keep that expression when the trumpets miss their entrance and we may all preserve our employment.", "next": "kindness"}, + "lima": {"speaker": "ishan", "text": "Her Highness ignores any protocol that places a tedious person between her and a useful book. I did not tell you this. I especially did not tell you she has already asked where you are staying.", "next": "kindness"}, + "kindness": {"speaker": "ishan", "text": "One serious instruction: the public pavilion is for everyone. The palace is host, not owner. Lima has fought for that distinction since she was seventeen. Stand beside her on it and the city will notice.", "effects": [{"type": "relationship", "key": "respect", "amount": 2}]} + } + }, + "anwara_memory": { + "start": "dust", + "nodes": { + "dust": {"speaker": "anwara", "text": "Do not wipe that dust away. It is clean dust, which is rarer than clean politics. It tells me the eastern cabinet has not been opened since yesterday.", "next": "lesson"}, + "lesson": {"speaker": "lima", "expression": "warm", "text": "Anwara taught me to read dust before she taught me the alphabet. I spent a month believing every shelf was a sentence.", "next": "z"}, + "z": {"speaker": "z", "expression": "thoughtful", "text": "Perhaps it is. Absence has grammar. A moved book leaves a pale verb behind; a missing folio creates an accusation.", "choices": [ + {"text": "Ask Anwara who requested access to the manuscript.", "next": "access", "effects": [{"type": "relationship", "key": "trust", "amount": 3}]}, + {"text": "Ask why the final folio matters to Lima personally.", "next": "personal", "effects": [{"type": "relationship", "key": "comfort", "amount": 3}]} + ]}, + "access": {"speaker": "anwara", "text": "The council, the conservators, the curator, and me. Too many keys for certainty; too few for coincidence. Yesterday a minister asked whether heat could make old ink appear brighter. I disliked the question.", "next": "warning"}, + "personal": {"speaker": "lima", "expression": "vulnerable", "text": "My mother read its dedication at the first festival I remember. She said a country becomes honest when ordinary names are spoken in royal rooms. After she died, the folio became the sound of her voice.", "next": "warning"}, + "warning": {"speaker": "anwara", "text": "The glass case is new. Its lamps are stronger than the conservators approved. If anyone values spectacle above the page, the manuscript will not survive their admiration.", "effects": [{"type": "flag", "id": "lamp_warning"}, {"type": "relationship", "key": "respect", "amount": 2}]} + } + }, + "library_balcony": { + "start": "rain", + "nodes": { + "rain": {"speaker": "lima", "expression": "thoughtful", "text": "Rain on the library roof used to frighten me. The ceiling leaked above the military histories, and I imagined every dead general swimming indignantly through the catalogue.", "next": "z"}, + "z": {"speaker": "z", "expression": "wry", "text": "A useful image. Most military histories would improve if the generals had to tread water while explaining themselves.", "next": "laugh"}, + "laugh": {"speaker": "lima", "expression": "playful", "text": "There. You have done it again—made irreverence sound like scholarship. Is that a visiting-scholars' technique, or are you uniquely troublesome?", "choices": [ + {"text": "Say the trouble appears only around her.", "next": "around", "effects": [{"type": "relationship", "key": "romance", "amount": 5}, {"type": "relationship", "key": "humour", "amount": 2}]}, + {"text": "Confess that unfamiliar places make humour feel like a bridge.", "next": "bridge", "effects": [{"type": "relationship", "key": "trust", "amount": 4}, {"type": "relationship", "key": "comfort", "amount": 3}]} + ]}, + "around": {"speaker": "lima", "expression": "tender", "text": "How inconvenient for us both. She leans against the balcony rail close enough that his sleeve brushes hers, then makes no effort to move.", "next": "home"}, + "bridge": {"speaker": "lima", "expression": "warm", "text": "Then I am glad you crossed. A palace can make guests feel observed instead of welcomed. With you, I keep forgetting which of us is supposed to be impressive.", "next": "home"}, + "home": {"speaker": "z", "expression": "thoughtful", "text": "He tells her about evenings of cardamom tea, cousins arguing over cricket, his mother's precise opinions on hospitality, and the quiet loneliness of being praised for travelling far from everyone who knows how he takes breakfast.", "next": "answer"}, + "answer": {"speaker": "lima", "expression": "vulnerable", "text": "I understand the loneliness of being known publicly and guessed at privately. Stay after the festival, if you can. Not forever. Just long enough that neither of us has to guess.", "effects": [{"type": "relationship", "key": "comfort", "amount": 5}, {"type": "relationship", "key": "romance", "amount": 4}, {"type": "discoverGift", "id": "spiced_tea"}]} + } + }, + "mira_garland": { + "start": "empty", + "nodes": { + "empty": {"speaker": "mira", "text": "The communal arch is bare because the palace florist sent only roses. Beautiful, expensive, and completely wrong. This garland should smell like five neighbourhoods, not one drawing room.", "next": "flowers"}, + "flowers": {"speaker": "mira", "text": "Jasmine for welcome, tuberose for evening, marigold for shared work, rose for affection, and bakul for memory. None belongs to one family. Together they belong to the festival.", "next": "lima"}, + "lima": {"speaker": "lima", "expression": "determined", "text": "We will find every one. Put the palace roses aside for the ministers' table; they may enjoy being arranged with their peers.", "choices": [ + {"text": "Volunteer to gather the flowers with Lima.", "next": "volunteer", "effects": [{"type": "relationship", "key": "adventure", "amount": 3}, {"type": "relationship", "key": "familyApproval", "amount": 2}]}, + {"text": "Tell Mira the garland is living historical evidence.", "next": "evidence", "effects": [{"type": "relationship", "key": "respect", "amount": 3}]} + ]}, + "volunteer": {"speaker": "mira", "text": "Good. A scholar who can identify a flower is worth three who merely classify it. Her Highness knows the garden; you carry the basket and try not to stare at her.", "next": "promise"}, + "evidence": {"speaker": "mira", "text": "Exactly. History that wilts by morning still matters. Perhaps especially then. Gather carefully, scholar; the roots must remain for next year's story.", "next": "promise"}, + "promise": {"speaker": "z", "expression": "warm", "text": "Z accepts the basket. Lima's hand closes over the other handle. For several steps neither admits that the path is wide enough to carry it separately.", "effects": [{"type": "quest", "id": "garland", "action": "start"}, {"type": "relationship", "key": "romance", "amount": 2}]} + } + }, + "garden_history": { + "start": "mosaic", + "nodes": { + "mosaic": {"speaker": "z", "expression": "thoughtful", "text": "The mosaic repeats the manuscript's four symbols, but flowers replace the royal seals. Whoever designed this wanted the clue understood by gardeners, not courtiers.", "next": "lima"}, + "lima": {"speaker": "lima", "expression": "curious", "text": "My tutors called it decorative. Mira calls it a planting calendar. Anwara called both answers incomplete and refused to explain why.", "next": "sequence"}, + "sequence": {"speaker": "z", "expression": "determined", "text": "Jasmine opens at welcome, tuberose carries into dusk, marigold marks the communal feast, and bakul keeps its fragrance after falling. Dawn, evening, fire, memory.", "choices": [ + {"text": "Invite Lima to press the first flower with you.", "next": "together", "effects": [{"type": "relationship", "key": "comfort", "amount": 4}, {"type": "relationship", "key": "romance", "amount": 3}]}, + {"text": "Ask whether she enjoys being proved right by a wall.", "next": "wall", "effects": [{"type": "relationship", "key": "humour", "amount": 3}]} + ]}, + "together": {"speaker": "lima", "expression": "tender", "text": "Their fingertips meet over the stone jasmine. The mechanism clicks, but Lima looks at him rather than the opening seam. “Remarkably effective scholarship,” she murmurs.", "next": "note"}, + "wall": {"speaker": "lima", "expression": "playful", "text": "Immensely. Walls interrupt less than ministers and apologise more gracefully. This one may even reveal a secret compartment.", "next": "note"}, + "note": {"speaker": "z", "expression": "thoughtful", "text": "Inside is Anwara's note: The straightest arrow is useless when the target is false. Beneath it rests an old brass range token stamped with a boatwoman's name.", "effects": [{"type": "relationship", "key": "trust", "amount": 3}]} + } + }, + "archery_after": { + "start": "retrieve", + "nodes": { + "retrieve": {"speaker": "lima", "expression": "playful", "text": "Lima retrieves the last arrow and examines the target with theatrical gravity. “The hay has submitted. The kingdom is safe.”", "next": "z"}, + "z": {"speaker": "z", "expression": "wry", "text": "I had hoped for a medal. Or at least a curator's favourable annotation.", "choices": [ + {"text": "Ask her to correct your stance at very close range.", "next": "stance", "effects": [{"type": "relationship", "key": "romance", "amount": 5}, {"type": "relationship", "key": "comfort", "amount": 2}]}, + {"text": "Offer to teach her the draw you learned from your aunt.", "next": "teach", "effects": [{"type": "relationship", "key": "respect", "amount": 4}, {"type": "relationship", "key": "adventure", "amount": 3}]} + ]}, + "stance": {"speaker": "lima", "expression": "tender", "text": "She steps behind him, one hand at his elbow and one at his waist. “Your stance is perfectly adequate,” she says near his ear. “I am correcting your unbearable confidence.”", "next": "token"}, + "teach": {"speaker": "lima", "expression": "warm", "text": "He adjusts her grip only after asking. She mirrors the unfamiliar draw, releases, and strikes the inner ring. Her delight is immediate and wholly unroyal.", "next": "token"}, + "token": {"speaker": "z", "expression": "thoughtful", "text": "The range ledger names the token's owner: Samira the boatwright, winner of the first riverside contest and one of the signatures omitted from the court catalogue.", "effects": [{"type": "item", "id": "founders_record"}, {"type": "relationship", "key": "trust", "amount": 2}]} + } + }, + "nadia_letters": { + "start": "shelf", + "nodes": { + "shelf": {"speaker": "nadia", "text": "If you are looking for respectable books, try the front table. If you are looking for truthful ones, help me move this crate before the inspectors notice.", "next": "letters"}, + "letters": {"speaker": "nadia", "text": "Three letters were filed under royal invitations because nobody read past the seals. They describe cooks feeding strikers, gardeners lending boats, and travellers carrying poems between the rivers.", "next": "lima"}, + "lima": {"speaker": "lima", "expression": "determined", "text": "Those letters should have been in the festival archive. My office requested all founder material last winter.", "choices": [ + {"text": "Defend Nadia's caution without dismissing Lima's frustration.", "next": "balance", "effects": [{"type": "relationship", "key": "trust", "amount": 4}, {"type": "relationship", "key": "respect", "amount": 3}]}, + {"text": "Point out that mistrust of the palace was historically earned.", "next": "earned", "effects": [{"type": "relationship", "key": "adventure", "amount": 3}, {"type": "relationship", "key": "respect", "amount": 2}]} + ]}, + "balance": {"speaker": "z", "expression": "thoughtful", "text": "Nadia protected vulnerable evidence; Lima tried to bring it into public care. Neither intention repairs the gap alone. They need a process the market can trust and the archive must honour.", "next": "agreement"}, + "earned": {"speaker": "z", "expression": "determined", "text": "A seal once removed ordinary names from the record. Another seal cannot demand confidence by decree. Let the booksellers decide how copies are made and credited.", "next": "agreement"}, + "agreement": {"speaker": "nadia", "text": "Copies first, originals later, and a market witness at every catalogue meeting. Agree to that and I will show you where the last two letters wandered.", "effects": [{"type": "quest", "id": "letters", "action": "start"}, {"type": "relationship", "key": "familyApproval", "amount": 2}]} + } + }, + "rayhan_kitchen": { + "start": "steam", + "nodes": { + "steam": {"speaker": "rayhan", "text": "Excellent, educated hands. Wash them. The council doubled the guest list and halved my warning. We are making fragrant rice for two hundred people and one princess who keeps stealing pistachios.", "next": "lima"}, + "lima": {"speaker": "lima", "expression": "playful", "text": "Allegations without witnesses. Z, as a scholar, you understand the importance of evidence.", "next": "z"}, + "z": {"speaker": "z", "expression": "wry", "text": "There is pistachio dust on your sleeve and an expression of profound guiltlessness. The evidence is unusually charming.", "choices": [ + {"text": "Take Lima's side and demand a formal tasting.", "next": "taste", "effects": [{"type": "relationship", "key": "humour", "amount": 4}, {"type": "relationship", "key": "romance", "amount": 2}]}, + {"text": "Ask Rayhan about the founders' communal meal.", "next": "history", "effects": [{"type": "relationship", "key": "respect", "amount": 3}]} + ]}, + "taste": {"speaker": "rayhan", "text": "A tasting. Naturally. Stand close together so I can mistrust you efficiently. Rice first, saffron second, citrus third, pistachio last.", "next": "recipe"}, + "history": {"speaker": "rayhan", "text": "The first meal used whatever five streets could spare. The palace supplied plates only after discovering everyone had already eaten. Remember that when someone calls generosity a royal invention.", "next": "recipe"}, + "recipe": {"speaker": "lima", "expression": "warm", "text": "Foundation, gold, brightness, green. She ties an apron around Z and leaves her hands at his waist one heartbeat longer than the knot requires. “For scholarship,” she says.", "effects": [{"type": "discoverGift", "id": "spiced_tea"}, {"type": "relationship", "key": "comfort", "amount": 2}]} + } + }, + "market_walk": { + "start": "crowd", + "nodes": { + "crowd": {"speaker": "lima", "expression": "warm", "text": "In the palace every silence becomes a rumour. Here I can disappear between a spice seller and a family arguing over sweets. It is the closest thing I have to privacy.", "next": "z"}, + "z": {"speaker": "z", "expression": "thoughtful", "text": "You are not very well disguised. Three vendors bowed, two children waved, and that woman gave me a look suggesting she has already planned our wedding menu.", "next": "laugh"}, + "laugh": {"speaker": "lima", "expression": "playful", "text": "That is Farida. She plans everyone's wedding menu. Last month she married two customers because they reached for the same mango.", "choices": [ + {"text": "Ask what Farida would serve at theirs.", "next": "wedding", "effects": [{"type": "relationship", "key": "romance", "amount": 6}, {"type": "relationship", "key": "humour", "amount": 3}]}, + {"text": "Say you like seeing the city know and love her.", "next": "known", "effects": [{"type": "relationship", "key": "comfort", "amount": 5}, {"type": "relationship", "key": "familyApproval", "amount": 3}]} + ]}, + "wedding": {"speaker": "lima", "expression": "tender", "text": "Lima stops walking. “You move quickly for a man who took ten minutes to choose a library chair.” Then, softer: “Saffron sweets. Farida believes optimism should stain the fingers.”", "next": "hand"}, + "known": {"speaker": "lima", "expression": "vulnerable", "text": "Loved is easier here because they knew me before the speeches. They remember when I fell into the fountain chasing a paper boat. I want you to know that version too.", "next": "hand"}, + "hand": {"speaker": "z", "expression": "warm", "text": "A surge in the crowd presses them together. Z offers his hand; Lima takes it, and keeps it after the street widens. Neither mentions the unnecessary duration.", "effects": [{"type": "relationship", "key": "trust", "amount": 3}, {"type": "relationship", "key": "romance", "amount": 3}, {"type": "discoverGift", "id": "brass_compass"}]} + } + }, + "garland_complete": { + "start": "finished", + "nodes": { + "finished": {"speaker": "mira", "text": "Five neighbourhoods, five fragrances, and not one palace rose in command. Hold the far end, both of you. A communal garland should be lifted by more than one pair of hands.", "next": "lift"}, + "lift": {"speaker": "lima", "expression": "triumphant", "text": "The arch rises above the market entrance. Children call the flower names while Mira knots each stem into the next. The crowd applauds itself rather than the palace.", "next": "z"}, + "z": {"speaker": "z", "expression": "warm", "text": "This is the festival Anwara was trying to protect: not an object beneath glass, but memory practised together.", "choices": [ + {"text": "Give the public credit when Saira arrives.", "next": "credit", "effects": [{"type": "relationship", "key": "familyApproval", "amount": 5}, {"type": "relationship", "key": "respect", "amount": 3}]}, + {"text": "Quietly tuck one fallen jasmine into Lima's hair.", "next": "jasmine", "effects": [{"type": "relationship", "key": "romance", "amount": 5}, {"type": "relationship", "key": "comfort", "amount": 3}]} + ]}, + "credit": {"speaker": "saira", "text": "Then the programme will name every contributing street. Tradition is not weakened by accurate credits; only vanity is.", "next": "memory"}, + "jasmine": {"speaker": "lima", "expression": "tender", "text": "She turns her face into his palm before propriety can object. “There,” she says. “Now the arrangement contains one dangerously observant scholar.”", "next": "memory"}, + "memory": {"speaker": "mira", "text": "Mira sends them away with jasmine-scented fingers and the knowledge that the first founders used this same sequence to mark safe meeting places.", "effects": [{"type": "unlockCG", "id": "garland"}]} + } + }, + "letters_complete": { + "start": "three", + "nodes": { + "three": {"speaker": "nadia", "text": "All three. A cook's account, a gardener's map, and a traveller's letter home. Read the signatures aloud; they have waited long enough.", "next": "names"}, + "names": {"speaker": "z", "expression": "thoughtful", "text": "Amina Rahman, who fed the night crews. Debu Sen, who opened the garden gates. Samira Qureshi, who carried verses upstream and returned with another language in the margins.", "next": "lima"}, + "lima": {"speaker": "lima", "expression": "vulnerable", "text": "My catalogue reduced them to 'local assistance.' Two words where three lives should be. I signed that edition.", "choices": [ + {"text": "Remind her that responsibility includes correction, not self-punishment.", "next": "correct", "effects": [{"type": "relationship", "key": "trust", "amount": 5}, {"type": "relationship", "key": "comfort", "amount": 4}]}, + {"text": "Offer to write the corrected entry beside her.", "next": "beside", "effects": [{"type": "relationship", "key": "respect", "amount": 4}, {"type": "relationship", "key": "romance", "amount": 3}]} + ]}, + "correct": {"speaker": "z", "expression": "warm", "text": "You inherited an archive designed to overlook them. Your responsibility is not to have been born knowing better; it is to change what the next curator inherits.", "next": "copy"}, + "beside": {"speaker": "z", "expression": "tender", "text": "Two chairs, one desk, several arguments over commas. I can imagine worse futures. Lima's answering smile makes the bookseller suddenly fascinated by another shelf.", "next": "copy"}, + "copy": {"speaker": "nadia", "text": "The private reading room is yours until moonrise. Make the copy together. Leave the door open for propriety if you must; the shelves have heard better gossip.", "effects": [{"type": "unlockCG", "id": "library"}, {"type": "relationship", "key": "familyApproval", "amount": 3}]} + } + }, + "ishan_stage": { + "start": "programme", + "nodes": { + "programme": {"speaker": "ishan", "text": "The council has rewritten the programme four times. At present the folio is unveiled, praised, illuminated, praised again, and possibly cooked by the lamps before dessert.", "next": "lima"}, + "lima": {"speaker": "lima", "expression": "determined", "text": "Then there will be no unveiling. We will display a facsimile, keep the original in controlled light, and give the founders' descendants the stage.", "next": "risk"}, + "risk": {"speaker": "saira", "text": "Several ministers will call that humiliation. They expected the palace to own the revelation.", "choices": [ + {"text": "Frame preservation as honour rather than retreat.", "next": "honour", "effects": [{"type": "relationship", "key": "respect", "amount": 4}, {"type": "relationship", "key": "familyApproval", "amount": 3}]}, + {"text": "Let the ministers be embarrassed by accurate history.", "next": "embarrassed", "effects": [{"type": "relationship", "key": "adventure", "amount": 4}, {"type": "relationship", "key": "humour", "amount": 2}]} + ]}, + "honour": {"speaker": "z", "expression": "determined", "text": "A fragile page is not honoured by being sacrificed to applause. The facsimile lets everyone read it; the original survives to correct us again.", "next": "ready"}, + "embarrassed": {"speaker": "lima", "expression": "triumphant", "text": "At last, a festival entertainment I can endorse. Ishan, place their reserved seats where they can hear every omitted name clearly.", "next": "ready"}, + "ready": {"speaker": "ishan", "text": "The descendants are waiting, the facsimile is mounted, and the lamps are safely dim. Your Highness, Master Z—the stage is yours.", "effects": [{"type": "relationship", "key": "trust", "amount": 2}]} + } + }, + "dance_after": { + "start": "breath", + "nodes": { + "breath": {"speaker": "lima", "expression": "tender", "text": "They leave the dancers beneath a storm of petals. Lima is breathless, laughing, and still holding both his hands. “You followed beautifully,” she says. “Even when you did not.”", "next": "z"}, + "z": {"speaker": "z", "expression": "flustered", "text": "I was conducting field research into whether a princess will catch a scholar who misses a turn.", "next": "result"}, + "result": {"speaker": "lima", "expression": "playful", "text": "And your findings?", "choices": [ + {"text": "She catches him, but keeps him longer than necessary.", "next": "keeps", "effects": [{"type": "relationship", "key": "romance", "amount": 6}, {"type": "relationship", "key": "comfort", "amount": 3}]}, + {"text": "Further trials require somewhere quieter.", "next": "quieter", "effects": [{"type": "relationship", "key": "humour", "amount": 3}, {"type": "relationship", "key": "adventure", "amount": 2}, {"type": "relationship", "key": "romance", "amount": 4}]} + ]}, + "keeps": {"speaker": "lima", "expression": "tender", "text": "“A serious methodological concern.” Her thumb traces his knuckles. “We should discuss it beside the river, where nobody will interrupt the peer review.”", "next": "leave"}, + "quieter": {"speaker": "lima", "expression": "playful", "text": "“At last, rigorous scholarship.” She steals him from the pavilion while Ishan heroically redirects three councillors and an aunt who is not fooled at all.", "next": "leave"}, + "leave": {"speaker": "z", "expression": "warm", "text": "They walk toward the moonlit steps as music softens behind them, the public argument safely alive and the private answer waiting by the water.", "effects": [{"type": "relationship", "key": "trust", "amount": 2}]} + } + } +} diff --git a/assets/games/two-rivers/data/dialogue/story.json b/assets/games/two-rivers/data/dialogue/story.json new file mode 100644 index 0000000..f170627 --- /dev/null +++ b/assets/games/two-rivers/data/dialogue/story.json @@ -0,0 +1,220 @@ +{ + "arrival": { + "start": "first", + "nodes": { + "first": { + "speaker": "lima", + "expression": "curious", + "text": "You must be Z. The scholar who corrected three footnotes in our festival catalogue before breakfast.", + "next": "reply" + }, + "reply": { + "speaker": "z", + "expression": "wry", + "text": "Two footnotes. The third was merely being watched very closely.", + "choices": [ + {"text": "I was hoping to impress the curator.", "next": "flirt", "effects": [{"type": "relationship", "key": "romance", "amount": 5}, {"type": "relationship", "key": "humour", "amount": 4}, {"type": "achievement", "id": "first_spark"}]}, + {"text": "Accuracy is a form of respect.", "next": "respect", "effects": [{"type": "relationship", "key": "respect", "amount": 6}, {"type": "relationship", "key": "trust", "amount": 3}]}, + {"text": "I panic-edit when beautiful royalty reads over my shoulder.", "next": "flirt", "effects": [{"type": "relationship", "key": "romance", "amount": 6}, {"type": "relationship", "key": "comfort", "amount": 3}, {"type": "achievement", "id": "first_spark"}]} + ] + }, + "flirt": { + "speaker": "lima", + "expression": "playful", + "text": "Careful. If you keep speaking that plainly, I may arrange for you to stand very close to me all week.", + "next": "business" + }, + "respect": { + "speaker": "lima", + "expression": "warm", + "text": "Then we agree. History deserves precision—especially when powerful people prefer decoration.", + "next": "business" + }, + "business": { + "speaker": "lima", + "expression": "regal", + "text": "Come. I want you to see the Moonlit Manuscript before the doors open. Its last folio carries the festival's oldest dedication.", + "effects": [{"type": "flag", "id": "met_lima"}, {"type": "relationship", "key": "adventure", "amount": 2}, {"type": "discoverGift", "id": "inkstone"}] + } + } + }, + "saira": { + "start": "welcome", + "nodes": { + "welcome": { + "speaker": "saira", + "text": "So you are the visiting scholar. Lima has mentioned your footnotes twice, which is once more than she mentions most diplomats.", + "choices": [ + {"text": "Her catalogue deserved the attention.", "next": "good", "effects": [{"type": "relationship", "key": "familyApproval", "amount": 5}, {"type": "relationship", "key": "respect", "amount": 2}]}, + {"text": "Only the footnotes? I must work harder.", "next": "bold", "effects": [{"type": "relationship", "key": "humour", "amount": 3}, {"type": "relationship", "key": "romance", "amount": 2}]} + ] + }, + "good": {"speaker": "saira", "text": "A diplomatic answer. Fortunately, your expression ruined it. I think I shall like you.", "effects": [{"type": "discoverGift", "id": "embroidered_ribbon"}]}, + "bold": {"speaker": "saira", "text": "Bold. I see why she smiled after breakfast. Do try not to cause an international incident before supper.", "effects": [{"type": "discoverGift", "id": "embroidered_ribbon"}]} + } + }, + "missing_folio": { + "start": "case", + "nodes": { + "case": { + "speaker": "lima", + "expression": "determined", + "text": "The seal is intact, the glass unbroken, and the final folio is gone. Someone with a keeper's key removed it.", + "next": "ribbon" + }, + "ribbon": { + "speaker": "z", + "expression": "thoughtful", + "text": "Not quite gone. This indigo thread was caught beneath the frame. The knot marks a reading order: dawn, rain, lantern, moon.", + "choices": [ + {"text": "Ask Lima what she thinks before proposing a theory.", "next": "together", "effects": [{"type": "relationship", "key": "respect", "amount": 5}, {"type": "relationship", "key": "trust", "amount": 4}]}, + {"text": "Suggest a secret keeper's trail.", "next": "trail", "effects": [{"type": "relationship", "key": "adventure", "amount": 5}, {"type": "relationship", "key": "trust", "amount": 2}]}, + {"text": "Observe that mysteries improve an already excellent first date.", "next": "date", "effects": [{"type": "relationship", "key": "humour", "amount": 4}, {"type": "relationship", "key": "romance", "amount": 5}]} + ] + }, + "together": { + "speaker": "lima", + "expression": "warm", + "text": "You ask instead of announcing. That is rarer in this palace than any illuminated folio. I think Anwara wants us to follow.", + "next": "close" + }, + "trail": { + "speaker": "lima", + "expression": "curious", + "text": "Anwara taught me that knot when I was twelve. If she left it, the disappearance is a message—not a theft.", + "next": "close" + }, + "date": { + "speaker": "lima", + "expression": "playful", + "text": "A first date usually includes tea. Solve my missing-manuscript problem and I may permit a scandalous second cup.", + "next": "close" + }, + "close": { + "speaker": "z", + "expression": "warm", + "text": "Then we begin with four shelves and one lantern. Stay close—the scholarship may become dangerous.", + "effects": [{"type": "item", "id": "folio_ribbon"}, {"type": "quest", "id": "letters", "action": "start"}, {"type": "discoverGift", "id": "poetry_book"}] + } + } + }, + "garden_confidence": { + "start": "bench", + "nodes": { + "bench": { + "speaker": "lima", + "expression": "vulnerable", + "text": "Everyone assumes confidence is effortless when one wears a crown. Mostly it means learning where to hide the trembling.", + "choices": [ + {"text": "Offer your hand without filling the silence.", "next": "hand", "effects": [{"type": "relationship", "key": "comfort", "amount": 7}, {"type": "relationship", "key": "trust", "amount": 5}, {"type": "relationship", "key": "romance", "amount": 3}]}, + {"text": "Tell her courage is action taken while trembling.", "next": "courage", "effects": [{"type": "relationship", "key": "respect", "amount": 5}, {"type": "relationship", "key": "adventure", "amount": 4}]}, + {"text": "Confess that she makes you forget several useful words.", "next": "words", "effects": [{"type": "relationship", "key": "humour", "amount": 4}, {"type": "relationship", "key": "romance", "amount": 7}]} + ] + }, + "hand": { + "speaker": "lima", + "expression": "tender", + "text": "Her fingers settle between yours. “You are surprisingly good at silence, Z. Do not become proud; I still prefer your voice.”", + "effects": [{"type": "discoverGift", "id": "spiced_tea"}] + }, + "courage": { + "speaker": "lima", + "expression": "determined", + "text": "Then let us tremble efficiently. There is a clue at the archery range—and I intend to beat you to it.", + "effects": [{"type": "discoverGift", "id": "archery_charm"}] + }, + "words": { + "speaker": "lima", + "expression": "playful", + "text": "Only several? I had hoped to be devastating. Sit nearer. Perhaps proximity will improve my performance.", + "effects": [{"type": "discoverGift", "id": "brass_compass"}] + } + } + }, + "anwara_truth": { + "start": "meeting", + "nodes": { + "meeting": { + "speaker": "anwara", + "text": "You followed every thread. Good. I hid the folio because the council planned to display it beneath hot lamps until its pigments flaked away.", + "next": "history" + }, + "history": { + "speaker": "anwara", + "text": "But protection was not my only reason. The dedication names cooks, boatwrights, poets, gardeners, and travellers as the festival's founders. The court catalogue names only kings.", + "next": "lima" + }, + "lima": { + "speaker": "lima", + "expression": "determined", + "text": "You should have trusted me with the truth.", + "choices": [ + {"text": "Anwara was wrong to act alone, but right to protect the folio.", "next": "balanced", "effects": [{"type": "relationship", "key": "trust", "amount": 5}, {"type": "relationship", "key": "respect", "amount": 5}, {"type": "relationship", "key": "familyApproval", "amount": 3}]}, + {"text": "The public deserves the complete history tonight.", "next": "public", "effects": [{"type": "relationship", "key": "adventure", "amount": 6}, {"type": "relationship", "key": "respect", "amount": 3}]}, + {"text": "Let Lima decide; it is her risk and responsibility.", "next": "decide", "effects": [{"type": "relationship", "key": "trust", "amount": 7}, {"type": "relationship", "key": "romance", "amount": 3}]} + ] + }, + "balanced": {"speaker": "anwara", "text": "A precise rebuke. You would have made a formidable keeper."}, + "public": {"speaker": "lima", "expression": "triumphant", "text": "Then tonight the founders recover their names—and the council learns that truth survives poor lighting."}, + "decide": {"speaker": "lima", "expression": "tender", "text": "Thank you. Not for agreeing—for remembering that the choice is mine. We reveal it, carefully and completely."} + } + }, + "resolution": { + "start": "stage", + "nodes": { + "stage": { + "speaker": "saira", + "text": "The council is listening, Lima. How shall the folio be presented?", + "choices": [ + {"text": "As a shared text, studied before it is celebrated.", "next": "scholar", "effects": [{"type": "bias", "id": "scholar", "amount": 12}, {"type": "relationship", "key": "respect", "amount": 6}]}, + {"text": "With the founders' descendants telling the story.", "next": "families", "effects": [{"type": "bias", "id": "families", "amount": 12}, {"type": "relationship", "key": "familyApproval", "amount": 7}]}, + {"text": "As the first clue in a travelling exhibition.", "next": "road", "effects": [{"type": "bias", "id": "road", "amount": 12}, {"type": "relationship", "key": "adventure", "amount": 7}]}, + {"text": "Briefly. Then we dance before the speeches breed.", "next": "laughter", "effects": [{"type": "bias", "id": "laughter", "amount": 12}, {"type": "relationship", "key": "humour", "amount": 7}]} + ] + }, + "scholar": {"speaker": "lima", "expression": "warm", "text": "Z and I will prepare the first public edition together. I expect lively arguments in the margins."}, + "families": {"speaker": "lima", "expression": "regal", "text": "The palace will lend the page; the city will supply the voices. That is how the festival began."}, + "road": {"speaker": "lima", "expression": "triumphant", "text": "Let the folio travel both rivers. History should meet the people who inherited it."}, + "laughter": {"speaker": "lima", "expression": "playful", "text": "A compelling scholarly principle. I second the motion—especially the dancing."} + } + }, + "moonrise": { + "start": "river", + "nodes": { + "river": { + "speaker": "lima", + "expression": "tender", + "text": "The city is still arguing about the folio. You realise this means the festival succeeded.", + "next": "z" + }, + "z": { + "speaker": "z", + "expression": "warm", + "text": "I have been distracted from the historical significance by the way moonlight keeps taking your side.", + "choices": [ + {"text": "Ask whether she would object to being kissed.", "next": "kiss", "effects": [{"type": "relationship", "key": "trust", "amount": 4}, {"type": "relationship", "key": "romance", "amount": 9}]}, + {"text": "Tell her you do not want this night to be an ending.", "next": "promise", "effects": [{"type": "relationship", "key": "comfort", "amount": 5}, {"type": "relationship", "key": "romance", "amount": 8}]}, + {"text": "Suggest investigating the dangerous proximity of her smile.", "next": "kiss", "effects": [{"type": "relationship", "key": "humour", "amount": 4}, {"type": "relationship", "key": "romance", "amount": 8}]} + ] + }, + "kiss": { + "speaker": "lima", + "expression": "playful", + "text": "“I would object only to further delay.” She catches his collar, and the river, the music, and every carefully prepared sentence disappear.", + "next": "after" + }, + "promise": { + "speaker": "lima", + "expression": "tender", + "text": "“Good. I have several mornings in mind.” Her kiss is warm, certain, and entirely unconcerned with the waiting palace.", + "next": "after" + }, + "after": { + "speaker": "z", + "expression": "flustered", + "text": "Much later, when the pavilion lanterns burn low, Z remembers that he once came here to correct footnotes.", + "effects": [{"type": "achievement", "id": "moonrise"}, {"type": "ending"}] + } + } + } +} diff --git a/assets/games/two-rivers/data/endings.json b/assets/games/two-rivers/data/endings.json new file mode 100644 index 0000000..f5c8864 --- /dev/null +++ b/assets/games/two-rivers/data/endings.json @@ -0,0 +1,22 @@ +{ + "scholar": { + "title": "The Scholar's Promise", + "weights": {"respect": 0.35, "trust": 0.25, "comfort": 0.2, "romance": 0.2}, + "epilogue": "They begin with letters—long, argumentative, tender letters—until two desks become one shared library. Lima calls it diplomacy. Z calls it losing every debate beautifully." + }, + "laughter": { + "title": "Laughter After Midnight", + "weights": {"humour": 0.35, "comfort": 0.3, "romance": 0.2, "adventure": 0.15}, + "epilogue": "Their courtship becomes a minor scandal of badly concealed smiles, midnight tea, and solemn meetings ruined by private jokes. Neither of them makes the slightest effort to improve." + }, + "road": { + "title": "The Road Beyond the Palace", + "weights": {"adventure": 0.35, "trust": 0.25, "humour": 0.2, "romance": 0.2}, + "epilogue": "At first light they leave with two horses, one map, and no agreement about the route. Lima brings books. Z brings snacks. The horizon learns to expect them." + }, + "families": { + "title": "Two Families, One Table", + "weights": {"familyApproval": 0.35, "respect": 0.25, "comfort": 0.2, "romance": 0.2}, + "epilogue": "The first shared supper runs past midnight: recipes exchanged, stories improved in the telling, and two families pretending not to notice whose hands remain joined beneath the table." + } +} diff --git a/assets/games/two-rivers/data/items.json b/assets/games/two-rivers/data/items.json new file mode 100644 index 0000000..8dc6449 --- /dev/null +++ b/assets/games/two-rivers/data/items.json @@ -0,0 +1,13 @@ +{ + "gifts": [ + {"id": "inkstone", "name": "Carved Inkstone", "hint": "For someone who writes in every margin.", "effects": {"respect": 5, "romance": 3}}, + {"id": "brass_compass", "name": "Brass Star Compass", "hint": "For a curious traveller who dislikes standing still.", "effects": {"adventure": 5, "romance": 3}}, + {"id": "poetry_book", "name": "Book of River Poems", "hint": "For a reader with an inconveniently warm smile.", "effects": {"comfort": 4, "romance": 5}}, + {"id": "embroidered_ribbon", "name": "Embroidered Ribbon", "hint": "A small festival keepsake in indigo and gold.", "effects": {"familyApproval": 4, "romance": 3}}, + {"id": "spiced_tea", "name": "Tea Blend", "hint": "Cardamom, citrus peel, and an excuse to linger.", "effects": {"comfort": 5, "humour": 2}}, + {"id": "archery_charm", "name": "Archer's Charm", "hint": "For someone who claimed she was not competitive.", "effects": {"adventure": 4, "humour": 4}} + ], + "clues": ["folio_ribbon", "lantern_rubbing", "anwara_note", "founders_record"], + "flowers": ["jasmine", "tuberose", "marigold", "rose", "bakul"], + "letters": ["letter_1", "letter_2", "letter_3"] +} diff --git a/assets/games/two-rivers/data/locations.json b/assets/games/two-rivers/data/locations.json new file mode 100644 index 0000000..d9ce10a --- /dev/null +++ b/assets/games/two-rivers/data/locations.json @@ -0,0 +1,106 @@ +{ + "courtyard": { + "name": "Palace Courtyard", + "act": "Act I · The Missing Folio", + "background": "courtyard", + "music": "palace", + "ambience": "birds", + "actors": [{"id": "ishan", "x": 0.5}, {"id": "saira", "x": 0.84}], + "spawn": 0.14, + "objective": "Meet Lima beside the exhibition tables.", + "hotspots": [ + {"id": "courtyard_preparations", "x": 0.3, "label": "Help arrange the festival banners", "dialogue": "courtyard_preparations", "once": true}, + {"id": "ishan_protocol", "x": 0.5, "label": "Ask Ishan about festival protocol", "dialogue": "ishan_protocol", "once": true}, + {"id": "lima_arrival", "x": 0.68, "label": "Speak with Lima", "dialogue": "arrival", "once": true}, + {"id": "delegation_walk", "x": 0.76, "label": "Walk the courtyard edge with Lima", "dialogue": "delegation_walk", "requires": ["dialogue:arrival"], "once": true}, + {"id": "saira_welcome", "x": 0.84, "label": "Greet Aunt Saira", "dialogue": "saira", "once": true}, + {"id": "to_library", "x": 0.94, "label": "Enter the royal library", "goto": "library", "requires": ["dialogue:arrival", "dialogue:delegation_walk", "dialogue:courtyard_preparations", "dialogue:ishan_protocol"]} + ] + }, + "library": { + "name": "Royal Library", + "act": "Act I · The Missing Folio", + "background": "library", + "music": "morning", + "ambience": "library", + "actors": [{"id": "anwara", "x": 0.16}], + "spawn": 0.1, + "objective": "Examine the empty display and solve Anwara's lantern cipher.", + "hotspots": [ + {"id": "anwara_memory", "x": 0.16, "label": "Inspect the eastern cabinet with Anwara", "dialogue": "anwara_memory", "once": true}, + {"id": "empty_case", "x": 0.3, "label": "Examine the empty case", "dialogue": "missing_folio", "once": true}, + {"id": "library_search", "x": 0.4, "label": "Search the altered shelves", "dialogue": "library_search", "requires": ["dialogue:missing_folio"], "once": true}, + {"id": "letter_one", "x": 0.48, "label": "Recover a misfiled letter", "collect": "letter_1", "quest": "letters", "once": true}, + {"id": "library_balcony", "x": 0.6, "label": "Step onto the rain balcony with Lima", "dialogue": "library_balcony", "requires": ["dialogue:missing_folio"], "once": true}, + {"id": "cipher", "x": 0.73, "label": "Solve the lantern cipher", "puzzle": "cipher", "requires": ["dialogue:missing_folio"], "once": true}, + {"id": "to_garden", "x": 0.94, "label": "Follow the clue to the moon garden", "goto": "garden", "requires": ["flag:cipher_solved", "dialogue:anwara_memory", "dialogue:library_search", "dialogue:library_balcony"]} + ] + }, + "garden": { + "name": "Moon Garden", + "act": "Act II · A City of Clues", + "background": "garden", + "music": "garden", + "ambience": "water", + "actors": [{"id": "mira", "x": 0.08}], + "spawn": 0.08, + "objective": "Find the flowers named in the folio and test the old archery clue.", + "hotspots": [ + {"id": "mira_garland", "x": 0.08, "label": "Ask Mira about the communal garland", "dialogue": "mira_garland", "once": true}, + {"id": "flower_jasmine", "x": 0.16, "label": "Gather jasmine", "collect": "jasmine", "quest": "garland", "once": true}, + {"id": "flower_tuberose", "x": 0.3, "label": "Gather tuberose", "collect": "tuberose", "quest": "garland", "once": true}, + {"id": "garden_talk", "x": 0.56, "label": "Sit with Lima", "dialogue": "garden_confidence", "once": true}, + {"id": "garden_walk", "x": 0.6, "label": "Carry Mira's basket with Lima", "dialogue": "garden_walk", "requires": ["dialogue:garden_confidence"], "once": true}, + {"id": "garden_history", "x": 0.64, "label": "Study the founders' flower mosaic", "dialogue": "garden_history", "requires": ["dialogue:garden_confidence"], "once": true}, + {"id": "flower_sequence", "x": 0.7, "label": "Solve the flower mosaic", "puzzle": "flower_sequence", "requires": ["dialogue:garden_history"], "once": true}, + {"id": "archery", "x": 0.79, "label": "Try the moonlit archery trial", "minigame": "archery", "requires": ["flag:flower_sequence_solved"], "once": true}, + {"id": "archery_after", "x": 0.87, "label": "Review the range ledger with Lima", "dialogue": "archery_after", "requires": ["minigame:archery"], "once": true}, + {"id": "to_market", "x": 0.94, "label": "Continue to the old marketplace", "goto": "market", "requires": ["minigame:archery", "dialogue:archery_after", "dialogue:mira_garland", "dialogue:garden_walk"]} + ] + }, + "market": { + "name": "Old Marketplace", + "act": "Act II · A City of Clues", + "background": "market", + "music": "marketplace", + "ambience": "crowd", + "actors": [{"id": "nadia", "x": 0.08}, {"id": "rayhan", "x": 0.81}, {"id": "anwara", "x": 0.91}], + "spawn": 0.08, + "objective": "Ask the market's keepers what the court history forgot.", + "hotspots": [ + {"id": "nadia_letters", "x": 0.08, "label": "Speak with Nadia the bookseller", "dialogue": "nadia_letters", "once": true}, + {"id": "flower_marigold", "x": 0.18, "label": "Gather marigold", "collect": "marigold", "quest": "garland", "once": true}, + {"id": "flower_rose", "x": 0.3, "label": "Gather a garden rose", "collect": "rose", "quest": "garland", "once": true}, + {"id": "flower_bakul", "x": 0.42, "label": "Gather bakul blossom", "collect": "bakul", "quest": "garland", "once": true}, + {"id": "letter_two", "x": 0.53, "label": "Recover a bookseller's letter", "collect": "letter_2", "quest": "letters", "once": true}, + {"id": "letter_three", "x": 0.62, "label": "Recover the final letter", "collect": "letter_3", "quest": "letters", "once": true}, + {"id": "market_walk", "x": 0.67, "label": "Walk through the market with Lima", "dialogue": "market_walk", "once": true}, + {"id": "gift", "x": 0.72, "label": "Choose a gift for Lima", "gift": true, "once": true}, + {"id": "garland_complete", "x": 0.75, "label": "Raise the completed communal garland", "dialogue": "garland_complete", "requires": ["quest:garland"], "once": true}, + {"id": "letters_complete", "x": 0.78, "label": "Read the recovered founders' letters", "dialogue": "letters_complete", "requires": ["quest:letters"], "once": true}, + {"id": "market_founders", "x": 0.79, "label": "Plan how to credit the festival founders", "dialogue": "market_founders", "requires": ["dialogue:nadia_letters", "dialogue:market_walk"], "once": true}, + {"id": "rayhan_kitchen", "x": 0.81, "label": "Join Rayhan at the festival kitchen", "dialogue": "rayhan_kitchen", "once": true}, + {"id": "cooking", "x": 0.85, "label": "Help Rayhan finish the festival dish", "minigame": "cooking", "requires": ["dialogue:rayhan_kitchen"], "once": true}, + {"id": "anwara_truth", "x": 0.91, "label": "Confront Keeper Anwara", "dialogue": "anwara_truth", "requires": ["minigame:cooking"], "once": true}, + {"id": "to_festival", "x": 0.97, "label": "Carry the truth to the riverside", "goto": "pavilion", "requires": ["dialogue:anwara_truth", "dialogue:nadia_letters", "dialogue:market_walk", "dialogue:market_founders"]} + ] + }, + "pavilion": { + "name": "Riverside Festival Pavilion", + "act": "Act III · Moonrise", + "background": "pavilion", + "music": "festival", + "ambience": "crowd", + "actors": [{"id": "ishan", "x": 0.28}, {"id": "saira", "x": 0.4}], + "spawn": 0.08, + "objective": "Choose how the true story will be told.", + "hotspots": [ + {"id": "ishan_stage", "x": 0.28, "label": "Prepare the public presentation", "dialogue": "ishan_stage", "once": true}, + {"id": "pavilion_waiting", "x": 0.38, "label": "Wait with the council before the unveiling", "dialogue": "pavilion_waiting", "requires": ["dialogue:ishan_stage"], "once": true}, + {"id": "resolution", "x": 0.48, "label": "Address the festival council", "dialogue": "resolution", "requires": ["dialogue:ishan_stage", "dialogue:pavilion_waiting"], "once": true}, + {"id": "dance", "x": 0.72, "label": "Join Lima for the moonrise dance", "minigame": "dance", "requires": ["dialogue:resolution"], "once": true}, + {"id": "dance_after", "x": 0.82, "label": "Catch your breath with Lima", "dialogue": "dance_after", "requires": ["minigame:dance"], "once": true}, + {"id": "moonrise", "x": 0.9, "label": "Walk with Lima by the river", "dialogue": "moonrise", "requires": ["dialogue:dance_after"], "once": true} + ] + } +} diff --git a/assets/games/two-rivers/data/quests.json b/assets/games/two-rivers/data/quests.json new file mode 100644 index 0000000..90e0bb4 --- /dev/null +++ b/assets/games/two-rivers/data/quests.json @@ -0,0 +1,14 @@ +{ + "garland": { + "name": "A Garland for Everyone", + "description": "Gather five flowers for Mira's communal festival garland.", + "target": 5, + "reward": {"familyApproval": 8, "comfort": 3, "cg": "garland"} + }, + "letters": { + "name": "The Bookseller's Letters", + "description": "Recover three letters that record the festival's forgotten founders.", + "target": 3, + "reward": {"trust": 5, "respect": 6, "cg": "library"} + } +} diff --git a/assets/games/two-rivers/images/backgrounds/courtyard.png b/assets/games/two-rivers/images/backgrounds/courtyard.png new file mode 100644 index 0000000..07ccf49 Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/courtyard.png differ diff --git a/assets/games/two-rivers/images/backgrounds/courtyard.webp b/assets/games/two-rivers/images/backgrounds/courtyard.webp new file mode 100644 index 0000000..902b344 Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/courtyard.webp differ diff --git a/assets/games/two-rivers/images/backgrounds/garden.png b/assets/games/two-rivers/images/backgrounds/garden.png new file mode 100644 index 0000000..bbd409b Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/garden.png differ diff --git a/assets/games/two-rivers/images/backgrounds/garden.webp b/assets/games/two-rivers/images/backgrounds/garden.webp new file mode 100644 index 0000000..79f8633 Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/garden.webp differ diff --git a/assets/games/two-rivers/images/backgrounds/library.png b/assets/games/two-rivers/images/backgrounds/library.png new file mode 100644 index 0000000..38a0a21 Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/library.png differ diff --git a/assets/games/two-rivers/images/backgrounds/library.webp b/assets/games/two-rivers/images/backgrounds/library.webp new file mode 100644 index 0000000..f47f39f Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/library.webp differ diff --git a/assets/games/two-rivers/images/backgrounds/market.png b/assets/games/two-rivers/images/backgrounds/market.png new file mode 100644 index 0000000..e50ed82 Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/market.png differ diff --git a/assets/games/two-rivers/images/backgrounds/market.webp b/assets/games/two-rivers/images/backgrounds/market.webp new file mode 100644 index 0000000..9aa56d0 Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/market.webp differ diff --git a/assets/games/two-rivers/images/backgrounds/pavilion.png b/assets/games/two-rivers/images/backgrounds/pavilion.png new file mode 100644 index 0000000..f0bd848 Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/pavilion.png differ diff --git a/assets/games/two-rivers/images/backgrounds/pavilion.webp b/assets/games/two-rivers/images/backgrounds/pavilion.webp new file mode 100644 index 0000000..a5cc4ba Binary files /dev/null and b/assets/games/two-rivers/images/backgrounds/pavilion.webp differ diff --git a/assets/games/two-rivers/images/cg/families.png b/assets/games/two-rivers/images/cg/families.png new file mode 100644 index 0000000..ae5be21 Binary files /dev/null and b/assets/games/two-rivers/images/cg/families.png differ diff --git a/assets/games/two-rivers/images/cg/families.webp b/assets/games/two-rivers/images/cg/families.webp new file mode 100644 index 0000000..f1bcf77 Binary files /dev/null and b/assets/games/two-rivers/images/cg/families.webp differ diff --git a/assets/games/two-rivers/images/cg/garland.png b/assets/games/two-rivers/images/cg/garland.png new file mode 100644 index 0000000..d8f7f5f Binary files /dev/null and b/assets/games/two-rivers/images/cg/garland.png differ diff --git a/assets/games/two-rivers/images/cg/garland.webp b/assets/games/two-rivers/images/cg/garland.webp new file mode 100644 index 0000000..b40e214 Binary files /dev/null and b/assets/games/two-rivers/images/cg/garland.webp differ diff --git a/assets/games/two-rivers/images/cg/laughter.png b/assets/games/two-rivers/images/cg/laughter.png new file mode 100644 index 0000000..4dc4df4 Binary files /dev/null and b/assets/games/two-rivers/images/cg/laughter.png differ diff --git a/assets/games/two-rivers/images/cg/laughter.webp b/assets/games/two-rivers/images/cg/laughter.webp new file mode 100644 index 0000000..e9b2699 Binary files /dev/null and b/assets/games/two-rivers/images/cg/laughter.webp differ diff --git a/assets/games/two-rivers/images/cg/library.png b/assets/games/two-rivers/images/cg/library.png new file mode 100644 index 0000000..e7c8b62 Binary files /dev/null and b/assets/games/two-rivers/images/cg/library.png differ diff --git a/assets/games/two-rivers/images/cg/library.webp b/assets/games/two-rivers/images/cg/library.webp new file mode 100644 index 0000000..3909d25 Binary files /dev/null and b/assets/games/two-rivers/images/cg/library.webp differ diff --git a/assets/games/two-rivers/images/cg/road.png b/assets/games/two-rivers/images/cg/road.png new file mode 100644 index 0000000..6495d17 Binary files /dev/null and b/assets/games/two-rivers/images/cg/road.png differ diff --git a/assets/games/two-rivers/images/cg/road.webp b/assets/games/two-rivers/images/cg/road.webp new file mode 100644 index 0000000..d8ac850 Binary files /dev/null and b/assets/games/two-rivers/images/cg/road.webp differ diff --git a/assets/games/two-rivers/images/cg/scholar.png b/assets/games/two-rivers/images/cg/scholar.png new file mode 100644 index 0000000..9bd7f01 Binary files /dev/null and b/assets/games/two-rivers/images/cg/scholar.png differ diff --git a/assets/games/two-rivers/images/cg/scholar.webp b/assets/games/two-rivers/images/cg/scholar.webp new file mode 100644 index 0000000..c4b47aa Binary files /dev/null and b/assets/games/two-rivers/images/cg/scholar.webp differ diff --git a/assets/games/two-rivers/images/characters/anwara.png b/assets/games/two-rivers/images/characters/anwara.png new file mode 100644 index 0000000..12ec5c4 Binary files /dev/null and b/assets/games/two-rivers/images/characters/anwara.png differ diff --git a/assets/games/two-rivers/images/characters/anwara.webp b/assets/games/two-rivers/images/characters/anwara.webp new file mode 100644 index 0000000..c121e85 Binary files /dev/null and b/assets/games/two-rivers/images/characters/anwara.webp differ diff --git a/assets/games/two-rivers/images/characters/ishan.png b/assets/games/two-rivers/images/characters/ishan.png new file mode 100644 index 0000000..45a4f3a Binary files /dev/null and b/assets/games/two-rivers/images/characters/ishan.png differ diff --git a/assets/games/two-rivers/images/characters/ishan.webp b/assets/games/two-rivers/images/characters/ishan.webp new file mode 100644 index 0000000..7456a93 Binary files /dev/null and b/assets/games/two-rivers/images/characters/ishan.webp differ diff --git a/assets/games/two-rivers/images/characters/lima.png b/assets/games/two-rivers/images/characters/lima.png new file mode 100644 index 0000000..d6e4876 Binary files /dev/null and b/assets/games/two-rivers/images/characters/lima.png differ diff --git a/assets/games/two-rivers/images/characters/lima.webp b/assets/games/two-rivers/images/characters/lima.webp new file mode 100644 index 0000000..bb0365c Binary files /dev/null and b/assets/games/two-rivers/images/characters/lima.webp differ diff --git a/assets/games/two-rivers/images/characters/mira.png b/assets/games/two-rivers/images/characters/mira.png new file mode 100644 index 0000000..a7370e1 Binary files /dev/null and b/assets/games/two-rivers/images/characters/mira.png differ diff --git a/assets/games/two-rivers/images/characters/mira.webp b/assets/games/two-rivers/images/characters/mira.webp new file mode 100644 index 0000000..f797dac Binary files /dev/null and b/assets/games/two-rivers/images/characters/mira.webp differ diff --git a/assets/games/two-rivers/images/characters/nadia.png b/assets/games/two-rivers/images/characters/nadia.png new file mode 100644 index 0000000..5ce91a7 Binary files /dev/null and b/assets/games/two-rivers/images/characters/nadia.png differ diff --git a/assets/games/two-rivers/images/characters/nadia.webp b/assets/games/two-rivers/images/characters/nadia.webp new file mode 100644 index 0000000..f4cfdc4 Binary files /dev/null and b/assets/games/two-rivers/images/characters/nadia.webp differ diff --git a/assets/games/two-rivers/images/characters/rayhan.png b/assets/games/two-rivers/images/characters/rayhan.png new file mode 100644 index 0000000..36bcbf6 Binary files /dev/null and b/assets/games/two-rivers/images/characters/rayhan.png differ diff --git a/assets/games/two-rivers/images/characters/rayhan.webp b/assets/games/two-rivers/images/characters/rayhan.webp new file mode 100644 index 0000000..d7f2332 Binary files /dev/null and b/assets/games/two-rivers/images/characters/rayhan.webp differ diff --git a/assets/games/two-rivers/images/characters/saira.png b/assets/games/two-rivers/images/characters/saira.png new file mode 100644 index 0000000..383889f Binary files /dev/null and b/assets/games/two-rivers/images/characters/saira.png differ diff --git a/assets/games/two-rivers/images/characters/saira.webp b/assets/games/two-rivers/images/characters/saira.webp new file mode 100644 index 0000000..8ca6215 Binary files /dev/null and b/assets/games/two-rivers/images/characters/saira.webp differ diff --git a/assets/games/two-rivers/images/characters/z.png b/assets/games/two-rivers/images/characters/z.png new file mode 100644 index 0000000..6dab0d8 Binary files /dev/null and b/assets/games/two-rivers/images/characters/z.png differ diff --git a/assets/games/two-rivers/images/characters/z.webp b/assets/games/two-rivers/images/characters/z.webp new file mode 100644 index 0000000..aaabf54 Binary files /dev/null and b/assets/games/two-rivers/images/characters/z.webp differ diff --git a/assets/games/two-rivers/images/items/anwara_note.png b/assets/games/two-rivers/images/items/anwara_note.png new file mode 100644 index 0000000..f814f48 Binary files /dev/null and b/assets/games/two-rivers/images/items/anwara_note.png differ diff --git a/assets/games/two-rivers/images/items/anwara_note.webp b/assets/games/two-rivers/images/items/anwara_note.webp new file mode 100644 index 0000000..745f478 Binary files /dev/null and b/assets/games/two-rivers/images/items/anwara_note.webp differ diff --git a/assets/games/two-rivers/images/items/archery_charm.png b/assets/games/two-rivers/images/items/archery_charm.png new file mode 100644 index 0000000..5fde914 Binary files /dev/null and b/assets/games/two-rivers/images/items/archery_charm.png differ diff --git a/assets/games/two-rivers/images/items/archery_charm.webp b/assets/games/two-rivers/images/items/archery_charm.webp new file mode 100644 index 0000000..e33b3bb Binary files /dev/null and b/assets/games/two-rivers/images/items/archery_charm.webp differ diff --git a/assets/games/two-rivers/images/items/bakul.png b/assets/games/two-rivers/images/items/bakul.png new file mode 100644 index 0000000..09ae4d3 Binary files /dev/null and b/assets/games/two-rivers/images/items/bakul.png differ diff --git a/assets/games/two-rivers/images/items/bakul.webp b/assets/games/two-rivers/images/items/bakul.webp new file mode 100644 index 0000000..7628ab8 Binary files /dev/null and b/assets/games/two-rivers/images/items/bakul.webp differ diff --git a/assets/games/two-rivers/images/items/brass_compass.png b/assets/games/two-rivers/images/items/brass_compass.png new file mode 100644 index 0000000..7c15733 Binary files /dev/null and b/assets/games/two-rivers/images/items/brass_compass.png differ diff --git a/assets/games/two-rivers/images/items/brass_compass.webp b/assets/games/two-rivers/images/items/brass_compass.webp new file mode 100644 index 0000000..d67b844 Binary files /dev/null and b/assets/games/two-rivers/images/items/brass_compass.webp differ diff --git a/assets/games/two-rivers/images/items/embroidered_ribbon.png b/assets/games/two-rivers/images/items/embroidered_ribbon.png new file mode 100644 index 0000000..51efb97 Binary files /dev/null and b/assets/games/two-rivers/images/items/embroidered_ribbon.png differ diff --git a/assets/games/two-rivers/images/items/embroidered_ribbon.webp b/assets/games/two-rivers/images/items/embroidered_ribbon.webp new file mode 100644 index 0000000..cbc1275 Binary files /dev/null and b/assets/games/two-rivers/images/items/embroidered_ribbon.webp differ diff --git a/assets/games/two-rivers/images/items/folio_ribbon.png b/assets/games/two-rivers/images/items/folio_ribbon.png new file mode 100644 index 0000000..81c0978 Binary files /dev/null and b/assets/games/two-rivers/images/items/folio_ribbon.png differ diff --git a/assets/games/two-rivers/images/items/folio_ribbon.webp b/assets/games/two-rivers/images/items/folio_ribbon.webp new file mode 100644 index 0000000..6c7125d Binary files /dev/null and b/assets/games/two-rivers/images/items/folio_ribbon.webp differ diff --git a/assets/games/two-rivers/images/items/founders_record.png b/assets/games/two-rivers/images/items/founders_record.png new file mode 100644 index 0000000..c1cdae1 Binary files /dev/null and b/assets/games/two-rivers/images/items/founders_record.png differ diff --git a/assets/games/two-rivers/images/items/founders_record.webp b/assets/games/two-rivers/images/items/founders_record.webp new file mode 100644 index 0000000..737529b Binary files /dev/null and b/assets/games/two-rivers/images/items/founders_record.webp differ diff --git a/assets/games/two-rivers/images/items/inkstone.png b/assets/games/two-rivers/images/items/inkstone.png new file mode 100644 index 0000000..e942ee1 Binary files /dev/null and b/assets/games/two-rivers/images/items/inkstone.png differ diff --git a/assets/games/two-rivers/images/items/inkstone.webp b/assets/games/two-rivers/images/items/inkstone.webp new file mode 100644 index 0000000..8ada53d Binary files /dev/null and b/assets/games/two-rivers/images/items/inkstone.webp differ diff --git a/assets/games/two-rivers/images/items/jasmine.png b/assets/games/two-rivers/images/items/jasmine.png new file mode 100644 index 0000000..072c917 Binary files /dev/null and b/assets/games/two-rivers/images/items/jasmine.png differ diff --git a/assets/games/two-rivers/images/items/jasmine.webp b/assets/games/two-rivers/images/items/jasmine.webp new file mode 100644 index 0000000..5965742 Binary files /dev/null and b/assets/games/two-rivers/images/items/jasmine.webp differ diff --git a/assets/games/two-rivers/images/items/lantern_rubbing.png b/assets/games/two-rivers/images/items/lantern_rubbing.png new file mode 100644 index 0000000..d8b12a5 Binary files /dev/null and b/assets/games/two-rivers/images/items/lantern_rubbing.png differ diff --git a/assets/games/two-rivers/images/items/lantern_rubbing.webp b/assets/games/two-rivers/images/items/lantern_rubbing.webp new file mode 100644 index 0000000..43025d9 Binary files /dev/null and b/assets/games/two-rivers/images/items/lantern_rubbing.webp differ diff --git a/assets/games/two-rivers/images/items/letter_1.png b/assets/games/two-rivers/images/items/letter_1.png new file mode 100644 index 0000000..3f4d40d Binary files /dev/null and b/assets/games/two-rivers/images/items/letter_1.png differ diff --git a/assets/games/two-rivers/images/items/letter_1.webp b/assets/games/two-rivers/images/items/letter_1.webp new file mode 100644 index 0000000..c1107b8 Binary files /dev/null and b/assets/games/two-rivers/images/items/letter_1.webp differ diff --git a/assets/games/two-rivers/images/items/letter_2.png b/assets/games/two-rivers/images/items/letter_2.png new file mode 100644 index 0000000..1511983 Binary files /dev/null and b/assets/games/two-rivers/images/items/letter_2.png differ diff --git a/assets/games/two-rivers/images/items/letter_2.webp b/assets/games/two-rivers/images/items/letter_2.webp new file mode 100644 index 0000000..cc3de4f Binary files /dev/null and b/assets/games/two-rivers/images/items/letter_2.webp differ diff --git a/assets/games/two-rivers/images/items/letter_3.png b/assets/games/two-rivers/images/items/letter_3.png new file mode 100644 index 0000000..74751e6 Binary files /dev/null and b/assets/games/two-rivers/images/items/letter_3.png differ diff --git a/assets/games/two-rivers/images/items/letter_3.webp b/assets/games/two-rivers/images/items/letter_3.webp new file mode 100644 index 0000000..7fc9975 Binary files /dev/null and b/assets/games/two-rivers/images/items/letter_3.webp differ diff --git a/assets/games/two-rivers/images/items/marigold.png b/assets/games/two-rivers/images/items/marigold.png new file mode 100644 index 0000000..3566245 Binary files /dev/null and b/assets/games/two-rivers/images/items/marigold.png differ diff --git a/assets/games/two-rivers/images/items/marigold.webp b/assets/games/two-rivers/images/items/marigold.webp new file mode 100644 index 0000000..b5c4908 Binary files /dev/null and b/assets/games/two-rivers/images/items/marigold.webp differ diff --git a/assets/games/two-rivers/images/items/poetry_book.png b/assets/games/two-rivers/images/items/poetry_book.png new file mode 100644 index 0000000..c36447f Binary files /dev/null and b/assets/games/two-rivers/images/items/poetry_book.png differ diff --git a/assets/games/two-rivers/images/items/poetry_book.webp b/assets/games/two-rivers/images/items/poetry_book.webp new file mode 100644 index 0000000..c4fa1a5 Binary files /dev/null and b/assets/games/two-rivers/images/items/poetry_book.webp differ diff --git a/assets/games/two-rivers/images/items/rose.png b/assets/games/two-rivers/images/items/rose.png new file mode 100644 index 0000000..051dd5f Binary files /dev/null and b/assets/games/two-rivers/images/items/rose.png differ diff --git a/assets/games/two-rivers/images/items/rose.webp b/assets/games/two-rivers/images/items/rose.webp new file mode 100644 index 0000000..33e0e8f Binary files /dev/null and b/assets/games/two-rivers/images/items/rose.webp differ diff --git a/assets/games/two-rivers/images/items/spiced_tea.png b/assets/games/two-rivers/images/items/spiced_tea.png new file mode 100644 index 0000000..0ef4988 Binary files /dev/null and b/assets/games/two-rivers/images/items/spiced_tea.png differ diff --git a/assets/games/two-rivers/images/items/spiced_tea.webp b/assets/games/two-rivers/images/items/spiced_tea.webp new file mode 100644 index 0000000..23ba37f Binary files /dev/null and b/assets/games/two-rivers/images/items/spiced_tea.webp differ diff --git a/assets/games/two-rivers/images/items/tuberose.png b/assets/games/two-rivers/images/items/tuberose.png new file mode 100644 index 0000000..c6c688a Binary files /dev/null and b/assets/games/two-rivers/images/items/tuberose.png differ diff --git a/assets/games/two-rivers/images/items/tuberose.webp b/assets/games/two-rivers/images/items/tuberose.webp new file mode 100644 index 0000000..9eeb8d3 Binary files /dev/null and b/assets/games/two-rivers/images/items/tuberose.webp differ diff --git a/assets/games/two-rivers/prompts/art-direction.md b/assets/games/two-rivers/prompts/art-direction.md new file mode 100644 index 0000000..ec9152f --- /dev/null +++ b/assets/games/two-rivers/prompts/art-direction.md @@ -0,0 +1,30 @@ +# Two Rivers at Moonrise — Art Direction + +## Visual language + +Painterly storybook fantasy for original fictional adults: vibrant natural colour, soft brush texture, expressive faces, cinematic light, detailed architecture, and grounded human proportions. Camera height remains near adult eye level. Environments use wide 16:9 side-view compositions with a clear walk lane in the lower third. + +## Cultural distinction + +- Lima and the palace-city draw specifically from Bengali textiles, river culture, terracotta craft, muslin, jamdani-inspired geometry, flower garlands, food presentation, and climate. +- Z’s clothing draws specifically from Pakistani tailoring and textiles: a structured kurta or sherwani-influenced long coat, waistcoat details, ajrak-informed geometry where appropriate, and practical scholar/traveller accessories. +- These references remain visibly distinct. Avoid a generic blended “South Asian” costume language. +- Sacred or ceremonial motifs are not used as arbitrary decoration. No pseudo-script or generated lettering appears in artwork. + +## Continuity + +Z is 27, with warm brown skin, dark wavy hair, a short neat beard, a teal long coat over a cream kurta, brown leather satchel, and archer’s bracer. Lima is 26, with warm brown skin, long dark braided hair, an ivory muslin sari with a deep red border, restrained gold jewellery, and an indigo curator’s sash when working. + +Full-body characters use the same proportions, three-quarter side-view orientation, soft outline treatment, warm key light from camera left, transparent final background, and foot anchor at the bottom centre. Portraits preserve facial identity and clothing. + +## Environment progression + +Afternoon begins with warm river light. Act II moves through golden dusk and blue garden shadow. Act III uses indigo moonlight, warm lantern pools, reflected river light, and restrained celebratory particles. + +## Exclusions + +No pixel art, anime conventions, photorealistic likenesses, celebrity resemblance, clip art, primitive placeholder geometry, generated words, watermarks, modern objects, sexualized anatomy, nudity, or characters who appear under 25. + +## Quality gate + +Reject assets with inconsistent faces, extra or malformed fingers, broken jewellery, unreadable silhouette, incorrect clothing construction, mixed cultural references without purpose, muddy alpha edges, chroma spill, watermarks, or embedded text. diff --git a/assets/games/two-rivers/prompts/audio-direction.md b/assets/games/two-rivers/prompts/audio-direction.md new file mode 100644 index 0000000..4bf45ea --- /dev/null +++ b/assets/games/two-rivers/prompts/audio-direction.md @@ -0,0 +1,5 @@ +# Audio Direction and Provenance + +Ten state names are Morning, Palace, Garden, Marketplace, Festival, Romantic, Sad, Night, Victory, and Ending. Seven ambience groups are birds, water, wind, crowd, garden insects, library room tone, and rain. SFX cover UI, footsteps, page turns, pickups, archery, cooking, puzzle feedback, and dance steps. + +Runtime derivatives use WebM/Opus first with MP3 fallback. `assets.json` records each original same-site source and licence statement. Howler is the only playback layer; Phaser audio remains disabled. diff --git a/assets/games/two-rivers/prompts/backgrounds.md b/assets/games/two-rivers/prompts/backgrounds.md new file mode 100644 index 0000000..7c52d8b --- /dev/null +++ b/assets/games/two-rivers/prompts/backgrounds.md @@ -0,0 +1,23 @@ +# Environment Production Prompts + +Shared constraints: `illustration-story`, 1536×1024 wide side-view game environment, painterly storybook fantasy, adult eye-level camera, clear walk lane across the lower third, layered foreground foliage or architecture, no people in the player lane, no text, watermark, photorealism, anime, or modern objects. Apply `art-direction.md`. + +## Palace Courtyard + +Warm afternoon Bengali river-palace courtyard prepared for a public manuscript exhibition; carved red stone arches, muslin canopies, indigo textiles, river visible beyond, framed folio displays at right, flowering planters, cinematic sunlight from upper left. + +## Royal Library + +Quiet Bengali royal archive with terracotta details, tall dark wood shelves, conservation tables, an empty glass folio case, brass lantern mechanisms, rain balcony, warm window light and deep scholarly shadows. + +## Moon Garden + +Lush palace moon garden at blue-gold dusk; jasmine, tuberose, marigolds, bakul, water channel, old floral mosaic, modest archery range, fireflies, pale moon emerging through foliage. + +## Old Marketplace + +Busy old Bengali river-city market at early evening; bookstall, flower sellers, communal kitchen, fabric awnings, terracotta façades, lanterns, clear walk lane, inviting civic energy without generated signs. + +## Riverside Pavilion + +Festival pavilion beside two moonlit rivers; indigo and ivory canopies, flower garlands, safe facsimile display, dance floor, public seating, lantern reflections, celebratory but intimate night atmosphere. diff --git a/assets/games/two-rivers/prompts/cg.md b/assets/games/two-rivers/prompts/cg.md new file mode 100644 index 0000000..724f95c --- /dev/null +++ b/assets/games/two-rivers/prompts/cg.md @@ -0,0 +1,27 @@ +# CG Production Prompts + +Shared constraints: `illustration-story`, 1536×1024 painterly romantic-adventure illustration, preserve approved Z and Lima identities, both unmistakably adult, consensual warmth, cinematic lighting, culturally distinct clothing, no text, watermark, explicit sex, nudity, or real-person resemblance. + +## Scholar's Promise + +Z and Lima sharing a library desk at dawn, annotated pages and two cups of tea, shoulders touching, affectionate argument visible in their expressions. + +## Laughter After Midnight + +Z and Lima laughing beneath late festival lanterns, private joke, hands linked, empty pavilion behind them. + +## Road Beyond the Palace + +Z and Lima at a river road overlook with travel satchels, one folded map, sunrise horizon, adventurous partnership. + +## Two Families + +Long shared supper table with Z and Lima seated together, joined hands discreetly visible, warm multigenerational welcome and distinct food traditions. + +## Garland + +Z and Lima raising the five-flower communal garland with Mira and neighbourhood helpers, civic celebration centred on ordinary contributors. + +## Library + +Private archive reading scene: Z and Lima copying the recovered founders’ letters side by side, tender eye contact, open door and warm lamplight. diff --git a/assets/games/two-rivers/prompts/characters.md b/assets/games/two-rivers/prompts/characters.md new file mode 100644 index 0000000..c35dd1b --- /dev/null +++ b/assets/games/two-rivers/prompts/characters.md @@ -0,0 +1,11 @@ +# Lead Character Production Prompts + +Apply `art-direction.md`. `illustration-story`, original fictional adult, full-body three-quarter side-view game cutout, 1024×1536, flat chroma-key source converted to transparent PNG/WebP, no shadow, text, watermark, anime, photorealism, or real-person resemblance. + +## Z + +Pakistani visiting scholar, age 27, warm brown skin, dark wavy hair, short neat beard, thoughtful expressive face, teal tailored long coat over cream kurta, brown leather satchel, practical archer’s bracer. Warm key light from left. Distinct Pakistani tailoring and restrained ajrak-informed geometry. + +## Lima + +Bengali princess and festival curator, age 26, warm brown skin, long dark braided hair, expressive adult face, ivory muslin sari with deep red Bengali woven border, restrained gold jewellery, curator’s working posture. Warm key light from left. diff --git a/assets/games/two-rivers/prompts/characters/supporting-cast.md b/assets/games/two-rivers/prompts/characters/supporting-cast.md new file mode 100644 index 0000000..6e7674d --- /dev/null +++ b/assets/games/two-rivers/prompts/characters/supporting-cast.md @@ -0,0 +1,27 @@ +# Supporting Cast Prompts + +All six assets use `illustration-story`, full-body game-character cutouts at 1024×1536. Apply the master art direction. Place one adult character alone on perfectly flat `#00ff00` chroma key with no shadow, floor, gradient, texture, lettering, or watermark. Keep generous padding and never use the key colour in clothing. + +## Keeper Anwara + +Fictional Bengali woman, age 58, senior manuscript keeper. Intelligent lined face, silver-streaked dark hair in a practical low bun, deep indigo sari with a restrained rust border, cream archive blouse, small spectacles on a cord, cotton gloves tucked at waist, one closed folio held carefully. Calm authority, three-quarter side view facing right. + +## Saira + +Fictional Bengali woman, age 49, Lima’s aunt and festival-council chair. Warm, formidable expression, elegant burgundy jamdani-inspired sari with ivory geometric weaving, modest gold jewellery, dignified upright posture, folded programme fan in one hand. Three-quarter side view facing left. + +## Nadia + +Fictional Bengali woman, age 34, independent old-market bookseller. Practical ochre sari over a teal blouse, ink-stained fingers, round spectacles, key ring and ledger pouch, alert amused expression, two tied letters held against her chest. Three-quarter side view facing right. + +## Rayhan + +Fictional Bengali man, age 31, cheerful festival head cook. Warm brown skin, short dark hair and moustache, cream panjabi with rolled sleeves, terracotta apron, patterned gamcha over one shoulder, wooden tasting spoon, broad welcoming expression. Three-quarter side view facing left. + +## Mira + +Fictional Bengali woman, age 29, gardener and garland artist. Leaf-green sari with a marigold border, practical wrapped sleeves, long dark braid threaded with one jasmine bloom, flower basket at hip, gentle observant expression. Three-quarter side view facing right. + +## Ishan + +Fictional Bengali man, age 45, master of ceremonies. Deep plum achkan with subtle woven pattern over ivory trousers, neatly groomed greying hair and moustache, festival programme scroll, composed expression with dry humour. Three-quarter side view facing left. diff --git a/assets/games/two-rivers/prompts/effects.md b/assets/games/two-rivers/prompts/effects.md new file mode 100644 index 0000000..88c242d --- /dev/null +++ b/assets/games/two-rivers/prompts/effects.md @@ -0,0 +1,3 @@ +# Effect Texture Prompt Specification + +Separate transparent textures for warm firefly, pale petal, rain streak, dust mote, lantern glow, water glint, leaf silhouette, and soft moon flare. Painterly edges, no lettering, no opaque background. Effects remain subtle and support reduced/minimal quality modes. diff --git a/assets/games/two-rivers/prompts/items/all-items.md b/assets/games/two-rivers/prompts/items/all-items.md new file mode 100644 index 0000000..e968f72 --- /dev/null +++ b/assets/games/two-rivers/prompts/items/all-items.md @@ -0,0 +1,22 @@ +# Item, Gift, and Clue Prompts + +Each item is a separate `illustration-story` inventory asset at 1024×1024, centred on flat `#00ff00` chroma key for removal. Painterly storybook rendering, three-quarter object view, crisp silhouette, generous padding, no shadow, lettering, watermark, hands, or extra props. + +1. `inkstone`: carved dark stone inkstone with river-wave border and a small dry brush rest. +2. `brass_compass`: aged brass star compass with indigo enamel points; no readable letters. +3. `poetry_book`: hand-bound indigo book with gold river ornament; blank spine and cover. +4. `embroidered_ribbon`: folded ivory ribbon with indigo and red geometric embroidery. +5. `spiced_tea`: small sealed tea tin with cardamom pods and dried citrus beside it; no label. +6. `archery_charm`: small brass bow-and-river charm on a red cord. +7. `folio_ribbon`: frayed indigo manuscript thread tied in a distinctive four-loop keeper’s knot. +8. `lantern_rubbing`: charcoal rubbing of four symbols—sunrise, rain drops, lantern, crescent moon—with no words. +9. `anwara_note`: folded cream archival note sealed with plain indigo thread; no visible writing. +10. `founders_record`: aged range ledger page with decorative lines and abstract marks only; no generated text. +11. `jasmine`: fresh white jasmine sprig. +12. `tuberose`: creamy tuberose stem. +13. `marigold`: vivid orange marigold cluster. +14. `rose`: deep red garden rose. +15. `bakul`: small cream bakul blossoms with glossy leaves. +16. `letter_1`: worn folded letter with ochre wax seal and cook’s ladle emblem. +17. `letter_2`: worn folded letter with green thread and garden-gate emblem. +18. `letter_3`: travel-stained folded letter with blue thread and small boat emblem. diff --git a/assets/games/two-rivers/prompts/items/anwara-note.md b/assets/games/two-rivers/prompts/items/anwara-note.md new file mode 100644 index 0000000..93b03af --- /dev/null +++ b/assets/games/two-rivers/prompts/items/anwara-note.md @@ -0,0 +1,3 @@ +# Anwara's Note + +`illustration-story`, 1:1 clue icon. Folded cream archival note tied closed with plain indigo thread, gently aged edges, centred on flat `#00ff00` for alpha removal. No visible writing, seal, hands, shadow, or watermark. diff --git a/assets/games/two-rivers/prompts/items/archery-charm.md b/assets/games/two-rivers/prompts/items/archery-charm.md new file mode 100644 index 0000000..b9e9377 --- /dev/null +++ b/assets/games/two-rivers/prompts/items/archery-charm.md @@ -0,0 +1,3 @@ +# Archer's Charm + +`illustration-story`, 1:1 inventory icon. Aged-brass recurved bow crossing a flowing-river line on deep-red cotton cord; centred on flat `#00ff00` for alpha removal. No shadow, hands, lettering, or watermark. diff --git a/assets/games/two-rivers/prompts/items/bakul.md b/assets/games/two-rivers/prompts/items/bakul.md new file mode 100644 index 0000000..b910f6b --- /dev/null +++ b/assets/games/two-rivers/prompts/items/bakul.md @@ -0,0 +1,3 @@ +# Bakul + +`illustration-story`, 1:1 botanical icon. Botanically plausible Mimusops elengi sprig with small cream star blossoms and glossy deep-green leaves, centred on flat `#ff00ff` for alpha removal. No vase, hand, text, or watermark. diff --git a/assets/games/two-rivers/prompts/items/brass-compass.md b/assets/games/two-rivers/prompts/items/brass-compass.md new file mode 100644 index 0000000..08d14cb --- /dev/null +++ b/assets/games/two-rivers/prompts/items/brass-compass.md @@ -0,0 +1,3 @@ +# Brass Star Compass + +`illustration-story`, 1:1 inventory icon. One aged brass eight-point star compass with restrained indigo enamel and simple needle; centred three-quarter view, warm upper-left light, flat `#00ff00` for alpha removal. No letters, numbers, shadow, hands, text, or watermark. diff --git a/assets/games/two-rivers/prompts/items/embroidered-ribbon.md b/assets/games/two-rivers/prompts/items/embroidered-ribbon.md new file mode 100644 index 0000000..d7da0ec --- /dev/null +++ b/assets/games/two-rivers/prompts/items/embroidered-ribbon.md @@ -0,0 +1,3 @@ +# Embroidered Ribbon + +`illustration-story`, 1:1 inventory icon. Folded ivory cotton ribbon with hand-embroidered indigo and deep-red geometric border motifs; tactile threadwork, centred on flat `#00ff00` for alpha removal. No shadow, hands, lettering, or watermark. diff --git a/assets/games/two-rivers/prompts/items/folio-ribbon.md b/assets/games/two-rivers/prompts/items/folio-ribbon.md new file mode 100644 index 0000000..1fd3d7f --- /dev/null +++ b/assets/games/two-rivers/prompts/items/folio-ribbon.md @@ -0,0 +1,3 @@ +# Folio Ribbon + +`illustration-story`, 1:1 clue icon. Frayed indigo manuscript thread tied in a distinctive four-loop keeper’s knot, tactile old cotton, centred on flat `#00ff00` for alpha removal. No paper, hands, shadow, writing, or watermark. diff --git a/assets/games/two-rivers/prompts/items/founders-record.md b/assets/games/two-rivers/prompts/items/founders-record.md new file mode 100644 index 0000000..a2966fa --- /dev/null +++ b/assets/games/two-rivers/prompts/items/founders-record.md @@ -0,0 +1,3 @@ +# Founders' Range Record + +`illustration-story`, 1:1 clue icon. Aged ruled range-ledger page with boat-and-arrow emblem and abstract ink marks only, centred on flat `#00ff00` for alpha removal. Absolutely no readable words or numbers. diff --git a/assets/games/two-rivers/prompts/items/inkstone.md b/assets/games/two-rivers/prompts/items/inkstone.md new file mode 100644 index 0000000..52a7367 --- /dev/null +++ b/assets/games/two-rivers/prompts/items/inkstone.md @@ -0,0 +1,3 @@ +# Carved Inkstone + +`illustration-story`, 1:1 inventory icon. One carved dark stone inkstone with shallow well, river-wave border, and dry brush rest; centred three-quarter view, warm upper-left light, crisp silhouette on flat `#00ff00` for alpha removal. No shadow, hands, text, watermark, or extra props. diff --git a/assets/games/two-rivers/prompts/items/jasmine.md b/assets/games/two-rivers/prompts/items/jasmine.md new file mode 100644 index 0000000..0470676 --- /dev/null +++ b/assets/games/two-rivers/prompts/items/jasmine.md @@ -0,0 +1,3 @@ +# Jasmine + +`illustration-story`, 1:1 botanical icon. Botanically plausible fresh white jasmine sprig with star-shaped blossoms and deep-green leaves, centred on flat `#ff00ff` for alpha removal. No vase, hand, shadow, text, or watermark. diff --git a/assets/games/two-rivers/prompts/items/lantern-rubbing.md b/assets/games/two-rivers/prompts/items/lantern-rubbing.md new file mode 100644 index 0000000..06e166f --- /dev/null +++ b/assets/games/two-rivers/prompts/items/lantern-rubbing.md @@ -0,0 +1,3 @@ +# Lantern Rubbing + +`illustration-story`, 1:1 clue icon. Cream charcoal rubbing with exactly four pictograms—sunrise, rain, lantern, crescent moon—on torn paper, centred on flat `#00ff00` for alpha removal. No words, hands, shadow, or watermark. diff --git a/assets/games/two-rivers/prompts/items/letter-1.md b/assets/games/two-rivers/prompts/items/letter-1.md new file mode 100644 index 0000000..cfd0bfd --- /dev/null +++ b/assets/games/two-rivers/prompts/items/letter-1.md @@ -0,0 +1,3 @@ +# Cook's Letter + +`illustration-story`, 1:1 historical-letter icon. Worn folded cream letter tied with ochre thread and ochre wax seal bearing a simple ladle emblem, centred on flat `#00ff00` for alpha removal. No writing, hands, shadow, or watermark. diff --git a/assets/games/two-rivers/prompts/items/letter-2.md b/assets/games/two-rivers/prompts/items/letter-2.md new file mode 100644 index 0000000..bde1bbd --- /dev/null +++ b/assets/games/two-rivers/prompts/items/letter-2.md @@ -0,0 +1,3 @@ +# Gardener's Letter + +`illustration-story`, 1:1 historical-letter icon. Worn folded cream letter tied with deep-green thread and terracotta seal bearing an open-gate emblem, centred on flat `#ff00ff` for alpha removal. No writing, hands, shadow, or watermark. diff --git a/assets/games/two-rivers/prompts/items/letter-3.md b/assets/games/two-rivers/prompts/items/letter-3.md new file mode 100644 index 0000000..56be339 --- /dev/null +++ b/assets/games/two-rivers/prompts/items/letter-3.md @@ -0,0 +1,3 @@ +# Traveller's Letter + +`illustration-story`, 1:1 historical-letter icon. Travel-stained folded cream letter tied with indigo thread and muted-blue seal bearing a river-boat emblem, centred on flat `#00ff00` for alpha removal. No writing, hands, shadow, or watermark. diff --git a/assets/games/two-rivers/prompts/items/marigold.md b/assets/games/two-rivers/prompts/items/marigold.md new file mode 100644 index 0000000..0f3831d --- /dev/null +++ b/assets/games/two-rivers/prompts/items/marigold.md @@ -0,0 +1,3 @@ +# Marigold + +`illustration-story`, 1:1 botanical icon. Three vivid orange, densely petalled marigold blooms with buds and deep-green leaves, centred on flat `#ff00ff` for alpha removal. No garland, vase, hand, text, or watermark. diff --git a/assets/games/two-rivers/prompts/items/poetry-book.md b/assets/games/two-rivers/prompts/items/poetry-book.md new file mode 100644 index 0000000..a421bf3 --- /dev/null +++ b/assets/games/two-rivers/prompts/items/poetry-book.md @@ -0,0 +1,3 @@ +# Book of River Poems + +`illustration-story`, 1:1 inventory icon. Small hand-bound indigo cloth book with stitched binding and gold flowing-river ornament; blank cover and spine, centred three-quarter view on flat `#00ff00` for alpha removal. No writing, shadow, hands, or watermark. diff --git a/assets/games/two-rivers/prompts/items/rose.md b/assets/games/two-rivers/prompts/items/rose.md new file mode 100644 index 0000000..36c775b --- /dev/null +++ b/assets/games/two-rivers/prompts/items/rose.md @@ -0,0 +1,3 @@ +# Garden Rose + +`illustration-story`, 1:1 botanical icon. Deep-red garden rose in full bloom with one bud, thorned stem, and deep-green leaves, centred on flat cyan chroma key for alpha removal. No bouquet, hand, shadow, text, or watermark. diff --git a/assets/games/two-rivers/prompts/items/spiced-tea.md b/assets/games/two-rivers/prompts/items/spiced-tea.md new file mode 100644 index 0000000..c20cc06 --- /dev/null +++ b/assets/games/two-rivers/prompts/items/spiced-tea.md @@ -0,0 +1,3 @@ +# Tea Blend + +`illustration-story`, 1:1 inventory icon. Sealed hammered-brass tea tin with blank indigo lid, three cardamom pods, and one curl of dried citrus; compact arrangement on flat `#00ff00` for alpha removal. No label, cup, shadow, hands, writing, or watermark. diff --git a/assets/games/two-rivers/prompts/items/tuberose.md b/assets/games/two-rivers/prompts/items/tuberose.md new file mode 100644 index 0000000..d80330b --- /dev/null +++ b/assets/games/two-rivers/prompts/items/tuberose.md @@ -0,0 +1,3 @@ +# Tuberose + +`illustration-story`, 1:1 botanical icon. Botanically plausible creamy tuberose stem with waxy blossoms, buds, and narrow green leaves, centred on flat `#ff00ff` for alpha removal. No vase, hand, shadow, text, or watermark. diff --git a/assets/games/two-rivers/prompts/portraits.md b/assets/games/two-rivers/prompts/portraits.md new file mode 100644 index 0000000..db7e39b --- /dev/null +++ b/assets/games/two-rivers/prompts/portraits.md @@ -0,0 +1,3 @@ +# Portrait and Expression Prompt Specification + +Create identity-consistent waist-up portrait variants from the approved full-body masters. Transparent background, 768×1024, same clothing and light direction. Z: warm, wry, thoughtful, flustered, determined, tender. Lima: warm, playful, regal, curious, vulnerable, tender, triumphant. Supporting cast: neutral, warm, concerned, and amused where narratively appropriate. No text or changed identity. diff --git a/assets/games/two-rivers/prompts/ui.md b/assets/games/two-rivers/prompts/ui.md new file mode 100644 index 0000000..ab98231 --- /dev/null +++ b/assets/games/two-rivers/prompts/ui.md @@ -0,0 +1,3 @@ +# UI Ornament Prompt Specification + +Twelve separate transparent painterly ornaments: paired river line, crescent-and-lantern divider, manuscript corner, indigo knot, jasmine corner, marigold corner, brass compass rose, archive seal, dance-step flourish, archery ring, cooking steam curl, and gallery frame. Gold/indigo/ivory palette, readable at small size, no words or pseudo-script. diff --git a/assets/scripts/games/two-rivers/core/content.mjs b/assets/scripts/games/two-rivers/core/content.mjs new file mode 100644 index 0000000..fca5667 --- /dev/null +++ b/assets/scripts/games/two-rivers/core/content.mjs @@ -0,0 +1,70 @@ +const ROOT = "/assets/games/two-rivers/data"; +const FILES = { + assets: "assets.json", + characters: "characters.json", + locations: "locations.json", + items: "items.json", + quests: "quests.json", + achievements: "achievements.json", + endings: "endings.json", + dialogue: ["dialogue/story.json", "dialogue/scenes.json", "dialogue/chapters.json"] +}; + +export async function loadContent(onProgress = () => {}) { + const content = {}; + const entries = Object.entries(FILES); + for (let index = 0; index < entries.length; index += 1) { + const [key, file] = entries[index]; + const files = Array.isArray(file) ? file : [file]; + const payloads = []; + for (const source of files) { + const response = await fetch(`${ROOT}/${source}`, {cache: "no-cache"}); + if (!response.ok) throw new Error(`Could not load ${source} (${response.status})`); + payloads.push(await response.json()); + } + content[key] = Array.isArray(file) ? Object.assign({}, ...payloads) : payloads[0]; + onProgress(Math.round(((index + 1) / entries.length) * 100), files.at(-1)); + } + const errors = validateContent(content); + if (errors.length) throw new Error(`Content validation failed: ${errors.join("; ")}`); + return Object.freeze(content); +} + +export function validateContent(content) { + const errors = []; + ["assets", "characters", "locations", "items", "quests", "achievements", "endings", "dialogue"].forEach((key) => { + if (!content[key] || typeof content[key] !== "object") errors.push(`missing ${key}`); + }); + if (errors.length) return errors; + Object.entries(content.locations).forEach(([id, location]) => { + if (!location.name || !Array.isArray(location.hotspots)) errors.push(`invalid location ${id}`); + if (!content.assets.images.some((asset) => asset.id === location.background)) errors.push(`unknown background ${id}/${location.background}`); + location.hotspots?.forEach((spot) => { + if (!spot.id || typeof spot.x !== "number" || spot.x < 0 || spot.x > 1) errors.push(`invalid hotspot ${id}/${spot.id || "unknown"}`); + if (spot.dialogue && !content.dialogue[spot.dialogue]) errors.push(`unknown dialogue ${spot.dialogue}`); + if (spot.goto && !content.locations[spot.goto]) errors.push(`unknown location ${spot.goto}`); + }); + }); + const effectTypes = new Set(["relationship", "flag", "item", "achievement", "unlockCG", "bias", "quest", "ending", "goto", "discoverGift"]); + Object.entries(content.dialogue).forEach(([dialogueId, dialogue]) => { + if (!dialogue.nodes?.[dialogue.start]) errors.push(`invalid start ${dialogueId}`); + Object.entries(dialogue.nodes || {}).forEach(([nodeId, node]) => { + if (!node.text || !node.speaker) errors.push(`invalid node ${dialogueId}/${nodeId}`); + if (!content.characters[node.speaker]) errors.push(`unknown speaker ${dialogueId}/${nodeId}/${node.speaker}`); + if (node.next && !dialogue.nodes[node.next]) errors.push(`unknown next ${dialogueId}/${nodeId}`); + if (node.conditions && !Array.isArray(node.conditions)) errors.push(`invalid conditions ${dialogueId}/${nodeId}`); + node.choices?.forEach((choice) => { + if (!dialogue.nodes[choice.next]) errors.push(`unknown choice next ${dialogueId}/${nodeId}`); + if (choice.conditions && !Array.isArray(choice.conditions)) errors.push(`invalid choice conditions ${dialogueId}/${nodeId}`); + choice.effects?.forEach((effect) => { + if (!effectTypes.has(effect.type)) errors.push(`invalid effect ${effect.type}`); + }); + }); + node.effects?.forEach((effect) => { + if (!effectTypes.has(effect.type)) errors.push(`invalid effect ${effect.type}`); + }); + }); + }); + if (Object.keys(content.endings).length !== 4) errors.push("exactly four endings required"); + return errors; +} diff --git a/assets/scripts/games/two-rivers/core/save.mjs b/assets/scripts/games/two-rivers/core/save.mjs new file mode 100644 index 0000000..aee61f5 --- /dev/null +++ b/assets/scripts/games/two-rivers/core/save.mjs @@ -0,0 +1,109 @@ +import {normalizeState} from "./state.mjs"; + +export const SAVE_KEY = "zxh_two_rivers_saves_v1"; +export const SETTINGS_KEY = "zxh_two_rivers_settings_v1"; +export const AGE_KEY = "zxh_two_rivers_age_gate_v1"; + +function checksum(value) { + let hash = 2166136261; + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + return (hash >>> 0).toString(16); +} + +export function createSaveService(storage = window.localStorage) { + let recovery = null; + + const parseEnvelope = (raw, source) => { + if (!raw) return null; + try { + const envelope = JSON.parse(raw); + if (!envelope || typeof envelope !== "object") throw new Error("Save data is not an object"); + if (envelope.version !== 1) throw new Error(`Unknown save version ${String(envelope.version)}`); + if (!envelope.slots || typeof envelope.slots !== "object") throw new Error("Save slots are missing"); + return envelope; + } catch (error) { + recovery = {source, message: error.message}; + return null; + } + }; + + const readEnvelope = () => { + recovery = null; + const primaryRaw = storage.getItem(SAVE_KEY); + if (!primaryRaw) { + const pending = parseEnvelope(storage.getItem(`${SAVE_KEY}:pending`), "pending"); + if (pending) { + recovery = {source: "pending", message: "Recovered an interrupted save write."}; + return pending; + } + return {version: 1, slots: {}}; + } + const primary = parseEnvelope(primaryRaw, "primary"); + if (primary) return primary; + const primaryIssue = recovery; + const backup = parseEnvelope(storage.getItem(`${SAVE_KEY}:backup`), "backup"); + if (backup) { + recovery = {source: "backup", message: `Recovered the previous valid save. ${primaryIssue?.message || ""}`.trim()}; + return backup; + } + recovery = primaryIssue || {source: "primary", message: "Save data could not be read."}; + return {version: 1, slots: {}}; + }; + + const writeEnvelope = (envelope) => { + const previous = storage.getItem(SAVE_KEY); + if (previous) storage.setItem(`${SAVE_KEY}:backup`, previous); + storage.setItem(`${SAVE_KEY}:pending`, JSON.stringify(envelope)); + storage.setItem(SAVE_KEY, JSON.stringify(envelope)); + storage.removeItem(`${SAVE_KEY}:pending`); + }; + + return { + list() { + return readEnvelope().slots; + }, + recoveryInfo() { + readEnvelope(); + return recovery ? {...recovery} : null; + }, + save(slot, state) { + const normalized = normalizeState(state); + if (!normalized) throw new Error("Refused invalid save state"); + const envelope = readEnvelope(); + const payload = JSON.stringify(normalized); + envelope.slots[slot] = { + version: 1, + timestamp: new Date().toISOString(), + checksum: checksum(payload), + location: normalized.location, + playTimeSeconds: normalized.playTimeSeconds, + state: normalized + }; + writeEnvelope(envelope); + return envelope.slots[slot]; + }, + load(slot) { + const record = readEnvelope().slots[slot]; + if (!record?.state) return null; + const normalized = normalizeState(record.state); + if (!normalized || checksum(JSON.stringify(normalized)) !== record.checksum) return null; + return normalized; + }, + hasSave() { + return Boolean(readEnvelope().slots.auto); + }, + settings(value) { + if (value) storage.setItem(SETTINGS_KEY, JSON.stringify(value)); + try { return JSON.parse(storage.getItem(SETTINGS_KEY) || "{}"); } catch (_error) { return {}; } + }, + ageConfirmed() { + return storage.getItem(AGE_KEY) === "yes"; + }, + confirmAge() { + storage.setItem(AGE_KEY, "yes"); + } + }; +} diff --git a/assets/scripts/games/two-rivers/core/state.mjs b/assets/scripts/games/two-rivers/core/state.mjs new file mode 100644 index 0000000..597255d --- /dev/null +++ b/assets/scripts/games/two-rivers/core/state.mjs @@ -0,0 +1,317 @@ +export const RELATIONSHIPS = ["trust", "respect", "comfort", "humour", "adventure", "romance", "familyApproval"]; +export const SAVE_VERSION = 1; + +const clamp = (value, min = 0, max = 100) => Math.max(min, Math.min(max, Number(value) || 0)); +const clone = (value) => JSON.parse(JSON.stringify(value)); + +export function freshState(settings = {}) { + return normalizeState({ + version: SAVE_VERSION, + location: "courtyard", + checkpoint: "courtyard", + position: 0.14, + relationships: { + trust: 24, respect: 28, comfort: 18, humour: 20, + adventure: 22, romance: 12, familyApproval: 20 + }, + inventory: [], + gifts: [], + discoveredGifts: [], + flags: {}, + choices: [], + quests: { + garland: {status: "active", count: 0, rewarded: false}, + letters: {status: "locked", count: 0, rewarded: false} + }, + puzzles: {}, + minigames: {}, + unlockedCGs: [], + achievements: [], + unlockedEndings: [], + readNodes: [], + dialogueHistory: [], + dialogue: null, + endingBias: {}, + finalChoice: null, + currentEnding: null, + minigameCheckpoint: null, + playTimeSeconds: 0, + settings: { + sound: false, + masterVolume: 0.65, + musicVolume: 0.55, + ambienceVolume: 0.35, + sfxVolume: 0.7, + textSpeed: "normal", + autoDelay: 1800, + reducedMotion: matchMediaSafe("(prefers-reduced-motion: reduce)"), + highContrast: false, + assistedMinigames: false, + effectsQuality: "full", + ...settings + } + }); +} + +function matchMediaSafe(query) { + return typeof window !== "undefined" && window.matchMedia ? window.matchMedia(query).matches : false; +} + +export function normalizeState(candidate) { + if (!candidate || typeof candidate !== "object") return null; + const base = { + version: SAVE_VERSION, + location: String(candidate.location || "courtyard"), + checkpoint: String(candidate.checkpoint || candidate.location || "courtyard"), + position: clamp(candidate.position ?? 0.14, 0.04, 0.96), + relationships: {}, + inventory: uniqueStrings(candidate.inventory, 80), + gifts: uniqueStrings(candidate.gifts, 6), + discoveredGifts: uniqueStrings(candidate.discoveredGifts, 6), + flags: cleanObject(candidate.flags), + choices: uniqueStrings(candidate.choices, 160), + quests: candidate.quests && typeof candidate.quests === "object" ? clone(candidate.quests) : {}, + puzzles: cleanObject(candidate.puzzles), + minigames: candidate.minigames && typeof candidate.minigames === "object" ? clone(candidate.minigames) : {}, + unlockedCGs: uniqueStrings(candidate.unlockedCGs, 16), + achievements: uniqueStrings(candidate.achievements, 24), + unlockedEndings: uniqueStrings(candidate.unlockedEndings, 4), + readNodes: uniqueStrings(candidate.readNodes, 400), + dialogueHistory: normalizeHistory(candidate.dialogueHistory), + dialogue: candidate.dialogue && typeof candidate.dialogue === "object" ? clone(candidate.dialogue) : null, + endingBias: candidate.endingBias && typeof candidate.endingBias === "object" ? clone(candidate.endingBias) : {}, + finalChoice: typeof candidate.finalChoice === "string" ? candidate.finalChoice : null, + currentEnding: typeof candidate.currentEnding === "string" ? candidate.currentEnding : null, + minigameCheckpoint: typeof candidate.minigameCheckpoint === "string" ? candidate.minigameCheckpoint : null, + playTimeSeconds: clamp(candidate.playTimeSeconds, 0, 999999), + settings: { + sound: Boolean(candidate.settings?.sound), + masterVolume: clamp(candidate.settings?.masterVolume ?? 0.65, 0, 1), + musicVolume: clamp(candidate.settings?.musicVolume ?? 0.55, 0, 1), + ambienceVolume: clamp(candidate.settings?.ambienceVolume ?? 0.35, 0, 1), + sfxVolume: clamp(candidate.settings?.sfxVolume ?? 0.7, 0, 1), + textSpeed: ["slow", "normal", "fast", "instant"].includes(candidate.settings?.textSpeed) ? candidate.settings.textSpeed : "normal", + autoDelay: clamp(candidate.settings?.autoDelay ?? 1800, 800, 5000), + reducedMotion: Boolean(candidate.settings?.reducedMotion), + highContrast: Boolean(candidate.settings?.highContrast), + assistedMinigames: Boolean(candidate.settings?.assistedMinigames), + effectsQuality: ["full", "reduced", "minimal"].includes(candidate.settings?.effectsQuality) ? candidate.settings.effectsQuality : "full" + } + }; + RELATIONSHIPS.forEach((key) => { base.relationships[key] = clamp(candidate.relationships?.[key] ?? 20); }); + ["garland", "letters"].forEach((id) => { + const value = base.quests[id] || {}; + base.quests[id] = { + status: ["locked", "active", "complete"].includes(value.status) ? value.status : (id === "garland" ? "active" : "locked"), + count: clamp(value.count, 0, id === "garland" ? 5 : 3), + rewarded: Boolean(value.rewarded) + }; + }); + return base; +} + +function uniqueStrings(value, cap) { + return Array.isArray(value) + ? [...new Set(value.filter((item) => typeof item === "string" && /^[a-z0-9_:-]{1,80}$/i.test(item)))].slice(0, cap) + : []; +} + +function cleanObject(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + return Object.fromEntries(Object.entries(value).filter(([key, item]) => + /^[a-z0-9_:-]{1,80}$/i.test(key) && ["boolean", "number", "string"].includes(typeof item)).slice(0, 200)); +} + +function normalizeHistory(value) { + if (!Array.isArray(value)) return []; + return value.filter((entry) => + entry && typeof entry === "object" && + typeof entry.speaker === "string" && typeof entry.text === "string") + .slice(-200) + .map((entry) => ({ + speaker: entry.speaker.slice(0, 80), + text: entry.text.slice(0, 1200) + })); +} + +export function createStore(initial) { + let state = normalizeState(initial) || freshState(); + const listeners = new Set(); + return { + get: () => clone(state), + dispatch(command) { + state = reduce(state, command); + listeners.forEach((listener) => listener(clone(state), command)); + return clone(state); + }, + replace(next) { + const normalized = normalizeState(next); + if (!normalized) throw new Error("Invalid game state"); + state = normalized; + listeners.forEach((listener) => listener(clone(state), {type: "replace"})); + }, + subscribe(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + } + }; +} + +export function reduce(current, command) { + const state = clone(current); + if (!command || typeof command.type !== "string") return normalizeState(state); + switch (command.type) { + case "relationship": + if (RELATIONSHIPS.includes(command.key)) { + state.relationships[command.key] = clamp(state.relationships[command.key] + Number(command.amount || 0)); + if (command.key === "trust" && state.relationships.trust >= 75 && !state.achievements.includes("open_book")) state.achievements.push("open_book"); + if (command.key === "familyApproval" && state.relationships.familyApproval >= 75 && !state.achievements.includes("royal_favourite")) state.achievements.push("royal_favourite"); + } + break; + case "flag": + if (/^[a-z0-9_:-]+$/i.test(command.id)) state.flags[command.id] = command.value !== false; + break; + case "goto": + state.location = command.location; + state.checkpoint = command.location; + state.position = clamp(command.position ?? 0.08, 0.04, 0.96); + state.dialogue = null; + break; + case "position": + state.position = clamp(command.value, 0.04, 0.96); + break; + case "item": + if (!state.inventory.includes(command.id)) state.inventory.push(command.id); + break; + case "choice": + if (!state.choices.includes(command.id)) state.choices.push(command.id); + break; + case "read": + if (!state.readNodes.includes(command.id)) state.readNodes.push(command.id); + break; + case "history": + state.dialogueHistory.push({speaker: String(command.speaker || "").slice(0, 80), text: String(command.text || "").slice(0, 1200)}); + state.dialogueHistory = state.dialogueHistory.slice(-200); + break; + case "dialogue": + state.dialogue = command.value ? clone(command.value) : null; + break; + case "questStart": + if (state.quests[command.id] && state.quests[command.id].status === "locked") state.quests[command.id].status = "active"; + break; + case "questProgress": + progressQuest(state, command.id, command.item, command.target, command.reward); + break; + case "puzzle": + state.puzzles[command.id] = true; + state.flags[`${command.id}_solved`] = true; + break; + case "minigameStart": + state.minigameCheckpoint = command.id; + break; + case "minigame": + { + const previous = state.minigames[command.id]; + const score = clamp(command.score, 0, 999); + state.minigames[command.id] = { + complete: true, + score: Math.max(previous?.score || 0, score), + perfect: Boolean(previous?.perfect || command.perfect), + attempts: clamp((previous?.attempts || 0) + 1, 1, 99) + }; + state.minigameCheckpoint = null; + } + break; + case "gift": + if (!state.gifts.includes(command.id)) state.gifts.push(command.id); + break; + case "discoverGift": + if (!state.discoveredGifts.includes(command.id)) state.discoveredGifts.push(command.id); + break; + case "achievement": + if (!state.achievements.includes(command.id)) state.achievements.push(command.id); + break; + case "unlockCG": + if (!state.unlockedCGs.includes(command.id)) state.unlockedCGs.push(command.id); + break; + case "bias": + state.endingBias[command.id] = clamp((state.endingBias[command.id] || 0) + Number(command.amount || 0), 0, 50); + state.finalChoice = command.id; + break; + case "ending": + state.currentEnding = command.id; + if (!state.unlockedEndings.includes(command.id)) state.unlockedEndings.push(command.id); + break; + case "setting": + if (Object.hasOwn(state.settings, command.key)) state.settings[command.key] = command.value; + break; + case "tick": + state.playTimeSeconds = clamp(state.playTimeSeconds + Number(command.seconds || 0), 0, 999999); + break; + } + return normalizeState(state); +} + +function progressQuest(state, id, item, target, reward = {}) { + const quest = state.quests[id]; + if (!quest || quest.status === "complete" || state.inventory.includes(item)) return; + state.inventory.push(item); + if (quest.status === "locked") quest.status = "active"; + quest.count = Math.min(target, quest.count + 1); + if (quest.count < target) return; + quest.status = "complete"; + if (quest.rewarded) return; + quest.rewarded = true; + Object.entries(reward).forEach(([key, amount]) => { + if (RELATIONSHIPS.includes(key)) state.relationships[key] = clamp(state.relationships[key] + Number(amount)); + }); + if (reward.cg && !state.unlockedCGs.includes(reward.cg)) state.unlockedCGs.push(reward.cg); + const achievement = id === "garland" ? "garland_for_all" : "letters_home"; + if (!state.achievements.includes(achievement)) state.achievements.push(achievement); +} + +export function applyEffects(store, effects = []) { + effects.forEach((effect) => { + if (effect.type === "relationship") store.dispatch(effect); + else if (effect.type === "flag") store.dispatch(effect); + else if (effect.type === "item") store.dispatch(effect); + else if (effect.type === "achievement") store.dispatch(effect); + else if (effect.type === "unlockCG") store.dispatch(effect); + else if (effect.type === "bias") store.dispatch(effect); + else if (effect.type === "goto") store.dispatch({type: "goto", location: effect.location, position: effect.position}); + else if (effect.type === "discoverGift") store.dispatch(effect); + else if (effect.type === "quest" && effect.action === "start") store.dispatch({type: "questStart", id: effect.id}); + }); +} + +export function relationshipStage(value) { + if (value >= 75) return "Deep"; + if (value >= 50) return "Warm"; + if (value >= 25) return "Growing"; + return "New"; +} + +export function resolveEnding(state, endings) { + const order = ["scholar", "laughter", "road", "families"]; + const scores = Object.fromEntries(order.map((id) => { + const weighted = Object.entries(endings[id].weights).reduce((sum, [key, weight]) => sum + state.relationships[key] * weight, 0); + return [id, weighted + Number(state.endingBias[id] || 0)]; + })); + return order.sort((a, b) => + scores[b] - scores[a] || + Number(Boolean(state.endingBias[b])) - Number(Boolean(state.endingBias[a])) || + state.relationships.romance - state.relationships.romance || + state.relationships.trust - state.relationships.trust || + order.indexOf(a) - order.indexOf(b))[0]; +} + +export function requirementMet(state, requirement) { + const [kind, id] = requirement.split(":"); + if (kind === "flag") return Boolean(state.flags[id]); + if (kind === "dialogue") return Boolean(state.flags[`dialogue_${id}`]); + if (kind === "minigame") return Boolean(state.minigames[id]?.complete); + if (kind === "item") return state.inventory.includes(id); + if (kind === "quest") return state.quests[id]?.status === "complete"; + if (kind === "puzzle") return Boolean(state.puzzles[id]); + return false; +} diff --git a/assets/scripts/games/two-rivers/main.js b/assets/scripts/games/two-rivers/main.js new file mode 100644 index 0000000..962d5b5 --- /dev/null +++ b/assets/scripts/games/two-rivers/main.js @@ -0,0 +1,282 @@ +import {loadContent} from "./core/content.mjs"; +import {createStore, freshState} from "./core/state.mjs"; +import {createSaveService} from "./core/save.mjs"; +import {AudioDirector} from "./systems/audio.mjs"; +import {Interface} from "./ui/interface.mjs"; +import {createScenes} from "./scenes/game-scenes.mjs"; + +const root = document.querySelector("[data-two-rivers]"); + +if (root) { + start().catch((error) => fatal(error)); +} + +async function start() { + if (!window.Phaser || !window.gsap || !window.Howl) throw new Error("A required local game library did not load."); + const saves = createSaveService(); + const loadingEl = root.querySelector("[data-tr-loading]"); + const progress = root.querySelector("[data-tr-progress]"); + const loadingCopy = root.querySelector("[data-tr-loading-copy]"); + loadingEl.hidden = false; + const content = await loadContent((value, file) => { + progress.value = value * 0.4; + loadingCopy.textContent = `Reading ${file.replace(".json", "").replaceAll("_", " ")}…`; + }); + const savedSettings = saves.settings(); + const store = createStore(freshState(savedSettings)); + const audio = new AudioDirector(content.assets.audio, () => store.get().settings); + const ui = new Interface(root, store, content, saves, audio); + let game; + let ready = false; + let paused = false; + let lastTick = performance.now(); + let recoveryShown = false; + + const controller = { + content, store, saves, audio, ui, + touchDirection: 0, + loading(value, copy) { + progress.value = 40 + value * 0.6; + loadingCopy.textContent = copy; + }, + assetError(src) { + ui.toast(`An artwork could not be loaded: ${src.split("/").pop()}`); + }, + locationLoading(active, copy = "Painting the city…") { + loadingEl.hidden = !active; + if (active) { + progress.removeAttribute("value"); + loadingCopy.textContent = copy; + } else { + progress.value = 100; + } + }, + ready() { + ready = true; + loadingEl.hidden = true; + showGateOrMenu(); + }, + uiBusy() { + return !ui.dialogueEl.hidden || !ui.overlay.hidden || paused; + }, + locationReady(location) { + paused = false; + root.classList.add("tr-exploring"); + root.querySelector("[data-tr-hud]").hidden = false; + root.querySelector("[data-tr-chapter]").textContent = location.act; + root.querySelector("[data-tr-location]").textContent = location.name; + root.querySelector("[data-tr-objective]").textContent = location.objective; + audio.playLocation(location.music, location.ambience); + controller.autosave(); + controller.interaction(null); + root.querySelector("#two-rivers-game").focus(); + checkOrientation(); + }, + interaction(label) { + const element = root.querySelector("[data-tr-interact]"); + element.hidden = !label; + if (label) element.querySelector("span").textContent = label; + }, + dialogue(id, done) { ui.startDialogue(id, done); }, + puzzle(id, done) { id === "flower_sequence" ? ui.flowerSequence(done) : ui.cipher(done); }, + gifts(done) { ui.gifts(done); }, + minigame(type, done) { + controller.pendingMiniDone = done; + store.dispatch({type: "minigameStart", id: type}); + controller.autosave(); + game.scene.pause("Story"); + game.scene.start(type[0].toUpperCase() + type.slice(1)); + }, + runMiniOverlay(type, sceneDone) { + ui.minigame(type, () => { + sceneDone(); + game.scene.resume("Story"); + controller.pendingMiniDone?.(); + controller.pendingMiniDone = null; + }); + }, + goto(location) { + store.dispatch({type: "goto", location, position: content.locations[location].spawn}); + controller.autosave(); + game.scene.stop("Story"); + game.scene.start("Story"); + }, + ending() { + game.scene.stop("Story"); + game.scene.start("Ending"); + }, + runEnding() { + ui.ending(() => { + root.classList.remove("tr-exploring"); + root.querySelector("[data-tr-hud]").hidden = true; + checkOrientation(); + game.scene.stop("Ending"); + showMenu(); + }); + }, + openJournal() { + if (!ready || ui.dialogue || !ui.overlay.hidden) return; + paused = true; + ui.onResume = () => { paused = false; ui.onResume = null; }; + ui.journal(); + }, + pause() { + if (!ready || ui.dialogue || !ui.overlay.hidden) return; + paused = true; + ui.onResume = () => { paused = false; ui.onResume = null; }; + ui.openPanel("Moonlit menu", ` +
${String(error.message || error)}
Return to playroomThe pockets are, for once, empty.
"}An original, locally hosted romantic adventure created for zainezq.com.
+Bengali and Pakistani influences are treated as distinct sources of clothing, food, architecture, and visual language inside a wholly fictional fantasy setting.
`); + } + + gallery() { + const state = this.store.get(); + const cards = ["garland", "library", "scholar", "laughter", "road", "families"].map((id) => { + const unlocked = state.unlockedCGs.includes(id) || state.unlockedEndings.includes(id); + return `
` : "${escapeHtml(info.message)}
+Your current game can still begin safely. A recovery copy was used when available; existing Princess Lima saves were not touched.
+ `, "A save needed attention", false); + this.panelBody.querySelector("[data-recovery-close]").addEventListener("click", () => { + this.overlay.hidden = true; + this.panelBody.innerHTML = ""; + }); + } + + savesPanel(onLoad) { + const slots = this.saves.list(); + const state = this.store.get(); + this.openPanel("Save slots", `${escapeHtml(entry.text)}
The indigo knot named an order. Touch the carved shelf emblems from first light to moonrise.
+${progress.length ? progress.join(" · ") : "The lantern waits."}
+ ${this.store.get().settings.assistedMinigames ? "Assisted hint: Dawn → Rain → Lantern → Moon" : ""}`, "Library puzzle", false); + this.panelBody.querySelectorAll("[data-symbol]").forEach((button) => button.addEventListener("click", () => { + const value = button.dataset.symbol; + if (value === answer[progress.length]) { + progress.push(value); + this.audio.sfx("puzzle"); + if (progress.length === answer.length) { + this.store.dispatch({type: "puzzle", id: "cipher"}); + this.store.dispatch({type: "achievement", id: "lantern_reader"}); + this.store.dispatch({type: "item", id: "lantern_rubbing"}); + this.overlay.hidden = true; + this.toast("The shelves answer with a hidden garden map."); + this.saves.save("auto", this.store.get()); + done(); + } else render(); + } else { + progress = []; + this.toast("The lanterns dim. Begin again."); + render(); + } + })); + }; + render(); + } + + flowerSequence(done) { + const answer = ["Jasmine", "Tuberose", "Marigold", "Bakul"]; + const symbols = ["Bakul", "Jasmine", "Marigold", "Tuberose"]; + let progress = []; + const render = () => { + this.openPanel("The founders' flower mosaic", ` +Four carved blooms follow the old festival day: welcome at dawn, fragrance at dusk, fire at the feast, memory at moonrise.
+${progress.length ? progress.join(" · ") : "The stone petals wait beneath your hand."}
+ ${this.store.get().settings.assistedMinigames ? "Assisted hint: Jasmine → Tuberose → Marigold → Bakul" : ""}`, + "Moon garden puzzle · untimed", false); + this.panelBody.querySelectorAll("[data-flower]").forEach((button) => button.addEventListener("click", () => { + const value = button.dataset.flower; + if (value === answer[progress.length]) { + progress.push(value); + this.audio.sfx("puzzle"); + if (progress.length === answer.length) { + this.store.dispatch({type: "puzzle", id: "flower_sequence"}); + this.store.dispatch({type: "item", id: "anwara_note"}); + this.overlay.hidden = true; + this.toast("The mosaic opens around a note and an archer's brass token."); + this.saves.save("auto", this.store.get()); + done(); + } else render(); + } else { + progress = []; + this.toast("The carved vine returns to its beginning."); + render(); + } + })); + }; + render(); + } + + gifts(done) { + const discovered = this.store.get().discoveredGifts; + const available = this.content.items.gifts.filter((gift) => discovered.includes(gift.id)); + const cards = available.map((gift) => + ``).join(""); + this.openPanel("A gift for Lima", `Choose from the ideas Z noticed in conversation. No sincere gift is punished, and only one gift is exchanged in this act.
${escapeHtml(instructions.copy)}
+${escapeHtml(instructions.input)}
+ ${this.store.get().settings.assistedMinigames ? "Assisted mode is on: cues move more slowly or reveal the next correct input.
" : ""} + `, "Practice instructions · pause safely · story progress is guaranteed", false); + this.panelBody.querySelector("[data-mini-begin]").addEventListener("click", () => { + if (type === "archery") this.archery(done); + else if (type === "cooking") this.cooking(done); + else this.dance(done); + }); + } + + archery(done) { + const firstCompletion = !this.store.get().minigames.archery?.complete; + let shots = 0; + let score = 0; + let direction = 1; + let position = 0; + const assisted = this.store.get().settings.assistedMinigames; + this.openPanel("Moonlit Archery", ` +Loose five arrows when the moving glint crosses the gold centre. Press Space or tap Loose.
+Arrows 0/5 · Score 0
+Build Rayhan's fragrant rice in the order hidden in Nadia's rhyme: foundation, gold, brightness, green.
+${index}/4 prepared · ${mistakes} ${mistakes === 1 ? "correction" : "corrections"}
+ ${this.store.get().settings.assistedMinigames ? "Assisted order: Rice, Saffron, Citrus, Pistachio." : ""}`, "Cooperative cooking", false); + this.panelBody.querySelectorAll("[data-ingredient]").forEach((button) => button.addEventListener("click", () => { + if (button.dataset.ingredient === recipe[index]) { + index += 1; + this.audio.sfx("cooking"); + if (index === recipe.length) { + this.store.dispatch({type: "minigame", id: "cooking", score: Math.max(0, 100 - mistakes * 20), perfect: mistakes === 0}); + if (firstCompletion) { + this.store.dispatch({type: "relationship", key: "comfort", amount: mistakes === 0 ? 7 : 4}); + this.store.dispatch({type: "relationship", key: "humour", amount: mistakes ? 5 : 2}); + if (!mistakes) this.store.dispatch({type: "achievement", id: "kitchen_conspiracy"}); + } + this.overlay.hidden = true; + this.toast(mistakes ? "The dish is delicious. The flour on Lima's cheek is a separate triumph." : "Rayhan declares the dish—and the teamwork—suspiciously perfect."); + this.saves.save("auto", this.store.get()); + done(); + } else render(); + } else { + mistakes += 1; + this.toast("Lima catches your wrist. “Bold. Incorrect, but bold.”"); + render(); + } + })); + this.panel.onkeydown = (event) => { + const number = Number(event.key); + if (number >= 1 && number <= 4) this.panelBody.querySelectorAll("[data-ingredient]")[number - 1]?.click(); + }; + }; + render(); + } + + dance(done) { + const firstCompletion = !this.store.get().minigames.dance?.complete; + const pattern = ["←", "↑", "→", "↓", "←", "→", "↑", "↓", "→", "←", "↓", "↑"]; + let index = 0; + let score = 0; + const assisted = this.store.get().settings.assistedMinigames; + const render = () => { + this.openPanel("Dance at moonrise", ` +Follow Lima's sequence. Grace is welcome; recovery is irresistible.
+Step ${index + 1}/${pattern.length} · Rhythm ${score}
+ ${assisted ? `Assisted: choose ${pattern[index]}` : ""}`, "Festival dance", false); + this.panelBody.querySelectorAll("[data-step]").forEach((button) => button.addEventListener("click", () => { + if (button.dataset.step === pattern[index]) score += 10; + else score += 3; + index += 1; + this.audio.sfx("dance"); + if (index >= pattern.length) { + this.store.dispatch({type: "minigame", id: "dance", score, perfect: score === 120}); + if (firstCompletion) { + this.store.dispatch({type: "relationship", key: "romance", amount: score >= 90 ? 8 : 5}); + this.store.dispatch({type: "relationship", key: "comfort", amount: score >= 90 ? 4 : 6}); + this.store.dispatch({type: "achievement", id: "two_left_feet"}); + } + this.overlay.hidden = true; + this.toast(score >= 90 ? "For one breath, the entire pavilion follows your rhythm." : "You miss a turn. Lima catches you—and does not let go."); + this.saves.save("auto", this.store.get()); + done(); + } else render(); + })); + this.panel.onkeydown = (event) => { + const arrows = {ArrowLeft: "←", ArrowUp: "↑", ArrowDown: "↓", ArrowRight: "→"}; + if (arrows[event.key]) { + event.preventDefault(); + this.panelBody.querySelector(`[data-step="${arrows[event.key]}"]`)?.click(); + } + }; + }; + render(); + } + + ending(done) { + const state = this.store.get(); + const id = resolveEnding(state, this.content.endings); + const ending = this.content.endings[id]; + this.store.dispatch({type: "ending", id}); + this.store.dispatch({type: "unlockCG", id}); + if (this.store.get().unlockedEndings.length === 4) this.store.dispatch({type: "achievement", id: "every_current"}); + this.saves.save("auto", this.store.get()); + this.audio.playLocation("ending", "water"); + this.openPanel(ending.title, ` +
+ ${escapeHtml(ending.epilogue)}
“Tomorrow?” Z asks.
“Obviously,” Lima says. “We have several footnotes left to ruin.”
n&&(t=n),t},clampY:function(t){var e=this._bounds,i=this.displayHeight,s=e.y+(i-this.height)/2,n=Math.max(s,s+e.height-i);return t {var s=i(25305),n=i(44603),r=i(23568),o=i(35154),a=i(28103);n.register("nineslice",(function(t,e){void 0===t&&(t={});var i=r(t,"key",null),n=r(t,"frame",null),h=o(t,"width",256),l=o(t,"height",256),u=o(t,"leftWidth",10),c=o(t,"rightWidth",10),d=o(t,"topHeight",0),f=o(t,"bottomHeight",0),p=new a(this.scene,0,0,i,n,h,l,u,c,d,f);return void 0!==e&&(t.add=e),s(this.scene,p,t),p}))},47521:(t,e,i)=>{var s=i(28103);i(39429).register("nineslice",(function(t,e,i,n,r,o,a,h,l,u){return this.displayList.add(new s(this.scene,t,e,i,n,r,o,a,h,l,u))}))},78023:(t,e,i)=>{var s=i(29747),n=s,r=s;n=i(52230),t.exports={renderWebGL:n,renderCanvas:r}},52230:(t,e,i)=>{var s=i(91296),n=i(70554);t.exports=function(t,e,i,r){var o=e.vertices,a=o.length;if(0!==a){i.addToRenderList(e);var h=t.pipelines.set(e.pipeline,e),l=s(e,i,r,!1).calc;t.pipelines.preBatch(e);var u=h.setGameObject(e),c=h.vertexViewF32,d=h.vertexViewU32,f=h.vertexCount*h.currentShader.vertexComponentCount-1,p=i.roundPixels,v=e.tintFill,g=i.alpha*e.alpha,m=n.getTintAppendFloatAlpha(e.tint,g),y=h.vertexAvailable(),x=-1;y{var s=i(83419),n=i(44777),r=i(37589),o=i(6113),a=i(91389),h=i(90664),l=new s({Extends:n,initialize:function(t){n.call(this,t,null,!1),this.active=!1,this.easeName="Linear",this.r=[],this.g=[],this.b=[]},getMethod:function(){return null===this.propertyValue?0:9},setMethods:function(){var t=this.propertyValue,e=t,i=this.defaultEmit,s=this.defaultUpdate;if(9===this.method){this.start=t[0],this.ease=o("Linear"),this.interpolation=a("linear"),i=this.easedValueEmit,s=this.easeValueUpdate,e=t[0],this.active=!0,this.r.length=0,this.g.length=0,this.b.length=0;for(var n=0;nn&&(t=n),t},removeBounds:function(){return this.useBounds=!1,this.dirty=!0,this._bounds.setEmpty(),this},setAngle:function(t){return void 0===t&&(t=0),this.rotation=r(t),this},setBackgroundColor:function(t){return void 0===t&&(t="rgba(0,0,0,0)"),this.backgroundColor=u(t),this.transparent=0===this.backgroundColor.alpha,this},setBounds:function(t,e,i,s,n){return void 0===n&&(n=!1),this._bounds.setTo(t,e,i,s),this.dirty=!0,this.useBounds=!0,n?this.centerToBounds():(this.scrollX=this.clampX(this.scrollX),this.scrollY=this.clampY(this.scrollY)),this},getBounds:function(t){void 0===t&&(t=new h);var e=this._bounds;return t.setTo(e.x,e.y,e.width,e.height),t},setName:function(t){return void 0===t&&(t=""),this.name=t,this},setPosition:function(t,e){return void 0===e&&(e=t),this.x=t,this.y=e,this},setRotation:function(t){return void 0===t&&(t=0),this.rotation=t,this},setRoundPixels:function(t){return this.roundPixels=t,this},setScene:function(t,e){void 0===e&&(e=!0),this.scene&&this._customViewport&&this.sceneManager.customViewports--,this.scene=t,this.isSceneCamera=e;var i=t.sys;return this.sceneManager=i.game.scene,this.scaleManager=i.scale,this.cameraManager=i.cameras,this.updateSystem(),this},setScroll:function(t,e){return void 0===e&&(e=t),this.scrollX=t,this.scrollY=e,this},setSize:function(t,e){return void 0===e&&(e=t),this.width=t,this.height=e,this},setViewport:function(t,e,i,s){return this.x=t,this.y=e,this.width=i,this.height=s,this},setZoom:function(t,e){return void 0===t&&(t=1),void 0===e&&(e=t),0===t&&(t=.001),0===e&&(e=.001),this.zoomX=t,this.zoomY=e,this},setMask:function(t,e){return void 0===e&&(e=!0),this.mask=t,this._maskCamera=e?this.cameraManager.default:this,this},clearMask:function(t){return void 0===t&&(t=!1),t&&this.mask&&this.mask.destroy(),this.mask=null,this},toJSON:function(){var t={name:this.name,x:this.x,y:this.y,width:this.width,height:this.height,zoom:this.zoom,rotation:this.rotation,roundPixels:this.roundPixels,scrollX:this.scrollX,scrollY:this.scrollY,backgroundColor:this.backgroundColor.rgba};return this.useBounds&&(t.bounds={x:this._bounds.x,y:this._bounds.y,width:this._bounds.width,height:this._bounds.height}),t},update:function(){},setIsSceneCamera:function(t){return this.isSceneCamera=t,this},updateSystem:function(){if(this.scaleManager&&this.isSceneCamera){var t=0!==this._x||0!==this._y||this.scaleManager.width!==this._width||this.scaleManager.height!==this._height,e=this.sceneManager;t&&!this._customViewport?e.customViewports++:!t&&this._customViewport&&e.customViewports--,this.dirty=!0,this._customViewport=t}},destroy:function(){this.emit(a.DESTROY,this),this.removeAllListeners(),this.matrix.destroy(),this.culledObjects=[],this._customViewport&&this.sceneManager.customViewports--,this.renderList=[],this._bounds=null,this.scene=null,this.scaleManager=null,this.sceneManager=null,this.cameraManager=null},x:{get:function(){return this._x},set:function(t){this._x=t,this.updateSystem()}},y:{get:function(){return this._y},set:function(t){this._y=t,this.updateSystem()}},width:{get:function(){return this._width},set:function(t){this._width=t,this.updateSystem()}},height:{get:function(){return this._height},set:function(t){this._height=t,this.updateSystem()}},scrollX:{get:function(){return this._scrollX},set:function(t){t!==this._scrollX&&(this._scrollX=t,this.dirty=!0)}},scrollY:{get:function(){return this._scrollY},set:function(t){t!==this._scrollY&&(this._scrollY=t,this.dirty=!0)}},zoom:{get:function(){return(this._zoomX+this._zoomY)/2},set:function(t){this._zoomX=t,this._zoomY=t,this.dirty=!0}},zoomX:{get:function(){return this._zoomX},set:function(t){this._zoomX=t,this.dirty=!0}},zoomY:{get:function(){return this._zoomY},set:function(t){this._zoomY=t,this.dirty=!0}},rotation:{get:function(){return this._rotation},set:function(t){this._rotation=t,this.dirty=!0}},centerX:{get:function(){return this.x+.5*this.width}},centerY:{get:function(){return this.y+.5*this.height}},displayWidth:{get:function(){return this.width/this.zoomX}},displayHeight:{get:function(){return this.height/this.zoomY}}});t.exports=d},38058:(t,e,i)=>{var s=i(71911),n=i(67502),r=i(45319),o=i(83419),a=i(31401),h=i(20052),l=i(19715),u=i(28915),c=i(87841),d=i(26099),f=new o({Extends:s,Mixins:[a.PostPipeline],initialize:function(t,e,i,n){s.call(this,t,e,i,n),this.initPostPipeline(),this.inputEnabled=!0,this.fadeEffect=new h.Fade(this),this.flashEffect=new h.Flash(this),this.shakeEffect=new h.Shake(this),this.panEffect=new h.Pan(this),this.rotateToEffect=new h.RotateTo(this),this.zoomEffect=new h.Zoom(this),this.lerp=new d(1,1),this.followOffset=new d,this.deadzone=null,this._follow=null},setDeadzone:function(t,e){if(void 0===t)this.deadzone=null;else{if(this.deadzone?(this.deadzone.width=t,this.deadzone.height=e):this.deadzone=new c(0,0,t,e),this._follow){var i=this.width/2,s=this.height/2,r=this._follow.x-this.followOffset.x,o=this._follow.y-this.followOffset.y;this.midPoint.set(r,o),this.scrollX=r-i,this.scrollY=o-s}n(this.deadzone,this.midPoint.x,this.midPoint.y)}return this},fadeIn:function(t,e,i,s,n,r){return this.fadeEffect.start(!1,t,e,i,s,!0,n,r)},fadeOut:function(t,e,i,s,n,r){return this.fadeEffect.start(!0,t,e,i,s,!0,n,r)},fadeFrom:function(t,e,i,s,n,r,o){return this.fadeEffect.start(!1,t,e,i,s,n,r,o)},fade:function(t,e,i,s,n,r,o){return this.fadeEffect.start(!0,t,e,i,s,n,r,o)},flash:function(t,e,i,s,n,r,o){return this.flashEffect.start(t,e,i,s,n,r,o)},shake:function(t,e,i,s,n){return this.shakeEffect.start(t,e,i,s,n)},pan:function(t,e,i,s,n,r,o){return this.panEffect.start(t,e,i,s,n,r,o)},rotateTo:function(t,e,i,s,n,r,o){return this.rotateToEffect.start(t,e,i,s,n,r,o)},zoomTo:function(t,e,i,s,n,r){return this.zoomEffect.start(t,e,i,s,n,r)},preRender:function(){this.renderList.length=0;var t=this.width,e=this.height,i=.5*t,s=.5*e,r=this.zoomX,o=this.zoomY,a=this.matrix;this.renderRoundPixels=this.roundPixels&&Number.isInteger(r)&&Number.isInteger(o);var h=t*this.originX,c=e*this.originY,d=this._follow,f=this.deadzone,p=this.scrollX,v=this.scrollY;f&&n(f,this.midPoint.x,this.midPoint.y);var g=!1;if(d&&!this.panEffect.isRunning){var m=this.lerp,y=d.x-this.followOffset.x,x=d.y-this.followOffset.y;f?(y=this.nextFpsUpdate&&this.updateFPS(t),this.framesThisSecond++,this.delta>=this._limitRate&&(this.callback(t,this.delta),this.delta=0),this.lastTime=t,this.frame++},step:function(t){this.now=t;var e=Math.max(0,t-this.lastTime);this.rawDelta=e,this.time+=this.rawDelta,this.smoothStep&&(e=this.smoothDelta(e)),this.delta=e,t>=this.nextFpsUpdate&&this.updateFPS(t),this.framesThisSecond++,this.callback(t,e),this.lastTime=t,this.frame++},tick:function(){var t=window.performance.now();this.hasFpsLimit?this.stepLimitFPS(t):this.step(t)},sleep:function(){this.running&&(this.raf.stop(),this.running=!1)},wake:function(t){void 0===t&&(t=!1);var e=window.performance.now();if(!this.running){t&&(this.startTime+=-this.lastTime+(this.lastTime+e));var i=this.hasFpsLimit?this.stepLimitFPS.bind(this):this.step.bind(this);this.raf.start(i,this.forceSetTimeOut,this._target),this.running=!0,this.nextFpsUpdate=e+1e3,this.framesThisSecond=0,this.fpsLimitTriggered=!1,this.tick()}},getDuration:function(){return Math.round(this.lastTime-this.startTime)/1e3},getDurationMS:function(){return Math.round(this.lastTime-this.startTime)},stop:function(){return this.running=!1,this.started=!1,this.raf.stop(),this},destroy:function(){this.stop(),this.raf.destroy(),this.raf=null,this.game=null,this.callback=null}});t.exports=a},51085:(t,e,i)=>{var s=i(8443);t.exports=function(t){var e,i=t.events;if(void 0!==document.hidden)e="visibilitychange";else{["webkit","moz","ms"].forEach((function(t){void 0!==document[t+"Hidden"]&&(document.hidden=function(){return document[t+"Hidden"]},e=t+"visibilitychange")}))}e&&document.addEventListener(e,(function(t){document.hidden||"pause"===t.type?i.emit(s.HIDDEN):i.emit(s.VISIBLE)}),!1),window.onblur=function(){i.emit(s.BLUR)},window.onfocus=function(){i.emit(s.FOCUS)},window.focus&&t.config.autoFocus&&window.focus()}},97217:t=>{t.exports="blur"},47548:t=>{t.exports="boot"},19814:t=>{t.exports="contextlost"},68446:t=>{t.exports="destroy"},41700:t=>{t.exports="focus"},25432:t=>{t.exports="hidden"},65942:t=>{t.exports="pause"},59211:t=>{t.exports="postrender"},47789:t=>{t.exports="poststep"},39066:t=>{t.exports="prerender"},460:t=>{t.exports="prestep"},16175:t=>{t.exports="ready"},42331:t=>{t.exports="resume"},11966:t=>{t.exports="step"},32969:t=>{t.exports="systemready"},94830:t=>{t.exports="visible"},8443:(t,e,i)=>{t.exports={BLUR:i(97217),BOOT:i(47548),CONTEXT_LOST:i(19814),DESTROY:i(68446),FOCUS:i(41700),HIDDEN:i(25432),PAUSE:i(65942),POST_RENDER:i(59211),POST_STEP:i(47789),PRE_RENDER:i(39066),PRE_STEP:i(460),READY:i(16175),RESUME:i(42331),STEP:i(11966),SYSTEM_READY:i(32969),VISIBLE:i(94830)}},42857:(t,e,i)=>{t.exports={Config:i(69547),CreateRenderer:i(86054),DebugHeader:i(96391),Events:i(8443),TimeStep:i(65898),VisibilityHandler:i(51085)}},99584:(t,e,i)=>{var s=i(5290),n=i(27919),r=i(35154);t.exports=function(t){var e=r(t,"data",[]),i=r(t,"canvas",null),o=r(t,"palette",s),a=r(t,"pixelWidth",1),h=r(t,"pixelHeight",a),l=r(t,"resizeCanvas",!0),u=r(t,"clearCanvas",!0),c=r(t,"preRender",null),d=r(t,"postRender",null),f=Math.floor(Math.abs(e[0].length*a)),p=Math.floor(Math.abs(e.length*h));i||(i=n.create2D(this,f,p),l=!1,u=!1),l&&(i.width=f,i.height=p);var v=i.getContext("2d",{willReadFrequently:!0});u&&v.clearRect(0,0,f,p),c&&c(i,v);for(var g=0;g{t.exports=function(t,e,i,s,n,r,o,a,h,l,u,c,d,f,p){var v=i.x-e.displayOriginX+n,g=i.y-e.displayOriginY+r,m=v+i.w,y=g+i.h,x=o.a,T=o.b,w=o.c,b=o.d,S=o.e,E=o.f,A=v*x+g*w+S,C=v*T+g*b+E,_=v*x+y*w+S,M=v*T+y*b+E,P=m*x+y*w+S,R=m*T+y*b+E,L=m*x+g*w+S,O=m*T+g*b+E;a&&(A=Math.round(A),C=Math.round(C),_=Math.round(_),M=Math.round(M),P=Math.round(P),R=Math.round(R),L=Math.round(L),O=Math.round(O)),t.batchQuad(e,A,C,_,M,P,R,L,O,s.u0,s.v0,s.u1,s.v1,h,l,u,c,d,f,p)}},53048:t=>{t.exports=function(t,e,i,s){if(void 0===i&&(i=!1),void 0===s)return s={local:{x:0,y:0,width:0,height:0},global:{x:0,y:0,width:0,height:0},lines:{shortest:0,longest:0,lengths:null,height:0},wrappedText:"",words:[],characters:[],scaleX:0,scaleY:0};var n,r,o,a=t.text,h=a.length,l=t.maxWidth,u=t.wordWrapCharCode,c=Number.MAX_VALUE,d=Number.MAX_VALUE,f=0,p=0,v=t.fontData.chars,g=t.fontData.lineHeight,m=t.letterSpacing,y=t.lineSpacing,x=0,T=0,w=0,b=null,S=t._align,E=0,A=0,C=t.fontSize/t.fontData.size,_=C*t.scaleX,M=C*t.scaleY,P=null,R=0,L=[],O=Number.MAX_VALUE,F=0,D=0,I=0,k=[],B=[],N=null,U=function(t,e){for(var i=0,s=0;sF&&(F=I),I
-1&&this.fillStyle(this.defaultFillColor,this.defaultFillAlpha),this.defaultStrokeColor>-1&&this.lineStyle(this.defaultStrokeWidth,this.defaultStrokeColor,this.defaultStrokeAlpha),this},generateTexture:function(t,e,i){var s,n,r=this.scene.sys,o=r.game.renderer;void 0===e&&(e=r.scale.width),void 0===i&&(i=r.scale.height),f.TargetCamera.setScene(this.scene),f.TargetCamera.setViewport(0,0,e,i),f.TargetCamera.scrollX=this.x,f.TargetCamera.scrollY=this.y;var a={willReadFrequently:!0};if("string"==typeof t)if(r.textures.exists(t)){var h=(s=r.textures.get(t)).getSourceImage();h instanceof HTMLCanvasElement&&(n=h.getContext("2d",a))}else n=(s=r.textures.createCanvas(t,e,i)).getSourceImage().getContext("2d",a);else t instanceof HTMLCanvasElement&&(n=t.getContext("2d",a));return n&&(this.renderCanvas(o,this,f.TargetCamera,null,n,!1),s&&s.refresh()),this},preDestroy:function(){this.commandBuffer=[]}});f.TargetCamera=new s,t.exports=f},32768:(t,e,i)=>{var s=i(85592),n=i(20926);t.exports=function(t,e,i,r,o,a){var h=e.commandBuffer,l=h.length,u=o||t.currentContext;if(0!==l&&n(t,u,e,i,r)){i.addToRenderList(e);var c=1,d=1,f=0,p=0,v=1,g=0,m=0,y=0;u.beginPath();for(var x=0;x0&&this.fastForward(t),this.emitting=!0,this.resetCounters(this.frequency,!0),void 0!==e&&(this.duration=Math.abs(e)),this.emit(c.START,this)),this},stop:function(t){return void 0===t&&(t=!1),this.emitting&&(this.emitting=!1,t&&this.killAll(),this.emit(c.STOP,this)),this},pause:function(){return this.active=!1,this},resume:function(){return this.active=!0,this},setSortProperty:function(t,e){return void 0===t&&(t=""),void 0===e&&(e=this.true),this.sortProperty=t,this.sortOrderAsc=e,this.sortCallback=this.depthSortCallback,this},setSortCallback:function(t){return t=""!==this.sortProperty?this.depthSortCallback:null,this.sortCallback=t,this},depthSort:function(){return R(this.alive,this.sortCallback.bind(this)),this},depthSortCallback:function(t,e){var i=this.sortProperty;return this.sortOrderAsc?t[i]-e[i]:e[i]-t[i]},flow:function(t,e,i){return void 0===e&&(e=1),this.emitting=!1,this.frequency=t,this.quantity=e,void 0!==i&&(this.stopAfter=i),this.start()},explode:function(t,e,i){this.frequency=-1,this.resetCounters(-1,!0);var s=this.emitParticle(t,e,i);return this.emit(c.EXPLODE,this,s),s},emitParticleAt:function(t,e,i){return this.emitParticle(i,t,e)},emitParticle:function(t,e,i){if(!this.atLimit()){void 0===t&&(t=this.ops.quantity.onEmit());for(var s=this.dead,n=this.stopAfter,r=this.follow?this.follow.x+this.followOffset.x:e,o=this.follow?this.follow.y+this.followOffset.y:i,a=0;a0){var u=0;for(this.skipping=!0;u{var s=i(8249);t.exports=function(t,e){var i=s(t);return i>s(e)?t.setSize(e.height*i,e.height):t.setSize(e.width,e.width/i),t.setPosition(e.centerX-t.width/2,e.centerY-t.height/2)}},80774:t=>{t.exports=function(t){return t.x=Math.floor(t.x),t.y=Math.floor(t.y),t}},83859:t=>{t.exports=function(t){return t.x=Math.floor(t.x),t.y=Math.floor(t.y),t.width=Math.floor(t.width),t.height=Math.floor(t.height),t}},19217:(t,e,i)=>{var s=i(87841),n=i(36383);t.exports=function(t,e){if(void 0===e&&(e=new s),0===t.length)return e;for(var i,r,o,a=Number.MAX_VALUE,h=Number.MAX_VALUE,l=n.MIN_SAFE_INTEGER,u=n.MIN_SAFE_INTEGER,c=0;c