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 Changed protection settings for "Module:GamePassives" ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite))
No edit summary
 
Line 45: Line 45:
end
end


-- CHANGED: tag body rows so we can center text without touching the hero row
-- 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 56: Line 56:
end
end


-- CHANGED: tag section header rows as body rows too (for centering)
-- Tag section header rows as body rows too (for centering)
local function addSectionHeader(tbl, label)
local function addSectionHeader(tbl, label)
     local row = tbl:tag("tr")
     local row = tbl:tag("tr")
Line 94: Line 94:
-- Formatting helpers
-- Formatting helpers
----------------------------------------------------------------------
----------------------------------------------------------------------
local function asUl(items)
    if type(items) ~= "table" or #items == 0 then
        return nil
    end
    return '<ul class="spiritvale-infobox-list"><li>'
        .. table.concat(items, "</li><li>")
        .. "</li></ul>"
end


local function formatBasePer(block)
local function formatBasePer(block)
Line 112: Line 121:
end
end


local function formatPassiveEffects(list)
-- Passive Effects: return rows (label/value), not a single text blob
local function passiveEffectRows(list)
     if type(list) ~= "table" or #list == 0 then
     if type(list) ~= "table" or #list == 0 then
         return nil
         return {}
     end
     end


     local parts = {}
     local rows = {}


     for _, eff in ipairs(list) do
     for _, eff in ipairs(list) do
Line 123: Line 133:
             local t = eff.Type or {}
             local t = eff.Type or {}
             local name = t.Name or eff.ID or "Unknown"
             local name = t.Name or eff.ID or "Unknown"
             local value = eff.Value or {}
             local value = eff.Value or {}
             local detail = {}
             local detail = {}


Line 137: Line 147:
             end
             end


             local seg = name
            -- Optional qualifiers (weapon/stance/etc.), if present in data
             if #detail > 0 then
             local qual = eff.Weapon or eff["Weapon"] or eff["Weapon Type"]
                 seg = seg .. " – " .. table.concat(detail, ", ")
                      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
 
    return table.concat(parts, "<br />")
end
end


Line 196: Line 205:
     end
     end


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


Line 230: Line 235:
     end
     end


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


Line 247: Line 248:
             local action = ev.Action or "On event"
             local action = ev.Action or "On event"
             local name  = ev["Skill Name"] or ev["Skill ID"] or "Unknown skill"
             local name  = ev["Skill Name"] or ev["Skill ID"] or "Unknown skill"
             local seg    = string.format("%s → %s", action, name)
             table.insert(parts, string.format("%s → %s", action, name))
            table.insert(parts, seg)
         end
         end
     end
     end


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


Line 286: Line 282:
     collect("Special",  mods["Special Modifiers"])
     collect("Special",  mods["Special Modifiers"])


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


Line 380: Line 372:
     addSectionHeader(root, "General")
     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"]))


     -- CHANGED: Remove Classes from passives (template usage is on class pages)
     -- 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)) -- removed
     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 410: Line 401:
                 end
                 end
             end
             end
             addRow(root, "Required skills", table.concat(skillParts, ", "))
             addRow(root, "Required Skills", table.concat(skillParts, ", "))
         end
         end


         addRow(root, "Required weapons", listToText(req["Required Weapons"]))
         addRow(root, "Required Weapons", listToText(req["Required Weapons"]))
         addRow(root, "Required stances", listToText(req["Required Stances"]))
         addRow(root, "Required Stances", listToText(req["Required Stances"]))
     end
     end


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Passive effects
     -- Passive Effects (one row per effect)
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local peText = formatPassiveEffects(rec["Passive Effects"])
     local peRows = passiveEffectRows(rec["Passive Effects"])
     if peText then
     if #peRows > 0 then
         addSectionHeader(root, "Passive effects")
         addSectionHeader(root, "Passive Effects")
         addRow(root, "Effects", peText)
         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
     end


Line 433: Line 437:
         addSectionHeader(root, "Modifiers")
         addSectionHeader(root, "Modifiers")
         addRow(root, "Flags", modsText)
         addRow(root, "Flags", modsText)
    end
    ------------------------------------------------------------------
    -- Status
    ------------------------------------------------------------------
    local statusApps = formatStatusApplications(rec["Status Applications"])
    local statusRem  = formatStatusRemoval(rec["Status Removal"])
    if statusApps or statusRem then
        addSectionHeader(root, "Status effects")
        addRow(root, "Applies", statusApps)
        addRow(root, "Removes", statusRem)
     end
     end


