From cf581aa2acc4bc4658e574da7bd473d20a45aeff Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 22 Aug 2026 19:04:27 +0700 Subject: [PATCH] the auction house opens, the Honor tab lands, and a day of fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the auction house end to end: right-clicking an auctioneer opens Browse/Bids/Auctions on the server's reply — search, the category tree from your own DBCs, the reference's 50-a-page pager, multi-key sorting (directions corrected at the bytes), bid/buyout, the sell slot with deposit and duration, cancel with its fee popup; prices sit on the reference's own money-frame kit in lined-up columns, the twenty auction messages split refusals-red / outcomes-to-chat, and a won or sold auction reads as a proper mail receipt — the window's Lua re-worded in our own terms after the overlap ratchet caught the first cut leaning on the reference's text - the PvP honor system whole: the Honor tab and the inspect page over a real wire model, with current rank and highest-ever rank correctly two different fields - a day of reported fixes: WASD survives a /logout re-entry, addon settings save on a window close and Cmd+Q (exactly once — a mac close could fire the tail twice), clams open again, random-enchant items name themselves on every surface, the loot window's rows hold their seats with legible counts, idle players stop sinking through building floors, the shooter re-pulls between Auto Shots, portraits loosen ~19% to the client's own aspect-1.0 matrix, and big creatures thump the camera off the CameraShakes table - character select clicks like 1.12 — captions whiten on hover, buttons fire on release, Humans face straight ahead off the stage bone's real yaw, the facing is per-character, and hunter/warlock pets stand beside you at their family-ramp size with their own emitters (the imp's flames), the Voidwalker's sequence-hidden shackles culled - the smaller player-facing set: nameplate on/off rows in Options with the CanAttack category gate (emissaries stop plating), the bonus bar slides up with igBonusBarOpen, the camera's framing height glides on a shapeshift, the open letter evicts the centre seat, the far-band fog hull stops printing a pale horizon stripe at low view distance, the guide arrow is gold with per-source rim art, error text fades through its shadow too, and /convertraid converts the party - README: the auction house and PvP honor move out of "not built yet" Co-Authored-By: Claude Fable 5 --- Cargo.lock | 1 + README.md | 8 +- crates/benilla-app/assets/ui/ActionBar.xml | 398 ++- crates/benilla-app/assets/ui/AuctionFrame.xml | 2492 +++++++++++++++++ crates/benilla-app/assets/ui/CraftFrame.xml | 10 +- .../benilla-app/assets/ui/GroupLootFrame.xml | 22 +- crates/benilla-app/assets/ui/LootFrame.xml | 63 +- crates/benilla-app/assets/ui/MailFrame.xml | 230 +- crates/benilla-app/assets/ui/MoneyFrame.xml | 35 +- crates/benilla-app/assets/ui/OptionsFrame.xml | 48 +- .../benilla-app/assets/ui/ScriptLogFrame.xml | 8 + crates/benilla-app/assets/ui/StanceBar.xml | 25 +- .../benilla-app/assets/ui/TradeSkillFrame.xml | 9 +- crates/benilla-app/assets/ui/TrainerFrame.xml | 12 +- crates/benilla-app/assets/ui/UiPanels.xml | 101 +- crates/benilla-app/assets/ui/benilla.toc | 22 +- crates/benilla-app/src/camera_shake.rs | 573 ++++ crates/benilla-app/src/capture/fixtures.rs | 8 + crates/benilla-app/src/capture/mod.rs | 4 + crates/benilla-app/src/capture/pick_probe.rs | 35 +- .../benilla-app/src/capture/probe_auction.rs | 1309 +++++++++ crates/benilla-app/src/capture/probe_clam.rs | 433 +++ .../src/capture/probe_guard_poi.rs | 2 +- crates/benilla-app/src/capture/probe_rig.rs | 3 + crates/benilla-app/src/char_create/mod.rs | 31 +- crates/benilla-app/src/char_create/refresh.rs | 9 +- crates/benilla-app/src/char_select/addons.rs | 19 +- crates/benilla-app/src/char_select/dialog.rs | 8 +- crates/benilla-app/src/char_select/input.rs | 33 +- crates/benilla-app/src/char_select/mod.rs | 75 +- crates/benilla-app/src/char_select/refresh.rs | 20 +- crates/benilla-app/src/creature_anim.rs | 23 +- .../src/creature_anim/driver/mode.rs | 70 +- .../src/creature_anim/driver/tests.rs | 315 ++- .../benilla-app/src/creature_anim/select.rs | 71 +- .../src/creature_anim/select/tests.rs | 32 +- .../src/creature_anim/spell_visual.rs | 71 +- .../src/creature_anim/spell_visual/tests.rs | 295 ++ crates/benilla-app/src/cvars.rs | 289 +- crates/benilla-app/src/debug_panel/inspect.rs | 120 +- crates/benilla-app/src/dev.rs | 14 + crates/benilla-app/src/entities.rs | 44 +- crates/benilla-app/src/entities/attach/mod.rs | 2 +- .../src/entities/attach/preview.rs | 224 +- .../src/entities/equipment/spawn.rs | 8 +- crates/benilla-app/src/entities/wmo_props.rs | 15 +- crates/benilla-app/src/glue/mod.rs | 249 +- crates/benilla-app/src/items.rs | 156 +- crates/benilla-app/src/lib.rs | 17 + crates/benilla-app/src/login/mod.rs | 54 +- crates/benilla-app/src/minimap/blips/mod.rs | 334 ++- crates/benilla-app/src/minimap/mod.rs | 43 +- crates/benilla-app/src/net.rs | 68 + crates/benilla-app/src/net/apply.rs | 41 + crates/benilla-app/src/net/apply/auction.rs | 351 +++ crates/benilla-app/src/net/apply/objects.rs | 1 - crates/benilla-app/src/net/apply/quests.rs | 4 +- crates/benilla-app/src/net/apply/session.rs | 5 + crates/benilla-app/src/net/io.rs | 52 + crates/benilla-app/src/net/motion/remote.rs | 274 +- crates/benilla-app/src/net/motion/tests.rs | 59 +- crates/benilla-app/src/player.rs | 158 +- crates/benilla-app/src/player/camera.rs | 256 +- crates/benilla-app/src/player/camera_saved.rs | 7 +- crates/benilla-app/src/player/embody.rs | 1 - crates/benilla-app/src/player/state.rs | 45 +- crates/benilla-app/src/player/view_subject.rs | 12 +- crates/benilla-app/src/player/wire_in.rs | 166 +- crates/benilla-app/src/portrait/booth.rs | 203 +- crates/benilla-app/src/portrait/framing.rs | 199 +- crates/benilla-app/src/portrait/glue_booth.rs | 558 +++- crates/benilla-app/src/portrait/mod.rs | 20 +- crates/benilla-app/src/quest_markers/mod.rs | 54 +- crates/benilla-app/src/shutdown.rs | 257 ++ crates/benilla-app/src/target/click.rs | 33 +- crates/benilla-app/src/target/ring.rs | 325 ++- crates/benilla-app/src/ui_action/errors.rs | 19 + crates/benilla-app/src/ui_action/mod.rs | 2 +- crates/benilla-app/src/ui_auction/mod.rs | 992 +++++++ crates/benilla-app/src/ui_auction/sort.rs | 258 ++ crates/benilla-app/src/ui_char.rs | 47 +- crates/benilla-app/src/ui_chat/commands.rs | 16 +- crates/benilla-app/src/ui_chat/input/mod.rs | 6 + crates/benilla-app/src/ui_chat/input/parse.rs | 5 + crates/benilla-app/src/ui_chat/tests.rs | 8 +- crates/benilla-app/src/ui_inspect.rs | 33 +- crates/benilla-app/src/ui_items/drain.rs | 126 +- crates/benilla-app/src/ui_items/feed.rs | 87 +- crates/benilla-app/src/ui_items/mod.rs | 14 +- crates/benilla-app/src/ui_loot.rs | 277 +- crates/benilla-app/src/ui_loot_roll.rs | 61 +- crates/benilla-app/src/ui_mail/invoice.rs | 193 ++ crates/benilla-app/src/ui_mail/mod.rs | 143 +- crates/benilla-app/src/ui_quest.rs | 16 +- .../src/ui_script/action_bar_tests.rs | 304 ++ .../src/ui_script/bagnon_render_tests.rs | 3 + .../benilla-app/src/ui_script/escape_tests.rs | 5 +- .../benilla-app/src/ui_script/extract/mod.rs | 37 +- .../benilla-app/src/ui_script/extract/text.rs | 51 +- .../src/ui_script/game_menu_tests.rs | 5 +- .../src/ui_script/group_loot_tests.rs | 3 + .../benilla-app/src/ui_script/loot_tests.rs | 125 +- crates/benilla-app/src/ui_script/mod.rs | 10 +- .../src/ui_script/multibar_stance_tests.rs | 99 + .../src/ui_script/options_tests.rs | 47 +- .../src/ui_script/world_entry_tests.rs | 3 + crates/benilla-app/src/ui_session.rs | 40 +- crates/benilla-app/src/vplates.rs | 122 +- crates/benilla-app/tests/auction_frame.rs | 763 +++++ crates/benilla-app/tests/mail_frame.rs | 289 +- crates/benilla-app/tests/world_api_wall.rs | 12 +- crates/benilla-assets/src/lib.rs | 4 +- crates/benilla-assets/src/m2.rs | 45 +- crates/benilla-assets/src/materials.rs | 16 +- crates/benilla-assets/src/shaders/wdl.wgsl | 40 +- .../benilla-formats/examples/fade_bucket.rs | 49 + crates/benilla-formats/src/auction_house.rs | 159 ++ .../src/bin/benilla-extract/main.rs | 8 + .../src/bin/benilla-extract/shakecensus.rs | 115 + crates/benilla-formats/src/camera_shakes.rs | 139 + .../benilla-formats/src/creature_families.rs | 134 +- crates/benilla-formats/src/creatures.rs | 110 + .../src/item_random_properties.rs | 188 ++ crates/benilla-formats/src/itemsubclass.rs | 24 + crates/benilla-formats/src/lib.rs | 41 +- crates/benilla-formats/src/models/anim.rs | 110 + .../benilla-formats/src/models/m2_batches.rs | 75 + crates/benilla-protocol/src/events.rs | 49 +- crates/benilla-protocol/src/events/decode.rs | 54 + .../benilla-protocol/src/messages/auction.rs | 506 ++++ crates/benilla-protocol/src/messages/mod.rs | 7 + .../benilla-protocol/src/messages/opcode.rs | 37 + .../benilla-protocol/src/messages/packet.rs | 86 +- crates/benilla-protocol/src/messages/parse.rs | 67 +- .../benilla-protocol/src/messages/roster.rs | 26 +- .../messages/update_object/fields/player.rs | 29 + .../src/world/writer/auction.rs | 145 + .../benilla-protocol/src/world/writer/mod.rs | 2 + crates/benilla-protocol/tests/auction.rs | 744 +++++ crates/benilla-protocol/tests/char_enum.rs | 17 +- crates/benilla-ui/src/loader/widgets.rs | 53 +- crates/benilla-ui/src/script/auction.rs | 1053 +++++++ crates/benilla-ui/src/script/container.rs | 17 + crates/benilla-ui/src/script/cursor.rs | 23 + crates/benilla-ui/src/script/item_stats.rs | 27 + crates/benilla-ui/src/script/loot.rs | 70 +- crates/benilla-ui/src/script/loot_roll.rs | 9 + crates/benilla-ui/src/script/mail.rs | 245 +- crates/benilla-ui/src/script/merchant.rs | 20 +- crates/benilla-ui/src/script/mod.rs | 12 +- crates/benilla-ui/src/script/model.rs | 55 +- .../script/tests/tooltip_item/mechanics.rs | 198 ++ .../benilla-ui/src/script/tooltip_item/mod.rs | 307 +- .../src/script/tooltip_item/render.rs | 25 +- crates/benilla-ui/src/script/trade.rs | 21 +- crates/benilla-world/Cargo.toml | 6 + crates/benilla-world/src/interact/mod.rs | 82 +- crates/benilla-world/src/interact/pick.rs | 341 ++- crates/benilla-world/src/lib.rs | 4 + crates/benilla-world/src/mac_quit.rs | 92 + crates/benilla-world/src/model_fade.rs | 21 +- crates/benilla-world/src/static_gx/bake.rs | 26 +- crates/benilla-world/src/static_gx/cull.rs | 10 +- crates/benilla-world/src/static_gx/mod.rs | 113 +- crates/benilla-world/src/static_gx/pick.rs | 198 ++ .../benilla-world/src/terrain_stream/merge.rs | 59 +- .../src/terrain_stream/spawn/assemble.rs | 24 +- .../src/terrain_stream/spawn/mod.rs | 111 +- crates/benilla-world/src/wdl.rs | 33 +- crates/benilla-world/src/worldview.rs | 8 +- 170 files changed, 20573 insertions(+), 1578 deletions(-) create mode 100644 crates/benilla-app/assets/ui/AuctionFrame.xml create mode 100644 crates/benilla-app/src/camera_shake.rs create mode 100644 crates/benilla-app/src/capture/probe_auction.rs create mode 100644 crates/benilla-app/src/capture/probe_clam.rs create mode 100644 crates/benilla-app/src/net/apply/auction.rs create mode 100644 crates/benilla-app/src/shutdown.rs create mode 100644 crates/benilla-app/src/ui_auction/mod.rs create mode 100644 crates/benilla-app/src/ui_auction/sort.rs create mode 100644 crates/benilla-app/src/ui_mail/invoice.rs create mode 100644 crates/benilla-app/tests/auction_frame.rs create mode 100644 crates/benilla-formats/examples/fade_bucket.rs create mode 100644 crates/benilla-formats/src/auction_house.rs create mode 100644 crates/benilla-formats/src/bin/benilla-extract/shakecensus.rs create mode 100644 crates/benilla-formats/src/camera_shakes.rs create mode 100644 crates/benilla-formats/src/item_random_properties.rs create mode 100644 crates/benilla-protocol/src/messages/auction.rs create mode 100644 crates/benilla-protocol/src/world/writer/auction.rs create mode 100644 crates/benilla-protocol/tests/auction.rs create mode 100644 crates/benilla-ui/src/script/auction.rs create mode 100644 crates/benilla-world/src/mac_quit.rs create mode 100644 crates/benilla-world/src/static_gx/pick.rs diff --git a/Cargo.lock b/Cargo.lock index fa7dffae..08039a43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -749,6 +749,7 @@ dependencies = [ "bytemuck", "image", "libc", + "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] diff --git a/README.md b/README.md index 25afb2f8..468837f2 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ and no bundled game assets. - **Movement:** a WoW-feel controller, networked movement in both directions, the server-granted modes from slow fall to roots, a follow camera with collision, boats, zeppelins and taxi flights. - **Networking:** SRP6 auth through world-session crypto, the object mirror into the ECS, and live - wire coverage from movement and chat through spells, party, quests, mail, trade, vendors, bank - and loot. + wire coverage from movement and chat through spells, party, quests, mail, trade, vendors, bank, + loot, the auction house and PvP honor. - **UI:** a from-scratch FrameXML + Lua engine driving the built-in interface, from the login and character screens through the full HUD, the classic windows (guild, macros and key bindings included), chat, nameplates, floating combat text and tooltips; third-party addons load from @@ -46,8 +46,8 @@ and no bundled game assets. ## Not built yet -Battlegrounds and honor, and the auction house. Third-party addons are partial: AtlasLoot and -Bagnon run, and the addon API fills in as other addons are tried. +Battlegrounds. Third-party addons are partial: AtlasLoot and Bagnon run, and the addon API +fills in as other addons are tried. ## Running it diff --git a/crates/benilla-app/assets/ui/ActionBar.xml b/crates/benilla-app/assets/ui/ActionBar.xml index e17afe1e..995e30aa 100644 --- a/crates/benilla-app/assets/ui/ActionBar.xml +++ b/crates/benilla-app/assets/ui/ActionBar.xml @@ -20,8 +20,10 @@ SCOPE / real-client divergences (noted, not bugs): - No frame scale: the real bar is UIParent-scaled; we render 1:1. Geometry is otherwise exact. - - Page up/down + the page number are ART ONLY (we have no bar paging — only the bonus-bar - offset; ref-ActionBarFrame.xml l.159-217). The page number renders a static "1". + - Page up/down page the bar for real (`CURRENT_ACTIONBAR_PAGE`, below) and the numeral + follows through `ChangeActionBarPage`, not through the ref's ACTIONBAR_PAGE_CHANGED — + which we never fire, so an addon LISTENING for the page change hears nothing (it is a + Rust-side event with no Lua producer). Addons that drive paging call the verb. - HotKey renders the LIVE binding label, abbreviated the ref's own way since 1008 (GetBindingText(key, "KEY_", 1) — UIParent.xml carries our own version; "s-2" for SHIFT-2, "·" for a 2+-modifier chord). A slot with NO binding wears the ref's @@ -87,10 +89,10 @@ -- than here among the true constants. A frozen 1 would still be a lie; this one moves. NUM_ACTIONBAR_PAGES = 6 NUM_ACTIONBAR_BUTTONS = 12 - BOTTOMLEFT_ACTIONBAR_PAGE = 6 - BOTTOMRIGHT_ACTIONBAR_PAGE = 5 - LEFT_ACTIONBAR_PAGE = 4 RIGHT_ACTIONBAR_PAGE = 3 + LEFT_ACTIONBAR_PAGE = 4 + BOTTOMRIGHT_ACTIONBAR_PAGE = 5 + BOTTOMLEFT_ACTIONBAR_PAGE = 6 -- Ref ActionButton.lua:4 / UIParent.lua: the flash blink period and the range-check cadence. ATTACK_BUTTON_FLASH_TIME = 0.4 @@ -172,10 +174,13 @@ -- -- Upstream it is a client binding; here it is FrameXML, deliberately. Our paged action id -- is computed in Lua by `BenillaActionBar_ActionFor`, so the page IS this global and the - -- only work left for the verb is the repaint the reference gets by firing - -- ACTIONBAR_PAGE_CHANGED. Addons CALL this, they never replace it, so the divergence is - -- unobservable from Lua — unlike a name we shadow, which is 1222's class. + -- only work left for the verb is the work the reference gets by firing + -- ACTIONBAR_PAGE_CHANGED: repaint the twelve buttons, and restamp the page numeral (the + -- ref's own listener for that event is the up arrow's OnEvent, l.176-181 — the numeral is + -- the ONLY thing it does there). Addons CALL this, they never replace it, so the + -- divergence is unobservable from Lua — unlike a name we shadow, which is 1222's class. function ChangeActionBarPage() + MainMenuBarPageNumber:SetText(CURRENT_ACTIONBAR_PAGE) for i = 1, table.getn(BENILLA_ACTION_BUTTONS) do BenillaActionButton_Update(BENILLA_ACTION_BUTTONS[i]) end @@ -206,8 +211,7 @@ if VIEWABLE_ACTION_BAR_PAGES[i] then prevPage = i break end end end - CURRENT_ACTIONBAR_PAGE = prevPage - ChangeActionBarPage() + CURRENT_ACTIONBAR_PAGE = prevPage; ChangeActionBarPage() end -- Seat a TOP-LEVEL frame that anchors INTO the bar art (the bag bar + backpack toggle, @@ -291,12 +295,112 @@ if frame and not show and frame:IsShown() then frame:Hide() end end + -- ── The bonus-bar slide (decision 1524; ref BonusActionBarFrame.lua:1-98) ────────────── + -- + -- The reference never re-pages its main bar: entering a form slides a 505x43 REPLICA of + -- the bar (own art, own 12 buttons on the bonus page) up out of the screen-bottom edge + -- over 0.15s, plays igBonusBarOpen when it lands, and leaves it covering the button strip + -- for as long as the form is up; leaving the form slides it back down carrying the OLD + -- form's icons (lastBonusBar) and hides it — no sound on the way down. benilla re-pages + -- the main bar (1223) — so here the overlay is the TRANSITION VEHICLE and the faithful + -- addon-visible state (`BonusActionBarFrame:IsShown()` is true exactly while a form is + -- up, as on the real client): the main bar keeps painting the OLD page under the rising + -- overlay (BenillaActionBar_DisplayedOffset) and adopts the bonus page when it lands. + -- + -- Ref timing/geometry, quoted: SLIDETIME 0.15, YPOS 43 (= the frame's own height, so it + -- rises exactly out of the screen edge), XPOS 4 (the ref Lua re-anchors every tick at 4, + -- overriding its own XML's 40; the art's left TexCoord is trimmed 4px to line back up). + -- Ours slides a PROGRESS fraction rather than the ref's mode+timer pair: flipping form + -- mid-slide turns the bar around from where it is, where the ref's timer arithmetic + -- mirror-jumps the position (a quirk, not the mechanism). + BONUSACTIONBAR_SLIDETIME = 0.15 + BONUSACTIONBAR_YPOS = 43 + BONUSACTIONBAR_XPOS = 4 + + -- Ref-named verb (BonusActionBarFrame.lua:74): addons call it. Begin (or continue toward) + -- the up-slide; a bar already landed is a no-op — a direct form→form swap (warrior stance + -- dance, cat→bear powershift) repaints without re-sliding or re-sounding, ref-verbatim. + function ShowBonusActionBar() + local frame = BonusActionBarFrame + if not frame:IsShown() then + frame.slideProgress = 0 + frame:Show() + end + if (frame.slideProgress or 0) < 1 then + frame.mode = "show" + end + end + + -- Ref-named verb (l.87): begin the down-slide. Nothing to do when already hidden; a + -- mid-show flip turns around from the current position. + function HideBonusActionBar() + local frame = BonusActionBarFrame + if frame:IsShown() and frame.mode ~= "hide" then + frame.mode = "hide" + end + end + + function BenillaBonusBar_OnLoad(frame) + frame.mode = "none" + frame.slideProgress = 0 + -- Ref field (l.17/23, read by ActionButton_GetPagedID:449-451): the last NONZERO + -- offset, so the down-slide keeps painting the form's icons after the offset is 0. + frame.lastBonusBar = 1 + end + + function BonusActionBar_OnUpdate(elapsed) + local frame = BonusActionBarFrame + if frame.mode == "show" then + frame.slideProgress = frame.slideProgress + elapsed / BONUSACTIONBAR_SLIDETIME + if frame.slideProgress >= 1 then + frame.slideProgress = 1 + frame.mode = "none" + frame:SetPoint("TOPLEFT", MainMenuBar, "BOTTOMLEFT", + BONUSACTIONBAR_XPOS, BONUSACTIONBAR_YPOS) + -- The landing edge IS the sound (ref l.61-64: played on slide-up completion, + -- never on the way down), and the moment the main bar under the overlay + -- adopts the bonus page (DisplayedOffset stops suppressing). + PlaySound("igBonusBarOpen") + for i = 1, table.getn(BENILLA_ACTION_BUTTONS) do + BenillaActionButton_Update(BENILLA_ACTION_BUTTONS[i]) + end + else + frame:SetPoint("TOPLEFT", MainMenuBar, "BOTTOMLEFT", + BONUSACTIONBAR_XPOS, frame.slideProgress * BONUSACTIONBAR_YPOS) + end + elseif frame.mode == "hide" then + frame.slideProgress = frame.slideProgress - elapsed / BONUSACTIONBAR_SLIDETIME + if frame.slideProgress <= 0 then + frame.slideProgress = 0 + frame.mode = "none" + frame:SetPoint("TOPLEFT", MainMenuBar, "BOTTOMLEFT", BONUSACTIONBAR_XPOS, 0) + frame:Hide() + else + frame:SetPoint("TOPLEFT", MainMenuBar, "BOTTOMLEFT", + BONUSACTIONBAR_XPOS, frame.slideProgress * BONUSACTIONBAR_YPOS) + end + end + end + + -- The offset the MAIN bar paints. Live, except during the up-slide: the old page keeps + -- painting under the rising overlay and the bonus page arrives when it lands (the ref's + -- main bar shows page 1 the whole time a form is up — ours adopts the bonus page at + -- landing instead, 1223's re-paging model; 1524). The getglobal guard: buttons paint + -- during load, before BonusActionBarFrame (declared after the bar) exists. + function BenillaActionBar_DisplayedOffset() + local frame = getglobal("BonusActionBarFrame") + if frame and frame.mode == "show" then + return 0 + end + return GetBonusBarOffset() + end + -- The wire/API action id the MAIN bar's button i displays (the vanilla page formula). -- INTERNAL to BenillaActionButton_Action below — the ONLY caller. A button path that -- reaches past it computes a MAIN-bar id for a multibar button (base ignored), which -- reads as "the slot below": the tooltip hover did exactly that until 2026-07-21. function BenillaActionBar_ActionFor(index) - local offset = GetBonusBarOffset() + local offset = BenillaActionBar_DisplayedOffset() local base = 0 if offset > 0 then base = (6 + offset - 1) * 12 @@ -361,6 +465,19 @@ if button.base then return button.base + index end + -- The overlay's own buttons (ref ActionButton_GetPagedID:447-452): the LIVE bonus + -- page — with lastBonusBar standing in once the offset has dropped, so the + -- down-slide keeps painting (and a click mid-descent keeps using) the old form's + -- slots, ref-verbatim. Distinct from the main bar's DisplayedOffset suppression: + -- these buttons show the NEW page from the first slide frame. + if button.isBonus then + local offset = GetBonusBarOffset() + if offset == 0 then + local frame = getglobal("BonusActionBarFrame") + offset = (frame and frame.lastBonusBar) or 1 + end + return (6 + offset - 1) * 12 + index + end return BenillaActionBar_ActionFor(index) end @@ -388,11 +505,7 @@ local parent = button:GetParent() local edgeBar = parent and (parent == MultiBarBottomRight or parent == MultiBarRight or parent == MultiBarLeft) - if edgeBar then - GameTooltip:SetOwner(button, "ANCHOR_LEFT") - else - GameTooltip:SetOwner(button, "ANCHOR_RIGHT") - end + if edgeBar then GameTooltip:SetOwner(button, "ANCHOR_LEFT") else GameTooltip:SetOwner(button, "ANCHOR_RIGHT") end else GameTooltip_SetDefaultAnchor(GameTooltip, button) end @@ -486,15 +599,28 @@ -- they take the bar INDEX in both clients — so nothing here changes but the names and the -- restored `onSelf`, which rides through to UseAction's third argument exactly as the ref's -- l.56 does (our binding accepts and ignores it — `script/action.rs`). + -- Both route to the overlay's buttons while it is up — ref ActionButton.lua:15-61's own + -- fork, and the half of the swap that must NOT wait for the slide: the overlay is shown + -- from the first slide frame, so a key pressed the instant you enter stealth already + -- drives the stealth page (and, during the down-slide, still drives the old form's page — + -- lastBonusBar — exactly as the reference does). function ActionButtonDown(id) - local button = getglobal("ActionButton" .. id) - if ( button:GetButtonState() == "NORMAL" ) then - button:SetButtonState("PUSHED") + if BonusActionBarFrame:IsShown() then + local button = getglobal("BonusActionButton" .. id) + if ( button:GetButtonState() == "NORMAL" ) then button:SetButtonState("PUSHED") end + return end + local button = getglobal("ActionButton" .. id) + if ( button:GetButtonState() == "NORMAL" ) then button:SetButtonState("PUSHED") end end function ActionButtonUp(id, onSelf) - local button = getglobal("ActionButton" .. id) + local button + if BonusActionBarFrame:IsShown() then + button = getglobal("BonusActionButton" .. id) + else + button = getglobal("ActionButton" .. id) + end if ( button:GetButtonState() == "PUSHED" ) then button:SetButtonState("NORMAL") UseAction(BenillaActionButton_Action(button), 0, onSelf) @@ -507,11 +633,7 @@ -- while auto-attacking, the in-flight cast) or the live auto-repeat spell. function BenillaActionButton_UpdateState(button) local action = BenillaActionButton_Action(button) - if ( IsCurrentAction(action) or IsAutoRepeatAction(action) ) then - button:SetChecked(1) - else - button:SetChecked(0) - end + if ( IsCurrentAction(action) or IsAutoRepeatAction(action) ) then button:SetChecked(1) else button:SetChecked(0) end end -- Ref ActionButton_UpdateUsable:269-283, verbatim colors: usable white; out of power the @@ -798,11 +920,11 @@ -- divergence (the SCOPE note) — its art has sockets; floating rings over bare -- world would not read as a bar. button.hotkey:SetText("") - if grid then + if not grid then + button:Hide() + else button:SetNormalTexture("Interface\\Buttons\\UI-Quickslot") button:Show() - else - button:Hide() end else -- While the grid is up, an empty slot shows the ref's own "no action" ring @@ -864,6 +986,20 @@ end function BenillaActionBar_OnEvent(frame) + -- The form/stance/stealth edge (ref BonusActionBar_OnEvent:20-35): start the overlay + -- slide, then FALL THROUGH to the tail — the roster repaint under the new state is + -- the same statement for this event as for any other. Deliberately in the handler + -- that repaints, not a second OnEvent on the overlay frame: one handler means the + -- slide state is set before any button paints, so there is no one-frame flash of the + -- new page before the suppression lands. + if event == "UPDATE_BONUS_ACTIONBAR" then + if GetBonusBarOffset() <= 0 then + HideBonusActionBar() + else + BonusActionBarFrame.lastBonusBar = GetBonusBarOffset() + ShowBonusActionBar() + end + end -- ACTIONBAR_SLOT_CHANGED names its action in arg1; repaint just the button showing it. if event == "ACTIONBAR_SLOT_CHANGED" and arg1 then for i = 1, table.getn(BENILLA_ACTION_BUTTONS) do @@ -1170,28 +1306,24 @@ -- that the text is currently pinned on — which is what the bar's own OnLeave reads before -- hiding it again. Note the Show verb only ever shows the text AT MAX LEVEL, where the -- strip has replaced the XP bar and is tall enough to hold numerals; below that it falls - -- through to the Hide one. Both function names, both field names and the FontString are the - -- addon-visible contract, so these two bodies have almost no freedom left in them and read - -- close to the reference's — which is what a compatible implementation of a four-line lock - -- over fixed names looks like, not a transcription. + -- through to the Hide one. Both function names, both field names and the FontString are + -- the addon-visible contract; the shapes below (early exits where the ref nests) are ours. function ShowWatchedReputationBarText(lock) - if lock then - ReputationWatchBar.cvarLocked = lock - end - if UnitLevel("player") == MAX_PLAYER_LEVEL and ReputationWatchBar:IsVisible() then - ReputationWatchStatusBarText:Show() - ReputationWatchBar.textLocked = 1 - else + if lock then ReputationWatchBar.cvarLocked = lock end + if UnitLevel("player") ~= MAX_PLAYER_LEVEL or not ReputationWatchBar:IsVisible() then HideWatchedReputationBarText() + return end + ReputationWatchBar.textLocked = 1 + ReputationWatchStatusBarText:Show() end function HideWatchedReputationBarText(unlock) - if unlock or not ReputationWatchBar.cvarLocked then - ReputationWatchBar.cvarLocked = nil - ReputationWatchStatusBarText:Hide() - ReputationWatchBar.textLocked = nil - end + -- Same gate as the ref's `unlock or not cvarLocked`, spun as the early exit. + if not unlock and ReputationWatchBar.cvarLocked then return end + ReputationWatchStatusBarText:Hide() + ReputationWatchBar.cvarLocked = nil + ReputationWatchBar.textLocked = nil end -- ── The watch bar's own handlers ──────────────────────────────────────────────────────── @@ -1227,11 +1359,7 @@ -- `script::cvars`, decision 1140, which carves exactly this). function BenillaWatchBar_OnEvent(bar, event) if event == "CVAR_UPDATE" and arg1 == "statusBarText" then - if arg2 == "1" then - ShowWatchedReputationBarText("lock") - else - HideWatchedReputationBarText("unlock") - end + if arg2 == "1" then ShowWatchedReputationBarText("lock") else HideWatchedReputationBarText("unlock") end return end if event == "UPDATE_FACTION" and bar:IsVisible() and ReputationFrame_Update then @@ -1248,9 +1376,7 @@ -- Coming up with the cvar set means the numerals were meant to be pinned on all along. function BenillaWatchBar_OnShow() - if GetCVar("statusBarText") == "1" then - ShowWatchedReputationBarText("lock") - end + if GetCVar("statusBarText") == "1" then ShowWatchedReputationBarText("lock") end UIParent_ManageFramePositions() end @@ -1316,9 +1442,7 @@ end -- ref-MainMenuBar.lua l.51-54: at max level with a watched faction the strip the -- tick rides is the REPUTATION bar, which has no rested pool — hide the tick. - if UnitLevel("player") == MAX_PLAYER_LEVEL and ReputationWatchBar:IsShown() then - ExhaustionTick:Hide() - end + if UnitLevel("player") == MAX_PLAYER_LEVEL and ReputationWatchBar:IsShown() then ExhaustionTick:Hide() end end -- The rest-state paint (only these two events repaint it, the ref's own split): one -- rest state, one hue, worn by all three pieces at once — the strip's fill, the pale @@ -1338,9 +1462,7 @@ -- REPLACED the XP bar (max level), so there is no XP strip left for the tick to sit on -- whatever the event was. Distinct from the guard above, which only runs on the four -- position events; this one runs on every one. - if ReputationWatchBar:IsShown() and not MainMenuExpBar:IsShown() then - ExhaustionTick:Hide() - end + if ReputationWatchBar:IsShown() and not MainMenuExpBar:IsShown() then ExhaustionTick:Hide() end end -- The tick's tooltip (ref-MainMenuBar.lua ExhaustionToolTipText, the live branches only — @@ -1353,30 +1475,24 @@ function ExhaustionToolTipText() if SHOW_NEWBIE_TIPS ~= "1" then local x, y = ExhaustionTick:GetCenter() - if ExhaustionTick:IsVisible() then - if x >= (GetScreenWidth() / 2) then - GameTooltip:SetOwner(ExhaustionTick, "ANCHOR_LEFT") - else - GameTooltip:SetOwner(ExhaustionTick, "ANCHOR_RIGHT") - end - else + if not ExhaustionTick:IsVisible() then GameTooltip_SetDefaultAnchor(GameTooltip, UIParent) + elseif x >= (GetScreenWidth() / 2) then + GameTooltip:SetOwner(ExhaustionTick, "ANCHOR_LEFT") + else + GameTooltip:SetOwner(ExhaustionTick, "ANCHOR_RIGHT") end end - local exhaustionStateID, exhaustionStateName, exhaustionStateMultiplier = GetRestState() local exhaustionThreshold = GetXPExhaustion() + local exhaustionStateID, exhaustionStateName, exhaustionStateMultiplier = GetRestState() exhaustionStateMultiplier = exhaustionStateMultiplier * 100 - local exhaustionCountdown = nil - if GetTimeToWellRested() then - exhaustionCountdown = GetTimeToWellRested() / 60 - end + -- nil while the wire has not answered; a number (minutes) once it has. + local exhaustionCountdown = GetTimeToWellRested() and GetTimeToWellRested() / 60 local tooltipText = format(EXHAUST_TOOLTIP1, exhaustionStateName, exhaustionStateMultiplier) local append = nil if IsResting() then - if exhaustionThreshold and exhaustionCountdown then - append = format(EXHAUST_TOOLTIP4, exhaustionCountdown) - end + if exhaustionThreshold and exhaustionCountdown then append = format(EXHAUST_TOOLTIP4, exhaustionCountdown) end elseif exhaustionStateID == 4 or exhaustionStateID == 5 then append = EXHAUST_TOOLTIP2 end @@ -1385,25 +1501,22 @@ end if SHOW_NEWBIE_TIPS ~= "1" then GameTooltip:SetText(tooltipText) - else - if GameTooltip.canAddRestStateLine then - GameTooltip:AddLine("\n" .. tooltipText) - GameTooltip:Show() - GameTooltip.canAddRestStateLine = nil - end + elseif GameTooltip.canAddRestStateLine then + GameTooltip:AddLine("\n" .. tooltipText) + GameTooltip.canAddRestStateLine = nil + GameTooltip:Show() end end -- Driven from the XP BAR's OnUpdate (the ref wires it there, not on the tick — the timer -- only ever counts while the mouse is on the bar, which is what arms it). function ExhaustionTick_OnUpdate(elapsed) - if ExhaustionTick.timer then - if ExhaustionTick.timer < 0 then - ExhaustionToolTipText() - ExhaustionTick.timer = nil - else - ExhaustionTick.timer = ExhaustionTick.timer - elapsed - end + if not ExhaustionTick.timer then return end + if ExhaustionTick.timer >= 0 then + ExhaustionTick.timer = ExhaustionTick.timer - elapsed + else + ExhaustionTick.timer = nil + ExhaustionToolTipText() end end @@ -1463,13 +1576,13 @@ end frame.updateInterval = PERFORMANCEBAR_UPDATE_INTERVAL local _, _, latency = GetNetStats() + local r, g, b = 0, 1, 0 if latency > PERFORMANCEBAR_MEDIUM_LATENCY then - MainMenuBarPerformanceBar:SetVertexColor(1, 0, 0) + r, g, b = 1, 0, 0 elseif latency > PERFORMANCEBAR_LOW_LATENCY then - MainMenuBarPerformanceBar:SetVertexColor(1, 1, 0) - else - MainMenuBarPerformanceBar:SetVertexColor(0, 1, 0) + r, g, b = 1, 1, 0 end + MainMenuBarPerformanceBar:SetVertexColor(r, g, b) -- Held-open tooltips re-read the number on the same 10 s beat (the ref's own refresh). if frame.hover then BenillaPerformanceBar_ShowTooltip(frame) @@ -1815,6 +1928,9 @@ + @@ -1877,26 +1993,35 @@ + art with no OnClick for as long as `CURRENT_ACTIONBAR_PAGE` did not exist. + + The HitRectInsets are LOAD-BEARING, not decoration: the two 32x32 squares + are stacked only 20px apart, so their raw rects OVERLAP by 12px and the + hit-test (later sibling wins) would give that band — the bottom third of + the visible up arrow — to the DOWN button. The ref's ±6/±7 insets shrink + each to the 20x18 arrow it actually draws, which separates them and stops + the down arrow's rect hanging below the screen edge and over the XP bar. --> @@ -1954,88 +2079,103 @@ - + BOTTOMLEFT +(40,0) as declared — every shown appearance re-anchors at x=4 (the ref Lua's + own BONUSACTIONBAR_XPOS, whose art trims 4px of left TexCoord to line back up); button 1 + at BOTTOMLEFT +(5,4), each next LEFT-of-previous-RIGHT +6; the two OVERLAY art strips are + the dwarf sheet rows the main bar itself draws (ref l.55-105). --> + + + + + + + + + + + + + + - BenillaActionButton_OnLoad(self, 1) + BenillaActionButton_OnLoad(self, 1) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 2) + BenillaActionButton_OnLoad(self, 2) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 3) + BenillaActionButton_OnLoad(self, 3) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 4) + BenillaActionButton_OnLoad(self, 4) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 5) + BenillaActionButton_OnLoad(self, 5) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 6) + BenillaActionButton_OnLoad(self, 6) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 7) + BenillaActionButton_OnLoad(self, 7) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 8) + BenillaActionButton_OnLoad(self, 8) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 9) + BenillaActionButton_OnLoad(self, 9) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 10) + BenillaActionButton_OnLoad(self, 10) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 11) + BenillaActionButton_OnLoad(self, 11) self.isBonus = 1 - BenillaActionButton_OnLoad(self, 12) + BenillaActionButton_OnLoad(self, 12) self.isBonus = 1 + + BenillaBonusBar_OnLoad(self) + BonusActionBar_OnUpdate(arg1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + getglobal(this:GetParent():GetName() .. "Silver"):SetFocus() + getglobal(this:GetParent():GetName() .. "Silver"):SetFocus() + this:ClearFocus() + BenillaAuctionMoneyInput_OnChanged(this) + this:HighlightText(0, 0) + this:HighlightText() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + getglobal(this:GetParent():GetName() .. "Copper"):SetFocus() + getglobal(this:GetParent():GetName() .. "Copper"):SetFocus() + this:ClearFocus() + BenillaAuctionMoneyInput_OnChanged(this) + this:HighlightText(0, 0) + this:HighlightText() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + this:ClearFocus() + BenillaAuctionMoneyInput_OnChanged(this) + this:HighlightText(0, 0) + this:HighlightText() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + if ( not this.tooltip or this.tooltip == "" ) then return end + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(this.tooltip) + + GameTooltip:Hide() + + + + + + SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("AUCTION") + + + + + + SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("AUCTION") + -- The buyout line is gold where the bid above it is white (ref-tmpl l.392). + SetMoneyFrameColor(this:GetName(), + NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b) + + + + + + + + + + + + + + + + + + + + + + + + + local index = this:GetID() + FauxScrollFrame_GetOffset(BrowseScrollFrame) + if ( not AuctionRow_ModifierClick("list", index) ) then BrowseButton_OnClick(this) end + + + + + + + + + + + + + + + + + + + + + + + + + if ( not this.tooltip or this.tooltip == "" ) then return end + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(this.tooltip) + + GameTooltip:Hide() + + + + + + SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("AUCTION") + + + + + + SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("AUCTION") + + + + + + local index = this:GetID() + FauxScrollFrame_GetOffset(BidScrollFrame) + if ( not AuctionRow_ModifierClick("bidder", index) ) then BidButton_OnClick(this) end + + + + + + + + + + + + + + + + + + + + + + + + + if ( not this.tooltip or this.tooltip == "" ) then return end + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(this.tooltip) + + GameTooltip:Hide() + + + + + + SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("AUCTION") + + + + + + + + + + + SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("AUCTION") + + + + + + local index = this:GetID() + FauxScrollFrame_GetOffset(AuctionsScrollFrame) + if ( not AuctionRow_ModifierClick("owner", index) ) then AuctionsButton_OnClick(this) end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BrowseMinLevel:SetFocus() + AuctionFrameBrowse_Search(); this:ClearFocus() + + + + + + + + + + + + + + + + BrowseMaxLevel:SetFocus() + this:ClearFocus() + + + + + + + + BrowseBidPriceGold:SetFocus() + this:ClearFocus() + + + + + + + + + + + + + + BrowseDropDown_OnLoad() + BrowseDropDown_OnLoad() + + + + + + + + + + + + AuctionFrameBrowse_OnLoad(self) + AuctionFrameBrowse_OnShow() + AuctionFrameBrowse_OnEvent() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AuctionFrameBid_OnLoad(self) + AuctionFrameBid_OnShow() + AuctionFrameBid_OnEvent() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AuctionsRadioButton_SetLabel(this, 2) + AuctionsRadioButton_OnClick(1) + + + + + + AuctionsRadioButton_SetLabel(this, 8) + AuctionsRadioButton_OnClick(2) + + + + + + AuctionsRadioButton_SetLabel(this, 24) + AuctionsRadioButton_OnClick(3) + + + + + + + + + + SmallMoneyFrame_OnLoad() + MoneyFrame_SetType("AUCTION_DEPOSIT") + + + + + + + + AuctionFrameAuctions_OnLoad(self) + AuctionFrameAuctions_OnShow() + AuctionFrameAuctions_OnEvent() + + + + + AuctionFrame_OnLoad(self) + AuctionFrame_OnShow() + AuctionFrame_OnHide() + AuctionFrame_OnEvent(self) + AuctionFrame_OnMouseWheel(arg1) + + + diff --git a/crates/benilla-app/assets/ui/CraftFrame.xml b/crates/benilla-app/assets/ui/CraftFrame.xml index bd4772ac..4d108dce 100644 --- a/crates/benilla-app/assets/ui/CraftFrame.xml +++ b/crates/benilla-app/assets/ui/CraftFrame.xml @@ -38,10 +38,12 @@ selection-highlight Frame, and the `button.craftIndex`/`Button:SetTextColor` engine-gap substitutions (deviation 7 below). - FLAT layout (TradeSkillFrame.xml's own house constraint, restated here): a child shown - at runtime does not render its own FontStrings in this engine, so the detail-pane - FontStrings + the title live on BenillaCraftFrame's OWN (OVERLAY, above the window's - own BACKGROUND/BORDER/ARTWORK art groups), never on a nested sub-frame. The ref's + FLAT layout — a CHOICE inherited from TradeSkillFrame.xml, which inherited it from + TrainerFrame.xml, which stated it as a house constraint it never was. A runtime-shown child + DOES render its own FontStrings in this engine (probed; decision 1517). The + detail-pane FontStrings + the title live on BenillaCraftFrame's OWN (OVERLAY, above + the window's own BACKGROUND/BORDER/ARTWORK art groups) because that is how this window was + built, not because nesting fails. The ref's `CraftDetailScrollFrame`/`CraftDetailScrollChildFrame` therefore don't exist here — the detail is a FIXED (non-scrolling) pane, computed once from the flattened ScrollFrame+ScrollChild offsets (TOPLEFT 20,-234 + each child's own ref-quoted offset — BYTE-IDENTICAL to TradeSkill's diff --git a/crates/benilla-app/assets/ui/GroupLootFrame.xml b/crates/benilla-app/assets/ui/GroupLootFrame.xml index 19511989..cf76f1fb 100644 --- a/crates/benilla-app/assets/ui/GroupLootFrame.xml +++ b/crates/benilla-app/assets/ui/GroupLootFrame.xml @@ -55,12 +55,13 @@ wire) that this button has never had. LootFrame.xml's own row template (`BenillaLootButtonTemplate`) carries the same hover-cursor omission. - THE ITEM TOOLTIP uses `GameTooltip:SetItemById(itemId, name, quality)` (LootFrame.xml's own - `BenillaLootItem_OnEnter` idiom), not the reference's `SetLootRollItem` (no such tooltip - method here) — `itemId` is the benilla-extension 6th return of `GetLootRollItemInfo` - (see `loot_roll.rs`), always populated once a roll is known (unlike name/texture/quality, which - may be `nil` while the item template is still in flight). `GetLootRollItemLink` DOES exist now - (decision 1059) but is the click's, not the hover's: the tooltip wants the id, not the markup. + THE ITEM TOOLTIP uses `GameTooltip:SetLootRollItem(rollID)` — the reference's own binding + (decision 1547; it replaced a `SetItemById` that could not see the drop's random-suffix + roll and so printed `` on every rolled item). The `itemId` guard stays: + it is the benilla-extension 6th return of `GetLootRollItemInfo` (see `loot_roll.rs`), + always populated once a roll is known — unlike name/texture/quality, which may be `nil` + while the item template is still in flight. `GetLootRollItemLink` (decision 1059) is the + click's, not the hover's: the tooltip wants the roll, not the markup. `toplevel="true"` (ref l.243) IS STILL OMITTED, but for a NEW reason — the old one ("`SetToplevel` isn't in the v1 object model yet ... trivially re-added the day `SetToplevel` lands") has expired. @@ -258,15 +259,16 @@ end -- The item icon's hover tooltip (ref $parentIconFrame OnEnter/OnLeave, l.341-349, minus the - -- CursorUpdate/ResetCursor calls this engine has no counterpart for — see the header). Uses - -- the LootFrame.xml row idiom (SetItemById + the benilla itemId extension) rather than the - -- reference's SetLootRollItem, which doesn't exist here. + -- CursorUpdate/ResetCursor calls this engine has no counterpart for — see the header). + -- SetLootRollItem, the reference's own binding: the ROLL is the source, so the hover sees + -- the drop's random-suffix roll (through SetItemById it showed `` — + -- decision 1547). function BenillaGroupLootFrame_Icon_OnEnter(button) local frame = button:GetParent() GameTooltip:SetOwner(button, "ANCHOR_RIGHT") local _, name, _, quality, _, itemId = GetLootRollItemInfo(frame.rollID) if itemId then - GameTooltip:SetItemById(itemId, name, quality) + GameTooltip:SetLootRollItem(frame.rollID) end end diff --git a/crates/benilla-app/assets/ui/LootFrame.xml b/crates/benilla-app/assets/ui/LootFrame.xml index 47d007d3..726cbf8c 100644 --- a/crates/benilla-app/assets/ui/LootFrame.xml +++ b/crates/benilla-app/assets/ui/LootFrame.xml @@ -41,9 +41,13 @@ Paging (LootFrame.lua l.27-31,70-73,105-118): 4 rows/page; > 4 items spends one slot on the pager, showing 3 rows + Up/Down. We keep the current page across content-change repaints (LOOT_UPDATE) and reset it to 1 only on a fresh open (LOOT_OPENED) — the merchant SHOW/UPDATE - split. (This replaces Blizzard's per-button LOOT_SLOT_CLEARED collapse with a full - re-snapshot; the app fires LOOT_UPDATE on any content change, incl. an async item name/quality - landing and a looted/coin-cleared row.) + split. (The per-button LOOT_SLOT_CLEARED event is replaced by a full re-snapshot repaint; the + app fires LOOT_UPDATE on any content change, incl. an async item name/quality landing and a + looted/coin-cleared row. The OBSERVABLE matches the ref exactly: the slot list is fixed while + the window is open — GetNumLootItems counts cleared slots, which answer neither IsItem nor + IsCoin, so a looted row's button hides IN PLACE and the rows below never shift up — and a + page left with every button hidden advances to the next page when one exists, the ref's + LOOT_SLOT_CLEARED tail, l.38-50.) UIPanel slot system (decision 0084 §2): registered in UiPanels.xml as area="left", pushable=7 — so it carries NO hardcoded ; ShowUIPanel SetPoints it (and, when a @@ -133,11 +137,14 @@ LootSlot(button.slot) end - -- Hover: the item name coloured by quality (loot rows carry it — GetLootSlotInfo's 4th - -- return), reusing loot_color. The coin pile has no item quality; its formatted text (already - -- the row name) shows white. + -- Hover: the plain engine item tooltip, ITEM rows only — the ref's own gate (its + -- LootButtonTemplate runs SetLootItem only `if LootSlotIsItem(slot)`, so the + -- coin pile shows no tooltip at all). No hand-added lines: the real tooltip never shows a + -- stack size (the 0274 P1 'Stack Size' drop retired the same line from the bag hovers; + -- this file's copy predated it and survived). function BenillaLootItem_OnEnter(button) if not button.slot then return end + if not LootSlotIsItem(button.slot) then return end local texture, item, quantity, quality, itemId = GetLootSlotInfo(button.slot) button.itemId = itemId if not item then return end @@ -145,16 +152,11 @@ -- MerchantFrame.xml walks ANCHOR_RIGHT back by a negative x): 37px icon on a 160 row -- → -123 lands the tip at the icon's right edge. GameTooltip:SetOwner(button, "ANCHOR_RIGHT", -123, 0) - -- The shared item tooltip when the row is an ITEM (coin rows have no item id/stats). - local color = loot_color(quality) - if button.itemId and button.itemId ~= 0 then - GameTooltip:SetItemById(button.itemId, item, quality) - else - GameTooltip:SetText(item, color.r, color.g, color.b) - end - if (quantity or 1) > 1 then - GameTooltip:AddLine("Stack Size: " .. quantity, 1, 1, 1) - end + -- SetLootItem, like the ref's own LootButtonTemplate: the loot ROW is the source, not + -- just its item id, so the tooltip sees the drop's random-suffix roll. Through + -- SetItemById (a template source) a "… of the Monkey" showed `` + -- until the item reached a bag — decision 1547. + GameTooltip:SetLootItem(button.slot) GameTooltip:Show() end @@ -171,8 +173,8 @@ if numItems > BENILLA_LOOT_NUMBUTTONS then perPage = perPage - 1 end - -- Clamp the page (a shrunk loot can leave us past the end — benilla's replacement for - -- Blizzard's per-button collapse, l.38-50). + -- Clamp the page. numItems is constant while a window is open (cleared slots stay in + -- the count — the fixed slot layout), so this only guards a stray page value. local maxPage = 1 if numItems > 0 then maxPage = math.ceil(numItems / perPage) @@ -250,8 +252,20 @@ ShowUIPanel(frame) elseif event == "LOOT_UPDATE" then -- A content change while open (name/quality landed, a row/coin removed): repaint, - -- keeping the current page. + -- keeping the current page. A looted slot's button hides IN PLACE (the slot stays + -- in the layout as a gap — the ref's LOOT_SLOT_CLEARED, l.22-37). LootFrame_Update() + -- The ref's LOOT_SLOT_CLEARED tail (l.38-50): a page left with every button hidden + -- moves down to the next page when one exists. + local allHidden = true + for index = 1, BENILLA_LOOT_NUMBUTTONS do + if getglobal("LootButton" .. index):IsVisible() then + allHidden = false + end + end + if allHidden and getglobal("LootDownButton"):IsVisible() then + LootFrame_PageDown() + end elseif event == "LOOT_CLOSED" then HideUIPanel(frame) end @@ -331,12 +345,13 @@ - - - + + - + diff --git a/crates/benilla-app/assets/ui/MailFrame.xml b/crates/benilla-app/assets/ui/MailFrame.xml index 121da0fb..110b3c3b 100644 --- a/crates/benilla-app/assets/ui/MailFrame.xml +++ b/crates/benilla-app/assets/ui/MailFrame.xml @@ -86,9 +86,7 @@ -- reset side effect — wow-re §5); either way reset the form + play the page turn. SendMailFrame_Reset() PlaySound("igAbiliityPageTurn") - if SendMailFrame.sendMode == "reply" then - MailFrameTab_OnClick(1) - end + if SendMailFrame.sendMode == "reply" then MailFrameTab_OnClick(1) end elseif event == "MAIL_FAILED" then -- Fires on EVERY SMSG_SEND_MAIL_RESULT for a send (success included, wow-re §5): its -- only job is to re-enable the button so the UI never stays blocked. @@ -148,9 +146,10 @@ MailFrameTopLeft:SetPoint("TOPLEFT", "MailFrame", "TOPLEFT", skin.x, skin.y) if not reading then - -- Arriving at the form repaints it and clears the mode a Reply had set. - SendMailFrame_Update() + -- Arriving at the form repaints it and clears the mode a Reply had set + -- (SendMailFrame_Update never reads the mode, so the order is free). SendMailFrame.sendMode = "send" + SendMailFrame_Update() end PlaySound("igSpellBookOpen") end @@ -208,11 +207,11 @@ expireTime:Show() -- COD tag (ref l.157-164) + money field. if CODAmount > 0 then - getglobal("MailItem"..i.."ButtonCOD"):Show() button.cod = CODAmount + getglobal("MailItem"..i.."ButtonCOD"):Show() else - getglobal("MailItem"..i.."ButtonCOD"):Hide() button.cod = nil + getglobal("MailItem"..i.."ButtonCOD"):Hide() end if money > 0 then button.money = money else button.money = nil end if InboxFrame.openMailID == index then @@ -221,10 +220,10 @@ button:SetChecked(nil) end else - getglobal("MailItem"..i.."Button"):Hide() - getglobal("MailItem"..i.."Sender"):SetText("") - getglobal("MailItem"..i.."Subject"):SetText("") getglobal("MailItem"..i.."ExpireTime"):Hide() + getglobal("MailItem"..i.."Button"):Hide() + getglobal("MailItem"..i.."Subject"):SetText("") + getglobal("MailItem"..i.."Sender"):SetText("") end index = index + 1 end @@ -240,14 +239,14 @@ -- Row click: open / close the letter (ref InboxFrame_OnClick l.201-213; this = the button). function InboxFrame_OnClick(index) - if this:GetChecked() then - InboxFrame.openMailID = index - OpenMail_Update() - ShowUIPanel(OpenMailFrame) - PlaySound("igSpellBookOpen") - else + if not this:GetChecked() then InboxFrame.openMailID = 0 HideUIPanel(OpenMailFrame) + else + InboxFrame.openMailID = index + OpenMail_Update() + PlaySound("igSpellBookOpen") + ShowUIPanel(OpenMailFrame) end InboxFrame_Update() end @@ -292,12 +291,81 @@ return end local pkg, sta, sender, subject, money, cod, days, itemID, wasRead, wasReturned, textCreated = GetInboxHeaderInfo(InboxFrame.openMailID) - if money == 0 and not itemID and textCreated then - DeleteInboxItem(InboxFrame.openMailID) - end + if money == 0 and not itemID and textCreated then DeleteInboxItem(InboxFrame.openMailID) end InboxFrame.openMailID = 0 - InboxFrame_Update() PlaySound("igSpellBookClose") + InboxFrame_Update() + end + + -- One GlobalString read — the player's own table, executed off their chain at boot and + -- absent in a bare-XML harness. (AuctionFrame.xml carries the twin of this; the static + -- labels below take theirs through `text=` instead, which the loader resolves the same way.) + local function gs(key, fallback) + local value = getglobal(key) + if value and value ~= "" then + return value + end + return fallback or "" + end + + -- The auction house's RECEIPT (ref l.300-362). An auction mail's body is machine-written + -- bookkeeping, not prose — the engine parses it (`ui_mail::invoice`) and hands back seven + -- values; this lays them out. The guard is on `playerName`, the THIRD value, because that + -- is the one the miss tail nils: an invoice whose counterparty has not been named yet is + -- not renderable, and the row re-resolves when the name query lands. + -- + -- Two shapes off one pane. A SELLER's is the full sum — sale price, plus what the deposit + -- refunds, minus the house's cut, ruled off and totalled. A BUYER's is a single line: what + -- you paid. The difference is not just which lines show, it is where the total and the + -- rule sit, so both get re-anchored every time. + local function OpenMail_UpdateInvoice(isInvoice, id) + local invoiceType, itemName, playerName, bid, buyout, deposit, consignment + if isInvoice then + invoiceType, itemName, playerName, bid, buyout, deposit, consignment = + GetInboxInvoiceInfo(id) + end + if not playerName then + OpenMailInvoiceFrame:Hide() + return + end + -- Paid the asking price, or won it on a bid? `bid == buyout` is the whole test. + local buyMode = "(" .. gs(bid == buyout and "BUYOUT" or "HIGH_BIDDER") .. ")" + if invoiceType == "buyer" then + OpenMailInvoiceItemLabel:SetText(gs("ITEM_PURCHASED_COLON") .. " " .. itemName .. " " .. buyMode) + OpenMailInvoicePurchaser:SetText(gs("SOLD_BY_COLON") .. " " .. playerName) + OpenMailInvoiceBuyMode:SetText("") + OpenMailInvoiceAmountReceived:SetText(gs("AMOUNT_PAID_COLON")) + -- One line, so the total rides up to where the sale price would have been. + OpenMailInvoiceAmountReceived:SetPoint("TOPRIGHT", "OpenMailInvoiceSalePrice", "TOPRIGHT", 0, 0) + OpenMailArithmeticLine:SetPoint("TOP", "OpenMailInvoicePurchaser", "BOTTOMLEFT", 125, 0) + MoneyFrame_Update("OpenMailTransactionAmountMoneyFrame", bid) + OpenMailInvoiceSalePrice:Hide() + OpenMailInvoiceDeposit:Hide() + OpenMailInvoiceHouseCut:Hide() + OpenMailSalePriceMoneyFrame:Hide() + OpenMailDepositMoneyFrame:Hide() + OpenMailHouseCutMoneyFrame:Hide() + else + OpenMailInvoiceItemLabel:SetText(gs("ITEM_SOLD_COLON") .. " " .. itemName) + OpenMailInvoicePurchaser:SetText(gs("PURCHASED_BY_COLON") .. " " .. playerName) + OpenMailInvoiceBuyMode:SetText(buyMode) + OpenMailInvoiceAmountReceived:SetText(gs("AMOUNT_RECEIVED_COLON")) + OpenMailArithmeticLine:SetPoint("TOP", "OpenMailInvoiceHouseCut", "BOTTOMRIGHT", 0, 9) + OpenMailInvoiceAmountReceived:SetPoint("TOPRIGHT", "OpenMailInvoiceHouseCut", "BOTTOMRIGHT", 0, -18) + MoneyFrame_Update("OpenMailDepositMoneyFrame", deposit) + MoneyFrame_Update("OpenMailSalePriceMoneyFrame", bid) + MoneyFrame_Update("OpenMailHouseCutMoneyFrame", consignment) + -- The cut is the one number that leaves — red, and the only recoloured line. + SetMoneyFrameColor("OpenMailHouseCutMoneyFrame", 1.0, 0, 0) + MoneyFrame_Update("OpenMailTransactionAmountMoneyFrame", bid + deposit - consignment) + OpenMailInvoiceSalePrice:Show() + OpenMailInvoiceDeposit:Show() + OpenMailInvoiceHouseCut:Show() + OpenMailSalePriceMoneyFrame:Show() + OpenMailDepositMoneyFrame:Show() + OpenMailHouseCutMoneyFrame:Show() + end + OpenMailInvoiceFrame:Show() end function OpenMail_Update() @@ -312,8 +380,20 @@ if not sender then sender = "Unknown" end OpenMailSender:SetText(sender) OpenMailSubject:SetText(subject) - OpenMailFrameIcon:SetTexture(stationeryIcon) -- ref SetPortraitToTexture l.174 + -- SetPortraitToTexture, not SetTexture (ref l.174). The stationery icon is an ordinary + -- square ITEM icon (`INV_Misc_Note_01`), and the portrait ring is a thin band with + -- transparent corners — set raw, the icon's dark border shows through them as four + -- little squares around the circle (director's report, 2026-08-22). The reference + -- reaches for the portrait verb here and nowhere else in this file for exactly that + -- reason: the INBOX window's ring holds `Mail-Icon`, purpose-drawn art that needs no + -- crop, and the reference leaves that one raw too (ref l.258, and so do we). + SetPortraitToTexture("OpenMailFrameIcon", stationeryIcon) local bodyText, texture, isTakeable, isInvoice = GetInboxText(id) + OpenMail_UpdateInvoice(isInvoice, id) + -- Unconditional, exactly as the reference writes it (ref l.293) — and it is safe to be + -- unconditional because `bodyText` is **nil** for an invoice: the engine's own + -- `GetInboxText` carve-out, not this file's (decision 1527). The receipt therefore has + -- nothing to sit on top of. OpenMailBodyText:SetText(bodyText) if texture then OpenStationeryBackgroundLeft:SetTexture(BENILLA_STATIONERY_PATH..texture.."1") @@ -347,13 +427,13 @@ else getglobal("OpenMailPackageButtonIcon"):SetVertexColor(1.0, 0.1, 0.1) end - OpenMailPackageButton:Enable() OpenMailPackageButton:Show() + OpenMailPackageButton:Enable() OpenMailMoneyButton:SetPoint("LEFT", "OpenMailPackageButton", "RIGHT", 10, 0) hasAttachments = 1 else - OpenMailFrame.itemName = nil OpenMailPackageButton:Hide() + OpenMailFrame.itemName = nil OpenMailMoneyButton:SetPoint("LEFT", "OpenMailPackageButton", "LEFT", 0, 0) end if CODAmount > 0 then OpenMailFrame.cod = CODAmount else OpenMailFrame.cod = nil end @@ -491,11 +571,9 @@ SendMailFrame.previousItem = itemName end else - if SendMailSubjectEditBox:GetText() == SendMailFrame.previousItem then - SendMailSubjectEditBox:SetText("") - end - SendMailFrame.previousItem = "" + if SendMailSubjectEditBox:GetText() == SendMailFrame.previousItem then SendMailSubjectEditBox:SetText("") end SendMailRadioButton_OnClick(1) + SendMailFrame.previousItem = "" SendMailCODButton:Disable() SendMailCODButtonText:SetTextColor(0.5, 0.5, 0.5) end @@ -541,21 +619,17 @@ SendMailErrorText:Hide() SendMailErrorCoin:Hide() end - if checks == checksRequired then - SendMailMailButton:Enable() - else - SendMailMailButton:Disable() - end + if checks == checksRequired then SendMailMailButton:Enable() else SendMailMailButton:Disable() end end function SendMailRadioButton_OnClick(index) if index == 1 then - SendMailSendMoneyButton:SetChecked(1) SendMailCODButton:SetChecked(nil) + SendMailSendMoneyButton:SetChecked(1) SendMailMoneyText:SetText("Amount to send:") -- AMOUNT_TO_SEND else - SendMailSendMoneyButton:SetChecked(nil) SendMailCODButton:SetChecked(1) + SendMailSendMoneyButton:SetChecked(nil) SendMailMoneyText:SetText("Cash on Delivery Amount:") -- COD_AMOUNT end PlaySound("igMainMenuOptionCheckBoxOn") @@ -1468,6 +1542,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SmallMoneyFrame_OnLoad() MoneyFrame_SetType("AUCTION") + + + + SmallMoneyFrame_OnLoad() MoneyFrame_SetType("AUCTION") + + + + + + + + + SmallMoneyFrame_OnLoad() MoneyFrame_SetType("AUCTION") + + + + + + + + + SmallMoneyFrame_OnLoad() MoneyFrame_SetType("AUCTION") + + + + + + local centre = GetCenterFrame() + if centre then + HideUIPanel(centre) + end + OpenMailFrame_OnHide() diff --git a/crates/benilla-app/assets/ui/MoneyFrame.xml b/crates/benilla-app/assets/ui/MoneyFrame.xml index eeebadc3..7353f8a7 100644 --- a/crates/benilla-app/assets/ui/MoneyFrame.xml +++ b/crates/benilla-app/assets/ui/MoneyFrame.xml @@ -16,23 +16,32 @@ by 9, and 13 declare a frame `inherits="SmallMoneyFrameTemplate"`. `MoneyTypeInfo` was one of the top runtime walls the frame rename left standing. - ── THE OTHER COIN KIT, and why there are two ──────────────────────────────────────────────── - benilla's own eight windows (merchant, bank, trade, mail, quest, quest log, taxi, trainer) do - NOT use this file. They render coins through `BenillaMoney_Set`/`_Clear`/`_SetColor` in + ── THE OTHER COIN KIT, and why there are still two ────────────────────────────────────────── + Eight of our windows (merchant, bank, trade, mail, quest, quest log, taxi, trainer) do NOT use + this file. They render coins through `BenillaMoney_Set`/`_Clear`/`_SetColor` in MerchantFrame.xml: a three-SLOT chain (`Coin1..3`) that fills only the non-zero denominations in display order and re-flows, rather than three fixed GoldButton/SilverButton/ CopperButton frames that hide themselves. That kit takes its coin TexCoords, its collapse and its shrink from the same specification as this one, and its layout is the one the director has signed off across those eight windows. - So the two coexist deliberately, and the split is by AUDIENCE, not by accident: - · `BenillaMoney_*` + `Coin1..3` — what our own windows are laid out on. + The auction house is the exception, and the reason the reconciliation started there rather + than anywhere else: the three-slot kit has no notion of a money TYPE, so it cannot express + `showSmallerCoins` or `fixedWidth` — and those two flags ARE the auction house's price column + (`MoneyTypeInfo["AUCTION"]`, declared below and, until AuctionFrame.xml moved, used by + nothing). On the three-slot kit "1g" showed as one lone gold coin where the reference shows + `1g 0s 0c`, and a column of prices packed tight instead of lining up. That is not a layout + preference to re-approve, it is the kit being the wrong kit. See decision 1522. + + So the two still coexist, and the split is now by KIT CAPABILITY as much as by audience: + · `BenillaMoney_*` + `Coin1..3` — the eight windows whose look is signed off and + whose money is all plain collapsing STATIC amounts, which that kit renders correctly. · `MoneyFrame_*` + `MoneyTypeInfo` here — the names and structure an addon writes against, - and what an addon's `inherits="SmallMoneyFrameTemplate"` resolves to. - The reconciliation — migrating those eight windows onto `SmallMoneyFrameTemplate` + - `MoneyFrame_Update` and retiring `BenillaMoney_*` — is what the specification itself does and - is the right end state; it is a re-layout of hand-authored art in eight files (an approved - look), so it is named here rather than done in passing. + what an addon's `inherits="SmallMoneyFrameTemplate"` resolves to, and what the auction + house is laid out on. + Finishing the migration — moving those eight onto `SmallMoneyFrameTemplate` and retiring + `BenillaMoney_*` — is still the right end state; it is a re-layout of hand-authored art in + eight files (an approved look), so it stays named here rather than done in passing. ── WHAT IS NOT CARRIED, and why (each is guarded at its own site below) ───────────────────── · THE COIN PICKUP. `OpenCoinPickupFrame`/`UpdateCoinPickupFrame`/`CoinPickupFrame` belong to @@ -341,6 +350,12 @@ -- `math.mod`, which truncates towards zero, so `mod(-1, 100)` is -1 and not 99 — the two -- forms part company the moment `money` goes negative (which `GetMoney()` minus a trade -- offer can), and this one is the specified answer. + -- A STATIC/AUCTION frame's amount is whatever its owner last parked on it, and on the + -- very first paint — `MoneyFrame_SetType`'s own repaint, in OnLoad, before any owner has + -- filled it — that is nil. The specification divides straight into it and throws there; + -- we answer 0, because the throw would take the whole ``/`` chain down + -- with it and an empty price column is the honest reading of "nothing set yet". + money = money or 0 local gold = floor(money / COPPER_PER_GOLD) local silver = floor((money - gold * COPPER_PER_GOLD) / COPPER_PER_SILVER) local copper = mod(money, COPPER_PER_SILVER) diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index de6fb98b..b3a98397 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -619,7 +619,10 @@ "RowEnableMusic", "RowEnableAmbience", }, Graphics = { "RowUiScale", "RowFarclip", "RowWorldDetail", "RowVerticalSync" }, - Nameplates = { "RowPlayerNames", "RowNpcNames", "RowOwnName" }, + Nameplates = { + "RowEnemyPlates", "RowFriendlyPlates", + "RowPlayerNames", "RowNpcNames", "RowOwnName", + }, -- The stock globals that were already defined AND consumed when 1134 opened the store -- (1136; Buff Durations 1139), plus the two equipment-display toggles (1472), in the -- reference's own on-screen order. Its Basic Options Display box is three COLUMNS read @@ -2742,22 +2745,53 @@ + category row joined). Two groups, plates first: the V-key + HEALTH-BAR plates (vplates.rs VPlateMode — the engine bitmask + [0xc4da34] bit 0 / bit 3), then the three 1.12 UnitName* + checkboxes over the overhead-name gates (nameplates.rs + NameConfig), rows in the 1.12 Interface Options' own index + order (21/30/67), labels its GlobalStrings verbatim + (SHOW_PLAYER_NAMES/SHOW_NPC_NAMES/SHOW_OWN_NAME). + + The plate pair leads because it is what "Nameplates" means to + a player pressing V, and it is the pair that had no surface at + all — 1.12's own options UI has NO nameplate row (its + UIOptionsFrame keeps NAMEPLATES_ON/FRIENDNAMEPLATES_ON as + RegisterForSave'd globals driven only by the keybinding, and + says so in a comment), so the rows are era grammar over a + 1.12 knob — 0959's law, the WorldDetail dropdown's posture. + Labels take the 1.12 binding names' own two-word spelling + ("Name Plates", BINDING_NAME_NAMEPLATES) with the era's + enemy/friendly split naming the two bits; no 1.12 + OPTION_TOOLTIP string exists for either (nil, like Auto + Loot). --> - + + OptionsRow_OnLoad(self, "nameplateShowEnemies", + "Enemy Name Plates") + + + + + + + OptionsRow_OnLoad(self, "nameplateShowFriends", + "Friendly Name Plates") + + + + + + OptionsRow_OnLoad(self, "UnitNamePlayer", "Player Names", "OPTION_TOOLTIP_SHOW_PLAYER_NAMES") diff --git a/crates/benilla-app/assets/ui/ScriptLogFrame.xml b/crates/benilla-app/assets/ui/ScriptLogFrame.xml index 76daed25..528f01ba 100644 --- a/crates/benilla-app/assets/ui/ScriptLogFrame.xml +++ b/crates/benilla-app/assets/ui/ScriptLogFrame.xml @@ -173,6 +173,10 @@ function BenillaScriptLogFrame_OnLoad(self) getglobal("BenillaScriptLogListScrollFrame").updateFunc = BenillaScriptLog_Update + -- The ring's icon, through the portrait verb rather than left at its `file=` — see the + -- texture's own note. MailFrame.xml's open-letter icon is the same fix. + SetPortraitToTexture("BenillaScriptLogFramePortrait", + "Interface\\Icons\\INV_Misc_Note_01") end -- The shared helper steps the faux frame's bar, whose OnValueChanged runs the kit's @@ -239,6 +243,10 @@ + diff --git a/crates/benilla-app/assets/ui/StanceBar.xml b/crates/benilla-app/assets/ui/StanceBar.xml index 084cc759..7228c0fe 100644 --- a/crates/benilla-app/assets/ui/StanceBar.xml +++ b/crates/benilla-app/assets/ui/StanceBar.xml @@ -102,6 +102,25 @@ function ShapeshiftBar_Update() local numForms = GetNumShapeshiftForms() if numForms > 0 then + -- ── The shelf's LENGTH is the form count (ref l.161-175) ───────────────────── + -- The three strips are DECLARED as the >2-form chain (Left → Middle → Right), so + -- the one- and two-form shelves have to re-point the end cap. Hiding the middle + -- strip does NOT close its gap — a hidden region still resolves its rect, here + -- and in the reference — so without this the 42x50 right cap sits 38px past the + -- last button as a stray empty recess: a rogue's Stealth with a second, always- + -- empty slot beside it. One form pulls the cap back INSIDE the left cap (+12, a + -- 54px shelf around the single button); two butt the caps together; past two the + -- middle strip carries one slot per extra form and the cap chains off it. + if numForms == 1 then + ShapeshiftBarRight:SetPoint("LEFT", ShapeshiftBarLeft, "LEFT", 12, 0) + elseif numForms == 2 then + ShapeshiftBarRight:SetPoint("LEFT", ShapeshiftBarLeft, "RIGHT", 0, 0) + else + ShapeshiftBarMiddle:SetPoint("LEFT", ShapeshiftBarLeft, "RIGHT", 0, 0) + ShapeshiftBarMiddle:SetWidth(38 * (numForms - 2)) + ShapeshiftBarMiddle:SetTexCoord(0, numForms - 2, 0, 1) + ShapeshiftBarRight:SetPoint("LEFT", ShapeshiftBarMiddle, "RIGHT", 0, 0) + end ShapeshiftBarFrame:Show() else ShapeshiftBarFrame:Hide() @@ -272,8 +291,10 @@ Geometry, anchor chain and TexCoords quoted whole: Left and Right are two crops of ONE `ShapeshiftBarEnds` file (0..0.453125 and 0.453125..0.875), Middle its own, and Right - hangs off Middle's RIGHT exactly as the ref chains them — which is why hiding the - middle strip leaves its gap rather than closing it, the reference's own result. + hangs off Middle's RIGHT — the >2-form chain, which is the DECLARED state only. The + cap is re-pointed per form count by `ShapeshiftBar_Update` above (the ref's l.161-175): + hiding the middle strip leaves its 38px rect behind, so the declared chain alone + strands the cap past the last button. `hidden="true"` is the RAISED state, and it is only the pre-pass value now: `ShapeshiftBar_UpdatePosition` (above) shows and hides all three from the bottom-left diff --git a/crates/benilla-app/assets/ui/TradeSkillFrame.xml b/crates/benilla-app/assets/ui/TradeSkillFrame.xml index e39bdfd7..61221aa7 100644 --- a/crates/benilla-app/assets/ui/TradeSkillFrame.xml +++ b/crates/benilla-app/assets/ui/TradeSkillFrame.xml @@ -28,10 +28,11 @@ UIPanelButtonTemplate/close-button/item-button idioms (no such templates ship in this house dialect — TalentFrame.xml's own ENGINE API note), the persistent selection-highlight Frame. - FLAT layout (TrainerFrame.xml's own house constraint, restated here): a child shown at - runtime does not render its own FontStrings in this engine, so the detail-pane - FontStrings + the title live on BenillaTradeSkillFrame's OWN (OVERLAY, above the - window's own BACKGROUND/BORDER/ARTWORK art groups), never on a nested sub-frame. The ref's + FLAT layout — a CHOICE inherited from TrainerFrame.xml, which stated it as a house constraint + it never was. A runtime-shown child DOES render its own FontStrings in this + engine (probed; decision 1517). The detail-pane FontStrings + the title live on + BenillaTradeSkillFrame's OWN (OVERLAY, above the window's own BACKGROUND/BORDER/ + ARTWORK art groups) because that is how this window was built, not because nesting fails. The ref's `TradeSkillDetailScrollFrame`/`TradeSkillDetailScrollChildFrame` therefore don't exist here — the detail is a FIXED (non-scrolling) pane, computed once from the flattened ScrollFrame+ ScrollChild offsets (TOPLEFT 20,-234 + each child's own ref-quoted offset). Per-region regions diff --git a/crates/benilla-app/assets/ui/TrainerFrame.xml b/crates/benilla-app/assets/ui/TrainerFrame.xml index 9cb99288..4aff9813 100644 --- a/crates/benilla-app/assets/ui/TrainerFrame.xml +++ b/crates/benilla-app/assets/ui/TrainerFrame.xml @@ -38,10 +38,14 @@ - COLLAPSE WORKS. The "All" tab collapses/expands every group (Collapse/ExpandTrainerSkillLine(0)); its +/- icon reflects whether all groups are folded (the backport's numHeaders/notExpanded). - FLAT layout (both windows' note): a child shown at runtime does not render its own - FontStrings in this engine, so the detail-pane FontStrings + the title/greeting live on - BenillaTrainerFrame's OWN , never on a nested sub-frame. Per-row regions live on the row - Button template (Buttons render their own Layers). The detail is a FIXED (non-scrolling) pane + FLAT layout — a CHOICE, and it was written down here as a constraint that does not exist. + This block used to claim "a child shown at runtime does not render its own + FontStrings in this engine". That is FALSE, and it was propagated from here into two more + windows before anyone probed it (decision 1517): MailFrame's own SendMailTitleText lives in a + runtime-shown pane's Layers and has rendered correctly the whole time. The detail-pane + FontStrings + the title/greeting do live on BenillaTrainerFrame's OWN , and that is + fine — it is simply one way to lay this out, not the only one available. Per-row regions live + on the row Button template. The detail is a FIXED (non-scrolling) pane (our descriptions are empty for now) — no real ScrollFrame, unlike the ref's DetailScrollFrame. Adaptations / deviations (flagged): diff --git a/crates/benilla-app/assets/ui/UiPanels.xml b/crates/benilla-app/assets/ui/UiPanels.xml index d0233f85..536e2be1 100644 --- a/crates/benilla-app/assets/ui/UiPanels.xml +++ b/crates/benilla-app/assets/ui/UiPanels.xml @@ -11,12 +11,13 @@ reference's window arbitration (a different number is a different window order), and the `UISpecialFrames` seeds. The code around that data is ours. - Scope: `left`, `center` and `full` are all live, and so is `UIChildWindows` (decision 1507 - — all four of the reference's child windows ship now). `doublewide` is not — its one 1.12 - tenant is the auction house, which benilla doesn't ship — but the registration table keeps - its exact shape so a later row drops in unchanged. **`UISpecialFrames` is not ours to write - into** (decision 1206): it is the table an ADDON puts its own window in, and 34 corpus call - sites do. + Scope: all four areas are live — `left`, `center`, `full` and `doublewide` — and so is + `UIChildWindows` (decision 1507: all four of the reference's child windows ship now). + `doublewide` was the last one out, on the honest grounds that its one 1.12 tenant is the + auction house and benilla didn't ship one; decision 1511 is the arc that does, so the slice + came with it rather than the window working around it. **`UISpecialFrames` is not ours to + write into** (decision 1206): it is the table an ADDON puts its own window in, and 34 corpus + call sites do. Rows are keyed by the frame names we actually ship. Most of those ARE the reference's own (`GossipFrame`, `MerchantFrame`, `LootFrame`); where ours differs, the row is `Benilla*` and @@ -74,6 +75,13 @@ -- registration line (`UIPanelWindows["TaxiFrame"] = { area = "left", pushable = 0 }`), -- quoted verbatim — same left-slot competition as Gossip/Merchant/Trainer above. UIPanelWindows["TaxiFrame"] = { area = "left", pushable = 0 } + -- The auction house (decision 1511, AuctionFrame.xml): the reference's own registration, + -- quoted verbatim from `Blizzard_AuctionUI.lua` l.13 rather than UIParent.lua — the window + -- ships as a loadable addon there, so it registers its own row. **The one and only + -- `doublewide` row in all of 1.12**: an 832-wide window takes the left AND centre seats at + -- once, which is why the slice above exists at all. Its `pushable = 0` is quoted and inert + -- — ShowUIPanel's doublewide arm never consults it (l.392-397). + UIPanelWindows["AuctionFrame"] = { area = "doublewide", pushable = 0 } -- The dressing room (decision 1060, DressUpFrame.xml): the ref's own UIParent.lua l.41 -- registration (`UIPanelWindows["DressUpFrame"] = { area = "left", pushable = 2 }`), quoted -- verbatim — pushable=2 lets the NPC-session windows (pushable=0) shove it aside, which is @@ -168,7 +176,7 @@ -- table instead of squatting the `UIParent` global name; a future `CreateFrame("Frame", -- "UIParent")` then lands cleanly on an empty global rather than silently coexisting with our -- state. Every function below is defined in this same chunk, so they all close over it. - local slots = { left = nil, center = nil, fullscreen = nil } + local slots = { left = nil, center = nil, fullscreen = nil, doublewide = nil } -- SetLeftFrame(frame) — UIParent.lua l.809-824. Hides the old left occupant, SetPoints + -- Shows the new one at the left slot's screen anchor (verified: TOPLEFT UIParent, 0, -104). @@ -268,6 +276,41 @@ return slots.fullscreen end + -- SetDoublewideFrame(frame) — UIParent.lua l.788-808. One frame takes the left AND center + -- seats at once: both incumbents are dropped (nil'd first, then hidden — the order matters, + -- since an OnHide that re-enters this manager must not find itself still seated), and the + -- new frame lands at the left slot's own screen anchor, because a doublewide window simply + -- starts where left starts and is wide enough to cover both. + -- + -- Note what the reference does NOT do: `HideUIPanel` has no doublewide branch (l.754-786), + -- so nothing ever clears this seat — `UIParent.doublewide` keeps pointing at the auction + -- window forever once it has been opened, and every later ShowUIPanel re-hides an + -- already-hidden frame. We reproduce that rather than inventing a clear: `Hide()` on a + -- hidden frame fires nothing, so the difference is unobservable, and a "fix" here would be + -- a silent divergence in the one function every window's placement runs through. + function SetDoublewideFrame(frame) + local oldLeft = slots.left + local oldCenter = slots.center + slots.doublewide = frame + slots.left = nil + slots.center = nil + if oldLeft then + oldLeft:Hide() + end + if oldCenter then + oldCenter:Hide() + end + if frame then + frame:SetPoint("TOPLEFT", "UIParent", "TOPLEFT", 0, -104) + frame:Show() + end + end + + -- GetDoublewideFrame — UIParent.lua l.924-926. + function GetDoublewideFrame() + return slots.doublewide + end + -- The area a frame is REGISTERED for — or nil, which is equally the answer for no frame at -- all and for a frame this manager has never heard of. Four rules below turn on exactly -- that question ("is a native-center window holding the screen?", "is the frame parked at @@ -365,10 +408,11 @@ end end - -- ShowUIPanel(frame) — UIParent.lua l.649-751, the left+center+full slice (pin §3 steps - -- 1,3-5, plus the native-center mode decision 0674 needed and the `full` slot the world - -- map took in 0203/0674). Still skipped by scope: `doublewide` (l.707-710 — no such row is - -- registered), which is why the last arm below is a bare Show. + -- ShowUIPanel(frame) — UIParent.lua l.649-751, now the WHOLE slice: left, center, full + -- and doublewide (pin §3 steps 1,3-5, plus the native-center mode decision 0674 needed and + -- the `full` slot the world map took in 0203/0674). `doublewide` (l.706-716) arrived with + -- the auction house, decision 1511 — the last arm below is now genuinely unreachable for a + -- registered row and stays only as the defensive fallback its comment says it is. -- -- Two questions, in order: may it open at all, and where does it sit. function ShowUIPanel(frame, force) @@ -404,10 +448,25 @@ CloseWindows() CloseAllBags() SetCenterFrame(frame, 1) + elseif info.area == "doublewide" then + -- l.706-710: it wants both seats, so it takes both and nothing else is consulted + -- — including its own `pushable`, which is why the auction house's registered 0 + -- never means anything. + SetDoublewideFrame(frame) elseif info.area == "left" then + -- l.712-716: a doublewide incumbent is squatting the seat we are about to use, so + -- it goes first. In the reference this is a bare statement between the branches + -- rather than part of one, but every earlier arm has already returned by then, so + -- the LEFT path is the only one that reaches it — which is why it lives here. And + -- since `HideUIPanel` never clears the slot, this is a plain Hide of a frame that + -- may well already be hidden — see SetDoublewideFrame's note. + local doublewide = GetDoublewideFrame() + if doublewide then + doublewide:Hide() + end SeatLeftAreaPanel(frame, info) else - -- No `doublewide` row is registered — defensive fallback only. + -- An area string this manager does not know: defensive fallback only. frame:Show() end end @@ -464,16 +523,24 @@ -- behaviour on that path, cancel-less Hide and all. UISpecialFrames = { "ItemRefTooltip", "ColorPickerFrame" } - -- CloseWindows(ignoreCenter) — UIParent.lua l.928-957. The doublewide slice stays dropped - -- (we have none); the UISpecialFrames loop at l.947-954 is now carried. + -- CloseWindows(ignoreCenter) — UIParent.lua l.928-957. Both once-dropped slices are now + -- carried: the UISpecialFrames loop at l.947-954, and the doublewide seat (decision 1511). function CloseWindows(ignoreCenter) - local leftFrame = GetLeftFrame() - local centerFrame = GetCenterFrame() local fullScreenFrame = GetFullScreenFrame() - local found = leftFrame or centerFrame or fullScreenFrame + local doublewideFrame = GetDoublewideFrame() + local centerFrame = GetCenterFrame() + local leftFrame = GetLeftFrame() HideUIPanel(leftFrame) HideUIPanel(fullScreenFrame) + -- l.938. HideUIPanel has no doublewide branch, so this lands on its "seated nowhere" + -- arm and is a plain Hide; the seat itself is never vacated (SetDoublewideFrame's note). + HideUIPanel(doublewideFrame) + -- l.934, quoted as-is: `found` deliberately does NOT consider the doublewide frame, + -- even though the line above closes it. Reproduced rather than corrected — ESC's "did + -- I close anything?" answer is this value, and changing it would change which key + -- press opens the game menu. (Computed off the locals read before any Hide landed.) + local found = leftFrame or centerFrame or fullScreenFrame -- `ignoreCenter` spares a NATIVE-center frame and nothing else: a left-area panel -- merely parked at the center seat closes with the rest of them. if centerFrame and not (ignoreCenter and AreaOf(centerFrame) == "center") then diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc index a7621aba..a570ca57 100644 --- a/crates/benilla-app/assets/ui/benilla.toc +++ b/crates/benilla-app/assets/ui/benilla.toc @@ -169,11 +169,12 @@ UIPanelTemplates.xml # (OptionsFrame.xml, far below): these are the shared names addons build panels out of. OptionsFrameTemplates.xml # The reference's money-frame kit: MoneyFrameTemplate/SmallMoneyFrameTemplate + MoneyTypeInfo -# + the MoneyFrame_* API. Needs only Fonts (NumberFontNormal/NumberFontNormalLarge), and -# nothing benilla ships consumes it yet — our own eight coin displays run on MerchantFrame.xml's -# BenillaMoney_* slot kit, and MoneyFrame.xml's header is where the two are reconciled. Seated -# beside the scroll kit (the other shared widget kit) and ahead of every window that shows -# coins, which is where the reference's own TOC puts it too. +# + the MoneyFrame_* API. Needs only Fonts (NumberFontNormal/NumberFontNormalLarge). AuctionFrame +# below is laid out on it (its price columns need a money TYPE, which the other kit has no notion +# of); our other eight coin displays still run on MerchantFrame.xml's BenillaMoney_* slot kit, and +# MoneyFrame.xml's header is where the two are reconciled. Seated beside the scroll kit (the other +# shared widget kit) and ahead of every window that shows coins, which is where the reference's own +# TOC puts it too. MoneyFrame.xml # The shared colour dialog + its opacity slider (`ColorPickerFrame`, `OpacitySliderFrame`, # `ColorPickerOkayButton`, `ColorPickerCancelButton`). Last of the "shared FrameXML surface with no @@ -339,6 +340,17 @@ BankFrame.xml # Loaded right after TrainerFrame per the same "NPC session windows grouped" posture — it # needs only Fonts/UiPanels/GameTooltip (SetTooltipMoney), both already loaded above. TaxiFrame.xml +# The auction house (decision 1511): the three-tab doublewide window over the auction API +# (`benilla-ui/src/script/auction.rs`) + the app's feed/drain. Kept in the "NPC session +# windows grouped" cluster beside Trainer/Bank/Taxi, and loaded AFTER MoneyFrame (its eight +# money displays are SmallMoneyFrameTemplate, and it registers the AUCTION_DEPOSIT money type +# into that file's MoneyTypeInfo), after MerchantFrame (it reuses that file's global +# BenillaMoneyInput_* helpers for its four money inputs), after ScrollTemplates (four faux lists), after +# UIPanelTemplates (UIPanelButtonTemplate / UIPanelCloseButton / InputBoxTemplate / +# UICheckButtonTemplate), after UIDropDownMenu (the rarity capsule) and after UiPanels (the +# panel manager's `doublewide` seat, `CharacterFrameTabButtonTemplate`, the PanelTemplates +# tab kit and the StaticPopup engine its two confirmations register into) — all above. +AuctionFrame.xml # The crafting window (decision 0437 phase 2): NOT an NPC session (opens off your own cast of # an effect-47 opener spell, TradeSkillFrame.xml's own header comment) — loaded right after # TrainerFrame per the same "player-state/profession window" grouping and because it depends diff --git a/crates/benilla-app/src/camera_shake.rs b/crates/benilla-app/src/camera_shake.rs new file mode 100644 index 00000000..15c83dc6 --- /dev/null +++ b/crates/benilla-app/src/camera_shake.rs @@ -0,0 +1,573 @@ +//! **Camera shake** — the thump a heavy creature's footfall puts through the camera, and the +//! one-off jolt as its body lands (B298; decision 1540). +//! +//! Not an animation event. `$SHK` exists in the M2 vocabulary but **`CGUnit_C::HandleAnimEvent` +//! does not decode it** — all 73 tags it dispatches were enumerated and `$SHK` is not among them; +//! the only two handlers that decode it hang off the GameObject (typemask `0x20`) and +//! DynamicObject (`0x40`) trampolines. A `$SHK` authored on a creature M2 logs +//! `UNHANDLEDANIMEVENT` and does nothing, so no creature shake can be `$SHK`-driven — and in fact +//! neither Ancient authors one. +//! +//! The real chain is **data, not animation**: +//! +//! - **`CreatureModelData.FootstepShakeSize`** (field 11) → a `CameraShakes.dbc` row id, fired from +//! the **visual** footfall channel — the per-foot plant tags (`$FL0`/`$FR0`/`$RL0`/`$RR0`…), the +//! same stream [`crate::footprints`] reads. **Not `$FSD`**, which is the sound handler's alone. +//! - **`CreatureModelData.DeathThudShakeSize`** (field 12) → the same table, fired on `$DTH`. +//! +//! Only 25 of the 430 shipped models carry a footstep shake, and the set is exactly the +//! thumping-giant list; 49 carry one of the two. See `benilla-extract shakecensus`. +//! +//! ## The law (wow-re `ui/scratch/camera-shake-law.md`, §5-verified) +//! +//! **Emission — the footstep**, gated in this order (`0x5fbf70`): not hovering · `BYTES_1` byte 3 +//! bit `0x2` clear (our stealth bit) · not a player-ghost · **`|camera − footplant|² ≤ 2500` +//! (50 yd)**. The shake sits *outside* the `showfootprints` CVar gate and *outside* the 25 yd gate +//! that follows it, so it reaches twice as far as a footprint decal and does not turn off with +//! them. The position is the planted foot's world point, exactly as the decal derives it. +//! +//! **Emission — the death thud** (`0x625c30`): **no gates at all** — no hover, no ghost, no +//! distance, no CVar — at the unit's own world position. +//! +//! **Per live record, per frame** (`0x511760`/`0x5116e0`): +//! +//! ```text +//! t = (now − start) + phase // seconds; phase is a TIME pre-roll, not an angle +//! if !(t < duration) → retire +//! A = amplitude / 36 // the DBC column is inches; yards on the wire out +//! d² = |eye − pos|² // pos is snapshotted at spawn +//! if d² > 6400 → contribute nothing (the record survives) +//! if d² > 81 → A *= 0.7^((√d² − 9) / 9) +//! a = A · sin(2π · frequency · t) +//! if shake_type == 1 → a *= exp(−coefficient · t) +//! ``` +//! +//! Three corrections to the conventional column map, all from the consumer: **`Phase` is a time +//! pre-roll in seconds**, not an angle — it advances the sine *and* the decay, and shortens the +//! real life to `duration − phase`; **`Duration` is a hard cutoff with no taper**, so a +//! `shake_type == 0` row is cut off mid-swing at full strength; and **`ShakeType` is a one-bit +//! decay switch**, not a type — `== 1` exactly enables the `exp` envelope, and `Coefficient` is +//! that rate in 1/s and is dead data on every `shake_type == 0` row. The decay is base **e**. +//! +//! **What moves.** A pure world-space **translation of the eye** — no rotation, no FOV change; the +//! look-at target is rebuilt as `eye + forward`, so it rides along. `Direction` selects an axis in +//! the **followed unit's body frame**, re-read every frame, which is why turning the player +//! rotates an in-flight horizontal shake: +//! +//! | `direction` | reference (WoW axes) | ours (Bevy) | +//! |---|---|---| +//! | 0 | `(a·cos φ, a·sin φ, 0)` | `a ·` forward | +//! | 1 | `(−a·sin φ, a·cos φ, 0)` | `a ·` left | +//! | 2 | `(0, 0, a)` | `a ·` up | +//! +//! (`bevy = (−wow.y, wow.z, −wow.x)` — decision 0002 — and our yaw about `+Y` equals the WoW +//! facing, so axis 0/1 land exactly on the unit's forward/left.) **Every creature row is +//! `direction = 2`**, so in practice a footstep shake is purely vertical; the other two axes are +//! reachable only from the spell-side table and are implemented for completeness. Axes 0 and 1 are +//! *not* independent — both sum into the horizontal plane. +//! +//! **Combination.** Three slots, one per axis, each keeping the single strongest live shake — same-axis +//! shakes do **not** sum, the losers are dropped outright, and at most three shakes play in a frame. +//! Ties keep the incumbent, and the walk is oldest→newest, so ties keep the **older** record. +//! +//! **Lifetime.** A record holds a *snapshot* position and no reference to its source, so a shake +//! fully outlives the creature that spawned it. The whole evaluation is skipped — offset zero, +//! nothing expiring — while the followed unit is **swimming**. +//! +//! Not modelled, deliberately: the reference's second wholesale free on `SetTarget(nullptr)`, and +//! its destructor's unlink-without-free (a shipped leak — our `Vec` cannot reproduce it). The +//! flying-spline (taxi) arm of the skip is noted in [`CameraShakes::evaluate`]. + +use std::f32::consts::TAU; + +use bevy::prelude::*; + +use benilla_formats::{CameraShake, CameraShakeCatalog}; + +use crate::creature_anim::{footfall_side, move_flags, AnimSoundEvent, MovementState}; +use crate::entities::{BoneAttach, Creatures}; +use crate::net::{Embodied, NetEntity, ObjectStore}; +use benilla_assets::{AssetSet, LockRecover, WorldAssets}; +use benilla_world::schedule::WorldStage; +use benilla_world::view::WorldCamera; + +/// Beyond this the record contributes nothing but is **not** retired (`0x5116e9`, `80 yd²`). +const CULL_DISTANCE_SQ: f32 = 6400.0; +/// Inside this the shake plays at full authored strength (`9 yd²`). +const FULL_DISTANCE_SQ: f32 = 81.0; +/// The falloff's half-life base and its per-9-yd exponent divisor. +const FALLOFF_BASE: f32 = 0.7; +const FALLOFF_SPAN: f32 = 9.0; +/// The footstep emitter's own radius, camera→footplant (`0x5fc00a`, `2500 yd²` = 50 yd). **The +/// camera, not the player** — the same correction wow-re applied to `dist2-gate.md` this round. +const EMIT_DISTANCE_SQ: f32 = 2500.0; +/// `CameraShakes.Amplitude` is authored in inches; the client scales it at spawn (`0x511d78`). +const INCHES_TO_YARDS: f32 = 1.0 / 36.0; + +/// One live shake: the authored row, where it happened, and when it started. +struct LiveShake { + row: CameraShake, + /// World position, **snapshotted at spawn** — the record never looks at its source again. + pos: Vec3, + /// App-clock seconds at spawn. + start: f32, +} + +/// The live shake set and the eye offset it composes for this frame. +#[derive(Resource, Default)] +pub(crate) struct CameraShakes { + live: Vec, +} + +impl CameraShakes { + /// Enqueue a shake at a world point. The row is copied in whole — the DBC is the only source + /// of shape, and nothing downstream re-reads the catalog. + fn add(&mut self, row: CameraShake, pos: Vec3, now: f32) { + self.live.push(LiveShake { + row, + pos, + start: now, + }); + } + + /// Retire what has expired and compose this frame's offset. + /// + /// `swimming` is the reference's skip (`0x50ea87`/`0x50ea8b`): while the followed unit swims, + /// the whole block is bypassed — the offset is zero **and nothing expires**, so a shake + /// resumes mid-flight on surfacing. The sibling arm of that skip, a flying (taxi) spline, is + /// not modelled here: we have no flying-spline predicate at the camera seat, and the case is a + /// heavy creature's footfall within 50 yd of a taxi path. Recorded rather than guessed. + fn evaluate(&mut self, eye: Vec3, facing_yaw: f32, now: f32, swimming: bool) -> Vec3 { + if swimming { + return Vec3::ZERO; + } + // Retire on the *unattenuated* clock: distance decides contribution, never lifetime. + self.live.retain(|s| s.elapsed(now) < s.row.duration); + + // One slot per axis, each holding (compare key, signed value). The walk is oldest→newest + // and the compare is strict, so a tie keeps the older record — the reference's own + // `jne`-skips-the-write shape. + let mut slots: [Option<(f32, f32)>; 3] = [None; 3]; + for s in &self.live { + let Some((key, value)) = s.sample(eye, now) else { + continue; + }; + let axis = s.row.direction as usize; + let Some(slot) = slots.get_mut(axis) else { + continue; // direction ≥ 3 writes nothing (and corrupts the reference's frame) + }; + if slot.is_none_or(|(best, _)| key > best) { + *slot = Some((key, value)); + } + } + + // Axis 0/1 are the followed unit's forward/left, 2 is world up (module doc's table). Our + // yaw about +Y is the WoW facing, so forward is exactly `(−sin, 0, −cos)`. + let (sin, cos) = facing_yaw.sin_cos(); + let forward = Vec3::new(-sin, 0.0, -cos); + let left = Vec3::new(-cos, 0.0, sin); + let value = |i: usize| slots[i].map_or(0.0, |(_, v)| v); + forward * value(0) + left * value(1) + Vec3::Y * value(2) + } +} + +impl LiveShake { + /// Seconds into the shake, including `phase` — which is a **time pre-roll**, so it advances + /// both the sine and the decay and shortens the real life to `duration − phase`. + fn elapsed(&self, now: f32) -> f32 { + (now - self.start) + self.row.phase + } + + /// This record's `(compare key, signed offset)` at `eye`, or `None` when out of cull range. + /// + /// The compare key is the **distance-attenuated amplitude** — a per-record quantity that does + /// not swing with the sine — per wow-re's reading of the accumulator's stored slot. + fn sample(&self, eye: Vec3, now: f32) -> Option<(f32, f32)> { + let d2 = eye.distance_squared(self.pos); + if d2 > CULL_DISTANCE_SQ { + return None; + } + let mut amp = self.row.amplitude * INCHES_TO_YARDS; + if d2 > FULL_DISTANCE_SQ { + amp *= FALLOFF_BASE.powf((d2.sqrt() - FALLOFF_SPAN) / FALLOFF_SPAN); + } + let t = self.elapsed(now); + let mut value = amp * (TAU * self.row.frequency * t).sin(); + // `shake_type == 1` exactly — the one-bit decay switch, base e, rate in 1/s. + if self.row.shake_type == 1 { + value *= (-self.row.coefficient * t).exp(); + } + Some((amp, value)) + } +} + +pub(crate) struct CameraShakePlugin; + +impl Plugin for CameraShakePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(Startup, load_shakes.after(AssetSet::Open)) + // Present, like the footprints and the footstep sounds: the same event stream, after + // the frame's animation drive and transforms have settled the foot bones. + // + // Capture-gated in step with the applier ([`crate::player`] schedules that one). The + // pairing is not cosmetic: the applier is what retires expired records, so an emitter + // still running while it is gated off would push a record per footfall that nothing + // ever reaps — a slow leak across a long capture. + .add_systems( + Update, + fire_shakes + .in_set(WorldStage::Present) + .run_if(not(resource_exists::)), + ); + } +} + +/// Read `CameraShakes.dbc` into its catalog resource. +fn load_shakes(mut commands: Commands, assets: Option>) { + let Some(assets) = assets else { return }; + let table = { + let mut chain = assets.chain.lock_recover(); + benilla_formats::load_camera_shakes(&mut chain) + }; + match table { + Ok(t) => { + debug!("camera_shake: {} presets", t.len()); + commands.insert_resource(Shakes(t)); + } + Err(e) => warn!("camera_shake: CameraShakes.dbc failed to load: {e:#}"), + } +} + +/// `CameraShakes.dbc`, loaded once. +#[derive(Resource)] +struct Shakes(CameraShakeCatalog); + +/// Enqueue a shake for each qualifying footfall and death thud on the frame's event stream. +#[allow(clippy::too_many_arguments)] +fn fire_shakes( + mut events: MessageReader, + time: Res