using System;
using System.Numerics;
using AcDream.Core.Physics;
using Xunit;
using Xunit.Abstractions;
using Plane = System.Numerics.Plane;
namespace AcDream.Core.Tests.Physics;
///
/// Conformance tests for the retail CSphere::intersects_sphere collision
/// family port (2026-07-07) — dispatcher 0x00537A80 + step_sphere_up
/// 0x00537900 + slide_sphere 0x00537440 +
/// land_on_sphere 0x005379a0 + collide_with_point
/// 0x00537230 + step_sphere_down 0x00536d20. Pseudocode:
/// docs/research/2026-07-07-csphere-collision-family-pseudocode.md.
///
///
/// The driving repro: the user's live report that a player packed inside a crowd
/// of monsters gets wedged and can't wiggle free, where retail leaves room to
/// shuffle out. Humanoid creatures/players collide as body Spheres
/// (Setup.Spheres → emits
/// ), so the crowd contact runs through
/// Transition.SphereCollision. Before this port that method was a
/// hand-rolled 3-D wall-slide with a forced fixed de-penetration (register
/// TS-45) — opposing radial pushes from neighbours cancelled and the
/// player wedged. The faithful family routes the slide through the shared crease
/// projection, restoring retail's tangential shuffle. Synthetic spheres only — no
/// dat dependency.
///
///
public class SphereCollisionFamilyTests
{
private readonly ITestOutputHelper _out;
public SphereCollisionFamilyTests(ITestOutputHelper output) => _out = output;
private const uint TestLandblockId = 0xA9B40000u;
private const uint TestCellId = TestLandblockId | 0x0001u; // landcell (0,0)
private const float SphereRadius = 0.48f; // retail player capsule radius
private const float SphereHeight = 1.20f;
private const float StepUpHeight = 0.60f;
private const float StepDownHeight = 0.04f;
private const float CreatureRadius = 0.48f; // a humanoid body sphere
///
/// THE headline behavior: the crease slide lets a grounded player SHUFFLE
/// AROUND a body-sphere creature that partly blocks the path, instead of
/// sticking to it. The creature sits slightly EAST of the player's northward
/// line; the player walks N, grazes the creature's south-west, and must slide
/// along it (crease = collisionNormal × ground-up) and continue past.
///
///
/// This is the retail-faithful tangential slide the port restores. The
/// pre-2026-07-07 hand-rolled SphereCollision force-pushed the contact
/// RADIALLY to a fixed combinedR + 1 cm shell (shoving the player
/// south-west, back along its approach) — TS-45. In a crowd those radial
/// shoves from several neighbours fight each other and wedge the player; the
/// tangential crease slide is what leaves "room to shuffle out". The player
/// starts CLEAR (not penetrating) — a start deep inside overlapping spheres
/// reverts in retail too (validate_transition restores curr_pos on a
/// non-clean step, 0x0050aa70:272593), so that is not a valid repro.
///
///
[Fact]
public void GroundedDiagonalApproach_SlidesPastOffsetCreature()
{
var engine = BuildEngine(out _);
RegisterCreatureSphere(engine, 0xC0C0u, 10.35f, 12f); // slightly EAST of the N path
var body = MakeGroundedBody(new Vector3(10f, 10f, 0f));
Vector3 pos = body.Position;
uint cellId = TestCellId;
bool grounded = true;
var perTick = new Vector3(0f, 0.08f, 0f); // push NORTH
for (int tick = 0; tick < 60; tick++)
{
var result = engine.ResolveWithTransition(
pos, pos + perTick, cellId,
SphereRadius, SphereHeight, StepUpHeight, StepDownHeight,
grounded, body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0);
body.Position = result.Position;
pos = result.Position;
cellId = result.CellId;
grounded = result.IsOnGround;
_out.WriteLine($"tick {tick,2}: ({pos.X:F3},{pos.Y:F3})");
}
_out.WriteLine($"final pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) grounded={grounded}");
// The creature centre is at Y=12. If the player slid around it, it ends
// NORTH of the creature; if it stuck, it wedges near Y≈11 (surface at
// Y = 12 − sqrt(0.96² − 0.35²) ≈ 11.11).
Assert.True(pos.Y > 12.5f,
$"Player must slide around the offset creature and continue north, not stick at " +
$"its surface; got Y={pos.Y:F3}");
}
///
/// A single body-sphere creature dead ahead: the player pushing straight into
/// it must stop AT the surface (a head-on push has no crease tangent → the
/// slide degenerates → Collided) and must NOT penetrate or pass through.
/// Surface contact is at Y = 11.5 − (0.48+0.48) = 10.54.
///
[Fact]
public void GroundedSingleCreature_HeadOnPush_BlocksWithoutPenetration()
{
var engine = BuildEngine(out _);
RegisterCreatureSphere(engine, 0xC0B0u, 12f, 11.5f); // due north
var body = MakeGroundedBody(new Vector3(12f, 10f, 0f));
Vector3 pos = body.Position;
uint cellId = TestCellId;
bool grounded = true;
var perTick = new Vector3(0f, 0.08f, 0f); // straight in
for (int tick = 0; tick < 30; tick++)
{
var result = engine.ResolveWithTransition(
pos, pos + perTick, cellId,
SphereRadius, SphereHeight, StepUpHeight, StepDownHeight,
grounded, body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0);
body.Position = result.Position;
pos = result.Position;
cellId = result.CellId;
grounded = result.IsOnGround;
}
_out.WriteLine($"final pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3})");
Assert.True(pos.Y < 10.7f,
$"Player must be blocked at the sphere surface (Y≈10.54), not penetrate; got Y={pos.Y:F3}");
Assert.True(pos.Y > 10.3f,
$"Player must actually reach the creature (not stop early); got Y={pos.Y:F3}");
}
///
/// An ETHEREAL sphere (state 0x4, non-static) is fully passable — the branch-1
/// early-OK consume plus the caller's Layer-2 override. Guards the door/ghost
/// passability against the family rewrite.
///
[Fact]
public void GroundedEtherealSphere_IsFullyPassable()
{
var engine = BuildEngine(out _);
RegisterCreatureSphere(engine, 0xC0E0u, 12f, 11.5f,
state: 0x4u, isCreatureFlag: false); // ETHEREAL_PS, non-static
var body = MakeGroundedBody(new Vector3(12f, 10f, 0f));
Vector3 pos = body.Position;
uint cellId = TestCellId;
bool grounded = true;
var perTick = new Vector3(0f, 0.08f, 0f);
for (int tick = 0; tick < 45; tick++)
{
var result = engine.ResolveWithTransition(
pos, pos + perTick, cellId,
SphereRadius, SphereHeight, StepUpHeight, StepDownHeight,
grounded, body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0);
body.Position = result.Position;
pos = result.Position;
cellId = result.CellId;
grounded = result.IsOnGround;
}
_out.WriteLine($"final pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3})");
Assert.True(pos.Y > 12.5f,
$"Ethereal sphere must not block (walked from 10 past the axis); got Y={pos.Y:F3}");
}
// ───────────────────────────────────────────────────────────────
// Harness (mirrors CylSphereFamilyTests)
// ───────────────────────────────────────────────────────────────
private static PhysicsEngine BuildEngine(out PhysicsDataCache cache)
{
cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
var heights = new byte[81];
var heightTable = new float[256]; // all zero → terrain Z = 0
engine.AddLandblock(
landblockId: TestLandblockId,
terrain: new TerrainSurface(heights, heightTable),
cells: Array.Empty(),
portals: Array.Empty(),
worldOffsetX: 0f,
worldOffsetY: 0f);
return engine;
}
///
/// Register a humanoid body sphere at foot height (Z = SphereRadius) so the
/// player's foot sphere overlaps it horizontally. Flagged as a creature by
/// default (the crowd case), mimicking a live monster's body-sphere shadow.
///
private static void RegisterCreatureSphere(PhysicsEngine engine, uint entityId,
float x, float y, float radius = CreatureRadius, uint state = 0u,
bool isCreatureFlag = true)
{
engine.ShadowObjects.Register(
entityId, gfxObjId: 0u,
new Vector3(x, y, SphereRadius), Quaternion.Identity, radius,
worldOffsetX: 0f, worldOffsetY: 0f, landblockId: TestLandblockId,
collisionType: ShadowCollisionType.Sphere,
cylHeight: 0f, scale: 1f,
state: state,
flags: isCreatureFlag ? EntityCollisionFlags.IsCreature : EntityCollisionFlags.None,
isStatic: false);
}
private static PhysicsBody MakeGroundedBody(Vector3 position)
{
var floorPlane = new Plane(Vector3.UnitZ, 0f);
var floorVerts = new[]
{
new Vector3(-100f, -100f, 0f),
new Vector3( 100f, -100f, 0f),
new Vector3( 100f, 100f, 0f),
new Vector3(-100f, 100f, 0f),
};
return new PhysicsBody
{
Position = position,
Orientation = Quaternion.Identity,
ContactPlaneValid = true,
ContactPlane = floorPlane,
ContactPlaneCellId = TestCellId,
WalkablePolygonValid = true,
WalkablePlane = floorPlane,
WalkableVertices = floorVerts,
WalkableUp = Vector3.UnitZ,
TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
};
}
}