Line 460: Line 453:
     if type(rec.Notes) == "table" and #rec.Notes > 0 then
     if type(rec.Notes) == "table" and #rec.Notes > 0 then
         addSectionHeader(root, "Notes")
         addSectionHeader(root, "Notes")
         addRow(root, "Notes", table.concat(rec.Notes, "<br />"))
         addRow(root, "Notes", asUl(rec.Notes) or table.concat(rec.Notes, "<br />"))
     end
     end



Latest revision as of 01:01, 13 December 2025

Module:GamePassives

Module:GamePassives renders passive skill data from Data:passives.json into a reusable infobox-style table.

It is intended to be used via a template (for example Template:Passive) so that passives can be embedded on any page without creating individual pages for each passive.

This module:

  • Loads data via Module:GameDataGameData.loadPassives().
  • Looks up passives primarily by display "Name" (what editors use), with "Internal Name" as an optional fallback.
  • Builds a table with only the fields that actually exist for that passive.

Data source

Passive data comes from Data:passives.json, which is a JSON page with this top-level structure (see Module:GameData/doc for full details):

{
  "version": "SpiritVale-0.9.3",
  "schema_version": 1,
  "generated_at": "2025-12-12T17:24:05.784284+00:00",
  "records": [
    {
      "Name": "Honed Blade",
      "Internal Name": "CritMastery",
      "...": "other fields specific to passives"
    }
  ]
}

Each record is a single passive. Important keys:

  • "Name" – the display name (what players and editors will usually see and use).
  • "Internal Name" – the stable ID used internally and available as an optional parameter for power users and tooling.

Common fields include:

  • Icon
  • Description
  • Max Level
  • Users (e.g. Classes)
  • Requirements
  • Passive Effects
  • Status Applications
  • Notes

Output

For a given passive, the module renders a table with the CSS class spiritvale-passive-infobox.

Depending on what exists in the JSON record, the table may include:

  • Header row with passive name (and icon, if present).
  • Description.
  • Max level.
  • Users:
    • Classes
    • Summons
    • Monsters
    • Events
  • Requirements:
    • Required Skills (with required level)
    • Required Weapons
    • Required Stances
  • Passive effects:
    • Each entry from "Passive Effects" (Type name, optional Expression, Base and Per Level where present)
  • Status interactions:
    • Status Applications (if any)
  • Notes:
    • Any entries from "Notes" (one per line)

Rows are only shown if the underlying field exists in the JSON for that passive.


Public interface

The module exposes a single entry point for templates:

GamePassives.infobox(frame)

This is usually called via #invoke from a template, not directly from pages.

It accepts the following parameters (either passed directly or via a wrapper template):

  • 1 – unnamed parameter; treated as the passive "Name".
  • name – explicit display "Name" of the passive (equivalent to 1).
  • id"Internal Name" of the passive (optional fallback / power use).

Lookup order:

  1. If name or the first unnamed parameter is provided and matches a record’s "Name", that record is used.
  2. Otherwise, if id is provided and matches an "Internal Name", that record is used.
  3. If nothing is found, a small error message is returned and the page is categorized for tracking.

Example direct usage (not recommended; normally use a template):

No passives found for: GamePassives

or:

No passives found for: GamePassives

Template:Passive

The recommended way to use this module is via a small wrapper template, for example:

Template:Passive
 No passives found for: GamePassives

Typical usage on any page:

Honed Blade
A life of combat has perfected your edge, each critical strike hits with ruthless efficiency.
General
Max Level10
Passive Effects
Critical Damage3 / Lv

or, explicitly:

Honed Blade
A life of combat has perfected your edge, each critical strike hits with ruthless efficiency.
General
Max Level10
Passive Effects
Critical Damage3 / Lv

Internal IDs can still be used when needed:

Honed Blade
A life of combat has perfected your edge, each critical strike hits with ruthless efficiency.
General
Max Level10
Passive Effects
Critical Damage3 / Lv

This keeps page wikitext simple while centralizing all JSON loading and formatting logic inside Lua.


