using System.Numerics;
namespace AcDream.App.Rendering;
///
/// Presentation-free port of gmCGAppearancePage::ZoomIn/ZoomOut
/// (0x0047CF00/0x0047D050) and DoZoomAnimation
/// (0x0047C960): a linear 0.6s tween of the preview camera's eye
/// between (zoomed IN)
/// and (zoomed OUT),
/// driving the SAME zoom-state swap the
/// button presses trigger in retail — immediately, not once the tween
/// finishes (see 's own doc comment).
///
///
/// One owner of the zoom state (fix round F2): retail's
/// m_bZoomedIn is a SINGLE field on gmCGAppearancePage that
/// gates both the camera target AND the animation swap — there is no way
/// for retail's own camera and animation to disagree about which zoom state
/// they're in. The first cut of this port kept two independent bools (one
/// here, one on ) synced only by
/// / calling a NULLABLE animator
/// parameter — a null pass, or any direct
/// call bypassing this
/// controller, would desync the camera's target from the animation's pose.
/// This class now takes its as a
/// REQUIRED constructor dependency and reads
/// straight through to — the
/// animator is the sole state owner, matching retail's own single-field
/// design, and there is no longer a second bool that could disagree with it.
///
///
///
/// Retail drives once per frame from a global-message-3
/// tick while m_bShouldZoomAnimate is set
/// (gmCGAppearancePage::ListenToGlobalMessage @ 0x0047CED0); the
/// CC6b page-mount half will bind the Zoom In/Out buttons to
/// / and the render loop to
/// . Direction is always (0,0,0) for this camera
/// (see 's own remarks), so only the eye
/// position tweens — retail's own m_vectCurDirection lerp is a no-op
/// here and is not reproduced.
///
///
internal sealed class ChargenPreviewZoomController
{
///
/// ZoomIn/ZoomOut's explicit invalidation write
/// (this->m_dAnimDuration = -0.1, pseudo-C ~0x0047cff1/0x0047cffb
/// and ~0x0047d12c/0x0047d136 — the exact IEEE-754 bit pattern for
/// -0.1) so the very next resets the duration
/// to and the
/// start time to "now", matching DoZoomAnimation's own
/// reset-if-invalid guard exactly.
///
private const double InvalidDurationSentinel = -0.1;
private readonly uint _heritageId;
private readonly ChargenPreviewAnimator _animator;
private Vector3 _startEye;
private Vector3 _targetEye;
private double _animStartTime;
private double _animDuration;
private bool _shouldAnimate;
public ChargenPreviewZoomController(uint heritageId, ChargenPreviewCamera camera, ChargenPreviewAnimator animator)
{
ArgumentNullException.ThrowIfNull(camera);
ArgumentNullException.ThrowIfNull(animator);
_heritageId = heritageId;
Camera = camera;
_animator = animator;
}
public ChargenPreviewCamera Camera { get; }
///
/// Mirrors retail's m_bZoomedIn — a straight read-through to
/// (see this class's own
/// "one owner" doc above), which itself defaults false per
/// gmCGAppearancePage::InitializePage @ 0x0047FDD0's explicit
/// this->m_bZoomedIn = 0; at 0x004802C3 — written right
/// after that same function points the camera at the zoomed-IN
/// per-heritage eye (0x00480286-0x0048029E). One retail quirk
/// this produces: the character starts framed close-up while
/// NOT-zoomed-in, so the first Zoom In click (once mounted) tweens
/// close-eye→close-eye — visually null — while still freezing the
/// animation; this port reproduces it faithfully.
///
public bool IsZoomedIn => _animator.IsZoomedIn;
///
/// gmCGAppearancePage::ZoomIn @ 0x0047CF00: no-op if already
/// zoomed in (retail's own early-return guard). Otherwise starts a tween
/// from the camera's CURRENT eye to the default (zoomed-IN) per-heritage
/// profile and swaps the animator to the frozen rest pose IMMEDIATELY
/// (gmCG3DView::StopAnimation's call site, pseudo-C ~0x0047d024,
/// precedes the tween's own completion by definition — it runs once,
/// synchronously, inside ZoomIn itself).
///
public void ZoomIn()
{
if (IsZoomedIn)
return;
StartTween(ChargenPreviewCamera.ResolveDefaultEye(_heritageId));
_animator.SetZoomedIn(true);
}
///
/// gmCGAppearancePage::ZoomOut @ 0x0047D050: no-op if not
/// currently zoomed in. Otherwise starts a tween toward the zoomed-OUT
/// per-heritage profile and swaps the animator back to the playing idle
/// loop immediately, mirroring .
///
public void ZoomOut()
{
if (!IsZoomedIn)
return;
StartTween(ChargenPreviewCamera.ResolveZoomedOutEye(_heritageId));
_animator.SetZoomedIn(false);
}
private void StartTween(Vector3 targetEye)
{
_startEye = Camera.Eye;
_targetEye = targetEye;
_shouldAnimate = true;
_animDuration = InvalidDurationSentinel;
}
///
/// gmCGAppearancePage::DoZoomAnimation @ 0x0047C960: a LINEAR
/// (not eased) lerp of the eye position from m_vectStartPosition
/// to m_vectTargPosition over
/// , clamping
/// t to exactly 1.0 (and clearing m_bShouldZoomAnimate) the
/// tick that reaches or passes the duration — the decomp shows a
/// straight (targ - start) * t + start per axis with no easing
/// curve applied anywhere in this function.
///
public void Tick(double now)
{
if (!_shouldAnimate)
return;
if (_animDuration <= 0d)
{
_animDuration = ChargenPreviewCamera.ZoomTweenDurationSeconds;
_animStartTime = now;
}
double elapsed = now - _animStartTime;
if (elapsed >= _animDuration)
{
_shouldAnimate = false;
elapsed = _animDuration;
}
float t = (float)(elapsed / _animDuration);
Camera.Eye = Vector3.Lerp(_startEye, _targetEye, t);
}
}