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
m Protected "Module:GamePassives" ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite)) [cascading]
No edit summary
 
(8 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 = scope .. " – " .. name
             local detail = {}


             local seg = name
             local dur = s.Duration
             if scope and scope ~= "" then
             if type(dur) == "table" then
                 seg = scope .. ": " .. seg
                 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
             end


Line 158: Line 205:
     end
     end


     if #parts == 0 then
    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
 
            local bp = formatBasePer(r)
            local seg = label
            if bp then
                seg = seg .. " – " .. bp
            end
            table.insert(parts, seg)
        end
    end
 
    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
        addSectionHeader(root, "Requirements")


    if type(req["Required Skills"]) == "table" 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"]))
    -- Passive Effects (one row per effect)
    ------------------------------------------------------------------
    local peRows = passiveEffectRows(rec["Passive Effects"])
    if #peRows > 0 then
        addSectionHeader(root, "Passive Effects")
        for _, r in ipairs(peRows) do
            addRow(root, r.label, r.value)
        end
    end
 
    ------------------------------------------------------------------
    -- Status Effects
    ------------------------------------------------------------------
    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


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Passive effects
     -- Modifiers
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local effectsText = formatPassiveEffects(rec["Passive Effects"])
     local modsText = formatModifiers(rec.Modifiers)
    addRow(root, "Passive effects", effectsText)
    if modsText then
        addSectionHeader(root, "Modifiers")
        addRow(root, "Flags", modsText)
    end


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Status interactions
     -- Events
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local statusApps = formatStatusApplications(rec["Status Applications"])
     local eventsText = formatEvents(rec.Events)
    addRow(root, "Status applications", statusApps)
    if eventsText then
        addSectionHeader(root, "Events")
        addRow(root, "Triggers", eventsText)
    end


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Notes
     -- Notes
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local notesText = formatNotes(rec.Notes)
     if type(rec.Notes) == "table" and #rec.Notes > 0 then
     addRow(root, "Notes", notesText)
        addSectionHeader(root, "Notes")
        addRow(root, "Notes", asUl(rec.Notes) or table.concat(rec.Notes, "<br />"))
     end
 
    return tostring(root)
end
 
----------------------------------------------------------------------
-- Public: list all passives 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 Passive list.</strong>"
    end
 
    local dataset = getPassives()
    local matches = {}
 
    for _, rec in ipairs(dataset.records or {}) do
        if passiveMatchesUser(rec, userName) then
            table.insert(matches, rec)
        end
    end
 
    if #matches == 0 then
        return string.format(
            "<strong>No passives found for:</strong> %s",
            mw.text.nowiki(userName)
        )
    end
 
    local root = mw.html.create("div")
    root:addClass("spiritvale-passive-list")
 
    for _, rec in ipairs(matches) do
        root:wikitext(buildInfobox(rec))
    end


     return tostring(root)
     return tostring(root)
Line 255: Line 503:


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


Line 261: 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 271: 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 nothing, decide if this is "list mode" or truly unknown.
     if not rec then
     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 == "")
        if noExplicitArgs then
            return p.listForUser(frame)
        end
        if name and name ~= "" and name == pageName and (not id or id == "") then
            return p.listForUser(frame)
        end
         local label = name or id or "?"
         local label = name or id or "?"
         return string.format(
         return string.format(