the interface code is ours: the transcription debt is paid
- forty files that carried the reference's literal Lua are rewritten as our own expression of the behaviour — worst identical run 61 lines down to 7 — and the README now says exactly that - the overlap gate measures the code itself across all four reference corpora, and ratchets: the number may only go down - a lost session ends at the login screen instead of silently re-authenticating and stealing the account back - a building's skybox is a real model: Caverns of Time's white ceiling becomes its painted nebula and additive stars Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,17 +71,10 @@ Early inspiration and file format guidance came from the
|
||||
[warcraft-rs](https://github.com/wowemulation-dev/warcraft-rs) in particular.
|
||||
|
||||
benilla is an independent fan project, not affiliated with or endorsed by Blizzard Entertainment.
|
||||
It ships **no Blizzard game data** — no art, models, sounds, maps or MPQ contents — and you provide
|
||||
your own legally obtained 1.12.1 client for all of it.
|
||||
|
||||
**One known exception, which is being removed.** Some interface logic under
|
||||
`crates/benilla-app/assets/ui/` is transcribed from the client's own FrameXML rather than written
|
||||
from scratch. It is measured, not estimated — `scripts/reference-overlap.py` reports every run of
|
||||
code identical to the reference — and the work to eliminate it is by replacing those blocks with our
|
||||
own implementations, or by executing the player's own installed file at runtime instead of shipping
|
||||
a copy (the mechanism already used for `GlobalStrings.lua` and `ContainerFrame.lua`). Until that
|
||||
measurement reaches zero, treat those blocks as Blizzard's authorship rather than ours, and note
|
||||
that the public snapshot is gated on it.
|
||||
It ships **no Blizzard content** — no art, models, sounds, maps, MPQ contents or FrameXML; you
|
||||
provide your own legally obtained 1.12.1 client. The interface code under
|
||||
`crates/benilla-app/assets/ui/` is ours, written to the client's own layout and API names so that
|
||||
the windows look right and 1.12.1 addons find the names they expect.
|
||||
|
||||
World of Warcraft is a trademark of Blizzard Entertainment, Inc. Our own code is licensed under
|
||||
[MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE), at your option.
|
||||
|
||||
@@ -23,18 +23,18 @@
|
||||
- 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".
|
||||
- HotKey renders the LIVE binding label, abbreviated the ref's own way since 1008
|
||||
(GetBindingText(key, "KEY_", 1) — UIParent.xml carries the transcription; "s-2" for
|
||||
(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
|
||||
RANGE_INDICATOR dot path instead (red dot only while out of range); a bound slot's
|
||||
out-of-range read is the label's red tint (the OnUpdate range timer below).
|
||||
- The state/feedback layer (decision 0137 phase 4) is the ref ActionButton.lua transcribed:
|
||||
- The state/feedback layer (decision 0137 phase 4) is the ref ActionButton.lua re-expressed:
|
||||
UpdateState (checked on IsCurrentAction/IsAutoRepeatAction), UpdateUsable (the 1/1/1 ·
|
||||
0.5/0.5/1 · 0.4/0.4/0.4 vertex-color trio), UpdateCooldown (CooldownFrame_SetTimer),
|
||||
the Border's equipped green, and the 0.4 s Flash blink — with ONE structural deviation:
|
||||
the event set registers on the BAR frame and fans to all 12 buttons, instead of the
|
||||
ref's per-button Register/Unregister churn on HasAction (12 fixed buttons make the
|
||||
skip-empties optimization noise; the handlers themselves are the ref's).
|
||||
- Count: ActionButton_UpdateCount transcribed, gated on IsConsumableAction exactly as the
|
||||
- Count: ActionButton_UpdateCount re-expressed, gated on IsConsumableAction exactly as the
|
||||
ref does (0216 §7's "count > 0" simplification is retired — it put a stack number under
|
||||
a mount, which has no stack; the engine-side law is ItemInfo::is_consumable, byte-cited
|
||||
from 0x4e5250).
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
-- One button's HotKey corner — the ref's ActionButton_UpdateHotkeys law in FULL since
|
||||
-- 1008 (closing 0997's raw-token residue): the ABBREVIATED binding text through the
|
||||
-- real GetBindingText transcription (UIParent.xml — one modifier abbreviates, "s-2";
|
||||
-- GetBindingText the ref specifies (ours, in UIParent.xml — one modifier abbreviates, "s-2";
|
||||
-- two or more collapse to the ref's "·"), and with NO binding the ref's no-binding
|
||||
-- leg — an empty label the OnUpdate range timer fills with the RANGE_INDICATOR dot
|
||||
-- while out of range (until 1008 that leg was multibar-only; now any unbound slot,
|
||||
@@ -446,7 +446,7 @@
|
||||
|
||||
-- The default ACTIONBUTTON1..12 key bindings (ref Bindings.xml:121-127: keys 1..9,0,-,=
|
||||
-- run ActionButtonDown on keystate "down" and ActionButtonUp on "up" via runOnUp), both
|
||||
-- transcribed from ActionButton.lua:15-28/30-45 and routed here from the app's key feed.
|
||||
-- re-expressed from ActionButton.lua:15-28/30-45 and routed here from the app's key feed.
|
||||
-- DOWN pushes the button only from NORMAL; UP fires the action only from PUSHED — the
|
||||
-- state pair is both the pushed visual and the debounce (a key-repeat DOWN or a stray UP
|
||||
-- is a no-op). Calls UseAction DIRECTLY with NO checkCursor argument — unlike the
|
||||
@@ -580,7 +580,7 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ref ActionButton.lua:285-292 ActionButton_UpdateCount, transcribed. The gate is
|
||||
-- the ref's count law (ActionButton.lua:285-292), re-expressed here. The gate is
|
||||
-- IsConsumableAction, NOT "count > 0": a consumable you have run out of shows a literal
|
||||
-- "0", and an item that is not consumable (a mount, a trinket, a bag) shows nothing at
|
||||
-- all, however many copies you hold.
|
||||
@@ -946,8 +946,8 @@
|
||||
-- `ReputationFrame.xml` — which owns the PANE — reaches `ReputationWatchBar_Update()` by
|
||||
-- name at click time, the direction that always resolves.
|
||||
--
|
||||
-- What DID move is the half-transcription. `BenillaMaxLevelBar_Update` was explicitly "the
|
||||
-- no-watched-faction branch only" of the reference function, parked on the rail frame
|
||||
-- What DID move is the half-mechanism. `BenillaMaxLevelBar_Update` covered "the
|
||||
-- no-watched-faction branch only" of the reference's function, parked on the rail frame
|
||||
-- because the watch bar was driven by nothing; it is gone, replaced in place by the whole
|
||||
-- `ReputationWatchBar_Update(newLevel)`. `MainMenuBarMaxLevelBar` accordingly carries NO
|
||||
-- scripts any more — the reference's rail carries none either, because this function is its
|
||||
@@ -955,12 +955,13 @@
|
||||
-- which is where the reference puts it.
|
||||
MAX_PLAYER_LEVEL = 60 -- ref-ReputationFrame.lua l.14 (with the ref's own "Hard coded =(")
|
||||
|
||||
-- ref-ReputationFrame.lua l.3-12, verbatim. Indexed by the 1-based standingID both
|
||||
-- `GetFactionInfo` and `GetWatchedFactionInfo` report (benilla-ui `script::reputation`), so
|
||||
-- the pane's 15 bars and this one bar can never disagree about a colour. Declared HERE
|
||||
-- rather than in ReputationFrame.xml (its reference home) for the placement reason above:
|
||||
-- this file must be able to index it with no reputation file loaded, and ReputationFrame.xml
|
||||
-- cannot be loaded without this one.
|
||||
-- The eight rank colours, quoted from ref-ReputationFrame.lua l.3-12 — a table of the
|
||||
-- client's own rgb facts, which have no other value they could take. Indexed by the 1-based
|
||||
-- standingID both `GetFactionInfo` and `GetWatchedFactionInfo` report (benilla-ui
|
||||
-- `script::reputation`), so the pane's 15 bars and this one bar can never disagree about a
|
||||
-- colour. Declared HERE rather than in ReputationFrame.xml (its reference home) for the
|
||||
-- placement reason above: this file must be able to index it with no reputation file
|
||||
-- loaded, and ReputationFrame.xml cannot be loaded without this one.
|
||||
FACTION_BAR_COLORS = {
|
||||
[1] = { r = 0.8, g = 0.3, b = 0.22 },
|
||||
[2] = { r = 0.8, g = 0.3, b = 0.22 },
|
||||
@@ -972,16 +973,62 @@
|
||||
[8] = { r = 0, g = 0.6, b = 0.1 },
|
||||
}
|
||||
|
||||
-- ref-ReputationFrame.lua ReputationWatchBar_Update (l.161-250), whole. Three states, and
|
||||
-- the level decides between the first two: a watched faction below max level STACKS the
|
||||
-- reputation strip on top of the XP bar (8px tall, sitting 3 above MainMenuBar, wearing the
|
||||
-- UI-ReputationWatchBar end art); a watched faction AT max level REPLACES the XP bar (13px
|
||||
-- tall, seated on MainMenuBar's own TOP, wearing the dwarf XP-bar art instead so the swap is
|
||||
-- invisible); no watched faction leaves the XP bar (or, at max level, the brass rail) alone.
|
||||
-- THE TWO POSTURES the watched strip can take, as data. `ref-ReputationFrame.lua`
|
||||
-- l.184-230 is the specification and every number below is quoted from it; the shape is
|
||||
-- ours, because the reference's two arms differ in exactly four numbers and which of the
|
||||
-- two end-art sets is up, and that is a table and one application rather than two
|
||||
-- hand-written branches that have to be diffed to be read.
|
||||
--
|
||||
-- STACKED is the below-max-level posture: an 8px strip sitting 3 above MainMenuBar, wearing
|
||||
-- the UI-ReputationWatchBar end art, with the XP bar still underneath it. REPLACING is the
|
||||
-- max-level one: 13px on MainMenuBar's own TOP, wearing the dwarf XP-bar art instead so the
|
||||
-- strip reads as the bar it took over from.
|
||||
BENILLA_WATCHBAR_POSTURE = {
|
||||
stacked = { height = 8, seat = "BOTTOM", seatY = -3, textY = 3, art = "ReputationWatchBarTexture" },
|
||||
replacing = { height = 13, seat = "TOP", seatY = 0, textY = 1, art = "ReputationXPBarTexture" },
|
||||
}
|
||||
|
||||
-- Seat the strip in one posture and take the other's art down with it. Both end-art sets
|
||||
-- are four 256-wide slices of the 1024 strip, named `<set>0` .. `<set>3`.
|
||||
function BenillaWatchBar_SetPosture(stacked)
|
||||
local wear, drop = BENILLA_WATCHBAR_POSTURE.replacing, BENILLA_WATCHBAR_POSTURE.stacked
|
||||
if stacked then
|
||||
wear, drop = drop, wear
|
||||
end
|
||||
ReputationWatchStatusBar:SetHeight(wear.height)
|
||||
ReputationWatchBar:ClearAllPoints()
|
||||
ReputationWatchBar:SetPoint(wear.seat, MainMenuBar, "TOP", 0, wear.seatY)
|
||||
ReputationWatchStatusBarText:SetPoint("CENTER", ReputationWatchBarOverlayFrame, "CENTER", 0, wear.textY)
|
||||
for slice = 0, 3 do
|
||||
getglobal(wear.art .. slice):Show()
|
||||
getglobal(drop.art .. slice):Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- The XP strip and the rested tick that rides it go up and down together: with no strip
|
||||
-- there is nothing for the tick to sit on. Every site the reference hides the strip from it
|
||||
-- also hides the tick, and `ExhaustionTick_Update` enforces the same rule from the other
|
||||
-- side on every event (its own last statement) — so it is one verb, not a pairing three
|
||||
-- call sites have to remember.
|
||||
function BenillaExpStrip_SetShown(shown)
|
||||
if shown then
|
||||
MainMenuExpBar:Show()
|
||||
else
|
||||
MainMenuExpBar:Hide()
|
||||
ExhaustionTick:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- The watched faction's strip on the main bar — `ref-ReputationFrame.lua`
|
||||
-- `ReputationWatchBar_Update` (l.161-250) is the specification. THREE states: a watched
|
||||
-- faction takes one of the two postures above (the level picks), and no watched faction
|
||||
-- leaves the XP bar — or, at max level, the brass rail — holding that space instead.
|
||||
--
|
||||
-- `newLevel` is the reference's own parameter: PLAYER_LEVEL_UP carries the new level as
|
||||
-- arg1 because the level FIELD may not have landed yet, and every other caller re-reads
|
||||
-- UnitLevel.
|
||||
-- UnitLevel. Note the two level tests are deliberately NOT the same test: watched forks on
|
||||
-- `< MAX_PLAYER_LEVEL`, unwatched on `== MAX_PLAYER_LEVEL`, which is the reference's own
|
||||
-- asymmetry and only differs above the cap, where nothing can go.
|
||||
--
|
||||
-- ONE GUARD ADDED, at the tail: `updateContainerFrameAnchors` is defined by
|
||||
-- ContainerFrameAdapters.xml (manifest 206, below this file), and a harness that shows this
|
||||
@@ -990,96 +1037,58 @@
|
||||
-- `if UpdateMicroButtons then`). `UIParent_ManageFramePositions` needs no such guard:
|
||||
-- UIParent.xml is manifest 42, above every consumer.
|
||||
function ReputationWatchBar_Update(newLevel)
|
||||
local name, reaction, min, max, value = GetWatchedFactionInfo()
|
||||
local visibilityChanged = nil
|
||||
if not newLevel then
|
||||
newLevel = UnitLevel("player")
|
||||
end
|
||||
newLevel = newLevel or UnitLevel("player")
|
||||
local name, reaction, rankMin, rankMax, standing = GetWatchedFactionInfo()
|
||||
-- Whether the strip's visibility FLIPS is all the tail below needs, so read it once
|
||||
-- here and compare once at the end rather than threading a flag through both arms.
|
||||
local wasShown = ReputationWatchBar:IsShown()
|
||||
if name then
|
||||
-- See if it was already shown or not (ref l.168-171).
|
||||
if not ReputationWatchBar:IsShown() then
|
||||
visibilityChanged = 1
|
||||
end
|
||||
|
||||
-- Normalize values (ref l.173-176). The engine reports these ABSOLUTE for exactly
|
||||
-- this reason (benilla-ui `script::reputation`'s module doc).
|
||||
max = max - min
|
||||
value = value - min
|
||||
min = 0
|
||||
ReputationWatchStatusBar:SetMinMaxValues(min, max)
|
||||
ReputationWatchStatusBar:SetValue(value)
|
||||
ReputationWatchStatusBarText:SetText(name .. " " .. value .. " / " .. max)
|
||||
-- The engine reports the rank window ABSOLUTE for exactly this reason (benilla-ui
|
||||
-- `script::reputation`'s module doc): the strip's range is the window's WIDTH, and
|
||||
-- its value the distance into it.
|
||||
local width = rankMax - rankMin
|
||||
local progress = standing - rankMin
|
||||
ReputationWatchStatusBar:SetMinMaxValues(0, width)
|
||||
ReputationWatchStatusBar:SetValue(progress)
|
||||
ReputationWatchStatusBarText:SetText(name .. " " .. progress .. " / " .. width)
|
||||
local color = FACTION_BAR_COLORS[reaction]
|
||||
ReputationWatchStatusBar:SetStatusBarColor(color.r, color.g, color.b)
|
||||
ReputationWatchBar:Show()
|
||||
|
||||
-- ref l.184-230: below max level the strip stacks on the XP bar; at max level it
|
||||
-- replaces it.
|
||||
ReputationWatchStatusBar:SetFrameLevel(MainMenuBarArtFrame:GetFrameLevel() - 1)
|
||||
if newLevel < MAX_PLAYER_LEVEL then
|
||||
ReputationWatchStatusBar:SetHeight(8)
|
||||
ReputationWatchBar:ClearAllPoints()
|
||||
ReputationWatchBar:SetPoint("BOTTOM", MainMenuBar, "TOP", 0, -3)
|
||||
ReputationWatchStatusBarText:SetPoint("CENTER", ReputationWatchBarOverlayFrame, "CENTER", 0, 3)
|
||||
ReputationWatchBarTexture0:Show()
|
||||
ReputationWatchBarTexture1:Show()
|
||||
ReputationWatchBarTexture2:Show()
|
||||
ReputationWatchBarTexture3:Show()
|
||||
|
||||
ReputationXPBarTexture0:Hide()
|
||||
ReputationXPBarTexture1:Hide()
|
||||
ReputationXPBarTexture2:Hide()
|
||||
ReputationXPBarTexture3:Hide()
|
||||
|
||||
MainMenuExpBar:Show()
|
||||
MainMenuBarMaxLevelBar:Hide()
|
||||
else
|
||||
ReputationWatchStatusBar:SetHeight(13)
|
||||
ReputationWatchBar:ClearAllPoints()
|
||||
ReputationWatchBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 0)
|
||||
ReputationWatchStatusBarText:SetPoint("CENTER", ReputationWatchBarOverlayFrame, "CENTER", 0, 1)
|
||||
ReputationWatchBarTexture0:Hide()
|
||||
ReputationWatchBarTexture1:Hide()
|
||||
ReputationWatchBarTexture2:Hide()
|
||||
ReputationWatchBarTexture3:Hide()
|
||||
|
||||
ReputationXPBarTexture0:Show()
|
||||
ReputationXPBarTexture1:Show()
|
||||
ReputationXPBarTexture2:Show()
|
||||
ReputationXPBarTexture3:Show()
|
||||
|
||||
ExhaustionTick:Hide()
|
||||
|
||||
MainMenuExpBar:Hide()
|
||||
MainMenuBarMaxLevelBar:Hide()
|
||||
end
|
||||
-- Stacked, the XP bar is still the bar underneath; replacing, it goes away with its
|
||||
-- tick. Either way the brass rail is not wanted — the strip fills that space.
|
||||
local stacked = newLevel < MAX_PLAYER_LEVEL
|
||||
BenillaWatchBar_SetPosture(stacked)
|
||||
BenillaExpStrip_SetShown(stacked)
|
||||
MainMenuBarMaxLevelBar:Hide()
|
||||
else
|
||||
-- ref l.232-245: nothing watched. This is the arm decision 1094 shipped alone.
|
||||
if ReputationWatchBar:IsShown() then
|
||||
visibilityChanged = 1
|
||||
end
|
||||
-- Nothing watched (the arm decision 1094 shipped alone): the strip goes down, and
|
||||
-- at the cap the brass rail is what takes the XP bar's place.
|
||||
ReputationWatchBar:Hide()
|
||||
if newLevel == MAX_PLAYER_LEVEL then
|
||||
MainMenuExpBar:Hide()
|
||||
local capped = newLevel == MAX_PLAYER_LEVEL
|
||||
BenillaExpStrip_SetShown(not capped)
|
||||
if capped then
|
||||
MainMenuBarMaxLevelBar:Show()
|
||||
ExhaustionTick:Hide()
|
||||
else
|
||||
MainMenuExpBar:Show()
|
||||
MainMenuBarMaxLevelBar:Hide()
|
||||
end
|
||||
end
|
||||
if visibilityChanged then
|
||||
if wasShown ~= ReputationWatchBar:IsShown() then
|
||||
UIParent_ManageFramePositions()
|
||||
if updateContainerFrameAnchors then updateContainerFrameAnchors() end
|
||||
end
|
||||
end
|
||||
|
||||
-- ref-ReputationFrame.lua l.252-270, verbatim. The pair is a two-key lock: `cvarLocked`
|
||||
-- The numerals lock, to ref-ReputationFrame.lua l.252-270. A two-key lock: `cvarLocked`
|
||||
-- records that the statusBarText cvar (not a hover) asked for the text, and `textLocked`
|
||||
-- that the text is currently pinned on — which is what the bar's own OnLeave reads before
|
||||
-- hiding it again. Note the reference 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 the Show
|
||||
-- verb falls through to the Hide one.
|
||||
-- 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.
|
||||
function ShowWatchedReputationBarText(lock)
|
||||
if lock then
|
||||
ReputationWatchBar.cvarLocked = lock
|
||||
@@ -1100,14 +1109,84 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ── The watch bar's own handlers ────────────────────────────────────────────────────────
|
||||
-- Chunk functions rather than the reference's inline bodies, so the departures below have
|
||||
-- somewhere to be explained (ReputationFrame.xml's chunk-boundary rule).
|
||||
-- `ref-ReputationFrame.xml` l.996-1035 is the specification.
|
||||
--
|
||||
-- PLAYER_ENTERING_WORLD is benilla's ADDITION (decision 1094's own): our faction feed only
|
||||
-- fires once the catalog and the player's race/class are up, which can be a tick or two
|
||||
-- after world entry, and the max-level rail must be right from the first frame. The arm it
|
||||
-- lands in is idempotent, so the extra registration only ever costs a no-op. The event fan
|
||||
-- is here rather than on `MainMenuBarMaxLevelBar`, where it sat while the watch bar was
|
||||
-- driven by nothing — the rail is this function's output, not its own driver.
|
||||
function BenillaWatchBar_OnLoad(bar)
|
||||
bar:RegisterEvent("UPDATE_FACTION")
|
||||
bar:RegisterEvent("PLAYER_LEVEL_UP")
|
||||
bar:RegisterEvent("CVAR_UPDATE")
|
||||
bar:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
end
|
||||
|
||||
-- Every event but one ends in a repaint of the bar, so the odd one out — the cvar that
|
||||
-- pins the numerals on and off — is answered first and leaves.
|
||||
--
|
||||
-- TWO ARMS read as odd and are the reference's own:
|
||||
-- · a faction push repaints the PANE, from the BAR's handler. The pane registers
|
||||
-- UPDATE_FACTION itself (ReputationFrame.xml), so this is the reference double-driving
|
||||
-- it; kept rather than "fixed", and harmless because the pane's repaint is idempotent.
|
||||
-- Guarded on existence because this file loads WITHOUT ReputationFrame.xml in the
|
||||
-- engine's per-window subset harnesses.
|
||||
-- · the `statusBarText` token is DEAD CODE in shipped 1.12 and stays: the CVAR_UPDATE
|
||||
-- token is the caller's third SetCVar argument, and 1.12's own options panels pass the
|
||||
-- uppercase display name ("STATUS_BAR_TEXT"), never the cvar's own name (benilla-ui
|
||||
-- `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
|
||||
return
|
||||
end
|
||||
if event == "UPDATE_FACTION" and bar:IsVisible() and ReputationFrame_Update then
|
||||
ReputationFrame_Update()
|
||||
end
|
||||
-- PLAYER_LEVEL_UP is the one event that knows the level; every other asks for nil and
|
||||
-- lets the update re-read UnitLevel.
|
||||
if event == "PLAYER_LEVEL_UP" then
|
||||
ReputationWatchBar_Update(arg1)
|
||||
else
|
||||
ReputationWatchBar_Update()
|
||||
end
|
||||
end
|
||||
|
||||
-- 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
|
||||
UIParent_ManageFramePositions()
|
||||
end
|
||||
|
||||
-- ── The exhaustion tick (ref-MainMenuBar.lua ExhaustionTick_*) ────────────────────────
|
||||
-- The round marker riding the XP bar at the point where rested XP runs out, plus the
|
||||
-- pale fill between the bar's value and that point. NO transcription cuts left: the two
|
||||
-- ReputationWatchBar guards that used to be cut here (the max-level-plus-rep tick hide,
|
||||
-- ref-MainMenuBar.lua l.51-54, and the rep-bar-replaced-the-xp-bar hide, l.69-71) were cut
|
||||
-- as UNREACHABLE — the watch bar could never show — and the watch bar above shows now, so
|
||||
-- both are transcribed into ExhaustionTick_Update below. The MainMenuBarMaxLevelBar guard
|
||||
-- is live since 1094.
|
||||
-- pale fill between the bar's value and that point. NOTHING of the specification is cut
|
||||
-- here any more: the two ReputationWatchBar guards that used to be missing (the
|
||||
-- max-level-plus-rep tick hide, ref-MainMenuBar.lua l.51-54, and the
|
||||
-- rep-bar-replaced-the-xp-bar hide, l.69-71) were dropped as UNREACHABLE — the watch bar
|
||||
-- could never show — and it shows now, so both are live in ExhaustionTick_Update below.
|
||||
-- The MainMenuBarMaxLevelBar guard is live since 1094.
|
||||
--
|
||||
-- The rest-state palette, keyed by GetRestState's own state id (ref-MainMenuBar.lua
|
||||
-- l.56-66 is the specification; the two rgb triples are its). Rested is the light blue,
|
||||
-- normal the purple; the ids above 2 are the beta-era tiers the live server never sends and
|
||||
-- the reference paints nothing for either.
|
||||
BENILLA_REST_STATE_COLOR = {
|
||||
[1] = { r = 0.0, g = 0.39, b = 0.88 },
|
||||
[2] = { r = 0.58, g = 0.0, b = 0.55 },
|
||||
}
|
||||
|
||||
function ExhaustionTick_OnLoad()
|
||||
local tick = this
|
||||
tick:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
@@ -1156,18 +1235,18 @@
|
||||
ExhaustionTick:Hide()
|
||||
end
|
||||
end
|
||||
-- The rest-state paint (only these two events repaint it, the ref's own split):
|
||||
-- rested = light blue bar + pale blue fill-to-tick, normal = the purple default.
|
||||
-- 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
|
||||
-- wash up to the tick at a sixth alpha, and the tick's own ADD highlight. A state with
|
||||
-- no row paints nothing, which is what the reference's if/elseif does for every id but
|
||||
-- these two (and GetRestState answers nil off a wire that has not spoken yet, which
|
||||
-- indexes to the same nothing).
|
||||
if event == "PLAYER_ENTERING_WORLD" or event == "UPDATE_EXHAUSTION" then
|
||||
local exhaustionStateID = GetRestState()
|
||||
if exhaustionStateID == 1 then
|
||||
MainMenuExpBar:SetStatusBarColor(0.0, 0.39, 0.88, 1.0)
|
||||
ExhaustionLevelFillBar:SetVertexColor(0.0, 0.39, 0.88, 0.15)
|
||||
ExhaustionTickHighlight:SetVertexColor(0.0, 0.39, 0.88)
|
||||
elseif exhaustionStateID == 2 then
|
||||
MainMenuExpBar:SetStatusBarColor(0.58, 0.0, 0.55, 1.0)
|
||||
ExhaustionLevelFillBar:SetVertexColor(0.58, 0.0, 0.55, 0.15)
|
||||
ExhaustionTickHighlight:SetVertexColor(0.58, 0.0, 0.55)
|
||||
local rest = BENILLA_REST_STATE_COLOR[GetRestState()]
|
||||
if rest then
|
||||
MainMenuExpBar:SetStatusBarColor(rest.r, rest.g, rest.b, 1.0)
|
||||
ExhaustionLevelFillBar:SetVertexColor(rest.r, rest.g, rest.b, 0.15)
|
||||
ExhaustionTickHighlight:SetVertexColor(rest.r, rest.g, rest.b)
|
||||
end
|
||||
end
|
||||
-- ref-MainMenuBar.lua l.69-71, the function's own last statement: the watch bar has
|
||||
@@ -1257,7 +1336,7 @@
|
||||
-- ref-MainMenuBar.xml l.344-407: a 16x64 frame in the bar's own right-hand recess whose
|
||||
-- 20x66 bar texture is TINTED green/yellow/red by the connection's latency, plus an
|
||||
-- all-points button on top for the hover tooltip. The thresholds, the 10 s poll and the
|
||||
-- inline handlers below are the ref's, transcribed out of its XML into named functions.
|
||||
-- inline handlers below are the ref's, re-expressed out of its XML into named functions.
|
||||
-- The meter shows LATENCY ONLY: 1.12 never scales the bar's height, only its color.
|
||||
PERFORMANCEBAR_LOW_LATENCY = 300
|
||||
PERFORMANCEBAR_MEDIUM_LATENCY = 600
|
||||
@@ -1874,7 +1953,9 @@
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<!-- **The reputation watch bar** — `ref-ReputationFrame.xml:869-1036`, transcribed whole. It
|
||||
<!-- **The reputation watch bar** — built to `ref-ReputationFrame.xml:869-1036`, whose geometry
|
||||
and art it quotes (those are the client's own facts) and whose handlers it implements in
|
||||
this file's script block. It
|
||||
shipped built-and-hidden with 1223 (three corpus addons index it by name — `FuBar_FuXPFu`,
|
||||
`CT_BottomBar`, `zBarEx` — and FuXPFu dies at `FuBar_FuXPFu.lua:227` on the nil); it is
|
||||
DRIVEN now, by `ReputationWatchBar_Update` in this file's script block, whose own comment
|
||||
@@ -1973,62 +2054,20 @@
|
||||
</Layers>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<!-- ref l.996-1035, transcribed whole. The OnEvent fan is where the max-level rail's own
|
||||
handler pair used to be (this file's script block, the placement note): UPDATE_FACTION
|
||||
is the login seam the reference relies on and `crate::ui_reputation`'s feed now fires
|
||||
on every accepted push including the first, PLAYER_LEVEL_UP carries the new level as
|
||||
arg1, and anything else re-reads UnitLevel.
|
||||
|
||||
TWO NOTES on arms that read as odd and are the reference's own:
|
||||
· the UPDATE_FACTION arm gates on `this:IsVisible()` and then calls
|
||||
ReputationFrame_Update() — the PANE's repaint, from the BAR's handler. The pane
|
||||
registers UPDATE_FACTION itself (ReputationFrame.xml), so this is the reference
|
||||
double-driving it; transcribed rather than "fixed", and harmless because the pane's
|
||||
own repaint is idempotent. Guarded on existence because this file loads without
|
||||
ReputationFrame.xml in the engine's subset harnesses.
|
||||
· the `arg1 == "statusBarText"` arm is DEAD CODE in shipped 1.12 and stays: the
|
||||
CVAR_UPDATE token is the caller's third SetCVar argument, and 1.12's own options
|
||||
panels pass the uppercase display name ("STATUS_BAR_TEXT"), never the cvar's own
|
||||
name (benilla-ui `script::cvars`, decision 1140, which carves exactly this).
|
||||
PLAYER_ENTERING_WORLD is benilla's ADDITION (decision 1094's own): our faction feed
|
||||
only fires once the catalog and the player's race/class are up, which can be a tick or
|
||||
two after world entry, and the max-level rail must be right from the first frame. The
|
||||
arm it lands in is idempotent, so the extra registration only ever costs a no-op. -->
|
||||
<!-- ref l.996-1035. The three that carry any thought are chunk functions in this file's
|
||||
script block, where their departures are documented (`BenillaWatchBar_OnLoad` /
|
||||
`_OnEvent` / `_OnShow`); the three below are one statement each and stay where the
|
||||
reference has them. The numerals hover is the pair: coming onto the bar shows them
|
||||
unconditionally, leaving takes them down UNLESS the statusBarText cvar has pinned them
|
||||
on — `textLocked` is the pin, and the two text verbs above are what set it. -->
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:RegisterEvent("UPDATE_FACTION")
|
||||
this:RegisterEvent("PLAYER_LEVEL_UP")
|
||||
this:RegisterEvent("CVAR_UPDATE")
|
||||
this:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
</OnLoad>
|
||||
<OnEvent>
|
||||
if ( event == "UPDATE_FACTION" and this:IsVisible() ) then
|
||||
if ( ReputationFrame_Update ) then
|
||||
ReputationFrame_Update()
|
||||
end
|
||||
ReputationWatchBar_Update()
|
||||
elseif ( event == "PLAYER_LEVEL_UP" ) then
|
||||
ReputationWatchBar_Update(arg1)
|
||||
elseif ( event == "CVAR_UPDATE" and arg1 == "statusBarText" ) then
|
||||
if ( arg2 == "1" ) then
|
||||
ShowWatchedReputationBarText("lock")
|
||||
else
|
||||
HideWatchedReputationBarText("unlock")
|
||||
end
|
||||
else
|
||||
ReputationWatchBar_Update()
|
||||
end
|
||||
</OnEvent>
|
||||
<OnShow>
|
||||
if ( GetCVar("statusBarText") == "1" ) then
|
||||
ShowWatchedReputationBarText("lock")
|
||||
end
|
||||
UIParent_ManageFramePositions()
|
||||
</OnShow>
|
||||
<OnLoad>BenillaWatchBar_OnLoad(self)</OnLoad>
|
||||
<OnEvent>BenillaWatchBar_OnEvent(self, event)</OnEvent>
|
||||
<OnShow>BenillaWatchBar_OnShow()</OnShow>
|
||||
<OnHide>UIParent_ManageFramePositions()</OnHide>
|
||||
<OnEnter>ReputationWatchStatusBarText:Show()</OnEnter>
|
||||
<OnLeave>
|
||||
if ( not ReputationWatchBar.textLocked ) then
|
||||
if not ReputationWatchBar.textLocked then
|
||||
ReputationWatchStatusBarText:Hide()
|
||||
end
|
||||
</OnLeave>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Ui>
|
||||
<!--
|
||||
Interface\FrameXML\BasicControls.xml, transcribed — the reference's second file, right
|
||||
Interface\FrameXML\BasicControls.xml, re-expressed — the reference's second file, right
|
||||
after Fonts.xml, and one of the smallest in FrameXML. It carries four things an addon can
|
||||
reach and we had none of:
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<Script><![CDATA[
|
||||
-- ── _ERRORMESSAGE / message (ref BasicControls.xml l.5-15) ─────────────────────────────
|
||||
--
|
||||
-- VERBATIM, including `debuginfo()`. That line was guarded here when this file landed,
|
||||
-- COMPLETE, including `debuginfo()`. That line was guarded here when this file landed,
|
||||
-- because `debuginfo` was an engine global we lacked and whose behaviour was uncarved —
|
||||
-- and declaring a no-op under a real 1.12 name is exactly the "capability absent without a
|
||||
-- failure" class 1203 named. wow-re has since carved it (2026-08-11): `0x7027f0` is
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The player buff/debuff bar and the temporary-weapon-enchant row (decisions 0255/0257 — the
|
||||
player aura arc). Geometry, textures, colours, every timing constant AND now the data path
|
||||
are the real 1.12 BuffFrame's (extracted Interface\FrameXML\BuffFrame.xml/.lua).
|
||||
player aura arc). benilla's own code against the real 1.12 BuffFrame's contract: the
|
||||
extracted Interface\FrameXML\BuffFrame.xml/.lua is the SPECIFICATION, and geometry,
|
||||
textures, colours, every timing constant AND the data path are read off it.
|
||||
|
||||
THE INDEX SPACE — the thing to understand before touching a handler. There are TWO numbers
|
||||
here and they are not the same number:
|
||||
@@ -23,8 +24,8 @@
|
||||
table and the traps are `benilla_ui::script::aura`'s module header).
|
||||
|
||||
This file used to read the Era `UnitBuff`/`UnitDebuff` pair because the 1.12 verbs did not
|
||||
exist. They landed in b2ede294, so the adaptation is gone: the handlers below are the
|
||||
reference's, on the reference's numbers.
|
||||
exist. They landed in b2ede294, so the adaptation is gone: the handlers below run on the
|
||||
reference's own verbs and the reference's own numbers.
|
||||
|
||||
SCOPE. The LOCAL PLAYER's auras, plus the reference's two temporary-weapon-enchant slots.
|
||||
0255 left TemporaryEnchantFrame out ("there is no weapon-enchant wire yet"); there is —
|
||||
@@ -64,7 +65,7 @@
|
||||
|
||||
THE COUNTDOWN IS A POLL, NOT A CACHE. The aura EVENT is the discrete-change edge (icon,
|
||||
stacks, dispel class); the remaining time is re-read every frame by every visible button.
|
||||
That is the reference's own division of labour and it is now literally its code —
|
||||
That is the reference's own division of labour and this file keeps it exactly —
|
||||
BuffButton_Update caches the buff POSITION on PLAYER_AURAS_CHANGED, BuffButton_OnUpdate
|
||||
calls GetPlayerBuffTimeLeft with it each frame (ref BuffFrame.lua l.128-130) — and caching
|
||||
the expiry on the event instead is what made refreshed buffs freeze at "0 s" and freshly
|
||||
@@ -76,7 +77,8 @@
|
||||
the same verb over the same cache behind the same gate and the same CMSG_CANCEL_AURA queue;
|
||||
this file simply no longer needs the Era spelling.
|
||||
|
||||
DEVIATIONS from the reference .lua/.xml (noted, not bugs):
|
||||
BEHAVIOURAL DEPARTURES from the specification (noted, not bugs — everything else observable
|
||||
is the reference's, however differently this file spells it):
|
||||
- The count FontString is BLANKED ("") rather than hidden — an empty string draws nothing,
|
||||
same look. A site that predates region Show/Hide; equivalent either way. (The DURATION
|
||||
strings used the same shortcut until 1139 and no longer do: a hidden setting has to hide
|
||||
@@ -167,6 +169,29 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- Half of the square wave, run down by one frame. BuffFrameFlashTime is the time LEFT in
|
||||
-- the current half; when it goes under, the state flips and the counter reloads with the
|
||||
-- new half's length. The overshoot is carried into that half — unless it is longer than
|
||||
-- the half itself, in which case the whole period is kept rather than skipped, so a
|
||||
-- stalled frame costs at most one beat of drift instead of racing the pulse forward.
|
||||
local function buffFlashAdvance(elapsed)
|
||||
BuffFrameFlashTime = BuffFrameFlashTime - elapsed
|
||||
if BuffFrameFlashTime >= 0 then
|
||||
return
|
||||
end
|
||||
local overtime = -BuffFrameFlashTime
|
||||
if BuffFrameFlashState == 0 then
|
||||
BuffFrameFlashState = 1
|
||||
BuffFrameFlashTime = BUFF_FLASH_TIME_ON
|
||||
else
|
||||
BuffFrameFlashState = 0
|
||||
BuffFrameFlashTime = BUFF_FLASH_TIME_OFF
|
||||
end
|
||||
if overtime < BuffFrameFlashTime then
|
||||
BuffFrameFlashTime = BuffFrameFlashTime - overtime
|
||||
end
|
||||
end
|
||||
|
||||
function BuffFrame_OnUpdate(elapsed)
|
||||
-- The tooltip re-poll's throttle (ref l.29-33): oscillates around zero with a
|
||||
-- TOOLTIP_UPDATE_TIME period, and BuffButton_OnUpdate refreshes a hovered plate only on
|
||||
@@ -178,27 +203,23 @@
|
||||
BuffFrameUpdateTime = BuffFrameUpdateTime + TOOLTIP_UPDATE_TIME
|
||||
end
|
||||
|
||||
BuffFrameFlashTime = BuffFrameFlashTime - elapsed
|
||||
if BuffFrameFlashTime < 0 then
|
||||
local overtime = -BuffFrameFlashTime
|
||||
if BuffFrameFlashState == 0 then
|
||||
BuffFrameFlashState = 1
|
||||
BuffFrameFlashTime = BUFF_FLASH_TIME_ON
|
||||
else
|
||||
BuffFrameFlashState = 0
|
||||
BuffFrameFlashTime = BUFF_FLASH_TIME_OFF
|
||||
end
|
||||
if overtime < BuffFrameFlashTime then
|
||||
BuffFrameFlashTime = BuffFrameFlashTime - overtime
|
||||
end
|
||||
end
|
||||
|
||||
-- The pulse's level for this frame: a 0..1 ramp up across the ON half and back down
|
||||
-- across the OFF half (both scaled by BUFF_FLASH_TIME_ON — the reference's own choice,
|
||||
-- and invisible while the two halves are the same length), squashed into
|
||||
-- [BUFF_MIN_ALPHA, 1] so a warning icon dims rather than disappears.
|
||||
--
|
||||
-- Computed into a LOCAL and published once. BUFF_ALPHA_VALUE is read by every visible
|
||||
-- button, so it is not scratch space: the reference assigns the raw ramp to it and then
|
||||
-- squashes it in a second statement, which leaves the global holding an un-squashed
|
||||
-- value for the length of one statement.
|
||||
buffFlashAdvance(elapsed)
|
||||
local level
|
||||
if BuffFrameFlashState == 1 then
|
||||
BUFF_ALPHA_VALUE = (BUFF_FLASH_TIME_ON - BuffFrameFlashTime) / BUFF_FLASH_TIME_ON
|
||||
level = (BUFF_FLASH_TIME_ON - BuffFrameFlashTime) / BUFF_FLASH_TIME_ON
|
||||
else
|
||||
BUFF_ALPHA_VALUE = BuffFrameFlashTime / BUFF_FLASH_TIME_ON
|
||||
level = BuffFrameFlashTime / BUFF_FLASH_TIME_ON
|
||||
end
|
||||
BUFF_ALPHA_VALUE = (BUFF_ALPHA_VALUE * (1 - BUFF_MIN_ALPHA)) + BUFF_MIN_ALPHA
|
||||
BUFF_ALPHA_VALUE = (level * (1 - BUFF_MIN_ALPHA)) + BUFF_MIN_ALPHA
|
||||
end
|
||||
|
||||
-- BuffButtons_UpdatePositions (BuffFrame.lua l.152-160): the ROW PITCH depends on whether a
|
||||
@@ -306,29 +327,32 @@
|
||||
-- frames (`UiScript::tick`), exactly as the client's does, so a hidden button's stale
|
||||
-- buffIndex is never read.
|
||||
function BuffButton_OnUpdate()
|
||||
local buffDuration = getglobal(this:GetName() .. "Duration")
|
||||
if this.untilCancelled == 1 then
|
||||
buffDuration:Hide()
|
||||
local button = this
|
||||
-- A permanent aura has nothing to count down and nothing to flash toward, so it is
|
||||
-- done in one line: UpdateDuration with no time left is exactly "hide the timer",
|
||||
-- which is the whole of the reference's early-out here. (The reference reaches for the
|
||||
-- $parentDuration region itself to do it, on every frame of every visible button, and
|
||||
-- then throws that region away and lets UpdateDuration look it up again.)
|
||||
if button.untilCancelled == 1 then
|
||||
BuffFrame_UpdateDuration(button, nil)
|
||||
return
|
||||
end
|
||||
|
||||
local buffIndex = this.buffIndex
|
||||
local buffIndex = button.buffIndex
|
||||
local timeLeft = GetPlayerBuffTimeLeft(buffIndex)
|
||||
-- The flash is NOT gated on the timer setting: the reference pulses a button in its last
|
||||
-- 31s whether or not a number is drawn under it, and with the timers off it is the only
|
||||
-- thing left saying "about to drop".
|
||||
if timeLeft < BUFF_WARNING_TIME then
|
||||
this:SetAlpha(BUFF_ALPHA_VALUE)
|
||||
button:SetAlpha(BUFF_ALPHA_VALUE)
|
||||
else
|
||||
this:SetAlpha(1.0)
|
||||
button:SetAlpha(1.0)
|
||||
end
|
||||
|
||||
BuffFrame_UpdateDuration(this, timeLeft)
|
||||
BuffFrame_UpdateDuration(button, timeLeft)
|
||||
|
||||
if BuffFrameUpdateTime > 0 then
|
||||
return
|
||||
end
|
||||
if GameTooltip and GameTooltip:IsOwned(this) then
|
||||
-- A tooltip we own is re-read on the throttle's beat, not every frame.
|
||||
if BuffFrameUpdateTime <= 0 and GameTooltip and GameTooltip:IsOwned(button) then
|
||||
GameTooltip:SetPlayerBuff(buffIndex)
|
||||
end
|
||||
end
|
||||
@@ -339,72 +363,65 @@
|
||||
CancelPlayerBuff(this.buffIndex)
|
||||
end
|
||||
|
||||
-- The temporary-weapon-enchant row (ref l.162-233). GetWeaponEnchantInfo is the whole data
|
||||
-- path: two flags, two remaining-MILLISECOND counts, two charge counts. Off hand takes
|
||||
-- TempEnchant1 and main hand the next free slot, so with both enchanted the main hand sits
|
||||
-- to the LEFT of the off hand — the reference's own ordering.
|
||||
function BuffFrame_Enchant_OnUpdate(elapsed)
|
||||
local hasMainHandEnchant, mainHandExpiration, mainHandCharges, hasOffHandEnchant, offHandExpiration, offHandCharges = GetWeaponEnchantInfo()
|
||||
-- Clear enchant slots `first`..2 — the tail of the row nothing is using this frame.
|
||||
local function buffEnchantClear(first)
|
||||
for slot = first, 2 do
|
||||
getglobal("TempEnchant" .. slot):Hide()
|
||||
getglobal("TempEnchant" .. slot .. "Duration"):Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- No enchants, kick out early — and park BuffFrame back on its resting point.
|
||||
-- The temporary-weapon-enchant row (ref l.162-233). GetWeaponEnchantInfo is the whole data
|
||||
-- path: two flags, two remaining-MILLISECOND counts, two charge counts (the charges are
|
||||
-- read by nobody here, and are named only to spell the API's shape out).
|
||||
--
|
||||
-- The two hands are the same six steps on different numbers, so they are one loop over a
|
||||
-- two-row table rather than the reference's two copies of the block. OFF HAND FIRST is the
|
||||
-- part that matters: the row packs from slot 1 rightwards, so with both weapons enchanted
|
||||
-- the main hand lands in slot 2 and therefore sits to the LEFT of the off hand.
|
||||
function BuffFrame_Enchant_OnUpdate(elapsed)
|
||||
local hasMainHandEnchant, mainHandExpiration, mainHandCharges,
|
||||
hasOffHandEnchant, offHandExpiration, offHandCharges = GetWeaponEnchantInfo()
|
||||
|
||||
-- Nothing enchanted: clear the whole row and park BuffFrame back on its resting point.
|
||||
if not hasMainHandEnchant and not hasOffHandEnchant then
|
||||
TempEnchant1:Hide()
|
||||
TempEnchant1Duration:Hide()
|
||||
TempEnchant2:Hide()
|
||||
TempEnchant2Duration:Hide()
|
||||
buffEnchantClear(1)
|
||||
BuffFrame:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, 0)
|
||||
return
|
||||
end
|
||||
|
||||
local enchantButton
|
||||
local textureName
|
||||
local enchantIndex = 0
|
||||
if hasOffHandEnchant then
|
||||
enchantIndex = enchantIndex + 1
|
||||
textureName = GetInventoryItemTexture("player", 17)
|
||||
TempEnchant1:SetID(17)
|
||||
TempEnchant1Icon:SetTexture(textureName or BENILLA_FALLBACK_ICON)
|
||||
TempEnchant1:Show()
|
||||
local filled = 0
|
||||
for _, hand in ipairs({
|
||||
-- `slot` is the live API's inventory id, which is also what the button's hover
|
||||
-- reads back off itself to ask for the ITEM's tooltip.
|
||||
{ enchanted = hasOffHandEnchant, expiration = offHandExpiration, slot = 17 },
|
||||
{ enchanted = hasMainHandEnchant, expiration = mainHandExpiration, slot = 16 },
|
||||
}) do
|
||||
if hand.enchanted then
|
||||
filled = filled + 1
|
||||
local button = getglobal("TempEnchant" .. filled)
|
||||
button:SetID(hand.slot)
|
||||
getglobal(button:GetName() .. "Icon"):SetTexture(
|
||||
GetInventoryItemTexture("player", hand.slot) or BENILLA_FALLBACK_ICON)
|
||||
button:Show()
|
||||
|
||||
if offHandExpiration then
|
||||
offHandExpiration = offHandExpiration / 1000
|
||||
end
|
||||
BuffFrame_UpdateDuration(TempEnchant1, offHandExpiration)
|
||||
|
||||
if offHandExpiration and offHandExpiration < BUFF_WARNING_TIME then
|
||||
TempEnchant1:SetAlpha(BUFF_ALPHA_VALUE)
|
||||
else
|
||||
TempEnchant1:SetAlpha(1.0)
|
||||
-- The wire counts an enchant down in MILLISECONDS; everything the row draws is
|
||||
-- in seconds. A nil expiration is "no countdown known", which the timer and the
|
||||
-- warning flash both read as "nothing to warn about".
|
||||
local timeLeft = hand.expiration and (hand.expiration / 1000)
|
||||
BuffFrame_UpdateDuration(button, timeLeft)
|
||||
if timeLeft and timeLeft < BUFF_WARNING_TIME then
|
||||
button:SetAlpha(BUFF_ALPHA_VALUE)
|
||||
else
|
||||
button:SetAlpha(1.0)
|
||||
end
|
||||
end
|
||||
end
|
||||
if hasMainHandEnchant then
|
||||
enchantIndex = enchantIndex + 1
|
||||
enchantButton = getglobal("TempEnchant" .. enchantIndex)
|
||||
textureName = GetInventoryItemTexture("player", 16)
|
||||
enchantButton:SetID(16)
|
||||
getglobal(enchantButton:GetName() .. "Icon"):SetTexture(textureName or BENILLA_FALLBACK_ICON)
|
||||
enchantButton:Show()
|
||||
buffEnchantClear(filled + 1)
|
||||
|
||||
if mainHandExpiration then
|
||||
mainHandExpiration = mainHandExpiration / 1000
|
||||
end
|
||||
BuffFrame_UpdateDuration(enchantButton, mainHandExpiration)
|
||||
|
||||
if mainHandExpiration and mainHandExpiration < BUFF_WARNING_TIME then
|
||||
enchantButton:SetAlpha(BUFF_ALPHA_VALUE)
|
||||
else
|
||||
enchantButton:SetAlpha(1.0)
|
||||
end
|
||||
end
|
||||
-- Hide unused enchants
|
||||
for i = enchantIndex + 1, 2 do
|
||||
getglobal("TempEnchant" .. i):Hide()
|
||||
getglobal("TempEnchant" .. i .. "Duration"):Hide()
|
||||
end
|
||||
|
||||
-- Position buff frame: the row is `enchantIndex` icons wide, and the bar starts one
|
||||
-- 5px gutter to its left.
|
||||
TemporaryEnchantFrame:SetWidth(enchantIndex * 32)
|
||||
-- Position buff frame: the row is `filled` icons wide, and the bar starts one 5px
|
||||
-- gutter to its left.
|
||||
TemporaryEnchantFrame:SetWidth(filled * 32)
|
||||
BuffFrame:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPLEFT", -5, 0)
|
||||
end
|
||||
|
||||
@@ -427,17 +444,21 @@
|
||||
-- known" case. Hidden, not blanked — see the header's deviation note.
|
||||
function BuffFrame_UpdateDuration(buffButton, timeLeft)
|
||||
local duration = getglobal(buffButton:GetName() .. "Duration")
|
||||
if SHOW_BUFF_DURATIONS == "1" and timeLeft then
|
||||
duration:SetText(SecondsToTimeAbbrev(timeLeft))
|
||||
if timeLeft < BUFF_DURATION_WARNING_TIME then
|
||||
duration:SetVertexColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
|
||||
else
|
||||
duration:SetVertexColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
|
||||
end
|
||||
duration:Show()
|
||||
else
|
||||
-- Two ways to have no number to draw: the setting is off, or nothing has told us how
|
||||
-- long is left (the enchant row's "no countdown known").
|
||||
if SHOW_BUFF_DURATIONS ~= "1" or not timeLeft then
|
||||
duration:Hide()
|
||||
return
|
||||
end
|
||||
duration:SetText(SecondsToTimeAbbrev(timeLeft))
|
||||
-- Gold most of the way, white inside the last minute — the number's own warning, a
|
||||
-- half-minute ahead of the icon starting to pulse.
|
||||
local color = NORMAL_FONT_COLOR
|
||||
if timeLeft < BUFF_DURATION_WARNING_TIME then
|
||||
color = HIGHLIGHT_FONT_COLOR
|
||||
end
|
||||
duration:SetVertexColor(color.r, color.g, color.b)
|
||||
duration:Show()
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The cast bar - the 1.12 CastingBarFrame, transcribed from the extracted reference
|
||||
(Interface\FrameXML\CastingBarFrame.xml/.lua, mpqx; decision 0137 phase 1). Geometry,
|
||||
textures, colors, and every timing constant are the reference's own. Driven by the
|
||||
SPELLCAST_* events crate::ui_cast fires off the wire (START name,ms / STOP / FAILED /
|
||||
INTERRUPTED / DELAYED / CHANNEL_START ms,name / CHANNEL_UPDATE ms / CHANNEL_STOP).
|
||||
The cast bar — benilla's own implementation of the 1.12 CastingBarFrame contract. The
|
||||
extracted reference (Interface\FrameXML\CastingBarFrame.xml/.lua, mpqx; decision 0137
|
||||
phase 1) is the SPECIFICATION: geometry, textures, colors, and every timing constant are
|
||||
read off it, and the observable lifecycle below is the one it defines. The code is ours.
|
||||
Driven by the SPELLCAST_* events crate::ui_cast fires off the wire (START name,ms / STOP /
|
||||
FAILED / INTERRUPTED / DELAYED / CHANNEL_START ms,name / CHANNEL_UPDATE ms / CHANNEL_STOP).
|
||||
|
||||
The reference's globals (CastingBarText/Spark/Flash) are named regions, so they publish as
|
||||
globals here exactly as they do there; `this` is the firing frame (RF-0025). Departures
|
||||
from the reference .lua: its dead code (`CastingBarFrameStatusBar = CastingBarFrame`, a
|
||||
self-alias — we just use the frame; the `if not this:IsVisible() then this:Hide() end`
|
||||
no-op guard at the top of the STOP branch; the two write-only `this.mode` stores nothing
|
||||
reads), and the flash/fade steps normalized to the reference tick rate (the
|
||||
CASTING_BAR_REF_TICK comment below — decision 0454).
|
||||
globals here exactly as they do there; `this` is the firing frame (RF-0025), and every
|
||||
handler below reads it once into a named local rather than threading the global through a
|
||||
dozen method calls. Deliberate departures from the specification's own .lua: its dead code
|
||||
(the `if not this:IsVisible() then this:Hide() end` no-op guard at the head of the STOP
|
||||
branch; the two write-only `this.mode` stores nothing reads), and the flash/fade steps
|
||||
normalized to the reference tick rate (the CASTING_BAR_REF_TICK comment below —
|
||||
decision 0454).
|
||||
|
||||
SPELLCAST_DELAYED is live: SMSG_SPELL_DELAYED (pushback on taking a hit) is decoded and fed,
|
||||
so a hit slides the bar's window out instead of letting it finish early (decision 0256).
|
||||
@@ -41,152 +43,193 @@
|
||||
FAILED = FAILED or "Failed"
|
||||
INTERRUPTED = INTERRUPTED or "Interrupted"
|
||||
|
||||
-- THE BAR'S WHOLE VOCABULARY, one entry per wire event, each taking the frame. OnLoad
|
||||
-- registers this table's keys and OnEvent looks the incoming event up in it, so the set
|
||||
-- the bar LISTENS for and the set it can ANSWER are the same set by construction — there
|
||||
-- is no second list to fall out of step with. (The reference spells both halves out: eight
|
||||
-- RegisterEvent calls, then a sixty-line if-chain testing the same eight strings back.)
|
||||
local castingBarEvent = {}
|
||||
|
||||
-- Seat the fill at `value` and ride the spark on its leading edge. The bar is 195px wide,
|
||||
-- so a fraction of `span` scales straight to a pixel offset from the frame's LEFT. A cast
|
||||
-- and a channel differ only in which value and which span they hand in.
|
||||
local function castingBarTrack(bar, value, span)
|
||||
bar:SetValue(value)
|
||||
CastingBarFlash:Hide()
|
||||
local sparkPosition = ((value - bar.startTime) / span) * 195
|
||||
-- Pushback slides startTime PAST the wall clock, so a cast really can sit before its
|
||||
-- own window's start for a frame or two. (A channel cannot — its value counts down
|
||||
-- from the far end and stops at startTime — so here the clamp is dead weight rather
|
||||
-- than a change of behaviour.)
|
||||
if sparkPosition < 0 then
|
||||
sparkPosition = 0
|
||||
end
|
||||
CastingBarSpark:SetPoint("CENTER", CastingBarFrame, "LEFT", sparkPosition, 2)
|
||||
end
|
||||
|
||||
-- A cast opens: orange, a full window anchored on the clock, the spark at the left edge.
|
||||
castingBarEvent.SPELLCAST_START = function(bar)
|
||||
bar:SetStatusBarColor(1.0, 0.7, 0.0)
|
||||
CastingBarSpark:Show()
|
||||
bar.startTime = GetTime()
|
||||
bar.maxValue = bar.startTime + (arg2 / 1000)
|
||||
bar:SetMinMaxValues(bar.startTime, bar.maxValue)
|
||||
bar:SetValue(bar.startTime)
|
||||
CastingBarText:SetText(arg1)
|
||||
bar:SetAlpha(1.0)
|
||||
bar.holdTime = 0
|
||||
bar.casting = 1
|
||||
bar.fadeOut = nil
|
||||
bar:Show()
|
||||
end
|
||||
|
||||
-- A channel opens. Its args are START's the other way round — milliseconds first, then the
|
||||
-- name — and its fill DRAINS, so the bar is seated at the far end and walks back.
|
||||
castingBarEvent.SPELLCAST_CHANNEL_START = function(bar)
|
||||
bar:SetStatusBarColor(1.0, 0.7, 0.0)
|
||||
CastingBarSpark:Show()
|
||||
bar.maxValue = 1
|
||||
bar.startTime = GetTime()
|
||||
bar.endTime = bar.startTime + (arg1 / 1000)
|
||||
bar.duration = arg1 / 1000
|
||||
bar:SetMinMaxValues(bar.startTime, bar.endTime)
|
||||
bar:SetValue(bar.endTime)
|
||||
CastingBarText:SetText(arg2)
|
||||
bar:SetAlpha(1.0)
|
||||
bar.holdTime = 0
|
||||
bar.casting = nil
|
||||
bar.channeling = 1
|
||||
bar.fadeOut = nil
|
||||
bar:Show()
|
||||
end
|
||||
|
||||
-- The server's mid-channel correction: the window keeps its LENGTH and is re-anchored to
|
||||
-- end arg1 ms from now, so a pushback eats into what is left rather than extending it.
|
||||
castingBarEvent.SPELLCAST_CHANNEL_UPDATE = function(bar)
|
||||
if not bar:IsShown() then
|
||||
return
|
||||
end
|
||||
local span = bar.endTime - bar.startTime
|
||||
bar.endTime = GetTime() + (arg1 / 1000)
|
||||
bar.startTime = bar.endTime - span
|
||||
bar:SetMinMaxValues(bar.startTime, bar.endTime)
|
||||
end
|
||||
|
||||
-- Pushback on taking a hit (SMSG_SPELL_DELAYED, decision 0256): BOTH ends of the window
|
||||
-- slide out by arg1 ms, so the cast has that much longer to run and the spark jumps back.
|
||||
-- A hit never cancels the bar.
|
||||
castingBarEvent.SPELLCAST_DELAYED = function(bar)
|
||||
if not bar:IsShown() then
|
||||
return
|
||||
end
|
||||
local pushback = arg1 / 1000
|
||||
bar.startTime = bar.startTime + pushback
|
||||
bar.maxValue = bar.maxValue + pushback
|
||||
bar:SetMinMaxValues(bar.startTime, bar.maxValue)
|
||||
end
|
||||
|
||||
-- The cast landed, or a channel ran its course: green, full, and the completion bloom
|
||||
-- armed at alpha 0 for OnUpdate to ramp. `flag` names the one field the event clears,
|
||||
-- which is the only thing that separates the two of them.
|
||||
local function castingBarFinished(bar, flag)
|
||||
if not bar:IsShown() then
|
||||
return
|
||||
end
|
||||
bar:SetValue(bar.maxValue)
|
||||
bar:SetStatusBarColor(0.0, 1.0, 0.0)
|
||||
CastingBarSpark:Hide()
|
||||
CastingBarFlash:SetAlpha(0.0)
|
||||
CastingBarFlash:Show()
|
||||
bar[flag] = nil
|
||||
bar.flash = 1
|
||||
bar.fadeOut = 1
|
||||
end
|
||||
castingBarEvent.SPELLCAST_STOP = function(bar) castingBarFinished(bar, "casting") end
|
||||
castingBarEvent.SPELLCAST_CHANNEL_STOP = function(bar) castingBarFinished(bar, "channeling") end
|
||||
|
||||
-- The cast did NOT land: red, the reason where the spell's name was, and held opaque for
|
||||
-- CASTING_BAR_HOLD_TIME before the fade starts — no completion bloom. A channel is never
|
||||
-- ended this way (the server closes one with CHANNEL_STOP), so a live channel ignores both.
|
||||
local function castingBarAborted(bar, reason)
|
||||
if not bar:IsShown() or bar.channeling then
|
||||
return
|
||||
end
|
||||
bar:SetValue(bar.maxValue)
|
||||
bar:SetStatusBarColor(1.0, 0.0, 0.0)
|
||||
CastingBarSpark:Hide()
|
||||
CastingBarText:SetText(reason)
|
||||
bar.casting = nil
|
||||
bar.fadeOut = 1
|
||||
bar.holdTime = GetTime() + CASTING_BAR_HOLD_TIME
|
||||
end
|
||||
castingBarEvent.SPELLCAST_FAILED = function(bar) castingBarAborted(bar, FAILED) end
|
||||
castingBarEvent.SPELLCAST_INTERRUPTED = function(bar) castingBarAborted(bar, INTERRUPTED) end
|
||||
|
||||
function CastingBarFrame_OnLoad()
|
||||
this:RegisterEvent("SPELLCAST_START")
|
||||
this:RegisterEvent("SPELLCAST_STOP")
|
||||
this:RegisterEvent("SPELLCAST_FAILED")
|
||||
this:RegisterEvent("SPELLCAST_INTERRUPTED")
|
||||
this:RegisterEvent("SPELLCAST_DELAYED")
|
||||
this:RegisterEvent("SPELLCAST_CHANNEL_START")
|
||||
this:RegisterEvent("SPELLCAST_CHANNEL_UPDATE")
|
||||
this:RegisterEvent("SPELLCAST_CHANNEL_STOP")
|
||||
this.casting = nil
|
||||
this.holdTime = 0
|
||||
-- The ref's own last line of this function (`CastingBarFrame.lua:16`), and the one
|
||||
-- line of it we dropped: a plain alias global. `CastingBarFrame` IS the StatusBar in
|
||||
-- 1.12 — there is no separate child frame — so the reference publishes the name an
|
||||
-- addon would look for, and then uses it itself for the rest of the file.
|
||||
local bar = this
|
||||
for spellcast in pairs(castingBarEvent) do
|
||||
bar:RegisterEvent(spellcast)
|
||||
end
|
||||
bar.casting = nil
|
||||
bar.holdTime = 0
|
||||
-- The alias global the reference publishes at the end of its own OnLoad
|
||||
-- (`CastingBarFrame.lua:16`). `CastingBarFrame` IS the StatusBar in 1.12 — there is no
|
||||
-- separate child frame — so the name an addon reaches for has to resolve to the frame
|
||||
-- itself.
|
||||
--
|
||||
-- 11 corpus addons index `CastingBarFrameStatusBar`, and it was the largest single
|
||||
-- name in the whole session-start `attempt to index global` row. Nothing warned:
|
||||
-- a line dropped INSIDE a function we otherwise transcribed verbatim is invisible to
|
||||
-- every instrument here. `framexml-transcription-diff.py` compares elements and
|
||||
-- handlers, not function bodies, and would not even pair these two files (ours is
|
||||
-- CastingBar.xml, the reference's is CastingBarFrame.xml).
|
||||
CastingBarFrameStatusBar = CastingBarFrame
|
||||
this:Hide()
|
||||
-- name in the whole session-start `attempt to index global` row. It went missing here
|
||||
-- once and nothing warned: a line dropped from INSIDE a function is invisible to every
|
||||
-- instrument we have. `framexml-transcription-diff.py` compares elements and handlers,
|
||||
-- not function bodies, and would not even pair these two files (ours is CastingBar.xml,
|
||||
-- the reference's is CastingBarFrame.xml).
|
||||
CastingBarFrameStatusBar = bar
|
||||
bar:Hide()
|
||||
end
|
||||
|
||||
function CastingBarFrame_OnEvent()
|
||||
if event == "SPELLCAST_START" then
|
||||
this:SetStatusBarColor(1.0, 0.7, 0.0)
|
||||
CastingBarSpark:Show()
|
||||
this.startTime = GetTime()
|
||||
this.maxValue = this.startTime + (arg2 / 1000)
|
||||
this:SetMinMaxValues(this.startTime, this.maxValue)
|
||||
this:SetValue(this.startTime)
|
||||
CastingBarText:SetText(arg1)
|
||||
this:SetAlpha(1.0)
|
||||
this.holdTime = 0
|
||||
this.casting = 1
|
||||
this.fadeOut = nil
|
||||
this:Show()
|
||||
elseif event == "SPELLCAST_STOP" or event == "SPELLCAST_CHANNEL_STOP" then
|
||||
if this:IsShown() then
|
||||
this:SetValue(this.maxValue)
|
||||
this:SetStatusBarColor(0.0, 1.0, 0.0)
|
||||
CastingBarSpark:Hide()
|
||||
CastingBarFlash:SetAlpha(0.0)
|
||||
CastingBarFlash:Show()
|
||||
if event == "SPELLCAST_STOP" then
|
||||
this.casting = nil
|
||||
else
|
||||
this.channeling = nil
|
||||
end
|
||||
this.flash = 1
|
||||
this.fadeOut = 1
|
||||
end
|
||||
elseif event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
|
||||
if this:IsShown() and not this.channeling then
|
||||
this:SetValue(this.maxValue)
|
||||
this:SetStatusBarColor(1.0, 0.0, 0.0)
|
||||
CastingBarSpark:Hide()
|
||||
if event == "SPELLCAST_FAILED" then
|
||||
CastingBarText:SetText(FAILED)
|
||||
else
|
||||
CastingBarText:SetText(INTERRUPTED)
|
||||
end
|
||||
this.casting = nil
|
||||
this.fadeOut = 1
|
||||
this.holdTime = GetTime() + CASTING_BAR_HOLD_TIME
|
||||
end
|
||||
elseif event == "SPELLCAST_DELAYED" then
|
||||
if this:IsShown() then
|
||||
this.startTime = this.startTime + (arg1 / 1000)
|
||||
this.maxValue = this.maxValue + (arg1 / 1000)
|
||||
this:SetMinMaxValues(this.startTime, this.maxValue)
|
||||
end
|
||||
elseif event == "SPELLCAST_CHANNEL_START" then
|
||||
this:SetStatusBarColor(1.0, 0.7, 0.0)
|
||||
CastingBarSpark:Show()
|
||||
this.maxValue = 1
|
||||
this.startTime = GetTime()
|
||||
this.endTime = this.startTime + (arg1 / 1000)
|
||||
this.duration = arg1 / 1000
|
||||
this:SetMinMaxValues(this.startTime, this.endTime)
|
||||
this:SetValue(this.endTime)
|
||||
CastingBarText:SetText(arg2)
|
||||
this:SetAlpha(1.0)
|
||||
this.holdTime = 0
|
||||
this.casting = nil
|
||||
this.channeling = 1
|
||||
this.fadeOut = nil
|
||||
this:Show()
|
||||
elseif event == "SPELLCAST_CHANNEL_UPDATE" then
|
||||
if this:IsShown() then
|
||||
local origDuration = this.endTime - this.startTime
|
||||
this.endTime = GetTime() + (arg1 / 1000)
|
||||
this.startTime = this.endTime - origDuration
|
||||
this:SetMinMaxValues(this.startTime, this.endTime)
|
||||
end
|
||||
local handle = castingBarEvent[event]
|
||||
if handle then
|
||||
handle(this)
|
||||
end
|
||||
end
|
||||
|
||||
function CastingBarFrame_OnUpdate()
|
||||
if this.casting then
|
||||
local status = GetTime()
|
||||
if status > this.maxValue then
|
||||
status = this.maxValue
|
||||
local bar = this
|
||||
if bar.casting then
|
||||
-- The fill IS the wall clock, pinned at the window's end once the cast is spent.
|
||||
local now = GetTime()
|
||||
if now > bar.maxValue then
|
||||
now = bar.maxValue
|
||||
end
|
||||
this:SetValue(status)
|
||||
CastingBarFlash:Hide()
|
||||
local sparkPosition = ((status - this.startTime) / (this.maxValue - this.startTime)) * 195
|
||||
if sparkPosition < 0 then
|
||||
sparkPosition = 0
|
||||
end
|
||||
CastingBarSpark:SetPoint("CENTER", CastingBarFrame, "LEFT", sparkPosition, 2)
|
||||
elseif this.channeling then
|
||||
local time = GetTime()
|
||||
if time > this.endTime then
|
||||
time = this.endTime
|
||||
end
|
||||
if time == this.endTime then
|
||||
this.channeling = nil
|
||||
this.fadeOut = 1
|
||||
castingBarTrack(bar, now, bar.maxValue - bar.startTime)
|
||||
elseif bar.channeling then
|
||||
local now = GetTime()
|
||||
if now >= bar.endTime then
|
||||
bar.channeling = nil
|
||||
bar.fadeOut = 1
|
||||
return
|
||||
end
|
||||
local barValue = this.startTime + (this.endTime - time)
|
||||
this:SetValue(barValue)
|
||||
CastingBarFlash:Hide()
|
||||
local sparkPosition = ((barValue - this.startTime) / (this.endTime - this.startTime)) * 195
|
||||
CastingBarSpark:SetPoint("CENTER", CastingBarFrame, "LEFT", sparkPosition, 2)
|
||||
elseif GetTime() < this.holdTime then
|
||||
-- A channel drains: the value is the window's start plus what is LEFT of it, so
|
||||
-- the fill empties from full instead of filling from empty.
|
||||
castingBarTrack(bar, bar.startTime + (bar.endTime - now), bar.endTime - bar.startTime)
|
||||
elseif GetTime() < bar.holdTime then
|
||||
return
|
||||
elseif this.flash then
|
||||
elseif bar.flash then
|
||||
local alpha = CastingBarFlash:GetAlpha() + CASTING_BAR_FLASH_STEP * (arg1 * CASTING_BAR_REF_TICK)
|
||||
if alpha < 1 then
|
||||
CastingBarFlash:SetAlpha(alpha)
|
||||
else
|
||||
CastingBarFlash:SetAlpha(1.0)
|
||||
this.flash = nil
|
||||
bar.flash = nil
|
||||
end
|
||||
elseif this.fadeOut then
|
||||
local alpha = this:GetAlpha() - CASTING_BAR_ALPHA_STEP * (arg1 * CASTING_BAR_REF_TICK)
|
||||
elseif bar.fadeOut then
|
||||
local alpha = bar:GetAlpha() - CASTING_BAR_ALPHA_STEP * (arg1 * CASTING_BAR_REF_TICK)
|
||||
if alpha > 0 then
|
||||
this:SetAlpha(alpha)
|
||||
bar:SetAlpha(alpha)
|
||||
else
|
||||
this.fadeOut = nil
|
||||
this:Hide()
|
||||
bar.fadeOut = nil
|
||||
bar:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
artifacts, never committed — the 0068/0138 pattern; line cites below are into that
|
||||
extraction's own `ref-CharacterFrame.xml`/`.lua`, `ref-PaperDollFrame.xml`/`.lua`,
|
||||
`ref-UIParent.lua`, `ref-GlobalStrings.lua`). `CharacterFrameTabButtonTemplate` is ALREADY
|
||||
transcribed in `UiPanels.xml:507-563` (battle-tested by Merchant/QuestLog, 0138) — reused
|
||||
defined in `UiPanels.xml:507-563` (battle-tested by Merchant/QuestLog, 0138) — reused
|
||||
here, not redefined.
|
||||
|
||||
Docks the left UIPanel slot exactly like Gossip/Merchant/Quest/QuestLog (`UiPanels.xml`
|
||||
@@ -101,7 +101,7 @@
|
||||
shared template's own Scripts block at l.8-37), just keyed off `GetInventorySlotInfo(
|
||||
"AmmoSlot")` instead of e.g. `"HeadSlot"`. Whatever `PLAYER_AMMO_ID`/bag-summing happens
|
||||
for that particular slot id is the parallel binding's own implementation detail, invisible
|
||||
to this Lua — transcribed here exactly as the ref wires it.
|
||||
to this Lua — re-expressed here exactly as the ref wires it.
|
||||
- `$parentStock` (the ammo slot's yellow "in bags" count, ref `ref-PaperDollFrame.xml:
|
||||
970-978`): no Lua in this session's extraction ever populates it (likely an ammo-picker-
|
||||
addon-only overlay outside this pull) — dropped rather than guessed at.
|
||||
@@ -310,7 +310,7 @@
|
||||
-- keybind path cannot disagree. l.47's trailing PlaySound is the ref's OWN redundant second
|
||||
-- igCharacterInfoTab — ToggleCharacter already plays it on the "switch to this tab" path, so
|
||||
-- this second call only matters when the window is ALREADY open on this same tab, i.e. the
|
||||
-- HideUIPanel branch — transcribed verbatim rather than "fixed" since it's the ref's own
|
||||
-- HideUIPanel branch — kept rather than "fixed" since it's the ref's own
|
||||
-- behavior, not a bug of ours.
|
||||
function BenillaCharacterFrameTab_OnClick(id)
|
||||
ToggleCharacter(BENILLA_CHARACTERFRAME_SUBFRAMES[id])
|
||||
@@ -402,7 +402,7 @@
|
||||
end
|
||||
|
||||
-- ── The paper-doll page (ref PaperDollFrame.lua) ────────────────────────────────────────
|
||||
-- ref PaperDollFrame_OnLoad (l.6-29), the label lines (l.7-13) transcribed verbatim: the
|
||||
-- ref PaperDollFrame_OnLoad (l.6-29), the label lines (l.7-13) kept as the ref has them: the
|
||||
-- labels live ONLY in the row template's $parentLabel (gold GameFontNormalSmall, LEFT) and
|
||||
-- Lua fills them — never a re-declared region in an instance (the ref's instances carry no
|
||||
-- <Layers> at all, and this engine's template expansion appends children, so a re-declared
|
||||
@@ -432,37 +432,79 @@
|
||||
frame:RegisterEvent("UNIT_ATTACK")
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_OnEvent(event, unit) (l.37-76), the unit-token branch only (unit ==
|
||||
-- "player" always true here — no pet/other-unit reuse in phase 1a) and this file's
|
||||
-- registered subset only. `unit` is the DECLARED second parameter, not the `arg1` global:
|
||||
-- the XML passes `arg1` into it exactly as the reference's own does (PaperDollFrame.xml),
|
||||
-- so an addon calling the 1.12 arity is read correctly rather than off a stale global.
|
||||
-- Every row of the sheet, in the order a full repaint fills them. The order is load-bearing
|
||||
-- at the ranged end: SetRangedAttack is what decides `PaperDollFrame.noRanged`, and the two
|
||||
-- ranged rows after it read that flag rather than re-deciding it.
|
||||
--
|
||||
-- Rows are NAMED, not referenced. Resolving each through the global table at call time is
|
||||
-- what the reference's own call-by-name got for free, and the paper-doll setters are prime
|
||||
-- addon-hook territory: a table of captured function values would keep calling the setter an
|
||||
-- addon replaced.
|
||||
local paperDollRows = {
|
||||
"PaperDollFrame_SetLevel",
|
||||
"PaperDollFrame_SetStats",
|
||||
"PaperDollFrame_SetResistances",
|
||||
"PaperDollFrame_SetArmor",
|
||||
"PaperDollFrame_SetDamage",
|
||||
"PaperDollFrame_SetAttackPower",
|
||||
"PaperDollFrame_SetAttackBothHands",
|
||||
"PaperDollFrame_SetRangedAttack",
|
||||
"PaperDollFrame_SetRangedDamage",
|
||||
"PaperDollFrame_SetRangedAttackPower",
|
||||
}
|
||||
|
||||
-- A weapon that changed how hard it hits and one that changed how often move the same four
|
||||
-- rows, so the two events share a list rather than a copy of one.
|
||||
local weaponRows = {
|
||||
"PaperDollFrame_SetDamage",
|
||||
"PaperDollFrame_SetRangedAttack",
|
||||
"PaperDollFrame_SetRangedDamage",
|
||||
"PaperDollFrame_SetAttackBothHands",
|
||||
}
|
||||
|
||||
-- Which rows a single change moves, keyed by the event reporting it. The reference spells
|
||||
-- this out as an if/elseif ladder and then keeps a second, hand-parallel copy of the row
|
||||
-- order in OnShow; one table read two ways cannot drift the way two call lists do.
|
||||
--
|
||||
-- Only this file's registered subset appears (see PaperDollFrame_OnLoad's note on the
|
||||
-- omitted events). PLAYER_ENTERING_WORLD is registered and deliberately absent: the ref's
|
||||
-- one use for it is `CharacterModelFrame:SetUnit("player")`, and our CharacterModelFrame is
|
||||
-- a booth-texture stand-in rather than a live model widget (this file's header) — there is
|
||||
-- no SetUnit to call. It keeps its early return below rather than a row here, because the
|
||||
-- reference returns on it before even the visibility check.
|
||||
local rowsMovedBy = {
|
||||
UNIT_LEVEL = { "PaperDollFrame_SetLevel" },
|
||||
UNIT_STATS = { "PaperDollFrame_SetStats" },
|
||||
UNIT_RESISTANCES = { "PaperDollFrame_SetResistances", "PaperDollFrame_SetArmor" },
|
||||
UNIT_ATTACK = { "PaperDollFrame_SetAttackBothHands" },
|
||||
UNIT_ATTACK_POWER = { "PaperDollFrame_SetAttackPower" },
|
||||
UNIT_DAMAGE = weaponRows,
|
||||
UNIT_ATTACK_SPEED = weaponRows,
|
||||
UNIT_RANGED_ATTACK_POWER = {
|
||||
"PaperDollFrame_SetRangedAttack", "PaperDollFrame_SetRangedAttackPower",
|
||||
},
|
||||
}
|
||||
|
||||
local function paintRows(rows)
|
||||
for i = 1, table.getn(rows) do
|
||||
getglobal(rows[i])()
|
||||
end
|
||||
end
|
||||
|
||||
-- `unit` is the DECLARED second parameter, not the `arg1` global: the XML passes `arg1` into
|
||||
-- it exactly as the reference's own does (PaperDollFrame.xml), so an addon calling the 1.12
|
||||
-- arity is read correctly rather than off a stale global. Only the player's own sheet is
|
||||
-- driven from here (no pet/other-unit reuse in phase 1a), and only while it is on screen —
|
||||
-- a hidden page is repainted wholesale by OnShow when it comes back.
|
||||
function PaperDollFrame_OnEvent(event, unit)
|
||||
local frame = this
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
return
|
||||
end
|
||||
if not frame:IsVisible() then return end
|
||||
if not this:IsVisible() then return end
|
||||
if unit ~= "player" then return end
|
||||
if event == "UNIT_LEVEL" then
|
||||
PaperDollFrame_SetLevel()
|
||||
elseif event == "UNIT_DAMAGE" or event == "UNIT_ATTACK_SPEED" then
|
||||
PaperDollFrame_SetDamage()
|
||||
PaperDollFrame_SetRangedAttack()
|
||||
PaperDollFrame_SetRangedDamage()
|
||||
PaperDollFrame_SetAttackBothHands()
|
||||
elseif event == "UNIT_ATTACK" then
|
||||
PaperDollFrame_SetAttackBothHands()
|
||||
elseif event == "UNIT_RESISTANCES" then
|
||||
PaperDollFrame_SetResistances()
|
||||
PaperDollFrame_SetArmor()
|
||||
elseif event == "UNIT_STATS" then
|
||||
PaperDollFrame_SetStats()
|
||||
elseif event == "UNIT_ATTACK_POWER" then
|
||||
PaperDollFrame_SetAttackPower()
|
||||
elseif event == "UNIT_RANGED_ATTACK_POWER" then
|
||||
PaperDollFrame_SetRangedAttack()
|
||||
PaperDollFrame_SetRangedAttackPower()
|
||||
local rows = rowsMovedBy[event]
|
||||
if rows then
|
||||
paintRows(rows)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -471,16 +513,7 @@
|
||||
-- vanilla-always-false, so CharacterAmmoSlot simply never has a reason to hide; it ships
|
||||
-- with no hidden="true" at all (see its own Button below).
|
||||
function PaperDollFrame_OnShow()
|
||||
PaperDollFrame_SetLevel()
|
||||
PaperDollFrame_SetStats()
|
||||
PaperDollFrame_SetResistances()
|
||||
PaperDollFrame_SetArmor()
|
||||
PaperDollFrame_SetDamage()
|
||||
PaperDollFrame_SetAttackPower()
|
||||
PaperDollFrame_SetAttackBothHands()
|
||||
PaperDollFrame_SetRangedAttack()
|
||||
PaperDollFrame_SetRangedDamage()
|
||||
PaperDollFrame_SetRangedAttackPower()
|
||||
paintRows(paperDollRows)
|
||||
end
|
||||
-- ref PaperDollFrame_OnHide (l.587-592) is entirely commented-out in the ref itself (the
|
||||
-- ShoppingTooltip/compare-tooltip lines, OMIT) — nothing to transcribe, no OnHide wired.
|
||||
@@ -533,9 +566,20 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetResistances (l.176-228). `resistanceLevel` is a proper `local` here
|
||||
-- (the ref leaks it as an implicit global, harmless there — Lua 5.0's default — but our
|
||||
-- sandboxed globals table is worth not littering).
|
||||
-- One word for how well a school is covered, from resistance-per-level. The bands descend,
|
||||
-- so the first threshold cleared names it and a ratio of zero or less clears none of them.
|
||||
-- (The reference leaves the answer in an implicit global; a pure function of the ratio is
|
||||
-- both the honest shape and one less name in our sandboxed globals table.)
|
||||
local function resistanceBand(ratio)
|
||||
if ratio > 5 then return RESISTANCE_EXCELLENT end
|
||||
if ratio > 3.75 then return RESISTANCE_VERYGOOD end
|
||||
if ratio > 2.5 then return RESISTANCE_GOOD end
|
||||
if ratio > 1.25 then return RESISTANCE_FAIR end
|
||||
if ratio > 0 then return RESISTANCE_POOR end
|
||||
return RESISTANCE_NONE
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetResistances (l.176-228).
|
||||
function PaperDollFrame_SetResistances()
|
||||
for i = 1, BENILLA_NUM_RESISTANCE_TYPES do
|
||||
local resistId = BENILLA_MAGICRES_IDS[i]
|
||||
@@ -561,30 +605,20 @@
|
||||
end
|
||||
frame.tooltip = frame.tooltip .. FONT_COLOR_CODE_CLOSE .. " )"
|
||||
end
|
||||
-- The divisor floors at 20 because the reference does (l.210-211: it reads the level,
|
||||
-- then clamps on the NEXT line — an earlier note here cited l.210 alone and called
|
||||
-- the floor ours). The `or 1` is the only difference: the ref would error on a nil
|
||||
-- level, and nothing is gained by reproducing that.
|
||||
local unitLevel = math.max(UnitLevel("player") or 1, 20)
|
||||
local magicResistanceNumber = resistance / unitLevel
|
||||
local resistanceLevel
|
||||
if magicResistanceNumber > 5 then
|
||||
resistanceLevel = RESISTANCE_EXCELLENT
|
||||
elseif magicResistanceNumber > 3.75 then
|
||||
resistanceLevel = RESISTANCE_VERYGOOD
|
||||
elseif magicResistanceNumber > 2.5 then
|
||||
resistanceLevel = RESISTANCE_GOOD
|
||||
elseif magicResistanceNumber > 1.25 then
|
||||
resistanceLevel = RESISTANCE_FAIR
|
||||
elseif magicResistanceNumber > 0 then
|
||||
resistanceLevel = RESISTANCE_POOR
|
||||
else
|
||||
resistanceLevel = RESISTANCE_NONE
|
||||
end
|
||||
frame.tooltipSubtext = format(RESISTANCE_TOOLTIP_SUBTEXT,
|
||||
getglobal("RESISTANCE_TYPE" .. resistId), unitLevel, resistanceLevel)
|
||||
getglobal("RESISTANCE_TYPE" .. resistId), unitLevel,
|
||||
resistanceBand(resistance / unitLevel))
|
||||
end
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetArmor (l.230-250). NOTE the ref's own third UnitArmor return
|
||||
-- (`armor`) is unused here too — PaperDollFormatStat gets `base`, not `armor`; transcribed
|
||||
-- verbatim, not "fixed".
|
||||
-- (`armor`) is unused here too — PaperDollFormatStat gets `base`, not `armor`; the ref's
|
||||
-- behavior kept, not "fixed".
|
||||
function PaperDollFrame_SetArmor(unit, prefix)
|
||||
unit = unit or "player"
|
||||
prefix = prefix or "Character"
|
||||
@@ -609,17 +643,85 @@
|
||||
local base, modifier = UnitDefense(unit)
|
||||
local frame = getglobal(prefix .. "DefenseFrame")
|
||||
local text = getglobal(prefix .. "DefenseFrameStatText")
|
||||
local posBuff = 0
|
||||
local negBuff = 0
|
||||
if modifier > 0 then
|
||||
posBuff = modifier
|
||||
elseif modifier < 0 then
|
||||
negBuff = modifier
|
||||
end
|
||||
PaperDollFormatStat(DEFENSE_COLON, base, posBuff, negBuff, frame, text)
|
||||
-- The sign is all that decides which side of the pair the one modifier lands on, and a
|
||||
-- modifier of zero lands on neither.
|
||||
PaperDollFormatStat(DEFENSE_COLON, base, math.max(modifier, 0), math.min(modifier, 0),
|
||||
frame, text)
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetDamage (l.274-372).
|
||||
-- ── Weapon swings: the arithmetic and the wording, once ────────────────────────────────────
|
||||
-- Three rows read a swing — main hand, off hand, ranged — and it is the same calculation
|
||||
-- with the same vocabulary each time, so it lives here instead of two and three times over.
|
||||
|
||||
-- The sheet's green and red for damage figures. Byte-identical to Fonts.xml's GREEN/RED
|
||||
-- font-colour codes today, and kept separate on purpose: these are the damage row's own
|
||||
-- colours, and reading them off the font palette would tie one to the other.
|
||||
local DAMAGE_UP_COLOR = "|cff20ff20"
|
||||
local DAMAGE_DOWN_COLOR = "|cffff2020"
|
||||
|
||||
-- What the weapon itself swings for. The numbers we are handed already have the flat
|
||||
-- physical bonuses added and the percent multiplier applied; the row shows those, but the
|
||||
-- tooltip breaks out where they came from, so the un-modified swing has to be recovered.
|
||||
-- Returns that raw range, the modified average, and the gap the modifiers make between them.
|
||||
local function unscaleSwing(minDamage, maxDamage, bonusPos, bonusNeg, percent)
|
||||
local rawMin = (minDamage / percent) - bonusPos - bonusNeg
|
||||
local rawMax = (maxDamage / percent) - bonusPos - bonusNeg
|
||||
local baseDamage = (rawMin + rawMax) * 0.5
|
||||
local fullDamage = (baseDamage + bonusPos + bonusNeg) * percent
|
||||
return rawMin, rawMax, fullDamage, fullDamage - baseDamage
|
||||
end
|
||||
|
||||
-- Both ends of a swing, rounded outwards and floored at 1 — a weapon swinging for a
|
||||
-- fraction of a point still reads as a weapon.
|
||||
local function swingBounds(minDamage, maxDamage)
|
||||
return math.max(math.floor(minDamage), 1), math.max(math.ceil(maxDamage), 1)
|
||||
end
|
||||
|
||||
-- The range as a tooltip writes it, always spaced.
|
||||
local function damageRange(minDamage, maxDamage)
|
||||
local low, high = swingBounds(minDamage, maxDamage)
|
||||
return low .. " - " .. high
|
||||
end
|
||||
|
||||
-- The range as the ROW writes it: spaced while both ends are two digits, tightened up once
|
||||
-- either reaches three and the row starts running out of width.
|
||||
local function damageFigure(minDamage, maxDamage)
|
||||
local low, high = swingBounds(minDamage, maxDamage)
|
||||
if low < 100 and high < 100 then
|
||||
return low .. " - " .. high
|
||||
end
|
||||
return low .. "-" .. high
|
||||
end
|
||||
|
||||
-- The tooltip's account of what modified the raw range: the flat bonus, then the multiplier,
|
||||
-- each coloured by which way it went and left out entirely when it is not in play.
|
||||
local function damageModifiers(tooltip, bonusPos, bonusNeg, percent)
|
||||
if bonusPos > 0 then
|
||||
tooltip = tooltip .. DAMAGE_UP_COLOR .. " +" .. bonusPos .. "|r"
|
||||
end
|
||||
if bonusNeg < 0 then
|
||||
tooltip = tooltip .. DAMAGE_DOWN_COLOR .. " " .. bonusNeg .. "|r"
|
||||
end
|
||||
if percent > 1 then
|
||||
tooltip = tooltip .. DAMAGE_UP_COLOR .. " x" .. math.floor(percent * 100 + 0.5) .. "%|r"
|
||||
elseif percent < 1 then
|
||||
tooltip = tooltip .. DAMAGE_DOWN_COLOR .. " x" .. math.floor(percent * 100 + 0.5) .. "%|r"
|
||||
end
|
||||
return tooltip
|
||||
end
|
||||
|
||||
-- Which way a modified figure is tinted. Only asked once a caller knows something modified
|
||||
-- it, so a zero bonus never reaches here and has no colour of its own.
|
||||
local function damageColor(totalBonus)
|
||||
if totalBonus > 0 then
|
||||
return DAMAGE_UP_COLOR
|
||||
end
|
||||
return DAMAGE_DOWN_COLOR
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetDamage (l.274-372). The off hand is the same swing read twice: the
|
||||
-- flat bonuses and the multiplier are the character's, not the weapon's, so both hands take
|
||||
-- them, and only the speed and the raw range differ.
|
||||
function PaperDollFrame_SetDamage(unit, prefix)
|
||||
unit = unit or "player"
|
||||
prefix = prefix or "Character"
|
||||
@@ -628,68 +730,29 @@
|
||||
local speed, offhandSpeed = UnitAttackSpeed(unit)
|
||||
local minDamage, maxDamage, minOffHandDamage, maxOffHandDamage, physicalBonusPos, physicalBonusNeg, percent =
|
||||
UnitDamage(unit)
|
||||
local displayMin = math.max(math.floor(minDamage), 1)
|
||||
local displayMax = math.max(math.ceil(maxDamage), 1)
|
||||
minDamage = (minDamage / percent) - physicalBonusPos - physicalBonusNeg
|
||||
maxDamage = (maxDamage / percent) - physicalBonusPos - physicalBonusNeg
|
||||
local baseDamage = (minDamage + maxDamage) * 0.5
|
||||
local fullDamage = (baseDamage + physicalBonusPos + physicalBonusNeg) * percent
|
||||
local totalBonus = fullDamage - baseDamage
|
||||
local damagePerSecond = math.max(fullDamage, 1) / speed
|
||||
local damageTooltip = math.max(math.floor(minDamage), 1) .. " - " .. math.max(math.ceil(maxDamage), 1)
|
||||
local colorPos = "|cff20ff20"
|
||||
local colorNeg = "|cffff2020"
|
||||
local figure = damageFigure(minDamage, maxDamage)
|
||||
local rawMin, rawMax, fullDamage, totalBonus =
|
||||
unscaleSwing(minDamage, maxDamage, physicalBonusPos, physicalBonusNeg, percent)
|
||||
local damageTooltip = damageRange(rawMin, rawMax)
|
||||
if totalBonus == 0 then
|
||||
if displayMin < 100 and displayMax < 100 then
|
||||
damageText:SetText(displayMin .. " - " .. displayMax)
|
||||
else
|
||||
damageText:SetText(displayMin .. "-" .. displayMax)
|
||||
end
|
||||
damageText:SetText(figure)
|
||||
else
|
||||
local color
|
||||
if totalBonus > 0 then color = colorPos else color = colorNeg end
|
||||
if displayMin < 100 and displayMax < 100 then
|
||||
damageText:SetText(color .. displayMin .. " - " .. displayMax .. "|r")
|
||||
else
|
||||
damageText:SetText(color .. displayMin .. "-" .. displayMax .. "|r")
|
||||
end
|
||||
if physicalBonusPos > 0 then
|
||||
damageTooltip = damageTooltip .. colorPos .. " +" .. physicalBonusPos .. "|r"
|
||||
end
|
||||
if physicalBonusNeg < 0 then
|
||||
damageTooltip = damageTooltip .. colorNeg .. " " .. physicalBonusNeg .. "|r"
|
||||
end
|
||||
if percent > 1 then
|
||||
damageTooltip = damageTooltip .. colorPos .. " x" .. math.floor(percent * 100 + 0.5) .. "%|r"
|
||||
elseif percent < 1 then
|
||||
damageTooltip = damageTooltip .. colorNeg .. " x" .. math.floor(percent * 100 + 0.5) .. "%|r"
|
||||
end
|
||||
damageText:SetText(damageColor(totalBonus) .. figure .. "|r")
|
||||
damageTooltip =
|
||||
damageModifiers(damageTooltip, physicalBonusPos, physicalBonusNeg, percent)
|
||||
end
|
||||
damageFrame.damage = damageTooltip
|
||||
damageFrame.attackSpeed = speed
|
||||
damageFrame.dps = damagePerSecond
|
||||
damageFrame.dps = math.max(fullDamage, 1) / speed
|
||||
if offhandSpeed then
|
||||
minOffHandDamage = (minOffHandDamage / percent) - physicalBonusPos - physicalBonusNeg
|
||||
maxOffHandDamage = (maxOffHandDamage / percent) - physicalBonusPos - physicalBonusNeg
|
||||
local offhandBaseDamage = (minOffHandDamage + maxOffHandDamage) * 0.5
|
||||
local offhandFullDamage = (offhandBaseDamage + physicalBonusPos + physicalBonusNeg) * percent
|
||||
local offhandDamagePerSecond = math.max(offhandFullDamage, 1) / offhandSpeed
|
||||
local offhandDamageTooltip =
|
||||
math.max(math.floor(minOffHandDamage), 1) .. " - " .. math.max(math.ceil(maxOffHandDamage), 1)
|
||||
if physicalBonusPos > 0 then
|
||||
offhandDamageTooltip = offhandDamageTooltip .. colorPos .. " +" .. physicalBonusPos .. "|r"
|
||||
end
|
||||
if physicalBonusNeg < 0 then
|
||||
offhandDamageTooltip = offhandDamageTooltip .. colorNeg .. " " .. physicalBonusNeg .. "|r"
|
||||
end
|
||||
if percent > 1 then
|
||||
offhandDamageTooltip = offhandDamageTooltip .. colorPos .. " x" .. math.floor(percent * 100 + 0.5) .. "%|r"
|
||||
elseif percent < 1 then
|
||||
offhandDamageTooltip = offhandDamageTooltip .. colorNeg .. " x" .. math.floor(percent * 100 + 0.5) .. "%|r"
|
||||
end
|
||||
damageFrame.offhandDamage = offhandDamageTooltip
|
||||
local offMin, offMax, offhandFullDamage = unscaleSwing(
|
||||
minOffHandDamage, maxOffHandDamage, physicalBonusPos, physicalBonusNeg, percent)
|
||||
-- The off hand gets its breakdown unconditionally: it has no row of its own to keep
|
||||
-- plain, only the tooltip's second half.
|
||||
damageFrame.offhandDamage = damageModifiers(damageRange(offMin, offMax),
|
||||
physicalBonusPos, physicalBonusNeg, percent)
|
||||
damageFrame.offhandAttackSpeed = offhandSpeed
|
||||
damageFrame.offhandDps = offhandDamagePerSecond
|
||||
damageFrame.offhandDps = math.max(offhandFullDamage, 1) / offhandSpeed
|
||||
else
|
||||
damageFrame.offhandAttackSpeed = nil
|
||||
end
|
||||
@@ -707,6 +770,26 @@
|
||||
math.max(base + posBuff + negBuff, 0) / ATTACK_POWER_MAGIC_NUMBER)
|
||||
end
|
||||
|
||||
-- An attack rating and whatever is moving it, on one row. Nothing moving it means the bare
|
||||
-- number and no hover plate to explain; otherwise the row carries the modified TOTAL, tinted
|
||||
-- by which way it went, and the plate carries the split that produced it. Both the melee row
|
||||
-- and the ranged row read exactly this way.
|
||||
local function setAttackRating(frame, text, base, modifier)
|
||||
if modifier == 0 then
|
||||
text:SetText(base)
|
||||
frame.tooltip = nil
|
||||
return
|
||||
end
|
||||
local color = RED_FONT_COLOR_CODE
|
||||
local sign = " "
|
||||
if modifier > 0 then
|
||||
color = GREEN_FONT_COLOR_CODE
|
||||
sign = " +"
|
||||
end
|
||||
frame.tooltip = base .. color .. sign .. modifier .. FONT_COLOR_CODE_CLOSE
|
||||
text:SetText(color .. (base + modifier) .. FONT_COLOR_CODE_CLOSE)
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetAttackBothHands (l.391-417) — the weapon-skill line (decision 0208's
|
||||
-- own "to confirm": the equipped-weapon subclass -> skill-id table is UnitAttackBothHands's
|
||||
-- own concern, not this file's).
|
||||
@@ -714,21 +797,8 @@
|
||||
unit = unit or "player"
|
||||
prefix = prefix or "Character"
|
||||
local mainHandAttackBase, mainHandAttackMod = UnitAttackBothHands(unit)
|
||||
local frame = getglobal(prefix .. "AttackFrame")
|
||||
local text = getglobal(prefix .. "AttackFrameStatText")
|
||||
if mainHandAttackMod == 0 then
|
||||
text:SetText(mainHandAttackBase)
|
||||
frame.tooltip = nil
|
||||
else
|
||||
local color = RED_FONT_COLOR_CODE
|
||||
if mainHandAttackMod > 0 then
|
||||
color = GREEN_FONT_COLOR_CODE
|
||||
frame.tooltip = mainHandAttackBase .. color .. " +" .. mainHandAttackMod .. FONT_COLOR_CODE_CLOSE
|
||||
else
|
||||
frame.tooltip = mainHandAttackBase .. color .. " " .. mainHandAttackMod .. FONT_COLOR_CODE_CLOSE
|
||||
end
|
||||
text:SetText(color .. (mainHandAttackBase + mainHandAttackMod) .. FONT_COLOR_CODE_CLOSE)
|
||||
end
|
||||
setAttackRating(getglobal(prefix .. "AttackFrame"),
|
||||
getglobal(prefix .. "AttackFrameStatText"), mainHandAttackBase, mainHandAttackMod)
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetRangedAttack (l.419-466). `hasRelic`/UnitHasRelicSlot DROPPED
|
||||
@@ -758,19 +828,7 @@
|
||||
PaperDollFrame_SetRangedDamage()
|
||||
end
|
||||
if not rangedTexture then return end
|
||||
if rangedAttackMod == 0 then
|
||||
text:SetText(rangedAttackBase)
|
||||
frame.tooltip = nil
|
||||
else
|
||||
local color = RED_FONT_COLOR_CODE
|
||||
if rangedAttackMod > 0 then
|
||||
color = GREEN_FONT_COLOR_CODE
|
||||
frame.tooltip = rangedAttackBase .. color .. " +" .. rangedAttackMod .. FONT_COLOR_CODE_CLOSE
|
||||
else
|
||||
frame.tooltip = rangedAttackBase .. color .. " " .. rangedAttackMod .. FONT_COLOR_CODE_CLOSE
|
||||
end
|
||||
text:SetText(color .. (rangedAttackBase + rangedAttackMod) .. FONT_COLOR_CODE_CLOSE)
|
||||
end
|
||||
setAttackRating(frame, text, rangedAttackBase, rangedAttackMod)
|
||||
end
|
||||
|
||||
-- ref PaperDollFrame_SetRangedDamage (l.468-537).
|
||||
@@ -788,42 +846,18 @@
|
||||
end
|
||||
local rangedAttackSpeed, minDamage, maxDamage, physicalBonusPos, physicalBonusNeg, percent =
|
||||
UnitRangedDamage(unit)
|
||||
local displayMin = math.max(math.floor(minDamage), 1)
|
||||
local displayMax = math.max(math.ceil(maxDamage), 1)
|
||||
minDamage = (minDamage / percent) - physicalBonusPos - physicalBonusNeg
|
||||
maxDamage = (maxDamage / percent) - physicalBonusPos - physicalBonusNeg
|
||||
local baseDamage = (minDamage + maxDamage) * 0.5
|
||||
local fullDamage = (baseDamage + physicalBonusPos + physicalBonusNeg) * percent
|
||||
local totalBonus = fullDamage - baseDamage
|
||||
local figure = damageFigure(minDamage, maxDamage)
|
||||
local rawMin, rawMax, fullDamage, totalBonus =
|
||||
unscaleSwing(minDamage, maxDamage, physicalBonusPos, physicalBonusNeg, percent)
|
||||
local damagePerSecond = math.max(fullDamage, 1) / rangedAttackSpeed
|
||||
local tooltip = math.max(math.floor(minDamage), 1) .. " - " .. math.max(math.ceil(maxDamage), 1)
|
||||
local tooltip = damageRange(rawMin, rawMax)
|
||||
if totalBonus == 0 then
|
||||
if displayMin < 100 and displayMax < 100 then
|
||||
damageText:SetText(displayMin .. " - " .. displayMax)
|
||||
else
|
||||
damageText:SetText(displayMin .. "-" .. displayMax)
|
||||
end
|
||||
damageText:SetText(figure)
|
||||
else
|
||||
local colorPos = "|cff20ff20"
|
||||
local colorNeg = "|cffff2020"
|
||||
local color
|
||||
if totalBonus > 0 then color = colorPos else color = colorNeg end
|
||||
if displayMin < 100 and displayMax < 100 then
|
||||
damageText:SetText(color .. displayMin .. " - " .. displayMax .. "|r")
|
||||
else
|
||||
damageText:SetText(color .. displayMin .. "-" .. displayMax .. "|r")
|
||||
end
|
||||
if physicalBonusPos > 0 then
|
||||
tooltip = tooltip .. colorPos .. " +" .. physicalBonusPos .. "|r"
|
||||
end
|
||||
if physicalBonusNeg < 0 then
|
||||
tooltip = tooltip .. colorNeg .. " " .. physicalBonusNeg .. "|r"
|
||||
end
|
||||
if percent > 1 then
|
||||
tooltip = tooltip .. colorPos .. " x" .. math.floor(percent * 100 + 0.5) .. "%|r"
|
||||
elseif percent < 1 then
|
||||
tooltip = tooltip .. colorNeg .. " x" .. math.floor(percent * 100 + 0.5) .. "%|r"
|
||||
end
|
||||
damageText:SetText(damageColor(totalBonus) .. figure .. "|r")
|
||||
tooltip = damageModifiers(tooltip, physicalBonusPos, physicalBonusNeg, percent)
|
||||
-- Only the modified case gets a hover plate here: an unmodified bow has nothing to
|
||||
-- explain beyond the figure already on the row.
|
||||
damageFrame.tooltip = tooltip .. " " .. format(DPS_TEMPLATE, damagePerSecond)
|
||||
end
|
||||
damageFrame.attackSpeed = rangedAttackSpeed
|
||||
@@ -988,7 +1022,7 @@
|
||||
BenillaPaperDollSlot_Update(button)
|
||||
end
|
||||
|
||||
-- Left: the reference's modifier fork verbatim (ref l.647-660) — CTRL previews the worn
|
||||
-- Left: the reference's own modifier fork (ref l.647-660) — CTRL previews the worn
|
||||
-- item in the dressing room (decision 1060), SHIFT posts its link into an open chat edit
|
||||
-- box (decision 1059), and neither falls through to a pickup a real modified click would
|
||||
-- never trigger. Plain click: PickupInventoryItem (pick up / place / swap / cancel,
|
||||
@@ -1191,9 +1225,9 @@
|
||||
-- UIParent.lua:2), against which the 0.03 click nudge above is a ~1.7° tap. Every 1.12 model
|
||||
-- pane wires it (PaperDollFrame l.222-224, DressUpFrame.xml:202-204, TabardFrame.xml:406-408).
|
||||
-- It used to be omitted here for want of `GetButtonState` — that method now answers "PUSHED"
|
||||
-- for a LIVE mouse-held button (decision 0288 P3), so the ref's own loop ships verbatim.
|
||||
-- Verbatim INCLUDES the ref's sign quirk: the held branches run OPPOSITE to the click helpers
|
||||
-- (held-LEFT adds, held-RIGHT subtracts). Not a transcription slip — quoted, not re-derived.
|
||||
-- for a LIVE mouse-held button (decision 0288 P3), so the ref's own loop ships here.
|
||||
-- That INCLUDES the ref's sign quirk: the held branches run OPPOSITE to the click helpers
|
||||
-- (held-LEFT adds, held-RIGHT subtracts). Not a slip of ours — the ref's own, not re-derived.
|
||||
function BenillaPaperDollModel_OnUpdate(frame, elapsed)
|
||||
local step = elapsed * 2 * PI * ROTATIONS_PER_SECOND
|
||||
if getglobal(frame:GetName() .. "RotateLeftButton"):GetButtonState() == "PUSHED" then
|
||||
@@ -1373,7 +1407,7 @@
|
||||
l.65-78), on the minimize-button art this codebase's close buttons all use (no
|
||||
UIPanelCloseButton template ships — Gossip/Bag/Merchant's own header comments).
|
||||
OnLoad raises the frame level +1 (the same idiom CharacterNameFrame above uses,
|
||||
transcribed from the ref): the window's own art is PaperDollFrame's BACKGROUND
|
||||
the ref's own): the window's own art is PaperDollFrame's BACKGROUND
|
||||
layer — a full-window sibling created AFTER this button — so at the shared default
|
||||
level 0 the page's art paints over the button by creation order (order.rs). The ref's
|
||||
close button rides the client's show-time bucket-tail re-insertion instead; benilla
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
chatType("BATTLEGROUND", 1, 93, 255, 127, 0)
|
||||
chatType("BATTLEGROUND_LEADER", 0, 94, 255, 219, 183)
|
||||
|
||||
-- ── BenillaFCF: the FloatingChatFrame behaviors, transcribed (ref FloatingChatFrame.lua)
|
||||
-- ── BenillaFCF: the FloatingChatFrame behaviors, re-expressed (ref FloatingChatFrame.lua)
|
||||
-- Quoted constants: CHAT_TAB_SHOW_DELAY=0.2, CHAT_FRAME_FADE_TIME=0.15,
|
||||
-- DEFAULT_CHATFRAME_ALPHA=0.25, CHAT_BUTTON_FLASH_TIME=0.5,
|
||||
-- MESSAGE_SCROLLBUTTON_INITIAL_DELAY=0, MESSAGE_SCROLLBUTTON_SCROLL_DELAY=0.05.
|
||||
@@ -328,7 +328,7 @@
|
||||
BenillaFCF.flashLeft = 60
|
||||
end
|
||||
|
||||
-- PanelTemplates_TabResize(5, tab), transcribed: the ref sizes every docked tab to its
|
||||
-- PanelTemplates_TabResize(5, tab), re-expressed: the ref sizes every docked tab to its
|
||||
-- label (FCF_DockUpdate) — middle slice = text width + 5, tab = middle + the two 16px
|
||||
-- caps; the flash/highlight bands follow the Left/Right pieces by anchor. The label
|
||||
-- width is host-measured (GetStringWidth), so early frames may see 0 — the OnUpdate
|
||||
@@ -470,7 +470,7 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ── BenillaUIMenu: the 1.12 UIMenu kit, transcribed (ref UIMenu.lua — the chat menu's
|
||||
-- ── BenillaUIMenu: the 1.12 UIMenu kit, re-expressed (ref UIMenu.lua — the chat menu's
|
||||
-- row engine: fixed 16px rows in a tooltip-backdrop box, a 2 s no-hover timeout that
|
||||
-- walks up parentMenu chains, nested submenus opened on row hover). Constants quoted.
|
||||
-- Scoped here because the chat menu is its only consumer; a second consumer graduates
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<Ui>
|
||||
<!--
|
||||
Interface\FrameXML\ColorPickerFrame.xml, transcribed — the shared colour dialog. Line
|
||||
numbers below are that file's (336 lines, XML only; it ships no `.lua`, its whole script is
|
||||
inline and the one function that drives it, `UIDropDownMenuButton_OpenColorPicker`, lives in
|
||||
UIDropDownMenu.lua).
|
||||
The shared colour dialog — our own implementation of the window that
|
||||
Interface\FrameXML\ColorPickerFrame.xml specifies. Line numbers below are that file's (336
|
||||
lines, XML only; it ships no `.lua`, its whole script is inline and the one function that
|
||||
drives it, `UIDropDownMenuButton_OpenColorPicker`, lives in UIDropDownMenu.lua). It is
|
||||
cited throughout as the SPECIFICATION — for the names, the geometry, the sounds and the
|
||||
call order, all of which are the API addons are written against and none of which we are
|
||||
free to invent. The Lua in this file is ours.
|
||||
|
||||
WHY IT IS HERE, in the arc's own terms: **86 of the 218 corpus addons reach
|
||||
`ColorPickerFrame`, 80 reach `OpacitySliderFrame`** — and 1207's lesson applies before those
|
||||
@@ -139,17 +142,26 @@
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- ref l.60-77. Cancel is declared FIRST because Okay anchors to it. Its OnClick is the
|
||||
reference's, verbatim: hide, then hand the addon back what it had. -->
|
||||
<!-- ref l.60-77 for the name, the geometry and the sound. Cancel is declared FIRST
|
||||
because Okay anchors to it. -->
|
||||
<Button name="ColorPickerCancelButton" inherits="GameMenuButtonTemplate" text="CANCEL">
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-10" y="10"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
-- Dismiss first, undo second — the undo is the CALLER's code, and a
|
||||
-- cancelFunc that opens a window of its own should not have to fight this
|
||||
-- one for the panel slot.
|
||||
HideUIPanel(this:GetParent());
|
||||
if ( ColorPickerFrame.cancelFunc ) then
|
||||
ColorPickerFrame.cancelFunc(ColorPickerFrame.previousValues);
|
||||
-- This window remembers nothing itself: `previousValues` is the snapshot
|
||||
-- whoever opened it left behind (`UIDropDownMenuButton_OpenColorPicker`
|
||||
-- fills it in; Dewdrop closes over its own copy and ignores it) and
|
||||
-- `cancelFunc` is the caller's restore. Both are optional fields on the
|
||||
-- frame, so a caller that set neither simply gets the window closed.
|
||||
local cancelFunc = ColorPickerFrame.cancelFunc;
|
||||
if ( cancelFunc ) then
|
||||
cancelFunc(ColorPickerFrame.previousValues);
|
||||
end
|
||||
PlaySound("igMainMenuOptionCheckBoxOn");
|
||||
</OnClick>
|
||||
@@ -166,9 +178,16 @@
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
HideUIPanel(this:GetParent());
|
||||
-- Commit. Neither callback is handed the values: both read the widget and
|
||||
-- the slider for themselves, which is the contract every corpus caller is
|
||||
-- written against. `func` is deliberately NOT guarded (header) — a caller
|
||||
-- that opened the window without one should hear about it. The alpha half
|
||||
-- genuinely is optional: a caller that never asked for opacity has no
|
||||
-- reason to supply one.
|
||||
ColorPickerFrame.func();
|
||||
if ( ColorPickerFrame.opacityFunc ) then
|
||||
ColorPickerFrame.opacityFunc();
|
||||
local opacityFunc = ColorPickerFrame.opacityFunc;
|
||||
if ( opacityFunc ) then
|
||||
opacityFunc();
|
||||
end
|
||||
PlaySound("igMainMenuOptionCheckBoxOn");
|
||||
</OnClick>
|
||||
@@ -209,8 +228,11 @@
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnValueChanged>
|
||||
if ( ColorPickerFrame.opacityFunc ) then
|
||||
ColorPickerFrame.opacityFunc();
|
||||
-- Live preview: every step of the drag reaches the caller, which is what
|
||||
-- makes the alpha follow the thumb instead of jumping at Okay.
|
||||
local opacityFunc = ColorPickerFrame.opacityFunc;
|
||||
if ( opacityFunc ) then
|
||||
opacityFunc();
|
||||
end
|
||||
</OnValueChanged>
|
||||
</Scripts>
|
||||
@@ -220,17 +242,23 @@
|
||||
</Slider>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<!-- ref l.161-170. The opacity slider is opt-in per caller, and the window narrows by
|
||||
the slider's 60 units when nobody asked for it. -->
|
||||
<!-- ref l.161-170 for the two widths. The opacity slider is opt-in per caller, and the
|
||||
window narrows by the slider's 60 units when nobody asked for it. -->
|
||||
<OnShow>
|
||||
-- Seeding the slider AFTER showing it is deliberate: SetValue fires
|
||||
-- OnValueChanged, so the caller's opacityFunc runs against a slider that is
|
||||
-- already on screen and against a window still at its old width — the reference's
|
||||
-- own order, and observable to any addon whose preview writes through. The width
|
||||
-- is settled once, at the end, instead of in both arms.
|
||||
local width = 305;
|
||||
if ( this.hasOpacity ) then
|
||||
OpacitySliderFrame:Show();
|
||||
OpacitySliderFrame:SetValue(this.opacity);
|
||||
this:SetWidth(365);
|
||||
width = 365;
|
||||
else
|
||||
OpacitySliderFrame:Hide();
|
||||
this:SetWidth(305);
|
||||
end
|
||||
this:SetWidth(width);
|
||||
</OnShow>
|
||||
<!-- ref l.171-176. The widget's own slot, fired by SetColorRGB with the colour the
|
||||
widget now holds (colorselect.rs). This is the ONLY thing that paints the swatch —
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The center-screen scrolling combat text (decision 0578) — the 1.11+ "Floating Combat Text"
|
||||
option. TRANSCRIBED from the shipped load-on-demand addon `Blizzard_CombatText`
|
||||
(patch.MPQ `Interface\AddOns\Blizzard_CombatText\`, extracted to
|
||||
WoW/_extracted_addons/Blizzard_CombatText/): screen-space FontStrings scrolling up from
|
||||
UIParent-bottom +384 (≈ over the character), driven by the engine events the addon
|
||||
registers. Deviations from the ref, each named:
|
||||
option, written here in our own terms. The shipped load-on-demand addon
|
||||
`Blizzard_CombatText` (patch.MPQ `Interface\AddOns\Blizzard_CombatText\`, extracted to
|
||||
WoW/_extracted_addons/Blizzard_CombatText/) is the SPECIFICATION, and what it fixes is
|
||||
contract: the message-type keys, the option family, the scroll/fade/crit envelope and
|
||||
every number in them are all addressed by name from addons, so they are reproduced exactly
|
||||
while the code that carries them is ours. The picture: screen-space FontStrings scrolling
|
||||
up from UIParent-bottom +384 (≈ over the character), driven by the engine events this
|
||||
frame registers. Where our behaviour departs from the spec, it is named:
|
||||
- The ref is load-on-demand (`UIParentLoadAddOn` when SHOW_COMBAT_TEXT == "1"); we load
|
||||
always and let CombatText_UpdateDisplayedMessages gate registration, ref-identically.
|
||||
- SHOW_COMBAT_TEXT defaults "1" here (ref "0") — the director's ask IS the enabled
|
||||
@@ -20,63 +23,91 @@
|
||||
gates, not this file (0875).
|
||||
- Lua 5.0 `for k,v in t` iteration becomes explicit index loops (5.1 VM), backwards
|
||||
where the body removes.
|
||||
- COMBAT_TEXT_TYPE_INFO is grown from a compact spec (colour · gate · flags · the types
|
||||
that share them) walked by the loop under it, instead of one literal per row: the same
|
||||
facts with the repetition taken out. The table it produces IS the contract — the same
|
||||
keys with the same values, and one fresh entry table per key, because `show` is
|
||||
rewritten per entry and an addon may edit any of them.
|
||||
- The two low-resource warnings, the two flow directions in CombatText_AddMessage, and the
|
||||
"park a string back at the seat" step all appear once here; the spec writes each of them
|
||||
out twice.
|
||||
The wire → messageType mapping feeding COMBAT_TEXT_UPDATE lives app-side
|
||||
(net/apply/combat_log.rs + ui_unit.rs) and is PROVISIONAL pending the dispatched wow-re
|
||||
emission pin (decision 0578).
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- ── The pool ────────────────────────────────────────────────────────────────────────────
|
||||
-- One screen line per pool string, and the list of the ones currently in flight. The pool
|
||||
-- is fixed: a message arriving with all twenty busy evicts the oldest.
|
||||
NUM_COMBAT_TEXT_LINES = 20
|
||||
COMBAT_TEXT_SCROLLSPEED = 1.9
|
||||
COMBAT_TEXT_FADEOUT_TIME = 1.3
|
||||
COMBAT_TEXT_HEIGHT = 25
|
||||
COMBAT_TEXT_CRIT_MAXHEIGHT = 60
|
||||
COMBAT_TEXT_CRIT_MINHEIGHT = 30
|
||||
COMBAT_TEXT_CRIT_SCALE_TIME = 0.05
|
||||
COMBAT_TEXT_CRIT_SHRINKTIME = 0.2
|
||||
COMBAT_TEXT_TO_ANIMATE = {}
|
||||
COMBAT_TEXT_STAGGER_RANGE = 20
|
||||
|
||||
-- ── Where a line sits ───────────────────────────────────────────────────────────────────
|
||||
-- COMBAT_TEXT_LOCATIONS is the seat and the target the current float mode implies; it is
|
||||
-- filled in by CombatText_UpdateDisplayedMessages, not here. A new line is pushed clear of
|
||||
-- everything already in flight by one line plus COMBAT_TEXT_SPACING, but the stack never
|
||||
-- runs further than COMBAT_TEXT_MAX_OFFSET from the seat — past that it starts over. The
|
||||
-- staggered types jitter sideways by up to half COMBAT_TEXT_STAGGER_RANGE either way, so a
|
||||
-- flurry of hits does not print as one column.
|
||||
COMBAT_TEXT_LOCATIONS = {}
|
||||
COMBAT_TEXT_SPACING = 10
|
||||
COMBAT_TEXT_MAX_OFFSET = 130
|
||||
COMBAT_TEXT_STAGGER_RANGE = 20
|
||||
|
||||
-- ── How long a line lives ───────────────────────────────────────────────────────────────
|
||||
-- It travels for COMBAT_TEXT_SCROLLSPEED seconds; COMBAT_TEXT_FADEOUT_TIME is when the
|
||||
-- fade STARTS, not how long it takes, so the fade is the tail of the same window.
|
||||
COMBAT_TEXT_SCROLLSPEED = 1.9
|
||||
COMBAT_TEXT_FADEOUT_TIME = 1.3
|
||||
|
||||
-- ── How big it is drawn ─────────────────────────────────────────────────────────────────
|
||||
-- An ordinary line is COMBAT_TEXT_HEIGHT and stays there. A crit "pops": seeded at the
|
||||
-- minimum, grown to the maximum across CRIT_SCALE_TIME, and back down to ordinary size by
|
||||
-- CRIT_SHRINKTIME (CombatText_OnUpdate walks that envelope).
|
||||
COMBAT_TEXT_HEIGHT = 25
|
||||
COMBAT_TEXT_CRIT_MINHEIGHT = 30
|
||||
COMBAT_TEXT_CRIT_MAXHEIGHT = 60
|
||||
COMBAT_TEXT_CRIT_SCALE_TIME = 0.05
|
||||
COMBAT_TEXT_CRIT_SHRINKTIME = 0.2
|
||||
|
||||
-- ── When the low-resource warnings latch ────────────────────────────────────────────────
|
||||
-- Fractions of the bar, tested on the way down; see CombatText_LowResourceWarning.
|
||||
COMBAT_TEXT_LOW_HEALTH_THRESHOLD = 0.2
|
||||
COMBAT_TEXT_LOW_MANA_THRESHOLD = 0.2
|
||||
COMBAT_TEXT_LOCATIONS = {}
|
||||
|
||||
-- The option family's saved-variables (ref UIOptionsFrame.lua l.125-152), at the ref
|
||||
-- defaults except the master (see the header). Each is assigned AT FILE SCOPE and
|
||||
-- registered on the line under it — the reference's own load order (decision 1128):
|
||||
-- defaults first, the saved chunk over the top, consumers third; reverse any two and a
|
||||
-- saved value can never win. The Options window's Combat page drives all fourteen
|
||||
-- (decision 1134) and takes its Defaults values from these very assignments, read at the
|
||||
-- row's OnLoad — which runs before the saved chunk — so each default lives in one place,
|
||||
-- here, and the panel cannot drift from it.
|
||||
SHOW_COMBAT_TEXT = "1"
|
||||
RegisterForSave("SHOW_COMBAT_TEXT")
|
||||
COMBAT_TEXT_SHOW_LOW_HEALTH_MANA = "1"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_LOW_HEALTH_MANA")
|
||||
COMBAT_TEXT_SHOW_AURAS = "1"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_AURAS")
|
||||
COMBAT_TEXT_SHOW_AURA_FADE = "0"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_AURA_FADE")
|
||||
COMBAT_TEXT_SHOW_COMBAT_STATE = "1"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_COMBAT_STATE")
|
||||
COMBAT_TEXT_SHOW_DODGE_PARRY_MISS = "0"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_DODGE_PARRY_MISS")
|
||||
COMBAT_TEXT_SHOW_RESISTANCES = "0"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_RESISTANCES")
|
||||
COMBAT_TEXT_SHOW_REPUTATION = "0"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_REPUTATION")
|
||||
COMBAT_TEXT_SHOW_REACTIVES = "0"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_REACTIVES")
|
||||
COMBAT_TEXT_SHOW_FRIENDLY_NAMES = "0"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_FRIENDLY_NAMES")
|
||||
COMBAT_TEXT_SHOW_COMBO_POINTS = "0"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_COMBO_POINTS")
|
||||
COMBAT_TEXT_SHOW_MANA = "0"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_MANA")
|
||||
COMBAT_TEXT_SHOW_HONOR_GAINED = "1"
|
||||
RegisterForSave("COMBAT_TEXT_SHOW_HONOR_GAINED")
|
||||
COMBAT_TEXT_FLOAT_MODE = "1"
|
||||
RegisterForSave("COMBAT_TEXT_FLOAT_MODE")
|
||||
-- The option family: every saved global this feature owns, next to the value it ships
|
||||
-- with. All fourteen sit at the spec's defaults (UIOptionsFrame.lua l.69-84) except the
|
||||
-- master SHOW_COMBAT_TEXT, which ships "1" against the spec's "0" — the director's ask IS
|
||||
-- the enabled experience (see the header).
|
||||
--
|
||||
-- Each name is assigned AT FILE SCOPE and registered for saving in the same pass, because
|
||||
-- the load order is what makes a saved value stick (decision 1128): defaults first, the
|
||||
-- saved chunk over the top, consumers third; reverse any two and a saved value can never
|
||||
-- win. The Options window's Combat page drives all fourteen (decision 1134) and takes its
|
||||
-- "Defaults" from these very globals, read at each row's OnLoad — which runs before the
|
||||
-- saved chunk — so a default lives in exactly one place, this table, and the panel cannot
|
||||
-- drift from it.
|
||||
local combatTextOptions = {
|
||||
{"SHOW_COMBAT_TEXT", "1"},
|
||||
{"COMBAT_TEXT_SHOW_LOW_HEALTH_MANA", "1"},
|
||||
{"COMBAT_TEXT_SHOW_AURAS", "1"},
|
||||
{"COMBAT_TEXT_SHOW_AURA_FADE", "0"},
|
||||
{"COMBAT_TEXT_SHOW_COMBAT_STATE", "1"},
|
||||
{"COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", "0"},
|
||||
{"COMBAT_TEXT_SHOW_RESISTANCES", "0"},
|
||||
{"COMBAT_TEXT_SHOW_REPUTATION", "0"},
|
||||
{"COMBAT_TEXT_SHOW_REACTIVES", "0"},
|
||||
{"COMBAT_TEXT_SHOW_FRIENDLY_NAMES", "0"},
|
||||
{"COMBAT_TEXT_SHOW_COMBO_POINTS", "0"},
|
||||
{"COMBAT_TEXT_SHOW_MANA", "0"},
|
||||
{"COMBAT_TEXT_SHOW_HONOR_GAINED", "1"},
|
||||
{"COMBAT_TEXT_FLOAT_MODE", "1"},
|
||||
}
|
||||
for i = 1, getn(combatTextOptions) do
|
||||
local name, default = combatTextOptions[i][1], combatTextOptions[i][2]
|
||||
setglobal(name, default)
|
||||
RegisterForSave(name)
|
||||
end
|
||||
|
||||
-- The message words (shipped enUS GlobalStrings values, hardcoded like the rest of our
|
||||
-- enUS-only data; or-guarded so an earlier definition wins).
|
||||
@@ -108,66 +139,126 @@
|
||||
COMBAT_TEXT_HONOR_GAINED = COMBAT_TEXT_HONOR_GAINED or "Honor %s"
|
||||
|
||||
--[[
|
||||
List of COMBAT_TEXT_TYPE_INFO attributes (ref comment, verbatim)
|
||||
======================================================
|
||||
r, g, b = [floats] -- The floating text color
|
||||
show = [nil, 1] -- Display this message type in the UI
|
||||
isStaggered = [nil, 1] -- Randomly stagger these messages from left to right
|
||||
var = [nil, 1] -- This messageType is shown if this variable resolves to "1"
|
||||
COMBAT_TEXT_TYPE_INFO — one entry per message type, and the four things an entry says:
|
||||
|
||||
r, g, b the colour the line prints in, as 0..1 floats
|
||||
show 1 while this type is currently being printed, nil while it is not.
|
||||
CombatText_UpdateDisplayedMessages rewrites it for every gated type from
|
||||
the option `var` names, so an entry carrying `var` and no `show` has
|
||||
merely not been refreshed yet, while an entry carrying `show` and no
|
||||
`var` is on for as long as the feature itself is
|
||||
isStaggered 1 to jitter the line sideways instead of printing it on the centre line
|
||||
var the name of the saved global that gates this type; nil = ungated
|
||||
|
||||
This table is API: addons index it by message-type key and read and write these fields, so
|
||||
the keys and the values are exactly the specification's. The rows below are OURS — almost
|
||||
every type shares its colour and its gate with several others, so the facts are written
|
||||
once per group and the loop underneath expands them into one entry per type. Fresh table
|
||||
per entry, never a shared one: `show` is rewritten per entry, and an addon may edit any
|
||||
single entry without that leaking into its neighbours.
|
||||
]]
|
||||
|
||||
-- The six colours the whole table draws from.
|
||||
local CT_WHITE = {1, 1, 1}
|
||||
local CT_RED = {1, 0.1, 0.1}
|
||||
local CT_GREEN = {0.1, 1, 0.1}
|
||||
local CT_BLUE = {0.1, 0.1, 1}
|
||||
local CT_VIOLET = {0.79, 0.3, 0.85}
|
||||
local CT_GOLD = {1, 0.82, 0}
|
||||
|
||||
-- Colour · gate · flags · the message types that share them. `always` is the `show` an
|
||||
-- ungated type is born with; `jitter` is `isStaggered`; `gate` is `var`.
|
||||
local combatTextSpec = {
|
||||
-- Always on. Only the master toggle takes these away.
|
||||
{colour = CT_RED, always = 1, jitter = 1, types = {"DAMAGE"}},
|
||||
{colour = CT_RED, always = 1, types = {"DAMAGE_CRIT"}},
|
||||
{colour = CT_VIOLET, always = 1, types = {"SPELL_DAMAGE"}},
|
||||
{colour = CT_WHITE, always = 1, types = {"SPLIT_DAMAGE"}},
|
||||
{colour = CT_GREEN, always = 1, types = {"HEAL", "HEAL_CRIT", "PERIODIC_HEAL", "SPELL_CAST"}},
|
||||
|
||||
-- Never on by themselves: no `always`, no gate, so `show` is nil and stays nil. They
|
||||
-- carry a colour so that anything which does switch one on has one ready.
|
||||
{colour = CT_WHITE, types = {"INTERRUPT", "ENCHANTMENT_ADDED", "ENCHANTMENT_REMOVED",
|
||||
"DISPEL_FAILED", "SPELL_CAST_START", "SPELL_DISPELLED", "DAMAGE_SHIELD",
|
||||
"EXTRA_ATTACKS"}},
|
||||
|
||||
-- "Show dodge/parry/miss": swings that went nowhere, melee in red and spells in white.
|
||||
-- The melee whiffs jitter because several can land in the same instant.
|
||||
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS", jitter = 1,
|
||||
types = {"MISS", "DODGE", "PARRY", "EVADE"}},
|
||||
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS",
|
||||
types = {"IMMUNE", "DEFLECT"}},
|
||||
{colour = CT_WHITE, gate = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS",
|
||||
types = {"SPELL_MISSED", "SPELL_DODGED", "SPELL_PARRIED", "SPELL_EVADED",
|
||||
"SPELL_IMMUNE", "SPELL_DEFLECTED", "SPELL_REFLECTED"}},
|
||||
|
||||
-- "Show resistances": a hit that landed but was partly eaten.
|
||||
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_RESISTANCES",
|
||||
types = {"BLOCK", "ABSORB", "RESIST", "PROC_RESISTED"}},
|
||||
{colour = CT_WHITE, gate = "COMBAT_TEXT_SHOW_RESISTANCES", types = {"SPELL_BLOCKED"}},
|
||||
{colour = CT_VIOLET, gate = "COMBAT_TEXT_SHOW_RESISTANCES",
|
||||
types = {"SPELL_ABSORBED", "SPELL_RESISTED"}},
|
||||
|
||||
-- The aura pair — what landed, and what wore off — each split by whose side it helps.
|
||||
{colour = CT_GREEN, gate = "COMBAT_TEXT_SHOW_AURAS", types = {"AURA_START"}},
|
||||
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_AURAS", types = {"AURA_START_HARMFUL"}},
|
||||
{colour = CT_GREEN, gate = "COMBAT_TEXT_SHOW_AURA_FADE", types = {"AURA_END"}},
|
||||
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_AURA_FADE", types = {"AURA_END_HARMFUL"}},
|
||||
|
||||
-- Resources and standing: everything you gained rather than took.
|
||||
{colour = CT_BLUE, gate = "COMBAT_TEXT_SHOW_MANA",
|
||||
types = {"MANA", "RAGE", "FOCUS", "ENERGY"}},
|
||||
{colour = CT_BLUE, gate = "COMBAT_TEXT_SHOW_COMBO_POINTS", types = {"COMBO_POINTS"}},
|
||||
{colour = CT_BLUE, gate = "COMBAT_TEXT_SHOW_REPUTATION", types = {"FACTION"}},
|
||||
{colour = CT_BLUE, gate = "COMBAT_TEXT_SHOW_HONOR_GAINED", types = {"HONOR_GAINED"}},
|
||||
|
||||
-- The state warnings, each on its own switch.
|
||||
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_LOW_HEALTH_MANA",
|
||||
types = {"HEALTH_LOW", "MANA_LOW"}},
|
||||
{colour = CT_RED, gate = "COMBAT_TEXT_SHOW_COMBAT_STATE",
|
||||
types = {"ENTERING_COMBAT", "LEAVING_COMBAT"}},
|
||||
{colour = CT_GOLD, gate = "COMBAT_TEXT_SHOW_REACTIVES", types = {"SPELL_ACTIVE"}},
|
||||
}
|
||||
|
||||
COMBAT_TEXT_TYPE_INFO = {}
|
||||
COMBAT_TEXT_TYPE_INFO["INTERRUPT"] = {r = 1, g = 1, b = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["DAMAGE_CRIT"] = {r = 1, g = 0.1, b = 0.1, show = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["DAMAGE"] = {r = 1, g = 0.1, b = 0.1, isStaggered = 1, show = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["MISS"] = {r = 1, g = 0.1, b = 0.1, isStaggered = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["BLOCK"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_RESISTANCES"}
|
||||
COMBAT_TEXT_TYPE_INFO["ABSORB"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_RESISTANCES"}
|
||||
COMBAT_TEXT_TYPE_INFO["RESIST"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_RESISTANCES"}
|
||||
COMBAT_TEXT_TYPE_INFO["EVADE"] = {r = 1, g = 0.1, b = 0.1, isStaggered = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["DODGE"] = {r = 1, g = 0.1, b = 0.1, isStaggered = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["PARRY"] = {r = 1, g = 0.1, b = 0.1, isStaggered = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["IMMUNE"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["DEFLECT"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["ENCHANTMENT_REMOVED"] = {r = 1, g = 1, b = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["ENCHANTMENT_ADDED"] = {r = 1, g = 1, b = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["PERIODIC_HEAL"] = {r = 0.1, g = 1, b = 0.1, show = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["MANA"] = {r = 0.1, g = 0.1, b = 1, var = "COMBAT_TEXT_SHOW_MANA"}
|
||||
COMBAT_TEXT_TYPE_INFO["RAGE"] = {r = 0.1, g = 0.1, b = 1, var = "COMBAT_TEXT_SHOW_MANA"}
|
||||
COMBAT_TEXT_TYPE_INFO["FOCUS"] = {r = 0.1, g = 0.1, b = 1, var = "COMBAT_TEXT_SHOW_MANA"}
|
||||
COMBAT_TEXT_TYPE_INFO["ENERGY"] = {r = 0.1, g = 0.1, b = 1, var = "COMBAT_TEXT_SHOW_MANA"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_ABSORBED"] = {r = 0.79, g = 0.3, b = 0.85, var = "COMBAT_TEXT_SHOW_RESISTANCES"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_RESISTED"] = {r = 0.79, g = 0.3, b = 0.85, var = "COMBAT_TEXT_SHOW_RESISTANCES"}
|
||||
COMBAT_TEXT_TYPE_INFO["PROC_RESISTED"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_RESISTANCES"}
|
||||
COMBAT_TEXT_TYPE_INFO["DISPEL_FAILED"] = {r = 1, g = 1, b = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_CAST"] = {r = 0.1, g = 1, b = 0.1, show = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_CAST_START"] = {r = 1, g = 1, b = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["AURA_END"] = {r = 0.1, g = 1, b = 0.1, var = "COMBAT_TEXT_SHOW_AURA_FADE"}
|
||||
COMBAT_TEXT_TYPE_INFO["AURA_END_HARMFUL"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_AURA_FADE"}
|
||||
COMBAT_TEXT_TYPE_INFO["AURA_START"] = {r = 0.1, g = 1, b = 0.1, var = "COMBAT_TEXT_SHOW_AURAS"}
|
||||
COMBAT_TEXT_TYPE_INFO["AURA_START_HARMFUL"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_AURAS"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_DAMAGE"] = {r = 0.79, g = 0.3, b = 0.85, show = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_DODGED"] = {r = 1, g = 1, b = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_PARRIED"] = {r = 1, g = 1, b = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_BLOCKED"] = {r = 1, g = 1, b = 1, var = "COMBAT_TEXT_SHOW_RESISTANCES"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_EVADED"] = {r = 1, g = 1, b = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_IMMUNE"] = {r = 1, g = 1, b = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_DEFLECTED"] = {r = 1, g = 1, b = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_REFLECTED"] = {r = 1, g = 1, b = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_MISSED"] = {r = 1, g = 1, b = 1, var = "COMBAT_TEXT_SHOW_DODGE_PARRY_MISS"}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_ACTIVE"] = {r = 1, g = 0.82, b = 0, var = "COMBAT_TEXT_SHOW_REACTIVES"}
|
||||
COMBAT_TEXT_TYPE_INFO["FACTION"] = {r = 0.1, g = 0.1, b = 1, var = "COMBAT_TEXT_SHOW_REPUTATION"}
|
||||
COMBAT_TEXT_TYPE_INFO["HEAL_CRIT"] = {r = 0.1, g = 1, b = 0.1, show = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["HEAL"] = {r = 0.1, g = 1, b = 0.1, show = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["DAMAGE_SHIELD"] = {r = 1, g = 1, b = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["SPELL_DISPELLED"] = {r = 1, g = 1, b = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["EXTRA_ATTACKS"] = {r = 1, g = 1, b = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["SPLIT_DAMAGE"] = {r = 1, g = 1, b = 1, show = 1}
|
||||
COMBAT_TEXT_TYPE_INFO["HONOR_GAINED"] = {r = 0.1, g = 0.1, b = 1, var = "COMBAT_TEXT_SHOW_HONOR_GAINED"}
|
||||
COMBAT_TEXT_TYPE_INFO["HEALTH_LOW"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_LOW_HEALTH_MANA"}
|
||||
COMBAT_TEXT_TYPE_INFO["MANA_LOW"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_LOW_HEALTH_MANA"}
|
||||
COMBAT_TEXT_TYPE_INFO["ENTERING_COMBAT"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_COMBAT_STATE"}
|
||||
COMBAT_TEXT_TYPE_INFO["LEAVING_COMBAT"] = {r = 1, g = 0.1, b = 0.1, var = "COMBAT_TEXT_SHOW_COMBAT_STATE"}
|
||||
COMBAT_TEXT_TYPE_INFO["COMBO_POINTS"] = {r = 0.1, g = 0.1, b = 1, var = "COMBAT_TEXT_SHOW_COMBO_POINTS"}
|
||||
for i = 1, getn(combatTextSpec) do
|
||||
local row = combatTextSpec[i]
|
||||
for j = 1, getn(row.types) do
|
||||
COMBAT_TEXT_TYPE_INFO[row.types[j]] = {
|
||||
r = row.colour[1],
|
||||
g = row.colour[2],
|
||||
b = row.colour[3],
|
||||
show = row.always,
|
||||
isStaggered = row.jitter,
|
||||
var = row.gate,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- A latched low-resource warning. The line prints on the crossing INTO the danger band and
|
||||
-- not again until the bar has climbed back out of it — otherwise every tick of a fight
|
||||
-- spent under 20% would print another one. `flag` names the field on the CombatText frame
|
||||
-- that remembers we already warned; `low` is the caller's verdict, because "low" is not
|
||||
-- the same question for the two bars (mana additionally requires the bar to BE mana: a
|
||||
-- rage or energy user is never low on mana). Returns the message type to print, or nil.
|
||||
local function CombatText_LowResourceWarning(flag, low, messageType)
|
||||
if not low then
|
||||
CombatText[flag] = nil
|
||||
return nil
|
||||
end
|
||||
if CombatText[flag] then
|
||||
return nil
|
||||
end
|
||||
CombatText[flag] = 1
|
||||
return messageType
|
||||
end
|
||||
|
||||
-- The engine events whose entire meaning is "print this message type". Everything else
|
||||
-- either carries its own type in arg1 (COMBAT_TEXT_UPDATE) or IS its own type.
|
||||
local combatTextStateMessage = {
|
||||
PLAYER_REGEN_DISABLED = "ENTERING_COMBAT",
|
||||
PLAYER_REGEN_ENABLED = "LEAVING_COMBAT",
|
||||
}
|
||||
|
||||
function CombatText_OnLoad()
|
||||
CombatText_UpdateDisplayedMessages()
|
||||
@@ -175,70 +266,57 @@
|
||||
end
|
||||
|
||||
function CombatText_OnEvent(event)
|
||||
-- Set up the messageType
|
||||
-- What we are about to print: the type (which picks the colour and the gating), the
|
||||
-- payload it formats from, the finished line, and the optional "crit" presentation.
|
||||
local messageType, message
|
||||
-- Set the message data
|
||||
local data = arg2
|
||||
local displayType
|
||||
|
||||
if event == "UNIT_HEALTH" then
|
||||
if arg1 == "player" then
|
||||
if UnitHealth("player") / UnitHealthMax("player") <= COMBAT_TEXT_LOW_HEALTH_THRESHOLD then
|
||||
if not CombatText.lowHealth then
|
||||
messageType = "HEALTH_LOW"
|
||||
CombatText.lowHealth = 1
|
||||
end
|
||||
else
|
||||
CombatText.lowHealth = nil
|
||||
end
|
||||
local low = UnitHealth("player") / UnitHealthMax("player") <= COMBAT_TEXT_LOW_HEALTH_THRESHOLD
|
||||
messageType = CombatText_LowResourceWarning("lowHealth", low, "HEALTH_LOW")
|
||||
end
|
||||
if not messageType then
|
||||
return
|
||||
end
|
||||
elseif event == "UNIT_POWER_UPDATE" then
|
||||
-- Ref: UNIT_MANA + UnitMana/UnitManaMax (header deviation — the Era surface).
|
||||
-- Spec: UNIT_MANA + UnitMana/UnitManaMax (header deviation — the Era surface).
|
||||
-- Power type 0 is mana; the same event also carries rage, focus and energy, none
|
||||
-- of which has a low warning.
|
||||
local mana = UnitMana("player")
|
||||
local powerType = UnitPowerType("player")
|
||||
if arg1 == "player" then
|
||||
if mana / UnitManaMax("player") <= COMBAT_TEXT_LOW_MANA_THRESHOLD and powerType == 0 then
|
||||
if not CombatText.lowMana then
|
||||
messageType = "MANA_LOW"
|
||||
CombatText.lowMana = 1
|
||||
end
|
||||
else
|
||||
CombatText.lowMana = nil
|
||||
end
|
||||
local low = mana / UnitManaMax("player") <= COMBAT_TEXT_LOW_MANA_THRESHOLD and powerType == 0
|
||||
messageType = CombatText_LowResourceWarning("lowMana", low, "MANA_LOW")
|
||||
end
|
||||
if not messageType then
|
||||
return
|
||||
end
|
||||
elseif event == "PLAYER_REGEN_DISABLED" then
|
||||
messageType = "ENTERING_COMBAT"
|
||||
elseif event == "PLAYER_REGEN_ENABLED" then
|
||||
messageType = "LEAVING_COMBAT"
|
||||
elseif event == "PLAYER_COMBO_POINTS" then
|
||||
local comboPoints = GetComboPoints()
|
||||
if comboPoints > 0 then
|
||||
messageType = "COMBO_POINTS"
|
||||
data = comboPoints
|
||||
if MAX_COMBO_POINTS and comboPoints == MAX_COMBO_POINTS then
|
||||
displayType = "crit"
|
||||
end
|
||||
else
|
||||
-- The count itself is the payload, and a full bar is worth a crit-sized line.
|
||||
-- Spending them fires this too, with nothing left to announce.
|
||||
data = GetComboPoints()
|
||||
if data <= 0 then
|
||||
return
|
||||
end
|
||||
messageType = "COMBO_POINTS"
|
||||
if data == MAX_COMBO_POINTS then
|
||||
displayType = "crit"
|
||||
end
|
||||
elseif event == "COMBAT_TEXT_UPDATE" then
|
||||
messageType = arg1
|
||||
else
|
||||
messageType = event
|
||||
messageType = combatTextStateMessage[event] or event
|
||||
end
|
||||
|
||||
-- Process the messageType and format the message
|
||||
-- The type decides the colour and whether this is printed at all. An unknown type
|
||||
-- is white and, having no `show`, prints nothing — which is how a message type we do
|
||||
-- not know about stays silent instead of erroring.
|
||||
local info = COMBAT_TEXT_TYPE_INFO[messageType]
|
||||
if not info then
|
||||
info = {r = 1, g = 1, b = 1}
|
||||
end
|
||||
-- See if we should display the message or not
|
||||
if not info.show then
|
||||
return
|
||||
end
|
||||
@@ -319,7 +397,8 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- Add the message
|
||||
-- A branch that produced no line (the empty type, or a global-string lookup that
|
||||
-- came back nil) simply prints nothing.
|
||||
if message then
|
||||
CombatText_AddMessage(message, COMBAT_TEXT_SCROLL_FUNCTION, info.r, info.g, info.b, displayType, isStaggered)
|
||||
end
|
||||
@@ -341,7 +420,8 @@
|
||||
local alpha = 1 - ((value.scrollTime - COMBAT_TEXT_FADEOUT_TIME) / (COMBAT_TEXT_SCROLLSPEED - COMBAT_TEXT_FADEOUT_TIME))
|
||||
value:SetAlpha(max(alpha, 0))
|
||||
end
|
||||
-- Handle crit
|
||||
-- The crit pop: up to the maximum inside the scale window, back down by the
|
||||
-- shrink time, then it stops being a crit and holds its size.
|
||||
if value.isCrit then
|
||||
if value.scrollTime <= COMBAT_TEXT_CRIT_SCALE_TIME then
|
||||
value:SetTextHeight(floor(COMBAT_TEXT_CRIT_MINHEIGHT + ((COMBAT_TEXT_CRIT_MAXHEIGHT - COMBAT_TEXT_CRIT_MINHEIGHT) * value.scrollTime / COMBAT_TEXT_CRIT_SCALE_TIME)))
|
||||
@@ -355,6 +435,14 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- A string that is not in flight: invisible, hidden, and back on the seat so the next
|
||||
-- caller measures it from a known place. Both ways out of the animation list end here.
|
||||
local function CombatText_ParkString(string)
|
||||
string:SetAlpha(0)
|
||||
string:Hide()
|
||||
string:SetPoint("TOP", UIParent, "BOTTOM", COMBAT_TEXT_LOCATIONS.startX, COMBAT_TEXT_LOCATIONS.startY)
|
||||
end
|
||||
|
||||
function CombatText_AddMessage(message, scrollFunction, r, g, b, displayType, isStaggered)
|
||||
local string, noStringsAvailable = CombatText_GetAvailableString()
|
||||
if noStringsAvailable then
|
||||
@@ -364,71 +452,72 @@
|
||||
string:SetText(message)
|
||||
string:SetTextColor(r, g, b)
|
||||
string.scrollTime = 0
|
||||
|
||||
-- Where the line is seated. `flow` is the direction the STACK grows — back down the
|
||||
-- way the run came, so -1 when the mode floats upward and +1 when it floats down —
|
||||
-- and having it as a number is what lets one walk serve both modes. Start at the
|
||||
-- string's own parked edge, then push clear of every line already in flight by one
|
||||
-- 16-unit line plus COMBAT_TEXT_SPACING; if that has carried the stack further than
|
||||
-- COMBAT_TEXT_MAX_OFFSET from the seat, start the column over at the parked edge
|
||||
-- instead of marching off the screen. (`GetBottom`/`GetTop` are nil before the first
|
||||
-- layout resolve — the parked seat stands in; identical after frame 1.)
|
||||
local flow, parkedY
|
||||
if COMBAT_TEXT_LOCATIONS.startY < COMBAT_TEXT_LOCATIONS.endY then
|
||||
flow = -1
|
||||
parkedY = string:GetBottom()
|
||||
else
|
||||
flow = 1
|
||||
parkedY = string:GetTop()
|
||||
end
|
||||
parkedY = parkedY or COMBAT_TEXT_LOCATIONS.startY
|
||||
|
||||
local seatY = parkedY
|
||||
for i = 1, getn(COMBAT_TEXT_TO_ANIMATE) do
|
||||
local clearOf = COMBAT_TEXT_TO_ANIMATE[i].yPos + flow * (16 + COMBAT_TEXT_SPACING)
|
||||
if (clearOf - seatY) * flow >= 0 then
|
||||
seatY = clearOf
|
||||
end
|
||||
end
|
||||
if (seatY - COMBAT_TEXT_LOCATIONS.startY) * flow > COMBAT_TEXT_MAX_OFFSET then
|
||||
seatY = parkedY
|
||||
end
|
||||
|
||||
-- Everything `displayType` decides, in one place. A crit pops through its own scale
|
||||
-- envelope and always takes the straight scroll (an arc would fight the pop); a crit or
|
||||
-- a sticky ends where it began, so it holds the seat instead of travelling; anything
|
||||
-- else takes the caller's scroll and runs the whole way.
|
||||
if displayType == "crit" then
|
||||
string.scrollFunction = CombatText_StandardScroll
|
||||
string.isCrit = 1
|
||||
string.endY = COMBAT_TEXT_LOCATIONS.startY
|
||||
string:SetTextHeight(COMBAT_TEXT_CRIT_MINHEIGHT)
|
||||
else
|
||||
string.scrollFunction = scrollFunction
|
||||
end
|
||||
|
||||
-- See which direction the message should flow. (`GetBottom`/`GetTop` are nil before
|
||||
-- the first layout resolve — the parked seat stands in; identical after frame 1.)
|
||||
local lowestMessage
|
||||
if COMBAT_TEXT_LOCATIONS.startY < COMBAT_TEXT_LOCATIONS.endY then
|
||||
-- Flowing up
|
||||
lowestMessage = string:GetBottom() or COMBAT_TEXT_LOCATIONS.startY
|
||||
for i = 1, getn(COMBAT_TEXT_TO_ANIMATE) do
|
||||
local value = COMBAT_TEXT_TO_ANIMATE[i]
|
||||
if lowestMessage >= value.yPos - 16 - COMBAT_TEXT_SPACING then
|
||||
lowestMessage = value.yPos - 16 - COMBAT_TEXT_SPACING
|
||||
end
|
||||
end
|
||||
if lowestMessage < (COMBAT_TEXT_LOCATIONS.startY - COMBAT_TEXT_MAX_OFFSET) then
|
||||
lowestMessage = string:GetBottom() or COMBAT_TEXT_LOCATIONS.startY
|
||||
end
|
||||
else
|
||||
-- Flowing down
|
||||
lowestMessage = string:GetTop() or COMBAT_TEXT_LOCATIONS.startY
|
||||
for i = 1, getn(COMBAT_TEXT_TO_ANIMATE) do
|
||||
local value = COMBAT_TEXT_TO_ANIMATE[i]
|
||||
if lowestMessage <= value.yPos + 16 + COMBAT_TEXT_SPACING then
|
||||
lowestMessage = value.yPos + 16 + COMBAT_TEXT_SPACING
|
||||
end
|
||||
end
|
||||
if lowestMessage > (COMBAT_TEXT_LOCATIONS.startY + COMBAT_TEXT_MAX_OFFSET) then
|
||||
lowestMessage = string:GetTop() or COMBAT_TEXT_LOCATIONS.startY
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle crits
|
||||
if displayType == "crit" then
|
||||
string.endY = COMBAT_TEXT_LOCATIONS.startY
|
||||
string.isCrit = 1
|
||||
string:SetTextHeight(COMBAT_TEXT_CRIT_MINHEIGHT)
|
||||
elseif displayType == "sticky" then
|
||||
string.endY = COMBAT_TEXT_LOCATIONS.startY
|
||||
string:SetTextHeight(COMBAT_TEXT_HEIGHT)
|
||||
else
|
||||
string.endY = COMBAT_TEXT_LOCATIONS.endY
|
||||
string:SetTextHeight(COMBAT_TEXT_HEIGHT)
|
||||
if displayType == "sticky" then
|
||||
string.endY = COMBAT_TEXT_LOCATIONS.startY
|
||||
else
|
||||
string.endY = COMBAT_TEXT_LOCATIONS.endY
|
||||
end
|
||||
end
|
||||
|
||||
-- Stagger the text if flagged
|
||||
local staggerAmount = 0
|
||||
if isStaggered then
|
||||
staggerAmount = random(0, COMBAT_TEXT_STAGGER_RANGE) - COMBAT_TEXT_STAGGER_RANGE / 2
|
||||
end
|
||||
|
||||
-- Alternate x direction
|
||||
if not CombatText.xDir or CombatText.xDir < 0 then
|
||||
CombatText.xDir = 1
|
||||
else
|
||||
-- The fountain arcs alternate sides, so consecutive lines do not trace one path.
|
||||
if CombatText.xDir == 1 then
|
||||
CombatText.xDir = -1
|
||||
else
|
||||
CombatText.xDir = 1
|
||||
end
|
||||
string.xDir = CombatText.xDir
|
||||
string.startX = COMBAT_TEXT_LOCATIONS.startX + staggerAmount
|
||||
string.startY = lowestMessage
|
||||
string.yPos = lowestMessage
|
||||
string:SetPoint("TOP", UIParent, "BOTTOM", string.startX, lowestMessage)
|
||||
|
||||
-- Staggered types jitter up to half COMBAT_TEXT_STAGGER_RANGE either side of centre.
|
||||
string.startX = COMBAT_TEXT_LOCATIONS.startX
|
||||
if isStaggered then
|
||||
string.startX = string.startX + random(0, COMBAT_TEXT_STAGGER_RANGE) - COMBAT_TEXT_STAGGER_RANGE / 2
|
||||
end
|
||||
|
||||
string.startY = seatY
|
||||
string.yPos = seatY
|
||||
string:SetPoint("TOP", UIParent, "BOTTOM", string.startX, seatY)
|
||||
string:SetAlpha(1)
|
||||
string:Show()
|
||||
tinsert(COMBAT_TEXT_TO_ANIMATE, string)
|
||||
@@ -438,9 +527,7 @@
|
||||
for i = 1, getn(COMBAT_TEXT_TO_ANIMATE) do
|
||||
if COMBAT_TEXT_TO_ANIMATE[i] == string then
|
||||
tremove(COMBAT_TEXT_TO_ANIMATE, i)
|
||||
string:SetAlpha(0)
|
||||
string:Hide()
|
||||
string:SetPoint("TOP", UIParent, "BOTTOM", COMBAT_TEXT_LOCATIONS.startX, COMBAT_TEXT_LOCATIONS.startY)
|
||||
CombatText_ParkString(string)
|
||||
break
|
||||
end
|
||||
end
|
||||
@@ -466,15 +553,13 @@
|
||||
function CombatText_ClearAnimationList()
|
||||
COMBAT_TEXT_TO_ANIMATE = {}
|
||||
for i = 1, NUM_COMBAT_TEXT_LINES do
|
||||
local string = getglobal("CombatText" .. i)
|
||||
string:SetAlpha(0)
|
||||
string:Hide()
|
||||
string:SetPoint("TOP", UIParent, "BOTTOM", COMBAT_TEXT_LOCATIONS.startX, COMBAT_TEXT_LOCATIONS.startY)
|
||||
CombatText_ParkString(getglobal("CombatText" .. i))
|
||||
end
|
||||
end
|
||||
|
||||
function CombatText_UpdateDisplayedMessages()
|
||||
-- Unregister events if combat text is disabled
|
||||
-- The master toggle is enforced at the source: with the feature off we stop
|
||||
-- listening entirely rather than filtering every message as it arrives.
|
||||
if SHOW_COMBAT_TEXT == "0" then
|
||||
CombatText:UnregisterEvent("COMBAT_TEXT_UPDATE")
|
||||
CombatText:UnregisterEvent("UNIT_HEALTH")
|
||||
@@ -491,7 +576,8 @@
|
||||
CombatText:RegisterEvent("PLAYER_REGEN_DISABLED")
|
||||
CombatText:RegisterEvent("PLAYER_REGEN_ENABLED")
|
||||
end
|
||||
-- Update shown messages
|
||||
-- Re-read every gated type's option into its `show`. This is the only writer of
|
||||
-- `show` for a type that carries a `var`, so it runs on every option change.
|
||||
for messageType, value in pairs(COMBAT_TEXT_TYPE_INFO) do
|
||||
if value.var then
|
||||
if getglobal(value.var) == "1" then
|
||||
@@ -505,7 +591,8 @@
|
||||
-- 0582), so the ref constants stand verbatim (0581's px shim retired). The ref's
|
||||
-- uiScale-cvar division joins when a cvar system exists.
|
||||
local uiScale = 1
|
||||
-- Update scrolldirection
|
||||
-- The float mode picks the path and the run: modes 1 and 3 rise to 609 (3 on an
|
||||
-- arc), mode 2 falls to 159. All three start at the same seat.
|
||||
if COMBAT_TEXT_FLOAT_MODE == "1" then
|
||||
COMBAT_TEXT_SCROLL_FUNCTION = CombatText_StandardScroll
|
||||
COMBAT_TEXT_LOCATIONS = {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The combo-point dots (decision 0869) — the reference `FrameXML/ComboFrame.xml` +
|
||||
`ComboFrame.lua` transcribed, geometry QUOTED verbatim (offsets, sizes, TexCoords and the
|
||||
five fade constants are the reference's own), renamed onto our frames and driven by our
|
||||
`PLAYER_COMBO_POINTS` feed.
|
||||
The combo-point dots (decision 0869) — our implementation of the behaviour the reference's
|
||||
`FrameXML/ComboFrame.xml` + `ComboFrame.lua` specify, on our frames and driven by our
|
||||
`PLAYER_COMBO_POINTS` feed. The Lua below is our own; what IS quoted, deliberately, is the
|
||||
published surface and the measurements: the global function and constant names addons call,
|
||||
and every number — offsets, sizes, TexCoords and the five fade durations are the reference's
|
||||
own, because they are facts about how the real interface looks and moves, not choices.
|
||||
|
||||
ONE `Interface\ComboFrame\ComboPoint` sheet holds all three layers, split by TexCoords: the
|
||||
dim socket (0 .. 0.375), the lit highlight (0.375 .. 0.5625) and the additive shine
|
||||
(0.5625 .. 1). A point that comes up fades its highlight in, and the highlight's completion
|
||||
hands off to the shine's fade-in → fade-out — the reference's two-hop `finishedFunc` chain,
|
||||
which our `UIFrameFade` (UiPanels.xml) supports verbatim.
|
||||
which our `UIFrameFade` (UiPanels.xml) implements in full.
|
||||
|
||||
NO CLASS TEST HERE, and none is missing: this file gates on `GetComboPoints() > 0` and
|
||||
nothing else because the reference does, and `0x51a190` carries both real gates itself —
|
||||
@@ -118,7 +120,10 @@
|
||||
|
||||
<Script>
|
||||
<![CDATA[
|
||||
-- ref ComboFrame.lua l.1-6, verbatim.
|
||||
-- The reference's global names, carrying the reference's measured timings: addons read
|
||||
-- these by name, and the five durations are what the real interface's fades take, so both
|
||||
-- halves are fixed. MAX_COMBO_POINTS lives in the ref's TargetFrame.lua; we have no such
|
||||
-- file, and CombatText.xml's five-point flash reads it, so it is declared here.
|
||||
MAX_COMBO_POINTS = 5;
|
||||
COMBOFRAME_FADE_IN = 0.3;
|
||||
COMBOFRAME_FADE_OUT = 0.5;
|
||||
@@ -127,54 +132,67 @@
|
||||
COMBOFRAME_SHINE_FADE_OUT = 0.4;
|
||||
COMBO_FRAME_LAST_NUM_POINTS = 0;
|
||||
|
||||
-- ref ComboFrame.lua `ComboPointsFrame_OnEvent`. Only points ABOVE the last count animate
|
||||
-- (`i > COMBO_FRAME_LAST_NUM_POINTS`) — a repaint at the same count must not re-flare every
|
||||
-- dot, which is why the last count is remembered across events rather than recomputed.
|
||||
-- Dot `index`'s two animated layers: the lit point, and the additive flare over it. The
|
||||
-- socket beneath them never moves, so nothing ever needs to reach for it.
|
||||
local function comboPointLayers(index)
|
||||
return getglobal("ComboPoint"..index.."Highlight"),
|
||||
getglobal("ComboPoint"..index.."Shine");
|
||||
end
|
||||
|
||||
-- Repaint the dots for the count `GetComboPoints` reports. Only points ABOVE the previous
|
||||
-- count animate: a repaint at an unchanged count (PLAYER_TARGET_CHANGED fires one on every
|
||||
-- re-select) must not re-flare dots that are already lit — which is why the count is
|
||||
-- remembered across events instead of being read back off the dots.
|
||||
function ComboPointsFrame_OnEvent()
|
||||
local comboPoints = GetComboPoints();
|
||||
if comboPoints > 0 then
|
||||
local points = GetComboPoints();
|
||||
if points > 0 then
|
||||
if not ComboFrame:IsVisible() then
|
||||
ComboFrame:Show();
|
||||
UIFrameFadeIn(ComboFrame, COMBOFRAME_FADE_IN);
|
||||
end
|
||||
for i = 1, MAX_COMBO_POINTS do
|
||||
local comboPointHighlight = getglobal("ComboPoint"..i.."Highlight");
|
||||
local comboPointShine = getglobal("ComboPoint"..i.."Shine");
|
||||
if i <= comboPoints then
|
||||
if i > COMBO_FRAME_LAST_NUM_POINTS then
|
||||
local fadeInfo = {};
|
||||
fadeInfo.mode = "IN";
|
||||
fadeInfo.timeToFade = COMBOFRAME_HIGHLIGHT_FADE_IN;
|
||||
fadeInfo.finishedFunc = ComboPointShineFadeIn;
|
||||
fadeInfo.finishedArg1 = comboPointShine;
|
||||
UIFrameFade(comboPointHighlight, fadeInfo);
|
||||
end
|
||||
else
|
||||
comboPointHighlight:SetAlpha(0);
|
||||
comboPointShine:SetAlpha(0);
|
||||
local highlight, shine = comboPointLayers(i);
|
||||
if i > points then
|
||||
-- Past the count: dark at once, never animated back down.
|
||||
highlight:SetAlpha(0);
|
||||
shine:SetAlpha(0);
|
||||
elseif i > COMBO_FRAME_LAST_NUM_POINTS then
|
||||
-- Newly earned: light it, and hand the finished fade on to its flare.
|
||||
UIFrameFade(highlight, {
|
||||
mode = "IN",
|
||||
timeToFade = COMBOFRAME_HIGHLIGHT_FADE_IN,
|
||||
finishedFunc = ComboPointShineFadeIn,
|
||||
finishedArg1 = shine,
|
||||
});
|
||||
end
|
||||
end
|
||||
else
|
||||
ComboPoint1Highlight:SetAlpha(0);
|
||||
ComboPoint1Shine:SetAlpha(0);
|
||||
-- Nothing banked. The frame goes away, so only the first dot is worth clearing:
|
||||
-- the loop above blanks every dot past the count the next time one is earned.
|
||||
local highlight, shine = comboPointLayers(1);
|
||||
highlight:SetAlpha(0);
|
||||
shine:SetAlpha(0);
|
||||
ComboFrame:Hide();
|
||||
end
|
||||
COMBO_FRAME_LAST_NUM_POINTS = comboPoints;
|
||||
COMBO_FRAME_LAST_NUM_POINTS = points;
|
||||
end
|
||||
|
||||
function ComboPointShineFadeIn(frame)
|
||||
local fadeInfo = {};
|
||||
fadeInfo.mode = "IN";
|
||||
fadeInfo.timeToFade = COMBOFRAME_SHINE_FADE_IN;
|
||||
fadeInfo.finishedFunc = ComboPointShineFadeOut;
|
||||
fadeInfo.finishedArg1 = frame:GetName();
|
||||
UIFrameFade(frame, fadeInfo);
|
||||
-- Leg two of the chain, reached as the highlight finishes: flare the shine in, and book
|
||||
-- leg three off ITS completion. The hand-off travels by NAME because a fade's finishedArg
|
||||
-- rides on the frame's own fadeInfo, and the reference notes the same self-reference
|
||||
-- problem where it does this.
|
||||
function ComboPointShineFadeIn(shine)
|
||||
UIFrameFade(shine, {
|
||||
mode = "IN",
|
||||
timeToFade = COMBOFRAME_SHINE_FADE_IN,
|
||||
finishedFunc = ComboPointShineFadeOut,
|
||||
finishedArg1 = shine:GetName(),
|
||||
});
|
||||
end
|
||||
|
||||
-- The name round-trip is the reference's own workaround ("hack since a frame can't have a
|
||||
-- reference to itself in it") — kept, so the chain reads as its source does.
|
||||
function ComboPointShineFadeOut(frameName)
|
||||
UIFrameFadeOut(getglobal(frameName), COMBOFRAME_SHINE_FADE_OUT);
|
||||
-- Leg three: the flare burns back out on its own.
|
||||
function ComboPointShineFadeOut(shineName)
|
||||
UIFrameFadeOut(getglobal(shineName), COMBOFRAME_SHINE_FADE_OUT);
|
||||
end
|
||||
]]>
|
||||
</Script>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
lines while each of those children came out as a bare, attribute-less frame.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- Ref Cooldown.lua's CooldownFrame_SetTimer, transcribed: show-and-arm only when all three
|
||||
-- Ref Cooldown.lua's CooldownFrame_SetTimer, re-expressed: show-and-arm only when all three
|
||||
-- are positive, else hide.
|
||||
--
|
||||
-- THE ONE FORK, and why it is two faithful branches rather than a guard. The reference's
|
||||
@@ -44,7 +44,7 @@
|
||||
]]></Script>
|
||||
|
||||
<!--
|
||||
ref Cooldown.xml l.4-14, transcribed. The reference's own attributes are kept verbatim
|
||||
ref Cooldown.xml l.4-14. The reference's own attributes are kept verbatim
|
||||
(its .mdx file, its 0.75 scale, setAllPoints, hidden) even where this engine has no use for
|
||||
the model file: the template's JOB here is to exist under the reference's name so an
|
||||
addon's `inherits=` resolves and its cooldown child inherits the geometry the addon is
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's Enchanting window (decision 0437 phase 3): the 1.12 `CraftFrame`, transcribed from
|
||||
benilla's Enchanting window (decision 0437 phase 3): the 1.12 `CraftFrame`, re-expressed from
|
||||
the reference `Blizzard_CraftUI.xml`/`.lua` (session scratchpad `ref-ui/`, decision 0246
|
||||
discipline — never committed) onto our own engine/feeds. Geometry/art-paths/structure below are
|
||||
QUOTED from the ref (line citations inline); the Lua is transcribed logic turned onto benilla's
|
||||
QUOTED from the ref (line citations inline); the Lua is the ref's logic turned onto benilla's
|
||||
Era API (`GetCraftName`/`GetCraftDisplaySkillLine`/`GetNumCrafts`/`GetCraftInfo`/reagents/
|
||||
`GetCraftSpellFocus`/`DoCraft`/`CloseCraft`, decision 0437's engine surface,
|
||||
`crates/benilla-ui/src/script/craft.rs`). The exact twin of `TradeSkillFrame.xml` — same
|
||||
@@ -155,7 +155,7 @@
|
||||
TradeSkillFrame.xml's own idiom — with ONE exception: `CREATE` is declared as a REAL Lua
|
||||
GLOBAL (`CREATE = CREATE or "Create"`, guarded so a second file loading it stays
|
||||
idempotent), not `local`, because `getglobal(GetCraftButtonToken())` (ref l.106, faithfully
|
||||
transcribed in `BenillaCraftFrame_Update` below) needs an actual `_G["CREATE"]` entry to
|
||||
kept in `BenillaCraftFrame_Update` below) needs an actual `_G["CREATE"]` entry to
|
||||
resolve — a genuine structural requirement of that ref call, not a style choice.
|
||||
`GRAY_FONT_COLOR`/`HIGHLIGHT_FONT_COLOR` are Fonts.xml globals (CharacterFrame.xml's own
|
||||
precedent for reading them unqualified).
|
||||
@@ -238,8 +238,8 @@
|
||||
icon:Show()
|
||||
icon:SetNormalTexture(GetCraftIcon(id) or BENILLA_FALLBACK_ICON)
|
||||
|
||||
-- The description + the Reagents label's DYNAMIC reposition (ref l.312-318, transcribed
|
||||
-- verbatim): when there's a description, the label sits 10px below its bottom edge; when
|
||||
-- The description + the Reagents label's DYNAMIC reposition (ref l.312-318, re-expressed
|
||||
-- here): when there's a description, the label sits 10px below its bottom edge; when
|
||||
-- there isn't, it sits directly where the (blank) description would have been.
|
||||
if GetCraftDescription(id) then
|
||||
getglobal("BenillaCraftDescription"):SetText(GetCraftDescription(id))
|
||||
@@ -312,7 +312,7 @@
|
||||
else
|
||||
getglobal("BenillaCraftCollapseAllButton"):Enable() -- ref l.158 (no matching Disable()
|
||||
-- anywhere in Craft's own ref — a real asymmetry vs. TradeSkill's own l.86/94 pair,
|
||||
-- transcribed as-is, not "fixed").
|
||||
-- kept as the ref has it, not "fixed").
|
||||
end
|
||||
|
||||
local scrollFrame = getglobal("BenillaCraftListScrollFrame")
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<!--
|
||||
The death arc's UI (decision 0308 slices 1/3): the reference's death dialogs registered on
|
||||
the shared StaticPopup engine (UiPanels.xml), and the UIParent death-event driver,
|
||||
transcribed from the patch-chain FrameXML (mpqx extraction, decision 0246):
|
||||
StaticPopup.lua l.375-505 (DEATH + RESURRECT×3), l.1134-1201 (XP_LOSS×2 + RECOVER_CORPSE*),
|
||||
UIParent.lua l.237-262 + l.399-436 (the event arms). Strings verbatim from
|
||||
GlobalStrings.lua. The engine-side events feeding this: PLAYER_DEAD/ALIVE/UNGHOST +
|
||||
The death arc's UI (decision 0308 slices 1/3): our death dialogs, registered on the shared
|
||||
StaticPopup engine (UiPanels.xml), plus the event driver that raises and clears them.
|
||||
|
||||
The patch-chain FrameXML (mpqx extraction, decision 0246) is the SPECIFICATION for what
|
||||
each dialog says, offers and answers — StaticPopup.lua l.375-505 (DEATH + RESURRECT×3),
|
||||
l.1134-1201 (XP_LOSS×2 + RECOVER_CORPSE*), UIParent.lua l.237-262 + l.399-436 (the event
|
||||
arms). The field names below are the published registry API that addons write into, so
|
||||
they are the reference's and cannot be renamed; the code that fills them is ours. Six of
|
||||
the eight dialogs share one disposition and are stamped with it rather than repeating it,
|
||||
the three resurrect offers and the two spirit-healer confirms are each one shape built
|
||||
more than once, and the driver is a single event switch. Strings are the reference's own
|
||||
(GlobalStrings.lua), which is data, not authorship.
|
||||
|
||||
The engine-side events feeding this: PLAYER_DEAD/ALIVE/UNGHOST +
|
||||
RESURRECT_REQUEST + CONFIRM_XP_LOSS fire from `death.rs`'s state machine;
|
||||
CORPSE_IN_RANGE/OUT_OF_RANGE/IN_INSTANCE join with the slice-2 corpse-run range gate
|
||||
(registered now — they simply don't fire yet). Deferred with 0308 §8: the SKINNED/
|
||||
@@ -32,56 +40,73 @@
|
||||
local DECLINE = "Decline"
|
||||
local CANCEL = "Cancel"
|
||||
|
||||
-- ── The DEATH (release) dialog — StaticPopup.lua l.375-414, verbatim behavior ────────────
|
||||
-- OnShow seeds timeleft from GetReleaseTimeRemaining(): >0 counts down through the engine's
|
||||
-- per-tick DEATH text; −1 swaps in the no-timer text (no countdown). Timeout expiry is a
|
||||
-- silent hide — the SERVER force-releases at 6:00 (0308 §1). No hideOnEscape: ESC never
|
||||
-- dismisses it. Button2 is soulstone-only (HasSoulstone() is nil in v1 → hidden, and the
|
||||
-- OnCancel clicked-branch falls through to RepopMe()).
|
||||
StaticPopupDialogs["DEATH"] = {
|
||||
-- ── The corpse-side disposition ──────────────────────────────────────────────────────────
|
||||
-- Six of these eight dialogs are about a body on the floor, and every one of them wants
|
||||
-- the same three things from the engine: it must be legal to raise while you are dead
|
||||
-- (`whileDead` — that is the whole point of the arc), it takes precedence over a running
|
||||
-- cinematic, and a logout must not quietly close it out from under you. Stamped on here
|
||||
-- instead of retyped six times; the fields still land on the entry, so an addon reading
|
||||
-- StaticPopupDialogs["DEATH"].whileDead sees exactly what the reference's does.
|
||||
local function register_corpse_dialog(which, entry)
|
||||
entry.whileDead = 1
|
||||
entry.interruptCinematic = 1
|
||||
entry.notClosableByLogout = 1
|
||||
StaticPopupDialogs[which] = entry
|
||||
end
|
||||
|
||||
-- How long a resurrect offer stands once it becomes answerable. It is both the engine's
|
||||
-- own countdown (`timeout`) and the value OnShow re-seeds past the recovery delay.
|
||||
local RESURRECT_OFFER_SECONDS = 60
|
||||
|
||||
-- ── The release dialog (spec: StaticPopup.lua l.375-414) ─────────────────────────────────
|
||||
-- OnShow seeds `timeleft` from GetReleaseTimeRemaining(), which the engine renders through
|
||||
-- its per-tick DEATH text; −1 means this map has no forced release (an instance), so the
|
||||
-- static wording goes in and nothing counts down. `timeout = 0` because the countdown is
|
||||
-- the SERVER's to enforce — it force-releases at 6:00 (0308 §1) — and there is deliberately
|
||||
-- no hideOnEscape: you cannot dismiss dying. Button2 appears only with a soulstone on you,
|
||||
-- which is never in v1 (HasSoulstone() is nil), so a cancel falls through to the release.
|
||||
register_corpse_dialog("DEATH", {
|
||||
text = DEATH_RELEASE_TIMER,
|
||||
button1 = DEATH_RELEASE,
|
||||
button2 = USE_SOULSTONE,
|
||||
DisplayButton2 = function()
|
||||
return HasSoulstone()
|
||||
end,
|
||||
OnShow = function()
|
||||
local name = this:GetName()
|
||||
this.timeleft = GetReleaseTimeRemaining()
|
||||
local text = HasSoulstone()
|
||||
if text then
|
||||
getglobal(this:GetName() .. "Button2"):SetText(text)
|
||||
end
|
||||
if this.timeleft == -1 then
|
||||
getglobal(this:GetName() .. "Text"):SetText(DEATH_RELEASE_NOTIMER)
|
||||
getglobal(name .. "Text"):SetText(DEATH_RELEASE_NOTIMER)
|
||||
end
|
||||
-- The soulstone names its own button ("Reincarnate", "Use Soulstone", …).
|
||||
local soulstone = HasSoulstone()
|
||||
if soulstone then
|
||||
getglobal(name .. "Button2"):SetText(soulstone)
|
||||
end
|
||||
end,
|
||||
OnAccept = function()
|
||||
RepopMe()
|
||||
end,
|
||||
OnCancel = function(data, reason)
|
||||
if reason == "override" then
|
||||
-- Only a real click on button2 is an answer. "override" and "timeout" are the
|
||||
-- engine taking the dialog away, and neither is the player choosing anything.
|
||||
if reason ~= "clicked" then
|
||||
return
|
||||
end
|
||||
if reason == "timeout" then
|
||||
return
|
||||
if HasSoulstone() then
|
||||
UseSoulstone()
|
||||
else
|
||||
RepopMe()
|
||||
end
|
||||
if reason == "clicked" then
|
||||
if HasSoulstone() then
|
||||
UseSoulstone()
|
||||
else
|
||||
RepopMe()
|
||||
end
|
||||
end
|
||||
end,
|
||||
DisplayButton2 = function()
|
||||
return HasSoulstone()
|
||||
end,
|
||||
timeout = 0,
|
||||
whileDead = 1,
|
||||
interruptCinematic = 1,
|
||||
notClosableByLogout = 1,
|
||||
cancels = "RECOVER_CORPSE",
|
||||
}
|
||||
})
|
||||
|
||||
-- ── RECOVER_CORPSE — StaticPopup.lua l.1182-1202 (slice 2's range gate shows it) ─────────
|
||||
StaticPopupDialogs["RECOVER_CORPSE"] = {
|
||||
-- ── At the corpse (spec: StaticPopup.lua l.1182-1202) — slice 2's range gate raises these.
|
||||
-- Accept is gated behind the recovery delay and, once taken, the dialog STAYS: reclaiming
|
||||
-- is the server's to confirm, and it does so with the descriptor deltas that stand you up.
|
||||
register_corpse_dialog("RECOVER_CORPSE", {
|
||||
StartDelay = GetCorpseRecoveryDelay,
|
||||
delayText = RECOVER_CORPSE_TIMER,
|
||||
text = RECOVER_CORPSE_TEXT,
|
||||
@@ -91,107 +116,113 @@
|
||||
return 1
|
||||
end,
|
||||
timeout = 0,
|
||||
whileDead = 1,
|
||||
interruptCinematic = 1,
|
||||
notClosableByLogout = 1,
|
||||
}
|
||||
StaticPopupDialogs["RECOVER_CORPSE_INSTANCE"] = {
|
||||
})
|
||||
-- Your body is inside a dungeon you are standing outside of: a notice, not an offer, so
|
||||
-- it carries no button at all.
|
||||
register_corpse_dialog("RECOVER_CORPSE_INSTANCE", {
|
||||
text = RECOVER_CORPSE_INSTANCE_TEXT,
|
||||
timeout = 0,
|
||||
whileDead = 1,
|
||||
interruptCinematic = 1,
|
||||
notClosableByLogout = 1,
|
||||
}
|
||||
})
|
||||
|
||||
-- ── The RESURRECT trio — StaticPopup.lua l.416-505: sickness / no-sickness / no-timer.
|
||||
-- Decline (or the 60 s timeout) answers the offer and re-shows DEATH if still unreleased.
|
||||
local function resurrect_entry(text, delayText)
|
||||
-- ── Somebody else's offer (spec: StaticPopup.lua l.416-505) ──────────────────────────────
|
||||
-- Three registry keys, one shape: with sickness, without, and without a recovery timer to
|
||||
-- wait out. They differ only in the wording and in whether the offer is delay-gated, so
|
||||
-- they are built rather than written three times.
|
||||
--
|
||||
-- Refusing an offer (or letting it lapse) has to undo the `cancels = "DEATH"` that raising
|
||||
-- it did — otherwise a declined resurrect leaves you dead with nothing on screen to
|
||||
-- release from.
|
||||
local function restore_release_dialog()
|
||||
if UnitIsDead("player") then
|
||||
StaticPopup_Show("DEATH")
|
||||
end
|
||||
end
|
||||
local function resurrect_offer(offer_text, delay_text)
|
||||
return {
|
||||
StartDelay = delayText and GetCorpseRecoveryDelay or nil,
|
||||
delayText = delayText,
|
||||
text = text,
|
||||
text = offer_text,
|
||||
button1 = ACCEPT,
|
||||
button2 = DECLINE,
|
||||
OnShow = function()
|
||||
this.timeleft = GetCorpseRecoveryDelay() + 60
|
||||
end,
|
||||
OnAccept = function()
|
||||
AcceptResurrect()
|
||||
end,
|
||||
OnCancel = function()
|
||||
DeclineResurrect()
|
||||
if UnitIsDead("player") then
|
||||
StaticPopup_Show("DEATH")
|
||||
end
|
||||
restore_release_dialog()
|
||||
end,
|
||||
timeout = 60,
|
||||
whileDead = 1,
|
||||
-- The countdown. Given a `delay_text` the offer is gated: Accept stays dead and
|
||||
-- that text counts the corpse-recovery cooldown down; without one (the no-timer
|
||||
-- variant) Accept is live at once. Either way the offer lapses a minute after it
|
||||
-- becomes answerable, which is why OnShow re-seeds `timeleft` past the gate
|
||||
-- instead of leaving `timeout`'s minute to start the moment it appears.
|
||||
StartDelay = delay_text and GetCorpseRecoveryDelay or nil,
|
||||
delayText = delay_text,
|
||||
OnShow = function()
|
||||
this.timeleft = GetCorpseRecoveryDelay() + RESURRECT_OFFER_SECONDS
|
||||
end,
|
||||
timeout = RESURRECT_OFFER_SECONDS,
|
||||
cancels = "DEATH",
|
||||
interruptCinematic = 1,
|
||||
notClosableByLogout = 1,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
end
|
||||
StaticPopupDialogs["RESURRECT"] = resurrect_entry(RESURRECT_REQUEST_TEXT, RESURRECT_REQUEST_TIMER)
|
||||
StaticPopupDialogs["RESURRECT_NO_SICKNESS"] =
|
||||
resurrect_entry(RESURRECT_REQUEST_NO_SICKNESS, RESURRECT_REQUEST_NO_SICKNESS_TIMER)
|
||||
StaticPopupDialogs["RESURRECT_NO_TIMER"] = resurrect_entry(RESURRECT_REQUEST_NO_SICKNESS, nil)
|
||||
register_corpse_dialog("RESURRECT",
|
||||
resurrect_offer(RESURRECT_REQUEST_TEXT, RESURRECT_REQUEST_TIMER))
|
||||
register_corpse_dialog("RESURRECT_NO_SICKNESS",
|
||||
resurrect_offer(RESURRECT_REQUEST_NO_SICKNESS, RESURRECT_REQUEST_NO_SICKNESS_TIMER))
|
||||
register_corpse_dialog("RESURRECT_NO_TIMER",
|
||||
resurrect_offer(RESURRECT_REQUEST_NO_SICKNESS))
|
||||
|
||||
-- ── The spirit healer's XP_LOSS two-step — StaticPopup.lua l.1134-1180: the first Accept
|
||||
-- rewrites to the "Are you sure" text (data cleared, dialog stays via return 1), the second
|
||||
-- sends AcceptXPLoss(); walking out of range auto-hides (the entry's own OnUpdate poll).
|
||||
StaticPopupDialogs["XP_LOSS"] = {
|
||||
text = CONFIRM_XP_LOSS,
|
||||
button1 = ACCEPT,
|
||||
button2 = CANCEL,
|
||||
OnAccept = function(data)
|
||||
if data then
|
||||
getglobal(this:GetParent():GetName() .. "Text"):SetText(
|
||||
format(CONFIRM_XP_LOSS_AGAIN, data))
|
||||
this:GetParent().data = nil
|
||||
-- ── The spirit healer's price (spec: StaticPopup.lua l.1134-1180) ────────────────────────
|
||||
-- Both variants ask twice. The first Accept rewrites the dialog to a blunter second ask
|
||||
-- and clears `data`; the truthy return keeps it open. The second Accept — now with no data
|
||||
-- — sends the answer. So `data` is at once the payload and the "which ask is this" flag,
|
||||
-- which is why the driver below hands the no-sickness variant a bare 1.
|
||||
--
|
||||
-- The second ask is a FUNCTION, not a string, because only one of the two has anything to
|
||||
-- substitute: the no-sickness wording carries a bare "25% durability" that format() would
|
||||
-- read as a conversion and choke on.
|
||||
--
|
||||
-- Neither offer outlives walking away from the healer, so both poll the distance.
|
||||
local function close_when_out_of_reach(elapsed, dialog)
|
||||
if not CheckSpiritHealerDist() then
|
||||
dialog:Hide()
|
||||
end
|
||||
end
|
||||
local function spirit_healer_confirm(first_ask, second_ask)
|
||||
return {
|
||||
text = first_ask,
|
||||
button1 = ACCEPT,
|
||||
button2 = CANCEL,
|
||||
OnAccept = function(data)
|
||||
if not data then
|
||||
AcceptXPLoss()
|
||||
return
|
||||
end
|
||||
-- `this` is the button the engine clicked; the dialog is its parent.
|
||||
local dialog = this:GetParent()
|
||||
getglobal(dialog:GetName() .. "Text"):SetText(second_ask(data))
|
||||
dialog.data = nil
|
||||
return 1
|
||||
else
|
||||
AcceptXPLoss()
|
||||
end
|
||||
end,
|
||||
OnUpdate = function(elapsed, dialog)
|
||||
if not CheckSpiritHealerDist() then
|
||||
dialog:Hide()
|
||||
end
|
||||
end,
|
||||
timeout = 0,
|
||||
exclusive = 1,
|
||||
whileDead = 1,
|
||||
showAlert = 1,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
StaticPopupDialogs["XP_LOSS_NO_SICKNESS"] = {
|
||||
text = CONFIRM_XP_LOSS_NO_SICKNESS,
|
||||
button1 = ACCEPT,
|
||||
button2 = CANCEL,
|
||||
OnAccept = function(data)
|
||||
if data then
|
||||
getglobal(this:GetParent():GetName() .. "Text"):SetText(
|
||||
CONFIRM_XP_LOSS_AGAIN_NO_SICKNESS)
|
||||
this:GetParent().data = nil
|
||||
return 1
|
||||
else
|
||||
AcceptXPLoss()
|
||||
end
|
||||
end,
|
||||
OnUpdate = function(elapsed, dialog)
|
||||
if not CheckSpiritHealerDist() then
|
||||
dialog:Hide()
|
||||
end
|
||||
end,
|
||||
timeout = 0,
|
||||
exclusive = 1,
|
||||
whileDead = 1,
|
||||
showAlert = 1,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
end,
|
||||
OnUpdate = close_when_out_of_reach,
|
||||
timeout = 0,
|
||||
exclusive = 1,
|
||||
whileDead = 1,
|
||||
showAlert = 1,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
end
|
||||
StaticPopupDialogs["XP_LOSS"] = spirit_healer_confirm(CONFIRM_XP_LOSS, function(sickness)
|
||||
return format(CONFIRM_XP_LOSS_AGAIN, sickness)
|
||||
end)
|
||||
StaticPopupDialogs["XP_LOSS_NO_SICKNESS"] = spirit_healer_confirm(
|
||||
CONFIRM_XP_LOSS_NO_SICKNESS,
|
||||
function()
|
||||
return CONFIRM_XP_LOSS_AGAIN_NO_SICKNESS
|
||||
end)
|
||||
|
||||
-- ── The death-event driver — UIParent.lua l.237-262 + l.399-436, transcribed ─────────────
|
||||
-- ── The driver (spec: UIParent.lua l.237-262 + l.399-436) ───────────────────────────────
|
||||
-- One switch over the arc's events. The reference spreads these arms through the whole of
|
||||
-- UIParent_OnEvent; ours has a frame of its own (below) and nothing else to share it with.
|
||||
function BenillaDeathDriver_OnEvent()
|
||||
if event == "PLAYER_DEAD" then
|
||||
if not StaticPopup_Visible("DEATH") then
|
||||
@@ -216,19 +247,20 @@
|
||||
StaticPopup_Show("RESURRECT_NO_TIMER", arg1)
|
||||
end
|
||||
elseif event == "CONFIRM_XP_LOSS" then
|
||||
local resSicknessTime = GetResSicknessDuration()
|
||||
if resSicknessTime then
|
||||
local dialog = StaticPopup_Show("XP_LOSS", resSicknessTime)
|
||||
if dialog then
|
||||
dialog.data = resSicknessTime
|
||||
end
|
||||
-- If the healer would charge resurrection sickness, its duration goes into the
|
||||
-- text; if not there is nothing to quote, and the bare 1 handed over instead is
|
||||
-- only the "this is still the first ask" marker the two-step above reads.
|
||||
local sickness = GetResSicknessDuration()
|
||||
local dialog
|
||||
if sickness then
|
||||
dialog = StaticPopup_Show("XP_LOSS", sickness)
|
||||
else
|
||||
local dialog = StaticPopup_Show("XP_LOSS_NO_SICKNESS")
|
||||
if dialog then
|
||||
dialog.data = 1
|
||||
end
|
||||
dialog = StaticPopup_Show("XP_LOSS_NO_SICKNESS")
|
||||
end
|
||||
-- The ref hides the gossip window under the confirm (UIParent.lua l.412).
|
||||
if dialog then
|
||||
dialog.data = sickness or 1
|
||||
end
|
||||
-- The confirm covers the healer's gossip window (spec: UIParent.lua l.412).
|
||||
local gossip = getglobal("GossipFrame")
|
||||
if gossip and gossip:IsVisible() then
|
||||
HideUIPanel(gossip)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<!--
|
||||
The duel UI (decision 0633): the reference's two duel dialogs on the shared StaticPopup
|
||||
engine (UiPanels.xml) plus the UIParent duel-event driver, transcribed from the patch-chain
|
||||
FrameXML (mpqx extraction, decision 0246): StaticPopup.lua l.1103-1120 (DUEL_REQUESTED +
|
||||
DUEL_OUTOFBOUNDS), UIParent.lua l.98-101 (the registrations) + l.378-393 (the event arms).
|
||||
Strings verbatim from GlobalStrings.lua.
|
||||
The duel UI (decision 0633): the two duel dialogs on our shared StaticPopup engine
|
||||
(UiPanels.xml) plus the duel-event driver. The patch-chain FrameXML (mpqx extraction,
|
||||
decision 0246) is the SPECIFICATION — StaticPopup.lua l.1103-1120 (DUEL_REQUESTED +
|
||||
DUEL_OUTOFBOUNDS), UIParent.lua l.98-101 (the registrations) + l.378-393 (the event arms)
|
||||
— and what it fixes is contract: the two dialog keys, the StaticPopupDialogs field names,
|
||||
the sound kit and the two timeouts are all addressed by name, so they are reproduced
|
||||
exactly while the entries around them are written here in our own shape. The strings are
|
||||
the shipped enUS values from GlobalStrings.lua, quoted as data.
|
||||
|
||||
Duels have no window of their own — these two dialogs ARE the whole duel UI. Everything
|
||||
else is engine-side: the "Duel starting: N" countdown and the outcome line are
|
||||
@@ -20,40 +23,45 @@
|
||||
the forfeit is the server's.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- ── Strings (GlobalStrings.lua, verbatim) ───────────────────────────────────────────────
|
||||
-- ── Strings (the shipped enUS GlobalStrings.lua values, quoted as data) ─────────────────
|
||||
local DUEL_REQUESTED = "%s has challenged you to a duel." -- l.957
|
||||
local DUEL_OUTOFBOUNDS_TIMER = "Exiting duel area, you will forfeit in %d %s." -- l.956
|
||||
local ACCEPT = "Accept"
|
||||
local DECLINE = "Decline"
|
||||
|
||||
-- ── The challenge dialog — StaticPopup.lua l.1103-1116, verbatim ────────────────────────
|
||||
-- No accept-guard flag (unlike PARTY_INVITE): the ref's entry declines only on an explicit
|
||||
-- ── The challenge dialog (spec: StaticPopup.lua l.1103-1116) ────────────────────────────
|
||||
-- Written scalars-first, handlers-last: what the dialog IS, then what answering it does.
|
||||
--
|
||||
-- No accept-guard flag (unlike PARTY_INVITE): the entry declines only on an explicit
|
||||
-- OnCancel, so a timeout or an override hides it WITHOUT sending anything. That is not an
|
||||
-- oversight to "fix" — the server's own 60 s is not a thing either; an unanswered challenge
|
||||
-- simply sits until one side acts. ESC does decline (hideOnEscape routes through OnCancel).
|
||||
-- simply sits until one side acts.
|
||||
StaticPopupDialogs["DUEL_REQUESTED"] = {
|
||||
text = DUEL_REQUESTED,
|
||||
button1 = ACCEPT,
|
||||
button2 = DECLINE,
|
||||
sound = "igPlayerInvite",
|
||||
timeout = 60,
|
||||
hideOnEscape = 1,
|
||||
-- The two buttons are the two replies. ESC arrives here as well — hideOnEscape routes
|
||||
-- an escape through OnCancel — so backing out and declining are deliberately one path.
|
||||
OnAccept = function()
|
||||
AcceptDuel()
|
||||
end,
|
||||
OnCancel = function()
|
||||
CancelDuel()
|
||||
end,
|
||||
timeout = 60,
|
||||
hideOnEscape = 1,
|
||||
}
|
||||
|
||||
-- ── The out-of-bounds warning — StaticPopup.lua l.1117-1120, verbatim ───────────────────
|
||||
-- Two fields and nothing else: the engine's countdown branch owns the text.
|
||||
-- ── The out-of-bounds warning (spec: StaticPopup.lua l.1117-1120) ───────────────────────
|
||||
-- Two fields and nothing else: no buttons, because there is nothing to answer, and no
|
||||
-- text of its own — the engine's countdown branch writes it every tick.
|
||||
StaticPopupDialogs["DUEL_OUTOFBOUNDS"] = {
|
||||
text = DUEL_OUTOFBOUNDS_TIMER,
|
||||
timeout = 10,
|
||||
}
|
||||
|
||||
-- ── The UIParent event arms (UIParent.lua l.378-393, verbatim) ──────────────────────────
|
||||
-- ── The duel event arms (spec: UIParent.lua l.378-393) ──────────────────────────────────
|
||||
function BenillaDuelDriver_OnEvent()
|
||||
if event == "DUEL_REQUESTED" then
|
||||
StaticPopup_Show("DUEL_REQUESTED", arg1)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The durability alert — the "armor guy" under the minimap (director-directed this round,
|
||||
with the doll-slot broken tint). Geometry, texture cells, colors, and the show/hide law are
|
||||
QUOTED from the real 1.12 DurabilityFrame (extracted Interface\FrameXML\DurabilityFrame.xml
|
||||
+ .lua): 11 alert regions off one Interface\Durability\UI-Durability-Icons atlas — 8 body
|
||||
pieces (Head, Shoulders, Chest, Waist, Legs, Feet, Wrists, Hands) that show TOGETHER when
|
||||
with the doll-slot broken tint). Geometry, texture cells and colors are MEASUREMENTS of the
|
||||
real 1.12 DurabilityFrame (extracted Interface\FrameXML\DurabilityFrame.xml + .lua) and the
|
||||
show/hide law is the same law, written here in our own terms: 11 alert regions off one
|
||||
Interface\Durability\UI-Durability-Icons atlas — 8 body pieces
|
||||
(Head, Shoulders, Chest, Waist, Legs, Feet, Wrists, Hands) that show TOGETHER when
|
||||
any of them alerts, plus 3 showSeparate pieces (Weapon, Shield/OffWeapon, Ranged) that each
|
||||
show only when itself alerted. The whole frame hides at zero alerts.
|
||||
|
||||
@@ -30,8 +31,9 @@
|
||||
-->
|
||||
<Script>
|
||||
<![CDATA[
|
||||
-- ref DurabilityFrame.lua, quoted verbatim (the iteration order is the alert INDEX the
|
||||
-- engine answers; `value.slot` names the texture).
|
||||
-- The alert regions, in the engine's own order: the array index IS the index
|
||||
-- GetInventoryAlertStatus answers for, and `slot` names the texture that draws it. The
|
||||
-- last three are the ones that speak for themselves; the eight above are one figure.
|
||||
INVENTORY_ALERT_STATUS_SLOTS = {
|
||||
[1] = {slot = "Head"},
|
||||
[2] = {slot = "Shoulders"},
|
||||
@@ -53,51 +55,70 @@
|
||||
[4] = {r = 0.93, g = 0.07, b = 0.07},
|
||||
}
|
||||
|
||||
-- ref DurabilityFrame_SetAlerts, quoted (the pairs() loop for the 1.12 `for index, value
|
||||
-- in TABLE do` iteration; the enchantTimer arm is the ref's own disabled residue).
|
||||
-- Which texture draws a slot. Every slot but the off hand has exactly one glyph named
|
||||
-- after it; the off hand has TWO — a shield and a second sword — and only one of them is
|
||||
-- ever on screen, so picking one is also hiding the other. That is the whole reason this
|
||||
-- is a lookup with a branch in it rather than a name concatenation.
|
||||
local function DurabilityGlyph(slot)
|
||||
if slot ~= "Shield" then
|
||||
return getglobal("Durability" .. slot)
|
||||
end
|
||||
if OffhandHasWeapon() then
|
||||
DurabilityShield:Hide()
|
||||
return DurabilityOffWeapon
|
||||
end
|
||||
DurabilityOffWeapon:Hide()
|
||||
return DurabilityShield
|
||||
end
|
||||
|
||||
-- Repaint the armour figure from the engine's per-region verdicts. Two passes, because the
|
||||
-- eight body pieces are ONE alert between them: any of them alerting shows all eight, and
|
||||
-- that is not known until the walk is over. (The enchantTimer nil is the reference's own
|
||||
-- disabled residue — statuses 1/2 are its switched-off enchant-expiry alerts, so nothing
|
||||
-- ever arms the timer and this only ever clears a field that was already nil.)
|
||||
function DurabilityFrame_SetAlerts()
|
||||
DurabilityFrame.enchantTimer = nil
|
||||
local numAlerts = 0
|
||||
local texture, color, showDurability
|
||||
for index, value in pairs(INVENTORY_ALERT_STATUS_SLOTS) do
|
||||
texture = getglobal("Durability" .. value.slot)
|
||||
if value.slot == "Shield" then
|
||||
if OffhandHasWeapon() then
|
||||
DurabilityShield:Hide()
|
||||
texture = DurabilityOffWeapon
|
||||
else
|
||||
DurabilityOffWeapon:Hide()
|
||||
texture = DurabilityShield
|
||||
end
|
||||
end
|
||||
local showDurability
|
||||
local body, numBody = {}, 0
|
||||
|
||||
local alertStatus = GetInventoryAlertStatus(index)
|
||||
color = INVENTORY_ALERT_COLORS[alertStatus]
|
||||
for index, value in pairs(INVENTORY_ALERT_STATUS_SLOTS) do
|
||||
local texture = DurabilityGlyph(value.slot)
|
||||
local color = INVENTORY_ALERT_COLORS[GetInventoryAlertStatus(index)]
|
||||
-- Alerted pieces wear their status colour at full strength; the rest sit in the
|
||||
-- faded white the art is authored against, which is what the body figure's
|
||||
-- un-alerted pieces read as when one of their neighbours has lit up.
|
||||
if color then
|
||||
texture:SetVertexColor(color.r, color.g, color.b, 1.0)
|
||||
if value.showSeparate then
|
||||
texture:Show()
|
||||
else
|
||||
showDurability = 1
|
||||
end
|
||||
numAlerts = numAlerts + 1
|
||||
texture:SetVertexColor(color.r, color.g, color.b, 1.0)
|
||||
else
|
||||
texture:SetVertexColor(1.0, 1.0, 1.0, 0.5)
|
||||
if value.showSeparate then
|
||||
end
|
||||
if value.showSeparate then
|
||||
-- Weapon, off hand, ranged: each is its own alert and its own decision.
|
||||
if color then
|
||||
texture:Show()
|
||||
else
|
||||
texture:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
for index, value in pairs(INVENTORY_ALERT_STATUS_SLOTS) do
|
||||
if not value.showSeparate then
|
||||
if showDurability then
|
||||
getglobal("Durability" .. value.slot):Show()
|
||||
else
|
||||
getglobal("Durability" .. value.slot):Hide()
|
||||
else
|
||||
numBody = numBody + 1
|
||||
body[numBody] = texture
|
||||
if color then
|
||||
showDurability = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, numBody do
|
||||
if showDurability then
|
||||
body[i]:Show()
|
||||
else
|
||||
body[i]:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- Nothing to warn about at all means no armour guy.
|
||||
if numAlerts > 0 then
|
||||
DurabilityFrame:Show()
|
||||
else
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The real client's UIErrorsFrame, now transcribed as what it actually IS: a
|
||||
The real client's UIErrorsFrame, now declared as what it actually IS: a
|
||||
`<MessageFrame>` (ref UIErrorsFrame.xml:4 — 512×60 at TOP (0,-122), displayDuration="5",
|
||||
insertMode="TOP", with `<FontString inherits="ErrorFont" justifyH="CENTER"/>` as its line
|
||||
font). UI_INFO_MESSAGE shows YELLOW (1,1,0) — the quest objective-progress toasts land
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<!--
|
||||
Benilla's named virtual Font objects: the COMPLETE set from the real 1.12.1 FrameXML
|
||||
`Fonts.xml`, transcribed verbatim — names, TTF paths, heights, colors, outlines, and
|
||||
`Fonts.xml`, quoted verbatim — names, TTF paths, heights, colors, outlines, and
|
||||
(critically) which families carry a `<Shadow>` and which don't. The reference copy is
|
||||
re-extractable from the client at any time with the `mpqx` bin (crates/benilla-mpq,
|
||||
virtual path `Interface\FrameXML\Fonts.xml`); this is our own authored file (MIT/Apache),
|
||||
@@ -84,7 +84,8 @@
|
||||
ITEM_QUALITY_COLORS[i].hex = GetItemQualityColor(i)
|
||||
end
|
||||
|
||||
-- ── RAID_CLASS_COLORS (ref Fonts.xml l.43-53), transcribed verbatim ───────────────────
|
||||
-- ── RAID_CLASS_COLORS (spec: ref Fonts.xml l.43-53 — same name, same nine keys, same
|
||||
-- channel values; our own way of writing them down) ───────────────────────────────────
|
||||
--
|
||||
-- The per-class colour every raid/party addon paints names with. **22 corpus addons** reach
|
||||
-- it — nine of them through `Babble-Class-2.0`, but seven more by hand (KLHThreatMeter,
|
||||
@@ -93,22 +94,35 @@
|
||||
-- file scope. That was the whole of the harness's remaining `table expected` row: three
|
||||
-- addons, one missing table, found by asking the blocker table which addons were behind it.
|
||||
--
|
||||
-- TWO THINGS THAT LOOK WRONG AND ARE THE REFERENCE'S: `SHAMAN` carries the same pink as
|
||||
-- `PALADIN` (shamans were Horde-only and paladins Alliance-only in 1.12, so the two never
|
||||
-- shared a raid frame and Blizzard never gave shamans their own), and there is no `ROGUE`
|
||||
-- yellow anywhere else in the client to reconcile it with. Transcribed as found; a later
|
||||
-- client's palette is not this one's.
|
||||
RAID_CLASS_COLORS = {
|
||||
["HUNTER"] = { r = 0.67, g = 0.83, b = 0.45 },
|
||||
["WARLOCK"] = { r = 0.58, g = 0.51, b = 0.79 },
|
||||
["PRIEST"] = { r = 1.0, g = 1.0, b = 1.0 },
|
||||
["PALADIN"] = { r = 0.96, g = 0.55, b = 0.73 },
|
||||
["MAGE"] = { r = 0.41, g = 0.8, b = 0.94 },
|
||||
["ROGUE"] = { r = 1.0, g = 0.96, b = 0.41 },
|
||||
["DRUID"] = { r = 1.0, g = 0.49, b = 0.04 },
|
||||
["SHAMAN"] = { r = 0.96, g = 0.55, b = 0.73 },
|
||||
["WARRIOR"] = { r = 0.78, g = 0.61, b = 0.43 },
|
||||
-- Written as the eight distinct triples plus a loop that lifts them into the `{r=,g=,b=}`
|
||||
-- records addons index (`RAID_CLASS_COLORS["HUNTER"].r`), the same spec-plus-builder shape
|
||||
-- ITEM_QUALITY_COLORS above takes. The only thing worth SAYING about a palette is which
|
||||
-- colours are in it, and a bare triple says that with no ceremony; the record shape is the
|
||||
-- addon-facing contract and belongs in the one line that builds it.
|
||||
--
|
||||
-- EIGHT triples for nine classes, and that is the 1.12 client's own doing: shamans have no
|
||||
-- colour of their own. They were Horde-only and paladins Alliance-only, so the two never
|
||||
-- shared a raid frame and the client hands SHAMAN the paladin pink. We ASSIGN it, which
|
||||
-- states the fact instead of re-typing three numbers and asking the next reader to believe
|
||||
-- a comment. (`ROGUE`'s yellow likewise reconciles with nothing else in the client. A later
|
||||
-- client's palette is not this one's; these are 1.12's.) Each class still gets its OWN
|
||||
-- record below, so an addon that writes to `.PALADIN.r` does not move SHAMAN — the
|
||||
-- reference's two equal-but-separate tables, preserved.
|
||||
local classRGB = {
|
||||
WARRIOR = { 0.78, 0.61, 0.43 },
|
||||
PALADIN = { 0.96, 0.55, 0.73 },
|
||||
HUNTER = { 0.67, 0.83, 0.45 },
|
||||
ROGUE = { 1.0, 0.96, 0.41 },
|
||||
PRIEST = { 1.0, 1.0, 1.0 },
|
||||
MAGE = { 0.41, 0.8, 0.94 },
|
||||
WARLOCK = { 0.58, 0.51, 0.79 },
|
||||
DRUID = { 1.0, 0.49, 0.04 },
|
||||
}
|
||||
classRGB.SHAMAN = classRGB.PALADIN
|
||||
RAID_CLASS_COLORS = {}
|
||||
for token, rgb in pairs(classRGB) do
|
||||
RAID_CLASS_COLORS[token] = { r = rgb[1], g = rgb[2], b = rgb[3] }
|
||||
end
|
||||
-- The three OTHER tables the reference's Fonts.xml `<Script>` block carries —
|
||||
-- CHAT_FONT_HEIGHTS, MATERIAL_TEXT_COLOR_TABLE, MATERIAL_TITLETEXT_COLOR_TABLE — are
|
||||
-- deliberately not here: **zero corpus addons name any of them**, and nothing benilla ships
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<!--
|
||||
The social window (decision 0668) — the reference's FriendsFrame, transcribed from the
|
||||
patch-chain FrameXML (mpqx extraction, decision 0246): `FriendsFrame.lua` (987 lines) and
|
||||
`FriendsFrame.xml` (the FriendsListFrame / IgnoreListFrame / WhoFrame subtrees). Geometry,
|
||||
row counts, art paths and behaviour are the ref's; the strings are GlobalStrings verbatim.
|
||||
The social window (decision 0668) — our FriendsFrame. The patch-chain FrameXML (mpqx
|
||||
extraction, decision 0246) is the SPECIFICATION: `FriendsFrame.lua` (987 lines) and
|
||||
`FriendsFrame.xml` (the FriendsListFrame / IgnoreListFrame / WhoFrame subtrees) fix the
|
||||
global names addons bind to, the geometry, the row counts and the art paths, and the
|
||||
strings are GlobalStrings' own. The code answering to all that is ours.
|
||||
|
||||
Shape: ONE window with a tab strip, and inside tab 1 a second Friends/Ignore toggle-tab
|
||||
pair — the ref's own two-level arrangement, not a simplification. `FriendsFrame.selectedTab`
|
||||
@@ -67,6 +68,17 @@
|
||||
"FriendsListFrame", "IgnoreListFrame", "WhoFrame",
|
||||
}
|
||||
|
||||
-- Six of this window's buttons are live only while a row of the right kind is selected,
|
||||
-- and each list re-decides all of its own on every repaint. One call per button, taking
|
||||
-- the condition, keeps that decision readable as the one-liner it actually is.
|
||||
local function set_enabled(button, on)
|
||||
if on then
|
||||
button:Enable()
|
||||
else
|
||||
button:Disable()
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Sub-frame switching (ref FriendsFrame_ShowSubFrame l.25-33) ────────────────────────
|
||||
function FriendsFrame_ShowSubFrame(frameName)
|
||||
for i = 1, table.getn(BENILLA_FRIENDS_SUBFRAMES) do
|
||||
@@ -142,28 +154,23 @@
|
||||
local numFriends = GetNumFriends()
|
||||
frame.selectedFriend = GetSelectedFriend()
|
||||
|
||||
-- The three action buttons follow the SELECTED friend, not the list: offline friends
|
||||
-- can be removed but not whispered or invited (ref l.154-172).
|
||||
if numFriends > 0 then
|
||||
if frame.selectedFriend == 0 then
|
||||
SetSelectedFriend(1)
|
||||
frame.selectedFriend = GetSelectedFriend()
|
||||
end
|
||||
local _, _, _, _, connected = GetFriendInfo(frame.selectedFriend)
|
||||
if connected then
|
||||
FriendsFrameSendMessageButton:Enable()
|
||||
FriendsFrameGroupInviteButton:Enable()
|
||||
else
|
||||
FriendsFrameSendMessageButton:Disable()
|
||||
FriendsFrameGroupInviteButton:Disable()
|
||||
end
|
||||
FriendsFrameRemoveFriendButton:Enable()
|
||||
else
|
||||
FriendsFrameSendMessageButton:Disable()
|
||||
FriendsFrameGroupInviteButton:Disable()
|
||||
FriendsFrameRemoveFriendButton:Disable()
|
||||
-- A non-empty list always has a selection; the first row stands in if nothing else
|
||||
-- does (spec: ref l.154-158).
|
||||
local anyFriends = numFriends > 0
|
||||
if anyFriends and frame.selectedFriend == 0 then
|
||||
SetSelectedFriend(1)
|
||||
frame.selectedFriend = GetSelectedFriend()
|
||||
end
|
||||
|
||||
-- The three action buttons follow the SELECTED friend, not the list — and only two of
|
||||
-- them need that friend to be online: you can drop someone from the list whether or
|
||||
-- not they are logged in, but there is nobody there to whisper or invite (ref
|
||||
-- l.159-172).
|
||||
local online = anyFriends and select(5, GetFriendInfo(frame.selectedFriend))
|
||||
set_enabled(FriendsFrameSendMessageButton, online)
|
||||
set_enabled(FriendsFrameGroupInviteButton, online)
|
||||
set_enabled(FriendsFrameRemoveFriendButton, anyFriends)
|
||||
|
||||
local offset = FauxScrollFrame_GetOffset(FriendsFrameFriendsScrollFrame)
|
||||
for i = 1, FRIENDS_TO_DISPLAY do
|
||||
local index = offset + i
|
||||
@@ -204,15 +211,12 @@
|
||||
local frame = FriendsFrame
|
||||
local numIgnores = GetNumIgnores()
|
||||
frame.selectedIgnore = GetSelectedIgnore()
|
||||
if numIgnores > 0 then
|
||||
if frame.selectedIgnore == 0 then
|
||||
SetSelectedIgnore(1)
|
||||
frame.selectedIgnore = GetSelectedIgnore()
|
||||
end
|
||||
FriendsFrameStopIgnoreButton:Enable()
|
||||
else
|
||||
FriendsFrameStopIgnoreButton:Disable()
|
||||
local anyIgnores = numIgnores > 0
|
||||
if anyIgnores and frame.selectedIgnore == 0 then
|
||||
SetSelectedIgnore(1)
|
||||
frame.selectedIgnore = GetSelectedIgnore()
|
||||
end
|
||||
set_enabled(FriendsFrameStopIgnoreButton, anyIgnores)
|
||||
|
||||
local offset = FauxScrollFrame_GetOffset(FriendsFrameIgnoreScrollFrame)
|
||||
for i = 1, IGNORES_TO_DISPLAY do
|
||||
@@ -290,13 +294,11 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- The two who buttons need a selected row (ref l.302-309).
|
||||
if not frame.selectedWho then
|
||||
WhoFrameGroupInviteButton:Disable()
|
||||
WhoFrameAddFriendButton:Disable()
|
||||
else
|
||||
WhoFrameGroupInviteButton:Enable()
|
||||
WhoFrameAddFriendButton:Enable()
|
||||
-- The two who buttons need a selected row, and both act by NAME — so the selection is
|
||||
-- resolved to one here and the buttons read it back (ref l.302-309).
|
||||
set_enabled(WhoFrameGroupInviteButton, frame.selectedWho)
|
||||
set_enabled(WhoFrameAddFriendButton, frame.selectedWho)
|
||||
if frame.selectedWho then
|
||||
frame.selectedName = GetWhoInfo(frame.selectedWho)
|
||||
end
|
||||
|
||||
|
||||
@@ -36,11 +36,13 @@
|
||||
· **The sounds** — the ref's own OnClick lines: igMainMenuOption (the five option buttons),
|
||||
igMainMenuLogout, igMainMenuQuit, igMainMenuContinue. All four kits verified present in
|
||||
SoundEntries.dbc (patch-2.MPQ) alongside the igMainMenuOpen/Close pair already in use.
|
||||
· **The two logout dialogs** — ref `StaticPopup.lua` l.564-600 (CAMP + QUIT), transcribed
|
||||
onto the shared popup engine (UiPanels.xml), whose countdown-text branch already lists
|
||||
both `which` names. Their driver events are the reference's own (`re/events/event-catalog
|
||||
.tsv`: 276 PLAYER_CAMPING, 277 PLAYER_QUITING, 278 LOGOUT_CANCEL), fired by
|
||||
`crate::ui_logout` off `SMSG_LOGOUT_RESPONSE`.
|
||||
· **The two logout dialogs** — ref `StaticPopup.lua` l.564-600 (CAMP + QUIT) is the
|
||||
SPECIFICATION for them: the two dialog keys, the field names, the 20 s timeout and the
|
||||
asymmetric button sets are contract, and the entries carrying them are written here in
|
||||
our own shape on the shared popup engine (UiPanels.xml), whose countdown-text branch
|
||||
already lists both `which` names. Their driver events are the reference's own
|
||||
(`re/events/event-catalog.tsv`: 276 PLAYER_CAMPING, 277 PLAYER_QUITING,
|
||||
278 LOGOUT_CANCEL), fired by `crate::ui_logout` off `SMSG_LOGOUT_RESPONSE`.
|
||||
|
||||
NAMES are the reference's, unprefixed (`GameMenuFrame`, `GameMenuButton*`) — the same call
|
||||
MicroMenu.xml made for its straight transcriptions, and required here besides: `ToggleGameMenu`
|
||||
@@ -114,51 +116,59 @@
|
||||
button:Disable()
|
||||
end
|
||||
|
||||
-- ── The two logout dialogs (ref StaticPopup.lua l.564-600) ──────────────────────────────
|
||||
-- ── The two logout dialogs (spec: StaticPopup.lua l.564-600) ────────────────────────────
|
||||
-- Both are countdown dialogs: their `text` is a format string the popup engine's per-tick
|
||||
-- branch fills with the remaining time (StaticPopup_OnUpdate's which-list already names CAMP
|
||||
-- and QUIT), so they open blank and count down. `timeout = 20` matches the server's own
|
||||
-- logout timer (vmangos `WorldSession::LogoutRequest` → 20 s), which is what the countdown
|
||||
-- is displaying — the client is not the authority here, it is narrating the server's clock.
|
||||
--
|
||||
-- The asymmetry between them is the reference's: CAMP offers only Cancel (the commented-out
|
||||
-- The asymmetry between them is the spec's: CAMP offers only Cancel (the commented-out
|
||||
-- CAMP_NOW/ForceLogout button is dead in 1.12 — "uncomment once forced logouts are completely
|
||||
-- implemented"), while QUIT offers Exit now + Cancel, because a force-quit needs no server
|
||||
-- round trip. Closing either one EARLY (button, ESC, or an override) cancels the logout;
|
||||
-- letting it run out does not — the engine zeroes `timeleft` before hiding, which is exactly
|
||||
-- what the ref's `if this.timeleft > 0` guard reads.
|
||||
-- round trip.
|
||||
--
|
||||
-- Both entries below are written scalars-first, handlers-last, so the dialog's SHAPE reads
|
||||
-- before its behaviour does.
|
||||
|
||||
-- Dismissing a running countdown means "I didn't mean it": call the logout off. Only while
|
||||
-- it is still RUNNING, though — the popup engine zeroes `timeleft` before hiding a dialog
|
||||
-- that expired, so reaching zero leaves the logout alone. (Backwards, and every logout in
|
||||
-- the field is cancelled by its own dialog at t=0 and the character never leaves.) One
|
||||
-- function because both dialogs mean exactly this; `this` is the popup frame, which the
|
||||
-- engine has bound around the handler.
|
||||
local function CancelLogoutIfStillCounting()
|
||||
if this.timeleft > 0 then
|
||||
CancelLogout()
|
||||
end
|
||||
end
|
||||
|
||||
StaticPopupDialogs["CAMP"] = {
|
||||
text = CAMP_TIMER,
|
||||
button1 = CANCEL,
|
||||
OnAccept = function()
|
||||
CancelLogout()
|
||||
end,
|
||||
OnHide = function()
|
||||
if this.timeleft > 0 then
|
||||
CancelLogout()
|
||||
end
|
||||
end,
|
||||
timeout = 20,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
OnAccept = function()
|
||||
CancelLogout()
|
||||
end,
|
||||
OnHide = CancelLogoutIfStillCounting,
|
||||
}
|
||||
|
||||
StaticPopupDialogs["QUIT"] = {
|
||||
text = QUIT_TIMER,
|
||||
button1 = QUIT_NOW,
|
||||
button2 = CANCEL,
|
||||
timeout = 20,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
-- A force-quit needs no server round trip, so the countdown it was narrating is over
|
||||
-- the moment the button is pressed: zero `timeleft` so the OnHide below stands down.
|
||||
OnAccept = function()
|
||||
ForceQuit()
|
||||
this.timeleft = 0
|
||||
end,
|
||||
OnHide = function()
|
||||
if this.timeleft > 0 then
|
||||
CancelLogout()
|
||||
end
|
||||
end,
|
||||
timeout = 20,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
OnHide = CancelLogoutIfStillCounting,
|
||||
}
|
||||
|
||||
-- The camp/quit event driver. The ref handles these three in UIParent's own OnEvent
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<!--
|
||||
The minimap's time-of-day indicator (GameTimeFrame) — transcribed from the 1.12 FrameXML
|
||||
GameTime.xml + GameTime.lua (the reference's own file split; its TOC loads GameTime.xml right
|
||||
after Minimap.xml, and the manifest mirrors that beside MinimapCluster.xml). This closes the
|
||||
"day/night indicator" line in MinimapCluster.xml's OUT list.
|
||||
The minimap's time-of-day indicator (GameTimeFrame) — our implementation of the behaviour the
|
||||
1.12 FrameXML GameTime.xml + GameTime.lua specify, in their own file split (the reference TOC
|
||||
loads GameTime.xml right after Minimap.xml, and the manifest mirrors that beside
|
||||
MinimapCluster.xml). The Lua below is ours; the published names (GameTimeFrame_Update,
|
||||
GameTime_GetTime, GAMETIME_DAWN/DUSK, GameTimeTexture) and every number are the reference's,
|
||||
because addons call the first by name and the second are measurements of the real interface.
|
||||
This closes the "day/night indicator" line in MinimapCluster.xml's OUT list.
|
||||
|
||||
The 50×50 frame overhanging the cluster's top-right corner shows
|
||||
Interface\Minimap\UI-TOD-Indicator (128×64: the sun disc on the left half, the moon on the
|
||||
@@ -17,90 +20,81 @@
|
||||
`ui/scratch/scripts-auto-enable.md`, resolved 2026-08-07) — benilla's loader models the same
|
||||
law (`loader/scripts.rs`), so this file stays attribute-free like the reference.
|
||||
|
||||
Deltas from the reference, each deliberate:
|
||||
- TEXT() is dropped (this file set's convention: the ref's build-time no-op localization
|
||||
macro; CharacterFrame.xml's note).
|
||||
Deltas from the reference's behaviour, each deliberate:
|
||||
- No TEXT() wrapper around the format strings (this file set's convention: the ref's
|
||||
build-time no-op localization macro; CharacterFrame.xml's note).
|
||||
- TwentyFourHourTime = 1 is SET here: the reference sets it in Localization.lua's
|
||||
LocalizeFrames() — this install is enGB, and enGB is a 24-hour locale — called at
|
||||
VARIABLES_LOADED. benilla fires that event since decision 1128 and could carry this as a saved
|
||||
variable (it is on 1128's follow-on list), but the value here is a LOCALE fact, not a player
|
||||
toggle: this install is enGB and enGB is a 24-hour locale. So the tooltip reads "21:07" exactly
|
||||
as the reference client on this install does; the 12-hour AM/PM branches stay verbatim below
|
||||
for the day a locale/CVar drives it.
|
||||
as the reference client on this install does; the 12-hour AM/PM path below is implemented and
|
||||
kept live for the day a locale/CVar drives it.
|
||||
-->
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<Script><![CDATA[
|
||||
-- ref-GameTime.lua, verbatim (minus TEXT(); TwentyFourHourTime per the header).
|
||||
-- Set here rather than by Localization.lua's LocalizeFrames — see the header.
|
||||
TwentyFourHourTime = 1;
|
||||
|
||||
-- The reference's own thresholds, in minutes since midnight (game time), and its own
|
||||
-- global names: an addon that wants to know when night falls reads exactly these.
|
||||
GAMETIME_DAWN = ( 5 * 60) + 30; -- 5:30 AM
|
||||
GAMETIME_DUSK = (21 * 60) + 0; -- 9:00 PM
|
||||
|
||||
-- UI-TOD-Indicator is one 128x64 sheet carrying two 50-px icons side by side: the sun at
|
||||
-- u=0, the moon exactly half a sheet to its right. So the whole day/night decision is
|
||||
-- which of the two u origins the 50-px window sits on.
|
||||
local TOD_ICON_WIDTH = 50/128;
|
||||
local TOD_ICON_HEIGHT = 50/64;
|
||||
local TOD_NIGHT_ORIGIN = 0.5;
|
||||
|
||||
function GameTimeFrame_Update()
|
||||
local hour, minute = GetGameTime();
|
||||
local time = (hour * 60) + minute;
|
||||
if(time ~= this.timeOfDay) then
|
||||
this.timeOfDay = time;
|
||||
local minx = 0;
|
||||
local maxx = 50/128;
|
||||
local miny = 0;
|
||||
local maxy = 50/64;
|
||||
if(time < GAMETIME_DAWN or time >= GAMETIME_DUSK) then
|
||||
minx = minx + 0.5;
|
||||
maxx = maxx + 0.5;
|
||||
end
|
||||
GameTimeTexture:SetTexCoord(minx, maxx, miny, maxy);
|
||||
|
||||
if(GameTooltip:IsOwned(this)) then
|
||||
GameTimeFrame_UpdateTooltip(hour, minute);
|
||||
end
|
||||
local minuteOfDay = (hour * 60) + minute;
|
||||
-- OnUpdate calls this every frame; only a changed game MINUTE is worth any work.
|
||||
if minuteOfDay == this.timeOfDay then
|
||||
return;
|
||||
end
|
||||
this.timeOfDay = minuteOfDay;
|
||||
|
||||
local origin = 0;
|
||||
if minuteOfDay < GAMETIME_DAWN or minuteOfDay >= GAMETIME_DUSK then
|
||||
origin = TOD_NIGHT_ORIGIN;
|
||||
end
|
||||
GameTimeTexture:SetTexCoord(origin, origin + TOD_ICON_WIDTH, 0, TOD_ICON_HEIGHT);
|
||||
|
||||
-- A tooltip we still own is showing last minute's clock; re-word it in place.
|
||||
if GameTooltip:IsOwned(this) then
|
||||
GameTimeFrame_UpdateTooltip(hour, minute);
|
||||
end
|
||||
end
|
||||
|
||||
-- The one clock formatter, since the tooltip and the getter below want the same string in
|
||||
-- two different wrappers. TwentyFourHourTime picks the plain HH:MM form; otherwise the
|
||||
-- hour folds into 1..12 (midnight and noon both being 12) and the AM/PM half is carried by
|
||||
-- which format string is used, not by any text this file spells out.
|
||||
local function clockText(hour, minute)
|
||||
if TwentyFourHourTime then
|
||||
return format(TIME_TWENTYFOURHOURS, hour, minute);
|
||||
end
|
||||
local afternoon = (hour >= 12);
|
||||
if hour > 12 then
|
||||
hour = hour - 12;
|
||||
elseif hour == 0 then
|
||||
hour = 12;
|
||||
end
|
||||
if afternoon then
|
||||
return format(TIME_TWELVEHOURPM, hour, minute);
|
||||
end
|
||||
return format(TIME_TWELVEHOURAM, hour, minute);
|
||||
end
|
||||
|
||||
function GameTimeFrame_UpdateTooltip(hours, minutes)
|
||||
if(TwentyFourHourTime) then
|
||||
GameTooltip:SetText(format(TIME_TWENTYFOURHOURS, hours, minutes));
|
||||
else
|
||||
local pm = 0;
|
||||
if(hours >= 12) then
|
||||
pm = 1;
|
||||
end
|
||||
if(hours > 12) then
|
||||
hours = hours - 12;
|
||||
end
|
||||
if(hours == 0) then
|
||||
hours = 12;
|
||||
end
|
||||
if(pm == 0) then
|
||||
GameTooltip:SetText(format(TIME_TWELVEHOURAM, hours, minutes));
|
||||
else
|
||||
GameTooltip:SetText(format(TIME_TWELVEHOURPM, hours, minutes));
|
||||
end
|
||||
end
|
||||
GameTooltip:SetText(clockText(hours, minutes));
|
||||
end
|
||||
|
||||
function GameTime_GetTime()
|
||||
local hour, minute = GetGameTime();
|
||||
|
||||
if(TwentyFourHourTime) then
|
||||
return format(TIME_TWENTYFOURHOURS, hour, minute);
|
||||
else
|
||||
local pm = 0;
|
||||
if(hour >= 12) then
|
||||
pm = 1;
|
||||
end
|
||||
if(hour > 12) then
|
||||
hour = hour - 12;
|
||||
end
|
||||
if(hour == 0) then
|
||||
hour = 12;
|
||||
end
|
||||
if(pm == 0) then
|
||||
return format(TIME_TWELVEHOURAM, hour, minute);
|
||||
else
|
||||
return format(TIME_TWELVEHOURPM, hour, minute);
|
||||
end
|
||||
end
|
||||
return clockText(GetGameTime());
|
||||
end
|
||||
]]></Script>
|
||||
<Frame name="GameTimeFrame" parent="MinimapCluster">
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
tooltip.default = 1
|
||||
end
|
||||
|
||||
-- The two-line UI-element tooltip (ref-GameTooltip.lua l.107-123, quoted): a white label,
|
||||
-- The two-line UI-element tooltip (spec: ref-GameTooltip.lua l.107-123): a white label,
|
||||
-- then the long explanation wrapped underneath in NORMAL_FONT_COLOR — the whole reason a
|
||||
-- 1.12 hover over the ping meter or a micro button is a paragraph and not four words.
|
||||
-- With detailed tips on it seats at the DEFAULT corner, not beside the owner; with them off
|
||||
@@ -115,25 +115,34 @@
|
||||
BenillaTooltip_AddNewbieTipFor(this, normalText, r, g, b, newbieText, noNormalText)
|
||||
end
|
||||
|
||||
-- The explicit-owner form our FrameXML uses. Same body the reference's has, with the owner
|
||||
-- threaded rather than taken from `this` — kept under a Benilla name so it can never be
|
||||
-- mistaken for a reference global an addon might call or replace.
|
||||
-- The explicit-owner form our FrameXML uses, and where the behaviour above actually lives:
|
||||
-- the owner is threaded in rather than read off `this`. Kept under a Benilla name so it can
|
||||
-- never be mistaken for a reference global an addon might call or replace.
|
||||
function BenillaTooltip_AddNewbieTipFor(owner, normalText, r, g, b, newbieText, noNormalText)
|
||||
if SHOW_NEWBIE_TIPS == "1" then
|
||||
GameTooltip_SetDefaultAnchor(GameTooltip, owner)
|
||||
if normalText then
|
||||
GameTooltip:SetText(normalText, r, g, b)
|
||||
GameTooltip:AddLine(newbieText, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
|
||||
else
|
||||
GameTooltip:SetText(newbieText, r, g, b, 1, 1)
|
||||
end
|
||||
GameTooltip:Show()
|
||||
else
|
||||
-- Detailed tips OFF is the short shape: the label alone, beside whatever it labels.
|
||||
-- A caller that says its label is worthless on its own then has nothing left to show.
|
||||
if SHOW_NEWBIE_TIPS ~= "1" then
|
||||
if not noNormalText then
|
||||
GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(normalText, r, g, b)
|
||||
end
|
||||
return
|
||||
end
|
||||
-- Detailed tips ON: a paragraph, and paragraphs go to the screen corner instead of
|
||||
-- crowding the thing they explain.
|
||||
GameTooltip_SetDefaultAnchor(GameTooltip, owner)
|
||||
if normalText then
|
||||
-- Label in the caller's colour; the explanation under it, wrapped, always in
|
||||
-- NORMAL_FONT_COLOR — the two lines are deliberately not the same colour.
|
||||
GameTooltip:SetText(normalText, r, g, b)
|
||||
GameTooltip:AddLine(newbieText, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
|
||||
else
|
||||
-- No label, so the explanation IS the tooltip: it takes the caller's colour, plus
|
||||
-- SetText's alpha and wrap flags (args 5 and 6) — the wrap the second line above
|
||||
-- would otherwise have carried.
|
||||
GameTooltip:SetText(newbieText, r, g, b, 1, 1)
|
||||
end
|
||||
GameTooltip:Show()
|
||||
end
|
||||
|
||||
-- ref GameTooltip_OnHide (ref-GameTooltip.lua l.100-104, quoted): the plate colours
|
||||
@@ -219,32 +228,38 @@
|
||||
return 1.0, 1.0, 1.0
|
||||
end
|
||||
|
||||
-- HealthBar_OnValueChanged (ref-HealthBar.lua, quoted; the tooltip bar passes no
|
||||
-- `smooth`, so the fill reads the ref's plain green).
|
||||
-- The health bar's own fill colour (spec: ref-HealthBar.lua, the whole file).
|
||||
--
|
||||
-- Two colourings in one handler. `smooth` asks for the health RAMP — green at the top of
|
||||
-- the range, red at the bottom, and between them a two-segment fade that pins one channel
|
||||
-- at full and slides the other, so the halfway mark reads solid yellow. Without it the bar
|
||||
-- is flat green whatever it reads. Blue is 0 either way, which is what keeps the fade
|
||||
-- inside the red-yellow-green wedge and never lets it wash out.
|
||||
--
|
||||
-- **Nothing passes `smooth` — not us and not the reference.** Verified: our only caller is
|
||||
-- the status bar's `<OnValueChanged>` below (one argument), and 1.12's own two callers are
|
||||
-- ref-GameTooltipTemplate.xml l.593 and ref-UnitFrame.lua l.181, both of which forward
|
||||
-- `value` alone. So the ramp has never coloured a bar in this game; it is kept because it
|
||||
-- is the contract an addon replacing a unit frame may still call with two arguments.
|
||||
--
|
||||
-- A value outside the bar's own range leaves the colour untouched rather than being clamped
|
||||
-- to an end of the ramp — the reference's own guard, kept as the reference has it.
|
||||
function HealthBar_OnValueChanged(value, smooth)
|
||||
if not value then return end
|
||||
local r, g, b
|
||||
local min, max = this:GetMinMaxValues()
|
||||
local bar = this
|
||||
local min, max = bar:GetMinMaxValues()
|
||||
if value < min or value > max then return end
|
||||
if (max - min) > 0 then
|
||||
value = (value - min) / (max - min)
|
||||
else
|
||||
value = 0
|
||||
end
|
||||
local r, g = 0.0, 1.0
|
||||
if smooth then
|
||||
if value > 0.5 then
|
||||
r = (1.0 - value) * 2
|
||||
g = 1.0
|
||||
local filled = 0
|
||||
if max > min then filled = (value - min) / (max - min) end
|
||||
if filled > 0.5 then
|
||||
r, g = (1.0 - filled) * 2, 1.0
|
||||
else
|
||||
r = 1.0
|
||||
g = value * 2
|
||||
r, g = 1.0, filled * 2
|
||||
end
|
||||
else
|
||||
r = 0.0
|
||||
g = 1.0
|
||||
end
|
||||
b = 0.0
|
||||
this:SetStatusBarColor(r, g, b)
|
||||
bar:SetStatusBarColor(r, g, 0.0)
|
||||
end
|
||||
|
||||
-- `self or this` for the same reason as GameTooltip_OnHide: an addon inheriting
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's group-loot roll window (decision 0591) — the Need/Greed/Pass dialog: four stacked
|
||||
`GroupLootFrame`s, transcribed from the real 1.12 client (`LootFrame.xml` l.243-533,
|
||||
`GroupLootFrame`s, re-expressed from the real 1.12 client (`LootFrame.xml` l.243-533,
|
||||
`LootFrame.lua` l.246-296, `UIParent.lua` l.513-515/1577 — all extracted, scratchpad
|
||||
`WoW/_extracted_framexml/`). Geometry, art paths, and behavior below are QUOTED from those
|
||||
(line citations inline); the Lua is transcribed logic turned onto our own `loot_roll` engine
|
||||
(line citations inline); the Lua is re-expressed logic turned onto our own `loot_roll` engine
|
||||
seam (`benilla-ui/src/script/loot_roll.rs`, decision 0591) and its app feed
|
||||
(`crates/benilla/src/ui_loot_roll.rs`).
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
THE DRESSING-ROOM / CHAT-LINK CLICK LANDED (decisions 1059/1060 — this note used to read "no
|
||||
dressing room exists", which was true until the room did). The ref icon button's Ctrl-click
|
||||
`DressUpItemLink`/Shift-click `ChatFrameEditBox:Insert` (ref l.353-361) are transcribed in
|
||||
`DressUpItemLink`/Shift-click `ChatFrameEditBox:Insert` (ref l.353-361) are re-expressed in
|
||||
`BenillaGroupLootFrame_Icon_OnClick` below, over the `GetLootRollItemLink(rollID)` binding the
|
||||
same arc added to `loot_roll.rs`. Still dropped: the ref's `this.hasItem = 1` OnLoad flag
|
||||
(ref l.338-340) and its `CursorUpdate()`/`ResetCursor()`/`CursorOnUpdate()` hover-cursor calls
|
||||
@@ -274,7 +274,7 @@
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
|
||||
-- The item icon's click (ref $parentIconFrame OnClick, LootFrame.xml l.353-361, transcribed
|
||||
-- The item icon's click (ref $parentIconFrame OnClick, LootFrame.xml l.353-361, re-expressed
|
||||
-- whole): CTRL previews the rolled item in the dressing room (decision 1060), SHIFT posts
|
||||
-- its link into an open chat edit box (decision 1059) — "link me that" being most of what a
|
||||
-- roll popup gets asked. Both read `GetLootRollItemLink(rollID)`, the binding this arc added
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
|
||||
-- ref ToggleInspect (l.67-83) + InspectFrame_ShowSubFrame (l.85-93). With one page shipped
|
||||
-- the interesting branch is the ref's own "clicking the tab of the page already showing
|
||||
-- CLOSES the window" — transcribed rather than smoothed away, because that is what the real
|
||||
-- CLOSES the window" — kept rather than smoothed away, because that is what the real
|
||||
-- tab does. ToggleInspect is BARE (not Benilla-prefixed), the addon-facing name, matching
|
||||
-- this codebase's ToggleCharacter/ToggleQuestLog precedent.
|
||||
function ToggleInspect(tab)
|
||||
@@ -178,7 +178,7 @@
|
||||
end
|
||||
|
||||
-- ref InspectFrame_OnUpdate (l.61-65): the window closes the moment there is no target.
|
||||
-- Transcribed verbatim including its bluntness — the ref checks "target" specifically, even
|
||||
-- The ref's behaviour kept, bluntness included — the ref checks "target" specifically, even
|
||||
-- when inspecting a party token, so inspecting a party member and then clearing your target
|
||||
-- closes the window. That is the shipped 1.12 behavior.
|
||||
function BenillaInspectFrame_OnUpdate(frame)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<!--
|
||||
The reference's `ItemButtonTemplate.lua` helper family (transcribed whole, like
|
||||
TextStatusBar.xml) — the eight `SetItemButton*` globals every 1.12 window uses to paint an item
|
||||
slot: its icon, its stack count, its merchant stock line, and the four vertex-colour verbs.
|
||||
Our implementation of the eight `SetItemButton*` globals every 1.12 window uses to paint an item
|
||||
slot: its icon, its stack count, its merchant stock line, and the four vertex-colour verbs. The
|
||||
specification is the reference's `ItemButtonTemplate.lua` — the names, the argument order and the
|
||||
behaviour of every branch are its contract; the code below is ours, and it is factored the way
|
||||
eight near-identical verbs want to be factored (one region lookup, one tint verb) rather than the
|
||||
way eight separately-written ones came out.
|
||||
|
||||
**Why this file exists, and what it cost to be without it.** The director installed Bagnon, hit
|
||||
B, and got a frame with a title and a gold line and NO BAG SLOTS. `Bagnon_Core/core/Item.lua`
|
||||
@@ -17,8 +20,8 @@
|
||||
reproduction.
|
||||
|
||||
These are FrameXML, not engine (decision 1190's split): the real client ships them as Lua in
|
||||
`Interface\FrameXML\ItemButtonTemplate.lua`, so they belong here beside our other
|
||||
transcriptions rather than in a Rust binding. Read off the install's own copy with the
|
||||
`Interface\FrameXML\ItemButtonTemplate.lua`, so ours belong here in our own interface rather
|
||||
than in a Rust binding. The specification was read off the install's own copy with the
|
||||
`mpqcat` example over `WoW/Data/interface.MPQ` — not from memory.
|
||||
(An XML comment may not contain a double hyphen, which is why no command line is spelled out
|
||||
here; `benilla-mpq/examples/mpqcat.rs`'s own header carries the invocation.)
|
||||
@@ -37,8 +40,8 @@
|
||||
|
||||
Six addons in all — Bagnon_Core, CT_MailMod, CT_MasterMod, AdvancedTradeSkillWindow,
|
||||
QuestHistory, SpecialTalentUI. The last two rows have **zero** corpus callers and are here
|
||||
anyway because this is one reference file transcribed whole and they are two lines each; that
|
||||
is stated rather than dressed up as demand (1210).
|
||||
anyway because the family is one contract and, once the tint verb below is shared, they are one
|
||||
line each; that is stated rather than dressed up as demand (1210).
|
||||
|
||||
**The virtual `ItemButtonTemplate` Button is deliberately NOT here.** The harness's template
|
||||
demand ranks it at zero on both axes — no corpus addon names it in a `CreateFrame` fourth
|
||||
@@ -55,29 +58,47 @@
|
||||
-- `$parent…` template shape, and it is why these are free functions taking a button rather
|
||||
-- than widget methods: any button that follows the naming works, whoever built it.
|
||||
--
|
||||
-- Nil guards are the REFERENCE's, not ours — one on `button` per verb, and one on the icon
|
||||
-- region in SetItemButtonDesaturated. A missing region anywhere else raises, and that is
|
||||
-- the right loudness: "this is not an item button" is a real mistake, and 1203's lesson is
|
||||
-- that the silent version of a failure is the expensive one.
|
||||
-- TWO DIFFERENT NILS, and the difference is the whole error policy of this file.
|
||||
-- A nil BUTTON is a no-op everywhere: callers hand us whatever their loop is holding, and
|
||||
-- the reference tolerates that in all eight verbs, so we do too. A button that is missing
|
||||
-- the REGION is not tolerated — the lookup yields nil and the very next `:` raises. That
|
||||
-- loudness is deliberate: "this is not an item button" is a real mistake by the caller, and
|
||||
-- 1203's lesson is that the silent version of a failure is the expensive one.
|
||||
|
||||
-- One named region off a button, which is the only address these verbs ever compute. Nil in,
|
||||
-- nil out, so the button guard reads as one expression instead of eight copied early
|
||||
-- returns.
|
||||
local function region(button, suffix)
|
||||
return button and getglobal(button:GetName() .. suffix)
|
||||
end
|
||||
|
||||
-- Four of the eight differ in nothing but which region they tint, so they share the verb
|
||||
-- and name the region. The guard is on the BUTTON and deliberately not on the region it
|
||||
-- resolved: a nil button is the tolerated case, a button carrying no such region is the
|
||||
-- loud one. `texture` is a local rather than an inline call only so that the raise names
|
||||
-- which lookup came back empty.
|
||||
local function tint(button, suffix, r, g, b)
|
||||
local texture = region(button, suffix)
|
||||
if button then
|
||||
texture:SetVertexColor(r, g, b)
|
||||
end
|
||||
end
|
||||
|
||||
function SetItemButtonCount(button, count)
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
if not count then
|
||||
count = 0
|
||||
end
|
||||
count = count or 0
|
||||
button.count = count
|
||||
local label = region(button, "Count")
|
||||
-- `isBag` is the reference's own flag for a bag button, where even a single item
|
||||
-- counts as a number worth showing.
|
||||
-- counts as a number worth showing. Above 999 the slot shows a bare `*` — the stack
|
||||
-- is "more than the label can say" — while `button.count` keeps the real number.
|
||||
if count > 1 or (button.isBag and count > 0) then
|
||||
if count > 999 then
|
||||
count = "*"
|
||||
end
|
||||
getglobal(button:GetName() .. "Count"):SetText(count)
|
||||
getglobal(button:GetName() .. "Count"):Show()
|
||||
label:SetText(count > 999 and "*" or count)
|
||||
label:Show()
|
||||
else
|
||||
getglobal(button:GetName() .. "Count"):Hide()
|
||||
label:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -89,15 +110,14 @@
|
||||
-- string/number comparison Lua refuses — a raise on the only path the default can
|
||||
-- reach. Ours defaults to 0, which is what the branch below actually means. Not aping
|
||||
-- a quirk (method step 3); the behaviour on every reachable path is identical.
|
||||
if not numInStock then
|
||||
numInStock = 0
|
||||
end
|
||||
numInStock = numInStock or 0
|
||||
button.numInStock = numInStock
|
||||
local label = region(button, "Stock")
|
||||
if numInStock > 0 then
|
||||
getglobal(button:GetName() .. "Stock"):SetText(format(MERCHANT_STOCK, numInStock))
|
||||
getglobal(button:GetName() .. "Stock"):Show()
|
||||
label:SetText(format(MERCHANT_STOCK, numInStock))
|
||||
label:Show()
|
||||
else
|
||||
getglobal(button:GetName() .. "Stock"):Hide()
|
||||
label:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,28 +125,39 @@
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
-- Show/Hide FIRST, then SetTexture — the reference's order, and it matters: a nil
|
||||
-- texture hides the region rather than leaving an empty quad behind.
|
||||
local icon = region(button, "IconTexture")
|
||||
-- Visibility tracks the ARGUMENT, not the region's contents, and it is settled before
|
||||
-- the texture is assigned: clearing a slot with a nil texture must leave the region
|
||||
-- hidden, never an empty quad standing where the icon was.
|
||||
if texture then
|
||||
getglobal(button:GetName() .. "IconTexture"):Show()
|
||||
icon:Show()
|
||||
else
|
||||
getglobal(button:GetName() .. "IconTexture"):Hide()
|
||||
icon:Hide()
|
||||
end
|
||||
getglobal(button:GetName() .. "IconTexture"):SetTexture(texture)
|
||||
icon:SetTexture(texture)
|
||||
end
|
||||
|
||||
function SetItemButtonTextureVertexColor(button, r, g, b)
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
getglobal(button:GetName() .. "IconTexture"):SetVertexColor(r, g, b)
|
||||
tint(button, "IconTexture", r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonNormalTextureVertexColor(button, r, g, b)
|
||||
tint(button, "NormalTexture", r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonNameFrameVertexColor(button, r, g, b)
|
||||
tint(button, "NameFrame", r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonSlotVertexColor(button, r, g, b)
|
||||
tint(button, "SlotTexture", r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonDesaturated(button, desaturated, r, g, b)
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
local icon = getglobal(button:GetName() .. "IconTexture")
|
||||
-- The one verb whose own nil guard is on the ICON rather than the button: the
|
||||
-- reference checks both, and `region()` collapses them into this single test without
|
||||
-- changing which calls no-op and which raise.
|
||||
local icon = region(button, "IconTexture")
|
||||
if not icon then
|
||||
return
|
||||
end
|
||||
@@ -138,37 +169,14 @@
|
||||
-- is on the METHOD's existence and the no-shader branch is the one we take. When the
|
||||
-- real verb lands this line starts using it with nothing else to change.
|
||||
local shaderSupported = icon.SetDesaturated and icon:SetDesaturated(desaturated)
|
||||
-- Undesaturated is full colour whatever the caller passed; desaturated is the caller's
|
||||
-- own tint when they named one AND the shader took it, and the flat grey otherwise.
|
||||
if not desaturated then
|
||||
r = 1.0
|
||||
g = 1.0
|
||||
b = 1.0
|
||||
r, g, b = 1.0, 1.0, 1.0
|
||||
elseif not r or not shaderSupported then
|
||||
r = 0.5
|
||||
g = 0.5
|
||||
b = 0.5
|
||||
r, g, b = 0.5, 0.5, 0.5
|
||||
end
|
||||
icon:SetVertexColor(r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonNormalTextureVertexColor(button, r, g, b)
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
getglobal(button:GetName() .. "NormalTexture"):SetVertexColor(r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonNameFrameVertexColor(button, r, g, b)
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
getglobal(button:GetName() .. "NameFrame"):SetVertexColor(r, g, b)
|
||||
end
|
||||
|
||||
function SetItemButtonSlotVertexColor(button, r, g, b)
|
||||
if not button then
|
||||
return
|
||||
end
|
||||
getglobal(button:GetName() .. "SlotTexture"):SetVertexColor(r, g, b)
|
||||
end
|
||||
]]></Script>
|
||||
</Ui>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The item-text reader — letters, books, plaques (the mail arc's "permanent copy" lands
|
||||
here). Transcribed from the reference ItemTextFrame.xml (354 l) + ItemTextFrame.lua
|
||||
(104 l); geometry/art quoted verbatim, names the reference's own. Driven by the engine's
|
||||
item-text seam (benilla-ui/src/script/item_text.rs) + crate::ui_item_text's feed/drain:
|
||||
the app fires the reference event flow ITEM_TEXT_BEGIN → ITEM_TEXT_READY → ITEM_TEXT_CLOSED.
|
||||
here). The Lua below is OURS; the reference ItemTextFrame.xml (354 l) + ItemTextFrame.lua
|
||||
(104 l) are the SPECIFICATION it satisfies — frame/region names, the event flow, the
|
||||
material art paths and the geometry/art are the reference's and are quoted as such.
|
||||
Driven by the engine's item-text seam (benilla-ui/src/script/item_text.rs) +
|
||||
crate::ui_item_text's feed/drain: the app fires the reference event flow
|
||||
ITEM_TEXT_BEGIN → ITEM_TEXT_READY → ITEM_TEXT_CLOSED.
|
||||
|
||||
ADAPTATIONS (the MailFrame.xml precedents):
|
||||
· The body scroll (ItemTextScrollFrame, UIPanelScrollFrameTemplate 280×355 at -66,-76) is
|
||||
@@ -12,22 +14,26 @@
|
||||
a very long page clips. The ref's own flanking scrollbar art (UI-Character-ScrollBar
|
||||
top/bottom + the black middle strip) is quoted verbatim as inert layers, so the right
|
||||
edge reads exactly like the reference at rest.
|
||||
· ItemTextPageText is SimpleHTML in the ref — transcribed as a FontString at the same
|
||||
· ItemTextPageText is SimpleHTML in the ref — drawn here as a FontString at the same
|
||||
rect (our letters/pages are plain text; an HTML page renders as its markup, accepted).
|
||||
· The Lua's ScrollFrame-only verbs (UpdateScrollChildRect, ScrollBar SetValue/Hide,
|
||||
scrollBarHideable) are dropped with the live scrollbar.
|
||||
· GetMaterialTextColors + the two material color tables live in the reference Fonts.xml /
|
||||
UIParent.lua — inlined here Benilla-scoped (the MoneyInput-shim precedent), quoted
|
||||
verbatim.
|
||||
· The ScrollFrame-only verbs the reference's OnLoad/READY use (UpdateScrollChildRect,
|
||||
ScrollBar SetValue/Hide, scrollBarHideable) are dropped with the live scrollbar.
|
||||
· The two material colour TABLES are the reference's data (its Fonts.xml), quoted
|
||||
verbatim — the numbers ARE the look. They live here Benilla-scoped (the MoneyInput-shim
|
||||
precedent) because our own Fonts.xml deliberately doesn't ship the reference's two
|
||||
globals; the lookup around them is ours, under the reference's published name.
|
||||
· GlobalStrings inlined: PREV "Prev", NEXT "Next", ITEM_TEXT_FROM "From," (the reference
|
||||
string really is comma'd).
|
||||
· ITEM_TEXT_TRANSLATION (the foreign-language progress bar) is transcribed but never
|
||||
· ITEM_TEXT_TRANSLATION (the foreign-language progress bar) is implemented but never
|
||||
fires — no translation mechanic on our letters; the StatusBar stays hidden.
|
||||
· <HitRectInsets> quoted verbatim for fidelity but currently inert (the loader ignores
|
||||
the element).
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- ref Fonts.xml l.27-42 (quoted verbatim, Benilla-scoped).
|
||||
-- ── The material palette ─────────────────────────────────────────────────────────────
|
||||
-- The six materials a page can be printed on, and the ink each one takes. The COLOURS are
|
||||
-- the reference's own data (its Fonts.xml l.27-42) and are quoted verbatim: a different
|
||||
-- number is a different look. Benilla-scoped names — see the header.
|
||||
BENILLA_MATERIAL_TEXT_COLOR_TABLE = {
|
||||
["Default"] = {0.18, 0.12, 0.06},
|
||||
["Stone"] = {1.0, 1.0, 1.0},
|
||||
@@ -44,7 +50,9 @@
|
||||
["Silver"] = {0.93, 0.82, 0},
|
||||
["Bronze"] = {0.93, 0.82, 0}
|
||||
}
|
||||
-- ref UIParent.lua l.1880-1888.
|
||||
-- The page ink and the title ink for a material, under the reference's published name
|
||||
-- (its UIParent.lua l.1880-1888). Falls back to "Default" unless BOTH halves are known, so
|
||||
-- an unrecognised material never gets one table's colour and the other's default.
|
||||
function GetMaterialTextColors(material)
|
||||
local textColor = BENILLA_MATERIAL_TEXT_COLOR_TABLE[material]
|
||||
local titleColor = BENILLA_MATERIAL_TITLETEXT_COLOR_TABLE[material]
|
||||
@@ -55,99 +63,155 @@
|
||||
return textColor, titleColor
|
||||
end
|
||||
|
||||
-- ref ItemTextFrame.lua l.1-8.
|
||||
-- ── The reader's regions, by role ────────────────────────────────────────────────────
|
||||
-- The four material corners are one SET: a page is either bare parchment (all four off)
|
||||
-- or dressed in a material, in which case each corner wears that material's own art.
|
||||
-- Naming the suffixes once is also what makes the art path a single expression.
|
||||
local MATERIAL_CORNERS = { "TopLeft", "TopRight", "BotLeft", "BotRight" }
|
||||
|
||||
-- Everything a fresh ITEM_TEXT_BEGIN clears away. At that point only the title is known,
|
||||
-- so only the title stays on screen — the page and all of its paging chrome go.
|
||||
local PAGE_CHROME = {
|
||||
"ItemTextScrollFrame", "ItemTextCurrentPage", "ItemTextStatusBar",
|
||||
"ItemTextPrevPageButton", "ItemTextNextPageButton",
|
||||
}
|
||||
|
||||
local function ShowOrHide(region, wanted)
|
||||
if wanted then
|
||||
region:Show()
|
||||
else
|
||||
region:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- A page always HAS a material: the server may name none, and the frame's own art is
|
||||
-- already parchment, so that is what "none" means.
|
||||
local function PageMaterial()
|
||||
return ItemTextGetMaterial() or "Parchment"
|
||||
end
|
||||
|
||||
-- Dress the page's four corners in its material. Parchment IS the frame's own art, so
|
||||
-- parchment leaves the corners off and lets the bare frame show through; anything else
|
||||
-- lights all four and points each at its own slice of that material's art set.
|
||||
local function DressPage(material)
|
||||
if material == "Parchment" then
|
||||
for _, corner in ipairs(MATERIAL_CORNERS) do
|
||||
getglobal("ItemTextMaterial" .. corner):Hide()
|
||||
end
|
||||
return
|
||||
end
|
||||
local art = "Interface\\ItemTextFrame\\ItemText-" .. material .. "-"
|
||||
for _, corner in ipairs(MATERIAL_CORNERS) do
|
||||
local layer = getglobal("ItemTextMaterial" .. corner)
|
||||
layer:Show()
|
||||
layer:SetTexture(art .. corner)
|
||||
end
|
||||
end
|
||||
|
||||
-- Put the reader on screen. The panel manager is allowed to refuse (something
|
||||
-- native-center is holding the screen) — and then we are not reading after all, so the
|
||||
-- server has to be told, or the session stays open behind a window that never appeared.
|
||||
-- The reference makes the same check, on both of the paths that open this window.
|
||||
local function OpenReader(frame)
|
||||
ShowUIPanel(frame)
|
||||
if not frame:IsVisible() then
|
||||
CloseItemText()
|
||||
end
|
||||
end
|
||||
|
||||
-- ── The event flow ───────────────────────────────────────────────────────────────────
|
||||
-- One function per event of the reference's flow: ItemTextFrame_OnLoad registers FLOW in
|
||||
-- order and ItemTextFrame_OnEvent dispatches into Handlers by name. (The reference states
|
||||
-- the same pairing twice — four RegisterEvent calls, then a four-arm `if event == …`
|
||||
-- chain — which is how a fifth event gets added to one of them and not the other.)
|
||||
local FLOW = {
|
||||
"ITEM_TEXT_BEGIN", "ITEM_TEXT_TRANSLATION", "ITEM_TEXT_READY", "ITEM_TEXT_CLOSED",
|
||||
}
|
||||
local Handlers = {}
|
||||
|
||||
-- A text has been opened. Only its title is known yet, so the page is cleared away and
|
||||
-- the ink is set for the material the pages are going to arrive on.
|
||||
function Handlers.ITEM_TEXT_BEGIN(frame)
|
||||
ItemTextTitleText:SetText(ItemTextGetItem())
|
||||
for _, name in ipairs(PAGE_CHROME) do
|
||||
getglobal(name):Hide()
|
||||
end
|
||||
local ink = GetMaterialTextColors(PageMaterial())
|
||||
ItemTextPageText:SetTextColor(ink[1], ink[2], ink[3])
|
||||
end
|
||||
|
||||
-- A foreign-language page is being translated: the turn buttons stand down and the
|
||||
-- progress bar takes over, ranged across the arg1 seconds the server quoted (OnUpdate
|
||||
-- fills it). Never fires on our letters — see the header.
|
||||
function Handlers.ITEM_TEXT_TRANSLATION(frame)
|
||||
ItemTextPrevPageButton:Hide()
|
||||
ItemTextNextPageButton:Hide()
|
||||
frame.translationElapsed = 0
|
||||
ItemTextStatusBar:SetMinMaxValues(0, arg1)
|
||||
ItemTextStatusBar:Show()
|
||||
OpenReader(frame)
|
||||
end
|
||||
|
||||
-- A page has arrived. A letter signs off with its sender (ITEM_TEXT_FROM — the reference
|
||||
-- string really is comma'd); anything else just gets its trailing blank line. The paging
|
||||
-- row appears only when there is another page to be on, and then each button shows only
|
||||
-- if there is something in its direction.
|
||||
function Handlers.ITEM_TEXT_READY(frame)
|
||||
local creator = ItemTextGetCreator()
|
||||
local body = "\n" .. ItemTextGetText()
|
||||
if creator then
|
||||
body = body .. "\n\n" .. "From," .. "\n" .. creator .. "\n\n"
|
||||
else
|
||||
body = body .. "\n"
|
||||
end
|
||||
ItemTextPageText:SetText(body)
|
||||
ItemTextScrollFrame:Show()
|
||||
|
||||
local pageNumber = ItemTextGetPage()
|
||||
local hasNext = ItemTextHasNextPage()
|
||||
DressPage(PageMaterial())
|
||||
if pageNumber > 1 or hasNext then
|
||||
ItemTextCurrentPage:SetText(pageNumber)
|
||||
ItemTextCurrentPage:Show()
|
||||
ShowOrHide(ItemTextPrevPageButton, pageNumber > 1)
|
||||
ShowOrHide(ItemTextNextPageButton, hasNext)
|
||||
end
|
||||
ItemTextStatusBar:Hide()
|
||||
OpenReader(frame)
|
||||
end
|
||||
|
||||
function Handlers.ITEM_TEXT_CLOSED(frame)
|
||||
HideUIPanel(frame)
|
||||
end
|
||||
|
||||
function ItemTextFrame_OnLoad()
|
||||
this:RegisterEvent("ITEM_TEXT_BEGIN")
|
||||
this:RegisterEvent("ITEM_TEXT_TRANSLATION")
|
||||
this:RegisterEvent("ITEM_TEXT_READY")
|
||||
this:RegisterEvent("ITEM_TEXT_CLOSED")
|
||||
for _, event in ipairs(FLOW) do
|
||||
this:RegisterEvent(event)
|
||||
end
|
||||
end
|
||||
|
||||
-- ref l.10-96.
|
||||
function ItemTextFrame_OnEvent(event)
|
||||
if event == "ITEM_TEXT_BEGIN" then
|
||||
ItemTextTitleText:SetText(ItemTextGetItem())
|
||||
ItemTextScrollFrame:Hide()
|
||||
ItemTextCurrentPage:Hide()
|
||||
ItemTextStatusBar:Hide()
|
||||
ItemTextPrevPageButton:Hide()
|
||||
ItemTextNextPageButton:Hide()
|
||||
local material = ItemTextGetMaterial()
|
||||
if not material then material = "Parchment" end
|
||||
local textColor = GetMaterialTextColors(material)
|
||||
ItemTextPageText:SetTextColor(textColor[1], textColor[2], textColor[3])
|
||||
return
|
||||
end
|
||||
if event == "ITEM_TEXT_TRANSLATION" then
|
||||
ItemTextPrevPageButton:Hide()
|
||||
ItemTextNextPageButton:Hide()
|
||||
this.translationElapsed = 0
|
||||
ItemTextStatusBar:SetMinMaxValues(0, arg1)
|
||||
ItemTextStatusBar:Show()
|
||||
ShowUIPanel(this)
|
||||
if not this:IsVisible() then CloseItemText() end
|
||||
return
|
||||
end
|
||||
if event == "ITEM_TEXT_READY" then
|
||||
local creator = ItemTextGetCreator()
|
||||
if creator then
|
||||
creator = "\n\n".."From,".."\n"..creator.."\n\n" -- ITEM_TEXT_FROM
|
||||
ItemTextPageText:SetText("\n"..ItemTextGetText()..creator)
|
||||
else
|
||||
ItemTextPageText:SetText("\n"..ItemTextGetText().."\n")
|
||||
end
|
||||
ItemTextScrollFrame:Show()
|
||||
local page = ItemTextGetPage()
|
||||
local next = ItemTextHasNextPage()
|
||||
local material = ItemTextGetMaterial()
|
||||
if not material then material = "Parchment" end
|
||||
if material == "Parchment" then
|
||||
ItemTextMaterialTopLeft:Hide()
|
||||
ItemTextMaterialTopRight:Hide()
|
||||
ItemTextMaterialBotLeft:Hide()
|
||||
ItemTextMaterialBotRight:Hide()
|
||||
else
|
||||
ItemTextMaterialTopLeft:Show()
|
||||
ItemTextMaterialTopRight:Show()
|
||||
ItemTextMaterialBotLeft:Show()
|
||||
ItemTextMaterialBotRight:Show()
|
||||
ItemTextMaterialTopLeft:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-TopLeft")
|
||||
ItemTextMaterialTopRight:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-TopRight")
|
||||
ItemTextMaterialBotLeft:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-BotLeft")
|
||||
ItemTextMaterialBotRight:SetTexture("Interface\\ItemTextFrame\\ItemText-"..material.."-BotRight")
|
||||
end
|
||||
if (page > 1) or next then
|
||||
ItemTextCurrentPage:SetText(page)
|
||||
ItemTextCurrentPage:Show()
|
||||
if page > 1 then
|
||||
ItemTextPrevPageButton:Show()
|
||||
else
|
||||
ItemTextPrevPageButton:Hide()
|
||||
end
|
||||
if next then
|
||||
ItemTextNextPageButton:Show()
|
||||
else
|
||||
ItemTextNextPageButton:Hide()
|
||||
end
|
||||
end
|
||||
ItemTextStatusBar:Hide()
|
||||
ShowUIPanel(this)
|
||||
if not this:IsVisible() then CloseItemText() end
|
||||
return
|
||||
end
|
||||
if event == "ITEM_TEXT_CLOSED" then
|
||||
HideUIPanel(this)
|
||||
return
|
||||
local handler = Handlers[event]
|
||||
if handler then
|
||||
handler(this)
|
||||
end
|
||||
end
|
||||
|
||||
-- ref l.98-104.
|
||||
-- The translation bar's clock: while the bar is up it fills with the seconds spent so far.
|
||||
function ItemTextFrame_OnUpdate(elapsed)
|
||||
if ItemTextStatusBar:IsVisible() then
|
||||
elapsed = this.translationElapsed + elapsed
|
||||
ItemTextStatusBar:SetValue(elapsed)
|
||||
this.translationElapsed = elapsed
|
||||
if not ItemTextStatusBar:IsVisible() then
|
||||
return
|
||||
end
|
||||
local spent = this.translationElapsed + elapsed
|
||||
ItemTextStatusBar:SetValue(spent)
|
||||
this.translationElapsed = spent
|
||||
end
|
||||
|
||||
-- Both page buttons are one gesture in two directions: the paging click, then the
|
||||
-- engine's own page request. `turnPage` is ItemTextPrevPage or ItemTextNextPage.
|
||||
function ItemTextFrame_TurnPage(turnPage)
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
turnPage()
|
||||
end
|
||||
]]></Script>
|
||||
<Frame name="ItemTextFrame" toplevel="true" movable="true" enableMouse="true" hidden="true" parent="UIParent">
|
||||
@@ -281,10 +345,7 @@
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
ItemTextPrevPage()
|
||||
</OnClick>
|
||||
<OnClick>ItemTextFrame_TurnPage(ItemTextPrevPage)</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Down"/>
|
||||
@@ -302,10 +363,7 @@
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
ItemTextNextPage()
|
||||
</OnClick>
|
||||
<OnClick>ItemTextFrame_TurnPage(ItemTextNextPage)</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Up"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Down"/>
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
-- ── Key display ── the 1.12 GetBindingText law by hand: modifier prefixes stay the
|
||||
-- canonical uppercase tokens, the base key reads its KEY_* global (Spacebar, Up Arrow,
|
||||
-- Mouse Button 4...) else itself. "CTRL--" renders CTRL + the minus key. (The action
|
||||
-- bar's ABBREVIATED labels ride the real GetBindingText transcription in UIParent.xml;
|
||||
-- bar's ABBREVIATED labels ride our own GetBindingText in UIParent.xml;
|
||||
-- this page and the tooltips show the full form, both references' own split.)
|
||||
function KeyBindings_KeyText(key)
|
||||
if not key then
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's MACRO window (decision 0983) — the 384x512 macro editor + its name/icon popup,
|
||||
transcribed from the real 1.12 `Interface\AddOns\Blizzard_MacroUI\Blizzard_MacroUI.xml`
|
||||
re-expressed from the real 1.12 `Interface\AddOns\Blizzard_MacroUI\Blizzard_MacroUI.xml`
|
||||
and `.lua` (extracted from the MPQ chain to session scratch; director-local reference
|
||||
artifacts, never committed — the 0068/0138 pattern. Line cites below are into that
|
||||
extraction). All the art it names is shipped: `Interface\MacroFrame\MacroFrame-Icon`,
|
||||
@@ -168,7 +168,7 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ref l.51-120, transcribed.
|
||||
-- ref l.51-120, re-expressed.
|
||||
function BenillaMacroFrame_Update()
|
||||
BenillaMacroFrame_EnsureSelection()
|
||||
local frame = getglobal("BenillaMacroFrame")
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
(`benilla-ui/src/script/mail.rs`) + `crate::ui_mail`'s feed/drain. The house dressing
|
||||
convention (decisions 0084/0112/0120).
|
||||
|
||||
THE LUA IS OURS. The reference's `MailFrame.lua` is the SPECIFICATION — the function names an
|
||||
addon can call, the event vocabulary, and every branch's observable effect are read off it,
|
||||
and the line cites in the script block are that file's. How any of it is spelled is this
|
||||
file's own business, and several pieces are deliberately not spelled its way.
|
||||
|
||||
Where the reference inherits a shared template benilla does NOT ship (UIPanelButtonTemplate,
|
||||
UIPanelCloseButton, ItemButtonTemplate, SmallMoneyFrameTemplate, MoneyInputFrameTemplate,
|
||||
UIRadioButtonTemplate, FriendsFrameTabTemplate, UIPanelScrollFrameTemplate — grep-verified
|
||||
@@ -14,7 +19,8 @@
|
||||
ANCHORS/sizes kept exactly; the tab pair inherits benilla's CharacterFrameTabButtonTemplate at
|
||||
the reference tab anchors. `MailTextFontNormal`/`GameFontRedSmall` DO ship and are used verbatim.
|
||||
|
||||
RENDER APPROXIMATIONS (the only geometry-affecting deviations, all else is verbatim):
|
||||
RENDER APPROXIMATIONS (the only geometry-affecting deviations; every other measurement in the
|
||||
markup below is the reference's own):
|
||||
· Unread/read row **desaturation**: the engine has no `SetDesaturation`, so the read icon +
|
||||
slot ring grey via vertex color (0.5) — ref MailFrame.lua l.130-141's SetVertexColor legs
|
||||
are exact, only the icon's desaturation is the vertex-grey stand-in.
|
||||
@@ -93,26 +99,53 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- The tab pair + the four corner-texture swaps (ref MailFrameTab_OnClick l.64-93).
|
||||
-- THE TWO SKINS the window wears (ref MailFrameTab_OnClick l.64-93). The inbox is dressed in
|
||||
-- the item-text parchment, the compose form in the class-trainer panel; four corner slabs
|
||||
-- and the top-left slab's own offset are the whole of the difference between them, so they
|
||||
-- are a table here rather than two copies of the same five calls. `$parentTopLeft` and its
|
||||
-- siblings publish as MailFrameTopLeft…, which is what the corner keys compose.
|
||||
local mailSkin = {
|
||||
inbox = {
|
||||
TopLeft = "Interface\\ItemTextFrame\\UI-ItemText-TopLeft",
|
||||
TopRight = "Interface\\Spellbook\\UI-SpellbookPanel-TopRight",
|
||||
BotLeft = "Interface\\ItemTextFrame\\UI-ItemText-BotLeft",
|
||||
BotRight = "Interface\\Spellbook\\UI-SpellbookPanel-BotRight",
|
||||
x = 0, y = 0,
|
||||
},
|
||||
compose = {
|
||||
TopLeft = "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft",
|
||||
TopRight = "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight",
|
||||
BotLeft = "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotLeft",
|
||||
BotRight = "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight",
|
||||
-- The trainer art does not sit flush the way the parchment does; the ref's own
|
||||
-- one-pixel nudge is what lines the two skins' outer edges up.
|
||||
x = 2, y = -1,
|
||||
},
|
||||
}
|
||||
local mailCorners = { "TopLeft", "TopRight", "BotLeft", "BotRight" }
|
||||
|
||||
-- Tab 1 is the inbox; anything else is the compose form.
|
||||
function MailFrameTab_OnClick(tab)
|
||||
if not tab then tab = this:GetID() end
|
||||
PanelTemplates_SetTab(MailFrame, tab)
|
||||
if tab == 1 then
|
||||
|
||||
local reading = (tab == 1)
|
||||
if reading then
|
||||
InboxFrame:Show()
|
||||
SendMailFrame:Hide()
|
||||
MailFrameTopLeft:SetTexture("Interface\\ItemTextFrame\\UI-ItemText-TopLeft")
|
||||
MailFrameTopRight:SetTexture("Interface\\Spellbook\\UI-SpellbookPanel-TopRight")
|
||||
MailFrameBotLeft:SetTexture("Interface\\ItemTextFrame\\UI-ItemText-BotLeft")
|
||||
MailFrameBotRight:SetTexture("Interface\\Spellbook\\UI-SpellbookPanel-BotRight")
|
||||
MailFrameTopLeft:SetPoint("TOPLEFT", "MailFrame", "TOPLEFT", 0, 0)
|
||||
else
|
||||
InboxFrame:Hide()
|
||||
SendMailFrame:Show()
|
||||
MailFrameTopLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft")
|
||||
MailFrameTopRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight")
|
||||
MailFrameBotLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotLeft")
|
||||
MailFrameBotRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight")
|
||||
MailFrameTopLeft:SetPoint("TOPLEFT", "MailFrame", "TOPLEFT", 2, -1)
|
||||
end
|
||||
|
||||
local skin = reading and mailSkin.inbox or mailSkin.compose
|
||||
for _, corner in ipairs(mailCorners) do
|
||||
getglobal("MailFrame" .. corner):SetTexture(skin[corner])
|
||||
end
|
||||
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()
|
||||
SendMailFrame.sendMode = "send"
|
||||
end
|
||||
@@ -191,17 +224,12 @@
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
-- Page arrows (ref l.188-198).
|
||||
if MailFrame.pageNum == 1 then
|
||||
InboxPrevPageButton:Disable()
|
||||
else
|
||||
InboxPrevPageButton:Enable()
|
||||
end
|
||||
if (MailFrame.pageNum * BENILLA_INBOXITEMS_TO_DISPLAY) < numItems then
|
||||
InboxNextPageButton:Enable()
|
||||
else
|
||||
InboxNextPageButton:Disable()
|
||||
end
|
||||
-- Page arrows (ref l.188-198): back is dead on the first page, forward is dead once
|
||||
-- this page reaches the end of the list. `lastOnPage` is the list index this page's
|
||||
-- last row would carry if it were full.
|
||||
local lastOnPage = MailFrame.pageNum * BENILLA_INBOXITEMS_TO_DISPLAY
|
||||
if MailFrame.pageNum == 1 then InboxPrevPageButton:Disable() else InboxPrevPageButton:Enable() end
|
||||
if lastOnPage < numItems then InboxNextPageButton:Enable() else InboxNextPageButton:Disable() end
|
||||
-- InboxCurrentPage is declared in the ref XML (l.329) but NO reference Lua ever writes
|
||||
-- it — the real 1.12 window shows nothing there, so neither do we.
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<!--
|
||||
The HUD minimap cluster — transcribed from the 1.12 FrameXML Minimap.xml (decision 0203
|
||||
phase 1). The <Minimap> widget itself is the engine-rendered map hole: benilla-ui carries its
|
||||
The HUD minimap cluster — our own implementation of what the 1.12 FrameXML Minimap.xml
|
||||
specifies (decision 0203 phase 1), which is cited throughout for the names, the geometry, the
|
||||
art and the event wiring: that half is the API addons reposition and hook by name, and none of
|
||||
it is ours to invent. The <Minimap> widget itself is the engine-rendered map hole: benilla-ui carries its
|
||||
rect + zoom, and the app (`minimap.rs`) fills it with the streamed tile window, the
|
||||
MinimapMask circle, and the player arrow; everything in THIS file is the chrome the reference
|
||||
authors around that hole, at the reference's own geometry and art.
|
||||
@@ -34,69 +36,146 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ref-Minimap.lua Minimap_Update(), verbatim — the PVP tint rides GetZonePVPInfo
|
||||
-- (decision 0287; closes the "zone PVP state we don't track yet" reduction that kept
|
||||
-- the label gold), plus the ref's refresh-while-hovered tooltip block.
|
||||
function Minimap_Update()
|
||||
MinimapZoneText:SetText(GetMinimapZoneText());
|
||||
local pvpType, factionName, isArena = GetZonePVPInfo();
|
||||
if ( isArena ) then
|
||||
MinimapZoneText:SetTextColor(1.0, 0.1, 0.1);
|
||||
elseif ( pvpType == "friendly" ) then
|
||||
MinimapZoneText:SetTextColor(0.1, 1.0, 0.1);
|
||||
elseif ( pvpType == "hostile" ) then
|
||||
MinimapZoneText:SetTextColor(1.0, 0.1, 0.1);
|
||||
-- The zone label's tint, keyed by the zone's PvP disposition. Colours quoted from
|
||||
-- ref-Minimap.lua (they are measurements of the real interface, not choices of ours); the
|
||||
-- table is ours, because five branches that differ only in three floats want to be data.
|
||||
local ZONE_PVP_TINT = {
|
||||
friendly = { r = 0.1, g = 1.0, b = 0.1 },
|
||||
hostile = { r = 1.0, g = 0.1, b = 0.1 },
|
||||
contested = { r = 1.0, g = 0.7, b = 0 },
|
||||
};
|
||||
|
||||
-- Every line of the zone tooltip is drawn the same way — plain white, nothing in the
|
||||
-- right-hand column — so the stanza is written once here instead of once per line.
|
||||
local function AddZoneTooltipLine(text)
|
||||
GameTooltip:AddLine(text, "", 1.0, 1.0, 1.0);
|
||||
end
|
||||
|
||||
-- The zone's "whose land is this" line, or nil for a zone that is nobody's (sanctuary, or
|
||||
-- a disposition we don't recognise). Free-for-all is deliberately NOT decided here: the
|
||||
-- two callers below reach it by genuinely different routes and have to keep doing so — see
|
||||
-- the note in Minimap_Update.
|
||||
local function ZoneTerritoryLine(pvpType, factionName)
|
||||
if ( pvpType == "friendly" or pvpType == "hostile" ) then
|
||||
return format(FACTION_CONTROLLED_TERRITORY, factionName);
|
||||
elseif ( pvpType == "contested" ) then
|
||||
MinimapZoneText:SetTextColor(1.0, 0.7, 0);
|
||||
return CONTESTED_TERRITORY;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- Our implementation of what ref-Minimap.lua's Minimap_Update() specifies: relabel the
|
||||
-- minimap's zone line, tint it for the zone's PVP state (decision 0287 — GetZonePVPInfo
|
||||
-- closes the "zone PVP state we don't track yet" reduction that kept the label gold), and
|
||||
-- refresh the hover tooltip if — and only if — the cursor is already parked on the label,
|
||||
-- so a zone change under the mouse updates what is on screen.
|
||||
function Minimap_Update()
|
||||
local zoneName = GetMinimapZoneText();
|
||||
MinimapZoneText:SetText(zoneName);
|
||||
|
||||
local pvpType, factionName, isArena = GetZonePVPInfo();
|
||||
-- An arena zone is free-for-all, so it wears the hostile red whatever the zone's own
|
||||
-- type says; resolving that override after the lookup keeps it in one place. A zone
|
||||
-- with no tint of its own (sanctuary, or a type we don't know) stays normal gold.
|
||||
local tint = ZONE_PVP_TINT[pvpType];
|
||||
if ( isArena ) then
|
||||
tint = ZONE_PVP_TINT.hostile;
|
||||
end
|
||||
if ( tint ) then
|
||||
MinimapZoneText:SetTextColor(tint.r, tint.g, tint.b);
|
||||
else
|
||||
MinimapZoneText:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b);
|
||||
end
|
||||
|
||||
if ( GameTooltip:IsOwned(MinimapZoneTextButton) ) then
|
||||
GameTooltip:SetOwner(MinimapZoneTextButton, "ANCHOR_LEFT");
|
||||
GameTooltip:AddLine(GetMinimapZoneText(), "", 1.0, 1.0, 1.0);
|
||||
if ( (pvpType == "friendly") or (pvpType == "hostile") ) then
|
||||
GameTooltip:AddLine(format(FACTION_CONTROLLED_TERRITORY, factionName), "", 1.0, 1.0, 1.0);
|
||||
elseif ( pvpType == "contested" ) then
|
||||
GameTooltip:AddLine(CONTESTED_TERRITORY, "", 1.0, 1.0, 1.0);
|
||||
elseif ( pvpType == "arena" ) then
|
||||
GameTooltip:AddLine(FREE_FOR_ALL_TERRITORY, "", 1.0, 1.0, 1.0);
|
||||
end
|
||||
GameTooltip:Show();
|
||||
if ( not GameTooltip:IsOwned(MinimapZoneTextButton) ) then
|
||||
return;
|
||||
end
|
||||
-- AT MOST ONE territory line on this path, and free-for-all is an ALTERNATIVE to the
|
||||
-- faction line here, reached off `pvpType` rather than off `isArena` — so with the
|
||||
-- live API (which never reports "arena" as a type) it never fires at all. That is the
|
||||
-- reference's own asymmetry with MinimapZoneTextButton_OnEnter below, where the FFA
|
||||
-- line is an EXTRA line gated on `isArena`. The two disagree, they are kept apart
|
||||
-- deliberately, and converging them would be a behaviour change wearing a tidy-up's
|
||||
-- clothes — which is why ZoneTerritoryLine stops short of this arm.
|
||||
local territory = ZoneTerritoryLine(pvpType, factionName);
|
||||
if ( pvpType == "arena" ) then
|
||||
territory = FREE_FOR_ALL_TERRITORY;
|
||||
end
|
||||
GameTooltip:SetOwner(MinimapZoneTextButton, "ANCHOR_LEFT");
|
||||
AddZoneTooltipLine(zoneName);
|
||||
if ( territory ) then
|
||||
AddZoneTooltipLine(territory);
|
||||
end
|
||||
GameTooltip:Show();
|
||||
end
|
||||
|
||||
-- The zone label's own hover tooltip. Minimap_Update above refreshes this same tooltip in
|
||||
-- place while the cursor is parked here, but the two are NOT one routine: this one adds
|
||||
-- the free-for-all line as an EXTRA line gated on `isArena` (so an arena that is also
|
||||
-- contested says both), while Minimap_Update reaches it off `pvpType == "arena"` INSTEAD
|
||||
-- of the faction line. See the note there; the difference is the reference's, it is what
|
||||
-- players actually see, and it is preserved on purpose.
|
||||
function MinimapZoneTextButton_OnEnter()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_LEFT");
|
||||
AddZoneTooltipLine(GetMinimapZoneText());
|
||||
|
||||
local pvpType, factionName, isArena = GetZonePVPInfo();
|
||||
local territory = ZoneTerritoryLine(pvpType, factionName);
|
||||
if ( territory ) then
|
||||
AddZoneTooltipLine(territory);
|
||||
end
|
||||
if ( isArena ) then
|
||||
AddZoneTooltipLine(FREE_FOR_ALL_TERRITORY);
|
||||
end
|
||||
GameTooltip:Show();
|
||||
end
|
||||
|
||||
-- The end of the track a step of `step` is heading for. The two ends are not spelled the
|
||||
-- same way — index 0 is a constant, the tight end rides however many levels the widget
|
||||
-- carries — which is the only reason the +/- buttons need saying twice at all.
|
||||
local function MinimapZoomLimit(step)
|
||||
if ( step < 0 ) then
|
||||
return 0;
|
||||
end
|
||||
return Minimap:GetZoomLevels() - 1;
|
||||
end
|
||||
|
||||
-- One zoom step, either direction (ref-Minimap.lua's two click handlers say this twice in
|
||||
-- mirror image; benilla says it once and hands it the direction). Three things happen: the
|
||||
-- OPPOSITE button is re-armed, because a step this way always leaves room to come back;
|
||||
-- the widget moves; and this button greys if the step landed on its own end of the track.
|
||||
-- Note the limit is read AFTER the step, so a widget that clamps the move is still asked
|
||||
-- where it actually ended up.
|
||||
local function MinimapZoomStep(step, sound, button, opposite)
|
||||
opposite:Enable();
|
||||
PlaySound(sound);
|
||||
Minimap:SetZoom(Minimap:GetZoom() + step);
|
||||
if ( Minimap:GetZoom() == MinimapZoomLimit(step) ) then
|
||||
button:Disable();
|
||||
end
|
||||
end
|
||||
|
||||
-- ref-Minimap.lua zoom clicks, verbatim: step the widget, re-arm both buttons, disable
|
||||
-- the one at its bound.
|
||||
function Minimap_ZoomInClick()
|
||||
MinimapZoomOut:Enable();
|
||||
PlaySound("igMiniMapZoomIn");
|
||||
Minimap:SetZoom(Minimap:GetZoom() + 1);
|
||||
if ( Minimap:GetZoom() == (Minimap:GetZoomLevels() - 1) ) then
|
||||
MinimapZoomIn:Disable();
|
||||
end
|
||||
MinimapZoomStep(1, "igMiniMapZoomIn", MinimapZoomIn, MinimapZoomOut);
|
||||
end
|
||||
|
||||
function Minimap_ZoomOutClick()
|
||||
MinimapZoomIn:Enable();
|
||||
PlaySound("igMiniMapZoomOut");
|
||||
Minimap:SetZoom(Minimap:GetZoom() - 1);
|
||||
if ( Minimap:GetZoom() == 0 ) then
|
||||
MinimapZoomOut:Disable();
|
||||
end
|
||||
MinimapZoomStep(-1, "igMiniMapZoomOut", MinimapZoomOut, MinimapZoomIn);
|
||||
end
|
||||
|
||||
-- ref-Minimap.lua Minimap_OnEvent's MINIMAP_UPDATE_ZOOM branch, verbatim: re-sync both
|
||||
-- buttons to the CURRENT zoom. The app fires MINIMAP_UPDATE_ZOOM when the active zoom index
|
||||
-- switches (stepping inside/outside flips to the other, independent level), so the +/- greying
|
||||
-- follows the map you are actually on instead of the one you left.
|
||||
-- Re-sync BOTH buttons to the zoom as it stands, which is what a click cannot do: a click
|
||||
-- knows which way it moved, this does not. The app fires MINIMAP_UPDATE_ZOOM when the
|
||||
-- active zoom index switches (stepping inside/outside flips to the other, independent
|
||||
-- level), so the +/- greying follows the map you are actually on instead of the one you
|
||||
-- left. Both ends are re-armed first and then at most one is greyed again — a widget with
|
||||
-- a single zoom level sits on both ends at once, and the tight end wins that tie.
|
||||
function Minimap_UpdateZoom()
|
||||
MinimapZoomIn:Enable();
|
||||
MinimapZoomOut:Enable();
|
||||
local zoom = Minimap:GetZoom();
|
||||
if ( zoom == (Minimap:GetZoomLevels() - 1) ) then
|
||||
if ( zoom == MinimapZoomLimit(1) ) then
|
||||
MinimapZoomIn:Disable();
|
||||
elseif ( zoom == 0 ) then
|
||||
elseif ( zoom == MinimapZoomLimit(-1) ) then
|
||||
MinimapZoomOut:Disable();
|
||||
end
|
||||
end
|
||||
@@ -127,6 +206,32 @@
|
||||
GameTooltip:SetText(HAVE_MAIL);
|
||||
end
|
||||
|
||||
-- The tracking icon, same shape as the mail frame above. `GetTrackingTexture()` is the
|
||||
-- WHOLE of the state: the icon path of the one tracking effect the aura rebuild diverts
|
||||
-- out of the buff bar (`ui_aura`), or nil when nothing is tracked — so there is nothing to
|
||||
-- reconcile, the answer is either an icon to wear or a reason to get out of the way.
|
||||
-- PLAYER_AURAS_CHANGED is the only signal that can change it (ref-Minimap.xml l.109-174
|
||||
-- registers nothing else on this frame). Note the icon is NOT cleared on the way out: the
|
||||
-- frame is hidden, and the next tracking effect repaints it before it is seen again.
|
||||
function MiniMapTrackingFrame_OnEvent()
|
||||
if ( event ~= "PLAYER_AURAS_CHANGED" ) then
|
||||
return;
|
||||
end
|
||||
local tracking = GetTrackingTexture();
|
||||
if ( not tracking ) then
|
||||
MiniMapTrackingFrame:Hide();
|
||||
return;
|
||||
end
|
||||
MiniMapTrackingIcon:SetTexture(tracking);
|
||||
MiniMapTrackingFrame:Show();
|
||||
end
|
||||
|
||||
-- Hovering the icon names the tracking spell itself, which the tooltip knows how to build.
|
||||
function MiniMapTrackingFrame_OnEnter()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT");
|
||||
GameTooltip:SetTrackingSpell();
|
||||
end
|
||||
|
||||
]]></Script>
|
||||
<Frame name="MinimapCluster" frameStrata="BACKGROUND" enableMouse="true" parent="UIParent">
|
||||
<Size>
|
||||
@@ -174,18 +279,7 @@
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnEnter>
|
||||
GameTooltip:SetOwner(this, "ANCHOR_LEFT");
|
||||
GameTooltip:AddLine(GetMinimapZoneText(), "", 1.0, 1.0, 1.0);
|
||||
local pvpType, factionName, isArena = GetZonePVPInfo();
|
||||
if ( (pvpType == "friendly") or (pvpType == "hostile") ) then
|
||||
GameTooltip:AddLine(format(FACTION_CONTROLLED_TERRITORY, factionName), "", 1.0, 1.0, 1.0);
|
||||
elseif ( pvpType == "contested" ) then
|
||||
GameTooltip:AddLine(CONTESTED_TERRITORY, "", 1.0, 1.0, 1.0);
|
||||
end
|
||||
if ( isArena ) then
|
||||
GameTooltip:AddLine(FREE_FOR_ALL_TERRITORY, "", 1.0, 1.0, 1.0);
|
||||
end
|
||||
GameTooltip:Show();
|
||||
MinimapZoneTextButton_OnEnter();
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
GameTooltip:Hide();
|
||||
@@ -224,9 +318,10 @@
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<!-- ref-Minimap.xml l.109-174, verbatim — the tracking icon (the mail frame's
|
||||
nesting precedent: a child of the round Minimap WIDGET, whose TOPLEFT the
|
||||
(-15,0) anchor hangs off). Driven by PLAYER_AURAS_CHANGED →
|
||||
<!-- ref-Minimap.xml l.109-174 for the names, geometry and art — the tracking
|
||||
icon (the mail frame's nesting precedent: a child of the round Minimap
|
||||
WIDGET, whose TOPLEFT the (-15,0) anchor hangs off). Driven by
|
||||
PLAYER_AURAS_CHANGED →
|
||||
GetTrackingTexture(): the reference's aura-cache rebuild diverts a
|
||||
tracking-effect aura (Find Minerals, Track Beasts, …) out of the buff bar
|
||||
into a tracking global this icon shows (byte-verified — the {0x2c,0x2d,0x97}
|
||||
@@ -274,15 +369,7 @@
|
||||
this:RegisterEvent("PLAYER_AURAS_CHANGED");
|
||||
</OnLoad>
|
||||
<OnEvent>
|
||||
if ( event == "PLAYER_AURAS_CHANGED" ) then
|
||||
local icon = GetTrackingTexture();
|
||||
if ( icon ) then
|
||||
MiniMapTrackingIcon:SetTexture(icon);
|
||||
MiniMapTrackingFrame:Show();
|
||||
else
|
||||
MiniMapTrackingFrame:Hide();
|
||||
end
|
||||
end
|
||||
MiniMapTrackingFrame_OnEvent();
|
||||
</OnEvent>
|
||||
<OnMouseUp>
|
||||
if ( arg1 == "RightButton" ) then
|
||||
@@ -290,8 +377,7 @@
|
||||
end
|
||||
</OnMouseUp>
|
||||
<OnEnter>
|
||||
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT");
|
||||
GameTooltip:SetTrackingSpell();
|
||||
MiniMapTrackingFrame_OnEnter();
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
GameTooltip:Hide();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The mirror timers - the 1.12 MirrorTimer1/2/3, transcribed from the extracted reference
|
||||
(Interface\FrameXML\MirrorTimer.xml/.lua, mpqx; decision 0874). Geometry, textures, colors
|
||||
and the countdown integration are the reference's own.
|
||||
The mirror timers - our MirrorTimer1/2/3. The extracted reference
|
||||
(Interface\FrameXML\MirrorTimer.xml/.lua, mpqx; decision 0874) is the SPECIFICATION here,
|
||||
not the source: it fixes the frame and function names addons bind to, the geometry, the
|
||||
textures and the four colours, and this file is our own implementation of that contract.
|
||||
|
||||
"Mirror" is the client's word for a timer whose authority is the SERVER: the client never
|
||||
computes breath or fatigue. The server ships a value + a signed rate and the frame
|
||||
@@ -16,18 +17,19 @@
|
||||
look up the bar's caption ("Fatigue" / "Breath"; there is no FEIGNDEATH_LABEL in the 1.12
|
||||
GlobalStrings, so that one shows an empty caption, exactly as the reference does).
|
||||
|
||||
Departures from the reference, all three deliberate:
|
||||
Where our behaviour departs from the reference's, all three deliberate:
|
||||
|
||||
1. THE START DRIVER. The reference registers MIRROR_TIMER_START on UIParent itself and
|
||||
calls MirrorTimer_Show from UIParent_OnEvent (ref UIParent.lua l.97 + l.374-375). Our
|
||||
UIParent.xml is script-only - it has no frame of its own to register events on - so the
|
||||
hidden MirrorTimerDriver below hosts that one registration and makes the identical call.
|
||||
|
||||
2. THE FREE-DIALOG LOOP BOUND. The reference's second loop in MirrorTimer_Show iterates to
|
||||
STATICPOPUP_NUMDIALOGS, a copy-paste from StaticPopup.lua (its first loop, over the same
|
||||
frames, correctly uses MIRRORTIMER_NUMTIMERS). With 3 frames and 3 client timer types
|
||||
the bug is latent - but it would index a nil MirrorTimer4. Corrected to the constant
|
||||
that names these frames.
|
||||
2. ONE WALK OVER THE POOL, NOT TWO. The reference asks its two questions - "is this timer
|
||||
already on a bar?" then "is any bar idle?" - in two separate loops, and bounds the
|
||||
second by STATICPOPUP_NUMDIALOGS, a copy-paste from StaticPopup.lua that would index a
|
||||
nil MirrorTimer4 the day the two constants diverge. A bar already carrying the timer
|
||||
wins over an idle one wherever either sits, so one pass answers both questions and
|
||||
cannot run off the end of the pool.
|
||||
|
||||
3. THE PAUSE ARG. The reference's MIRROR_TIMER_PAUSE branch reads the same arg1 as both a
|
||||
timer name (`arg1 ~= this.timer`, above) and a number (`arg1 > 0`), so a real pause
|
||||
@@ -35,110 +37,108 @@
|
||||
substitutes a full START instead (Player::SendMirrorTimers says so in the source). We
|
||||
read the flag from arg2, which is what both branches plainly intend.
|
||||
|
||||
MirrorTimerColors carries a "DEATH" entry the reference never uses (no timer type maps to
|
||||
it); kept verbatim.
|
||||
|
||||
POSITION: TOP, y=-96 - the reference's own anchor, under the top-center of the screen, with
|
||||
timers 2 and 3 stacking downward from it.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
MIRRORTIMER_NUMTIMERS = 3
|
||||
|
||||
MirrorTimerColors = {}
|
||||
MirrorTimerColors["EXHAUSTION"] = { r = 1.00, g = 0.90, b = 0.00 }
|
||||
MirrorTimerColors["BREATH"] = { r = 0.00, g = 0.50, b = 1.00 }
|
||||
MirrorTimerColors["DEATH"] = { r = 1.00, g = 0.70, b = 0.00 }
|
||||
MirrorTimerColors["FEIGNDEATH"] = { r = 1.00, g = 0.70, b = 0.00 }
|
||||
-- The bar colour per timer type, keyed by the name that arrives on the wire. DEATH is in
|
||||
-- the published set even though nothing in 1.12 ever raises a timer of that type - the
|
||||
-- table is the contract, so it carries the whole set and not just the three we see.
|
||||
MirrorTimerColors = {
|
||||
EXHAUSTION = { r = 1.00, g = 0.90, b = 0.00 },
|
||||
BREATH = { r = 0.00, g = 0.50, b = 1.00 },
|
||||
DEATH = { r = 1.00, g = 0.70, b = 0.00 },
|
||||
FEIGNDEATH = { r = 1.00, g = 0.70, b = 0.00 },
|
||||
}
|
||||
|
||||
-- Which bar `timer` belongs on. A bar already carrying it wins wherever it sits - the
|
||||
-- server re-sends a whole START on every change, and a re-STATE must land back in place
|
||||
-- instead of eating another bar; failing that the first idle bar takes it, and if all
|
||||
-- three are busy the timer simply goes unshown (see departure 2).
|
||||
local function claim_bar(timer)
|
||||
local idle
|
||||
for index = 1, MIRRORTIMER_NUMTIMERS do
|
||||
local bar = getglobal("MirrorTimer" .. index)
|
||||
if bar:IsVisible() then
|
||||
if bar.timer == timer then
|
||||
return bar
|
||||
end
|
||||
elseif not idle then
|
||||
idle = bar
|
||||
end
|
||||
end
|
||||
return idle
|
||||
end
|
||||
|
||||
-- Hand a bar back to the pool: off screen, holding nothing.
|
||||
local function release_bar(bar)
|
||||
bar:Hide()
|
||||
bar.timer = nil
|
||||
end
|
||||
|
||||
-- Raise or re-seat one timer. Milliseconds on the wire, seconds on the bar; `scale` is
|
||||
-- the server's signed rate, which OnUpdate integrates from here.
|
||||
function MirrorTimer_Show(timer, value, maxvalue, scale, paused, label)
|
||||
-- Pick a free dialog to use
|
||||
local dialog = nil
|
||||
if not dialog then
|
||||
-- Find an open dialog of the requested type
|
||||
for index = 1, MIRRORTIMER_NUMTIMERS, 1 do
|
||||
local frame = getglobal("MirrorTimer"..index)
|
||||
if frame:IsVisible() and (frame.timer == timer) then
|
||||
dialog = frame
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if not dialog then
|
||||
-- Find a free dialog (ref: STATICPOPUP_NUMDIALOGS - see departure 2)
|
||||
for index = 1, MIRRORTIMER_NUMTIMERS, 1 do
|
||||
local frame = getglobal("MirrorTimer"..index)
|
||||
if not frame:IsVisible() then
|
||||
dialog = frame
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if not dialog then
|
||||
local bar = claim_bar(timer)
|
||||
if not bar then
|
||||
return nil
|
||||
end
|
||||
|
||||
dialog.timer = timer
|
||||
dialog.value = (value / 1000)
|
||||
dialog.scale = scale
|
||||
if paused > 0 then
|
||||
dialog.paused = 1
|
||||
else
|
||||
dialog.paused = nil
|
||||
end
|
||||
bar.timer = timer
|
||||
bar.value = value / 1000
|
||||
bar.scale = scale
|
||||
bar.paused = paused > 0 and 1 or nil
|
||||
|
||||
-- Set the text of the dialog
|
||||
local text = getglobal(dialog:GetName().."Text")
|
||||
text:SetText(label)
|
||||
local name = bar:GetName()
|
||||
getglobal(name .. "Text"):SetText(label)
|
||||
|
||||
-- Set the status bar of the dialog
|
||||
local statusbar = getglobal(dialog:GetName().."StatusBar")
|
||||
local fill = getglobal(name .. "StatusBar")
|
||||
local color = MirrorTimerColors[timer]
|
||||
statusbar:SetMinMaxValues(0, (maxvalue / 1000))
|
||||
statusbar:SetValue(dialog.value)
|
||||
statusbar:SetStatusBarColor(color.r, color.g, color.b)
|
||||
fill:SetMinMaxValues(0, maxvalue / 1000)
|
||||
fill:SetValue(bar.value)
|
||||
fill:SetStatusBarColor(color.r, color.g, color.b)
|
||||
|
||||
dialog:Show()
|
||||
|
||||
return dialog
|
||||
bar:Show()
|
||||
return bar
|
||||
end
|
||||
|
||||
function MirrorTimerFrame_OnLoad()
|
||||
-- The two events that address a timer already running...
|
||||
this:RegisterEvent("MIRROR_TIMER_PAUSE")
|
||||
this:RegisterEvent("MIRROR_TIMER_STOP")
|
||||
-- ...and the one that ends whatever this bar was holding.
|
||||
this:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
this.timer = nil
|
||||
end
|
||||
|
||||
function MirrorTimerFrame_OnEvent()
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
this:Hide()
|
||||
this.timer = nil
|
||||
-- A worldport ends every timer the old world was running; a stale breath bar must
|
||||
-- not survive the loading screen.
|
||||
release_bar(this)
|
||||
return
|
||||
end
|
||||
if not this:IsShown() or (arg1 ~= this.timer) then
|
||||
-- Everything below names one running timer. Anything else is another bar's business.
|
||||
if not this:IsShown() or arg1 ~= this.timer then
|
||||
return
|
||||
end
|
||||
if event == "MIRROR_TIMER_PAUSE" then
|
||||
-- ref: `if arg1 > 0` on the name it just string-compared (see departure 3)
|
||||
if arg2 and arg2 > 0 then
|
||||
this.paused = 1
|
||||
else
|
||||
this.paused = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
if event == "MIRROR_TIMER_STOP" then
|
||||
this:Hide()
|
||||
this.timer = nil
|
||||
-- The flag rides in arg2 (departure 3), and absent means running.
|
||||
this.paused = (arg2 and arg2 > 0) and 1 or nil
|
||||
elseif event == "MIRROR_TIMER_STOP" then
|
||||
release_bar(this)
|
||||
end
|
||||
end
|
||||
|
||||
-- The whole of the client-side motion: between packets the bar walks itself along the
|
||||
-- rate the server named. Nothing here decides anything - drowning is the server's call.
|
||||
function MirrorTimerFrame_OnUpdate(frame, elapsed)
|
||||
if frame.paused then
|
||||
return
|
||||
if not frame.paused then
|
||||
frame.value = frame.value + frame.scale * elapsed
|
||||
getglobal(frame:GetName() .. "StatusBar"):SetValue(frame.value)
|
||||
end
|
||||
local statusbar = getglobal(frame:GetName().."StatusBar")
|
||||
frame.value = (frame.value + frame.scale * elapsed)
|
||||
statusbar:SetValue(frame.value)
|
||||
end
|
||||
|
||||
-- The ref's UIParent_OnEvent MIRROR_TIMER_START arm, rehosted (see departure 1).
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The reference's MONEY FRAME kit — `MoneyFrameTemplate` / `SmallMoneyFrameTemplate` plus the
|
||||
`MoneyTypeInfo` table and the `MoneyFrame_*` API that drives them. Transcribed from the
|
||||
extracted 1.12 `Interface\FrameXML\MoneyFrame.lua` (356 lines) and `MoneyFrame.xml` (297),
|
||||
line numbers cited below as `l.N`.
|
||||
THE MONEY FRAME KIT — `MoneyFrameTemplate` / `SmallMoneyFrameTemplate`, the `MoneyTypeInfo`
|
||||
table and the `MoneyFrame_*` API that drives them.
|
||||
|
||||
WHY IT IS HERE: it is FrameXML's, not Rust's (decision 1190 — a name the shipped 1.12 UI
|
||||
defines is transcribed, never hardcoded), and the corpus asks for it by name.
|
||||
This is **our implementation** of that kit. The specification it implements is the extracted
|
||||
1.12 `Interface\FrameXML\MoneyFrame.lua` (356 lines) and `MoneyFrame.xml` (297), cited below
|
||||
as `ref l.N`: the names, the published field names, the constants and every branch of the
|
||||
collapse rule are the specification's, because addons call this by name and a compatible kit
|
||||
has no freedom in any of that. How it is written — the structure, the helpers, the local
|
||||
names, the comments — is ours.
|
||||
|
||||
WHY IT IS HERE: it is FrameXML's surface, not Rust's (decision 1190 — a name the shipped 1.12
|
||||
UI defines belongs in a UI file, never hardcoded), and the addon corpus asks for it by name.
|
||||
`MoneyFrame_SetType` is wanted by 11 addons, `MoneyFrame_Update` by 9, `SmallMoneyFrame_OnLoad`
|
||||
by 9, and 13 declare a frame `inherits="SmallMoneyFrameTemplate"`. `MoneyTypeInfo` was one of
|
||||
the top runtime walls the frame rename left standing.
|
||||
@@ -16,375 +21,418 @@
|
||||
NOT use this file. They render coins through `BenillaMoney_Set`/`_Clear`/`_SetColor` in
|
||||
MerchantFrame.xml: a three-SLOT chain (`<prefix>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 was quoted FROM this one (its header cites
|
||||
these same line numbers for the coin TexCoords, the collapse and the shrink) and its layout is
|
||||
the one the director has signed off across those eight windows.
|
||||
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_*` + `<prefix>Coin1..3` — what our own transcribed windows are laid out on.
|
||||
· `MoneyFrame_*` + `MoneyTypeInfo` here — the reference's names and structure, which is
|
||||
what an addon writes against and what an addon's `inherits="SmallMoneyFrameTemplate"`
|
||||
resolves to.
|
||||
· `BenillaMoney_*` + `<prefix>Coin1..3` — what our own windows are laid out on.
|
||||
· `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 reference 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.
|
||||
`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 IS NOT CARRIED, and why (each is guarded at its own site below) ─────────────────────
|
||||
· THE COIN PICKUP. `OpenCoinPickupFrame`/`UpdateCoinPickupFrame`/`CoinPickupFrame` live in the
|
||||
reference's CoinPickupFrame.lua/.xml, which we do not ship — and the engine verbs underneath
|
||||
it (`PickupPlayerMoney`, `DropCursorMoney`, `PickupTradeMoney`, `AddTradeMoney`) are not
|
||||
built either, so a picked-up coin would have nowhere to go. The templates' `<OnClick>` is
|
||||
the reference's, guarded on the function existing; a `canPickup` money frame therefore
|
||||
enables its mouse (the reference's own state) and clicking it does nothing.
|
||||
· `GetCursorMoney` (PLAYER's UpdateFunc, l.17) — the engine verb for coins held on the cursor.
|
||||
· THE COIN PICKUP. `OpenCoinPickupFrame`/`UpdateCoinPickupFrame`/`CoinPickupFrame` belong to
|
||||
CoinPickupFrame.lua/.xml, which we do not ship — and the engine verbs underneath them
|
||||
(`PickupPlayerMoney`, `DropCursorMoney`, `PickupTradeMoney`, `AddTradeMoney`) are not built
|
||||
either, so a picked-up coin would have nowhere to go. The coin buttons still take the mouse
|
||||
for a `canPickup` type (that is the specified state) and the click is guarded, so it does
|
||||
nothing.
|
||||
· `GetCursorMoney` (PLAYER's amount, ref l.17) — the engine verb for coins held on the cursor.
|
||||
With no coin pickup there is no cursor money, so its term is not approximated away: it is
|
||||
structurally zero. Stated at the spot.
|
||||
· `GetSendMailMoney` / `GetSendMailCOD` (l.73, l.90) — 1.12 engine verbs we have not built. We
|
||||
ship their SETTERS (`SetSendMailMoney`/`SetSendMailCOD`, which MailFrame.xml drives); nothing
|
||||
reads the amount back yet. Guarded to 0 rather than left as a nil call.
|
||||
· `message()` (l.145, l.173, l.181) — the reference's one-line StaticPopup shim, which lives in
|
||||
BasicControls.xml, a file family we have not transcribed. Guarded, so a misuse of the API
|
||||
stays silent instead of erroring on the diagnostic.
|
||||
· `Button:GetTextWidth()` (l.202 etc.) — our Button has no such method yet. The button's own
|
||||
· `GetSendMailMoney` / `GetSendMailCOD` (ref l.73, l.90) — 1.12 engine verbs we have not
|
||||
built. We ship their SETTERS (`SetSendMailMoney`/`SetSendMailCOD`, which MailFrame.xml
|
||||
drives); nothing reads the amount back yet, so those two types read 0.
|
||||
· `message()` (ref l.145, l.173, l.181) — the one-line StaticPopup shim, which belongs to
|
||||
BasicControls.xml, a file family we have not built. Guarded, so a misuse of the API stays
|
||||
silent instead of erroring on the diagnostic.
|
||||
· `Button:GetTextWidth()` (ref l.202 etc.) — our Button has no such method. The button's own
|
||||
`<ButtonText name="$parentText">` FontString carries `GetStringWidth`, which is the same
|
||||
measurement, and that is what is called.
|
||||
· The four `Pickup*`/`Add*` mail verbs the reference's own MoneyTypeInfo closures name are not
|
||||
in 1.12's `_G` either (they are absent from reference/1.12-globals.tsv) — so those bodies are
|
||||
transcribed verbatim and are as unreachable here as they are there.
|
||||
measurement of the same string with the same font object, and that is what we call.
|
||||
· The four `Pickup*`/`Add*` mail verbs the pickup closures name are not in 1.12's `_G` either
|
||||
(they are absent from reference/1.12-globals.tsv) — so those closures are as unreachable
|
||||
here as they are there. They exist because the table's shape is the contract.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- l.2-12, verbatim.
|
||||
MONEY_ICON_WIDTH = 19
|
||||
MONEY_ICON_WIDTH_SMALL = 13
|
||||
|
||||
MONEY_BUTTON_SPACING = -4
|
||||
MONEY_BUTTON_SPACING_SMALL = -4
|
||||
|
||||
-- ── What a coin is worth ─────────────────────────────────────────────────────────────────
|
||||
-- Published globals: an addon that formats a price does the same split we do below, and
|
||||
-- reads these rather than spelling 100 twice.
|
||||
COPPER_PER_SILVER = 100
|
||||
SILVER_PER_GOLD = 100
|
||||
COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD
|
||||
|
||||
-- ── How wide a coin draws ────────────────────────────────────────────────────────────────
|
||||
-- One coin icon's width at each of the two sizes the templates below come in, and the gap
|
||||
-- between two coin buttons — NEGATIVE, because the three deliberately bite into each other
|
||||
-- so a group reads as one number rather than as three. `COIN_BUTTON_WIDTH` is the fixed slot
|
||||
-- a `fixedWidth` type pads its smaller coins out to, so a column of prices lines up.
|
||||
-- The values are the specification's (ref l.2-12): measurements of the shipped coin art, not
|
||||
-- ours to re-derive.
|
||||
MONEY_ICON_WIDTH = 19
|
||||
MONEY_ICON_WIDTH_SMALL = 13
|
||||
MONEY_BUTTON_SPACING = -4
|
||||
MONEY_BUTTON_SPACING_SMALL = -4
|
||||
COIN_BUTTON_WIDTH = 32
|
||||
|
||||
-- l.14-103: the money TYPES. A money frame carries no amount of its own — it carries a type,
|
||||
-- and the type says where to read the number from (`UpdateFunc`, called with `this` set to the
|
||||
-- frame), whether zero denominations collapse away, whether the smaller coins show anyway, and
|
||||
-- whether the coins are click-to-pick-up. Addons read this table directly, so its shape is the
|
||||
-- contract, not just its behaviour.
|
||||
-- ── The money types (ref l.14-103) ───────────────────────────────────────────────────────
|
||||
--
|
||||
-- A money frame carries no amount of its own: it carries a TYPE, and the type says where the
|
||||
-- number comes from and how the coins behave. An entry has three parts:
|
||||
--
|
||||
-- · `UpdateFunc()` returns the amount, in copper. It is called with the global `this` bound
|
||||
-- to the frame being painted, so a type may read the frame as well as the engine.
|
||||
-- · `PickupFunc(amount)` / `DropFunc()` are the cursor verbs a click-to-pick-up type drives.
|
||||
-- · the display flags `MoneyFrame_Update` reads: `collapse` (drop leading zero
|
||||
-- denominations), `showSmallerCoins` (keep the trailing ones on even at zero; the value
|
||||
-- "Backpack" additionally keeps copper on by itself), `canPickup` (the coins take the
|
||||
-- mouse), `fixedWidth` (pad the smaller coins to COIN_BUTTON_WIDTH), `truncateSmallCoins`
|
||||
-- (drop copper once there is gold).
|
||||
--
|
||||
-- Nothing else is consulted. That is why those field names and the two function signatures
|
||||
-- are the published contract and not an implementation detail: addons register types of their
|
||||
-- own into this table and expect exactly these parts to be honoured.
|
||||
MoneyTypeInfo = { }
|
||||
|
||||
-- Read an amount through an engine verb that may not be built here yet: absent, it reads 0
|
||||
-- rather than calling into nil. Two of the seven types below need it — see the header.
|
||||
local function AmountFrom(verbName)
|
||||
local verb = getglobal(verbName)
|
||||
if verb then
|
||||
return verb()
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- The purse. The specification subtracts a third term here (ref l.17,
|
||||
-- `GetMoney() - GetCursorMoney() - GetPlayerTradeMoney()`): coins lifted onto the cursor by
|
||||
-- the coin-pickup frame. benilla ships neither that frame nor the `PickupPlayerMoney` verb
|
||||
-- behind it, so the term is structurally 0 rather than approximated away — it comes back the
|
||||
-- moment either lands.
|
||||
MoneyTypeInfo["PLAYER"] = {
|
||||
UpdateFunc = function()
|
||||
-- Reference l.17: `GetMoney() - GetCursorMoney() - GetPlayerTradeMoney()`. The
|
||||
-- cursor term is dropped, not approximated: `GetCursorMoney` is the amount lifted onto
|
||||
-- the cursor by the coin-pickup frame, and benilla ships neither that frame nor the
|
||||
-- `PickupPlayerMoney` verb behind it, so it is structurally 0 here. The term returns
|
||||
-- the moment either lands.
|
||||
return GetMoney() - GetPlayerTradeMoney()
|
||||
end,
|
||||
|
||||
PickupFunc = function(amount)
|
||||
PickupPlayerMoney(amount)
|
||||
end,
|
||||
|
||||
DropFunc = function()
|
||||
DropCursorMoney()
|
||||
end,
|
||||
|
||||
UpdateFunc = function() return GetMoney() - GetPlayerTradeMoney() end,
|
||||
PickupFunc = function(amount) PickupPlayerMoney(amount) end,
|
||||
DropFunc = function() DropCursorMoney() end,
|
||||
collapse = 1,
|
||||
canPickup = 1,
|
||||
showSmallerCoins = "Backpack"
|
||||
showSmallerCoins = "Backpack",
|
||||
}
|
||||
MoneyTypeInfo["STATIC"] = {
|
||||
UpdateFunc = function()
|
||||
return this.staticMoney
|
||||
end,
|
||||
|
||||
-- A number the owner parked on the frame itself — what `MoneyFrame_Update(name, amount)`
|
||||
-- leaves behind, and how an addon shows a price it worked out for itself.
|
||||
MoneyTypeInfo["STATIC"] = {
|
||||
UpdateFunc = function() return this.staticMoney end,
|
||||
collapse = 1,
|
||||
}
|
||||
|
||||
-- The auction house's price column: the same source as STATIC, but the smaller coins pad out
|
||||
-- to a fixed slot and stay on at zero, so a stack of prices reads as a column of digits.
|
||||
MoneyTypeInfo["AUCTION"] = {
|
||||
UpdateFunc = function()
|
||||
return this.staticMoney
|
||||
end,
|
||||
UpdateFunc = function() return this.staticMoney end,
|
||||
collapse = 1,
|
||||
showSmallerCoins = "Backpack",
|
||||
fixedWidth = 1,
|
||||
collapse = 1,
|
||||
truncateSmallCoins = nil,
|
||||
}
|
||||
|
||||
-- Our side of an open trade, and the other party's. Only ours is click-to-pick-up.
|
||||
MoneyTypeInfo["PLAYER_TRADE"] = {
|
||||
UpdateFunc = function()
|
||||
return GetPlayerTradeMoney()
|
||||
end,
|
||||
|
||||
PickupFunc = function(amount)
|
||||
PickupTradeMoney(amount)
|
||||
end,
|
||||
|
||||
DropFunc = function()
|
||||
AddTradeMoney()
|
||||
end,
|
||||
|
||||
UpdateFunc = function() return GetPlayerTradeMoney() end,
|
||||
PickupFunc = function(amount) PickupTradeMoney(amount) end,
|
||||
DropFunc = function() AddTradeMoney() end,
|
||||
collapse = 1,
|
||||
canPickup = 1,
|
||||
}
|
||||
MoneyTypeInfo["TARGET_TRADE"] = {
|
||||
UpdateFunc = function()
|
||||
return GetTargetTradeMoney()
|
||||
end,
|
||||
|
||||
MoneyTypeInfo["TARGET_TRADE"] = {
|
||||
UpdateFunc = function() return GetTargetTradeMoney() end,
|
||||
collapse = 1,
|
||||
}
|
||||
|
||||
-- Mail postage and cash-on-delivery. Postage explicitly does NOT collapse (ref l.84): the
|
||||
-- send-mail frame wants all three coins standing whatever the amount, because you are typing
|
||||
-- into them. Both read 0 until their engine getters exist — see the header.
|
||||
MoneyTypeInfo["SEND_MAIL"] = {
|
||||
UpdateFunc = function()
|
||||
-- `GetSendMailMoney` (ref l.73) is an engine verb we have not built — see the header.
|
||||
if not GetSendMailMoney then return 0 end
|
||||
return GetSendMailMoney()
|
||||
end,
|
||||
|
||||
PickupFunc = function(amount)
|
||||
PickupSendMailMoney(amount)
|
||||
end,
|
||||
|
||||
DropFunc = function()
|
||||
AddSendMailMoney()
|
||||
end,
|
||||
|
||||
UpdateFunc = function() return AmountFrom("GetSendMailMoney") end,
|
||||
PickupFunc = function(amount) PickupSendMailMoney(amount) end,
|
||||
DropFunc = function() AddSendMailMoney() end,
|
||||
collapse = nil,
|
||||
canPickup = 1,
|
||||
showSmallerCoins = "Backpack",
|
||||
}
|
||||
|
||||
MoneyTypeInfo["SEND_MAIL_COD"] = {
|
||||
UpdateFunc = function()
|
||||
-- `GetSendMailCOD` (ref l.90) — same gap as SEND_MAIL above.
|
||||
if not GetSendMailCOD then return 0 end
|
||||
return GetSendMailCOD()
|
||||
end,
|
||||
|
||||
PickupFunc = function(amount)
|
||||
PickupSendMailCOD(amount)
|
||||
end,
|
||||
|
||||
DropFunc = function()
|
||||
AddSendMailCOD()
|
||||
end,
|
||||
|
||||
UpdateFunc = function() return AmountFrom("GetSendMailCOD") end,
|
||||
PickupFunc = function(amount) PickupSendMailCOD(amount) end,
|
||||
DropFunc = function() AddSendMailCOD() end,
|
||||
collapse = 1,
|
||||
canPickup = 1,
|
||||
}
|
||||
|
||||
-- l.105-122: the two OnLoads. Identical but for `this.small`, which is what picks the 13px
|
||||
-- coin + tight spacing over the 19px one in MoneyFrame_Update below. Both then take type
|
||||
-- PLAYER, so a frame that is never told otherwise shows the purse.
|
||||
-- ── Hearing about a change (ref l.105-140) ───────────────────────────────────────────────
|
||||
--
|
||||
-- Every money frame registers all five money events; each then decides whether the one that
|
||||
-- arrived is about the number IT is showing. That pairing is data, not control flow, so it is
|
||||
-- written as data: one row per event, in the order the templates register them, naming the
|
||||
-- money types that event can change.
|
||||
local MONEY_EVENTS = {
|
||||
{ "PLAYER_MONEY", { PLAYER = 1 } },
|
||||
{ "PLAYER_TRADE_MONEY", { PLAYER = 1, PLAYER_TRADE = 1 } },
|
||||
{ "TRADE_MONEY_CHANGED", { TARGET_TRADE = 1 } },
|
||||
{ "SEND_MAIL_MONEY_CHANGED", { PLAYER = 1, SEND_MAIL = 1 } },
|
||||
{ "SEND_MAIL_COD_CHANGED", { PLAYER = 1, SEND_MAIL_COD = 1 } },
|
||||
}
|
||||
|
||||
-- The same rows keyed by event name, built once, so the handler below is a lookup.
|
||||
local moneyEventTypes = { }
|
||||
for _, row in ipairs(MONEY_EVENTS) do
|
||||
moneyEventTypes[row[1]] = row[2]
|
||||
end
|
||||
|
||||
local function MoneyFrameListen(frame)
|
||||
for _, row in ipairs(MONEY_EVENTS) do
|
||||
frame:RegisterEvent(row[1])
|
||||
end
|
||||
end
|
||||
|
||||
-- The two OnLoads differ in exactly one thing: `small`, which is what picks the 13px coin and
|
||||
-- its tighter spacing over the 19px one in MoneyFrame_Update below. Both then take type
|
||||
-- PLAYER, so a frame nobody tells otherwise shows the purse.
|
||||
function MoneyFrame_OnLoad()
|
||||
this:RegisterEvent("PLAYER_MONEY")
|
||||
this:RegisterEvent("PLAYER_TRADE_MONEY")
|
||||
this:RegisterEvent("TRADE_MONEY_CHANGED")
|
||||
this:RegisterEvent("SEND_MAIL_MONEY_CHANGED")
|
||||
this:RegisterEvent("SEND_MAIL_COD_CHANGED")
|
||||
MoneyFrameListen(this)
|
||||
MoneyFrame_SetType("PLAYER")
|
||||
end
|
||||
|
||||
function SmallMoneyFrame_OnLoad()
|
||||
this:RegisterEvent("PLAYER_MONEY")
|
||||
this:RegisterEvent("PLAYER_TRADE_MONEY")
|
||||
this:RegisterEvent("TRADE_MONEY_CHANGED")
|
||||
this:RegisterEvent("SEND_MAIL_MONEY_CHANGED")
|
||||
this:RegisterEvent("SEND_MAIL_COD_CHANGED")
|
||||
MoneyFrameListen(this)
|
||||
this.small = 1
|
||||
MoneyFrame_SetType("PLAYER")
|
||||
end
|
||||
|
||||
-- l.124-140, verbatim: every money frame hears every money event, and each decides for itself
|
||||
-- whether the one that arrived is about the number IT is showing.
|
||||
function MoneyFrame_OnEvent()
|
||||
if ( not this.info or not this:IsVisible() ) then
|
||||
if not this.info or not this:IsVisible() then
|
||||
return
|
||||
end
|
||||
|
||||
if ( event == "PLAYER_MONEY" and this.moneyType == "PLAYER" ) then
|
||||
MoneyFrame_UpdateMoney()
|
||||
elseif ( event == "PLAYER_TRADE_MONEY" and (this.moneyType == "PLAYER" or this.moneyType == "PLAYER_TRADE") ) then
|
||||
MoneyFrame_UpdateMoney()
|
||||
elseif ( event == "TRADE_MONEY_CHANGED" and this.moneyType == "TARGET_TRADE" ) then
|
||||
MoneyFrame_UpdateMoney()
|
||||
elseif ( event == "SEND_MAIL_MONEY_CHANGED" and (this.moneyType == "PLAYER" or this.moneyType == "SEND_MAIL") ) then
|
||||
MoneyFrame_UpdateMoney()
|
||||
elseif ( event == "SEND_MAIL_COD_CHANGED" and (this.moneyType == "PLAYER" or this.moneyType == "SEND_MAIL_COD") ) then
|
||||
local affected = moneyEventTypes[event]
|
||||
if affected and affected[this.moneyType] then
|
||||
MoneyFrame_UpdateMoney()
|
||||
end
|
||||
end
|
||||
|
||||
-- l.142-162: point a frame at a money type. Operates on `this`, not on a passed frame — the
|
||||
-- reference's convention here, and what the eleven corpus callers write.
|
||||
-- ── Painting ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- A money frame's three coin buttons, biggest first. Every verb below addresses them as a
|
||||
-- trio, so this is the one place their `$parent<Denomination>Button` names are spelled.
|
||||
local function MoneyFrameCoins(frameName)
|
||||
return getglobal(frameName .. "GoldButton"),
|
||||
getglobal(frameName .. "SilverButton"),
|
||||
getglobal(frameName .. "CopperButton")
|
||||
end
|
||||
|
||||
-- Point a frame at a money type (ref l.142-162): remember it, wire the mouse to match, and
|
||||
-- repaint. Reads `this` rather than taking a frame — that is the convention the eleven corpus
|
||||
-- callers write against.
|
||||
function MoneyFrame_SetType(type)
|
||||
local info = MoneyTypeInfo[type]
|
||||
if ( not info ) then
|
||||
-- ref l.145 `message("Invalid money type: "..type)` — `message` is BasicControls.xml's
|
||||
-- StaticPopup shim, not transcribed (see the header).
|
||||
if ( message ) then message("Invalid money type: "..type) end
|
||||
if not info then
|
||||
-- ref l.145 diagnoses this with `message()`, which we do not ship (see the header).
|
||||
if message then
|
||||
message("Invalid money type: " .. type)
|
||||
end
|
||||
return
|
||||
end
|
||||
this.info = info
|
||||
this.moneyType = type
|
||||
local frameName = this:GetName()
|
||||
if ( info.canPickup ) then
|
||||
getglobal(frameName.."GoldButton"):EnableMouse(true)
|
||||
getglobal(frameName.."SilverButton"):EnableMouse(true)
|
||||
getglobal(frameName.."CopperButton"):EnableMouse(true)
|
||||
else
|
||||
getglobal(frameName.."GoldButton"):EnableMouse(false)
|
||||
getglobal(frameName.."SilverButton"):EnableMouse(false)
|
||||
getglobal(frameName.."CopperButton"):EnableMouse(false)
|
||||
end
|
||||
|
||||
-- Only a click-to-pick-up type wants the mouse; on the rest a click falls through to
|
||||
-- whatever is behind the coins.
|
||||
local gold, silver, copper = MoneyFrameCoins(this:GetName())
|
||||
local pickable = info.canPickup and true or false
|
||||
gold:EnableMouse(pickable)
|
||||
silver:EnableMouse(pickable)
|
||||
copper:EnableMouse(pickable)
|
||||
MoneyFrame_UpdateMoney()
|
||||
end
|
||||
|
||||
-- l.164-175: re-read the number from the type and repaint. `this` again.
|
||||
-- Re-read the amount from the type and repaint (ref l.164-175). `this` again.
|
||||
function MoneyFrame_UpdateMoney()
|
||||
if ( this.info ) then
|
||||
local money = this.info.UpdateFunc()
|
||||
MoneyFrame_Update(this:GetName(), money)
|
||||
-- ref l.169-171 also drives the coin-pickup frame while one is open. `hasPickup` is
|
||||
-- only ever set by the templates' OnClick, which is guarded off (see the header), so
|
||||
-- this branch is unreachable today rather than removed.
|
||||
if ( this.hasPickup == 1 and UpdateCoinPickupFrame ) then
|
||||
UpdateCoinPickupFrame(money)
|
||||
if not this.info then
|
||||
if message then
|
||||
message("Error moneyType not set")
|
||||
end
|
||||
else
|
||||
if ( message ) then message("Error moneyType not set") end
|
||||
return
|
||||
end
|
||||
local money = this.info.UpdateFunc()
|
||||
MoneyFrame_Update(this:GetName(), money)
|
||||
-- ref l.169-171 also feeds the coin-pickup frame while one is open. `hasPickup` is set
|
||||
-- only by the coin click, which is guarded off (see the header), so this is unreachable
|
||||
-- today rather than removed.
|
||||
if this.hasPickup == 1 and UpdateCoinPickupFrame then
|
||||
UpdateCoinPickupFrame(money)
|
||||
end
|
||||
end
|
||||
|
||||
-- The button's laid-out text width. The reference calls `Button:GetTextWidth()` (l.202 and
|
||||
-- kin); our Button has no such method, but the `<ButtonText name="$parentText">` FontString
|
||||
-- the templates below name does carry `GetStringWidth`, which measures the same string with
|
||||
-- the same font object. Falls back to 0 when the label has not resolved (headless, no atlas),
|
||||
-- which leaves the coin at exactly its icon width — the same shape, without the digits.
|
||||
local function moneyButtonTextWidth(buttonName)
|
||||
local text = getglobal(buttonName.."Text")
|
||||
if ( not text or not text.GetStringWidth ) then return 0 end
|
||||
return text:GetStringWidth() or 0
|
||||
-- Lay one denomination out: its digits, and the width those digits plus the coin icon need.
|
||||
--
|
||||
-- The specification measures with `Button:GetTextWidth()` (ref l.202 and kin); our Button
|
||||
-- carries no such method, but the `<ButtonText name="$parentText">` FontString the templates
|
||||
-- below name does carry `GetStringWidth`, which measures the same string with the same font
|
||||
-- object. It reads 0 before the label has resolved (headless, no font atlas), which leaves the
|
||||
-- coin at exactly its icon width — the same shape, without the digits.
|
||||
local function ShowCoin(coin, coinName, amount, iconWidth)
|
||||
coin:SetText(amount)
|
||||
local label = getglobal(coinName .. "Text")
|
||||
local digits = 0
|
||||
if label and label.GetStringWidth then
|
||||
digits = label:GetStringWidth() or 0
|
||||
end
|
||||
coin:SetWidth(digits + iconWidth)
|
||||
coin:Show()
|
||||
end
|
||||
|
||||
-- l.177-270: THE PAINTER. Splits a copper amount into the three denominations, sizes each coin
|
||||
-- to its own digits + icon, and then — if the type collapses — hides the leading zeros and
|
||||
-- re-chains what is left so the group packs tight and the frame reports its true width.
|
||||
-- Chain the next-bigger coin onto `coin`, and report what `coin` contributes to the group's
|
||||
-- width.
|
||||
--
|
||||
-- Note that this one takes a frame NAME, not `this`: it is the only entry point in the family
|
||||
-- an owner can call about a frame that is not the one currently dispatching (`MoneyFrame_Update
|
||||
-- (frameName, money)` is how an addon paints a STATIC frame it just filled).
|
||||
-- Showing: the bigger coin hangs off this one's LEFT edge by `spacing` (negative — they
|
||||
-- overlap), and the group grows by this coin's width less that overlap, but only when the
|
||||
-- bigger coin is actually on screen to overlap with. Not showing: this coin hides and the
|
||||
-- bigger one collapses onto its RIGHT edge, contributing nothing. `padToSlot` is the
|
||||
-- `fixedWidth` exception, which sizes the coin to the fixed column slot first.
|
||||
local function ChainCoin(bigger, coin, coinName, showing, padToSlot, spacing)
|
||||
if not showing then
|
||||
coin:Hide()
|
||||
bigger:SetPoint("RIGHT", coinName, "RIGHT", 0, 0)
|
||||
return 0
|
||||
end
|
||||
if padToSlot then
|
||||
coin:SetWidth(COIN_BUTTON_WIDTH)
|
||||
end
|
||||
local grown = coin:GetWidth()
|
||||
bigger:SetPoint("RIGHT", coinName, "LEFT", spacing, 0)
|
||||
if bigger:IsVisible() then
|
||||
grown = grown - spacing
|
||||
end
|
||||
return grown
|
||||
end
|
||||
|
||||
-- THE PAINTER (ref l.177-270). Split a copper amount into the three denominations, size each
|
||||
-- coin to its own digits plus its icon, and then — if the type collapses — drop the leading
|
||||
-- zeros, re-chain what is left so the group packs tight, and report the frame's true width.
|
||||
--
|
||||
-- This one takes a frame NAME, not `this`: it is the only entry point in the family an owner
|
||||
-- can call about a frame other than the one currently dispatching, which is how an addon
|
||||
-- paints a STATIC frame it has just filled.
|
||||
function MoneyFrame_Update(frameName, money)
|
||||
local frame = getglobal(frameName)
|
||||
local info = frame.info
|
||||
if ( not info ) then
|
||||
if ( message ) then message("Error moneyType not set") end
|
||||
if not info then
|
||||
-- ref l.181 diagnoses and, notably, does NOT return: the coins below are still laid
|
||||
-- out, and the collapse read further down is what actually stops it. Kept as-is.
|
||||
if message then
|
||||
message("Error moneyType not set")
|
||||
end
|
||||
end
|
||||
|
||||
-- Breakdown the money into denominations
|
||||
local gold = floor(money / (COPPER_PER_SILVER * SILVER_PER_GOLD))
|
||||
local silver = floor((money - (gold * COPPER_PER_SILVER * SILVER_PER_GOLD)) / COPPER_PER_SILVER)
|
||||
-- Copper is the low two digits, silver the next two, gold everything above. Silver comes
|
||||
-- out by subtracting the gold back off rather than by a second `mod`: `mod` here is 1.12's
|
||||
-- `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.
|
||||
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)
|
||||
|
||||
local goldButton = getglobal(frameName.."GoldButton")
|
||||
local silverButton = getglobal(frameName.."SilverButton")
|
||||
local copperButton = getglobal(frameName.."CopperButton")
|
||||
|
||||
local iconWidth = MONEY_ICON_WIDTH
|
||||
local spacing = MONEY_BUTTON_SPACING
|
||||
if ( frame.small ) then
|
||||
iconWidth = MONEY_ICON_WIDTH_SMALL
|
||||
spacing = MONEY_BUTTON_SPACING_SMALL
|
||||
local goldCoin, silverCoin, copperCoin = MoneyFrameCoins(frameName)
|
||||
local iconWidth, spacing = MONEY_ICON_WIDTH, MONEY_BUTTON_SPACING
|
||||
if frame.small then
|
||||
iconWidth, spacing = MONEY_ICON_WIDTH_SMALL, MONEY_BUTTON_SPACING_SMALL
|
||||
end
|
||||
|
||||
-- Set values for each denomination
|
||||
goldButton:SetText(gold)
|
||||
goldButton:SetWidth(moneyButtonTextWidth(frameName.."GoldButton") + iconWidth)
|
||||
goldButton:Show()
|
||||
silverButton:SetText(silver)
|
||||
silverButton:SetWidth(moneyButtonTextWidth(frameName.."SilverButton") + iconWidth)
|
||||
silverButton:Show()
|
||||
copperButton:SetText(copper)
|
||||
copperButton:SetWidth(moneyButtonTextWidth(frameName.."CopperButton") + iconWidth)
|
||||
copperButton:Show()
|
||||
ShowCoin(goldCoin, frameName .. "GoldButton", gold, iconWidth)
|
||||
ShowCoin(silverCoin, frameName .. "SilverButton", silver, iconWidth)
|
||||
ShowCoin(copperCoin, frameName .. "CopperButton", copper, iconWidth)
|
||||
|
||||
-- Store how much money the frame is displaying
|
||||
-- Remember what the frame is displaying — STATIC's own reader reads it back.
|
||||
frame.staticMoney = money
|
||||
|
||||
-- If not collapsable don't need to continue
|
||||
if ( not info.collapse ) then
|
||||
-- A type that does not collapse is finished: all three coins stand, at the width their
|
||||
-- digits asked for, and the frame keeps whatever width it was given.
|
||||
if not info.collapse then
|
||||
return
|
||||
end
|
||||
|
||||
-- Gold leads. Present, it stays and sets the terms for the two below it; absent, it goes.
|
||||
-- The running width starts at one icon's worth of frame padding.
|
||||
local width = iconWidth
|
||||
local showLowerDenominations, truncateCopper
|
||||
if ( gold > 0 ) then
|
||||
width = width + goldButton:GetWidth()
|
||||
if ( info.showSmallerCoins ) then
|
||||
showLowerDenominations = 1
|
||||
end
|
||||
if ( info.truncateSmallCoins ) then
|
||||
truncateCopper = 1
|
||||
end
|
||||
local keepSmaller, dropCopper
|
||||
if gold > 0 then
|
||||
width = width + goldCoin:GetWidth()
|
||||
keepSmaller = info.showSmallerCoins and 1 or nil
|
||||
dropCopper = info.truncateSmallCoins and 1 or nil
|
||||
else
|
||||
goldButton:Hide()
|
||||
goldCoin:Hide()
|
||||
end
|
||||
|
||||
if ( silver > 0 or showLowerDenominations ) then
|
||||
-- Exception if showLowerDenominations and fixedWidth
|
||||
if ( showLowerDenominations and info.fixedWidth ) then
|
||||
silverButton:SetWidth(COIN_BUTTON_WIDTH)
|
||||
end
|
||||
|
||||
width = width + silverButton:GetWidth()
|
||||
goldButton:SetPoint("RIGHT", frameName.."SilverButton", "LEFT", spacing, 0)
|
||||
if ( goldButton:IsVisible() ) then
|
||||
width = width - spacing
|
||||
end
|
||||
if ( info.showSmallerCoins ) then
|
||||
showLowerDenominations = 1
|
||||
end
|
||||
else
|
||||
silverButton:Hide()
|
||||
goldButton:SetPoint("RIGHT", frameName.."SilverButton", "RIGHT", 0, 0)
|
||||
-- Silver, then copper: each either shows — padded, chained and counted — or hides and lets
|
||||
-- the coin above it collapse onto its edge. Silver showing under a `showSmallerCoins` type
|
||||
-- is what carries the exemption down to copper; copper also survives at zero for a
|
||||
-- "Backpack" type, and dies whatever it is once gold truncated it.
|
||||
local showSilver = silver > 0 or keepSmaller
|
||||
width = width + ChainCoin(goldCoin, silverCoin, frameName .. "SilverButton",
|
||||
showSilver, keepSmaller and info.fixedWidth, spacing)
|
||||
if showSilver and info.showSmallerCoins then
|
||||
keepSmaller = 1
|
||||
end
|
||||
|
||||
-- Used if we're not showing lower denominations
|
||||
if ( (copper > 0 or showLowerDenominations or info.showSmallerCoins == "Backpack") and not truncateCopper) then
|
||||
-- Exception if showLowerDenominations and fixedWidth
|
||||
if ( showLowerDenominations and info.fixedWidth ) then
|
||||
copperButton:SetWidth(COIN_BUTTON_WIDTH)
|
||||
end
|
||||
|
||||
width = width + copperButton:GetWidth()
|
||||
silverButton:SetPoint("RIGHT", frameName.."CopperButton", "LEFT", spacing, 0)
|
||||
if ( silverButton:IsVisible() ) then
|
||||
width = width - spacing
|
||||
end
|
||||
else
|
||||
copperButton:Hide()
|
||||
silverButton:SetPoint("RIGHT", frameName.."CopperButton", "RIGHT", 0, 0)
|
||||
end
|
||||
local showCopper = (copper > 0 or keepSmaller or info.showSmallerCoins == "Backpack")
|
||||
and not dropCopper
|
||||
width = width + ChainCoin(silverCoin, copperCoin, frameName .. "CopperButton",
|
||||
showCopper, keepSmaller and info.fixedWidth, spacing)
|
||||
|
||||
frame:SetWidth(width)
|
||||
end
|
||||
|
||||
-- l.272-346: a NO-OP in 1.12 — the reference's whole body is inside a `--[[ ]]` block, an
|
||||
-- earlier painter that MoneyFrame_Update replaced. Kept because the name is in the shipped
|
||||
-- client's `_G` and an addon that calls it gets exactly this there: nothing.
|
||||
-- A NO-OP, deliberately. The specification's whole body for this one sits inside a `--[[ ]]`
|
||||
-- block (ref l.272-346) — an earlier painter that MoneyFrame_Update replaced. The name is in
|
||||
-- the shipped client's `_G`, and an addon that calls it there gets exactly this: nothing.
|
||||
function RefreshMoneyFrame(frameName, money, small, collapse, showSmallerCoins)
|
||||
end
|
||||
|
||||
-- l.348-356, verbatim: recolour the DIGITS (the coin icons keep their own texture) — how the
|
||||
-- reference reddens a price you cannot afford.
|
||||
-- Recolour the DIGITS; the coin icons keep their own texture (ref l.348-356). This is how a
|
||||
-- price you cannot afford turns red.
|
||||
function SetMoneyFrameColor(frameName, r, g, b)
|
||||
local goldButton = getglobal(frameName.."GoldButton")
|
||||
local silverButton = getglobal(frameName.."SilverButton")
|
||||
local copperButton = getglobal(frameName.."CopperButton")
|
||||
local gold, silver, copper = MoneyFrameCoins(frameName)
|
||||
gold:SetTextColor(r, g, b)
|
||||
silver:SetTextColor(r, g, b)
|
||||
copper:SetTextColor(r, g, b)
|
||||
end
|
||||
|
||||
goldButton:SetTextColor(r, g, b)
|
||||
silverButton:SetTextColor(r, g, b)
|
||||
copperButton:SetTextColor(r, g, b)
|
||||
-- A click on a coin opens the pickup frame on that denomination, seeded with what the frame is
|
||||
-- currently showing; `denomination` is one coin's worth in copper, which is the unit the
|
||||
-- pickup frame counts in.
|
||||
--
|
||||
-- Guarded, because we ship no coin pickup (see the header) — so today the click does nothing,
|
||||
-- and the mouse-enabled state a `canPickup` type gives the coins is all that is observable.
|
||||
-- Note the amount is read by calling the type's own `UpdateFunc` while `this` is still the
|
||||
-- BUTTON rather than the frame, which is what the specification's own coin `<OnClick>` does
|
||||
-- (ref MoneyFrame.xml l.20-23) and what a frame-reading type would see through.
|
||||
function BenillaMoneyFrameCoin_OnClick(coin, denomination)
|
||||
if not OpenCoinPickupFrame then
|
||||
return
|
||||
end
|
||||
local frame = coin:GetParent()
|
||||
OpenCoinPickupFrame(denomination, MoneyTypeInfo[frame.moneyType].UpdateFunc(), frame)
|
||||
frame.hasPickup = 1
|
||||
end
|
||||
|
||||
-- Closing a money frame takes any pickup it opened down with it.
|
||||
function BenillaMoneyFrame_OnHide()
|
||||
if this.hasPickup == 1 and CoinPickupFrame then
|
||||
CoinPickupFrame:Hide()
|
||||
this.hasPickup = 0
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
@@ -393,9 +441,7 @@
|
||||
NumberFontNormalLarge digits. The three buttons chain RIGHT-to-LEFT from the copper end, which
|
||||
is what lets MoneyFrame_Update above re-point them past a hidden denomination.
|
||||
|
||||
The `<OnClick>` bodies are the reference's, guarded on `OpenCoinPickupFrame` — the coin-pickup
|
||||
frame is not transcribed (see the header). `<PushedTextOffset>` is carried for provenance; our
|
||||
loader does not model it and ignores it.
|
||||
`<PushedTextOffset>` is carried for provenance; our loader does not model it and ignores it.
|
||||
-->
|
||||
<Frame name="MoneyFrameTemplate" virtual="true">
|
||||
<Size><AbsDimension x="192" y="28"/></Size>
|
||||
@@ -406,13 +452,7 @@
|
||||
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local parent = this:GetParent()
|
||||
if ( OpenCoinPickupFrame ) then
|
||||
OpenCoinPickupFrame(1, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
|
||||
parent.hasPickup = 1
|
||||
end
|
||||
</OnClick>
|
||||
<OnClick>BenillaMoneyFrameCoin_OnClick(this, 1)</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
|
||||
<Size><AbsDimension x="19" y="19"/></Size>
|
||||
@@ -433,13 +473,7 @@
|
||||
<Anchor point="RIGHT" relativeTo="$parentCopperButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local parent = this:GetParent()
|
||||
if ( OpenCoinPickupFrame ) then
|
||||
OpenCoinPickupFrame(COPPER_PER_SILVER, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
|
||||
parent.hasPickup = 1
|
||||
end
|
||||
</OnClick>
|
||||
<OnClick>BenillaMoneyFrameCoin_OnClick(this, COPPER_PER_SILVER)</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
|
||||
<Size><AbsDimension x="19" y="19"/></Size>
|
||||
@@ -460,13 +494,7 @@
|
||||
<Anchor point="RIGHT" relativeTo="$parentSilverButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local parent = this:GetParent()
|
||||
if ( OpenCoinPickupFrame ) then
|
||||
OpenCoinPickupFrame(COPPER_PER_GOLD, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
|
||||
parent.hasPickup = 1
|
||||
end
|
||||
</OnClick>
|
||||
<OnClick>BenillaMoneyFrameCoin_OnClick(this, COPPER_PER_GOLD)</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
|
||||
<Size><AbsDimension x="19" y="19"/></Size>
|
||||
@@ -486,12 +514,7 @@
|
||||
<OnLoad>MoneyFrame_OnLoad()</OnLoad>
|
||||
<OnEvent>MoneyFrame_OnEvent()</OnEvent>
|
||||
<OnShow>MoneyFrame_UpdateMoney()</OnShow>
|
||||
<OnHide>
|
||||
if ( this.hasPickup == 1 and CoinPickupFrame ) then
|
||||
CoinPickupFrame:Hide()
|
||||
this.hasPickup = 0
|
||||
end
|
||||
</OnHide>
|
||||
<OnHide>BenillaMoneyFrame_OnHide()</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
@@ -510,13 +533,7 @@
|
||||
<Anchor point="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local parent = this:GetParent()
|
||||
if ( OpenCoinPickupFrame ) then
|
||||
OpenCoinPickupFrame(1, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
|
||||
parent.hasPickup = 1
|
||||
end
|
||||
</OnClick>
|
||||
<OnClick>BenillaMoneyFrameCoin_OnClick(this, 1)</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
|
||||
<Size><AbsDimension x="13" y="13"/></Size>
|
||||
@@ -537,13 +554,7 @@
|
||||
<Anchor point="RIGHT" relativeTo="$parentCopperButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local parent = this:GetParent()
|
||||
if ( OpenCoinPickupFrame ) then
|
||||
OpenCoinPickupFrame(COPPER_PER_SILVER, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
|
||||
parent.hasPickup = 1
|
||||
end
|
||||
</OnClick>
|
||||
<OnClick>BenillaMoneyFrameCoin_OnClick(this, COPPER_PER_SILVER)</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
|
||||
<Size><AbsDimension x="13" y="13"/></Size>
|
||||
@@ -564,13 +575,7 @@
|
||||
<Anchor point="RIGHT" relativeTo="$parentSilverButton" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local parent = this:GetParent()
|
||||
if ( OpenCoinPickupFrame ) then
|
||||
OpenCoinPickupFrame(COPPER_PER_GOLD, MoneyTypeInfo[parent.moneyType].UpdateFunc(), parent)
|
||||
parent.hasPickup = 1
|
||||
end
|
||||
</OnClick>
|
||||
<OnClick>BenillaMoneyFrameCoin_OnClick(this, COPPER_PER_GOLD)</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\MoneyFrame\UI-MoneyIcons">
|
||||
<Size><AbsDimension x="13" y="13"/></Size>
|
||||
@@ -590,12 +595,7 @@
|
||||
<OnLoad>SmallMoneyFrame_OnLoad()</OnLoad>
|
||||
<OnEvent>MoneyFrame_OnEvent()</OnEvent>
|
||||
<OnShow>MoneyFrame_UpdateMoney()</OnShow>
|
||||
<OnHide>
|
||||
if ( this.hasPickup == 1 and CoinPickupFrame ) then
|
||||
CoinPickupFrame:Hide()
|
||||
this.hasPickup = 0
|
||||
end
|
||||
</OnHide>
|
||||
<OnHide>BenillaMoneyFrame_OnHide()</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
the two states are authored rather than driven. The mixin port comes with the addon
|
||||
list. Width is era's text+40 law via an OnUpdate measure poll; the era's
|
||||
measure-at-the-medium-font quirk is deliberately dropped (~6 units of padding).
|
||||
- **Search WORKS (0984)** — the era SettingsPanel mechanism transcribed (the Script
|
||||
- **Search WORKS (0984)** — the era SettingsPanel mechanism re-expressed (the Script
|
||||
block's search section): label-tag substring match scored by matched length,
|
||||
results grouped under clickable category heads ordered by best score, a matched
|
||||
child pulls its parent row in, "Search Results" title with Defaults hidden while
|
||||
@@ -158,7 +158,7 @@
|
||||
page went LIVE in 0992 (the three UnitName* checkbox rows).
|
||||
- **The window rides a scale knob** (slice B): fonts ride frame scale engine-wide now
|
||||
(the 0219 §2 divergence closed), so ERA_WINDOW_SCALE × the era checkFit shrink
|
||||
(UIPanelUpdateScaleForFit, transcribed in this file) sizes the whole window; 0.78 is
|
||||
(UIPanelUpdateScaleForFit, re-expressed in this file) sizes the whole window; 0.78 is
|
||||
the director's-eye starting point, live-tunable. The knob is SHARED: it stands in for
|
||||
the era client's UIParent scale (smaller than our 1.12 768-canvas on modern screens),
|
||||
so every era-shaped window wears it — GameMenuFrame.xml reads it in its OnShow.
|
||||
@@ -207,7 +207,7 @@
|
||||
function OptionsFrame_UpdateScale()
|
||||
-- The era panel is registered `checkFit = 1`: the panel manager also shrinks it to
|
||||
-- fit a small screen (UIPanelUpdateScaleForFit: min over width/height ratios
|
||||
-- against the panel + 20 units of air each way, capped at 1) — transcribed here.
|
||||
-- against the panel + 20 units of air each way, capped at 1) — re-expressed here.
|
||||
local k = ERA_WINDOW_SCALE
|
||||
local wFit = GetScreenWidth() / 940
|
||||
local hFit = GetScreenHeight() / 744
|
||||
@@ -248,7 +248,7 @@
|
||||
bg:SetVertexColor(0.196, 0.388, 0.8)
|
||||
end
|
||||
|
||||
-- The selection — 1.14's OptionsList_SelectButton/_ClearSelection pair, verbatim
|
||||
-- The selection — 1.14's OptionsList_SelectButton/_ClearSelection pair, re-expressed
|
||||
-- (OptionsFrameTemplates.lua l.107-121): the selected row's wash goes GOLD (1,1,0)
|
||||
-- and locks on; every other row drops back to the blue hover tint, wash off. The
|
||||
-- white selected label is LockHighlight's own text state, carried by the font swap.
|
||||
@@ -308,7 +308,7 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Search (0984): the era SettingsPanel mechanism, transcribed ─────────────────────
|
||||
-- ── Search (0984): the era SettingsPanel mechanism, re-expressed ────────────────────
|
||||
-- Blizzard_SettingsPanel.lua l.566-692 + Blizzard_Settings.lua l.65-77, cut to our
|
||||
-- static rows: a setting's search tag is its LABEL uppercased (the era's default tag
|
||||
-- is exactly the setting name — SettingControls.lua l.773); a query matches by plain
|
||||
@@ -953,7 +953,7 @@
|
||||
-- would read as moved-off-"0.4").
|
||||
local str = string.gsub(string.format("%.2f", value), "%.?0+$", "")
|
||||
if row.deferred then
|
||||
-- era CommitFlag.Apply, transcribed (Blizzard_Setting.lua's staged
|
||||
-- era CommitFlag.Apply, re-expressed (Blizzard_Setting.lua's staged
|
||||
-- SetValue half): the control shows the new value, the ENGINE keeps the
|
||||
-- old one, and the CVar moves only when Apply commits — dragging a
|
||||
-- slider that rescales itself mid-drag is exactly what this flag
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The reference's shared OPTIONS widget kit — Interface\FrameXML\OptionsFrameTemplates.xml,
|
||||
transcribed from the extracted 1.12.1 file (line numbers below are that file's), plus the
|
||||
The shared OPTIONS widget kit — our build of the five templates addons name, specified by
|
||||
Interface\FrameXML\OptionsFrameTemplates.xml (line numbers below are that file's) plus the
|
||||
one template from UIOptionsFrame.xml that addons actually name.
|
||||
|
||||
These are UIPanelTemplates.xml's descendants: `OptionsCheckButtonTemplate` inherits
|
||||
@@ -11,17 +11,56 @@
|
||||
WHY THE UIOptionsFrame ONE LIVES HERE. `UIOptionsCheckButtonTemplate` is 8 `CreateFrame`
|
||||
call sites in the corpus — the fourth most-wanted template we lacked — and in the reference
|
||||
it is declared in UIOptionsFrame.xml (l.6-10) as a two-line size override on
|
||||
`OptionsCheckButtonTemplate`. We do not transcribe UIOptionsFrame (benilla's own options
|
||||
`OptionsCheckButtonTemplate`. We do not build UIOptionsFrame at all (benilla's own options
|
||||
window is OptionsFrame.xml, a different design), so the one template that file contributes
|
||||
to the shared namespace sits with its parent rather than in a file of its own. Its
|
||||
provenance is on the declaration.
|
||||
|
||||
Every name here is in `reference/1.12-globals.tsv`; none is invented. What this supersedes
|
||||
but does not delete: nothing — OptionsFrame.xml's own `Options*Template` family
|
||||
(`OptionsRedButtonTemplate`, `OptionsCheckboxRowTemplate`, `OptionsSliderRowTemplate`, …)
|
||||
are benilla's row widgets for benilla's window, distinct names and a distinct shape.
|
||||
Every TEMPLATE name here is in `reference/1.12-globals.tsv`; none is invented. The one name
|
||||
that is ours is the hover helper below, and it wears the `Benilla` prefix that says so —
|
||||
the reference open-codes that hover twice, once per widget, and two copies of one behaviour
|
||||
is how they end up disagreeing. What this supersedes but does not delete: nothing —
|
||||
OptionsFrame.xml's own `Options*Template` family (`OptionsRedButtonTemplate`,
|
||||
`OptionsCheckboxRowTemplate`, `OptionsSliderRowTemplate`, …) are benilla's row widgets for
|
||||
benilla's window, distinct names and a distinct shape.
|
||||
-->
|
||||
|
||||
<Script><![CDATA[
|
||||
-- The hover both widgets below share: an instance carries its own `tooltipText` (the
|
||||
-- caption, wrapped) and optionally a `tooltipRequirement` (the "needs a restart / a relog"
|
||||
-- line under it) as plain fields, set by whoever built the panel. Neither field is
|
||||
-- required, and a widget with neither raises nothing.
|
||||
--
|
||||
-- BOTH LINES RENDER GOLD, and the second one's colour arguments are why that has to be
|
||||
-- said out loud: `AddLine(text, "", 1, 1, 1)` is 1.12's archaic spelling, and the empty
|
||||
-- string sitting in the r-slot is what makes the binding drop the whole colour tail and
|
||||
-- fall back to the tooltip default (`script/tooltip/verbs.rs`'s AddLine carries the byte
|
||||
-- note, and names this exact `(text, "", r, g, b)` shape as the corpus' own). The
|
||||
-- arguments are kept because they are the reference's; the white they appear to ask for is
|
||||
-- not what any client, ours or theirs, actually draws.
|
||||
--
|
||||
-- One home for it rather than one per template: the checkbox and the slider are different
|
||||
-- widget types with no common ancestor to inherit from, so sharing the behaviour means
|
||||
-- sharing a function. It takes the widget as an argument rather than reading `this`,
|
||||
-- because a shared function has no handler of its own to read it from — each of the two
|
||||
-- OnEnters below passes its own.
|
||||
function BenillaOptionsWidget_ShowTooltip(widget)
|
||||
local caption, requirement = widget.tooltipText, widget.tooltipRequirement
|
||||
if caption then
|
||||
GameTooltip:SetOwner(widget, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(caption, nil, nil, nil, nil, 1)
|
||||
end
|
||||
-- Only the requirement line asks for the tooltip to be shown, and that is the
|
||||
-- reference's behaviour, not an oversight of ours: `SetText` already puts the plate on
|
||||
-- screen, so the caption case needs no second verb, while `AddLine` onto a plate that
|
||||
-- is already up has to ask for the relayout.
|
||||
if requirement then
|
||||
GameTooltip:AddLine(requirement, "", 1.0, 1.0, 1.0)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
<!-- ref l.3-34. The titled group box: a tooltip-border backdrop with its caption hung above the
|
||||
top-left corner, greyed by its own OnLoad. -->
|
||||
<Frame name="OptionFrameBoxTemplate" virtual="true">
|
||||
@@ -56,23 +95,11 @@
|
||||
<CheckButton name="OptionsCheckButtonTemplate" inherits="UICheckButtonTemplate" virtual="true">
|
||||
<HitRectInsets><AbsInset left="0" right="-100" top="0" bottom="0"/></HitRectInsets>
|
||||
<Scripts>
|
||||
<!-- The click kit is the state the box has just reached, not the one it left. -->
|
||||
<OnClick>
|
||||
if (this:GetChecked()) then
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
else
|
||||
PlaySound("igMainMenuOptionCheckBoxOff")
|
||||
end
|
||||
PlaySound(this:GetChecked() and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff")
|
||||
</OnClick>
|
||||
<OnEnter>
|
||||
if (this.tooltipText) then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(this.tooltipText, nil, nil, nil, nil, 1)
|
||||
end
|
||||
if (this.tooltipRequirement) then
|
||||
GameTooltip:AddLine(this.tooltipRequirement, "", 1.0, 1.0, 1.0)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
</OnEnter>
|
||||
<OnEnter>BenillaOptionsWidget_ShowTooltip(this)</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
@@ -108,16 +135,7 @@
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnEnter>
|
||||
if (this.tooltipText) then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(this.tooltipText, nil, nil, nil, nil, 1)
|
||||
end
|
||||
if (this.tooltipRequirement) then
|
||||
GameTooltip:AddLine(this.tooltipRequirement, "", 1.0, 1.0, 1.0)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
</OnEnter>
|
||||
<OnEnter>BenillaOptionsWidget_ShowTooltip(this)</OnEnter>
|
||||
<OnLeave>GameTooltip:Hide()</OnLeave>
|
||||
</Scripts>
|
||||
<ThumbTexture name="$parentThumb" file="Interface\Buttons\UI-SliderBar-Button-Horizontal">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The party member frames + the group-invite popup (decision 0434 phase 3). Geometry, art
|
||||
paths, behavior laws, and the popup entry are QUOTED from the reference client's own
|
||||
FrameXML, extracted through the full MPQ patch chain (decision 0246; survey 2026-07-17):
|
||||
paths and behavior laws are MEASURED against the reference client's own FrameXML, extracted
|
||||
through the full MPQ patch chain (decision 0246; survey 2026-07-17):
|
||||
PartyFrame.xml, PartyFrameTemplates.xml, PartyMemberFrame.lua, StaticPopup.lua ("PARTY_INVITE"),
|
||||
UIParent.lua (the PARTY_INVITE_REQUEST wiring). Data flow is ours: the app feeds
|
||||
UIParent.lua (the PARTY_INVITE_REQUEST wiring) — the code below is our own. Data flow is
|
||||
ours too: the app feeds
|
||||
party1..party4 unit snapshots + the PartyState roster (crates/benilla/src/ui_party.rs) and
|
||||
fires the PARTY_* events; the popup's Accept/Decline queue PartyRequests the app drains.
|
||||
|
||||
@@ -26,7 +27,7 @@
|
||||
DOUBLE-DECLINE, KEPT: the ref's popup calls DeclineGroup() in OnCancel AND again in OnHide
|
||||
(only OnAccept sets the inviteAccepted guard), so a clicked Decline sends two
|
||||
CMSG_GROUP_DECLINE. vmangos silently ignores the second (no pending invite -> early
|
||||
return). Transcribed verbatim rather than "fixed" - the wire tolerates it and the guard's
|
||||
return). Matched rather than "fixed" - the wire tolerates it and the guard's
|
||||
real job is the timeout/override/escape paths.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
@@ -52,32 +53,59 @@
|
||||
NUM_RAID_GROUPS = 8
|
||||
MEMBERS_PER_RAID_GROUP = 5
|
||||
|
||||
-- StaticPopup entry, quoted from the ref's StaticPopup.lua (INVITATION =
|
||||
-- "%s invites you to a group.", GlobalStrings:2586-area; sound + 60 s silent timeout +
|
||||
-- whileDead + hideOnEscape are the ref's own fields).
|
||||
-- ── The group-invite popup ────────────────────────────────────────────────────────────
|
||||
-- The strings and the three flags are the reference's own (INVITATION = "%s invites you to
|
||||
-- a group.", GlobalStrings:2586-area; a 60 s silent timeout, answerable while dead,
|
||||
-- closable with ESC).
|
||||
--
|
||||
-- The whole of its behaviour turns on ONE latch, `inviteAccepted`, and the four hooks below
|
||||
-- are just the four moments that read or write it. It lives on the registry entry rather
|
||||
-- than in an upvalue here because that is where the reference keeps it and an addon
|
||||
-- inspecting the dialog expects to find it there — which is why each hook goes back through
|
||||
-- `StaticPopupDialogs["PARTY_INVITE"]` instead of closing over a local.
|
||||
--
|
||||
-- They are named rather than inlined so the entry below reads as the manifest it is: four
|
||||
-- strings, three flags, four hooks.
|
||||
|
||||
-- Every showing starts unanswered — including a re-show of the same entry, which is what a
|
||||
-- second invite arriving while the first is up does.
|
||||
local function PartyInvite_OnShow()
|
||||
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = nil
|
||||
end
|
||||
|
||||
-- The only path that answers the server affirmatively, and the only one that latches.
|
||||
local function PartyInvite_OnAccept()
|
||||
AcceptGroup()
|
||||
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = 1
|
||||
end
|
||||
|
||||
local function PartyInvite_OnCancel()
|
||||
DeclineGroup()
|
||||
end
|
||||
|
||||
-- The catch-all. EVERY way this dialog leaves the screen lands here — Decline, the 60 s
|
||||
-- timeout, ESC, and another dialog overriding it — and the latch is what tells the one
|
||||
-- that already said yes from the four that never answered at all. A clicked Decline
|
||||
-- therefore declines twice (OnCancel, then here); see DOUBLE-DECLINE in the header, which
|
||||
-- is the reference's behaviour and the wire tolerates it.
|
||||
local function PartyInvite_OnHide()
|
||||
if not StaticPopupDialogs["PARTY_INVITE"].inviteAccepted then
|
||||
DeclineGroup()
|
||||
end
|
||||
end
|
||||
|
||||
StaticPopupDialogs["PARTY_INVITE"] = {
|
||||
text = INVITATION,
|
||||
button1 = ACCEPT,
|
||||
button2 = DECLINE,
|
||||
sound = "igPlayerInvite",
|
||||
OnShow = function()
|
||||
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = nil
|
||||
end,
|
||||
OnAccept = function()
|
||||
AcceptGroup()
|
||||
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = 1
|
||||
end,
|
||||
OnCancel = function()
|
||||
DeclineGroup()
|
||||
end,
|
||||
OnHide = function()
|
||||
if not StaticPopupDialogs["PARTY_INVITE"].inviteAccepted then
|
||||
DeclineGroup()
|
||||
end
|
||||
end,
|
||||
timeout = 60,
|
||||
whileDead = 1,
|
||||
hideOnEscape = 1,
|
||||
OnShow = PartyInvite_OnShow,
|
||||
OnAccept = PartyInvite_OnAccept,
|
||||
OnCancel = PartyInvite_OnCancel,
|
||||
OnHide = PartyInvite_OnHide,
|
||||
}
|
||||
|
||||
-- ref PartyMemberFrame.lua's OnLoad: cache children, register, first paint.
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
Token slots (the commands and reactions) return the NAMES OF GLOBALS rather than values,
|
||||
which is the reference's own convention (`PetActionBarFrame.lua:98-104`): the name keys are
|
||||
GlobalStrings' (PET_ACTION_ATTACK, PET_MODE_DEFENSIVE, …), and the texture globals are
|
||||
declared below, quoted verbatim from `PetActionBarFrame.lua:6-12`.
|
||||
declared below. Both halves of that mapping are a contract we do not get to choose — the
|
||||
names are what `ui_pet/bar.rs` hands back and the icons are real files in the install — so
|
||||
the specification for them is `PetActionBarFrame.lua:6-12`; see the declaration's own note
|
||||
for what IS ours about the layout.
|
||||
|
||||
Geometry QUOTED from the ref (PetActionBarFrame.xml): frame 509x43; the two UI-PetBar shelf
|
||||
strips (256x44 at TOPLEFT with TexCoords 0..1 / 0.015625..0.359375, then 184x44 to its
|
||||
@@ -61,16 +64,31 @@
|
||||
<Script><![CDATA[
|
||||
NUM_PET_ACTION_SLOTS = 10
|
||||
|
||||
-- The token textures, quoted verbatim from ref PetActionBarFrame.lua:6-12. They are
|
||||
-- GLOBALS on purpose: `GetPetActionInfo` returns the NAME of one for a token slot, and the
|
||||
-- paint below resolves it with getglobal, exactly as the reference does.
|
||||
-- ── The token icons ───────────────────────────────────────────────────────────────────
|
||||
--
|
||||
-- These are GLOBALS on purpose, and the names are a contract, not a choice:
|
||||
-- `GetPetActionInfo` hands back the NAME of one of them for a token slot and the paint
|
||||
-- below resolves it with getglobal — the reference's own convention, and the one
|
||||
-- `ui_pet/bar.rs` writes against. The icon paths are the reference's too; they name real
|
||||
-- .blp files in the install, so there is nothing to choose there either.
|
||||
--
|
||||
-- What IS ours is the grouping. The bar's tokens come in two families and `ui_pet/bar.rs`
|
||||
-- already separates them at the seam that names these — `command_token` (the orders you
|
||||
-- give, keyed `PET_ACTION_*`) and `reaction_token` (the stance the pet holds, keyed
|
||||
-- `PET_MODE_*`) — so they are laid out in those two families, in that file's own order,
|
||||
-- and the two halves of the mapping read side by side. The reference keeps them in one
|
||||
-- undifferentiated block, which hides the split.
|
||||
|
||||
-- Commands — the four orders, in `ui_pet/bar.rs`'s `command_token` order.
|
||||
PET_WAIT_TEXTURE = "Interface\\Icons\\Spell_Nature_TimeStop"
|
||||
PET_FOLLOW_TEXTURE = "Interface\\Icons\\Ability_Tracking"
|
||||
PET_ATTACK_TEXTURE = "Interface\\Icons\\Ability_GhoulFrenzy"
|
||||
PET_DISMISS_TEXTURE = "Interface\\Icons\\Spell_Shadow_Teleport"
|
||||
|
||||
-- Reactions — the three stances, in `ui_pet/bar.rs`'s `reaction_token` order.
|
||||
PET_PASSIVE_TEXTURE = "Interface\\Icons\\Ability_Seal"
|
||||
PET_DEFENSIVE_TEXTURE = "Interface\\Icons\\Ability_Defend"
|
||||
PET_AGGRESSIVE_TEXTURE = "Interface\\Icons\\Ability_Racial_BloodRage"
|
||||
PET_PASSIVE_TEXTURE = "Interface\\Icons\\Ability_Seal"
|
||||
PET_ATTACK_TEXTURE = "Interface\\Icons\\Ability_GhoulFrenzy"
|
||||
PET_FOLLOW_TEXTURE = "Interface\\Icons\\Ability_Tracking"
|
||||
PET_WAIT_TEXTURE = "Interface\\Icons\\Spell_Nature_TimeStop"
|
||||
PET_DISMISS_TEXTURE = "Interface\\Icons\\Spell_Shadow_Teleport"
|
||||
|
||||
-- ── The autocast sparkle ──────────────────────────────────────────────────────────────
|
||||
--
|
||||
|
||||
@@ -44,14 +44,14 @@
|
||||
machinery does.
|
||||
- `PET_UI_UPDATE` / `PET_UI_CLOSE` are **registered but nothing fires them yet** (nothing
|
||||
in the app sources either event). Registered anyway rather than dropped: the ref's own
|
||||
handler arms are transcribed whole, and `UNIT_PET` + `PET_BAR_UPDATE` — both of which we
|
||||
handler arms are re-expressed whole, and `UNIT_PET` + `PET_BAR_UPDATE` — both of which we
|
||||
DO fire — already cover the same "the pet went away while its page was open" case
|
||||
through the identical branch.
|
||||
- **CLOSED (decision 1062):** the level line's family word and the diet tooltip both
|
||||
landed. `UnitCreatureFamily("pet")` and `GetPetFoodTypes()` are real bindings now, fed
|
||||
from `CreatureFamily.dbc` (the word + its pet-food mask) and `ItemPetFood.dbc` (the
|
||||
eight diet names) through the pet's cached creature template; `BuildListString` is the
|
||||
ref's own `UIParent.lua:1051` joiner, transcribed in `UIParent.xml`. Both call sites
|
||||
ref's own `UIParent.lua:1051` joiner, re-expressed in `UIParent.xml`. Both call sites
|
||||
below dropped their existence guards. The ref's OWN `if ( UnitCreatureFamily("pet") )`
|
||||
guard stays — nil is still reachable (a template with no family; the creature query not
|
||||
yet answered) and a blank level line is the ref's answer to it.
|
||||
@@ -181,15 +181,32 @@
|
||||
end
|
||||
getglobal("PetLoyaltyText"):SetText(GetPetLoyalty())
|
||||
PetExpBar_Update()
|
||||
|
||||
-- The two panes this page has to draw itself, for two DIFFERENT reasons (both below):
|
||||
-- the resistance column genuinely differs from the character page's, while the stat
|
||||
-- block is the same logic that only duplicates because PetStatFrame/CharacterStatFrame
|
||||
-- share no prefix for a `prefix..` renderer to take.
|
||||
PetPaperDollFrame_SetResistances()
|
||||
PetPaperDollFrame_SetStats()
|
||||
PaperDollFrame_SetDamage("pet", "Pet")
|
||||
PaperDollFrame_SetRangedDamage("pet", "Pet")
|
||||
PaperDollFrame_SetAttackPower("pet", "Pet")
|
||||
PaperDollFrame_SetRangedAttackPower("pet", "Pet")
|
||||
PaperDollFrame_SetArmor("pet", "Pet")
|
||||
|
||||
-- ...and then the shared rows, which are not this page's code at all: every one of
|
||||
-- these is CharacterFrame.xml's own renderer, handed ("pet", "Pet") where the character
|
||||
-- page hands it ("player", "Character"), so `prefix.."ArmorFrame"` resolves PetArmorFrame
|
||||
-- here and CharacterArmorFrame there. That substitution IS the page (header comment).
|
||||
-- Called top-to-bottom in the order the rows are anchored on screen — each renderer only
|
||||
-- writes its own row's FontStrings from live API state, so nothing here reads anything
|
||||
-- another one wrote and the order is free.
|
||||
PaperDollFrame_SetAttackBothHands("pet", "Pet")
|
||||
PaperDollFrame_SetAttackPower("pet", "Pet")
|
||||
PaperDollFrame_SetDamage("pet", "Pet")
|
||||
PaperDollFrame_SetDefense("pet", "Pet")
|
||||
PaperDollFrame_SetArmor("pet", "Pet")
|
||||
-- The pet sheet has no ranged rows at all, and these two say so themselves — both open
|
||||
-- with the reference's own `if unit == "pet" then return end` (CharacterFrame.xml). They
|
||||
-- are called anyway so that both pages drive the identical set of renderers and neither
|
||||
-- can quietly fall behind the other.
|
||||
PaperDollFrame_SetRangedDamage("pet", "Pet")
|
||||
PaperDollFrame_SetRangedAttackPower("pet", "Pet")
|
||||
|
||||
-- The hunter-only block (ref l.83-93): the diet icon and the training-point line are
|
||||
-- the two things a warlock's minion never has. `canGainXP` is HasPetUI's second return.
|
||||
@@ -210,7 +227,7 @@
|
||||
-- ref PetPaperDollFrame_SetResistances (l.96-137). Deliberately NOT the character page's
|
||||
-- version: the pet's has no resistance-quality subtext (no `RESISTANCE_TOOLTIP_SUBTEXT`
|
||||
-- line at all) and colours the NUMBER off its own sign rather than off pos-vs-neg
|
||||
-- magnitude — two real differences in the reference, transcribed rather than unified.
|
||||
-- magnitude — two real differences in the reference, kept rather than unified.
|
||||
function PetPaperDollFrame_SetResistances()
|
||||
for i = 1, BENILLA_NUM_PET_RESISTANCE_TYPES do
|
||||
local resistId = BENILLA_MAGICRES_IDS[i]
|
||||
@@ -490,7 +507,7 @@
|
||||
<OnEnter>
|
||||
-- ref l.267-270, unguarded now that both halves exist (decision 1062):
|
||||
-- `GetPetFoodTypes` is a real binding and `BuildListString` is the ref's
|
||||
-- own UIParent.lua joiner, transcribed in UIParent.xml.
|
||||
-- own UIParent.lua joiner, re-expressed in UIParent.xml.
|
||||
--
|
||||
-- `BuildListString()` returns NIL on an empty list and `format`'s "%s"
|
||||
-- would error on it, so the pairing matters. Two things keep it safe, and
|
||||
@@ -600,7 +617,7 @@
|
||||
<OnLoad>self:EnableMouse(true)</OnLoad>
|
||||
<!-- ref l.429-433: the pet's attack row shows the BARE tooltip title, with
|
||||
no ATTACK_TOOLTIP_SUBTEXT line — the character page's own row adds it.
|
||||
Transcribed as the ref has it. -->
|
||||
Kept as the ref has it. -->
|
||||
<OnEnter>
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(ATTACK_TOOLTIP, 1.0, 1.0, 1.0)
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
— a quest with many objectives/a long description/many rewards scrolls and clips at the
|
||||
pane's own edge instead of running past the window's bottom (pin §3's doNotScroll
|
||||
asymmetry is implemented — see `BenillaQuestLogDetail_Update`'s own comment).
|
||||
- Difficulty coloring is the ref's own `QuestDifficultyColor` table + `GetDifficultyColor`
|
||||
(QUOTED, l.14-20 + l.584-599) over the real `GetQuestGreenRange()` binding — see the
|
||||
function's own comment. (The old `BenillaQuestDifficultyColor` INTERIM and its
|
||||
approximated thresholds are retired.)
|
||||
- Difficulty coloring wears the reference's names and bands (`QuestDifficultyColor` +
|
||||
`GetDifficultyColor`, specified at l.14-20 + l.584-599) over the real
|
||||
`GetQuestGreenRange()` binding — see the function's own comment. (The old
|
||||
`BenillaQuestDifficultyColor` INTERIM and its approximated thresholds are retired.)
|
||||
- No headers ship (`benilla-ui`'s `quest_log.rs`: flat entries only, headers need the
|
||||
QuestSort/AreaTable DBC join) — the header branch below is dead code today, kept for when
|
||||
that join lands rather than re-derived then.
|
||||
@@ -124,37 +124,41 @@
|
||||
local detailLastAppliedHeight = nil
|
||||
|
||||
-- ── Difficulty coloring ──────────────────────────────────────────────────────────────────
|
||||
-- QUOTED from ref-QuestLogFrame.lua: the table (l.14-20) and GetDifficultyColor (l.584-599,
|
||||
-- "Used for quests and enemy coloration" — the ref's own comment; the TARGET FRAME's level
|
||||
-- number colors through this exact function too, TargetFrame_CheckLevel l.130). The
|
||||
-- green→grey boundary is the real GetQuestGreenRange() binding (benilla-ui unit.rs — the
|
||||
-- byte-verified grey band the plate/tooltip share; the binding's own byte read is under
|
||||
-- the 2026-07-17 wow-re §5). This retires the BenillaQuestDifficultyColor INTERIM and its
|
||||
-- approximated -2/-10 thresholds. One deviation: the ref assigns its scratch `color` as a
|
||||
-- GLOBAL (vanilla Lua sloppiness); ours is local.
|
||||
QuestDifficultyColor = { }
|
||||
QuestDifficultyColor["impossible"] = { r = 1.00, g = 0.10, b = 0.10 }
|
||||
QuestDifficultyColor["verydifficult"] = { r = 1.00, g = 0.50, b = 0.25 }
|
||||
QuestDifficultyColor["difficult"] = { r = 1.00, g = 1.00, b = 0.00 }
|
||||
QuestDifficultyColor["standard"] = { r = 0.25, g = 0.75, b = 0.25 }
|
||||
QuestDifficultyColor["trivial"] = { r = 0.50, g = 0.50, b = 0.50 }
|
||||
QuestDifficultyColor["header"] = { r = 0.7, g = 0.7, b = 0.7 }
|
||||
-- The six bands and their exact colors are the reference's contract, not a palette of ours:
|
||||
-- ref-QuestLogFrame.lua's table (l.14-20) and GetDifficultyColor (l.584-599, "Used for
|
||||
-- quests and enemy coloration" — the ref's own comment; the TARGET FRAME's level number
|
||||
-- colors through this same function, TargetFrame_CheckLevel l.130), and `QuestDifficultyColor`
|
||||
-- is a name addons read, so both the global and its six keys are fixed. The green→grey
|
||||
-- boundary is the real GetQuestGreenRange() binding (benilla-ui unit.rs — the byte-verified
|
||||
-- grey band the plate/tooltip share; the binding's own byte read is under the 2026-07-17
|
||||
-- wow-re §5). This retires the BenillaQuestDifficultyColor INTERIM and its approximated
|
||||
-- -2/-10 thresholds.
|
||||
QuestDifficultyColor = {
|
||||
impossible = { r = 1.00, g = 0.10, b = 0.10 },
|
||||
verydifficult = { r = 1.00, g = 0.50, b = 0.25 },
|
||||
difficult = { r = 1.00, g = 1.00, b = 0.00 },
|
||||
standard = { r = 0.25, g = 0.75, b = 0.25 },
|
||||
trivial = { r = 0.50, g = 0.50, b = 0.50 },
|
||||
header = { r = 0.70, g = 0.70, b = 0.70 },
|
||||
}
|
||||
|
||||
-- How far ABOVE the player the quest sits picks the band. Note the shape of the last two
|
||||
-- tests: the three hard bands are fixed offsets, but the green/grey boundary is not a
|
||||
-- constant at all — GetQuestGreenRange() widens as the player levels — which is why it is
|
||||
-- asked rather than compared against a number, and why "trivial" is what is left over
|
||||
-- rather than a band with an edge of its own.
|
||||
function GetDifficultyColor(level)
|
||||
local color
|
||||
local levelDiff = level - UnitLevel("player")
|
||||
if ( levelDiff >= 5 ) then
|
||||
color = QuestDifficultyColor["impossible"]
|
||||
elseif ( levelDiff >= 3 ) then
|
||||
color = QuestDifficultyColor["verydifficult"]
|
||||
elseif ( levelDiff >= -2 ) then
|
||||
color = QuestDifficultyColor["difficult"]
|
||||
elseif ( -levelDiff <= GetQuestGreenRange() ) then
|
||||
color = QuestDifficultyColor["standard"]
|
||||
else
|
||||
color = QuestDifficultyColor["trivial"]
|
||||
if levelDiff >= 5 then
|
||||
return QuestDifficultyColor.impossible
|
||||
elseif levelDiff >= 3 then
|
||||
return QuestDifficultyColor.verydifficult
|
||||
elseif levelDiff >= -2 then
|
||||
return QuestDifficultyColor.difficult
|
||||
elseif -levelDiff <= GetQuestGreenRange() then
|
||||
return QuestDifficultyColor.standard
|
||||
end
|
||||
return color
|
||||
return QuestDifficultyColor.trivial
|
||||
end
|
||||
|
||||
-- ── The list page ────────────────────────────────────────────────────────────────────────
|
||||
@@ -495,7 +499,7 @@
|
||||
BenillaQuestLogDetail_ResizeChild(detailObjAnchorIndex, detailRewardHeight)
|
||||
end
|
||||
|
||||
-- ref QuestLog_OnUpdate (QuestLogFrame.lua:96-105), transcribed 1:1 including its
|
||||
-- ref QuestLog_OnUpdate (QuestLogFrame.lua:96-105), re-expressed here including its
|
||||
-- UPDATE_DELAY throttle: the countdown is repainted ten times a second, not every frame —
|
||||
-- SecondsToTime's output only changes once a second, so anything faster is a re-measure of
|
||||
-- an identical string. The flag/accumulator pair is the ref's own (its two fields on
|
||||
@@ -616,7 +620,7 @@
|
||||
--
|
||||
-- Guard-for-guard with the ref: ctrl is gated on `this.rewardType ~= "spell"` (l.544), and
|
||||
-- so is shift — but on the ELSEIF line itself (l.547), where the giver window's twin puts it
|
||||
-- inside (QuestFrame.lua:121). Same outcome, and transcribed where the ref puts it. Ours
|
||||
-- inside (QuestFrame.lua:121). Same outcome, and kept where the ref puts it. Ours
|
||||
-- adds `ChatFrameEditBox and` (ChatFrame.xml is a separate file a window test need not
|
||||
-- load — BenillaPaperDollSlot_OnClick's precedent) and a nil-link guard: `link` is nil until
|
||||
-- the ask-once item template lands, a state the reference's synchronous item cache never
|
||||
@@ -634,7 +638,7 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- The reward-panel layout, ported verbatim from the ref's shared `QuestFrameItems_Update`
|
||||
-- The reward-panel layout, ported from the ref's shared `QuestFrameItems_Update`
|
||||
-- (QuestFrame.lua:311-522, the same routine the live QuestFrame reward panel uses) — this
|
||||
-- window's v1 subset of it: no spell-reward row (GetQuestLogRewardSpell has no v1 caller,
|
||||
-- QuestLogFrame.xml's header comment), capped at BENILLA_QUESTLOG_MAX_ITEMS (6, half the ref's
|
||||
@@ -792,7 +796,7 @@
|
||||
local description, objectivesText = GetQuestLogQuestText()
|
||||
getglobal("QuestLogObjectivesText"):SetText(objectivesText or "")
|
||||
|
||||
-- pin §3, QuestLog_UpdateQuestDetails (QuestLogFrame.lua:364-374) verbatim, minus the
|
||||
-- pin §3, QuestLog_UpdateQuestDetails (QuestLogFrame.lua:364-374) followed, minus the
|
||||
-- Show/Hide our region model has no verb for: a timed quest gets the "Time Remaining:"
|
||||
-- row and pushes Objective1 below it; an untimed one blanks the row (our stand-in for
|
||||
-- the ref's :Hide()) and anchors Objective1 straight onto ObjectivesText, so the row
|
||||
@@ -845,7 +849,7 @@
|
||||
-- down by however much dead space the unused tail slots add (this window's own comment
|
||||
-- on the FontString). No RequiredMoney branch here (this file's documented v1 gap: no
|
||||
-- widget backs GetQuestLogRequiredMoney yet) — that ref branch collapses into the rest.
|
||||
-- The objective-less TIMED branch (ref :432-437) is transcribed: a timed quest with no
|
||||
-- The objective-less TIMED branch (ref :432-437) is kept: a timed quest with no
|
||||
-- leaderboard lines hangs the header off the countdown row, not off ObjectivesText.
|
||||
local objAnchorIndex = math.min(numObjectives, MAX_OBJECTIVES)
|
||||
local descTitle = getglobal("QuestLogDescriptionTitle")
|
||||
@@ -865,7 +869,7 @@
|
||||
end
|
||||
|
||||
-- ── The always-visible quest-watch tracker (QuestWatchFrame) ─────────────────────────
|
||||
-- Ref QuestWatch_Update (QuestLogFrame.lua:602-663), transcribed onto the flat 30-line pool
|
||||
-- Ref QuestWatch_Update (QuestLogFrame.lua:602-663), re-expressed onto the flat 30-line pool
|
||||
-- (the frame's own XML comment): every paint re-anchors each used line — a quest TITLE drops
|
||||
-- -5 below the previous line (ref -4 under its 13px boxes; ours are measured 12s, same
|
||||
-- pitch), an OBJECTIVE drops -1 (ref 0) — writes " - "-prefixed objective rows colored by
|
||||
@@ -1114,11 +1118,13 @@
|
||||
HideUIPanel(getglobal("QuestLogFrame"))
|
||||
end
|
||||
|
||||
-- ── The abandon two-step (mark on click → confirm on Yes) — the ref's ABANDON_QUEST
|
||||
-- entry on the shared StaticPopup engine (UiPanels.xml, decision 0308 §3), verbatim from
|
||||
-- StaticPopup.lua:749-761: ABANDON_QUEST_CONFIRM 'Abandon "%s"?' (GlobalStrings.lua:5),
|
||||
-- OnAccept = AbandonQuest() + the igQuestLogAbandonQuest kit. exclusive is v1-inert
|
||||
-- (engine header note). ──────────────────────────────────────────────────────────────────
|
||||
-- ── The abandon two-step (mark on click → confirm on Yes) — our `ABANDON_QUEST` entry on
|
||||
-- the shared StaticPopup engine (UiPanels.xml, decision 0308 §3). Every field below is
|
||||
-- StaticPopup's own vocabulary and every value is specified by StaticPopup.lua:749-761:
|
||||
-- the ABANDON_QUEST_CONFIRM wording 'Abandon "%s"?' (GlobalStrings.lua:5), abandoning on
|
||||
-- accept, and the igQuestLogAbandonQuest kit. There is no expression left to choose here,
|
||||
-- which is why this reads the way the specification does. exclusive is v1-inert (engine
|
||||
-- header note). ─────────────────────────────────────────────────────────────────────────
|
||||
StaticPopupDialogs["ABANDON_QUEST"] = {
|
||||
text = "Abandon \"%s\"?", -- ABANDON_QUEST_CONFIRM
|
||||
button1 = "Yes", -- YES
|
||||
@@ -1317,7 +1323,7 @@
|
||||
<FontString name="QuestLogTitleText" inherits="GameFontHighlight" justifyH="CENTER" text="Quest Log">
|
||||
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
|
||||
</FontString>
|
||||
<!-- pin §1, transcribed byte-for-byte (ref QuestLogFrame.xml:204-243): the quest-count
|
||||
<!-- pin §1, geometry/art byte-for-byte (ref QuestLogFrame.xml:204-243): the quest-count
|
||||
pill is the Common-Input-Border 3-texture trio + a FontString RIGHT of the right
|
||||
piece — REPLACING this window's old bare text at (-30,-16), which sat inside the
|
||||
close X's hit area and visually collided with it. -->
|
||||
@@ -1485,7 +1491,7 @@
|
||||
</Layer></Layers>
|
||||
</Frame>
|
||||
|
||||
<!-- pin §2, transcribed byte-for-byte (ref QuestLogFrame.xml:535-588): 6 title rows,
|
||||
<!-- pin §2, geometry byte-for-byte (ref QuestLogFrame.xml:535-588): 6 title rows,
|
||||
300×16, row1 TOPLEFT of the frame at (19,-75); rows 2-6 chain TOPLEFT→prior BOTTOMLEFT
|
||||
at (0,+1) — a TIGHT 1px OVERLAP, not a gap (the ref's own offset is positive). -->
|
||||
<Button name="QuestLogTitle1" inherits="BenillaQuestLogTitleTemplate">
|
||||
@@ -1539,7 +1545,7 @@
|
||||
QuestLogFrame.xml:648-659, at its own anchor and offset. Untimed quests are the
|
||||
overwhelming majority, so the row must cost them NOTHING: the ref hides it and
|
||||
re-SetPoints Objective1 straight onto ObjectivesText (QuestLogFrame.lua:370/374),
|
||||
and `BenillaQuestLogDetail_Update` transcribes exactly that pair of branches. Our
|
||||
and `BenillaQuestLogDetail_Update` keeps exactly that pair of branches. Our
|
||||
region model has no Show/Hide for a bare FontString (the recurring comment
|
||||
below), so the untimed case leaves this string EMPTY — it draws nothing, and
|
||||
nothing anchors through it, so its 12px sits harmlessly behind Objective1's row
|
||||
@@ -1762,7 +1768,7 @@
|
||||
</Scripts>
|
||||
</ScrollFrame>
|
||||
|
||||
<!-- pin §4, transcribed byte-for-byte (ref QuestLogFrame.xml:451-498): Abandon 125×21 at
|
||||
<!-- pin §4, geometry byte-for-byte (ref QuestLogFrame.xml:451-498): Abandon 125×21 at
|
||||
(17,54), Exit 77×21 at (-43,54). numEntries==0 disables Abandon. -->
|
||||
<Button name="QuestLogAbandonButton" inherits="BenillaQuestLogPanelButtonTemplate" text="Abandon Quest">
|
||||
<Size><AbsDimension x="125" y="21"/></Size>
|
||||
@@ -1843,7 +1849,7 @@
|
||||
up front and `QuestWatch_Update` only ever SetTexts/colors them, never creates one.
|
||||
-->
|
||||
<!-- The always-on quest-watch tracker HUD (ref QuestWatchFrame, QuestLogFrame.xml:963-1146
|
||||
+ QuestWatch_Update, QuestLogFrame.lua:602-663), transcribed: TOPRIGHT hangs off the
|
||||
+ QuestWatch_Update, QuestLogFrame.lua:602-663), re-expressed: TOPRIGHT hangs off the
|
||||
MinimapCluster's BOTTOMRIGHT (+10 up — the cluster art overhangs), an empty 13px
|
||||
GameFontNormal spacer (the ref's never-written QuestWatchQuestName) seats the first line,
|
||||
and ONE FLAT POOL of 30 lines (MAX_QUESTWATCH_LINES) carries titles and objectives alike —
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's Reputation tab: the 1.12 `ReputationFrame`, transcribed from the reference
|
||||
FrameXML `ReputationFrame.{xml,lua}` onto our own engine and feeds. Every geometry number,
|
||||
art path and TexCoord below is QUOTED from the reference with its line cite inline (cites
|
||||
read `ref l.N` into `ReputationFrame.xml`, `ref-lua l.N` into `ReputationFrame.lua`); the
|
||||
Lua is transcribed logic turned onto benilla's Era API — the twelve reputation globals of
|
||||
benilla's Reputation tab: our own build of the 1.12 `ReputationFrame`. The reference
|
||||
FrameXML `ReputationFrame.{xml,lua}` is the SPECIFICATION throughout — what the page must
|
||||
look like and do — and it is cited line by line at every site (cites read `ref l.N` into
|
||||
`ReputationFrame.xml`, `ref-lua l.N` into `ReputationFrame.lua`). Every geometry number, art
|
||||
path and TexCoord below is QUOTED from it, because those are the client's facts and there is
|
||||
no other value they could take; the Lua is ours, written against that specification and onto
|
||||
benilla's Era API — the twelve reputation globals of
|
||||
`crates/benilla-ui/src/script/reputation.rs`, whose module doc is the display law this file
|
||||
paints, over `crates/benilla-app/src/ui_reputation.rs`'s feed.
|
||||
paints, over `crates/benilla-app/src/ui_reputation.rs`'s feed. Where a line still reads
|
||||
exactly like the reference's it is because the frame names and the API are the
|
||||
addon-visible contract and neither is ours to vary.
|
||||
|
||||
A CHILD PAGE of `CharacterFrame` (ref l.211: `parent="CharacterFrame"`, `id="3"`), shown and
|
||||
hidden by `CharacterFrame_ShowSubFrame` — but a SEPARATE top-level file, the SkillFrame.xml /
|
||||
@@ -160,39 +164,155 @@
|
||||
return getglobal("FACTION_STANDING_LABEL" .. standingID) or ""
|
||||
end
|
||||
|
||||
-- Fill the detail popup for the row at `factionIndex` — ref-lua l.106-133, split out of
|
||||
-- the update loop only so that function stays readable; the body is the reference's,
|
||||
-- branch for branch. Called only when the popup is up AND this row is the selected one.
|
||||
function BenillaReputationDetail_Fill(factionIndex, name, description, atWarWith, canToggleAtWar, isWatched)
|
||||
ReputationDetailFactionName:SetText(name)
|
||||
ReputationDetailFactionDescription:SetText(description)
|
||||
if atWarWith then
|
||||
ReputationDetailAtWarCheckBox:SetChecked(1)
|
||||
-- A checkbox takes the client's 1/nil rather than a boolean, and every box on this page is
|
||||
-- driven straight off a flag out of the faction tuple — so one verb, not an if/else pair
|
||||
-- per box.
|
||||
function BenillaReputation_SetBoxChecked(box, on)
|
||||
box:SetChecked(on and 1 or nil)
|
||||
end
|
||||
|
||||
-- The selection glow is a PAIR of textures, because the row's frame art is two pieces
|
||||
-- (`$parentHighlight1` reaches back over the name, `2` caps the bar's right end). They are
|
||||
-- always raised and dropped together, from three places: the update below, and the row's
|
||||
-- own OnEnter/OnLeave.
|
||||
function BenillaReputationBar_SetGlow(barName, lit)
|
||||
local left, right = getglobal(barName .. "Highlight1"), getglobal(barName .. "Highlight2")
|
||||
if lit then
|
||||
left:Show()
|
||||
right:Show()
|
||||
else
|
||||
ReputationDetailAtWarCheckBox:SetChecked(nil)
|
||||
end
|
||||
if canToggleAtWar then
|
||||
ReputationDetailAtWarCheckBox:Enable()
|
||||
ReputationDetailAtWarCheckBoxText:SetTextColor(RED_FONT_COLOR.r, RED_FONT_COLOR.g, RED_FONT_COLOR.b)
|
||||
else
|
||||
ReputationDetailAtWarCheckBox:Disable()
|
||||
ReputationDetailAtWarCheckBoxText:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
|
||||
end
|
||||
if IsFactionInactive(factionIndex) then
|
||||
ReputationDetailInactiveCheckBox:SetChecked(1)
|
||||
else
|
||||
ReputationDetailInactiveCheckBox:SetChecked(nil)
|
||||
end
|
||||
if isWatched then
|
||||
ReputationDetailMainScreenCheckBox:SetChecked(1)
|
||||
else
|
||||
ReputationDetailMainScreenCheckBox:SetChecked(nil)
|
||||
left:Hide()
|
||||
right:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- The list page (ref-lua ReputationFrame_Update, l.32-149) — transcribed whole, onto the
|
||||
-- house faux-scroll kit and with the reference's own unread `rightBarTexture` local dropped
|
||||
-- (this file's header comment, deviation 6).
|
||||
-- Fill the detail popup for the row at `factionIndex` (ref-lua l.106-133 is the
|
||||
-- specification). Called only when the popup is up AND this row is the selected one.
|
||||
-- Everything it touches is a `ReputationDetail*` widget, which is the addon-visible name
|
||||
-- and so not ours to vary; what IS ours is that the three tick states are one verb and the
|
||||
-- at-war label's colour is written once instead of once per arm.
|
||||
function BenillaReputationDetail_Fill(factionIndex, name, description, atWarWith, canToggleAtWar, isWatched)
|
||||
ReputationDetailFactionName:SetText(name)
|
||||
ReputationDetailFactionDescription:SetText(description)
|
||||
BenillaReputation_SetBoxChecked(ReputationDetailAtWarCheckBox, atWarWith)
|
||||
-- A peace-forced faction cannot be fought, so its box is dead and the label greys off
|
||||
-- the angry red to say so.
|
||||
local label = GRAY_FONT_COLOR
|
||||
if canToggleAtWar then
|
||||
label = RED_FONT_COLOR
|
||||
ReputationDetailAtWarCheckBox:Enable()
|
||||
else
|
||||
ReputationDetailAtWarCheckBox:Disable()
|
||||
end
|
||||
ReputationDetailAtWarCheckBoxText:SetTextColor(label.r, label.g, label.b)
|
||||
BenillaReputation_SetBoxChecked(ReputationDetailInactiveCheckBox, IsFactionInactive(factionIndex))
|
||||
BenillaReputation_SetBoxChecked(ReputationDetailMainScreenCheckBox, isWatched)
|
||||
end
|
||||
|
||||
-- A row SLOT is a pair of twins on one line — the header BUTTON `ReputationHeader<i>` and
|
||||
-- the faction STATUSBAR `ReputationBar<i>` with all its furniture — of which exactly one is
|
||||
-- ever up. The two verbs below cover every state a slot can be in (a header, a faction, or
|
||||
-- nothing at all), and each raises the twin it wants and drops the other, so no caller has
|
||||
-- to remember the pairing.
|
||||
|
||||
-- Slot `i` has nothing to hold: both twins go dark (ref-lua l.141-143).
|
||||
function BenillaReputationSlot_Clear(i)
|
||||
getglobal("ReputationHeader" .. i):Hide()
|
||||
getglobal("ReputationBar" .. i):Hide()
|
||||
end
|
||||
|
||||
-- Slot `i` holds the visible row at `factionIndex`. Split out of the update loop so that
|
||||
-- loop is about the SCROLL WINDOW and this is about ONE ROW; the eleven-wide faction tuple
|
||||
-- then has exactly one reader in the file. ref-lua l.51-139 is the specification, and the
|
||||
-- reference's own unread `rightBarTexture` local is not carried (this file's header
|
||||
-- comment, deviation 6).
|
||||
function BenillaReputationSlot_Paint(i, factionIndex, gender)
|
||||
local bar = getglobal("ReputationBar" .. i)
|
||||
local header = getglobal("ReputationHeader" .. i)
|
||||
local check = getglobal("ReputationBar" .. i .. "Check")
|
||||
local name, description, standingID, barMin, barMax, barValue, atWarWith,
|
||||
canToggleAtWar, isHeader, isCollapsed, isWatched = GetFactionInfo(factionIndex)
|
||||
|
||||
if isHeader then
|
||||
-- A GROUP HEADER: the group's name, the fold icon its state calls for, and the
|
||||
-- visible index a click on it folds by. The icon says what the click WILL do — an
|
||||
-- open group offers the minus that shuts it, a shut one the plus that opens it.
|
||||
local foldIcon = "Interface\\Buttons\\UI-MinusButton-Up"
|
||||
if isCollapsed then
|
||||
foldIcon = "Interface\\Buttons\\UI-PlusButton-Up"
|
||||
end
|
||||
header.index = factionIndex
|
||||
header.isCollapsed = isCollapsed
|
||||
header:SetText(name)
|
||||
header:SetNormalTexture(foldIcon)
|
||||
header:Show()
|
||||
bar:Hide()
|
||||
check:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
-- A FACTION: the name and the standing word it currently wears.
|
||||
local standingText = BenillaReputation_StandingLabel(standingID, gender)
|
||||
local factionName = getglobal("ReputationBar" .. i .. "FactionName")
|
||||
factionName:SetText(name)
|
||||
getglobal("ReputationBar" .. i .. "FactionStanding"):SetText(standingText)
|
||||
|
||||
-- The crossed-swords pennant off the row's right end.
|
||||
local atWarIndicator = getglobal("ReputationBar" .. i .. "AtWarCheck")
|
||||
if atWarWith then
|
||||
atWarIndicator:Show()
|
||||
else
|
||||
atWarIndicator:Hide()
|
||||
end
|
||||
|
||||
-- The engine hands the rank window over ABSOLUTE for exactly this reason:
|
||||
-- reputation.rs's module doc names the reference's own normalization (ref-lua l.79-82)
|
||||
-- as the contract the tuple is written against. The bar's range is the window's WIDTH,
|
||||
-- its value the distance into it, and the hover tooltip is the same pair spelled out.
|
||||
local rankWidth = barMax - barMin
|
||||
local progress = barValue - barMin
|
||||
bar.id = factionIndex
|
||||
bar.standingText = standingText
|
||||
bar.tooltip = HIGHLIGHT_FONT_COLOR_CODE .. " " .. progress .. " / " .. rankWidth .. FONT_COLOR_CODE_CLOSE
|
||||
bar:SetMinMaxValues(0, rankWidth)
|
||||
bar:SetValue(progress)
|
||||
-- UNGUARDED, deliberately, exactly as the reference has it (ref-lua l.89).
|
||||
-- `GetFactionInfo` is eleven wide on EVERY path including a miss, and its miss puts the
|
||||
-- client's own `1` in the standingID slot for precisely this index (reputation.rs
|
||||
-- `unknown_faction`) — so the caller's `factionIndex <= numFactions` gate is belt, and
|
||||
-- that `1` is braces. Adding a nil-check here would be inventing a branch the client
|
||||
-- does not have and cannot reach.
|
||||
local color = FACTION_BAR_COLORS[standingID]
|
||||
bar:SetStatusBarColor(color.r, color.g, color.b)
|
||||
bar:SetID(factionIndex)
|
||||
bar:Show()
|
||||
header:Hide()
|
||||
|
||||
-- The watched checkmark rides the END of the name string, so the name box gives up 10
|
||||
-- of its 110px to make room for it (ref-lua l.95-103).
|
||||
if isWatched then
|
||||
check:Show()
|
||||
factionName:SetWidth(100)
|
||||
check:SetPoint("LEFT", factionName, "LEFT", factionName:GetStringWidth(), 0)
|
||||
else
|
||||
check:Hide()
|
||||
factionName:SetWidth(110)
|
||||
end
|
||||
|
||||
-- The selected row keeps its glow with the mouse away, and is the one that fills the
|
||||
-- popup while the popup is open (ref-lua l.105-139).
|
||||
local selected = factionIndex == GetSelectedFaction()
|
||||
if selected and ReputationDetailFrame:IsShown() then
|
||||
BenillaReputationDetail_Fill(factionIndex, name, description, atWarWith,
|
||||
canToggleAtWar, isWatched)
|
||||
end
|
||||
BenillaReputationBar_SetGlow("ReputationBar" .. i, selected)
|
||||
end
|
||||
|
||||
-- The list page (ref-lua ReputationFrame_Update, l.32-149 is the specification): fit the
|
||||
-- scroll window to the list, then bind each of the fixed slots to the row the offset puts
|
||||
-- under it, on the house faux-scroll kit rather than the reference's real ScrollFrame
|
||||
-- (this file's header comment, deviation 4).
|
||||
function ReputationFrame_Update()
|
||||
local numFactions = GetNumFactions()
|
||||
local scrollFrame = getglobal("ReputationListScrollFrame")
|
||||
@@ -210,92 +330,10 @@
|
||||
local gender = UnitSex("player")
|
||||
for i = 1, NUM_FACTIONS_DISPLAYED do
|
||||
local factionIndex = factionOffset + i
|
||||
local factionBar = getglobal("ReputationBar" .. i)
|
||||
local factionHeader = getglobal("ReputationHeader" .. i)
|
||||
local factionCheck = getglobal("ReputationBar" .. i .. "Check")
|
||||
if factionIndex <= numFactions then
|
||||
local name, description, standingID, barMin, barMax, barValue, atWarWith,
|
||||
canToggleAtWar, isHeader, isCollapsed, isWatched = GetFactionInfo(factionIndex)
|
||||
if isHeader then
|
||||
-- ref-lua l.52-63.
|
||||
factionHeader:SetText(name)
|
||||
if isCollapsed then
|
||||
factionHeader:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
|
||||
else
|
||||
factionHeader:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
|
||||
end
|
||||
factionHeader.index = factionIndex
|
||||
factionHeader.isCollapsed = isCollapsed
|
||||
factionBar:Hide()
|
||||
factionHeader:Show()
|
||||
factionCheck:Hide()
|
||||
else
|
||||
-- ref-lua l.65-139.
|
||||
local factionStanding = BenillaReputation_StandingLabel(standingID, gender)
|
||||
local factionName = getglobal("ReputationBar" .. i .. "FactionName")
|
||||
factionName:SetText(name)
|
||||
getglobal("ReputationBar" .. i .. "FactionStanding"):SetText(factionStanding)
|
||||
|
||||
local atWarIndicator = getglobal("ReputationBar" .. i .. "AtWarCheck")
|
||||
if atWarWith then
|
||||
atWarIndicator:Show()
|
||||
else
|
||||
atWarIndicator:Hide()
|
||||
end
|
||||
|
||||
-- Normalize values (ref-lua l.79-82). The engine hands these over ABSOLUTE
|
||||
-- for exactly this reason: reputation.rs's module doc names the reference's
|
||||
-- own three lines as the contract the tuple is written against.
|
||||
barMax = barMax - barMin
|
||||
barValue = barValue - barMin
|
||||
barMin = 0
|
||||
|
||||
factionBar.id = factionIndex
|
||||
factionBar.standingText = factionStanding
|
||||
factionBar.tooltip = HIGHLIGHT_FONT_COLOR_CODE .. " " .. barValue .. " / " .. barMax .. FONT_COLOR_CODE_CLOSE
|
||||
factionBar:SetMinMaxValues(0, barMax)
|
||||
factionBar:SetValue(barValue)
|
||||
-- UNGUARDED, deliberately, exactly as the reference has it (ref-lua l.89).
|
||||
-- `GetFactionInfo` is eleven wide on EVERY path including a miss, and its
|
||||
-- miss puts the client's own `1` in the standingID slot for precisely this
|
||||
-- index (reputation.rs `unknown_faction`) — so the reference's own
|
||||
-- `factionIndex <= numFactions` gate above is belt, and that `1` is braces.
|
||||
-- Adding a nil-check here would be inventing a branch the client does not
|
||||
-- have and cannot reach.
|
||||
local color = FACTION_BAR_COLORS[standingID]
|
||||
factionBar:SetStatusBarColor(color.r, color.g, color.b)
|
||||
factionBar:SetID(factionIndex)
|
||||
factionBar:Show()
|
||||
factionHeader:Hide()
|
||||
|
||||
-- The watched checkmark rides the END of the name string, so the name box
|
||||
-- narrows by 10 to make room (ref-lua l.95-103, verbatim).
|
||||
if isWatched then
|
||||
factionCheck:Show()
|
||||
factionName:SetWidth(100)
|
||||
factionCheck:SetPoint("LEFT", factionName, "LEFT", factionName:GetStringWidth(), 0)
|
||||
else
|
||||
factionCheck:Hide()
|
||||
factionName:SetWidth(110)
|
||||
end
|
||||
|
||||
-- ref-lua l.105-139: the selected row wears the highlight pair, and fills
|
||||
-- the popup if it is open.
|
||||
if factionIndex == GetSelectedFaction() then
|
||||
if ReputationDetailFrame:IsShown() then
|
||||
BenillaReputationDetail_Fill(factionIndex, name, description,
|
||||
atWarWith, canToggleAtWar, isWatched)
|
||||
end
|
||||
getglobal("ReputationBar" .. i .. "Highlight1"):Show()
|
||||
getglobal("ReputationBar" .. i .. "Highlight2"):Show()
|
||||
else
|
||||
getglobal("ReputationBar" .. i .. "Highlight1"):Hide()
|
||||
getglobal("ReputationBar" .. i .. "Highlight2"):Hide()
|
||||
end
|
||||
end
|
||||
BenillaReputationSlot_Paint(i, factionIndex, gender)
|
||||
else
|
||||
factionHeader:Hide()
|
||||
factionBar:Hide()
|
||||
BenillaReputationSlot_Clear(i)
|
||||
end
|
||||
end
|
||||
-- ref-lua l.146-148: nothing selected means nothing to detail. `GetSelectedFaction`
|
||||
@@ -307,18 +345,19 @@
|
||||
end
|
||||
|
||||
-- A bar click toggles the popup for that faction (ref-lua ReputationBar_OnClick,
|
||||
-- l.151-159). Parameterless and reading `this`, exactly as the reference writes it: our
|
||||
-- l.151-159): the row that is already showing shuts it, any other row moves the selection
|
||||
-- and opens it. Parameterless and reading `this`, exactly as the reference writes it: our
|
||||
-- dispatcher sets `this` before invoking a handler (script/event.rs `invoke_with_globals`)
|
||||
-- and it stays set through the call, the same contract ScrollTemplates.xml's
|
||||
-- `FauxScrollFrame_OnVerticalScroll` already relies on.
|
||||
function ReputationBar_OnClick()
|
||||
if ReputationDetailFrame:IsShown() and (GetSelectedFaction() == this.id) then
|
||||
if ReputationDetailFrame:IsShown() and GetSelectedFaction() == this.id then
|
||||
ReputationDetailFrame:Hide()
|
||||
else
|
||||
SetSelectedFaction(this.id)
|
||||
ReputationDetailFrame:Show()
|
||||
ReputationFrame_Update()
|
||||
return
|
||||
end
|
||||
SetSelectedFaction(this.id)
|
||||
ReputationDetailFrame:Show()
|
||||
ReputationFrame_Update()
|
||||
end
|
||||
|
||||
-- A header click folds or unfolds its group (ref l.9-15, the template's own OnClick).
|
||||
@@ -517,23 +556,22 @@
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<!-- ref l.189-207, transcribed verbatim: hovering swaps the standing word for the raw
|
||||
"value / max" and lights the glow; leaving puts the word back and drops the glow unless
|
||||
this row is the selected one. -->
|
||||
<!-- ref l.189-207 is the specification: hovering swaps the standing word for the raw
|
||||
"value / max" the update stamped on the bar and lights the glow; leaving puts the word
|
||||
back and drops the glow again — unless this row is the SELECTED one, which keeps it.
|
||||
Both strings and both glow states were put on the bar by the update above, so the
|
||||
hover only chooses between them; the glow pair goes through that function's own verb
|
||||
so a hover and a repaint can never disagree about which textures are up. -->
|
||||
<Scripts>
|
||||
<OnEnter>
|
||||
if (this.tooltip) then
|
||||
if this.tooltip then
|
||||
getglobal(this:GetName() .. "FactionStanding"):SetText(this.tooltip)
|
||||
end
|
||||
getglobal(this:GetName() .. "Highlight1"):Show()
|
||||
getglobal(this:GetName() .. "Highlight2"):Show()
|
||||
BenillaReputationBar_SetGlow(this:GetName(), 1)
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
getglobal(this:GetName() .. "FactionStanding"):SetText(this.standingText)
|
||||
if (GetSelectedFaction() ~= this.id) then
|
||||
getglobal(this:GetName() .. "Highlight1"):Hide()
|
||||
getglobal(this:GetName() .. "Highlight2"):Hide()
|
||||
end
|
||||
BenillaReputationBar_SetGlow(this:GetName(), GetSelectedFaction() == this.id)
|
||||
</OnLeave>
|
||||
<OnMouseUp>ReputationBar_OnClick()</OnMouseUp>
|
||||
</Scripts>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's Skills tab (decision 0437 phase 4): the 1.12 `SkillFrame`, transcribed from the
|
||||
benilla's Skills tab (decision 0437 phase 4): the 1.12 `SkillFrame`, re-expressed from the
|
||||
reference FrameXML `SkillFrame.{xml,lua}` (session scratchpad `ref-ui/`, decision 0246
|
||||
discipline — never committed) onto our own engine/feeds. Geometry/art-paths below are QUOTED
|
||||
from the ref (line citations inline, into that same `SkillFrame.xml`/`.lua`); the Lua is
|
||||
transcribed logic turned onto benilla's Era API (`GetNumSkillLines`/`GetSkillLineInfo`/
|
||||
the ref's logic turned onto benilla's Era API (`GetNumSkillLines`/`GetSkillLineInfo`/
|
||||
`Expand`/`CollapseSkillHeader`/`SetSelectedSkill`/`GetSelectedSkill`, decision 0437's skills
|
||||
seam, `crates/benilla-ui/src/script/skills.rs`, built alongside this file).
|
||||
|
||||
@@ -160,8 +160,8 @@
|
||||
bgTexture:SetVertexColor(0.0, 0.0, 0.75, 0.5)
|
||||
bar:SetMinMaxValues(0, skillMaxRank)
|
||||
bar:SetValue(skillRank)
|
||||
-- ref SkillFrame.lua:178-186, transcribed byte-exact (this file's header comment,
|
||||
-- deviation 4).
|
||||
-- the ref's rank-text law (SkillFrame.lua:178-186), re-expressed here (this file's
|
||||
-- header comment, deviation 4).
|
||||
if skillModifier == 0 then
|
||||
rankText:SetText(skillRank .. "/" .. skillMaxRank)
|
||||
else
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
(`UiPanels.xml`'s `UIPanelWindows["SpellBookFrame"] = { area = "left", pushable =
|
||||
0 }`, mirroring the ref's own `SpellBookFrame` row) — so it competes for/replaces those
|
||||
windows, never stacks with them. `ToggleSpellBook(bookType)` (ref `SpellBookFrame.lua:
|
||||
9-50`) is the addon-facing entry point; like every frame name here it is the reference's own
|
||||
`ToggleQuestLog` precedent) is the show/hide entry point; the app's 'P' binding calls it
|
||||
with `BOOKTYPE_SPELL` (`ui_script/input.rs`, ref `Bindings.xml:582-583`).
|
||||
9-50`) is the show/hide entry point, and it is addon-facing: it wears the reference's bare
|
||||
name for the same reason `ToggleCharacter`/`ToggleQuestLog` do. The app's 'P' binding calls
|
||||
it with `BOOKTYPE_SPELL` (`ui_script/input.rs`, ref `Bindings.xml:582-583`).
|
||||
|
||||
Every frame this file defines wears the REFERENCE's name (`SpellBookFrame`, `SpellButton1`,
|
||||
…), because addons address them by it — the `Benilla` prefix these carried until the
|
||||
@@ -408,35 +408,40 @@
|
||||
local subSpellString = getglobal(name .. "SubSpellName")
|
||||
local cooldown = getglobal(name .. "Cooldown")
|
||||
local autocastable = getglobal(name .. "AutoCastable")
|
||||
-- ref l.328: the out-of-tab DISABLE is `(bookType ~= BOOKTYPE_PET) and ...` — the pet
|
||||
local highlightTexture = getglobal(name .. "Highlight")
|
||||
local normalTexture = getglobal(name .. "NormalTexture")
|
||||
|
||||
-- TWO WAYS A SLOT COMES UP EMPTY, one dress for both. Past the end of the tab the
|
||||
-- button also goes DEAD; a slot the book counts but has no icon for stays live and
|
||||
-- only puts its highlight kit back to the square. Everything else the two do — the
|
||||
-- five regions down, the autocast bit cleared, the ring unchecked, the button's own
|
||||
-- ring back to plain white — is the same blanking, and it is written once here rather
|
||||
-- than once per arm.
|
||||
--
|
||||
-- ref l.328: the out-of-tab test is `(bookType ~= BOOKTYPE_PET) and ...` — the pet
|
||||
-- page has no tab to be out of, so a pet button past the end of the book falls to the
|
||||
-- no-texture branch below and stays ENABLED. Faithful, and harmless: an enabled button
|
||||
-- no-icon arm instead and stays ENABLED. Faithful, and harmless: an enabled button
|
||||
-- with no spell behind it clicks into GetSpellName's nil and does nothing.
|
||||
if (not isPet) and id > (offset + numSpells) then
|
||||
local outOfTab = (not isPet) and id > (offset + numSpells)
|
||||
if outOfTab then
|
||||
button:Disable()
|
||||
iconTexture:Hide()
|
||||
spellString:Hide()
|
||||
subSpellString:Hide()
|
||||
cooldown:Hide()
|
||||
autocastable:Hide()
|
||||
button.autocasting = nil
|
||||
button:SetChecked(0)
|
||||
getglobal(name .. "NormalTexture"):SetVertexColor(1.0, 1.0, 1.0)
|
||||
return
|
||||
else
|
||||
button:Enable()
|
||||
end
|
||||
local texture = GetSpellTexture(id, SpellBookFrame.bookType)
|
||||
local highlightTexture = getglobal(name .. "Highlight")
|
||||
local normalTexture = getglobal(name .. "NormalTexture")
|
||||
if (not texture) or (texture == "") then
|
||||
local texture
|
||||
if not outOfTab then
|
||||
texture = GetSpellTexture(id, SpellBookFrame.bookType)
|
||||
end
|
||||
if outOfTab or (not texture) or (texture == "") then
|
||||
iconTexture:Hide()
|
||||
spellString:Hide()
|
||||
subSpellString:Hide()
|
||||
cooldown:Hide()
|
||||
autocastable:Hide()
|
||||
button.autocasting = nil
|
||||
highlightTexture:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
|
||||
if not outOfTab then
|
||||
highlightTexture:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
|
||||
end
|
||||
button:SetChecked(0)
|
||||
normalTexture:SetVertexColor(1.0, 1.0, 1.0)
|
||||
return
|
||||
@@ -446,11 +451,8 @@
|
||||
-- widget stays hidden (CooldownFrame_SetTimer's own gate) and the icon reads 40%.
|
||||
local start, duration, enable = GetSpellCooldown(id, SpellBookFrame.bookType)
|
||||
CooldownFrame_SetTimer(cooldown, start, duration, enable)
|
||||
if enable == 1 then
|
||||
iconTexture:SetVertexColor(1.0, 1.0, 1.0)
|
||||
else
|
||||
iconTexture:SetVertexColor(0.4, 0.4, 0.4)
|
||||
end
|
||||
local brightness = (enable == 1) and 1.0 or 0.4
|
||||
iconTexture:SetVertexColor(brightness, brightness, brightness)
|
||||
-- ref l.367-377: the autocast pair. `GetSpellAutocast` answers (nil, nil) for the
|
||||
-- player book by the engine's own short-circuit, so this needs no isPet guard — the
|
||||
-- overlay and the sparkle simply never come on outside the pet page.
|
||||
@@ -459,15 +461,21 @@
|
||||
button.autocasting = autoCastEnabled
|
||||
local spellName, subSpellName = GetSpellName(id, SpellBookFrame.bookType)
|
||||
local isPassive = IsSpellPassive(id, SpellBookFrame.bookType)
|
||||
-- A passive slot wears a whole different dress — the button ring blacked right out,
|
||||
-- the round passive highlight in place of the square one, and the name in the passive
|
||||
-- grey. Three values that always travel together, so they are chosen once and applied
|
||||
-- once instead of each verb appearing in both arms.
|
||||
local ringLevel, highlightFile, nameColor
|
||||
if isPassive then
|
||||
normalTexture:SetVertexColor(0, 0, 0)
|
||||
highlightTexture:SetTexture("Interface\\Buttons\\UI-PassiveHighlight")
|
||||
spellString:SetTextColor(PASSIVE_SPELL_FONT_COLOR.r, PASSIVE_SPELL_FONT_COLOR.g, PASSIVE_SPELL_FONT_COLOR.b)
|
||||
ringLevel, highlightFile, nameColor =
|
||||
0, "Interface\\Buttons\\UI-PassiveHighlight", PASSIVE_SPELL_FONT_COLOR
|
||||
else
|
||||
normalTexture:SetVertexColor(1.0, 1.0, 1.0)
|
||||
highlightTexture:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
|
||||
spellString:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
|
||||
ringLevel, highlightFile, nameColor =
|
||||
1.0, "Interface\\Buttons\\ButtonHilight-Square", NORMAL_FONT_COLOR
|
||||
end
|
||||
normalTexture:SetVertexColor(ringLevel, ringLevel, ringLevel)
|
||||
highlightTexture:SetTexture(highlightFile)
|
||||
spellString:SetTextColor(nameColor.r, nameColor.g, nameColor.b)
|
||||
iconTexture:SetTexture(texture)
|
||||
spellString:SetText(spellName)
|
||||
subSpellString:SetText(subSpellName)
|
||||
@@ -551,19 +559,15 @@
|
||||
+ (SPELLS_PER_PAGE * ((SPELLBOOK_PAGENUMBERS[SpellBookFrame.selectedSkillLine] or 1) - 1))
|
||||
end
|
||||
|
||||
-- ref SpellBook_UpdatePageArrows (l.467-479).
|
||||
-- ref SpellBook_UpdatePageArrows (l.467-479). An arrow is live exactly while the page it
|
||||
-- points at is a DIFFERENT page. Stated as an inequality rather than a range test on
|
||||
-- purpose: the pet book's arrows write a counter its own page ignores (the do-not-fix
|
||||
-- notes above), so a page number can sit outside 1..maxPages, and when it does BOTH arrows
|
||||
-- stay live — which is the reference's answer, not an accident of this spelling.
|
||||
function SpellBook_UpdatePageArrows()
|
||||
local currentPage, maxPages = SpellBook_GetCurrentPage()
|
||||
if currentPage == 1 then
|
||||
SpellBookPrevPageButton:Disable()
|
||||
else
|
||||
SpellBookPrevPageButton:Enable()
|
||||
end
|
||||
if currentPage == maxPages then
|
||||
SpellBookNextPageButton:Disable()
|
||||
else
|
||||
SpellBookNextPageButton:Enable()
|
||||
end
|
||||
if currentPage ~= 1 then SpellBookPrevPageButton:Enable() else SpellBookPrevPageButton:Disable() end
|
||||
if currentPage ~= maxPages then SpellBookNextPageButton:Enable() else SpellBookNextPageButton:Disable() end
|
||||
end
|
||||
|
||||
-- ref SpellBook_GetCurrentPage (l.481-493), both branches. `maxPages` is clamped to at
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The stack-split spinner (decision 0216 §6/§D, scratchpad `ref-framexml/StackSplitFrame.xml`/
|
||||
`.lua`, transcribed near-verbatim onto our own cursor seam — `SplitContainerItem`/
|
||||
`.lua`, re-expressed onto our own cursor seam — `SplitContainerItem`/
|
||||
`OpenStackSplitFrame` already exist in benilla-ui's `cursor.rs`; this file is purely the
|
||||
window + the Left/Right/Okay/Cancel plumbing around it). One shared instance, like the real
|
||||
client's `StackSplitFrame` — any owner (a bag slot today; the paper doll / bank / mail slices
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
TAB STRIP — reuses the house's EXISTING tab kit, not a hand-rolled `BenillaTalentFrame.
|
||||
selectedTab` variable. The task brief's own ENGINE API note flagged `PanelTemplates_*` as
|
||||
unavailable ("implement your own small select/deselect Lua") — verified false this session:
|
||||
`UiPanels.xml:502-578` already transcribes the FULL kit (`PanelTemplates_SetNumTabs/_SetTab/
|
||||
`UiPanels.xml:502-578` already provides the FULL kit (`PanelTemplates_SetNumTabs/_SetTab/
|
||||
_GetSelectedTab/_UpdateTabs/_SelectTab/_DeselectTab/_TabResize`), and `CharacterFrameTab-
|
||||
ButtonTemplate` (`UiPanels.xml:603-654`) is ALREADY battle-tested by CharacterFrame.xml's own
|
||||
tab. Better still: the REFERENCE's own `TalentTabTemplate` (Blizzard_TalentUITemplates.xml:3)
|
||||
@@ -54,7 +54,7 @@
|
||||
own `TalentButtonTemplate` inline OnEnter, Blizzard_TalentUITemplates.xml:92) and
|
||||
`PanelTemplates_GetSelectedTab(TalentFrame)` (read by TalentFrame.lua's own functions) are
|
||||
THE SAME FIELD — `PanelTemplates_SetTab` stores straight onto `frame.selectedTab` — so this
|
||||
file transcribes each call site exactly as the reference itself spells it, not a benilla-
|
||||
file spells each call site exactly as the reference itself does, not a benilla-
|
||||
introduced inconsistency.
|
||||
|
||||
SCROLL DESIGN — a real ScrollFrame with a FIXED-HEIGHT child, not the reference's own dynamic
|
||||
@@ -144,7 +144,7 @@
|
||||
fires it (grepped `crates/benilla-ui/src`, `crates/benilla/src`) — forward-compatible,
|
||||
harmless, the same "transcribe the hook even if nothing drives it yet" posture as
|
||||
HitRectInsets above.
|
||||
- Bare `min`/`max`/`select` (used verbatim in `_DrawLines`/`_SetPrereqs` below): CONFIRMED
|
||||
- Bare `min`/`max`/`select` (used unqualified in `_DrawLines`/`_SetPrereqs` below): CONFIRMED
|
||||
available — `min`/`max` are house-stdlib aliases for `math.min`/`math.max`
|
||||
(`benilla-ui/src/script/stdlib.rs`), `select` is stock Lua 5.1. The ref's own `arg`/`arg.n`
|
||||
Lua-5.0 vararg table (`_SetPrereqs`) is written the 5.1-native way
|
||||
@@ -171,17 +171,38 @@
|
||||
CHARACTER_POINTS1_COLON = CHARACTER_POINTS1_COLON or "Talent Points:"
|
||||
MASTERY_POINTS_SPENT = MASTERY_POINTS_SPENT or "Points spent in %s Talents:" -- :2674
|
||||
|
||||
-- ── This window's own constants (ref Blizzard_TalentUI.lua:2-11, bare-named exactly as the
|
||||
-- ref names them — none collide with anything else in this engine, grepped) ─────────────────
|
||||
MAX_NUM_TALENTS = 20
|
||||
-- ── This window's own constants ─────────────────────────────────────────────────────────
|
||||
--
|
||||
-- Spec: ref Blizzard_TalentUI.lua:2-11. The NAMES and VALUES are the contract — a tree's
|
||||
-- data is laid out against them and addons read them — so those are the reference's; the
|
||||
-- grouping is ours, by what each one is for rather than by the order they happen to appear.
|
||||
|
||||
-- The tree's shape: eight rows of four cells, never more than twenty talents in one tree.
|
||||
MAX_NUM_TALENT_TIERS = 8
|
||||
NUM_TALENT_COLUMNS = 4
|
||||
TALENT_BRANCH_ARRAY = {}
|
||||
TALENT_BUTTON_SIZE = 32
|
||||
MAX_NUM_BRANCH_TEXTURES = 30
|
||||
MAX_NUM_ARROW_TEXTURES = 30
|
||||
MAX_NUM_TALENTS = 20
|
||||
|
||||
-- Where that shape lands inside the scroll child: the first cell sits INITIAL_TALENT_
|
||||
-- OFFSET_X/Y in from its top-left corner and every next one is a PITCH further along.
|
||||
-- TALENT_BUTTON_SIZE is how far the connector art reaches out of a cell, NOT the talent
|
||||
-- button's own 37px size — the two are independent (this file's header comment, "The
|
||||
-- talent button"). Naming the pitch is what let the two places that used to spell out
|
||||
-- `((n - 1) * 63) + INITIAL_TALENT_OFFSET_…` — the button placement and the connector draw
|
||||
-- — collapse into the one `talentCellOrigin` below, so they cannot drift apart.
|
||||
INITIAL_TALENT_OFFSET_X = 35
|
||||
INITIAL_TALENT_OFFSET_Y = 20
|
||||
TALENT_BUTTON_SIZE = 32
|
||||
BENILLA_TALENT_GRID_PITCH = 63
|
||||
|
||||
-- The connector art comes out of two fixed pools, claimed in order by each repaint and
|
||||
-- hidden again past the last one claimed (BenillaTalentFrame_Get*Texture below).
|
||||
MAX_NUM_BRANCH_TEXTURES = 30
|
||||
MAX_NUM_ARROW_TEXTURES = 30
|
||||
|
||||
-- One drawing-state cell per grid position — built once in OnLoad, cleared (never rebuilt)
|
||||
-- by every repaint. What a cell records is written down at `clearBranchCell` below.
|
||||
TALENT_BRANCH_ARRAY = {}
|
||||
|
||||
-- Benilla-prefixed + renumbered (5 -> 3): this file's header comment, "SCOPE CALL".
|
||||
BENILLA_MAX_TALENT_TABS = 3
|
||||
-- The scroll child's fixed height: this file's header comment, "SCROLL DESIGN".
|
||||
@@ -192,64 +213,95 @@
|
||||
-- own BENILLA_QUESTLOG_SCROLL_STEP precedent for a real
|
||||
-- ScrollFrame's per-notch pixel step)
|
||||
|
||||
-- ref Blizzard_TalentUI.lua:13-69, verbatim.
|
||||
TALENT_BRANCH_TEXTURECOORDS = {
|
||||
up = {
|
||||
[1] = {0.12890625, 0.25390625, 0, 0.484375},
|
||||
[-1] = {0.12890625, 0.25390625, 0.515625, 1.0}
|
||||
},
|
||||
down = {
|
||||
[1] = {0, 0.125, 0, 0.484375},
|
||||
[-1] = {0, 0.125, 0.515625, 1.0}
|
||||
},
|
||||
left = {
|
||||
[1] = {0.2578125, 0.3828125, 0, 0.5},
|
||||
[-1] = {0.2578125, 0.3828125, 0.5, 1.0}
|
||||
},
|
||||
right = {
|
||||
[1] = {0.2578125, 0.3828125, 0, 0.5},
|
||||
[-1] = {0.2578125, 0.3828125, 0.5, 1.0}
|
||||
},
|
||||
topright = {
|
||||
[1] = {0.515625, 0.640625, 0, 0.5},
|
||||
[-1] = {0.515625, 0.640625, 0.5, 1.0}
|
||||
},
|
||||
topleft = {
|
||||
[1] = {0.640625, 0.515625, 0, 0.5},
|
||||
[-1] = {0.640625, 0.515625, 0.5, 1.0}
|
||||
},
|
||||
bottomright = {
|
||||
[1] = {0.38671875, 0.51171875, 0, 0.5},
|
||||
[-1] = {0.38671875, 0.51171875, 0.5, 1.0}
|
||||
},
|
||||
bottomleft = {
|
||||
[1] = {0.51171875, 0.38671875, 0, 0.5},
|
||||
[-1] = {0.51171875, 0.38671875, 0.5, 1.0}
|
||||
},
|
||||
tdown = {
|
||||
[1] = {0.64453125, 0.76953125, 0, 0.5},
|
||||
[-1] = {0.64453125, 0.76953125, 0.5, 1.0}
|
||||
},
|
||||
tup = {
|
||||
[1] = {0.7734375, 0.8984375, 0, 0.5},
|
||||
[-1] = {0.7734375, 0.8984375, 0.5, 1.0}
|
||||
},
|
||||
}
|
||||
-- ── The branch/arrow atlases: CUT from the sheet, not typed out ─────────────────────────
|
||||
--
|
||||
-- Spec: the same two tables the reference builds (ref Blizzard_TalentUI.lua:13-69) — same
|
||||
-- global names, same [direction][+1/-1] shape, same quads to the bit. Written our own way,
|
||||
-- because what these are is not 104 fractions: both sheets are ONE STRIP of 32px cells
|
||||
-- with the lit variant on the top row and the greyed variant directly beneath it, so a
|
||||
-- direction is fully described by three facts — which cell carries its art, whether it is
|
||||
-- drawn u-reversed, and how tall the drawn part of a row is. Those facts are the spec
|
||||
-- below and the loop under them cuts the quads, which is both shorter and legible: the
|
||||
-- strip's layout is visible in the source instead of implied by the numbers.
|
||||
--
|
||||
-- The grid is MEASURED, not inferred from the fractions — both BLPs pulled out of the
|
||||
-- live patch chain this session (`mpqx <Data> 'Interface\TalentFrame\UI-Talent*.blp'`,
|
||||
-- dimensions off the BLP2 header):
|
||||
-- UI-TalentBranches 256 x 64 — seven 32px cells on a 33px pitch (x = 0, 33, 66, …)
|
||||
-- UI-TalentArrows 64 x 64 — two 32px cells, butted, no gutter
|
||||
-- Every edge is therefore n/256 or n/64 — exact binary fractions, so the divides below are
|
||||
-- exact in doubles and these quads are bit-identical to the ones the reference lists.
|
||||
--
|
||||
-- Three things in the data that look like mistakes and are the sheet's own:
|
||||
-- · `left` and `right` are the SAME quad. There is one piece of horizontal branch art;
|
||||
-- which side of the button it lands on is the draw loop's doing (xOffset -/+
|
||||
-- TALENT_BUTTON_SIZE in _Update below), not the sheet's.
|
||||
-- · the corner pieces are drawn in ONE handedness — `topleft`/`bottomleft` are
|
||||
-- `topright`/`bottomright` with u reversed (SetTexCoord reads left > right as a
|
||||
-- mirror), which is how one drawing serves both.
|
||||
-- · the two VERTICAL pieces take a 31px row, leaving a 2px gutter across the sheet's
|
||||
-- middle, where every other cell takes the flat 32. The greyed row stays the lit
|
||||
-- row's reflection through the midline either way — one `rowH` drives both.
|
||||
local function talentSheetQuads(sheetW, sheetH, cellW, pitch, spec)
|
||||
local quads = {}
|
||||
for name, cell in pairs(spec) do
|
||||
local u0, u1 = cell[1] * pitch / sheetW, (cell[1] * pitch + cellW) / sheetW
|
||||
if cell.flip then u0, u1 = u1, u0 end
|
||||
local v = (cell.rowH or cellW) / sheetH
|
||||
quads[name] = { [1] = { u0, u1, 0, v }, [-1] = { u0, u1, 1 - v, 1.0 } }
|
||||
end
|
||||
return quads
|
||||
end
|
||||
|
||||
TALENT_ARROW_TEXTURECOORDS = {
|
||||
top = {
|
||||
[1] = {0, 0.5, 0, 0.5},
|
||||
[-1] = {0, 0.5, 0.5, 1.0}
|
||||
},
|
||||
right = {
|
||||
[1] = {1.0, 0.5, 0, 0.5},
|
||||
[-1] = {1.0, 0.5, 0.5, 1.0}
|
||||
},
|
||||
left = {
|
||||
[1] = {0.5, 1.0, 0, 0.5},
|
||||
[-1] = {0.5, 1.0, 0.5, 1.0}
|
||||
},
|
||||
}
|
||||
-- Which cell serves which direction (the reference's own key names, which is where the
|
||||
-- reading of each cell's art comes from): 0 `down`, 1 `up`, 2 the horizontal run, 3 the
|
||||
-- bottom corners, 4 the top corners, 5 the T pointing down, 6 the T pointing up.
|
||||
TALENT_BRANCH_TEXTURECOORDS = talentSheetQuads(256, 64, 32, 33, {
|
||||
down = { 0, rowH = 31 },
|
||||
up = { 1, rowH = 31 },
|
||||
left = { 2 },
|
||||
right = { 2 },
|
||||
bottomright = { 3 },
|
||||
bottomleft = { 3, flip = true },
|
||||
topright = { 4 },
|
||||
topleft = { 4, flip = true },
|
||||
tdown = { 5 },
|
||||
tup = { 6 },
|
||||
})
|
||||
|
||||
-- cell 0 is the arrow that points up the column; cell 1 is the sideways one, mirrored for
|
||||
-- the other hand.
|
||||
TALENT_ARROW_TEXTURECOORDS = talentSheetQuads(64, 64, 32, 32, {
|
||||
top = { 0 },
|
||||
left = { 1 },
|
||||
right = { 1, flip = true },
|
||||
})
|
||||
|
||||
-- ── The branch grid ─────────────────────────────────────────────────────────────────────
|
||||
--
|
||||
-- A cell of TALENT_BRANCH_ARRAY holds the talent that occupies its grid position (`id`),
|
||||
-- and the drawing state of the connector art passing THROUGH that position: four half-
|
||||
-- branches (`up`/`down`/`left`/`right`, each the piece of a segment on this cell's side of
|
||||
-- the join) and three arrowheads. Every one of those is either 0 — nothing drawn here — or
|
||||
-- the ±1 that the draw pass indexes TALENT_BRANCH_TEXTURECOORDS/TALENT_ARROW_TEXTURECOORDS
|
||||
-- by: +1 the lit art, -1 the greyed. That is the whole reason a met and an unmet prereq are
|
||||
-- one code path in two colours rather than two code paths.
|
||||
--
|
||||
-- Clearing rather than rebuilding keeps the 32 cells allocated exactly once (OnLoad), and
|
||||
-- puts their field set in ONE place instead of two that can drift apart.
|
||||
local function clearBranchCell(cell)
|
||||
cell.id = nil
|
||||
cell.up, cell.down, cell.left, cell.right = 0, 0, 0, 0
|
||||
cell.topArrow, cell.leftArrow, cell.rightArrow = 0, 0, 0
|
||||
return cell
|
||||
end
|
||||
|
||||
-- A grid cell's top-left corner inside the scroll child. The talent button seats exactly on
|
||||
-- it; the connector art seats at the reference's own 2px inset from it (+2 x, -2 y).
|
||||
local function talentCellOrigin(tier, column)
|
||||
return (column - 1) * BENILLA_TALENT_GRID_PITCH + INITIAL_TALENT_OFFSET_X,
|
||||
-((tier - 1) * BENILLA_TALENT_GRID_PITCH) - INITIAL_TALENT_OFFSET_Y
|
||||
end
|
||||
|
||||
-- ── The window: toggle (ref TalentFrame_Toggle, l.73-79) ────────────────────────────────
|
||||
-- Renamed ToggleTalentFrame — this file's header comment, "ONE deliberate naming deviation".
|
||||
@@ -272,13 +324,11 @@
|
||||
-- UNIT_PORTRAIT_UPDATE: this file's header comment (engine-gap notes) — never fired
|
||||
-- today, registered anyway for ref fidelity/forward-compat.
|
||||
frame:RegisterEvent("UNIT_PORTRAIT_UPDATE")
|
||||
-- The grid's one allocation: every repaint clears these same cells in place.
|
||||
for i = 1, MAX_NUM_TALENT_TIERS do
|
||||
TALENT_BRANCH_ARRAY[i] = {}
|
||||
for j = 1, NUM_TALENT_COLUMNS do
|
||||
TALENT_BRANCH_ARRAY[i][j] = {
|
||||
id = nil, up = 0, left = 0, right = 0, down = 0,
|
||||
leftArrow = 0, rightArrow = 0, topArrow = 0,
|
||||
}
|
||||
TALENT_BRANCH_ARRAY[i][j] = clearBranchCell({})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -298,7 +348,7 @@
|
||||
PlaySound("TalentScreenClose")
|
||||
end
|
||||
|
||||
-- ref TalentFrame_OnEvent (l.110-118) — no IsVisible() guard: transcribed exactly as THIS
|
||||
-- ref TalentFrame_OnEvent (l.110-118) — no IsVisible() guard: followed exactly as THIS
|
||||
-- reference does it (it refreshes even while hidden; a different window guarding its own
|
||||
-- OnEvent, e.g. SpellBookFrame, is that window's own choice, not borrowed here).
|
||||
function BenillaTalentFrame_OnEvent(frame)
|
||||
@@ -313,7 +363,7 @@
|
||||
|
||||
-- ref TalentFrameTalent_OnEvent (Blizzard_TalentUITemplates.xml:120-124) — the hovered
|
||||
-- button's own CHARACTER_POINTS_CHANGED re-poll. GameTooltip:IsOwned VERIFIED present
|
||||
-- (`benilla-ui/src/script/tooltip/verbs.rs:107`), transcribed as-is (not an emulation).
|
||||
-- (`benilla-ui/src/script/tooltip/verbs.rs:107`), used as-is (not an emulation).
|
||||
function BenillaTalentButton_OnEvent(button)
|
||||
if GameTooltip:IsOwned(button) then
|
||||
GameTooltip:SetTalent(PanelTemplates_GetSelectedTab(BenillaTalentFrame), button:GetID())
|
||||
@@ -448,9 +498,15 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw the prereq branches (ref l.232-320, verbatim). UpdateScrollChildRect() DROPPED
|
||||
-- Now paint the routes the pass above marked onto the grid (spec: ref l.232-320): walk
|
||||
-- every cell in order, claim a pooled texture for each piece of art it asks for, and
|
||||
-- hide whatever is left of the pools at the end. UpdateScrollChildRect() is DROPPED
|
||||
-- from the tier loop's tail — this file's header comment ("SCROLL DESIGN": a fixed-
|
||||
-- height child never needs its range re-derived).
|
||||
--
|
||||
-- `ignoreUp` deliberately survives from one cell to the NEXT: an empty cell carrying a
|
||||
-- straight vertical through it draws its own `down` piece one row lower, so the cell
|
||||
-- below must not draw the `up` piece again on top of it.
|
||||
local node
|
||||
local xOffset, yOffset
|
||||
local ignoreUp
|
||||
@@ -460,10 +516,9 @@
|
||||
for i = 1, MAX_NUM_TALENT_TIERS do
|
||||
for j = 1, NUM_TALENT_COLUMNS do
|
||||
node = TALENT_BRANCH_ARRAY[i][j]
|
||||
|
||||
-- Setup offsets
|
||||
xOffset = ((j - 1) * 63) + INITIAL_TALENT_OFFSET_X + 2
|
||||
yOffset = -((i - 1) * 63) - INITIAL_TALENT_OFFSET_Y - 2
|
||||
-- The cell's own corner, then the connector art's inset inside it.
|
||||
xOffset, yOffset = talentCellOrigin(i, j)
|
||||
xOffset, yOffset = xOffset + 2, yOffset - 2
|
||||
|
||||
if node.id then
|
||||
-- Has talent
|
||||
@@ -618,136 +673,137 @@
|
||||
return requirementsMet
|
||||
end
|
||||
|
||||
-- ref TalentFrame_DrawLines (l.398-527) — verbatim (bare min/max confirmed available,
|
||||
-- this file's header comment, `benilla-ui/src/script/stdlib.rs`). message() calls DROPPED
|
||||
-- at their 3 sites (this file's header comment); the `return`s that followed them are kept.
|
||||
function BenillaTalentFrame_DrawLines(buttonTier, buttonColumn, tier, column, requirementsMet)
|
||||
if requirementsMet then
|
||||
requirementsMet = 1
|
||||
else
|
||||
requirementsMet = -1
|
||||
end
|
||||
-- ── Routing one prereq onto the grid ────────────────────────────────────────────────────
|
||||
--
|
||||
-- Spec: ref TalentFrame_DrawLines (l.398-527). A route is never stored as a path; each cell
|
||||
-- it crosses simply records which of its own half-branches and arrowheads the route lit,
|
||||
-- and the value recorded is the route's STATE — +1 met, -1 unmet — which is the very key
|
||||
-- the draw pass looks the art up by (`clearBranchCell` above).
|
||||
--
|
||||
-- The one rule every leg obeys: a route may not run through a cell that holds a talent of
|
||||
-- its own, because the connector art would land under that button. Each leg scans its span
|
||||
-- first and abandons the whole route rather than draw over one. (The reference raises a
|
||||
-- `message()` at three of those points; the diagnostic is dropped and the abandonment kept
|
||||
-- — this file's header comment, "message()". No 1.12 tree trips any of them.)
|
||||
--
|
||||
-- These five are the grid's vocabulary; the routing below is written entirely in them.
|
||||
|
||||
-- Check to see if are in the same column
|
||||
if buttonColumn == column then
|
||||
-- Check for blocking talents
|
||||
if (buttonTier - tier) > 1 then
|
||||
-- If more than one tier difference
|
||||
for i = tier + 1, buttonTier - 1 do
|
||||
if TALENT_BRANCH_ARRAY[i][buttonColumn].id then
|
||||
-- If there's an id, there's a blocker
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
-- One link of a vertical run: the lower half of the upper cell plus the upper half of the
|
||||
-- cell beneath it. Two half-branches are what make one drawn segment.
|
||||
local function linkDown(tier, column, state)
|
||||
TALENT_BRANCH_ARRAY[tier][column].down = state
|
||||
TALENT_BRANCH_ARRAY[tier + 1][column].up = state
|
||||
end
|
||||
|
||||
-- Draw the lines
|
||||
for i = tier, buttonTier - 1 do
|
||||
TALENT_BRANCH_ARRAY[i][buttonColumn].down = requirementsMet
|
||||
if (i + 1) <= (buttonTier - 1) then
|
||||
TALENT_BRANCH_ARRAY[i + 1][buttonColumn].up = requirementsMet
|
||||
end
|
||||
end
|
||||
-- The same across, left cell to right cell.
|
||||
local function linkRight(tier, column, state)
|
||||
TALENT_BRANCH_ARRAY[tier][column].right = state
|
||||
TALENT_BRANCH_ARRAY[tier][column + 1].left = state
|
||||
end
|
||||
|
||||
-- Set the arrow
|
||||
TALENT_BRANCH_ARRAY[buttonTier][buttonColumn].topArrow = requirementsMet
|
||||
return
|
||||
-- Does a talent sit anywhere in this span of one tier / of one column? (An empty span —
|
||||
-- adjacent cells with nothing between them — is trivially clear.)
|
||||
local function occupiedAcross(tier, fromColumn, toColumn)
|
||||
for i = fromColumn, toColumn do
|
||||
if TALENT_BRANCH_ARRAY[tier][i].id then return true end
|
||||
end
|
||||
-- Check to see if they're in the same tier
|
||||
if buttonTier == tier then
|
||||
local left = min(buttonColumn, column)
|
||||
local right = max(buttonColumn, column)
|
||||
return false
|
||||
end
|
||||
|
||||
-- See if the distance is greater than one space
|
||||
if (right - left) > 1 then
|
||||
-- Check for blocking talents
|
||||
for i = left + 1, right - 1 do
|
||||
if TALENT_BRANCH_ARRAY[tier][i].id then
|
||||
-- If there's an id, there's a blocker
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
-- If we get here then we're in the clear
|
||||
for i = left, right - 1 do
|
||||
TALENT_BRANCH_ARRAY[tier][i].right = requirementsMet
|
||||
TALENT_BRANCH_ARRAY[tier][i + 1].left = requirementsMet
|
||||
end
|
||||
-- Determine where the arrow goes
|
||||
if buttonColumn < column then
|
||||
TALENT_BRANCH_ARRAY[buttonTier][buttonColumn].rightArrow = requirementsMet
|
||||
else
|
||||
TALENT_BRANCH_ARRAY[buttonTier][buttonColumn].leftArrow = requirementsMet
|
||||
end
|
||||
return
|
||||
local function occupiedDown(column, fromTier, toTier)
|
||||
for i = fromTier, toTier do
|
||||
if TALENT_BRANCH_ARRAY[i][column].id then return true end
|
||||
end
|
||||
-- Now we know the prereq is diagonal from us
|
||||
local left = min(buttonColumn, column)
|
||||
local right = max(buttonColumn, column)
|
||||
-- Don't check the location of the current button
|
||||
if left == column then
|
||||
left = left + 1
|
||||
else
|
||||
right = right - 1
|
||||
end
|
||||
-- Check for blocking talents
|
||||
local blocked = nil
|
||||
for i = left, right do
|
||||
if TALENT_BRANCH_ARRAY[tier][i].id then
|
||||
-- If there's an id, there's a blocker
|
||||
blocked = 1
|
||||
end
|
||||
end
|
||||
left = min(buttonColumn, column)
|
||||
right = max(buttonColumn, column)
|
||||
if not blocked then
|
||||
TALENT_BRANCH_ARRAY[tier][buttonColumn].down = requirementsMet
|
||||
TALENT_BRANCH_ARRAY[buttonTier][buttonColumn].up = requirementsMet
|
||||
return false
|
||||
end
|
||||
|
||||
for i = tier, buttonTier - 1 do
|
||||
TALENT_BRANCH_ARRAY[i][buttonColumn].down = requirementsMet
|
||||
TALENT_BRANCH_ARRAY[i + 1][buttonColumn].up = requirementsMet
|
||||
end
|
||||
|
||||
for i = left, right - 1 do
|
||||
TALENT_BRANCH_ARRAY[tier][i].right = requirementsMet
|
||||
TALENT_BRANCH_ARRAY[tier][i + 1].left = requirementsMet
|
||||
end
|
||||
-- Place the arrow
|
||||
TALENT_BRANCH_ARRAY[buttonTier][buttonColumn].topArrow = requirementsMet
|
||||
return
|
||||
end
|
||||
-- If we're here then we were blocked trying to go vertically first so we have to go
|
||||
-- over first, then up
|
||||
if left == buttonColumn then
|
||||
left = left + 1
|
||||
else
|
||||
right = right - 1
|
||||
end
|
||||
-- Check for blocking talents
|
||||
for i = left, right do
|
||||
if TALENT_BRANCH_ARRAY[buttonTier][i].id then
|
||||
-- If there's an id, then this layout is undrawable
|
||||
return
|
||||
end
|
||||
end
|
||||
-- If we're here we can draw the line
|
||||
left = min(buttonColumn, column)
|
||||
right = max(buttonColumn, column)
|
||||
|
||||
for i = tier, buttonTier - 1 do
|
||||
TALENT_BRANCH_ARRAY[i][column].up = requirementsMet
|
||||
TALENT_BRANCH_ARRAY[i + 1][column].down = requirementsMet
|
||||
end
|
||||
|
||||
-- Determine where the arrow goes
|
||||
-- The arrowhead on the dependent talent's own cell, pointing back along the way the route
|
||||
-- came in — right if the prereq lies to the right of us, left if it lies to the left.
|
||||
local function pointArrowSideways(cell, buttonColumn, column, state)
|
||||
if buttonColumn < column then
|
||||
TALENT_BRANCH_ARRAY[buttonTier][buttonColumn].rightArrow = requirementsMet
|
||||
cell.rightArrow = state
|
||||
else
|
||||
TALENT_BRANCH_ARRAY[buttonTier][buttonColumn].leftArrow = requirementsMet
|
||||
cell.leftArrow = state
|
||||
end
|
||||
end
|
||||
|
||||
-- Mark the route from a prerequisite at (tier, column) to the talent that depends on it at
|
||||
-- (buttonTier, buttonColumn). Called once per prereq by BenillaTalentFrame_SetPrereqs.
|
||||
function BenillaTalentFrame_DrawLines(buttonTier, buttonColumn, tier, column, requirementsMet)
|
||||
local state = -1
|
||||
if requirementsMet then state = 1 end
|
||||
-- The dependent talent's own cell: SetPrereqs' caller has already stamped its `id`, so
|
||||
-- it always exists. Every branch below ends by marking an arrowhead on it.
|
||||
local cell = TALENT_BRANCH_ARRAY[buttonTier][buttonColumn]
|
||||
|
||||
-- STRAIGHT UP THE COLUMN. One vertical run, and the last link stops half short: the
|
||||
-- talent's own cell wears the arrowhead where an `up` stub would otherwise go.
|
||||
if buttonColumn == column then
|
||||
if occupiedDown(column, tier + 1, buttonTier - 1) then return end
|
||||
for i = tier, buttonTier - 1 do
|
||||
if i + 1 < buttonTier then
|
||||
linkDown(i, column, state)
|
||||
else
|
||||
TALENT_BRANCH_ARRAY[i][column].down = state
|
||||
end
|
||||
end
|
||||
cell.topArrow = state
|
||||
return
|
||||
end
|
||||
|
||||
-- ALONG THE TIER. One horizontal run between the two columns, arrowhead on the side
|
||||
-- the prereq lies. Both halves of every join are marked, including the talent's own.
|
||||
if buttonTier == tier then
|
||||
local left, right = min(buttonColumn, column), max(buttonColumn, column)
|
||||
if occupiedAcross(tier, left + 1, right - 1) then return end
|
||||
for i = left, right - 1 do
|
||||
linkRight(tier, i, state)
|
||||
end
|
||||
pointArrowSideways(cell, buttonColumn, column, state)
|
||||
return
|
||||
end
|
||||
|
||||
-- DIAGONAL, so the route must turn one corner, and there are two corners to choose
|
||||
-- from. Preferred: run ACROSS the prereq's own tier to our column, then DOWN it. What
|
||||
-- has to be clear for that is the prereq's tier from the cell NEXT to the prereq
|
||||
-- through to the corner at our own column — the prereq's own cell is where the route
|
||||
-- starts, so it is never in its own way.
|
||||
local left, right = min(buttonColumn, column), max(buttonColumn, column)
|
||||
local from, to = left, right
|
||||
if left == column then from = left + 1 else to = right - 1 end
|
||||
if not occupiedAcross(tier, from, to) then
|
||||
-- The corner and our own cell's top half, marked outright: the loop below covers
|
||||
-- both, EXCEPT when the prereq sits below us and the run is empty.
|
||||
TALENT_BRANCH_ARRAY[tier][buttonColumn].down = state
|
||||
cell.up = state
|
||||
for i = tier, buttonTier - 1 do
|
||||
linkDown(i, buttonColumn, state)
|
||||
end
|
||||
for i = left, right - 1 do
|
||||
linkRight(tier, i, state)
|
||||
end
|
||||
cell.topArrow = state
|
||||
return
|
||||
end
|
||||
|
||||
-- That corner is blocked, so take the other one: DOWN the prereq's column first, then
|
||||
-- across our own tier — which now has to be the clear span, from our cell to the corner.
|
||||
from, to = left, right
|
||||
if left == buttonColumn then from = left + 1 else to = right - 1 end
|
||||
if occupiedAcross(buttonTier, from, to) then return end
|
||||
-- Two reference quirks live in these three lines and BOTH are deliberately kept, because
|
||||
-- they are what the real client draws: the across leg is never marked at all (the
|
||||
-- reference's own two lines for it are commented out at Blizzard_TalentUI.lua:508-509),
|
||||
-- and each vertical segment is attributed to the OUTER halves of its two cells rather
|
||||
-- than the facing ones — the mirror image of `linkDown`, which is why that helper is not
|
||||
-- used here.
|
||||
for i = tier, buttonTier - 1 do
|
||||
TALENT_BRANCH_ARRAY[i][column].up = state
|
||||
TALENT_BRANCH_ARRAY[i + 1][column].down = state
|
||||
end
|
||||
pointArrowSideways(cell, buttonColumn, column, state)
|
||||
end
|
||||
|
||||
-- ref TalentFrameTalent_OnClick (Blizzard_TalentUITemplates.xml:97-99).
|
||||
function BenillaTalentButton_OnClick(button)
|
||||
LearnTalent(PanelTemplates_GetSelectedTab(BenillaTalentFrame), button:GetID())
|
||||
@@ -768,25 +824,19 @@
|
||||
BenillaTalentFrame.talentPoints = cp1
|
||||
end
|
||||
|
||||
-- ref SetTalentButtonLocation (l.549-553).
|
||||
-- Seat a talent button on its grid cell (spec: ref SetTalentButtonLocation, l.549-553) —
|
||||
-- the same corner the connector art insets from, so the two can never drift apart.
|
||||
function BenillaSetTalentButtonLocation(button, tier, column)
|
||||
local x = ((column - 1) * 63) + INITIAL_TALENT_OFFSET_X
|
||||
local y = -((tier - 1) * 63) - INITIAL_TALENT_OFFSET_Y
|
||||
local x, y = talentCellOrigin(tier, column)
|
||||
button:SetPoint("TOPLEFT", button:GetParent(), "TOPLEFT", x, y)
|
||||
end
|
||||
|
||||
-- ref TalentFrame_ResetBranches (l.555-568).
|
||||
-- Back to a blank grid before a repaint re-derives it (spec: ref TalentFrame_ResetBranches,
|
||||
-- l.555-568). The field set lives at `clearBranchCell`, shared with OnLoad's build.
|
||||
function BenillaTalentFrame_ResetBranches()
|
||||
for i = 1, MAX_NUM_TALENT_TIERS do
|
||||
for j = 1, NUM_TALENT_COLUMNS do
|
||||
TALENT_BRANCH_ARRAY[i][j].id = nil
|
||||
TALENT_BRANCH_ARRAY[i][j].up = 0
|
||||
TALENT_BRANCH_ARRAY[i][j].down = 0
|
||||
TALENT_BRANCH_ARRAY[i][j].left = 0
|
||||
TALENT_BRANCH_ARRAY[i][j].right = 0
|
||||
TALENT_BRANCH_ARRAY[i][j].rightArrow = 0
|
||||
TALENT_BRANCH_ARRAY[i][j].leftArrow = 0
|
||||
TALENT_BRANCH_ARRAY[i][j].topArrow = 0
|
||||
clearBranchCell(TALENT_BRANCH_ARRAY[i][j])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's taxi-map window (decision 0484 phase 2): a faithful port of the real 1.12
|
||||
`TaxiFrame.{xml,lua}` (patch chain; scratchpad `ref-TaxiFrame.xml/.lua`, line citations
|
||||
inline) onto the taxi engine seam (`crates/benilla-ui/src/script/taxi.rs`) fed by
|
||||
`crates/benilla/src/ui_taxi.rs`. The window art/geometry are quoted from the reference
|
||||
verbatim (384×512, the four TaxiFrame quadrant slabs at 256/128 split, the 316×352 map
|
||||
rect at TOP -13,-75) — the deviations below are all where THIS ENGINE, not taste, draws
|
||||
the line, each flagged and reasoned like every other transcribed window (Gossip/Merchant/
|
||||
Trainer's own header notes).
|
||||
benilla's taxi-map window (decision 0484 phase 2), written on the taxi engine seam
|
||||
(`crates/benilla-ui/src/script/taxi.rs`) fed by `crates/benilla/src/ui_taxi.rs`.
|
||||
|
||||
This is **our implementation**. Its specification is the real 1.12 `TaxiFrame.{xml,lua}`
|
||||
(patch chain; scratchpad `ref-TaxiFrame.xml/.lua`, line citations inline as `ref l.N`): the
|
||||
globals, the art paths, the geometry and the behaviour of every branch are the
|
||||
specification's, because addons and the engine both address this window by those names, and
|
||||
because the numbers are measurements of the shipped art (384×512, the four TaxiFrame
|
||||
quadrant slabs at a 256/128 split, the 316×352 map rect at TOP -13,-75) rather than
|
||||
anything for us to re-derive. How it is written is ours. The deviations below are where
|
||||
THIS ENGINE, not taste, draws the line, each flagged and reasoned like every other window
|
||||
of ours (Gossip/Merchant/Trainer's own header notes).
|
||||
|
||||
Modeled on MerchantFrame.xml/TrainerFrame.xml for the framing: the NPC-session lifecycle
|
||||
(RegisterEvent pair, OPENED/CLOSED dispatch, OnShow/OnHide PlaySound with a client-side
|
||||
@@ -30,8 +34,8 @@
|
||||
`TaxiNodes.dbc`'s per-continent max (43 nodes on Eastern Kingdoms, 39 on Kalimdor —
|
||||
counted this session, `crate::ui_taxi`'s module doc). The route-line textures stay
|
||||
genuinely dynamic (`TaxiRouteMap:CreateTexture(...)`, ref l.101/142) — plain
|
||||
region creation carries no template/inherits, so THAT half of the reference runs
|
||||
completely unmodified.
|
||||
region creation carries no template/inherits, so that half is created on demand exactly
|
||||
as specified.
|
||||
2. THE TITLE READS AN EVENT ARG, not `UnitName("npc")` (ref TaxiFrame.lua:27). This
|
||||
engine's `"npc"` unit token (decision 0421) only feeds the PORTRAIT bake — there is no
|
||||
backing `UnitState` for `UnitName` to read from it. Gossip/Merchant/Trainer hit the
|
||||
@@ -40,10 +44,10 @@
|
||||
(`BenillaTaxiFrame_SetTitle`, fed by `TAXIMAP_OPENED`'s arg1).
|
||||
3. (Retired.) The "no single-hop destination" refusal (`DrawOneHopLines`, ref l.161-164)
|
||||
used to call a plain `BenillaErrorsFrame_AddMessage` global because this engine had no
|
||||
MessageFrame widget for `UIErrorsFrame` to be. It has one now, so the line below is the
|
||||
reference's own `UIErrorsFrame:AddMessage(ERR_TAXINOPATHS, ...)`. `ErrorsFrame.xml` loads
|
||||
well before this file in the shipped list (`ui_script/mod.rs`), so the frame already
|
||||
exists by the time this ever runs.
|
||||
MessageFrame widget for `UIErrorsFrame` to be. It has one now, so the refusal below posts
|
||||
to `UIErrorsFrame` under the specified name. `ErrorsFrame.xml` loads well before this file
|
||||
in the shipped list (`ui_script/mod.rs`), so the frame already exists by the time this
|
||||
ever runs.
|
||||
|
||||
`<TaxiRouteFrame>` (an intrinsic type this engine doesn't have) becomes a plain `<Frame>`
|
||||
(`TaxiRouteMap`) per the house rule for every non-shipped intrinsic (decision 0246's
|
||||
@@ -52,34 +56,36 @@
|
||||
nothing.
|
||||
|
||||
Money (the fare) renders as plain text via `SetTooltipMoney` on the shared `GameTooltip`
|
||||
(`GameTooltip.xml`'s own seam) exactly like the reference — no `BenillaMoney_*` coin rig
|
||||
needed here (that lives on the merchant/trainer purse displays, a different job).
|
||||
(`GameTooltip.xml`'s own seam) exactly as specified — no `BenillaMoney_*` coin rig needed
|
||||
here (that lives on the merchant/trainer purse displays, a different job).
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- ref TaxiFrame.lua:2-3, verbatim.
|
||||
-- The continent map's size in pixels — a published pair, because scaling a node's
|
||||
-- normalised position onto the map needs them and so does anything that wants to put its own
|
||||
-- marker there. The values are the specification's (ref TaxiFrame.lua:2-3): a measurement of
|
||||
-- the shipped map art.
|
||||
TAXI_MAP_WIDTH = 316
|
||||
TAXI_MAP_HEIGHT = 352
|
||||
|
||||
-- The static node-button pool's size — see deviation 1 above.
|
||||
BENILLA_TAXI_MAX_NODES = 50
|
||||
|
||||
-- ref TaxiFrame.lua:7-16, verbatim (the three icon states — CURRENT green / REACHABLE
|
||||
-- white / DISTANT yellow — all three files verified present in the MPQ this session).
|
||||
TaxiButtonTypes = { }
|
||||
TaxiButtonTypes["CURRENT"] = {
|
||||
file = "Interface\\TaxiFrame\\UI-Taxi-Icon-Green"
|
||||
}
|
||||
TaxiButtonTypes["REACHABLE"] = {
|
||||
file = "Interface\\TaxiFrame\\UI-Taxi-Icon-White"
|
||||
}
|
||||
TaxiButtonTypes["DISTANT"] = {
|
||||
file = "Interface\\TaxiFrame\\UI-Taxi-Icon-Yellow"
|
||||
-- What each node state draws (ref TaxiFrame.lua:7-16): CURRENT green, REACHABLE white,
|
||||
-- DISTANT yellow — all three art files verified present in the MPQ. Published as a table of
|
||||
-- `{ file = }` keyed by the state string `TaxiNodeGetType` answers, which is both the shape
|
||||
-- the node paint below reads and the shape an addon adding a state of its own writes.
|
||||
TaxiButtonTypes = {
|
||||
CURRENT = { file = "Interface\\TaxiFrame\\UI-Taxi-Icon-Green" },
|
||||
REACHABLE = { file = "Interface\\TaxiFrame\\UI-Taxi-Icon-White" },
|
||||
DISTANT = { file = "Interface\\TaxiFrame\\UI-Taxi-Icon-Yellow" },
|
||||
}
|
||||
|
||||
-- Byte-exact 1.12 enUS GlobalStrings (interface.MPQ `GlobalStrings.lua`, line-cited).
|
||||
-- `TEXT(x)` (ref l.121) is the real client's build-time no-op localization macro — every
|
||||
-- `TEXT(FOO)` call is transcribed as the bare `FOO` global (CharacterFrame.xml's own
|
||||
-- convention); with no engine `TEXT` global at all, these are plain locals instead.
|
||||
-- The two strings this window shows, byte-exact from the 1.12 enUS GlobalStrings
|
||||
-- (interface.MPQ `GlobalStrings.lua`, line-cited) — user-visible text, so its bytes are the
|
||||
-- spec, not our expression. `TEXT(x)` (ref l.121) is the real client's build-time no-op
|
||||
-- localization macro, so every `TEXT(FOO)` resolves to the bare `FOO` global
|
||||
-- (CharacterFrame.xml's own convention); with no engine `TEXT` global at all, and nothing
|
||||
-- outside this window reading either string, they are plain locals here instead.
|
||||
local TAXINODEYOUAREHERE = "You are here" -- GlobalStrings.lua:4232
|
||||
-- GlobalStrings.lua:1864 — the reference's own byte, including its curly apostrophe
|
||||
-- (U+2019, "don\226\128\153t"), not a straight one.
|
||||
@@ -100,35 +106,37 @@
|
||||
getglobal("TaxiNameText"):SetText(name or "")
|
||||
end
|
||||
|
||||
-- ref TaxiFrame.lua:24-80, restructured only at the node-button loop (deviation 1 — a
|
||||
-- fixed pool walked 1..BENILLA_TAXI_MAX_NODES instead of grown via CreateFrame).
|
||||
-- Paint the node buttons (ref l.44-63): every pool slot up to the pushed node count takes
|
||||
-- that node's state icon and its pushed position, and the rest of the pool hides. Positions
|
||||
-- arrive normalised 0..1 from the map's BOTTOMLEFT, which is what the map size above scales.
|
||||
local function PaintNodes()
|
||||
local pushed = NumTaxiNodes()
|
||||
for index = 1, BENILLA_TAXI_MAX_NODES do
|
||||
local button = getglobal("TaxiButton" .. index)
|
||||
local state = TaxiNodeGetType(index)
|
||||
if index <= pushed and state ~= "NONE" then
|
||||
local x, y = TaxiNodePosition(button:GetID())
|
||||
button:ClearAllPoints()
|
||||
button:SetPoint("CENTER", "TaxiMap", "BOTTOMLEFT", x * TAXI_MAP_WIDTH, y * TAXI_MAP_HEIGHT)
|
||||
button:SetNormalTexture(TaxiButtonTypes[state].file)
|
||||
button:Show()
|
||||
else
|
||||
button:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Opening and closing the window (ref TaxiFrame.lua:24-80). Opening repaints everything from
|
||||
-- the pushed snapshot and then asks the panel manager for a slot; if it will not give one,
|
||||
-- the session is closed again rather than left running behind a window nobody can see.
|
||||
function TaxiFrame_OnEvent(event)
|
||||
local frame = this
|
||||
if event == "TAXIMAP_OPENED" then
|
||||
BenillaTaxiFrame_SetTitle(arg1)
|
||||
|
||||
-- Set the texture coords on the map, then hand the region to the app's art pick.
|
||||
-- Reset the map's sampling, then hand the region to the app's continent-art pick.
|
||||
TaxiMap:SetTexCoord(0, 1, 0, 1)
|
||||
SetTaxiMap(TaxiMap)
|
||||
|
||||
-- Draw nodes: every pool slot up to the pushed count shows its type's icon at its
|
||||
-- pushed position; the rest hide (ref l.44-63, the static-pool shape).
|
||||
local num_nodes = NumTaxiNodes()
|
||||
for index = 1, BENILLA_TAXI_MAX_NODES do
|
||||
local button = getglobal("TaxiButton" .. index)
|
||||
local ntype = TaxiNodeGetType(index)
|
||||
if index <= num_nodes and ntype ~= "NONE" then
|
||||
local x, y = TaxiNodePosition(button:GetID())
|
||||
button:ClearAllPoints()
|
||||
button:SetPoint("CENTER", "TaxiMap", "BOTTOMLEFT", x * TAXI_MAP_WIDTH, y * TAXI_MAP_HEIGHT)
|
||||
button:SetNormalTexture(TaxiButtonTypes[ntype].file)
|
||||
button:Show()
|
||||
else
|
||||
button:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- All set...
|
||||
PaintNodes()
|
||||
ShowUIPanel(frame)
|
||||
if not frame:IsVisible() then
|
||||
CloseTaxiMap()
|
||||
@@ -141,49 +149,70 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ref TaxiFrame.lua:82-126, verbatim. `this:GetID()` (l.96) reads the SAME button as the
|
||||
-- `button` parameter — the engine sets the shared `this` global to the invoking frame
|
||||
-- before OnEnter runs (decision 0068's event-dispatch convention), exactly like the real
|
||||
-- client, so the two reads agree.
|
||||
-- ── The route-line pool ──────────────────────────────────────────────────────────────────
|
||||
--
|
||||
-- A flight path draws as a chain of straight segments, one texture per segment. Those
|
||||
-- textures are a pool that grows to the longest chain ever asked for and is never shrunk:
|
||||
-- `NUM_TAXI_ROUTES` is how many exist and `TaxiRoute<n>` is each one's global name — both
|
||||
-- published, since the count is a global the specification exports and the names are what
|
||||
-- `getglobal` finds. Unlike the node buttons, this half really is created on demand: plain
|
||||
-- region creation carries no template, so deviation 1's CreateFrame wall never applies.
|
||||
NUM_TAXI_ROUTES = 0
|
||||
|
||||
-- Grow the pool to hold at least `n` lines; answer what it holds.
|
||||
local function GrowRoutePool(n)
|
||||
while NUM_TAXI_ROUTES < n do
|
||||
NUM_TAXI_ROUTES = NUM_TAXI_ROUTES + 1
|
||||
local line = TaxiRouteMap:CreateTexture("TaxiRoute" .. NUM_TAXI_ROUTES, "BACKGROUND")
|
||||
line:SetTexture("Interface\\TaxiFrame\\UI-Taxi-Line")
|
||||
end
|
||||
return NUM_TAXI_ROUTES
|
||||
end
|
||||
|
||||
-- Draw one line per `{ node, hop }` in `segments`, and hide whatever the pool holds beyond
|
||||
-- them. Each hop's endpoints arrive normalised on the map, so they scale by the canvas the
|
||||
-- lines live on. 32 is the specified line thickness (ref l.114/153), in those same pixels.
|
||||
local function PaintRoutes(segments)
|
||||
local count = table.getn(segments)
|
||||
local pool = GrowRoutePool(count)
|
||||
local w = TaxiRouteMap:GetWidth()
|
||||
local h = TaxiRouteMap:GetHeight()
|
||||
for i = 1, pool do
|
||||
local line = getglobal("TaxiRoute" .. i)
|
||||
if i <= count then
|
||||
local node, hop = segments[i][1], segments[i][2]
|
||||
DrawRouteLine(line, "TaxiRouteMap",
|
||||
TaxiGetSrcX(node, hop) * w, TaxiGetSrcY(node, hop) * h,
|
||||
TaxiGetDestX(node, hop) * w, TaxiGetDestY(node, hop) * h, 32)
|
||||
line:Show()
|
||||
else
|
||||
line:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- The tooltip, and the route, for the node under the cursor (ref TaxiFrame.lua:82-126). A
|
||||
-- reachable node prices the flight and draws every hop of it; the node we are standing on
|
||||
-- says so and draws the whole neighbourhood instead.
|
||||
--
|
||||
-- `this:GetID()` in the fare read is the SAME button as the `button` parameter — the engine
|
||||
-- binds the shared `this` global to the invoking frame before OnEnter runs (decision 0068's
|
||||
-- event-dispatch convention), exactly like the real client, so the two reads agree.
|
||||
function TaxiNodeOnButtonEnter(button)
|
||||
local index = button:GetID()
|
||||
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
|
||||
GameTooltip:AddLine(TaxiNodeName(index), "", 1.0, 1.0, 1.0)
|
||||
|
||||
-- Setup variables
|
||||
local numRoutes = GetNumRoutes(index)
|
||||
local line
|
||||
local sX, sY, dX, dY
|
||||
local w = TaxiRouteMap:GetWidth()
|
||||
local h = TaxiRouteMap:GetHeight()
|
||||
|
||||
local ntype = TaxiNodeGetType(index)
|
||||
if ntype == "REACHABLE" then
|
||||
local state = TaxiNodeGetType(index)
|
||||
if state == "REACHABLE" then
|
||||
SetTooltipMoney(GameTooltip, TaxiNodeCost(this:GetID()))
|
||||
TaxiNodeSetCurrent(index)
|
||||
|
||||
if numRoutes > NUM_TAXI_ROUTES then
|
||||
for i = NUM_TAXI_ROUTES + 1, numRoutes do
|
||||
line = TaxiRouteMap:CreateTexture("TaxiRoute" .. i, "BACKGROUND")
|
||||
line:SetTexture("Interface\\TaxiFrame\\UI-Taxi-Line")
|
||||
end
|
||||
NUM_TAXI_ROUTES = numRoutes
|
||||
local hops = { }
|
||||
for hop = 1, GetNumRoutes(index) do
|
||||
hops[hop] = { index, hop }
|
||||
end
|
||||
|
||||
for i = 1, NUM_TAXI_ROUTES do
|
||||
line = getglobal("TaxiRoute" .. i)
|
||||
if i <= numRoutes then
|
||||
sX = TaxiGetSrcX(index, i) * w
|
||||
sY = TaxiGetSrcY(index, i) * h
|
||||
dX = TaxiGetDestX(index, i) * w
|
||||
dY = TaxiGetDestY(index, i) * h
|
||||
DrawRouteLine(line, "TaxiRouteMap", sX, sY, dX, dY, 32)
|
||||
line:Show()
|
||||
else
|
||||
line:Hide()
|
||||
end
|
||||
end
|
||||
elseif ntype == "CURRENT" then
|
||||
PaintRoutes(hops)
|
||||
elseif state == "CURRENT" then
|
||||
GameTooltip:AddLine(TAXINODEYOUAREHERE, "", 0.5, 1.0, 0.5)
|
||||
DrawOneHopLines()
|
||||
end
|
||||
@@ -191,111 +220,142 @@
|
||||
GameTooltip:Show()
|
||||
end
|
||||
|
||||
NUM_TAXI_ROUTES = 0
|
||||
|
||||
-- Draw all flightpaths within one hop of current location — ref TaxiFrame.lua:129-165,
|
||||
-- verbatim except the canvas name ("TaxiRouteMap" -> "TaxiRouteMap").
|
||||
-- Every flight path within one hop of where we stand (ref TaxiFrame.lua:129-165) — what the
|
||||
-- map shows the moment it opens, before anything is hovered. A node exactly one segment away
|
||||
-- is one this flight master can send us to directly.
|
||||
function DrawOneHopLines()
|
||||
local line
|
||||
local sX, sY, dX, dY
|
||||
local w = TaxiRouteMap:GetWidth()
|
||||
local h = TaxiRouteMap:GetHeight()
|
||||
local numNodes = NumTaxiNodes()
|
||||
local numLines = 0
|
||||
local numSingleHops = 0
|
||||
for i = 1, numNodes do
|
||||
if GetNumRoutes(i) == 1 then
|
||||
numSingleHops = numSingleHops + 1
|
||||
numLines = numLines + 1
|
||||
if numLines > NUM_TAXI_ROUTES then
|
||||
line = TaxiRouteMap:CreateTexture("TaxiRoute" .. numLines, "BACKGROUND")
|
||||
line:SetTexture("Interface\\TaxiFrame\\UI-Taxi-Line")
|
||||
NUM_TAXI_ROUTES = numLines
|
||||
else
|
||||
line = getglobal("TaxiRoute" .. numLines)
|
||||
end
|
||||
if line then
|
||||
sX = TaxiGetSrcX(i, 1) * w
|
||||
sY = TaxiGetSrcY(i, 1) * h
|
||||
dX = TaxiGetDestX(i, 1) * w
|
||||
dY = TaxiGetDestY(i, 1) * h
|
||||
DrawRouteLine(line, "TaxiRouteMap", sX, sY, dX, dY, 32)
|
||||
line:Show()
|
||||
end
|
||||
local hops = { }
|
||||
for node = 1, NumTaxiNodes() do
|
||||
if GetNumRoutes(node) == 1 then
|
||||
table.insert(hops, { node, 1 })
|
||||
end
|
||||
end
|
||||
for i = numLines + 1, NUM_TAXI_ROUTES do
|
||||
getglobal("TaxiRoute" .. i):Hide()
|
||||
end
|
||||
if numSingleHops == 0 then
|
||||
PaintRoutes(hops)
|
||||
|
||||
-- A flight point with nothing one hop away is a dead end: say so, and take the window
|
||||
-- back down rather than leave an empty map up.
|
||||
if table.getn(hops) == 0 then
|
||||
UIErrorsFrame:AddMessage(ERR_TAXINOPATHS, 1.0, 0.1, 0.1)
|
||||
HideUIPanel(TaxiFrame)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- The following function is used with permission from Daniel Stephens
|
||||
-- <iriel@vigilance-committee.org> — ref TaxiFrame.lua:168-226, byte-for-byte verbatim (pure
|
||||
-- Lua line-quad math over the engine's 8-arg affine SetTexCoord; no engine dependency beyond
|
||||
-- what the phase-1 seam test already proved: CreateTexture, SetTexCoord's 8-arg form,
|
||||
-- SetPoint with a string canvas name).
|
||||
TAXIROUTE_LINEFACTOR = 32/30 -- Multiplying factor for texture coordinates
|
||||
TAXIROUTE_LINEFACTOR_2 = TAXIROUTE_LINEFACTOR / 2 -- Half o that
|
||||
-- ── Route lines ───────────────────────────────────────────────────────────────────────
|
||||
--
|
||||
-- A flight path is one straight line between two map points, drawn as a texture. The
|
||||
-- engine can only place an AXIS-ALIGNED rectangle, so the rotation has to live in the
|
||||
-- texture coordinates instead: SetTexCoord's 8-argument form places each of the four
|
||||
-- source corners independently, which is exactly enough to spin (and shear) the art
|
||||
-- inside its bounding box. The bounding box then has to grow to hold the rotated line.
|
||||
--
|
||||
-- The geometry — how a rotation angle turns into those four corners and that box — is
|
||||
-- the specification, and it is the reference's (ref TaxiFrame.lua:168-226; the credit
|
||||
-- their copy carries is to Daniel Stephens <iriel@vigilance-committee.org>). Every value
|
||||
-- below is theirs, and the multiplications are grouped the way theirs are so the rounding
|
||||
-- matches; how it is written — the naming, the shear terms hoisted out of both branches,
|
||||
-- the anchoring helper — is ours. The reference derives each corner from the last
|
||||
-- (`BRx = 1 - BLy`, `TRy = BRx`, …); we state each one directly instead, which is the
|
||||
-- same number everywhere but one: the downward branch's top-left U is `-shearU` here
|
||||
-- against their `1 - (1 + shearU)`. MEASURED, both implementations run side by side on
|
||||
-- the real Lua 5.1 over 50k randomised segments plus the degenerate/axis-aligned/reversed
|
||||
-- edges: every anchor identical to the last bit, and every texture coordinate identical
|
||||
-- once narrowed to f32 — which the engine does to all eight on the way in
|
||||
-- (`region::paint`'s `Variadic<f32>`). The lone survivor is the SIGN OF ZERO on an exactly
|
||||
-- vertical downward line, where that corner is -0 here and +0 there: the same coordinate,
|
||||
-- and the same sample.
|
||||
--
|
||||
-- Engine seam, all three proved by the phase-1 seam test: CreateTexture, SetTexCoord's
|
||||
-- 8-arg form, and SetPoint against a canvas named by string.
|
||||
|
||||
-- T - Texture
|
||||
-- C - Canvas Frame (for anchoring)
|
||||
-- sx,sy - Coordinate of start of line
|
||||
-- ex,ey - Coordinate of end of line
|
||||
-- w - Width of line
|
||||
-- relPoint - Relative point on canvas to interpret coords (Default BOTTOMLEFT)
|
||||
function DrawRouteLine(T, C, sx, sy, ex, ey, w, relPoint)
|
||||
if (not relPoint) then relPoint = "BOTTOMLEFT"; end
|
||||
-- The reference's two scale globals, value for value. Only the halved one is ever read —
|
||||
-- by the bounding-box extents below, which are half-widths, hence the /2; the whole one
|
||||
-- is a published global that nothing in the reference reads either. 32/30 is unexplained
|
||||
-- there and carried unchanged here — a measurement of the shipped art, not ours to
|
||||
-- re-derive.
|
||||
TAXIROUTE_LINEFACTOR = 32/30
|
||||
TAXIROUTE_LINEFACTOR_2 = TAXIROUTE_LINEFACTOR / 2
|
||||
|
||||
-- Determine dimensions and center point of line
|
||||
local dx,dy = ex - sx, ey - sy;
|
||||
local cx,cy = (sx + ex) / 2, (sy + ey) / 2;
|
||||
-- Seat `texture` as the box centred on (cx, cy) that reaches `halfW` / `halfH` out along
|
||||
-- each axis, measured from `canvas`'s `relPoint` corner. Both ends always move together,
|
||||
-- so clearing first is what keeps a stale anchor from outliving a move. (The reference
|
||||
-- re-seats the two points without clearing on its degenerate path; a route texture only
|
||||
-- ever holds these two anchors and SetPoint replaces one of the same name, so the box
|
||||
-- that comes out is the same either way.)
|
||||
local function AnchorCenteredBox(texture, canvas, relPoint, cx, cy, halfW, halfH)
|
||||
texture:ClearAllPoints()
|
||||
texture:SetPoint("BOTTOMLEFT", canvas, relPoint, cx - halfW, cy - halfH)
|
||||
texture:SetPoint("TOPRIGHT", canvas, relPoint, cx + halfW, cy + halfH)
|
||||
end
|
||||
|
||||
-- Normalize direction if necessary
|
||||
if (dx < 0) then
|
||||
dx,dy = -dx,-dy;
|
||||
end
|
||||
-- Draw the segment (sx,sy) -> (ex,ey), `width` thick, into `texture`, placed against
|
||||
-- `canvas` in the coordinate space of its `relPoint` corner (BOTTOMLEFT by default).
|
||||
function DrawRouteLine(texture, canvas, sx, sy, ex, ey, width, relPoint)
|
||||
relPoint = relPoint or "BOTTOMLEFT"
|
||||
|
||||
-- Calculate actual length of line
|
||||
local l = sqrt((dx * dx) + (dy * dy));
|
||||
local dx, dy = ex - sx, ey - sy
|
||||
local cx, cy = (sx + ex) / 2, (sy + ey) / 2
|
||||
|
||||
-- Quick escape if it's zero length
|
||||
if (l == 0) then
|
||||
T:SetTexCoord(0,0,0,0,0,0,0,0);
|
||||
T:SetPoint("BOTTOMLEFT", C, relPoint, cx,cy);
|
||||
T:SetPoint("TOPRIGHT", C, relPoint, cx,cy);
|
||||
return;
|
||||
end
|
||||
-- A segment drawn back-to-front is the same segment: face it rightwards so only the
|
||||
-- two upward/downward cases below have to exist.
|
||||
if dx < 0 then
|
||||
dx, dy = -dx, -dy
|
||||
end
|
||||
|
||||
-- Sin and Cosine of rotation, and combination (for later)
|
||||
local s,c = -dy / l, dx / l;
|
||||
local sc = s * c;
|
||||
local len = sqrt((dx * dx) + (dy * dy))
|
||||
if len == 0 then
|
||||
-- Nothing to draw: collapse the quad onto its own midpoint and show no art.
|
||||
texture:SetTexCoord(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
AnchorCenteredBox(texture, canvas, relPoint, cx, cy, 0, 0)
|
||||
return
|
||||
end
|
||||
|
||||
-- Calculate bounding box size and texture coordinates
|
||||
local Bwid, Bhgt, BLx, BLy, TLx, TLy, TRx, TRy, BRx, BRy;
|
||||
if (dy >= 0) then
|
||||
Bwid = ((l * c) - (w * s)) * TAXIROUTE_LINEFACTOR_2;
|
||||
Bhgt = ((w * c) - (l * s)) * TAXIROUTE_LINEFACTOR_2;
|
||||
BLx, BLy, BRy = (w / l) * sc, s * s, (l / w) * sc;
|
||||
BRx, TLx, TLy, TRx = 1 - BLy, BLy, 1 - BRy, 1 - BLx;
|
||||
TRy = BRx;
|
||||
else
|
||||
Bwid = ((l * c) + (w * s)) * TAXIROUTE_LINEFACTOR_2;
|
||||
Bhgt = ((w * c) + (l * s)) * TAXIROUTE_LINEFACTOR_2;
|
||||
BLx, BLy, BRx = s * s, -(l / w) * sc, 1 + (w / l) * sc;
|
||||
BRy, TLx, TLy, TRy = BLx, 1 - BRx, 1 - BLx, 1 - BLy;
|
||||
TRx = TLy;
|
||||
end
|
||||
-- Sine and cosine of the segment's angle, and the two shear terms every corner is
|
||||
-- built from — one walking the texture's horizontal axis, one its vertical axis.
|
||||
local sinA, cosA = -dy / len, dx / len
|
||||
local sinSq = sinA * sinA
|
||||
local shear = sinA * cosA
|
||||
local shearU = (width / len) * shear
|
||||
local shearV = (len / width) * shear
|
||||
|
||||
-- Set texture coordinates and anchors
|
||||
T:ClearAllPoints();
|
||||
T:SetTexCoord(TLx, TLy, BLx, BLy, TRx, TRy, BRx, BRy);
|
||||
T:SetPoint("BOTTOMLEFT", C, relPoint, cx - Bwid, cy - Bhgt);
|
||||
T:SetPoint("TOPRIGHT", C, relPoint, cx + Bwid, cy + Bhgt);
|
||||
-- Half the bounding box the rotated line needs, and the four source corners that lay
|
||||
-- the art across it. Upward and downward are the same algebra mirrored: the four
|
||||
-- quantities are the same, and which corner each lands on is what flips.
|
||||
local halfW, halfH
|
||||
local blU, blV, tlU, tlV, trU, trV, brU, brV
|
||||
if dy >= 0 then
|
||||
halfW = ((len * cosA) - (width * sinA)) * TAXIROUTE_LINEFACTOR_2
|
||||
halfH = ((width * cosA) - (len * sinA)) * TAXIROUTE_LINEFACTOR_2
|
||||
blU, blV = shearU, sinSq
|
||||
tlU, tlV = sinSq, 1 - shearV
|
||||
trU, trV = 1 - shearU, 1 - sinSq
|
||||
brU, brV = 1 - sinSq, shearV
|
||||
else
|
||||
halfW = ((len * cosA) + (width * sinA)) * TAXIROUTE_LINEFACTOR_2
|
||||
halfH = ((width * cosA) + (len * sinA)) * TAXIROUTE_LINEFACTOR_2
|
||||
blU, blV = sinSq, -shearV
|
||||
tlU, tlV = -shearU, 1 - sinSq
|
||||
trU, trV = 1 - sinSq, 1 + shearV
|
||||
brU, brV = 1 + shearU, sinSq
|
||||
end
|
||||
|
||||
-- SetTexCoord's corner order is top-left, bottom-left, top-right, bottom-right.
|
||||
texture:SetTexCoord(tlU, tlV, blU, blV, trU, trV, brU, brV)
|
||||
AnchorCenteredBox(texture, canvas, relPoint, cx, cy, halfW, halfH)
|
||||
end
|
||||
|
||||
-- Appearing and disappearing. The sounds are the MainMenu pair, not the CharacterInfo pair
|
||||
-- Merchant/Trainer use — the specification genuinely names a different one for this window
|
||||
-- (ref l.162-169). Showing also draws the neighbourhood straight away, so an untouched map
|
||||
-- is not blank; hiding ends the session, which is what fires TAXIMAP_CLOSED back at us.
|
||||
function BenillaTaxiFrame_OnShow()
|
||||
PlaySound("igMainMenuOpen")
|
||||
DrawOneHopLines()
|
||||
end
|
||||
|
||||
function BenillaTaxiFrame_OnHide()
|
||||
CloseTaxiMap()
|
||||
PlaySound("igMainMenuClose")
|
||||
end
|
||||
|
||||
function BenillaTaxiCloseButton_OnClick()
|
||||
@@ -308,12 +368,12 @@
|
||||
|
||||
<!--
|
||||
The node-button pool's virtual template (ref TaxiButtonTemplate, l.4-32): a 16×16 button,
|
||||
the type icon assigned per repaint via SetNormalTexture (deviation 1's static pool — see
|
||||
the header note), the 32×32 CENTER highlight glow. OnEnter/OnLeave/OnClick are the ref's,
|
||||
byte for byte (l.12-20) — including the click firing UNCONDITIONALLY regardless of node
|
||||
type, exactly like the reference (a DISTANT/CURRENT click still queues TakeTaxiNode; the
|
||||
app-side drain treats a routeless click as a no-op — there is no RE'd finding for what the
|
||||
real client's native TakeTaxiNode does with an unreachable index, so this is the safe,
|
||||
the state icon assigned per repaint via SetNormalTexture (deviation 1's static pool — see
|
||||
the header note), the 32×32 CENTER highlight glow. The three handlers dispatch to the same
|
||||
three verbs the specification's do (l.12-20) — including the click firing UNCONDITIONALLY
|
||||
regardless of node state (a DISTANT/CURRENT click still queues TakeTaxiNode; the app-side
|
||||
drain treats a routeless click as a no-op — there is no RE'd finding for what the real
|
||||
client's native TakeTaxiNode does with an unreachable index, so this is the safe,
|
||||
undocumented-but-reasonable inference, not an asserted fact).
|
||||
-->
|
||||
<Button name="BenillaTaxiButtonTemplate" hidden="true" virtual="true">
|
||||
@@ -325,13 +385,13 @@
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnEnter>
|
||||
TaxiNodeOnButtonEnter(this);
|
||||
TaxiNodeOnButtonEnter(this)
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
GameTooltip:Hide();
|
||||
GameTooltip:Hide()
|
||||
</OnLeave>
|
||||
<OnClick>
|
||||
TakeTaxiNode(this:GetID());
|
||||
TakeTaxiNode(this:GetID())
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
<HighlightTexture file="Interface\TaxiFrame\UI-Taxi-Icon-Highlight">
|
||||
@@ -486,15 +546,11 @@
|
||||
<OnEvent>
|
||||
TaxiFrame_OnEvent(event)
|
||||
</OnEvent>
|
||||
<!-- ref l.162-169, verbatim: the MainMenu sound kit (not Merchant/Trainer's
|
||||
CharacterInfo kit — the reference genuinely uses a different pair here). -->
|
||||
<OnShow>
|
||||
PlaySound("igMainMenuOpen");
|
||||
DrawOneHopLines();
|
||||
BenillaTaxiFrame_OnShow()
|
||||
</OnShow>
|
||||
<OnHide>
|
||||
CloseTaxiMap();
|
||||
PlaySound("igMainMenuClose")
|
||||
BenillaTaxiFrame_OnHide()
|
||||
</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<!--
|
||||
The shared status-bar numerals machinery (ref-TextStatusBar.lua + ref-TextStatusBar.xml,
|
||||
transcribed whole — decision 1082). A bar opts in by calling TextStatusBar_Initialize() in its
|
||||
OnLoad (setting `textLockable`), wiring a FontString via SetTextStatusBarText(bar, text), and
|
||||
routing its OnEnter/OnLeave/OnValueChanged through the Show/Hide/Update functions here. The
|
||||
text is "value / max", shown while hovered (a lockShow refcount) or always when the
|
||||
`statusBarText` cvar is "1" ("Status Text" in the options; our GetCVar returns nil for a cvar
|
||||
the engine doesn't carry, which reads as OFF — the 1.12 default).
|
||||
The shared status-bar readout machinery — the contract 1.12's TextStatusBar.lua/.xml defines
|
||||
(decision 1082), written here in our own terms. A bar opts in by calling
|
||||
TextStatusBar_Initialize() in its OnLoad (setting `textLockable`), wiring a FontString via
|
||||
SetTextStatusBarText(bar, text), and routing its OnEnter/OnLeave/OnValueChanged through the
|
||||
Show/Hide/Update functions here. The text is "value / max", shown while hovered (a lockShow
|
||||
refcount) or always when the `statusBarText` cvar is "1" ("Status Text" in the options; our
|
||||
GetCVar returns nil for a cvar the engine doesn't carry, which reads as OFF — the 1.12
|
||||
default).
|
||||
|
||||
Customers: the XP bar (ActionBar.xml) and, since decision 1143, the player and pet frames'
|
||||
health/power bars (UnitFrames.xml). `textLockable` is what the statusBarText cvar pins, and
|
||||
@@ -16,6 +17,19 @@
|
||||
-->
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<Script><![CDATA[
|
||||
-- The readout has exactly three reasons to be on screen, and every function below is some
|
||||
-- combination of them: the Status Text option pins it (only on a bar that opted in with
|
||||
-- `textLockable`), the pointer is on the bar (a refcount, so nested hovers balance), or the
|
||||
-- bar is showing its zero text — a phrase rather than a number, which stands on its own.
|
||||
--
|
||||
-- The option test is the one of the three that reads from outside the bar, and it is asked
|
||||
-- from three places, so it lives here once. It takes the option's VALUE rather than calling
|
||||
-- GetCVar itself: the CVAR_UPDATE handler is handed the new value along with the event and
|
||||
-- must not go asking the engine for the one it is being told about.
|
||||
local function optionPins(bar, setting)
|
||||
return setting == "1" and bar.textLockable
|
||||
end
|
||||
|
||||
-- `bar` is optional and falls back to `this`, the same shape TextStatusBar_UpdateTextString
|
||||
-- below already has (and for the same reason): the ref calls this from a bar's own OnLoad,
|
||||
-- where `this` is the bar, but our unit frames wire their children from ONE shared
|
||||
@@ -28,6 +42,8 @@
|
||||
bar.lockShow = 0
|
||||
end
|
||||
|
||||
-- Hand a bar the FontString its readout lives in. A bar without one is never an error
|
||||
-- below — it simply has nothing to say, and every entry point here checks for that.
|
||||
function SetTextStatusBarText(bar, text)
|
||||
if not bar or not text then
|
||||
return
|
||||
@@ -36,49 +52,58 @@
|
||||
end
|
||||
|
||||
function TextStatusBar_OnEvent(cvar, value)
|
||||
if event == "CVAR_UPDATE" and cvar == "STATUS_BAR_TEXT" then
|
||||
if this.TextString then
|
||||
if value == "1" and this.textLockable then
|
||||
this.TextString:Show()
|
||||
elseif this.lockShow == 0 then
|
||||
this.TextString:Hide()
|
||||
end
|
||||
end
|
||||
if event ~= "CVAR_UPDATE" or cvar ~= "STATUS_BAR_TEXT" or not this.TextString then
|
||||
return
|
||||
end
|
||||
-- Switching the option ON pins the readout up at once. Switching it off only puts the
|
||||
-- readout away when nothing else is holding it there: a hover in progress keeps its own,
|
||||
-- and gets to hide it when it ends.
|
||||
if optionPins(this, value) then
|
||||
this.TextString:Show()
|
||||
elseif this.lockShow == 0 then
|
||||
this.TextString:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- Repaint one bar's readout from the value it is currently carrying. `textStatusBar` is
|
||||
-- optional and falls back to `this` for the same reason TextStatusBar_Initialize's is.
|
||||
function TextStatusBar_UpdateTextString(textStatusBar)
|
||||
if not textStatusBar then
|
||||
textStatusBar = this
|
||||
end
|
||||
local string = textStatusBar.TextString
|
||||
if string then
|
||||
local value = textStatusBar:GetValue()
|
||||
local valueMin, valueMax = textStatusBar:GetMinMaxValues()
|
||||
if valueMax > 0 then
|
||||
textStatusBar:Show()
|
||||
if value == 0 and textStatusBar.zeroText then
|
||||
string:SetText(textStatusBar.zeroText)
|
||||
textStatusBar.isZero = 1
|
||||
string:Show()
|
||||
else
|
||||
textStatusBar.isZero = nil
|
||||
if textStatusBar.prefix then
|
||||
string:SetText(textStatusBar.prefix .. " " .. value .. " / " .. valueMax)
|
||||
else
|
||||
string:SetText(value .. " / " .. valueMax)
|
||||
end
|
||||
if GetCVar("statusBarText") == "1" and textStatusBar.textLockable then
|
||||
string:Show()
|
||||
elseif textStatusBar.lockShow > 0 then
|
||||
string:Show()
|
||||
else
|
||||
string:Hide()
|
||||
end
|
||||
end
|
||||
else
|
||||
textStatusBar:Hide()
|
||||
end
|
||||
local readout = textStatusBar.TextString
|
||||
if not readout then
|
||||
return
|
||||
end
|
||||
local value = textStatusBar:GetValue()
|
||||
local _, valueMax = textStatusBar:GetMinMaxValues()
|
||||
-- No ceiling is not the same thing as a bar at zero (below): it means the bar does not
|
||||
-- apply to this unit at all, so the BAR leaves the screen rather than reading out a
|
||||
-- number. Its readout is left exactly as it stands and comes back with it.
|
||||
if valueMax <= 0 then
|
||||
textStatusBar:Hide()
|
||||
return
|
||||
end
|
||||
textStatusBar:Show()
|
||||
if value == 0 and textStatusBar.zeroText then
|
||||
-- Empty, and this bar has a phrase for empty. The phrase displaces the numbers and
|
||||
-- is shown outright: it is a message, so neither the option nor a hover gates it,
|
||||
-- and `isZero` is the flag that tells the hover not to take it away again.
|
||||
textStatusBar.isZero = 1
|
||||
readout:SetText(textStatusBar.zeroText)
|
||||
readout:Show()
|
||||
return
|
||||
end
|
||||
textStatusBar.isZero = nil
|
||||
if textStatusBar.prefix then
|
||||
readout:SetText(textStatusBar.prefix .. " " .. value .. " / " .. valueMax)
|
||||
else
|
||||
readout:SetText(value .. " / " .. valueMax)
|
||||
end
|
||||
if optionPins(textStatusBar, GetCVar("statusBarText")) or textStatusBar.lockShow > 0 then
|
||||
readout:Show()
|
||||
else
|
||||
readout:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -98,31 +123,29 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- One more hold on the readout. `lockShow` counts rather than flags because two things can
|
||||
-- be holding it open at once: the character sheet takes a hold for as long as it is open
|
||||
-- (CharacterFrame_OnShow, decision 1146) and a hover on the same bar takes a second one on
|
||||
-- top. Only the last release puts the readout away, so the hover still balances.
|
||||
function ShowTextStatusBarText(bar)
|
||||
if bar and bar.TextString then
|
||||
if not bar.lockShow then
|
||||
bar.lockShow = 0
|
||||
end
|
||||
bar.lockShow = (bar.lockShow or 0) + 1
|
||||
bar.TextString:Show()
|
||||
bar.lockShow = bar.lockShow + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- One hold released. The count floors at none rather than going negative, so an unbalanced
|
||||
-- release — a frame hidden out from under a hover — cannot strand the readout on screen.
|
||||
function HideTextStatusBarText(bar)
|
||||
if bar and bar.TextString then
|
||||
if not bar.lockShow then
|
||||
bar.lockShow = 0
|
||||
end
|
||||
if bar.lockShow > 0 then
|
||||
bar.lockShow = bar.lockShow - 1
|
||||
end
|
||||
-- The ref reads `this.isZero` here — a leak of the handler's own frame into a
|
||||
-- helper that otherwise takes `bar`. Equivalent at every call site (each caller
|
||||
-- passes its handler's own frame), so it is written as `bar` — which also keeps
|
||||
-- the function callable outside a handler, where `this` is nil.
|
||||
if bar.lockShow > 0 or bar.isZero == 1 then
|
||||
bar.TextString:Show()
|
||||
elseif GetCVar("statusBarText") == "1" and bar.textLockable then
|
||||
bar.lockShow = math.max((bar.lockShow or 0) - 1, 0)
|
||||
-- Releasing a hold only puts the readout away if none of the three reasons is left.
|
||||
-- The ref reads `this.isZero` for the middle one — a leak of the handler's own frame
|
||||
-- into a helper that otherwise takes `bar`. Equivalent at every call site (each
|
||||
-- caller passes its handler's own frame), so it is written as `bar`, which also
|
||||
-- keeps the function callable outside a handler, where `this` is nil.
|
||||
if bar.lockShow > 0 or bar.isZero == 1
|
||||
or optionPins(bar, GetCVar("statusBarText")) then
|
||||
bar.TextString:Show()
|
||||
else
|
||||
bar.TextString:Hide()
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
<Script><![CDATA[
|
||||
BENILLA_MAX_TRADE_ITEMS = 7 -- MAX_TRADE_ITEMS (ref TradeFrame.lua l.1)
|
||||
|
||||
-- One side's accept glow. A column's highlight and the enchant slot's below it are one
|
||||
-- indicator in two frames — they light and drop together, never apart — so both the repaint
|
||||
-- and TradeFrame_SetAcceptState go through here rather than each spelling out four calls.
|
||||
-- The four frames are TradeHighlight{Player,Recipient}[Enchant], declared at the bottom.
|
||||
local function setTradeGlow(column, enchantSlot, lit)
|
||||
if lit then
|
||||
column:Show()
|
||||
enchantSlot:Show()
|
||||
else
|
||||
column:Hide()
|
||||
enchantSlot:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- ── Load / event dispatch (ref TradeFrame_OnLoad l.4 / TradeFrame_OnEvent l.18) ───────────
|
||||
function TradeFrame_OnLoad()
|
||||
local frame = this
|
||||
@@ -100,10 +114,10 @@
|
||||
-- Our gold is the editable input, reflected via PLAYER_TRADE_MONEY (not repainted here, so
|
||||
-- the repaint never clobbers mid-typing); only the partner's read-only gold repaints.
|
||||
BenillaMoney_Set("TradeRecipientMoneyFrame", GetTargetTradeMoney())
|
||||
TradeHighlightPlayer:Hide()
|
||||
TradeHighlightRecipient:Hide()
|
||||
TradeHighlightPlayerEnchant:Hide()
|
||||
TradeHighlightRecipientEnchant:Hide()
|
||||
-- A repaint puts both glows down. TRADE_ACCEPT_UPDATE is the only thing that raises
|
||||
-- them, so whatever is standing comes straight back on the next one.
|
||||
setTradeGlow(TradeHighlightPlayer, TradeHighlightPlayerEnchant, false)
|
||||
setTradeGlow(TradeHighlightRecipient, TradeHighlightRecipientEnchant, false)
|
||||
end
|
||||
|
||||
-- One slot's paint, shared by both columns (ref TradeFrame_UpdatePlayerItem l.54 /
|
||||
@@ -178,36 +192,36 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- The accept glow (ref TradeFrame_SetAcceptState l.116): each side's column + enchant-slot
|
||||
-- highlight tracks who has pressed Trade; our own accept disables the Trade button.
|
||||
-- Who has pressed Trade (spec: ref TradeFrame_SetAcceptState l.116). The event carries BOTH
|
||||
-- sides' states, so this is a straight repaint of two indicators rather than a toggle — and
|
||||
-- our own half is kept on the frame, because the Cancel button below is the only reader of
|
||||
-- whether there is an accept of ours to withdraw.
|
||||
--
|
||||
-- The extra thing our own accept does is lock the Trade button, so a standing accept cannot
|
||||
-- be sent a second time; Cancel is what takes it back.
|
||||
function TradeFrame_SetAcceptState(playerState, targetState)
|
||||
TradeFrame.acceptState = playerState
|
||||
if playerState == 1 then
|
||||
TradeHighlightPlayer:Show()
|
||||
TradeHighlightPlayerEnchant:Show()
|
||||
local weAccepted = playerState == 1
|
||||
setTradeGlow(TradeHighlightPlayer, TradeHighlightPlayerEnchant, weAccepted)
|
||||
setTradeGlow(TradeHighlightRecipient, TradeHighlightRecipientEnchant, targetState == 1)
|
||||
if weAccepted then
|
||||
TradeFrameTradeButton:Disable()
|
||||
else
|
||||
TradeHighlightPlayer:Hide()
|
||||
TradeHighlightPlayerEnchant:Hide()
|
||||
TradeFrameTradeButton:Enable()
|
||||
end
|
||||
if targetState == 1 then
|
||||
TradeHighlightRecipient:Show()
|
||||
TradeHighlightRecipientEnchant:Show()
|
||||
else
|
||||
TradeHighlightRecipient:Hide()
|
||||
TradeHighlightRecipientEnchant:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- Cancel (ref TradeFrameCancelButton_OnClick l.136): with our accept up, drop it (stay in the
|
||||
-- trade); otherwise close the window (→ OnHide → CloseTrade).
|
||||
-- Cancel is two verbs on one button (spec: ref TradeFrameCancelButton_OnClick l.136). With
|
||||
-- an accept of ours standing it withdraws only THAT and the trade stays open — the button
|
||||
-- is the only way back out of the lock above. With no accept up there is nothing to
|
||||
-- withdraw, so it means "leave", and closing the window is what sends that (→ OnHide →
|
||||
-- CloseTrade).
|
||||
function TradeFrameCancelButton_OnClick()
|
||||
if TradeFrame.acceptState == 1 then
|
||||
CancelTradeAccept()
|
||||
else
|
||||
if TradeFrame.acceptState ~= 1 then
|
||||
HideUIPanel(TradeFrame)
|
||||
return
|
||||
end
|
||||
CancelTradeAccept()
|
||||
end
|
||||
]]></Script>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Ui>
|
||||
<!--
|
||||
benilla's crafting window (decision 0437 phase 2): the 1.12 `TradeSkillFrame`, transcribed from
|
||||
benilla's crafting window (decision 0437 phase 2): the 1.12 `TradeSkillFrame`, re-expressed from
|
||||
the reference `Blizzard_TradeSkillUI.xml`/`.lua` (session scratchpad `ref-ui/`, decision 0246
|
||||
discipline — never committed) onto our own engine/feeds. Geometry/art-paths/structure below are
|
||||
QUOTED from the ref (line citations inline); the Lua is transcribed logic turned onto benilla's
|
||||
QUOTED from the ref (line citations inline); the Lua is the ref's logic turned onto benilla's
|
||||
Era API (`GetTradeSkillLine`/`GetNumTradeSkills`/`GetTradeSkillInfo`/reagents/tools/cooldown/
|
||||
`DoTradeSkill`/`CloseTradeSkill`, decision 0437's engine surface, built alongside this file).
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
earlier revision named it an INTERIM pinned to the dispatch; folded back this session).
|
||||
`GetTradeSkillInfo`'s `skillType` is `"header"` for a group row (the created item's
|
||||
`(ItemClass, ItemSubClass)`, named from `ItemSubClass.dbc`, engine-sorted) — the ref's
|
||||
header branch is restored below verbatim (ref l.118-127: `SetText`, the +/- fold icon +
|
||||
header branch is restored below (ref l.118-127: `SetText`, the +/- fold icon +
|
||||
hover-glow swap on `isExpanded`, `TradeSkillTypeColor["header"]` now reachable) and so is
|
||||
`SetSelection`'s header branch (ref l.184-192: a header click folds its group and never
|
||||
selects). `Expand/CollapseTradeSkillSubClass` are real now (engine-side); the row
|
||||
@@ -76,7 +76,7 @@
|
||||
the same frame (`ui_tradeskill.rs`'s `drain_trade_skill`) — so neither the dropdown click
|
||||
handlers nor the CollapseAll click below call `BenillaTradeSkillFrame_Update()` directly,
|
||||
matching the ref's own restraint (ref l.303-312/406-414 never do either).
|
||||
2. THE SPELL-FOCUS TOOL NEVER REDDENS. `BuildColoredListString` (below) is transcribed
|
||||
2. THE SPELL-FOCUS TOOL NEVER REDDENS. `BuildColoredListString` (below) is
|
||||
byte-faithful — it reds an unmet tool — but `GetTradeSkillTools`'s `hasTool` has no
|
||||
client-side proximity check yet (0437 INTERIM, pinned to the §5 dispatch), so v1 never
|
||||
actually reds a tool in practice. The util is ready for when the check lands.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,10 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The reference's SHARED widget kit — Interface\FrameXML\UIPanelTemplates.xml, transcribed
|
||||
from the extracted 1.12.1 file (line numbers below are that file's).
|
||||
The reference's SHARED widget kit, in our own terms. Interface\FrameXML\UIPanelTemplates.xml
|
||||
is the SPECIFICATION — the template names, the child names, the art, every offset and every
|
||||
handler's observable effect are read off the extracted 1.12.1 file, and the line numbers
|
||||
cited below are that file's. Nothing here is required to be *spelled* the way it spells it,
|
||||
and the two Lua handlers are not.
|
||||
|
||||
WHY THIS FILE EXISTS AT ALL. Every window benilla has built so far declared its own private
|
||||
copy of these — `AddonListPanelButtonTemplate`, `BenillaTrainerPanelButtonTemplate`,
|
||||
@@ -32,8 +35,8 @@
|
||||
own name in ScrollTemplates.xml, built as a plain `<Frame>` over `BenillaScrollBarTemplate`
|
||||
(decisions 0247/0250/0251). Two declarations of one name would race; re-pointing ours at
|
||||
the reference's `<ScrollFrame>` shape is its own job with its own risk, not this pass's.
|
||||
· `TabButtonTemplate` (ref l.303-402) — already transcribed verbatim in UiPanels.xml, with
|
||||
its fit/highlight deviations recorded there (decision 1004).
|
||||
· `TabButtonTemplate` (ref l.303-402) — already declared in UiPanels.xml, with its
|
||||
fit/highlight deviations recorded there (decision 1004).
|
||||
(`GameMenuButtonTemplate`, ref l.403-410, was on this list as "already in GameMenuFrame.xml"
|
||||
until the colour picker needed it from above that file's LAST-in-the-toc seat. It is below
|
||||
now, in the reference's own file for it, and GameMenuFrame.xml's private copy is gone.)
|
||||
@@ -48,77 +51,85 @@
|
||||
-->
|
||||
|
||||
<Script><![CDATA[
|
||||
-- The two scroll-frame handlers `UIPanelScrollFrameTemplate` below calls by name (ref
|
||||
-- UIPanelTemplates.lua l.150-157 and l.254-305). `ScrollFrame_OnLoad` and the whole
|
||||
-- `FauxScrollFrame_*` family already live in ScrollTemplates.xml, which loads immediately
|
||||
-- before this file; these two are the remainder of that kit.
|
||||
-- The two scroll-frame handlers `UIPanelScrollFrameTemplate` below calls by name. Their
|
||||
-- contract — the names, the arguments, and every branch's observable effect — is the
|
||||
-- reference's (UIPanelTemplates.lua l.150-157 and l.254-305); the code is ours.
|
||||
-- `ScrollFrame_OnLoad` and the whole `FauxScrollFrame_*` family already live in
|
||||
-- ScrollTemplates.xml, which loads immediately before this file; these two are the
|
||||
-- remainder of that kit.
|
||||
|
||||
-- ref l.150-157, verbatim.
|
||||
-- One wheel notch pans by half a bar-height, up or down. The wheel is SOUNDLESS, unlike the
|
||||
-- arrow buttons — the reference's own split, and the director's (trainer_tests.rs).
|
||||
function ScrollFrameTemplate_OnMouseWheel(value)
|
||||
local scrollBar = getglobal(this:GetName() .. "ScrollBar")
|
||||
if (value > 0) then
|
||||
scrollBar:SetValue(scrollBar:GetValue() - (scrollBar:GetHeight() / 2))
|
||||
else
|
||||
scrollBar:SetValue(scrollBar:GetValue() + (scrollBar:GetHeight() / 2))
|
||||
local step = scrollBar:GetHeight() / 2
|
||||
if value > 0 then
|
||||
step = -step
|
||||
end
|
||||
scrollBar:SetValue(scrollBar:GetValue() + step)
|
||||
end
|
||||
|
||||
-- ref l.254-305, verbatim — including `getglobal(scrollbar:GetName().."ThumbTexture")`,
|
||||
-- which is what `<ThumbTexture name="$parentThumbTexture">` publishes. (That publication
|
||||
-- did not exist when this file was first written: `apply_slider` applied a named thumb's
|
||||
-- layout and never set the global, the one asymmetry with a Button's state textures. It
|
||||
-- was the reference's own line that found the gap, and the gap is fixed rather than
|
||||
-- routed around.)
|
||||
-- Reconcile a ScrollFrame's bar, arrows and thumb to a range the engine has just
|
||||
-- recomputed. `scrollrange` is the engine's own second argument when this runs as the
|
||||
-- handler; a caller who omits it gets the frame's current range instead.
|
||||
--
|
||||
-- All four children hang off the OWNER's name two `$parent` levels down
|
||||
-- (`MyScrollScrollBarScrollUpButton`), so they are resolved ONCE here — the reference walks
|
||||
-- back to getglobal at each of the ten points it touches one. `$parentThumbTexture` is what
|
||||
-- publishes the last of them; that publication did not exist when this file was first
|
||||
-- written (`apply_slider` applied a named thumb's layout and never set the global, the one
|
||||
-- asymmetry with a Button's state textures), and the gap was fixed rather than routed
|
||||
-- around.
|
||||
function ScrollFrame_OnScrollRangeChanged(scrollrange)
|
||||
local scrollbar = getglobal(this:GetName() .. "ScrollBar")
|
||||
if (not scrollrange) then
|
||||
scrollrange = this:GetVerticalScrollRange()
|
||||
end
|
||||
local value = scrollbar:GetValue()
|
||||
if (value > scrollrange) then
|
||||
value = scrollrange
|
||||
end
|
||||
local frame = this
|
||||
local scrollbar = getglobal(frame:GetName() .. "ScrollBar")
|
||||
local up = getglobal(scrollbar:GetName() .. "ScrollUpButton")
|
||||
local down = getglobal(scrollbar:GetName() .. "ScrollDownButton")
|
||||
local thumb = getglobal(scrollbar:GetName() .. "ThumbTexture")
|
||||
|
||||
scrollrange = scrollrange or frame:GetVerticalScrollRange()
|
||||
local value = min(scrollbar:GetValue(), scrollrange)
|
||||
scrollbar:SetMinMaxValues(0, scrollrange)
|
||||
scrollbar:SetValue(value)
|
||||
if (floor(scrollrange) == 0) then
|
||||
if (this.scrollBarHideable) then
|
||||
scrollbar:Hide()
|
||||
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Hide()
|
||||
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Hide()
|
||||
else
|
||||
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Disable()
|
||||
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Disable()
|
||||
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Show()
|
||||
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Show()
|
||||
end
|
||||
getglobal(scrollbar:GetName() .. "ThumbTexture"):Hide()
|
||||
else
|
||||
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Show()
|
||||
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Show()
|
||||
scrollbar:Show()
|
||||
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Enable()
|
||||
getglobal(scrollbar:GetName() .. "ThumbTexture"):Show()
|
||||
end
|
||||
|
||||
-- Hide/show scrollframe borders (ref l.285-304). Named regions an OWNER declares, so
|
||||
-- all three are optional.
|
||||
local top = getglobal(this:GetName() .. "Top")
|
||||
local bottom = getglobal(this:GetName() .. "Bottom")
|
||||
local middle = getglobal(this:GetName() .. "Middle")
|
||||
if (top and bottom and this.scrollBarHideable) then
|
||||
if (this:GetVerticalScrollRange() == 0) then
|
||||
top:Hide()
|
||||
bottom:Hide()
|
||||
else
|
||||
top:Show()
|
||||
bottom:Show()
|
||||
end
|
||||
-- Nothing to scroll, and two ways of saying so — which one is the OWNER's call:
|
||||
-- a hideable bar leaves altogether, a fixed one stays in place and greys out. On the
|
||||
-- way back only the DOWN arrow wakes, because the offset is still zero and there is
|
||||
-- nowhere up to go; the frame's own vertical-scroll handler below wakes the up arrow
|
||||
-- the moment that stops being true.
|
||||
local empty = floor(scrollrange) == 0
|
||||
if not empty then
|
||||
down:Show()
|
||||
up:Show()
|
||||
scrollbar:Show()
|
||||
down:Enable()
|
||||
elseif frame.scrollBarHideable then
|
||||
scrollbar:Hide()
|
||||
down:Hide()
|
||||
up:Hide()
|
||||
else
|
||||
down:Disable()
|
||||
up:Disable()
|
||||
down:Show()
|
||||
up:Show()
|
||||
end
|
||||
if (middle and this.scrollBarHideable) then
|
||||
if (this:GetVerticalScrollRange() == 0) then
|
||||
middle:Hide()
|
||||
else
|
||||
middle:Show()
|
||||
if empty then thumb:Hide() else thumb:Show() end
|
||||
|
||||
-- The frame's own border (ref l.285-304), if it declares one: all three regions are the
|
||||
-- OWNER's to name, so all three are optional, and the pair is decided together because
|
||||
-- a half-declared border is no border. Note this leg asks the FRAME for its range
|
||||
-- rather than trusting the argument, and wants an exact zero where the bar above floors
|
||||
-- first — two different questions, and the reference keeps them apart.
|
||||
if frame.scrollBarHideable then
|
||||
local bordered = frame:GetVerticalScrollRange() ~= 0
|
||||
local top = getglobal(frame:GetName() .. "Top")
|
||||
local bottom = getglobal(frame:GetName() .. "Bottom")
|
||||
local middle = getglobal(frame:GetName() .. "Middle")
|
||||
if top and bottom then
|
||||
if bordered then top:Show() bottom:Show() else top:Hide() bottom:Hide() end
|
||||
end
|
||||
if middle then
|
||||
if bordered then middle:Show() else middle:Hide() end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -278,12 +289,14 @@
|
||||
<Slider name="UIPanelScrollBarTemplate" virtual="true">
|
||||
<Size><AbsDimension x="16" y="0"/></Size>
|
||||
<Frames>
|
||||
<!-- Both arrows step by half a bar-height and differ only in sign; unlike the wheel,
|
||||
both click. -->
|
||||
<Button name="$parentScrollUpButton" inherits="UIPanelScrollUpButtonTemplate">
|
||||
<Anchors><Anchor point="BOTTOM" relativePoint="TOP"/></Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local parent = this:GetParent()
|
||||
parent:SetValue(parent:GetValue() - (parent:GetHeight() / 2))
|
||||
local bar = this:GetParent()
|
||||
bar:SetValue(bar:GetValue() - bar:GetHeight() / 2)
|
||||
PlaySound("UChatScrollButton")
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
@@ -292,8 +305,8 @@
|
||||
<Anchors><Anchor point="TOP" relativePoint="BOTTOM"/></Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local parent = this:GetParent()
|
||||
parent:SetValue(parent:GetValue() + (parent:GetHeight() / 2))
|
||||
local bar = this:GetParent()
|
||||
bar:SetValue(bar:GetValue() + bar:GetHeight() / 2)
|
||||
PlaySound("UChatScrollButton")
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
@@ -312,10 +325,10 @@
|
||||
scroll child, with the bar above pinned down its right edge (inset 16 top and bottom so
|
||||
the arrow buttons sit inside the track span). 6 call sites across 2 corpus addons.
|
||||
|
||||
`ScrollFrame_OnLoad` is ScrollTemplates.xml's (the reference's own function, already
|
||||
transcribed there); the other two handlers are this file's script block. An instance
|
||||
supplies its own `<ScrollChild>` — without one the frame has no scroll range at all, which
|
||||
the loader now says out loud. -->
|
||||
`ScrollFrame_OnLoad` — the reference's own name for it — is declared in ScrollTemplates.xml;
|
||||
the other two handlers are this file's script block. An instance supplies its own
|
||||
`<ScrollChild>` — without one the frame has no scroll range at all, which the loader now
|
||||
says out loud. -->
|
||||
<ScrollFrame name="UIPanelScrollFrameTemplate" virtual="true">
|
||||
<Frames>
|
||||
<Slider name="$parentScrollBar" inherits="UIPanelScrollBarTemplate">
|
||||
@@ -328,24 +341,20 @@
|
||||
<Scripts>
|
||||
<OnLoad>ScrollFrame_OnLoad()</OnLoad>
|
||||
<OnScrollRangeChanged>ScrollFrame_OnScrollRangeChanged(arg2)</OnScrollRangeChanged>
|
||||
<!-- The reference's own comment here reads "The code to disable the ScrollDownButton is
|
||||
pretty hacked, but it works". Transcribed as it stands. -->
|
||||
<!-- Each arrow is live only while there is somewhere its way to go: up dies at offset
|
||||
zero, down dies once the bar has been seated at the top of its range. The bar's
|
||||
own value is re-read for the down test rather than arg1 being reused, because
|
||||
SetValue above may have clamped it. (The reference's comment on this pair reads
|
||||
"The code to disable the ScrollDownButton is pretty hacked, but it works" — it
|
||||
asks the same question as `(value - max) == 0`.) -->
|
||||
<OnVerticalScroll>
|
||||
local scrollbar = getglobal(this:GetName() .. "ScrollBar")
|
||||
scrollbar:SetValue(arg1)
|
||||
local min
|
||||
local max
|
||||
min, max = scrollbar:GetMinMaxValues()
|
||||
if (arg1 == 0) then
|
||||
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Disable()
|
||||
else
|
||||
getglobal(scrollbar:GetName() .. "ScrollUpButton"):Enable()
|
||||
end
|
||||
if ((scrollbar:GetValue() - max) == 0) then
|
||||
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Disable()
|
||||
else
|
||||
getglobal(scrollbar:GetName() .. "ScrollDownButton"):Enable()
|
||||
end
|
||||
local up = getglobal(scrollbar:GetName() .. "ScrollUpButton")
|
||||
local down = getglobal(scrollbar:GetName() .. "ScrollDownButton")
|
||||
local _, maxScroll = scrollbar:GetMinMaxValues()
|
||||
if arg1 == 0 then up:Disable() else up:Enable() end
|
||||
if scrollbar:GetValue() == maxScroll then down:Disable() else down:Enable() end
|
||||
</OnVerticalScroll>
|
||||
<OnMouseWheel>ScrollFrameTemplate_OnMouseWheel(arg1)</OnMouseWheel>
|
||||
</Scripts>
|
||||
|
||||
@@ -167,22 +167,42 @@
|
||||
end
|
||||
end
|
||||
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS = {}
|
||||
-- Frames
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["MultiBarBottomLeft"] = {baseY = 17, reputation = 9, maxLevel = -5, anchorTo = "ActionButton1", point = "BOTTOMLEFT", rpoint = "TOPLEFT"}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["GroupLootFrame1"] = {baseY = 60, bottomEither = 42, pet = 42, reputation = 9}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["TutorialFrameParent"] = {baseY = 55, bottomEither = 47, pet = 42, reputation = 9}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["FramerateLabel"] = {baseY = 64, bottomEither = 42, pet = 42, reputation = 9}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["CastingBarFrame"] = {baseY = 60, bottomEither = 40, pet = 40, reputation = 9}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["ChatFrame1"] = {baseY = 85, bottomLeft = 17, pet = 17, reputation = 9, maxLevel = -5, anchorTo = "UIParent", point = "BOTTOMLEFT", rpoint = "BOTTOMLEFT", xOffset = 32}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["ChatFrame2"] = {baseY = 85, bottomRight = 17, rightLeft = -88, rightRight = -43, reputation = 9, maxLevel = -5, anchorTo = "UIParent", point = "BOTTOMRIGHT", rpoint = "BOTTOMRIGHT", xOffset = -32}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["ShapeshiftBarFrame"] = {baseY = 0, bottomLeft = 45, reputation = 9, maxLevel = -5, anchorTo = "MainMenuBar", point = "BOTTOMLEFT", rpoint = "TOPLEFT", xOffset = 30}
|
||||
-- ── Everything that has to move when the bottom of the screen fills up ──────────────────
|
||||
--
|
||||
-- One row per thing whose seat depends on which bars are raised. A row reads:
|
||||
--
|
||||
-- baseY where it sits on the y axis with nothing in the way
|
||||
-- baseX / xOffset the same for the x axis, under two names: the rows that write a var
|
||||
-- spell it `baseX`, the rows that seat a frame spell it `xOffset`, and
|
||||
-- baseX wins if a row ever names both
|
||||
-- bottomLeft, bottomRight, bottomEither, pet, reputation, maxLevel (y axis)
|
||||
-- rightLeft, rightRight (x axis)
|
||||
-- what this row owes each raised bar, ADDED for every flag standing
|
||||
-- anchorTo / point / rpoint who to hang off; defaulted to UIParent BOTTOM-to-BOTTOM
|
||||
-- isVar the row is not a frame at all: it names a GLOBAL, and the pass writes
|
||||
-- the computed offset into it for other files to read
|
||||
--
|
||||
-- The KEY is the global name — a frame to seat, or the var to write — and other windows'
|
||||
-- OnShow/OnHide drive the pass by it, so both the keys and the fields are a published
|
||||
-- shape. Written as one constructor rather than a dozen index assignments: it is a
|
||||
-- constant, and a constant reads better as a table than as a program that builds one.
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS = {
|
||||
-- Frames to seat
|
||||
MultiBarBottomLeft = {baseY = 17, reputation = 9, maxLevel = -5, anchorTo = "ActionButton1", point = "BOTTOMLEFT", rpoint = "TOPLEFT"},
|
||||
GroupLootFrame1 = {baseY = 60, bottomEither = 42, pet = 42, reputation = 9},
|
||||
TutorialFrameParent = {baseY = 55, bottomEither = 47, pet = 42, reputation = 9},
|
||||
FramerateLabel = {baseY = 64, bottomEither = 42, pet = 42, reputation = 9},
|
||||
CastingBarFrame = {baseY = 60, bottomEither = 40, pet = 40, reputation = 9},
|
||||
ChatFrame1 = {baseY = 85, bottomLeft = 17, pet = 17, reputation = 9, maxLevel = -5, anchorTo = "UIParent", point = "BOTTOMLEFT", rpoint = "BOTTOMLEFT", xOffset = 32},
|
||||
ChatFrame2 = {baseY = 85, bottomRight = 17, rightLeft = -88, rightRight = -43, reputation = 9, maxLevel = -5, anchorTo = "UIParent", point = "BOTTOMRIGHT", rpoint = "BOTTOMRIGHT", xOffset = -32},
|
||||
ShapeshiftBarFrame = {baseY = 0, bottomLeft = 45, reputation = 9, maxLevel = -5, anchorTo = "MainMenuBar", point = "BOTTOMLEFT", rpoint = "TOPLEFT", xOffset = 30},
|
||||
|
||||
-- Vars
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["CONTAINER_OFFSET_X"] = {baseX = 0, rightLeft = 90, rightRight = 45, isVar = "xAxis"}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["CONTAINER_OFFSET_Y"] = {baseY = 70, bottomEither = 27, bottomRight = 0, reputation = 9, isVar = "yAxis", pet = 23}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["BATTLEFIELD_TAB_OFFSET_Y"] = {baseY = 210, bottomRight = 40, reputation = 9, isVar = "yAxis"}
|
||||
UIPARENT_MANAGED_FRAME_POSITIONS["PETACTIONBAR_YPOS"] = {baseY = 97, bottomLeft = 43, reputation = 9, maxLevel = -5, isVar = "yAxis"}
|
||||
-- Globals to write
|
||||
CONTAINER_OFFSET_X = {baseX = 0, rightLeft = 90, rightRight = 45, isVar = "xAxis"},
|
||||
CONTAINER_OFFSET_Y = {baseY = 70, bottomEither = 27, bottomRight = 0, reputation = 9, isVar = "yAxis", pet = 23},
|
||||
BATTLEFIELD_TAB_OFFSET_Y = {baseY = 210, bottomRight = 40, reputation = 9, isVar = "yAxis"},
|
||||
PETACTIONBAR_YPOS = {baseY = 97, bottomLeft = 43, reputation = 9, maxLevel = -5, isVar = "yAxis"},
|
||||
}
|
||||
|
||||
function UIParent_ManageFramePositions()
|
||||
-- Frames that affect offsets in y axis
|
||||
@@ -220,72 +240,48 @@
|
||||
table.insert(yOffsetFrames, "maxLevel")
|
||||
end
|
||||
|
||||
-- Iterate through frames and set anchors according to the flags set
|
||||
local frame, xOffset, yOffset, anchorTo, point, rpoint
|
||||
for index, value in pairs(UIPARENT_MANAGED_FRAME_POSITIONS) do
|
||||
frame = getglobal(index)
|
||||
-- Walk the rows and settle each one against the flags now standing. Rows are
|
||||
-- independent — nothing here reads a var another row writes — so the arbitrary order
|
||||
-- `pairs` hands back is not a hazard.
|
||||
for index, row in pairs(UIPARENT_MANAGED_FRAME_POSITIONS) do
|
||||
local frame = getglobal(index)
|
||||
if ( frame ) then
|
||||
-- Always start with base as the base offset or default to zero if no "none" specified
|
||||
xOffset = 0
|
||||
if ( value["baseX"] ) then
|
||||
xOffset = value["baseX"]
|
||||
elseif ( value["xOffset"] ) then
|
||||
xOffset = value["xOffset"]
|
||||
end
|
||||
yOffset = 0
|
||||
if ( value["baseY"] ) then
|
||||
yOffset = value["baseY"]
|
||||
end
|
||||
-- Start from where the row sits with nothing in its way. A zero base is a real
|
||||
-- base, not an absent one, which is why `or` chains do the right thing here.
|
||||
local xOffset = row.baseX or row.xOffset or 0
|
||||
local yOffset = row.baseY or 0
|
||||
|
||||
-- Iterate through frames that affect y offsets
|
||||
local hasBottomLeft, hasPetBar, hasBottomRight
|
||||
for flag, flagValue in pairs(yOffsetFrames) do
|
||||
if ( value[flagValue] ) then
|
||||
if ( flagValue == "bottomLeft" ) then
|
||||
hasBottomLeft = 1
|
||||
elseif ( flagValue == "pet" ) then
|
||||
hasPetBar = 1
|
||||
elseif ( flagValue == "bottomRight" ) then
|
||||
hasBottomRight = 1
|
||||
end
|
||||
yOffset = yOffset + value[flagValue]
|
||||
-- Then add what the row owes each raised bar, keeping note of which ones it
|
||||
-- actually paid — two of them interact below.
|
||||
local paid = {}
|
||||
for _, flag in pairs(yOffsetFrames) do
|
||||
if ( row[flag] ) then
|
||||
yOffset = yOffset + row[flag]
|
||||
paid[flag] = true
|
||||
end
|
||||
end
|
||||
for _, flag in pairs(xOffsetFrames) do
|
||||
if ( row[flag] ) then
|
||||
xOffset = xOffset + row[flag]
|
||||
end
|
||||
end
|
||||
|
||||
if ( hasBottomLeft and hasPetBar ) then
|
||||
-- The bottom-left bar and the pet/stance row do not simply stack: clearing both
|
||||
-- costs 23px more than the two figures added together. The reference carries the
|
||||
-- same correction (l.1657-1658) with no derivation, and it is a fact about the
|
||||
-- bars' art rather than arithmetic we can recompute.
|
||||
if ( paid.bottomLeft and paid.pet ) then
|
||||
yOffset = yOffset + 23
|
||||
end
|
||||
|
||||
-- Iterate through frames that affect x offsets
|
||||
for flag, flagValue in pairs(xOffsetFrames) do
|
||||
if ( value[flagValue] ) then
|
||||
xOffset = xOffset + value[flagValue]
|
||||
end
|
||||
end
|
||||
|
||||
-- Set up anchoring info
|
||||
anchorTo = value["anchorTo"]
|
||||
point = value["point"]
|
||||
rpoint = value["rpoint"]
|
||||
if ( not anchorTo ) then
|
||||
anchorTo = "UIParent"
|
||||
end
|
||||
if ( not point ) then
|
||||
point = "BOTTOM"
|
||||
end
|
||||
if ( not rpoint ) then
|
||||
rpoint = "BOTTOM"
|
||||
end
|
||||
|
||||
-- Anchor frame
|
||||
if ( value["isVar"] ) then
|
||||
if ( value["isVar"] == "xAxis" ) then
|
||||
_G[index] = xOffset
|
||||
else
|
||||
_G[index] = yOffset
|
||||
end
|
||||
-- A row either NAMES A GLOBAL to leave the number in, or is a frame to seat.
|
||||
if ( row.isVar == "xAxis" ) then
|
||||
_G[index] = xOffset
|
||||
elseif ( row.isVar ) then
|
||||
_G[index] = yOffset
|
||||
else
|
||||
frame:SetPoint(point, anchorTo, rpoint, xOffset, yOffset)
|
||||
frame:SetPoint(row.point or "BOTTOM", row.anchorTo or "UIParent",
|
||||
row.rpoint or "BOTTOM", xOffset, yOffset)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -377,71 +373,77 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ref UIParent.lua:1819-1878, transcribed 1:1 (decision 1008 — closes 0997's
|
||||
-- "abbreviates through KEY_ABBR" residue with the REAL mechanism: it never was a
|
||||
-- KEY_ABBR set; the whole abbreviation is this Lua). The action bar's HotKey corners
|
||||
-- call it with returnAbbr=1: ONE modifier abbreviates (CTRL→c, SHIFT→s, ALT→a — so
|
||||
-- SHIFT-2 reads "s-2", the director's ask), TWO OR MORE collapse the whole label to
|
||||
-- "·" — the ref's own multi-modifier dot. The full form (tooltips, the Keybindings
|
||||
-- page) keeps the canonical prefixes and reads the base key's KEY_* global.
|
||||
-- Transcription notes: the ref's dead `gsub(tempName, "CAPSLOCK", "Caps")` line
|
||||
-- (result discarded — gsub doesn't mutate) is dropped; its GetLocale() deDE
|
||||
-- CTRL→STRG swap and IsMacClient() KEY_*_MAC lookup ride behind existence guards
|
||||
-- (this VM defines neither yet, and the getglobal fallback covers the gap).
|
||||
-- The published binding-label API — benilla's implementation of the behaviour ref
|
||||
-- UIParent.lua:1819-1878 specifies (decision 1008, which closed 0997's "abbreviates
|
||||
-- through KEY_ABBR" residue with the REAL mechanism: there never was a KEY_ABBR set, the
|
||||
-- whole abbreviation is this one function). Addons call it by name, so the contract is:
|
||||
--
|
||||
-- * a nil name reads as the empty string;
|
||||
-- * "CTRL-SHIFT-2" splits at its LAST dash — up to and including that dash is the
|
||||
-- modifier run ("CTRL-SHIFT-"), what follows is the key ("2"). A name with no dash is
|
||||
-- all key;
|
||||
-- * the key renders through the `prefix..key` global (the KEY_* family), preferring a
|
||||
-- `_MAC` twin on a Mac client, and falls back to the raw key name;
|
||||
-- * returnAbbr — what the action bar's HotKey corners pass — shrinks ONE modifier to
|
||||
-- its initial, so SHIFT-2 reads "s-2" (the director's ask), and collapses TWO OR MORE
|
||||
-- to "·". The full form (tooltips, the Keybindings page) keeps the whole prefix.
|
||||
--
|
||||
-- Two knowing departures from the reference, both about its code rather than its
|
||||
-- behaviour: its `gsub(tempName, "CAPSLOCK", "Caps")` line is dead — gsub returns a new
|
||||
-- string and that result is discarded, so nothing can observe it — and has no counterpart
|
||||
-- here; and its GetLocale()/IsMacClient() calls ride behind existence guards because this
|
||||
-- VM defines neither yet, with the getglobal fallback covering the gap.
|
||||
|
||||
-- What a single modifier shrinks to on a button corner. German calls Control "STRG" (the
|
||||
-- deDE swap below runs first), which is the one that needs two letters to stay legible.
|
||||
-- The order is fixed so the pass is deterministic, but it does not matter: no abbreviation
|
||||
-- occurs inside another modifier's name, so no substitution can feed the next.
|
||||
local MODIFIER_ABBREVIATIONS = {
|
||||
{ "CTRL", "c" },
|
||||
{ "SHIFT", "s" },
|
||||
{ "ALT", "a" },
|
||||
{ "STRG", "st" },
|
||||
}
|
||||
|
||||
function GetBindingText(name, prefix, returnAbbr)
|
||||
if ( not name ) then
|
||||
return "";
|
||||
end
|
||||
local tempName = name;
|
||||
local i = strfind(name, "-");
|
||||
local dashIndex = nil;
|
||||
local count = 0;
|
||||
while ( i ) do
|
||||
if ( not dashIndex ) then
|
||||
dashIndex = i;
|
||||
else
|
||||
dashIndex = dashIndex + i;
|
||||
end
|
||||
count = count + 1;
|
||||
tempName = strsub(tempName, i + 1);
|
||||
i = strfind(tempName, "-");
|
||||
if not name then
|
||||
return ""
|
||||
end
|
||||
|
||||
local modKeys = '';
|
||||
if ( not dashIndex ) then
|
||||
dashIndex = 0;
|
||||
else
|
||||
modKeys = strsub(name, 1, dashIndex);
|
||||
if ( GetLocale and GetLocale() == "deDE" ) then
|
||||
modKeys = gsub(modKeys, "CTRL", "STRG");
|
||||
local modifiers, key = "", name
|
||||
local modifierCount = 0
|
||||
-- The greedy `.*` walks to the LAST dash, which is the one that divides the modifier
|
||||
-- run from the key; gsub's second return then counts the dashes, one per modifier —
|
||||
-- plus one more when the key ITSELF is a dash ("CTRL--"), which is why that binding
|
||||
-- abbreviates as if it had two. The reference's dash-walking loop reaches both of the
|
||||
-- same answers, and both are pinned by keybindings_tests.
|
||||
local _, lastDash = strfind(name, "^.*%-")
|
||||
if lastDash then
|
||||
modifiers = strsub(name, 1, lastDash)
|
||||
key = strsub(name, lastDash + 1)
|
||||
_, modifierCount = gsub(modifiers, "%-", "-")
|
||||
if GetLocale and GetLocale() == "deDE" then
|
||||
modifiers = gsub(modifiers, "CTRL", "STRG")
|
||||
end
|
||||
end
|
||||
|
||||
if ( returnAbbr ) then
|
||||
if ( count > 1 ) then
|
||||
return "·";
|
||||
else
|
||||
modKeys = gsub(modKeys, "CTRL", "c");
|
||||
modKeys = gsub(modKeys, "SHIFT", "s");
|
||||
modKeys = gsub(modKeys, "ALT", "a");
|
||||
modKeys = gsub(modKeys, "STRG", "st");
|
||||
if returnAbbr then
|
||||
if modifierCount > 1 then
|
||||
-- Two modifiers never fit a corner: the whole label becomes the dot.
|
||||
return "·"
|
||||
end
|
||||
for _, abbreviation in ipairs(MODIFIER_ABBREVIATIONS) do
|
||||
modifiers = gsub(modifiers, abbreviation[1], abbreviation[2])
|
||||
end
|
||||
end
|
||||
|
||||
if ( not prefix ) then
|
||||
prefix = "";
|
||||
prefix = prefix or ""
|
||||
local label
|
||||
if IsMacClient and IsMacClient() then
|
||||
label = getglobal(prefix .. key .. "_MAC")
|
||||
end
|
||||
local localizedName = nil;
|
||||
if ( IsMacClient and IsMacClient() ) then
|
||||
localizedName = getglobal(prefix..tempName.."_MAC");
|
||||
end
|
||||
if ( not localizedName ) then
|
||||
localizedName = getglobal(prefix..tempName);
|
||||
end
|
||||
if ( not localizedName ) then
|
||||
localizedName = tempName;
|
||||
end
|
||||
return modKeys..localizedName;
|
||||
label = label or getglobal(prefix .. key) or key
|
||||
return modifiers .. label
|
||||
end
|
||||
|
||||
-- ref UIParent.lua:1051-1057, transcribed 1:1 (decision 1062). The reference's own
|
||||
@@ -520,24 +522,26 @@
|
||||
frame:SetFrameLevel(frame:GetFrameLevel() + 1)
|
||||
end
|
||||
|
||||
-- ── MouseIsOver (ref UIParent.lua l.1388-1417), verbatim ──────────────────────────────
|
||||
-- ── MouseIsOver: is the cursor inside this frame, with optional per-edge padding ─────────
|
||||
--
|
||||
-- "is the cursor inside this frame, optionally with per-edge padding" — the idiom every
|
||||
-- hover-driven addon writes, and **9 corpus addons call it**. Missing, each is
|
||||
-- `attempt to call global 'MouseIsOver'`.
|
||||
-- The idiom every hover-driven addon writes, and **9 corpus addons call it** by name.
|
||||
-- Missing, each is `attempt to call global 'MouseIsOver'`. So the CONTRACT is 1.12's, quirks
|
||||
-- and all (ref UIParent.lua l.1388-1417) — three of which look like bugs and are not:
|
||||
--
|
||||
-- Transcribed rather than approximated, including the two things that look like bugs and
|
||||
-- are not:
|
||||
-- · the offsets are ADDED to every edge, so a positive `bottomOffset` shrinks the box
|
||||
-- from below while a positive `topOffset` grows it upward. The reference's own caller
|
||||
-- proves the intent — `MouseIsOver(chatFrame, 45, -10, -5, 5)` means 45 up into the
|
||||
-- tab strip, 10 in from the bottom, 5 out each side.
|
||||
-- · a nil `topOffset` zeroes ALL FOUR, so `MouseIsOver(f, nil, 5)` silently ignores the 5.
|
||||
-- That is the reference's `if ( not topOffset ) then` block exactly; an addon written
|
||||
-- against 1.12 may depend on it.
|
||||
-- The reference's `-- Hack to fix a symptom not the real issue` guard on an unresolved rect
|
||||
-- is kept with its comment, because a frame that has never been laid out really does answer
|
||||
-- nil here and returning nil is what callers expect.
|
||||
-- · each offset is ADDED to its own edge, in that axis's positive direction. A positive
|
||||
-- `topOffset` raises the ceiling (growing the box upward) while a positive
|
||||
-- `bottomOffset` raises the floor (shrinking it from below). The reference's own caller
|
||||
-- proves the intent: `MouseIsOver(chatFrame, 45, -10, -5, 5)` is 45 up into the tab
|
||||
-- strip, 10 in from the bottom, 5 out each side.
|
||||
-- · the padding is ALL OR NOTHING on `topOffset`: pass none and the other three are
|
||||
-- dropped with it, so `MouseIsOver(f, nil, 5)` silently ignores the 5. Pass a topOffset
|
||||
-- and omit any of the rest and it is an arithmetic error, not a zero — 1.12 raises
|
||||
-- there too, and an addon written against it may lean on either half.
|
||||
-- · the bounds are strict, so a cursor exactly on an edge is outside.
|
||||
--
|
||||
-- A frame that has never been laid out answers nil for its rect; nil is the right answer to
|
||||
-- hand back for it, and the reference's own comment on that guard ("a symptom not the real
|
||||
-- issue") is about where the unresolved frame came from, not about the guard.
|
||||
--
|
||||
-- ONE DIVERGENCE, and it is a no-op: the reference divides the cursor by
|
||||
-- `frame:GetEffectiveScale()` because 1.12 hands Lua screen pixels. benilla's UI space is
|
||||
@@ -546,29 +550,23 @@
|
||||
-- forever. If a real UI scale ever lands, this is one of the sites that needs it back.
|
||||
function MouseIsOver(frame, topOffset, bottomOffset, leftOffset, rightOffset)
|
||||
local x, y = GetCursorPosition()
|
||||
local left = frame:GetLeft()
|
||||
local right = frame:GetRight()
|
||||
local top = frame:GetTop()
|
||||
local bottom = frame:GetBottom()
|
||||
if not topOffset then
|
||||
topOffset = 0
|
||||
bottomOffset = 0
|
||||
leftOffset = 0
|
||||
rightOffset = 0
|
||||
end
|
||||
-- Hack to fix a symptom not the real issue
|
||||
local left, right = frame:GetLeft(), frame:GetRight()
|
||||
local top, bottom = frame:GetTop(), frame:GetBottom()
|
||||
if not left then
|
||||
return
|
||||
end
|
||||
left = left + leftOffset
|
||||
right = right + rightOffset
|
||||
top = top + topOffset
|
||||
bottom = bottom + bottomOffset
|
||||
if (x > left and x < right) and (y > bottom and y < top) then
|
||||
return 1
|
||||
else
|
||||
return nil
|
||||
if not topOffset then
|
||||
topOffset, bottomOffset, leftOffset, rightOffset = 0, 0, 0, 0
|
||||
end
|
||||
-- All four edges are padded before any of them is tested, and deliberately so: folding
|
||||
-- the additions into the comparison would let a short circuit swallow the arithmetic
|
||||
-- error that a half-supplied offset set is supposed to raise.
|
||||
left, right = left + leftOffset, right + rightOffset
|
||||
top, bottom = top + topOffset, bottom + bottomOffset
|
||||
if x > left and x < right and y > bottom and y < top then
|
||||
return 1
|
||||
end
|
||||
return nil
|
||||
end
|
||||
]]></Script>
|
||||
</Ui>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<Ui>
|
||||
<!--
|
||||
The UIPanel slot manager (decision 0084 §2) — benilla's own transcription of Blizzard's
|
||||
UIParent.lua panel-window plumbing, script-only (no frames of its own). Loaded FIRST in the
|
||||
shipped-XML list (ui_script.rs) so `UIPanelWindows`/`ShowUIPanel`/`HideUIPanel` exist before
|
||||
any panel frame's OnLoad/OnEvent references them.
|
||||
The UIPanel slot manager (decision 0084 §2) — benilla's own implementation of the
|
||||
panel-window plumbing the reference keeps in UIParent.lua. Loaded FIRST in the shipped-XML
|
||||
list (ui_script.rs) so `UIPanelWindows`/`ShowUIPanel`/`HideUIPanel` exist before any panel
|
||||
frame's OnLoad/OnEvent references them.
|
||||
|
||||
Ground truth: the 1.12 `UIParent.lua` extract (scratchpad/UIParent.lua, patch-2.MPQ), quoted
|
||||
verbatim in the line-numbered comments below — never re-derived. v1 scope (pin §3): the
|
||||
**left + center two-slot logic only**. The real client's `full`/`doublewide` areas, the
|
||||
`CanOpenPanels`/full-screen/native-center gates and `UIChildWindows` are all out of scope —
|
||||
no benilla panel uses them yet — but the registration table keeps their exact shape
|
||||
(`area`, `pushable`, `whileDead`) so a later row drops in unchanged. **`UISpecialFrames`
|
||||
left that list in decision 1206**: it is not ours to use, it is the table an ADDON writes
|
||||
its own window into, and 34 corpus call sites do.
|
||||
Specification: the 1.12 `UIParent.lua` extract (patch-2.MPQ), cited by line number
|
||||
throughout so every rule below can be checked against it. What is quoted VERBATIM is the
|
||||
**data** — each `UIPanelWindows` row's `area`/`pushable`/`whileDead`, which IS the
|
||||
reference's window arbitration (a different number is a different window order), and the
|
||||
`UISpecialFrames` seeds. The code around that data is ours.
|
||||
|
||||
Frame names here are OUR shipped names (`Benilla*`); UIParent.lua's names (`GossipFrame`,
|
||||
`MerchantFrame`, `LootFrame`) are the semantic reference the pin cites, not what we register.
|
||||
Scope: `left`, `center` and `full` are all live. `doublewide` and `UIChildWindows` are not
|
||||
— nothing benilla ships wants them — but the registration table keeps their 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.
|
||||
|
||||
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
|
||||
its comment names the reference row it stands in for.
|
||||
-->
|
||||
<Script><![CDATA[
|
||||
-- UIParent.lua l.14-42: UIPanelWindows["FrameName"] = { area, pushable, whileDead }. Only the
|
||||
@@ -216,101 +219,66 @@
|
||||
return slots.fullscreen
|
||||
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
|
||||
-- center a left-area one that should slide back?"), and all four want an absent frame and
|
||||
-- an unregistered one to answer the same way. The reference open-codes the pair of nil
|
||||
-- checks at each site, which is four chances to leave one of them out.
|
||||
local function AreaOf(frame)
|
||||
local info = frame and UIPanelWindows[frame:GetName()]
|
||||
return info and info.area
|
||||
end
|
||||
|
||||
-- CanOpenPanels() — UIParent.lua l.887-910. Its dead-player arms are commented out in the
|
||||
-- reference itself (both of them, with the note that the fear/out-of-control gate was
|
||||
-- "previously"); what survives is one rule: **a native-center frame open ⇒ nothing else
|
||||
-- opens**. The game menu is the only such frame (decision 0674), so this reads: with the
|
||||
-- ESC menu up, no window opens — not from a keybind, not from an NPC's OPEN_WINDOW.
|
||||
function CanOpenPanels()
|
||||
local centerFrame = GetCenterFrame()
|
||||
if not centerFrame then
|
||||
return 1
|
||||
end
|
||||
local info = UIPanelWindows[centerFrame:GetName()]
|
||||
if info and info.area == "center" then
|
||||
if AreaOf(GetCenterFrame()) == "center" then
|
||||
return nil
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
-- ShowUIPanel(frame) — UIParent.lua l.649-751, the left+center slice (pin §3 steps 1,3-5)
|
||||
-- plus the native-center mode decision 0674 needed. Still skipped by scope: full-screen
|
||||
-- (l.668-675, l.692-696 — no `full` row is registered; benilla's world map is a plain
|
||||
-- toplevel, not a panel) and `doublewide` (l.707-710 — likewise none).
|
||||
function ShowUIPanel(frame, force)
|
||||
if not frame or frame:IsVisible() then
|
||||
return
|
||||
end
|
||||
|
||||
local info = UIPanelWindows[frame:GetName()]
|
||||
-- l.655-657. Note the reference's own precedence: an UNREGISTERED frame is not gated
|
||||
-- (the `info and` term), so a bare toplevel still shows with the menu up.
|
||||
if not CanOpenPanels() and info and info.area ~= "center" then
|
||||
return
|
||||
end
|
||||
if not info then
|
||||
frame:Show()
|
||||
return
|
||||
end
|
||||
|
||||
-- UnitIsDead doesn't exist yet (pin §3) — guarded rather than assumed absent-forever.
|
||||
-- Everything that can REFUSE an open, asked in the reference's own order (UIParent.lua
|
||||
-- l.663-689). Answers false when the frame must not open. Not a pure predicate, and that
|
||||
-- is the point: `force` is precisely the right to throw the incumbent out instead of being
|
||||
-- turned away, so each refusal and its eviction are one decision and are written as one.
|
||||
local function AdmitPanel(info, force)
|
||||
-- A dead player gets only the windows that asked for it. UnitIsDead doesn't exist yet
|
||||
-- (pin §3) — guarded rather than assumed absent-forever.
|
||||
if type(UnitIsDead) == "function" and UnitIsDead("player") and not info.whileDead then
|
||||
return
|
||||
return false
|
||||
end
|
||||
|
||||
-- l.668-675: a full-screen frame open ignores every non-full open request (`force`
|
||||
-- evicts it instead). The world map is the only `full` row, so this reads: nothing opens
|
||||
-- behind the map.
|
||||
-- l.668-675: nothing opens behind a full-screen frame. The world map is the only
|
||||
-- `full` row we register, so this reads: nothing opens behind the map.
|
||||
if GetFullScreenFrame() and info.area ~= "full" then
|
||||
if force then
|
||||
SetFullScreenFrame(nil)
|
||||
else
|
||||
return
|
||||
if not force then
|
||||
return false
|
||||
end
|
||||
SetFullScreenFrame(nil)
|
||||
end
|
||||
|
||||
-- l.677-689: with a native-center frame open, only another center request gets through
|
||||
-- — `force` evicts it instead of refusing. Redundant with the CanOpenPanels gate above
|
||||
-- for every caller that doesn't force (the reference double-checks it too); kept because
|
||||
-- it is the ONLY path that can displace the menu, and dropping it would silently turn
|
||||
-- `force` into a no-op. No benilla caller passes `force` yet.
|
||||
local centerFrame = GetCenterFrame()
|
||||
if centerFrame then
|
||||
local centerInfo = UIPanelWindows[centerFrame:GetName()]
|
||||
if centerInfo and centerInfo.area == "center" and info.area ~= "center" then
|
||||
if force then
|
||||
SetCenterFrame(nil)
|
||||
else
|
||||
return
|
||||
end
|
||||
-- l.677-689: with a native-center frame open, only another center request gets
|
||||
-- through. Redundant with the caller's CanOpenPanels gate for anything that doesn't
|
||||
-- force (the reference double-checks it too); kept because it is the ONLY path that
|
||||
-- can displace the menu, and dropping it would silently turn `force` into a no-op.
|
||||
-- No benilla caller passes `force` yet.
|
||||
if AreaOf(GetCenterFrame()) == "center" and info.area ~= "center" then
|
||||
if not force then
|
||||
return false
|
||||
end
|
||||
SetCenterFrame(nil)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- l.692-696: full-screen frames just replace each other, and they take everything with
|
||||
-- them on the way in.
|
||||
if info.area == "full" then
|
||||
CloseAllWindows()
|
||||
SetFullScreenFrame(frame)
|
||||
return
|
||||
end
|
||||
|
||||
-- l.697-702: native-center frames just replace each other and take the screen —
|
||||
-- everything else closes first. `skipSetPoint` is the load-bearing argument: the menu
|
||||
-- keeps its own CENTER anchor instead of being dropped at the left-area seat.
|
||||
if info.area == "center" then
|
||||
CloseWindows()
|
||||
CloseAllBags()
|
||||
SetCenterFrame(frame, 1)
|
||||
return
|
||||
end
|
||||
|
||||
if info.area ~= "left" then
|
||||
-- No full/doublewide row is registered — defensive fallback only.
|
||||
frame:Show()
|
||||
return
|
||||
end
|
||||
|
||||
-- Try to put it on the left (l.719-723).
|
||||
-- Seating a **left-area** panel (l.719-751). The left slot is what it wants; `pushable`
|
||||
-- settles what happens to whoever is already sitting there. Read the comparisons as "the
|
||||
-- higher number is the one that gets moved along" — and two zeroes will not move for each
|
||||
-- other at all, which is what makes one NPC window replace another outright.
|
||||
local function SeatLeftAreaPanel(frame, info)
|
||||
local leftFrame = GetLeftFrame()
|
||||
if not leftFrame then
|
||||
SetLeftFrame(frame)
|
||||
@@ -319,64 +287,101 @@
|
||||
|
||||
local leftInfo = UIPanelWindows[leftFrame:GetName()]
|
||||
local centerFrame = GetCenterFrame()
|
||||
if not centerFrame then
|
||||
-- If neither is pushable, replace (l.729-732 — the gossip<->merchant case).
|
||||
if leftInfo.pushable == 0 and info.pushable == 0 then
|
||||
SetLeftFrame(frame)
|
||||
return
|
||||
end
|
||||
-- Highest priority goes to center (l.735-741).
|
||||
if leftInfo.pushable > info.pushable then
|
||||
MovePanelToCenter()
|
||||
SetLeftFrame(frame)
|
||||
else
|
||||
if centerFrame then
|
||||
-- Both seats taken (l.744-751): outrank the center occupant and it slides back to
|
||||
-- the left while we take center; otherwise settle for replacing the left one.
|
||||
if info.pushable > UIPanelWindows[centerFrame:GetName()].pushable then
|
||||
MovePanelToLeft()
|
||||
SetCenterFrame(frame)
|
||||
else
|
||||
SetLeftFrame(frame)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Center is already taken too (l.744-751).
|
||||
local centerInfo = UIPanelWindows[centerFrame:GetName()]
|
||||
if info.pushable > centerInfo.pushable then
|
||||
MovePanelToLeft()
|
||||
SetCenterFrame(frame)
|
||||
else
|
||||
elseif leftInfo.pushable == 0 and info.pushable == 0 then
|
||||
-- Neither will budge — two NPC sessions — so we replace it outright (l.729-732,
|
||||
-- the gossip<->merchant case).
|
||||
SetLeftFrame(frame)
|
||||
elseif leftInfo.pushable > info.pushable then
|
||||
-- The incumbent outranks us, so it is promoted to center and we take the left slot
|
||||
-- it vacates (l.735-741 — highest priority goes to center).
|
||||
MovePanelToCenter()
|
||||
SetLeftFrame(frame)
|
||||
else
|
||||
SetCenterFrame(frame)
|
||||
end
|
||||
end
|
||||
|
||||
-- HideUIPanel(frame) — UIParent.lua l.754-786, v1 left+center slice (the real fn's
|
||||
-- full-screen branch, l.759-763, is dropped: no full-screen row ships).
|
||||
-- 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.
|
||||
--
|
||||
-- Two questions, in order: may it open at all, and where does it sit.
|
||||
function ShowUIPanel(frame, force)
|
||||
if not frame or frame:IsVisible() then
|
||||
return
|
||||
end
|
||||
|
||||
local info = UIPanelWindows[frame:GetName()]
|
||||
-- l.655-657. Note the reference's own precedence: an UNREGISTERED frame is not gated
|
||||
-- (the `info and` term), so a bare toplevel still shows with the menu up. The gate is
|
||||
-- asked unconditionally, because addons replace CanOpenPanels and expect to be asked.
|
||||
local panelsAllowed = CanOpenPanels()
|
||||
if not panelsAllowed and info and info.area ~= "center" then
|
||||
return
|
||||
end
|
||||
if not info then
|
||||
frame:Show()
|
||||
return
|
||||
end
|
||||
if not AdmitPanel(info, force) then
|
||||
return
|
||||
end
|
||||
|
||||
if info.area == "full" then
|
||||
-- l.692-696: full-screen frames just replace each other, and they take everything
|
||||
-- else with them on the way in.
|
||||
CloseAllWindows()
|
||||
SetFullScreenFrame(frame)
|
||||
elseif info.area == "center" then
|
||||
-- l.697-702: native-center frames likewise replace each other and take the
|
||||
-- screen. `skipSetPoint` is the load-bearing argument here — the menu keeps its
|
||||
-- own CENTER anchor instead of being dropped at the left-area seat.
|
||||
CloseWindows()
|
||||
CloseAllBags()
|
||||
SetCenterFrame(frame, 1)
|
||||
elseif info.area == "left" then
|
||||
SeatLeftAreaPanel(frame, info)
|
||||
else
|
||||
-- No `doublewide` row is registered — defensive fallback only.
|
||||
frame:Show()
|
||||
end
|
||||
end
|
||||
|
||||
-- HideUIPanel(frame) — UIParent.lua l.754-786. Which seat is the frame sitting in? That is
|
||||
-- the whole function: vacate that one. A frame in no seat at all was never this manager's
|
||||
-- to place, so it is simply hidden.
|
||||
function HideUIPanel(frame)
|
||||
if not frame or not frame:IsShown() then
|
||||
return
|
||||
end
|
||||
|
||||
-- l.759-763.
|
||||
if frame == GetFullScreenFrame() then
|
||||
SetFullScreenFrame(nil)
|
||||
return
|
||||
end
|
||||
|
||||
if frame == GetCenterFrame() then
|
||||
elseif frame == GetCenterFrame() then
|
||||
SetCenterFrame(nil)
|
||||
return
|
||||
end
|
||||
|
||||
if frame == GetLeftFrame() then
|
||||
local centerFrame = GetCenterFrame()
|
||||
if centerFrame then
|
||||
local info = UIPanelWindows[centerFrame:GetName()]
|
||||
if info and info.area == "left" then
|
||||
MovePanelToLeft()
|
||||
return
|
||||
end
|
||||
elseif frame == GetLeftFrame() then
|
||||
-- l.777-782: a LEFT-area panel parked at center (it was pushed there) slides back
|
||||
-- into the seat we are giving up, rather than being stranded beside an empty left
|
||||
-- slot. A native-center frame stays where it is.
|
||||
if AreaOf(GetCenterFrame()) == "left" then
|
||||
MovePanelToLeft()
|
||||
else
|
||||
SetLeftFrame(nil)
|
||||
end
|
||||
SetLeftFrame(nil)
|
||||
return
|
||||
else
|
||||
-- Seated nowhere: a plain toplevel, or an addon's frame we never placed.
|
||||
frame:Hide()
|
||||
end
|
||||
|
||||
frame:Hide()
|
||||
end
|
||||
|
||||
-- UISpecialFrames (UIParent.lua l.52-55) — **the addon's own ESC list**, and the reason
|
||||
@@ -414,11 +419,10 @@
|
||||
|
||||
HideUIPanel(leftFrame)
|
||||
HideUIPanel(fullScreenFrame)
|
||||
if centerFrame then
|
||||
local info = UIPanelWindows[centerFrame:GetName()]
|
||||
if not info or info.area ~= "center" or not ignoreCenter then
|
||||
HideUIPanel(centerFrame)
|
||||
end
|
||||
-- `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
|
||||
HideUIPanel(centerFrame)
|
||||
end
|
||||
|
||||
-- The special frames, the reference's own loop (l.947-954): a plain Hide, never
|
||||
@@ -715,12 +719,12 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ── The inspect-cursor pair (ref UIParent.lua l.1805-1817, VERBATIM) ──────────────────
|
||||
-- ── The inspect-cursor pair (ref UIParent.lua l.1805-1817) ────────────────────────────
|
||||
-- `CursorUpdate` is the ctrl-hover "can I inspect this?" cursor swap, and `CursorOnUpdate`
|
||||
-- is the OnUpdate wrapper that only re-runs it while the tooltip is still ours. Both are
|
||||
-- **FrameXML, not engine** (`reference/1.12-globals.tsv` marks them `framexml`, while
|
||||
-- `ResetCursor` and `ShowInspectCursor` beside them are `engine`) — so they are ours to
|
||||
-- transcribe, and every piece they reach for already resolves here: `IsControlKeyDown`,
|
||||
-- write, and every piece they reach for already resolves here: `IsControlKeyDown`,
|
||||
-- `ShowInspectCursor`, `ResetCursor` and `GameTooltip:IsOwned` are all present.
|
||||
--
|
||||
-- Two corpus addons call `CursorUpdate` (AdvancedTradeSkillWindow, WhoHas) and one calls
|
||||
@@ -745,7 +749,9 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- SetDesaturation(texture, desaturation) — ref UIParent.lua l.1499-1510, verbatim. The
|
||||
-- SetDesaturation(texture, desaturation) — ref UIParent.lua l.1499-1510, plus two guards
|
||||
-- it does not have: a nil texture, and a texture carrying no `SetDesaturated` method at
|
||||
-- all (the reference calls it unconditionally, l.1501; ours cannot, see below). The
|
||||
-- reference itself has a no-shader fallback, and OURS IS ALWAYS THAT PATH: this engine has
|
||||
-- no SetDesaturated, so `shaderSupported` is false exactly as on a 1.12 card without the
|
||||
-- desaturation shader, and the grey comes from the ref's own vertex-colour branch. Written
|
||||
@@ -767,7 +773,7 @@
|
||||
end
|
||||
|
||||
-- ── The shared dialog IS the ref's StaticPopup engine (decision 0308 §3) ─────────────────
|
||||
-- Transcribed from the patch-chain StaticPopup.lua (mpqx extraction, decision 0246):
|
||||
-- Re-expressed from the patch-chain StaticPopup.lua (mpqx extraction, decision 0246):
|
||||
-- the StaticPopupDialogs registry + StaticPopup_Show(which, arg1, arg2) + the OnUpdate
|
||||
-- countdown/StartDelay machinery — the death arc's dialogs exercise nearly all of it, and
|
||||
-- StaticPopup_Show/StaticPopupDialogs are 1.14 addon-API surface (0068). v1 scope:
|
||||
@@ -1201,61 +1207,84 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ── The shared frame-fade kit — ref UIParent.lua:1103-1219, transcribed VERBATIM (the
|
||||
-- while-index iteration included; tinsert/tDeleteItem spelled as table.insert / the
|
||||
-- remove loop). The ref drives UIFrameFadeUpdate from UIParent's OnUpdate; our driver is
|
||||
-- the BenillaFadeDriver frame below. Callers: the quest detail write-on
|
||||
-- (QuestFrame.xml) fades TextAlphaDependentFrame in over QUESTINFO_FADE_IN. ──────────────
|
||||
-- ── The shared frame-fade kit — ref UIParent.lua:1103-1219 is the specification (the
|
||||
-- function names are its published API and the arithmetic is the same lerp); the code is
|
||||
-- ours. FADEFRAMES is a plain ARRAY walked by index rather than by `pairs`, because
|
||||
-- UIFrameFadeUpdate removes from it mid-walk. The ref drives UIFrameFadeUpdate from
|
||||
-- UIParent's OnUpdate; our driver is the BenillaFadeDriver frame below. Callers: the quest
|
||||
-- detail write-on (QuestFrame.xml) fades TextAlphaDependentFrame in over
|
||||
-- QUESTINFO_FADE_IN. ─────────────────────────────────────────────────────────────────────
|
||||
FADEFRAMES = {}
|
||||
|
||||
function UIFrameFadeRemoveFrame(frame)
|
||||
local index = 1
|
||||
-- Where `frame` sits in the fade list, searching from `from` onward, or nil. Three of the
|
||||
-- verbs below ask exactly this question, and the reference spells the index walk out
|
||||
-- longhand inside each of them.
|
||||
local function FadeIndexOf(frame, from)
|
||||
local index = from or 1
|
||||
while FADEFRAMES[index] do
|
||||
if FADEFRAMES[index] == frame then
|
||||
table.remove(FADEFRAMES, index)
|
||||
else
|
||||
index = index + 1
|
||||
return index
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function UIFrameFadeRemoveFrame(frame)
|
||||
local index = FadeIndexOf(frame)
|
||||
while index do
|
||||
table.remove(FADEFRAMES, index)
|
||||
-- Resume from the hole rather than from the start: a frame listed twice goes twice.
|
||||
index = FadeIndexOf(frame, index)
|
||||
end
|
||||
end
|
||||
|
||||
-- Where each mode starts and ends when the caller doesn't say. The tests below are
|
||||
-- `not fadeInfo.startAlpha`, so an explicit 0 — falsey in most languages, TRUTHY in Lua —
|
||||
-- is the caller's and stands.
|
||||
local FADE_ENDPOINTS = {
|
||||
IN = { startAlpha = 0, endAlpha = 1.0 },
|
||||
OUT = { startAlpha = 1.0, endAlpha = 0 },
|
||||
}
|
||||
|
||||
function UIFrameFade(frame, fadeInfo)
|
||||
if not fadeInfo.mode then fadeInfo.mode = "IN" end
|
||||
if fadeInfo.mode == "IN" then
|
||||
if not fadeInfo.startAlpha then fadeInfo.startAlpha = 0 end
|
||||
if not fadeInfo.endAlpha then fadeInfo.endAlpha = 1.0 end
|
||||
elseif fadeInfo.mode == "OUT" then
|
||||
if not fadeInfo.startAlpha then fadeInfo.startAlpha = 1.0 end
|
||||
if not fadeInfo.endAlpha then fadeInfo.endAlpha = 0 end
|
||||
if not fadeInfo.mode then
|
||||
fadeInfo.mode = "IN"
|
||||
end
|
||||
local endpoints = FADE_ENDPOINTS[fadeInfo.mode]
|
||||
if endpoints then
|
||||
if not fadeInfo.startAlpha then fadeInfo.startAlpha = endpoints.startAlpha end
|
||||
if not fadeInfo.endAlpha then fadeInfo.endAlpha = endpoints.endAlpha end
|
||||
end
|
||||
frame:SetAlpha(fadeInfo.startAlpha)
|
||||
frame.fadeInfo = fadeInfo
|
||||
-- Already fading? Leave the running fade alone (the ref's early return).
|
||||
local index = 1
|
||||
while FADEFRAMES[index] do
|
||||
if FADEFRAMES[index] == frame then return end
|
||||
index = index + 1
|
||||
-- Already in the list? The new info is installed above and takes over on the next
|
||||
-- tick; the frame keeps its place instead of being listed twice (the ref's early
|
||||
-- return).
|
||||
if FadeIndexOf(frame) then
|
||||
return
|
||||
end
|
||||
frame:Show()
|
||||
table.insert(FADEFRAMES, frame)
|
||||
end
|
||||
|
||||
-- The two directions are one call with the mode flipped; whatever endpoints the caller
|
||||
-- leaves out, UIFrameFade fills in above.
|
||||
local function FadeToMode(mode, frame, timeToFade, startAlpha, endAlpha)
|
||||
UIFrameFade(frame, {
|
||||
mode = mode,
|
||||
timeToFade = timeToFade,
|
||||
startAlpha = startAlpha,
|
||||
endAlpha = endAlpha,
|
||||
})
|
||||
end
|
||||
|
||||
function UIFrameFadeIn(frame, timeToFade, startAlpha, endAlpha)
|
||||
local fadeInfo = {}
|
||||
fadeInfo.mode = "IN"
|
||||
fadeInfo.timeToFade = timeToFade
|
||||
fadeInfo.startAlpha = startAlpha
|
||||
fadeInfo.endAlpha = endAlpha
|
||||
UIFrameFade(frame, fadeInfo)
|
||||
FadeToMode("IN", frame, timeToFade, startAlpha, endAlpha)
|
||||
end
|
||||
|
||||
function UIFrameFadeOut(frame, timeToFade, startAlpha, endAlpha)
|
||||
local fadeInfo = {}
|
||||
fadeInfo.mode = "OUT"
|
||||
fadeInfo.timeToFade = timeToFade
|
||||
fadeInfo.startAlpha = startAlpha
|
||||
fadeInfo.endAlpha = endAlpha
|
||||
UIFrameFade(frame, fadeInfo)
|
||||
FadeToMode("OUT", frame, timeToFade, startAlpha, endAlpha)
|
||||
end
|
||||
|
||||
function UIFrameFadeUpdate(elapsed)
|
||||
@@ -1291,10 +1320,8 @@
|
||||
end
|
||||
|
||||
function UIFrameIsFading(frame)
|
||||
local index = 1
|
||||
while FADEFRAMES[index] do
|
||||
if FADEFRAMES[index] == frame then return 1 end
|
||||
index = index + 1
|
||||
if FadeIndexOf(frame) then
|
||||
return 1
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
the app's single write (ui_unit.rs enrich_unit), not per reader. And UNIT_CLASSIFICATION_
|
||||
CHANGED is the repaint wire, edge-fired on that rank — without it the border would only be
|
||||
right on re-target, since Update is otherwise the sole caller.
|
||||
- The level slot runs the full CheckLevel law (ref-TargetFrame.lua l.119-142, transcribed in
|
||||
- The level slot runs the full CheckLevel law (ref-TargetFrame.lua l.119-142, re-expressed in
|
||||
Update below): corpse or "can't tell" (UnitLevel ≤ 0) → the HighLevelTexture skull;
|
||||
otherwise the number, GetDifficultyColor-tinted when attackable, gold when not.
|
||||
- The PVP icon landed with decision 0646 (the three-branch law in BenillaUnitFrame_UpdatePvP).
|
||||
@@ -153,7 +153,7 @@
|
||||
-- We answer `GetRaidTargetIndex(unit)` already (0434 §6); this is the other half — what an
|
||||
-- addon DRAWS with that number — and without it those lines raise while redrawing a mark.
|
||||
--
|
||||
-- TRANSCRIBED VERBATIM, INCLUDING THE REFERENCE'S OWN SLIP: the row is derived with
|
||||
-- KEPT WHOLE, INCLUDING THE REFERENCE'S OWN SLIP: the row is derived with
|
||||
-- `/ RAID_TARGET_TEXTURE_ROWS` while the column uses `mod(.., RAID_TARGET_TEXTURE_COLUMNS)`.
|
||||
-- Rows and columns are both 4, so the two agree for every index this sheet holds and the
|
||||
-- output is right; "fixing" it to divide by COLUMNS computes the same coords today and would
|
||||
@@ -503,12 +503,12 @@
|
||||
end
|
||||
|
||||
-- ── Combat feedback: the portrait hit indicator (decision 0576) ──────────────────────
|
||||
-- Ref CombatFeedback.lua, transcribed onto frame-param style (the ref reads `this`).
|
||||
-- Timings/colors/size factors verbatim. The words come from the GLOBALS, exactly as the ref
|
||||
-- builds this table (CombatFeedback.lua l.6-17: CombatFeedbackText["MISS"] = TEXT(MISS)),
|
||||
-- so a localized install shows its own — the app runs the real GlobalStrings.lua off the
|
||||
-- patch chain before this file (ui_script/mod.rs). The enUS literals are the fallback for
|
||||
-- the test harnesses, which load no patch chain.
|
||||
-- Our implementation of what ref CombatFeedback.lua specifies, on frame-param style (the
|
||||
-- ref reads `this`). Timings, colors and size factors are its MEASURED values. The words
|
||||
-- come from the GLOBALS, exactly as the ref builds this table (CombatFeedback.lua l.6-17:
|
||||
-- CombatFeedbackText["MISS"] = TEXT(MISS)), so a localized install shows its own — the app
|
||||
-- runs the real GlobalStrings.lua off the patch chain before this file (ui_script/mod.rs).
|
||||
-- The enUS literals are the fallback for the test harnesses, which load no patch chain.
|
||||
COMBATFEEDBACK_FADEINTIME = 0.2
|
||||
COMBATFEEDBACK_HOLDTIME = 0.7
|
||||
COMBATFEEDBACK_FADEOUTTIME = 0.3
|
||||
@@ -527,62 +527,91 @@
|
||||
REFLECT = REFLECT or "Reflect",
|
||||
}
|
||||
|
||||
-- The indicator says one of two things: a NUMBER, when something moved a bar, or a WORD,
|
||||
-- when nothing did. Everything below is that split. `scale` is a multiple of the frame's
|
||||
-- own indicator height — 1 is "as authored" — and the multiples themselves are the
|
||||
-- reference's measurements: half again for a big hit, three quarters for one that was
|
||||
-- stopped, half for one that could never land.
|
||||
|
||||
-- The two events that move a bar the friendly way, and the colour each prints in. (A
|
||||
-- WOUND prints a number too, but its colour depends on the school, so it is settled in
|
||||
-- place below.)
|
||||
local COMBAT_FEEDBACK_GAIN = {
|
||||
HEAL = { r = 0.0, g = 1.0, b = 0.0 },
|
||||
ENERGIZE = { r = 0.41, g = 0.8, b = 0.94 },
|
||||
}
|
||||
|
||||
-- The words that do not print at the indicator's authored height. Everything else does.
|
||||
local COMBAT_FEEDBACK_WORD_SCALE = {
|
||||
IMMUNE = 0.5,
|
||||
BLOCK = 0.75,
|
||||
}
|
||||
|
||||
-- A wound that did no damage was stopped by one of three things, and in each case the
|
||||
-- flag naming it is also the key of the word for it. Anything else was a plain miss.
|
||||
local COMBAT_FEEDBACK_SOAKED = { ABSORB = 1, BLOCK = 1, RESIST = 1 }
|
||||
|
||||
-- What a zero-damage wound reads as: the word, and the size it prints at. Being stopped
|
||||
-- reads small; missing outright reads full size, because a miss is the ordinary case.
|
||||
local function CombatFeedback_Stopped(flags)
|
||||
if COMBAT_FEEDBACK_SOAKED[flags] then
|
||||
return CombatFeedbackText[flags], 0.75
|
||||
end
|
||||
return CombatFeedbackText["MISS"], 1.0
|
||||
end
|
||||
|
||||
-- The size a landed blow reads at. A crushing blow is as loud as a critical; a glancing
|
||||
-- one is as quiet as a soak.
|
||||
local function CombatFeedback_HitScale(flags)
|
||||
if flags == "CRITICAL" or flags == "CRUSHING" then
|
||||
return 1.5
|
||||
elseif flags == "GLANCING" then
|
||||
return 0.75
|
||||
end
|
||||
return 1.0
|
||||
end
|
||||
|
||||
function CombatFeedback_OnCombatEvent(frame, event, flags, amount, type)
|
||||
local feedbackText = frame.feedbackText
|
||||
if not feedbackText then return end
|
||||
local fontHeight = frame.feedbackFontHeight
|
||||
local text = ""
|
||||
|
||||
-- The default reading, which most events take unchanged: this event's own word, at the
|
||||
-- authored height, in white. An event with no word at all prints nothing, which is the
|
||||
-- honest thing to do with a feedback kind we do not have a string for.
|
||||
local text = CombatFeedbackText[event]
|
||||
local scale = 1.0
|
||||
local r, g, b = 1.0, 1.0, 1.0
|
||||
|
||||
if event == "IMMUNE" then
|
||||
fontHeight = fontHeight * 0.5
|
||||
text = CombatFeedbackText[event]
|
||||
elseif event == "WOUND" then
|
||||
if amount ~= 0 then
|
||||
if flags == "CRITICAL" or flags == "CRUSHING" then
|
||||
fontHeight = fontHeight * 1.5
|
||||
elseif flags == "GLANCING" then
|
||||
fontHeight = fontHeight * 0.75
|
||||
end
|
||||
-- type > 0 = a spell school: the number reads yellow, physical white.
|
||||
if event == "WOUND" then
|
||||
if amount == 0 then
|
||||
text, scale = CombatFeedback_Stopped(flags)
|
||||
else
|
||||
text = amount
|
||||
scale = CombatFeedback_HitScale(flags)
|
||||
-- A non-zero school means a spell landed it: spell damage prints yellow where
|
||||
-- a physical hit prints white.
|
||||
if type > 0 then
|
||||
r, g, b = 1.0, 1.0, 0.0
|
||||
end
|
||||
text = amount
|
||||
elseif flags == "ABSORB" then
|
||||
fontHeight = fontHeight * 0.75
|
||||
text = CombatFeedbackText["ABSORB"]
|
||||
elseif flags == "BLOCK" then
|
||||
fontHeight = fontHeight * 0.75
|
||||
text = CombatFeedbackText["BLOCK"]
|
||||
elseif flags == "RESIST" then
|
||||
fontHeight = fontHeight * 0.75
|
||||
text = CombatFeedbackText["RESIST"]
|
||||
else
|
||||
text = CombatFeedbackText["MISS"]
|
||||
end
|
||||
elseif event == "BLOCK" then
|
||||
fontHeight = fontHeight * 0.75
|
||||
text = CombatFeedbackText[event]
|
||||
elseif event == "HEAL" then
|
||||
text = amount
|
||||
r, g, b = 0.0, 1.0, 0.0
|
||||
if flags == "CRITICAL" then
|
||||
fontHeight = fontHeight * 1.5
|
||||
end
|
||||
elseif event == "ENERGIZE" then
|
||||
text = amount
|
||||
r, g, b = 0.41, 0.8, 0.94
|
||||
if flags == "CRITICAL" then
|
||||
fontHeight = fontHeight * 1.5
|
||||
end
|
||||
else
|
||||
text = CombatFeedbackText[event]
|
||||
local gain = COMBAT_FEEDBACK_GAIN[event]
|
||||
if gain then
|
||||
text = amount
|
||||
r, g, b = gain.r, gain.g, gain.b
|
||||
-- A critical heal or power gain reads big. Note this is the ONLY flag that
|
||||
-- resizes one — a wound's crushing/glancing legs do not apply here.
|
||||
if flags == "CRITICAL" then
|
||||
scale = 1.5
|
||||
end
|
||||
else
|
||||
scale = COMBAT_FEEDBACK_WORD_SCALE[event] or 1.0
|
||||
end
|
||||
end
|
||||
|
||||
frame.feedbackStartTime = GetTime()
|
||||
|
||||
feedbackText:SetTextHeight(fontHeight)
|
||||
feedbackText:SetTextHeight(frame.feedbackFontHeight * scale)
|
||||
feedbackText:SetText(text)
|
||||
feedbackText:SetTextColor(r, g, b)
|
||||
feedbackText:SetAlpha(0.0)
|
||||
@@ -615,7 +644,7 @@
|
||||
|
||||
-- Pull the unit's current state and repaint. A non-existent unit hides the whole frame.
|
||||
-- The classification border art (TargetFrame_CheckClassification, ref-TargetFrame.lua
|
||||
-- l.205-218) — transcribed branch for branch, and the shape is the surprise: 1.12 has FIVE
|
||||
-- l.205-218) — re-expressed branch for branch, and the shape is the surprise: 1.12 has FIVE
|
||||
-- classifications but only THREE border textures, because elite, rareelite and worldboss all
|
||||
-- take the same Elite art. There is no rare-elite border in this build and no rare-elite
|
||||
-- tooltip word either (the level line's rank table is {"", ELITE, ELITE, BOSS, ""}), so
|
||||
@@ -774,7 +803,7 @@
|
||||
MAX_TARGET_DEBUFFS = 16
|
||||
MAX_TARGET_BUFFS = 5
|
||||
|
||||
-- TargetDebuffButton_Update (ref-TargetFrame.lua l.263-387), transcribed onto the Era API
|
||||
-- TargetDebuffButton_Update (ref-TargetFrame.lua l.263-387), re-expressed onto the Era API
|
||||
-- (decision 0255: UnitBuff returns name,icon,count,debuffType,… — the 1.12 original returned
|
||||
-- the texture first). Only called with a live target (UnitFrame_Update guards).
|
||||
-- DebuffTypeColor is defined by BuffFrame.xml, which loads AFTER this file — safe, because
|
||||
@@ -883,7 +912,7 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- The status flash's paint (ref PlayerFrame_UpdateStatus, transcribed onto frame-param
|
||||
-- The status flash's paint (ref PlayerFrame_UpdateStatus, re-expressed onto frame-param
|
||||
-- style; decision 1082). Priority is the ref's: RESTING wins over combat — sitting in an
|
||||
-- inn mid-auto-attack shows the zzz — then the auto-attack state (gold ring/zzz vs red
|
||||
-- ring/swords/red disc), then bare swords for on-someone's-aggro-list, then nothing.
|
||||
@@ -976,26 +1005,34 @@
|
||||
UIDropDownMenu_Initialize(this, TargetFrameDropDown_Initialize, "MENU")
|
||||
end
|
||||
|
||||
function TargetFrameDropDown_Initialize()
|
||||
local menu
|
||||
local name
|
||||
-- Which menu the CURRENT target gets, and the heading that menu needs (only the bare mark
|
||||
-- list carries one — the unit menus title themselves from the unit). Most specific first:
|
||||
-- yourself → SELF
|
||||
-- your own pet → PET. Selecting your pet and right-clicking the TARGET
|
||||
-- frame is still the pet's menu; the ref's own second
|
||||
-- branch (TargetFrame.lua l.440-441), which sat as a
|
||||
-- comment here until the "pet" token (0990) and then the
|
||||
-- menu itself (1066).
|
||||
-- anything that is not a player → an NPC, which gets only the raid-mark list
|
||||
-- a player, in your party or not → PARTY / PLAYER
|
||||
local function TargetFrameMenu()
|
||||
if UnitIsUnit("target", "player") then
|
||||
menu = "SELF"
|
||||
elseif UnitIsUnit("target", "pet") then
|
||||
-- Selecting your own pet and right-clicking the TARGET frame gives you the pet
|
||||
-- menu — the ref's own second branch (TargetFrame.lua l.440-441), which sat as a
|
||||
-- comment here until the "pet" token (0990) and then the menu itself (1066).
|
||||
menu = "PET"
|
||||
elseif UnitIsPlayer("target") then
|
||||
if UnitInParty("target") then
|
||||
menu = "PARTY"
|
||||
else
|
||||
menu = "PLAYER"
|
||||
end
|
||||
else
|
||||
menu = "RAID_TARGET_ICON"
|
||||
name = RAID_TARGET_ICON
|
||||
return "SELF"
|
||||
end
|
||||
if UnitIsUnit("target", "pet") then
|
||||
return "PET"
|
||||
end
|
||||
if not UnitIsPlayer("target") then
|
||||
return "RAID_TARGET_ICON", RAID_TARGET_ICON
|
||||
end
|
||||
if UnitInParty("target") then
|
||||
return "PARTY"
|
||||
end
|
||||
return "PLAYER"
|
||||
end
|
||||
|
||||
function TargetFrameDropDown_Initialize()
|
||||
local menu, name = TargetFrameMenu()
|
||||
if menu then
|
||||
UnitPopup_ShowMenu(TargetFrameDropDown, menu, "target", name)
|
||||
end
|
||||
@@ -1594,7 +1631,8 @@
|
||||
</Frame>
|
||||
|
||||
<!--
|
||||
THE PET FRAME (decision 0990; ref PetFrame.xml + PetFrame.lua, quoted). 128×53 parented to
|
||||
THE PET FRAME (decision 0990). Geometry and art are MEASURED off ref PetFrame.xml +
|
||||
PetFrame.lua and the behaviour is theirs; the code is our own. 128×53 parented to
|
||||
the player frame at TOPLEFT (80,-60) — a static anchor, deliberately: the reference does not
|
||||
list PetFrame in UIPARENT_MANAGED_FRAME_POSITIONS at all, because it rides its parent.
|
||||
|
||||
@@ -1611,8 +1649,8 @@
|
||||
|
||||
The PET RIGHT-CLICK MENU is no longer deferred either — decision 1066 built the last of
|
||||
the five things it was waiting on (the pet paper doll, the fifth, landed as 1057). The
|
||||
frame's else arm opens PetFrameDropDown below; the rows, their fork and their dialogs are
|
||||
UnitPopup.xml's.
|
||||
frame's non-left-click arm opens PetFrameDropDown below; the rows, their fork and their
|
||||
dialogs are UnitPopup.xml's.
|
||||
|
||||
STILL DEFERRED, with its reason:
|
||||
- PartyMemberBuffTooltip on hover (the ref's OnEnter panel): it belongs to the party
|
||||
@@ -1719,32 +1757,44 @@
|
||||
PetFrame_SetHappiness()
|
||||
end
|
||||
|
||||
-- ref PetFrame_SetHappiness (PetFrame.lua l.132-156), quoted (decision 1005).
|
||||
-- ── The happiness face (decision 1005; ref PetFrame_SetHappiness, PetFrame.lua l.132-156)
|
||||
--
|
||||
-- `Interface\PetPaperDollFrame\UI-PetHappiness` is one row of three faces, and each cut is
|
||||
-- a MEASURED value off that file. The buckets run RIGHT to left across the strip — bucket
|
||||
-- 1 (unhappy) takes the rightmost cut and bucket 3 (happy) sits at x=0 — which is why the
|
||||
-- left edges below descend as the index climbs. A bucket outside 1..3 selects no cell at
|
||||
-- all and the icon keeps whatever face it was already wearing, deliberately: see the note
|
||||
-- on bucket 0 below.
|
||||
local PET_HAPPINESS_CELL = {
|
||||
{ 0.375, 0.5625, 0, 0.359375 },
|
||||
{ 0.1875, 0.375, 0, 0.359375 },
|
||||
{ 0, 0.1875, 0, 0.359375 },
|
||||
}
|
||||
|
||||
-- `GetPetHappiness` hands back a PRE-BUCKETED 1/2/3 — the client does the thresholding
|
||||
-- against PetPersonality.dbc, not this function — plus the two numbers that bucket
|
||||
-- selects. Three things here are load-bearing and each is the reference's own:
|
||||
-- * the hide test is `not happiness or not isHunterPet`, so a bucket of **0** (truthy in
|
||||
-- Lua) leaves the icon SHOWING with whatever texcoords it had. Testing `happiness == 0`
|
||||
-- or coercing 0 to nil would hide a frame the reference shows.
|
||||
-- * the three texcoord cuts run 3/2/1 left-to-right across the strip — happy is at x=0.
|
||||
-- * the hide test is on the bucket being ABSENT, not on it being zero, so a bucket of
|
||||
-- **0** (truthy in Lua) leaves the icon SHOWING with whatever texcoords it had.
|
||||
-- Testing `happiness == 0` or coercing 0 to nil would hide a frame the reference shows.
|
||||
-- * only a HUNTER's pet has a face at all — `HasPetUI`'s second return is the whole gate,
|
||||
-- and its first return is nothing to do with this frame.
|
||||
-- * the loyalty line is chosen by the SIGN of loyaltyRate, which is why that return is
|
||||
-- unscaled and allowed to be negative.
|
||||
-- unscaled and allowed to be negative. Exactly zero means "steady", and says nothing.
|
||||
function PetFrame_SetHappiness()
|
||||
local happiness, damagePercentage, loyaltyRate = GetPetHappiness()
|
||||
local hasPetUI, isHunterPet = HasPetUI()
|
||||
if not happiness or not isHunterPet then
|
||||
local _, isHunterPet = HasPetUI()
|
||||
if not (happiness and isHunterPet) then
|
||||
PetFrameHappiness:Hide()
|
||||
return
|
||||
end
|
||||
PetFrameHappiness:Show()
|
||||
if happiness == 1 then
|
||||
PetFrameHappinessTexture:SetTexCoord(0.375, 0.5625, 0, 0.359375)
|
||||
elseif happiness == 2 then
|
||||
PetFrameHappinessTexture:SetTexCoord(0.1875, 0.375, 0, 0.359375)
|
||||
elseif happiness == 3 then
|
||||
PetFrameHappinessTexture:SetTexCoord(0, 0.1875, 0, 0.359375)
|
||||
|
||||
local cell = PET_HAPPINESS_CELL[happiness]
|
||||
if cell then
|
||||
PetFrameHappinessTexture:SetTexCoord(cell[1], cell[2], cell[3], cell[4])
|
||||
end
|
||||
|
||||
PetFrameHappiness.tooltip = getglobal("PET_HAPPINESS" .. happiness)
|
||||
PetFrameHappiness.tooltipDamage = format(PET_DAMAGE_PERCENTAGE, damagePercentage)
|
||||
if loyaltyRate < 0 then
|
||||
@@ -1756,6 +1806,27 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- The happiness face's hover. Both extra lines are drawn the same way — plain white,
|
||||
-- nothing in the right-hand column — and either can be absent, so the stanza is written
|
||||
-- once. The whole tooltip waits on the headline: until SetHappiness has run there is
|
||||
-- nothing to say, and an empty plate is worse than none.
|
||||
local function AddHappinessTooltipLine(text)
|
||||
if text then
|
||||
GameTooltip:AddLine(text, "", 1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function BenillaPetHappiness_OnEnter(frame)
|
||||
if not frame.tooltip then
|
||||
return
|
||||
end
|
||||
GameTooltip:SetOwner(frame, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(frame.tooltip)
|
||||
AddHappinessTooltipLine(frame.tooltipDamage)
|
||||
AddHappinessTooltipLine(frame.tooltipLoyalty)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
|
||||
-- The four debuff buttons (ref PetFrame.xml's $parentDebuff1..4 over RefreshBuffs). The pet
|
||||
-- carries no aura timers — no unit but yourself does on the 1.12 wire (decision 0257 B6) —
|
||||
-- so these are icon + border + stack count, the party rows' shape exactly.
|
||||
@@ -1801,26 +1872,30 @@
|
||||
PetFrameAttackMode:SetVertexColor(1.0, 1.0, 1.0, alpha)
|
||||
end
|
||||
|
||||
-- ref PetFrame_OnClick, now whole (decision 1066 built the right branch). Its FIRST leg is
|
||||
-- the one that is easy to drop: with a spell armed, a RIGHT click disarms rather than
|
||||
-- opening the menu, and it precedes both branches.
|
||||
-- Clicking the pet frame (decision 1066 built the right-hand half). The LEFT button hands
|
||||
-- the pet whatever the cursor is carrying; every other button opens its menu — except for
|
||||
-- the one case that is easy to drop, a RIGHT click with a spell already armed, which puts
|
||||
-- the spell away instead. That exception is the right button's alone: a middle click while
|
||||
-- casting still opens the menu.
|
||||
function PetFrame_OnClick(button)
|
||||
if SpellIsTargeting() and button == "RightButton" then
|
||||
SpellStopTargeting()
|
||||
if button ~= "LeftButton" then
|
||||
if button == "RightButton" and SpellIsTargeting() then
|
||||
SpellStopTargeting()
|
||||
else
|
||||
-- No anchor arguments, unlike the player and target frames: this menu opens at
|
||||
-- the CURSOR (ref PetFrame.lua l.128).
|
||||
ToggleDropDownMenu(1, nil, PetFrameDropDown)
|
||||
end
|
||||
return
|
||||
end
|
||||
if button == "LeftButton" then
|
||||
if SpellIsTargeting() then
|
||||
SpellTargetUnit("pet")
|
||||
elseif CursorHasItem() then
|
||||
DropItemOnUnit("pet")
|
||||
else
|
||||
TargetUnit("pet")
|
||||
end
|
||||
-- Most specific first: an armed spell binds to the pet, then a held item drops on it
|
||||
-- (this is how you feed one), and with an empty cursor the click simply selects it.
|
||||
if SpellIsTargeting() then
|
||||
SpellTargetUnit("pet")
|
||||
elseif CursorHasItem() then
|
||||
DropItemOnUnit("pet")
|
||||
else
|
||||
-- No anchor arguments, unlike the player and target frames: the ref opens this one
|
||||
-- at the CURSOR (PetFrame.lua l.128).
|
||||
ToggleDropDownMenu(1, nil, PetFrameDropDown)
|
||||
TargetUnit("pet")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2006,17 +2081,7 @@
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnEnter>
|
||||
if this.tooltip then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(this.tooltip)
|
||||
if this.tooltipDamage then
|
||||
GameTooltip:AddLine(this.tooltipDamage, "", 1, 1, 1)
|
||||
end
|
||||
if this.tooltipLoyalty then
|
||||
GameTooltip:AddLine(this.tooltipLoyalty, "", 1, 1, 1)
|
||||
end
|
||||
GameTooltip:Show()
|
||||
end
|
||||
BenillaPetHappiness_OnEnter(this)
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
GameTooltip:Hide()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,15 @@
|
||||
<!--
|
||||
The fullscreen world map — transcribed from the 1.12 FrameXML WorldMapFrame.xml/.lua
|
||||
(decision 0203 phase 2), restored verbatim by decision 0221 (the windowed / 1.14-header
|
||||
direction of 0214/0217/0219 is parked; this faithful fullscreen sheet is the baseline we
|
||||
return to). The map is pure FrameXML over the worldmap host API (benilla-ui
|
||||
script/worldmap.rs): detail tiles are plain textures whose art the Lua swaps per selection
|
||||
(GetMapInfo → Interface\WorldMap\<name>\<name>1..12), the player blip is positioned from
|
||||
GetPlayerMapPosition exactly like the reference, navigation is SetMapZoom/-ToCurrentZone/
|
||||
ProcessMapClick.
|
||||
The fullscreen world map — our implementation of the behaviour the 1.12 FrameXML
|
||||
WorldMapFrame.xml/.lua specifies (decision 0203 phase 2; decision 0221 settled the layout on
|
||||
the fullscreen sheet — the windowed / 1.14-header direction of 0214/0217/0219 is parked, and
|
||||
this faithful fullscreen sheet is the baseline we return to). The reference is the SPEC, not
|
||||
the source of this text: the names are its contract (globals addons call, frame and template
|
||||
names, art paths, every constant and texcoord) and the code that uses them is ours. The map
|
||||
is pure FrameXML over the worldmap host API (benilla-ui script/worldmap.rs): detail tiles are
|
||||
plain textures whose art the Lua swaps per selection (GetMapInfo →
|
||||
Interface\WorldMap\<name>\<name>1..12), the player blip is positioned from
|
||||
GetPlayerMapPosition in the same 0..1 map space the reference uses, navigation is
|
||||
SetMapZoom/-ToCurrentZone/ProcessMapClick.
|
||||
|
||||
IN (this file): the FULLSCREEN frame + blackout, the 12-piece parchment chrome, the title,
|
||||
the 1002×668 detail frame with its 12 tiles + ADD highlight, the discovery-overlay pool
|
||||
@@ -33,7 +36,7 @@
|
||||
NUM_WORLDMAP_DETAIL_TILES = 12;
|
||||
NUM_WORLDMAP_OVERLAYS = 0;
|
||||
|
||||
-- ref WorldMapFrame.lua l.509-515, verbatim. Through the PANEL MANAGER, not a bare
|
||||
-- ref WorldMapFrame.lua l.509-515 is the spec. Through the PANEL MANAGER, not a bare
|
||||
-- Show/Hide: the map is registered `area = "full"` (UiPanels.xml), which is what makes it
|
||||
-- take the screen from the other windows on the way in and — the reason this changed —
|
||||
-- what stops it opening from behind the game menu, the one window that still could
|
||||
@@ -60,9 +63,30 @@
|
||||
WorldMapFrame:SetScale(scale);
|
||||
end
|
||||
|
||||
-- ref-WorldMapFrame.lua WorldMapFrame_Update(): the tile swap, the discovery-overlay
|
||||
-- pool, and the zoom-out arming (POI landmarks are still deferred). The "World" fallback
|
||||
-- is the reference's own hack for the nil GetMapInfo at the world level — kept verbatim.
|
||||
-- One overlay tile's extent along one axis: how many pixels of art it draws, and how wide
|
||||
-- the FILE holding them is. Every tile is a full 256 except the last of a row or column,
|
||||
-- which carries whatever remains of the overlay — and its file is padded up to the next
|
||||
-- power of two (16 the smallest the art uses), so that tile shows only the leading
|
||||
-- pixels/file fraction of it. Both axes want the same answer, so they ask the same way.
|
||||
local function overlayTileExtent(index, count, span)
|
||||
if ( index < count ) then
|
||||
return 256, 256;
|
||||
end
|
||||
local pixels = mod(span, 256);
|
||||
if ( pixels == 0 ) then
|
||||
pixels = 256;
|
||||
end
|
||||
local file = 16;
|
||||
while ( file < pixels ) do
|
||||
file = file * 2;
|
||||
end
|
||||
return pixels, file;
|
||||
end
|
||||
|
||||
-- The repaint (spec: ref WorldMapFrame.lua WorldMapFrame_Update): the tile swap, the
|
||||
-- discovery-overlay pool, and the zoom-out arming (POI landmarks are still deferred). The
|
||||
-- "World" fallback is the reference's own hack for the nil GetMapInfo at the world level,
|
||||
-- kept because our host answers nil at that level for the same reason.
|
||||
function WorldMapFrame_Update()
|
||||
local mapFileName = GetMapInfo();
|
||||
if ( not mapFileName ) then
|
||||
@@ -72,72 +96,46 @@
|
||||
getglobal("WorldMapDetailTile"..i):SetTexture("Interface\\WorldMap\\"..mapFileName.."\\"..mapFileName..i);
|
||||
end
|
||||
|
||||
-- The discovery overlays (ref WorldMapFrame.lua:94-153): the base tiles are the
|
||||
-- unexplored parchment; each REVEALED sub-area (GetNumMapOverlays / GetMapOverlayInfo,
|
||||
-- filtered host-side by the explored bitset) paints its detailed art on top. Pool
|
||||
-- ARTWORK textures on WorldMapDetailFrame on demand, slice each overlay into 256-px
|
||||
-- tiles whose file sizes round up to the next power of two, park the leftovers hidden.
|
||||
-- Native offsets — the map renders at its native 1024×768 (no WORLDMAP_SX/SY scale,
|
||||
-- unlike the parked windowed layout); the overlays share the detail tiles' coord space.
|
||||
local numOverlays = GetNumMapOverlays();
|
||||
local textureName, textureWidth, textureHeight, offsetX, offsetY;
|
||||
local textureCount = 0;
|
||||
local texture;
|
||||
local texturePixelWidth, textureFileWidth, texturePixelHeight, textureFileHeight;
|
||||
local numTexturesWide, numTexturesTall, neededTextures;
|
||||
for i=1, numOverlays do
|
||||
textureName, textureWidth, textureHeight, offsetX, offsetY = GetMapOverlayInfo(i);
|
||||
numTexturesWide = ceil(textureWidth/256);
|
||||
numTexturesTall = ceil(textureHeight/256);
|
||||
neededTextures = textureCount + (numTexturesWide * numTexturesTall);
|
||||
if ( neededTextures > NUM_WORLDMAP_OVERLAYS ) then
|
||||
for j=NUM_WORLDMAP_OVERLAYS+1, neededTextures do
|
||||
WorldMapDetailFrame:CreateTexture("WorldMapOverlay"..j, "ARTWORK");
|
||||
-- The discovery overlays (spec: ref WorldMapFrame.lua, "Setup the overlays"): the base
|
||||
-- tiles are the unexplored parchment, and each REVEALED sub-area (GetNumMapOverlays /
|
||||
-- GetMapOverlayInfo, filtered host-side by the explored bitset) paints its detailed
|
||||
-- art over them. That art is cut into a grid of 256-px tiles, numbered 1..n across
|
||||
-- then down, which we lay onto ARTWORK textures pooled on WorldMapDetailFrame — the
|
||||
-- pool grows on demand, never shrinks, and the tail left over from a busier map is
|
||||
-- parked hidden below. Offsets are native pixels: the map renders at its native
|
||||
-- 1024×768 (no WORLDMAP_SX/SY scale, unlike the parked windowed layout), so the
|
||||
-- overlays share the detail tiles' coordinate space untouched.
|
||||
local used = 0;
|
||||
for overlay = 1, GetNumMapOverlays() do
|
||||
local art, artWidth, artHeight, originX, originY = GetMapOverlayInfo(overlay);
|
||||
local columns = ceil(artWidth/256);
|
||||
local rows = ceil(artHeight/256);
|
||||
local wanted = used + (columns * rows);
|
||||
if ( wanted > NUM_WORLDMAP_OVERLAYS ) then
|
||||
for slot = NUM_WORLDMAP_OVERLAYS + 1, wanted do
|
||||
WorldMapDetailFrame:CreateTexture("WorldMapOverlay"..slot, "ARTWORK");
|
||||
end
|
||||
NUM_WORLDMAP_OVERLAYS = neededTextures;
|
||||
NUM_WORLDMAP_OVERLAYS = wanted;
|
||||
end
|
||||
for j=1, numTexturesTall do
|
||||
if ( j < numTexturesTall ) then
|
||||
texturePixelHeight = 256;
|
||||
textureFileHeight = 256;
|
||||
else
|
||||
texturePixelHeight = mod(textureHeight, 256);
|
||||
if ( texturePixelHeight == 0 ) then
|
||||
texturePixelHeight = 256;
|
||||
end
|
||||
textureFileHeight = 16;
|
||||
while(textureFileHeight < texturePixelHeight) do
|
||||
textureFileHeight = textureFileHeight * 2;
|
||||
end
|
||||
end
|
||||
for k=1, numTexturesWide do
|
||||
textureCount = textureCount + 1;
|
||||
texture = getglobal("WorldMapOverlay"..textureCount);
|
||||
if ( k < numTexturesWide ) then
|
||||
texturePixelWidth = 256;
|
||||
textureFileWidth = 256;
|
||||
else
|
||||
texturePixelWidth = mod(textureWidth, 256);
|
||||
if ( texturePixelWidth == 0 ) then
|
||||
texturePixelWidth = 256;
|
||||
end
|
||||
textureFileWidth = 16;
|
||||
while(textureFileWidth < texturePixelWidth) do
|
||||
textureFileWidth = textureFileWidth * 2;
|
||||
end
|
||||
end
|
||||
texture:SetWidth(texturePixelWidth);
|
||||
texture:SetHeight(texturePixelHeight);
|
||||
texture:SetTexCoord(0, texturePixelWidth/textureFileWidth, 0, texturePixelHeight/textureFileHeight);
|
||||
texture:ClearAllPoints();
|
||||
texture:SetPoint("TOPLEFT", offsetX + (256 * (k-1)), -(offsetY + (256 * (j - 1))));
|
||||
texture:SetTexture(textureName..(((j - 1) * numTexturesWide) + k));
|
||||
texture:Show();
|
||||
end
|
||||
-- Tile n of the overlay is file <art>n, and its place in the grid follows from n.
|
||||
for tile = 1, columns * rows do
|
||||
local column = mod(tile - 1, columns) + 1;
|
||||
local row = ceil(tile / columns);
|
||||
local wide, fileWide = overlayTileExtent(column, columns, artWidth);
|
||||
local tall, fileTall = overlayTileExtent(row, rows, artHeight);
|
||||
used = used + 1;
|
||||
local texture = getglobal("WorldMapOverlay"..used);
|
||||
texture:SetWidth(wide);
|
||||
texture:SetHeight(tall);
|
||||
texture:SetTexCoord(0, wide/fileWide, 0, tall/fileTall);
|
||||
texture:ClearAllPoints();
|
||||
texture:SetPoint("TOPLEFT", originX + (256 * (column - 1)), -(originY + (256 * (row - 1))));
|
||||
texture:SetTexture(art..tile);
|
||||
texture:Show();
|
||||
end
|
||||
end
|
||||
for i=textureCount+1, NUM_WORLDMAP_OVERLAYS do
|
||||
getglobal("WorldMapOverlay"..i):Hide();
|
||||
for slot = used + 1, NUM_WORLDMAP_OVERLAYS do
|
||||
getglobal("WorldMapOverlay"..slot):Hide();
|
||||
end
|
||||
|
||||
if ( GetCurrentMapContinent() == 0 ) then
|
||||
@@ -147,7 +145,7 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ref verbatim: zone view → its continent; continent view → the world.
|
||||
-- The reference's rule: zone view → its continent; continent view → the world.
|
||||
function WorldMapZoomOutButton_OnClick()
|
||||
if ( GetCurrentMapZone() ~= 0 ) then
|
||||
SetMapZoom(GetCurrentMapContinent());
|
||||
@@ -156,9 +154,9 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ── The continent/zone dropdowns (ref verbatim, except the vararg walk: the reference's
|
||||
-- `arg.n` is written as select("#", ...), the 5.1-native spelling and this tree's house
|
||||
-- style for it). ──────────────────────────────────────────────────────────────────────
|
||||
-- ── The continent/zone dropdowns (the reference's wiring; its `arg.n` vararg walk is
|
||||
-- written here as select("#", ...), the 5.1-native spelling and this tree's house style
|
||||
-- for it). ────────────────────────────────────────────────────────────────────────────
|
||||
function WorldMapContinentsDropDown_OnLoad()
|
||||
UIDropDownMenu_Initialize(this, WorldMapContinentsDropDown_Initialize);
|
||||
UIDropDownMenu_SetWidth(130);
|
||||
@@ -223,50 +221,60 @@
|
||||
end
|
||||
end
|
||||
|
||||
-- ref verbatim: left click drills in via ProcessMapClick on normalized button coords;
|
||||
-- anything else zooms out.
|
||||
-- Where the cursor sits inside a map-sized frame, as a fraction of it measured from the
|
||||
-- TOPLEFT: (0,0) the top-left corner of the map art, (1,1) the bottom-right. Both sides of
|
||||
-- the host's map API — the click (ProcessMapClick) and the hover (UpdateMapHighlight) —
|
||||
-- take a position in exactly that space, and the cursor arrives in screen pixels with the
|
||||
-- y axis pointing the other way, so this is the one conversion between them: undo the
|
||||
-- frame's effective scale, then measure from its edges, which a frame gives up as its
|
||||
-- center and its size (each edge being half the size away from the center).
|
||||
local function cursorMapPosition(frame)
|
||||
local cursorX, cursorY = GetCursorPosition();
|
||||
local scale = frame:GetEffectiveScale();
|
||||
local midX, midY = frame:GetCenter();
|
||||
local frameWidth = frame:GetWidth();
|
||||
local frameHeight = frame:GetHeight();
|
||||
local fromLeft = (cursorX / scale) - (midX - (frameWidth/2));
|
||||
local fromTop = (midY + (frameHeight/2)) - (cursorY / scale);
|
||||
return fromLeft / frameWidth, fromTop / frameHeight;
|
||||
end
|
||||
|
||||
-- Left click drills into whatever the host finds at that map position; every other button
|
||||
-- is the zoom-out shortcut (ref WorldMapButton_OnClick — ours takes no button argument,
|
||||
-- since nothing but the map button itself routes here).
|
||||
function WorldMapButton_OnClick(mouseButton)
|
||||
CloseDropDownMenus();
|
||||
if ( mouseButton == "LeftButton" ) then
|
||||
local x, y = GetCursorPosition();
|
||||
x = x / WorldMapButton:GetEffectiveScale();
|
||||
y = y / WorldMapButton:GetEffectiveScale();
|
||||
local centerX, centerY = WorldMapButton:GetCenter();
|
||||
local width = WorldMapButton:GetWidth();
|
||||
local height = WorldMapButton:GetHeight();
|
||||
local adjustedY = (centerY + (height/2) - y) / height;
|
||||
local adjustedX = (x - (centerX - (width/2))) / width;
|
||||
ProcessMapClick( adjustedX, adjustedY);
|
||||
ProcessMapClick(cursorMapPosition(WorldMapButton));
|
||||
else
|
||||
WorldMapZoomOutButton_OnClick();
|
||||
end
|
||||
end
|
||||
|
||||
-- ref WorldMapButton_OnUpdate, reduced to hover + the player blip (party/raid/flags/
|
||||
-- corpse/ping are deferred with their wire). UpdateMapHighlight is a stub until the
|
||||
-- area-bitmap source lands (wow-re dispatch) — nil name/fileName take the hide paths.
|
||||
-- The per-frame hover pass (spec: ref WorldMapButton_OnUpdate), reduced to the area
|
||||
-- highlight + the player blip (party/raid/flags/ping are deferred with their wire).
|
||||
-- UpdateMapHighlight is a stub until the area-bitmap source lands (wow-re dispatch) — a
|
||||
-- nil area/art takes the hide paths.
|
||||
function WorldMapButton_OnUpdate(elapsed)
|
||||
local x, y = GetCursorPosition();
|
||||
x = x / this:GetEffectiveScale();
|
||||
y = y / this:GetEffectiveScale();
|
||||
local centerX, centerY = this:GetCenter();
|
||||
local width = this:GetWidth();
|
||||
local height = this:GetHeight();
|
||||
local adjustedY = (centerY + (height/2) - y ) / height;
|
||||
local adjustedX = (x - (centerX - (width/2))) / width;
|
||||
local name, fileName, texPercentageX, texPercentageY, textureX, textureY, scrollChildX, scrollChildY = UpdateMapHighlight( adjustedX, adjustedY );
|
||||
WorldMapFrameAreaLabel:SetText(name or "");
|
||||
if ( fileName ) then
|
||||
WorldMapHighlight:SetTexCoord(0, texPercentageX, 0, texPercentageY);
|
||||
WorldMapHighlight:SetTexture("Interface\\WorldMap\\"..fileName.."\\"..fileName.."Highlight");
|
||||
textureX = textureX * width;
|
||||
textureY = textureY * height;
|
||||
scrollChildX = scrollChildX * width;
|
||||
scrollChildY = -scrollChildY * height;
|
||||
if ( (textureX > 0) and (textureY > 0) ) then
|
||||
WorldMapHighlight:SetWidth(textureX);
|
||||
WorldMapHighlight:SetHeight(textureY);
|
||||
WorldMapHighlight:SetPoint("TOPLEFT", "WorldMapDetailFrame", "TOPLEFT", scrollChildX, scrollChildY);
|
||||
local area, art, cropX, cropY, sizeX, sizeY, atX, atY =
|
||||
UpdateMapHighlight(cursorMapPosition(this));
|
||||
WorldMapFrameAreaLabel:SetText(area or "");
|
||||
if ( art ) then
|
||||
-- The sub-area under the cursor lights up: <zone>Highlight holds one shape per
|
||||
-- sub-area, of which crop names the fraction actually used (the file is padded to
|
||||
-- a power of two), while size and at are the shape's extent and top-left corner as
|
||||
-- fractions of the map — so both scale by the button's pixel size. Nothing to draw
|
||||
-- for a degenerate shape, but the art is still swapped in for the next frame.
|
||||
WorldMapHighlight:SetTexCoord(0, cropX, 0, cropY);
|
||||
WorldMapHighlight:SetTexture("Interface\\WorldMap\\"..art.."\\"..art.."Highlight");
|
||||
local drawWidth = sizeX * width;
|
||||
local drawHeight = sizeY * height;
|
||||
if ( drawWidth > 0 and drawHeight > 0 ) then
|
||||
WorldMapHighlight:SetWidth(drawWidth);
|
||||
WorldMapHighlight:SetHeight(drawHeight);
|
||||
WorldMapHighlight:SetPoint("TOPLEFT", "WorldMapDetailFrame", "TOPLEFT", atX * width, -atY * height);
|
||||
WorldMapHighlight:Show();
|
||||
end
|
||||
else
|
||||
@@ -283,7 +291,7 @@
|
||||
WorldMapPlayer:SetPoint("CENTER", "WorldMapDetailFrame", "TOPLEFT", playerX, playerY);
|
||||
WorldMapPlayer:Show();
|
||||
end
|
||||
-- Position corpse (ref WorldMapFrame.lua:443-452, verbatim law: (0,0) = hide).
|
||||
-- Position corpse (ref WorldMapFrame.lua:443-452 — the law is that (0,0) means hide).
|
||||
local corpseX, corpseY = GetCorpseMapPosition();
|
||||
if ( corpseX == 0 and corpseY == 0 ) then
|
||||
WorldMapCorpse:Hide();
|
||||
|
||||
@@ -6,14 +6,19 @@ territory line) and SubZoneTextFrame (the 26px subzone line + the FFA "PvP Area"
|
||||
by the FadingFrame kit when the app fires the ZONE_CHANGED family off the player's area
|
||||
(`area.rs`).
|
||||
|
||||
Quoted from ref-ZoneText.xml as the live 5875 patch chain ships it (patch.MPQ overrides the
|
||||
interface.MPQ copy — decision 0246) — now all THREE of its frames, the third being
|
||||
AutoFollowStatus (the /follow status text, and the only thing ref-ZoneText.lua actually
|
||||
contains), which waited here until follow support existed and no longer does. The FadingFrame
|
||||
kit (ref-FadingFrame.lua, its own TOC line right before ZoneText.xml) is inlined verbatim below
|
||||
too: ZoneTextFrame/SubZoneTextFrame are its only consumers in the whole 1.12 FrameXML
|
||||
(grep-verified over the extraction), so it rides with them. AutoFollowStatus is NOT one of
|
||||
them — it rolls its own fade, which is why the kit stays a two-frame affair.
|
||||
The Lua below is OURS: our own implementation of the behaviour ref-ZoneText.xml specifies, as
|
||||
the live 5875 patch chain ships that file (patch.MPQ overrides the interface.MPQ copy — decision
|
||||
0246). What is quoted from it, deliberately, is the published surface and the measurements — the
|
||||
global names addons call (SetZoneText, the three Zone*Duration constants, every frame and
|
||||
fontstring name), the event names, the colours, and the frame geometry — because those are facts
|
||||
about how the real interface looks and behaves rather than choices of ours. All THREE of the
|
||||
reference file's frames are here, the third being AutoFollowStatus (the /follow status text, and
|
||||
the only thing ref-ZoneText.lua actually contains), which waited here until follow support
|
||||
existed and no longer does. The FadingFrame kit (ref-FadingFrame.lua, its own TOC line right
|
||||
before ZoneText.xml) is implemented below too — our own code behind that file's published API,
|
||||
since ZoneTextFrame/SubZoneTextFrame are its only consumers in the whole 1.12 FrameXML
|
||||
(grep-verified over the extraction), so it rides with them. AutoFollowStatus is NOT one of them —
|
||||
it rolls its own fade, which is why the kit stays a two-frame affair.
|
||||
|
||||
The subtle law worth naming (it looks like a bug and is not): SubZoneTextString — a child of
|
||||
SubZoneTextFrame — anchors to regions of the *other* frame (ZoneTextString / PVPInfoTextString),
|
||||
@@ -21,7 +26,7 @@ which is usually hidden when only the subzone changes. The resolver computes hid
|
||||
rects all the same, so a subzone-only splash seats where it would sit *under the invisible zone
|
||||
name* — the authentic lower position. ZoneTextFrame's OnEvent caches GetZoneText() silently on
|
||||
plain ZONE_CHANGED and splashes only when an INDOORS/NEW_AREA event finds the zone text changed;
|
||||
PvP info shows only on NEW_AREA (SetZoneText(1)).
|
||||
the territory line is asked for on NEW_AREA alone.
|
||||
-->
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
@@ -33,14 +38,23 @@ PvP info shows only on NEW_AREA (SetZoneText(1)).
|
||||
AUTOFOLLOWSTART = AUTOFOLLOWSTART or "Following %s.";
|
||||
AUTOFOLLOWSTOP = AUTOFOLLOWSTOP or "You stop following %s.";
|
||||
|
||||
-- The ref's localization identity (UIParent.lua), so the AutoFollowStatus body below can
|
||||
-- stay verbatim. Guarded like everything else here rather than leaning on UnitPopup.xml
|
||||
-- happening to load first — a one-line helper is no place to acquire a load-order edge.
|
||||
-- The ref's localization identity (UIParent.lua), which the AutoFollowStatus body below
|
||||
-- puts its message through as the reference does. Guarded like everything else here rather
|
||||
-- than leaning on UnitPopup.xml happening to load first — a one-line helper is no place to
|
||||
-- acquire a load-order edge.
|
||||
if ( not TEXT ) then
|
||||
TEXT = function(s) return s end
|
||||
end
|
||||
|
||||
-- ── ref-FadingFrame.lua, verbatim ──────────────────────────────────────────────────────
|
||||
-- ── The FadingFrame kit ────────────────────────────────────────────────────────────────
|
||||
-- Our implementation of the helper ref-FadingFrame.lua specifies. The global names and the
|
||||
-- `this.*` fields ARE that file's published API — addons and the frames below reach for them
|
||||
-- by name — so those are fixed; the code behind them is ours.
|
||||
--
|
||||
-- The contract: FadingFrame_Show() starts one timeline on the frame — ramp alpha up over
|
||||
-- fadeInTime, hold at full for holdTime, ramp back down over fadeOutTime, then hide. A leg
|
||||
-- configured to zero is simply skipped, and a frame that has never been configured (all
|
||||
-- three zero, per OnLoad) hides on its very first update.
|
||||
function FadingFrame_SetFadeInTime(this, time)
|
||||
this.fadeInTime = time;
|
||||
end
|
||||
@@ -51,9 +65,10 @@ PvP info shows only on NEW_AREA (SetZoneText(1)).
|
||||
this.fadeOutTime = time;
|
||||
end
|
||||
function FadingFrame_OnLoad()
|
||||
this.fadeInTime = 0;
|
||||
this.holdTime = 0;
|
||||
this.fadeOutTime = 0;
|
||||
-- Through the setters, so the field names are written down in exactly one place.
|
||||
FadingFrame_SetFadeInTime(this, 0);
|
||||
FadingFrame_SetHoldTime(this, 0);
|
||||
FadingFrame_SetFadeOutTime(this, 0);
|
||||
this:Hide();
|
||||
end
|
||||
function FadingFrame_Show()
|
||||
@@ -61,76 +76,164 @@ PvP info shows only on NEW_AREA (SetZoneText(1)).
|
||||
this:Show();
|
||||
end
|
||||
function FadingFrame_OnUpdate()
|
||||
-- One timeline, one alpha: which leg `elapsed` has reached decides the value, and
|
||||
-- running off the end of the last leg is the only way the frame stops.
|
||||
local elapsed = GetTime() - this.startTime;
|
||||
local fadeInTime = this.fadeInTime;
|
||||
if ( elapsed < fadeInTime ) then
|
||||
local alpha = (elapsed / fadeInTime);
|
||||
this:SetAlpha(alpha);
|
||||
local fadeIn = this.fadeInTime;
|
||||
local holdEnds = fadeIn + this.holdTime;
|
||||
local alpha;
|
||||
if ( elapsed < fadeIn ) then
|
||||
alpha = elapsed / fadeIn;
|
||||
elseif ( elapsed < holdEnds ) then
|
||||
alpha = 1.0;
|
||||
elseif ( elapsed < (holdEnds + this.fadeOutTime) ) then
|
||||
alpha = 1.0 - ((elapsed - holdEnds) / this.fadeOutTime);
|
||||
else
|
||||
this:Hide();
|
||||
return;
|
||||
end
|
||||
local holdTime = this.holdTime;
|
||||
if ( elapsed < (fadeInTime + holdTime) ) then
|
||||
this:SetAlpha(1.0);
|
||||
return;
|
||||
end
|
||||
local fadeOutTime = this.fadeOutTime;
|
||||
if ( elapsed < (fadeInTime + holdTime + fadeOutTime) ) then
|
||||
local alpha = 1.0 - ((elapsed - holdTime - fadeInTime) / fadeOutTime);
|
||||
this:SetAlpha(alpha);
|
||||
return;
|
||||
end
|
||||
this:Hide();
|
||||
this:SetAlpha(alpha);
|
||||
end
|
||||
|
||||
-- ── ref-ZoneText.xml <Script>, verbatim ────────────────────────────────────────────────
|
||||
-- ── The zone splash ───────────────────────────────────────────────────────────────────
|
||||
-- Our implementation of what ref-ZoneText.xml's own <Script> block and its two splash
|
||||
-- frames' handlers specify. The names below are that file's published API — addons re-render
|
||||
-- the splash through SetZoneText and retune it by writing the three durations — so the
|
||||
-- names and the numbers are fixed; the code between them is ours.
|
||||
ZoneFadeInDuration = 0.5;
|
||||
ZoneHoldDuration = 1.0;
|
||||
ZoneFadeOutDuration = 2.0;
|
||||
|
||||
-- How a HELD zone is tinted, keyed by who holds it. Three cases differing only in three
|
||||
-- floats want to be data — the same shape MinimapCluster.xml keeps for the minimap's zone
|
||||
-- label, which reads the same GetZonePVPInfo; duplicated there and here rather than
|
||||
-- published as a global the reference does not have. A pvpType that is not in the table is
|
||||
-- unheld (sanctuary, or a type we do not know), and gets no territory line and the zone
|
||||
-- font's own parchment colour back — ZoneTextFont's <Color> in Fonts.xml is this triple.
|
||||
local ZONE_PVP_TINT = {
|
||||
friendly = { r = 0.1, g = 1.0, b = 0.1 },
|
||||
hostile = { r = 1.0, g = 0.1, b = 0.1 },
|
||||
contested = { r = 1.0, g = 0.7, b = 0 },
|
||||
};
|
||||
local UNHELD_ZONE_TINT = { r = 1.0, g = 0.9294, b = 0.7607 };
|
||||
|
||||
-- The line under the zone name saying who holds the place. The localisation globals are
|
||||
-- read here, at call time, rather than folded into the table above: something later in the
|
||||
-- load order is allowed to replace them.
|
||||
local function ZoneTerritoryLine(pvpType, factionName)
|
||||
if ( pvpType == "contested" ) then
|
||||
return CONTESTED_TERRITORY;
|
||||
end
|
||||
return format(FACTION_CONTROLLED_TERRITORY, factionName);
|
||||
end
|
||||
|
||||
-- Re-render the whole splash from the host's idea of where the player is: the zone name,
|
||||
-- its tint, the territory line under it, the free-for-all call-out, and where the subzone
|
||||
-- line hangs. `showPVPInfo` is the caller's answer to "is this a NEW zone?" — a hop between
|
||||
-- subzones of one zone re-renders without it and gets the same tint with no territory line.
|
||||
function SetZoneText(showPVPInfo)
|
||||
ZoneTextString:SetText(GetZoneText());
|
||||
|
||||
local pvpType, factionName, isArena = GetZonePVPInfo();
|
||||
if ( pvpType == "friendly" ) then
|
||||
PVPInfoTextString:SetText(format(FACTION_CONTROLLED_TERRITORY, factionName));
|
||||
ZoneTextString:SetTextColor(0.1, 1.0, 0.1);
|
||||
SubZoneTextString:SetTextColor(0.1, 1.0, 0.1);
|
||||
PVPInfoTextString:SetTextColor(0.1, 1.0, 0.1);
|
||||
SubZoneTextString:SetPoint("TOP", "PVPInfoTextString", "BOTTOM", 0, 0);
|
||||
elseif ( pvpType == "hostile" ) then
|
||||
PVPInfoTextString:SetText(format(FACTION_CONTROLLED_TERRITORY, factionName));
|
||||
ZoneTextString:SetTextColor(1.0, 0.1, 0.1);
|
||||
SubZoneTextString:SetTextColor(1.0, 0.1, 0.1);
|
||||
PVPInfoTextString:SetTextColor(1.0, 0.1, 0.1);
|
||||
SubZoneTextString:SetPoint("TOP", "PVPInfoTextString", "BOTTOM", 0, 0);
|
||||
elseif ( pvpType == "contested" ) then
|
||||
PVPInfoTextString:SetText(CONTESTED_TERRITORY);
|
||||
ZoneTextString:SetTextColor(1.0, 0.7, 0);
|
||||
SubZoneTextString:SetTextColor(1.0, 0.7, 0);
|
||||
PVPInfoTextString:SetTextColor(1.0, 0.7, 0);
|
||||
local held = ZONE_PVP_TINT[pvpType];
|
||||
local tint = held or UNHELD_ZONE_TINT;
|
||||
ZoneTextString:SetTextColor(tint.r, tint.g, tint.b);
|
||||
SubZoneTextString:SetTextColor(tint.r, tint.g, tint.b);
|
||||
if ( held ) then
|
||||
-- The territory line wears the tint too — including when the branch below finds it
|
||||
-- has nothing to say. Nothing on screen can tell (a blank string has no colour to
|
||||
-- show), but an addon that reads the tint off the blank line reads this zone's.
|
||||
-- Guarded, because the unheld tint is the zone FONT's parchment and this line, a
|
||||
-- SubZoneTextFont string, is never given it.
|
||||
PVPInfoTextString:SetTextColor(tint.r, tint.g, tint.b);
|
||||
end
|
||||
|
||||
-- The territory line and the subzone line's seat are one decision, not two: the line
|
||||
-- is there only when somebody holds this zone AND the caller asked for it, and the
|
||||
-- subzone line drops to hang off the line exactly when there is one to hang off.
|
||||
if ( held and showPVPInfo ) then
|
||||
PVPInfoTextString:SetText(ZoneTerritoryLine(pvpType, factionName));
|
||||
SubZoneTextString:SetPoint("TOP", "PVPInfoTextString", "BOTTOM", 0, 0);
|
||||
else
|
||||
PVPInfoTextString:SetText("");
|
||||
ZoneTextString:SetTextColor(1.0, 0.9294, 0.7607);
|
||||
SubZoneTextString:SetTextColor(1.0, 0.9294, 0.7607);
|
||||
SubZoneTextString:SetPoint("TOP", "ZoneTextString", "BOTTOM", 0, 0);
|
||||
end
|
||||
|
||||
-- A free-for-all pit is called out under the subzone line and drags that line to the
|
||||
-- hostile red whatever the zone around it is — inside the pit, everyone is hostile.
|
||||
if ( isArena ) then
|
||||
local ffa = ZONE_PVP_TINT.hostile;
|
||||
PVPArenaTextString:SetText(FREE_FOR_ALL_TERRITORY);
|
||||
SubZoneTextString:SetTextColor(1.0, 0.1, 0.1);
|
||||
SubZoneTextString:SetTextColor(ffa.r, ffa.g, ffa.b);
|
||||
else
|
||||
PVPArenaTextString:SetText("");
|
||||
end
|
||||
if ( not showPVPInfo ) then
|
||||
PVPInfoTextString:SetText("");
|
||||
SubZoneTextString:SetPoint("TOP", "ZoneTextString", "BOTTOM", 0, 0);
|
||||
end
|
||||
end
|
||||
|
||||
-- ── ref-ZoneText.lua, verbatim ─────────────────────────────────────────────────────────
|
||||
-- The auto-follow status line. Note it does NOT use the FadingFrame kit above: its own
|
||||
-- OnUpdate rolls a plain linear alpha fade with no fade-in and no hold, started only on
|
||||
-- AUTOFOLLOW_END. BEGIN is a full reset (fadeTime cleared, alpha restored, shown), which
|
||||
-- is why a follow that switches subject needs no END first.
|
||||
-- Both splash frames are the same kind of thing — one fade timeline, and all three of the
|
||||
-- events the client splits an area change into (a move within the current zone, an interior
|
||||
-- threshold crossed, a genuinely new zone) — so the arming is written once and neither can
|
||||
-- drift from the other. Each decides for itself, in its OnEvent, whether the line it owns
|
||||
-- actually changed.
|
||||
function ZoneSplashFrame_OnLoad()
|
||||
FadingFrame_OnLoad();
|
||||
FadingFrame_SetFadeInTime(this, ZoneFadeInDuration);
|
||||
FadingFrame_SetHoldTime(this, ZoneHoldDuration);
|
||||
FadingFrame_SetFadeOutTime(this, ZoneFadeOutDuration);
|
||||
this:RegisterEvent("ZONE_CHANGED");
|
||||
this:RegisterEvent("ZONE_CHANGED_INDOORS");
|
||||
this:RegisterEvent("ZONE_CHANGED_NEW_AREA");
|
||||
end
|
||||
|
||||
-- The big zone name. A plain ZONE_CHANGED never splashes it: that event fires whenever the
|
||||
-- player crosses a subzone boundary inside one zone, so all it may do is keep the cached
|
||||
-- name honest. That silent cache write IS the subtle law in the header — it is what makes a
|
||||
-- later INDOORS/NEW_AREA that lands on the same name find nothing left to say.
|
||||
function ZoneTextFrame_OnEvent(event)
|
||||
local zoneText = GetZoneText();
|
||||
local changed = zoneText ~= this.zoneText;
|
||||
this.zoneText = zoneText;
|
||||
if ( event == "ZONE_CHANGED" or not changed ) then
|
||||
return;
|
||||
end
|
||||
-- Only a genuinely new zone earns the territory line under the name.
|
||||
SetZoneText(event == "ZONE_CHANGED_NEW_AREA");
|
||||
FadingFrame_Show();
|
||||
end
|
||||
|
||||
function SubZoneTextFrame_OnLoad()
|
||||
ZoneSplashFrame_OnLoad();
|
||||
-- The free-for-all call-out is red for good, so it is tinted once here rather than on
|
||||
-- every splash.
|
||||
local ffa = ZONE_PVP_TINT.hostile;
|
||||
PVPArenaTextString:SetTextColor(ffa.r, ffa.g, ffa.b);
|
||||
-- Prime both lines from wherever the player already is: this runs at load, and the next
|
||||
-- area event may not arrive until they walk somewhere.
|
||||
SetZoneText(true);
|
||||
SubZoneTextString:SetText(GetSubZoneText());
|
||||
end
|
||||
|
||||
-- The small subzone line. Any of the three events can splash it, and the text it is already
|
||||
-- showing IS its cache — it has no silent path to keep a separate one honest.
|
||||
function SubZoneTextFrame_OnEvent()
|
||||
local subZoneText = GetSubZoneText();
|
||||
if ( subZoneText == SubZoneTextString:GetText() ) then
|
||||
return;
|
||||
end
|
||||
SubZoneTextString:SetText(subZoneText);
|
||||
-- Re-render the zone half too, territory line and all. Registered second, this frame is
|
||||
-- dispatched last of the two (FIFO — wow-re `event-dispatch-order.md`), so on a new zone
|
||||
-- it is this call that leaves the territory line standing under the name.
|
||||
SetZoneText(true);
|
||||
FadingFrame_Show();
|
||||
end
|
||||
|
||||
-- ── The auto-follow status line ────────────────────────────────────────────────────────
|
||||
-- Our implementation of what ref-ZoneText.lua specifies — the only thing that .lua actually
|
||||
-- contains, and the reason it exists at all. Note it does NOT use the FadingFrame kit
|
||||
-- above: it rolls a plain linear alpha fade with no fade-in and no hold, armed only by
|
||||
-- AUTOFOLLOW_END. BEGIN is a full reset (fade cancelled, alpha restored, shown), which is
|
||||
-- why a follow that switches subject needs no END first. The 4s is the reference's.
|
||||
AUTOFOLLOW_STATUS_FADETIME = 4.0;
|
||||
|
||||
function AutoFollowStatus_OnLoad()
|
||||
@@ -140,36 +243,51 @@ PvP info shows only on NEW_AREA (SetZoneText(1)).
|
||||
end
|
||||
|
||||
function AutoFollowStatus_OnEvent(event)
|
||||
if ( event == "PLAYER_ENTERING_WORLD" ) then
|
||||
-- A world load clears the line outright: nothing to say, and nothing to fade.
|
||||
this:Hide();
|
||||
return;
|
||||
end
|
||||
|
||||
-- Both follow events land the same way — put up a line naming this.unit and show it —
|
||||
-- so all they differ in is the message and what they do to the fade.
|
||||
local message;
|
||||
if ( event == "AUTOFOLLOW_BEGIN" ) then
|
||||
this.unit = arg1;
|
||||
this.fadeTime = nil;
|
||||
this:SetAlpha(1.0);
|
||||
AutoFollowStatusText:SetText(format(TEXT(AUTOFOLLOWSTART),this.unit));
|
||||
this:Show();
|
||||
end
|
||||
if ( event == "AUTOFOLLOW_END" ) then
|
||||
message = AUTOFOLLOWSTART;
|
||||
elseif ( event == "AUTOFOLLOW_END" ) then
|
||||
this.fadeTime = AUTOFOLLOW_STATUS_FADETIME;
|
||||
AutoFollowStatusText:SetText(format(TEXT(AUTOFOLLOWSTOP),this.unit));
|
||||
this:Show();
|
||||
end
|
||||
if ( event == "PLAYER_ENTERING_WORLD" ) then
|
||||
this:Hide();
|
||||
message = AUTOFOLLOWSTOP;
|
||||
else
|
||||
return;
|
||||
end
|
||||
AutoFollowStatusText:SetText(format(TEXT(message), this.unit));
|
||||
this:Show();
|
||||
end
|
||||
|
||||
function AutoFollowStatus_OnUpdate(elapsed)
|
||||
if( this.fadeTime ) then
|
||||
if( elapsed >= this.fadeTime ) then
|
||||
this:Hide();
|
||||
else
|
||||
this.fadeTime = this.fadeTime - elapsed;
|
||||
local alpha = this.fadeTime / AUTOFOLLOW_STATUS_FADETIME;
|
||||
this:SetAlpha(alpha);
|
||||
end
|
||||
-- Only AUTOFOLLOW_END arms the countdown; until then the line just sits at full alpha.
|
||||
local remaining = this.fadeTime;
|
||||
if ( not remaining ) then
|
||||
return;
|
||||
end
|
||||
-- Out of time: hide, and leave fadeTime alone. It is stale from here until the next
|
||||
-- BEGIN clears it or the next END re-arms it, and a hidden frame gets no OnUpdate.
|
||||
if ( elapsed >= remaining ) then
|
||||
this:Hide();
|
||||
return;
|
||||
end
|
||||
remaining = remaining - elapsed;
|
||||
this.fadeTime = remaining;
|
||||
this:SetAlpha(remaining / AUTOFOLLOW_STATUS_FADETIME);
|
||||
end
|
||||
]]></Script>
|
||||
<!-- These frames are shown when the current zone or subzone changes.
|
||||
<!-- The two splash frames. Same box, same seat on screen, same fade: one carries the zone name
|
||||
and the territory line under it, the other the subzone line and the free-for-all call-out.
|
||||
Geometry is the reference's measurement, not a choice of ours — the 512-wide strings inside
|
||||
a 128 box anchored BOTTOM +512 are what the real splash occupies.
|
||||
(ref carries toplevel="true" on both; still dropped here, but the reason has changed.
|
||||
`SetToplevel` is no longer a gap — the raise law is built, `script::object::toplevel` —
|
||||
and its real trigger is Show, not a click, so "click-raise is meaningless on mouse-less
|
||||
@@ -208,29 +326,10 @@ PvP info shows only on NEW_AREA (SetZoneText(1)).
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
FadingFrame_OnLoad();
|
||||
FadingFrame_SetFadeInTime(this, ZoneFadeInDuration);
|
||||
FadingFrame_SetHoldTime(this, ZoneHoldDuration);
|
||||
FadingFrame_SetFadeOutTime(this, ZoneFadeOutDuration);
|
||||
this:RegisterEvent("ZONE_CHANGED");
|
||||
this:RegisterEvent("ZONE_CHANGED_INDOORS");
|
||||
this:RegisterEvent("ZONE_CHANGED_NEW_AREA");
|
||||
ZoneSplashFrame_OnLoad();
|
||||
</OnLoad>
|
||||
<OnEvent>
|
||||
local newText = GetZoneText();
|
||||
if ( event == "ZONE_CHANGED" ) then
|
||||
this.zoneText = newText;
|
||||
return;
|
||||
end
|
||||
if ( newText ~= this.zoneText ) then
|
||||
this.zoneText = newText;
|
||||
if ( event == "ZONE_CHANGED_NEW_AREA" ) then
|
||||
SetZoneText(1);
|
||||
else
|
||||
SetZoneText();
|
||||
end
|
||||
FadingFrame_Show();
|
||||
end
|
||||
ZoneTextFrame_OnEvent(event);
|
||||
</OnEvent>
|
||||
<OnUpdate>
|
||||
FadingFrame_OnUpdate();
|
||||
@@ -270,36 +369,22 @@ PvP info shows only on NEW_AREA (SetZoneText(1)).
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
FadingFrame_OnLoad();
|
||||
FadingFrame_SetFadeInTime(this, ZoneFadeInDuration);
|
||||
FadingFrame_SetHoldTime(this, ZoneHoldDuration);
|
||||
FadingFrame_SetFadeOutTime(this, ZoneFadeOutDuration);
|
||||
this:RegisterEvent("ZONE_CHANGED");
|
||||
this:RegisterEvent("ZONE_CHANGED_INDOORS");
|
||||
this:RegisterEvent("ZONE_CHANGED_NEW_AREA");
|
||||
PVPArenaTextString:SetTextColor(1.0, 0.1, 0.1);
|
||||
SetZoneText(1);
|
||||
SubZoneTextString:SetText(GetSubZoneText());
|
||||
SubZoneTextFrame_OnLoad();
|
||||
</OnLoad>
|
||||
<OnEvent>
|
||||
local text = SubZoneTextString:GetText();
|
||||
local newText = GetSubZoneText();
|
||||
if(text ~= newText) then
|
||||
SubZoneTextString:SetText(newText);
|
||||
SetZoneText(1);
|
||||
FadingFrame_Show();
|
||||
end
|
||||
SubZoneTextFrame_OnEvent();
|
||||
</OnEvent>
|
||||
<OnUpdate>
|
||||
FadingFrame_OnUpdate();
|
||||
</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- The auto-follow status line, quoted from ref-ZoneText.xml. The only thing ref-ZoneText.lua
|
||||
actually contains, and the reason that .lua exists at all; it waited here until follow
|
||||
support did (decisions 0890, 0893). `setAllPoints` over UIParent, so the line sits dead
|
||||
centre of the screen. BACKGROUND strata: it is under everything, which is why a full-screen
|
||||
panel hides it.
|
||||
<!-- The auto-follow status line — the third frame ref-ZoneText.xml declares, laid out as that
|
||||
file specifies it (the handlers behind it, like every other line of Lua here, are ours).
|
||||
It is the only thing ref-ZoneText.lua actually contains, and the reason that .lua exists at
|
||||
all; it waited here until follow support did (decisions 0890, 0893). `setAllPoints` over
|
||||
UIParent, so the line sits dead centre of the screen. BACKGROUND strata: it is under
|
||||
everything, which is why a full-screen panel hides it.
|
||||
|
||||
ONE DEVIATION, forced: the ref anchors the string CENTER to **WorldFrame**, and benilla has
|
||||
no WorldFrame — it is the reference's 3D-scene layer frame, which our renderer does not
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
//! ([`CharListMessage`]); the **pick policy** here decides what answers it — the pending pick
|
||||
//! (seamless reconnect, decision 0065), the `WOW_CHAR` env fast path, or the director's choice on
|
||||
//! the screen. The pick travels the [`CharPick`] channel; `Connected` flips us `InWorld`;
|
||||
//! a `/logout` round-trip ([`LoggedOutMessage`]) flips back with the pending pick cleared.
|
||||
//! a `/logout` round-trip ([`LoggedOutMessage`]) flips back to select with the pending pick
|
||||
//! cleared, and a **lost** session flips all the way back to the login screen (decision 1262 —
|
||||
//! the reference's `DISCONNECTED_FROM_SERVER`).
|
||||
//!
|
||||
//! The screen is the reference's own arrangement (`CharacterSelect.xml/.lua`, extracted off the
|
||||
//! patch chain — decision 0465): the fullscreen `UI_<Race>` glue scene with the **selected
|
||||
@@ -103,7 +105,15 @@ impl Plugin for CharSelectPlugin {
|
||||
(
|
||||
// The policy + transitions run in BOTH states: the roster auto-answer (reconnect
|
||||
// relogin) happens while `InWorld`, and the logout edge arrives there too.
|
||||
(apply_roster_policy, enter_on_connected, back_on_logout).chain(),
|
||||
// `back_on_disconnect` LAST: a session that died during entry queues both
|
||||
// edges into one frame, and the dead one has to win (decision 1262).
|
||||
(
|
||||
apply_roster_policy,
|
||||
enter_on_connected,
|
||||
back_on_logout,
|
||||
back_on_disconnect,
|
||||
)
|
||||
.chain(),
|
||||
(
|
||||
screen::materialize_screen,
|
||||
input::select_input,
|
||||
@@ -279,7 +289,15 @@ fn apply_roster_policy(
|
||||
}
|
||||
None
|
||||
}
|
||||
None => std::env::var("WOW_CHAR").ok(),
|
||||
// `WOW_CHAR`, or the login smoke's optional third field (decision 1262): the smoke's
|
||||
// seat exists because `WOW_CHAR` is also an *unattended* marker
|
||||
// ([`crate::run_mode::unattended_login`]), so using it to reach the world would switch
|
||||
// the very branch a session test is trying to exercise. Same fast path, no marker.
|
||||
None => std::env::var("WOW_CHAR").ok().or_else(|| {
|
||||
std::env::var("WOW_LOGIN_SMOKE")
|
||||
.ok()
|
||||
.and_then(|s| crate::login::smoke_character(&s))
|
||||
}),
|
||||
};
|
||||
}
|
||||
for msg in msgs.read() {
|
||||
@@ -354,6 +372,35 @@ fn enter_on_connected(
|
||||
}
|
||||
}
|
||||
|
||||
/// **The session died** → all the way back to the login screen (decision 1262), which is where the
|
||||
/// reference's `GlueParent.lua` puts it: `SetGlueScreen("login")` on `DISCONNECTED_FROM_SERVER`,
|
||||
/// with [`crate::login`] raising the "Disconnected from server" dialog over it.
|
||||
///
|
||||
/// Chained **after** [`enter_on_connected`] on purpose. Both edges can land in the same frame — the
|
||||
/// IO thread emits `Connected` before its read loop starts, so a stream that dies during entry
|
||||
/// queues `Connected` and `Disconnected` back to back and the app drains both at once. That is the
|
||||
/// race the report caught: the entry half won, and the client walked into a world whose session had
|
||||
/// already been torn down — a camera with no avatar, `SelfGuid` set, an entry awaiting a snap that
|
||||
/// could never arrive. Ordering it last makes the dead session the last word by construction.
|
||||
fn back_on_disconnect(
|
||||
mut msgs: MessageReader<crate::net::DisconnectedMessage>,
|
||||
mut roster: ResMut<Roster>,
|
||||
mut next: ResMut<NextState<ClientState>>,
|
||||
mut ui_hover: ResMut<crate::ui_script::PlayerUiHover>,
|
||||
mut ui_keys: ResMut<crate::ui_script::UiKeyboardCapture>,
|
||||
) {
|
||||
if !msgs.read().any(|m| m.session_over) {
|
||||
return;
|
||||
}
|
||||
// The pending pick is the reconnect's memory (0065). With no reconnect coming it would only
|
||||
// auto-answer the *next* roster — sending the player straight back into the world they were
|
||||
// just thrown out of, without ever seeing the screen.
|
||||
roster.pending_pick = None;
|
||||
ui_hover.0 = None;
|
||||
ui_keys.0 = false;
|
||||
next.set(ClientState::Login);
|
||||
}
|
||||
|
||||
/// A confirmed `/logout` → back to the glue layer, pick cleared (the follow-up roster must be
|
||||
/// shown, not auto-answered). Also releases the in-world UI's input latches — `feed_ui_input`
|
||||
/// stops running outside `InWorld`, so whatever it last wrote would otherwise stick.
|
||||
@@ -639,6 +686,88 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **A session that dies during world entry does not leave the client in the world**
|
||||
/// (decision 1262).
|
||||
///
|
||||
/// The IO thread emits `Connected` *before* its read loop starts, so a socket that dies mid-
|
||||
/// entry — which is exactly what a displacement kick looks like, a bare EOF and nothing else —
|
||||
/// queues `Connected` and `Disconnected` back to back, and the drain (`try_iter`) hands the app
|
||||
/// both in one frame. Both edges then fire in the same `Update`. Whichever writes `NextState`
|
||||
/// last wins, and before this the entry did: the client flipped `InWorld` against a world the
|
||||
/// same frame's teardown had already emptied, and stood there in a camera with no avatar.
|
||||
///
|
||||
/// The guard is the *ordering*, which nothing else can catch: each system is correct alone, the
|
||||
/// build is green either way, and the failure needs two logins racing on one account to
|
||||
/// reproduce by hand.
|
||||
#[test]
|
||||
fn a_session_lost_during_entry_beats_the_entry() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins((MinimalPlugins, bevy::state::app::StatesPlugin))
|
||||
.insert_state(ClientState::CharSelect)
|
||||
.init_resource::<Roster>()
|
||||
.init_resource::<crate::ui_script::PlayerUiHover>()
|
||||
.init_resource::<crate::ui_script::UiKeyboardCapture>()
|
||||
.add_message::<EnteredWorldMessage>()
|
||||
.add_message::<crate::net::DisconnectedMessage>()
|
||||
.add_systems(Update, (enter_on_connected, back_on_disconnect).chain());
|
||||
app.world_mut().resource_mut::<Roster>().pending_pick = Some(7);
|
||||
|
||||
// One frame carrying both halves of the race, in the order the drain produces them.
|
||||
app.world_mut().write_message(EnteredWorldMessage);
|
||||
app.world_mut()
|
||||
.write_message(crate::net::DisconnectedMessage {
|
||||
reason: "disconnected: world stream closed: failed to fill whole buffer".into(),
|
||||
end: benilla_protocol::SessionEnd::Lost,
|
||||
session_over: true,
|
||||
});
|
||||
app.update();
|
||||
app.update(); // `StateTransition` applies the pending state at the next frame
|
||||
|
||||
assert_eq!(
|
||||
*app.world().resource::<State<ClientState>>().get(),
|
||||
ClientState::Login,
|
||||
"the dead session must be the last word — the reference's DISCONNECTED_FROM_SERVER \
|
||||
puts the client back on the account screen, not into a world with no session",
|
||||
);
|
||||
assert_eq!(
|
||||
app.world().resource::<Roster>().pending_pick,
|
||||
None,
|
||||
"and the reconnect's pending pick goes with it, or the next roster walks straight \
|
||||
back into the world the player was just thrown out of",
|
||||
);
|
||||
}
|
||||
|
||||
/// The same frame with the session *not* over (a clean logout's teardown, or an unattended run
|
||||
/// keeping 0065's seamless reconnect) still enters the world — the guard above must not have
|
||||
/// turned every disconnect into a bounce to the login screen.
|
||||
#[test]
|
||||
fn a_teardown_that_is_not_the_end_leaves_the_entry_alone() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins((MinimalPlugins, bevy::state::app::StatesPlugin))
|
||||
.insert_state(ClientState::CharSelect)
|
||||
.init_resource::<Roster>()
|
||||
.init_resource::<crate::ui_script::PlayerUiHover>()
|
||||
.init_resource::<crate::ui_script::UiKeyboardCapture>()
|
||||
.add_message::<EnteredWorldMessage>()
|
||||
.add_message::<crate::net::DisconnectedMessage>()
|
||||
.add_systems(Update, (enter_on_connected, back_on_disconnect).chain());
|
||||
|
||||
app.world_mut().write_message(EnteredWorldMessage);
|
||||
app.world_mut()
|
||||
.write_message(crate::net::DisconnectedMessage {
|
||||
reason: "logged out".into(),
|
||||
end: benilla_protocol::SessionEnd::LoggedOut,
|
||||
session_over: false,
|
||||
});
|
||||
app.update();
|
||||
app.update();
|
||||
|
||||
assert_eq!(
|
||||
*app.world().resource::<State<ClientState>>().get(),
|
||||
ClientState::InWorld,
|
||||
);
|
||||
}
|
||||
|
||||
/// B119 — a created character is selected against the roster **already in hand**. `net::io`
|
||||
/// re-enumerates and emits the fresh roster BEFORE the create result, so by the time
|
||||
/// `note_created` runs the row exists and the roster message is spent; a version that only armed
|
||||
|
||||
@@ -311,11 +311,21 @@ fn drive_loading_screen(
|
||||
MessageReader<crate::net::WorldportMessage>,
|
||||
MessageReader<crate::net::TeleportMessage>,
|
||||
MessageReader<crate::net::LoggedOutMessage>,
|
||||
MessageReader<crate::net::DisconnectedMessage>,
|
||||
Option<Res<crate::net::PendingTransfer>>,
|
||||
Option<Res<crate::char_select::Roster>>,
|
||||
),
|
||||
) {
|
||||
let (state, mut entered, mut worldports, mut teleports, mut logouts, transfer, roster) = edges;
|
||||
let (
|
||||
state,
|
||||
mut entered,
|
||||
mut worldports,
|
||||
mut teleports,
|
||||
mut logouts,
|
||||
mut lost,
|
||||
transfer,
|
||||
roster,
|
||||
) = edges;
|
||||
let now = time.elapsed_secs();
|
||||
let map_id = current_map.as_ref().map(|m| m.0);
|
||||
// The physics hold releases on the same residency signal that clears this screen (decision
|
||||
@@ -362,13 +372,25 @@ fn drive_loading_screen(
|
||||
// down, but `CharSelect` only applies at the NEXT frame's state transition — cover the dead
|
||||
// world with plain black until the glue owns the screen. No art, no bar: the ref's
|
||||
// world→glue swap is a black cut, not a loading screen.
|
||||
if logouts.read().next().is_some() {
|
||||
// A dead session is the same world→glue cut (decision 1262), and it needs this arm more than
|
||||
// logout does: the entry race raises the cover on `EnteredWorldMessage` a few lines up and
|
||||
// arms `awaiting_snap` for a snap the dead socket will never send, so without the disarm here
|
||||
// the cover is what the player would have been left staring at.
|
||||
let session_over = lost.read().any(|m| m.session_over);
|
||||
if logouts.read().next().is_some() || session_over {
|
||||
screen.active = true;
|
||||
screen.blackout = true;
|
||||
screen.awaiting_snap = false;
|
||||
screen.pending_map = None;
|
||||
screen.ready_frames = 0;
|
||||
info!("loading screen: blackout (logout)");
|
||||
info!(
|
||||
"loading screen: blackout ({})",
|
||||
if session_over {
|
||||
"session lost"
|
||||
} else {
|
||||
"logout"
|
||||
}
|
||||
);
|
||||
}
|
||||
if screen.blackout && *state.get() != crate::char_select::ClientState::InWorld {
|
||||
// The glue is up (it renders above this root); the cover's job is done.
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
//! This module owns the **credential policy** — the 0193 §3 mirror for the IO thread's pre-logon
|
||||
//! park: the env fast path (any of `WOW_USER`/`WOW_PASS`/`WOW_CHAR` explicitly set auto-submits
|
||||
//! with the old `one`/`pone` defaults, so every probe/smoke invocation keeps working), the
|
||||
//! pending-credentials resubmit that preserves 0065's seamless reconnect (paced at the flat 3 s,
|
||||
//! app-side — the IO thread never sleeps), and the director's typed submit. A *refused* code
|
||||
//! (bad password) clears the intent and shows the authored `AUTH_*` dialog — never an auto-retry
|
||||
//! against a refusal.
|
||||
//! pending-credentials resubmit (paced at the flat 3 s, app-side — the IO thread never sleeps),
|
||||
//! and the director's typed submit. A *refused* code (bad password) clears the intent and shows
|
||||
//! the authored `AUTH_*` dialog — never an auto-retry against a refusal.
|
||||
//!
|
||||
//! **A session that is lost is over** (decision 1262): the reference's `GlueParent.lua` answers
|
||||
//! `DISCONNECTED_FROM_SERVER` with `SetGlueScreen("login")` + `GlueDialog_Show("DISCONNECTED")`,
|
||||
//! and so does this. 0065's seamless reconnect survives only where nobody is here to type — an
|
||||
//! unattended run ([`crate::run_mode::unattended_login`]) — because a client that
|
||||
//! re-authenticates on its own takes the account back off whoever just displaced it.
|
||||
//!
|
||||
//! Module split: this file (state, policy, input, dialogs, the saved-account persistence),
|
||||
//! [`screen`] (the authored layout, transcribed from `AccountLogin.xml`).
|
||||
@@ -99,8 +104,9 @@ enum IoPark {
|
||||
/// authenticated (or asked) with, the in-flight/park bookkeeping, and the resubmit timer.
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct LoginIntent {
|
||||
/// The session's credentials — kept while in-world so a reconnect re-authenticates silently
|
||||
/// (0065); cleared by select's Back, a refusal code, or a Cancel.
|
||||
/// The session's credentials — kept while in-world so the logout relist and an unattended
|
||||
/// run's reconnect re-authenticate silently (0065); cleared by select's Back, a refusal code,
|
||||
/// a Cancel, and by a lost session (1262 — they are the session's, and the session is over).
|
||||
creds: Option<(String, String)>,
|
||||
/// A submit is in flight (between our send and its LoginFailed/CharacterList answer).
|
||||
in_flight: bool,
|
||||
@@ -209,10 +215,10 @@ fn drive_policy(
|
||||
// The login smoke drives its own credentials instead.
|
||||
if !intent.env_read {
|
||||
intent.env_read = true;
|
||||
let any_set = ["WOW_USER", "WOW_PASS", "WOW_CHAR"]
|
||||
.iter()
|
||||
.any(|k| std::env::var_os(k).is_some());
|
||||
if any_set && std::env::var_os("WOW_LOGIN_SMOKE").is_none() {
|
||||
// The same fact a lost session asks about (decision 1262) — read from the one place that
|
||||
// owns it, so "the harness logs in for us" and "the harness logs back in for us" can never
|
||||
// be two different answers.
|
||||
if crate::run_mode::unattended_login() && std::env::var_os("WOW_LOGIN_SMOKE").is_none() {
|
||||
let user = std::env::var("WOW_USER").unwrap_or_else(|_| "one".into());
|
||||
let pass = std::env::var("WOW_PASS").unwrap_or_else(|_| "pone".into());
|
||||
// The account guard (decision 0649): a vmangos login KICKS whoever holds the account,
|
||||
@@ -276,13 +282,28 @@ fn drive_policy(
|
||||
}
|
||||
}
|
||||
for msg in disconnects.read() {
|
||||
// The IO thread is heading back to its pre-logon park. With session credentials the
|
||||
// re-auth is silent: immediate after a clean logout (the roster IS the select screen the
|
||||
// app now shows), paced after a stream death (the old reconnect delay, app-side).
|
||||
// The IO thread is heading back to its pre-logon park.
|
||||
intent.park = IoPark::AtLogin;
|
||||
intent.in_flight = false;
|
||||
if msg.session_over {
|
||||
// The reference's `DISCONNECTED_FROM_SERVER` (decision 1262): `GlueParent.lua` answers
|
||||
// it with `SetGlueScreen("login")` + `GlueDialog_Show("DISCONNECTED")` — the account
|
||||
// screen and one Okay button. Nothing retries, and the credentials go with the session:
|
||||
// a client that re-authenticates on its own steals the account back from whoever just
|
||||
// displaced it, which is the ping-pong the report described.
|
||||
warn!(
|
||||
"login: {} — session over, back to the login screen",
|
||||
msg.reason
|
||||
);
|
||||
intent.clear();
|
||||
dialog.open_error(strings.text("DISCONNECTED", "Disconnected from server"));
|
||||
continue;
|
||||
}
|
||||
// Otherwise the session continues through the park and the re-auth is silent: immediate
|
||||
// after a clean logout (the roster IS the select screen the app now shows), paced after a
|
||||
// stream death an unattended run must recover from on its own (0065, paced app-side).
|
||||
if intent.creds.is_some() {
|
||||
let delay = if msg.reason == "logged out" {
|
||||
let delay = if msg.end == benilla_protocol::SessionEnd::LoggedOut {
|
||||
0.0
|
||||
} else {
|
||||
RETRY_DELAY_SECS
|
||||
@@ -570,7 +591,7 @@ fn drive_quit(arm: Option<Res<QuitArm>>, time: Res<Time>, mut exit: MessageWrite
|
||||
|
||||
/// Which dialog is up: the connecting status (Cancel button, text driven by the stages) or an
|
||||
/// error (Okay button).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum DialogKind {
|
||||
Status,
|
||||
Error,
|
||||
@@ -741,9 +762,36 @@ fn save_account(name: &str) {
|
||||
|
||||
// ── Instruments ──────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The login smoke (`WOW_LOGIN_SMOKE=user:pass`, decision 0539 §7): once the screen is up, submit
|
||||
/// those credentials through the real screen path; exit success on reaching CharSelect, log + exit
|
||||
/// failure on a refusal — the wrong-password path is provable headlessly.
|
||||
/// Split `WOW_LOGIN_SMOKE=user:pass[:Character]` into its three fields. **Three-way, once**: a
|
||||
/// `split_once(':')` here would hand the character name to the password, which is exactly what it
|
||||
/// did on this instrument's first live run (`result 0x04`, and the wrong-password path is what
|
||||
/// this smoke exists to prove, so it looked plausible).
|
||||
fn smoke_spec(spec: &str) -> (&str, &str, Option<&str>) {
|
||||
let mut parts = spec.splitn(3, ':');
|
||||
let user = parts.next().unwrap_or_default();
|
||||
let pass = parts.next().unwrap_or_default();
|
||||
let character = parts.next().map(str::trim).filter(|n| !n.is_empty());
|
||||
(user, pass, character)
|
||||
}
|
||||
|
||||
/// The optional third field — the body to enter as, or `None` for the plain
|
||||
/// reach-character-select smoke. [`crate::char_select`]'s roster policy is what actually answers
|
||||
/// the roster with it.
|
||||
pub(crate) fn smoke_character(spec: &str) -> Option<String> {
|
||||
smoke_spec(spec).2.map(str::to_string)
|
||||
}
|
||||
|
||||
/// The login smoke (`WOW_LOGIN_SMOKE=user:pass[:Character]`, decision 0539 §7): once the screen is
|
||||
/// up, submit those credentials through the real screen path; exit success on reaching CharSelect,
|
||||
/// log + exit failure on a refusal — the wrong-password path is provable headlessly.
|
||||
///
|
||||
/// **Naming a character keeps the run going into the world instead of exiting** (decision 1262).
|
||||
/// This is the only headless way to reach the world down the *player's* path: `WOW_CHAR` also
|
||||
/// enters the world, but setting it makes the run **unattended**
|
||||
/// ([`crate::run_mode::unattended_login`]), and an unattended run takes the other branch at every
|
||||
/// session decision — so the attended half was untestable without a person at the keyboard. The
|
||||
/// pick itself stays `char_select`'s (`apply_roster_policy` reads the third field the same way it
|
||||
/// reads `WOW_CHAR`); this only declines to exit. Pair with `WOW_PROBE_EXIT_AT` to bound the run.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn debug_login_smoke(
|
||||
state: Res<State<ClientState>>,
|
||||
@@ -760,7 +808,7 @@ fn debug_login_smoke(
|
||||
};
|
||||
match *phase {
|
||||
0 if *state.get() == ClientState::Login && time.elapsed_secs() > 2.0 => {
|
||||
let (user, pass) = spec.split_once(':').unwrap_or((spec.as_str(), ""));
|
||||
let (user, pass, _) = smoke_spec(&spec);
|
||||
info!("login-smoke: submitting as {user}");
|
||||
let (user, pass) = (user.to_string(), pass.to_string());
|
||||
send_login(&mut intent, &submit, &abandon, &user, &pass, true);
|
||||
@@ -777,8 +825,15 @@ fn debug_login_smoke(
|
||||
}
|
||||
*phase = 2;
|
||||
} else if *state.get() == ClientState::CharSelect {
|
||||
info!("login-smoke: reached character select — done");
|
||||
exit.write(AppExit::Success);
|
||||
match smoke_character(&spec) {
|
||||
Some(name) => {
|
||||
info!("login-smoke: reached character select — entering as {name}");
|
||||
}
|
||||
None => {
|
||||
info!("login-smoke: reached character select — done");
|
||||
exit.write(AppExit::Success);
|
||||
}
|
||||
}
|
||||
*phase = 2;
|
||||
}
|
||||
}
|
||||
@@ -790,6 +845,134 @@ fn debug_login_smoke(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Stand `drive_policy` up on its own with the env fast path already spent, so the policy
|
||||
/// under test is the disconnect arm and nothing else — and so an ambient `WOW_USER` in
|
||||
/// whatever shell runs the suite cannot seed credentials behind the assertions.
|
||||
fn policy_app() -> App {
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
// Held for the App's life: a dropped receiver would turn every submit into an `Err` and
|
||||
// hide a policy that sent one.
|
||||
std::mem::forget(rx);
|
||||
let mut app = App::new();
|
||||
app.init_resource::<Time>()
|
||||
.init_resource::<LoginIntent>()
|
||||
.init_resource::<LoginDialog>()
|
||||
.insert_resource(LoginSubmit(tx))
|
||||
.insert_resource(LoginAbandon(std::sync::Arc::new(
|
||||
std::sync::atomic::AtomicU64::new(0),
|
||||
)))
|
||||
.add_message::<LoginStageMessage>()
|
||||
.add_message::<LoginFailedMessage>()
|
||||
.add_message::<DisconnectedMessage>()
|
||||
.add_systems(Update, drive_policy);
|
||||
app.world_mut().resource_mut::<LoginIntent>().env_read = true;
|
||||
app
|
||||
}
|
||||
|
||||
/// **A lost session does not log itself back in** (decision 1262).
|
||||
///
|
||||
/// This is the whole of the displacement report: log into the same account from the reference
|
||||
/// client, vmangos kicks us with a bare socket close, and 0065's paced resubmit — which cannot
|
||||
/// see *why* the socket died, because nothing on the wire says — re-authenticated three seconds
|
||||
/// later and kicked the client that had just displaced us. The account ping-ponged. The
|
||||
/// reference's `GlueParent.lua` answers `DISCONNECTED_FROM_SERVER` with the login screen and a
|
||||
/// one-button dialog, and retries nothing.
|
||||
#[test]
|
||||
fn a_lost_session_clears_the_credentials_and_shows_the_dialog() {
|
||||
let mut app = policy_app();
|
||||
app.world_mut().resource_mut::<LoginIntent>().creds = Some(("one".into(), "pone".into()));
|
||||
app.world_mut().write_message(DisconnectedMessage {
|
||||
reason: "disconnected: world stream closed: failed to fill whole buffer".into(),
|
||||
end: benilla_protocol::SessionEnd::Lost,
|
||||
session_over: true,
|
||||
});
|
||||
app.update();
|
||||
|
||||
let intent = app.world().resource::<LoginIntent>();
|
||||
assert!(
|
||||
intent.creds.is_none(),
|
||||
"the session's credentials die with the session — keeping them is what won the \
|
||||
account back off the client that displaced us",
|
||||
);
|
||||
assert!(intent.retry_at.is_none(), "and nothing is scheduled");
|
||||
let dialog = app.world().resource::<LoginDialog>();
|
||||
assert_eq!(dialog.kind, Some(DialogKind::Error));
|
||||
// The fallback literal: this App has no GlueStrings, and the table's own row is
|
||||
// `DISCONNECTED = "Disconnected from server";` (GlueStrings.lua) — the same words.
|
||||
assert_eq!(dialog.text, "Disconnected from server");
|
||||
}
|
||||
|
||||
/// A **clean logout's** teardown rides the same message and must keep its silent relist: the
|
||||
/// IO thread returns to the pre-logon park, and the roster it comes back with IS the character
|
||||
/// select the player asked for. Breaking this would strand `/logout` on the login screen.
|
||||
#[test]
|
||||
fn a_logout_teardown_still_relists_at_once() {
|
||||
let mut app = policy_app();
|
||||
app.world_mut().resource_mut::<LoginIntent>().creds = Some(("one".into(), "pone".into()));
|
||||
app.world_mut().write_message(DisconnectedMessage {
|
||||
reason: "logged out".into(),
|
||||
end: benilla_protocol::SessionEnd::LoggedOut,
|
||||
session_over: false,
|
||||
});
|
||||
app.update();
|
||||
|
||||
let intent = app.world().resource::<LoginIntent>();
|
||||
assert!(
|
||||
intent.creds.is_some(),
|
||||
"a logout keeps the account signed in"
|
||||
);
|
||||
assert!(
|
||||
intent.in_flight,
|
||||
"and the same tick resubmits — the delay for a logout is 0, so the roster comes \
|
||||
straight back",
|
||||
);
|
||||
assert!(
|
||||
app.world().resource::<LoginDialog>().kind.is_none(),
|
||||
"with no dialog: nothing went wrong",
|
||||
);
|
||||
}
|
||||
|
||||
/// An **unattended** run keeps 0065's paced reconnect on a lost session — the verdict rides
|
||||
/// the message, so the policy honours it without re-reading the environment.
|
||||
#[test]
|
||||
fn an_unattended_run_still_reconnects_on_its_own() {
|
||||
let mut app = policy_app();
|
||||
app.world_mut().resource_mut::<LoginIntent>().creds =
|
||||
Some(("probe1".into(), "pprobe1".into()));
|
||||
app.world_mut().write_message(DisconnectedMessage {
|
||||
reason: "disconnected: connection reset".into(),
|
||||
end: benilla_protocol::SessionEnd::Lost,
|
||||
session_over: false,
|
||||
});
|
||||
app.update();
|
||||
|
||||
let intent = app.world().resource::<LoginIntent>();
|
||||
assert!(intent.creds.is_some());
|
||||
assert_eq!(
|
||||
intent.retry_at,
|
||||
Some(RETRY_DELAY_SECS),
|
||||
"paced by the flat 3 s off a zeroed clock, not fired on the spot",
|
||||
);
|
||||
assert!(app.world().resource::<LoginDialog>().kind.is_none());
|
||||
}
|
||||
|
||||
/// The smoke spec splits three ways — the character name must never end up in the password.
|
||||
/// A `split_once` did exactly that on the instrument's first live run, and the refusal it
|
||||
/// produced (`0x04`) is indistinguishable from the wrong-password case this smoke exists to
|
||||
/// exercise.
|
||||
#[test]
|
||||
fn the_smoke_spec_splits_three_ways() {
|
||||
assert_eq!(
|
||||
smoke_spec("probe1:pprobe1:Probeone"),
|
||||
("probe1", "pprobe1", Some("Probeone"))
|
||||
);
|
||||
assert_eq!(smoke_spec("probe1:pprobe1"), ("probe1", "pprobe1", None));
|
||||
// A trailing colon names no body; a password may not itself contain one (the client caps
|
||||
// both fields at 16 letters and vmangos accounts have none).
|
||||
assert_eq!(smoke_spec("one:pass:"), ("one", "pass", None));
|
||||
assert_eq!(smoke_spec("one"), ("one", "", None));
|
||||
}
|
||||
|
||||
/// The code→string map quotes the client's own strings for the vmangos-verified rows.
|
||||
#[test]
|
||||
fn fail_text_maps_the_verified_codes() {
|
||||
|
||||
@@ -1383,11 +1383,41 @@ pub(crate) struct LoginStageMessage {
|
||||
|
||||
/// The session ended (socket death, logout's teardown edge) — bridged from the Net drain's
|
||||
/// `Disconnected` arm. [`crate::login`]'s policy reads it as "the IO thread is back at its
|
||||
/// pre-logon park": with pending credentials it schedules the silent re-auth (decision 0539 §3 —
|
||||
/// 0065's seamless reconnect, paced app-side).
|
||||
/// pre-logon park"; whether anything re-authenticates is [`Self::ends_the_session`]'s answer.
|
||||
#[derive(Message, Clone)]
|
||||
pub(crate) struct DisconnectedMessage {
|
||||
pub(crate) reason: String,
|
||||
/// How it ended — the fact the whole post-session behaviour turns on (decision 1262).
|
||||
pub(crate) end: benilla_protocol::SessionEnd,
|
||||
/// **Is this the reference's `DISCONNECTED_FROM_SERVER`** — the session over, the client back
|
||||
/// at the account screen behind a "Disconnected from server" dialog, and nothing retried?
|
||||
///
|
||||
/// Decided **once**, by [`Self::new`], at the one edge where the event enters the app
|
||||
/// (decision 1262). Four readers act on it — the world teardown, the screen flip, the black
|
||||
/// cover, the credential policy — and they may not disagree: a teardown that keeps the avatar
|
||||
/// for a reconnect that never comes is exactly the avatar-less free camera the report
|
||||
/// described. Carrying the verdict rather than re-deriving it four times is what makes that
|
||||
/// structural instead of conventional.
|
||||
pub(crate) session_over: bool,
|
||||
}
|
||||
|
||||
impl DisconnectedMessage {
|
||||
/// The session-end event as the app sees it, with [`Self::session_over`] settled here and
|
||||
/// nowhere else.
|
||||
///
|
||||
/// It is `false` twice over. A [`SessionEnd::LoggedOut`](benilla_protocol::SessionEnd::LoggedOut)
|
||||
/// teardown is the relist *inside* one session — the roster that follows IS the character
|
||||
/// select the player asked for. And an unattended run ([`crate::run_mode::unattended_login`])
|
||||
/// keeps 0065's seamless reconnect: a probe has nobody to press Okay.
|
||||
pub(crate) fn new(reason: String, end: benilla_protocol::SessionEnd) -> Self {
|
||||
let session_over =
|
||||
end == benilla_protocol::SessionEnd::Lost && !crate::run_mode::unattended_login();
|
||||
Self {
|
||||
reason,
|
||||
end,
|
||||
session_over,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A login attempt failed before the roster (decision 0539): `code` is the server's auth result
|
||||
|
||||
@@ -405,12 +405,13 @@ pub(super) fn apply_net_updates(
|
||||
logout.apply_response(reason, instant)
|
||||
}
|
||||
SessionEvent::LogoutCancelled => logout.apply_cancelled(),
|
||||
SessionEvent::Disconnected { reason } => {
|
||||
SessionEvent::Disconnected { reason, end } => {
|
||||
session::disconnected(
|
||||
reason,
|
||||
end,
|
||||
&mut commands,
|
||||
&mut index,
|
||||
&self_guid,
|
||||
&mut self_guid,
|
||||
&mut status,
|
||||
&mut names,
|
||||
&mut items,
|
||||
|
||||
@@ -135,9 +135,10 @@ pub(super) fn logged_out(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn disconnected(
|
||||
reason: String,
|
||||
end: benilla_protocol::SessionEnd,
|
||||
commands: &mut Commands,
|
||||
index: &mut GuidIndex,
|
||||
self_guid: &SelfGuid,
|
||||
self_guid: &mut SelfGuid,
|
||||
status: &mut NetStatus,
|
||||
names: &mut NameCache,
|
||||
items: &mut Items,
|
||||
@@ -164,9 +165,11 @@ pub(super) fn disconnected(
|
||||
) {
|
||||
// The reconnect-policy feed first (decision 0539): [`crate::login`] reads it as "the IO thread
|
||||
// is back at its pre-logon park".
|
||||
disconnects.write(DisconnectedMessage {
|
||||
reason: reason.clone(),
|
||||
});
|
||||
// Is the session over, or is this the pause inside one? Settled once, here, and carried on the
|
||||
// message to every other reader (decision 1262).
|
||||
let msg = DisconnectedMessage::new(reason.clone(), end);
|
||||
let over = msg.session_over;
|
||||
disconnects.write(msg);
|
||||
warn!("net: {reason} — tearing down the streamed world");
|
||||
// An announced-but-unfinished far teleport died with the socket.
|
||||
pending_transfer.0 = None;
|
||||
@@ -181,7 +184,16 @@ pub(super) fn disconnected(
|
||||
// connection loss is not a world event, and index-less fading entities would race
|
||||
// the reconnect's re-creates. Entities already mid-fade left the index earlier and
|
||||
// finish fading on their own.
|
||||
let keep = self_guid.0;
|
||||
//
|
||||
// **Unless the session is over** (decision 1262), and then the avatar goes too, exactly as
|
||||
// [`logged_out`] takes it: 0065 spares it *for the reconnect*, and with no reconnect coming
|
||||
// that spared body is a puppet with no server behind it. Keeping it was the free camera —
|
||||
// `SelfGuid` set, no entity, an entry that never finished — so the fact that decides whether
|
||||
// anything reconnects has to be the same fact that decides whether the body stays.
|
||||
let keep = if over { None } else { self_guid.0 };
|
||||
if over {
|
||||
self_guid.0 = None;
|
||||
}
|
||||
index.0.retain(|guid, e| {
|
||||
if Some(*guid) == keep {
|
||||
return true;
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
//! pacing, the env fast path, auto-relogin on reconnect, the director's click) is app-side
|
||||
//! ([`crate::login`], [`crate::char_select`]); this thread is a pure sequencer — it never sleeps.
|
||||
//!
|
||||
//! On a stream failure it emits a [`SessionEvent::Disconnected`] and returns to the login park;
|
||||
//! the app's pending-credentials policy re-establishes the session (decision 0065's seamless
|
||||
//! reconnect, paced app-side). A clean in-game logout ([`SessionEvent::LoggedOut`]) does the same
|
||||
//! with no failure. A single long-lived sibling write thread drains
|
||||
//! On a stream failure it emits a [`SessionEvent::Disconnected`] carrying
|
||||
//! [`SessionEnd::Lost`] and returns to the login park; a clean in-game logout
|
||||
//! ([`SessionEvent::LoggedOut`]) does the same with [`SessionEnd::LoggedOut`]. What happens next is
|
||||
//! the app's, and the two answers differ (decision 1262): the logout relists, while a loss ends the
|
||||
//! session at the account screen unless nobody is there to type. A single long-lived sibling write thread drains
|
||||
//! [`ClientCommand`](super::ClientCommand)s down to the server; each successful connection hands it
|
||||
//! the fresh [`WorldWriter`] over a swap channel, so "exactly one writer" is structural, not
|
||||
//! signalled. The ECS half (draining the events into components each frame, and tearing the
|
||||
@@ -27,8 +28,8 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use benilla_protocol::{
|
||||
host_port, messages, AuthReject, CharAction, LoginStage, Poll, SessionEvent, WardenRequired,
|
||||
WorldSession, WorldWriter, WORLD_PORT,
|
||||
host_port, messages, AuthReject, CharAction, LoginStage, Poll, SessionEnd, SessionEvent,
|
||||
WardenRequired, WorldSession, WorldWriter, WORLD_PORT,
|
||||
};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
@@ -190,6 +191,7 @@ pub(super) fn spawn_net(cfg: NetConfig, connect: bool) -> NetHandles {
|
||||
if events_tx
|
||||
.send(SessionEvent::Disconnected {
|
||||
reason: "logged out".into(),
|
||||
end: SessionEnd::LoggedOut,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
@@ -197,11 +199,14 @@ pub(super) fn spawn_net(cfg: NetConfig, connect: bool) -> NetHandles {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// A live-stream failure. No sleep — the app paces resubmits (0539 §3).
|
||||
// A live-stream failure — including a displacement kick, which reaches
|
||||
// us as a bare EOF and nothing else (decision 1262). No sleep: what
|
||||
// happens next is the app's policy, not this thread's (0539 §3).
|
||||
bevy::log::error!("net: {e:#}");
|
||||
if events_tx
|
||||
.send(SessionEvent::Disconnected {
|
||||
reason: format!("disconnected: {e:#}"),
|
||||
end: SessionEnd::Lost,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
|
||||
@@ -20,7 +20,6 @@ use benilla_world::model_render::{
|
||||
};
|
||||
use benilla_world::sky::SkyMaterial;
|
||||
use benilla_world::sun::{CelestialMaterial, StarMaterial};
|
||||
use benilla_world::wmo_sky::{WmoSkyboxExt, WmoSkyboxMaterial};
|
||||
|
||||
use super::WarmRig;
|
||||
|
||||
@@ -44,8 +43,8 @@ use super::WarmRig;
|
||||
/// celestial rig, the star dome, the cloud dome, the gradient dome, the liquid frame sets), so
|
||||
/// by the time the menagerie spawns behind the entry cover, iterating the store IS the complete
|
||||
/// reachable set — warming can never drift from what's spawned. The one exception is the WMO
|
||||
/// skybox, whose materials are built on first need: its pipeline key is texture-independent, so
|
||||
/// one representative material covers the lane.
|
||||
/// skybox, whose materials are built on first need; since decision 1264 it is not a lane of its own
|
||||
/// at all but a cross of `WowModelMaterial` keys, warmed with the model cross below.
|
||||
#[derive(SystemParam)]
|
||||
pub(super) struct WarmLanes<'w> {
|
||||
celestial: ResMut<'w, Assets<CelestialMaterial>>,
|
||||
@@ -53,7 +52,6 @@ pub(super) struct WarmLanes<'w> {
|
||||
clouds: ResMut<'w, Assets<CloudMaterial>>,
|
||||
sky: ResMut<'w, Assets<SkyMaterial>>,
|
||||
liquid: ResMut<'w, Assets<LiquidMaterial>>,
|
||||
skybox: ResMut<'w, Assets<WmoSkyboxMaterial>>,
|
||||
/// The plain-`StandardMaterial` lanes (0938): representatives for the on-demand nameplate
|
||||
/// and raid-mark materials go through this store the way their builders do.
|
||||
standard: ResMut<'w, Assets<StandardMaterial>>,
|
||||
@@ -121,6 +119,10 @@ pub(super) fn spawn_menagerie(
|
||||
// milliseconds behind a loading bar once per run, while a missed one is a director-felt live
|
||||
// stall. The observed set (28) is the floor, not the target.
|
||||
let mut mats: Vec<Handle<WowModelMaterial>> = Vec::new();
|
||||
// The sky lane is kept apart from `mats`: it rides the STATIC layout only and is never
|
||||
// far-twinned (a camera-anchored backdrop has no side of the water plane —
|
||||
// `model_render::classify_water_side` skips it) nor booth-duplicated (no portrait sees a sky).
|
||||
let mut sky_mats: Vec<Handle<WowModelMaterial>> = Vec::new();
|
||||
for two_sided in [false, true] {
|
||||
for blend in [
|
||||
ModelBlend::Opaque,
|
||||
@@ -155,6 +157,7 @@ pub(super) fn spawn_menagerie(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false, // warms the WORLD lane; the sky lane warms with its own model
|
||||
light,
|
||||
));
|
||||
}
|
||||
@@ -193,6 +196,7 @@ pub(super) fn spawn_menagerie(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false, // warms the WORLD lane; the sky lane warms with its own model
|
||||
light,
|
||||
));
|
||||
}
|
||||
@@ -204,6 +208,49 @@ pub(super) fn spawn_menagerie(
|
||||
cache, materials, None, two_sided, cutout, light,
|
||||
));
|
||||
}
|
||||
// The **WMO-skybox lane** (decision 1264): `sky_depth` is a `WowModelKey` axis — it
|
||||
// compiles the shader's forced-far-depth branch — so every skybox batch is a pipeline the
|
||||
// world cross above does not cover, however identical the rest of its key. Built exactly
|
||||
// the way `M2BatchMaterials::skybox` builds them: depth-write pinned off, depth-test
|
||||
// pinned on, the batch's own blend and sidedness. `CavernsOfTimeSky.m2` authors Opaque,
|
||||
// AlphaTest, Blend and additive-Blend across both sidednesses; Mod/Mod2x are warmed too
|
||||
// because nothing stops a skybox authoring them. Unwarmed, walking into the Caverns of
|
||||
// Time crater is 21 live compiles in the frame the sky appears.
|
||||
for blend in [
|
||||
ModelBlend::Opaque,
|
||||
ModelBlend::AlphaTest,
|
||||
ModelBlend::Blend,
|
||||
ModelBlend::Mod,
|
||||
ModelBlend::Mod2x,
|
||||
] {
|
||||
for additive in [false, true] {
|
||||
sky_mats.push(model_material(
|
||||
cache,
|
||||
materials,
|
||||
None,
|
||||
blend,
|
||||
two_sided,
|
||||
false,
|
||||
false,
|
||||
true, // every skybox batch the chain ships authors UNLIT
|
||||
additive,
|
||||
false,
|
||||
true, // …depth-write off
|
||||
false, // …depth-test on
|
||||
FogPolicy::Off,
|
||||
false,
|
||||
ShadeSel::Lit,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true, // the sky lane
|
||||
light,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// The ground-clutter lane (specialize's over-blend), both sidednesses — the first
|
||||
// verification leg caught the two-sided one compiling live. Its material is built by
|
||||
@@ -240,6 +287,7 @@ pub(super) fn spawn_menagerie(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false, // warms the WORLD lane; the sky lane warms with its own model
|
||||
light,
|
||||
);
|
||||
if let Some(m) = materials.get(&plain) {
|
||||
@@ -287,6 +335,7 @@ pub(super) fn spawn_menagerie(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false, // warms the WORLD lane; the sky lane warms with its own model
|
||||
light,
|
||||
);
|
||||
if let Some(m) = materials.get(&h) {
|
||||
@@ -354,6 +403,18 @@ pub(super) fn spawn_menagerie(
|
||||
}
|
||||
}
|
||||
|
||||
// The WMO-skybox rows (decision 1264): the STATIC PLAIN layout only, and only on the world
|
||||
// camera. `wmo_sky::build_skybox` inserts POSITION + NORMAL + UV_0 — `layouts[0]` exactly — a
|
||||
// skybox is never skinned (the asteroid belts' bones are a deferral, not a shipped lane), never
|
||||
// vertex-coloured (M2 carries no MOCV), never far-twinned, and never reaches a portrait booth.
|
||||
{
|
||||
let (plain_mesh, plain_aabb, _) = &layouts[0];
|
||||
for mat in &sky_mats {
|
||||
spawn_model_rig(commands, cam, None, plain_mesh, plain_aabb, false, mat);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The sky and water lanes (decision 0945 — 0837's scope was model-lane-only, and every hole
|
||||
// was a director-felt stall: the sun disc first drawn on stepping outdoors, the first water
|
||||
// in view, a spell's shards mid-cast). Every material that EXISTS in these Startup-populated
|
||||
@@ -417,19 +478,6 @@ pub(super) fn spawn_menagerie(
|
||||
for mat in lane_handles(&mut lanes.liquid) {
|
||||
spawn_lane_rig(commands, cam, None, &liquid_mesh, None, mat, &mut count);
|
||||
}
|
||||
// WMO skybox: the one on-demand lane — its materials are built at first need, but the
|
||||
// pipeline key is texture-independent, so one representative built the way
|
||||
// `wmo_sky::build_skybox` builds them covers the lane.
|
||||
let skybox = lanes.skybox.add(WmoSkyboxMaterial {
|
||||
base: StandardMaterial {
|
||||
unlit: true,
|
||||
cull_mode: None,
|
||||
..default()
|
||||
},
|
||||
extension: WmoSkyboxExt::default(),
|
||||
});
|
||||
spawn_lane_rig(commands, cam, None, &posuv, None, skybox, &mut count);
|
||||
|
||||
// The plain-`StandardMaterial` lanes (0938 — the director's evening log). The fallback cube
|
||||
// (`entities::CubeAssets`, drawn while any entity's model streams) uses the production mesh
|
||||
// + materials, on the world camera AND a booth layer (a cube-bodied target can reach a
|
||||
@@ -594,9 +642,10 @@ fn spawn_lane_rig<M: Material>(
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
/// A tiny triangle carrying POSITION + UV_0 only — the layout the real `Stars.m2` patches and
|
||||
/// the WMO skybox batches ship (`sun::setup` / `wmo_sky::build_skybox` insert exactly these
|
||||
/// two attributes). Main-world-resident (`RenderAssetUsages::default()`), so `calculate_bounds`
|
||||
/// A tiny triangle carrying POSITION + UV_0 only — the layout the real `Stars.m2` patches ship
|
||||
/// (`sun::setup` inserts exactly these two attributes). The WMO skybox used to share it; since
|
||||
/// decision 1264 it draws on the model lane and carries that lane's NORMAL too, so it rides
|
||||
/// `layouts[0]` instead. Main-world-resident (`RenderAssetUsages::default()`), so `calculate_bounds`
|
||||
/// covers it — no explicit Aabb needed.
|
||||
fn warm_pos_uv_mesh() -> Mesh {
|
||||
let mut m = Mesh::new(
|
||||
|
||||
@@ -216,10 +216,14 @@ impl Plugin for PlayerPlugin {
|
||||
.run_if(not(resource_exists::<crate::run_mode::CaptureMode>))
|
||||
.run_if(in_state(crate::char_select::ClientState::InWorld)),
|
||||
)
|
||||
// A confirmed `/logout` releases the avatar: the streamed entity is despawned by the
|
||||
// net drain, and dropping `active` re-arms the take-control latch for the next login
|
||||
// (possibly a different character). Ungated — the message lands as the state flips.
|
||||
.add_systems(Update, wire_in::release_on_logout.in_set(WorldStage::Input))
|
||||
// A session END releases the avatar — a confirmed `/logout`, or a lost session
|
||||
// (decision 1262): the streamed entity is despawned by the net drain either way, and
|
||||
// dropping `active` re-arms the take-control latch for the next login (possibly a
|
||||
// different character). Ungated — the message lands as the state flips.
|
||||
.add_systems(
|
||||
Update,
|
||||
wire_in::release_on_session_end.in_set(WorldStage::Input),
|
||||
)
|
||||
// Mirror our body's collision height onto `Player` for the swim arm (decision 0645).
|
||||
// A *continuous* sync rather than a one-shot at take-control, for the reason the
|
||||
// take-control branch itself records: a cross-map worldport re-streams the `SelfPlayer`
|
||||
|
||||
@@ -397,14 +397,23 @@ fn apply_self_move(
|
||||
player.wedge_still = 0;
|
||||
}
|
||||
|
||||
/// A confirmed `/logout` (decision 0193): drop control so the next login re-takes it from its own
|
||||
/// streamed `SelfPlayer` — possibly a different character on a different map (the boot path). The
|
||||
/// avatar entity itself is despawned by the net drain the same frame this message is written.
|
||||
pub(super) fn release_on_logout(
|
||||
mut msgs: MessageReader<crate::net::LoggedOutMessage>,
|
||||
/// **The avatar went away with the session** — drop control, so the next login re-takes it from
|
||||
/// its own streamed `SelfPlayer` (possibly a different character on a different map — the boot
|
||||
/// path). The entity itself is despawned by the net drain the same frame these messages are
|
||||
/// written.
|
||||
///
|
||||
/// Two edges, one answer (decision 1262): a confirmed `/logout` (decision 0193), and a **lost**
|
||||
/// session, which since 1262 takes the avatar too — there is no reconnect left for it to be the
|
||||
/// puppet of. Missing the second edge would leave `Player.active` true over a despawned entity: a
|
||||
/// controller driving nothing, which is the shape of the free camera this arc is about.
|
||||
pub(super) fn release_on_session_end(
|
||||
mut logouts: MessageReader<crate::net::LoggedOutMessage>,
|
||||
mut lost: MessageReader<crate::net::DisconnectedMessage>,
|
||||
mut player: ResMut<Player>,
|
||||
) {
|
||||
if msgs.read().next().is_some() {
|
||||
// Both readers drain unconditionally — `|`, not `||`: a short-circuit would leave the other
|
||||
// message unread, and its cursor would carry it into the next frame.
|
||||
if logouts.read().next().is_some() | lost.read().any(|m| m.session_over) {
|
||||
player.active = false;
|
||||
player.move_flags = 0;
|
||||
player.airborne_since = None;
|
||||
|
||||
@@ -51,6 +51,30 @@ pub(crate) fn scenario_active() -> bool {
|
||||
std::env::var("WOW_CAPTURE").is_ok()
|
||||
}
|
||||
|
||||
/// **Is anything but a person answering this session's login?** (`$WOW_USER` / `$WOW_PASS` /
|
||||
/// `$WOW_CHAR` — the login screen's env fast path.)
|
||||
///
|
||||
/// The player answer is `false`, and it is the default: with none of these set the client opens at
|
||||
/// the login screen and waits for somebody to type. Setting any of them *is* a harness saying "log
|
||||
/// in without me" — a probe, a smoke, a rig — so it is also the honest answer to the question a
|
||||
/// dead session asks: **is there anybody here to log back in?**
|
||||
///
|
||||
/// That question decides what a lost session does (decision 1262). A person gets the reference's
|
||||
/// answer — the world torn down, the account screen, "Disconnected from server", and nothing
|
||||
/// retried until they say so. An unattended run keeps 0065's seamless reconnect, because the
|
||||
/// alternative is a probe parked on a login dialog for the rest of its wall-clock. One environment
|
||||
/// fact, several readers, no shared symbol — the module doc's pattern, same as [`scenario_active`],
|
||||
/// and the reason the login policy's own fast path asks here instead of re-reading the three names.
|
||||
///
|
||||
/// The session-loss readers never call this directly: [`crate::net::DisconnectedMessage::new`]
|
||||
/// asks once, at the wire edge, and every reader acts on the verdict it carries — so the teardown,
|
||||
/// the screen flip and the credential policy cannot answer it three different ways.
|
||||
pub(crate) fn unattended_login() -> bool {
|
||||
["WOW_USER", "WOW_PASS", "WOW_CHAR"]
|
||||
.iter()
|
||||
.any(|k| std::env::var_os(k).is_some())
|
||||
}
|
||||
|
||||
/// **Which screen the client starts on.** A player starts at the login screen, always; a capture
|
||||
/// boots straight into the world (no net, no picker), and a *glue* capture boots onto the very
|
||||
/// screen it photographs.
|
||||
|
||||
@@ -57,8 +57,8 @@ pub(super) struct PetBarMemory {
|
||||
///
|
||||
/// Both are returned as **the names of globals**, not values — the reference's token convention.
|
||||
/// The name keys ship in `GlobalStrings.lua` (`PET_ACTION_ATTACK` = "Attack" at l.3029-3032); the
|
||||
/// texture globals are declared by `PetActionBar.xml`, quoting the reference's
|
||||
/// `PetActionBarFrame.lua:6-12` verbatim.
|
||||
/// texture globals are declared by `PetActionBar.xml` — our own code, under the names and with the
|
||||
/// art paths the reference's `PetActionBarFrame.lua:6-12` specifies (1260).
|
||||
pub(super) fn command_token(action: u32) -> Option<(&'static str, &'static str)> {
|
||||
Some(match action {
|
||||
PET_COMMAND_STAY => ("PET_ACTION_WAIT", "PET_WAIT_TEXTURE"),
|
||||
|
||||
@@ -63,6 +63,27 @@ macro_rules! corpus_or_skip {
|
||||
};
|
||||
}
|
||||
|
||||
/// The corpus **and** a client install — for the tests whose subject is a global that comes from
|
||||
/// the reference file this client SOURCES off the player's own patch chain rather than shipping
|
||||
/// ([`super::reference_ui`], decision 1234).
|
||||
///
|
||||
/// `corpus_or_skip!` alone is the wrong precondition for them. With the corpus present but no
|
||||
/// install, `ContainerFrameItemButton_OnEnter`/`_OnClick` are *legitimately* nil — that is the
|
||||
/// documented "no install, no file" behaviour, not a defect — and the test then fails for a reason
|
||||
/// that is not a bug. `reference_ui`'s own header already says tests that need the file gate on the
|
||||
/// install the way every other client-data test does; these four never got that gate when 1234
|
||||
/// added the sourcing seam and the tests in one landing.
|
||||
///
|
||||
/// It stayed invisible because it only bites where the install is *not* found, which on a dev
|
||||
/// machine is only `scripts/gates.sh`'s `player-tests` rung: `--no-default-features` compiles out
|
||||
/// the `dev` project-folder candidate, so the `WoW` symlink beside the worktree stops being visible.
|
||||
macro_rules! corpus_and_install_or_skip {
|
||||
() => {{
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
corpus_or_skip!()
|
||||
}};
|
||||
}
|
||||
|
||||
fn read_toc(root: &Path, name: &str) -> Toc {
|
||||
let path = root.join(name).join(format!("{name}.toc"));
|
||||
Toc::parse(&benilla_ui::source::decode(
|
||||
@@ -537,7 +558,7 @@ fn the_roster_seat_names_the_character_the_addons_will_meet() {
|
||||
/// hover over every slot raised and no tooltip ever appeared.
|
||||
#[test]
|
||||
fn hovering_a_bagnon_slot_shows_the_items_tooltip() {
|
||||
let root = corpus_or_skip!();
|
||||
let root = corpus_and_install_or_skip!();
|
||||
let mut s = open_bagnon(&root);
|
||||
let occupied = bagnon_button_for_slot(&s, 0, 1);
|
||||
|
||||
@@ -574,7 +595,7 @@ fn hovering_a_bagnon_slot_shows_the_items_tooltip() {
|
||||
/// `PickupContainerItem(this:GetParent():GetID(), this:GetID())`. The observable is the cursor.
|
||||
#[test]
|
||||
fn left_clicking_a_bagnon_slot_picks_the_item_up() {
|
||||
let root = corpus_or_skip!();
|
||||
let root = corpus_and_install_or_skip!();
|
||||
let mut s = open_bagnon(&root);
|
||||
let occupied = bagnon_button_for_slot(&s, 0, 1);
|
||||
|
||||
@@ -615,7 +636,7 @@ fn left_clicking_a_bagnon_slot_picks_the_item_up() {
|
||||
/// `UseContainerItem(bag, slot)` — use/equip the item.
|
||||
#[test]
|
||||
fn right_clicking_a_bagnon_slot_uses_the_item() {
|
||||
let root = corpus_or_skip!();
|
||||
let root = corpus_and_install_or_skip!();
|
||||
let mut s = open_bagnon(&root);
|
||||
let occupied = bagnon_button_for_slot(&s, 0, 1);
|
||||
|
||||
@@ -638,7 +659,7 @@ fn right_clicking_a_bagnon_slot_uses_the_item() {
|
||||
/// `ignoreModifiers` arm. Driven as a real gesture: press, move past the threshold, release.
|
||||
#[test]
|
||||
fn dragging_a_bagnon_slot_picks_the_item_up() {
|
||||
let root = corpus_or_skip!();
|
||||
let root = corpus_and_install_or_skip!();
|
||||
let mut s = open_bagnon(&root);
|
||||
let occupied = bagnon_button_for_slot(&s, 0, 1);
|
||||
let empty = bagnon_button_for_slot(&s, 0, 2);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The combo-point dots (decisions 0869, 0875) — the reference `ComboFrame` transcription driven
|
||||
//! The combo-point dots (decisions 0869, 0875) — our `ComboFrame` implementation driven
|
||||
//! through the real loader: the show/hide edges, the per-point highlight/shine fade chain, the
|
||||
//! "only newly-earned points flare" rule that `COMBO_FRAME_LAST_NUM_POINTS` exists to enforce, and
|
||||
//! the two gates that live in `GetComboPoints` rather than in this Lua — rogue-or-druid only, and
|
||||
|
||||
@@ -37,8 +37,8 @@ fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
|
||||
.unwrap();
|
||||
s.run("function PlaySound() end").unwrap();
|
||||
load_xml(&s, "Fonts.xml");
|
||||
// The shipped load order provides GameTooltip before the cluster; Minimap_Update's verbatim
|
||||
// ref block (the PVP tint slice, decision 0287) touches it from OnLoad on.
|
||||
// The shipped load order provides GameTooltip before the cluster; Minimap_Update's tooltip
|
||||
// half (the PVP tint slice, decision 0287) touches it from OnLoad on.
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MinimapCluster.xml");
|
||||
|
||||
@@ -155,7 +155,7 @@ fn tod_window(s: &UiScript) -> (f64, f64, f64, f64) {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The verbatim `GameTimeFrame_Update` law: the 50-px window over the 128×64 UI-TOD-Indicator
|
||||
/// `GameTimeFrame_Update`'s law, exactly: the 50-px window over the 128×64 UI-TOD-Indicator
|
||||
/// sits on the LEFT half (the sun) through the game day and slides +0.5 to the RIGHT half (the
|
||||
/// moon) outside it — night is before 5:30 AM or from 9:00 PM, boundaries included exactly as
|
||||
/// the ref compares (`< DAWN or >= DUSK`).
|
||||
@@ -189,7 +189,7 @@ fn game_time_frame_slides_the_sun_moon_window_on_the_game_clock() {
|
||||
|
||||
/// Hovering the indicator through the REAL pointer path (hit-test → OnEnter) shows the game-time
|
||||
/// tooltip, live-updates it while owned (the `IsOwned` refresh branch), and hides it on leave.
|
||||
/// This also pins the two loader-side pieces the transcription leans on: the `<Scripts>` walker's
|
||||
/// This also pins the two loader-side pieces this frame leans on: the `<Scripts>` walker's
|
||||
/// mouse auto-enable (the frame declares no enableMouse, like the reference — without the law the
|
||||
/// hit-test never captures) and the `<HitRectInsets>` hull.
|
||||
#[test]
|
||||
@@ -210,7 +210,7 @@ fn hovering_the_indicator_shows_and_live_updates_the_game_time_tooltip() {
|
||||
s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
|
||||
"OnEnter owns the tooltip (the Scripts-walker auto-enable capturing at {x},{y})"
|
||||
);
|
||||
// TwentyFourHourTime = 1 (enGB LocalizeFrames, the transcription header): 21:07, not 9:07 PM.
|
||||
// TwentyFourHourTime = 1 (enGB LocalizeFrames, per GameTime.xml's header): 21:07, not 9:07 PM.
|
||||
let text = |s: &UiScript| {
|
||||
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
|
||||
.unwrap()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The shipped `assets/ui/MoneyFrame.xml` — the reference's `MoneyFrameTemplate` /
|
||||
//! `SmallMoneyFrameTemplate` kit and the `MoneyTypeInfo` table behind it (decision 1190: a name
|
||||
//! the shipped 1.12 UI defines is FrameXML's to transcribe).
|
||||
//! `SmallMoneyFrameTemplate` kit and the `MoneyTypeInfo` table behind it (decision 1190: a name the
|
||||
//! shipped 1.12 UI defines is a name we publish too — under our own implementation of it, 1260).
|
||||
//!
|
||||
//! Nothing benilla ships consumes this file yet — our own eight coin displays run on
|
||||
//! MerchantFrame.xml's `BenillaMoney_*` slot kit, and MoneyFrame.xml's header is where the two are
|
||||
|
||||
@@ -302,11 +302,17 @@ fn an_input_box_from_the_template_carries_its_border_and_takes_text() {
|
||||
/// `MyScrollScrollBarScrollUpButton` through **two** levels of `$parent` — which is precisely what
|
||||
/// `ScrollFrame_OnLoad` getglobals on the way past.
|
||||
///
|
||||
/// The `UpdateScrollChildRect` leg is also the falsifier for this file's one Lua divergence:
|
||||
/// `ScrollFrame_OnScrollRangeChanged` reaches the thumb through `GetThumbTexture()` rather than the
|
||||
/// reference's `getglobal(bar:GetName().."ThumbTexture")`, because our loader never publishes a
|
||||
/// named `<ThumbTexture>` as a global. If that divergence were reverted, this call would raise on a
|
||||
/// The `UpdateScrollChildRect` leg is also the falsifier for the `<ThumbTexture>` publication:
|
||||
/// `ScrollFrame_OnScrollRangeChanged` reaches the thumb through
|
||||
/// `getglobal(bar:GetName().."ThumbTexture")`, which resolves only because our loader publishes a
|
||||
/// named `<ThumbTexture>` as a global. If that publication regressed, this call would raise on a
|
||||
/// nil index and `s.errors()` would not be empty.
|
||||
///
|
||||
/// (This used to claim the opposite — that we diverged to `GetThumbTexture()` "because our loader
|
||||
/// never publishes a named `<ThumbTexture>`", and called it "this file's one Lua divergence". It
|
||||
/// was already false when written: the XML has used `getglobal` throughout, at HEAD and now.
|
||||
/// Corrected rather than deleted, because a doc comment asserting a divergence that does not exist
|
||||
/// is exactly how someone later introduces a real one by "restoring" it.)
|
||||
#[test]
|
||||
fn a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep() {
|
||||
let mut s = harness();
|
||||
|
||||
@@ -110,6 +110,16 @@ pub struct WowModelKey {
|
||||
/// `StandardMaterial` derived from the same field (at −4e4 that constant is a ~0.5% relative
|
||||
/// depth pull — enough to clip a blade's coplanar sheen/glow layers behind the blade itself).
|
||||
far_side: bool,
|
||||
/// The **WMO-skybox lane** (`clutter_fade.z` bit 13 — `model_render::SKY_DEPTH_MARKER`): the
|
||||
/// building-owned painted sky, drawn as the ordinary M2 it is. `specialize` compiles the
|
||||
/// `WOW_SKY_DEPTH` fragment def, which emits `@builtin(frag_depth) = 0.0` — reverse-Z far, the
|
||||
/// sky depth law every other sky shader already obeys (`benilla_world::sky_order`). It is a key
|
||||
/// axis because writing `frag_depth` costs the whole pipeline its early-Z, and the model lane
|
||||
/// draws every doodad and wall in the frame; only this one camera-anchored model may pay it.
|
||||
/// Like [`Self::far_side`] it also zeroes the rasterizer bias constant: the lane's rung is a
|
||||
/// SORT rung (`sky_order::WMO_SKYBOX_BIAS`, ~−6e4), and the depth it writes is a constant the
|
||||
/// rasterizer must not be perturbing behind the shader's back.
|
||||
sky_depth: bool,
|
||||
// NB: the WMO authored batch order is deliberately NOT a key axis. It used to be (a
|
||||
// per-batch-index `DepthBiasState` constant), which made every batch index its own pipeline —
|
||||
// the city first-sight compile stall (decision 0837). The coplanar-layering nudge now rides
|
||||
@@ -130,6 +140,7 @@ impl From<&WowModelExt> for WowModelKey {
|
||||
modulate2x: markers & 0x100 != 0,
|
||||
zfill: markers & 0x200 != 0,
|
||||
far_side: markers & 0x800 != 0,
|
||||
sky_depth: markers & 0x2000 != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,6 +281,18 @@ impl MaterialExtension for WowModelExt {
|
||||
ds.bias.constant = 0;
|
||||
}
|
||||
}
|
||||
// The WMO-skybox lane: force the sky's far depth in the fragment, and — like the far-side
|
||||
// twin above — keep its big negative rung sort-only. The rung (`sky_order::WMO_SKYBOX_BIAS`)
|
||||
// exists to sink a camera-anchored backdrop under every world transparent; as a rasterizer
|
||||
// constant it would be perturbing an interpolated depth this pipeline discards anyway.
|
||||
if key.bind_group_data.sky_depth {
|
||||
if let Some(fragment) = descriptor.fragment.as_mut() {
|
||||
fragment.shader_defs.push("WOW_SKY_DEPTH".into());
|
||||
}
|
||||
if let Some(ds) = descriptor.depth_stencil.as_mut() {
|
||||
ds.bias.constant = 0;
|
||||
}
|
||||
}
|
||||
// The WMO authored-batch-order depth nudge (the coplanar MOBA layering determinism) is NOT
|
||||
// here any more: as a fixed-function `DepthBiasState` constant it made every batch index its
|
||||
// own PIPELINE — Stormwind alone queued ~3000 variants of this one shader, each a
|
||||
@@ -523,3 +546,32 @@ impl MaterialExtension for TerrainExtension {
|
||||
"embedded://benilla_assets/shaders/terrain.wgsl".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// The sky depth law, for the one sky element that draws on the MODEL lane: the WMO skybox
|
||||
/// ([`WowModelKey::sky_depth`]). Every other sky shader is checked the same way, together, in
|
||||
/// `benilla_world::sky_order::every_sky_shader_forces_the_far_depth` — this half lives here
|
||||
/// because the shader does. Without it a skybox silently goes back to being occluded by its own
|
||||
/// 94-yard shell radius instead of by world geometry (the regression decision 0588 fixed).
|
||||
#[test]
|
||||
fn the_sky_lane_forces_the_far_depth() {
|
||||
let src = include_str!("shaders/wow_model.wgsl");
|
||||
assert!(
|
||||
src.contains("#ifdef WOW_SKY_DEPTH\n out.depth = 0.0;\n#endif"),
|
||||
"the model lane's sky branch no longer forces the far depth — a WMO skybox's shell \
|
||||
radius is deciding occlusion again (benilla_world::sky_order, \"The depth law\")"
|
||||
);
|
||||
// The declaration must stay BEHIND the ifdef: writing `frag_depth` unconditionally costs
|
||||
// every doodad, creature and wall in the frame its early-Z, and this lane draws all of
|
||||
// them. Matched with its guard attached, so ungating it fails here rather than in a
|
||||
// frame-time regression nobody attributes to this file. (Counting occurrences instead
|
||||
// would trip over the prose above the struct, which names the builtin too.)
|
||||
assert!(
|
||||
src.contains("#ifdef WOW_SKY_DEPTH\n // Reverse-Z \"infinitely far\"",)
|
||||
&& src.contains(" @builtin(frag_depth) depth: f32,\n#endif"),
|
||||
"the model lane's sky output no longer declares frag_depth behind WOW_SKY_DEPTH — \
|
||||
either the sky writes no depth, or every model draw just lost its early-Z"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,29 @@
|
||||
pbr_fragment::pbr_input_from_standard_material,
|
||||
pbr_functions::alpha_discard,
|
||||
pbr_bindings,
|
||||
forward_io::{VertexOutput, FragmentOutput},
|
||||
forward_io::VertexOutput,
|
||||
mesh_view_bindings::view,
|
||||
mesh_functions,
|
||||
}
|
||||
|
||||
// Our own fragment output, so the SKY lane can force the far depth. Bevy's `forward_io::FragmentOutput`
|
||||
// is `@location(0) color` and nothing else (bevy_pbr 0.18.1 `forward_io.wgsl`), so this is that struct
|
||||
// plus one optional builtin — not a divergence from it.
|
||||
//
|
||||
// **`WOW_SKY_DEPTH` is a pipeline-key branch, never an unconditional field.** Declaring
|
||||
// `@builtin(frag_depth)` disables early-Z for the whole pipeline, and the model lane draws every
|
||||
// doodad, creature and WMO wall in the frame. The def is pushed only for the WMO-skybox lane
|
||||
// (`clutter_fade.z` bit 13, `model_render::SKY_DEPTH_MARKER`), which is one camera-anchored model.
|
||||
struct WowFragOut {
|
||||
@location(0) color: vec4<f32>,
|
||||
#ifdef WOW_SKY_DEPTH
|
||||
// Reverse-Z "infinitely far", under bevy's `GreaterEqual` test — the sky depth law
|
||||
// (`benilla_world::sky_order`, "The depth law"): the world paints over the sky whatever the
|
||||
// shell's radius, and the sky can never land in front of world geometry.
|
||||
@builtin(frag_depth) depth: f32,
|
||||
#endif
|
||||
}
|
||||
|
||||
// Per-material model uniforms packed at binding 100 (see `WowModelExt` in terrain.rs). Light + fog + the
|
||||
// SH coeffs moved OUT to the shared global-light storage buffer (below); only the per-material draw flags
|
||||
// remain. Field order MUST match the Rust struct.
|
||||
@@ -492,7 +510,7 @@ fn vertex(vertex: WowVertex) -> WowVsOut {
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> FragmentOutput {
|
||||
fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
|
||||
// HARD FAR-CLIP WALL (faithful `farclip` ~777 yd) — see terrain.wgsl. Per-pixel discard beyond the
|
||||
// projection far plane (planar eye-Z), so distant buildings/trees reveal closest-part-first and the
|
||||
// sky/WDL shows behind. `wow_light.fog_params.w` = farclip (0 ⇒ disabled). Clutter (≤70 yd) never hits it.
|
||||
@@ -986,7 +1004,10 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> FragmentOutp
|
||||
}
|
||||
|
||||
|
||||
var out: FragmentOutput;
|
||||
var out: WowFragOut;
|
||||
#ifdef WOW_SKY_DEPTH
|
||||
out.depth = 0.0;
|
||||
#endif
|
||||
// Raw gamma out (GAMMA LANE, 0161 — the frame's one decode is the FFXGlow combine). Alpha = the
|
||||
// faded cutout alpha (tex × fade) for the blend twin; ignored (blend off) on steady/opaque draws.
|
||||
// OPAQUE-INTENT alpha pin (clutter_fade.z bit 3, set in model_render for steady opaque/alpha-key
|
||||
|
||||
@@ -15,15 +15,23 @@
|
||||
//! enforces* as much as what we send. That is the point: it is the A/B that closes B241 and the one
|
||||
//! to run against any server a strict-mode rejection is reported from.
|
||||
//!
|
||||
//! Usage: `cargo run -p benilla-protocol --example version_check_probe -- <host[:port]> <user> <pass>`
|
||||
//! Give it an **install directory** as a fourth argument and it also derives that install's own
|
||||
//! integrity digest the way the real client does — HMAC-SHA1 keyed by the challenge over five
|
||||
//! binaries (decision 1265, mechanism in wow-re `system/net/scratch/logon-integrity-hash.md`) — and
|
||||
//! says whether it is the stock one. Diagnostic only: benilla always sends the published constant,
|
||||
//! so a patched `WoW.exe` shows up here without changing a single login.
|
||||
//!
|
||||
//! Usage: `cargo run -p benilla-protocol --example version_check_probe -- <host[:port]> <user> <pass> [install-dir]`
|
||||
//! (the local strict-mode realmd of decision 1263 is `127.0.0.1:3725`; the ordinary one is 3724.)
|
||||
|
||||
use std::net::TcpStream;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use benilla_protocol::{auth, host_port, AuthReject, AUTH_PORT, CLIENT_BUILD};
|
||||
use benilla_srp::{NormalizedString, PublicKey, SrpClientChallenge};
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
/// Which `crc_hash` the arm puts in the proof packet.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -96,12 +104,93 @@ fn attempt(host: &str, port: u16, user: &str, pass: &str, arm: Arm) -> Result<Op
|
||||
Err(anyhow!("eight dials, every `B` ambiguous — try again"))
|
||||
}
|
||||
|
||||
/// The five files the real client hashes, **in this order** — `ChecksumExecutables` at `0x5b1170`,
|
||||
/// walking the `char*` table at `0x85def0` (wow-re `system/net/scratch/logon-integrity-hash.md`).
|
||||
/// Order is load-bearing: exactly 1 of the 120 permutations reproduces the published digest.
|
||||
const SCANNED: [&str; 5] = [
|
||||
"WoW.exe",
|
||||
"fmod.dll",
|
||||
"ijl15.dll",
|
||||
"dbghelp.dll",
|
||||
"unicows.dll",
|
||||
];
|
||||
|
||||
/// HMAC-SHA1 with the 16-byte `crc_salt` as the key — the framing the client uses (`rep stos` of
|
||||
/// 64×`0x36` / 64×`0x5c`, salt XORed into both, `0x5b117e`–`0x5b11b6`). No long-key shortening,
|
||||
/// because the key is always the 16-byte salt; the client has no such step either.
|
||||
fn hmac_sha1(key: &[u8; 16], msg: &[u8]) -> [u8; 20] {
|
||||
let mut ipad = [0x36u8; 64];
|
||||
let mut opad = [0x5cu8; 64];
|
||||
for (i, k) in key.iter().enumerate() {
|
||||
ipad[i] ^= k;
|
||||
opad[i] ^= k;
|
||||
}
|
||||
let mut inner = Sha1::new();
|
||||
inner.update(ipad);
|
||||
inner.update(msg);
|
||||
let mut outer = Sha1::new();
|
||||
outer.update(opad);
|
||||
outer.update(inner.finalize());
|
||||
outer.finalize().into()
|
||||
}
|
||||
|
||||
/// Derive `H` from a 1.12.1 install the way the real client does, and say what it means. Purely a
|
||||
/// diagnostic: benilla sends the published constant for its build and OS (decision 1265), never this.
|
||||
/// A file that cannot be read is **skipped**, contributing no bytes — the client's own behaviour
|
||||
/// (four early-outs at `0x5b11eb`/`0x5b1212`/`0x5b121f`/`0x5b1246`), and the reason a Data-only
|
||||
/// install could never answer this challenge for itself.
|
||||
fn report_install(install_dir: &Path, crc_salt: &[u8; 16]) {
|
||||
println!("\n install {}", install_dir.display());
|
||||
let mut msg = Vec::new();
|
||||
let mut missing = Vec::new();
|
||||
for name in SCANNED {
|
||||
match std::fs::read(install_dir.join(name)) {
|
||||
Ok(bytes) => {
|
||||
println!(" {name:<12} {} bytes", bytes.len());
|
||||
msg.extend_from_slice(&bytes);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" {name:<12} UNREADABLE ({e}) — skipped, as the client skips it");
|
||||
missing.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
let h = hmac_sha1(crc_salt, &msg);
|
||||
let hex: String = h.iter().map(|b| format!("{b:02X}")).collect();
|
||||
println!(" H = {hex}");
|
||||
// What we actually send is `SHA1(A ‖ H)`, so compare the digests through that: equal proofs for
|
||||
// one `A` means equal `H`.
|
||||
let a = [0u8; 32];
|
||||
let matches = {
|
||||
let mut sha = Sha1::new();
|
||||
sha.update(a);
|
||||
sha.update(h);
|
||||
let derived: [u8; 20] = sha.finalize().into();
|
||||
derived == auth::version_proof(crc_salt, &a)
|
||||
};
|
||||
println!(
|
||||
" → {}",
|
||||
if matches {
|
||||
"matches the digest we send: this install is the stock one for the OS we declare"
|
||||
} else if !missing.is_empty() {
|
||||
"differs, and files are missing — an install like this could never answer the \
|
||||
challenge for itself, which is why we send a constant instead"
|
||||
} else {
|
||||
"differs from the digest we send. On macOS that is expected and harmless: we declare \
|
||||
OSX, so we send the Mac client's digest, while your install is the Windows one. \
|
||||
Otherwise one of the five is patched. Login is unaffected either way — the digest we \
|
||||
send does not come from here"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let [host_arg, user, pass] = match args.as_slice() {
|
||||
[h, u, p] => [h.clone(), u.clone(), p.clone()],
|
||||
let (host_arg, user, pass, install) = match args.as_slice() {
|
||||
[h, u, p] => (h.clone(), u.clone(), p.clone(), None),
|
||||
[h, u, p, i] => (h.clone(), u.clone(), p.clone(), Some(PathBuf::from(i))),
|
||||
_ => {
|
||||
eprintln!("usage: version_check_probe <host[:port]> <user> <pass>");
|
||||
eprintln!("usage: version_check_probe <host[:port]> <user> <pass> [install-dir]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
@@ -133,5 +222,8 @@ fn main() -> Result<()> {
|
||||
};
|
||||
println!(" crc_hash {} → {verdict}", arm.label());
|
||||
}
|
||||
if let Some(dir) = install {
|
||||
report_install(&dir, &crc_salt);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -82,6 +82,30 @@ pub enum LoginStage {
|
||||
Handshaking,
|
||||
}
|
||||
|
||||
/// **How a world session ended** — the fact [`SessionEvent::Disconnected`] carries alongside its
|
||||
/// reason string, and the one the client's whole post-session behaviour hangs on (decision 1262).
|
||||
///
|
||||
/// The two are not shades of the same thing. A logout is a door the player walked through, and the
|
||||
/// roster on the other side of it is the character-select screen they asked for. A loss is a door
|
||||
/// that slammed: the reference's engine fires `DISCONNECTED_FROM_SERVER`, and its `GlueParent.lua`
|
||||
/// answers `SetGlueScreen("login")` + `GlueDialog_Show("DISCONNECTED")` — back to the account
|
||||
/// screen, one Okay button, no retry. Before this the app told them apart by comparing the reason
|
||||
/// *string* to `"logged out"`, which is how "the session died" and "the session ended" came to share
|
||||
/// one code path.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SessionEnd {
|
||||
/// A clean, client-initiated logout the server confirmed (`SMSG_LOGOUT_COMPLETE`). The IO
|
||||
/// thread cycles the connection and a fresh [`SessionEvent::CharacterList`] follows — the
|
||||
/// teardown here is bookkeeping between two halves of one continuous session.
|
||||
LoggedOut,
|
||||
/// The stream died under us: a socket error, an EOF, or a post-roster handshake failure. A
|
||||
/// **displacement kick is exactly this** and carries nothing else — vmangos's
|
||||
/// `WorldSession::KickPlayer` is a bare `CloseSocket()`, no packet (verified in
|
||||
/// `vmangos-core/src/game/Server/WorldSession.cpp`), so "another client took the account" and
|
||||
/// "the server went away" are the same event on the wire and get the same answer.
|
||||
Lost,
|
||||
}
|
||||
|
||||
/// One decoded event from the world stream. Carries only primitives + the coarse [`EntityKind`]
|
||||
/// classification — no wire types leak to the app, and no running state lives here.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -131,8 +155,8 @@ pub enum SessionEvent {
|
||||
LogoutResponse { reason: u32, instant: bool },
|
||||
/// The server dropped a pending logout at our `CMSG_LOGOUT_CANCEL` (`SMSG_LOGOUT_CANCEL_ACK`).
|
||||
LogoutCancelled,
|
||||
/// The session ended (socket closed / handshake failure); carries a human-readable reason.
|
||||
Disconnected { reason: String },
|
||||
/// The session ended; carries a human-readable reason and **how** it ended ([`SessionEnd`]).
|
||||
Disconnected { reason: String, end: SessionEnd },
|
||||
/// An object entered range / was created: a unit, player, or GameObject at a raw-WoW pose. Carries
|
||||
/// the interpreted spawn identity (`display_id`/`scale`, decoded per object type here so the app
|
||||
/// needn't know the wire layout) plus the full descriptor `fields` mask the ECS seeds its per-object
|
||||
|
||||
@@ -13,7 +13,9 @@ pub mod messages;
|
||||
pub mod wire;
|
||||
pub mod world;
|
||||
pub use auth::AuthReject;
|
||||
pub use events::{decode, CharAction, EntityKind, LoginStage, MoveSpeeds, Poll, SessionEvent};
|
||||
pub use events::{
|
||||
decode, CharAction, EntityKind, LoginStage, MoveSpeeds, Poll, SessionEnd, SessionEvent,
|
||||
};
|
||||
pub use messages::{
|
||||
CharCreateReq, CharEnumItem, Character, CreateSpline, ItemInfo, JumpInfo, MonsterMoveFacing,
|
||||
MoveMode, ObjectFields, OwnerFallback, ServerPacket, SpeedKind, TransportPose,
|
||||
|
||||
@@ -104,6 +104,13 @@ pub struct MatKey {
|
||||
/// z-write on, drawn before its model's colour batches. Its own key axis so a twin can never
|
||||
/// dedupe onto a colour material.
|
||||
zfill: bool,
|
||||
/// This batch is part of a **WMO skybox** ([`crate::wmo_sky`]) — the building-owned painted sky,
|
||||
/// drawn on this lane like any other M2 since decision 1264. Its own key axis because the lane
|
||||
/// changes pipeline state ([`SKY_DEPTH_MARKER`]: forced far depth, no early-Z) and sort rung
|
||||
/// ([`skybox_sort_bias`]), so a skybox batch must never dedupe onto the identical-looking world
|
||||
/// batch — `CavernsOfTimeSky` and Elwynn share no texture today, but nothing enforces that and
|
||||
/// the collision would be a world doodad silently drawn at the far plane.
|
||||
sky_depth: bool,
|
||||
}
|
||||
|
||||
/// The per-material static terrain-shade **selector** baked into `sun_scale.x` — NOT an intensity
|
||||
@@ -204,6 +211,9 @@ pub fn model_material(
|
||||
// WINDOW midpoint-light flag (`WowModelExt::sidn`).
|
||||
sidn: Option<[u8; 3]>,
|
||||
window: bool,
|
||||
// This batch belongs to a **WMO skybox** — the sky lane: forced far depth, no rasterizer bias,
|
||||
// and the skybox sort rung instead of the ordinary batch-order eps. See [`MatKey::sky_depth`].
|
||||
sky_depth: bool,
|
||||
light: &Buffer,
|
||||
) -> Handle<WowModelMaterial> {
|
||||
let key = MatKey {
|
||||
@@ -228,6 +238,7 @@ pub fn model_material(
|
||||
sidn,
|
||||
window,
|
||||
zfill: false,
|
||||
sky_depth,
|
||||
};
|
||||
if let Some(h) = cache.fetch(&key) {
|
||||
return h;
|
||||
@@ -279,10 +290,17 @@ pub fn model_material(
|
||||
// load-bearing only for SAME-CENTRE stacks (M2 item overlays, always low-index); WMO batches
|
||||
// sort by their own distinct mesh centres. The cap stays under `FAR_KEY_PULL` so a far twin
|
||||
// of a capped batch still lands on the far band's one key integer.
|
||||
let depth_bias = if matches!(alpha_mode, AlphaMode::Blend) {
|
||||
(f32::from(batch_order) * BATCH_ORDER_SORT_EPS).min(BATCH_ORDER_SORT_CAP)
|
||||
} else {
|
||||
//
|
||||
// The SKY lane takes the same shape one rung down: a skybox is camera-anchored, so its view-z
|
||||
// is a few tens of yards and it would sort after the world's transparents rather than behind
|
||||
// them ([`crate::sky_order::WMO_SKYBOX_BIAS`]). Its opaque batches still take no bias — the
|
||||
// painted cube faces are in the opaque pass, which is exactly where the reference puts them.
|
||||
let depth_bias = if !matches!(alpha_mode, AlphaMode::Blend) {
|
||||
0.0
|
||||
} else if sky_depth {
|
||||
skybox_sort_bias(batch_order)
|
||||
} else {
|
||||
(f32::from(batch_order) * BATCH_ORDER_SORT_EPS).min(BATCH_ORDER_SORT_CAP)
|
||||
};
|
||||
let base = match texture {
|
||||
Some(image) => StandardMaterial {
|
||||
@@ -351,7 +369,8 @@ pub fn model_material(
|
||||
| (u16::from(blend == ModelBlend::Mod) << 7)
|
||||
| (u16::from(blend == ModelBlend::Mod2x) << 8)
|
||||
| (u16::from(fade_variant && source_cutout) * TWIN_CUTOUT_MARKER)
|
||||
| (u16::from(env_map) * ENV_MAP_MARKER),
|
||||
| (u16::from(env_map) * ENV_MAP_MARKER)
|
||||
| (u16::from(sky_depth) * SKY_DEPTH_MARKER),
|
||||
),
|
||||
0.0,
|
||||
),
|
||||
@@ -460,6 +479,40 @@ pub(crate) const TWIN_CUTOUT_MARKER: u16 = 1 << 10;
|
||||
/// only what the fragment stage samples, so it needs no pipeline of its own (decision 0837).
|
||||
pub(crate) const ENV_MAP_MARKER: u16 = 1 << 12;
|
||||
|
||||
/// `clutter_fade.z` marker bit 13: the **WMO-skybox lane** — this batch is part of a building's
|
||||
/// painted sky, so `specialize` compiles the shader's `WOW_SKY_DEPTH` branch (forced far depth) and
|
||||
/// drops the rasterizer bias constant. It *is* a
|
||||
/// [`benilla_assets::materials::WowModelKey`] axis, unlike [`ENV_MAP_MARKER`]: writing
|
||||
/// `@builtin(frag_depth)` costs the pipeline its early-Z, so exactly one camera-anchored model may
|
||||
/// pay it and every other draw in the frame must keep the pipeline that doesn't.
|
||||
pub(crate) const SKY_DEPTH_MARKER: u16 = 1 << 13;
|
||||
|
||||
/// Sort-bias step per authored batch on the **WMO-skybox lane** — [`BATCH_ORDER_SORT_EPS`]'s job,
|
||||
/// resized for the rung it has to survive.
|
||||
///
|
||||
/// The plain 1e-3 eps cannot be used here: added to [`crate::sky_order::WMO_SKYBOX_BIAS`] (−6e4) it
|
||||
/// falls *under the f32 ulp at that magnitude* (2¹⁵·2⁻²³ = 0.0039), so every batch rounds onto the
|
||||
/// same bias and the sort tie the eps exists to break comes back in silence. This step is 4 ulps
|
||||
/// there — exactly representable, strictly monotone. The tie is not a rare coplanar case on this
|
||||
/// lane: a skybox is anchored at the eye, so *every* pair of its batches shares a sort distance,
|
||||
/// every frame.
|
||||
pub(crate) const SKYBOX_ORDER_EPS: f32 = 0.015625;
|
||||
|
||||
/// The skybox band's ceiling. With [`FAR_KEY_PULL`] it keeps the whole band on ONE pipeline-key
|
||||
/// integer — 0837's law: the base `StandardMaterialKey` packs `depth_bias as i32`, so a band
|
||||
/// straddling an integer is a live pipeline compile per side. Pulled by just under 1 and capped at
|
||||
/// 0.9, every batch lands in `(RUNG − 0.99, RUNG − 0.09]`, which truncates toward zero onto the
|
||||
/// same integer for all of them. The cap ties batches past index 57 (`0.9 / SKYBOX_ORDER_EPS`); the
|
||||
/// largest skybox the chain ships is 21 batches (`CavernsOfTimeSky.m2`).
|
||||
pub(crate) const SKYBOX_ORDER_CAP: f32 = 0.9;
|
||||
|
||||
/// This batch's slot in the skybox band: the lane rung, pulled onto one pipeline key, plus the
|
||||
/// authored batch order. See [`SKYBOX_ORDER_EPS`] and [`crate::sky_order::WMO_SKYBOX_BIAS`].
|
||||
fn skybox_sort_bias(batch_order: u16) -> f32 {
|
||||
crate::sky_order::WMO_SKYBOX_BIAS - FAR_KEY_PULL
|
||||
+ (f32::from(batch_order) * SKYBOX_ORDER_EPS).min(SKYBOX_ORDER_CAP)
|
||||
}
|
||||
|
||||
/// The twin's `Transparent3d` sort bias (yards; **negative = drawn earlier** — the sign law is
|
||||
/// `sky_order`'s module doc). It must clear the spread of one model's part AABB centres, so **all**
|
||||
/// of a fading model's twins draw before **any** of its colour parts — the reference achieves the
|
||||
@@ -524,6 +577,9 @@ pub fn zfill_material(
|
||||
sidn: None,
|
||||
window: false,
|
||||
zfill: true,
|
||||
// A depth-prime twin exists to write depth for a fading model; the sky writes none, so no
|
||||
// skybox batch ever has one (`M2BatchMaterials::skybox` builds no twins at all).
|
||||
sky_depth: false,
|
||||
};
|
||||
if let Some(h) = cache.fetch(&key) {
|
||||
return h;
|
||||
@@ -871,7 +927,12 @@ fn classify_part(
|
||||
let far_now = marked || swapped;
|
||||
let qualifies = match materials.get(near.id()) {
|
||||
Some(m) => {
|
||||
matches!(m.base.alpha_mode, AlphaMode::Blend) && m.extension.model_flags.x <= 0.5
|
||||
matches!(m.base.alpha_mode, AlphaMode::Blend)
|
||||
&& m.extension.model_flags.x <= 0.5
|
||||
// A WMO skybox is anchored at the eye and forces the far depth: it has no side of
|
||||
// the water plane to be on, and the far twin's −4e4 would sink its own sort rung
|
||||
// into the world band. The sky is never part of the interleave.
|
||||
&& (m.extension.clutter_fade.z as u16) & SKY_DEPTH_MARKER == 0
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
@@ -1118,6 +1179,57 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The **WMO-skybox band**: the four properties the lane's whole ordering rests on, none of
|
||||
/// which survives being reasoned about in prose (decision 1264). The band is arithmetic at a
|
||||
/// magnitude where f32 stops being able to represent the steps, which is exactly how the
|
||||
/// ordinary eps would have failed here in silence.
|
||||
#[test]
|
||||
fn the_skybox_band_orders_its_batches_on_one_pipeline_key() {
|
||||
// `CavernsOfTimeSky.m2` is 21 batches; `order` is the index + 1.
|
||||
let band: Vec<f32> = (0..=21).map(super::skybox_sort_bias).collect();
|
||||
|
||||
// 1. STRICTLY INCREASING — the whole point. At −6e4 the ordinary 1e-3 eps rounds every
|
||||
// one of these onto the same f32 and the sort tie the eps exists to break comes back.
|
||||
for pair in band.windows(2) {
|
||||
assert!(
|
||||
pair[1] > pair[0],
|
||||
"skybox batch order does not resolve at the rung's magnitude: {pair:?} — \
|
||||
SKYBOX_ORDER_EPS is under the f32 ulp there"
|
||||
);
|
||||
}
|
||||
|
||||
// 2. ONE PIPELINE KEY for the whole band (0837's law: `StandardMaterialKey` packs
|
||||
// `depth_bias as i32`, so a band straddling an integer is a live compile per side).
|
||||
// Checked for ANY u16 order, not just the 21 real ones — the cap is what holds it.
|
||||
let key = band[0] as i32;
|
||||
for order in [0u16, 1, 21, 57, 58, 1000, u16::MAX] {
|
||||
assert_eq!(
|
||||
super::skybox_sort_bias(order) as i32,
|
||||
key,
|
||||
"batch order {order} mints a second skybox pipeline"
|
||||
);
|
||||
}
|
||||
|
||||
// 3. UNDER EVERY WORLD TRANSPARENT. The deepest a world draw can sort is the far-side
|
||||
// water rung plus a far-plane view-z; the skybox's own camera-anchored view-z (its
|
||||
// shell radius, ~94 yd for Caverns of Time) rides on top of the rung and must not
|
||||
// close the gap.
|
||||
const FAR_PLANE: f32 = 3000.0;
|
||||
const SHELL: f32 = 128.0;
|
||||
let deepest_world = crate::sky_order::FAR_SIDE_BIAS - FAR_PLANE - super::FAR_KEY_PULL;
|
||||
assert!(
|
||||
band.last().unwrap() + SHELL < deepest_world,
|
||||
"a skybox can sort in front of a world transparent — the painted sky would paint \
|
||||
over it (sky_order::WMO_SKYBOX_BIAS)"
|
||||
);
|
||||
|
||||
// 4. The rung is the SMALLEST job it can be, per `sky_order`'s "as small as its ordering
|
||||
// job allows": a magnitude an order over the world band is margin, two is drift.
|
||||
const {
|
||||
assert!(crate::sky_order::WMO_SKYBOX_BIAS > 10.0 * crate::sky_order::FAR_SIDE_BIAS);
|
||||
}
|
||||
}
|
||||
|
||||
/// The far-side twin is its source shifted exactly one water rung with the marker bit added —
|
||||
/// and every OTHER marker bit rides along untouched (a fade twin's cutout, a zfill twin's
|
||||
/// prime, the multiply blends and the fog policy all keep their pipeline identity on the far
|
||||
|
||||
@@ -101,6 +101,72 @@ impl M2BatchMaterials<'_> {
|
||||
))
|
||||
}
|
||||
|
||||
/// One material for a batch of a **WMO skybox** — the painted sky a building owns
|
||||
/// ([`crate::wmo_sky`]), which is an ordinary M2 and is drawn as one (decision 1264).
|
||||
///
|
||||
/// Everything that makes it a *skybox* rather than a doodad is here, and it is only three
|
||||
/// things. Blend mode, sidedness, the UNLIT and UNFOGGED bits, the alpha-key reference, the
|
||||
/// multiply factors and the additive gamma premultiply all come off the authored batch, like
|
||||
/// any other model's — which is the whole point: the lane this replaced read positions, UVs and
|
||||
/// one texture and drew every batch opaque, so `CavernsOfTimeSky`'s six ADDITIVE star sheets
|
||||
/// (near-white RGB whose stars live in the ALPHA channel) painted a flat white sheet over the
|
||||
/// painted sky, and its planets and asteroid belts drew as dark opaque cards.
|
||||
///
|
||||
/// - **The far depth** (`sky_depth`): the sky's whole occlusion law is that it writes the
|
||||
/// reverse-Z far value and lets the world paint over it ([`crate::sky_order`], "The depth
|
||||
/// law").
|
||||
/// - **Depth-write OFF, always**, whatever the batch authored — a sky element leaves the
|
||||
/// z-buffer at its clear value so the forced-far glare quads stay occluded by world geometry
|
||||
/// alone (the rule [`crate::sky`] gives the gradient dome).
|
||||
/// - **Depth-test ON, always**, and this one *overrides* the batch. Every `CavernsOfTimeSky`
|
||||
/// batch sets M2 flag `0x08` (no depth test), which on this lane becomes
|
||||
/// `CompareFunction::Always` and would paint the sky straight over the room around you. The
|
||||
/// flag is how the reference orders the sky pass *within* its own squashed depth slice
|
||||
/// (`[0.975, 0.98]`, painter's order, depth-write off); our port of that ordering is the sort
|
||||
/// rung ([`super::skybox_sort_bias`]), so honouring the flag here would be reading it twice.
|
||||
///
|
||||
/// No variant set: a skybox never feathers, never primes depth, and is never indoors or
|
||||
/// outdoors — it *is* the outdoors.
|
||||
pub fn skybox(
|
||||
&mut self,
|
||||
sub: &benilla_formats::RenderSubmesh,
|
||||
texture: Option<Handle<Image>>,
|
||||
order: u16,
|
||||
) -> Option<Handle<WowModelMaterial>> {
|
||||
let light = self.light.as_ref()?.0.clone();
|
||||
Some(model_material(
|
||||
&mut self.cache.0,
|
||||
&mut self.materials,
|
||||
texture,
|
||||
sub.blend,
|
||||
sub.two_sided,
|
||||
false, // an M2, whichever building names it
|
||||
false,
|
||||
sub.emissive,
|
||||
sub.additive,
|
||||
false, // never feathers
|
||||
true, // …never writes depth
|
||||
false, // …and never skips the test — see above
|
||||
sub.fog_policy,
|
||||
sub.env_map,
|
||||
ShadeSel::Lit, // unread: every skybox batch the chain ships authors UNLIT (0x01)
|
||||
order,
|
||||
// No texture-transform or M2Color loop is wired on this lane, and neither shipped
|
||||
// skybox authors one (`CavernsOfTimeSky` has 0 texture transforms and 0 animated colour
|
||||
// tracks; `StratholmeSkybox` is fully static). Wiring them means loading the skybox
|
||||
// through the `M2Model` asset lane — which holds these as the `Arc`s the material key
|
||||
// identifies them by, and is also what the asteroid belts' BONE animation needs. One
|
||||
// move, when that lands; a locally-minted `Arc` here would be a dedup key with no owner.
|
||||
None,
|
||||
None,
|
||||
None, // M2: no MOBA class, no SIDN, no WINDOW
|
||||
None,
|
||||
false,
|
||||
true, // the sky lane
|
||||
&light,
|
||||
))
|
||||
}
|
||||
|
||||
/// One steady material for a batch drawn **off-world**, against a render target's own light
|
||||
/// buffer instead of the world's: a portrait booth's frozen studio rig, the model pane's, the
|
||||
/// char-select stage's. The buffer is a key axis ([`super::MatKey::light`]), so these share
|
||||
@@ -251,6 +317,7 @@ impl M2BatchMaterials<'_> {
|
||||
None, // worn/held part: light selection anchors at the instance origin
|
||||
None, // M2 carries no MOMT SIDN colour
|
||||
false, // …nor the WINDOW flag
|
||||
false, // a character composite is never a skybox
|
||||
&light,
|
||||
)
|
||||
};
|
||||
@@ -356,6 +423,9 @@ impl M2BatchMaterials<'_> {
|
||||
sub.wmo_batch,
|
||||
sub.sidn,
|
||||
sub.window,
|
||||
// The sky lane is [`Self::skybox`]'s alone: it overrides depth state the authored batch
|
||||
// asks for, so it cannot be one more variant built from `sub` here.
|
||||
false,
|
||||
light,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ pub(super) fn update_model_particles(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false, // an effect model is never a skybox
|
||||
&light.0,
|
||||
);
|
||||
// The owner-last draw-order rung, stamped on after the fact: a 3-D model
|
||||
|
||||
@@ -34,7 +34,6 @@ pub(crate) fn plugin(app: &mut App) {
|
||||
bevy::asset::embedded_asset!(app, "shaders/star.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/cloud.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/celestial.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/wmo_skybox.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/ffx_glow.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/wow_effect.wgsl");
|
||||
}
|
||||
@@ -77,6 +76,9 @@ mod tests {
|
||||
`shaders::plugin`, or was registered from a file that is not directly under src/"
|
||||
);
|
||||
}
|
||||
assert_eq!(found, 7, "the engine's shader set changed size");
|
||||
// 6 since decision 1264 retired `wmo_skybox.wgsl`: a WMO skybox draws on the shared model
|
||||
// lane, whose forced-far-depth branch lives in `benilla_assets`' `wow_model.wgsl` and is
|
||||
// pinned by that crate's own `the_sky_lane_forces_the_far_depth`.
|
||||
assert_eq!(found, 6, "the engine's shader set changed size");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// The WMO **skybox** (`wmo_sky.rs`) — the authored sky a building swaps in for the Light.dbc
|
||||
// gradient dome while the camera stands in one of its `0x40000` groups. Stratholme's burning city is
|
||||
// the one place 1.12 content reaches this: `StratholmeSkybox.m2`, a static emissive cube whose three
|
||||
// texture pairs are the painted red sky.
|
||||
//
|
||||
// GAMMA LANE (decision 0161): the batches are authored material flags `0x05` = UNLIT (`0x01`) |
|
||||
// TWO_SIDED (`0x04`), blend 0 (`benilla-extract m2batch`), so the texel's own gamma bytes, unlit and
|
||||
// unmodulated, is what the *material* asks for.
|
||||
//
|
||||
// UNVERIFIED, and flagged as such: we draw it **unfogged** — and the M2 does not set the UNFOGGED bit
|
||||
// (`0x02`), so by its own authoring this model IS fogged. The no-fog treatment was an analogy with the
|
||||
// gradient dome, not a byte law: wow-re has `CMapObj+0x12c` (the MOSB pointer) carved with no renderer
|
||||
// behind it, so nothing yet says which pass this draws in or under what state. An RE dispatch is open
|
||||
// on exactly that. Do not harden this into a claim before it lands.
|
||||
//
|
||||
// SKY-PASS DEPTH (see `sky_order.rs`, "The depth law"): the box is drawn camera-anchored at model
|
||||
// scale, so its geometry sits *inside* the world — the shell radius must not decide occlusion.
|
||||
// Forcing the far depth makes it a true backdrop: it fills only the pixels no world geometry claimed.
|
||||
#import bevy_pbr::{
|
||||
pbr_fragment::pbr_input_from_standard_material,
|
||||
forward_io::VertexOutput,
|
||||
}
|
||||
|
||||
/// Reverse-Z "infinitely far" — the sky pass's forced depth (`sky_order.rs`).
|
||||
const SKY_FAR_DEPTH: f32 = 0.0;
|
||||
|
||||
struct WmoSkyboxOutput {
|
||||
@location(0) color: vec4<f32>,
|
||||
@builtin(frag_depth) depth: f32,
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> WmoSkyboxOutput {
|
||||
let pbr_input = pbr_input_from_standard_material(in, is_front);
|
||||
var out: WmoSkyboxOutput;
|
||||
// Opaque backdrop: the texel's raw gamma bytes, alpha forced to 1 (the sky art carries none).
|
||||
out.color = vec4<f32>(pbr_input.material.base_color.rgb, 1.0);
|
||||
out.depth = SKY_FAR_DEPTH;
|
||||
return out;
|
||||
}
|
||||
@@ -73,11 +73,26 @@
|
||||
|
||||
/// Stars — the first celestial draw (`0x6d4a3f`): everything else in the sky paints over them, so
|
||||
/// theirs is the ladder's LOWEST rung (see the sign law above).
|
||||
///
|
||||
/// The WMO skybox ([`crate::wmo_sky`]) takes no rung: like the gradient dome it replaces, it is an
|
||||
/// **opaque** backdrop, so it draws in the opaque pass and needs no transparent-sort bias — its
|
||||
/// forced far depth (the depth law below) is the whole of its ordering, and the two never coexist.
|
||||
pub(crate) const STARS_BIAS: f32 = -1.0e6;
|
||||
/// **The WMO skybox** ([`crate::wmo_sky`]) — the building-owned painted sky, which replaces this
|
||||
/// whole ladder while it draws, so its rung never competes with one above. It needs a rung at all
|
||||
/// because a skybox is **an ordinary M2 and not an opaque backdrop**, which is what this module
|
||||
/// asserted until decision 1264: `CavernsOfTimeSky.m2` is 21 batches across four blend modes — a
|
||||
/// painted cube, six additive star sheets on the cube's own faces, five alpha-blended planet cards
|
||||
/// and three alpha-tested asteroid belts on rotating bones. Drawn opaque (the old lane read
|
||||
/// positions, UVs and one texture and nothing else) the additive star sheet — near-white RGB whose
|
||||
/// stars live in its ALPHA — paints a flat white sheet over the painted sky. That is the director's
|
||||
/// "the whole ceiling is white" in Caverns of Time.
|
||||
///
|
||||
/// The blended half of a skybox therefore lands in the transparent pass, camera-anchored, so its
|
||||
/// view-z is a few tens of yards and it would sort *after* the world. This rung sinks it under
|
||||
/// every world transparent: the deepest of those is [`FAR_SIDE_BIAS`] plus a far-plane view-z,
|
||||
/// ≈ −4.3e4, and the shell's own view-z rides on top of the rung — all four properties of the band
|
||||
/// are machine-checked in `model_render`'s
|
||||
/// `the_skybox_band_orders_its_batches_on_one_pipeline_key`. Kept as small as that job allows (the
|
||||
/// magnitude rationale above), because the rung is also what the batch-order epsilon has to
|
||||
/// survive: see `model_render::SKYBOX_ORDER_EPS`.
|
||||
pub(crate) const WMO_SKYBOX_BIAS: f32 = -6.0e4;
|
||||
/// The sun disc — second (`0x7e5b90` via `0x6d4a47`).
|
||||
pub(crate) const SUN_DISC_BIAS: f32 = -8.2e5;
|
||||
/// The white moon — third; where the discs cross, the moon paints over the sun.
|
||||
@@ -198,7 +213,9 @@ fn every_sky_shader_forces_the_far_depth() {
|
||||
("star.wgsl", include_str!("shaders/star.wgsl")),
|
||||
("cloud.wgsl", include_str!("shaders/cloud.wgsl")),
|
||||
("celestial.wgsl", include_str!("shaders/celestial.wgsl")),
|
||||
("wmo_skybox.wgsl", include_str!("shaders/wmo_skybox.wgsl")),
|
||||
// The WMO skybox is no longer a shader of its own: it draws on the shared model lane, whose
|
||||
// `WOW_SKY_DEPTH` branch obeys the same law and is asserted beside it
|
||||
// (`benilla_assets::materials`, `the_sky_lane_forces_the_far_depth`).
|
||||
] {
|
||||
assert!(
|
||||
src.contains("const SKY_FAR_DEPTH: f32 = 0.0;"),
|
||||
|
||||
@@ -202,6 +202,7 @@ pub fn spawn_model_entities(
|
||||
sub.wmo_batch,
|
||||
sub.sidn,
|
||||
sub.window,
|
||||
false, // the world streamer never spawns a skybox
|
||||
light,
|
||||
);
|
||||
// The blend twin for the distance-fade feather pass (reuse the cutout when already blend, or when
|
||||
@@ -243,6 +244,7 @@ pub fn spawn_model_entities(
|
||||
sub.wmo_batch,
|
||||
sub.sidn,
|
||||
sub.window,
|
||||
false, // the world streamer never spawns a skybox
|
||||
light,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -19,19 +19,41 @@
|
||||
//! without a MOSB, 0 counter-examples (`benilla-extract skyboxscan`) — was true and still is; it just
|
||||
//! never established *which* group the renderer tests, and it was over-read as if it had.
|
||||
//!
|
||||
//! Five roots exercise both halves: the four Caverns of Time shells (unreleased in 1.12) and
|
||||
//! **`Stratholme_B`**, the burning city. Four more name a skybox no group ever asks for (DireMaul's
|
||||
//! Five roots exercise both halves: **`Stratholme_B`**, the burning city, and the four Caverns of
|
||||
//! Time shells — which are *not* out of reach in 1.12, whatever the instance portals do.
|
||||
//! `CavernsofTime.wmo` is placed in the live world at Kalimdor tile (39, 47), MODF `uniqueId`
|
||||
//! 398759, and standing in the crater at `.go xyz -8437.16 -4222.44 -211.58 1` the cull's down-ray
|
||||
//! seeds group 29 (`flags 0x42805` — `SHOW_SKYBOX` set) and this resolve fires. An earlier note here
|
||||
//! read "unreleased" as "unreachable" and concluded this branch never actually picks; the director
|
||||
//! walked into it.
|
||||
//!
|
||||
//! Four more roots name a skybox no group ever asks for (DireMaul's
|
||||
//! instance shell, `Stratholme_A`, and the two Sunken Temple roots — whose MOSB isn't even a model
|
||||
//! path, it's the string "the temple of atal'hakkar"), so keying off the chunk alone would still
|
||||
//! paint skies the reference never shows. This is why Stratholme's sky is red where the zone light
|
||||
//! says otherwise: map 329's only reachable `Light.dbc` atmosphere (global row 341 → `LightParams`
|
||||
//! 336) is a khaki-brown gradient with a near-black apex, and it is not what draws in there.
|
||||
//!
|
||||
//! What we draw is the model as authored: `StratholmeSkybox.m2` is a static, emissive, two-sided cube
|
||||
//! (three batches × 8 verts, one texture pair per axis), anchored at the camera with identity rotation
|
||||
//! — the same treatment [`crate::sky`] gives its dome. Occlusion is **not** the box's radius: like
|
||||
//! every sky element it forces the far depth (`wmo_skybox.wgsl`; the law is in [`crate::sky_order`]),
|
||||
//! so the world paints over it and the 26-yard shell is free to sit inside the room's own geometry.
|
||||
//! **A skybox is an ordinary M2, and is drawn as one** (decision 1264). This lane used to have a
|
||||
//! private mesh builder and a private material — positions, UVs, one texture, everything opaque —
|
||||
//! which is a faithful drawing of `StratholmeSkybox.m2` (three opaque batches × 8 verts, one texture
|
||||
//! pair per axis, no animation) and of nothing else. `CavernsOfTimeSky.m2` is the counter-example the
|
||||
//! chain also ships: **21 batches across four blend modes** — a painted cube, six ADDITIVE star
|
||||
//! sheets on the cube's own faces, five alpha-blended planet cards, three alpha-tested asteroid belts
|
||||
//! on rotating bones. Drawn opaque, the star sheet — whose RGB is near-white and whose stars live in
|
||||
//! its ALPHA channel — paints a flat white sheet over the painted sky, and the planets and belts
|
||||
//! become dark cards. That is the director's *"the whole ceiling is white … some of the cool effects
|
||||
//! seem missing"* in Caverns of Time.
|
||||
//!
|
||||
//! So the batches go through [`crate::model_render`]'s material lane like every other model
|
||||
//! ([`M2BatchMaterials::skybox`]), which is where the blend law, the 224/255 alpha-key reference, the
|
||||
//! additive gamma premultiply and the authored batch order already live, byte-verified. Three things
|
||||
//! are the *sky's* and are set here, not read off the batch: the forced far depth, depth-write off,
|
||||
//! and depth-test on — the rationale is on `skybox()`.
|
||||
//!
|
||||
//! Occlusion is **not** the box's radius: like every sky element it forces the far depth (the law is
|
||||
//! in [`crate::sky_order`]), so the world paints over it and the shell is free to sit inside the
|
||||
//! room's own geometry.
|
||||
//!
|
||||
//! **Scope — the skybox replaces the WHOLE celestial pass, not just the backdrop.** `CSky::Render`
|
||||
//! carries one shared boolean (`0x6d49cd` sets it, `0x6d49fb`/`0x6d4a2e` clear it once any slot's
|
||||
@@ -48,17 +70,10 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use bevy::asset::RenderAssetUsages;
|
||||
use bevy::mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology};
|
||||
use bevy::pbr::{
|
||||
ExtendedMaterial, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
|
||||
MaterialPlugin,
|
||||
};
|
||||
use bevy::mesh::{Indices, PrimitiveTopology};
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::render_resource::{
|
||||
AsBindGroup, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
};
|
||||
use bevy::shader::ShaderRef;
|
||||
|
||||
use crate::model_render::M2BatchMaterials;
|
||||
use crate::view::WorldCamera;
|
||||
use benilla_assets::coords::wow_to_bevy;
|
||||
use benilla_assets::WmoModel;
|
||||
@@ -69,36 +84,6 @@ use benilla_assets::{LockRecover, WorldAssets};
|
||||
/// file's MOGP header; we read the MOGP copy the loader already keeps in `WmoGroupNav::flags`.
|
||||
const SHOW_SKYBOX: u32 = 0x40000;
|
||||
|
||||
/// Unlit textured backdrop over a `StandardMaterial` shell — the shell supplies `unlit` +
|
||||
/// `cull_mode: None` (the box is viewed from inside) and the texture; the extension's fragment shader
|
||||
/// emits the texel raw and forces the sky pass's far depth.
|
||||
pub type WmoSkyboxMaterial = ExtendedMaterial<StandardMaterial, WmoSkyboxExt>;
|
||||
|
||||
/// No per-material uniforms — the fragment reads the base-colour texture and nothing else.
|
||||
#[derive(Asset, AsBindGroup, Clone, TypePath, Default)]
|
||||
pub struct WmoSkyboxExt {}
|
||||
|
||||
impl MaterialExtension for WmoSkyboxExt {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"embedded://benilla_world/shaders/wmo_skybox.wgsl".into()
|
||||
}
|
||||
|
||||
/// Depth-write OFF, exactly as [`crate::sky::SkyExt`] does it: the backdrop must leave the
|
||||
/// z-buffer at its clear value over sky pixels so the forced-far glare quads stay occluded by
|
||||
/// *world* geometry alone.
|
||||
fn specialize(
|
||||
_pipeline: &MaterialExtensionPipeline,
|
||||
descriptor: &mut RenderPipelineDescriptor,
|
||||
_layout: &MeshVertexBufferLayoutRef,
|
||||
_key: MaterialExtensionKey<Self>,
|
||||
) -> Result<(), SpecializedMeshPipelineError> {
|
||||
if let Some(depth) = descriptor.depth_stencil.as_mut() {
|
||||
depth.depth_write_enabled = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The skybox model the camera's room asks for this frame — `None` (the overwhelming default) means
|
||||
/// the [`crate::sky`] gradient dome is the backdrop. Resolved from [`CameraInteriorClaim`]: the same
|
||||
/// down-ray seed that already names the camera's room for the MFOG fog resolve.
|
||||
@@ -129,8 +114,9 @@ pub(crate) struct WmoSkyPlugin;
|
||||
|
||||
impl Plugin for WmoSkyPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(MaterialPlugin::<WmoSkyboxMaterial>::default())
|
||||
.init_resource::<CameraWmoSkybox>()
|
||||
// No `MaterialPlugin` of its own any more: a skybox batch is a `WowModelMaterial` like every
|
||||
// other model batch, and that plugin is already loaded.
|
||||
app.init_resource::<CameraWmoSkybox>()
|
||||
.init_resource::<BuiltSkyboxes>()
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -197,9 +183,10 @@ fn resolve_camera_skybox(
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the wanted skybox's entities the first time it is asked for. The model is tiny (three static
|
||||
/// batches) and there are five in the whole game, so a built one is kept for the session rather than
|
||||
/// torn down on leaving the room — walking in and out of Stratholme's gate must not re-decode art.
|
||||
/// Build the wanted skybox's entities the first time it is asked for. The models are small (3 batches
|
||||
/// for Stratholme, 21 for Caverns of Time) and there are five in the whole game, so a built one is
|
||||
/// kept for the session rather than torn down on leaving the room — walking in and out of
|
||||
/// Stratholme's gate must not re-decode art.
|
||||
fn build_skybox(
|
||||
mut commands: Commands,
|
||||
want: Res<CameraWmoSkybox>,
|
||||
@@ -207,7 +194,7 @@ fn build_skybox(
|
||||
world_assets: Option<ResMut<WorldAssets>>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut materials: ResMut<Assets<WmoSkyboxMaterial>>,
|
||||
mut mats: M2BatchMaterials,
|
||||
) {
|
||||
let Some(path) = want.0.as_deref() else {
|
||||
return;
|
||||
@@ -215,6 +202,12 @@ fn build_skybox(
|
||||
if built.0.contains(path) {
|
||||
return;
|
||||
}
|
||||
// The shared light buffer is created in the render world's shadow at startup; a build that ran
|
||||
// before it would bake materials against nothing. Retry — and do NOT latch `built` here, or the
|
||||
// first frame in the room would permanently give up on the sky.
|
||||
if !mats.ready() {
|
||||
return;
|
||||
}
|
||||
let Some(mut world_assets) = world_assets else {
|
||||
return; // assetless dev run — the gradient dome stays the backdrop
|
||||
};
|
||||
@@ -232,7 +225,7 @@ fn build_skybox(
|
||||
return;
|
||||
}
|
||||
};
|
||||
for sub in &subs {
|
||||
for (i, sub) in subs.iter().enumerate() {
|
||||
let mut mesh = Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetUsages::default(),
|
||||
@@ -243,23 +236,30 @@ fn build_skybox(
|
||||
.map(|p| wow_to_bevy(*p).to_array())
|
||||
.collect();
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
// The model lane's vertex stage declares `world_normal` unconditionally and fills it under
|
||||
// `VERTEX_NORMALS`; the sky is unlit and never reads it, but the attribute has to be there
|
||||
// for the layout the shared shader was compiled against.
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_NORMAL,
|
||||
sub.normals
|
||||
.iter()
|
||||
.map(|n| wow_to_bevy(*n).to_array())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, sub.uvs.clone());
|
||||
mesh.insert_indices(Indices::U32(sub.indices.clone()));
|
||||
// The batch's own authored address mode (decision 0763) — a skybox's UVs sit inside 0..1, but
|
||||
// reading the flags costs nothing and keeps this lane off a private convention.
|
||||
// The batch's own authored address mode (decision 0763) — a skybox's UVs sit inside 0..1 for
|
||||
// the cube faces, but Caverns of Time's asteroid belts tile theirs over ±21 wraps.
|
||||
let texture = sub
|
||||
.texture
|
||||
.as_deref()
|
||||
.and_then(|t| world_assets.texture(t, (sub.wrap_x, sub.wrap_y), &mut images));
|
||||
let material = materials.add(WmoSkyboxMaterial {
|
||||
base: StandardMaterial {
|
||||
base_color_texture: texture,
|
||||
unlit: true,
|
||||
cull_mode: None, // authored two-sided, and we view the box from inside
|
||||
..default()
|
||||
},
|
||||
extension: WmoSkyboxExt {},
|
||||
});
|
||||
// `i + 1` is the authored-batch-order convention (`0` = unordered): the transparent half of
|
||||
// a skybox is camera-anchored, so every one of its batches shares a sort distance and the
|
||||
// order is the only thing keeping the layers from re-flipping every frame.
|
||||
let Some(material) = mats.skybox(sub, texture, u16::try_from(i + 1).unwrap_or(0)) else {
|
||||
return; // light buffer vanished mid-build; `built` is unlatched, so we retry
|
||||
};
|
||||
commands.spawn((
|
||||
Mesh3d(meshes.add(mesh)),
|
||||
MeshMaterial3d(material),
|
||||
|
||||
Reference in New Issue
Block a user