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
(7 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:GameSkills
-- Module:GameSkills
--
--
-- Renders skill data (from Data:skills.json) into a nice infobox/table.
-- Renders active skill data (from Data:skills.json) into an infobox-style table
-- Data is loaded via Module:GameData.
-- and can also list all skills for a given user/class.
--
--
-- Typical usage (via Template:Skill):
-- Usage (single skill):
--  {{Skill|id=Dispell}}
--  {{Skill|Heal}}
--  {{Skill|name=Absolution}} -- slower fallback by display name
--  {{Skill|name=Heal}}
--  {{Skill|id=Heal_InternalId}}
--
-- Usage (auto-list on class page, e.g. "Acolyte"):
--  {{Skill}}                  -> lists all Acolyte skills (page name)
--  {{Skill|Acolyte}}          -> same, if no skill literally called "Acolyte"


local GameData = require('Module:GameData')
local GameData = require("Module:GameData")


local p = {}
local p = {}


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


local function getDataset()
local skillsCache
     return GameData.loadSkills()
 
local function getSkills()
     if not skillsCache then
        skillsCache = GameData.loadSkills()
    end
    return skillsCache
end
 
local function getArgs(frame)
    local parent = frame:getParent()
    if parent then
        return parent.args
    end
    return frame.args
end
end


-- Fast lookup by Internal Name
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
 
local function addSectionHeader(tbl, label)
    local row = tbl:tag("tr")
    local cell = row:tag("th")
    cell:attr("colspan", 2)
    cell:addClass("spiritvale-infobox-section-header")
    cell:wikitext(label)
end
 
-- Lookup by Internal Name
local function getSkillById(id)
local function getSkillById(id)
     if not id or id == '' then
     if not id or id == "" then
         return nil
         return nil
     end
     end
     local dataset = getDataset()
     local dataset = getSkills()
     local byId = dataset.byId or {}
     local byId = dataset.byId or {}
     return byId[id]
     return byId[id]
end
end


-- Slower fallback: lookup by display Name
-- Lookup by display Name (for editors)
local function findSkillByName(name)
local function findSkillByName(name)
     if not name or name == '' then
     if not name or name == "" then
         return nil
         return nil
     end
     end
     local dataset = getDataset()
     local dataset = getSkills()
     for _, rec in ipairs(dataset.records or {}) do
     for _, rec in ipairs(dataset.records or {}) do
         if rec["Name"] == name then
         if rec["Name"] == name then
Line 48: Line 90:
----------------------------------------------------------------------
----------------------------------------------------------------------


local function formatTypeLine(t)
local function formatBasePer(block)
     if type(t) ~= 'table' then
     if type(block) ~= "table" then
        return nil
    end
    local parts = {}
    if block.Base ~= nil then
        table.insert(parts, string.format("Base %s", tostring(block.Base)))
    end
    if block["Per Level"] ~= nil then
        table.insert(parts, string.format("%s / Lv", tostring(block["Per Level"])))
    end
    if #parts == 0 then
         return nil
         return nil
     end
     end
    return table.concat(parts, ", ")
end


local function formatMainDamage(list)
    if type(list) ~= "table" or #list == 0 then
        return nil
    end
     local parts = {}
     local parts = {}
    for _, d in ipairs(list) do
        if type(d) == "table" then
            local kind = d.Type or "Damage"
            local base = d["Base %"]
            local per  = d["Per Level %"]
            local seg  = kind
            local detail = {}
            if base ~= nil then
                table.insert(detail, string.format("Base %s%%", tostring(base)))
            end
            if per ~= nil then
                table.insert(detail, string.format("%s%% / Lv", tostring(per)))
            end
            if d["ATK-Based"] then
                table.insert(detail, "ATK-based")
            end
            if d["MATK-Based"] then
                table.insert(detail, "MATK-based")
            end
            if #detail > 0 then
                seg = seg .. " – " .. table.concat(detail, ", ")
            end
            table.insert(parts, seg)
        end
    end
    if #parts == 0 then
        return nil
    end
    return table.concat(parts, "<br />")
end


     local dmg = t["Damage Type"]
local function formatReflectDamage(list)
    if dmg and dmg["Name"] then
     if type(list) ~= "table" or #list == 0 then
        table.insert(parts, dmg["Name"])
        return nil
    end
    local parts = {}
    for _, d in ipairs(list) do
        if type(d) == "table" then
            local base = d["Base %"]
            local per  = d["Per Level %"]
            local seg  = "Reflect"
            local detail = {}
            if base ~= nil then
                table.insert(detail, string.format("Base %s%%", tostring(base)))
            end
            if per ~= nil then
                table.insert(detail, string.format("%s%% / Lv", tostring(per)))
            end
            if #detail > 0 then
                seg = seg .. " – " .. table.concat(detail, ", ")
            end
            table.insert(parts, seg)
        end
     end
     end
 
     if #parts == 0 then
     local elem = t["Element Type"]
         return nil
    if elem and elem["Name"] then
         table.insert(parts, elem["Name"])
     end
     end
    return table.concat(parts, "<br />")
end


    local target = t["Target Type"]
local function formatScaling(list)
     if target and target["Name"] then
     if type(list) ~= "table" or #list == 0 then
         table.insert(parts, target["Name"])
         return nil
     end
     end
 
    local parts = {}
     local cast = t["Cast Type"]
     for _, s in ipairs(list) do
    if cast and cast["Name"] then
        if type(s) == "table" then
        table.insert(parts, cast["Name"])
            local name = s["Scaling Name"] or s["Scaling ID"] or "Unknown"
            local pct  = s.Percent
            local seg  = name
            local detail = {}
            if pct ~= nil then
                table.insert(detail, string.format("%s%%", tostring(pct)))
            end
            if s["ATK-Based"] then
                table.insert(detail, "ATK-based")
            end
            if s["MATK-Based"] then
                table.insert(detail, "MATK-based")
            end
            if #detail > 0 then
                seg = seg .. " – " .. table.concat(detail, ", ")
            end
            table.insert(parts, seg)
        end
     end
     end
     if #parts == 0 then
     if #parts == 0 then
         return nil
         return nil
     end
     end
 
     return table.concat(parts, "<br />")
     return table.concat(parts, " ")
end
end


local function formatUsers(users)
local function formatArea(area)
     if type(users) ~= 'table' then
     if type(area) ~= "table" then
         return nil
         return nil
     end
     end
 
     local parts = {}
     local chunks = {}
     local size = area["Area Size"]
 
     if size and size ~= "" then
     local classes = users["Classes"]
        table.insert(parts, "Size: " .. tostring(size))
     if type(classes) == 'table' and #classes > 0 then
    end
         table.insert(chunks, "Classes: " .. table.concat(classes, ", "))
    local dist = area["Area Distance"]
    local eff  = area["Effective Distance"]
    local distText = formatBasePer(dist)
    if distText then
         table.insert(parts, "Distance: " .. distText)
    end
    if eff ~= nil then
        table.insert(parts, string.format("Effective: %s", tostring(eff)))
    end
    if #parts == 0 then
        return nil
     end
     end
    return table.concat(parts, "<br />")
end


    local summons = users["Summons"]
local function formatTimingBlock(bt)
     if type(summons) == 'table' and #summons > 0 then
     if type(bt) ~= "table" then
         table.insert(chunks, "Summons: " .. table.concat(summons, ", "))
         return nil
     end
     end
    local parts = {}


     local monsters = users["Monsters"]
     local function add(name, key)
    if type(monsters) == 'table' and #monsters > 0 then
        local block = bt[key]
        table.insert(chunks, "Monsters: " .. table.concat(monsters, ", "))
        local txt = formatBasePer(block)
        if txt then
            table.insert(parts, name .. ": " .. txt)
        end
     end
     end


     -- (Users.Events exists in the data, but we skip it for now – mostly internal hooks.)
     add("Cast Time", "Cast Time")
    add("Cooldown", "Cooldown")
    add("Duration", "Duration")


     if #chunks == 0 then
     if bt["Effect Cast Time"] ~= nil then
         return nil
        table.insert(parts, "Effect Cast Time: " .. tostring(bt["Effect Cast Time"]))
    end
    if bt["Damage Delay"] ~= nil then
         table.insert(parts, "Damage Delay: " .. tostring(bt["Damage Delay"]))
    end
    if bt["Effect Remove Delay"] ~= nil then
        table.insert(parts, "Effect Remove Delay: " .. tostring(bt["Effect Remove Delay"]))
     end
     end


    return table.concat(chunks, "<br />")
     if #parts == 0 then
end
 
local function formatArea(mech)
     if type(mech) ~= 'table' then
         return nil
         return nil
     end
     end
    return table.concat(parts, "<br />")
end


    local area = mech["Area"]
local function formatResourceCost(rc)
     if type(area) ~= 'table' then
     if type(rc) ~= "table" then
         return nil
         return nil
     end
     end
     local parts = {}
     local parts = {}
    local mana = rc["Mana Cost"]
    local hp  = rc["Health Cost"]


     local size = area["Area Size"]
     local manaTxt = formatBasePer(mana)
     if size then
     if manaTxt then
         table.insert(parts, tostring(size))
         table.insert(parts, "MP: " .. manaTxt)
     end
     end


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


Line 138: Line 283:
         return nil
         return nil
     end
     end
 
     return table.concat(parts, "<br />")
     return table.concat(parts, " ")
end
end


local function formatCastAndCooldown(mech)
local function formatCombo(combo)
     if type(mech) ~= 'table' then
     if type(combo) ~= "table" then
         return nil, nil, nil
         return nil
    end
    local parts = {}
    if combo.Type then
        table.insert(parts, "Type: " .. tostring(combo.Type))
    end
    if combo.Duration ~= nil then
        table.insert(parts, "Duration: " .. tostring(combo.Duration))
    end
    if combo.Percent ~= nil then
        table.insert(parts, string.format("Bonus: %s%%", tostring(combo.Percent * 100)))
    end
    if #parts == 0 then
        return nil
     end
     end
    return table.concat(parts, ", ")
end


    local basic = mech["Basic Timings"]
local function formatMechanicEffects(effects)
     if type(basic) ~= 'table' then
     if type(effects) ~= "table" then
         return nil, nil, nil
         return nil
     end
     end
 
     local parts = {}
     local cast, cd, dur
     for name, block in pairs(effects) do
 
        if type(block) == "table" then
     local castBlock = basic["Cast Time"]
            local bp = formatBasePer(block)
    if type(castBlock) == 'table' and castBlock["Base"] then
            local seg = name
        cast = tostring(castBlock["Base"]) .. " s"
            if bp then
        if castBlock["Per Level"] and castBlock["Per Level"] ~= 0 then
                seg = seg .. " " .. bp
            local per = castBlock["Per Level"]
             end
             local sign = per > 0 and "+" or ""
             table.insert(parts, seg)
             cast = cast .. string.format(" (%s%g per level)", sign, per)
         end
         end
     end
     end
    if #parts == 0 then
        return nil
    end
    return table.concat(parts, "<br />")
end


    local cdBlock = basic["Cooldown"]
local function formatModifiers(mods)
     if type(cdBlock) == 'table' and cdBlock["Base"] then
     if type(mods) ~= "table" then
         cd = tostring(cdBlock["Base"]) .. " s"
         return nil
        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
     end
    local parts = {}


     local durBlock = basic["Duration"]
     local function collect(label, sub)
    if type(durBlock) == 'table' and durBlock["Base"] then
        if type(sub) ~= "table" then
         dur = tostring(durBlock["Base"]) .. " s"
            return
        if durBlock["Per Level"] and durBlock["Per Level"] ~= 0 then
        end
             local per = durBlock["Per Level"]
         local flags = {}
            local sign = per > 0 and "+" or ""
        for k, v in pairs(sub) do
             dur = dur .. string.format(" (%s%g per level)", sign, per)
            if v then
                table.insert(flags, k)
             end
        end
        table.sort(flags)
        if #flags > 0 then
             table.insert(parts, string.format("%s: %s", label, table.concat(flags, ", ")))
         end
         end
     end
     end


     return cast, cd, dur
     collect("Movement", mods["Movement Modifiers"])
end
    collect("Combat",   mods["Combat Modifiers"])
    collect("Special",  mods["Special Modifiers"])


local function formatResourceCost(mech)
     if #parts == 0 then
     if type(mech) ~= 'table' then
         return nil
         return nil
     end
     end
    return table.concat(parts, "<br />")
end


    local cost = mech["Resource Cost"]
local function formatStatusApplications(list)
     if type(cost) ~= 'table' then
     if type(list) ~= "table" or #list == 0 then
         return nil
         return nil
     end
     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 seg = scope .. " – " .. name
            local detail = {}
            local dur = s.Duration
            if type(dur) == "table" then
                local t = formatBasePer(dur)
                if t then
                    table.insert(detail, "Duration " .. t)
                end
            end


    local parts = {}
            local ch = s.Chance
            if type(ch) == "table" then
                local t = formatBasePer(ch)
                if t then
                    table.insert(detail, "Chance " .. t)
                end
            end
 
            if s["Fixed Duration"] then
                table.insert(detail, "Fixed duration")
            end
 
            if #detail > 0 then
                seg = seg .. " (" .. table.concat(detail, ", ") .. ")"
            end


    local mana = cost["Mana Cost"]
             table.insert(parts, seg)
    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
     end
     end
    if #parts == 0 then
        return nil
    end
    return table.concat(parts, "<br />")
end


     local hp = cost["Health Cost"]
local function formatStatusRemoval(list)
     if type(hp) == 'table' then
     if type(list) ~= "table" or #list == 0 then
        local base = hp["Base"]
        return nil
        local per  = hp["Per Level"]
    end
        if base and per then
    local parts = {}
            table.insert(parts, string.format("HP: %s + %s per level", base, per))
     for _, r in ipairs(list) do
        elseif base then
        if type(r) == "table" then
             table.insert(parts, string.format("HP: %s", base))
            local names = r["Status Name"]
            local label
            if type(names) == "table" then
                label = table.concat(names, ", ")
            elseif type(names) == "string" then
                label = names
            else
                label = "Status"
            end
            local bp = formatBasePer(r)
            local seg = label
            if bp then
                seg = seg .. " – " .. bp
            end
             table.insert(parts, seg)
         end
         end
     end
     end
     if #parts == 0 then
     if #parts == 0 then
         return nil
         return nil
     end
     end
     return table.concat(parts, "<br />")
     return table.concat(parts, "<br />")
end
end


local function formatStatusesApplied(rec)
local function formatEvents(list)
    local apps = rec["Status Applications"]
     if type(list) ~= "table" or #list == 0 then
     if type(apps) ~= 'table' or #apps == 0 then
         return nil
         return nil
     end
     end
 
     local parts = {}
     local names = {}
     for _, ev in ipairs(list) do
     for _, entry in ipairs(apps) do
         if type(ev) == "table" then
         if type(entry) == 'table' then
             local action = ev.Action or "On event"
             local n = entry["Status Name"]
            local name  = ev["Skill Name"] or ev["Skill ID"] or "Unknown skill"
             if type(n) == 'string' then
             local seg    = string.format("%s → %s", action, name)
                table.insert(names, n)
            table.insert(parts, seg)
            end
         end
         end
     end
     end
 
     if #parts == 0 then
     if #names == 0 then
         return nil
         return nil
     end
     end
    return table.concat(parts, "<br />")
end
----------------------------------------------------------------------
-- User matching (for auto lists on class pages)
----------------------------------------------------------------------


     return table.concat(names, ", ")
local function skillMatchesUser(rec, userName)
end
     if type(rec) ~= "table" or not userName or userName == "" then
        return false
    end


local function formatStatusesRemoved(rec)
     local users = rec.Users
     local entries = rec["Status Removal"]
     if type(users) ~= "table" then
     if type(entries) ~= 'table' or #entries == 0 then
         return false
         return nil
     end
     end


     local names = {}
     local userLower = mw.ustring.lower(userName)
     for _, entry in ipairs(entries) do
 
         if type(entry) == 'table' then
     local function listHas(list)
             local list = entry["Status Name"]
         if type(list) ~= "table" then
            if type(list) == 'table' then
             return false
                for _, n in ipairs(list) do
        end
                    if type(n) == 'string' then
        for _, v in ipairs(list) do
                        table.insert(names, n)
            if type(v) == "string" and mw.ustring.lower(v) == userLower then
                    end
                 return true
                 end
             end
             end
         end
         end
        return false
     end
     end


     if #names == 0 then
     if listHas(users.Classes)  then return true end
        return nil
    if listHas(users.Summons)  then return true end
     end
     if listHas(users.Monsters) then return true end
    if listHas(users.Events)  then return true end


     return table.concat(names, ", ")
     return false
end
end


Line 283: Line 495:


local function buildInfobox(rec)
local function buildInfobox(rec)
     local box = mw.html.create('table')
     local root = mw.html.create("table")
        :addClass('sv-skill-infobox')
    root:addClass("wikitable spiritvale-skill-infobox")
 
    -- ==========================================================
    -- Top "hero" row: icon + name (left), description (right)
    -- ==========================================================
    local icon  = rec.Icon
    local title = rec.Name or rec["Internal Name"] or "Unknown Skill"
    local desc  = rec.Description or ""
 
    local headerRow = root:tag("tr")
    headerRow:addClass("spiritvale-infobox-main")


     local name = rec["Name"] or rec["Internal Name"] or "Unknown Skill"
    -- Left cell: icon + name
     local leftCell = headerRow:tag("th")
    leftCell:addClass("spiritvale-infobox-main-left")


     -- Header
     local leftInner = leftCell:tag("div")
    box:tag('tr')
    leftInner:addClass("spiritvale-infobox-main-left-inner")
        :tag('th')
            :attr('colspan', 2)
            :addClass('sv-skill-infobox-title')
            :wikitext(name)
        :done()


    -- Icon
    local icon = rec["Icon"]
     if icon and icon ~= "" then
     if icon and icon ~= "" then
         box:tag('tr')
         leftInner:wikitext(string.format("[[File:%s|80px|link=]]", icon))
            :tag('td')
                :attr('colspan', 2)
                :addClass('sv-skill-infobox-icon')
                :wikitext(string.format('[[File:%s|128px|center]]', icon))
            :done()
     end
     end


     -- Description
     leftInner:tag("div")
     local desc = rec["Description"]
        :addClass("spiritvale-infobox-title")
     if desc and desc ~= "" then
        :wikitext(title)
         box:tag('tr')
 
             :tag('th'):wikitext('Description'):done()
    -- Right cell: italic description
            :tag('td'):wikitext(desc):done()
     local rightCell = headerRow:tag("td")
    rightCell:addClass("spiritvale-infobox-main-right")
 
    local rightInner = rightCell:tag("div")
    rightInner:addClass("spiritvale-infobox-main-right-inner")
 
     if desc ~= "" then
         rightInner:tag("div")
             :addClass("spiritvale-infobox-description")
            :wikitext(string.format("''%s''", desc))
     end
     end


     -- Type line
     ------------------------------------------------------------------
     local typeLine = formatTypeLine(rec["Type"])
    -- General
    if typeLine then
    ------------------------------------------------------------------
         box:tag('tr')
    addSectionHeader(root, "General")
             :tag('th'):wikitext('Type'):done()
 
             :tag('td'):wikitext(typeLine):done()
    -- Description now lives in the hero row.
    -- addRow(root, "Description", rec.Description)
 
    addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"]))
 
     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)
        or (req["Required Weapons"] and #req["Required Weapons"] > 0)
        or (req["Required Stances"] and #req["Required Stances"] > 0) then
 
         addSectionHeader(root, "Requirements")
 
        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"]))
     end
     end


     -- Max Level
     ------------------------------------------------------------------
     local maxLevel = rec["Max Level"]
    -- Type
     if maxLevel then
    ------------------------------------------------------------------
         box:tag('tr')
     local typeBlock = rec.Type or {}
            :tag('th'):wikitext('Max Level'):done()
     if next(typeBlock) ~= nil then
             :tag('td'):wikitext(tostring(maxLevel)):done()
         addSectionHeader(root, "Type")
    end
 
        local dt = typeBlock["Damage Type"]
        if type(dt) == "table" and dt.Name then
            addRow(root, "Damage type", dt.Name)
        end
 
        local et = typeBlock["Element Type"]
        if type(et) == "table" and et.Name then
             addRow(root, "Element", et.Name)
        end
 
        local tt = typeBlock["Target Type"]
        if type(tt) == "table" and tt.Name then
            addRow(root, "Target", tt.Name)
        end


    -- Users
        local ct = typeBlock["Cast Type"]
    local usersText = formatUsers(rec["Users"])
        if type(ct) == "table" and ct.Name then
    if usersText then
             addRow(root, "Cast type", ct.Name)
        box:tag('tr')
        end
             :tag('th'):wikitext('Users'):done()
            :tag('td'):wikitext(usersText):done()
     end
     end


    ------------------------------------------------------------------
     -- Mechanics
     -- Mechanics
     local mech = rec["Mechanics"]
    ------------------------------------------------------------------
     if type(mech) == 'table' then
     local mech = rec.Mechanics or {}
         if mech["Range"] then
     if next(mech) ~= nil then
             box:tag('tr')
        addSectionHeader(root, "Mechanics")
                :tag('th'):wikitext('Range'):done()
 
                :tag('td'):wikitext(tostring(mech["Range"])):done()
         if mech.Range ~= nil then
             addRow(root, "Range", tostring(mech.Range))
         end
         end


         local areaText = formatArea(mech)
         local areaText = formatArea(mech.Area)
         if areaText then
        addRow(root, "Area", areaText)
             box:tag('tr')
 
                :tag('th'):wikitext('Area'):done()
         if mech["Autocast Multiplier"] ~= nil then
                :tag('td'):wikitext(areaText):done()
             addRow(root, "Autocast multiplier", tostring(mech["Autocast Multiplier"]))
         end
         end


         local cast, cd, dur = formatCastAndCooldown(mech)
         local btText = formatTimingBlock(mech["Basic Timings"])
         if cast then
         addRow(root, "Timing", btText)
            box:tag('tr')
 
                :tag('th'):wikitext('Cast Time'):done()
        local rcText = formatResourceCost(mech["Resource Cost"])
                :tag('td'):wikitext(cast):done()
        addRow(root, "Resource cost", rcText)
        end
 
        if cd then
        local comboText = formatCombo(mech.Combo)
            box:tag('tr')
        addRow(root, "Combo", comboText)
                :tag('th'):wikitext('Cooldown'):done()
 
                :tag('td'):wikitext(cd):done()
        local effText = formatMechanicEffects(mech.Effects)
        end
        addRow(root, "Special mechanics", effText)
         if dur then
    end
             box:tag('tr')
 
                :tag('th'):wikitext('Duration'):done()
    ------------------------------------------------------------------
                :tag('td'):wikitext(dur):done()
    -- Damage & Healing
    ------------------------------------------------------------------
    local dmg = rec.Damage or {}
    if next(dmg) ~= nil then
        addSectionHeader(root, "Damage and scaling")
 
         if dmg["Healing Present"] then
             addRow(root, "Healing", "Yes")
         end
         end


         local cost = formatResourceCost(mech)
         local mainText = formatMainDamage(dmg["Main Damage"])
         if cost then
         addRow(root, "Main damage", mainText)
            box:tag('tr')
 
                :tag('th'):wikitext('Cost'):done()
        local reflText = formatReflectDamage(dmg["Reflect Damage"])
                :tag('td'):wikitext(cost):done()
        addRow(root, "Reflect damage", reflText)
 
        local scaleText = formatScaling(dmg.Scaling)
        addRow(root, "Scaling", scaleText)
    end
 
    ------------------------------------------------------------------
    -- Modifiers
    ------------------------------------------------------------------
    local modsText = formatModifiers(rec.Modifiers)
    if modsText then
        addSectionHeader(root, "Modifiers")
        addRow(root, "Flags", modsText)
    end
 
    ------------------------------------------------------------------
    -- Status
    ------------------------------------------------------------------
    local statusApps = formatStatusApplications(rec["Status Applications"])
    local statusRem  = formatStatusRemoval(rec["Status Removal"])
    if statusApps or statusRem then
        addSectionHeader(root, "Status effects")
        addRow(root, "Applies", statusApps)
        addRow(root, "Removes", statusRem)
    end
 
    ------------------------------------------------------------------
    -- Events
    ------------------------------------------------------------------
    local eventsText = formatEvents(rec.Events)
    if eventsText then
        addSectionHeader(root, "Events")
        addRow(root, "Triggers", eventsText)
    end
 
    ------------------------------------------------------------------
    -- Notes
    ------------------------------------------------------------------
    if type(rec.Notes) == "table" and #rec.Notes > 0 then
        addSectionHeader(root, "Notes")
        addRow(root, "Notes", table.concat(rec.Notes, "<br />"))
    end
 
    return tostring(root)
end
 
----------------------------------------------------------------------
-- Public: list all skills for a given user/class
----------------------------------------------------------------------
 
function p.listForUser(frame)
    local args = getArgs(frame)
 
    -- Prefer explicit param, then unnamed, then fall back to the current page name.
    local userName = args.user or args[1]
    if not userName or userName == "" then
        userName = mw.title.getCurrentTitle().text
    end
 
    if not userName or userName == "" then
        return "<strong>No user name provided to Skill list.</strong>"
    end
 
    local dataset = getSkills()
    local matches = {}
 
    for _, rec in ipairs(dataset.records or {}) do
        if skillMatchesUser(rec, userName) then
            table.insert(matches, rec)
         end
         end
     end
     end


     -- Statuses
     if #matches == 0 then
    local applied = formatStatusesApplied(rec)
         return string.format(
    if applied then
             "<strong>No skills found for:</strong> %s",
         box:tag('tr')
             mw.text.nowiki(userName)
             :tag('th'):wikitext('Statuses Applied'):done()
        )
             :tag('td'):wikitext(applied):done()
     end
     end


     local removed = formatStatusesRemoved(rec)
     local root = mw.html.create("div")
     if removed then
     root:addClass("spiritvale-skill-list")
        box:tag('tr')
 
            :tag('th'):wikitext('Statuses Removed'):done()
    for _, rec in ipairs(matches) do
            :tag('td'):wikitext(removed):done()
        root:wikitext(buildInfobox(rec))
     end
     end


     return tostring(box)
     return tostring(root)
end
end


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Public entry point
-- Public: single-skill or auto-list dispatcher
----------------------------------------------------------------------
----------------------------------------------------------------------


function p.infobox(frame)
function p.infobox(frame)
    -- Support both direct #invoke and wrapper templates
     local args = getArgs(frame)
     local parent = frame:getParent()
    local args = parent and parent.args or frame.args


     local id   = args.id or args[1]
     -- Allow three styles:
     local name = args.name
    --  {{Skill|Bash}}              -> args[1] = "Bash"  (Name)
    --  {{Skill|name=Bash}}        -> args.name = "Bash"
    --  {{Skill|id=Bash_Internal}}  -> args.id = "Bash_Internal"
    local raw1 = args[1]
     local name = args.name or raw1
    local id  = args.id


     local rec = nil
     local rec


     if id and id ~= '' then
    -- 1) Prefer display Name
         rec = getSkillById(id)
     if name and name ~= "" then
         rec = findSkillByName(name)
     end
     end


     if not rec and name and name ~= '' then
    -- 2) Fallback: internal ID
         rec = findSkillByName(name)
     if not rec and id and id ~= "" then
         rec = getSkillById(id)
     end
     end


    -- 3) If still nothing, decide if this is "list mode" or truly unknown.
     if not rec then
     if not rec then
         local label = id or name or "?"
         local pageTitle = mw.title.getCurrentTitle()
         return string.format('<strong>Unknown skill: %s</strong>', label)
        local pageName  = pageTitle and pageTitle.text or ""
 
        local noExplicitArgs =
            (not raw1 or raw1 == "") and
            (not args.name or args.name == "") and
            (not id or id == "")
 
        -- Case A: {{Skill}} with no parameters on a page → list for that page name.
        if noExplicitArgs then
            return p.listForUser(frame)
        end
 
        -- Case B: {{Skill|Acolyte}} on the "Acolyte" page and no id → treat as list.
        if name and name ~= "" and name == pageName and (not id or id == "") then
            return p.listForUser(frame)
        end
 
        -- Otherwise, genuinely unknown skill.
        local label = name or id or "?"
         return string.format(
            "<strong>Unknown skill:</strong> %s[[Category:Pages with unknown skill|%s]]",
            mw.text.nowiki(label),
            label
        )
     end
     end


    -- Normal single-skill behavior
     return buildInfobox(rec)
     return buildInfobox(rec)
end
end


return p
return p