Desert notes after retiring the alternative renders; mod documentation

This commit is contained in:
cthangover committed 2026-10-29 09:26:13 +01:00
1 parent c2a5074337
commit 36882e50cf
40 files changed
+1867 -70

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ obj/
gen/ gen/
obj/ obj/
bin/ bin/
art_sources/
*.tres~ *.tres~
*.tscn~ *.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.
+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. **某个对话选项不见了。** 有些选项只有推进到某个任务阶段或持有特定物品后才会出现。
+5 -1
View File
@@ -1,5 +1,5 @@
mod_rpg_plus_game_name = RPG Plus 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/name = Living resin
rpg_plus/item/resin/desc = The slime left this instead of a will. Burns even in the rain. rpg_plus/item/resin/desc = The slime left this instead of a will. Burns even in the rain.
@@ -33,3 +33,7 @@ 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/recipe/lantern/desc = Amber, two resin and two branch bundles. Light needs a frame.
rpg_plus/station/name = The well workbench rpg_plus/station/name = The well workbench
rpg_plus/map/shrine = The forest remembers 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_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_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_name = RPG Plus
mod_rpg_plus_game_desc = RPG Plus 샘플의 이야기. RPG 데모 옆이 아니라 그 위에 만들었습니다: 갈림길의 노벨식 프롤로그에서 직업을 고르고 첫 매력 판정을 하고, 이어서 rpg_demo의 마을과 숲 전체를 한 줄도 고치지 않고 — 스승과 점쟁이는 이 모드의 자체 맵 이벤트 파일로 추가, 수련장 장면, 상인의 장비(상점 패치), 능력치를 가진 늑대(캐릭터 패치). 데이터와 시나리오만. mod_rpg_plus_game_desc = 완성된 소규모 RPG: 클래스를 고르는 프롤로그, 마을과 숲의 미스터리, 제작, 거래, 사막, 턴제 전투. 데이터와 시나리오만 있습니다.
+5 -1
View File
@@ -1,5 +1,5 @@
mod_rpg_plus_game_name = RPG Plus 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/name = Живая смола
rpg_plus/item/resin/desc = Слизень оставил её вместо завещания. Горит даже под дождём. rpg_plus/item/resin/desc = Слизень оставил её вместо завещания. Горит даже под дождём.
@@ -33,3 +33,7 @@ rpg_plus/recipe/lantern/name = Сторожевой фонарь
rpg_plus/recipe/lantern/desc = Янтарное сердце, две смолы, две связки веток. Свету нужен каркас. rpg_plus/recipe/lantern/desc = Янтарное сердце, две смолы, две связки веток. Свету нужен каркас.
rpg_plus/station/name = Верстак у колодца rpg_plus/station/name = Верстак у колодца
rpg_plus/map/shrine = Лес помнит rpg_plus/map/shrine = Лес помнит
rpg_plus/desert/map/name=Сухой предел
rpg_plus/desert/enemy/scorpion/name=Песчаный скорпион
rpg_plus/desert/enemy/beetle/name=Пустынный жук
+18
View File
@@ -95,3 +95,21 @@ rpg_plus/woodland/lamplighter/3 = “Brighter forest. Darker village. At least t
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/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/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.
+18
View File
@@ -95,3 +95,21 @@ rpg_plus/woodland/lamplighter/3 = — В лесу светлее. В дерев
rpg_plus/woodland/elder/ready = — Собрал? Тогда через пещеру к святилищу. И не поднимай створки, пока не решишь, кому светить. rpg_plus/woodland/elder/ready = — Собрал? Тогда через пещеру к святилищу. И не поднимай створки, пока не решишь, кому светить.
rpg_plus/woodland/amber/recovered = Сердце янтаря осталось у пещеры. На этот раз забираешь его с собой. 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_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_name = RPG Plus
mod_rpg_plus_game_desc = RPG Plus 示例的故事,建立在 RPG 演示之上而非之旁:十字路口的小说式序章,选择职业并进行第一次魅力检定;之后是 rpg_demo 的整个村庄和森林,一处未改——导师和占卜师由本模组自己的地图事件文件加入,练武场场景,商人处的装备(商店补丁),带属性的狼(角色补丁)。仅有数据和剧本。 mod_rpg_plus_game_desc = 一款完整的小型 RPG:选择职业的序章、村庄与森林之谜、制作、交易、沙漠和回合制战斗——只有数据和剧本。
+7 -1
View File
@@ -3,7 +3,7 @@
"name": "RPG Plus", "name": "RPG Plus",
"version": "1.0.0", "version": "1.0.0",
"author": "ct", "author": "ct",
"description": "A woodland RPG with a class prologue, a village ward mystery, an apothecary and missing courier, a ruined shrine with two endings, monster loot and recipes. World and story are data and scenarios; crafting is a separate reusable mechanic.", "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", "category": "story",
"depends": [ "depends": [
"rpg_demo_game>=1", "rpg_demo_game>=1",
@@ -31,6 +31,12 @@
"activate": [ "activate": [
"--itest=woodland" "--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"
]
}
]
}
]
}
+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"
}
}
]
}
+11 -7
View File
@@ -4,6 +4,8 @@
"Id": "shrine_exit", "Id": "shrine_exit",
"X": 1, "X": 1,
"Y": 11, "Y": 11,
"Tile": -1,
"Dir": "Down",
"Trigger": "Touch", "Trigger": "Touch",
"Teleport": { "Teleport": {
"Map": "rpg_forest", "Map": "rpg_forest",
@@ -17,14 +19,12 @@
"X": 12, "X": 12,
"Y": 5, "Y": 5,
"Tile": 36, "Tile": 36,
"Dir": "Down",
"Trigger": "Action",
"Scenario": "rpg_plus/woodland/beacon", "Scenario": "rpg_plus/woodland/beacon",
"Light": {
"Radius": 2,
"Color": "#699baf"
},
"Pages": [ "Pages": [
{ {
"Condition": "rpg_ward_done == 1 && rpg_ward_choice == 'pact'", "Condition": "rpg_ward_done == 1 \u0026\u0026 rpg_ward_choice == \u0027pact\u0027",
"Tile": 36, "Tile": 36,
"Light": { "Light": {
"Radius": 6, "Radius": 6,
@@ -32,14 +32,18 @@
} }
}, },
{ {
"Condition": "rpg_ward_done == 1 && rpg_ward_choice == 'free'", "Condition": "rpg_ward_done == 1 \u0026\u0026 rpg_ward_choice == \u0027free\u0027",
"Tile": 38, "Tile": 38,
"Light": { "Light": {
"Radius": 6, "Radius": 6,
"Color": "#9ce3c7" "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"
}
]
}
+52 -52
View File
@@ -8,69 +8,67 @@
"Width": 24, "Width": 24,
"Height": 16, "Height": 16,
"Zoom": 2, "Zoom": 2,
"Ambient": "#a3bbc1",
"Atmosphere": "woodland",
"Layers": { "Layers": {
"Back": [ "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 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24", "24 24 24 24 24 24 24 24 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 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 6 6 6 6 6 6 6 4 24 24 24 24 24 24 24", "24 24 24 24 24 24 24 24 4 4 4 4 4 4 4 4 4 24 24 24 24 24 24 24",
" 24 24 24 24 24 24 24 24 4 6 6 6 6 6 6 6 4 24 24 24 24 24 24 24", "24 24 24 24 24 24 24 24 4 3 3 3 3 3 3 3 4 24 24 24 24 24 24 24",
" 24 24 24 24 24 24 24 24 4 6 6 6 6 6 6 6 4 24 24 24 24 24 24 24", "24 24 24 24 24 24 24 24 4 3 3 3 3 3 3 3 4 24 24 24 24 24 24 24",
" 24 24 24 24 24 24 24 24 4 6 6 6 160 6 6 6 4 24 24 24 24 24 24 24", "24 24 24 24 24 24 24 24 4 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 170 4 4 4 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 170 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 24 24 24 24 24 24 24 24 24 24 24 170 24 24 24 24 24 24 24 24 24 24 24", "24 24 24 24 24 24 24 24 24 24 24 24 3 24 24 24 24 24 24 24 24 24 24 24",
" 24 173 165 165 165 165 165 165 165 165 165 165 166 24 24 24 24 24 24 24 24 24 24 24", "24 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",
" 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24", "24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24",
" 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24" "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": [ "Main": [
" . . . . . . . . . . . . . . . . . . . . . . . .", ". . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . . . .", ". . . . . . . . . . . . . . . . . . . . . . . .",
" 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 70 71 72",
" . . . . . . . . . . . . . . . . . . . . . . . .", ". . . . . . . . . . . . . . . . . . . . . . . .",
" . . . . . . . . 37 . . . . . . . 37 . . . . . . .", ". . . . . . . . 37 . 124 125 126 127 . . 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 . . . . . 37 . . . . . . . 37 . . . . 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 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": [ "Front": [
" 64 65 66 64 65 66 64 65 66 . . . . . . 64 65 66 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 . 112 113 114 115 . 67 68 69 67 68 69 67 68 69", "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 . . . . . . . . . .", ". . . . . . . . . . 116 117 118 119 . . . . . . . . . .",
" 64 65 66 64 65 66 . . . . 120 121 122 123 . . . . 64 65 66 64 65 66", "64 65 66 64 65 66 . . . . 120 121 122 123 . . . . 64 65 66 64 65 66",
" 67 68 69 67 68 69 . . . . 124 125 126 127 . . . . 67 68 69 67 68 69", "67 68 69 67 68 69 . . . . . . . . . . . . 67 68 69 67 68 69",
" . . . . . . . . . . . . . . . . . . . . . . . .", ". . . . . . . . . . . . . . . . . . . . . . . .",
" 64 65 66 . . . . . . . . . . . . . . . . . . 64 65 66", "64 65 66 . . . . . . . . . . . . . . . . . . 64 65 66",
" 67 68 69 . 64 65 66 . . . . . . . . . . . . . . 67 68 69", "67 68 69 . 64 65 66 . . . . . . . . . . . . . . 67 68 69",
" . . . . 67 68 69 . . . . . . . . . . . . . . . . .", ". . . . 67 68 69 . . . . . . . . . . . . . . . . .",
" . . . . . . . . . . . . . . . . . . . . . 64 65 66", ". . . . . . . . . . . . . . . . . . . . . 64 65 66",
" . . . . . . . . . . . . . . . . . . . . . 67 68 69", ". . . . . . . . . . . . . . . . . . . . . 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", "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", "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": [ "Collision": [
"########################", "########################",
"#......................#", "#......................#",
"#......................#", "#......................#",
"#.........#..#.........#", "#.........####.........#",
"#.........####.........#", "#.........####.........#",
"#......................#", "#......................#",
"#......................#", "#......................#",
@@ -88,7 +86,9 @@
"X": 2, "X": 2,
"Y": 11, "Y": 11,
"Dir": "Right" "Dir": "Right"
} },
"Ambient": "#a3bbc1",
"Atmosphere": "woodland"
} }
] ]
} }
+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
+5
View File
@@ -0,0 +1,5 @@
{
"name": "rpg_desert",
"defaultBackground": "desert/outpost",
"defaultAmbient": ""
}
+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++; nightPhase++;
return false; return false;
case 1: 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, "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; var weights = material?.GetShaderParameter("time_weights").AsVector4() ?? Vector4.Zero;
Check(Mathf.IsEqualApprox(weights.W, 1f), "23:00 lights the map as night"); 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"); Check((material?.GetShaderParameter("light_count").AsInt32() ?? 0) >= 4, "the player, the lamp and windows give light");
+41 -1
View File
@@ -33,6 +33,7 @@ namespace Cthangover.Mods.RpgPlus.Tests
private readonly Queue<int> choices = new(); private readonly Queue<int> choices = new();
private readonly bool pact; private readonly bool pact;
private readonly string shots; private readonly string shots;
private readonly bool terrainReview;
private int step; private int step;
private int ticks; private int ticks;
private int passed; private int passed;
@@ -60,6 +61,7 @@ namespace Cthangover.Mods.RpgPlus.Tests
{ {
this.pact = pact; this.pact = pact;
this.shots = shots; this.shots = shots;
terrainReview = OS.GetCmdlineUserArgs().Contains("--terrain-review");
Name = "WoodlandTestDriver"; Name = "WoodlandTestDriver";
var timer = new Timer { WaitTime = 0.1, Autostart = true }; var timer = new Timer { WaitTime = 0.1, Autostart = true };
timer.Timeout += Tick; timer.Timeout += Tick;
@@ -204,6 +206,7 @@ namespace Cthangover.Mods.RpgPlus.Tests
Go("rpg_elder_house", 6, 4, RpgDirection.Up); Next(); return; Go("rpg_elder_house", 6, 4, RpgDirection.Up); Next(); return;
case 21: case 21:
if (View?.Map.ID != "rpg_elder_house") return; if (View?.Map.ID != "rpg_elder_house") return;
Shot("interior");
Tap(Key.E); Next(); return; Tap(Key.E); Next(); return;
case 22: case 22:
Check(Quest("ward").Data.Status == 2, "elder supplies the lantern recipe objective"); Check(Quest("ward").Data.Status == 2, "elder supplies the lantern recipe objective");
@@ -423,12 +426,49 @@ namespace Cthangover.Mods.RpgPlus.Tests
if (villageShotPhase == 1) if (villageShotPhase == 1)
{ {
Shot("village"); Shot("village");
time.SetTime(shotTime); if (terrainReview)
CheckWaterMaterials();
else
time.SetTime(shotTime);
villageShotPhase = 2; villageShotPhase = 2;
} }
return true; 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) private void Shot(string name)
{ {
if (string.IsNullOrEmpty(shots) || DisplayServer.GetName() == "headless") return; if (string.IsNullOrEmpty(shots) || DisplayServer.GetName() == "headless") return;