fix(app): Phase B.2 — use server position directly, fix yaw wrap + turn spam
Three more fixes from the diagnostic dump: 1. Initial position: PhysicsEngine.Resolve was mapping the player into an indoor EnvCell (foundry at Z=66) when they're standing on outdoor terrain at Z=93+. The cell-containment check was too aggressive for initial placement. Now uses the server-sent position directly — the server already gave us a valid position. 2. Yaw unbounded: mouse delta accumulated without wrapping, growing to 24+ radians. Now wraps to [-PI, PI] after every turn. 3. Turn command spam: MouseDeltaX > 0.5 threshold was too low for raw pixel deltas. Any mouse jitter triggered turnCmd flips every frame → stateChanged=True → MoveToState flood to the server. Mouse turning now only affects yaw directly; turn COMMANDS only come from A/D keyboard (matching retail client behavior where mouse-look doesn't generate a TurnRight/TurnLeft command). 265 tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6202c5d153
commit
97c17c5bc3
3 changed files with 39 additions and 8 deletions
|
|
@ -79,6 +79,10 @@ public sealed class PlayerMovementController
|
|||
if (input.TurnLeft)
|
||||
Yaw += TurnSpeed * dt;
|
||||
Yaw -= input.MouseDeltaX * MouseTurnSensitivity;
|
||||
// Wrap yaw to [-PI, PI] so it doesn't grow unbounded and cause
|
||||
// NaN/overflow in sin/cos and confuse the chase camera offset.
|
||||
while (Yaw > MathF.PI) Yaw -= 2f * MathF.PI;
|
||||
while (Yaw < -MathF.PI) Yaw += 2f * MathF.PI;
|
||||
|
||||
// 2. Compute movement delta in the player's facing direction.
|
||||
float speed = input.Run ? RunSpeed : WalkSpeed;
|
||||
|
|
@ -130,12 +134,17 @@ public sealed class PlayerMovementController
|
|||
sidestepSpeed = speed * 0.5f / WalkSpeed;
|
||||
}
|
||||
|
||||
if (input.TurnRight || input.MouseDeltaX > 0.5f)
|
||||
// Turn commands from KEYBOARD only (A/D). Mouse turning is applied
|
||||
// directly to Yaw above and doesn't generate a turn command — if it
|
||||
// did, mouse jitter would flip turnCmd between TurnRight/TurnLeft
|
||||
// every frame, causing stateChanged=True on every frame and flooding
|
||||
// the server with MoveToState spam.
|
||||
if (input.TurnRight)
|
||||
{
|
||||
turnCmd = 0x6500000Du; // TurnRight
|
||||
turnSpeed = TurnSpeed;
|
||||
}
|
||||
else if (input.TurnLeft || input.MouseDeltaX < -0.5f)
|
||||
else if (input.TurnLeft)
|
||||
{
|
||||
turnCmd = 0x6500000Eu; // TurnLeft
|
||||
turnSpeed = TurnSpeed;
|
||||
|
|
|
|||
|
|
@ -203,12 +203,15 @@ public sealed class GameWindow : IDisposable
|
|||
float plocalX = playerEntity.Position.X - (plbX - _liveCenterX) * 192f;
|
||||
float plocalY = playerEntity.Position.Y - (plbY - _liveCenterY) * 192f;
|
||||
uint pinitCellId = ((uint)plbX << 24) | ((uint)plbY << 16) | 0x0001u;
|
||||
// Let the physics engine resolve the correct Z for the initial position
|
||||
// so we don't start underground.
|
||||
var initResult = _physicsEngine.Resolve(
|
||||
playerEntity.Position, pinitCellId,
|
||||
System.Numerics.Vector3.Zero, 10f);
|
||||
_playerController.SetPosition(initResult.Position, initResult.CellId);
|
||||
// Use the server-sent position directly — the server already
|
||||
// gave us a valid position at CreateObject time. Running it
|
||||
// through PhysicsEngine.Resolve was mapping the player into
|
||||
// an indoor cell (e.g., the foundry at Z=66) when they're
|
||||
// actually standing on outdoor terrain at Z=93+. The physics
|
||||
// engine's cell-containment check is too aggressive for the
|
||||
// initial placement. Once the player starts moving, Resolve
|
||||
// will keep them on the correct surface.
|
||||
_playerController.SetPosition(playerEntity.Position, pinitCellId & 0xFFFFu);
|
||||
// Derive initial yaw from the entity's server-sent rotation
|
||||
// rather than hardcoding. Extract yaw from the quaternion.
|
||||
var q = playerEntity.Rotation;
|
||||
|
|
@ -216,6 +219,12 @@ public sealed class GameWindow : IDisposable
|
|||
2f * (q.W * q.Z + q.X * q.Y),
|
||||
1f - 2f * (q.Y * q.Y + q.Z * q.Z));
|
||||
_playerController.Yaw = yaw;
|
||||
|
||||
Console.WriteLine($"[PLAYER-INIT] entityPos=({playerEntity.Position.X:F1},{playerEntity.Position.Y:F1},{playerEntity.Position.Z:F1}) " +
|
||||
$"entityRot=({q.X:F3},{q.Y:F3},{q.Z:F3},{q.W:F3}) " +
|
||||
$"initCellId=0x{(pinitCellId & 0xFFFFu):X4} " +
|
||||
$"yaw={yaw:F3} " +
|
||||
$"physics landblocks={_physicsEngine.LandblockCount}");
|
||||
_chaseCamera = new AcDream.App.Rendering.ChaseCamera
|
||||
{
|
||||
Aspect = _window!.Size.X / (float)_window.Size.Y,
|
||||
|
|
@ -1489,6 +1498,16 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
var result = _playerController.Update((float)dt, input);
|
||||
|
||||
// DIAG: dump player state every ~60 frames to see what's happening.
|
||||
if (_perfFrameCount % 60 == 0)
|
||||
{
|
||||
Console.WriteLine($"[PLAYER] pos=({result.Position.X:F1},{result.Position.Y:F1},{result.Position.Z:F1}) " +
|
||||
$"cell=0x{result.CellId:X8} ground={result.IsOnGround} " +
|
||||
$"yaw={_playerController.Yaw:F2} " +
|
||||
$"fwdCmd={result.ForwardCommand?.ToString("X8") ?? "idle"} " +
|
||||
$"stateChanged={result.MotionStateChanged}");
|
||||
}
|
||||
|
||||
// Update the player entity's position + rotation so it renders at
|
||||
// the physics-resolved location each frame.
|
||||
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ public sealed class PhysicsEngine
|
|||
{
|
||||
private readonly Dictionary<uint, LandblockPhysics> _landblocks = new();
|
||||
|
||||
/// <summary>Number of registered landblocks (diagnostic).</summary>
|
||||
public int LandblockCount => _landblocks.Count;
|
||||
|
||||
private sealed record LandblockPhysics(
|
||||
TerrainSurface Terrain,
|
||||
IReadOnlyList<CellSurface> Cells,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue