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:
cthangover committed 2026-10-21 13:29:30 +02:00
1 parent 65e632a66f
commit 968b8d6fe1
20 files changed
+1740 -1710

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
# Line endings: LF everywhere, on every OS, whatever core.autocrlf says.
* text=auto eol=lf
# cmd.exe misreads labels and goto in LF batch files, so they stay CRLF.
*.bat text eol=crlf
*.cmd text eol=crlf
# Binaries: never converted, never diffed as text.
*.png binary
*.jpg binary
*.jpeg binary
*.webp binary
*.svg text
*.ogg binary
*.wav binary
*.mp3 binary
*.ttf binary
*.otf binary
*.woff binary
*.woff2 binary
*.dll binary
*.so binary
*.dylib binary
*.pdb binary
*.exe binary
*.zip binary
*.pck binary
*.moc3 binary
*.res binary
*.scn binary
+37 -37
View File
@@ -1,37 +1,37 @@
# Godot 4.x # Godot 4.x
.godot/ .godot/
.idea/ .idea/
sdk/ sdk/
bin/ bin/
obj/ obj/
#ai #ai
.omo/ .omo/
.opencode/ .opencode/
gen/ gen/
obj/ obj/
bin/ bin/
*.tres~ *.tres~
*.tscn~ *.tscn~
# GDScript LSP # GDScript LSP
.godot/ .godot/
# Export # Export
*.exe *.exe
*.dmg *.dmg
*.apk *.apk
*.aab *.aab
*.pck *.pck
*.zip *.zip
*.log *.log
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Editor # Editor
*.import *.import
*.godot.uid *.godot.uid
+101 -101
View File
@@ -1,101 +1,101 @@
using Cthangover.CardBattle.UI; using Cthangover.CardBattle.UI;
using Cthangover.Core.Battle.Actions; using Cthangover.Core.Battle.Actions;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
using Godot; using Godot;
namespace Cthangover.CardBattle.Actions namespace Cthangover.CardBattle.Actions
{ {
/// <summary> /// <summary>
/// Base class for animated battle actions that move, rotate, and scale card controls /// Base class for animated battle actions that move, rotate, and scale card controls
/// through a sequence of visual phases. Subclasses implement <c>DoInternalAction</c> to advance /// through a sequence of visual phases. Subclasses implement <c>DoInternalAction</c> to advance
/// a frame-by-frame animation state machine driven by <see cref="CardBattleCore"/>'s /// a frame-by-frame animation state machine driven by <see cref="CardBattleCore"/>'s
/// per-frame polling loop (<c>while (!DoAction()) await frame;</c>). /// per-frame polling loop (<c>while (!DoAction()) await frame;</c>).
/// Captures source/target positions and rotations in <see cref="DoStart"/> so that /// Captures source/target positions and rotations in <see cref="DoStart"/> so that
/// <see cref="DoEnd"/> can restore them regardless of what the animation did. /// <see cref="DoEnd"/> can restore them regardless of what the animation did.
/// </summary> /// </summary>
public abstract class AbstractBattleAction : IBattleAction public abstract class AbstractBattleAction : IBattleAction
{ {
/// <summary>Seconds timestamp of the current phase start (used for phase progress); -1 before the first phase.</summary> /// <summary>Seconds timestamp of the current phase start (used for phase progress); -1 before the first phase.</summary>
protected double Timestamp { get; set; } = -1; protected double Timestamp { get; set; } = -1;
/// <summary>Playback speed multiplier applied to all phase durations.</summary> /// <summary>Playback speed multiplier applied to all phase durations.</summary>
protected float Speed { get; set; } protected float Speed { get; set; }
/// <summary>The action definition whose stats drive the animation.</summary> /// <summary>The action definition whose stats drive the animation.</summary>
protected ActionCharacter Action { get; set; } protected ActionCharacter Action { get; set; }
/// <summary>Acting card node.</summary> /// <summary>Acting card node.</summary>
protected CharacterCardNode Source { get; set; } protected CharacterCardNode Source { get; set; }
/// <summary>Receiving card node.</summary> /// <summary>Receiving card node.</summary>
protected CharacterCardNode Target { get; set; } protected CharacterCardNode Target { get; set; }
/// <summary>Visual control of the source card (null-safe abort when freed mid-animation).</summary> /// <summary>Visual control of the source card (null-safe abort when freed mid-animation).</summary>
protected Control SourceControl { get; set; } protected Control SourceControl { get; set; }
/// <summary>Source position captured in <see cref="DoStart"/> for restoration in <see cref="DoEnd"/>.</summary> /// <summary>Source position captured in <see cref="DoStart"/> for restoration in <see cref="DoEnd"/>.</summary>
protected Vector2 SourcePos { get; set; } protected Vector2 SourcePos { get; set; }
/// <summary>Source rotation captured in <see cref="DoStart"/>.</summary> /// <summary>Source rotation captured in <see cref="DoStart"/>.</summary>
protected float SourceRot { get; set; } protected float SourceRot { get; set; }
/// <summary>Visual control of the target card.</summary> /// <summary>Visual control of the target card.</summary>
protected Control TargetControl { get; set; } protected Control TargetControl { get; set; }
/// <summary>Target position captured in <see cref="DoStart"/>.</summary> /// <summary>Target position captured in <see cref="DoStart"/>.</summary>
protected Vector2 TargetPos { get; set; } protected Vector2 TargetPos { get; set; }
/// <summary>Target rotation captured in <see cref="DoStart"/>.</summary> /// <summary>Target rotation captured in <see cref="DoStart"/>.</summary>
protected float TargetRot { get; set; } protected float TargetRot { get; set; }
/// <summary> /// <summary>
/// Advances the animation by one frame. Called in a loop by <see cref="CardBattleCore.RunEnemyTurn"/> /// Advances the animation by one frame. Called in a loop by <see cref="CardBattleCore.RunEnemyTurn"/>
/// with <c>await root.ToSignal(frame)</c> between calls. Returns <c>true</c> when the animation /// with <c>await root.ToSignal(frame)</c> between calls. Returns <c>true</c> when the animation
/// has completed all phases and the action result has been applied. /// has completed all phases and the action result has been applied.
/// Returns <c>true</c> early if any required node became null (card was destroyed mid-animation). /// Returns <c>true</c> early if any required node became null (card was destroyed mid-animation).
/// </summary> /// </summary>
/// <returns><c>true</c> when the animation is finished or cannot proceed.</returns> /// <returns><c>true</c> when the animation is finished or cannot proceed.</returns>
public bool DoAction() public bool DoAction()
{ {
if (Source == null || Target == null || Action == null || SourceControl == null || TargetControl == null) if (Source == null || Target == null || Action == null || SourceControl == null || TargetControl == null)
return true; return true;
return DoInternalAction(); return DoInternalAction();
} }
/// <summary>Frame tick of the animation; implementors advance phases and return true when finished.</summary> /// <summary>Frame tick of the animation; implementors advance phases and return true when finished.</summary>
protected abstract bool DoInternalAction(); protected abstract bool DoInternalAction();
/// <summary>Phase setup called once before the first <see cref="DoInternalAction"/> tick.</summary> /// <summary>Phase setup called once before the first <see cref="DoInternalAction"/> tick.</summary>
protected abstract void DoInternalStart(); protected abstract void DoInternalStart();
/// <summary>Cleanup called once after completion — restore captured positions/rotations here.</summary> /// <summary>Cleanup called once after completion — restore captured positions/rotations here.</summary>
protected abstract void DoInternalEnd(); protected abstract void DoInternalEnd();
protected AbstractBattleAction(CharacterCardNode source, CharacterCardNode target, ActionCharacter action, float speed = 1) protected AbstractBattleAction(CharacterCardNode source, CharacterCardNode target, ActionCharacter action, float speed = 1)
{ {
Source = source; Source = source;
Target = target; Target = target;
Action = action; Action = action;
Speed = speed; Speed = speed;
} }
/// <summary> /// <summary>
/// Captures the current positions, rotations, and <see cref="Control"/> references of both source /// Captures the current positions, rotations, and <see cref="Control"/> references of both source
/// and target cards before the animation begins, then delegates to the subclass /// and target cards before the animation begins, then delegates to the subclass
/// <see cref="DoInternalStart"/> for any additional setup (e.g. resetting timestamps). /// <see cref="DoInternalStart"/> for any additional setup (e.g. resetting timestamps).
/// </summary> /// </summary>
public void DoStart() public void DoStart()
{ {
if (Source == null || Target == null || Action == null) if (Source == null || Target == null || Action == null)
return; return;
SourceControl = Source.GetControlNode(); SourceControl = Source.GetControlNode();
SourcePos = SourceControl.GlobalPosition; SourcePos = SourceControl.GlobalPosition;
SourceRot = SourceControl.Rotation; SourceRot = SourceControl.Rotation;
TargetControl = Target.GetControlNode(); TargetControl = Target.GetControlNode();
TargetPos = TargetControl.GlobalPosition; TargetPos = TargetControl.GlobalPosition;
TargetRot = TargetControl.Rotation; TargetRot = TargetControl.Rotation;
DoInternalStart(); DoInternalStart();
} }
/// <summary> /// <summary>
/// Restores source and target card controls to their original positions, rotations, and visual state /// Restores source and target card controls to their original positions, rotations, and visual state
/// captured during <see cref="DoStart"/>. Delegates specific cleanup to <see cref="DoInternalEnd"/>. /// captured during <see cref="DoStart"/>. Delegates specific cleanup to <see cref="DoInternalEnd"/>.
/// </summary> /// </summary>
public void DoEnd() public void DoEnd()
{ {
if (Source == null || Target == null || Action == null) if (Source == null || Target == null || Action == null)
return; return;
DoInternalEnd(); DoInternalEnd();
} }
} }
} }
+26 -26
View File
@@ -1,26 +1,26 @@
namespace Cthangover.CardBattle.Actions namespace Cthangover.CardBattle.Actions
{ {
/// <summary> /// <summary>
/// Phases of the animated battle action state machine used by <see cref="PhysicsAttackAction"/> /// Phases of the animated battle action state machine used by <see cref="PhysicsAttackAction"/>
/// and <see cref="PhysicsDefenceAction"/>. Each tick, the action evaluates the current phase, /// and <see cref="PhysicsDefenceAction"/>. Each tick, the action evaluates the current phase,
/// interpolates card positions/rotations/scales, and transitions to the next phase when its /// interpolates card positions/rotations/scales, and transitions to the next phase when its
/// duration elapses. The <c>Recover</c> phase signals completion by returning <c>true</c> from <c>DoInternalAction</c>. /// duration elapses. The <c>Recover</c> phase signals completion by returning <c>true</c> from <c>DoInternalAction</c>.
/// </summary> /// </summary>
public enum ActionPhase public enum ActionPhase
{ {
/// <summary>Initial delay before any movement.</summary> /// <summary>Initial delay before any movement.</summary>
Wait, Wait,
/// <summary>Source card rotates toward target and begins slight movement.</summary> /// <summary>Source card rotates toward target and begins slight movement.</summary>
Prepare, Prepare,
/// <summary>Source card travels from its position to the target along a movement pattern.</summary> /// <summary>Source card travels from its position to the target along a movement pattern.</summary>
MoveForAttack, MoveForAttack,
/// <summary>Damage/effect is applied to the target card at the point of impact.</summary> /// <summary>Damage/effect is applied to the target card at the point of impact.</summary>
AttackImpact, AttackImpact,
/// <summary>Target card shows knockback, squashing, and shaking reaction.</summary> /// <summary>Target card shows knockback, squashing, and shaking reaction.</summary>
TargetReaction, TargetReaction,
/// <summary>Source card returns toward its original position.</summary> /// <summary>Source card returns toward its original position.</summary>
MoveForBack, MoveForBack,
/// <summary>All cards smoothly return to their original transforms.</summary> /// <summary>All cards smoothly return to their original transforms.</summary>
Recover Recover
} }
} }
+403 -403
View File
@@ -1,403 +1,403 @@
using Cthangover.Core.Battle; using Cthangover.Core.Battle;
using Cthangover.Core.Battle.Actions; using Cthangover.Core.Battle.Actions;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Cthangover.Core.Utils; using Cthangover.Core.Utils;
using Cthangover.TurnBattle; using Cthangover.TurnBattle;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
using Cthangover.CardBattle.UI; using Cthangover.CardBattle.UI;
using Godot; using Godot;
namespace Cthangover.CardBattle.Actions namespace Cthangover.CardBattle.Actions
{ {
/// <summary> /// <summary>
/// Animated battle action that moves the source card toward an enemy target, applies damage/defence /// Animated battle action that moves the source card toward an enemy target, applies damage/defence
/// via <see cref="ActionExecutorHub"/>, and animates the target's knockback/squash reaction. /// via <see cref="ActionExecutorHub"/>, and animates the target's knockback/squash reaction.
/// The source card follows one of five randomly-selected <see cref="MovementPattern"/> trajectories /// The source card follows one of five randomly-selected <see cref="MovementPattern"/> trajectories
/// (direct, arc, zigzag, spiral, dash). This is the visual counterpart of <see cref="PhysicsDamageActionCard"/> /// (direct, arc, zigzag, spiral, dash). This is the visual counterpart of <see cref="PhysicsDamageActionCard"/>
/// — it handles the presentation while the executor handles the stat changes. /// — it handles the presentation while the executor handles the stat changes.
/// Created by <see cref="CardBattleCore.CreateAnimatedAction"/> for enemy turns. /// Created by <see cref="CardBattleCore.CreateAnimatedAction"/> for enemy turns.
/// </summary> /// </summary>
public class PhysicsAttackAction : AbstractBattleAction public class PhysicsAttackAction : AbstractBattleAction
{ {
/// <summary> /// <summary>
/// Pre-defined movement trajectory patterns for the source card's approach to the target. /// Pre-defined movement trajectory patterns for the source card's approach to the target.
/// Selected randomly at construction to add visual variety to enemy attack animations. /// Selected randomly at construction to add visual variety to enemy attack animations.
/// </summary> /// </summary>
public enum MovementPattern public enum MovementPattern
{ {
Direct, Direct,
Arc, Arc,
Zigzag, Zigzag,
Spiral, Spiral,
Dash Dash
} }
private ActionPhase phase = ActionPhase.Wait; private ActionPhase phase = ActionPhase.Wait;
private float attackRotation; private float attackRotation;
private bool damageApplied; private bool damageApplied;
private bool stunEffectSpawned; private bool stunEffectSpawned;
private Vector2 targetOriginalPos; private Vector2 targetOriginalPos;
private Vector2 targetOriginalScale; private Vector2 targetOriginalScale;
private Vector2 knockbackDirection; private Vector2 knockbackDirection;
private float targetReactionIntensity = 0.3f; private float targetReactionIntensity = 0.3f;
private Vector2 knockbackPos; private Vector2 knockbackPos;
private float shake; private float shake;
private Vector2 squashScale; private Vector2 squashScale;
private float waitDuration = 0.2f; private float waitDuration = 0.2f;
private float prepareDuration = 0.3f; private float prepareDuration = 0.3f;
private float moveForAttackDuration = 0.4f; private float moveForAttackDuration = 0.4f;
private float attackImpactDuration = 0.3f; private float attackImpactDuration = 0.3f;
private float targetReactionDuration = 0.4f; private float targetReactionDuration = 0.4f;
private float moveForBackDuration = 0.5f; private float moveForBackDuration = 0.5f;
private float recoverDuration = 0.3f; private float recoverDuration = 0.3f;
private MovementPattern movementPattern = MovementPattern.Direct; private MovementPattern movementPattern = MovementPattern.Direct;
private Vector2 movementControlPoint; private Vector2 movementControlPoint;
/// <summary> /// <summary>
/// Creates an attack animation from <paramref name="source"/> to <paramref name="target"/> /// Creates an attack animation from <paramref name="source"/> to <paramref name="target"/>
/// using the stats defined in <paramref name="action"/>. The <see cref="MovementPattern"/> is /// using the stats defined in <paramref name="action"/>. The <see cref="MovementPattern"/> is
/// randomly selected on construction to make enemy attacks visually distinct. /// randomly selected on construction to make enemy attacks visually distinct.
/// <paramref name="speed"/> acts as a multiplier on all phase durations. /// <paramref name="speed"/> acts as a multiplier on all phase durations.
/// </summary> /// </summary>
public PhysicsAttackAction(CharacterCardNode source, CharacterCardNode target, ActionCharacter action, float speed = 1) public PhysicsAttackAction(CharacterCardNode source, CharacterCardNode target, ActionCharacter action, float speed = 1)
: base(source, target, action, speed) : base(source, target, action, speed)
{ {
movementPattern = (MovementPattern)GD.RandRange(0, 5); movementPattern = (MovementPattern)GD.RandRange(0, 5);
} }
/// <summary>Resets the phase clock and starts the attack sequence from the Wait phase.</summary> /// <summary>Resets the phase clock and starts the attack sequence from the Wait phase.</summary>
protected override void DoInternalStart() protected override void DoInternalStart()
{ {
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
damageApplied = false; damageApplied = false;
stunEffectSpawned = false; stunEffectSpawned = false;
phase = ActionPhase.Wait; phase = ActionPhase.Wait;
targetOriginalScale = TargetControl.Scale; targetOriginalScale = TargetControl.Scale;
} }
private float GetPhaseProgress() private float GetPhaseProgress()
{ {
float elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale; float elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale;
float duration = GetCurrentPhaseDuration(); float duration = GetCurrentPhaseDuration();
return Mathf.Clamp(elapsed / duration, 0f, 1f); return Mathf.Clamp(elapsed / duration, 0f, 1f);
} }
private float GetCurrentPhaseDuration() private float GetCurrentPhaseDuration()
{ {
switch (phase) switch (phase)
{ {
case ActionPhase.Wait: return waitDuration; case ActionPhase.Wait: return waitDuration;
case ActionPhase.Prepare: return prepareDuration; case ActionPhase.Prepare: return prepareDuration;
case ActionPhase.MoveForAttack: return moveForAttackDuration; case ActionPhase.MoveForAttack: return moveForAttackDuration;
case ActionPhase.AttackImpact: return attackImpactDuration; case ActionPhase.AttackImpact: return attackImpactDuration;
case ActionPhase.TargetReaction: return targetReactionDuration; case ActionPhase.TargetReaction: return targetReactionDuration;
case ActionPhase.MoveForBack: return moveForBackDuration; case ActionPhase.MoveForBack: return moveForBackDuration;
case ActionPhase.Recover: return recoverDuration; case ActionPhase.Recover: return recoverDuration;
default: return 1f; default: return 1f;
} }
} }
private void ResetTimestamp() private void ResetTimestamp()
{ {
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
} }
/// <summary>Advances the multi-phase attack: dash in, impact, damage/stun application, return and recover.</summary> /// <summary>Advances the multi-phase attack: dash in, impact, damage/stun application, return and recover.</summary>
protected override bool DoInternalAction() protected override bool DoInternalAction()
{ {
float progress = GetPhaseProgress(); float progress = GetPhaseProgress();
switch (phase) switch (phase)
{ {
case ActionPhase.Wait: case ActionPhase.Wait:
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.Prepare; phase = ActionPhase.Prepare;
ResetTimestamp(); ResetTimestamp();
targetOriginalPos = TargetControl.GlobalPosition; targetOriginalPos = TargetControl.GlobalPosition;
targetOriginalScale = TargetControl.Scale; targetOriginalScale = TargetControl.Scale;
knockbackDirection = (TargetControl.GlobalPosition - SourceControl.GlobalPosition).Normalized(); knockbackDirection = (TargetControl.GlobalPosition - SourceControl.GlobalPosition).Normalized();
InitializeMovementPattern(); InitializeMovementPattern();
} }
break; break;
case ActionPhase.Prepare: case ActionPhase.Prepare:
{ {
float prepareProgress = EaseOutQuad(Mathf.Min(progress, 1f)); float prepareProgress = EaseOutQuad(Mathf.Min(progress, 1f));
Vector2 preparePosition = GetMovementPosition(prepareProgress * 0.1f); Vector2 preparePosition = GetMovementPosition(prepareProgress * 0.1f);
SourceControl.GlobalPosition = SourcePos.Lerp(preparePosition, prepareProgress); SourceControl.GlobalPosition = SourcePos.Lerp(preparePosition, prepareProgress);
SourceControl.Rotation = Mathf.Lerp(SourceRot, TargetRot, prepareProgress * 0.3f); SourceControl.Rotation = Mathf.Lerp(SourceRot, TargetRot, prepareProgress * 0.3f);
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.MoveForAttack; phase = ActionPhase.MoveForAttack;
ResetTimestamp(); ResetTimestamp();
} }
break; break;
} }
case ActionPhase.MoveForAttack: case ActionPhase.MoveForAttack:
{ {
float attackProgress = EaseInOutQuad(Mathf.Min(progress, 1f)); float attackProgress = EaseInOutQuad(Mathf.Min(progress, 1f));
Vector2 targetPosition = GetMovementPosition(attackProgress); Vector2 targetPosition = GetMovementPosition(attackProgress);
SourceControl.GlobalPosition = targetPosition; SourceControl.GlobalPosition = targetPosition;
SourceControl.Rotation = Mathf.Lerp(SourceRot, TargetRot, attackProgress); SourceControl.Rotation = Mathf.Lerp(SourceRot, TargetRot, attackProgress);
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.AttackImpact; phase = ActionPhase.AttackImpact;
ResetTimestamp(); ResetTimestamp();
attackRotation = (float)GD.RandRange(-35f, 35f) * Mathf.Pi / 180f; attackRotation = (float)GD.RandRange(-35f, 35f) * Mathf.Pi / 180f;
} }
break; break;
} }
case ActionPhase.AttackImpact: case ActionPhase.AttackImpact:
{ {
float impactProgress = EaseInOutQuad(Mathf.Min(progress * 3f, 1f)); float impactProgress = EaseInOutQuad(Mathf.Min(progress * 3f, 1f));
SourceControl.Rotation = Mathf.Lerp(TargetRot, attackRotation, impactProgress); SourceControl.Rotation = Mathf.Lerp(TargetRot, attackRotation, impactProgress);
if (!damageApplied && progress >= 0.3f) if (!damageApplied && progress >= 0.3f)
{ {
damageApplied = true; damageApplied = true;
var result = ActionExecutorHub.Instance.Execute(Action, Source.Card, Target.Card); var result = ActionExecutorHub.Instance.Execute(Action, Source.Card, Target.Card);
Source.UpdateInfo(); Source.UpdateInfo();
Target.UpdateInfo(); Target.UpdateInfo();
if (result.Result) if (result.Result)
{ {
LogAttackResult(result); LogAttackResult(result);
} }
else else
{ {
GameLogger.Log("CARD_ACT", GameLogger.Log("CARD_ACT",
$"PhysicsAttackAction: failed to execute action {Action.Name} (source={Source.Card.Name} target={Target.Card.Name})", $"PhysicsAttackAction: failed to execute action {Action.Name} (source={Source.Card.Name} target={Target.Card.Name})",
LogLevel.Error); LogLevel.Error);
} }
} }
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.TargetReaction; phase = ActionPhase.TargetReaction;
ResetTimestamp(); ResetTimestamp();
} }
break; break;
} }
case ActionPhase.TargetReaction: case ActionPhase.TargetReaction:
{ {
float reactionProgress = EaseInOutQuad(Mathf.Min(progress, 1f)); float reactionProgress = EaseInOutQuad(Mathf.Min(progress, 1f));
bool isStun = Action != null && Action.GetInt("Turn", 0) > 1; bool isStun = Action != null && Action.GetInt("Turn", 0) > 1;
float currentKnockback = targetReactionIntensity; float currentKnockback = targetReactionIntensity;
if (isStun) if (isStun)
currentKnockback *= 2.5f; currentKnockback *= 2.5f;
knockbackPos = targetOriginalPos + knockbackDirection * currentKnockback; knockbackPos = targetOriginalPos + knockbackDirection * currentKnockback;
TargetControl.GlobalPosition = targetOriginalPos.Lerp(knockbackPos, reactionProgress); TargetControl.GlobalPosition = targetOriginalPos.Lerp(knockbackPos, reactionProgress);
squashScale = targetOriginalScale; squashScale = targetOriginalScale;
float currentSquash = isStun ? 0.35f : 0.2f; float currentSquash = isStun ? 0.35f : 0.2f;
squashScale.X *= 1f - reactionProgress * currentSquash; squashScale.X *= 1f - reactionProgress * currentSquash;
squashScale.Y *= 1f + reactionProgress * (currentSquash * 0.5f); squashScale.Y *= 1f + reactionProgress * (currentSquash * 0.5f);
TargetControl.Scale = squashScale; TargetControl.Scale = squashScale;
float currentShakeMultiplier = isStun ? 1.8f : 1f; float currentShakeMultiplier = isStun ? 1.8f : 1f;
shake = Mathf.Sin(reactionProgress * Mathf.Pi * (isStun ? 6f : 4f)) * (1f - reactionProgress) * 0.05f * currentShakeMultiplier; shake = Mathf.Sin(reactionProgress * Mathf.Pi * (isStun ? 6f : 4f)) * (1f - reactionProgress) * 0.05f * currentShakeMultiplier;
TargetControl.Rotation = shake * 30f * Mathf.Pi / 180f; TargetControl.Rotation = shake * 30f * Mathf.Pi / 180f;
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.MoveForBack; phase = ActionPhase.MoveForBack;
ResetTimestamp(); ResetTimestamp();
} }
break; break;
} }
case ActionPhase.MoveForBack: case ActionPhase.MoveForBack:
{ {
float backProgress = EaseOutQuad(Mathf.Min(progress, 1f)); float backProgress = EaseOutQuad(Mathf.Min(progress, 1f));
Vector2 backPosition = GetMovementPosition(1f - progress); Vector2 backPosition = GetMovementPosition(1f - progress);
SourceControl.GlobalPosition = backPosition.Lerp(SourcePos, backProgress); SourceControl.GlobalPosition = backPosition.Lerp(SourcePos, backProgress);
SourceControl.Rotation = Mathf.Lerp(attackRotation, SourceRot, backProgress); SourceControl.Rotation = Mathf.Lerp(attackRotation, SourceRot, backProgress);
TargetControl.GlobalPosition = knockbackPos.Lerp(targetOriginalPos, backProgress); TargetControl.GlobalPosition = knockbackPos.Lerp(targetOriginalPos, backProgress);
TargetControl.Scale = squashScale.Lerp(targetOriginalScale, backProgress); TargetControl.Scale = squashScale.Lerp(targetOriginalScale, backProgress);
float currentShake = Mathf.Sin(backProgress * Mathf.Pi) * shake * (1f - backProgress); float currentShake = Mathf.Sin(backProgress * Mathf.Pi) * shake * (1f - backProgress);
TargetControl.Rotation = Mathf.Lerp(currentShake * 30f * Mathf.Pi / 180f, TargetRot, backProgress); TargetControl.Rotation = Mathf.Lerp(currentShake * 30f * Mathf.Pi / 180f, TargetRot, backProgress);
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.Recover; phase = ActionPhase.Recover;
ResetTimestamp(); ResetTimestamp();
} }
break; break;
} }
case ActionPhase.Recover: case ActionPhase.Recover:
{ {
float recoverProgress = EaseOutQuad(Mathf.Min(progress, 1f)); float recoverProgress = EaseOutQuad(Mathf.Min(progress, 1f));
SourceControl.GlobalPosition = SourceControl.GlobalPosition.Lerp(SourcePos, recoverProgress); SourceControl.GlobalPosition = SourceControl.GlobalPosition.Lerp(SourcePos, recoverProgress);
SourceControl.Rotation = Mathf.Lerp(SourceControl.Rotation, SourceRot, recoverProgress); SourceControl.Rotation = Mathf.Lerp(SourceControl.Rotation, SourceRot, recoverProgress);
TargetControl.GlobalPosition = TargetControl.GlobalPosition.Lerp(TargetPos, recoverProgress); TargetControl.GlobalPosition = TargetControl.GlobalPosition.Lerp(TargetPos, recoverProgress);
TargetControl.Rotation = Mathf.Lerp(TargetControl.Rotation, TargetRot, recoverProgress); TargetControl.Rotation = Mathf.Lerp(TargetControl.Rotation, TargetRot, recoverProgress);
TargetControl.Scale = TargetControl.Scale.Lerp(targetOriginalScale, recoverProgress); TargetControl.Scale = TargetControl.Scale.Lerp(targetOriginalScale, recoverProgress);
if (progress >= 1f) if (progress >= 1f)
return true; return true;
break; break;
} }
default: default:
return true; return true;
} }
return false; return false;
} }
/// <summary>Restores the target's captured position, scale and rotation after the attack animation.</summary> /// <summary>Restores the target's captured position, scale and rotation after the attack animation.</summary>
protected override void DoInternalEnd() protected override void DoInternalEnd()
{ {
if (SourceControl != null) if (SourceControl != null)
{ {
SourceControl.GlobalPosition = SourcePos; SourceControl.GlobalPosition = SourcePos;
SourceControl.Rotation = SourceRot; SourceControl.Rotation = SourceRot;
} }
if (TargetControl != null) if (TargetControl != null)
{ {
TargetControl.GlobalPosition = TargetPos; TargetControl.GlobalPosition = TargetPos;
TargetControl.Rotation = TargetRot; TargetControl.Rotation = TargetRot;
TargetControl.Scale = targetOriginalScale; TargetControl.Scale = targetOriginalScale;
} }
} }
/// <summary> /// <summary>
/// Mirrors the resolved action onto the UI: floating damage / absorbed /// Mirrors the resolved action onto the UI: floating damage / absorbed
/// shield numbers, a colour pulse on the target card and journal rows, so /// shield numbers, a colour pulse on the target card and journal rows, so
/// an AI attack reads the same as the player's own attack. Stun actions /// an AI attack reads the same as the player's own attack. Stun actions
/// (a "Turn" above 1) flash the electric zap instead of damage numbers. /// (a "Turn" above 1) flash the electric zap instead of damage numbers.
/// </summary> /// </summary>
private void LogAttackResult(ChangedAttributes result) private void LogAttackResult(ChangedAttributes result)
{ {
var targetName = TranslationServer.Translate(Target.Card.Name).ToString(); var targetName = TranslationServer.Translate(Target.Card.Name).ToString();
var isStun = Action != null && Action.GetInt("Turn", 0) > 1; var isStun = Action != null && Action.GetInt("Turn", 0) > 1;
var defenceLost = -result.Target.Defence; var defenceLost = -result.Target.Defence;
if (defenceLost > 0) if (defenceLost > 0)
{ {
ShowDamageBehaviour.SpawnDefence(defenceLost, TargetControl, TargetControl.GlobalPosition); ShowDamageBehaviour.SpawnDefence(defenceLost, TargetControl, TargetControl.GlobalPosition);
BattleLogHub.Log( BattleLogHub.Log(
string.Format(Translate("battle/log_shield_absorb"), targetName, defenceLost), string.Format(Translate("battle/log_shield_absorb"), targetName, defenceLost),
UI.BattleLogKind.Shield); UI.BattleLogKind.Shield);
} }
if (isStun) if (isStun)
{ {
Target.PlayZap(); Target.PlayZap();
BattleLogHub.Log( BattleLogHub.Log(
string.Format(Translate("battle/log_stun"), targetName, Action.GetInt("Turn", 0)), string.Format(Translate("battle/log_stun"), targetName, Action.GetInt("Turn", 0)),
UI.BattleLogKind.Status); UI.BattleLogKind.Status);
return; return;
} }
if (result.Target.Damage > 0) if (result.Target.Damage > 0)
{ {
ShowDamageBehaviour.SpawnDamage(result.Target.Damage, TargetControl, TargetControl.GlobalPosition); ShowDamageBehaviour.SpawnDamage(result.Target.Damage, TargetControl, TargetControl.GlobalPosition);
Target.Flash(UiTokens.DangerText); Target.Flash(UiTokens.DangerText);
BattleLogHub.Log( BattleLogHub.Log(
string.Format(Translate("battle/log_damage"), string.Format(Translate("battle/log_damage"),
Translate(Action.Name), Translate(Action.Name),
TranslationServer.Translate(Source.Card.Name).ToString(), TranslationServer.Translate(Source.Card.Name).ToString(),
targetName, targetName,
result.Target.Damage), result.Target.Damage),
UI.BattleLogKind.Damage); UI.BattleLogKind.Damage);
} }
} }
private static string Translate(string key) private static string Translate(string key)
=> TranslationServer.Translate(key).ToString(); => TranslationServer.Translate(key).ToString();
private void InitializeMovementPattern() private void InitializeMovementPattern()
{ {
Vector2 direction = (TargetPos - SourcePos).Normalized(); Vector2 direction = (TargetPos - SourcePos).Normalized();
Vector2 perpendicular = new Vector2(-direction.Y, direction.X); Vector2 perpendicular = new Vector2(-direction.Y, direction.X);
switch (movementPattern) switch (movementPattern)
{ {
case MovementPattern.Arc: case MovementPattern.Arc:
movementControlPoint = SourcePos + direction * 0.5f + perpendicular * 2f; movementControlPoint = SourcePos + direction * 0.5f + perpendicular * 2f;
break; break;
case MovementPattern.Zigzag: case MovementPattern.Zigzag:
movementControlPoint = SourcePos + direction * 0.5f + perpendicular * 1.5f; movementControlPoint = SourcePos + direction * 0.5f + perpendicular * 1.5f;
break; break;
case MovementPattern.Spiral: case MovementPattern.Spiral:
movementControlPoint = TargetPos + perpendicular * 1.5f; movementControlPoint = TargetPos + perpendicular * 1.5f;
break; break;
case MovementPattern.Dash: case MovementPattern.Dash:
movementControlPoint = SourcePos + direction * 0.3f; movementControlPoint = SourcePos + direction * 0.3f;
break; break;
default: default:
movementControlPoint = TargetPos; movementControlPoint = TargetPos;
break; break;
} }
} }
private Vector2 GetMovementPosition(float progress) private Vector2 GetMovementPosition(float progress)
{ {
switch (movementPattern) switch (movementPattern)
{ {
case MovementPattern.Direct: case MovementPattern.Direct:
return SourcePos.Lerp(TargetPos, progress); return SourcePos.Lerp(TargetPos, progress);
case MovementPattern.Arc: case MovementPattern.Arc:
return SourcePos.Lerp(movementControlPoint, progress) return SourcePos.Lerp(movementControlPoint, progress)
.Lerp(movementControlPoint.Lerp(TargetPos, progress), progress); .Lerp(movementControlPoint.Lerp(TargetPos, progress), progress);
case MovementPattern.Zigzag: case MovementPattern.Zigzag:
if (progress < 0.5f) if (progress < 0.5f)
return SourcePos.Lerp(movementControlPoint, progress * 2f); return SourcePos.Lerp(movementControlPoint, progress * 2f);
else else
return movementControlPoint.Lerp(TargetPos, (progress - 0.5f) * 2f); return movementControlPoint.Lerp(TargetPos, (progress - 0.5f) * 2f);
case MovementPattern.Spiral: case MovementPattern.Spiral:
{ {
Vector2 toTarget = TargetPos - SourcePos; Vector2 toTarget = TargetPos - SourcePos;
float angle = progress * Mathf.Pi * 2f; float angle = progress * Mathf.Pi * 2f;
float radius = (1f - progress) * 1.5f; float radius = (1f - progress) * 1.5f;
Vector2 spiralOffset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius; Vector2 spiralOffset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
return SourcePos.Lerp(TargetPos, progress) + spiralOffset; return SourcePos.Lerp(TargetPos, progress) + spiralOffset;
} }
case MovementPattern.Dash: case MovementPattern.Dash:
if (progress < 0.3f) if (progress < 0.3f)
return SourcePos.Lerp(movementControlPoint, progress / 0.3f); return SourcePos.Lerp(movementControlPoint, progress / 0.3f);
else if (progress < 0.7f) else if (progress < 0.7f)
return movementControlPoint; return movementControlPoint;
else else
return movementControlPoint.Lerp(TargetPos, (progress - 0.7f) / 0.3f); return movementControlPoint.Lerp(TargetPos, (progress - 0.7f) / 0.3f);
default: default:
return SourcePos.Lerp(TargetPos, progress); return SourcePos.Lerp(TargetPos, progress);
} }
} }
private float EaseOutQuad(float t) private float EaseOutQuad(float t)
{ {
return 1f - (1f - t) * (1f - t); return 1f - (1f - t) * (1f - t);
} }
private float EaseInOutQuad(float t) private float EaseInOutQuad(float t)
{ {
return t < 0.5f ? 2f * t * t : 1f - Mathf.Pow(-2f * t + 2f, 2f) / 2f; return t < 0.5f ? 2f * t * t : 1f - Mathf.Pow(-2f * t + 2f, 2f) / 2f;
} }
} }
} }
+56 -56
View File
@@ -1,56 +1,56 @@
using Cthangover.Core.Cards.StatusEffect; using Cthangover.Core.Cards.StatusEffect;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
using Godot; using Godot;
namespace Cthangover.CardBattle.Actions namespace Cthangover.CardBattle.Actions
{ {
/// <summary> /// <summary>
/// Executor that applies physical damage from an attack card to a target. /// Executor that applies physical damage from an attack card to a target.
/// Calculates damage as <c>card_attack_multiplier × user_attack_stat</c>, reduces it by /// Calculates damage as <c>card_attack_multiplier × user_attack_stat</c>, reduces it by
/// the target's current defence value (which is consumed in the process), and applies the /// the target's current defence value (which is consumed in the process), and applies the
/// remainder to the target's health. Invokes <c>OnTakeDamage</c>/<c>OnDealDamage</c> hooks /// remainder to the target's health. Invokes <c>OnTakeDamage</c>/<c>OnDealDamage</c> hooks
/// on both characters' <see cref="StatusEffectQueue"/> so that damage-modifying status effects /// on both characters' <see cref="StatusEffectQueue"/> so that damage-modifying status effects
/// can intercept and adjust the values before they are applied. /// can intercept and adjust the values before they are applied.
/// Registered in <see cref="CardBattleActionProvider"/> under <c>"physics/attack"</c>. /// Registered in <see cref="CardBattleActionProvider"/> under <c>"physics/attack"</c>.
/// </summary> /// </summary>
public class PhysicsDamageActionCard : ActionBase public class PhysicsDamageActionCard : ActionBase
{ {
/// <summary> /// <summary>
/// Unique identifier registered in <see cref="CardBattleActionProvider"/> as <c>"physics/attack"</c>. /// Unique identifier registered in <see cref="CardBattleActionProvider"/> as <c>"physics/attack"</c>.
/// </summary> /// </summary>
public override string ID => "PhysicsDamageActionCard"; public override string ID => "PhysicsDamageActionCard";
/// <summary> /// <summary>
/// Resolves the attack: deducts action points from <paramref name="user"/> via <c>CheckRequiredAndUsePoint</c>, /// Resolves the attack: deducts action points from <paramref name="user"/> via <c>CheckRequiredAndUsePoint</c>,
/// computes raw damage from the card's attack multiplier and the user's attack stat, /// computes raw damage from the card's attack multiplier and the user's attack stat,
/// applies status-effect damage hooks, consumes <paramref name="target"/>'s defence shield first, /// applies status-effect damage hooks, consumes <paramref name="target"/>'s defence shield first,
/// then subtracts remaining damage from health. Returns a <see cref="ChangedAttributes"/> with /// then subtracts remaining damage from health. Returns a <see cref="ChangedAttributes"/> with
/// the actual damage and defence-loss deltas for the floating text display. /// the actual damage and defence-loss deltas for the floating text display.
/// </summary> /// </summary>
public override ChangedAttributes Execute(ActionCharacter actionCard, Character user, Character target) public override ChangedAttributes Execute(ActionCharacter actionCard, Character user, Character target)
{ {
if (target == null || user == null || !CheckRequiredAndUsePoint(actionCard, user)) if (target == null || user == null || !CheckRequiredAndUsePoint(actionCard, user))
return new ChangedAttributes { Result = false }; return new ChangedAttributes { Result = false };
var damageDelta = Mathf.RoundToInt(actionCard.GetFloat("Attack", 1f) * user.Attributes.Attack.Value); var damageDelta = Mathf.RoundToInt(actionCard.GetFloat("Attack", 1f) * user.Attributes.Attack.Value);
StatusEffectService.Of(target).OnTakeDamage(user, ref damageDelta); StatusEffectService.Of(target).OnTakeDamage(user, ref damageDelta);
StatusEffectService.Of(user).OnDealDamage(target, ref damageDelta); StatusEffectService.Of(user).OnDealDamage(target, ref damageDelta);
var defenceDelta = 0; var defenceDelta = 0;
var ignoreArmor = user.HasPassiveEffect("IgnoreArmor"); var ignoreArmor = user.HasPassiveEffect("IgnoreArmor");
if (!ignoreArmor && target.Attributes.Defence.Value > 0) if (!ignoreArmor && target.Attributes.Defence.Value > 0)
{ {
defenceDelta = target.Attributes.Defence.Value >= damageDelta ? damageDelta : target.Attributes.Defence.Value; defenceDelta = target.Attributes.Defence.Value >= damageDelta ? damageDelta : target.Attributes.Defence.Value;
damageDelta -= defenceDelta; damageDelta -= defenceDelta;
} }
target.Attributes.Defence.Value -= defenceDelta; target.Attributes.Defence.Value -= defenceDelta;
target.Attributes.Health.Value -= damageDelta; target.Attributes.Health.Value -= damageDelta;
return new ChangedAttributes { Result = true, Target = { Damage = damageDelta, Defence = -defenceDelta}}; return new ChangedAttributes { Result = true, Target = { Damage = damageDelta, Defence = -defenceDelta}};
} }
} }
} }
+231 -231
View File
@@ -1,231 +1,231 @@
using Cthangover.Core.Battle; using Cthangover.Core.Battle;
using Cthangover.Core.Battle.Actions; using Cthangover.Core.Battle.Actions;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
using Cthangover.TurnBattle; using Cthangover.TurnBattle;
using Cthangover.CardBattle.UI; using Cthangover.CardBattle.UI;
using Godot; using Godot;
namespace Cthangover.CardBattle.Actions namespace Cthangover.CardBattle.Actions
{ {
/// <summary> /// <summary>
/// Animated battle action that plays a self-buff or ally-support animation: the source card /// Animated battle action that plays a self-buff or ally-support animation: the source card
/// briefly rotates toward the target, applies a defence boost via <see cref="ActionExecutorHub"/>, /// briefly rotates toward the target, applies a defence boost via <see cref="ActionExecutorHub"/>,
/// and shows a subtle block/stagger reaction on the target. Used for both <c>ToSelf</c> and /// and shows a subtle block/stagger reaction on the target. Used for both <c>ToSelf</c> and
/// <c>ToAlias</c> action types. This is the visual counterpart of <see cref="PhysicsDefenceActionCard"/>. /// <c>ToAlias</c> action types. This is the visual counterpart of <see cref="PhysicsDefenceActionCard"/>.
/// Created by <see cref="CardBattleCore.CreateAnimatedAction"/> for enemy self-buff/support turns. /// Created by <see cref="CardBattleCore.CreateAnimatedAction"/> for enemy self-buff/support turns.
/// </summary> /// </summary>
public class PhysicsDefenceAction : AbstractBattleAction public class PhysicsDefenceAction : AbstractBattleAction
{ {
private ActionPhase phase = ActionPhase.Prepare; private ActionPhase phase = ActionPhase.Prepare;
private float defendRotation; private float defendRotation;
private bool effectApplied; private bool effectApplied;
private Vector2 targetOriginalScale; private Vector2 targetOriginalScale;
private Color targetOriginalColor; private Color targetOriginalColor;
private Vector2 blockScale; private Vector2 blockScale;
private Vector2 blockKnockback; private Vector2 blockKnockback;
private Color blockColor; private Color blockColor;
private float prepareDuration = 0.3f; private float prepareDuration = 0.3f;
private float attackImpactDuration = 0.4f; private float attackImpactDuration = 0.4f;
private float targetReactionDuration = 0.3f; private float targetReactionDuration = 0.3f;
private float moveForBackDuration = 0.4f; private float moveForBackDuration = 0.4f;
private float recoverDuration = 0.3f; private float recoverDuration = 0.3f;
/// <summary> /// <summary>
/// Creates a defence/support animation. Both <paramref name="source"/> and <paramref name="target"/> /// Creates a defence/support animation. Both <paramref name="source"/> and <paramref name="target"/>
/// can be the same card (for self-buffs) or different (for ally support). /// can be the same card (for self-buffs) or different (for ally support).
/// <paramref name="speed"/> multiplies all phase durations. /// <paramref name="speed"/> multiplies all phase durations.
/// </summary> /// </summary>
public PhysicsDefenceAction(CharacterCardNode source, CharacterCardNode target, ActionCharacter action, float speed = 1) public PhysicsDefenceAction(CharacterCardNode source, CharacterCardNode target, ActionCharacter action, float speed = 1)
: base(source, target, action, speed) { } : base(source, target, action, speed) { }
/// <summary>Resets the phase clock and captures the target's original scale and colour.</summary> /// <summary>Resets the phase clock and captures the target's original scale and colour.</summary>
protected override void DoInternalStart() protected override void DoInternalStart()
{ {
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
effectApplied = false; effectApplied = false;
phase = ActionPhase.Prepare; phase = ActionPhase.Prepare;
targetOriginalScale = TargetControl.Scale; targetOriginalScale = TargetControl.Scale;
targetOriginalColor = TargetControl.Modulate; targetOriginalColor = TargetControl.Modulate;
} }
private float GetPhaseProgress() private float GetPhaseProgress()
{ {
float elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale; float elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale;
float duration = GetCurrentPhaseDuration(); float duration = GetCurrentPhaseDuration();
return Mathf.Clamp(elapsed / duration, 0f, 1f); return Mathf.Clamp(elapsed / duration, 0f, 1f);
} }
private float GetCurrentPhaseDuration() private float GetCurrentPhaseDuration()
{ {
switch (phase) switch (phase)
{ {
case ActionPhase.Prepare: return prepareDuration; case ActionPhase.Prepare: return prepareDuration;
case ActionPhase.AttackImpact: return attackImpactDuration; case ActionPhase.AttackImpact: return attackImpactDuration;
case ActionPhase.TargetReaction: return targetReactionDuration; case ActionPhase.TargetReaction: return targetReactionDuration;
case ActionPhase.MoveForBack: return moveForBackDuration; case ActionPhase.MoveForBack: return moveForBackDuration;
case ActionPhase.Recover: return recoverDuration; case ActionPhase.Recover: return recoverDuration;
default: return 1f; default: return 1f;
} }
} }
private void ResetTimestamp() private void ResetTimestamp()
{ {
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
} }
/// <summary>Advances the guard animation: approach, impact, defence stat application, return and recover.</summary> /// <summary>Advances the guard animation: approach, impact, defence stat application, return and recover.</summary>
protected override bool DoInternalAction() protected override bool DoInternalAction()
{ {
float progress = GetPhaseProgress(); float progress = GetPhaseProgress();
switch (phase) switch (phase)
{ {
case ActionPhase.Prepare: case ActionPhase.Prepare:
{ {
float prepareProgress = EaseOutQuad(Mathf.Min(progress, 1f)); float prepareProgress = EaseOutQuad(Mathf.Min(progress, 1f));
SourceControl.Rotation = Mathf.Lerp(SourceRot, TargetRot, prepareProgress); SourceControl.Rotation = Mathf.Lerp(SourceRot, TargetRot, prepareProgress);
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.AttackImpact; phase = ActionPhase.AttackImpact;
ResetTimestamp(); ResetTimestamp();
defendRotation = (float)GD.RandRange(-15f, 15f) * Mathf.Pi / 180f; defendRotation = (float)GD.RandRange(-15f, 15f) * Mathf.Pi / 180f;
targetOriginalScale = TargetControl.Scale; targetOriginalScale = TargetControl.Scale;
targetOriginalColor = TargetControl.Modulate; targetOriginalColor = TargetControl.Modulate;
} }
break; break;
} }
case ActionPhase.AttackImpact: case ActionPhase.AttackImpact:
{ {
float impactProgress = EaseInOutQuad(Mathf.Min(progress * 2f, 1f)); float impactProgress = EaseInOutQuad(Mathf.Min(progress * 2f, 1f));
SourceControl.Rotation = Mathf.Lerp(TargetRot, defendRotation, impactProgress); SourceControl.Rotation = Mathf.Lerp(TargetRot, defendRotation, impactProgress);
blockScale = targetOriginalScale * (1f + impactProgress * 0.1f); blockScale = targetOriginalScale * (1f + impactProgress * 0.1f);
TargetControl.Scale = blockScale; TargetControl.Scale = blockScale;
blockColor = targetOriginalColor; blockColor = targetOriginalColor;
blockColor.G = Mathf.Min(1f, blockColor.G + impactProgress * 0.3f); blockColor.G = Mathf.Min(1f, blockColor.G + impactProgress * 0.3f);
blockColor.B = Mathf.Min(1f, blockColor.B + impactProgress * 0.2f); blockColor.B = Mathf.Min(1f, blockColor.B + impactProgress * 0.2f);
TargetControl.Modulate = blockColor; TargetControl.Modulate = blockColor;
if (!effectApplied && progress >= 0.4f) if (!effectApplied && progress >= 0.4f)
{ {
effectApplied = true; effectApplied = true;
var result = ActionExecutorHub.Instance.Execute(Action, Source.Card, Target.Card); var result = ActionExecutorHub.Instance.Execute(Action, Source.Card, Target.Card);
Source.UpdateInfo(); Source.UpdateInfo();
Target.UpdateInfo(); Target.UpdateInfo();
if (result.Result && result.Target.Defence > 0) if (result.Result && result.Target.Defence > 0)
{ {
ShowDamageBehaviour.SpawnDefence(result.Target.Defence, TargetControl, TargetControl.GlobalPosition); ShowDamageBehaviour.SpawnDefence(result.Target.Defence, TargetControl, TargetControl.GlobalPosition);
Target.Flash(UiTokens.Info); Target.Flash(UiTokens.Info);
BattleLogHub.Log( BattleLogHub.Log(
string.Format(Translate("battle/log_shield_gain"), string.Format(Translate("battle/log_shield_gain"),
TranslationServer.Translate(Target.Card.Name).ToString(), TranslationServer.Translate(Target.Card.Name).ToString(),
result.Target.Defence), result.Target.Defence),
UI.BattleLogKind.Buff); UI.BattleLogKind.Buff);
} }
} }
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.TargetReaction; phase = ActionPhase.TargetReaction;
ResetTimestamp(); ResetTimestamp();
} }
break; break;
} }
case ActionPhase.TargetReaction: case ActionPhase.TargetReaction:
{ {
float reactionProgress = EaseInOutQuad(Mathf.Min(progress * 1.5f, 1f)); float reactionProgress = EaseInOutQuad(Mathf.Min(progress * 1.5f, 1f));
blockKnockback = TargetPos + (TargetPos - SourcePos).Normalized() * 0.1f; blockKnockback = TargetPos + (TargetPos - SourcePos).Normalized() * 0.1f;
TargetControl.GlobalPosition = TargetPos.Lerp(blockKnockback, reactionProgress * 0.5f); TargetControl.GlobalPosition = TargetPos.Lerp(blockKnockback, reactionProgress * 0.5f);
TargetControl.Scale = blockScale.Lerp(targetOriginalScale, reactionProgress); TargetControl.Scale = blockScale.Lerp(targetOriginalScale, reactionProgress);
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.MoveForBack; phase = ActionPhase.MoveForBack;
ResetTimestamp(); ResetTimestamp();
} }
break; break;
} }
case ActionPhase.MoveForBack: case ActionPhase.MoveForBack:
{ {
float backProgress = EaseOutQuad(Mathf.Min(progress, 1f)); float backProgress = EaseOutQuad(Mathf.Min(progress, 1f));
SourceControl.GlobalPosition = TargetPos.Lerp(SourcePos, backProgress); SourceControl.GlobalPosition = TargetPos.Lerp(SourcePos, backProgress);
SourceControl.Rotation = Mathf.Lerp(defendRotation, SourceRot, backProgress); SourceControl.Rotation = Mathf.Lerp(defendRotation, SourceRot, backProgress);
TargetControl.GlobalPosition = TargetControl.GlobalPosition.Lerp(TargetPos, backProgress); TargetControl.GlobalPosition = TargetControl.GlobalPosition.Lerp(TargetPos, backProgress);
TargetControl.Scale = TargetControl.Scale.Lerp(targetOriginalScale, backProgress); TargetControl.Scale = TargetControl.Scale.Lerp(targetOriginalScale, backProgress);
Color currentColor = TargetControl.Modulate; Color currentColor = TargetControl.Modulate;
TargetControl.Modulate = currentColor.Lerp(targetOriginalColor, backProgress); TargetControl.Modulate = currentColor.Lerp(targetOriginalColor, backProgress);
if (progress >= 1f) if (progress >= 1f)
{ {
phase = ActionPhase.Recover; phase = ActionPhase.Recover;
ResetTimestamp(); ResetTimestamp();
} }
break; break;
} }
case ActionPhase.Recover: case ActionPhase.Recover:
{ {
float recoverProgress = EaseOutQuad(Mathf.Min(progress, 1f)); float recoverProgress = EaseOutQuad(Mathf.Min(progress, 1f));
SourceControl.GlobalPosition = SourceControl.GlobalPosition.Lerp(SourcePos, recoverProgress); SourceControl.GlobalPosition = SourceControl.GlobalPosition.Lerp(SourcePos, recoverProgress);
SourceControl.Rotation = Mathf.Lerp(SourceControl.Rotation, SourceRot, recoverProgress); SourceControl.Rotation = Mathf.Lerp(SourceControl.Rotation, SourceRot, recoverProgress);
TargetControl.Rotation = Mathf.Lerp(TargetControl.Rotation, TargetRot, recoverProgress); TargetControl.Rotation = Mathf.Lerp(TargetControl.Rotation, TargetRot, recoverProgress);
if (progress >= 1f) if (progress >= 1f)
return true; return true;
break; break;
} }
default: default:
return true; return true;
} }
return false; return false;
} }
/// <summary>Restores the target's captured visual state after the defence animation.</summary> /// <summary>Restores the target's captured visual state after the defence animation.</summary>
protected override void DoInternalEnd() protected override void DoInternalEnd()
{ {
if (SourceControl != null) if (SourceControl != null)
{ {
SourceControl.GlobalPosition = SourcePos; SourceControl.GlobalPosition = SourcePos;
SourceControl.Rotation = SourceRot; SourceControl.Rotation = SourceRot;
} }
if (TargetControl != null) if (TargetControl != null)
{ {
TargetControl.GlobalPosition = TargetPos; TargetControl.GlobalPosition = TargetPos;
TargetControl.Rotation = TargetRot; TargetControl.Rotation = TargetRot;
TargetControl.Scale = targetOriginalScale; TargetControl.Scale = targetOriginalScale;
TargetControl.Modulate = targetOriginalColor; TargetControl.Modulate = targetOriginalColor;
} }
} }
private float EaseOutQuad(float t) private float EaseOutQuad(float t)
{ {
return 1f - (1f - t) * (1f - t); return 1f - (1f - t) * (1f - t);
} }
private float EaseInOutQuad(float t) private float EaseInOutQuad(float t)
{ {
return t < 0.5f ? 2f * t * t : 1f - Mathf.Pow(-2f * t + 2f, 2f) / 2f; return t < 0.5f ? 2f * t * t : 1f - Mathf.Pow(-2f * t + 2f, 2f) / 2f;
} }
private static string Translate(string key) private static string Translate(string key)
=> TranslationServer.Translate(key).ToString(); => TranslationServer.Translate(key).ToString();
} }
} }
+39 -39
View File
@@ -1,39 +1,39 @@
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
namespace Cthangover.CardBattle.Actions namespace Cthangover.CardBattle.Actions
{ {
/// <summary> /// <summary>
/// Executor that adds a fixed amount of defence to the target character. /// Executor that adds a fixed amount of defence to the target character.
/// Defence acts as a damage-absorbing shield that is consumed before health when the /// Defence acts as a damage-absorbing shield that is consumed before health when the
/// character takes damage (see <see cref="PhysicsDamageActionCard"/>). The defence delta /// character takes damage (see <see cref="PhysicsDamageActionCard"/>). The defence delta
/// comes from the <c>"Defence"</c> property on the action card. /// comes from the <c>"Defence"</c> property on the action card.
/// Registered in <see cref="CardBattleActionProvider"/> under <c>"physics/defence"</c>. /// Registered in <see cref="CardBattleActionProvider"/> under <c>"physics/defence"</c>.
/// </summary> /// </summary>
public class PhysicsDefenceActionCard : ActionBase public class PhysicsDefenceActionCard : ActionBase
{ {
/// <summary> /// <summary>
/// Unique identifier registered in <see cref="CardBattleActionProvider"/> as <c>"physics/defence"</c>. /// Unique identifier registered in <see cref="CardBattleActionProvider"/> as <c>"physics/defence"</c>.
/// </summary> /// </summary>
public override string ID => "PhysicsDefenceActionCard"; public override string ID => "PhysicsDefenceActionCard";
/// <summary> /// <summary>
/// Adds a flat defence value to <paramref name="target"/> after deducting action points /// Adds a flat defence value to <paramref name="target"/> after deducting action points
/// from <paramref name="user"/>. The returned <see cref="ChangedAttributes"/> carries the /// from <paramref name="user"/>. The returned <see cref="ChangedAttributes"/> carries the
/// defence delta for floating text display. /// defence delta for floating text display.
/// </summary> /// </summary>
public override ChangedAttributes Execute(ActionCharacter actionCard, Character user, Character target) public override ChangedAttributes Execute(ActionCharacter actionCard, Character user, Character target)
{ {
if (target == null || user == null || !CheckRequiredAndUsePoint(actionCard, user)) if (target == null || user == null || !CheckRequiredAndUsePoint(actionCard, user))
return new ChangedAttributes { Result = false }; return new ChangedAttributes { Result = false };
var defenceDelta = actionCard.GetInt("Defence"); var defenceDelta = actionCard.GetInt("Defence");
target.Attributes.Defence.Value += defenceDelta; target.Attributes.Defence.Value += defenceDelta;
return new ChangedAttributes { Result = true, Target = { Defence = defenceDelta}}; return new ChangedAttributes { Result = true, Target = { Defence = defenceDelta}};
} }
} }
} }
+40 -40
View File
@@ -1,40 +1,40 @@
using Cthangover.Core.Cards.StatusEffect; using Cthangover.Core.Cards.StatusEffect;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
namespace Cthangover.CardBattle.Actions namespace Cthangover.CardBattle.Actions
{ {
/// <summary> /// <summary>
/// Executor that applies a stun status effect to the target character. /// Executor that applies a stun status effect to the target character.
/// The stun duration (number of turns) is read from the <c>"Turn"</c> property on the action card. /// The stun duration (number of turns) is read from the <c>"Turn"</c> property on the action card.
/// During a stunned character's turn, <see cref="StatusEffectQueue.HasStun"/> returns <c>true</c>, /// During a stunned character's turn, <see cref="StatusEffectQueue.HasStun"/> returns <c>true</c>,
/// causing <see cref="CardBattleCore.RunEnemyTurn"/> to skip that character's actions entirely. /// causing <see cref="CardBattleCore.RunEnemyTurn"/> to skip that character's actions entirely.
/// Registered in <see cref="CardBattleActionProvider"/> under <c>"physics/stun"</c>. /// Registered in <see cref="CardBattleActionProvider"/> under <c>"physics/stun"</c>.
/// </summary> /// </summary>
public class PhysicsStunActionCard : ActionBase public class PhysicsStunActionCard : ActionBase
{ {
/// <summary> /// <summary>
/// Unique identifier registered in <see cref="CardBattleActionProvider"/> as <c>"physics/stun"</c>. /// Unique identifier registered in <see cref="CardBattleActionProvider"/> as <c>"physics/stun"</c>.
/// </summary> /// </summary>
public override string ID => "PhysicsStunActionCard"; public override string ID => "PhysicsStunActionCard";
/// <summary> /// <summary>
/// Adds a <see cref="StunStatusEffect"/> with the configured turn count to <paramref name="target"/>'s /// Adds a <see cref="StunStatusEffect"/> with the configured turn count to <paramref name="target"/>'s
/// <see cref="StatusEffectQueue"/> after deducting action points from <paramref name="user"/>. /// <see cref="StatusEffectQueue"/> after deducting action points from <paramref name="user"/>.
/// </summary> /// </summary>
public override ChangedAttributes Execute(ActionCharacter actionCard, Character user, Character target) public override ChangedAttributes Execute(ActionCharacter actionCard, Character user, Character target)
{ {
if (target == null || user == null || !CheckRequiredAndUsePoint(actionCard, user)) if (target == null || user == null || !CheckRequiredAndUsePoint(actionCard, user))
return new ChangedAttributes { Result = false }; return new ChangedAttributes { Result = false };
if (target.HasPassiveEffect("StunImmune")) if (target.HasPassiveEffect("StunImmune"))
return new ChangedAttributes { Result = true }; return new ChangedAttributes { Result = true };
StatusEffectService.Of(target).Add("effect/physics/stun", actionCard.GetInt("Turn", 3)); StatusEffectService.Of(target).Add("effect/physics/stun", actionCard.GetInt("Turn", 3));
return new ChangedAttributes { Result = true }; return new ChangedAttributes { Result = true };
} }
} }
} }
+52 -52
View File
@@ -1,52 +1,52 @@
using Cthangover.CardBattle.UI; using Cthangover.CardBattle.UI;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
namespace Cthangover.CardBattle.Player namespace Cthangover.CardBattle.Player
{ {
/// <summary> /// <summary>
/// Factory that returns the appropriate <see cref="ICardActionStrategy"/> implementation /// Factory that returns the appropriate <see cref="ICardActionStrategy"/> implementation
/// for a given <see cref="ActionCharacter"/>. Dispatches based on <see cref="ActionCharacterType"/>: /// for a given <see cref="ActionCharacter"/>. Dispatches based on <see cref="ActionCharacterType"/>:
/// <c>ToEnemy</c> → <see cref="AttackToEnemyStrategy"/>, /// <c>ToEnemy</c> → <see cref="AttackToEnemyStrategy"/>,
/// <c>ToAlias</c> → <see cref="SupportStrategy"/>, /// <c>ToAlias</c> → <see cref="SupportStrategy"/>,
/// <c>ToSelf</c> → <see cref="SelfStrategy"/>. /// <c>ToSelf</c> → <see cref="SelfStrategy"/>.
/// Used by <see cref="CardController"/> during drag-and-drop to determine valid targeting behavior /// Used by <see cref="CardController"/> during drag-and-drop to determine valid targeting behavior
/// and by <see cref="CanTarget"/> for pre-validation. /// and by <see cref="CanTarget"/> for pre-validation.
/// </summary> /// </summary>
public static class CardActionStrategyFactory public static class CardActionStrategyFactory
{ {
/// <summary> /// <summary>
/// Returns the matching strategy for the action's type, or <c>null</c> if any argument is null /// Returns the matching strategy for the action's type, or <c>null</c> if any argument is null
/// or the type is unrecognized. /// or the type is unrecognized.
/// </summary> /// </summary>
public static ICardActionStrategy Get(ActionCharacter action, CharacterCardNode source, CharacterCardNode target) public static ICardActionStrategy Get(ActionCharacter action, CharacterCardNode source, CharacterCardNode target)
{ {
if (action == null || source == null || target == null) if (action == null || source == null || target == null)
return null; return null;
switch (action.Type) switch (action.Type)
{ {
case ActionCharacterType.ToEnemy: case ActionCharacterType.ToEnemy:
return new AttackToEnemyStrategy(); return new AttackToEnemyStrategy();
case ActionCharacterType.ToAlias: case ActionCharacterType.ToAlias:
return new SupportStrategy(); return new SupportStrategy();
case ActionCharacterType.ToSelf: case ActionCharacterType.ToSelf:
return new SelfStrategy(); return new SelfStrategy();
default: default:
return null; return null;
} }
} }
/// <summary> /// <summary>
/// Convenience method that gets the strategy via <see cref="Get"/> and immediately calls /// Convenience method that gets the strategy via <see cref="Get"/> and immediately calls
/// <see cref="ICardActionStrategy.Check"/> on it. Returns <c>false</c> if no strategy matches /// <see cref="ICardActionStrategy.Check"/> on it. Returns <c>false</c> if no strategy matches
/// or any argument is null. /// or any argument is null.
/// </summary> /// </summary>
public static bool CanTarget(ActionCharacter action, CharacterCardNode source, CharacterCardNode target) public static bool CanTarget(ActionCharacter action, CharacterCardNode source, CharacterCardNode target)
{ {
if (action == null || source == null || target == null) if (action == null || source == null || target == null)
return false; return false;
return Get(action, source, target)?.Check(action, source, target) ?? false; return Get(action, source, target)?.Check(action, source, target) ?? false;
} }
} }
} }
+32 -32
View File
@@ -1,32 +1,32 @@
using Cthangover.CardBattle.UI; using Cthangover.CardBattle.UI;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
namespace Cthangover.CardBattle.Player namespace Cthangover.CardBattle.Player
{ {
/// <summary> /// <summary>
/// Strategy interface that defines validation, visual highlighting, and execution for /// Strategy interface that defines validation, visual highlighting, and execution for
/// a specific type of card action during the player's drag-and-drop targeting flow. /// a specific type of card action during the player's drag-and-drop targeting flow.
/// The <see cref="CardActionStrategyFactory"/> dispatches to the correct strategy based on /// The <see cref="CardActionStrategyFactory"/> dispatches to the correct strategy based on
/// the action's <see cref="ActionCharacterType"/>. <see cref="CardController"/> calls /// the action's <see cref="ActionCharacterType"/>. <see cref="CardController"/> calls
/// <see cref="Check"/> during drag to validate targets, <see cref="HighlightTarget"/> to show /// <see cref="Check"/> during drag to validate targets, <see cref="HighlightTarget"/> to show
/// valid/invalid visual feedback, and <see cref="Execute"/> when the card is dropped on a valid target. /// valid/invalid visual feedback, and <see cref="Execute"/> when the card is dropped on a valid target.
/// </summary> /// </summary>
public interface ICardActionStrategy public interface ICardActionStrategy
{ {
/// <summary> /// <summary>
/// Validates whether <paramref name="action"/> can be performed by <paramref name="source"/> /// Validates whether <paramref name="action"/> can be performed by <paramref name="source"/>
/// on <paramref name="target"/>. Called each frame during drag to determine visual feedback. /// on <paramref name="target"/>. Called each frame during drag to determine visual feedback.
/// </summary> /// </summary>
bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target); bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target);
/// <summary> /// <summary>
/// Applies visual highlighting to both the <paramref name="actionCard"/> being dragged /// Applies visual highlighting to both the <paramref name="actionCard"/> being dragged
/// and the <paramref name="target"/> character card (e.g. red outline for attack, green for support). /// and the <paramref name="target"/> character card (e.g. red outline for attack, green for support).
/// </summary> /// </summary>
void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target); void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target);
/// <summary> /// <summary>
/// Executes the action via <see cref="ActionExecutorHub"/> and spawns floating damage/defence text. /// Executes the action via <see cref="ActionExecutorHub"/> and spawns floating damage/defence text.
/// Called on successful card drop by <see cref="CardController.OnEndDrag"/>. /// Called on successful card drop by <see cref="CardController.OnEndDrag"/>.
/// </summary> /// </summary>
void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action); void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action);
} }
} }
+49 -49
View File
@@ -1,49 +1,49 @@
using Cthangover.CardBattle.UI; using Cthangover.CardBattle.UI;
using Cthangover.Core.Battle.Actions; using Cthangover.Core.Battle.Actions;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
namespace Cthangover.CardBattle.Player namespace Cthangover.CardBattle.Player
{ {
/// <summary> /// <summary>
/// Strategy for stun actions targeting enemies. Extends the standard <c>ToEnemy</c> check /// Strategy for stun actions targeting enemies. Extends the standard <c>ToEnemy</c> check
/// by also verifying that the action has at least 2 turns of stun duration /// by also verifying that the action has at least 2 turns of stun duration
/// (<c>"Turn" > 1</c>). Highlights with red attack overlays and applies /// (<c>"Turn" > 1</c>). Highlights with red attack overlays and applies
/// the stun effect via <see cref="ActionExecutorHub"/> without spawning damage text /// the stun effect via <see cref="ActionExecutorHub"/> without spawning damage text
/// (stun has no numerical effect to display). /// (stun has no numerical effect to display).
/// Not currently returned by <see cref="CardActionStrategyFactory"/> — available for /// Not currently returned by <see cref="CardActionStrategyFactory"/> — available for
/// future use when stun cards need distinct drag-and-drop behavior from attack cards. /// future use when stun cards need distinct drag-and-drop behavior from attack cards.
/// </summary> /// </summary>
public class StunToEnemyStrategy : ICardActionStrategy public class StunToEnemyStrategy : ICardActionStrategy
{ {
/// <summary> /// <summary>
/// Returns <c>true</c> when the action targets an enemy and has a turn count greater than 1. /// Returns <c>true</c> when the action targets an enemy and has a turn count greater than 1.
/// </summary> /// </summary>
public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target) public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target)
{ {
return action.Type == ActionCharacterType.ToEnemy && target != null && !target.IsPlayer return action.Type == ActionCharacterType.ToEnemy && target != null && !target.IsPlayer
&& action.GetInt("Turn", 0) > 1; && action.GetInt("Turn", 0) > 1;
} }
/// <summary> /// <summary>
/// Shows a red attack overlay on both cards, same as <see cref="AttackToEnemyStrategy"/>. /// Shows a red attack overlay on both cards, same as <see cref="AttackToEnemyStrategy"/>.
/// </summary> /// </summary>
public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target) public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target)
{ {
actionCard.Attack(); actionCard.Attack();
target.Attack(); target.Attack();
} }
/// <summary> /// <summary>
/// Applies the stun via <see cref="ActionExecutorHub"/>. Does not spawn damage/defence text. /// Applies the stun via <see cref="ActionExecutorHub"/>. Does not spawn damage/defence text.
/// </summary> /// </summary>
public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action) public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action)
{ {
if (source?.Card == null || target?.Card == null || action == null) if (source?.Card == null || target?.Card == null || action == null)
return; return;
ActionExecutorHub.Instance.Execute(action, source.Card, target.Card); ActionExecutorHub.Instance.Execute(action, source.Card, target.Card);
source.UpdateInfo(); source.UpdateInfo();
target.UpdateInfo(); target.UpdateInfo();
} }
} }
} }
+21 -21
View File
@@ -1,21 +1,21 @@
namespace Cthangover.CardBattle.StatusEffect namespace Cthangover.CardBattle.StatusEffect
{ {
/// <summary> /// <summary>
/// Interface exposed by <see cref="StatusEffectQueue"/> to <see cref="IStatusEffect"/> instances /// Interface exposed by <see cref="StatusEffectQueue"/> to <see cref="IStatusEffect"/> instances
/// so that status effects can communicate back to the queue during turn lifecycle callbacks. /// so that status effects can communicate back to the queue during turn lifecycle callbacks.
/// <c>SkipTurn</c> allows an effect (e.g. stun) to mark the character's entire turn as skipped. /// <c>SkipTurn</c> allows an effect (e.g. stun) to mark the character's entire turn as skipped.
/// <c>RemoveStatus</c> allows an effect to self-remove from the queue. /// <c>RemoveStatus</c> allows an effect to self-remove from the queue.
/// </summary> /// </summary>
public interface IStatusActions public interface IStatusActions
{ {
/// <summary> /// <summary>
/// Marks the character's current turn as skipped. Called by <see cref="StunStatusEffect.OnTurnStart"/>. /// Marks the character's current turn as skipped. Called by <see cref="StunStatusEffect.OnTurnStart"/>.
/// </summary> /// </summary>
void SkipTurn(); void SkipTurn();
/// <summary> /// <summary>
/// Removes the specified status effect from the queue. Called by effects that want to /// Removes the specified status effect from the queue. Called by effects that want to
/// self-terminate before their turn count expires. /// self-terminate before their turn count expires.
/// </summary> /// </summary>
void RemoveStatus(IStatusEffect status); void RemoveStatus(IStatusEffect status);
} }
} }
+26 -26
View File
@@ -1,26 +1,26 @@
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
namespace Cthangover.CardBattle.StatusEffect namespace Cthangover.CardBattle.StatusEffect
{ {
/// <summary> /// <summary>
/// Defines a status effect that is applied to a <see cref="Character"/> during battle /// Defines a status effect that is applied to a <see cref="Character"/> during battle
/// and processed each turn by <see cref="StatusEffectQueue"/>. Effects receive callbacks /// and processed each turn by <see cref="StatusEffectQueue"/>. Effects receive callbacks
/// at turn start and turn end, and self-report expiration via <see cref="IsExpired"/> /// at turn start and turn end, and self-report expiration via <see cref="IsExpired"/>
/// when their <see cref="Turns"/> counter reaches zero. The <see cref="IStatusActions"/> /// when their <see cref="Turns"/> counter reaches zero. The <see cref="IStatusActions"/>
/// callback parameter allows effects to influence the character's turn (e.g. skip it). /// callback parameter allows effects to influence the character's turn (e.g. skip it).
/// </summary> /// </summary>
public interface IStatusEffect public interface IStatusEffect
{ {
/// <summary>Integer identifier for the effect type (1 = stun). Used for serialization.</summary> /// <summary>Integer identifier for the effect type (1 = stun). Used for serialization.</summary>
int Type { get; } int Type { get; }
/// <summary>Remaining turn count before expiry. Decremented by <see cref="StatusEffectQueue"/>.</summary> /// <summary>Remaining turn count before expiry. Decremented by <see cref="StatusEffectQueue"/>.</summary>
int Turns { get; set; } int Turns { get; set; }
/// <summary>Returns <c>true</c> when <see cref="Turns"/> ≤ 0, signalling removal from the queue.</summary> /// <summary>Returns <c>true</c> when <see cref="Turns"/> ≤ 0, signalling removal from the queue.</summary>
bool IsExpired { get; } bool IsExpired { get; }
/// <summary>Called by <see cref="StatusEffectQueue.OnTurnStart"/> before the turn counter is decremented.</summary> /// <summary>Called by <see cref="StatusEffectQueue.OnTurnStart"/> before the turn counter is decremented.</summary>
void OnTurnStart(Character character, IStatusActions actions); void OnTurnStart(Character character, IStatusActions actions);
/// <summary>Called by <see cref="StatusEffectQueue.OnTurnEnd"/> after the character's actions have resolved.</summary> /// <summary>Called by <see cref="StatusEffectQueue.OnTurnEnd"/> after the character's actions have resolved.</summary>
void OnTurnEnd(Character character, IStatusActions actions); void OnTurnEnd(Character character, IStatusActions actions);
} }
} }
+27 -27
View File
@@ -1,27 +1,27 @@
namespace Cthangover.CardBattle.StatusEffect namespace Cthangover.CardBattle.StatusEffect
{ {
/// <summary> /// <summary>
/// Data-only container for a status effect's serializable fields (<see cref="Type"/> and <see cref="Turns"/>). /// Data-only container for a status effect's serializable fields (<see cref="Type"/> and <see cref="Turns"/>).
/// Used in <see cref="StatusEffectQueue.Copy"/> to deep-clone status effect state when a character /// Used in <see cref="StatusEffectQueue.Copy"/> to deep-clone status effect state when a character
/// is copied for battle. Cast to <see cref="IStatusEffect"/> when added back to a queue. /// is copied for battle. Cast to <see cref="IStatusEffect"/> when added back to a queue.
/// </summary> /// </summary>
public class StatusEffectItem public class StatusEffectItem
{ {
/// <summary>Effect type identifier (matches <see cref="IStatusEffect.Type"/>).</summary> /// <summary>Effect type identifier (matches <see cref="IStatusEffect.Type"/>).</summary>
public int Type { get; set; } public int Type { get; set; }
/// <summary>Remaining turns before expiry.</summary> /// <summary>Remaining turns before expiry.</summary>
public int Turns { get; set; } public int Turns { get; set; }
/// <summary> /// <summary>
/// Creates a field-by-field copy for deep-cloning during character duplication. /// Creates a field-by-field copy for deep-cloning during character duplication.
/// </summary> /// </summary>
public StatusEffectItem Copy() public StatusEffectItem Copy()
{ {
return new StatusEffectItem return new StatusEffectItem
{ {
Type = Type, Type = Type,
Turns = Turns Turns = Turns
}; };
} }
} }
} }
+103 -103
View File
@@ -1,103 +1,103 @@
using System.Collections.Generic; using System.Collections.Generic;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
namespace Cthangover.CardBattle.StatusEffect namespace Cthangover.CardBattle.StatusEffect
{ {
/// <summary> /// <summary>
/// Manages the collection of <see cref="IStatusEffect"/> instances attached to a single <see cref="Character"/>. /// Manages the collection of <see cref="IStatusEffect"/> instances attached to a single <see cref="Character"/>.
/// Implements <see cref="IStatusActions"/> so that individual effects can call back into the queue /// Implements <see cref="IStatusActions"/> so that individual effects can call back into the queue
/// (e.g. to skip the turn). During <see cref="OnTurnStart"/>, each effect's callback is invoked and /// (e.g. to skip the turn). During <see cref="OnTurnStart"/>, each effect's callback is invoked and
/// its turn counter is decremented; expired effects are removed. <see cref="SkipTurn"/> is checked by /// its turn counter is decremented; expired effects are removed. <see cref="SkipTurn"/> is checked by
/// <see cref="CardBattleCore.RunEnemyTurn"/> to skip stunned enemies. /// <see cref="CardBattleCore.RunEnemyTurn"/> to skip stunned enemies.
/// Supports deep copying for character duplication via <see cref="Copy"/>. /// Supports deep copying for character duplication via <see cref="Copy"/>.
/// </summary> /// </summary>
public class StatusEffectQueue : IStatusActions public class StatusEffectQueue : IStatusActions
{ {
private readonly Character _character; private readonly Character _character;
private readonly List<IStatusEffect> _effects = new(); private readonly List<IStatusEffect> _effects = new();
/// <summary> /// <summary>
/// Set to <c>true</c> when an effect calls <see cref="IStatusActions.SkipTurn"/> during <see cref="OnTurnStart"/>. /// Set to <c>true</c> when an effect calls <see cref="IStatusActions.SkipTurn"/> during <see cref="OnTurnStart"/>.
/// Reset to <c>false</c> at the start of each turn before effects are processed. /// Reset to <c>false</c> at the start of each turn before effects are processed.
/// </summary> /// </summary>
public bool SkipTurn { get; private set; } public bool SkipTurn { get; private set; }
/// <summary> /// <summary>
/// Creates a new queue bound to the given <paramref name="character"/>. /// Creates a new queue bound to the given <paramref name="character"/>.
/// </summary> /// </summary>
public StatusEffectQueue(Character character) public StatusEffectQueue(Character character)
{ {
_character = character; _character = character;
} }
/// <summary> /// <summary>
/// Creates a deep copy of this queue bound to <paramref name="newCharacter"/>. /// Creates a deep copy of this queue bound to <paramref name="newCharacter"/>.
/// <see cref="StatusEffectItem"/> effects are field-copied; other effect types are shared by reference. /// <see cref="StatusEffectItem"/> effects are field-copied; other effect types are shared by reference.
/// </summary> /// </summary>
public StatusEffectQueue Copy(Character newCharacter) public StatusEffectQueue Copy(Character newCharacter)
{ {
var clone = new StatusEffectQueue(newCharacter); var clone = new StatusEffectQueue(newCharacter);
foreach (var effect in _effects) foreach (var effect in _effects)
{ {
if (effect is StatusEffectItem item) if (effect is StatusEffectItem item)
clone._effects.Add(item.Copy() as IStatusEffect); clone._effects.Add(item.Copy() as IStatusEffect);
else else
clone._effects.Add(effect); clone._effects.Add(effect);
} }
return clone; return clone;
} }
/// <summary> /// <summary>
/// Adds a new status effect to the end of the processing list. Effects are processed /// Adds a new status effect to the end of the processing list. Effects are processed
/// in reverse order during turn callbacks (last-added first). /// in reverse order during turn callbacks (last-added first).
/// </summary> /// </summary>
public void AddEffect(IStatusEffect effect) public void AddEffect(IStatusEffect effect)
{ {
_effects.Add(effect); _effects.Add(effect);
} }
/// <summary> /// <summary>
/// Processes all effects at the start of the character's turn: resets <see cref="SkipTurn"/>, /// Processes all effects at the start of the character's turn: resets <see cref="SkipTurn"/>,
/// calls <see cref="IStatusEffect.OnTurnStart"/> for each effect in reverse order, /// calls <see cref="IStatusEffect.OnTurnStart"/> for each effect in reverse order,
/// decrements the turn counter, and removes expired effects. /// decrements the turn counter, and removes expired effects.
/// Called by <see cref="CardBattleCore.StartPlayerTurn"/> and <see cref="CardBattleCore.RunEnemyTurn"/>. /// Called by <see cref="CardBattleCore.StartPlayerTurn"/> and <see cref="CardBattleCore.RunEnemyTurn"/>.
/// </summary> /// </summary>
public void OnTurnStart() public void OnTurnStart()
{ {
SkipTurn = false; SkipTurn = false;
for (int i = _effects.Count - 1; i >= 0; i--) for (int i = _effects.Count - 1; i >= 0; i--)
{ {
var effect = _effects[i]; var effect = _effects[i];
effect.OnTurnStart(_character, this); effect.OnTurnStart(_character, this);
effect.Turns--; effect.Turns--;
if (effect.IsExpired) if (effect.IsExpired)
_effects.RemoveAt(i); _effects.RemoveAt(i);
} }
} }
/// <summary> /// <summary>
/// Processes all effects at the end of the character's turn: calls /// Processes all effects at the end of the character's turn: calls
/// <see cref="IStatusEffect.OnTurnEnd"/> in reverse order and removes expired effects. /// <see cref="IStatusEffect.OnTurnEnd"/> in reverse order and removes expired effects.
/// </summary> /// </summary>
public void OnTurnEnd() public void OnTurnEnd()
{ {
for (int i = _effects.Count - 1; i >= 0; i--) for (int i = _effects.Count - 1; i >= 0; i--)
{ {
var effect = _effects[i]; var effect = _effects[i];
effect.OnTurnEnd(_character, this); effect.OnTurnEnd(_character, this);
if (effect.IsExpired) if (effect.IsExpired)
_effects.RemoveAt(i); _effects.RemoveAt(i);
} }
} }
void IStatusActions.SkipTurn() void IStatusActions.SkipTurn()
{ {
SkipTurn = true; SkipTurn = true;
} }
void IStatusActions.RemoveStatus(IStatusEffect status) void IStatusActions.RemoveStatus(IStatusEffect status)
{ {
_effects.Remove(status); _effects.Remove(status);
} }
} }
} }
+52 -52
View File
@@ -1,52 +1,52 @@
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
namespace Cthangover.CardBattle.StatusEffect namespace Cthangover.CardBattle.StatusEffect
{ {
/// <summary> /// <summary>
/// Status effect that causes a character to skip their entire turn. /// Status effect that causes a character to skip their entire turn.
/// On <see cref="OnTurnStart"/>, calls <see cref="IStatusActions.SkipTurn"/> which sets /// On <see cref="OnTurnStart"/>, calls <see cref="IStatusActions.SkipTurn"/> which sets
/// <see cref="StatusEffectQueue.SkipTurn"/> to <c>true</c>. <see cref="CardBattleCore.RunEnemyTurn"/> /// <see cref="StatusEffectQueue.SkipTurn"/> to <c>true</c>. <see cref="CardBattleCore.RunEnemyTurn"/>
/// checks this flag and skips the character's action selection and resolution entirely. /// checks this flag and skips the character's action selection and resolution entirely.
/// Created by <see cref="PhysicsStunActionCard"/> when a stun card is played, /// Created by <see cref="PhysicsStunActionCard"/> when a stun card is played,
/// and added to the target's <see cref="StatusEffectQueue"/> via <c>Add("effect/physics/stun", turns)</c>. /// and added to the target's <see cref="StatusEffectQueue"/> via <c>Add("effect/physics/stun", turns)</c>.
/// </summary> /// </summary>
public class StunStatusEffect : IStatusEffect public class StunStatusEffect : IStatusEffect
{ {
/// <summary>Type identifier for stun (1).</summary> /// <summary>Type identifier for stun (1).</summary>
public int Type => 1; public int Type => 1;
/// <summary>Number of turns this stun remains active.</summary> /// <summary>Number of turns this stun remains active.</summary>
public int Turns { get; set; } public int Turns { get; set; }
/// <summary>Expired when the turn counter reaches zero or below.</summary> /// <summary>Expired when the turn counter reaches zero or below.</summary>
public bool IsExpired => Turns <= 0; public bool IsExpired => Turns <= 0;
/// <summary> /// <summary>
/// Creates a stun effect lasting for <paramref name="turns"/> turns. /// Creates a stun effect lasting for <paramref name="turns"/> turns.
/// </summary> /// </summary>
public StunStatusEffect(int turns) public StunStatusEffect(int turns)
{ {
Turns = turns; Turns = turns;
} }
/// <summary> /// <summary>
/// Parameterless constructor for deserialization. Sets <see cref="Turns"/> to 0 (expired immediately). /// Parameterless constructor for deserialization. Sets <see cref="Turns"/> to 0 (expired immediately).
/// </summary> /// </summary>
public StunStatusEffect() public StunStatusEffect()
{ {
} }
/// <summary> /// <summary>
/// Calls <see cref="IStatusActions.SkipTurn"/> to mark the character's turn as skipped. /// Calls <see cref="IStatusActions.SkipTurn"/> to mark the character's turn as skipped.
/// </summary> /// </summary>
public void OnTurnStart(Character character, IStatusActions actions) public void OnTurnStart(Character character, IStatusActions actions)
{ {
actions.SkipTurn(); actions.SkipTurn();
} }
/// <summary> /// <summary>
/// Stun has no end-of-turn effect. /// Stun has no end-of-turn effect.
/// </summary> /// </summary>
public void OnTurnEnd(Character character, IStatusActions actions) public void OnTurnEnd(Character character, IStatusActions actions)
{ {
} }
} }
} }
+267 -267
View File
@@ -1,269 +1,269 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
using Cthangover.Core.Scenes; using Cthangover.Core.Scenes;
using Cthangover.Core.UI; using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists; using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.View; using Cthangover.Mods.Interface.View;
namespace Cthangover.CardBattle.UI namespace Cthangover.CardBattle.UI
{ {
/// <summary> /// <summary>
/// Container panel that manages a list of <see cref="CharacterCardNode"/> instances for one side /// Container panel that manages a list of <see cref="CharacterCardNode"/> instances for one side
/// of the battle (player or enemy). Handles card creation, multi-row layout when enemy count exceeds /// of the battle (player or enemy). Handles card creation, multi-row layout when enemy count exceeds
/// <c>MAX_CARDS_ROW</c> (10 per row), smooth animated repositioning, health-triggered death, /// <c>MAX_CARDS_ROW</c> (10 per row), smooth animated repositioning, health-triggered death,
/// and the <see cref="CardDeathAnimation"/> dissolve sequence. /// and the <see cref="CardDeathAnimation"/> dissolve sequence.
/// Two instances are created by <see cref="CardBattleCore.Init"/>: a left-aligned player panel /// Two instances are created by <see cref="CardBattleCore.Init"/>: a left-aligned player panel
/// and a right-aligned enemy panel. /// and a right-aligned enemy panel.
/// </summary> /// </summary>
public partial class BattleCardPanel : ModWidget public partial class BattleCardPanel : ModWidget
{ {
private const int MAX_CARDS_ROW = 10; private const int MAX_CARDS_ROW = 10;
private const float BASE_CARD_HEIGHT = 280f; private const float BASE_CARD_HEIGHT = 280f;
[Export] private Control content; [Export] private Control content;
/// <summary> /// <summary>
/// Controls card alignment within the panel. <c>"Left"</c> for the player side, /// Controls card alignment within the panel. <c>"Left"</c> for the player side,
/// <c>"Right"</c> for the enemy side. Affects <see cref="CalcTargetPosition"/>. /// <c>"Right"</c> for the enemy side. Affects <see cref="CalcTargetPosition"/>.
/// </summary> /// </summary>
[Export] public string AlignType { get; set; } = "Left"; [Export] public string AlignType { get; set; } = "Left";
[Export] private float cellOffset = 10f; [Export] private float cellOffset = 10f;
/// <summary> /// <summary>
/// All character cards currently managed by this panel. Populated by <see cref="Init"/> /// All character cards currently managed by this panel. Populated by <see cref="Init"/>
/// and modified by <see cref="Dead"/> (removal) and <see cref="ClearAll"/> (full clear). /// and modified by <see cref="Dead"/> (removal) and <see cref="ClearAll"/> (full clear).
/// </summary> /// </summary>
public List<CharacterCardNode> CardList { get; } = new(); public List<CharacterCardNode> CardList { get; } = new();
private Dictionary<CharacterCardNode, Tween> activeTweens = new(); private Dictionary<CharacterCardNode, Tween> activeTweens = new();
/// <summary> /// <summary>
/// When <c>true</c>, cards are laid out in multiple rows (up to 3) instead of a single row. /// When <c>true</c>, cards are laid out in multiple rows (up to 3) instead of a single row.
/// Set by <see cref="CardBattleCore.Start"/> when enemy count exceeds 6. /// Set by <see cref="CardBattleCore.Start"/> when enemy count exceeds 6.
/// </summary> /// </summary>
public bool UseRows { get; set; } public bool UseRows { get; set; }
/// <summary> /// <summary>
/// Fired when a card's death animation completes. <see cref="CardBattleCore"/> subscribes /// Fired when a card's death animation completes. <see cref="CardBattleCore"/> subscribes
/// to record the defeated character in <see cref="BattleSceneContext"/>. /// to record the defeated character in <see cref="BattleSceneContext"/>.
/// </summary> /// </summary>
public event System.Action<CharacterCardNode> OnCardDead; public event System.Action<CharacterCardNode> OnCardDead;
/// <summary>Builds the card grid container rows for one battle side.</summary> /// <summary>Builds the card grid container rows for one battle side.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
content ??= GetNodeOrNull<Control>("Content"); content ??= GetNodeOrNull<Control>("Content");
} }
/// <summary> /// <summary>
/// Clears the current card list and creates new <see cref="CharacterCardNode"/> instances /// Clears the current card list and creates new <see cref="CharacterCardNode"/> instances
/// for each <paramref name="cards"/> entry. Sets player/enemy team colors and subscribes /// for each <paramref name="cards"/> entry. Sets player/enemy team colors and subscribes
/// the health-change handler. Returns the created card list. /// the health-change handler. Returns the created card list.
/// Called by <see cref="CardBattleCore.Start"/> with the full character roster for each side. /// Called by <see cref="CardBattleCore.Start"/> with the full character roster for each side.
/// </summary> /// </summary>
public List<CharacterCardNode> Init(IEnumerable<Character> cards, bool isPlayer, float cardScale) public List<CharacterCardNode> Init(IEnumerable<Character> cards, bool isPlayer, float cardScale)
{ {
CardList.Clear(); CardList.Clear();
activeTweens.Clear(); activeTweens.Clear();
int idx = 0; int idx = 0;
foreach (var card in cards) foreach (var card in cards)
{ {
CardList.Add(Create(card, isPlayer, cardScale)); CardList.Add(Create(card, isPlayer, cardScale));
idx++; idx++;
} }
Redraw(animate: false); Redraw(animate: false);
return CardList; return CardList;
} }
/// <summary> /// <summary>
/// Instantiates a single <see cref="CharacterCardNode"/>, adds it to the content panel, /// Instantiates a single <see cref="CharacterCardNode"/>, adds it to the content panel,
/// sets its scale, initializes it with card data, applies team colors, and hooks /// sets its scale, initializes it with card data, applies team colors, and hooks
/// the health-change event. Called by <see cref="Init"/> for each character. /// the health-change event. Called by <see cref="Init"/> for each character.
/// </summary> /// </summary>
public CharacterCardNode Create(Character card, bool isPlayer, float cardScale) public CharacterCardNode Create(Character card, bool isPlayer, float cardScale)
{ {
var behaviour = (CharacterCardNode)TscnScenes.LoadAndBuild("scenes/character_card.tscn"); var behaviour = (CharacterCardNode)TscnScenes.LoadAndBuild("scenes/character_card.tscn");
behaviour.EnsureConstructed(); behaviour.EnsureConstructed();
if (content != null) if (content != null)
content.AddChild(behaviour); content.AddChild(behaviour);
else else
AddChild(behaviour); AddChild(behaviour);
behaviour.Scale = new Vector2(cardScale, cardScale); behaviour.Scale = new Vector2(cardScale, cardScale);
behaviour.Init(card); behaviour.Init(card);
behaviour.IsPlayer = isPlayer; behaviour.IsPlayer = isPlayer;
behaviour.SetTeamColors(); behaviour.SetTeamColors();
if (card != null) if (card != null)
card.Attributes.Health.OnChange += CheckHealthAttribute; card.Attributes.Health.OnChange += CheckHealthAttribute;
return behaviour; return behaviour;
} }
private void CheckHealthAttribute(float value, float basevalue) private void CheckHealthAttribute(float value, float basevalue)
{ {
if (value > 0) if (value > 0)
return; return;
foreach (var cardItem in CardList.ToList()) foreach (var cardItem in CardList.ToList())
{ {
if (cardItem.Card.Attributes.Health.Value <= 0) if (cardItem.Card.Attributes.Health.Value <= 0)
Dead(cardItem); Dead(cardItem);
} }
} }
/// <summary> /// <summary>
/// Frees all card nodes from the scene tree, clears the card list, and triggers a redraw. /// Frees all card nodes from the scene tree, clears the card list, and triggers a redraw.
/// Called by <see cref="CardBattleCore.OnBattleCleared"/> when the battle scene is cleaned up. /// Called by <see cref="CardBattleCore.OnBattleCleared"/> when the battle scene is cleaned up.
/// </summary> /// </summary>
public void ClearAll() public void ClearAll()
{ {
foreach (var card in CardList.ToList()) foreach (var card in CardList.ToList())
{ {
if (GodotObject.IsInstanceValid(card)) if (GodotObject.IsInstanceValid(card))
card.QueueFree(); card.QueueFree();
} }
CardList.Clear(); CardList.Clear();
activeTweens.Clear(); activeTweens.Clear();
Redraw(animate: false); Redraw(animate: false);
} }
/// <summary> /// <summary>
/// Recalculates and applies positions for all cards in the list. When <paramref name="animate"/> /// Recalculates and applies positions for all cards in the list. When <paramref name="animate"/>
/// is <c>true</c>, cards tween to their new positions with a cubic ease-out; otherwise they /// is <c>true</c>, cards tween to their new positions with a cubic ease-out; otherwise they
/// snap instantly. Called after cards are added, removed, or when a selection/unselection /// snap instantly. Called after cards are added, removed, or when a selection/unselection
/// changes the sort order. /// changes the sort order.
/// </summary> /// </summary>
public void Redraw(bool animate = true) public void Redraw(bool animate = true)
{ {
for (int i = 0; i < CardList.Count; i++) for (int i = 0; i < CardList.Count; i++)
{ {
var card = CardList[i]; var card = CardList[i];
if (card == null) if (card == null)
continue; continue;
var (row, col) = GetRowCol(i); var (row, col) = GetRowCol(i);
SetCardPosition(card, i, row, col, animate); SetCardPosition(card, i, row, col, animate);
} }
} }
private (int row, int col) GetRowCol(int index) private (int row, int col) GetRowCol(int index)
{ {
if (!UseRows || index < MAX_CARDS_ROW) if (!UseRows || index < MAX_CARDS_ROW)
return (0, index); return (0, index);
if (index < MAX_CARDS_ROW * 2) if (index < MAX_CARDS_ROW * 2)
return (1, index - MAX_CARDS_ROW); return (1, index - MAX_CARDS_ROW);
return (2, index - MAX_CARDS_ROW * 2); return (2, index - MAX_CARDS_ROW * 2);
} }
private int CalcBaseZIndex(int row, int col) private int CalcBaseZIndex(int row, int col)
{ {
return row * 20 + col; return row * 20 + col;
} }
private Vector2 CalcTargetPosition(CharacterCardNode card, int row, int col) private Vector2 CalcTargetPosition(CharacterCardNode card, int row, int col)
{ {
var panelSize = Size; var panelSize = Size;
float effectiveWidth = card.Size.X * card.Scale.X; float effectiveWidth = card.Size.X * card.Scale.X;
float effectiveHeight = card.Size.Y * card.Scale.Y; float effectiveHeight = card.Size.Y * card.Scale.Y;
float y = row * (effectiveHeight + cellOffset); float y = row * (effectiveHeight + cellOffset);
if (AlignType == "Left") if (AlignType == "Left")
return new Vector2((cellOffset + effectiveWidth) * col, y); return new Vector2((cellOffset + effectiveWidth) * col, y);
else else
return new Vector2(panelSize.X - (cellOffset + effectiveWidth) * (col + 1), y); return new Vector2(panelSize.X - (cellOffset + effectiveWidth) * (col + 1), y);
} }
/// <summary> /// <summary>
/// Calculates how many rows are needed to display <paramref name="enemyCount"/> enemies /// Calculates how many rows are needed to display <paramref name="enemyCount"/> enemies
/// when <see cref="UseRows"/> is enabled. Returns 1, 2, or 3. /// when <see cref="UseRows"/> is enabled. Returns 1, 2, or 3.
/// Called by <see cref="CardBattleCore.CalculateCardScale"/> to determine maximum card scale. /// Called by <see cref="CardBattleCore.CalculateCardScale"/> to determine maximum card scale.
/// </summary> /// </summary>
public int GetCurrentRowCount(int enemyCount) public int GetCurrentRowCount(int enemyCount)
{ {
if (!UseRows) if (!UseRows)
return 1; return 1;
if (enemyCount <= MAX_CARDS_ROW) if (enemyCount <= MAX_CARDS_ROW)
return 1; return 1;
if (enemyCount <= MAX_CARDS_ROW * 2) if (enemyCount <= MAX_CARDS_ROW * 2)
return 2; return 2;
return 3; return 3;
} }
/// <summary> /// <summary>
/// Returns the maximum cards per row constant (<c>MAX_CARDS_ROW = 10</c>). /// Returns the maximum cards per row constant (<c>MAX_CARDS_ROW = 10</c>).
/// Used by <see cref="CardBattleCore.CalculateCardScale"/> for scale calculations. /// Used by <see cref="CardBattleCore.CalculateCardScale"/> for scale calculations.
/// </summary> /// </summary>
public int GetMaxCardsPerRow() public int GetMaxCardsPerRow()
{ {
return MAX_CARDS_ROW; return MAX_CARDS_ROW;
} }
private void SetCardPosition(CharacterCardNode card, int index, int row, int col, bool animate) private void SetCardPosition(CharacterCardNode card, int index, int row, int col, bool animate)
{ {
if (card == null) if (card == null)
return; return;
var targetPos = CalcTargetPosition(card, row, col); var targetPos = CalcTargetPosition(card, row, col);
if (activeTweens.TryGetValue(card, out var prevTween) && prevTween != null && prevTween.IsValid()) if (activeTweens.TryGetValue(card, out var prevTween) && prevTween != null && prevTween.IsValid())
prevTween.Kill(); prevTween.Kill();
if (animate) if (animate)
{ {
var tween = CreateTween(); var tween = CreateTween();
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic); tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
tween.TweenProperty(card, "position", targetPos, 0.3f); tween.TweenProperty(card, "position", targetPos, 0.3f);
activeTweens[card] = tween; activeTweens[card] = tween;
} }
else else
{ {
card.Position = targetPos; card.Position = targetPos;
activeTweens.Remove(card); activeTweens.Remove(card);
} }
var baseZ = CalcBaseZIndex(row, col); var baseZ = CalcBaseZIndex(row, col);
card.SetBaseZIndex(baseZ); card.SetBaseZIndex(baseZ);
if (content != null) if (content != null)
content.MoveChild(card, index); content.MoveChild(card, index);
else else
MoveChild(card, index); MoveChild(card, index);
card.UpdateInfo(); card.UpdateInfo();
} }
/// <summary> /// <summary>
/// Marks the card as dead and starts the <see cref="CardDeathAnimation"/> dissolve sequence. /// Marks the card as dead and starts the <see cref="CardDeathAnimation"/> dissolve sequence.
/// Once the animation completes, the card is removed from <see cref="CardList"/>, /// Once the animation completes, the card is removed from <see cref="CardList"/>,
/// <see cref="OnCardDead"/> is fired, and the remaining cards are redrawn. /// <see cref="OnCardDead"/> is fired, and the remaining cards are redrawn.
/// Triggered by the health-change handler when a character's health reaches zero. /// Triggered by the health-change handler when a character's health reaches zero.
/// </summary> /// </summary>
public void Dead(CharacterCardNode characterCardNode) public void Dead(CharacterCardNode characterCardNode)
{ {
if (characterCardNode == null || characterCardNode.IsDead) if (characterCardNode == null || characterCardNode.IsDead)
return; return;
characterCardNode.IsDead = true; characterCardNode.IsDead = true;
var animator = new Cthangover.CardBattle.UI.CardDeathAnimation(); var animator = new Cthangover.CardBattle.UI.CardDeathAnimation();
characterCardNode.AddChild(animator); characterCardNode.AddChild(animator);
animator.StartAnimation(characterCardNode, () => animator.StartAnimation(characterCardNode, () =>
{ {
OnCardDead?.Invoke(characterCardNode); OnCardDead?.Invoke(characterCardNode);
CardList.Remove(characterCardNode); CardList.Remove(characterCardNode);
activeTweens.Remove(characterCardNode); activeTweens.Remove(characterCardNode);
Redraw(); Redraw();
}); });
} }
} }
} }
+101 -101
View File
@@ -1,101 +1,101 @@
using System; using System;
using Cthangover.Core.Mods; using Cthangover.Core.Mods;
using Cthangover.Core.Mods.Resolvers; using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.UI; using Cthangover.Core.UI;
using Godot; using Godot;
namespace Cthangover.CardBattle.UI namespace Cthangover.CardBattle.UI
{ {
/// <summary> /// <summary>
/// Plays a death animation on a <see cref="CharacterCardNode"/> when its health reaches zero. /// Plays a death animation on a <see cref="CharacterCardNode"/> when its health reaches zero.
/// The card simultaneously fades out, shrinks toward the screen center, spins 360 degrees, /// The card simultaneously fades out, shrinks toward the screen center, spins 360 degrees,
/// and applies a dissolve shader to all its <see cref="CharacterCardNode.AllImages"/> textures. /// and applies a dissolve shader to all its <see cref="CharacterCardNode.AllImages"/> textures.
/// The dissolve shader (<c>"scene_transition"</c>) is resolved once via <see cref="ModManager"/> /// The dissolve shader (<c>"scene_transition"</c>) is resolved once via <see cref="ModManager"/>
/// and cached statically. Created and started by <see cref="BattleCardPanel.Dead"/>. /// and cached statically. Created and started by <see cref="BattleCardPanel.Dead"/>.
/// </summary> /// </summary>
public partial class CardDeathAnimation : Node public partial class CardDeathAnimation : Node
{ {
private CharacterCardNode card; private CharacterCardNode card;
private Vector2 center; private Vector2 center;
private Action onComplete; private Action onComplete;
private static Shader dissolveShader; private static Shader dissolveShader;
/// <summary> /// <summary>
/// Begins the death animation sequence. Calculates a target position at the viewport center /// Begins the death animation sequence. Calculates a target position at the viewport center
/// (relative to the card's parent), starts fading all child <see cref="Label"/> nodes, /// (relative to the card's parent), starts fading all child <see cref="Label"/> nodes,
/// applies the dissolve shader material to all card images, and tweens position/rotation/alpha. /// applies the dissolve shader material to all card images, and tweens position/rotation/alpha.
/// On completion, invokes <paramref name="onComplete"/> and frees this node. /// On completion, invokes <paramref name="onComplete"/> and frees this node.
/// </summary> /// </summary>
public void StartAnimation(CharacterCardNode card, Action onComplete) public void StartAnimation(CharacterCardNode card, Action onComplete)
{ {
this.card = card; this.card = card;
this.onComplete = onComplete; this.onComplete = onComplete;
var viewportCenter = card.GetViewport().GetVisibleRect().Size / 2; var viewportCenter = card.GetViewport().GetVisibleRect().Size / 2;
center = card.GetParent<Control>() is Control parent center = card.GetParent<Control>() is Control parent
? viewportCenter - parent.GlobalPosition ? viewportCenter - parent.GlobalPosition
: viewportCenter - card.GlobalPosition + card.Position; : viewportCenter - card.GlobalPosition + card.Position;
RunAnimation(); RunAnimation();
} }
private void RunAnimation() private void RunAnimation()
{ {
float duration = 0.8f; float duration = 0.8f;
Vector2 endPos = center; Vector2 endPos = center;
StartFadeOutRecursive(card, duration); StartFadeOutRecursive(card, duration);
if (dissolveShader == null) if (dissolveShader == null)
dissolveShader = Shaders.Resolve("scene_transition"); dissolveShader = Shaders.Resolve("scene_transition");
if (dissolveShader != null) if (dissolveShader != null)
{ {
foreach (var img in card.AllImages) foreach (var img in card.AllImages)
{ {
var mat = new ShaderMaterial { Shader = dissolveShader }; var mat = new ShaderMaterial { Shader = dissolveShader };
mat.SetShaderParameter("noise_scale", 5f); mat.SetShaderParameter("noise_scale", 5f);
mat.SetShaderParameter("smoothness", 0.25f); mat.SetShaderParameter("smoothness", 0.25f);
mat.SetShaderParameter("distortion", 0.092f); mat.SetShaderParameter("distortion", 0.092f);
mat.SetShaderParameter("glow_intensity", 0f); mat.SetShaderParameter("glow_intensity", 0f);
mat.SetShaderParameter("hue_shift", 0f); mat.SetShaderParameter("hue_shift", 0f);
mat.SetShaderParameter("invert_direction", false); mat.SetShaderParameter("invert_direction", false);
mat.SetShaderParameter("progress", 0f); mat.SetShaderParameter("progress", 0f);
img.Material = mat; img.Material = mat;
var matTween = CreateTween(); var matTween = CreateTween();
matTween.TweenMethod( matTween.TweenMethod(
Callable.From<float>(t => mat.SetShaderParameter("progress", t)), Callable.From<float>(t => mat.SetShaderParameter("progress", t)),
0f, 1f, duration); 0f, 1f, duration);
} }
} }
var tween = CreateTween(); var tween = CreateTween();
tween.SetParallel(true); tween.SetParallel(true);
tween.TweenProperty(card, "position", endPos, duration); tween.TweenProperty(card, "position", endPos, duration);
tween.TweenProperty(card, "rotation", Mathf.DegToRad(360f), duration); tween.TweenProperty(card, "rotation", Mathf.DegToRad(360f), duration);
tween.TweenProperty(card, "modulate:a", 0f, duration); tween.TweenProperty(card, "modulate:a", 0f, duration);
tween.Finished += () => tween.Finished += () =>
{ {
onComplete?.Invoke(); onComplete?.Invoke();
QueueFree(); QueueFree();
}; };
} }
private static void StartFadeOutRecursive(Node node, float duration) private static void StartFadeOutRecursive(Node node, float duration)
{ {
foreach (Node child in node.GetChildren()) foreach (Node child in node.GetChildren())
{ {
if (child is Label label) if (child is Label label)
{ {
var t = child.CreateTween(); var t = child.CreateTween();
t.TweenProperty(label, "modulate:a", 0f, duration); t.TweenProperty(label, "modulate:a", 0f, duration);
} }
StartFadeOutRecursive(child, duration); StartFadeOutRecursive(child, duration);
} }
} }
} }
} }
+47 -47
View File
@@ -1,48 +1,48 @@
using Cthangover.Core.UI; using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists; using Cthangover.Mods.Interface.Base.Lists;
namespace Cthangover.CardBattle.UI namespace Cthangover.CardBattle.UI
{ {
/// <summary> /// <summary>
/// A styled button widget that allows the player to manually end their turn. /// A styled button widget that allows the player to manually end their turn.
/// Displayed during the player's turn and hidden during enemy turns and battle cleanup. /// Displayed during the player's turn and hidden during enemy turns and battle cleanup.
/// Fires <see cref="OnPressed"/> when clicked, which <see cref="CardBattleCore"/> binds /// Fires <see cref="OnPressed"/> when clicked, which <see cref="CardBattleCore"/> binds
/// to <c>OnPlayerTurnEnd</c>. The button text is translated via the <c>"battle/end_turn"</c> key. /// to <c>OnPlayerTurnEnd</c>. The button text is translated via the <c>"battle/end_turn"</c> key.
/// </summary> /// </summary>
public partial class EndTurnButton : ModWidget public partial class EndTurnButton : ModWidget
{ {
private Button _button; private Button _button;
/// <summary> /// <summary>
/// Fired when the button is pressed. Subscribed by <see cref="CardBattleCore.OnEndTurnPressed"/>. /// Fired when the button is pressed. Subscribed by <see cref="CardBattleCore.OnEndTurnPressed"/>.
/// </summary> /// </summary>
public event System.Action OnPressed; public event System.Action OnPressed;
/// <summary>Builds the end-turn button and binds its pressed event.</summary> /// <summary>Builds the end-turn button and binds its pressed event.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
_button = GetNode<Button>("BtnEndTurn"); _button = GetNode<Button>("BtnEndTurn");
CthangoverTheme.ApplyPrimary(_button); CthangoverTheme.ApplyPrimary(_button);
Size = CustomMinimumSize; Size = CustomMinimumSize;
_button.Pressed += RaisePressed; _button.Pressed += RaisePressed;
} }
private void RaisePressed() => OnPressed?.Invoke(); private void RaisePressed() => OnPressed?.Invoke();
/// <summary> /// <summary>
/// Shows or hides the underlying <see cref="Button"/> control. /// Shows or hides the underlying <see cref="Button"/> control.
/// Called by <see cref="CardBattleCore"/> during turn transitions and battle cleanup. /// Called by <see cref="CardBattleCore"/> during turn transitions and battle cleanup.
/// </summary> /// </summary>
public void SetVisible(bool visible) public void SetVisible(bool visible)
{ {
if (_button != null) if (_button != null)
_button.Visible = visible; _button.Visible = visible;
} }
} }
} }