using System; using System.Linq; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; using Xunit; namespace AcDream.Core.Tests.Physics; public class ShadowShapeBuilderTests { /// /// Synthetic Setup mirroring live dump of 0x020019FF (cottage door) /// captured 2026-05-24: 0 cylSpheres, 1 sphere (r=0.100, origin=(0,0,0.018)), /// 3 parts (0x010044B5 + 0x010044B6 + 0x010044B6), setup.Radius=0.141, /// setup.Height=0.200. PlacementFrames[Default] has identity transforms /// for all 3 parts. /// private static Setup CreateDoorSetup() { var setup = new Setup { Radius = 0.141f, Height = 0.200f, StepUpHeight = 0.090f, StepDownHeight = 0.090f, Parts = { 0x010044B5u, 0x010044B6u, 0x010044B6u }, Spheres = { new Sphere { Radius = 0.100f, Origin = new Vector3(0f, 0f, 0.018f) } }, PlacementFrames = { [Placement.Default] = new AnimationFrame(3) { Frames = { new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }, new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity }, new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity } } } } }; return setup; } [Fact] public void FromSetup_DoorSetup_ProducesFourShapes() { var setup = CreateDoorSetup(); Func hasBsp = id => id == 0x010044B5u || id == 0x010044B6u; var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1.0f, hasBsp); Assert.Equal(4, shapes.Count); int cylinderCount = 0; int bspCount = 0; foreach (var s in shapes) { if (s.CollisionType == ShadowCollisionType.Cylinder) cylinderCount++; else if (s.CollisionType == ShadowCollisionType.BSP) bspCount++; } Assert.Equal(1, cylinderCount); Assert.Equal(3, bspCount); } [Fact] public void FromSetup_DoorSetup_SphereAtExpectedLocalOffset() { var setup = CreateDoorSetup(); var shapes = ShadowShapeBuilder.FromSetup(setup, 1.0f, _ => true); var sphereAsCyl = shapes.FirstOrDefault(s => s.CollisionType == ShadowCollisionType.Cylinder); Assert.NotEqual(default, sphereAsCyl); Assert.Equal(0f, sphereAsCyl.LocalPosition.X, 4); Assert.Equal(0f, sphereAsCyl.LocalPosition.Y, 4); Assert.Equal(0.018f, sphereAsCyl.LocalPosition.Z, 4); Assert.Equal(0.100f, sphereAsCyl.Radius, 4); } [Fact] public void FromSetup_PartWithoutBsp_SkipsBspShape() { var setup = CreateDoorSetup(); Func hasBsp = id => id == 0x010044B5u; var shapes = ShadowShapeBuilder.FromSetup(setup, 1.0f, hasBsp); int bspCount = 0; foreach (var s in shapes) if (s.CollisionType == ShadowCollisionType.BSP) bspCount++; Assert.Equal(1, bspCount); } [Fact] public void FromSetup_CreatureWithCylSpheres_OnlyEmitsCylinders() { var setup = new Setup { Parts = { 0x02000001u }, CylSpheres = { new CylSphere { Radius = 0.40f, Height = 1.20f, Origin = new Vector3(0, 0, 0.6f) } }, Spheres = { new Sphere { Radius = 0.50f, Origin = new Vector3(0, 0, 0.7f) } } }; var shapes = ShadowShapeBuilder.FromSetup(setup, 1.0f, _ => false); Assert.Single(shapes); Assert.Equal(ShadowCollisionType.Cylinder, shapes[0].CollisionType); Assert.Equal(0.40f, shapes[0].Radius, 3); Assert.Equal(1.20f, shapes[0].CylHeight, 3); } [Fact] public void FromSetup_ScaleFactor_MultipliesAllRadiiAndOffsets() { var setup = CreateDoorSetup(); var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 2.0f, _ => true); foreach (var s in shapes) { Assert.Equal(2.0f, s.Scale, 3); if (s.CollisionType == ShadowCollisionType.Cylinder) { Assert.Equal(0.200f, s.Radius, 3); Assert.Equal(0.036f, s.LocalPosition.Z, 3); } } } [Fact] public void FromSetup_EmptySetup_ReturnsEmptyList() { var setup = new Setup(); var shapes = ShadowShapeBuilder.FromSetup(setup, 1.0f, _ => true); Assert.Empty(shapes); } [Fact] public void FromSetup_NullSetup_Throws() { Assert.Throws( () => ShadowShapeBuilder.FromSetup(null!, 1.0f, _ => true)); } }