commit 5de3fd7ad88c9403853b30207da65e898f359b93 Author: iisaev Date: Sun Oct 18 12:41:38 2026 +0300 init mod diff --git a/locale/en.properties b/locale/en.properties new file mode 100644 index 0000000..0b6eed8 --- /dev/null +++ b/locale/en.properties @@ -0,0 +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. diff --git a/locale/ru.properties b/locale/ru.properties new file mode 100644 index 0000000..173b73f --- /dev/null +++ b/locale/ru.properties @@ -0,0 +1,2 @@ +mod_dialog_base_name = Движок диалогов +mod_dialog_base_desc = Подсистема диалогов движка: UI DialogBox/DialogCanvas/AnswerBox, очередь диалогов с полным набором DSL-действий, парсер .scenario, цепочки событий и /root/DialogRunner для интерактивных диалогов. diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..203decf --- /dev/null +++ b/manifest.json @@ -0,0 +1,17 @@ +{ + "id": "dialog_base", + "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).", + "category": "engine, library", + "sources": [ + "src/**/*.cs" + ], + "depends": ["base2d>=1", "interactive_base>=1", "effects_base>=1", "time_base>=1", "map_base>=1", "interface>=1"], + "ui": [], + "test_suites": [ + {"id": "autoplay", "activate": ["--choices=", "--timeout="]}, + {"id": "gdscript", "activate": ["--itest=gdscript"]} + ] +} diff --git a/patches/scenes/ui/base_scene.tscn b/patches/scenes/ui/base_scene.tscn new file mode 100644 index 0000000..ad37719 --- /dev/null +++ b/patches/scenes/ui/base_scene.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="PackedScene" path="res://mods/base2d/scenes/ui/base_scene.tscn" id="1_target"] +[ext_resource type="Script" path="res://mods/dialog_base/src/UI/Executable/ExecutableMainEventChain.cs" id="2_chain"] + +[node name="BaseScene" instance=ExtResource("1_target")] +script = ExtResource("2_chain") diff --git a/src/Actions/ScenarioActionContext.cs b/src/Actions/ScenarioActionContext.cs new file mode 100644 index 0000000..432246a --- /dev/null +++ b/src/Actions/ScenarioActionContext.cs @@ -0,0 +1,115 @@ +using Cthangover.Core.Actions; +using Cthangover.Core.Items; +using Cthangover.Core.Mods; +using Cthangover.Core.Quests; +using Cthangover.Core.Settings; +using Cthangover.Mods.Dialog; +using Cthangover.Core.Utils; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; +using Cthangover.Mods.Map; +using Cthangover.Mods.Time; + +namespace Cthangover.Mods.Dialog +{ + /// + /// Concrete IActionContext wired to a DialogRuntime. Each service property + /// is instantiated inline (no DI container) — this is intentional: scenario + /// actions are short-lived commands, so creating new service instances per + /// context is cheap and avoids shared state. GetParam delegates to the + /// dialog runtime's variable store, bridging the dialog DSL's "set" variables + /// to the atomic action system. The Inventory reference is try-caught because + /// it may not exist at the moment of context creation (e.g. before game data + /// is fully loaded). + /// + internal class ScenarioActionContext : IActionContext + { + private readonly DialogRuntime runtime; + + /// + /// Service accessor for quest operations. Resolved from the kernel + /// quest registry — the implementation (QuestServiceImpl) lives in + /// the quests_base engine mod and is published there during mod + /// load, so contexts created after boot always see a live service. + /// + public IQuestService Quests => QuestServiceRegistry.Provider; + /// + /// Service accessor for character party operations. Instantiated + /// per-context. Routes to CharacterData via strict enum parsing. + /// + public ICharacterService Character { get; } = new CharacterServiceImpl(); + /// + /// Service accessor for lighting control. Instantiated per-context. + /// Thin wrapper around UiLightController singleton. + /// + public ILightingService Lighting { get; } = new LightingServiceImpl(); + /// + /// Service accessor for runtime scene node manipulation. + /// Instantiated per-context. All operations route through + /// SceneContextNode.Instance as the scene root. + /// + public ISceneNodeService Scene { get; } = new SceneNodeServiceImpl(); + /// + /// Singleton reference to the mod registry. Shared across all + /// contexts because mod registration is global and mod data + /// doesn't change during a single dialog execution. + /// + public IModRegistry ModRegistry { get; } = Cthangover.Core.Mods.ModRegistry.Instance; + /// + /// Player inventory reference, captured once at context + /// construction time. Try-caught because GameData may not be + /// fully loaded when the context is created (e.g. during early + /// dialog initialization before the game scene is ready). + /// Consumers must tolerate a null inventory. + /// + public IInventory Inventory { get; } + /// + /// Map zone control. Stateless adapter over the + /// MapZoneService singleton, which owns the actual state; + /// safe to instantiate per-context like the quest service, because the + /// state does not live in the adapter. + /// + public IMapZoneService Map { get; } = new MapZoneServiceImpl(); + + /// + /// Creates a context wired to the given DialogRuntime. The + /// runtime provides the variable store for GetParam — without + /// it, actions would have no way to receive parameters from the + /// scenario DSL. Inventory is captured eagerly (not lazily) + /// because it's fixed for the lifetime of this context. + /// + public ScenarioActionContext(DialogRuntime runtime) + { + this.runtime = runtime; + try { Inventory = GameData.Instance?.Runtime?.Inventory; } + catch { Inventory = null; } + } + + /// + /// Reads a named variable from the dialog runtime's variable + /// store. This is the bridge between the scenario DSL's "set" + /// command and the atomic action system — variables set in the + /// script are consumed here. Returns null for undefined variables. + /// + public string GetParam(string name) => runtime.GetVariable(name); + + /// + /// Emits a debug-level log entry via GameLogger. The category + /// groups related entries for filtering; use consistent + /// categories (e.g. "EVENT", "QUEST", "BATTLE") across actions. + /// + public void Log(string category, string message) => GameLogger.Log(category, message); + + /// + /// Emits a warning-level log entry. Automatically prepends + /// "[WARN]" to the message and logs at LogLevel.Warning. + /// + public void LogWarning(string category, string message) => GameLogger.Log(category, $"[WARN] {message}", LogLevel.Warning); + } +} diff --git a/src/DialogModInitializer.cs b/src/DialogModInitializer.cs new file mode 100644 index 0000000..b054c0d --- /dev/null +++ b/src/DialogModInitializer.cs @@ -0,0 +1,63 @@ +using Cthangover.Core.Bootstrap; +using Cthangover.Core.Events; +using Cthangover.Core.Mods; +using Cthangover.Core.Scenarios; +using Cthangover.Core.UI; +using Cthangover.Core.Utils; +using Cthangover.Mods.Dialog.Scenarios; +using Godot; + +namespace Cthangover.Mods.Dialog +{ + /// + /// Engine-mod initializer for the dialog subsystem. On resources-ready it + /// publishes the scenario event factory used by the core SceneManager and + /// parks the /root/DialogRunner node implementing the core + /// IInteractiveDialogRunner contract. All operations are idempotent: + /// ModManager.Reload() re-runs OnModResourcesReady, so node + /// creation must tolerate repeat invocations. Scenario command strategies + /// of every mod reach their factory through + /// , not through this initializer. + /// + public class DialogModInitializer : IModInitializer + { + private static bool initialized; + + /// No per-mod setup is required; registration happens on resources-ready. + public void OnModLoaded(string modId) { } + + /// + /// Publishes the scenario event factory and queues the dialog runner + /// node. Safe to call again after a mod reload. + /// + public void OnModResourcesReady() + { + if (initialized) + return; + initialized = true; + + // Scenario-backed events (scene definitions and test-play) are built + // by the core SceneManager through this factory. + AppServices.Current.SceneEvents.ScenarioEventFactory = new ScenarioEventFactory(); + + EnsureDialogRunner(); + } + + private static void EnsureDialogRunner() + { + var root = ModEventBus.Instance?.GetTree()?.Root; + if (root == null) + { + GameLogger.Log("DIALOG", "DialogModInitializer: tree root not available, skipping runner setup", LogLevel.Error); + return; + } + + if (root.GetNodeOrNull("DialogRunner") != null) + return; + + // 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" }); + } + } +} \ No newline at end of file diff --git a/src/DialogScriptRunner.cs b/src/DialogScriptRunner.cs new file mode 100644 index 0000000..bb03586 --- /dev/null +++ b/src/DialogScriptRunner.cs @@ -0,0 +1,131 @@ +using Cthangover.Core.Factories.Impls; +using Cthangover.Core.Interactive; +using Cthangover.Core.Mods.Resolvers; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Mods; +using Cthangover.Core.Scenes; +using Cthangover.Core.UI; +using Cthangover.Core.Utils; +using Cthangover.Mods.Dialog.Scenarios; + +using Cthangover.Mods.Interface.Base; +namespace Cthangover.Mods.Dialog +{ + /// + /// Dialog execution entry point of the dialog_base engine mod, parked at + /// /root/DialogRunner. Implements the core IInteractiveDialogRunner + /// contract: interactive click-dialogs (a scenario file combined with + /// inline DSL commands) and standalone inline DSL commands are parsed here + /// and executed through the mod-owned dialog runtime and the shared + /// . + /// + public partial class DialogScriptRunner : ModNode, IInteractiveDialogRunner + { + /// Runs the click action of the interactive definition with the given id through the shared DialogBox. The definition is resolved through the interactive_base factory. Returns false when the definition is unknown, the box is missing, busy, or the definition carries no runnable commands. + public bool TryExecuteClickDialog(string definitionId) + { + var definition = InteractiveFactory.Instance.Get(definitionId); + if (definition == null) + { + GameLogger.Log("INTERACTIVE", $"TryExecuteClickDialog: definition '{definitionId}' not found", LogLevel.Error); + return false; + } + + var dialogBox = SceneContextNode.FindNode("DialogBox"); + if (dialogBox == null) + { + GameLogger.Log("INTERACTIVE", "TryExecuteClickDialog: DialogBox not found", LogLevel.Error); + return false; + } + + if (dialogBox.IsRunning) + { + GameLogger.Log("INTERACTIVE", "TryExecuteClickDialog: dialog already running, skipping"); + return false; + } + + var dlg = BuildClickDialog(definition); + if (dlg == null || dlg.Queue.Count == 0) + return false; + + dialogBox.SetDialogQueueAndRun(dlg, null, 0, null); + return true; + } + + /// Runs inline DSL commands (hover effects and similar) through a standalone dialog runtime. + public void ExecuteInlineCommands(string dsl) + { + if (string.IsNullOrWhiteSpace(dsl)) + return; + + try + { + var dlg = ScenarioParser.Parse(dsl); + if (dlg?.Queue == null || dlg.Queue.Count == 0) + return; + + var runtime = new DialogRuntime(); + runtime.SetDialogQueueAndRun(null, dlg.Queue, null, 0); + runtime.Run(); + } + catch (System.Exception ex) + { + GameLogger.Log("INTERACTIVE", $"ExecuteInlineCommands failed: {ex.Message}", LogLevel.Error); + } + } + + private DialogQueue BuildClickDialog(InteractiveDefinition def) + { + if (def?.Actions?.OnClick == null) + return null; + + var clickAction = def.Actions.OnClick; + var combinedText = ""; + + if (!string.IsNullOrEmpty(clickAction.Scenario)) + { + var modId = def.ModId; + if (string.IsNullOrEmpty(modId)) + modId = ResolveModIdForScenario(clickAction.Scenario); + + if (!string.IsNullOrEmpty(modId)) + { + var scenarioText = ModManager.Instance.ReadFileText(modId, clickAction.Scenario); + if (!string.IsNullOrEmpty(scenarioText)) + { + var (metadata, body) = ScenarioMetadata.ParseMetadata(scenarioText); + combinedText += body + "\n"; + } + } + } + + if (clickAction.Commands != null && clickAction.Commands.Length > 0) + { + combinedText += string.Join("\n", clickAction.Commands); + } + + if (string.IsNullOrWhiteSpace(combinedText)) + return null; + + return ScenarioParser.Parse(combinedText); + } + + private string ResolveModIdForScenario(string scenarioPath) + { + var defs = ScenarioDefs.CollectDefinitions(); + var fileName = System.IO.Path.GetFileNameWithoutExtension( + scenarioPath.Replace('\\', '/').Split('/')[^1]); + + foreach (var kv in defs) + { + foreach (var sd in kv.Value) + { + if (sd.Name == fileName) + return sd.ModId; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Factories/ScenarioCommandStrategyFactory.cs b/src/Factories/ScenarioCommandStrategyFactory.cs new file mode 100644 index 0000000..118157e --- /dev/null +++ b/src/Factories/ScenarioCommandStrategyFactory.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using Cthangover.Mods.Dialog.Scenarios; +using Cthangover.Core.Utils; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Registry of implementations, + /// indexed by their name. + /// It discovers nothing itself: + /// delivers the strategies of every mod as the kernel registers its + /// assembly and takes them back when the mod unloads, so the table never + /// holds a strategy of a dead assembly (which would pin it and keep running + /// its old code after a hot reload). + /// + public static class ScenarioCommandStrategyFactory + { + private static readonly Dictionary Strategies = new(); + + /// + /// Looks up the registered for . + /// Returns null if no strategy exists for the given command name. + /// + public static IScenarioCommandStrategy Get(string command) + { + Strategies.TryGetValue(command, out var strategy); + return strategy; + } + + /// + /// Enumerates all registered DSL command names (lowercase). + /// + public static IEnumerable GetAllCommandNames() + { + return Strategies.Keys; + } + + /// + /// Returns the for a command if its + /// strategy implements that interface, or null otherwise. Used by the + /// build pipeline to collect asset dependencies from scenario scripts. + /// + public static ICommandReferenceMetadata GetReferenceMetadata(string command) + { + if (Strategies.TryGetValue(command, out var strategy) && strategy is ICommandReferenceMetadata meta) + return meta; + return null; + } + + /// + /// Registers a strategy under its command name. The first strategy of a + /// command wins: a mod cannot silently take over a built-in command. + /// + public static void Register(IScenarioCommandStrategy strategy) + { + if (strategy != null && !Strategies.ContainsKey(strategy.Command)) + Strategies[strategy.Command] = strategy; + } + + /// + /// Removes (its mod is being unloaded). A + /// command owned by another strategy instance is left alone. + /// + public static void Unregister(IScenarioCommandStrategy strategy) + { + if (strategy != null && Strategies.TryGetValue(strategy.Command, out var registered) && registered == strategy) + Strategies.Remove(strategy.Command); + } + } +} diff --git a/src/ScenarioEventFactory.cs b/src/ScenarioEventFactory.cs new file mode 100644 index 0000000..014840c --- /dev/null +++ b/src/ScenarioEventFactory.cs @@ -0,0 +1,33 @@ +using Cthangover.Core.UI.Executable; +using Cthangover.Mods.Dialog.Executable; + +namespace Cthangover.Mods.Dialog +{ + /// + /// Builds scenario-backed executable events for the core SceneManager. + /// The returned is parented to the requesting + /// event chain by IExecutableEventChain.AddEvent, so the factory + /// only needs to configure it. + /// + public class ScenarioEventFactory : IScenarioEventFactory + { + /// Builds a one-shot event that executes the inline scenario text (test-play path). + public IExecutableEvent CreateFromText(string scenarioText) + { + return new ScenarioEvent { ScenarioText = scenarioText }; + } + + /// Builds an event from a scene event definition's scenario fields. + public IExecutableEvent CreateFromDefinition(string scenarioText, string scenarioId, string condition, bool? lightUseTime, bool isOneRun) + { + return new ScenarioEvent + { + ScenarioText = scenarioText, + ScenarioId = scenarioId, + Condition = condition, + LightUseTime = lightUseTime, + IsOneRun = isOneRun, + }; + } + } +} \ No newline at end of file diff --git a/src/Scenarios/ICommandReferenceMetadata.cs b/src/Scenarios/ICommandReferenceMetadata.cs new file mode 100644 index 0000000..774db35 --- /dev/null +++ b/src/Scenarios/ICommandReferenceMetadata.cs @@ -0,0 +1,40 @@ +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Classifies the type of resource that the first positional argument of a + /// scenario command refers to. Used for dependency tracking — the build tool + /// can scan commands by their to + /// discover which assets (backgrounds, scenes, music, etc.) are referenced + /// and need to be packed into the final build. + /// + public enum PositionalReferenceKind + { + /// No resource reference in the first positional argument. + None, + /// A background image resource path. + Background, + /// A scene resource path (for scene switching). + Scene, + /// A music/audio resource path. + Music, + /// A sound effect resource path. + Sound, + /// An effect/shader resource path. + Effect, + /// An interactive action definition ID. + Action + } + + /// + /// Optional interface for command strategies whose first positional argument + /// references an external resource. Strategies implementing this interface + /// are inspected by the build pipeline to collect asset dependencies. + /// + public interface ICommandReferenceMetadata + { + /// + /// The kind of resource referenced by the first positional argument of this command. + /// + PositionalReferenceKind Positional0Kind { get; } + } +} diff --git a/src/Scenarios/IScenarioCommandSpec.cs b/src/Scenarios/IScenarioCommandSpec.cs new file mode 100644 index 0000000..f476c3d --- /dev/null +++ b/src/Scenarios/IScenarioCommandSpec.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Optional declarative contract of a scenario command, implemented by + /// classes that want their arguments + /// checked without the checker knowing the command name. The default members + /// mean a strategy overrides only what differs from "accepts anything". + /// + /// The contract belongs to the command itself, so including a new command + /// from a mod (or Core) automatically teaches the validator its rules; a + /// command that does not implement this interface is still recognised as a + /// known command but its arguments are not constrained. + /// + public interface IScenarioCommandSpec + { + /// Minimum number of positional arguments. Default: 0. + int MinPositional => 0; + + /// Maximum number of positional arguments; -1 means unlimited. Default: -1. + int MaxPositional => -1; + + /// + /// Minimum number of arguments in total (positional + named). Used by + /// commands whose required input may arrive either way, e.g. text that + /// is either literal or a key= lookup. + /// + int MinArguments => 0; + + /// Named arguments that must be present. Default: none. + IReadOnlyList RequiredNamed => Array.Empty(); + + /// + /// Named arguments the command understands. Empty/null means the command + /// accepts arbitrary named arguments (e.g. action forwards them as + /// dialog variables), so unknown-name checks are skipped. + /// + IReadOnlyList AllowedNamed => null; + + /// + /// Named argument that carries a :label jump target, or null. + /// Lets the checker validate that the target exists without knowing the + /// command; goto declares "target" here. + /// + string JumpTargetNamedKey => null; + } +} diff --git a/src/Scenarios/IScenarioCommandStrategy.cs b/src/Scenarios/IScenarioCommandStrategy.cs new file mode 100644 index 0000000..9c1a782 --- /dev/null +++ b/src/Scenarios/IScenarioCommandStrategy.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Strategy interface for handling a single DSL command type in scenario scripts. + /// Each command word (e.g. text, music, goto) maps to one + /// strategy implementation. The + /// auto-discovers all implementations via reflection and dispatches commands + /// to the matching strategy during parsing. + /// + public interface IScenarioCommandStrategy + { + /// + /// The lowercase DSL command word this strategy handles (e.g. "text", "music"). + /// + string Command { get; } + + /// + /// Executes the command by appending one or more dialog actions to . + /// + /// The dialog queue to push actions onto. + /// Unnamed tokens following the command word in order. + /// Key-value pairs parsed from name=value tokens (keys are lowercased). + /// The target label following a -> arrow token, if present. + /// Optional localization provider for resolving key= lookups. + void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale); + } +} diff --git a/src/Scenarios/Impls/ActionCommandStrategy.cs b/src/Scenarios/Impls/ActionCommandStrategy.cs new file mode 100644 index 0000000..9a963cc --- /dev/null +++ b/src/Scenarios/Impls/ActionCommandStrategy.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the action DSL command. Triggers a named interactive action + /// (defined in the action system) and forwards any extra named parameters as + /// context variables via . + /// Implements to signal that the + /// first positional argument is an reference. + /// + public class ActionCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec + { + /// + public string Command => "action"; + /// + public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Action; + /// + public int MinPositional => 1; + /// + public int MaxPositional => 1; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var actionName = positional.Count > 0 ? positional[0] : ""; + + foreach (var kv in named) + { + if (kv.Key is "name" or "key") continue; + dlg.Set(kv.Key, kv.Value); + } + + dlg.Action(actionName); + } + } +} diff --git a/src/Scenarios/Impls/AnimationCommandStrategy.cs b/src/Scenarios/Impls/AnimationCommandStrategy.cs new file mode 100644 index 0000000..0c3b82b --- /dev/null +++ b/src/Scenarios/Impls/AnimationCommandStrategy.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Globalization; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the animation DSL command. Supports two modes: + /// (a) multiple sprites via sprites= named parameter, or + /// (b) a single sprite base with start, count, speed, and loop controls. + /// Delegates to . + /// + public class AnimationCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "animation"; + /// + public int MinArguments => 1; + /// + public int MaxPositional => 1; + /// + public IReadOnlyList AllowedNamed => new[] { "sprites", "speed", "loop", "start", "count" }; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + if (named.TryGetValue("sprites", out var spritesStr)) + { + var sprites = new List(spritesStr.Split(',')); + var speed = named.TryGetValue("speed", out var sp) ? ParseFloat(sp) : 1f; + var loop = !named.TryGetValue("loop", out var lp) || lp == "true"; + dlg.Animation(sprites, speed, 1f, loop); + } + else if (positional.Count > 0) + { + var sprite = positional[0]; + var start = named.TryGetValue("start", out var st) ? ParseInt(st) : 0; + var count = named.TryGetValue("count", out var cn) ? ParseInt(cn) : 1; + var speed = named.TryGetValue("speed", out var sp) ? ParseFloat(sp) : 1f; + var loop = !named.TryGetValue("loop", out var lp) || lp == "true"; + dlg.Animation(sprite, start, count, speed, 1f, loop); + } + } + + private static float ParseFloat(string s) + { + return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : 0f; + } + + private static int ParseInt(string s) + { + return int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v : 0; + } + } +} diff --git a/src/Scenarios/Impls/BackgroundColorCommandStrategy.cs b/src/Scenarios/Impls/BackgroundColorCommandStrategy.cs new file mode 100644 index 0000000..a4217d8 --- /dev/null +++ b/src/Scenarios/Impls/BackgroundColorCommandStrategy.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the background_color DSL command. Sets the dialog scene's + /// background color from a named color string (e.g. black, red) + /// or an HTML hex color (e.g. #FF0000). Delegates to . + /// + public class BackgroundColorCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "background_color"; + /// + public int MinPositional => 1; + /// + public int MaxPositional => 1; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + if (positional.Count > 0) + dlg.BackgroundColor(ColorFromString(positional[0])); + } + + private static Color ColorFromString(string s) + { + if (s.StartsWith('#')) + return Color.FromHtml(s.Substring(1)); + + var named = s.ToLowerInvariant() switch + { + "black" => Colors.Black, + "white" => Colors.White, + "red" => Colors.Red, + "green" => Colors.Green, + "blue" => Colors.Blue, + "yellow" => Colors.Yellow, + "gray" or "grey" => Colors.Gray, + _ => Colors.Black, + }; + return named; + } + } +} diff --git a/src/Scenarios/Impls/BackgroundCommandStrategy.cs b/src/Scenarios/Impls/BackgroundCommandStrategy.cs new file mode 100644 index 0000000..56f9088 --- /dev/null +++ b/src/Scenarios/Impls/BackgroundCommandStrategy.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the background DSL command. Sets the dialog scene background + /// image from a resource path. Implements + /// with so the build pipeline + /// can track referenced background assets. + /// + public class BackgroundCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec + { + /// + public string Command => "background"; + /// + public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Background; + /// + public int MinPositional => 1; + /// + public int MaxPositional => 1; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + dlg.Background(positional.Count > 0 ? positional[0] : ""); + } + } +} diff --git a/src/Scenarios/Impls/BackgroundShowHideCommandStrategy.cs b/src/Scenarios/Impls/BackgroundShowHideCommandStrategy.cs new file mode 100644 index 0000000..cd2ee67 --- /dev/null +++ b/src/Scenarios/Impls/BackgroundShowHideCommandStrategy.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Globalization; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Mods.Dialog.Action.Impls; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the background_show_hide DSL command. Animates the background + /// in (show) or out (hide) with configurable duration and + /// wait flag. Delegates to . + /// + public class BackgroundShowHideCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "background_show_hide"; + /// + public int MaxPositional => 1; + /// + public IReadOnlyList AllowedNamed => new[] { "duration", "wait" }; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var typeStr = positional.Count > 0 ? positional[0] : "show"; + var type = typeStr.ToLowerInvariant() == "show" + ? BackgroundActionType.Show + : BackgroundActionType.Hide; + var duration = named.TryGetValue("duration", out var dur) ? ParseFloat(dur) : 1f; + var wait = named.TryGetValue("wait", out var w) && w == "true"; + dlg.BackgroundShowHide(type, duration, wait); + } + + private static float ParseFloat(string s) + { + return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : 0f; + } + } +} diff --git a/src/Scenarios/Impls/DelayCommandStrategy.cs b/src/Scenarios/Impls/DelayCommandStrategy.cs new file mode 100644 index 0000000..fc8788d --- /dev/null +++ b/src/Scenarios/Impls/DelayCommandStrategy.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Globalization; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the delay DSL command. Pauses dialog execution for a + /// number of seconds, optionally displaying a text label during the pause. + /// If the hidden flag is set, the delay runs without visible text. + /// Supports localization via the key= named parameter. + /// + public class DelayCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "delay"; + /// + public int MaxPositional => 2; + /// + public IReadOnlyList AllowedNamed => new[] { "key", "hidden" }; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var time = positional.Count > 0 ? ParseFloat(positional[0]) : 1f; + var hidden = positional.Count > 1 && positional[1] == "hidden" + || named.TryGetValue("hidden", out var h) && h == "true"; + var showText = hidden ? null + : positional.Count > 1 ? positional[1] + : null; + + if (named.TryGetValue("key", out var key) && locale != null && showText != null) + { + var loc = locale.Get(key); + if (loc != null) showText = loc; + } + + if (showText != null) + dlg.Delay(time, showText); + else + dlg.Delay(time, hidden); + } + + private static float ParseFloat(string s) + { + return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : 0f; + } + } +} diff --git a/src/Scenarios/Impls/EffectCommandStrategy.cs b/src/Scenarios/Impls/EffectCommandStrategy.cs new file mode 100644 index 0000000..6f08048 --- /dev/null +++ b/src/Scenarios/Impls/EffectCommandStrategy.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the effect DSL command. Applies a named visual effect/shader + /// to the scene. Implements with + /// for build-time dependency tracking. + /// + public class EffectCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec + { + /// + public string Command => "effect"; + /// + public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Effect; + /// + public int MinPositional => 1; + /// + public int MaxPositional => 1; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + if (positional.Count > 0) + dlg.Effect(positional[0]); + } + } +} diff --git a/src/Scenarios/Impls/EmptyCommandStrategy.cs b/src/Scenarios/Impls/EmptyCommandStrategy.cs new file mode 100644 index 0000000..96aa3cc --- /dev/null +++ b/src/Scenarios/Impls/EmptyCommandStrategy.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the empty DSL command. Queues a no-op placeholder action + /// () that is skipped instantly — the DSL-level label + /// mechanism (:label via ) is built on the + /// same action type, and a bare empty line works as a placeholder marker. + /// + public class EmptyCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "empty"; + /// + public int MaxPositional => 0; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + dlg.Empty(); + } + } +} diff --git a/src/Scenarios/Impls/EndCommandStrategy.cs b/src/Scenarios/Impls/EndCommandStrategy.cs new file mode 100644 index 0000000..36675c9 --- /dev/null +++ b/src/Scenarios/Impls/EndCommandStrategy.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the end DSL command. Marks the end of the dialog sequence, + /// terminating execution of the current scenario. Typically used at the final + /// line of a scenario script to signal the dialog engine to close or transition. + /// + public class EndCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "end"; + /// + public int MaxPositional => 0; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + dlg.End(); + } + } +} diff --git a/src/Scenarios/Impls/ForegroundCommandStrategy.cs b/src/Scenarios/Impls/ForegroundCommandStrategy.cs new file mode 100644 index 0000000..c65629d --- /dev/null +++ b/src/Scenarios/Impls/ForegroundCommandStrategy.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the foreground DSL command. Places a foreground overlay + /// image on top of the scene (above background and characters). The first + /// positional argument specifies the resource path to the image asset. + /// + public class ForegroundCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "foreground"; + /// + public int MinPositional => 1; + /// + public int MaxPositional => 1; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + dlg.Foreground(positional.Count > 0 ? positional[0] : ""); + } + } +} diff --git a/src/Scenarios/Impls/GotoCommandStrategy.cs b/src/Scenarios/Impls/GotoCommandStrategy.cs new file mode 100644 index 0000000..b2d8e71 --- /dev/null +++ b/src/Scenarios/Impls/GotoCommandStrategy.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the goto DSL command. Jumps execution to a labeled anchor + /// point (:labelname) in the scenario script. Supports two forms: + /// goto target_name via target= named parameter, or + /// goto -> target_name via the arrow target syntax. + /// + public class GotoCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "goto"; + /// + public int MinArguments => 1; + /// + public int MaxPositional => 0; + /// + public IReadOnlyList AllowedNamed => new[] { "target" }; + /// + public string JumpTargetNamedKey => "target"; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + if (!string.IsNullOrEmpty(arrowTarget)) + dlg.GoTo(arrowTarget); + else if (named.TryGetValue("target", out var target)) + dlg.GoTo(target); + } + } +} diff --git a/src/Scenarios/Impls/HideDialogCommandStrategy.cs b/src/Scenarios/Impls/HideDialogCommandStrategy.cs new file mode 100644 index 0000000..ca061f8 --- /dev/null +++ b/src/Scenarios/Impls/HideDialogCommandStrategy.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the hide_dialog DSL command. Hides the dialog UI panel + /// (text box, avatars, and UI chrome) from the screen. The inverse of + /// . + /// + public class HideDialogCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "hide_dialog"; + /// + public int MaxPositional => 0; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + dlg.HideDialog(); + } + } +} diff --git a/src/Scenarios/Impls/InteractiveAddCommandStrategy.cs b/src/Scenarios/Impls/InteractiveAddCommandStrategy.cs new file mode 100644 index 0000000..6c04295 --- /dev/null +++ b/src/Scenarios/Impls/InteractiveAddCommandStrategy.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Scenario DSL command interactive_add: instantiates an interactive + /// object from its definition ID. Optional layer parameter overrides + /// the layer from the definition. + /// + /// Usage: interactive_add door_to_kitchen + /// interactive_add door_to_kitchen layer=foreground + /// + public class InteractiveAddCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// The lowercase DSL keyword handled by this strategy: interactive_add. + public string Command => "interactive_add"; + + /// + public int MinPositional => 1; + + /// + public int MaxPositional => 1; + + /// + public IReadOnlyList AllowedNamed => new[] { "layer" }; + + /// Queues an for the definition id in positional[0]; lines without an id are skipped. + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + if (positional.Count == 0) + return; + + var id = positional[0]; + dlg.InteractiveAdd(id); + } + } +} diff --git a/src/Scenarios/Impls/InteractiveClearCommandStrategy.cs b/src/Scenarios/Impls/InteractiveClearCommandStrategy.cs new file mode 100644 index 0000000..4d17113 --- /dev/null +++ b/src/Scenarios/Impls/InteractiveClearCommandStrategy.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Scenario DSL command interactive_clear: removes all active + /// interactive objects from the scene. + /// + /// Usage: interactive_clear + /// + public class InteractiveClearCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// The lowercase DSL keyword handled by this strategy: interactive_clear. + public string Command => "interactive_clear"; + + /// + public int MaxPositional => 0; + + /// Queues an that removes every active interactive object from the scene. + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + dlg.InteractiveClear(); + } + } +} diff --git a/src/Scenarios/Impls/InteractiveRemoveCommandStrategy.cs b/src/Scenarios/Impls/InteractiveRemoveCommandStrategy.cs new file mode 100644 index 0000000..03bec35 --- /dev/null +++ b/src/Scenarios/Impls/InteractiveRemoveCommandStrategy.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Scenario DSL command interactive_remove: removes and destroys + /// an interactive object by its definition ID. + /// + /// Usage: interactive_remove door_to_kitchen + /// + public class InteractiveRemoveCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// The lowercase DSL keyword handled by this strategy: interactive_remove. + public string Command => "interactive_remove"; + + /// + public int MinPositional => 1; + + /// + public int MaxPositional => 1; + + /// Queues an for the definition id in positional[0]; lines without an id are skipped. + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + if (positional.Count == 0) + return; + + var id = positional[0]; + dlg.InteractiveRemove(id); + } + } +} diff --git a/src/Scenarios/Impls/InteractiveSetCommandStrategy.cs b/src/Scenarios/Impls/InteractiveSetCommandStrategy.cs new file mode 100644 index 0000000..72e2918 --- /dev/null +++ b/src/Scenarios/Impls/InteractiveSetCommandStrategy.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Scenario DSL command interactive_set: modifies a property of an + /// active interactive object. Supported properties: enabled, visible. + /// + /// Usage: interactive_set door_to_kitchen enabled=false + /// interactive_set door_to_kitchen visible=true + /// + public class InteractiveSetCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// The lowercase DSL keyword handled by this strategy: interactive_set. + public string Command => "interactive_set"; + + /// + public int MinPositional => 1; + + /// + public int MinArguments => 2; + + /// + public IReadOnlyList AllowedNamed => new[] { "enabled", "visible" }; + + /// Queues an for positional[0]; enabled= wins over visible= when both are given, and lines without both an id and a property are skipped. + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + if (positional.Count < 2) + return; + + var id = positional[0]; + + if (named.TryGetValue("enabled", out var enabled)) + dlg.InteractiveSet(id, "enabled", enabled); + else if (named.TryGetValue("visible", out var visible)) + dlg.InteractiveSet(id, "visible", visible); + } + } +} diff --git a/src/Scenarios/Impls/LightSetCommandStrategy.cs b/src/Scenarios/Impls/LightSetCommandStrategy.cs new file mode 100644 index 0000000..5ba3c45 --- /dev/null +++ b/src/Scenarios/Impls/LightSetCommandStrategy.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios.Impls +{ + /// + /// Handles the light_set DSL command. Passes a JSON string that + /// describes lighting configuration to the dialog engine via + /// . The JSON structure is forwarded + /// as-is to the rendering layer for point-light setup. + /// + public class LightSetCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "light_set"; + + /// + public int MaxPositional => 1; + + /// + public void Execute(DialogQueue dlg, List positional, + Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var json = positional.Count > 0 ? positional[0] : null; + dlg.LightSet(json); + } + } +} diff --git a/src/Scenarios/Impls/LightUseTimeCommandStrategy.cs b/src/Scenarios/Impls/LightUseTimeCommandStrategy.cs new file mode 100644 index 0000000..8685634 --- /dev/null +++ b/src/Scenarios/Impls/LightUseTimeCommandStrategy.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the light_use_time DSL command. Toggles whether the + /// scene lighting should be driven by the simulated time-of-day system. + /// Passes a boolean flag to . + /// + public class LightUseTimeCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "light_use_time"; + /// + public int MaxPositional => 1; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var val = positional.Count > 0 ? positional[0] : "true"; + dlg.LightUseTime(val == "true"); + } + } +} diff --git a/src/Scenarios/Impls/MusicCommandStrategy.cs b/src/Scenarios/Impls/MusicCommandStrategy.cs new file mode 100644 index 0000000..896ed51 --- /dev/null +++ b/src/Scenarios/Impls/MusicCommandStrategy.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the music DSL command. Starts playing a background music + /// track from the given resource path. Implements + /// with so the build pipeline + /// can track referenced audio assets. + /// + public class MusicCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec + { + /// + public string Command => "music"; + /// + public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Music; + /// + public int MinPositional => 1; + /// + public int MaxPositional => 1; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + if (positional.Count > 0) + dlg.Music(positional[0]); + } + } +} diff --git a/src/Scenarios/Impls/MusicPauseCommandStrategy.cs b/src/Scenarios/Impls/MusicPauseCommandStrategy.cs new file mode 100644 index 0000000..c774100 --- /dev/null +++ b/src/Scenarios/Impls/MusicPauseCommandStrategy.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the music_pause DSL command. Pauses the currently + /// playing background music stream. Playback can be resumed later with + /// . + /// + public class MusicPauseCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "music_pause"; + /// + public int MaxPositional => 0; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + dlg.MusicPause(); + } + } +} diff --git a/src/Scenarios/Impls/MusicPlayCommandStrategy.cs b/src/Scenarios/Impls/MusicPlayCommandStrategy.cs new file mode 100644 index 0000000..952a291 --- /dev/null +++ b/src/Scenarios/Impls/MusicPlayCommandStrategy.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the music_play DSL command. Resumes the currently + /// paused background music stream. Has no effect if music is already playing. + /// + public class MusicPlayCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "music_play"; + /// + public int MaxPositional => 0; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + dlg.MusicPlay(); + } + } +} diff --git a/src/Scenarios/Impls/NotifyCommandStrategy.cs b/src/Scenarios/Impls/NotifyCommandStrategy.cs new file mode 100644 index 0000000..927a31d --- /dev/null +++ b/src/Scenarios/Impls/NotifyCommandStrategy.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI.Messages; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the notify DSL command. Queues a notification text + /// into for display in the top-left + /// notification panel. This is a fire-and-forget command — it does not + /// block dialog execution. + /// + public class NotifyCommandStrategy : IScenarioCommandStrategy + { + /// + /// The lowercase DSL command word. + /// + public string Command => "notify"; + + /// + public int MinArguments => 1; + + /// + public int MaxPositional => 1; + + /// + public IReadOnlyList AllowedNamed => new[] { "key" }; + + /// + /// Extracts the notification text from the first positional argument + /// and enqueues it into . Supports + /// localization via the key= named parameter. + /// + /// The dialog queue (unused — notifications bypass dialog). + /// Positional arguments; positional[0] is the notification text. + /// Named arguments; "key" is a locale lookup key. + /// Unused by this command. + /// Optional localization provider for key= resolution. + public void Execute(DialogQueue dlg, List positional, + Dictionary named, string arrowTarget, + ILocalizationProvider locale) + { + var text = positional.Count > 0 ? positional[0] : null; + + if (named.TryGetValue("key", out var key) && locale != null) + { + var loc = locale.Get(key); + if (loc != null) text = loc; + } + + if (text != null) + NotificationBuffer.Add(text); + } + } +} diff --git a/src/Scenarios/Impls/PTextCommandStrategy.cs b/src/Scenarios/Impls/PTextCommandStrategy.cs new file mode 100644 index 0000000..fff8331 --- /dev/null +++ b/src/Scenarios/Impls/PTextCommandStrategy.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Mods.Dialog.Action; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the ptext DSL command. A "plain text" variant of + /// that skips speaker attribution + /// but still supports first/second avatar faces and + /// hide_color and wait behavior flags. Text content + /// can be provided directly or resolved via a key= localization key. + /// + public class PTextCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "ptext"; + /// + public int MinArguments => 1; + /// + public int MaxPositional => 1; + /// + public IReadOnlyList AllowedNamed => new[] { "key", "first", "second", "hide_color", "wait" }; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var textVal = positional.Count > 0 ? positional[0] : ""; + if (named.TryGetValue("key", out var key) && locale != null) + { + var loc = locale.Get(key); + if (loc != null) textVal = loc; + } + + var first = named.TryGetValue("first", out var f) ? f : null; + var second = named.TryGetValue("second", out var s) ? s : null; + var hideColor = named.TryGetValue("hide_color", out var hc) && hc == "true"; + + var action = dlg.PText(textVal, first, second, hideColor); + + if (named.TryGetValue("wait", out var wait) && wait == "false") + action.WaitType = WaitType.NoWait; + } + } +} diff --git a/src/Scenarios/Impls/SetCommandStrategy.cs b/src/Scenarios/Impls/SetCommandStrategy.cs new file mode 100644 index 0000000..9da25e7 --- /dev/null +++ b/src/Scenarios/Impls/SetCommandStrategy.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the set DSL command. Stores an arbitrary key-value pair + /// into the dialog session's variable state via . + /// Values are read back later through ${var} substitution in + /// ptext lines and via + /// in scenario actions. + /// + public class SetCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "set"; + /// + public int MinPositional => 2; + /// + public int MaxPositional => 2; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var name = positional.Count > 0 ? positional[0] : ""; + var value = positional.Count > 1 ? positional[1] : ""; + dlg.Set(name, value); + } + } +} diff --git a/src/Scenarios/Impls/ShowDialogCommandStrategy.cs b/src/Scenarios/Impls/ShowDialogCommandStrategy.cs new file mode 100644 index 0000000..73645ff --- /dev/null +++ b/src/Scenarios/Impls/ShowDialogCommandStrategy.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the show_dialog DSL command. Makes the dialog UI panel + /// (text box, avatars, and UI chrome) visible on screen. The inverse of + /// . + /// + public class ShowDialogCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "show_dialog"; + /// + public int MaxPositional => 0; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + dlg.ShowDialog(); + } + } +} diff --git a/src/Scenarios/Impls/SoundCommandStrategy.cs b/src/Scenarios/Impls/SoundCommandStrategy.cs new file mode 100644 index 0000000..8fa2d0a --- /dev/null +++ b/src/Scenarios/Impls/SoundCommandStrategy.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the sound DSL command. Plays a one-shot sound effect + /// from the given resource path. Implements + /// with so the build pipeline + /// can track referenced audio assets. + /// + public class SoundCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec + { + /// + public string Command => "sound"; + /// + public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Sound; + /// + public int MinPositional => 1; + /// + public int MaxPositional => 1; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + if (positional.Count > 0) + dlg.Sound(positional[0]); + } + } +} diff --git a/src/Scenarios/Impls/SwitchSceneCommandStrategy.cs b/src/Scenarios/Impls/SwitchSceneCommandStrategy.cs new file mode 100644 index 0000000..c9ea5dd --- /dev/null +++ b/src/Scenarios/Impls/SwitchSceneCommandStrategy.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Globalization; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the switch_scene DSL command. Transitions to a new Godot + /// scene (PackedScene) with configurable transition speed and an + /// optional hidden mode that shows only the scene background during + /// the transition. Implements with + /// for build-time dependency tracking. + /// + public class SwitchSceneCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec + { + /// + public string Command => "switch_scene"; + /// + public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Scene; + /// + public int MinPositional => 1; + /// + public int MaxPositional => 1; + /// + public IReadOnlyList AllowedNamed => new[] { "speed", "hidden" }; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var scene = positional.Count > 0 ? positional[0] : ""; + var speed = named.TryGetValue("speed", out var sp) ? ParseFloat(sp) : 4f; + var hidden = named.TryGetValue("hidden", out var h) && h == "true"; + dlg.SwitchScene(scene, speed, hidden); + } + + private static float ParseFloat(string s) + { + return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : 0f; + } + } +} diff --git a/src/Scenarios/Impls/TextCommandStrategy.cs b/src/Scenarios/Impls/TextCommandStrategy.cs new file mode 100644 index 0000000..4cd6375 --- /dev/null +++ b/src/Scenarios/Impls/TextCommandStrategy.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Mods.Dialog.Action; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the text DSL command, the primary dialog display instruction. + /// Shows character text with optional first/second speaker + /// avatar faces, hide_color tint suppression, and a wait flag + /// that controls whether the dialog waits for user input before continuing. + /// Text can be literal or resolved from a key= localization entry. + /// + public class TextCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "text"; + /// + public int MinArguments => 1; + /// + public int MaxPositional => 1; + /// + public IReadOnlyList AllowedNamed => new[] { "key", "first", "second", "hide_color", "wait" }; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var textVal = positional.Count > 0 ? positional[0] : ""; + if (named.TryGetValue("key", out var key) && locale != null) + { + var loc = locale.Get(key); + if (loc != null) textVal = loc; + } + + var first = named.TryGetValue("first", out var f) ? f : null; + var second = named.TryGetValue("second", out var s) ? s : null; + var hideColor = named.TryGetValue("hide_color", out var hc) && hc == "true"; + + var action = dlg.Text(textVal, first, second, hideColor); + + if (named.TryGetValue("wait", out var wait) && wait == "false") + action.WaitType = WaitType.NoWait; + } + } +} diff --git a/src/Scenarios/Impls/TitleCommandStrategy.cs b/src/Scenarios/Impls/TitleCommandStrategy.cs new file mode 100644 index 0000000..c1679da --- /dev/null +++ b/src/Scenarios/Impls/TitleCommandStrategy.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Handles the title DSL command. Displays a centered title/caption + /// text on screen, typically used for chapter headings or location labels. + /// Text can be literal or resolved from a key= localization entry. + /// Silently does nothing if no text value is provided or resolved. + /// + public class TitleCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec + { + /// + public string Command => "title"; + /// + public int MinArguments => 1; + /// + public int MaxPositional => 1; + /// + public IReadOnlyList AllowedNamed => new[] { "key" }; + + /// + public void Execute(DialogQueue dlg, List positional, Dictionary named, string arrowTarget, ILocalizationProvider locale) + { + var textVal = positional.Count > 0 ? positional[0] : null; + if (named.TryGetValue("key", out var key) && locale != null) + { + var loc = locale.Get(key); + if (loc != null) textVal = loc; + } + if (textVal != null) + dlg.Title(textVal); + } + } +} diff --git a/src/Scenarios/ScenarioCommandStrategyConsumer.cs b/src/Scenarios/ScenarioCommandStrategyConsumer.cs new file mode 100644 index 0000000..bfdcf3a --- /dev/null +++ b/src/Scenarios/ScenarioCommandStrategyConsumer.cs @@ -0,0 +1,23 @@ +using Cthangover.Core.Mods.Consumers; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Feeds with every mod's + /// implementations as the mods load + /// (dialog_base's own built-in commands included, through the join-time + /// catch-up) and removes them when their mod unloads, so a hot-reloaded + /// mod's commands run its fresh code and the dying assembly is not pinned. + /// + public class ScenarioCommandStrategyConsumer : ModTypeConsumer + { + /// + public override string Id => "dialog.command_strategies"; + + /// + protected override void Add(IScenarioCommandStrategy instance, string modId) => ScenarioCommandStrategyFactory.Register(instance); + + /// + protected override void Remove(IScenarioCommandStrategy instance, string modId) => ScenarioCommandStrategyFactory.Unregister(instance); + } +} diff --git a/src/Scenarios/ScenarioConverter.cs b/src/Scenarios/ScenarioConverter.cs new file mode 100644 index 0000000..b663fe4 --- /dev/null +++ b/src/Scenarios/ScenarioConverter.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using Cthangover.Core.Localization; +using Cthangover.Core.Mods; +using Cthangover.Mods.Dialog; +using Cthangover.Mods.Dialog.Action; +using Cthangover.Mods.Dialog.Action.Impls; +using Cthangover.Core.Utils; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Converts scenario DSL text or files into instances. + /// Wraps with a configurable + /// that defaults to when none is supplied. + /// + public class ScenarioConverter + { + private readonly ILocalizationProvider locale; + + /// + /// Creates a converter with an optional localization provider. + /// + /// Localization provider for key= text lookups. Defaults to Godot translations. + public ScenarioConverter(ILocalizationProvider locale = null) + { + this.locale = locale ?? GodotTranslationProvider.Instance; + } + + /// + /// Parses raw scenario script text into a dialog queue using the configured locale. + /// + public DialogQueue Convert(string text) + { + return ScenarioParser.Parse(text, locale); + } + + /// + /// Reads a scenario file from disk and parses it into a dialog queue. + /// + /// Filesystem path to the .scenario file. + /// The parsed , or null if the file could not be opened. + public DialogQueue ConvertFile(string path) + { + foreach (var kvp in ModRegistry.Instance.Mods) + { + var text = kvp.Value.FileProvider.ReadFileText(path); + if (text != null) + return Convert(text); + } + + var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + if (file == null) + { + GameLogger.Log("CONVERTER", $"Failed to open '{path}'", LogLevel.Error); + return null; + } + + var fileText = file.GetAsText(); + file.Close(); + + return Convert(fileText); + } + } +} diff --git a/src/Scenarios/ScenarioLoader.cs b/src/Scenarios/ScenarioLoader.cs new file mode 100644 index 0000000..b0fbec4 --- /dev/null +++ b/src/Scenarios/ScenarioLoader.cs @@ -0,0 +1,54 @@ +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Core.Utils; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Loads a .scenario file from disk, reads its text content, and + /// parses it via into a . + /// Uses as the default localization source. + /// + public static class ScenarioLoader + { + /// + /// Loads and parses a scenario file using the default Godot translation provider. + /// + /// Filesystem path to the .scenario file. + /// The parsed , or null if the file could not be opened. + public static DialogQueue Load(string path) + { + return Load(path, GodotTranslationProvider.Instance); + } + + /// + /// Loads and parses a scenario file with a custom localization provider for text lookups. + /// + /// Filesystem path to the .scenario file. + /// Localization provider to resolve key= references during parsing. + /// The parsed , or null if the file could not be opened. + public static DialogQueue Load(string path, ILocalizationProvider provider) + { + var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + if (file == null) + { + GameLogger.Log("SCENE", $"ScenarioLoader: failed to open '{path}'", LogLevel.Error); + return null; + } + + var text = file.GetAsText(); + file.Close(); + + return ScenarioParser.Parse(text, provider); + } + } +} diff --git a/src/Scenarios/ScenarioParser.cs b/src/Scenarios/ScenarioParser.cs new file mode 100644 index 0000000..cca95cd --- /dev/null +++ b/src/Scenarios/ScenarioParser.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using Cthangover.Core.Factories.Impls; +using Cthangover.Core.Localization; +using Cthangover.Mods.Dialog; +using Cthangover.Mods.Dialog.Action.Impls; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Scenarios +{ + /// + /// Parses scenario DSL script text into a of dialog actions. + /// The pipeline works in two phases: first the text is split into lines, then each + /// non-empty, non-comment line is tokenized and dispatched to the matching + /// via . + /// Special handling exists for :label anchor points, select/option + /// blocks (accumulated into ), and -> arrow targets. + /// + public static class ScenarioParser + { + /// + /// Parses a complete scenario script text and returns the resulting dialog queue. + /// + /// Raw scenario DSL content. + /// Optional localization provider for key= text lookups. + public static DialogQueue Parse(string text, ILocalizationProvider locale = null) + { + var dlg = new DialogQueue(); + var lines = text.Split('\n'); + ActionSelect pendingSelect = null; + + foreach (var rawLine in lines) + { + var line = rawLine.Trim(); + if (line.Length == 0 || line.StartsWith('#')) + { + FinalizePendingSelect(dlg, ref pendingSelect); + continue; + } + + if (line.StartsWith(':')) + { + FinalizePendingSelect(dlg, ref pendingSelect); + dlg.Point(line.Substring(1)); + continue; + } + + var tokens = Tokenize(line); + if (tokens.Count == 0) continue; + + var cmd = tokens[0].ToLowerInvariant(); + tokens.RemoveAt(0); + + var positional = new List(); + var named = new Dictionary(); + var arrowTarget = ""; + + for (var i = 0; i < tokens.Count; i++) + { + var t = tokens[i]; + if (t == "->") + { + if (i + 1 < tokens.Count) + { + arrowTarget = tokens[i + 1].TrimStart(':'); + i++; + } + continue; + } + + var eqIdx = t.IndexOf('='); + if (eqIdx > 0) + { + var name = t.Substring(0, eqIdx); + if (IsValidIdent(name)) + { + named[name.ToLowerInvariant()] = t.Substring(eqIdx + 1); + continue; + } + } + + positional.Add(t); + } + + if (cmd == "select") + { + FinalizePendingSelect(dlg, ref pendingSelect); + pendingSelect = new ActionSelect(); + var promptText = positional.Count > 0 ? positional[0] : null; + + if (named.TryGetValue("key", out var key) && locale != null) + { + var loc = locale.Get(key); + if (loc != null) promptText = loc; + } + if (promptText != null) + pendingSelect.Text = promptText; + + if (named.TryGetValue("first", out var first)) + pendingSelect.FirstAvatar = first; + if (named.TryGetValue("second", out var second)) + pendingSelect.SecondAvatar = second; + + continue; + } + + if (cmd == "option") + { + if (pendingSelect == null) continue; + + var optText = positional.Count > 0 ? positional[0] : ""; + + if (named.TryGetValue("key", out var key) && locale != null) + { + var loc = locale.Get(key); + if (loc != null) optText = loc; + } + + var goTo = arrowTarget; + if (string.IsNullOrEmpty(goTo)) + { + if (named.TryGetValue("goto", out var gt)) + goTo = gt; + else if (positional.Count > 1 && positional[1].StartsWith(':')) + goTo = positional[1].TrimStart(':'); + } + + if (pendingSelect.Variants == null) + pendingSelect.Variants = new List(); + pendingSelect.Variants.Add(SelectVariant.New(optText, goTo)); + continue; + } + + FinalizePendingSelect(dlg, ref pendingSelect); + + var strategy = ScenarioCommandStrategyFactory.Get(cmd); + if (strategy != null) + strategy.Execute(dlg, positional, named, arrowTarget, locale); + } + + FinalizePendingSelect(dlg, ref pendingSelect); + return dlg; + } + + /// + /// Extracts YAML-like front-matter metadata from script text. Lines before + /// a --- separator or the first non-key:value line are parsed as + /// key: value pairs. Returns both the metadata dictionary and the + /// remaining script content after the metadata block. + /// + public static (Dictionary metadata, string remainingText) ParseMetadata(string text) + { + var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); + var lines = text.Split('\n'); + var contentStart = 0; + var inMetadata = true; + + for (var i = 0; i < lines.Length; i++) + { + var line = lines[i].Trim(); + + if (!inMetadata) + { + continue; + } + + if (line == "---") + { + contentStart = i + 1; + break; + } + + if (string.IsNullOrEmpty(line)) + { + contentStart = i; + break; + } + + var colonIdx = line.IndexOf(':'); + if (colonIdx > 0) + { + var key = line.Substring(0, colonIdx).Trim(); + var val = line.Substring(colonIdx + 1).Trim(); + metadata[key] = val; + } + else + { + contentStart = i; + break; + } + } + + var remaining = string.Join("\n", lines, contentStart, lines.Length - contentStart); + return (metadata, remaining); + } + + private static void FinalizePendingSelect(DialogQueue dlg, ref ActionSelect pending) + { + if (pending == null) return; + + if (pending.Variants == null || pending.Variants.Count == 0) + { + pending = null; + return; + } + + dlg.Queue.Add(pending); + pending = null; + } + + private static List Tokenize(string line) + { + var result = new List(); + var i = 0; + + while (i < line.Length) + { + if (char.IsWhiteSpace(line[i])) + { + i++; + continue; + } + + if (line[i] == '"') + { + i++; + var start = i; + while (i < line.Length && line[i] != '"') + { + if (line[i] == '\\' && i + 1 < line.Length) + i++; + i++; + } + result.Add(line[start..i].Replace("\\\"", "\"")); + if (i < line.Length) i++; + continue; + } + + var tokenStart = i; + while (i < line.Length && !char.IsWhiteSpace(line[i])) + i++; + result.Add(line[tokenStart..i]); + } + + return result; + } + + private static bool IsValidIdent(string s) + { + if (string.IsNullOrEmpty(s)) return false; + for (var i = 0; i < s.Length; i++) + { + var c = s[i]; + if (!char.IsLetterOrDigit(c) && c != '_') + return false; + } + return true; + } + } +} diff --git a/src/SubsystemRegistration.cs b/src/SubsystemRegistration.cs new file mode 100644 index 0000000..684bb70 --- /dev/null +++ b/src/SubsystemRegistration.cs @@ -0,0 +1,14 @@ +namespace Cthangover.Mods.Dialog +{ + /// + /// Migration marker for the coverage pipeline + /// (SubsystemsBridge reflection scan): declares the dialog + /// subsystem fully moved out of the core assembly into this engine mod. + /// The id must match the DeclaredTests entry in + /// SubsystemsCoverageTestSetup. + /// + public static class SubsystemRegistration + { + public static string SubsystemId => "dialog"; + } +} \ No newline at end of file diff --git a/src/UI/Dialog/Action/ActionCommand.cs b/src/UI/Dialog/Action/ActionCommand.cs new file mode 100644 index 0000000..301aad7 --- /dev/null +++ b/src/UI/Dialog/Action/ActionCommand.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using Godot; +using Cthangover.Core.Actions; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action +{ + /// + /// Base for all dialog actions with a lifecycle of Construct → Run → Destruct. + /// Each action gets an auto-generated GUID for targeting by GoTo/If commands. + /// The three timing enums control when Construct and Destruct fire relative to + /// the action's lifecycle and queue life, enabling both immediate teardown + /// (OnEndAction) and deferred cleanup (OnEndQueue). WaitType determines whether + /// the runtime pauses here (WaitClick/Select/Time/Event) or advances immediately + /// (NoWait). DelayedDestruct is a set of action IDs whose Destruct should be + /// called after this action's own destruct — used for cross-action cleanup + /// dependencies where one action's visual state must outlast another. + /// + public abstract class ActionCommand : IActionCommand + { + /// Unique identifier for this action. Auto-generated GUID by default; can be overridden via for GoTo targeting. + public string ID { get; set; } + /// Whether has been called and has completed. + public bool IsConstructed { get; private set; } + /// Whether has been called and this action is finished. + public bool IsDestructed { get; private set; } + /// When to call : OnStartQueue (preload) or OnStartAction (lazy, just before run). Default is OnStartAction. + public virtual ConstructType ConstructType { get; set; } = ConstructType.OnStartAction; + /// When to call : OnEndAction (immediate), OnDelayed (manual cleanup), or OnEndQueue (when dialog finishes). Default is OnEndAction. + public virtual DestructType DestructType { get; set; } = DestructType.OnEndAction; + /// Controls how the runtime advances past this action. Default is WaitClick (pause for user input). + public virtual WaitType WaitType { get; set; } = WaitType.WaitClick; + /// Duration in seconds for WaitTime. Used only when is WaitTime. + public virtual float WaitTime { get; set; } + /// Timestamp when was called. Used by WaitTime actions to measure elapsed time. + public float StartTime { get; private set; } + /// Action IDs whose will be triggered when this action destructs. Used for cross-action cleanup dependencies. + public ISet DelayedDestruct { get; } = new HashSet(); + + /// + /// Scene node lookup seam for subclasses. Replaces direct + /// SceneContextNode.FindNode calls so action behavior can be exercised + /// against a fake lookup in unit tests instead of the global scene-tree + /// singleton. Defaults to the runtime implementation + /// () and is instantiated per action + /// instance — the same no-container style as ScenarioActionContext. + /// + protected ISceneNodeService Scene { get; set; } = new SceneNodeServiceImpl(); + + /// Generates a unique ID so every action is addressable by GoTo/If even without an explicit call. + public ActionCommand() + { + ID = Guid.NewGuid().ToString(); + } + + /// Idempotent construction: calls once and sets to true. + public void Construct() + { + if(IsConstructed) return; + DoConstruct(); + IsConstructed = true; + } + + /// Idempotent desctruction: calls once, then sets false and true. + public void Destruct() + { + if(IsDestructed) return; + DoDestruct(); + IsConstructed = false; + IsDestructed = true; + } + + /// Constructs if not already done, records start time, and delegates execution to . + public void Run(DialogRuntime runtime) + { + if (!IsConstructed) DoConstruct(); + StartTime = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0); + DoRun(runtime); + } + + /// Override in subclasses to implement action-specific behavior. Receives the runtime for text processing and dialog box access. + public abstract void DoRun(DialogRuntime runtime); + /// Override to preload resources or register listeners. Called by . + public virtual void DoConstruct() { } + /// Override to release resources or disconnect listeners. Called by . + public virtual void DoDestruct() { } + + /// Fluent setter for . Use to assign a named target for GoTo/If commands. + public ActionCommand SetID(string id) + { + ID = id; + return this; + } + } +} diff --git a/src/UI/Dialog/Action/ActionType.cs b/src/UI/Dialog/Action/ActionType.cs new file mode 100644 index 0000000..d812639 --- /dev/null +++ b/src/UI/Dialog/Action/ActionType.cs @@ -0,0 +1,14 @@ +namespace Cthangover.Mods.Dialog.Action +{ + /// + /// High-level action category discriminator. Currently minimal (Text, End) + /// — may expand as additional action families are added. + /// + public enum ActionType + { + /// A dialog text display action that pauses for click input. + Text, + /// Terminates the dialog queue. + End + } +} diff --git a/src/UI/Dialog/Action/ConstructType.cs b/src/UI/Dialog/Action/ConstructType.cs new file mode 100644 index 0000000..9b125a5 --- /dev/null +++ b/src/UI/Dialog/Action/ConstructType.cs @@ -0,0 +1,17 @@ +namespace Cthangover.Mods.Dialog.Action +{ + /// + /// Controls when an action's Construct() fires. OnStartQueue — construct + /// when the dialog queue is first loaded (good for preloading); OnStartAction — + /// lazy-construct just before the action runs (saves resources for unreached + /// branches). Default for most actions is OnStartAction. + /// + public enum ConstructType + { + /// Construct when the dialog queue is first loaded. Useful for actions that need to preload resources. + OnStartQueue, + /// Construct lazily just before the action runs. Default for most actions — saves resources for unreached branches. + OnStartAction + } + +} diff --git a/src/UI/Dialog/Action/DestructType.cs b/src/UI/Dialog/Action/DestructType.cs new file mode 100644 index 0000000..6289cbd --- /dev/null +++ b/src/UI/Dialog/Action/DestructType.cs @@ -0,0 +1,20 @@ +namespace Cthangover.Mods.Dialog.Action +{ + /// + /// Controls when an action's Destruct() fires. OnEndAction — immediately after + /// the action advances (typical for most commands). OnDelayed — deferred until + /// specifically cleaned up (for visual state that must persist across actions). + /// OnEndQueue — destruct when the entire dialog queue finishes (for resources + /// that must survive the whole dialog but not beyond). + /// + public enum DestructType + { + /// Destruct immediately when the action is advanced past. Default for most commands. + OnEndAction, + /// Destruct is deferred — must be manually triggered or triggered via . + OnDelayed, + /// Destruct when the entire dialog queue finishes. Use for resources that must survive the whole dialog but not beyond. + OnEndQueue, + } + +} diff --git a/src/UI/Dialog/Action/IActionCommand.cs b/src/UI/Dialog/Action/IActionCommand.cs new file mode 100644 index 0000000..aae9824 --- /dev/null +++ b/src/UI/Dialog/Action/IActionCommand.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action +{ + /// + /// Full dialog action contract: extends IActionDestruct with construction, + /// execution (Run), and queryable state (WaitType, timing enums, delayed-destruct + /// dependencies). The ID property enables cross-action references (GoTo, If). + /// + public interface IActionCommand : IActionDestruct + { + + #region Properties + + /// Unique action identifier, auto-generated GUID. Can be overridden for named GoTo targets. + string ID { get; set; } + + /// Whether has completed. + bool IsConstructed { get; } + /// Whether has been called and the action is finished. + bool IsDestructed { get; } + /// Controls when construction fires relative to the action's lifecycle. + ConstructType ConstructType { get; } + /// Controls when destruction fires relative to the action's lifecycle. + DestructType DestructType { get; } + /// How the runtime should handle advancement past this action. + WaitType WaitType { get; } + /// IDs of actions whose Destruct should be called when this action destructs. + ISet DelayedDestruct { get; } + /// Duration in seconds when WaitType is WaitTime. + float WaitTime { get; } + /// Timestamp of the last call, in seconds since epoch. + float StartTime { get; } + + #endregion + + #region Methods + + /// Called by the runtime to initialize the action before execution. + void Construct(); + /// Called by the runtime to execute the action. Receives the runtime for context. + void Run(DialogRuntime runtime); + + #endregion + + } + +} diff --git a/src/UI/Dialog/Action/IActionDestruct.cs b/src/UI/Dialog/Action/IActionDestruct.cs new file mode 100644 index 0000000..c84c747 --- /dev/null +++ b/src/UI/Dialog/Action/IActionDestruct.cs @@ -0,0 +1,13 @@ +namespace Cthangover.Mods.Dialog.Action +{ + /// + /// Minimal destructible contract — anything in the dialog runtime's object list + /// that must be cleaned up when a dialog ends. + /// + public interface IActionDestruct + { + /// Called to release resources, disconnect signals, and mark the object as finished. Must be idempotent. + void Destruct(); + } + +} diff --git a/src/UI/Dialog/Action/Impls/ActionAnimatedForeground.cs b/src/UI/Dialog/Action/Impls/ActionAnimatedForeground.cs new file mode 100644 index 0000000..d727bd5 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionAnimatedForeground.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using Cthangover.Mods.Effects.Animation; +using Cthangover.Mods.Dialog.Action; +using Cthangover.Core.UI.Event; +using Cthangover.Mods.Interface.View; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Frame-animates the scene foreground by swapping ViewBox textures at a + /// configurable speed. Loads frames from SpriteIDs on construction + /// (OnStartQueue). Unlike AnimationController which crossfades, this does + /// hard cuts — simpler but acceptable for foreground effects. The speed + /// switches to NextFrameSpeed after the first frame so first-frame delay + /// can differ from subsequent frames. Registers as IOnUpdateEvent for + /// per-frame advancement. Non-looping mode auto-stops at last frame. + /// + public class ActionAnimatedForeground : ActionCommand, IOnUpdateEvent + { + /// Update priority. Runs after higher-priority listeners. Value 0. + public int Priority => 0; + + /// List of sprite resource paths to display as animation frames. Loaded in . + public List SpriteIDs { get; set; } + /// Initial frame rate multiplier for the first frame. + public float Speed { get; set; } = 1f; + /// Frame rate multiplier for subsequent frames after the first. Allows different pacing for the initial frame. + public float NextFrameSpeed { get; set; } = 1f; + /// Whether the animation wraps after the last frame. When false, playback stops at the final frame. + public bool IsLoop { get; set; } = true; + + private int currentFrame; + private float elapsed; + private bool isAnimating; + private List frames; + + /// Preloads frames when the dialog queue is first loaded, so textures are ready before playback. + public override ConstructType ConstructType { get; set; } = ConstructType.OnStartQueue; + /// Animation runs in the background — the dialog queue continues immediately. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Preloads every sprite in into memory so playback never stalls on I/O. + public override void DoConstruct() + { + base.DoConstruct(); + LoadFrames(); + } + + /// Stops the animation loop; the last shown frame stays on screen. + public override void DoDestruct() + { + base.DoDestruct(); + isAnimating = false; + } + + private void LoadFrames() + { + frames = new List(); + if (SpriteIDs == null) + return; + foreach (var id in SpriteIDs) + { + var tex = GD.Load(id); + if (tex != null) + frames.Add(tex); + } + } + + /// Resets playback to the first frame and shows it on the ViewBox; per-frame stepping happens through . + public override void DoRun(DialogRuntime runtime) + { + currentFrame = 0; + elapsed = 0f; + isAnimating = frames != null && frames.Count > 0; + + if (isAnimating) + { + var viewBox = Scene.Find("ViewBox"); + viewBox?.SetForegroundTexture(frames[0]); + } + } + + void IOnUpdateEvent.OnUpdate() + { + if (!isAnimating || frames == null || frames.Count == 0) + return; + + var viewBox = Scene.Find("ViewBox"); + if (viewBox == null) + return; + + elapsed += 1f / 60f; + var frameDuration = 1f / Mathf.Max(Speed, 0.01f); + if (elapsed >= frameDuration) + { + elapsed -= frameDuration; + currentFrame++; + if (currentFrame >= frames.Count) + { + if (IsLoop) + currentFrame = 0; + else + { + isAnimating = false; + return; + } + } + viewBox.SetForegroundTexture(frames[currentFrame]); + Speed = NextFrameSpeed; + } + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionBackground.cs b/src/UI/Dialog/Action/Impls/ActionBackground.cs new file mode 100644 index 0000000..97d0b68 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionBackground.cs @@ -0,0 +1,81 @@ +using Cthangover.Core.Factories.Impls; +using Cthangover.Core.Scenes; +using Cthangover.Mods.Dialog.Action; +using Cthangover.Mods.Time; +using Cthangover.Mods.Interface.View; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Sets the scene background on the ViewBox, including depth and albedo maps + /// for the lighting system. Resolves textures through BackgroundFactory on + /// construction (OnStartQueue) — depth and albedo are loaded from the same + /// sprite ID with "_depth" / "_albedo" suffixes. Kills any in-progress + /// background transition before setting the new texture. Textures are nulled + /// after application to prevent stale reuse across dialog restarts. + /// Sets SceneContextNode.LastBackgroundID so the scene manager can restore + /// the correct background on scene revisit. + /// + public class ActionBackground : ActionCommand + { + /// Sprite resource ID resolved through on construction. Also loads depth (_depth) and albedo (_albedo) variants. + public string SpriteID { get; set; } + /// Direct texture override. When non-null, bypasses factory resolution. Set to null after application to prevent stale reuse. + public Texture2D Texture { get; set; } + /// Depth map for the lighting system, loaded from + "_depth". + public Texture2D DepthTexture { get; set; } + /// Albedo color map for the lighting system, loaded from + "_albedo". + public Texture2D AlbedoTexture { get; set; } + + /// Preloads textures when the dialog queue is first created, resolving SpriteID through the factory. + public override ConstructType ConstructType { get; set; } = ConstructType.OnStartQueue; + /// Background is set imperatively — the dialog continues immediately. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Resolves plus its _depth/_albedo variants through the background factory. + public override void DoConstruct() + { + base.DoConstruct(); + if (Texture == null && !string.IsNullOrEmpty(SpriteID)) + Texture = BackgroundFactory.Instance.Get(SpriteID); + if (!string.IsNullOrEmpty(SpriteID)) + { + DepthTexture = BackgroundFactory.Instance.Get(SpriteID + "_depth"); + AlbedoTexture = BackgroundFactory.Instance.Get(SpriteID + "_albedo"); + } + } + + /// + /// Applies the background to the ViewBox, kills any in-flight background + /// transition and feeds the depth/albedo maps to the light controller. + /// Consumed textures are nulled so a reused action cannot re-apply stale data. + /// + public override void DoRun(DialogRuntime runtime) + { + if (Texture == null) + return; + + SceneContextNode.LastBackgroundID = SpriteID; + + var viewBox = Scene.Find("ViewBox"); + viewBox?.KillTransitionTween(); + viewBox?.SetBackgroundTexture(Texture); + + var controller = UiLightController.Instance; + controller?.SetupDepthMap(DepthTexture); + controller?.SetupAlbedoMap(AlbedoTexture); + + Texture = null; + DepthTexture = null; + AlbedoTexture = null; + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionBackgroundColor.cs b/src/UI/Dialog/Action/Impls/ActionBackgroundColor.cs new file mode 100644 index 0000000..559c9db --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionBackgroundColor.cs @@ -0,0 +1,33 @@ +using Cthangover.Mods.Dialog.Action; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Sets the background tint color. DoRun is currently a no-op — the Color + /// property is stored for external systems to read. Likely intended as a + /// declarative instruction consumed by a background-rendering node rather + /// than applied imperatively. + /// + public class ActionBackgroundColor : ActionCommand + { + /// Target tint color. Stored declaratively — consumed by background-rendering nodes rather than applied imperatively here. + public Color Color { get; set; } = Colors.Black; + + /// Imperative — the dialog continues immediately after this action is processed. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Deliberate no-op: the tint is declarative data for background-rendering nodes, not applied here. + public override void DoRun(DialogRuntime runtime) + { + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionBackgroundShowHide.cs b/src/UI/Dialog/Action/Impls/ActionBackgroundShowHide.cs new file mode 100644 index 0000000..99a46ca --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionBackgroundShowHide.cs @@ -0,0 +1,66 @@ +using Cthangover.Mods.Dialog.Action; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Tween-driven background fade in/out. Animates the modulate alpha of the + /// dialog box's parent Control over Duration seconds. Creates and manages + /// its own Tween internally, killing any previous tween before starting a + /// new one to prevent overlap. Can optionally set WaitType to WaitTime with + /// a matching duration so the dialog pauses during the transition. + /// + public class ActionBackgroundShowHide : ActionCommand + { + /// Whether to fade the background in (Show) or out (Hide). + public BackgroundActionType ActionType { get; set; } = BackgroundActionType.Show; + /// Duration of the fade tween in seconds. + public float Duration { get; set; } = 1f; + + private Tween activeTween; + + /// Default NoWait — overridden by the DialogQueue builder if the wait flag is set, changing to WaitTime to pause during transition. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Starts an alpha tween of the dialog box parent's modulate:a towards fully visible (Show) or fully transparent (Hide). + public override void DoRun(DialogRuntime runtime) + { + var dialogBox = runtime.DialogBox; + if (dialogBox == null) + return; + + var targetAlpha = ActionType == BackgroundActionType.Show ? 1f : 0f; + + if (dialogBox.GetParent() is Control backgroundNode) + { + StopTween(); + activeTween = backgroundNode.CreateTween(); + activeTween.TweenProperty(backgroundNode, "modulate:a", targetAlpha, Duration); + } + } + + /// Kills the fade tween so a destructed action cannot keep animating. + public override void DoDestruct() + { + base.DoDestruct(); + StopTween(); + } + + private void StopTween() + { + if (activeTween != null) + { + activeTween.Kill(); + activeTween = null; + } + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionDelay.cs b/src/UI/Dialog/Action/Impls/ActionDelay.cs new file mode 100644 index 0000000..5e4018c --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionDelay.cs @@ -0,0 +1,56 @@ +using Cthangover.Mods.Dialog.Action; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Waits for a fixed duration. When HiddenMode is true, hides the dialog + /// body for the duration (for dramatic pauses or cutscene beats). ShowText, + /// if set, displays a temporary message during the wait. DoDestruct is a + /// no-op because body visibility is implicitly restored on next Show. + /// + public class ActionDelay : ActionCommand + { + /// When true, hides the dialog body for the duration of the wait (dramatic pauses, cutscene beats). + public bool HiddenMode { get; set; } + /// Optional temporary text displayed while waiting. Set to null to keep the current dialog text. + public string ShowText { get; set; } + + /// Pauses the dialog until elapses. + public override WaitType WaitType { get; set; } = WaitType.WaitTime; + /// Duration of the pause in seconds. Default 1.0. + public override float WaitTime { get; set; } = 1f; + + /// Shows the optional and, in , hides the dialog body for the pause duration. + public override void DoRun(DialogRuntime runtime) + { + var dialogBox = runtime.DialogBox; + if (dialogBox == null) + return; + + if (ShowText != null) + dialogBox.SetText(ShowText); + + if (HiddenMode) + dialogBox.Body.SetDeferred("visible", false); + } + + /// No-op: the dialog body hidden by is restored implicitly by the next show. + public override void DoDestruct() + { + base.DoDestruct(); + if (HiddenMode) + { + // Body restored on next Show + } + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionEffect.cs b/src/UI/Dialog/Action/Impls/ActionEffect.cs new file mode 100644 index 0000000..06aac5a --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionEffect.cs @@ -0,0 +1,67 @@ +using Cthangover.Mods.Effects; +using Cthangover.Core.Utils; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Spawns a visual effect from the EffectFactory by ID. The spawned node + /// persists until the action is destructed. Uses DestructType.OnDelayed + /// because the effect must outlive the action's run — the effect node's own + /// animation or lifetime controls when it disappears. DoRun is a no-op + /// because construction handles spawning. + /// + public class ActionEffect : ActionCommand + { + /// Identifier resolved through to a PackedScene. The spawned node persists until destructed. + public string EffectID { get; set; } + + private Node spawnedEffect; + + /// The dialog continues immediately — the spawned effect plays independently. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + /// OnDelayed ensures the effect node outlives the action's run phase. Destruct is called manually when the effect finishes. + public override DestructType DestructType { get; set; } = DestructType.OnDelayed; + + /// Resolves through and instantiates the effect scene. + public override void DoConstruct() + { + base.DoConstruct(); + if (string.IsNullOrEmpty(EffectID)) + return; + + var packedScene = EffectFactory.Instance.Get(EffectID); + if (packedScene == null) + { + GameLogger.Log("DIALOG", $"ActionEffect: effect '{EffectID}' not found", LogLevel.Error); + return; + } + + spawnedEffect = packedScene.Instantiate(); + } + + /// No-op: spawning happens during construction so the effect is ready before the queue reaches this point. + public override void DoRun(DialogRuntime runtime) + { + } + + /// Frees the spawned effect node; paired with the OnDelayed destruct so effects outlive the action's run. + public override void DoDestruct() + { + base.DoDestruct(); + if (spawnedEffect != null) + { + spawnedEffect.QueueFree(); + spawnedEffect = null; + } + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionEmpty.cs b/src/UI/Dialog/Action/Impls/ActionEmpty.cs new file mode 100644 index 0000000..e98c917 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionEmpty.cs @@ -0,0 +1,29 @@ +using Cthangover.Mods.Dialog.Action; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// No-op placeholder action. Used as a labeled jump target (Point) for GoTo + /// commands — provides an addressable marker in the dialog sequence without + /// any visible effect. NoWait so the dialog skips through labels instantly. + /// + public class ActionEmpty : ActionCommand + { + /// Label actions skip immediately so the dialog doesn't pause on markers. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// No-op: the action exists only to carry the jump-target ID. + public override void DoRun(DialogRuntime runtime) + { + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionEnd.cs b/src/UI/Dialog/Action/Impls/ActionEnd.cs new file mode 100644 index 0000000..dfc1f08 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionEnd.cs @@ -0,0 +1,30 @@ +using Cthangover.Mods.Dialog.Action; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Terminates the dialog queue immediately. Calls Runtime.End() which + /// destructs all remaining actions, processes the cleanup queue, hides + /// the dialog box, and fires the dialog-end event. + /// + public class ActionEnd : ActionCommand + { + /// Ends the dialog queue immediately — no pause, no return. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Terminates the queue via — destructs remainders, runs cleanup, hides the box, fires the end event. + public override void DoRun(DialogRuntime runtime) + { + runtime.End(); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionForeground.cs b/src/UI/Dialog/Action/Impls/ActionForeground.cs new file mode 100644 index 0000000..5c13896 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionForeground.cs @@ -0,0 +1,49 @@ +using Cthangover.Core.Factories.Impls; +using Cthangover.Mods.Interface.View; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Sets the scene foreground texture on the ViewBox. Resolves SpriteID through + /// BackgroundFactory on construction (OnStartQueue) so the texture is loaded + /// before the dialog starts. Sets the texture to null after applying it — + /// ActionCommand instances may be reused by the runtime, and nulling prevents + /// double-applying stale references. + /// + public class ActionForeground : ActionCommand + { + /// Sprite resource ID resolved through on construction. + public string SpriteID { get; set; } + /// Direct texture override. Bypasses factory resolution. Nulled after application to prevent stale reuse across dialog cycles. + public Texture2D Texture { get; set; } + + /// Preloads the texture when the dialog queue is first created. + public override ConstructType ConstructType { get; set; } = ConstructType.OnStartQueue; + /// Foreground is set imperatively — the dialog continues immediately. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Resolves through the background factory unless a direct texture override is set. + public override void DoConstruct() + { + base.DoConstruct(); + if (Texture == null && !string.IsNullOrEmpty(SpriteID)) + Texture = BackgroundFactory.Instance.Get(SpriteID); + } + + /// Applies the preloaded texture to the ViewBox foreground, then nulls it so a reused action cannot re-apply stale data. + public override void DoRun(DialogRuntime runtime) + { + var viewBox = Scene.Find("ViewBox"); + viewBox?.SetForegroundTexture(Texture); + Texture = null; + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionGoTo.cs b/src/UI/Dialog/Action/Impls/ActionGoTo.cs new file mode 100644 index 0000000..24ac163 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionGoTo.cs @@ -0,0 +1,33 @@ +using Cthangover.Mods.Dialog.Action; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Unconditional jump to a labeled action in the dialog queue. NoWait so + /// the jump happens transparently. The target is resolved by action ID + /// (set via ActionCommand.SetID or the Point()/Empty() fluent methods). + /// + public class ActionGoTo : ActionCommand + { + /// Target action ID to jump to. Resolved by linear search through the dialog queue. + public string GoTo { get; set; } + + /// Jump happens transparently — the dialog continues from the target without pausing. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Jumps the queue index to the action whose ID matches . + public override void DoRun(DialogRuntime runtime) + { + runtime.TryGoTo(GoTo); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionHideDialog.cs b/src/UI/Dialog/Action/Impls/ActionHideDialog.cs new file mode 100644 index 0000000..9c86ae9 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionHideDialog.cs @@ -0,0 +1,24 @@ + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Hides the dialog box. Used in cutscenes where UI chrome should be + /// temporarily hidden while animations or background transitions play. + /// Paired with ActionShowDialog to restore visibility. + /// + public class ActionHideDialog : ActionCommand + { + /// Hides the dialog immediately — the queue continues without pause. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Hides the dialog box chrome while keeping the queue running. + public override void DoRun(DialogRuntime runtime) + { + var dialogBox = runtime.DialogBox; + if (dialogBox == null) + return; + + dialogBox.Visible = false; + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionIf.cs b/src/UI/Dialog/Action/Impls/ActionIf.cs new file mode 100644 index 0000000..e011ddf --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionIf.cs @@ -0,0 +1,50 @@ +using System; +using Cthangover.Core.Utils; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Conditional branch: evaluates a Func<bool> and jumps to TrueGoTo or + /// FalseGoTo based on the result. If the chosen target is null/empty, falls + /// through to Next(). NoWait so branching is transparent to the player. + /// + public class ActionIf : ActionCommand + { + /// Delegate evaluated at runtime. Must return true/false to select the branch target. + public Func Condition { get; set; } + /// Target action ID if returns true. Empty or null causes fall-through. + public string TrueGoTo { get; set; } + /// Target action ID if returns false. Empty or null causes fall-through. + public string FalseGoTo { get; set; } + + /// Branch evaluation is transparent — the dialog jumps to the target without user input. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Evaluates and jumps to or ; an empty target falls through to the next action. + public override void DoRun(DialogRuntime runtime) + { + if (Condition == null) + { + GameLogger.Log("DIALOG", "ActionIf: Condition delegate is null", LogLevel.Error); + runtime.Next(); + return; + } + + var result = Condition(); + var goTo = result ? TrueGoTo : FalseGoTo; + + if (!string.IsNullOrEmpty(goTo)) + runtime.TryGoTo(goTo); + else + runtime.Next(); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionInteractiveAdd.cs b/src/UI/Dialog/Action/Impls/ActionInteractiveAdd.cs new file mode 100644 index 0000000..1e462a8 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionInteractiveAdd.cs @@ -0,0 +1,44 @@ +using Cthangover.Core.Interactive; +using Cthangover.Core.Utils; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Dialog action that instantiates an interactive object from its definition + /// and places it on the scene. Executes immediately (NoWait). + /// + public class ActionInteractiveAdd : ActionCommand + { + /// The interactive definition ID to load and instantiate. + public string DefinitionId { get; set; } + + /// Placement happens immediately — the dialog does not pause. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Instantiates the interactive object named by through . + public override void DoRun(DialogRuntime runtime) + { + if (string.IsNullOrEmpty(DefinitionId)) + { + GameLogger.Log("INTERACTIVE", "ActionInteractiveAdd: DefinitionId is empty", LogLevel.Error); + return; + } + + var mgr = InteractiveManager.Instance; + if (mgr == null) + { + GameLogger.Log("INTERACTIVE", "ActionInteractiveAdd: InteractiveManager not initialized yet", LogLevel.Error); + return; + } + + mgr.Add(DefinitionId); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionInteractiveClear.cs b/src/UI/Dialog/Action/Impls/ActionInteractiveClear.cs new file mode 100644 index 0000000..2bd76d6 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionInteractiveClear.cs @@ -0,0 +1,29 @@ +using Cthangover.Core.Interactive; +using Cthangover.Core.Utils; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Dialog action that removes and destroys all active interactive objects. + /// Typically used on scene exit to ensure clean state. + /// + public class ActionInteractiveClear : ActionCommand + { + /// Clearing happens immediately — the dialog does not pause. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Destroys every active interactive object via . + public override void DoRun(DialogRuntime runtime) + { + GameLogger.Log("INTERACTIVE", "ActionInteractiveClear: clearing all interactives"); + InteractiveManager.Instance?.ClearAll(); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionInteractiveRemove.cs b/src/UI/Dialog/Action/Impls/ActionInteractiveRemove.cs new file mode 100644 index 0000000..f21daa7 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionInteractiveRemove.cs @@ -0,0 +1,36 @@ +using Cthangover.Core.Interactive; +using Cthangover.Core.Utils; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Dialog action that removes and destroys an interactive object by its definition ID. + /// + public class ActionInteractiveRemove : ActionCommand + { + /// The definition ID of the interactive object to remove. + public string DefinitionId { get; set; } + + /// Removal happens immediately — the dialog does not pause. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Destroys the interactive object named by . + public override void DoRun(DialogRuntime runtime) + { + if (string.IsNullOrEmpty(DefinitionId)) + { + GameLogger.Log("INTERACTIVE", "ActionInteractiveRemove: DefinitionId is empty", LogLevel.Error); + return; + } + + InteractiveManager.Instance?.Remove(DefinitionId); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionInteractiveSet.cs b/src/UI/Dialog/Action/Impls/ActionInteractiveSet.cs new file mode 100644 index 0000000..1a7361d --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionInteractiveSet.cs @@ -0,0 +1,64 @@ +using Cthangover.Core.Interactive; +using Cthangover.Core.Utils; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Dialog action that modifies a property of an active interactive object. + /// Supported properties: enabled (bool), visible (bool). + /// + public class ActionInteractiveSet : ActionCommand + { + /// The definition ID of the interactive object to modify. + public string DefinitionId { get; set; } + + /// Property name: "enabled" or "visible". + public string Property { get; set; } + + /// String value to parse: "true" or "false". + public string Value { get; set; } + + /// Property changes happen immediately — the dialog does not pause. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Parses as a bool and applies it to the enabled or visible property of the object named by . + public override void DoRun(DialogRuntime runtime) + { + if (string.IsNullOrEmpty(DefinitionId) || string.IsNullOrEmpty(Property) || string.IsNullOrEmpty(Value)) + { + GameLogger.Log("INTERACTIVE", "ActionInteractiveSet: missing parameters", LogLevel.Error); + return; + } + + var mgr = InteractiveManager.Instance; + if (mgr == null) + return; + + if (!bool.TryParse(Value, out var boolVal)) + { + GameLogger.Log("INTERACTIVE", $"ActionInteractiveSet: invalid bool value '{Value}'", LogLevel.Error); + return; + } + + switch (Property.ToLowerInvariant()) + { + case "enabled": + mgr.SetEnabled(DefinitionId, boolVal); + break; + case "visible": + mgr.SetVisible(DefinitionId, boolVal); + break; + default: + GameLogger.Log("INTERACTIVE", $"ActionInteractiveSet: unknown property '{Property}'", LogLevel.Error); + break; + } + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionLightSet.cs b/src/UI/Dialog/Action/Impls/ActionLightSet.cs new file mode 100644 index 0000000..4f34a6a --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionLightSet.cs @@ -0,0 +1,40 @@ +using Cthangover.Mods.Time; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Applies static lights from a JSON string to the UiLightController. + /// An empty or null string clears all static lights. The JSON is deserialized + /// into LightDef objects with viewport-relative coordinates, which are then + /// converted to pixel positions. Supports up to 10 static lights plus the + /// player's dynamic lamp. + /// + public class ActionLightSet : ActionCommand + { + /// JSON string defining static light positions and properties. Empty or null clears all static lights. + public string LightsJson { get; set; } + + /// Lights are set imperatively — the dialog continues immediately. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Clears static lights when is empty, otherwise parses and applies the light definitions to the scene's . + public override void DoRun(DialogRuntime runtime) + { + var controller = Scene.Find("Lights"); + if (controller == null) + return; + + if (string.IsNullOrEmpty(LightsJson)) + controller.ClearStaticLights(); + else + controller.SetStaticLights(LightsJson); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionLightUseTime.cs b/src/UI/Dialog/Action/Impls/ActionLightUseTime.cs new file mode 100644 index 0000000..31f8cc7 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionLightUseTime.cs @@ -0,0 +1,33 @@ +using Cthangover.Mods.Dialog.Action; +using Cthangover.Mods.Time; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Toggles the light controller's time-of-day mode. When UseTime is false, + /// static lighting is used; when true, lighting responds to the in-game clock. + /// + public class ActionLightUseTime : ActionCommand + { + /// True = lights respond to in-game time of day; false = static lighting mode. + public bool UseTime { get; set; } = true; + + /// Time mode toggle is instant — the dialog continues without pause. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Switches the scene's between time-of-day lighting and static lights. + public override void DoRun(DialogRuntime runtime) + { + var controller = Scene.Find("Lights"); + if (controller != null) + controller.IsUseLight = UseTime; + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionMusic.cs b/src/UI/Dialog/Action/Impls/ActionMusic.cs new file mode 100644 index 0000000..4ada36d --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionMusic.cs @@ -0,0 +1,37 @@ +using Cthangover.Core.Audio; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Starts background music playback via IAudioService. Uses MusicType.Ambient + /// so the music layer is treated as atmospheric background rather than UI + /// feedback. If the same music track is already playing, the audio service handles + /// deduplication. + /// + public class ActionMusic : ActionCommand + { + /// Music track identifier resolved through the audio system. Empty strings are silently ignored. + public string Music { get; set; } + + /// Music starts playing in the background — the dialog continues immediately. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Starts the named track as ambient background music through ; empty names are ignored. + public override void DoRun(DialogRuntime runtime) + { + if (string.IsNullOrEmpty(Music)) + return; + + var audioService = Scene.Find("AudioService") as IAudioService; + audioService?.PlayMusic(Music, MusicType.Ambient); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionMusicPause.cs b/src/UI/Dialog/Action/Impls/ActionMusicPause.cs new file mode 100644 index 0000000..2b75a55 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionMusicPause.cs @@ -0,0 +1,29 @@ +using Cthangover.Core.Audio; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Pauses background music playback via IAudioService without stopping the + /// track. Can be resumed with ActionMusicPlay. + /// + public class ActionMusicPause : ActionCommand + { + /// Pause is instant — the dialog continues without waiting for audio acknowledgment. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Pauses the current music track without stopping it, so can resume it. + public override void DoRun(DialogRuntime runtime) + { + var audioService = Scene.Find("AudioService") as IAudioService; + audioService?.PauseMusic(true); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionMusicPlay.cs b/src/UI/Dialog/Action/Impls/ActionMusicPlay.cs new file mode 100644 index 0000000..acbbc02 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionMusicPlay.cs @@ -0,0 +1,28 @@ +using Cthangover.Core.Audio; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Resumes paused background music. Paired with ActionMusicPause. + /// + public class ActionMusicPlay : ActionCommand + { + /// Resume is instant — the dialog continues without waiting for audio. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Resumes music paused by . + public override void DoRun(DialogRuntime runtime) + { + var audioService = Scene.Find("AudioService") as IAudioService; + audioService?.PauseMusic(false); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionRun.cs b/src/UI/Dialog/Action/Impls/ActionRun.cs new file mode 100644 index 0000000..681a9aa --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionRun.cs @@ -0,0 +1,39 @@ +using Cthangover.Core.Utils; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Executes an arbitrary Action delegate inside the dialog queue. + /// Allows imperative code injection (e.g. quest state changes, UI toggles) + /// without creating a dedicated action class. NoWait so the queue advances + /// immediately after the delegate returns. + /// + public class ActionRun : ActionCommand + { + /// Arbitrary delegate executed during dialog sequencing. Null delegates are logged and skipped. + public System.Action Executable { get; set; } + + /// The delegate runs and the dialog advances — no pausing. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Invokes ; a null delegate is logged and skipped so the queue keeps flowing. + public override void DoRun(DialogRuntime runtime) + { + if (Executable == null) + { + GameLogger.Log("DIALOG", "ActionRun: Executable delegate is null", LogLevel.Error); + return; + } + + Executable(); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionScenario.cs b/src/UI/Dialog/Action/Impls/ActionScenario.cs new file mode 100644 index 0000000..4cffeab --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionScenario.cs @@ -0,0 +1,77 @@ +using Cthangover.Core.Actions; +using Cthangover.Core.Factories.Impls; +using Cthangover.Core.Utils; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Bridges the dialog system to the broader scenario action system + /// (ScenarioActionFactory). Resolves an action by name and runs it with a + /// ScenarioActionContext wrapping the dialog runtime. Wraps execution in + /// try/catch so a faulty scenario action doesn't crash the dialog queue. + /// Every error log carries the owning scenario identity (see + /// ): the runtime cannot report the source + /// file and line — the parser drops them — but it can name the scenario + /// definition, which together with the validator's file:line report + /// pinpoints the broken command. + /// + public class ActionScenario : ActionCommand + { + /// Scenario action name resolved through . Empty names are logged and skipped. + public string ActionName { get; set; } + + /// The scenario action runs and the dialog advances — no synchronization. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Resolves through and runs it with a dialog-backed context; exceptions are logged, never propagated. + public override void DoRun(DialogRuntime runtime) + { + if (string.IsNullOrEmpty(ActionName)) + { + GameLogger.Log("DIALOG", "ActionScenario: ActionName is null or empty" + DescribeScenario(runtime), LogLevel.Error); + return; + } + + var action = ScenarioActionFactory.Instance.Get(ActionName); + if (action == null) + { + GameLogger.Log("DIALOG", $"ActionScenario: scenario action '{ActionName}' is not registered — the action is skipped, check the spelling and that the defining mod is loaded" + DescribeScenario(runtime), LogLevel.Error); + return; + } + + var ctx = new ScenarioActionContext(runtime); + try + { + action.Run(ctx); + } + catch (System.Exception ex) + { + GameLogger.Log("DIALOG", $"ActionScenario: {ex.Message}\n{ex.StackTrace}" + DescribeScenario(runtime), LogLevel.Error); + } + } + + /// + /// Formats the scenario suffix for error logs. The locker that started + /// the dialog is the event owning the scenario; its ID is the scenario + /// definition name (ScenarioId) or, for code-driven lockers, the event + /// type name. Returns a human-readable suffix either way, so a missing + /// action in a shipped game can be traced to its scenario from + /// user://logs/cthangover.log alone. + /// + private static string DescribeScenario(DialogRuntime runtime) + { + var id = runtime?.DialogBox?.Locker?.ID; + return string.IsNullOrEmpty(id) + ? " (scenario unknown)" + : $" (scenario '{id}')"; + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionSelect.cs b/src/UI/Dialog/Action/Impls/ActionSelect.cs new file mode 100644 index 0000000..a9b4c92 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionSelect.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Cthangover.Mods.Dialog.Action; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Presents player choices by spawning answer variants in the DialogBox. + /// WaitType is WaitSelect so the dialog pauses until the player picks an + /// option (which triggers DialogBox.SelectVariant → Runtime.TryGoTo). + /// Each variant's text is processed through variable substitution before + /// display. Also sets an accompanying text message to contextualize the + /// choice (e.g. "What will you do?"). + /// + public class ActionSelect : ActionCommand + { + /// The list of selectable choice variants. Each carries display text and a GoTo target. + public List Variants { get; set; } + /// Context text shown alongside the choices (e.g. "What will you do?"). Supports variable substitution. + public string Text { get; set; } + /// Left avatar sprite ID shown during the choice. Null hides the slot. + public string FirstAvatar { get; set; } + /// Right avatar sprite ID shown during the choice. Null hides the slot. + public string SecondAvatar { get; set; } + + /// Dialog pauses until the player selects a variant — then the runtime jumps to the chosen GoTo target. + 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. + public override void DoRun(DialogRuntime runtime) + { + var dialogBox = runtime.DialogBox; + if (dialogBox == null) + return; + + foreach (var variant in Variants) + variant.Text = runtime.ProcessText(variant.Text); + + dialogBox.SetVariants(Variants); + dialogBox.SetText(runtime.ProcessText(Text)); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionSet.cs b/src/UI/Dialog/Action/Impls/ActionSet.cs new file mode 100644 index 0000000..ed0a5c6 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionSet.cs @@ -0,0 +1,39 @@ +using System; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Sets a dialog runtime variable (Name=Value) for use by subsequent actions + /// via the ${var} substitution syntax. The optional Callback Func<string> + /// allows lazy evaluation — if set, Value is ignored and Callback is invoked + /// at runtime. This enables dynamic values (e.g. quest state, character names) + /// that can't be known when the dialog script is authored. + /// + public class ActionSet : ActionCommand + { + /// Variable name. Referenced by subsequent actions via ${name} syntax. + public string Name { get; set; } + /// Literal value to store. Ignored if is set. + public string Value { get; set; } + /// Lazy evaluation delegate. When non-null, invoked at runtime to compute the value dynamically. + public Func Callback { get; set; } + + /// Variable assignment is instant — the dialog continues without pause. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Stores — or the result of when set — as the runtime variable . + public override void DoRun(DialogRuntime runtime) + { + var val = Callback != null ? Callback() : Value; + runtime.SetVariable(Name, val ?? string.Empty); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionShowDialog.cs b/src/UI/Dialog/Action/Impls/ActionShowDialog.cs new file mode 100644 index 0000000..4c9f503 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionShowDialog.cs @@ -0,0 +1,23 @@ + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Makes the dialog box visible. Typically paired with ActionHideDialog + /// for cutscenes that temporarily hide the dialog UI while animations play. + /// + public class ActionShowDialog : ActionCommand + { + /// Dialog visibility change is instant — the queue continues without pause. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Makes the dialog box chrome visible again after . + public override void DoRun(DialogRuntime runtime) + { + var dialogBox = runtime.DialogBox; + if (dialogBox == null) + return; + + dialogBox.Visible = true; + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionSound.cs b/src/UI/Dialog/Action/Impls/ActionSound.cs new file mode 100644 index 0000000..4b9e04d --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionSound.cs @@ -0,0 +1,36 @@ +using Cthangover.Core.Audio; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Plays a sound through the audio service using the UI sound channel (SoundType.UI). + /// NoWait so the sound plays and the dialog continues immediately — no + /// synchronization with audio completion. + /// + public class ActionSound : ActionCommand + { + /// Sound effect identifier resolved through . Played on the UI channel. + public string Sound { get; set; } + + /// Sound is fire-and-forget — the dialog continues immediately without synchronization. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Plays on the UI sound channel through ; empty names are ignored. + public override void DoRun(DialogRuntime runtime) + { + if (string.IsNullOrEmpty(Sound)) + return; + + var audioService = Scene.Find("AudioService") as IAudioService; + audioService?.PlaySound(Sound, SoundType.UI); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionSwitchScene.cs b/src/UI/Dialog/Action/Impls/ActionSwitchScene.cs new file mode 100644 index 0000000..d652da3 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionSwitchScene.cs @@ -0,0 +1,66 @@ +using Cthangover.Core.Scenes; +using Cthangover.Core.Utils; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Deferred scene switch: calls SceneManager.SwitchScene via CallDeferred + /// to avoid mid-queue scene tree mutations. NoWait ensures the dialog ends + /// immediately — there's no return from a scene switch. + /// + /// Does NOT force the owning scenario into OneRun: re-runs are the scenario + /// author's decision (is_one_run metadata) plus per-event conditions. + /// Navigation menus (select + switch_scene) must survive into the next + /// visit, and forcing OneRun recorded them in CompletedScenarioIds — after + /// which FilterCompletedScenarios silently dropped the whole location menu + /// on re-entry. + /// + public class ActionSwitchScene : ActionCommand + { + /// Target scene name passed to . + public string SceneName { get; set; } + /// Whether to transition with the dialog hidden. Default true. + public bool HiddenMode { get; set; } = true; + /// Transition speed parameter passed to the scene manager. Default 4.0. + public float Speed { get; set; } = 4f; + + /// Scene switch is deferred — the dialog ends immediately with no return. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Marks the dialog locker as OneRun and defers SceneManager.SwitchScene so the tree is not mutated mid-queue. + public override void DoRun(DialogRuntime runtime) + { + if (string.IsNullOrEmpty(SceneName)) + { + GameLogger.Log("DIALOG", "ActionSwitchScene: SceneName is null or empty", LogLevel.Error); + return; + } + + // No forced IsOneRun here: location navigation scenarios are meant to + // re-run on every visit. Forcing it made DialogRuntime record the + // scenario as completed, and the next visit to the same scene started + // without its menu. One-time story scenes declare is_one_run: true + // (or a condition:) in their metadata instead. + + GameLogger.Log("DIALOG", $"ActionSwitchScene: deferring switch to '{SceneName}'"); + + var sceneManager = Scene.Find("SceneManager"); + if (sceneManager != null) + { + Callable.From(() => sceneManager.SwitchScene(SceneName)).CallDeferred(); + } + else + { + GameLogger.Log("DIALOG", $"ActionSwitchScene: SceneManager not found for '{SceneName}'", LogLevel.Error); + } + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionText.cs b/src/UI/Dialog/Action/Impls/ActionText.cs new file mode 100644 index 0000000..8f52ba6 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionText.cs @@ -0,0 +1,49 @@ +using Cthangover.Core.Factories; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// The primary dialog text display action. Sets the text and both avatar slots + /// (left/right) on the DialogBox. WaitType is WaitClick so the dialog pauses + /// for player input before advancing. Avatars are resolved through + /// IAvatarProvider by string ID; a null/empty avatar hides that slot. When + /// UseProcessText is true, runtime variables (${var}) are substituted before display. + /// + public class ActionText : ActionCommand + { + /// The dialog text to display. Supports ${var} substitution when is true. + public string Text { get; set; } + /// Left avatar sprite ID resolved through . Null or empty hides the slot. + public string FirstAvatar { get; set; } + /// Right avatar sprite ID resolved through . Null or empty hides the slot. + public string SecondAvatar { get; set; } + /// When true, the avatar is rendered in shader-based silhouette/hidden-color mode. + public bool HideColor { get; set; } + /// When true, runtime variable substitution (${var}) is applied to before display. + public bool UseProcessText { get; set; } + + /// Pauses for player click/key input — the dialog waits for user acknowledgment. + public override WaitType WaitType { get; set; } = WaitType.WaitClick; + + /// Sets the dialog text (optionally variable-substituted) and both avatar slots, then waits for the player's click. + public override void DoRun(DialogRuntime runtime) + { + var dialogBox = runtime.DialogBox; + if (dialogBox == null) + return; + + var processedText = UseProcessText ? runtime.ProcessText(Text) : Text; + dialogBox.SetText(processedText); + dialogBox.SetFirstAvatar(AvatarProviderRegistry.Get(FirstAvatar), HideColor); + dialogBox.SetSecondAvatar(AvatarProviderRegistry.Get(SecondAvatar), HideColor); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionTimedUpdate.cs b/src/UI/Dialog/Action/Impls/ActionTimedUpdate.cs new file mode 100644 index 0000000..812632d --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionTimedUpdate.cs @@ -0,0 +1,92 @@ +using Cthangover.Core.UI.Event; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Timeline-based update action: registers itself as an IOnUpdateEvent + /// subscriber and fires OnUpdateCallback(float t) each frame with a normalized + /// progress value (0→1) over WaitTime seconds. WaitType is WaitEvent so the + /// dialog runtime pauses until elapsed >= WaitTime, at which point the action + /// advances the runtime directly (event-driven — no DialogBox poll) and + /// destructs itself. OnStartCallback/OnFinishCallback bracket the animation + /// for setup/teardown. Fixed 60fps step for deterministic progress regardless + /// of actual frame rate. + /// + public class ActionTimedUpdate : ActionCommand, IOnUpdateEvent + { + /// Update priority. Value 0 — runs before most other listeners. + public int Priority => 0; + + /// Fires once when is called, before the first update frame. + public System.Action OnStartCallback { get; set; } + /// Fires each frame during the active period with a normalized progress value t (0→1) over seconds. + public System.Action OnUpdateCallback { get; set; } + /// Fires once when the elapsed time exceeds or when destructed. + public System.Action OnFinishCallback { get; set; } + + /// Pauses the dialog until the elapsed time meets WaitTime — the action then advances the runtime itself. + public override WaitType WaitType { get; set; } = WaitType.WaitEvent; + /// Total duration in seconds over which receives normalized progress. + public override float WaitTime { get; set; } = 1f; + + private float elapsed; + private bool isRunning; + private DialogRuntime runtime; + + /// Registers this action as an listener on the scene's event controller. + public override void DoConstruct() + { + base.DoConstruct(); + var controller = Scene.Find("EventController"); + controller?.AddUpdateEventListener(this); + } + + /// Starts the timeline: resets elapsed time and fires before the first frame. + public override void DoRun(DialogRuntime runtime) + { + this.runtime = runtime; + elapsed = 0f; + isRunning = true; + OnStartCallback?.Invoke(); + } + + /// Detaches from the update controller, stops ticking and fires . + public override void DoDestruct() + { + base.DoDestruct(); + isRunning = false; + runtime = null; + var controller = Scene.Find("EventController"); + controller?.RemoveUpdateEventListener(this); + OnFinishCallback?.Invoke(); + } + + void IOnUpdateEvent.OnUpdate() + { + if (!isRunning) + return; + + elapsed += 1f / 60f; + + var t = WaitTime > 0f ? Mathf.Min(elapsed / WaitTime, 1f) : 1f; + OnUpdateCallback?.Invoke(t); + + if (elapsed >= WaitTime) + { + // Advances the runtime directly instead of relying on a DialogBox poll. + // Next() destructs this action (firing OnFinishCallback and detaching the + // update listener), then Run() continues the queue past the WaitEvent point. + isRunning = false; + runtime?.Next(); + runtime?.Run(); + } + } + } +} diff --git a/src/UI/Dialog/Action/Impls/ActionTitle.cs b/src/UI/Dialog/Action/Impls/ActionTitle.cs new file mode 100644 index 0000000..d0157f5 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/ActionTitle.cs @@ -0,0 +1,26 @@ + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Sets the dialog box title text. A null title hides the title bar. + /// NoWait — title updates are instant visual changes. + /// + public class ActionTitle : ActionCommand + { + /// Text to display in the title bar. Null hides the title bar. + public string TitleText { get; set; } + + /// Title changes are instant visual updates — the dialog continues immediately. + public override WaitType WaitType { get; set; } = WaitType.NoWait; + + /// Applies to the dialog title bar; null hides the bar. + public override void DoRun(DialogRuntime runtime) + { + var dialogBox = runtime.DialogBox; + if (dialogBox == null) + return; + + dialogBox.SetTitle(TitleText); + } + } +} diff --git a/src/UI/Dialog/Action/Impls/BackgroundActionType.cs b/src/UI/Dialog/Action/Impls/BackgroundActionType.cs new file mode 100644 index 0000000..b2c9519 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/BackgroundActionType.cs @@ -0,0 +1,13 @@ +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Discriminator for background show/hide transitions. + /// + public enum BackgroundActionType + { + /// Fade the dialog background visible (target alpha = 1.0). + Show, + /// Fade the dialog background transparent (target alpha = 0.0). + Hide + } +} diff --git a/src/UI/Dialog/Action/Impls/SelectVariant.cs b/src/UI/Dialog/Action/Impls/SelectVariant.cs new file mode 100644 index 0000000..e8769e9 --- /dev/null +++ b/src/UI/Dialog/Action/Impls/SelectVariant.cs @@ -0,0 +1,35 @@ +using System; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog.Action.Impls +{ + /// + /// Data model for a dialog choice option: display Text and a GoTo target + /// action ID. The static Point property returns a fresh GUID for runtime + /// goto targets. The New factory provides concise C# construction without + /// object-initializer syntax. + /// + public class SelectVariant + { + /// The text displayed on the choice button. Supports runtime variable substitution. + public string Text { get; set; } + /// Target action ID to jump to via when this variant is selected. + public string GoTo { get; set; } + + /// Generates a new unique action ID for use as a GoTo target (e.g. with ). + public static string Point => Guid.NewGuid().ToString(); + + /// Factory for concise C# construction without object-initializer syntax. + public static SelectVariant New(string text, string goTo) + { + return new SelectVariant { Text = text, GoTo = goTo }; + } + } +} diff --git a/src/UI/Dialog/Action/WaitType.cs b/src/UI/Dialog/Action/WaitType.cs new file mode 100644 index 0000000..37c920d --- /dev/null +++ b/src/UI/Dialog/Action/WaitType.cs @@ -0,0 +1,24 @@ +namespace Cthangover.Mods.Dialog.Action +{ + /// + /// Determines how the DialogRuntime advances past this action. NoWait chains + /// immediately to the next action (for imperative commands). WaitClick pauses + /// until user input (click/key). WaitSelect pauses for variant selection. + /// WaitTime pauses for a fixed duration. WaitEvent pauses until an external + /// event marks the action as destructed. + /// + public enum WaitType + { + /// Don't wait — the runtime advances to the next action immediately in the same Run call. + NoWait, + /// Pause until the player clicks or presses the accept key. Used by dialog text actions. + WaitClick, + /// Pause until the player selects a choice variant. Used by choice menus. + WaitSelect, + /// Pause for a fixed duration in seconds (). + WaitTime, + /// Pause until an external event marks the action as destructed (). + WaitEvent, + } + +} diff --git a/src/UI/Dialog/AnswerBox.cs b/src/UI/Dialog/AnswerBox.cs new file mode 100644 index 0000000..43d29c4 --- /dev/null +++ b/src/UI/Dialog/AnswerBox.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using Cthangover.Mods.Interface.Base.Lists.Impls; +using Cthangover.Mods.Dialog.Action.Impls; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog +{ + /// + /// Answer choice container: extends VerticalListWidget but deliberately + /// overrides PutToLayout to do nothing — the prefab-based layout system + /// handles positioning internally (answer_box.tscn: Content VBoxContainer + /// lays out the items as a container), and the base ColumnCellListWidget + /// layout would conflict. Content size is computed as count * 60px height, + /// creating a fixed-height per-option layout. Self-destructs on hide. + /// + public partial class AnswerBox : VerticalListWidget + { + + private ICollection variants; + + /// answer_box.tscn binds Content via the content export; the lookup covers instances built without the binding. + protected override void OnReady() + { + if (Content == null) + { + var found = FindChild("Content", true, false); + if (found != null) + Set("content", found); + } + } + + /// Stores the variants and triggers to build and display the answer list. + public void CreateVariantsUI(ICollection variants) + { + this.variants = variants; + Show(); + } + + protected override void HideDestruct() + { + base.HideDestruct(); + QueueFree(); + } + + /// Returns the stored variants collection. Called by the base pipeline. + public override ICollection CreateModels() + { + return variants; + } + + /// Deliberate no-op — answer items are positioned by their own layout system, not by the list widget's PutToLayout. + protected override void PutToLayout(AnswerItem item, int index, Control container, Vector2 contentSize) + { + } + + /// Fixed-height layout: content height = * 60px. Width is inherited from the Content container. + public override Vector2 GetContentSize(int count) + { + return new Vector2(Content.Size.X, count * 60); + } + + } + +} diff --git a/src/UI/Dialog/AnswerItem.cs b/src/UI/Dialog/AnswerItem.cs new file mode 100644 index 0000000..84be781 --- /dev/null +++ b/src/UI/Dialog/AnswerItem.cs @@ -0,0 +1,70 @@ +using Cthangover.Mods.Interface.Base.Lists; +using Cthangover.Mods.Dialog.Action.Impls; +using Godot; +using Cthangover.Core.UI; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Event; +using Cthangover.Core.UI.Executable; +using Cthangover.Core.Scenarios; +using Cthangover.Core.Scenes; +using Cthangover.Core.Interactive; + +namespace Cthangover.Mods.Dialog +{ + /// + /// Renders a single dialog choice option. Walks up the scene tree in OnReady + /// to find the nearest IDialogBox parent — this avoids coupling to a specific + /// dialog implementation and lets answer items work regardless of nesting depth. + /// On click, calls SelectVariant on the found dialog box, passing the bound + /// SelectVariant model (which carries the GoTo target and display text). + /// + public partial class AnswerItem : ListItem + { + + [Export] private Label textField; + + private DialogBox dialogBox; + + /// Binds the choice button from the answer_item scene prefab and finds the owning dialog box by walking up the tree, so items work at any nesting depth. + protected override void OnReady() + { + textField ??= GetNode