init mod
This commit is contained in:
commit
5de3fd7ad8
121 files changed
+6632
No files matched your search
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
internal class ScenarioActionContext : IActionContext
|
||||
{
|
||||
private readonly DialogRuntime runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public IQuestService Quests => QuestServiceRegistry.Provider;
|
||||
/// <summary>
|
||||
/// Service accessor for character party operations. Instantiated
|
||||
/// per-context. Routes to CharacterData via strict enum parsing.
|
||||
/// </summary>
|
||||
public ICharacterService Character { get; } = new CharacterServiceImpl();
|
||||
/// <summary>
|
||||
/// Service accessor for lighting control. Instantiated per-context.
|
||||
/// Thin wrapper around UiLightController singleton.
|
||||
/// </summary>
|
||||
public ILightingService Lighting { get; } = new LightingServiceImpl();
|
||||
/// <summary>
|
||||
/// Service accessor for runtime scene node manipulation.
|
||||
/// Instantiated per-context. All operations route through
|
||||
/// SceneContextNode.Instance as the scene root.
|
||||
/// </summary>
|
||||
public ISceneNodeService Scene { get; } = new SceneNodeServiceImpl();
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public IModRegistry ModRegistry { get; } = Cthangover.Core.Mods.ModRegistry.Instance;
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public IInventory Inventory { get; }
|
||||
/// <summary>
|
||||
/// Map zone control. Stateless adapter over the
|
||||
/// <c>MapZoneService</c> singleton, which owns the actual state;
|
||||
/// safe to instantiate per-context like the quest service, because the
|
||||
/// state does not live in the adapter.
|
||||
/// </summary>
|
||||
public IMapZoneService Map { get; } = new MapZoneServiceImpl();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public ScenarioActionContext(DialogRuntime runtime)
|
||||
{
|
||||
this.runtime = runtime;
|
||||
try { Inventory = GameData.Instance?.Runtime?.Inventory; }
|
||||
catch { Inventory = null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public string GetParam(string name) => runtime.GetVariable(name);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void Log(string category, string message) => GameLogger.Log(category, message);
|
||||
|
||||
/// <summary>
|
||||
/// Emits a warning-level log entry. Automatically prepends
|
||||
/// "[WARN]" to the message and logs at LogLevel.Warning.
|
||||
/// </summary>
|
||||
public void LogWarning(string category, string message) => GameLogger.Log(category, $"[WARN] {message}", LogLevel.Warning);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Engine-mod initializer for the dialog subsystem. On resources-ready it
|
||||
/// publishes the scenario event factory used by the core SceneManager and
|
||||
/// parks the <c>/root/DialogRunner</c> node implementing the core
|
||||
/// <c>IInteractiveDialogRunner</c> contract. All operations are idempotent:
|
||||
/// <c>ModManager.Reload()</c> re-runs <c>OnModResourcesReady</c>, so node
|
||||
/// creation must tolerate repeat invocations. Scenario command strategies
|
||||
/// of every mod reach their factory through
|
||||
/// <see cref="ScenarioCommandStrategyConsumer"/>, not through this initializer.
|
||||
/// </summary>
|
||||
public class DialogModInitializer : IModInitializer
|
||||
{
|
||||
private static bool initialized;
|
||||
|
||||
/// <summary>No per-mod setup is required; registration happens on resources-ready.</summary>
|
||||
public void OnModLoaded(string modId) { }
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the scenario event factory and queues the dialog runner
|
||||
/// node. Safe to call again after a mod reload.
|
||||
/// </summary>
|
||||
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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Dialog execution entry point of the dialog_base engine mod, parked at
|
||||
/// /root/DialogRunner. Implements the core <c>IInteractiveDialogRunner</c>
|
||||
/// 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
|
||||
/// <see cref="DialogBox"/>.
|
||||
/// </summary>
|
||||
public partial class DialogScriptRunner : ModNode, IInteractiveDialogRunner
|
||||
{
|
||||
/// <summary>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.</summary>
|
||||
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>("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;
|
||||
}
|
||||
|
||||
/// <summary>Runs inline DSL commands (hover effects and similar) through a standalone dialog runtime.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Registry of <see cref="IScenarioCommandStrategy"/> implementations,
|
||||
/// indexed by their <see cref="IScenarioCommandStrategy.Command"/> name.
|
||||
/// It discovers nothing itself: <see cref="ScenarioCommandStrategyConsumer"/>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public static class ScenarioCommandStrategyFactory
|
||||
{
|
||||
private static readonly Dictionary<string, IScenarioCommandStrategy> Strategies = new();
|
||||
|
||||
/// <summary>
|
||||
/// Looks up the <see cref="IScenarioCommandStrategy"/> registered for <paramref name="command"/>.
|
||||
/// Returns <c>null</c> if no strategy exists for the given command name.
|
||||
/// </summary>
|
||||
public static IScenarioCommandStrategy Get(string command)
|
||||
{
|
||||
Strategies.TryGetValue(command, out var strategy);
|
||||
return strategy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates all registered DSL command names (lowercase).
|
||||
/// </summary>
|
||||
public static IEnumerable<string> GetAllCommandNames()
|
||||
{
|
||||
return Strategies.Keys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="ICommandReferenceMetadata"/> for a command if its
|
||||
/// strategy implements that interface, or <c>null</c> otherwise. Used by the
|
||||
/// build pipeline to collect asset dependencies from scenario scripts.
|
||||
/// </summary>
|
||||
public static ICommandReferenceMetadata GetReferenceMetadata(string command)
|
||||
{
|
||||
if (Strategies.TryGetValue(command, out var strategy) && strategy is ICommandReferenceMetadata meta)
|
||||
return meta;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a strategy under its command name. The first strategy of a
|
||||
/// command wins: a mod cannot silently take over a built-in command.
|
||||
/// </summary>
|
||||
public static void Register(IScenarioCommandStrategy strategy)
|
||||
{
|
||||
if (strategy != null && !Strategies.ContainsKey(strategy.Command))
|
||||
Strategies[strategy.Command] = strategy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes <paramref name="strategy"/> (its mod is being unloaded). A
|
||||
/// command owned by another strategy instance is left alone.
|
||||
/// </summary>
|
||||
public static void Unregister(IScenarioCommandStrategy strategy)
|
||||
{
|
||||
if (strategy != null && Strategies.TryGetValue(strategy.Command, out var registered) && registered == strategy)
|
||||
Strategies.Remove(strategy.Command);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Cthangover.Core.UI.Executable;
|
||||
using Cthangover.Mods.Dialog.Executable;
|
||||
|
||||
namespace Cthangover.Mods.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds scenario-backed executable events for the core SceneManager.
|
||||
/// The returned <see cref="ScenarioEvent"/> is parented to the requesting
|
||||
/// event chain by <c>IExecutableEventChain.AddEvent</c>, so the factory
|
||||
/// only needs to configure it.
|
||||
/// </summary>
|
||||
public class ScenarioEventFactory : IScenarioEventFactory
|
||||
{
|
||||
/// <summary>Builds a one-shot event that executes the inline scenario text (test-play path).</summary>
|
||||
public IExecutableEvent CreateFromText(string scenarioText)
|
||||
{
|
||||
return new ScenarioEvent { ScenarioText = scenarioText };
|
||||
}
|
||||
|
||||
/// <summary>Builds an event from a scene event definition's scenario fields.</summary>
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace Cthangover.Mods.Dialog.Scenarios
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="ICommandReferenceMetadata"/> to
|
||||
/// discover which assets (backgrounds, scenes, music, etc.) are referenced
|
||||
/// and need to be packed into the final build.
|
||||
/// </summary>
|
||||
public enum PositionalReferenceKind
|
||||
{
|
||||
/// <summary>No resource reference in the first positional argument.</summary>
|
||||
None,
|
||||
/// <summary>A background image resource path.</summary>
|
||||
Background,
|
||||
/// <summary>A scene resource path (for scene switching).</summary>
|
||||
Scene,
|
||||
/// <summary>A music/audio resource path.</summary>
|
||||
Music,
|
||||
/// <summary>A sound effect resource path.</summary>
|
||||
Sound,
|
||||
/// <summary>An effect/shader resource path.</summary>
|
||||
Effect,
|
||||
/// <summary>An interactive action definition ID.</summary>
|
||||
Action
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface ICommandReferenceMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The kind of resource referenced by the first positional argument of this command.
|
||||
/// </summary>
|
||||
PositionalReferenceKind Positional0Kind { get; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional declarative contract of a scenario command, implemented by
|
||||
/// <see cref="IScenarioCommandStrategy"/> 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.
|
||||
/// </summary>
|
||||
public interface IScenarioCommandSpec
|
||||
{
|
||||
/// <summary>Minimum number of positional arguments. Default: 0.</summary>
|
||||
int MinPositional => 0;
|
||||
|
||||
/// <summary>Maximum number of positional arguments; -1 means unlimited. Default: -1.</summary>
|
||||
int MaxPositional => -1;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>key=</c> lookup.
|
||||
/// </summary>
|
||||
int MinArguments => 0;
|
||||
|
||||
/// <summary>Named arguments that must be present. Default: none.</summary>
|
||||
IReadOnlyList<string> RequiredNamed => Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Named arguments the command understands. Empty/null means the command
|
||||
/// accepts arbitrary named arguments (e.g. <c>action</c> forwards them as
|
||||
/// dialog variables), so unknown-name checks are skipped.
|
||||
/// </summary>
|
||||
IReadOnlyList<string> AllowedNamed => null;
|
||||
|
||||
/// <summary>
|
||||
/// Named argument that carries a <c>:label</c> jump target, or null.
|
||||
/// Lets the checker validate that the target exists without knowing the
|
||||
/// command; <c>goto</c> declares <c>"target"</c> here.
|
||||
/// </summary>
|
||||
string JumpTargetNamedKey => null;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Strategy interface for handling a single DSL command type in scenario scripts.
|
||||
/// Each command word (e.g. <c>text</c>, <c>music</c>, <c>goto</c>) maps to one
|
||||
/// strategy implementation. The <see cref="ScenarioCommandStrategyFactory"/>
|
||||
/// auto-discovers all implementations via reflection and dispatches commands
|
||||
/// to the matching strategy during parsing.
|
||||
/// </summary>
|
||||
public interface IScenarioCommandStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// The lowercase DSL command word this strategy handles (e.g. <c>"text"</c>, <c>"music"</c>).
|
||||
/// </summary>
|
||||
string Command { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command by appending one or more dialog actions to <paramref name="dlg"/>.
|
||||
/// </summary>
|
||||
/// <param name="dlg">The dialog queue to push actions onto.</param>
|
||||
/// <param name="positional">Unnamed tokens following the command word in order.</param>
|
||||
/// <param name="named">Key-value pairs parsed from <c>name=value</c> tokens (keys are lowercased).</param>
|
||||
/// <param name="arrowTarget">The target label following a <c>-></c> arrow token, if present.</param>
|
||||
/// <param name="locale">Optional localization provider for resolving <c>key=</c> lookups.</param>
|
||||
void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>action</c> DSL command. Triggers a named interactive action
|
||||
/// (defined in the action system) and forwards any extra named parameters as
|
||||
/// context variables via <see cref="DialogQueue.Set"/>.
|
||||
/// Implements <see cref="ICommandReferenceMetadata"/> to signal that the
|
||||
/// first positional argument is an <see cref="PositionalReferenceKind.Action"/> reference.
|
||||
/// </summary>
|
||||
public class ActionCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "action";
|
||||
/// <inheritdoc/>
|
||||
public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Action;
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>animation</c> DSL command. Supports two modes:
|
||||
/// (a) multiple sprites via <c>sprites=</c> named parameter, or
|
||||
/// (b) a single sprite base with <c>start</c>, <c>count</c>, <c>speed</c>, and <c>loop</c> controls.
|
||||
/// Delegates to <see cref="DialogQueue.Animation"/>.
|
||||
/// </summary>
|
||||
public class AnimationCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "animation";
|
||||
/// <inheritdoc/>
|
||||
public int MinArguments => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "sprites", "speed", "loop", "start", "count" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
if (named.TryGetValue("sprites", out var spritesStr))
|
||||
{
|
||||
var sprites = new List<string>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>background_color</c> DSL command. Sets the dialog scene's
|
||||
/// background color from a named color string (e.g. <c>black</c>, <c>red</c>)
|
||||
/// or an HTML hex color (e.g. <c>#FF0000</c>). Delegates to <see cref="DialogQueue.BackgroundColor"/>.
|
||||
/// </summary>
|
||||
public class BackgroundColorCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "background_color";
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>background</c> DSL command. Sets the dialog scene background
|
||||
/// image from a resource path. Implements <see cref="ICommandReferenceMetadata"/>
|
||||
/// with <see cref="PositionalReferenceKind.Background"/> so the build pipeline
|
||||
/// can track referenced background assets.
|
||||
/// </summary>
|
||||
public class BackgroundCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "background";
|
||||
/// <inheritdoc/>
|
||||
public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Background;
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
dlg.Background(positional.Count > 0 ? positional[0] : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>background_show_hide</c> DSL command. Animates the background
|
||||
/// in (<c>show</c>) or out (<c>hide</c>) with configurable <c>duration</c> and
|
||||
/// <c>wait</c> flag. Delegates to <see cref="DialogQueue.BackgroundShowHide"/>.
|
||||
/// </summary>
|
||||
public class BackgroundShowHideCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "background_show_hide";
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "duration", "wait" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>delay</c> DSL command. Pauses dialog execution for a
|
||||
/// number of seconds, optionally displaying a text label during the pause.
|
||||
/// If the <c>hidden</c> flag is set, the delay runs without visible text.
|
||||
/// Supports localization via the <c>key=</c> named parameter.
|
||||
/// </summary>
|
||||
public class DelayCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "delay";
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 2;
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "key", "hidden" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>effect</c> DSL command. Applies a named visual effect/shader
|
||||
/// to the scene. Implements <see cref="ICommandReferenceMetadata"/> with
|
||||
/// <see cref="PositionalReferenceKind.Effect"/> for build-time dependency tracking.
|
||||
/// </summary>
|
||||
public class EffectCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "effect";
|
||||
/// <inheritdoc/>
|
||||
public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Effect;
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
if (positional.Count > 0)
|
||||
dlg.Effect(positional[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>empty</c> DSL command. Queues a no-op placeholder action
|
||||
/// (<see cref="ActionEmpty"/>) that is skipped instantly — the DSL-level label
|
||||
/// mechanism (<c>:label</c> via <see cref="DialogQueue.Point"/>) is built on the
|
||||
/// same action type, and a bare <c>empty</c> line works as a placeholder marker.
|
||||
/// </summary>
|
||||
public class EmptyCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "empty";
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
dlg.Empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>end</c> 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.
|
||||
/// </summary>
|
||||
public class EndCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "end";
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
dlg.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>foreground</c> 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.
|
||||
/// </summary>
|
||||
public class ForegroundCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "foreground";
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
dlg.Foreground(positional.Count > 0 ? positional[0] : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>goto</c> DSL command. Jumps execution to a labeled anchor
|
||||
/// point (<c>:labelname</c>) in the scenario script. Supports two forms:
|
||||
/// <c>goto target_name</c> via <c>target=</c> named parameter, or
|
||||
/// <c>goto -> target_name</c> via the arrow target syntax.
|
||||
/// </summary>
|
||||
public class GotoCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "goto";
|
||||
/// <inheritdoc/>
|
||||
public int MinArguments => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 0;
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "target" };
|
||||
/// <inheritdoc/>
|
||||
public string JumpTargetNamedKey => "target";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(arrowTarget))
|
||||
dlg.GoTo(arrowTarget);
|
||||
else if (named.TryGetValue("target", out var target))
|
||||
dlg.GoTo(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>hide_dialog</c> DSL command. Hides the dialog UI panel
|
||||
/// (text box, avatars, and UI chrome) from the screen. The inverse of
|
||||
/// <see cref="ShowDialogCommandStrategy"/>.
|
||||
/// </summary>
|
||||
public class HideDialogCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "hide_dialog";
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
dlg.HideDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Scenario DSL command <c>interactive_add</c>: instantiates an interactive
|
||||
/// object from its definition ID. Optional <c>layer</c> parameter overrides
|
||||
/// the layer from the definition.
|
||||
///
|
||||
/// Usage: <c>interactive_add door_to_kitchen</c>
|
||||
/// <c>interactive_add door_to_kitchen layer=foreground</c>
|
||||
/// </summary>
|
||||
public class InteractiveAddCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <summary>The lowercase DSL keyword handled by this strategy: <c>interactive_add</c>.</summary>
|
||||
public string Command => "interactive_add";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "layer" };
|
||||
|
||||
/// <summary>Queues an <see cref="ActionInteractiveAdd"/> for the definition id in <c>positional[0]</c>; lines without an id are skipped.</summary>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
if (positional.Count == 0)
|
||||
return;
|
||||
|
||||
var id = positional[0];
|
||||
dlg.InteractiveAdd(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Scenario DSL command <c>interactive_clear</c>: removes all active
|
||||
/// interactive objects from the scene.
|
||||
///
|
||||
/// Usage: <c>interactive_clear</c>
|
||||
/// </summary>
|
||||
public class InteractiveClearCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <summary>The lowercase DSL keyword handled by this strategy: <c>interactive_clear</c>.</summary>
|
||||
public string Command => "interactive_clear";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 0;
|
||||
|
||||
/// <summary>Queues an <see cref="ActionInteractiveClear"/> that removes every active interactive object from the scene.</summary>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
dlg.InteractiveClear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Scenario DSL command <c>interactive_remove</c>: removes and destroys
|
||||
/// an interactive object by its definition ID.
|
||||
///
|
||||
/// Usage: <c>interactive_remove door_to_kitchen</c>
|
||||
/// </summary>
|
||||
public class InteractiveRemoveCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <summary>The lowercase DSL keyword handled by this strategy: <c>interactive_remove</c>.</summary>
|
||||
public string Command => "interactive_remove";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <summary>Queues an <see cref="ActionInteractiveRemove"/> for the definition id in <c>positional[0]</c>; lines without an id are skipped.</summary>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
if (positional.Count == 0)
|
||||
return;
|
||||
|
||||
var id = positional[0];
|
||||
dlg.InteractiveRemove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Scenario DSL command <c>interactive_set</c>: modifies a property of an
|
||||
/// active interactive object. Supported properties: <c>enabled</c>, <c>visible</c>.
|
||||
///
|
||||
/// Usage: <c>interactive_set door_to_kitchen enabled=false</c>
|
||||
/// <c>interactive_set door_to_kitchen visible=true</c>
|
||||
/// </summary>
|
||||
public class InteractiveSetCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <summary>The lowercase DSL keyword handled by this strategy: <c>interactive_set</c>.</summary>
|
||||
public string Command => "interactive_set";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MinArguments => 2;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "enabled", "visible" };
|
||||
|
||||
/// <summary>Queues an <see cref="ActionInteractiveSet"/> for <c>positional[0]</c>; <c>enabled=</c> wins over <c>visible=</c> when both are given, and lines without both an id and a property are skipped.</summary>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>light_set</c> DSL command. Passes a JSON string that
|
||||
/// describes lighting configuration to the dialog engine via
|
||||
/// <see cref="DialogQueue.LightSet"/>. The JSON structure is forwarded
|
||||
/// as-is to the rendering layer for point-light setup.
|
||||
/// </summary>
|
||||
public class LightSetCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "light_set";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional,
|
||||
Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
var json = positional.Count > 0 ? positional[0] : null;
|
||||
dlg.LightSet(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>light_use_time</c> DSL command. Toggles whether the
|
||||
/// scene lighting should be driven by the simulated time-of-day system.
|
||||
/// Passes a boolean flag to <see cref="DialogQueue.LightUseTime"/>.
|
||||
/// </summary>
|
||||
public class LightUseTimeCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "light_use_time";
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
var val = positional.Count > 0 ? positional[0] : "true";
|
||||
dlg.LightUseTime(val == "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>music</c> DSL command. Starts playing a background music
|
||||
/// track from the given resource path. Implements <see cref="ICommandReferenceMetadata"/>
|
||||
/// with <see cref="PositionalReferenceKind.Music"/> so the build pipeline
|
||||
/// can track referenced audio assets.
|
||||
/// </summary>
|
||||
public class MusicCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "music";
|
||||
/// <inheritdoc/>
|
||||
public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Music;
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
if (positional.Count > 0)
|
||||
dlg.Music(positional[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>music_pause</c> DSL command. Pauses the currently
|
||||
/// playing background music stream. Playback can be resumed later with
|
||||
/// <see cref="MusicPlayCommandStrategy"/>.
|
||||
/// </summary>
|
||||
public class MusicPauseCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "music_pause";
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
dlg.MusicPause();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>music_play</c> DSL command. Resumes the currently
|
||||
/// paused background music stream. Has no effect if music is already playing.
|
||||
/// </summary>
|
||||
public class MusicPlayCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "music_play";
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
dlg.MusicPlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>notify</c> DSL command. Queues a notification text
|
||||
/// into <see cref="NotificationBuffer"/> for display in the top-left
|
||||
/// notification panel. This is a fire-and-forget command — it does not
|
||||
/// block dialog execution.
|
||||
/// </summary>
|
||||
public class NotifyCommandStrategy : IScenarioCommandStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// The lowercase DSL command word.
|
||||
/// </summary>
|
||||
public string Command => "notify";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MinArguments => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "key" };
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the notification text from the first positional argument
|
||||
/// and enqueues it into <see cref="NotificationBuffer"/>. Supports
|
||||
/// localization via the <c>key=</c> named parameter.
|
||||
/// </summary>
|
||||
/// <param name="dlg">The dialog queue (unused — notifications bypass dialog).</param>
|
||||
/// <param name="positional">Positional arguments; <c>positional[0]</c> is the notification text.</param>
|
||||
/// <param name="named">Named arguments; <c>"key"</c> is a locale lookup key.</param>
|
||||
/// <param name="arrowTarget">Unused by this command.</param>
|
||||
/// <param name="locale">Optional localization provider for <c>key=</c> resolution.</param>
|
||||
public void Execute(DialogQueue dlg, List<string> positional,
|
||||
Dictionary<string, string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>ptext</c> DSL command. A "plain text" variant of
|
||||
/// <see cref="TextCommandStrategy"/> that skips speaker attribution
|
||||
/// but still supports <c>first</c>/<c>second</c> avatar faces and
|
||||
/// <c>hide_color</c> and <c>wait</c> behavior flags. Text content
|
||||
/// can be provided directly or resolved via a <c>key=</c> localization key.
|
||||
/// </summary>
|
||||
public class PTextCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "ptext";
|
||||
/// <inheritdoc/>
|
||||
public int MinArguments => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "key", "first", "second", "hide_color", "wait" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>set</c> DSL command. Stores an arbitrary key-value pair
|
||||
/// into the dialog session's variable state via <see cref="DialogQueue.Set"/>.
|
||||
/// Values are read back later through <c>${var}</c> substitution in
|
||||
/// <c>ptext</c> lines and via <see cref="Cthangover.Core.Actions.IActionContext.GetParam"/>
|
||||
/// in scenario actions.
|
||||
/// </summary>
|
||||
public class SetCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "set";
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 2;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 2;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
var name = positional.Count > 0 ? positional[0] : "";
|
||||
var value = positional.Count > 1 ? positional[1] : "";
|
||||
dlg.Set(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>show_dialog</c> DSL command. Makes the dialog UI panel
|
||||
/// (text box, avatars, and UI chrome) visible on screen. The inverse of
|
||||
/// <see cref="HideDialogCommandStrategy"/>.
|
||||
/// </summary>
|
||||
public class ShowDialogCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "show_dialog";
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
dlg.ShowDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>sound</c> DSL command. Plays a one-shot sound effect
|
||||
/// from the given resource path. Implements <see cref="ICommandReferenceMetadata"/>
|
||||
/// with <see cref="PositionalReferenceKind.Sound"/> so the build pipeline
|
||||
/// can track referenced audio assets.
|
||||
/// </summary>
|
||||
public class SoundCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "sound";
|
||||
/// <inheritdoc/>
|
||||
public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Sound;
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
if (positional.Count > 0)
|
||||
dlg.Sound(positional[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>switch_scene</c> DSL command. Transitions to a new Godot
|
||||
/// scene (PackedScene) with configurable transition <c>speed</c> and an
|
||||
/// optional <c>hidden</c> mode that shows only the scene background during
|
||||
/// the transition. Implements <see cref="ICommandReferenceMetadata"/> with
|
||||
/// <see cref="PositionalReferenceKind.Scene"/> for build-time dependency tracking.
|
||||
/// </summary>
|
||||
public class SwitchSceneCommandStrategy : IScenarioCommandStrategy, ICommandReferenceMetadata, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "switch_scene";
|
||||
/// <inheritdoc/>
|
||||
public PositionalReferenceKind Positional0Kind => PositionalReferenceKind.Scene;
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "speed", "hidden" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>text</c> DSL command, the primary dialog display instruction.
|
||||
/// Shows character text with optional <c>first</c>/<c>second</c> speaker
|
||||
/// avatar faces, <c>hide_color</c> tint suppression, and a <c>wait</c> flag
|
||||
/// that controls whether the dialog waits for user input before continuing.
|
||||
/// Text can be literal or resolved from a <c>key=</c> localization entry.
|
||||
/// </summary>
|
||||
public class TextCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "text";
|
||||
/// <inheritdoc/>
|
||||
public int MinArguments => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "key", "first", "second", "hide_color", "wait" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>title</c> 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 <c>key=</c> localization entry.
|
||||
/// Silently does nothing if no text value is provided or resolved.
|
||||
/// </summary>
|
||||
public class TitleCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "title";
|
||||
/// <inheritdoc/>
|
||||
public int MinArguments => 1;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 1;
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AllowedNamed => new[] { "key" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Cthangover.Core.Mods.Consumers;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Scenarios
|
||||
{
|
||||
/// <summary>
|
||||
/// Feeds <see cref="ScenarioCommandStrategyFactory"/> with every mod's
|
||||
/// <see cref="IScenarioCommandStrategy"/> 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.
|
||||
/// </summary>
|
||||
public class ScenarioCommandStrategyConsumer : ModTypeConsumer<IScenarioCommandStrategy>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string Id => "dialog.command_strategies";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Add(IScenarioCommandStrategy instance, string modId) => ScenarioCommandStrategyFactory.Register(instance);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Remove(IScenarioCommandStrategy instance, string modId) => ScenarioCommandStrategyFactory.Unregister(instance);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts scenario DSL text or files into <see cref="DialogQueue"/> instances.
|
||||
/// Wraps <see cref="ScenarioParser"/> with a configurable <see cref="ILocalizationProvider"/>
|
||||
/// that defaults to <see cref="GodotTranslationProvider.Instance"/> when none is supplied.
|
||||
/// </summary>
|
||||
public class ScenarioConverter
|
||||
{
|
||||
private readonly ILocalizationProvider locale;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a converter with an optional localization provider.
|
||||
/// </summary>
|
||||
/// <param name="locale">Localization provider for <c>key=</c> text lookups. Defaults to Godot translations.</param>
|
||||
public ScenarioConverter(ILocalizationProvider locale = null)
|
||||
{
|
||||
this.locale = locale ?? GodotTranslationProvider.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses raw scenario script text into a dialog queue using the configured locale.
|
||||
/// </summary>
|
||||
public DialogQueue Convert(string text)
|
||||
{
|
||||
return ScenarioParser.Parse(text, locale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a scenario file from disk and parses it into a dialog queue.
|
||||
/// </summary>
|
||||
/// <param name="path">Filesystem path to the <c>.scenario</c> file.</param>
|
||||
/// <returns>The parsed <see cref="DialogQueue"/>, or <c>null</c> if the file could not be opened.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads a <c>.scenario</c> file from disk, reads its text content, and
|
||||
/// parses it via <see cref="ScenarioParser.Parse"/> into a <see cref="DialogQueue"/>.
|
||||
/// Uses <see cref="GodotTranslationProvider.Instance"/> as the default localization source.
|
||||
/// </summary>
|
||||
public static class ScenarioLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads and parses a scenario file using the default Godot translation provider.
|
||||
/// </summary>
|
||||
/// <param name="path">Filesystem path to the <c>.scenario</c> file.</param>
|
||||
/// <returns>The parsed <see cref="DialogQueue"/>, or <c>null</c> if the file could not be opened.</returns>
|
||||
public static DialogQueue Load(string path)
|
||||
{
|
||||
return Load(path, GodotTranslationProvider.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads and parses a scenario file with a custom localization provider for text lookups.
|
||||
/// </summary>
|
||||
/// <param name="path">Filesystem path to the <c>.scenario</c> file.</param>
|
||||
/// <param name="provider">Localization provider to resolve <c>key=</c> references during parsing.</param>
|
||||
/// <returns>The parsed <see cref="DialogQueue"/>, or <c>null</c> if the file could not be opened.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses scenario DSL script text into a <see cref="DialogQueue"/> 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
|
||||
/// <see cref="IScenarioCommandStrategy"/> via <see cref="ScenarioCommandStrategyFactory"/>.
|
||||
/// Special handling exists for <c>:label</c> anchor points, <c>select/option</c>
|
||||
/// blocks (accumulated into <see cref="ActionSelect"/>), and <c>-></c> arrow targets.
|
||||
/// </summary>
|
||||
public static class ScenarioParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a complete scenario script text and returns the resulting dialog queue.
|
||||
/// </summary>
|
||||
/// <param name="text">Raw scenario DSL content.</param>
|
||||
/// <param name="locale">Optional localization provider for <c>key=</c> text lookups.</param>
|
||||
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<string>();
|
||||
var named = new Dictionary<string, string>();
|
||||
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<SelectVariant>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts YAML-like front-matter metadata from script text. Lines before
|
||||
/// a <c>---</c> separator or the first non-key:value line are parsed as
|
||||
/// <c>key: value</c> pairs. Returns both the metadata dictionary and the
|
||||
/// remaining script content after the metadata block.
|
||||
/// </summary>
|
||||
public static (Dictionary<string, string> metadata, string remainingText) ParseMetadata(string text)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>(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<string> Tokenize(string line)
|
||||
{
|
||||
var result = new List<string>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Cthangover.Mods.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Migration marker for the coverage pipeline
|
||||
/// (<c>SubsystemsBridge</c> reflection scan): declares the dialog
|
||||
/// subsystem fully moved out of the core assembly into this engine mod.
|
||||
/// The id must match the <c>DeclaredTests</c> entry in
|
||||
/// <c>SubsystemsCoverageTestSetup</c>.
|
||||
/// </summary>
|
||||
public static class SubsystemRegistration
|
||||
{
|
||||
public static string SubsystemId => "dialog";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public abstract class ActionCommand : IActionCommand
|
||||
{
|
||||
/// <summary>Unique identifier for this action. Auto-generated GUID by default; can be overridden via <see cref="SetID"/> for GoTo targeting.</summary>
|
||||
public string ID { get; set; }
|
||||
/// <summary>Whether <see cref="Construct"/> has been called and <see cref="DoConstruct"/> has completed.</summary>
|
||||
public bool IsConstructed { get; private set; }
|
||||
/// <summary>Whether <see cref="Destruct"/> has been called and this action is finished.</summary>
|
||||
public bool IsDestructed { get; private set; }
|
||||
/// <summary>When to call <see cref="Construct"/>: OnStartQueue (preload) or OnStartAction (lazy, just before run). Default is OnStartAction.</summary>
|
||||
public virtual ConstructType ConstructType { get; set; } = ConstructType.OnStartAction;
|
||||
/// <summary>When to call <see cref="Destruct"/>: OnEndAction (immediate), OnDelayed (manual cleanup), or OnEndQueue (when dialog finishes). Default is OnEndAction.</summary>
|
||||
public virtual DestructType DestructType { get; set; } = DestructType.OnEndAction;
|
||||
/// <summary>Controls how the runtime advances past this action. Default is WaitClick (pause for user input).</summary>
|
||||
public virtual WaitType WaitType { get; set; } = WaitType.WaitClick;
|
||||
/// <summary>Duration in seconds for WaitTime. Used only when <see cref="WaitType"/> is WaitTime.</summary>
|
||||
public virtual float WaitTime { get; set; }
|
||||
/// <summary>Timestamp when <see cref="Run"/> was called. Used by WaitTime actions to measure elapsed time.</summary>
|
||||
public float StartTime { get; private set; }
|
||||
/// <summary>Action IDs whose <see cref="Destruct"/> will be triggered when this action destructs. Used for cross-action cleanup dependencies.</summary>
|
||||
public ISet<string> DelayedDestruct { get; } = new HashSet<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// (<see cref="SceneNodeServiceImpl"/>) and is instantiated per action
|
||||
/// instance — the same no-container style as ScenarioActionContext.
|
||||
/// </summary>
|
||||
protected ISceneNodeService Scene { get; set; } = new SceneNodeServiceImpl();
|
||||
|
||||
/// <summary>Generates a unique ID so every action is addressable by GoTo/If even without an explicit <see cref="SetID"/> call.</summary>
|
||||
public ActionCommand()
|
||||
{
|
||||
ID = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
/// <summary>Idempotent construction: calls <see cref="DoConstruct"/> once and sets <see cref="IsConstructed"/> to true.</summary>
|
||||
public void Construct()
|
||||
{
|
||||
if(IsConstructed) return;
|
||||
DoConstruct();
|
||||
IsConstructed = true;
|
||||
}
|
||||
|
||||
/// <summary>Idempotent desctruction: calls <see cref="DoDestruct"/> once, then sets <see cref="IsConstructed"/> false and <see cref="IsDestructed"/> true.</summary>
|
||||
public void Destruct()
|
||||
{
|
||||
if(IsDestructed) return;
|
||||
DoDestruct();
|
||||
IsConstructed = false;
|
||||
IsDestructed = true;
|
||||
}
|
||||
|
||||
/// <summary>Constructs if not already done, records start time, and delegates execution to <see cref="DoRun"/>.</summary>
|
||||
public void Run(DialogRuntime runtime)
|
||||
{
|
||||
if (!IsConstructed) DoConstruct();
|
||||
StartTime = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0);
|
||||
DoRun(runtime);
|
||||
}
|
||||
|
||||
/// <summary>Override in subclasses to implement action-specific behavior. Receives the runtime for text processing and dialog box access.</summary>
|
||||
public abstract void DoRun(DialogRuntime runtime);
|
||||
/// <summary>Override to preload resources or register listeners. Called by <see cref="Construct"/>.</summary>
|
||||
public virtual void DoConstruct() { }
|
||||
/// <summary>Override to release resources or disconnect listeners. Called by <see cref="Destruct"/>.</summary>
|
||||
public virtual void DoDestruct() { }
|
||||
|
||||
/// <summary>Fluent setter for <see cref="ID"/>. Use to assign a named target for GoTo/If commands.</summary>
|
||||
public ActionCommand SetID(string id)
|
||||
{
|
||||
ID = id;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Cthangover.Mods.Dialog.Action
|
||||
{
|
||||
/// <summary>
|
||||
/// High-level action category discriminator. Currently minimal (Text, End)
|
||||
/// — may expand as additional action families are added.
|
||||
/// </summary>
|
||||
public enum ActionType
|
||||
{
|
||||
/// <summary>A dialog text display action that pauses for click input.</summary>
|
||||
Text,
|
||||
/// <summary>Terminates the dialog queue.</summary>
|
||||
End
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Cthangover.Mods.Dialog.Action
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public enum ConstructType
|
||||
{
|
||||
/// <summary>Construct when the dialog queue is first loaded. Useful for actions that need to preload resources.</summary>
|
||||
OnStartQueue,
|
||||
/// <summary>Construct lazily just before the action runs. Default for most actions — saves resources for unreached branches.</summary>
|
||||
OnStartAction
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Cthangover.Mods.Dialog.Action
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public enum DestructType
|
||||
{
|
||||
/// <summary>Destruct immediately when the action is advanced past. Default for most commands.</summary>
|
||||
OnEndAction,
|
||||
/// <summary>Destruct is deferred — must be manually triggered or triggered via <see cref="ActionCommand.DelayedDestruct"/>.</summary>
|
||||
OnDelayed,
|
||||
/// <summary>Destruct when the entire dialog queue finishes. Use for resources that must survive the whole dialog but not beyond.</summary>
|
||||
OnEndQueue,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public interface IActionCommand : IActionDestruct
|
||||
{
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>Unique action identifier, auto-generated GUID. Can be overridden for named GoTo targets.</summary>
|
||||
string ID { get; set; }
|
||||
|
||||
/// <summary>Whether <see cref="Construct"/> has completed.</summary>
|
||||
bool IsConstructed { get; }
|
||||
/// <summary>Whether <see cref="Destruct"/> has been called and the action is finished.</summary>
|
||||
bool IsDestructed { get; }
|
||||
/// <summary>Controls when construction fires relative to the action's lifecycle.</summary>
|
||||
ConstructType ConstructType { get; }
|
||||
/// <summary>Controls when destruction fires relative to the action's lifecycle.</summary>
|
||||
DestructType DestructType { get; }
|
||||
/// <summary>How the runtime should handle advancement past this action.</summary>
|
||||
WaitType WaitType { get; }
|
||||
/// <summary>IDs of actions whose Destruct should be called when this action destructs.</summary>
|
||||
ISet<string> DelayedDestruct { get; }
|
||||
/// <summary>Duration in seconds when WaitType is WaitTime.</summary>
|
||||
float WaitTime { get; }
|
||||
/// <summary>Timestamp of the last <see cref="Run"/> call, in seconds since epoch.</summary>
|
||||
float StartTime { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>Called by the runtime to initialize the action before execution.</summary>
|
||||
void Construct();
|
||||
/// <summary>Called by the runtime to execute the action. Receives the runtime for context.</summary>
|
||||
void Run(DialogRuntime runtime);
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Cthangover.Mods.Dialog.Action
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimal destructible contract — anything in the dialog runtime's object list
|
||||
/// that must be cleaned up when a dialog ends.
|
||||
/// </summary>
|
||||
public interface IActionDestruct
|
||||
{
|
||||
/// <summary>Called to release resources, disconnect signals, and mark the object as finished. Must be idempotent.</summary>
|
||||
void Destruct();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionAnimatedForeground : ActionCommand, IOnUpdateEvent
|
||||
{
|
||||
/// <summary>Update priority. Runs after higher-priority listeners. Value 0.</summary>
|
||||
public int Priority => 0;
|
||||
|
||||
/// <summary>List of sprite resource paths to display as animation frames. Loaded in <see cref="DoConstruct"/>.</summary>
|
||||
public List<string> SpriteIDs { get; set; }
|
||||
/// <summary>Initial frame rate multiplier for the first frame.</summary>
|
||||
public float Speed { get; set; } = 1f;
|
||||
/// <summary>Frame rate multiplier for subsequent frames after the first. Allows different pacing for the initial frame.</summary>
|
||||
public float NextFrameSpeed { get; set; } = 1f;
|
||||
/// <summary>Whether the animation wraps after the last frame. When false, playback stops at the final frame.</summary>
|
||||
public bool IsLoop { get; set; } = true;
|
||||
|
||||
private int currentFrame;
|
||||
private float elapsed;
|
||||
private bool isAnimating;
|
||||
private List<Texture2D> frames;
|
||||
|
||||
/// <summary>Preloads frames when the dialog queue is first loaded, so textures are ready before playback.</summary>
|
||||
public override ConstructType ConstructType { get; set; } = ConstructType.OnStartQueue;
|
||||
/// <summary>Animation runs in the background — the dialog queue continues immediately.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Preloads every sprite in <see cref="SpriteIDs"/> into memory so playback never stalls on I/O.</summary>
|
||||
public override void DoConstruct()
|
||||
{
|
||||
base.DoConstruct();
|
||||
LoadFrames();
|
||||
}
|
||||
|
||||
/// <summary>Stops the animation loop; the last shown frame stays on screen.</summary>
|
||||
public override void DoDestruct()
|
||||
{
|
||||
base.DoDestruct();
|
||||
isAnimating = false;
|
||||
}
|
||||
|
||||
private void LoadFrames()
|
||||
{
|
||||
frames = new List<Texture2D>();
|
||||
if (SpriteIDs == null)
|
||||
return;
|
||||
foreach (var id in SpriteIDs)
|
||||
{
|
||||
var tex = GD.Load<Texture2D>(id);
|
||||
if (tex != null)
|
||||
frames.Add(tex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resets playback to the first frame and shows it on the ViewBox; per-frame stepping happens through <see cref="IOnUpdateEvent.OnUpdate"/>.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
currentFrame = 0;
|
||||
elapsed = 0f;
|
||||
isAnimating = frames != null && frames.Count > 0;
|
||||
|
||||
if (isAnimating)
|
||||
{
|
||||
var viewBox = Scene.Find<ViewBox>("ViewBox");
|
||||
viewBox?.SetForegroundTexture(frames[0]);
|
||||
}
|
||||
}
|
||||
|
||||
void IOnUpdateEvent.OnUpdate()
|
||||
{
|
||||
if (!isAnimating || frames == null || frames.Count == 0)
|
||||
return;
|
||||
|
||||
var viewBox = Scene.Find<ViewBox>("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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionBackground : ActionCommand
|
||||
{
|
||||
/// <summary>Sprite resource ID resolved through <see cref="BackgroundFactory"/> on construction. Also loads depth (_depth) and albedo (_albedo) variants.</summary>
|
||||
public string SpriteID { get; set; }
|
||||
/// <summary>Direct texture override. When non-null, bypasses factory resolution. Set to null after application to prevent stale reuse.</summary>
|
||||
public Texture2D Texture { get; set; }
|
||||
/// <summary>Depth map for the lighting system, loaded from <see cref="SpriteID"/> + "_depth".</summary>
|
||||
public Texture2D DepthTexture { get; set; }
|
||||
/// <summary>Albedo color map for the lighting system, loaded from <see cref="SpriteID"/> + "_albedo".</summary>
|
||||
public Texture2D AlbedoTexture { get; set; }
|
||||
|
||||
/// <summary>Preloads textures when the dialog queue is first created, resolving SpriteID through the factory.</summary>
|
||||
public override ConstructType ConstructType { get; set; } = ConstructType.OnStartQueue;
|
||||
/// <summary>Background is set imperatively — the dialog continues immediately.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Resolves <see cref="SpriteID"/> plus its <c>_depth</c>/<c>_albedo</c> variants through the background factory.</summary>
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
if (Texture == null)
|
||||
return;
|
||||
|
||||
SceneContextNode.LastBackgroundID = SpriteID;
|
||||
|
||||
var viewBox = Scene.Find<ViewBox>("ViewBox");
|
||||
viewBox?.KillTransitionTween();
|
||||
viewBox?.SetBackgroundTexture(Texture);
|
||||
|
||||
var controller = UiLightController.Instance;
|
||||
controller?.SetupDepthMap(DepthTexture);
|
||||
controller?.SetupAlbedoMap(AlbedoTexture);
|
||||
|
||||
Texture = null;
|
||||
DepthTexture = null;
|
||||
AlbedoTexture = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionBackgroundColor : ActionCommand
|
||||
{
|
||||
/// <summary>Target tint color. Stored declaratively — consumed by background-rendering nodes rather than applied imperatively here.</summary>
|
||||
public Color Color { get; set; } = Colors.Black;
|
||||
|
||||
/// <summary>Imperative — the dialog continues immediately after this action is processed.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Deliberate no-op: the tint is declarative data for background-rendering nodes, not applied here.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionBackgroundShowHide : ActionCommand
|
||||
{
|
||||
/// <summary>Whether to fade the background in (Show) or out (Hide).</summary>
|
||||
public BackgroundActionType ActionType { get; set; } = BackgroundActionType.Show;
|
||||
/// <summary>Duration of the fade tween in seconds.</summary>
|
||||
public float Duration { get; set; } = 1f;
|
||||
|
||||
private Tween activeTween;
|
||||
|
||||
/// <summary>Default NoWait — overridden by the DialogQueue builder if the wait flag is set, changing to WaitTime to pause during transition.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Starts an alpha tween of the dialog box parent's <c>modulate:a</c> towards fully visible (Show) or fully transparent (Hide).</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Kills the fade tween so a destructed action cannot keep animating.</summary>
|
||||
public override void DoDestruct()
|
||||
{
|
||||
base.DoDestruct();
|
||||
StopTween();
|
||||
}
|
||||
|
||||
private void StopTween()
|
||||
{
|
||||
if (activeTween != null)
|
||||
{
|
||||
activeTween.Kill();
|
||||
activeTween = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionDelay : ActionCommand
|
||||
{
|
||||
/// <summary>When true, hides the dialog body for the duration of the wait (dramatic pauses, cutscene beats).</summary>
|
||||
public bool HiddenMode { get; set; }
|
||||
/// <summary>Optional temporary text displayed while waiting. Set to null to keep the current dialog text.</summary>
|
||||
public string ShowText { get; set; }
|
||||
|
||||
/// <summary>Pauses the dialog until <see cref="WaitTime"/> elapses.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.WaitTime;
|
||||
/// <summary>Duration of the pause in seconds. Default 1.0.</summary>
|
||||
public override float WaitTime { get; set; } = 1f;
|
||||
|
||||
/// <summary>Shows the optional <see cref="ShowText"/> and, in <see cref="HiddenMode"/>, hides the dialog body for the pause duration.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>No-op: the dialog body hidden by <see cref="HiddenMode"/> is restored implicitly by the next show.</summary>
|
||||
public override void DoDestruct()
|
||||
{
|
||||
base.DoDestruct();
|
||||
if (HiddenMode)
|
||||
{
|
||||
// Body restored on next Show
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionEffect : ActionCommand
|
||||
{
|
||||
/// <summary>Identifier resolved through <see cref="EffectFactory"/> to a PackedScene. The spawned node persists until destructed.</summary>
|
||||
public string EffectID { get; set; }
|
||||
|
||||
private Node spawnedEffect;
|
||||
|
||||
/// <summary>The dialog continues immediately — the spawned effect plays independently.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
/// <summary>OnDelayed ensures the effect node outlives the action's run phase. Destruct is called manually when the effect finishes.</summary>
|
||||
public override DestructType DestructType { get; set; } = DestructType.OnDelayed;
|
||||
|
||||
/// <summary>Resolves <see cref="EffectID"/> through <see cref="EffectFactory"/> and instantiates the effect scene.</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>No-op: spawning happens during construction so the effect is ready before the queue reaches this point.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Frees the spawned effect node; paired with the OnDelayed destruct so effects outlive the action's run.</summary>
|
||||
public override void DoDestruct()
|
||||
{
|
||||
base.DoDestruct();
|
||||
if (spawnedEffect != null)
|
||||
{
|
||||
spawnedEffect.QueueFree();
|
||||
spawnedEffect = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionEmpty : ActionCommand
|
||||
{
|
||||
/// <summary>Label actions skip immediately so the dialog doesn't pause on markers.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>No-op: the action exists only to carry the jump-target ID.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionEnd : ActionCommand
|
||||
{
|
||||
/// <summary>Ends the dialog queue immediately — no pause, no return.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Terminates the queue via <see cref="DialogRuntime.End"/> — destructs remainders, runs cleanup, hides the box, fires the end event.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
runtime.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionForeground : ActionCommand
|
||||
{
|
||||
/// <summary>Sprite resource ID resolved through <see cref="BackgroundFactory"/> on construction.</summary>
|
||||
public string SpriteID { get; set; }
|
||||
/// <summary>Direct texture override. Bypasses factory resolution. Nulled after application to prevent stale reuse across dialog cycles.</summary>
|
||||
public Texture2D Texture { get; set; }
|
||||
|
||||
/// <summary>Preloads the texture when the dialog queue is first created.</summary>
|
||||
public override ConstructType ConstructType { get; set; } = ConstructType.OnStartQueue;
|
||||
/// <summary>Foreground is set imperatively — the dialog continues immediately.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Resolves <see cref="SpriteID"/> through the background factory unless a direct texture override is set.</summary>
|
||||
public override void DoConstruct()
|
||||
{
|
||||
base.DoConstruct();
|
||||
if (Texture == null && !string.IsNullOrEmpty(SpriteID))
|
||||
Texture = BackgroundFactory.Instance.Get(SpriteID);
|
||||
}
|
||||
|
||||
/// <summary>Applies the preloaded texture to the ViewBox foreground, then nulls it so a reused action cannot re-apply stale data.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var viewBox = Scene.Find<ViewBox>("ViewBox");
|
||||
viewBox?.SetForegroundTexture(Texture);
|
||||
Texture = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public class ActionGoTo : ActionCommand
|
||||
{
|
||||
/// <summary>Target action ID to jump to. Resolved by linear search through the dialog queue.</summary>
|
||||
public string GoTo { get; set; }
|
||||
|
||||
/// <summary>Jump happens transparently — the dialog continues from the target without pausing.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Jumps the queue index to the action whose ID matches <see cref="GoTo"/>.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
runtime.TryGoTo(GoTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionHideDialog : ActionCommand
|
||||
{
|
||||
/// <summary>Hides the dialog immediately — the queue continues without pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Hides the dialog box chrome while keeping the queue running.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var dialogBox = runtime.DialogBox;
|
||||
if (dialogBox == null)
|
||||
return;
|
||||
|
||||
dialogBox.Visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionIf : ActionCommand
|
||||
{
|
||||
/// <summary>Delegate evaluated at runtime. Must return true/false to select the branch target.</summary>
|
||||
public Func<bool> Condition { get; set; }
|
||||
/// <summary>Target action ID if <see cref="Condition"/> returns true. Empty or null causes fall-through.</summary>
|
||||
public string TrueGoTo { get; set; }
|
||||
/// <summary>Target action ID if <see cref="Condition"/> returns false. Empty or null causes fall-through.</summary>
|
||||
public string FalseGoTo { get; set; }
|
||||
|
||||
/// <summary>Branch evaluation is transparent — the dialog jumps to the target without user input.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Evaluates <see cref="Condition"/> and jumps to <see cref="TrueGoTo"/> or <see cref="FalseGoTo"/>; an empty target falls through to the next action.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Dialog action that instantiates an interactive object from its definition
|
||||
/// and places it on the scene. Executes immediately (NoWait).
|
||||
/// </summary>
|
||||
public class ActionInteractiveAdd : ActionCommand
|
||||
{
|
||||
/// <summary>The interactive definition ID to load and instantiate.</summary>
|
||||
public string DefinitionId { get; set; }
|
||||
|
||||
/// <summary>Placement happens immediately — the dialog does not pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Instantiates the interactive object named by <see cref="DefinitionId"/> through <see cref="InteractiveManager"/>.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Dialog action that removes and destroys all active interactive objects.
|
||||
/// Typically used on scene exit to ensure clean state.
|
||||
/// </summary>
|
||||
public class ActionInteractiveClear : ActionCommand
|
||||
{
|
||||
/// <summary>Clearing happens immediately — the dialog does not pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Destroys every active interactive object via <see cref="InteractiveManager"/>.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
GameLogger.Log("INTERACTIVE", "ActionInteractiveClear: clearing all interactives");
|
||||
InteractiveManager.Instance?.ClearAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Dialog action that removes and destroys an interactive object by its definition ID.
|
||||
/// </summary>
|
||||
public class ActionInteractiveRemove : ActionCommand
|
||||
{
|
||||
/// <summary>The definition ID of the interactive object to remove.</summary>
|
||||
public string DefinitionId { get; set; }
|
||||
|
||||
/// <summary>Removal happens immediately — the dialog does not pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Destroys the interactive object named by <see cref="DefinitionId"/>.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(DefinitionId))
|
||||
{
|
||||
GameLogger.Log("INTERACTIVE", "ActionInteractiveRemove: DefinitionId is empty", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
InteractiveManager.Instance?.Remove(DefinitionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Dialog action that modifies a property of an active interactive object.
|
||||
/// Supported properties: <c>enabled</c> (bool), <c>visible</c> (bool).
|
||||
/// </summary>
|
||||
public class ActionInteractiveSet : ActionCommand
|
||||
{
|
||||
/// <summary>The definition ID of the interactive object to modify.</summary>
|
||||
public string DefinitionId { get; set; }
|
||||
|
||||
/// <summary>Property name: "enabled" or "visible".</summary>
|
||||
public string Property { get; set; }
|
||||
|
||||
/// <summary>String value to parse: "true" or "false".</summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <summary>Property changes happen immediately — the dialog does not pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Parses <see cref="Value"/> as a bool and applies it to the <c>enabled</c> or <c>visible</c> property of the object named by <see cref="DefinitionId"/>.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionLightSet : ActionCommand
|
||||
{
|
||||
/// <summary>JSON string defining static light positions and properties. Empty or null clears all static lights.</summary>
|
||||
public string LightsJson { get; set; }
|
||||
|
||||
/// <summary>Lights are set imperatively — the dialog continues immediately.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Clears static lights when <see cref="LightsJson"/> is empty, otherwise parses and applies the light definitions to the scene's <see cref="UiLightController"/>.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var controller = Scene.Find<UiLightController>("Lights");
|
||||
if (controller == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(LightsJson))
|
||||
controller.ClearStaticLights();
|
||||
else
|
||||
controller.SetStaticLights(LightsJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionLightUseTime : ActionCommand
|
||||
{
|
||||
/// <summary>True = lights respond to in-game time of day; false = static lighting mode.</summary>
|
||||
public bool UseTime { get; set; } = true;
|
||||
|
||||
/// <summary>Time mode toggle is instant — the dialog continues without pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Switches the scene's <see cref="UiLightController"/> between time-of-day lighting and static lights.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var controller = Scene.Find<UiLightController>("Lights");
|
||||
if (controller != null)
|
||||
controller.IsUseLight = UseTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionMusic : ActionCommand
|
||||
{
|
||||
/// <summary>Music track identifier resolved through the audio system. Empty strings are silently ignored.</summary>
|
||||
public string Music { get; set; }
|
||||
|
||||
/// <summary>Music starts playing in the background — the dialog continues immediately.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Starts the named track as ambient background music through <see cref="IAudioService"/>; empty names are ignored.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Music))
|
||||
return;
|
||||
|
||||
var audioService = Scene.Find<Node>("AudioService") as IAudioService;
|
||||
audioService?.PlayMusic(Music, MusicType.Ambient);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Pauses background music playback via IAudioService without stopping the
|
||||
/// track. Can be resumed with ActionMusicPlay.
|
||||
/// </summary>
|
||||
public class ActionMusicPause : ActionCommand
|
||||
{
|
||||
/// <summary>Pause is instant — the dialog continues without waiting for audio acknowledgment.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Pauses the current music track without stopping it, so <see cref="ActionMusicPlay"/> can resume it.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var audioService = Scene.Find<Node>("AudioService") as IAudioService;
|
||||
audioService?.PauseMusic(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Resumes paused background music. Paired with ActionMusicPause.
|
||||
/// </summary>
|
||||
public class ActionMusicPlay : ActionCommand
|
||||
{
|
||||
/// <summary>Resume is instant — the dialog continues without waiting for audio.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Resumes music paused by <see cref="ActionMusicPause"/>.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var audioService = Scene.Find<Node>("AudioService") as IAudioService;
|
||||
audioService?.PauseMusic(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionRun : ActionCommand
|
||||
{
|
||||
/// <summary>Arbitrary delegate executed during dialog sequencing. Null delegates are logged and skipped.</summary>
|
||||
public System.Action Executable { get; set; }
|
||||
|
||||
/// <summary>The delegate runs and the dialog advances — no pausing.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Invokes <see cref="Executable"/>; a null delegate is logged and skipped so the queue keeps flowing.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
if (Executable == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "ActionRun: Executable delegate is null", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Executable();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="DescribeScenario"/>): 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.
|
||||
/// </summary>
|
||||
public class ActionScenario : ActionCommand
|
||||
{
|
||||
/// <summary>Scenario action name resolved through <see cref="ScenarioActionFactory"/>. Empty names are logged and skipped.</summary>
|
||||
public string ActionName { get; set; }
|
||||
|
||||
/// <summary>The scenario action runs and the dialog advances — no synchronization.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Resolves <see cref="ActionName"/> through <see cref="ScenarioActionFactory"/> and runs it with a dialog-backed context; exceptions are logged, never propagated.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static string DescribeScenario(DialogRuntime runtime)
|
||||
{
|
||||
var id = runtime?.DialogBox?.Locker?.ID;
|
||||
return string.IsNullOrEmpty(id)
|
||||
? " (scenario unknown)"
|
||||
: $" (scenario '{id}')";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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?").
|
||||
/// </summary>
|
||||
public class ActionSelect : ActionCommand
|
||||
{
|
||||
/// <summary>The list of selectable choice variants. Each carries display text and a GoTo target.</summary>
|
||||
public List<SelectVariant> Variants { get; set; }
|
||||
/// <summary>Context text shown alongside the choices (e.g. "What will you do?"). Supports variable substitution.</summary>
|
||||
public string Text { get; set; }
|
||||
/// <summary>Left avatar sprite ID shown during the choice. Null hides the slot.</summary>
|
||||
public string FirstAvatar { get; set; }
|
||||
/// <summary>Right avatar sprite ID shown during the choice. Null hides the slot.</summary>
|
||||
public string SecondAvatar { get; set; }
|
||||
|
||||
/// <summary>Dialog pauses until the player selects a variant — then the runtime jumps to the chosen GoTo target.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.WaitSelect;
|
||||
|
||||
/// <summary>Substitutes variables in the variants and context text, then shows the answer box; the runtime stays paused until a variant is picked.</summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionSet : ActionCommand
|
||||
{
|
||||
/// <summary>Variable name. Referenced by subsequent actions via ${name} syntax.</summary>
|
||||
public string Name { get; set; }
|
||||
/// <summary>Literal value to store. Ignored if <see cref="Callback"/> is set.</summary>
|
||||
public string Value { get; set; }
|
||||
/// <summary>Lazy evaluation delegate. When non-null, invoked at runtime to compute the value dynamically.</summary>
|
||||
public Func<string> Callback { get; set; }
|
||||
|
||||
/// <summary>Variable assignment is instant — the dialog continues without pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Stores <see cref="Value"/> — or the result of <see cref="Callback"/> when set — as the runtime variable <see cref="Name"/>.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var val = Callback != null ? Callback() : Value;
|
||||
runtime.SetVariable(Name, val ?? string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes the dialog box visible. Typically paired with ActionHideDialog
|
||||
/// for cutscenes that temporarily hide the dialog UI while animations play.
|
||||
/// </summary>
|
||||
public class ActionShowDialog : ActionCommand
|
||||
{
|
||||
/// <summary>Dialog visibility change is instant — the queue continues without pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Makes the dialog box chrome visible again after <see cref="ActionHideDialog"/>.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var dialogBox = runtime.DialogBox;
|
||||
if (dialogBox == null)
|
||||
return;
|
||||
|
||||
dialogBox.Visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionSound : ActionCommand
|
||||
{
|
||||
/// <summary>Sound effect identifier resolved through <see cref="IAudioService"/>. Played on the UI channel.</summary>
|
||||
public string Sound { get; set; }
|
||||
|
||||
/// <summary>Sound is fire-and-forget — the dialog continues immediately without synchronization.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Plays <see cref="Sound"/> on the UI sound channel through <see cref="IAudioService"/>; empty names are ignored.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Sound))
|
||||
return;
|
||||
|
||||
var audioService = Scene.Find<Node>("AudioService") as IAudioService;
|
||||
audioService?.PlaySound(Sound, SoundType.UI);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para/>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionSwitchScene : ActionCommand
|
||||
{
|
||||
/// <summary>Target scene name passed to <see cref="SceneManager.SwitchScene"/>.</summary>
|
||||
public string SceneName { get; set; }
|
||||
/// <summary>Whether to transition with the dialog hidden. Default true.</summary>
|
||||
public bool HiddenMode { get; set; } = true;
|
||||
/// <summary>Transition speed parameter passed to the scene manager. Default 4.0.</summary>
|
||||
public float Speed { get; set; } = 4f;
|
||||
|
||||
/// <summary>Scene switch is deferred — the dialog ends immediately with no return.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Marks the dialog locker as OneRun and defers <c>SceneManager.SwitchScene</c> so the tree is not mutated mid-queue.</summary>
|
||||
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>("SceneManager");
|
||||
if (sceneManager != null)
|
||||
{
|
||||
Callable.From(() => sceneManager.SwitchScene(SceneName)).CallDeferred();
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"ActionSwitchScene: SceneManager not found for '{SceneName}'", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionText : ActionCommand
|
||||
{
|
||||
/// <summary>The dialog text to display. Supports ${var} substitution when <see cref="UseProcessText"/> is true.</summary>
|
||||
public string Text { get; set; }
|
||||
/// <summary>Left avatar sprite ID resolved through <see cref="IAvatarProvider"/>. Null or empty hides the slot.</summary>
|
||||
public string FirstAvatar { get; set; }
|
||||
/// <summary>Right avatar sprite ID resolved through <see cref="IAvatarProvider"/>. Null or empty hides the slot.</summary>
|
||||
public string SecondAvatar { get; set; }
|
||||
/// <summary>When true, the avatar is rendered in shader-based silhouette/hidden-color mode.</summary>
|
||||
public bool HideColor { get; set; }
|
||||
/// <summary>When true, runtime variable substitution (${var}) is applied to <see cref="Text"/> before display.</summary>
|
||||
public bool UseProcessText { get; set; }
|
||||
|
||||
/// <summary>Pauses for player click/key input — the dialog waits for user acknowledgment.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.WaitClick;
|
||||
|
||||
/// <summary>Sets the dialog text (optionally variable-substituted) and both avatar slots, then waits for the player's click.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ActionTimedUpdate : ActionCommand, IOnUpdateEvent
|
||||
{
|
||||
/// <summary>Update priority. Value 0 — runs before most other listeners.</summary>
|
||||
public int Priority => 0;
|
||||
|
||||
/// <summary>Fires once when <see cref="DoRun"/> is called, before the first update frame.</summary>
|
||||
public System.Action OnStartCallback { get; set; }
|
||||
/// <summary>Fires each frame during the active period with a normalized progress value t (0→1) over <see cref="WaitTime"/> seconds.</summary>
|
||||
public System.Action<float> OnUpdateCallback { get; set; }
|
||||
/// <summary>Fires once when the elapsed time exceeds <see cref="WaitTime"/> or when destructed.</summary>
|
||||
public System.Action OnFinishCallback { get; set; }
|
||||
|
||||
/// <summary>Pauses the dialog until the elapsed time meets WaitTime — the action then advances the runtime itself.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.WaitEvent;
|
||||
/// <summary>Total duration in seconds over which <see cref="OnUpdateCallback"/> receives normalized progress.</summary>
|
||||
public override float WaitTime { get; set; } = 1f;
|
||||
|
||||
private float elapsed;
|
||||
private bool isRunning;
|
||||
private DialogRuntime runtime;
|
||||
|
||||
/// <summary>Registers this action as an <see cref="IOnUpdateEvent"/> listener on the scene's event controller.</summary>
|
||||
public override void DoConstruct()
|
||||
{
|
||||
base.DoConstruct();
|
||||
var controller = Scene.Find<SceneEventController>("EventController");
|
||||
controller?.AddUpdateEventListener(this);
|
||||
}
|
||||
|
||||
/// <summary>Starts the timeline: resets elapsed time and fires <see cref="OnStartCallback"/> before the first frame.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
this.runtime = runtime;
|
||||
elapsed = 0f;
|
||||
isRunning = true;
|
||||
OnStartCallback?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Detaches from the update controller, stops ticking and fires <see cref="OnFinishCallback"/>.</summary>
|
||||
public override void DoDestruct()
|
||||
{
|
||||
base.DoDestruct();
|
||||
isRunning = false;
|
||||
runtime = null;
|
||||
var controller = Scene.Find<SceneEventController>("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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the dialog box title text. A null title hides the title bar.
|
||||
/// NoWait — title updates are instant visual changes.
|
||||
/// </summary>
|
||||
public class ActionTitle : ActionCommand
|
||||
{
|
||||
/// <summary>Text to display in the title bar. Null hides the title bar.</summary>
|
||||
public string TitleText { get; set; }
|
||||
|
||||
/// <summary>Title changes are instant visual updates — the dialog continues immediately.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <summary>Applies <see cref="TitleText"/> to the dialog title bar; null hides the bar.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var dialogBox = runtime.DialogBox;
|
||||
if (dialogBox == null)
|
||||
return;
|
||||
|
||||
dialogBox.SetTitle(TitleText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Discriminator for background show/hide transitions.
|
||||
/// </summary>
|
||||
public enum BackgroundActionType
|
||||
{
|
||||
/// <summary>Fade the dialog background visible (target alpha = 1.0).</summary>
|
||||
Show,
|
||||
/// <summary>Fade the dialog background transparent (target alpha = 0.0).</summary>
|
||||
Hide
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class SelectVariant
|
||||
{
|
||||
/// <summary>The text displayed on the choice button. Supports runtime variable substitution.</summary>
|
||||
public string Text { get; set; }
|
||||
/// <summary>Target action ID to jump to via <see cref="DialogRuntime.TryGoTo"/> when this variant is selected.</summary>
|
||||
public string GoTo { get; set; }
|
||||
|
||||
/// <summary>Generates a new unique action ID for use as a GoTo target (e.g. with <see cref="DialogQueue.Point"/>).</summary>
|
||||
public static string Point => Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>Factory for concise C# construction without object-initializer syntax.</summary>
|
||||
public static SelectVariant New(string text, string goTo)
|
||||
{
|
||||
return new SelectVariant { Text = text, GoTo = goTo };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Cthangover.Mods.Dialog.Action
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public enum WaitType
|
||||
{
|
||||
/// <summary>Don't wait — the runtime advances to the next action immediately in the same Run call.</summary>
|
||||
NoWait,
|
||||
/// <summary>Pause until the player clicks or presses the accept key. Used by dialog text actions.</summary>
|
||||
WaitClick,
|
||||
/// <summary>Pause until the player selects a choice variant. Used by choice menus.</summary>
|
||||
WaitSelect,
|
||||
/// <summary>Pause for a fixed duration in seconds (<see cref="ActionCommand.WaitTime"/>).</summary>
|
||||
WaitTime,
|
||||
/// <summary>Pause until an external event marks the action as destructed (<see cref="ActionCommand.IsDestructed"/>).</summary>
|
||||
WaitEvent,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public partial class AnswerBox : VerticalListWidget<AnswerItem, SelectVariant>
|
||||
{
|
||||
|
||||
private ICollection<SelectVariant> variants;
|
||||
|
||||
/// <summary>answer_box.tscn binds Content via the content export; the lookup covers instances built without the binding.</summary>
|
||||
protected override void OnReady()
|
||||
{
|
||||
if (Content == null)
|
||||
{
|
||||
var found = FindChild("Content", true, false);
|
||||
if (found != null)
|
||||
Set("content", found);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stores the variants and triggers <see cref="Widget.Show"/> to build and display the answer list.</summary>
|
||||
public void CreateVariantsUI(ICollection<SelectVariant> variants)
|
||||
{
|
||||
this.variants = variants;
|
||||
Show();
|
||||
}
|
||||
|
||||
protected override void HideDestruct()
|
||||
{
|
||||
base.HideDestruct();
|
||||
QueueFree();
|
||||
}
|
||||
|
||||
/// <summary>Returns the stored variants collection. Called by the base <see cref="ListWidget{T, M}.ConstructUI"/> pipeline.</summary>
|
||||
public override ICollection<SelectVariant> CreateModels()
|
||||
{
|
||||
return variants;
|
||||
}
|
||||
|
||||
/// <summary>Deliberate no-op — answer items are positioned by their own layout system, not by the list widget's PutToLayout.</summary>
|
||||
protected override void PutToLayout(AnswerItem item, int index, Control container, Vector2 contentSize)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Fixed-height layout: content height = <paramref name="count"/> * 60px. Width is inherited from the Content container.</summary>
|
||||
public override Vector2 GetContentSize(int count)
|
||||
{
|
||||
return new Vector2(Content.Size.X, count * 60);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public partial class AnswerItem : ListItem<SelectVariant>
|
||||
{
|
||||
|
||||
[Export] private Label textField;
|
||||
|
||||
private DialogBox dialogBox;
|
||||
|
||||
/// <summary>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.</summary>
|
||||
protected override void OnReady()
|
||||
{
|
||||
textField ??= GetNode<Label>("Button/TextLabel");
|
||||
|
||||
var btn = GetNode<Button>("Button");
|
||||
if (btn != null)
|
||||
btn.Pressed += OnClick;
|
||||
|
||||
if (dialogBox == null)
|
||||
{
|
||||
var parent = GetParent();
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent is DialogBox db)
|
||||
{
|
||||
dialogBox = db;
|
||||
break;
|
||||
}
|
||||
parent = parent.GetParent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Binds the variant model and sets the button label text to <see cref="SelectVariant.Text"/>.</summary>
|
||||
public override void Construct(SelectVariant variant)
|
||||
{
|
||||
base.Construct(variant);
|
||||
textField.Text = variant.Text;
|
||||
}
|
||||
|
||||
private void OnClick()
|
||||
{
|
||||
dialogBox?.SelectVariant(Model);
|
||||
}
|
||||
|
||||
/// <summary>Required by <see cref="IListItem{TModel}"/>. Intentionally empty — the parent AnswerBox handles cleanup.</summary>
|
||||
public override void Destruct()
|
||||
{ }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Audio;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
using Cthangover.Mods.Dialog.Executable;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The visible dialog UI: dual avatars with shader-based hide-color mode,
|
||||
/// answer box spawning, and click/keyboard advance. Validates that no existing
|
||||
/// dialog locker is running before accepting a new queue, ensuring only one
|
||||
/// dialog can occupy the box at a time. Integrates with the audio
|
||||
/// system for skip sounds on mouse click. Uses the same Widget lifecycle but
|
||||
/// overrides HideDestruct to reset avatar textures and release the locker.
|
||||
/// The shader material for avatars is resolved through the mod system on _Ready,
|
||||
/// supporting mod-provided avatar effects.
|
||||
/// </summary>
|
||||
public partial class DialogBox : Widget, IDialogBox
|
||||
{
|
||||
|
||||
[Export] private PackedScene answerBoxScene;
|
||||
[Export] private Label textField;
|
||||
[Export] private Label titleField;
|
||||
[Export] private Control titleBody;
|
||||
[Export] private TextureRect firstAvatar;
|
||||
[Export] private TextureRect secondAvatar;
|
||||
[Export] private Texture2D emptyAvatar;
|
||||
|
||||
private IAudioService audioService;
|
||||
private AnswerBox answerBox;
|
||||
private SceneTreeTimer waitTimer;
|
||||
|
||||
/// <summary>Resolves the body, text and title labels, avatars and the audio service, and applies the mod-provided avatar shader.</summary>
|
||||
protected override void OnReady()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
|
||||
if (Body == null)
|
||||
{
|
||||
var found = FindChild("DialogBody", true, false);
|
||||
if (found != null)
|
||||
Set("body", found);
|
||||
}
|
||||
audioService = GetNodeOrNull<Node>("/root/AudioService") as IAudioService;
|
||||
textField ??= GetNode<Label>("DialogBody/TextField");
|
||||
titleField ??= GetNode<Label>("TitleBody/TitleField");
|
||||
titleBody ??= GetNode<Control>("TitleBody");
|
||||
firstAvatar ??= GetNode<TextureRect>("FirstAvatar");
|
||||
secondAvatar ??= GetNode<TextureRect>("SecondAvatar");
|
||||
|
||||
var avatarShader = Shaders.Resolve("avatar");
|
||||
if (avatarShader != null)
|
||||
{
|
||||
if (firstAvatar != null && firstAvatar.Material == null)
|
||||
firstAvatar.Material = new ShaderMaterial { Shader = avatarShader };
|
||||
if (secondAvatar != null && secondAvatar.Material == null)
|
||||
secondAvatar.Material = new ShaderMaterial { Shader = avatarShader };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mutable lock object that prevents concurrent dialogs. Set when a queue starts, cleared when the dialog ends.</summary>
|
||||
public ExecutableEvent Locker { get; set; }
|
||||
/// <summary>The execution engine. Created once at construction and reused for all dialog queues.</summary>
|
||||
public DialogRuntime Runtime { get; } = new();
|
||||
/// <summary>Whether the answer/choice box is currently visible.</summary>
|
||||
public bool IsAnswerBoxShowed => answerBox != null;
|
||||
/// <summary>If false, new dialog queues are rejected. Set externally to prevent dialog during non-dialog scenes.</summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>Called by answer items when the player picks a choice. Hides the answer box and jumps the runtime to the variant's GoTo target.</summary>
|
||||
public void SelectVariant(SelectVariant variant)
|
||||
{
|
||||
if (answerBox != null)
|
||||
{
|
||||
answerBox.Hide();
|
||||
answerBox = null;
|
||||
}
|
||||
|
||||
Runtime.TryGoTo(variant.GoTo);
|
||||
Runtime.Run();
|
||||
ScheduleWaitTimer();
|
||||
}
|
||||
|
||||
/// <summary>Sets the title bar text. Null or empty hides the title bar.</summary>
|
||||
public void SetTitle(string title)
|
||||
{
|
||||
if (title == null) title = string.Empty;
|
||||
titleField.Text = title;
|
||||
titleBody.Visible = title != string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Instantiates the answer_box scene prefab (bound in dialog_box.tscn), adds it as a child, and populates it with the given choice variants.</summary>
|
||||
public void SetVariants(ICollection<SelectVariant> variants)
|
||||
{
|
||||
if (answerBoxScene == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "SetVariants: answerBoxScene is not bound in the dialog_box scene", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var box = answerBoxScene.Instantiate<AnswerBox>();
|
||||
AddChild(box);
|
||||
answerBox = box;
|
||||
answerBox.CreateVariantsUI(variants);
|
||||
}
|
||||
|
||||
/// <summary>Sets the main dialog text. Hides the Body canvas item when text is null or whitespace.</summary>
|
||||
public void SetText(string text)
|
||||
{
|
||||
if (text == null) text = string.Empty;
|
||||
textField.Text = text;
|
||||
var body = Body as CanvasItem;
|
||||
if (body != null)
|
||||
body.Visible = !string.IsNullOrWhiteSpace(text);
|
||||
}
|
||||
|
||||
/// <summary>Sets the left avatar texture. Null hides the slot. <paramref name="hideColor"/> enables shader silhouette mode when true.</summary>
|
||||
public void SetFirstAvatar(Texture2D avatar, bool hideColor = false)
|
||||
{
|
||||
if (avatar == null)
|
||||
{
|
||||
firstAvatar.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
firstAvatar.Texture = avatar;
|
||||
firstAvatar.Visible = true;
|
||||
if (firstAvatar.Material is ShaderMaterial mat)
|
||||
mat.SetShaderParameter("hide_mode", hideColor ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sets the right avatar texture. Null hides the slot. <paramref name="hideColor"/> enables shader silhouette mode when true.</summary>
|
||||
public void SetSecondAvatar(Texture2D avatar, bool hideColor = false)
|
||||
{
|
||||
if (avatar == null)
|
||||
{
|
||||
secondAvatar.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
secondAvatar.Texture = avatar;
|
||||
secondAvatar.Visible = true;
|
||||
if (secondAvatar.Material is ShaderMaterial mat)
|
||||
mat.SetShaderParameter("hide_mode", hideColor ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resets avatars to empty and releases the dialog locker, preparing for the next dialog session.</summary>
|
||||
protected override void HideDestruct()
|
||||
{
|
||||
ClearWaitTimer();
|
||||
firstAvatar.Texture = emptyAvatar;
|
||||
firstAvatar.Visible = false;
|
||||
secondAvatar.Texture = emptyAvatar;
|
||||
secondAvatar.Visible = false;
|
||||
Locker = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads and starts a dialog sequence. Validates that: (1) dialog is not null, (2) this DialogBox is active,
|
||||
/// (3) no existing dialog locker is running. On success, shows the box and delegates to <see cref="DialogRuntime.SetDialogQueueAndRun"/>.
|
||||
/// </summary>
|
||||
public void SetDialogQueueAndRun(DialogQueue dialog, IEnumerable<IActionCommand> endDialogQueue, int startIndex, ExecutableEvent locker)
|
||||
{
|
||||
if (dialog == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"can't run '{locker?.GetType().FullName}', dialog is null", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
if (!IsActive)
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"can't run '{locker?.GetType().FullName}', DialogBox is not active", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Locker != null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"can't run '{locker?.GetType().FullName}', locker already executing!", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
GameLogger.Log("DIALOG", $"SetDialogQueueAndRun: locker='{locker?.GetType().Name}', queue size={dialog?.Queue.Count}");
|
||||
Locker = locker;
|
||||
Show();
|
||||
Runtime.SetDialogQueueAndRun(this, dialog.Queue, endDialogQueue, startIndex);
|
||||
Runtime.Dialog = dialog;
|
||||
Runtime.Run();
|
||||
ScheduleWaitTimer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the dialog to the next action. Skips if the runtime is waiting for an answer or has ended.
|
||||
/// Respects <see cref="WaitType.WaitEvent"/> (waits for destruct) and <see cref="WaitType.WaitTime"/> (waits for elapsed duration).
|
||||
/// Called by click/keyboard input, the wait timer, and self-completing WaitEvent actions.
|
||||
/// </summary>
|
||||
public void NextAction()
|
||||
{
|
||||
if (Runtime.IsWaitAnswer)
|
||||
return;
|
||||
if (Runtime.IsEnd)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "NextAction: dialog ended, hiding");
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var action = Runtime.CurrentAction;
|
||||
if (action == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "NextAction: no current action, hiding");
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
double now = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
if (action.WaitType == WaitType.WaitEvent && !action.IsDestructed)
|
||||
return;
|
||||
if (action.WaitType == WaitType.WaitTime && now - action.StartTime < action.WaitTime)
|
||||
return;
|
||||
|
||||
GameLogger.Log("DIALOG", $"NextAction: advancing past '{action.GetType().Name}' (idx={Runtime.CurrentAction?.GetType().Name})");
|
||||
|
||||
Runtime.Next();
|
||||
Runtime.Run();
|
||||
ScheduleWaitTimer();
|
||||
}
|
||||
|
||||
/// <summary>Advances the dialog on <c>ui_accept</c> or on a click outside the body, unless a choice is pending or the queue has ended.</summary>
|
||||
protected override void OnInput(InputEvent @event)
|
||||
{
|
||||
if (Runtime.IsEnd || Runtime.IsWaitAnswer)
|
||||
return;
|
||||
|
||||
if (@event.IsActionPressed("ui_accept"))
|
||||
{
|
||||
GameLogger.Log("DIALOG", "input: advancing dialog");
|
||||
NextAction();
|
||||
}
|
||||
else if (@event is InputEventMouseButton btn && btn.ButtonIndex == MouseButton.Left && btn.Pressed)
|
||||
{
|
||||
var action = Runtime.CurrentAction;
|
||||
if (action == null || action.WaitType == WaitType.WaitTime || action.WaitType == WaitType.WaitEvent)
|
||||
return;
|
||||
|
||||
if (Body is Control bodyControl)
|
||||
{
|
||||
var bodyRect = bodyControl.GetGlobalRect();
|
||||
if (bodyRect.HasPoint(btn.Position))
|
||||
{
|
||||
GameLogger.Log("DIALOG", "input: advancing dialog");
|
||||
audioService.PlaySound("ui/skip", SoundType.UI);
|
||||
NextAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether a dialog is currently running (Locker is not null).</summary>
|
||||
public bool IsRunning => Locker != null;
|
||||
|
||||
/// <summary>Whether the current dialog queue reached its end (also true before the first queue loads).</summary>
|
||||
public bool IsDialogEnded => Runtime.IsEnd;
|
||||
|
||||
/// <summary>Whether the dialog is waiting for the player to pick a variant.</summary>
|
||||
public bool IsWaitingAnswer => Runtime.IsWaitAnswer;
|
||||
|
||||
/// <summary>Number of answer variants currently shown (0 when the answer box is hidden).</summary>
|
||||
public int AnswerVariantCount => answerBox?.CreateModels()?.Count ?? 0;
|
||||
|
||||
/// <summary>Selects the shown variant at <paramref name="index"/>. Returns false when nothing is shown or the index is out of range.</summary>
|
||||
public bool TrySelectVariant(int index)
|
||||
{
|
||||
var models = answerBox?.CreateModels();
|
||||
if (models == null || models.Count == 0 || index < 0 || index >= models.Count)
|
||||
return false;
|
||||
|
||||
var list = new List<SelectVariant>(models);
|
||||
SelectVariant(list[index]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules a one-shot <see cref="SceneTreeTimer"/> that advances the dialog past the
|
||||
/// current <see cref="WaitType.WaitTime"/> action once its <see cref="ActionCommand.WaitTime"/>
|
||||
/// elapses. Replaces the former per-frame polling: the runtime now yields until the timer
|
||||
/// fires exactly once, so no Update work happens between delays.
|
||||
/// </summary>
|
||||
private void ScheduleWaitTimer()
|
||||
{
|
||||
ClearWaitTimer();
|
||||
|
||||
if (!Runtime.IsWaitTime)
|
||||
return;
|
||||
|
||||
var wait = Runtime.CurrentAction?.WaitTime ?? 0f;
|
||||
if (wait <= 0f)
|
||||
{
|
||||
NextAction();
|
||||
return;
|
||||
}
|
||||
|
||||
waitTimer = GetTree().CreateTimer(wait);
|
||||
waitTimer.Timeout += OnWaitTimerTimeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the delay timer elapses. The timer already guarantees the wait has
|
||||
/// elapsed, so the runtime is advanced directly — no wall-clock re-check. The
|
||||
/// <see cref="Runtime.IsWaitTime"/> guard skips a stale timer that fired after the
|
||||
/// dialog already moved on.
|
||||
/// </summary>
|
||||
private void OnWaitTimerTimeout()
|
||||
{
|
||||
waitTimer = null;
|
||||
|
||||
if (!Runtime.IsWaitTime)
|
||||
return;
|
||||
|
||||
Runtime.Next();
|
||||
Runtime.Run();
|
||||
ScheduleWaitTimer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a pending delay timer. Called before scheduling a new one and when the dialog
|
||||
/// hides, so a stale timer can never advance a later, unrelated dialog session.
|
||||
/// </summary>
|
||||
private void ClearWaitTimer()
|
||||
{
|
||||
if (waitTimer == null)
|
||||
return;
|
||||
|
||||
waitTimer.Timeout -= OnWaitTimerTimeout;
|
||||
waitTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Thin wrapper node that hosts the dialog UI. The canvas instantiates its
|
||||
/// <see cref="DialogBox"/> child at runtime from the core mod through
|
||||
/// <see cref="ModScenePrefab"/> and keeps the well-known node name
|
||||
/// <c>DialogBox</c>. The rest of the game locates it by that name via
|
||||
/// <see cref="SceneContextNode.FindNode{T}"/>, not by hardcoded paths.
|
||||
/// </summary>
|
||||
public partial class DialogCanvas : Control
|
||||
{
|
||||
private static readonly ModScenePrefab DialogBoxPrefab = new("scenes/ui/dialog_box.tscn");
|
||||
|
||||
private DialogBox dialogBox;
|
||||
|
||||
/// <summary>The active DialogBox child, resolved in _Ready. Used by the scene tree root to access dialog without path traversal.</summary>
|
||||
public DialogBox DialogBox => dialogBox;
|
||||
|
||||
/// <summary>Instantiates the <see cref="DialogBox"/> child from the core prefab when the scene does not ship one, keeping the well-known node name for path-free lookup.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
dialogBox ??= GetNodeOrNull<DialogBox>("DialogBox");
|
||||
if (dialogBox == null)
|
||||
{
|
||||
dialogBox = DialogBoxPrefab.Instantiate<DialogBox>();
|
||||
if (dialogBox != null)
|
||||
{
|
||||
dialogBox.Name = "DialogBox";
|
||||
AddChild(dialogBox);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("DIALOG", "DialogCanvas._Ready: failed to build 'scenes/ui/dialog_box.tscn'", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
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.Mods.Effects;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Interactive;
|
||||
|
||||
namespace Cthangover.Mods.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Fluent builder for dialog sequences. Each method creates an action of the
|
||||
/// corresponding type, appends it to the Queue, and returns the action for
|
||||
/// optional further configuration. The API vocabulary mirrors the scenario DSL
|
||||
/// commands (Text, Title, Background, Music, Sound, Delay, GoTo, If, Select,
|
||||
/// SwitchScene, etc.), making the C# API feel like a script. RuntimeObjectList
|
||||
/// is a separate list of IActionDestruct objects that must be cleaned up when
|
||||
/// the dialog ends but are not part of the sequential queue (e.g. spawned
|
||||
/// resources, scheduled callbacks).
|
||||
/// </summary>
|
||||
public class DialogQueue
|
||||
{
|
||||
|
||||
/// <summary>Objects that need <see cref="IActionDestruct.Destruct"/> cleanup when the dialog ends but are not part of the sequential queue (e.g. spawned resources).</summary>
|
||||
public List<IActionDestruct> RuntimeObjectList { get; } = new();
|
||||
/// <summary>The ordered sequence of dialog actions. Appended via fluent builder methods; consumed by <see cref="DialogRuntime"/>.</summary>
|
||||
public List<IActionCommand> Queue { get; } = new();
|
||||
|
||||
/// <summary>Spawns a visual effect identified by <paramref name="effectID"/> through <see cref="EffectFactory"/>. Returns the action for further configuration.</summary>
|
||||
public ActionEffect Effect(string effectID)
|
||||
{
|
||||
var action = new ActionEffect { EffectID = effectID };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Creates a frame-animation foreground from a base sprite path, generating frame IDs from <paramref name="start"/> to <paramref name="start"/> + <paramref name="count"/>.</summary>
|
||||
public ActionAnimatedForeground Animation(string sprite, int start, int count, float speed = 1, float nextFrameSpeed = 1, bool isLoop = true)
|
||||
{
|
||||
var list = new List<string>();
|
||||
for (int i = start; i <= start + count; i++)
|
||||
list.Add(sprite + $"{i:00}");
|
||||
return Animation(list, speed, nextFrameSpeed, isLoop);
|
||||
}
|
||||
|
||||
/// <summary>Creates a frame-animation foreground from a pre-built list of sprite resource paths.</summary>
|
||||
public ActionAnimatedForeground Animation(List<string> sprites, float speed = 1, float nextFrameSpeed = 1, bool isLoop = true)
|
||||
{
|
||||
var action = new ActionAnimatedForeground { SpriteIDs = sprites, Speed = speed, NextFrameSpeed = nextFrameSpeed, IsLoop = isLoop };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets the scene background from a sprite ID resolved through <see cref="BackgroundFactory"/>. Also loads depth and albedo maps for lighting.</summary>
|
||||
public ActionBackground Background(string spriteId)
|
||||
{
|
||||
var action = new ActionBackground { SpriteID = spriteId };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets the scene background directly from a pre-loaded <see cref="Texture2D"/>. Bypasses factory resolution.</summary>
|
||||
public ActionBackground BackgroundTexture(Texture2D texture)
|
||||
{
|
||||
var action = new ActionBackground { Texture = texture };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets the scene foreground from a sprite ID resolved through <see cref="BackgroundFactory"/>.</summary>
|
||||
public ActionForeground Foreground(string spriteId)
|
||||
{
|
||||
var action = new ActionForeground { SpriteID = spriteId };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets the scene foreground directly from a pre-loaded <see cref="Texture2D"/>.</summary>
|
||||
public ActionForeground ForegroundTexture(Texture2D texture)
|
||||
{
|
||||
var action = new ActionForeground { Texture = texture };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Displays dialog text with optional avatars. Pauses for click before advancing.</summary>
|
||||
public ActionText Text(string text, string first = null, string second = null, bool hideColor = false)
|
||||
{
|
||||
var action = new ActionText { Text = text, FirstAvatar = first, SecondAvatar = second, HideColor = hideColor };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="Text"/> but enables runtime variable substitution (${var}) on the text content.</summary>
|
||||
public ActionText PText(string text, string first = null, string second = null, bool hideColor = false)
|
||||
{
|
||||
var action = new ActionText { Text = text, FirstAvatar = first, SecondAvatar = second, HideColor = hideColor, UseProcessText = true };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets the dialog title bar. Non-null shows it; null hides.</summary>
|
||||
public ActionTitle Title(string title)
|
||||
{
|
||||
var action = new ActionTitle { TitleText = title };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Explicitly hides the title bar by setting TitleText to null.</summary>
|
||||
public ActionTitle HideTitle()
|
||||
{
|
||||
var action = new ActionTitle { TitleText = null };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets a runtime variable by name and literal value. Available to subsequent actions via ${name} syntax.</summary>
|
||||
public ActionSet Set(string name, string value)
|
||||
{
|
||||
var action = new ActionSet { Name = name, Value = value };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets a runtime variable by name, using a lazy-evaluated callback for dynamic values not known at script authoring time.</summary>
|
||||
public ActionSet Set(string name, Func<string> callback)
|
||||
{
|
||||
var action = new ActionSet { Name = name, Callback = callback };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Presents player choices. Each <see cref="SelectVariant"/> has display text and a GoTo target. Dialog pauses until a choice is picked.</summary>
|
||||
public ActionSelect Select(List<SelectVariant> variants, string text = null, string first = null, string second = null)
|
||||
{
|
||||
var action = new ActionSelect { Variants = variants, Text = text, FirstAvatar = first, SecondAvatar = second };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Terminates the dialog queue, calling <see cref="DialogRuntime.End"/>.</summary>
|
||||
public ActionEnd End()
|
||||
{
|
||||
var action = new ActionEnd();
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>A labeled no-op. Used as a named jump target for GoTo/If commands.</summary>
|
||||
public ActionEmpty Empty()
|
||||
{
|
||||
var action = new ActionEmpty();
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Creates a named empty action (a "point") identified by <paramref name="id"/> for GoTo targeting.</summary>
|
||||
public ActionEmpty Point(string id)
|
||||
{
|
||||
return (ActionEmpty)Empty().SetID(id);
|
||||
}
|
||||
|
||||
/// <summary>Starts background music playback via <see cref="IAudioService"/> using <see cref="MusicType.Ambient"/> channel.</summary>
|
||||
public ActionMusic Music(string music)
|
||||
{
|
||||
var action = new ActionMusic { Music = music };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Pauses background music without stopping the track. Resume with <see cref="MusicPlay"/>.</summary>
|
||||
public ActionMusicPause MusicPause()
|
||||
{
|
||||
var action = new ActionMusicPause();
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Resumes previously paused background music.</summary>
|
||||
public ActionMusicPlay MusicPlay()
|
||||
{
|
||||
var action = new ActionMusicPlay();
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Waits for <paramref name="waitTime"/> seconds while optionally displaying a text message via <paramref name="showText"/>.</summary>
|
||||
public ActionDelay Delay(float waitTime, string showText)
|
||||
{
|
||||
var action = new ActionDelay { WaitTime = waitTime, HiddenMode = false, ShowText = showText };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Waits for <paramref name="waitTime"/> seconds. When <paramref name="hiddenMode"/> is true, hides the dialog body for the duration (dramatic pauses, cutscene beats).</summary>
|
||||
public ActionDelay Delay(float waitTime, bool hiddenMode = false)
|
||||
{
|
||||
var action = new ActionDelay { WaitTime = waitTime, HiddenMode = hiddenMode, ShowText = null };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Plays a UI sound effect through <see cref="IAudioService"/>. Non-blocking — the dialog continues immediately.</summary>
|
||||
public ActionSound Sound(string sound)
|
||||
{
|
||||
var action = new ActionSound { Sound = sound };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Invokes an external scenario action by name, resolved through <see cref="ScenarioActionFactory"/>.</summary>
|
||||
public ActionScenario Action(string actionName)
|
||||
{
|
||||
var action = new ActionScenario { ActionName = actionName };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Runs arbitrary C# code as a dialog action. Useful for quest state changes or UI toggles without a dedicated action class.</summary>
|
||||
public ActionRun Run(System.Action executable)
|
||||
{
|
||||
var action = new ActionRun { Executable = executable };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Evaluates a condition and jumps to <paramref name="trueGoTo"/> or <paramref name="falseGoTo"/> based on the result. Falls through if the chosen target is empty.</summary>
|
||||
public ActionIf IfGoTo(Func<bool> condition, string trueGoTo, string falseGoTo = null)
|
||||
{
|
||||
var action = new ActionIf { Condition = condition, TrueGoTo = trueGoTo, FalseGoTo = falseGoTo };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Unconditionally jumps to the action identified by <paramref name="goTo"/> in the queue.</summary>
|
||||
public ActionGoTo GoTo(string goTo)
|
||||
{
|
||||
var action = new ActionGoTo { GoTo = goTo };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Deferred scene switch via <see cref="SceneManager"/>. Sets the locker as OneRun to prevent re-triggering after scene reload.</summary>
|
||||
public ActionSwitchScene SwitchScene(string targetScene, float speed = 4f, bool hiddenMode = true)
|
||||
{
|
||||
var action = new ActionSwitchScene { SceneName = targetScene, HiddenMode = hiddenMode, Speed = speed };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Declares a background tint color to be consumed by background-rendering nodes.</summary>
|
||||
public ActionBackgroundColor BackgroundColor(Color color)
|
||||
{
|
||||
var action = new ActionBackgroundColor { Color = color };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Makes the dialog box visible. Paired with <see cref="HideDialog"/> for cutscenes that temporarily hide dialog chrome.</summary>
|
||||
public ActionShowDialog ShowDialog()
|
||||
{
|
||||
var action = new ActionShowDialog();
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Hides the dialog box. Paired with <see cref="ShowDialog"/> for cutscene transitions.</summary>
|
||||
public ActionHideDialog HideDialog()
|
||||
{
|
||||
var action = new ActionHideDialog();
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Toggles whether lighting responds to in-game time of day (true) or uses static lights (false).</summary>
|
||||
public ActionLightUseTime LightUseTime(bool useTime)
|
||||
{
|
||||
var action = new ActionLightUseTime { UseTime = useTime };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Applies static light definitions from a JSON string to the light controller. Empty string clears all static lights.</summary>
|
||||
public ActionLightSet LightSet(string json)
|
||||
{
|
||||
var action = new ActionLightSet { LightsJson = json };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Fade the dialog background in (<see cref="BackgroundActionType.Show"/>) or out (<see cref="BackgroundActionType.Hide"/>). Optionally set <paramref name="wait"/> to pause the dialog during the transition.</summary>
|
||||
public ActionBackgroundShowHide BackgroundShowHide(BackgroundActionType type, float duration, bool wait = false)
|
||||
{
|
||||
var action = new ActionBackgroundShowHide { ActionType = type, Duration = duration };
|
||||
if (wait)
|
||||
{
|
||||
action.WaitType = WaitType.WaitTime;
|
||||
action.WaitTime = duration;
|
||||
}
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Adds an interactive element to the scene by definition ID.</summary>
|
||||
public ActionInteractiveAdd InteractiveAdd(string definitionId)
|
||||
{
|
||||
var action = new ActionInteractiveAdd { DefinitionId = definitionId };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Removes an interactive element from the scene by definition ID.</summary>
|
||||
public ActionInteractiveRemove InteractiveRemove(string definitionId)
|
||||
{
|
||||
var action = new ActionInteractiveRemove { DefinitionId = definitionId };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Clears all interactive elements from the scene.</summary>
|
||||
public ActionInteractiveClear InteractiveClear()
|
||||
{
|
||||
var action = new ActionInteractiveClear();
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets a property on an interactive element identified by definition ID.</summary>
|
||||
public ActionInteractiveSet InteractiveSet(string definitionId, string property, string value)
|
||||
{
|
||||
var action = new ActionInteractiveSet { DefinitionId = definitionId, Property = property, Value = value };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Cthangover.Core.UI.Event;
|
||||
using Cthangover.Mods.Dialog.Executable;
|
||||
using Cthangover.Core.Utils;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Dialog execution engine: manages the action queue with goto-based branching
|
||||
/// and variable substitution. Run() iterates actions in a tight loop, breaking
|
||||
/// only on wait points (WaitClick/Select/Time/Event). Text variables use the
|
||||
/// "${name}" syntax and are resolved via GetVariable/SetVariable — values are
|
||||
/// stored in a simple Dictionary, not a cascading scope, keeping lookups O(1).
|
||||
/// GoTo searches the queue linearly by action ID and jumps the index; the
|
||||
/// source action's destruct fires on jump. The isLastGoTo flag prevents double
|
||||
/// advancement when a GoTo is the last action in its branch. End() processes
|
||||
/// an optional cleanup queue (endDialogQueue) before tearing down, and raises
|
||||
/// the dialog-end event via the SceneEventController for chaining.
|
||||
/// Infinite loop protection caps at 100k iterations per Run call.
|
||||
/// </summary>
|
||||
public class DialogRuntime
|
||||
{
|
||||
|
||||
private List<IActionCommand> dialogQueue;
|
||||
private List<IActionCommand> endDialogQueue;
|
||||
private int goToActionIndex;
|
||||
private int index;
|
||||
private bool isLastGoTo;
|
||||
private IDictionary<string, string> variables = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>The dialog box instance this runtime drives. Set when a new queue is loaded.</summary>
|
||||
public DialogBox DialogBox { get; private set; }
|
||||
/// <summary>The currently active dialog queue. Set by <see cref="SetDialogQueueAndRun"/>.</summary>
|
||||
public DialogQueue Dialog { get; set; }
|
||||
|
||||
/// <summary>The action at the current queue index. Automatically constructs it if <see cref="ConstructType"/> is OnStartAction and it hasn't been constructed yet.</summary>
|
||||
public IActionCommand CurrentAction => GetByIndex(index);
|
||||
|
||||
/// <summary>True when the current action is waiting for a player choice (<see cref="WaitType.WaitSelect"/>).</summary>
|
||||
public bool IsWaitAnswer => IsWaitType(WaitType.WaitSelect);
|
||||
/// <summary>True when the current action is waiting for a timer (<see cref="WaitType.WaitTime"/>).</summary>
|
||||
public bool IsWaitTime => IsWaitType(WaitType.WaitTime);
|
||||
/// <summary>True when the current action is waiting for an external event (<see cref="WaitType.WaitEvent"/>).</summary>
|
||||
public bool IsWaitEvent => IsWaitType(WaitType.WaitEvent);
|
||||
|
||||
/// <summary>True when the dialog queue is exhausted (index past end or queue is empty).</summary>
|
||||
public bool IsEnd => Lists.IsEmpty(dialogQueue) || index >= dialogQueue.Count;
|
||||
|
||||
/// <summary>Retrieves a stored variable value by name. Returns null if not set.</summary>
|
||||
public string GetVariable(string name)
|
||||
{
|
||||
variables.TryGetValue(name, out var val);
|
||||
return val;
|
||||
}
|
||||
|
||||
/// <summary>Stores a variable for later substitution via ${name} syntax. Overwrites existing values.</summary>
|
||||
public void SetVariable(string name, string value)
|
||||
{
|
||||
variables[name] = value;
|
||||
}
|
||||
|
||||
/// <summary>Scans <paramref name="text"/> for ${key} patterns and replaces them with stored variable values. Unresolved keys become "?".</summary>
|
||||
public string ProcessText(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return null;
|
||||
|
||||
var vars = text.GetIncludesInQuotes("${", "}");
|
||||
if (Lists.IsNotEmpty(vars))
|
||||
foreach (var variable in vars)
|
||||
{
|
||||
if (!variables.TryGetValue(variable, out var val))
|
||||
val = "?";
|
||||
text = text.Replace("${" + variable + "}", val);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private bool IsWaitType(WaitType type)
|
||||
{
|
||||
if (Lists.IsEmpty(dialogQueue))
|
||||
return false;
|
||||
if (index < 0 || index >= dialogQueue.Count)
|
||||
return false;
|
||||
var action = dialogQueue[index];
|
||||
return action != null && action.WaitType == type;
|
||||
}
|
||||
|
||||
private IActionCommand GetByIndex(int idx)
|
||||
{
|
||||
if (Lists.IsEmpty(dialogQueue))
|
||||
return null;
|
||||
if (idx < 0 || idx >= dialogQueue.Count)
|
||||
return null;
|
||||
var action = dialogQueue[idx];
|
||||
if (action != null && action.ConstructType == ConstructType.OnStartAction && !action.IsConstructed)
|
||||
action.Construct();
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a new dialog queue and prepares for execution. Resets variables, sets the dialog box reference,
|
||||
/// ends any previous queue (destructing its actions), copies the new queue, and constructs OnStartQueue actions.
|
||||
/// Note: does NOT call Run() — the caller must call Run() separately.
|
||||
/// </summary>
|
||||
public void SetDialogQueueAndRun(DialogBox dialogBox,
|
||||
IEnumerable<IActionCommand> queue,
|
||||
IEnumerable<IActionCommand> endQueue,
|
||||
int startIndex = 0)
|
||||
{
|
||||
variables = new Dictionary<string, string>();
|
||||
DialogBox = dialogBox;
|
||||
|
||||
this.endDialogQueue = endQueue == null ? null : endQueue.ToList();
|
||||
if (Lists.IsNotEmpty(this.dialogQueue))
|
||||
End();
|
||||
|
||||
this.dialogQueue = queue.ToList();
|
||||
|
||||
index = startIndex;
|
||||
foreach (var action in this.dialogQueue)
|
||||
if (action != null && action.ConstructType == ConstructType.OnStartQueue && !action.IsConstructed)
|
||||
action.Construct();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches the queue linearly for an action with ID == <paramref name="actionID"/> and jumps the index there.
|
||||
/// Destructs the source action before jumping. If not found, calls <see cref="End"/>.
|
||||
/// <paramref name="lastGoTo"/> prevents the runtime from auto-advancing past the target when the GoTo is the last action in its branch.
|
||||
/// </summary>
|
||||
public void TryGoTo(string actionID, bool lastGoTo = false)
|
||||
{
|
||||
goToActionIndex = index;
|
||||
|
||||
if (string.IsNullOrEmpty(actionID))
|
||||
return;
|
||||
|
||||
for (var i = 0; i < dialogQueue.Count; i++)
|
||||
{
|
||||
var action = dialogQueue[i];
|
||||
if (action?.ID == actionID)
|
||||
{
|
||||
index = i;
|
||||
var actionGoTo = GetByIndex(goToActionIndex);
|
||||
if (actionGoTo != null && actionGoTo.IsConstructed && actionGoTo.DestructType == DestructType.OnEndAction)
|
||||
actionGoTo.Destruct();
|
||||
this.isLastGoTo = lastGoTo;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
GameLogger.Log("DIALOG", "Dialog goto action not found!", LogLevel.Error);
|
||||
|
||||
End();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main execution loop. Iterates through actions, calling <see cref="IActionCommand.Run"/> on each and advancing
|
||||
/// automatically for <see cref="WaitType.NoWait"/> actions. Stops the loop on any wait-type action (WaitClick, WaitSelect,
|
||||
/// WaitTime, WaitEvent) so the runtime yields control until the condition is satisfied. Protected against infinite loops
|
||||
/// with a 100,000-iteration cap.
|
||||
/// </summary>
|
||||
public void Run()
|
||||
{
|
||||
var iteration = 0;
|
||||
for (;;)
|
||||
{
|
||||
var action = CurrentAction;
|
||||
if (action == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "Run: action is null, ending", LogLevel.Debug);
|
||||
|
||||
End();
|
||||
return;
|
||||
}
|
||||
|
||||
GameLogger.Log("DIALOG", $"Run: idx={index}/{dialogQueue.Count-1} type={action.GetType().Name} wait={action.WaitType}", LogLevel.Debug);
|
||||
|
||||
action.Run(this);
|
||||
|
||||
if (iteration++ > 100000)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "Run: infinite loop protection (100k iterations)", LogLevel.Warning);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (action.WaitType)
|
||||
{
|
||||
case WaitType.WaitClick:
|
||||
case WaitType.WaitSelect:
|
||||
case WaitType.WaitTime:
|
||||
case WaitType.WaitEvent:
|
||||
break;
|
||||
case WaitType.NoWait:
|
||||
Next();
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the queue index by one. Destructs the current action if its <see cref="DestructType"/> is OnEndAction.
|
||||
/// Processes any delayed-destruct targets registered by the current action. Calls <see cref="End"/> if the queue is exhausted.
|
||||
/// Respects <see cref="isLastGoTo"/> — if a GoTo was the final action, the index is not incremented.
|
||||
/// </summary>
|
||||
public void Next()
|
||||
{
|
||||
if (isLastGoTo)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "Next: last goto, returning");
|
||||
isLastGoTo = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var action = CurrentAction;
|
||||
if (action != null && action.IsConstructed && action.DestructType == DestructType.OnEndAction)
|
||||
action.Destruct();
|
||||
|
||||
if (action != null && Lists.IsNotEmpty(dialogQueue) && Lists.IsNotEmpty(action.DelayedDestruct))
|
||||
foreach (var item in dialogQueue)
|
||||
if (action.DelayedDestruct.Contains(item.ID))
|
||||
item.Destruct();
|
||||
|
||||
if (Lists.IsEmpty(dialogQueue) || index < 0 || index >= dialogQueue.Count)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "Next: queue exhausted, ending");
|
||||
End();
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the dialog queue. Destructs all remaining constructed actions, hides the dialog box, processes the
|
||||
/// end-dialog cleanup queue (if any), and raises the dialog-end event via <see cref="SceneEventController"/>.
|
||||
/// Guards against end-queue actions starting a new dialog mid-cleanup by checking if the queue changed.
|
||||
/// </summary>
|
||||
public void End()
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"End: {dialogQueue?.Count ?? 0} actions in queue, {(endDialogQueue?.Count ?? 0)} end actions");
|
||||
|
||||
if (Lists.IsNotEmpty(dialogQueue))
|
||||
{
|
||||
foreach (var action in dialogQueue)
|
||||
if (action != null && action.IsConstructed)
|
||||
action.Destruct();
|
||||
dialogQueue.Clear();
|
||||
}
|
||||
|
||||
ExecutableEvent locker = null;
|
||||
if (DialogBox != null)
|
||||
{
|
||||
locker = DialogBox.Locker;
|
||||
TrackOneRunCompletion(locker);
|
||||
DialogBox.Locker = null;
|
||||
DialogBox.Hide();
|
||||
}
|
||||
|
||||
if (Lists.IsNotEmpty(endDialogQueue))
|
||||
{
|
||||
GameLogger.Log("DIALOG", "End: processing end actions...");
|
||||
var oldQueue = dialogQueue;
|
||||
foreach (var action in endDialogQueue)
|
||||
{
|
||||
if (action == null || action.IsDestructed)
|
||||
continue;
|
||||
if (!action.IsConstructed) action.Construct();
|
||||
action.Run(this);
|
||||
action.Destruct();
|
||||
}
|
||||
endDialogQueue.Clear();
|
||||
|
||||
if (dialogQueue != oldQueue)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "End: new dialog started during end actions, skipping cleanup", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dialogQueue = null;
|
||||
endDialogQueue = null;
|
||||
index = 0;
|
||||
|
||||
if (DialogBox != null)
|
||||
{
|
||||
ModEventBus.Publish(new DialogEndedEvent { LockerId = locker?.ID });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a finished one-run scenario in the per-game completion
|
||||
/// history (<see cref="RuntimeData.CompletedScenarioIds"/>) so the
|
||||
/// scenario runner never re-triggers it on later visits to the
|
||||
/// scene or after save/load. Only <see cref="ScenarioEvent"/> with
|
||||
/// an explicit ScenarioId is tracked: its ID matches the scenario
|
||||
/// definition name used by the runner's filters, whereas generic
|
||||
/// lockers (e.g. the ones flagged OneRun by ActionSwitchScene) carry
|
||||
/// type-name IDs that would poison the history.
|
||||
/// </summary>
|
||||
private static void TrackOneRunCompletion(ExecutableEvent locker)
|
||||
{
|
||||
if (!(locker is ScenarioEvent scenario) || !scenario.IsOneRun || string.IsNullOrEmpty(scenario.ScenarioId))
|
||||
return;
|
||||
|
||||
var runtime = GameData.Instance?.Runtime;
|
||||
if (runtime != null && runtime.CompletedScenarioIds.Add(scenario.ID))
|
||||
GameLogger.Log("DIALOG", $"One-run scenario '{scenario.ID}' marked as completed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Cthangover.Core.UI.Event;
|
||||
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.Executable;
|
||||
using Cthangover.Core.Scenarios;
|
||||
using Cthangover.Core.Interactive;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Executable
|
||||
{
|
||||
/// <summary>
|
||||
/// Base for auto-triggering dialog events. A standalone event (not inside an
|
||||
/// IExecutableEventChain) only re-fires when the condition state changes
|
||||
/// (bumped <see cref="ConditionStateMonitor.Version"/>), after its own running
|
||||
/// dialog ends, or on the first run — with a 0.5s debounce. Checks that the
|
||||
/// DialogBox is free (IsRunning == false) before building and executing a
|
||||
/// dialog queue. IsOneRun prevents re-triggering after first execution;
|
||||
/// CanRepeat allows the event to fire again on subsequent scenes/visits.
|
||||
/// CheckConditions is virtual — ScenarioEvent overrides it to evaluate script
|
||||
/// conditions.
|
||||
/// </summary>
|
||||
public partial class ExecutableEvent : Node, IOnUpdateEvent, IExecutableEvent
|
||||
{
|
||||
protected DialogBox dialogBox;
|
||||
private SceneEventController eventController;
|
||||
private float timestamp;
|
||||
|
||||
// Version at which the event last ran (MinValue forces the first run).
|
||||
private long _lastRunVersion = long.MinValue;
|
||||
// "Waiting for own dialog to end" flag, set after starting a run.
|
||||
private bool _awaitingOwnEnd;
|
||||
|
||||
/// <summary>
|
||||
/// <c>true</c> until <see cref="RunDialog"/> is called for the first time.
|
||||
/// Used by chains to determine whether a one-shot event has already fired.
|
||||
/// </summary>
|
||||
public bool IsFirstRun { get; private set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// If <c>true</c>, the event fires exactly once and never again, even across
|
||||
/// scene revisits. After <see cref="RunDialog"/> completes, <see cref="IsFirstRun"/>
|
||||
/// becomes <c>false</c>.
|
||||
/// </summary>
|
||||
[Export] public bool IsOneRun { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// If <c>true</c>, the event can re-trigger on subsequent scene visits.
|
||||
/// When <c>false</c>, the event only fires on the first visit to the scene.
|
||||
/// </summary>
|
||||
[Export] public bool CanRepeat { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Virtual guard evaluated before triggering. Defaults to <c>true</c>.
|
||||
/// Override to implement conditional logic (e.g. <see cref="ScenarioEvent"/>
|
||||
/// checks script conditions via <see cref="ScenarioCondition.Evaluate"/>).
|
||||
/// </summary>
|
||||
public virtual bool CheckConditions => true;
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier for this event instance. Defaults to the full type name.
|
||||
/// Override to provide a stable, save-friendly ID.
|
||||
/// </summary>
|
||||
public virtual string ID => GetType().FullName;
|
||||
|
||||
/// <summary>
|
||||
/// Priority within the event chain — lower values execute first. Default
|
||||
/// is <c>1</c>, placing this after high-priority handlers.
|
||||
/// </summary>
|
||||
public int Priority => 1;
|
||||
|
||||
/// <summary>Finds the shared <see cref="DialogBox"/> and registers with the event controller for update polling unless a parent chain already drives this event.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
if (dialogBox == null)
|
||||
{
|
||||
var root = GetTree()?.Root;
|
||||
if (root != null)
|
||||
{
|
||||
dialogBox = SceneContextNode.FindNode<DialogBox>("DialogBox");
|
||||
}
|
||||
}
|
||||
|
||||
var isInChain = GetParent() is IExecutableEventChain;
|
||||
if (!isInChain)
|
||||
{
|
||||
eventController = SceneContextNode.FindNode<SceneEventController>("EventController");
|
||||
eventController?.AddUpdateEventListener(this);
|
||||
}
|
||||
|
||||
GameLogger.Log("EVENT", $"{GetType().Name}._Ready: dialogBox={(dialogBox != null ? "found" : "null")}, chain={isInChain}, registered={!isInChain}", LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>Detaches from the event controller's update polling when the event leaves the tree.</summary>
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (eventController != null)
|
||||
eventController.RemoveUpdateEventListener(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debounced poll (twice per second) that re-checks the repeat guards and
|
||||
/// the <c>ConditionStateMonitor</c> version, then triggers the dialog when
|
||||
/// the scene state changed since the last run.
|
||||
/// </summary>
|
||||
public void OnUpdate()
|
||||
{
|
||||
if (!IsFirstRun && !CanRepeat)
|
||||
return;
|
||||
|
||||
if (dialogBox == null)
|
||||
return;
|
||||
|
||||
if (_awaitingOwnEnd)
|
||||
{
|
||||
if (dialogBox.IsRunning)
|
||||
return;
|
||||
_awaitingOwnEnd = false;
|
||||
_lastRunVersion = ConditionStateMonitor.Version;
|
||||
}
|
||||
|
||||
if (ConditionStateMonitor.Version == _lastRunVersion)
|
||||
return;
|
||||
|
||||
var time = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
if (time - timestamp < 0.5f)
|
||||
return;
|
||||
timestamp = (float)time;
|
||||
|
||||
if (dialogBox.IsRunning)
|
||||
return;
|
||||
|
||||
if (RunDialog())
|
||||
_awaitingOwnEnd = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="DialogQueue"/> via <see cref="CreateDialog"/> and queues
|
||||
/// it on the <see cref="DialogBox"/>, passing <c>this</c> as the locker to
|
||||
/// block other dialog attempts. Returns <c>false</c> if the one-shot already
|
||||
/// ran, the dialog box is unavailable, or another locker is active.
|
||||
/// </summary>
|
||||
public bool RunDialog()
|
||||
{
|
||||
if (!IsFirstRun && IsOneRun)
|
||||
return false;
|
||||
|
||||
if (dialogBox == null)
|
||||
return false;
|
||||
|
||||
if (dialogBox.Locker != null)
|
||||
return false;
|
||||
|
||||
GameLogger.Log("EVENT", $"{GetType().Name}.RunDialog: starting dialog...");
|
||||
|
||||
var dlg = new DialogQueue();
|
||||
CreateDialog(dlg);
|
||||
|
||||
dialogBox.SetDialogQueueAndRun(dlg, null, 0, this);
|
||||
IsFirstRun = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual void CreateDialog(DialogQueue dlg)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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.Executable
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract chain base: manages a list of ExecutableEvents and exposes
|
||||
/// Play()/Stop()/IsActive for external control. GetNext() is abstract so
|
||||
/// subclasses define their own selection strategy (sequential, conditional,
|
||||
/// random). Play() calls RunDialog() on the next event, which internally
|
||||
/// calls SetDialogQueueAndRun with `this` as the locker — the chain is
|
||||
/// blocked until the dialog finishes.
|
||||
/// </summary>
|
||||
public abstract partial class ExecutableEventChainBase : Godot.Node, IExecutableEventChain
|
||||
{
|
||||
|
||||
protected bool isActive = true;
|
||||
protected System.Collections.Generic.List<ExecutableEvent> chain;
|
||||
|
||||
protected DialogBox dialogBox;
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether the chain is actively polling. Setting to <c>false</c>
|
||||
/// pauses event dispatch; setting to <c>true</c> resumes.
|
||||
/// </summary>
|
||||
public bool IsActive
|
||||
{
|
||||
get => isActive;
|
||||
set { isActive = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses the chain. No further events will be dispatched until
|
||||
/// <see cref="Play"/> is called.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the chain and immediately attempts to run the next event
|
||||
/// returned by <see cref="GetNext"/>. If no event is available, the
|
||||
/// chain deactivates itself.
|
||||
/// </summary>
|
||||
public void Play()
|
||||
{
|
||||
IsActive = true;
|
||||
var next = GetNext();
|
||||
if (next == null)
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
next.RunDialog();
|
||||
}
|
||||
|
||||
protected abstract ExecutableEvent GetNext();
|
||||
|
||||
/// <summary>Interface implementation: casts to <see cref="ExecutableEvent"/> and registers it with the chain node.</summary>
|
||||
public virtual void AddEvent(IExecutableEvent evt)
|
||||
{
|
||||
var typed = (ExecutableEvent)evt;
|
||||
AddChild(typed);
|
||||
chain ??= new System.Collections.Generic.List<ExecutableEvent>();
|
||||
chain.Add(typed);
|
||||
}
|
||||
|
||||
/// <summary>Interface implementation: frees every registered event and empties the chain list.</summary>
|
||||
public virtual void ClearEvents()
|
||||
{
|
||||
if (chain == null)
|
||||
return;
|
||||
foreach (var evt in chain)
|
||||
evt.QueueFree();
|
||||
chain.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using Cthangover.Core.Scenes;
|
||||
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.Interactive;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Executable
|
||||
{
|
||||
/// <summary>
|
||||
/// Sequential list-based chain: rescans GetNext() only when the condition
|
||||
/// state changes (bumped <see cref="ConditionStateMonitor.Version"/>), on the
|
||||
/// end of its own running dialog, or on the first scan — with a debounce.
|
||||
/// When an event runs, it's removed from the list first, so it's consumed and
|
||||
/// the chain shrinks until empty (no self-loop is possible, so no skip-guard
|
||||
/// is needed). Respects IsOneRun (skips already-run events) and
|
||||
/// CheckConditions (skips events whose conditions fail). Use for ordered
|
||||
/// event sequences where each event should play once in order.
|
||||
/// </summary>
|
||||
public partial class ExecutableEventListChain : ExecutableEventChainBase
|
||||
{
|
||||
|
||||
private float checkDelay = 1f;
|
||||
private float timestamp;
|
||||
|
||||
private long _lastScanVersion = long.MinValue;
|
||||
private bool _pendingDialog;
|
||||
private bool _wakeup;
|
||||
|
||||
/// <summary>Debounced chain driver: waits out the running dialog, rescans when the condition state changed and starts the next unconsumed event.</summary>
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (!isActive || chain.Count == 0)
|
||||
{
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pendingDialog)
|
||||
{
|
||||
if (dialogBox == null || dialogBox.Locker != null)
|
||||
return;
|
||||
_pendingDialog = false;
|
||||
_lastScanVersion = ConditionStateMonitor.Version;
|
||||
_wakeup = true;
|
||||
return;
|
||||
}
|
||||
|
||||
double time = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
if(time - timestamp < checkDelay)
|
||||
return;
|
||||
timestamp = (float)time;
|
||||
|
||||
var version = ConditionStateMonitor.Version;
|
||||
if (version == _lastScanVersion && !_wakeup)
|
||||
return;
|
||||
_wakeup = false;
|
||||
_lastScanVersion = version;
|
||||
|
||||
var next = GetNext();
|
||||
if(next == null)
|
||||
return;
|
||||
|
||||
chain.Remove(next);
|
||||
next.RunDialog();
|
||||
_pendingDialog = true;
|
||||
}
|
||||
|
||||
protected override ExecutableEvent GetNext()
|
||||
{
|
||||
if (Lists.IsEmpty(chain))
|
||||
return null;
|
||||
foreach (var additional in chain)
|
||||
{
|
||||
if(additional.IsOneRun && !additional.IsFirstRun)
|
||||
continue;
|
||||
if (additional.CheckConditions)
|
||||
return additional;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Cthangover.Core.UI.Event;
|
||||
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.Executable;
|
||||
using Cthangover.Core.Scenarios;
|
||||
using Cthangover.Core.Interactive;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Executable
|
||||
{
|
||||
/// <summary>
|
||||
/// The primary event chain for a scene. Registers as IOnUpdateEvent and
|
||||
/// rescans registered ExecutableEvents only when the condition state changes
|
||||
/// (bumped <see cref="ConditionStateMonitor.Version"/>), on dialog end, or on
|
||||
/// the first scan — with a 0.5s debounce. On _Ready, auto-collects child
|
||||
/// ExecutableEvent nodes and handles deferred scene switches (pending scene
|
||||
/// name from SceneManager). Exposes LastBackgroundID so action commands can
|
||||
/// remember which background was active for scene restore on revisit.
|
||||
/// AddEvent/RemoveEvent/ClearEvents allow dynamic event management via code.
|
||||
/// Events are checked in insertion order; the first satisfying both IsOneRun
|
||||
/// and CheckConditions gets executed. A skip-guard prevents re-running the
|
||||
/// same event at the same state version, breaking self-trigger loops while
|
||||
/// preserving A→B chaining.
|
||||
/// </summary>
|
||||
public partial class ExecutableMainEventChain : Control, IExecutableEventChain, IOnUpdateEvent
|
||||
{
|
||||
private bool isActive = true;
|
||||
private List<ExecutableEvent> allEvents = new();
|
||||
private DialogBox dialogBox;
|
||||
private SceneEventController eventController;
|
||||
private float timestamp;
|
||||
|
||||
// Version of the last event scan (MinValue forces the first scan).
|
||||
private long _lastScanVersion = long.MinValue;
|
||||
// Version at the moment the chain's own last-run event finished its dialog.
|
||||
private long _lastRunEndVersion = long.MinValue;
|
||||
// The last event the chain actually started.
|
||||
private ExecutableEvent _lastRunEvent;
|
||||
// "Wake up and rescan" flag, set on dialog end or new event added.
|
||||
private bool _wakeup;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the last background ID set by action commands (e.g. "bg_home").
|
||||
/// Used on scene revisit to restore the previously active background.
|
||||
/// </summary>
|
||||
public string LastBackgroundID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event priority for <see cref="SceneEventController"/> sorting. Value
|
||||
/// <c>1</c> places this chain after high-priority handlers but before
|
||||
/// general subscribers.
|
||||
/// </summary>
|
||||
public int Priority => 1;
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables event polling. When <c>false</c>,
|
||||
/// <see cref="OnUpdate"/> returns immediately without dispatching events.
|
||||
/// </summary>
|
||||
public bool IsActive
|
||||
{
|
||||
get => isActive;
|
||||
set { isActive = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes and frees all currently registered events from the chain via
|
||||
/// <c>QueueFree</c> and empties the internal event list.
|
||||
/// </summary>
|
||||
public void ClearEvents()
|
||||
{
|
||||
GameLogger.Log("CHAIN", $"ClearEvents: removing {allEvents.Count} event(s)");
|
||||
|
||||
foreach (var evt in allEvents)
|
||||
{
|
||||
var id = evt is ScenarioEvent se ? se.Condition ?? "no_condition" : evt.GetType().Name;
|
||||
GameLogger.Log("CHAIN", $" QueueFree: {evt.GetType().Name} [condition={id}]");
|
||||
evt.QueueFree();
|
||||
}
|
||||
allEvents.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an <see cref="ExecutableEvent"/> to the chain and reparents it via
|
||||
/// <c>AddChild</c>. Events are dispatched in insertion order.
|
||||
/// </summary>
|
||||
/// <param name="evt">The event to register and parent to this chain.</param>
|
||||
public void AddEvent(IExecutableEvent evt)
|
||||
{
|
||||
var typed = (ExecutableEvent)evt;
|
||||
var id = typed is ScenarioEvent se ? se.Condition ?? "no_condition" : typed.GetType().Name;
|
||||
GameLogger.Log("CHAIN", $"AddEvent: {typed.GetType().Name} [condition={id}] — calling AddChild");
|
||||
AddChild(typed);
|
||||
allEvents.Add(typed);
|
||||
_wakeup = true;
|
||||
GameLogger.Log("CHAIN", $" AddEvent: done, allEvents.Count={allEvents.Count}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an event from the chain and queues it for deletion. No-op if
|
||||
/// the event is not currently registered.
|
||||
/// </summary>
|
||||
/// <param name="evt">The event to unregister and free.</param>
|
||||
public void RemoveEvent(ExecutableEvent evt)
|
||||
{
|
||||
if (allEvents.Remove(evt))
|
||||
{
|
||||
evt.QueueFree();
|
||||
GameLogger.Log("CHAIN", $"removed event '{evt.GetType().Name}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Finds the shared <see cref="DialogBox"/> and event controller, collects child events, publishes the shell-ready event and replays any pending scene switch.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
GameLogger.Log("CHAIN", $"_Ready: starting, tree scene='{GetTree()?.CurrentScene?.Name}'");
|
||||
|
||||
dialogBox = SceneContextNode.FindNode<DialogBox>("DialogBox");
|
||||
|
||||
eventController = SceneContextNode.FindNode<SceneEventController>("EventController");
|
||||
eventController?.AddUpdateEventListener(this);
|
||||
ModEventBus.Subscribe<DialogEndedEvent>(OnDialogEnded);
|
||||
|
||||
foreach (var child in GetChildren())
|
||||
{
|
||||
if (child is ExecutableEvent evt)
|
||||
{
|
||||
allEvents.Add(evt);
|
||||
GameLogger.Log("CHAIN", $"collected event '{evt.GetType().Name}'");
|
||||
}
|
||||
}
|
||||
|
||||
ModEventBus.Publish(new ShellReadyEvent { Root = this });
|
||||
|
||||
var sceneManager = GetNodeOrNull<SceneManager>("/root/SceneManager");
|
||||
if (sceneManager != null && !string.IsNullOrEmpty(sceneManager.PendingSceneName))
|
||||
{
|
||||
var pending = sceneManager.PendingSceneName;
|
||||
GameLogger.Log("CHAIN", $"_Ready: deferring SwitchScene('{pending}')");
|
||||
sceneManager.PendingSceneName = null;
|
||||
Callable.From(() => sceneManager.SwitchScene(pending)).CallDeferred();
|
||||
}
|
||||
|
||||
GameLogger.Log("CHAIN", $"_Ready: done, allEvents={allEvents.Count}");
|
||||
}
|
||||
|
||||
/// <summary>Detaches the update and dialog-end listeners from the event controller when the chain leaves the tree.</summary>
|
||||
public override void _ExitTree()
|
||||
{
|
||||
eventController?.RemoveUpdateEventListener(this);
|
||||
ModEventBus.Unsubscribe<DialogEndedEvent>(OnDialogEnded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when any dialog queue finishes. Wakes the chain so the next event
|
||||
/// can be picked up without waiting for a state change, and — for the chain's
|
||||
/// own last-run event — records the version at its dialog end so that the
|
||||
/// state bumps it produced are considered accounted for (breaks self-loops).
|
||||
/// </summary>
|
||||
private void OnDialogEnded(DialogEndedEvent evt)
|
||||
{
|
||||
_wakeup = true;
|
||||
if (_lastRunEvent != null && evt.LockerId == _lastRunEvent.ID)
|
||||
_lastRunEndVersion = ConditionStateMonitor.Version;
|
||||
}
|
||||
|
||||
/// <summary>Debounced scan that starts the next runnable event when the condition state changed since the last completed run.</summary>
|
||||
public void OnUpdate()
|
||||
{
|
||||
if (!isActive)
|
||||
return;
|
||||
|
||||
var time = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
if (time - timestamp < 0.5f)
|
||||
return;
|
||||
timestamp = (float)time;
|
||||
|
||||
if (dialogBox == null || dialogBox.IsRunning)
|
||||
return;
|
||||
|
||||
var version = ConditionStateMonitor.Version;
|
||||
if (version == _lastScanVersion && !_wakeup)
|
||||
return;
|
||||
|
||||
_wakeup = false;
|
||||
_lastScanVersion = version;
|
||||
|
||||
var dialog = FindEvent(allEvents, _lastRunEvent, _lastRunEndVersion);
|
||||
if (dialog == null)
|
||||
return;
|
||||
|
||||
if (dialog.RunDialog())
|
||||
_lastRunEvent = dialog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>true</c> if the chain has any registered events remaining. Used to
|
||||
/// check whether a scene still has pending dialog content.
|
||||
/// </summary>
|
||||
public bool HasEvents => allEvents.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates the chain, preventing any further event dispatch until
|
||||
/// reactivation.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
private ExecutableEvent FindEvent(List<ExecutableEvent> list, ExecutableEvent skipEvent, long skipEndVersion)
|
||||
{
|
||||
if (Lists.IsEmpty(list))
|
||||
return null;
|
||||
foreach (var nextEvent in list)
|
||||
{
|
||||
if (nextEvent == skipEvent && ConditionStateMonitor.Version <= skipEndVersion)
|
||||
continue;
|
||||
if (nextEvent.IsOneRun && !nextEvent.IsFirstRun)
|
||||
continue;
|
||||
if (nextEvent.CheckConditions)
|
||||
return nextEvent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Cthangover.Mods.Dialog.Executable
|
||||
{
|
||||
using Cthangover.Core.UI.Executable;
|
||||
|
||||
/// <summary>
|
||||
/// Simplest IEventObject implementation: carries an ID and self-destructs
|
||||
/// via QueueFree when Destruct() is called. Used as a base for scene objects
|
||||
/// that must be tracked by the event system for cleanup.
|
||||
/// </summary>
|
||||
public partial class IdentifiableEventObjectBehaviour : Godot.Node, IEventObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier for this event object. Set externally to allow the event
|
||||
/// tracking system to locate and manage this instance.
|
||||
/// </summary>
|
||||
public string ID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Signals cleanup by calling <c>QueueFree</c> to remove this node from
|
||||
/// the scene tree and release resources.
|
||||
/// </summary>
|
||||
public void Destruct()
|
||||
{
|
||||
QueueFree();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Cthangover.Mods.Dialog.Executable
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder/stub node. Likely represents a future or deprecated behaviour
|
||||
/// component for manually triggering ExecutableEvents.
|
||||
/// </summary>
|
||||
public partial class RunEventBehaviour : Godot.Node
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Mods.Dialog.Scenarios;
|
||||
using Cthangover.Core.Scenes;
|
||||
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.Interactive;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Executable
|
||||
{
|
||||
/// <summary>
|
||||
/// ExecutableEvent backed by a scenario file (.scenario). Converts scenario
|
||||
/// text using ScenarioConverter and appends the resulting actions to the
|
||||
/// dialog queue. Resolves the scenario path via convention: if ScenarioPath
|
||||
/// is set, uses that; otherwise searches all mods for a matching file name
|
||||
/// derived from the class name (stripping "Event" suffix), with a fallback
|
||||
/// to res://mods/visual_novel_core/scenarios/. CustomMappings allows code to register
|
||||
/// arbitrary class-to-path bindings for mod-provided scenarios. Supports
|
||||
/// inline ScenarioText for embedded scenarios that don't need a separate
|
||||
/// file. Checks conditions through ScenarioCondition.Evaluate before triggering.
|
||||
/// </summary>
|
||||
public partial class ScenarioEvent : ExecutableEvent, IConditionedEvent
|
||||
{
|
||||
private static readonly Dictionary<string, string> CustomMappings = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom mapping from a class name to a scenario file path.
|
||||
/// Use from mod initialization code for mod-provided scenario paths that
|
||||
/// deviate from the default naming convention.
|
||||
/// </summary>
|
||||
/// <param name="className">The C# class name (without namespace).</param>
|
||||
/// <param name="scenarioPath">The resource path to the .scenario file.</param>
|
||||
public static void RegisterScenario(string className, string scenarioPath)
|
||||
{
|
||||
CustomMappings[className] = scenarioPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicit resource path to a scenario file. When set, overrides the
|
||||
/// default path resolution in <see cref="ResolveScenarioPath"/>.
|
||||
/// </summary>
|
||||
[Export] public string ScenarioPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Inline scenario text. If non-empty, parsed by <see cref="ScenarioConverter"/>
|
||||
/// instead of loading from a file. Enables embedding small scenarios directly
|
||||
/// in code or scene data.
|
||||
/// </summary>
|
||||
public string ScenarioText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A condition expression evaluated by <see cref="ScenarioCondition.Evaluate"/>.
|
||||
/// If empty, the event always passes condition checks.
|
||||
/// </summary>
|
||||
public string Condition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stable identifier for save/load tracking. If set, becomes the value of
|
||||
/// <see cref="ExecutableEvent.ID"/> instead of the type name.
|
||||
/// </summary>
|
||||
public string ScenarioId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the time-of-day lighting flag for this event's dialog.
|
||||
/// <c>true</c> enables time blending, <c>false</c> disables it,
|
||||
/// <c>null</c> leaves the current setting unchanged.
|
||||
/// </summary>
|
||||
public bool? LightUseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="ScenarioId"/> if set; otherwise falls back to the
|
||||
/// full type name from <see cref="ExecutableEvent.ID"/>.
|
||||
/// </summary>
|
||||
public override string ID => ScenarioId ?? base.ID;
|
||||
|
||||
protected override void CreateDialog(DialogQueue dlg)
|
||||
{
|
||||
if (LightUseTime.HasValue)
|
||||
dlg.LightUseTime(LightUseTime.Value);
|
||||
|
||||
var converter = new ScenarioConverter();
|
||||
DialogQueue loaded;
|
||||
|
||||
if (!string.IsNullOrEmpty(ScenarioText))
|
||||
{
|
||||
loaded = converter.Convert(ScenarioText);
|
||||
}
|
||||
else
|
||||
{
|
||||
loaded = converter.Convert(ScenarioDefs.Resolve(ScenarioId));
|
||||
}
|
||||
|
||||
if (loaded == null)
|
||||
{
|
||||
GameLogger.Log("SCENE", $"Failed to load scenario for {GetType().Name}", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var action in loaded.Queue)
|
||||
dlg.Queue.Add(action);
|
||||
|
||||
foreach (var obj in loaded.RuntimeObjectList)
|
||||
dlg.RuntimeObjectList.Add(obj);
|
||||
}
|
||||
|
||||
private long _cachedVersion = long.MinValue;
|
||||
private bool _cachedResult;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates <see cref="Condition"/> through <see cref="ScenarioCondition.Evaluate"/>.
|
||||
/// Empty conditions always pass. The result is cached and reused until
|
||||
/// <see cref="ConditionStateMonitor.Version"/> changes, so unchanged world
|
||||
/// state does not trigger repeated parsing/evaluation of the same condition.
|
||||
/// </summary>
|
||||
public override bool CheckConditions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Condition))
|
||||
return true;
|
||||
|
||||
var version = ConditionStateMonitor.Version;
|
||||
if (_cachedVersion == version)
|
||||
return _cachedResult;
|
||||
|
||||
_cachedResult = ScenarioCondition.Evaluate(Condition);
|
||||
_cachedVersion = version;
|
||||
return _cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user