using System.Numerics;
using AcDream.Core.Physics;
namespace AcDream.App.Rendering;
///
/// backed by the player's swept-sphere
/// engine. Ports retail's SmartBox::update_viewer (0x00453ce0): sweep
/// the 0.3 m viewer_sphere from the head-pivot to the desired eye via a
/// CTransition and use the stopped position. Reusing
/// collides against indoor
/// cell walls (FindEnvCollisions) AND outdoor/baked GfxObj shells
/// (FindObjCollisions) in one faithful path.
///
public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
{
/// Retail viewer_sphere radius (acclient :93314).
public const float ViewerSphereRadius = 0.3f;
private readonly PhysicsEngine _physics;
public PhysicsCameraCollisionProbe(PhysicsEngine physics) => _physics = physics;
public Vector3 SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId)
{
// No starting cell → nothing to sweep against; keep the desired eye.
if (cellId == 0) return desiredEye;
// SpherePath.InitPath puts sphere0's center at pathPos + (0,0,radius)
// (the player foot-capsule convention). Retail's viewer_sphere center is
// (0,0,0), so shift the path DOWN by the radius to make the SPHERE CENTER
// travel pivot→eye, then add it back to the swept stop position.
Vector3 begin = ToSpherePath(pivot, ViewerSphereRadius);
Vector3 end = ToSpherePath(desiredEye, ViewerSphereRadius);
var r = _physics.ResolveWithTransition(
currentPos: begin,
targetPos: end,
cellId: cellId,
sphereRadius: ViewerSphereRadius,
sphereHeight: 0f, // single sphere (no head sphere)
stepUpHeight: 0f, // no step-up for a camera
stepDownHeight: 0f, // no step-down / ground snap
isOnGround: false, // no contact-plane / walkable semantics
body: null, // no cross-frame persistence
// Retail SmartBox::update_viewer calls init_object(player, 0x5c) =
// IsViewer | PathClipped | FreeRotate | PerfectClip (acclient
// pseudo-C :92864; enum TransitionTypes.cs:24-33). PathClipped makes
// the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the
// spring-arm pull-in, not the player's edge-slide. IsViewer lets the
// eye pass through creatures, colliding only with world geometry
// (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in
// acdream today but set to match retail's exact value. NOT IsPlayer
// (0x100), so camera sweeps stay out of the #98 capture filter.
moverFlags: ObjectInfoState.IsViewer | ObjectInfoState.PathClipped
| ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip,
movingEntityId: selfEntityId); // skip the player's own ShadowEntry
Vector3 eye = FromSpherePath(r.Position, ViewerSphereRadius);
// Phase U.4c spike apparatus (THROWAWAY — strip with ACDREAM_PROBE_FLAP).
// The post-fix [flap-cam] capture shows the eye flying to full chase distance
// (eyeInRoot=n ~90%) in cells like 0xA9B40174/0175 — i.e. this sweep is not
// stopping it. This line answers WHY, the fork that picks the primary residual
// fix: pulledIn≈0 with resolved=Y bsp=ok ⇒ the sweep ran but found NOTHING in
// that cell (space genuinely open, or wall geometry the per-cell sweep can't
// reach → clip-robustness is primary); resolved=n / bsp=nobsp/noroot ⇒ collision
// can't even run there (cell/BSP not loaded → camera-collision reliability is
// primary); pulledIn large ⇒ collision IS engaging (eye leaving is then expected
// through an opening). Paired per-frame with the builder's [flap]/[flap-cam].
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
{
var cp = _physics.DataCache?.GetCellStruct(cellId);
string bsp = cp?.BSP is null ? "nobsp" : (cp.BSP.Root is null ? "noroot" : "ok");
float desiredBack = Vector3.Distance(pivot, desiredEye);
float eyeBack = Vector3.Distance(pivot, eye);
System.Console.WriteLine(
$"[flap-sweep] cell=0x{cellId:X8} resolved={(cp is not null ? "Y" : "n")} bsp={bsp} " +
$"desiredBack={desiredBack:F2} eyeBack={eyeBack:F2} pulledIn={desiredBack - eyeBack:F2} " +
$"collNormValid={r.CollisionNormalValid}");
}
return eye;
}
/// Eye/pivot point → InitPath path point (subtract the sphere-center offset).
internal static Vector3 ToSpherePath(Vector3 spherePoint, float radius)
=> spherePoint - new Vector3(0f, 0f, radius);
/// InitPath path point → eye point (add the sphere-center offset back).
internal static Vector3 FromSpherePath(Vector3 pathPoint, float radius)
=> pathPoint + new Vector3(0f, 0f, radius);
}