Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Join the Playtest on Steam Now: SpiritVale

Module:GameSkills: Difference between revisions

From SpiritVale Wiki
No edit summary
No edit summary
Line 1: Line 1:
-- Module:GameSkills
-- Module:GameSkills
--
--
-- Renders SpiritVale skill infoboxes and class/user skill lists.
-- Phase 6.5+ (Plug-in Slot Architecture)
--
--
-- Phase 6.5+ (Plug-in Slot Architecture)
-- Layout:
-- Layout:
--  1) hero-title-bar        (TOP BAR, 2 slots: herobar 1..2)
--  1) hero-title-bar        (TOP BAR, 2 slots: herobar 1..2)
Line 10: Line 9:
--
--
-- Requires Common.js:
-- Requires Common.js:
--  - Updates .sv-dyn spans via data-series
--  - updates .sv-dyn spans via data-series
--  - Updates .sv-level-num + data-level on .sv-skill-card
--  - updates .sv-level-num + data-level on .sv-skill-card
--  - Binds to input.sv-level-range inside each card
--  - binds to input.sv-level-range inside each card


local GameData = require("Module:GameData")
local GameData = require("Module:GameData")
Line 24: Line 23:
local skillsCache
local skillsCache


-- Load skills once per page parse (cached).
-- getSkills: lazy-load + cache skill dataset from GameData.
local function getSkills()
local function getSkills()
if not skillsCache then
if not skillsCache then
Line 36: Line 35:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Read template args from parent if present.
-- getArgs: read args from parent frame when invoked from a template.
local function getArgs(frame)
local function getArgs(frame)
local parent = frame:getParent()
local parent = frame:getParent()
Line 42: Line 41:
end
end


-- Trim string, returning nil if empty/non-string.
-- trim: normalize strings (nil if empty).
local function trim(s)
local function trim(s)
if type(s) ~= "string" then
if type(s) ~= "string" then
Line 51: Line 50:
end
end


-- Coerce number-like values to a Lua number (supports { Value = ... }).
-- toNum: convert common scalar/table forms to a Lua number.
local function toNum(v)
local function toNum(v)
if type(v) == "number" then
if type(v) == "number" then
Line 65: Line 64:
end
end


-- Clamp numeric value into [lo, hi].
-- clamp: clamp a number into [lo, hi].
local function clamp(n, lo, hi)
local function clamp(n, lo, hi)
if type(n) ~= "number" then
if type(n) ~= "number" then
Line 75: Line 74:
end
end


-- Format numbers cleanly (integers without decimals, floats up to 4 dp).
-- fmtNum: consistent number formatting (trim trailing zeros).
local function fmtNum(n)
local function fmtNum(n)
if type(n) ~= "number" then
if type(n) ~= "number" then
Line 91: Line 90:
end
end


-- Join list to text, or nil if empty.
-- listToText: join an array into a readable string.
local function listToText(list, sep)
local function listToText(list, sep)
if type(list) ~= "table" or #list == 0 then
if type(list) ~= "table" or #list == 0 then
Line 99: Line 98:
end
end


-- Treat common “none-like” strings as empty.
-- isNoneLike: treat common "none" spellings as empty.
local function isNoneLike(v)
local function isNoneLike(v)
if v == nil then return true end
if v == nil then return true end
Line 108: Line 107:
end
end


-- Add an infobox row (label/value). Skips nil/empty values.
-- addRow: add a standard <tr><th>Label</th><td>Value</td></tr> row.
local function addRow(tbl, label, value, rowClass, dataKey)
local function addRow(tbl, label, value, rowClass, dataKey)
if value == nil or value == "" then
if value == nil or value == "" then
Line 123: Line 122:
end
end


-- Format either a scalar or { Value, Unit } pair to a display string.
-- formatUnitValue: format {Value, Unit} blocks (or scalar) for display.
local function formatUnitValue(v)
local function formatUnitValue(v)
if type(v) == "table" and v.Value ~= nil then
if type(v) == "table" and v.Value ~= nil then
Line 151: Line 150:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Build a .sv-dyn span that Common.js can update per selected level.
-- dynSpan: render a JS-updated span for a level series.
local function dynSpan(series, level)
local function dynSpan(series, level)
if type(series) ~= "table" or #series == 0 then
if type(series) ~= "table" or #series == 0 then
Line 167: Line 166:
end
end


-- True if list values are all identical (no need for a dyn span).
-- isFlatList: true if all values in list are identical.
local function isFlatList(list)
local function isFlatList(list)
if type(list) ~= "table" or #list == 0 then
if type(list) ~= "table" or #list == 0 then
Line 181: Line 180:
end
end


-- Treat value as present if not “zeroish”.
-- isNonZeroScalar: detect if a value is present and not effectively zero.
local function isNonZeroScalar(v)
local function isNonZeroScalar(v)
if v == nil then return false end
if v == nil then return false end
Line 196: Line 195:
end
end


-- Aggressive “is zero/empty” check used for hiding values.
-- isZeroish: aggressively treat common “zero” text forms as zero.
local function isZeroish(v)
local function isZeroish(v)
if v == nil then return true end
if v == nil then return true end
Line 215: Line 214:
end
end


-- Render a Base/Per Level block as a single readable string (non-dynamic).
-- valuePairRawText: render Base/Per Level blocks into readable text (fallback).
local function valuePairRawText(block)
local function valuePairRawText(block)
if type(block) ~= "table" then
if type(block) ~= "table" then
Line 249: Line 248:
end
end


-- Render only the value portion (dynamic if per-level series exists).
-- valuePairDynamicValueOnly: render Base/Per Level blocks using dyn spans where possible.
local function valuePairDynamicValueOnly(block, maxLevel, level)
local function valuePairDynamicValueOnly(block, maxLevel, level)
if type(block) ~= "table" then
if type(block) ~= "table" then
Line 285: Line 284:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Fetch skill record by internal ID.
-- getSkillById: locate a skill by internal ID.
local function getSkillById(id)
local function getSkillById(id)
id = trim(id)
id = trim(id)
Line 293: Line 292:
end
end


