From Arms of God Wiki
Documentation for this module may be created at Module:Compare/doc
-- Module:Compare — sortable side-by-side comparison tables.
--
-- WHAT IT DOES
-- Builds the big comparison tables (used on the Weapons index, the
-- Enemies index, and the Weapon comparison / Hero comparison pages;
-- 'Enemy comparison' is a redirect to Enemies since 2026-06-10f),
-- including the COMPUTED columns that are not stored anywhere:
-- * Weapon DPS = (Damage + Holy + Fire + Electric) x Attack Speed,
-- rounded to 1 decimal.
-- * Enemy wave-10 projections = base + per-wave x 9.
--
-- HOW TO INVOKE
-- {{#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
--
-- SOURCE DATA IT READS (via Module:Core)
-- Data:Weapons.json, Data:Enemies.json, Data:Characters.json
--
-- EDITING NOTES
-- * Every table opens with a Thumbnail column (icon only, header marked
-- class="unsortable") followed by a sortable linked Name column.
-- * Stat sentinels: a value >= 1000 renders as the infinity sign
-- (sorts to the top); <= -1000 renders as an em-dash (sorts bottom).
-- * Each row carries data-element / data-class / data-tier / data-type
-- attributes — MediaWiki:Common.js uses them for the filter chips
-- above the tables. Removing them breaks the chip filtering.
-- * To add a column, add a header in the relevant *Rows() builder and
-- a matching cell in the same position.
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);'
-- Colored element swatch for column headers. When the damage-type hub
-- page exists (existence-checked via Core.statPageTarget) the label links
-- to it; an inner span keeps the element color inside the anchor.
local function swatch(label, tok, hexv, statName)
local inner = label
local target = statName and Core.statPageTarget(statName)
if target then
inner = '[[' .. target .. '|<span style="color:var(' .. tok .. ', '
.. hexv .. ');font-weight:bold;">' .. label .. '</span>]]'
end
return '<span style="color:var(' .. tok .. ', ' .. hexv
.. ');font-weight:bold;white-space:nowrap;">● ' .. inner .. '</span>'
end
-- Abbreviated column headers -> the stat key whose page they should link
-- to. Full-name headers resolve via Core.statPageTarget directly.
local HDR_STAT = {
['Atk Speed'] = 'Attack Speed',
['Crit'] = 'Critical Chance',
['Proj.'] = 'Projectiles',
}
-- Link a plain-text column header to its stat reference page when one
-- exists; non-stat headers (and already-HTML headers like the swatches)
-- pass through untouched. Page-less stats stay plain — never a redlink.
local function headerLink(h)
if h:find('[<%[]') then return h end
local target = Core.statPageTarget(HDR_STAT[h] or h)
if not target then return h end
if target == h then return '[[' .. h .. ']]' end
return '[[' .. target .. '|' .. h .. ']]'
end
-- Coerce a raw stat value to {display, sortval}: '15%' keeps the %
-- sign for display but sorts by the number 15.
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
-- Header marker for the icon-only Thumbnail column: the renderer emits
-- class="unsortable" on this th so the sort arrows skip it.
local THUMB = {'Thumbnail', unsortable = true}
-- Icon-only Thumbnail cell (links to the entity page; em-dash when the
-- record has no icon). 48px matches the category-index thumbnail size.
local function iconcell(rec)
if rec.icon and rec.icon ~= '' then
local slug = rec.slug or rec.name or ''
return {t = '[[File:' .. rec.icon .. '|48px|link=' .. slug
.. '|alt=' .. (rec.name or slug) .. ']]'}
end
return {t = '—'}
end
-- Linked-name cell, sortable by lowercased name, wrapped in .wm-tip
-- (Common.js hover-infobox popup).
local function namecell(rec)
local slug = rec.slug or rec.name or ''
local name = rec.name or slug
local t = '<span class="wm-tip" data-tip-title="' .. slug .. '">[['
.. slug .. '|' .. name .. ']]</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 = {THUMB, 'Name', 'Tier', 'Price', 'Damage',
swatch('Holy', '--dmg-holy', '#e8c34a', 'Holy Damage'),
swatch('Fire', '--dmg-fire', '#ff6b35', 'Fire Damage'),
swatch('Electric', '--dmg-electric', '#4ea3ff', 'Electric Damage'),
'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 = {
iconcell(rec),
namecell(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 (cell 9 after the Thumbnail/Name split; stable:
-- name asc tiebreak via the Name cell sortkey, cell 2).
table.sort(rows, function(x, y)
local dx, dy = x.c[9].s or 0, y.c[9].s or 0
if dx ~= dy then return dx > dy end
return (x.c[2].s or '') < (y.c[2].s or '')
end)
return headers, rows
end
local function enemyRows()
local headers = {THUMB, 'Name', '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 = {
iconcell(rec),
namecell(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 = {THUMB, 'Name', '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 = {
iconcell(rec),
namecell(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
local label, attrs = h, ''
if type(h) == 'table' then
label = h[1]
if h.unsortable then attrs = ' class="unsortable"' end
end
out[#out+1] = '!' .. attrs .. ' style="' .. TH_STYLE .. '" | '
.. headerLink(tostring(label))
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