DopingControl v0.6.4

This commit is contained in:
2026-08-30 15:37:17 +02:00
parent ecab5ba845
commit 695dd9dcd4
23 changed files with 2278 additions and 296 deletions
+109
View File
@@ -4,6 +4,115 @@ All notable changes to DopingControl. Versions are the ones actually
released here — a new feature raises the middle number and resets the last released here — a new feature raises the middle number and resets the last
one, the last number is for fixes without new features. one, the last number is for fixes without new features.
## Unreleased
## v0.6.4
### Equipment expectation top-up now self-heals every refresh, not just once at ADDON_LOADED
Field report after v0.6.3 shipped (WAIST seeded `true` for every role):
after deploying and reloading, the Equipment matrix still showed 16
columns (no WAI) and the owner's own row read "(read failed)" / sum "--".
Offline reproduction against the real captured dump
(`dc_raid_1788070493.txt`, the scan taken right after the reload) and the
real live `db.expectations` shape
(`WTF\...\SavedVariables\DopingControl.lua`, all 20 role/class lines —
`NECK`/`R1`/`R2 = true`, `WAIST` absent everywhere) through the actual
`DC_Gear.Assemble` -> `DC_Model.build` -> `DC_Matrix` pipeline found no
Lua error anywhere — no nil dereference, nothing caught by a `pcall`.
Both symptoms are real, and neither is what it first looked like:
* The "(read failed)"/"--" row is a correct, unrelated read: the dump's
own `P`/`H`/`X` lines already carry `aurasRead=0`/
`unreadableReason=read failed` before any render happens —
`scan/aura.lua`'s "zero USABLE auras this scan => aurasRead=false" rule
firing on a genuine zero-buff moment. Nothing to do with WAIST or
equipment.
* The missing WAIST column traces to `core/model.lua`'s `anyClassExpects`:
it only falls back to the `DC.DEFAULT_EXPECT` seed for a (role, class)
combo with NO saved line at all — an *existing* class line (any line
materialized before the v0.6.3 seed change) is trusted as complete, key
by key. `DC.EnsureExpectations`/`topUpLine` (`core/config.lua`) are
themselves correct — proven by running them against the exact real
saved shape above — but they only ran once, at `ADDON_LOADED`. A
`db.expectations` line materialized before v0.6.3 shipped therefore
stayed short the new key until a SECOND full reload happened to run
`EnsureExpectations` again against the current seed.
Fix: `ui/matrix.lua`'s `MX.ExpectTable` — the one resolver both the header
and `MX.Refresh` read the expectation table from — now defensively re-runs
`DC.EnsureExpectations(d)` every time it resolves, i.e. on every
scan/window redraw, not only once at boot. `topUpLine` only ever fills
`nil` keys, so this is idempotent and side-effect-free on an
already-current table; it self-heals a stale class line the moment the
window redraws, without requiring a second reload.
New regression test `tools/luatests/test_expectations_topup.lua`: embeds
the real captured `db.expectations` shape (all 20 role/class lines,
verbatim) and a real gear/hit dump line, reproduces the exact
16-column/no-WAI/`(read failed)` symptom through the unmodified
`DC_Model.build`, then proves `MX.ExpectTable` self-heals it to 17 columns
with WAIST at column 6.
## v0.6.3
### Waist is expected by default for every role
Owner decision, reversing the "confirmed, not a bug" call below: every role
wears an enchantable belt, so WAIST now belongs with the other worn
equipment slots (HEAD, CHST, WRST, ...) instead of the opt-in neck/ring
treatment. `data/expectations.lua`'s seed now carries `WAIST = true` for
all five roles. Existing SavedVariables pick up the flag automatically —
`core/config.lua`'s `topUpLine` already adds any seed slot the saved line
has never seen (`nil`) as `true` on load, the same mechanism that
delivered the neck/ring columns to existing saves; no separate migration
was needed. An explicit `WAIST = false` set for a role/class still hides
the column, same opt-out behavior as any other slot.
### Confirmed: the waist column's off-by-default is intended, not a bug
A field report said the belt was worn but the Equipment tab still showed
16 columns, no WAI, after the v0.6.2 waist-slot fix was deployed and the
UI reloaded. Traced end to end (`core/model.lua` `tabSlots`/
`anyClassExpects`, `ui/matrix.lua`'s `vm.slots` column build, the live
SavedVariables `expectations` table): the gating is working exactly as
v0.6.2 shipped and tested it — the waist enchant column is opt-in per
role/class, the same treatment as neck/rings, and simply had not been
enabled for any role/class yet (unlike neck/rings, which already carry
`= true` in every class line of the reporting player's save from before
this default existed). No code was hiding anything; the column appears
the moment the checkbox is enabled in Options > Equipment.
Added a regression test (`tools/luatests/test_model.lua`, section 8d)
that exercises the real production path — `core/model.lua`'s `M.build`
through `tabSlots`/`anyClassExpects`, the same function `ui/matrix.lua`
renders from — instead of the static `DC.SLOTS_EQUIPMENT` array the
layout test uses. It pins both halves: WAIST absent from `vm.slots`
under the default seed, and present + classifying correctly (HAS/
MISSING/UNKNOWN) once a role/class expectation is switched on. This
closes the gap that let the "is it really gated correctly" question go
unanswered by the existing suite.
## v0.6.2
A fix release.
### The waist slot was never scanned
No equipment read — the raid dump, the Equipment tab, the Hit tab's
belt contribution — ever covered the waist (inventory slot 6). The slot
list that drives every `GetInventoryItemLink` read was borrowed from the
Equipment tab's enchant-tracking column list, which had no waist column;
that list never included the waist at all, so it was silently skipped in
every read that reused it, not only display.
The waist turns out to be enchantable on this server (a custom "Belt
Buckle" item line, the same kind of addition that already made the neck
and both rings enchantable beyond stock behaviour), so it now gets its
own Equipment-tab column — off by default, like the neck and ring
columns, until enabled per role/class in the options grid.
## v0.6.1 ## v0.6.1
A fix release. Everything here is something that was supposed to work A fix release. Everything here is something that was supposed to work
+7 -1
View File
@@ -1,7 +1,8 @@
## Interface: 11200 ## Interface: 11200
## Title: DopingControl ## Title: DopingControl
## Notes: Raid consumables, buffs, debuffs, resistances, hit and enchant checker ## Notes: Raid consumables, buffs, debuffs, resistances, hit and enchant checker
## Version: 0.6.1 ## Author: ShempError
## Version: 0.6.4
## SavedVariables: DopingControlDB ## SavedVariables: DopingControlDB
core\const.lua core\const.lua
@@ -16,6 +17,7 @@ data\resist.lua
core\model.lua core\model.lua
core\roles.lua core\roles.lua
core\sim.lua core\sim.lua
core\demo.lua
scan\talents.lua scan\talents.lua
scan\reach.lua scan\reach.lua
scan\aura.lua scan\aura.lua
@@ -24,9 +26,13 @@ scan\resist.lua
scan\hit.lua scan\hit.lua
scan\engine.lua scan\engine.lua
core\probe.lua core\probe.lua
ui\cellsheet.lua
ui\widgets.lua ui\widgets.lua
ui\matrix.lua ui\matrix.lua
ui\options.lua ui\options.lua
ui\report.lua ui\report.lua
features\minimap.lua features\minimap.lua
core\init.lua core\init.lua
# DEV ONLY -- remove this line and dev\ when cutting a release orphan commit.
dev\raiddump.lua
+12
View File
@@ -183,6 +183,13 @@ same gaps, every time. The full UI (all six tabs, roles, pills, report preview)
inspected and configured without being in a group, and all chat output is disabled while test inspected and configured without being in a group, and all chat output is disabled while test
mode is on, so nothing simulated can ever reach another player. mode is on, so nothing simulated can ever reach another player.
## Demo mode
`/dc demo` (or the button in the options panel) shows the same simulated raid, but with **all
six school protection columns at once** — fire, frost, nature, shadow, holy and arcane — even
though five of them are switched off by default. It is a viewing mode, nothing else: your saved
expectations are never modified, and the mode itself is not saved either, so a reload ends it.
## Install ## Install
**Option A — download (simplest):** **Option A — download (simplest):**
@@ -213,6 +220,7 @@ git clone https://github.com/ShempError/DopingControl.git DopingControl
| `/dc options` | opens the options panel (also: right-click the minimap button) | | `/dc options` | opens the options panel (also: right-click the minimap button) |
| `/dc report` | opens the raid report preview | | `/dc report` | opens the raid report preview |
| `/dc test` | toggles test mode (simulated 40-player raid, chat output disabled) | | `/dc test` | toggles test mode (simulated 40-player raid, chat output disabled) |
| `/dc demo` | toggles demo mode (the simulated raid with all six protection columns shown; your expectations stay untouched, the mode is not saved) |
| `/dc probe [range <name>]` | dumps raw scan-API returns to a file for debugging data-source gaps (SuperWoW required) | | `/dc probe [range <name>]` | dumps raw scan-API returns to a file for debugging data-source gaps (SuperWoW required) |
| `/dc talents [on\|off\|clear]` | shows how many players' talents are in, or stops asking for them | | `/dc talents [on\|off\|clear]` | shows how many players' talents are in, or stops asking for them |
| `/dc unknown` | lists buff names the scanner saw but could not classify — useful for reporting gaps in the data tables | | `/dc unknown` | lists buff names the scanner saw but could not classify — useful for reporting gaps in the data tables |
@@ -239,6 +247,10 @@ either way.
See [CHANGELOG.md](CHANGELOG.md). The short form: See [CHANGELOG.md](CHANGELOG.md). The short form:
- **v0.6.4** — the equipment expectation top-up self-heals on every refresh, so a profile
saved before the waist column existed picks it up without a relogin.
- **v0.6.3** — the waist slot is expected equipment by default for every role.
- **v0.6.2** — fix: the waist slot was never scanned, so a missing belt could not be reported.
- **v0.6.1** — fixes: other players' resistances are reconstructed from their gear (the tab - **v0.6.1** — fixes: other players' resistances are reconstructed from their gear (the tab
showed `?` for everyone but you), the weapon column can report a missing imbue again, six showed `?` for everyone but you), the weapon column can report a missing imbue again, six
consumables whose aura name differs from the item name are matched, "gaps only" no longer consumables whose aura name differs from the item name are matched, "gaps only" no longer
+38
View File
@@ -47,6 +47,44 @@ DC.DEFAULTS = {
skillBooks = {}, skillBooks = {},
} }
-- ------------------------------------------------------------------
-- DC.SimOwnsStore(db) -> bool
--
-- The ONE owner predicate for DC.store, and the question every scan gate
-- asks -- never db.testMode alone. Both simulated modes replace the store
-- with the simulated raid, and while either is on "real data never beats
-- the simulation":
-- * db.testMode -- /dc test, persisted (DEFAULTS above)
-- * DC.demoMode -- /dc demo, transient (core/demo.lua)
--
-- Why it exists: the gates named db.testMode only, so a finished scan
-- overwrote the sim store while demo mode was still armed. The demo
-- expectation layer then graded the REAL raid against all six school
-- protection columns (five of which ship with no seed entry at all): every
-- raider grew fabricated MISSING cells, the banner vanished with the "sim"
-- source that had been the only marker that this is a mode and not a
-- measurement, and each row's whisper button went live against a real
-- player.
--
-- Why HERE and not in core/demo.lua, where the mode lives: config.lua loads
-- second, ahead of every gate that calls this, so the gates need no
-- load-order guard -- and a demo module that fails to load can never take
-- test mode's protection down with it (DC.demoMode is then simply nil).
--
-- db is optional; the fallback is for callers that do not hold one.
-- Always returns a real boolean -- the gates invert it.
-- ------------------------------------------------------------------
function DC.SimOwnsStore(db)
db = db or DC.db or DopingControlDB
if db and db.testMode then
return true
end
if DC.demoMode then
return true
end
return false
end
-- Generic deep copy (tables only; keys are copied by reference, values -- Generic deep copy (tables only; keys are copied by reference, values
-- recursively). No cycle handling -- config/expectation shapes are trees. -- recursively). No cycle handling -- config/expectation shapes are trees.
function DC.DeepCopy(src) function DC.DeepCopy(src)
+154
View File
@@ -0,0 +1,154 @@
-- DopingControl core/demo.lua
-- Demo mode: the showcase view. It reuses test mode's simulated raid
-- (DC_Sim.buildStore -- no second roster exists) and lays its OWN
-- expectation table on top so ALL SIX school protection columns
-- (FR/FrR/NR/SR/HR/AR) are visible at once.
--
-- Two hard rules, both of them the reason this file exists at all:
--
-- 1. The demo layer NEVER writes into db.expectations. Live, five of the
-- six school columns ship with no seed entry (data/expectations.lua)
-- and are therefore hidden -- that is a deliberate default, and a
-- screenshot mode must not rewrite the user's saved matrix to get its
-- picture. BuildDemoExpectations deep-copies every line it starts
-- from, so no table is ever shared with the DB either.
-- 2. Demo state is TRANSIENT -- DC.demoMode / DC.demoExpectations live in
-- memory only and are gone after a reload. Deliberately unlike
-- db.testMode (which is persisted): the same treatment DC.simRoles
-- gets in core/roles.lua, and the BulwarkFrame /bf demo pattern.
--
-- Why the columns must be forced per (role, class) and not per role: the
-- model resolves a row through expect[role][class] and falls back to the
-- DC.DEFAULT_EXPECT ROLE line whenever that class line is missing
-- (core/model.lua, expectLine) -- a partially filled demo table would fall
-- back into the seed and silently lose the school columns again. The same
-- fallback drives the dynamic-column check (anyClassExpects), so an
-- incomplete layer would also drop the columns from the header.
--
-- FR is forced too, although the seed already ships it true for all five
-- roles: the user may have unchecked it, and "all six schools" is the
-- point of the mode.
--
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
DopingControl = DopingControl or {}
local DC = DopingControl
-- ------------------------------------------------------------------
-- Transient state (NEVER SavedVariables -- see rule 2 above).
-- ------------------------------------------------------------------
DC.demoMode = false
DC.demoExpectations = nil
-- DC.demoMode is the demo half of DC.SimOwnsStore (core/config.lua) -- the
-- predicate every scan gate asks. It deliberately lives in config.lua, not
-- here: config.lua loads second, ahead of every gate, so a broken demo
-- module can never take TEST mode's protection down with it.
-- The six school protection columns of the consumables slot table
-- (data/consumables.lua). FR is the only one with a seed entry.
DC.DEMO_SCHOOL_SLOTS = { "FR", "FrR", "NR", "SR", "HR", "AR" }
local function forceSchools(line)
local n = table.getn(DC.DEMO_SCHOOL_SLOTS)
for i = 1, n do
line[DC.DEMO_SCHOOL_SLOTS[i]] = true
end
end
-- ------------------------------------------------------------------
-- DC.BuildDemoExpectations(db) -> demo table (also cached in
-- DC.demoExpectations)
--
-- Shape is exactly the live one -- demo[role][class][slotId] -- built per
-- (role, performing class) over DC_Roles.ROLE_CLASSES, so it can be handed
-- to DC_Model.build in place of db.expectations without any special case
-- in the model.
--
-- Source per class line, in order: the user's saved line, else the
-- DC.DEFAULT_EXPECT seed line -- deep-copied either way, then the six
-- school slots forced on. A legacy old-shape role line (booleans directly
-- under [role], migrated away by DC.EnsureExpectations at ADDON_LOADED)
-- is not a table per class and therefore falls back to the seed; demo mode
-- showing seed defaults for one session is acceptable, writing into the DB
-- to avoid it is not.
-- ------------------------------------------------------------------
function DC.BuildDemoExpectations(db)
local src = db and db.expectations
local out = {}
local nRoles = table.getn(DC.ROLES)
for r = 1, nRoles do
local role = DC.ROLES[r]
local seedLine = (DC.DEFAULT_EXPECT and DC.DEFAULT_EXPECT[role]) or {}
local savedByClass = src and src[role]
local classes = (DC_Roles and DC_Roles.ROLE_CLASSES
and DC_Roles.ROLE_CLASSES[role]) or {}
local nClasses = table.getn(classes)
if nClasses > 0 then
local byClass = {}
for c = 1, nClasses do
local cls = classes[c]
local saved = savedByClass and savedByClass[cls]
local line
if type(saved) == "table" then
line = DC.DeepCopy(saved)
else
line = DC.DeepCopy(seedLine)
end
forceSchools(line)
byClass[cls] = line
end
out[role] = byClass
else
-- defensive: without DC_Roles there is no class domain to fill,
-- so emit the legacy role x slot line (the model falls back to
-- the seed for the cells, but the header tooltip and any
-- legacy reader still see the schools)
local line = DC.DeepCopy(seedLine)
forceSchools(line)
out[role] = line
end
end
DC.demoExpectations = out
return out
end
-- ------------------------------------------------------------------
-- DC.SetDemoMode(v) -- the switch behind /dc demo and the options button.
-- Mirrors DC.SetTestMode (core/init.lua) but touches no SavedVariables.
--
-- Store handling: entering demo mode swaps in the simulated raid. Leaving
-- it hands the store back to the REAL scan -- unless db.testMode is on,
-- which owns the simulated store on its own account and must not be kicked
-- out of it. DC.SetTestMode mirrors that in the other direction, so neither
-- mode can empty the other's showcase; both ask DC.SimOwnsStore
-- (core/config.lua), which is also what every scan gate now asks instead
-- of db.testMode.
-- ------------------------------------------------------------------
function DC.SetDemoMode(v)
local db = DC.db or DopingControlDB
if v then
DC.demoMode = true
DC.BuildDemoExpectations(db)
if DC_Sim and DC_Sim.buildStore then
-- buildStore() also refills the transient DC.simRoles -- do NOT
-- wipe them afterwards (see DC.SetTestMode)
DC.store = DC_Sim.buildStore()
end
else
DC.demoMode = false
DC.demoExpectations = nil
if not (db and db.testMode) then
DC.store = { players = {}, source = "scan" }
if DC_Scan and DC_Scan.Start then
DC_Scan.Start(true) -- leaving demo mode: immediate rescan
end
end
end
if DC_Matrix and DC_Matrix.Refresh then
DC_Matrix.Refresh()
end
if DC_Options and DC_Options.Refresh then
DC_Options.Refresh() -- relabels the options button when it is open
end
end
+36 -3
View File
@@ -59,6 +59,14 @@ function DC.SetTestMode(v)
-- into a suggestion badge (the sim roster deliberately contains -- into a suggestion badge (the sim roster deliberately contains
-- exactly two suggestion-badge cases). -- exactly two suggestion-badge cases).
DC.store = DC_Sim.buildStore() DC.store = DC_Sim.buildStore()
elseif DC.demoMode then
-- Demo mode owns the simulated store on its own account
-- (DC.SimOwnsStore) -- the mirror image of core/demo.lua's
-- "leaving demo mode must not kick test mode out of the sim
-- store". Without this the showcase would be emptied here and the
-- rescan below would be dropped by the scan gate anyway, leaving a
-- blank window. /dc demo is what ends demo mode.
DC.store = DC_Sim.buildStore()
else else
DC.store = { players = {}, source = "scan" } DC.store = { players = {}, source = "scan" }
DC_Scan.Start(true) -- leaving test mode: immediate real rescan DC_Scan.Start(true) -- leaving test mode: immediate real rescan
@@ -108,7 +116,7 @@ end
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
-- Slash commands: /dc, /doping -- Slash commands: /dc, /doping
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
local HELP_TEXT = "commands: /dc (window) | scan | options | report | test | probe [range <name>] | unknown | talents [on|off|clear]" local HELP_TEXT = "commands: /dc (window) | scan | options | report | test | demo | probe [range <name>] | unknown | talents [on|off|clear]"
function DC.HandleSlash(msg) function DC.HandleSlash(msg)
msg = msg or "" msg = msg or ""
@@ -119,9 +127,15 @@ function DC.HandleSlash(msg)
if cmd == "" then if cmd == "" then
DC_Matrix.Toggle() DC_Matrix.Toggle()
elseif cmd == "scan" then elseif cmd == "scan" then
-- Real data never beats the simulation while EITHER simulated mode
-- owns the store (DC.SimOwnsStore, core/config.lua). Test mode is
-- named first because it is the persisted one -- but the refusal
-- must name the mode that is actually on, or the user toggles the
-- wrong switch and the scan keeps refusing.
if db and db.testMode then if db and db.testMode then
-- real data never beats the simulation while test mode is on
DC.Print("test mode is on - /dc test to switch back before scanning") DC.Print("test mode is on - /dc test to switch back before scanning")
elseif DC.SimOwnsStore(db) then
DC.Print("demo mode is on - /dc demo to switch back before scanning")
else else
DC_Scan.Start() DC_Scan.Start()
end end
@@ -133,9 +147,28 @@ function DC.HandleSlash(msg)
DC.SetTestMode(not (db and db.testMode)) DC.SetTestMode(not (db and db.testMode))
if db and db.testMode then if db and db.testMode then
DC.Print("test mode ON - simulated raid, chat output disabled") DC.Print("test mode ON - simulated raid, chat output disabled")
elseif DC.demoMode then
-- SetTestMode kept the sim store for demo mode and started no
-- rescan; a "rescanning" message here would be false.
DC.Print("test mode OFF - demo mode still on, still showing the simulated raid (/dc demo to leave)")
else else
DC.Print("test mode OFF - rescanning the real group") DC.Print("test mode OFF - rescanning the real group")
end end
elseif cmd == "demo" then
-- Showcase view: the simulated raid plus a TRANSIENT expectation
-- layer that shows all six school protection columns at once
-- (core/demo.lua). Nothing about it is saved -- unlike /dc test,
-- which persists db.testMode.
if not DC.SetDemoMode then
DC.Print("demo module not loaded")
else
DC.SetDemoMode(not DC.demoMode)
if DC.demoMode then
DC.Print("demo mode ON - simulated raid, all six protection columns shown")
else
DC.Print("demo mode OFF - back to the normal view")
end
end
elseif cmd == "probe" then elseif cmd == "probe" then
-- core/probe.lua owns the probe subcommand grammar: -- core/probe.lua owns the probe subcommand grammar:
-- DC_Probe.HandleSlash(rest) handles "" (battery), -- DC_Probe.HandleSlash(rest) handles "" (battery),
@@ -222,7 +255,7 @@ if CreateFrame then
elseif event == "RAID_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then elseif event == "RAID_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then
local db = DC.db local db = DC.db
if db and not db.testMode and MatrixIsShown() then if db and not DC.SimOwnsStore(db) and MatrixIsShown() then
DC_Scan.Start() DC_Scan.Start()
end end
+16
View File
@@ -850,6 +850,22 @@ function M.classify(player, slotdef, expected, books)
for name in pairs(names) do for name in pairs(names) do
if nameMapsToSlot(name, slotdef.id) then if nameMapsToSlot(name, slotdef.id) then
if not (haveIds and nameHasIds[name]) then if not (haveIds and nameHasIds[name]) then
-- Consumable/class-buff name collision (decision
-- 2026-08-15, data/classbuffs.lua
-- DC.CONSUMABLE_CLASSBUFF_NAME_COLLISION): a
-- name-only match (this branch only runs when no
-- spell ID resolved the buff) can never satisfy a
-- CONSUMABLE slot when the name is also a class buff
-- name -- UNKNOWN, not HAS. One-sided: the class-buff
-- slot side of the same name is unaffected (see the
-- constant's comment for why).
local cdef = DC.CONSUMABLES and DC.CONSUMABLES[name]
local collide = DC.CONSUMABLE_CLASSBUFF_NAME_COLLISION
if cdef and cdef.slot == slotdef.id
and collide and collide[name] then
return S.UNKNOWN,
"name-only, ambiguous with class buff: " .. name
end
local icon = nil local icon = nil
if textures then if textures then
icon = textures[name] icon = textures[name]
+194 -56
View File
@@ -35,6 +35,27 @@
-- (built programmatically below, so id-matched and name-matched -- (built programmatically below, so id-matched and name-matched
-- players are both covered), and every gear entry TABLE carries -- players are both covered), and every gear entry TABLE carries
-- texture + name + quality -- test mode showcases the icon feature. -- texture + name + quality -- test mode showcases the icon feature.
-- * SCHOOL PROTECTION carriers, one per school, ALL by spell ID:
-- Simtank SR (17548), Simsteel FrR (17544), Simsham NR (17546),
-- Simheala HR (7245), Simmage AR (17549) -- plus the seeded FR
-- carriers. Their columns stay hidden under the shipped defaults; demo
-- mode (core/demo.lua) switches all six on at once and these five are
-- what makes that view show HAS cells rather than an empty block. Held
-- by ID on purpose: "Shadow Protection" is ALSO the priest class buff
-- (data/consumables.lua:312), so a name-based carrier would fake an SR
-- fulfillment the moment the column is visible.
-- * v6 SLOT MODEL (17 active default columns of the 22-slot table --
-- the five school protection slots FrR/NR/SR/HR/AR default off and
-- stay hidden) + identification-ladder showcase: the
-- four TANKs run the full KG tank set (green pill across all 17
-- columns); every new column (BL/ZANZA/GAE/SCHOOL/DREAMT/SHARD/ALC)
-- has HAS, MISSING and NOTEXP cells somewhere in the roster. Generic
-- "Well Fed" carriers split three ways for the matrix ladder:
-- effect line resolving to an ICON-carrying item (Simwall Dumplings,
-- Simrogue Squid, Simmage Danonzo's Delight), effect line resolving
-- to an item WITHOUT a sourced icon (Simheala Herbal Salad ->
-- slot-generic icon, tooltip names the item), and no effects table
-- at all (Simsteel and others -> "item unknown" fallback).
-- * DEBUFFS tab: exactly 3 afflicted players -- * DEBUFFS tab: exactly 3 afflicted players
-- -- Simrogue + Simtree share "Resurrection Sickness" (afflicted=2 => -- -- Simrogue + Simtree share "Resurrection Sickness" (afflicted=2 =>
-- first column, count sorting testable), Simfrost carries "Curse of -- first column, count sorting testable), Simfrost carries "Curse of
@@ -191,13 +212,29 @@ end
-- one shared list is safe to reuse across every role and class. -- one shared list is safe to reuse across every role and class.
-- cleanAuras() returns a FRESH table on every call (set() already builds -- cleanAuras() returns a FRESH table on every call (set() already builds
-- a new table each time); the source list itself is read-only. -- a new table each time); the source list itself is read-only.
--
-- SPROT is filled by "Prayer of Shadow Protection" and NEVER by the bare
-- "Shadow Protection": that bare name belongs to BOTH the priest buff
-- (data/classbuffs.lua SPROT) and the lesser SR potion aura
-- (data/consumables.lua:312, spell 7242). These players carry no aura IDs
-- at all (ids = {}), so the model's ID-before-name rule cannot fire
-- (core/model.lua: haveIds == false) and the bare name would satisfy the
-- SR POTION column as well. Live that stays invisible because SR ships
-- default-off -- but demo mode (core/demo.lua) switches SR on for
-- everyone, which is exactly where the false HAS would show up. The
-- Prayer variant maps to SPROT only (classbuffs.lua:51, ids {27683}) and
-- is the raid-wide form anyway, so the class-buff showcase is unchanged
-- while the SR column stays honest.
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
local CLEAN_AURA_NAMES = { local CLEAN_AURA_NAMES = {
"Flask of the Titans", "Well Fed", "Juju Might", "Juju Power", "Flask of the Titans", "Well Fed", "Juju Might", "Juju Power",
"Elixir of the Mongoose", "Greater Arcane Elixir", "Mageblood Potion", "Elixir of the Mongoose", "R.O.I.D.S.", "Spirit of Zanza",
"Elixir of Superior Defense", "Elixir of Fortitude", "Greater Arcane Elixir", "Elixir of Shadow Power", "Dreamtonic",
"Dreamshard Elixir", "Mageblood Potion",
"Elixir of Superior Defense", "Elixir of Fortitude", "Medivh's Merlot",
"Greater Fire Protection", "Mark of the Wild", "Power Word: Fortitude", "Greater Fire Protection", "Mark of the Wild", "Power Word: Fortitude",
"Shadow Protection", "Arcane Intellect", "Soulstone Resurrection", "Prayer of Shadow Protection", "Arcane Intellect",
"Soulstone Resurrection",
} }
local function cleanAuras() local function cleanAuras()
@@ -225,31 +262,59 @@ local ICONS = {
["Juju Power"] = "Interface\\Icons\\INV_Misc_MonsterScales_11", ["Juju Power"] = "Interface\\Icons\\INV_Misc_MonsterScales_11",
["Elixir of Giants"] = "Interface\\Icons\\INV_Potion_61", ["Elixir of Giants"] = "Interface\\Icons\\INV_Potion_61",
["Elixir of the Giants"] = "Interface\\Icons\\INV_Potion_61", ["Elixir of the Giants"] = "Interface\\Icons\\INV_Potion_61",
["Strike of the Scorpok"] = "Interface\\Icons\\INV_Misc_MonsterScales_09",
["Rage of Ages"] = "Interface\\Icons\\INV_Scroll_03",
-- AGI -- AGI
["Elixir of the Mongoose"] = "Interface\\Icons\\INV_Potion_32", ["Elixir of the Mongoose"] = "Interface\\Icons\\INV_Potion_32",
["Elixir of Greater Agility"] = "Interface\\Icons\\INV_Potion_94", ["Elixir of Greater Agility"] = "Interface\\Icons\\INV_Potion_94",
["Greater Agility"] = "Interface\\Icons\\INV_Potion_94", ["Greater Agility"] = "Interface\\Icons\\INV_Potion_94",
-- SP -- BL (aura names + item-alias keys, like the data table)
["Rage of Ages"] = "Interface\\Icons\\INV_Stone_15",
["R.O.I.D.S."] = "Interface\\Icons\\INV_Stone_15",
["Strike of the Scorpok"] = "Interface\\Icons\\INV_Misc_Dust_02",
["Ground Scorpok Assay"] = "Interface\\Icons\\INV_Misc_Dust_02",
["Spirit of Boar"] = "Interface\\Icons\\INV_Drink_12",
["Lung Juice Cocktail"] = "Interface\\Icons\\INV_Drink_12",
["Infallible Mind"] = "Interface\\Icons\\INV_Potion_32",
["Spiritual Domination"] = "Interface\\Icons\\INV_Misc_Food_30",
["Gizzard Gum"] = "Interface\\Icons\\INV_Misc_Food_30",
-- ZANZA
["Spirit of Zanza"] = "Interface\\Icons\\INV_Potion_30",
-- GAE / SCHOOL / DREAMT / SHARD (split out of the old SP column)
["Greater Arcane Elixir"] = "Interface\\Icons\\INV_Potion_25", ["Greater Arcane Elixir"] = "Interface\\Icons\\INV_Potion_25",
["Arcane Elixir"] = "Interface\\Icons\\INV_Potion_30", ["Arcane Elixir"] = "Interface\\Icons\\INV_Potion_30",
["Elixir of Shadow Power"] = "Interface\\Icons\\INV_Potion_46", ["Elixir of Shadow Power"] = "Interface\\Icons\\INV_Potion_46",
["Elixir of Frost Power"] = "Interface\\Icons\\INV_Potion_03",
["Greater Frost Power"] = "Interface\\Icons\\INV_Potion_13", ["Greater Frost Power"] = "Interface\\Icons\\INV_Potion_13",
["Dreamtonic"] = "Interface\\Icons\\INV_Potion_10",
["Dreamshard Elixir"] = "Interface\\Icons\\INV_Potion_12",
-- MP5 (both TWoW name variants share the item icon) -- MP5 (both TWoW name variants share the item icon)
["Mageblood Potion"] = "Interface\\Icons\\INV_Potion_45", ["Mageblood Potion"] = "Interface\\Icons\\INV_Potion_45",
["Mageblood"] = "Interface\\Icons\\INV_Potion_45", ["Mageblood"] = "Interface\\Icons\\INV_Potion_45",
-- ARM -- ARM
["Elixir of Superior Defense"] = "Interface\\Icons\\INV_Potion_66", ["Elixir of Superior Defense"] = "Interface\\Icons\\INV_Potion_66",
["Elixir of Greater Defense"] = "Interface\\Icons\\INV_Potion_43", ["Elixir of Greater Defense"] = "Interface\\Icons\\INV_Potion_43",
-- HP -- HPELX
["Elixir of Fortitude"] = "Interface\\Icons\\INV_Potion_44", ["Elixir of Fortitude"] = "Interface\\Icons\\INV_Potion_44",
["Health II"] = "Interface\\Icons\\INV_Potion_44", ["Health II"] = "Interface\\Icons\\INV_Potion_44",
-- ALC (alcohol column, split out of the old HP column)
["Rumsey Rum Black Label"] = "Interface\\Icons\\INV_Drink_04", ["Rumsey Rum Black Label"] = "Interface\\Icons\\INV_Drink_04",
["Rumsey Rum"] = "Interface\\Icons\\INV_Drink_08", ["Rumsey Rum"] = "Interface\\Icons\\INV_Drink_08",
["Medivh's Merlot"] = "Interface\\Icons\\INV_Drink_Waterskin_05",
["Medivh's Merlot Blue"] = "Interface\\Icons\\INV_Drink_Waterskin_01",
["Medivh's Merlot Blue Label"] = "Interface\\Icons\\INV_Drink_Waterskin_01",
["Kreeg's Stout Beatdown"] = "Interface\\Icons\\INV_Drink_05",
-- FR -- FR
["Greater Fire Protection"] = "Interface\\Icons\\INV_Potion_24", ["Greater Fire Protection"] = "Interface\\Icons\\INV_Potion_24",
["Fire Protection"] = "Interface\\Icons\\INV_Potion_16", ["Fire Protection"] = "Interface\\Icons\\INV_Potion_16",
-- FrR/NR/SR/HR/AR (school protection showcase -- these columns are
-- hidden until their expectation is enabled per role/class, and demo
-- mode switches all six on at once; the carriers hold their potion by
-- SPELL ID, never by name -- "Shadow Protection" is also the priest
-- class buff, so a name-based carrier would fake an SR fulfillment)
["Greater Shadow Protection"] = "Interface\\Icons\\INV_Potion_23",
["Greater Frost Protection"] = "Interface\\Icons\\INV_Potion_20",
["Greater Nature Protection"] = "Interface\\Icons\\INV_Potion_22",
["Holy Protection"] = "Interface\\Icons\\INV_Potion_09",
["Greater Arcane Protection"] = "Interface\\Icons\\INV_Potion_83",
-- class buffs -- class buffs
["Arcane Intellect"] = "Interface\\Icons\\Spell_Holy_MagicalSentry", ["Arcane Intellect"] = "Interface\\Icons\\Spell_Holy_MagicalSentry",
["Arcane Brilliance"] = "Interface\\Icons\\Spell_Holy_ArcaneIntellect", ["Arcane Brilliance"] = "Interface\\Icons\\Spell_Holy_ArcaneIntellect",
@@ -257,7 +322,17 @@ local ICONS = {
["Gift of the Wild"] = "Interface\\Icons\\Spell_Nature_GiftoftheWild", ["Gift of the Wild"] = "Interface\\Icons\\Spell_Nature_GiftoftheWild",
["Power Word: Fortitude"] = "Interface\\Icons\\Spell_Holy_WordFortitude", ["Power Word: Fortitude"] = "Interface\\Icons\\Spell_Holy_WordFortitude",
["Prayer of Fortitude"] = "Interface\\Icons\\Spell_Holy_PrayerOfFortitude", ["Prayer of Fortitude"] = "Interface\\Icons\\Spell_Holy_PrayerOfFortitude",
-- the bare name stays: ID-bearing SPROT carriers resolve through
-- idToName (976/10957/10958 -> "Shadow Protection") and texturesFor
-- looks the icon up under THAT name. Name-only players use the Prayer
-- variant instead (see CLEAN_AURA_NAMES).
-- both spellings share the vanilla Shadow Protection art (the same
-- alias convention the AP/STR/MP5 entries above use). Deliberately NOT
-- a "Spell_Holy_PrayerofShadowProtection" path: the Prayer rank is not
-- a stock 1.12 spell, so that texture cannot be assumed to exist in
-- the client, and a missing path renders an empty icon.
["Shadow Protection"] = "Interface\\Icons\\Spell_Shadow_AntiShadow", ["Shadow Protection"] = "Interface\\Icons\\Spell_Shadow_AntiShadow",
["Prayer of Shadow Protection"] = "Interface\\Icons\\Spell_Shadow_AntiShadow",
["Soulstone Resurrection"] = "Interface\\Icons\\Spell_Shadow_SoulGem", ["Soulstone Resurrection"] = "Interface\\Icons\\Spell_Shadow_SoulGem",
} }
@@ -335,9 +410,10 @@ local HIT_MAIN_SUB = {
DRUID = "Staves", DRUID = "Staves",
} }
-- All 16 equipment slots the scan reads (11 enchant + neck + 2 rings + -- All 17 equipment slots the scan reads (12 armor enchant, WAIST
-- 2 trinket invSlots). -- included -- 2026-08-30 owner decision, WAIST is worn equipment like
local INV_SLOTS = { 1, 2, 3, 15, 5, 9, 10, 7, 8, 16, 17, 18, 11, 12, 13, 14 } -- any other slot now -- + neck + 2 rings + 2 trinket invSlots).
local INV_SLOTS = { 1, 2, 3, 15, 5, 6, 9, 10, 7, 8, 16, 17, 18, 11, 12, 13, 14 }
-- Per-slot base item cosmetics: EVERY gear entry table carries texture, -- Per-slot base item cosmetics: EVERY gear entry table carries texture,
-- name and quality (the UI renders item icons). -- name and quality (the UI renders item icons).
@@ -347,6 +423,7 @@ local SLOT_BASE = {
[3] = { name = "Simulated Spaulders", quality = 3, texture = "Interface\\Icons\\INV_Shoulder_01" }, [3] = { name = "Simulated Spaulders", quality = 3, texture = "Interface\\Icons\\INV_Shoulder_01" },
[15] = { name = "Simulated Drape", quality = 3, texture = "Interface\\Icons\\INV_Misc_Cape_16" }, [15] = { name = "Simulated Drape", quality = 3, texture = "Interface\\Icons\\INV_Misc_Cape_16" },
[5] = { name = "Simulated Breastplate", quality = 4, texture = "Interface\\Icons\\INV_Chest_Plate04" }, [5] = { name = "Simulated Breastplate", quality = 4, texture = "Interface\\Icons\\INV_Chest_Plate04" },
[6] = { name = "Simulated Girdle", quality = 3, texture = "Interface\\Icons\\INV_Belt_03" },
[9] = { name = "Simulated Bracers", quality = 3, texture = "Interface\\Icons\\INV_Bracer_13" }, [9] = { name = "Simulated Bracers", quality = 3, texture = "Interface\\Icons\\INV_Bracer_13" },
[10] = { name = "Simulated Gauntlets", quality = 3, texture = "Interface\\Icons\\INV_Gauntlets_28" }, [10] = { name = "Simulated Gauntlets", quality = 3, texture = "Interface\\Icons\\INV_Gauntlets_28" },
[7] = { name = "Simulated Legplates", quality = 4, texture = "Interface\\Icons\\INV_Pants_04" }, [7] = { name = "Simulated Legplates", quality = 4, texture = "Interface\\Icons\\INV_Pants_04" },
@@ -365,6 +442,9 @@ local SLOT_BASE = {
-- SLOT_BASE. Table overrides are MERGED with the slot cosmetics (missing -- SLOT_BASE. Table overrides are MERGED with the slot cosmetics (missing
-- name/quality/texture filled in from SLOT_BASE, everything else -- id, -- name/quality/texture filled in from SLOT_BASE, everything else -- id,
-- enchant, subType, own cosmetics -- kept); "EMPTY" stays "EMPTY". -- enchant, subType, own cosmetics -- kept); "EMPTY" stays "EMPTY".
-- WAIST (6) is treated like the plain armor slots here -- always
-- enchanted in the base gear, expected by default for every role -- NOT
-- grouped with the enchant-free neck/rings/trinkets below.
-- Neck/rings (2/11/12) default to enchant=0 like trinkets -- but unlike -- Neck/rings (2/11/12) default to enchant=0 like trinkets -- but unlike
-- trinkets they ARE enchantable on this server (data/enchants.lua, kind -- trinkets they ARE enchantable on this server (data/enchants.lua, kind
-- "ench"), so Simtank's override below demonstrates a real neck enchant -- "ench"), so Simtank's override below demonstrates a real neck enchant
@@ -438,10 +518,15 @@ function DC_Sim.buildStore()
add{ name = "Simtank", class = "WARRIOR", online = true, add{ name = "Simtank", class = "WARRIOR", online = true,
guid = "0xF530000000000001", subgroup = 1, guid = "0xF530000000000001", subgroup = 1,
aurasRead = true, aurasRead = true,
auras = { names = {}, ids = set({ 17626, 15852, 16323, 17538, auras = { names = {}, ids = set({ 17626, 15852, 17038, 16323,
11348, 3593, 17543, 9885, 10938, 10958 }) }, 17538, 10667, 24382, 11348, 3593, 25804, 17543, 17548,
-- FLASK Titans, FOOD Chili, STR JujuPower, AGI Mongoose, ARM SupDef, 9885, 10938, 10958 }) },
-- HP Fortitude, FR GFPP, MOTW r7, PWF r6, SPROT r3 -- FLASK Titans, FOOD Chili, AP Firewater, STR JujuPower,
-- AGI Mongoose, BL RageOfAges(ROIDS), ZANZA, ARM SupDef,
-- HPELX Fortitude, ALC RumseyBlack, FR GFPP, SR GSPP (shadow-boss
-- prep -- NOTEXP/hidden until the SR expectation is enabled, then
-- the column lights up green for him), MOTW r7, PWF r6,
-- SPROT r3 -- the full KG tank set, 0 gaps (green pill)
weaponMain = true, weaponMainName = "Dense Sharpening Stone", weaponMain = true, weaponMainName = "Dense Sharpening Stone",
gearRead = true, gearRead = true,
resistsRead = true, resists = { [2] = 315 }, resistsRead = true, resists = { [2] = 315 },
@@ -476,10 +561,16 @@ function DC_Sim.buildStore()
guid = "0xF530000000000002", subgroup = 1, guid = "0xF530000000000002", subgroup = 1,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Flask of the Titans", "Well Fed", auras = { names = set({ "Flask of the Titans", "Well Fed",
"Juju Power", "Elixir of the Mongoose", "Winterfall Firewater", "Juju Power", "Elixir of the Mongoose",
"R.O.I.D.S.", "Spirit of Zanza",
"Elixir of Superior Defense", "Elixir of Fortitude", "Elixir of Superior Defense", "Elixir of Fortitude",
"Greater Fire Protection", "Mark of the Wild", "Medivh's Merlot", "Greater Fire Protection",
"Power Word: Fortitude", "Shadow Protection" }), ids = {} }, "Mark of the Wild", "Power Word: Fortitude",
-- Prayer variant, not the bare name: see CLEAN_AURA_NAMES
"Prayer of Shadow Protection" }), ids = {},
-- effect line for the generic "Well Fed": the identification
-- ladder resolves it to Smoked Desert Dumplings in the matrix
effects = { ["Well Fed"] = "Increases Strength by 20." } },
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
resistsRead = true, resists = { [2] = 240 }, resistsRead = true, resists = { [2] = 240 },
-- hit: the BELOW-cap showcase -- a human swordsman, so the racial -- hit: the BELOW-cap showcase -- a human swordsman, so the racial
@@ -505,10 +596,15 @@ function DC_Sim.buildStore()
guid = "0xF530000000000003", subgroup = 1, guid = "0xF530000000000003", subgroup = 1,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed" }), auras = { names = set({ "Well Fed" }),
ids = set({ 17629, 11405, 11334, 11349, 25804, 7233, ids = set({ 17629, 16329, 11405, 11334, 10669, 24382,
21849, 21562, 10957 }) }, 11349, 3593, 25804, 7233, 17544, 21849, 21562, 10957 }) },
-- FLASK ChromRes, STR Giants, AGI GreaterAgi, ARM GreaterDef, -- FLASK ChromRes, AP JujuMight, STR Giants, AGI GreaterAgi,
-- HP RumseyBlack, FR FireProt, MOTW Gift, PWF Prayer, SPROT r2 -- BL Scorpok, ZANZA, ARM GreaterDef, HPELX Fortitude(HealthII),
-- ALC RumseyBlack, FR FireProt, FrR GFrPP (17544 -- the frost
-- carrier of the six-school demo view, hidden by default),
-- MOTW Gift, PWF Prayer, SPROT r2.
-- NO effects table: his "Well Fed" stays unresolvable -- the
-- matrix ladder's slot-generic-icon floor ("item unknown")
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
resistsRead = true, resists = { [2] = 180 }, resistsRead = true, resists = { [2] = 180 },
-- hit: the BOOK showcase -- an orc axeman (racial +5 => cap 7.0); -- hit: the BOOK showcase -- an orc axeman (racial +5 => cap 7.0);
@@ -553,9 +649,13 @@ function DC_Sim.buildStore()
guid = "0xF530000000000005", subgroup = 2, guid = "0xF530000000000005", subgroup = 2,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed" }), auras = { names = set({ "Well Fed" }),
ids = set({ 16329, 17538, 16323, 3593, 9885, 10938 }) }, ids = set({ 17626, 16329, 17538, 16323, 10692, 24382,
-- AP JujuMight, AGI Mongoose, STR JujuPower, HP Fortitude, 3593, 9885, 10938 }),
-- MOTW r7, PWF r6 -- +10 Agility resolves to Grilled Squid through the ladder
effects = { ["Well Fed"] = "Increases Agility by 10." } },
-- FLASK Titans, AP JujuMight, AGI Mongoose, STR JujuPower,
-- BL Cortex(InfallibleMind), ZANZA, HPELX Fortitude (NOTEXP for
-- MELEE, harmless), MOTW r7, PWF r6
debuffs = debuffsOf({ "Resurrection Sickness" }, { 15007 }), debuffs = debuffsOf({ "Resurrection Sickness" }, { 15007 }),
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
-- hit: the DUAL-WIELD showcase -- two weapon subtypes, so the cell -- hit: the DUAL-WIELD showcase -- two weapon subtypes, so the cell
@@ -612,10 +712,12 @@ function DC_Sim.buildStore()
add{ name = "Simstab", class = "ROGUE", online = true, add{ name = "Simstab", class = "ROGUE", online = true,
guid = "0xF530000000000007", subgroup = 2, guid = "0xF530000000000007", subgroup = 2,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed", "Juju Might", "Juju Power", auras = { names = set({ "Flask of the Titans", "Well Fed",
"Elixir of Greater Agility", "Elixir of Fortitude", "Juju Might", "Juju Power", "Elixir of Greater Agility",
"Fire Protection", "Mark of the Wild", "Lung Juice Cocktail", "Spirit of Zanza",
"Power Word: Fortitude", "Shadow Protection" }), ids = {} }, "Elixir of Fortitude", "Fire Protection", "Mark of the Wild",
"Power Word: Fortitude",
"Prayer of Shadow Protection" }), ids = {} },
weaponMain = false, gearRead = true, weaponMain = false, gearRead = true,
gear = gearWith({ gear = gearWith({
[9] = { id = 5169, enchant = 0 }, -- wrist enchant missing [9] = { id = 5169, enchant = 0 }, -- wrist enchant missing
@@ -650,10 +752,15 @@ function DC_Sim.buildStore()
guid = "0xF530000000000009", subgroup = 2, guid = "0xF530000000000009", subgroup = 2,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed" }), auras = { names = set({ "Well Fed" }),
ids = set({ 17038, 11405, 17538, 25804, 17543, ids = set({ 17626, 17038, 11405, 17538, 10693, 24382,
9885, 10938, 10958 }) }, 25804, 17543, 17546, 9885, 10938, 10958 }) },
-- AP Firewater, STR Giants, AGI Mongoose, HP RumseyBlack, FR GFPP, -- FLASK Titans, AP Firewater, STR Giants, AGI Mongoose,
-- MOTW r7, PWF r6, SPROT r3 -- BL Gizzard(SpiritualDomination), ZANZA, ALC RumseyBlack
-- (NOTEXP for MELEE, harmless), FR GFPP, NR GNPP (17546 -- the
-- nature carrier of the six-school demo view), MOTW r7, PWF r6,
-- SPROT r3 (the priest buff by ID -- he holds no SR potion, which
-- is exactly the name-collision case the demo view must not
-- mistake for a fulfilled SR column)
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
gear = gearWith({ gear = gearWith({
[17] = { id = 5197, enchant = 917, subType = "Shields", quality = 3 }, [17] = { id = 5197, enchant = 917, subType = "Shields", quality = 3 },
@@ -694,9 +801,12 @@ function DC_Sim.buildStore()
guid = "0xF53000000000000B", subgroup = 3, distance = 31, guid = "0xF53000000000000B", subgroup = 3, distance = 31,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed", "Juju Might", auras = { names = set({ "Well Fed", "Juju Might",
"Elixir of Greater Agility", "Rumsey Rum", "Elixir of Greater Agility", "Ground Scorpok Assay",
"Spirit of Zanza", "Rumsey Rum",
"Greater Fire Protection", "Mark of the Wild", "Greater Fire Protection", "Mark of the Wild",
"Power Word: Fortitude", "Shadow Protection" }), ids = {} }, "Power Word: Fortitude",
"Prayer of Shadow Protection" }), ids = {} },
-- ALC (Rumsey Rum) is NOTEXP for RANGED -- harmless extra
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
-- hit: the RANGED showcase -- a real bow, so the ranged column is -- hit: the RANGED showcase -- a real bow, so the ranged column is
-- a question at all; 8 meets its 8.0 cap (a dwarf's racial is on -- a question at all; 8 meets its 8.0 cap (a dwarf's racial is on
@@ -727,8 +837,15 @@ function DC_Sim.buildStore()
guid = "0xF53000000000000D", subgroup = 4, guid = "0xF53000000000000D", subgroup = 4,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed" }), auras = { names = set({ "Well Fed" }),
ids = set({ 17628, 23028, 17539, 24363, 9885, 10938, 10958 }) }, ids = set({ 17628, 23028, 17539, 21920, 45489, 45427, 24382,
-- FLASK Supreme, AI Brilliance, SP GAE, MP5 Mageblood, MOTW, PWF, SPROT 24363, 17549, 9885, 10938, 10958 }),
-- +22 spell damage -> Danonzo's Tel'Abim Delight (ladder)
effects = { ["Well Fed"] =
"Increases spell damage by up to 22." } },
-- FLASK Supreme, AI Brilliance, GAE, SCHOOL FrostPower,
-- DREAMT, SHARD, ZANZA, MP5 Mageblood, AR GAPP (17549 -- the
-- arcane carrier of the six-school demo view), MOTW, PWF, SPROT --
-- the full KG caster set except FR (her designed gap)
weaponMain = true, weaponMainName = "Brilliant Wizard Oil", weaponMain = true, weaponMainName = "Brilliant Wizard Oil",
gearRead = true, gearRead = true,
resistsRead = true, resists = { [4] = 85, [6] = 45 }, resistsRead = true, resists = { [4] = 85, [6] = 45 },
@@ -795,7 +912,9 @@ function DC_Sim.buildStore()
guid = "0xF530000000000010", subgroup = 5, guid = "0xF530000000000010", subgroup = 5,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed", "Flask of Supreme Power", auras = { names = set({ "Well Fed", "Flask of Supreme Power",
"Greater Arcane Elixir", "Mageblood", "Fire Protection", "Greater Arcane Elixir", "Elixir of Shadow Power",
"Dreamtonic", "Dreamshard Elixir", "Spirit of Zanza",
"Mageblood", "Fire Protection",
"Arcane Intellect", "Mark of the Wild", "Arcane Intellect", "Mark of the Wild",
"Power Word: Fortitude" }), "Power Word: Fortitude" }),
ids = {} }, ids = {} },
@@ -838,10 +957,12 @@ function DC_Sim.buildStore()
guid = "0xF530000000000012", subgroup = 5, guid = "0xF530000000000012", subgroup = 5,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Shadowform", "Flask of Supreme Power", auras = { names = set({ "Shadowform", "Flask of Supreme Power",
"Well Fed", "Elixir of Shadow Power", "Mageblood Potion", "Well Fed", "Greater Arcane Elixir", "Elixir of Shadow Power",
"Dreamtonic", "Dreamshard Elixir", "Spirit of Zanza",
"Mageblood Potion",
"Greater Fire Protection", "Arcane Intellect", "Greater Fire Protection", "Arcane Intellect",
"Mark of the Wild", "Power Word: Fortitude", "Mark of the Wild", "Power Word: Fortitude",
"Shadow Protection" }), ids = {} }, "Prayer of Shadow Protection" }), ids = {} },
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
gear = gearWith({ gear = gearWith({
[18] = { id = 5238, enchant = 0, subType = "Wands" }, [18] = { id = 5238, enchant = 0, subType = "Wands" },
@@ -868,10 +989,21 @@ function DC_Sim.buildStore()
guid = "0xF530000000000014", subgroup = 6, guid = "0xF530000000000014", subgroup = 6,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed" }), auras = { names = set({ "Well Fed" }),
ids = set({ 17627, 20765, 24363, 3593, 7233, ids = set({ 17627, 20765, 24363, 45427, 24382, 22790,
10157, 9885, 10938, 10958 }) }, 3593, 7233, 7245, 10157, 9885, 10938, 10958 }),
-- FLASK Wisdom, SOUL r5, MP5 Mageblood, HP Fortitude, FR FireProt, -- mid-ladder showcase: Le Fishe Au Chocolat resolves by its
-- AI r5, MOTW r7, PWF r6, SPROT r3 -- dodge effect line but has NO sourced icon in the data (icon
-- is a questionmark placeholder even upstream) -> the matrix
-- shows the slot-generic FOOD icon while the tooltip still
-- names the item. (Herbal Salad carried this showcase until
-- its icon was sourced on 2026-08-11.)
effects = { ["Well Fed"] =
"Increases your chance to dodge by 1% and Defense by 4." } },
-- FLASK Wisdom, SOUL r5, MP5 Mageblood, SHARD, ZANZA,
-- ALC Kreeg's, HPELX Fortitude (NOTEXP for HEALER, harmless),
-- FR FireProt, HR HPP (7245 -- the holy carrier of the six-school
-- demo view; no Greater tier exists for holy), AI r5, MOTW r7,
-- PWF r6, SPROT r3
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
gear = gearWith({ gear = gearWith({
[17] = { id = 5257, enchant = 0, subType = "Miscellaneous", [17] = { id = 5257, enchant = 0, subType = "Miscellaneous",
@@ -895,7 +1027,8 @@ function DC_Sim.buildStore()
aurasRead = true, aurasRead = true,
auras = { names = set({ "Flask of Distilled Wisdom", "Well Fed", auras = { names = set({ "Flask of Distilled Wisdom", "Well Fed",
"Mageblood", "Arcane Intellect", "Mark of the Wild", "Mageblood", "Arcane Intellect", "Mark of the Wild",
"Power Word: Fortitude", "Shadow Protection" }), ids = {} }, "Power Word: Fortitude",
"Prayer of Shadow Protection" }), ids = {} },
debuffs = debuffsOf({ "Resurrection Sickness" }, { 15007 }), debuffs = debuffsOf({ "Resurrection Sickness" }, { 15007 }),
weaponMain = false, gearRead = true, weaponMain = false, gearRead = true,
gear = gearWith({ gear = gearWith({
@@ -908,9 +1041,10 @@ function DC_Sim.buildStore()
guid = "0xF530000000000017", subgroup = 7, guid = "0xF530000000000017", subgroup = 7,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed" }), auras = { names = set({ "Well Fed" }),
ids = set({ 17627, 24363, 25804, 7233, 10156, 9885, 10938 }) }, ids = set({ 17627, 24363, 45427, 24382, 25804, 7233,
-- FLASK Wisdom, MP5 Mageblood, HP RumseyBlack, FR FireProt, 10156, 9885, 10938 }) },
-- AI r4, MOTW r7, PWF r6 -- FLASK Wisdom, MP5 Mageblood, SHARD, ZANZA, ALC RumseyBlack,
-- FR FireProt, AI r4, MOTW r7, PWF r6
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
gear = gearWith({ gear = gearWith({
[17] = { id = 5277, enchant = 917, subType = "Shields", quality = 3 }, [17] = { id = 5277, enchant = 917, subType = "Shields", quality = 3 },
@@ -923,10 +1057,12 @@ function DC_Sim.buildStore()
guid = "0xF530000000000018", subgroup = 7, guid = "0xF530000000000018", subgroup = 7,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Flask of Distilled Wisdom", "Well Fed", auras = { names = set({ "Flask of Distilled Wisdom", "Well Fed",
"Mageblood Potion", "Elixir of Fortitude", "Mageblood Potion", "Dreamshard Elixir", "Spirit of Zanza",
"Kreeg's Stout Beatdown", "Elixir of Fortitude",
"Greater Fire Protection", "Arcane Intellect", "Greater Fire Protection", "Arcane Intellect",
"Mark of the Wild", "Power Word: Fortitude", "Mark of the Wild", "Power Word: Fortitude",
"Shadow Protection", "Soulstone Resurrection" }), ids = {} }, "Prayer of Shadow Protection",
"Soulstone Resurrection" }), ids = {} },
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
gear = gearWith({ gear = gearWith({
[5] = { id = 5285, enchant = 0 }, [5] = { id = 5285, enchant = 0 },
@@ -935,16 +1071,18 @@ function DC_Sim.buildStore()
[18] = "EMPTY", [18] = "EMPTY",
}) } }) }
-- Simglow: HP via the id-less "Rumsey Rum" name entry (spellID 0) WHILE -- Simglow: ALC via the id-less "Medivh's Merlot Blue" name entry
-- ids are present -- exercises exactly the model rule that id-less -- (spellID 0) WHILE ids are present -- exercises exactly the model
-- names still count for players with ids. SPROT missing, EMPTY ranged. -- rule that id-less names still count for players with ids.
-- SPROT missing, EMPTY ranged.
add{ name = "Simglow", class = "PALADIN", online = true, add{ name = "Simglow", class = "PALADIN", online = true,
guid = "0xF530000000000019", subgroup = 7, guid = "0xF530000000000019", subgroup = 7,
aurasRead = true, aurasRead = true,
auras = { names = set({ "Well Fed", "Rumsey Rum" }), auras = { names = set({ "Well Fed", "Medivh's Merlot Blue" }),
ids = set({ 17627, 21850, 21564, 24363, 7233, 1461, 20762 }) }, ids = set({ 17627, 21850, 21564, 24363, 45427, 24382,
-- FLASK Wisdom, MOTW Gift, PWF Prayer, MP5 Mageblood, FR FireProt, 7233, 1461, 20762 }) },
-- AI r3, SOUL r2 -- FLASK Wisdom, MOTW Gift, PWF Prayer, MP5 Mageblood, SHARD,
-- ZANZA, FR FireProt, AI r3, SOUL r2
weaponMain = true, gearRead = true, weaponMain = true, gearRead = true,
gear = gearWith({ gear = gearWith({
[17] = { id = 5297, enchant = 917, subType = "Shields", quality = 3 }, [17] = { id = 5297, enchant = 917, subType = "Shields", quality = 3 },
+34
View File
@@ -66,3 +66,37 @@ for name, def in pairs(DC.CLASSBUFFS) do
DC.SPELL_TO_SLOT[def.ids[i]] = def.slot DC.SPELL_TO_SLOT[def.ids[i]] = def.slot
end end
end end
-- ------------------------------------------------------------------
-- Name-only aura collisions with CONSUMABLE slots (decision 2026-08-15):
-- a name-only aura match (no spell ID at all for that scan) must NEVER
-- satisfy a CONSUMABLE slot when the matched display name is also a class
-- buff name -- the scan cannot tell the two apart, and defaulting to HAS
-- would hide a missing consumable behind someone else's class buff. Such
-- a slot classifies UNKNOWN instead (core/model.lua's aura name-fallback
-- branch consults this table; core/const.lua's Iron Rule: UNKNOWN is
-- never collapsed into HAS or MISSING).
--
-- Scope is deliberately ONE-SIDED: only the CONSUMABLE slot side of the
-- collision is affected. The class-buff slot (e.g. SPROT) keeps the
-- existing name-fallback behavior unchanged -- the ambiguity here means
-- "we can't prove the CONSUMABLE was used", not "we can't prove the class
-- buff is missing". Do not widen this into "name-only never counts"; that
-- would make every ID-less scan MISSING/UNKNOWN across the whole matrix.
--
-- Keyed by buff display name (a key shared by DC.CONSUMABLES and
-- DC.CLASSBUFFS). test_data.lua asserts every entry here is a key in BOTH
-- tables, so a stale or speculative entry cannot ship silently.
-- "Shadow Protection" -- DC.CONSUMABLES["Shadow Protection"] (SR slot,
-- Shadow Protection Potion, spell 7242, data/consumables.lua) vs.
-- DC.CLASSBUFFS["Shadow Protection"] above (SPROT slot, priest buff,
-- ids 976/10957/10958) -- identical display name, already flagged as a
-- collision at both definitions and wowhead-confirmed 2026-08-11 (see
-- the SR entry's comment in data/consumables.lua). This is the ONLY
-- name shared between DC.CONSUMABLES and DC.CLASSBUFFS (checked against
-- every key in both tables) -- no other entry is added without the same
-- level of evidence.
-- ------------------------------------------------------------------
DC.CONSUMABLE_CLASSBUFF_NAME_COLLISION = {
["Shadow Protection"] = true,
}
+317 -80
View File
@@ -1,6 +1,65 @@
-- DopingControl data/consumables.lua -- DopingControl data/consumables.lua
-- Consumables tab: slot definitions + buff->slot table + spellID->slot map. -- Consumables tab: slot definitions + buff->entry table + spellID->slot map
-- + the pure item-identification ladder (DC.ResolveItem).
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline. -- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
--
-- SLOT MODEL (v6, 22 slots -- the 17-slot base design plus the five school
-- protection-potion columns FrR/NR/SR/HR/AR added 2026-08-11):
-- One column = one thing that can be active independently of every other
-- column; items competing INSIDE a column are the "pick one" alternatives.
-- Source: community-sourced TurtleWoW 1.17.2 consumable-stacking rules,
-- trust external -- vanilla 1.12 slot rules, no TBC
-- Battle/Guardian categories. The old 11-slot model conflated four
-- independent slots into SP (GAE / school elixir / Dreamtonic / Dreamshard
-- are simultaneously active in the caster set) and two into HP (Fortitude
-- + stamina alcohol stack), and lacked BL/ZANZA entirely.
--
-- Deliberate compromises (documented, not hidden):
-- * ALC is ONE column although different alcohol types stack (Merlot +
-- Rumsey Black Label at once; same types do not): modelled as one
-- column showing the strongest active, detail in tooltip. The
-- slot-collision tests exempt exactly this column.
-- * Gift of Arthas and Juju Flurry get NO column (defense elixirs all
-- stack; Flurry lasts 20 s).
-- * UNVERIFIED gaps (community source does not cover them; marked
-- `unverified = true` on the slot/entries, upgrade via dev/raiddump
-- evidence next raid): (1) WPN main-hand/off-hand coexistence (oil on
-- MH + stone on OH), (2) protection-potion slot rules (all six school
-- columns FR/FrR/NR/SR/HR/AR -- cross-school stacking is NOT
-- evidenced, so each school gets its own column but every one stays
-- flagged unverified), (3) world-buff interactions (no world-buff
-- columns at all).
--
-- Entry fields (DC.CONSUMABLES[buffName]):
-- slot : slot id (column) the buff fills
-- spellID : aura spell id; 0 = "match by name only" (no known ID)
-- item : the consumable's display name (whisper/tooltip/cell)
-- icon : BARE icon texture name (no "Interface\Icons\" prefix, no
-- extension -- prefix is added at render time); nil when the
-- icon could not be sourced (the matrix fallback ladder
-- handles nil: slot-generic icon, then the green check)
-- discrim : optional Lua 5.0 string.find pattern matched against the
-- buff tooltip's EFFECT line (scan/aura.lua auras.effects) --
-- only where the buff NAME is generic
-- variants : optional ARRAY of { item, icon, discrim } for generic buff
-- names ("Well Fed") that many different items produce; the
-- identification ladder walks them in order, FIRST match wins
-- unverified : optional true -- see the honesty gaps above
--
-- Icon provenance (2026-08-11, never guessed): wowhead classic tooltip
-- API for the vanilla items, octowow.st/db + wowauctions for TurtleWoW
-- customs; icon = nil where no source confirmed one. Two lookalike
-- collisions are GENUINE, not typos: Dragonbreath Chili and Danonzo's
-- Delight share inv_drink_17 with Nightfin Soup; Gordok Green Grog
-- shares inv_drink_03 with Rumsey Rum; Cerebral Cortex Compound shares
-- inv_potion_32 with the Mongoose elixir.
--
-- Separator decision (once for all data files): items strings are
-- tooltip/FontString-only, so they may use the middle-dot list separator
-- "\194\183" (UTF-8 middle dot renders fine in own FontStrings; the group
-- headers already use the same glyph). Em dashes stay ASCII "-" (that
-- glyph is unproven in the 1.12 font). SENT chat lines remain pure ASCII
-- -- they never use these strings.
DopingControl = DopingControl or {} DopingControl = DopingControl or {}
local DC = DopingControl local DC = DopingControl
@@ -8,116 +67,294 @@ local DC = DopingControl
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
-- Slot definitions (array, order = column order). -- Slot definitions (array, order = column order).
-- Fields: id (stable key, used by DEFAULT_EXPECT / model / UI), -- Fields: id (stable key, used by DEFAULT_EXPECT / model / UI),
-- label (column tile abbrev), sub (tile sub-label), full (long name, -- label (column tile abbrev), sub (tile sub-label), full (long
-- used in whisper/report), kind ("aura" | "weapon"), -- name, used in whisper/report), kind ("aura" | "weapon"),
-- items (tooltip example text, optional). -- items (tooltip example text, optional), unverified (optional,
-- Separator decision (once for all data files): items strings are -- see header),
-- tooltip/FontString-only, so they may use the middle-dot list separator -- icon (BARE slot-generic icon texture name -- same bare-name
-- "\194\183" (UTF-8 middle dot renders fine in own -- rule as the entry icons above; the matrix HAS-cell fallback
-- FontStrings; the group headers already use the same glyph). Em dashes -- ladder renders it when the ITEM behind a buff cannot be
-- stay ASCII "-" (that glyph is unproven in the 1.12 font). SENT -- resolved: resolved item icon -> this slot icon -> green check.
-- chat lines remain pure ASCII -- they never use these -- A representative member item's icon by design -- the cell must
-- strings. -- read as "this column", not as a question mark).
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
DC.SLOTS_CONSUMABLES = { DC.SLOTS_CONSUMABLES = {
{ id = "FLASK", label = "FLK", sub = "Flask", full = "Flask", kind = "aura", { id = "FLASK", label = "FLK", sub = "Flask", full = "Flask", kind = "aura",
icon = "inv_potion_62",
items = "Flask of the Titans \194\183 Distilled Wisdom \194\183 Supreme Power" }, items = "Flask of the Titans \194\183 Distilled Wisdom \194\183 Supreme Power" },
{ id = "FOOD", label = "FOD", sub = "Food", full = "Food buff", kind = "aura", { id = "FOOD", label = "FOD", sub = "Food", full = "Food buff", kind = "aura",
items = "Well Fed - e.g. Grilled Squid \194\183 Blessed Sunfruit" }, icon = "inv_misc_food_15",
{ id = "AP", label = "AP", sub = "Attack", full = "Attack power", kind = "aura", items = "one food buff at a time - e.g. Smoked Desert Dumplings \194\183 Danonzo's Tel'Abim foods" },
{ id = "AP", label = "AP", sub = "Attack", full = "Attack power", kind = "aura",
icon = "inv_potion_92",
items = "Winterfall Firewater \194\183 Juju Might" }, items = "Winterfall Firewater \194\183 Juju Might" },
{ id = "STR", label = "STR", sub = "Str.", full = "Strength", kind = "aura", { id = "STR", label = "STR", sub = "Str.", full = "Strength", kind = "aura",
icon = "inv_potion_61",
items = "Juju Power \194\183 Elixir of Giants" }, items = "Juju Power \194\183 Elixir of Giants" },
{ id = "AGI", label = "AGI", sub = "Agi.", full = "Agility", kind = "aura", { id = "AGI", label = "AGI", sub = "Agi.", full = "Agility", kind = "aura",
items = "Elixir of the Mongoose" }, icon = "inv_potion_32",
{ id = "SP", label = "SP", sub = "Spell", full = "Spell power", kind = "aura", items = "Elixir of the Mongoose - free-stacker, own column" },
items = "Greater Arcane Elixir \194\183 Shadow Power \194\183 Firepower" }, { id = "BL", label = "BL", sub = "Blasted", full = "Blasted Lands buff", kind = "aura",
{ id = "MP5", label = "MP5", sub = "Mana", full = "Mana regen", kind = "aura", icon = "inv_stone_15",
items = "Mageblood Potion \194\183 Nightfin Soup" }, items = "R.O.I.D.S. \194\183 Ground Scorpok Assay \194\183 Lung Juice Cocktail \194\183 Cortex Compound \194\183 Gizzard Gum" },
{ id = "ARM", label = "ARM", sub = "Armor", full = "Armor", kind = "aura", { id = "ZANZA", label = "ZAN", sub = "Zanza", full = "Spirit of Zanza", kind = "aura",
icon = "inv_potion_30",
items = "Spirit of Zanza - free-stacker, one Zanza effect at a time" },
{ id = "GAE", label = "GAE", sub = "Arcane", full = "Greater Arcane Elixir", kind = "aura",
icon = "inv_potion_25",
items = "Greater Arcane Elixir \194\183 Arcane Elixir" },
{ id = "SCHOOL", label = "SCH", sub = "School", full = "School elixir", kind = "aura",
icon = "inv_potion_46",
items = "Shadow Power \194\183 Frost Power \194\183 Greater Firepower - one per school slot" },
{ id = "DREAMT", label = "DRT", sub = "Dreamt.", full = "Dreamtonic", kind = "aura",
icon = "inv_potion_10",
items = "Dreamtonic (TurtleWoW custom)" },
{ id = "SHARD", label = "SHD", sub = "Shard", full = "Dreamshard Elixir", kind = "aura",
icon = "inv_potion_12",
items = "Dreamshard Elixir (TurtleWoW custom)" },
{ id = "MP5", label = "MP5", sub = "Mana", full = "Mana regen", kind = "aura",
icon = "inv_potion_45",
items = "Mageblood Potion - Nightfin Soup is a FOOD buff, not this column" },
{ id = "ARM", label = "ARM", sub = "Armor", full = "Armor", kind = "aura",
icon = "inv_potion_66",
items = "Elixir of Superior Defense" }, items = "Elixir of Superior Defense" },
{ id = "HP", label = "HP", sub = "Stam.", full = "Stamina/HP", kind = "aura", { id = "HPELX", label = "FRT", sub = "Fort.", full = "Elixir of Fortitude", kind = "aura",
items = "Elixir of Fortitude \194\183 Rumsey Rum Black Label" }, icon = "inv_potion_43",
{ id = "FR", label = "FR", sub = "fire", full = "Fire protection", kind = "aura", items = "Elixir of Fortitude - stacks with the alcohol column" },
{ id = "ALC", label = "ALC", sub = "Alcohol", full = "Alcohol buff", kind = "aura",
icon = "inv_drink_04",
items = "Medivh's Merlot \194\183 Rumsey Rum Black Label \194\183 Kreeg's Stout Beatdown - different types stack, same type does not; one column, strongest shown" },
{ id = "FR", label = "FR", sub = "fire", full = "Fire protection", kind = "aura", unverified = true,
icon = "inv_potion_24",
items = "Greater Fire Protection Potion - situational per boss; enable/disable via the expectation matrix" }, items = "Greater Fire Protection Potion - situational per boss; enable/disable via the expectation matrix" },
{ id = "WPN", label = "WPN", sub = "temp.", full = "Weapon (temporary)", kind = "weapon", -- NOT an aura slot -- The remaining school protection columns follow the FR pattern (one
items = "Dense Sharpening Stone \194\183 Brilliant Wizard/Mana Oil - permanent enchant: Equipment tab" }, -- column per school, cross-school stacking unverified -- see header).
-- Unlike FR they carry NO seed entry in data/expectations.lua: the
-- dynamic-column rule keeps them hidden until enabled per role/class.
{ id = "FrR", label = "FrR", sub = "frost", full = "Frost protection", kind = "aura", unverified = true,
icon = "inv_potion_20",
items = "Greater Frost Protection Potion - situational per boss; enable/disable via the expectation matrix" },
{ id = "NR", label = "NR", sub = "nature", full = "Nature protection", kind = "aura", unverified = true,
icon = "inv_potion_22",
items = "Greater Nature Protection Potion - situational per boss; enable/disable via the expectation matrix" },
{ id = "SR", label = "SR", sub = "shadow", full = "Shadow protection", kind = "aura", unverified = true,
icon = "inv_potion_23",
items = "Greater Shadow Protection Potion - situational per boss; enable/disable via the expectation matrix" },
{ id = "HR", label = "HR", sub = "holy", full = "Holy protection", kind = "aura", unverified = true,
icon = "inv_potion_09",
items = "Holy Protection Potion - situational per boss; enable/disable via the expectation matrix" },
{ id = "AR", label = "AR", sub = "arcane", full = "Arcane protection", kind = "aura", unverified = true,
icon = "inv_potion_83",
items = "Greater Arcane Protection Potion - situational per boss; enable/disable via the expectation matrix" },
{ id = "WPN", label = "WPN", sub = "temp.", full = "Weapon (temporary)", kind = "weapon", unverified = true, -- NOT an aura slot; MH/OH split unverified
icon = "inv_stone_sharpeningstone_01",
items = "Elemental/Dense Sharpening Stone \194\183 Brilliant Wizard/Mana Oil - permanent enchant: Equipment tab" },
} }
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
-- Buff name -> slot table. -- Buff name -> entry table.
-- spellID = 0 means "match by name only" (no known aura spell ID). -- spellID = 0 means "match by name only" (no known aura spell ID).
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
DC.CONSUMABLES = { DC.CONSUMABLES = {
-- FLASK -- FLASK
["Flask of the Titans"] = { slot = "FLASK", spellID = 17626, item = "Flask of the Titans" }, ["Flask of the Titans"] = { slot = "FLASK", spellID = 17626, item = "Flask of the Titans", icon = "inv_potion_62" },
["Flask of Supreme Power"] = { slot = "FLASK", spellID = 17628, item = "Flask of Supreme Power" }, ["Flask of Supreme Power"] = { slot = "FLASK", spellID = 17628, item = "Flask of Supreme Power", icon = "inv_potion_41" },
["Flask of Distilled Wisdom"] = { slot = "FLASK", spellID = 17627, item = "Flask of Distilled Wisdom" }, ["Flask of Distilled Wisdom"] = { slot = "FLASK", spellID = 17627, item = "Flask of Distilled Wisdom", icon = "inv_potion_97" },
["Flask of Chromatic Resistance"] = { slot = "FLASK", spellID = 17629, item = "Flask of Chromatic Resistance" }, ["Flask of Chromatic Resistance"] = { slot = "FLASK", spellID = 17629, item = "Flask of Chromatic Resistance", icon = "inv_potion_48" },
["Flask of Petrification"] = { slot = "FLASK", spellID = 17624, item = "Flask of Petrification" }, ["Flask of Petrification"] = { slot = "FLASK", spellID = 17624, item = "Flask of Petrification", icon = "inv_potion_26" }, -- NOT inv_potion_98 (a often-repeated wrong guess) -- wowhead-confirmed
-- FOOD (buff names, not item names; TWoW custom foods likely all show "Well Fed" - unverified) -- FOOD (buff names, not item names).
["Well Fed"] = { slot = "FOOD", spellID = 0, item = "Grilled Squid etc." }, -- "Well Fed" is the generic food aura: the ITEM is identified through
-- ["Food"] (the eating tick) deliberately REMOVED: it is -- the tooltip EFFECT line (scan/aura.lua auras.effects) against the
-- the sit-and-eat channel aura, not a food buff -- it made the FOOD -- per-variant discrim patterns below -- first match wins, no match =
-- cell green for anyone currently chewing (field-tested). Only -- nil = the matrix falls back to the slot-generic icon ("food buff,
-- lasting food buffs count. -- unknown which"). The variant list is the food set of the KG
["Increased Stamina"] = { slot = "FOOD", spellID = 0, item = "stamina food" }, -- stacking-rules atom plus the classic raid staples; the discrim
["Dragonbreath Chili"] = { slot = "FOOD", spellID = 15852, item = "Dragonbreath Chili" }, -- patterns are best-effort data (recorded-tooltip verification via
-- dev/raiddump pending) -- a wrong one is a one-line fix.
-- ["Food"] (the eating tick) deliberately REMOVED: it is the
-- sit-and-eat channel aura, not a food buff -- it made the FOOD cell
-- green for anyone currently chewing (field-tested). Only lasting
-- food buffs count.
["Well Fed"] = { slot = "FOOD", spellID = 0, item = "food buff", icon = nil,
variants = {
-- ORDER MATTERS: first match wins, so specific effect lines
-- (crit, ranged AP, spell damage) sit ABOVE the generic stat
-- words they could otherwise be swallowed by.
-- Several Turtle customs SHARE their aura spell with a classic
-- staple (icon research 2026-08-11: Power Mushroom = 24800 =
-- Dumplings, Sour Mountain Berry = 18230 = Grilled Squid,
-- Juicy Striped Melon = 22731 = Runn Tum Tuber) -- those pairs
-- are indistinguishable by effect line; the listed-first item
-- is shown as the representative. Same slot, so harmless.
{ item = "Danonzo's Tel'Abim Delight", icon = "inv_drink_17", discrim = "[Ss]pell [Dd]amage" },
{ item = "Danonzo's Tel'Abim Medley", icon = "inv_misc_food_08", discrim = "[Hh]aste" },
{ item = "Danonzo's Tel'Abim Surprise", icon = "inv_misc_food_09", discrim = "[Rr]anged [Aa]ttack [Pp]ower" },
{ item = "Gurubashi Gumbo", icon = "inv_misc_food_64", discrim = "[Cc]rit" },
{ item = "Le Fishe Au Chocolat", icon = nil, discrim = "[Dd]odge" }, -- icon: questionmark placeholder even upstream (octowow + wowauctions), stays nil
{ item = "Empowering Herbal Salad", icon = "inv_misc_food_salad", discrim = "[Hh]ealing" },
{ item = "Nightfin Soup", icon = "inv_drink_17", discrim = "8 [Mm]ana" },
{ item = "Sagefish Delight", icon = "inv_misc_fish_21", discrim = "6 [Mm]ana" },
-- +20 Strength: Dumplings and Power Mushroom, same spell 24800
{ item = "Smoked Desert Dumplings", icon = "inv_misc_food_64", discrim = "[Ss]trength" },
{ item = "Power Mushroom", icon = "inv_mushroom_11", discrim = "[Ss]trength" }, -- NOT "+10 all stats" (dead pattern until 2026-08-11); tooltip-verified +20 STR
-- +10 Agility: Grilled Squid and Sour Mountain Berry, same spell 18230
{ item = "Grilled Squid", icon = "inv_misc_fish_13", discrim = "[Aa]gility" },
{ item = "Sour Mountain Berry", icon = "inv_misc_food_40", discrim = "[Aa]gility" },
-- +10 Intellect: classic Tuber and Turtle Melon, same spell 22731
{ item = "Runn Tum Tuber Surprise", icon = "inv_misc_food_63", discrim = "[Ii]ntellect" },
{ item = "Juicy Striped Melon", icon = "inv_misc_food_22", discrim = "[Ii]ntellect" },
-- generic stamina LAST: Gumbo's line also contains Stamina and
-- must be caught by its crit pattern above, never by this one
{ item = "Hardened Mushroom", icon = "inv_mushroom_11", discrim = "[Ss]tamina" }, -- NOT "+armor" (dead pattern until 2026-08-11); tooltip-verified +25 STA, spell 25660
} },
["Increased Stamina"] = { slot = "FOOD", spellID = 0, item = "stamina food", icon = nil },
["Dragonbreath Chili"] = { slot = "FOOD", spellID = 15852, item = "Dragonbreath Chili", icon = "inv_drink_17" }, -- yes, the chili really uses a drink icon (wowhead-confirmed via og:image)
-- AP -- AP
["Winterfall Firewater"] = { slot = "AP", spellID = 17038, item = "Winterfall Firewater" }, ["Winterfall Firewater"] = { slot = "AP", spellID = 17038, item = "Winterfall Firewater", icon = "inv_potion_92" },
["Juju Might"] = { slot = "AP", spellID = 16329, item = "Juju Might" }, ["Juju Might"] = { slot = "AP", spellID = 16329, item = "Juju Might", icon = "inv_misc_monsterscales_07" },
-- STR -- STR
["Juju Power"] = { slot = "STR", spellID = 16323, item = "Juju Power" }, ["Juju Power"] = { slot = "STR", spellID = 16323, item = "Juju Power", icon = "inv_misc_monsterscales_11" },
["Elixir of Giants"] = { slot = "STR", spellID = 11405, item = "Elixir of Giants" }, ["Elixir of Giants"] = { slot = "STR", spellID = 11405, item = "Elixir of Giants", icon = "inv_potion_61" },
["Elixir of the Giants"] = { slot = "STR", spellID = 11405, item = "Elixir of Giants" }, -- AURA name on this client (measured live: SpellInfo(11405) = "Elixir of the Giants"); the item is named without "the", and matching runs on the aura name first ["Elixir of the Giants"] = { slot = "STR", spellID = 11405, item = "Elixir of Giants", icon = "inv_potion_61" }, -- AURA name on this client (measured live: SpellInfo(11405) = "Elixir of the Giants"); the item is named without "the", and matching runs on the aura name first
["Elixir of Brute Force"] = { slot = "STR", spellID = 17537, item = "Elixir of Brute Force" }, ["Elixir of Brute Force"] = { slot = "STR", spellID = 17537, item = "Elixir of Brute Force", icon = "inv_potion_40" },
["Strike of the Scorpok"] = { slot = "STR", spellID = 10669, item = "Strike of the Scorpok" }, -- measured live in a raid (26 players); was tallied as unknown
["Rage of Ages"] = { slot = "STR", spellID = 0, item = "R.O.I.D.S." }, -- aura name differs from the item; own spell ID not measured yet
-- AGI -- AGI
["Elixir of the Mongoose"] = { slot = "AGI", spellID = 17538, item = "Elixir of the Mongoose" }, ["Elixir of the Mongoose"] = { slot = "AGI", spellID = 17538, item = "Elixir of the Mongoose", icon = "inv_potion_32" },
["Elixir of Greater Agility"] = { slot = "AGI", spellID = 11334, item = "Elixir of Greater Agility" }, ["Elixir of Greater Agility"] = { slot = "AGI", spellID = 11334, item = "Elixir of Greater Agility", icon = "inv_potion_94" },
["Greater Agility"] = { slot = "AGI", spellID = 11334, item = "Elixir of Greater Agility" }, -- AURA name (the item name never appears as a buff); was tallied as unknown 14x ["Greater Agility"] = { slot = "AGI", spellID = 11334, item = "Elixir of Greater Agility", icon = "inv_potion_94" }, -- AURA name (the item name never appears as a buff); was tallied as unknown 14x
["Elixir of Agility"] = { slot = "AGI", spellID = 11328, item = "Elixir of Agility" }, ["Elixir of Agility"] = { slot = "AGI", spellID = 11328, item = "Elixir of Agility", icon = "inv_potion_93" },
-- SP (merged: arcane_elixir + school_elixir + dreamtonic + dreamshard) -- BL (Blasted Lands buff slot -- one at a time, pick one; the five
["Greater Arcane Elixir"] = { slot = "SP", spellID = 17539, item = "Greater Arcane Elixir" }, -- items were split across STR/nowhere in the old model).
["Arcane Elixir"] = { slot = "SP", spellID = 11390, item = "Arcane Elixir" }, -- Aura names + spell ids wowhead-confirmed 2026-08-11 ("Strike of the
["Elixir of Shadow Power"] = { slot = "SP", spellID = 11474, item = "Elixir of Shadow Power" }, -- Scorpok" additionally measured live in a 26-man raid). The item
["Elixir of Frost Power"] = { slot = "SP", spellID = 21920, item = "Elixir of Frost Power" }, -- names ride along as alias keys (spellID 0) because the Turtle
["Greater Frost Power"] = { slot = "SP", spellID = 56544, item = "Elixir of Greater Frost Power" }, -- AURA name (TurtleWoW-added). NOT measured in-game: taken from a TurtleWoW spell database (56544 = "Greater Frost Power", "Increases frost spell damage by up to 40 for 3600 sec.") plus the item name in pfQuest-turtle's item DB; the aura drops "Elixir of" the same way Giants/Agility above do -- client may surface the permanent aura under the item name (KG
["Elixir of Greater Firepower"] = { slot = "SP", spellID = 26276, item = "Elixir of Greater Firepower" }, -- note); a key that never matches is harmless.
["Elixir of Firepower"] = { slot = "SP", spellID = 7844, item = "Elixir of Firepower" }, ["Rage of Ages"] = { slot = "BL", spellID = 10667, item = "R.O.I.D.S.", icon = "inv_stone_15" },
["Dreamtonic"] = { slot = "SP", spellID = 45489, item = "Dreamtonic" }, -- use-spell ID; the aura's own ID is unverified ["R.O.I.D.S."] = { slot = "BL", spellID = 0, item = "R.O.I.D.S.", icon = "inv_stone_15" },
["Dreamshard Elixir"] = { slot = "SP", spellID = 45427, item = "Dreamshard Elixir" }, -- use-spell ID; the aura's own ID is unverified ["Strike of the Scorpok"] = { slot = "BL", spellID = 10669, item = "Ground Scorpok Assay", icon = "inv_misc_dust_02" }, -- old model filed it under STR with the aura name as item -- both wrong (it is +25 AGI, and the item is the Assay)
["Ground Scorpok Assay"] = { slot = "BL", spellID = 0, item = "Ground Scorpok Assay", icon = "inv_misc_dust_02" },
["Spirit of Boar"] = { slot = "BL", spellID = 10668, item = "Lung Juice Cocktail", icon = "inv_drink_12" },
["Lung Juice Cocktail"] = { slot = "BL", spellID = 0, item = "Lung Juice Cocktail", icon = "inv_drink_12" },
["Infallible Mind"] = { slot = "BL", spellID = 10692, item = "Cerebral Cortex Compound", icon = "inv_potion_32" }, -- same icon as the Mongoose elixir -- genuine, wowhead-confirmed
["Cerebral Cortex Compound"] = { slot = "BL", spellID = 0, item = "Cerebral Cortex Compound", icon = "inv_potion_32" },
["Spiritual Domination"] = { slot = "BL", spellID = 10693, item = "Gizzard Gum", icon = "inv_misc_food_30" },
["Gizzard Gum"] = { slot = "BL", spellID = 0, item = "Gizzard Gum", icon = "inv_misc_food_30" },
-- MP5 -- ZANZA (free-stacker; only one Zanza-potion effect at a time; buff
["Mageblood Potion"] = { slot = "MP5", spellID = 24363, item = "Mageblood Potion" }, -- name = item name, wowhead-confirmed)
["Mageblood"] = { slot = "MP5", spellID = 24363, item = "Mageblood Potion" }, -- TWoW buff-name variant (old Data.lua kept both) ["Spirit of Zanza"] = { slot = "ZANZA", spellID = 24382, item = "Spirit of Zanza", icon = "inv_potion_30" },
-- GAE (split out of the old SP column)
["Greater Arcane Elixir"] = { slot = "GAE", spellID = 17539, item = "Greater Arcane Elixir", icon = "inv_potion_25" },
["Arcane Elixir"] = { slot = "GAE", spellID = 11390, item = "Arcane Elixir", icon = "inv_potion_30" },
-- SCHOOL (split out of the old SP column; one school elixir at a time)
["Elixir of Shadow Power"] = { slot = "SCHOOL", spellID = 11474, item = "Elixir of Shadow Power", icon = "inv_potion_46" },
["Elixir of Frost Power"] = { slot = "SCHOOL", spellID = 21920, item = "Elixir of Frost Power", icon = "inv_potion_03" },
["Greater Frost Power"] = { slot = "SCHOOL", spellID = 56544, item = "Elixir of Greater Frost Power", icon = "inv_potion_13" }, -- AURA name (TurtleWoW-added). NOT measured in-game: taken from a TurtleWoW spell database (56544 = "Greater Frost Power") plus the item name in pfQuest-turtle's item DB; the aura drops "Elixir of" the same way Giants/Agility above do
["Elixir of Greater Firepower"] = { slot = "SCHOOL", spellID = 26276, item = "Elixir of Greater Firepower", icon = "inv_potion_60" },
["Elixir of Firepower"] = { slot = "SCHOOL", spellID = 7844, item = "Elixir of Firepower", icon = "inv_potion_33" },
-- DREAMT (TurtleWoW custom; own slot -- simultaneously active with
-- GAE, SCHOOL and SHARD in the caster set)
["Dreamtonic"] = { slot = "DREAMT", spellID = 45489, item = "Dreamtonic", icon = "inv_potion_10" }, -- use-spell ID; the aura's own ID is unverified
-- SHARD (TurtleWoW custom; own slot)
["Dreamshard Elixir"] = { slot = "SHARD", spellID = 45427, item = "Dreamshard Elixir", icon = "inv_potion_12" }, -- use-spell ID; the aura's own ID is unverified
-- MP5 (narrowed: Nightfin Soup is a FOOD buff and lives there now)
["Mageblood Potion"] = { slot = "MP5", spellID = 24363, item = "Mageblood Potion", icon = "inv_potion_45" },
["Mageblood"] = { slot = "MP5", spellID = 24363, item = "Mageblood Potion", icon = "inv_potion_45" }, -- TWoW buff-name variant (old Data.lua kept both)
-- ARM -- ARM
["Elixir of Superior Defense"] = { slot = "ARM", spellID = 11348, item = "Elixir of Superior Defense" }, ["Elixir of Superior Defense"] = { slot = "ARM", spellID = 11348, item = "Elixir of Superior Defense", icon = "inv_potion_66" },
["Elixir of Greater Defense"] = { slot = "ARM", spellID = 11349, item = "Elixir of Greater Defense" }, ["Elixir of Greater Defense"] = { slot = "ARM", spellID = 11349, item = "Elixir of Greater Defense", icon = "inv_potion_65" },
-- HP (merged: def_fortitude + alcohol STA variants) -- HPELX (alcohol split out into ALC; Fortitude stacks with it)
["Elixir of Fortitude"] = { slot = "HP", spellID = 3593, item = "Elixir of Fortitude" }, ["Elixir of Fortitude"] = { slot = "HPELX", spellID = 3593, item = "Elixir of Fortitude", icon = "inv_potion_43" },
["Health II"] = { slot = "HP", spellID = 3593, item = "Elixir of Fortitude" }, -- AURA name on this client (measured live: SpellInfo(3593) = "Health II") ["Health II"] = { slot = "HPELX", spellID = 3593, item = "Elixir of Fortitude", icon = "inv_potion_43" }, -- AURA name on this client (measured live: SpellInfo(3593) = "Health II")
["Rumsey Rum Black Label"] = { slot = "HP", spellID = 25804, item = "Rumsey Rum Black Label" },
["Rumsey Rum"] = { slot = "HP", spellID = 0, item = "Rumsey Rum" },
["Rumsey Rum Light"] = { slot = "HP", spellID = 0, item = "Rumsey Rum Light" },
["Medivh's Merlot"] = { slot = "HP", spellID = 57106, item = "Medivh's Merlot" }, -- use-spell ID; the aura's own ID is unverified
["Gordok Green Grog"] = { slot = "HP", spellID = 0, item = "Gordok Green Grog" },
-- FR -- ALC (alcohol buffs -- ONE column by design although different
["Greater Fire Protection"] = { slot = "FR", spellID = 17543, item = "Greater Fire Protection Potion" }, -- types stack; see the header compromise note). Buff names and spell
["Fire Protection"] = { slot = "FR", spellID = 7233, item = "Fire Protection Potion" }, -- ids wowhead/octowow-confirmed 2026-08-11; the "Blue Label" aura
-- name differs from the Blue item name, both keys are kept.
["Medivh's Merlot"] = { slot = "ALC", spellID = 57106, item = "Medivh's Merlot", icon = "inv_drink_waterskin_05" }, -- use-spell ID; the aura's own ID is unverified
["Medivh's Merlot Blue"] = { slot = "ALC", spellID = 0, item = "Medivh's Merlot Blue", icon = "inv_drink_waterskin_01" },
["Medivh's Merlot Blue Label"] = { slot = "ALC", spellID = 57107, item = "Medivh's Merlot Blue", icon = "inv_drink_waterskin_01" }, -- the AURA name (octowow: spell 57107 "Medivh's Merlot Blue Label")
["Rumsey Rum Black Label"] = { slot = "ALC", spellID = 25804, item = "Rumsey Rum Black Label", icon = "inv_drink_04" },
["Rumsey Rum"] = { slot = "ALC", spellID = 20875, item = "Rumsey Rum", icon = "inv_drink_03" },
["Rumsey Rum Light"] = { slot = "ALC", spellID = 25037, item = "Rumsey Rum Light", icon = "inv_drink_08" },
["Gordok Green Grog"] = { slot = "ALC", spellID = 22789, item = "Gordok Green Grog", icon = "inv_drink_03" }, -- same icon as Rumsey Rum -- genuine, wowhead-confirmed
["Kreeg's Stout Beatdown"] = { slot = "ALC", spellID = 22790, item = "Kreeg's Stout Beatdown", icon = "inv_drink_05" }, -- +25 Spirit/-5 Int -- the healer set's alcohol, not a stamina drink
-- FR (protection-potion slot rules are an UNVERIFIED gap, see header)
["Greater Fire Protection"] = { slot = "FR", spellID = 17543, item = "Greater Fire Protection Potion", icon = "inv_potion_24", unverified = true },
["Fire Protection"] = { slot = "FR", spellID = 7233, item = "Fire Protection Potion", icon = "inv_potion_16", unverified = true },
-- FrR/NR/SR/HR/AR (same UNVERIFIED protection-potion gap as FR).
-- Use-effect spell IDs + item icons wowhead-confirmed 2026-08-11.
-- Tier coverage is asymmetric by design: no Lesser Arcane potion and
-- no Greater Holy potion exist in vanilla 1.12. The aura on the
-- client likely drops the "Greater" (wowhead names both tiers
-- "<School> Protection"); the greater keys mirror the FR convention
-- and the ID match carries them regardless of the live aura name.
["Greater Frost Protection"] = { slot = "FrR", spellID = 17544, item = "Greater Frost Protection Potion", icon = "inv_potion_20", unverified = true },
["Frost Protection"] = { slot = "FrR", spellID = 7239, item = "Frost Protection Potion", icon = "inv_potion_13", unverified = true },
["Greater Nature Protection"] = { slot = "NR", spellID = 17546, item = "Greater Nature Protection Potion", icon = "inv_potion_22", unverified = true },
["Nature Protection"] = { slot = "NR", spellID = 7254, item = "Nature Protection Potion", icon = "inv_potion_06", unverified = true }, -- live aura name carries a trailing space (SpellInfo(7254)); scan/aura.lua's trimmed retry covers it
["Greater Shadow Protection"] = { slot = "SR", spellID = 17548, item = "Greater Shadow Protection Potion", icon = "inv_potion_23", unverified = true },
["Shadow Protection"] = { slot = "SR", spellID = 7242, item = "Shadow Protection Potion", icon = "inv_potion_44", unverified = true }, -- NAME collides with the priest buff (data/classbuffs.lua SPROT, ids 976/10957/10958): ID-before-name keeps them apart; a name-only scan cannot tell the two, which SR's default-off expectation mitigates
["Holy Protection"] = { slot = "HR", spellID = 7245, item = "Holy Protection Potion", icon = "inv_potion_09", unverified = true },
["Greater Arcane Protection"] = { slot = "AR", spellID = 17549, item = "Greater Arcane Protection Potion", icon = "inv_potion_83", unverified = true },
-- WPN: intentionally NO aura entries - temp enchant is not a buff -- WPN: intentionally NO aura entries - temp enchant is not a buff
} }
-- ------------------------------------------------------------------
-- DC.ResolveItem(buffName, effectText) -> item, icon
-- The PURE identification ladder (spec: "Identification ladder"):
-- 1. unique buff name -> item/icon straight from the entry
-- 2. generic buff name -> match effectText against the entry's
-- variant discrim patterns (plain Lua 5.0 string.find, first match
-- wins) -> that variant's item/icon
-- 3. no match / no effect text -> nil, nil (the caller falls back to
-- the slot-generic icon: "slot filled, item unknown")
-- An entry-level discrim (no variants) gates rule 1 the same way: the
-- item only resolves when the effect line matches. Offline-testable --
-- no WoW API, no upvalues beyond the data table.
-- ------------------------------------------------------------------
function DC.ResolveItem(buffName, effectText)
local def = DC.CONSUMABLES[buffName]
if not def then
return nil, nil
end
if def.variants then
if type(effectText) == "string" then
local n = table.getn(def.variants)
for i = 1, n do
local v = def.variants[i]
if v.discrim and string.find(effectText, v.discrim) then
return v.item, v.icon
end
end
end
return nil, nil
end
if def.discrim then
if type(effectText) == "string"
and string.find(effectText, def.discrim) then
return def.item, def.icon
end
return nil, nil
end
return def.item, def.icon
end
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
-- spellID -> slotId map, generated from DC.CONSUMABLES (spellID > 0). -- spellID -> slotId map, generated from DC.CONSUMABLES (spellID > 0).
-- Collision rule: at classify time an ID match beats a -- Collision rule: at classify time an ID match beats a
+22 -8
View File
@@ -1,5 +1,5 @@
-- DopingControl data/enchants.lua -- DopingControl data/enchants.lua
-- Equipment tab: 14 enchant slots + 2 item-tile slots (both trinkets), in -- Equipment tab: 15 enchant slots + 2 item-tile slots (both trinkets), in
-- paperdoll order, with 1.12 inventory slot IDs. -- paperdoll order, with 1.12 inventory slot IDs.
-- Enchant columns are checked present/missing only (enchant ID in the item -- Enchant columns are checked present/missing only (enchant ID in the item
-- link ~= 0); an EMPTY enchant slot is always a gap (core/model.lua), so it -- link ~= 0); an EMPTY enchant slot is always a gap (core/model.lua), so it
@@ -7,13 +7,26 @@
-- question is. Item-tile slots (trinkets) show the equipped item and -- question is. Item-tile slots (trinkets) show the equipped item and
-- ignore expectations entirely; only an EMPTY slot counts as a gap there. -- ignore expectations entirely; only an EMPTY slot counts as a gap there.
-- --
-- Server note: neck and both rings ARE enchantable on this server (a -- Server note: neck, both rings AND the waist ARE enchantable on this
-- server-custom feature beyond vanilla WoW, where they take no enchant) -- -- server (a server-custom feature beyond vanilla WoW, where none of them
-- NECK/R1/R2 are therefore kind="ench" like any armor slot, not item -- take an enchant) -- NECK/R1/R2/WAIST are therefore kind="ench" like any
-- tiles. Their expectation defaults to OFF (data/expectations.lua): unlike -- armor slot, not item tiles. The waist enchant is a "Belt Buckle" item
-- the ARMOR enchant slots, a neck/ring enchant is not assumed baseline -- (Copper/Bronze/Thorium/Dreamsteel/Bloody Belt Buckle),
-- raid gear, so the columns only track it once a role/class opts in via -- the same custom itemization line as the neck/ring gems. Their
-- the options grid. -- expectation defaults to OFF (data/expectations.lua): unlike the ARMOR
-- enchant slots, a neck/ring/waist enchant is not assumed baseline raid
-- gear, so the columns only track it once a role/class opts in via the
-- options grid.
--
-- Bug history (2026-08-30): the waist (invSlot 6) was missing from this
-- table entirely -- not a deliberate exclusion, an oversight that predates
-- the Belt Buckle KG lookup above. Because scan/gear.lua's G.InvSlots()
-- derives the full GetInventoryItemLink read list from THIS table, the
-- omission silently dropped the waist from every equipment read: no raid
-- dump ever carried a ";6=" entry, and the HIT tab under-counted any
-- +hit/+resist on a belt (scan/hit.lua's H.SLOTS already listed 6 and
-- expected scan/gear.lua's rawLinks to cover it -- see that file's
-- ReadUnit comment). Adding WAIST here fixes both at the source.
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline. -- Pure Lua 5.0, no WoW API -- dofile-loadable offline.
DopingControl = DopingControl or {} DopingControl = DopingControl or {}
@@ -30,6 +43,7 @@ DC.SLOTS_EQUIPMENT = {
{ id = "SHLD", label = "SHO", sub = "slot 3", full = "Shoulders", kind = "ench", invSlot = 3 }, { id = "SHLD", label = "SHO", sub = "slot 3", full = "Shoulders", kind = "ench", invSlot = 3 },
{ id = "BACK", label = "BCK", sub = "slot 15", full = "Back", kind = "ench", invSlot = 15 }, { id = "BACK", label = "BCK", sub = "slot 15", full = "Back", kind = "ench", invSlot = 15 },
{ id = "CHST", label = "CHE", sub = "slot 5", full = "Chest", kind = "ench", invSlot = 5 }, { id = "CHST", label = "CHE", sub = "slot 5", full = "Chest", kind = "ench", invSlot = 5 },
{ id = "WAIST", label = "WAI", sub = "slot 6", full = "Waist", kind = "ench", invSlot = 6 }, -- enchantable on this server (custom Belt Buckle line, unlike vanilla)
{ id = "WRST", label = "WRI", sub = "slot 9", full = "Wrist", kind = "ench", invSlot = 9 }, { id = "WRST", label = "WRI", sub = "slot 9", full = "Wrist", kind = "ench", invSlot = 9 },
{ id = "HAND", label = "HND", sub = "slot 10", full = "Hands", kind = "ench", invSlot = 10 }, { id = "HAND", label = "HND", sub = "slot 10", full = "Hands", kind = "ench", invSlot = 10 },
{ id = "LEGS", label = "LEG", sub = "slot 7", full = "Legs", kind = "ench", invSlot = 7 }, { id = "LEGS", label = "LEG", sub = "slot 7", full = "Legs", kind = "ench", invSlot = 7 },
+63 -32
View File
@@ -15,7 +15,21 @@
-- a CASTER/HEALER role default for every performing class. -- a CASTER/HEALER role default for every performing class.
-- --
-- Role-line choices: -- Role-line choices:
-- Consumables: standard raid role lines. -- Consumables: the four role sets of the KG stacking-rules atom
-- (data-tables/consumable-stacking-rules, TurtleWoW
-- 1.17.2 -- melee / caster / tank / healer, each "all
-- active simultaneously") ARE the defaults now; see the
-- spec 2026-08-11-slot-model-item-icons-design.md. RANGED
-- has no KG set of its own and is derived from the melee
-- line (no STR -- ranged AP scales off AGI -- and no
-- FLASK). FR stays situational for all five roles
-- (enable/disable per boss via the expectation matrix),
-- WPN as before. The other school protection slots
-- (FrR/NR/SR/HR/AR, added 2026-08-11) carry NO seed entry
-- at all: default off for every role, so the dynamic-
-- column rule hides them until someone enables them per
-- role/class for a matching boss (same treatment as EMB
-- and T1/T2 below) -- unlike FR, which ships seeded true.
-- Class buffs: SOUL only HEALER, AI only CASTER/HEALER, SPROT situational -- Class buffs: SOUL only HEALER, AI only CASTER/HEALER, SPROT situational
-- for all five -- like FR. EMB (Emerald Blessing) has NO -- for all five -- like FR. EMB (Emerald Blessing) has NO
-- seed entry: the buff comes from a raid questline and most -- seed entry: the buff comes from a raid questline and most
@@ -23,12 +37,16 @@
-- until someone enables it per role/class in the options -- until someone enables it per role/class in the options
-- grid; the dynamic-column rule makes an unexpected slot -- grid; the dynamic-column rule makes an unexpected slot
-- disappear entirely (same treatment as T1/T2 below). -- disappear entirely (same treatment as T1/T2 below).
-- Equipment: ALL armor enchant slots for ALL roles; -- Equipment: ALL armor enchant slots for ALL roles, WAIST included
-- (owner decision, 2026-08-30: every role wears a belt,
-- and it is enchantable on this server via the Belt
-- Buckle item line -- so it is worn equipment like any
-- other slot, not an optional extra like neck/rings);
-- per-item exemptions (wand/holdable) are handled at -- per-item exemptions (wand/holdable) are handled at
-- classify time via DC.ENCH_EXEMPT, not here. -- classify time via DC.ENCH_EXEMPT, not here.
-- NECK/R1/R2 (enchantable on this server, a custom feature -- NECK/R1/R2 (also enchantable on this server, a custom
-- beyond vanilla) default to OFF for every role: unlike -- feature beyond vanilla) stay OFF by default for every
-- the armor slots, a neck/ring enchant is not assumed -- role: unlike WAIST, a neck/ring enchant is not assumed
-- baseline raid gear -- enable it per role/class in the -- baseline raid gear -- enable it per role/class in the
-- options grid when it matters. T1/T2 (trinket item tiles) -- options grid when it matters. T1/T2 (trinket item tiles)
-- carry NO seed entry at all -- the model ignores -- carry NO seed entry at all -- the model ignores
@@ -37,19 +55,16 @@
-- --
-- Known debatable rows -- documented deliberately, the shipped defaults -- Known debatable rows -- documented deliberately, the shipped defaults
-- win: -- win:
-- * TANK+AP: some tank consumable lists include Winterfall -- * TANK+AP / MELEE+FLASK: the OLD defaults deliberately excluded them;
-- Firewater; the default has no AP for TANK. -- the KG role sets include them (tank runs Winterfall
-- * MELEE+FLASK: some melee lists include Flask of the Titans; the -- Firewater, melee runs Flask of the Titans), so both
-- default has no FLASK for MELEE. -- flipped to true with the v6 slot model.
-- * MELEE/RANGED+HP: some melee lists have neither Fortitude nor -- * RANGED line: derived, not KG-covered (see above) -- BL is included
-- alcohol; HP=true is the weakest row of the matrix. -- (Ground Scorpok Assay is +25 AGI), STR/FLASK are not.
-- * RANGED+AP/WPN: Firewater/Juju Might are MELEE attack power; ranged -- * HEALER+HPELX: the KG healer set carries no Elixir of Fortitude, so
-- AP comes from food. Arguably AP=false, WPN=false -- HPELX defaults off for HEALER while ALC (Kreeg's
-- for RANGED; the default says true. -- Stout Beatdown + Medivh's Merlot Blue) is on.
-- * HEALER+SP: healer sets may carry Dreamshard (+healing), which the -- * ZANZA: expected for every role (all four KG sets carry it).
-- 11-slot merge puts into SP; the default has no SP for
-- HEALER -> Dreamshard is invisible for healers by
-- default.
-- * Equipment: an alternative default would differentiate per role -- * Equipment: an alternative default would differentiate per role
-- (OFFH only TANK/MELEE, RNGD only RANGED, -- (OFFH only TANK/MELEE, RNGD only RANGED,
-- casters/healers neither); deliberately flattened to -- casters/healers neither); deliberately flattened to
@@ -65,9 +80,12 @@ local DC = DopingControl
DC.DEFAULT_EXPECT = { DC.DEFAULT_EXPECT = {
TANK = { TANK = {
-- consumables -- consumables (KG tank set: Mongoose, Firewater, Juju Power,
FLASK = true, FOOD = true, STR = true, AGI = true, ARM = true, -- R.O.I.D.S., Zanza, Superior Defense, Fortitude, Titans, food,
HP = true, FR = true, WPN = true, -- Merlot + Rumsey, sharpening stone; Gift of Arthas has no column)
FLASK = true, FOOD = true, AP = true, STR = true, AGI = true,
BL = true, ZANZA = true, ARM = true, HPELX = true, ALC = true,
FR = true, WPN = true,
-- class buffs -- class buffs
MOTW = true, PWF = true, SPROT = true, MOTW = true, PWF = true, SPROT = true,
-- equipment (RNGD default only for RANGED: -- equipment (RNGD default only for RANGED:
@@ -75,48 +93,61 @@ DC.DEFAULT_EXPECT = {
-- expectation matrix anytime) -- expectation matrix anytime)
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true, HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true, HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
WAIST = true,
}, },
MELEE = { MELEE = {
-- consumables -- consumables (KG melee set: Mongoose, Firewater/Juju Might,
FOOD = true, AP = true, STR = true, AGI = true, -- Juju Power/Giants, R.O.I.D.S./Scorpok, Zanza, Titans, food,
HP = true, FR = true, WPN = true, -- sharpening stone; Juju Flurry has no column)
FLASK = true, FOOD = true, AP = true, STR = true, AGI = true,
BL = true, ZANZA = true,
FR = true, WPN = true,
-- class buffs -- class buffs
MOTW = true, PWF = true, SPROT = true, MOTW = true, PWF = true, SPROT = true,
-- equipment -- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true, HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true, HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
WAIST = true,
}, },
RANGED = { RANGED = {
-- consumables -- consumables (derived from the melee line -- no KG set; no STR,
FOOD = true, AP = true, AGI = true, -- no FLASK, see "Known debatable rows")
HP = true, FR = true, WPN = true, FOOD = true, AP = true, AGI = true, BL = true, ZANZA = true,
FR = true, WPN = true,
-- class buffs -- class buffs
MOTW = true, PWF = true, SPROT = true, MOTW = true, PWF = true, SPROT = true,
-- equipment -- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true, HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true, HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
WAIST = true,
RNGD = true, RNGD = true,
}, },
-- CASTER/HEALER: RNGD also dropped by role default (their classes are -- CASTER/HEALER: RNGD also dropped by role default (their classes are
-- additionally exempt via ENCH_CLASS_EXEMPT - belt and suspenders) -- additionally exempt via ENCH_CLASS_EXEMPT - belt and suspenders)
CASTER = { CASTER = {
-- consumables -- consumables (KG caster set: Dreamshard, GAE, school elixir,
FLASK = true, FOOD = true, SP = true, MP5 = true, -- Dreamtonic, Mageblood, wizard oil, Zanza, Supreme Power, food)
FLASK = true, FOOD = true, GAE = true, SCHOOL = true,
DREAMT = true, SHARD = true, MP5 = true, ZANZA = true,
FR = true, WPN = true, FR = true, WPN = true,
-- class buffs -- class buffs
AI = true, MOTW = true, PWF = true, SPROT = true, AI = true, MOTW = true, PWF = true, SPROT = true,
-- equipment -- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true, HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true, HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
WAIST = true,
}, },
HEALER = { HEALER = {
-- consumables -- consumables (KG healer set: Dreamshard, Mageblood, mana oil,
FLASK = true, FOOD = true, MP5 = true, -- Zanza, Distilled Wisdom, food, Kreeg's + Merlot Blue)
HP = true, FR = true, WPN = true, FLASK = true, FOOD = true, MP5 = true, SHARD = true, ZANZA = true,
ALC = true,
FR = true, WPN = true,
-- class buffs (SOUL: expected ONLY here) -- class buffs (SOUL: expected ONLY here)
AI = true, MOTW = true, PWF = true, SPROT = true, SOUL = true, AI = true, MOTW = true, PWF = true, SPROT = true, SOUL = true,
-- equipment -- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true, HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true, HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
WAIST = true,
}, },
} }
+36 -13
View File
@@ -19,6 +19,12 @@
-- resolved via SuperWoW SpellInfo(id) (return 3 = icon) when the function -- resolved via SuperWoW SpellInfo(id) (return 3 = icon) when the function
-- exists. textures is OPTIONAL/partial by design -- every consumer -- exists. textures is OPTIONAL/partial by design -- every consumer
-- nil-guards, nothing downstream may require it. -- nil-guards, nothing downstream may require it.
-- Buff EFFECT lines: auras.effects[name] = the tooltip's line 2 text
-- (same hidden-tooltip read as the name, TextLeft2) for every path-A buff
-- that yielded both a name and a non-empty line 2. This feeds the item
-- identification ladder for generic buff names ("Well Fed" -> which food,
-- via DC.ResolveItem's discrim patterns). effects is OPTIONAL/partial
-- exactly like textures -- every consumer nil-guards.
-- Disagreements between the two ID sets are counted (symmetric difference, -- Disagreements between the two ID sets are counted (symmetric difference,
-- only when BOTH paths yielded at least one ID -- an absent path is missing -- only when BOTH paths yielded at least one ID -- an absent path is missing
-- data, not a disagreement) and accumulated by the engine into -- data, not a disagreement) and accumulated by the engine into
@@ -68,8 +74,9 @@ local A = DC_Aura
-- Assemble(rawA, rawB, idNames) -> auras, aurasRead, disagreements, unknown -- Assemble(rawA, rawB, idNames) -> auras, aurasRead, disagreements, unknown
-- rawA : array of { name = string|nil, id = number|nil, -- rawA : array of { name = string|nil, id = number|nil,
-- texture = string|nil } (path A; texture is the 1st -- texture = string|nil, effect = string|nil }
-- UnitBuff return, optional) -- (path A; texture is the 1st UnitBuff return,
-- effect the tooltip's line 2 -- both optional)
-- rawB : array of spell ids (numbers, path B) -- rawB : array of spell ids (numbers, path B)
-- idNames : OPTIONAL { [spellId] = "Buff Name" } for path-B ids, -- idNames : OPTIONAL { [spellId] = "Buff Name" } for path-B ids,
-- filled by the WoW-side caller (SuperWoW SpellInfo lookup -- -- filled by the WoW-side caller (SuperWoW SpellInfo lookup --
@@ -78,9 +85,10 @@ local A = DC_Aura
-- as before (no behavior change without it). -- as before (no behavior change without it).
-- Returns: -- Returns:
-- auras : { names = { [name]=true }, ids = { [id]=true }, -- auras : { names = { [name]=true }, ids = { [id]=true },
-- textures = { [name]=iconPath } } -- textures = { [name]=iconPath },
-- (store shape; textures may be empty/partial -- -- effects = { [name]=effectLine } }
-- consumers nil-guard) -- (store shape; textures AND effects may be empty/
-- partial -- consumers nil-guard)
-- aurasRead : bool -- true iff at least one USABLE aura arrived: a -- aurasRead : bool -- true iff at least one USABLE aura arrived: a
-- path-A entry carrying a name or an id, or a path-B id. -- path-A entry carrying a name or an id, or a path-B id.
-- Entries with NEITHER (UnitBuff texture present but the -- Entries with NEITHER (UnitBuff texture present but the
@@ -131,6 +139,7 @@ function A.Assemble(rawA, rawB, idNames)
local idsA = {} local idsA = {}
local idsB = {} local idsB = {}
local textures = {} -- [buffName] = icon path local textures = {} -- [buffName] = icon path
local effects = {} -- [buffName] = tooltip effect line (line 2)
local namedIds = {} -- ids that arrived WITH a name (path A rows) local namedIds = {} -- ids that arrived WITH a name (path A rows)
local usable = 0 -- entries that carry actual evidence (see header) local usable = 0 -- entries that carry actual evidence (see header)
@@ -144,6 +153,9 @@ function A.Assemble(rawA, rawB, idNames)
if e.texture then if e.texture then
textures[e.name] = e.texture textures[e.name] = e.texture
end end
if type(e.effect) == "string" and e.effect ~= "" then
effects[e.name] = e.effect
end
end end
if type(e.id) == "number" and e.id > 0 then if type(e.id) == "number" and e.id > 0 then
idsA[e.id] = true idsA[e.id] = true
@@ -259,7 +271,8 @@ function A.Assemble(rawA, rawB, idNames)
end end
end end
return { names = names, ids = ids, textures = textures }, return { names = names, ids = ids, textures = textures,
effects = effects },
aurasRead, disagreements, unknown aurasRead, disagreements, unknown
end end
@@ -376,15 +389,22 @@ function A.GetScanTip()
return ensureTip() return ensureTip()
end end
-- Returns name (tooltip line 1) AND effect (tooltip line 2 -- the buff's
-- effect text, feeds the item identification ladder for generic names).
-- raises an error on bad units -> always called through pcall -- raises an error on bad units -> always called through pcall
local function tipBuffName(unit, buffIndex) local function tipBuffName(unit, buffIndex)
scanTip:ClearLines() scanTip:ClearLines()
scanTip:SetUnitBuff(unit, buffIndex) scanTip:SetUnitBuff(unit, buffIndex)
local name, effect = nil, nil
local textObj = getglobal("DopingControlScanTipTextLeft1") local textObj = getglobal("DopingControlScanTipTextLeft1")
if textObj then if textObj then
return textObj:GetText() name = textObj:GetText()
end end
return nil local effObj = getglobal("DopingControlScanTipTextLeft2")
if effObj then
effect = effObj:GetText()
end
return name, effect
end end
-- debuff twin of tipBuffName (same hidden tooltip, SetUnitDebuff); -- debuff twin of tipBuffName (same hidden tooltip, SetUnitDebuff);
@@ -420,20 +440,23 @@ function A.ReadUnit(unit, guid)
if not ok or not texture then if not ok or not texture then
break break
end end
local name = nil local name, effect = nil, nil
if tip then if tip then
local ok2, nm = pcall(tipBuffName, unit, i) local ok2, nm, eff = pcall(tipBuffName, unit, i)
if ok2 then if ok2 then
name = nm name = nm
effect = eff
end end
end end
local id = nil local id = nil
if type(auraID) == "number" and auraID > 0 then if type(auraID) == "number" and auraID > 0 then
id = auraID id = auraID
end end
-- texture (UnitBuff return 1) rides along so Assemble can key -- texture (UnitBuff return 1) and the tooltip effect line ride
-- it under the tooltip name (auras.textures) -- along so Assemble can key them under the tooltip name
table.insert(rawA, { name = name, id = id, texture = texture }) -- (auras.textures / auras.effects)
table.insert(rawA, { name = name, id = id, texture = texture,
effect = effect })
end end
end end
if UnitDebuff then if UnitDebuff then
+21 -7
View File
@@ -120,6 +120,15 @@ function E.NormalizeWeapon(isSelf, v, readable)
end end
return false return false
end end
-- Stock 1.12 GetWeaponEnchantInfo takes NO arguments: on a client without
-- SuperWoW the extra unit is ignored and the first return is the PLAYER's
-- hasMainHandEnchant (1|nil). Only the SuperWoW name channel answers about
-- the foreign unit, and it is always string-or-nil (measured: 11 names,
-- 14 nils, "" never). A non-string therefore says nothing about this unit
-- => not readable, never "has an imbue".
if v ~= nil and type(v) ~= "string" then
return nil
end
if v == nil then if v == nil then
if readable == true then if readable == true then
return false return false
@@ -998,10 +1007,12 @@ if CreateFrame then
end end
E.SweepLastKnown(DC.lastKnown, rosterSet, E.CACHE_CAP) E.SweepLastKnown(DC.lastKnown, rosterSet, E.CACHE_CAP)
local db = DopingControlDB local db = DopingControlDB
if db and db.testMode then if DC.SimOwnsStore(db) then
-- Test mode was switched ON while this scan was in flight: the -- Test mode OR demo mode was on (or switched on while this scan
-- simulator fully replaces the data source ("real -- was in flight): the simulator fully replaces the data source
-- data never beats the simulation") -- drop the result. -- ("real data never beats the simulation") -- drop the result.
-- This is the load-bearing gate: it is the only thing between a
-- finished scan and DC.store = E.NewStore(...) below.
results = {} results = {}
return return
end end
@@ -1055,8 +1066,11 @@ if CreateFrame then
-- Start(force) -> bool (scan started/restarted). -- Start(force) -> bool (scan started/restarted).
-- Already running: no-op unless force, which restarts with a fresh -- Already running: no-op unless force, which restarts with a fresh
-- roster snapshot. Test mode is NOT checked here -- an explicit Start -- roster snapshot. DC.SimOwnsStore is NOT checked here -- an explicit
-- is user intent; only the READY_CHECK auto-trigger honors testMode. -- Start is user intent, and the callers that are NOT user intent
-- (READY_CHECK, the roster-change and window-open auto-scans) check it
-- themselves. The scan may therefore run in test/demo mode; what it may
-- never do is publish, so finish() drops the result instead.
function E.Start(force) function E.Start(force)
if running then if running then
if not force then if not force then
@@ -1087,7 +1101,7 @@ if CreateFrame then
eventFrame:SetScript("OnEvent", function() eventFrame:SetScript("OnEvent", function()
if event == "READY_CHECK" then if event == "READY_CHECK" then
local db = DopingControlDB local db = DopingControlDB
if db and db.readyCheckScan and not db.testMode then if db and db.readyCheckScan and not DC.SimOwnsStore(db) then
E.Start() E.Start()
end end
return return
+2 -2
View File
@@ -29,9 +29,9 @@ local G = DC_Gear
-- Fallback inventory-slot list (= invSlot column of DC.SLOTS_EQUIPMENT, -- Fallback inventory-slot list (= invSlot column of DC.SLOTS_EQUIPMENT,
-- data/enchants.lua) so this file works standalone in tests. -- data/enchants.lua) so this file works standalone in tests.
local FALLBACK_INV_SLOTS = { 1, 2, 3, 15, 5, 9, 10, 7, 8, 16, 17, 18, 11, 12, 13, 14 } local FALLBACK_INV_SLOTS = { 1, 2, 3, 15, 5, 6, 9, 10, 7, 8, 16, 17, 18, 11, 12, 13, 14 }
-- InvSlots() -> array of the 16 inventory slot numbers, in column order. -- InvSlots() -> array of the 17 inventory slot numbers, in column order.
-- Prefers DC.SLOTS_EQUIPMENT (single source of truth once data/ is loaded). -- Prefers DC.SLOTS_EQUIPMENT (single source of truth once data/ is loaded).
function G.InvSlots() function G.InvSlots()
if DC.SLOTS_EQUIPMENT then if DC.SLOTS_EQUIPMENT then
+7 -6
View File
@@ -122,9 +122,11 @@ DC_Hit = DC_Hit or {}
local H = DC_Hit local H = DC_Hit
-- Equipment slots that can carry +hit, in inventory-slot order. This is -- Equipment slots that can carry +hit, in inventory-slot order. This is
-- deliberately its OWN list and not DC_Gear.InvSlots(): the enchant tab -- deliberately its OWN list and not DC_Gear.InvSlots(): the two lists
-- has no waist column, but a belt most certainly can carry hit. Shirt (4) -- happen to agree today (both cover the waist, invSlot 6 -- see
-- and tabard (19) never carry stats and are left out. -- data/enchants.lua's bug-history note), but this file must not assume
-- that stays true, so it keeps every hit-bearing slot spelled out here.
-- Shirt (4) and tabard (19) never carry stats and are left out.
H.SLOTS = { 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 } H.SLOTS = { 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
H.SLOT_MAIN = 16 H.SLOT_MAIN = 16
@@ -888,9 +890,8 @@ end
-- ReadUnit(unit, rawLinks) -> hit, race -- ReadUnit(unit, rawLinks) -> hit, race
-- rawLinks : OPTIONAL { [invSlot] = link } that scan/gear.lua already -- rawLinks : OPTIONAL { [invSlot] = link } that scan/gear.lua already
-- fetched for this unit. Slots it covers are taken from it -- fetched for this unit. Slots it covers are taken from it
-- (no second GetInventoryItemLink pass); slots outside its -- (no second GetInventoryItemLink pass); any slot H.SLOTS
-- coverage -- the waist, which the enchant tab has no column -- needs that rawLinks did not cover is fetched here instead.
-- for -- are fetched here.
-- hit : the store shape documented in the file header -- hit : the store shape documented in the file header
-- race : race token from UnitRace (2nd return, e.g. "NightElf"); -- race : race token from UnitRace (2nd return, e.g. "NightElf");
-- needed for the racial weapon-skill estimate of foreign -- needed for the racial weapon-skill estimate of foreign
+62 -6
View File
@@ -67,6 +67,11 @@ T.ASK_GAP = 5
-- do not ask the same player again for this long. Talents change on -- do not ask the same player again for this long. Talents change on
-- respec, which is rare and never mid-raid. -- respec, which is rare and never mid-raid.
T.REASK_AFTER = 1800 T.REASK_AFTER = 1800
-- how long an unfinished reply stays open. A whole reply lands within a
-- fraction of a second, so a line arriving this much later cannot belong to
-- it -- it is the start of a NEW reply and must not be folded into the old
-- accumulator (see T.AccStale).
T.ACC_TTL = 10
-- ================================================================== -- ==================================================================
-- PURE SECTION (offline-testable) -- PURE SECTION (offline-testable)
@@ -147,9 +152,24 @@ end
-- spent = <sum of pointsSpent over the trees that reported>, -- spent = <sum of pointsSpent over the trees that reported>,
-- sumRank = <sum of every rank we stored>, -- sumRank = <sum of every rank we stored>,
-- trees = <how many tab lines arrived>, -- trees = <how many tab lines arrived>,
-- complete = <INSTalentEND seen> } -- complete = <INSTalentEND seen>,
function T.NewAcc() -- at = <when the first line arrived, nil offline> }
return { ranks = {}, spent = 0, sumRank = 0, trees = 0, complete = false } function T.NewAcc(now)
return { ranks = {}, spent = 0, sumRank = 0, trees = 0, complete = false,
at = now }
end
-- Has this accumulator been waiting so long that the next line must belong to
-- a different reply? Only the END marker drops an accumulator, so a reply
-- whose tail was lost would otherwise stay open forever and swallow the next
-- one: tab lines add up twice while ranks are deduplicated by name, so
-- T.Plausible rejects a perfectly good retry. Without a clock (offline) or an
-- opening stamp nothing expires -- the old behaviour, unchanged.
function T.AccStale(acc, now)
if not acc or not acc.at or not now then
return false
end
return (now - acc.at) > T.ACC_TTL
end end
-- Fold one parsed line into the accumulator. Returns the accumulator so -- Fold one parsed line into the accumulator. Returns the accumulator so
@@ -281,6 +301,9 @@ if CreateFrame then
return ok return ok
end end
-- set below, once the frame exists: arms the paced sender
local arm
-- Called by the scan with the players it can see. Nothing is sent here -- Called by the scan with the players it can see. Nothing is sent here
-- -- names are only lined up; the timer below paces them out. -- -- names are only lined up; the timer below paces them out.
function T.Request(names) function T.Request(names)
@@ -296,6 +319,9 @@ if CreateFrame then
table.insert(queue, n) table.insert(queue, n)
end end
end end
if arm and table.getn(queue) > 0 then
arm()
end
end end
local f = CreateFrame("Frame", "DopingControlTalentFrame") local f = CreateFrame("Frame", "DopingControlTalentFrame")
@@ -313,9 +339,14 @@ if CreateFrame then
if not kind then if not kind then
return return
end end
local now = GetTime()
local a = acc[sender] local a = acc[sender]
if a and T.AccStale(a, now) then
-- the previous reply lost its END line; this one starts fresh
a = nil
end
if not a then if not a then
a = T.NewAcc() a = T.NewAcc(now)
acc[sender] = a acc[sender] = a
end end
T.Feed(a, kind, data) T.Feed(a, kind, data)
@@ -338,14 +369,26 @@ if CreateFrame then
-- queue the scan filled. Deliberately a plain timer rather than a burst -- queue the scan filled. Deliberately a plain timer rather than a burst
-- after each scan -- 40 requests in one frame is what a rate limit -- after each scan -- 40 requests in one frame is what a rate limit
-- would punish, and nothing documents where that limit sits. -- would punish, and nothing documents where that limit sits.
--
-- It is installed only while there is something to send and takes itself
-- off again the moment the queue runs dry (or the feature is switched
-- off): an OnUpdate runs 60+ times a second for the whole session, and a
-- player who never scans must not pay for a queue that is always empty.
-- Re-arming needs no extra wiring -- every scan calls T.Request.
local elapsed = 0 local elapsed = 0
f:SetScript("OnUpdate", function()
local function disarm()
f:SetScript("OnUpdate", nil)
end
local function drain()
elapsed = elapsed + (arg1 or 0) elapsed = elapsed + (arg1 or 0)
if elapsed < 1 then if elapsed < 1 then
return return
end end
elapsed = 0 elapsed = 0
if not enabled() or table.getn(queue) == 0 then if not enabled() or table.getn(queue) == 0 then
disarm()
return return
end end
local now = GetTime() local now = GetTime()
@@ -359,10 +402,23 @@ if CreateFrame then
T.Ask(name) T.Ask(name)
end end
end end
end) if table.getn(queue) == 0 then
disarm()
end
end
arm = function()
if f:GetScript("OnUpdate") then
return
end
-- a long idle gap must not fire an immediate burst
elapsed = 0
f:SetScript("OnUpdate", drain)
end
-- forget everything (respec, or a deliberate re-read) -- forget everything (respec, or a deliberate re-read)
function T.Clear() function T.Clear()
disarm()
acc = {} acc = {}
ranks = {} ranks = {}
readAt = {} readAt = {}
Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

+118
View File
@@ -0,0 +1,118 @@
-- DopingControl ui/cellsheet.lua
--
-- The matrix draws one small box per cell: a solid background with a 1px
-- border whose color carries the state. Done with SetBackdrop that costs NINE
-- texture regions per cell (measured in the client: bare frame 0, backdrop
-- with bgFile only 1, backdrop with edgeFile 9). At 805 cells that is ~7200
-- regions of pure decoration.
--
-- Instead every box is baked into one sheet and a cell owns a single texture
-- region that picks its box with SetTexCoord. The border is part of the image,
-- so the result is pixel-identical -- the sheet is generated from the same
-- DC.COLORS palette by tools/gen-cellsheet.js.
--
-- Pure Lua on purpose: no WoW API at file scope, so the offline tests can
-- dofile it under lua50.exe.
DC_Cells = {}
DC_Cells.SHEET = {
path = "Interface\\AddOns\\DopingControl\\textures\\cells",
size = 256, -- power of two, mandatory: a 12x12 TGA fails silently
tileW = 64, -- holds the widest sprite (34) with room to spare
tileH = 32, -- holds CELL_H (23)
perRow = 4, -- 256 / 64
}
-- Cell height never varies; only the width does (trinket columns are 34).
DC_Cells.CELL_H = 23
DC_Cells.WIDTHS = { 30, 34 }
-- Every (background, border) pair a CELL can take, in sheet order. Borders
-- given as a number are DC.QUALITY indices, everything else is a DC.COLORS
-- key. Order is the wire format of the sheet: appending is safe, reordering
-- means regenerating the TGA.
DC_Cells.VARIANTS = {
{ name = "HAS", bg = "hatBg", border = "hatBorder" },
{ name = "MISSING", bg = "fehltBg", border = "fehltBorder" },
{ name = "UNKNOWN", bg = "unbekBg", border = "unbekBorder" },
{ name = "SKIP", bg = "skip", border = "skipBorder" },
{ name = "NEUTRAL", bg = "theadBg", border = "line" },
{ name = "ITEM_HAS", bg = "theadBg", border = "hatBorder" },
{ name = "ITEM_MISSING", bg = "theadBg", border = "fehltBorder" },
{ name = "Q_RARE", bg = "theadBg", border = "rareBorder" },
{ name = "Q_EPIC", bg = "theadBg", border = "epicBorder" },
{ name = "Q_0", bg = "theadBg", border = 0 },
{ name = "Q_1", bg = "theadBg", border = 1 },
{ name = "Q_2", bg = "theadBg", border = 2 },
{ name = "Q_5", bg = "theadBg", border = 5 },
{ name = "Q_6", bg = "theadBg", border = 6 },
}
DC_Cells.INDEX = {}
for i = 1, table.getn(DC_Cells.VARIANTS) do
DC_Cells.INDEX[DC_Cells.VARIANTS[i].name] = i
end
-- Tile slot of a variant/width pair: each variant owns two consecutive tiles,
-- one per width, in DC_Cells.WIDTHS order.
local function slotOf(name, w)
local vi = DC_Cells.INDEX[name]
if not vi then
return nil
end
for k = 1, table.getn(DC_Cells.WIDTHS) do
if DC_Cells.WIDTHS[k] == w then
return (vi - 1) * table.getn(DC_Cells.WIDTHS) + (k - 1)
end
end
return nil
end
DC_Cells.SlotOf = slotOf
-- SetTexCoord arguments for one box. Returns four numbers or nil -- nil so a
-- typo shows up as a missing box instead of silently sampling tile 0.
function DC_Cells.Coords(name, w)
local slot = slotOf(name, w)
if not slot then
return nil
end
local S = DC_Cells.SHEET
local col = math.mod(slot, S.perRow)
local row = math.floor(slot / S.perRow)
local x0 = col * S.tileW
local y0 = row * S.tileH
return x0 / S.size,
(x0 + w) / S.size,
y0 / S.size,
(y0 + DC_Cells.CELL_H) / S.size
end
-- State + slot kind + item quality -> variant name. Mirrors exactly what the
-- old SetBackdropG call sites in ui/matrix.lua chose -- which, for a nil
-- quality, differed BY KIND (see ui/matrix.lua's M.PaintItemTile, old lines
-- 1965-1990, confirmed at tools/luatests/test_cellsheet.lua):
-- * trinket: M.qualityColors(item.quality) ran unconditionally, and for
-- quality == nil that resolves to DC.QUALITY[1] (common/white) via the
-- `q or 1` fallback -- state-INDEPENDENT.
-- * ench: quality == nil skipped M.qualityColors and fell back to
-- C.hatBorder/C.fehltBorder/C.line BY STATE -- exactly
-- ITEM_HAS/ITEM_MISSING/NEUTRAL.
-- So trinket's nil-quality case must NOT share ench's state-based fallback.
function DC_Cells.VariantFor(state, kind, quality)
if kind == "ench" or kind == "trinket" then
if quality == 3 then return "Q_RARE" end
if quality == 4 then return "Q_EPIC" end
if quality ~= nil then return "Q_" .. quality end
if kind == "trinket" then
return "Q_1"
end
if state == "HAS" then return "ITEM_HAS" end
if state == "MISSING" then return "ITEM_MISSING" end
return "NEUTRAL"
end
if state == "HAS" then return "HAS" end
if state == "MISSING" then return "MISSING" end
if state == "UNKNOWN" then return "UNKNOWN" end
return "SKIP"
end
+731 -58
View File
File diff suppressed because it is too large Load Diff
+68 -10
View File
@@ -149,10 +149,12 @@ O.GRID_LABEL_W = 74 -- role/class label column (expectation grid)
O.BOOK_NAME_W = 96 -- player-name column (skill book grid) O.BOOK_NAME_W = 96 -- player-name column (skill book grid)
O.ROW_H = 22 -- one grid row's height, both grids O.ROW_H = 22 -- one grid row's height, both grids
-- equipment has the most configurable slots (11 armor enchants + neck + -- consumables (v6 slot model) has the most configurable slots (22 incl.
-- 2 rings; trinkets are item tiles and never configurable, see -- the five school protection columns);
-- slotsForTab) -- the widest grid the pools have to serve. -- equipment follows with 14 (11 armor enchants + neck + 2 rings --
O.MAX_SLOT_COLS = 14 -- trinkets are item tiles and never configurable, see slotsForTab).
-- The widest grid the pools have to serve.
O.MAX_SLOT_COLS = 22
-- x-offset of the 20 px check button inside its O.CELL_W-wide column -- x-offset of the 20 px check button inside its O.CELL_W-wide column
O.CELL_INSET = math.floor((O.CELL_W - 20) / 2) O.CELL_INSET = math.floor((O.CELL_W - 20) / 2)
@@ -489,6 +491,17 @@ function O.ClassTitle(class)
.. string.lower(string.sub(class, 2)) .. string.lower(string.sub(class, 2))
end end
-- Label of the demo-mode button. Demo mode is TRANSIENT (core/demo.lua:
-- never SavedVariables), so it gets a self-relabeling button rather than a
-- checkbox -- the checkbox row above is bound to db fields, and a checkbox
-- whose state dies on reload would read like a broken setting.
function O.DemoButtonLabel()
if DC.demoMode then
return "Demo mode: ON (click to stop)"
end
return "Demo mode: OFF (click to start)"
end
-- ================================================================== -- ==================================================================
-- WOW SECTION -- frame, widgets, wiring -- WOW SECTION -- frame, widgets, wiring
-- ================================================================== -- ==================================================================
@@ -504,7 +517,7 @@ if CreateFrame then
-- O.GRID_LABEL_W label column O.COL_PITCH column pitch -- O.GRID_LABEL_W label column O.COL_PITCH column pitch
-- O.BOOK_NAME_W player-name column O.ROW_H row pitch -- O.BOOK_NAME_W player-name column O.ROW_H row pitch
-- O.CELL_INSET check button inset O.MAX_SLOT_COLS widest grid -- O.CELL_INSET check button inset O.MAX_SLOT_COLS widest grid
-- The two grids differ in column count (14 equipment slots vs. 15 -- The two grids differ in column count (22 consumable slots vs. 15
-- weapon skills); the frame is BUILT at the wider of the two and -- weapon skills); the frame is BUILT at the wider of the two and
-- narrowed per tab by ApplyEditorWidth, so no widget is ever created -- narrowed per tab by ApplyEditorWidth, so no widget is ever created
-- against a width it later has to grow past. -- against a width it later has to grow past.
@@ -558,9 +571,9 @@ if CreateFrame then
elseif tab == "CLASSBUFFS" then elseif tab == "CLASSBUFFS" then
return DC.SLOTS_CLASSBUFFS or {} return DC.SLOTS_CLASSBUFFS or {}
elseif tab == "EQUIPMENT" then elseif tab == "EQUIPMENT" then
-- only kind=="ench" slots are configurable here (14: the 11 -- only kind=="ench" slots are configurable here (15: the 11
-- armor slots + neck/rings, which are enchantable on this -- armor slots + neck/rings/waist, which are enchantable on
-- server -- data/enchants.lua). T1/T2 (trinket item tiles) -- this server -- data/enchants.lua). T1/T2 (trinket item tiles)
-- have no enchant question to ask and never appear: the -- have no enchant question to ask and never appear: the
-- matrix always shows them (item present/absent decides, -- matrix always shows them (item present/absent decides,
-- expectations are never consulted for tiles). -- expectations are never consulted for tiles).
@@ -1536,6 +1549,42 @@ if CreateFrame then
place(cbTest, -28) place(cbTest, -28)
AddTooltip(cbTest, "Replace the scan with a fixed simulated raid.\nChat outputs are disabled while active.") AddTooltip(cbTest, "Replace the scan with a fixed simulated raid.\nChat outputs are disabled while active.")
-- Demo mode sits right below the test-mode row: same idea (the
-- simulated raid), plus a transient expectation layer that shows
-- all six school protection columns at once. A BUTTON, not a
-- checkbox -- the state is never saved, so it relabels itself
-- instead of pretending to be a stored setting.
local btnDemo = CreateButton(f, "DopingControlOptDemoBtn",
O.DemoButtonLabel(), 200,
function()
-- Capture the widget BEFORE SetDemoMode runs. `this` is a
-- plain global the 1.12 dispatcher sets per script call and
-- never restores: SetDemoMode synchronously drives
-- DC_Matrix.Refresh -> M.Render, whose leftover-hide loop
-- Hide()s pooled rows and group heads that carry their own
-- OnHide handlers (ui/matrix.lua M.RowOnHide /
-- M.GroupHeadOnHide -- they read `this` themselves). After
-- that nested dispatch `this` is the last-hidden widget, and
-- a row frame is a plain Frame with no SetText. Same order
-- the other handlers in this file already keep
-- (GridCellOnClick, ResetOnClick): finish the `this` reads
-- and writes before refreshing anything.
local btn = this
if DC.SetDemoMode then
DC.SetDemoMode(not DC.demoMode)
end
btn:SetText(O.DemoButtonLabel())
end)
place(btnDemo, -28)
AddTooltip(btnDemo, "Simulated raid with ALL SIX protection columns"
.. " (fire, frost, nature, shadow, holy, arcane) shown at once."
.. "\nYour saved expectations are left untouched, and demo mode"
.. " is never saved - it ends with the next reload."
-- the demo layer is snapshotted on entry (core/demo.lua), so
-- grid edits made while it is armed do not reach the matrix
.. "\nThe view is a snapshot: restart demo mode to pick up"
.. " expectation changes made while it is running.")
local cbMinimap = CreateCheckbox(f, "Show minimap button", local cbMinimap = CreateCheckbox(f, "Show minimap button",
function() return db().showMinimapButton end, function() return db().showMinimapButton end,
function(v) function(v)
@@ -1788,6 +1837,11 @@ if CreateFrame then
end end
RefreshEditor() -- grid/books pools + tab visibility + buttons RefreshEditor() -- grid/books pools + tab visibility + buttons
ResetDisarm(getglobal("DopingControlOptResetBtn")) ResetDisarm(getglobal("DopingControlOptResetBtn"))
-- demo mode can have been toggled by /dc demo meanwhile
local bd = getglobal("DopingControlOptDemoBtn")
if bd then
bd:SetText(O.DemoButtonLabel())
end
end) end)
-- ESC closes -- ESC closes
@@ -1827,14 +1881,18 @@ if CreateFrame then
f:Show() f:Show()
end end
-- external state changes (/dc test, minimap toggle) can resync an -- external state changes (/dc test, /dc demo, minimap toggle) can
-- open options window -- resync an open options window
function O.Refresh() function O.Refresh()
if frame and frame:IsShown() then if frame and frame:IsShown() then
for i = 1, table.getn(checkboxes) do for i = 1, table.getn(checkboxes) do
local cb = checkboxes[i] local cb = checkboxes[i]
cb:SetChecked(cb.dcGet() and 1 or nil) cb:SetChecked(cb.dcGet() and 1 or nil)
end end
local bd = getglobal("DopingControlOptDemoBtn")
if bd then
bd:SetText(O.DemoButtonLabel())
end
RefreshEditor() -- a fresh scan can add or drop book rows RefreshEditor() -- a fresh scan can add or drop book rows
end end
end end
+29 -2
View File
@@ -59,6 +59,22 @@ function R.SendDisabled(source)
return source == "sim" return source == "sim"
end end
-- The chat API rejects the WHOLE message with "Invalid escape code in chat
-- message" when it meets a "|" that does not open a valid escape sequence
-- (|c |r |H |h |T |t |n ||). Our header line uses " | " as a plain ASCII
-- separator, so every report was refused. "||" is the escape for a literal
-- pipe and still renders as a single "|", which is why this runs at the
-- send point and NOT inside DC_Model.reportLines: the preview shows what
-- reportLines returns and must keep showing the text as it APPEARS in chat.
-- Report lines are pure ASCII by design (no item links, no colour codes),
-- so doubling every pipe cannot damage a real escape sequence here.
function R.ChatSafe(line)
if not line then
return line
end
return (string.gsub(line, "|", "||"))
end
-- Create a drain queue from an array of lines. Takes an independent copy -- Create a drain queue from an array of lines. Takes an independent copy
-- (the preview may be rebuilt while an old queue is still draining). -- (the preview may be rebuilt while an old queue is still draining).
function R.QueueCreate(lines) function R.QueueCreate(lines)
@@ -135,7 +151,18 @@ end
function R.BuildLines(store, dbt, tab) function R.BuildLines(store, dbt, tab)
store = store or DC.store or { players = {} } store = store or DC.store or { players = {} }
tab = tab or "CONSUMABLES" tab = tab or "CONSUMABLES"
local expect = (dbt and dbt.expectations) or DC.DEFAULT_EXPECT or {} -- Same expectation resolver the matrix uses, or the report preview
-- would show different columns and different gap totals than the grid
-- it was opened from (demo mode). Safe for the SEND path too: the demo
-- layer only applies over the "sim" store, and R.SendDisabled blocks
-- sending on exactly that source. Guarded because ui/report.lua is
-- loadable without ui/matrix.lua (tools/luatests/test_report_queue.lua).
local expect
if DC_Matrix and DC_Matrix.ExpectTable then
expect = DC_Matrix.ExpectTable(dbt)
else
expect = (dbt and dbt.expectations) or DC.DEFAULT_EXPECT or {}
end
local function roleOf(p) local function roleOf(p)
return DC_Roles.resolve(dbt, store.source, p) return DC_Roles.resolve(dbt, store.source, p)
end end
@@ -234,7 +261,7 @@ if CreateFrame then
end end
local line = R.QueueTick(queue, GetTime(), currentSource()) local line = R.QueueTick(queue, GetTime(), currentSource())
if line then if line then
SendChatMessage(line, R.CHAT_TYPE) SendChatMessage(R.ChatSafe(line), R.CHAT_TYPE)
UpdateSendState() UpdateSendState()
end end
if queue.done or queue.aborted then if queue.done or queue.aborted then
+202 -12
View File
@@ -44,6 +44,88 @@ W.SOLID_BACKDROP = {
W.WHITE = { r = 1, g = 1, b = 1 } W.WHITE = { r = 1, g = 1, b = 1 }
-- ------------------------------------------------------------------
-- Cell-box fallback palette: variant name -> (bg, border) color reference,
-- used ONLY when ui/cellsheet.lua's baked sheet is unavailable (see
-- W.HasCellSheet below). `border` is a DC.COLORS key, or a number that
-- indexes DC.QUALITY -- same convention as DC_Cells.VARIANTS in
-- ui/cellsheet.lua, which this table intentionally mirrors: colors are
-- pulled LIVE from DC.COLORS/DC.QUALITY (core/const.lua, already loaded --
-- ui/cellsheet.lua only bakes that same palette into a texture, it does
-- not own it), so nothing here duplicates a color VALUE. The pairing
-- itself (which key goes with which variant) can't be derived
-- mechanically -- it is the one piece of information that has to exist
-- twice given the failure mode this guards (ui/cellsheet.lua not loaded
-- at all). tools/luatests/test_widgets.lua cross-checks every entry
-- against the real DC_Cells.VARIANTS so the two cannot silently drift.
W.CELL_FALLBACK_VARIANTS = {
HAS = { bg = "hatBg", border = "hatBorder" },
MISSING = { bg = "fehltBg", border = "fehltBorder" },
UNKNOWN = { bg = "unbekBg", border = "unbekBorder" },
SKIP = { bg = "skip", border = "skipBorder" },
NEUTRAL = { bg = "theadBg", border = "line" },
ITEM_HAS = { bg = "theadBg", border = "hatBorder" },
ITEM_MISSING = { bg = "theadBg", border = "fehltBorder" },
Q_RARE = { bg = "theadBg", border = "rareBorder" },
Q_EPIC = { bg = "theadBg", border = "epicBorder" },
Q_0 = { bg = "theadBg", border = 0 },
Q_1 = { bg = "theadBg", border = 1 },
Q_2 = { bg = "theadBg", border = 2 },
Q_5 = { bg = "theadBg", border = 5 },
Q_6 = { bg = "theadBg", border = 6 },
}
-- ------------------------------------------------------------------
-- DC_Cells fallback shim -- covers a mid-session /reload where THIS file
-- (ui/widgets.lua, an EXISTING file whose new content /reload happily
-- re-executes) deploys, but ui/cellsheet.lua (a NEW file added in the same
-- change) does not: 1.12's /reload re-runs existing files but never loads
-- a file that was not already part of the running session (verified
-- in-game). Left alone, DC_Cells stays nil, and it is not only
-- W.MakeCell/W.SetCellBox below that touch it -- ui/matrix.lua calls
-- DC_Cells.VariantFor(...) directly at two paint sites (M.PaintItemTile
-- and the legacy trinket path, ui/matrix.lua ~2182/2381 -- out of scope
-- for this change) and would error on the very first painted item/trinket
-- cell regardless of anything done here.
--
-- The shim supplies ONLY the pure classification function -- state/kind/
-- quality -> variant NAME string -- no sheet, no texture, no coordinate
-- math, because none of that can work without ui/cellsheet.lua's baked
-- sheet file. It is a verbatim copy of DC_Cells.VariantFor's logic (see
-- ui/cellsheet.lua); test_widgets.lua cross-checks the two stay in sync.
-- Installs only when DC_Cells does not already exist, so the normal
-- (full-restart) load path -- where ui/cellsheet.lua runs before this file
-- per DopingControl.toc and sets the real DC_Cells -- is untouched.
if not DC_Cells then
DC_Cells = {
VariantFor = function(state, kind, quality)
if kind == "ench" or kind == "trinket" then
if quality == 3 then return "Q_RARE" end
if quality == 4 then return "Q_EPIC" end
if quality ~= nil then return "Q_" .. quality end
if kind == "trinket" then
return "Q_1"
end
if state == "HAS" then return "ITEM_HAS" end
if state == "MISSING" then return "ITEM_MISSING" end
return "NEUTRAL"
end
if state == "HAS" then return "HAS" end
if state == "MISSING" then return "MISSING" end
if state == "UNKNOWN" then return "UNKNOWN" end
return "SKIP"
end,
}
end
-- Whether ui/cellsheet.lua's baked sheet is actually available (as
-- opposed to the shim above, which never sets .SHEET). One seam shared by
-- W.MakeCell and W.SetCellBox, and directly testable offline without
-- CreateFrame (unlike W.MakeCell itself).
function W.HasCellSheet()
return DC_Cells ~= nil and DC_Cells.SHEET ~= nil
end
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
-- Font helper (we keep OUR pixel -- Font helper (we keep OUR pixel
-- size -- the matrix layout depends on it -- and only take pfUI's font -- size -- the matrix layout depends on it -- and only take pfUI's font
@@ -125,28 +207,117 @@ end
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
-- Cell factory: 30x23 state cell (the 30x23 size is part of the fixed -- Cell factory: 30x23 state cell (the 30x23 size is part of the fixed
-- visual layout). Carries one texture region (check/cross glyph OR trinket -- visual layout). The box (background + 1px border) is one texture region
-- item icon) and one FontString (?, skip dot, trinket monogram); the -- sampled from the baked sheet in ui/cellsheet.lua -- SetBackdrop with an
-- matrix paint code shows exactly one of them per state. -- edgeFile costs NINE texture regions per cell, this costs one. The icon
-- (check/cross glyph OR trinket item icon) and FontString (?, skip dot,
-- trinket monogram) are created on demand via W.CellIcon / W.CellText;
-- the matrix paint code shows exactly one of them per state.
-- Pool children stay anonymous (documented factory exception). -- Pool children stay anonymous (documented factory exception).
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
function W.MakeCell(parent, name, w, h) function W.MakeCell(parent, name, w, h)
local c = CreateFrame("Frame", name, parent) local c = CreateFrame("Frame", name, parent)
c:SetWidth(w or 30) c:SetWidth(w or 30)
c:SetHeight(h or 23) c:SetHeight(h or 23)
c:SetBackdrop(W.SOLID_BACKDROP)
c:EnableMouse(true) c:EnableMouse(true)
c.icon = c:CreateTexture(nil, "ARTWORK") c.dcW = w or 30
c.icon:SetPoint("CENTER", c, "CENTER", 0, 0) if W.HasCellSheet() then
c.icon:SetWidth(16) -- The state box (background + 1px border) comes from one baked sheet:
c.icon:SetHeight(16) -- SetBackdrop with an edgeFile costs NINE texture regions per cell,
c.icon:Hide() -- this costs one. See ui/cellsheet.lua.
c.text = c:CreateFontString(nil, "OVERLAY") c.boxTex = c:CreateTexture(nil, "BACKGROUND")
W.ApplyFont(c.text, 9) c.boxTex:SetAllPoints(c)
c.text:SetPoint("CENTER", c, "CENTER", 0, 0) c.boxTex:SetTexture(DC_Cells.SHEET.path)
-- Without an initial texcoord the region would show the WHOLE sheet
-- stretched across the cell until the first paint. Start on NEUTRAL.
local l, r, t, b = DC_Cells.Coords("NEUTRAL", c.dcW)
if l then
c.boxTex:SetTexCoord(l, r, t, b)
c.dcBoxVariant = "NEUTRAL"
c.dcBoxW = c.dcW
end
else
-- Fallback: ui/cellsheet.lua did not load this session (see
-- W.HasCellSheet above) -- pre-refactor SetBackdrop path (no
-- boxTex region at all) so the matrix still draws instead of
-- erroring on a nil DC_Cells.SHEET. W.SetBackdropG and
-- W.SOLID_BACKDROP are untouched by the sprite refactor, so this
-- is exactly what W.MakeCell did before it.
c:SetBackdrop(W.SOLID_BACKDROP)
W.SetCellBox(c, "NEUTRAL")
end
return c return c
end end
-- Icon and FontString on demand: a cell shows one or the other, and in 1.12
-- eagerly created regions cost even while hidden.
function W.CellIcon(cell)
if not cell.icon then
cell.icon = cell:CreateTexture(nil, "ARTWORK")
cell.icon:SetPoint("CENTER", cell, "CENTER", 0, 0)
cell.icon:SetWidth(16)
cell.icon:SetHeight(16)
cell.icon:Hide()
end
return cell.icon
end
function W.CellText(cell)
if not cell.text then
cell.text = cell:CreateFontString(nil, "OVERLAY")
W.ApplyFont(cell.text, 9)
cell.text:SetPoint("CENTER", cell, "CENTER", 0, 0)
end
return cell.text
end
-- Guards on variant AND width (cell.dcBoxW, the width the box was last
-- painted at) -- NOT variant alone. ui/matrix.lua resizes a pooled cell in
-- place when its column width changes (trinket columns are 34, normal
-- cells 30: see ui/matrix.lua's column-layout path around SetWidth(col.w))
-- and only ever writes cell.dcW; it has no notion of dcBoxVariant and isn't
-- expected to invalidate it. If this guard only checked variant, a cell
-- that kept its variant across such a resize would keep a stale texcoord
-- sized for the OLD width -- the baked border would sit off the new cell
-- edge. Tracking dcBoxW here keeps the guard self-contained: correctness
-- does not depend on any caller convention.
function W.SetCellBox(cell, variant)
if not W.HasCellSheet() then
-- Fallback: same pre-refactor SetBackdrop recipe, colors pulled
-- LIVE from DC.COLORS/DC.QUALITY via W.CELL_FALLBACK_VARIANTS
-- above. Never touches cell.boxTex -- a fallback cell (see
-- W.MakeCell) does not have one. W.SetBackdropG already
-- change-guards by table reference, so no separate
-- dcBoxVariant/dcBoxW bookkeeping is needed here.
local v = W.CELL_FALLBACK_VARIANTS[variant]
if not v then
return
end
local bg = DC.COLORS[v.bg]
local border = v.border
if type(border) == "number" then
border = DC.QUALITY[border]
else
border = DC.COLORS[border]
end
if not bg or not border then
return
end
W.SetBackdropG(cell, bg, border)
return
end
if cell.dcBoxVariant == variant and cell.dcBoxW == cell.dcW then
return
end
local l, r, t, b = DC_Cells.Coords(variant, cell.dcW)
if not l then
return
end
cell.dcBoxVariant = variant
cell.dcBoxW = cell.dcW
cell.boxTex:SetTexCoord(l, r, t, b)
end
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------
-- Pill factory (ready pill, coverage pill): bordered mini frame with a -- Pill factory (ready pill, coverage pill): bordered mini frame with a
-- centered FontString; width is adjusted by the caller to fit the text. -- centered FontString; width is adjusted by the caller to fit the text.
@@ -247,6 +418,21 @@ function W.TipDouble(left, right, c, cr)
GameTooltip:AddDoubleLine(left, right, r, g, b, r2, g2, b2) GameTooltip:AddDoubleLine(left, right, r, g, b, r2, g2, b2)
end end
-- W.dcTipOwner: our own record of which of OUR frames currently owns
-- GameTooltip. WoW 1.12's GameTooltip has no "GetOwner" method (confirmed
-- against FrameXML-1.12.1: zero real hits -- the only "GetOwner" match
-- anywhere in that tree is the unrelated global function
-- GetOwnerAuctionItems). Every caller in this addon that ever needs "who
-- currently owns the tooltip" goes through W.AttachTooltip to open one in
-- the first place, so this is the single place that needs to record it.
-- Set right after SetOwner
-- (matching order: SetOwner, then the builder runs, then Show -- a builder
-- that itself re-SetOwner()s the SAME frame, like M.CellTooltip's
-- ANCHOR_LEFT override, does not change who owns it). Cleared in OnLeave,
-- guarded on identity so a late/out-of-order OnLeave can never clobber a
-- newer owner.
W.dcTipOwner = nil
function W.AttachTooltip(frame, builder, anchor) function W.AttachTooltip(frame, builder, anchor)
frame.dcTipBuilder = builder frame.dcTipBuilder = builder
frame.dcTipAnchor = anchor or "ANCHOR_LEFT" frame.dcTipAnchor = anchor or "ANCHOR_LEFT"
@@ -258,6 +444,7 @@ function W.AttachTooltip(frame, builder, anchor)
end end
if GameTooltip and this.dcTipBuilder then if GameTooltip and this.dcTipBuilder then
GameTooltip:SetOwner(this, this.dcTipAnchor) GameTooltip:SetOwner(this, this.dcTipAnchor)
W.dcTipOwner = this
this.dcTipBuilder(this) this.dcTipBuilder(this)
GameTooltip:Show() GameTooltip:Show()
end end
@@ -266,6 +453,9 @@ function W.AttachTooltip(frame, builder, anchor)
if this.dcOldLeave then if this.dcOldLeave then
this.dcOldLeave() this.dcOldLeave()
end end
if W.dcTipOwner == this then
W.dcTipOwner = nil
end
if GameTooltip then if GameTooltip then
GameTooltip:Hide() GameTooltip:Hide()
end end