Compare commits
6
Commits
v1.0.0
..
650f87c6ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
650f87c6ed | ||
|
|
fe68178a7f | ||
|
|
ab6849d089 | ||
|
|
d1b2cae314 | ||
|
|
68ed11cadc | ||
|
|
dde5262aa0 |
No files matched your search
@@ -12,6 +12,7 @@ obj/
|
||||
gen/
|
||||
obj/
|
||||
bin/
|
||||
art_sources/
|
||||
|
||||
*.tres~
|
||||
*.tscn~
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
+99
@@ -0,0 +1,99 @@
|
||||
# Dialog Engine — the dialog box and the scenario language
|
||||
|
||||
Dialog Engine is what the player reads and clicks: the dialog box with its lines, portraits and answers. Behind it is the **scenario language** — small `.scenario` text files in which a story is written line by line: what is said, which answers there are, where each answer leads, and what happens in the game (a new background, music, an item, a quest step, another scene). Every game in the collection, from the novel template to RPG Plus, tells its story with it.
|
||||
|
||||
## What the player sees
|
||||
|
||||
A scene's scenario starts by itself when the player enters the scene. Lines appear in the box at the bottom; a click shows the next one:
|
||||
|
||||

|
||||
|
||||
When the story asks something, the answers appear on the right. The story waits until one is chosen:
|
||||
|
||||

|
||||
|
||||
"Go into the bar" takes the player to the bar scene, which starts its own scenario:
|
||||
|
||||

|
||||
|
||||
## The scenario behind these pictures
|
||||
|
||||
This is the whole town scenario of the novel template (`my_novel_game/scenarios/my_novel_town.scenario`):
|
||||
|
||||
```scenario
|
||||
scene: my_novel_town
|
||||
priority: 10
|
||||
---
|
||||
# The town is the hub: every visit shows the same choice.
|
||||
text "Evening town. The street lamps are already lit and a cool draught runs along the cobbles." key=my_novel/town/intro
|
||||
select "Where to go?" key=my_novel/town/prompt
|
||||
option "Go into the bar" key=my_novel/town/opt_bar -> :bar
|
||||
option "Go back home" key=my_novel/town/opt_home -> :home
|
||||
|
||||
:bar
|
||||
switch_scene my_novel_bar
|
||||
end
|
||||
|
||||
:home
|
||||
switch_scene my_novel_home
|
||||
end
|
||||
```
|
||||
|
||||
- **The header** above `---` says when the scenario plays: `scene:` names the scene, `priority:` orders several scenarios of one scene (lower plays first). More header lines are below.
|
||||
- `text "…"` shows a line and waits for a click. `key=` is the translation key of the line; the quoted text is what is shown when no translation is found.
|
||||
- `select` asks a question, and each `option` is an answer that jumps to a **label** (`-> :bar`).
|
||||
- `:bar` is a label; `switch_scene` goes to another scene; `end` finishes the scenario.
|
||||
- A line starting with `#` is a note for the author.
|
||||
|
||||
To add a story to a scene, put a `.scenario` file anywhere in your mod's `scenarios/` folder and name the scene in its header.
|
||||
|
||||
## Writing a story
|
||||
|
||||
**Lines and portraits.** `text "…" first=marao/sad` shows a portrait in the first place, `second=…` in the second (see Characters Engine). `ptext` is the same as `text`, and also replaces `${name}` with the value of a variable. `title "…"` shows a chapter title.
|
||||
|
||||
**Choices and jumps.** An answer can be shown only when a condition holds: `option "Pay" "if=item('money/coin') >= 5" -> :pay` (a condition with spaces is quoted together with `if=`). `goto -> :label` jumps without asking. `when "<condition>" -> :label` jumps if the condition holds at that moment:
|
||||
|
||||
```scenario
|
||||
when "rpg_wolf_dead == 1" -> :lantern
|
||||
```
|
||||
|
||||
**Remembering things.** `flag rpg_quest_taken 1` sets a story flag that is saved with the game; conditions read it as `rpg_quest_taken == 1`. `set name value` sets a variable of the running dialog; `ptext` shows it as `${name}` and actions can read it.
|
||||
|
||||
**Conditions** are one shared language: flags, `item('id')` from Items Base, quest checks from Quests Engine, stats and equipment from their mods, joined with `&&`, `||` and `!`.
|
||||
|
||||
**The scene.** `background`, `foreground`, `background_color`, `music`, `sound`, `effect`, `delay`, `light_set`, `hide_dialog`/`show_dialog`, `interactive_add`/`interactive_remove`, `notify`.
|
||||
|
||||
**Game actions.** `action <name> key=value …` runs an action of another mod, for example `action item.add id=money/coin count=3` or `action quest.set_status quest_id=woodland_ward status=Progress`. The schema files of the mods list the actions, and Editor Tools offers them in its scenario editor and command palette.
|
||||
|
||||
## When a scenario plays
|
||||
|
||||
The header decides it:
|
||||
|
||||
```scenario
|
||||
scene: prolog_alley
|
||||
priority: 15
|
||||
condition: AwakeningQuest.notHasTag("cloak_taken")
|
||||
is_one_run: true
|
||||
save_allowed: true
|
||||
---
|
||||
```
|
||||
|
||||
- `scene:` — the scene where it plays when the player arrives.
|
||||
- `priority:` — order among scenarios of the same scene; the lowest number that may play goes first, and the next one starts when the dialog ends.
|
||||
- `condition:` — it plays only while the condition holds; when a flag or quest changes, the scene checks again.
|
||||
- `is_one_run: true` — it plays only once per game.
|
||||
- `after:` — it waits until the named scenario has played.
|
||||
- `save_allowed: true` — the player may save in this scene.
|
||||
|
||||
A scenario without a `scene:` header is not started by a scene; it is run by something else, such as a click on an interactive object or a map event.
|
||||
|
||||
## Other mods add and replace scenarios
|
||||
|
||||
Scenarios of all mods are collected together. A mod adds a story to someone else's scene simply by naming that scene. A mod that depends on another and ships a scenario with the same file name replaces it.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
1. **A scenario never plays.** Its `scene:` name is wrong, its `condition` is false, or it has `is_one_run` and has already played.
|
||||
2. **The line is shown in the wrong language.** The `key=` is missing from the mod's `locale` files for that language; the quoted text is shown instead.
|
||||
3. **An answer leads nowhere.** The label after `->` must exist in the same file, written with a colon (`:bar`).
|
||||
4. **An `action` does nothing.** The mod that owns the action is not part of the game (the log says the action "is not registered"), or a parameter name is wrong — compare it with the action in the scenario editor of Editor Tools.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Dialog Engine — la caja de diálogo y el lenguaje de escenarios
|
||||
|
||||
Dialog Engine es lo que el jugador lee y pulsa: la caja de diálogo con sus líneas, retratos y respuestas. Detrás está el **lenguaje de escenarios** — pequeños archivos de texto `.scenario` en los que la historia se escribe línea a línea: quién dice qué, qué respuestas hay, adónde conduce cada una y qué ocurre en el juego (un fondo nuevo, música, un objeto, un paso de misión, otra escena). Con él cuentan su historia todos los juegos de la colección, desde la plantilla de novela hasta RPG Plus.
|
||||
|
||||
## Lo que ve el jugador
|
||||
|
||||
El escenario de una escena arranca solo cuando el jugador entra en ella. Las líneas aparecen en la caja inferior; un clic muestra la siguiente:
|
||||
|
||||

|
||||
|
||||
Cuando la historia hace una pregunta, las respuestas aparecen a la derecha, y espera a que se elija una:
|
||||
|
||||

|
||||
|
||||
«Entrar al bar» lleva al jugador a la escena del bar, que arranca su propio escenario:
|
||||
|
||||

|
||||
|
||||
## El escenario detrás de estas imágenes
|
||||
|
||||
Este es el escenario completo del pueblo de la plantilla de novela (`my_novel_game/scenarios/my_novel_town.scenario`):
|
||||
|
||||
```scenario
|
||||
scene: my_novel_town
|
||||
priority: 10
|
||||
---
|
||||
# El pueblo es el centro: en cada visita aparece la misma elección.
|
||||
text "El pueblo, al anochecer. Las farolas ya están encendidas y un aire fresco corre entre los adoquines." key=my_novel/town/intro
|
||||
select "¿A dónde ir?" key=my_novel/town/prompt
|
||||
option "Entrar al bar" key=my_novel/town/opt_bar -> :bar
|
||||
option "Volver a casa" key=my_novel/town/opt_home -> :home
|
||||
|
||||
:bar
|
||||
switch_scene my_novel_bar
|
||||
end
|
||||
|
||||
:home
|
||||
switch_scene my_novel_home
|
||||
end
|
||||
```
|
||||
|
||||
- **La cabecera**, sobre `---`, dice cuándo se reproduce el escenario: `scene:` nombra la escena y `priority:` ordena varios escenarios de una misma escena (se reproduce antes el número más bajo). Más adelante hay más líneas de cabecera.
|
||||
- `text "…"` muestra una línea y espera un clic. `key=` es la clave de traducción de la línea; el texto entrecomillado es lo que se muestra cuando no hay traducción.
|
||||
- `select` plantea una pregunta, y cada `option` es una respuesta que salta a una **etiqueta** (`-> :bar`).
|
||||
- `:bar` es una etiqueta; `switch_scene` pasa a otra escena; `end` termina el escenario.
|
||||
- Una línea que empieza por `#` es una nota para el autor.
|
||||
|
||||
Para añadir una historia a una escena, coloca un archivo `.scenario` en cualquier punto de la carpeta `scenarios/` de tu mod y nombra la escena en la cabecera.
|
||||
|
||||
## Escribir una historia
|
||||
|
||||
**Líneas y retratos.** `text "…" first=marao/sad` muestra un retrato en el primer puesto, `second=…` en el segundo (véase Characters Engine). `ptext` hace lo mismo que `text` y además sustituye `${name}` por el valor de una variable. `title "…"` muestra el título de un capítulo.
|
||||
|
||||
**Opciones y saltos.** Una respuesta puede mostrarse solo mientras se cumpla una condición: `option "Pay" "if=item('money/coin') >= 5" -> :pay` (una condición con espacios se entrecomilla junto con `if=`). `goto -> :label` salta sin preguntar. `when "<condition>" -> :label` salta si la condición se cumple en ese momento:
|
||||
|
||||
```scenario
|
||||
when "rpg_wolf_dead == 1" -> :lantern
|
||||
```
|
||||
|
||||
**Recordar cosas.** `flag rpg_quest_taken 1` activa un marcador argumental que se guarda con la partida; las condiciones lo leen como `rpg_quest_taken == 1`. `set name value` asigna una variable del diálogo en curso: `ptext` la muestra como `${name}` y las acciones pueden leerla.
|
||||
|
||||
**Las condiciones** son un único lenguaje común: marcadores, `item('id')` de Items Base, comprobaciones de misiones de Quests Engine, atributos y equipo de sus respectivos mods, unidos con `&&`, `||` y `!`.
|
||||
|
||||
**La escena.** `background`, `foreground`, `background_color`, `music`, `sound`, `effect`, `delay`, `light_set`, `hide_dialog`/`show_dialog`, `interactive_add`/`interactive_remove`, `notify`.
|
||||
|
||||
**Acciones de juego.** `action <name> key=value …` ejecuta una acción de otro mod, por ejemplo `action item.add id=money/coin count=3` o `action quest.set_status quest_id=woodland_ward status=Progress`. Los archivos schema de los mods enumeran las acciones, y Editor Tools las ofrece en su editor de escenarios y en la paleta de comandos.
|
||||
|
||||
## Cuándo se reproduce un escenario
|
||||
|
||||
Lo decide la cabecera:
|
||||
|
||||
```scenario
|
||||
scene: prolog_alley
|
||||
priority: 15
|
||||
condition: AwakeningQuest.notHasTag("cloak_taken")
|
||||
is_one_run: true
|
||||
save_allowed: true
|
||||
---
|
||||
```
|
||||
|
||||
- `scene:` — la escena donde se reproduce cuando el jugador llega.
|
||||
- `priority:` — el orden entre los escenarios de una misma escena; va primero el número más bajo que pueda reproducirse, y el siguiente empieza cuando termina el diálogo.
|
||||
- `condition:` — solo se reproduce mientras se cumple la condición; cuando cambia un marcador o una misión, la escena vuelve a comprobar.
|
||||
- `is_one_run: true` — se reproduce una sola vez por partida.
|
||||
- `after:` — espera a que se haya reproducido el escenario indicado.
|
||||
- `save_allowed: true` — el jugador puede guardar en esta escena.
|
||||
|
||||
Un escenario sin cabecera `scene:` no lo arranca ninguna escena: lo ejecuta otra cosa, como un clic sobre un objeto interactivo o un evento del mapa.
|
||||
|
||||
## Los demás mods añaden y reemplazan escenarios
|
||||
|
||||
Los escenarios de todos los mods se reúnen en un mismo conjunto. Un mod añade una historia a la escena de otro con solo nombrarla. Y un mod que depende de otro e incluye un escenario con el mismo nombre de archivo lo reemplaza.
|
||||
|
||||
## Si algo falla
|
||||
|
||||
1. **El escenario nunca se reproduce.** El nombre de `scene:` es incorrecto, la `condition` es falsa, o tiene `is_one_run` y ya se reprodujo.
|
||||
2. **La línea aparece en otro idioma.** Falta la clave `key=` en los archivos `locale` del mod para ese idioma, y se muestra el texto entrecomillado.
|
||||
3. **Una respuesta no lleva a ninguna parte.** La etiqueta tras `->` debe existir en el mismo archivo, escrita con dos puntos (`:bar`).
|
||||
4. **Una `action` no hace nada.** El mod dueño de la acción no forma parte del juego (el registro dice que la acción "is not registered") o hay un nombre de parámetro equivocado: compáralo con la acción en el editor de escenarios de Editor Tools.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Dialog Engine — ダイアログボックスとシナリオ言語
|
||||
|
||||
Dialog Engine は、プレイヤーが読んでクリックする部分すべてを担います。台詞・ポートレート・選択肢を表示するダイアログボックスであり、その背後には**シナリオ言語**があります。`.scenario` という小さなテキストファイルに物語を一行ずつ書いていく言語で、誰が何を言うか、どんな選択肢があり、それぞれがどこへ続くか、ゲーム内で何が起きるか(背景の切り替え、音楽、アイテム、クエストの進行、別シーンへの移動)をすべて記述します。コレクションに収録されたすべてのゲームが、小説テンプレートから RPG Plus まで、この言語で物語を語ります。
|
||||
|
||||
## プレイヤーに見えるもの
|
||||
|
||||
シーンに入ると、そのシーンのシナリオは自動的に始まります。台詞が画面下部のボックスに表示され、クリックで次の台詞に進みます。
|
||||
|
||||

