Commit pending working-tree changes (local edits, line endings)

This commit is contained in:
cthangover committed 2026-10-21 12:32:24 +02:00
1 parent 68976523f0
commit 10f5b3455b
27 files changed
+1677 -1677

No files matched your search

+79 -79
View File
@@ -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
View File
@@ -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();
}
}
}
+27 -27
View File
@@ -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);
}
}
+18 -18
View File
@@ -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);
}
}
+32 -32
View File
@@ -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();
}
}
+42 -42
View File
@@ -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;
}
}
}
+79 -79
View File
@@ -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();
}
}
}
+33 -33
View File
@@ -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; }
}
}
+26 -26
View File
@@ -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);
}
}
+27 -27
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
}
}
}
+360 -360
View File
@@ -1,362 +1,362 @@
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();
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();
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;
}
}
private void ClearBattle()
{
BattleActionMachine?.StopMachine();
ActiveCharacter = null;
pendingPlayerCards?.Clear();
pendingEnemyCards?.Clear();
OnBattleCleared?.Invoke();
ModEventBus.Publish(new BattleEndedEvent
{
Result = IsDestroyed ? "defeat" : "victory",
});
}
}
}
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();
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();
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;
}
}
private void ClearBattle()
{
BattleActionMachine?.StopMachine();
ActiveCharacter = null;
pendingPlayerCards?.Clear();
pendingEnemyCards?.Clear();
OnBattleCleared?.Invoke();
ModEventBus.Publish(new BattleEndedEvent
{
Result = IsDestroyed ? "defeat" : "victory",
});
}
}
}
+39 -39
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 &lt;= 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 &lt;= 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 &lt;= 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 &lt;= 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();
}
}
}
+58 -58
View File
@@ -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);
}
}
+49 -49
View File
@@ -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)
{ }
}
}
+21 -21
View File
@@ -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;
}
}
+13 -13
View File
@@ -1,14 +1,14 @@
{
"Kind": "status_effect",
"Items": [
{
"Id": "effect/physics/stun",
"Name": "status/",
"Description": "",
"Icon": "characters/effects/stun",
"Type": "Stun",
"Duration": 3,
"Actions": "basic/stun"
}
]
{
"Kind": "status_effect",
"Items": [
{
"Id": "effect/physics/stun",
"Name": "status/",
"Description": "",
"Icon": "characters/effects/stun",
"Type": "Stun",
"Duration": 3,
"Actions": "basic/stun"
}
]
}