Compare commits

...
2 Commits
Author SHA1 Message Date
cthangover 1018c90c25 Line endings: LF everywhere
.gitattributes forces LF (CRLF only for .bat/.cmd), core.autocrlf is off;
files that differed only by CRLF are normalized.
2026-10-21 13:29:57 +02:00
cthangover 392b1caf0a Battle end reports the real outcome
Both result screens set IsDestroyed before ClearBattle, which derived the
result from that flag, so BattleEndedEvent.Result was always "defeat".
The screens now pass the outcome in.
2026-10-21 13:22:17 +02:00
28 changed files with 1713 additions and 1677 deletions

No files matched your search

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