feat(ui): D.2b item interaction + retail cursors + live character sheet
Lands the codex-worktree D.2b stream plus the extraction the 2026-07-02 UI architecture review mandated before commit: - ItemInteractionController: single owner of double-click use/equip/ container-open, targeted-use mode (health kits), drag-out drop; toolbar shortcut drags don't drop the real item. ItemEquipRules for multi-slot (coat) coverage via equip masks. - Cursor phase: CursorFeedbackController (semantic priority chain: drag > resize > window-move > target-mode > text) + RetailCursorCatalog (enums 0x27/0x28/0x29, hotspot 14,14; ClientUISystem::UpdateCursorState 0x00564630) resolved through the portal EnumIDMap chain by RetailCursorResolver; RetailCursorManager applies dat cursor art to the OS cursor. Register row AP-72 covers the OS standard-cursor fallback. - Character window goes live: CharacterSheetProvider owns sheet assembly, XP-curve/raise-cost math and the raise flow — extracted out of GameWindow per Code Structure Rule 1 instead of committing the ~430-line feature body there. Optimistic XP/credit debits go through eventful store APIs (new ClientObjectTable.UpdateInt64Property + LocalPlayerState.DebitIntProperty/DebitInt64Property) instead of raw property-dictionary writes; register row AP-73 covers the still-missing raise ledger (#163). - RetailWindowFrame: the shared nine-slice window mount recipe; the character window uses it, remaining windows migrate via #164. - Status-bar buttons toggle inventory/character windows; retail row-major backpack ordering; WorldSession.SendUseWithTarget + raise/train sends. GameWindow shrinks 14,214 -> 13,877 lines despite the new features; the sheet/raise logic is unit-tested in CharacterSheetProviderTests instead of trapped in the god object. Build green; full suite 3,286 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e3fc7ac5ba
commit
b7dc91a053
74 changed files with 6669 additions and 238 deletions
108
src/AcDream.App/UI/Layout/RetailWindowFrame.cs
Normal file
108
src/AcDream.App/UI/Layout/RetailWindowFrame.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Mounts an imported retail window's content into the shared 8-piece beveled
|
||||
/// chrome (<see cref="UiNineSlicePanel"/>) and optionally registers it with
|
||||
/// the <see cref="UiRoot"/> window manager. This is THE mount recipe every
|
||||
/// retail window follows: frame sized content + 2×border, content inset by
|
||||
/// the border with Draggable/Resizable off (the frame owns move/resize).
|
||||
///
|
||||
/// <para>The vitals/chat/toolbar/inventory windows still inline this recipe
|
||||
/// in <c>GameWindow</c> (pre-extraction); new windows must mount through this
|
||||
/// helper instead of copying the block again — see
|
||||
/// docs/research/2026-07-02-ui-architecture-review.md (theme T1).</para>
|
||||
/// </summary>
|
||||
public static class RetailWindowFrame
|
||||
{
|
||||
/// <summary>Per-window placement + behavior knobs. Null-valued knobs keep
|
||||
/// the widget defaults (<see cref="UiElement"/> / <see cref="UiNineSlicePanel"/>).</summary>
|
||||
public sealed record Options
|
||||
{
|
||||
/// <summary>Initial window position (screen px).</summary>
|
||||
public float Left { get; init; }
|
||||
public float Top { get; init; }
|
||||
|
||||
/// <summary>Minimum frame size; null = lock to content + 2×border (non-shrinkable).</summary>
|
||||
public float? MinWidth { get; init; }
|
||||
public float? MinHeight { get; init; }
|
||||
|
||||
/// <summary>Maximum frame height while resizing; null = unbounded.</summary>
|
||||
public float? MaxHeight { get; init; }
|
||||
|
||||
/// <summary>Allow horizontal resize (frame default: true).</summary>
|
||||
public bool ResizeX { get; init; } = true;
|
||||
|
||||
/// <summary>Restrict which edges start a resize; null = all edges.</summary>
|
||||
public ResizeEdges? ResizableEdges { get; init; }
|
||||
|
||||
/// <summary>Window translucency (retail SetDefaultOpacity analog).</summary>
|
||||
public float Opacity { get; init; } = 1f;
|
||||
|
||||
/// <summary>Start shown or hidden (hidden windows toggle via the window manager).</summary>
|
||||
public bool Visible { get; init; } = true;
|
||||
|
||||
/// <summary>How the content tracks the frame when it resizes.</summary>
|
||||
public AnchorEdges ContentAnchors { get; init; } =
|
||||
AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom;
|
||||
|
||||
/// <summary>Force the content's ClickThrough; null = leave as imported.</summary>
|
||||
public bool? ContentClickThrough { get; init; }
|
||||
|
||||
/// <summary>Register the frame under this name in the UiRoot window
|
||||
/// manager (<see cref="WindowNames"/>); null = unmanaged window.</summary>
|
||||
public string? WindowName { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrap <paramref name="content"/> (sized by the importer) in the beveled
|
||||
/// chrome, add it to <paramref name="root"/>, and register it when
|
||||
/// <see cref="Options.WindowName"/> is set. Returns the frame.
|
||||
/// </summary>
|
||||
public static UiNineSlicePanel Mount(
|
||||
UiRoot root,
|
||||
UiElement content,
|
||||
Func<uint, (uint handle, int w, int h)> resolveChrome,
|
||||
Options options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(root);
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
ArgumentNullException.ThrowIfNull(resolveChrome);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
const int border = RetailChromeSprites.Border;
|
||||
float contentW = content.Width;
|
||||
float contentH = content.Height;
|
||||
|
||||
var frame = new UiNineSlicePanel(resolveChrome)
|
||||
{
|
||||
Left = options.Left,
|
||||
Top = options.Top,
|
||||
Width = contentW + 2 * border,
|
||||
Height = contentH + 2 * border,
|
||||
MinWidth = options.MinWidth ?? contentW + 2 * border,
|
||||
MinHeight = options.MinHeight ?? contentH + 2 * border,
|
||||
ResizeX = options.ResizeX,
|
||||
Opacity = options.Opacity,
|
||||
Visible = options.Visible,
|
||||
};
|
||||
if (options.MaxHeight is { } maxHeight) frame.MaxHeight = maxHeight;
|
||||
if (options.ResizableEdges is { } edges) frame.ResizableEdges = edges;
|
||||
|
||||
content.Left = border;
|
||||
content.Top = border;
|
||||
content.Width = contentW;
|
||||
content.Height = contentH;
|
||||
content.Anchors = options.ContentAnchors;
|
||||
if (options.ContentClickThrough is { } clickThrough) content.ClickThrough = clickThrough;
|
||||
content.Draggable = false; // the frame owns window move/resize
|
||||
content.Resizable = false;
|
||||
|
||||
frame.AddChild(content);
|
||||
root.AddChild(frame);
|
||||
if (options.WindowName is { } name)
|
||||
root.RegisterWindow(name, frame);
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue