Module:GameSkills: Difference between revisions
More actions
No edit summary |
No edit summary |
||
| Line 24: | Line 24: | ||
local function getSkills() | local function getSkills() | ||
if not skillsCache then | |||
skillsCache = GameData.loadSkills() | |||
end | |||
return skillsCache | |||
end | end | ||
local function getArgs(frame) | local function getArgs(frame) | ||
local parent = frame:getParent() | |||
if parent then | |||
return parent.args | |||
end | |||
return frame.args | |||
end | end | ||
local function listToText(list, sep) | local function listToText(list, sep) | ||
if type(list) ~= "table" or #list == 0 then | |||
return nil | |||
end | |||
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 | |||
return | |||
end | |||
local row = tbl:tag("tr") | |||
row:tag("th"):wikitext(label):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 cell = row:tag("th") | |||
cell:attr("colspan", 2) | |||
cell:addClass("spiritvale-infobox-section-header") | |||
cell:wikitext(label) | |||
end | end | ||
local function trim(s) | |||
if type(s) ~= "string" then return nil end | |||
s = mw.text.trim(s) | |||
if s == "" then return nil end | |||
return s | |||
end | |||
-- Handles either a scalar OR {Value=..., Unit=...} | |||
local function formatUnitValue(v) | |||
if type(v) == "table" and v.Value ~= nil then | |||
local unit = v.Unit | |||
local val = v.Value | |||
-- Best-effort formatting for common unit types | |||
if unit == "percent_decimal" then | |||
return tostring((val or 0) * 100) .. "%" | |||
elseif unit == "percent_whole" or unit == "percent" then | |||
return tostring(val) .. "%" | |||
elseif unit == "seconds" then | |||
return tostring(val) .. "s" | |||
elseif unit == "meters" then | |||
return tostring(val) .. "m" | |||
elseif unit == "tiles" then | |||
return tostring(val) .. " tiles" | |||
elseif unit and unit ~= "" then | |||
return tostring(val) .. " " .. tostring(unit) | |||
else | |||
return tostring(val) | |||
end | |||
end | |||
if v == nil then | |||
return nil | |||
end | |||
return tostring(v) | |||
end | |||
---------------------------------------------------------------------- | |||
-- Lookups | |||
---------------------------------------------------------------------- | |||
-- Lookup by Internal Name | -- Lookup by Internal Name | ||
local function getSkillById(id) | local function getSkillById(id) | ||
id = trim(id) | |||
if not id then return nil end | |||
local dataset = getSkills() | |||
local byId = dataset.byId or {} | |||
return byId[id] | |||
end | end | ||
-- Lookup by display Name (for editors) | -- Lookup by display/external Name (for editors) | ||
local function findSkillByName(name) | local function findSkillByName(name) | ||
name = trim(name) | |||
if not name then return nil end | |||
local dataset = getSkills() | |||
-- Fast path if GameData built byName | |||
local byName = dataset.byName or {} | |||
if byName[name] then | |||
return byName[name] | |||
end | |||
-- Fallback scan (older GameData) | |||
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 | |||
return nil | |||
end | end | ||
| Line 89: | Line 137: | ||
-- Formatting helpers | -- Formatting helpers | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local function isFlatPerLevel(baseStr, perList) | |||
if type(perList) ~= "table" or #perList == 0 then | |||
return false | |||
end | |||
-- If all per-level entries are identical, treat as "no scaling" | |||
local first = tostring(perList[1]) | |||
for i = 2, #perList do | |||
if tostring(perList[i]) ~= first then | |||
return false | |||
end | |||
end | |||
-- If base matches that identical value, definitely flat/no-op | |||
if baseStr ~= nil and tostring(baseStr) == first then | |||
return true | |||
end | |||
-- Even if base differs (sometimes Base is "0" but Lv list is constant), still no meaningful scaling | |||
return true | |||
end | |||
local function formatBasePer(block) | local function formatBasePer(block) | ||
if type(block) ~= "table" then | |||
return nil | |||
end | |||
local base = block.Base | |||
local per = block["Per Level"] | |||
local baseText = (base ~= nil) and formatUnitValue(base) or nil | |||
-- Per Level might be a scalar OR list (wikiprep expansion) | |||
if type(per) == "table" then | |||
if isFlatPerLevel(baseText, per) then | |||
-- Show only Base (or Lv1 if Base is missing) | |||
if baseText then | |||
return "Base: " .. baseText | |||
end | |||
if per[1] ~= nil then | |||
return "Lv1: " .. tostring(per[1]) | |||
end | |||
return nil | |||
end | |||
local lines = {} | |||
if baseText then | |||
table.insert(lines, "Base: " .. baseText) | |||
end | |||
for i, v in ipairs(per) do | |||
table.insert(lines, string.format("Lv%d: %s", i, tostring(v))) | |||
end | |||
return table.concat(lines, "<br />") | |||
else | |||
local parts = {} | |||
if baseText then | |||
table.insert(parts, "Base " .. baseText) | |||
end | |||
if per ~= nil then | |||
table.insert(parts, tostring(per) .. " / Lv") | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, ", ") | |||
end | |||
end | end | ||
local function formatMainDamage(list) | local function formatMainDamage(list) | ||
if type(list) ~= "table" or #list == 0 then | |||
return nil | |||
end | |||
local parts = {} | |||
for _, d in ipairs(list) do | |||
if type(d) == "table" then | |||
local kind = d.Type or "Damage" | |||
local base = d["Base %"] | |||
local per = d["Per Level %"] | |||
local seg = kind | |||
local detail = {} | |||
if base ~= nil then | |||
table.insert(detail, string.format("Base %s%%", tostring(base))) | |||
end | |||
if per ~= nil then | |||
table.insert(detail, string.format("%s%% / Lv", tostring(per))) | |||
end | |||
if d["ATK-Based"] then | |||
table.insert(detail, "ATK-based") | |||
end | |||
if d["MATK-Based"] then | |||
table.insert(detail, "MATK-based") | |||
end | |||
if #detail > 0 then | |||
seg = seg .. " – " .. table.concat(detail, ", ") | |||
end | |||
table.insert(parts, seg) | |||
end | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatReflectDamage(list) | local function formatReflectDamage(list) | ||
if type(list) ~= "table" or #list == 0 then | |||
return nil | |||
end | |||
local parts = {} | |||
for _, d in ipairs(list) do | |||
if type(d) == "table" then | |||
local base = d["Base %"] | |||
local per = d["Per Level %"] | |||
local seg = "Reflect" | |||
local detail = {} | |||
if base ~= nil then | |||
table.insert(detail, string.format("Base %s%%", tostring(base))) | |||
end | |||
if per ~= nil then | |||
table.insert(detail, string.format("%s%% / Lv", tostring(per))) | |||
end | |||
if #detail > 0 then | |||
seg = seg .. " – " .. table.concat(detail, ", ") | |||
end | |||
table.insert(parts, seg) | |||
end | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatScaling(list) | local function formatScaling(list) | ||
if type(list) ~= "table" or #list == 0 then | |||
return nil | |||
end | |||
local parts = {} | |||
for _, s in ipairs(list) do | |||
if type(s) == "table" then | |||
local name = s["Scaling Name"] or s["Scaling ID"] or "Unknown" | |||
local pct = s.Percent | |||
local seg = name | |||
local detail = {} | |||
if pct ~= nil then | |||
-- If Unit is percent_decimal, convert; otherwise assume already percent | |||
if s.Unit == "percent_decimal" then | |||
table.insert(detail, string.format("%s%%", tostring((pct or 0) * 100))) | |||
else | |||
table.insert(detail, string.format("%s%%", tostring(pct))) | |||
end | |||
end | |||
if s["ATK-Based"] then | |||
table.insert(detail, "ATK-based") | |||
end | |||
if s["MATK-Based"] then | |||
table.insert(detail, "MATK-based") | |||
end | |||
if #detail > 0 then | |||
seg = seg .. " – " .. table.concat(detail, ", ") | |||
end | |||
table.insert(parts, seg) | |||
end | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatArea(area) | local function formatArea(area) | ||
if type(area) ~= "table" then | |||
return nil | |||
end | |||
local parts = {} | |||
local size = area["Area Size"] | |||
if size and size ~= "" then | |||
table.insert(parts, "Size: " .. tostring(size)) | |||
end | |||
local dist = area["Area Distance"] | |||
local eff = area["Effective Distance"] | |||
local distText = formatBasePer(dist) | |||
if distText then | |||
table.insert(parts, "Distance: " .. distText) | |||
end | |||
local effText = formatUnitValue(eff) | |||
if effText then | |||
table.insert(parts, "Effective: " .. effText) | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatTimingBlock(bt) | local function formatTimingBlock(bt) | ||
if type(bt) ~= "table" then | |||
return nil | |||
end | |||
local parts = {} | |||
local function add(name, key) | |||
local block = bt[key] | |||
local txt = formatBasePer(block) | |||
if txt then | |||
table.insert(parts, name .. ": " .. txt) | |||
end | |||
end | |||
add("Cast Time", "Cast Time") | |||
add("Cooldown", "Cooldown") | |||
add("Duration", "Duration") | |||
if bt["Effect Cast Time"] ~= nil then | |||
table.insert(parts, "Effect Cast Time: " .. tostring(bt["Effect Cast Time"])) | |||
end | |||
if bt["Damage Delay"] ~= nil then | |||
table.insert(parts, "Damage Delay: " .. tostring(bt["Damage Delay"])) | |||
end | |||
if bt["Effect Remove Delay"] ~= nil then | |||
table.insert(parts, "Effect Remove Delay: " .. tostring(bt["Effect Remove Delay"])) | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatResourceCost(rc) | local function formatResourceCost(rc) | ||
if type(rc) ~= "table" then | |||
return nil | |||
end | |||
local parts = {} | |||
local manaTxt = formatBasePer(rc["Mana Cost"]) | |||
if manaTxt then | |||
table.insert(parts, "MP: " .. manaTxt) | |||
end | |||
local hpTxt = formatBasePer(rc["Health Cost"]) | |||
if hpTxt then | |||
table.insert(parts, "HP: " .. hpTxt) | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatCombo(combo) | local function formatCombo(combo) | ||
if type(combo) ~= "table" then | |||
return nil | |||
end | |||
local parts = {} | |||
if combo.Type then | |||
table.insert(parts, "Type: " .. tostring(combo.Type)) | |||
end | |||
local durText = formatUnitValue(combo.Duration) | |||
if durText then | |||
table.insert(parts, "Duration: " .. durText) | |||
end | |||
-- Percent may be scalar or {Value,Unit} | |||
if combo.Percent ~= nil then | |||
local pctText = formatUnitValue(combo.Percent) | |||
if pctText then | |||
table.insert(parts, "Bonus: " .. pctText) | |||
end | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, ", ") | |||
end | end | ||
local function formatMechanicEffects(effects) | local function formatMechanicEffects(effects) | ||
if type(effects) ~= "table" then | |||
return nil | |||
end | |||
local keys = {} | |||
for k, _ in pairs(effects) do | |||
table.insert(keys, k) | |||
end | |||
table.sort(keys) | |||
local parts = {} | |||
for _, name in ipairs(keys) do | |||
local block = effects[name] | |||
if type(block) == "table" then | |||
local bp = formatBasePer(block) | |||
local seg = name | |||
if bp then | |||
seg = seg .. " – " .. bp | |||
end | |||
table.insert(parts, seg) | |||
end | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatModifiers(mods) | local function formatModifiers(mods) | ||
if type(mods) ~= "table" then | |||
return nil | |||
end | |||
local parts = {} | |||
local function collect(label, sub) | |||
if type(sub) ~= "table" then | |||
return | |||
end | |||
local flags = {} | |||
for k, v in pairs(sub) do | |||
if v then | |||
table.insert(flags, k) | |||
end | |||
end | |||
table.sort(flags) | |||
if #flags > 0 then | |||
table.insert(parts, string.format("%s: %s", label, table.concat(flags, ", "))) | |||
end | |||
end | |||
collect("Movement", mods["Movement Modifiers"]) | |||
collect("Combat", mods["Combat Modifiers"]) | |||
collect("Special", mods["Special Modifiers"]) | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatStatusApplications(list) | local function formatStatusApplications(list) | ||
if type(list) ~= "table" or #list == 0 then | |||
return nil | |||
end | |||
local parts = {} | |||
for _, s in ipairs(list) do | |||
if type(s) == "table" then | |||
local scope = s.Scope or "Target" | |||
local name = s["Status External Name"] or s["Status Internal Name"] or "Unknown status" | |||
local seg = scope .. " – " .. tostring(name) | |||
local detail = {} | |||
local dur = s.Duration | |||
if type(dur) == "table" then | |||
local t = formatBasePer(dur) | |||
if t then | |||
table.insert(detail, "Duration " .. t) | |||
end | |||
end | |||
local ch = s.Chance | |||
if type(ch) == "table" then | |||
local t = formatBasePer(ch) | |||
if t then | |||
table.insert(detail, "Chance " .. t) | |||
end | |||
end | |||
if s["Fixed Duration"] then | |||
table.insert(detail, "Fixed duration") | |||
end | |||
if #detail > 0 then | |||
seg = seg .. " (" .. table.concat(detail, ", ") .. ")" | |||
end | |||
table.insert(parts, seg) | |||
end | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatStatusRemoval(list) | local function formatStatusRemoval(list) | ||
if type(list) ~= "table" or #list == 0 then | |||
return nil | |||
end | |||
local parts = {} | |||
for _, r in ipairs(list) do | |||
if type(r) == "table" then | |||
local names = r["Status External Name"] | |||
local label | |||
if type(names) == "table" then | |||
label = table.concat(names, ", ") | |||
elseif type(names) == "string" then | |||
label = names | |||
else | |||
label = "Status" | |||
end | |||
local bp = formatBasePer(r) | |||
local seg = label | |||
if bp then | |||
seg = seg .. " – " .. bp | |||
end | |||
table.insert(parts, seg) | |||
end | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
local function formatEvents(list) | local function formatEvents(list) | ||
if type(list) ~= "table" or #list == 0 then | |||
return nil | |||
end | |||
local parts = {} | |||
for _, ev in ipairs(list) do | |||
if type(ev) == "table" then | |||
local action = ev.Action or "On event" | |||
local name = ev["Skill Internal Name"] or ev["Skill External Name"] or "Unknown skill" | |||
local seg = string.format("%s → %s", action, name) | |||
table.insert(parts, seg) | |||
end | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, "<br />") | |||
end | end | ||
| Line 459: | Line 583: | ||
local function skillMatchesUser(rec, userName) | local function skillMatchesUser(rec, userName) | ||
if type(rec) ~= "table" or not userName or userName == "" then | |||
return false | |||
end | |||
local users = rec.Users | |||
if type(users) ~= "table" then | |||
return false | |||
end | |||
local userLower = mw.ustring.lower(userName) | |||
local function listHas(list) | |||
if type(list) ~= "table" then | |||
return false | |||
end | |||
for _, v in ipairs(list) do | |||
if type(v) == "string" and mw.ustring.lower(v) == userLower then | |||
return true | |||
end | |||
end | |||
return false | |||
end | |||
if listHas(users.Classes) then return true end | |||
if listHas(users.Summons) then return true end | |||
if listHas(users.Monsters) then return true end | |||
if listHas(users.Events) then return true end | |||
return false | |||
end | end | ||
| Line 495: | Line 619: | ||
local function buildInfobox(rec) | local function buildInfobox(rec) | ||
local root = mw.html.create("table") | |||
root:addClass("wikitable spiritvale-skill-infobox") | |||
-- ========================================================== | |||
-- Top "hero" row: icon + name (left), description (right) | |||
-- ========================================================== | |||
local icon = rec.Icon | |||
local title = rec["External Name"] or rec.Name or rec["Internal Name"] or "Unknown Skill" | |||
local desc = rec.Description or "" | |||
local headerRow = root:tag("tr") | |||
headerRow:addClass("spiritvale-infobox-main") | |||
-- Left cell: icon + name | |||
local leftCell = headerRow:tag("th") | |||
leftCell:addClass("spiritvale-infobox-main-left") | |||
local leftInner = leftCell:tag("div") | |||
leftInner:addClass("spiritvale-infobox-main-left-inner") | |||
if icon and icon ~= "" then | |||
leftInner:wikitext(string.format("[[File:%s|80px|link=]]", icon)) | |||
end | |||
leftInner:tag("div") | |||
:addClass("spiritvale-infobox-title") | |||
:wikitext(title) | |||
-- Right cell: italic description | |||
local rightCell = headerRow:tag("td") | |||
rightCell:addClass("spiritvale-infobox-main-right") | |||
local rightInner = rightCell:tag("div") | |||
rightInner:addClass("spiritvale-infobox-main-right-inner") | |||
if desc ~= "" then | |||
rightInner:tag("div") | |||
:addClass("spiritvale-infobox-description") | |||
:wikitext(string.format("''%s''", desc)) | |||
end | |||
------------------------------------------------------------------ | |||
-- General | |||
------------------------------------------------------------------ | |||
addSectionHeader(root, "General") | |||
addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"])) | |||
local users = rec.Users or {} | |||
addRow(root, "Classes", listToText(users.Classes)) | |||
addRow(root, "Summons", listToText(users.Summons)) | |||
addRow(root, "Monsters", listToText(users.Monsters)) | |||
addRow(root, "Events", listToText(users.Events)) | |||
------------------------------------------------------------------ | |||
-- Requirements | |||
------------------------------------------------------------------ | |||
local req = rec.Requirements or {} | |||
if (req["Required Skills"] and #req["Required Skills"] > 0) | |||
or (req["Required Weapons"] and #req["Required Weapons"] > 0) | |||
or (req["Required Stances"] and #req["Required Stances"] > 0) then | |||
addSectionHeader(root, "Requirements") | |||
if type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0 then | |||
local skillParts = {} | |||
for _, rs in ipairs(req["Required Skills"]) do | |||
local name = rs["Skill External Name"] or rs["Skill Internal Name"] or "Unknown" | |||
local level = rs["Required Level"] | |||
if level then | |||
table.insert(skillParts, string.format("%s (Lv.%s)", name, level)) | |||
else | |||
table.insert(skillParts, name) | |||
end | |||
end | |||
addRow(root, "Required skills", table.concat(skillParts, ", ")) | |||
end | |||
addRow(root, "Required weapons", listToText(req["Required Weapons"])) | |||
addRow(root, "Required stances", listToText(req["Required Stances"])) | |||
end | |||
------------------------------------------------------------------ | |||
-- Type | |||
------------------------------------------------------------------ | |||
local typeBlock = rec.Type or {} | |||
if next(typeBlock) ~= nil then | |||
addSectionHeader(root, "Type") | |||
local dt = typeBlock["Damage Type"] | |||
if type(dt) == "table" and dt.Name then | |||
addRow(root, "Damage type", dt.Name) | |||
end | |||
local et = typeBlock["Element Type"] | |||
if type(et) == "table" and et.Name then | |||
addRow(root, "Element", et.Name) | |||
end | |||
local tt = typeBlock["Target Type"] | |||
if type(tt) == "table" and tt.Name then | |||
addRow(root, "Target", tt.Name) | |||
end | |||
local ct = typeBlock["Cast Type"] | |||
if type(ct) == "table" and ct.Name then | |||
addRow(root, "Cast type", ct.Name) | |||
end | |||
end | |||
------------------------------------------------------------------ | |||
-- Mechanics | |||
------------------------------------------------------------------ | |||
local mech = rec.Mechanics or {} | |||
if next(mech) ~= nil then | |||
addSectionHeader(root, "Mechanics") | |||
local rangeText = formatUnitValue(mech.Range) | |||
addRow(root, "Range", rangeText) | |||
local areaText = formatArea(mech.Area) | |||
addRow(root, "Area", areaText) | |||
if mech["Autocast Multiplier"] ~= nil then | |||
addRow(root, "Autocast multiplier", tostring(mech["Autocast Multiplier"])) | |||
end | |||
local btText = formatTimingBlock(mech["Basic Timings"]) | |||
addRow(root, "Timing", btText) | |||
local rcText = formatResourceCost(mech["Resource Cost"]) | |||
addRow(root, "Resource cost", rcText) | |||
local comboText = formatCombo(mech.Combo) | |||
addRow(root, "Combo", comboText) | |||
local effText = formatMechanicEffects(mech.Effects) | |||
addRow(root, "Special mechanics", effText) | |||
end | |||
------------------------------------------------------------------ | |||
-- Damage & Healing | |||
------------------------------------------------------------------ | |||
local dmg = rec.Damage or {} | |||
if next(dmg) ~= nil then | |||
addSectionHeader(root, "Damage and scaling") | |||
if dmg["Healing Present"] then | |||
addRow(root, "Healing", "Yes") | |||
end | |||
local mainText = formatMainDamage(dmg["Main Damage"]) | |||
addRow(root, "Main damage", mainText) | |||
local reflText = formatReflectDamage(dmg["Reflect Damage"]) | |||
addRow(root, "Reflect damage", reflText) | |||
local scaleText = formatScaling(dmg.Scaling) | |||
addRow(root, "Scaling", scaleText) | |||
end | |||
------------------------------------------------------------------ | |||
-- Modifiers | |||
------------------------------------------------------------------ | |||
local modsText = formatModifiers(rec.Modifiers) | |||
if modsText then | |||
addSectionHeader(root, "Modifiers") | |||
addRow(root, "Flags", modsText) | |||
end | |||
------------------------------------------------------------------ | |||
-- Status | |||
------------------------------------------------------------------ | |||
local statusApps = formatStatusApplications(rec["Status Applications"]) | |||
local statusRem = formatStatusRemoval(rec["Status Removal"]) | |||
if statusApps or statusRem then | |||
addSectionHeader(root, "Status effects") | |||
addRow(root, "Applies", statusApps) | |||
addRow(root, "Removes", statusRem) | |||
end | |||
------------------------------------------------------------------ | |||
-- Events | |||
------------------------------------------------------------------ | |||
local eventsText = formatEvents(rec.Events) | |||
if eventsText then | |||
addSectionHeader(root, "Events") | |||
addRow(root, "Triggers", eventsText) | |||
end | |||
------------------------------------------------------------------ | |||
-- Notes | |||
------------------------------------------------------------------ | |||
if type(rec.Notes) == "table" and #rec.Notes > 0 then | |||
addSectionHeader(root, "Notes") | |||
addRow(root, "Notes", table.concat(rec.Notes, "<br />")) | |||
end | |||
return tostring(root) | |||
end | end | ||
| Line 705: | Line 824: | ||
function p.listForUser(frame) | function p.listForUser(frame) | ||
local args = getArgs(frame) | |||
-- Prefer explicit param, then unnamed, then fall back to the current page name. | |||
local userName = args.user or args[1] | |||
if not userName or userName == "" then | |||
userName = mw.title.getCurrentTitle().text | |||
end | |||
if not userName or userName == "" then | |||
return "<strong>No user name provided to Skill list.</strong>" | |||
end | |||
local dataset = getSkills() | |||
local matches = {} | |||
for _, rec in ipairs(dataset.records or {}) do | |||
if skillMatchesUser(rec, userName) then | |||
table.insert(matches, rec) | |||
end | |||
end | |||
if #matches == 0 then | |||
return string.format( | |||
"<strong>No skills found for:</strong> %s", | |||
mw.text.nowiki(userName) | |||
) | |||
end | |||
local root = mw.html.create("div") | |||
root:addClass("spiritvale-skill-list") | |||
for _, rec in ipairs(matches) do | |||
root:wikitext(buildInfobox(rec)) | |||
end | |||
return tostring(root) | |||
end | end | ||
| Line 748: | Line 867: | ||
function p.infobox(frame) | function p.infobox(frame) | ||
local args = getArgs(frame) | |||
-- Allow three styles: | |||
-- {{Skill|Bash}} -> args[1] = "Bash" (External Name) | |||
-- {{Skill|name=Bash}} -> args.name = "Bash" | |||
-- {{Skill|id=Bash_Internal}} -> args.id = "Bash_Internal" | |||
local raw1 = args[1] | |||
local name = args.name or raw1 | |||
local id = args.id | |||
local rec | |||
-- 1) Prefer External/Display Name | |||
if name and name ~= "" then | |||
rec = findSkillByName(name) | |||
end | |||
-- 2) Fallback: internal ID | |||
if not rec and id and id ~= "" then | |||
rec = getSkillById(id) | |||
end | |||
-- 3) If still nothing, decide if this is "list mode" or truly unknown. | |||
if not rec then | |||
local pageTitle = mw.title.getCurrentTitle() | |||
local pageName = pageTitle and pageTitle.text or "" | |||
local noExplicitArgs = | |||
(not raw1 or raw1 == "") and | |||
(not args.name or args.name == "") and | |||
(not id or id == "") | |||
-- Case A: {{Skill}} with no parameters on a page → list for that page name. | |||
if noExplicitArgs then | |||
return p.listForUser(frame) | |||
end | |||
-- Case B: {{Skill|Acolyte}} on the "Acolyte" page and no id → treat as list. | |||
if name and name ~= "" and name == pageName and (not id or id == "") then | |||
return p.listForUser(frame) | |||
end | |||
-- Otherwise, genuinely unknown skill. | |||
local label = name or id or "?" | |||
return string.format( | |||
"<strong>Unknown skill:</strong> %s[[Category:Pages with unknown skill|%s]]", | |||
mw.text.nowiki(label), | |||
label | |||
) | |||
end | |||
return buildInfobox(rec) | |||
end | end | ||
return p | return p | ||
Revision as of 19:36, 14 December 2025
Module:GameSkills
Module:GameSkills renders skill data from Data:skills.json into a reusable infobox-style table.
It is intended to be used via a template (for example Template:Skill) so that skills can be embedded on any page without creating individual skill pages.
This module:
- Loads data via Module:GameData →
GameData.loadSkills(). - Looks up skills primarily by display
"Name"(what editors use), with"Internal Name"as a fallback. - Builds a table with only the fields that actually exist for that skill.
Data source
Skill data comes from Data:skills.json, which is a JSON page with this top-level structure (see Module:GameData/doc for full details):
{
"version": "SpiritVale-0.8.2",
"schema_version": 1,
"generated_at": "2025-12-12T17:24:05.807675+00:00",
"records": [
{
"Name": "Some Skill",
"Internal Name": "SomeSkillInternalId",
"...": "other fields specific to skills"
}
]
}
Each record is a single skill. Important keys:
"Name"– the display name (what players and editors will usually see and use)."Internal Name"– the stable ID used internally and available as an optional parameter for power users and tooling.
Output
For a given skill, the module renders a table with the CSS class spiritvale-skill-infobox.
Depending on what exists in the JSON record, the table may include:
- Header row with skill name (and icon, if present).
- Icon (from
"Icon", as a file name likeskill-example.webp). - Description.
- Type information:
- Damage Type
- Element Type
- Target Type
- Cast Type
- Max Level.
- Users:
- Classes
- Summons
- Monsters
- Events
- Requirements:
- Required Skills (with required level)
- Required Weapons
- Required Stances
- Mechanics:
- Range
- Cast Time / Cooldown / Duration
- Mana Cost
- Damage and scaling:
- Main Damage (base and per-level, where present)
- Scaling (stat-based contributions)
- Status interactions:
- Status Applications (status name, scope, basic duration/chance info)
Rows are only shown if the underlying field exists in the JSON for that skill.
Public interface
The module exposes a single entry point for templates:
GameSkills.infobox(frame)
This is usually called via #invoke from a template, not directly from pages.
It accepts the following parameters (either passed directly or via a wrapper template):
1– unnamed parameter; treated as the skill"Name".name– explicit display"Name"of the skill (equivalent to1).id–"Internal Name"of the skill (optional fallback / power use).
Lookup order:
- If
nameor the first unnamed parameter is provided and matches a record’s"Name", that record is used. - Otherwise, if
idis provided and matches an"Internal Name", that record is used. - If nothing is found, a small error message is returned and the page is categorized for tracking.
Example direct usage (not recommended; normally use a template):
No skills found for: GameSkills
or:
No skills found for: GameSkills
Template:Skill
The recommended way to use this module is via a small wrapper template, for example:
Template:Skill
No skills found for: GameSkills
Typical usage on any page:
Bash | Delivers a crushing blow with a chance to stun the target. |
|---|---|
| General | |
| Max level | 10 |
| Classes | Warrior, Weaver |
| Monsters | Ashrend, Goblin King |
| Requirements | |
| Required skills | Axe Mastery (Lv.1) |
| Type | |
| Damage type | Melee |
| Element | Neutral |
| Target | Enemy |
| Cast type | Target |
| Mechanics | |
| Resource cost | MP: Base: 3 Lv1: 4 Lv2: 5 Lv3: 6 Lv4: 7 Lv5: 8 Lv6: 9 Lv7: 10 Lv8: 11 Lv9: 12 Lv10: 13 |
| Combo | Type: Ready, Duration: 4s |
| Damage and scaling | |
| Main damage | Damage – Base 100%, 25% / Lv, ATK-based |
| Scaling | Strength – 2%, ATK-based |
| Modifiers | |
| Flags | Special: Self Centered |
| Status effects | |
| Applies | Target – Stun (Duration Base 3s, Chance Base: 0% Lv1: 3% Lv2: 6% Lv3: 9% Lv4: 12% Lv5: 15% Lv6: 18% Lv7: 21% Lv8: 24% Lv9: 27% Lv10: 30%) |
or, explicitly:
Bash | Delivers a crushing blow with a chance to stun the target. |
|---|---|
| General | |
| Max level | 10 |
| Classes | Warrior, Weaver |
| Monsters | Ashrend, Goblin King |
| Requirements | |
| Required skills | Axe Mastery (Lv.1) |
| Type | |
| Damage type | Melee |
| Element | Neutral |
| Target | Enemy |
| Cast type | Target |
| Mechanics | |
| Resource cost | MP: Base: 3 Lv1: 4 Lv2: 5 Lv3: 6 Lv4: 7 Lv5: 8 Lv6: 9 Lv7: 10 Lv8: 11 Lv9: 12 Lv10: 13 |
| Combo | Type: Ready, Duration: 4s |
| Damage and scaling | |
| Main damage | Damage – Base 100%, 25% / Lv, ATK-based |
| Scaling | Strength – 2%, ATK-based |
| Modifiers | |
| Flags | Special: Self Centered |
| Status effects | |
| Applies | Target – Stun (Duration Base 3s, Chance Base: 0% Lv1: 3% Lv2: 6% Lv3: 9% Lv4: 12% Lv5: 15% Lv6: 18% Lv7: 21% Lv8: 24% Lv9: 27% Lv10: 30%) |
Internal IDs can still be used when needed:
Unknown skill: Bash_InternalId
This keeps page wikitext simple while centralizing all JSON loading and formatting logic inside Lua.
-- Module:GameSkills
--
-- Renders active skill data (from Data:skills.json) into an infobox-style table
-- and can also list all skills for a given user/class.
--
-- Usage (single skill):
-- {{Skill|Heal}}
-- {{Skill|name=Heal}}
-- {{Skill|id=Heal_InternalId}}
--
-- Usage (auto-list on class page, e.g. "Acolyte"):
-- {{Skill}} -> lists all Acolyte skills (page name)
-- {{Skill|Acolyte}} -> same, if no skill literally called "Acolyte"
local GameData = require("Module:GameData")
local p = {}
----------------------------------------------------------------------
-- Internal helpers
----------------------------------------------------------------------
local skillsCache
local function getSkills()
if not skillsCache then
skillsCache = GameData.loadSkills()
end
return skillsCache
end
local function getArgs(frame)
local parent = frame:getParent()
if parent then
return parent.args
end
return frame.args
end
local function listToText(list, sep)
if type(list) ~= "table" or #list == 0 then
return nil
end
return table.concat(list, sep or ", ")
end
local function addRow(tbl, label, value)
if value == nil or value == "" then
return
end
local row = tbl:tag("tr")
row:tag("th"):wikitext(label):done()
row:tag("td"):wikitext(value):done()
end
local function addSectionHeader(tbl, label)
local row = tbl:tag("tr")
local cell = row:tag("th")
cell:attr("colspan", 2)
cell:addClass("spiritvale-infobox-section-header")
cell:wikitext(label)
end
local function trim(s)
if type(s) ~= "string" then return nil end
s = mw.text.trim(s)
if s == "" then return nil end
return s
end
-- Handles either a scalar OR {Value=..., Unit=...}
local function formatUnitValue(v)
if type(v) == "table" and v.Value ~= nil then
local unit = v.Unit
local val = v.Value
-- Best-effort formatting for common unit types
if unit == "percent_decimal" then
return tostring((val or 0) * 100) .. "%"
elseif unit == "percent_whole" or unit == "percent" then
return tostring(val) .. "%"
elseif unit == "seconds" then
return tostring(val) .. "s"
elseif unit == "meters" then
return tostring(val) .. "m"
elseif unit == "tiles" then
return tostring(val) .. " tiles"
elseif unit and unit ~= "" then
return tostring(val) .. " " .. tostring(unit)
else
return tostring(val)
end
end
if v == nil then
return nil
end
return tostring(v)
end
----------------------------------------------------------------------
-- Lookups
----------------------------------------------------------------------
-- Lookup by Internal Name
local function getSkillById(id)
id = trim(id)
if not id then return nil end
local dataset = getSkills()
local byId = dataset.byId or {}
return byId[id]
end
-- Lookup by display/external Name (for editors)
local function findSkillByName(name)
name = trim(name)
if not name then return nil end
local dataset = getSkills()
-- Fast path if GameData built byName
local byName = dataset.byName or {}
if byName[name] then
return byName[name]
end
-- Fallback scan (older GameData)
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
return nil
end
----------------------------------------------------------------------
-- Formatting helpers
----------------------------------------------------------------------
local function isFlatPerLevel(baseStr, perList)
if type(perList) ~= "table" or #perList == 0 then
return false
end
-- If all per-level entries are identical, treat as "no scaling"
local first = tostring(perList[1])
for i = 2, #perList do
if tostring(perList[i]) ~= first then
return false
end
end
-- If base matches that identical value, definitely flat/no-op
if baseStr ~= nil and tostring(baseStr) == first then
return true
end
-- Even if base differs (sometimes Base is "0" but Lv list is constant), still no meaningful scaling
return true
end
local function formatBasePer(block)
if type(block) ~= "table" then
return nil
end
local base = block.Base
local per = block["Per Level"]
local baseText = (base ~= nil) and formatUnitValue(base) or nil
-- Per Level might be a scalar OR list (wikiprep expansion)
if type(per) == "table" then
if isFlatPerLevel(baseText, per) then
-- Show only Base (or Lv1 if Base is missing)
if baseText then
return "Base: " .. baseText
end
if per[1] ~= nil then
return "Lv1: " .. tostring(per[1])
end
return nil
end
local lines = {}
if baseText then
table.insert(lines, "Base: " .. baseText)
end
for i, v in ipairs(per) do
table.insert(lines, string.format("Lv%d: %s", i, tostring(v)))
end
return table.concat(lines, "<br />")
else
local parts = {}
if baseText then
table.insert(parts, "Base " .. baseText)
end
if per ~= nil then
table.insert(parts, tostring(per) .. " / Lv")
end
if #parts == 0 then
return nil
end
return table.concat(parts, ", ")
end
end
local function formatMainDamage(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local parts = {}
for _, d in ipairs(list) do
if type(d) == "table" then
local kind = d.Type or "Damage"
local base = d["Base %"]
local per = d["Per Level %"]
local seg = kind
local detail = {}
if base ~= nil then
table.insert(detail, string.format("Base %s%%", tostring(base)))
end
if per ~= nil then
table.insert(detail, string.format("%s%% / Lv", tostring(per)))
end
if d["ATK-Based"] then
table.insert(detail, "ATK-based")
end
if d["MATK-Based"] then
table.insert(detail, "MATK-based")
end
if #detail > 0 then
seg = seg .. " – " .. table.concat(detail, ", ")
end
table.insert(parts, seg)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatReflectDamage(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local parts = {}
for _, d in ipairs(list) do
if type(d) == "table" then
local base = d["Base %"]
local per = d["Per Level %"]
local seg = "Reflect"
local detail = {}
if base ~= nil then
table.insert(detail, string.format("Base %s%%", tostring(base)))
end
if per ~= nil then
table.insert(detail, string.format("%s%% / Lv", tostring(per)))
end
if #detail > 0 then
seg = seg .. " – " .. table.concat(detail, ", ")
end
table.insert(parts, seg)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatScaling(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local parts = {}
for _, s in ipairs(list) do
if type(s) == "table" then
local name = s["Scaling Name"] or s["Scaling ID"] or "Unknown"
local pct = s.Percent
local seg = name
local detail = {}
if pct ~= nil then
-- If Unit is percent_decimal, convert; otherwise assume already percent
if s.Unit == "percent_decimal" then
table.insert(detail, string.format("%s%%", tostring((pct or 0) * 100)))
else
table.insert(detail, string.format("%s%%", tostring(pct)))
end
end
if s["ATK-Based"] then
table.insert(detail, "ATK-based")
end
if s["MATK-Based"] then
table.insert(detail, "MATK-based")
end
if #detail > 0 then
seg = seg .. " – " .. table.concat(detail, ", ")
end
table.insert(parts, seg)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatArea(area)
if type(area) ~= "table" then
return nil
end
local parts = {}
local size = area["Area Size"]
if size and size ~= "" then
table.insert(parts, "Size: " .. tostring(size))
end
local dist = area["Area Distance"]
local eff = area["Effective Distance"]
local distText = formatBasePer(dist)
if distText then
table.insert(parts, "Distance: " .. distText)
end
local effText = formatUnitValue(eff)
if effText then
table.insert(parts, "Effective: " .. effText)
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatTimingBlock(bt)
if type(bt) ~= "table" then
return nil
end
local parts = {}
local function add(name, key)
local block = bt[key]
local txt = formatBasePer(block)
if txt then
table.insert(parts, name .. ": " .. txt)
end
end
add("Cast Time", "Cast Time")
add("Cooldown", "Cooldown")
add("Duration", "Duration")
if bt["Effect Cast Time"] ~= nil then
table.insert(parts, "Effect Cast Time: " .. tostring(bt["Effect Cast Time"]))
end
if bt["Damage Delay"] ~= nil then
table.insert(parts, "Damage Delay: " .. tostring(bt["Damage Delay"]))
end
if bt["Effect Remove Delay"] ~= nil then
table.insert(parts, "Effect Remove Delay: " .. tostring(bt["Effect Remove Delay"]))
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatResourceCost(rc)
if type(rc) ~= "table" then
return nil
end
local parts = {}
local manaTxt = formatBasePer(rc["Mana Cost"])
if manaTxt then
table.insert(parts, "MP: " .. manaTxt)
end
local hpTxt = formatBasePer(rc["Health Cost"])
if hpTxt then
table.insert(parts, "HP: " .. hpTxt)
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatCombo(combo)
if type(combo) ~= "table" then
return nil
end
local parts = {}
if combo.Type then
table.insert(parts, "Type: " .. tostring(combo.Type))
end
local durText = formatUnitValue(combo.Duration)
if durText then
table.insert(parts, "Duration: " .. durText)
end
-- Percent may be scalar or {Value,Unit}
if combo.Percent ~= nil then
local pctText = formatUnitValue(combo.Percent)
if pctText then
table.insert(parts, "Bonus: " .. pctText)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, ", ")
end
local function formatMechanicEffects(effects)
if type(effects) ~= "table" then
return nil
end
local keys = {}
for k, _ in pairs(effects) do
table.insert(keys, k)
end
table.sort(keys)
local parts = {}
for _, name in ipairs(keys) do
local block = effects[name]
if type(block) == "table" then
local bp = formatBasePer(block)
local seg = name
if bp then
seg = seg .. " – " .. bp
end
table.insert(parts, seg)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatModifiers(mods)
if type(mods) ~= "table" then
return nil
end
local parts = {}
local function collect(label, sub)
if type(sub) ~= "table" then
return
end
local flags = {}
for k, v in pairs(sub) do
if v then
table.insert(flags, k)
end
end
table.sort(flags)
if #flags > 0 then
table.insert(parts, string.format("%s: %s", label, table.concat(flags, ", ")))
end
end
collect("Movement", mods["Movement Modifiers"])
collect("Combat", mods["Combat Modifiers"])
collect("Special", mods["Special Modifiers"])
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatStatusApplications(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local parts = {}
for _, s in ipairs(list) do
if type(s) == "table" then
local scope = s.Scope or "Target"
local name = s["Status External Name"] or s["Status Internal Name"] or "Unknown status"
local seg = scope .. " – " .. tostring(name)
local detail = {}
local dur = s.Duration
if type(dur) == "table" then
local t = formatBasePer(dur)
if t then
table.insert(detail, "Duration " .. t)
end
end
local ch = s.Chance
if type(ch) == "table" then
local t = formatBasePer(ch)
if t then
table.insert(detail, "Chance " .. t)
end
end
if s["Fixed Duration"] then
table.insert(detail, "Fixed duration")
end
if #detail > 0 then
seg = seg .. " (" .. table.concat(detail, ", ") .. ")"
end
table.insert(parts, seg)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatStatusRemoval(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local parts = {}
for _, r in ipairs(list) do
if type(r) == "table" then
local names = r["Status External Name"]
local label
if type(names) == "table" then
label = table.concat(names, ", ")
elseif type(names) == "string" then
label = names
else
label = "Status"
end
local bp = formatBasePer(r)
local seg = label
if bp then
seg = seg .. " – " .. bp
end
table.insert(parts, seg)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatEvents(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local parts = {}
for _, ev in ipairs(list) do
if type(ev) == "table" then
local action = ev.Action or "On event"
local name = ev["Skill Internal Name"] or ev["Skill External Name"] or "Unknown skill"
local seg = string.format("%s → %s", action, name)
table.insert(parts, seg)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
----------------------------------------------------------------------
-- User matching (for auto lists on class pages)
----------------------------------------------------------------------
local function skillMatchesUser(rec, userName)
if type(rec) ~= "table" or not userName or userName == "" then
return false
end
local users = rec.Users
if type(users) ~= "table" then
return false
end
local userLower = mw.ustring.lower(userName)
local function listHas(list)
if type(list) ~= "table" then
return false
end
for _, v in ipairs(list) do
if type(v) == "string" and mw.ustring.lower(v) == userLower then
return true
end
end
return false
end
if listHas(users.Classes) then return true end
if listHas(users.Summons) then return true end
if listHas(users.Monsters) then return true end
if listHas(users.Events) then return true end
return false
end
----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------
local function buildInfobox(rec)
local root = mw.html.create("table")
root:addClass("wikitable spiritvale-skill-infobox")
-- ==========================================================
-- Top "hero" row: icon + name (left), description (right)
-- ==========================================================
local icon = rec.Icon
local title = rec["External Name"] or rec.Name or rec["Internal Name"] or "Unknown Skill"
local desc = rec.Description or ""
local headerRow = root:tag("tr")
headerRow:addClass("spiritvale-infobox-main")
-- Left cell: icon + name
local leftCell = headerRow:tag("th")
leftCell:addClass("spiritvale-infobox-main-left")
local leftInner = leftCell:tag("div")
leftInner:addClass("spiritvale-infobox-main-left-inner")
if icon and icon ~= "" then
leftInner:wikitext(string.format("[[File:%s|80px|link=]]", icon))
end
leftInner:tag("div")
:addClass("spiritvale-infobox-title")
:wikitext(title)
-- Right cell: italic description
local rightCell = headerRow:tag("td")
rightCell:addClass("spiritvale-infobox-main-right")
local rightInner = rightCell:tag("div")
rightInner:addClass("spiritvale-infobox-main-right-inner")
if desc ~= "" then
rightInner:tag("div")
:addClass("spiritvale-infobox-description")
:wikitext(string.format("''%s''", desc))
end
------------------------------------------------------------------
-- General
------------------------------------------------------------------
addSectionHeader(root, "General")
addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"]))
local users = rec.Users or {}
addRow(root, "Classes", listToText(users.Classes))
addRow(root, "Summons", listToText(users.Summons))
addRow(root, "Monsters", listToText(users.Monsters))
addRow(root, "Events", listToText(users.Events))
------------------------------------------------------------------
-- Requirements
------------------------------------------------------------------
local req = rec.Requirements or {}
if (req["Required Skills"] and #req["Required Skills"] > 0)
or (req["Required Weapons"] and #req["Required Weapons"] > 0)
or (req["Required Stances"] and #req["Required Stances"] > 0) then
addSectionHeader(root, "Requirements")
if type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0 then
local skillParts = {}
for _, rs in ipairs(req["Required Skills"]) do
local name = rs["Skill External Name"] or rs["Skill Internal Name"] or "Unknown"
local level = rs["Required Level"]
if level then
table.insert(skillParts, string.format("%s (Lv.%s)", name, level))
else
table.insert(skillParts, name)
end
end
addRow(root, "Required skills", table.concat(skillParts, ", "))
end
addRow(root, "Required weapons", listToText(req["Required Weapons"]))
addRow(root, "Required stances", listToText(req["Required Stances"]))
end
------------------------------------------------------------------
-- Type
------------------------------------------------------------------
local typeBlock = rec.Type or {}
if next(typeBlock) ~= nil then
addSectionHeader(root, "Type")
local dt = typeBlock["Damage Type"]
if type(dt) == "table" and dt.Name then
addRow(root, "Damage type", dt.Name)
end
local et = typeBlock["Element Type"]
if type(et) == "table" and et.Name then
addRow(root, "Element", et.Name)
end
local tt = typeBlock["Target Type"]
if type(tt) == "table" and tt.Name then
addRow(root, "Target", tt.Name)
end
local ct = typeBlock["Cast Type"]
if type(ct) == "table" and ct.Name then
addRow(root, "Cast type", ct.Name)
end
end
------------------------------------------------------------------
-- Mechanics
------------------------------------------------------------------
local mech = rec.Mechanics or {}
if next(mech) ~= nil then
addSectionHeader(root, "Mechanics")
local rangeText = formatUnitValue(mech.Range)
addRow(root, "Range", rangeText)
local areaText = formatArea(mech.Area)
addRow(root, "Area", areaText)
if mech["Autocast Multiplier"] ~= nil then
addRow(root, "Autocast multiplier", tostring(mech["Autocast Multiplier"]))
end
local btText = formatTimingBlock(mech["Basic Timings"])
addRow(root, "Timing", btText)
local rcText = formatResourceCost(mech["Resource Cost"])
addRow(root, "Resource cost", rcText)
local comboText = formatCombo(mech.Combo)
addRow(root, "Combo", comboText)
local effText = formatMechanicEffects(mech.Effects)
addRow(root, "Special mechanics", effText)
end
------------------------------------------------------------------
-- Damage & Healing
------------------------------------------------------------------
local dmg = rec.Damage or {}
if next(dmg) ~= nil then
addSectionHeader(root, "Damage and scaling")
if dmg["Healing Present"] then
addRow(root, "Healing", "Yes")
end
local mainText = formatMainDamage(dmg["Main Damage"])
addRow(root, "Main damage", mainText)
local reflText = formatReflectDamage(dmg["Reflect Damage"])
addRow(root, "Reflect damage", reflText)
local scaleText = formatScaling(dmg.Scaling)
addRow(root, "Scaling", scaleText)
end
------------------------------------------------------------------
-- Modifiers
------------------------------------------------------------------
local modsText = formatModifiers(rec.Modifiers)
if modsText then
addSectionHeader(root, "Modifiers")
addRow(root, "Flags", modsText)
end
------------------------------------------------------------------
-- Status
------------------------------------------------------------------
local statusApps = formatStatusApplications(rec["Status Applications"])
local statusRem = formatStatusRemoval(rec["Status Removal"])
if statusApps or statusRem then
addSectionHeader(root, "Status effects")
addRow(root, "Applies", statusApps)
addRow(root, "Removes", statusRem)
end
------------------------------------------------------------------
-- Events
------------------------------------------------------------------
local eventsText = formatEvents(rec.Events)
if eventsText then
addSectionHeader(root, "Events")
addRow(root, "Triggers", eventsText)
end
------------------------------------------------------------------
-- Notes
------------------------------------------------------------------
if type(rec.Notes) == "table" and #rec.Notes > 0 then
addSectionHeader(root, "Notes")
addRow(root, "Notes", table.concat(rec.Notes, "<br />"))
end
return tostring(root)
end
----------------------------------------------------------------------
-- Public: list all skills for a given user/class
----------------------------------------------------------------------
function p.listForUser(frame)
local args = getArgs(frame)
-- Prefer explicit param, then unnamed, then fall back to the current page name.
local userName = args.user or args[1]
if not userName or userName == "" then
userName = mw.title.getCurrentTitle().text
end
if not userName or userName == "" then
return "<strong>No user name provided to Skill list.</strong>"
end
local dataset = getSkills()
local matches = {}
for _, rec in ipairs(dataset.records or {}) do
if skillMatchesUser(rec, userName) then
table.insert(matches, rec)
end
end
if #matches == 0 then
return string.format(
"<strong>No skills found for:</strong> %s",
mw.text.nowiki(userName)
)
end
local root = mw.html.create("div")
root:addClass("spiritvale-skill-list")
for _, rec in ipairs(matches) do
root:wikitext(buildInfobox(rec))
end
return tostring(root)
end
----------------------------------------------------------------------
-- Public: single-skill or auto-list dispatcher
----------------------------------------------------------------------
function p.infobox(frame)
local args = getArgs(frame)
-- Allow three styles:
-- {{Skill|Bash}} -> args[1] = "Bash" (External Name)
-- {{Skill|name=Bash}} -> args.name = "Bash"
-- {{Skill|id=Bash_Internal}} -> args.id = "Bash_Internal"
local raw1 = args[1]
local name = args.name or raw1
local id = args.id
local rec
-- 1) Prefer External/Display Name
if name and name ~= "" then
rec = findSkillByName(name)
end
-- 2) Fallback: internal ID
if not rec and id and id ~= "" then
rec = getSkillById(id)
end
-- 3) If still nothing, decide if this is "list mode" or truly unknown.
if not rec then
local pageTitle = mw.title.getCurrentTitle()
local pageName = pageTitle and pageTitle.text or ""
local noExplicitArgs =
(not raw1 or raw1 == "") and
(not args.name or args.name == "") and
(not id or id == "")
-- Case A: {{Skill}} with no parameters on a page → list for that page name.
if noExplicitArgs then
return p.listForUser(frame)
end
-- Case B: {{Skill|Acolyte}} on the "Acolyte" page and no id → treat as list.
if name and name ~= "" and name == pageName and (not id or id == "") then
return p.listForUser(frame)
end
-- Otherwise, genuinely unknown skill.
local label = name or id or "?"
return string.format(
"<strong>Unknown skill:</strong> %s[[Category:Pages with unknown skill|%s]]",
mw.text.nowiki(label),
label
)
end
return buildInfobox(rec)
end
return p