the dressing room opens, and the pet gets its paper doll

- ctrl-click an item to try it on: the dressing room dresses your double, and shift-click posts the item link to chat
- the pet tab lands in the character window: paper doll stats, family and diet, the right-click menu, and dropping food on the pet feeds it
- water-only spells refuse on dry land: aquatic form gates to swimming, client-side like the real one
- the macro icon picker's white squares get their art back, and option rows explain themselves on hover

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-07 16:02:21 +07:00
co-authored by Claude Fable 5
parent 31c224548c
commit d99ad8bc7d
109 changed files with 11304 additions and 2532 deletions
+63 -3
View File
@@ -155,6 +155,13 @@ pub(crate) struct Model {
/// (`ToggleGameMenu`'s order: the clear runs only when nothing earlier ate the press).
/// Drained by [`super::UiScript::take_target_clear`]; the app commits the deselect ([`unit`]).
pub(crate) target_clear: bool,
/// Unit tokens `DropItemOnUnit` queued since the app's last
/// [`UiScript::take_drop_item_on_unit`] drain — the cursor's held item being dropped onto a
/// unit (`0x48d960`). The binding queues the token only; **every gate is the app's**, because
/// all of them read state the VM does not hold (the pet's owner fields, the learned Feed-Pet
/// spell, the held item's guid). A token the app refuses is silent and keeps the payload,
/// which is the reference's own behaviour ([`super::cursor`]).
pub(crate) drop_item_on_unit: Vec<String>,
/// The party/raid roster snapshot the app pushes (`GroupState`'s merged view, decision 0434
/// §2) — `GetNumPartyMembers`/`GetPartyLeaderIndex`/`GetLootMethod`/… read it ([`party`]).
@@ -285,6 +292,14 @@ pub(crate) struct Model {
/// `SpellStopTargeting()`, whose 1/nil return the ESC chain's rung (`UIParent.lua:1490`)
/// falls through on, exactly like [`Self::casting`]'s.
pub(crate) spell_targeting: bool,
/// Whether the standing targeting word could bind a UNIT — what `SpellCanTargetUnit` answers
/// (`0x6e6d00` → `0x6e6460`'s unit leg). Pushed by the app beside `spell_targeting`.
///
/// Always `false` today, and *derived* rather than hardcoded so it stops being false the moment
/// that stops being true: benilla's targeting cursor models the location / item / gameobject
/// words (0792/0923/0939), and no unit satisfies any of them — a unit-target spell never enters
/// targeting mode at all, it resolves to `CastWireTarget::Unit` or refuses.
pub(crate) spell_can_target_unit: bool,
/// Set when `SpellStopTargeting()` fired while [`Self::spell_targeting`] — the ESC-chain
/// targeting cancel, drained by [`super::UiScript::take_stop_targeting`] ([`spellbook`]).
pub(crate) spell_stop_targeting: bool,
@@ -323,6 +338,15 @@ pub(crate) struct Model {
/// two-pair form **by body size**, so a relocation and its write must travel together and must
/// not be flattened into a stream of singles.
pub(crate) pet_set_actions: Vec<Vec<(u32, u32)>>,
/// `PetAbandon()` and `PetDismiss()` calls queued — two counts, not one, even though both menu
/// rows end at the same opcode (decision 1066). They are two *bindings*, with two Lua names and
/// two menu rows the reference shows to different classes, so the seam keeps them apart and
/// lets the app decide each one's wire; folding them together here would bake a wire fact into
/// an engine that is supposed to hold none.
pub(crate) pet_abandons: u32,
pub(crate) pet_dismisses: u32,
/// Names `PetRename(name)` queued, in order — the `PETRENAMECONFIRM` popup's payload.
pub(crate) pet_renames: Vec<String>,
/// The per-bag container snapshot (keyed by live-API bag id, 0 = backpack) the app pushes,
/// and the `UseContainerItem` intents it drains — the container seam ([`container`]).
@@ -392,6 +416,17 @@ pub(crate) struct Model {
/// [`merchant`]) — the single "displayed mode" the real client keeps at `0xbe2c2c`, restored to
/// the base (world-classifier) mode by `ResetCursor`. `None` = no override (show the base mode).
pub(crate) ui_cursor: Option<container::UiCursorMode>,
/// Set by every FrameXML cursor call — `Show*SellCursor` / `ShowInspectCursor` / `SetCursor` /
/// `ResetCursor` — and drained by the app each frame ([`UiScript::take_cursor_write`]).
///
/// **The cursor mode is a WRITE, not a level** (decision 1061), and that distinction is the
/// whole of B208's regression. The reference keeps one sticky global (`0xbe2c2c`): the world
/// classifier writes it while the pointer is over the world, FrameXML writes it from a hover
/// handler, and in between **nothing** writes it — so the last value simply stands. Reading
/// `ui_cursor` as a level made "no override" mean "show the base", which turned every UI
/// element with no cursor handler at all (a spellbook button, a panel) into a forced Point and
/// killed the armed cast cursor the moment the mouse left the world.
pub(crate) ui_cursor_dirty: bool,
/// `(bag, slot)` sources queued by `AutoEquipCursorItem` (decision 0208 phase 1b, `cursor`'s
/// `doll` submodule) — drained by the app into `CMSG_AUTOEQUIP_ITEM`.
pub(crate) container_autoequips: Vec<(i64, u32)>,
@@ -666,9 +701,15 @@ pub(crate) struct Model {
/// The paper doll's combat-stats snapshot (`None` until the app's feed lands), the
/// equipment/ammo slot views, and the model pane's persistent bake yaw — the character-window
/// seam ([`char_stats`], decision 0208 §3). All player-only data: the backing descriptor
/// fields are PRIVATE/OWNER_ONLY.
pub(crate) player_combat_stats: Option<char_stats::PlayerCombatStats>,
/// seam ([`char_stats`], decision 0208 §3).
pub(crate) player_combat_stats: Option<char_stats::UnitCombatStats>,
/// The **pet's** combat-stats snapshot (decision 1057) — the same shape under the `"pet"`
/// token, because the reference's own pet sheet calls the very same `UnitStat`/`UnitResistance`/
/// `PaperDollFrame_Set*(unit, prefix)` family with `unit = "pet"` (ref
/// `PetPaperDollFrame.lua:73-81`). A second slot rather than a token map: exactly two units
/// ever have one, they are fed by two different systems on two different clocks, and every
/// binding's route is a fork between them (the [`Self::pet_book`] precedent).
pub(crate) pet_combat_stats: Option<char_stats::UnitCombatStats>,
pub(crate) inventory_slots: char_stats::InventorySlots,
/// The 12 alert-region statuses (`GetInventoryAlertStatus`, DurabilityFrame's armor-guy
/// feed) in the client's own `0x806eb8` table order (11 equipment regions + the low-ammo
@@ -691,6 +732,15 @@ pub(crate) struct Model {
pub(crate) inspect_clear: bool,
/// The inspect model pane's bake yaw, the twin of [`Self::paperdoll_yaw`].
pub(crate) inspect_yaw: f32,
/// The **pet** paper doll's model-pane bake yaw (decision 1057) — a third scalar for the same
/// reason the inspect pane got a second: character tab 1 and tab 2 are two panes that can sit
/// at two different facings, and the ref carries a `rotation` per `<PlayerModel>`.
pub(crate) pet_paperdoll_yaw: f32,
/// The dressing room's queued intents (decision 1060) — `BenillaDressUpModel_Dress/TryOn/Close`,
/// drained by the app in order (see [`super::dressup`] on why order matters).
pub(crate) dressup_intents: Vec<super::dressup::DressUpIntent>,
/// The dressing-room pane's bake yaw, the fourth of those scalars.
pub(crate) dressup_yaw: f32,
/// Unit token → squared distance from the player, for every popup token the app resolved to a
/// live inspectable player this frame — the input to both verified range predicates
/// (`CanInspect`, `CheckInteractDistance`). Absent token = in range (see
@@ -779,6 +829,7 @@ impl Model {
cancel_aura_requests: Vec::new(),
tracking: None,
target_requests: Vec::new(),
drop_item_on_unit: Vec::new(),
target_clear: false,
party: party::PartyState::default(),
party_requests: Vec::new(),
@@ -812,6 +863,7 @@ impl Model {
casting: false,
spell_stop: false,
spell_targeting: false,
spell_can_target_unit: false,
spell_stop_targeting: false,
talents: super::talent::TalentUiState::default(),
talent_learns: Vec::new(),
@@ -822,6 +874,9 @@ impl Model {
pet_autocast_toggles: Vec::new(),
pet_stop_attacks: 0,
pet_set_actions: Vec::new(),
pet_abandons: 0,
pet_dismisses: 0,
pet_renames: Vec::new(),
containers: HashMap::new(),
container_uses: Vec::new(),
container_cooldowns: HashMap::new(),
@@ -837,6 +892,7 @@ impl Model {
enchant_confirms: Vec::new(),
container_destroys: Vec::new(),
ui_cursor: None,
ui_cursor_dirty: false,
container_autoequips: Vec::new(),
drag_registered: HashMap::new(),
drag: None,
@@ -938,6 +994,7 @@ impl Model {
combo_points: 0,
combo_target: 0,
player_combat_stats: None,
pet_combat_stats: None,
inventory_slots: Default::default(),
inventory_alerts: [0; 12],
paperdoll_yaw: 0.0,
@@ -946,6 +1003,9 @@ impl Model {
inspect_notifies: Vec::new(),
inspect_clear: false,
inspect_yaw: 0.0,
pet_paperdoll_yaw: 0.0,
dressup_intents: Vec::new(),
dressup_yaw: 0.0,
inspect_reach: HashMap::new(),
chat_input: Vec::new(),
skills: skills::SkillsState::default(),