Module:GamePassives
More actions
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):
Unknown passive: ?
or:
Unknown passive: ?
Template:Passive
The recommended way to use this module is via a small wrapper template, for example:
Template:Passive
Unknown passive: ?
Typical usage on any page:
Lua error at line 194: attempt to perform arithmetic on local 'titleText' (a string value).
or, explicitly:
Lua error at line 194: attempt to perform arithmetic on local 'titleText' (a string value).
Internal IDs can still be used when needed:
Lua error at line 194: attempt to perform arithmetic on local 'titleText' (a string value).
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.
-- Data is loaded via Module:GameData.
--
-- Supported usage patterns (via Template:Passive):
-- {{Passive|Honed Blade}} -> uses display Name (recommended)
-- {{Passive|name=Honed Blade}} -> explicit Name
-- {{Passive|id=CritMastery}} -> Internal Name (power use)
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)
if type(list) ~= "table" or #list == 0 then
return nil
end
return table.concat(list, ", ")
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
-- 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 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 typeName = eff.Type and eff.Type.Name or eff.ID or "Unknown"
local value = eff.Value or {}
local expr = value.Expression
local base = value.Base
local per = value["Per Level"]
local seg = typeName
if expr and expr ~= "" then
seg = seg .. string.format(" (%s)", expr)
end
local detail = {}
if base then
table.insert(detail, string.format("Base %.2f", base))
end
if per then
table.insert(detail, string.format("%.2f / Lv", 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
-- One per line
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 dur = s.Duration and s.Duration.Base
local chance = s.Chance and s.Chance.Base
local seg = name
if scope and scope ~= "" then
seg = scope .. ": " .. seg
end
local detailParts = {}
if dur then
table.insert(detailParts, string.format("Dur %.2f", dur))
end
if chance then
table.insert(detailParts, string.format("Chance %.2f", chance))
end
if #detailParts > 0 then
seg = seg .. " (" .. table.concat(detailParts, ", ") .. ")"
end
table.insert(parts, seg)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatNotes(notes)
if type(notes) ~= "table" or #notes == 0 then
return nil
end
return table.concat(notes, "<br />")
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)
------------------------------------------------------------------
-- Basic info
------------------------------------------------------------------
addRow(root, "Description", rec.Description)
addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"]))
------------------------------------------------------------------
-- Users
------------------------------------------------------------------
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 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"]))
------------------------------------------------------------------
-- Passive effects
------------------------------------------------------------------
local effectsText = formatPassiveEffects(rec["Passive Effects"])
addRow(root, "Passive effects", effectsText)
------------------------------------------------------------------
-- Status interactions
------------------------------------------------------------------
local statusApps = formatStatusApplications(rec["Status Applications"])
addRow(root, "Status applications", statusApps)
------------------------------------------------------------------
-- Notes
------------------------------------------------------------------
local notesText = formatNotes(rec.Notes)
addRow(root, "Notes", notesText)
return tostring(root)
end
----------------------------------------------------------------------
-- Public entry point
----------------------------------------------------------------------
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 (what editors actually know)
if name and name ~= "" then
rec = findPassiveByName(name)
end
-- 2) Fallback: Internal Name if explicitly given
if not rec and id and id ~= "" then
rec = getPassiveById(id)
end
if not rec then
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
return p