diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 5b3199f..897c26c 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -197,8 +197,25 @@ public sealed class GameWindow : IDisposable if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var playerEntity)) { _playerController = new AcDream.App.Input.PlayerMovementController(_physicsEngine); - _playerController.SetPosition(playerEntity.Position, 0x0001u); - _playerController.Yaw = MathF.PI / 2f; // default facing +Y + // Derive initial cell ID from the entity's world position. + int plbX = _liveCenterX + (int)MathF.Floor(playerEntity.Position.X / 192f); + int plbY = _liveCenterY + (int)MathF.Floor(playerEntity.Position.Y / 192f); + 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); + // Derive initial yaw from the entity's server-sent rotation + // rather than hardcoding. Extract yaw from the quaternion. + var q = playerEntity.Rotation; + float yaw = MathF.Atan2( + 2f * (q.W * q.Z + q.X * q.Y), + 1f - 2f * (q.Y * q.Y + q.Z * q.Z)); + _playerController.Yaw = yaw; _chaseCamera = new AcDream.App.Rendering.ChaseCamera { Aspect = _window!.Size.X / (float)_window.Size.Y, @@ -1547,8 +1564,11 @@ public sealed class GameWindow : IDisposable var mouse = _input.Mice.FirstOrDefault(); if (mouse is null) return; - mouse.Cursor.CursorMode = isFlyMode ? CursorMode.Raw : CursorMode.Normal; - _capturedMouse = isFlyMode ? mouse : null; + // Raw cursor mode for both fly AND chase (player) mode — both need + // mouse deltas for look/turn. Only orbit mode uses normal cursor. + bool needsRawCursor = isFlyMode || _playerMode; + mouse.Cursor.CursorMode = needsRawCursor ? CursorMode.Raw : CursorMode.Normal; + _capturedMouse = needsRawCursor ? mouse : null; } // Performance overlay state — updated every ~0.5s and written to the @@ -1731,7 +1751,35 @@ public sealed class GameWindow : IDisposable _playerCurrentAnimCommand = animCommand; if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe)) return; - if (!_animatedEntities.TryGetValue(pe.Id, out var ae)) return; + + // The player entity may not be in _animatedEntities if a post-spawn + // UpdateMotion removed it (Phase 6.8 pattern). In that case, load + // the Setup and re-register. This is the player's own character so + // we always want it animated in player mode. + if (!_animatedEntities.TryGetValue(pe.Id, out var ae)) + { + var setup = _dats.Get(pe.SourceGfxObjOrSetupId); + if (setup is null) return; + + // Build a minimal part template from the entity's current MeshRefs. + var template = new (uint, IReadOnlyDictionary?)[pe.MeshRefs.Count]; + for (int i = 0; i < pe.MeshRefs.Count; i++) + template[i] = (pe.MeshRefs[i].GfxObjId, pe.MeshRefs[i].SurfaceOverrides); + + ae = new AnimatedEntity + { + Entity = pe, + Setup = setup, + Animation = null!, // filled below + LowFrame = 0, + HighFrame = 0, + Framerate = 30f, + Scale = 1f, + PartTemplate = template, + CurrFrame = 0f, + }; + _animatedEntities[pe.Id] = ae; + } ushort cmdOverride = (ushort)(animCommand & 0xFFFFu); var cycle = AcDream.Core.Meshing.MotionResolver.GetIdleCycle(