diff --git a/src/AcDream.App/Rendering/DollCamera.cs b/src/AcDream.App/Rendering/DollCamera.cs
index b2ba8ff4..c1616bfe 100644
--- a/src/AcDream.App/Rendering/DollCamera.cs
+++ b/src/AcDream.App/Rendering/DollCamera.cs
@@ -3,21 +3,26 @@ using System.Numerics;
namespace AcDream.App.Rendering;
///
-/// Fixed camera for the paperdoll mini-scene. Eye and look-at from
-/// gmPaperDollUI::PostInit (decomp lines 175521–175527):
-/// eye (0.12, -2.4, 0.88) looking at the origin, AC up-axis = +Z.
+/// Fixed camera for the paperdoll mini-scene. Derived from gmPaperDollUI::PostInit
+/// (decomp lines 175521–175527: eye offset ~(0.12, -2.4, 0.88) about the origin), but with the
+/// look-at raised to mid-body and the camera pulled back after the first visual gate: the doll's
+/// model origin is at the FEET (z=0), so aiming at the origin framed only the lower legs. We aim
+/// at ~0.95 m (a human's vertical centre) and stand back ~3.7 m so the whole ~1.9 m figure fits.
+/// AC up-axis = +Z.
///
/// Uses the same /
/// convention as
/// so the doll's triangle winding and back-face
/// culling match the world render pass.
///
-/// Per-race camera distance (UpdateForRace) is deferred polish.
+/// Exact per-race camera framing (UpdateForRace) is deferred polish.
///
public sealed class DollCamera : ICamera
{
- private static readonly Vector3 Eye = new(0.12f, -2.4f, 0.88f);
- private static readonly Vector3 Target = Vector3.Zero;
+ // Body centre height (metres) the camera aims at — model origin is at the feet (z=0).
+ private const float BodyCentreZ = 0.95f;
+ private static readonly Vector3 Eye = new(0.12f, -3.7f, BodyCentreZ);
+ private static readonly Vector3 Target = new(0f, 0f, BodyCentreZ);
private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera
/// Vertical field of view in radians. ~34° default; tune at the visual gate.
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 860568d6..c36f3d1b 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -2261,7 +2261,8 @@ public sealed class GameWindow : IDisposable
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
// Empty equip slots show a visible frame (same square as the inventory grid) so every
// slot position is seen + usable; the live 3D character (the doll) is Slice 2.
- emptySlotSprite: contentsEmpty);
+ emptySlotSprite: contentsEmpty,
+ datFont: vitalsDatFont); // Slice 2: caption the "Slots" toggle button
// Slice 2: capture the doll viewport widget (Type 0xD, element 0x100001D5) + the inventory
// frame so the per-frame pre-UI hook can render the 3-D doll into the widget's texture only
diff --git a/src/AcDream.App/UI/Layout/PaperdollController.cs b/src/AcDream.App/UI/Layout/PaperdollController.cs
index 82c47758..77d5f9c4 100644
--- a/src/AcDream.App/UI/Layout/PaperdollController.cs
+++ b/src/AcDream.App/UI/Layout/PaperdollController.cs
@@ -85,7 +85,7 @@ public sealed class PaperdollController : IItemListDragHandler
private PaperdollController(
ImportedLayout layout, ClientObjectTable objects, Func playerGuid,
Func iconIds, Action? sendWield,
- uint emptySlotSprite)
+ uint emptySlotSprite, UiDatFont? datFont)
{
_objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _sendWield = sendWield;
@@ -117,8 +117,27 @@ public sealed class PaperdollController : IItemListDragHandler
_dollViewport = layout.FindElement(0x100001D5u); // doll viewport (may be null until Slice 3)
- if (layout.FindElement(0x100005BEu) is UiButton slotsBtn)
+ var slotsBtnEl = layout.FindElement(0x100005BEu);
+ if (slotsBtnEl is UiButton slotsBtn)
+ {
slotsBtn.OnClick = () => { _viewState.Toggle(); ApplyView(); };
+ // The dat element has no face sprite, so without a label the button is invisible. Give it a
+ // "Slots" caption (gold, like the chat Send button) so it's findable + clickable.
+ if (datFont is not null)
+ {
+ slotsBtn.Label = "Slots";
+ slotsBtn.LabelFont = datFont;
+ slotsBtn.LabelColor = new System.Numerics.Vector4(1f, 0.92f, 0.72f, 1f);
+ }
+ }
+
+ // Slice 2 diagnostic (temporary): confirm the importer surfaced the toggle button + doll
+ // viewport, and WHERE the button sits. Stripped once the toggle is visually confirmed.
+ Console.WriteLine($"[Slice2-paperdoll] armorSlots={_armorSlots.Count}/9 " +
+ $"viewport={_dollViewport?.GetType().Name ?? "MISSING"} " +
+ $"slotsButton={slotsBtnEl?.GetType().Name ?? "MISSING"} " +
+ $"btnRect=({slotsBtnEl?.Left ?? -1},{slotsBtnEl?.Top ?? -1},{slotsBtnEl?.Width ?? -1},{slotsBtnEl?.Height ?? -1}) " +
+ $"btnVisible={slotsBtnEl?.Visible.ToString() ?? "n/a"}");
ApplyView(); // initial state = doll-view (armor slots hidden)
@@ -128,8 +147,8 @@ public sealed class PaperdollController : IItemListDragHandler
public static PaperdollController Bind(
ImportedLayout layout, ClientObjectTable objects, Func playerGuid,
Func iconIds, Action? sendWield = null,
- uint emptySlotSprite = 0u)
- => new PaperdollController(layout, objects, playerGuid, iconIds, sendWield, emptySlotSprite);
+ uint emptySlotSprite = 0u, UiDatFont? datFont = null)
+ => new PaperdollController(layout, objects, playerGuid, iconIds, sendWield, emptySlotSprite, datFont);
private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); }
private void OnObjectMoved(ClientObject o, uint from, uint to)
diff --git a/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs b/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs
index c65afdfb..6a82af78 100644
--- a/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/DollCameraTests.cs
@@ -7,14 +7,16 @@ namespace AcDream.App.Tests.Rendering;
public class DollCameraTests
{
[Fact]
- public void Eye_position_is_the_retail_camera_offset()
+ public void Eye_position_frames_the_doll_from_the_front()
{
var cam = new DollCamera { Aspect = 100f / 214f };
Assert.True(Matrix4x4.Invert(cam.View, out var inv));
var eye = inv.Translation;
+ // Tuned at the visual gate: aim at mid-body (z≈0.95) and stand back ~3.7 m so the whole
+ // figure fits (model origin is at the feet, so aiming at the origin framed only the legs).
Assert.Equal(0.12f, eye.X, 3);
- Assert.Equal(-2.4f, eye.Y, 3);
- Assert.Equal(0.88f, eye.Z, 3);
+ Assert.Equal(-3.7f, eye.Y, 3);
+ Assert.Equal(0.95f, eye.Z, 3);
}
[Fact]