using System;
using System.Numerics;
using DatReaderWriter.DBObjs;
namespace AcDream.Core.Physics;
///
/// Retail's simplest animation-clip playback shape: advance a frame position
/// at a fixed framerate and wrap it back into [LowFrame, HighFrame],
/// then linearly interpolate one part's origin/orientation between the two
/// bracketing frames. This is the effect of
/// CPhysicsObj::set_sequence_animation (0x0050F6F0) when called
/// with a constant DID and a nonzero framerate and no further motion-command
/// traffic — e.g. gmCG3DView::StartAnimation (0x004EE600),
/// which plays the chargen preview's idle DID at a flat 30 fps with no
/// transitional blending.
///
///
/// This exact advance-with-wrap-then-lerp/slerp algorithm already exists as
/// an inline, App-layer-only implementation for the "legacy" (no
/// ) NPC idle-cycle path —
/// LiveEntityAnimationPresenter.Present's non-sequencer branch
/// (CurrFrame += legacyAdvanceSeconds * Framerate with the same
/// modulo wrap) and its private TryResolvePartFrame helper (the same
/// frame-bracket lerp/slerp). That call site has a live entity, a
/// LiveEntityRuntime membership, and per-tick elapsed time supplied by
/// the render loop; the chargen preview has none of that (there is no live
/// entity — character creation hasn't happened yet), so it cannot reuse that
/// class directly. Rather than re-typing the same formula a second time,
/// this Core, pure, unit-testable class is the shared primitive: the
/// chargen preview (AcDream.App.Rendering.ChargenPreviewAnimator)
/// consumes it directly, and it is safe for a future pass to redirect
/// LiveEntityAnimationPresenter's inline copy through it as a
/// behavior-preserving mechanical follow-up (not done here — that file is
/// live, heavily tested production entity-rendering code with zero relation
/// to this preview-only feature, so touching it is out of this slice's
/// blast radius by design, not oversight). Tracked as
/// docs/ISSUES.md #403 so the follow-up has an owner.
///
///
public static class RetailAnimationCyclePlayback
{
///
/// Advances by elapsedSeconds * framerate
/// and wraps it back into [lowFrame, highFrame] with the SAME modulo
/// shape LiveEntityAnimationPresenter.Present's legacy branch uses
/// (over % (span + 1), not a plain clamp — a frame position that
/// overshoots the end by more than one span wraps around more than once
/// rather than sticking at the boundary, matching a long stall/resume).
/// Returns unchanged for a degenerate cycle
/// ( <= ), a
/// non-positive , or a non-positive
/// .
///
public static float Advance(
float currFrame,
int lowFrame,
int highFrame,
float framerate,
float elapsedSeconds)
{
int span = highFrame - lowFrame;
if (span <= 0 || framerate <= 0f || elapsedSeconds <= 0f)
return currFrame;
float next = currFrame + elapsedSeconds * framerate;
if (next > highFrame)
{
float over = next - lowFrame;
next = lowFrame + (over % (span + 1));
}
else if (next < lowFrame)
{
next = lowFrame;
}
return next;
}
///
/// Resolves part 's origin/orientation at
/// by linearly interpolating (lerp origin,
/// slerp orientation) between the frame at floor(currFrame) and
/// the next frame in the cycle (wrapping +1
/// back to ). Returns false — with
/// default outputs — when is outside
/// the bracketing frame's part list, matching
/// LiveEntityAnimationPresenter.TryResolvePartFrame's no-
/// sequence-frames branch exactly.
///
public static bool TryInterpolatePart(
Animation animation,
float currFrame,
int lowFrame,
int highFrame,
int partIndex,
out Vector3 origin,
out Quaternion orientation)
{
ArgumentNullException.ThrowIfNull(animation);
int frameIndex = (int)MathF.Floor(currFrame);
if (frameIndex < lowFrame || frameIndex > highFrame || frameIndex >= animation.PartFrames.Count)
frameIndex = lowFrame;
int nextIndex = frameIndex + 1;
if (nextIndex > highFrame || nextIndex >= animation.PartFrames.Count)
nextIndex = lowFrame;
float t = Math.Clamp(currFrame - frameIndex, 0f, 1f);
var frames = animation.PartFrames[frameIndex].Frames;
var nextFrames = animation.PartFrames[nextIndex].Frames;
if (partIndex < frames.Count)
{
var first = frames[partIndex];
var next = partIndex < nextFrames.Count ? nextFrames[partIndex] : first;
origin = Vector3.Lerp(first.Origin, next.Origin, t);
orientation = Quaternion.Slerp(first.Orientation, next.Orientation, t);
return true;
}
origin = default;
orientation = default;
return false;
}
}