Line endings: LF everywhere
.gitattributes forces LF (CRLF only for .bat/.cmd), core.autocrlf is off; files that differed only by CRLF are normalized.
This commit is contained in:
1 parent
392b1caf0a
commit
1018c90c25
28 files changed
+1713
-1683
No files matched your search
@@ -1,79 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Central action-execution dispatcher. Resolves an action ID to an
|
||||
/// IActionExecutor in two tiers: first the active provider (set by
|
||||
/// the current IBattleCore, allowing per-battle-engine overrides),
|
||||
/// then a global fallback registry. This two-level lookup lets mods
|
||||
/// supply custom executors for specific battle cores without replacing
|
||||
/// the global ones. Execute returns ChangedAttributes to allow the
|
||||
/// caller to inspect the result (success/failure, stat deltas).
|
||||
/// </summary>
|
||||
public class ActionExecutorHub
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton hub instance. There is one global dispatcher shared
|
||||
/// across all battle cores; cores swap providers via
|
||||
/// <see cref="SetActiveProvider"/> when they activate.
|
||||
/// </summary>
|
||||
public static readonly ActionExecutorHub Instance = new();
|
||||
|
||||
private IActionExecutorProvider _activeProvider;
|
||||
private readonly Dictionary<string, IActionExecutor> _globalExecutors = new();
|
||||
|
||||
/// <summary>
|
||||
/// Installs a per-core executor provider. The active provider is
|
||||
/// consulted before the global registry, allowing the current
|
||||
/// battle core to override specific action handlers.
|
||||
/// </summary>
|
||||
public void SetActiveProvider(IActionExecutorProvider provider)
|
||||
{
|
||||
_activeProvider = provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a fallback executor in the global pool, keyed by
|
||||
/// <see cref="IActionExecutor.ActionId"/>. Global executors are
|
||||
/// used when no active provider supplies a match.
|
||||
/// </summary>
|
||||
public void RegisterGlobal(IActionExecutor executor)
|
||||
{
|
||||
if (executor != null && !string.IsNullOrEmpty(executor.ActionId))
|
||||
_globalExecutors[executor.ActionId] = executor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an action to the appropriate executor. Resolution
|
||||
/// order: active provider first, then global registry. Returns
|
||||
/// <c>Result = false</c> if no executor is found or if
|
||||
/// <paramref name="action"/> is null.
|
||||
/// </summary>
|
||||
public ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
|
||||
{
|
||||
if (action == null)
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
var executor = Resolve(action.ID);
|
||||
if (executor == null)
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
return executor.Execute(action, user, target);
|
||||
}
|
||||
|
||||
private IActionExecutor Resolve(string actionId)
|
||||
{
|
||||
if (_activeProvider != null)
|
||||
{
|
||||
var executor = _activeProvider.GetExecutor(actionId);
|
||||
if (executor != null)
|
||||
return executor;
|
||||
}
|
||||
|
||||
_globalExecutors.TryGetValue(actionId, out var fallback);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Central action-execution dispatcher. Resolves an action ID to an
|
||||
/// IActionExecutor in two tiers: first the active provider (set by
|
||||
/// the current IBattleCore, allowing per-battle-engine overrides),
|
||||
/// then a global fallback registry. This two-level lookup lets mods
|
||||
/// supply custom executors for specific battle cores without replacing
|
||||
/// the global ones. Execute returns ChangedAttributes to allow the
|
||||
/// caller to inspect the result (success/failure, stat deltas).
|
||||
/// </summary>
|
||||
public class ActionExecutorHub
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton hub instance. There is one global dispatcher shared
|
||||
/// across all battle cores; cores swap providers via
|
||||
/// <see cref="SetActiveProvider"/> when they activate.
|
||||
/// </summary>
|
||||
public static readonly ActionExecutorHub Instance = new();
|
||||
|
||||
private IActionExecutorProvider _activeProvider;
|
||||
private readonly Dictionary<string, IActionExecutor> _globalExecutors = new();
|
||||
|
||||
/// <summary>
|
||||
/// Installs a per-core executor provider. The active provider is
|
||||
/// consulted before the global registry, allowing the current
|
||||
/// battle core to override specific action handlers.
|
||||
/// </summary>
|
||||
public void SetActiveProvider(IActionExecutorProvider provider)
|
||||
{
|
||||
_activeProvider = provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a fallback executor in the global pool, keyed by
|
||||
/// <see cref="IActionExecutor.ActionId"/>. Global executors are
|
||||
/// used when no active provider supplies a match.
|
||||
/// </summary>
|
||||
public void RegisterGlobal(IActionExecutor executor)
|
||||
{
|
||||
if (executor != null && !string.IsNullOrEmpty(executor.ActionId))
|
||||
_globalExecutors[executor.ActionId] = executor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an action to the appropriate executor. Resolution
|
||||
/// order: active provider first, then global registry. Returns
|
||||
/// <c>Result = false</c> if no executor is found or if
|
||||
/// <paramref name="action"/> is null.
|
||||
/// </summary>
|
||||
public ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
|
||||
{
|
||||
if (action == null)
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
var executor = Resolve(action.ID);
|
||||
if (executor == null)
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
return executor.Execute(action, user, target);
|
||||
}
|
||||
|
||||
private IActionExecutor Resolve(string actionId)
|
||||
{
|
||||
if (_activeProvider != null)
|
||||
{
|
||||
var executor = _activeProvider.GetExecutor(actionId);
|
||||
if (executor != null)
|
||||
return executor;
|
||||
}
|
||||
|
||||
_globalExecutors.TryGetValue(actionId, out var fallback);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
-121
@@ -1,122 +1,122 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.UI;
|
||||
using Godot;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.UI;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base;
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Sequential action queue that processes one IBattleAction per frame
|
||||
/// until it signals completion (DoAction returns true), then moves to
|
||||
/// the next. Fires OnStopMachine when the queue drains naturally or
|
||||
/// is explicitly stopped — used by battle cores to detect when a turn's
|
||||
/// animation sequence is finished. Actions can be added before or during
|
||||
/// processing; the machine starts automatically once at least one action
|
||||
/// is queued and no current action is running.
|
||||
/// <para/>
|
||||
/// Derives from <see cref="ModNode"/> so the engine lifecycle callbacks reach
|
||||
/// this runtime-compiled script through the core base class (mod assemblies
|
||||
/// have no Godot source generation of their own).
|
||||
/// </summary>
|
||||
public partial class BattleActionMachine : ModNode
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Pending action queue. Actions are dequeued one-by-one in the process
|
||||
/// hook, starting the next as soon as the current signals completion.
|
||||
/// Can be extended while the machine is running — new actions will
|
||||
/// be processed after the current queue drains.
|
||||
/// </summary>
|
||||
public List<IBattleAction> Actions { get; set; } = new();
|
||||
private IBattleAction CurrentAction { get; set; }
|
||||
private bool _hadActions;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the machine drains naturally or is explicitly stopped.
|
||||
/// Battle cores subscribe to advance the turn phase without polling.
|
||||
/// </summary>
|
||||
public event StopMachineDelegate OnStopMachine;
|
||||
|
||||
/// <summary>Stops the machine and fires <see cref="OnStopMachine"/> when the node leaves the tree, so subscribers never wait on a dead node.</summary>
|
||||
protected override void OnExitTree()
|
||||
{
|
||||
base.OnExitTree();
|
||||
StopMachine();
|
||||
}
|
||||
|
||||
/// <summary>Drives the queued actions once per frame; fires <see cref="OnStopMachine"/> when the queue drains.</summary>
|
||||
protected override void OnProcess(double delta)
|
||||
{
|
||||
if (Actions.Count == 0 && CurrentAction == null)
|
||||
{
|
||||
if (_hadActions)
|
||||
{
|
||||
_hadActions = false;
|
||||
StopMachine();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var action = CurrentAction;
|
||||
if (action == null)
|
||||
{
|
||||
CurrentAction = Actions[0];
|
||||
Actions.RemoveAt(0);
|
||||
action = CurrentAction;
|
||||
_hadActions = true;
|
||||
action?.DoStart();
|
||||
}
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
_hadActions = false;
|
||||
StopMachine();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.DoAction())
|
||||
{
|
||||
action.DoEnd();
|
||||
CurrentAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a single action. If the machine is idle (no current
|
||||
/// action), processing starts on the next _Process tick.
|
||||
/// </summary>
|
||||
public void AddAction(IBattleAction action)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
Actions.Add(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch-enqueues multiple actions. All are appended to the existing
|
||||
/// queue and processed in FIFO order on subsequent frames.
|
||||
/// </summary>
|
||||
public void AddActions(List<IBattleAction> actions)
|
||||
{
|
||||
if(actions == null || actions.Count == 0)
|
||||
return;
|
||||
Actions.AddRange(actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately clears the queue, aborts the current action, and
|
||||
/// fires <see cref="OnStopMachine"/>. Called on tree exit and
|
||||
/// when the queue drains naturally.
|
||||
/// </summary>
|
||||
public void StopMachine()
|
||||
{
|
||||
CurrentAction = null;
|
||||
Actions.Clear();
|
||||
_hadActions = false;
|
||||
OnStopMachine?.Invoke();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Sequential action queue that processes one IBattleAction per frame
|
||||
/// until it signals completion (DoAction returns true), then moves to
|
||||
/// the next. Fires OnStopMachine when the queue drains naturally or
|
||||
/// is explicitly stopped — used by battle cores to detect when a turn's
|
||||
/// animation sequence is finished. Actions can be added before or during
|
||||
/// processing; the machine starts automatically once at least one action
|
||||
/// is queued and no current action is running.
|
||||
/// <para/>
|
||||
/// Derives from <see cref="ModNode"/> so the engine lifecycle callbacks reach
|
||||
/// this runtime-compiled script through the core base class (mod assemblies
|
||||
/// have no Godot source generation of their own).
|
||||
/// </summary>
|
||||
public partial class BattleActionMachine : ModNode
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Pending action queue. Actions are dequeued one-by-one in the process
|
||||
/// hook, starting the next as soon as the current signals completion.
|
||||
/// Can be extended while the machine is running — new actions will
|
||||
/// be processed after the current queue drains.
|
||||
/// </summary>
|
||||
public List<IBattleAction> Actions { get; set; } = new();
|
||||
private IBattleAction CurrentAction { get; set; }
|
||||
private bool _hadActions;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the machine drains naturally or is explicitly stopped.
|
||||
/// Battle cores subscribe to advance the turn phase without polling.
|
||||
/// </summary>
|
||||
public event StopMachineDelegate OnStopMachine;
|
||||
|
||||
/// <summary>Stops the machine and fires <see cref="OnStopMachine"/> when the node leaves the tree, so subscribers never wait on a dead node.</summary>
|
||||
protected override void OnExitTree()
|
||||
{
|
||||
base.OnExitTree();
|
||||
StopMachine();
|
||||
}
|
||||
|
||||
/// <summary>Drives the queued actions once per frame; fires <see cref="OnStopMachine"/> when the queue drains.</summary>
|
||||
protected override void OnProcess(double delta)
|
||||
{
|
||||
if (Actions.Count == 0 && CurrentAction == null)
|
||||
{
|
||||
if (_hadActions)
|
||||
{
|
||||
_hadActions = false;
|
||||
StopMachine();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var action = CurrentAction;
|
||||
if (action == null)
|
||||
{
|
||||
CurrentAction = Actions[0];
|
||||
Actions.RemoveAt(0);
|
||||
action = CurrentAction;
|
||||
_hadActions = true;
|
||||
action?.DoStart();
|
||||
}
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
_hadActions = false;
|
||||
StopMachine();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.DoAction())
|
||||
{
|
||||
action.DoEnd();
|
||||
CurrentAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a single action. If the machine is idle (no current
|
||||
/// action), processing starts on the next _Process tick.
|
||||
/// </summary>
|
||||
public void AddAction(IBattleAction action)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
Actions.Add(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch-enqueues multiple actions. All are appended to the existing
|
||||
/// queue and processed in FIFO order on subsequent frames.
|
||||
/// </summary>
|
||||
public void AddActions(List<IBattleAction> actions)
|
||||
{
|
||||
if(actions == null || actions.Count == 0)
|
||||
return;
|
||||
Actions.AddRange(actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately clears the queue, aborts the current action, and
|
||||
/// fires <see cref="OnStopMachine"/>. Called on tree exit and
|
||||
/// when the queue drains naturally.
|
||||
/// </summary>
|
||||
public void StopMachine()
|
||||
{
|
||||
CurrentAction = null;
|
||||
Actions.Clear();
|
||||
_hadActions = false;
|
||||
OnStopMachine?.Invoke();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a specific battle action. The executor is keyed by ActionId
|
||||
/// (matching the action's ID) so the hub can look it up without
|
||||
/// knowing the concrete type. Returns ChangedAttributes so callers
|
||||
/// can apply stat deltas and check whether the action succeeded.
|
||||
/// </summary>
|
||||
public interface IActionExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// String key that matches <see cref="ActionCharacter.ID"/>.
|
||||
/// The hub uses this to resolve executors without knowing
|
||||
/// concrete types.
|
||||
/// </summary>
|
||||
string ActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the action's effects. Receives the action definition
|
||||
/// and the user/target <see cref="Character"/> instances; returns
|
||||
/// <see cref="ChangedAttributes"/> with deltas and success/failure.
|
||||
/// </summary>
|
||||
ChangedAttributes Execute(ActionCharacter action, Character user, Character target);
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a specific battle action. The executor is keyed by ActionId
|
||||
/// (matching the action's ID) so the hub can look it up without
|
||||
/// knowing the concrete type. Returns ChangedAttributes so callers
|
||||
/// can apply stat deltas and check whether the action succeeded.
|
||||
/// </summary>
|
||||
public interface IActionExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// String key that matches <see cref="ActionCharacter.ID"/>.
|
||||
/// The hub uses this to resolve executors without knowing
|
||||
/// concrete types.
|
||||
/// </summary>
|
||||
string ActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the action's effects. Receives the action definition
|
||||
/// and the user/target <see cref="Character"/> instances; returns
|
||||
/// <see cref="ChangedAttributes"/> with deltas and success/failure.
|
||||
/// </summary>
|
||||
ChangedAttributes Execute(ActionCharacter action, Character user, Character target);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Typed executor source. Each IBattleCore returns its own provider,
|
||||
/// giving cores the ability to supply custom executor sets without
|
||||
/// polluting the global registry.
|
||||
/// </summary>
|
||||
public interface IActionExecutorProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the executor registered for <paramref name="actionId"/>,
|
||||
/// or null if no override is configured. Called by
|
||||
/// <see cref="ActionExecutorHub"/> before falling back to the
|
||||
/// global registry.
|
||||
/// </summary>
|
||||
IActionExecutor GetExecutor(string actionId);
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Typed executor source. Each IBattleCore returns its own provider,
|
||||
/// giving cores the ability to supply custom executor sets without
|
||||
/// polluting the global registry.
|
||||
/// </summary>
|
||||
public interface IActionExecutorProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the executor registered for <paramref name="actionId"/>,
|
||||
/// or null if no override is configured. Called by
|
||||
/// <see cref="ActionExecutorHub"/> before falling back to the
|
||||
/// global registry.
|
||||
/// </summary>
|
||||
IActionExecutor GetExecutor(string actionId);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// A single step in the battle action queue (animation, damage, effect).
|
||||
/// DoStart is called once when the action begins, DoAction is polled
|
||||
/// every frame — it returns true when the action is done — and DoEnd
|
||||
/// is the cleanup callback. This polling model lets actions drive
|
||||
/// their own duration (e.g. a tween runs until finished) without
|
||||
/// the machine needing timers.
|
||||
/// </summary>
|
||||
public interface IBattleAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Called once when the action is dequeued and begins execution.
|
||||
/// Use for setup: spawning nodes, caching references, starting
|
||||
/// tweens or coroutines.
|
||||
/// </summary>
|
||||
void DoStart();
|
||||
/// <summary>
|
||||
/// Polled every frame. Returns <c>true</c> when the action has
|
||||
/// finished its work; returns <c>false</c> to remain active.
|
||||
/// This model eliminates the need for the machine to manage timers.
|
||||
/// </summary>
|
||||
bool DoAction();
|
||||
/// <summary>
|
||||
/// Cleanup callback fired after <see cref="DoAction"/> returns
|
||||
/// <c>true</c>. Use for teardown: freeing nodes, resetting state,
|
||||
/// applying final stat deltas.
|
||||
/// </summary>
|
||||
void DoEnd();
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// A single step in the battle action queue (animation, damage, effect).
|
||||
/// DoStart is called once when the action begins, DoAction is polled
|
||||
/// every frame — it returns true when the action is done — and DoEnd
|
||||
/// is the cleanup callback. This polling model lets actions drive
|
||||
/// their own duration (e.g. a tween runs until finished) without
|
||||
/// the machine needing timers.
|
||||
/// </summary>
|
||||
public interface IBattleAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Called once when the action is dequeued and begins execution.
|
||||
/// Use for setup: spawning nodes, caching references, starting
|
||||
/// tweens or coroutines.
|
||||
/// </summary>
|
||||
void DoStart();
|
||||
/// <summary>
|
||||
/// Polled every frame. Returns <c>true</c> when the action has
|
||||
/// finished its work; returns <c>false</c> to remain active.
|
||||
/// This model eliminates the need for the machine to manage timers.
|
||||
/// </summary>
|
||||
bool DoAction();
|
||||
/// <summary>
|
||||
/// Cleanup callback fired after <see cref="DoAction"/> returns
|
||||
/// <c>true</c>. Use for teardown: freeing nodes, resetting state,
|
||||
/// applying final stat deltas.
|
||||
/// </summary>
|
||||
void DoEnd();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Global animation-player index keyed by AnimationType string.
|
||||
/// Mods or cores register players; the action machine looks up the
|
||||
/// player for a given animation type. Singleton — there is one set
|
||||
/// of players for the entire runtime, but each player can make
|
||||
/// per-core decisions internally.
|
||||
/// </summary>
|
||||
public class AnimationRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton registry. One set of animation players serves the
|
||||
/// entire runtime; individual players dispatch per-core via
|
||||
/// internal logic.
|
||||
/// </summary>
|
||||
public static readonly AnimationRegistry Instance = new();
|
||||
|
||||
private readonly Dictionary<string, IBattleAnimationPlayer> _players = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a player under <paramref name="animType"/>.
|
||||
/// Replaces any existing player for the same key — last write wins.
|
||||
/// </summary>
|
||||
public void Register(string animType, IBattleAnimationPlayer player)
|
||||
{
|
||||
_players[animType] = player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the player registered for <paramref name="animType"/>,
|
||||
/// or null if no match exists.
|
||||
/// </summary>
|
||||
public IBattleAnimationPlayer Get(string animType)
|
||||
{
|
||||
_players.TryGetValue(animType, out var player);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Global animation-player index keyed by AnimationType string.
|
||||
/// Mods or cores register players; the action machine looks up the
|
||||
/// player for a given animation type. Singleton — there is one set
|
||||
/// of players for the entire runtime, but each player can make
|
||||
/// per-core decisions internally.
|
||||
/// </summary>
|
||||
public class AnimationRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton registry. One set of animation players serves the
|
||||
/// entire runtime; individual players dispatch per-core via
|
||||
/// internal logic.
|
||||
/// </summary>
|
||||
public static readonly AnimationRegistry Instance = new();
|
||||
|
||||
private readonly Dictionary<string, IBattleAnimationPlayer> _players = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a player under <paramref name="animType"/>.
|
||||
/// Replaces any existing player for the same key — last write wins.
|
||||
/// </summary>
|
||||
public void Register(string animType, IBattleAnimationPlayer player)
|
||||
{
|
||||
_players[animType] = player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the player registered for <paramref name="animType"/>,
|
||||
/// or null if no match exists.
|
||||
/// </summary>
|
||||
public IBattleAnimationPlayer Get(string animType)
|
||||
{
|
||||
_players.TryGetValue(animType, out var player);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,79 @@
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Default IBattleContext wired to the current BattleSceneContext.
|
||||
/// ApplyDamage clamps at 0 (no negative health from defence overflow),
|
||||
/// EndBattle delegates to ShowWinground/ShowDeadground on the scene
|
||||
/// context. ScheduleAnimation and EndTurn are stubs — specific battle
|
||||
/// cores are expected to override or supplement this behaviour.
|
||||
/// </summary>
|
||||
public class BattleContextImpl : IBattleContext
|
||||
{
|
||||
private readonly BattleSceneContext _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// Root Godot <see cref="Node"/> of the battle scene, used by
|
||||
/// animation actions to parent spawned visual effects.
|
||||
/// </summary>
|
||||
public Node RootNode => _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// The owning <see cref="BattleSceneContext"/> that manages
|
||||
/// the battle lifecycle.
|
||||
/// </summary>
|
||||
public BattleSceneContext SceneContext => _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a context adapter around the given scene context.
|
||||
/// </summary>
|
||||
public BattleContextImpl(BattleSceneContext sceneContext)
|
||||
{
|
||||
_sceneContext = sceneContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts <paramref name="defence"/> from
|
||||
/// <paramref name="rawDamage"/>, clamping at 0 to prevent negative
|
||||
/// health from defence overflow, and applies the result to
|
||||
/// <paramref name="target"/>'s health.
|
||||
/// </summary>
|
||||
public void ApplyDamage(Character source, Character target, int rawDamage, int defence)
|
||||
{
|
||||
var finalDamage = Mathf.Max(0, rawDamage - defence);
|
||||
target.Attributes.Health.Value -= finalDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub — specific battle cores override this to enqueue
|
||||
/// animation actions into the <see cref="BattleActionMachine"/>.
|
||||
/// </summary>
|
||||
public void ScheduleAnimation(IBattleAnimation animation)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub — cores implement this to advance the turn sequence
|
||||
/// when a side finishes its action phase.
|
||||
/// </summary>
|
||||
public void EndTurn(BattleSide side)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the battle outcome by delegating to
|
||||
/// <see cref="BattleSceneContext.ShowWinground"/> for player
|
||||
/// victory or <see cref="BattleSceneContext.ShowDeadground"/>
|
||||
/// for enemy victory.
|
||||
/// </summary>
|
||||
public void EndBattle(BattleSide winner)
|
||||
{
|
||||
if (winner == BattleSide.Player)
|
||||
_sceneContext.ShowWinground();
|
||||
else
|
||||
_sceneContext.ShowDeadground();
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Default IBattleContext wired to the current BattleSceneContext.
|
||||
/// ApplyDamage clamps at 0 (no negative health from defence overflow),
|
||||
/// EndBattle delegates to ShowWinground/ShowDeadground on the scene
|
||||
/// context. ScheduleAnimation and EndTurn are stubs — specific battle
|
||||
/// cores are expected to override or supplement this behaviour.
|
||||
/// </summary>
|
||||
public class BattleContextImpl : IBattleContext
|
||||
{
|
||||
private readonly BattleSceneContext _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// Root Godot <see cref="Node"/> of the battle scene, used by
|
||||
/// animation actions to parent spawned visual effects.
|
||||
/// </summary>
|
||||
public Node RootNode => _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// The owning <see cref="BattleSceneContext"/> that manages
|
||||
/// the battle lifecycle.
|
||||
/// </summary>
|
||||
public BattleSceneContext SceneContext => _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a context adapter around the given scene context.
|
||||
/// </summary>
|
||||
public BattleContextImpl(BattleSceneContext sceneContext)
|
||||
{
|
||||
_sceneContext = sceneContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts <paramref name="defence"/> from
|
||||
/// <paramref name="rawDamage"/>, clamping at 0 to prevent negative
|
||||
/// health from defence overflow, and applies the result to
|
||||
/// <paramref name="target"/>'s health.
|
||||
/// </summary>
|
||||
public void ApplyDamage(Character source, Character target, int rawDamage, int defence)
|
||||
{
|
||||
var finalDamage = Mathf.Max(0, rawDamage - defence);
|
||||
target.Attributes.Health.Value -= finalDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub — specific battle cores override this to enqueue
|
||||
/// animation actions into the <see cref="BattleActionMachine"/>.
|
||||
/// </summary>
|
||||
public void ScheduleAnimation(IBattleAnimation animation)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub — cores implement this to advance the turn sequence
|
||||
/// when a side finishes its action phase.
|
||||
/// </summary>
|
||||
public void EndTurn(BattleSide side)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the battle outcome by delegating to
|
||||
/// <see cref="BattleSceneContext.ShowWinground"/> for player
|
||||
/// victory or <see cref="BattleSceneContext.ShowDeadground"/>
|
||||
/// for enemy victory.
|
||||
/// </summary>
|
||||
public void EndBattle(BattleSide winner)
|
||||
{
|
||||
if (winner == BattleSide.Player)
|
||||
_sceneContext.ShowWinground();
|
||||
else
|
||||
_sceneContext.ShowDeadground();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a battle animation without binding to a concrete player.
|
||||
/// Carries Source/Target characters, an AnimationType key for dispatch,
|
||||
/// a Speed multiplier, and an open-ended Parameters dictionary so
|
||||
/// custom animations can pass arbitrary data (damage value, element
|
||||
/// type, etc.) without interface changes.
|
||||
/// </summary>
|
||||
public interface IBattleAnimation
|
||||
{
|
||||
/// <summary>The <see cref="Character"/> performing the action.</summary>
|
||||
Character Source { get; }
|
||||
/// <summary>The <see cref="Character"/> receiving the action.</summary>
|
||||
Character Target { get; }
|
||||
/// <summary>
|
||||
/// Key used by <see cref="AnimationRegistry"/> to dispatch to the
|
||||
/// correct <see cref="IBattleAnimationPlayer"/>.
|
||||
/// </summary>
|
||||
string AnimationType { get; }
|
||||
/// <summary>Multiplier for the playback speed (1.0 = normal).</summary>
|
||||
float Speed { get; }
|
||||
/// <summary>
|
||||
/// Open-ended data bag for animation-specific values
|
||||
/// (e.g. damage amount, elemental flag) — no interface changes
|
||||
/// needed when adding new animation types.
|
||||
/// </summary>
|
||||
Dictionary<string, object> Parameters { get; }
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a battle animation without binding to a concrete player.
|
||||
/// Carries Source/Target characters, an AnimationType key for dispatch,
|
||||
/// a Speed multiplier, and an open-ended Parameters dictionary so
|
||||
/// custom animations can pass arbitrary data (damage value, element
|
||||
/// type, etc.) without interface changes.
|
||||
/// </summary>
|
||||
public interface IBattleAnimation
|
||||
{
|
||||
/// <summary>The <see cref="Character"/> performing the action.</summary>
|
||||
Character Source { get; }
|
||||
/// <summary>The <see cref="Character"/> receiving the action.</summary>
|
||||
Character Target { get; }
|
||||
/// <summary>
|
||||
/// Key used by <see cref="AnimationRegistry"/> to dispatch to the
|
||||
/// correct <see cref="IBattleAnimationPlayer"/>.
|
||||
/// </summary>
|
||||
string AnimationType { get; }
|
||||
/// <summary>Multiplier for the playback speed (1.0 = normal).</summary>
|
||||
float Speed { get; }
|
||||
/// <summary>
|
||||
/// Open-ended data bag for animation-specific values
|
||||
/// (e.g. damage amount, elemental flag) — no interface changes
|
||||
/// needed when adding new animation types.
|
||||
/// </summary>
|
||||
Dictionary<string, object> Parameters { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Plays a specific animation type. Each player is keyed by
|
||||
/// AnimationType so the registry can dispatch animations without
|
||||
/// knowing concrete player types. The onComplete callback chains
|
||||
/// into the action machine's polling loop.
|
||||
/// </summary>
|
||||
public interface IBattleAnimationPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// String key used by <see cref="AnimationRegistry"/> to select
|
||||
/// this player for a given <see cref="IBattleAnimation.AnimationType"/>.
|
||||
/// </summary>
|
||||
string AnimationType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts playing the animation described by <paramref name="anim"/>.
|
||||
/// Must invoke <paramref name="onComplete"/> once the animation
|
||||
/// finishes, so the action machine can advance to the next step.
|
||||
/// </summary>
|
||||
void Play(IBattleAnimation anim, Action onComplete);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Plays a specific animation type. Each player is keyed by
|
||||
/// AnimationType so the registry can dispatch animations without
|
||||
/// knowing concrete player types. The onComplete callback chains
|
||||
/// into the action machine's polling loop.
|
||||
/// </summary>
|
||||
public interface IBattleAnimationPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// String key used by <see cref="AnimationRegistry"/> to select
|
||||
/// this player for a given <see cref="IBattleAnimation.AnimationType"/>.
|
||||
/// </summary>
|
||||
string AnimationType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts playing the animation described by <paramref name="anim"/>.
|
||||
/// Must invoke <paramref name="onComplete"/> once the animation
|
||||
/// finishes, so the action machine can advance to the next step.
|
||||
/// </summary>
|
||||
void Play(IBattleAnimation anim, Action onComplete);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Bridge from the battle core back to the Godot scene. Cores work
|
||||
/// against this interface so they remain engine-agnostic.
|
||||
/// ApplyDamage computes final-damage-minus-defence and mutates
|
||||
/// the target's Health directly (no event — the UI layer polls).
|
||||
/// ScheduleAnimation is the hook for visual feedback; EndTurn/EndBattle
|
||||
/// are phase signals. RootNode gives cores a Godot anchor for
|
||||
/// instantiating nodes (e.g. damage numbers).
|
||||
/// </summary>
|
||||
public interface IBattleContext
|
||||
{
|
||||
void ApplyDamage(Character source, Character target, int rawDamage, int defence);
|
||||
|
||||
void ScheduleAnimation(IBattleAnimation animation);
|
||||
|
||||
void EndTurn(BattleSide side);
|
||||
|
||||
void EndBattle(BattleSide winner);
|
||||
|
||||
Node RootNode { get; }
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Bridge from the battle core back to the Godot scene. Cores work
|
||||
/// against this interface so they remain engine-agnostic.
|
||||
/// ApplyDamage computes final-damage-minus-defence and mutates
|
||||
/// the target's Health directly (no event — the UI layer polls).
|
||||
/// ScheduleAnimation is the hook for visual feedback; EndTurn/EndBattle
|
||||
/// are phase signals. RootNode gives cores a Godot anchor for
|
||||
/// instantiating nodes (e.g. damage numbers).
|
||||
/// </summary>
|
||||
public interface IBattleContext
|
||||
{
|
||||
void ApplyDamage(Character source, Character target, int rawDamage, int defence);
|
||||
|
||||
void ScheduleAnimation(IBattleAnimation animation);
|
||||
|
||||
void EndTurn(BattleSide side);
|
||||
|
||||
void EndBattle(BattleSide winner);
|
||||
|
||||
Node RootNode { get; }
|
||||
}
|
||||
}
|
||||
+46
-46
@@ -1,46 +1,46 @@
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Pluggable battle engine contract. Each implementation is a
|
||||
/// self-contained battle rule set (turn order, AI, phases).
|
||||
/// Init receives the two character arrays and a context adapter;
|
||||
/// Start begins the loop. The ActionProvider property lets the
|
||||
/// core inject its own executor overrides via the hub.
|
||||
/// Cores are instantiated fresh per battle — they carry no
|
||||
/// persistent state between encounters.
|
||||
/// </summary>
|
||||
public interface IBattleCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this battle engine (e.g. "card_battle",
|
||||
/// "ff_battle"). Used by <see cref="BattleCoreRegistry"/> and
|
||||
/// <see cref="BattleData.ActiveBattleCore"/> for lookup.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider that returns core-specific <see cref="IActionExecutor"/>
|
||||
/// overrides. Set on the <see cref="ActionExecutorHub"/> when
|
||||
/// this core activates, so actions route through custom logic
|
||||
/// before falling back to the global registry.
|
||||
/// </summary>
|
||||
IActionExecutorProvider ActionProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialises the core with the two character arrays and a
|
||||
/// <see cref="IBattleContext"/> for callbacks. Called once per
|
||||
/// battle before <see cref="Start"/>.
|
||||
/// </summary>
|
||||
void Init(Character[] playerChars, Character[] enemyChars, IBattleContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// Begins the turn loop. Cores may use coroutines, polling, or
|
||||
/// event-driven state machines — the core owns its own lifecycle
|
||||
/// after this call.
|
||||
/// </summary>
|
||||
void Start();
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Pluggable battle engine contract. Each implementation is a
|
||||
/// self-contained battle rule set (turn order, AI, phases).
|
||||
/// Init receives the two character arrays and a context adapter;
|
||||
/// Start begins the loop. The ActionProvider property lets the
|
||||
/// core inject its own executor overrides via the hub.
|
||||
/// Cores are instantiated fresh per battle — they carry no
|
||||
/// persistent state between encounters.
|
||||
/// </summary>
|
||||
public interface IBattleCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this battle engine (e.g. "card_battle",
|
||||
/// "ff_battle"). Used by <see cref="BattleCoreRegistry"/> and
|
||||
/// <see cref="BattleData.ActiveBattleCore"/> for lookup.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider that returns core-specific <see cref="IActionExecutor"/>
|
||||
/// overrides. Set on the <see cref="ActionExecutorHub"/> when
|
||||
/// this core activates, so actions route through custom logic
|
||||
/// before falling back to the global registry.
|
||||
/// </summary>
|
||||
IActionExecutorProvider ActionProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialises the core with the two character arrays and a
|
||||
/// <see cref="IBattleContext"/> for callbacks. Called once per
|
||||
/// battle before <see cref="Start"/>.
|
||||
/// </summary>
|
||||
void Init(Character[] playerChars, Character[] enemyChars, IBattleContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// Begins the turn loop. Cores may use coroutines, polling, or
|
||||
/// event-driven state machines — the core owns its own lifecycle
|
||||
/// after this call.
|
||||
/// </summary>
|
||||
void Start();
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -1,15 +1,15 @@
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies which side of the battle a character or turn belongs to.
|
||||
/// Used by IBattleContext.EndTurn and EndBattle to route results
|
||||
/// (victory vs defeat screen).
|
||||
/// </summary>
|
||||
public enum BattleSide
|
||||
{
|
||||
/// <summary>The player's party.</summary>
|
||||
Player,
|
||||
/// <summary>The opposing force (monsters, NPCs).</summary>
|
||||
Enemy,
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies which side of the battle a character or turn belongs to.
|
||||
/// Used by IBattleContext.EndTurn and EndBattle to route results
|
||||
/// (victory vs defeat screen).
|
||||
/// </summary>
|
||||
public enum BattleSide
|
||||
{
|
||||
/// <summary>The player's party.</summary>
|
||||
Player,
|
||||
/// <summary>The opposing force (monsters, NPCs).</summary>
|
||||
Enemy,
|
||||
}
|
||||
}
|
||||
+62
-62
@@ -1,62 +1,62 @@
|
||||
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Convenience base for IActionExecutor implementations. Provides
|
||||
/// CheckRequiredAndUsePoint which validates that the user has enough
|
||||
/// "Point" attribute (a generic action-point / mana pool) before
|
||||
/// deducting it — returning false lets the action fail gracefully.
|
||||
/// ID is abstract so each action declares its own identity.
|
||||
/// </summary>
|
||||
public abstract class ActionBase : IActionExecutor
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Maps to the abstract <see cref="ID"/> so the hub can resolve
|
||||
/// this executor by action name.
|
||||
/// </summary>
|
||||
public string ActionId => ID;
|
||||
|
||||
/// <summary>
|
||||
/// Unique action identifier that matches
|
||||
/// <see cref="ActionCharacter.ID"/> in data.
|
||||
/// </summary>
|
||||
public abstract string ID { get; }
|
||||
/// <summary>
|
||||
/// Applies the action's effects to <paramref name="target"/>
|
||||
/// using <paramref name="user"/>'s stats and the
|
||||
/// <paramref name="action"/> definition. Returns stat deltas
|
||||
/// and a success/failure flag.
|
||||
/// </summary>
|
||||
public abstract ChangedAttributes Execute(ActionCharacter action, Character user, Character target);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that <paramref name="user"/> has at least
|
||||
/// <c>"RequiredPoint"</c> remaining, deducts it, and
|
||||
/// returns <c>true</c>. Returns <c>false</c> with a warning log
|
||||
/// if the user lacks sufficient points, allowing the action to
|
||||
/// fail gracefully.
|
||||
/// </summary>
|
||||
protected bool CheckRequiredAndUsePoint(ActionCharacter action, Character user)
|
||||
{
|
||||
var requiredPoint = action.GetInt("RequiredPoint");
|
||||
|
||||
if (user.Attributes.Point.Value < requiredPoint)
|
||||
{
|
||||
GameLogger.Log("BATTLE", user.ID + " not checked point required [" + user.Attributes.Point.Value + "] for action " + action.ID + " [" + requiredPoint + "]", LogLevel.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
GameLogger.Log("BATTLE", user.ID + " use [" + requiredPoint + "] points for action " + action.ID, LogLevel.Debug);
|
||||
|
||||
user.Attributes.Point.Value -= requiredPoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Convenience base for IActionExecutor implementations. Provides
|
||||
/// CheckRequiredAndUsePoint which validates that the user has enough
|
||||
/// "Point" attribute (a generic action-point / mana pool) before
|
||||
/// deducting it — returning false lets the action fail gracefully.
|
||||
/// ID is abstract so each action declares its own identity.
|
||||
/// </summary>
|
||||
public abstract class ActionBase : IActionExecutor
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Maps to the abstract <see cref="ID"/> so the hub can resolve
|
||||
/// this executor by action name.
|
||||
/// </summary>
|
||||
public string ActionId => ID;
|
||||
|
||||
/// <summary>
|
||||
/// Unique action identifier that matches
|
||||
/// <see cref="ActionCharacter.ID"/> in data.
|
||||
/// </summary>
|
||||
public abstract string ID { get; }
|
||||
/// <summary>
|
||||
/// Applies the action's effects to <paramref name="target"/>
|
||||
/// using <paramref name="user"/>'s stats and the
|
||||
/// <paramref name="action"/> definition. Returns stat deltas
|
||||
/// and a success/failure flag.
|
||||
/// </summary>
|
||||
public abstract ChangedAttributes Execute(ActionCharacter action, Character user, Character target);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that <paramref name="user"/> has at least
|
||||
/// <c>"RequiredPoint"</c> remaining, deducts it, and
|
||||
/// returns <c>true</c>. Returns <c>false</c> with a warning log
|
||||
/// if the user lacks sufficient points, allowing the action to
|
||||
/// fail gracefully.
|
||||
/// </summary>
|
||||
protected bool CheckRequiredAndUsePoint(ActionCharacter action, Character user)
|
||||
{
|
||||
var requiredPoint = action.GetInt("RequiredPoint");
|
||||
|
||||
if (user.Attributes.Point.Value < requiredPoint)
|
||||
{
|
||||
GameLogger.Log("BATTLE", user.ID + " not checked point required [" + user.Attributes.Point.Value + "] for action " + action.ID + " [" + requiredPoint + "]", LogLevel.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
GameLogger.Log("BATTLE", user.ID + " use [" + requiredPoint + "] points for action " + action.ID, LogLevel.Debug);
|
||||
|
||||
user.Attributes.Point.Value -= requiredPoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+98
-98
@@ -1,98 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Quests;
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable descriptor for a battle encounter, constructed via the
|
||||
/// static factory <see cref="InitBattle"/> overloads. Holds the
|
||||
/// background texture, enemy roster, quest reference, card order side,
|
||||
/// and the scene to return to when the battle ends. The private
|
||||
/// constructor forces creation through the factories so that all
|
||||
/// required data is supplied upfront.
|
||||
/// </summary>
|
||||
public class BattleData
|
||||
{
|
||||
/// <summary>The quest that triggered this battle; may be <c>null</c>
|
||||
/// for non-quest skirmishes.</summary>
|
||||
public QuestBase Quest { get; set; }
|
||||
/// <summary>Auxiliary tag set on new characters acquired via recruitment
|
||||
/// after this battle.</summary>
|
||||
public string NewTag { get; set; }
|
||||
|
||||
/// <summary>List of enemy <see cref="Character"/> instances spawned from
|
||||
/// the provided IDs or default templates.</summary>
|
||||
public List<Character> EnemiesCards { get; private set; }
|
||||
/// <summary>Battle background texture (full-resolution).</summary>
|
||||
public Texture2D Background { get; private set; }
|
||||
/// <summary>Depth map texture for the parallax post-process effect.</summary>
|
||||
public Texture2D DepthMap { get; set; }
|
||||
/// <summary>Albedo/colour map texture for the parallax post-process effect.</summary>
|
||||
public Texture2D AlbedoMap { get; set; }
|
||||
/// <summary>Which side of the screen the player's cards are drawn on.</summary>
|
||||
public BattleSide BattleSide { get; private set; } = BattleSide.Player;
|
||||
/// <summary>Identifier for the active battle core logic module.</summary>
|
||||
public string ActiveBattleCore { get; set; }
|
||||
/// <summary>Scene name to transition to when the battle finishes or is
|
||||
/// fled. Stored as a string to avoid type coupling.</summary>
|
||||
public string ReturnScene { get; private set; }
|
||||
|
||||
private BattleData()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Convenience overload that defaults <see cref="BattleSide"/> to
|
||||
/// <c>BattleSide.Player</c>. Constructs enemies from their string IDs
|
||||
/// via <see cref="Cthangover.Core.Factories.Impls.CharacterFactory"/>,
|
||||
/// falling back to a level-1 template when the ID is unknown.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, params string[] enemies)
|
||||
{
|
||||
return InitBattle(background, returnScene, BattleSide.Player, enemies);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a battle with a specified <see cref="BattleSide"/> and
|
||||
/// enemy IDs. Each ID is resolved through
|
||||
/// <see cref="Cthangover.Core.Factories.Impls.CharacterFactory.Get"/>.
|
||||
/// Unknown IDs produce a fallback <see cref="Character"/> with 20 HP
|
||||
/// and 3 action points.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, BattleSide cardOrder = BattleSide.Player, params string[] enemies)
|
||||
{
|
||||
var list = enemies.Select(o => CharacterFactory.Instance.Get(o) ?? new Character
|
||||
{
|
||||
ID = o,
|
||||
Attributes = new CharacterAttributes
|
||||
{
|
||||
Health = new Attribute { Value = 20, BaseValue = 20 },
|
||||
Point = new Attribute { Value = 3, BaseValue = 3 },
|
||||
},
|
||||
Level = 1,
|
||||
Exp = 10,
|
||||
}).ToList();
|
||||
return InitBattle(background, returnScene, list, cardOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core factory that directly accepts a pre-built
|
||||
/// <see cref="List{Character}"/> for the enemy roster. This is the
|
||||
/// overload that all other <c>InitBattle</c> variants delegate to.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, List<Character> enemiesCards, BattleSide cardOrder = BattleSide.Player)
|
||||
{
|
||||
return new BattleData
|
||||
{
|
||||
Background = background,
|
||||
ReturnScene = returnScene,
|
||||
EnemiesCards = enemiesCards,
|
||||
BattleSide = cardOrder,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Quests;
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable descriptor for a battle encounter, constructed via the
|
||||
/// static factory <see cref="InitBattle"/> overloads. Holds the
|
||||
/// background texture, enemy roster, quest reference, card order side,
|
||||
/// and the scene to return to when the battle ends. The private
|
||||
/// constructor forces creation through the factories so that all
|
||||
/// required data is supplied upfront.
|
||||
/// </summary>
|
||||
public class BattleData
|
||||
{
|
||||
/// <summary>The quest that triggered this battle; may be <c>null</c>
|
||||
/// for non-quest skirmishes.</summary>
|
||||
public QuestBase Quest { get; set; }
|
||||
/// <summary>Auxiliary tag set on new characters acquired via recruitment
|
||||
/// after this battle.</summary>
|
||||
public string NewTag { get; set; }
|
||||
|
||||
/// <summary>List of enemy <see cref="Character"/> instances spawned from
|
||||
/// the provided IDs or default templates.</summary>
|
||||
public List<Character> EnemiesCards { get; private set; }
|
||||
/// <summary>Battle background texture (full-resolution).</summary>
|
||||
public Texture2D Background { get; private set; }
|
||||
/// <summary>Depth map texture for the parallax post-process effect.</summary>
|
||||
public Texture2D DepthMap { get; set; }
|
||||
/// <summary>Albedo/colour map texture for the parallax post-process effect.</summary>
|
||||
public Texture2D AlbedoMap { get; set; }
|
||||
/// <summary>Which side of the screen the player's cards are drawn on.</summary>
|
||||
public BattleSide BattleSide { get; private set; } = BattleSide.Player;
|
||||
/// <summary>Identifier for the active battle core logic module.</summary>
|
||||
public string ActiveBattleCore { get; set; }
|
||||
/// <summary>Scene name to transition to when the battle finishes or is
|
||||
/// fled. Stored as a string to avoid type coupling.</summary>
|
||||
public string ReturnScene { get; private set; }
|
||||
|
||||
private BattleData()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Convenience overload that defaults <see cref="BattleSide"/> to
|
||||
/// <c>BattleSide.Player</c>. Constructs enemies from their string IDs
|
||||
/// via <see cref="Cthangover.Core.Factories.Impls.CharacterFactory"/>,
|
||||
/// falling back to a level-1 template when the ID is unknown.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, params string[] enemies)
|
||||
{
|
||||
return InitBattle(background, returnScene, BattleSide.Player, enemies);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a battle with a specified <see cref="BattleSide"/> and
|
||||
/// enemy IDs. Each ID is resolved through
|
||||
/// <see cref="Cthangover.Core.Factories.Impls.CharacterFactory.Get"/>.
|
||||
/// Unknown IDs produce a fallback <see cref="Character"/> with 20 HP
|
||||
/// and 3 action points.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, BattleSide cardOrder = BattleSide.Player, params string[] enemies)
|
||||
{
|
||||
var list = enemies.Select(o => CharacterFactory.Instance.Get(o) ?? new Character
|
||||
{
|
||||
ID = o,
|
||||
Attributes = new CharacterAttributes
|
||||
{
|
||||
Health = new Attribute { Value = 20, BaseValue = 20 },
|
||||
Point = new Attribute { Value = 3, BaseValue = 3 },
|
||||
},
|
||||
Level = 1,
|
||||
Exp = 10,
|
||||
}).ToList();
|
||||
return InitBattle(background, returnScene, list, cardOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core factory that directly accepts a pre-built
|
||||
/// <see cref="List{Character}"/> for the enemy roster. This is the
|
||||
/// overload that all other <c>InitBattle</c> variants delegate to.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, List<Character> enemiesCards, BattleSide cardOrder = BattleSide.Player)
|
||||
{
|
||||
return new BattleData
|
||||
{
|
||||
Background = background,
|
||||
ReturnScene = returnScene,
|
||||
EnemiesCards = enemiesCards,
|
||||
BattleSide = cardOrder,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+366
-366
@@ -1,368 +1,368 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Time;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Time;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton scene-root for battle, wired as a self-registering Node
|
||||
/// (sets Instance on enter-tree, clears on exit-tree). On ready it
|
||||
/// assembles player characters from CharacterData.BattleSet, enemy
|
||||
/// cards from the pending <see cref="BattleRequest"/>, resolves the active
|
||||
/// <see cref="IBattleCore"/> via <see cref="BattleCoreRegistry"/>, and defers
|
||||
/// Init+Start to the next frame so the scene tree is fully built before core
|
||||
/// logic runs. Tracks PlayerExpGained (per-character, split evenly among
|
||||
/// survivors) and DefeatedEnemies for the victory/loot screen. Exposes
|
||||
/// OnCharacterDied and OnBattleCleared events so external systems (e.g.
|
||||
/// quests, the action machine) can react without coupling.
|
||||
/// ShowWinground / ShowDeadground are mutually exclusive end-of-battle
|
||||
/// paths — both set IsDestroyed to prevent double-triggering and
|
||||
/// reset battle speed before clearing state.
|
||||
/// <para/>
|
||||
/// The context derives from <see cref="ModNode"/> because mod assemblies get
|
||||
/// no Godot source generation: the core base receives the engine lifecycle
|
||||
/// callbacks and forwards them to the <c>OnXxx</c> overrides. It also
|
||||
/// implements <see cref="IActiveCharacterProvider"/> so core UI (the inventory
|
||||
/// panel's action-point row) can read the acting character without knowing
|
||||
/// anything about battle.
|
||||
/// </summary>
|
||||
public partial class BattleSceneContext : ModNode, IActiveCharacterProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton reference to the active battle scene context.
|
||||
/// Set on enter-tree, cleared on exit-tree.
|
||||
/// External systems read this to access battle state.
|
||||
/// </summary>
|
||||
public static BattleSceneContext Instance { get; private set; }
|
||||
|
||||
private TextureRect battleBg;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Actions.BattleActionMachine"/> node that
|
||||
/// processes the animation queue for this battle. Cores push
|
||||
/// animation actions through the context and the machine drives
|
||||
/// playback frame-by-frame.
|
||||
/// </summary>
|
||||
public Actions.BattleActionMachine BattleActionMachine { get; private set; }
|
||||
|
||||
private IBattleSpeedControl _battleSpeedControl;
|
||||
private Widget deadground;
|
||||
private Widget winground;
|
||||
|
||||
/// <summary>
|
||||
/// Per-character EXP earned during this battle, keyed by
|
||||
/// <see cref="Character.ID"/>. Populated by
|
||||
/// <see cref="RecordEnemyDefeated"/>; consumed by the victory screen
|
||||
/// provided by the turn-battle GUI mod.
|
||||
/// </summary>
|
||||
public Dictionary<string, int> PlayerExpGained { get; private set; } = new();
|
||||
/// <summary>
|
||||
/// Enemies defeated during this battle (for loot generation
|
||||
/// and recruitment rolls).
|
||||
/// </summary>
|
||||
public List<Character> DefeatedEnemies { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Guards against double-triggering end-of-battle paths.
|
||||
/// Set to <c>true</c> by <see cref="ShowWinground"/> or
|
||||
/// <see cref="ShowDeadground"/>.
|
||||
/// </summary>
|
||||
public bool IsDestroyed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, the battle core pauses processing (e.g.
|
||||
/// while a UI overlay is open). Set externally by pause widgets.
|
||||
/// </summary>
|
||||
public bool IsWait { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Character"/> whose turn is currently active
|
||||
/// in battle. Set by the battle core when a character is selected
|
||||
/// and cleared when the turn ends or selection is cancelled.
|
||||
/// <c>null</c> means no character is active (out of battle,
|
||||
/// between turns, or idle state). External panels such as the
|
||||
/// inventory panel read this to check whether the active
|
||||
/// character has enough action points before performing
|
||||
/// item-related actions.
|
||||
/// </summary>
|
||||
public Character ActiveCharacter { get; set; }
|
||||
|
||||
/// <summary>Fired when a character's health reaches 0.</summary>
|
||||
public event System.Action<Character> OnCharacterDied;
|
||||
/// <summary>
|
||||
/// Fired after battle state is cleared (both victory and defeat).
|
||||
/// External systems can hook this to release battle resources.
|
||||
/// </summary>
|
||||
public event System.Action OnBattleCleared;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="OnCharacterDied"/> to notify listeners
|
||||
/// (quests, achievements, the battle core) that a character
|
||||
/// has been defeated.
|
||||
/// </summary>
|
||||
public void NotifyCharacterDied(Character character)
|
||||
{
|
||||
OnCharacterDied?.Invoke(character);
|
||||
ModEventBus.Publish(new CharacterDiedEvent
|
||||
{
|
||||
CharacterId = character?.ID,
|
||||
CharacterName = character?.Name,
|
||||
Source = "battle",
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Claims the scene singleton and registers as the <see cref="ActiveCharacterRegistry"/> provider.</summary>
|
||||
protected override void OnEnterTree()
|
||||
{
|
||||
if (Instance != null && GodotObject.IsInstanceValid(Instance))
|
||||
{
|
||||
var scene = GetTree()?.CurrentScene?.Name ?? "?";
|
||||
var existingPath = Instance.GetPath().ToString();
|
||||
var myPath = GetPath().ToString();
|
||||
GameLogger.Log("DUPLICATE", $"BattleSceneContext.OnEnterTree: Instance ALREADY SET by '{existingPath}', overwriting with duplicate at '{myPath}' on scene '{scene}'", LogLevel.Error);
|
||||
}
|
||||
Instance = this;
|
||||
ActiveCharacterRegistry.Provider = this;
|
||||
}
|
||||
|
||||
/// <summary>Releases the singleton slot and the active-character provider so a later scene can claim them.</summary>
|
||||
protected override void OnExitTree()
|
||||
{
|
||||
base.OnExitTree();
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
if (ReferenceEquals(ActiveCharacterRegistry.Provider, this))
|
||||
ActiveCharacterRegistry.Provider = null;
|
||||
}
|
||||
|
||||
private List<Character> pendingPlayerCards;
|
||||
private List<Character> pendingEnemyCards;
|
||||
private IBattleCore activeBattleCore;
|
||||
|
||||
/// <summary>
|
||||
/// The battle core driving this scene, resolved from
|
||||
/// <see cref="BattleCoreRegistry"/> when the scene becomes ready. Exposed
|
||||
/// so tooling and tests can query the active core (for example for
|
||||
/// <see cref="IBattleAutoPlayable"/>) without reaching into UI nodes.
|
||||
/// </summary>
|
||||
public IBattleCore ActiveBattleCore => activeBattleCore;
|
||||
|
||||
/// <summary>Caches the battle backdrop node, resolves the active <see cref="IBattleCore"/> and starts the battle.</summary>
|
||||
protected override void OnReady()
|
||||
{
|
||||
battleBg = GetNodeOrNull<TextureRect>("BattleBg");
|
||||
|
||||
if (battleBg == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "battleBg not found!", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
BattleActionMachine = GetNodeOrNull<Actions.BattleActionMachine>("ToolPanel/BattleActionMachine");
|
||||
_battleSpeedControl = GetNodeOrNull<Control>("BattleSpeedControl") as IBattleSpeedControl;
|
||||
deadground = GetNodeOrNull<Widget>("Deadground");
|
||||
|
||||
winground = GetNodeOrNull<Widget>("Winground");
|
||||
|
||||
var data = BattleRequest.Current;
|
||||
if (data == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "BattleRequest.Current is null!", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
GameLogger.Log("BATTLE", $"set battle background: battleBg={(battleBg != null ? "found" : "NULL")}, texture={(data.Background != null ? "has" : "NULL")}", battleBg == null || data.Background == null ? LogLevel.Error : LogLevel.Debug);
|
||||
|
||||
if (data.Background != null)
|
||||
{
|
||||
battleBg.Texture = data.Background;
|
||||
GameLogger.Log("BATTLE", $" battleBg.Texture assigned: width={battleBg.Texture.GetWidth()}, height={battleBg.Texture.GetHeight()}", LogLevel.Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("BATTLE", $" data.Background is NULL — no background texture set", LogLevel.Error);
|
||||
}
|
||||
|
||||
var timedShader = Shaders.Resolve("timed_sprite");
|
||||
if (timedShader != null)
|
||||
{
|
||||
var bgMaterial = new ShaderMaterial { Shader = timedShader };
|
||||
battleBg.Material = bgMaterial;
|
||||
|
||||
if (data.DepthMap != null)
|
||||
bgMaterial.SetShaderParameter("depth_mask", data.DepthMap);
|
||||
if (data.AlbedoMap != null)
|
||||
bgMaterial.SetShaderParameter("albedo_map", data.AlbedoMap);
|
||||
|
||||
var lights = GetNodeOrNull<UiLightController>("Lights");
|
||||
if (lights != null)
|
||||
{
|
||||
var viewportSize = GetViewport().GetVisibleRect().Size;
|
||||
lights.SetPlayerLight(viewportSize / 2, 500f, 1f);
|
||||
lights.RegisterMaterial(bgMaterial);
|
||||
}
|
||||
GameLogger.Log("BATTLE", $"battleBg shader: timed_sprite applied, depthMap={(data.DepthMap != null ? "has" : "NULL")}, albedoMap={(data.AlbedoMap != null ? "has" : "NULL")}, lightsCtrl={(lights != null ? "found" : "NULL")}", lights == null ? LogLevel.Error : LogLevel.Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("BATTLE", "battleBg shader: timed_sprite NOT FOUND", LogLevel.Error);
|
||||
}
|
||||
|
||||
pendingPlayerCards = new List<Character>();
|
||||
foreach (var characterType in GameData.Instance.Runtime.CharacterData.BattleSet)
|
||||
{
|
||||
var characterInfo = GameData.Instance.Runtime.CharacterData.Characters[characterType];
|
||||
var template = CharacterFactory.Instance.Get(characterInfo.ID);
|
||||
var character = template != null ? template.Copy() : new Character { ID = characterInfo.ID };
|
||||
character.Attributes = characterInfo.Attributes ?? new CharacterAttributes();
|
||||
character.Level = characterInfo.Level;
|
||||
character.Exp = characterInfo.Exp;
|
||||
if (characterInfo.ActionSlots != null && characterInfo.ActionSlots.Any(id => !string.IsNullOrEmpty(id)))
|
||||
{
|
||||
character.Actions = characterInfo.ActionSlots
|
||||
.Where(id => !string.IsNullOrEmpty(id))
|
||||
.Select(id => ActionCharacterFactory.Instance.Get(id))
|
||||
.Where(a => a != null)
|
||||
.ToList();
|
||||
}
|
||||
pendingPlayerCards.Add(character);
|
||||
}
|
||||
|
||||
pendingEnemyCards = data.EnemiesCards?.Where(c => c != null).ToList() ?? new List<Character>();
|
||||
|
||||
if (!string.IsNullOrEmpty(data.ActiveBattleCore) && BattleCoreRegistry.Instance.HasCore(data.ActiveBattleCore))
|
||||
BattleCoreRegistry.Instance.SetActive(data.ActiveBattleCore);
|
||||
|
||||
try
|
||||
{
|
||||
activeBattleCore = BattleCoreRegistry.Instance.GetActive();
|
||||
}
|
||||
catch
|
||||
{
|
||||
activeBattleCore = null;
|
||||
}
|
||||
|
||||
if (activeBattleCore == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "No battle core available", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Callable.From(DeferredInit).CallDeferred();
|
||||
}
|
||||
|
||||
private void DeferredInit()
|
||||
{
|
||||
if (IsDestroyed || activeBattleCore == null)
|
||||
return;
|
||||
|
||||
var ctx = new BattleContextImpl(this);
|
||||
var playerArray = pendingPlayerCards?.ToArray() ?? new Character[0];
|
||||
var enemyArray = pendingEnemyCards?.ToArray() ?? new Character[0];
|
||||
|
||||
activeBattleCore.Init(playerArray, enemyArray, ctx);
|
||||
activeBattleCore.Start();
|
||||
|
||||
ModEventBus.Publish(new BattleStartedEvent
|
||||
{
|
||||
BattleType = activeBattleCore.GetType().Name,
|
||||
Enemies = enemyArray.Select(c => c.Name).ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the defeat screen: sets <see cref="IsDestroyed"/>,
|
||||
/// resets battle speed, clears battle state, and shows the
|
||||
/// deadground widget.
|
||||
/// </summary>
|
||||
public void ShowDeadground()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
IsDestroyed = true;
|
||||
_battleSpeedControl?.ResetToNormal();
|
||||
ClearBattle("defeat");
|
||||
deadground?.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the victory screen: sets <see cref="IsDestroyed"/>,
|
||||
/// resets battle speed, clears battle state, and shows the
|
||||
/// winground widget.
|
||||
/// </summary>
|
||||
public void ShowWinground()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
IsDestroyed = true;
|
||||
_battleSpeedControl?.ResetToNormal();
|
||||
ClearBattle("victory");
|
||||
winground?.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a defeated enemy: adds it to
|
||||
/// <see cref="DefeatedEnemies"/> and distributes its EXP
|
||||
/// (<c>Level * Exp</c>) equally among all surviving player
|
||||
/// characters in <see cref="PlayerExpGained"/>.
|
||||
/// </summary>
|
||||
public void RecordEnemyDefeated(Character enemy)
|
||||
{
|
||||
if (enemy == null)
|
||||
return;
|
||||
DefeatedEnemies.Add(enemy);
|
||||
|
||||
var enemyExp = enemy.Level * enemy.Exp;
|
||||
var alivePlayers = 0;
|
||||
foreach (var c in pendingPlayerCards)
|
||||
{
|
||||
if (c?.Attributes?.Health?.Value > 0)
|
||||
alivePlayers++;
|
||||
}
|
||||
if (alivePlayers == 0)
|
||||
return;
|
||||
|
||||
var expPerPlayer = enemyExp / alivePlayers;
|
||||
foreach (var c in pendingPlayerCards)
|
||||
{
|
||||
if (c?.Attributes?.Health?.Value <= 0)
|
||||
continue;
|
||||
var id = c.ID;
|
||||
if (id == null)
|
||||
continue;
|
||||
PlayerExpGained.TryGetValue(id, out var current);
|
||||
PlayerExpGained[id] = current + expPerPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the battle and publishes <see cref="BattleEndedEvent"/>. The
|
||||
/// outcome is passed in by the caller: both screens set
|
||||
/// <see cref="IsDestroyed"/> before clearing, so it cannot be derived
|
||||
/// from that flag (it used to be, and every battle reported "defeat").
|
||||
/// </summary>
|
||||
private void ClearBattle(string result)
|
||||
{
|
||||
BattleActionMachine?.StopMachine();
|
||||
ActiveCharacter = null;
|
||||
pendingPlayerCards?.Clear();
|
||||
pendingEnemyCards?.Clear();
|
||||
OnBattleCleared?.Invoke();
|
||||
ModEventBus.Publish(new BattleEndedEvent
|
||||
{
|
||||
Result = result,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton scene-root for battle, wired as a self-registering Node
|
||||
/// (sets Instance on enter-tree, clears on exit-tree). On ready it
|
||||
/// assembles player characters from CharacterData.BattleSet, enemy
|
||||
/// cards from the pending <see cref="BattleRequest"/>, resolves the active
|
||||
/// <see cref="IBattleCore"/> via <see cref="BattleCoreRegistry"/>, and defers
|
||||
/// Init+Start to the next frame so the scene tree is fully built before core
|
||||
/// logic runs. Tracks PlayerExpGained (per-character, split evenly among
|
||||
/// survivors) and DefeatedEnemies for the victory/loot screen. Exposes
|
||||
/// OnCharacterDied and OnBattleCleared events so external systems (e.g.
|
||||
/// quests, the action machine) can react without coupling.
|
||||
/// ShowWinground / ShowDeadground are mutually exclusive end-of-battle
|
||||
/// paths — both set IsDestroyed to prevent double-triggering and
|
||||
/// reset battle speed before clearing state.
|
||||
/// <para/>
|
||||
/// The context derives from <see cref="ModNode"/> because mod assemblies get
|
||||
/// no Godot source generation: the core base receives the engine lifecycle
|
||||
/// callbacks and forwards them to the <c>OnXxx</c> overrides. It also
|
||||
/// implements <see cref="IActiveCharacterProvider"/> so core UI (the inventory
|
||||
/// panel's action-point row) can read the acting character without knowing
|
||||
/// anything about battle.
|
||||
/// </summary>
|
||||
public partial class BattleSceneContext : ModNode, IActiveCharacterProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton reference to the active battle scene context.
|
||||
/// Set on enter-tree, cleared on exit-tree.
|
||||
/// External systems read this to access battle state.
|
||||
/// </summary>
|
||||
public static BattleSceneContext Instance { get; private set; }
|
||||
|
||||
private TextureRect battleBg;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Actions.BattleActionMachine"/> node that
|
||||
/// processes the animation queue for this battle. Cores push
|
||||
/// animation actions through the context and the machine drives
|
||||
/// playback frame-by-frame.
|
||||
/// </summary>
|
||||
public Actions.BattleActionMachine BattleActionMachine { get; private set; }
|
||||
|
||||
private IBattleSpeedControl _battleSpeedControl;
|
||||
private Widget deadground;
|
||||
private Widget winground;
|
||||
|
||||
/// <summary>
|
||||
/// Per-character EXP earned during this battle, keyed by
|
||||
/// <see cref="Character.ID"/>. Populated by
|
||||
/// <see cref="RecordEnemyDefeated"/>; consumed by the victory screen
|
||||
/// provided by the turn-battle GUI mod.
|
||||
/// </summary>
|
||||
public Dictionary<string, int> PlayerExpGained { get; private set; } = new();
|
||||
/// <summary>
|
||||
/// Enemies defeated during this battle (for loot generation
|
||||
/// and recruitment rolls).
|
||||
/// </summary>
|
||||
public List<Character> DefeatedEnemies { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Guards against double-triggering end-of-battle paths.
|
||||
/// Set to <c>true</c> by <see cref="ShowWinground"/> or
|
||||
/// <see cref="ShowDeadground"/>.
|
||||
/// </summary>
|
||||
public bool IsDestroyed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, the battle core pauses processing (e.g.
|
||||
/// while a UI overlay is open). Set externally by pause widgets.
|
||||
/// </summary>
|
||||
public bool IsWait { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Character"/> whose turn is currently active
|
||||
/// in battle. Set by the battle core when a character is selected
|
||||
/// and cleared when the turn ends or selection is cancelled.
|
||||
/// <c>null</c> means no character is active (out of battle,
|
||||
/// between turns, or idle state). External panels such as the
|
||||
/// inventory panel read this to check whether the active
|
||||
/// character has enough action points before performing
|
||||
/// item-related actions.
|
||||
/// </summary>
|
||||
public Character ActiveCharacter { get; set; }
|
||||
|
||||
/// <summary>Fired when a character's health reaches 0.</summary>
|
||||
public event System.Action<Character> OnCharacterDied;
|
||||
/// <summary>
|
||||
/// Fired after battle state is cleared (both victory and defeat).
|
||||
/// External systems can hook this to release battle resources.
|
||||
/// </summary>
|
||||
public event System.Action OnBattleCleared;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="OnCharacterDied"/> to notify listeners
|
||||
/// (quests, achievements, the battle core) that a character
|
||||
/// has been defeated.
|
||||
/// </summary>
|
||||
public void NotifyCharacterDied(Character character)
|
||||
{
|
||||
OnCharacterDied?.Invoke(character);
|
||||
ModEventBus.Publish(new CharacterDiedEvent
|
||||
{
|
||||
CharacterId = character?.ID,
|
||||
CharacterName = character?.Name,
|
||||
Source = "battle",
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Claims the scene singleton and registers as the <see cref="ActiveCharacterRegistry"/> provider.</summary>
|
||||
protected override void OnEnterTree()
|
||||
{
|
||||
if (Instance != null && GodotObject.IsInstanceValid(Instance))
|
||||
{
|
||||
var scene = GetTree()?.CurrentScene?.Name ?? "?";
|
||||
var existingPath = Instance.GetPath().ToString();
|
||||
var myPath = GetPath().ToString();
|
||||
GameLogger.Log("DUPLICATE", $"BattleSceneContext.OnEnterTree: Instance ALREADY SET by '{existingPath}', overwriting with duplicate at '{myPath}' on scene '{scene}'", LogLevel.Error);
|
||||
}
|
||||
Instance = this;
|
||||
ActiveCharacterRegistry.Provider = this;
|
||||
}
|
||||
|
||||
/// <summary>Releases the singleton slot and the active-character provider so a later scene can claim them.</summary>
|
||||
protected override void OnExitTree()
|
||||
{
|
||||
base.OnExitTree();
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
if (ReferenceEquals(ActiveCharacterRegistry.Provider, this))
|
||||
ActiveCharacterRegistry.Provider = null;
|
||||
}
|
||||
|
||||
private List<Character> pendingPlayerCards;
|
||||
private List<Character> pendingEnemyCards;
|
||||
private IBattleCore activeBattleCore;
|
||||
|
||||
/// <summary>
|
||||
/// The battle core driving this scene, resolved from
|
||||
/// <see cref="BattleCoreRegistry"/> when the scene becomes ready. Exposed
|
||||
/// so tooling and tests can query the active core (for example for
|
||||
/// <see cref="IBattleAutoPlayable"/>) without reaching into UI nodes.
|
||||
/// </summary>
|
||||
public IBattleCore ActiveBattleCore => activeBattleCore;
|
||||
|
||||
/// <summary>Caches the battle backdrop node, resolves the active <see cref="IBattleCore"/> and starts the battle.</summary>
|
||||
protected override void OnReady()
|
||||
{
|
||||
battleBg = GetNodeOrNull<TextureRect>("BattleBg");
|
||||
|
||||
if (battleBg == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "battleBg not found!", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
BattleActionMachine = GetNodeOrNull<Actions.BattleActionMachine>("ToolPanel/BattleActionMachine");
|
||||
_battleSpeedControl = GetNodeOrNull<Control>("BattleSpeedControl") as IBattleSpeedControl;
|
||||
deadground = GetNodeOrNull<Widget>("Deadground");
|
||||
|
||||
winground = GetNodeOrNull<Widget>("Winground");
|
||||
|
||||
var data = BattleRequest.Current;
|
||||
if (data == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "BattleRequest.Current is null!", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
GameLogger.Log("BATTLE", $"set battle background: battleBg={(battleBg != null ? "found" : "NULL")}, texture={(data.Background != null ? "has" : "NULL")}", battleBg == null || data.Background == null ? LogLevel.Error : LogLevel.Debug);
|
||||
|
||||
if (data.Background != null)
|
||||
{
|
||||
battleBg.Texture = data.Background;
|
||||
GameLogger.Log("BATTLE", $" battleBg.Texture assigned: width={battleBg.Texture.GetWidth()}, height={battleBg.Texture.GetHeight()}", LogLevel.Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("BATTLE", $" data.Background is NULL — no background texture set", LogLevel.Error);
|
||||
}
|
||||
|
||||
var timedShader = Shaders.Resolve("timed_sprite");
|
||||
if (timedShader != null)
|
||||
{
|
||||
var bgMaterial = new ShaderMaterial { Shader = timedShader };
|
||||
battleBg.Material = bgMaterial;
|
||||
|
||||
if (data.DepthMap != null)
|
||||
bgMaterial.SetShaderParameter("depth_mask", data.DepthMap);
|
||||
if (data.AlbedoMap != null)
|
||||
bgMaterial.SetShaderParameter("albedo_map", data.AlbedoMap);
|
||||
|
||||
var lights = GetNodeOrNull<UiLightController>("Lights");
|
||||
if (lights != null)
|
||||
{
|
||||
var viewportSize = GetViewport().GetVisibleRect().Size;
|
||||
lights.SetPlayerLight(viewportSize / 2, 500f, 1f);
|
||||
lights.RegisterMaterial(bgMaterial);
|
||||
}
|
||||
GameLogger.Log("BATTLE", $"battleBg shader: timed_sprite applied, depthMap={(data.DepthMap != null ? "has" : "NULL")}, albedoMap={(data.AlbedoMap != null ? "has" : "NULL")}, lightsCtrl={(lights != null ? "found" : "NULL")}", lights == null ? LogLevel.Error : LogLevel.Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("BATTLE", "battleBg shader: timed_sprite NOT FOUND", LogLevel.Error);
|
||||
}
|
||||
|
||||
pendingPlayerCards = new List<Character>();
|
||||
foreach (var characterType in GameData.Instance.Runtime.CharacterData.BattleSet)
|
||||
{
|
||||
var characterInfo = GameData.Instance.Runtime.CharacterData.Characters[characterType];
|
||||
var template = CharacterFactory.Instance.Get(characterInfo.ID);
|
||||
var character = template != null ? template.Copy() : new Character { ID = characterInfo.ID };
|
||||
character.Attributes = characterInfo.Attributes ?? new CharacterAttributes();
|
||||
character.Level = characterInfo.Level;
|
||||
character.Exp = characterInfo.Exp;
|
||||
if (characterInfo.ActionSlots != null && characterInfo.ActionSlots.Any(id => !string.IsNullOrEmpty(id)))
|
||||
{
|
||||
character.Actions = characterInfo.ActionSlots
|
||||
.Where(id => !string.IsNullOrEmpty(id))
|
||||
.Select(id => ActionCharacterFactory.Instance.Get(id))
|
||||
.Where(a => a != null)
|
||||
.ToList();
|
||||
}
|
||||
pendingPlayerCards.Add(character);
|
||||
}
|
||||
|
||||
pendingEnemyCards = data.EnemiesCards?.Where(c => c != null).ToList() ?? new List<Character>();
|
||||
|
||||
if (!string.IsNullOrEmpty(data.ActiveBattleCore) && BattleCoreRegistry.Instance.HasCore(data.ActiveBattleCore))
|
||||
BattleCoreRegistry.Instance.SetActive(data.ActiveBattleCore);
|
||||
|
||||
try
|
||||
{
|
||||
activeBattleCore = BattleCoreRegistry.Instance.GetActive();
|
||||
}
|
||||
catch
|
||||
{
|
||||
activeBattleCore = null;
|
||||
}
|
||||
|
||||
if (activeBattleCore == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "No battle core available", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Callable.From(DeferredInit).CallDeferred();
|
||||
}
|
||||
|
||||
private void DeferredInit()
|
||||
{
|
||||
if (IsDestroyed || activeBattleCore == null)
|
||||
return;
|
||||
|
||||
var ctx = new BattleContextImpl(this);
|
||||
var playerArray = pendingPlayerCards?.ToArray() ?? new Character[0];
|
||||
var enemyArray = pendingEnemyCards?.ToArray() ?? new Character[0];
|
||||
|
||||
activeBattleCore.Init(playerArray, enemyArray, ctx);
|
||||
activeBattleCore.Start();
|
||||
|
||||
ModEventBus.Publish(new BattleStartedEvent
|
||||
{
|
||||
BattleType = activeBattleCore.GetType().Name,
|
||||
Enemies = enemyArray.Select(c => c.Name).ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the defeat screen: sets <see cref="IsDestroyed"/>,
|
||||
/// resets battle speed, clears battle state, and shows the
|
||||
/// deadground widget.
|
||||
/// </summary>
|
||||
public void ShowDeadground()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
IsDestroyed = true;
|
||||
_battleSpeedControl?.ResetToNormal();
|
||||
ClearBattle("defeat");
|
||||
deadground?.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the victory screen: sets <see cref="IsDestroyed"/>,
|
||||
/// resets battle speed, clears battle state, and shows the
|
||||
/// winground widget.
|
||||
/// </summary>
|
||||
public void ShowWinground()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
IsDestroyed = true;
|
||||
_battleSpeedControl?.ResetToNormal();
|
||||
ClearBattle("victory");
|
||||
winground?.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a defeated enemy: adds it to
|
||||
/// <see cref="DefeatedEnemies"/> and distributes its EXP
|
||||
/// (<c>Level * Exp</c>) equally among all surviving player
|
||||
/// characters in <see cref="PlayerExpGained"/>.
|
||||
/// </summary>
|
||||
public void RecordEnemyDefeated(Character enemy)
|
||||
{
|
||||
if (enemy == null)
|
||||
return;
|
||||
DefeatedEnemies.Add(enemy);
|
||||
|
||||
var enemyExp = enemy.Level * enemy.Exp;
|
||||
var alivePlayers = 0;
|
||||
foreach (var c in pendingPlayerCards)
|
||||
{
|
||||
if (c?.Attributes?.Health?.Value > 0)
|
||||
alivePlayers++;
|
||||
}
|
||||
if (alivePlayers == 0)
|
||||
return;
|
||||
|
||||
var expPerPlayer = enemyExp / alivePlayers;
|
||||
foreach (var c in pendingPlayerCards)
|
||||
{
|
||||
if (c?.Attributes?.Health?.Value <= 0)
|
||||
continue;
|
||||
var id = c.ID;
|
||||
if (id == null)
|
||||
continue;
|
||||
PlayerExpGained.TryGetValue(id, out var current);
|
||||
PlayerExpGained[id] = current + expPerPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the battle and publishes <see cref="BattleEndedEvent"/>. The
|
||||
/// outcome is passed in by the caller: both screens set
|
||||
/// <see cref="IsDestroyed"/> before clearing, so it cannot be derived
|
||||
/// from that flag (it used to be, and every battle reported "defeat").
|
||||
/// </summary>
|
||||
private void ClearBattle(string result)
|
||||
{
|
||||
BattleActionMachine?.StopMachine();
|
||||
ActiveCharacter = null;
|
||||
pendingPlayerCards?.Clear();
|
||||
pendingEnemyCards?.Clear();
|
||||
OnBattleCleared?.Invoke();
|
||||
ModEventBus.Publish(new BattleEndedEvent
|
||||
{
|
||||
Result = result,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-39
@@ -1,40 +1,40 @@
|
||||
using Cthangover.Core.Actions;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects which battle core (combat ruleset) the next battle will use.
|
||||
/// The "core" variable maps to a <see cref="BattleCoreRegistry"/> entry by
|
||||
/// string ID. Must be called before a battle starts, as
|
||||
/// <see cref="BattleService.Init"/> reads the active core at initialization time.
|
||||
/// </summary>
|
||||
public class BattleSetCoreAction : IScenarioAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Registered as "battle.set_core" — selects the combat ruleset
|
||||
/// for the next battle encounter. If "core" is empty or missing, logs a
|
||||
/// warning and returns without modifying the registry.
|
||||
/// </summary>
|
||||
public string Name => "battle.set_core";
|
||||
|
||||
/// <summary>
|
||||
/// Reads the "core" variable and sets it as the active battle core
|
||||
/// via <see cref="BattleCoreRegistry.SetActive"/>. The core determines
|
||||
/// combat mechanics (turn order, ability system, etc.) for the
|
||||
/// next initiated battle.
|
||||
/// </summary>
|
||||
public void Run(IActionContext ctx)
|
||||
{
|
||||
var core = ctx.GetParam("core");
|
||||
if (string.IsNullOrEmpty(core))
|
||||
{
|
||||
ctx.Log("EVENT", "BattleSetCoreAction: missing 'core' variable");
|
||||
return;
|
||||
}
|
||||
|
||||
BattleCoreRegistry.Instance.SetActive(core);
|
||||
ctx.Log("EVENT", $"BattleSetCoreAction: battle core set to '{core}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Actions;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects which battle core (combat ruleset) the next battle will use.
|
||||
/// The "core" variable maps to a <see cref="BattleCoreRegistry"/> entry by
|
||||
/// string ID. Must be called before a battle starts, as
|
||||
/// <see cref="BattleService.Init"/> reads the active core at initialization time.
|
||||
/// </summary>
|
||||
public class BattleSetCoreAction : IScenarioAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Registered as "battle.set_core" — selects the combat ruleset
|
||||
/// for the next battle encounter. If "core" is empty or missing, logs a
|
||||
/// warning and returns without modifying the registry.
|
||||
/// </summary>
|
||||
public string Name => "battle.set_core";
|
||||
|
||||
/// <summary>
|
||||
/// Reads the "core" variable and sets it as the active battle core
|
||||
/// via <see cref="BattleCoreRegistry.SetActive"/>. The core determines
|
||||
/// combat mechanics (turn order, ability system, etc.) for the
|
||||
/// next initiated battle.
|
||||
/// </summary>
|
||||
public void Run(IActionContext ctx)
|
||||
{
|
||||
var core = ctx.GetParam("core");
|
||||
if (string.IsNullOrEmpty(core))
|
||||
{
|
||||
ctx.Log("EVENT", "BattleSetCoreAction: missing 'core' variable");
|
||||
return;
|
||||
}
|
||||
|
||||
BattleCoreRegistry.Instance.SetActive(core);
|
||||
ctx.Log("EVENT", $"BattleSetCoreAction: battle core set to '{core}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+110
-110
@@ -1,110 +1,110 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// In-battle speed toggle bar (x1/x4/x8). Directly sets
|
||||
/// Godot.Engine.TimeScale to globally accelerate the entire game loop —
|
||||
/// not just animations. Persists the choice to GameData.Settings
|
||||
/// so it survives scene reloads. <see cref="ResetToNormal"/> restores 1x
|
||||
/// without saving and is the part the core scene consumes through
|
||||
/// <see cref="IBattleSpeedControl"/> when battle ends.
|
||||
/// </summary>
|
||||
public partial class BattleSpeedControl : Control, IBattleSpeedControl
|
||||
{
|
||||
private readonly Dictionary<int, Button> _buttons = new();
|
||||
private int _activeSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the speed bar in the constructor. Godot never runs a
|
||||
/// <c>_Ready</c> override declared in a mod assembly, so the buttons and the
|
||||
/// saved-speed restore must happen here. Anchors are used instead of a
|
||||
/// viewport-size lookup because the viewport is unavailable before the node
|
||||
/// enters the tree.
|
||||
/// </summary>
|
||||
public BattleSpeedControl()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
var speeds = new[] { 1, 4, 8 };
|
||||
var hbox = new HBoxContainer { Name = "SpeedButtons" };
|
||||
hbox.Alignment = BoxContainer.AlignmentMode.Begin;
|
||||
|
||||
foreach (var speed in speeds)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Text = $"x{speed}",
|
||||
CustomMinimumSize = new Vector2(48, 32)
|
||||
};
|
||||
btn.Pressed += () => OnSpeedSelected(speed);
|
||||
hbox.AddChild(btn);
|
||||
_buttons[speed] = btn;
|
||||
}
|
||||
|
||||
AddChild(hbox);
|
||||
|
||||
AnchorLeft = 1f;
|
||||
AnchorRight = 1f;
|
||||
AnchorTop = 0f;
|
||||
AnchorBottom = 0f;
|
||||
OffsetLeft = -200f;
|
||||
OffsetRight = -20f;
|
||||
OffsetTop = 20f;
|
||||
OffsetBottom = 52f;
|
||||
|
||||
ApplySavedSpeed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the speed from <see cref="GameData.Settings.BattleSpeed"/>.
|
||||
/// Called on <c>_Ready</c> to reapply the saved preference.
|
||||
/// </summary>
|
||||
public void ApplySavedSpeed()
|
||||
{
|
||||
var speed = GameData.Instance.Settings.BattleSpeed;
|
||||
if (speed < 1 || speed > 8)
|
||||
speed = 1;
|
||||
|
||||
_activeSpeed = speed;
|
||||
Godot.Engine.TimeScale = speed;
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets <see cref="Godot.Engine.TimeScale"/> to 1.0x without
|
||||
/// persisting to settings. Used when battle ends so the lobby
|
||||
/// and menus run at normal speed regardless of the player's
|
||||
/// in-battle preference.
|
||||
/// </summary>
|
||||
public void ResetToNormal()
|
||||
{
|
||||
Godot.Engine.TimeScale = 1.0f;
|
||||
}
|
||||
|
||||
private void OnSpeedSelected(int speed)
|
||||
{
|
||||
_activeSpeed = speed;
|
||||
Godot.Engine.TimeScale = speed;
|
||||
|
||||
var settings = GameData.Instance.Settings;
|
||||
settings.BattleSpeed = speed;
|
||||
settings.Save();
|
||||
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
private void UpdateHighlight()
|
||||
{
|
||||
foreach (var kv in _buttons)
|
||||
{
|
||||
var isActive = kv.Key == _activeSpeed;
|
||||
kv.Value.Modulate = isActive ? UiTokens.Success : UiTokens.White;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// In-battle speed toggle bar (x1/x4/x8). Directly sets
|
||||
/// Godot.Engine.TimeScale to globally accelerate the entire game loop —
|
||||
/// not just animations. Persists the choice to GameData.Settings
|
||||
/// so it survives scene reloads. <see cref="ResetToNormal"/> restores 1x
|
||||
/// without saving and is the part the core scene consumes through
|
||||
/// <see cref="IBattleSpeedControl"/> when battle ends.
|
||||
/// </summary>
|
||||
public partial class BattleSpeedControl : Control, IBattleSpeedControl
|
||||
{
|
||||
private readonly Dictionary<int, Button> _buttons = new();
|
||||
private int _activeSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the speed bar in the constructor. Godot never runs a
|
||||
/// <c>_Ready</c> override declared in a mod assembly, so the buttons and the
|
||||
/// saved-speed restore must happen here. Anchors are used instead of a
|
||||
/// viewport-size lookup because the viewport is unavailable before the node
|
||||
/// enters the tree.
|
||||
/// </summary>
|
||||
public BattleSpeedControl()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
var speeds = new[] { 1, 4, 8 };
|
||||
var hbox = new HBoxContainer { Name = "SpeedButtons" };
|
||||
hbox.Alignment = BoxContainer.AlignmentMode.Begin;
|
||||
|
||||
foreach (var speed in speeds)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Text = $"x{speed}",
|
||||
CustomMinimumSize = new Vector2(48, 32)
|
||||
};
|
||||
btn.Pressed += () => OnSpeedSelected(speed);
|
||||
hbox.AddChild(btn);
|
||||
_buttons[speed] = btn;
|
||||
}
|
||||
|
||||
AddChild(hbox);
|
||||
|
||||
AnchorLeft = 1f;
|
||||
AnchorRight = 1f;
|
||||
AnchorTop = 0f;
|
||||
AnchorBottom = 0f;
|
||||
OffsetLeft = -200f;
|
||||
OffsetRight = -20f;
|
||||
OffsetTop = 20f;
|
||||
OffsetBottom = 52f;
|
||||
|
||||
ApplySavedSpeed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the speed from <see cref="GameData.Settings.BattleSpeed"/>.
|
||||
/// Called on <c>_Ready</c> to reapply the saved preference.
|
||||
/// </summary>
|
||||
public void ApplySavedSpeed()
|
||||
{
|
||||
var speed = GameData.Instance.Settings.BattleSpeed;
|
||||
if (speed < 1 || speed > 8)
|
||||
speed = 1;
|
||||
|
||||
_activeSpeed = speed;
|
||||
Godot.Engine.TimeScale = speed;
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets <see cref="Godot.Engine.TimeScale"/> to 1.0x without
|
||||
/// persisting to settings. Used when battle ends so the lobby
|
||||
/// and menus run at normal speed regardless of the player's
|
||||
/// in-battle preference.
|
||||
/// </summary>
|
||||
public void ResetToNormal()
|
||||
{
|
||||
Godot.Engine.TimeScale = 1.0f;
|
||||
}
|
||||
|
||||
private void OnSpeedSelected(int speed)
|
||||
{
|
||||
_activeSpeed = speed;
|
||||
Godot.Engine.TimeScale = speed;
|
||||
|
||||
var settings = GameData.Instance.Settings;
|
||||
settings.BattleSpeed = speed;
|
||||
settings.Save();
|
||||
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
private void UpdateHighlight()
|
||||
{
|
||||
foreach (var kv in _buttons)
|
||||
{
|
||||
var isActive = kv.Key == _activeSpeed;
|
||||
kv.Value.Modulate = isActive ? UiTokens.Success : UiTokens.White;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-30
@@ -1,30 +1,30 @@
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
/// <summary>
|
||||
/// Result struct for battle action resolution. Source and Target carry
|
||||
/// Damage/Defence snapshots for both sides of the exchange. Result indicates
|
||||
/// whether the action succeeded (e.g. hit landed vs. missed/blocked).
|
||||
/// A struct rather than a class because these are short-lived stack values
|
||||
/// created in tight battle loops — no heap allocation.
|
||||
/// </summary>
|
||||
public struct ChangedAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Snapshot of the action source's damage and defence after the
|
||||
/// exchange.
|
||||
/// </summary>
|
||||
public CharacterSet Source;
|
||||
/// <summary>
|
||||
/// Snapshot of the action target's damage and defence after the
|
||||
/// exchange.
|
||||
/// </summary>
|
||||
public CharacterSet Target;
|
||||
/// <summary>
|
||||
/// Whether the action succeeded (e.g. hit landed vs. missed or
|
||||
/// blocked).
|
||||
/// </summary>
|
||||
public bool Result;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
/// <summary>
|
||||
/// Result struct for battle action resolution. Source and Target carry
|
||||
/// Damage/Defence snapshots for both sides of the exchange. Result indicates
|
||||
/// whether the action succeeded (e.g. hit landed vs. missed/blocked).
|
||||
/// A struct rather than a class because these are short-lived stack values
|
||||
/// created in tight battle loops — no heap allocation.
|
||||
/// </summary>
|
||||
public struct ChangedAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Snapshot of the action source's damage and defence after the
|
||||
/// exchange.
|
||||
/// </summary>
|
||||
public CharacterSet Source;
|
||||
/// <summary>
|
||||
/// Snapshot of the action target's damage and defence after the
|
||||
/// exchange.
|
||||
/// </summary>
|
||||
public CharacterSet Target;
|
||||
/// <summary>
|
||||
/// Whether the action succeeded (e.g. hit landed vs. missed or
|
||||
/// blocked).
|
||||
/// </summary>
|
||||
public bool Result;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+20
-20
@@ -1,20 +1,20 @@
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
/// <summary>
|
||||
/// Lightweight snapshot of damage/defence values for battle action results.
|
||||
/// A struct to avoid heap allocation in the battle loop where these are
|
||||
/// created for every action exchange. Used by ChangedAttributes to carry
|
||||
/// both source and target state.
|
||||
/// </summary>
|
||||
public struct CharacterSet
|
||||
{
|
||||
/// <summary>
|
||||
/// Damage dealt or received in this exchange.
|
||||
/// </summary>
|
||||
public int Damage;
|
||||
/// <summary>
|
||||
/// Defence value at the time of the exchange.
|
||||
/// </summary>
|
||||
public int Defence;
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
/// <summary>
|
||||
/// Lightweight snapshot of damage/defence values for battle action results.
|
||||
/// A struct to avoid heap allocation in the battle loop where these are
|
||||
/// created for every action exchange. Used by ChangedAttributes to carry
|
||||
/// both source and target state.
|
||||
/// </summary>
|
||||
public struct CharacterSet
|
||||
{
|
||||
/// <summary>
|
||||
/// Damage dealt or received in this exchange.
|
||||
/// </summary>
|
||||
public int Damage;
|
||||
/// <summary>
|
||||
/// Defence value at the time of the exchange.
|
||||
/// </summary>
|
||||
public int Defence;
|
||||
}
|
||||
}
|
||||
+63
-63
@@ -1,63 +1,63 @@
|
||||
using Cthangover.Core.Audio;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Defeat screen widget. Minimal compared to WingroundBehaviour —
|
||||
/// only sets the deadground background texture (resolved from mods)
|
||||
/// and offers a Main Menu button that clears BattleData and reloads
|
||||
/// the menu scene. No loot, no EXP.
|
||||
/// </summary>
|
||||
public partial class DeadgroundBehaviour : TransitionWidget
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears battle data and switches to the main menu scene.
|
||||
/// Bound to the "Main Menu" button on the defeat screen.
|
||||
/// </summary>
|
||||
public void ToMainMenuClick()
|
||||
{
|
||||
var sceneService = GetNode<GodotSceneService>("/root/GodotSceneService");
|
||||
sceneService?.SwitchToMenu();
|
||||
BattleRequest.Current = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the defeat sound, resolves the deadground background
|
||||
/// texture from mods, and shows the transition widget.
|
||||
/// </summary>
|
||||
public override void Show()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
CthangoverTheme.ApplyPrimary(GetNodeOrNull<Button>("Content/CenterPanel/MainMenuButton"));
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show() ENTER: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
|
||||
var audioService = GetNodeOrNull<Node>("/root/AudioService") as IAudioService;
|
||||
audioService?.PlaySound("battle/deadground", SoundType.UI);
|
||||
|
||||
var bg = GetNodeOrNull<TextureRect>("TransitionBg");
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show: TransitionBg={(bg != null ? "found" : "NULL")}, texture={(bg?.Texture != null ? "has" : "null")}", LogLevel.Error);
|
||||
|
||||
if (bg != null && bg.Texture == null)
|
||||
{
|
||||
var tex = Textures.Resolve("deadground");
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show: ResolveTexture('deadground') = {(tex != null ? "loaded" : "NULL")}", LogLevel.Error);
|
||||
bg.Texture = tex;
|
||||
}
|
||||
|
||||
base.Show();
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show() EXIT: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Audio;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Defeat screen widget. Minimal compared to WingroundBehaviour —
|
||||
/// only sets the deadground background texture (resolved from mods)
|
||||
/// and offers a Main Menu button that clears BattleData and reloads
|
||||
/// the menu scene. No loot, no EXP.
|
||||
/// </summary>
|
||||
public partial class DeadgroundBehaviour : TransitionWidget
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears battle data and switches to the main menu scene.
|
||||
/// Bound to the "Main Menu" button on the defeat screen.
|
||||
/// </summary>
|
||||
public void ToMainMenuClick()
|
||||
{
|
||||
var sceneService = GetNode<GodotSceneService>("/root/GodotSceneService");
|
||||
sceneService?.SwitchToMenu();
|
||||
BattleRequest.Current = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the defeat sound, resolves the deadground background
|
||||
/// texture from mods, and shows the transition widget.
|
||||
/// </summary>
|
||||
public override void Show()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
CthangoverTheme.ApplyPrimary(GetNodeOrNull<Button>("Content/CenterPanel/MainMenuButton"));
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show() ENTER: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
|
||||
var audioService = GetNodeOrNull<Node>("/root/AudioService") as IAudioService;
|
||||
audioService?.PlaySound("battle/deadground", SoundType.UI);
|
||||
|
||||
var bg = GetNodeOrNull<TextureRect>("TransitionBg");
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show: TransitionBg={(bg != null ? "found" : "NULL")}, texture={(bg?.Texture != null ? "has" : "null")}", LogLevel.Error);
|
||||
|
||||
if (bg != null && bg.Texture == null)
|
||||
{
|
||||
var tex = Textures.Resolve("deadground");
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show: ResolveTexture('deadground') = {(tex != null ? "loaded" : "NULL")}", LogLevel.Error);
|
||||
bg.Texture = tex;
|
||||
}
|
||||
|
||||
base.Show();
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show() EXIT: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-46
@@ -1,46 +1,46 @@
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Single character EXP report in the victory screen. Displays the
|
||||
/// character icon, localized name, and "+N" EXP value. Uses the same
|
||||
/// ResolveMissingNodes pattern as LootReportItem for robustness
|
||||
/// against missing scene links.
|
||||
/// </summary>
|
||||
public partial class ExpReportCard : Control
|
||||
{
|
||||
[Export] private TextureRect icon;
|
||||
[Export] private Label nameLabel;
|
||||
[Export] private Label expLabel;
|
||||
|
||||
private void ResolveMissingNodes()
|
||||
{
|
||||
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||
nameLabel ??= GetNodeOrNull<Label>("Panel/NameLabel");
|
||||
expLabel ??= GetNodeOrNull<Label>("Panel/ExpLabel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates the card with a character icon, localised name, and
|
||||
/// "+N" EXP value. Calls <c>ResolveMissingNodes</c> to tolerate
|
||||
/// incomplete scene wiring in the editor.
|
||||
/// </summary>
|
||||
public void Setup(Texture2D characterIcon, string characterName, int exp)
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
ResolveMissingNodes();
|
||||
|
||||
if (icon != null && characterIcon != null)
|
||||
icon.Texture = characterIcon;
|
||||
|
||||
if (nameLabel != null)
|
||||
nameLabel.Text = characterName;
|
||||
|
||||
if (expLabel != null)
|
||||
expLabel.Text = "+" + exp;
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Single character EXP report in the victory screen. Displays the
|
||||
/// character icon, localized name, and "+N" EXP value. Uses the same
|
||||
/// ResolveMissingNodes pattern as LootReportItem for robustness
|
||||
/// against missing scene links.
|
||||
/// </summary>
|
||||
public partial class ExpReportCard : Control
|
||||
{
|
||||
[Export] private TextureRect icon;
|
||||
[Export] private Label nameLabel;
|
||||
[Export] private Label expLabel;
|
||||
|
||||
private void ResolveMissingNodes()
|
||||
{
|
||||
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||
nameLabel ??= GetNodeOrNull<Label>("Panel/NameLabel");
|
||||
expLabel ??= GetNodeOrNull<Label>("Panel/ExpLabel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates the card with a character icon, localised name, and
|
||||
/// "+N" EXP value. Calls <c>ResolveMissingNodes</c> to tolerate
|
||||
/// incomplete scene wiring in the editor.
|
||||
/// </summary>
|
||||
public void Setup(Texture2D characterIcon, string characterName, int exp)
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
ResolveMissingNodes();
|
||||
|
||||
if (icon != null && characterIcon != null)
|
||||
icon.Texture = characterIcon;
|
||||
|
||||
if (nameLabel != null)
|
||||
nameLabel.Text = characterName;
|
||||
|
||||
if (expLabel != null)
|
||||
expLabel.Text = "+" + exp;
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-46
@@ -1,46 +1,46 @@
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// A single loot cell in the victory grid. Shows icon with an optional
|
||||
/// "xN" count label (hidden when count is 1). ResolveMissingNodes
|
||||
/// supports both editor-assigned exports and runtime lookups so the
|
||||
/// prefab works with incomplete scene wiring. TooltipText is set to
|
||||
/// the localized item name for hover.
|
||||
/// </summary>
|
||||
public partial class LootReportItem : Control
|
||||
{
|
||||
[Export] private TextureRect icon;
|
||||
[Export] private Label countLabel;
|
||||
|
||||
private void ResolveMissingNodes()
|
||||
{
|
||||
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||
countLabel ??= GetNodeOrNull<Label>("Panel/CountLabel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the loot cell: sets the item icon, a "xN" count
|
||||
/// label (hidden when <paramref name="count"/> is 1), and the
|
||||
/// localised name as the tooltip. Calls
|
||||
/// <c>ResolveMissingNodes</c> to handle missing editor
|
||||
/// assignments gracefully.
|
||||
/// </summary>
|
||||
public void Setup(Texture2D itemIcon, string localizedName, int count)
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
ResolveMissingNodes();
|
||||
|
||||
if (icon != null && itemIcon != null)
|
||||
icon.Texture = itemIcon;
|
||||
|
||||
if (countLabel != null)
|
||||
countLabel.Text = count > 1 ? "x" + count : "";
|
||||
|
||||
TooltipText = localizedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// A single loot cell in the victory grid. Shows icon with an optional
|
||||
/// "xN" count label (hidden when count is 1). ResolveMissingNodes
|
||||
/// supports both editor-assigned exports and runtime lookups so the
|
||||
/// prefab works with incomplete scene wiring. TooltipText is set to
|
||||
/// the localized item name for hover.
|
||||
/// </summary>
|
||||
public partial class LootReportItem : Control
|
||||
{
|
||||
[Export] private TextureRect icon;
|
||||
[Export] private Label countLabel;
|
||||
|
||||
private void ResolveMissingNodes()
|
||||
{
|
||||
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||
countLabel ??= GetNodeOrNull<Label>("Panel/CountLabel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the loot cell: sets the item icon, a "xN" count
|
||||
/// label (hidden when <paramref name="count"/> is 1), and the
|
||||
/// localised name as the tooltip. Calls
|
||||
/// <c>ResolveMissingNodes</c> to handle missing editor
|
||||
/// assignments gracefully.
|
||||
/// </summary>
|
||||
public void Setup(Texture2D itemIcon, string localizedName, int count)
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
ResolveMissingNodes();
|
||||
|
||||
if (icon != null && itemIcon != null)
|
||||
icon.Texture = itemIcon;
|
||||
|
||||
if (countLabel != null)
|
||||
countLabel.Text = count > 1 ? "x" + count : "";
|
||||
|
||||
TooltipText = localizedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
-117
@@ -1,117 +1,117 @@
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Floating damage-number overlay. Self-contained: creates its own Label
|
||||
/// in _Ready, floats upward with random horizontal drift, fades alpha,
|
||||
/// then QueueFrees itself. Static factory methods (SpawnDamage,
|
||||
/// SpawnDefence) instantiate and parent it in one call — damage is red,
|
||||
/// heal is green, defence is blue. MouseFilter is Ignore so clicks pass
|
||||
/// through to cards underneath.
|
||||
/// </summary>
|
||||
public partial class ShowDamageBehaviour : Control
|
||||
{
|
||||
private Label _label;
|
||||
|
||||
private float duration = 1.0f;
|
||||
private float floatDistance = 80f;
|
||||
private float driftRange = 30f;
|
||||
private Vector2 startPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the label on first use. Godot does not dispatch <c>_Ready</c>
|
||||
/// overrides declared in mod assemblies (only methods generated in the core
|
||||
/// project run), so the label cannot be created in <c>_Ready</c> — it is
|
||||
/// created here, right before the first animation starts.
|
||||
/// </summary>
|
||||
private void EnsureLabel()
|
||||
{
|
||||
if (_label != null)
|
||||
return;
|
||||
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
|
||||
_label = new Label();
|
||||
_label.SetAnchorsAndOffsetsPreset(LayoutPreset.Center);
|
||||
_label.HorizontalAlignment = HorizontalAlignment.Center;
|
||||
_label.VerticalAlignment = VerticalAlignment.Center;
|
||||
_label.AddThemeFontSizeOverride("font_size", 28);
|
||||
_label.AddThemeColorOverride("font_outline_color", UiTokens.Alpha(Colors.Black, 1f));
|
||||
_label.AddThemeConstantOverride("outline_size", 6);
|
||||
AddChild(_label);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and parents a floating damage (or heal) number.
|
||||
/// Damage is red, heal is green. The label floats upward with
|
||||
/// a random horizontal drift and auto-destructs after the
|
||||
/// tween completes. Skips values <= 0.
|
||||
/// </summary>
|
||||
public static void SpawnDamage(int amount, Node parent, Vector2 position, bool isHeal = false)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
var color = isHeal
|
||||
? UiTokens.SuccessText
|
||||
: UiTokens.DangerText;
|
||||
|
||||
var hint = new ShowDamageBehaviour();
|
||||
parent.AddChild(hint);
|
||||
hint.Init(amount, position, color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and parents a floating defence number (blue).
|
||||
/// Same floating behaviour as <see cref="SpawnDamage"/>.
|
||||
/// Skips values <= 0.
|
||||
/// </summary>
|
||||
public static void SpawnDefence(int amount, Node parent, Vector2 position)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
var hint = new ShowDamageBehaviour();
|
||||
parent.AddChild(hint);
|
||||
hint.Init(amount, position, UiTokens.Info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the label text, colour, and world position, then starts
|
||||
/// the floating animation tween. Called by the static factory
|
||||
/// methods after instantiation.
|
||||
/// </summary>
|
||||
public void Init(int amount, Vector2 worldPosition, Color? color = null)
|
||||
{
|
||||
EnsureLabel();
|
||||
|
||||
_label.Text = amount.ToString();
|
||||
if (color.HasValue)
|
||||
_label.Modulate = color.Value;
|
||||
|
||||
GlobalPosition = worldPosition;
|
||||
startPosition = Position;
|
||||
|
||||
RunAnimation();
|
||||
}
|
||||
|
||||
private async void RunAnimation()
|
||||
{
|
||||
float driftX = (float)GD.RandRange(-driftRange, driftRange);
|
||||
Vector2 targetPos = startPosition + new Vector2(driftX, -floatDistance);
|
||||
|
||||
var tween = CreateTween();
|
||||
tween.SetParallel(true);
|
||||
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
||||
tween.TweenProperty(this, "position", targetPos, duration);
|
||||
|
||||
var fadeTween = CreateTween();
|
||||
fadeTween.TweenProperty(_label, "modulate:a", 0f, duration * 0.8f);
|
||||
|
||||
await ToSignal(tween, "finished");
|
||||
QueueFree();
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Floating damage-number overlay. Self-contained: creates its own Label
|
||||
/// in _Ready, floats upward with random horizontal drift, fades alpha,
|
||||
/// then QueueFrees itself. Static factory methods (SpawnDamage,
|
||||
/// SpawnDefence) instantiate and parent it in one call — damage is red,
|
||||
/// heal is green, defence is blue. MouseFilter is Ignore so clicks pass
|
||||
/// through to cards underneath.
|
||||
/// </summary>
|
||||
public partial class ShowDamageBehaviour : Control
|
||||
{
|
||||
private Label _label;
|
||||
|
||||
private float duration = 1.0f;
|
||||
private float floatDistance = 80f;
|
||||
private float driftRange = 30f;
|
||||
private Vector2 startPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the label on first use. Godot does not dispatch <c>_Ready</c>
|
||||
/// overrides declared in mod assemblies (only methods generated in the core
|
||||
/// project run), so the label cannot be created in <c>_Ready</c> — it is
|
||||
/// created here, right before the first animation starts.
|
||||
/// </summary>
|
||||
private void EnsureLabel()
|
||||
{
|
||||
if (_label != null)
|
||||
return;
|
||||
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
|
||||
_label = new Label();
|
||||
_label.SetAnchorsAndOffsetsPreset(LayoutPreset.Center);
|
||||
_label.HorizontalAlignment = HorizontalAlignment.Center;
|
||||
_label.VerticalAlignment = VerticalAlignment.Center;
|
||||
_label.AddThemeFontSizeOverride("font_size", 28);
|
||||
_label.AddThemeColorOverride("font_outline_color", UiTokens.Alpha(Colors.Black, 1f));
|
||||
_label.AddThemeConstantOverride("outline_size", 6);
|
||||
AddChild(_label);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and parents a floating damage (or heal) number.
|
||||
/// Damage is red, heal is green. The label floats upward with
|
||||
/// a random horizontal drift and auto-destructs after the
|
||||
/// tween completes. Skips values <= 0.
|
||||
/// </summary>
|
||||
public static void SpawnDamage(int amount, Node parent, Vector2 position, bool isHeal = false)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
var color = isHeal
|
||||
? UiTokens.SuccessText
|
||||
: UiTokens.DangerText;
|
||||
|
||||
var hint = new ShowDamageBehaviour();
|
||||
parent.AddChild(hint);
|
||||
hint.Init(amount, position, color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and parents a floating defence number (blue).
|
||||
/// Same floating behaviour as <see cref="SpawnDamage"/>.
|
||||
/// Skips values <= 0.
|
||||
/// </summary>
|
||||
public static void SpawnDefence(int amount, Node parent, Vector2 position)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
var hint = new ShowDamageBehaviour();
|
||||
parent.AddChild(hint);
|
||||
hint.Init(amount, position, UiTokens.Info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the label text, colour, and world position, then starts
|
||||
/// the floating animation tween. Called by the static factory
|
||||
/// methods after instantiation.
|
||||
/// </summary>
|
||||
public void Init(int amount, Vector2 worldPosition, Color? color = null)
|
||||
{
|
||||
EnsureLabel();
|
||||
|
||||
_label.Text = amount.ToString();
|
||||
if (color.HasValue)
|
||||
_label.Modulate = color.Value;
|
||||
|
||||
GlobalPosition = worldPosition;
|
||||
startPosition = Position;
|
||||
|
||||
RunAnimation();
|
||||
}
|
||||
|
||||
private async void RunAnimation()
|
||||
{
|
||||
float driftX = (float)GD.RandRange(-driftRange, driftRange);
|
||||
Vector2 targetPos = startPosition + new Vector2(driftX, -floatDistance);
|
||||
|
||||
var tween = CreateTween();
|
||||
tween.SetParallel(true);
|
||||
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
||||
tween.TweenProperty(this, "position", targetPos, duration);
|
||||
|
||||
var fadeTween = CreateTween();
|
||||
fadeTween.TweenProperty(_label, "modulate:a", 0f, duration * 0.8f);
|
||||
|
||||
await ToSignal(tween, "finished");
|
||||
QueueFree();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,58 @@
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Cards.StatusEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Behaviour hooks for a status effect type. Each hook receives the
|
||||
/// owning character so a single IStatusActions instance can be
|
||||
/// shared across multiple characters. OnDealDamage / OnTakeDamage
|
||||
/// use ref int to let effects amplify or reduce damage before it
|
||||
/// is applied. ModifyAttributes is a passive stat modifier called
|
||||
/// independently of turn phases. OnFinalAction fires once when the
|
||||
/// effect expires. All hooks have empty default semantics — effects
|
||||
/// override only the hooks they care about.
|
||||
/// </summary>
|
||||
public interface IStatusActions
|
||||
{
|
||||
/// <summary>Factory key matching the <c>Actions</c> field in
|
||||
/// <see cref="StatusEffectInfo"/>, e.g. <c>"basic/stun"</c>.</summary>
|
||||
string ID { get; }
|
||||
|
||||
/// <summary>Fires when the owning character's turn begins. Use for
|
||||
/// start-of-turn damage-over-time or regeneration effects.</summary>
|
||||
void OnTurnStart(Character target);
|
||||
/// <summary>Fires when the owning character's turn ends. Mirrors
|
||||
/// <see cref="OnTurnStart"/> for symmetry; useful for effects that
|
||||
/// activate after the character acts.</summary>
|
||||
void OnTurnEnd(Character target);
|
||||
/// <summary>Called before the owning character deals damage to
|
||||
/// <paramref name="target"/>. Modify <paramref name="damage"/> via
|
||||
/// <see langword="ref"/> to amplify or reduce outgoing damage —
|
||||
/// all active effects stack multiplicatively through the queue's
|
||||
/// iteration.</summary>
|
||||
void OnDealDamage(Character source, Character target, ref int damage);
|
||||
/// <summary>Called before the owning character receives damage from
|
||||
/// <paramref name="source"/>. Modify <paramref name="damage"/> via
|
||||
/// <see langword="ref"/> to add damage reduction or vulnerability.
|
||||
/// Fires after <see cref="OnDealDamage"/> in the damage
|
||||
/// pipeline.</summary>
|
||||
void OnTakeDamage(Character target, Character source, ref int damage);
|
||||
/// <summary>Fires immediately when the effect is first placed on a
|
||||
/// character. Use for one-shot stat boosts, visual feedback, or
|
||||
/// initialisation logic.</summary>
|
||||
void OnApply(Character target);
|
||||
/// <summary>Fires when the effect is manually removed (e.g. via a
|
||||
/// cleanse card). Not called on natural expiration — use
|
||||
/// <see cref="OnFinalAction"/> for that path.</summary>
|
||||
void OnRemove(Character target);
|
||||
/// <summary>Fires once when the effect expires naturally (timer reaches
|
||||
/// zero). Use for end-of-effect triggers like explosion damage or
|
||||
/// stat restoration. Not called on manual removal.</summary>
|
||||
void OnFinalAction(Character target);
|
||||
/// <summary>Passive stat modifier invoked independently of turn phases.
|
||||
/// Receives a reference to the character's attribute container;
|
||||
/// implementors can add flat or percentage bonuses that other
|
||||
/// effects may also contribute to.</summary>
|
||||
void ModifyAttributes(CharacterAttributes attributes);
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Cards.StatusEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Behaviour hooks for a status effect type. Each hook receives the
|
||||
/// owning character so a single IStatusActions instance can be
|
||||
/// shared across multiple characters. OnDealDamage / OnTakeDamage
|
||||
/// use ref int to let effects amplify or reduce damage before it
|
||||
/// is applied. ModifyAttributes is a passive stat modifier called
|
||||
/// independently of turn phases. OnFinalAction fires once when the
|
||||
/// effect expires. All hooks have empty default semantics — effects
|
||||
/// override only the hooks they care about.
|
||||
/// </summary>
|
||||
public interface IStatusActions
|
||||
{
|
||||
/// <summary>Factory key matching the <c>Actions</c> field in
|
||||
/// <see cref="StatusEffectInfo"/>, e.g. <c>"basic/stun"</c>.</summary>
|
||||
string ID { get; }
|
||||
|
||||
/// <summary>Fires when the owning character's turn begins. Use for
|
||||
/// start-of-turn damage-over-time or regeneration effects.</summary>
|
||||
void OnTurnStart(Character target);
|
||||
/// <summary>Fires when the owning character's turn ends. Mirrors
|
||||
/// <see cref="OnTurnStart"/> for symmetry; useful for effects that
|
||||
/// activate after the character acts.</summary>
|
||||
void OnTurnEnd(Character target);
|
||||
/// <summary>Called before the owning character deals damage to
|
||||
/// <paramref name="target"/>. Modify <paramref name="damage"/> via
|
||||
/// <see langword="ref"/> to amplify or reduce outgoing damage —
|
||||
/// all active effects stack multiplicatively through the queue's
|
||||
/// iteration.</summary>
|
||||
void OnDealDamage(Character source, Character target, ref int damage);
|
||||
/// <summary>Called before the owning character receives damage from
|
||||
/// <paramref name="source"/>. Modify <paramref name="damage"/> via
|
||||
/// <see langword="ref"/> to add damage reduction or vulnerability.
|
||||
/// Fires after <see cref="OnDealDamage"/> in the damage
|
||||
/// pipeline.</summary>
|
||||
void OnTakeDamage(Character target, Character source, ref int damage);
|
||||
/// <summary>Fires immediately when the effect is first placed on a
|
||||
/// character. Use for one-shot stat boosts, visual feedback, or
|
||||
/// initialisation logic.</summary>
|
||||
void OnApply(Character target);
|
||||
/// <summary>Fires when the effect is manually removed (e.g. via a
|
||||
/// cleanse card). Not called on natural expiration — use
|
||||
/// <see cref="OnFinalAction"/> for that path.</summary>
|
||||
void OnRemove(Character target);
|
||||
/// <summary>Fires once when the effect expires naturally (timer reaches
|
||||
/// zero). Use for end-of-effect triggers like explosion damage or
|
||||
/// stat restoration. Not called on manual removal.</summary>
|
||||
void OnFinalAction(Character target);
|
||||
/// <summary>Passive stat modifier invoked independently of turn phases.
|
||||
/// Receives a reference to the character's attribute container;
|
||||
/// implementors can add flat or percentage bonuses that other
|
||||
/// effects may also contribute to.</summary>
|
||||
void ModifyAttributes(CharacterAttributes attributes);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,49 @@
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Cards.StatusEffect.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Stun behaviour set — intentionally all no-ops. The stun condition
|
||||
/// is checked separately via StatusEffectQueue.HasStun() rather than
|
||||
/// through action hooks, so the Actions implementation is empty.
|
||||
/// Exists as a concrete type so the factory can resolve "basic/stun"
|
||||
/// to a valid instance.
|
||||
/// </summary>
|
||||
public class StunStatusEffect : IStatusActions
|
||||
{
|
||||
/// <inheritdoc cref="IStatusActions.ID"/>
|
||||
public string ID => "basic/stun";
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTurnStart"/>
|
||||
public void OnTurnStart(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTurnEnd"/>
|
||||
public void OnTurnEnd(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnDealDamage"/>
|
||||
public void OnDealDamage(Character source, Character target, ref int damage)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTakeDamage"/>
|
||||
public void OnTakeDamage(Character target, Character source, ref int damage)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnApply"/>
|
||||
public void OnApply(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnRemove"/>
|
||||
public void OnRemove(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnFinalAction"/>
|
||||
public void OnFinalAction(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.ModifyAttributes"/>
|
||||
public void ModifyAttributes(CharacterAttributes attributes)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Cards.StatusEffect.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Stun behaviour set — intentionally all no-ops. The stun condition
|
||||
/// is checked separately via StatusEffectQueue.HasStun() rather than
|
||||
/// through action hooks, so the Actions implementation is empty.
|
||||
/// Exists as a concrete type so the factory can resolve "basic/stun"
|
||||
/// to a valid instance.
|
||||
/// </summary>
|
||||
public class StunStatusEffect : IStatusActions
|
||||
{
|
||||
/// <inheritdoc cref="IStatusActions.ID"/>
|
||||
public string ID => "basic/stun";
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTurnStart"/>
|
||||
public void OnTurnStart(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTurnEnd"/>
|
||||
public void OnTurnEnd(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnDealDamage"/>
|
||||
public void OnDealDamage(Character source, Character target, ref int damage)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTakeDamage"/>
|
||||
public void OnTakeDamage(Character target, Character source, ref int damage)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnApply"/>
|
||||
public void OnApply(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnRemove"/>
|
||||
public void OnRemove(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnFinalAction"/>
|
||||
public void OnFinalAction(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.ModifyAttributes"/>
|
||||
public void ModifyAttributes(CharacterAttributes attributes)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
using System;
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
|
||||
namespace Cthangover.Core.Factories.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Thin <c>FileFactory</c> for status effect metadata — duration, stack
|
||||
/// behaviour, visual icon, etc. Works in tandem with
|
||||
/// <c>StatusEffectActionFactory</c>: this factory provides the <b>data</b>
|
||||
/// (what the effect is), while the action factory provides the
|
||||
/// <b>behaviour</b> (what the effect does each turn). Files are discovered
|
||||
/// by the shared entity resolver through the DTO's
|
||||
/// <c>[EntityKind("status_effect")]</c> attribute.
|
||||
/// </summary>
|
||||
public class StatusEffectInfoFactory : FileFactory<StatusEffectInfo>
|
||||
{
|
||||
private static readonly Lazy<StatusEffectInfoFactory> lazy = new(() => new StatusEffectInfoFactory());
|
||||
/// <summary>Lazy singleton facade over the AppServices-owned factory.</summary>
|
||||
public static StatusEffectInfoFactory Instance => lazy.Value;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
|
||||
namespace Cthangover.Core.Factories.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Thin <c>FileFactory</c> for status effect metadata — duration, stack
|
||||
/// behaviour, visual icon, etc. Works in tandem with
|
||||
/// <c>StatusEffectActionFactory</c>: this factory provides the <b>data</b>
|
||||
/// (what the effect is), while the action factory provides the
|
||||
/// <b>behaviour</b> (what the effect does each turn). Files are discovered
|
||||
/// by the shared entity resolver through the DTO's
|
||||
/// <c>[EntityKind("status_effect")]</c> attribute.
|
||||
/// </summary>
|
||||
public class StatusEffectInfoFactory : FileFactory<StatusEffectInfo>
|
||||
{
|
||||
private static readonly Lazy<StatusEffectInfoFactory> lazy = new(() => new StatusEffectInfoFactory());
|
||||
/// <summary>Lazy singleton facade over the AppServices-owned factory.</summary>
|
||||
public static StatusEffectInfoFactory Instance => lazy.Value;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user