-- Find skill record by display/external/internal names.
-- findSkillByName: locate a skill by external/display name.
local function findSkillByName(name)
local function findSkillByName(name)
name = trim(name)
name = trim(name)
Line 320: Line 319:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Decide label for legacy damage basis.
-- basisLabel: label ATK/MATK basis in legacy damage blocks.
local function basisLabel(entry, isHealing)
local function basisLabel(entry, isHealing)
if isHealing then
if isHealing then
Line 340: Line 339:
end
end


-- Build dynamic legacy damage percent string for a single entry.
-- formatDamageEntry: legacy percent damage formatting (dynamic by level).
local function formatDamageEntry(entry, maxLevel, level)
local function formatDamageEntry(entry, maxLevel, level)
if type(entry) ~= "table" then
if type(entry) ~= "table" then
Line 392: Line 391:
end
end


-- Render a list of legacy damage entries (stacked).
-- formatDamageList: render a list of legacy damage entries into <br/> blocks.
local function formatDamageList(list, maxLevel, level, includeTypePrefix)
local function formatDamageList(list, maxLevel, level, includeTypePrefix)
if type(list) ~= "table" or #list == 0 then
if type(list) ~= "table" or #list == 0 then
Line 419: Line 418:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- True if a skill record belongs to a given user/class/summon/etc.
-- skillMatchesUser: check if a skill is used by a specific class/monster/summon/event.
local function skillMatchesUser(rec, userName)
local function skillMatchesUser(rec, userName)
if type(rec) ~= "table" or not userName or userName == "" then
if type(rec) ~= "table" or not userName or userName == "" then
Line 451: Line 450:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Determine if current page is the skill’s own page (so we can hide Users rows).
-- isDirectSkillPage: hide Users rows when viewing the skill page itself.
local function isDirectSkillPage(rec)
local function isDirectSkillPage(rec)
if type(rec) ~= "table" then
if type(rec) ~= "table" then
Line 478: Line 477:
local HERO_BAR_SLOT_ASSIGNMENT = {
local HERO_BAR_SLOT_ASSIGNMENT = {
[1] = "IconName",
[1] = "IconName",
[2] = "SkillType",
[2] = "SkillType", -- Damage/Element/Hits/Target/Cast/Combo strip
}
}


Line 492: Line 491:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Render a hero-bar slot wrapper.
-- heroBarBox: wrapper for hero-bar slot modules.
local function heroBarBox(slot, extraClasses, innerHtml, isEmpty)
local function heroBarBox(slot, extraClasses, innerHtml, isEmpty)
local box = mw.html.create("div")
local box = mw.html.create("div")
Line 523: Line 522:
end
end


-- Render a hero-module slot wrapper (2x2 grid tiles).
-- moduleBox: wrapper for hero-module (2x2 grid) slot modules.
local function moduleBox(slot, extraClasses, innerHtml, isEmpty)
local function moduleBox(slot, extraClasses, innerHtml, isEmpty)
local box = mw.html.create("div")
local box = mw.html.create("div")
Line 554: Line 553:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Format scaling list into “X% Stat” compact lines.
-- formatScalingCompactLines: build compact “Scaling” lines for SourceType.
local function formatScalingCompactLines(scaling)
local function formatScalingCompactLines(scaling)
if type(scaling) ~= "table" then
if type(scaling) ~= "table" then
Line 587: Line 586:
end
end


-- Compute modifier word from ATK/MATK flags (used in SourceType module).
-- basisWordFromFlags: compute “Attack/Magic/Hybrid” from ATK/MATK booleans.
local function basisWordFromFlags(atkFlag, matkFlag)
local function basisWordFromFlags(atkFlag, matkFlag)
if atkFlag and matkFlag then
if atkFlag and matkFlag then
Line 599: Line 598:
end
end


-- Legacy damage percent at a given level.
-- legacyPercentAtLevel: compute “Base% + PerLevel%*level” for legacy entries.
local function legacyPercentAtLevel(entry, level)
local function legacyPercentAtLevel(entry, level)
if type(entry) ~= "table" then
if type(entry) ~= "table" then
Line 625: Line 624:
end
end


-- Expand a Base/Per Level block into a per-level display series.
-- seriesFromValuePair: normalize Base/Per Level blocks into a level-indexed series.
local function seriesFromValuePair(block, maxLevel)
local function seriesFromValuePair(block, maxLevel)
if type(block) ~= "table" then
if type(block) ~= "table" then
Line 706: Line 705:
end
end


-- Convert a per-level series into a display value (flat -> plain text, else dyn span).
-- displayFromSeries: render a series as fixed text or dynSpan (nil if all “—”).
local function displayFromSeries(series, level)
local function displayFromSeries(series, level)
if type(series) ~= "table" or #series == 0 then
if type(series) ~= "table" or #series == 0 then
Line 729: Line 728:
end
end


-- Format Area Size block into a human display string.
-- formatAreaSize: human readable area sizing for QuickStats.
local function formatAreaSize(area)
local function formatAreaSize(area)
if type(area) ~= "table" then
if type(area) ~= "table" then
Line 779: Line 778:
end
end


-- True if any non-empty damage/source exists for this skill (used for hiding Damage/Element/Hits).
-- skillHasAnyDamage: determine if a skill has any meaningful damage (for non-damaging rules).
local function skillHasAnyDamage(rec, maxLevel)
local function skillHasAnyDamage(rec, maxLevel)
if type(rec.Source) == "table" then
if type(rec.Source) == "table" then
Line 803: Line 802:
end
end


-- Promote first status-application duration into QuickStats if Duration is otherwise empty (non-damaging only).
-- computeDurationPromotion: promote status-duration into QuickStats when a skill is non-damaging.
local function computeDurationPromotion(rec, maxLevel)
local function computeDurationPromotion(rec, maxLevel)
if type(rec) ~= "table" then return nil end
if type(rec) ~= "table" then return nil end
Line 850: Line 849:
local PLUGINS = {}
local PLUGINS = {}


-- Hero Bar Slot 1: Icon + Name
-- PLUGIN: IconName (Hero Bar Slot 1) - icon + name.
function PLUGINS.IconName(rec, ctx)
function PLUGINS.IconName(rec, ctx)
local icon  = rec.Icon
local icon  = rec.Icon
Line 874: Line 873:
end
end


