Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Join the Playtest on Steam Now: SpiritVale

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:GameDataGameData.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.
  • Status Applications:
    • Each entry from "Status Applications", showing scope (Self / Target), status name, duration, chance, and whether the duration is fixed.
  • Removal:
    • Any entries from "Effect Removal", such as "Removed by hit", "Stacks with itself", etc.
  • Events:
    • Any entries from "Events", such as On Take Hit Reflect → Counter Slash.

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 to 1).
  • id"Internal Name" of the effect (optional fallback / power use).

Lookup order:

  1. If name or the first unnamed parameter is provided and matches a record’s "Name", that record is used.
  2. Otherwise, if id is provided and matches an "Internal Name", that record is used.
  3. 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:

Poison
DescriptionDeals stacking damage over time. Damage scales with STR and AGI.
Internal namePoison
UsersClasses: 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)
MechanicsFlat damage: 1
Element: None (ID 10)
Status effectsNo Mana Regeneration – Base 1
RemovalStacks with itself

or, explicitly:

Poison
DescriptionDeals stacking damage over time. Damage scales with STR and AGI.
Internal namePoison
UsersClasses: 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)
MechanicsFlat damage: 1
Element: None (ID 10)
Status effectsNo Mana Regeneration – Base 1
RemovalStacks with itself

Internal IDs can still be used when needed:

Poison
DescriptionDeals stacking damage over time. Damage scales with STR and AGI.
Internal namePoison
UsersClasses: 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)
MechanicsFlat damage: 1
Element: None (ID 10)
Status effectsNo Mana Regeneration – Base 1
RemovalStacks 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