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
 
(11 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 20: Line 24:


local function getPassives()
local function getPassives()
    if not passivesCache then
if not passivesCache then
        passivesCache = GameData.loadPassives()
passivesCache = GameData.loadPassives()
    end
end
    return passivesCache
return passivesCache
end
end


local function getArgs(frame)
local function getArgs(frame)
    local parent = frame:getParent()
local parent = frame:getParent()
    if parent then
if parent then
        return parent.args
return parent.args
    end
end
    return frame.args
return frame.args
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 rows
local function addRow(tbl, label, value)
local function addRow(tbl, label, value)
    if value == nil or value == "" then
if value == nil or value == "" then
        return
return
    end
end
    local row = tbl:tag("tr")
 
    row:tag("th"):wikitext(label):done()
local row = tbl:tag("tr")
    row:tag("td"):wikitext(value):done()
row:addClass("spiritvale-passive-body-row")
row:tag("th"):wikitext(label):done()
row:tag("td"):wikitext(value):done()
end
end


-- Lookup by Internal Name
-- Lookup by Internal Name
local function getPassiveById(id)
local function getPassiveById(id)
    if not id or id == "" then
if not id or id == "" then
        return nil
return nil
    end
end
    local dataset = getPassives()
local dataset = getPassives()
    local byId = dataset.byId or {}
local byId = dataset.byId or {}
    return byId[id]
return byId[id]
end
end


-- Lookup by display Name (for editors)
-- Lookup by display Name (for editors)
local function findPassiveByName(name)
local function findPassiveByName(name)
    if not name or name == "" then
if not name or name == "" then
        return nil
return nil
    end
end
    local dataset = getPassives()
local dataset = getPassives()
    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
            return rec
return rec
        end
end
    end
end
    return nil
return nil
end
end


Line 78: Line 85:
----------------------------------------------------------------------
----------------------------------------------------------------------


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 parts = {}
local function formatBasePer(block)
if type(block) ~= "table" then
return nil
end


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


            local expr = value.Expression
if block.Base ~= nil then
            local base = value.Base
table.insert(parts, string.format("Base %s", tostring(block.Base)))
            local per  = value["Per Level"]
end
if block["Per Level"] ~= nil then
table.insert(parts, string.format("%s / Lv", tostring(block["Per Level"])))
end


            local seg = typeName
if #parts == 0 then
return nil
end


            if expr and expr ~= "" then
return table.concat(parts, ", ")
                seg = seg .. string.format(" (%s)", expr)
end
            end


            local detail = {}
-- Passive Effects: return rows (label/value), not a single text blob
            if base then
local function passiveEffectRows(list)
                table.insert(detail, string.format("Base %.2f", base))
if type(list) ~= "table" or #list == 0 then
            end
return {}
            if per then
end
                table.insert(detail, string.format("%.2f / Lv", per))
            end


            if #detail > 0 then
local rows = {}
                seg = seg .. " – " .. table.concat(detail, ", ")
            end


            table.insert(parts, seg)
for _, eff in ipairs(list) do
        end
if type(eff) == "table" then
    end
local t = eff.Type or {}
local name = t.Name or eff.ID or "Unknown"


    if #parts == 0 then
local value = eff.Value or {}
        return nil
local detail = {}
    end


    -- One per line
if value.Base ~= nil then
    return table.concat(parts, "<br />")
table.insert(detail, string.format("Base %s", tostring(value.Base)))
end
if value["Per Level"] ~= nil then
table.insert(detail, string.format("%s / Lv", tostring(value["Per Level"])))
end
if value.Expression ~= nil and value.Expression ~= "" then
table.insert(detail, tostring(value.Expression))
end
 
-- Optional qualifiers (weapon/stance/etc.), if present in data
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
 
local right = (#detail > 0) and table.concat(detail, ", ") or "—"
table.insert(rows, { label = name, value = right })
end
end
 
return rows
end
end


local function formatStatusApplications(list)
local function formatStatusApplications(list)
    if type(list) ~= "table" or #list == 0 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 parts = {}
local seg = scope .. " " .. name
    for _, s in ipairs(list) do
local detail = {}
        if type(s) == "table" then
            local scope = s.Scope or "Target"
            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
if type(s.Duration) == "table" then
            if scope and scope ~= "" then
local t = formatBasePer(s.Duration)
                seg = scope .. ": " .. seg
if t then
            end
table.insert(detail, "Duration " .. t)
end
end


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


            if #detailParts > 0 then
if s["Fixed Duration"] then
                seg = seg .. " (" .. table.concat(detailParts, ", ") .. ")"
table.insert(detail, "Fixed duration")
            end
end


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


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


    return table.concat(parts, "<br />")
return asUl(parts)
end
end


local function formatNotes(notes)
local function formatStatusRemoval(list)
    if type(notes) ~= "table" or #notes == 0 then
if type(list) ~= "table" or #list == 0 then
        return nil
return nil
    end
end
    return table.concat(notes, "<br />")
 
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
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"
table.insert(parts, string.format("%s → %s", action, name))
end
end
 
return asUl(parts)
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
 
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 177: Line 328:


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")
 
-- ==========================================================
-- Top "hero" rows: title row (icon + name), then description row
-- ==========================================================
local icon  = rec.Icon
local title = rec.Name or rec["Internal Name"] or "Unknown Passive"
local desc  = rec.Description or ""
 
-- Row 1: centered icon + title (single cell)
local titleRow = root:tag("tr")
titleRow:addClass("spiritvale-infobox-main")
titleRow:addClass("sv-hero-title-row")


    -- Header: icon + name
local titleCell = titleRow:tag("th")
    local icon  = rec.Icon
titleCell:attr("colspan", 2)
    local title = rec.Name or rec["Internal Name"] or "Unknown Passive"


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


    local titleText = ""
if icon and icon ~= "" then
    if icon and icon ~= "" then
titleInner:wikitext(string.format("[[File:%s|80px|link=]]", icon))
        titleText = string.format("[[File:%s|64px|link=]] ", icon)
end
    end
    titleText = titleText .. title
    headerCell:wikitext(titleText)


titleInner:tag("div")
:addClass("spiritvale-infobox-title")
:wikitext(title)


    ------------------------------------------------------------------
-- Row 2: description (single cell)
    -- Basic info
if desc ~= "" then
    ------------------------------------------------------------------
local descRow = root:tag("tr")
    addRow(root, "Description", rec.Description)
descRow:addClass("spiritvale-infobox-main")
    addRow(root, "Max level", rec["Max Level"] and tostring(rec["Max Level"]))
descRow:addClass("sv-hero-desc-row")


    ------------------------------------------------------------------
local descCell = descRow:tag("td")
    -- Users
descCell:attr("colspan", 2)
    ------------------------------------------------------------------
    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))


    ------------------------------------------------------------------
