fix(D.2b): Slice 2 visual gate — frame the whole doll + caption the Slots toggle

Visual gate 1 (user): the doll rendered but only the legs showed (camera
aimed at the model origin = the feet) and the Slots button was invisible.

- DollCamera: aim the look-at at mid-body (~0.95 m) and stand back ~3.7 m
  so the whole ~1.9 m figure fits. (Size is a later retail-comparison
  polish per the user.)
- PaperdollController: the Slots button (0x100005BE) is found + wired
  (diagnostic confirmed armorSlots=9/9, viewport=UiViewport,
  slotsButton=UiButton) but its dat element has no face sprite, so it drew
  nothing. Give it a gold "Slots" caption (UiButton.Label, like chat Send)
  via a new datFont param on Bind. Temporary [Slice2-paperdoll] diagnostic
  logs the button rect + the found widgets (stripped at wrap-up).

Idle animation deferred to a focused follow-up (faithful idle needs a full
AnimatedEntity + Sequencer through the TickAnimations multi-branch path +
re-dress coordination — real integration risk vs the verified static doll).

Build + full App suite green (594).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-23 09:58:22 +02:00
parent 8b0365e7a9
commit fe319bd2aa
4 changed files with 41 additions and 14 deletions

View file

@ -3,21 +3,26 @@ using System.Numerics;
namespace AcDream.App.Rendering;
/// <summary>
/// Fixed camera for the paperdoll mini-scene. Eye and look-at from
/// <c>gmPaperDollUI::PostInit</c> (decomp lines 175521175527):
/// eye (0.12, -2.4, 0.88) looking at the origin, AC up-axis = +Z.
/// Fixed camera for the paperdoll mini-scene. Derived from <c>gmPaperDollUI::PostInit</c>
/// (decomp lines 175521175527: 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 <see cref="Matrix4x4.CreateLookAt"/> /
/// <see cref="Matrix4x4.CreatePerspectiveFieldOfView"/> convention as
/// <see cref="ChaseCamera"/> so the doll's triangle winding and back-face
/// culling match the world render pass.
///
/// Per-race camera distance (<c>UpdateForRace</c>) is deferred polish.
/// Exact per-race camera framing (<c>UpdateForRace</c>) is deferred polish.
/// </summary>
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
/// <summary>Vertical field of view in radians. ~34° default; tune at the visual gate.</summary>

View file

@ -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

View file

@ -85,7 +85,7 @@ public sealed class PaperdollController : IItemListDragHandler
private PaperdollController(
ImportedLayout layout, ClientObjectTable objects, Func<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds, Action<uint, uint>? 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<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds, Action<uint, uint>? 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)