using System;
using System.Numerics;
using AcDream.Core.Physics;
using Xunit;
using Plane = System.Numerics.Plane;
namespace AcDream.Core.Tests.Physics;
///
/// Conformance tests for AP-10 (Campaign P Slice P4, 2026-07-30) — two
/// water-semantics gaps identified in
/// docs/research/2026-07-29-remote-and-world-specials-pseudocode.md §5:
///
///
/// -
/// Retail's 0.1 m dry-corner water sink-in
/// () was collapsed to 0 —
/// now restored. PhysicsEngine.SampleTerrainWalkable's
/// isWater = waterDepth >= 0.45f threshold means the dry-corner
/// restore does NOT flip a dry corner's water classification (0.1 <
/// 0.45, same as the old 0 < 0.45) — only the sink-in depth changes.
///
/// -
/// WATER_CONTACT_TS ()
/// was declared but never written — now mirrored alongside
/// at every commit point:
/// ,
/// , and
/// PhysicsEngine's per-resolve body-state commit.
///
///
///
public class Ap10WaterSemanticsTests
{
// ── §1: TerrainSurface.SampleWaterDepth golden values ──────────────────
/// terrainTypes byte whose (byte>>2)&0x1F == 0x10 (WaterRunning, the lowest water type).
private const byte WaterTerrainByte = 0x10 << 2; // 0x40
private const byte DryTerrainByte = 0x00;
private static byte[] AllVertices(byte value)
{
var arr = new byte[81];
Array.Fill(arr, value);
return arr;
}
[Fact]
public void SampleWaterDepth_NotWaterCell_ReturnsZero()
{
var surface = new TerrainSurface(
new byte[81], new float[256],
terrainTypes: AllVertices(DryTerrainByte));
Assert.Equal(0f, surface.SampleWaterDepth(12f, 12f));
}
[Fact]
public void SampleWaterDepth_EntirelyWaterCell_ReturnsPoint9()
{
var surface = new TerrainSurface(
new byte[81], new float[256],
terrainTypes: AllVertices(WaterTerrainByte));
Assert.Equal(0.9f, surface.SampleWaterDepth(12f, 12f));
}
[Fact]
public void SampleWaterDepth_PartiallyWaterCell_WaterCorner_ReturnsPoint45()
{
// Cell (0,0): corners are vertices (0,0),(1,0),(1,1),(0,1). Make only
// (1,1) water so cell (0,0) is PartiallyWater (1 of 4 corners).
// Sampling near local (18,18) (>= 12 into the 24m cell on both axes)
// rounds to vertex (1,1) — the water corner.
var types = new byte[81];
types[1 * 9 + 1] = WaterTerrainByte; // vertex (x=1, y=1)
var surface = new TerrainSurface(new byte[81], new float[256], terrainTypes: types);
Assert.Equal(0.45f, surface.SampleWaterDepth(18f, 18f));
}
[Fact]
public void SampleWaterDepth_PartiallyWaterCell_DryCorner_ReturnsPoint1_RestoredRetailConstant()
{
// Same PartiallyWater cell as above, but sample near local (4,4)
// (< 12 into the cell) which rounds to vertex (0,0) — the DRY corner.
// AP-10: this is the value that was collapsed to 0 and is now restored.
var types = new byte[81];
types[1 * 9 + 1] = WaterTerrainByte; // vertex (x=1, y=1) is the only water corner
var surface = new TerrainSurface(new byte[81], new float[256], terrainTypes: types);
Assert.Equal(0.1f, surface.SampleWaterDepth(4f, 4f));
}
[Fact]
public void SampleWaterDepth_DryCorner_StaysBelowTheIsWaterClassificationThreshold()
{
// PhysicsEngine.SampleTerrainWalkable classifies isWater as
// waterDepth >= 0.45f. The restored 0.1f dry-corner value must NOT
// cross that threshold — only the sink-in depth changes, not whether
// the point is treated as "water" for contact-plane/animation purposes.
var types = new byte[81];
types[1 * 9 + 1] = WaterTerrainByte;
var surface = new TerrainSurface(new byte[81], new float[256], terrainTypes: types);
float dryDepth = surface.SampleWaterDepth(4f, 4f);
Assert.True(dryDepth < 0.45f, $"Dry-corner depth {dryDepth} must stay below the isWater threshold");
}
// ── §2: WATER_CONTACT_TS mirroring in PhysicsObjUpdate ─────────────────
private static PhysicsBody MakeBody(bool contactPlaneIsWater) => new()
{
TransientState = TransientStateFlags.None,
ContactPlaneIsWater = contactPlaneIsWater,
};
[Fact]
public void ApplySetPositionContact_WaterContactPlane_SetsWaterContactBit()
{
var body = MakeBody(contactPlaneIsWater: true);
PhysicsObjUpdate.ApplySetPositionContact(body, inContact: true, onWalkable: true);
Assert.True(body.IsWaterContact);
}
[Fact]
public void ApplySetPositionContact_DryContactPlane_ClearsWaterContactBit()
{
var body = MakeBody(contactPlaneIsWater: false);
body.TransientState |= TransientStateFlags.WaterContact; // pre-seed stale bit
PhysicsObjUpdate.ApplySetPositionContact(body, inContact: true, onWalkable: true);
Assert.False(body.IsWaterContact);
}
[Fact]
public void ApplySetPositionContact_WaterContactMirrorsIndependentlyOfContactBit()
{
// WaterContact tracks ContactPlaneIsWater, not the inContact argument
// itself — matches retail writing the two bits from two different
// per-call locals in the same statement block.
var body = MakeBody(contactPlaneIsWater: true);
PhysicsObjUpdate.ApplySetPositionContact(body, inContact: false, onWalkable: false);
Assert.False(body.InContact);
Assert.True(body.IsWaterContact);
}
[Fact]
public void CommitSetPositionTransition_WaterContactPlane_SetsWaterContactBit()
{
var body = MakeBody(contactPlaneIsWater: true);
PhysicsObjUpdate.CommitSetPositionTransition(
body,
inContact: true,
onWalkable: true,
collisionNormalValid: false,
collisionNormal: Vector3.Zero,
previousContact: false,
previousOnWalkable: false);
Assert.True(body.IsWaterContact);
}
[Fact]
public void CommitSetPositionTransition_DryContactPlane_ClearsWaterContactBit()
{
var body = MakeBody(contactPlaneIsWater: false);
body.TransientState |= TransientStateFlags.WaterContact; // pre-seed stale bit
PhysicsObjUpdate.CommitSetPositionTransition(
body,
inContact: true,
onWalkable: true,
collisionNormalValid: false,
collisionNormal: Vector3.Zero,
previousContact: true,
previousOnWalkable: true);
Assert.False(body.IsWaterContact);
}
// ── §3: end-to-end through PhysicsEngine.ResolveWithTransition ─────────
// (retail's actual SetPositionInternal-equivalent per-resolve commit)
private const uint TestLandblockId = 0xA9B40000u;
private const uint TestCellId = TestLandblockId | 0x0001u;
private const float SphereRadius = 0.4f;
private const float SphereHeight = 1.2f;
private static PhysicsEngine BuildEngineWithFlatWaterTerrain(bool water)
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
var heights = new byte[81]; // all zero -> terrain Z = 0 everywhere
var heightTable = new float[256];
var types = water ? AllVertices(WaterTerrainByte) : AllVertices(DryTerrainByte);
engine.AddLandblock(
landblockId: TestLandblockId,
terrain: new TerrainSurface(heights, heightTable, terrainTypes: types),
cells: Array.Empty(),
portals: Array.Empty(),
worldOffsetX: 0f,
worldOffsetY: 0f);
return engine;
}
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,
};
}
///
/// Lets a body sink/settle onto terrain via repeated resolves, exactly the
/// multi-tick pattern CylSphereFamilyTests.Grounded_WalkIntoWideLowCylinder
/// _StepsUpOntoTop uses. A single resolve's step-down is bounded
/// (WalkInterp), so a body starting well above a water-shifted resting
/// depth needs several ticks to converge — this mirrors retail's own
/// gradual settle, not an instant snap.
///
private static void SettleOntoTerrain(PhysicsEngine engine, PhysicsBody body, int ticks = 60)
{
Vector3 pos = body.Position;
uint cellId = TestCellId;
bool grounded = true;
for (int tick = 0; tick < ticks; tick++)
{
var target = pos + new Vector3(0f, 0.001f, -0.05f);
var result = engine.ResolveWithTransition(
pos, target, cellId,
SphereRadius, SphereHeight,
stepUpHeight: 0.04f, stepDownHeight: 0.04f,
isOnGround: grounded,
body: body,
moverFlags: ObjectInfoState.IsPlayer,
movingEntityId: 0);
body.Position = result.Position;
pos = result.Position;
cellId = result.CellId;
grounded = result.IsOnGround;
}
}
[Fact]
public void EndToEnd_SettleOntoEntirelyWaterTerrain_SetsBodyWaterContact()
{
var engine = BuildEngineWithFlatWaterTerrain(water: true);
var body = MakeGroundedBody(new Vector3(12f, 12f, 1.0f));
SettleOntoTerrain(engine, body);
// Settles exactly waterDepth (0.9 m) below the nominal Z=0 terrain plane —
// the "submerged" visual retail produces (no separate water surface mesh;
// the character just sits lower than terrain by the allowed sink-in).
Assert.True(MathF.Abs(body.Position.Z - (-0.9f)) < 0.05f,
$"Body should settle 0.9m below the nominal terrain plane in an EntirelyWater cell; got Z={body.Position.Z:F3}");
Assert.True(body.ContactPlaneIsWater, "Body must record the water contact plane");
Assert.True(body.IsWaterContact, "WATER_CONTACT_TS must mirror ContactPlaneIsWater");
}
[Fact]
public void EndToEnd_SettleOntoDryTerrain_LeavesBodyWaterContactClear()
{
// Dry-land behavior unchanged: an ordinary flat dry landblock must
// never set WaterContact, exactly as before AP-10.
var engine = BuildEngineWithFlatWaterTerrain(water: false);
var body = MakeGroundedBody(new Vector3(12f, 12f, 1.0f));
body.TransientState |= TransientStateFlags.WaterContact; // pre-seed stale bit
SettleOntoTerrain(engine, body);
Assert.True(MathF.Abs(body.Position.Z) < 0.05f,
$"Body should settle exactly on the dry Z=0 terrain plane (no sink-in); got Z={body.Position.Z:F3}");
Assert.False(body.ContactPlaneIsWater);
Assert.False(body.IsWaterContact,
"Dry-land resolves must clear any stale WaterContact bit, not just leave it unset");
}
}