local descInner = descCell:tag("div")
    -- Requirements
descInner:addClass("spiritvale-infobox-main-right-inner")
    ------------------------------------------------------------------
    local req = rec.Requirements or {}


    if type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0 then
-- Use <i> to avoid apostrophes breaking ''...'' formatting
        local skillParts = {}
local d = descInner:tag("div")
        for _, rs in ipairs(req["Required Skills"]) do
d:addClass("spiritvale-infobox-description")
            local name  = rs["Skill Name"] or rs["Skill ID"] or "Unknown"
d:tag("i"):wikitext(desc)
            local level = rs["Required Level"]
end
            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"]))
-- General
------------------------------------------------------------------
addRow(root, "Max Level", rec["Max Level"] and tostring(rec["Max Level"]))


    ------------------------------------------------------------------
-- Classes intentionally removed (template is used on class pages)
    -- Passive effects
local users = rec.Users or {}
    ------------------------------------------------------------------
addRow(root, "Summons",  listToText(users.Summons))
    local effectsText = formatPassiveEffects(rec["Passive Effects"])
addRow(root, "Monsters", listToText(users.Monsters))
    addRow(root, "Passive effects", effectsText)
addRow(root, "Events",   listToText(users.Events))


    ------------------------------------------------------------------
------------------------------------------------------------------
    -- Status interactions
-- Requirements
    ------------------------------------------------------------------
------------------------------------------------------------------
    local statusApps = formatStatusApplications(rec["Status Applications"])
local req = rec.Requirements or {}
    addRow(root, "Status applications", statusApps)
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
    -- Notes
local skillParts = {}
    ------------------------------------------------------------------
for _, rs in ipairs(req["Required Skills"]) do
    local notesText = formatNotes(rec.Notes)
local sname = rs["Skill Name"] or rs["Skill ID"] or "Unknown"
    addRow(root, "Notes", notesText)
local rlv  = rs["Required Level"]
if rlv then
table.insert(skillParts, string.format("%s (Lv.%s)", sname, rlv))
else
table.insert(skillParts, sname)
end
end
addRow(root, "Required Skills", table.concat(skillParts, ", "))
end


    return tostring(root)
