- 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>
227 lines
10 KiB
C#
227 lines
10 KiB
C#
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 0–5).
|
||
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 0–5) ──────────────────────────
|
||
|
||
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);
|
||
}
|
||
}
|