Module:GamePassives: Difference between revisions
From SpiritVale Wiki
More actions
m Protected "Module:GamePassives" ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite)) [cascading] |
No edit summary |
||
| (8 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- Module:GamePassives | -- Module:GamePassives | ||
-- | -- | ||
-- Renders passive skill data (from Data:passives.json) into an infobox-style table | -- Renders passive skill data (from Data:passives.json) into an | ||
-- infobox-style table and can also list all passives for a given user/class. | |||
-- | -- | ||
-- | -- Usage (single passive): | ||
-- {{Passive|Honed Blade}} | -- {{Passive|Honed Blade}} | ||
-- {{Passive|name=Honed Blade}} | -- {{Passive|name=Honed Blade}} | ||
-- {{Passive|id=CritMastery}} | -- {{Passive|id=CritMastery}} | ||
-- | |||
-- Usage (auto-list on class page, e.g. "Acolyte"): | |||
-- {{Passive}} -> lists all Acolyte passives (page name) | |||
-- {{Passive|Acolyte}} -> same, if no passive literally called "Acolyte" | |||
local GameData = require("Module:GameData") | local GameData = require("Module:GameData") | ||
| Line 34: | Line 38: | ||
end | end | ||
local function listToText(list) | 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, ", ") | return table.concat(list, sep or ", ") | ||
end | end | ||
-- Tag body rows so we can style/center without touching the hero row | |||
local function addRow(tbl, label, value) | local function addRow(tbl, label, value) | ||
if value == nil or value == "" then | if value == nil or value == "" then | ||
| Line 46: | Line 51: | ||
end | end | ||
local row = tbl:tag("tr") | local row = tbl:tag("tr") | ||
row:addClass("spiritvale-passive-body-row") | |||
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 | |||
-- Tag section header rows as body rows too (for centering) | |||
local function addSectionHeader(tbl, label) | |||
local row = tbl:tag("tr") | |||
row:addClass("spiritvale-passive-body-row") | |||
local cell = row:tag("th") | |||
cell:attr("colspan", 2) | |||
cell:addClass("spiritvale-infobox-section-header") | |||
cell:wikitext(label) | |||
end | end | ||
| Line 78: | Line 95: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local function | local function asUl(items) | ||
if type( | if type(items) ~= "table" or #items == 0 then | ||
return nil | return nil | ||
end | end | ||
return '<ul class="spiritvale-infobox-list"><li>' | |||
.. table.concat(items, "</li><li>") | |||
.. "</li></ul>" | |||
end | |||
local function formatBasePer(block) | |||
if type(block) ~= "table" then | |||
return nil | |||
end | |||
local parts = {} | local parts = {} | ||
if block.Base ~= nil then | |||
table.insert(parts, string.format("Base %s", tostring(block.Base))) | |||
end | |||
if block["Per Level"] ~= nil then | |||
table.insert(parts, string.format("%s / Lv", tostring(block["Per Level"]))) | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
return table.concat(parts, ", ") | |||
end | |||
-- Passive Effects: return rows (label/value), not a single text blob | |||
local function passiveEffectRows(list) | |||
if type(list) ~= "table" or #list == 0 then | |||
return {} | |||
end | |||
local rows = {} | |||
for _, eff in ipairs(list) do | for _, eff in ipairs(list) do | ||
if type(eff) == "table" then | if type(eff) == "table" then | ||
local | local t = eff.Type or {} | ||
local name = t.Name or eff.ID or "Unknown" | |||
local | local value = eff.Value or {} | ||
local detail = {} | |||
local | |||
if value.Base ~= nil then | |||
table.insert(detail, string.format("Base %s", tostring(value.Base))) | |||
if | |||
end | end | ||
if value["Per Level"] ~= nil then | |||
table.insert(detail, string.format("%s / Lv", tostring(value["Per Level"]))) | |||
table.insert(detail, string.format(" | |||
end | end | ||
if | if value.Expression ~= nil and value.Expression ~= "" then | ||
table.insert(detail, | table.insert(detail, tostring(value.Expression)) | ||
end | end | ||
if | -- Optional qualifiers (weapon/stance/etc.), if present in data | ||
local qual = eff.Weapon or eff["Weapon"] or eff["Weapon Type"] | |||
or eff.Stance or eff["Stance"] or eff["Stance Type"] | |||
if type(qual) == "string" and qual ~= "" then | |||
table.insert(detail, qual) | |||
end | end | ||
table.insert( | local right = (#detail > 0) and table.concat(detail, ", ") or "—" | ||
table.insert(rows, { label = name, value = right }) | |||
end | end | ||
end | end | ||
return rows | |||
end | end | ||
| Line 130: | Line 168: | ||
local parts = {} | local parts = {} | ||
for _, s in ipairs(list) do | for _, s in ipairs(list) do | ||
if type(s) == "table" then | if type(s) == "table" then | ||
local scope | local scope = s.Scope or "Target" | ||
local name | local name = s["Status Name"] or s["Status ID"] or "Unknown status" | ||
local | |||
local | local seg = scope .. " – " .. name | ||
local detail = {} | |||
local | local dur = s.Duration | ||
if | if type(dur) == "table" then | ||
local t = formatBasePer(dur) | |||
if t then | |||
table.insert(detail, "Duration " .. t) | |||
end | |||
end | end | ||
local | local ch = s.Chance | ||
if | if type(ch) == "table" then | ||
table.insert( | local t = formatBasePer(ch) | ||
if t then | |||
table.insert(detail, "Chance " .. t) | |||
end | |||
end | end | ||
if | |||
table.insert( | if s["Fixed Duration"] then | ||
table.insert(detail, "Fixed duration") | |||
end | end | ||
if # | if #detail > 0 then | ||
seg = seg .. " (" .. table.concat( | seg = seg .. " (" .. table.concat(detail, ", ") .. ")" | ||
end | end | ||
| Line 158: | Line 205: | ||
end | end | ||
if #parts == 0 then | return asUl(parts) | ||
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 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 | |||
return asUl(parts) | |||
end | |||
local function formatEvents(list) | |||
if type(list) ~= "table" or #list == 0 then | |||
return nil | return nil | ||
end | 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 Name"] or ev["Skill ID"] or "Unknown skill" | |||
table.insert(parts, string.format("%s → %s", action, name)) | |||
end | |||
end | |||
return asUl(parts) | |||
end | end | ||
local function | local function formatModifiers(mods) | ||
if type( | if type(mods) ~= "table" then | ||
return nil | return nil | ||
end | end | ||
return table.concat( | |||
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"]) | |||
return asUl(parts) | |||
end | |||
---------------------------------------------------------------------- | |||
-- User matching (for auto lists on class pages) | |||
---------------------------------------------------------------------- | |||
local function passiveMatchesUser(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 178: | Line 327: | ||
local function buildInfobox(rec) | local function buildInfobox(rec) | ||
local root = mw.html.create("table") | local root = mw.html.create("table") | ||
root:addClass(" | root:addClass("spiritvale-passive-infobox") | ||
-- | -- ========================================================== | ||
-- Top "hero" row: icon + name (left), description (right) | |||
-- ========================================================== | |||
local icon = rec.Icon | local icon = rec.Icon | ||
local title = rec.Name or rec["Internal Name"] or "Unknown Passive" | local title = rec.Name or rec["Internal Name"] or "Unknown Passive" | ||
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 | local leftInner = leftCell:tag("div") | ||
leftInner:addClass("spiritvale-infobox-main-left-inner") | |||
if icon and icon ~= "" then | if icon and icon ~= "" then | ||
leftInner:wikitext(string.format("[[File:%s|80px|link=]]", icon)) | |||
end | 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"])) | |||
-- | -- Classes intentionally removed (template is used on class pages) | ||
local users = rec.Users or {} | local users = rec.Users or {} | ||
addRow(root, "Summons", listToText(users.Summons)) | addRow(root, "Summons", listToText(users.Summons)) | ||
addRow(root, "Monsters", listToText(users.Monsters)) | addRow(root, "Monsters", listToText(users.Monsters)) | ||
| Line 215: | Line 384: | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
local req = rec.Requirements or {} | 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 Name"] or rs["Skill ID"] 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 | end | ||
addRow(root, "Required Skills", table.concat(skillParts, ", ")) | |||
end | end | ||
addRow(root, "Required | |||
addRow(root, "Required Weapons", listToText(req["Required Weapons"])) | |||
addRow(root, "Required Stances", listToText(req["Required Stances"])) | |||
end | end | ||
------------------------------------------------------------------ | |||
addRow(root, " | -- Passive Effects (one row per effect) | ||
------------------------------------------------------------------ | |||
local peRows = passiveEffectRows(rec["Passive Effects"]) | |||
if #peRows > 0 then | |||
addSectionHeader(root, "Passive Effects") | |||
for _, r in ipairs(peRows) do | |||
addRow(root, r.label, r.value) | |||
end | |||
end | |||
------------------------------------------------------------------ | |||
-- Status Effects | |||
------------------------------------------------------------------ | |||
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 | |||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
-- | -- Modifiers | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
local | local modsText = formatModifiers(rec.Modifiers) | ||
if modsText then | |||
addSectionHeader(root, "Modifiers") | |||
addRow(root, "Flags", modsText) | |||
end | |||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
-- | -- Events | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
local | local eventsText = formatEvents(rec.Events) | ||
if eventsText then | |||
addSectionHeader(root, "Events") | |||
addRow(root, "Triggers", eventsText) | |||
end | |||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
-- Notes | -- Notes | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
if type(rec.Notes) == "table" and #rec.Notes > 0 then | |||
addSectionHeader(root, "Notes") | |||
addRow(root, "Notes", asUl(rec.Notes) or table.concat(rec.Notes, "<br />")) | |||
end | |||
return tostring(root) | |||
end | |||
---------------------------------------------------------------------- | |||
-- Public: list all passives 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 Passive list.</strong>" | |||
end | |||
local dataset = getPassives() | |||
local matches = {} | |||
for _, rec in ipairs(dataset.records or {}) do | |||
if passiveMatchesUser(rec, userName) then | |||
table.insert(matches, rec) | |||
end | |||
end | |||
if #matches == 0 then | |||
return string.format( | |||
"<strong>No passives found for:</strong> %s", | |||
mw.text.nowiki(userName) | |||
) | |||
end | |||
local root = mw.html.create("div") | |||
root:addClass("spiritvale-passive-list") | |||
for _, rec in ipairs(matches) do | |||
root:wikitext(buildInfobox(rec)) | |||
end | |||
return tostring(root) | return tostring(root) | ||
| Line 255: | Line 503: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- Public | -- Public: single-passive or auto-list dispatcher | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
| Line 261: | Line 509: | ||
local args = getArgs(frame) | local args = getArgs(frame) | ||
local raw1 = args[1] | local raw1 = args[1] | ||
local name = args.name or raw1 | local name = args.name or raw1 | ||
| Line 271: | Line 515: | ||
local rec | local rec | ||
-- 1) Prefer display Name | -- 1) Prefer display Name | ||
if name and name ~= "" then | if name and name ~= "" then | ||
rec = findPassiveByName(name) | rec = findPassiveByName(name) | ||
end | end | ||
-- 2) Fallback: | -- 2) Fallback: internal ID | ||
if not rec and id and id ~= "" then | if not rec and id and id ~= "" then | ||
rec = getPassiveById(id) | rec = getPassiveById(id) | ||
end | end | ||
-- 3) If still nothing, decide if this is "list mode" or truly unknown. | |||
if not rec then | 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 == "") | |||
if noExplicitArgs then | |||
return p.listForUser(frame) | |||
end | |||
if name and name ~= "" and name == pageName and (not id or id == "") then | |||
return p.listForUser(frame) | |||
end | |||
local label = name or id or "?" | local label = name or id or "?" | ||
return string.format( | return string.format( | ||