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
--
-- Phase 6.5+ (Plug-in Slot Architecture)
--
-- Standard Hero Layout (reused across the wiki):
-- Standard Hero Layout (reused across the wiki):
--  1) hero-title-bar        (TOP BAR, 2 slots: herobar 1..2)
--  1) hero-title-bar        (TOP BAR, 2 slots: herobar 1..2)
--        - Herobar 1: Icon + Skill Name (left)
--        - Herobar 1: Icon + Skill Name (left)
--        - Herobar 2: Reserved for compact info (right)
--        - Herobar 2: Reserved for compact info (right)
--  2) hero-description-bar  (description strip)
--  2) hero-description-bar  (description strip) [special-cased for now]
--  3) hero-modules row      (4 slots: hero-module-1..4)
--  3) hero-modules row      (4 slots: hero-module-1..4)
--        - Slot 1: module-level-selector
--        - Slot 1: Level Selector
--        - Slot 2: module-skill-type
--        - Slot 2: Skill Type
--        - Slot 3: skill-source-module (Modifier + Source + Scaling) OR blank
--        - Slot 3: SourceType (Modifier + Source + Scaling) OR blank
--        - Slot 4: module-quick-stats (Range/Area/Cost/Cast/Cooldown/Duration)
--        - Slot 4: Quick Stats (Range/Area/Cost/Cast/Cooldown/Duration)
--
--
-- Requires Common.js logic that updates:
-- Requires Common.js logic that updates:
Line 372: Line 376:


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Formatting helpers
-- Formatting helpers (legacy + mechanics/status/etc.)
----------------------------------------------------------------------
----------------------------------------------------------------------


Line 394: Line 398:
end
end


-- BACKCOMPAT: old damage list formatter (for infobox rows)
local function formatDamageEntry(entry, maxLevel, level)
local function formatDamageEntry(entry, maxLevel, level)
if type(entry) ~= "table" then
if type(entry) ~= "table" then
Line 598: Line 601:
local detail = {}
local detail = {}


-- Duration (optionally suppressed if Module 4 “promoted” it)
-- Duration (optionally suppressed if QuickStats promoted it)
if idx ~= suppressDurationIndex and type(s.Duration) == "table" then
if idx ~= suppressDurationIndex and type(s.Duration) == "table" then
local t = valuePairDynamicText("Duration", s.Duration, maxLevel, level, "; ")
local t = valuePairDynamicText("Duration", s.Duration, maxLevel, level, "; ")
Line 735: Line 738:


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Hero bar modules (2-slot scaffold)
-- Plug-in slot config (EDIT THESE TABLES ONLY to rearrange layout)
--   - Slot 1: Icon + Title (left)
----------------------------------------------------------------------
--   - Slot 2: Reserved for future compact info (right)
 
-- Hero bar: 2 slots
local HERO_BAR_SLOT_ASSIGNMENT = {
[1] = "IconName",
[2] = "ReservedInfo", -- set to nil to blank the slot entirely
}
 
-- Modules row: 4 slots
local HERO_MODULE_SLOT_ASSIGNMENT = {
[1] = "LevelSelector",
[2] = "SkillType",
[3] = "SourceType",  -- returns nil/"" when not applicable -> becomes blank slot
[4] = "QuickStats",
}
 
----------------------------------------------------------------------
-- Slot scaffolds (stable containers; plug-ins provide inner content)
----------------------------------------------------------------------
----------------------------------------------------------------------


Line 768: Line 787:
end
end


local function buildEmptyHeroBar(slot)
local function moduleBox(slot, extraClasses, innerHtml, isEmpty)
return heroBarBox(slot, nil, "", true)
local box = mw.html.create("div")
end
box:addClass("hero-module")
box:addClass("hero-module-" .. tostring(slot))
box:attr("data-hero-module", tostring(slot))


local function buildHeroBar1(icon, title)
if extraClasses then
local wrap = mw.html.create("div")
if type(extraClasses) == "string" then
wrap:addClass("sv-herobar-1-wrap")
box:addClass(extraClasses)
elseif type(extraClasses) == "table" then
for _, c in ipairs(extraClasses) do
box:addClass(c)
end
end
end


if icon and icon ~= "" then
if isEmpty then
wrap:tag("div")
box:addClass("hero-module-empty")
:addClass("sv-herobar-icon")
:wikitext(string.format("[[File:%s|80px|link=]]", icon))
end
end


wrap:tag("div")
local body = box:tag("div"):addClass("hero-module-body")
:addClass("spiritvale-infobox-title")
if innerHtml and innerHtml ~= "" then
:wikitext(title or "Unknown Skill")
body:wikitext(innerHtml)
end


return heroBarBox(1, "module-herobar-1", tostring(wrap), false)
return tostring(box)
end
 
-- Reserved slot: keep it empty for now (but fully wired for later)
local function buildHeroBar2(rec, level, maxLevel)
-- Return an empty container so the slot exists for future use.
-- When you're ready, populate this with compact badges, tags, etc.
local wrap = mw.html.create("div")
wrap:addClass("sv-herobar-2-wrap")
return heroBarBox(2, "module-herobar-2", tostring(wrap), false)
end
 
local function buildHeroBarUI(rec, level, maxLevel, icon, title)
local bar = mw.html.create("div")
bar:addClass("hero-bar-grid")
 
bar:wikitext(buildHeroBar1(icon, title))
local hb2 = buildHeroBar2(rec, level, maxLevel)
bar:wikitext(hb2 or buildEmptyHeroBar(2))
 
return tostring(bar)
end
end


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Hero modules (4-slot scaffold)
-- Shared helpers used by plug-ins (Source + QuickStats)
----------------------------------------------------------------------
----------------------------------------------------------------------


local function moduleBox(slot, extraClasses, innerHtml, isEmpty)
local function formatScalingCompactLines(scaling)
local box = mw.html.create("div")
if type(scaling) ~= "table" then
box:addClass("hero-module")
return {}
box:addClass("hero-module-" .. tostring(slot))
end
box:attr("data-hero-module", tostring(slot))