|
||||
|
||||
物語が何かを尋ねるときは、選択肢が右側に現れます。選ばれるまで物語は待ちます。
|
||||
|
||||

|
||||
|
||||
「バーに入る」を選ぶと、プレイヤーはバーのシーンへ移動し、そこのシナリオが始まります。
|
||||
|
||||

|
||||
|
||||
## これらの画面の裏にあるシナリオ
|
||||
|
||||
小説テンプレートの町のシナリオ全文がこちらです(`my_novel_game/scenarios/my_novel_town.scenario`)。
|
||||
|
||||
```scenario
|
||||
scene: my_novel_town
|
||||
priority: 10
|
||||
---
|
||||
# 町はハブ:訪れるたびに同じ選択肢が表示される。
|
||||
text "夕暮れの町。街灯はすでにともり、石畳を涼しい風が抜けていく。" key=my_novel/town/intro
|
||||
select "どこへ行く?" key=my_novel/town/prompt
|
||||
option "バーに入る" key=my_novel/town/opt_bar -> :bar
|
||||
option "家に帰る" key=my_novel/town/opt_home -> :home
|
||||
|
||||
:bar
|
||||
switch_scene my_novel_bar
|
||||
end
|
||||
|
||||
:home
|
||||
switch_scene my_novel_home
|
||||
end
|
||||
```
|
||||
|
||||
- `---` より上の**ヘッダー**は、シナリオがいつ再生されるかを決めます。`scene:` はシーン名、`priority:` は同じシーンの複数シナリオの順序(小さいほうが先)です。ヘッダー行はこのほかにもあります。
|
||||
- `text "…"` は台詞を表示してクリックを待ちます。`key=` は台詞の翻訳キーで、引用したテキストは翻訳が見つからないときに表示されます。
|
||||
- `select` は質問を出し、各 `option` は**ラベル**(`-> :bar`)へ飛ぶ選択肢です。
|
||||
- `:bar` はラベル、`switch_scene` は別のシーンへの移動、`end` はシナリオの終了です。
|
||||
- `#` で始まる行は作者向けのメモです。
|
||||
|
||||
シーンに物語を追加するには、`.scenario` ファイルを自分の mod の `scenarios/` フォルダーのどこかに置き、ヘッダーでシーン名を指定します。
|
||||
|
||||
## 物語を書く
|
||||
|
||||
**台詞とポートレート。** `text "…" first=marao/sad` は一つ目の位置にポートレートを表示し、`second=…` は二つ目に表示します(Characters Engine を参照)。`ptext` は `text` と同じ動作に加え、`${name}` を変数の値で置き換えます。`title "…"` は章タイトルを表示します。
|
||||
|
||||
**選択肢とジャンプ。** 条件が満たされたときだけ選択肢を表示できます。`option "Pay" "if=item('money/coin') >= 5" -> :pay`(空白を含む条件は `if=` ごと引用符で囲みます)。`goto -> :label` は尋ねずにジャンプします。`when "<condition>" -> :label` は、その時点で条件が満たされていればジャンプします。
|
||||
|
||||
```scenario
|
||||
when "rpg_wolf_dead == 1" -> :lantern
|
||||
```
|
||||
|
||||
**状態の記憶。** `flag rpg_quest_taken 1` は、ゲームのセーブと一緒に保存される物語フラグを立てます。条件では `rpg_quest_taken == 1` と読みます。`set name value` は実行中のダイアログの変数を設定します。`ptext` はこれを `${name}` として表示し、アクションからも読めます。
|
||||
|
||||
**条件**は共通の言語です。フラグ、Items Base の `item('id')`、Quests Engine のクエスト判定、各 mod のステータスや装備を、`&&`、`||`、`!` で組み合わせられます。
|
||||
|
||||
**シーンの演出。** `background`、`foreground`、`background_color`、`music`、`sound`、`effect`、`delay`、`light_set`、`hide_dialog`/`show_dialog`、`interactive_add`/`interactive_remove`、`notify`。
|
||||
|
||||
**ゲームアクション。** `action <name> key=value …` は他の mod のアクションを実行します。例は `action item.add id=money/coin count=3` や `action quest.set_status quest_id=woodland_ward status=Progress` です。アクションの一覧は各 mod のスキーマファイルにあり、Editor Tools のシナリオエディターとコマンドパレットからも入力できます。
|
||||
|
||||
## シナリオが再生されるタイミング
|
||||
|
||||
決めるのはヘッダーです。
|
||||
|
||||
```scenario
|
||||
scene: prolog_alley
|
||||
priority: 15
|
||||
condition: AwakeningQuest.notHasTag("cloak_taken")
|
||||
is_one_run: true
|
||||
save_allowed: true
|
||||
---
|
||||
```
|
||||
|
||||
- `scene:` — プレイヤーが到着したときに再生されるシーン。
|
||||
- `priority:` — 同じシーンのシナリオ同士の順序。再生できる最小の番号が先になり、ダイアログが終わると次が始まります。
|
||||
- `condition:` — 条件が満たされている間だけ再生します。フラグやクエストが変わると、シーンは再度判定します。
|
||||
- `is_one_run: true` — 1 回のゲームで一度だけ再生。
|
||||
- `after:` — 指定したシナリオが再生されるのを待ちます。
|
||||
- `save_allowed: true` — このシーンではセーブできます。
|
||||
|
||||
`scene:` ヘッダーのないシナリオはシーンからは始まりません。インタラクティブオブジェクトのクリックやマップイベントなど、別のきっかけで実行されます。
|
||||
|
||||
## 他の mod による追加と置き換え
|
||||
|
||||
すべての mod のシナリオは一つにまとめられます。mod は、シーン名を書くだけで他人のシーンに物語を追加できます。別の mod に依存し、同じファイル名のシナリオを同梱した mod は、それを置き換えます。
|
||||
|
||||
## うまく動かないとき
|
||||
|
||||
1. **シナリオが再生されない。** `scene:` の名前が違う、`condition` が偽、あるいは `is_one_run` 付きで既に再生済みです。
|
||||
2. **台詞が別の言語で表示される。** その言語の翻訳キー `key=` が mod の `locale` ファイルになく、引用したテキストがそのまま表示されています。
|
||||
3. **選択肢がどこにも行かない。** `->` の後のラベルは、同じファイル内にコロン付き(`:bar`)で存在しなければなりません。
|
||||
4. **`action` が何もしない。** アクションを持つ mod がゲームに含まれていない(ログに "is not registered" と出ます)か、パラメーター名が間違っています。Editor Tools のシナリオエディターで該当アクションと見比べてください。
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Dialog Engine — 대화 상자와 시나리오 언어
|
||||
|
||||
Dialog Engine은 플레이어가 읽고 클릭하는 모든 것을 담당합니다. 대사·초상화·선택지가 표시되는 대화 상자이며, 그 뒤에는 **시나리오 언어**가 있습니다. `.scenario`라는 짧은 텍스트 파일에 이야기를 한 줄씩 적어 나가는 언어로, 누가 무엇을 말하는지, 어떤 선택지가 있는지, 각 선택지가 어디로 이어지는지, 게임 안에서 무슨 일이 일어나는지(배경 전환, 음악, 아이템, 퀘스트 진행, 다른 장면으로 이동)를 모두 적습니다. 컬렉션의 모든 게임이 소설 템플릿부터 RPG Plus까지 이 언어로 이야기를 전합니다.
|
||||
|
||||
## 플레이어가 보는 것
|
||||
|
||||
장면에 들어가면 그 장면의 시나리오가 저절로 시작됩니다. 대사는 화면 아래 상자에 나타나고, 클릭하면 다음 대사로 넘어갑니다.
|
||||
|
||||

|
||||
|
||||
이야기가 무언가를 물으면 선택지가 오른쪽에 나타나고, 플레이어가 고를 때까지 기다립니다.
|
||||
|
||||

|
||||
|
||||
"술집에 들어가기"를 고르면 플레이어는 술집 장면으로 이동하고, 그곳의 시나리오가 시작됩니다.
|
||||
|
||||

|
||||
|
||||
## 이 화면 뒤의 시나리오
|
||||
|
||||
소설 템플릿 마을 시나리오의 전문입니다(`my_novel_game/scenarios/my_novel_town.scenario`).
|
||||
|
||||
```scenario
|
||||
scene: my_novel_town
|
||||
priority: 10
|
||||
---
|
||||
# 마을은 허브:방문할 때마다 같은 선택지가 표시된다.
|
||||
text "저녁 마을. 가로등은 이미 켜졌고, 돌바닥을 따라 차가운 바람이 불어 옵니다." key=my_novel/town/intro
|
||||
select "어디로 갈까요?" key=my_novel/town/prompt
|
||||
option "술집에 들어가기" key=my_novel/town/opt_bar -> :bar
|
||||
option "집으로 돌아가기" key=my_novel/town/opt_home -> :home
|
||||
|
||||
:bar
|
||||
switch_scene my_novel_bar
|
||||
end
|
||||
|
||||
:home
|
||||
switch_scene my_novel_home
|
||||
end
|
||||
```
|
||||
|
||||
- `---` 위의 **헤더**는 시나리오가 언제 재생되는지를 정합니다. `scene:`은 장면 이름, `priority:`는 같은 장면의 여러 시나리오 순서(작은 쪽이 먼저)입니다. 헤더 행은 아래에 더 있습니다.
|
||||
- `text "…"`는 대사를 보여 주고 클릭을 기다립니다. `key=`는 대사의 번역 키이며, 따옴표 안의 텍스트는 번역이 없을 때 표시됩니다.
|
||||
- `select`는 질문을 던지고, 각 `option`은 **레이블**( `-> :bar` )로 이동하는 선택지입니다.
|
||||
- `:bar`는 레이블, `switch_scene`는 다른 장면으로 이동, `end`는 시나리오 종료입니다.
|
||||
- `#`으로 시작하는 줄은 작성자를 위한 메모입니다.
|
||||
|
||||
장면에 이야기를 추가하려면 `.scenario` 파일을 자신의 mod `scenarios/` 폴더 어디에나 두고 헤더에 장면 이름을 적으면 됩니다.
|
||||
|
||||
## 이야기 쓰기
|
||||
|
||||
**대사와 초상화.** `text "…" first=marao/sad`는 첫 번째 자리에 초상화를, `second=…`는 두 번째 자리에 표시합니다(Characters Engine 참조). `ptext`는 `text`와 같으며 `${name}`을 변수 값으로 바꾸는 기능이 하나 더 있습니다. `title "…"`는 장 제목을 표시합니다.
|
||||
|
||||
**선택지와 점프.** 조건이 맞을 때만 선택지를 보이게 할 수 있습니다. `option "Pay" "if=item('money/coin') >= 5" -> :pay` (공백이 있는 조건은 `if=`까지 따옴표로 묶습니다). `goto -> :label`은 묻지 않고 건너뜁니다. `when "<condition>" -> :label`은 그 순간 조건이 맞으면 이동합니다.
|
||||
|
||||
```scenario
|
||||
when "rpg_wolf_dead == 1" -> :lantern
|
||||
```
|
||||
|
||||
**상태 기억.** `flag rpg_quest_taken 1`은 게임 저장에 함께 저장되는 스토리 플래그를 설정합니다. 조건에서는 `rpg_quest_taken == 1`로 읽습니다. `set name value`는 실행 중인 대화의 변수를 정합니다. `ptext`는 이를 `${name}`으로 표시하고 액션에서도 읽을 수 있습니다.
|
||||
|
||||
**조건**은 하나의 공용 언어입니다. 플래그, Items Base의 `item('id')`, Quests Engine의 퀘스트 확인, 각 mod의 스탯과 장비를 `&&`, `||`, `!`로 조합합니다.
|
||||
|
||||
**장면 연출.** `background`, `foreground`, `background_color`, `music`, `sound`, `effect`, `delay`, `light_set`, `hide_dialog`/`show_dialog`, `interactive_add`/`interactive_remove`, `notify`.
|
||||
|
||||
**게임 액션.** `action <name> key=value …`는 다른 mod의 액션을 실행합니다. 예를 들어 `action item.add id=money/coin count=3`, `action quest.set_status quest_id=woodland_ward status=Progress`입니다. 액션 목록은 각 mod의 스키마 파일에 있으며, Editor Tools의 시나리오 편집기와 커맨드 팔레트에서 바로 쓸 수 있습니다.
|
||||
|
||||
## 시나리오가 재생되는 시점
|
||||
|
||||
헤더가 결정합니다.
|
||||
|
||||
```scenario
|
||||
scene: prolog_alley
|
||||
priority: 15
|
||||
condition: AwakeningQuest.notHasTag("cloak_taken")
|
||||
is_one_run: true
|
||||
save_allowed: true
|
||||
---
|
||||
```
|
||||
|
||||
- `scene:` — 플레이어가 도착했을 때 시나리오가 재생되는 장면.
|
||||
- `priority:` — 같은 장면 시나리오끼리의 순서. 재생 가능한 가장 작은 번호가 먼저이며, 대화가 끝나면 다음이 시작됩니다.
|
||||
- `condition:` — 조건이 유지되는 동안에만 재생. 플래그나 퀘스트가 바뀌면 장면이 다시 확인합니다.
|
||||
- `is_one_run: true` — 게임당 한 번만 재생.
|
||||
- `after:` — 지정한 시나리오가 재생될 때까지 기다립니다.
|
||||
- `save_allowed: true` — 이 장면에서 저장할 수 있습니다.
|
||||
|
||||
`scene:` 헤더가 없는 시나리오는 장면이 시작하지 않습니다. 인터랙티브 오브젝트 클릭이나 맵 이벤트 같은 다른 계기가 실행합니다.
|
||||
|
||||
## 다른 mod의 추가와 교체
|
||||
|
||||
모든 mod의 시나리오는 한데 모입니다. mod는 장면 이름만 적으면 다른 mod의 장면에 이야기를 추가할 수 있습니다. 다른 mod에 의존하면서 같은 파일 이름의 시나리오를 포함한 mod는 그 시나리오를 교체합니다.
|
||||
|
||||
## 문제가 생겼을 때
|
||||
|
||||
1. **시나리오가 재생되지 않습니다.** `scene:` 이름이 틀렸거나, `condition`이 거짓이거나, `is_one_run`이 붙어 있어 이미 재생된 경우입니다.
|
||||
2. **대사가 다른 언어로 표시됩니다.** 해당 언어의 번역 키 `key=`가 mod의 `locale` 파일에 없어서 따옴표 안의 텍스트가 그대로 보입니다.
|
||||
3. **선택지가 어디로도 이어지지 않습니다.** `->` 뒤의 레이블은 같은 파일 안에 콜론을 붙여(`:bar`) 존재해야 합니다.
|
||||
4. **`action`이 아무것도 하지 않습니다.** 액션을 소유한 mod가 게임에 없거나(로그에 "is not registered"라고 기록됨), 매개변수 이름이 틀렸습니다. Editor Tools의 시나리오 편집기에서 해당 액션과 비교해 보세요.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Dialog Engine — окно диалога и язык сценариев
|
||||
|
||||
Dialog Engine — это то, что игрок читает и по чему щёлкает: окно диалога с репликами, портретами и вариантами ответа. За ним стоит **язык сценариев** — небольшие текстовые файлы `.scenario`, в которых история записана строка за строкой: кто что говорит, какие есть варианты ответа, куда ведёт каждый из них и что при этом происходит в игре (новый фон, музыка, предмет, шаг задания, другая сцена). С его помощью рассказывает свою история каждая игра коллекции — от шаблона новеллы до RPG Plus.
|
||||
|
||||
## Что видит игрок
|
||||
|
||||
Сценарий сцены запускается сам, как только игрок входит в неё. Реплики появляются в окне внизу экрана; щелчок показывает следующую:
|
||||
|
||||