addRow(root, "Required Weapons", listToText(req["Required Weapons"]))
end
addRow(root, "Required Stances", listToText(req["Required Stances"]))
end


local function passiveMatchesUser(rec, userName)
------------------------------------------------------------------
    if type(rec) ~= "table" or not userName or userName == "" then
-- Passive Effects (one row per effect)
        return false
------------------------------------------------------------------
    end
local peRows = passiveEffectRows(rec["Passive Effects"])
for _, r in ipairs(peRows) do
addRow(root, r.label, r.value)
end


    local users = rec.Users
------------------------------------------------------------------
    if type(users) ~= "table" then
-- Status Effects
        return false
------------------------------------------------------------------
    end
local statusApps = formatStatusApplications(rec["Status Applications"])
local statusRem  = formatStatusRemoval(rec["Status Removal"])
if statusApps or statusRem then
addRow(root, "Applies", statusApps)
addRow(root, "Removes", statusRem)
end


    local userLower = mw.ustring.lower(userName)
------------------------------------------------------------------
-- Modifiers
------------------------------------------------------------------
local modsText = formatModifiers(rec.Modifiers)
if modsText then
addRow(root, "Flags", modsText)
end


    local function listHas(list)
------------------------------------------------------------------
        if type(list) ~= "table" then
-- Events
            return false
------------------------------------------------------------------
        end
local eventsText = formatEvents(rec.Events)
        for _, v in ipairs(list) do
if eventsText then
            if type(v) == "string" and mw.ustring.lower(v) == userLower then
addRow(root, "Triggers", eventsText)
                return true
end
            end
        end
        return false
    end


    -- Adjust this if you *only* want Classes.
------------------------------------------------------------------
    if listHas(users.Classes) then return true end
-- Notes
    if listHas(users.Summons) then return true end
------------------------------------------------------------------
    if listHas(users.Monsters) then return true end
if type(rec.Notes) == "table" and #rec.Notes > 0 then
    if listHas(users.Events)  then return true end
addRow(root, "Notes", asUl(rec.Notes) or table.concat(rec.Notes, "<br />"))
end


    return false
return tostring(root)
end
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
        userName = mw.title.getCurrentTitle().text
userName = mw.title.getCurrentTitle().text
    end
end


    if not userName or userName == "" then
if not userName or userName == "" then
        return "<strong>No user name provided to Passive list.</strong>"
return "<strong>No user name provided to Passive list.</strong>"
    end
end


    local dataset = getPassives()
local dataset = getPassives()
    local matches = {}
local matches = {}


    for _, rec in ipairs(dataset.records or {}) do
for _, rec in ipairs(dataset.records or {}) do
        if passiveMatchesUser(rec, userName) then
if passiveMatchesUser(rec, userName) then
            table.insert(matches, rec)
table.insert(matches, rec)
        end
end
    end
end


    if #matches == 0 then
if #matches == 0 then
        return string.format(
return string.format(
            "<strong>No passives found for:</strong> %s",
"<strong>No passives found for:</strong> %s",
            mw.text.nowiki(userName)
mw.text.nowiki(userName)
        )
)
    end
end


    local root = mw.html.create("div")
local root = mw.html.create("div")
    root:addClass("spiritvale-passive-list")
root:addClass("spiritvale-passive-list")


    for _, rec in ipairs(matches) do
for _, rec in ipairs(matches) do
        root:wikitext(buildInfobox(rec))
root:wikitext(buildInfobox(rec))
    end
end


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


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


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


    -- Allow:
local raw1 = args[1]
    --  {{Passive|Honed Blade}}          -> args[1] = "Honed Blade" (Name)
local name = args.name or raw1
    --  {{Passive|name=Honed Blade}}      -> args.name
local id  = args.id
    --  {{Passive|id=CritMastery}}        -> args.id (Internal Name)
    local raw1 = args[1]
    local name = args.name or raw1
    local id  = args.id


    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()
        local pageName  = pageTitle and pageTitle.text or ""
local pageName  = pageTitle and pageTitle.text or ""


        local noExplicitArgs =
local noExplicitArgs =
            (not raw1 or raw1 == "") and
(not raw1 or raw1 == "") and
            (not args.name or args.name == "") and
(not args.name or args.name == "") and
            (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(
"<strong>Unknown passive:</strong> %s[[Category:Pages with unknown passive|%s]]",
            "<strong>Unknown passive:</strong> %s[[Category:Pages with unknown passive|%s]]",
mw.text.nowiki(label),
            mw.text.nowiki(label),
label
            label
)
        )
end
    end


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


return p
return p