-- Hero Bar Slot 2: Skill Type (Damage/Element/Hits/Target/Cast/Combo)
-- PLUGIN: ReservedInfo (Hero Bar Slot 2 placeholder) - kept for future.
function PLUGINS.SkillType(rec, ctx)
function PLUGINS.ReservedInfo(rec, ctx)
local wrap = mw.html.create("div")
wrap:addClass("sv-herobar-2-wrap")
return {
inner = tostring(wrap),
classes = "module-herobar-2",
}
end
 
-- PLUGIN: LevelSelector (Hero Module Slot 4) - JS level slider.
function PLUGINS.LevelSelector(rec, ctx)
local level = ctx.level or 1
local level = ctx.level or 1
local maxLevel = ctx.maxLevel or 1
local maxLevel = ctx.maxLevel or 1


local typeBlock = (type(rec.Type) == "table") and rec.Type or {}
local inner = mw.html.create("div")
local mech = (type(rec.Mechanics) == "table") and rec.Mechanics or {}
inner:addClass("sv-level-ui")
 
inner:tag("div")
:addClass("sv-level-label")
:wikitext("Level <span class=\"sv-level-num\">" .. tostring(level) .. "</span> / " .. tostring(maxLevel))


-- Rule: If Damage is none/non-damaging -> hide Damage, Element, Hits.
local slider = inner:tag("div"):addClass("sv-level-slider")
local hideDamageElementHits = (ctx.nonDamaging == true)


-- Extract a friendly display string from common { Name/ID/Value } tables.
if tonumber(maxLevel) and tonumber(maxLevel) > 1 then
local function valName(x)
slider:tag("input")
if x == nil then return nil end
:attr("type", "range")
if type(x) == "table" then
:attr("min", "1")
if x.Name and x.Name ~= "" then return tostring(x.Name) end
:attr("max", tostring(maxLevel))
if x.ID and x.ID ~= "" then return tostring(x.ID) end
:attr("value", tostring(level))
:addClass("sv-level-range")
:attr("aria-label", "Skill level select")
else
inner:addClass("sv-level-ui-single")
slider:addClass("sv-level-slider-single")
end
 
return {
inner = tostring(inner),
classes = "module-level-selector",
}
end
 
-- PLUGIN: SkillType (Hero Bar Slot 2) - 2 rows x 3 cells (desktop + mobile).
-- Rules:
--  - If skill is non-damaging, hide Damage/Element/Hits.
--  - If Hits is empty, hide Hits.
--  - If Combo is empty, hide Combo.
-- Ordering:
--  - Desktop: Damage, Element, Hits, Target, Cast, Combo
--  - Mobile:  Damage, Element, Target, Cast, Hits, Combo (CSS reorder)
function PLUGINS.SkillType(rec, ctx)
local typeBlock = (type(rec.Type) == "table") and rec.Type or {}
local mech      = (type(rec.Mechanics) == "table") and rec.Mechanics or {}
 
local level    = ctx.level or 1
local maxLevel = ctx.maxLevel or 1
 
local hideDamageBundle = (ctx.nonDamaging == true)
 
-- valName: extract a display string from typical {Name/ID/Value} objects.
local function valName(x)
if x == nil then return nil end
if type(x) == "table" then
if x.Name and x.Name ~= "" then return tostring(x.Name) end
if x.ID and x.ID ~= "" then return tostring(x.ID) end
if x.Value ~= nil then return tostring(x.Value) end
if x.Value ~= nil then return tostring(x.Value) end
end
end
Line 899: Line 947:
end
end


-- Build “Combo” display text (Type + optional percent/duration).
-- hitsDisplay: find + render Hits from multiple possible structured locations.
local function formatComboText(combo)
local function hitsDisplay()
if combo == nil then return nil end
local h =
typeBlock.Hits or typeBlock["Hits"] or typeBlock["Hit Count"] or typeBlock["Hits Count"] or
mech.Hits or mech["Hits"] or mech["Hit Count"] or mech["Hits Count"] or
rec.Hits or rec["Hits"]


-- Sometimes data may store combo as a simple string.
if h == nil or isNoneLike(h) then
if type(combo) == "string" then
return nil
local s = trim(combo)
end
return (s and not isNoneLike(s)) and mw.text.nowiki(s) or nil
 
-- ValuePair-style table (Base/Per Level) => dynamic series
if type(h) == "table" then
if h.Base ~= nil or h["Per Level"] ~= nil or type(h["Per Level"]) == "table" then
return displayFromSeries(seriesFromValuePair(h, maxLevel), level)
end
 
-- Unit block {Value, Unit}
if h.Value ~= nil then
local t = formatUnitValue(h)
return t and mw.text.nowiki(t) or nil
end
 
-- Fallback name extraction
local vn = valName(h)
if vn and not isNoneLike(vn) then
return mw.text.nowiki(vn)
end
end
end


if type(combo) ~= "table" then
-- Scalar number/string
return nil
if type(h) == "number" then
return mw.text.nowiki(fmtNum(h))
end
if type(h) == "string" then
local t = trim(h)
return (t and not isNoneLike(t)) and mw.text.nowiki(t) or nil
end
end


local typ = trim(combo.Type)
return nil
end
 
-- comboDisplay: render Combo as a compact text block (Type (+ details)).
local function comboDisplay()
local c = (type(mech.Combo) == "table") and mech.Combo or nil
if not c then return nil end
 
local typ = trim(c.Type)
if not typ or isNoneLike(typ) then
if not typ or isNoneLike(typ) then
return nil
return nil
Line 920: Line 1,001:
local details = {}
local details = {}


local pct = formatUnitValue(combo.Percent)
local pct = formatUnitValue(c.Percent)
if pct and not isZeroish(pct) then
if pct and not isZeroish(pct) then
table.insert(details, mw.text.nowiki(pct))
table.insert(details, mw.text.nowiki(pct))
end
end


