feat(ui): port retail creature appraisal presentation
Render assessed creatures through the shared private viewport with retail heading, bounding-box camera, and light. Build the exact authored nine-row stat list and resolve creature names from the retail EnumMapper while keeping remaining font/sequencer adaptations explicit. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
f1a7912160
commit
7eaa68a5f4
26 changed files with 2780 additions and 237 deletions
|
|
@ -0,0 +1,196 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class CreatureAppraisalPresentationTests
|
||||
{
|
||||
[Fact]
|
||||
public void CameraFitsRetailVerticalSpanAndKeepsIdentityLookDirection()
|
||||
{
|
||||
var camera = new CreatureAppraisalCamera { Aspect = 2f };
|
||||
|
||||
camera.Fit(
|
||||
new Vector3(-1f, -0.5f, 0f),
|
||||
new Vector3(1f, 0.5f, 4f));
|
||||
|
||||
Assert.Equal(0f, camera.Eye.X, 5);
|
||||
Assert.Equal(-(4f * 1.20710683f + 0.5f), camera.Eye.Y, 5);
|
||||
Assert.Equal(2f, camera.Eye.Z, 5);
|
||||
Vector3 forward = -new Vector3(
|
||||
camera.View.M13,
|
||||
camera.View.M23,
|
||||
camera.View.M33);
|
||||
Assert.Equal(Vector3.UnitY, forward);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraUsesViewportAspectWhenCreatureIsWiderThanTall()
|
||||
{
|
||||
var camera = new CreatureAppraisalCamera { Aspect = 1f };
|
||||
|
||||
camera.Fit(
|
||||
new Vector3(-4f, -0.5f, 0f),
|
||||
new Vector3(4f, 0.5f, 2f));
|
||||
|
||||
Assert.Equal(-(8f * 1.20710683f + 0.5f), camera.Eye.Y, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CloneFactoryReusesIdentityAndFollowsCurrentAnimatedMeshRefs()
|
||||
{
|
||||
var firstMeshes = new[]
|
||||
{
|
||||
new MeshRef(0x01000001u, Matrix4x4.Identity),
|
||||
};
|
||||
var source = Entity(
|
||||
id: 17u,
|
||||
setup: 0x02000001u,
|
||||
firstMeshes);
|
||||
source.SetLocalBounds(
|
||||
new Vector3(-1f, -0.5f, 0f),
|
||||
new Vector3(1f, 0.5f, 2f));
|
||||
var lookup = new Lookup { Entity = source };
|
||||
var factory = new RetailCreatureAppraisalCloneFactory(lookup);
|
||||
|
||||
Assert.True(factory.TrySynchronize(
|
||||
17u,
|
||||
currentClone: null,
|
||||
out WorldEntity? firstClone,
|
||||
out Vector3 firstMin,
|
||||
out Vector3 firstMax));
|
||||
var nextMeshes = new[]
|
||||
{
|
||||
new MeshRef(
|
||||
0x01000001u,
|
||||
Matrix4x4.CreateTranslation(0f, 0f, 0.25f)),
|
||||
};
|
||||
source.MeshRefs = nextMeshes;
|
||||
Assert.True(factory.TrySynchronize(
|
||||
17u,
|
||||
firstClone,
|
||||
out WorldEntity? nextClone,
|
||||
out Vector3 nextMin,
|
||||
out Vector3 nextMax));
|
||||
|
||||
Assert.Same(firstClone, nextClone);
|
||||
Assert.Same(nextMeshes, nextClone!.MeshRefs);
|
||||
Assert.Equal(CreatureAppraisalEntityBuilder.RenderId, nextClone.Id);
|
||||
Assert.Equal(firstMin, nextMin);
|
||||
Assert.Equal(firstMax, nextMax);
|
||||
Assert.Equal([17u, 17u], lookup.Requests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PresenterRendersVisibleTargetAndClearsWhenProjectionDisappears()
|
||||
{
|
||||
WorldEntity clone = Entity(
|
||||
CreatureAppraisalEntityBuilder.RenderId,
|
||||
0x02000001u,
|
||||
[new MeshRef(0x01000001u, Matrix4x4.Identity)]);
|
||||
var renderer = new Renderer { Texture = 73u };
|
||||
var view = new View();
|
||||
var factory = new Factory { Clone = clone };
|
||||
var presenter = new CreatureAppraisalFramePresenter(
|
||||
renderer,
|
||||
view,
|
||||
factory);
|
||||
|
||||
presenter.Render();
|
||||
factory.Available = false;
|
||||
presenter.Render();
|
||||
|
||||
Assert.Equal([17u, 17u], factory.Requests);
|
||||
Assert.Same(clone, renderer.Creatures[0]);
|
||||
Assert.Null(renderer.Creatures[1]);
|
||||
Assert.Equal([(300, 265)], renderer.RenderSizes);
|
||||
Assert.Equal([73u, 0u], view.Textures);
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(
|
||||
uint id,
|
||||
uint setup,
|
||||
IReadOnlyList<MeshRef> meshes)
|
||||
=> new()
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = id,
|
||||
SourceGfxObjOrSetupId = setup,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = meshes,
|
||||
};
|
||||
|
||||
private sealed class Lookup : ICreatureAppraisalEntityLookup
|
||||
{
|
||||
public WorldEntity? Entity { get; init; }
|
||||
public List<uint> Requests { get; } = [];
|
||||
|
||||
public bool TryGet(uint serverGuid, out WorldEntity entity)
|
||||
{
|
||||
Requests.Add(serverGuid);
|
||||
entity = Entity!;
|
||||
return Entity is not null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Renderer : ICreatureAppraisalRenderer
|
||||
{
|
||||
public uint Texture { get; init; }
|
||||
public List<WorldEntity?> Creatures { get; } = [];
|
||||
public List<(int Width, int Height)> RenderSizes { get; } = [];
|
||||
|
||||
public void SetCreature(
|
||||
WorldEntity? creature,
|
||||
Vector3 boundsMin,
|
||||
Vector3 boundsMax)
|
||||
=> Creatures.Add(creature);
|
||||
|
||||
public uint Render(int width, int height)
|
||||
{
|
||||
RenderSizes.Add((width, height));
|
||||
return Texture;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class View : ICreatureAppraisalFrameView
|
||||
{
|
||||
public List<uint> Textures { get; } = [];
|
||||
|
||||
public bool TryGetVisibleTarget(
|
||||
out uint serverGuid,
|
||||
out int width,
|
||||
out int height)
|
||||
{
|
||||
serverGuid = 17u;
|
||||
width = 300;
|
||||
height = 265;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetTextureHandle(uint textureHandle) =>
|
||||
Textures.Add(textureHandle);
|
||||
}
|
||||
|
||||
private sealed class Factory : ICreatureAppraisalCloneFactory
|
||||
{
|
||||
public bool Available { get; set; } = true;
|
||||
public WorldEntity? Clone { get; init; }
|
||||
public List<uint> Requests { get; } = [];
|
||||
|
||||
public bool TrySynchronize(
|
||||
uint serverGuid,
|
||||
WorldEntity? currentClone,
|
||||
out WorldEntity? synchronizedClone,
|
||||
out Vector3 boundsMin,
|
||||
out Vector3 boundsMax)
|
||||
{
|
||||
Requests.Add(serverGuid);
|
||||
synchronizedClone = Available ? Clone : null;
|
||||
boundsMin = new Vector3(-1f);
|
||||
boundsMax = new Vector3(1f);
|
||||
return Available;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ public sealed class GameWindowRenderLeafCompositionTests
|
|||
AssertAppearsInOrder(
|
||||
presentation,
|
||||
"_portal.Draw(",
|
||||
"_paperdoll?.Render();",
|
||||
"_entityViewports?.Render();",
|
||||
"_gameplayUi?.Render(",
|
||||
"_devTools?.Render(",
|
||||
"_screenshots?.CapturePending(");
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public sealed class PrivatePresentationRendererTests
|
|||
var presentation = new PrivatePresentationRenderer(
|
||||
new Portal(calls),
|
||||
new FoundationSource(portalVisible: true),
|
||||
paperdoll: null,
|
||||
entityViewports: null,
|
||||
gameplayUi: null,
|
||||
devTools: null,
|
||||
screenshots: null);
|
||||
|
|
@ -64,6 +64,20 @@ public sealed class PrivatePresentationRendererTests
|
|||
Assert.False(outcome.ScreenshotCaptured);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrivateEntityViewportGroup_RendersEveryViewportInOrder()
|
||||
{
|
||||
List<string> calls = [];
|
||||
var group = new PrivateEntityViewportFrameGroup(
|
||||
new NamedViewport(calls, "paperdoll"),
|
||||
null,
|
||||
new NamedViewport(calls, "examination"));
|
||||
|
||||
group.Render();
|
||||
|
||||
Assert.Equal(["paperdoll", "examination"], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_ReportsThePreparedPortalSnapshotWhenDrawMutatesTheSource()
|
||||
{
|
||||
|
|
@ -72,7 +86,7 @@ public sealed class PrivatePresentationRendererTests
|
|||
var presentation = new PrivatePresentationRenderer(
|
||||
new MutatingPortal(calls, () => foundation.PortalVisible = false),
|
||||
foundation,
|
||||
paperdoll: null,
|
||||
entityViewports: null,
|
||||
gameplayUi: null,
|
||||
devTools: null,
|
||||
screenshots: null);
|
||||
|
|
@ -209,11 +223,17 @@ public sealed class PrivatePresentationRendererTests
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class Paperdoll(List<string> calls) : IPrivatePaperdollFrame
|
||||
private sealed class Paperdoll(List<string> calls) : IPrivateEntityViewportFrame
|
||||
{
|
||||
public void Render() => calls.Add("paperdoll");
|
||||
}
|
||||
|
||||
private sealed class NamedViewport(List<string> calls, string name) :
|
||||
IPrivateEntityViewportFrame
|
||||
{
|
||||
public void Render() => calls.Add(name);
|
||||
}
|
||||
|
||||
private sealed class GameplayUi(List<string> calls) : IRetainedGameplayUiFrame
|
||||
{
|
||||
public RenderFrameInput Input { get; private set; }
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ public sealed class RenderFrameRecoveryIntegrationTests
|
|||
public void Draw(int width, int height) => failure.ThrowIf("portal");
|
||||
}
|
||||
|
||||
private sealed class Paperdoll(FailureSwitch failure) : IPrivatePaperdollFrame
|
||||
private sealed class Paperdoll(FailureSwitch failure) : IPrivateEntityViewportFrame
|
||||
{
|
||||
public void Render() => failure.ThrowIf("paperdoll");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue