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
+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 },