From 10f5b3455b7ff1d6396b7955ba04d2ef06dd66a2 Mon Sep 17 00:00:00 2001 From: iisaev Date: Wed, 21 Oct 2026 12:32:03 +0200 Subject: [PATCH] Commit pending working-tree changes (local edits, line endings) --- src/Actions/ActionExecutorHub.cs | 158 ++--- src/Actions/BattleActionMachine.cs | 242 +++---- src/Actions/IActionExecutor.cs | 54 +- src/Actions/IActionExecutorProvider.cs | 36 +- src/Actions/IBattleAction.cs | 64 +- src/Adapters/AnimationRegistry.cs | 84 +-- src/Adapters/BattleContextImpl.cs | 158 ++--- src/Adapters/IBattleAnimation.cs | 66 +- src/Adapters/IBattleAnimationPlayer.cs | 52 +- src/Adapters/IBattleContext.cs | 54 +- src/Adapters/IBattleCore.cs | 92 +-- src/Adapters/IBattleSide.cs | 30 +- src/Base/ActionBase.cs | 124 ++-- src/BattleData.cs | 196 +++--- src/BattleSceneContext.cs | 720 ++++++++++----------- src/BattleSetCoreAction.cs | 78 +-- src/BattleSpeedControl.cs | 220 +++---- src/ChangedAttributes.cs | 60 +- src/CharacterSet.cs | 40 +- src/DeadgroundBehaviour.cs | 126 ++-- src/ExpReportCard.cs | 92 +-- src/LootReportItem.cs | 92 +-- src/ShowDamageBehaviour.cs | 234 +++---- src/StatusEffect/IStatusActions.cs | 116 ++-- src/StatusEffect/Impls/StunStatusEffect.cs | 98 +-- src/StatusEffectInfoFactory.cs | 42 +- status_effects/status_effects.json | 26 +- 27 files changed, 1677 insertions(+), 1677 deletions(-) diff --git a/src/Actions/ActionExecutorHub.cs b/src/Actions/ActionExecutorHub.cs index 15fa226..d7f473a 100644 --- a/src/Actions/ActionExecutorHub.cs +++ b/src/Actions/ActionExecutorHub.cs @@ -1,79 +1,79 @@ -using System.Collections.Generic; -using Cthangover.Core.Characters; - -namespace Cthangover.Core.Battle.Actions -{ - /// - /// 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). - /// - public class ActionExecutorHub - { - /// - /// Singleton hub instance. There is one global dispatcher shared - /// across all battle cores; cores swap providers via - /// when they activate. - /// - public static readonly ActionExecutorHub Instance = new(); - - private IActionExecutorProvider _activeProvider; - private readonly Dictionary _globalExecutors = new(); - - /// - /// 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. - /// - public void SetActiveProvider(IActionExecutorProvider provider) - { - _activeProvider = provider; - } - - /// - /// Registers a fallback executor in the global pool, keyed by - /// . Global executors are - /// used when no active provider supplies a match. - /// - public void RegisterGlobal(IActionExecutor executor) - { - if (executor != null && !string.IsNullOrEmpty(executor.ActionId)) - _globalExecutors[executor.ActionId] = executor; - } - - /// - /// Dispatches an action to the appropriate executor. Resolution - /// order: active provider first, then global registry. Returns - /// Result = false if no executor is found or if - /// is null. - /// - 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 +{ + /// + /// 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). + /// + public class ActionExecutorHub + { + /// + /// Singleton hub instance. There is one global dispatcher shared + /// across all battle cores; cores swap providers via + /// when they activate. + /// + public static readonly ActionExecutorHub Instance = new(); + + private IActionExecutorProvider _activeProvider; + private readonly Dictionary _globalExecutors = new(); + + /// + /// 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. + /// + public void SetActiveProvider(IActionExecutorProvider provider) + { + _activeProvider = provider; + } + + /// + /// Registers a fallback executor in the global pool, keyed by + /// . Global executors are + /// used when no active provider supplies a match. + /// + public void RegisterGlobal(IActionExecutor executor) + { + if (executor != null && !string.IsNullOrEmpty(executor.ActionId)) + _globalExecutors[executor.ActionId] = executor; + } + + /// + /// Dispatches an action to the appropriate executor. Resolution + /// order: active provider first, then global registry. Returns + /// Result = false if no executor is found or if + /// is null. + /// + 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; + } + } +} diff --git a/src/Actions/BattleActionMachine.cs b/src/Actions/BattleActionMachine.cs index d20e2ab..dbac98a 100644 --- a/src/Actions/BattleActionMachine.cs +++ b/src/Actions/BattleActionMachine.cs @@ -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 -{ - - /// - /// 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. - /// - /// Derives from 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). - /// - public partial class BattleActionMachine : ModNode - { - - /// - /// 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. - /// - public List Actions { get; set; } = new(); - private IBattleAction CurrentAction { get; set; } - private bool _hadActions; - - /// - /// Fired when the machine drains naturally or is explicitly stopped. - /// Battle cores subscribe to advance the turn phase without polling. - /// - public event StopMachineDelegate OnStopMachine; - - /// Stops the machine and fires when the node leaves the tree, so subscribers never wait on a dead node. - protected override void OnExitTree() - { - base.OnExitTree(); - StopMachine(); - } - - /// Drives the queued actions once per frame; fires when the queue drains. - 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; - } - } - - /// - /// Enqueues a single action. If the machine is idle (no current - /// action), processing starts on the next _Process tick. - /// - public void AddAction(IBattleAction action) - { - if (action == null) - return; - Actions.Add(action); - } - - /// - /// Batch-enqueues multiple actions. All are appended to the existing - /// queue and processed in FIFO order on subsequent frames. - /// - public void AddActions(List actions) - { - if(actions == null || actions.Count == 0) - return; - Actions.AddRange(actions); - } - - /// - /// Immediately clears the queue, aborts the current action, and - /// fires . Called on tree exit and - /// when the queue drains naturally. - /// - public void StopMachine() - { - CurrentAction = null; - Actions.Clear(); - _hadActions = false; - OnStopMachine?.Invoke(); - } - - } - -} +namespace Cthangover.Core.Battle.Actions +{ + + /// + /// 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. + /// + /// Derives from 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). + /// + public partial class BattleActionMachine : ModNode + { + + /// + /// 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. + /// + public List Actions { get; set; } = new(); + private IBattleAction CurrentAction { get; set; } + private bool _hadActions; + + /// + /// Fired when the machine drains naturally or is explicitly stopped. + /// Battle cores subscribe to advance the turn phase without polling. + /// + public event StopMachineDelegate OnStopMachine; + + /// Stops the machine and fires when the node leaves the tree, so subscribers never wait on a dead node. + protected override void OnExitTree() + { + base.OnExitTree(); + StopMachine(); + } + + /// Drives the queued actions once per frame; fires when the queue drains. + 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; + } + } + + /// + /// Enqueues a single action. If the machine is idle (no current + /// action), processing starts on the next _Process tick. + /// + public void AddAction(IBattleAction action) + { + if (action == null) + return; + Actions.Add(action); + } + + /// + /// Batch-enqueues multiple actions. All are appended to the existing + /// queue and processed in FIFO order on subsequent frames. + /// + public void AddActions(List actions) + { + if(actions == null || actions.Count == 0) + return; + Actions.AddRange(actions); + } + + /// + /// Immediately clears the queue, aborts the current action, and + /// fires . Called on tree exit and + /// when the queue drains naturally. + /// + public void StopMachine() + { + CurrentAction = null; + Actions.Clear(); + _hadActions = false; + OnStopMachine?.Invoke(); + } + + } + +} diff --git a/src/Actions/IActionExecutor.cs b/src/Actions/IActionExecutor.cs index 6c4ef98..4ef1413 100644 --- a/src/Actions/IActionExecutor.cs +++ b/src/Actions/IActionExecutor.cs @@ -1,27 +1,27 @@ -using Cthangover.Core.Characters; - -namespace Cthangover.Core.Battle.Actions -{ - /// - /// 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. - /// - public interface IActionExecutor - { - /// - /// String key that matches . - /// The hub uses this to resolve executors without knowing - /// concrete types. - /// - string ActionId { get; } - - /// - /// Applies the action's effects. Receives the action definition - /// and the user/target instances; returns - /// with deltas and success/failure. - /// - ChangedAttributes Execute(ActionCharacter action, Character user, Character target); - } -} +using Cthangover.Core.Characters; + +namespace Cthangover.Core.Battle.Actions +{ + /// + /// 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. + /// + public interface IActionExecutor + { + /// + /// String key that matches . + /// The hub uses this to resolve executors without knowing + /// concrete types. + /// + string ActionId { get; } + + /// + /// Applies the action's effects. Receives the action definition + /// and the user/target instances; returns + /// with deltas and success/failure. + /// + ChangedAttributes Execute(ActionCharacter action, Character user, Character target); + } +} diff --git a/src/Actions/IActionExecutorProvider.cs b/src/Actions/IActionExecutorProvider.cs index c2d238b..71acb13 100644 --- a/src/Actions/IActionExecutorProvider.cs +++ b/src/Actions/IActionExecutorProvider.cs @@ -1,18 +1,18 @@ -namespace Cthangover.Core.Battle.Actions -{ - /// - /// Typed executor source. Each IBattleCore returns its own provider, - /// giving cores the ability to supply custom executor sets without - /// polluting the global registry. - /// - public interface IActionExecutorProvider - { - /// - /// Returns the executor registered for , - /// or null if no override is configured. Called by - /// before falling back to the - /// global registry. - /// - IActionExecutor GetExecutor(string actionId); - } -} +namespace Cthangover.Core.Battle.Actions +{ + /// + /// Typed executor source. Each IBattleCore returns its own provider, + /// giving cores the ability to supply custom executor sets without + /// polluting the global registry. + /// + public interface IActionExecutorProvider + { + /// + /// Returns the executor registered for , + /// or null if no override is configured. Called by + /// before falling back to the + /// global registry. + /// + IActionExecutor GetExecutor(string actionId); + } +} diff --git a/src/Actions/IBattleAction.cs b/src/Actions/IBattleAction.cs index 090c9b7..6fee639 100644 --- a/src/Actions/IBattleAction.cs +++ b/src/Actions/IBattleAction.cs @@ -1,32 +1,32 @@ -namespace Cthangover.Core.Battle.Actions -{ - /// - /// 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. - /// - public interface IBattleAction - { - /// - /// Called once when the action is dequeued and begins execution. - /// Use for setup: spawning nodes, caching references, starting - /// tweens or coroutines. - /// - void DoStart(); - /// - /// Polled every frame. Returns true when the action has - /// finished its work; returns false to remain active. - /// This model eliminates the need for the machine to manage timers. - /// - bool DoAction(); - /// - /// Cleanup callback fired after returns - /// true. Use for teardown: freeing nodes, resetting state, - /// applying final stat deltas. - /// - void DoEnd(); - } -} +namespace Cthangover.Core.Battle.Actions +{ + /// + /// 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. + /// + public interface IBattleAction + { + /// + /// Called once when the action is dequeued and begins execution. + /// Use for setup: spawning nodes, caching references, starting + /// tweens or coroutines. + /// + void DoStart(); + /// + /// Polled every frame. Returns true when the action has + /// finished its work; returns false to remain active. + /// This model eliminates the need for the machine to manage timers. + /// + bool DoAction(); + /// + /// Cleanup callback fired after returns + /// true. Use for teardown: freeing nodes, resetting state, + /// applying final stat deltas. + /// + void DoEnd(); + } +} diff --git a/src/Adapters/AnimationRegistry.cs b/src/Adapters/AnimationRegistry.cs index 759aa44..019013d 100644 --- a/src/Adapters/AnimationRegistry.cs +++ b/src/Adapters/AnimationRegistry.cs @@ -1,42 +1,42 @@ -using System.Collections.Generic; - -namespace Cthangover.Core.Battle -{ - /// - /// 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. - /// - public class AnimationRegistry - { - /// - /// Singleton registry. One set of animation players serves the - /// entire runtime; individual players dispatch per-core via - /// internal logic. - /// - public static readonly AnimationRegistry Instance = new(); - - private readonly Dictionary _players = new(); - - /// - /// Registers a player under . - /// Replaces any existing player for the same key — last write wins. - /// - public void Register(string animType, IBattleAnimationPlayer player) - { - _players[animType] = player; - } - - /// - /// Retrieves the player registered for , - /// or null if no match exists. - /// - public IBattleAnimationPlayer Get(string animType) - { - _players.TryGetValue(animType, out var player); - return player; - } - } -} +using System.Collections.Generic; + +namespace Cthangover.Core.Battle +{ + /// + /// 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. + /// + public class AnimationRegistry + { + /// + /// Singleton registry. One set of animation players serves the + /// entire runtime; individual players dispatch per-core via + /// internal logic. + /// + public static readonly AnimationRegistry Instance = new(); + + private readonly Dictionary _players = new(); + + /// + /// Registers a player under . + /// Replaces any existing player for the same key — last write wins. + /// + public void Register(string animType, IBattleAnimationPlayer player) + { + _players[animType] = player; + } + + /// + /// Retrieves the player registered for , + /// or null if no match exists. + /// + public IBattleAnimationPlayer Get(string animType) + { + _players.TryGetValue(animType, out var player); + return player; + } + } +} diff --git a/src/Adapters/BattleContextImpl.cs b/src/Adapters/BattleContextImpl.cs index 19d4369..5b32fc8 100644 --- a/src/Adapters/BattleContextImpl.cs +++ b/src/Adapters/BattleContextImpl.cs @@ -1,79 +1,79 @@ -using Cthangover.Core.Characters; -using Godot; - -namespace Cthangover.Core.Battle -{ - /// - /// 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. - /// - public class BattleContextImpl : IBattleContext - { - private readonly BattleSceneContext _sceneContext; - - /// - /// Root Godot of the battle scene, used by - /// animation actions to parent spawned visual effects. - /// - public Node RootNode => _sceneContext; - - /// - /// The owning that manages - /// the battle lifecycle. - /// - public BattleSceneContext SceneContext => _sceneContext; - - /// - /// Creates a context adapter around the given scene context. - /// - public BattleContextImpl(BattleSceneContext sceneContext) - { - _sceneContext = sceneContext; - } - - /// - /// Subtracts from - /// , clamping at 0 to prevent negative - /// health from defence overflow, and applies the result to - /// 's health. - /// - public void ApplyDamage(Character source, Character target, int rawDamage, int defence) - { - var finalDamage = Mathf.Max(0, rawDamage - defence); - target.Attributes.Health.Value -= finalDamage; - } - - /// - /// Stub — specific battle cores override this to enqueue - /// animation actions into the . - /// - public void ScheduleAnimation(IBattleAnimation animation) - { - } - - /// - /// Stub — cores implement this to advance the turn sequence - /// when a side finishes its action phase. - /// - public void EndTurn(BattleSide side) - { - } - - /// - /// Resolves the battle outcome by delegating to - /// for player - /// victory or - /// for enemy victory. - /// - public void EndBattle(BattleSide winner) - { - if (winner == BattleSide.Player) - _sceneContext.ShowWinground(); - else - _sceneContext.ShowDeadground(); - } - } -} +using Cthangover.Core.Characters; +using Godot; + +namespace Cthangover.Core.Battle +{ + /// + /// 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. + /// + public class BattleContextImpl : IBattleContext + { + private readonly BattleSceneContext _sceneContext; + + /// + /// Root Godot of the battle scene, used by + /// animation actions to parent spawned visual effects. + /// + public Node RootNode => _sceneContext; + + /// + /// The owning that manages + /// the battle lifecycle. + /// + public BattleSceneContext SceneContext => _sceneContext; + + /// + /// Creates a context adapter around the given scene context. + /// + public BattleContextImpl(BattleSceneContext sceneContext) + { + _sceneContext = sceneContext; + } + + /// + /// Subtracts from + /// , clamping at 0 to prevent negative + /// health from defence overflow, and applies the result to + /// 's health. + /// + public void ApplyDamage(Character source, Character target, int rawDamage, int defence) + { + var finalDamage = Mathf.Max(0, rawDamage - defence); + target.Attributes.Health.Value -= finalDamage; + } + + /// + /// Stub — specific battle cores override this to enqueue + /// animation actions into the . + /// + public void ScheduleAnimation(IBattleAnimation animation) + { + } + + /// + /// Stub — cores implement this to advance the turn sequence + /// when a side finishes its action phase. + /// + public void EndTurn(BattleSide side) + { + } + + /// + /// Resolves the battle outcome by delegating to + /// for player + /// victory or + /// for enemy victory. + /// + public void EndBattle(BattleSide winner) + { + if (winner == BattleSide.Player) + _sceneContext.ShowWinground(); + else + _sceneContext.ShowDeadground(); + } + } +} diff --git a/src/Adapters/IBattleAnimation.cs b/src/Adapters/IBattleAnimation.cs index aa38d21..7771549 100644 --- a/src/Adapters/IBattleAnimation.cs +++ b/src/Adapters/IBattleAnimation.cs @@ -1,33 +1,33 @@ -using System.Collections.Generic; -using Cthangover.Core.Characters; - -namespace Cthangover.Core.Battle -{ - /// - /// 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. - /// - public interface IBattleAnimation - { - /// The performing the action. - Character Source { get; } - /// The receiving the action. - Character Target { get; } - /// - /// Key used by to dispatch to the - /// correct . - /// - string AnimationType { get; } - /// Multiplier for the playback speed (1.0 = normal). - float Speed { get; } - /// - /// Open-ended data bag for animation-specific values - /// (e.g. damage amount, elemental flag) — no interface changes - /// needed when adding new animation types. - /// - Dictionary Parameters { get; } - } -} +using System.Collections.Generic; +using Cthangover.Core.Characters; + +namespace Cthangover.Core.Battle +{ + /// + /// 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. + /// + public interface IBattleAnimation + { + /// The performing the action. + Character Source { get; } + /// The receiving the action. + Character Target { get; } + /// + /// Key used by to dispatch to the + /// correct . + /// + string AnimationType { get; } + /// Multiplier for the playback speed (1.0 = normal). + float Speed { get; } + /// + /// Open-ended data bag for animation-specific values + /// (e.g. damage amount, elemental flag) — no interface changes + /// needed when adding new animation types. + /// + Dictionary Parameters { get; } + } +} diff --git a/src/Adapters/IBattleAnimationPlayer.cs b/src/Adapters/IBattleAnimationPlayer.cs index d903399..73b947c 100644 --- a/src/Adapters/IBattleAnimationPlayer.cs +++ b/src/Adapters/IBattleAnimationPlayer.cs @@ -1,26 +1,26 @@ -using System; - -namespace Cthangover.Core.Battle -{ - /// - /// 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. - /// - public interface IBattleAnimationPlayer - { - /// - /// String key used by to select - /// this player for a given . - /// - string AnimationType { get; } - - /// - /// Starts playing the animation described by . - /// Must invoke once the animation - /// finishes, so the action machine can advance to the next step. - /// - void Play(IBattleAnimation anim, Action onComplete); - } -} +using System; + +namespace Cthangover.Core.Battle +{ + /// + /// 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. + /// + public interface IBattleAnimationPlayer + { + /// + /// String key used by to select + /// this player for a given . + /// + string AnimationType { get; } + + /// + /// Starts playing the animation described by . + /// Must invoke once the animation + /// finishes, so the action machine can advance to the next step. + /// + void Play(IBattleAnimation anim, Action onComplete); + } +} diff --git a/src/Adapters/IBattleContext.cs b/src/Adapters/IBattleContext.cs index 2d0191c..b7e340d 100644 --- a/src/Adapters/IBattleContext.cs +++ b/src/Adapters/IBattleContext.cs @@ -1,27 +1,27 @@ -using Cthangover.Core.Characters; -using Godot; - -namespace Cthangover.Core.Battle -{ - /// - /// 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). - /// - 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 +{ + /// + /// 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). + /// + 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; } + } +} diff --git a/src/Adapters/IBattleCore.cs b/src/Adapters/IBattleCore.cs index 53a62bc..f5522ce 100644 --- a/src/Adapters/IBattleCore.cs +++ b/src/Adapters/IBattleCore.cs @@ -1,46 +1,46 @@ -using Cthangover.Core.Battle.Actions; -using Cthangover.Core.Characters; - -namespace Cthangover.Core.Battle -{ - /// - /// 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. - /// - public interface IBattleCore - { - /// - /// Unique identifier for this battle engine (e.g. "card_battle", - /// "ff_battle"). Used by and - /// for lookup. - /// - string Id { get; } - - /// - /// Provider that returns core-specific - /// overrides. Set on the when - /// this core activates, so actions route through custom logic - /// before falling back to the global registry. - /// - IActionExecutorProvider ActionProvider { get; } - - /// - /// Initialises the core with the two character arrays and a - /// for callbacks. Called once per - /// battle before . - /// - void Init(Character[] playerChars, Character[] enemyChars, IBattleContext ctx); - - /// - /// Begins the turn loop. Cores may use coroutines, polling, or - /// event-driven state machines — the core owns its own lifecycle - /// after this call. - /// - void Start(); - } -} +using Cthangover.Core.Battle.Actions; +using Cthangover.Core.Characters; + +namespace Cthangover.Core.Battle +{ + /// + /// 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. + /// + public interface IBattleCore + { + /// + /// Unique identifier for this battle engine (e.g. "card_battle", + /// "ff_battle"). Used by and + /// for lookup. + /// + string Id { get; } + + /// + /// Provider that returns core-specific + /// overrides. Set on the when + /// this core activates, so actions route through custom logic + /// before falling back to the global registry. + /// + IActionExecutorProvider ActionProvider { get; } + + /// + /// Initialises the core with the two character arrays and a + /// for callbacks. Called once per + /// battle before . + /// + void Init(Character[] playerChars, Character[] enemyChars, IBattleContext ctx); + + /// + /// Begins the turn loop. Cores may use coroutines, polling, or + /// event-driven state machines — the core owns its own lifecycle + /// after this call. + /// + void Start(); + } +} diff --git a/src/Adapters/IBattleSide.cs b/src/Adapters/IBattleSide.cs index e92f70e..11520b4 100644 --- a/src/Adapters/IBattleSide.cs +++ b/src/Adapters/IBattleSide.cs @@ -1,15 +1,15 @@ -namespace Cthangover.Core.Battle -{ - /// - /// 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). - /// - public enum BattleSide - { - /// The player's party. - Player, - /// The opposing force (monsters, NPCs). - Enemy, - } -} +namespace Cthangover.Core.Battle +{ + /// + /// 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). + /// + public enum BattleSide + { + /// The player's party. + Player, + /// The opposing force (monsters, NPCs). + Enemy, + } +} diff --git a/src/Base/ActionBase.cs b/src/Base/ActionBase.cs index b6f5557..dbd0c47 100644 --- a/src/Base/ActionBase.cs +++ b/src/Base/ActionBase.cs @@ -1,62 +1,62 @@ - -using Cthangover.Core.Battle.Actions; -using Cthangover.Core.Utils; - -namespace Cthangover.Core.Characters -{ - - /// - /// 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. - /// - public abstract class ActionBase : IActionExecutor - { - - /// - /// Maps to the abstract so the hub can resolve - /// this executor by action name. - /// - public string ActionId => ID; - - /// - /// Unique action identifier that matches - /// in data. - /// - public abstract string ID { get; } - /// - /// Applies the action's effects to - /// using 's stats and the - /// definition. Returns stat deltas - /// and a success/failure flag. - /// - public abstract ChangedAttributes Execute(ActionCharacter action, Character user, Character target); - - /// - /// Validates that has at least - /// "RequiredPoint" remaining, deducts it, and - /// returns true. Returns false with a warning log - /// if the user lacks sufficient points, allowing the action to - /// fail gracefully. - /// - 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 +{ + + /// + /// 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. + /// + public abstract class ActionBase : IActionExecutor + { + + /// + /// Maps to the abstract so the hub can resolve + /// this executor by action name. + /// + public string ActionId => ID; + + /// + /// Unique action identifier that matches + /// in data. + /// + public abstract string ID { get; } + /// + /// Applies the action's effects to + /// using 's stats and the + /// definition. Returns stat deltas + /// and a success/failure flag. + /// + public abstract ChangedAttributes Execute(ActionCharacter action, Character user, Character target); + + /// + /// Validates that has at least + /// "RequiredPoint" remaining, deducts it, and + /// returns true. Returns false with a warning log + /// if the user lacks sufficient points, allowing the action to + /// fail gracefully. + /// + 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; + } + + } + +} diff --git a/src/BattleData.cs b/src/BattleData.cs index acf1c98..2a451bc 100644 --- a/src/BattleData.cs +++ b/src/BattleData.cs @@ -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 -{ - /// - /// Immutable descriptor for a battle encounter, constructed via the - /// static factory 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. - /// - public class BattleData - { - /// The quest that triggered this battle; may be null - /// for non-quest skirmishes. - public QuestBase Quest { get; set; } - /// Auxiliary tag set on new characters acquired via recruitment - /// after this battle. - public string NewTag { get; set; } - - /// List of enemy instances spawned from - /// the provided IDs or default templates. - public List EnemiesCards { get; private set; } - /// Battle background texture (full-resolution). - public Texture2D Background { get; private set; } - /// Depth map texture for the parallax post-process effect. - public Texture2D DepthMap { get; set; } - /// Albedo/colour map texture for the parallax post-process effect. - public Texture2D AlbedoMap { get; set; } - /// Which side of the screen the player's cards are drawn on. - public BattleSide BattleSide { get; private set; } = BattleSide.Player; - /// Identifier for the active battle core logic module. - public string ActiveBattleCore { get; set; } - /// Scene name to transition to when the battle finishes or is - /// fled. Stored as a string to avoid type coupling. - public string ReturnScene { get; private set; } - - private BattleData() - { } - - /// - /// Convenience overload that defaults to - /// BattleSide.Player. Constructs enemies from their string IDs - /// via , - /// falling back to a level-1 template when the ID is unknown. - /// - public static BattleData InitBattle(Texture2D background, string returnScene, params string[] enemies) - { - return InitBattle(background, returnScene, BattleSide.Player, enemies); - } - - /// - /// Creates a battle with a specified and - /// enemy IDs. Each ID is resolved through - /// . - /// Unknown IDs produce a fallback with 20 HP - /// and 3 action points. - /// - 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); - } - - /// - /// Core factory that directly accepts a pre-built - /// for the enemy roster. This is the - /// overload that all other InitBattle variants delegate to. - /// - public static BattleData InitBattle(Texture2D background, string returnScene, List 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 +{ + /// + /// Immutable descriptor for a battle encounter, constructed via the + /// static factory 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. + /// + public class BattleData + { + /// The quest that triggered this battle; may be null + /// for non-quest skirmishes. + public QuestBase Quest { get; set; } + /// Auxiliary tag set on new characters acquired via recruitment + /// after this battle. + public string NewTag { get; set; } + + /// List of enemy instances spawned from + /// the provided IDs or default templates. + public List EnemiesCards { get; private set; } + /// Battle background texture (full-resolution). + public Texture2D Background { get; private set; } + /// Depth map texture for the parallax post-process effect. + public Texture2D DepthMap { get; set; } + /// Albedo/colour map texture for the parallax post-process effect. + public Texture2D AlbedoMap { get; set; } + /// Which side of the screen the player's cards are drawn on. + public BattleSide BattleSide { get; private set; } = BattleSide.Player; + /// Identifier for the active battle core logic module. + public string ActiveBattleCore { get; set; } + /// Scene name to transition to when the battle finishes or is + /// fled. Stored as a string to avoid type coupling. + public string ReturnScene { get; private set; } + + private BattleData() + { } + + /// + /// Convenience overload that defaults to + /// BattleSide.Player. Constructs enemies from their string IDs + /// via , + /// falling back to a level-1 template when the ID is unknown. + /// + public static BattleData InitBattle(Texture2D background, string returnScene, params string[] enemies) + { + return InitBattle(background, returnScene, BattleSide.Player, enemies); + } + + /// + /// Creates a battle with a specified and + /// enemy IDs. Each ID is resolved through + /// . + /// Unknown IDs produce a fallback with 20 HP + /// and 3 action points. + /// + 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); + } + + /// + /// Core factory that directly accepts a pre-built + /// for the enemy roster. This is the + /// overload that all other InitBattle variants delegate to. + /// + public static BattleData InitBattle(Texture2D background, string returnScene, List enemiesCards, BattleSide cardOrder = BattleSide.Player) + { + return new BattleData + { + Background = background, + ReturnScene = returnScene, + EnemiesCards = enemiesCards, + BattleSide = cardOrder, + }; + } + } +} diff --git a/src/BattleSceneContext.cs b/src/BattleSceneContext.cs index 82d88a8..3d793a5 100644 --- a/src/BattleSceneContext.cs +++ b/src/BattleSceneContext.cs @@ -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 -{ - /// - /// 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 , resolves the active - /// via , 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. - /// - /// The context derives from because mod assemblies get - /// no Godot source generation: the core base receives the engine lifecycle - /// callbacks and forwards them to the OnXxx overrides. It also - /// implements so core UI (the inventory - /// panel's action-point row) can read the acting character without knowing - /// anything about battle. - /// - public partial class BattleSceneContext : ModNode, IActiveCharacterProvider - { - /// - /// Singleton reference to the active battle scene context. - /// Set on enter-tree, cleared on exit-tree. - /// External systems read this to access battle state. - /// - public static BattleSceneContext Instance { get; private set; } - - private TextureRect battleBg; - - /// - /// The node that - /// processes the animation queue for this battle. Cores push - /// animation actions through the context and the machine drives - /// playback frame-by-frame. - /// - public Actions.BattleActionMachine BattleActionMachine { get; private set; } - - private IBattleSpeedControl _battleSpeedControl; - private Widget deadground; - private Widget winground; - - /// - /// Per-character EXP earned during this battle, keyed by - /// . Populated by - /// ; consumed by the victory screen - /// provided by the turn-battle GUI mod. - /// - public Dictionary PlayerExpGained { get; private set; } = new(); - /// - /// Enemies defeated during this battle (for loot generation - /// and recruitment rolls). - /// - public List DefeatedEnemies { get; private set; } = new(); - - /// - /// Guards against double-triggering end-of-battle paths. - /// Set to true by or - /// . - /// - public bool IsDestroyed { get; private set; } - - /// - /// When true, the battle core pauses processing (e.g. - /// while a UI overlay is open). Set externally by pause widgets. - /// - public bool IsWait { get; set; } - - /// - /// The 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. - /// null 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. - /// - public Character ActiveCharacter { get; set; } - - /// Fired when a character's health reaches 0. - public event System.Action OnCharacterDied; - /// - /// Fired after battle state is cleared (both victory and defeat). - /// External systems can hook this to release battle resources. - /// - public event System.Action OnBattleCleared; - - /// - /// Invokes to notify listeners - /// (quests, achievements, the battle core) that a character - /// has been defeated. - /// - public void NotifyCharacterDied(Character character) - { - OnCharacterDied?.Invoke(character); - ModEventBus.Publish(new CharacterDiedEvent - { - CharacterId = character?.ID, - CharacterName = character?.Name, - Source = "battle", - }); - } - - /// Claims the scene singleton and registers as the provider. - 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; - } - - /// Releases the singleton slot and the active-character provider so a later scene can claim them. - protected override void OnExitTree() - { - base.OnExitTree(); - if (Instance == this) - Instance = null; - if (ReferenceEquals(ActiveCharacterRegistry.Provider, this)) - ActiveCharacterRegistry.Provider = null; - } - - private List pendingPlayerCards; - private List pendingEnemyCards; - private IBattleCore activeBattleCore; - - /// - /// The battle core driving this scene, resolved from - /// when the scene becomes ready. Exposed - /// so tooling and tests can query the active core (for example for - /// ) without reaching into UI nodes. - /// - public IBattleCore ActiveBattleCore => activeBattleCore; - - /// Caches the battle backdrop node, resolves the active and starts the battle. - protected override void OnReady() - { - battleBg = GetNodeOrNull("BattleBg"); - - if (battleBg == null) - { - GameLogger.Log("BATTLE", "battleBg not found!", LogLevel.Error); - return; - } - - BattleActionMachine = GetNodeOrNull("ToolPanel/BattleActionMachine"); - _battleSpeedControl = GetNodeOrNull("BattleSpeedControl") as IBattleSpeedControl; - deadground = GetNodeOrNull("Deadground"); - - winground = GetNodeOrNull("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("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(); - 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(); - - 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(), - }); - } - - /// - /// Triggers the defeat screen: sets , - /// resets battle speed, clears battle state, and shows the - /// deadground widget. - /// - public void ShowDeadground() - { - if (IsDestroyed) - return; - IsDestroyed = true; - _battleSpeedControl?.ResetToNormal(); - ClearBattle(); - deadground?.Show(); - } - - /// - /// Triggers the victory screen: sets , - /// resets battle speed, clears battle state, and shows the - /// winground widget. - /// - public void ShowWinground() - { - if (IsDestroyed) - return; - IsDestroyed = true; - _battleSpeedControl?.ResetToNormal(); - ClearBattle(); - winground?.Show(); - } - - /// - /// Records a defeated enemy: adds it to - /// and distributes its EXP - /// (Level * Exp) equally among all surviving player - /// characters in . - /// - 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 +{ + /// + /// 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 , resolves the active + /// via , 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. + /// + /// The context derives from because mod assemblies get + /// no Godot source generation: the core base receives the engine lifecycle + /// callbacks and forwards them to the OnXxx overrides. It also + /// implements so core UI (the inventory + /// panel's action-point row) can read the acting character without knowing + /// anything about battle. + /// + public partial class BattleSceneContext : ModNode, IActiveCharacterProvider + { + /// + /// Singleton reference to the active battle scene context. + /// Set on enter-tree, cleared on exit-tree. + /// External systems read this to access battle state. + /// + public static BattleSceneContext Instance { get; private set; } + + private TextureRect battleBg; + + /// + /// The node that + /// processes the animation queue for this battle. Cores push + /// animation actions through the context and the machine drives + /// playback frame-by-frame. + /// + public Actions.BattleActionMachine BattleActionMachine { get; private set; } + + private IBattleSpeedControl _battleSpeedControl; + private Widget deadground; + private Widget winground; + + /// + /// Per-character EXP earned during this battle, keyed by + /// . Populated by + /// ; consumed by the victory screen + /// provided by the turn-battle GUI mod. + /// + public Dictionary PlayerExpGained { get; private set; } = new(); + /// + /// Enemies defeated during this battle (for loot generation + /// and recruitment rolls). + /// + public List DefeatedEnemies { get; private set; } = new(); + + /// + /// Guards against double-triggering end-of-battle paths. + /// Set to true by or + /// . + /// + public bool IsDestroyed { get; private set; } + + /// + /// When true, the battle core pauses processing (e.g. + /// while a UI overlay is open). Set externally by pause widgets. + /// + public bool IsWait { get; set; } + + /// + /// The 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. + /// null 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. + /// + public Character ActiveCharacter { get; set; } + + /// Fired when a character's health reaches 0. + public event System.Action OnCharacterDied; + /// + /// Fired after battle state is cleared (both victory and defeat). + /// External systems can hook this to release battle resources. + /// + public event System.Action OnBattleCleared; + + /// + /// Invokes to notify listeners + /// (quests, achievements, the battle core) that a character + /// has been defeated. + /// + public void NotifyCharacterDied(Character character) + { + OnCharacterDied?.Invoke(character); + ModEventBus.Publish(new CharacterDiedEvent + { + CharacterId = character?.ID, + CharacterName = character?.Name, + Source = "battle", + }); + } + + /// Claims the scene singleton and registers as the provider. + 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; + } + + /// Releases the singleton slot and the active-character provider so a later scene can claim them. + protected override void OnExitTree() + { + base.OnExitTree(); + if (Instance == this) + Instance = null; + if (ReferenceEquals(ActiveCharacterRegistry.Provider, this)) + ActiveCharacterRegistry.Provider = null; + } + + private List pendingPlayerCards; + private List pendingEnemyCards; + private IBattleCore activeBattleCore; + + /// + /// The battle core driving this scene, resolved from + /// when the scene becomes ready. Exposed + /// so tooling and tests can query the active core (for example for + /// ) without reaching into UI nodes. + /// + public IBattleCore ActiveBattleCore => activeBattleCore; + + /// Caches the battle backdrop node, resolves the active and starts the battle. + protected override void OnReady() + { + battleBg = GetNodeOrNull("BattleBg"); + + if (battleBg == null) + { + GameLogger.Log("BATTLE", "battleBg not found!", LogLevel.Error); + return; + } + + BattleActionMachine = GetNodeOrNull("ToolPanel/BattleActionMachine"); + _battleSpeedControl = GetNodeOrNull("BattleSpeedControl") as IBattleSpeedControl; + deadground = GetNodeOrNull("Deadground"); + + winground = GetNodeOrNull("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("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(); + 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(); + + 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(), + }); + } + + /// + /// Triggers the defeat screen: sets , + /// resets battle speed, clears battle state, and shows the + /// deadground widget. + /// + public void ShowDeadground() + { + if (IsDestroyed) + return; + IsDestroyed = true; + _battleSpeedControl?.ResetToNormal(); + ClearBattle(); + deadground?.Show(); + } + + /// + /// Triggers the victory screen: sets , + /// resets battle speed, clears battle state, and shows the + /// winground widget. + /// + public void ShowWinground() + { + if (IsDestroyed) + return; + IsDestroyed = true; + _battleSpeedControl?.ResetToNormal(); + ClearBattle(); + winground?.Show(); + } + + /// + /// Records a defeated enemy: adds it to + /// and distributes its EXP + /// (Level * Exp) equally among all surviving player + /// characters in . + /// + 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", + }); + } + } +} diff --git a/src/BattleSetCoreAction.cs b/src/BattleSetCoreAction.cs index ec35f4a..d3392a7 100644 --- a/src/BattleSetCoreAction.cs +++ b/src/BattleSetCoreAction.cs @@ -1,40 +1,40 @@ -using Cthangover.Core.Actions; - -namespace Cthangover.Core.Battle -{ - /// - /// Selects which battle core (combat ruleset) the next battle will use. - /// The "core" variable maps to a entry by - /// string ID. Must be called before a battle starts, as - /// reads the active core at initialization time. - /// - public class BattleSetCoreAction : IScenarioAction - { - /// - /// 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. - /// - public string Name => "battle.set_core"; - - /// - /// Reads the "core" variable and sets it as the active battle core - /// via . The core determines - /// combat mechanics (turn order, ability system, etc.) for the - /// next initiated battle. - /// - 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 +{ + /// + /// Selects which battle core (combat ruleset) the next battle will use. + /// The "core" variable maps to a entry by + /// string ID. Must be called before a battle starts, as + /// reads the active core at initialization time. + /// + public class BattleSetCoreAction : IScenarioAction + { + /// + /// 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. + /// + public string Name => "battle.set_core"; + + /// + /// Reads the "core" variable and sets it as the active battle core + /// via . The core determines + /// combat mechanics (turn order, ability system, etc.) for the + /// next initiated battle. + /// + 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}'"); + } + } +} diff --git a/src/BattleSpeedControl.cs b/src/BattleSpeedControl.cs index 7cbd738..73327e9 100644 --- a/src/BattleSpeedControl.cs +++ b/src/BattleSpeedControl.cs @@ -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 -{ - /// - /// 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. restores 1x - /// without saving and is the part the core scene consumes through - /// when battle ends. - /// - public partial class BattleSpeedControl : Control, IBattleSpeedControl - { - private readonly Dictionary _buttons = new(); - private int _activeSpeed; - - /// - /// Builds the speed bar in the constructor. Godot never runs a - /// _Ready 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. - /// - 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(); - } - - /// - /// Restores the speed from . - /// Called on _Ready to reapply the saved preference. - /// - public void ApplySavedSpeed() - { - var speed = GameData.Instance.Settings.BattleSpeed; - if (speed < 1 || speed > 8) - speed = 1; - - _activeSpeed = speed; - Godot.Engine.TimeScale = speed; - UpdateHighlight(); - } - - /// - /// Resets 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. - /// - 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 +{ + /// + /// 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. restores 1x + /// without saving and is the part the core scene consumes through + /// when battle ends. + /// + public partial class BattleSpeedControl : Control, IBattleSpeedControl + { + private readonly Dictionary _buttons = new(); + private int _activeSpeed; + + /// + /// Builds the speed bar in the constructor. Godot never runs a + /// _Ready 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. + /// + 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(); + } + + /// + /// Restores the speed from . + /// Called on _Ready to reapply the saved preference. + /// + public void ApplySavedSpeed() + { + var speed = GameData.Instance.Settings.BattleSpeed; + if (speed < 1 || speed > 8) + speed = 1; + + _activeSpeed = speed; + Godot.Engine.TimeScale = speed; + UpdateHighlight(); + } + + /// + /// Resets 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. + /// + 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; + } + } + } +} diff --git a/src/ChangedAttributes.cs b/src/ChangedAttributes.cs index 57175f4..ad751d1 100644 --- a/src/ChangedAttributes.cs +++ b/src/ChangedAttributes.cs @@ -1,30 +1,30 @@ -namespace Cthangover.Core.Characters -{ - /// - /// 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. - /// - public struct ChangedAttributes - { - /// - /// Snapshot of the action source's damage and defence after the - /// exchange. - /// - public CharacterSet Source; - /// - /// Snapshot of the action target's damage and defence after the - /// exchange. - /// - public CharacterSet Target; - /// - /// Whether the action succeeded (e.g. hit landed vs. missed or - /// blocked). - /// - public bool Result; - - } - -} +namespace Cthangover.Core.Characters +{ + /// + /// 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. + /// + public struct ChangedAttributes + { + /// + /// Snapshot of the action source's damage and defence after the + /// exchange. + /// + public CharacterSet Source; + /// + /// Snapshot of the action target's damage and defence after the + /// exchange. + /// + public CharacterSet Target; + /// + /// Whether the action succeeded (e.g. hit landed vs. missed or + /// blocked). + /// + public bool Result; + + } + +} diff --git a/src/CharacterSet.cs b/src/CharacterSet.cs index d3fa44c..9802ab6 100644 --- a/src/CharacterSet.cs +++ b/src/CharacterSet.cs @@ -1,20 +1,20 @@ -namespace Cthangover.Core.Characters -{ - /// - /// 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. - /// - public struct CharacterSet - { - /// - /// Damage dealt or received in this exchange. - /// - public int Damage; - /// - /// Defence value at the time of the exchange. - /// - public int Defence; - } -} +namespace Cthangover.Core.Characters +{ + /// + /// 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. + /// + public struct CharacterSet + { + /// + /// Damage dealt or received in this exchange. + /// + public int Damage; + /// + /// Defence value at the time of the exchange. + /// + public int Defence; + } +} diff --git a/src/DeadgroundBehaviour.cs b/src/DeadgroundBehaviour.cs index 5605b94..419b359 100644 --- a/src/DeadgroundBehaviour.cs +++ b/src/DeadgroundBehaviour.cs @@ -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 -{ - /// - /// 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. - /// - public partial class DeadgroundBehaviour : TransitionWidget - { - /// - /// Clears battle data and switches to the main menu scene. - /// Bound to the "Main Menu" button on the defeat screen. - /// - public void ToMainMenuClick() - { - var sceneService = GetNode("/root/GodotSceneService"); - sceneService?.SwitchToMenu(); - BattleRequest.Current = null; - } - - /// - /// Plays the defeat sound, resolves the deadground background - /// texture from mods, and shows the transition widget. - /// - public override void Show() - { - CthangoverTheme.Apply(this); - CthangoverTheme.ApplyPrimary(GetNodeOrNull