v3.2.0
What's new
tag ab78e564218cca21698bdeec830077cc54d187b1 v3.2.0
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Mon Aug 24 11:32:12 2026 +0200
v3.2.0 - Crest advice, shared keystones, and a lighter addon
The advisor reads a whole wallet rather than one piece at a time: a crest
tier is counted over slots instead of over the pieces already worn, a
slot's bill is priced off the mark it will have rather than the one it
has, and a track you own nothing on can be advised at all. Crafting is
in.
Keystones moved onto LibKeystone, the library BigWigs, DBM and
EllesmereUI already embed, so the Guild tab lists people who do not run
this addon -- which, before, was everybody. This is a .toc change, so it
needs a full client restart rather than a /reload.
The loot browser writes down its instance list, its loot and its journal
link index, so a reload no longer refetches them, and a pass that
disagrees with itself is refused instead of saved.
Two features came out. The interrupt tracker had not worked for the whole
of 12.1 and could not be repaired in place -- the client stopped handing
out which spell a party member cast, so the party half was a guess keyed
on a 150ms timing coincidence. The Delves tab was a third place to keep
facts the game does not hide and this addon already said elsewhere;
delve chores, delve advice and the Progression page's delve tiers all
stay.
And a sweep for work done during ordinary play, none of it feature work:
the login prewarm no longer takes a whole frame's budget or runs inside a
dungeon, the battle-res timer no longer wakes on every charge-based spell
the character owns, the home page no longer rebuilds for other addons'
quests, and UNIT_INVENTORY_CHANGED is filtered to the player.
The watermark sweep is the one worth naming. It was fixed once and went
on freezing the client anyway, because only half of it had been spread
across frames: to decide what to queue it read all sixteen slots up
front, and that is the expensive half -- each read ends in a real
SetInventoryItem tooltip build, on a cache the calling handler wipes
immediately beforehand. The harness had missed it by stubbing exactly
that function. It counts slot reads now, controlled against the old
queue-builder.
commit e34e04a4edd1d1d8b9e41e7b6d4ac0c6f9ef50b1
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Mon Aug 24 11:31:42 2026 +0200
Date the changelog for v3.2.0
The version in the .toc went to 3.2.0 with the work itself so the build
under test was the build that ships. This is the other half: the
Unreleased heading becomes a dated one, which is the last thing that has
to be true before the tag points at anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit e358796fccae8352a0ce7a9a9de04d5b5dec6860
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Mon Aug 24 11:01:13 2026 +0200
The Delves tab out, and the client stubs nothing else was using
A whole page to restate a companion's friendship rank, a key count and a
tier ladder. The game hides none of that, and this addon already says
two of the three somewhere else -- so the page was a third place to keep
the same facts correct.
Out with it: Features/Delves/, the shell page it registered, its entry in
the /yh shell <page> list, and its check in the harness. The client stubs
went too, because the page was their only consumer -- C_DelvesUI, the
C_GossipInfo friendship pair, C_PartyInfo.IsDelveInProgress and
C_Reputation are now gone from loadcheck rather than sitting there
answering nobody.
Delves are not gone from the addon, only their tab, and the difference is
worth stating because "delve" still appears all over the source. The
weekly checklist keeps its delve chores, PlannerData still raises "run a
tier N delve" on the Dashboard when that is the cheapest upgrade
available, the Progression page keeps its delve tier section reading
ns.PROGRESSION.DELVES, and the vault's World row is still the one delves
fill. W:HeroCard also keeps borrowing the delve companion ring atlas for
its portrait rim -- the shell's character column draws it, and the atlas
name is incidental to what it is used for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit b91f710790949cac9144827e4482dcb567327ad6
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Sun Aug 23 23:49:06 2026 +0200
Crest advice, shared keystones, and the interrupt tracker out
The accumulated crest-advice branch, committed in one go because the
strands are interleaved inside the same files -- Core.lua and
LootBrowserData.lua each carry several of them -- and this environment
has no interactive staging to separate them by hunk. CHANGELOG.md has
the entry-by-entry account; this is the shape of it.
Gear. The advisor reads a whole wallet rather than one piece at a time:
a tier is counted over slots instead of over the pieces already worn, a
slot's bill is priced off the mark it will have rather than the one it
has, and a track you own nothing on can now be advised at all. Crafting
is in. The per-wallet sentence strip above Improvements is out.
Mythic+. Keystones moved onto LibKeystone, the library BigWigs, DBM and
EllesmereUI already embed, so the Guild tab lists people who do not run
this addon -- which, before, was everybody. Vendored from BigWigs;
LibStub hands out whichever copy loads first, so keeping ours current
matters. Needs a full client restart, not a /reload: it is a .toc change.
Loot browser. The instance list, the loot and the journal link index are
all written down now, so a reload does not refetch them, and a pass that
disagrees with itself is refused rather than saved.
Performance. A sweep for work done during ordinary play, none of it
feature work: the login prewarm no longer takes a whole frame's budget
or runs inside a dungeon, the battle-res timer no longer wakes on every
charge-based spell the character owns, the home page no longer rebuilds
for other addons' quests, and UNIT_INVENTORY_CHANGED is filtered to the
player. `/yh trace` now genuinely costs nothing while off -- its
watchdog is a parentless frame, which the client shows by default, so it
had been running an OnUpdate every frame of every session.
And the watermark sweep, which was fixed once and went on freezing the
client anyway. Only half of it had been spread across frames: to decide
what to queue it read all sixteen slots up front, and that is the
expensive half -- each read ends in a real SetInventoryItem tooltip
build, on a cache the calling handler wipes immediately beforehand. The
harness missed it by stubbing exactly that function, so it measured the
half that had been fixed. It counts slot reads now, controlled against
the old queue-builder.
Removed: the interrupt tracker. It had not worked for the whole of 12.1
and could not be repaired in place -- the client stopped handing out
which spell a party member cast, so the party half was a guess keyed on
a 150ms timing coincidence. Doing it properly means the combat log,
which is a rewrite and the one event class this addon has stayed clear
of. Its settings and Edit Mode position are cleared on next login.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 8abc85518f017fcae829c07ceb4b4ea437737511
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 20:53:39 2026 +0200
The spark row counts residue instead of guessing from seven quests
Tidal Spark Dust is one grain per Spark of Tides OBTAINED and never goes
down, so it answers the question the sparks themselves cannot: holding
zero is equally consistent with "not collected" and "collected and
already crafted with", and the second is normal for anyone who crafts.
The client was asked rather than assumed, which is the only reason this
is wired the way it is:
totalEarned 4 the Total the tooltip shows
maxQuantity 4 its "Current Season Maximum"
useTotalEarnedForMaxQty true so totalEarned is the number to read
isAccountWide false this character, which a per-character
chore list wants
quantityEarnedThisWeek 0 on a character holding four
maxWeeklyQuantity 0
That last pair is the trap. There is no "did you get one THIS week"
field -- the currency does not meter itself weekly -- so the row reads a
season total against a season ceiling instead. Which is better than what
it asked before: behind is behind whether it happened this week or three
weeks ago, and catching up is the part the player can act on.
Out goes SPARK_SOURCES and its inference over seven quest ids. That
worked and was wrong at both edges: it could not see the catch-up routes
the tooltip names -- delves, Mythic+, raids, instanced PvP, outdoor
events -- so somebody who made up a missed spark in a delve still read
as owing one; and it needed 96995 Turn Back the Surge held out by hand
for listing Spark of Tides among rewards it does not pay. The residue
count has no edges to get wrong. LIADRIN_WEEKLY stays; its own row uses
it.
No progress bar, though sparkProgress would feed one directly. Only the
crest row measures itself on this page and loadcheck enforces it,
because the renderer draws a bar for anything carrying a fraction -- so
a second one is a visible change to the page rather than a change to
this row. Worth doing deliberately, not as a side effect.
Checked at four points, because a row that always says done is
indistinguishable from a correct one if the fixture only ever sits at
the cap: behind, caught up, overshot by a catch-up week, and the client
not having answered at all -- where it has to hand back to a manual tick
the way it did before any currency was involved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 3183c2bf26599554e21edb9465436d90ae1552de
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 20:01:30 2026 +0200
Ask the client what Tidal Spark Dust knows
The weekly checklist infers "this week's spark is in" from seven quest
flags. That workaround exists because counting Sparks of Tides in the
bags cannot distinguish "not collected" from "collected and already
spent", and the second is the normal case for anyone who crafts.
Currency 3509 is residue: one per spark OBTAINED, and it does not go
down when the spark is used. Its tooltip carries a Total and a "Current
Season Maximum" -- which is a better answer than the row currently
gives, because it says how far behind you are rather than just yes or
no, and it counts the catch-up routes the quest list has never heard of.
Not rewiring anything on that yet. The tooltip does not say which API
fields carry those two numbers, and guessing at an API's shape is
exactly how C_ItemUpgrade.GetHighWatermarkForItem spent a season
returning a silent zero. /yh debug now prints what
GetCurrencyInfo(3509) actually returns -- name, quantity, maxQuantity,
totalEarned, useTotalEarnedForMaxQty and the weekly and account fields
-- so the decision can be made on the answer instead of the guess.
ns:GetCurrencyCapProgress already computed exactly this pair and was
deleted as dead an hour ago. If the fields line up it comes back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 9540caa5f6bad2d15f43941bfe17d78994c685c0
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 19:45:00 2026 +0200
Open the Raid page on the half of it that works alone
Boss Guide leads the sub-tab strip and is what the page opens on. Those
are one change, not two: Shell:InitialSubTab falls back to tabs[1].id
when there is no remembered tab, so list order decides both. There is no
separate default to set, which also means reordering that list for looks
would silently change which view opens -- hence the check.
The overview needs a group before it has anything to say, so leading
with it meant most visits opened on an empty state and had to be
clicked out of. The guide works on your own.
Which is also why its hint line is gone. "The Boss Guide tab works on
your own" was the empty overview apologising and pointing somewhere
else; now that somewhere else is where you already are, and a sentence
telling you to go to the tab you just came from is worse than nothing.
ns:SetRaidPageTab's own fallback moves to "guide" with it. Nothing
passes nil today, but a page whose internal default disagrees with
tabs[1] lights one tab and draws the other -- the exact desync
Shell:Mount already carries a long comment about, from when Trinkets did
it.
No initial show/hide change was needed: Shell:Mount calls OnSubTab after
Build, so SetRaidPageTab runs on mount and sorts the containers out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 5096b604fb9d688f7cdd5eea8362f74778f8cb5f
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 19:38:06 2026 +0200
Sweep the code nothing calls, after twice being told the wrong answer
742 lines across 18 files: 16 file-local functions, 17 ns.* functions,
and six members written and never read. Three passes, because deleting
dead code exposes more of it -- MakeWaypointButton was the only caller
of SetWaypoint and ParseWaypoint, and RefreshRaidPerformance took
PerfAcquire, FormatDPS, GetCombatData and Launder with it. The scanner
reports zero dead file-locals now.
The scanner itself was wrong twice, both times reporting live code as
dead, and both bugs are now written down in it:
1. `^\s*` under re.M. \s matches newlines, so the anchor slides past
line starts and a call three lines down counts as a definition. It
flagged 75 live functions.
2. Treating `ns.Foo(` at the start of a line as a definition. It is a
CALL. With that in the pattern uses == defs for every function in
the addon, and all 88 looked dead -- including AddGlowHighlight,
used in fifteen places, and CreateConsumablesFrame, wired up an
hour earlier.
A definition is `function ns.Foo(`, `function ns:Foo(` or `ns.Foo =`,
and references are counted across .lua .xml .toc .py .md, because
ShellPages drives entry points by name string and so does the harness.
The largest single piece was ns:RefreshRaidPerformance at 215 lines. The
biggest cluster was ReadyCheck's buff detection -- UnitHasAuraByName,
FindAura, FindWellFed, IsEating, EvalFood, FindFlask, FindVantus,
FindBronze -- a per-buff FindAura scan each, superseded by the batched
single-pass scanner whose own comment says it replaced them. Kept
EATING_AURA_NAMES: that scanner still reads the table.
Also gone: ns.EnsureGearWindowBuilt, which I added this morning "so the
next caller does not reach for the flag". No caller wanted it.
Deliberately kept, and not an oversight:
* The game-fact constants nothing currently reads -- CRAFT_CREST_COST,
CREST_SAVE_DISCOUNT, VAULT_BREAKPOINTS, VENOMSTONE_SLOTS,
VENOMSTONES_PER_UPGRADE, VETERAN_EMBELLISH_RESERVE,
SEASON_MPLUS_START, SEASON_RAID_START. Each is one line and each
encodes something that had to be established about the game. They
cost nothing at runtime and re-deriving them costs real work.
* ns.LOOT_DIFF_COLORS, which is dead together with the `color =
"GREEN"` keys in the LOOT_DIFFICULTIES table beneath it. Removing
the table alone would leave those strings pointing at nothing, and
removing both is churn through every row for five lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit a0acba302ec00f85ff3a7c66519c0fb00b1ca1b7
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 18:59:34 2026 +0200
Consumables built its tab strip during the loading screen
32 frames and 77 regions -- the panel, the filter bar with its two
dropdowns, and a tab button, scroll frame and container for each of
three tabs -- built at file scope for a page that has to be clicked to
be seen. Load is now 74 frames and 161 -> 84 regions, from 256 and 901
at the start of the day.
Same shape as Progression: no events, no registration, one construction
script, so it becomes the body of ns:CreateConsumablesFrame and
Core\ShellPages.lua gets `create`. 73 locals move inside, against Lua's
cap of 200. Deleting the pre-shell windows made this simpler than
Progression was -- the shell is the only door in now, so there were no
other call sites to teach.
Two things had to come OUT of the wrapper: TAB_DEFS and
ns:GetConsumablesTabs. Shell:Mount restores the saved sub-tab, resolves
the tab list and draws the strip BEFORE it calls Build, so a page built
on first open still has to answer that question with no frames in
existence. GetConsumablesTabs reads TAB_DEFS and nothing else, which is
what makes it able to. Its partner SetConsumablesTab stays inside: that
one drives real tab buttons and the shell only calls it on a click.
This is the failure mode worth having a check for, because it hides
from anyone who looks twice -- an empty strip on the first visit and a
correct one on the second. The harness now asks every lazily-built page
for its sub-tabs at load, before the render phase can build anything,
and fails a page that answers with nothing:
consumables:lazy:3 loot:lazy:2 mythicplus:built:2
The loot browser has been lazy since it was written and already kept
its tab function outside; Mythic+ is still eager, so it is covered
against the day it is not. Putting TAB_DEFS back inside the builder
fails the check with the first-visit/second-visit sentence spelled out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 28cdb1e933d1d1775f854c07116a93c8efc71e02
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 14:14:54 2026 +0200
Remove the boss trainer and the pre-shell window it was built beside
Two removals that turned out to be one. The raid trainer's arena was
already switched off in the guide -- TRAINER_READY = false, with a note
saying a trainer that is confidently wrong teaches the wrong reflex --
and Mr. Yeeper had no display surface outside Core\AppFrame.lua, the
window the shell replaced. Both were loading in full, every session,
for nobody.
Gone: RaidTrainer.lua and RaidTrainerScenarios.lua (6,626 lines),
AppFrame.lua and Integrations\Dashboard.lua (2,004), and Advisor.lua,
Snark.lua and FunStats.lua (2,343). Snark had no callers at all and had
not for some time. With them go /yh train, /yh advisor, /yh introreset,
/yh fun and /yh classic, the guide's "Test my knowledge" button, and
the yeeper-voice skill.
The shell was already the only door. OpenMain and OpenTo tried the
shell, then the app frame, then the dashboard; the second and third
rungs only ran when Core\Shell.lua was absent, which is a .toc that
predates it. mainIsOpen asked all three. All of that is now one branch.
What this leaves at login: 106 frames and 161 regions, from 256 and 901
at the start of the day. memcheck totals 3216 KB, from 4157. The addon
is 49,772 lines of Lua, down from 60,745.
The harness loses 27 trainer checks and 2,964 lines with them -- it was
a third of the file. Removing them mechanically left one orphaned
L.eval whose if/else had been swallowed by an overlapping span, which
still ran and still reached for ns.RaidTrainer; the first boundary rule
I used walked back to a blank line, and Lua source inside the triple
quotes is full of them. Anchoring on the `= L.eval(` line instead is
what made it reliable. Also dropped: the trainer's garbage budget, its
scenarios file in LC_DATA_FILES, the guideOnly-has-no-scenario
assertion, and the pre-shell-window check added earlier today.
README loses the Mr. Yeeper section and five command rows. ROADMAP's
Cracked Keystone item kept its reasoning but lost its conclusion --
the advisor was the home it proposed, so it now says it needs a new one.
CHANGELOG is left alone; it is history, not description.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit b839d2f9a9716da9ce6098712b1be806e59e56d4
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 13:34:10 2026 +0200
Progression drew its card grid during the loading screen
26 frames and 198 regions of Mythic+, raid and delve cards, built at
file scope, for a page that has to be clicked to be seen. Load is now
129 frames and 282 regions, from 155 and 480 after the pre-shell window
and 256 and 901 before any of this.
Core\ShellPages.lua already had the mechanism -- `create`, looked up by
name and run before the page mounts -- and the loot browser has used it
since it was written. This is the second page on it, not a new idea.
Wrapped whole rather than picked apart. The file is one long
construction script with no events and no registration, so the smallest
honest change is to stop running it until somebody asks: the header goes
in above the first CreateFrame and an `end` at the bottom, and its 29
locals become the function's, which is why they were counted first (Lua
allows 200). ns:SetProgressionAppMode moves inside with them, which is
right rather than tolerable -- ShellPages looks it up by name AFTER
running the creator, and every other caller guards on it.
The two doors that skip the shell -- Integrations\Dashboard.lua and the
pre-shell window's ShowAppPage -- now ask for the page instead of
testing for it, the same shape the loot branch beside them already used.
The harness needed the creator added to its render list. Without it
SetProgressionAppMode reads "absent", which is not a failure, so the
whole page would have quietly lost its coverage in the same commit that
made it lazy.
Next two down the list are not this shape. Both Consumables and the
raid trainer publish a small API that has to answer BEFORE their frame
exists -- Shell:Mount resolves a page's sub-tabs and builds its tab
strip before it calls Build, and RaidGuideUI asks the trainer
HasScenario while drawing a page the trainer has never opened. Wrapping
either one whole would take those with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 25e2aa544d4881ccd5f2e47c51fbc54bf69b805a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 13:25:12 2026 +0200
The pre-shell window built a dashboard nobody can open
Core\AppFrame.lua is the window that opened before Core\Shell.lua
existed. Both ns:OpenMain and ns:OpenTo reach for the shell first, so
the only way to land here is to have no shell at all -- which happens
in exactly one situation: an update installed and reloaded rather than
restarted, where the .toc predates Shell.lua and the file is not
loaded. "Nothing happens" is the worst answer to a key press, so the
fallback stays.
Its cost did not. The file built its whole home dashboard at load --
character bar, profile dropdown, crest bars, Yeeper panel, planner,
vault strip, nav buttons -- 64 frames and 209 regions, the largest
single contributor in the addon, for a window that in any normal
session cannot be opened by any means. It now returns at line one when
the shell can Toggle and Open, and builds nothing.
Load across the addon is now 155 frames and 480 regions, from 219 and
689 after the gear window, from 256 and 901 before either. memcheck
totals 3804 KB, from 4157.
Everything this file publishes is read behind an `if ns.X then` by
every caller, so absent reads as "this build has no old window".
ns:ToggleDashboard survives regardless -- Integrations\Dashboard.lua
loads later and defines its own.
That leaves 1700 lines with no coverage at all, reachable only in the
one case nothing tests. So the harness now takes the shell away, runs
the file again, and checks the window it should leave behind: the frame,
ShowAppPage, ToggleApp, and that it lands on home rather than blank.
Neutering the guard fails it in the other direction, which is the half
that proves the saving is real.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 5b665c9032086456180e2028b7180bc5885d935a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 12:49:03 2026 +0200
The gear window cost 44 frames at login to open at a vendor
Core\UI.lua built sixteen slot buttons and a crest frame per track at
file scope: 48 frames and 228 regions created during login, every login,
for a window that opens at an upgrade vendor -- somewhere most sessions
never go. It is now 11 and 16. Across the addon, load went from 256
frames and 901 regions to 219 and 689.
Nothing had measured this because nothing could. A frame's real cost is
C memory the client does not attribute to the addon, so
GetAddOnMemoryUsage never showed it; and Tools/memcheck.py models frames
as plain Lua tables, so it reported 211 KB against this file and made it
look like data. Counting CreateFrame calls per file is what found it --
the same count says Core\AppFrame.lua is now the biggest at 64 and 209.
memcheck's total drops 4157 KB to 3999.
The container stays eager: eleven places test ns.MainFrame for
existence and a nil one reads as "not part of this build". Everything
inside it moves behind EnsureBuilt, called from the container's OnShow
and from the two refreshes that write into the pieces it builds.
ns.AppCrestFrames is published empty at load and FILLED on build rather
than replaced, because Features\Gear\Crests.lua takes its reference at
load and would otherwise write into a table nothing draws.
The harness snapshots the window's state the instant loading finishes --
read any later and it would only ever say "built", whatever the code
did -- then checks both doors in, that a second build does not
duplicate, and that the table identity holds. Removing BuildSlotButtons
from EnsureBuilt fails it with a nil button inside RefreshAllSlots,
which is the crash a player would have seen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit ebff73a03c89cb174f8d6b435a0f8d95a60e2e14
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 12:06:22 2026 +0200
Eight teleport spells that exist in no game, written down twice
1289772..1289782 covered every Midnight Season 2 portal in a tidy
contiguous run, and appear in no addon on disk, no database and no game.
Every Midnight tile on the Teleports page and the Mythic+ page was a
dead button. They survived because they were plausible, sequential, and
kept in TWO tables that agreed with each other -- the Teleports page had
one grouped by expansion, the Mythic+ page a second keyed by dungeon
name.
Corrected against BigWigs\Tools\Keystones.lua, which is hand-maintained
per season, and cross-checked against EllesmereUI's SEASON_PORTALS.
Where the two disagreed with us they agreed with each other:
Altar of Fangs 1289772 -> 1286812
Murder Row 1289775 -> 1286809
Den of Nalorakk 1289773 -> 1286807
The Blinding Vale 1289776 -> 1286801
Voidscar Arena 1289777 -> 1286804
Kings' Rest 1289778 -> 1286831 (+ the BfA 272261)
Temple of Sethraliss 1289782 -> 1286828 (+ the BfA 272267)
Ruby Life Pools dropped its invented second id; it kept the
Dragonflight spell when it came back
Two more were wrong in ways nobody would have connected to this. Siege
of Boralus and The MOTHERLODE!! have one spell per faction, and both
were filed with a single id -- so half of all players saw a locked tile
they could never unlock. The MOTHERLODE's was 272268, which
AllTheThings files under NeverImplemented: it shipped in the data and
never in the game, so IsSpellKnown could not tell it from "you have not
earned this yet". Also dropped 1248186, 1253942 and 1286119, three more
candidates with no trace anywhere.
Now one table, Features\Teleports\TeleportData.lua, read by both pages.
A wrong id is still possible; a wrong id in only half the addon is not.
The harness asserts no two dungeons claim the same spell, that every
season name and Mythic+ alias resolves, and that a faction pair keeps
both spells with the player's first -- and it stubs UnitFactionGroup,
which it never had, so that branch runs at all. It also stops
hardcoding a teleport id of its own, which was a third copy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit cd269869d4432f4289995b0119634d12811a1e11
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 11:48:00 2026 +0200
The query wanted an item and got a location, so nothing was ever free
C_ItemUpgrade.GetHighWatermarkForItem takes itemInfo -- the link/name/id
form -- and had been handed an ItemLocation since it was written. That
does not return zero, it throws, and the pcall around it turned every
throw into an honest-looking "this slot has been nowhere". /yh debug
printed wm:none down all sixteen slots and every row priced its ranks as
paid. The whole free-upgrade half of the advisor was switched off, in a
way nothing could see.
The harness could not have caught it: its stub took the location and
answered cheerfully. A stub more forgiving than the client is a stub
that certifies a bug, so it throws on a non-string now, and the gear
fixtures carry slot-encoded links so the query has something of the
right shape to ask about. Play/Stop/IsPlaying became real for the same
reason -- a no-op that answers "not playing" forever makes every
`if not anim:IsPlaying() then anim:Play()` read as correct.
Underneath that, two answers instead of one. GetMarkRead is what the
client said, zero for unread; GetFreeUpgradeIlvl is that raised by
anything bound in the bags, and standing on the bags alone when the
client says nothing. Which one a caller wants depends on the direction
being wrong would hurt: "this rank is free" off a floor is always true,
and a price off a floor over-charges for everything above the line. So
the free-rank rules run on the floor and work with no answer at all,
while GetMarkLaunder and GetBagLift guard on the read.
Rings and trinkets keep one line between them and it follows the lower
of the pair, so the floor pools both worn pieces with the bagged ones
and takes the second highest: one 295 over a 292 and a 292 buys nothing,
a second 295 frees a rank in both slots.
New: RULE 0c for a piece still inside its trade window, saying what
keeping it is worth and that letting the timer run out counts as
keeping. A pulsing cyan border on gear cards whose ranks are already
paid for. A "Collect N free gear upgrades" row leading Worth Doing
Tonight -- the only row on that panel that can be finished during a
loading screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 15e49d027f1a70b41778ab148b71a79f3cc8288a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Fri Aug 21 10:09:57 2026 +0200
Commit the tree as it stands, before another trinket scrape
Everything that was outstanding on this branch, taken together at the
user's request so the next data run lands on a clean baseline rather
than mixed into work in progress.
Not all of it is mine. The crest-advice work -- the raid guide and its
Encounter Journal reader, the weekly checklist, the Mythic+ data and the
roadmap -- was in the tree already, along with WhatsNew.lua's removal and
the two .toc entries that went with it. What is mine here is the
bloodmallet re-scrape from earlier today (Protection Warrior's
single-target list came off last season's data, Enhancement's 5-target
list was re-simmed), the README coverage counts that follow from it, and
Tools/memcheck.py.
memcheck.py reports where the addon's memory goes, per file, by loading
it against loadcheck's stub and collecting garbage around each file. The
headline: about 4.1 MB, 40% of it generated data tables, and the trinket
file alone is 563 KB of which 350 KB is per-row table overhead rather
than data.
No tag and no push, as asked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 94a2484b279a478cb79b35eac44c3f1dbfbc3b3f
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 23:01:57 2026 +0200
Putting it on is one of the ways it binds, not the exception to it
The last commit wrote the rule down as "binding marks a slot, not
equipping it", which reads as though wearing a piece were the case that
does NOT mark. Wearing it is how most pieces bind. So is keeping one
until its trade timer runs out. The mark is the default outcome of
having looted something, and there is exactly one way to miss it: hand
the piece to somebody else while there is still time.
Nothing computed changes -- unbound is read off the tooltip's own lines
either way -- but one of the sentences a player reads said it, and a
line that teaches the wrong rule is worse than one that teaches none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 26fb015d70450b7983019de191a97175bba85026
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 22:49:51 2026 +0200
Binding is what marks a slot, and a fresh drop has not decided yet
Not equipping it and not upgrading it: the moment a piece is soulbound
the slot has its item level, and a piece handed back to the group inside
its trade window never marks anything at all. The previous pass left
that as an open question in a comment; it is written down now, where the
next person to price against a mark will be standing.
It has one consequence, and it lands on the row shipped last commit. A
Hero drop still sitting in the bags with time on it has put nothing on
the slot, so the Champion piece next to it prices at a hundred -- and
costs twenty the moment the player decides to keep what they looted. The
row was confidently telling them to hold out for a cheaper spare on the
strength of that hundred.
So a slot with an unbound piece above its mark says the price is not
settled and names the piece it is waiting on, instead of advising. How
far the mark would move is deliberately not predicted: rings and
trinkets share one between two slots.
Read off the tooltip's own trade-window and binds-when-equipped lines,
from the client globals so it survives a locale, rather than inferred
from "above the mark, so it must be unbound" -- an inference that is
wrong for exactly the paired slots this matters most in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 12d4c4fb469ad25c35beacd08be2ed6a32065363
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 22:22:54 2026 +0200
The spare in your bags is the only trade between two crest tiers
A slot remembers the highest item level it has held, and upgrades up to
that level cost no crests. That makes a lower-track piece for the same
slot the one way to pay for a rank with a crest you have too many of --
and it is a piece that is never worn, so nothing here could see it.
Bags are scanned for upgradeable pieces by the slot they belong to, and
ns:GetMarkLaunder prices the move against what the slot has already
reached rather than against how far down its track the spare sits. That
distinction is the point: a Champion 1/6 under a slot that has seen 305
is one rank and twenty crests, not five and a hundred, because the other
four land under the mark. Finishing it then hands the worn Hero piece
its own 305 to 308 for nothing.
Where it really is a hundred -- a paired slot whose mark is still down
at the lower of the two -- the row says so, points at the cheaper spare
worth waiting for, and prints the price of not waiting. That is a
judgement call and it is left as one.
A slot the client has not answered a mark for says nothing at all. Every
number here is a difference from that one, so a zero does not make the
advice vague, it makes it wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 575cc6a51a1c80fd7cd4ee4d5f163dc6afc14adc
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 18:56:38 2026 +0200
One paper doll order, not two that had drifted
The gear page and the best-in-slot page each drew a doll from their own
hardcoded list, and the lists disagreed. Hands and Waist finished the
left column on one and started the right column on the other, so the
same character read differently depending on which page was open.
The best-in-slot page had it right -- the character sheet's order, head
down the left and hands down the right. That list moves to
ns.DOLL_LAYOUT and both pages read it.
left Head, Neck, Shoulders, Back, Chest, Wrist, Trinket 1, Main Hand
right Hands, Waist, Legs, Feet, Ring 1, Ring 2, Trinket 2, Off Hand
The weapons are one row in the layout, side "W", because the
best-in-slot page draws them spanning both columns. A page that wants
two flat columns gets ns:GetDollColumns, which splits that row so the
main hand lands under the left column and the off hand under the right
-- side by side on the same line, which is what the weapon row looks
like anyway.
The check pins the order by name rather than by slot id, since the id
order is exactly what made this unreadable, and pins that every slot the
advisor has an opinion about has somewhere to be drawn. A slot missing
from both columns is one the page silently cannot show.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 0ea164ad3e82d330a67e4f0dc456367c2a752dd2
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 17:51:13 2026 +0200
Stop saying "the mark", and say why one trinket beat the other
"You have to set the mark before you upgrade the Hero piece" explains a
mechanic nobody asked to be taught, in vocabulary that belongs to this
file rather than to the game or the player. What a held row needs is the
price, why it is a bad price right now, and the two things that would
change it:
100 Champion to take this from 1/6 to 308, and 6 of your pieces are
this far down — 580 to cover them all.
Too expensive to spend on a guess. Hold until a Hero 1/6 actually
lands here — finishing this piece then starts it at 2/6 instead — or
until a Champion piece turns up further up its track, where the same
crests reach the top for less.
The same word was in the pair lines, which had not been reworded when
the hold hover was:
before caps the track; mark needs Trinket 2 at 308 too
after caps it, but Trinket 2 must reach 308 too
before Rings and trinkets share one mark and it follows the LOWER of
the pair, so this moves nothing on its own...
after Rings and trinkets count as a pair and the lower of the two is
what matters — Trinket 2 is at 305, so nothing changes for
either slot until it reaches 308 as well.
Guarded now across the row and every hover line at once, rather than in
the one place it was last found.
The tie-break also says why it won, which is the question two identical
trinkets actually raise:
On your best-in-slot list, so it comes before Trinket 2.
Only when the pair genuinely split on it -- announcing best-in-slot
about a piece with nothing to be chosen over is noise.
Two smaller things while in here. The stranded warning no longer repeats
the pair line: naming the partner as short, twice, reads as two separate
problems. And a single rank is "its last rank" rather than "all 1 rank".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit dab43dc68f5de06432b031bfd2984f1e699b7c78
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 16:59:44 2026 +0200
Stop naming held slots as prerequisites, and let best-in-slot break ties
Two things the panel got wrong at once, both visible on the same screen.
IT CONTRADICTED ITSELF. Trinket 1 read "Hold 100 Champion" while Trinket
2 read "Feet and Trinket 1 first" -- naming as a prerequisite the very
slot it had just said not to spend on. The plan still ALLOCATES to a held
slot, because the walk knows nothing about holding; the hold is applied
afterwards, per slot, and the blocker list was reading the allocation.
Both ask ns:IsHeldForDrops now, so Trinket 2 reads "Main Hand first".
AND IT BROKE TIES ON SLOT NUMBER. Two trinkets at the same rank and item
level are the same purchase to every term in the scoring, so the walk
fell through to slot id -- Trinket 1 beat Trinket 2 because 13 is less
than 14. When one is the spec's best-in-slot and the other is filler
that is not a tie at all: crests on the keeper stay bought, crests on
the filler leave when it does.
The scraped guide and the player's own pins both count, pins first --
someone who has pinned a choice has simmed it or decided it, and that
outranks a page scraped in August.
Deliberately only a tie-break. Best-in-slot says which of two equal
purchases to make; it does not say a best-in-slot piece is worth more
crests than a bigger upgrade elsewhere. The guide is also one opinion at
one point in time, which ns.GEAR_CAVEAT already says at length.
Both pinned, and both from the direction that discriminates: the
contradiction check refuses vacuity if no row is held, and the tie-break
check first shows the tie falling to slot order with no guide data, then
that marking the SECOND trinket moves the crests against that order, then
that marking the first moves them back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit f20f9b9b8b164dbf16bfac83415aeabadfb0e50e
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 15:49:57 2026 +0200
Keep the overlap warning on the slot it is headed with
On a row titled Neck, three-quarters of the sentence was about a belt:
20 Hero for 305 to 308 — a maxed Champion piece lands there too.
Spend Champion on Waist +5 more (140 held); keep Hero for rank 3+
All true, and the wrong place for it. Someone reading the Neck row is
deciding about the neck, and the fact that decides it is that the first
ranks of this track land on item levels the track below already reaches.
Ranks 1-2 are wasted Hero — Champion reaches 308 too. Keep Hero for
rank 3+
Seventy-four characters against a hundred and thirty, and it says the
thing the old one buried between two instructions about other slots.
Where the cheaper crests could go is still worth knowing, so it moved to
the hover along with the arithmetic that was also crowded out -- which
ranks, at what item levels, and how many crests are at stake. Both
branches of the rule share the row now and differ only in whether the
hover has somewhere to point.
The check pins that no other slot's name appears on the row. It cannot
be a blanket rule -- "Main Hand and Feet first" is a legitimate thing for
a held row to say -- so it is scoped to the overlap warning, where
directing crests elsewhere is what went wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 6b09daecaf8a3689ef11936cf9cbfe4b40a06f5c
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 15:05:22 2026 +0200
Remember the high-water marks, so free upgrades can be seen
RULE 0 is the best advice the addon has -- ranks the player already owns,
for no crests at all, sorted above every row that costs something. It
could not fire for most of the addon's runtime.
RefreshWatermarks ran only while the upgrade vendor was open, and
ns.watermarkCache was a plain table that died with the session. So on any
fresh login away from a vendor every mark read zero, GetFreeUpgradeIlvl
returned zero, and a slot sitting below a rank it had already reached was
listed as a normal paid upgrade.
The cache is persisted to SavedVariables now and restored at
ADDON_LOADED, and the refresh is no longer gated on standing in front of
an NPC -- if the API answers away from one the cache fills on its own,
and if it does not, nothing is lost by asking. Marks only ever go up, so
a remembered one is never wrong; at worst it is behind, and the live
query corrects it the moment it answers. That asymmetry is what makes
caching it safe.
Nothing stubbed C_ItemUpgrade or ItemLocation, so this rule had never
been exercised by any check ever written against the advisor -- the mark
always read zero there too, and the branch was invisible from both sides
at once. Both are stubbed now, keyed by equipment slot so a check can put
a mark above the equipped item and watch the advice change. It covers the
rule firing, that free sorts first, and that the mark survives a reload
with the live query dark.
The check clears its own stored mark on the way out. Slot 16 is the
weapon every other gear fixture uses, and leaving a 305 watermark on it
would have had them reporting free upgrades for reasons of ours.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 1ba4fa79673ae7ac08fdd1f55ee15899321dda43
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 14:44:35 2026 +0200
Record that the mark is retroactive
The hold-and-react rule turns on this and the comment stated it wrong.
Finishing the lower-track piece is enough on its own: the mark applies
retroactively and the piece does not have to be worn, so a Champion
piece can be topped up after the Hero drop has already landed in that
slot and the Hero piece still starts a rank higher.
Confirmed against the game rather than derived, which is why it is
written down. Nothing user-facing says it -- a player upgrading a piece
does not need the mechanic explained, only the advice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit ba37060060e68a364327d363fb1ddb04bee522b9
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 14:38:48 2026 +0200
Say the player has no choice, not that the drop has one
"A Hero drop picks its own slot" was carrying the whole argument for
holding, and it reads as the drop having a preference. The point is the
opposite: the player has no say in where one lands, which is exactly why
the crests are worth keeping until it does.
before The mark has to be set before the Hero piece is upgraded, and
a Hero drop picks its own slot — so the crests are worth more
in hand, ready for whichever one it lands in.
after You have to set the mark before you upgrade the Hero piece,
and you do not choose which slot one drops in — so the crests
are worth more in hand, ready for whichever slot it turns out
to be.
Same for the line under it: "a guess at which one the drop will choose"
is now "a guess at which slot the drop will land in".
The check refuses the animated phrasing outright, since it is the sort
of thing that reads fine while writing it and not at all while playing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 2428749cc40ff5ef5d8102250d3debf4750e341c
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 14:36:33 2026 +0200
Hold the crests the drop has not chosen a slot for yet
Two corrections, and the second undoes something this branch removed in
its first commit.
THE MARK IS INSURANCE, NOT A RACE. The previous pass killed the rebate
entirely on the reasoning that a +10 hands out 311 in every slot, so a
308 mark is always overtaken. True, and useless -- of course better gear
exists, the whole season is the trip to it. Some Hero pieces arrive at
1/6, and a slot marked at 308 starts those at 2/6. That is worth having.
But it is worth a FIXED amount, and the price of it varies fivefold. A
Champion piece one rank from its cap buys the same insurance for 20 that
a piece at 1/6 buys for 100. Applied as a cost discount the rebate
helped the expensive runs most -- a hundred crests knocked down to
seventy-three still looked like a bargain -- which is backwards. It is a
term in the numerator now, priced as the item level the saved ranks are
worth, and the cheap mark wins by the margin it should.
It also carries the slot's weight. Unweighted it competed against a
number four or five times its scale and never won: a trinket took a
hundred crests to reach a cap whose mark could not move, beating a slot
that would have banked something.
AND THE RESERVE WAS RIGHT. The mark has to be set BEFORE the higher
piece is upgraded, and a Hero 1/6 picks its own slot without asking. So
on a track the content has outgrown, a deep run is a bet on which slot
that will be -- six pieces at the bottom, one drop, same hundred crests
either way:
Hold 100 Champion — spend it when a Hero 1/6 lands here
Only for runs deep enough to be a real bet. A piece three ranks out is
cheap enough to just do, and the anti-hoarding rule stands everywhere
else -- crests held with nothing to react to are power not worn.
Best Champion: 60 for 298 to 308, all 3 ranks — caps it; a Hero 1/6
drop here then starts at 2/6
Recorded as still-unverified: everything above rests on the mark saving
one rank against a 305 arrival, and on raid item level varying by boss
rather than by difficulty. Neither is checkable from the harness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit b1db07260dbb49990d4f1164ceef9e6ddb7794bc
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 12:31:09 2026 +0200
A mark you can farm past is a race, not a rebate
End-of-dungeon loot is not slot-specific. A +10 hands out 311 in any
slot it likes, so a 308 mark is overtaken everywhere sooner or later,
and the crests that set it bought item level until then and nothing
after.
The raid could still drop a 305 into that slot first and cash the mark
in -- and raid item level is per BOSS, not per difficulty, so the same
slot comes off an early boss low and a late one high. But that is a coin
toss on which source arrives, and advice should not be built on winning
it.
So the rebate is measured against the HIGH end of the drop band rather
than the low one. The low end says what a mark could pay out on; the
high end says whether it survives. Champion caps at 308 under a 311 farm
level, so its rebate is now zero -- and every Champion row says why:
before Best Champion: 100 for 292 to 308, all 5 ranks -- caps the
track, next Hero free to 308
after Best Champion: 100 for 292 to 308, all 5 ranks -- tops out at
308; you farm 311 in every slot
which is a fact about the TRACK, so it reads the same on a row that only
reaches 298. The old phrasing said "caps at 308" there, claiming
something the run did not do.
Two consequences worth naming.
The Champion plan is stats-only now, so it orders by slot value again:
Main Hand, then a trinket. Nothing on that track is banked, and the
panel no longer pretends otherwise.
And the pair rule moved with it. A pair only matters where a rebate
exists to withhold, so the checks that pin it -- one trinket never
carried alone, both finished together when affordable -- now run on
Hero, whose 321 cap does clear the band. On Champion there was nothing
left to withhold and they were passing for the wrong reason.
Recorded but not built: the Loot Browser already scans the Encounter
Journal per boss, per spec, per slot, which is the data that would make
this exact instead of banded. It is also the scan a previous probe got
wrong by reading item level off an unresolved link, so it wants doing
carefully rather than quickly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 49bbe2bcd0884f4a9bedc7548e684c96148a8615
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 11:02:13 2026 +0200
Rings and trinkets share one mark, and it follows the lower
The high-water mark on a paired slot only moves when BOTH pieces reach
the level. Carrying one trinket to 308 while its partner sits at 292
leaves the mark at 292: the next piece to land in either slot is lifted
to 292 and no further. A hundred crests bought item level on one item
and no rebate at all.
The advisor did not know that, and it was the load-bearing assumption
under the last three commits. It recommended exactly the spend that
wastes the crests -- and that is the trap this was reported from in the
first place, a season lost to maxing trinkets.
So the rebate is now gated on the partner. A run that caps a ring or a
trinket earns it only if the other half is already there, and the plan
re-sorts itself around that:
before Main Hand -> 308, Trinket 1 -> 308, Trinket 2 -> 298
after Main Hand -> 308, Feet -> 308, Trinket 1 -> 298
Feet is worth less by slot priority and more by outcome, because Feet is
a slot of one. The pair is a single purchase priced at two, and with 200
in hand it is not affordable, so nothing is spent pretending otherwise.
Where a pair IS the right call the row says what the other half needs
rather than promising a rebate that cannot land:
caps the track; mark needs Trinket 2 at 308 too
and the hover names the mechanic and the partner's actual level.
Pinned from both directions, because a rule that only ever refuses is
indistinguishable from one that never funds a trinket: with 200 no
paired slot is carried alone, and with 600 both trinkets finish
together.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit aa0fab559c283b30cd7590515ef9437467f3583a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 10:48:28 2026 +0200
Say which floor, and whether the track can reach it
"Under your 305 floor, so a drop replaces it" on a Veteran piece quotes
a Hero raid drop that has nothing to do with the item, and is vacuously
true besides -- Veteran stops at 295, so no Veteran rank can ever clear
305. It fired on every row of the track without telling any of them
apart.
Two different situations were being given one sentence:
A track that CAN cross the floor. Champion reaches 308 against a 305
floor, so stopping at 298 is a stopgap the player can fix, and the fix
is the useful half:
40 Champion for 292 to 298, 2 of 5 ranks -- stops under the 305 you
loot; 308 clears it
A track that cannot. Veteran caps ten below the worst thing this
character is handed, so the floor is not the point -- the track is:
Best Veteran: 60 for 285 to 295, all 3 ranks -- Veteran tops out under
everything you loot
That second case turned out to be worse than reported. It is tested
before everything else now, because reaching such a cap previously read
"caps the track" -- which sounds like an achievement and hides that the
mark it sets can never pay out. No drop will ever arrive below 295, so
the last rank buys exactly as little as the first.
The wording check grew a Veteran piece to reach that branch at all, and
refuses a row that quotes 305 on a track that cannot see it.
Unrelated, and not from this change: the weekly checklist check failed
twice during this work with two different messages, and has passed seven
consecutive runs since. Both failures landed while WeeklyChecklist.lua
was being edited in the working tree, so the likely cause is the harness
reading it mid-save rather than anything in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit a020c0277582124e503de1debd681145055f5c71
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 10:13:52 2026 +0200
The cap is the constraint, not where the crest comes from
"You only earn Champion once Hero caps" is false. Champion is on offer
all season -- M0, low keys, tier 7-10 delves, the weekly outdoor stuff
-- and a player 60 short of their cap will find those 60 without being
told how. What they cannot do is exceed the cap, and that is the only
thing standing between them and the rest of the track.
So the shortfall resolves against the allowance instead:
before 4 more Champion pieces to max would cost 400, and you have
140. You only earn Champion once Hero caps, so drops will fix
those slots before crests do.
after 4 more Champion pieces to max would cost 400, and you have
140. The cap allows 120 more this season, so about 3 weeks --
or a drop finishes any of them sooner.
Which also removes a branch. The allowance grows at the same rate on
every track, so an outgrown one gets the same answer in the same shape,
and the special case that existed only to refuse a week count goes with
it. `outgrown` still decides what a rank is WORTH -- rental against
banked, off the drop band -- and has no business making claims about
income. The "(overflow only)" tag on the rows that cannot afford
anything was the same error and is gone too.
The checks now reject "only earn", "overflow" and "trickles" outright,
so this particular invention cannot come back quietly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 00fd2b3a17135f2128edd623965b2726085e9ad1
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 09:57:48 2026 +0200
Say it in ranks, and in sentences that parse
Two rewrites, both reported as unreadable rather than wrong.
RANKS, NOT ITEM LEVELS. "A Hero piece dropping here would start at 308
instead of 305" states the payoff in a unit nobody carries in their
head. The game shows gear as Hero 1/6 and players talk about it that
way, so the sentence does too:
The next Hero piece to land here then starts at 2/6 instead of 1/6
-- 20 Hero you never spend.
MarkRebate already computed both ladder positions to get the difference
and returned only the difference; it hands back all three now.
AND ONE THOUGHT PER SENTENCE. This was the whole of what a track had
left to say:
Champion: 4 pieces and 400 crests left against 140 in hand, and it
only comes in behind Hero now, so a drop in those slots is the faster
route.
which is the state, the income model and the verdict in one breath, and
none of the three survives it. "Against 140 in hand" leaves the
comparison implied, "comes in behind Hero" is the overflow rule
compressed past the point of meaning anything, and "those slots" points
at nothing the reader can see.
4 more Champion pieces to max would cost 400, and you have 140. You
only earn Champion once Hero caps, so drops will fix those slots
before crests do.
The checks now refuse item-level phrasing where a rank belongs, so the
first of those cannot come back by accident.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 26a42d6f4ab270ce26ba2ddd5394bbb8be701f97
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 09:51:45 2026 +0200
Say it in three lines, not five
The hover had grown into a briefing: a progress count, a shortfall, the
shape of what was left, a drops-or-crests verdict and a warning against
saving up. Every line true and the whole thing unreadable -- nobody
hovers a row to be briefed.
The progress count went first. "0 of your 16 slots are already at 308 or
better, and this makes 1" is filler on a good day and absurd on the day
it fires, which is any character who has not started the track.
The shortfall was the real failure. "260 short" is a number, not a
decision, and it sat there as one. It now ends in the answer instead:
about three weeks, or a drop in any of them. Counted the way a player
counts -- weeks until the track is done, this one included -- where the
old field reported only the resets beyond the season allowance and
answered "2" to a question whose honest answer is 3.
What replaces the four dropped lines is one that was missing.
Reported from a real season: advice to max trinkets drained the wallet,
and a Hero piece one rank from its cap could not be finished. The season
cap was already reached, so there was no going back for it, and the Myth
piece that wanted that slot's mark waited a reset. Twenty crests, a week
late. Run scoring makes that rare on its own -- a one-rank completion is
the best value per crest on any page -- but a high-priority deep run can
still outbid one, and that is exactly when the row has to say so:
Leaves Ring 2 20 short of its own cap, and the season cap is reached
-- that one waits for reset.
Both new branches are pinned, since neither is reachable from the
standing fixture: the week arithmetic across all three shortfall shapes,
and the stranded completion including that it does not cry wolf on the
row being left behind.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit d272909746547ce3d2b14e4c9d3d5027b7e20536
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 02:34:08 2026 +0200
Turn the shortfall into a decision
"120 short" is a number, not something anyone can act on. The same
figure is an evening's work or half a season, and the panel stated it
and stopped.
Three things decide which:
WHERE THE CAP SITS. A shortfall inside this season's allowance is not a
wait at all -- those crests are sitting in content the player has not
run yet, and telling them to come back next reset is wrong. Only the
part beyond the allowance costs resets, at CREST_WEEKLY_INCREMENT a
week. That constant has been defined and read by nothing since it was
written; this is its first caller.
An outgrown track gets no week count on purpose. Its income is whatever
spills out of a higher track capping, and nothing the addon can see says
when that lands, so it states the condition rather than inventing "2
resets".
THE SHAPE OF WHAT IS LEFT. 260 crests across three pieces a rank from
their cap is a drop away; the same 260 across three pieces at 1/6 is
most of a season, and those slots are cheaper to replace than to carry.
Same total, opposite advice -- so the remaining pieces are sorted and
the nearest and deepest reported, and the line splits three ways:
all close, uneven (name the cheap one), or all deep (chase gear first).
AND NOT HOARDING. Crests held toward a total that will not be reached
are power not being worn for every week until a drop resets the plan
anyway. It fires exactly where saving up is the tempting mistake: a real
shortfall with something affordable sitting in front of it.
4 pieces left after this, 400 crests to finish them, 140 in hand, so
260 short -- and Champion only trickles in behind a higher track now.
All of them are 5+ ranks down, which a drop does for free. Chase gear
in those slots before crests.
Spend what you have anyway -- crests in the bag are power you are not
wearing.
Trimmed while adding: "drop" was being said in three separate lines, and
the slots that need one folded into the progress count where they read
as part of the same thought.
The timing arithmetic is pinned across all three branches -- enough in
hand, short inside the allowance, short beyond it -- since none of them
were reachable from the standing fixture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 580b6f0100a81c17c49e6938ac63306355cf5e09
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 02:21:49 2026 +0200
Say what the upgrade does, not how the system works
The hover was a rules lecture dressed as advice:
Champion tops out at 308 and the item stops there; it does not move
onto the Hero track.
What carries on is the slot. Its mark sits at 308, so the next Hero
piece to land here is lifted to 308 free -- worth 20 Hero.
Everything you are handed arrives on the Hero track (305 and up),
which Champion crests cannot upgrade.
Every sentence true, none of them about this character. It was the
reasoning behind the feature transcribed into the panel, which is a
thing an author finds interesting and a player does not. Nobody hovers a
recommendation to learn how the game works. They hover it to find out
why THIS one.
Maxes Main Hand -- the last 3 ranks Champion can give it, for 60
crests.
A Hero piece dropping here would start at 308 instead of 305 -- 1 free
rank, 20 Hero you never spend.
6 of your 16 slots are already at 308 or better, and this makes 7.
3 pieces left after this, 260 crests to finish them, and 140 in hand,
so 120 short.
2 slots are below 308 and need a drop, not crests.
Same mechanics, stated as what they do. "A Hero piece dropping here
would start at 308" says everything the paragraph about high-water marks
said, about the player's own weapon.
The missing idea was that a track has a FINISH LINE, and it is not the
achievement -- it is the moment every slot sits at or above the track's
cap, because from then on the crest has nothing left to buy.
GetTrackCompletion counts it, and slots count as done however they got
there: a Hero piece at 311 is past the Champion cap and will never want
a Champion crest, so it is finished for this purpose without a crest
being spent on it.
That turns a spend into progress instead of a purchase. "3 pieces and
you never need Champion again, then it is only Hero and Myth that
matter" is a reason to act; "Champion tops out at 308" is a fact to
memorise.
The style is pinned. The check refuses "tops out at", "does not move
onto", "What carries on", "A rank above" and "cannot upgrade" outright,
and requires the hover to name the slot, say what maxing buys on the
piece that replaces it, and say how far along the track is -- so the
next pass cannot drift back into explaining the system.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit a65f46a0f367b884cd3a9e2359ecf12b240ca488
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 02:04:11 2026 +0200
Finishing a track pays the slot, not the item
An item does not move onto the next track. A Champion piece at 6/6 is
308 and is finished forever. The previous commit had it climbing onto
Hero and told the player so on the panel, which is wrong twice over --
about the mechanic, and about what the spend is worth.
What carries on is the SLOT. Its high-water mark now sits at 308, so the
next Hero piece to land there -- arriving at 305 -- is lifted to 308 for
nothing. The crests saved are HERO crests, on an item not owned yet.
That inverts a weight. The old promotionCovered discounted the payoff
when the content already dropped the next track, on the theory that a
drop was coming anyway. The opposite is true: the mark only ever pays
out when such a piece DOES land, so a track the player is handed often
is what makes finishing worth doing. MarkRebate reads the size off the
drop band instead of the flat two ranks TRACK_FREE_RANKS names -- a 305
raid piece is lifted one rank, a 311 key piece starts above the mark and
owes it nothing -- and applies it to the run's COST, because 20 Hero
saved is worth more than the 20 Champion it cost.
before 292 to 308, all 5 ranks -- finishes the track, so the piece
moves to Hero and can keep climbing (100 of your 200 Champion)
after 100 Champion for 292 to 308, all 5 ranks -- caps the track,
next Hero free to 308
Which is also 50 characters shorter, and it needed to be: the Gear page
set SetWordWrap(false) on a fixed 42px row, so every sentence long
enough to need the space was cut mid-word -- the advice with most to say
was the advice you could not read. The reason now wraps and the row
measures itself, the way Core/UI.lua has rendered this same list all
along, and the fixed 42/46 pair is named constants so they cannot drift.
The reasoning the short line had to drop moved to a hover: the
arithmetic, both ends of the drop band, what the mark does, and where
the whole track's budget stands.
That last one is new. Every question in this file was asked one slot at
a time, and the most useful one about a track cannot be -- "can I finish
this at all" is a fact about sixteen slots and one wallet. Without it
the panel opens a 100-crest climb on the fifth Champion piece while four
others sit in the same state and the wallet holds 200: every row true,
the set of them describing a plan that cannot happen.
Champion: 5 pieces, 460 to finish them all, you hold 200. Enough for
2 -- Main Hand, Trinket 1. The other 3 want drops, not crests.
GetGearCensus counts the whole doll, old-season and empty slots
included, because those are demand that has not arrived yet. Cheapest
completions first, since topping out is worth the same on every slot but
costs whatever that piece has left to climb.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit 5c5b428a689d54830bb15407f228f8b23e78eda7
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 01:42:35 2026 +0200
Plan crest runs, not ranks
The walk bought one rank at a time and re-picked the best slot after
each, so equal-priority slots interleaved by item level. That reads like
fairness and costs the player the only thing on a track worth saving
for: the cap. Reaching it promotes the piece onto the next track, and
the ranks nearest the cap are the ones landing high enough for the slot
to keep them -- neither of which a one-rank lookahead can see.
On the panel that prompted this, 160 Champion bought one piece to its
cap and stranded two trinkets mid-track. The same crests finish two.
Every candidate now offers every run it could still buy -- one rank, two,
up to its cap -- scored on value kept per crest, best run wins whole. A
promotion only counts toward a run the wallet can actually finish, which
is what stops the plan talking itself into a climb it cannot complete.
Runs are chosen whole but RECORDED rank by rank, so the steps list, the
running total and the paid/unpaid line are untouched.
What a rank is worth is no longer its item level. It is its item level
times how long the player keeps it, which the drop band decides: above
the band it is permanent, inside it the slot's mark refunds it when the
weaker source fills the slot, below it any drop clears it. ns.PLAN_VALUE
carries the weights and why they are coarse.
The strings answered "what does this buy" and never "does any of it
survive", and they had to CHOOSE between the spend-here-first lead and
the clause explaining that -- first won, which is why a weapon and a
trinket in identical positions got opposite advice. Both print now, and
the lead names its wallet, so five tracks no longer each claim to be THE
first thing to do.
before Spend here first: 298 to 302, all 3 ranks to 308
(60 of your 160 Champion to spend)
after Best Champion spend: 298 to 308, all 3 ranks -- finishes the
track, so the piece moves to Hero and can keep climbing
(60 of your 200 Champion)
"N more Champion covers this one too" read as an instruction to farm
Champion. On an outgrown wallet nothing the player runs pays that tier
directly; it arrives by capping a higher one and spilling down, which is
a different plan for the week, so the row says so.
riskRank no longer orders the walk. Risk is a gap against a ceiling built
from the raid track's MAX -- ten levels above what the raid drops -- so
on a Heroic profile every slot below Hero reads "high" and the signal is
flat. The band replaces it and is measured against the right number.
Also drops a GetReplacementRisk call per slot that nothing read.
Two checks added: the plan outcome on the original numbers, and the
sentences the panel builds from it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
commit a88ccc65a57551f0060e7cd9d7f4fbce58c1cc63
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date: Thu Aug 20 01:24:00 2026 +0200
Stop reserving crests the content has outgrown
The Improvements panel reported a 200-crest Champion wallet as 160 and
spent it in an order that put two rows under "Spend here first". The 40
missing crests were a reserve, held back for the next drop.
But Champion caps at 308 and this character is handed 305 by the raid
and 311 by a +10 key. Every piece they get arrives on the Hero track,
which Champion crests cannot upgrade -- so there is no future drop the
reserve could ever pay for. It was retiring item levels the player could
be wearing, in a currency with nowhere else to go, and the 40 it held
were exactly what a second piece needed to reach its track cap.
GetDropCeiling already computed the two numbers this turns on and
returned only the higher one. GetDropBand returns both: a rank landing
under the low end is rental, one landing between the ends banks a refund
the weaker source pays out through the slot's high-water mark.
Worth recording that this leaves the reserve unreachable. It needs a
capped track with no income and a "high" risk slot, and risk is a 20+
gap against a ceiling built from the raid track's max -- which no Hero
or Myth slot can ever show. Every wallet that could reach the branch was
one the player had outgrown, which is where reserving is wrong. The gate
is the correct semantic and stays; the risk model is due to be
reconciled against the band.
Also here, both small and both long overdue:
The advisor and /yh discounts named the achievement "of the Dawn" while
checking Season 2 ids -- players were sent to look up an achievement
that no longer matched. One shared suffix now, guarded by a check.
GetAchievementBlockers exposes the split GetAchievementProgress was
already making and throwing away. Rule 5.5 only speaks when every
remaining slot is upgradeable, so the moment one slot needs a drop the
addon worked the whole thing out and said nothing -- silent in exactly
the case worth naming.
ROADMAP carries the rest of the model: promotion-aware planning, one
global order, scarcity derived from CREST_SOURCES rather than declared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This mod has no additional files

