Files
mod_ff_battle/src/Actions/FFAbstractAnimation.cs
T
iisaevandClaude Opus 5.5 00aa70e302 upd: sync local changes
Fix enemy turn hanging when a lethal hit frees the target widget:
UpdateInfo skips freed widgets, animations finish early and restore
the attacker only while it is alive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:18:14 +00:00

95 lines
4.7 KiB
C#

using Cthangover.Core.Battle.Actions;
using Cthangover.Core.Characters;
using Cthangover.FFBattle.UI;
using Godot;
namespace Cthangover.FFBattle.Actions
{
/// <summary>
/// Abstract base for all FF battle animations. Implements a three-phase lifecycle
/// (<see cref="DoStart"/>, <see cref="DoAction"/>, <see cref="DoEnd"/>) driven
/// by the battle core's per-frame loop. Subclasses implement their animation logic
/// in <c>DoInternalStart/DoInternalAction/DoInternalEnd</c> and are instantiated
/// by <see cref="FFBattleCore"/> when a player or enemy action is executed.
/// The <see cref="Speed"/> multiplier scales all timing, used e.g. to speed up
/// enemy turn animations (<c>0.8f</c>).
/// </summary>
public abstract class FFAbstractAnimation : IBattleAction
{
/// <summary>Start time of the current animation phase, in seconds (microsecond precision).</summary>
protected double Timestamp { get; set; } = -1;
/// <summary>Animation speed multiplier; lower values produce faster animations.</summary>
protected float Speed { get; set; } = 1f;
/// <summary>The action descriptor being executed (determines which executor runs).</summary>
protected ActionCharacter Action { get; set; }
/// <summary>The character widget performing the action.</summary>
protected FFCharacterWidget Source { get; set; }
/// <summary>The character widget receiving the action.</summary>
protected FFCharacterWidget Target { get; set; }
/// <summary>World position of <see cref="Source"/> captured at <see cref="DoStart"/>.</summary>
protected Vector2 SourcePos { get; set; }
/// <summary>World position of <see cref="Target"/> captured at <see cref="DoStart"/>.</summary>
protected Vector2 TargetPos { get; set; }
/// <summary>Creates an animation binding a source, target, and action with an optional speed override.</summary>
protected FFAbstractAnimation(FFCharacterWidget source, FFCharacterWidget target, ActionCharacter action, float speed = 1f)
{
Source = source;
Target = target;
Action = action;
Speed = speed;
}
/// <summary>
/// Advances the animation by one frame. Returns <c>true</c> when the animation
/// sequence has completed. Called in a tight <c>while</c> loop by
/// <see cref="FFBattleCore.RunAnimation"/> each process frame.
/// </summary>
public bool DoAction()
{
if (Source == null || Target == null || Action == null)
return true;
// The target's death animation may free its widget mid-animation;
// finish early and let DoEnd snap the source back.
if (!IsAlive(Source) || !IsAlive(Target))
return true;
return DoInternalAction();
}
/// <summary>True while the widget is still in the tree and not queued for deletion.</summary>
protected static bool IsAlive(FFCharacterWidget widget)
=> GodotObject.IsInstanceValid(widget) && !widget.IsQueuedForDeletion();
/// <summary>Captures the initial world positions of source and target, then delegates to subclass setup.</summary>
public void DoStart()
{
if (Source == null || Target == null || Action == null)
return;
SourcePos = Source.GlobalPosition;
TargetPos = Target.GlobalPosition;
DoInternalStart();
}
/// <summary>Restores source to original position and delegates cleanup to subclass.</summary>
public void DoEnd()
{
if (Source == null || Target == null || Action == null)
return;
DoInternalEnd();
}
/// <summary>Frame tick of the animation; implementors advance phases and return true when finished.</summary>
protected abstract bool DoInternalAction();
/// <summary>Phase setup called once by <see cref="DoStart"/> after positions are captured.</summary>
protected abstract void DoInternalStart();
/// <summary>Cleanup called once by <see cref="DoEnd"/> — restore captured positions here.</summary>
protected abstract void DoInternalEnd();
/// <summary>Quadratic ease-out: fast start, decelerating to the target. Used for return-to-start motion.</summary>
protected float EaseOutQuad(float t) => 1f - (1f - t) * (1f - t);
/// <summary>Quadratic ease-in-out: slow start and end, fast in the middle. Used for approach motion.</summary>
protected float EaseInOutQuad(float t) => t < 0.5f ? 2f * t * t : 1f - Mathf.Pow(-2f * t + 2f, 2f) / 2f;
}
}