Module:GamePassives: Difference between revisions
From SpiritVale Wiki
More actions
No edit summary |
No edit summary |
||
| Line 24: | Line 24: | ||
local function getPassives() | local function getPassives() | ||
if not passivesCache then | |||
passivesCache = GameData.loadPassives() | |||
end | |||
return passivesCache | |||
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 | ||
-- Tag body rows so we can style/center without touching the hero | -- Tag body rows so we can style/center without touching the hero rows | ||
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:addClass("spiritvale-passive-body-row") | |||
row:tag("th"):wikitext(label):done() | |||
row:tag("td"):wikitext(value):done() | |||
end | end | ||
-- Tag section header rows as body rows too (for centering) | -- Tag section header rows as body rows too (for centering) | ||
local function addSectionHeader(tbl, label) | 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 | ||
-- Lookup by Internal Name | -- Lookup by Internal Name | ||
local function getPassiveById(id) | local function getPassiveById(id) | ||
if not id or id == "" then | |||
return nil | |||
end | |||
local dataset = getPassives() | |||
local byId = dataset.byId or {} | |||
return byId[id] | |||
end | end | ||
-- Lookup by display Name (for editors) | -- Lookup by display Name (for editors) | ||
local function findPassiveByName(name) | local function findPassiveByName(name) | ||
if not name or name == "" then | |||
return nil | |||
end | |||
local dataset = getPassives() | |||
for _, rec in ipairs(dataset.records or {}) do | |||
if rec["Name"] == name then | |||
return rec | |||
end | |||
end | |||
return nil | |||
end | end | ||
| Line 96: | Line 97: | ||
local function asUl(items) | local function asUl(items) | ||
if type(items) ~= "table" or #items == 0 then | |||
return nil | |||
end | |||
return '<ul class="spiritvale-infobox-list"><li>' | |||
.. table.concat(items, "</li><li>") | |||
.. "</li></ul>" | |||
end | end | ||
local function formatBasePer(block) | local function formatBasePer(block) | ||
if type(block) ~= "table" then | |||
return nil | |||
end | |||
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 | end | ||
-- Passive Effects: return rows (label/value), not a single text blob | -- Passive Effects: return rows (label/value), not a single text blob | ||
local function passiveEffectRows(list) | local function passiveEffectRows(list) | ||
if type(list) ~= "table" or #list == 0 then | |||
return {} | |||
end | |||
local rows = {} | |||
for _, eff in ipairs(list) do | |||
if type(eff) == "table" then | |||
local t = eff.Type or {} | |||
local name = t.Name or eff.ID or "Unknown" | |||
local value = eff.Value or {} | |||
local detail = {} | |||
if value.Base ~= nil then | |||
table.insert(detail, string.format("Base %s", tostring(value.Base))) | |||
end | |||
if value["Per Level"] ~= nil then | |||
table.insert(detail, string.format("%s / Lv", tostring(value["Per Level"]))) | |||
end | |||
if value.Expression ~= nil and value.Expression ~= "" then | |||
table.insert(detail, tostring(value.Expression)) | |||
end | |||
-- 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 | |||
local right = (#detail > 0) and table.concat(detail, ", ") or "—" | |||
table.insert(rows, { label = name, value = right }) | |||
end | |||
end | |||
return rows | |||
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 Name"] or s["Status ID"] or "Unknown status" | |||
local seg = scope .. " – " .. 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 | |||
return asUl(parts) | |||
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 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 | 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 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 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"]) | |||
return asUl(parts) | |||
end | end | ||
| Line 290: | Line 293: | ||
local function passiveMatchesUser(rec, userName) | 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 326: | Line 329: | ||
local function buildInfobox(rec) | local function buildInfobox(rec) | ||
local root = mw.html.create("table") | |||
root:addClass("spiritvale-passive-infobox") | |||
-- ========================================================== | |||
-- Top "hero" rows: title row (icon + name), then description row | |||
-- ========================================================== | |||
local icon = rec.Icon | |||
local title = rec.Name or rec["Internal Name"] or "Unknown Passive" | |||
local desc = rec.Description or "" | |||
-- Row 1: centered icon + title (single cell) | |||
local titleRow = root:tag("tr") | |||
titleRow:addClass("spiritvale-infobox-main") | |||
titleRow:addClass("sv-hero-title-row") | |||
local titleCell = titleRow:tag("th") | |||
titleCell:attr("colspan", 2) | |||
local titleInner = titleCell:tag("div") | |||
titleInner:addClass("spiritvale-infobox-main-left-inner") | |||
if icon and icon ~= "" then | |||
titleInner:wikitext(string.format("[[File:%s|80px|link=]]", icon)) | |||
end | |||
titleInner:tag("div") | |||
:addClass("spiritvale-infobox-title") | |||
:wikitext(title) | |||
-- Row 2: description (single cell) | |||
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) | |||
local descInner = descCell:tag("div") | |||
descInner:addClass("spiritvale-infobox-main-right-inner") | |||
-- Use <i> to avoid apostrophes breaking ''...'' formatting | |||
local d = descInner:tag("div") | |||
d:addClass("spiritvale-infobox-description") | |||
d:tag("i"):wikitext(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 {} | |||
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 sname = 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)", sname, level)) | |||
else | |||
table.insert(skillParts, sname) | |||
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 | |||
------------------------------------------------------------------ | |||
-- 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 modsText = formatModifiers(rec.Modifiers) | |||
if modsText then | |||
addSectionHeader(root, "Modifiers") | |||
addRow(root, "Flags", modsText) | |||
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", asUl(rec.Notes) or table.concat(rec.Notes, "<br />")) | |||
end | |||
return tostring(root) | |||
end | end | ||
| Line 470: | Line 472: | ||
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 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) | |||
end | end | ||
| Line 513: | Line 515: | ||
function p.infobox(frame) | function p.infobox(frame) | ||
local args = getArgs(frame) | |||
local raw1 = args[1] | |||
local name = args.name or raw1 | |||
local id = args.id | |||
local rec | |||
-- 1) Prefer display Name | |||
if name and name ~= "" then | |||
rec = findPassiveByName(name) | |||
end | |||
-- 2) Fallback: internal ID | |||
if not rec and id and id ~= "" then | |||
rec = getPassiveById(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 == "") | |||
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 "?" | |||
return string.format( | |||
"<strong>Unknown passive:</strong> %s[[Category:Pages with unknown passive|%s]]", | |||
mw.text.nowiki(label), | |||
label | |||
) | |||
end | |||
return buildInfobox(rec) | |||
end | end | ||
return p | return p | ||