Compare commits

..
2 Commits
68 changed files with 3289 additions and 19 deletions

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ obj/
gen/
obj/
bin/
art_sources/
*.tres~
*.tscn~
+60
View File
@@ -0,0 +1,60 @@
# Desert expansion
From the village, walk west along the existing horizontal road to cell (0,9).
This enters `rpg_desert` at (38,12). Its eastern edge at (39,12) returns to
village (1,9); the landing cells are outside the trigger cells.
The 40x24 map uses the separate `desert` tileset: sand and rocky surface patches,
an adobe house, a caravan shelter, a ruined tower, date palms, cacti and scrub.
Buildings are complete multi-cell compositions. Their occupied footprints
block movement; no interior map is provided. Door/notice interactions are at
the front of the buildings.
Five visible monsters use two new character templates:
`rpg_desert_scorpion` and `rpg_desert_beetle`. Interact with E/F to fight them.
Victories grant coins and small resources through normal battle loot and set
per-monster flags; defeated monsters stay gone across save/load.
Original barrel tiles were converted to actor events in the village and
elder's house. Two more supply barrels are beside village buildings and
three are in the desert. Every barrel has independent closed/open pages,
its own saved flag, a distinct open sprite, and one-time small rewards.
Reopening only displays the empty message. Existing chests are unchanged.
Barrel definitions: `map_events/<map>/barrels.json` and desert `events.json`.
## Art choices
The map uses the `desert` tileset (built-in image_gen source). Two alternative
ComfyUI renders with identical logical ids (`qwen_image_21_clean_00110_.png`,
`qwen_image_21_clean_00111_.png`) were used by no map and were removed from the
mod on 2026-10-29; their installed tilesets are archived in
`../art_sources/cthangover/village_pack/retired_tilesets_2026-10-29/`, and
`build_desert_v1.cjs` still builds them into `gen/desert_v1/<name>/` for comparison.
Installing one again as a tileset swaps the desert art without moving buildings or
changing event/collision coordinates.
The original ComfyUI PNGs and embedded workflows/prompts are archived under
`gen/desert_v1/`. Background extraction used built-in image_gen; its separate
outputs and exact extraction prompts are beside the originals. Full-resolution
selected sheets and individual alternative objects live in
`../art_sources/cthangover/village_pack/art_sources/desert_qwen110/` and `desert_qwen111/`.
The main source sheets are in `art_sources/desert_v1/`.
Prompts for the main generation are in `comfy/rpg_life/desert_v1_prompts.json`.
`comfy/rpg_life/build_desert_v1.cjs` assembles images, charsets, metadata and the
authored content (Node with `sharp` required). It also writes
`gen/desert_v1/map_preview.png` and `variants_preview.png`.
It is an authoring builder: rerunning restores its authored map placements.
It does not modify game collection definitions.
## Verification
`--itest=desert` / TestRunner `--test desert` checks one-time barrel rewards,
save/load, both transitions by real movement, both monster battles and loot,
and persistent defeated-monster/open-barrel states. Optional
`--shots=<directory>` captures the desert in a windowed run.
The test uses the reserved save slot `slot_9918` and removes it on success.
2026-10-28 host verification: desert 29/29, rpg_walk 34/34,
resource_atlas_rpg_plus 39/39, locale_atlas 89/89 (zero locale errors).
The existing shutdown RID diagnostics are separate from those passing assertions.
+43
View File
@@ -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/<map>/*.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=<dir>` also captures the forest and both monster loot screens.
+75
View File
@@ -0,0 +1,75 @@
{
"Kind": "character",
"Items": [
{
"Id": "rpg_desert_scorpion",
"Name": "rpg_plus/desert/enemy/scorpion/name",
"Image": "characters/desert/scorpion",
"Behaviour": "base",
"Level": 2,
"Point": 1,
"Health": 16,
"Defence": 0,
"Attack": 4,
"Exp": 8,
"RecruitmentChance": 0,
"Actions": [
"physics/attack"
],
"Stats": {
"str": 4,
"vit": 3,
"agi": 6
},
"Loot": [
{
"ItemId": "money/coin",
"MinCount": 2,
"MaxCount": 5,
"Probability": 100
},
{
"ItemId": "rpg_demo/stones",
"MinCount": 1,
"MaxCount": 2,
"Probability": 100
}
]
},
{
"Id": "rpg_desert_beetle",
"Name": "rpg_plus/desert/enemy/beetle/name",
"Image": "characters/desert/beetle",
"Behaviour": "base",
"Level": 1,
"Point": 1,
"Health": 12,
"Defence": 1,
"Attack": 3,
"Exp": 5,
"RecruitmentChance": 0,
"Actions": [
"physics/attack"
],
"Stats": {
"str": 3,
"vit": 3,
"agi": 3
},
"Loot": [
{
"ItemId": "money/coin",
"MinCount": 2,
"MaxCount": 5,
"Probability": 100
},
{
"ItemId": "rpg_plus/resin",
"MinCount": 1,
"MaxCount": 2,
"Probability": 100
}
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+115
View File
@@ -0,0 +1,115 @@
# RPG Plus — a woodland RPG built from mods
RPG Plus is a complete small RPG: a visual-novel prologue where you choose who your hero was, then a tile-map village and forest, a mystery about the forest's lost light, side quests, a workbench, a merchant, a desert to the west and turn-based battles. The game is made of data files and scenarios only; its rules — stats, gear, checks, battles, crafting — come from separate mods that any other game can use too.
Start it from **Play → RPG Plus → New game**.
![The crossroads prologue: a signpost at a forest road and the question "Who were you before you came here?" with three answers: a soldier, a mage's apprentice, a wanderer.](assets/prologue.jpg)
## The story and how to play
1. **The crossroads.** Choose your past: a **soldier** (warrior), a **mage's apprentice** (mage) or a **wanderer** (rogue). The choice sets your stats and your starting gear. You start with 25 coins.
2. **The village by the Grey Forest.** Walk with WASD or the arrows, run with Shift, use E or F to talk and interact. The **journal** always lists your concrete next step.
3. **The elder** asks about the missing light or sends you straight after the wolf at the eastern cave.
4. **The forest.** Gather brushwood and mushrooms; slimes drop living resin and coins. The wolf always drops an amber heart and enough fur for a mantle.
5. **The well workbench.** Make a woodland salve (mushroom + resin), a warden mantle (2 fur + 2 resin) or the ward lantern (amber + 2 resin + 2 branches). Put the mantle on from the bag: it changes your real battle numbers.
6. **The herbalist** by the western fence needs salve; helping her leads to a missing courier in the southern forest.
7. **The shrine.** Take the lantern through the cave behind the wolf to the ruined shrine. Return the light to the forest, or strike a pact for the village. Both endings finish the adventure, and the elder's last words follow your choice.
Other people in the village: the **trainer** takes you to the training yard (experience once, and a strength check to lift a log), and the **seer** reads your luck. The hearth by the well restores health and mana. Rewards cannot be collected twice.
![The well workbench with three recipes: Woodland salve, Warden mantle and Ward lantern, the ingredients you have and need, and the Craft button.](assets/workbench.jpg)
![The ruined shrine in the forest, "The forest remembers", the hero carrying the lantern.](assets/shrine.jpg)
### The desert
From the village, follow the main road west off the edge of the map to reach **the Dry March**: sand, an adobe house, a caravan shelter, a ruined tower, palms and cacti. Five scorpions and beetles wait to be fought, and supply barrels hold small one-time rewards. The east edge leads back to the village.
![The Dry March: sand with a caravan shelter, cacti, palms, barrels, scorpions and beetles; the hero arrives along the road from the east.](assets/desert.jpg)
## Your hero
Open the **character sheet** from the toolbar to see your class, level and experience, attributes (Strength, Agility, Intelligence, Vitality, Charisma, Luck), combat values and gear in the weapon, body and accessory slots. Equip things from the bag.
![The character sheet of a level-2 rogue: attributes, health and mana, attack with a +3 bonus from gear, empty equipment slots.](assets/character-sheet.jpg)
Battles use the command-style (FF) battle with damage computed by formulas. Some dialog options test an attribute against a d20 roll, such as the trainer's **[Strength 12]**.
![A battle against the wolf: the hero at the bottom left, the wolf with its health, attack and defence at the top, the battle log on the right.](assets/battle.jpg)
## How the game is assembled
RPG Plus is the collection **RPG Plus**. Its blocks:
```text
Story rpg_plus_game (this mod)
Village and forest rpg_demo_game: the maps, people and items this game extends
Assets and rules village_pack (art), classic_rpg_pack (stats, classes, gear)
Stats and checks stats_base, stat_checks
Tile world rpg
Items, gear, trade items_base, equipment, trading, crafting
2D engine shell2d_base, dialog_base, characters_base, audio_base
Battle turn_battle_base, ff_battle, battle_formulas
Card battle card_battle (optional)
Interface ui_base, menu_base
Tile map editor rpg_editor (optional)
Data editors schema_tools, items_editor, characters_editor (optional)
Tile renderer tile_topdown_base, tile_isometric_base
```
Settings of the collection: `battle.core` is `ff_battle`, checks roll `d20`, the hero's map sprite is `village/hero`, maps are drawn top-down. Change them in **Mods & Collections → Collections → RPG Plus → Edit scheme**.
## For authors: change it without code
```filestree
rpg_plus_game/
├── scenarios/rpg_plus/ prologue, training_yard, events/ (trainer, seer), woodland/, desert/
├── map_events/<map>/ new people, the workbench, cave door, barrels; same ids replace rpg_demo_game's
├── maps/ rpg_woodland_shrine.json, rpg_desert.json
├── scenes/ crossroads, training yard, shrine, desert
├── quests/woodland.json journal stages
├── items/, recipes/, stations/ materials, crafted things, the workbench's menu
├── patches/character.json monster loot added to rpg_demo_game's monsters
├── patches/shop.json more goods for the village merchant
└── characters/, charsets/, backgrounds/, locale/
```
**Extend another mod instead of copying it.** The village, house and forest still come from `rpg_demo_game`. An event with the same id as one there replaces it (the elder gets new dialogs); new ids add people. Patches add loot to its monsters and goods to its shop.
**Checks and conditions in scenarios.** The seer tests Luck and then looks at your gear:
```scenario
text "— Want to know what awaits you? Give me your hand. Not that one — the one you don't steal with." key=rpg_plus/seer/hello
check luk 14 who=rpg_hero else=fail -> :ok
:ok
text "— The wolf at the cave fears fire more than steel. And strike where it has no shield. That part is free. The rest isn't." key=rpg_plus/seer/fortune
when "!equipped(rpg_hero, 'classic/clover_charm')" -> :charm
end
```
`when "item('rpg_plus/salve') >= 1"` checks the bag, `level(rpg_hero) >= 3` the hero's level; flags make one-time rewards safe. Lines are written in Russian with a `key=` each; the translations are in `locale/scenarios_<lang>.properties`.
**A recipe** is data in `recipes/woodland.json`:
```json
{
"Id": "rpg_plus/salve",
"Name": "rpg_plus/recipe/salve/name",
"Type": "Cooking",
"Input": [
{ "Id": "rpg_demo/mushroom", "Count": 1 },
{ "Id": "rpg_plus/resin", "Count": 1 }
],
"Output": [{ "Id": "rpg_plus/salve", "Count": 1 }]
}
```
The optional **tile map editor** paints ground, collision and events, and the **Data Editor** edits items, recipes and characters. Turn them on in the game's scheme.
## If something goes wrong
1. **The wolf's amber is missing in an old save.** Go to the wolf's spot by the cave and press E/F: the amber can be picked up there once if you have neither it nor the lantern.
2. **Not sure what to do next.** Open the journal.
3. **A dialog option is missing.** Some options appear only after a quest stage or with a certain item.
+115
View File
@@ -0,0 +1,115 @@
# RPG Plus — un RPG de bosque construido con mods
RPG Plus es un RPG pequeño y completo: un prólogo tipo novela visual donde eliges quién era tu héroe, después una aldea y un bosque sobre mapa de tiles, un misterio sobre la luz perdida del bosque, misiones secundarias, una mesa de trabajo, un mercader, un desierto al oeste y combates por turnos. El juego está hecho solo de archivos de datos y escenarios; sus reglas — atributos, equipo, pruebas, combates, crafteo — vienen de mods separados que cualquier otro juego también puede usar.
Se juega desde **Jugar → RPG Plus → Nueva partida**.
![El prólogo del cruce de caminos: un poste indicador en un camino forestal y la pregunta "¿Quién eras antes de llegar aquí?" con tres respuestas: un soldado, un aprendiz de mago, un vagabundo.](assets/prologue.jpg)
## La historia y cómo jugar
1. **El cruce de caminos.** Elige tu pasado: **soldado** (guerrero), **aprendiz de mago** (mago) o **vagabundo** (pícaro). La elección fija tus atributos y tu equipo inicial. Empiezas con 25 monedas.
2. **La aldea junto al Bosque Gris.** Camina con WASD o las flechas, corre con Shift, usa E o F para hablar e interactuar. El **diario** siempre anota tu siguiente paso concreto.
3. **El anciano** pregunta por la luz desaparecida o te manda directamente tras el lobo de la cueva del este.
4. **El bosque.** Recoge leña y setas; los limos sueltan resina viva y monedas. El lobo siempre suelta un corazón de ámbar y suficiente pelaje para un manto.
5. **La mesa de trabajo del pozo.** Prepara una pomada del bosque (seta + resina), un manto del guardián (2 pelajes + 2 resinas) o el farol guardián (ámbar + 2 resinas + 2 ramas). Ponte el manto desde la bolsa: cambia tus números reales de combate.
6. **La herborista**, junto a la valla occidental, necesita pomada; ayudarla lleva a un correo desaparecido en el bosque del sur.
7. **El santuario.** Lleva el farol por la cueva que está tras el lobo hasta el santuario en ruinas. Devuélvele la luz al bosque, o pacta por la aldea. Ambos finales cierran la aventura, y las últimas palabras del anciano siguen tu elección.
Otra gente de la aldea: el **instructor** te lleva al patio de entrenamiento (experiencia una vez, y una prueba de Fuerza para alzar un tronco), y la **adivina** lee tu suerte. El hogar junto al pozo restaura salud y maná. Las recompensas no se pueden cobrar dos veces.
![La mesa de trabajo del pozo con tres recetas: Pomada del bosque, Manto del guardián y Farol guardián, los ingredientes que tienes y necesitas, y el botón de crear.](assets/workbench.jpg)
![El santuario en ruinas del bosque, "El bosque recuerda", el héroe cargando el farol.](assets/shrine.jpg)
### El desierto
Desde la aldea, sigue el camino principal hacia el oeste hasta salir del borde del mapa y llegarás a **la Marca Seca**: arena, una casa de adobe, un refugio de caravana, una torre en ruinas, palmeras y cactus. Cinco escorpiones y escarabajos esperan pelea, y los barriles de provisiones guardan pequeñas recompensas de una sola vez. El borde este devuelve a la aldea.
![La Marca Seca: arena con un refugio de caravana, cactus, palmeras, barriles, escorpiones y escarabajos; el héroe llega por el camino desde el este.](assets/desert.jpg)
## Tu héroe
Abre la **hoja de personaje** desde la barra de herramientas para ver tu clase, nivel y experiencia, los atributos (Fuerza, Agilidad, Inteligencia, Vitalidad, Carisma, Suerte), los valores de combate y el equipo en las ranuras de arma, cuerpo y accesorio. Equípate desde la bolsa.
![La hoja de personaje de un pícaro de nivel 2: atributos, salud y maná, ataque con un bono de +3 del equipo, ranuras de equipo vacías.](assets/character-sheet.jpg)
Los combates usan el estilo de órdenes (FF) con daño calculado por fórmulas. Algunas opciones de diálogo enfrentan un atributo a una tirada de d20, como el **[Fuerza 12]** del instructor.
![Un combate contra el lobo: el héroe abajo a la izquierda, el lobo con su salud, ataque y defensa arriba, el registro de combate a la derecha.](assets/battle.jpg)
## Cómo se monta el juego
RPG Plus es la colección **RPG Plus**. Sus bloques:
```text
Historia rpg_plus_game (este mod)
Aldea y bosque rpg_demo_game: los mapas, la gente y los objetos que este juego amplía
Recursos y reglas village_pack (arte), classic_rpg_pack (atributos, clases, equipo)
Atributos y pruebas stats_base, stat_checks
Mundo de tiles rpg
Objetos, equipo, comercio items_base, equipment, trading, crafting
Motor 2D shell2d_base, dialog_base, characters_base, audio_base
Combate turn_battle_base, ff_battle, battle_formulas
Combate de cartas card_battle (opcional)
Interfaz ui_base, menu_base
Editor de mapas de tiles rpg_editor (opcional)
Editores de datos schema_tools, items_editor, characters_editor (opcional)
Render de tiles tile_topdown_base, tile_isometric_base
```
Ajustes de la colección: `battle.core` es `ff_battle`, las pruebas tiran `d20`, el sprite del héroe en el mapa es `village/hero`, los mapas se dibujan de arriba a abajo. Se cambian en **Mods & Collections → Colecciones → RPG Plus → Editar esquema**.
## Para autores: cámbialo sin código
```filestree
rpg_plus_game/
├── scenarios/rpg_plus/ prólogo, training_yard, events/ (instructor, adivina), woodland/, desert/
├── map_events/<map>/ gente nueva, la mesa de trabajo, la puerta de la cueva, barriles; los mismos id sustituyen a los de rpg_demo_game
├── maps/ rpg_woodland_shrine.json, rpg_desert.json
├── scenes/ cruce de caminos, patio de entrenamiento, santuario, desierto
├── quests/woodland.json etapas del diario
├── items/, recipes/, stations/ materiales, objetos fabricados, el menú de la mesa de trabajo
├── patches/character.json botín añadido a los monstruos de rpg_demo_game
├── patches/shop.json más mercancías para el mercader de la aldea
└── characters/, charsets/, backgrounds/, locale/
```
**Amplía el mod de otro en vez de copiarlo.** La aldea, la casa y el bosque siguen viniendo de `rpg_demo_game`. Un evento con el mismo id que uno de allí lo sustituye (el anciano recibe diálogos nuevos); los id nuevos añaden gente. Los parches añaden botín a sus monstruos y mercancías a su tienda.
**Pruebas y condiciones en los escenarios.** La adivina prueba la Suerte y luego mira tu equipo:
```scenario
text "—¿Quieres saber qué te espera? Dame la mano. Esa no: la que no usas para robar." key=rpg_plus/seer/hello
check luk 14 who=rpg_hero else=fail -> :ok
:ok
text "—El lobo de la cueva teme más al fuego que al acero. Y golpea donde no tenga escudo. Esto es gratis. Lo demás, no." key=rpg_plus/seer/fortune
when "!equipped(rpg_hero, 'classic/clover_charm')" -> :charm
end
```
`when "item('rpg_plus/salve') >= 1"` mira la bolsa, `level(rpg_hero) >= 3` el nivel del héroe; las banderas hacen seguras las recompensas de una sola vez. Las líneas están escritas en ruso con un `key=` cada una; las traducciones están en `locale/scenarios_<lang>.properties`.
**Una receta** son datos en `recipes/woodland.json`:
```json
{
"Id": "rpg_plus/salve",
"Name": "rpg_plus/recipe/salve/name",
"Type": "Cooking",
"Input": [
{ "Id": "rpg_demo/mushroom", "Count": 1 },
{ "Id": "rpg_plus/resin", "Count": 1 }
],
"Output": [{ "Id": "rpg_plus/salve", "Count": 1 }]
}
```
El **editor de mapas de tiles** opcional pinta suelo, colisiones y eventos, y el **Editor de datos** edita objetos, recetas y personajes. Actívalos en el esquema del juego.
## Si algo falla
1. **Falta el ámbar del lobo en una partida guardada antigua.** Ve al punto del lobo junto a la cueva y pulsa E/F: el ámbar se puede recoger allí una vez si no tienes ni él ni el farol.
2. **No sabes qué hacer ahora.** Abre el diario.
3. **Falta una opción de diálogo.** Algunas opciones aparecen solo tras una etapa de misión o con cierto objeto.
+115
View File
@@ -0,0 +1,115 @@
# RPG Plus — MOD で組み上げた森の RPG
RPG Plus は完成された小規模 RPG です。まず自分のヒーローの過去を選ぶビジュアルノベル形式のプロローグ、続いてタイルマップの村と森、森から消えた光の謎、サブクエスト、作業台、商人、西の砂漠、そしてターン制バトル。ゲームはデータファイルとシナリオだけでできており、ルール — ステータス、装備、判定、戦闘、クラフト — は別々の MOD が担います。どの MOD も他のゲームでも使えます。
開始は **プレイ → RPG Plus → ニューゲーム** から。
![岐路のプロローグ:森の道の道標と、「ここへ来る前、きみは何者だった?」という問い。答えは三つ:兵士、魔法使いの弟子、流浪人。](assets/prologue.jpg)
## 物語と遊び方
1. **岐路。** 過去を選びます:**兵士**(ウォリアー)、**魔法使いの弟子**(メイジ)、**流浪人**(ローグ)。選択がステータスと初期装備を決めます。所持金は 25 硬貨から。
2. **灰色の森のそばの村。** WASD か矢印キーで歩き、Shift で走り、E か F で話しかけ・調べます。**ジャーナル**は常に、次の具体的な一歩を示してくれます。
3. **村長**は消えた光について尋ねてくるか、東の洞窟の狼のもとへすぐに送り出します。
4. **森。** 柴とキノコを集めます。スライムは生きた樹脂と硬貨を落とします。狼は必ず琥珀の心臓と、マント一枚ぶんの毛皮を落とします。
5. **井戸の作業台。** 森の軟膏(キノコ + 樹脂)、森の番人のマント(毛皮 2 + 樹脂 2)、護りの灯籠(琥珀 + 樹脂 2 + 柴 2)を作れます。マントはインベントリから装備します。実際の戦闘の数値が変わります。
6. 西の柵のそばの**薬草師**は軟膏を求めています。手伝うと、南の森で行方不明になった伝令へとつながります。
7. **祠。** 灯籠を持って、狼の奥の洞窟を抜け、荒れた祠へ。光を森に返すか、村のために契りを交わすか。どちらの結末でも冒険は終わり、村長の最期の言葉はあなたの選択に従います。
村のその他の人々:**師匠**は稽古場へ連れて行ってくれ(経験値は一度、丸太を持ち上げる力の判定つき)、**占い師**はあなたの運を読みます。井戸のそばの囲み炉は体力とマナを回復します。報酬は二度は受け取れません。
![井戸の作業台と三つのレシピ:森の軟膏、森の番人のマント、護りの灯籠。持っている材料と必要な材料、そして作成ボタン。](assets/workbench.jpg)
![森の荒れた祠、「森は覚えている」、灯籠を捧げ持つヒーロー。](assets/shrine.jpg)
### 砂漠
村から大通りを西へ、マップの端を出ていくと**乾いた辺境**に着きます。砂、日干しレンガの家、隊商の休憩小屋、崩れた塔、ヤシとサボテン。サソリと甲虫が五匹戦いを待ち、補給の樽には小さな一度きりの報酬が入っています。東の端から村へ戻れます。
![乾いた辺境:隊商の休憩小屋のある砂地、サボテン、ヤシ、樽、サソリと甲虫。ヒーローは東の道からやって来ます。](assets/desert.jpg)
## あなたのヒーロー
ツールバーから**キャラクターシート**を開くと、クラス、レベルと経験値、属性(力、敏捷、知力、体力、カリスマ、幸運)、戦闘値、武器・体・装飾品のスロットの装備が見られます。装備はインベントリから着けます。
![レベル 2 のローグのキャラクターシート:属性、体力とマナ、装備による +3 の攻撃ボーナス、空き装備スロット。](assets/character-sheet.jpg)
戦闘はコマンド式(FF)バトルで、ダメージは数式で計算されます。会話の選択肢の一部は属性を d20 ロールで判定します。師匠の **[力 12]** がその例です。
![狼との戦闘:左下にヒーロー、上に体力・攻撃・防御つきの狼、右に戦闘ログ。](assets/battle.jpg)
## ゲームの構成
RPG Plus の正体はコレクション **RPG Plus** です。そのブロック:
```text
物語 rpg_plus_game(この MOD)
村と森 rpg_demo_game:このゲームが拡張するマップ、人、アイテム
素材とルール village_pack(アート)、classic_rpg_pack(ステータス、クラス、装備)
ステータスと判定 stats_base, stat_checks
タイル世界 rpg
アイテム、装備、交易 items_base, equipment, trading, crafting
2D エンジン shell2d_base, dialog_base, characters_base, audio_base
戦闘 turn_battle_base, ff_battle, battle_formulas
カードバトル card_battle(任意)
インターフェース ui_base, menu_base
タイルマップエディター rpg_editor(任意)
データエディター schema_tools, items_editor, characters_editor(任意)
タイルレンダラー tile_topdown_base, tile_isometric_base
```
コレクションの設定:`battle.core` は `ff_battle`、判定は `d20` をロール、ヒーローのマップスプライトは `village/hero`、マップはトップダウン描画。**Mods & Collections → コレクション → RPG Plus → 構成を編集** から変えられます。
## 作者向け:コードなしで変える
```filestree
rpg_plus_game/
├── scenarios/rpg_plus/ プロローグ、training_yard、events/(師匠、占い師)、woodland/、desert/
├── map_events/<map>/ 新しい人々、作業台、洞窟の扉、樽。同じ id は rpg_demo_game のものを置き換える
├── maps/ rpg_woodland_shrine.json、rpg_desert.json
├── scenes/ 岐路、稽古場、祠、砂漠
├── quests/woodland.json ジャーナルの段階
├── items/, recipes/, stations/ 素材、作成品、作業台のメニュー
├── patches/character.json rpg_demo_game のモンスターに追加されたドロップ
├── patches/shop.json 村の商人の商品の追加
└── characters/, charsets/, backgrounds/, locale/
```
**コピーせず、他の MOD を拡張する。** 村も家も森も今も `rpg_demo_game` から来ます。そこと同じ id のイベントはそれを置き換え(村長は新しい会話を得る)、新しい id は人を増やします。パッチはモンスターのドロップと店の商品を足します。
**シナリオの判定と条件。** 占い師は幸運を試し、それからあなたの装備を見ます:
```scenario
text "「この先を知りたいかい?手をお出し。そっちじゃない——盗みに使わないほうの手だよ」" key=rpg_plus/seer/hello
check luk 14 who=rpg_hero else=fail -> :ok
:ok
text "「洞窟の狼は鋼より火を恐れる。それと盾のないところを打ちな。ここまでは無料。残りは有料だよ」" key=rpg_plus/seer/fortune
when "!equipped(rpg_hero, 'classic/clover_charm')" -> :charm
end
```
`when "item('rpg_plus/salve') >= 1"` はインベントリを、`level(rpg_hero) >= 3` はヒーローのレベルを見ます。フラグのおかげで、一度きりの報酬は安全です。台詞はロシア語で書かれ、各行に `key=` が付きます。翻訳は `locale/scenarios_<lang>.properties` にあります。
**レシピ**は `recipes/woodland.json` のデータです:
```json
{
"Id": "rpg_plus/salve",
"Name": "rpg_plus/recipe/salve/name",
"Type": "Cooking",
"Input": [
{ "Id": "rpg_demo/mushroom", "Count": 1 },
{ "Id": "rpg_plus/resin", "Count": 1 }
],
"Output": [{ "Id": "rpg_plus/salve", "Count": 1 }]
}
```
任意の**タイルマップエディター**は地面・衝突・イベントを描き、**データエディター**はアイテム・レシピ・キャラクターを編集します。ゲームの構成で有効にしてください。
## うまくいかないとき
1. **古いセーブに狼の琥珀がない。** 洞窟のそばの狼の場所へ行って E/F を押してください。琥珀も灯籠も持っていなければ、そこで一度だけ拾えます。
2. **次に何をすればいいか分からない。** ジャーナルを開きましょう。
3. **会話の選択肢が出ない。** 一部の選択肢は、特定のクエスト段階の後か、特定のアイテム所持時だけ現れます。
+115
View File
@@ -0,0 +1,115 @@
# RPG Plus — 모드로 지어 올린 숲의 RPG
RPG Plus는 완성된 소규모 RPG입니다. 영웅의 과거를 고르는 비주얼 노벨 프롤로그, 이어서 타일맵의 마을과 숲, 숲에서 사라진 빛의 미스터리, 사이드 퀘스트, 작업대, 상인, 서쪽의 사막, 그리고 턴제 전투. 게임은 데이터 파일과 시나리오만으로 이루어져 있고, 규칙 — 스탯, 장비, 판정, 전투, 제작 — 은 별개의 모드가 담당합니다. 어떤 모드든 다른 게임에서도 쓸 수 있습니다.
**플레이 → RPG Plus → 새 게임**에서 시작합니다.
![갈림길 프롤로그: 숲길의 이정표와 “여기 오기 전엔 누구였나?”라는 질문, 세 가지 답: 군인, 마법사 견습생, 방랑자.](assets/prologue.jpg)
## 이야기와 진행 방법
1. **갈림길.** 과거를 고르세요: **군인**(전사), **마법사 견습생**(메이지), **방랑자**(로그). 선택이 스탯과 시작 장비를 정합니다. 시작 자금은 동전 25닢.
2. **잿빛 숲 옆 마을.** WASD나 방향키로 걷고, Shift로 뛰고, E나 F로 말하고 상호작용합니다. **저널**은 언제나 다음 구체적 한 걸음을 알려 줍니다.
3. **촌장**은 사라진 빛을 묻거나, 곧장 동쪽 동굴의 늑대에게 보냅니다.
4. **숲.** 땔감과 버섯을 모으세요. 슬라임은 산 수지와 동전을 남깁니다. 늑대는 언제나 호박 심장과 망토 한 벌分의 털을 남깁니다.
5. **우물 작업대.** 숲 연고(버섯 + 수지), 숲 수호자 망토(털 2 + 수지 2), 수호 등불(호박 + 수지 2 + 땔감 2)을 만들 수 있습니다. 망토는 가방에서 착용하세요. 실제 전투 수치가 바뀝니다.
6. 서쪽 울타리 옆 **약초상**은 연고가 필요합니다. 돕다 보면 남쪽 숲에서 실종된 전령으로 이어집니다.
7. **성소.** 등불을 들고 늑대 너머의 동굴을 지나 폐허가 된 성소로 가세요. 숲에 빛을 돌려주거나, 마을을 위해 계약을 맺거나. 두 결말 모두 모험을 끝내고, 촌장의 마지막 말은 당신의 선택을 따릅니다.
마을의 다른 사람들: **훈련관**은 수련장으로 안내해 주고(경험치는 한 번, 통나무 드는 힘 판정 포함), **점쟁이**는 당신의 행운을 읽습니다. 우물 곁 화덕은 체력과 마나를 회복합니다. 보상은 두 번 받을 수 없습니다.
![우물 작업대와 세 레시피: 숲 연고, 숲 수호자 망토, 수호 등불. 가지고 있는 재료와 필요한 재료, 그리고 제작 버튼.](assets/workbench.jpg)
![숲 속 폐허가 된 성소, “숲은 기억한다”, 등불을 든 영웅.](assets/shrine.jpg)
### 사막
마을에서 큰길을 따라 서쪽으로, 맵 끝을 벗어나면 **마른 변경**에 닿습니다. 모래, 흙벽돌 집, 상로막 쉼터, 무너진 탑, 야자수와 선인장. 전갈과 딱정벌레 다섯 마리가 싸움을 기다리고, 보급 통에는 작은 일회성 보상이 들어 있습니다. 동쪽 끝은 다시 마을로 돌아갑니다.
![마른 변경: 상로막 쉼터가 있는 모래사장, 선인장, 야자수, 통, 전갈과 딱정벌레. 영웅은 동쪽 길에서 도착합니다.](assets/desert.jpg)
## 당신의 영웅
툴바에서 **캐릭터 시트**를 열면 클래스, 레벨과 경험치, 속성(힘, 민첩, 지능, 생명력, 매력, 행운), 전투 수치, 그리고 무기·몸·장신구 슬롯의 장비를 볼 수 있습니다. 장비는 가방에서 착용합니다.
![2레벨 로그의 캐릭터 시트: 속성, 체력과 마나, 장비의 +3 보너스가 붙은 공격, 빈 장비 슬롯.](assets/character-sheet.jpg)
전투는 커맨드식(FF) 전투로, 피해는 공식으로 계산됩니다. 일부 대화 선택지는 속성을 d20 굴림으로 판정합니다. 훈련관의 **[힘 12]**가 그 예입니다.
![늑대와의 전투: 왼쪽 아래 영웅, 위에 체력·공격·방어를 가진 늑대, 오른쪽에 전투 기록.](assets/battle.jpg)
## 게임의 구성
RPG Plus의 정체는 컬렉션 **RPG Plus**입니다. 그 블록:
```text
이야기 rpg_plus_game(이 모드)
마을과 숲 rpg_demo_game: 이 게임이 확장하는 맵, 사람, 아이템
에셋과 규칙 village_pack(그림), classic_rpg_pack(스탯, 클래스, 장비)
스탯과 판정 stats_base, stat_checks
타일 세계 rpg
아이템, 장비, 거래 items_base, equipment, trading, crafting
2D 엔진 shell2d_base, dialog_base, characters_base, audio_base
전투 turn_battle_base, ff_battle, battle_formulas
카드 배틀 card_battle(선택)
인터페이스 ui_base, menu_base
타일맵 편집기 rpg_editor(선택)
데이터 편집기 schema_tools, items_editor, characters_editor(선택)
타일 렌더러 tile_topdown_base, tile_isometric_base
```
컬렉션 설정: `battle.core`는 `ff_battle`, 판정은 `d20`을 굴리고, 영웅의 맵 스프라이트는 `village/hero`, 맵은 탑다운으로 그려집니다. **Mods & Collections → 컬렉션 → RPG Plus → 구성 편집**에서 바꿀 수 있습니다.
## 모드 작성자를 위해: 코드 없이 바꾸기
```filestree
rpg_plus_game/
├── scenarios/rpg_plus/ 프롤로그, training_yard, events/(훈련관, 점쟁이), woodland/, desert/
├── map_events/<map>/ 새 사람들, 작업대, 동굴 문, 통. 같은 id는 rpg_demo_game의 것을 대체
├── maps/ rpg_woodland_shrine.json, rpg_desert.json
├── scenes/ 갈림길, 수련장, 성소, 사막
├── quests/woodland.json 저널 단계
├── items/, recipes/, stations/ 재료, 제작품, 작업대 메뉴
├── patches/character.json rpg_demo_game 몬스터에 추가된 전리품
├── patches/shop.json 마을 상인의 물품 추가
└── characters/, charsets/, backgrounds/, locale/
```
**복사하지 말고 다른 모드를 확장하세요.** 마을도 집도 숲도 여전히 `rpg_demo_game`에서 옵니다. 거기와 같은 id의 이벤트는 그것을 대체하고(촌장은 새 대사를 얻음), 새 id는 사람을 더합니다. 패치는 그 몬스터의 전리품과 가게 물품을 더합니다.
**시나리오의 판정과 조건.** 점쟁이는 행운을 시험한 뒤 당신의 장비를 봅니다:
```scenario
text "“무엇이 기다리는지 알고 싶어? 손을 줘. 그거 말고 — 도둑질 안 하는 손.”" key=rpg_plus/seer/hello
check luk 14 who=rpg_hero else=fail -> :ok
:ok
text "“동굴의 늑대는 강철보다 불을 더 무서워해. 그리고 보호막이 없는 데를 쳐. 여기까진 공짜야. 나머진 아니고.”" key=rpg_plus/seer/fortune
when "!equipped(rpg_hero, 'classic/clover_charm')" -> :charm
end
```
`when "item('rpg_plus/salve') >= 1"`은 가방을, `level(rpg_hero) >= 3`은 영웅의 레벨을 봅니다. 플래그 덕분에 일회성 보상은 안전합니다. 대사는 러시아어로 쓰였고 각 줄에 `key=`가 있습니다. 번역은 `locale/scenarios_<lang>.properties`에 있습니다.
**레시피**는 `recipes/woodland.json`의 데이터입니다:
```json
{
"Id": "rpg_plus/salve",
"Name": "rpg_plus/recipe/salve/name",
"Type": "Cooking",
"Input": [
{ "Id": "rpg_demo/mushroom", "Count": 1 },
{ "Id": "rpg_plus/resin", "Count": 1 }
],
"Output": [{ "Id": "rpg_plus/salve", "Count": 1 }]
}
```
선택인 **타일맵 편집기**는 지면·충돌·이벤트를 그리고, **데이터 편집기**는 아이템·레시피·캐릭터를 편집합니다. 게임의 구성에서 켜세요.
## 문제가 생겼을 때
1. **오래된 저장 파일에 늑대의 호박이 없습니다.** 동굴 옆 늑대 자리로 가서 E/F를 누르세요. 호박과 등불이 둘 다 없다면 그곳에서 한 번 주울 수 있습니다.
2. **다음에 뭘 해야 할지 모르겠습니다.** 저널을 여세요.
3. **대화 선택지가 사라졌습니다.** 어떤 선택지는 특정 퀘스트 단계 후에, 또는 특정 아이템이 있을 때만 나타납니다.
+115
View File
@@ -0,0 +1,115 @@
# RPG Plus — лесная RPG, собранная из модов
RPG Plus — законченная небольшая RPG: пролог-новелла, где вы выбираете, кем был ваш герой, затем деревня и лес на тайловой карте, тайна пропавшего лесного света, побочные задания, верстак, торговец, пустыня на западе и пошаговые бои. Игра состоит только из файлов данных и сценариев; правила — характеристики, снаряжение, проверки, бои, крафт — приносят отдельные моды, которые может взять и любая другая игра.
Запуск — **Играть → RPG Plus → Новая игра**.
![Пролог на перекрёстке: указатель у лесной дороги и вопрос «Кем ты был до того, как пришёл сюда?» с тремя ответами: солдат, ученик мага, странник.](assets/prologue.jpg)
## История и как играть
1. **Перекрёсток.** Выберите своё прошлое: **солдат** (воин), **ученик мага** (маг) или **странник** (плут). Выбор задаёт характеристики и стартовое снаряжение. На старте у вас 25 монет.
2. **Деревня у Серого леса.** Ходите на WASD или стрелки, бегите на Shift, говорите и взаимодействуйте на E или F. **Журнал** всегда называет ваш конкретный следующий шаг.
3. **Староста** спрашивает о пропавшем свете или сразу отправляет вас за волком у восточной пещеры.
4. **Лес.** Собирайте хворост и грибы; слизни оставляют живую смолу и монеты. Волк всегда оставляет сердце янтаря и столько шерсти, что хватит на плащ.
5. **Верстак у колодца.** Сварите лесную мазь (гриб + смола), сшейте плащ лесного стража (2 шерсти + 2 смолы) или соберите сторожевой фонарь (янтарь + 2 смолы + 2 веток). Плащ надевается из сумки: он меняет ваши настоящие боевые числа.
6. **Травница** у западного забора просит мазь; помощь ей приводит к пропавшему курьеру в южном лесу.
7. **Святилище.** Пронесите фонарь через пещеру за волком к разрушенному святилищу. Верните лесу свет — или заключите сделку ради деревни. Обе концовки завершают приключение, а последние слова старосты зависят от вашего выбора.
Прочие жители деревни: **наставник** ведёт на тренировочную площадку (опыт один раз и проверка силы с бревном), **гадалка** читает вашу удачу. Очаг у колодца восстанавливает здоровье и ману. Награды нельзя собрать дважды.
![Верстак у колодца с тремя рецептами: Лесная мазь, Плащ лесного стража и Сторожевой фонарь, имеющиеся и нужные ингредиенты и кнопка изготовления.](assets/workbench.jpg)
![Разрушенное святилище в лесу, «Лес помнит», герой несёт фонарь.](assets/shrine.jpg)
### Пустыня
От деревни пройдите по главной дороге на запад, за край карты, — попадёте в **Сухой предел**: песок, глинобитный дом, навес каравана, разрушенная башня, пальмы и кактусы. Пять скорпионов и жуков ждут боя, а бочки припасов хранят небольшие разовые награды. Восточный край ведёт обратно к деревне.
![Сухой предел: песок, навес каравана, кактусы, пальмы, бочки, скорпионы и жуки; герой приезжает по дороге с востока.](assets/desert.jpg)
## Ваш герой
Откройте **лист персонажа** с панели инструментов: класс, уровень и опыт, характеристики (Сила, Ловкость, Интеллект, Выносливость, Харизма, Удача), боевые значения и снаряжение в слотах оружия, брони и аксессуара. Надевайте вещи из сумки.
![Лист персонажа плута второго уровня: характеристики, здоровье и мана, атака с бонусом +3 от снаряжения, пустые слоты экипировки.](assets/character-sheet.jpg)
Бои идут в командном стиле (FF), урон считается по формулам. Часть вариантов диалога проверяет характеристику броском d20 — например, **[Сила 12]** у наставника.
![Бой с волком: герой внизу слева, волк со здоровьем, атакой и защитой вверху, журнал боя справа.](assets/battle.jpg)
## Как игра собрана
RPG Plus — это коллекция **RPG Plus**. Её блоки:
```text
Сюжет rpg_plus_game (этот мод)
Деревня и лес rpg_demo_game: карты, люди и предметы, которые игра расширяет
Ресурсы и правила village_pack (арт), classic_rpg_pack (характеристики, классы, снаряжение)
Характеристики и проверки stats_base, stat_checks
Тайловый мир rpg
Предметы, снаряжение, торговля items_base, equipment, trading, crafting
2D-движок shell2d_base, dialog_base, characters_base, audio_base
Бой turn_battle_base, ff_battle, battle_formulas
Карточный бой card_battle (опционально)
Интерфейс ui_base, menu_base
Редактор тайловых карт rpg_editor (опционально)
Редакторы данных schema_tools, items_editor, characters_editor (опционально)
Тайловый рендер tile_topdown_base, tile_isometric_base
```
Настройки коллекции: `battle.core` — `ff_battle`, проверки бросают `d20`, спрайт героя на карте — `village/hero`, карты рисуются сверху. Меняются в **Mods & Collections → Коллекции → RPG Plus → Изменить схему**.
## Авторам: меняйте без кода
```filestree
rpg_plus_game/
├── scenarios/rpg_plus/ пролог, training_yard, events/ (наставник, гадалка), woodland/, desert/
├── map_events/<map>/ новые люди, верстак, дверь пещеры, бочки; события с теми же id заменяют события rpg_demo_game
├── maps/ rpg_woodland_shrine.json, rpg_desert.json
├── scenes/ перекрёсток, тренировочная площадка, святилище, пустыня
├── quests/woodland.json этапы журнала
├── items/, recipes/, stations/ материалы, изделия, меню верстака
├── patches/character.json добыча, добавленная монстрам rpg_demo_game
├── patches/shop.json товары сверх обычных у деревенского торговца
└── characters/, charsets/, backgrounds/, locale/
```
**Расширяйте чужой мод, а не копируйте его.** Деревня, дом и лес по-прежнему берутся из `rpg_demo_game`. Событие с тем же id, что там, заменяет его (староста получает новые диалоги); новые id добавляют людей. Патчи добавляют добычу его монстрам и товары его лавке.
**Проверки и условия в сценариях.** Гадалка испытывает Удачу, а потом смотрит на ваше снаряжение:
```scenario
text "— Хочешь знать, что тебя ждёт? Дай руку. Не эту — ту, которой не воруешь." key=rpg_plus/seer/hello
check luk 14 who=rpg_hero else=fail -> :ok
:ok
text "— Волк у пещеры боится огня больше, чем стали. И бей туда, где у него нет щита. Это бесплатно. Остальное — нет." key=rpg_plus/seer/fortune
when "!equipped(rpg_hero, 'classic/clover_charm')" -> :charm
end
```
`when "item('rpg_plus/salve') >= 1"` заглядывает в сумку, `level(rpg_hero) >= 3` — в уровень героя; флаги делают разовые награды безопасными. Строки написаны по-русски, у каждой есть `key=`; переводы лежат в `locale/scenarios_<lang>.properties`.
**Рецепт** — это данные в `recipes/woodland.json`:
```json
{
"Id": "rpg_plus/salve",
"Name": "rpg_plus/recipe/salve/name",
"Type": "Cooking",
"Input": [
{ "Id": "rpg_demo/mushroom", "Count": 1 },
{ "Id": "rpg_plus/resin", "Count": 1 }
],
"Output": [{ "Id": "rpg_plus/salve", "Count": 1 }]
}
```
Опциональный **редактор тайловых карт** рисует землю, коллизии и события, а **редактор данных** правит предметы, рецепты и персонажей. Включите их в схеме игры.
## Если что-то пошло не так
1. **В старом сохранении нет янтаря волка.** Подойдите к месту волка у пещеры и нажмите E/F: янтарь можно подобрать там один раз, если у вас нет ни его, ни фонаря.
2. **Неясно, что делать дальше.** Откройте журнал.
3. **Пропал вариант диалога.** Некоторые варианты появляются только после определённого этапа задания или при нужном предмете.
+115
View File
@@ -0,0 +1,115 @@
# RPG Plus — một RPG rừng rậm dựng từ các mod
RPG Plus là một trò RPG nhỏ mà hoàn chỉnh: phần mở đầu kiểu tiểu thuyết tương tác để bạn chọn quá khứ của anh hùng, rồi một làng và khu rừng trên bản đồ ô vuông, bí ẩn về ánh sáng lạc mất của khu rừng, nhiệm vụ phụ, bàn chế tạo, thương nhân, một hoang mạc phía tây và chiến đấu theo lượt. Trò chơi chỉ gồm tệp dữ liệu và kịch bản; còn luật chơi — chỉ số, trang bị, phép kiểm, chiến đấu, chế tạo — đến từ những mod riêng mà trò chơi khác cũng dùng được.
Chơi từ **Chơi → RPG Plus → Trò chơi mới**.
![Phần mở đầu ở ngã ba: tấm biển chỉ đường bên con đường rừng và câu hỏi “Trước khi đến đây, ngươi là ai?” với ba câu trả lời: lính, đệ tử pháp sư, kẻ phiêu bạt.](assets/prologue.jpg)
## Cốt truyện và cách chơi
1. **Ngã ba.** Chọn quá khứ của bạn: **lính** (chiến binh), **đệ tử pháp sư** (pháp sư) hay **kẻ phiêu bạt** (rogue). Lựa chọn quyết định chỉ số và trang bị khởi đầu. Bạn bắt đầu với 25 đồng xu.
2. **Ngôi làng bên Rừng Xám.** Đi lại bằng WASD hoặc phím mũi tên, chạy bằng Shift, dùng E hoặc F để nói chuyện và tương tác. **Nhật ký** luôn ghi rõ bước cụ thể tiếp theo của bạn.
3. **Trưởng làng** hỏi về ánh sáng biến mất, hoặc phái bạn đi ngay theo con sói ở hang phía đông.
4. **Khu rừng.** Hái củi và nấm; slime rơi ra nhựa sống và đồng xu. Con sói luôn rơi trái tim hổ phách và đủ lông để may một chiếc áo choàng.
5. **Bàn chế tạo bên giếng.** Làm thuốc mỡ rừng (nấm + nhựa), áo choàng người canh rừng (2 lông + 2 nhựa) hoặc đèn lồng hộ mệnh (hổ phách + 2 nhựa + 2 củi). Mang áo choàng từ túi đồ: nó thay đổi con số chiến đấu thật của bạn.
6. **Bà thuốc** ở hàng rào phía tây cần thuốc mỡ; giúp bà dẫn tới người đưa thư mất tích trong rừng phương nam.
7. **Đền thờ.** Xách đèn qua hang động phía sau con sói tới đền thờ hoang tàn. Trả ánh sáng cho khu rừng, hoặc lập khế ước cho ngôi làng. Cả hai kết thúc đều khép lại cuộc phiêu lưu, và lời cuối của trưởng làng sẽ theo lựa chọn của bạn.
Một số người khác trong làng: **võ sư** dẫn bạn ra bãi tập (kinh nghiệm một lần, kèm phép kiểm Sức mạnh để nhấc khúc gỗ), và **bà thầy bói** đọc vận may của bạn. Lò sưởi bên giếng hồi máu và mana. Phần thưởng không nhận lại được lần thứ hai.
![Bàn chế tạo bên giếng với ba công thức: Thuốc mỡ rừng, Áo choàng người canh rừng và Đèn lồng hộ mệnh, nguyên liệu bạn có và cần, cùng nút Chế tạo.](assets/workbench.jpg)
![Đền thờ hoang trong rừng, “Khu rừng nhớ”, anh hùng đang xách đèn.](assets/shrine.jpg)
### Hoang mạc
Từ làng, đi theo con đường chính về phía tây, ra khỏi mép bản đồ là tới **Vùng Biên Khô**: cát, một ngôi nhà bằng đất nặn, chỗ trú của đoàn thương xa, một tòa tháp hoang, cây cọ và xương rồng. Năm con bọ cạp và bọ hung chờ được đánh, và các thùng tiếp tế cất phần thưởng nhỏ một lần. Mép đông dẫn về lại làng.
![Vùng Biên Khô: cát với chỗ trú của đoàn thương xa, xương rồng, cây cọ, thùng hàng, bọ cạp và bọ hung; anh hùng tới bằng con đường từ phía đông.](assets/desert.jpg)
## Anh hùng của bạn
Mở **bảng nhân vật** từ thanh công cụ để xem class, cấp và kinh nghiệm, các chỉ số (Sức mạnh, Nhanh nhẹn, Trí tuệ, Sinh lực, Sức hút, May mắn), giá trị chiến đấu và trang bị trong ba ô vũ khí, thân thể và phụ kiện. Trang bị lấy từ túi đồ.
![Bảng nhân vật của một rogue cấp 2: chỉ số, máu và mana, tấn công có +3 từ trang bị, các ô trang bị trống.](assets/character-sheet.jpg)
Trận đánh dùng lối chiến đấu ra lệnh (FF), sát thương tính theo công thức. Một số lựa chọn hội thoại đem chỉ số ra thử với một lần gieo d20, ví dụ **[Sức mạnh 12]** của võ sư.
![Trận đánh với con sói: anh hùng ở dưới bên trái, con sói cùng máu, tấn công và phòng thủ ở phía trên, nhật ký chiến đấu bên phải.](assets/battle.jpg)
## Trò chơi được ghép thế nào
RPG Plus là bộ sưu tập **RPG Plus**. Các khối của nó:
```text
Cốt truyện rpg_plus_game (mod này)
Làng và rừng rpg_demo_game: bản đồ, con người và vật phẩm mà trò này mở rộng
Tài nguyên và luật village_pack (hình ảnh), classic_rpg_pack (chỉ số, class, trang bị)
Chỉ số và phép kiểm stats_base, stat_checks
Thế giới ô vuông rpg
Vật phẩm, trang bị, mua bán items_base, equipment, trading, crafting
Engine 2D shell2d_base, dialog_base, characters_base, audio_base
Chiến đấu turn_battle_base, ff_battle, battle_formulas
Chiến đấu thẻ bài card_battle (tùy chọn)
Giao diện ui_base, menu_base
Trình soạn bản đồ ô rpg_editor (tùy chọn)
Trình soạn dữ liệu schema_tools, items_editor, characters_editor (tùy chọn)
Bộ dựng ô tile tile_topdown_base, tile_isometric_base
```
Cấu hình của bộ sưu tập: `battle.core` là `ff_battle`, phép kiểm gieo `d20`, hình anh hùng trên bản đồ là `village/hero`, bản đồ vẽ nhìn từ trên xuống. Đổi trong **Mods & Collections → Bộ sưu tập → RPG Plus → Sửa sơ đồ**.
## Dành cho tác giả: đổi nội dung không cần mã
```filestree
rpg_plus_game/
├── scenarios/rpg_plus/ phần mở đầu, training_yard, events/ (võ sư, thầy bói), woodland/, desert/
├── map_events/<map>/ người mới, bàn chế tạo, cửa hang, thùng hàng; id trùng sẽ thay của rpg_demo_game
├── maps/ rpg_woodland_shrine.json, rpg_desert.json
├── scenes/ ngã ba, bãi tập, đền thờ, hoang mạc
├── quests/woodland.json các chặng nhật ký
├── items/, recipes/, stations/ nguyên liệu, sản phẩm, trình đơn của bàn chế tạo
├── patches/character.json chiến lợi phẩm thêm cho quái của rpg_demo_game
├── patches/shop.json thêm hàng cho thương nhân trong làng
└── characters/, charsets/, backgrounds/, locale/
```
**Hãy mở rộng mod của người khác thay vì copy nó.** Làng, nhà và rừng vẫn đến từ `rpg_demo_game`. Sự kiện trùng id với sự kiện ở đó sẽ thay thế nó (trưởng làng nhận hội thoại mới); id mới thêm người. Các bản vá thêm chiến lợi phẩm cho quái và thêm hàng cho cửa hàng của nó.
**Phép kiểm và điều kiện trong kịch bản.** Bà thầy bói thử May mắn rồi nhìn trang bị của bạn:
```scenario
text "“Muốn biết điều gì đang chờ ngươi? Đưa tay đây. Không phải tay đó — tay không dùng để trộm ấy.”" key=rpg_plus/seer/hello
check luk 14 who=rpg_hero else=fail -> :ok
:ok
text "“Con sói ở hang sợ lửa hơn thép. Và hãy đánh vào chỗ nó không có khiên. Phần này miễn phí. Phần còn lại thì không.”" key=rpg_plus/seer/fortune
when "!equipped(rpg_hero, 'classic/clover_charm')" -> :charm
end
```
`when "item('rpg_plus/salve') >= 1"` nhìn vào túi đồ, `level(rpg_hero) >= 3` nhìn cấp của anh hùng; nhờ các cờ mà phần thưởng một lần không bị nhận hai lần. Các câu viết bằng tiếng Nga, mỗi câu một `key=`; bản dịch nằm trong `locale/scenarios_<lang>.properties`.
**Một công thức** là dữ liệu trong `recipes/woodland.json`:
```json
{
"Id": "rpg_plus/salve",
"Name": "rpg_plus/recipe/salve/name",
"Type": "Cooking",
"Input": [
{ "Id": "rpg_demo/mushroom", "Count": 1 },
{ "Id": "rpg_plus/resin", "Count": 1 }
],
"Output": [{ "Id": "rpg_plus/salve", "Count": 1 }]
}
```
**Trình soạn bản đồ ô** tùy chọn vẽ nền, va chạm và sự kiện, còn **Trình soạn dữ liệu** sửa vật phẩm, công thức và nhân vật. Bật chúng trong sơ đồ của trò chơi.
## Nếu có trục trặc
1. **Hổ phách của con sói thiếu trong bản lưu cũ.** Ra chỗ con sói bên hang và nhấn E/F: nếu bạn chưa có hổ phách cũng như đèn, có thể nhặt nó ở đó một lần.
2. **Chưa biết làm gì tiếp.** Mở nhật ký.
3. **Một lựa chọn hội thoại mất tích.** Một số lựa chọn chỉ xuất hiện sau một chặng nhiệm vụ nhất định hoặc khi có vật phẩm nhất định.
+115
View File
@@ -0,0 +1,115 @@
# RPG Plus — 用模组搭起来的森林 RPG
RPG Plus 是一款完整的小型 RPG:先是一段视觉小说序章,让你选择英雄的过去;然后是图块地图上的村庄与森林、关于森林失落的神秘之光的主线、支线任务、工作台、商人、西边的沙漠和回合制战斗。游戏只由数据文件和剧本组成;规则——属性、装备、检定、战斗、制作——则来自独立的模组,任何其他游戏同样可以复用。
从“开始游戏 → RPG Plus → 新游戏”进入。
![岔路口序章:林间路边的路标,问题“来这里之前,你是谁?”以及三个选项:士兵、魔法师学徒、流浪者。](assets/prologue.jpg)
## 剧情与玩法
1. **岔路口。** 选择你的过去:**士兵**(战士)、**魔法师学徒**(法师)或**流浪者**(盗贼)。选择决定你的属性和初始装备。开局有 25 枚硬币。
2. **灰森林旁的村庄。** 用 WASD 或方向键行走,Shift 奔跑,E 或 F 交谈与互动。**日志**始终列着你接下来该做的具体一步。
3. **村长**会问起失踪的光,或者直接派你去东边洞穴对付那只狼。
4. **森林。** 采集柴火和蘑菇;史莱姆掉落活树脂和硬币。狼必定掉落琥珀之心,以及足够做一件斗篷的毛皮。
5. **井边工作台。** 制作林间药膏(蘑菇 + 树脂)、守林人斗篷(2 毛皮 + 2 树脂)或守护提灯(琥珀 + 2 树脂 + 2 柴火)。斗篷可以从背包直接穿上:它会改变你真实的战斗数值。
6. 西边栅栏旁的**草药师**需要药膏;帮她之后,会引出南方森林里失踪的信使。
7. **圣祠。** 带着提灯穿过狼身后的洞穴,抵达荒废的圣祠。把光还给森林,或者为村庄立下一纸契约。两个结局都会完结这段冒险,村长最后的嘱托也随你的选择而变。
村里还有别人:**教头**会带你去练武场(经验只有一次,还有一次举起原木的力量检定),**占卜师**为你解读运气。井边的炉火恢复生命和法力。奖励不能重复领取。
![井边工作台和三个配方:林间药膏、守林人斗篷和守护提灯,标注了你已有和所需的材料,以及“制作”按钮。](assets/workbench.jpg)
![森林中荒废的圣祠,“森林记得”,英雄提着提灯。](assets/shrine.jpg)
### 沙漠
从村庄沿主路向西走出地图边缘,就到了**枯荒之地**:黄沙、一座土坯房、一处商队棚屋、一座残塔、棕榈和仙人掌。五只蝎子和甲虫等着开打,补给桶里装着小份的一次性奖励。东边边缘通回村庄。
![枯荒之地:沙地上的商队棚屋、仙人掌、棕榈、木桶、蝎子和甲虫;英雄从东边的道路抵达。](assets/desert.jpg)
## 你的英雄
从工具栏打开**角色面板**,可以看到职业、等级和经验、属性(力量、敏捷、智力、体质、魅力、幸运)、战斗数值,以及武器、身体、饰品三个栏位里的装备。装备从背包里穿上。
![二级盗贼的角色面板:属性、生命和法力、带装备 +3 加成的攻击、空的装备栏位。](assets/character-sheet.jpg)
战斗采用指令式(FF),伤害由公式计算。部分对话选项会让属性对抗一次 d20 掷骰,比如教头的**[力量 12]**。
![与狼的战斗:英雄在左下角,狼连同其生命、攻击和防御在上方,战斗记录在右侧。](assets/battle.jpg)
## 游戏的组成
RPG Plus 就是合集 **RPG Plus**。它的分组:
```text
剧情 rpg_plus_game(本模组)
村庄与森林 rpg_demo_game:本游戏在其上扩展的地图、人物和物品
素材与规则 village_pack(美术)、classic_rpg_pack(属性、职业、装备)
属性与检定 stats_base, stat_checks
图块世界 rpg
物品、装备、交易 items_base, equipment, trading, crafting
2D 引擎 shell2d_base, dialog_base, characters_base, audio_base
战斗 turn_battle_base, ff_battle, battle_formulas
卡牌战斗 card_battle(可选)
界面 ui_base, menu_base
图块地图编辑器 rpg_editor(可选)
数据编辑器 schema_tools, items_editor, characters_editor(可选)
图块渲染器 tile_topdown_base, tile_isometric_base
```
合集的设置:`battle.core` 为 `ff_battle`,检定掷 `d20`,英雄的地图贴图为 `village/hero`,地图以俯视方式绘制。可在 **Mods & Collections → 合集 → RPG Plus → 编辑结构** 中修改。
## 给模组作者:不改代码就能改内容
```filestree
rpg_plus_game/
├── scenarios/rpg_plus/ 序章、training_yard、events/(教头、占卜师)、woodland/、desert/
├── map_events/<map>/ 新人物、工作台、洞穴门、木桶;相同 id 的事件会覆盖 rpg_demo_game 的
├── maps/ rpg_woodland_shrine.json, rpg_desert.json
├── scenes/ 岔路口、练武场、圣祠、沙漠
├── quests/woodland.json 日志的阶段
├── items/, recipes/, stations/ 材料、成品、工作台的菜单
├── patches/character.json 给 rpg_demo_game 的怪物追加的掉落
├── patches/shop.json 给村庄商人追加的商品
└── characters/, charsets/, backgrounds/, locale/
```
**去扩展别的模组,而不是复制它。** 村庄、房屋和森林仍然来自 `rpg_demo_game`。与那里相同 id 的事件会将其替换(村长得到新对话);新 id 则添加人物。补丁给它的怪物添加掉落,给它的商店添加商品。
**剧本中的检定与条件。** 占卜师先测试幸运,再打量你的装备:
```scenario
text "“想知道等着你的是什么?把手给我。不是这只——是你不用来偷东西的那只。”" key=rpg_plus/seer/hello
check luk 14 who=rpg_hero else=fail -> :ok
:ok
text "“洞口的狼怕火胜过怕钢。还有,往它没护盾的地方打。这些免费。其余的不免费。”" key=rpg_plus/seer/fortune
when "!equipped(rpg_hero, 'classic/clover_charm')" -> :charm
end
```
`when "item('rpg_plus/salve') >= 1"` 检查背包,`level(rpg_hero) >= 3` 检查英雄的等级;标志位让一次性奖励不会重复。台词以俄语写成,每行带一个 `key=`;翻译在 `locale/scenarios_<lang>.properties` 里。
**配方**就是 `recipes/woodland.json` 里的数据:
```json
{
"Id": "rpg_plus/salve",
"Name": "rpg_plus/recipe/salve/name",
"Type": "Cooking",
"Input": [
{ "Id": "rpg_demo/mushroom", "Count": 1 },
{ "Id": "rpg_plus/resin", "Count": 1 }
],
"Output": [{ "Id": "rpg_plus/salve", "Count": 1 }]
}
```
可选的**图块地图编辑器**负责绘制地面、碰撞和事件,**数据编辑器**则编辑物品、配方和角色。在游戏的结构里打开它们。
## 出问题了怎么办
1. **旧存档里没有狼的琥珀。** 走到洞穴旁狼所在的位置按 E/F:只要你既没有琥珀也没有提灯,就能在那里捡到一次。
2. **不知道下一步做什么。** 打开日志。
3. **某个对话选项不见了。** 有些选项只有推进到某个任务阶段或持有特定物品后才会出现。
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

+71
View File
@@ -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
}
]
}
]
}
+38 -1
View File
@@ -1,2 +1,39 @@
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.
mod_rpg_plus_game_desc = A complete small RPG: a prologue that picks the class, a village and forest mystery, crafting, trade, a desert and turn-based battles — data and scenarios only.
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
rpg_plus/desert/map/name=The Dry March
rpg_plus/desert/enemy/scorpion/name=Sand scorpion
rpg_plus/desert/enemy/beetle/name=Desert beetle
+1 -1
View File
@@ -1,2 +1,2 @@
mod_rpg_plus_game_name = RPG Plus
mod_rpg_plus_game_desc = La historia del ejemplo RPG Plus, construida encima de la demo RPG y no a su lado: un prólogo estilo novela en un cruce con elección de clase y una primera tirada de carisma, luego toda la aldea y el bosque de rpg_demo sin tocar nada — un maestro y una adivina añadidos con el propio archivo de eventos de mapa del mod, una escena del patio de entrenamiento, equipo en la tienda (un parche de la tienda), un lobo con estadísticas (un parche de personaje). Solo datos y escenarios.
mod_rpg_plus_game_desc = Un RPG pequeño y completo: un prólogo que elige la clase, el misterio de la aldea y el bosque, crafteo, comercio, un desierto y combates por turnos. Solo datos y escenarios.
+1 -1
View File
@@ -1,2 +1,2 @@
mod_rpg_plus_game_name = RPG Plus
mod_rpg_plus_game_desc = RPG Plus サンプルの物語。RPG デモの横ではなく上に作られている:十字路でのノベル風プロローグで職業を選び、最初のカリスマ判定。その後は rpg_demo の村と森をそのまま——師匠と占い師はこの MOD 自身のマップイベントファイルで追加、稽古場のシーン、商人の装備(店のパッチ)、ステータス付きの狼(キャラクターのパッチ)。データとシナリオのみ。
mod_rpg_plus_game_desc = 完成された小さな RPG:クラスを選ぶプロローグ、村と森の謎、クラフト、交易、砂漠、ターン制バトル。データとシナリオのみ。
+1 -1
View File
@@ -1,2 +1,2 @@
mod_rpg_plus_game_name = RPG Plus
mod_rpg_plus_game_desc = RPG Plus 샘플의 이야기. RPG 데모 옆이 아니라 그 위에 만들었습니다: 갈림길의 노벨식 프롤로그에서 직업을 고르고 첫 매력 판정을 하고, 이어서 rpg_demo의 마을과 숲 전체를 한 줄도 고치지 않고 — 스승과 점쟁이는 이 모드의 자체 맵 이벤트 파일로 추가, 수련장 장면, 상인의 장비(상점 패치), 능력치를 가진 늑대(캐릭터 패치). 데이터와 시나리오만.
mod_rpg_plus_game_desc = 완성된 소규모 RPG: 클래스를 고르는 프롤로그, 마을과 숲의 미스터리, 제작, 거래, 사막, 턴제 전투. 데이터와 시나리오만 있습니다.
+38 -1
View File
@@ -1,2 +1,39 @@
mod_rpg_plus_game_name = RPG Plus
mod_rpg_plus_game_desc = Сюжет образца RPG Plus, построенный поверх RPG-демо, а не рядом с ним: пролог-новелла на перекрёстке с выбором класса и первой проверкой харизмы, затем вся деревня и лес rpg_demo без единой правки — наставник и гадалка добавлены собственным файлом событий карты, сцена тренировочной площадки, снаряжение у торговца (патч лавки), волк со статами (патч персонажа). Только данные и сценарии.
mod_rpg_plus_game_desc = Полная маленькая RPG: пролог с выбором класса, тайна деревни и леса, крафт, торговля, пустыня и пошаговые бои — только данные и сценарии.
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 = Лес помнит
rpg_plus/desert/map/name=Сухой предел
rpg_plus/desert/enemy/scorpion/name=Песчаный скорпион
rpg_plus/desert/enemy/beetle/name=Пустынный жук
+73
View File
@@ -40,3 +40,76 @@ 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.
rpg_plus/desert/barrel/supply_rpg_village_19_11=You find a few supplies in the barrel.
rpg_plus/desert/barrel/supply_rpg_village_20_11=You find a few supplies in the barrel.
rpg_plus/desert/barrel/supply_rpg_village_4_7=You find a few supplies in the barrel.
rpg_plus/desert/barrel/supply_rpg_village_22_11=You find a few supplies in the barrel.
rpg_plus/desert/barrel/supply_rpg_elder_house_1_2=You find a few supplies in the barrel.
rpg_plus/desert/barrel/supply_rpg_elder_house_2_2=You find a few supplies in the barrel.
rpg_plus/desert/barrel/supply_rpg_elder_house_10_2=You find a few supplies in the barrel.
rpg_plus/desert/enter=Beyond the last tree, the shade ends. The scorpions here are larger than the local hopes.
rpg_plus/desert/barrel/desert_barrel_supply=You find a few supplies in the barrel.
rpg_plus/desert/barrel/empty=Empty. Even the rubbish has been taken.
rpg_plus/desert/barrel/desert_barrel_caravan=You find a few supplies in the barrel.
rpg_plus/desert/barrel/desert_barrel_ruins=You find a few supplies in the barrel.
rpg_plus/desert/house=A note on the door reads: Back when it cools down. The ink has faded.
rpg_plus/desert/caravan=The caravan has left. The prices remain. Nothing has become cheaper.
rpg_plus/desert/enemy/scorpion=The scorpion raises its tail. Negotiations will be brief.
rpg_plus/desert/enemy/beetle=The beetle scrapes the sand. It charges meat for passage.
+73
View File
@@ -40,3 +40,76 @@ 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 = Сердце янтаря осталось у пещеры. На этот раз забираешь его с собой.
rpg_plus/desert/barrel/supply_rpg_village_19_11=В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.
rpg_plus/desert/barrel/supply_rpg_village_20_11=В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.
rpg_plus/desert/barrel/supply_rpg_village_4_7=В бочке нашлась мелочь: 2 монет(ы), 1 смола.
rpg_plus/desert/barrel/supply_rpg_village_22_11=В бочке нашлась мелочь: 2 монет(ы), 1 смола.
rpg_plus/desert/barrel/supply_rpg_elder_house_1_2=В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.
rpg_plus/desert/barrel/supply_rpg_elder_house_2_2=В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.
rpg_plus/desert/barrel/supply_rpg_elder_house_10_2=В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.
rpg_plus/desert/enter=За последним деревом кончается тень. Скорпионы здесь крупнее местных надежд.
rpg_plus/desert/barrel/desert_barrel_supply=В бочке нашлась мелочь: 3 монет(ы), 1 эликсир.
rpg_plus/desert/barrel/empty=Пусто. Даже мусор уже забрали.
rpg_plus/desert/barrel/desert_barrel_caravan=В бочке нашлась мелочь: 4 монет(ы), 1 смола.
rpg_plus/desert/barrel/desert_barrel_ruins=В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.
rpg_plus/desert/house=На двери записка: «Вернусь, когда станет прохладно». Чернила выцвели.
rpg_plus/desert/caravan=Караван ушёл. Цены остались. Дешевле от этого не стало.
rpg_plus/desert/enemy/scorpion=Скорпион поднимает хвост. Переговоры будут короткими.
rpg_plus/desert/enemy/beetle=Жук скребёт песок. За проход он берёт мясом.
+1 -1
View File
@@ -1,2 +1,2 @@
mod_rpg_plus_game_name = RPG Plus
mod_rpg_plus_game_desc = Câu chuyện của mẫu RPG Plus, xây trên bản demo RPG chứ không đặt cạnh nó: phần mở đầu kiểu tiểu thuyết ở ngã ba với lựa chọn lớp nhân vật và lần kiểm tra sức hút đầu tiên, rồi toàn bộ làng và rừng của rpg_demo không sửa một dòng — sư phụ và bà thầy bói được thêm bằng chính tệp sự kiện bản đồ của mod, cảnh bãi tập, trang bị ở cửa hàng (bản vá cửa hàng), con sói có chỉ số (bản vá nhân vật). Chỉ dữ liệu và kịch bản.
mod_rpg_plus_game_desc = Một RPG nhỏ nhưng trọn vẹn: phần mở đầu chọn class, bí ẩn của làng và khu rừng, chế tạo, mua bán, hoang mạc và chiến đấu theo lượt — chỉ dữ liệu và kịch bản.
+1 -1
View File
@@ -1,2 +1,2 @@
mod_rpg_plus_game_name = RPG Plus
mod_rpg_plus_game_desc = RPG Plus 示例的故事,建立在 RPG 演示之上而非之旁:十字路口的小说式序章,选择职业并进行第一次魅力检定;之后是 rpg_demo 的整个村庄和森林,一处未改——导师和占卜师由本模组自己的地图事件文件加入,练武场场景,商人处的装备(商店补丁),带属性的狼(角色补丁)。仅有数据和剧本。
mod_rpg_plus_game_desc = 一款完整的小型 RPG:选择职业的序章、村庄与森林之谜、制作、交易、沙漠和回合制战斗——只有数据和剧本。
+16 -2
View File
@@ -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 complete small RPG: a prologue that picks the class, a village and forest mystery, crafting, trade, a desert and turn-based battles — data and scenarios only.",
"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,18 @@
"activate": [
"--itest=rpg_plus"
]
},
{
"id": "woodland",
"activate": [
"--itest=woodland"
]
},
{
"id": "desert",
"activate": [
"--itest=desert"
]
}
]
}
+198
View File
@@ -0,0 +1,198 @@
{
"Events": [
{
"Id": "desert_to_village",
"X": 39,
"Y": 12,
"Tile": -1,
"Dir": "Down",
"Trigger": "Touch",
"Solid": false,
"Teleport": {
"Map": "rpg_village",
"X": 1,
"Y": 9,
"Dir": "Right"
}
},
{
"Id": "desert_barrel_supply",
"X": 17,
"Y": 8,
"Sprite": "desert/barrel_closed",
"Tile": -1,
"Dir": "Down",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag desert_barrel_supply_open 1",
"action item.add id=money/coin count=3",
"action item.add id=rpg_demo/elixir count=1",
"ptext \u0022\u0412 \u0431\u043E\u0447\u043A\u0435 \u043D\u0430\u0448\u043B\u0430\u0441\u044C \u043C\u0435\u043B\u043E\u0447\u044C: 3 \u043C\u043E\u043D\u0435\u0442(\u044B), 1 \u044D\u043B\u0438\u043A\u0441\u0438\u0440.\u0022 key=rpg_plus/desert/barrel/desert_barrel_supply"
],
"Pages": [
{
"Condition": "desert_barrel_supply_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \u0022\u041F\u0443\u0441\u0442\u043E. \u0414\u0430\u0436\u0435 \u043C\u0443\u0441\u043E\u0440 \u0443\u0436\u0435 \u0437\u0430\u0431\u0440\u0430\u043B\u0438.\u0022 key=rpg_plus/desert/barrel/empty"
]
}
]
},
{
"Id": "desert_barrel_caravan",
"X": 25,
"Y": 8,
"Sprite": "desert/barrel_closed",
"Tile": -1,
"Dir": "Down",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag desert_barrel_caravan_open 1",
"action item.add id=money/coin count=4",
"action item.add id=rpg_plus/resin count=1",
"ptext \u0022\u0412 \u0431\u043E\u0447\u043A\u0435 \u043D\u0430\u0448\u043B\u0430\u0441\u044C \u043C\u0435\u043B\u043E\u0447\u044C: 4 \u043C\u043E\u043D\u0435\u0442(\u044B), 1 \u0441\u043C\u043E\u043B\u0430.\u0022 key=rpg_plus/desert/barrel/desert_barrel_caravan"
],
"Pages": [
{
"Condition": "desert_barrel_caravan_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \u0022\u041F\u0443\u0441\u0442\u043E. \u0414\u0430\u0436\u0435 \u043C\u0443\u0441\u043E\u0440 \u0443\u0436\u0435 \u0437\u0430\u0431\u0440\u0430\u043B\u0438.\u0022 key=rpg_plus/desert/barrel/empty"
]
}
]
},
{
"Id": "desert_barrel_ruins",
"X": 10,
"Y": 20,
"Sprite": "desert/barrel_closed",
"Tile": -1,
"Dir": "Down",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag desert_barrel_ruins_open 1",
"action item.add id=money/coin count=2",
"action item.add id=rpg_demo/branches count=1",
"ptext \u0022\u0412 \u0431\u043E\u0447\u043A\u0435 \u043D\u0430\u0448\u043B\u0430\u0441\u044C \u043C\u0435\u043B\u043E\u0447\u044C: 2 \u043C\u043E\u043D\u0435\u0442(\u044B), 1 \u0441\u0432\u044F\u0437\u043A\u0430 \u0445\u0432\u043E\u0440\u043E\u0441\u0442\u0430.\u0022 key=rpg_plus/desert/barrel/desert_barrel_ruins"
],
"Pages": [
{
"Condition": "desert_barrel_ruins_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \u0022\u041F\u0443\u0441\u0442\u043E. \u0414\u0430\u0436\u0435 \u043C\u0443\u0441\u043E\u0440 \u0443\u0436\u0435 \u0437\u0430\u0431\u0440\u0430\u043B\u0438.\u0022 key=rpg_plus/desert/barrel/empty"
]
}
]
},
{
"Id": "desert_house_door",
"X": 8,
"Y": 8,
"Tile": -1,
"Dir": "Down",
"Trigger": "Action",
"Solid": false,
"Commands": [
"ptext \u0022\u041D\u0430 \u0434\u0432\u0435\u0440\u0438 \u0437\u0430\u043F\u0438\u0441\u043A\u0430: \u00AB\u0412\u0435\u0440\u043D\u0443\u0441\u044C, \u043A\u043E\u0433\u0434\u0430 \u0441\u0442\u0430\u043D\u0435\u0442 \u043F\u0440\u043E\u0445\u043B\u0430\u0434\u043D\u043E\u00BB. \u0427\u0435\u0440\u043D\u0438\u043B\u0430 \u0432\u044B\u0446\u0432\u0435\u043B\u0438.\u0022 key=rpg_plus/desert/house"
]
},
{
"Id": "desert_caravan_notice",
"X": 21,
"Y": 8,
"Tile": -1,
"Dir": "Down",
"Trigger": "Action",
"Solid": false,
"Commands": [
"ptext \u0022\u041A\u0430\u0440\u0430\u0432\u0430\u043D \u0443\u0448\u0451\u043B. \u0426\u0435\u043D\u044B \u043E\u0441\u0442\u0430\u043B\u0438\u0441\u044C. \u0414\u0435\u0448\u0435\u0432\u043B\u0435 \u043E\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0441\u0442\u0430\u043B\u043E.\u0022 key=rpg_plus/desert/caravan"
]
},
{
"Id": "desert_scorpion_1",
"X": 27,
"Y": 12,
"Sprite": "desert/scorpion",
"Tile": -1,
"Dir": "Down",
"Trigger": "Action",
"Solid": true,
"Commands": [
"ptext \u0022\u0421\u043A\u043E\u0440\u043F\u0438\u043E\u043D \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442 \u0445\u0432\u043E\u0441\u0442. \u041F\u0435\u0440\u0435\u0433\u043E\u0432\u043E\u0440\u044B \u0431\u0443\u0434\u0443\u0442 \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u043C\u0438.\u0022 key=rpg_plus/desert/enemy/scorpion",
"action rpg.battle enemies=rpg_desert_scorpion win_flag=desert_scorpion_1_dead",
"end"
],
"Condition": "desert_scorpion_1_dead != 1"
},
{
"Id": "desert_beetle_2",
"X": 22,
"Y": 13,
"Sprite": "desert/beetle",
"Tile": -1,
"Dir": "Left",
"Trigger": "Action",
"Solid": true,
"Commands": [
"ptext \u0022\u0416\u0443\u043A \u0441\u043A\u0440\u0435\u0431\u0451\u0442 \u043F\u0435\u0441\u043E\u043A. \u0417\u0430 \u043F\u0440\u043E\u0445\u043E\u0434 \u043E\u043D \u0431\u0435\u0440\u0451\u0442 \u043C\u044F\u0441\u043E\u043C.\u0022 key=rpg_plus/desert/enemy/beetle",
"action rpg.battle enemies=rpg_desert_beetle win_flag=desert_beetle_2_dead",
"end"
],
"Condition": "desert_beetle_2_dead != 1"
},
{
"Id": "desert_scorpion_3",
"X": 13,
"Y": 14,
"Sprite": "desert/scorpion",
"Tile": -1,
"Dir": "Down",
"Trigger": "Action",
"Solid": true,
"Commands": [
"ptext \u0022\u0421\u043A\u043E\u0440\u043F\u0438\u043E\u043D \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442 \u0445\u0432\u043E\u0441\u0442. \u041F\u0435\u0440\u0435\u0433\u043E\u0432\u043E\u0440\u044B \u0431\u0443\u0434\u0443\u0442 \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u043C\u0438.\u0022 key=rpg_plus/desert/enemy/scorpion",
"action rpg.battle enemies=rpg_desert_scorpion win_flag=desert_scorpion_3_dead",
"end"
],
"Condition": "desert_scorpion_3_dead != 1"
},
{
"Id": "desert_beetle_4",
"X": 11,
"Y": 10,
"Sprite": "desert/beetle",
"Tile": -1,
"Dir": "Left",
"Trigger": "Action",
"Solid": true,
"Commands": [
"ptext \u0022\u0416\u0443\u043A \u0441\u043A\u0440\u0435\u0431\u0451\u0442 \u043F\u0435\u0441\u043E\u043A. \u0417\u0430 \u043F\u0440\u043E\u0445\u043E\u0434 \u043E\u043D \u0431\u0435\u0440\u0451\u0442 \u043C\u044F\u0441\u043E\u043C.\u0022 key=rpg_plus/desert/enemy/beetle",
"action rpg.battle enemies=rpg_desert_beetle win_flag=desert_beetle_4_dead",
"end"
],
"Condition": "desert_beetle_4_dead != 1"
},
{
"Id": "desert_beetle_5",
"X": 28,
"Y": 19,
"Sprite": "desert/beetle",
"Tile": -1,
"Dir": "Down",
"Trigger": "Action",
"Solid": true,
"Commands": [
"ptext \u0022\u0416\u0443\u043A \u0441\u043A\u0440\u0435\u0431\u0451\u0442 \u043F\u0435\u0441\u043E\u043A. \u0417\u0430 \u043F\u0440\u043E\u0445\u043E\u0434 \u043E\u043D \u0431\u0435\u0440\u0451\u0442 \u043C\u044F\u0441\u043E\u043C.\u0022 key=rpg_plus/desert/enemy/beetle",
"action rpg.battle enemies=rpg_desert_beetle win_flag=desert_beetle_5_dead",
"end"
],
"Condition": "desert_beetle_5_dead != 1"
}
]
}
+73
View File
@@ -0,0 +1,73 @@
{
"Events": [
{
"Id": "supply_rpg_elder_house_1_2",
"X": 1,
"Y": 2,
"Sprite": "desert/barrel_closed",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag supply_rpg_elder_house_1_2_open 1",
"action item.add id=money/coin count=2",
"action item.add id=rpg_demo/branches count=1",
"ptext \"В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.\" key=rpg_plus/desert/barrel/supply_rpg_elder_house_1_2"
],
"Pages": [
{
"Condition": "supply_rpg_elder_house_1_2_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \"Пусто. Даже мусор уже забрали.\" key=rpg_plus/desert/barrel/empty"
]
}
]
},
{
"Id": "supply_rpg_elder_house_2_2",
"X": 2,
"Y": 2,
"Sprite": "desert/barrel_closed",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag supply_rpg_elder_house_2_2_open 1",
"action item.add id=money/coin count=2",
"action item.add id=rpg_demo/branches count=1",
"ptext \"В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.\" key=rpg_plus/desert/barrel/supply_rpg_elder_house_2_2"
],
"Pages": [
{
"Condition": "supply_rpg_elder_house_2_2_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \"Пусто. Даже мусор уже забрали.\" key=rpg_plus/desert/barrel/empty"
]
}
]
},
{
"Id": "supply_rpg_elder_house_10_2",
"X": 10,
"Y": 2,
"Sprite": "desert/barrel_closed",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag supply_rpg_elder_house_10_2_open 1",
"action item.add id=money/coin count=2",
"action item.add id=rpg_demo/branches count=1",
"ptext \"В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.\" key=rpg_plus/desert/barrel/supply_rpg_elder_house_10_2"
],
"Pages": [
{
"Condition": "supply_rpg_elder_house_10_2_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \"Пусто. Даже мусор уже забрали.\" key=rpg_plus/desert/barrel/empty"
]
}
]
}
]
}
+22
View File
@@ -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"
}
]
}
+68
View File
@@ -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"
}
}
]
}
+96
View File
@@ -0,0 +1,96 @@
{
"Events": [
{
"Id": "supply_rpg_village_19_11",
"X": 19,
"Y": 11,
"Sprite": "desert/barrel_closed",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag supply_rpg_village_19_11_open 1",
"action item.add id=money/coin count=2",
"action item.add id=rpg_demo/branches count=1",
"ptext \"В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.\" key=rpg_plus/desert/barrel/supply_rpg_village_19_11"
],
"Pages": [
{
"Condition": "supply_rpg_village_19_11_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \"Пусто. Даже мусор уже забрали.\" key=rpg_plus/desert/barrel/empty"
]
}
]
},
{
"Id": "supply_rpg_village_20_11",
"X": 20,
"Y": 11,
"Sprite": "desert/barrel_closed",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag supply_rpg_village_20_11_open 1",
"action item.add id=money/coin count=2",
"action item.add id=rpg_demo/branches count=1",
"ptext \"В бочке нашлась мелочь: 2 монет(ы), 1 связка хвороста.\" key=rpg_plus/desert/barrel/supply_rpg_village_20_11"
],
"Pages": [
{
"Condition": "supply_rpg_village_20_11_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \"Пусто. Даже мусор уже забрали.\" key=rpg_plus/desert/barrel/empty"
]
}
]
},
{
"Id": "supply_rpg_village_4_7",
"X": 4,
"Y": 7,
"Sprite": "desert/barrel_closed",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag supply_rpg_village_4_7_open 1",
"action item.add id=money/coin count=2",
"action item.add id=rpg_plus/resin count=1",
"ptext \"В бочке нашлась мелочь: 2 монет(ы), 1 смола.\" key=rpg_plus/desert/barrel/supply_rpg_village_4_7"
],
"Pages": [
{
"Condition": "supply_rpg_village_4_7_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \"Пусто. Даже мусор уже забрали.\" key=rpg_plus/desert/barrel/empty"
]
}
]
},
{
"Id": "supply_rpg_village_22_11",
"X": 22,
"Y": 11,
"Sprite": "desert/barrel_closed",
"Trigger": "Action",
"Solid": true,
"Commands": [
"flag supply_rpg_village_22_11_open 1",
"action item.add id=money/coin count=2",
"action item.add id=rpg_plus/resin count=1",
"ptext \"В бочке нашлась мелочь: 2 монет(ы), 1 смола.\" key=rpg_plus/desert/barrel/supply_rpg_village_22_11"
],
"Pages": [
{
"Condition": "supply_rpg_village_22_11_open == 1",
"Sprite": "desert/barrel_open",
"Commands": [
"ptext \"Пусто. Даже мусор уже забрали.\" key=rpg_plus/desert/barrel/empty"
]
}
]
}
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"Events": [
{
"Id": "village_to_desert",
"X": 0,
"Y": 9,
"Trigger": "Touch",
"Solid": false,
"Teleport": {
"Map": "rpg_desert",
"X": 38,
"Y": 12,
"Dir": "Left"
}
}
]
}
+63
View File
@@ -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
}
}
]
}
@@ -0,0 +1,49 @@
{
"Events": [
{
"Id": "shrine_exit",
"X": 1,
"Y": 11,
"Tile": -1,
"Dir": "Down",
"Trigger": "Touch",
"Teleport": {
"Map": "rpg_forest",
"X": 25,
"Y": 8,
"Dir": "Left"
}
},
{
"Id": "shrine_beacon",
"X": 12,
"Y": 5,
"Tile": 36,
"Dir": "Down",
"Trigger": "Action",
"Scenario": "rpg_plus/woodland/beacon",
"Pages": [
{
"Condition": "rpg_ward_done == 1 \u0026\u0026 rpg_ward_choice == \u0027pact\u0027",
"Tile": 36,
"Light": {
"Radius": 6,
"Color": "#f0cd8d"
}
},
{
"Condition": "rpg_ward_done == 1 \u0026\u0026 rpg_ward_choice == \u0027free\u0027",
"Tile": 38,
"Light": {
"Radius": 6,
"Color": "#9ce3c7"
}
}
],
"Light": {
"Radius": 2,
"Color": "#699baf"
}
}
]
}
+125
View File
@@ -0,0 +1,125 @@
{
"Kind": "rpg_map",
"Items": [
{
"Id": "rpg_desert",
"Name": "rpg_plus/desert/map/name",
"Tileset": "desert",
"Width": 40,
"Height": 24,
"Zoom": 2,
"Layers": {
"Back": [
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 1 0 1 1 1 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 1 1 0 1 1 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 1 0 0 1 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 1 1 0 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 1 1 1 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
],
"Main": [
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . 244 245 . . . . . . . . . . 244 245 . . . . . . . . . . . . . . . . . . . . 252 253 . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229 230 231 . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . 124 125 126 127 128 129 . . . . . . 164 165 166 167 168 169 . . . . . . . 244 245 . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . 252 253 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . 252 253 . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . 252 253 . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . 244 245 . . . . . . . . . . . . . .",
" . . . . . 196 197 198 199 . . . . . . 229 230 231 . . . . . . . . . . . . . . 229 230 231 . 252 253 . .",
" . . 252 253 . . . . . . . . . . . . . . . . . . . . . . . . 252 253 . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ."
],
"Front": [
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . 240 241 . . . . . . . . . . 240 241 . . . . . . . . . . . . . 220 221 222 . . . . . . . .",
" . . 242 243 . . . . . . . . . . 242 243 . . . . . . . . . . . . . 223 224 225 . . . . 250 251 . .",
" . . . . . . . 100 101 102 103 104 105 . . . . . . 140 141 142 143 144 145 . . . . 226 227 228 . . . . . . . .",
" . . . . . . . 106 107 108 109 110 111 . . . . . . 146 147 148 149 150 151 . . . . . . . . . . . . . . .",
" . . . . . . . 112 113 114 115 116 117 . . . . . . 152 153 154 155 156 157 . . . . . . . 240 241 . . . . . .",
" . . . . . . . 118 119 120 121 122 123 . . . . . . 158 159 160 161 162 163 . . . . . . . 242 243 . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . 250 251 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . 250 251 . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . 250 251 . . . . . . . . . . . . . . . . . . .",
" . . . . . 180 181 182 183 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . 184 185 186 187 . . . . . . 220 221 222 . . . . . . 240 241 . . . . . . 220 221 222 . . . . .",
" . . . . . 188 189 190 191 . . . . . . 223 224 225 . . . . . . 242 243 . . . . . . 223 224 225 . . . . .",
" . . . . . 192 193 194 195 . . . . . . 226 227 228 . . . . . . . . . . . . . . 226 227 228 . 250 251 . .",
" . . 250 251 . . . . . . . . . . . . . . . . . . . . . . . . 250 251 . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ."
]
},
"Collision": [
"########################################",
"#......................................#",
"#......................................#",
"#......................................#",
"#......######......######..............#",
"#......######......######..............#",
"#......######......######..............#",
"#......######......######..............#",
"#......######......######..............#",
"#......................................#",
"#......................................#",
"#......................................#",
"#......................................o",
"#......................................#",
"#......................................#",
"#......................................#",
"#....####..............................#",
"#....####..............................#",
"#....####..............................#",
"#....####..............................#",
"#....####..............................#",
"#......................................#",
"#......................................#",
"########################################"
],
"Spawn": {
"X": 38,
"Y": 12,
"Dir": "Left"
},
"Ambient": "#fff0d8"
}
]
}
+94
View File
@@ -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,
"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 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 3 3 3 3 3 3 3 4 24 24 24 24 24 24 24",
"24 24 24 24 24 24 24 24 4 3 3 3 3 3 3 3 4 24 24 24 24 24 24 24",
"24 24 24 24 24 24 24 24 4 3 3 3 3 3 3 3 4 24 24 24 24 24 24 24",
"24 24 24 24 24 24 24 24 4 4 4 4 3 4 4 4 4 24 24 24 24 24 24 24",
"24 24 24 24 24 24 24 24 24 24 24 24 3 24 24 24 24 24 24 24 24 24 24 24",
"24 24 24 24 24 24 24 24 24 24 24 24 3 24 24 24 24 24 24 24 24 24 24 24",
"24 3 3 3 3 3 3 3 3 3 3 3 3 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 . 124 125 126 127 . . 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 . . . . . . . . . . . . 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"
},
"Ambient": "#a3bbc1",
"Atmosphere": "woodland"
}
]
}
+48 -1
View File
@@ -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
}
]
}
]
}
+54 -7
View File
@@ -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
}
]
}
]
+32
View File
@@ -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"
}
}
]
}
+78
View File
@@ -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
}
]
}
]
}
+7
View File
@@ -0,0 +1,7 @@
scene: rpg_desert
priority: 10
is_one_run: true
save_allowed: true
---
ptext "За последним деревом кончается тень. Скорпионы здесь крупнее местных надежд." key=rpg_plus/desert/enter
end
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
ptext "У очага никто не требует подвига. Редкое место." key=rpg_plus/woodland/hearth/1
action stat.restore who=party
end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
ptext "На камне вырезано: «К святилищу — через пещеру на востоке. Не выносить свет. Не вносить поправки»." key=rpg_plus/woodland/waystone/1
end
@@ -0,0 +1,3 @@
ptext "На верстаке лежит инструкция: «Не пробовать. Не нюхать. Если изделие заговорило — мастер не виноват»." key=rpg_plus/woodland/workbench/1
action craft.open station=rpg_plus/workbench
end
+5
View File
@@ -0,0 +1,5 @@
{
"name": "rpg_desert",
"defaultBackground": "desert/outpost",
"defaultAmbient": ""
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "rpg_woodland_shrine",
"defaultBackground": "village/forest",
"defaultAmbient": ""
}
+14
View File
@@ -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"
]
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Desert and supply barrel test",
"description": "West/east transitions, one-time barrel loot, save/load and both desert monster battles.",
"sources": ["src/*.cs"],
"depends": ["rpg_plus_game>=1", "ff_battle>=1"]
}
+212
View File
@@ -0,0 +1,212 @@
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.Games;
using Cthangover.Core.Items;
using Cthangover.Core.Scenes;
using Cthangover.Core.Settings;
using Cthangover.Core.UI;
using Cthangover.Core.Utils;
using Cthangover.Mods.Dialog;
using Cthangover.Mods.Interface.View;
using Cthangover.Mods.Rpg;
using Cthangover.FFBattle.UI;
using Godot;
/// <summary>
/// Exercises authored data through real interaction, battle rewards and save/load.
/// Enemy HP is reduced only after the battle starts, keeping the UI victory/loot path.
/// </summary>
public partial class DesertTestDriver : Node
{
private const string Slot = "slot_9918";
private const string VillageBarrel = "supply_rpg_village_19_11";
private readonly Queue<int> choices = new(new[] { 0, 1 });
private readonly string shots;
private int step, ticks, passed, failed, coins, branches, elixirs, stones, resin;
private double elapsed;
private ulong nextAttack;
private BattleSceneContext testedBattle;
private Key? releaseKey;
private static RpgWorldView View => RpgWorldHost.Current?.View;
private static DialogBox Box => RpgInput.FindDialogBox();
private static IInventory Bag => GameData.Instance.Runtime.Inventory;
public DesertTestDriver(string shots)
{
this.shots = shots;
Name = "DesertTestDriver";
var timer = new Timer { WaitTime = 0.1, Autostart = true };
timer.Timeout += Tick;
AddChild(timer);
}
private void Tick()
{
if (step < 0) return;
if (releaseKey.HasValue)
{
var key = releaseKey.Value;
Input.ParseInputEvent(new InputEventKey { PhysicalKeycode = key, Keycode = key, Pressed = false });
releaseKey = null;
}
elapsed += 0.1;
if (elapsed > 180) { Check(false, $"timeout at step {step}"); Finish(); return; }
try { RunStep(); }
catch (Exception ex) { Check(false, ex.ToString()); Finish(); }
}
private void RunStep()
{
var scene = GetTree().Root.GetNodeOrNull<SceneManager>("SceneManager")?.CurrentSceneName;
if (!Boot.IsReady || string.IsNullOrEmpty(scene)) return;
if (Box?.IsRunning == true)
{
if (Box.IsWaitingAnswer) Box.TrySelectVariant(choices.Count > 0 ? choices.Dequeue() : 0);
else Box.NextAction();
return;
}
if (GodotSceneService.IsTransitioning || ++ticks < 3) return;
switch (step)
{
case 0:
if (View?.Map.ID != "rpg_village") return;
Check(View.FindEvent("village_to_desert")?.Current.Teleport.Map == "rpg_desert", "west exit points to desert");
Check(View.CanEnter(new Vector2I(0, 9)), "west village border is passable");
coins = Bag.CheckCount("money/coin"); branches = Bag.CheckCount("rpg_demo/branches");
View.PlacePlayer(19, 12, RpgDirection.Up);
Check(View.Interact(), "closed supply barrel can be opened");
Next(); return;
case 1:
Check(Bag.CheckCount("money/coin") == coins + 2 && Bag.CheckCount("rpg_demo/branches") == branches + 1, "barrel grants the authored small loot");
Check(View.FindEvent(VillageBarrel)?.Current.Sprite == "desert/barrel_open", "barrel switches to open graphic");
View.Interact(); Next(); return;
case 2:
Check(Bag.CheckCount("money/coin") == coins + 2 && Bag.CheckCount("rpg_demo/branches") == branches + 1, "opening twice does not duplicate loot");
SaveService.Save(Slot);
Check(GameSession.LoadAndEnter(this, Slot), "barrel save reloads");
Next(); return;
case 3:
if (View?.Map.ID != "rpg_village") return;
Check(View.FindEvent(VillageBarrel)?.PageIndex == 1, "open barrel survives reload");
Check(Bag.CheckCount("money/coin") == coins + 2, "barrel coins survive reload without duplication");
View.PlacePlayer(1, 9, RpgDirection.Left); Tap(Key.Left); Next(); return;
case 4:
if (View?.Map.ID != "rpg_desert") return;
Check(View.Player.Cell == new Vector2I(38, 12), "walking west arrives at the east desert entrance");
Check(View.Tileset.ID == "desert", "desert uses its own art and sand surface");
Check(View.FindEvent("desert_scorpion_1")?.Visible == true && View.FindEvent("desert_beetle_2")?.Visible == true, "both desert monster species are present");
Check(View.CanEnter(new Vector2I(39, 12)), "return passage is open");
Shot("desert");
coins = Bag.CheckCount("money/coin"); elixirs = Bag.CheckCount("rpg_demo/elixir");
View.PlacePlayer(17, 9, RpgDirection.Up); Check(View.Interact(), "desert supply barrel opens"); Next(); return;
case 5:
Check(Bag.CheckCount("money/coin") == coins + 3 && Bag.CheckCount("rpg_demo/elixir") == elixirs + 1, "desert barrel grants coins and an elixir");
View.Interact(); Next(); return;
case 6:
Check(Bag.CheckCount("money/coin") == coins + 3 && Bag.CheckCount("rpg_demo/elixir") == elixirs + 1, "desert barrel is repeat safe");
stones = Bag.CheckCount("rpg_demo/stones");
View.PlacePlayer(26, 12, RpgDirection.Right);
Check(View.Interact(), "scorpion interaction starts its battle"); Next(); return;
case 7:
if (!Win("rpg_desert_scorpion")) return;
Next(); return;
case 8:
if (!ContinueVictory()) return;
Next(); return;
case 9:
if (View?.Map.ID != "rpg_desert") return;
Check(View.FindEvent("desert_scorpion_1")?.Visible == false, "defeated scorpion disappears");
Check(Bag.CheckCount("rpg_demo/stones") >= stones + 1, "scorpion victory grants its loot");
resin = Bag.CheckCount("rpg_plus/resin");
View.PlacePlayer(22, 14, RpgDirection.Up);
Check(View.Interact(), "beetle interaction starts its battle"); Next(); return;
case 10:
if (!Win("rpg_desert_beetle")) return;
Next(); return;
case 11:
if (!ContinueVictory()) return;
Next(); return;
case 12:
if (View?.Map.ID != "rpg_desert") return;
Check(View.FindEvent("desert_beetle_2")?.Visible == false, "defeated beetle disappears");
Check(Bag.CheckCount("rpg_plus/resin") >= resin + 1, "beetle grants its loot");
SaveService.Save(Slot);
Check(GameSession.LoadAndEnter(this, Slot), "desert progress reloads");
Next(); return;
case 13:
if (View?.Map.ID != "rpg_desert") return;
Check(View.FindEvent("desert_scorpion_1")?.Visible == false && View.FindEvent("desert_beetle_2")?.Visible == false, "defeated monsters stay gone after reload");
Check(View.FindEvent("desert_barrel_supply")?.PageIndex == 1, "desert barrel stays open after reload");
View.PlacePlayer(38, 12, RpgDirection.Right); Tap(Key.Right); Next(); return;
case 14:
if (View?.Map.ID != "rpg_village") return;
Check(View.Player.Cell == new Vector2I(1, 9), "east desert exit returns inside west village edge");
Check(View.FindEvent(VillageBarrel)?.PageIndex == 1, "village barrel remains open after the round trip");
SaveService.DeleteSave(Slot); Finish(); return;
}
}
private bool Win(string id)
{
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<FFMenuPanel>().FirstOrDefault();
if (enemies == null || players == null || menu == null) return false;
if (testedBattle != battle)
{
testedBattle = battle;
Check(enemies.Widgets.Count == 1 && enemies.Widgets[0].Card.ID == id, "battle contains " + id);
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 bool ContinueVictory()
{
if (GetTree().Root.FindChild("ContinueButton", true, false) is not Button button || !button.IsVisibleInTree()) return false;
button.EmitSignal(BaseButton.SignalName.Pressed); return true;
}
private static void Click(Control c) => c.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
private void Tap(Key key)
{
// Movement samples held input in _Process; retain the press until
// the next timer tick so at least one real simulation frame sees it.
Input.ParseInputEvent(new InputEventKey { PhysicalKeycode = key, Keycode = key, Pressed = true });
releaseKey = key;
}
private void Shot(string name)
{
if (string.IsNullOrEmpty(shots) || DisplayServer.GetName() == "headless") return;
DirAccess.MakeDirRecursiveAbsolute(shots);
ViewportCapture.SavePng(GetViewport(), shots.TrimEnd('/') + "/" + name + ".png");
}
private void Next() { step++; ticks = 0; }
private void Check(bool ok, string message)
{
if (ok) passed++; else failed++;
GameLogger.Log("TEST", (ok ? "PASS " : "FAIL ") + message);
}
private void Finish()
{
step = -1;
GameLogger.Log("TEST", $"[TEST_RESULT] name=desert passed={passed} failed={failed}");
GetTree().Quit(failed == 0 ? 0 : 1);
}
}
}
@@ -0,0 +1,23 @@
namespace Cthangover.Mods.RpgPlus.Tests
{
using System.Linq;
using Cthangover.Core.Mods;
using Godot;
/// <summary>Mounts the opt-in desert playthrough without adding production gameplay code.</summary>
public class DesertTestInitializer : IModInitializer
{
private static bool queued;
public void OnModLoaded(string id) { }
public void OnModResourcesReady()
{
var args = OS.GetCmdlineUserArgs();
if (queued || !args.Contains("--itest=desert")) return;
queued = true;
string shots = null;
foreach (var arg in args)
if (arg.StartsWith("--shots=")) shots = arg.Substring(8);
ModRoot.AddDeferred(new DesertTestDriver(shots));
}
}
}
+2 -2
View File
@@ -419,9 +419,9 @@ namespace Cthangover.Mods.RpgPlus.Tests
nightPhase++;
return false;
case 1:
var material = View.GetNodeOrNull<TileMapLayer>("Main")?.Material as ShaderMaterial;
var material = View.Presentation?.SharedMaterial;
Check(material != null, "the tiles have the map's light material");
Check(material != null && View.Player.Material == material, "the player shares it");
Check(material != null && View.Presentation.ActorsShareMaterial, "the player shares it");
var weights = material?.GetShaderParameter("time_weights").AsVector4() ?? Vector4.Zero;
Check(Mathf.IsEqualApprox(weights.W, 1f), "23:00 lights the map as night");
Check((material?.GetShaderParameter("light_count").AsInt32() ?? 0) >= 4, "the player, the lamp and windows give light");
+6
View File
@@ -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"]
}
@@ -0,0 +1,486 @@
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;
/// <summary>
/// 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.
/// </summary>
public partial class WoodlandTestDriver : Node
{
private readonly Queue<int> choices = new();
private readonly bool pact;
private readonly string shots;
private readonly bool terrainReview;
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);
/// <summary>Two deterministic choice paths share the same checks and isolated test slot.</summary>
public WoodlandTestDriver(bool pact, string shots)
{
this.pact = pact;
this.shots = shots;
terrainReview = OS.GetCmdlineUserArgs().Contains("--terrain-review");
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>("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;
Shot("interior");
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<FFMenuPanel>().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<VBoxContainer>("Panel/Margin/Root/Body/Scroll/Recipes").GetChildren().OfType<CraftRecipeButton>())
if (row.RecipeId == id)
{
row.EmitSignal(BaseButton.SignalName.Pressed);
break;
}
var button = panel.GetNode<Button>("Panel/Margin/Root/Body/Details/Craft");
Check(!button.Disabled, $"recipe {id} has enough materials");
if (!button.Disabled) button.EmitSignal(BaseButton.SignalName.Pressed);
}
private static void Tap(Key key)
{
Input.ParseInputEvent(new InputEventKey { PhysicalKeycode = key, Keycode = key, Pressed = true });
Input.ParseInputEvent(new InputEventKey { PhysicalKeycode = key, Keycode = key, Pressed = false });
}
private void Next() { step++; ticks = 0; }
private void CheckTrees()
{
var grid = RpgMapGrid.Parse(View.Map);
var complete = true;
var rootsSolid = true;
var fragments = 0;
var roots = 0;
var invisibleWalls = 0;
for (var y = 0; y < grid.Height; y++)
for (var x = 0; x < grid.Width; x++)
{
if (x > 0 && y > 0 && x < grid.Width-1 && y < grid.Height-1
&& grid.GetCollision(x, y) == RpgCollision.Block
&& grid.GetTile(RpgLayer.Main, x, y) < 0 && grid.GetTile(RpgLayer.Front, x, y) < 0
&& !View.Tileset.IsSolid(grid.GetTile(RpgLayer.Back, x, y)))
invisibleWalls++;
foreach (var layer in new[] { RpgLayer.Main, RpgLayer.Front })
{
var tile = grid.GetTile(layer, x, y);
if (tile < 64 || tile > 72) continue;
fragments++;
if (tile != 71) continue;
roots++;
rootsSolid &= !View.CanEnter(new Vector2I(x, y));
for (var dy = 0; dy < 3; dy++)
for (var dx = 0; dx < 3; dx++)
complete &= grid.GetTile(dy == 2 ? RpgLayer.Main : RpgLayer.Front, x-1+dx, y-2+dy) == 64+dy*3+dx;
}
}
Check(roots > 0 && complete && fragments == roots * 9, View.Map.ID + " has complete trees without orphan fragments");
Check(rootsSolid, View.Map.ID + " tree trunks block walking");
Check(invisibleWalls == 0, View.Map.ID + " has no invisible interior collision cells");
}
private void Check(bool condition, string message)
{
if (condition) passed++; else failed++;
GameLogger.Log("TEST", (condition ? "[PASS] " : "[FAIL] ") + message, condition ? LogLevel.Message : LogLevel.Error);
}
private bool CaptureVillage()
{
if (string.IsNullOrEmpty(shots))
return true;
var time = GameData.Instance.Runtime.Time;
if (villageShotPhase == 0)
{
Shot("village_night");
shotTime = time.Tick;
time.SetTime(shotTime - shotTime % (24 * 60) + 12 * 60);
villageShotPhase = 1;
ticks = 0;
return false;
}
if (villageShotPhase == 1)
{
Shot("village");
if (terrainReview)
CheckWaterMaterials();
else
time.SetTime(shotTime);
villageShotPhase = 2;
}
return true;
}
private void CheckWaterMaterials()
{
var tileset = View.Tileset;
var textures = new RpgTextures();
Check(tileset.Image == "village/objects" && tileset.SurfaceImage == "village/surfaces",
"objects and surfaces use separate atlases");
Check(tileset.TileOf(40)?.Surface != null && tileset.AutotileOf(40) == null,
"water uses a base material with runtime diffusion");
Check(tileset.Tiles.Count(t => t.Surface != null) == 7 &&
tileset.Tiles.All(t => t.Index < 192 || t.Index >= 1000 && t.Index < 1120),
"only seven base surfaces remain, with no baked phase or transition ids");
var roomTop = tileset.TileOf(1000)?.AtlasIndex ?? -1;
var roomPacked = roomTop >= 0 && roomTop % tileset.Columns + 12 <= tileset.Columns;
for (var cell = 0; cell < 120; cell++)
roomPacked &= tileset.TileOf(1000 + cell)?.AtlasIndex == roomTop + cell / 12 * tileset.Columns + cell % 12;
Check(roomPacked, "the authored room occupies a connected 12x10 atlas block");
Check(View.Presentation?.SurfaceChunkCount > 0,
"the world renders the computed surface layer");
var caveTop = tileset.TileOf(128)?.AtlasIndex ?? -1;
Check(caveTop >= 0 && caveTop % tileset.Columns + 1 < tileset.Columns &&
tileset.TileOf(129)?.AtlasIndex == caveTop + 1 &&
tileset.TileOf(27)?.AtlasIndex == caveTop + tileset.Columns &&
tileset.TileOf(130)?.AtlasIndex == caveTop + tileset.Columns + 1,
"the cave occupies a complete 2x2 atlas rectangle");
var lampTop = tileset.TileOf(29)?.AtlasIndex ?? -1;
Check(lampTop >= 0 && tileset.TileOf(28)?.AtlasIndex == lampTop + tileset.Columns,
"the streetlamp head and foot form one vertical atlas rectangle");
var grid = RpgMapGrid.Parse(View.Map);
var generated = RpgSurfaceRenderer.Build(grid, RpgLayer.Back, tileset, textures);
Check(generated?.GetWidth() == grid.Width * tileset.TileSize,
"diffusion is generated from the current map at runtime");
generated?.Dispose();
}
private void Shot(string name)
{
if (string.IsNullOrEmpty(shots) || DisplayServer.GetName() == "headless") return;
DirAccess.MakeDirRecursiveAbsolute(shots);
ViewportCapture.SavePng(GetViewport(), shots.TrimEnd('/') + "/woodland_" + name + ".png");
}
private void Finish()
{
step = -1;
GameLogger.Log("TEST", $"[TEST_RESULT] name={TestName} passed={passed} failed={failed}");
GetTree().Quit(failed == 0 ? 0 : 1);
}
}
}
@@ -0,0 +1,29 @@
namespace Cthangover.Mods.RpgPlus.Tests
{
using System.Linq;
using Cthangover.Core.Mods;
using Godot;
/// <summary>Inert outside its opt-in suite; the story mod stays entirely data and scenarios.</summary>
public class WoodlandTestInitializer : IModInitializer
{
private static bool queued;
/// <inheritdoc />
public void OnModLoaded(string id) { }
/// <inheritdoc />
public void OnModResourcesReady()
{
var args = OS.GetCmdlineUserArgs();
if (queued || !args.Contains("--itest=woodland"))
return;
queued = true;
string shots = null;
foreach (var arg in args)
if (arg.StartsWith("--shots="))
shots = arg.Substring(8);
ModRoot.AddDeferred(new WoodlandTestDriver(args.Contains("--ward-ending=pact"), shots));
}
}
}