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

From SpiritVale Wiki
No edit summary
No edit summary
 
(7 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:GamePassives
-- Module:GamePassives
--
--
-- Renders passive skill data (from Data:passives.json) into an infobox-style table.
-- Renders passive skill data (from Data:passives.json) into an
-- Data is loaded via Module:GameData.
-- infobox-style table and can also list all passives for a given user/class.
--
--
-- Supported usage patterns (via Template:Passive):
-- Usage (single passive):
--  {{Passive|Honed Blade}}             -> uses display Name (recommended)
--  {{Passive|Honed Blade}}
--  {{Passive|name=Honed Blade}}         -> explicit Name
--  {{Passive|name=Honed Blade}}
--  {{Passive|id=CritMastery}}           -> Internal Name (power use)
--  {{Passive|id=CritMastery}}
--
-- Usage (auto-list on class page, e.g. "Acolyte"):
--  {{Passive}}                -> lists all Acolyte passives (page name)
--  {{Passive|Acolyte}}        -> same, if no passive literally called "Acolyte"


local GameData = require("Module:GameData")
local GameData = require("Module:GameData")
Line 34: Line 38:
end
end


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


-- Tag body rows so we can style/center without touching the hero row
local function addRow(tbl, label, value)
local function addRow(tbl, label, value)
     if value == nil or value == "" then
     if value == nil or value == "" then
Line 46: Line 51:
     end
     end
     local row = tbl:tag("tr")
     local row = tbl:tag("tr")
    row:addClass("spiritvale-passive-body-row")
     row:tag("th"):wikitext(label):done()
     row:tag("th"):wikitext(label):done()
     row:tag("td"):wikitext(value):done()
     row:tag("td"):wikitext(value):done()
end
-- Tag section header rows as body rows too (for centering)
local function addSectionHeader(tbl, label)
    local row = tbl:tag("tr")
    row:addClass("spiritvale-passive-body-row")
    local cell = row:tag("th")
    cell:attr("colspan", 2)
    cell:addClass("spiritvale-infobox-section-header")
    cell:wikitext(label)
end
end


Line 78: Line 95:
----------------------------------------------------------------------
----------------------------------------------------------------------


local function formatPassiveEffects(list)
local function asUl(items)
     if type(list) ~= "table" or #list == 0 then
     if type(items) ~= "table" or #items == 0 then
         return nil
         return nil
     end
     end
    return '<ul class="spiritvale-infobox-list"><li>'
        .. table.concat(items, "</li><li>")
        .. "</li></ul>"
end


local function formatBasePer(block)
    if type(block) ~= "table" then
        return nil
    end
     local parts = {}
     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
-- Passive Effects: return rows (label/value), not a single text blob
local function passiveEffectRows(list)
    if type(list) ~= "table" or #list == 0 then
        return {}
    end
    local rows = {}


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


             local expr = value.Expression
             local value = eff.Value or {}
            local base = value.Base
             local detail = {}
             local per  = value["Per Level"]


            local seg = typeName
             if value.Base ~= nil then
 
                 table.insert(detail, string.format("Base %s", tostring(value.Base)))
             if expr and expr ~= "" then
                 seg = seg .. string.format(" (%s)", expr)
             end
             end
 
             if value["Per Level"] ~= nil then
             local detail = {}
                 table.insert(detail, string.format("%s / Lv", tostring(value["Per Level"])))
            if base then
                 table.insert(detail, string.format("Base %.2f", base))
             end
             end
             if per then
             if value.Expression ~= nil and value.Expression ~= "" then
                 table.insert(detail, string.format("%.2f / Lv", per))
                 table.insert(detail, tostring(value.Expression))
             end
             end


             if #detail > 0 then
             -- Optional qualifiers (weapon/stance/etc.), if present in data
                seg = seg .. " " .. table.concat(detail, ", ")
            local qual = eff.Weapon or eff["Weapon"] or eff["Weapon Type"]
                      or eff.Stance or eff["Stance"] or eff["Stance Type"]
            if type(qual) == "string" and qual ~= "" then
                table.insert(detail, qual)
             end
             end


             table.insert(parts, seg)
            local right = (#detail > 0) and table.concat(detail, ", ") or "—"
             table.insert(rows, { label = name, value = right })
         end
         end
     end
     end


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


Line 130: Line 168:


     local parts = {}
     local parts = {}
     for _, s in ipairs(list) do
     for _, s in ipairs(list) do
         if type(s) == "table" then
         if type(s) == "table" then
             local scope = s.Scope or "Target"
             local scope = s.Scope or "Target"
             local name   = s["Status Name"] or s["Status ID"] or "Unknown status"
             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
             local seg = scope .. " – " .. name
             if scope and scope ~= "" then
            local detail = {}
                 seg = scope .. ": " .. seg
 
            local dur = s.Duration
             if type(dur) == "table" then
                 local t = formatBasePer(dur)
                if t then
                    table.insert(detail, "Duration " .. t)
                end
             end
             end


             local detailParts = {}
             local ch = s.Chance
             if dur then
             if type(ch) == "table" then
                 table.insert(detailParts, string.format("Dur %.2f", dur))
                 local t = formatBasePer(ch)
                if t then
                    table.insert(detail, "Chance " .. t)
                end
             end
             end
             if chance then
 
                 table.insert(detailParts, string.format("Chance %.2f", chance))
             if s["Fixed Duration"] then
                 table.insert(detail, "Fixed duration")
             end
             end


             if #detailParts > 0 then
             if #detail > 0 then
                 seg = seg .. " (" .. table.concat(detailParts, ", ") .. ")"
                 seg = seg .. " (" .. table.concat(detail, ", ") .. ")"
            end
 
            table.insert(parts, seg)
        end
    end
 
    return asUl(parts)
end
 
local function formatStatusRemoval(list)
    if type(list) ~= "table" or #list == 0 then
        return nil
    end
 
    local parts = {}
    for _, r in ipairs(list) do
        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
             end


            local bp = formatBasePer(r)
            local seg = label
            if bp then
                seg = seg .. " – " .. bp
            end
             table.insert(parts, seg)
             table.insert(parts, seg)
         end
         end
     end
     end


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


     return table.concat(parts, "<br />")
     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"
            table.insert(parts, string.format("%s → %s", action, name))
        end
    end
 
    return asUl(parts)
end
end


local function formatNotes(notes)
local function formatModifiers(mods)
     if type(notes) ~= "table" or #notes == 0 then
     if type(mods) ~= "table" then
         return nil
         return nil
     end
     end
     return table.concat(notes, "<br />")
 
     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
 
    collect("Movement", mods["Movement Modifiers"])
    collect("Combat",  mods["Combat Modifiers"])
    collect("Special",  mods["Special Modifiers"])
 
    return asUl(parts)
end
 
----------------------------------------------------------------------
-- User matching (for auto lists on class pages)
----------------------------------------------------------------------
 
local function passiveMatchesUser(rec, userName)
    if type(rec) ~= "table" or not userName or userName == "" then
        return false
    end
 
    local users = rec.Users
    if type(users) ~= "table" then
        return false
    end
 
    local userLower = mw.ustring.lower(userName)
 
    local function listHas(list)
        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
 
    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
end


Line 178: Line 327:
local function buildInfobox(rec)
local function buildInfobox(rec)
     local root = mw.html.create("table")
     local root = mw.html.create("table")
     root:addClass("wikitable spiritvale-passive-infobox")
     root:addClass("spiritvale-passive-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 rec["Internal Name"] or "Unknown Passive"
     local title = rec.Name or rec["Internal Name"] or "Unknown Passive"
    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
     end
    titleText = titleText .. title
    headerCell:wikitext(titleText)


    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


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Basic info
     -- General
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     addRow(root, "Description", rec.Description)
     addSectionHeader(root, "General")
    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
 
     ------------------------------------------------------------------
     -- Classes intentionally removed (template is used on class pages)
     local users = rec.Users or {}
     local users = rec.Users or {}
    addRow(root, "Classes",  listToText(users.Classes))
     addRow(root, "Summons",  listToText(users.Summons))
     addRow(root, "Summons",  listToText(users.Summons))
     addRow(root, "Monsters", listToText(users.Monsters))
     addRow(root, "Monsters", listToText(users.Monsters))
Line 215: Line 384:
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     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


    if type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0 then
        addSectionHeader(root, "Requirements")
        local skillParts = {}
 
        for _, rs in ipairs(req["Required Skills"]) do
        if type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0 then
            local name  = rs["Skill Name"] or rs["Skill ID"] or "Unknown"
            local skillParts = {}
            local level = rs["Required Level"]
            for _, rs in ipairs(req["Required Skills"]) do
            if level then
                local name  = rs["Skill Name"] or rs["Skill ID"] or "Unknown"
                table.insert(skillParts, string.format("%s (Lv.%s)", name, level))
                local level = rs["Required Level"]
            else
                if level then
                table.insert(skillParts, name)
                    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
         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"]))


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Passive effects
     -- Passive Effects (one row per effect)
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local effectsText = formatPassiveEffects(rec["Passive Effects"])
     local peRows = passiveEffectRows(rec["Passive Effects"])
     addRow(root, "Passive effects", effectsText)
     if #peRows > 0 then
        addSectionHeader(root, "Passive Effects")
        for _, r in ipairs(peRows) do
            addRow(root, r.label, r.value)
        end
    end


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Status interactions
     -- Status Effects
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local statusApps = formatStatusApplications(rec["Status Applications"])
     local statusApps = formatStatusApplications(rec["Status Applications"])
     addRow(root, "Status applications", statusApps)
     local statusRem  = formatStatusRemoval(rec["Status Removal"])
    if statusApps or statusRem then
        addSectionHeader(root, "Status Effects")
        addRow(root, "Applies", statusApps)
        addRow(root, "Removes", statusRem)
    end


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Notes
     -- Modifiers
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local notesText = formatNotes(rec.Notes)
     local modsText = formatModifiers(rec.Modifiers)
     addRow(root, "Notes", notesText)
     if modsText then
 
        addSectionHeader(root, "Modifiers")
    return tostring(root)
        addRow(root, "Flags", modsText)
end
 
local function passiveMatchesUser(rec, userName)
    if type(rec) ~= "table" or not userName or userName == "" then
        return false
     end
     end


     local users = rec.Users
    ------------------------------------------------------------------
     if type(users) ~= "table" then
    -- Events
         return false
    ------------------------------------------------------------------
     local eventsText = formatEvents(rec.Events)
     if eventsText then
        addSectionHeader(root, "Events")
         addRow(root, "Triggers", eventsText)
     end
     end


     local userLower = mw.ustring.lower(userName)
     ------------------------------------------------------------------
 
    -- Notes
     local function listHas(list)
     ------------------------------------------------------------------
        if type(list) ~= "table" then
    if type(rec.Notes) == "table" and #rec.Notes > 0 then
            return false
         addSectionHeader(root, "Notes")
         end
        addRow(root, "Notes", asUl(rec.Notes) or table.concat(rec.Notes, "<br />"))
        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


     -- Adjust this if you *only* want Classes.
     return tostring(root)
    if listHas(users.Classes)  then return true end
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
-- Public: list all passives for a given user/class
----------------------------------------------------------------------


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


     -- Preferred explicit param, then unnamed, then fall back to the current page name.
     -- Prefer explicit param, then unnamed, then fall back to the current page name.
     local userName = args.user or args[1]
     local userName = args.user or args[1]
     if not userName or userName == "" then
     if not userName or userName == "" then
Line 327: Line 503:


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


Line 333: Line 509:
     local args = getArgs(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 raw1 = args[1]
     local name = args.name or raw1
     local name = args.name or raw1
Line 343: Line 515:
     local rec
     local rec


     -- 1) Prefer display Name (what editors actually know)
     -- 1) Prefer display Name
     if name and name ~= "" then
     if name and name ~= "" then
         rec = findPassiveByName(name)
         rec = findPassiveByName(name)
     end
     end


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


     -- 3) If still no record, decide: list mode or unknown passive?
     -- 3) If still nothing, decide if this is "list mode" or truly unknown.
     if not rec then
     if not rec then
         local pageTitle = mw.title.getCurrentTitle()
         local pageTitle = mw.title.getCurrentTitle()
Line 363: Line 535:
             (not id or id == "")
             (not id or id == "")


        -- Case A: {{Passive}} with no parameters on a page → list for that page name.
         if noExplicitArgs then
         if noExplicitArgs then
             return p.listForUser(frame)
             return p.listForUser(frame)
         end
         end


        -- Case B: {{Passive|Acolyte}} on the "Acolyte" page and no id → treat as list.
         if name and name ~= "" and name == pageName and (not id or id == "") then
         if name and name ~= "" and name == pageName and (not id or id == "") then
             return p.listForUser(frame)
             return p.listForUser(frame)
         end
         end


        -- Otherwise, genuinely unknown passive.
         local label = name or id or "?"
         local label = name or id or "?"
         return string.format(
         return string.format(
Line 382: Line 551:
     end
     end


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


return p
return p