using System;
using System.Numerics;
using AcDream.Core.CharGen;
namespace AcDream.App.Rendering;
///
/// Heritage-parameterized camera for the chargen 3D preview
/// (gmCG3DView, Appearance page viewport 0x100003bb / Summary
/// 0x10000406). Retail-exact eye positions, ported from
/// gmCGAppearancePage::Update @ 0x0047E8F0 (pseudo-C ~139037-139114,
/// which sets m_vectTargPosition/m_vectCurPosition per
/// heritage and snaps them together with no tween — CC6a's static preview
/// renders that snapped default, the "zoomed-in" framing) and cross-checked
/// against the IDENTICAL literals in gmCGAppearancePage::ZoomIn @
/// 0x0047CF00 (pseudo-C ~137618-137638). Direction is always
/// (0,0,0) ⇒ CreatureMode::SetCameraDirection resets the view
/// frame to IDENTITY — the SAME zero-yaw/zero-pitch convention
/// already established for the paperdoll (look
/// straight down +Y, +Z up); every camera position below is used AS the
/// world-space eye directly, matching that camera's approach.
///
///
/// Rotation is NOT a camera property. Retail's continuous-rotation
/// button (gmCGAppearancePage::DoRotation @ 0x0047CA80) advances a
/// HEADING applied to the preview CHARACTER (CPhysicsObj::set_heading
/// inside gmCG3DView::Update, pseudo-C ~242088) — the camera's own
/// position/direction never change during a rotation. The heading itself
/// lives on (CC6b: the
/// DoRotation/Rotate port) and is applied to the entity via
/// ChargenPreviewEntityBuilder.TryBuild/TryBuildAnimated's
/// heading parameter, not here; this class stays a fixed-per-heritage
/// eye, exactly like retail's own camera.
/// (CC6b: the ZoomIn/ZoomOut/DoZoomAnimation port) DOES
/// mutate this class's — zoom is a camera concern, unlike
/// rotation.
///
///
public sealed class ChargenPreviewCamera : ICamera
{
private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as DollCamera/ChaseCamera.
private Vector3 _eye;
public ChargenPreviewCamera(uint heritageId = 0u)
{
_eye = ResolveDefaultEye(heritageId);
}
///
/// The camera's current world-space eye. Settable so CC6b can react to a
/// heritage change without reconstructing the camera.
///
public Vector3 Eye
{
get => _eye;
set => _eye = value;
}
/// Re-derives for the given heritage id (retail's mHeritageGroup).
public void SetHeritage(uint heritageId) => _eye = ResolveDefaultEye(heritageId);
///
/// Retail default (zoomed-in) camera eye per heritage. All four profiles
/// share X=0; only (Y, Z) — the AC world-space forward
/// offset and height — vary. FOUR distinct profiles across the 13
/// heritages, not five: standard heritages (Aluvian, Gharu'ndim, Sho,
/// Viamontian, Shadowbound, Gearknight, Lugian, Empyrean, Penumbraen,
/// Undead — everything except Tumerok/Olthoi/OlthoiAcid) share the SAME
/// numeric offset as Gearknight's own dedicated branch in the decomp.
///
public static Vector3 ResolveDefaultEye(uint heritageId) => heritageId switch
{
(uint)ChargenHeritageGroup.Olthoi => new Vector3(0f, -1.85000002f, 1.85000002f),
(uint)ChargenHeritageGroup.OlthoiAcid => new Vector3(0f, -3.04999995f, 2.75f),
(uint)ChargenHeritageGroup.Tumerok => new Vector3(0f, -0.850000024f, 1.64999998f),
_ => new Vector3(0f, -0.550000012f, 1.64999998f),
};
///
/// Retail zoomed-OUT camera eye per heritage
/// (gmCGAppearancePage::ZoomOut @ 0x0047D050, pseudo-C
/// ~137671-137687). CC6a does not implement the zoom button (CC6b) —
/// recorded here as the verified target CC6b's tween will animate
/// toward. Olthoi/OlthoiAcid each keep their own dedicated profile;
/// every other heritage — INCLUDING Tumerok, whose zoomed-IN profile is
/// special-cased but whose zoomed-OUT is not — shares one value.
///
public static Vector3 ResolveZoomedOutEye(uint heritageId) => heritageId switch
{
(uint)ChargenHeritageGroup.Olthoi => new Vector3(0f, -3.79999995f, 1.14999998f),
(uint)ChargenHeritageGroup.OlthoiAcid => new Vector3(0f, -5.69999981f, 1.64999998f),
_ => new Vector3(0f, -2.5f, 0.95f),
};
///
/// Seconds per 360° revolution for the continuous-rotation button
/// (gmCGAppearancePage::m_dRotationPerSec, ctor pseudo-C
/// ~137523-137524 / ~226652-226653: raw double bits low32=0x00000000,
/// high32=0x40080000 → exactly 3.0 — the decompiler shows this cleanly,
/// no reconstruction needed). Retail's own per-tick formula
/// (gmCGAppearancePage::DoRotation @ 0x0047CA80, pseudo-C
/// ~0x0047CAC7): deltaDegrees = ((now - lastRotateTime) /
/// RotationSecondsPerRevolution) * 360 — CC6b's rotation controller
/// consumes this constant in exactly that shape, not as a
/// degrees-per-second rate. NOT applied here; see this class's own doc
/// comment on why rotation is not a camera concern.
///
public const float RotationSecondsPerRevolution = 3.0f;
///
/// Zoom tween duration in seconds
/// (gmCGAppearancePage::DoZoomAnimation @ 0x0047C960's
/// reset-if-invalid default, cross-confirmed by ZoomIn/ZoomOut's
/// own -0.1 sentinel write, which deliberately invalidates
/// m_dAnimDuration so the very next DoZoomAnimation tick
/// resets it to this same value). The campaign plan flagged this
/// constant as decompiler-garbled (both sites split the raw double
/// across two 32-bit stores, and the decompiler mis-renders the LOW
/// dword's store as a bogus float literal instead of raw bits) — it is
/// NOT unrecoverable: reinterpreting each garbled float literal as its
/// own raw 32-bit pattern and pairing it with the store's (clean) high
/// dword reconstructs an exact IEEE-754 double both times.
/// DoZoomAnimation's own reset path: low32 from
/// 4.17232506e-08f reinterpreted = 0x33333333, high32 =
/// 0x3fe33333 (clean) → exactly 0.6. Cross-check via
/// ZoomIn/ZoomOut's sentinel: low32 from
/// -1.58818684e-23f reinterpreted = 0x9999999A, high32 =
/// 0xbfb99999 (clean) → exactly -0.1, the well-known
/// IEEE-754 bit pattern for -0.1 (0xBFB999999999999A) — confirming
/// the reconstruction technique itself, not just this one value.
///
public const float ZoomTweenDurationSeconds = 0.6f;
public float FovRadians { get; set; } = MathF.PI / 4f; // retail CreatureMode default, same as DollCamera.
public float Near { get; set; } = 0.1f;
public float Far { get; set; } = 50f;
public float Aspect { get; set; } = 1f;
public Matrix4x4 View =>
Matrix4x4.CreateLookAt(_eye, _eye + Vector3.UnitY, Up);
public Matrix4x4 Projection =>
Matrix4x4.CreatePerspectiveFieldOfView(FovRadians, Aspect <= 0f ? 1f : Aspect, Near, Far);
}
///
/// Internal private-viewport adapter, mirroring DollViewportCamera's
/// role for .
///
internal sealed class ChargenPreviewViewportCamera : IPrivateEntityViewportCamera
{
private readonly ChargenPreviewCamera _camera;
public ChargenPreviewViewportCamera(uint heritageId = 0u)
{
_camera = new ChargenPreviewCamera(heritageId);
}
///
/// CC6b-MOUNT seam: wraps an EXTERNALLY-owned
/// instead of constructing a private one.
/// needs a settable to tween — the
/// other constructor's private _camera field is unreachable from
/// outside this class, so the page-mount composition (which owns the
/// zoom controller) must supply the SAME camera instance both this
/// adapter and the zoom controller mutate/read.
///
public ChargenPreviewViewportCamera(ChargenPreviewCamera camera)
{
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
}
public void SetHeritage(uint heritageId) => _camera.SetHeritage(heritageId);
public Vector3 Eye => _camera.Eye;
public float FovRadians
{
get => _camera.FovRadians;
set => _camera.FovRadians = value;
}
public float Near
{
get => _camera.Near;
set => _camera.Near = value;
}
public float Far
{
get => _camera.Far;
set => _camera.Far = value;
}
public float Aspect
{
get => _camera.Aspect;
set => _camera.Aspect = value;
}
public Matrix4x4 View => _camera.View;
public Matrix4x4 Projection => _camera.Projection;
}