From 68ed11cadc4d81cafe0f6d7d7148dd0776a0d327 Mon Sep 17 00:00:00 2001 From: iisaev Date: Sat, 24 Oct 2026 10:01:11 +0200 Subject: [PATCH] Scenario conditions in dialogs: option if= hides choices, when moved here from stat checks; command schemas Co-Authored-By: Claude Opus 5.5 --- locale/schema_en.properties | 29 ++++ locale/schema_es.properties | 29 ++++ locale/schema_ja.properties | 29 ++++ locale/schema_ko.properties | 29 ++++ locale/schema_ru.properties | 29 ++++ locale/schema_vi.properties | 29 ++++ locale/schema_zh.properties | 29 ++++ schemas/dialog_base.json | 164 ++++++++++++++++++++ src/Scenarios/Impls/WhenCommandStrategy.cs | 43 +++++ src/Scenarios/ScenarioParser.cs | 5 +- src/UI/Dialog/Action/Impls/ActionSelect.cs | 19 ++- src/UI/Dialog/Action/Impls/SelectVariant.cs | 10 ++ tests/OptionConditionTests.cs | 59 +++++++ tests/WhenCommandStrategyTests.cs | 42 +++++ 14 files changed, 542 insertions(+), 3 deletions(-) create mode 100644 locale/schema_en.properties create mode 100644 locale/schema_es.properties create mode 100644 locale/schema_ja.properties create mode 100644 locale/schema_ko.properties create mode 100644 locale/schema_ru.properties create mode 100644 locale/schema_vi.properties create mode 100644 locale/schema_zh.properties create mode 100644 schemas/dialog_base.json create mode 100644 src/Scenarios/Impls/WhenCommandStrategy.cs create mode 100644 tests/OptionConditionTests.cs create mode 100644 tests/WhenCommandStrategyTests.cs diff --git a/locale/schema_en.properties b/locale/schema_en.properties new file mode 100644 index 0000000..b83dc49 --- /dev/null +++ b/locale/schema_en.properties @@ -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 diff --git a/locale/schema_es.properties b/locale/schema_es.properties new file mode 100644 index 0000000..019bb85 --- /dev/null +++ b/locale/schema_es.properties @@ -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 diff --git a/locale/schema_ja.properties b/locale/schema_ja.properties new file mode 100644 index 0000000..9ad00da --- /dev/null +++ b/locale/schema_ja.properties @@ -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"。空なら常に diff --git a/locale/schema_ko.properties b/locale/schema_ko.properties new file mode 100644 index 0000000..6691d3f --- /dev/null +++ b/locale/schema_ko.properties @@ -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". 비우면 항상 diff --git a/locale/schema_ru.properties b/locale/schema_ru.properties new file mode 100644 index 0000000..3e6eb41 --- /dev/null +++ b/locale/schema_ru.properties @@ -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"; пусто — всегда diff --git a/locale/schema_vi.properties b/locale/schema_vi.properties new file mode 100644 index 0000000..8f934c8 --- /dev/null +++ b/locale/schema_vi.properties @@ -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 diff --git a/locale/schema_zh.properties b/locale/schema_zh.properties new file mode 100644 index 0000000..8f1ca15 --- /dev/null +++ b/locale/schema_zh.properties @@ -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";留空——总是显示 diff --git a/schemas/dialog_base.json b/schemas/dialog_base.json new file mode 100644 index 0000000..3da982c --- /dev/null +++ b/schemas/dialog_base.json @@ -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" + } + ] + } + ] +} diff --git a/src/Scenarios/Impls/WhenCommandStrategy.cs b/src/Scenarios/Impls/WhenCommandStrategy.cs new file mode 100644 index 0000000..7c2af52 --- /dev/null +++ b/src/Scenarios/Impls/WhenCommandStrategy.cs @@ -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; + + /// + /// A conditional jump in the middle of a dialog: + /// when "stat(cha) >= 5 && door == 'open'" -> :persuaded [else=refused] + /// 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 else when it does not (or goes on). + /// + /// It is the RPG Maker "Conditional Branch" without a new syntax: the condition is the + /// same text a scenario header's condition: takes. Several words without quotes + /// are joined back with spaces, so short conditions need no quotes. + /// + public class WhenCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "when"; + + /// + public int MinPositional => 1; + + /// + public IReadOnlyList AllowedNamed => new[] { "else" }; + + /// + public string JumpTargetNamedKey => "else"; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary 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(':'); + } +} diff --git a/src/Scenarios/ScenarioParser.cs b/src/Scenarios/ScenarioParser.cs index 4044666..0a419d7 100644 --- a/src/Scenarios/ScenarioParser.cs +++ b/src/Scenarios/ScenarioParser.cs @@ -103,7 +103,10 @@ namespace Cthangover.Mods.Dialog.Scenarios if (pendingSelect.Variants == null) pendingSelect.Variants = new List(); - 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; } diff --git a/src/UI/Dialog/Action/Impls/ActionSelect.cs b/src/UI/Dialog/Action/Impls/ActionSelect.cs index a9b4c92..a669055 100644 --- a/src/UI/Dialog/Action/Impls/ActionSelect.cs +++ b/src/UI/Dialog/Action/Impls/ActionSelect.cs @@ -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; /// Substitutes variables in the variants and context text, then shows the answer box; the runtime stays paused until a variant is picked. + /// + /// 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. + /// 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)); } } diff --git a/src/UI/Dialog/Action/Impls/SelectVariant.cs b/src/UI/Dialog/Action/Impls/SelectVariant.cs index e8769e9..ec7cbcc 100644 --- a/src/UI/Dialog/Action/Impls/SelectVariant.cs +++ b/src/UI/Dialog/Action/Impls/SelectVariant.cs @@ -23,6 +23,16 @@ namespace Cthangover.Mods.Dialog.Action.Impls /// Target action ID to jump to via when this variant is selected. public string GoTo { get; set; } + /// + /// Condition of the shared condition language (option "…" if=… -> :label); 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. + /// + public string Condition { get; set; } + + /// Whether the option is offered now. + public bool IsShown() => string.IsNullOrWhiteSpace(Condition) || ScenarioCondition.Evaluate(Condition); + /// Generates a new unique action ID for use as a GoTo target (e.g. with ). public static string Point => Guid.NewGuid().ToString(); diff --git a/tests/OptionConditionTests.cs b/tests/OptionConditionTests.cs new file mode 100644 index 0000000..4a884a6 --- /dev/null +++ b/tests/OptionConditionTests.cs @@ -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; + + /// + /// option … if=…: 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. + /// + 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().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(); + } + } + } +} diff --git a/tests/WhenCommandStrategyTests.cs b/tests/WhenCommandStrategyTests.cs new file mode 100644 index 0000000..ce9384b --- /dev/null +++ b/tests/WhenCommandStrategyTests.cs @@ -0,0 +1,42 @@ +namespace Cthangover.Mods.Dialog.Tests +{ + using Cthangover.Core.Mods.Testing; + using Cthangover.Core.Scenes; + using Cthangover.Mods.Dialog.Scenarios; + + /// when jumps on the shared condition language, evaluated when the line runs. + 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"); + } + } + } +}