Module:GameSkills: Difference between revisions
From SpiritVale Wiki
More actions
No edit summary |
No edit summary |
||
| (17 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- Module:GameSkills | -- Module:GameSkills | ||
-- | -- | ||
-- Renders active skill data ( | -- Renders active skill data (Data:skills.json) into an infobox-style table, | ||
-- and can also list all skills for a given user/class. | -- and can also list all skills for a given user/class (page name). | ||
-- | -- | ||
-- | -- Features: | ||
-- | -- - Per-skill Level Select slider (client-side JS updates .sv-dyn spans) | ||
-- | -- - Default level = Max Level | ||
-- | -- - .sv-skill-card + data-max-level / data-level hooks for JS | ||
-- - Uses dynamic spans instead of long Lv1/Lv2/... lists | |||
-- - Unified top band: Level Select (left) + Type/Element/Target/Cast (right) | |||
-- - List-mode: wraps all skills in one wrapper panel + stable .sv-skill-item divs | |||
-- | -- | ||
-- | -- NOTE: We add a small amount of inline styling on the top-band inner panels | ||
-- | -- (Level Select + Type box) so light mode remains readable even if a skin/theme | ||
-- | -- class selector isn’t reliable. | ||
local GameData = require("Module:GameData") | local GameData = require("Module:GameData") | ||
| Line 18: | Line 21: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- Data cache | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
| Line 29: | Line 32: | ||
return skillsCache | return skillsCache | ||
end | end | ||
---------------------------------------------------------------------- | |||
-- Small utilities | |||
---------------------------------------------------------------------- | |||
local function getArgs(frame) | local function getArgs(frame) | ||
local parent = frame:getParent() | local parent = frame:getParent() | ||
if | return (parent and parent.args) or frame.args | ||
return | end | ||
local function trim(s) | |||
if type(s) ~= "string" then | |||
return nil | |||
end | |||
s = mw.text.trim(s) | |||
return (s ~= "" and s) or nil | |||
end | |||
local function toNum(v) | |||
if type(v) == "number" then | |||
return v | |||
end | |||
if type(v) == "string" then | |||
return tonumber(v) | |||
end | |||
if type(v) == "table" and v.Value ~= nil then | |||
return toNum(v.Value) | |||
end | end | ||
return | return nil | ||
end | |||
local function clamp(n, lo, hi) | |||
if type(n) ~= "number" then | |||
return lo | |||
end | |||
if n < lo then return lo end | |||
if n > hi then return hi end | |||
return n | |||
end | |||
local function fmtNum(n) | |||
if type(n) ~= "number" then | |||
return (n ~= nil) and tostring(n) or nil | |||
end | |||
if math.abs(n - math.floor(n)) < 1e-9 then | |||
return tostring(math.floor(n)) | |||
end | |||
local s = string.format("%.4f", n) | |||
s = mw.ustring.gsub(s, "0+$", "") | |||
s = mw.ustring.gsub(s, "%.$", "") | |||
return s | |||
end | end | ||
| Line 54: | Line 103: | ||
end | end | ||
-- Handles either a scalar OR { Value = ..., Unit = ... } | |||
-- IMPORTANT: wikiprep often converts unit-wrapped pairs into strings already. | |||
-- Handles either a scalar OR {Value=..., Unit=...} | |||
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 | ||
local unit = v.Unit | local unit = v.Unit | ||
local val = v.Value | local val = v.Value | ||
if unit == "percent_decimal" or unit == "percent_whole" or unit == "percent" then | |||
if unit == "percent_decimal" | |||
return tostring(val) .. "%" | return tostring(val) .. "%" | ||
elseif unit == "seconds" then | elseif unit == "seconds" then | ||
| Line 93: | Line 125: | ||
end | end | ||
return (v ~= nil) and tostring(v) or nil | |||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- Dynamic spans (JS-driven) | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local function dynSpan(series, level) | |||
local function | if type(series) ~= "table" or #series == 0 then | ||
return nil | |||
if | end | ||
end | |||
level = clamp(level or #series, 1, #series) | |||
- | local span = mw.html.create("span") | ||
span:addClass("sv-dyn") | |||
span:attr("data-series", mw.text.jsonEncode(series)) | |||
span:wikitext(mw.text.nowiki(series[level] or "")) | |||
return tostring(span) | |||
end | end | ||
local function isFlatList(list) | local function isFlatList(list) | ||
| Line 153: | Line 161: | ||
local function isNonZeroScalar(v) | local function isNonZeroScalar(v) | ||
if v == nil then return false end | if v == nil then | ||
if type(v) == "number" then return v ~= 0 end | return false | ||
end | |||
if type(v) == "number" then | |||
return v ~= 0 | |||
end | |||
if type(v) == "string" then | if type(v) == "string" then | ||
local n = tonumber(v) | local n = tonumber(v) | ||
| Line 168: | Line 180: | ||
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 | local function valuePairDynamicLines(name, block, maxLevel, level) | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
return {} | return {} | ||
| Line 179: | Line 191: | ||
local per = block["Per Level"] | local per = block["Per Level"] | ||
-- Per Level list ( | -- 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 = 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 | local series = {} | ||
for _, v in ipairs(per) do | for _, v in ipairs(per) do | ||
table.insert( | table.insert(series, formatUnitValue(v) or tostring(v)) | ||
end | end | ||
local dyn = dynSpan(series, level) | |||
return dyn and { string.format("%s: %s", name, dyn) } or {} | |||
return { string.format("%s: %s", name, | |||
end | end | ||
-- | -- scalar Per Level | ||
local lines = {} | local lines = {} | ||
local baseText = formatUnitValue(base) | local baseText = formatUnitValue(base) | ||
| Line 216: | Line 220: | ||
if baseText then | if baseText then | ||
table.insert(lines, string.format("%s: %s", name, baseText)) | table.insert(lines, string.format("%s: %s", name, mw.text.nowiki(baseText))) | ||
end | end | ||
if perText and isNonZeroScalar(per) then | if perText and isNonZeroScalar(per) then | ||
table.insert(lines, string.format("%s Per Level: %s", name, perText)) | table.insert(lines, string.format("%s Per Level: %s", name, mw.text.nowiki(perText))) | ||
end | end | ||
| Line 225: | Line 229: | ||
end | end | ||
local function | local function valuePairDynamicText(name, block, maxLevel, level, sep) | ||
local lines = | local lines = valuePairDynamicLines(name, block, maxLevel, level) | ||
return (#lines > 0) and table.concat(lines, sep or "<br />") or nil | |||
end | end | ||
-- | -- Raw text for a Base/Per Level block (used by Special Mechanics & Source fallbacks) | ||
local function valuePairRawText(block) | local function valuePairRawText(block) | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
| Line 249: | Line 250: | ||
return formatUnitValue(base) or tostring(per[1]) | return formatUnitValue(base) or tostring(per[1]) | ||
end | end | ||
local vals = {} | local vals = {} | ||
for _, v in ipairs(per) do | for _, v in ipairs(per) do | ||
| Line 262: | Line 264: | ||
return string.format("%s (Per Level: %s)", baseText, perText) | return string.format("%s (Per Level: %s)", baseText, perText) | ||
end | end | ||
return baseText or perText | return baseText or perText | ||
end | end | ||
local function | ---------------------------------------------------------------------- | ||
if | -- Lookups | ||
---------------------------------------------------------------------- | |||
local function getSkillById(id) | |||
id = trim(id) | |||
if not id then | |||
return nil | |||
end | |||
local dataset = getSkills() | |||
return (dataset.byId or {})[id] | |||
end | |||
local function findSkillByName(name) | |||
name = trim(name) | |||
if not name then | |||
return nil | return nil | ||
end | end | ||
local | |||
for _, | local dataset = getSkills() | ||
if type( | local byName = dataset.byName or {} | ||
if byName[name] then | |||
return byName[name] | |||
end | |||
for _, rec in ipairs(dataset.records or {}) do | |||
if type(rec) == "table" then | |||
if rec["External Name"] == name or rec["Name"] == name or rec["Display Name"] == name then | |||
return rec | |||
end | end | ||
end | end | ||
end | end | ||
if # | |||
return nil | |||
end | |||
---------------------------------------------------------------------- | |||
-- Formatting helpers | |||
---------------------------------------------------------------------- | |||
local function basisLabel(entry, isHealing) | |||
if isHealing then | |||
return "Healing" | |||
end | |||
local atk = entry and entry["ATK-Based"] | |||
local matk = entry and entry["MATK-Based"] | |||
if atk and matk then | |||
return "Attack/Magic Attack" | |||
elseif atk then | |||
return "Attack" | |||
elseif matk then | |||
return "Magic Attack" | |||
end | |||
return "Damage" | |||
end | |||
-- Build a value-only output for a Base/Per Level block, using dyn spans when needed | |||
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 | |||
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 | return nil | ||
end | end | ||
return table. | |||
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 (safe to remove once migrated) | |||
local function formatDamageEntry(entry, maxLevel, level) | |||
if type(entry) ~= "table" then | |||
return nil | |||
end | |||
local isHealing = (entry.Type == "Healing") | |||
local basis = isHealing and "Healing" or basisLabel(entry, false) | |||
local baseRaw = entry["Base %"] | |||
local perRaw = entry["Per Level %"] | |||
local baseN = toNum(baseRaw) | |||
local perN = toNum(perRaw) | |||
local function baseIsPresent() | |||
if baseN ~= nil then | |||
return baseN ~= 0 | |||
end | |||
if baseRaw ~= nil then | |||
local s = tostring(baseRaw) | |||
return (s ~= "" and s ~= "0" and s ~= "0.0" and s ~= "0.00") | |||
end | |||
return false | |||
end | |||
local baseText | |||
if baseIsPresent() then | |||
baseText = (baseN ~= nil) and (fmtNum(baseN) .. "%") or (tostring(baseRaw) .. "%") | |||
end | |||
if perN == nil or perN == 0 or not maxLevel or maxLevel <= 0 then | |||
return baseText and mw.text.nowiki(baseText .. " " .. basis) or nil | |||
end | |||
local series = {} | |||
for lv = 1, maxLevel do | |||
local perPart = perN * lv | |||
if baseText and baseN ~= nil then | |||
local total = baseN + perPart | |||
table.insert(series, string.format("%s%% %s", fmtNum(total), basis)) | |||
elseif baseText then | |||
table.insert(series, string.format("%s + %s%% %s", baseText, fmtNum(perPart), basis)) | |||
else | |||
table.insert(series, string.format("%s%% %s", fmtNum(perPart), basis)) | |||
end | |||
end | |||
return dynSpan(series, level) | |||
end | end | ||
local function | local function formatDamageList(list, maxLevel, level, includeTypePrefix) | ||
if type(list) ~= "table" or #list == 0 then | if type(list) ~= "table" or #list == 0 then | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
for _, d in ipairs(list) do | for _, d in ipairs(list) do | ||
if type(d) == "table" then | if type(d) == "table" then | ||
local | local txt = formatDamageEntry(d, maxLevel, level) | ||
if txt then | |||
if includeTypePrefix and d.Type and d.Type ~= "" then | |||
table.insert(parts, mw.text.nowiki(tostring(d.Type) .. ": ") .. txt) | |||
else | |||
table.insert(parts, txt) | |||
end | |||
end | end | ||
end | end | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | |||
-- Scaling supports either: | |||
-- - single dict: {Percent=..., Scaling ID/Name...} | |||
-- - list of dicts: [{...},{...}] | |||
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 | ||
local parts = {} | local parts = {} | ||
for _, s in ipairs(list) do | for _, s in ipairs(list) do | ||
if type(s) == "table" then | if type(s) == "table" then | ||
local | local stat = s["Scaling Name"] or s["Scaling ID"] or "Unknown" | ||
local pct = s.Percent | local pct = s.Percent | ||
local | local pctN = toNum(pct) | ||
local | |||
if | local basis = basisOverride or basisLabel(s, false) | ||
if pctN ~= nil and pctN ~= 0 then | |||
table.insert(parts, string.format("%s%% %s Per %s", fmtNum(pctN), basis, stat)) | |||
elseif pct ~= nil and tostring(pct) ~= "" and tostring(pct) ~= "0" then | |||
table.insert(parts, string.format("%s%% %s Per %s", tostring(pct), basis, stat)) | |||
end | end | ||
end | end | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
local function formatArea(area, maxLevel, level) | |||
local function formatArea(area) | |||
if type(area) ~= "table" then | if type(area) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
local distLine = | local distLine = valuePairDynamicText("Distance", area["Area Distance"], maxLevel, level, "<br />") | ||
if distLine then | if distLine then | ||
table.insert(parts, distLine) | table.insert(parts, distLine) | ||
| Line 381: | Line 512: | ||
local size = area["Area Size"] | local size = area["Area Size"] | ||
if size and size ~= "" then | if size and size ~= "" then | ||
table.insert(parts, "Size: " .. tostring(size)) | table.insert(parts, "Size: " .. mw.text.nowiki(tostring(size))) | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
local function formatTimingBlock(bt) | local function formatTimingBlock(bt, maxLevel, level) | ||
if type(bt) ~= "table" then | if type(bt) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
| Line 401: | Line 530: | ||
return | return | ||
end | end | ||
local lines = | local lines = valuePairDynamicLines(label, block, maxLevel, level) | ||
for _, line in ipairs(lines) do | for _, line in ipairs(lines) do | ||
table.insert(parts, line) | table.insert(parts, line) | ||
| Line 408: | Line 537: | ||
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 | ||
table.insert(parts, "Effect Cast Time: " .. tostring(bt["Effect Cast Time"])) | table.insert(parts, "Effect Cast Time: " .. mw.text.nowiki(tostring(bt["Effect Cast Time"]))) | ||
end | end | ||
if bt["Damage Delay"] ~= nil then | if bt["Damage Delay"] ~= nil then | ||
table.insert(parts, "Damage Delay: " .. tostring(bt["Damage Delay"])) | table.insert(parts, "Damage Delay: " .. mw.text.nowiki(tostring(bt["Damage Delay"]))) | ||
end | end | ||
if bt["Effect Remove Delay"] ~= nil then | if bt["Effect Remove Delay"] ~= nil then | ||
table.insert(parts, "Effect Remove Delay: " .. tostring(bt["Effect Remove Delay"])) | table.insert(parts, "Effect Remove Delay: " .. mw.text.nowiki(tostring(bt["Effect Remove Delay"]))) | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
local function formatResourceCost(rc) | local function formatResourceCost(rc, maxLevel, level) | ||
if type(rc) ~= "table" then | if type(rc) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
local manaLines = | local manaLines = valuePairDynamicLines("MP", rc["Mana Cost"], maxLevel, level) | ||
for _, line in ipairs(manaLines) do | for _, line in ipairs(manaLines) do | ||
table.insert(parts, line) | table.insert(parts, line) | ||
end | end | ||
local hpLines = | local hpLines = valuePairDynamicLines("HP", rc["Health Cost"], maxLevel, level) | ||
for _, line in ipairs(hpLines) do | for _, line in ipairs(hpLines) do | ||
table.insert(parts, line) | table.insert(parts, line) | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 453: | Line 577: | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
if combo.Type then | if combo.Type then | ||
table.insert(parts, "Type: " .. tostring(combo.Type)) | table.insert(parts, "Type: " .. mw.text.nowiki(tostring(combo.Type))) | ||
end | end | ||
local durText = formatUnitValue(combo.Duration) | local durText = formatUnitValue(combo.Duration) | ||
if durText then | if durText then | ||
table.insert(parts, "Duration: " .. durText) | table.insert(parts, "Duration: " .. mw.text.nowiki(durText)) | ||
end | end | ||
if combo.Percent ~= nil then | if combo.Percent ~= nil then | ||
local pctText = formatUnitValue(combo.Percent) | local pctText = formatUnitValue(combo.Percent) | ||
if pctText then | if pctText then | ||
table.insert(parts, "Bonus: " .. pctText) | table.insert(parts, "Bonus: " .. mw.text.nowiki(pctText)) | ||
end | end | ||
end | end | ||
return (#parts > 0) and table.concat(parts, ", ") or nil | |||
end | end | ||
local function formatMechanicEffects(effects) | local function formatMechanicEffects(effects, maxLevel, level) | ||
if type(effects) ~= "table" then | if type(effects) ~= "table" then | ||
return nil | return nil | ||
| Line 490: | Line 611: | ||
local parts = {} | local parts = {} | ||
local function effectAmount(block) | |||
if type(block) ~= "table" then | |||
return nil | |||
end | |||
local per = block["Per Level"] | |||
if type(per) == "table" and #per > 0 then | |||
if isFlatList(per) then | |||
return mw.text.nowiki(formatUnitValue(per[1]) or tostring(per[1])) | |||
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 | |||
for _, name in ipairs(keys) do | for _, name in ipairs(keys) do | ||
local block = effects[name] | local block = effects[name] | ||
if type(block) == "table" then | if type(block) == "table" then | ||
- | local t = block.Type | ||
local txt = | |||
if t ~= nil and tostring(t) ~= "" then | |||
local amt = effectAmount(block) | |||
local seg = mw.text.nowiki(tostring(t) .. " - " .. tostring(name)) | |||
if amt then | |||
seg = seg .. " + " .. amt | |||
end | |||
table.insert(parts, seg) | |||
else | |||
local txt = valuePairDynamicText(name, block, maxLevel, level, ", ") | |||
if txt then | |||
table.insert(parts, txt) | |||
end | |||
end | end | ||
end | end | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 511: | Line 661: | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
| Line 517: | Line 668: | ||
return | return | ||
end | end | ||
local flags = {} | local flags = {} | ||
for k, v in pairs(sub) do | for k, v in pairs(sub) do | ||
| Line 524: | Line 676: | ||
end | end | ||
table.sort(flags) | table.sort(flags) | ||
if #flags > 0 then | if #flags > 0 then | ||
table.insert(parts, string.format("%s: %s", label, table.concat(flags, ", "))) | table.insert(parts, string.format("%s: %s", label, table.concat(flags, ", "))) | ||
| Line 533: | Line 686: | ||
collect("Special", mods["Special Modifiers"]) | collect("Special", mods["Special Modifiers"]) | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
local function formatStatusApplications(list) | local function formatStatusApplications(list, maxLevel, level) | ||
if type(list) ~= "table" or #list == 0 then | if type(list) ~= "table" or #list == 0 then | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
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 | |||
local t = valuePairDynamicText("Duration", s.Duration, maxLevel, level, "; ") | |||
local t = | if t then table.insert(detail, t) end | ||
if t then | |||
end | end | ||
if s | if type(s.Chance) == "table" then | ||
table.insert(detail, | local t = valuePairDynamicText("Chance", s.Chance, maxLevel, level, "; ") | ||
if t then table.insert(detail, t) end | |||
end | end | ||
| Line 579: | Line 720: | ||
end | end | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
local function formatStatusRemoval(list) | local function formatStatusRemoval(list, maxLevel, level) | ||
if type(list) ~= "table" or #list == 0 then | if type(list) ~= "table" or #list == 0 then | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
for _, r in ipairs(list) do | for _, r in ipairs(list) do | ||
| Line 594: | Line 734: | ||
local names = r["Status External Name"] | local names = r["Status External Name"] | ||
local label | local label | ||
if type(names) == "table" then | if type(names) == "table" then | ||
label = table.concat(names, ", ") | label = table.concat(names, ", ") | ||
| Line 602: | Line 743: | ||
end | end | ||
local amt = valuePairRawText(r) | local amt | ||
local seg = label | if type(r["Per Level"]) == "table" and #r["Per Level"] > 0 and not isFlatList(r["Per Level"]) then | ||
local series = {} | |||
for _, v in ipairs(r["Per Level"]) do | |||
table.insert(series, formatUnitValue(v) or tostring(v)) | |||
end | |||
amt = dynSpan(series, level) | |||
else | |||
amt = valuePairRawText(r) | |||
amt = amt and mw.text.nowiki(amt) or nil | |||
end | |||
local seg = mw.text.nowiki(label) | |||
if amt then | if amt then | ||
seg = seg .. " – " .. amt | seg = seg .. " – " .. amt | ||
| Line 610: | Line 762: | ||
end | end | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 620: | Line 770: | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
for _, ev in ipairs(list) do | for _, ev in ipairs(list) do | ||
| Line 625: | Line 776: | ||
local action = ev.Action or "On event" | local action = ev.Action or "On event" | ||
local name = ev["Skill Internal Name"] or ev["Skill External Name"] or "Unknown skill" | local name = ev["Skill Internal Name"] or ev["Skill External Name"] or "Unknown skill" | ||
table.insert(parts, string.format("%s → %s", action, name)) | |||
end | end | ||
end | end | ||
return (#parts > 0) and table.concat(parts, "<br />") or nil | |||
end | end | ||
| Line 663: | Line 811: | ||
end | end | ||
return listHas(users.Classes) or listHas(users.Summons) or listHas(users.Monsters) or listHas(users.Events) | |||
end | end | ||
| Line 681: | Line 824: | ||
local pageTitle = mw.title.getCurrentTitle() | local pageTitle = mw.title.getCurrentTitle() | ||
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 | ||
end | end | ||
pageName = mw.ustring.lower(pageName) | pageName = mw.ustring.lower(pageName) | ||
| Line 691: | Line 835: | ||
local internal = trim(rec["Internal Name"] or rec["InternalName"] or rec["InternalID"]) | 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 | |||
return | end | ||
---------------------------------------------------------------------- | |||
-- Top band UI | |||
---------------------------------------------------------------------- | |||
local function buildLevelSelectUI(level, maxLevel) | |||
local wrap = mw.html.create("div") | |||
wrap:addClass("sv-level-ui") | |||
-- Inline “light-mode-safe” panel styling (works in both modes) | |||
wrap:css("background", "rgba(90, 78, 124, 0.18)") | |||
wrap:css("border", "1px solid rgba(55, 43, 84, 0.22)") | |||
wrap:css("border-radius", "10px") | |||
wrap:tag("div") | |||
:addClass("sv-level-title") | |||
:wikitext("Level Select") | |||
wrap:tag("div") | |||
:addClass("sv-level-label") | |||
:wikitext("Level <span class=\"sv-level-num\">" .. tostring(level) .. "</span> / " .. tostring(maxLevel)) | |||
wrap:tag("div"):addClass("sv-level-slider") | |||
return tostring(wrap) | |||
end | |||
local function buildTypeTableUI(typeBlock) | |||
if type(typeBlock) ~= "table" or next(typeBlock) == nil then | |||
return nil | |||
end | end | ||
if | |||
return true | local wrap = mw.html.create("div") | ||
wrap:addClass("sv-type-grid") | |||
-- Inline “light-mode-safe” panel styling (works in both modes) | |||
wrap:css("background", "rgba(90, 78, 124, 0.18)") | |||
wrap:css("border", "1px solid rgba(55, 43, 84, 0.22)") | |||
wrap:css("border-radius", "10px") | |||
local added = false | |||
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 | |||
end | |||
if type(x) == "string" and x ~= "" then | |||
return x | |||
end | |||
return nil | |||
end | |||
local function addChunk(label, rawVal) | |||
local v = valName(rawVal) | |||
if not v or v == "" then | |||
return | |||
end | |||
added = true | |||
local chunk = wrap: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 | |||
-- NEW keys (with fallback to old keys) | |||
addChunk("Damage", typeBlock.Damage or typeBlock["Damage Type"]) | |||
addChunk("Element", typeBlock.Element or typeBlock["Element Type"]) | |||
addChunk("Target", typeBlock.Target or typeBlock["Target Type"]) | |||
addChunk("Cast", typeBlock.Cast or typeBlock["Cast Type"]) | |||
if not added then | |||
return nil | |||
end | |||
return tostring(wrap) | |||
end | |||
local function addTopBand(tbl, levelUI, typeUI) | |||
if not levelUI and not typeUI then | |||
return | |||
end | |||
local row = tbl:tag("tr") | |||
local cell = row:tag("td") | |||
cell:attr("colspan", 2) | |||
cell:addClass("sv-topband-cell") | |||
-- Keep nested table (your CSS targets it) | |||
local inner = cell:tag("table") | |||
inner:addClass("sv-topband-table") | |||
local tr = inner:tag("tr") | |||
if levelUI and typeUI then | |||
tr:tag("td"):wikitext(levelUI):done() | |||
tr:tag("td"):wikitext(typeUI):done() | |||
elseif levelUI then | |||
tr:tag("td"):attr("colspan", 2):wikitext(levelUI):done() | |||
else | |||
tr:tag("td"):attr("colspan", 2):wikitext(typeUI):done() | |||
end | end | ||
end | end | ||
| Line 707: | Line 951: | ||
opts = opts or {} | opts = opts or {} | ||
local showUsers = (opts.showUsers ~= false) | 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") | ||
root:addClass(" | root:addClass("spiritvale-skill-infobox") | ||
root:addClass("sv-skill-card") | |||
root:attr("data-max-level", tostring(maxLevel)) | |||
root:attr("data-level", tostring(level)) | |||
if opts.inList then | |||
root:addClass("sv-skill-inlist") | |||
end | |||
-- | -- Hero rows | ||
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 | local heroRow = root:tag("tr") | ||
heroRow:addClass("spiritvale-infobox-main") | |||
heroRow:addClass("sv-hero-title-row") | |||
local heroCell = heroRow:tag("th") | |||
local | heroCell:attr("colspan", 2) | ||
heroCell:addClass("sv-hero-title-cell") | |||
local | local heroInner = heroCell:tag("div") | ||
heroInner:addClass("spiritvale-infobox-main-left-inner") | |||
if icon and icon ~= "" then | if icon and icon ~= "" then | ||
heroInner:wikitext(string.format("[[File:%s|80px|link=]]", icon)) | |||
end | end | ||
heroInner:tag("div") | |||
:addClass("spiritvale-infobox-title") | :addClass("spiritvale-infobox-title") | ||
:wikitext(title) | :wikitext(title) | ||
if desc ~= "" then | |||
local descRow = root:tag("tr") | |||
descRow:addClass("spiritvale-infobox-main") | |||
descRow:addClass("sv-hero-desc-row") | |||
local descCell = descRow:tag("td") | |||
descCell:attr("colspan", 2) | |||
descCell:addClass("sv-hero-desc-cell") | |||
local descInner = descCell:tag("div") | |||
descInner:addClass("spiritvale-infobox-main-right-inner") | |||
descInner:tag("div") | |||
:addClass("spiritvale-infobox-description") | :addClass("spiritvale-infobox-description") | ||
:wikitext(string.format("''%s''", desc)) | :wikitext(string.format("''%s''", desc)) | ||
end | end | ||
-- | -- Unified top band | ||
addTopBand( | |||
root, | |||
buildLevelSelectUI(level, maxLevel), | |||
buildTypeTableUI(rec.Type or {}) | |||
) | |||
-- | -- Users | ||
if showUsers then | if showUsers then | ||
local users = rec.Users or {} | local users = rec.Users or {} | ||
| Line 764: | Line 1,023: | ||
end | end | ||
-- Requirements | -- Requirements | ||
local req = rec.Requirements or {} | local req = rec.Requirements or {} | ||
if (req["Required Skills"] and #req["Required Skills"] > 0) | if (req["Required Skills"] and #req["Required Skills"] > 0) | ||
or (req["Required Weapons"] and #req["Required Weapons"] > 0) | or (req["Required Weapons"] and #req["Required Weapons"] > 0) | ||
or (req["Required Stances"] and #req["Required Stances"] > 0) then | or (req["Required Stances"] and #req["Required Stances"] > 0) 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 = {} | ||
for _, rs in ipairs(req["Required Skills"]) do | for _, rs in ipairs(req["Required Skills"]) do | ||
local | local nameReq = rs["Skill External Name"] or rs["Skill Internal Name"] or "Unknown" | ||
local | local lvlReq = rs["Required Level"] | ||
if | if lvlReq then | ||
table.insert(skillParts, string.format("%s (Lv.%s)", | table.insert(skillParts, string.format("%s (Lv.%s)", nameReq, lvlReq)) | ||
else | else | ||
table.insert(skillParts, | table.insert(skillParts, nameReq) | ||
end | end | ||
end | end | ||
addRow(root, "Required | addRow(root, "Required Skills", table.concat(skillParts, ", ")) | ||
end | end | ||
addRow(root, "Required | addRow(root, "Required Weapons", listToText(req["Required Weapons"])) | ||
addRow(root, "Required | addRow(root, "Required Stances", listToText(req["Required Stances"])) | ||
end | end | ||
-- Mechanics | -- Mechanics | ||
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, "Area", formatArea(mech.Area, maxLevel, level)) | |||
addRow(root, " | |||
if mech["Autocast Multiplier"] ~= nil then | if mech["Autocast Multiplier"] ~= nil then | ||
addRow(root, "Autocast | addRow(root, "Autocast Multiplier", tostring(mech["Autocast Multiplier"])) | ||
end | end | ||
addRow(root, "Timing", formatTimingBlock(mech["Basic Timings"], maxLevel, level)) | |||
addRow(root, " | addRow(root, "Resource Cost", formatResourceCost(mech["Resource Cost"], maxLevel, level)) | ||
addRow(root, "Combo", formatCombo(mech.Combo)) | |||
addRow(root, "Special Mechanics", formatMechanicEffects(mech.Effects, maxLevel, level)) | |||
end | |||
-- Source (new) + Scaling, with backcompat for old Damage | |||
addRow(root, " | if type(rec.Source) == "table" then | ||
addRow(root, "Source", formatSource(rec.Source, maxLevel, level)) | |||
local | local basisOverride = | ||
(rec.Source.Type == "Healing" or rec.Source.Healing == true) and "Healing" or nil | |||
local | addRow(root, "Scaling", formatScaling(rec.Source.Scaling, basisOverride)) | ||
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 | |||
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))) | |||
addRow(root, "Flat Damage", formatDamageList(flatList, maxLevel, level, false)) | |||
addRow(root, "Reflect Damage", formatDamageList(reflList, maxLevel, level, false)) | |||
addRow(root, "Healing", formatDamageList(healOnly, maxLevel, level, false)) | |||
addRow(root, "Scaling", formatScaling(dmg.Scaling, pureHealing and "Healing" or nil)) | |||
end | |||
end | end | ||
-- Modifiers | -- Modifiers | ||
local modsText = formatModifiers(rec.Modifiers) | local modsText = formatModifiers(rec.Modifiers) | ||
if modsText then | if modsText then | ||
addRow(root, "Flags", modsText) | addRow(root, "Flags", modsText) | ||
end | end | ||
-- Status | -- Status | ||
local statusApps = formatStatusApplications(rec["Status Applications"], maxLevel, level) | |||
local statusApps = formatStatusApplications(rec["Status Applications"]) | local statusRem = formatStatusRemoval(rec["Status Removal"], maxLevel, level) | ||
local statusRem = formatStatusRemoval(rec["Status Removal"]) | |||
if statusApps or statusRem then | if statusApps or statusRem then | ||
addRow(root, "Applies", statusApps) | addRow(root, "Applies", statusApps) | ||
addRow(root, "Removes", statusRem) | addRow(root, "Removes", statusRem) | ||
end | end | ||
-- Events | -- Events | ||
local eventsText = formatEvents(rec.Events) | local eventsText = formatEvents(rec.Events) | ||
if eventsText then | if eventsText then | ||
addRow(root, "Triggers", eventsText) | addRow(root, "Triggers", eventsText) | ||
end | end | ||
-- Notes | -- 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 />")) | addRow(root, "Notes", table.concat(rec.Notes, "<br />")) | ||
end | end | ||
| Line 918: | Line 1,139: | ||
local args = getArgs(frame) | local args = getArgs(frame) | ||
local userName = args.user or args[1] | local userName = args.user or args[1] | ||
if not userName or userName == "" then | if not userName or userName == "" then | ||
| Line 945: | Line 1,165: | ||
local root = mw.html.create("div") | local root = mw.html.create("div") | ||
root:addClass(" | root:addClass("sv-skill-collection") | ||
for _, rec in ipairs(matches) do | for _, rec in ipairs(matches) do | ||
root:wikitext(buildInfobox(rec, { showUsers = true })) | local item = root:tag("div"):addClass("sv-skill-item") | ||
item:wikitext(buildInfobox(rec, { showUsers = false, inList = true })) | |||
end | end | ||
| Line 961: | Line 1,182: | ||
local args = getArgs(frame) | local args = getArgs(frame) | ||
local raw1 = args[1] | local raw1 = args[1] | ||
local name = args.name or raw1 | local name = args.name or raw1 | ||
| Line 970: | Line 1,187: | ||
local rec | local rec | ||
if name and name ~= "" then | if name and name ~= "" then | ||
rec = findSkillByName(name) | rec = findSkillByName(name) | ||
end | end | ||
if not rec and id and id ~= "" then | if not rec and id and id ~= "" then | ||
rec = getSkillById(id) | rec = getSkillById(id) | ||
end | end | ||
if not rec then | if not rec then | ||
local pageTitle = mw.title.getCurrentTitle() | local pageTitle = mw.title.getCurrentTitle() | ||
| Line 991: | Line 1,203: | ||
(not id or id == "") | (not id or id == "") | ||
if noExplicitArgs then | if noExplicitArgs then | ||
return p.listForUser(frame) | return p.listForUser(frame) | ||
end | end | ||
if name and name ~= "" and name == pageName and (not id or id == "") then | if name and name ~= "" and name == pageName and (not id or id == "") then | ||
return p.listForUser(frame) | return p.listForUser(frame) | ||
end | end | ||
local label = name or id or "?" | local label = name or id or "?" | ||
return string.format( | return string.format( | ||
"<strong>Unknown | "<strong>Unknown Skill:</strong> %s[[Category:Pages with unknown skill|%s]]", | ||
mw.text.nowiki(label), | mw.text.nowiki(label), | ||
label | label | ||
| Line 1,010: | Line 1,219: | ||
end | end | ||
local showUsers = not isDirectSkillPage(rec) | local showUsers = not isDirectSkillPage(rec) | ||
return buildInfobox(rec, { showUsers = showUsers }) | return buildInfobox(rec, { showUsers = showUsers }) | ||