local dur = formatUnitValue(combo.Duration)
local dur = formatUnitValue(c.Duration)
if dur and not isZeroish(dur) then
if dur and not isZeroish(dur) then
table.insert(details, mw.text.nowiki(dur))
table.insert(details, mw.text.nowiki(dur))
Line 935: Line 1,016:
return mw.text.nowiki(typ)
return mw.text.nowiki(typ)
end
end
-- Compute “Hits” display (supports Base/Per Level blocks and scalars).
local function formatHitsValue(rawHits)
if rawHits == nil or isNoneLike(rawHits) or isZeroish(rawHits) then
return nil
end
if type(rawHits) == "table" then
-- If it looks like a value-pair block, prefer series rendering.
local s = seriesFromValuePair(rawHits, maxLevel)
local disp = displayFromSeries(s, level)
if disp then return disp end
-- Fallback: raw rendering.
local txt = valuePairDynamicValueOnly(rawHits, maxLevel, level)
return txt
end
return mw.text.nowiki(tostring(rawHits))
end
local hitsVal = nil
if not hideDamageElementHits then
hitsVal = formatHitsValue(mech.Hits or mech["Hits"])
end
local comboVal = formatComboText(mech.Combo or mech["Combo"])


local grid = mw.html.create("div")
local grid = mw.html.create("div")
Line 969: Line 1,023:
local added = false
local added = false


-- Add a grid “chunk” cell. If isHtml=true, value is already safe wikitext/HTML.
-- addChunk: add one labeled value cell (key drives CSS ordering).
local function addChunk(label, value, chunkClass, isHtml)
local function addChunk(key, label, valueHtml)
if value == nil or value == "" then return end
if valueHtml == nil or valueHtml == "" then return end
added = true
added = true


local chunk = grid:tag("div"):addClass("sv-type-chunk")
local chunk = grid:tag("div")
if chunkClass then chunk:addClass(chunkClass) end
:addClass("sv-type-chunk")
:addClass("sv-type-" .. tostring(key))
:attr("data-type-key", tostring(key))


chunk:tag("div"):addClass("sv-type-label"):wikitext(mw.text.nowiki(label))
chunk:tag("div")
:addClass("sv-type-label")
:wikitext(mw.text.nowiki(label))


local v = chunk:tag("div"):addClass("sv-type-value")
chunk:tag("div")
if isHtml then
:addClass("sv-type-value")
v:wikitext(value)
:wikitext(valueHtml)
else
v:wikitext(mw.text.nowiki(tostring(value)))
end
end
end


-- Desktop order (row-major 4 columns):
-- Damage + Element + Hits bundle (hidden when non-damaging)
-- Damage, Element, Hits, Target / Cast, Combo
if not hideDamageBundle then
if not hideDamageElementHits then
local dmg = valName(typeBlock.Damage or typeBlock["Damage Type"])
addChunk("Damage"valName(typeBlock.Damage  or typeBlock["Damage Type"]), "sv-type-chunk--damage", false)
local ele = valName(typeBlock.Element or typeBlock["Element Type"])
addChunk("Element", valName(typeBlock.Element or typeBlock["Element Type"]), "sv-type-chunk--element", false)
local hits = hitsDisplay()
addChunk("Hits",   hitsVal,                                              "sv-type-chunk--hits",   true)
 
if dmg and not isNoneLike(dmg) then
addChunk("damage", "Damage", mw.text.nowiki(dmg))
end
if ele and not isNoneLike(ele) then
addChunk("element", "Element", mw.text.nowiki(ele))
end
-- Hits: only render when present and meaningful
if hits then
addChunk("hits", "Hits", hits)
end
end
 
-- Target + Cast
local tgt = valName(typeBlock.Target or typeBlock["Target Type"])
local cst = valName(typeBlock.Cast  or typeBlock["Cast Type"])
 
if tgt and not isNoneLike(tgt) then
addChunk("target", "Target", mw.text.nowiki(tgt))
end
if cst and not isNoneLike(cst) then
addChunk("cast", "Cast", mw.text.nowiki(cst))
end
end


addChunk("Target", valName(typeBlock.Target or typeBlock["Target Type"]), "sv-type-chunk--target", false)
-- Combo (moved here)
addChunk("Cast",  valName(typeBlock.Cast  or typeBlock["Cast Type"]),  "sv-type-chunk--cast",  false)
local combo = comboDisplay()
addChunk("Combo", comboVal,                                              "sv-type-chunk--combo", true)
if combo then
addChunk("combo", "Combo", combo)
end


return {
return {
Line 1,005: Line 1,083:
end
end


-- Module Tile: Level Selector (hero-module-4)
-- PLUGIN: SourceType (Hero Module Slot 1) - Modifier + Source + Scaling.
function PLUGINS.LevelSelector(rec, ctx)
function PLUGINS.SourceType(rec, ctx)
local level = ctx.level or 1
local level = ctx.level or 1
local maxLevel = ctx.maxLevel or 1
local maxLevel = ctx.maxLevel or 1


local inner = mw.html.create("div")
local basisWord = nil
inner:addClass("sv-level-ui")
local sourceKind = nil
local sourceVal  = nil
local scaling    = nil


inner:tag("div")
-- sourceValueForLevel: dynamic formatting for structured Source blocks.
:addClass("sv-level-label")
local function sourceValueForLevel(src)
:wikitext("Level <span class=\"sv-level-num\">" .. tostring(level) .. "</span> / " .. tostring(maxLevel))
if type(src) ~= "table" then
return nil
end


local slider = inner:tag("div"):addClass("sv-level-slider")
local per = src["Per Level"]
if type(per) == "table" and #per > 0 then
if isFlatList(per) then
local one  = formatUnitValue(per[1]) or tostring(per[1])
local show = formatUnitValue(src.Base) or one
return show and mw.text.nowiki(show) or nil
end
 
local series = {}
for _, v in ipairs(per) do
table.insert(series, formatUnitValue(v) or tostring(v))
end
return dynSpan(series, level)
end


if tonumber(maxLevel) and tonumber(maxLevel) > 1 then
return valuePairDynamicValueOnly(src, maxLevel, level)
slider:tag("input")
:attr("type", "range")
:attr("min", "1")
:attr("max", tostring(maxLevel))
:attr("value", tostring(level))
:addClass("sv-level-range")
:attr("aria-label", "Skill level select")
else
inner:addClass("sv-level-ui-single")
slider:addClass("sv-level-slider-single")
end
end


return {
if type(rec.Source) == "table" then
inner = tostring(inner),
local src = rec.Source
classes = "module-level-selector",
local atkFlag  = (src["ATK-Based"] == true)
}
local matkFlag = (src["MATK-Based"] == true)
end
basisWord = basisWordFromFlags(atkFlag, matkFlag)


-- Module Tile: SourceType (Modifier + Source + Scaling)
sourceKind = src.Type or ((src.Healing == true) and "Healing") or "Damage"
function PLUGINS.SourceType(rec, ctx)
sourceVal  = sourceValueForLevel(src)
local level = ctx.level or 1
scaling    = src.Scaling
local maxLevel = ctx.maxLevel or 1
end


local basisWord = nil
-- Fallback to legacy Damage lists if Source absent
local sourceKind = nil
if (sourceVal == nil or sourceVal == "") and type(rec.Damage) == "table" then
local sourceVal  = nil
local dmg = rec.Damage
local scaling   = nil
scaling = scaling or dmg.Scaling


-- Render Source value at selected level (supports per-level series).
local main = dmg["Main Damage"]
local function sourceValueForLevel(src)
local refl = dmg["Reflect Damage"]
if type(src) ~= "table" then
local flat = dmg["Flat Damage"]
return nil
end


local per = src["Per Level"]
if type(main) == "table" and #main > 0 then
if type(per) == "table" and #per > 0 then
local pick = nil
if isFlatList(per) then
for _, d in ipairs(main) do
local one  = formatUnitValue(per[1]) or tostring(per[1])
if type(d) == "table" and d.Type ~= "Healing" then
local show = formatUnitValue(src.Base) or one
pick = d
return show and mw.text.nowiki(show) or nil
break
end
end
end
pick = pick or main[1]


local series = {}
if type(pick) == "table" then
for _, v in ipairs(per) do
local atkFlag  = (pick["ATK-Based"] == true)
table.insert(series, formatUnitValue(v) or tostring(v))
local matkFlag = (pick["MATK-Based"] == true)
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)
 
sourceKind = (pick.Type == "Healing") and "Healing" or "Damage"
sourceVal  = legacyPercentAtLevel(pick, level)
end
end
return dynSpan(series, level)
elseif type(refl) == "table" and #refl > 0 and type(refl[1]) == "table" then
local pick = refl[1]
local atkFlag  = (pick["ATK-Based"] == true)
local matkFlag = (pick["MATK-Based"] == true)
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)
 
