Module:GamePassives: Difference between revisions
More actions
No edit summary |
No edit summary |
||
| 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 | ||
| Line 48: | Line 52: | ||
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 | |||
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 | ||
| Line 77: | Line 89: | ||
-- Formatting helpers | -- Formatting helpers | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
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 | |||
local function formatPassiveEffects(list) | local function formatPassiveEffects(list) | ||
| Line 87: | Line 116: | ||
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 value | local name = t.Name or eff.ID or "Unknown" | ||
local value = eff.Value or {} | |||
local | local detail = {} | ||
if | if value.Base ~= nil then | ||
table.insert(detail, string.format("Base %s", tostring(value.Base))) | |||
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 | ||
local seg = name | |||
if #detail > 0 then | if #detail > 0 then | ||
seg = seg .. " – " .. table.concat(detail, ", ") | seg = seg -- name | ||
.. " – " .. table.concat(detail, ", ") | |||
end | end | ||
| Line 120: | Line 146: | ||
end | end | ||
return table.concat(parts, "<br />") | return table.concat(parts, "<br />") | ||
end | end | ||
| Line 130: | Line 155: | ||
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 dur | |||
local | 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 | |||
if | table.insert(detail, "Fixed duration") | ||
end | end | ||
if #detail > 0 then | |||
if | seg = seg .. " (" .. table.concat(detail, ", ") .. ")" | ||
end | end | ||
if | |||
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 Name"] | |||
local label | |||
if type(names) == "table" then | |||
label = table.concat(names, ", ") | |||
elseif type(names) == "string" then | |||
label = names | |||
else | |||
label = "Status" | |||
end | end | ||
if | local bp = formatBasePer(r) | ||
seg = seg .. " | local seg = label | ||
if bp then | |||
seg = seg .. " – " .. bp | |||
end | 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 Name"] or ev["Skill ID"] or "Unknown skill" | |||
local seg = string.format("%s → %s", action, name) | |||
table.insert(parts, seg) | table.insert(parts, seg) | ||
end | end | ||
| Line 165: | Line 255: | ||
end | end | ||
local function | local function formatModifiers(mods) | ||
if type( | 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 | return nil | ||
end | end | ||
return table.concat( | |||
return table.concat(parts, "<br />") | |||
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 194: | Line 347: | ||
titleText = titleText .. title | titleText = titleText .. title | ||
headerCell:wikitext(titleText) | headerCell:wikitext(titleText) | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
-- | -- General | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
addSectionHeader(root, "General") | |||
addRow(root, "Description", rec.Description) | addRow(root, "Description", rec.Description) | ||
addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"])) | addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"])) | ||
local users = rec.Users or {} | local users = rec.Users or {} | ||
addRow(root, "Classes", listToText(users.Classes)) | addRow(root, "Classes", listToText(users.Classes)) | ||
| Line 215: | Line 365: | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
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 | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
-- Passive effects | -- Passive effects | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
local | local peText = formatPassiveEffects(rec["Passive Effects"]) | ||
if peText then | |||
addSectionHeader(root, "Passive effects") | |||
addRow(root, "Effects", peText) | |||
end | |||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
-- | -- Modifiers | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
local | local modsText = formatModifiers(rec.Modifiers) | ||
if modsText then | |||
addSectionHeader(root, "Modifiers") | |||
addRow(root, "Flags", modsText) | |||
end | |||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
-- | -- Status | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
local | 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 | end | ||
local | ------------------------------------------------------------------ | ||
if | -- Events | ||
------------------------------------------------------------------ | |||
local eventsText = formatEvents(rec.Events) | |||
if eventsText then | |||
addSectionHeader(root, "Events") | |||
addRow(root, "Triggers", eventsText) | |||
end | end | ||
------------------------------------------------------------------ | |||
-- Notes | |||
------------------------------------------------------------------ | |||
if type(rec.Notes) == "table" and #rec.Notes > 0 then | |||
addSectionHeader(root, "Notes") | |||
addRow(root, "Notes", table.concat(rec.Notes, "<br />")) | |||
end | end | ||
return tostring(root) | |||
end | |||
---------------------------------------------------------------------- | |||
-- Public: list all passives for a given user/class | |||
---------------------------------------------------------------------- | |||
function p.listForUser(frame) | function p.listForUser(frame) | ||
local args = getArgs(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] | local userName = args.user or args[1] | ||
if not userName or userName == "" then | if not userName or userName == "" then | ||
| Line 327: | Line 482: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- Public | -- Public: single-passive or auto-list dispatcher | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
| Line 343: | Line 498: | ||
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 | -- 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 pageTitle = mw.title.getCurrentTitle() | ||
Revision as of 22:20, 12 December 2025
Module:GamePassives
Module:GamePassives renders passive skill data from Data:passives.json into a reusable infobox-style table.
It is intended to be used via a template (for example Template:Passive) so that passives can be embedded on any page without creating individual pages for each passive.
This module:
- Loads data via Module:GameData →
GameData.loadPassives(). - Looks up passives primarily by display
"Name"(what editors use), with"Internal Name"as an optional fallback. - Builds a table with only the fields that actually exist for that passive.
Data source
Passive data comes from Data:passives.json, which is a JSON page with this top-level structure (see Module:GameData/doc for full details):
{
"version": "SpiritVale-0.9.3",
"schema_version": 1,
"generated_at": "2025-12-12T17:24:05.784284+00:00",
"records": [
{
"Name": "Honed Blade",
"Internal Name": "CritMastery",
"...": "other fields specific to passives"
}
]
}
Each record is a single passive. 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.
Common fields include:
IconDescriptionMax LevelUsers(e.g. Classes)RequirementsPassive EffectsStatus ApplicationsNotes
Output
For a given passive, the module renders a table with the CSS class spiritvale-passive-infobox.
Depending on what exists in the JSON record, the table may include:
- Header row with passive name (and icon, if present).
- Description.
- Max level.
- Users:
- Classes
- Summons
- Monsters
- Events
- Requirements:
- Required Skills (with required level)
- Required Weapons
- Required Stances
- Passive effects:
- Each entry from
"Passive Effects"(Type name, optional Expression, Base and Per Level where present)
- Each entry from
- Status interactions:
- Status Applications (if any)
- Notes:
- Any entries from
"Notes"(one per line)
- Any entries from
Rows are only shown if the underlying field exists in the JSON for that passive.
Public interface
The module exposes a single entry point for templates:
GamePassives.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 passive"Name".name– explicit display"Name"of the passive (equivalent to1).id–"Internal Name"of the passive (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 passives found for: GamePassives
or:
No passives found for: GamePassives
Template:Passive
The recommended way to use this module is via a small wrapper template, for example:
Template:Passive
No passives found for: GamePassives
Typical usage on any page:
| General | |
|---|---|
| Description | A life of combat has perfected your edge, each critical strike hits with ruthless efficiency. |
| Max level | 10 |
| Classes | Warrior |
| Passive effects | |
| Effects | Critical Damage – 3 / Lv |
or, explicitly:
| General | |
|---|---|
| Description | A life of combat has perfected your edge, each critical strike hits with ruthless efficiency. |
| Max level | 10 |
| Classes | Warrior |
| Passive effects | |
| Effects | Critical Damage – 3 / Lv |
Internal IDs can still be used when needed:
| General | |
|---|---|
| Description | A life of combat has perfected your edge, each critical strike hits with ruthless efficiency. |
| Max level | 10 |
| Classes | Warrior |
| Passive effects | |
| Effects | Critical Damage – 3 / Lv |
This keeps page wikitext simple while centralizing all JSON loading and formatting logic inside Lua.
-- Module:GamePassives
--
-- 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|name=Honed Blade}}
-- {{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 p = {}
----------------------------------------------------------------------
-- Internal helpers
----------------------------------------------------------------------
local passivesCache
local function getPassives()
if not passivesCache then
passivesCache = GameData.loadPassives()
end
return passivesCache
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
-- Lookup by Internal Name
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
-- Lookup by display Name (for editors)
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
----------------------------------------------------------------------
-- Formatting helpers
----------------------------------------------------------------------
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
local function formatPassiveEffects(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local parts = {}
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
local seg = name
if #detail > 0 then
seg = seg -- name
.. " – " .. 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 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
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 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 Name"] or ev["Skill ID"] 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
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
----------------------------------------------------------------------
-- 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
----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------
local function buildInfobox(rec)
local root = mw.html.create("table")
root:addClass("wikitable spiritvale-passive-infobox")
-- Header: icon + name
local icon = rec.Icon
local title = rec.Name or rec["Internal Name"] or "Unknown Passive"
local header = root:tag("tr")
local headerCell = header:tag("th")
headerCell:attr("colspan", 2)
local titleText = ""
if icon and icon ~= "" then
titleText = string.format("[[File:%s|64px|link=]] ", icon)
end
titleText = titleText .. title
headerCell:wikitext(titleText)
------------------------------------------------------------------
-- General
------------------------------------------------------------------
addSectionHeader(root, "General")
addRow(root, "Description", rec.Description)
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 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
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
------------------------------------------------------------------
local peText = formatPassiveEffects(rec["Passive Effects"])
if peText then
addSectionHeader(root, "Passive effects")
addRow(root, "Effects", peText)
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 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)
end
----------------------------------------------------------------------
-- Public: single-passive or auto-list dispatcher
----------------------------------------------------------------------
function p.infobox(frame)
local args = getArgs(frame)
-- Allow:
-- {{Passive|Honed Blade}} -> args[1] = "Honed Blade" (Name)
-- {{Passive|name=Honed Blade}} -> args.name
-- {{Passive|id=CritMastery}} -> args.id (Internal Name)
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 == "")
-- Case A: {{Passive}} with no parameters on a page → list for that page name.
if noExplicitArgs then
return p.listForUser(frame)
end
-- Case B: {{Passive|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 passive.
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
-- Normal single-passive behavior
return buildInfobox(rec)
end
return p