acdream/src/AcDream.App/Studio/SampleData.cs
Erik defbde1f86 feat(studio): Attributes tab Pass 2 — click-to-select (highlight + footer B + raise triangles) + tab states
- UiClickablePanel: new UiPanel subclass with OnClick action + HandlesClick=true
  so row clicks survive whole-window-Draggable ancestor frames.

- CharacterStatController overhaul (Pass 2):
  - Tab bar button states: SetButtonStateRecursive walks each tab group container
    (0x10000228/229/538) setting UiButton.ActiveState="Open" (Attributes) or
    "Closed" (Skills/Titles) via UIStateId enum string names from DatReaderWriter.
  - Row click: 9 rows become UiClickablePanel; sel[] mutable box drives footer/highlight.
  - Toggle: clicking the same row deselects (→ footer State A); click a new row
    updates title="Attrib: value", line-1 label="Experience To Raise:", line-1
    value=cost, line-2="Unassigned Experience:" in both states.
  - Row highlight: BackgroundColor=HighlightBg (semi-translucent gold) on selected row.
  - Raise buttons (0x10000246 ×1 + 0x100005EB ×10): hidden initially; shown on
    selection with ActiveState="Normal" (affordable) or "Ghosted" (cost=0 or unaffordable).
    CollectButtonsById tree-walk finds ALL copies of the button across tab-page mounts
    (not just the last-registered _byId copy) so all instances are controlled.

- CharacterSheet: AttributeRaiseCosts long[] (Strength…Mana raise costs in retail
  display order; cost=0 → max/disabled row demos the Ghosted button state).
- SampleData.SampleCharacter: fills AttributeRaiseCosts[9] — Strength/Quickness=0
  (maxed), Focus@10→110 matching the retail screenshot (spec §4).

- 35 new tests (total 673 pass) covering: row click→footer B title/line1/line2,
  toggle deselect→footer A, switch row, highlight set/clear, raise button
  hidden/Normal/Ghosted/deselect, tab Open/Closed states, GetRaiseCost helper,
  GetRowName helper, SampleData fixture sanity.

Console.WriteLine("[CharacterStat] Row click: index=N → selected=N (Name)") fires
on every click for the user's live verification in the studio.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:48:16 +02:00

