fix(client): restore retail interaction parity
All checks were successful
CI / linux-portable (push) Successful in 3m27s
CI / windows-gate (push) Successful in 6m42s
CI / release (push) Successful in 2m12s

Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
Erik 2026-08-26 20:45:11 +02:00
parent 0c699240e0
commit f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions

View file

@ -9,17 +9,18 @@ namespace AcDream.App.Tests.Rendering;
public sealed class PaperdollFramePresenterTests
{
[Fact]
public void HiddenView_DoesNotBuildOrRender()
public void HiddenView_BuildsAndPrewarmsWithoutRendering()
{
var renderer = new RecordingRenderer();
var view = new RecordingView { Visible = false };
var factory = new RecordingFactory();
var factory = new RecordingFactory { Doll = CreateDoll() };
var presenter = new PaperdollFramePresenter(renderer, view, factory);
presenter.Render();
Assert.True(presenter.IsDirty);
Assert.Equal(0, factory.BuildCount);
Assert.False(presenter.IsDirty);
Assert.Equal(1, factory.BuildCount);
Assert.Equal(1, renderer.PrepareCount);
Assert.Equal(0, renderer.RenderCount);
Assert.Empty(view.TextureHandles);
}
@ -255,9 +256,12 @@ public sealed class PaperdollFramePresenterTests
public List<WorldEntity?> Dolls { get; } = [];
public List<(int Width, int Height)> RenderSizes { get; } = [];
public int RenderCount => RenderSizes.Count;
public int PrepareCount { get; private set; }
public void SetDoll(WorldEntity? doll) => Dolls.Add(doll);
public void Prepare() => PrepareCount++;
public uint Render(int width, int height)
{
RenderSizes.Add((width, height));

View file

@ -209,6 +209,25 @@ public class RetailChaseCameraTests
Assert.Equal(Vector3.Normalize(pivot - eye), forward);
}
[Fact]
public void MapMode_TargetDirectionTransformsViewerOffsetIntoOverheadPose()
{
var pivot = new Vector3(10f, 20f, 1.5f);
float distance = MathF.Sqrt(450f * 450f + 0.75f * 0.75f);
float pitch = MathF.Atan2(0.75f, 450f);
var (eye, forward) = RetailChaseCamera.ComputeTargetDirectionPose(
pivot,
Vector3.UnitX,
distance,
pitch,
new Vector3(0f, 0.5f, -1.8f));
Assert.True(eye.Z > 430f, $"expected retail overhead eye, got Z={eye.Z}");
Assert.InRange(Vector2.Distance(new Vector2(eye.X, eye.Y), new Vector2(pivot.X, pivot.Y)), 119f, 122f);
Assert.True(forward.Z < -0.95f, $"expected steep downward view, got {forward}");
}
[Fact]
public void Basis_HorizontalHeading_IsOrthonormalAndRightHanded()
{
@ -528,6 +547,41 @@ public class RetailChaseCameraTests
Assert.Equal(RetailChaseCamera.DistanceMax, cam.Distance);
}
[Fact]
public void SetRetailFirstPersonView_PlacesEyeAheadAndLooksForward()
{
var cam = new RetailChaseCamera();
cam.SetRetailFirstPersonView();
cam.Update(
playerPosition: Vector3.Zero,
playerYaw: 0f,
playerVelocity: Vector3.Zero,
isOnGround: true,
contactPlaneNormal: Vector3.UnitZ,
dt: 1f / 60f);
Assert.True(cam.IsInHead);
Assert.Equal(new Vector3(0.18f, 0f, 1.5f), cam.Position);
Assert.Equal(1f, cam.PlayerTranslucency, 5);
var (_, forward) = RetailChaseCamera.ComputeInHeadPose(
new Vector3(0f, 0f, 1.5f),
Vector3.UnitX);
Assert.Equal(Vector3.UnitX, forward);
}
[Fact]
public void AdjustingZoomExitsRetailFirstPersonView()
{
var cam = new RetailChaseCamera();
cam.SetRetailFirstPersonView();
cam.AdjustDistance(1f);
Assert.False(cam.IsInHead);
Assert.Equal(RetailChaseCamera.DistanceMin + 1f, cam.Distance);
}
[Fact]
public void AdjustPitch_ClampsToRange()
{