using System;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
namespace AcDream.Core.Physics.Motion;
///
/// R1-P1 — verbatim port of retail's AnimSequenceNode (Phase R plan
/// `docs/plans/2026-07-02-retail-motion-animation-rewrite.md`, stage R1;
/// oracle `docs/research/2026-07-02-r1-csequence/r1-csequence-decomp.md`
/// §25-28).
///
/// One node of a CSequence's animation list: a resolved dat
/// plus a playable frame window
/// (..) and a signed
/// whose SIGN is retail's playback-direction flag.
///
/// Retail semantics preserved exactly (gap-map items G1/G2/G16/G18):
///
/// - The boundary pair ( /
/// ) is direction-aware and returns BARE
/// integers — retail has NO epsilon here (0x00525c80/0x00525cb0). ACE's
/// PhysicsGlobals.EPSILON subtraction compensates for ACE's own
/// float FrameNumber and must not be copied (P0-pins.md).
/// - SWAPS and
/// when the factor is negative (0x00525be0) —
/// coupled with the boundary pair's framerate < 0 test.
/// - clamps in retail's exact order
/// (0x00525d60): high<0 → num−1; low≥num → num−1; high≥num → num−1;
/// low>high → high=low.
/// - returns null out of range
/// (retail; ACE's identity-frame return is an ACE-ism).
///
///
/// Unlike the legacy AnimationSequencer.AnimNode, retail nodes carry
/// NO per-node IsLooping/Velocity/Omega — loop membership is list structure
/// (first_cyclic) and physics accumulators live on the sequence
/// (G16).
///
public sealed class AnimSequenceNode
{
/// Resolved dat animation, or null (id 0 / missing).
public Animation? Anim { get; private set; }
///
/// Frames per second; NEGATIVE means reverse playback (retail's
/// direction flag). Default 30f (0x00525d30).
///
public float Framerate = 30f;
/// Inclusive window low bound. Default −1 (0x00525d30).
public int LowFrame = -1;
/// Inclusive window high bound; −1 = "to the end" sentinel
/// resolved by . Default −1.
public int HighFrame = -1;
public bool HasAnim => Anim is not null;
/// Default ctor — retail defaults (0x00525d30).
public AnimSequenceNode()
{
}
///
/// Ctor from a MotionData entry (0x00525f90):
/// copy framerate/low/high, then resolve + clamp via
/// .
///
public AnimSequenceNode(AnimData animData, IAnimationLoader loader)
{
Framerate = animData.Framerate;
LowFrame = animData.LowFrame;
HighFrame = animData.HighFrame;
SetAnimationId((uint)animData.AnimId, loader);
}
///
/// Resolve the dat animation and clamp the frame window
/// (0x00525d60). The clamp block runs only when an animation resolved;
/// order is retail-exact.
///
public void SetAnimationId(uint animId, IAnimationLoader loader)
{
Anim = animId == 0 ? null : loader.LoadAnimation(animId);
if (Anim is null)
return;
int numFrames = Anim.PartFrames.Count;
if (HighFrame < 0)
HighFrame = numFrames - 1;
if (LowFrame >= numFrames)
LowFrame = numFrames - 1;
if (HighFrame >= numFrames)
HighFrame = numFrames - 1;
if (LowFrame > HighFrame)
HighFrame = LowFrame;
}
///
/// Direction-aware starting boundary (0x00525c80): reverse playback
/// starts at high_frame + 1, forward at low_frame.
/// BARE int — no epsilon (G1).
///
public int GetStartingFrame() => Framerate < 0f ? HighFrame + 1 : LowFrame;
///
/// Direction-aware ending boundary (0x00525cb0): reverse ends at
/// low_frame, forward at high_frame + 1. BARE int.
///
public int GetEndingFrame() => Framerate < 0f ? LowFrame : HighFrame + 1;
///
/// Scale playback rate (0x00525be0): a NEGATIVE factor swaps
/// low/high before multiplying — the swapped fields plus the
/// now-negative framerate are how retail encodes reversed windows.
///
public void MultiplyFramerate(float factor)
{
if (factor < 0f)
{
(LowFrame, HighFrame) = (HighFrame, LowFrame);
}
Framerate *= factor;
}
///
/// Root-motion frame at a double position (0x005247b0): floor then the
/// int overload.
///
public Frame? GetPosFrame(double frameNumber)
=> GetPosFrame((int)Math.Floor(frameNumber));
///
/// Root-motion frame by index (0x00525c10): null when no animation,
/// index out of range, or the animation carries no PosFrames.
///
public Frame? GetPosFrame(int index)
{
if (Anim is null || index < 0 || index >= Anim.PartFrames.Count)
return null;
if (Anim.PosFrames is null || index >= Anim.PosFrames.Count)
return null;
return Anim.PosFrames[index];
}
///
/// Skeletal part frame by index — same bounds discipline as
/// .
///
public AnimationFrame? GetPartFrame(int index)
{
if (Anim is null || index < 0 || index >= Anim.PartFrames.Count)
return null;
return Anim.PartFrames[index];
}
}