Pure function translating Setup -> IReadOnlyList<ShadowShape>. Walks CylSpheres + Spheres (only when no CylSpheres) + Parts (only when the GfxObj has a non-null PhysicsBSP), using PlacementFrames in the same Resting -> Default -> first-available priority as SetupMesh.Flatten. Six tests pin the behavior: door setup produces 4 shapes (0+1+3), sphere local offset matches Setup data, parts without BSP are skipped, creature setups with CylSpheres skip Spheres, scale factor multiplies all radii and offsets, empty setup returns empty list, null setup throws. No callers in this commit; RegisterMultiPart + the GameWindow callers follow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
157 lines
4.9 KiB
C#
157 lines
4.9 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<uint, bool> 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<uint, bool> 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<ArgumentNullException>(
|
|
() => ShadowShapeBuilder.FromSetup(null!, 1.0f, _ => true));
|
|
}
|
|
}
|