sourceKind = "Reflect"
sourceVal  = legacyPercentAtLevel(pick, level)
elseif type(flat) == "table" and #flat > 0 and type(flat[1]) == "table" then
local pick = flat[1]
local atkFlag  = (pick["ATK-Based"] == true)
local matkFlag = (pick["MATK-Based"] == true)
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)
 
sourceKind = "Flat"
sourceVal  = legacyPercentAtLevel(pick, level)
end
end
end


return valuePairDynamicValueOnly(src, maxLevel, level)
local scalingLines = formatScalingCompactLines(scaling)
local hasSource    = (sourceVal ~= nil and tostring(sourceVal) ~= "")
local hasScaling  = (type(scalingLines) == "table" and #scalingLines > 0)
 
if (not hasSource) and (not hasScaling) then
return nil
end
end


if type(rec.Source) == "table" then
local hasMod = (basisWord ~= nil and tostring(basisWord) ~= "")
local src = rec.Source
 
local atkFlag  = (src["ATK-Based"] == true)
local extra = { "skill-source-module" }
local matkFlag = (src["MATK-Based"] == true)
table.insert(extra, hasMod and "sv-has-mod" or "sv-no-mod")
basisWord = basisWordFromFlags(atkFlag, matkFlag)


sourceKind = src.Type or ((src.Healing == true) and "Healing") or "Damage"
if hasSource and (not hasScaling) then
sourceVal  = sourceValueForLevel(src)
table.insert(extra, "sv-only-source")
scaling   = src.Scaling
elseif hasScaling and (not hasSource) then
table.insert(extra, "sv-only-scaling")
end
end


-- Fallback to legacy Damage lists if Source absent
local wrap = mw.html.create("div")
if (sourceVal == nil or sourceVal == "") and type(rec.Damage) == "table" then
wrap:addClass("sv-source-grid")
local dmg = rec.Damage
wrap:addClass("sv-compact-root")
scaling = scaling or dmg.Scaling


local main = dmg["Main Damage"]
if hasMod then
local refl = dmg["Reflect Damage"]
local modCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-modifier")
local flat = dmg["Flat Damage"]
modCol:tag("div"):addClass("sv-source-pill"):wikitext("Modifier")
modCol:tag("div"):addClass("sv-modifier-value"):wikitext(mw.text.nowiki(basisWord))
end


if type(main) == "table" and #main > 0 then
if hasSource then
local pick = nil
local sourceCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-main")
for _, d in ipairs(main) do
sourceCol:tag("div"):addClass("sv-source-pill"):wikitext(mw.text.nowiki(sourceKind or "Source"))
if type(d) == "table" and d.Type ~= "Healing" then
sourceCol:tag("div"):addClass("sv-source-value"):wikitext(sourceVal)
pick = d
end
break
end
end
pick = pick or main[1]


if type(pick) == "table" then
if hasScaling then
local atkFlag  = (pick["ATK-Based"] == true)
local scalingCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-scaling")
local matkFlag = (pick["MATK-Based"] == true)
scalingCol:tag("div"):addClass("sv-source-pill"):wikitext("Scaling")
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)


sourceKind = (pick.Type == "Healing") and "Healing" or "Damage"
local list = scalingCol:tag("div"):addClass("sv-scaling-list")
sourceVal  = legacyPercentAtLevel(pick, level)
for _, line in ipairs(scalingLines) do
end
list:tag("div"):addClass("sv-scaling-item"):wikitext(mw.text.nowiki(line))
elseif type(refl) == "table" and #refl > 0 and type(refl[1]) == "table" then
local pick = refl[1]
local atkFlag  = (pick["ATK-Based"] == true)
local matkFlag = (pick["MATK-Based"] == true)
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)
 
sourceKind = "Reflect"
sourceVal  = legacyPercentAtLevel(pick, level)
elseif type(flat) == "table" and #flat > 0 and type(flat[1]) == "table" then
local pick = flat[1]
local atkFlag  = (pick["ATK-Based"] == true)
local matkFlag = (pick["MATK-Based"] == true)
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)
 
