Module:GameEffects
More actions
Module:GameEffects
Module:GameEffects renders effect / status data from Data:effects.json into a reusable infobox-style table.
It is intended to be used via a template (for example Template:Effect) so that status effects can be embedded on any page without creating individual pages for each effect.
This module:
- Loads data via Module:GameData →
GameData.loadEffects(). - Looks up effects 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 effect.
Data source
Effect data comes from Data:effects.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.762333+00:00",
"records": [
{
"Name": "Aegis",
"Internal Name": "Aegis",
"Icon": "status-aegis.webp",
"Description": "...",
"Users": { ... },
"Mechanics": { ... },
"Status Effects": [ ... ],
"Status Applications": [ ... ],
"Effect Removal": [ ... ],
"Events": [ ... ]
}
]
}
Key fields for each effect:
"Name"– display name used on the wiki."Internal Name"– stable ID used as the main lookup key."Icon"– image file name for the status icon."Description"– human-readable description of the effect."Users"– who can use / apply this effect (classes, monsters, summons, skills, events, etc.)."Mechanics"– core numeric behavior, such as percent damage, percent healing, flat damage, and element information."Status Effects"– the underlying stat modifiers (e.g. Attack Speed, Elemental Damage, Damage Taken from Magic) with Base / Per Level / Expression values."Status Applications"– which statuses this effect applies to other entities (scope, status name, duration, chance, etc.)."Effect Removal"– how or when the effect ends (e.g. removed by hit, removed by attack, stacks with itself)."Events"– event hooks that trigger skills while the effect is active (e.g. On Take Hit Reflect → Counter Slash).
Output
For a given effect, the module renders a table with the CSS class spiritvale-effect-infobox.
Depending on what exists in the JSON record, the table may include:
- Header row with icon and effect name.
- Description text.
- Internal name (for debugging / internal reference).
- Users:
- Classes, Monsters, Summons, Events, Skills, and any linked Status Effects.
- Mechanics:
- Percent damage / healing.
- Flat damage.
- Element and element ID (if present).
- Status Effects:
- Each entry from
"Status Effects", showing type name and Base / Per Level / Expression values.
- Each entry from
- Status Applications:
- Each entry from
"Status Applications", showing scope (Self / Target), status name, duration, chance, and whether the duration is fixed.
- Each entry from
- Removal:
- Any entries from
"Effect Removal", such as "Removed by hit", "Stacks with itself", etc.
- Any entries from
- Events:
- Any entries from
"Events", such asOn Take Hit Reflect → Counter Slash.
- Any entries from
Rows are only shown if the underlying field exists in the JSON for that effect.
Public interface
The module exposes a single entry point for templates:
GameEffects.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 effect"Name".name– explicit display"Name"of the effect (equivalent to1).id–"Internal Name"of the effect (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 effect: ?
or:
Unknown effect: ?
Template:Effect
The recommended way to use this module is via a small wrapper template:
Template:Effect
Unknown effect: ?
Typical usage on any page:
| Description | Deals stacking damage over time. Damage scales with STR and AGI. |
|---|---|
| Internal name | Poison |
| Users | Classes: Acolyte, Knight, Mage, Rogue, Scout, Summoner, Warrior, Weaver Monsters: Bat Lord, Bloom, Blossom, Bogbloom, Cactus King, Earthworm, Fangroot, Fungi, Housefly Junk, Housefly Nom, Octopus Baby, Poison Bomb, Queen Worm, Spider Queen, Spider Robot, Spider Toxin, Spiderling Robot, Toadstool, Worm, Worm Creep Skills: Cure, Gunk Shot (NPC), Mass Cure, Venom Strike, Wide Poison (NPC) |
| Mechanics | Flat damage: 1 Element: None (ID 10) |
| Status effects | No Mana Regeneration – Base 1 |
| Removal | Stacks with itself |
or, explicitly:
| Description | Deals stacking damage over time. Damage scales with STR and AGI. |
|---|---|
| Internal name | Poison |
| Users | Classes: Acolyte, Knight, Mage, Rogue, Scout, Summoner, Warrior, Weaver Monsters: Bat Lord, Bloom, Blossom, Bogbloom, Cactus King, Earthworm, Fangroot, Fungi, Housefly Junk, Housefly Nom, Octopus Baby, Poison Bomb, Queen Worm, Spider Queen, Spider Robot, Spider Toxin, Spiderling Robot, Toadstool, Worm, Worm Creep Skills: Cure, Gunk Shot (NPC), Mass Cure, Venom Strike, Wide Poison (NPC) |
| Mechanics | Flat damage: 1 Element: None (ID 10) |
| Status effects | No Mana Regeneration – Base 1 |
| Removal | Stacks with itself |
Internal IDs can still be used when needed:
| Description | Deals stacking damage over time. Damage scales with STR and AGI. |
|---|---|
| Internal name | Poison |
| Users | Classes: Acolyte, Knight, Mage, Rogue, Scout, Summoner, Warrior, Weaver Monsters: Bat Lord, Bloom, Blossom, Bogbloom, Cactus King, Earthworm, Fangroot, Fungi, Housefly Junk, Housefly Nom, Octopus Baby, Poison Bomb, Queen Worm, Spider Queen, Spider Robot, Spider Toxin, Spiderling Robot, Toadstool, Worm, Worm Creep Skills: Cure, Gunk Shot (NPC), Mass Cure, Venom Strike, Wide Poison (NPC) |
| Mechanics | Flat damage: 1 Element: None (ID 10) |
| Status effects | No Mana Regeneration – Base 1 |
| Removal | Stacks with itself |
This keeps page wikitext simple while centralizing all JSON loading and formatting logic inside Lua.
-- Module:GameEffects
--
-- Renders effect / status data (from Data:effects.json) into
-- an infobox-style table. Data is loaded via Module:GameData.
--
-- Supported usage patterns (via Template:Effect):
-- {{Effect|Poison}} -> uses display Name (recommended)
-- {{Effect|name=Poison}} -> explicit Name
-- {{Effect|id=Poison}} -> Internal Name (power use)
local GameData = require("Module:GameData")
local p = {}
----------------------------------------------------------------------
-- Internal helpers
----------------------------------------------------------------------
local effectsCache
local function getEffects()
if not effectsCache then
effectsCache = GameData.loadEffects()
end
return effectsCache
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
-- Lookup by Internal Name
local function getEffectById(id)
if not id or id == "" then
return nil
end
local dataset = getEffects()
local byId = dataset.byId or {}
return byId[id]
end
-- Lookup by display Name (for editors)
local function findEffectByName(name)
if not name or name == "" then
return nil
end
local dataset = getEffects()
for _, rec in ipairs(dataset.records or {}) do
if rec["Name"] == name then
return rec
end
end
return nil
end
----------------------------------------------------------------------
-- Formatting helpers
----------------------------------------------------------------------
local function formatUsers(users)
if type(users) ~= "table" then
return nil
end
local parts = {}
local function add(label, key)
local list = users[key]
if type(list) == "table" and #list > 0 then
table.insert(parts, string.format("%s: %s", label, table.concat(list, ", ")))
end
end
add("Classes", "Classes")
add("Monsters", "Monsters")
add("Summons", "Summons")
add("Events", "Events")
add("Skills", "Skills")
add("Status effects", "Status Effects")
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatMechanics(mech)
if type(mech) ~= "table" then
return nil
end
local parts = {}
local pd = mech["Percent Damage"]
if pd then
table.insert(parts, string.format("Percent damage: %s", tostring(pd)))
end
local ph = mech["Percent Healing"]
if ph then
table.insert(parts, string.format("Percent healing: %s", tostring(ph)))
end
local fd = mech["Flat Damage"]
if fd then
table.insert(parts, string.format("Flat damage: %s", tostring(fd)))
end
local elType = mech["Element Type"]
if elType and elType ~= "" then
local elId = mech["Element ID"]
if elId then
table.insert(parts, string.format("Element: %s (ID %s)", elType, tostring(elId)))
else
table.insert(parts, "Element: " .. elType)
end
end
if #parts == 0 then
return nil
end
return table.concat(parts, "<br />")
end
local function formatStatusEffects(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local lines = {}
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 .. " – " .. table.concat(detail, ", ")
end
table.insert(lines, seg)
end
end
if #lines == 0 then
return nil
end
return table.concat(lines, "<br />")
end
local function formatApplications(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local lines = {}
for _, app in ipairs(list) do
if type(app) == "table" then
local scope = app.Scope or "Target"
local name = app["Status Name"] or app["Status ID"] or "Unknown"
local pieces = {}
local dur = app.Duration
if type(dur) == "table" then
local dParts = {}
if dur.Base ~= nil then
table.insert(dParts, string.format("Base %s", tostring(dur.Base)))
end
if dur["Per Level"] ~= nil then
table.insert(dParts, string.format("%s / Lv", tostring(dur["Per Level"])))
end
if #dParts > 0 then
table.insert(pieces, "Duration " .. table.concat(dParts, ", "))
end
end
local ch = app.Chance
if type(ch) == "table" then
local cParts = {}
if ch.Base ~= nil then
table.insert(cParts, string.format("Base %s", tostring(ch.Base)))
end
if ch["Per Level"] ~= nil then
table.insert(cParts, string.format("%s / Lv", tostring(ch["Per Level"])))
end
if #cParts > 0 then
table.insert(pieces, "Chance " .. table.concat(cParts, ", "))
end
end
if app["Fixed Duration"] then
table.insert(pieces, "Fixed duration")
end
local seg = scope .. " – " .. name
if #pieces > 0 then
seg = seg .. " (" .. table.concat(pieces, ", ") .. ")"
end
table.insert(lines, seg)
end
end
if #lines == 0 then
return nil
end
return table.concat(lines, "<br />")
end
local function formatEffectRemoval(list)
if type(list) ~= "table" or #list == 0 then
return nil
end
local parts = {}
for _, v in ipairs(list) do
if type(v) == "string" and v ~= "" then
table.insert(parts, v)
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
----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------
local function buildInfobox(rec)
local root = mw.html.create("table")
root:addClass("wikitable spiritvale-effect-infobox")
local icon = rec.Icon
local title = rec.Name or rec["Internal Name"] or "Unknown Effect"
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)
-- Description
addRow(root, "Description", rec.Description)
-- Basic identifiers
addRow(root, "Internal name", rec["Internal Name"])
-- Users
addRow(root, "Users", formatUsers(rec.Users))
-- Mechanics / core behavior
addRow(root, "Mechanics", formatMechanics(rec.Mechanics))
-- Status modifiers
addRow(root, "Status effects", formatStatusEffects(rec["Status Effects"]))
-- Status applications (what this applies to others)
addRow(root, "Applies statuses", formatApplications(rec["Status Applications"]))
-- How it is removed or ends
addRow(root, "Removal", formatEffectRemoval(rec["Effect Removal"]))
-- Event hooks / triggers
addRow(root, "Events", formatEvents(rec.Events))
return tostring(root)
end
----------------------------------------------------------------------
-- Public entry point
----------------------------------------------------------------------
function p.infobox(frame)
local args = getArgs(frame)
-- Allow:
-- {{Effect|Poison}} -> args[1] = "Poison" (Name)
-- {{Effect|name=Poison}} -> args.name
-- {{Effect|id=Poison}} -> 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 = findEffectByName(name)
end
-- 2) Fallback: Internal Name if explicitly given
if not rec and id and id ~= "" then
rec = getEffectById(id)
end
if not rec then
local label = name or id or "?"
return string.format(
"<strong>Unknown effect:</strong> %s[[Category:Pages with unknown effect|%s]]",
mw.text.nowiki(label),
label
)
end
return buildInfobox(rec)
end
return p