acdream/tests/AcDream.App.Tests/Physics/PlayerOutboundPositionTests.cs
Erik 7b7ffcd278 fix(gameplay): reconcile wield ownership and target facing
Preserve PlayerDescription inventory/equipment ownership across authoritative manifest replacement, make weapon switching and combat/UI consumers read the same canonical object state, and carry the complete outbound player position frame across landblocks.

Route target-facing and mouse-look through the shared MovementManager and MotionInterpreter completion owner. Match retail input aggregation, toggle ordering, turn/sidestep remapping, per-axis hold keys, and synchronous movement publication without render-only heading state.

Initialize the live streaming origin from the first accepted canonical player Position, defer other projections until that origin exists, and retain logical entity identity through hydration.

Advance the project ledger from completed M2 to active M3, synchronize CLAUDE.md/AGENTS.md and durable memory, and record the next cast-lifecycle, spellbook/enchantment, and two-client portal gates.

Co-Authored-By: Codex <noreply@openai.com>
2026-07-15 08:19:23 +02:00

196 lines
6.8 KiB
C#

using System.Numerics;
using AcDream.App.Input;
using AcDream.Core.Physics;
using Xunit;
namespace AcDream.App.Tests.Physics;
public sealed class PlayerOutboundPositionTests
{
private static readonly Plane GroundPlane =
new(Vector3.UnitZ, 0f);
[Fact]
public void CrossLandblockWorldFrame_SendsCarriedCellLocalPosition()
{
var controller = new PlayerMovementController(new PhysicsEngine());
// The render origin remains A9B1, so a point 22.56 m into A9B2 is
// represented at world Y=214.56. The wire frame must remain the
// carried A9B2-local 22.56, never the render-space 214.56.
controller.SetPosition(
new Vector3(116.07f, 214.56f, 83.76f),
0xA9B20021u,
new Vector3(116.07f, 22.56f, 83.76f));
Assert.True(controller.TryGetOutboundPosition(
Quaternion.Identity,
out uint cellId,
out Vector3 localOrigin));
Assert.Equal(0xA9B20021u, cellId);
Assert.Equal(new Vector3(116.07f, 22.56f, 83.76f), localOrigin);
Assert.NotEqual(controller.Position, localOrigin);
}
[Fact]
public void UnplacedBody_DoesNotProduceWirePosition()
{
var controller = new PlayerMovementController(new PhysicsEngine());
Assert.False(controller.TryGetOutboundPosition(
Quaternion.Identity,
out _,
out _));
}
[Fact]
public void PositionEvent_FirstFrameIsDue()
{
var controller = new PlayerMovementController(new PhysicsEngine());
var current = PositionAt(Quaternion.Identity);
Assert.True(controller.ShouldSendPositionEvent(current, GroundPlane, 0f));
}
[Fact]
public void PositionEvent_StationaryHeadingChangeIsDueAfterRetailInterval()
{
var controller = new PlayerMovementController(new PhysicsEngine());
var sent = PositionAt(Quaternion.Identity);
var turned = PositionAt(Quaternion.CreateFromAxisAngle(
Vector3.UnitZ,
MathF.PI / 2f));
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
Assert.False(controller.ShouldSendPositionEvent(
turned,
GroundPlane,
PlayerMovementController.HeartbeatInterval));
Assert.True(controller.ShouldSendPositionEvent(
turned,
GroundPlane,
PlayerMovementController.HeartbeatInterval + 0.01f));
}
[Fact]
public void PositionEvent_UnchangedCompleteFrameIsNotIdleHeartbeat()
{
var controller = new PlayerMovementController(new PhysicsEngine());
var sent = PositionAt(Quaternion.Identity);
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
Assert.False(controller.ShouldSendPositionEvent(
sent,
GroundPlane,
PlayerMovementController.HeartbeatInterval + 0.01f));
}
[Fact]
public void MovementEvent_StampsOnlyTime_NotTheRememberedFrame()
{
var controller = new PlayerMovementController(new PhysicsEngine());
var sent = PositionAt(Quaternion.Identity);
var turned = PositionAt(Quaternion.CreateFromAxisAngle(
Vector3.UnitZ,
MathF.PI / 2f));
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
controller.NoteMovementSent(nowSeconds: 0.9f);
Assert.False(controller.ShouldSendPositionEvent(
turned,
GroundPlane,
nowSeconds: 1.1f));
Assert.True(controller.ShouldSendPositionEvent(
turned,
GroundPlane,
nowSeconds: 1.91f));
}
[Fact]
public void PositionEvent_CellOrContactPlaneChangeIsDueInsideInterval()
{
var controller = new PlayerMovementController(new PhysicsEngine());
var sent = PositionAt(Quaternion.Identity);
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
var otherCell = sent with { ObjCellId = 0xA9B20022u };
var otherPlane = new Plane(Vector3.Normalize(new Vector3(0.01f, 0f, 1f)), 0f);
Assert.True(controller.ShouldSendPositionEvent(otherCell, GroundPlane, 0.1f));
Assert.True(controller.ShouldSendPositionEvent(sent, otherPlane, 0.1f));
}
[Fact]
public void PositionEvent_QuaternionSignIsStoredFrameDataLikeRetail()
{
var controller = new PlayerMovementController(new PhysicsEngine());
var sent = PositionAt(Quaternion.Identity);
var oppositeStoredQuaternion = PositionAt(new Quaternion(0f, 0f, 0f, -1f));
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
Assert.True(controller.ShouldSendPositionEvent(
oppositeStoredQuaternion,
GroundPlane,
PlayerMovementController.HeartbeatInterval + 0.01f));
}
[Fact]
public void PositionEvent_RetailEpsilonUsesDifferentOriginAndQuaternionBoundaries()
{
const float epsilon = 0.000199999995f;
var controller = new PlayerMovementController(new PhysicsEngine());
var sent = new Position(0xA9B20021u, Vector3.Zero, Quaternion.Identity);
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
var exactOriginBoundary = sent with
{
Frame = sent.Frame with { Origin = new Vector3(epsilon, 0f, 0f) },
};
var exactQuaternionBoundary = sent with
{
Frame = sent.Frame with
{
Orientation = new Quaternion(epsilon, 0f, 0f, 1f),
},
};
float elapsed = PlayerMovementController.HeartbeatInterval + 0.01f;
Assert.False(controller.ShouldSendPositionEvent(
exactOriginBoundary,
GroundPlane,
elapsed));
Assert.True(controller.ShouldSendPositionEvent(
exactQuaternionBoundary,
GroundPlane,
elapsed));
}
[Fact]
public void PositionEvent_RetailPlaneDistanceBoundaryIsStrict()
{
const float epsilon = 0.000199999995f;
var controller = new PlayerMovementController(new PhysicsEngine());
var sent = PositionAt(Quaternion.Identity);
controller.NotePositionSent(sent, GroundPlane, nowSeconds: 0f);
var exactNormalBoundary = new Plane(
new Vector3(epsilon, 0f, 1f),
0f);
var exactDistanceBoundary = new Plane(Vector3.UnitZ, epsilon);
Assert.False(controller.ShouldSendPositionEvent(
sent,
exactNormalBoundary,
nowSeconds: 0.1f));
Assert.True(controller.ShouldSendPositionEvent(
sent,
exactDistanceBoundary,
nowSeconds: 0.1f));
}
private static Position PositionAt(Quaternion orientation)
=> new(
0xA9B20021u,
new Vector3(116.07f, 22.56f, 83.76f),
orientation);
}