sourceKind = "Flat"
sourceVal  = legacyPercentAtLevel(pick, level)
end
end
end
end


local scalingLines = formatScalingCompactLines(scaling)
return {
local hasSource    = (sourceVal ~= nil and tostring(sourceVal) ~= "")
inner = tostring(wrap),
local hasScaling  = (type(scalingLines) == "table" and #scalingLines > 0)
classes = extra,
}
end


if (not hasSource) and (not hasScaling) then
-- PLUGIN: QuickStats (Hero Module Slot 2) - 3x2 grid (range/area/cost/cast/cd/duration).
return nil
function PLUGINS.QuickStats(rec, ctx)
end
local level = ctx.level or 1
local maxLevel = ctx.maxLevel or 1
local promo = ctx.promo


local hasMod = (basisWord ~= nil and tostring(basisWord) ~= "")
local mech = (type(rec) == "table" and type(rec.Mechanics) == "table") and rec.Mechanics or {}
local bt  = (type(mech["Basic Timings"]) == "table") and mech["Basic Timings"] or {}
local rc  = (type(mech["Resource Cost"]) == "table") and mech["Resource Cost"] or {}


local extra = { "skill-source-module" }
local function dash() return "" end
table.insert(extra, hasMod and "sv-has-mod" or "sv-no-mod")


if hasSource and (not hasScaling) then
-- Range (0 => —)
table.insert(extra, "sv-only-source")
local rangeVal = nil
elseif hasScaling and (not hasSource) then
if mech.Range ~= nil and not isNoneLike(mech.Range) then
table.insert(extra, "sv-only-scaling")
local n = toNum(mech.Range)
end
if n ~= nil then
 
if n ~= 0 then
local wrap = mw.html.create("div")
rangeVal = mw.text.nowiki(formatUnitValue(mech.Range) or tostring(mech.Range))
wrap:addClass("sv-source-grid")
end
wrap:addClass("sv-compact-root")
else
local t = mw.text.trim(tostring(mech.Range))
if t ~= "" and not isNoneLike(t) then
rangeVal = mw.text.nowiki(t)
end
end
end
 
-- Area
local areaVal = formatAreaSize(mech.Area)


if hasMod then
-- Timings
local modCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-modifier")
local castVal = displayFromSeries(seriesFromValuePair(bt["Cast Time"], maxLevel), level)
modCol:tag("div"):addClass("sv-source-pill"):wikitext("Modifier")
local cdVal  = displayFromSeries(seriesFromValuePair(bt["Cooldown"],  maxLevel), level)
modCol:tag("div"):addClass("sv-modifier-value"):wikitext(mw.text.nowiki(basisWord))
local durVal  = displayFromSeries(seriesFromValuePair(bt["Duration"],  maxLevel), level)
end


if hasSource then
-- Promote status duration if needed
local sourceCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-main")
if (durVal == nil) and type(promo) == "table" and type(promo.durationBlock) == "table" then
sourceCol:tag("div"):addClass("sv-source-pill"):wikitext(mw.text.nowiki(sourceKind or "Source"))
durVal = displayFromSeries(seriesFromValuePair(promo.durationBlock, maxLevel), level)
sourceCol:tag("div"):addClass("sv-source-value"):wikitext(sourceVal)
end
end


if hasScaling then
-- Cost: MP + HP
local scalingCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-scaling")
local function labeledSeries(block, label)
scalingCol:tag("div"):addClass("sv-source-pill"):wikitext("Scaling")
local s = seriesFromValuePair(block, maxLevel)
 
if not s then return nil end
local list = scalingCol:tag("div"):addClass("sv-scaling-list")
local any = false
for _, line in ipairs(scalingLines) do
for i, v in ipairs(s) do
list:tag("div"):addClass("sv-scaling-item"):wikitext(mw.text.nowiki(line))
if v ~= "" then
s[i] = tostring(v) .. " " .. label
any = true
else
s[i] = "—"
end
end
end
return any and s or nil
end
end


return {
local mpS = labeledSeries(rc["Mana Cost"], "MP")
inner = tostring(wrap),
local hpS = labeledSeries(rc["Health Cost"], "HP")
classes = extra,
}
end


-- Module Tile: Quick Stats (Range/Area/Cost/Cast/CD/Duration)
local costSeries = {}
function PLUGINS.QuickStats(rec, ctx)
for lv = 1, maxLevel do
local level = ctx.level or 1
local mp = mpS and mpS[lv] or "—"
local maxLevel = ctx.maxLevel or 1
local hp = hpS and hpS[lv] or "—"
local promo = ctx.promo
 
if mp ~= "—" and hp ~= "—" then
costSeries[lv] = mp .. " + " .. hp
elseif mp ~= "—" then
costSeries[lv] = mp
elseif hp ~= "—" then
costSeries[lv] = hp
else
costSeries[lv] = "—"
end
end


local mech = (type(rec) == "table" and type(rec.Mechanics) == "table") and rec.Mechanics or {}
local costVal = displayFromSeries(costSeries, level)
local bt  = (type(mech["Basic Timings"]) == "table") and mech["Basic Timings"] or {}
local rc  = (type(mech["Resource Cost"]) == "table") and mech["Resource Cost"] or {}


local function dash() return "" end
local grid = mw.html.create("div")
grid:addClass("sv-m4-grid")
grid:addClass("sv-compact-root")


-- Range: hide if 0/none-like.
local function addCell(label, val)
local rangeVal = nil
local cell = grid:tag("div"):addClass("sv-m4-cell")
if mech.Range ~= nil and not isNoneLike(mech.Range) then
cell:tag("div"):addClass("sv-m4-label"):wikitext(mw.text.nowiki(label))
local n = toNum(mech.Range)
cell:tag("div"):addClass("sv-m4-value"):wikitext(val or dash())
if n ~= nil then
if n ~= 0 then
rangeVal = mw.text.nowiki(formatUnitValue(mech.Range) or tostring(mech.Range))
end
else
local t = mw.text.trim(tostring(mech.Range))
if t ~= "" and not isNoneLike(t) then
rangeVal = mw.text.nowiki(t)
end
end
end
end


-- Area (friendly formatting)
addCell("Range",    rangeVal)
local areaVal = formatAreaSize(mech.Area)
addCell("Area",      areaVal)
addCell("Cost",      costVal)
addCell("Cast Time", castVal)
addCell("Cooldown",  cdVal)
addCell("Duration",  durVal)


-- Timings
return {
local castVal = displayFromSeries(seriesFromValuePair(bt["Cast Time"], maxLevel), level)
inner = tostring(grid),
local cdVal  = displayFromSeries(seriesFromValuePair(bt["Cooldown"],  maxLevel), level)
classes = "module-quick-stats",
local durVal  = displayFromSeries(seriesFromValuePair(bt["Duration"],  maxLevel), level)
}
end


-- Promote status duration into QuickStats when needed
-- PLUGIN: SpecialMechanics (Hero Module Slot 3)
if (durVal == nil) and type(promo) == "table" and type(promo.durationBlock) == "table" then
-- Shows:
durVal = displayFromSeries(seriesFromValuePair(promo.durationBlock, maxLevel), level)
--  - Flags (deduped)
end
--  - Special mechanics (mech.Effects)
-- NOTE: Combo has been moved to SkillType (Hero Bar Slot 2).
function PLUGINS.SpecialMechanics(rec, ctx)
local level = ctx.level or 1
local maxLevel = ctx.maxLevel or 1


-- Cost: MP + HP (combined)
local mech    = (type(rec) == "table" and type(rec.Mechanics) == "table") and rec.Mechanics or {}
local function labeledSeries(block, label)
local effects = (type(mech.Effects) == "table") and mech.Effects or nil
local s = seriesFromValuePair(block, maxLevel)
local mods    = (type(rec.Modifiers) == "table") and rec.Modifiers or nil
if not s then return nil end
local any = false
for i, v in ipairs(s) do
if v ~= "" then
s[i] = tostring(v) .. " " .. label
any = true
else
s[i] = ""
end
end
return any and s or nil
end


local mpS = labeledSeries(rc["Mana Cost"], "MP")
------------------------------------------------------------------
local hpS = labeledSeries(rc["Health Cost"], "HP")
-- Flags (flat, de-duped)
------------------------------------------------------------------
local flagSet = {}


local costSeries = {}
-- Filter out mechanics you said to remove/ignore here.
for lv = 1, maxLevel do
local denyFlags = {
local mp = mpS and mpS[lv] or "—"
["self centered"] = true,
local hp = hpS and hpS[lv] or "—"
["self-centred"] = true,
 
if mp ~= "—" and hp ~= "—" then
costSeries[lv] = mp .. " + " .. hp
elseif mp ~= "—" then
costSeries[lv] = mp
elseif hp ~= "—" then
costSeries[lv] = hp
else
costSeries[lv] = "—"
end
end
 
local costVal = displayFromSeries(costSeries, level)
 
local grid = mw.html.create("div")
grid:addClass("sv-m4-grid")
grid:addClass("sv-compact-root")
 
-- Add one QuickStats cell.
local function addCell(label, val)
local cell = grid:tag("div"):addClass("sv-m4-cell")
cell:tag("div"):addClass("sv-m4-label"):wikitext(mw.text.nowiki(label))
cell:tag("div"):addClass("sv-m4-value"):wikitext(val or dash())
end
 
addCell("Range",    rangeVal)
addCell("Area",      areaVal)
addCell("Cost",      costVal)
addCell("Cast Time", castVal)
addCell("Cooldown",  cdVal)
addCell("Duration",  durVal)
 
return {
inner = tostring(grid),
classes = "module-quick-stats",
}
end
 
-- Module Tile: Special Mechanics (Flags + Special Effects)
-- NOTE: Hybrid/Self-centered/Bond/Combo/Hits are intentionally excluded here.
function PLUGINS.SpecialMechanics(rec, ctx)
local level = ctx.level or 1
local maxLevel = ctx.maxLevel or 1
 
local mech    = (type(rec) == "table" and type(rec.Mechanics) == "table") and rec.Mechanics or {}
local effects = (type(mech.Effects) == "table") and mech.Effects or nil
local mods    = (type(rec.Modifiers) == "table") and rec.Modifiers or nil
 
-- Case-insensitive omit list for mechanics/flags we are not showing in this module.
local OMIT = {
["hybrid"] = true,
["self centered"] = true,
["self centered"] = true,
["self-centered"] = true,
["bond"] = true,
["bond"] = true,
["combo"] = true,
["combo"] = true,
Line 1,302: Line 1,354:
}
}


-- True if a name should be omitted from this module.
local function allowFlag(name)
local function shouldOmit(name)
if not name then return false end
if not name then return false end
local s = mw.ustring.lower(mw.text.trim(tostring(name)))
local k = mw.ustring.lower(mw.text.trim(tostring(name)))
return (s ~= "" and OMIT[s] == true) or false
if k == "" then return false end
if denyFlags[k] then return false end
return true
end
end


------------------------------------------------------------------
-- Flags (flat, de-duped)
------------------------------------------------------------------
local flagSet = {}
-- Add boolean flags from a sub-table into a set.
local function addFlags(sub)
local function addFlags(sub)
if type(sub) ~= "table" then return end
if type(sub) ~= "table" then return end
for k, v in pairs(sub) do
for k, v in pairs(sub) do
if v and not shouldOmit(k) then
if v and allowFlag(k) then
flagSet[tostring(k)] = true
flagSet[tostring(k)] = true
end
end
Line 1,329: Line 1,376:
addFlags(mods["Special Modifiers"])
addFlags(mods["Special Modifiers"])
for k, v in pairs(mods) do
for k, v in pairs(mods) do
if type(v) == "boolean" and v and not shouldOmit(k) then
if type(v) == "boolean" and v and allowFlag(k) then
flagSet[tostring(k)] = true
flagSet[tostring(k)] = true
end
end
Line 1,340: Line 1,387:


------------------------------------------------------------------
------------------------------------------------------------------
-- Special effects (name => value)
-- Special mechanics (name => value)
------------------------------------------------------------------
------------------------------------------------------------------
local mechItems = {}
local mechItems = {}
Line 1,346: Line 1,393:
if effects then
if effects then
local keys = {}
local keys = {}
for k, _ in pairs(effects) do
for k, _ in pairs(effects) do table.insert(keys, k) end
if not shouldOmit(k) then
table.insert(keys, k)
end
end
table.sort(keys)
table.sort(keys)


Line 1,396: Line 1,439:
if hasMech  then count = count + 1 end
if hasMech  then count = count + 1 end


-- Desktop:
--  1 group => centered
--  2 groups => 2 columns (flags | mechanics)
local root = mw.html.create("div")
local root = mw.html.create("div")
root:addClass("sv-sm-root")
root:addClass("sv-sm-root")
Line 1,406: Line 1,446:
layout:addClass("sv-sm-count-" .. tostring(count))
layout:addClass("sv-sm-count-" .. tostring(count))


-- Column: Flags
-- Column 1: Flags
if hasFlags then
if hasFlags then
local fcol = layout:tag("div"):addClass("sv-sm-col"):addClass("sv-sm-col-flags")
local fcol = layout:tag("div"):addClass("sv-sm-col"):addClass("sv-sm-col-flags")
Line 1,414: Line 1,454:
end
end


-- Column: Special Effects (stacked)
-- Column 2: Special Mechanics (stacked)
if hasMech then
if hasMech then
local mcol = layout:tag("div"):addClass("sv-sm-col"):addClass("sv-sm-col-mech")
local mcol = layout:tag("div"):addClass("sv-sm-col"):addClass("sv-sm-col-mech")
Line 1,434: Line 1,474:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Normalize plugin return into { inner = string, classes = string|table|nil }.
-- normalizeResult: normalize plugin return values into {inner, classes}.
local function normalizeResult(res)
local function normalizeResult(res)
if res == nil then return nil end
if res == nil then return nil end
Line 1,450: Line 1,490:
end
end


-- Safe plugin call (pcall) to prevent infobox from breaking the page.
-- safeCallPlugin: pcall wrapper to prevent infobox failure on plugin errors.
local function safeCallPlugin(name, rec, ctx)
local function safeCallPlugin(name, rec, ctx)
local fn = PLUGINS[name]
local fn = PLUGINS[name]
Line 1,463: Line 1,503:
end
end


-- Render one hero-bar slot via assignment table.
-- renderHeroBarSlot: render a hero-bar slot by plugin assignment.
local function renderHeroBarSlot(slotIndex, rec, ctx)
local function renderHeroBarSlot(slotIndex, rec, ctx)
local pluginName = HERO_BAR_SLOT_ASSIGNMENT[slotIndex]
local pluginName = HERO_BAR_SLOT_ASSIGNMENT[slotIndex]
Line 1,478: Line 1,518:
end
end


-- Render one hero-module tile via assignment table.
-- renderModuleSlot: render a hero-module slot by plugin assignment.
local function renderModuleSlot(slotIndex, rec, ctx)
local function renderModuleSlot(slotIndex, rec, ctx)
local pluginName = HERO_MODULE_SLOT_ASSIGNMENT[slotIndex]
local pluginName = HERO_MODULE_SLOT_ASSIGNMENT[slotIndex]
Line 1,497: Line 1,537:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Build the hero title bar (2 slots).
-- buildHeroBarUI: build the top hero bar (2 slots).
local function buildHeroBarUI(rec, ctx)
local function buildHeroBarUI(rec, ctx)
local bar = mw.html.create("div")
local bar = mw.html.create("div")
Line 1,506: Line 1,546:
end
end


-- Build the 2x2 hero-module grid (4 slots).
-- buildHeroModulesUI: build the 2x2 module grid row (4 slots).
local function buildHeroModulesUI(rec, ctx)
local function buildHeroModulesUI(rec, ctx)
local grid = mw.html.create("div")
local grid = mw.html.create("div")
Line 1,516: Line 1,556:
end
end


-- Add the modules grid row (single cell spanning infobox width).
-- addHeroModulesRow: add the hero-modules row into the infobox table.
local function addHeroModulesRow(tbl, modulesUI)
local function addHeroModulesRow(tbl, modulesUI)
if not modulesUI or modulesUI == "" then
if not modulesUI or modulesUI == "" then
Line 1,535: Line 1,575:
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Build a single skill infobox table.
-- buildInfobox: render a single skill infobox.
local function buildInfobox(rec, opts)
local function buildInfobox(rec, opts)
opts = opts or {}
opts = opts or {}
Line 1,551: Line 1,591:
}
}