if extraClasses then
local list = scaling
if type(extraClasses) == "string" then
if #list == 0 then
box:addClass(extraClasses)
if scaling.Percent ~= nil or scaling["Scaling ID"] or scaling["Scaling Name"] then
elseif type(extraClasses) == "table" then
list = { scaling }
for _, c in ipairs(extraClasses) do
else
box:addClass(c)
return {}
end
end
end
end
end


if isEmpty then
local out = {}
box:addClass("hero-module-empty")
for _, s in ipairs(list) do
end
if type(s) == "table" then
local stat = s["Scaling Name"] or s["Scaling ID"] or "Unknown"
local pct  = s.Percent
local pctN = toNum(pct)


local body = box:tag("div"):addClass("hero-module-body")
if pctN ~= nil and pctN ~= 0 then
if innerHtml and innerHtml ~= "" then
table.insert(out, string.format("%s%% %s", fmtNum(pctN), stat))
body:wikitext(innerHtml)
elseif pct ~= nil and tostring(pct) ~= "" and tostring(pct) ~= "0" then
table.insert(out, string.format("%s%% %s", tostring(pct), stat))
end
end
end
end


return tostring(box)
return out
end
end


local function buildEmptyModule(slot)
local function basisWordFromFlags(atkFlag, matkFlag)
return moduleBox(slot, nil, "", true)
if atkFlag and matkFlag then
return "Hybrid"
elseif atkFlag then
return "Attack"
elseif matkFlag then
return "Magic Attack"
end
return nil
end
end


-- ------------------------------------------------------------
local function sourceValueForLevel(src, maxLevel, level)
-- Module 1 – Level Selector
if type(src) ~= "table" then
-- ------------------------------------------------------------
return nil
local function buildModuleLevelSelector(level, maxLevel)
end
local inner = mw.html.create("div")
inner:addClass("sv-level-ui")


inner:tag("div")
local per = src["Per Level"]
:addClass("sv-level-label")
if type(per) == "table" and #per > 0 then
:wikitext("Level <span class=\"sv-level-num\">" .. tostring(level) .. "</span> / " .. tostring(maxLevel))
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 slider = inner:tag("div"):addClass("sv-level-slider")
local series = {}
 
for _, v in ipairs(per) do
if tonumber(maxLevel) and tonumber(maxLevel) > 1 then
table.insert(series, formatUnitValue(v) or tostring(v))
slider:tag("input")
end
:attr("type", "range")
return dynSpan(series, level)
: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 moduleBox(1, "module-level-selector", tostring(inner), false)
return valuePairDynamicValueOnly(src, maxLevel, level)
end
end


-- ------------------------------------------------------------
local function legacyPercentAtLevel(entry, level)
-- Module 2 – Skill Type
if type(entry) ~= "table" then
-- (If non-damaging, hide Damage + Element; keep Target + Cast)
-- ------------------------------------------------------------
local function buildModuleSkillType(typeBlock, hideDamageAndElement)
typeBlock = (type(typeBlock) == "table") and typeBlock or {}
 
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
end
if type(x) == "string" and x ~= "" then
return x
end
return nil
return nil
end
end


local grid = mw.html.create("div")
local baseRaw = entry["Base %"]
grid:addClass("sv-type-grid")
local perRaw  = entry["Per Level %"]
local baseN  = toNum(baseRaw)
local perN    = toNum(perRaw)


local added = false
if perN ~= nil and perN ~= 0 then
local function addChunk(label, rawVal)
local total = (baseN or 0) + (perN * level)
local v = valName(rawVal)
return fmtNum(total) .. "%"
if not v or v == "" then
end
return
end
added = true


local chunk = grid:tag("div"):addClass("sv-type-chunk")
if baseN ~= nil then
chunk:tag("div"):addClass("sv-type-label"):wikitext(mw.text.nowiki(label))
return fmtNum(baseN) .. "%"
chunk:tag("div"):addClass("sv-type-value"):wikitext(mw.text.nowiki(v))
end
end
 
if baseRaw ~= nil and tostring(baseRaw) ~= "" then
if not hideDamageAndElement then
return tostring(baseRaw) .. "%"
addChunk("Damage",  typeBlock.Damage  or typeBlock["Damage Type"])
addChunk("Element", typeBlock.Element or typeBlock["Element Type"])
end
end


addChunk("Target",  typeBlock.Target  or typeBlock["Target Type"])
return nil
addChunk("Cast",    typeBlock.Cast    or typeBlock["Cast Type"])
 
local body = added and tostring(grid) or ""
return moduleBox(2, "module-skill-type", body, false)
end
end


-- ============================================================
local function seriesFromValuePair(block, maxLevel)
-- Module 3 – Skill Source (Modifier + Source + Scaling)
if type(block) ~= "table" then
-- (unchanged from your current system)
return nil
-- ============================================================
end


local function formatScalingCompactLines(scaling)
local base = block.Base
if type(scaling) ~= "table" then
local per  = block["Per Level"]
return {}
end


local list = scaling
local function pickUnit(v)
if #list == 0 then
if type(v) == "table" and v.Unit and v.Unit ~= "" then
if scaling.Percent ~= nil or scaling["Scaling ID"] or scaling["Scaling Name"] then
return v.Unit
list = { scaling }
else
return {}
end
end
return nil
end
end
local unit = pickUnit(base) or pickUnit(per)


local out = {}
local function fmtAny(v)
for _, s in ipairs(list) do
local t = formatUnitValue(v)
if type(s) == "table" then
return t and tostring(t) or (v ~= nil and tostring(v) or nil)
local stat = s["Scaling Name"] or s["Scaling ID"] or "Unknown"
local pct  = s.Percent
local pctN = toNum(pct)
 
if pctN ~= nil and pctN ~= 0 then
table.insert(out, string.format("%s%% %s", fmtNum(pctN), stat))
elseif pct ~= nil and tostring(pct) ~= "" and tostring(pct) ~= "0" then
table.insert(out, string.format("%s%% %s", tostring(pct), stat))
end
end
end
end


return out
local series = {}
end


