diff --git a/Cargo.lock b/Cargo.lock
index 0c72824c..5981928e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -592,6 +592,7 @@ dependencies = [
"benilla-formats",
"benilla-protocol",
"benilla-ui",
+ "benilla-wmo",
"bevy",
"bevy_egui",
"bytemuck",
diff --git a/crates/benilla-app/Cargo.toml b/crates/benilla-app/Cargo.toml
index b43c8112..335e99c2 100644
--- a/crates/benilla-app/Cargo.toml
+++ b/crates/benilla-app/Cargo.toml
@@ -82,6 +82,12 @@ mint = "0.5"
# source. Pinned to 0.8: the version `benilla-srp` already depends on, so this adds no crate.
rand = "0.8"
+[dev-dependencies]
+# The WMO portal-cull audit's raw-face instruments (`WOW_PIN_COL`): the pin probe compares the
+# filtered walking-collision set against the group file's FULL face list with per-face MOPY flags,
+# which only the raw parser exposes — benilla-formats' gathers are exactly the filtered views.
+benilla-wmo = { path = "../benilla-wmo" }
+
# Linux/BSD: the Wayland clipboard (decision 0702). We build with bevy's `wayland` feature, so on a
# Wayland session winit gives us a native Wayland window and `arboard` — X11-only without its
# non-default `wayland-data-control` feature — finds no X11 CLIPBOARD owner and every paste fails.
diff --git a/crates/benilla-app/assets/ui/ActionBar.xml b/crates/benilla-app/assets/ui/ActionBar.xml
index 5567011d..a09af17c 100644
--- a/crates/benilla-app/assets/ui/ActionBar.xml
+++ b/crates/benilla-app/assets/ui/ActionBar.xml
@@ -648,13 +648,54 @@
BenillaExhaustionTick.timer = nil
end
+ -- ── The max-level rail (ref-ReputationFrame.lua, the max-level slice) ─────────────────
+ -- ref l.14: the constant lives in ReputationFrame.lua, which isn't built yet — it moves
+ -- there with the reputation system.
+ MAX_PLAYER_LEVEL = 60
+
+ function BenillaMaxLevelBar_OnLoad(bar)
+ bar:RegisterEvent("PLAYER_ENTERING_WORLD")
+ bar:RegisterEvent("PLAYER_LEVEL_UP")
+ end
+
+ -- ref ReputationWatchBar_Update's else arm (ref-ReputationFrame.lua l.161-166 +
+ -- l.236-244), the no-watched-faction branch only — the reputation branches wait for the
+ -- reputation system: at MAX_PLAYER_LEVEL the XP strip gives way to the rail and the
+ -- tick goes with it. Login seam: the ref initializes off UPDATE_FACTION (its OnEvent's
+ -- else arm; the faction list lands at every login), an event benilla doesn't carry yet,
+ -- so the rail listens to PLAYER_ENTERING_WORLD instead — same moment, same values.
+ function BenillaMaxLevelBar_Update(newLevel)
+ if not newLevel then
+ newLevel = UnitLevel("player")
+ end
+ if newLevel == MAX_PLAYER_LEVEL then
+ BenillaExpBar:Hide()
+ BenillaMaxLevelBar:Show()
+ BenillaExhaustionTick:Hide()
+ else
+ BenillaExpBar:Show()
+ BenillaMaxLevelBar:Hide()
+ end
+ end
+
+ -- The ref's event fan (ref-ReputationFrame.xml l.1002-1016): PLAYER_LEVEL_UP passes
+ -- arg1 (the new level — the level FIELD may not have landed yet), everything else
+ -- re-reads UnitLevel.
+ function BenillaMaxLevelBar_OnEvent()
+ if event == "PLAYER_LEVEL_UP" then
+ BenillaMaxLevelBar_Update(arg1)
+ else
+ BenillaMaxLevelBar_Update()
+ end
+ 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. Two transcription cuts, named:
- -- the ref's MainMenuBarMaxLevelBar / ReputationWatchBar guards reference max-level bar
- -- art and the faction watch bar, neither built — each is its own slice, and until then
- -- the guards' frames can't be shown, so the cuts are unreachable-code cuts, not behavior
- -- cuts. (A level-60 body simply keeps its inert XP bar for now.)
+ -- pale fill between the bar's value and that point. One transcription cut left, named:
+ -- the ref's two ReputationWatchBar guards (the max-level+rep tick hide, l.51-54, and
+ -- the rep-bar-replaced-the-xp-bar hide, l.69-71) reference the faction watch bar, not
+ -- built — its frame can't be shown, so the cut is an unreachable-code cut, not a
+ -- behavior cut. The MainMenuBarMaxLevelBar guard is live since 1094 (the rail below).
function BenillaExhaustionTick_OnLoad(tick)
tick:RegisterEvent("PLAYER_ENTERING_WORLD")
tick:RegisterEvent("PLAYER_XP_UPDATE")
@@ -683,7 +724,8 @@
local exhaustionTickSet = max(((playerCurrXP + exhaustionThreshold)
/ playerMaxXP) * BenillaExpBar:GetWidth(), 0)
tick:ClearAllPoints()
- if exhaustionTickSet > BenillaExpBar:GetWidth() then
+ if exhaustionTickSet > BenillaExpBar:GetWidth()
+ or BenillaMaxLevelBar:IsShown() then
tick:Hide()
BenillaExhaustionLevelFillBar:Hide()
else
@@ -1049,6 +1091,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BenillaMaxLevelBar_OnLoad(self)
+ BenillaMaxLevelBar_OnEvent()
+
+
+
+ OnShow resets it to the skull and swaps a fishing loot to FishingLoot-Icon
+ (decision 1086), exactly as the ref's OnShow does (l.133-140). -->
diff --git a/crates/benilla-app/assets/ui/SkillFrame.xml b/crates/benilla-app/assets/ui/SkillFrame.xml
index c60ca7ff..65d32dd7 100644
--- a/crates/benilla-app/assets/ui/SkillFrame.xml
+++ b/crates/benilla-app/assets/ui/SkillFrame.xml
@@ -111,19 +111,52 @@
function BenillaSkillFrame_PaintBar(bar, skillName, skillRank, skillModifier, skillMaxRank)
local nameText = getglobal(bar:GetName() .. "SkillName")
local rankText = getglobal(bar:GetName() .. "SkillRank")
+ -- The trough behind the fill (ref `$parentBackground`), recoloured per branch alongside
+ -- the fill itself — ref SkillFrame.lua:158 (normal) and :167 (proficiency), and the
+ -- detail pane's identical pair at :356/:376. The ref sets the normal-skill pair BEFORE
+ -- its `skillMaxRank` gate and lets the proficiency arm overwrite it; setting each arm's
+ -- own pair here is the same result with no dead write. The ref's THIRD writer of this
+ -- texture — the `skillCostType == 1/2/3` green/yellow/red block at :96-111, and the
+ -- `stepCost` "Learn " gray at :126 — is unreachable for us and would be
+ -- overwritten here even if it weren't: `stepCost`/`rankCost` are always nil and
+ -- `numTempPoints` always 0 (skills.rs's module doc — dead 1.12 data, deviation 1/5), so
+ -- every entry row takes the ref's own `else -- Normal skill` arm.
+ local bgTexture = getglobal(bar:GetName() .. "Background")
nameText:SetText(skillName)
nameText:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
rankText:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
- if skillMaxRank == 0 then
- -- A 1.12 proficiency line the app reports as 0/0 (SkillEntry's own doc) — barless
- -- plain text, deviation-free reading of the ref's own "skillMaxRank == 1" gray-bar
- -- intent, adapted to our app's 0/0 shape (this file's header comment, no-fillbar note).
+ if skillMaxRank == 1 then
+ -- ref SkillFrame.lua:161-167 verbatim: "If max rank in a skill is 1 assume that
+ -- its a proficiency" — a FULL gray bar and NO rank text. Two kinds of line land
+ -- here, and the ref can't tell them apart because it doesn't need to: the armor
+ -- proficiencies, which the server really does report as 1/1, and the single-rank
+ -- lines (class skills, Dual Wield, racials, the per-mount riding lines) whose
+ -- `skillMaxRank` the engine already overrode to 1 off
+ -- `SkillRaceClassInfo.flags & 0x400` (skills.rs's `SkillEntry::mono`) — which is
+ -- why a hunter's `Beast Mastery 300/300` draws gray and numberless here, exactly
+ -- as it does in the real client.
+ bar:SetMinMaxValues(0, 1)
+ bar:SetValue(1)
+ bar:SetStatusBarColor(0.5, 0.5, 0.5)
+ rankText:SetText("")
+ bgTexture:SetVertexColor(1.0, 1.0, 1.0, 0.5)
+ elseif skillMaxRank == 0 then
+ -- Not reachable off a live server (a line the player HAS always carries a max ≥ 1)
+ -- and the ref leaves this case to fall through its own if/elseif untouched —
+ -- which, on a recycled row, would leave the PREVIOUS skill's bar and rank text
+ -- sitting there. We blank instead: a stale-state hole is a quirk, not behaviour.
bar:SetMinMaxValues(0, 1)
bar:SetValue(0)
bar:SetStatusBarColor(0, 0, 0, 0)
rankText:SetText("")
+ -- back to the template's own declared base, so a recycled row shows a neutral empty
+ -- trough rather than the previous skill's blue or white. The base lives in the
+ -- TEXEL (the template's ``), so restoring it means an IDENTITY
+ -- vertex colour — writing 0.2 here would multiply it to 0.04 (decision 1101).
+ bgTexture:SetVertexColor(1.0, 1.0, 1.0, 1.0)
else
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5)
+ 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,
diff --git a/crates/benilla-app/assets/ui/UnitFrames.xml b/crates/benilla-app/assets/ui/UnitFrames.xml
index cfaef29f..50f1b9e7 100644
--- a/crates/benilla-app/assets/ui/UnitFrames.xml
+++ b/crates/benilla-app/assets/ui/UnitFrames.xml
@@ -946,6 +946,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-