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
No edit summary
No edit summary
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:GameSkills
--
-- Renders active skill data (from Data:skills.json) into an infobox-style table
-- and can also list all skills for a given user/class.
--
-- Usage (single skill):
--  {{Skill|Heal}}
--  {{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")


Line 17: Line 31:


local function getArgs(frame)
local function getArgs(frame)
    -- Prefer parent template args if present (usual #invoke pattern)
     local parent = frame:getParent()
     local parent = frame:getParent()
     if parent then
     if parent then
Line 25: Line 38:
end
end


local function listToText(list)
local function listToText(list, sep)
     if not list or #list == 0 then
     if type(list) ~= "table" or #list == 0 then
         return nil
         return nil
     end
     end
     return table.concat(list, ", ")
     return table.concat(list, sep or ", ")
end
end


Line 41: Line 54:
end
end


local function formatBasePer(block)
local function addSectionHeader(tbl, label)
     if not block or type(block) ~= "table" then
    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)
     if not id or id == "" then
         return nil
         return nil
     end
     end
     local base = block.Base
     local dataset = getSkills()
     local per  = block["Per Level"]
     local byId = dataset.byId or {}
    return byId[id]
end


    if base and per then
-- Lookup by display Name (for editors)
        return string.format("%.2f (%.2f / Lv)", base, per)
local function findSkillByName(name)
    elseif base then
     if not name or name == "" then
        return string.format("%.2f", base)
     elseif per then
        return string.format("%.2f / Lv", per)
    else
         return nil
         return nil
     end
     end
    local dataset = getSkills()
    for _, rec in ipairs(dataset.records or {}) do
        if rec["Name"] == name then
            return rec
        end
    end
    return nil
end
end


local function formatManaCost(block)
----------------------------------------------------------------------
     if not block or type(block) ~= "table" then
-- Formatting helpers
----------------------------------------------------------------------
 
local function formatBasePer(block)
     if type(block) ~= "table" then
         return nil
         return nil
     end
     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
    end
    return table.concat(parts, ", ")
end


     local base = block.Base
local function formatMainDamage(list)
    local per  = block["Per Level"]
    if type(list) ~= "table" or #list == 0 then
        return nil
    end
     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


     if base and per then
local function formatReflectDamage(list)
         return string.format("%.0f (%.0f / Lv)", base, per)
     if type(list) ~= "table" or #list == 0 then
    elseif base then
         return nil
        return string.format("%.0f", base)
    end
    elseif per then
    local parts = {}
        return string.format("%.0f / Lv", per)
    for _, d in ipairs(list) do
     else
        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
     if #parts == 0 then
         return nil
         return nil
     end
     end
    return table.concat(parts, "<br />")
end
end


local function formatMainDamage(damageTable)
local function formatScaling(list)
     if not damageTable or #damageTable == 0 then
     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 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
    if #parts == 0 then
         return nil
         return nil
     end
     end
    return table.concat(parts, "<br />")
end


     -- Current schema only has one entry, but we’ll still handle arrays.
local function formatArea(area)
     if type(area) ~= "table" then
        return nil
    end
     local parts = {}
     local parts = {}
     for _, entry in ipairs(damageTable) do
     local size = area["Area Size"]
        local base = entry["Base %"]
    if size and size ~= "" then
        local per = entry["Per Level %"]
        table.insert(parts, "Size: " .. tostring(size))
         local txt
    end
    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
    return table.concat(parts, "<br />")
end


        if base and per then
local function formatTimingBlock(bt)
            txt = string.format("Base %.2f, +%.2f / Lv", base, per)
    if type(bt) ~= "table" then
        elseif base then
         return nil
            txt = string.format("Base %.2f", base)
    end
        elseif per then
    local parts = {}
            txt = string.format("+%.2f / Lv", per)
         end


    local function add(name, key)
        local block = bt[key]
        local txt = formatBasePer(block)
         if txt then
         if txt then
             table.insert(parts, txt)
             table.insert(parts, name .. ": " .. txt)
         end
         end
    end
    add("Cast Time", "Cast Time")
    add("Cooldown", "Cooldown")
    add("Duration", "Duration")
    if bt["Effect Cast Time"] ~= nil then
        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


Line 106: Line 259:
         return nil
         return nil
     end
     end
     return table.concat(parts, "; ")
     return table.concat(parts, "<br />")
end
end


local function formatScaling(scalingList)
local function formatResourceCost(rc)
     if not scalingList or #scalingList == 0 then
     if type(rc) ~= "table" then
         return nil
         return nil
    end
    local parts = {}
    local mana = rc["Mana Cost"]
    local hp  = rc["Health Cost"]
    local manaTxt = formatBasePer(mana)
    if manaTxt then
        table.insert(parts, "MP: " .. manaTxt)
    end
    local hpTxt = formatBasePer(hp)
    if hpTxt then
        table.insert(parts, "HP: " .. hpTxt)
     end
     end


    if #parts == 0 then
        return nil
    end
    return table.concat(parts, "<br />")
end
local function formatCombo(combo)
    if type(combo) ~= "table" then
        return nil
    end
     local parts = {}
     local parts = {}
     for _, s in ipairs(scalingList) do
    if combo.Type then
         local name = s["Scaling Name"] or s["Scaling ID"] or "Unknown"
        table.insert(parts, "Type: " .. tostring(combo.Type))
         local pct  = s.Percent
    end
         if pct then
    if combo.Duration ~= nil then
             table.insert(parts, string.format("%s: %.2f", name, pct))
        table.insert(parts, "Duration: " .. tostring(combo.Duration))
        else
    end
            table.insert(parts, name)
    if combo.Percent ~= nil then
        table.insert(parts, string.format("Bonus: %s%%", tostring(combo.Percent * 100)))
    end
    if #parts == 0 then
        return nil
    end
    return table.concat(parts, ", ")
end
 
local function formatMechanicEffects(effects)
    if type(effects) ~= "table" then
        return nil
    end
    local parts = {}
     for name, block in pairs(effects) do
         if type(block) == "table" then
            local bp = formatBasePer(block)
            local seg = name
            if bp then
                seg = seg .. " " .. bp
            end
            table.insert(parts, seg)
        end
    end
    if #parts == 0 then
        return nil
    end
    return table.concat(parts, "<br />")
end
 
local function formatModifiers(mods)
    if type(mods) ~= "table" then
         return nil
    end
    local parts = {}
 
    local function collect(label, sub)
        if type(sub) ~= "table" then
            return
        end
        local flags = {}
        for k, v in pairs(sub) do
            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
    collect("Movement", mods["Movement Modifiers"])
    collect("Combat",  mods["Combat Modifiers"])
    collect("Special",  mods["Special Modifiers"])


     if #parts == 0 then
     if #parts == 0 then
         return nil
         return nil
     end
     end
     return table.concat(parts, ", ")
     return table.concat(parts, "<br />")
end
end


local function formatStatusApplications(list)
local function formatStatusApplications(list)
     if not list or #list == 0 then
     if type(list) ~= "table" or #list == 0 then
         return nil
         return nil
     end
     end
     local parts = {}
     local parts = {}
     for _, s in ipairs(list) do
     for _, s in ipairs(list) do
         local scope = s.Scope or "Target"
         if type(s) == "table" then
        local name   = s["Status Name"] or s["Status ID"] or "Unknown status"
            local scope = s.Scope or "Target"
        local dur   = s.Duration and s.Duration.Base
            local name = s["Status Name"] or s["Status ID"] or "Unknown status"
        local chance = s.Chance and s.Chance.Base
 
            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 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


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


        if scope and scope ~= "" then
             table.insert(parts, seg)
             seg = scope .. ": " .. seg
         end
         end
    end
    if #parts == 0 then
        return nil
    end
    return table.concat(parts, "<br />")
end


         local detailParts = {}
local function formatStatusRemoval(list)
         if dur then
    if type(list) ~= "table" or #list == 0 then
             table.insert(detailParts, string.format("Dur %.2f", dur))
         return nil
        end
    end
        if chance then
    local parts = {}
             -- Stored as 0–1; we’ll keep it raw to avoid guessing units.
    for _, r in ipairs(list) do
             table.insert(detailParts, string.format("Chance %.2f", chance))
         if type(r) == "table" then
             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
    if #parts == 0 then
        return nil
    end
    return table.concat(parts, "<br />")
end


        if #detailParts > 0 then
local function formatEvents(list)
             seg = seg .. " (" .. table.concat(detailParts, ", ") .. ")"
    if type(list) ~= "table" or #list == 0 then
        return nil
    end
    local parts = {}
    for _, ev in ipairs(list) do
        if type(ev) == "table" then
             local action = ev.Action or "On event"
            local name  = ev["Skill Name"] or ev["Skill ID"] or "Unknown skill"
            local seg    = string.format("%s → %s", action, name)
            table.insert(parts, seg)
         end
         end
        table.insert(parts, seg)
     end
     end
 
    if #parts == 0 then
     return table.concat(parts, "; ")
        return nil
    end
     return table.concat(parts, "<br />")
end
end


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Public: infobox
-- User matching (for auto lists on class pages)
----------------------------------------------------------------------
----------------------------------------------------------------------


function p.infobox(frame)
local function skillMatchesUser(rec, userName)
     local args = getArgs(frame)
     if type(rec) ~= "table" or not userName or userName == "" then
     local id  = args.id or args[1]
        return false
     end


     if not id or id == "" then
    local users = rec.Users
         return "[[Category:Pages with missing skill id]]"
     if type(users) ~= "table" then
         return false
     end
     end


     local dataset = getSkills()
     local userLower = mw.ustring.lower(userName)
    local rec    = dataset.byId[id]


     if not rec then
     local function listHas(list)
         return "[[Category:Pages with unknown skill id|" .. mw.text.nowiki(id) .. "]]"
        if type(list) ~= "table" then
            return false
        end
         for _, v in ipairs(list) do
            if type(v) == "string" and mw.ustring.lower(v) == userLower then
                return true
            end
        end
        return false
     end
     end


    if listHas(users.Classes)  then return true end
    if listHas(users.Summons)  then return true end
    if listHas(users.Monsters) then return true end
    if listHas(users.Events)  then return true end
    return false
end
----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------
local function buildInfobox(rec)
     local root = mw.html.create("table")
     local root = mw.html.create("table")
     root:addClass("wikitable spiritvale-skill-infobox")
     root:addClass("wikitable spiritvale-skill-infobox")


     -- Header: icon + name
     -- ==========================================================
    -- Top "hero" row: icon + name (left), description (right)
    -- ==========================================================
     local icon  = rec.Icon
     local icon  = rec.Icon
     local title = rec.Name or id
     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")
 
    -- Left cell: icon + name
    local leftCell = headerRow:tag("th")
    leftCell:addClass("spiritvale-infobox-main-left")


     local header = root:tag("tr")
     local leftInner = leftCell:tag("div")
     local headerCell = header:tag("th")
     leftInner:addClass("spiritvale-infobox-main-left-inner")
    headerCell:attr("colspan", 2)


    local titleText = ""
     if icon and icon ~= "" then
     if icon and icon ~= "" then
         titleText = string.format("[[File:%s|64px|link=]] ", icon)
         leftInner:wikitext(string.format("[[File:%s|80px|link=]]", icon))
    end
 
    leftInner:tag("div")
        :addClass("spiritvale-infobox-title")
        :wikitext(title)
 
    -- Right cell: italic description
    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
    titleText = titleText .. (title or id)
    headerCell:wikitext(titleText)


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Basic info
     -- General
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     addRow(root, "Description", rec.Description)
     addSectionHeader(root, "General")
 
    -- Description now lives in the hero row.
    -- addRow(root, "Description", rec.Description)
 
     addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"]))
     addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"]))


    ------------------------------------------------------------------
    -- Users
    ------------------------------------------------------------------
     local users = rec.Users or {}
     local users = rec.Users or {}
     addRow(root, "Classes",  listToText(users.Classes))
     addRow(root, "Classes",  listToText(users.Classes))
Line 224: Line 556:
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local req = rec.Requirements or {}
     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 req["Required Skills"] and #req["Required Skills"] > 0 then
        if type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0 then
        local skillParts = {}
            local skillParts = {}
        for _, rs in ipairs(req["Required Skills"]) do
            for _, rs in ipairs(req["Required Skills"]) do
            local name  = rs["Skill Name"] or rs["Skill ID"] or "Unknown"
                local name  = rs["Skill Name"] or rs["Skill ID"] or "Unknown"
            local level = rs["Required Level"]
                local level = rs["Required Level"]
            if level then
                if level then
                table.insert(skillParts, string.format("%s (Lv.%s)", name, level))
                    table.insert(skillParts, string.format("%s (Lv.%s)", name, level))
            else
                else
                table.insert(skillParts, name)
                    table.insert(skillParts, name)
                end
             end
             end
            addRow(root, "Required skills", table.concat(skillParts, ", "))
         end
         end
         addRow(root, "Required skills", table.concat(skillParts, ", "))
 
         addRow(root, "Required weapons", listToText(req["Required Weapons"]))
        addRow(root, "Required stances", listToText(req["Required Stances"]))
     end
     end
    addRow(root, "Required weapons", listToText(req["Required Weapons"]))
    addRow(root, "Required stances", listToText(req["Required Stances"]))


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Type
     -- Type
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local t = rec.Type or {}
     local typeBlock = rec.Type or {}
     local damageType = t["Damage Type"] and t["Damage Type"].Name
     if next(typeBlock) ~= nil then
    local element    = t["Element Type"] and t["Element Type"].Name
        addSectionHeader(root, "Type")
    local target    = t["Target Type"] and t["Target Type"].Name
 
    local castType  = t["Cast Type"]    and t["Cast Type"].Name
        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


    addRow(root, "Damage type", damageType)
        local ct = typeBlock["Cast Type"]
    addRow(root, "Element",    element)
        if type(ct) == "table" and ct.Name then
    addRow(root, "Targeting",  target)
            addRow(root, "Cast type", ct.Name)
    addRow(root, "Cast type",   castType)
        end
    end


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Mechanics
     -- Mechanics
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local mech         = rec.Mechanics or {}
     local mech = rec.Mechanics or {}
     local basicTimings = mech["Basic Timings"] or {}
     if next(mech) ~= nil then
    local resource    = mech["Resource Cost"] or {}
        addSectionHeader(root, "Mechanics")
 
        if mech.Range ~= nil then
            addRow(root, "Range", tostring(mech.Range))
        end
 
        local areaText = formatArea(mech.Area)
        addRow(root, "Area", areaText)
 
        if mech["Autocast Multiplier"] ~= nil then
            addRow(root, "Autocast multiplier", tostring(mech["Autocast Multiplier"]))
        end


    if mech.Range then
        local btText = formatTimingBlock(mech["Basic Timings"])
         addRow(root, "Range", string.format("%.2f", mech.Range))
        addRow(root, "Timing", btText)
 
        local rcText = formatResourceCost(mech["Resource Cost"])
         addRow(root, "Resource cost", rcText)
 
        local comboText = formatCombo(mech.Combo)
        addRow(root, "Combo", comboText)
 
        local effText = formatMechanicEffects(mech.Effects)
        addRow(root, "Special mechanics", effText)
     end
     end


     addRow(root, "Cast time", formatBasePer(basicTimings["Cast Time"]))
     ------------------------------------------------------------------
    addRow(root, "Cooldown", formatBasePer(basicTimings["Cooldown"]))
    -- Damage & Healing
    addRow(root, "Duration", formatBasePer(basicTimings["Duration"]))
    ------------------------------------------------------------------
    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
 
        local mainText = formatMainDamage(dmg["Main Damage"])
        addRow(root, "Main damage", mainText)
 
        local reflText = formatReflectDamage(dmg["Reflect Damage"])
        addRow(root, "Reflect damage", reflText)
 
        local scaleText = formatScaling(dmg.Scaling)
        addRow(root, "Scaling", scaleText)
    end


     local manaCost = formatManaCost(resource["Mana Cost"])
    ------------------------------------------------------------------
    addRow(root, "Mana cost", manaCost)
    -- Modifiers
    ------------------------------------------------------------------
     local modsText = formatModifiers(rec.Modifiers)
    if modsText then
        addSectionHeader(root, "Modifiers")
        addRow(root, "Flags", modsText)
    end


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Damage + Scaling
     -- Status
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local dmg    = rec.Damage or {}
     local statusApps = formatStatusApplications(rec["Status Applications"])
     local mainDmg = formatMainDamage(dmg["Main Damage"])
     local statusRem  = formatStatusRemoval(rec["Status Removal"])
     local scaling = formatScaling(dmg.Scaling)
     if statusApps or statusRem then
        addSectionHeader(root, "Status effects")
        addRow(root, "Applies", statusApps)
        addRow(root, "Removes", statusRem)
    end


     addRow(root, "Main damage", mainDmg)
     ------------------------------------------------------------------
    addRow(root, "Scaling",    scaling)
    -- Events
    ------------------------------------------------------------------
    local eventsText = formatEvents(rec.Events)
    if eventsText then
        addSectionHeader(root, "Events")
        addRow(root, "Triggers", eventsText)
     end


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Status interactions
     -- Notes
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local statusApps = formatStatusApplications(rec["Status Applications"])
    if type(rec.Notes) == "table" and #rec.Notes > 0 then
     addRow(root, "Status applications", statusApps)
        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
 
    if #matches == 0 then
        return string.format(
            "<strong>No skills found for:</strong> %s",
            mw.text.nowiki(userName)
        )
    end
 
    local root = mw.html.create("div")
    root:addClass("spiritvale-skill-list")


     -- We could add Status Removal here later if you want it visible.
     for _, rec in ipairs(matches) do
        root:wikitext(buildInfobox(rec))
    end


     return tostring(root)
     return tostring(root)
end
----------------------------------------------------------------------
-- Public: single-skill or auto-list dispatcher
----------------------------------------------------------------------
function p.infobox(frame)
    local args = getArgs(frame)
    -- Allow three styles:
    --  {{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
    -- 1) Prefer display Name
    if name and name ~= "" then
        rec = findSkillByName(name)
    end
    -- 2) Fallback: internal ID
    if not rec and id and id ~= "" then
        rec = getSkillById(id)
    end
    -- 3) If still nothing, decide if this is "list mode" or truly unknown.
    if not rec then
        local pageTitle = mw.title.getCurrentTitle()
        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
    -- Normal single-skill behavior
    return buildInfobox(rec)
end
end


return p
return p