-- Determine “non-damaging” for hiding Damage/Element/Hits in SkillType.
-- Non-damaging hides Damage/Element/Hits in SkillType
do
do
local dmgVal = nil
local dmgVal = nil
Line 1,563: Line 1,603:
end
end


-- Duration promotion into QuickStats (non-damaging only).
ctx.promo = computeDurationPromotion(rec, maxLevel)
ctx.promo = computeDurationPromotion(rec, maxLevel)


Line 1,616: Line 1,655:
addHeroModulesRow(root, buildHeroModulesUI(rec, ctx))
addHeroModulesRow(root, buildHeroModulesUI(rec, ctx))


-- Users (optionally hidden on direct skill pages)
-- Users (hide on direct skill page)
if showUsers then
if showUsers then
local users = rec.Users or {}
local users = rec.Users or {}
Line 1,651: Line 1,690:
end
end


-- Mechanics (keep only small extras here; most are in hero tiles)
-- Mechanics (keep small extras only)
local mech = rec.Mechanics or {}
local mech = rec.Mechanics or {}
if next(mech) ~= nil then
if next(mech) ~= nil then
Line 1,684: Line 1,723:


-- Status rows
-- Status rows
-- Format Status Applications list into display text.
local function formatStatusApplications(list, suppressDurationIndex)
local function formatStatusApplications(list, suppressDurationIndex)
if type(list) ~= "table" or #list == 0 then return nil end
if type(list) ~= "table" or #list == 0 then return nil end
Line 1,717: Line 1,755:
end
end


-- Format Status Removal list into display text.
local function formatStatusRemoval(list)
local function formatStatusRemoval(list)
if type(list) ~= "table" or #list == 0 then return nil end
if type(list) ~= "table" or #list == 0 then return nil end
Line 1,758: Line 1,795:


-- Events
-- Events
-- Format event triggers list into display text.
local function formatEvents(list)
local function formatEvents(list)
if type(list) ~= "table" or #list == 0 then return nil end
if type(list) ~= "table" or #list == 0 then return nil end