From Arms of God Wiki
bot: operator iteration 2026-06-10d (hover tooltips / filter chips + DPS sort / damage-type hubs / stat reverse-lookup pages) |
bot: render-time derivation refactor (phase 1) |
||
| Line 1: | Line 1: | ||
-- Module:Compare — comparison-table renderer (Arms of God). | -- Module:Compare — comparison-table renderer (Arms of God). | ||
-- | -- RENDER-TIME DERIVATION: computes every dataset from the source | ||
-- | -- Data:<Category>.json pages via Module:Core. No Data:Compare_* pages. | ||
-- | -- | ||
-- | -- {{#invoke:Compare|render|Weapons_Melee}} — melee weapons, DPS desc | ||
-- | -- {{#invoke:Compare|render|Weapons_Ranged}} — ranged weapons, DPS desc | ||
-- | -- {{#invoke:Compare|render|Enemies}} — base + per-wave + wave-10 | ||
-- | -- {{#invoke:Compare|render|Characters}} — hero stat modifiers | ||
-- | |||
-- Weapon DPS = (Damage + Holy + Fire + Electric) × Attack Speed, rounded | |||
-- to 1 decimal. Sentinels: value >= 1000 renders '∞' (sorts top); | |||
-- value <= -1000 renders '—' (sorts bottom). Rows carry data-element / | |||
-- data-class / data-tier / data-type attributes for the Common.js chips. | |||
local Core = require('Module:Core') | |||
local p = {} | local p = {} | ||
local TBL_STYLE = 'font-size:0.92em;background:var(--table-row-odd, #1b1c20);color:var(--table-text, #e6e6e6);border-color:var(--table-border, #3a3c44);' | local TBL_STYLE = 'font-size:0.92em;background:var(--table-row-odd, #1b1c20);color:var(--table-text, #e6e6e6);border-color:var(--table-border, #3a3c44);' | ||
local TH_STYLE = 'background:var(--table-header-bg, #26272e);color:var(--infobox-header-fg, #f1e9d2);' | local TH_STYLE = 'background:var(--table-header-bg, #26272e);color:var(--infobox-header-fg, #f1e9d2);' | ||
local function swatch(label, tok, hexv) | |||
return '<span style="color:var(' .. tok .. ', ' .. hexv | |||
.. ');font-weight:bold;white-space:nowrap;">● ' .. label .. '</span>' | |||
end | |||
-- Coerce a raw stat value to {display, sortval}. Mirrors the historic | |||
-- emit-time _num(): '15%' keeps the % display but sorts numerically. | |||
local function num(v) | |||
if v == nil then return '—', nil end | |||
if type(v) == 'string' then | |||
local s = v:gsub('^%s+', ''):gsub('%s+$', '') | |||
local n = tonumber(s) or tonumber(s:match('^([%-%d%.]+)%%$')) | |||
if n == nil then return s, nil end | |||
if s:sub(-1) == '%' then return s, n end | |||
v = n | |||
end | |||
if type(v) == 'number' then | |||
if v >= 1000 then return '∞', 1e9 end | |||
if v <= -1000 then return '—', -1 end | |||
return Core.fmtNum(v), v | |||
end | |||
return tostring(v), nil | |||
end | |||
local function numcell(v) | |||
local t, s = num(v) | |||
return {t = t, s = s} | |||
end | |||
local DMG = { | |||
Holy = {'--dmg-holy', '#e8c34a', 'wm-dmg-holy'}, | |||
Fire = {'--dmg-fire', '#ff6b35', 'wm-dmg-fire'}, | |||
Electric = {'--dmg-electric', '#4ea3ff', 'wm-dmg-electric'}, | |||
} | |||
local function chipcell(dmg, v) | |||
local t, s = num(v) | |||
if s == nil or s <= 0 or t == '—' then return {t = '—', s = 0} end | |||
local c = DMG[dmg] | |||
return {t = '<span class="' .. c[3] .. '" style="color:var(' .. c[1] .. ', ' | |||
.. c[2] .. ');font-weight:bold;">' .. t .. '</span>', s = s} | |||
end | |||
local function iconlinkcell(rec) | |||
local slug = rec.slug or rec.name or '' | |||
local name = rec.name or slug | |||
local t | |||
if rec.icon and rec.icon ~= '' then | |||
t = '[[File:' .. rec.icon .. '|24px|link=' .. slug .. '|alt=' .. name | |||
.. ']] [[' .. slug .. '|' .. name .. ']]' | |||
else | |||
t = '[[' .. slug .. '|' .. name .. ']]' | |||
end | |||
t = '<span class="wm-tip" data-tip-title="' .. slug .. '">' .. t .. '</span>' | |||
return {t = t, s = mw.ustring.lower(name)} | |||
end | |||
local function stat(rec, k) | |||
return (rec.stats or {})[k] | |||
end | |||
local function nstat(rec, k) | |||
return Core.numStat(stat(rec, k)) | |||
end | |||
local function totalDPS(rec) | |||
local s = rec.stats or {} | |||
local atk = Core.numStat(s['Attack Speed']) | |||
if atk == nil then return nil end | |||
local total = Core.numStat(s['Damage']) or 0 | |||
for _, k in ipairs({'Holy Damage', 'Fire Damage', 'Electric Damage'}) do | |||
local v = Core.numStat(s[k]) | |||
if v ~= nil and v > 0 and v < 1000 then total = total + v end | |||
end | |||
return Core.round1(total * atk) | |||
end | |||
local function wave10(rec, base, per) | |||
local b, pv = nstat(rec, base), nstat(rec, per) | |||
if b == nil or pv == nil then return nil end | |||
return Core.round1(b + pv * 9) | |||
end | |||
-- ----- dataset builders ---------------------------------------------- | |||
local function weaponRows(class) | |||
local headers = {'Weapon', 'Tier', 'Price', 'Damage', | |||
swatch('Holy', '--dmg-holy', '#e8c34a'), | |||
swatch('Fire', '--dmg-fire', '#ff6b35'), | |||
swatch('Electric', '--dmg-electric', '#4ea3ff'), | |||
'DPS', 'Atk Speed', 'Crit', 'Range', 'Pierce', 'Proj.', 'Bounce'} | |||
local rows = {} | |||
for _, rec in ipairs(Core.load('Weapons')) do | |||
if Core.weaponClass(rec) == class then | |||
local cells = { | |||
iconlinkcell(rec), | |||
numcell(rec.tier), | |||
numcell(rec.price), | |||
numcell(stat(rec, 'Damage')), | |||
chipcell('Holy', stat(rec, 'Holy Damage')), | |||
chipcell('Fire', stat(rec, 'Fire Damage')), | |||
chipcell('Electric', stat(rec, 'Electric Damage')), | |||
numcell(totalDPS(rec)), | |||
numcell(stat(rec, 'Attack Speed')), | |||
numcell(stat(rec, 'Critical Chance')), | |||
numcell(stat(rec, 'Range')), | |||
numcell(stat(rec, 'Pierce')), | |||
numcell(stat(rec, 'Projectiles')), | |||
numcell(stat(rec, 'Bounce')), | |||
} | |||
local elements = {} | |||
for _, e in ipairs({{'holy', 'Holy Damage'}, {'fire', 'Fire Damage'}, | |||
{'electric', 'Electric Damage'}}) do | |||
local v = nstat(rec, e[2]) | |||
if v ~= nil and v > 0 and v < 1000 then elements[#elements + 1] = e[1] end | |||
end | |||
local attrs = ' data-element="' .. (#elements > 0 and table.concat(elements, ' ') or 'none') | |||
.. '" data-class="' .. class:lower() | |||
.. '" data-tier="' .. tostring(rec.tier or '') .. '"' | |||
rows[#rows + 1] = {a = attrs, c = cells} | |||
end | |||
end | |||
-- DPS descending (stable: name asc tiebreak via the first cell sortkey). | |||
table.sort(rows, function(x, y) | |||
local dx, dy = x.c[8].s or 0, y.c[8].s or 0 | |||
if dx ~= dy then return dx > dy end | |||
return (x.c[1].s or '') < (y.c[1].s or '') | |||
end) | |||
return headers, rows | |||
end | |||
local function enemyRows() | |||
local headers = {'Enemy', 'Health', 'HP/Wave', 'HP @ W10', 'Damage', | |||
'Dmg/Wave', 'Dmg @ W10', 'Speed', 'Resources'} | |||
local rows = {} | |||
for _, rec in ipairs(Core.sorted('Enemies')) do | |||
local keys = {} | |||
for _, k in ipairs(rec.tags or {}) do keys[#keys + 1] = k:lower() end | |||
rows[#rows + 1] = { | |||
a = ' data-type="' .. (#keys > 0 and table.concat(keys, ' ') or 'other') .. '"', | |||
c = { | |||
iconlinkcell(rec), | |||
numcell(stat(rec, 'Health')), | |||
numcell(stat(rec, 'HP per Wave')), | |||
numcell(wave10(rec, 'Health', 'HP per Wave')), | |||
numcell(stat(rec, 'Damage')), | |||
numcell(stat(rec, 'Damage per Wave')), | |||
numcell(wave10(rec, 'Damage', 'Damage per Wave')), | |||
numcell(stat(rec, 'Speed')), | |||
numcell(stat(rec, 'Resources')), | |||
}, | |||
} | |||
end | |||
return headers, rows | |||
end | |||
local function characterRows() | |||
local headers = {'Hero', 'Health Points', 'Damage', 'Critical Chance', | |||
'Attack Speed', 'Range', 'Speed', 'Armor', 'Dodge'} | |||
local rows = {} | |||
for _, rec in ipairs(Core.sorted('Characters')) do | |||
rows[#rows + 1] = {a = '', c = { | |||
iconlinkcell(rec), | |||
numcell(stat(rec, 'Health Points')), | |||
numcell(stat(rec, 'Damage')), | |||
numcell(stat(rec, 'Critical Chance')), | |||
numcell(stat(rec, 'Attack Speed')), | |||
numcell(stat(rec, 'Range')), | |||
numcell(stat(rec, 'Speed')), | |||
numcell(stat(rec, 'Armor')), | |||
numcell(stat(rec, 'Dodge')), | |||
}} | |||
end | |||
return headers, rows | |||
end | |||
local DATASETS = { | |||
Weapons_Melee = function() return weaponRows('Melee') end, | |||
Weapons_Ranged = function() return weaponRows('Ranged') end, | |||
Enemies = enemyRows, | |||
Characters = characterRows, | |||
} | |||
-- ----- renderer -------------------------------------------------------- | |||
local function cell(c) | local function cell(c) | ||
local t = tostring(c.t or '—') | local t = tostring(c.t or '—') | ||
if c.s ~= nil then | if c.s ~= nil then | ||
return '| data-sort-value="' .. tostring( | local s = c.s | ||
if type(s) == 'number' then s = Core.fmtNum(s) end | |||
return '| data-sort-value="' .. tostring(s) .. '" | ' .. t | |||
end | end | ||
return '| ' .. t | return '| ' .. t | ||
| Line 26: | Line 219: | ||
end | end | ||
ds = ds:gsub('^%s+', ''):gsub('%s+$', '') | ds = ds:gsub('^%s+', ''):gsub('%s+$', '') | ||
local | local build = DATASETS[ds] | ||
if not | if not build then | ||
return '<strong class="error">Module:Compare: | return '<strong class="error">Module:Compare: unknown dataset ' | ||
.. ds .. ' | .. mw.text.nowiki(ds) .. '</strong>' | ||
end | end | ||
local headers, rows = build() | |||
local out = { '{| class="wikitable sortable" style="' .. TBL_STYLE .. '"' } | local out = { '{| class="wikitable sortable" style="' .. TBL_STYLE .. '"' } | ||
out[#out+1] = '|-' | out[#out+1] = '|-' | ||
for _, h in ipairs( | for _, h in ipairs(headers) do | ||
out[#out+1] = '! style="' .. TH_STYLE .. '" | ' .. tostring(h) | out[#out+1] = '! style="' .. TH_STYLE .. '" | ' .. tostring(h) | ||
end | end | ||
for _, row in ipairs( | for _, row in ipairs(rows) do | ||
out[#out+1] = '|-' .. tostring(row.a or '') | out[#out+1] = '|-' .. tostring(row.a or '') | ||
for _, c in ipairs( | for _, c in ipairs(row.c) do | ||
out[#out+1] = cell(c) | out[#out+1] = cell(c) | ||
end | end | ||
Revision as of 08:29, 10 June 2026
Documentation for this module may be created at Module:Compare/doc
-- Module:Compare — comparison-table renderer (Arms of God).
-- RENDER-TIME DERIVATION: computes every dataset from the source
-- Data:<Category>.json pages via Module:Core. No Data:Compare_* pages.
--
-- {{#invoke:Compare|render|Weapons_Melee}} — melee weapons, DPS desc
-- {{#invoke:Compare|render|Weapons_Ranged}} — ranged weapons, DPS desc
-- {{#invoke:Compare|render|Enemies}} — base + per-wave + wave-10
-- {{#invoke:Compare|render|Characters}} — hero stat modifiers
--
-- Weapon DPS = (Damage + Holy + Fire + Electric) × Attack Speed, rounded
-- to 1 decimal. Sentinels: value >= 1000 renders '∞' (sorts top);
-- value <= -1000 renders '—' (sorts bottom). Rows carry data-element /
-- data-class / data-tier / data-type attributes for the Common.js chips.
local Core = require('Module:Core')
local p = {}
local TBL_STYLE = 'font-size:0.92em;background:var(--table-row-odd, #1b1c20);color:var(--table-text, #e6e6e6);border-color:var(--table-border, #3a3c44);'
local TH_STYLE = 'background:var(--table-header-bg, #26272e);color:var(--infobox-header-fg, #f1e9d2);'
local function swatch(label, tok, hexv)
return '<span style="color:var(' .. tok .. ', ' .. hexv
.. ');font-weight:bold;white-space:nowrap;">● ' .. label .. '</span>'
end
-- Coerce a raw stat value to {display, sortval}. Mirrors the historic
-- emit-time _num(): '15%' keeps the % display but sorts numerically.
local function num(v)
if v == nil then return '—', nil end
if type(v) == 'string' then
local s = v:gsub('^%s+', ''):gsub('%s+$', '')
local n = tonumber(s) or tonumber(s:match('^([%-%d%.]+)%%$'))
if n == nil then return s, nil end
if s:sub(-1) == '%' then return s, n end
v = n
end
if type(v) == 'number' then
if v >= 1000 then return '∞', 1e9 end
if v <= -1000 then return '—', -1 end
return Core.fmtNum(v), v
end
return tostring(v), nil
end
local function numcell(v)
local t, s = num(v)
return {t = t, s = s}
end
local DMG = {
Holy = {'--dmg-holy', '#e8c34a', 'wm-dmg-holy'},
Fire = {'--dmg-fire', '#ff6b35', 'wm-dmg-fire'},
Electric = {'--dmg-electric', '#4ea3ff', 'wm-dmg-electric'},
}
local function chipcell(dmg, v)
local t, s = num(v)
if s == nil or s <= 0 or t == '—' then return {t = '—', s = 0} end
local c = DMG[dmg]
return {t = '<span class="' .. c[3] .. '" style="color:var(' .. c[1] .. ', '
.. c[2] .. ');font-weight:bold;">' .. t .. '</span>', s = s}
end
local function iconlinkcell(rec)
local slug = rec.slug or rec.name or ''
local name = rec.name or slug
local t
if rec.icon and rec.icon ~= '' then
t = '[[File:' .. rec.icon .. '|24px|link=' .. slug .. '|alt=' .. name
.. ']] [[' .. slug .. '|' .. name .. ']]'
else
t = '[[' .. slug .. '|' .. name .. ']]'
end
t = '<span class="wm-tip" data-tip-title="' .. slug .. '">' .. t .. '</span>'
return {t = t, s = mw.ustring.lower(name)}
end
local function stat(rec, k)
return (rec.stats or {})[k]
end
local function nstat(rec, k)
return Core.numStat(stat(rec, k))
end
local function totalDPS(rec)
local s = rec.stats or {}
local atk = Core.numStat(s['Attack Speed'])
if atk == nil then return nil end
local total = Core.numStat(s['Damage']) or 0
for _, k in ipairs({'Holy Damage', 'Fire Damage', 'Electric Damage'}) do
local v = Core.numStat(s[k])
if v ~= nil and v > 0 and v < 1000 then total = total + v end
end
return Core.round1(total * atk)
end
local function wave10(rec, base, per)
local b, pv = nstat(rec, base), nstat(rec, per)
if b == nil or pv == nil then return nil end
return Core.round1(b + pv * 9)
end
-- ----- dataset builders ----------------------------------------------
local function weaponRows(class)
local headers = {'Weapon', 'Tier', 'Price', 'Damage',
swatch('Holy', '--dmg-holy', '#e8c34a'),
swatch('Fire', '--dmg-fire', '#ff6b35'),
swatch('Electric', '--dmg-electric', '#4ea3ff'),
'DPS', 'Atk Speed', 'Crit', 'Range', 'Pierce', 'Proj.', 'Bounce'}
local rows = {}
for _, rec in ipairs(Core.load('Weapons')) do
if Core.weaponClass(rec) == class then
local cells = {
iconlinkcell(rec),
numcell(rec.tier),
numcell(rec.price),
numcell(stat(rec, 'Damage')),
chipcell('Holy', stat(rec, 'Holy Damage')),
chipcell('Fire', stat(rec, 'Fire Damage')),
chipcell('Electric', stat(rec, 'Electric Damage')),
numcell(totalDPS(rec)),
numcell(stat(rec, 'Attack Speed')),
numcell(stat(rec, 'Critical Chance')),
numcell(stat(rec, 'Range')),
numcell(stat(rec, 'Pierce')),
numcell(stat(rec, 'Projectiles')),
numcell(stat(rec, 'Bounce')),
}
local elements = {}
for _, e in ipairs({{'holy', 'Holy Damage'}, {'fire', 'Fire Damage'},
{'electric', 'Electric Damage'}}) do
local v = nstat(rec, e[2])
if v ~= nil and v > 0 and v < 1000 then elements[#elements + 1] = e[1] end
end
local attrs = ' data-element="' .. (#elements > 0 and table.concat(elements, ' ') or 'none')
.. '" data-class="' .. class:lower()
.. '" data-tier="' .. tostring(rec.tier or '') .. '"'
rows[#rows + 1] = {a = attrs, c = cells}
end
end
-- DPS descending (stable: name asc tiebreak via the first cell sortkey).
table.sort(rows, function(x, y)
local dx, dy = x.c[8].s or 0, y.c[8].s or 0
if dx ~= dy then return dx > dy end
return (x.c[1].s or '') < (y.c[1].s or '')
end)
return headers, rows
end
local function enemyRows()
local headers = {'Enemy', 'Health', 'HP/Wave', 'HP @ W10', 'Damage',
'Dmg/Wave', 'Dmg @ W10', 'Speed', 'Resources'}
local rows = {}
for _, rec in ipairs(Core.sorted('Enemies')) do
local keys = {}
for _, k in ipairs(rec.tags or {}) do keys[#keys + 1] = k:lower() end
rows[#rows + 1] = {
a = ' data-type="' .. (#keys > 0 and table.concat(keys, ' ') or 'other') .. '"',
c = {
iconlinkcell(rec),
numcell(stat(rec, 'Health')),
numcell(stat(rec, 'HP per Wave')),
numcell(wave10(rec, 'Health', 'HP per Wave')),
numcell(stat(rec, 'Damage')),
numcell(stat(rec, 'Damage per Wave')),
numcell(wave10(rec, 'Damage', 'Damage per Wave')),
numcell(stat(rec, 'Speed')),
numcell(stat(rec, 'Resources')),
},
}
end
return headers, rows
end
local function characterRows()
local headers = {'Hero', 'Health Points', 'Damage', 'Critical Chance',
'Attack Speed', 'Range', 'Speed', 'Armor', 'Dodge'}
local rows = {}
for _, rec in ipairs(Core.sorted('Characters')) do
rows[#rows + 1] = {a = '', c = {
iconlinkcell(rec),
numcell(stat(rec, 'Health Points')),
numcell(stat(rec, 'Damage')),
numcell(stat(rec, 'Critical Chance')),
numcell(stat(rec, 'Attack Speed')),
numcell(stat(rec, 'Range')),
numcell(stat(rec, 'Speed')),
numcell(stat(rec, 'Armor')),
numcell(stat(rec, 'Dodge')),
}}
end
return headers, rows
end
local DATASETS = {
Weapons_Melee = function() return weaponRows('Melee') end,
Weapons_Ranged = function() return weaponRows('Ranged') end,
Enemies = enemyRows,
Characters = characterRows,
}
-- ----- renderer --------------------------------------------------------
local function cell(c)
local t = tostring(c.t or '—')
if c.s ~= nil then
local s = c.s
if type(s) == 'number' then s = Core.fmtNum(s) end
return '| data-sort-value="' .. tostring(s) .. '" | ' .. t
end
return '| ' .. t
end
function p.render(frame)
local ds = frame.args[1] or frame.args.cat
if not ds or ds == '' then
return '<strong class="error">Module:Compare: missing dataset arg</strong>'
end
ds = ds:gsub('^%s+', ''):gsub('%s+$', '')
local build = DATASETS[ds]
if not build then
return '<strong class="error">Module:Compare: unknown dataset '
.. mw.text.nowiki(ds) .. '</strong>'
end
local headers, rows = build()
local out = { '{| class="wikitable sortable" style="' .. TBL_STYLE .. '"' }
out[#out+1] = '|-'
for _, h in ipairs(headers) do
out[#out+1] = '! style="' .. TH_STYLE .. '" | ' .. tostring(h)
end
for _, row in ipairs(rows) do
out[#out+1] = '|-' .. tostring(row.a or '')
for _, c in ipairs(row.c) do
out[#out+1] = cell(c)
end
end
out[#out+1] = '|}'
return table.concat(out, '\n')
end
return p