Module:GameSkills
More actions
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):
Unknown skill: ?
or:
Unknown skill: ?
Template:Skill
The recommended way to use this module is via a small wrapper template, for example:
Template:Skill
Unknown skill: ?
Typical usage on any page:
Unknown skill: Bash
or, explicitly:
Unknown skill: Bash
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 skill data (from Data:skills.json) into an infobox/table.
-- Data is loaded via Module:GameData.
--
-- Supported usage patterns (via Template:Skill):
-- {{Skill|Bash}} -- uses display Name (recommended)
-- {{Skill|name=Bash}} -- explicit name
-- {{Skill|id=Bash_InternalId}} -- internal ID (power use)
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)
-- Prefer parent template args if present (usual #invoke pattern)
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
local function formatBasePer(block)
if type(block) ~= "table" then
return nil
end
local base = block.Base
local per = block["Per Level"]
if base and per then
return string.format("%.2f (%.2f / Lv)", base, per)
elseif base then
return string.format("%.2f", base)
elseif per then
return string.format("%.2f / Lv", per)
else
return nil
end
end
local function formatManaCost(block)
if type(block) ~= "table" then
return nil
end
local base = block.Base
local per = block["Per Level"]
if base and per then
return string.format("%.0f (%.0f / Lv)", base, per)
elseif base then
return string.format("%.0f", base)
elseif per then
return string.format("%.0f / Lv", per)
else
return nil
end
end
local function formatMainDamage(damageTable)
if type(damageTable) ~= "table" or #damageTable == 0 then
return nil
end
local parts = {}
for _, entry in ipairs(damageTable) do
local base = entry["Base %"]
local per = entry["Per Level %"]
local txt
if base and per then
txt = string.format("Base %.2f, +%.2f / Lv", base, per)
elseif base then
txt = string.format("Base %.2f", base)
elseif per then
txt = string.format("+%.2f / Lv", per)
end
if txt then
table.insert(parts, txt)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "; ")
end
local function formatScaling(scalingList)
if type(scalingList) ~= "table" or #scalingList == 0 then
return nil
end
local parts = {}
for _, s in ipairs(scalingList) do
local name = s["Scaling Name"] or s["Scaling ID"] or "Unknown"
local pct = s.Percent
if pct then
table.insert(parts, string.format("%s: %.2f", name, pct))
else
table.insert(parts, name)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, ", ")
end
local function formatStatusApplications(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local parts = {}
for _, s in ipairs(list) do
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
return table.concat(parts, "; ")
end
-- Lookup by internal ID (for tools / power use)
local function getSkillById(id)
if not id or id == "" then
return nil
end
local dataset = getSkills()
local byId = dataset.byId or {}
return byId[id]
end
-- Lookup by display Name (for editors)
local function findSkillByName(name)
if not name or name == "" then
return nil
end
local dataset = getSkills()
for _, rec in ipairs(dataset.records or {}) do
if rec["Name"] == name then
return rec
end
end
return nil
end
----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------
local function buildInfobox(rec)
local root = mw.html.create("table")
root:addClass("wikitable spiritvale-skill-infobox")
-- Header: icon + name
local icon = rec.Icon
local title = rec.Name or rec["Internal Name"] or "Unknown Skill"
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"]))
-- Type
local t = rec.Type or {}
local damageType = t["Damage Type"] and t["Damage Type"].Name
local element = t["Element Type"] and t["Element Type"].Name
local target = t["Target Type"] and t["Target Type"].Name
local castType = t["Cast Type"] and t["Cast Type"].Name
addRow(root, "Damage type", damageType)
addRow(root, "Element", element)
addRow(root, "Targeting", target)
addRow(root, "Cast type", castType)
-- Mechanics
local mech = rec.Mechanics or {}
local basicTimings = mech["Basic Timings"] or {}
local resource = mech["Resource Cost"] or {}
if mech.Range then
addRow(root, "Range", string.format("%.2f", mech.Range))
end
addRow(root, "Cast time", formatBasePer(basicTimings["Cast Time"]))
addRow(root, "Cooldown", formatBasePer(basicTimings["Cooldown"]))
addRow(root, "Duration", formatBasePer(basicTimings["Duration"]))
local manaCost = formatManaCost(resource["Mana Cost"])
addRow(root, "Mana cost", manaCost)
-- Damage + Scaling
local dmg = rec.Damage or {}
local mainDmg = formatMainDamage(dmg["Main Damage"])
local scaling = formatScaling(dmg.Scaling)
addRow(root, "Main damage", mainDmg)
addRow(root, "Scaling", scaling)
-- Status interactions
local statusApps = formatStatusApplications(rec["Status Applications"])
addRow(root, "Status applications", statusApps)
return tostring(root)
end
----------------------------------------------------------------------
-- Public entry point
----------------------------------------------------------------------
function p.infobox(frame)
local args = getArgs(frame)
-- Allow three styles:
-- {{Skill|Bash}} -> args[1] = "Bash" (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 display Name (what editors will actually use)
if name and name ~= "" then
rec = findSkillByName(name)
end
-- 2) Fallback: internal ID if explicitly given
if not rec and id and id ~= "" then
rec = getSkillById(id)
end
if not rec then
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