using System;
using System.Collections.Generic;
using System.Numerics;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
namespace AcDream.Core.Physics;
///
/// Pure-function builder that translates a into a list of
/// s suitable for registration via
/// .
///
///
/// Walks (1) every CylSphere → Cylinder shape, (2) every Sphere ONLY when no
/// CylSpheres are present (matches retail and the existing landblock-static
/// convention at GameWindow.cs:6034), and (3) every Part whose GfxObj has a
/// non-null PhysicsBSP → per-part BSP shape, with local transforms from
/// PlacementFrames[Resting | Default | first available].
///
///
///
/// Retail anchor: CPhysicsObj::FindObjCollisions calls
/// CPartArray::FindObjCollisions which iterates parts; each part's
/// find_obj_collisions tests CylSpheres + GfxObj BSP. We emit one
/// ShadowShape per part contribution so the existing FindObjCollisions
/// iteration loop in tests each part independently.
///
///
public static class ShadowShapeBuilder
{
///
/// Build the shape list for a Setup.
///
/// The Setup to walk.
/// The entity's overall scale factor; multiplies
/// every radius, height, and local offset.
/// Predicate: does the GfxObj with this id
/// have a non-null PhysicsBSP? Production: id => cache.GetGfxObj(id)?.BSP?.Root is not null.
public static IReadOnlyList FromSetup(
Setup setup,
float entScale,
Func hasPhysicsBsp)
{
if (setup is null) throw new ArgumentNullException(nameof(setup));
if (hasPhysicsBsp is null) throw new ArgumentNullException(nameof(hasPhysicsBsp));
var result = new List();
// 1. CylSpheres — each becomes a Cylinder shape.
foreach (var cyl in setup.CylSpheres)
{
if (cyl.Radius <= 0f) continue;
float baseHeight = cyl.Height > 0f ? cyl.Height : cyl.Radius * 4f;
result.Add(new ShadowShape(
GfxObjId: 0u,
LocalPosition: new Vector3(cyl.Origin.X, cyl.Origin.Y, cyl.Origin.Z) * entScale,
LocalRotation: Quaternion.Identity,
Scale: entScale,
CollisionType: ShadowCollisionType.Cylinder,
Radius: cyl.Radius * entScale,
CylHeight: baseHeight * entScale));
}
// 2. Spheres — only when no CylSpheres (matches landblock-static convention
// at GameWindow.cs:6034). Each becomes a short Cylinder.
if (setup.CylSpheres.Count == 0)
{
foreach (var sph in setup.Spheres)
{
if (sph.Radius <= 0f) continue;
result.Add(new ShadowShape(
GfxObjId: 0u,
LocalPosition: new Vector3(sph.Origin.X, sph.Origin.Y, sph.Origin.Z) * entScale,
LocalRotation: Quaternion.Identity,
Scale: entScale,
CollisionType: ShadowCollisionType.Cylinder,
Radius: sph.Radius * entScale,
CylHeight: sph.Radius * 2f * entScale));
}
}
// 3. Parts — one BSP shape per part with a non-null PhysicsBSP.
AnimationFrame? placementFrame = ResolvePlacementFrame(setup);
for (int i = 0; i < setup.Parts.Count; i++)
{
uint gfxId = (uint)setup.Parts[i];
if (!hasPhysicsBsp(gfxId)) continue;
Frame partFrame = placementFrame is not null && i < placementFrame.Frames.Count
? placementFrame.Frames[i]
: new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity };
// BSP radius default; caller substitutes the real BoundingSphere.Radius
// at registration time when available. Loose-but-safe broadphase value.
float bspRadius = 2f * entScale;
result.Add(new ShadowShape(
GfxObjId: gfxId,
LocalPosition: new Vector3(partFrame.Origin.X, partFrame.Origin.Y, partFrame.Origin.Z) * entScale,
LocalRotation: partFrame.Orientation,
Scale: entScale,
CollisionType: ShadowCollisionType.BSP,
Radius: bspRadius,
CylHeight: 0f));
}
return result;
}
/// Resolve the placement frame in priority Resting → Default →
/// first available. Mirrors SetupMesh.Flatten's convention.
private static AnimationFrame? ResolvePlacementFrame(Setup setup)
{
if (setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting)) return resting;
if (setup.PlacementFrames.TryGetValue(Placement.Default, out var def)) return def;
foreach (var kvp in setup.PlacementFrames) return kvp.Value;
return null;
}
}