Line endings: LF everywhere
.gitattributes forces LF (CRLF only for .bat/.cmd), core.autocrlf is off; files that differed only by CRLF are normalized.
This commit is contained in:
1 parent
392b1caf0a
commit
1018c90c25
28 files changed
+1713
-1683
No files matched your search
@@ -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
|
||||||
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
-366
@@ -1,368 +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("defeat");
|
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("victory");
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the battle and publishes <see cref="BattleEndedEvent"/>. The
|
/// Stops the battle and publishes <see cref="BattleEndedEvent"/>. The
|
||||||
/// outcome is passed in by the caller: both screens set
|
/// outcome is passed in by the caller: both screens set
|
||||||
/// <see cref="IsDestroyed"/> before clearing, so it cannot be derived
|
/// <see cref="IsDestroyed"/> before clearing, so it cannot be derived
|
||||||
/// from that flag (it used to be, and every battle reported "defeat").
|
/// from that flag (it used to be, and every battle reported "defeat").
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ClearBattle(string result)
|
private void ClearBattle(string result)
|
||||||
{
|
{
|
||||||
BattleActionMachine?.StopMachine();
|
BattleActionMachine?.StopMachine();
|
||||||
ActiveCharacter = null;
|
ActiveCharacter = null;
|
||||||
pendingPlayerCards?.Clear();
|
pendingPlayerCards?.Clear();
|
||||||
pendingEnemyCards?.Clear();
|
pendingEnemyCards?.Clear();
|
||||||
OnBattleCleared?.Invoke();
|
OnBattleCleared?.Invoke();
|
||||||
ModEventBus.Publish(new BattleEndedEvent
|
ModEventBus.Publish(new BattleEndedEvent
|
||||||
{
|
{
|
||||||
Result = result,
|
Result = result,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+39
-39
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 <= 0.
|
/// tween completes. Skips values <= 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 <= 0.
|
/// Skips values <= 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
{ }
|
{ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user