227 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AcDream.App.UI.Layout;
using AcDream.Core.Items;
namespace AcDream.App.Studio;
/// <summary>
/// Static sample data for the UI Studio fixture provider.
/// Provides a pre-built <see cref="ClientObjectTable"/> populated with a
/// representative player, their main-pack items, side bags, and equipped gear,
/// so the 2-D inventory / paperdoll panels render populated when previewed in
/// the studio without a live game session.
///
/// Icon ids used (all real 0x06xxxxxx RenderSurface ids confirmed in the dats):
/// Inventory empty-slot sprite : 0x06004D20
/// Generic "misc" item : 0x060011D4 (icon_misc_underlay / fallback)
/// Iron Sword (melee weapon) : 0x060011CBu (acclient default weapon underlay)
/// Leather Breastplate (armor) : 0x060011CFu (acclient default armor underlay)
/// Leather Gloves : 0x060011F3u (acclient default clothing underlay)
/// Steel Ring : 0x060011D5u (acclient default jewelry underlay)
/// Healing Kit : 0x060011D4u (generic misc fallback)
/// Spell components : 0x060011D4u (generic misc fallback)
/// Side-bag 1 (Container) : 0x06004D20u
/// Side-bag 2 (Container) : 0x06004D20u
/// Equipped helm (HeadWear) : 0x060011F3u
/// Equipped chest armor : 0x060011CFu
/// Equipped melee weapon : 0x060011CBu
///
/// These are the icon *base* RenderSurface ids — the same ids GameWindow passes
/// as `iconId` into the iconIds lambda. FixtureProvider resolves them via
/// <see cref="Rendering.RenderStack.ResolveChrome"/> and returns the raw GL handle.
/// </summary>
public static class SampleData
{
// ── Guids ────────────────────────────────────────────────────────────────
/// <summary>Fake server guid for the studio's synthetic player.</summary>
public const uint PlayerGuid = 0x50000001u;
// Items in main pack (slots 05).
private const uint SwordGuid = 0x50000010u;
private const uint ChestGuid = 0x50000011u;
private const uint GlovesGuid = 0x50000012u;
private const uint RingGuid = 0x50000013u;
private const uint HealKitGuid = 0x50000014u;
private const uint CompGuid = 0x50000015u;
// Side bags (also in main pack, ContainerId = PlayerGuid; slots 6 & 7).
private const uint Bag1Guid = 0x50000020u;
private const uint Bag2Guid = 0x50000021u;
// Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set).
private const uint HelmGuid = 0x50000030u;
private const uint ChestEqGuid = 0x50000031u;
private const uint WeaponEqGuid = 0x50000032u;
// ── Icon ids (0x06xxxxxx RenderSurface dat ids) ───────────────────────────
// These are the same underlay/fallback icon ids the IconComposer tests pin.
private const uint IconWeapon = 0x060011CBu; // weapon underlay
private const uint IconArmor = 0x060011CFu; // armor underlay
private const uint IconClothing = 0x060011F3u; // clothing underlay
private const uint IconJewelry = 0x060011D5u; // jewelry underlay
private const uint IconMisc = 0x060011D4u; // misc / fallback underlay
// ── Public API ──────────────────────────────────────────────────────────
/// <summary>
/// Build a fresh <see cref="ClientObjectTable"/> populated with the
/// studio's sample player + a realistic inventory snapshot.
/// The table is owned by the caller and should be kept alive for the
/// window's lifetime.
/// </summary>
public static ClientObjectTable BuildObjectTable()
{
var t = new ClientObjectTable();
// ── Player object ─────────────────────────────────────────────────
t.AddOrUpdate(new ClientObject
{
ObjectId = PlayerGuid,
Name = "Studio Player",
Type = ItemType.Creature,
ItemsCapacity = 102,
ContainersCapacity = 7,
});
// ── Loose items in main pack (slots 05) ──────────────────────────
AddItem(t, SwordGuid, ItemType.MeleeWeapon, IconWeapon, "Iron Sword", PlayerGuid, 0, burden: 60);
AddItem(t, ChestGuid, ItemType.Armor, IconArmor, "Leather Breastplate", PlayerGuid, 1, burden: 200);
AddItem(t, GlovesGuid, ItemType.Clothing, IconClothing, "Leather Gloves", PlayerGuid, 2, burden: 50);
AddItem(t, RingGuid, ItemType.Jewelry, IconJewelry, "Steel Ring", PlayerGuid, 3, burden: 10);
AddItem(t, HealKitGuid, ItemType.Misc, IconMisc, "Healing Kit", PlayerGuid, 4, burden: 30);
AddItem(t, CompGuid, ItemType.SpellComponents, IconMisc, "Spell Comps", PlayerGuid, 5, burden: 25, stackSize: 50, stackMax: 100);
// ── Side bags (Container items in main pack, slots 6 & 7) ─────────
AddItem(t, Bag1Guid, ItemType.Container, IconMisc, "Small Pack 1",
containerId: PlayerGuid, slot: 6, burden: 20, itemsCapacity: 24);
AddItem(t, Bag2Guid, ItemType.Container, IconMisc, "Small Pack 2",
containerId: PlayerGuid, slot: 7, burden: 20, itemsCapacity: 24);
// ── Equipped items (ContainerId = PlayerGuid, CurrentlyEquippedLocation set) ──
AddEquipped(t, HelmGuid, ItemType.Armor, IconClothing, "Tin Helm", EquipMask.HeadWear);
AddEquipped(t, ChestEqGuid, ItemType.Armor, IconArmor, "Chain Coat", EquipMask.ChestArmor);
AddEquipped(t, WeaponEqGuid, ItemType.MeleeWeapon, IconWeapon, "Wooden Sword", EquipMask.MeleeWeapon);
return t;
}
// ── Sample vital constants (used by FixtureProvider) ────────────────────
public const float HealthPct = 0.8f;
public const float StaminaPct = 0.6f;
public const float ManaPct = 0.9f;
// ── Sample character sheet (used by CharacterController in the Studio) ───
/// <summary>
/// Returns a representative <see cref="CharacterSheet"/> for the studio's
/// synthetic character. Values are plausible retail-scale numbers so the
/// report renders with well-proportioned text in all sections.
/// </summary>
public static CharacterSheet SampleCharacter() => new()
{
Name = "Studio Player",
Level = 126,
Race = "Aluvian",
Heritage = "Aluvian Heritage",
Title = "the Adventurer",
BirthDate = "January 5, 2001",
PlayTime = "2 years, 114 days, 4 hours",
Deaths = 42,
PkStatus = "Non-Player Killer",
TotalXp = 1_250_000_000,
XpToNextLevel = 42_000_000,
XpFraction = 0.63f,
// Vitals: retail screenshot spec (Pass 1 acceptance criteria §Goal).
HealthCurrent = 5, HealthMax = 5,
StaminaCurrent = 10, StaminaMax = 10,
ManaCurrent = 10, ManaMax = 10,
// Attributes: Strength + Quickness = 200; all others = 10 (retail screenshot spec §Goal).
Strength = 200,
Endurance = 10,
Quickness = 200,
Coordination = 10,
Focus = 10,
Self = 10,
UnspentSkillCredits = 12,
SpecializedSkillCredits = 4,
// Available skill credits (retail InqInt(0x18)); shown in Attributes tab footer State-A.
SkillCredits = 96,
// Unassigned (banked) XP (retail InqInt64(2)); footer State-A line-2 value.
UnassignedXp = 87_757_321_741L,
// Raise costs in retail display order (Strength, Endurance, Coordination, Quickness,
// Focus, Self, Health, Stamina, Mana).
// Str@200 = maxed → 0 (disabled). Quickness@200 = maxed → 0. Others @10 → affordable.
// Focus@10 → 110 matches the authoritative retail screenshot (spec §4).
// Formula bracket at value=10: ExperienceToAttributeLevel(11) ExperienceToAttributeLevel(10).
AttributeRaiseCosts = new long[] { 0L, 95L, 100L, 0L, 110L, 105L, 90L, 88L, 112L },
AugmentationName = "Swords",
BurdenCurrent = 1200,
BurdenMax = 4500,
};
// ── Helpers ─────────────────────────────────────────────────────────────
private static void AddItem(
ClientObjectTable t,
uint guid,
ItemType type,
uint iconId,
string name,
uint containerId,
int slot,
int burden = 0,
int stackSize = 1,
int stackMax = 1,
int itemsCapacity = 0)
{
t.AddOrUpdate(new ClientObject
{
ObjectId = guid,
Name = name,
Type = type,
IconId = iconId,
Burden = burden,
StackSize = stackSize,
StackSizeMax = stackMax,
ItemsCapacity = itemsCapacity,
});
t.MoveItem(guid, containerId, slot);
}
private static void AddEquipped(
ClientObjectTable t,
uint guid,
ItemType type,
uint iconId,
string name,
EquipMask equipMask)
{
t.AddOrUpdate(new ClientObject
{
ObjectId = guid,
Name = name,
Type = type,
IconId = iconId,
ValidLocations = equipMask,
CurrentlyEquippedLocation = equipMask,
ContainerId = PlayerGuid,
});
// Update the equip location on the object via MoveItem (sets ContainerId +
// CurrentlyEquippedLocation via the equip overload).
t.MoveItem(guid, PlayerGuid, newSlot: -1, newEquipLocation: equipMask);
}
}