From c2a5074337b176e712338c16062529aed76779b3 Mon Sep 17 00:00:00 2001 From: iisaev Date: Tue, 27 Oct 2026 01:07:34 +0100 Subject: [PATCH] Add woodland adventure, crafting quests and verified loot recovery --- WOODLAND.md | 43 ++ items/rpg_plus/amber.png | Bin 0 -> 6032 bytes items/rpg_plus/resin.png | Bin 0 -> 5443 bytes items/rpg_plus/salve.png | Bin 0 -> 8261 bytes items/rpg_plus/ward_lantern.png | Bin 0 -> 6545 bytes items/rpg_plus/wolf_pelt.png | Bin 0 -> 8747 bytes items/woodland.json | 71 +++ locale/en.properties | 33 ++ locale/ru.properties | 33 ++ locale/scenarios_en.properties | 55 +++ locale/scenarios_ru.properties | 55 +++ manifest.json | 12 +- map_events/rpg_elder_house/woodland.json | 22 + map_events/rpg_forest/woodland.json | 68 +++ map_events/rpg_village/woodland.json | 63 +++ map_events/rpg_woodland_shrine/events.json | 45 ++ maps/rpg_woodland_shrine.json | 94 ++++ patches/character.json | 49 +- patches/shop.json | 61 ++- quests/woodland.json | 32 ++ recipes/woodland.json | 78 +++ scenarios/rpg_plus/woodland/beacon.scenario | 37 ++ scenarios/rpg_plus/woodland/courier.scenario | 11 + scenarios/rpg_plus/woodland/elder.scenario | 46 ++ scenarios/rpg_plus/woodland/hearth.scenario | 3 + .../rpg_plus/woodland/herbalist.scenario | 48 ++ scenarios/rpg_plus/woodland/journal.scenario | 7 + .../rpg_plus/woodland/lamplighter.scenario | 10 + scenarios/rpg_plus/woodland/shrine.scenario | 12 + scenarios/rpg_plus/woodland/waystone.scenario | 2 + .../rpg_plus/woodland/workbench.scenario | 3 + scenes/rpg_woodland_shrine.json | 5 + stations/woodland.json | 14 + test_suites/woodland/manifest.json | 6 + .../woodland/src/WoodlandTestDriver.cs | 446 ++++++++++++++++++ .../woodland/src/WoodlandTestInitializer.cs | 29 ++ 36 files changed, 1483 insertions(+), 10 deletions(-) create mode 100644 WOODLAND.md create mode 100644 items/rpg_plus/amber.png create mode 100644 items/rpg_plus/resin.png create mode 100644 items/rpg_plus/salve.png create mode 100644 items/rpg_plus/ward_lantern.png create mode 100644 items/rpg_plus/wolf_pelt.png create mode 100644 items/woodland.json create mode 100644 map_events/rpg_elder_house/woodland.json create mode 100644 map_events/rpg_forest/woodland.json create mode 100644 map_events/rpg_village/woodland.json create mode 100644 map_events/rpg_woodland_shrine/events.json create mode 100644 maps/rpg_woodland_shrine.json create mode 100644 quests/woodland.json create mode 100644 recipes/woodland.json create mode 100644 scenarios/rpg_plus/woodland/beacon.scenario create mode 100644 scenarios/rpg_plus/woodland/courier.scenario create mode 100644 scenarios/rpg_plus/woodland/elder.scenario create mode 100644 scenarios/rpg_plus/woodland/hearth.scenario create mode 100644 scenarios/rpg_plus/woodland/herbalist.scenario create mode 100644 scenarios/rpg_plus/woodland/journal.scenario create mode 100644 scenarios/rpg_plus/woodland/lamplighter.scenario create mode 100644 scenarios/rpg_plus/woodland/shrine.scenario create mode 100644 scenarios/rpg_plus/woodland/waystone.scenario create mode 100644 scenarios/rpg_plus/woodland/workbench.scenario create mode 100644 scenes/rpg_woodland_shrine.json create mode 100644 stations/woodland.json create mode 100644 test_suites/woodland/manifest.json create mode 100644 test_suites/woodland/src/WoodlandTestDriver.cs create mode 100644 test_suites/woodland/src/WoodlandTestInitializer.cs diff --git a/WOODLAND.md b/WOODLAND.md new file mode 100644 index 0000000..52597a8 --- /dev/null +++ b/WOODLAND.md @@ -0,0 +1,43 @@ +# The woodland adventure: an authoring example + +Start the `rpg_plus` collection. Choose any class in the prologue. WASD/arrows move, Shift runs, E/F interacts with the marked cell. The journal records concrete next steps. + +## Play through + +- Visit the elder in the western house. Ask about the missing light or take the wolf job directly. +- Gather branches and mushrooms in the forest; slimes drop living resin and coins. The merchant also sells these materials. +- Defeat the wolf at the eastern cave. It always drops the amber heart and enough fur for a mantle. Amber and the ward lantern cannot be dropped. +- Saves affected by missing victory loot can recover the amber at the wolf's cell by the cave (E/F). The pickup only appears after the wolf is beaten, while neither amber nor a lantern is held and the ward is unfinished; ordinary victories cannot duplicate it. +- At the well workbench, make salve (mushroom + resin), a warden mantle (2 fur + 2 resin), or the ward lantern (amber + 2 resin + 2 branches). Equip the mantle from the bag: it changes real combat stats. +- The herbalist by the western fence needs salve. Helping her leads to a missing courier in the southern forest; his note reveals what the forest wants. Return to her for payment and more resin. +- Return to the elder after the wolf for the lantern objective. Enter the cave behind the wolf and bring the lantern to the shrine. Return the light to the forest or negotiate a pact for the village. Both paths finish the adventure; the elder's final dialog reflects the choice. +- Collect the elder's final payment. Repeated visits cannot farm rewards. Rest at the hearth by the well to recover health and mana. + +## Change it without C# + +| What | Where | +| --- | --- | +| Dialogs, choices, rewards | `scenarios/rpg_plus/woodland/*.scenario` | +| NPCs, workbench, cave door, changing beacon | `map_events//*.json` | +| Journal stages | `quests/woodland.json` + `locale/*.properties` | +| Guaranteed and random monster loot | `patches/character.json` | +| Materials, consumables, crafted armor | `items/woodland.json` | +| Recipes and the station's menu | `recipes/woodland.json`, `stations/woodland.json` | +| Merchant's stock and buyback | `patches/shop.json` | +| Shrine ground and collision | `maps/rpg_woodland_shrine.json` | + +The RPG map editor paints ground, collision and events. It now exposes map atmosphere and event move routes. The Data Editor exposes recipes and crafting stations through the crafting mod's schemas; its ordinary item form exposes gear traits through equipment's schemas. + +`when "item('rpg_plus/salve') >= 1" -> :offer` checks the actual bag, including after load. `action item.remove id=rpg_plus/salve` consumes a delivery only after that check. Reward flags make side quests and the final payment repeat-safe. Journal stages are ordinary `quest.set_data_status` actions. + +Event ids shared with `rpg_demo_game` replace its elder events in load order, while new ids add the herbalist, workbench and shrine door. This mod declares the owner's dependency. The demo still supplies the village, house and forest; village_pack supplies reusable art. All production content in this mod remains data and scenarios. Test C# is inert under `test_suites/` unless requested. + +The `crafting` mod is reusable across genres. Woodland quests stay here; tile lighting, shadows, camera movement, interaction markers and atmosphere stay in `rpg`. The novel's art is unaffected. + +## Rebuild art / verify + +`comfy/rpg_life/make_jobs.py` queues six local ComfyUI renders through the workspace watcher. `comfy/rpg_life/build_woodland.py --install` builds the atlas, icons, feathered path edges and maps using Pillow/numpy on the host. Original build inputs are cached in `gen/rpg_life_source`, so reruns are deterministic. Generated assets are installed in the actual mods, not referenced from `gen/`. + +Trees are placed as complete 3×3 stamps, without overlapping another tree, a building or an event. Only visible trunks retain interior tree collision; the shrine's stone piers and altar base have explicit collision. The workbench has its own atlas entry (39), separate from barrels (31). These rules live in the generator so an art rebuild preserves the fixes. + +Tests: `woodland` / `woodland_pact` (both endings, side quests, real FF attack-menu lethal hits and victory loot, old-save amber recovery, complete tree stamps, visible collision, UI crafting, gear and save/load), existing `rpg_walk`, `rpg_plus`, `trade`, `rpg_editor_edit`, `mod_units`, `resource_atlas_rpg_plus` and `locale_atlas`. `--shots=` also captures the forest and both monster loot screens. diff --git a/items/rpg_plus/amber.png b/items/rpg_plus/amber.png new file mode 100644 index 0000000000000000000000000000000000000000..8f856fa5fd949c86eacf13e3b82b6ca56fbd3c85 GIT binary patch literal 6032 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 literal 0 HcmV?d00001 diff --git a/items/rpg_plus/salve.png b/items/rpg_plus/salve.png new file mode 100644 index 0000000000000000000000000000000000000000..fcf35e9140bf97b07683936f35a0bc3620f78dc3 GIT binary patch literal 8261 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 z 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 literal 0 HcmV?d00001 diff --git a/items/woodland.json b/items/woodland.json new file mode 100644 index 0000000..877dfb1 --- /dev/null +++ b/items/woodland.json @@ -0,0 +1,71 @@ +{ + "Kind": "item", + "Items": [ + { + "Id": "rpg_plus/resin", + "Name": "rpg_plus/item/resin/name", + "Description": "rpg_plus/item/resin/desc", + "Cost": 4, + "Sprite": "rpg_plus/resin", + "ItemType": "Resource", + "ItemAction": "null" + }, + { + "Id": "rpg_plus/wolf_pelt", + "Name": "rpg_plus/item/wolf_pelt/name", + "Description": "rpg_plus/item/wolf_pelt/desc", + "Cost": 4, + "Sprite": "rpg_plus/wolf_pelt", + "ItemType": "Resource", + "ItemAction": "null" + }, + { + "Id": "rpg_plus/amber", + "Name": "rpg_plus/item/amber/name", + "Description": "rpg_plus/item/amber/desc", + "Cost": 18, + "Sprite": "rpg_plus/amber", + "ItemType": "Quest,CantDrop", + "ItemAction": "null" + }, + { + "Id": "rpg_plus/salve", + "Name": "rpg_plus/item/salve/name", + "Description": "rpg_plus/item/salve/desc", + "Cost": 8, + "Sprite": "rpg_plus/salve", + "ItemType": "TargetUsed", + "ItemAction": "action/heal", + "heal": 18 + }, + { + "Id": "rpg_plus/ward_lantern", + "Name": "rpg_plus/item/ward_lantern/name", + "Description": "rpg_plus/item/ward_lantern/desc", + "Cost": 0, + "Sprite": "rpg_plus/ward_lantern", + "ItemType": "Quest,CantDrop", + "ItemAction": "null" + }, + { + "Id": "rpg_plus/fur_mantle", + "Name": "rpg_plus/item/fur_mantle/name", + "Description": "rpg_plus/item/fur_mantle/desc", + "Cost": 22, + "Sprite": "classic/leather_armor", + "ItemType": "TargetUsed", + "ItemAction": "action/equip", + "Equip": "armor", + "Traits": [ + { + "Stat": "def", + "Add": 3 + }, + { + "Stat": "vit", + "Add": 1 + } + ] + } + ] +} diff --git a/locale/en.properties b/locale/en.properties index b67537a..533986c 100644 --- a/locale/en.properties +++ b/locale/en.properties @@ -1,2 +1,35 @@ mod_rpg_plus_game_name = RPG Plus mod_rpg_plus_game_desc = The story of the RPG Plus sample, built on top of the RPG demo rather than beside it: a novel-style prologue at a crossroads with a class choice and a first charisma check, then the whole rpg_demo village and forest without a single edit — a trainer and a fortune teller are added by this mod's own map events file, a training yard scene, gear at the merchant's (a shop patch), a wolf with stats (a character patch). Only data and scenarios. + +rpg_plus/item/resin/name = Living resin +rpg_plus/item/resin/desc = The slime left this instead of a will. Burns even in the rain. +rpg_plus/item/wolf_pelt/name = Silver wolf fur +rpg_plus/item/wolf_pelt/desc = The wolf sheds posthumously. Warm and tough. +rpg_plus/item/amber/name = Amber heart +rpg_plus/item/amber/desc = Something inside is beating. Resin should not do that. A lantern component. +rpg_plus/item/salve/name = Woodland salve +rpg_plus/item/salve/desc = Restores 18 health. Smells like soup. Is not soup. +rpg_plus/item/ward_lantern/name = Ward lantern +rpg_plus/item/ward_lantern/desc = Amber in a resin cradle. Take it to the woodland shrine. +rpg_plus/item/fur_mantle/name = Warden mantle +rpg_plus/item/fur_mantle/desc = Fur and resin: +3 defence, +1 vitality. Any class can wear it. +rpg_plus/quest/ward/name = A light on the ledger +rpg_plus/quest/ward/0 = Speak to the elder in the western house. Find out why the forest is closing in. +rpg_plus/quest/ward/1 = Defeat the wolf by the cave in the eastern forest. Find the amber heart. +rpg_plus/quest/ward/2 = Craft a ward lantern at the well workbench: amber, 2 resin, 2 branch bundles. Slimes drop resin; the merchant also sells it. Then enter the cave beyond the wolf to reach the shrine. +rpg_plus/quest/ward/3 = Enter the cave beyond the wolf to reach the shrine. Decide whom the new light will serve. +rpg_plus/quest/ward/4 = Return to the elder for payment. The forest already knows your answer. +rpg_plus/quest/apothecary/name = Soup for external use +rpg_plus/quest/apothecary/0 = The herbalist by the western fence needs medicine for burned hands. Speak to her. +rpg_plus/quest/apothecary/1 = Craft woodland salve: mushroom + living resin. Use the well workbench, then deliver it to the herbalist. +rpg_plus/quest/courier/name = The last delivery +rpg_plus/quest/courier/0 = Find the missing courier in the southern Greywood. The herbalist gave him a parcel. +rpg_plus/quest/courier/1 = Bring the courier’s note to the herbalist by the western fence. +rpg_plus/recipe/salve/name = Woodland salve +rpg_plus/recipe/salve/desc = Mushroom and resin. Works better outside than inside. +rpg_plus/recipe/mantle/name = Warden mantle +rpg_plus/recipe/mantle/desc = Silver fur cured with resin. Warmth and protection. +rpg_plus/recipe/lantern/name = Ward lantern +rpg_plus/recipe/lantern/desc = Amber, two resin and two branch bundles. Light needs a frame. +rpg_plus/station/name = The well workbench +rpg_plus/map/shrine = The forest remembers diff --git a/locale/ru.properties b/locale/ru.properties index d91d827..1e13a57 100644 --- a/locale/ru.properties +++ b/locale/ru.properties @@ -1,2 +1,35 @@ mod_rpg_plus_game_name = RPG Plus mod_rpg_plus_game_desc = Сюжет образца RPG Plus, построенный поверх RPG-демо, а не рядом с ним: пролог-новелла на перекрёстке с выбором класса и первой проверкой харизмы, затем вся деревня и лес rpg_demo без единой правки — наставник и гадалка добавлены собственным файлом событий карты, сцена тренировочной площадки, снаряжение у торговца (патч лавки), волк со статами (патч персонажа). Только данные и сценарии. + +rpg_plus/item/resin/name = Живая смола +rpg_plus/item/resin/desc = Слизень оставил её вместо завещания. Горит даже под дождём. +rpg_plus/item/wolf_pelt/name = Серебристая шерсть +rpg_plus/item/wolf_pelt/desc = Волк линяет посмертно. Тёплая и прочная. +rpg_plus/item/amber/name = Сердце янтаря +rpg_plus/item/amber/desc = Внутри бьётся что-то, чего в смоле быть не должно. Нужно для фонаря. +rpg_plus/item/salve/name = Лесная мазь +rpg_plus/item/salve/desc = Восстанавливает 18 здоровья. Пахнет супом. Не суп. +rpg_plus/item/ward_lantern/name = Сторожевой фонарь +rpg_plus/item/ward_lantern/desc = Янтарь в смоляном гнезде. Отнеси к лесному святилищу. +rpg_plus/item/fur_mantle/name = Плащ лесного стража +rpg_plus/item/fur_mantle/desc = Шерсть и смола: +3 защиты, +1 выносливости. Подходит любому классу. +rpg_plus/quest/ward/name = Свет по ведомости +rpg_plus/quest/ward/0 = Поговори со старостой в западном доме. Узнай, почему лес подходит к деревне. +rpg_plus/quest/ward/1 = Прогони волка у пещеры на востоке леса. Найди янтарное сердце. +rpg_plus/quest/ward/2 = На верстаке у колодца изготовь сторожевой фонарь: янтарь, 2 смолы, 2 связки веток. Смола выпадает из слизней и продаётся у торговца. Затем через пещеру за волком пройди к святилищу. +rpg_plus/quest/ward/3 = Пройди через пещеру за волком к святилищу. Реши, кому будет служить новый свет. +rpg_plus/quest/ward/4 = Вернись к старосте за расчётом. Лес уже знает твой ответ. +rpg_plus/quest/apothecary/name = Суп наружного применения +rpg_plus/quest/apothecary/0 = Травница у западной ограды ищет лекарство для обожжённых рук. Поговори с ней. +rpg_plus/quest/apothecary/1 = Изготовь лесную мазь: гриб + живая смола. Верстак у колодца. Отдай мазь травнице. +rpg_plus/quest/courier/name = Последняя доставка +rpg_plus/quest/courier/0 = Найди пропавшего курьера в южной части Серого леса. Травница дала ему посылку. +rpg_plus/quest/courier/1 = Покажи найденную записку травнице у западной ограды. +rpg_plus/recipe/salve/name = Лесная мазь +rpg_plus/recipe/salve/desc = Гриб, смола. Снаружи помогает лучше, чем внутри. +rpg_plus/recipe/mantle/name = Плащ лесного стража +rpg_plus/recipe/mantle/desc = Серебристая шерсть, пропитанная смолой. Согревает и держит удар. +rpg_plus/recipe/lantern/name = Сторожевой фонарь +rpg_plus/recipe/lantern/desc = Янтарное сердце, две смолы, две связки веток. Свету нужен каркас. +rpg_plus/station/name = Верстак у колодца +rpg_plus/map/shrine = Лес помнит diff --git a/locale/scenarios_en.properties b/locale/scenarios_en.properties index 28b8d9c..4fc0be3 100644 --- a/locale/scenarios_en.properties +++ b/locale/scenarios_en.properties @@ -40,3 +40,58 @@ rpg_plus/chest/closed = The chest is not locked. Its owner is either trusting or rpg_plus/chest/coins = Ten coins. The owner was trusting. Was. rpg_plus/chest/empty = Empty. The trust ran out with the coins. rpg_plus/slime/pet = A tame slime. Someone feeds it by hand. The hands are still whole. + +rpg_plus/woodland/elder/1 = “The wolf kills goats. The forest puts out lamps. The ledger requires one culprit. The wolf is already listed.” +rpg_plus/woodland/elder/2 = Can a wolf put out lamps? +rpg_plus/woodland/elder/3 = Where do I find it? +rpg_plus/woodland/elder/4 = “No. But writing ‘unknown’ requires three more forms.” +rpg_plus/woodland/elder/5 = The elder lays a broken lantern on the table. Its empty cradle is the size of a fist. +rpg_plus/woodland/elder/6 = “Amber used to burn in the shrine. Then we decided the village needed the light more. The forest disagreed.” +rpg_plus/woodland/elder/7 = “East, past the pond. The wolf guards the cave. Drive it off. If you find amber, do not sell it. We have profited from it once already.” +rpg_plus/woodland/elder/8 = “You have the amber? Then the forest no longer trusts even wolves.” +rpg_plus/woodland/elder/9 = “There is a workbench by the well. Amber, two resin and two branch bundles make a ward lantern. Slimes leave resin; the merchant stocks it too.” +rpg_plus/woodland/elder/10 = “Then through the cave to the shrine. There is work for the light there. Just do not ask me what kind.” +rpg_plus/woodland/elder/11 = “The village sleeps again.” The elder counts out thirty-five coins. “For the first time this month, nobody filed a complaint about darkness.” +rpg_plus/woodland/elder/12 = “At least now we will know how much we take.” +rpg_plus/woodland/elder/13 = The elder puts out his desk lamp. Outside, the light grows. +rpg_plus/woodland/elder/14 = “The light tax is abolished.” He counts out thirty-five coins. “Do not look at me like that. I am not introducing a darkness tax either. Yet.” +rpg_plus/woodland/elder/15 = “Ledger closed. Goats alive. If you find another ancient mystery, first find out who maintains it.” +rpg_plus/woodland/herbalist/1 = “Burned my hands on the old lantern. The elder said it was ordinary resin. Ordinary resin does not bite.” +rpg_plus/woodland/herbalist/2 = “Mix a mushroom with slime resin at the well workbench. Bring me the salve. ‘Woodland’ describes the ingredients, not the warranty.” +rpg_plus/woodland/herbalist/3 = “Woodland salve. Mushroom and resin. If you have neither, the merchant by the eastern house will help for coins.” +rpg_plus/woodland/herbalist/4 = Give her the woodland salve +rpg_plus/woodland/herbalist/5 = Keep it for now +rpg_plus/woodland/herbalist/6 = The herbalist unscrews the lid with her teeth and treats her hands in silence. Only then does she reach for her purse. +rpg_plus/woodland/herbalist/7 = “Twelve coins. And another job: a courier took a spare wick to the shrine. He never came back. Search the southern forest.” +rpg_plus/woodland/herbalist/8 = “The courier is still in the forest. South of the path. Green vest, bad feeling.” +rpg_plus/woodland/herbalist/9 = She reads the note twice. The second time she turns it over. Nothing on the back. +rpg_plus/woodland/herbalist/10 = “‘The forest is not angry. It is counting.’” She pockets the note. “Two resin will do for the lantern. Keep the third. Delivery fee.” +rpg_plus/woodland/herbalist/11 = “Hands healed. Courier alive. If you need rest, use the hearth by the well. No prescription.” +rpg_plus/woodland/courier/1 = “Parcel delivered.” The courier shows his empty bag. “Recipient would not sign. Recipient has roots.” +rpg_plus/woodland/courier/2 = He hands you a note: “The forest is not angry. It is counting. Return the light, or start paying fairly.” +rpg_plus/woodland/courier/3 = “Take it to the herbalist. I will sit here. Contract says I get lunch. I have not got the contract. Still get lunch.” +rpg_plus/woodland/courier/4 = “Go on. I will follow. First I need someone to sign for the roots.” +rpg_plus/woodland/workbench/1 = Instructions on the bench: “Do not taste. Do not sniff. If the product speaks, the maker accepts no liability.” +rpg_plus/woodland/hearth/1 = Nobody at the hearth demands a heroic deed. A rare place. +rpg_plus/woodland/shrine/1 = Someone beyond the arch is counting. They stumble over “village” and start again. +rpg_plus/woodland/beacon/1 = An empty cradle. Light has been brought here before. And carried away. +rpg_plus/woodland/beacon/2 = You need a ward lantern: amber, two resin and two branch bundles. The workbench is by the village well. +rpg_plus/woodland/beacon/3 = “LIGHT DEBT: THIRTY YEARS.” The voice beneath the roots rustles like paper. “WHO IS PAYING?” +rpg_plus/woodland/beacon/4 = Return the light to the forest. The village can learn to make its own. +rpg_plus/woodland/beacon/5 = Keep the lantern for the village and negotiate a fair pact. +rpg_plus/woodland/beacon/6 = Think about it first +rpg_plus/woodland/beacon/7 = You set the lantern on the stone and open its shutters. Fine roots sprout from the amber. +rpg_plus/woodland/beacon/8 = “ACCEPTED.” A pause. “NO RECEIPT AVAILABLE.” +rpg_plus/woodland/beacon/9 = “One lantern for the village. In return, no axes beyond the old path. And the elder does not keep the accounts.” +rpg_plus/woodland/beacon/10 = “AGREED.” A handprint appears on the stone. Too many fingers. You place your hand on it anyway. +rpg_plus/woodland/beacon/11 = The way back feels easier. Not safer. The road simply knows your name now. +rpg_plus/woodland/beacon/12 = The lantern burns steadily. Someone beneath the roots is carefully crossing out lines. +rpg_plus/woodland/beacon/13 = Young leaves grow on the stone. The ledger has taken root. +rpg_plus/woodland/waystone/1 = Carved in the stone: “To the shrine: through the eastern cave. Do not remove the light. Do not amend the terms.” + +rpg_plus/woodland/lamplighter/1 = “Counting lamps. Seven yesterday. Six today. The seventh is counting me.” +rpg_plus/woodland/lamplighter/2 = “Polished the new lantern three times. It never demanded a goat. Progress.” +rpg_plus/woodland/lamplighter/3 = “Brighter forest. Darker village. At least the light no longer charges interest.” + +rpg_plus/woodland/elder/ready = “Built it? Then through the cave to the shrine. Keep the shutters closed until you decide whom the light serves.” +rpg_plus/woodland/amber/recovered = The amber heart was left by the cave. This time you take it with you. diff --git a/locale/scenarios_ru.properties b/locale/scenarios_ru.properties index 4c50ef0..faed9f5 100644 --- a/locale/scenarios_ru.properties +++ b/locale/scenarios_ru.properties @@ -40,3 +40,58 @@ rpg_plus/chest/closed = Сундук не заперт. Хозяин либо д rpg_plus/chest/coins = Десять монет. Хозяин был доверчив. Был. rpg_plus/chest/empty = Пусто. Доверие кончилось вместе с монетами. rpg_plus/slime/pet = Ручной слизень. Его кормят с руки. Руки пока целы. + +rpg_plus/woodland/elder/1 = — Волк режет коз. Лес гасит фонари. Ведомость требует одного виновного. Волк уже внесён. +rpg_plus/woodland/elder/2 = Волк умеет гасить фонари? +rpg_plus/woodland/elder/3 = Где его искать? +rpg_plus/woodland/elder/4 = — Нет. Но писать «неизвестное» — это ещё три бланка. +rpg_plus/woodland/elder/5 = Староста кладёт на стол обломок фонаря. Внутри — пустое гнездо размером с кулак. +rpg_plus/woodland/elder/6 = — Раньше в святилище горел янтарь. Потом мы решили, что светить деревне полезнее. Лес оказался другого мнения. +rpg_plus/woodland/elder/7 = — На восток, за пруд. Волк сидит у пещеры. Прогони его. Если найдёшь янтарь — не продавай. Один раз мы уже заработали. +rpg_plus/woodland/elder/8 = — Янтарь у тебя? Значит, лес перестал доверять даже волкам. +rpg_plus/woodland/elder/9 = — У колодца верстак. Янтарь, две смолы, две связки веток — получится сторожевой фонарь. Смола остаётся от слизней; торговец тоже держит запас. +rpg_plus/woodland/elder/10 = — Потом через пещеру к святилищу. Свету там найдётся работа. Только не спрашивай меня, какая. +rpg_plus/woodland/elder/11 = — Деревня снова спит. — Староста отсчитывает тридцать пять монет. — Впервые за месяц никто не написал жалобу на темноту. +rpg_plus/woodland/elder/12 = — Теперь хотя бы будем знать, сколько берём. +rpg_plus/woodland/elder/13 = Староста гасит настольную лампу. За окном светлеет. +rpg_plus/woodland/elder/14 = — Налог на свет отменяю. — Он отсчитывает тридцать пять монет. — Не делай такого лица. На темноту пока тоже не ввожу. +rpg_plus/woodland/elder/15 = — Ведомость закрыта. Козы живы. Если найдёшь ещё одну древнюю тайну — сначала узнай, кто её обслуживает. +rpg_plus/woodland/herbalist/1 = — Руки обожгла о старый фонарь. Староста сказал: обычная смола. Обычная смола не кусается. +rpg_plus/woodland/herbalist/2 = — Смешай гриб со смолой от слизня на верстаке у колодца. Мазь принеси мне. Название не пугайся: «лесная» — это состав, не гарантия. +rpg_plus/woodland/herbalist/3 = — Нужна лесная мазь. Гриб и смола. Если материалов нет — торговец у восточного дома поможет за монеты. +rpg_plus/woodland/herbalist/4 = Отдать лесную мазь +rpg_plus/woodland/herbalist/5 = Оставить пока себе +rpg_plus/woodland/herbalist/6 = Травница отвинчивает крышку зубами и молча мажет руки. Только после этого достаёт кошель. +rpg_plus/woodland/herbalist/7 = — Двенадцать монет. И ещё работа: курьер понёс к святилищу запасной фитиль. Не вернулся. Ищи на юге леса. +rpg_plus/woodland/herbalist/8 = — Курьер всё ещё в лесу. Южнее тропы. Он носит зелёный жилет и дурные предчувствия. +rpg_plus/woodland/herbalist/9 = Она читает записку дважды. На второй раз переворачивает: оборот пуст. +rpg_plus/woodland/herbalist/10 = — «Лес не злится. Он считает». — Она прячет бумагу. — Для фонаря хватит двух смол. Третью оставь себе. За доставку. +rpg_plus/woodland/herbalist/11 = — Руки зажили. Курьер жив. Если хочешь отдохнуть — очаг у колодца. Без рецепта. +rpg_plus/woodland/courier/1 = — Посылку я доставил. — Курьер показывает пустую сумку. — Получатель не подписал. У получателя корни. +rpg_plus/woodland/courier/2 = Он протягивает записку: «Лес не злится. Он считает. Верните свет или начните платить честно». +rpg_plus/woodland/courier/3 = — Травнице отнеси. А я посижу. По договору мне положен обед. Договора при себе нет, но обед положен. +rpg_plus/woodland/courier/4 = — Иди. Я следом. Сначала найду, кому предъявить корни. +rpg_plus/woodland/workbench/1 = На верстаке лежит инструкция: «Не пробовать. Не нюхать. Если изделие заговорило — мастер не виноват». +rpg_plus/woodland/hearth/1 = У очага никто не требует подвига. Редкое место. +rpg_plus/woodland/shrine/1 = За аркой кто-то ведёт счёт. На слове «деревня» сбивается и начинает заново. +rpg_plus/woodland/beacon/1 = Пустое гнездо. Свет сюда уже приносили. Уносили тоже. +rpg_plus/woodland/beacon/2 = Нужен сторожевой фонарь: янтарь, две смолы, две связки веток. Верстак у деревенского колодца. +rpg_plus/woodland/beacon/3 = — ДОЛГ ПО СВЕТУ: ТРИДЦАТЬ ЛЕТ. — Голос под корнями шелестит бумагой. — КТО ОПЛАЧИВАЕТ? +rpg_plus/woodland/beacon/4 = Вернуть свет лесу. Пусть деревня научится зажигать свой. +rpg_plus/woodland/beacon/5 = Оставить фонарь деревне, заключить честный договор. +rpg_plus/woodland/beacon/6 = Сначала подумать +rpg_plus/woodland/beacon/7 = Ты ставишь фонарь на камень и открываешь створки. Янтарь прорастает тонкими корнями. +rpg_plus/woodland/beacon/8 = — ПРИНЯТО. — Пауза. — КВИТАНЦИЯ НЕ ПРЕДУСМОТРЕНА. +rpg_plus/woodland/beacon/9 = — Один фонарь на деревню. Взамен — ни одного топора за старой тропой. И учёт без старосты. +rpg_plus/woodland/beacon/10 = — СОГЛАСОВАНО. — На камне проступает отпечаток ладони. Слишком много пальцев. Ты всё равно прикладываешь свою. +rpg_plus/woodland/beacon/11 = Обратно идти легче. Не потому, что стало безопасно. Просто теперь дорога знает твоё имя. +rpg_plus/woodland/beacon/12 = В фонаре горит ровный свет. Кто-то под корнями старательно вычёркивает строки. +rpg_plus/woodland/beacon/13 = На камне растут молодые листья. Ведомость проросла. +rpg_plus/woodland/waystone/1 = На камне вырезано: «К святилищу — через пещеру на востоке. Не выносить свет. Не вносить поправки». + +rpg_plus/woodland/lamplighter/1 = — Считаю фонари. Вчера семь. Сегодня шесть. Седьмой считает меня. +rpg_plus/woodland/lamplighter/2 = — Три раза протёр новый фонарь. Ни разу не потребовал козу. Успех. +rpg_plus/woodland/lamplighter/3 = — В лесу светлее. В деревне темнее. Зато свет теперь без процентов. + +rpg_plus/woodland/elder/ready = — Собрал? Тогда через пещеру к святилищу. И не поднимай створки, пока не решишь, кому светить. +rpg_plus/woodland/amber/recovered = Сердце янтаря осталось у пещеры. На этот раз забираешь его с собой. diff --git a/manifest.json b/manifest.json index 7cad74c..18c5dfd 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "RPG Plus", "version": "1.0.0", "author": "ct", - "description": "Content of the RPG Plus sample (collection rpg_plus), built on top of the RPG demo instead of beside it: a visual-novel prologue at a crossroads where the player picks the hero's class (warrior, mage, rogue: stats_base archetypes) and starting gear, and makes a first charisma check; then the whole rpg_demo village and forest, untouched, with a trainer and a fortune teller added through this mod's own map_events file, a training yard novel scene with exp and a strength check, the merchant selling the classic pack's gear (a shop patch), the wolf given stats (a character patch). Only data and scenarios, no C#.", + "description": "A woodland RPG with a class prologue, a village ward mystery, an apothecary and missing courier, a ruined shrine with two endings, monster loot and recipes. World and story are data and scenarios; crafting is a separate reusable mechanic.", "category": "story", "depends": [ "rpg_demo_game>=1", @@ -15,7 +15,9 @@ "trading>=1", "dialog_base>=1", "characters_base>=1", - "items_base>=1" + "items_base>=1", + "crafting>=1", + "quests_base>=1" ], "test_suites": [ { @@ -23,6 +25,12 @@ "activate": [ "--itest=rpg_plus" ] + }, + { + "id": "woodland", + "activate": [ + "--itest=woodland" + ] } ] } diff --git a/map_events/rpg_elder_house/woodland.json b/map_events/rpg_elder_house/woodland.json new file mode 100644 index 0000000..18949f3 --- /dev/null +++ b/map_events/rpg_elder_house/woodland.json @@ -0,0 +1,22 @@ +{ + "Events": [ + { + "Id": "elder_quest", + "X": 6, + "Y": 3, + "Sprite": "village/elder", + "Dir": "Down", + "Scenario": "rpg_plus/woodland/elder", + "Condition": "rpg_wolf_dead != 1" + }, + { + "Id": "elder_thanks", + "X": 6, + "Y": 3, + "Sprite": "village/elder", + "Dir": "Down", + "Scenario": "rpg_plus/woodland/elder", + "Condition": "rpg_wolf_dead == 1" + } + ] +} diff --git a/map_events/rpg_forest/woodland.json b/map_events/rpg_forest/woodland.json new file mode 100644 index 0000000..07b3b4a --- /dev/null +++ b/map_events/rpg_forest/woodland.json @@ -0,0 +1,68 @@ +{ + "Events": [ + { + "Id": "woodland_lost_amber", + "X": 25, + "Y": 8, + "Icon": "rpg_plus/amber", + "Solid": false, + "Condition": "rpg_wolf_dead == 1 && item('rpg_plus/amber') == 0 && item('rpg_plus/ward_lantern') == 0 && rpg_ward_done != 1", + "Commands": [ + "ptext \"Сердце янтаря осталось у пещеры. На этот раз забираешь его с собой.\" key=rpg_plus/woodland/amber/recovered", + "action item.add id=rpg_plus/amber count=1", + "end" + ] + }, + { + "Id": "woodland_courier", + "X": 5, + "Y": 13, + "Sprite": "village/merchant", + "Dir": "Down", + "Scenario": "rpg_plus/woodland/courier", + "Pages": [ + { + "Condition": "rpg_courier_paid == 1", + "Solid": false, + "Route": { + "Steps": [ + "up", + "up", + "up", + "up", + "up", + "left", + "left", + "left", + "left", + "wait" + ], + "Every": 0.8, + "Repeat": false + } + } + ] + }, + { + "Id": "woodland_waystone", + "X": 20, + "Y": 9, + "Tile": 37, + "Scenario": "rpg_plus/woodland/waystone" + }, + { + "Id": "woodland_to_shrine", + "X": 26, + "Y": 8, + "Trigger": "Action", + "Solid": true, + "Condition": "rpg_wolf_dead == 1", + "Teleport": { + "Map": "rpg_woodland_shrine", + "X": 2, + "Y": 11, + "Dir": "Right" + } + } + ] +} diff --git a/map_events/rpg_village/woodland.json b/map_events/rpg_village/woodland.json new file mode 100644 index 0000000..09bc97c --- /dev/null +++ b/map_events/rpg_village/woodland.json @@ -0,0 +1,63 @@ +{ + "Events": [ + { + "Id": "woodland_herbalist", + "X": 3, + "Y": 12, + "Sprite": "village/villager", + "Dir": "Right", + "Scenario": "rpg_plus/woodland/herbalist" + }, + { + "Id": "woodland_workbench", + "X": 17, + "Y": 12, + "Tile": 39, + "Scenario": "rpg_plus/woodland/workbench" + }, + { + "Id": "woodland_hearth", + "X": 13, + "Y": 12, + "Tile": 35, + "Light": { + "Radius": 3, + "Color": "#ffc37a" + }, + "Scenario": "rpg_plus/woodland/hearth" + }, + { + "Id": "woodland_beacon", + "X": 15, + "Y": 7, + "Tile": 36, + "Light": { + "Radius": 2, + "Color": "#88d5c0" + }, + "Condition": "rpg_ward_done == 1 && rpg_ward_choice == 'pact'" + }, + { + "Id": "woodland_lamplighter", + "X": 24, + "Y": 10, + "Sprite": "village/elder", + "Dir": "Left", + "Scenario": "rpg_plus/woodland/lamplighter", + "Route": { + "Steps": [ + "left", + "wait", + "left", + "face_up", + "wait", + "right", + "right", + "face_down", + "wait" + ], + "Every": 1.1 + } + } + ] +} diff --git a/map_events/rpg_woodland_shrine/events.json b/map_events/rpg_woodland_shrine/events.json new file mode 100644 index 0000000..3e68cd6 --- /dev/null +++ b/map_events/rpg_woodland_shrine/events.json @@ -0,0 +1,45 @@ +{ + "Events": [ + { + "Id": "shrine_exit", + "X": 1, + "Y": 11, + "Trigger": "Touch", + "Teleport": { + "Map": "rpg_forest", + "X": 25, + "Y": 8, + "Dir": "Left" + } + }, + { + "Id": "shrine_beacon", + "X": 12, + "Y": 5, + "Tile": 36, + "Scenario": "rpg_plus/woodland/beacon", + "Light": { + "Radius": 2, + "Color": "#699baf" + }, + "Pages": [ + { + "Condition": "rpg_ward_done == 1 && rpg_ward_choice == 'pact'", + "Tile": 36, + "Light": { + "Radius": 6, + "Color": "#f0cd8d" + } + }, + { + "Condition": "rpg_ward_done == 1 && rpg_ward_choice == 'free'", + "Tile": 38, + "Light": { + "Radius": 6, + "Color": "#9ce3c7" + } + } + ] + } + ] +} diff --git a/maps/rpg_woodland_shrine.json b/maps/rpg_woodland_shrine.json new file mode 100644 index 0000000..93d9d05 --- /dev/null +++ b/maps/rpg_woodland_shrine.json @@ -0,0 +1,94 @@ +{ + "Kind": "rpg_map", + "Items": [ + { + "Id": "rpg_woodland_shrine", + "Name": "rpg_plus/map/shrine", + "Tileset": "village", + "Width": 24, + "Height": 16, + "Zoom": 2, + "Ambient": "#a3bbc1", + "Atmosphere": "woodland", + "Layers": { + "Back": [ + " 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 4 4 4 4 4 4 4 4 4 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 4 6 6 6 6 6 6 6 4 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 4 6 6 6 6 6 6 6 4 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 4 6 6 6 6 6 6 6 4 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 4 6 6 6 160 6 6 6 4 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 4 4 4 4 170 4 4 4 4 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 24 24 24 24 170 24 24 24 24 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 24 24 24 24 170 24 24 24 24 24 24 24 24 24 24 24", + " 24 173 165 165 165 165 165 165 165 165 165 165 166 24 24 24 24 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24", + " 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24" + ], + "Main": [ + " . . . . . . . . . . . . . . . . . . . . . . . .", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " 70 71 72 70 71 72 70 71 72 . . . . . . 70 71 72 70 71 72 70 71 72", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " . . . . . . . . 37 . . . . . . . 37 . . . . . . .", + " 70 71 72 70 71 72 . . . . . . . . . . . . 70 71 72 70 71 72", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " 70 71 72 . . . . . 37 . . . . . . . 37 . . . . 70 71 72", + " . . . . 70 71 72 . . . . . . . . . . . . . . . . .", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " . . . . . . . . . . . . . . . . . . . . . 70 71 72", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " 70 71 72 70 71 72 70 71 72 70 71 72 70 71 72 70 71 72 70 71 72 70 71 72" + ], + "Front": [ + " 64 65 66 64 65 66 64 65 66 . . . . . . 64 65 66 64 65 66 64 65 66", + " 67 68 69 67 68 69 67 68 69 . 112 113 114 115 . 67 68 69 67 68 69 67 68 69", + " . . . . . . . . . . 116 117 118 119 . . . . . . . . . .", + " 64 65 66 64 65 66 . . . . 120 121 122 123 . . . . 64 65 66 64 65 66", + " 67 68 69 67 68 69 . . . . 124 125 126 127 . . . . 67 68 69 67 68 69", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " 64 65 66 . . . . . . . . . . . . . . . . . . 64 65 66", + " 67 68 69 . 64 65 66 . . . . . . . . . . . . . . 67 68 69", + " . . . . 67 68 69 . . . . . . . . . . . . . . . . .", + " . . . . . . . . . . . . . . . . . . . . . 64 65 66", + " . . . . . . . . . . . . . . . . . . . . . 67 68 69", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " . . . . . . . . . . . . . . . . . . . . . . . .", + " 64 65 66 64 65 66 64 65 66 64 65 66 64 65 66 64 65 66 64 65 66 64 65 66", + " 67 68 69 67 68 69 67 68 69 67 68 69 67 68 69 67 68 69 67 68 69 67 68 69", + " . . . . . . . . . . . . . . . . . . . . . . . ." + ] + }, + "Collision": [ + "########################", + "#......................#", + "#......................#", + "#.........#..#.........#", + "#.........####.........#", + "#......................#", + "#......................#", + "#......................#", + "#......................#", + "#......................#", + "#......................#", + "#......................#", + "#......................#", + "#......................#", + "#......................#", + "########################" + ], + "Spawn": { + "X": 2, + "Y": 11, + "Dir": "Right" + } + } + ] +} diff --git a/patches/character.json b/patches/character.json index 620a6b7..ae1eedb 100644 --- a/patches/character.json +++ b/patches/character.json @@ -2,7 +2,54 @@ "Items": [ { "Id": "rpg_wolf", - "Stats": { "str": 5, "vit": 5, "agi": 7 } + "Stats": { + "str": 5, + "vit": 5, + "agi": 7 + }, + "Loot": [ + { + "ItemId": "rpg_plus/wolf_pelt", + "MinCount": 2, + "MaxCount": 3, + "Probability": 100 + }, + { + "ItemId": "money/coin", + "MinCount": 8, + "MaxCount": 14, + "Probability": 100 + }, + { + "ItemId": "rpg_plus/amber", + "MinCount": 1, + "MaxCount": 1, + "Probability": 100 + } + ] + }, + { + "Id": "rpg_slime", + "Loot": [ + { + "ItemId": "rpg_plus/resin", + "MinCount": 1, + "MaxCount": 2, + "Probability": 100 + }, + { + "ItemId": "money/coin", + "MinCount": 2, + "MaxCount": 4, + "Probability": 100 + }, + { + "ItemId": "rpg_demo/mushroom", + "MinCount": 1, + "MaxCount": 1, + "Probability": 25 + } + ] } ] } diff --git a/patches/shop.json b/patches/shop.json index dedd06e..4616321 100644 --- a/patches/shop.json +++ b/patches/shop.json @@ -3,13 +3,60 @@ { "Id": "rpg_demo/village_merchant", "Sells$add": [ - { "Item": "classic/iron_sword", "Price": 30 }, - { "Item": "classic/dagger", "Price": 18 }, - { "Item": "classic/oak_staff", "Price": 24 }, - { "Item": "classic/leather_armor", "Price": 20 }, - { "Item": "classic/robe", "Price": 16 }, - { "Item": "classic/silver_ring", "Price": 22 }, - { "Item": "classic/strength_tonic", "Price": 40 } + { + "Item": "classic/iron_sword", + "Price": 30 + }, + { + "Item": "classic/dagger", + "Price": 18 + }, + { + "Item": "classic/oak_staff", + "Price": 24 + }, + { + "Item": "classic/leather_armor", + "Price": 20 + }, + { + "Item": "classic/robe", + "Price": 16 + }, + { + "Item": "classic/silver_ring", + "Price": 22 + }, + { + "Item": "classic/strength_tonic", + "Price": 40 + }, + { + "Item": "rpg_plus/resin", + "Price": 4 + }, + { + "Item": "rpg_demo/mushroom", + "Price": 3 + }, + { + "Item": "rpg_demo/branches", + "Price": 2 + } + ], + "Buys$add": [ + { + "Item": "rpg_plus/resin", + "Price": 2 + }, + { + "Item": "rpg_plus/wolf_pelt", + "Price": 4 + }, + { + "Item": "rpg_plus/salve", + "Price": 5 + } ] } ] diff --git a/quests/woodland.json b/quests/woodland.json new file mode 100644 index 0000000..86bdd30 --- /dev/null +++ b/quests/woodland.json @@ -0,0 +1,32 @@ +{ + "Kind": "quest", + "Items": [ + { + "Id": "woodland_ward", + "Name": "rpg_plus/quest/ward/name", + "StatusToDescription": { + "0": "rpg_plus/quest/ward/0", + "1": "rpg_plus/quest/ward/1", + "2": "rpg_plus/quest/ward/2", + "3": "rpg_plus/quest/ward/3", + "4": "rpg_plus/quest/ward/4" + } + }, + { + "Id": "woodland_apothecary", + "Name": "rpg_plus/quest/apothecary/name", + "StatusToDescription": { + "0": "rpg_plus/quest/apothecary/0", + "1": "rpg_plus/quest/apothecary/1" + } + }, + { + "Id": "woodland_courier", + "Name": "rpg_plus/quest/courier/name", + "StatusToDescription": { + "0": "rpg_plus/quest/courier/0", + "1": "rpg_plus/quest/courier/1" + } + } + ] +} diff --git a/recipes/woodland.json b/recipes/woodland.json new file mode 100644 index 0000000..26914ab --- /dev/null +++ b/recipes/woodland.json @@ -0,0 +1,78 @@ +{ + "Kind": "recipe", + "Items": [ + { + "Id": "rpg_plus/salve", + "Name": "rpg_plus/recipe/salve/name", + "Description": "rpg_plus/recipe/salve/desc", + "Type": "Cooking", + "Time": 5, + "Input": [ + { + "Id": "rpg_demo/mushroom", + "Count": 1 + }, + { + "Id": "rpg_plus/resin", + "Count": 1 + } + ], + "Output": [ + { + "Id": "rpg_plus/salve", + "Count": 1 + } + ] + }, + { + "Id": "rpg_plus/mantle", + "Name": "rpg_plus/recipe/mantle/name", + "Description": "rpg_plus/recipe/mantle/desc", + "Type": "Cooking", + "Time": 10, + "Input": [ + { + "Id": "rpg_plus/wolf_pelt", + "Count": 2 + }, + { + "Id": "rpg_plus/resin", + "Count": 2 + } + ], + "Output": [ + { + "Id": "rpg_plus/fur_mantle", + "Count": 1 + } + ] + }, + { + "Id": "rpg_plus/lantern", + "Name": "rpg_plus/recipe/lantern/name", + "Description": "rpg_plus/recipe/lantern/desc", + "Type": "Cooking", + "Time": 10, + "Input": [ + { + "Id": "rpg_plus/amber", + "Count": 1 + }, + { + "Id": "rpg_plus/resin", + "Count": 2 + }, + { + "Id": "rpg_demo/branches", + "Count": 2 + } + ], + "Output": [ + { + "Id": "rpg_plus/ward_lantern", + "Count": 1 + } + ] + } + ] +} diff --git a/scenarios/rpg_plus/woodland/beacon.scenario b/scenarios/rpg_plus/woodland/beacon.scenario new file mode 100644 index 0000000..ab025b2 --- /dev/null +++ b/scenarios/rpg_plus/woodland/beacon.scenario @@ -0,0 +1,37 @@ +when "rpg_ward_done == 1" -> :after +when "item('rpg_plus/ward_lantern') >= 1" -> :choose +ptext "Пустое гнездо. Свет сюда уже приносили. Уносили тоже." key=rpg_plus/woodland/beacon/1 +ptext "Нужен сторожевой фонарь: янтарь, две смолы, две связки веток. Верстак у деревенского колодца." key=rpg_plus/woodland/beacon/2 +end +:choose +ptext "— ДОЛГ ПО СВЕТУ: ТРИДЦАТЬ ЛЕТ. — Голос под корнями шелестит бумагой. — КТО ОПЛАЧИВАЕТ?" key=rpg_plus/woodland/beacon/3 +select "" +option "Вернуть свет лесу. Пусть деревня научится зажигать свой." key=rpg_plus/woodland/beacon/4 -> :free +option "Оставить фонарь деревне, заключить честный договор." key=rpg_plus/woodland/beacon/5 -> :pact +option "Сначала подумать" key=rpg_plus/woodland/beacon/6 -> :end +:free +action item.remove id=rpg_plus/ward_lantern +flag rpg_ward_choice free +ptext "Ты ставишь фонарь на камень и открываешь створки. Янтарь прорастает тонкими корнями." key=rpg_plus/woodland/beacon/7 +ptext "— ПРИНЯТО. — Пауза. — КВИТАНЦИЯ НЕ ПРЕДУСМОТРЕНА." key=rpg_plus/woodland/beacon/8 +goto -> :done +:pact +action item.remove id=rpg_plus/ward_lantern +flag rpg_ward_choice pact +ptext "— Один фонарь на деревню. Взамен — ни одного топора за старой тропой. И учёт без старосты." key=rpg_plus/woodland/beacon/9 +ptext "— СОГЛАСОВАНО. — На камне проступает отпечаток ладони. Слишком много пальцев. Ты всё равно прикладываешь свою." key=rpg_plus/woodland/beacon/10 +:done +flag rpg_ward_done 1 +action quest.set_status quest_id=woodland_ward status=Progress +action quest.set_data_status quest_id=woodland_ward level=4 +action stat.exp who=rpg_hero value=20 +ptext "Обратно идти легче. Не потому, что стало безопасно. Просто теперь дорога знает твоё имя." key=rpg_plus/woodland/beacon/11 +end +:after +when "rpg_ward_choice == 'free'" -> :green +ptext "В фонаре горит ровный свет. Кто-то под корнями старательно вычёркивает строки." key=rpg_plus/woodland/beacon/12 +end +:green +ptext "На камне растут молодые листья. Ведомость проросла." key=rpg_plus/woodland/beacon/13 +:end +end diff --git a/scenarios/rpg_plus/woodland/courier.scenario b/scenarios/rpg_plus/woodland/courier.scenario new file mode 100644 index 0000000..5b3bbf0 --- /dev/null +++ b/scenarios/rpg_plus/woodland/courier.scenario @@ -0,0 +1,11 @@ +when "rpg_courier_found == 1" -> :after +ptext "— Посылку я доставил. — Курьер показывает пустую сумку. — Получатель не подписал. У получателя корни." key=rpg_plus/woodland/courier/1 +ptext "Он протягивает записку: «Лес не злится. Он считает. Верните свет или начните платить честно»." key=rpg_plus/woodland/courier/2 +flag rpg_courier_found 1 +action quest.set_status quest_id=woodland_courier status=Progress +action quest.set_data_status quest_id=woodland_courier level=1 +ptext "— Травнице отнеси. А я посижу. По договору мне положен обед. Договора при себе нет, но обед положен." key=rpg_plus/woodland/courier/3 +end +:after +ptext "— Иди. Я следом. Сначала найду, кому предъявить корни." key=rpg_plus/woodland/courier/4 +end diff --git a/scenarios/rpg_plus/woodland/elder.scenario b/scenarios/rpg_plus/woodland/elder.scenario new file mode 100644 index 0000000..2228b01 --- /dev/null +++ b/scenarios/rpg_plus/woodland/elder.scenario @@ -0,0 +1,46 @@ +# The elder's reward is guarded separately from the battle flag: revisits cannot pay twice. +when "rpg_ward_paid == 1" -> :after +when "rpg_ward_done == 1" -> :reward +when "item('rpg_plus/ward_lantern') >= 1" -> :ready +when "rpg_wolf_dead == 1" -> :lantern +ptext "— Волк режет коз. Лес гасит фонари. Ведомость требует одного виновного. Волк уже внесён." key=rpg_plus/woodland/elder/1 +select "" +option "Волк умеет гасить фонари?" key=rpg_plus/woodland/elder/2 -> :truth +option "Где его искать?" key=rpg_plus/woodland/elder/3 -> :work +:truth +ptext "— Нет. Но писать «неизвестное» — это ещё три бланка." key=rpg_plus/woodland/elder/4 +ptext "Староста кладёт на стол обломок фонаря. Внутри — пустое гнездо размером с кулак." key=rpg_plus/woodland/elder/5 +ptext "— Раньше в святилище горел янтарь. Потом мы решили, что светить деревне полезнее. Лес оказался другого мнения." key=rpg_plus/woodland/elder/6 +:work +flag rpg_quest_taken 1 +action quest.set_status quest_id=woodland_ward status=Progress +action quest.set_data_status quest_id=woodland_ward level=1 +ptext "— На восток, за пруд. Волк сидит у пещеры. Прогони его. Если найдёшь янтарь — не продавай. Один раз мы уже заработали." key=rpg_plus/woodland/elder/7 +end +:lantern +action quest.set_status quest_id=woodland_ward status=Progress +action quest.set_data_status quest_id=woodland_ward level=2 +ptext "— Янтарь у тебя? Значит, лес перестал доверять даже волкам." key=rpg_plus/woodland/elder/8 +ptext "— У колодца верстак. Янтарь, две смолы, две связки веток — получится сторожевой фонарь. Смола остаётся от слизней; торговец тоже держит запас." key=rpg_plus/woodland/elder/9 +ptext "— Потом через пещеру к святилищу. Свету там найдётся работа. Только не спрашивай меня, какая." key=rpg_plus/woodland/elder/10 +end +:ready +action quest.set_data_status quest_id=woodland_ward level=3 +ptext "— Собрал? Тогда через пещеру к святилищу. И не поднимай створки, пока не решишь, кому светить." key=rpg_plus/woodland/elder/ready +end +:reward +flag rpg_ward_paid 1 +action quest.set_status quest_id=woodland_ward status=End +action stat.exp who=rpg_hero value=35 +action item.add id=money/coin count=35 +when "rpg_ward_choice == 'free'" -> :free +ptext "— Деревня снова спит. — Староста отсчитывает тридцать пять монет. — Впервые за месяц никто не написал жалобу на темноту." key=rpg_plus/woodland/elder/11 +ptext "— Теперь хотя бы будем знать, сколько берём." key=rpg_plus/woodland/elder/12 +end +:free +ptext "Староста гасит настольную лампу. За окном светлеет." key=rpg_plus/woodland/elder/13 +ptext "— Налог на свет отменяю. — Он отсчитывает тридцать пять монет. — Не делай такого лица. На темноту пока тоже не ввожу." key=rpg_plus/woodland/elder/14 +end +:after +ptext "— Ведомость закрыта. Козы живы. Если найдёшь ещё одну древнюю тайну — сначала узнай, кто её обслуживает." key=rpg_plus/woodland/elder/15 +end diff --git a/scenarios/rpg_plus/woodland/hearth.scenario b/scenarios/rpg_plus/woodland/hearth.scenario new file mode 100644 index 0000000..9e8b9b4 --- /dev/null +++ b/scenarios/rpg_plus/woodland/hearth.scenario @@ -0,0 +1,3 @@ +ptext "У очага никто не требует подвига. Редкое место." key=rpg_plus/woodland/hearth/1 +action stat.restore who=party +end diff --git a/scenarios/rpg_plus/woodland/herbalist.scenario b/scenarios/rpg_plus/woodland/herbalist.scenario new file mode 100644 index 0000000..fd63559 --- /dev/null +++ b/scenarios/rpg_plus/woodland/herbalist.scenario @@ -0,0 +1,48 @@ +when "rpg_salve_given != 1" -> :salve_start +when "rpg_courier_paid == 1" -> :after +when "rpg_courier_found == 1" -> :letter +goto -> :waiting +:salve_start +when "rpg_salve_asked == 1" -> :medicine +ptext "— Руки обожгла о старый фонарь. Староста сказал: обычная смола. Обычная смола не кусается." key=rpg_plus/woodland/herbalist/1 +flag rpg_salve_asked 1 +action quest.set_status quest_id=woodland_apothecary status=Progress +action quest.set_data_status quest_id=woodland_apothecary level=1 +ptext "— Смешай гриб со смолой от слизня на верстаке у колодца. Мазь принеси мне. Название не пугайся: «лесная» — это состав, не гарантия." key=rpg_plus/woodland/herbalist/2 +:medicine +when "item('rpg_plus/salve') >= 1" -> :offer +ptext "— Нужна лесная мазь. Гриб и смола. Если материалов нет — торговец у восточного дома поможет за монеты." key=rpg_plus/woodland/herbalist/3 +end +:offer +select "" +option "Отдать лесную мазь" key=rpg_plus/woodland/herbalist/4 -> :deliver +option "Оставить пока себе" key=rpg_plus/woodland/herbalist/5 -> :bye +:deliver +action item.remove id=rpg_plus/salve +flag rpg_salve_given 1 +action quest.set_status quest_id=woodland_apothecary status=End +action item.add id=money/coin count=12 +action item.add id=rpg_plus/resin count=2 +action stat.exp who=rpg_hero value=10 +ptext "Травница отвинчивает крышку зубами и молча мажет руки. Только после этого достаёт кошель." key=rpg_plus/woodland/herbalist/6 +ptext "— Двенадцать монет. И ещё работа: курьер понёс к святилищу запасной фитиль. Не вернулся. Ищи на юге леса." key=rpg_plus/woodland/herbalist/7 +action quest.set_status quest_id=woodland_courier status=Progress +when "rpg_courier_found == 1" -> :letter +end +:waiting +ptext "— Курьер всё ещё в лесу. Южнее тропы. Он носит зелёный жилет и дурные предчувствия." key=rpg_plus/woodland/herbalist/8 +end +:letter +flag rpg_courier_paid 1 +action quest.set_status quest_id=woodland_courier status=End +action item.add id=rpg_plus/resin count=3 +action item.add id=money/coin count=15 +action stat.exp who=rpg_hero value=12 +ptext "Она читает записку дважды. На второй раз переворачивает: оборот пуст." key=rpg_plus/woodland/herbalist/9 +ptext "— «Лес не злится. Он считает». — Она прячет бумагу. — Для фонаря хватит двух смол. Третью оставь себе. За доставку." key=rpg_plus/woodland/herbalist/10 +end +:after +ptext "— Руки зажили. Курьер жив. Если хочешь отдохнуть — очаг у колодца. Без рецепта." key=rpg_plus/woodland/herbalist/11 +end +:bye +end diff --git a/scenarios/rpg_plus/woodland/journal.scenario b/scenarios/rpg_plus/woodland/journal.scenario new file mode 100644 index 0000000..7c1423f --- /dev/null +++ b/scenarios/rpg_plus/woodland/journal.scenario @@ -0,0 +1,7 @@ +scene: rpg_village +priority: 5 +is_one_run: true +save_allowed: true +--- +action quest.set_status quest_id=woodland_ward status=Progress +end diff --git a/scenarios/rpg_plus/woodland/lamplighter.scenario b/scenarios/rpg_plus/woodland/lamplighter.scenario new file mode 100644 index 0000000..b192fd3 --- /dev/null +++ b/scenarios/rpg_plus/woodland/lamplighter.scenario @@ -0,0 +1,10 @@ +when "rpg_ward_choice == 'pact'" -> :pact +when "rpg_ward_choice == 'free'" -> :free +ptext "— Считаю фонари. Вчера семь. Сегодня шесть. Седьмой считает меня." key=rpg_plus/woodland/lamplighter/1 +end +:pact +ptext "— Три раза протёр новый фонарь. Ни разу не потребовал козу. Успех." key=rpg_plus/woodland/lamplighter/2 +end +:free +ptext "— В лесу светлее. В деревне темнее. Зато свет теперь без процентов." key=rpg_plus/woodland/lamplighter/3 +end diff --git a/scenarios/rpg_plus/woodland/shrine.scenario b/scenarios/rpg_plus/woodland/shrine.scenario new file mode 100644 index 0000000..456136b --- /dev/null +++ b/scenarios/rpg_plus/woodland/shrine.scenario @@ -0,0 +1,12 @@ +scene: rpg_woodland_shrine +priority: 10 +is_one_run: true +save_allowed: true +--- +title "Лес помнит" key=rpg_plus/map/shrine +ptext "За аркой кто-то ведёт счёт. На слове «деревня» сбивается и начинает заново." key=rpg_plus/woodland/shrine/1 +when "rpg_ward_done == 1" -> :end +action quest.set_status quest_id=woodland_ward status=Progress +action quest.set_data_status quest_id=woodland_ward level=3 +:end +end diff --git a/scenarios/rpg_plus/woodland/waystone.scenario b/scenarios/rpg_plus/woodland/waystone.scenario new file mode 100644 index 0000000..1a9b543 --- /dev/null +++ b/scenarios/rpg_plus/woodland/waystone.scenario @@ -0,0 +1,2 @@ +ptext "На камне вырезано: «К святилищу — через пещеру на востоке. Не выносить свет. Не вносить поправки»." key=rpg_plus/woodland/waystone/1 +end diff --git a/scenarios/rpg_plus/woodland/workbench.scenario b/scenarios/rpg_plus/woodland/workbench.scenario new file mode 100644 index 0000000..d39e584 --- /dev/null +++ b/scenarios/rpg_plus/woodland/workbench.scenario @@ -0,0 +1,3 @@ +ptext "На верстаке лежит инструкция: «Не пробовать. Не нюхать. Если изделие заговорило — мастер не виноват»." key=rpg_plus/woodland/workbench/1 +action craft.open station=rpg_plus/workbench +end diff --git a/scenes/rpg_woodland_shrine.json b/scenes/rpg_woodland_shrine.json new file mode 100644 index 0000000..a257495 --- /dev/null +++ b/scenes/rpg_woodland_shrine.json @@ -0,0 +1,5 @@ +{ + "name": "rpg_woodland_shrine", + "defaultBackground": "village/forest", + "defaultAmbient": "" +} diff --git a/stations/woodland.json b/stations/woodland.json new file mode 100644 index 0000000..0baf19b --- /dev/null +++ b/stations/woodland.json @@ -0,0 +1,14 @@ +{ + "Kind": "craft_station", + "Items": [ + { + "Id": "rpg_plus/workbench", + "Name": "rpg_plus/station/name", + "Recipes": [ + "rpg_plus/salve", + "rpg_plus/mantle", + "rpg_plus/lantern" + ] + } + ] +} diff --git a/test_suites/woodland/manifest.json b/test_suites/woodland/manifest.json new file mode 100644 index 0000000..9868b36 --- /dev/null +++ b/test_suites/woodland/manifest.json @@ -0,0 +1,6 @@ +{ + "name":"Woodland adventure test", + "description":"The woodland quest chain, loot-to-crafting loop, both shrine choices, repeat-safe rewards and save/load through the real game services and windows.", + "sources":["src/*.cs"], + "depends":["rpg_plus_game>=1", "ff_battle>=1"] +} diff --git a/test_suites/woodland/src/WoodlandTestDriver.cs b/test_suites/woodland/src/WoodlandTestDriver.cs new file mode 100644 index 0000000..8fda3e2 --- /dev/null +++ b/test_suites/woodland/src/WoodlandTestDriver.cs @@ -0,0 +1,446 @@ +namespace Cthangover.Mods.RpgPlus.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Cthangover.Core.Battle; + using Cthangover.Core.Bootstrap; + using Cthangover.Core.Factories.Impls; + using Cthangover.Core.Games; + using Cthangover.Core.Items; + using Cthangover.Core.Quests; + using Cthangover.Core.Scenes; + using Cthangover.Core.Settings; + using Cthangover.Core.UI; + using Cthangover.Core.Utils; + using Cthangover.Mods.Crafting; + using Cthangover.Mods.Dialog; + using Cthangover.Mods.Equipment; + using Cthangover.Mods.Interface.View; + using Cthangover.Mods.Rpg; + using Cthangover.Mods.Stats; + using Cthangover.FFBattle.UI; + using Godot; + + /// + /// Drives NPC event dialogs and the crafting UI, then verifies quest state and rewards + /// across a real save/load. Battles click the FF attack menus and enemy widgets; + /// enemies have one HP so lethal-hit reward timing is deterministic. + /// Gathering is covered by trade/rpg_walk; only mushrooms and branches are fixtures here. + /// + public partial class WoodlandTestDriver : Node + { + private readonly Queue choices = new(); + private readonly bool pact; + private readonly string shots; + private int step; + private int ticks; + private int passed; + private int failed; + private int resinBefore; + private int paidCoins; + private int savedResin; + private int villageShotPhase; + private long shotTime; + private double elapsed; + private BattleSceneContext testedBattle; + private ulong nextAttack; + private string victoryShot; + private bool recoveringAmber; + private const string SaveSlot = "slot_9917"; + private string TestName => pact ? "woodland_pact" : "woodland"; + private static RpgWorldView View => RpgWorldHost.Current?.View; + private static DialogBox Box => RpgInput.FindDialogBox(); + private static IInventory Bag => GameData.Instance.Runtime.Inventory; + private static string Flag(string key) => GameData.Instance.Runtime.Flags.TryGetValue(key, out var value) ? value : null; + private static QuestBase Quest(string id) => (QuestBase)QuestFactory.Instance.Get("woodland_" + id); + + /// Two deterministic choice paths share the same checks and isolated test slot. + public WoodlandTestDriver(bool pact, string shots) + { + this.pact = pact; + this.shots = shots; + Name = "WoodlandTestDriver"; + var timer = new Timer { WaitTime = 0.1, Autostart = true }; + timer.Timeout += Tick; + AddChild(timer); + } + + private void Tick() + { + if (step < 0) + return; + elapsed += 0.1; + if (elapsed > 180) + { + Check(false, $"timeout at woodland step {step}"); + Finish(); + return; + } + try { RunStep(); } + catch (Exception ex) + { + Check(false, $"step {step}: {ex}"); + Finish(); + } + } + + private void RunStep() + { + // The early DialogBox lookup trap is avoided: wait for a current scene first. + var scene = GetTree().Root.GetNodeOrNull("SceneManager")?.CurrentSceneName; + if (!Boot.IsReady || string.IsNullOrEmpty(scene)) + return; + if (step == 0) + { + choices.Enqueue(0); // warrior + choices.Enqueue(1); // pass the seer, no dice dependency + Next(); + } + if (AdvanceDialog() || GodotSceneService.IsTransitioning) + return; + if (++ticks < 3) + return; + switch (step) + { + case 1: + if (View?.Map.ID != "rpg_village") return; + if (!CaptureVillage()) return; + CheckTrees(); + Check(View.FindEvent("woodland_workbench").Current.Tile == 39, "workbench has its own sprite instead of a barrel"); + Check(Quest("ward").Status == QuestStatus.Progress, "journal starts in the village"); + Go("rpg_elder_house", 6, 4, RpgDirection.Up); + Next(); return; + case 2: + if (View?.Map.ID != "rpg_elder_house") return; + choices.Enqueue(0); + Tap(Key.E); Next(); return; + case 3: + Check(Flag("rpg_quest_taken") == "1", "elder explains the ward and wolf"); + Check(Quest("ward").Data.Status == 1, "journal points to the forest"); + Go("rpg_forest", 9, 8, RpgDirection.Right); Next(); return; + case 4: + if (View?.Map.ID != "rpg_forest") return; + CheckTrees(); + Shot("forest"); + View.Map.Encounters = null; + Tap(Key.E); Next(); return; + case 5: + if (!Win("rpg_slime")) return; + Next(); return; + case 6: + if (!ContinueVictory()) return; + Next(); return; + case 7: + if (View?.Map.ID != "rpg_forest") return; + Check(Bag.CheckCount("rpg_plus/resin") >= 1, "slime loot is granted by the victory screen"); + View.PlacePlayer(24, 8, RpgDirection.Right); + Tap(Key.E); Next(); return; + case 8: + if (!Win("rpg_wolf", "rpg_slime")) return; + Next(); return; + case 9: + if (!ContinueVictory()) return; + Next(); return; + case 10: + if (View?.Map.ID != "rpg_forest") return; + if (recoveringAmber) + { + Check(Bag.CheckCount("rpg_plus/amber") == 1, "old save recovers exactly one missing amber through the cave pickup"); + Check(!View.FindEvent("woodland_lost_amber").Visible, "recovered amber cannot be picked twice"); + Bag.Add("rpg_demo/mushroom", 1); + Bag.Add("rpg_demo/branches", 2); + Go("rpg_village", 2, 12, RpgDirection.Right); Next(); return; + } + Check(Flag("rpg_wolf_dead") == "1", "wolf victory changes the world"); + Check(Bag.CheckCount("rpg_plus/amber") == 1, "wolf drops the guaranteed quest amber"); + Check(Bag.CheckCount("rpg_plus/wolf_pelt") >= 2, "wolf drops enough fur for the mantle"); + Check(!View.FindEvent("woodland_lost_amber").Visible, "normal victory does not duplicate the amber"); + Bag.Remove("rpg_plus/amber", 1); + View.RefreshEvents(); + Check(View.FindEvent("woodland_lost_amber").Visible, "missing quest amber remains recoverable after an old wolf victory"); + View.PlacePlayer(24, 8, RpgDirection.Right); + recoveringAmber = true; + Tap(Key.E); ticks = 0; return; + case 11: + if (View?.Map.ID != "rpg_village") return; + Tap(Key.E); Next(); return; + case 12: + Check(Quest("apothecary").Status == QuestStatus.Progress, "herbalist requests salve"); + View.PlacePlayer(17, 13, RpgDirection.Up); + Tap(Key.E); Next(); return; + case 13: + if (CraftWindow() == null) return; + resinBefore = Bag.CheckCount("rpg_plus/resin"); + CraftRecipe("rpg_plus/salve"); + Next(); return; + case 14: + Check(Bag.CheckCount("rpg_plus/salve") == 1, "crafting UI creates the salve"); + Check(Bag.CheckCount("rpg_plus/resin") == resinBefore - 1, "salve consumes one resin"); + Check(RpgInput.IsWalkBlocked(this), "crafting overlay blocks movement"); + Shot("crafting"); + Tap(Key.Escape); Next(); return; + case 15: + if (CraftWindow() != null) return; + View.PlacePlayer(2, 12, RpgDirection.Right); + choices.Enqueue(0); + Tap(Key.E); Next(); return; + case 16: + Check(Quest("apothecary").Status == QuestStatus.End, "salve delivery finishes the first side quest"); + Check(Bag.CheckCount("rpg_plus/salve") == 0, "delivery consumes the salve"); + Go("rpg_forest", 5, 14, RpgDirection.Up); Next(); return; + case 17: + if (View?.Map.ID != "rpg_forest") return; + Tap(Key.E); Next(); return; + case 18: + Check(Flag("rpg_courier_found") == "1", "courier provides a note"); + Check(Quest("courier").Data.Status == 1, "courier journal points back to the herbalist"); + Go("rpg_village", 2, 12, RpgDirection.Right); Next(); return; + case 19: + if (View?.Map.ID != "rpg_village") return; + Tap(Key.E); Next(); return; + case 20: + Check(Quest("courier").Status == QuestStatus.End, "courier delivery closes the quest chain"); + Go("rpg_elder_house", 6, 4, RpgDirection.Up); Next(); return; + case 21: + if (View?.Map.ID != "rpg_elder_house") return; + Tap(Key.E); Next(); return; + case 22: + Check(Quest("ward").Data.Status == 2, "elder supplies the lantern recipe objective"); + Go("rpg_village", 17, 13, RpgDirection.Up); Next(); return; + case 23: + if (View?.Map.ID != "rpg_village") return; + Tap(Key.E); Next(); return; + case 24: + if (CraftWindow() == null) return; + CraftRecipe("rpg_plus/lantern"); Next(); return; + case 25: + Check(Bag.CheckCount("rpg_plus/ward_lantern") == 1 && Bag.CheckCount("rpg_plus/amber") == 0, "lantern consumes amber and appears in the bag"); + CraftRecipe("rpg_plus/mantle"); Next(); return; + case 26: + Check(Bag.CheckCount("rpg_plus/fur_mantle") == 1, "loot also crafts usable equipment"); + var defence = StatsService.Sheet("rpg_hero").Get("def"); + Check(EquipmentService.Equip("rpg_hero", "rpg_plus/fur_mantle"), "crafted mantle equips"); + Check(StatsService.Sheet("rpg_hero").Get("def") > defence, "crafted equipment changes combat stats"); + Check(EquipmentService.ItemIn("rpg_hero", "body") == "rpg_plus/fur_mantle", "mantle uses the body slot before saving"); + Tap(Key.Escape); Next(); return; + case 27: + if (CraftWindow() != null) return; + Go("rpg_forest", 25, 8, RpgDirection.Right); Next(); return; + case 28: + if (View?.Map.ID != "rpg_forest") return; + Tap(Key.E); Next(); return; + case 29: + if (View?.Map.ID != "rpg_woodland_shrine") return; + CheckTrees(); + Check(!View.CanEnter(new Vector2I(12, 4)) && !View.CanEnter(new Vector2I(10, 4)) && !View.CanEnter(new Vector2I(13, 4)), "altar base and stone piers block walking"); + Check(!View.CanEnter(new Vector2I(12, 5)) && View.CanEnter(new Vector2I(12, 6)), "beacon is solid with a reachable interaction cell"); + Check(Quest("ward").Data.Status == 3, "cave leads to the shrine and next journal stage"); + View.PlacePlayer(12, 6, RpgDirection.Up); + choices.Enqueue(pact ? 1 : 0); + Tap(Key.E); Next(); return; + case 30: + Check(Flag("rpg_ward_choice") == (pact ? "pact" : "free"), "chosen ending reaches state"); + Check(Bag.CheckCount("rpg_plus/ward_lantern") == 0, "shrine consumes exactly one lantern"); + Check(View.FindEvent("shrine_beacon").Current.Light.Radius == 6, "event page relights the shrine"); + Check(View.FindEvent("shrine_beacon").Current.Tile == (pact ? 36 : 38), "ending changes the beacon appearance"); + Shot("shrine"); + savedResin = Bag.CheckCount("rpg_plus/resin"); + SaveService.Save(SaveSlot); + GameSession.OpenMainMenu(GetTree()); Next(); return; + case 31: + if (RpgWorldHost.Current != null) return; + Check(GameSession.LoadAndEnter(this, SaveSlot), "woodland save loads"); + Next(); return; + case 32: + if (View?.Map.ID != "rpg_woodland_shrine") return; + Check(Flag("rpg_ward_done") == "1", "shrine choice survives save/load"); + Check(Flag("rpg_ward_choice") == (pact ? "pact" : "free"), "specific ending survives save/load"); + Check(View.FindEvent("shrine_beacon").Current.Tile == (pact ? 36 : 38), "beacon appearance survives save/load"); + Check(Quest("ward").Data.Status == 4, "journal stage survives save/load"); + Check(Bag.CheckCount("rpg_plus/resin") == savedResin, "crafted inventory survives save/load"); + Check(EquipmentService.ItemIn("rpg_hero", "body") == "rpg_plus/fur_mantle", "crafted gear survives save/load"); + Go("rpg_elder_house", 6, 4, RpgDirection.Up); Next(); return; + case 33: + if (View?.Map.ID != "rpg_elder_house") return; + paidCoins = Bag.CheckCount("money/coin"); + Tap(Key.E); Next(); return; + case 34: + Check(Quest("ward").Status == QuestStatus.End, "main quest completes"); + Check(Bag.CheckCount("money/coin") == paidCoins + 35, "elder pays the promised reward"); + paidCoins = Bag.CheckCount("money/coin"); + Tap(Key.E); Next(); return; + case 35: + Check(Bag.CheckCount("money/coin") == paidCoins, "repeat interaction cannot duplicate the reward"); + SaveService.DeleteSave(SaveSlot); + Finish(); return; + } + } + + private bool AdvanceDialog() + { + if (Box?.IsRunning != true) return false; + if (Box.IsWaitingAnswer) Box.TrySelectVariant(choices.Count > 0 ? choices.Dequeue() : 0); + else Box.NextAction(); + return true; + } + + private bool Win(params string[] ids) + { + if (GetTree().CurrentScene is not BattleSceneContext battle) + return false; + if (battle.IsDestroyed) + return true; + if (battle.ActiveBattleCore is not IBattleAutoPlayable auto || !auto.IsWaitingForPlayerInput || Time.GetTicksMsec() < nextAttack) + return false; + var enemies = battle.FindChild("FFEnemyPanel", true, false) as FFEnemyPanel; + var players = battle.FindChild("FFPlayerPanel", true, false) as FFPlayerPanel; + var menu = battle.FindChildren("*", "", true, false).OfType().FirstOrDefault(); + if (enemies == null || players == null || menu == null) return false; + if (testedBattle != battle) + { + testedBattle = battle; + victoryShot = ids.Contains("rpg_wolf") ? "wolf_loot" : "slime_loot"; + Check(enemies.Widgets.Select(w => w.Card.ID).OrderBy(id => id).SequenceEqual(ids.OrderBy(id => id)), "battle contains the expected monsters"); + foreach (var enemy in enemies.Widgets) + { + enemy.Card.Attributes.Defence.Value = 0; + enemy.Card.Attributes.Health.Value = 1; + } + } + var target = enemies.Widgets.FirstOrDefault(w => !w.IsDead); + var player = players.Widgets.FirstOrDefault(w => !w.IsDead); + if (target == null || player == null) return false; + player.Card.Attributes.Point.Value = 100; + Click(player); + menu.SelectedIndex = menu.Entries.FindIndex(e => e.Key == "attack"); + menu.ConfirmSelection(); + menu.SelectedIndex = menu.Entries.FindIndex(e => e.Data is Cthangover.Core.Characters.ActionCharacter action && action.Type == Cthangover.Core.Characters.ActionCharacterType.ToEnemy); + menu.ConfirmSelection(); + Click(target); + nextAttack = Time.GetTicksMsec() + 1600; + return false; + } + + private static void Click(Control control) + => control.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true }); + + private bool ContinueVictory() + { + if (GetTree().Root.FindChild("ContinueButton", true, false) is not Button button || !button.IsVisibleInTree()) return false; + Shot(victoryShot); + button.EmitSignal(BaseButton.SignalName.Pressed); + return true; + } + + private static void Go(string map, int x, int y, RpgDirection dir) + => RpgWorldHost.Teleport(new RpgTeleport { Map = map, X = x, Y = y, Dir = dir }); + + private static CraftPanel CraftWindow() + { + var host = UiHost.Instance; + if (host == null || !host.TryGetInstance(CraftPanel.EntryId, out var node) || !node.IsVisibleInTree()) + return null; + return node as CraftPanel; + } + + private void CraftRecipe(string id) + { + var panel = CraftWindow(); + foreach (var row in panel.GetNode("Panel/Margin/Root/Body/Scroll/Recipes").GetChildren().OfType()) + if (row.RecipeId == id) + { + row.EmitSignal(BaseButton.SignalName.Pressed); + break; + } + var button = panel.GetNode