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:
Lua error at line 206: bad argument #3 to 'format' (string expected, got table).
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 a nice infobox/table.
-- Data is loaded via Module:GameData.
--
-- Typical usage (via Template:Skill):
-- {{Skill|id=Dispell}}
-- {{Skill|name=Absolution}} -- slower fallback by display name
local GameData = require('Module:GameData')
local p = {}
----------------------------------------------------------------------
-- Internal helpers: lookups
----------------------------------------------------------------------
local function getDataset()
return GameData.loadSkills()
end
-- Fast lookup by Internal Name
local function getSkillById(id)
if not id or id == '' then
return nil
end
local dataset = getDataset()
local byId = dataset.byId or {}
return byId[id]
end
-- Slower fallback: lookup by display Name
local function findSkillByName(name)
if not name or name == '' then
return nil
end
local dataset = getDataset()
for _, rec in ipairs(dataset.records or {}) do
if rec["Name"] == name then
return rec
end
end
return nil
end
----------------------------------------------------------------------
-- Formatting helpers
----------------------------------------------------------------------
local function formatTypeLine(t)
if type(t) ~= 'table' then
return nil
end
local parts = {}
local dmg = t["Damage Type"]
if dmg and dmg["Name"] then
table.insert(parts, dmg["Name"])
end
local elem = t["Element Type"]
if elem and elem["Name"] then
table.insert(parts, elem["Name"])
end
local target = t["Target Type"]
if target and target["Name"] then
table.insert(parts, target["Name"])
end
local cast = t["Cast Type"]
if cast and cast["Name"] then
table.insert(parts, cast["Name"])
end
if #parts == 0 then
return nil
end
return table.concat(parts, " • ")
end
local function formatUsers(users)
if type(users) ~= 'table' then
return nil
end
local chunks = {}
local classes = users["Classes"]
if type(classes) == 'table' and #classes > 0 then
table.insert(chunks, "Classes: " .. table.concat(classes, ", "))
end
local summons = users["Summons"]
if type(summons) == 'table' and #summons > 0 then
table.insert(chunks, "Summons: " .. table.concat(summons, ", "))
end
local monsters = users["Monsters"]
if type(monsters) == 'table' and #monsters > 0 then
table.insert(chunks, "Monsters: " .. table.concat(monsters, ", "))
end
-- (Users.Events exists in the data, but we skip it for now – mostly internal hooks.)
if #chunks == 0 then
return nil
end
return table.concat(chunks, "<br />")
end
local function formatArea(mech)
if type(mech) ~= 'table' then
return nil
end
local area = mech["Area"]
if type(area) ~= 'table' then
return nil
end
local parts = {}
local size = area["Area Size"]
if size then
table.insert(parts, tostring(size))
end
local eff = area["Effective Distance"]
if eff then
table.insert(parts, string.format("Radius: %s", eff))
end
if #parts == 0 then
return nil
end
return table.concat(parts, " • ")
end
local function formatCastAndCooldown(mech)
if type(mech) ~= 'table' then
return nil, nil, nil
end
local basic = mech["Basic Timings"]
if type(basic) ~= 'table' then
return nil, nil, nil
end
local cast, cd, dur
local castBlock = basic["Cast Time"]
if type(castBlock) == 'table' and castBlock["Base"] then
cast = tostring(castBlock["Base"]) .. " s"
if castBlock["Per Level"] and castBlock["Per Level"] ~= 0 then
local per = castBlock["Per Level"]
local sign = per > 0 and "+" or ""
cast = cast .. string.format(" (%s%g per level)", sign, per)
end
end
local cdBlock = basic["Cooldown"]
if type(cdBlock) == 'table' and cdBlock["Base"] then
cd = tostring(cdBlock["Base"]) .. " s"
if cdBlock["Per Level"] and cdBlock["Per Level"] ~= 0 then
local per = cdBlock["Per Level"]
local sign = per > 0 and "+" or ""
cd = cd .. string.format(" (%s%g per level)", sign, per)
end
end
local durBlock = basic["Duration"]
if type(durBlock) == 'table' and durBlock["Base"] then
dur = tostring(durBlock["Base"]) .. " s"
if durBlock["Per Level"] and durBlock["Per Level"] ~= 0 then
local per = durBlock["Per Level"]
local sign = per > 0 and "+" or ""
dur = dur .. string.format(" (%s%g per level)", sign, per)
end
end
return cast, cd, dur
end
local function formatResourceCost(mech)
if type(mech) ~= 'table' then
return nil
end
local cost = mech["Resource Cost"]
if type(cost) ~= 'table' then
return nil
end
local parts = {}
local mana = cost["Mana Cost"]
if type(mana) == 'table' then
local base = mana["Base"]
local per = mana["Per Level"]
if base and per then
table.insert(parts, string.format("Mana: %s + %s per level", base, per))
elseif base then
table.insert(parts, string.format("Mana: %s", base))
end
end
local hp = cost["Health Cost"]
if type(hp) == 'table' then
local base = hp["Base"]
local per = hp["Per Level"]
if base and per then
table.insert(parts, string.format("HP: %s + %s per level", base, per))
elseif base then
table.insert(parts, string.format("HP: %s", base))
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatStatusesApplied(rec)
local apps = rec["Status Applications"]
if type(apps) ~= 'table' or #apps == 0 then
return nil
end
local names = {}
for _, entry in ipairs(apps) do
if type(entry) == 'table' then
local n = entry["Status Name"]
if type(n) == 'string' then
table.insert(names, n)
end
end
end
if #names == 0 then
return nil
end
return table.concat(names, ", ")
end
local function formatStatusesRemoved(rec)
local entries = rec["Status Removal"]
if type(entries) ~= 'table' or #entries == 0 then
return nil
end
local names = {}
for _, entry in ipairs(entries) do
if type(entry) == 'table' then
local list = entry["Status Name"]
if type(list) == 'table' then
for _, n in ipairs(list) do
if type(n) == 'string' then
table.insert(names, n)
end
end
end
end
end
if #names == 0 then
return nil
end
return table.concat(names, ", ")
end
----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------
local function buildInfobox(rec)
local box = mw.html.create('table')
:addClass('sv-skill-infobox')
local name = rec["Name"] or rec["Internal Name"] or "Unknown Skill"
-- Header
box:tag('tr')
:tag('th')
:attr('colspan', 2)
:addClass('sv-skill-infobox-title')
:wikitext(name)
:done()
-- Icon
local icon = rec["Icon"]
if icon and icon ~= "" then
box:tag('tr')
:tag('td')
:attr('colspan', 2)
:addClass('sv-skill-infobox-icon')
:wikitext(string.format('[[File:%s|128px|center]]', icon))
:done()
end
-- Description
local desc = rec["Description"]
if desc and desc ~= "" then
box:tag('tr')
:tag('th'):wikitext('Description'):done()
:tag('td'):wikitext(desc):done()
end
-- Type line
local typeLine = formatTypeLine(rec["Type"])
if typeLine then
box:tag('tr')
:tag('th'):wikitext('Type'):done()
:tag('td'):wikitext(typeLine):done()
end
-- Max Level
local maxLevel = rec["Max Level"]
if maxLevel then
box:tag('tr')
:tag('th'):wikitext('Max Level'):done()
:tag('td'):wikitext(tostring(maxLevel)):done()
end
-- Users
local usersText = formatUsers(rec["Users"])
if usersText then
box:tag('tr')
:tag('th'):wikitext('Users'):done()
:tag('td'):wikitext(usersText):done()
end
-- Mechanics
local mech = rec["Mechanics"]
if type(mech) == 'table' then
if mech["Range"] then
box:tag('tr')
:tag('th'):wikitext('Range'):done()
:tag('td'):wikitext(tostring(mech["Range"])):done()
end
local areaText = formatArea(mech)
if areaText then
box:tag('tr')
:tag('th'):wikitext('Area'):done()
:tag('td'):wikitext(areaText):done()
end
local cast, cd, dur = formatCastAndCooldown(mech)
if cast then
box:tag('tr')
:tag('th'):wikitext('Cast Time'):done()
:tag('td'):wikitext(cast):done()
end
if cd then
box:tag('tr')
:tag('th'):wikitext('Cooldown'):done()
:tag('td'):wikitext(cd):done()
end
if dur then
box:tag('tr')
:tag('th'):wikitext('Duration'):done()
:tag('td'):wikitext(dur):done()
end
local cost = formatResourceCost(mech)
if cost then
box:tag('tr')
:tag('th'):wikitext('Cost'):done()
:tag('td'):wikitext(cost):done()
end
end
-- Statuses
local applied = formatStatusesApplied(rec)
if applied then
box:tag('tr')
:tag('th'):wikitext('Statuses Applied'):done()
:tag('td'):wikitext(applied):done()
end
local removed = formatStatusesRemoved(rec)
if removed then
box:tag('tr')
:tag('th'):wikitext('Statuses Removed'):done()
:tag('td'):wikitext(removed):done()
end
return tostring(box)
end
----------------------------------------------------------------------
-- Public entry point
----------------------------------------------------------------------
function p.infobox(frame)
-- Support both direct #invoke and wrapper templates
local parent = frame:getParent()
local args = parent and parent.args or frame.args
local id = args.id or args[1]
local name = args.name
local rec = nil
if id and id ~= '' then
rec = getSkillById(id)
end
if not rec and name and name ~= '' then
rec = findSkillByName(name)
end
if not rec then
local label = id or name or "?"
return string.format('<strong>Unknown skill: %s</strong>', label)
end
return buildInfobox(rec)
end
return p