Module:GameSkills: Difference between revisions
From SpiritVale Wiki
More actions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
--Module: GameSkills | -- Module:GameSkills | ||
-- | -- | ||
--Upgrades: | -- Upgrades: | ||
-- - Adds a per - skill Level Select slider(client - side JS updates fields) | -- - Adds a per-skill Level Select slider (client-side JS updates fields) | ||
-- - Default level = Max Level | -- - Default level = Max Level | ||
-- - Adds.sv - skill - card + data - max - level / data - level hooks for JS | -- - Adds .sv-skill-card + data-max-level / data-level hooks for JS | ||
-- - Replaces large Lv1 / Lv2 /... lists with data - series dynamic spans | -- - Replaces large Lv1/Lv2/... lists with data-series dynamic spans | ||
-- - Removes "General" + "Type" section bars and merges Level Select + Type into one unified top band | -- - Removes "General" + "Type" section bars and merges Level Select + Type into one unified top band | ||
--(Level Select on the left; Type / Element / Target / Cast Type | -- (Level Select on the left; Type/Element/Target/Cast Type on the right) | ||
-- - NEW(list - mode): Wraps all skills in ONE wrapper panel and each skill in a stable.sv - skill - item | -- - NEW (list-mode): Wraps all skills in ONE wrapper panel and each skill in a stable .sv-skill-item | ||
--so CSS dividers work reliably under Citizen / table wrappers. | -- so CSS dividers work reliably under Citizen/table wrappers. | ||
-- | -- | ||
--Requires the JS you installed in MediaWiki: Common.js. | -- Requires the JS you installed in MediaWiki:Common.js. | ||
--Uses Common.css classes: | -- Uses Common.css classes: | ||
-- .sv - topband - cell, .sv - topband - table, .sv - type - | -- .sv-topband-cell, .sv-topband-table, .sv-type-grid, .sv-type-chunk, .sv-type-label, .sv-type-value | ||
-- .sv - skill - collection, .sv - skill - item | -- .sv-level-ui, .sv-level-title, .sv-level-label, .sv-level-slider | ||
-- .sv-skill-collection, .sv-skill-item | |||
local GameData = require("Module:GameData") | local GameData = require("Module:GameData") | ||
| Line 21: | Line 22: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
--Internal helpers | -- Internal helpers | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local skillsCache | |||
local function getSkills() | local function getSkills() | ||
if not skillsCache then | if not skillsCache then | ||
skillsCache = GameData.loadSkills() | skillsCache = GameData.loadSkills() | ||
end | end | ||
return skillsCache | return skillsCache | ||
end | end | ||
local function getArgs(frame) | local function getArgs(frame) | ||
local parent = frame: getParent() | local parent = frame:getParent() | ||
if parent then | if parent then | ||
return parent.args | return parent.args | ||
end | end | ||
return frame.args | return frame.args | ||
end | end | ||
local function listToText(list, sep) | local function listToText(list, sep) | ||
if type(list) ~= "table" or #list == 0 then | if type(list) ~= "table" or #list == 0 then | ||
return nil | return nil | ||
end | end | ||
return table.concat(list, sep or ", ") | return table.concat(list, sep or ", ") | ||
end | end | ||
local function addRow(tbl, label, value) | local function addRow(tbl, label, value) | ||
if value == nil or value == "" then | if value == nil or value == "" then | ||
return | return | ||
end | end | ||
local row = tbl: tag("tr") | local row = tbl:tag("tr") | ||
row: tag("th"): wikitext(label): done() | row:tag("th"):wikitext(label):done() | ||
row: tag("td"): wikitext(value): done() | row:tag("td"):wikitext(value):done() | ||
end | end | ||
local function addSectionHeader(tbl, label) | local function addSectionHeader(tbl, label) | ||
local row = tbl: tag("tr") | local row = tbl:tag("tr") | ||
local cell = row: tag("th") | local cell = row:tag("th") | ||
cell: attr("colspan", 2) | cell:attr("colspan", 2) | ||
cell: addClass("spiritvale-infobox-section-header") | cell:addClass("spiritvale-infobox-section-header") | ||
cell: wikitext(label) | cell:wikitext(label) | ||
end | end | ||
local function trim(s) | local function trim(s) | ||
if type(s) ~= "string" then return nil end | if type(s) ~= "string" then | ||
s = mw.text.trim(s) | return nil | ||
if s == "" then return nil end | end | ||
return s | s = mw.text.trim(s) | ||
if s == "" then | |||
return nil | |||
end | |||
return s | |||
end | end | ||
local function toNum(v) | local function toNum(v) | ||
if type(v) == "number" then return v end | if type(v) == "number" then | ||
if type(v) == "string" then return tonumber(v) end | return v | ||
if type(v) == "table" and v.Value ~= nil then | end | ||
return toNum(v.Value) | if type(v) == "string" then | ||
end | return tonumber(v) | ||
return nil | end | ||
if type(v) == "table" and v.Value ~= nil then | |||
return toNum(v.Value) | |||
end | |||
return nil | |||
end | end | ||
local function clamp(n, lo, hi) | local function clamp(n, lo, hi) | ||
if type(n) ~= "number" then return lo end | if type(n) ~= "number" then | ||
if n < lo then return lo end | return lo | ||
if n > hi then return hi end | end | ||
return n | if n < lo then | ||
return lo | |||
end | |||
if n > hi then | |||
return hi | |||
end | |||
return n | |||
end | end | ||
local function fmtNum(n) | local function fmtNum(n) | ||
if type(n) ~= "number" then | if type(n) ~= "number" then | ||
return (n ~= nil) and tostring(n) or nil | return (n ~= nil) and tostring(n) or nil | ||
end | end | ||
if math.abs(n - math.floor(n)) < 1e-9 then | |||
return tostring(math.floor(n)) | if math.abs(n - math.floor(n)) < 1e-9 then | ||
end | return tostring(math.floor(n)) | ||
end | |||
local s = string.format("%.4f", n) | local s = string.format("%.4f", n) | ||
s = mw.ustring.gsub(s, "0+$", "") | s = mw.ustring.gsub(s, "0+$", "") | ||
s = mw.ustring.gsub(s, "%.$", "") | s = mw.ustring.gsub(s, "%.$", "") | ||
return s | return s | ||
end | end | ||
--Handles either a scalar OR { Value =..., Unit =... } | -- Handles either a scalar OR { Value = ..., Unit = ... } | ||
--IMPORTANT: do NOT convert percent_decimal / percent_whole; just format around stored values. | -- IMPORTANT: do NOT convert percent_decimal / percent_whole; just format around stored values. | ||
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" or unit == "percent_whole" or unit == "percent" then | ||
return tostring(val).. "%" | return tostring(val) .. "%" | ||
elseif unit == "seconds" then | elseif unit == "seconds" then | ||
return tostring(val).. "s" | return tostring(val) .. "s" | ||
elseif unit == "meters" then | elseif unit == "meters" then | ||
return tostring(val).. "m" | return tostring(val) .. "m" | ||
elseif unit == "tiles" then | elseif unit == "tiles" then | ||
return tostring(val).. " tiles" | return tostring(val) .. " tiles" | ||
elseif unit and unit ~= "" then | elseif unit and unit ~= "" then | ||
return tostring(val).. " "..tostring(unit) | return tostring(val) .. " " .. tostring(unit) | ||
else | else | ||
return tostring(val) | return tostring(val) | ||
end | end | ||
end | end | ||
if v == nil then | if v == nil then | ||
return nil | return nil | ||
end | end | ||
return tostring(v) | return tostring(v) | ||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
--Dynamic field helpers(JS - driven) | -- Dynamic field helpers (JS-driven) | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local function dynSpan(series, level) | |||
if type(series) ~= "table" or #series == 0 then | if type(series) ~= "table" or #series == 0 then | ||
return nil | return nil | ||
end | end | ||
level = clamp(level or #series, 1, #series) | |||
level = clamp(level or #series, 1, #series) | |||
local span = mw.html.create("span") | local span = mw.html.create("span") | ||
span: addClass("sv-dyn") | span:addClass("sv-dyn") | ||
span: attr("data-series", mw.text.jsonEncode(series)) | span:attr("data-series", mw.text.jsonEncode(series)) | ||
span: wikitext(mw.text.nowiki(series[level] or "")) | span:wikitext(mw.text.nowiki(series[level] or "")) | ||
return tostring(span) | return tostring(span) | ||
end | end | ||
local function isFlatList(list) | local function isFlatList(list) | ||
if type(list) ~= "table" or #list == 0 then | if type(list) ~= "table" or #list == 0 then | ||
return false | return false | ||
end | end | ||
local first = tostring(list[1]) | local first = tostring(list[1]) | ||
for i = 2, #list do | for i = 2, #list do | ||
if tostring(list[i]) ~= first then | |||
return false | return false | ||
end | end | ||
end | end | ||
return true | return true | ||
end | end | ||
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 | ||
if type(v) == "string" then | end | ||
if type(v) == "number" then | |||
return v ~= 0 | |||
end | |||
if type(v) == "string" then | |||
local n = tonumber(v) | local n = tonumber(v) | ||
if n == nil then | if n == nil then | ||
return v ~= "" | return v ~= "" | ||
end | end | ||
return n ~= 0 | return n ~= 0 | ||
end | end | ||
if type(v) == "table" and v.Value ~= nil then | if type(v) == "table" and v.Value ~= nil then | ||
return isNonZeroScalar(v.Value) | return isNonZeroScalar(v.Value) | ||
end | end | ||
return true | return true | ||
end | end | ||
--Like your old valuePairLines / valuePairText, but: | -- Like your old valuePairLines/valuePairText, but: | ||
-- - if Per Level is a list, render a dynamic span instead of "v1 / v2 / ..." | -- - if Per Level is a list, render a dynamic span instead of "v1 / v2 / ..." | ||
-- - scalar Per Level stays as the old "Base" + "Per Level" lines(still short) | -- - scalar Per Level stays as the old "Base" + "Per Level" lines (still short) | ||
local function valuePairDynamicLines(name, block, maxLevel, level) | local function valuePairDynamicLines(name, block, maxLevel, level) | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
return {} | return {} | ||
end | end | ||
local base = block.Base | local base = block.Base | ||
local per = block["Per Level"] | local per = block["Per Level"] | ||
--Per Level list(expanded) | -- Per Level list (expanded) | ||
if type(per) == "table" then | if type(per) == "table" then | ||
--empty list -> show Base only | -- empty list -> show Base only | ||
if #per == 0 then | if #per == 0 then | ||
local baseText = formatUnitValue(base) | local baseText = formatUnitValue(base) | ||
if baseText then | if baseText then | ||
return { string.format("%s: %s", name, mw.text.nowiki(baseText)) } | return { string.format("%s: %s", name, mw.text.nowiki(baseText)) } | ||
end | end | ||
return {} | return {} | ||
end | end | ||
--flat list -> show single value | -- flat list -> show single value | ||
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 | ||
if show then | if show then | ||
return { string.format("%s: %s", name, mw.text.nowiki(show)) } | return { string.format("%s: %s", name, mw.text.nowiki(show)) } | ||
end | end | ||
return {} | return {} | ||
end | end | ||
--dynamic series | -- dynamic series | ||
local series = {} | local series = {} | ||
for _, v in ipairs(per) do | for _, v in ipairs(per) do | ||
table.insert(series, formatUnitValue(v) or tostring(v)) | |||
end | end | ||
local dyn = dynSpan(series, level) | local dyn = dynSpan(series, level) | ||
if dyn then | if dyn then | ||
return { string.format("%s: %s", name, dyn) } | return { string.format("%s: %s", name, dyn) } | ||
end | end | ||
return {} | return {} | ||
end | end | ||
--scalar Per Level(keep old style) | -- scalar Per Level (keep old style) | ||
local lines = {} | local lines = {} | ||
local baseText = formatUnitValue(base) | local baseText = formatUnitValue(base) | ||
local perText = formatUnitValue(per) | local perText = formatUnitValue(per) | ||
if baseText then | if baseText then | ||
table.insert(lines, string.format("%s: %s", name, mw.text.nowiki(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, mw.text.nowiki(perText))) | table.insert(lines, string.format("%s Per Level: %s", name, mw.text.nowiki(perText))) | ||
end | end | ||
return lines | return lines | ||
end | end | ||
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) | ||
if #lines == 0 then return nil end | if #lines == 0 then | ||
return table.concat(lines, sep or "<br />") | return nil | ||
end | |||
return table.concat(lines, sep or "<br />") | |||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
--Lookups | -- Lookups | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local function getSkillById(id) | |||
id = trim(id) | id = trim(id) | ||
if not id then return nil end | if not id then | ||
return nil | |||
end | |||
local dataset = getSkills() | local dataset = getSkills() | ||
local byId = dataset.byId or { } | local byId = dataset.byId or {} | ||
return byId[id] | return byId[id] | ||
end | end | ||
local function findSkillByName(name) | local function findSkillByName(name) | ||
name = trim(name) | name = trim(name) | ||
if not name then return nil end | if not name then | ||
return nil | |||
end | |||
local dataset = getSkills() | local dataset = getSkills() | ||
local byName = dataset.byName or {} | |||
if byName[name] then | |||
if byName[name] then | return byName[name] | ||
return byName[name] | end | ||
end | |||
for _, rec in ipairs(dataset.records or {}) do | 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 | if rec["External Name"] == name or rec["Name"] == name or rec["Display Name"] == name then | ||
return rec | return rec | ||
end | end | ||
end | end | ||
end | end | ||
return nil | |||
return nil | |||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
--Formatting helpers | -- Formatting helpers | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local function basisLabel(entry, isHealing) | |||
if isHealing then | if isHealing then | ||
return "Healing" | return "Healing" | ||
end | end | ||
local atk = entry and entry["ATK-Based"] | |||
local atk = entry and entry["ATK-Based"] | |||
local matk = entry and entry["MATK-Based"] | local matk = entry and entry["MATK-Based"] | ||
if atk and matk then | |||
return "Attack/Magic Attack" | if atk and matk then | ||
return "Attack/Magic Attack" | |||
elseif atk then | elseif atk then | ||
return "Attack" | return "Attack" | ||
elseif matk then | elseif matk then | ||
return "Magic Attack" | return "Magic Attack" | ||
end | end | ||
return "Damage" | |||
return "Damage" | |||
end | end | ||
--Dynamic damage entry: | -- Dynamic damage entry: | ||
--Build a series for Lv1..LvMax, and show only the selected one. | -- Build a series for Lv1..LvMax, and show only the selected one. | ||
local function formatDamageEntry(entry, maxLevel, level) | local function formatDamageEntry(entry, maxLevel, level) | ||
if type(entry) ~= "table" then | if type(entry) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local isHealing = (entry.Type == "Healing") | local isHealing = (entry.Type == "Healing") | ||
| Line 306: | Line 339: | ||
local baseRaw = entry["Base %"] | local baseRaw = entry["Base %"] | ||
local perRaw = entry["Per Level %"] | local perRaw = entry["Per Level %"] | ||
local baseN = toNum(baseRaw) | local baseN = toNum(baseRaw) | ||
local perN = toNum(perRaw) | local perN = toNum(perRaw) | ||
local function baseIsPresent() | local function baseIsPresent() | ||
if baseN ~= nil then | if baseN ~= nil then | ||
return baseN ~= 0 | return baseN ~= 0 | ||
end | end | ||
if baseRaw ~= nil then | if baseRaw ~= nil then | ||
local s = tostring(baseRaw) | local s = tostring(baseRaw) | ||
return (s ~= "" and s ~= "0" and s ~= "0.0" and s ~= "0.00") | return (s ~= "" and s ~= "0" and s ~= "0.0" and s ~= "0.00") | ||
end | end | ||
return false | return false | ||
end | end | ||
local baseText = nil | local baseText = nil | ||
if baseIsPresent() then | if baseIsPresent() then | ||
if baseN ~= nil then | if baseN ~= nil then | ||
baseText = fmtNum(baseN).. "%" | baseText = fmtNum(baseN) .. "%" | ||
else | else | ||
baseText = tostring(baseRaw).. "%" | baseText = tostring(baseRaw) .. "%" | ||
end | end | ||
end | end | ||
--No scaling -> just show base | -- No scaling -> just show base | ||
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 | ||
--Build series strings for each level | -- Build series strings for each level | ||
local series = {} | local series = {} | ||
for lv = 1, maxLevel do | for lv = 1, maxLevel do | ||
local perPart = perN * lv | local perPart = perN * lv | ||
if baseText and baseN ~= nil then | if baseText and baseN ~= nil then | ||
local total = baseN + perPart | local total = baseN + perPart | ||
table.insert(series, string.format("%s%% %s", fmtNum(total), basis)) | table.insert(series, string.format("%s%% %s", fmtNum(total), basis)) | ||
elseif baseText then | elseif baseText then | ||
table.insert(series, string.format("%s + %s%% %s", baseText, fmtNum(perPart), basis)) | table.insert(series, string.format("%s + %s%% %s", baseText, fmtNum(perPart), basis)) | ||
else | else | ||
table.insert(series, string.format("%s%% %s", fmtNum(perPart), basis)) | table.insert(series, string.format("%s%% %s", fmtNum(perPart), basis)) | ||
end | end | ||
end | end | ||
return dynSpan(series, level) | return dynSpan(series, level) | ||
end | end | ||
local function formatDamageList(list, maxLevel, level, includeTypePrefix) | local function formatDamageList(list, maxLevel, level, includeTypePrefix) | ||
if type(list) ~= "table" or #list == 0 then | if type(list) ~= "table" or #list == 0 then | ||
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 | |||
local txt = formatDamageEntry(d, maxLevel, level) | local txt = formatDamageEntry(d, maxLevel, level) | ||
if txt then | if txt then | ||
if includeTypePrefix and d.Type and d.Type ~= "" then | if includeTypePrefix and d.Type and d.Type ~= "" then | ||
table.insert(parts, mw.text.nowiki(tostring(d.Type).. ": ")..txt) | table.insert(parts, mw.text.nowiki(tostring(d.Type) .. ": ") .. txt) | ||
else | else | ||
table.insert(parts, txt) | table.insert(parts, txt) | ||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
if #parts == 0 then | if #parts == 0 then | ||
return nil | return nil | ||
end | end | ||
return table.concat(parts, "<br />") | return table.concat(parts, "<br />") | ||
end | end | ||
local function formatScaling(list, basisOverride) | local function formatScaling(list, basisOverride) | ||
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 | |||
local stat = s["Scaling Name"] or s["Scaling ID"] or "Unknown" | local stat = s["Scaling Name"] or s["Scaling ID"] or "Unknown" | ||
local pct = s.Percent | local pct = s.Percent | ||
local pctN = toNum(pct) | local pctN = toNum(pct) | ||
local basis = basisOverride or basisLabel(s, false) | local basis = basisOverride or basisLabel(s, false) | ||
if pctN ~= nil and pctN ~= 0 then | if pctN ~= nil and pctN ~= 0 then | ||
table.insert(parts, string.format("%s%% %s Per %s", fmtNum(pctN), basis, stat)) | table.insert(parts, string.format("%s%% %s Per %s", fmtNum(pctN), basis, stat)) | ||
elseif pct ~= nil and tostring(pct) ~= "" and tostring(pct) ~= "0" then | elseif pct ~= nil and tostring(pct) ~= "" and tostring(pct) ~= "0" then | ||
table.insert(parts, string.format("%s%% %s Per %s", tostring(pct), basis, stat)) | table.insert(parts, string.format("%s%% %s Per %s", tostring(pct), basis, stat)) | ||
end | end | ||
end | end | ||
end | end | ||
if #parts == 0 then | if #parts == 0 then | ||
return nil | return nil | ||
end | end | ||
return table.concat(parts, "<br />") | return table.concat(parts, "<br />") | ||
end | end | ||
local function formatArea(area, maxLevel, level) | local function formatArea(area, maxLevel, level) | ||
if type(area) ~= "table" then | if type(area) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
local distLine = valuePairDynamicText("Distance", area["Area Distance"], maxLevel, level, "<br />") | local distLine = valuePairDynamicText("Distance", area["Area Distance"], maxLevel, level, "<br />") | ||
if distLine then | if distLine then | ||
table.insert(parts, distLine) | table.insert(parts, distLine) | ||
end | end | ||
local size = area["Area Size"] | local size = area["Area Size"] | ||
if size and size ~= "" then | if size and size ~= "" then | ||
table.insert(parts, "Size: "..mw.text.nowiki(tostring(size))) | table.insert(parts, "Size: " .. mw.text.nowiki(tostring(size))) | ||
end | end | ||
if #parts == 0 then | if #parts == 0 then | ||
return nil | return nil | ||
end | end | ||
return table.concat(parts, "<br />") | return table.concat(parts, "<br />") | ||
end | end | ||
local function formatTimingBlock(bt, maxLevel, level) | 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 = {} | ||
local function add(label, key) | local function add(label, key) | ||
local block = bt[key] | local block = bt[key] | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
return | return | ||
end | end | ||
local lines = valuePairDynamicLines(label, block, maxLevel, level) | local lines = valuePairDynamicLines(label, block, maxLevel, level) | ||
for _, line in ipairs(lines) do | for _, line in ipairs(lines) do | ||
table.insert(parts, line) | |||
end | end | ||
end | end | ||
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: "..mw.text.nowiki(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: "..mw.text.nowiki(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: "..mw.text.nowiki(tostring(bt["Effect Remove Delay"]))) | table.insert(parts, "Effect Remove Delay: " .. mw.text.nowiki(tostring(bt["Effect Remove Delay"]))) | ||
end | end | ||
if #parts == 0 then | if #parts == 0 then | ||
return nil | return nil | ||
end | end | ||
return table.concat(parts, "<br />") | return table.concat(parts, "<br />") | ||
end | end | ||
local function formatResourceCost(rc, maxLevel, level) | 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 = valuePairDynamicLines("MP", rc["Mana Cost"], maxLevel, level) | 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) | |||
end | end | ||
local hpLines = valuePairDynamicLines("HP", rc["Health Cost"], maxLevel, level) | 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) | |||
end | end | ||
if #parts == 0 then | if #parts == 0 then | ||
return nil | return nil | ||
end | end | ||
return table.concat(parts, "<br />") | return table.concat(parts, "<br />") | ||
end | end | ||
local function formatCombo(combo) | local function formatCombo(combo) | ||
if type(combo) ~= "table" then | if type(combo) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
if combo.Type then | if combo.Type then | ||
table.insert(parts, "Type: "..mw.text.nowiki(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: "..mw.text.nowiki(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: "..mw.text.nowiki(pctText)) | table.insert(parts, "Bonus: " .. mw.text.nowiki(pctText)) | ||
end | end | ||
end | end | ||
if #parts == 0 then | if #parts == 0 then | ||
return nil | return nil | ||
end | end | ||
return table.concat(parts, ", ") | return table.concat(parts, ", ") | ||
end | end | ||
local function valuePairRawText(block) | local function valuePairRawText(block) | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local base = block.Base | local base = block.Base | ||
local per = block["Per Level"] | 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 = {} | local vals = {} | ||
for _, v in ipairs(per) do | for _, v in ipairs(per) do | ||
table.insert(vals, formatUnitValue(v) or tostring(v)) | |||
end | end | ||
return (#vals > 0) and table.concat(vals, " / ") or nil | return (#vals > 0) and table.concat(vals, " / ") or nil | ||
end | end | ||
local baseText = formatUnitValue(base) | local baseText = formatUnitValue(base) | ||
local perText = formatUnitValue(per) | local perText = formatUnitValue(per) | ||
if baseText and perText and isNonZeroScalar(per) then | if baseText and perText and isNonZeroScalar(per) then | ||
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 | ||
--Special Mechanics supports dynamic Per Level lists | -- Special Mechanics supports dynamic Per Level lists | ||
local function formatMechanicEffects(effects, maxLevel, level) | local function formatMechanicEffects(effects, maxLevel, level) | ||
if type(effects) ~= "table" then | if type(effects) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local keys = {} | local keys = {} | ||
for k, _ in pairs(effects) do | for k, _ in pairs(effects) do | ||
table.insert(keys, k) | |||
end | end | ||
table.sort(keys) | table.sort(keys) | ||
| Line 562: | Line 600: | ||
local function effectAmount(block) | local function effectAmount(block) | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local per = block["Per Level"] | local per = block["Per Level"] | ||
if type(per) == "table" and #per > 0 then | |||
if type(per) == "table" and #per > 0 then | if isFlatList(per) then | ||
if isFlatList(per) then | return mw.text.nowiki(formatUnitValue(per[1]) or tostring(per[1])) | ||
return mw.text.nowiki(formatUnitValue(per[1]) or tostring(per[1])) | end | ||
end | |||
local series = {} | local series = {} | ||
for _, v in ipairs(per) do | for _, v in ipairs(per) do | ||
table.insert(series, formatUnitValue(v) or tostring(v)) | |||
end | end | ||
return dynSpan(series, level) | return dynSpan(series, level) | ||
end | end | ||
local pair = { Base = block.Base, ["Per Level"] = block["Per Level"] } | local pair = { Base = block.Base, ["Per Level"] = block["Per Level"] } | ||
local txt = valuePairRawText(pair) | local txt = valuePairRawText(pair) | ||
return txt and mw.text.nowiki(txt) or nil | return txt and mw.text.nowiki(txt) or nil | ||
end | 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 t = block.Type | ||
if t ~= nil and tostring(t) ~= "" then | if t ~= nil and tostring(t) ~= "" then | ||
local amt = effectAmount(block) | local amt = effectAmount(block) | ||
local seg = mw.text.nowiki(tostring(t).. " - "..tostring(name)) | local seg = mw.text.nowiki(tostring(t) .. " - " .. tostring(name)) | ||
if amt then | if amt then | ||
seg = seg.. " + "..amt | seg = seg .. " + " .. amt | ||
end | end | ||
table.insert(parts, seg) | table.insert(parts, seg) | ||
else | else | ||
local txt = valuePairDynamicText(name, block, maxLevel, level, ", ") | local txt = valuePairDynamicText(name, block, maxLevel, level, ", ") | ||
if txt then | if txt then | ||
table.insert(parts, txt) | table.insert(parts, txt) | ||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
if #parts == 0 then | if #parts == 0 then | ||
return nil | return nil | ||
end | end | ||
return table.concat(parts, "<br />") | return table.concat(parts, "<br />") | ||
end | end | ||
local function formatModifiers(mods) | local function formatModifiers(mods) | ||
if type(mods) ~= "table" then | if type(mods) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
local parts = {} | local parts = {} | ||
local function collect(label, sub) | local function collect(label, sub) | ||
if type(sub) ~= "table" then | if type(sub) ~= "table" then | ||
return | return | ||
end | end | ||
local flags = {} | local flags = {} | ||
for k, v in pairs(sub) do | for k, v in pairs(sub) do | ||
if v then | |||
table.insert(flags, k) | table.insert(flags, k) | ||
end | end | ||
end | end | ||
table.sort(flags) | table.sort(flags) | ||
if #flags > 0 then | |||
table.insert(parts, string.format("%s: %s", label, table.concat(flags, ", "))) | if #flags > 0 then | ||
end | table.insert(parts, string.format("%s: %s", label, table.concat(flags, ", "))) | ||
end | end | ||
end | |||
collect("Movement", mods["Movement Modifiers"]) | collect("Movement", mods["Movement Modifiers"]) | ||
collect("Combat", mods["Combat Modifiers"]) | collect("Combat", mods["Combat Modifiers"]) | ||
collect("Special", mods["Special Modifiers"]) | collect("Special", mods["Special Modifiers"]) | ||
if #parts == 0 then | if #parts == 0 then | ||
return nil | return nil | ||
end | end | ||
return table.concat(parts, "<br />") | return table.concat(parts, "<br />") | ||
end | end | ||
local function formatStatusApplications(list, maxLevel, level) | 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 | |||
local scope = s.Scope or "Target" | local scope = s.Scope or "Target" | ||
local name = s["Status External Name"] or s["Status Internal Name"] or "Unknown status" | local name = s["Status External Name"] or s["Status Internal Name"] or "Unknown status" | ||
local seg = scope.. " – "..tostring(name) | local seg = scope .. " – " .. 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 table.insert(detail, t) end | if t then | ||
end | table.insert(detail, t) | ||
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 table.insert(detail, t) end | if t then | ||
end | table.insert(detail, t) | ||
end | |||
end | |||
if s["Fixed Duration"] then | |||
table.insert(detail, "Fixed duration") | |||
end | |||
if | if #detail > 0 then | ||
table. | seg = seg .. " (" .. table.concat(detail, ", ") .. ")" | ||
end | end | ||
table.insert(parts, seg) | |||
end | |||
end | end | ||
if #parts == 0 then | |||
return nil | |||
end | |||
if #parts == 0 then | return table.concat(parts, "<br />") | ||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatStatusRemoval(list, maxLevel, level) | 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 | ||
if type(r) == "table" then | |||
local names = r["Status External Name"] | local names = r["Status External Name"] | ||
local label | local label | ||
if type(names) == "table" then | |||
label = table.concat(names, ", ") | if type(names) == "table" then | ||
label = table.concat(names, ", ") | |||
elseif type(names) == "string" then | elseif type(names) == "string" then | ||
label = names | label = names | ||
else | else | ||
label = "Status" | label = "Status" | ||
end | end | ||
local amt = nil | local amt = nil | ||
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 = {} | ||
for _, v in ipairs(r["Per Level"]) do | for _, v in ipairs(r["Per Level"]) do | ||
table.insert(series, formatUnitValue(v) or tostring(v)) | |||
end | end | ||
amt = dynSpan(series, level) | amt = dynSpan(series, level) | ||
else | else | ||
amt = valuePairRawText(r) | amt = valuePairRawText(r) | ||
if amt then amt = mw.text.nowiki(amt) end | if amt then | ||
end | amt = mw.text.nowiki(amt) | ||
end | |||
end | |||
local seg = mw.text.nowiki(label) | local seg = mw.text.nowiki(label) | ||
if amt then | if amt then | ||
seg = seg.. " – "..amt | seg = seg .. " – " .. amt | ||
end | end | ||
table.insert(parts, seg) | table.insert(parts, seg) | ||
end | end | ||
end | end | ||
if #parts == 0 then | |||
return nil | if #parts == 0 then | ||
end | return nil | ||
return table.concat(parts, "<br />") | end | ||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatEvents(list) | local function formatEvents(list) | ||
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 _, ev in ipairs(list) do | for _, ev in ipairs(list) do | ||
if type(ev) == "table" then | |||
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)) | table.insert(parts, string.format("%s → %s", action, name)) | ||
end | end | ||
end | end | ||
if #parts == 0 then | |||
return nil | if #parts == 0 then | ||
end | return nil | ||
return table.concat(parts, "<br />") | end | ||
return table.concat(parts, "<br />") | |||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
--User matching(for auto lists on class pages) | -- User matching (for auto lists on class pages) | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local function skillMatchesUser(rec, userName) | |||
if type(rec) ~= "table" or not userName or userName == "" then | if type(rec) ~= "table" or not userName or userName == "" then | ||
return false | return false | ||
end | end | ||
local users = rec.Users | local users = rec.Users | ||
if type(users) ~= "table" then | if type(users) ~= "table" then | ||
return false | return false | ||
end | end | ||
local userLower = mw.ustring.lower(userName) | local userLower = mw.ustring.lower(userName) | ||
local function listHas(list) | local function listHas(list) | ||
if type(list) ~= "table" then | if type(list) ~= "table" then | ||
return false | return false | ||
end | end | ||
for _, v in ipairs(list) do | for _, v in ipairs(list) do | ||
if type(v) == "string" and mw.ustring.lower(v) == userLower then | |||
return true | return true | ||
end | end | ||
end | end | ||
return false | return false | ||
end | end | ||
if listHas(users.Classes) then return true end | if listHas(users.Classes) then return true end | ||
if listHas(users.Summons) then return true end | if listHas(users.Summons) then return true end | ||
if listHas(users.Monsters) then return true end | if listHas(users.Monsters) then return true end | ||
if listHas(users.Events) then return true end | if listHas(users.Events) then return true end | ||
return false | return false | ||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
--Direct page detection(hide Users on the skill's own page) | -- Direct page detection (hide Users on the skill's own page) | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local function isDirectSkillPage(rec) | local function isDirectSkillPage(rec) | ||
if type(rec) ~= "table" then | if type(rec) ~= "table" then | ||
return false | return false | ||
end | end | ||
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 | |||
return false | if not pageName then | ||
end | return false | ||
pageName = mw.ustring.lower(pageName) | end | ||
pageName = mw.ustring.lower(pageName) | |||
local ext = trim(rec["External Name"] or rec["Name"] or rec["Display Name"]) | local ext = trim(rec["External Name"] or rec["Name"] or rec["Display Name"]) | ||
local internal = trim(rec["Internal Name"] or rec["InternalName"] or rec["InternalID"]) | local internal = trim(rec["Internal Name"] or rec["InternalName"] or rec["InternalID"]) | ||
if ext and mw.ustring.lower(ext) == pageName then | if ext and mw.ustring.lower(ext) == pageName then | ||
return true | return true | ||
end | end | ||
if internal and mw.ustring.lower(internal) == pageName then | if internal and mw.ustring.lower(internal) == pageName then | ||
return true | return true | ||
end | end | ||
return false | |||
return false | |||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
--Infobox builder(Top band: Level Select + Type) | -- Infobox builder (Top band: Level Select + Type) | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local function buildLevelSelectUI(level, maxLevel) | |||
--JS expectations: | -- JS expectations: | ||
-- .sv - level - slider placeholder for the < input type = "range" > | -- .sv-level-slider => placeholder for the <input type="range"> | ||
-- .sv - level - num span for updating the number | -- .sv-level-num => span for updating the number | ||
local wrap = mw.html.create("div") | local wrap = mw.html.create("div") | ||
wrap: addClass("sv-level-ui") | wrap:addClass("sv-level-ui") | ||
wrap: tag("div") | wrap:tag("div") | ||
: addClass("sv-level-title") | :addClass("sv-level-title") | ||
: wikitext("Level Select") | :wikitext("Level Select") | ||
wrap: tag("div") | wrap: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)) | ||
wrap: tag("div"): addClass("sv-level-slider") | wrap:tag("div"):addClass("sv-level-slider") | ||
return tostring(wrap) | return tostring(wrap) | ||
end | end | ||
local function buildTypeTableUI(typeBlock) | local function buildTypeTableUI(typeBlock) | ||
if type(typeBlock) ~= "table" or next(typeBlock) == nil then | if type(typeBlock) ~= "table" or next(typeBlock) == nil then | ||
return nil | return nil | ||
end | end | ||
local wrap = mw.html.create("div") | local wrap = mw.html.create("div") | ||
wrap: addClass("sv-type-grid") | wrap:addClass("sv-type-grid") | ||
local added = false | local added = false | ||
local function valName(x) | local function valName(x) | ||
if x == nil then return nil end | if x == nil then | ||
if type(x) == "table" then | return nil | ||
if x.Name and x.Name ~= "" then return tostring(x.Name) end | end | ||
if x.ID and x.ID ~= "" then return tostring(x.ID) end | if type(x) == "table" then | ||
end | if x.Name and x.Name ~= "" then return tostring(x.Name) end | ||
if type(x) == "string" and x ~= "" then return x end | if x.ID and x.ID ~= "" then return tostring(x.ID) end | ||
return nil | end | ||
end | if type(x) == "string" and x ~= "" then | ||
return x | |||
end | |||
return nil | |||
end | |||
local function addChunk(label, rawVal) | local function addChunk(label, rawVal) | ||
local v = valName(rawVal) | local v = valName(rawVal) | ||
if not v or v == "" then return end | if not v or v == "" then | ||
added = true | return | ||
end | |||
added = true | |||
local chunk = wrap: tag("div"): addClass("sv-type-chunk") | 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-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("Type", typeBlock["Damage Type"]) | |||
addChunk("Element", typeBlock["Element Type"]) | |||
addChunk("Target", typeBlock["Target Type"]) | |||
addChunk("Cast Type", typeBlock["Cast Type"]) | |||
if not added then | |||
return nil | |||
end | |||
return tostring(wrap) | |||
return tostring(wrap) | |||
end | end | ||
local function addTopBand(tbl, levelUI, typeUI) | local function addTopBand(tbl, levelUI, typeUI) | ||
if not levelUI and not typeUI then | if not levelUI and not typeUI then | ||
return | return | ||
end | end | ||
local row = tbl: tag("tr") | local row = tbl:tag("tr") | ||
local cell = row: tag("td") | local cell = row:tag("td") | ||
cell: attr("colspan", 2) | cell:attr("colspan", 2) | ||
cell: addClass("sv-topband-cell") | cell:addClass("sv-topband-cell") | ||
--Nested table(what Common.css targets) | -- Nested table (what Common.css targets) | ||
local inner = cell: tag("table") | local inner = cell:tag("table") | ||
inner: addClass("sv-topband-table") | inner:addClass("sv-topband-table") | ||
local tr = inner: tag("tr") | local tr = inner:tag("tr") | ||
if levelUI and typeUI then | if levelUI and typeUI then | ||
tr: tag("td"): wikitext(levelUI): done() | tr:tag("td"):wikitext(levelUI):done() | ||
tr: tag("td"): wikitext(typeUI): done() | tr:tag("td"):wikitext(typeUI):done() | ||
elseif levelUI then | elseif levelUI then | ||
tr: tag("td"): attr("colspan", 2): wikitext(levelUI): done() | tr:tag("td"):attr("colspan", 2):wikitext(levelUI):done() | ||
else | else | ||
tr: tag("td"): attr("colspan", 2): wikitext(typeUI): done() | tr:tag("td"):attr("colspan", 2):wikitext(typeUI):done() | ||
end | end | ||
end | end | ||
local function buildInfobox(rec, opts) | local function buildInfobox(rec, opts) | ||
opts = opts or { } | opts = opts or {} | ||
local showUsers = (opts.showUsers ~= false) | local showUsers = (opts.showUsers ~= false) | ||
local maxLevel = tonumber(rec["Max Level"]) or 1 | local maxLevel = tonumber(rec["Max Level"]) or 1 | ||
if maxLevel < 1 then maxLevel = 1 end | if maxLevel < 1 then | ||
maxLevel = 1 | |||
end | |||
--Always default to max level | -- Always default to max level | ||
local level = clamp(maxLevel, 1, maxLevel) | local level = clamp(maxLevel, 1, maxLevel) | ||
local root = mw.html.create("table") | local root = mw.html.create("table") | ||
--IMPORTANT: | -- IMPORTANT: | ||
--Don't use "wikitable" here; it triggers default borders + Citizen wrapper styling. | -- Don't use "wikitable" here; it triggers default borders + Citizen wrapper styling. | ||
root: addClass("spiritvale-skill-infobox") | root:addClass("spiritvale-skill-infobox") | ||
--JS hook: treat the table itself as the "card" | -- JS hook: treat the table itself as the "card" | ||
root: addClass("sv-skill-card") | root:addClass("sv-skill-card") | ||
root:attr("data-max-level", tostring(maxLevel)) | |||
root:attr("data-level", tostring(level)) | |||
--Helpful flag for list - mode overrides(optional | -- Helpful flag for list-mode overrides (optional) | ||
if | if opts.inList then | ||
root: addClass("sv-skill-inlist") | root:addClass("sv-skill-inlist") | ||
end | end | ||
-- | -- Top "hero" rows: | ||
-- 1) Title row: icon + name centered (single cell) | |||
-- 2) Description row: its own row below (still part of hero band) | |||
local icon = rec.Icon | |||
local title = rec["External Name"] or rec.Name or rec["Internal Name"] or "Unknown Skill" | |||
local desc = rec.Description or "" | |||
-- | -- Row 1: icon + title (single column) | ||
local heroRow = root:tag("tr") | |||
-- | heroRow:addClass("spiritvale-infobox-main") | ||
heroRow:addClass("sv-hero-title-row") | |||
local | local heroCell = heroRow:tag("th") | ||
heroCell:attr("colspan", 2) | |||
heroCell:addClass("sv-hero-title-cell") | |||
local heroInner = heroCell:tag("div") | |||
local | heroInner:addClass("spiritvale-infobox-main-left-inner") -- reuse centered flex column | ||
if icon and icon ~= "" then | |||
heroInner:wikitext(string.format("[[File:%s|80px|link=]]", icon)) | |||
end | |||
heroInner:tag("div") | |||
:addClass("spiritvale-infobox-title") | |||
:wikitext(title) | |||
if | -- Row 2: description (below, still hero-styled) | ||
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") | |||
:wikitext(string.format("''%s''", desc)) | |||
end | |||
descInner: tag("div") | |||
end | |||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
--Unified top band(Level Select left, Type list right) | -- Unified top band (Level Select left, Type list right) | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
local levelUI = buildLevelSelectUI(level, maxLevel) | local levelUI = buildLevelSelectUI(level, maxLevel) | ||
local typeUI = buildTypeTableUI(rec.Type or {}) | local typeUI = buildTypeTableUI(rec.Type or {}) | ||
addTopBand(root, levelUI, typeUI) | addTopBand(root, levelUI, typeUI) | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
--Users(kept as rows; caller decides show / hide) | -- Users (kept as rows; caller decides show/hide) | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
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)) | ||
addRow(root, "Summons", listToText(users.Summons)) | addRow(root, "Summons", listToText(users.Summons)) | ||
addRow(root, "Monsters", listToText(users.Monsters)) | addRow(root, "Monsters", listToText(users.Monsters)) | ||
addRow(root, "Events", listToText(users.Events)) | addRow(root, "Events", listToText(users.Events)) | ||
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 nameReq = rs["Skill External Name"] or rs["Skill Internal Name"] or "Unknown" | local nameReq = rs["Skill External Name"] or rs["Skill Internal Name"] or "Unknown" | ||
local lvlReq = rs["Required Level"] | local lvlReq = rs["Required Level"] | ||
if lvlReq then | if lvlReq then | ||
table.insert(skillParts, string.format("%s (Lv.%s)", nameReq, lvlReq)) | table.insert(skillParts, string.format("%s (Lv.%s)", nameReq, lvlReq)) | ||
else | else | ||
table.insert(skillParts, nameReq) | table.insert(skillParts, nameReq) | ||
end | end | ||
end | end | ||
addRow(root, "Required Skills", table.concat(skillParts, ", ")) | addRow(root, "Required Skills", table.concat(skillParts, ", ")) | ||
end | end | ||
addRow(root, "Required Weapons", listToText(req["Required Weapons"])) | addRow(root, "Required Weapons", listToText(req["Required Weapons"])) | ||
addRow(root, "Required Stances", listToText(req["Required Stances"])) | 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)) | |||
if mech["Autocast Multiplier"] ~= nil then | |||
addRow(root, " | addRow(root, "Autocast Multiplier", tostring(mech["Autocast Multiplier"])) | ||
end | |||
addRow(root, "Timing", formatTimingBlock(mech["Basic Timings"], maxLevel, level)) | |||
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)) | |||
addRow(root, "Timing", formatTimingBlock(mech["Basic Timings"], maxLevel, level)) | end | ||
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 | |||
------------------------------------------------------------------ | |||
-- Damage & Scaling | |||
------------------------------------------------------------------ | |||
local dmg = rec.Damage or {} | |||
if next(dmg) ~= nil then | |||
local main = dmg["Main Damage"] | local main = dmg["Main Damage"] | ||
local mainNonHeal, healOnly = {}, {} | local mainNonHeal, healOnly = {}, {} | ||
if type(main) == "table" then | if type(main) == "table" then | ||
for _, d in ipairs(main) do | for _, d in ipairs(main) do | ||
if type(d) == "table" and d.Type == "Healing" then | |||
table.insert(healOnly, d) | table.insert(healOnly, d) | ||
else | else | ||
table.insert(mainNonHeal, d) | table.insert(mainNonHeal, d) | ||
end | end | ||
end | end | ||
end | end | ||
local flatList = dmg["Flat Damage"] | local flatList = dmg["Flat Damage"] | ||
| Line 1,062: | Line 1,121: | ||
local reflHas = (type(reflList) == "table" and #reflList > 0) | local reflHas = (type(reflList) == "table" and #reflList > 0) | ||
local pureHealing = (#healOnly > 0) and(#mainNonHeal == 0) and(not flatHas) and(not reflHas) | 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, "Main Damage", formatDamageList(mainNonHeal, maxLevel, level, (#mainNonHeal > 1))) | ||
addRow(root, "Flat Damage", formatDamageList(flatList, maxLevel, level, false)) | addRow(root, "Flat Damage", formatDamageList(flatList, maxLevel, level, false)) | ||
addRow(root, "Reflect Damage", formatDamageList(reflList, maxLevel, level, false)) | addRow(root, "Reflect Damage", formatDamageList(reflList, maxLevel, level, false)) | ||
addRow(root, "Healing", formatDamageList(healOnly, maxLevel, level, false)) | addRow(root, "Healing", formatDamageList(healOnly, maxLevel, level, false)) | ||
addRow(root, "Scaling", formatScaling(dmg.Scaling, pureHealing and "Healing" or nil)) | addRow(root, "Scaling", formatScaling(dmg.Scaling, pureHealing and "Healing" or nil)) | ||
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"], 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) | ||
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 | ||
return tostring(root) | return tostring(root) | ||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
--Public: list all skills for a given user / class | -- Public: list all skills for a given user/class | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
function p.listForUser(frame) | |||
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 | ||
userName = mw.title.getCurrentTitle().text | userName = mw.title.getCurrentTitle().text | ||
end | end | ||
if not userName or userName == "" then | if not userName or userName == "" then | ||
return "<strong>No user name provided to Skill list.</strong>" | return "<strong>No user name provided to Skill list.</strong>" | ||
end | end | ||
local dataset = getSkills() | local dataset = getSkills() | ||
local matches = {} | local matches = {} | ||
for _, rec in ipairs(dataset.records or {}) do | for _, rec in ipairs(dataset.records or {}) do | ||
if skillMatchesUser(rec, userName) then | |||
table.insert(matches, rec) | table.insert(matches, rec) | ||
end | end | ||
end | end | ||
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 | ||
--ONE wrapper panel(table - like list mode) | -- ONE wrapper panel (table-like list mode) | ||
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 | ||
-- Stable per-skill wrapper so dividers work even under Citizen wrappers | |||
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 })) | ||
end | end | ||
return tostring(root) | return tostring(root) | ||
end | end | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
--Public: single - skill or auto - list dispatcher | -- Public: single-skill or auto-list dispatcher | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
function p.infobox(frame) | |||
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 | ||
local id = args.id | local id = args.id | ||
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() | ||
local pageName = pageTitle and pageTitle.text or "" | local pageName = pageTitle and pageTitle.text or "" | ||
local noExplicitArgs = | local noExplicitArgs = | ||
(not raw1 or raw1 == "") and | |||
(not args.name or args.name == "") and | |||
(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 Skill:</strong> %s[[Category:Pages with unknown skill|%s]]", | |||
mw.text.nowiki(label), | |||
label | |||
) | ) | ||
end | end | ||
local showUsers = not isDirectSkillPage(rec) | local showUsers = not isDirectSkillPage(rec) | ||
return buildInfobox(rec, { showUsers = showUsers }) | return buildInfobox(rec, { showUsers = showUsers }) | ||
end | end | ||
return p | return p | ||