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:18, 12 December 2025 by Eviand (talk | contribs) (Created page with "-- 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 --------------------------------------------...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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):

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