|
||||
|
||||
Когда история о чём-то спрашивает, справа появляются варианты ответа — и она ждёт, пока игрок выберет один из них:
|
||||
|
||||

|
||||
|
||||
Вариант «Зайти в бар» переносит игрока в сцену бара, где запускается уже его собственный сценарий:
|
||||
|
||||

|
||||
|
||||
## Сценарий этих кадров
|
||||
|
||||
Вот весь сценарий города из шаблона новеллы (`my_novel_game/scenarios/my_novel_town.scenario`):
|
||||
|
||||
```scenario
|
||||
scene: my_novel_town
|
||||
priority: 10
|
||||
---
|
||||
# Город — хаб: при каждом визите игрок видит один и тот же выбор.
|
||||
text "Вечерний город. Фонари уже горят, по мостовой тянет прохладой." key=my_novel/town/intro
|
||||
select "Куда пойти?" key=my_novel/town/prompt
|
||||
option "Зайти в бар" key=my_novel/town/opt_bar -> :bar
|
||||
option "Вернуться домой" key=my_novel/town/opt_home -> :home
|
||||
|
||||
:bar
|
||||
switch_scene my_novel_bar
|
||||
end
|
||||
|
||||
:home
|
||||
switch_scene my_novel_home
|
||||
end
|
||||
```
|
||||
|
||||
- **Заголовок** над `---` говорит, когда сценарий играет: `scene:` называет сцену, `priority:` упорядочивает несколько сценариев одной сцены (с меньшим числом играет первым). Ниже встречаются и другие строки заголовка.
|
||||
- `text "…"` показывает реплику и ждёт щелчка. `key=` — ключ перевода реплики; текст в кавычках показывается, если перевода не нашлось.
|
||||
- `select` задаёт вопрос, и каждый `option` — вариант ответа, который ведёт к **метке** (`-> :bar`).
|
||||
- `:bar` — метка; `switch_scene` уходит в другую сцену; `end` завершает сценарий.
|
||||
- Строка, начинающаяся с `#`, — заметка для автора.
|
||||
|
||||
Чтобы добавить историю в сцену, положите файл `.scenario` в любое место папки `scenarios/` своего мода и назовите сцену в заголовке.
|
||||
|
||||
## Как писать историю
|
||||
|
||||
**Реплики и портреты.** `text "…" first=marao/sad` показывает портрет на первом месте, `second=…` — на втором (см. Characters Engine). `ptext` делает то же, что `text`, и вдобавок подставляет вместо `${name}` значение переменной. `title "…"` показывает заголовок главы.
|
||||
|
||||
**Выбор и переходы.** Вариант ответа можно показывать только при выполнении условия: `option "Pay" "if=item('money/coin') >= 5" -> :pay` (условие с пробелами берётся в кавычки вместе с `if=`). `goto -> :label` перескакивает без вопросов. `when "<condition>" -> :label` выполняет переход, если условие выполнено в этот момент:
|
||||
|
||||
```scenario
|
||||
when "rpg_wolf_dead == 1" -> :lantern
|
||||
```
|
||||
|
||||
**Запоминание.** `flag rpg_quest_taken 1` ставит сюжетный флаг, который сохраняется вместе с игрой; условия читают его как `rpg_quest_taken == 1`. `set name value` задаёт переменную текущего диалога: `ptext` покажет её как `${name}`, а действия смогут её прочитать.
|
||||
|
||||
**Условия** — один общий язык: флаги, `item('id')` из Items Base, проверки заданий из Quests Engine, характеристики и экипировка из своих модов, соединённые через `&&`, `||` и `!`.
|
||||
|
||||
**Сцена.** `background`, `foreground`, `background_color`, `music`, `sound`, `effect`, `delay`, `light_set`, `hide_dialog`/`show_dialog`, `interactive_add`/`interactive_remove`, `notify`.
|
||||
|
||||
**Игровые действия.** `action <name> key=value …` запускает действие другого мода, например `action item.add id=money/coin count=3` или `action quest.set_status quest_id=woodland_ward status=Progress`. Все действия перечислены в schema-файлах модов, а Editor Tools предлагает их в редакторе сценариев и палитре команд.
|
||||
|
||||
## Когда сценарий запускается
|
||||
|
||||
Решает заголовок:
|
||||
|
||||
```scenario
|
||||
scene: prolog_alley
|
||||
priority: 15
|
||||
condition: AwakeningQuest.notHasTag("cloak_taken")
|
||||
is_one_run: true
|
||||
save_allowed: true
|
||||
---
|
||||
```
|
||||
|
||||
- `scene:` — сцена, в которой сценарий играет, когда игрок приходит.
|
||||
- `priority:` — порядок среди сценариев одной сцены; первым играет тот, у кого наименьшее допустимое число, а следующий стартует, когда диалог закончится.
|
||||
- `condition:` — сценарий играет, только пока условие выполнено; когда флаг или задание меняется, сцена проверяет снова.
|
||||
- `is_one_run: true` — играется один раз за игру.
|
||||
- `after:` — ждёт, пока названный сценарий не будет пройден.
|
||||
- `save_allowed: true` — в этой сцене можно сохраняться.
|
||||
|
||||
Сценарий без строки `scene:` не запускается сценой — его запускает что-то другое: например, щелчок по интерактивному объекту или событие на карте.
|
||||
|
||||
## Чужие моды добавляют и заменяют сценарии
|
||||
|
||||
Сценарии всех модов собираются в общий набор. Добавить историю в чужую сцену просто: назовите эту сцену в заголовке. А мод, который зависит от владельца и кладёт в себя сценарий с тем же именем файла, заменяет его.
|
||||
|
||||
## Если что-то пошло не так
|
||||
|
||||
1. **Сценарий никогда не играет.** Ошибочное имя в `scene:`, ложное условие в `condition` — или у сценария есть `is_one_run`, и он уже играл.
|
||||
2. **Реплика показывается не на том языке.** Ключа `key=` нет в файлах `locale` мода для этого языка, и вместо перевода показывается текст в кавычках.
|
||||
3. **Ответ ведёт в никуда.** Метка после `->` должна существовать в том же файле и записываться с двоеточием (`:bar`).
|
||||
4. **`action` ничего не делает.** Мод — владелец действия не входит в игру (в логе написано, что действие "is not registered"), или имя параметра неверное — сверьте его с действием в редакторе сценариев Editor Tools.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Dialog Engine — hộp hội thoại và ngôn ngữ kịch bản
|
||||
|
||||
Dialog Engine là mọi thứ người chơi đọc và nhấp vào: hộp hội thoại với các câu thoại, chân dung và phương án trả lời. Đằng sau đó là **ngôn ngữ kịch bản** — những tệp văn bản `.scenario` nhỏ, nơi câu chuyện được viết từng dòng: ai nói gì, có những lựa chọn nào, mỗi lựa chọn dẫn đi đâu, và chuyện gì xảy ra trong game (đổi nền, bật nhạc, thêm vật phẩm, tiến nhiệm vụ, sang cảnh khác). Mọi trò chơi trong bộ sưu tập, từ mẫu truyện đến RPG Plus, đều kể câu chuyện của mình bằng ngôn ngữ này.
|
||||
|
||||
## Những gì người chơi nhìn thấy
|
||||
|
||||
Kịch bản của một cảnh tự bắt đầu khi người chơi bước vào. Các câu thoại hiện trong hộp ở đáy màn hình; mỗi cú nhấp hiện câu tiếp theo.
|
||||
|
||||

|
||||
|
||||
Khi câu chuyện hỏi điều gì đó, các lựa chọn hiện ra bên phải, và câu chuyện chờ cho đến khi người chơi chọn xong.
|
||||
|
||||

|
||||
|
||||
"Ghé vào quán bar" đưa người chơi sang cảnh quán bar, nơi bắt đầu kịch bản riêng của nó.
|
||||
|
||||

