Event-driven dialog queue, story flag command, mod unit tests
Push-based event chains, runtime fixes (single end, goto lifecycle, if, busy box, held delay, empty options, timed/animated/effect actions), the 'flag name value' DSL command, autoplay --dialogs=N, 19 mod unit tests.
This commit is contained in:
1 parent
9f86bdbb0c
commit
bd55716349
86 files changed
+3173
-2181
No files matched your search
+37
-37
@@ -1,37 +1,37 @@
|
||||
# Godot 4.x
|
||||
.godot/
|
||||
.idea/
|
||||
sdk/
|
||||
bin/
|
||||
obj/
|
||||
|
||||
#ai
|
||||
.omo/
|
||||
.opencode/
|
||||
|
||||
gen/
|
||||
obj/
|
||||
bin/
|
||||
|
||||
*.tres~
|
||||
*.tscn~
|
||||
|
||||
# GDScript LSP
|
||||
.godot/
|
||||
|
||||
# Export
|
||||
*.exe
|
||||
*.dmg
|
||||
*.apk
|
||||
*.aab
|
||||
*.pck
|
||||
*.zip
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor
|
||||
*.import
|
||||
*.godot.uid
|
||||
# Godot 4.x
|
||||
.godot/
|
||||
.idea/
|
||||
sdk/
|
||||
bin/
|
||||
obj/
|
||||
|
||||
#ai
|
||||
.omo/
|
||||
.opencode/
|
||||
|
||||
gen/
|
||||
obj/
|
||||
bin/
|
||||
|
||||
*.tres~
|
||||
*.tscn~
|
||||
|
||||
# GDScript LSP
|
||||
.godot/
|
||||
|
||||
# Export
|
||||
*.exe
|
||||
*.dmg
|
||||
*.apk
|
||||
*.aab
|
||||
*.pck
|
||||
*.zip
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor
|
||||
*.import
|
||||
*.godot.uid
|
||||
@@ -8,6 +8,9 @@
|
||||
"sources": [
|
||||
"src/**/*.cs"
|
||||
],
|
||||
"test_sources": [
|
||||
"tests/*.cs"
|
||||
],
|
||||
"depends": ["shell2d_base>=1", "interactive_base>=1", "effects_base>=1", "time_base>=1", "map_base>=1", "ui_base>=1"],
|
||||
"ui": [],
|
||||
"test_suites": [
|
||||
|
||||
@@ -48,8 +48,7 @@ namespace Cthangover.Mods.Dialog
|
||||
if (dlg == null || dlg.Queue.Count == 0)
|
||||
return false;
|
||||
|
||||
dialogBox.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
return true;
|
||||
return dialogBox.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
}
|
||||
|
||||
/// <summary>Runs inline DSL commands (hover effects and similar) through a standalone dialog runtime.</summary>
|
||||
|
||||
@@ -1,78 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +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; }
|
||||
}
|
||||
}
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,31 +9,31 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,39 +9,39 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -10,55 +10,55 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Godot;
|
||||
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;
|
||||
@@ -10,47 +10,47 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,30 +9,30 @@ 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] : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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] : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
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;
|
||||
@@ -11,38 +11,38 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -10,49 +10,49 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,30 +9,30 @@ 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,26 +9,26 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,25 +9,25 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,25 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Scenarios
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>flag name value</c> DSL command: sets a story flag of the
|
||||
/// game through <see cref="DialogQueue.Flag"/>. Flags outlive the dialog
|
||||
/// (saved, reset by a new game) and are what <c>condition:</c> headers read;
|
||||
/// <c>set</c> is the dialog-local counterpart for <c>${var}</c> substitution.
|
||||
/// </summary>
|
||||
public class FlagCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "flag";
|
||||
/// <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)
|
||||
=> dlg.Flag(positional[0], positional[1]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,27 +9,27 @@ 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] : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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] : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,35 +9,35 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,25 +9,25 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,29 +9,29 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,26 +9,26 @@ 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,31 +9,31 @@ 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,25 +9,25 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,24 +9,24 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
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;
|
||||
@@ -10,45 +10,45 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,31 +9,31 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,25 +9,25 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,31 +9,31 @@ 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -10,41 +10,41 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
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;
|
||||
@@ -10,45 +10,45 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
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;
|
||||
@@ -9,37 +9,37 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
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 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;
|
||||
@@ -15,60 +15,60 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
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;
|
||||
@@ -10,45 +10,45 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,12 +63,17 @@ namespace Cthangover.Mods.Dialog.Action
|
||||
ID = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
/// <summary>Idempotent construction: calls <see cref="DoConstruct"/> once and sets <see cref="IsConstructed"/> to true.</summary>
|
||||
/// <summary>
|
||||
/// Idempotent construction: calls <see cref="DoConstruct"/> once and sets <see cref="IsConstructed"/> to true.
|
||||
/// Construction also clears <see cref="IsDestructed"/>: an action reached again through a looping goto is a
|
||||
/// new life of the same object, so its next <see cref="Destruct"/> must release what this construction took.
|
||||
/// </summary>
|
||||
public void Construct()
|
||||
{
|
||||
if(IsConstructed) return;
|
||||
DoConstruct();
|
||||
IsConstructed = true;
|
||||
IsDestructed = false;
|
||||
}
|
||||
|
||||
/// <summary>Idempotent desctruction: calls <see cref="DoDestruct"/> once, then sets <see cref="IsConstructed"/> false and <see cref="IsDestructed"/> true.</summary>
|
||||
@@ -83,7 +88,7 @@ namespace Cthangover.Mods.Dialog.Action
|
||||
/// <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();
|
||||
Construct();
|
||||
StartTime = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0);
|
||||
DoRun(runtime);
|
||||
}
|
||||
|
||||
@@ -1,14 +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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,17 +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
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +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,
|
||||
}
|
||||
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +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();
|
||||
}
|
||||
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Mods.Effects.Animation;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Cthangover.Core.UI.Event;
|
||||
using Cthangover.Mods.Interface.View;
|
||||
using Godot;
|
||||
using Cthangover.Core.UI;
|
||||
@@ -15,106 +14,150 @@ 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.
|
||||
/// configurable speed: the first frame lasts 1/<see cref="Speed"/> seconds,
|
||||
/// every later one 1/<see cref="NextFrameSpeed"/>. Hard cuts, no crossfade.
|
||||
/// Frames come from <see cref="Textures"/> or are resolved from
|
||||
/// <see cref="SpriteIDs"/> through <see cref="BackgroundFactory"/> (the same
|
||||
/// texture names a <c>foreground</c> command takes) on construction
|
||||
/// (OnStartQueue).
|
||||
/// <para/>
|
||||
/// Event-driven: each frame schedules one <see cref="SceneTreeTimer"/> for the
|
||||
/// next cut — no per-frame update. The dialog does not wait (NoWait); the
|
||||
/// animation keeps playing while the dialog goes on and stops when the queue
|
||||
/// ends (OnEndQueue), when another animation starts or when a
|
||||
/// <c>foreground</c> command replaces the picture (<see cref="StopActive"/>).
|
||||
/// </summary>
|
||||
public class ActionAnimatedForeground : ActionCommand, IOnUpdateEvent
|
||||
public class ActionAnimatedForeground : ActionCommand
|
||||
{
|
||||
/// <summary>Update priority. Runs after higher-priority listeners. Value 0.</summary>
|
||||
public int Priority => 0;
|
||||
private static ActionAnimatedForeground active;
|
||||
|
||||
/// <summary>List of sprite resource paths to display as animation frames. Loaded in <see cref="DoConstruct"/>.</summary>
|
||||
/// <summary>Texture names resolved through <see cref="BackgroundFactory"/>; ignored when <see cref="Textures"/> is set.</summary>
|
||||
public List<string> SpriteIDs { get; set; }
|
||||
/// <summary>Initial frame rate multiplier for the first frame.</summary>
|
||||
/// <summary>Ready frames used instead of resolving <see cref="SpriteIDs"/> (bypasses the factory like ActionForeground.Texture).</summary>
|
||||
public List<Texture2D> Textures { get; set; }
|
||||
/// <summary>Frames per second of 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>
|
||||
/// <summary>Frames per second of every frame after the first one.</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>
|
||||
/// <summary>Whether playback restarts from the first frame after the last one; otherwise it stops on the last frame.</summary>
|
||||
public bool IsLoop { get; set; } = true;
|
||||
|
||||
private int currentFrame;
|
||||
private float elapsed;
|
||||
private bool isAnimating;
|
||||
private List<Texture2D> frames;
|
||||
/// <summary>Whether the animation is still cutting frames.</summary>
|
||||
public bool IsAnimating { get; private set; }
|
||||
|
||||
/// <summary>Preloads frames when the dialog queue is first loaded, so textures are ready before playback.</summary>
|
||||
private int currentFrame;
|
||||
private List<Texture2D> frames;
|
||||
private SceneTreeTimer frameTimer;
|
||||
|
||||
/// <summary>Frames are resolved when the queue starts, so the first cut does not stall on loading.</summary>
|
||||
public override ConstructType ConstructType { get; set; } = ConstructType.OnStartQueue;
|
||||
/// <summary>Animation runs in the background — the dialog queue continues immediately.</summary>
|
||||
/// <summary>The animation lives until the dialog ends, not until the next action.</summary>
|
||||
public override DestructType DestructType { get; set; } = DestructType.OnEndQueue;
|
||||
/// <summary>The dialog goes on while the foreground animates.</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>
|
||||
/// <summary>Stops the animation that currently drives the foreground, if any; the shown frame stays.</summary>
|
||||
public static void StopActive()
|
||||
{
|
||||
active?.Stop();
|
||||
}
|
||||
|
||||
/// <summary>Resolves the frames.</summary>
|
||||
public override void DoConstruct()
|
||||
{
|
||||
base.DoConstruct();
|
||||
LoadFrames();
|
||||
}
|
||||
|
||||
/// <summary>Stops the animation loop; the last shown frame stays on screen.</summary>
|
||||
/// <summary>Stops the frame timer; the last shown frame stays on the ViewBox.</summary>
|
||||
public override void DoDestruct()
|
||||
{
|
||||
base.DoDestruct();
|
||||
isAnimating = false;
|
||||
Stop();
|
||||
}
|
||||
|
||||
private void LoadFrames()
|
||||
{
|
||||
frames = new List<Texture2D>();
|
||||
var source = Textures;
|
||||
if (source != null)
|
||||
{
|
||||
foreach (var tex in source)
|
||||
if (tex != null)
|
||||
frames.Add(tex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SpriteIDs == null)
|
||||
return;
|
||||
foreach (var id in SpriteIDs)
|
||||
{
|
||||
var tex = GD.Load<Texture2D>(id);
|
||||
var tex = BackgroundFactory.Instance.Get(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>
|
||||
/// <summary>Stops a previous animation, shows the first frame on the ViewBox and schedules the next cut.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
StopActive();
|
||||
currentFrame = 0;
|
||||
elapsed = 0f;
|
||||
isAnimating = frames != null && frames.Count > 0;
|
||||
IsAnimating = frames != null && frames.Count > 0;
|
||||
if (!IsAnimating)
|
||||
return;
|
||||
|
||||
if (isAnimating)
|
||||
{
|
||||
var viewBox = Scene.Find<ViewBox>("ViewBox");
|
||||
viewBox?.SetForegroundTexture(frames[0]);
|
||||
}
|
||||
active = this;
|
||||
ShowFrame();
|
||||
ScheduleNextFrame(Speed);
|
||||
}
|
||||
|
||||
void IOnUpdateEvent.OnUpdate()
|
||||
private void ShowFrame()
|
||||
{
|
||||
if (!isAnimating || frames == null || frames.Count == 0)
|
||||
return;
|
||||
|
||||
var viewBox = Scene.Find<ViewBox>("ViewBox");
|
||||
if (viewBox == null)
|
||||
viewBox?.SetForegroundTexture(frames[currentFrame]);
|
||||
}
|
||||
|
||||
private void ScheduleNextFrame(float speed)
|
||||
{
|
||||
if (Godot.Engine.GetMainLoop() is not SceneTree tree)
|
||||
return;
|
||||
|
||||
elapsed += 1f / 60f;
|
||||
var frameDuration = 1f / Mathf.Max(Speed, 0.01f);
|
||||
if (elapsed >= frameDuration)
|
||||
frameTimer = tree.CreateTimer(1f / Mathf.Max(speed, 0.01f));
|
||||
frameTimer.Timeout += OnFrameTimeout;
|
||||
}
|
||||
|
||||
private void OnFrameTimeout()
|
||||
{
|
||||
frameTimer = null;
|
||||
if (!IsAnimating)
|
||||
return;
|
||||
|
||||
currentFrame++;
|
||||
if (currentFrame >= frames.Count)
|
||||
{
|
||||
elapsed -= frameDuration;
|
||||
currentFrame++;
|
||||
if (currentFrame >= frames.Count)
|
||||
if (!IsLoop)
|
||||
{
|
||||
if (IsLoop)
|
||||
currentFrame = 0;
|
||||
else
|
||||
{
|
||||
isAnimating = false;
|
||||
return;
|
||||
}
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
viewBox.SetForegroundTexture(frames[currentFrame]);
|
||||
Speed = NextFrameSpeed;
|
||||
currentFrame = 0;
|
||||
}
|
||||
|
||||
ShowFrame();
|
||||
ScheduleNextFrame(NextFrameSpeed);
|
||||
}
|
||||
|
||||
private void Stop()
|
||||
{
|
||||
IsAnimating = false;
|
||||
if (frameTimer != null)
|
||||
{
|
||||
frameTimer.Timeout -= OnFrameTimeout;
|
||||
frameTimer = null;
|
||||
}
|
||||
if (active == this)
|
||||
active = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
@@ -8,26 +8,26 @@ 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,13 @@ 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.
|
||||
/// dialog box's parent Control over Duration seconds. The fade belongs to the
|
||||
/// background, not to the action: by default (NoWait) the runtime destructs the
|
||||
/// action right after it runs, and the fade must go on. So the tween is shared
|
||||
/// by all instances — a new fade kills the previous one to prevent overlap —
|
||||
/// and destructing the action leaves it running; the tween is bound to the
|
||||
/// background node and dies with it. Can optionally set WaitType to WaitTime
|
||||
/// with a matching duration so the dialog pauses during the transition.
|
||||
/// </summary>
|
||||
public class ActionBackgroundShowHide : ActionCommand
|
||||
{
|
||||
@@ -25,7 +28,7 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
/// <summary>Duration of the fade tween in seconds.</summary>
|
||||
public float Duration { get; set; } = 1f;
|
||||
|
||||
private Tween activeTween;
|
||||
private static 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;
|
||||
@@ -47,18 +50,12 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Kills the fade tween so a destructed action cannot keep animating.</summary>
|
||||
public override void DoDestruct()
|
||||
{
|
||||
base.DoDestruct();
|
||||
StopTween();
|
||||
}
|
||||
|
||||
private void StopTween()
|
||||
private static void StopTween()
|
||||
{
|
||||
if (activeTween != null)
|
||||
{
|
||||
activeTween.Kill();
|
||||
if (GodotObject.IsInstanceValid(activeTween))
|
||||
activeTween.Kill();
|
||||
activeTween = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
@@ -8,49 +8,49 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,57 +9,83 @@ using Cthangover.Core.UI.Executable;
|
||||
using Cthangover.Core.Scenarios;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Interactive;
|
||||
using Cthangover.Mods.Interface.View;
|
||||
|
||||
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.
|
||||
/// Spawns a visual effect from the EffectFactory by ID (or a ready
|
||||
/// <see cref="Prefab"/>, bypassing the factory like ActionBackground.Texture).
|
||||
/// The scene is instantiated on construction and put into the ViewBox's UI
|
||||
/// layer (above all scene visuals, below the dialog) when the action runs; the
|
||||
/// node persists until the action is destructed. Uses DestructType.OnDelayed
|
||||
/// because the effect must outlive the action's run: the runtime frees it when
|
||||
/// the dialog ends (End destructs every constructed action), unless the effect
|
||||
/// node frees itself first.
|
||||
/// </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; }
|
||||
|
||||
/// <summary>Effect scene to spawn instead of resolving <see cref="EffectID"/>; null resolves through the factory.</summary>
|
||||
public PackedScene Prefab { 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>
|
||||
/// <summary>OnDelayed keeps the effect node alive past the action's run; it is freed when the dialog ends.</summary>
|
||||
public override DestructType DestructType { get; set; } = DestructType.OnDelayed;
|
||||
|
||||
/// <summary>Resolves <see cref="EffectID"/> through <see cref="EffectFactory"/> and instantiates the effect scene.</summary>
|
||||
/// <summary>Resolves the effect scene (<see cref="Prefab"/> or <see cref="EffectID"/> through <see cref="EffectFactory"/>) and instantiates it.</summary>
|
||||
public override void DoConstruct()
|
||||
{
|
||||
base.DoConstruct();
|
||||
if (string.IsNullOrEmpty(EffectID))
|
||||
return;
|
||||
|
||||
var packedScene = EffectFactory.Instance.Get(EffectID);
|
||||
var packedScene = Prefab;
|
||||
if (packedScene == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"ActionEffect: effect '{EffectID}' not found", LogLevel.Error);
|
||||
return;
|
||||
if (string.IsNullOrEmpty(EffectID))
|
||||
return;
|
||||
|
||||
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>
|
||||
/// <summary>Shows the instantiated effect: adds it to the ViewBox UI layer. Without a ViewBox the effect is dropped with an error.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
if (spawnedEffect == null || spawnedEffect.IsInsideTree())
|
||||
return;
|
||||
|
||||
var layer = Scene.Find<ViewBox>("ViewBox")?.GetInteractiveLayer("ui");
|
||||
if (layer == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"ActionEffect: no ViewBox to show effect '{EffectID}'", LogLevel.Error);
|
||||
spawnedEffect.QueueFree();
|
||||
spawnedEffect = null;
|
||||
return;
|
||||
}
|
||||
|
||||
layer.AddChild(spawnedEffect);
|
||||
}
|
||||
|
||||
/// <summary>Frees the spawned effect node; paired with the OnDelayed destruct so effects outlive the action's run.</summary>
|
||||
/// <summary>Frees the spawned effect node (unless it already freed itself); paired with the OnDelayed destruct so effects outlive the action's run.</summary>
|
||||
public override void DoDestruct()
|
||||
{
|
||||
base.DoDestruct();
|
||||
if (spawnedEffect != null)
|
||||
{
|
||||
spawnedEffect.QueueFree();
|
||||
if (GodotObject.IsInstanceValid(spawnedEffect))
|
||||
spawnedEffect.QueueFree();
|
||||
spawnedEffect = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
@@ -8,22 +8,22 @@ 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
@@ -8,23 +8,23 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,10 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
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>
|
||||
/// <summary>Stops a running foreground animation (it would overwrite the picture), 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)
|
||||
{
|
||||
ActionAnimatedForeground.StopActive();
|
||||
var viewBox = Scene.Find<ViewBox>("ViewBox");
|
||||
viewBox?.SetForegroundTexture(Texture);
|
||||
Texture = null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Godot;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
@@ -8,26 +8,26 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,10 @@ 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.
|
||||
/// through to the next action. NoWait so branching is transparent to the player:
|
||||
/// the runtime itself advances past a NoWait action, so the branch never calls
|
||||
/// <see cref="DialogRuntime.Next"/> — doing it here as well skipped the action
|
||||
/// after the <c>if</c> without running it.
|
||||
/// </summary>
|
||||
public class ActionIf : ActionCommand
|
||||
{
|
||||
@@ -34,7 +37,6 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
if (Condition == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "ActionIf: Condition delegate is null", LogLevel.Error);
|
||||
runtime.Next();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,8 +45,6 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
|
||||
if (!string.IsNullOrEmpty(goTo))
|
||||
runtime.TryGoTo(goTo);
|
||||
else
|
||||
runtime.Next();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
@@ -7,33 +7,33 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,25 @@
|
||||
using Cthangover.Core.Scenes;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets a story flag of the running game (<see cref="ScenarioCondition.SetFlag"/>).
|
||||
/// Unlike <see cref="ActionSet"/>, whose variables end with the dialog, a
|
||||
/// flag is game state: it is saved, read by scenario conditions
|
||||
/// (<c>condition: door == "open"</c>) and its change rescans the scene's
|
||||
/// event chain, so a scenario waiting for it can start right after.
|
||||
/// </summary>
|
||||
public class ActionSetFlag : ActionCommand
|
||||
{
|
||||
/// <summary>Flag name, as used in conditions.</summary>
|
||||
public string Name { get; set; }
|
||||
/// <summary>Flag value; numeric values compare as integers in conditions.</summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <summary>Setting a flag is instant — the dialog continues without pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoRun(DialogRuntime runtime) => ScenarioCondition.SetFlag(Name, Value ?? string.Empty);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
/// <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>
|
||||
/// <summary>Defers <c>SceneManager.SwitchScene</c> so the tree is not mutated mid-queue; the owning scenario is not forced into OneRun.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(SceneName))
|
||||
|
||||
@@ -14,10 +14,12 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
/// 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.
|
||||
/// completes itself through <see cref="DialogBox.CompleteAction"/> (event-driven —
|
||||
/// no DialogBox poll) and the box advances the queue, scheduling the timer of a
|
||||
/// following <c>delay</c> like any other advance. OnStartCallback/OnFinishCallback
|
||||
/// bracket the animation for setup/teardown. Progress follows the controller's
|
||||
/// process delta, so the timeline lasts WaitTime seconds of game time at any frame
|
||||
/// rate (and scales with <c>Engine.TimeScale</c> exactly like <c>delay</c> timers).
|
||||
/// </summary>
|
||||
public class ActionTimedUpdate : ActionCommand, IOnUpdateEvent
|
||||
{
|
||||
@@ -31,7 +33,7 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
/// <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>
|
||||
/// <summary>Pauses the dialog until the elapsed time meets WaitTime — the action then completes 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;
|
||||
@@ -39,12 +41,13 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
private float elapsed;
|
||||
private bool isRunning;
|
||||
private DialogRuntime runtime;
|
||||
private SceneEventController controller;
|
||||
|
||||
/// <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 = Scene.Find<SceneEventController>("EventController");
|
||||
controller?.AddUpdateEventListener(this);
|
||||
}
|
||||
|
||||
@@ -63,8 +66,8 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
base.DoDestruct();
|
||||
isRunning = false;
|
||||
runtime = null;
|
||||
var controller = Scene.Find<SceneEventController>("EventController");
|
||||
controller?.RemoveUpdateEventListener(this);
|
||||
controller = null;
|
||||
OnFinishCallback?.Invoke();
|
||||
}
|
||||
|
||||
@@ -73,19 +76,26 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
elapsed += 1f / 60f;
|
||||
elapsed += (float)(controller?.GetProcessDeltaTime() ?? 0d);
|
||||
|
||||
var t = WaitTime > 0f ? Mathf.Min(elapsed / WaitTime, 1f) : 1f;
|
||||
OnUpdateCallback?.Invoke(t);
|
||||
|
||||
if (elapsed >= WaitTime)
|
||||
if (elapsed < WaitTime)
|
||||
return;
|
||||
|
||||
// Advancing destructs this action, which clears the runtime field:
|
||||
// keep the runtime in a local for the whole completion.
|
||||
isRunning = false;
|
||||
var owner = runtime;
|
||||
if (owner?.DialogBox != null)
|
||||
{
|
||||
// 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();
|
||||
owner.DialogBox.CompleteAction(this);
|
||||
}
|
||||
else if (owner != null)
|
||||
{
|
||||
owner.Next();
|
||||
owner.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +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,
|
||||
}
|
||||
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
}
|
||||
+70
-70
@@ -1,70 +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()
|
||||
{ }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
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()
|
||||
{ }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+49
-17
@@ -80,7 +80,11 @@ namespace Cthangover.Mods.Dialog
|
||||
/// <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>
|
||||
/// <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; a variant without a target falls through to the action after the choice (re-running
|
||||
/// the choice itself would show the same answers again forever).
|
||||
/// </summary>
|
||||
public void SelectVariant(SelectVariant variant)
|
||||
{
|
||||
if (answerBox != null)
|
||||
@@ -89,7 +93,10 @@ namespace Cthangover.Mods.Dialog
|
||||
answerBox = null;
|
||||
}
|
||||
|
||||
Runtime.TryGoTo(variant.GoTo);
|
||||
if (string.IsNullOrEmpty(variant.GoTo))
|
||||
Runtime.Next();
|
||||
else
|
||||
Runtime.TryGoTo(variant.GoTo);
|
||||
Runtime.Run();
|
||||
ScheduleWaitTimer();
|
||||
}
|
||||
@@ -172,32 +179,51 @@ namespace Cthangover.Mods.Dialog
|
||||
|
||||
/// <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"/>.
|
||||
/// (3) no dialog is running — neither one with a locker nor a locker-less one (an interactive's click dialog),
|
||||
/// see <see cref="IsRunning"/>. On success, shows the box and delegates to <see cref="DialogRuntime.SetDialogQueueAndRun"/>.
|
||||
/// Returns false when the queue was rejected, so callers (event chains) do not count a run that never happened.
|
||||
/// </summary>
|
||||
public void SetDialogQueueAndRun(DialogQueue dialog, IEnumerable<IActionCommand> endDialogQueue, int startIndex, ExecutableEvent locker)
|
||||
public bool 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;
|
||||
return false;
|
||||
}
|
||||
if (!IsActive)
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"can't run '{locker?.GetType().FullName}', DialogBox is not active", LogLevel.Error);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Locker != null)
|
||||
if (IsRunning)
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"can't run '{locker?.GetType().FullName}', locker already executing!", LogLevel.Error);
|
||||
return;
|
||||
GameLogger.Log("DIALOG", $"can't run '{locker?.GetType().FullName}', another dialog is already running!", LogLevel.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
Locker = locker;
|
||||
Show();
|
||||
Runtime.Run();
|
||||
ScheduleWaitTimer();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completion entry point for actions that finish on their own (<see cref="WaitType.WaitEvent"/>): advances
|
||||
/// past <paramref name="action"/> and schedules the wait timer of whatever comes next, exactly like a click
|
||||
/// or an elapsed delay. Ignored when <paramref name="action"/> is no longer the current one (a stale
|
||||
/// completion of an action the dialog already left) or while the debugger holds the runtime.
|
||||
/// </summary>
|
||||
public void CompleteAction(IActionCommand action)
|
||||
{
|
||||
if (Runtime.IsHeld || action == null || Runtime.CurrentAction != action)
|
||||
return;
|
||||
|
||||
Runtime.Next();
|
||||
Runtime.Run();
|
||||
ScheduleWaitTimer();
|
||||
}
|
||||
@@ -347,8 +373,12 @@ namespace Cthangover.Mods.Dialog
|
||||
Locker = null;
|
||||
}
|
||||
|
||||
/// <summary>Whether a dialog is currently running (Locker is not null).</summary>
|
||||
public bool IsRunning => Locker != null;
|
||||
/// <summary>
|
||||
/// Whether a dialog occupies the box: one started by an event (Locker is not null) or a locker-less queue
|
||||
/// that has not ended yet (an interactive's click dialog runs without a locker). Event chains and the click
|
||||
/// runner wait for false before starting anything, which keeps one dialog at a time.
|
||||
/// </summary>
|
||||
public bool IsRunning => Locker != null || !Runtime.IsEnd;
|
||||
|
||||
/// <summary>Whether the current dialog queue reached its end (also true before the first queue loads).</summary>
|
||||
public bool IsDialogEnded => Runtime.IsEnd;
|
||||
@@ -375,13 +405,15 @@ namespace Cthangover.Mods.Dialog
|
||||
/// 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.
|
||||
/// fires exactly once, so no Update work happens between delays. Nothing is scheduled while the
|
||||
/// debugger holds the runtime: the current action has not run yet, its wait starts on
|
||||
/// <see cref="Resume"/>.
|
||||
/// </summary>
|
||||
private void ScheduleWaitTimer()
|
||||
{
|
||||
ClearWaitTimer();
|
||||
|
||||
if (!Runtime.IsWaitTime)
|
||||
if (Runtime.IsHeld || !Runtime.IsWaitTime)
|
||||
return;
|
||||
|
||||
var wait = Runtime.CurrentAction?.WaitTime ?? 0f;
|
||||
@@ -399,13 +431,13 @@ namespace Cthangover.Mods.Dialog
|
||||
/// 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.
|
||||
/// dialog already moved on; the hold guard keeps a held, not yet run action in place.
|
||||
/// </summary>
|
||||
private void OnWaitTimerTimeout()
|
||||
{
|
||||
waitTimer = null;
|
||||
|
||||
if (!Runtime.IsWaitTime)
|
||||
if (Runtime.IsHeld || !Runtime.IsWaitTime)
|
||||
return;
|
||||
|
||||
Runtime.Next();
|
||||
|
||||
@@ -130,6 +130,14 @@ namespace Cthangover.Mods.Dialog
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets a story flag of the game (saved, read by scenario conditions) when the queue reaches this point.</summary>
|
||||
public ActionSetFlag Flag(string name, string value)
|
||||
{
|
||||
var action = new ActionSetFlag { 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)
|
||||
{
|
||||
|
||||
@@ -25,8 +25,14 @@ namespace Cthangover.Mods.Dialog
|
||||
/// 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.
|
||||
/// an optional cleanup queue (endDialogQueue) before tearing down, and publishes
|
||||
/// <see cref="DialogEndedEvent"/> on the <see cref="ModEventBus"/> exactly once per
|
||||
/// queue — event chains pick the next scenario from it.
|
||||
/// <para/>
|
||||
/// An action may end the queue (<c>end</c>, a goto to a missing label) or start
|
||||
/// another dialog from inside <see cref="Run"/>. Every load, end and abort bumps
|
||||
/// a generation counter, and <see cref="Run"/> stops as soon as the generation
|
||||
/// changed under it: the rest of the loop belongs to a queue that no longer runs.
|
||||
/// Infinite loop protection caps at 100k iterations per Run call.
|
||||
/// </summary>
|
||||
public class DialogRuntime
|
||||
@@ -37,6 +43,7 @@ namespace Cthangover.Mods.Dialog
|
||||
private int goToActionIndex;
|
||||
private int index;
|
||||
private bool isLastGoTo;
|
||||
private int generation;
|
||||
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>
|
||||
@@ -131,8 +138,9 @@ namespace Cthangover.Mods.Dialog
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// Loads a new dialog queue and prepares for execution. Ends any previous queue first — with its own
|
||||
/// end actions and dialog box, before anything of the new queue is set — then resets variables, sets the
|
||||
/// dialog box reference, 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,
|
||||
@@ -140,14 +148,16 @@ namespace Cthangover.Mods.Dialog
|
||||
IEnumerable<IActionCommand> endQueue,
|
||||
int startIndex = 0)
|
||||
{
|
||||
variables = new Dictionary<string, string>();
|
||||
DialogBox = dialogBox;
|
||||
IsHeld = false;
|
||||
|
||||
this.endDialogQueue = endQueue == null ? null : endQueue.ToList();
|
||||
if (Lists.IsNotEmpty(this.dialogQueue))
|
||||
End();
|
||||
|
||||
generation++;
|
||||
variables = new Dictionary<string, string>();
|
||||
DialogBox = dialogBox;
|
||||
IsHeld = false;
|
||||
isLastGoTo = false;
|
||||
|
||||
this.endDialogQueue = endQueue == null ? null : endQueue.ToList();
|
||||
this.dialogQueue = queue.ToList();
|
||||
|
||||
index = startIndex;
|
||||
@@ -215,12 +225,14 @@ namespace Cthangover.Mods.Dialog
|
||||
/// <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
|
||||
/// WaitTime, WaitEvent) so the runtime yields control until the condition is satisfied. Returns right after an action
|
||||
/// that ended or replaced the queue (see the generation note on the class). Protected against infinite loops
|
||||
/// with a 100,000-iteration cap.
|
||||
/// </summary>
|
||||
public void Run()
|
||||
{
|
||||
var iteration = 0;
|
||||
var runGeneration = generation;
|
||||
for (;;)
|
||||
{
|
||||
var action = CurrentAction;
|
||||
@@ -241,6 +253,8 @@ namespace Cthangover.Mods.Dialog
|
||||
}
|
||||
|
||||
action.Run(this);
|
||||
if (runGeneration != generation)
|
||||
return;
|
||||
|
||||
if (iteration++ > 100000)
|
||||
{
|
||||
@@ -266,7 +280,8 @@ namespace Cthangover.Mods.Dialog
|
||||
|
||||
/// <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.
|
||||
/// Processes any delayed-destruct targets registered by the current action. Calls <see cref="End"/> if the queue is
|
||||
/// already exhausted (nothing to advance to).
|
||||
/// Respects <see cref="isLastGoTo"/> — if a GoTo was the final action, the index is not incremented.
|
||||
/// </summary>
|
||||
public void Next()
|
||||
@@ -291,6 +306,7 @@ namespace Cthangover.Mods.Dialog
|
||||
{
|
||||
GameLogger.Log("DIALOG", "Next: queue exhausted, ending");
|
||||
End();
|
||||
return;
|
||||
}
|
||||
|
||||
index++;
|
||||
@@ -298,11 +314,17 @@ namespace Cthangover.Mods.Dialog
|
||||
|
||||
/// <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"/>.
|
||||
/// end-dialog cleanup queue (if any), and publishes <see cref="DialogEndedEvent"/> on the <see cref="ModEventBus"/>.
|
||||
/// Guards against end-queue actions starting a new dialog mid-cleanup by checking if the queue changed.
|
||||
/// Idempotent: a queue that already ended (or never started) is not ended again, so the event is published
|
||||
/// once per queue however many paths reach the end.
|
||||
/// </summary>
|
||||
public void End()
|
||||
{
|
||||
if (dialogQueue == null)
|
||||
return;
|
||||
|
||||
generation++;
|
||||
GameLogger.Log("DIALOG", $"End: {dialogQueue?.Count ?? 0} actions in queue, {(endDialogQueue?.Count ?? 0)} end actions");
|
||||
|
||||
if (Lists.IsNotEmpty(dialogQueue))
|
||||
@@ -369,6 +391,7 @@ namespace Cthangover.Mods.Dialog
|
||||
action.Destruct();
|
||||
}
|
||||
|
||||
generation++;
|
||||
dialogQueue = null;
|
||||
endDialogQueue = null;
|
||||
index = 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Cthangover.Core.UI.Event;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
using Cthangover.Core.UI;
|
||||
@@ -13,21 +13,23 @@ 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.
|
||||
/// Base for auto-triggering dialog events. Inside an IExecutableEventChain the
|
||||
/// chain decides when the event runs. A standalone event (not inside a chain)
|
||||
/// drives itself, event-driven: it asks for a deferred check when it becomes
|
||||
/// ready, when the condition state changes (<see cref="ConditionStateChangedEvent"/>)
|
||||
/// and when a dialog ends (<see cref="DialogEndedEvent"/>), and runs when the
|
||||
/// state changed since its own last run (or on the first run) and the
|
||||
/// DialogBox is free. 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
|
||||
public partial class ExecutableEvent : Node, IExecutableEvent
|
||||
{
|
||||
protected DialogBox dialogBox;
|
||||
private SceneEventController eventController;
|
||||
private float timestamp;
|
||||
private bool isStandalone;
|
||||
private bool checkQueued;
|
||||
|
||||
// Version at which the event last ran (MinValue forces the first run).
|
||||
private long _lastRunVersion = long.MinValue;
|
||||
@@ -35,7 +37,7 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
private bool _awaitingOwnEnd;
|
||||
|
||||
/// <summary>
|
||||
/// <c>true</c> until <see cref="RunDialog"/> is called for the first time.
|
||||
/// <c>true</c> until <see cref="RunDialog"/> starts the event's dialog 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;
|
||||
@@ -66,13 +68,7 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
/// </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>
|
||||
/// <summary>Finds the shared <see cref="DialogBox"/>; a standalone event (no parent chain) subscribes to its triggers and asks for the first check.</summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
if (dialogBox == null)
|
||||
@@ -84,30 +80,56 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
}
|
||||
}
|
||||
|
||||
var isInChain = GetParent() is IExecutableEventChain;
|
||||
if (!isInChain)
|
||||
isStandalone = GetParent() is not IExecutableEventChain;
|
||||
if (isStandalone)
|
||||
{
|
||||
eventController = SceneContextNode.FindNode<SceneEventController>("EventController");
|
||||
eventController?.AddUpdateEventListener(this);
|
||||
ModEventBus.Subscribe<ConditionStateChangedEvent>(OnConditionStateChanged);
|
||||
ModEventBus.Subscribe<DialogEndedEvent>(OnDialogEnded);
|
||||
RequestCheck();
|
||||
}
|
||||
|
||||
GameLogger.Log("EVENT", $"{GetType().Name}._Ready: dialogBox={(dialogBox != null ? "found" : "null")}, chain={isInChain}, registered={!isInChain}", LogLevel.Debug);
|
||||
GameLogger.Log("EVENT", $"{GetType().Name}._Ready: dialogBox={(dialogBox != null ? "found" : "null")}, chain={!isStandalone}, standalone={isStandalone}", LogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>Detaches from the event controller's update polling when the event leaves the tree.</summary>
|
||||
/// <summary>Detaches a standalone event from its triggers when it leaves the tree.</summary>
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (eventController != null)
|
||||
eventController.RemoveUpdateEventListener(this);
|
||||
if (!isStandalone)
|
||||
return;
|
||||
ModEventBus.Unsubscribe<ConditionStateChangedEvent>(OnConditionStateChanged);
|
||||
ModEventBus.Unsubscribe<DialogEndedEvent>(OnDialogEnded);
|
||||
}
|
||||
|
||||
private void OnConditionStateChanged(ConditionStateChangedEvent evt)
|
||||
{
|
||||
RequestCheck();
|
||||
}
|
||||
|
||||
private void OnDialogEnded(DialogEndedEvent evt)
|
||||
{
|
||||
RequestCheck();
|
||||
}
|
||||
|
||||
// Coalesces triggers of one frame into one deferred check.
|
||||
private void RequestCheck()
|
||||
{
|
||||
if (checkQueued)
|
||||
return;
|
||||
checkQueued = true;
|
||||
Callable.From(Check).CallDeferred();
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// Deferred check of a standalone event: 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()
|
||||
private void Check()
|
||||
{
|
||||
checkQueued = false;
|
||||
if (!IsInstanceValid(this) || !IsInsideTree())
|
||||
return;
|
||||
|
||||
if (!IsFirstRun && !CanRepeat)
|
||||
return;
|
||||
|
||||
@@ -125,11 +147,6 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
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;
|
||||
|
||||
@@ -140,8 +157,10 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
/// <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.
|
||||
/// block other dialog attempts. Returns <c>false</c> — and leaves
|
||||
/// <see cref="IsFirstRun"/> untouched — if the one-shot already ran, the dialog
|
||||
/// box is unavailable or busy, or the box rejected the queue (e.g. inactive
|
||||
/// during a scene switch): a run that never happened is not counted.
|
||||
/// </summary>
|
||||
public bool RunDialog()
|
||||
{
|
||||
@@ -151,7 +170,7 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
if (dialogBox == null)
|
||||
return false;
|
||||
|
||||
if (dialogBox.Locker != null)
|
||||
if (dialogBox.IsRunning)
|
||||
return false;
|
||||
|
||||
GameLogger.Log("EVENT", $"{GetType().Name}.RunDialog: starting dialog...");
|
||||
@@ -159,7 +178,8 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
var dlg = new DialogQueue();
|
||||
CreateDialog(dlg);
|
||||
|
||||
dialogBox.SetDialogQueueAndRun(dlg, null, StartIndex(dlg), this);
|
||||
if (!dialogBox.SetDialogQueueAndRun(dlg, null, StartIndex(dlg), this))
|
||||
return false;
|
||||
IsFirstRun = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
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;
|
||||
@@ -15,35 +14,41 @@ 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.
|
||||
/// The primary event chain for a scene: picks the next ExecutableEvent to run
|
||||
/// in the dialog box. Event-driven — there is no per-frame work. A scan is
|
||||
/// requested when the condition state changes (<see cref="ConditionStateChangedEvent"/>),
|
||||
/// when a dialog ends (<see cref="DialogEndedEvent"/>), when an event is added,
|
||||
/// when the chain becomes ready and when it is re-activated. Requests are
|
||||
/// coalesced into one deferred scan at the end of the frame: a burst of flag
|
||||
/// changes costs one pass, the state is read after the change completed (some
|
||||
/// publishers raise their event before storing the value), and a scan never
|
||||
/// runs inside the dialog runtime that ended a queue.
|
||||
/// <para/>
|
||||
/// On _Ready the chain collects child ExecutableEvent nodes and replays a
|
||||
/// pending scene switch (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.
|
||||
/// <para/>
|
||||
/// Self-loop guard: the chain remembers, per event, the condition version at
|
||||
/// the moment its dialog ended, and does not run that event again until the
|
||||
/// state changed after it. An event's own state changes (made during its
|
||||
/// dialog) therefore never re-trigger it, A→B chaining still works, and two
|
||||
/// repeatable events cannot ping-pong A→B→A while nothing changes.
|
||||
/// </summary>
|
||||
public partial class ExecutableMainEventChain : Control, IExecutableEventChain, IOnUpdateEvent
|
||||
public partial class ExecutableMainEventChain : Control, IExecutableEventChain
|
||||
{
|
||||
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;
|
||||
// Condition version at the end of each event's last dialog run by this chain.
|
||||
private readonly Dictionary<ExecutableEvent, long> endVersions = new();
|
||||
// The event whose dialog the chain started and is waiting to end.
|
||||
private ExecutableEvent running;
|
||||
// A deferred scan is already queued for this frame.
|
||||
private bool scanQueued;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the last background ID set by action commands (e.g. "bg_home").
|
||||
@@ -52,20 +57,18 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
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.
|
||||
/// Enables or disables event dispatch. While <c>false</c>, scans do nothing;
|
||||
/// switching back to <c>true</c> requests a scan.
|
||||
/// </summary>
|
||||
public bool IsActive
|
||||
{
|
||||
get => isActive;
|
||||
set { isActive = value; }
|
||||
set
|
||||
{
|
||||
isActive = value;
|
||||
if (value)
|
||||
RequestScan();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -83,11 +86,14 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
evt.QueueFree();
|
||||
}
|
||||
allEvents.Clear();
|
||||
endVersions.Clear();
|
||||
running = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an <see cref="ExecutableEvent"/> to the chain and reparents it via
|
||||
/// <c>AddChild</c>. Events are dispatched in insertion order.
|
||||
/// <c>AddChild</c>. Events are dispatched in insertion order; adding one
|
||||
/// requests a scan.
|
||||
/// </summary>
|
||||
/// <param name="evt">The event to register and parent to this chain.</param>
|
||||
public void AddEvent(IExecutableEvent evt)
|
||||
@@ -97,7 +103,7 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
GameLogger.Log("CHAIN", $"AddEvent: {typed.GetType().Name} [condition={id}] — calling AddChild");
|
||||
AddChild(typed);
|
||||
allEvents.Add(typed);
|
||||
_wakeup = true;
|
||||
RequestScan();
|
||||
GameLogger.Log("CHAIN", $" AddEvent: done, allEvents.Count={allEvents.Count}");
|
||||
}
|
||||
|
||||
@@ -110,21 +116,23 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
{
|
||||
if (allEvents.Remove(evt))
|
||||
{
|
||||
endVersions.Remove(evt);
|
||||
if (running == evt)
|
||||
running = null;
|
||||
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>
|
||||
/// <summary>Finds the shared <see cref="DialogBox"/>, subscribes to the scan triggers, collects child events, publishes the shell-ready event, replays any pending scene switch and requests the first scan.</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);
|
||||
ModEventBus.Subscribe<ConditionStateChangedEvent>(OnConditionStateChanged);
|
||||
|
||||
foreach (var child in GetChildren())
|
||||
{
|
||||
@@ -146,56 +154,66 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
Callable.From(() => sceneManager.SwitchScene(pending)).CallDeferred();
|
||||
}
|
||||
|
||||
RequestScan();
|
||||
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>
|
||||
/// <summary>Detaches the scan triggers when the chain leaves the tree.</summary>
|
||||
public override void _ExitTree()
|
||||
{
|
||||
eventController?.RemoveUpdateEventListener(this);
|
||||
ModEventBus.Unsubscribe<DialogEndedEvent>(OnDialogEnded);
|
||||
ModEventBus.Unsubscribe<ConditionStateChangedEvent>(OnConditionStateChanged);
|
||||
}
|
||||
|
||||
/// <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).
|
||||
/// Called when any dialog queue finishes. For the chain's own running event
|
||||
/// records the version at its dialog end, so the state changes it produced
|
||||
/// are accounted for (self-loop guard); then requests a scan so the next
|
||||
/// event starts right away.
|
||||
/// </summary>
|
||||
private void OnDialogEnded(DialogEndedEvent evt)
|
||||
{
|
||||
_wakeup = true;
|
||||
if (_lastRunEvent != null && evt.LockerId == _lastRunEvent.ID)
|
||||
_lastRunEndVersion = ConditionStateMonitor.Version;
|
||||
if (running != null && evt.LockerId == running.ID)
|
||||
{
|
||||
endVersions[running] = ConditionStateMonitor.Version;
|
||||
running = null;
|
||||
}
|
||||
RequestScan();
|
||||
}
|
||||
|
||||
/// <summary>Debounced scan that starts the next runnable event when the condition state changed since the last completed run.</summary>
|
||||
public void OnUpdate()
|
||||
private void OnConditionStateChanged(ConditionStateChangedEvent evt)
|
||||
{
|
||||
if (!isActive)
|
||||
RequestScan();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks for a scan at the end of the frame. Any number of requests within
|
||||
/// one frame result in a single scan.
|
||||
/// </summary>
|
||||
public void RequestScan()
|
||||
{
|
||||
if (scanQueued)
|
||||
return;
|
||||
scanQueued = true;
|
||||
Callable.From(Scan).CallDeferred();
|
||||
}
|
||||
|
||||
/// <summary>Starts the first runnable event, unless the chain is inactive or the dialog box is busy or frozen (a dialog end or a state change asks again).</summary>
|
||||
private void Scan()
|
||||
{
|
||||
scanQueued = false;
|
||||
if (!IsInstanceValid(this) || !IsInsideTree() || !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)
|
||||
if (dialogBox == null || !IsInstanceValid(dialogBox) || !dialogBox.IsActive || dialogBox.IsRunning)
|
||||
return;
|
||||
|
||||
var version = ConditionStateMonitor.Version;
|
||||
if (version == _lastScanVersion && !_wakeup)
|
||||
return;
|
||||
|
||||
_wakeup = false;
|
||||
_lastScanVersion = version;
|
||||
|
||||
var dialog = FindEvent(allEvents, _lastRunEvent, _lastRunEndVersion);
|
||||
var dialog = FindEvent();
|
||||
if (dialog == null)
|
||||
return;
|
||||
|
||||
if (dialog.RunDialog())
|
||||
_lastRunEvent = dialog;
|
||||
running = dialog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -213,13 +231,14 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
private ExecutableEvent FindEvent(List<ExecutableEvent> list, ExecutableEvent skipEvent, long skipEndVersion)
|
||||
private ExecutableEvent FindEvent()
|
||||
{
|
||||
if (Lists.IsEmpty(list))
|
||||
if (Lists.IsEmpty(allEvents))
|
||||
return null;
|
||||
foreach (var nextEvent in list)
|
||||
var version = ConditionStateMonitor.Version;
|
||||
foreach (var nextEvent in allEvents)
|
||||
{
|
||||
if (nextEvent == skipEvent && ConditionStateMonitor.Version <= skipEndVersion)
|
||||
if (endVersions.TryGetValue(nextEvent, out var endVersion) && version <= endVersion)
|
||||
continue;
|
||||
if (nextEvent.IsOneRun && !nextEvent.IsFirstRun)
|
||||
continue;
|
||||
|
||||
@@ -1,27 +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();
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
@@ -23,6 +24,9 @@ namespace Mods.TestRunner
|
||||
/// <c>IsDialogEnded</c> истинно до загрузки первой очереди, поэтому
|
||||
/// end-обработка ждёт, пока диалог реально наблюдался запущенным, плюс
|
||||
/// стартовая пауза 0.5с на построение сцены.
|
||||
/// With <c>--dialogs=N</c> the run passes after N <see cref="DialogEndedEvent"/>s
|
||||
/// instead: hub games never go idle, because the next scene's dialog starts
|
||||
/// in the same frame the previous one ends.
|
||||
/// </summary>
|
||||
public partial class DialogAutoDriverNode : Node
|
||||
{
|
||||
@@ -40,6 +44,7 @@ namespace Mods.TestRunner
|
||||
|
||||
private readonly List<int> choices;
|
||||
private readonly float? timeoutSeconds;
|
||||
private readonly int? dialogsTarget;
|
||||
|
||||
private IDialogBox dialogBox;
|
||||
private int choiceIndex;
|
||||
@@ -47,6 +52,7 @@ namespace Mods.TestRunner
|
||||
private double dialogFoundAtSeconds;
|
||||
private bool hasSeenRunningDialog;
|
||||
private bool finished;
|
||||
private int endedDialogs;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт драйвер; <paramref name="choices"/> — индексы вариантов по
|
||||
@@ -56,11 +62,16 @@ namespace Mods.TestRunner
|
||||
/// вне source-генератора Godot): WaitTime 0.1с с автостартом
|
||||
/// воспроизводит ритм оригинального драйвера, а сигнал
|
||||
/// <c>Timer.Timeout</c> — Godot-сигнал и работает из мод-сборок.
|
||||
/// <paramref name="dialogsTarget"/> — number of finished dialogs that passes
|
||||
/// the run (null — pass when the box goes idle).
|
||||
/// </summary>
|
||||
public DialogAutoDriverNode(List<int> choices, float? timeoutSeconds)
|
||||
public DialogAutoDriverNode(List<int> choices, float? timeoutSeconds, int? dialogsTarget)
|
||||
{
|
||||
this.choices = choices ?? new List<int>();
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
this.dialogsTarget = dialogsTarget;
|
||||
if (dialogsTarget.HasValue)
|
||||
ModEventBus.Subscribe<DialogEndedEvent>(OnDialogEnded);
|
||||
|
||||
var timer = new Timer
|
||||
{
|
||||
@@ -112,7 +123,7 @@ namespace Mods.TestRunner
|
||||
var waitingForFirstDialog = !hasSeenRunningDialog
|
||||
&& elapsedSeconds - dialogFoundAtSeconds < FirstDialogGraceSeconds;
|
||||
|
||||
if (dialogBox.IsDialogEnded && !waitingForFirstDialog)
|
||||
if (!dialogsTarget.HasValue && dialogBox.IsDialogEnded && !waitingForFirstDialog)
|
||||
{
|
||||
GameLogger.Log("TEST", "DialogAutoDriverNode: dialog ended, quitting");
|
||||
Finish(1, 0, null);
|
||||
@@ -128,6 +139,15 @@ namespace Mods.TestRunner
|
||||
dialogBox.NextAction();
|
||||
}
|
||||
|
||||
/// <summary>Counts finished dialogs and passes the run once the --dialogs= target is reached.</summary>
|
||||
private void OnDialogEnded(DialogEndedEvent evt)
|
||||
{
|
||||
endedDialogs++;
|
||||
GameLogger.Log("TEST", $"DialogAutoDriverNode: dialog #{endedDialogs} ended");
|
||||
if (endedDialogs >= dialogsTarget.Value)
|
||||
Finish(1, 0, $"{endedDialogs} dialog(s) ended, quitting");
|
||||
}
|
||||
|
||||
/// <summary>Выбирает показанный вариант по индексу из --choices= (вне диапазона — первый).</summary>
|
||||
private void HandleSelection()
|
||||
{
|
||||
@@ -167,6 +187,8 @@ namespace Mods.TestRunner
|
||||
if (finished)
|
||||
return;
|
||||
finished = true;
|
||||
if (dialogsTarget.HasValue)
|
||||
ModEventBus.Unsubscribe<DialogEndedEvent>(OnDialogEnded);
|
||||
|
||||
if (reason != null)
|
||||
GameLogger.Log("TEST", $"DialogAutoDriverNode: {reason}");
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace Mods.TestRunner
|
||||
|
||||
var choices = ParseChoices(args);
|
||||
var timeout = ParseTimeout(args);
|
||||
var dialogs = ParseDialogs(args);
|
||||
|
||||
// Godot. квалифицирован: в dev-компиляции модов (ToolsModCompileTests)
|
||||
// в глобальном пространстве имён существует namespace Engine из тестовой сборки,
|
||||
@@ -57,12 +58,12 @@ namespace Mods.TestRunner
|
||||
|
||||
driverQueued = true;
|
||||
GameLogger.Log("TEST",
|
||||
$"TestRunnerInitializer: activating dialog auto-driver, choices=[{string.Join(",", choices)}], timeout={timeout?.ToString() ?? "none"}s");
|
||||
$"TestRunnerInitializer: activating dialog auto-driver, choices=[{string.Join(",", choices)}], timeout={timeout?.ToString() ?? "none"}s, dialogs={dialogs?.ToString() ?? "idle"}");
|
||||
|
||||
// Deferred: сцена ещё строится; драйвер внутри сам выдерживает
|
||||
// стартовую паузу и лениво ищет DialogBox, поэтому порядок с
|
||||
// загрузкой сцены не важен.
|
||||
root.CallDeferred("add_child", new DialogAutoDriverNode(choices, timeout));
|
||||
root.CallDeferred("add_child", new DialogAutoDriverNode(choices, timeout, dialogs));
|
||||
}
|
||||
|
||||
private static List<int> ParseChoices(string[] args)
|
||||
@@ -80,6 +81,15 @@ namespace Mods.TestRunner
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static int? ParseDialogs(string[] args)
|
||||
{
|
||||
var raw = args
|
||||
.Where(a => a.StartsWith("--dialogs=", System.StringComparison.Ordinal))
|
||||
.Select(a => a.Substring("--dialogs=".Length))
|
||||
.FirstOrDefault();
|
||||
return int.TryParse(raw, out var n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
private static float? ParseTimeout(string[] args)
|
||||
{
|
||||
var raw = args
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ActionAnimatedForeground"/> really animates: frames are cut on the
|
||||
/// ViewBox foreground while the dialog goes on, a one-shot animation stops on its
|
||||
/// last frame, and a looping one stops when the dialog ends.
|
||||
/// </summary>
|
||||
public class ActionAnimatedForegroundTests
|
||||
{
|
||||
[ModTest("animation/one_shot_reaches_last_frame", TimeoutSeconds = 10)]
|
||||
static async Task OneShotReachesLastFrame(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var view = stage.AddViewBox();
|
||||
await DialogStage.Frames(1);
|
||||
try
|
||||
{
|
||||
var frames = Frames(3);
|
||||
var anim = new ActionAnimatedForeground { Textures = frames, Speed = 20f, NextFrameSpeed = 20f, IsLoop = false };
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Queue.Add(anim);
|
||||
dlg.Queue.Add(new ProbeAction());
|
||||
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
ModAssert.True(view.Foreground.Texture == frames[0], "first frame is shown at once");
|
||||
ModAssert.True(anim.IsAnimating, "animation outlives its NoWait action");
|
||||
await DialogStage.Seconds(0.6);
|
||||
|
||||
ModAssert.True(view.Foreground.Texture == frames[2], "animation reached the last frame");
|
||||
ModAssert.False(anim.IsAnimating, "one-shot animation stopped");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("animation/loop_stops_with_dialog_end", TimeoutSeconds = 10)]
|
||||
static async Task LoopStopsWithDialogEnd(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var view = stage.AddViewBox();
|
||||
await DialogStage.Frames(1);
|
||||
try
|
||||
{
|
||||
var frames = Frames(2);
|
||||
var anim = new ActionAnimatedForeground { Textures = frames, Speed = 20f, NextFrameSpeed = 20f, IsLoop = true };
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Queue.Add(anim);
|
||||
dlg.Queue.Add(new ProbeAction());
|
||||
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
await DialogStage.Seconds(0.3);
|
||||
ModAssert.True(anim.IsAnimating, "looping animation keeps playing while the dialog waits");
|
||||
|
||||
stage.Box.NextAction();
|
||||
ModAssert.False(anim.IsAnimating, "dialog end stops the animation");
|
||||
var shown = view.Foreground.Texture;
|
||||
await DialogStage.Seconds(0.3);
|
||||
ModAssert.True(view.Foreground.Texture == shown, "no frame is cut after the dialog ended");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Texture2D> Frames(int count)
|
||||
{
|
||||
var list = new List<Texture2D>();
|
||||
for (var i = 0; i < count; i++)
|
||||
list.Add(ImageTexture.CreateFromImage(Image.CreateEmpty(2, 2, false, Image.Format.Rgba8)));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ActionBackgroundShowHide"/> without <c>wait</c>: the dialog goes on at
|
||||
/// once and the fade still plays to the end.
|
||||
/// </summary>
|
||||
public class ActionBackgroundShowHideTests
|
||||
{
|
||||
[ModTest("background_fade/no_wait_fade_completes", TimeoutSeconds = 10)]
|
||||
static async Task NoWaitFadeCompletes(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
try
|
||||
{
|
||||
var after = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.BackgroundShowHide(BackgroundActionType.Hide, 0.1f);
|
||||
dlg.Queue.Add(after);
|
||||
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
ModAssert.Equal(1, after.Runs, "dialog does not wait for the fade");
|
||||
await DialogStage.Seconds(0.4);
|
||||
|
||||
ModAssert.True(stage.Host.Modulate.A < 0.01f, $"background faded out (alpha {stage.Host.Modulate.A})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ActionEffect"/> shows its effect: the instance lands in the ViewBox UI
|
||||
/// layer when the action runs and is freed when the dialog ends.
|
||||
/// </summary>
|
||||
public class ActionEffectTests
|
||||
{
|
||||
[ModTest("effect/spawned_into_view_until_dialog_end", TimeoutSeconds = 10)]
|
||||
static async Task SpawnedIntoViewUntilDialogEnd(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var view = stage.AddViewBox();
|
||||
var template = new Node2D { Name = "TestEffect" };
|
||||
var prefab = new PackedScene();
|
||||
prefab.Pack(template);
|
||||
template.Free();
|
||||
try
|
||||
{
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Queue.Add(new ActionEffect { Prefab = prefab });
|
||||
dlg.Queue.Add(new ProbeAction());
|
||||
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
var layer = view.GetInteractiveLayer("ui");
|
||||
ModAssert.Equal(1, layer.GetChildCount(), "effect is shown in the UI layer");
|
||||
ModAssert.Equal("TestEffect", layer.GetChild(0).Name.ToString(), "the shown node is the effect");
|
||||
|
||||
stage.Box.NextAction();
|
||||
await DialogStage.Frames(2);
|
||||
ModAssert.Equal(0, layer.GetChildCount(), "effect is freed when the dialog ends");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ActionTimedUpdate"/> completes itself: when its timeline elapses the
|
||||
/// dialog goes on, including the timer of a <c>delay</c> right after it.
|
||||
/// </summary>
|
||||
public class ActionTimedUpdateTests
|
||||
{
|
||||
[ModTest("timed_update/completion_continues_dialog", TimeoutSeconds = 10)]
|
||||
static async Task CompletionContinuesDialog(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
try
|
||||
{
|
||||
var timed = new ActionTimedUpdate { WaitTime = 0.1f };
|
||||
var after = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Queue.Add(timed);
|
||||
dlg.Delay(0.1f);
|
||||
dlg.Queue.Add(after);
|
||||
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
ModAssert.True(stage.Box.Runtime.CurrentAction == timed, "dialog waits on the timeline");
|
||||
await DialogStage.Seconds(0.8);
|
||||
|
||||
ModAssert.True(timed.IsDestructed, "timeline is finished and detached");
|
||||
ModAssert.Equal(1, after.Runs, "timeline and the delay after it elapsed, the dialog went on");
|
||||
ModAssert.True(stage.Box.Runtime.CurrentAction == after, "runtime waits on the action after the delay");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
using Cthangover.Mods.Dialog.Executable;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DialogBox"/> as the single owner of the dialog: a locker-less click
|
||||
/// dialog keeps the box busy, a debugger hold keeps a <c>delay</c> from being
|
||||
/// skipped by its timer, and choice answers advance correctly with and without a
|
||||
/// target.
|
||||
/// </summary>
|
||||
public class DialogBoxTests
|
||||
{
|
||||
[ModTest("box/click_dialog_blocks_event_dialog", TimeoutSeconds = 10)]
|
||||
static async Task ClickDialogBlocksEventDialog(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
try
|
||||
{
|
||||
var click = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Queue.Add(click);
|
||||
ModAssert.True(stage.Box.SetDialogQueueAndRun(dlg, null, 0, null), "click dialog starts");
|
||||
ModAssert.True(stage.Box.IsRunning, "a locker-less dialog occupies the box");
|
||||
|
||||
var evt = new ScenarioEvent { ScenarioText = "text \"event\"", ScenarioId = "dialog_base_test_one_run", IsOneRun = true };
|
||||
stage.Host.AddChild(evt);
|
||||
ModAssert.False(evt.RunDialog(), "event dialog is refused while the click dialog runs");
|
||||
await DialogStage.Frames(3);
|
||||
|
||||
ModAssert.Null(stage.Box.Locker, "the click dialog keeps no locker");
|
||||
ModAssert.True(stage.Box.Visible, "box stays visible");
|
||||
ModAssert.True(stage.Box.Runtime.CurrentAction == click, "click dialog still waits on its action");
|
||||
ModAssert.True(evt.IsFirstRun, "refused event is not counted as run");
|
||||
var completed = GameData.Instance?.Runtime?.CompletedScenarioIds;
|
||||
ModAssert.False(completed != null && completed.Contains("dialog_base_test_one_run"), "refused one-run scenario is not marked completed");
|
||||
|
||||
var other = new DialogQueue();
|
||||
other.Queue.Add(new ProbeAction());
|
||||
ModAssert.False(stage.Box.SetDialogQueueAndRun(other, null, 0, null), "a second click dialog is refused too");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("box/held_delay_is_not_skipped", TimeoutSeconds = 10)]
|
||||
static async Task HeldDelayIsNotSkipped(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var previous = DialogRuntime.Debugger;
|
||||
try
|
||||
{
|
||||
var delay = new ActionDelay { WaitTime = 0.05f };
|
||||
var after = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Queue.Add(delay);
|
||||
dlg.Queue.Add(after);
|
||||
DialogRuntime.Debugger = new HoldDebugger { Target = delay };
|
||||
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
ModAssert.True(stage.Box.Runtime.IsHeld, "runtime is held before the delay");
|
||||
await DialogStage.Seconds(0.3);
|
||||
|
||||
ModAssert.True(stage.Box.Runtime.IsHeld, "hold survives the delay's duration");
|
||||
ModAssert.True(stage.Box.Runtime.CurrentAction == delay, "held delay stays current");
|
||||
ModAssert.Equal(0, after.Runs, "action after the held delay has not run");
|
||||
|
||||
DialogRuntime.Debugger = null;
|
||||
stage.Box.Resume();
|
||||
await DialogStage.Seconds(0.3);
|
||||
|
||||
ModAssert.Equal(1, after.Runs, "after resume the delay elapses and the dialog goes on");
|
||||
ModAssert.True(stage.Box.Runtime.CurrentAction == after, "runtime waits on the action after the delay");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DialogRuntime.Debugger = previous;
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("box/option_without_target_falls_through", TimeoutSeconds = 10)]
|
||||
static async Task OptionWithoutTargetFallsThrough(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
try
|
||||
{
|
||||
var after = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Select(new List<SelectVariant> { SelectVariant.New("go on", null) }, "choose");
|
||||
dlg.Queue.Add(after);
|
||||
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
ModAssert.True(stage.Box.IsWaitingAnswer, "choice is shown");
|
||||
ModAssert.True(stage.Box.TrySelectVariant(0), "variant is picked");
|
||||
|
||||
ModAssert.False(stage.Box.IsWaitingAnswer, "the same choice is not shown again");
|
||||
ModAssert.Equal(1, after.Runs, "action after the choice runs");
|
||||
ModAssert.True(stage.Box.Runtime.CurrentAction == after, "runtime waits on the action after the choice");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("box/option_to_missing_label_ends_once", TimeoutSeconds = 10)]
|
||||
static async Task OptionToMissingLabelEndsOnce(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var recorder = new DialogEndRecorder();
|
||||
recorder.Start();
|
||||
try
|
||||
{
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Select(new List<SelectVariant> { SelectVariant.New("broken", "no_such_label") }, "choose");
|
||||
dlg.Queue.Add(new ProbeAction());
|
||||
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
stage.Box.TrySelectVariant(0);
|
||||
|
||||
ModAssert.Equal(1, recorder.LockerIds.Count, "DialogEndedEvent count after an option to a missing label");
|
||||
ModAssert.False(stage.Box.IsRunning, "box is free");
|
||||
}
|
||||
finally
|
||||
{
|
||||
recorder.Stop();
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Records every <see cref="DialogEndedEvent"/> published on the mod event bus
|
||||
/// between <see cref="Start"/> and <see cref="Stop"/> — the locker id of each, in
|
||||
/// publication order. Tests count how often a queue reported its end.
|
||||
/// </summary>
|
||||
public class DialogEndRecorder
|
||||
{
|
||||
/// <summary>Locker ids of the recorded events (null for a locker-less dialog).</summary>
|
||||
public List<string> LockerIds { get; } = new();
|
||||
|
||||
/// <summary>Subscribes to the event bus.</summary>
|
||||
public void Start()
|
||||
{
|
||||
ModEventBus.Subscribe<DialogEndedEvent>(OnDialogEnded);
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes; call it in <c>finally</c> so a failed test leaves no handler behind.</summary>
|
||||
public void Stop()
|
||||
{
|
||||
ModEventBus.Unsubscribe<DialogEndedEvent>(OnDialogEnded);
|
||||
}
|
||||
|
||||
private void OnDialogEnded(DialogEndedEvent evt)
|
||||
{
|
||||
LockerIds.Add(evt.LockerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
|
||||
/// <summary>
|
||||
/// Queue mechanics of <see cref="DialogRuntime"/>: a queue ends exactly once however
|
||||
/// it reaches the end, actions reached again through a looping goto get a full
|
||||
/// construct/destruct life every pass, and a fall-through branch does not skip the
|
||||
/// action after it.
|
||||
/// </summary>
|
||||
public class DialogRuntimeTests
|
||||
{
|
||||
[ModTest("runtime/end_command_publishes_dialog_ended_once", TimeoutSeconds = 10)]
|
||||
static async Task EndCommandPublishesOnce(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var recorder = new DialogEndRecorder();
|
||||
recorder.Start();
|
||||
try
|
||||
{
|
||||
var after = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Set("x", "1");
|
||||
dlg.End();
|
||||
dlg.Queue.Add(after);
|
||||
|
||||
ModAssert.True(stage.Box.SetDialogQueueAndRun(dlg, null, 0, null), "dialog starts");
|
||||
|
||||
ModAssert.Equal(1, recorder.LockerIds.Count, "DialogEndedEvent count after 'end'");
|
||||
ModAssert.Equal(0, after.Runs, "action after 'end' never runs");
|
||||
ModAssert.False(stage.Box.IsRunning, "box is free after 'end'");
|
||||
}
|
||||
finally
|
||||
{
|
||||
recorder.Stop();
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("runtime/goto_missing_label_publishes_dialog_ended_once", TimeoutSeconds = 10)]
|
||||
static async Task GotoMissingLabelPublishesOnce(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var recorder = new DialogEndRecorder();
|
||||
recorder.Start();
|
||||
try
|
||||
{
|
||||
var after = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.GoTo("no_such_label");
|
||||
dlg.Queue.Add(after);
|
||||
|
||||
stage.Box.SetDialogQueueAndRun(dlg, null, 0, null);
|
||||
|
||||
ModAssert.Equal(1, recorder.LockerIds.Count, "DialogEndedEvent count after a goto to a missing label");
|
||||
ModAssert.Equal(0, after.Runs, "action after the broken goto never runs");
|
||||
ModAssert.False(stage.Box.IsRunning, "box is free after the broken goto");
|
||||
}
|
||||
finally
|
||||
{
|
||||
recorder.Stop();
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("runtime/goto_loop_destructs_every_pass")]
|
||||
static void GotoLoopDestructsEveryPass(ModTestContext ctx)
|
||||
{
|
||||
var probe = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Point("loop");
|
||||
dlg.Queue.Add(probe);
|
||||
dlg.GoTo("loop");
|
||||
|
||||
var runtime = new DialogRuntime();
|
||||
runtime.SetDialogQueueAndRun(null, dlg.Queue, null);
|
||||
runtime.Run();
|
||||
for (var pass = 0; pass < 2; pass++)
|
||||
{
|
||||
runtime.Next();
|
||||
runtime.Run();
|
||||
}
|
||||
|
||||
ModAssert.Equal(3, probe.Runs, "probe ran on every pass");
|
||||
ModAssert.Equal(3, probe.Constructs, "probe constructed on every pass");
|
||||
ModAssert.Equal(2, probe.Destructs, "probe destructed after every finished pass");
|
||||
ModAssert.True(runtime.CurrentAction == probe, "runtime waits on the probe again");
|
||||
runtime.Abort();
|
||||
ModAssert.Equal(3, probe.Destructs, "abort destructs the live pass");
|
||||
}
|
||||
|
||||
[ModTest("runtime/if_fall_through_runs_next_action")]
|
||||
static void IfFallThroughRunsNextAction(ModTestContext ctx)
|
||||
{
|
||||
var probe = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.IfGoTo(AlwaysTrue, null, null);
|
||||
dlg.Queue.Add(probe);
|
||||
|
||||
var runtime = new DialogRuntime();
|
||||
runtime.SetDialogQueueAndRun(null, dlg.Queue, null);
|
||||
runtime.Run();
|
||||
|
||||
ModAssert.Equal(1, probe.Runs, "action after a fall-through if runs");
|
||||
ModAssert.True(runtime.CurrentAction == probe, "runtime waits on the action after the if");
|
||||
}
|
||||
|
||||
[ModTest("runtime/if_without_condition_runs_next_action")]
|
||||
static void IfWithoutConditionRunsNextAction(ModTestContext ctx)
|
||||
{
|
||||
var probe = new ProbeAction();
|
||||
var dlg = new DialogQueue();
|
||||
dlg.Queue.Add(new ActionIf { TrueGoTo = "nowhere" });
|
||||
dlg.Queue.Add(probe);
|
||||
|
||||
var runtime = new DialogRuntime();
|
||||
runtime.SetDialogQueueAndRun(null, dlg.Queue, null);
|
||||
runtime.Run();
|
||||
|
||||
ModAssert.Equal(1, probe.Runs, "action after an if without condition runs");
|
||||
ModAssert.True(runtime.CurrentAction == probe, "runtime waits on the action after the if");
|
||||
}
|
||||
|
||||
private static bool AlwaysTrue() => true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Mods.Interface.View;
|
||||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// A minimal live dialog stage for mod tests: a host Control under <c>/root</c>
|
||||
/// with the real <c>dialog_box.tscn</c> prefab (named <c>DialogBox</c>, so
|
||||
/// <see cref="SceneContextNode.FindNode{T}"/> and the event classes find it like in
|
||||
/// a scene) and, on request, a bare <see cref="ViewBox"/> with a foreground slot.
|
||||
/// The scene-context lookup cache is cleared on build and on <see cref="Free"/>:
|
||||
/// it remembers misses, and a test box must be found even if an earlier lookup
|
||||
/// failed. The host is the box's parent Control — background fades animate it.
|
||||
/// </summary>
|
||||
public class DialogStage
|
||||
{
|
||||
/// <summary>Parent of every stage node; freed by <see cref="Free"/>.</summary>
|
||||
public Control Host { get; private set; }
|
||||
|
||||
/// <summary>The dialog box built from its prefab.</summary>
|
||||
public DialogBox Box { get; private set; }
|
||||
|
||||
/// <summary>The view box, after <see cref="AddViewBox"/>.</summary>
|
||||
public ViewBox View { get; private set; }
|
||||
|
||||
/// <summary>The running scene tree.</summary>
|
||||
public static SceneTree Tree => (SceneTree)Engine.GetMainLoop();
|
||||
|
||||
/// <summary>Builds the host and the dialog box and waits a frame for their <c>_Ready</c>.</summary>
|
||||
public static async Task<DialogStage> Create()
|
||||
{
|
||||
SceneContextNode.Instance?.ClearData();
|
||||
var stage = new DialogStage { Host = new Control { Name = "DialogTestHost" } };
|
||||
Tree.Root.AddChild(stage.Host);
|
||||
|
||||
stage.Box = new ModScenePrefab("scenes/ui/dialog_box.tscn").Instantiate<DialogBox>();
|
||||
ModAssert.NotNull(stage.Box, "dialog_box.tscn builds a DialogBox");
|
||||
stage.Box.Name = "DialogBox";
|
||||
stage.Host.AddChild(stage.Box);
|
||||
await Frames(1);
|
||||
return stage;
|
||||
}
|
||||
|
||||
/// <summary>Adds a <see cref="ViewBox"/> named <c>ViewBox</c> with a <c>Foreground</c> TextureRect.</summary>
|
||||
public ViewBox AddViewBox()
|
||||
{
|
||||
View = new ViewBox { Name = "ViewBox" };
|
||||
View.AddChild(new TextureRect { Name = "Foreground" });
|
||||
Host.AddChild(View);
|
||||
SceneContextNode.Instance?.ClearData();
|
||||
return View;
|
||||
}
|
||||
|
||||
/// <summary>Removes the stage from the tree and forgets cached lookups of its nodes.</summary>
|
||||
public void Free()
|
||||
{
|
||||
if (Box != null && Box.IsRunning)
|
||||
Box.Abort();
|
||||
if (GodotObject.IsInstanceValid(Host))
|
||||
{
|
||||
Tree.Root.RemoveChild(Host);
|
||||
Host.QueueFree();
|
||||
}
|
||||
SceneContextNode.Instance?.ClearData();
|
||||
}
|
||||
|
||||
/// <summary>Waits <paramref name="count"/> process frames (deferred calls run at the end of each).</summary>
|
||||
public static async Task Frames(int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
await Tree.ToSignal(Tree, SceneTree.SignalName.ProcessFrame);
|
||||
}
|
||||
|
||||
/// <summary>Waits <paramref name="seconds"/> of game time.</summary>
|
||||
public static async Task Seconds(double seconds)
|
||||
{
|
||||
await Tree.ToSignal(Tree.CreateTimer(seconds), SceneTreeTimer.SignalName.Timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Mods.Dialog.Executable;
|
||||
|
||||
/// <summary>
|
||||
/// A standalone <see cref="ExecutableEvent"/> (not inside a chain): a run the dialog
|
||||
/// box refused is not counted, and the event starts by itself on the next state
|
||||
/// change, without polling.
|
||||
/// </summary>
|
||||
public class ExecutableEventTests
|
||||
{
|
||||
[ModTest("event/refused_run_is_not_counted", TimeoutSeconds = 10)]
|
||||
static async Task RefusedRunIsNotCounted(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
try
|
||||
{
|
||||
stage.Box.IsActive = false;
|
||||
var evt = new ScenarioEvent { ScenarioText = "text \"S\"", ScenarioId = "dialog_base_test_standalone", IsOneRun = true };
|
||||
stage.Host.AddChild(evt);
|
||||
await DialogStage.Frames(2);
|
||||
|
||||
ModAssert.False(stage.Box.IsRunning, "inactive box refused the event");
|
||||
ModAssert.True(evt.IsFirstRun, "refused automatic run is not counted");
|
||||
ModAssert.False(evt.RunDialog(), "direct run is refused too");
|
||||
ModAssert.True(evt.IsFirstRun, "refused direct run is not counted");
|
||||
|
||||
stage.Box.IsActive = true;
|
||||
ConditionStateMonitor.MarkStateChanged();
|
||||
await DialogStage.Frames(2);
|
||||
|
||||
ModAssert.True(stage.Box.Locker == evt, "one-run event starts on the next state change");
|
||||
ModAssert.False(evt.IsFirstRun, "the real run is counted");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Mods.Dialog.Executable;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ExecutableMainEventChain"/> is event-driven and loop-safe: the next
|
||||
/// event starts within a couple of frames after a dialog ends (no polling delay),
|
||||
/// two repeatable events do not ping-pong while the state stays the same, and a
|
||||
/// state change makes them eligible again.
|
||||
/// </summary>
|
||||
public class ExecutableMainEventChainTests
|
||||
{
|
||||
[ModTest("chain/push_start_and_no_ping_pong", TimeoutSeconds = 15)]
|
||||
static async Task PushStartAndNoPingPong(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
try
|
||||
{
|
||||
var chain = new ExecutableMainEventChain { Name = "TestChain" };
|
||||
stage.Host.AddChild(chain);
|
||||
var a = new ScenarioEvent { ScenarioText = "text \"A\"", ScenarioId = "dialog_base_test_chain_a" };
|
||||
var b = new ScenarioEvent { ScenarioText = "text \"B\"", ScenarioId = "dialog_base_test_chain_b" };
|
||||
chain.AddEvent(a);
|
||||
chain.AddEvent(b);
|
||||
|
||||
await DialogStage.Frames(2);
|
||||
ModAssert.True(stage.Box.Locker == a, "first event starts right after it was added");
|
||||
|
||||
stage.Box.NextAction();
|
||||
await DialogStage.Frames(2);
|
||||
ModAssert.True(stage.Box.Locker == b, "second event starts within two frames after the first ended");
|
||||
|
||||
stage.Box.NextAction();
|
||||
await DialogStage.Frames(4);
|
||||
ModAssert.False(stage.Box.IsRunning, "no event re-runs while the state did not change");
|
||||
|
||||
ConditionStateMonitor.MarkStateChanged();
|
||||
await DialogStage.Frames(2);
|
||||
ModAssert.True(stage.Box.Locker == a, "a state change makes the first event eligible again");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
|
||||
/// <summary>
|
||||
/// Debugger stand-in that holds the runtime once, right before
|
||||
/// <see cref="Target"/> — what the scenario studio does on a breakpoint.
|
||||
/// </summary>
|
||||
public class HoldDebugger : IDialogDebugger
|
||||
{
|
||||
private bool held;
|
||||
|
||||
/// <summary>The action to hold before.</summary>
|
||||
public IActionCommand Target { get; set; }
|
||||
|
||||
/// <summary>Holds before <see cref="Target"/> the first time it is reached.</summary>
|
||||
public bool ShouldHold(DialogRuntime runtime, IActionCommand action)
|
||||
{
|
||||
if (held || action != Target)
|
||||
return false;
|
||||
held = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.UI.Event;
|
||||
|
||||
/// <summary>
|
||||
/// Update listener for <see cref="SceneEventController"/> tests: appends its
|
||||
/// <see cref="Name"/> to a shared <see cref="Log"/> on every update and may change
|
||||
/// the controller's listener list from inside the callback — remove
|
||||
/// <see cref="RemoveOnUpdate"/>, add <see cref="AddOnUpdate"/> — to check how a
|
||||
/// dispatch copes with a list changed under it.
|
||||
/// </summary>
|
||||
public class OrderListener : IOnUpdateEvent
|
||||
{
|
||||
/// <summary>Name written to <see cref="Log"/>.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>Dispatch priority.</summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>Shared call log.</summary>
|
||||
public List<string> Log { get; set; }
|
||||
|
||||
/// <summary>Controller whose list the callback changes.</summary>
|
||||
public SceneEventController Controller { get; set; }
|
||||
|
||||
/// <summary>Listener to unregister during the update, if any.</summary>
|
||||
public IOnUpdateEvent RemoveOnUpdate { get; set; }
|
||||
|
||||
/// <summary>Listener to register during the update, if any.</summary>
|
||||
public IOnUpdateEvent AddOnUpdate { get; set; }
|
||||
|
||||
/// <summary>Logs the call and applies the requested list changes once.</summary>
|
||||
public void OnUpdate()
|
||||
{
|
||||
Log.Add(Name);
|
||||
if (RemoveOnUpdate != null)
|
||||
{
|
||||
Controller.RemoveUpdateEventListener(RemoveOnUpdate);
|
||||
RemoveOnUpdate = null;
|
||||
}
|
||||
if (AddOnUpdate != null)
|
||||
{
|
||||
Controller.AddUpdateEventListener(AddOnUpdate);
|
||||
AddOnUpdate = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// Test action that only counts its lifecycle calls: how often the runtime ran,
|
||||
/// constructed and destructed it. Waits for a click by default, so a test can
|
||||
/// park the dialog on it and look at where the runtime stands.
|
||||
/// </summary>
|
||||
public class ProbeAction : ActionCommand
|
||||
{
|
||||
/// <summary>Number of <see cref="DoRun"/> calls.</summary>
|
||||
public int Runs { get; private set; }
|
||||
|
||||
/// <summary>Number of <see cref="DoConstruct"/> calls.</summary>
|
||||
public int Constructs { get; private set; }
|
||||
|
||||
/// <summary>Number of <see cref="DoDestruct"/> calls.</summary>
|
||||
public int Destructs { get; private set; }
|
||||
|
||||
/// <summary>Counts the run.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
Runs++;
|
||||
}
|
||||
|
||||
/// <summary>Counts the construction.</summary>
|
||||
public override void DoConstruct()
|
||||
{
|
||||
Constructs++;
|
||||
}
|
||||
|
||||
/// <summary>Counts the destruction.</summary>
|
||||
public override void DoDestruct()
|
||||
{
|
||||
Destructs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Core.UI.Event;
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch contract of the core <see cref="SceneEventController"/> (tested here:
|
||||
/// it is a Godot node, xUnit cannot create it; the dialog actions are its main
|
||||
/// listeners): lower priority first, registration order within a priority, and a
|
||||
/// listener list changed during a dispatch neither skips nor double-calls anyone.
|
||||
/// </summary>
|
||||
public class SceneEventControllerTests
|
||||
{
|
||||
[ModTest("event_controller/lower_priority_first_then_registration_order")]
|
||||
static void LowerPriorityFirst(ModTestContext ctx)
|
||||
{
|
||||
var log = new List<string>();
|
||||
var controller = new SceneEventController();
|
||||
try
|
||||
{
|
||||
controller.AddUpdateEventListener(new OrderListener { Name = "b", Priority = 1, Log = log });
|
||||
controller.AddUpdateEventListener(new OrderListener { Name = "a", Priority = 0, Log = log });
|
||||
controller.AddUpdateEventListener(new OrderListener { Name = "c", Priority = 1, Log = log });
|
||||
|
||||
controller._Process(0.0);
|
||||
|
||||
ModAssert.Equal("a,b,c", string.Join(",", log), "dispatch order");
|
||||
}
|
||||
finally
|
||||
{
|
||||
controller.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("event_controller/list_changed_during_dispatch")]
|
||||
static void ListChangedDuringDispatch(ModTestContext ctx)
|
||||
{
|
||||
var log = new List<string>();
|
||||
var controller = new SceneEventController();
|
||||
try
|
||||
{
|
||||
var removed = new OrderListener { Name = "removed", Priority = 1, Log = log };
|
||||
var added = new OrderListener { Name = "added", Priority = 0, Log = log };
|
||||
var changer = new OrderListener
|
||||
{
|
||||
Name = "changer", Priority = 0, Log = log, Controller = controller,
|
||||
RemoveOnUpdate = removed, AddOnUpdate = added,
|
||||
};
|
||||
var last = new OrderListener { Name = "last", Priority = 2, Log = log };
|
||||
controller.AddUpdateEventListener(changer);
|
||||
controller.AddUpdateEventListener(removed);
|
||||
controller.AddUpdateEventListener(last);
|
||||
|
||||
controller._Process(0.0);
|
||||
ModAssert.Equal("changer,last", string.Join(",", log), "first dispatch: removed one skipped, added one waits");
|
||||
|
||||
log.Clear();
|
||||
controller._Process(0.0);
|
||||
ModAssert.Equal("changer,added,last", string.Join(",", log), "second dispatch includes the added listener");
|
||||
}
|
||||
finally
|
||||
{
|
||||
controller.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Dialog.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// The scenario <c>flag</c> command sets a story flag of the game: once the
|
||||
/// dialog reaches it, scenario conditions see the value, the game state
|
||||
/// holds it (so saves carry it), and conditions are rescanned.
|
||||
/// </summary>
|
||||
public class StoryFlagTests
|
||||
{
|
||||
private const string Flag = "__test_story_flag";
|
||||
|
||||
[ModTest("flags/flag_command_sets_game_flag", TimeoutSeconds = 10)]
|
||||
static async Task FlagCommandSetsGameFlag(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var flags = GameData.Instance.Runtime.Flags;
|
||||
try
|
||||
{
|
||||
var dlg = new ScenarioConverter().Convert($"flag {Flag} open\nend\n");
|
||||
var versionBefore = ConditionStateMonitor.Version;
|
||||
|
||||
ModAssert.False(ScenarioCondition.Evaluate($"{Flag} == \"open\""), "condition false before the dialog");
|
||||
ModAssert.True(stage.Box.SetDialogQueueAndRun(dlg, null, 0, null), "dialog starts");
|
||||
|
||||
ModAssert.True(ScenarioCondition.Evaluate($"{Flag} == \"open\""), "condition sees the flag");
|
||||
ModAssert.Equal("open", flags.TryGetValue(Flag, out var value) ? value : null, "game state holds the flag");
|
||||
ModAssert.True(ConditionStateMonitor.Version > versionBefore, "conditions are rescanned");
|
||||
}
|
||||
finally
|
||||
{
|
||||
flags.Remove(Flag);
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user