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