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

Revision as of 19:41, 12 December 2025 by Eviand (talk | contribs)

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:GameDataGameData.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 like skill-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 to 1).
  • id"Internal Name" of the skill (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):

or:


Template:Skill

The recommended way to use this module is via a small wrapper template, for example:

Template:Skill

Typical usage on any page:

Lua error at line 70: bad argument #3 to 'format' (number expected, got table).

or, explicitly:

Internal IDs can still be used when needed:

This keeps page wikitext simple while centralizing all JSON loading and formatting logic inside Lua.


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 not list 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 not block or 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 not block or 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 not damageTable or #damageTable == 0 then
        return nil
    end

    -- Current schema only has one entry, but we’ll still handle arrays.
    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 not scalingList 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 not list 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
            -- Stored as 0–1; we’ll keep it raw to avoid guessing units.
            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

----------------------------------------------------------------------
-- Public: infobox
----------------------------------------------------------------------

function p.infobox(frame)
    local args = getArgs(frame)
    local id   = args.id or args[1]

    if not id or id == "" then
        return "[[Category:Pages with missing skill id]]"
    end

    local dataset = getSkills()
    local rec     = dataset.byId[id]

    if not rec then
        return "[[Category:Pages with unknown skill id|" .. mw.text.nowiki(id) .. "]]"
    end

    local root = mw.html.create("table")
    root:addClass("wikitable spiritvale-skill-infobox")

    -- Header: icon + name
    local icon  = rec.Icon
    local title = rec.Name or id

    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 or id)
    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 req["Required Skills"] 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)

    -- We could add Status Removal here later if you want it visible.

    return tostring(root)
end

return p