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
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
A fix release. Everything here is something that was supposed to work
+7 -1
View File
@@ -1,7 +1,8 @@
## Interface: 11200
## Title: DopingControl
## Notes: Raid consumables, buffs, debuffs, resistances, hit and enchant checker
## Version: 0.6.1
## Author: ShempError
## Version: 0.6.4
## SavedVariables: DopingControlDB
core\const.lua
@@ -16,6 +17,7 @@ data\resist.lua
core\model.lua
core\roles.lua
core\sim.lua
core\demo.lua
scan\talents.lua
scan\reach.lua
scan\aura.lua
@@ -24,9 +26,13 @@ scan\resist.lua
scan\hit.lua
scan\engine.lua
core\probe.lua
ui\cellsheet.lua
ui\widgets.lua
ui\matrix.lua
ui\options.lua
ui\report.lua
features\minimap.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
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
**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 report` | opens the raid report preview |
| `/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 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 |
@@ -239,6 +247,10 @@ either way.
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
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
+38
View File
@@ -47,6 +47,44 @@ DC.DEFAULTS = {
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
-- recursively). No cycle handling -- config/expectation shapes are trees.
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
-- exactly two suggestion-badge cases).
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
DC.store = { players = {}, source = "scan" }
DC_Scan.Start(true) -- leaving test mode: immediate real rescan
@@ -108,7 +116,7 @@ end
-- ------------------------------------------------------------------
-- 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)
msg = msg or ""
@@ -119,9 +127,15 @@ function DC.HandleSlash(msg)
if cmd == "" then
DC_Matrix.Toggle()
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
-- real data never beats the simulation while test mode is on
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
DC_Scan.Start()
end
@@ -133,9 +147,28 @@ function DC.HandleSlash(msg)
DC.SetTestMode(not (db and db.testMode))
if db and db.testMode then
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
DC.Print("test mode OFF - rescanning the real group")
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
-- core/probe.lua owns the probe subcommand grammar:
-- DC_Probe.HandleSlash(rest) handles "" (battery),
@@ -222,7 +255,7 @@ if CreateFrame then
elseif event == "RAID_ROSTER_UPDATE" or event == "PARTY_MEMBERS_CHANGED" then
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()
end
+16
View File
@@ -850,6 +850,22 @@ function M.classify(player, slotdef, expected, books)
for name in pairs(names) do
if nameMapsToSlot(name, slotdef.id) 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
if textures then
icon = textures[name]
+194 -56
View File
@@ -35,6 +35,27 @@
-- (built programmatically below, so id-matched and name-matched
-- players are both covered), and every gear entry TABLE carries
-- 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
-- -- Simrogue + Simtree share "Resurrection Sickness" (afflicted=2 =>
-- 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.
-- cleanAuras() returns a FRESH table on every call (set() already builds
-- 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 = {
"Flask of the Titans", "Well Fed", "Juju Might", "Juju Power",
"Elixir of the Mongoose", "Greater Arcane Elixir", "Mageblood Potion",
"Elixir of Superior Defense", "Elixir of Fortitude",
"Elixir of the Mongoose", "R.O.I.D.S.", "Spirit of Zanza",
"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",
"Shadow Protection", "Arcane Intellect", "Soulstone Resurrection",
"Prayer of Shadow Protection", "Arcane Intellect",
"Soulstone Resurrection",
}
local function cleanAuras()
@@ -225,31 +262,59 @@ local ICONS = {
["Juju Power"] = "Interface\\Icons\\INV_Misc_MonsterScales_11",
["Elixir of 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
["Elixir of the Mongoose"] = "Interface\\Icons\\INV_Potion_32",
["Elixir of 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",
["Arcane Elixir"] = "Interface\\Icons\\INV_Potion_30",
["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",
["Dreamtonic"] = "Interface\\Icons\\INV_Potion_10",
["Dreamshard Elixir"] = "Interface\\Icons\\INV_Potion_12",
-- MP5 (both TWoW name variants share the item icon)
["Mageblood Potion"] = "Interface\\Icons\\INV_Potion_45",
["Mageblood"] = "Interface\\Icons\\INV_Potion_45",
-- ARM
["Elixir of Superior Defense"] = "Interface\\Icons\\INV_Potion_66",
["Elixir of Greater Defense"] = "Interface\\Icons\\INV_Potion_43",
-- HP
-- HPELX
["Elixir of Fortitude"] = "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"] = "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
["Greater Fire Protection"] = "Interface\\Icons\\INV_Potion_24",
["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
["Arcane Intellect"] = "Interface\\Icons\\Spell_Holy_MagicalSentry",
["Arcane Brilliance"] = "Interface\\Icons\\Spell_Holy_ArcaneIntellect",
@@ -257,7 +322,17 @@ local ICONS = {
["Gift of the Wild"] = "Interface\\Icons\\Spell_Nature_GiftoftheWild",
["Power Word: Fortitude"] = "Interface\\Icons\\Spell_Holy_WordFortitude",
["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",
["Prayer of Shadow Protection"] = "Interface\\Icons\\Spell_Shadow_AntiShadow",
["Soulstone Resurrection"] = "Interface\\Icons\\Spell_Shadow_SoulGem",
}
@@ -335,9 +410,10 @@ local HIT_MAIN_SUB = {
DRUID = "Staves",
}
-- All 16 equipment slots the scan reads (11 enchant + neck + 2 rings +
-- 2 trinket invSlots).
local INV_SLOTS = { 1, 2, 3, 15, 5, 9, 10, 7, 8, 16, 17, 18, 11, 12, 13, 14 }
-- All 17 equipment slots the scan reads (12 armor enchant, WAIST
-- included -- 2026-08-30 owner decision, WAIST is worn equipment like
-- 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,
-- 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" },
[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" },
[6] = { name = "Simulated Girdle", quality = 3, texture = "Interface\\Icons\\INV_Belt_03" },
[9] = { name = "Simulated Bracers", quality = 3, texture = "Interface\\Icons\\INV_Bracer_13" },
[10] = { name = "Simulated Gauntlets", quality = 3, texture = "Interface\\Icons\\INV_Gauntlets_28" },
[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
-- name/quality/texture filled in from SLOT_BASE, everything else -- id,
-- 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
-- trinkets they ARE enchantable on this server (data/enchants.lua, kind
-- "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,
guid = "0xF530000000000001", subgroup = 1,
aurasRead = true,
auras = { names = {}, ids = set({ 17626, 15852, 16323, 17538,
11348, 3593, 17543, 9885, 10938, 10958 }) },
-- FLASK Titans, FOOD Chili, STR JujuPower, AGI Mongoose, ARM SupDef,
-- HP Fortitude, FR GFPP, MOTW r7, PWF r6, SPROT r3
auras = { names = {}, ids = set({ 17626, 15852, 17038, 16323,
17538, 10667, 24382, 11348, 3593, 25804, 17543, 17548,
9885, 10938, 10958 }) },
-- 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",
gearRead = true,
resistsRead = true, resists = { [2] = 315 },
@@ -476,10 +561,16 @@ function DC_Sim.buildStore()
guid = "0xF530000000000002", subgroup = 1,
aurasRead = true,
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",
"Greater Fire Protection", "Mark of the Wild",
"Power Word: Fortitude", "Shadow Protection" }), ids = {} },
"Medivh's Merlot", "Greater Fire Protection",
"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,
resistsRead = true, resists = { [2] = 240 },
-- hit: the BELOW-cap showcase -- a human swordsman, so the racial
@@ -505,10 +596,15 @@ function DC_Sim.buildStore()
guid = "0xF530000000000003", subgroup = 1,
aurasRead = true,
auras = { names = set({ "Well Fed" }),
ids = set({ 17629, 11405, 11334, 11349, 25804, 7233,
21849, 21562, 10957 }) },
-- FLASK ChromRes, STR Giants, AGI GreaterAgi, ARM GreaterDef,
-- HP RumseyBlack, FR FireProt, MOTW Gift, PWF Prayer, SPROT r2
ids = set({ 17629, 16329, 11405, 11334, 10669, 24382,
11349, 3593, 25804, 7233, 17544, 21849, 21562, 10957 }) },
-- FLASK ChromRes, AP JujuMight, STR Giants, AGI GreaterAgi,
-- 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,
resistsRead = true, resists = { [2] = 180 },
-- hit: the BOOK showcase -- an orc axeman (racial +5 => cap 7.0);
@@ -553,9 +649,13 @@ function DC_Sim.buildStore()
guid = "0xF530000000000005", subgroup = 2,
aurasRead = true,
auras = { names = set({ "Well Fed" }),
ids = set({ 16329, 17538, 16323, 3593, 9885, 10938 }) },
-- AP JujuMight, AGI Mongoose, STR JujuPower, HP Fortitude,
-- MOTW r7, PWF r6
ids = set({ 17626, 16329, 17538, 16323, 10692, 24382,
3593, 9885, 10938 }),
-- +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 }),
weaponMain = true, gearRead = true,
-- 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,
guid = "0xF530000000000007", subgroup = 2,
aurasRead = true,
auras = { names = set({ "Well Fed", "Juju Might", "Juju Power",
"Elixir of Greater Agility", "Elixir of Fortitude",
"Fire Protection", "Mark of the Wild",
"Power Word: Fortitude", "Shadow Protection" }), ids = {} },
auras = { names = set({ "Flask of the Titans", "Well Fed",
"Juju Might", "Juju Power", "Elixir of Greater Agility",
"Lung Juice Cocktail", "Spirit of Zanza",
"Elixir of Fortitude", "Fire Protection", "Mark of the Wild",
"Power Word: Fortitude",
"Prayer of Shadow Protection" }), ids = {} },
weaponMain = false, gearRead = true,
gear = gearWith({
[9] = { id = 5169, enchant = 0 }, -- wrist enchant missing
@@ -650,10 +752,15 @@ function DC_Sim.buildStore()
guid = "0xF530000000000009", subgroup = 2,
aurasRead = true,
auras = { names = set({ "Well Fed" }),
ids = set({ 17038, 11405, 17538, 25804, 17543,
9885, 10938, 10958 }) },
-- AP Firewater, STR Giants, AGI Mongoose, HP RumseyBlack, FR GFPP,
-- MOTW r7, PWF r6, SPROT r3
ids = set({ 17626, 17038, 11405, 17538, 10693, 24382,
25804, 17543, 17546, 9885, 10938, 10958 }) },
-- FLASK Titans, AP Firewater, STR Giants, AGI Mongoose,
-- 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,
gear = gearWith({
[17] = { id = 5197, enchant = 917, subType = "Shields", quality = 3 },
@@ -694,9 +801,12 @@ function DC_Sim.buildStore()
guid = "0xF53000000000000B", subgroup = 3, distance = 31,
aurasRead = true,
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",
"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,
-- 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
@@ -727,8 +837,15 @@ function DC_Sim.buildStore()
guid = "0xF53000000000000D", subgroup = 4,
aurasRead = true,
auras = { names = set({ "Well Fed" }),
ids = set({ 17628, 23028, 17539, 24363, 9885, 10938, 10958 }) },
-- FLASK Supreme, AI Brilliance, SP GAE, MP5 Mageblood, MOTW, PWF, SPROT
ids = set({ 17628, 23028, 17539, 21920, 45489, 45427, 24382,
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",
gearRead = true,
resistsRead = true, resists = { [4] = 85, [6] = 45 },
@@ -795,7 +912,9 @@ function DC_Sim.buildStore()
guid = "0xF530000000000010", subgroup = 5,
aurasRead = true,
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",
"Power Word: Fortitude" }),
ids = {} },
@@ -838,10 +957,12 @@ function DC_Sim.buildStore()
guid = "0xF530000000000012", subgroup = 5,
aurasRead = true,
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",
"Mark of the Wild", "Power Word: Fortitude",
"Shadow Protection" }), ids = {} },
"Prayer of Shadow Protection" }), ids = {} },
weaponMain = true, gearRead = true,
gear = gearWith({
[18] = { id = 5238, enchant = 0, subType = "Wands" },
@@ -868,10 +989,21 @@ function DC_Sim.buildStore()
guid = "0xF530000000000014", subgroup = 6,
aurasRead = true,
auras = { names = set({ "Well Fed" }),
ids = set({ 17627, 20765, 24363, 3593, 7233,
10157, 9885, 10938, 10958 }) },
-- FLASK Wisdom, SOUL r5, MP5 Mageblood, HP Fortitude, FR FireProt,
-- AI r5, MOTW r7, PWF r6, SPROT r3
ids = set({ 17627, 20765, 24363, 45427, 24382, 22790,
3593, 7233, 7245, 10157, 9885, 10938, 10958 }),
-- mid-ladder showcase: Le Fishe Au Chocolat resolves by its
-- 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,
gear = gearWith({
[17] = { id = 5257, enchant = 0, subType = "Miscellaneous",
@@ -895,7 +1027,8 @@ function DC_Sim.buildStore()
aurasRead = true,
auras = { names = set({ "Flask of Distilled Wisdom", "Well Fed",
"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 }),
weaponMain = false, gearRead = true,
gear = gearWith({
@@ -908,9 +1041,10 @@ function DC_Sim.buildStore()
guid = "0xF530000000000017", subgroup = 7,
aurasRead = true,
auras = { names = set({ "Well Fed" }),
ids = set({ 17627, 24363, 25804, 7233, 10156, 9885, 10938 }) },
-- FLASK Wisdom, MP5 Mageblood, HP RumseyBlack, FR FireProt,
-- AI r4, MOTW r7, PWF r6
ids = set({ 17627, 24363, 45427, 24382, 25804, 7233,
10156, 9885, 10938 }) },
-- FLASK Wisdom, MP5 Mageblood, SHARD, ZANZA, ALC RumseyBlack,
-- FR FireProt, AI r4, MOTW r7, PWF r6
weaponMain = true, gearRead = true,
gear = gearWith({
[17] = { id = 5277, enchant = 917, subType = "Shields", quality = 3 },
@@ -923,10 +1057,12 @@ function DC_Sim.buildStore()
guid = "0xF530000000000018", subgroup = 7,
aurasRead = true,
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",
"Mark of the Wild", "Power Word: Fortitude",
"Shadow Protection", "Soulstone Resurrection" }), ids = {} },
"Prayer of Shadow Protection",
"Soulstone Resurrection" }), ids = {} },
weaponMain = true, gearRead = true,
gear = gearWith({
[5] = { id = 5285, enchant = 0 },
@@ -935,16 +1071,18 @@ function DC_Sim.buildStore()
[18] = "EMPTY",
}) }
-- Simglow: HP via the id-less "Rumsey Rum" name entry (spellID 0) WHILE
-- ids are present -- exercises exactly the model rule that id-less
-- names still count for players with ids. SPROT missing, EMPTY ranged.
-- Simglow: ALC via the id-less "Medivh's Merlot Blue" name entry
-- (spellID 0) WHILE ids are present -- exercises exactly the model
-- rule that id-less names still count for players with ids.
-- SPROT missing, EMPTY ranged.
add{ name = "Simglow", class = "PALADIN", online = true,
guid = "0xF530000000000019", subgroup = 7,
aurasRead = true,
auras = { names = set({ "Well Fed", "Rumsey Rum" }),
ids = set({ 17627, 21850, 21564, 24363, 7233, 1461, 20762 }) },
-- FLASK Wisdom, MOTW Gift, PWF Prayer, MP5 Mageblood, FR FireProt,
-- AI r3, SOUL r2
auras = { names = set({ "Well Fed", "Medivh's Merlot Blue" }),
ids = set({ 17627, 21850, 21564, 24363, 45427, 24382,
7233, 1461, 20762 }) },
-- FLASK Wisdom, MOTW Gift, PWF Prayer, MP5 Mageblood, SHARD,
-- ZANZA, FR FireProt, AI r3, SOUL r2
weaponMain = true, gearRead = true,
gear = gearWith({
[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
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
-- 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.
--
-- 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 {}
local DC = DopingControl
@@ -8,116 +67,294 @@ local DC = DopingControl
-- ------------------------------------------------------------------
-- Slot definitions (array, order = column order).
-- Fields: id (stable key, used by DEFAULT_EXPECT / model / UI),
-- label (column tile abbrev), sub (tile sub-label), full (long name,
-- used in whisper/report), kind ("aura" | "weapon"),
-- items (tooltip example text, optional).
-- 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.
-- label (column tile abbrev), sub (tile sub-label), full (long
-- name, used in whisper/report), kind ("aura" | "weapon"),
-- items (tooltip example text, optional), unverified (optional,
-- see header),
-- icon (BARE slot-generic icon texture name -- same bare-name
-- rule as the entry icons above; the matrix HAS-cell fallback
-- ladder renders it when the ITEM behind a buff cannot be
-- resolved: resolved item icon -> this slot icon -> green check.
-- A representative member item's icon by design -- the cell must
-- read as "this column", not as a question mark).
-- ------------------------------------------------------------------
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" },
{ id = "FOOD", label = "FOD", sub = "Food", full = "Food buff", kind = "aura",
items = "Well Fed - e.g. Grilled Squid \194\183 Blessed Sunfruit" },
{ id = "AP", label = "AP", sub = "Attack", full = "Attack power", kind = "aura",
{ id = "FOOD", label = "FOD", sub = "Food", full = "Food buff", kind = "aura",
icon = "inv_misc_food_15",
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" },
{ 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" },
{ id = "AGI", label = "AGI", sub = "Agi.", full = "Agility", kind = "aura",
items = "Elixir of the Mongoose" },
{ id = "SP", label = "SP", sub = "Spell", full = "Spell power", kind = "aura",
items = "Greater Arcane Elixir \194\183 Shadow Power \194\183 Firepower" },
{ id = "MP5", label = "MP5", sub = "Mana", full = "Mana regen", kind = "aura",
items = "Mageblood Potion \194\183 Nightfin Soup" },
{ id = "ARM", label = "ARM", sub = "Armor", full = "Armor", kind = "aura",
{ id = "AGI", label = "AGI", sub = "Agi.", full = "Agility", kind = "aura",
icon = "inv_potion_32",
items = "Elixir of the Mongoose - free-stacker, own column" },
{ id = "BL", label = "BL", sub = "Blasted", full = "Blasted Lands buff", kind = "aura",
icon = "inv_stone_15",
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 = "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" },
{ id = "HP", label = "HP", sub = "Stam.", full = "Stamina/HP", kind = "aura",
items = "Elixir of Fortitude \194\183 Rumsey Rum Black Label" },
{ id = "FR", label = "FR", sub = "fire", full = "Fire protection", kind = "aura",
{ id = "HPELX", label = "FRT", sub = "Fort.", full = "Elixir of Fortitude", kind = "aura",
icon = "inv_potion_43",
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" },
{ id = "WPN", label = "WPN", sub = "temp.", full = "Weapon (temporary)", kind = "weapon", -- NOT an aura slot
items = "Dense Sharpening Stone \194\183 Brilliant Wizard/Mana Oil - permanent enchant: Equipment tab" },
-- The remaining school protection columns follow the FR pattern (one
-- 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).
-- ------------------------------------------------------------------
DC.CONSUMABLES = {
-- FLASK
["Flask of the Titans"] = { slot = "FLASK", spellID = 17626, item = "Flask of the Titans" },
["Flask of Supreme Power"] = { slot = "FLASK", spellID = 17628, item = "Flask of Supreme Power" },
["Flask of Distilled Wisdom"] = { slot = "FLASK", spellID = 17627, item = "Flask of Distilled Wisdom" },
["Flask of Chromatic Resistance"] = { slot = "FLASK", spellID = 17629, item = "Flask of Chromatic Resistance" },
["Flask of Petrification"] = { slot = "FLASK", spellID = 17624, item = "Flask of Petrification" },
["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", icon = "inv_potion_41" },
["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", icon = "inv_potion_48" },
["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)
["Well Fed"] = { slot = "FOOD", spellID = 0, item = "Grilled Squid etc." },
-- ["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.
["Increased Stamina"] = { slot = "FOOD", spellID = 0, item = "stamina food" },
["Dragonbreath Chili"] = { slot = "FOOD", spellID = 15852, item = "Dragonbreath Chili" },
-- FOOD (buff names, not item names).
-- "Well Fed" is the generic food aura: the ITEM is identified through
-- the tooltip EFFECT line (scan/aura.lua auras.effects) against the
-- per-variant discrim patterns below -- first match wins, no match =
-- nil = the matrix falls back to the slot-generic icon ("food buff,
-- unknown which"). The variant list is the food set of the KG
-- stacking-rules atom plus the classic raid staples; the discrim
-- 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
["Winterfall Firewater"] = { slot = "AP", spellID = 17038, item = "Winterfall Firewater" },
["Juju Might"] = { slot = "AP", spellID = 16329, item = "Juju Might" },
["Winterfall Firewater"] = { slot = "AP", spellID = 17038, item = "Winterfall Firewater", icon = "inv_potion_92" },
["Juju Might"] = { slot = "AP", spellID = 16329, item = "Juju Might", icon = "inv_misc_monsterscales_07" },
-- STR
["Juju Power"] = { slot = "STR", spellID = 16323, item = "Juju Power" },
["Elixir of Giants"] = { slot = "STR", spellID = 11405, item = "Elixir of Giants" },
["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 Brute Force"] = { slot = "STR", spellID = 17537, item = "Elixir of Brute Force" },
["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
["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", icon = "inv_potion_61" },
["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", icon = "inv_potion_40" },
-- AGI
["Elixir of the Mongoose"] = { slot = "AGI", spellID = 17538, item = "Elixir of the Mongoose" },
["Elixir of Greater Agility"] = { slot = "AGI", spellID = 11334, item = "Elixir of Greater Agility" },
["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
["Elixir of Agility"] = { slot = "AGI", spellID = 11328, item = "Elixir of Agility" },
["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", icon = "inv_potion_94" },
["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", icon = "inv_potion_93" },
-- SP (merged: arcane_elixir + school_elixir + dreamtonic + dreamshard)
["Greater Arcane Elixir"] = { slot = "SP", spellID = 17539, item = "Greater Arcane Elixir" },
["Arcane Elixir"] = { slot = "SP", spellID = 11390, item = "Arcane Elixir" },
["Elixir of Shadow Power"] = { slot = "SP", spellID = 11474, item = "Elixir of Shadow Power" },
["Elixir of Frost Power"] = { slot = "SP", spellID = 21920, item = "Elixir of Frost Power" },
["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
["Elixir of Greater Firepower"] = { slot = "SP", spellID = 26276, item = "Elixir of Greater Firepower" },
["Elixir of Firepower"] = { slot = "SP", spellID = 7844, item = "Elixir of Firepower" },
["Dreamtonic"] = { slot = "SP", spellID = 45489, item = "Dreamtonic" }, -- use-spell ID; the aura's own ID is unverified
["Dreamshard Elixir"] = { slot = "SP", spellID = 45427, item = "Dreamshard Elixir" }, -- use-spell ID; the aura's own ID is unverified
-- BL (Blasted Lands buff slot -- one at a time, pick one; the five
-- items were split across STR/nowhere in the old model).
-- Aura names + spell ids wowhead-confirmed 2026-08-11 ("Strike of the
-- Scorpok" additionally measured live in a 26-man raid). The item
-- names ride along as alias keys (spellID 0) because the Turtle
-- client may surface the permanent aura under the item name (KG
-- note); a key that never matches is harmless.
["Rage of Ages"] = { slot = "BL", spellID = 10667, item = "R.O.I.D.S.", icon = "inv_stone_15" },
["R.O.I.D.S."] = { slot = "BL", spellID = 0, item = "R.O.I.D.S.", icon = "inv_stone_15" },
["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
["Mageblood Potion"] = { slot = "MP5", spellID = 24363, item = "Mageblood Potion" },
["Mageblood"] = { slot = "MP5", spellID = 24363, item = "Mageblood Potion" }, -- TWoW buff-name variant (old Data.lua kept both)
-- ZANZA (free-stacker; only one Zanza-potion effect at a time; buff
-- name = item name, wowhead-confirmed)
["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
["Elixir of Superior Defense"] = { slot = "ARM", spellID = 11348, item = "Elixir of Superior Defense" },
["Elixir of Greater Defense"] = { slot = "ARM", spellID = 11349, item = "Elixir of Greater 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", icon = "inv_potion_65" },
-- HP (merged: def_fortitude + alcohol STA variants)
["Elixir of Fortitude"] = { slot = "HP", spellID = 3593, item = "Elixir of Fortitude" },
["Health II"] = { slot = "HP", spellID = 3593, item = "Elixir of Fortitude" }, -- 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" },
-- HPELX (alcohol split out into ALC; Fortitude stacks with it)
["Elixir of Fortitude"] = { slot = "HPELX", spellID = 3593, item = "Elixir of Fortitude", icon = "inv_potion_43" },
["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")
-- FR
["Greater Fire Protection"] = { slot = "FR", spellID = 17543, item = "Greater Fire Protection Potion" },
["Fire Protection"] = { slot = "FR", spellID = 7233, item = "Fire Protection Potion" },
-- ALC (alcohol buffs -- ONE column by design although different
-- types stack; see the header compromise note). Buff names and spell
-- 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
}
-- ------------------------------------------------------------------
-- 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).
-- Collision rule: at classify time an ID match beats a
+22 -8
View File
@@ -1,5 +1,5 @@
-- 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.
-- 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
@@ -7,13 +7,26 @@
-- question is. Item-tile slots (trinkets) show the equipped item and
-- 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-custom feature beyond vanilla WoW, where they take no enchant) --
-- NECK/R1/R2 are therefore kind="ench" like any armor slot, not item
-- tiles. Their expectation defaults to OFF (data/expectations.lua): unlike
-- the ARMOR enchant slots, a neck/ring enchant is not assumed baseline
-- raid gear, so the columns only track it once a role/class opts in via
-- the options grid.
-- Server note: neck, both rings AND the waist ARE enchantable on this
-- server (a server-custom feature beyond vanilla WoW, where none of them
-- take an enchant) -- NECK/R1/R2/WAIST are therefore kind="ench" like any
-- armor slot, not item tiles. The waist enchant is a "Belt Buckle" item
-- (Copper/Bronze/Thorium/Dreamsteel/Bloody Belt Buckle),
-- the same custom itemization line as the neck/ring gems. Their
-- 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.
DopingControl = DopingControl or {}
@@ -30,6 +43,7 @@ DC.SLOTS_EQUIPMENT = {
{ 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 = "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 = "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 },
+63 -32
View File
@@ -15,7 +15,21 @@
-- a CASTER/HEALER role default for every performing class.
--
-- 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
-- for all five -- like FR. EMB (Emerald Blessing) has NO
-- 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
-- grid; the dynamic-column rule makes an unexpected slot
-- 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
-- classify time via DC.ENCH_EXEMPT, not here.
-- NECK/R1/R2 (enchantable on this server, a custom feature
-- beyond vanilla) default to OFF for every role: unlike
-- the armor slots, a neck/ring enchant is not assumed
-- NECK/R1/R2 (also enchantable on this server, a custom
-- feature beyond vanilla) stay OFF by default for every
-- role: unlike WAIST, a neck/ring enchant is not assumed
-- baseline raid gear -- enable it per role/class in the
-- options grid when it matters. T1/T2 (trinket item tiles)
-- carry NO seed entry at all -- the model ignores
@@ -37,19 +55,16 @@
--
-- Known debatable rows -- documented deliberately, the shipped defaults
-- win:
-- * TANK+AP: some tank consumable lists include Winterfall
-- Firewater; the default has no AP for TANK.
-- * MELEE+FLASK: some melee lists include Flask of the Titans; the
-- default has no FLASK for MELEE.
-- * MELEE/RANGED+HP: some melee lists have neither Fortitude nor
-- alcohol; HP=true is the weakest row of the matrix.
-- * RANGED+AP/WPN: Firewater/Juju Might are MELEE attack power; ranged
-- AP comes from food. Arguably AP=false, WPN=false
-- for RANGED; the default says true.
-- * HEALER+SP: healer sets may carry Dreamshard (+healing), which the
-- 11-slot merge puts into SP; the default has no SP for
-- HEALER -> Dreamshard is invisible for healers by
-- default.
-- * TANK+AP / MELEE+FLASK: the OLD defaults deliberately excluded them;
-- the KG role sets include them (tank runs Winterfall
-- Firewater, melee runs Flask of the Titans), so both
-- flipped to true with the v6 slot model.
-- * RANGED line: derived, not KG-covered (see above) -- BL is included
-- (Ground Scorpok Assay is +25 AGI), STR/FLASK are not.
-- * HEALER+HPELX: the KG healer set carries no Elixir of Fortitude, so
-- HPELX defaults off for HEALER while ALC (Kreeg's
-- Stout Beatdown + Medivh's Merlot Blue) is on.
-- * ZANZA: expected for every role (all four KG sets carry it).
-- * Equipment: an alternative default would differentiate per role
-- (OFFH only TANK/MELEE, RNGD only RANGED,
-- casters/healers neither); deliberately flattened to
@@ -65,9 +80,12 @@ local DC = DopingControl
DC.DEFAULT_EXPECT = {
TANK = {
-- consumables
FLASK = true, FOOD = true, STR = true, AGI = true, ARM = true,
HP = true, FR = true, WPN = true,
-- consumables (KG tank set: Mongoose, Firewater, Juju Power,
-- R.O.I.D.S., Zanza, Superior Defense, Fortitude, Titans, food,
-- 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
MOTW = true, PWF = true, SPROT = true,
-- equipment (RNGD default only for RANGED:
@@ -75,48 +93,61 @@ DC.DEFAULT_EXPECT = {
-- expectation matrix anytime)
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
WAIST = true,
},
MELEE = {
-- consumables
FOOD = true, AP = true, STR = true, AGI = true,
HP = true, FR = true, WPN = true,
-- consumables (KG melee set: Mongoose, Firewater/Juju Might,
-- Juju Power/Giants, R.O.I.D.S./Scorpok, Zanza, Titans, food,
-- 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
MOTW = true, PWF = true, SPROT = true,
-- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
WAIST = true,
},
RANGED = {
-- consumables
FOOD = true, AP = true, AGI = true,
HP = true, FR = true, WPN = true,
-- consumables (derived from the melee line -- no KG set; no STR,
-- no FLASK, see "Known debatable rows")
FOOD = true, AP = true, AGI = true, BL = true, ZANZA = true,
FR = true, WPN = true,
-- class buffs
MOTW = true, PWF = true, SPROT = true,
-- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
WAIST = true,
RNGD = true,
},
-- CASTER/HEALER: RNGD also dropped by role default (their classes are
-- additionally exempt via ENCH_CLASS_EXEMPT - belt and suspenders)
CASTER = {
-- consumables
FLASK = true, FOOD = true, SP = true, MP5 = true,
-- consumables (KG caster set: Dreamshard, GAE, school elixir,
-- 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,
-- class buffs
AI = true, MOTW = true, PWF = true, SPROT = true,
-- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = true,
HAND = true, LEGS = true, FEET = true, MAIN = true, OFFH = true,
WAIST = true,
},
HEALER = {
-- consumables
FLASK = true, FOOD = true, MP5 = true,
HP = true, FR = true, WPN = true,
-- consumables (KG healer set: Dreamshard, Mageblood, mana oil,
-- Zanza, Distilled Wisdom, food, Kreeg's + Merlot Blue)
FLASK = true, FOOD = true, MP5 = true, SHARD = true, ZANZA = true,
ALC = true,
FR = true, WPN = true,
-- class buffs (SOUL: expected ONLY here)
AI = true, MOTW = true, PWF = true, SPROT = true, SOUL = true,
-- equipment
HEAD = true, SHLD = true, BACK = true, CHST = true, WRST = 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
-- exists. textures is OPTIONAL/partial by design -- every consumer
-- 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,
-- only when BOTH paths yielded at least one ID -- an absent path is missing
-- 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
-- rawA : array of { name = string|nil, id = number|nil,
-- texture = string|nil } (path A; texture is the 1st
-- UnitBuff return, optional)
-- texture = string|nil, effect = string|nil }
-- (path A; texture is the 1st UnitBuff return,
-- effect the tooltip's line 2 -- both optional)
-- rawB : array of spell ids (numbers, path B)
-- idNames : OPTIONAL { [spellId] = "Buff Name" } for path-B ids,
-- filled by the WoW-side caller (SuperWoW SpellInfo lookup --
@@ -78,9 +85,10 @@ local A = DC_Aura
-- as before (no behavior change without it).
-- Returns:
-- auras : { names = { [name]=true }, ids = { [id]=true },
-- textures = { [name]=iconPath } }
-- (store shape; textures may be empty/partial --
-- consumers nil-guard)
-- textures = { [name]=iconPath },
-- effects = { [name]=effectLine } }
-- (store shape; textures AND effects may be empty/
-- partial -- consumers nil-guard)
-- 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.
-- Entries with NEITHER (UnitBuff texture present but the
@@ -131,6 +139,7 @@ function A.Assemble(rawA, rawB, idNames)
local idsA = {}
local idsB = {}
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 usable = 0 -- entries that carry actual evidence (see header)
@@ -144,6 +153,9 @@ function A.Assemble(rawA, rawB, idNames)
if e.texture then
textures[e.name] = e.texture
end
if type(e.effect) == "string" and e.effect ~= "" then
effects[e.name] = e.effect
end
end
if type(e.id) == "number" and e.id > 0 then
idsA[e.id] = true
@@ -259,7 +271,8 @@ function A.Assemble(rawA, rawB, idNames)
end
end
return { names = names, ids = ids, textures = textures },
return { names = names, ids = ids, textures = textures,
effects = effects },
aurasRead, disagreements, unknown
end
@@ -376,15 +389,22 @@ function A.GetScanTip()
return ensureTip()
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
local function tipBuffName(unit, buffIndex)
scanTip:ClearLines()
scanTip:SetUnitBuff(unit, buffIndex)
local name, effect = nil, nil
local textObj = getglobal("DopingControlScanTipTextLeft1")
if textObj then
return textObj:GetText()
name = textObj:GetText()
end
return nil
local effObj = getglobal("DopingControlScanTipTextLeft2")
if effObj then
effect = effObj:GetText()
end
return name, effect
end
-- debuff twin of tipBuffName (same hidden tooltip, SetUnitDebuff);
@@ -420,20 +440,23 @@ function A.ReadUnit(unit, guid)
if not ok or not texture then
break
end
local name = nil
local name, effect = nil, nil
if tip then
local ok2, nm = pcall(tipBuffName, unit, i)
local ok2, nm, eff = pcall(tipBuffName, unit, i)
if ok2 then
name = nm
effect = eff
end
end
local id = nil
if type(auraID) == "number" and auraID > 0 then
id = auraID
end
-- texture (UnitBuff return 1) rides along so Assemble can key
-- it under the tooltip name (auras.textures)
table.insert(rawA, { name = name, id = id, texture = texture })
-- texture (UnitBuff return 1) and the tooltip effect line ride
-- along so Assemble can key them under the tooltip name
-- (auras.textures / auras.effects)
table.insert(rawA, { name = name, id = id, texture = texture,
effect = effect })
end
end
if UnitDebuff then
+21 -7
View File
@@ -120,6 +120,15 @@ function E.NormalizeWeapon(isSelf, v, readable)
end
return false
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 readable == true then
return false
@@ -998,10 +1007,12 @@ if CreateFrame then
end
E.SweepLastKnown(DC.lastKnown, rosterSet, E.CACHE_CAP)
local db = DopingControlDB
if db and db.testMode then
-- Test mode was switched ON while this scan was in flight: the
-- simulator fully replaces the data source ("real
-- data never beats the simulation") -- drop the result.
if DC.SimOwnsStore(db) then
-- Test mode OR demo mode was on (or switched on while this scan
-- was in flight): the simulator fully replaces the data source
-- ("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 = {}
return
end
@@ -1055,8 +1066,11 @@ if CreateFrame then
-- Start(force) -> bool (scan started/restarted).
-- Already running: no-op unless force, which restarts with a fresh
-- roster snapshot. Test mode is NOT checked here -- an explicit Start
-- is user intent; only the READY_CHECK auto-trigger honors testMode.
-- roster snapshot. DC.SimOwnsStore is NOT checked here -- an explicit
-- 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)
if running then
if not force then
@@ -1087,7 +1101,7 @@ if CreateFrame then
eventFrame:SetScript("OnEvent", function()
if event == "READY_CHECK" then
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()
end
return
+2 -2
View File
@@ -29,9 +29,9 @@ local G = DC_Gear
-- Fallback inventory-slot list (= invSlot column of DC.SLOTS_EQUIPMENT,
-- 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).
function G.InvSlots()
if DC.SLOTS_EQUIPMENT then
+7 -6
View File
@@ -122,9 +122,11 @@ DC_Hit = DC_Hit or {}
local H = DC_Hit
-- Equipment slots that can carry +hit, in inventory-slot order. This is
-- deliberately its OWN list and not DC_Gear.InvSlots(): the enchant tab
-- has no waist column, but a belt most certainly can carry hit. Shirt (4)
-- and tabard (19) never carry stats and are left out.
-- deliberately its OWN list and not DC_Gear.InvSlots(): the two lists
-- happen to agree today (both cover the waist, invSlot 6 -- see
-- 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.SLOT_MAIN = 16
@@ -888,9 +890,8 @@ end
-- ReadUnit(unit, rawLinks) -> hit, race
-- rawLinks : OPTIONAL { [invSlot] = link } that scan/gear.lua already
-- fetched for this unit. Slots it covers are taken from it
-- (no second GetInventoryItemLink pass); slots outside its
-- coverage -- the waist, which the enchant tab has no column
-- for -- are fetched here.
-- (no second GetInventoryItemLink pass); any slot H.SLOTS
-- needs that rawLinks did not cover is fetched here instead.
-- hit : the store shape documented in the file header
-- race : race token from UnitRace (2nd return, e.g. "NightElf");
-- 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
-- respec, which is rare and never mid-raid.
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)
@@ -147,9 +152,24 @@ end
-- spent = <sum of pointsSpent over the trees that reported>,
-- sumRank = <sum of every rank we stored>,
-- trees = <how many tab lines arrived>,
-- complete = <INSTalentEND seen> }
function T.NewAcc()
return { ranks = {}, spent = 0, sumRank = 0, trees = 0, complete = false }
-- complete = <INSTalentEND seen>,
-- at = <when the first line arrived, nil offline> }
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
-- Fold one parsed line into the accumulator. Returns the accumulator so
@@ -281,6 +301,9 @@ if CreateFrame then
return ok
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
-- -- names are only lined up; the timer below paces them out.
function T.Request(names)
@@ -296,6 +319,9 @@ if CreateFrame then
table.insert(queue, n)
end
end
if arm and table.getn(queue) > 0 then
arm()
end
end
local f = CreateFrame("Frame", "DopingControlTalentFrame")
@@ -313,9 +339,14 @@ if CreateFrame then
if not kind then
return
end
local now = GetTime()
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
a = T.NewAcc()
a = T.NewAcc(now)
acc[sender] = a
end
T.Feed(a, kind, data)
@@ -338,14 +369,26 @@ if CreateFrame then
-- 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
-- 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
f:SetScript("OnUpdate", function()
local function disarm()
f:SetScript("OnUpdate", nil)
end
local function drain()
elapsed = elapsed + (arg1 or 0)
if elapsed < 1 then
return
end
elapsed = 0
if not enabled() or table.getn(queue) == 0 then
disarm()
return
end
local now = GetTime()
@@ -359,10 +402,23 @@ if CreateFrame then
T.Ask(name)
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)
function T.Clear()
disarm()
acc = {}
ranks = {}
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.ROW_H = 22 -- one grid row's height, both grids
-- equipment has the most configurable slots (11 armor enchants + neck +
-- 2 rings; trinkets are item tiles and never configurable, see
-- slotsForTab) -- the widest grid the pools have to serve.
O.MAX_SLOT_COLS = 14
-- consumables (v6 slot model) has the most configurable slots (22 incl.
-- the five school protection columns);
-- equipment follows with 14 (11 armor enchants + neck + 2 rings --
-- 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
O.CELL_INSET = math.floor((O.CELL_W - 20) / 2)
@@ -489,6 +491,17 @@ function O.ClassTitle(class)
.. string.lower(string.sub(class, 2))
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
-- ==================================================================
@@ -504,7 +517,7 @@ if CreateFrame then
-- O.GRID_LABEL_W label column O.COL_PITCH column 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
-- 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
-- narrowed per tab by ApplyEditorWidth, so no widget is ever created
-- against a width it later has to grow past.
@@ -558,9 +571,9 @@ if CreateFrame then
elseif tab == "CLASSBUFFS" then
return DC.SLOTS_CLASSBUFFS or {}
elseif tab == "EQUIPMENT" then
-- only kind=="ench" slots are configurable here (14: the 11
-- armor slots + neck/rings, which are enchantable on this
-- server -- data/enchants.lua). T1/T2 (trinket item tiles)
-- only kind=="ench" slots are configurable here (15: the 11
-- armor slots + neck/rings/waist, which are enchantable on
-- this server -- data/enchants.lua). T1/T2 (trinket item tiles)
-- have no enchant question to ask and never appear: the
-- matrix always shows them (item present/absent decides,
-- expectations are never consulted for tiles).
@@ -1536,6 +1549,42 @@ if CreateFrame then
place(cbTest, -28)
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",
function() return db().showMinimapButton end,
function(v)
@@ -1788,6 +1837,11 @@ if CreateFrame then
end
RefreshEditor() -- grid/books pools + tab visibility + buttons
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)
-- ESC closes
@@ -1827,14 +1881,18 @@ if CreateFrame then
f:Show()
end
-- external state changes (/dc test, minimap toggle) can resync an
-- open options window
-- external state changes (/dc test, /dc demo, minimap toggle) can
-- resync an open options window
function O.Refresh()
if frame and frame:IsShown() then
for i = 1, table.getn(checkboxes) do
local cb = checkboxes[i]
cb:SetChecked(cb.dcGet() and 1 or nil)
end
local bd = getglobal("DopingControlOptDemoBtn")
if bd then
bd:SetText(O.DemoButtonLabel())
end
RefreshEditor() -- a fresh scan can add or drop book rows
end
end
+29 -2
View File
@@ -59,6 +59,22 @@ function R.SendDisabled(source)
return source == "sim"
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
-- (the preview may be rebuilt while an old queue is still draining).
function R.QueueCreate(lines)
@@ -135,7 +151,18 @@ end
function R.BuildLines(store, dbt, tab)
store = store or DC.store or { players = {} }
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)
return DC_Roles.resolve(dbt, store.source, p)
end
@@ -234,7 +261,7 @@ if CreateFrame then
end
local line = R.QueueTick(queue, GetTime(), currentSource())
if line then
SendChatMessage(line, R.CHAT_TYPE)
SendChatMessage(R.ChatSafe(line), R.CHAT_TYPE)
UpdateSendState()
end
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 }
-- ------------------------------------------------------------------
-- 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
-- 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
-- visual layout). Carries one texture region (check/cross glyph OR trinket
-- item icon) and one FontString (?, skip dot, trinket monogram); the
-- matrix paint code shows exactly one of them per state.
-- visual layout). The box (background + 1px border) is one texture region
-- sampled from the baked sheet in ui/cellsheet.lua -- SetBackdrop with an
-- 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).
-- ------------------------------------------------------------------
function W.MakeCell(parent, name, w, h)
local c = CreateFrame("Frame", name, parent)
c:SetWidth(w or 30)
c:SetHeight(h or 23)
c:SetBackdrop(W.SOLID_BACKDROP)
c:EnableMouse(true)
c.icon = c:CreateTexture(nil, "ARTWORK")
c.icon:SetPoint("CENTER", c, "CENTER", 0, 0)
c.icon:SetWidth(16)
c.icon:SetHeight(16)
c.icon:Hide()
c.text = c:CreateFontString(nil, "OVERLAY")
W.ApplyFont(c.text, 9)
c.text:SetPoint("CENTER", c, "CENTER", 0, 0)
c.dcW = w or 30
if W.HasCellSheet() then
-- The state box (background + 1px border) comes from one baked sheet:
-- SetBackdrop with an edgeFile costs NINE texture regions per cell,
-- this costs one. See ui/cellsheet.lua.
c.boxTex = c:CreateTexture(nil, "BACKGROUND")
c.boxTex:SetAllPoints(c)
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
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
-- 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)
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)
frame.dcTipBuilder = builder
frame.dcTipAnchor = anchor or "ANCHOR_LEFT"
@@ -258,6 +444,7 @@ function W.AttachTooltip(frame, builder, anchor)
end
if GameTooltip and this.dcTipBuilder then
GameTooltip:SetOwner(this, this.dcTipAnchor)
W.dcTipOwner = this
this.dcTipBuilder(this)
GameTooltip:Show()
end
@@ -266,6 +453,9 @@ function W.AttachTooltip(frame, builder, anchor)
if this.dcOldLeave then
this.dcOldLeave()
end
if W.dcTipOwner == this then
W.dcTipOwner = nil
end
if GameTooltip then
GameTooltip:Hide()
end