local function basisWordFromFlags(atkFlag, matkFlag)
-- Preferred: expanded series (wikiprep)
if atkFlag and matkFlag then
if type(per) == "table" and #per > 0 then
return "Hybrid"
for lv = 1, maxLevel do
elseif atkFlag then
local raw = per[lv] or per[#per]
return "Attack"
local one = fmtAny(raw)
elseif matkFlag then
if one == nil or isZeroish(raw) or isZeroish(one) then
return "Magic Attack"
one = ""
end
series[lv] = one
end
return series
end
end
return nil
end


local function sourceValueForLevel(src, maxLevel, level)
-- Empty per-list -> base only
if type(src) ~= "table" then
if type(per) == "table" and #per == 0 then
return nil
local one = fmtAny(base)
end
if one == nil or isZeroish(base) or isZeroish(one) then
 
one = "—"
local base = src.Base
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(base) or one
return show and mw.text.nowiki(show) or nil
end
end
 
for lv = 1, maxLevel do
local series = {}
series[lv] = one
for _, v in ipairs(per) do
table.insert(series, formatUnitValue(v) or tostring(v))
end
end
return dynSpan(series, level)
return series
end
end


return valuePairDynamicValueOnly(src, maxLevel, level)
-- Scalar per -> compute base + per*level (fallback)
end
local baseN = toNum(base) or 0
local perN  = toNum(per)


local function legacyPercentAtLevel(entry, level)
if perN ~= nil then
if type(entry) ~= "table" then
for lv = 1, maxLevel do
return nil
local total = baseN + (perN * lv)
local v = unit and { Value = total, Unit = unit } or total
local one = fmtAny(v)
if one == nil or total == 0 or isZeroish(one) then
one = ""
end
series[lv] = one
end
return series
end
end


local baseRaw = entry["Base %"]
-- Base-only scalar
local perRaw  = entry["Per Level %"]
local raw = (base ~= nil) and base or per
local baseN  = toNum(baseRaw)
local one = fmtAny(raw)
local perN    = toNum(perRaw)
if one == nil then
 
return nil
if perN ~= nil and perN ~= 0 then
local total = (baseN or 0) + (perN * level)
return fmtNum(total) .. "%"
end
end
 
if isZeroish(raw) or isZeroish(one) then
if baseN ~= nil then
one = ""
return fmtNum(baseN) .. "%"
end
end
if baseRaw ~= nil and tostring(baseRaw) ~= "" then
for lv = 1, maxLevel do
return tostring(baseRaw) .. "%"
series[lv] = one
end
end
 
return series
return nil
end
end


local function buildModuleSkillSource(rec, level, maxLevel)
local function displayFromSeries(series, level)
local basisWord = nil
if type(series) ~= "table" or #series == 0 then
local sourceKind = nil
return nil
local sourceVal  = nil
local scaling    = nil
 
if type(rec.Source) == "table" then
local src = rec.Source
local atkFlag  = (src["ATK-Based"] == true)
local matkFlag = (src["MATK-Based"] == true)
basisWord = basisWordFromFlags(atkFlag, matkFlag)
 
sourceKind = src.Type or ((src.Healing == true) and "Healing") or "Damage"
sourceVal  = sourceValueForLevel(src, maxLevel, level)
scaling    = src.Scaling
end
end


if (sourceVal == nil or sourceVal == "") and type(rec.Damage) == "table" then
local any = false
local dmg = rec.Damage
for _, v in ipairs(series) do
scaling = scaling or dmg.Scaling
if v ~= "" then
any = true
break
end
end
if not any then
return nil
end
 
if isFlatList(series) then
return mw.text.nowiki(series[1])
end
return dynSpan(series, level)
end


local main = dmg["Main Damage"]
local function formatAreaSize(area)
local refl = dmg["Reflect Damage"]
if type(area) ~= "table" then
local flat = dmg["Flat Damage"]
return nil
end


if type(main) == "table" and #main > 0 then
local raw = area["Area Size"]
local pick = nil
if raw == nil then
for _, d in ipairs(main) do
return nil
if type(d) == "table" and d.Type ~= "Healing" then
end
pick = d
break
end
end
pick = pick or main[1]


if type(pick) == "table" then
local name, num
local atkFlag  = (pick["ATK-Based"] == true)
local matkFlag = (pick["MATK-Based"] == true)
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)


sourceKind = (pick.Type == "Healing") and "Healing" or "Damage"
if type(raw) == "table" then
sourceVal  = legacyPercentAtLevel(pick, level)
name = raw.Name or raw.ID or raw.Value
end
num = raw.Value
elseif type(refl) == "table" and #refl > 0 and type(refl[1]) == "table" then
if raw.Name or raw.ID then
local pick = refl[1]
name = raw.Name or raw.ID
local atkFlag = (pick["ATK-Based"] == true)
end
local matkFlag = (pick["MATK-Based"] == true)
elseif type(raw) == "string" then
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)
name = raw
 
elseif type(raw) == "number" then
sourceKind = "Reflect"
num = raw
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


