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:53, 12 December 2025 by Eviand (talk | contribs) (Created page with "-- Module:GamePassives -- -- Renders passive skill data (from Data:passives.json) into an infobox-style table. -- Data is loaded via Module:GameData. -- -- Supported usage patterns (via Template:Passive): -- {{Passive|Honed Blade}} -> uses display Name (recommended) -- {{Passive|name=Honed Blade}} -> explicit Name -- {{Passive|id=CritMastery}} -> Internal Name (power use) local GameData = require("Module:GameData") local p = {} ---...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Module:GamePassives

Module:GamePassives renders passive skill data from Data:passives.json into a reusable infobox-style table.

It is intended to be used via a template (for example Template:Passive) so that passives can be embedded on any page without creating individual pages for each passive.

This module:

  • Loads data via Module:GameDataGameData.loadPassives().
  • Looks up passives 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 passive.

Data source

Passive data comes from Data:passives.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.784284+00:00",
  "records": [
    {
      "Name": "Honed Blade",
      "Internal Name": "CritMastery",
      "...": "other fields specific to passives"
    }
  ]
}

Each record is a single passive. 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.

Common fields include:

  • Icon
  • Description
  • Max Level
  • Users (e.g. Classes)
  • Requirements
  • Passive Effects
  • Status Applications
  • Notes

Output

For a given passive, the module renders a table with the CSS class spiritvale-passive-infobox.

Depending on what exists in the JSON record, the table may include:

  • Header row with passive name (and icon, if present).
  • Description.
  • Max level.
  • Users:
    • Classes
    • Summons
    • Monsters
    • Events
  • Requirements:
    • Required Skills (with required level)
    • Required Weapons
    • Required Stances
  • Passive effects:
    • Each entry from "Passive Effects" (Type name, optional Expression, Base and Per Level where present)
  • Status interactions:
    • Status Applications (if any)
  • Notes:
    • Any entries from "Notes" (one per line)

Rows are only shown if the underlying field exists in the JSON for that passive.


Public interface

The module exposes a single entry point for templates:

GamePassives.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 passive "Name".
  • name – explicit display "Name" of the passive (equivalent to 1).
  • id"Internal Name" of the passive (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 passive: ?

or:

Unknown passive: ?

Template:Passive

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

Template:Passive
 Unknown passive: ?

Typical usage on any page:

Lua error at line 194: attempt to perform arithmetic on local 'titleText' (a string value).

or, explicitly:

Lua error at line 194: attempt to perform arithmetic on local 'titleText' (a string value).

Internal IDs can still be used when needed:

Lua error at line 194: attempt to perform arithmetic on local 'titleText' (a string value).

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


-- Module:GamePassives
--
-- Renders passive skill data (from Data:passives.json) into an infobox-style table.
-- Data is loaded via Module:GameData.
--
-- Supported usage patterns (via Template:Passive):
--   {{Passive|Honed Blade}}              -> uses display Name (recommended)
--   {{Passive|name=Honed Blade}}         -> explicit Name
--   {{Passive|id=CritMastery}}           -> Internal Name (power use)

local GameData = require("Module:GameData")

local p = {}

----------------------------------------------------------------------
-- Internal helpers
----------------------------------------------------------------------

local passivesCache

local function getPassives()
    if not passivesCache then
        passivesCache = GameData.loadPassives()
    end
    return passivesCache
end

local function getArgs(frame)
    local parent = frame:getParent()
    if parent then
        return parent.args
    end
    return frame.args
end

local function listToText(list)
    if type(list) ~= "table" 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

-- Lookup by Internal Name
local function getPassiveById(id)
    if not id or id == "" then
        return nil
    end
    local dataset = getPassives()
    local byId = dataset.byId or {}
    return byId[id]
end

-- Lookup by display Name (for editors)
local function findPassiveByName(name)
    if not name or name == "" then
        return nil
    end
    local dataset = getPassives()
    for _, rec in ipairs(dataset.records or {}) do
        if rec["Name"] == name then
            return rec
        end
    end
    return nil
end

----------------------------------------------------------------------
-- Formatting helpers
----------------------------------------------------------------------

local function formatPassiveEffects(list)
    if type(list) ~= "table" or #list == 0 then
        return nil
    end

    local parts = {}

    for _, eff in ipairs(list) do
        if type(eff) == "table" then
            local typeName = eff.Type and eff.Type.Name or eff.ID or "Unknown"
            local value    = eff.Value or {}

            local expr = value.Expression
            local base = value.Base
            local per  = value["Per Level"]

            local seg = typeName

            if expr and expr ~= "" then
                seg = seg .. string.format(" (%s)", expr)
            end

            local detail = {}
            if base then
                table.insert(detail, string.format("Base %.2f", base))
            end
            if per then
                table.insert(detail, string.format("%.2f / Lv", per))
            end

            if #detail > 0 then
                seg = seg .. " – " .. table.concat(detail, ", ")
            end

            table.insert(parts, seg)
        end
    end

    if #parts == 0 then
        return nil
    end

    -- One per line
    return table.concat(parts, "<br />")
end

local function formatStatusApplications(list)
    if type(list) ~= "table" or #list == 0 then
        return nil
    end

    local parts = {}
    for _, s in ipairs(list) do
        if type(s) == "table" then
            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
                table.insert(detailParts, string.format("Chance %.2f", chance))
            end

            if #detailParts > 0 then
                seg = seg .. " (" .. table.concat(detailParts, ", ") .. ")"
            end

            table.insert(parts, seg)
        end
    end

    if #parts == 0 then
        return nil
    end

    return table.concat(parts, "<br />")
end

local function formatNotes(notes)
    if type(notes) ~= "table" or #notes == 0 then
        return nil
    end
    return table.concat(notes, "<br />")
end

----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------

local function buildInfobox(rec)
    local root = mw.html.create("table")
    root:addClass("wikitable spiritvale-passive-infobox")

    -- Header: icon + name
    local icon  = rec.Icon
    local title = rec.Name or rec["Internal Name"] or "Unknown Passive"

    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)

    ------------------------------------------------------------------
    -- 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 type(req["Required Skills"]) == "table" 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"]))

    ------------------------------------------------------------------
    -- Passive effects
    ------------------------------------------------------------------
    local effectsText = formatPassiveEffects(rec["Passive Effects"])
    addRow(root, "Passive effects", effectsText)

    ------------------------------------------------------------------
    -- Status interactions
    ------------------------------------------------------------------
    local statusApps = formatStatusApplications(rec["Status Applications"])
    addRow(root, "Status applications", statusApps)

    ------------------------------------------------------------------
    -- Notes
    ------------------------------------------------------------------
    local notesText = formatNotes(rec.Notes)
    addRow(root, "Notes", notesText)

    return tostring(root)
end

----------------------------------------------------------------------
-- Public entry point
----------------------------------------------------------------------

function p.infobox(frame)
    local args = getArgs(frame)

    -- Allow:
    --   {{Passive|Honed Blade}}           -> args[1] = "Honed Blade" (Name)
    --   {{Passive|name=Honed Blade}}      -> args.name
    --   {{Passive|id=CritMastery}}        -> 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 = findPassiveByName(name)
    end

    -- 2) Fallback: Internal Name if explicitly given
    if not rec and id and id ~= "" then
        rec = getPassiveById(id)
    end

    if not rec then
        local label = name or id or "?"
        return string.format(
            "<strong>Unknown passive:</strong> %s[[Category:Pages with unknown passive|%s]]",
            mw.text.nowiki(label),
            label
        )
    end

    return buildInfobox(rec)
end

return p