|
||||
|
||||
## Kịch bản đứng sau những hình này
|
||||
|
||||
Đây là toàn bộ kịch bản thị trấn của mẫu truyện (`my_novel_game/scenarios/my_novel_town.scenario`).
|
||||
|
||||
```scenario
|
||||
scene: my_novel_town
|
||||
priority: 10
|
||||
---
|
||||
# Thị trấn là điểm tụ: lần nào ghé thăm cũng thấy cùng một lựa chọn.
|
||||
text "Buổi chiều ở thị trấn. Đèn đường đã sáng, một cơn gió mát thổi qua đường lát đá." key=my_novel/town/intro
|
||||
select "Đi đâu đây?" key=my_novel/town/prompt
|
||||
option "Ghé vào quán bar" key=my_novel/town/opt_bar -> :bar
|
||||
option "Về nhà" key=my_novel/town/opt_home -> :home
|
||||
|
||||
:bar
|
||||
switch_scene my_novel_bar
|
||||
end
|
||||
|
||||
:home
|
||||
switch_scene my_novel_home
|
||||
end
|
||||
```
|
||||
|
||||
- **Phần đầu** phía trên `---` cho biết kịch bản chạy khi nào: `scene:` nêu tên cảnh, `priority:` sắp thứ tự cho nhiều kịch bản của cùng một cảnh (số nhỏ chạy trước). Bên dưới còn vài dòng đầu khác sẽ được giới thiệu.
|
||||
- `text "…"` hiện một câu thoại và chờ cú nhấp. `key=` là khóa dịch của câu thoại; văn bản trong ngoặc kép được hiển thị khi không tìm thấy bản dịch.
|
||||
- `select` đặt một câu hỏi, mỗi `option` là một lựa chọn nhảy tới một **nhãn** (`-> :bar`).
|
||||
- `:bar` là nhãn; `switch_scene` chuyển sang cảnh khác; `end` kết thúc kịch bản.
|
||||
- Dòng bắt đầu bằng `#` là ghi chú dành cho tác giả.
|
||||
|
||||
Muốn thêm câu chuyện vào một cảnh, bạn chỉ cần đặt tệp `.scenario` vào bất kỳ đâu trong thư mục `scenarios/` của mod và nêu tên cảnh trong phần đầu.
|
||||
|
||||
## Viết câu chuyện
|
||||
|
||||
**Câu thoại và chân dung.** `text "…" first=marao/sad` hiện chân dung ở vị trí thứ nhất, `second=…` ở vị trí thứ hai (xem Characters Engine). `ptext` giống `text` và thay thêm `${name}` bằng giá trị của biến. `title "…"` hiện tiêu đề chương.
|
||||
|
||||
**Lựa chọn và bước nhảy.** Có thể chỉ hiện một lựa chọn khi điều kiện thỏa mãn: `option "Pay" "if=item('money/coin') >= 5" -> :pay` (điều kiện có dấu cách thì bọc cả `if=` trong ngoặc kép). `goto -> :label` nhảy luôn không hỏi. `when "<condition>" -> :label` nhảy nếu điều kiện thỏa tại thời điểm đó.
|
||||
|
||||
```scenario
|
||||
when "rpg_wolf_dead == 1" -> :lantern
|
||||
```
|
||||
|
||||
**Ghi nhớ thông tin.** `flag rpg_quest_taken 1` đặt một cờ cốt truyện được lưu cùng game; điều kiện đọc nó dưới dạng `rpg_quest_taken == 1`. `set name value` đặt biến của phiên hội thoại đang chạy: `ptext` hiển thị nó dưới dạng `${name}` và các hành động cũng đọc được.
|
||||
|
||||
**Điều kiện** là một ngôn ngữ chung: cờ, `item('id')` từ Items Base, phép kiểm tra nhiệm vụ từ Quests Engine, chỉ số và trang bị từ mod tương ứng, nối với nhau bằng `&&`, `||` và `!`.
|
||||
|
||||
**Tổ chức cảnh.** `background`, `foreground`, `background_color`, `music`, `sound`, `effect`, `delay`, `light_set`, `hide_dialog`/`show_dialog`, `interactive_add`/`interactive_remove`, `notify`.
|
||||
|
||||
**Hành động của game.** `action <name> key=value …` chạy một hành động của mod khác, ví dụ `action item.add id=money/coin count=3` hay `action quest.set_status quest_id=woodland_ward status=Progress`. Các tệp schema của mod liệt kê hành động, còn Editor Tools đưa chúng vào trình soạn thảo kịch bản và bảng lệnh.
|
||||
|
||||
## Khi nào kịch bản chạy
|
||||
|
||||
Phần đầu quyết định điều đó.
|
||||
|
||||
```scenario
|
||||
scene: prolog_alley
|
||||
priority: 15
|
||||
condition: AwakeningQuest.notHasTag("cloak_taken")
|
||||
is_one_run: true
|
||||
save_allowed: true
|
||||
---
|
||||
```
|
||||
|
||||
- `scene:` — cảnh mà kịch bản chạy khi người chơi tới.
|
||||
- `priority:` — thứ tự giữa các kịch bản của cùng một cảnh; số nhỏ nhất được phép chạy sẽ chạy trước, kịch bản kế tiếp bắt đầu khi hội thoại kết thúc.
|
||||
- `condition:` — chỉ chạy khi điều kiện còn thỏa mãn; khi cờ hoặc nhiệm vụ đổi, cảnh sẽ kiểm tra lại.
|
||||
- `is_one_run: true` — chỉ chạy một lần mỗi ván chơi.
|
||||
- `after:` — chờ cho đến khi kịch bản được nêu đã chạy xong.
|
||||
- `save_allowed: true` — người chơi được lưu trong cảnh này.
|
||||
|
||||
Kịch bản không có dòng `scene:` thì không do cảnh khởi chạy; nó do thứ khác chạy, chẳng hạn cú nhấp vào vật thể tương tác hay sự kiện bản đồ.
|
||||
|
||||
## Mod khác thêm và thay thế kịch bản
|
||||
|
||||
Kịch bản của mọi mod được gom chung với nhau. Một mod thêm câu chuyện vào cảnh của mod khác đơn giản chỉ bằng cách nêu tên cảnh đó. Mod phụ thuộc vào mod khác và kèm theo một tệp kịch bản trùng tên sẽ thay thế tệp ấy.
|
||||
|
||||
## Khi có trục trặc
|
||||
|
||||
1. **Kịch bản không bao giờ chạy.** Tên trong `scene:` sai, `condition` sai, hoặc kịch bản có `is_one_run` và đã chạy rồi.
|
||||
2. **Câu thoại hiện sai ngôn ngữ.** Thiếu khóa `key=` trong các tệp `locale` của mod cho ngôn ngữ đó; văn bản trong ngoặc kép được hiển thị thay thế.
|
||||
3. **Lựa chọn dẫn đến hư không.** Nhãn sau `->` phải tồn tại trong cùng tệp, viết kèm dấu hai chấm (`:bar`).
|
||||
4. **`action` không làm gì cả.** Mod sở hữu hành động không có trong game (log ghi hành động "is not registered"), hoặc tên tham số sai — hãy so với hành động trong trình soạn thảo kịch bản của Editor Tools.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Dialog Engine — 对话框与剧本语言
|
||||
|
||||
Dialog Engine 负责玩家阅读和点击的一切:带台词、肖像和选项的对话框。它背后是**剧本语言**——一些简短的 `.scenario` 文本文件,故事在其中一行一行地写下来:谁说了什么、有哪些选项、每个选项通向哪里、游戏里会发生什么(换背景、放音乐、给物品、推进任务、切换场景)。合集里的每一款游戏,从小说模板到 RPG Plus,都用它讲述自己的故事。
|
||||
|
||||
## 玩家看到什么
|
||||
|
||||
进入场景时,场景的剧本会自动开始。台词出现在屏幕下方的对话框里,点击一下就显示下一句:
|
||||
|
||||

|
||||
|
||||
当故事向玩家提问时,选项会出现在右侧,并等待玩家做出选择:
|
||||
|
||||

|
||||
|
||||
选择"去酒吧"后,玩家来到酒吧场景,那里开始播放自己的剧本:
|
||||
|
||||