local scalingLines = formatScalingCompactLines(scaling)
if num == nil then
local hasSource    = (sourceVal ~= nil and tostring(sourceVal) ~= "")
num = toNum(area["Area Value"]) or toNum(area["Area Size Value"]) or toNum(area["Area Number"]) or toNum(area["Area Radius"])
local hasScaling  = (type(scalingLines) == "table" and #scalingLines > 0)
 
if (not hasSource) and (not hasScaling) then
return nil
end
end


local hasMod = (basisWord ~= nil and tostring(basisWord) ~= "")
if name ~= nil then
local s = mw.text.trim(tostring(name))
if s == "" or isNoneLike(s) then
return nil
end
if mw.ustring.find(s, "%(") then
return mw.text.nowiki(s)
end
if num ~= nil and num ~= 0 then
return mw.text.nowiki(string.format("%s (%s)", s, fmtNum(num)))
end
return mw.text.nowiki(s)
end


local extra = { "skill-source-module" }
if num ~= nil and num ~= 0 then
table.insert(extra, hasMod and "sv-has-mod" or "sv-no-mod")
return mw.text.nowiki(string.format("(%s)", fmtNum(num)))
 
if hasSource and (not hasScaling) then
table.insert(extra, "sv-only-source")
elseif hasScaling and (not hasSource) then
table.insert(extra, "sv-only-scaling")
end
end


local wrap = mw.html.create("div")
return nil
wrap:addClass("sv-source-grid")
end


if hasMod then
local function skillHasAnyDamage(rec, maxLevel)
local modCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-modifier")
if type(rec.Source) == "table" then
modCol:tag("div"):addClass("sv-source-pill"):wikitext("Modifier")
local s = seriesFromValuePair(rec.Source, maxLevel)
modCol:tag("div"):addClass("sv-modifier-value"):wikitext(mw.text.nowiki(basisWord))
if s then
for _, v in ipairs(s) do
if v ~= "" then return true end
end
end
end
end


if hasSource then
if type(rec.Damage) == "table" then
local sourceCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-main")
local dmg = rec.Damage
sourceCol:tag("div"):addClass("sv-source-pill"):wikitext(mw.text.nowiki(sourceKind or "Source"))
for _, key in ipairs({ "Main Damage", "Flat Damage", "Reflect Damage" }) do
sourceCol:tag("div"):addClass("sv-source-value"):wikitext(sourceVal)
local lst = dmg[key]
if type(lst) == "table" and #lst > 0 then
return true
end
end
end
end


if hasScaling then
return false
local scalingCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-scaling")
end
scalingCol:tag("div"):addClass("sv-source-pill"):wikitext("Scaling")


local list = scalingCol:tag("div"):addClass("sv-scaling-list")
local function computeDurationPromotion(rec, maxLevel)
for _, line in ipairs(scalingLines) do
-- Only:
list:tag("div"):addClass("sv-scaling-item"):wikitext(mw.text.nowiki(line))
-- - non-damaging skills
-- - AND missing/blank Duration in Basic Timings
-- - AND a Status Application has a Duration block
-- Then: promote that status duration into QuickStats and suppress it in Applies row.
if type(rec) ~= "table" then return nil end
if skillHasAnyDamage(rec, maxLevel) then return nil end
 
local mech = (type(rec.Mechanics) == "table") and rec.Mechanics or {}
local bt  = (type(mech["Basic Timings"]) == "table") and mech["Basic Timings"] or {}
local durS = seriesFromValuePair(bt["Duration"], maxLevel)
 
if durS ~= nil then
local any = false
for _, v in ipairs(durS) do
if v ~= "" then any = true break end
end
if any then
return nil
end
end
end
end
-- if durS is nil OR all "—", allow promotion


return moduleBox(3, extra, tostring(wrap), false)
local apps = rec["Status Applications"]
end
if type(apps) ~= "table" then return nil end


----------------------------------------------------------------------
for idx, app in ipairs(apps) do
-- Module 4 – Quick Stats (3x2)
if type(app) == "table" and type(app.Duration) == "table" then
-- Range / Area / Cost / Cast Time / Cooldown / Duration
local s = seriesFromValuePair(app.Duration, maxLevel)
-- - Zero values => —
if s then
-- - Area “none/missing” => —
for _, v in ipairs(s) do
-- - Cost: HP only shows if non-zero at some level; never show "0 HP"
if v ~= "" then
-- - Special: if Duration missing AND skill is non-damaging, promote a status-application duration
return {
----------------------------------------------------------------------
durationBlock = app.Duration,
 
suppressDurationIndex = idx,
local function seriesFromValuePair(block, maxLevel)
}
if type(block) ~= "table" then
end
return nil
end
end
end
end
end


local base = block.Base
return nil
local per  = block["Per Level"]
end
 
----------------------------------------------------------------------
-- Plug-ins (name -> { inner = "...", classes = "..."/{...} } OR nil/"")
----------------------------------------------------------------------


local function pickUnit(v)
local PLUGINS = {}
if type(v) == "table" and v.Unit and v.Unit ~= "" then
return v.Unit
end
return nil
end
local unit = pickUnit(base) or pickUnit(per)


local function fmtAny(v)
-- Hero Bar Slot 1: Icon + Name
local t = formatUnitValue(v)
function PLUGINS.IconName(rec, ctx)
return t and tostring(t) or (v ~= nil and tostring(v) or nil)
local icon  = rec.Icon
end
local title = rec["External Name"] or rec.Name or rec["Internal Name"] or "Unknown Skill"


local series = {}
local wrap = mw.html.create("div")
wrap:addClass("sv-herobar-1-wrap")


-- Preferred: expanded series (wikiprep)
if icon and icon ~= "" then
if type(per) == "table" and #per > 0 then
wrap:tag("div")
for lv = 1, maxLevel do
:addClass("sv-herobar-icon")
local raw = per[lv] or per[#per]
:wikitext(string.format("[[File:%s|80px|link=]]", icon))
local one = fmtAny(raw)
if one == nil or isZeroish(raw) or isZeroish(one) then
one = "—"
end
series[lv] = one
end
return series
end
end


-- Empty per-list -> base only
wrap:tag("div")
if type(per) == "table" and #per == 0 then
:addClass("spiritvale-infobox-title")
local one = fmtAny(base)
:wikitext(title)
if one == nil or isZeroish(base) or isZeroish(one) then
 
one = ""
return {
end
inner = tostring(wrap),
for lv = 1, maxLevel do
classes = "module-herobar-1",
series[lv] = one
}
end
end
return series
 
end
-- Hero Bar Slot 2: Reserved container (kept wired for future use)
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


-- Scalar per -> compute base + per*level (fallback)
-- Module Slot 1: Level Selector
local baseN = toNum(base) or 0
function PLUGINS.LevelSelector(rec, ctx)
local perN  = toNum(per)
local level = ctx.level or 1
local maxLevel = ctx.maxLevel or 1


if perN ~= nil then
local inner = mw.html.create("div")
for lv = 1, maxLevel do
inner:addClass("sv-level-ui")
local total = baseN + (perN * lv)
 
local v = unit and { Value = total, Unit = unit } or total
inner:tag("div")
local one = fmtAny(v)
:addClass("sv-level-label")
if one == nil or total == 0 or isZeroish(one) then
:wikitext("Level <span class=\"sv-level-num\">" .. tostring(level) .. "</span> / " .. tostring(maxLevel))
one = ""
 
end
local slider = inner:tag("div"):addClass("sv-level-slider")
series[lv] = one
 
end
if tonumber(maxLevel) and tonumber(maxLevel) > 1 then
return series
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


-- Base-only scalar
return {
local raw = (base ~= nil) and base or per
inner = tostring(inner),
local one = fmtAny(raw)
classes = "module-level-selector",
if one == nil then
}
return nil
end
if isZeroish(raw) or isZeroish(one) then
one = ""
end
for lv = 1, maxLevel do
series[lv] = one
end
return series
end
end


local function displayFromSeries(series, level)
-- Module Slot 2: Skill Type
if type(series) ~= "table" or #series == 0 then
function PLUGINS.SkillType(rec, ctx)
local typeBlock = (type(rec.Type) == "table") and rec.Type or {}
local hideDamageAndElement = (ctx.nonDamaging == true)
 
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
end
if type(x) == "string" and x ~= "" then
return x
end
return nil
return nil
end
end


local any = false
local grid = mw.html.create("div")
for _, v in ipairs(series) do
grid:addClass("sv-type-grid")
if v ~= "" then
 
any = true
local added = false
break
local function addChunk(label, rawVal)
end
local v = valName(rawVal)
if not v or v == "" then return end
added = true
 
local chunk = grid:tag("div"):addClass("sv-type-chunk")
chunk:tag("div"):addClass("sv-type-label"):wikitext(mw.text.nowiki(label))
chunk:tag("div"):addClass("sv-type-value"):wikitext(mw.text.nowiki(v))
end
end
if not any then
 
return nil
if not hideDamageAndElement then
addChunk("Damage",  typeBlock.Damage  or typeBlock["Damage Type"])
addChunk("Element", typeBlock.Element or typeBlock["Element Type"])
end
end


if isFlatList(series) then
addChunk("Target", typeBlock.Target or typeBlock["Target Type"])
return mw.text.nowiki(series[1])
addChunk("Cast",  typeBlock.Cast  or typeBlock["Cast Type"])
end
 
return dynSpan(series, level)
return {
inner = added and tostring(grid) or "",
classes = "module-skill-type",
}
end
end


local function formatAreaSize(area)
-- Module Slot 3: SourceType (Modifier + Source + Scaling) OR nil for blank
if type(area) ~= "table" then
function PLUGINS.SourceType(rec, ctx)
return nil
local level = ctx.level or 1
end
local maxLevel = ctx.maxLevel or 1


local raw = area["Area Size"]
local basisWord = nil
if raw == nil then
local sourceKind = nil
return nil
local sourceVal  = nil
end
local scaling    = nil


local name, num
if type(rec.Source) == "table" then
local src = rec.Source
local atkFlag  = (src["ATK-Based"] == true)
local matkFlag = (src["MATK-Based"] == true)
basisWord = basisWordFromFlags(atkFlag, matkFlag)


if type(raw) == "table" then
sourceKind = src.Type or ((src.Healing == true) and "Healing") or "Damage"
name = raw.Name or raw.ID or raw.Value
sourceVal  = sourceValueForLevel(src, maxLevel, level)
num  = raw.Value
scaling    = src.Scaling
-- if name accidentally came from Value, prefer Name/ID if present
if raw.Name or raw.ID then
name = raw.Name or raw.ID
end
elseif type(raw) == "string" then
name = raw
elseif type(raw) == "number" then
num = raw
end
end


-- fallback keys that sometimes exist depending on dict/wikiprep
-- Fallback to legacy Damage lists if Source absent
if num == nil then
if (sourceVal == nil or sourceVal == "") and type(rec.Damage) == "table" then
num = toNum(area["Area Value"]) or toNum(area["Area Size Value"]) or toNum(area["Area Number"]) or toNum(area["Area Radius"])
local dmg = rec.Damage
end
scaling = scaling or dmg.Scaling


if name ~= nil then
local main = dmg["Main Damage"]
local s = mw.text.trim(tostring(name))
local refl = dmg["Reflect Damage"]
if s == "" or isNoneLike(s) then
local flat = dmg["Flat Damage"]
return nil
end
-- if already contains "(...)" keep it
if mw.ustring.find(s, "%(") then
return mw.text.nowiki(s)
end
if num ~= nil and num ~= 0 then
return mw.text.nowiki(string.format("%s (%s)", s, fmtNum(num)))
end
return mw.text.nowiki(s)
end


if num ~= nil and num ~= 0 then
if type(main) == "table" and #main > 0 then
return mw.text.nowiki(string.format("(%s)", fmtNum(num)))
local pick = nil
end
for _, d in ipairs(main) do
if type(d) == "table" and d.Type ~= "Healing" then
pick = d
break
end
end
pick = pick or main[1]


return nil
if type(pick) == "table" then
end
local atkFlag  = (pick["ATK-Based"] == true)
local matkFlag = (pick["MATK-Based"] == true)
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)


local function skillHasAnyDamage(rec, maxLevel)
sourceKind = (pick.Type == "Healing") and "Healing" or "Damage"
-- “Has damage/source” for our UI decisions:
sourceVal  = legacyPercentAtLevel(pick, level)
-- - New Source with any non-zero at any level
-- - OR legacy damage lists present
if type(rec.Source) == "table" then
local s = seriesFromValuePair(rec.Source, maxLevel)
if s then
for _, v in ipairs(s) do
if v ~= "—" then return true end
end
end
end
elseif type(refl) == "table" and #refl > 0 and type(refl[1]) == "table" then
end
local pick = refl[1]
local atkFlag  = (pick["ATK-Based"] == true)
local matkFlag = (pick["MATK-Based"] == true)
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)


