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 is the shared implementation used by both the "legacy" (no /// ) NPC idle-cycle path and the chargen /// preview. The live presenter 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 neither consumer can /// own the primitive. Keeping it here prevents the two paths from drifting. /// /// public static class RetailAnimationCyclePlayback { /// /// Advances by elapsedSeconds * framerate /// and wraps it back into [lowFrame, highFrame] with the SAME modulo /// shape retail playback and the live presenter's legacy branch use /// (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 /// ( <= ) or a /// non-positive . A negative framerate /// advances backward and clamps at the low frame, matching the former /// live-presenter implementation exactly. /// public static float Advance( float currFrame, int lowFrame, int highFrame, float framerate, float elapsedSeconds) { int span = highFrame - lowFrame; if (span <= 0 || 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 /// the live presenter'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; } }