Module:GameSkills: Difference between revisions
From SpiritVale Wiki
More actions
No edit summary |
No edit summary |
||
| (7 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- Module:GameSkills | -- Module:GameSkills | ||
-- | -- | ||
-- | -- Renders active skill data (Data:skills.json) into an infobox-style table, | ||
-- and can also list all skills for a given user/class (page name). | |||
- | |||
-- | |||
-- | -- | ||
-- | -- Standard Hero Layout (reused across the wiki): | ||
-- | -- 1) hero-title-bar (icon + name) | ||
-- | -- 2) hero-description-bar (description strip) | ||
-- | -- 3) hero-modules row (4 slots: hero-module-1..4) | ||
-- | -- - Slot 1: module-level-selector | ||
-- - Slot 2: module-skill-type | |||
-- - Slot 3: skill-source-module (Basis + Source + Scaling) OR blank | |||
-- - Slot 4: empty (reserved) | |||
-- | |||
-- Requires Common.js logic that updates: | |||
-- - .sv-dyn spans via data-series | |||
-- - .sv-level-num + data-level on .sv-skill-card | |||
-- - binds to input.sv-level-range inside each card | |||
local GameData = require("Module:GameData") | local GameData = require("Module:GameData") | ||
| Line 22: | Line 23: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- Data cache | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
| Line 33: | Line 34: | ||
return skillsCache | return skillsCache | ||
end | end | ||
---------------------------------------------------------------------- | |||
-- Small utilities | |||
---------------------------------------------------------------------- | |||
local function getArgs(frame) | local function getArgs(frame) | ||
local parent = frame:getParent() | local parent = frame:getParent() | ||
return (parent and parent.args) or frame.args | |||
end | end | ||
| Line 71: | Line 49: | ||
end | end | ||
s = mw.text.trim(s) | s = mw.text.trim(s) | ||
return (s ~= "" and s) or nil | |||
end | end | ||
| Line 94: | Line 69: | ||
return lo | return lo | ||
end | end | ||
if n < lo then | if n < lo then return lo end | ||
if n > hi then return hi end | |||
if n > hi then | |||
return n | return n | ||
end | end | ||
| Line 116: | Line 87: | ||
s = mw.ustring.gsub(s, "%.$", "") | s = mw.ustring.gsub(s, "%.$", "") | ||
return s | return s | ||
end | |||
local function listToText(list, sep) | |||
if type(list) ~= "table" or #list == 0 then | |||
return nil | |||
end | |||
return table.concat(list, sep or ", ") | |||
end | |||
-- Add a labeled infobox row (with optional hooks for future) | |||
local function addRow(tbl, label, value, rowClass, dataKey) | |||
if value == nil or value == "" then | |||
return | |||
end | |||
local row = tbl:tag("tr") | |||
row:addClass("sv-row") | |||
if rowClass then | |||
row:addClass(rowClass) | |||
end | |||
if dataKey then | |||
row:attr("data-field", dataKey) | |||
end | |||
row:tag("th"):wikitext(label):done() | |||
row:tag("td"):wikitext(value):done() | |||
end | end | ||
-- Handles either a scalar OR { Value = ..., Unit = ... } | -- Handles either a scalar OR { Value = ..., Unit = ... } | ||
-- IMPORTANT: | -- IMPORTANT: wikiprep often converts unit-wrapped pairs into strings already. | ||
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 140: | Line 137: | ||
end | end | ||
return (v ~= nil) and tostring(v) or nil | |||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- Dynamic | -- Dynamic spans (JS-driven) | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
| Line 198: | Line 192: | ||
end | end | ||
-- | -- Base/Per Level renderer: | ||
-- - | -- - Per Level list -> dynamic span (or single value if flat) | ||
-- - | -- - Per Level scalar -> "Base" + "Per Level" lines | ||
local function valuePairDynamicLines(name, block, maxLevel, level) | local function valuePairDynamicLines(name, block, maxLevel, level) | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
| Line 209: | Line 203: | ||
local per = block["Per Level"] | local per = block["Per Level"] | ||
-- Per Level list (expanded) | -- Per Level list (expanded by wikiprep) | ||
if type(per) == "table" then | if type(per) == "table" then | ||
if #per == 0 then | if #per == 0 then | ||
local baseText = formatUnitValue(base) | local baseText = formatUnitValue(base) | ||
return baseText and { string.format("%s: %s", name, mw.text.nowiki(baseText)) } or {} | |||
end | end | ||
if isFlatList(per) then | if isFlatList(per) then | ||
local baseText = formatUnitValue(base) | local baseText = formatUnitValue(base) | ||
local one = formatUnitValue(per[1]) or tostring(per[1]) | local one = formatUnitValue(per[1]) or tostring(per[1]) | ||
local show = baseText or one | local show = baseText or one | ||
return show and { string.format("%s: %s", name, mw.text.nowiki(show)) } or {} | |||
end | end | ||
local series = {} | local series = {} | ||
for _, v in ipairs(per) do | for _, v in ipairs(per) do | ||
| Line 238: | Line 223: | ||
local dyn = dynSpan(series, level) | local dyn = dynSpan(series, level) | ||
return dyn and { string.format("%s: %s", name, dyn) } or {} | |||
end | end | ||
-- scalar Per Level | -- scalar Per Level | ||
local lines = {} | local lines = {} | ||
local baseText = formatUnitValue(base) | local baseText = formatUnitValue(base) | ||
| Line 261: | Line 243: | ||
local function valuePairDynamicText(name, block, maxLevel, level, sep) | local function valuePairDynamicText(name, block, maxLevel, level, sep) | ||
local lines = valuePairDynamicLines(name, block, maxLevel, level) | local lines = valuePairDynamicLines(name, block, maxLevel, level) | ||
return (#lines > 0) and table.concat(lines, sep or "<br />") or nil | |||
end | |||
local function valuePairRawText(block) | |||
if type(block) ~= "table" then | |||
return nil | return nil | ||
end | end | ||
return table.concat( | |||
local base = block.Base | |||
local per = block["Per Level"] | |||
if type(per) == "table" then | |||
if #per == 0 then | |||
return formatUnitValue(base) | |||
end | |||
if isFlatList(per) then | |||
return formatUnitValue(base) or tostring(per[1]) | |||
end | |||
local vals = {} | |||
for _, v in ipairs(per) do | |||
table.insert(vals, formatUnitValue(v) or tostring(v)) | |||
end | |||
return (#vals > 0) and table.concat(vals, " / ") or nil | |||
end | |||
local baseText = formatUnitValue(base) | |||
local perText = formatUnitValue(per) | |||
if baseText and perText and isNonZeroScalar(per) then | |||
return string.format("%s (Per Level: %s)", baseText, perText) | |||
end | |||
return baseText or perText | |||
end | end | ||
| Line 277: | Line 289: | ||
end | end | ||
local dataset = getSkills() | local dataset = getSkills() | ||
return (dataset.byId or {})[id] | |||
end | end | ||
| Line 296: | Line 307: | ||
for _, rec in ipairs(dataset.records or {}) do | for _, rec in ipairs(dataset.records or {}) do | ||
if type(rec) == "table" then | if type(rec) == "table" then | ||
if rec["External Name"] == name or rec | if rec["External Name"] == name or rec.Name == name or rec["Display Name"] == name then | ||
return rec | return rec | ||
end | end | ||
| Line 328: | Line 339: | ||
end | end | ||
local function valuePairDynamicValueOnly(block, maxLevel, level) | |||
-- | if type(block) ~= "table" then | ||
return nil | |||
end | |||
local base = block.Base | |||
local per = block["Per Level"] | |||
if type(per) == "table" then | |||
if #per == 0 then | |||
local baseText = formatUnitValue(base) | |||
return baseText and mw.text.nowiki(baseText) or nil | |||
end | |||
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 | |||
local series = {} | |||
for _, v in ipairs(per) do | |||
table.insert(series, formatUnitValue(v) or tostring(v)) | |||
end | |||
return dynSpan(series, level) | |||
end | |||
local txt = valuePairRawText(block) | |||
return txt and mw.text.nowiki(txt) or nil | |||
end | |||
-- Source (new unified Damage/Flat/Reflect/Healing) formatter (for infobox rows) | |||
local function formatSource(src, maxLevel, level) | |||
if type(src) ~= "table" then | |||
return nil | |||
end | |||
local kind = src.Type or "Damage" | |||
local isHealing = (kind == "Healing") or (src.Healing == true) | |||
local basis = basisLabel(src, isHealing) | |||
if basis and mw.ustring.lower(tostring(basis)) == mw.ustring.lower(tostring(kind)) then | |||
basis = nil | |||
end | |||
local val = valuePairDynamicValueOnly(src, maxLevel, level) | |||
if not val then | |||
return nil | |||
end | |||
local out = mw.text.nowiki(tostring(kind)) .. ": " .. val | |||
if basis then | |||
out = out .. " " .. mw.text.nowiki(tostring(basis)) | |||
end | |||
return out | |||
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 336: | Line 404: | ||
local isHealing = (entry.Type == "Healing") | local isHealing = (entry.Type == "Healing") | ||
local basis = isHealing and "Healing" or basisLabel(entry) | local basis = isHealing and "Healing" or basisLabel(entry, false) | ||
local baseRaw = entry["Base %"] | local baseRaw = entry["Base %"] | ||
| Line 355: | Line 423: | ||
end | end | ||
local baseText | local baseText | ||
if baseIsPresent() then | if baseIsPresent() then | ||
baseText = (baseN ~= nil) and (fmtNum(baseN) .. "%") or (tostring(baseRaw) .. "%") | |||
end | end | ||
if perN == nil or perN == 0 or not maxLevel or maxLevel <= 0 then | if perN == nil or perN == 0 or not maxLevel or maxLevel <= 0 then | ||
return baseText and mw.text.nowiki(baseText .. " " .. basis) or nil | return baseText and mw.text.nowiki(baseText .. " " .. basis) or nil | ||
end | end | ||
local series = {} | local series = {} | ||
for lv = 1, maxLevel do | for lv = 1, maxLevel do | ||
| Line 406: | Line 468: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | |||
-- Scaling supports either a single dict or a list (for infobox rows) | |||
local function formatScaling(scaling, basisOverride) | |||
if type(scaling) ~= "table" then | |||
return nil | return nil | ||
end | end | ||
local | local list = scaling | ||
if | if #list == 0 then | ||
return nil | if scaling.Percent ~= nil or scaling["Scaling ID"] or scaling["Scaling Name"] then | ||
list = { scaling } | |||
else | |||
return nil | |||
end | |||
end | end | ||
| Line 434: | Line 503: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 457: | Line 523: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 482: | Line 545: | ||
add("Cast Time", "Cast Time") | add("Cast Time", "Cast Time") | ||
add("Cooldown", "Cooldown") | add("Cooldown", "Cooldown") | ||
add("Duration", "Duration") | add("Duration", "Duration") | ||
if bt["Effect Cast Time"] ~= nil then | if bt["Effect Cast Time"] ~= nil then | ||
| Line 495: | Line 558: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 518: | Line 578: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 547: | Line 604: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, ", ") or nil | |||
end | end | ||
local function formatMechanicEffects(effects, maxLevel, level) | local function formatMechanicEffects(effects, maxLevel, level) | ||
if type(effects) ~= "table" then | if type(effects) ~= "table" then | ||
| Line 616: | Line 637: | ||
end | end | ||
local txt = valuePairRawText(block) | |||
local txt = valuePairRawText( | |||
return txt and mw.text.nowiki(txt) or nil | return txt and mw.text.nowiki(txt) or nil | ||
end | end | ||
| Line 642: | Line 662: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 677: | Line 694: | ||
collect("Special", mods["Special Modifiers"]) | collect("Special", mods["Special Modifiers"]) | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 691: | Line 705: | ||
for _, s in ipairs(list) do | for _, s in ipairs(list) do | ||
if type(s) == "table" then | if type(s) == "table" then | ||
local | local typ = s.Type or s.Scope or "Target" | ||
local name | local name = s["Status External Name"] or s["Status Internal Name"] or "Unknown status" | ||
local seg = | local seg = tostring(typ) .. " – " .. tostring(name) | ||
local detail = {} | local detail = {} | ||
if type(s.Duration) == "table" then | if type(s.Duration) == "table" then | ||
local t = valuePairDynamicText("Duration", s.Duration, maxLevel, level, "; ") | local t = valuePairDynamicText("Duration", s.Duration, maxLevel, level, "; ") | ||
if t then | if t then table.insert(detail, t) end | ||
end | end | ||
if type(s.Chance) == "table" then | if type(s.Chance) == "table" then | ||
local t = valuePairDynamicText("Chance", s.Chance, maxLevel, level, "; ") | local t = valuePairDynamicText("Chance", s.Chance, maxLevel, level, "; ") | ||
if t then | if t then table.insert(detail, t) end | ||
end | end | ||
| Line 723: | Line 729: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 748: | Line 751: | ||
end | end | ||
local amt | local amt | ||
if type(r["Per Level"]) == "table" and #r["Per Level"] > 0 and not isFlatList(r["Per Level"]) then | if type(r["Per Level"]) == "table" and #r["Per Level"] > 0 and not isFlatList(r["Per Level"]) then | ||
local series = {} | local series = {} | ||
| Line 757: | Line 760: | ||
else | else | ||
amt = valuePairRawText(r) | amt = valuePairRawText(r) | ||
amt = amt and mw.text.nowiki(amt) or nil | |||
end | end | ||
| Line 770: | Line 771: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 790: | Line 788: | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 824: | Line 819: | ||
end | end | ||
return listHas(users.Classes) or listHas(users.Summons) or listHas(users.Monsters) or listHas(users.Events) | |||
end | end | ||
| Line 844: | Line 834: | ||
local pageName = pageTitle and pageTitle.text or "" | local pageName = pageTitle and pageTitle.text or "" | ||
pageName = trim(pageName) | pageName = trim(pageName) | ||
if not pageName then | if not pageName then | ||
return false | return false | ||
| Line 851: | Line 840: | ||
pageName = mw.ustring.lower(pageName) | pageName = mw.ustring.lower(pageName) | ||
local ext = trim(rec["External Name"] or rec | local ext = trim(rec["External Name"] or rec.Name or rec["Display Name"]) | ||
local internal = trim(rec["Internal Name"] or rec | local internal = trim(rec["Internal Name"] or rec.InternalName or rec.InternalID) | ||
return (ext and mw.ustring.lower(ext) == pageName) or (internal and mw.ustring.lower(internal) == pageName) or false | |||
end | |||
---------------------------------------------------------------------- | |||
-- Hero modules (4-slot scaffold) | |||
---------------------------------------------------------------------- | |||
local function moduleBox(slot, extraClasses, innerHtml, isEmpty) | |||
local box = mw.html.create("div") | |||
box:addClass("hero-module") | |||
box:addClass("hero-module-" .. tostring(slot)) | |||
box:attr("data-hero-module", tostring(slot)) | |||
if extraClasses then | |||
if type(extraClasses) == "string" then | |||
box:addClass(extraClasses) | |||
elseif type(extraClasses) == "table" then | |||
for _, c in ipairs(extraClasses) do | |||
box:addClass(c) | |||
end | |||
end | |||
end | |||
if | if isEmpty then | ||
box:addClass("hero-module-empty") | |||
end | end | ||
if | |||
local body = box:tag("div"):addClass("hero-module-body") | |||
if innerHtml and innerHtml ~= "" then | |||
body:wikitext(innerHtml) | |||
end | end | ||
return | return tostring(box) | ||
end | end | ||
---------------------------------------------------------------- | -- ------------------------------------------------------------ | ||
-- | -- Module 1 – Level Selector | ||
-- FIX: If maxLevel == 1, do NOT emit a range input (prevents JS edge-cases) | |||
-- ------------------------------------------------------------ | |||
local function | local function buildModuleLevelSelector(level, maxLevel) | ||
local inner = mw.html.create("div") | |||
inner:addClass("sv-level-ui") | |||
local | |||
inner:tag("div") | |||
:addClass("sv-level-title") | :addClass("sv-level-title") | ||
:wikitext("Level Select") | :wikitext("Level Select") | ||
inner:tag("div") | |||
:addClass("sv-level-label") | :addClass("sv-level-label") | ||
:wikitext("Level <span class=\"sv-level-num\">" .. tostring(level) .. "</span> / " .. tostring(maxLevel)) | :wikitext("Level <span class=\"sv-level-num\">" .. tostring(level) .. "</span> / " .. tostring(maxLevel)) | ||
local slider = inner:tag("div"):addClass("sv-level-slider") | |||
if tonumber(maxLevel) and tonumber(maxLevel) > 1 then | |||
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 | |||
-- single-level skills: keep layout but no <input> | |||
inner:addClass("sv-level-ui-single") | |||
slider:addClass("sv-level-slider-single") | |||
end | end | ||
return moduleBox(1, "module-level-selector", tostring(inner), false) | |||
end | |||
-- ------------------------------------------------------------ | |||
-- Module 2 – Skill Type | |||
-- ------------------------------------------------------------ | |||
local function buildModuleSkillType(typeBlock) | |||
typeBlock = (type(typeBlock) == "table") and typeBlock or {} | |||
local function valName(x) | local function valName(x) | ||
| Line 912: | Line 933: | ||
end | end | ||
local grid = mw.html.create("div") | |||
grid:addClass("sv-type-grid") | |||
local added = false | |||
local function addChunk(label, rawVal) | local function addChunk(label, rawVal) | ||
local v = valName(rawVal) | local v = valName(rawVal) | ||
| Line 919: | Line 944: | ||
added = true | added = true | ||
local chunk = | 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-label"):wikitext(mw.text.nowiki(label)) | ||
chunk:tag("div"):addClass("sv-type-value"):wikitext(mw.text.nowiki(v)) | chunk:tag("div"):addClass("sv-type-value"):wikitext(mw.text.nowiki(v)) | ||
end | end | ||
addChunk(" | addChunk("Damage", typeBlock.Damage or typeBlock["Damage Type"]) | ||
addChunk("Element", | addChunk("Element", typeBlock.Element or typeBlock["Element Type"]) | ||
addChunk("Target", | addChunk("Target", typeBlock.Target or typeBlock["Target Type"]) | ||
addChunk("Cast | addChunk("Cast", typeBlock.Cast or typeBlock["Cast Type"]) | ||
local body = added and tostring(grid) or "" | |||
return moduleBox(2, "module-skill-type", body, false) | |||
end | |||
-- ============================================================ | |||
-- Module 3 – Skill Source (Basis + Source + Scaling) | |||
-- UPDATES: | |||
-- 1) If basisWord is nil/empty -> hide column 1 entirely | |||
-- 2) Basis word is bold + emphasis; "Magic Attack" forced to wrap nicely | |||
-- 3) Source/Scaling pills always align (header row), regardless of scaling height | |||
-- ============================================================ | |||
local function formatScalingCompactLines(scaling) | |||
if type(scaling) ~= "table" then | |||
return {} | |||
end | |||
local list = scaling | |||
if #list == 0 then | |||
if scaling.Percent ~= nil or scaling["Scaling ID"] or scaling["Scaling Name"] then | |||
list = { scaling } | |||
else | |||
return {} | |||
end | |||
end | |||
local out = {} | |||
for _, s in ipairs(list) do | |||
if type(s) == "table" then | |||
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 | |||
return out | |||
end | |||
local function basisWordFromFlags(atkFlag, matkFlag) | |||
if atkFlag and matkFlag then | |||
return "Hybrid" | |||
elseif atkFlag then | |||
return "Attack" | |||
elseif matkFlag then | |||
return "Magic Attack" | |||
end | |||
return nil | |||
end | |||
local function sourceValueForLevel(src, maxLevel, level) | |||
if type(src) ~= "table" then | |||
return nil | |||
end | |||
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 | |||
local series = {} | |||
for _, v in ipairs(per) do | |||
table.insert(series, formatUnitValue(v) or tostring(v)) | |||
end | |||
return dynSpan(series, level) | |||
end | |||
return valuePairDynamicValueOnly(src, maxLevel, level) | |||
end | |||
local function legacyPercentAtLevel(entry, level) | |||
if type(entry) ~= "table" then | |||
return nil | |||
end | |||
local baseRaw = entry["Base %"] | |||
local perRaw = entry["Per Level %"] | |||
local baseN = toNum(baseRaw) | |||
local perN = toNum(perRaw) | |||
if perN ~= nil and perN ~= 0 then | |||
local total = (baseN or 0) + (perN * level) | |||
return fmtNum(total) .. "%" | |||
end | |||
if baseN ~= nil then | |||
return fmtNum(baseN) .. "%" | |||
end | |||
if baseRaw ~= nil and tostring(baseRaw) ~= "" then | |||
return tostring(baseRaw) .. "%" | |||
end | |||
return nil | |||
end | |||
if not | local function buildBasisMarkup(word) | ||
if not word or word == "" then | |||
return nil | return nil | ||
end | |||
local wrap = mw.html.create("div") | |||
wrap:addClass("sv-basis-word") | |||
wrap:addClass("sv-basis-emph") | |||
local strong = wrap:tag("strong") | |||
if word == "Magic Attack" then | |||
-- forced clean wrap at the desired boundary | |||
strong:wikitext("Magic<br />Attack") | |||
else | |||
strong:wikitext(mw.text.nowiki(word)) | |||
end | end | ||
| Line 936: | Line 1,080: | ||
end | end | ||
local function | local function buildModuleSkillSource(rec, level, maxLevel) | ||
if | local basisWord = nil | ||
local sourceKind = 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 | |||
if (sourceVal == nil or sourceVal == "") and type(rec.Damage) == "table" then | |||
local dmg = rec.Damage | |||
scaling = scaling or dmg.Scaling | |||
local main = dmg["Main Damage"] | |||
local refl = dmg["Reflect Damage"] | |||
local flat = dmg["Flat Damage"] | |||
if type(main) == "table" and #main > 0 then | |||
local pick = nil | |||
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] | |||
if type(pick) == "table" then | |||
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" | |||
sourceVal = legacyPercentAtLevel(pick, level) | |||
end | |||
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 | ||
local | local scalingLines = formatScalingCompactLines(scaling) | ||
local | 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 | |||
local | local basisMarkup = buildBasisMarkup(basisWord) | ||
local hasBasis = (basisMarkup ~= nil and basisMarkup ~= "") | |||
if | local extra = { "skill-source-module" } | ||
if hasSource then table.insert(extra, "sv-has-source") end | |||
if hasScaling then table.insert(extra, "sv-has-scaling") end | |||
if hasBasis then | |||
table.insert(extra, "sv-has-basis") | |||
else | else | ||
table.insert(extra, "sv-no-basis") | |||
end | |||
local wrap = mw.html.create("div") | |||
wrap:addClass("sv-source-grid") | |||
wrap:addClass(hasBasis and "sv-source-cols-3" or "sv-source-cols-2") | |||
-- Helper to add a cell | |||
local function addCell(row, colClass) | |||
local c = row:tag("div") | |||
c:addClass("sv-source-cell") | |||
if colClass then c:addClass(colClass) end | |||
return c | |||
end | end | ||
-- Row 1: header pills (kept parallel) | |||
local head = wrap:tag("div"):addClass("sv-source-row"):addClass("sv-source-head") | |||
if hasBasis then | |||
addCell(head, "sv-source-basis-head") -- empty header cell for alignment | |||
end | |||
local sourceHead = addCell(head, "sv-source-main-head") | |||
if hasSource then | |||
sourceHead:tag("div") | |||
:addClass("sv-source-pill") | |||
:wikitext(mw.text.nowiki(sourceKind or "Damage")) | |||
end | |||
local scalingHead = addCell(head, "sv-source-scaling-head") | |||
if hasScaling then | |||
scalingHead:tag("div") | |||
:addClass("sv-source-pill") | |||
:wikitext("Scaling") | |||
end | |||
-- Row 2: values/content | |||
local body = wrap:tag("div"):addClass("sv-source-row"):addClass("sv-source-body") | |||
if hasBasis then | |||
addCell(body, "sv-source-basis"):wikitext(basisMarkup) | |||
end | |||
local sourceBody = addCell(body, "sv-source-main") | |||
if hasSource then | |||
sourceBody:tag("div") | |||
:addClass("sv-source-value") | |||
:wikitext(sourceVal) | |||
end | |||
local scalingBody = addCell(body, "sv-source-scaling") | |||
if hasScaling then | |||
local list = scalingBody:tag("div"):addClass("sv-scaling-list") | |||
for _, line in ipairs(scalingLines) do | |||
list:tag("div") | |||
:addClass("sv-scaling-item") | |||
:wikitext(mw.text.nowiki(line)) | |||
end | |||
end | |||
return moduleBox(3, extra, tostring(wrap), false) | |||
end | end | ||
local function buildEmptyModule(slot) | |||
return moduleBox(slot, nil, "", true) | |||
end | |||
local function buildHeroModulesUI(rec, level, maxLevel) | |||
local grid = mw.html.create("div") | |||
grid:addClass("hero-modules-grid") | |||
grid:wikitext(buildModuleLevelSelector(level, maxLevel)) | |||
grid:wikitext(buildModuleSkillType(rec.Type or {})) | |||
local m3 = buildModuleSkillSource(rec, level, maxLevel) | |||
grid:wikitext(m3 or buildEmptyModule(3)) | |||
grid:wikitext(buildEmptyModule(4)) | |||
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) | local function buildInfobox(rec, opts) | ||
| Line 967: | Line 1,265: | ||
local maxLevel = tonumber(rec["Max Level"]) or 1 | local maxLevel = tonumber(rec["Max Level"]) or 1 | ||
if maxLevel < 1 then | if maxLevel < 1 then maxLevel = 1 end | ||
local level = clamp(maxLevel, 1, maxLevel) | local level = clamp(maxLevel, 1, maxLevel) | ||
local root = mw.html.create("table") | local root = mw.html.create("table") | ||
root:addClass("spiritvale-skill-infobox") | root:addClass("spiritvale-skill-infobox") | ||
root:addClass("sv-skill-card") | root:addClass("sv-skill-card") | ||
root:attr("data-max-level", tostring(maxLevel)) | root:attr("data-max-level", tostring(maxLevel)) | ||
root:attr("data-level", tostring(level)) | root:attr("data-level", tostring(level)) | ||
if opts.inList then | if opts.inList then | ||
root:addClass("sv-skill-inlist") | root:addClass("sv-skill-inlist") | ||
end | end | ||
local internalId = trim(rec["Internal Name"] or rec.InternalID or rec.ID) | |||
-- | if internalId then | ||
root:attr("data-skill-id", internalId) | |||
end | |||
local icon = rec.Icon | local icon = rec.Icon | ||
local title = rec["External Name"] or rec.Name or rec["Internal Name"] or "Unknown Skill" | 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 "" | ||
local heroRow = root:tag("tr") | local heroRow = root:tag("tr") | ||
heroRow:addClass("spiritvale-infobox-main") | heroRow:addClass("spiritvale-infobox-main") | ||
heroRow:addClass("sv-hero-title-row") | heroRow:addClass("sv-hero-title-row") | ||
heroRow:addClass("hero-title-bar") | |||
local heroCell = heroRow:tag("th") | local heroCell = heroRow:tag("th") | ||
| Line 1,007: | Line 1,297: | ||
local heroInner = heroCell:tag("div") | local heroInner = heroCell:tag("div") | ||
heroInner:addClass("spiritvale-infobox-main-left-inner") | heroInner:addClass("spiritvale-infobox-main-left-inner") | ||
if icon and icon ~= "" then | if icon and icon ~= "" then | ||
| Line 1,017: | Line 1,307: | ||
:wikitext(title) | :wikitext(title) | ||
if desc ~= "" then | if desc ~= "" then | ||
local descRow = root:tag("tr") | local descRow = root:tag("tr") | ||
descRow:addClass("spiritvale-infobox-main") | descRow:addClass("spiritvale-infobox-main") | ||
descRow:addClass("sv-hero-desc-row") | descRow:addClass("sv-hero-desc-row") | ||
descRow:addClass("hero-description-bar") | |||
local descCell = descRow:tag("td") | local descCell = descRow:tag("td") | ||
| Line 1,035: | Line 1,325: | ||
end | end | ||
addHeroModulesRow(root, buildHeroModulesUI(rec, level, maxLevel)) | |||
if showUsers then | if showUsers then | ||
local users = rec.Users or {} | local users = rec.Users or {} | ||
addRow(root, "Classes", listToText(users.Classes)) | addRow(root, "Classes", listToText(users.Classes), "sv-row-users", "Users.Classes") | ||
addRow(root, "Summons", listToText(users.Summons)) | addRow(root, "Summons", listToText(users.Summons), "sv-row-users", "Users.Summons") | ||
addRow(root, "Monsters", listToText(users.Monsters)) | addRow(root, "Monsters", listToText(users.Monsters), "sv-row-users", "Users.Monsters") | ||
addRow(root, "Events", listToText(users.Events)) | addRow(root, "Events", listToText(users.Events), "sv-row-users", "Users.Events") | ||
end | end | ||
local req = rec.Requirements or {} | local req = rec.Requirements or {} | ||
local hasReq = | |||
(type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0) or | |||
(type(req["Required Weapons"]) == "table" and #req["Required Weapons"] > 0) or | |||
(type(req["Required Stances"]) == "table" and #req["Required Stances"] > 0) | |||
if hasReq then | |||
if type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0 then | if type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0 then | ||
local skillParts = {} | local skillParts = {} | ||
| Line 1,072: | Line 1,353: | ||
end | end | ||
end | end | ||
addRow(root, "Required Skills", table.concat(skillParts, ", ")) | addRow(root, "Required Skills", table.concat(skillParts, ", "), "sv-row-req", "Requirements.Required Skills") | ||
end | end | ||
addRow(root, "Required Weapons", listToText(req["Required Weapons"])) | addRow(root, "Required Weapons", listToText(req["Required Weapons"]), "sv-row-req", "Requirements.Required Weapons") | ||
addRow(root, "Required Stances", listToText(req["Required Stances"])) | addRow(root, "Required Stances", listToText(req["Required Stances"]), "sv-row-req", "Requirements.Required Stances") | ||
end | end | ||
local mech = rec.Mechanics or {} | local mech = rec.Mechanics or {} | ||
if next(mech) ~= nil then | if next(mech) ~= nil then | ||
addRow(root, "Range", formatUnitValue(mech.Range)) | addRow(root, "Range", formatUnitValue(mech.Range), "sv-row-mech", "Mechanics.Range") | ||
addRow(root, "Area", formatArea(mech.Area, maxLevel, level)) | addRow(root, "Area", formatArea(mech.Area, maxLevel, level), "sv-row-mech", "Mechanics.Area") | ||
if mech["Autocast Multiplier"] ~= nil then | if mech["Autocast Multiplier"] ~= nil then | ||
addRow(root, "Autocast Multiplier", tostring(mech["Autocast Multiplier"])) | addRow(root, "Autocast Multiplier", tostring(mech["Autocast Multiplier"]), "sv-row-mech", "Mechanics.Autocast Multiplier") | ||
end | end | ||
addRow(root, "Timing", | addRow(root, "Timing", formatTimingBlock(mech["Basic Timings"], maxLevel, level), "sv-row-mech", "Mechanics.Basic Timings") | ||
addRow(root, "Resource Cost", | addRow(root, "Resource Cost", formatResourceCost(mech["Resource Cost"], maxLevel, level), "sv-row-mech", "Mechanics.Resource Cost") | ||
addRow(root, "Combo", | addRow(root, "Combo", formatCombo(mech.Combo), "sv-row-mech", "Mechanics.Combo") | ||
addRow(root, "Special Mechanics", formatMechanicEffects(mech.Effects, maxLevel, level)) | addRow(root, "Special Mechanics", formatMechanicEffects(mech.Effects, maxLevel, level), "sv-row-mech", "Mechanics.Effects") | ||
end | end | ||
---- | -- Source (rows) + Scaling (rows) remain below for now (as you had them) | ||
if type(rec.Source) == "table" then | |||
addRow(root, "Source", formatSource(rec.Source, maxLevel, level), "sv-row-source", "Source") | |||
local basisOverride = | |||
(rec.Source.Type == "Healing" or rec.Source.Healing == true) and "Healing" or nil | |||
addRow(root, "Scaling", formatScaling(rec.Source.Scaling, basisOverride), "sv-row-source", "Source.Scaling") | |||
else | |||
local dmg = rec.Damage or {} | |||
if next(dmg) ~= nil then | |||
local main = dmg["Main Damage"] | |||
local mainNonHeal, healOnly = {}, {} | |||
if type(main) == "table" then | |||
for _, d in ipairs(main) do | |||
if type(d) == "table" and d.Type == "Healing" then | |||
table.insert(healOnly, d) | |||
else | |||
table.insert(mainNonHeal, d) | |||
end | |||
end | end | ||
end | end | ||
local flatList = dmg["Flat Damage"] | |||
local reflList = dmg["Reflect Damage"] | |||
local flatHas = (type(flatList) == "table" and #flatList > 0) | |||
local reflHas = (type(reflList) == "table" and #reflList > 0) | |||
local pureHealing = (#healOnly > 0) and (#mainNonHeal == 0) and (not flatHas) and (not reflHas) | |||
addRow(root, "Main Damage", formatDamageList(mainNonHeal, maxLevel, level, (#mainNonHeal > 1)), "sv-row-source", "Damage.Main Damage") | |||
addRow(root, "Flat Damage", formatDamageList(flatList, maxLevel, level, false), "sv-row-source", "Damage.Flat Damage") | |||
addRow(root, "Reflect Damage", formatDamageList(reflList, maxLevel, level, false), "sv-row-source", "Damage.Reflect Damage") | |||
addRow(root, "Healing", formatDamageList(healOnly, maxLevel, level, false), "sv-row-source", "Damage.Healing") | |||
addRow(root, "Scaling", formatScaling(dmg.Scaling, pureHealing and "Healing" or nil), "sv-row-source", "Damage.Scaling") | |||
end | |||
end | end | ||
local modsText = formatModifiers(rec.Modifiers) | local modsText = formatModifiers(rec.Modifiers) | ||
if modsText then | if modsText then | ||
addRow(root, "Flags", modsText) | addRow(root, "Flags", modsText, "sv-row-meta", "Modifiers") | ||
end | end | ||
local statusApps = formatStatusApplications(rec["Status Applications"], maxLevel, level) | local statusApps = formatStatusApplications(rec["Status Applications"], maxLevel, level) | ||
local statusRem = formatStatusRemoval(rec["Status Removal"], maxLevel, level) | local statusRem = formatStatusRemoval(rec["Status Removal"], maxLevel, level) | ||
if statusApps or statusRem then | if statusApps or statusRem then | ||
addRow(root, "Applies", statusApps) | addRow(root, "Applies", statusApps, "sv-row-status", "Status Applications") | ||
addRow(root, "Removes", statusRem) | addRow(root, "Removes", statusRem, "sv-row-status", "Status Removal") | ||
end | end | ||
local eventsText = formatEvents(rec.Events) | local eventsText = formatEvents(rec.Events) | ||
if eventsText then | if eventsText then | ||
addRow(root, "Triggers", eventsText) | addRow(root, "Triggers", eventsText, "sv-row-meta", "Events") | ||
end | end | ||
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 />")) | addRow(root, "Notes", table.concat(rec.Notes, "<br />"), "sv-row-meta", "Notes") | ||
end | end | ||
| Line 1,193: | Line 1,466: | ||
if #matches == 0 then | if #matches == 0 then | ||
return string.format( | return string.format("<strong>No skills found for:</strong> %s", mw.text.nowiki(userName)) | ||
end | end | ||
local root = mw.html.create("div") | local root = mw.html.create("div") | ||
root:addClass("sv-skill-collection") | root:addClass("sv-skill-collection") | ||
for _, rec in ipairs(matches) do | for _, rec in ipairs(matches) do | ||
local item = root:tag("div"):addClass("sv-skill-item") | local item = root:tag("div"):addClass("sv-skill-item") | ||
item:wikitext(buildInfobox(rec, { showUsers = false, inList = true })) | item:wikitext(buildInfobox(rec, { showUsers = false, inList = true })) | ||