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:GameSkills: Difference between revisions

From SpiritVale Wiki
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 --------------------------------------------..."
 
No edit summary
Line 1: Line 1:
-- Module:GameSkills
local GameData = require("Module:GameData")
--
-- 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 = {}
local p = {}


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Internal helpers: lookups
-- Internal helpers
----------------------------------------------------------------------
----------------------------------------------------------------------


local function getDataset()
local skillsCache
    return GameData.loadSkills()
end


-- Fast lookup by Internal Name
local function getSkills()
local function getSkillById(id)
     if not skillsCache then
     if not id or id == '' then
         skillsCache = GameData.loadSkills()
         return nil
     end
     end
    local dataset = getDataset()
     return skillsCache
    local byId = dataset.byId or {}
     return byId[id]
end
end


-- Slower fallback: lookup by display Name
local function getArgs(frame)
local function findSkillByName(name)
     -- Prefer parent template args if present (usual #invoke pattern)
     if not name or name == '' then
     local parent = frame:getParent()
        return nil
     if parent then
    end
        return parent.args
     local dataset = getDataset()
     for _, rec in ipairs(dataset.records or {}) do
        if rec["Name"] == name then
            return rec
        end
     end
     end
     return nil
     return frame.args
end
end


----------------------------------------------------------------------
local function listToText(list)
-- Formatting helpers
     if not list or #list == 0 then
----------------------------------------------------------------------
 
local function formatTypeLine(t)
     if type(t) ~= 'table' then
         return nil
         return nil
     end
     end
    return table.concat(list, ", ")
end


    local parts = {}
local function addRow(tbl, label, value)
 
     if value == nil or value == "" then
     local dmg = t["Damage Type"]
         return
    if dmg and dmg["Name"] then
         table.insert(parts, dmg["Name"])
     end
     end
    local row = tbl:tag("tr")
    row:tag("th"):wikitext(label):done()
    row:tag("td"):wikitext(value):done()
end


    local elem = t["Element Type"]
local function formatBasePer(block)
     if elem and elem["Name"] then
     if not block or type(block) ~= "table" then
         table.insert(parts, elem["Name"])
         return nil
     end
     end
    local base = block.Base
    local per  = block["Per Level"]


    local target = t["Target Type"]
     if base and per then
     if target and target["Name"] then
         return string.format("%.2f (%.2f / Lv)", base, per)
         table.insert(parts, target["Name"])
     elseif base then
     end
        return string.format("%.2f", base)
 
     elseif per then
    local cast = t["Cast Type"]
         return string.format("%.2f / Lv", per)
     if cast and cast["Name"] then
     else
         table.insert(parts, cast["Name"])
     end
 
    if #parts == 0 then
         return nil
         return nil
     end
     end
    return table.concat(parts, " • ")
end
end


local function formatUsers(users)
local function formatManaCost(block)
     if type(users) ~= 'table' then
     if not block or type(block) ~= "table" then
         return nil
         return nil
     end
     end


     local chunks = {}
     local base = block.Base
 
     local per  = block["Per Level"]
    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 base and per then
     if type(monsters) == 'table' and #monsters > 0 then
         return string.format("%.0f (%.0f / Lv)", base, per)
         table.insert(chunks, "Monsters: " .. table.concat(monsters, ", "))
    elseif base then
     end
        return string.format("%.0f", base)
 
     elseif per then
    -- (Users.Events exists in the data, but we skip it for now – mostly internal hooks.)
        return string.format("%.0f / Lv", per)
 
     else
     if #chunks == 0 then
         return nil
         return nil
     end
     end
    return table.concat(chunks, "<br />")
end
end


local function formatArea(mech)
local function formatMainDamage(damageTable)
     if type(mech) ~= 'table' then
     if not damageTable or #damageTable == 0 then
        return nil
    end
 
    local area = mech["Area"]
    if type(area) ~= 'table' then
         return nil
         return nil
     end
     end


    -- Current schema only has one entry, but we’ll still handle arrays.
     local parts = {}
     local parts = {}
    for _, entry in ipairs(damageTable) do
        local base = entry["Base %"]
        local per  = entry["Per Level %"]
        local txt


    local size = area["Area Size"]
        if base and per then
    if size then
            txt = string.format("Base %.2f, +%.2f / Lv", base, per)
         table.insert(parts, tostring(size))
        elseif base then
    end
            txt = string.format("Base %.2f", base)
         elseif per then
            txt = string.format("+%.2f / Lv", per)
        end


    local eff = area["Effective Distance"]
        if txt then
    if eff then
            table.insert(parts, txt)
        table.insert(parts, string.format("Radius: %s", eff))
        end
     end
     end


Line 138: Line 106:
         return nil
         return nil
     end
     end
 
     return table.concat(parts, "; ")
     return table.concat(parts, " ")
end
end


local function formatCastAndCooldown(mech)
local function formatScaling(scalingList)
     if type(mech) ~= 'table' then
     if not scalingList or #scalingList == 0 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
         return nil
     end
     end


     local parts = {}
     local parts = {}
 
     for _, s in ipairs(scalingList) do
     local mana = cost["Mana Cost"]
         local name = s["Scaling Name"] or s["Scaling ID"] or "Unknown"
    if type(mana) == 'table' then
         local pct = s.Percent
         local base = mana["Base"]
         if pct then
        local per  = mana["Per Level"]
             table.insert(parts, string.format("%s: %.2f", name, pct))
        if base and per then
         else
            table.insert(parts, string.format("Mana: %s + %s per level", base, per))
             table.insert(parts, name)
        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
     end
     end
Line 224: Line 128:
         return nil
         return nil
     end
     end
 
     return table.concat(parts, ", ")
     return table.concat(parts, "<br />")
end
end


local function formatStatusesApplied(rec)
local function formatStatusApplications(list)
    local apps = rec["Status Applications"]
     if not list or #list == 0 then
     if type(apps) ~= 'table' or #apps == 0 then
         return nil
         return nil
     end
     end


     local names = {}
     local parts = {}
     for _, entry in ipairs(apps) do
     for _, s in ipairs(list) do
         if type(entry) == 'table' then
         local scope  = s.Scope or "Target"
            local n = entry["Status Name"]
        local name  = s["Status Name"] or s["Status ID"] or "Unknown status"
            if type(n) == 'string' then
        local dur    = s.Duration and s.Duration.Base
                table.insert(names, n)
         local chance = s.Chance and s.Chance.Base
            end
         end
    end


    if #names == 0 then
        local seg = name
        return nil
    end


    return table.concat(names, ", ")
        if scope and scope ~= "" then
end
            seg = scope .. ": " .. seg
        end


local function formatStatusesRemoved(rec)
        local detailParts = {}
    local entries = rec["Status Removal"]
        if dur then
    if type(entries) ~= 'table' or #entries == 0 then
            table.insert(detailParts, string.format("Dur %.2f", dur))
         return nil
        end
    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


    local names = {}
         if #detailParts > 0 then
    for _, entry in ipairs(entries) do
             seg = seg .. " (" .. table.concat(detailParts, ", ") .. ")"
         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
    end


    if #names == 0 then
         table.insert(parts, seg)
         return nil
     end
     end


     return table.concat(names, ", ")
     return table.concat(parts, "; ")
end
end


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Infobox builder
-- Public: infobox
----------------------------------------------------------------------
----------------------------------------------------------------------


local function buildInfobox(rec)
function p.infobox(frame)
     local box = mw.html.create('table')
     local args = getArgs(frame)
        :addClass('sv-skill-infobox')
    local id  = args.id or args[1]


     local name = rec["Name"] or rec["Internal Name"] or "Unknown Skill"
     if not id or id == "" then
        return "[[Category:Pages with missing skill id]]"
    end


     -- Header
     local dataset = getSkills()
    box:tag('tr')
    local rec    = dataset.byId[id]
        :tag('th')
            :attr('colspan', 2)
            :addClass('sv-skill-infobox-title')
            :wikitext(name)
        :done()


     -- Icon
     if not rec then
    local icon = rec["Icon"]
         return "[[Category:Pages with unknown skill id|" .. mw.text.nowiki(id) .. "]]"
    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
     end


    -- Description
     local root = mw.html.create("table")
     local desc = rec["Description"]
     root:addClass("wikitable spiritvale-skill-infobox")
     if desc and desc ~= "" then
        box:tag('tr')
            :tag('th'):wikitext('Description'):done()
            :tag('td'):wikitext(desc):done()
    end


     -- Type line
     -- Header: icon + name
     local typeLine = formatTypeLine(rec["Type"])
     local icon  = rec.Icon
     if typeLine then
     local title = rec.Name or id
        box:tag('tr')
            :tag('th'):wikitext('Type'):done()
            :tag('td'):wikitext(typeLine):done()
    end


    -- Max Level
     local header = root:tag("tr")
     local maxLevel = rec["Max Level"]
    local headerCell = header:tag("th")
    if maxLevel then
    headerCell:attr("colspan", 2)
        box:tag('tr')
            :tag('th'):wikitext('Max Level'):done()
            :tag('td'):wikitext(tostring(maxLevel)):done()
    end


    -- Users
     local titleText = ""
     local usersText = formatUsers(rec["Users"])
     if icon and icon ~= "" then
     if usersText then
         titleText = string.format("[[File:%s|64px|link=]] ", icon)
         box:tag('tr')
            :tag('th'):wikitext('Users'):done()
            :tag('td'):wikitext(usersText):done()
     end
     end
    titleText = titleText .. (title or id)
    headerCell:wikitext(titleText)


     -- Mechanics
     ------------------------------------------------------------------
     local mech = rec["Mechanics"]
     -- Basic info
     if type(mech) == 'table' then
    ------------------------------------------------------------------
        if mech["Range"] then
    addRow(root, "Description", rec.Description)
            box:tag('tr')
     addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"]))
                :tag('th'):wikitext('Range'):done()
                :tag('td'):wikitext(tostring(mech["Range"])):done()
        end


        local areaText = formatArea(mech)
    ------------------------------------------------------------------
        if areaText then
    -- Users
            box:tag('tr')
    ------------------------------------------------------------------
                :tag('th'):wikitext('Area'):done()
    local users = rec.Users or {}
                :tag('td'):wikitext(areaText):done()
    addRow(root, "Classes",  listToText(users.Classes))
        end
    addRow(root, "Summons",  listToText(users.Summons))
    addRow(root, "Monsters", listToText(users.Monsters))
    addRow(root, "Events",  listToText(users.Events))


        local cast, cd, dur = formatCastAndCooldown(mech)
    ------------------------------------------------------------------
        if cast then
    -- Requirements
            box:tag('tr')
    ------------------------------------------------------------------
                :tag('th'):wikitext('Cast Time'):done()
    local req = rec.Requirements or {}
                :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 req["Required Skills"] and #req["Required Skills"] > 0 then
        if cost then
         local skillParts = {}
            box:tag('tr')
        for _, rs in ipairs(req["Required Skills"]) do
                :tag('th'):wikitext('Cost'):done()
            local name  = rs["Skill Name"] or rs["Skill ID"] or "Unknown"
                 :tag('td'):wikitext(cost):done()
            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
         end
        addRow(root, "Required skills", table.concat(skillParts, ", "))
     end
     end


     -- Statuses
     addRow(root, "Required weapons", listToText(req["Required Weapons"]))
     local applied = formatStatusesApplied(rec)
    addRow(root, "Required stances", listToText(req["Required Stances"]))
     if applied then
 
        box:tag('tr')
    ------------------------------------------------------------------
            :tag('th'):wikitext('Statuses Applied'):done()
    -- Type
            :tag('td'):wikitext(applied):done()
    ------------------------------------------------------------------
     end
     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


     local removed = formatStatusesRemoved(rec)
     addRow(root, "Damage type", damageType)
     if removed then
     addRow(root, "Element",    element)
        box:tag('tr')
    addRow(root, "Targeting",  target)
            :tag('th'):wikitext('Statuses Removed'):done()
    addRow(root, "Cast type",  castType)
            :tag('td'):wikitext(removed):done()
    end


     return tostring(box)
     ------------------------------------------------------------------
end
    -- Mechanics
    ------------------------------------------------------------------
    local mech        = rec.Mechanics or {}
    local basicTimings = mech["Basic Timings"] or {}
    local resource    = mech["Resource Cost"] or {}


----------------------------------------------------------------------
    if mech.Range then
-- Public entry point
        addRow(root, "Range", string.format("%.2f", mech.Range))
----------------------------------------------------------------------
    end


function p.infobox(frame)
    addRow(root, "Cast time", formatBasePer(basicTimings["Cast Time"]))
     -- Support both direct #invoke and wrapper templates
     addRow(root, "Cooldown",  formatBasePer(basicTimings["Cooldown"]))
     local parent = frame:getParent()
     addRow(root, "Duration",  formatBasePer(basicTimings["Duration"]))
    local args = parent and parent.args or frame.args


     local id  = args.id or args[1]
     local manaCost = formatManaCost(resource["Mana Cost"])
     local name = args.name
     addRow(root, "Mana cost", manaCost)


     local rec = nil
    ------------------------------------------------------------------
    -- Damage + Scaling
    ------------------------------------------------------------------
     local dmg    = rec.Damage or {}
    local mainDmg = formatMainDamage(dmg["Main Damage"])
    local scaling = formatScaling(dmg.Scaling)


     if id and id ~= '' then
     addRow(root, "Main damage", mainDmg)
        rec = getSkillById(id)
     addRow(root, "Scaling",    scaling)
     end


     if not rec and name and name ~= '' then
     ------------------------------------------------------------------
        rec = findSkillByName(name)
    -- Status interactions
     end
    ------------------------------------------------------------------
    local statusApps = formatStatusApplications(rec["Status Applications"])
     addRow(root, "Status applications", statusApps)


     if not rec then
     -- We could add Status Removal here later if you want it visible.
        local label = id or name or "?"
        return string.format('<strong>Unknown skill: %s</strong>', label)
    end


     return buildInfobox(rec)
     return tostring(root)
end
end


return p
return p