if type(rec.Damage) == "table" then
sourceKind = "Reflect"
local dmg = rec.Damage
sourceVal  = legacyPercentAtLevel(pick, level)
for _, key in ipairs({ "Main Damage", "Flat Damage", "Reflect Damage" }) do
elseif type(flat) == "table" and #flat > 0 and type(flat[1]) == "table" then
local lst = dmg[key]
local pick = flat[1]
if type(lst) == "table" and #lst > 0 then
local atkFlag  = (pick["ATK-Based"] == true)
return true
local matkFlag = (pick["MATK-Based"] == true)
end
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag)
 
sourceKind = "Flat"
sourceVal  = legacyPercentAtLevel(pick, level)
end
end
end
end


return false
local scalingLines = formatScalingCompactLines(scaling)
end
local hasSource    = (sourceVal ~= nil and tostring(sourceVal) ~= "")
local hasScaling  = (type(scalingLines) == "table" and #scalingLines > 0)


local function buildModuleQuickStats(rec, level, maxLevel, promo)
if (not hasSource) and (not hasScaling) then
local mech = (type(rec) == "table" and type(rec.Mechanics) == "table") and rec.Mechanics or {}
return nil
local bt  = (type(mech["Basic Timings"]) == "table") and mech["Basic Timings"] or {}
end
local rc  = (type(mech["Resource Cost"]) == "table") and mech["Resource Cost"] or {}
 
local hasMod = (basisWord ~= nil and tostring(basisWord) ~= "")


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


-- Range (0 => —)
if hasSource and (not hasScaling) then
local rangeVal = nil
table.insert(extra, "sv-only-source")
if mech.Range ~= nil and not isNoneLike(mech.Range) then
elseif hasScaling and (not hasSource) then
local n = toNum(mech.Range)
table.insert(extra, "sv-only-scaling")
if n ~= nil then
if n ~= 0 then
rangeVal = mw.text.nowiki(formatUnitValue(mech.Range) or tostring(mech.Range))
end
else
-- non-numeric range (rare) – show unless “none”
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: Size (Number), none/missing => —
local wrap = mw.html.create("div")
local areaVal = formatAreaSize(mech.Area)
wrap:addClass("sv-source-grid")


-- Timings: 0 => —
if hasMod then
local castSeries = seriesFromValuePair(bt["Cast Time"], maxLevel)
local modCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-modifier")
local cdSeries  = seriesFromValuePair(bt["Cooldown"],  maxLevel)
modCol:tag("div"):addClass("sv-source-pill"):wikitext("Modifier")
local durSeries  = seriesFromValuePair(bt["Duration"],  maxLevel)
modCol:tag("div"):addClass("sv-modifier-value"):wikitext(mw.text.nowiki(basisWord))
end
 
if hasSource then
local sourceCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-main")
sourceCol:tag("div"):addClass("sv-source-pill"):wikitext(mw.text.nowiki(sourceKind or "Source"))
sourceCol:tag("div"):addClass("sv-source-value"):wikitext(sourceVal)
end


-- Promote status duration if needed
if hasScaling then
local scalingCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-scaling")
scalingCol:tag("div"):addClass("sv-source-pill"):wikitext("Scaling")


local castVal = displayFromSeries(castSeries, level)
local list = scalingCol:tag("div"):addClass("sv-scaling-list")
local cdVal  = displayFromSeries(cdSeries, level)
for _, line in ipairs(scalingLines) do
local durVal = displayFromSeries(durSeries, level)
list:tag("div"):addClass("sv-scaling-item"):wikitext(mw.text.nowiki(line))
end
end


if (durVal == nil) and type(promo) == "table" and type(promo.durationBlock) == "table" then
return {
  durSeries = seriesFromValuePair(promo.durationBlock, maxLevel)
inner = tostring(wrap),
  durVal = displayFromSeries(durSeries, level)
classes = extra,
}
end
end


-- Cost: build combined series, and never show "0 HP"
-- Module Slot 4: Quick Stats (3x2)
local function labeledSeries(block, label)
function PLUGINS.QuickStats(rec, ctx)
local s = seriesFromValuePair(block, maxLevel)
local level = ctx.level or 1
if not s then return nil end
local maxLevel = ctx.maxLevel or 1
local any = false
local promo = ctx.promo
for i, v in ipairs(s) do
 
if v ~= "" then
local mech = (type(rec) == "table" and type(rec.Mechanics) == "table") and rec.Mechanics or {}
s[i] = tostring(v) .. " " .. label
local bt  = (type(mech["Basic Timings"]) == "table") and mech["Basic Timings"] or {}
any = true
local rc  = (type(mech["Resource Cost"]) == "table") and mech["Resource Cost"] or {}
else
s[i] = ""
end
end
return any and s or nil
end


local mpS = labeledSeries(rc["Mana Cost"], "MP")
local function dash() return "" end
local hpS = labeledSeries(rc["Health Cost"], "HP")


local costSeries = {}
-- Range (0 => —)
for lv = 1, maxLevel do
local rangeVal = nil
local mp = mpS and mpS[lv] or "—"
if mech.Range ~= nil and not isNoneLike(mech.Range) then
local hp = hpS and hpS[lv] or "—"
local n = toNum(mech.Range)
 
if n ~= nil then
if mp ~= "—" and hp ~= "—" then
if n ~= 0 then
costSeries[lv] = mp .. " + " .. hp
rangeVal = mw.text.nowiki(formatUnitValue(mech.Range) or tostring(mech.Range))
elseif mp ~= "—" then
end
costSeries[lv] = mp
elseif hp ~= "—" then
costSeries[lv] = hp
else
else
costSeries[lv] = ""
local t = mw.text.trim(tostring(mech.Range))
if t ~= "" and not isNoneLike(t) then
rangeVal = mw.text.nowiki(t)
end
end
end
end
end


local costVal = displayFromSeries(costSeries, level)
-- Area
local areaVal = formatAreaSize(mech.Area)


local grid = mw.html.create("div")
-- Timings
grid:addClass("sv-m4-grid")
local castSeries = seriesFromValuePair(bt["Cast Time"], maxLevel)
local cdSeries  = seriesFromValuePair(bt["Cooldown"],  maxLevel)
local durSeries  = seriesFromValuePair(bt["Duration"],  maxLevel)


local function addCell(label, val)
local castVal = displayFromSeries(castSeries, level)
local cell = grid:tag("div"):addClass("sv-m4-cell")
local cdVal  = displayFromSeries(cdSeries, level)
cell:tag("div"):addClass("sv-m4-label"):wikitext(mw.text.nowiki(label))
local durVal  = displayFromSeries(durSeries, level)
cell:tag("div"):addClass("sv-m4-value"):wikitext(val or dash())
 
-- Promote status duration if needed
if (durVal == nil) and type(promo) == "table" and type(promo.durationBlock) == "table" then
durSeries = seriesFromValuePair(promo.durationBlock, maxLevel)
durVal = displayFromSeries(durSeries, level)
end
end


addCell("Range",    rangeVal)
-- Cost: combine MP + HP; never show "0 HP" (seriesFromValuePair already turns 0 into "")
addCell("Area",     areaVal)
local function labeledSeries(block, label)
addCell("Cost",     costVal)
local s = seriesFromValuePair(block, maxLevel)
addCell("Cast Time", castVal)
if not s then return nil end
addCell("Cooldown",  cdVal)
local any = false
addCell("Duration",  durVal)
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


return moduleBox(4, "module-quick-stats", tostring(grid), false)
local mpS = labeledSeries(rc["Mana Cost"], "MP")
end
local hpS = labeledSeries(rc["Health Cost"], "HP")


local function computeDurationPromotion(rec, maxLevel)
local costSeries = {}
-- Only:
for lv = 1, maxLevel do
-- - non-damaging skills
local mp = mpS and mpS[lv] or "—"
-- - AND missing/blank Duration in Basic Timings
local hp = hpS and hpS[lv] or ""
-- - AND a Status Application has a Duration block
-- Then: promote that status duration into Module 4 and suppress it in Applies row.
if type(rec) ~= "table" then return nil end
if skillHasAnyDamage(rec, maxLevel) then return nil end


local mech = (type(rec.Mechanics) == "table") and rec.Mechanics or {}
if mp ~= "—" and hp ~= "—" then
local bt  = (type(mech["Basic Timings"]) == "table") and mech["Basic Timings"] or {}
costSeries[lv] = mp .. " + " .. hp
local durS = seriesFromValuePair(bt["Duration"], maxLevel)
elseif mp ~= "" then
costSeries[lv] = mp
elseif hp ~= "" then
costSeries[lv] = hp
else
costSeries[lv] = ""
end
end


if durS ~= nil then
local costVal = displayFromSeries(costSeries, level)
  local any = false
  for _, v in ipairs(durS) do
    if v ~= "—" then any = true break end
  end
  if any then
    return nil
  end
end
-- if durS is nil OR all "—", allow promotion


local grid = mw.html.create("div")
grid:addClass("sv-m4-grid")
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


local apps = rec["Status Applications"]
addCell("Range",    rangeVal)
if type(apps) ~= "table" then return nil end
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
 
----------------------------------------------------------------------
-- Generic slot renderers
----------------------------------------------------------------------
 
local function normalizeResult(res)
if res == nil then return nil end
if type(res) == "string" then
return { inner = res, classes = nil }
end
if type(res) == "table" then
local inner = res.inner
if type(inner) ~= "string" then
inner = (inner ~= nil) and tostring(inner) or ""
end
return { inner = inner, classes = res.classes }
end
return { inner = tostring(res), classes = nil }
end


for idx, app in ipairs(apps) do
local function safeCallPlugin(name, rec, ctx)
if type(app) == "table" and type(app.Duration) == "table" then
local fn = PLUGINS[name]
local s = seriesFromValuePair(app.Duration, maxLevel)
if type(fn) ~= "function" then
if s then
return nil
for _, v in ipairs(s) do
end
if v ~= "" then
local ok, out = pcall(fn, rec, ctx)
return {
if not ok then
durationBlock = app.Duration,
return nil
suppressDurationIndex = idx,
end
}
return normalizeResult(out)
end
end
end
 
end
local function renderHeroBarSlot(slotIndex, rec, ctx)
end
local pluginName = HERO_BAR_SLOT_ASSIGNMENT[slotIndex]
end
if not pluginName then
return heroBarBox(slotIndex, nil, "", true)
end
 
local res = safeCallPlugin(pluginName, rec, ctx)
if not res or not res.inner or res.inner == "" then
return heroBarBox(slotIndex, nil, "", true)
end
 
return heroBarBox(slotIndex, res.classes, res.inner, false)
end
 
local function renderModuleSlot(slotIndex, rec, ctx)
local pluginName = HERO_MODULE_SLOT_ASSIGNMENT[slotIndex]
if not pluginName then
return moduleBox(slotIndex, nil, "", true)
end
 
local res = safeCallPlugin(pluginName, rec, ctx)
if not res or not res.inner or res.inner == "" then
return moduleBox(slotIndex, nil, "", true)
end
 
return moduleBox(slotIndex, res.classes, res.inner, false)
end
 
----------------------------------------------------------------------
-- UI builders (hero bar + modules grid)
----------------------------------------------------------------------
 
local function buildHeroBarUI(rec, ctx)
local bar = mw.html.create("div")
bar:addClass("hero-bar-grid")
 
bar:wikitext(renderHeroBarSlot(1, rec, ctx))
bar:wikitext(renderHeroBarSlot(2, rec, ctx))
 
return tostring(bar)
end
 
local function buildHeroModulesUI(rec, ctx)
local grid = mw.html.create("div")
grid:addClass("hero-modules-grid")
 
for slot = 1, 4 do
grid:wikitext(renderModuleSlot(slot, rec, ctx))
end
 
return tostring(grid)
end
 
local function addHeroModulesRow(tbl, modulesUI)
if not modulesUI or modulesUI == "" then
return
end
 
local row = tbl:tag("tr")
row:addClass("hero-modules-row")
 
local cell = row:tag("td")
cell:attr("colspan", 2)
cell:addClass("hero-modules-cell")
cell:wikitext(modulesUI)
end
 
----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------
 
local function buildInfobox(rec, opts)
opts = opts or {}
local showUsers = (opts.showUsers ~= false)
 
local maxLevel = tonumber(rec["Max Level"]) or 1
if maxLevel < 1 then maxLevel = 1 end
local level = clamp(maxLevel, 1, maxLevel)


return nil
-- ctx passed to every plug-in (the only allowed “shared state”)
end
local ctx = {
maxLevel = maxLevel,
level = level,
-- computed below:
nonDamaging = false,
promo = nil,
}


local function buildHeroModulesUI(rec, level, maxLevel)
-- Determine “non-damaging” for SkillType (hide Damage/Element)
local grid = mw.html.create("div")
grid:addClass("hero-modules-grid")
 
local nonDamaging = false
do
do
local dmgVal = nil
local dmgVal = nil
Line 1,479: Line 1,599:
end
end
end
end
nonDamaging = isNoneLike(dmgVal) or (not skillHasAnyDamage(rec, maxLevel))
ctx.nonDamaging = isNoneLike(dmgVal) or (not skillHasAnyDamage(rec, maxLevel))
end
 
local promo = computeDurationPromotion(rec, maxLevel)
 
grid:wikitext(buildModuleLevelSelector(level, maxLevel))
grid:wikitext(buildModuleSkillType(rec.Type or {}, nonDamaging))
 
local m3 = buildModuleSkillSource(rec, level, maxLevel)
grid:wikitext(m3 or buildEmptyModule(3))
 
grid:wikitext(buildModuleQuickStats(rec, level, maxLevel, promo))
 
return tostring(grid), promo
end
 
local function addHeroModulesRow(tbl, modulesUI)
if not modulesUI or modulesUI == "" then
return
end
end


local row = tbl:tag("tr")
-- Duration promotion (for QuickStats + Applies suppression)
row:addClass("hero-modules-row")
ctx.promo = computeDurationPromotion(rec, maxLevel)
 
local cell = row:tag("td")
cell:attr("colspan", 2)
cell:addClass("hero-modules-cell")
cell:wikitext(modulesUI)
end
 
----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------
 
local function buildInfobox(rec, opts)
opts = opts or {}
local showUsers = (opts.showUsers ~= false)
 
local maxLevel = tonumber(rec["Max Level"]) or 1
if maxLevel < 1 then maxLevel = 1 end
local level = clamp(maxLevel, 1, maxLevel)


local root = mw.html.create("table")
local root = mw.html.create("table")
Line 1,536: Line 1,620:
end
end


local icon  = rec.Icon
local title = rec["External Name"] or rec.Name or rec["Internal Name"] or "Unknown Skill"
local desc  = rec.Description or ""
local desc  = rec.Description or ""


-- Hero Title Bar
local heroRow = root:tag("tr")
local heroRow = root:tag("tr")
heroRow:addClass("spiritvale-infobox-main")
heroRow:addClass("spiritvale-infobox-main")
Line 1,548: Line 1,631:
heroCell:attr("colspan", 2)
heroCell:attr("colspan", 2)
heroCell:addClass("sv-hero-title-cell")
heroCell:addClass("sv-hero-title-cell")
heroCell:wikitext(buildHeroBarUI(rec, ctx))


    heroCell:wikitext(buildHeroBarUI(rec, level, maxLevel, icon, title))
-- Description Bar (special-cased for now)
 
if desc ~= "" then
if desc ~= "" then
local descRow = root:tag("tr")
local descRow = root:tag("tr")
Line 1,569: Line 1,652:
end
end


local modulesUI, promo = buildHeroModulesUI(rec, level, maxLevel)
-- Modules row
local modulesUI = buildHeroModulesUI(rec, ctx)
addHeroModulesRow(root, modulesUI)
addHeroModulesRow(root, modulesUI)


-- Users (hide on direct skill page)
if showUsers then
if showUsers then
local users = rec.Users or {}
local users = rec.Users or {}
Line 1,580: Line 1,665:
end
end


-- Requirements
local req = rec.Requirements or {}
local req = rec.Requirements or {}
local hasReq =
local hasReq =
Line 1,605: Line 1,691:
end
end


-- Mechanics (detailed rows still shown; Range/Area/Timings/Cost are in QuickStats)
local mech = rec.Mechanics or {}
local mech = rec.Mechanics or {}
if next(mech) ~= nil then
if next(mech) ~= nil then
-- Range / Area / Timing / Resource Cost rows are removed:
-- They’re now covered by Module 4.
if mech["Autocast Multiplier"] ~= nil then
if mech["Autocast Multiplier"] ~= nil then
addRow(root, "Autocast Multiplier", tostring(mech["Autocast Multiplier"]), "sv-row-mech", "Mechanics.Autocast Multiplier")
addRow(root, "Autocast Multiplier", tostring(mech["Autocast Multiplier"]), "sv-row-mech", "Mechanics.Autocast Multiplier")
Line 1,617: Line 1,702:
end
end


-- Source + Scaling are displayed in Module 3 now, so we do NOT repeat them as body rows.
-- Source + Scaling are displayed in SourceType module now, so we do NOT repeat them as body rows.
-- Keep the detailed legacy damage breakdown rows only (Main/Flat/Reflect/Healing).
-- Keep the detailed legacy damage breakdown rows only (Main/Flat/Reflect/Healing) when Source missing.
if type(rec.Source) ~= "table" then
if type(rec.Source) ~= "table" then
local dmg = rec.Damage or {}
local dmg = rec.Damage or {}
Line 1,645: Line 1,730:
end
end


-- Modifiers flags
local modsText = formatModifiers(rec.Modifiers)
local modsText = formatModifiers(rec.Modifiers)
if modsText then
if modsText then
Line 1,650: Line 1,736:
end
end


local suppressIdx = (type(promo) == "table") and promo.suppressDurationIndex or nil
-- Status (suppress promoted duration line if needed)
local suppressIdx = (type(ctx.promo) == "table") and ctx.promo.suppressDurationIndex or nil
local statusApps = formatStatusApplications(rec["Status Applications"], maxLevel, level, suppressIdx)
local statusApps = formatStatusApplications(rec["Status Applications"], maxLevel, level, suppressIdx)
local statusRem  = formatStatusRemoval(rec["Status Removal"], maxLevel, level)
local statusRem  = formatStatusRemoval(rec["Status Removal"], maxLevel, level)
Line 1,658: Line 1,745:
end
end


-- Events
local eventsText = formatEvents(rec.Events)
local eventsText = formatEvents(rec.Events)
if eventsText then
if eventsText then
Line 1,663: Line 1,751:
end
end


-- Notes
if type(rec.Notes) == "table" and #rec.Notes > 0 then
if type(rec.Notes) == "table" and #rec.Notes > 0 then
addRow(root, "Notes", table.concat(rec.Notes, "<br />"), "sv-row-meta", "Notes")
addRow(root, "Notes", table.concat(rec.Notes, "<br />"), "sv-row-meta", "Notes")