-- Module:GamePassives
--
-- Renders passive skill data (from Data:passives.json) into an
-- infobox-style table and can also list all passives for a given user/class.
--
-- Usage (single passive):
--   {{Passive|Honed Blade}}
--   {{Passive|name=Honed Blade}}
--   {{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 p = {}

----------------------------------------------------------------------
-- Internal helpers
----------------------------------------------------------------------

local passivesCache

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

local function getArgs(frame)
    local parent = frame:getParent()
    if parent then
        return parent.args
    end
    return frame.args
end

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

-- Tag body rows so we can style/center without touching the hero row
local function addRow(tbl, label, value)
    if value == nil or value == "" then
        return
    end
    local row = tbl:tag("tr")
    row:addClass("spiritvale-passive-body-row")
    row:tag("th"):wikitext(label):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

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

-- Lookup by display Name (for editors)
local function findPassiveByName(name)
    if not name or name == "" then
        return nil
    end
    local dataset = getPassives()
    for _, rec in ipairs(dataset.records or {}) do
        if rec["Name"] == name then
            return rec
        end
    end
    return nil
end

----------------------------------------------------------------------
-- Formatting helpers
----------------------------------------------------------------------

local function asUl(items)
    if type(items) ~= "table" or #items == 0 then
        return nil
    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 = {}
    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
        if type(eff) == "table" then
            local t = eff.Type or {}
            local name = t.Name or eff.ID or "Unknown"

            local value = eff.Value or {}
            local detail = {}

            if value.Base ~= nil then
                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

local function formatStatusApplications(list)
    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 scope = s.Scope or "Target"
            local name  = s["Status Name"] or s["Status ID"] or "Unknown status"

            local seg = scope .. " – " .. name
            local detail = {}

            local dur = s.Duration
            if type(dur) == "table" then
                local t = formatBasePer(dur)
                if t then
                    table.insert(detail, "Duration " .. t)
                end
            end

            local ch = s.Chance
            if type(ch) == "table" then
                local t = formatBasePer(ch)
                if t then
                    table.insert(detail, "Chance " .. t)
                end
            end

            if s["Fixed Duration"] then
                table.insert(detail, "Fixed duration")
            end

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

            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

            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

----------------------------------------------------------------------
-- Infobox builder
----------------------------------------------------------------------

local function buildInfobox(rec)
    local root = mw.html.create("table")
    root:addClass("spiritvale-passive-infobox")

    -- ==========================================================
    -- Top "hero" row: icon + name (left), description (right)
    -- ==========================================================
    local icon  = rec.Icon
    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 leftInner = leftCell:tag("div")
    leftInner:addClass("spiritvale-infobox-main-left-inner")

    if icon and icon ~= "" then
        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

    ------------------------------------------------------------------
    -- General
    ------------------------------------------------------------------
    addSectionHeader(root, "General")

    addRow(root, "Max Level", rec["Max Level"] and tostring(rec["Max Level"]))

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

    ------------------------------------------------------------------
    -- Requirements
    ------------------------------------------------------------------
    local req = rec.Requirements or {}
    if (req["Required Skills"] and #req["Required Skills"] > 0)
        or (req["Required Weapons"] and #req["Required Weapons"] > 0)
        or (req["Required Stances"] and #req["Required Stances"] > 0) then

        addSectionHeader(root, "Requirements")

        if type(req["Required Skills"]) == "table" and #req["Required Skills"] > 0 then
            local skillParts = {}
            for _, rs in ipairs(req["Required Skills"]) do
                local name  = rs["Skill Name"] or rs["Skill ID"] or "Unknown"
                local level = rs["Required Level"]
                if level then
                    table.insert(skillParts, string.format("%s (Lv.%s)", name, level))
                else
                    table.insert(skillParts, name)
                end
            end
            addRow(root, "Required Skills", table.concat(skillParts, ", "))
        end

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

    ------------------------------------------------------------------
    -- 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

    ------------------------------------------------------------------
    -- Modifiers
    ------------------------------------------------------------------
    local modsText = formatModifiers(rec.Modifiers)
    if modsText then
        addSectionHeader(root, "Modifiers")
        addRow(root, "Flags", modsText)
    end

    ------------------------------------------------------------------
    -- Events
    ------------------------------------------------------------------
    local eventsText = formatEvents(rec.Events)
    if eventsText then
        addSectionHeader(root, "Events")
        addRow(root, "Triggers", eventsText)
    end

    ------------------------------------------------------------------
    -- Notes
    ------------------------------------------------------------------
    if type(rec.Notes) == "table" and #rec.Notes > 0 then
        addSectionHeader(root, "Notes")
        addRow(root, "Notes", 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)
end

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

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

    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 = findPassiveByName(name)
    end

    -- 2) Fallback: internal ID
    if not rec and id and id ~= "" then
        rec = getPassiveById(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 == "")

        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 "?"
        return string.format(
            "<strong>Unknown passive:</strong> %s[[Category:Pages with unknown passive|%s]]",
            mw.text.nowiki(label),
            label
        )
    end

    return buildInfobox(rec)
end

return p