using System.Collections.Generic; using System.Numerics; using DatReaderWriter.Enums; using DatReaderWriter.Types; using AcDream.Core.Physics; namespace AcDream.Core.Tests.Physics; /// /// Synthetic BSP tree fixtures for step-up and roof-landing conformance tests. /// /// /// These fixtures construct minimal trees plus /// matching dictionaries that represent canonical /// AC collision shapes without needing real DAT content. The shapes cover every /// interesting branch in Path 5 and Path 6. /// /// /// /// Coordinate convention: +Z is up, all geometry is expressed in object-local /// space (identity rotation, scale = 1.0) with objects at world origin so that /// localSphere.Origin == worldPosition. /// /// /// /// Retail references: /// BSPTREE::find_collisions Path 5 — acclient_2013_pseudo_c.txt:323849 / /// ACE BSPTree.cs:192-196. /// BSPTREE::find_collisions Path 6 / set_collide — /// acclient_2013_pseudo_c.txt:323819 / ACE BSPTree.cs:210-219. /// CTransition::step_up — acclient_2013_pseudo_c.txt:273099-273133 / /// ACE Transition.cs:746-777. /// SPHEREPATH::set_collide — acclient_2013_pseudo_c.txt:321594-321607 / /// ACE SpherePath.cs:279-286. /// /// public static class BSPStepUpFixtures { // ------------------------------------------------------------------------- // Polygon ID constants — each fixture uses a distinct range so the // resolved-polygon dictionary is unambiguous when fixtures are composed. // ------------------------------------------------------------------------- public const ushort LowStep_FloorId = 10; public const ushort LowStep_WallId = 11; public const ushort LowStep_UpperFloorId = 12; public const ushort TallWall_FloorId = 20; public const ushort TallWall_WallId = 21; public const ushort FlatRoof_FloorId = 30; public const ushort FlatRoof_RoofId = 31; public const ushort SlopedUnwalkable_FloorId = 40; public const ushort SlopedUnwalkable_SlopeId = 41; // ------------------------------------------------------------------------- // Sphere radius used in every test. // ------------------------------------------------------------------------- public const float SphereRadius = 0.2f; // ========================================================================= // Fixture 1 — Low step (25 cm) // // Schema (side view, XZ plane): // // +X ──────────────────► // Z // 0.5 ┆ ┌─────── ← UpperFloor at z=0.25 (vert 8..11) // 0.25├───────────┤ // ┆ Wall ┆ (x=0.5, z=[0,0.25]) // 0.0 ┆═══════════┘ // ← Floor at z=0 (vert 0..3) // // The mover starts grounded at x=-0.5, z=SphereRadius and walks toward +X. // Expected: step-up succeeds when Contact is set; sphere lifts to z=0.25+eps. // ========================================================================= /// /// Constructs a BSP tree and resolved-polygon dict representing a 25 cm step. /// /// Geometry (object-local space): /// /// Floor polygon at z = 0, x ∈ [-2, 0.5], y ∈ [-1, 1]. /// Vertical wall polygon at x = 0.5, z ∈ [0, 0.25], y ∈ [-1, 1], facing -X. /// Upper floor polygon at z = 0.25, x ∈ [0.2, 2], y ∈ [-1, 1] — extends /// left of the wall face so the vertical step-down probe finds it when the /// sphere is at x ≈ 0.3–0.5 (the wall contact zone). /// /// public static (PhysicsBSPNode Root, Dictionary Resolved) LowStep() { var resolved = new Dictionary(); // Lower floor: z=0, x∈[-2,0.5], y∈[-1,1], normal = +Z resolved[LowStep_FloorId] = MakeFloor( new Vector3(-2f, -1f, 0f), new Vector3(0.5f, -1f, 0f), new Vector3(0.5f, 1f, 0f), new Vector3(-2f, 1f, 0f)); // Vertical wall facing -X at x=0.5, z∈[0,0.25], normal = -X // For normal=(-1,0,0), the winding that makes cross(normal,edge)·disp > 0 // for interior points is: (y=-1,z=0)→(y=-1,z=0.25)→(y=1,z=0.25)→(y=1,z=0). resolved[LowStep_WallId] = MakeQuad( new Vector3(0.5f, -1f, 0f), new Vector3(0.5f, -1f, 0.25f), new Vector3(0.5f, 1f, 0.25f), new Vector3(0.5f, 1f, 0f), expectedNormal: new Vector3(-1f, 0f, 0f)); // Upper floor at z=0.25, x∈[0.2,2], y∈[-1,1], normal = +Z. // The upper floor extends slightly left of the wall face (x=0.5) // so the step-down probe (vertical, from the wall-contact XY) can // find it when the sphere is at x≈0.3-0.5. Retail BSPs have the // same overlap because geometry is continuous across the step. resolved[LowStep_UpperFloorId] = MakeFloor( new Vector3(0.2f, -1f, 0.25f), new Vector3(2f, -1f, 0.25f), new Vector3(2f, 1f, 0.25f), new Vector3(0.2f, 1f, 0.25f)); // Build a flat BSP tree: one internal node with all three polys in a leaf. // The bounding sphere covers everything. var leaf = new PhysicsBSPNode { Type = BSPNodeType.Leaf, BoundingSphere = new Sphere { Origin = Vector3.Zero, Radius = 10f }, }; leaf.Polygons.Add(LowStep_FloorId); leaf.Polygons.Add(LowStep_WallId); leaf.Polygons.Add(LowStep_UpperFloorId); return (leaf, resolved); } // ========================================================================= // Fixture 2 — Too-tall wall (5 m) // // A floor at z=0 and a 5 m wall at x=0.5 with no floor on the other side. // Expected: step-up fails (wall too tall), mover slides along wall. // ========================================================================= /// /// Constructs a BSP tree and resolved-polygon dict representing a wall that /// is too tall to step over (5 m), so step-up should fail. /// public static (PhysicsBSPNode Root, Dictionary Resolved) TallWall() { var resolved = new Dictionary(); // Floor at z=0 resolved[TallWall_FloorId] = MakeFloor( new Vector3(-2f, -1f, 0f), new Vector3(0.5f, -1f, 0f), new Vector3(0.5f, 1f, 0f), new Vector3(-2f, 1f, 0f)); // Tall wall at x=0.5, z∈[0,5], normal = -X // Winding for normal=(-1,0,0): (y=-1,z=0)→(y=-1,z=5)→(y=1,z=5)→(y=1,z=0). resolved[TallWall_WallId] = MakeQuad( new Vector3(0.5f, -1f, 0f), new Vector3(0.5f, -1f, 5f), new Vector3(0.5f, 1f, 5f), new Vector3(0.5f, 1f, 0f), expectedNormal: new Vector3(-1f, 0f, 0f)); var leaf = new PhysicsBSPNode { Type = BSPNodeType.Leaf, BoundingSphere = new Sphere { Origin = new Vector3(0f, 0f, 2.5f), Radius = 10f }, }; leaf.Polygons.Add(TallWall_FloorId); leaf.Polygons.Add(TallWall_WallId); return (leaf, resolved); } // ========================================================================= // Fixture 3 — Flat roof (3 m) // // A horizontal polygon at z=3 representing a building rooftop. // The mover is airborne (no Contact flag) descending toward the roof. // Expected (after L.2.2): Path 6 sets Collide flag; the Collide-flag handler // re-tests as Placement; ContactPlane is set; OnWalkable is established. // ========================================================================= /// /// Constructs a BSP tree and resolved-polygon dict representing a 3 m flat roof. /// public static (PhysicsBSPNode Root, Dictionary Resolved) FlatRoof() { var resolved = new Dictionary(); // Ground floor for reference (not involved in landing test) resolved[FlatRoof_FloorId] = MakeFloor( new Vector3(-2f, -1f, 0f), new Vector3(2f, -1f, 0f), new Vector3(2f, 1f, 0f), new Vector3(-2f, 1f, 0f)); // Roof at z=3.0, x∈[-2,2], y∈[-1,1], normal = +Z resolved[FlatRoof_RoofId] = MakeFloor( new Vector3(-2f, -1f, 3f), new Vector3(2f, -1f, 3f), new Vector3(2f, 1f, 3f), new Vector3(-2f, 1f, 3f)); var leaf = new PhysicsBSPNode { Type = BSPNodeType.Leaf, BoundingSphere = new Sphere { Origin = new Vector3(0f, 0f, 1.5f), Radius = 10f }, }; leaf.Polygons.Add(FlatRoof_FloorId); leaf.Polygons.Add(FlatRoof_RoofId); return (leaf, resolved); } // ========================================================================= // Fixture 4 — Sloped unwalkable surface (60°) // // A flat reference floor plus an angled slope at ~60° from horizontal. // normal.Z = cos(60°) ≈ 0.5 < PhysicsGlobals.FloorZ (0.6642). // Expected: no contact plane set; mover slides off. // ========================================================================= /// /// Constructs a BSP tree and resolved-polygon dict representing a steep (60°) /// slope whose normal.Z is below the walkable threshold. /// public static (PhysicsBSPNode Root, Dictionary Resolved) SlopedUnwalkable() { var resolved = new Dictionary(); // Reference floor at z=0 resolved[SlopedUnwalkable_FloorId] = MakeFloor( new Vector3(-2f, -1f, 0f), new Vector3(0f, -1f, 0f), new Vector3(0f, 1f, 0f), new Vector3(-2f, 1f, 0f)); // Steep slope: rises 2 m over 1 m horizontal run (63.4° from horizontal). // Vertices: (0,-1,0), (1,-1,2), (1,1,2), (0,1,0) // Normal direction: cross((1,0,2)-(0,0,0), (0,1,0)-(0,0,0)) ∝ (-2,0,1) normalised // After normalisation: (-0.894, 0, 0.447) — normal.Z ≈ 0.447 < FloorZ. // We point the normal outward (-X side) so it represents a wall-like slope. var v0 = new Vector3(0f, -1f, 0f); var v1 = new Vector3(1f, -1f, 2f); var v2 = new Vector3(1f, 1f, 2f); var v3 = new Vector3(0f, 1f, 0f); var raw = Vector3.Cross(v1 - v0, v3 - v0); var slopeNormal = Vector3.Normalize(raw); // Ensure the normal faces away from the approach side (-X direction). if (slopeNormal.X > 0) slopeNormal = -slopeNormal; var vertices = new[] { v0, v1, v2, v3 }; float dotSum = 0f; foreach (var v in vertices) dotSum += Vector3.Dot(slopeNormal, v); float d = -(dotSum / vertices.Length); resolved[SlopedUnwalkable_SlopeId] = new ResolvedPolygon { Vertices = vertices, Plane = new Plane(slopeNormal, d), NumPoints = 4, SidesType = CullMode.None, }; var leaf = new PhysicsBSPNode { Type = BSPNodeType.Leaf, BoundingSphere = new Sphere { Origin = new Vector3(0.5f, 0f, 1f), Radius = 10f }, }; leaf.Polygons.Add(SlopedUnwalkable_FloorId); leaf.Polygons.Add(SlopedUnwalkable_SlopeId); return (leaf, resolved); } // ========================================================================= // Transition builder helpers // ========================================================================= /// /// Build a for a grounded mover (Contact + OnWalkable set). /// /// /// The mover's foot sphere starts at and is headed /// toward . is /// set to so the test can control which step /// heights succeed. /// /// public static Transition MakeGroundedTransition( Vector3 from, Vector3 to, float stepUpHeight = 0.30f, uint cellId = 0xA9B40001u) { var t = new Transition(); t.SpherePath.InitPath(from, to, cellId, SphereRadius); t.ObjectInfo.State = ObjectInfoState.Contact | ObjectInfoState.OnWalkable; t.ObjectInfo.StepUpHeight = stepUpHeight; t.ObjectInfo.StepDownHeight = 0.04f; t.ObjectInfo.StepDown = true; // Seed LastKnownContactPlane so the mover is "on the floor". t.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f); t.CollisionInfo.LastKnownContactPlaneValid = true; return t; } /// /// Build a for an airborne mover (no Contact, no OnWalkable). /// /// /// Represents a character that has just jumped or fallen and is now moving /// downward to land on a surface. /// /// public static Transition MakeAirborneTransition( Vector3 from, Vector3 to, uint cellId = 0xA9B40001u) { var t = new Transition(); t.SpherePath.InitPath(from, to, cellId, SphereRadius); t.ObjectInfo.State = ObjectInfoState.None; t.ObjectInfo.StepUpHeight = 0.04f; t.ObjectInfo.StepDownHeight = 0.04f; t.ObjectInfo.StepDown = false; return t; } // ========================================================================= // Internal polygon builders // ========================================================================= // Build a horizontal floor polygon (normal = +Z) from four CCW vertices // (as viewed from above). private static ResolvedPolygon MakeFloor( Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3) { var verts = new[] { v0, v1, v2, v3 }; var normal = Vector3.UnitZ; float dotSum = 0f; foreach (var v in verts) dotSum += Vector3.Dot(normal, v); float d = -(dotSum / verts.Length); return new ResolvedPolygon { Vertices = verts, Plane = new Plane(normal, d), NumPoints = 4, SidesType = CullMode.None, }; } // Build a quad polygon with a specified outward normal. // Vertices should be ordered so that the cross-product of two edges aligns // with expectedNormal; we explicitly override the computed plane so the test // is deterministic regardless of winding order. private static ResolvedPolygon MakeQuad( Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3, Vector3 expectedNormal) { var verts = new[] { v0, v1, v2, v3 }; float dotSum = 0f; foreach (var v in verts) dotSum += Vector3.Dot(expectedNormal, v); float d = -(dotSum / verts.Length); return new ResolvedPolygon { Vertices = verts, Plane = new Plane(expectedNormal, d), NumPoints = 4, SidesType = CullMode.None, }; } }