|
||||
|
||||
## 这些截图背后的剧本
|
||||
|
||||
这就是小说模板小镇场景的完整剧本(`my_novel_game/scenarios/my_novel_town.scenario`):
|
||||
|
||||
```scenario
|
||||
scene: my_novel_town
|
||||
priority: 10
|
||||
---
|
||||
# 小镇是枢纽:每次到访都显示同样的选择。
|
||||
text "傍晚的小镇。路灯已经亮起,石板路上吹过一阵凉风。" key=my_novel/town/intro
|
||||
select "接下来去哪儿?" key=my_novel/town/prompt
|
||||
option "去酒吧" key=my_novel/town/opt_bar -> :bar
|
||||
option "回家" key=my_novel/town/opt_home -> :home
|
||||
|
||||
:bar
|
||||
switch_scene my_novel_bar
|
||||
end
|
||||
|
||||
:home
|
||||
switch_scene my_novel_home
|
||||
end
|
||||
```
|
||||
|
||||
- `---` 之上的**头部**说明剧本何时播放:`scene:` 指明场景,`priority:` 排定同一场景多个剧本的先后(数字小的先播)。后文还会介绍其他头部行。
|
||||
- `text "…"` 显示一句台词并等待点击。`key=` 是这句台词的翻译键;引号里的文本在找不到翻译时显示。
|
||||
- `select` 提出一个问题,每个 `option` 都是一个选项,跳转到某个**标签**(`-> :bar`)。
|
||||
- `:bar` 是标签;`switch_scene` 切换到另一场景;`end` 结束剧本。
|
||||
- 以 `#` 开头的行是给作者看的注释。
|
||||
|
||||
要给场景添加故事,只需把 `.scenario` 文件放进自己 mod 的 `scenarios/` 文件夹(位置不限),并在头部写明场景名。
|
||||
|
||||
## 编写故事
|
||||
|
||||
**台词与肖像。** `text "…" first=marao/sad` 在第一个位置显示肖像,`second=…` 在第二个位置(参见 Characters Engine)。`ptext` 与 `text` 相同,还会把 `${name}` 替换为变量的值。`title "…"` 显示章节标题。
|
||||
|
||||
**选项与跳转。** 选项可以只在条件成立时显示:`option "Pay" "if=item('money/coin') >= 5" -> :pay`(条件里有空格时,连 `if=` 一起加引号)。`goto -> :label` 不作询问直接跳转。`when "<condition>" -> :label` 在条件此刻成立时跳转:
|
||||
|
||||
```scenario
|
||||
when "rpg_wolf_dead == 1" -> :lantern
|
||||
```
|
||||
|
||||
**记住状态。** `flag rpg_quest_taken 1` 设置一个随游戏存档保存的剧情标记;条件写作 `rpg_quest_taken == 1` 即可读取。`set name value` 设置当前对话的变量:`ptext` 把它显示为 `${name}`,动作也能读取它。
|
||||
|
||||
**条件**是一门统一的语言:标记、来自 Items Base 的 `item('id')`、来自 Quests Engine 的任务检查、各 mod 自带的属性与装备,用 `&&`、`||` 和 `!` 组合起来。
|
||||
|
||||
**场景演出。** `background`、`foreground`、`background_color`、`music`、`sound`、`effect`、`delay`、`light_set`、`hide_dialog`/`show_dialog`、`interactive_add`/`interactive_remove`、`notify`。
|
||||
|
||||
**游戏动作。** `action <name> key=value …` 执行另一个 mod 的动作,例如 `action item.add id=money/coin count=3` 或 `action quest.set_status quest_id=woodland_ward status=Progress`。各 mod 的 schema 文件列出了这些动作,Editor Tools 会在剧本编辑器和命令面板中提供它们。
|
||||
|
||||
## 剧本何时播放
|
||||
|
||||
这由头部决定:
|
||||
|
||||
```scenario
|
||||
scene: prolog_alley
|
||||
priority: 15
|
||||
condition: AwakeningQuest.notHasTag("cloak_taken")
|
||||
is_one_run: true
|
||||
save_allowed: true
|
||||
---
|
||||
```
|
||||
|
||||
- `scene:` — 玩家到达时播放剧本的场景。
|
||||
- `priority:` — 同一场景中各剧本的先后顺序;可播放的编号最小者先播,对话结束后下一个才开始。
|
||||
- `condition:` — 仅在条件成立时播放;标记或任务发生变化时,场景会重新检查。
|
||||
- `is_one_run: true` — 每局游戏只播放一次。
|
||||
- `after:` — 等待指定剧本播放完毕。
|
||||
- `save_allowed: true` — 玩家可以在该场景存档。
|
||||
|
||||
没有 `scene:` 头部的剧本不会由场景启动,而是由其他东西触发,比如点击场景中的交互物或地图事件。
|
||||
|
||||
## 其他 mod 的添加与替换
|
||||
|
||||
所有 mod 的剧本汇总在一起。mod 只要在头部写明场景名,就能把自己的故事加进别人的场景。声明了依赖并附带同名剧本文件的 mod 会替换原剧本。
|
||||
|
||||
## 出问题时
|
||||
|
||||
1. **剧本从不播放。** `scene:` 的场景名写错了、`condition` 为假,或者剧本带 `is_one_run` 且已经播放过。
|
||||
2. **台词显示成了别的语言。** 该语言的翻译键 `key=` 在 mod 的 `locale` 文件里缺失,于是显示引号里的原文。
|
||||
3. **选项跳了个空。** `->` 后面的标签必须存在于同一文件中,并带冒号(`:bar`)。
|
||||
4. **`action` 没有反应。** 动作所属的 mod 不在游戏内(日志提示动作 "is not registered"),或者参数名写错——与 Editor Tools 剧本编辑器中的动作对照一下。
|
||||
@@ -1,2 +1,2 @@
|
||||
mod_dialog_base_name = Dialog Engine
|
||||
mod_dialog_base_desc = Engine dialog subsystem: DialogBox/DialogCanvas/AnswerBox UI, the dialog queue with the full DSL action set, the .scenario parser, executable event chains and the /root/DialogRunner for interactive dialogs.
|
||||
mod_dialog_base_desc = Dialog engine: the dialog box with portraits and answers, and the .scenario language in which every game of the collection tells its story.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base_name = Motor de diálogos
|
||||
mod_dialog_base_desc = Motor de diálogos: la caja de diálogo con retratos y respuestas, y el lenguaje .scenario con el que cada juego de la colección cuenta su historia.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base_name = ダイアログエンジン
|
||||
mod_dialog_base_desc = ダイアログエンジン:ポートレートと選択肢を備えたダイアログボックス、そしてコレクションのすべてのゲームが物語を語る .scenario 言語。
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base_name = 대화 엔진
|
||||
mod_dialog_base_desc = 대화 엔진: 초상화와 선택지가 있는 대화 상자, 그리고 컬렉션의 모든 게임이 이야기를 전하게 하는 .scenario 언어.
|
||||
@@ -1,2 +1,2 @@
|
||||
mod_dialog_base_name = Движок диалогов
|
||||
mod_dialog_base_desc = Подсистема диалогов движка: UI DialogBox/DialogCanvas/AnswerBox, очередь диалогов с полным набором DSL-действий, парсер .scenario, цепочки событий и /root/DialogRunner для интерактивных диалогов.
|
||||
mod_dialog_base_desc = Движок диалогов: окно диалога с портретами и вариантами ответов и язык .scenario, на котором рассказывает свою история каждая игра коллекции.
|
||||
@@ -0,0 +1,29 @@
|
||||
dialog/schema/builtin/text = Line
|
||||
dialog/schema/builtin/text_desc = A dialog line; the locale key is optional
|
||||
dialog/schema/field/text = Text
|
||||
dialog/schema/field/locale_key = Locale key
|
||||
dialog/schema/builtin/ptext = Narration
|
||||
dialog/schema/builtin/ptext_desc = The narrator text, fills ${var}
|
||||
dialog/schema/builtin/flag = Story flag
|
||||
dialog/schema/builtin/flag_desc = A saved mark that conditions react to
|
||||
dialog/schema/field/flag = Flag
|
||||
dialog/schema/field/value = Value
|
||||
dialog/schema/builtin/switch_scene = Switch scene
|
||||
dialog/schema/builtin/switch_scene_desc = A novel scene or a map — a map is a scene too
|
||||
dialog/schema/field/scene = Scene
|
||||
dialog/schema/builtin/goto = Go to a label
|
||||
dialog/schema/builtin/goto_desc = An unconditional jump
|
||||
dialog/schema/field/label = Label
|
||||
dialog/schema/builtin/end = End the dialog
|
||||
dialog/schema/builtin/end_desc = Closes the dialog box
|
||||
dialog/schema/checks/when = When — jump
|
||||
dialog/schema/checks/when_desc = A conditional jump on the shared condition language: flags, quests, stats, gear
|
||||
dialog/schema/checks/arrow_true = When true, go to
|
||||
dialog/schema/field/condition = Condition
|
||||
dialog/schema/field/else = Else label
|
||||
dialog/schema/builtin/select = Choice
|
||||
dialog/schema/builtin/select_desc = The question before the answers; option lines follow
|
||||
dialog/schema/builtin/option = Answer
|
||||
dialog/schema/builtin/option_desc = An answer and the label it leads to; with a condition it is shown only while it holds
|
||||
dialog/schema/field/option_if = Show if
|
||||
dialog/schema/builtin/option_if_desc = For example archetype(rogue) or "stat(cha) >= 5"; empty — always
|
||||
@@ -0,0 +1,29 @@
|
||||
dialog/schema/builtin/text = Réplica
|
||||
dialog/schema/builtin/text_desc = Una línea de diálogo; la clave de traducción es opcional
|
||||
dialog/schema/field/text = Texto
|
||||
dialog/schema/field/locale_key = Clave de traducción
|
||||
dialog/schema/builtin/ptext = Narración
|
||||
dialog/schema/builtin/ptext_desc = El texto del narrador, rellena ${var}
|
||||
dialog/schema/builtin/flag = Marca de historia
|
||||
dialog/schema/builtin/flag_desc = Una marca guardada a la que reaccionan las condiciones
|
||||
dialog/schema/field/flag = Marca
|
||||
dialog/schema/field/value = Valor
|
||||
dialog/schema/builtin/switch_scene = Cambiar de escena
|
||||
dialog/schema/builtin/switch_scene_desc = Una escena de novela o un mapa: un mapa también es una escena
|
||||
dialog/schema/field/scene = Escena
|
||||
dialog/schema/builtin/goto = Ir a una etiqueta
|
||||
dialog/schema/builtin/goto_desc = Un salto incondicional
|
||||
dialog/schema/field/label = Etiqueta
|
||||
dialog/schema/builtin/end = Fin del diálogo
|
||||
dialog/schema/builtin/end_desc = Cierra el cuadro de diálogo
|
||||
dialog/schema/checks/when = Si — saltar
|
||||
dialog/schema/checks/when_desc = Un salto condicional según el lenguaje común de condiciones: marcas, misiones, estadísticas, equipo
|
||||
dialog/schema/checks/arrow_true = Si es cierto, ir a
|
||||
dialog/schema/field/condition = Condición
|
||||
dialog/schema/field/else = Etiqueta si no
|
||||
dialog/schema/builtin/select = Elección
|
||||
dialog/schema/builtin/select_desc = La pregunta antes de las respuestas; siguen las líneas option
|
||||
dialog/schema/builtin/option = Respuesta
|
||||
dialog/schema/builtin/option_desc = Una respuesta y la etiqueta a la que lleva; con condición solo se muestra mientras se cumple
|
||||
dialog/schema/field/option_if = Mostrar si
|
||||
dialog/schema/builtin/option_if_desc = Por ejemplo archetype(rogue) o "stat(cha) >= 5"; vacío: siempre
|
||||
@@ -0,0 +1,29 @@
|
||||
dialog/schema/builtin/text = セリフ
|
||||
dialog/schema/builtin/text_desc = 会話の一行。翻訳キーは任意
|
||||
dialog/schema/field/text = テキスト
|
||||
dialog/schema/field/locale_key = 翻訳キー
|
||||
dialog/schema/builtin/ptext = 地の文
|
||||
dialog/schema/builtin/ptext_desc = 語りの文。${var} を埋め込む
|
||||
dialog/schema/builtin/flag = ストーリーフラグ
|
||||
dialog/schema/builtin/flag_desc = 保存される印。条件がそれに反応する
|
||||
dialog/schema/field/flag = フラグ
|
||||
dialog/schema/field/value = 値
|
||||
dialog/schema/builtin/switch_scene = シーン切り替え
|
||||
dialog/schema/builtin/switch_scene_desc = ノベルのシーンかマップ——マップもシーン
|
||||
dialog/schema/field/scene = シーン
|
||||
dialog/schema/builtin/goto = ラベルへ移動
|
||||
dialog/schema/builtin/goto_desc = 無条件のジャンプ
|
||||
dialog/schema/field/label = ラベル
|
||||
dialog/schema/builtin/end = 会話の終わり
|
||||
dialog/schema/builtin/end_desc = 会話ウィンドウを閉じる
|
||||
dialog/schema/checks/when = もし——ジャンプ
|
||||
dialog/schema/checks/when_desc = 共通の条件言語による条件ジャンプ:フラグ、クエスト、ステータス、装備
|
||||
dialog/schema/checks/arrow_true = 真のときのラベル
|
||||
dialog/schema/field/condition = 条件
|
||||
dialog/schema/field/else = それ以外のラベル
|
||||
dialog/schema/builtin/select = 選択肢
|
||||
dialog/schema/builtin/select_desc = 選択肢の前の問い。続けて option 行
|
||||
dialog/schema/builtin/option = 選択肢
|
||||
dialog/schema/builtin/option_desc = 答えと行き先のラベル。条件付きなら成り立つ間だけ表示
|
||||
dialog/schema/field/option_if = 表示条件
|
||||
dialog/schema/builtin/option_if_desc = 例:archetype(rogue) や "stat(cha) >= 5"。空なら常に
|
||||
@@ -0,0 +1,29 @@
|
||||
dialog/schema/builtin/text = 대사
|
||||
dialog/schema/builtin/text_desc = 대화 한 줄. 번역 키는 선택
|
||||
dialog/schema/field/text = 텍스트
|
||||
dialog/schema/field/locale_key = 번역 키
|
||||
dialog/schema/builtin/ptext = 서술
|
||||
dialog/schema/builtin/ptext_desc = 서술자 텍스트. ${var}를 채움
|
||||
dialog/schema/builtin/flag = 스토리 플래그
|
||||
dialog/schema/builtin/flag_desc = 저장되는 표시. 조건이 반응합니다
|
||||
dialog/schema/field/flag = 플래그
|
||||
dialog/schema/field/value = 값
|
||||
dialog/schema/builtin/switch_scene = 장면 전환
|
||||
dialog/schema/builtin/switch_scene_desc = 노벨 장면이나 맵 — 맵도 장면입니다
|
||||
dialog/schema/field/scene = 장면
|
||||
dialog/schema/builtin/goto = 라벨로 이동
|
||||
dialog/schema/builtin/goto_desc = 무조건 이동
|
||||
dialog/schema/field/label = 라벨
|
||||
dialog/schema/builtin/end = 대화 끝
|
||||
dialog/schema/builtin/end_desc = 대화창을 닫음
|
||||
dialog/schema/checks/when = 조건 — 이동
|
||||
dialog/schema/checks/when_desc = 공통 조건 언어로 하는 조건 이동: 플래그, 퀘스트, 능력치, 장비
|
||||
dialog/schema/checks/arrow_true = 참이면 이동
|
||||
dialog/schema/field/condition = 조건
|
||||
dialog/schema/field/else = 아니면 라벨
|
||||
dialog/schema/builtin/select = 선택
|
||||
dialog/schema/builtin/select_desc = 답 앞의 질문. 뒤에 option 줄이 옵니다
|
||||
dialog/schema/builtin/option = 선택지
|
||||
dialog/schema/builtin/option_desc = 답과 이어지는 라벨. 조건이 있으면 참일 때만 보입니다
|
||||
dialog/schema/field/option_if = 표시 조건
|
||||
dialog/schema/builtin/option_if_desc = 예: archetype(rogue) 또는 "stat(cha) >= 5". 비우면 항상
|
||||
@@ -0,0 +1,29 @@
|
||||
dialog/schema/builtin/text = Реплика
|
||||
dialog/schema/builtin/text_desc = Строка диалога; ключ перевода — по желанию
|
||||
dialog/schema/field/text = Текст
|
||||
dialog/schema/field/locale_key = Ключ перевода
|
||||
dialog/schema/builtin/ptext = Повествование
|
||||
dialog/schema/builtin/ptext_desc = Текст рассказчика, подставляет ${var}
|
||||
dialog/schema/builtin/flag = Флаг сюжета
|
||||
dialog/schema/builtin/flag_desc = Сохраняемая отметка, по ней срабатывают условия
|
||||
dialog/schema/field/flag = Флаг
|
||||
dialog/schema/field/value = Значение
|
||||
dialog/schema/builtin/switch_scene = Сменить сцену
|
||||
dialog/schema/builtin/switch_scene_desc = Сцена новеллы или карта — карта тоже сцена
|
||||
dialog/schema/field/scene = Сцена
|
||||
dialog/schema/builtin/goto = Перейти к метке
|
||||
dialog/schema/builtin/goto_desc = Безусловный переход
|
||||
dialog/schema/field/label = Метка
|
||||
dialog/schema/builtin/end = Конец диалога
|
||||
dialog/schema/builtin/end_desc = Закрывает окно диалога
|
||||
dialog/schema/checks/when = Если — переход
|
||||
dialog/schema/checks/when_desc = Условный переход по общему языку условий: флаги, квесты, статы, снаряжение
|
||||
dialog/schema/checks/arrow_true = Если верно — метка
|
||||
dialog/schema/field/condition = Условие
|
||||
dialog/schema/field/else = Иначе — метка
|
||||
dialog/schema/builtin/select = Выбор
|
||||
dialog/schema/builtin/select_desc = Вопрос перед вариантами ответа; дальше идут строки option
|
||||
dialog/schema/builtin/option = Вариант ответа
|
||||
dialog/schema/builtin/option_desc = Вариант выбора и метка, куда он ведёт; с условием — показывается, только пока оно верно
|
||||
dialog/schema/field/option_if = Показывать, если
|
||||
dialog/schema/builtin/option_if_desc = Например archetype(rogue) или "stat(cha) >= 5"; пусто — всегда
|
||||
@@ -0,0 +1,29 @@
|
||||
dialog/schema/builtin/text = Lời thoại
|
||||
dialog/schema/builtin/text_desc = Một dòng hội thoại; khóa dịch tùy chọn
|
||||
dialog/schema/field/text = Văn bản
|
||||
dialog/schema/field/locale_key = Khóa dịch
|
||||
dialog/schema/builtin/ptext = Lời dẫn
|
||||
dialog/schema/builtin/ptext_desc = Lời người kể, điền ${var}
|
||||
dialog/schema/builtin/flag = Cờ cốt truyện
|
||||
dialog/schema/builtin/flag_desc = Một dấu được lưu, điều kiện sẽ phản ứng theo
|
||||
dialog/schema/field/flag = Cờ
|
||||
dialog/schema/field/value = Giá trị
|
||||
dialog/schema/builtin/switch_scene = Chuyển cảnh
|
||||
dialog/schema/builtin/switch_scene_desc = Cảnh tiểu thuyết hoặc bản đồ — bản đồ cũng là cảnh
|
||||
dialog/schema/field/scene = Cảnh
|
||||
dialog/schema/builtin/goto = Đến nhãn
|
||||
dialog/schema/builtin/goto_desc = Bước nhảy vô điều kiện
|
||||
dialog/schema/field/label = Nhãn
|
||||
dialog/schema/builtin/end = Kết thúc hội thoại
|
||||
dialog/schema/builtin/end_desc = Đóng hộp hội thoại
|
||||
dialog/schema/checks/when = Nếu — nhảy
|
||||
dialog/schema/checks/when_desc = Bước nhảy có điều kiện theo ngôn ngữ điều kiện chung: cờ, nhiệm vụ, chỉ số, trang bị
|
||||
dialog/schema/checks/arrow_true = Nếu đúng, đến
|
||||
dialog/schema/field/condition = Điều kiện
|
||||
dialog/schema/field/else = Nhãn nếu không
|
||||
dialog/schema/builtin/select = Lựa chọn
|
||||
dialog/schema/builtin/select_desc = Câu hỏi trước các đáp án; sau đó là các dòng option
|
||||
dialog/schema/builtin/option = Đáp án
|
||||
dialog/schema/builtin/option_desc = Một đáp án và nhãn nó dẫn tới; có điều kiện thì chỉ hiện khi điều kiện đúng
|
||||
dialog/schema/field/option_if = Hiện nếu
|
||||
dialog/schema/builtin/option_if_desc = Ví dụ archetype(rogue) hoặc "stat(cha) >= 5"; để trống — luôn hiện
|
||||
@@ -0,0 +1,29 @@
|
||||
dialog/schema/builtin/text = 台词
|
||||
dialog/schema/builtin/text_desc = 一句对白;翻译键可选
|
||||
dialog/schema/field/text = 文本
|
||||
dialog/schema/field/locale_key = 翻译键
|
||||
dialog/schema/builtin/ptext = 旁白
|
||||
dialog/schema/builtin/ptext_desc = 旁白文本,会代入 ${var}
|
||||
dialog/schema/builtin/flag = 剧情标记
|
||||
dialog/schema/builtin/flag_desc = 会被保存的标记,条件据此触发
|
||||
dialog/schema/field/flag = 标记
|
||||
dialog/schema/field/value = 数值
|
||||
dialog/schema/builtin/switch_scene = 切换场景
|
||||
dialog/schema/builtin/switch_scene_desc = 小说场景或地图——地图也是场景
|
||||
dialog/schema/field/scene = 场景
|
||||
dialog/schema/builtin/goto = 跳到标签
|
||||
dialog/schema/builtin/goto_desc = 无条件跳转
|
||||
dialog/schema/field/label = 标签
|
||||
dialog/schema/builtin/end = 结束对话
|
||||
dialog/schema/builtin/end_desc = 关闭对话框
|
||||
dialog/schema/checks/when = 如果——跳转
|
||||
dialog/schema/checks/when_desc = 依据通用条件语言的条件跳转:标记、任务、属性、装备
|
||||
dialog/schema/checks/arrow_true = 为真时跳转
|
||||
dialog/schema/field/condition = 条件
|
||||
dialog/schema/field/else = 否则跳转
|
||||
dialog/schema/builtin/select = 选择
|
||||
dialog/schema/builtin/select_desc = 回答前的问题;后面跟着 option 行
|
||||
dialog/schema/builtin/option = 回答选项
|
||||
dialog/schema/builtin/option_desc = 一个回答及其跳转标签;带条件时仅在条件成立时显示
|
||||
dialog/schema/field/option_if = 显示条件
|
||||
dialog/schema/builtin/option_if_desc = 例如 archetype(rogue) 或 "stat(cha) >= 5";留空——总是显示
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base_name = Engine hội thoại
|
||||
mod_dialog_base_desc = Engine hội thoại: hộp hội thoại với chân dung và các lựa chọn, cùng ngôn ngữ .scenario mà mọi trò chơi trong bộ sưu tập dùng để kể câu chuyện của mình.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base_name = 对话引擎
|
||||
mod_dialog_base_desc = 对话引擎:带肖像与选项的对话框,以及让合集中的每个游戏讲述自身故事的 .scenario 语言。
|
||||
+3
-2
@@ -3,7 +3,7 @@
|
||||
"name": "Dialog Engine",
|
||||
"version": "1.0.0",
|
||||
"author": "ct",
|
||||
"description": "Engine dialog subsystem: DialogBox/DialogCanvas/AnswerBox UI, the dialog queue runtime with the full DSL action set, the .scenario parser with command strategies, the executable event chains and the /root/DialogRunner for interactive dialogs. Replaces the former Core/UI/Dialog, Core/UI/Executable and Core/Scenarios implementations — SceneManager and SaveService keep driving chains and events through the core contracts (IDialogBox, IExecutableEvent, IExecutableEventChain, IScenarioEventFactory).",
|
||||
"description": "Dialog engine: the dialog box with portraits and answers, and the .scenario language in which every game of the collection tells its story.",
|
||||
"category": "engine, library",
|
||||
"sources": [
|
||||
"src/**/*.cs"
|
||||
@@ -15,6 +15,7 @@
|
||||
"ui": [],
|
||||
"test_suites": [
|
||||
{"id": "autoplay", "activate": ["--choices=", "--timeout="]},
|
||||
{"id": "gdscript", "activate": ["--itest=gdscript"]}
|
||||
{"id": "gdscript", "activate": ["--itest=gdscript"]},
|
||||
{"id": "doc_shots", "activate": ["--itest=doc_shots"]}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"Commands": [
|
||||
{
|
||||
"Id": "text",
|
||||
"Name": "dialog/schema/builtin/text",
|
||||
"Description": "dialog/schema/builtin/text_desc",
|
||||
"Category": "dialog",
|
||||
"Positional": [
|
||||
{
|
||||
"Key": "text",
|
||||
"Type": "text",
|
||||
"Required": true,
|
||||
"Name": "dialog/schema/field/text"
|
||||
}
|
||||
],
|
||||
"Params": [
|
||||
{
|
||||
"Key": "key",
|
||||
"Type": "text",
|
||||
"Name": "dialog/schema/field/locale_key"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "ptext",
|
||||
"Name": "dialog/schema/builtin/ptext",
|
||||
"Description": "dialog/schema/builtin/ptext_desc",
|
||||
"Category": "dialog",
|
||||
"Positional": [
|
||||
{
|
||||
"Key": "text",
|
||||
"Type": "text",
|
||||
"Required": true,
|
||||
"Name": "dialog/schema/field/text"
|
||||
}
|
||||
],
|
||||
"Params": [
|
||||
{
|
||||
"Key": "key",
|
||||
"Type": "text",
|
||||
"Name": "dialog/schema/field/locale_key"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "flag",
|
||||
"Name": "dialog/schema/builtin/flag",
|
||||
"Description": "dialog/schema/builtin/flag_desc",
|
||||
"Category": "flow",
|
||||
"Positional": [
|
||||
{
|
||||
"Key": "name",
|
||||
"Type": "flag",
|
||||
"Required": true,
|
||||
"Name": "dialog/schema/field/flag"
|
||||
},
|
||||
{
|
||||
"Key": "value",
|
||||
"Type": "text",
|
||||
"Required": true,
|
||||
"Default": "1",
|
||||
"Name": "dialog/schema/field/value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "switch_scene",
|
||||
"Name": "dialog/schema/builtin/switch_scene",
|
||||
"Description": "dialog/schema/builtin/switch_scene_desc",
|
||||
"Category": "flow",
|
||||
"Positional": [
|
||||
{
|
||||
"Key": "scene",
|
||||
"Type": "scene",
|
||||
"Required": true,
|
||||
"Name": "dialog/schema/field/scene"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "goto",
|
||||
"Name": "dialog/schema/builtin/goto",
|
||||
"Description": "dialog/schema/builtin/goto_desc",
|
||||
"Category": "flow",
|
||||
"Arrow": "dialog/schema/field/label"
|
||||
},
|
||||
{
|
||||
"Id": "end",
|
||||
"Name": "dialog/schema/builtin/end",
|
||||
"Description": "dialog/schema/builtin/end_desc",
|
||||
"Category": "flow"
|
||||
},
|
||||
{
|
||||
"Id": "when",
|
||||
"Name": "dialog/schema/checks/when",
|
||||
"Description": "dialog/schema/checks/when_desc",
|
||||
"Category": "flow",
|
||||
"Arrow": "dialog/schema/checks/arrow_true",
|
||||
"Positional": [
|
||||
{
|
||||
"Key": "condition",
|
||||
"Type": "condition",
|
||||
"Required": true,
|
||||
"Name": "dialog/schema/field/condition"
|
||||
}
|
||||
],
|
||||
"Params": [
|
||||
{
|
||||
"Key": "else",
|
||||
"Type": "label",
|
||||
"Name": "dialog/schema/field/else"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "select",
|
||||
"Name": "dialog/schema/builtin/select",
|
||||
"Description": "dialog/schema/builtin/select_desc",
|
||||
"Category": "dialog",
|
||||
"Positional": [
|
||||
{
|
||||
"Key": "text",
|
||||
"Type": "text",
|
||||
"Name": "dialog/schema/field/text"
|
||||
}
|
||||
],
|
||||
"Params": [
|
||||
{
|
||||
"Key": "key",
|
||||
"Type": "text",
|
||||
"Name": "dialog/schema/field/locale_key"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "option",
|
||||
"Name": "dialog/schema/builtin/option",
|
||||
"Description": "dialog/schema/builtin/option_desc",
|
||||
"Category": "dialog",
|
||||
"Arrow": "dialog/schema/field/label",
|
||||
"Positional": [
|
||||
{
|
||||
"Key": "text",
|
||||
"Type": "text",
|
||||
"Required": true,
|
||||
"Name": "dialog/schema/field/text"
|
||||
}
|
||||
],
|
||||
"Params": [
|
||||
{
|
||||
"Key": "key",
|
||||
"Type": "text",
|
||||
"Name": "dialog/schema/field/locale_key"
|
||||
},
|
||||
{
|
||||
"Key": "if",
|
||||
"Type": "condition",
|
||||
"Name": "dialog/schema/field/option_if",
|
||||
"Description": "dialog/schema/builtin/option_if_desc"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -57,7 +57,7 @@ namespace Cthangover.Mods.Dialog
|
||||
|
||||
// Deferred: OnModResourcesReady fires while the root is still
|
||||
// setting up autoload children, so a direct AddChild is rejected.
|
||||
root.CallDeferred("add_child", new DialogScriptRunner { Name = "DialogRunner" });
|
||||
ModRoot.AddDeferred(new DialogScriptRunner { Name = "DialogRunner" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace Cthangover.Mods.Dialog.Scenarios
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Mods.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// A conditional jump in the middle of a dialog:
|
||||
/// <code>when "stat(cha) >= 5 && door == 'open'" -> :persuaded [else=refused]</code>
|
||||
/// evaluates the shared condition language (flags, quests, stats, equipment, GDScript
|
||||
/// conditions — everything mods registered) at the moment the line runs, and jumps to
|
||||
/// the arrow label when it holds, to <c>else</c> when it does not (or goes on).
|
||||
/// <para/>
|
||||
/// It is the RPG Maker "Conditional Branch" without a new syntax: the condition is the
|
||||
/// same text a scenario header's <c>condition:</c> takes. Several words without quotes
|
||||
/// are joined back with spaces, so short conditions need no quotes.
|
||||
/// </summary>
|
||||
public class WhenCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Command => "when";
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MinPositional => 1;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "else" };
|
||||
|
||||
/// <inheritdoc />
|
||||
public string JumpTargetNamedKey => "else";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
var condition = string.Join(" ", positional);
|
||||
named.TryGetValue("else", out var otherwise);
|
||||
dlg.IfGoTo(() => ScenarioCondition.Evaluate(condition), Label(arrowTarget), Label(otherwise));
|
||||
}
|
||||
|
||||
private static string Label(string target) => string.IsNullOrWhiteSpace(target) ? null : target.Trim().TrimStart(':');
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,10 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
|
||||
if (pendingSelect.Variants == null)
|
||||
pendingSelect.Variants = new List<SelectVariant>();
|
||||
pendingSelect.Variants.Add(SelectVariant.New(optText, goTo));
|
||||
var variant = SelectVariant.New(optText, goTo);
|
||||
if (named.TryGetValue("if", out var condition))
|
||||
variant.Condition = condition;
|
||||
pendingSelect.Variants.Add(variant);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Core.UI;
|
||||
@@ -35,16 +37,29 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
public override WaitType WaitType { get; set; } = WaitType.WaitSelect;
|
||||
|
||||
/// <summary>Substitutes variables in the variants and context text, then shows the answer box; the runtime stays paused until a variant is picked.</summary>
|
||||
/// <summary>
|
||||
/// Offers the options whose conditions hold now. When none does, the choice is
|
||||
/// skipped and the dialog goes on with the next line, so a choice of only gated
|
||||
/// options never leaves the player stuck.
|
||||
/// </summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var dialogBox = runtime.DialogBox;
|
||||
if (dialogBox == null)
|
||||
return;
|
||||
|
||||
foreach (var variant in Variants)
|
||||
var shown = Variants.Where(v => v.IsShown()).ToList();
|
||||
WaitType = shown.Count == 0 ? WaitType.NoWait : WaitType.WaitSelect;
|
||||
if (shown.Count == 0)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "select: no option is shown (all conditions false), going on", LogLevel.Debug);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var variant in shown)
|
||||
variant.Text = runtime.ProcessText(variant.Text);
|
||||
|
||||
dialogBox.SetVariants(Variants);
|
||||
dialogBox.SetVariants(shown);
|
||||
dialogBox.SetText(runtime.ProcessText(Text));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,16 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
/// <summary>Target action ID to jump to via <see cref="DialogRuntime.TryGoTo"/> when this variant is selected.</summary>
|
||||
public string GoTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Condition of the shared condition language (<c>option "…" if=… -> :label</c>); the
|
||||
/// option is shown only while it holds. Null = always shown. Evaluated each time the
|
||||
/// choice appears, so a stat, a flag or an item gained a moment ago counts.
|
||||
/// </summary>
|
||||
public string Condition { get; set; }
|
||||
|
||||
/// <summary>Whether the option is offered now.</summary>
|
||||
public bool IsShown() => string.IsNullOrWhiteSpace(Condition) || ScenarioCondition.Evaluate(Condition);
|
||||
|
||||
/// <summary>Generates a new unique action ID for use as a GoTo target (e.g. with <see cref="DialogQueue.Point"/>).</summary>
|
||||
public static string Point => Guid.NewGuid().ToString();
|
||||
|
||||
|
||||
@@ -205,6 +205,10 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
if (!IsInstanceValid(this) || !IsInsideTree() || !isActive)
|
||||
return;
|
||||
|
||||
// Shell rebuilds after battles may make the early _Ready lookup miss.
|
||||
// Resolve again on the deferred scan, after SceneManager clears scene caches.
|
||||
if (dialogBox == null || !IsInstanceValid(dialogBox))
|
||||
dialogBox = SceneContextNode.FindNode<DialogBox>("DialogBox");
|
||||
if (dialogBox == null || !IsInstanceValid(dialogBox) || !dialogBox.IsActive || dialogBox.IsRunning)
|
||||
return;
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace Mods.TestRunner
|
||||
// Deferred: сцена ещё строится; драйвер внутри сам выдерживает
|
||||
// стартовую паузу и лениво ищет DialogBox, поэтому порядок с
|
||||
// загрузкой сцены не важен.
|
||||
root.CallDeferred("add_child", new DialogAutoDriverNode(choices, timeout, dialogs));
|
||||
ModRoot.AddDeferred(new DialogAutoDriverNode(choices, timeout, dialogs));
|
||||
}
|
||||
|
||||
private static List<int> ParseChoices(string[] args)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Documentation Screenshots",
|
||||
"description": "Plays a steps file (scene switches, clock, dialog answers, panels, keys, inventory/party seeding) in the running game and saves a screenshot at every 'shot' step for mod documentation. Activated by --itest=doc_shots with --doc-steps=<file> and --shots=<dir> (windowed runs only).",
|
||||
"sources": [
|
||||
"src/*.cs"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Bootstrap;
|
||||
using Cthangover.Core.Interactive;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Cthangover.Mods.Interface.View;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.DocShots
|
||||
{
|
||||
/// <summary>
|
||||
/// Plays a plain-text steps file in a running game and saves screenshots for
|
||||
/// mod documentation, so a page's pictures come from the real application
|
||||
/// without one driver per mod. One step per line, <c>#</c> starts a comment:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>scene <name></c> — aborts the running dialog (as "Go to scene" does) and switches; waits for the scene, then settles.</item>
|
||||
/// <item><c>time HH:MM</c> — sets the clock of the current day.</item>
|
||||
/// <item><c>wait <ticks></c> — 0.1 s ticks.</item>
|
||||
/// <item><c>abort</c>, <c>next [n]</c>, <c>choose <i></c>, <c>waitanswer</c> — the dialog box.</item>
|
||||
/// <item><c>panel <id></c> — toggles a toolbar panel like its button.</item>
|
||||
/// <item><c>key <Key> [ticks]</c> — presses a key (Godot name, e.g. <c>E</c>, <c>W</c>, <c>Escape</c>) and releases it after the ticks.</item>
|
||||
/// <item><c>tap <node> <i></c> — left-clicks child <c>i</c> of the first node with that name (an inventory cell); <c>press <text></c> — presses the visible button with that text or with a label of that text inside.</item>
|
||||
/// <item><c>give <item> [count]</c>, <c>party <character></c> — seeds the inventory and the party.</item>
|
||||
/// <item><c>signal <node> <signal> [arg]</c> — emits a signal of the first node with that name, as its own input handling would (a world map zone click).</item>
|
||||
/// <item><c>hover <x> <y></c>, <c>click <x> <y></c> — moves the mouse to (and left-clicks at) a point given as fractions of the window, through the normal input routing (scene interactives).</item>
|
||||
/// <item><c>drag <x1> <y1> <x2> <y2></c> — presses the left button at the first point, moves to the second with it held and releases there (dragging a battle card onto a target).</item>
|
||||
/// <item><c>hurt <character> <hp></c> — sets a party member's current health (to show healing).</item>
|
||||
/// <item><c>tool <id></c> — opens a registered tool window (<c>schema_palette</c>, <c>items_editor</c>…) the way the toolbar's tool list does.</item>
|
||||
/// <item><c>type <text></c> — replaces the text of the focused line edit (click it first).</item>
|
||||
/// <item><c>call <tool> <method> [args…]</c> — calls a public method with string arguments on a window opened by <c>tool</c>.</item>
|
||||
/// <item><c>run <scenario line></c> — runs one scenario line through the dialog runner, as an interactive object's inline commands do (<c>run action rpg.battle enemies=rpg_wolf</c>).</item>
|
||||
/// <item><c>shot <name></c> — saves <c><name>.png</c> (windowed runs only).</item>
|
||||
/// </list>
|
||||
/// Waits are tick counts, not conditions on the picture; only reaching each
|
||||
/// scene and the dialog states is checked. Result line:
|
||||
/// <c>[TEST_RESULT] name=doc_shots passed=N failed=M</c>, exit 0/1, 300 s watchdog.
|
||||
/// </summary>
|
||||
public partial class DocShotsDriver : Node
|
||||
{
|
||||
private const float TickSeconds = 0.1f;
|
||||
private const float TimeoutSeconds = 300f;
|
||||
private const int SettleTicks = 30;
|
||||
private const int NextGapTicks = 5;
|
||||
private const long MinutesPerDay = 24 * 60;
|
||||
|
||||
private readonly string shotsDir;
|
||||
private readonly List<string[]> steps = new();
|
||||
private readonly List<string> failures = new();
|
||||
private int passed;
|
||||
private int index;
|
||||
private int waitTicks;
|
||||
private int repeat;
|
||||
private double elapsed;
|
||||
private bool started;
|
||||
private string pendingScene;
|
||||
private Key heldKey = Key.None;
|
||||
private readonly Dictionary<string, Window> tools = new();
|
||||
private Window lastTool;
|
||||
|
||||
/// <summary>Reads the steps; a missing file fails the run at the first tick.</summary>
|
||||
public DocShotsDriver(string stepsFile, string shotsDir)
|
||||
{
|
||||
this.shotsDir = shotsDir;
|
||||
Name = "DocShotsDriver";
|
||||
if (stepsFile != null && File.Exists(stepsFile))
|
||||
{
|
||||
foreach (var line in File.ReadAllLines(stepsFile))
|
||||
{
|
||||
var text = line.Split('#')[0].Trim();
|
||||
if (text.Length > 0)
|
||||
steps.Add(text.Split(' ', StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
failures.Add($"steps file '{stepsFile}' not found");
|
||||
}
|
||||
var timer = new Timer { WaitTime = TickSeconds, Autostart = true };
|
||||
AddChild(timer);
|
||||
timer.Timeout += Tick;
|
||||
}
|
||||
|
||||
private SceneManager Scenes => GetTree().Root.GetNodeOrNull<SceneManager>("SceneManager");
|
||||
|
||||
private static DialogBox Box => SceneContextNode.FindNode<DialogBox>("DialogBox");
|
||||
|
||||
private void Tick()
|
||||
{
|
||||
if (index < 0)
|
||||
return;
|
||||
elapsed += TickSeconds;
|
||||
if (failures.Count > 0 && !started)
|
||||
{
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
if (elapsed > TimeoutSeconds)
|
||||
{
|
||||
Fail($"timeout at step {index + 1} (scene='{SceneManager.GetCurrentSceneName()}')");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
if (waitTicks > 0)
|
||||
{
|
||||
waitTicks--;
|
||||
return;
|
||||
}
|
||||
if (heldKey != Key.None)
|
||||
{
|
||||
Input.ParseInputEvent(new InputEventKey { PhysicalKeycode = heldKey, Keycode = heldKey, Pressed = false });
|
||||
heldKey = Key.None;
|
||||
waitTicks = NextGapTicks;
|
||||
return;
|
||||
}
|
||||
if (!started)
|
||||
{
|
||||
if (!Boot.IsReady || string.IsNullOrEmpty(SceneManager.GetCurrentSceneName()))
|
||||
return;
|
||||
started = true;
|
||||
GameLogger.Log("TEST", $"DocShotsDriver: started in '{SceneManager.GetCurrentSceneName()}', {steps.Count} steps");
|
||||
waitTicks = SettleTicks;
|
||||
return;
|
||||
}
|
||||
if (index >= steps.Count)
|
||||
{
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (RunStep(steps[index]))
|
||||
{
|
||||
index++;
|
||||
repeat = 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Fail($"step {index + 1} '{string.Join(' ', steps[index])}': {ex.GetType().Name}: {ex.Message}");
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Runs one step; false keeps it for the next tick.</summary>
|
||||
private bool RunStep(string[] step)
|
||||
{
|
||||
var arg = step.Length > 1 ? step[1] : null;
|
||||
switch (step[0])
|
||||
{
|
||||
case "scene":
|
||||
if (pendingScene == null)
|
||||
{
|
||||
pendingScene = arg;
|
||||
Box?.Abort();
|
||||
Scenes.SwitchScene(arg);
|
||||
return false;
|
||||
}
|
||||
if (SceneManager.GetCurrentSceneName() != pendingScene)
|
||||
return false;
|
||||
Check(true, $"entered '{pendingScene}'");
|
||||
pendingScene = null;
|
||||
waitTicks = SettleTicks;
|
||||
return true;
|
||||
case "time":
|
||||
var parts = arg.Split(':');
|
||||
var time = GameData.Instance.Runtime.Time;
|
||||
time.SetTime(time.Tick - time.Tick % MinutesPerDay + int.Parse(parts[0]) * 60 + int.Parse(parts[1]));
|
||||
waitTicks = NextGapTicks;
|
||||
return true;
|
||||
case "wait":
|
||||
waitTicks = int.Parse(arg);
|
||||
return true;
|
||||
case "abort":
|
||||
Box?.Abort();
|
||||
waitTicks = NextGapTicks;
|
||||
return true;
|
||||
case "next":
|
||||
Box?.NextAction();
|
||||
waitTicks = NextGapTicks;
|
||||
repeat++;
|
||||
return repeat >= (arg == null ? 1 : int.Parse(arg));
|
||||
case "waitanswer":
|
||||
if (Box?.IsWaitingAnswer != true)
|
||||
{
|
||||
if (Box != null && Box.IsRunning)
|
||||
Box.NextAction();
|
||||
waitTicks = NextGapTicks;
|
||||
return false;
|
||||
}
|
||||
Check(true, "dialog waits for an answer");
|
||||
return true;
|
||||
case "choose":
|
||||
Check(Box != null && Box.TrySelectVariant(int.Parse(arg)), $"option {arg} taken");
|
||||
waitTicks = SettleTicks;
|
||||
return true;
|
||||
case "panel":
|
||||
UiRegistry.Toggle(arg);
|
||||
waitTicks = SettleTicks / 2;
|
||||
return true;
|
||||
case "key":
|
||||
heldKey = Enum.Parse<Key>(arg);
|
||||
Input.ParseInputEvent(new InputEventKey { PhysicalKeycode = heldKey, Keycode = heldKey, Pressed = true });
|
||||
waitTicks = step.Length > 2 ? int.Parse(step[2]) : 1;
|
||||
return true;
|
||||
case "tap":
|
||||
Check(Tap(arg, int.Parse(step[2])), $"tap {arg} {step[2]}");
|
||||
waitTicks = NextGapTicks;
|
||||
return true;
|
||||
case "press":
|
||||
var label = string.Join(" ", step.Skip(1));
|
||||
Check(Press(GetTree().Root, label), $"press '{label}'");
|
||||
waitTicks = NextGapTicks;
|
||||
return true;
|
||||
case "give":
|
||||
GameData.Instance.Runtime.Inventory.Add(arg, step.Length > 2 ? int.Parse(step[2]) : 1);
|
||||
return true;
|
||||
case "party":
|
||||
GameData.Instance.Runtime.CharacterData.AddCharacterToParty(arg);
|
||||
return true;
|
||||
case "signal":
|
||||
var source = GetTree().Root.FindChild(arg, true, false);
|
||||
Check(source != null && source.HasSignal(step[2]), $"signal {arg} {step[2]}");
|
||||
if (source != null && source.HasSignal(step[2]))
|
||||
{
|
||||
if (step.Length > 3)
|
||||
source.EmitSignal(step[2], step[3]);
|
||||
else
|
||||
source.EmitSignal(step[2]);
|
||||
}
|
||||
waitTicks = NextGapTicks;
|
||||
return true;
|
||||
case "hover":
|
||||
case "click":
|
||||
Mouse(ParseFraction(arg), ParseFraction(step[2]), step[0] == "click");
|
||||
waitTicks = NextGapTicks;
|
||||
return true;
|
||||
case "drag":
|
||||
Drag(ParseFraction(arg), ParseFraction(step[2]), ParseFraction(step[3]), ParseFraction(step[4]));
|
||||
waitTicks = NextGapTicks;
|
||||
return true;
|
||||
case "hurt":
|
||||
Check(GameData.Instance.Runtime.CharacterData.Characters.TryGetValue(arg, out var hurt) && hurt.Attributes?.Health != null, $"hurt {arg}");
|
||||
if (hurt?.Attributes?.Health != null)
|
||||
hurt.Attributes.Health.Value = int.Parse(step[2]);
|
||||
return true;
|
||||
case "tool":
|
||||
var provider = ToolFactory.Instance.GetAll().FirstOrDefault(t => t.Id == arg);
|
||||
Check(provider != null, $"tool {arg}");
|
||||
if (provider != null)
|
||||
tools[arg] = lastTool = ToolWindow.ShowWindow(provider.CreateWindow());
|
||||
waitTicks = SettleTicks;
|
||||
return true;
|
||||
case "type":
|
||||
TypeText(string.Join(' ', step.Skip(1)));
|
||||
waitTicks = SettleTicks / 3;
|
||||
return true;
|
||||
case "call":
|
||||
CallTool(step);
|
||||
waitTicks = SettleTicks / 2;
|
||||
return true;
|
||||
case "run":
|
||||
var runner = GetTree().Root.GetNodeOrNull<Node>("/root/DialogRunner") as IInteractiveDialogRunner;
|
||||
Check(runner != null, $"run {string.Join(' ', step.Skip(1))}");
|
||||
runner?.ExecuteInlineCommands(string.Join(' ', step.Skip(1)));
|
||||
waitTicks = NextGapTicks;
|
||||
return true;
|
||||
case "shot":
|
||||
Shot(arg);
|
||||
waitTicks = NextGapTicks;
|
||||
return true;
|
||||
default:
|
||||
throw new InvalidOperationException("unknown step");
|
||||
}
|
||||
}
|
||||
|
||||
private void Check(bool condition, string what)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
passed++;
|
||||
GameLogger.Log("TEST", $"[PASS] {what}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Fail(what);
|
||||
}
|
||||
}
|
||||
|
||||
private void Fail(string what)
|
||||
{
|
||||
failures.Add(what);
|
||||
GameLogger.Log("TEST", $"[FAIL] {what}", LogLevel.Error);
|
||||
}
|
||||
|
||||
private bool Tap(string nodeName, int child)
|
||||
{
|
||||
var parent = GetTree().Root.FindChild(nodeName, true, false);
|
||||
if (parent == null || child >= parent.GetChildCount() || parent.GetChild(child) is not Control target)
|
||||
return false;
|
||||
target.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool Press(Node node, string label)
|
||||
{
|
||||
if (node is BaseButton button && button.IsVisibleInTree() && !button.Disabled
|
||||
&& (((button as Button)?.Text ?? string.Empty).Trim() == label || HasLabel(button, label)))
|
||||
{
|
||||
button.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
return true;
|
||||
}
|
||||
foreach (var child in node.GetChildren())
|
||||
if (Press(child, label))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasLabel(Node node, string label)
|
||||
{
|
||||
foreach (var child in node.GetChildren())
|
||||
if ((child is Label text && text.Text.Trim() == label) || HasLabel(child, label))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Mouse(float x, float y, bool click)
|
||||
{
|
||||
var viewport = GetViewport();
|
||||
var position = new Vector2(x, y) * viewport.GetVisibleRect().Size;
|
||||
viewport.WarpMouse(position);
|
||||
viewport.PushInput(new InputEventMouseMotion { Position = position, GlobalPosition = position });
|
||||
if (!click)
|
||||
return;
|
||||
viewport.PushInput(new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true, Position = position, GlobalPosition = position });
|
||||
viewport.PushInput(new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = false, Position = position, GlobalPosition = position });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the text of the focused line edit (in the last opened tool window, else in the game) and emits its
|
||||
/// change signal, as a player's typing would; focus it first with <c>click</c>.
|
||||
/// </summary>
|
||||
private void TypeText(string text)
|
||||
{
|
||||
var viewport = GodotObject.IsInstanceValid(lastTool) ? (Viewport)lastTool : GetViewport();
|
||||
var edit = viewport.GuiGetFocusOwner() as LineEdit;
|
||||
Check(edit != null, $"type {text}");
|
||||
if (edit == null)
|
||||
return;
|
||||
edit.Text = text;
|
||||
edit.EmitSignal(LineEdit.SignalName.TextChanged, text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls a public method of a tool window opened by an earlier <c>tool</c> step, by name and with string
|
||||
/// arguments: <c>call schema_palette SetValue stat str</c>. The tool types live in mods this suite does not
|
||||
/// depend on, so the method is found by reflection; a missing window or method is a failure.
|
||||
/// </summary>
|
||||
private void CallTool(string[] step)
|
||||
{
|
||||
var window = step.Length > 2 && tools.TryGetValue(step[1], out var w) ? w : null;
|
||||
var args = step.Skip(3).Cast<object>().ToArray();
|
||||
var method = window?.GetType().GetMethods().FirstOrDefault(m => m.Name == step[2]
|
||||
&& m.GetParameters().Length == args.Length && m.GetParameters().All(p => p.ParameterType == typeof(string)));
|
||||
Check(method != null, $"call {string.Join(' ', step.Skip(1))}");
|
||||
method?.Invoke(window, args);
|
||||
}
|
||||
|
||||
private void Drag(float x1, float y1, float x2, float y2)
|
||||
{
|
||||
var viewport = GetViewport();
|
||||
var size = viewport.GetVisibleRect().Size;
|
||||
var from = new Vector2(x1, y1) * size;
|
||||
var to = new Vector2(x2, y2) * size;
|
||||
viewport.WarpMouse(from);
|
||||
viewport.PushInput(new InputEventMouseMotion { Position = from, GlobalPosition = from });
|
||||
viewport.PushInput(new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true, Position = from, GlobalPosition = from, ButtonMask = MouseButtonMask.Left });
|
||||
for (var i = 1; i <= 8; i++)
|
||||
{
|
||||
var point = from.Lerp(to, i / 8f);
|
||||
viewport.PushInput(new InputEventMouseMotion { Position = point, GlobalPosition = point, ButtonMask = MouseButtonMask.Left, Relative = (to - from) / 8f });
|
||||
}
|
||||
viewport.WarpMouse(to);
|
||||
viewport.PushInput(new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = false, Position = to, GlobalPosition = to });
|
||||
}
|
||||
|
||||
private static float ParseFraction(string text)
|
||||
{
|
||||
return float.Parse(text, System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void Shot(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(shotsDir) || DisplayServer.GetName() == "headless")
|
||||
return;
|
||||
DirAccess.MakeDirRecursiveAbsolute(shotsDir);
|
||||
ViewportCapture.SavePng(GetViewport(), shotsDir.TrimEnd('/', '\\') + "/" + name + ".png");
|
||||
GameLogger.Log("TEST", $"DocShotsDriver: saved {name}.png");
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
index = -1;
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name=doc_shots passed={passed} failed={failures.Count}");
|
||||
GetTree().Quit(failures.Count == 0 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.DocShots
|
||||
{
|
||||
/// <summary>
|
||||
/// Entry point of the doc_shots suite: adds the driver when the exact flag
|
||||
/// <c>--itest=doc_shots</c> is given. <c>--doc-steps=<file></c> is the
|
||||
/// steps file, <c>--shots=<dir></c> where the pictures go.
|
||||
/// </summary>
|
||||
public class DocShotsTestInitializer : IModInitializer
|
||||
{
|
||||
private static bool driverQueued;
|
||||
|
||||
/// <summary>Nothing per mod.</summary>
|
||||
public void OnModLoaded(string modId)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Queues the driver once.</summary>
|
||||
public void OnModResourcesReady()
|
||||
{
|
||||
var args = OS.GetCmdlineUserArgs();
|
||||
if (driverQueued || !args.Any(a => a == "--itest=doc_shots"))
|
||||
return;
|
||||
|
||||
driverQueued = true;
|
||||
var steps = ArgValue(args, "--doc-steps=");
|
||||
var shots = ArgValue(args, "--shots=");
|
||||
GameLogger.Log("TEST", $"DocShotsTestInitializer: steps={steps ?? "none"}, shots={shots ?? "off"}");
|
||||
ModRoot.AddDeferred(new DocShotsDriver(steps, shots));
|
||||
}
|
||||
|
||||
private static string ArgValue(string[] args, string prefix) =>
|
||||
args.Where(a => a.StartsWith(prefix, System.StringComparison.Ordinal))
|
||||
.Select(a => a.Substring(prefix.Length))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base__gdscript_name = Mod de prueba de integración con GDScript
|
||||
mod_dialog_base__gdscript_desc = Pruebas de integración de acciones de mods en GDScript: carga de .gd, compilación, registro de acciones y ejecución de escenarios.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base__gdscript_name = GDScript 統合テスト MOD
|
||||
mod_dialog_base__gdscript_desc = GDScript による MOD アクションの統合テスト:.gd の読み込み、コンパイル、アクション登録、シナリオ実行。
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base__gdscript_name = GDScript 통합 테스트 모드
|
||||
mod_dialog_base__gdscript_desc = GDScript 모드 액션의 통합 테스트: .gd 로드, 컴파일, 액션 등록, 시나리오 실행.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base__gdscript_name = Mod kiểm thử tích hợp GDScript
|
||||
mod_dialog_base__gdscript_desc = Kiểm thử tích hợp các hành động mod bằng GDScript: tải .gd, biên dịch, đăng ký hành động và chạy kịch bản.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_dialog_base__gdscript_name = GDScript 集成测试模组
|
||||
mod_dialog_base__gdscript_desc = GDScript 模组动作的集成测试:.gd 加载、编译、动作注册和剧本执行。
|
||||
@@ -24,7 +24,7 @@ namespace Mods.DialogBase.Tests
|
||||
return;
|
||||
queued = true;
|
||||
GameLogger.Log("TEST", "GdscriptTest: activating");
|
||||
(Godot.Engine.GetMainLoop() as SceneTree)?.Root?.CallDeferred("add_child", new GdscriptTestDriverNode());
|
||||
ModRoot.AddDeferred(new GdscriptTestDriverNode());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
using Cthangover.Mods.Dialog.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// <c>option … if=…</c>: an option is offered only while its condition holds; a choice whose
|
||||
/// options are all hidden is skipped, so the dialog never waits for an answer nobody can give.
|
||||
/// </summary>
|
||||
public class OptionConditionTests
|
||||
{
|
||||
[ModTest("options/parser_keeps_the_condition")]
|
||||
static void ParserKeepsTheCondition(ModTestContext ctx)
|
||||
{
|
||||
var dlg = ScenarioParser.Parse("select \"?\"\noption \"A\" -> :a\noption \"B\" \"if=door == 'open' && !false\" -> :b\n:a\nend\n:b\nend\n");
|
||||
var select = dlg.Queue.OfType<ActionSelect>().Single();
|
||||
ModAssert.Null(select.Variants[0].Condition, "a plain option has no condition");
|
||||
ModAssert.Equal("door == 'open' && !false", select.Variants[1].Condition, "the quoted pair keeps spaces");
|
||||
ModAssert.Equal("b", select.Variants[1].GoTo, "the jump target stays");
|
||||
}
|
||||
|
||||
[ModTest("options/hidden_options_are_not_offered", TimeoutSeconds = 10)]
|
||||
static async Task HiddenOptionsAreNotOffered(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
try
|
||||
{
|
||||
var dlg = ScenarioParser.Parse("select \"?\"\noption \"A\" if=false -> :a\noption \"B\" if=true -> :b\noption \"C\" -> :c\n:a\nend\n:b\nend\n:c\nend\n");
|
||||
ModAssert.True(stage.Box.SetDialogQueueAndRun(dlg, null, 0, null), "dialog starts");
|
||||
ModAssert.True(stage.Box.IsWaitingAnswer, "waits for an answer");
|
||||
ModAssert.Equal(2, stage.Box.AnswerVariantCount, "the false option is hidden");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("options/all_hidden_skips_the_choice", TimeoutSeconds = 10)]
|
||||
static async Task AllHiddenSkipsTheChoice(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
try
|
||||
{
|
||||
var dlg = ScenarioParser.Parse("select \"?\"\noption \"A\" if=false -> :a\n\nset outcome skipped\nend\n:a\nset outcome a\nend\n");
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
ModAssert.False(stage.Box.IsWaitingAnswer, "no answer is awaited");
|
||||
ModAssert.Equal("skipped", stage.Box.Runtime.GetVariable("outcome"), "the dialog went on past the choice");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Mods.Dialog.Scenarios;
|
||||
|
||||
/// <summary><c>when</c> jumps on the shared condition language, evaluated when the line runs.</summary>
|
||||
public class WhenCommandStrategyTests
|
||||
{
|
||||
private static string Outcome(string scenario)
|
||||
{
|
||||
var dlg = ScenarioParser.Parse(scenario);
|
||||
var runtime = new DialogRuntime();
|
||||
runtime.SetDialogQueueAndRun(null, dlg.Queue, null);
|
||||
runtime.Run();
|
||||
return runtime.GetVariable("outcome");
|
||||
}
|
||||
|
||||
[ModTest("when/true_false_and_else")]
|
||||
static void TrueFalseAndElse(ModTestContext ctx)
|
||||
{
|
||||
ModAssert.Equal("yes", Outcome("when true -> :yes\nset outcome no\nend\n:yes\nset outcome yes\nend\n"), "true jumps");
|
||||
ModAssert.Equal("no", Outcome("when false -> :yes\nset outcome no\nend\n:yes\nset outcome yes\nend\n"), "false falls through");
|
||||
ModAssert.Equal("other", Outcome("when false -> :yes else=other\nset outcome no\nend\n:yes\nset outcome yes\nend\n:other\nset outcome other\nend\n"), "false takes else");
|
||||
}
|
||||
|
||||
[ModTest("when/reads_state_when_the_line_runs")]
|
||||
static void ReadsStateWhenTheLineRuns(ModTestContext ctx)
|
||||
{
|
||||
ScenarioCondition.SetFlag("test_when_flag", "0");
|
||||
try
|
||||
{
|
||||
const string scenario = "flag test_when_flag 2\nwhen \"test_when_flag >= 2 && !false\" -> :yes\nset outcome no\nend\n:yes\nset outcome yes\nend\n";
|
||||
ModAssert.Equal("yes", Outcome(scenario), "a flag set two lines earlier counts");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ScenarioCondition.SetFlag("test_when_flag", "0");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user