Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:C: Difference between revisions

From Teriock
Content deleted Content added
// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(69 intermediate revisions by the same user not shown)
Line 1: Line 1:
local p = {}
local p = {}


local function makeBar(frame, title, content, link, flags, bottom)
local tags = {
if content == '' then return '' end
traits = {
local barSpace = mw.html.create('span')
undead = {
barSpace:wikitext(frame:preprocess(' ')):css('display', 'none')
text = 'Undead',
local barTitle = mw.html.create('span')
link = 'Undead creatures',
local barTitleClass = "{{lc:" .. title .. "}}"
symbol = '🪦',
barTitleClass = barTitleClass:gsub(' ', '-')
},
barTitleClass = barTitleClass:gsub('\'', '')
corporeal = {
barTitleClass = barTitleClass:gsub('\"', '')
text = 'Corporeal',
local barText = "{{ucfirst:" .. title .. "}}"
link = 'Corporeal undead creatures',
if link then barText = "[[" .. link .. "|" .. barText .. "]]" end
symbol = '🧟',
barText = barText .. ":"
},
barTitle:addClass('ability-bar-title'):wikitext(frame:preprocess(barText))
skeletal = {
:css('font-weight', 'bold'):css('margin-right', '0.5em')
text = 'Skeletal',
local barContent = mw.html.create('span')
link = 'Skeletal undead creatures',
barContent:addClass('ability-bar-content'):wikitext(content)
symbol = '☠️',
local output = mw.html.create('div')
},
if bottom then output = mw.html.create('span') end
spectral = {
if not flags then flags = {} end
text = 'Spectral',
for k, v in pairs(flags) do
link = 'Spectral undead creatures',
if type(v) == 'string' then
symbol = '👻',
output:addClass('flag-' .. k .. '-' .. v)
},
end
outsider = {
end
text = 'Outsider',
output:addClass('ability-bar'):addClass('ability-bar-' .. barTitleClass)
link = 'Outsider creatures',
:attr(flags):attr('test', 'out'):node(barSpace):node(barTitle):node(
symbol = '👽️',
barSpace):node(barContent):css('display', 'block')
},
-- if flags then
angelic = {
-- for k, v in ipairs(flags) do
text = 'Angelic',
-- output:attr(k, v)
link = 'Angelic creatures',
-- end
symbol = '😇',
-- end
},
return tostring(output)
demonic = {
text = 'Demonic',
link = 'Demonic creatures',
symbol = '👿',
},
elemental = {
text = 'Elemental',
link = 'Elemental creatures',
symbol = '🌞',
},
plant = {
text = 'Plant',
link = 'Plant creatures',
symbol = '🌿'
},
construct = {
text = 'Construct',
link = 'Construct creatures',
symbol = '🗿'
},
draconic = {
text = 'Draconic',
link = 'Draconic creatures',
symbol = '🐲',
},
fae = {
text = 'Fae',
link = 'Fae creatures',
symbol = '🧚',
},
animal = {
text = 'Animal',
link = 'Animal creatures',
symbol = '🦀',
},
common = {
text = 'Common',
link = 'Common animal creatures',
symbol = '🐾',
},
ooze = {
text = 'Ooze',
link = 'Ooze creatures',
symbol = '💩',
},
monstrous = {
text = 'Monstrous',
link = 'Monstrous creatures',
symbol = '🧌',
},
humanoid = {
text = 'Humanoid',
link = 'Humanoid creatures',
symbol = '👤',
},
aquatic = {
text = 'Aquatic',
link = 'Aquatic creatures',
symbol = '🐟️',
}
}
}

local function sortString(s)
local items = {}
for item in string.gmatch(s, '([^,]+)') do
local trimmed_item = item:match("^%s*(.-)%s*$")
table.insert(items, trimmed_item)
end
table.sort(items)
return items
end
end


-- === NEW: shared abilities parsing helpers ===
local function makeTag(frame, key, value, makeCat, transparent)
local function parseAbilitiesString(abilitiesString)
if not tags[key] or not tags[key][value] then
local abilities = {}
if value then
for ability in mw.text.gsplit(abilitiesString or '', ";;", true) do
local out = frame:preprocess("{{ucfirst:" .. tostring(value) .. "}}")
ability = mw.text.trim(ability)
-- local out = 'pranked'
if not ('.' == out:sub(-1)) then
if ability ~= "" then
out = out .. '.'
local name = ability
local limited, improved = "", ""
local gifted, adept = false, false

-- flags
if mw.ustring.find(ability, "%-g%-") then gifted = true end
if mw.ustring.find(ability, "%-a%-") then adept = true end

-- notes (order-insensitive, tolerate any following marker)
local limitedMatch = mw.ustring.match(ability,
"%-l%-(.-)(%-[liag]%-|$)") or
mw.ustring.match(ability, "%-l%-(.+)$")
if limitedMatch then limited = mw.text.trim(limitedMatch) end

local improvedMatch = mw.ustring.match(ability,
"%-i%-(.-)(%-[liag]%-|$)") or
mw.ustring.match(ability, "%-i%-(.+)$")
if improvedMatch then
improved = mw.text.trim(improvedMatch)
end
end

return out
-- clean name (strip markers + payloads/flags)
else
return ''
name = mw.ustring.gsub(name, "%-l%-.+", "")
name = mw.ustring.gsub(name, "%-i%-.+", "")
name = mw.ustring.gsub(name, "%-g%-", "")
name = mw.ustring.gsub(name, "%-a%-", "")
name = mw.text.trim(name)

table.insert(abilities, {
name = name,
limited = limited,
improved = improved,
gifted = gifted,
adept = adept
})
end
end
end
end

local tag = tags[key][value]
table.sort(abilities, function(a, b) return a.name < b.name end)
if tag.no then
return abilities
local out = frame:preprocess("{{ucfirst:" .. tag.text .. "}}")
end
if not ('.' == out:sub(-1)) then

out = out .. '.'
local function buildAETransclusions(abilities)
local transclusions = {}
for _, a in ipairs(abilities or {}) do
local t = {"{{AE|", a.name}
if a.limited ~= "" then table.insert(t, "|limited=" .. a.limited) end
if a.improved ~= "" then
table.insert(t, "|improved=" .. a.improved)
end
end
if a.gifted then table.insert(t, "|gifted=1") end
return out
if a.adept then table.insert(t, "|adept=1") end
table.insert(t, "}}")
table.insert(transclusions, table.concat(t))
end
end
-- no visible separators between AE templates
if tag.tags then
return table.concat(transclusions, "")
local out = ''
end
for _, t in ipairs(tag.tags) do
-- === END helpers ===
out = out .. makeTag(frame, key, t, makeCat, transparent)

end
function p.c(frame)
return out
local args = frame.args
local rawAbilities = args['a'] or ''

-- === TAGS & CATEGORIES via TagBuilder ===
local TagBuilder = require('Module:TagBuilder')
local DiceEval = require('Module:DiceEval')

local tRaw = args['t'] or ''
local looks = args['l'] or ''
local ranks = args['r'] or ''
local name = args['name'] or ''
local hpDice = args['hp'] or '1d10'
local mpDice = args['mp'] or '1d10'
local hpDiceSizeStep = args['hps'] or ''
local mpDiceSizeStep = args['mps'] or ''
if hpDiceSizeStep:len() > 0 then
local hpPlural = 's'
if hpDiceSizeStep == '1' then hpPlural = '' end
hpDiceSizeStep = "Gain another hit die for every " .. hpDiceSizeStep ..
" additional size" .. hpPlural .. "."
end
end
if mpDiceSizeStep:len() > 0 then
local tagText = "{{Tag|{{ucfirst:" .. tag.text .. "}}"
local mpPlural = 's'
if tag.symbol then
tagText = tagText .. "|" .. tag.symbol
if mpDiceSizeStep == '1' then mpPlural = '' end
mpDiceSizeStep = "Gain another mana die for every " .. mpDiceSizeStep ..
" additional size" .. mpPlural .. "."
end
end
local baseHp = tostring(DiceEval._eval(hpDice))
if tag.link then
local baseMp = tostring(DiceEval._eval(mpDice))
tagText = tagText .. "|l=:Category:{{ucfirst:" .. tag.link .. "}}"
local size = args['size'] or '3'
local br = args['br'] or '0'
local int = args['int'] or '0'
local str = args['str'] or '0'
local mov = args['mov'] or '0'
local snk = args['snk'] or '0'
local per = args['per'] or '0'

local preAbilities = ''
if (int == '0') then
preAbilities = preAbilities .. "Normal Intelligence;; "
end
end
if (str == '0') then preAbilities = preAbilities .. "Normal Strength;; " end
if tag.color then
tagText = tagText .. "|c=" .. tag.color
if (mov == '0') then preAbilities = preAbilities .. "Normal Movement;; " end
if (snk == '0') then preAbilities = preAbilities .. "Normal Sneak;; " end
if (per == '0') then
preAbilities = preAbilities .. "Normal Perception;; "
end
end

if transparent then
tagText = tagText .. "|o=1"
local abilitiesString = preAbilities .. rawAbilities
local opts = {
name = name,
ns = 'Creature',
transparent = args.o or args.transparent
}
local tagOut, catOut =
TagBuilder.build(tRaw, 'TagCreatures', 'traits', opts)

-- === Parse and render main abilities with shared helpers ===
local abilities = parseAbilitiesString(abilitiesString)
local inner = buildAETransclusions(abilities)

local categoryString = '[[Category:Creatures]]'

local statContent = ''
statContent = statContent .. "{{Tag|" .. hpDice ..
" Hit Dice|❤️|l=:Category:" .. hpDice ..
" hit die creatures|c=#e01b24|class=hp-dice-tagged " ..
hpDice .. "}}" .. "{{Tag|" .. baseHp ..
" HP|❤️|l=:Category:" .. baseHp ..
" HP creatures|c=#e01b24|class=cost-tagged hp" .. baseHp ..
"}}"
categoryString = categoryString .. "[[Category:" .. hpDice ..
" hit die creatures" .. "]]"
categoryString = categoryString .. "[[Category:" .. baseHp ..
" HP creatures" .. "]]"
statContent = statContent .. "{{Tag|" .. mpDice ..
" Mana Dice|🩵|l=:Category:" .. mpDice ..
" mana die creatures|c=#3584e4|class=mp-dice-tagged " ..
mpDice .. "}}" .. "{{Tag|" .. baseMp ..
" MP|🩵|l=:Category:" .. baseMp ..
" MP creatures|c=#3584e4|class=cost-tagged hp" .. baseMp ..
"}}"
categoryString = categoryString .. "[[Category:" .. baseMp ..
" MP creatures" .. "]]"
categoryString = categoryString .. "[[Category:" .. mpDice ..
" mana die creatures" .. "]]"
statContent = statContent .. "{{Tag|Size " .. size ..
"|⬆️|l=:Category:Size " .. size ..
" creatures|c=#e4a835|class=size-tagged size" .. size ..
"}}"
categoryString = categoryString .. "[[Category:Size " .. size ..
" creatures" .. "]]"

local attributeContent = ''
attributeContent = ""
attributeContent = attributeContent .. "{{Tag|" .. int ..
"|INT|l=:Category:INT value of " .. int ..
" creatures}}"
categoryString = categoryString .. "[[Category:INT value of " .. int ..
" creatures]]"
attributeContent = attributeContent .. "{{Tag|" .. str ..
"|STR|l=:Category:STR value of " .. str ..
" creatures}}"
categoryString = categoryString .. "[[Category:STR value of " .. str ..
" creatures]]"
attributeContent = attributeContent .. "{{Tag|" .. mov ..
"|MOV|l=:Category:MOV value of " .. mov ..
" creatures}}"
categoryString = categoryString .. "[[Category:MOV value of " .. mov ..
" creatures]]"
attributeContent = attributeContent .. "{{Tag|" .. snk ..
"|SNK|l=:Category:SNK value of " .. snk ..
" creatures}}"
categoryString = categoryString .. "[[Category:SNK value of " .. snk ..
" creatures]]"
attributeContent = attributeContent .. "{{Tag|" .. per ..
"|PER|l=:Category:PER value of " .. per ..
" creatures}}"
categoryString = categoryString .. "[[Category:PER value of " .. per ..
" creatures]]"
traitsContent = (tagOut or '') .. "{{Tag|BR " .. br ..
"|⚠️|l=:Category:BR " .. br ..
" creatures|class=br-tagged br" .. br .. "}}"
categoryString = categoryString .. "[[Category:BR " .. br .. " creatures]]"

local out = ""
out = out .. makeBar(frame, "Stats", statContent, nil, nil, false)
out = out .. makeBar(frame, "Attributes", attributeContent, nil, nil, false)
out = out .. makeBar(frame, "Traits", traitsContent, nil, nil, false)
out = out .. makeBar(frame, "Looks", looks, nil, nil, false)
out = out .. makeBar(frame, "Innate ranks", ranks, nil, nil, false)
out = out .. makeBar(frame, "HP increase", hpDiceSizeStep, nil, nil, false)
out = out .. makeBar(frame, "MP increase", mpDiceSizeStep, nil, nil, false)
if args['lifespan'] then
local lifespanText = args['lifespan'] .. " years."
categoryString = categoryString .. "[[Category:" .. args['lifespan'] ..
" year lifespan creatures]]"
if args['adult'] then
lifespanText = lifespanText .. " Adult at age " .. args['adult'] ..
"."
end
out = out .. makeBar(frame, "Lifespan", lifespanText, nil, nil, false)
end

out = out .. '<hr>'
if args['d'] then
out = out .. '<span class="creature-description>' .. args['d'] ..
'</span><hr>'
end

-- main abilities block
out = out .. '<div class="expandable-table"><div>' ..
frame:preprocess(inner) .. '</div></div>'

-- === familiar abilities (args['f']) using the same parsing ===
if args['f'] and mw.text.trim(args['f']) ~= '' then
local fAbilities = parseAbilitiesString(args['f'])
local fInner = buildAETransclusions(fAbilities)
-- You can wrap in a bar or just mirror the main output style; here we label it:
out = out .. '<hr>' .. makeBar(frame, "Familiar Abilities",
frame:preprocess(
'<div class="expandable-table"><div>' ..
fInner .. '</div></div>'), nil,
nil, false)
end

out = out .. (catOut or '')

local page = mw.title.getCurrentTitle().text
local namespace = frame:preprocess('{{NAMESPACE}}')
if ((namespace == "Creature") and (page == name)) then
out = out .. categoryString
end
end
tagText = tagText .. "}}"
-- out = frame:preprocess(tagText)
out = tagText
return out
end


return frame:preprocess(out)
function p.c ( frame )
local args = frame.args
local tagIn = ''
local abilitiesIn = ''
local hp = ''
local mp = ''
local name = args['name']
if not name then
return 'Must provide name.'
end
local catString = ''
if args['t'] then
tagIn = args['t']
end
if args['a'] then
abilitiesIn = args['a']
end
if args['hp'] then
hp = args['hp']
end
if args['mp'] then
mp = args['mp']
end
local tagItems = sortString(tagIn)
tagOut = ''
for _, item in ipairs(tagItems) do
tag = makeTag(frame, 'traits', item, true)
tagOut = tagOut .. tag
catString = catString .. '[[Category:' .. tags['traits'][item]['link'] .. ']]'
end
local page = mw.title.getCurrentTitle().text
local namespace = frame:preprocess('{{NAMESPACE}}')
local addCats = ((namespace == 'Creature') and (page == name))
local out = "'''CREATURE PAGES ARE A WORK IN PROGRESS'''\n\n"
out = out .. "'''Traits and Types:''' " .. tagOut .. '\n\n'
if hp ~= '' then
out = out .. "'''HP:''' " .. hp .. '\n\n'
end
if mp ~= '' then
out = out .. "'''MP:''' " .. mp .. '\n\n'
end
out = out .. "== Abilities ==\n"
out = out .. '{{AESort|' .. abilitiesIn .. '}}'
if addCats then
out = out .. catString
end
return frame:preprocess(out)
end
end



Latest revision as of 05:01, 20 August 2025

Documentation for this module may be created at Module:C/doc

local p = {}

local function makeBar(frame, title, content, link, flags, bottom)
    if content == '' then return '' end
    local barSpace = mw.html.create('span')
    barSpace:wikitext(frame:preprocess('&nbsp;')):css('display', 'none')
    local barTitle = mw.html.create('span')
    local barTitleClass = "{{lc:" .. title .. "}}"
    barTitleClass = barTitleClass:gsub(' ', '-')
    barTitleClass = barTitleClass:gsub('\'', '')
    barTitleClass = barTitleClass:gsub('\"', '')
    local barText = "{{ucfirst:" .. title .. "}}"
    if link then barText = "[[" .. link .. "|" .. barText .. "]]" end
    barText = barText .. ":"
    barTitle:addClass('ability-bar-title'):wikitext(frame:preprocess(barText))
        :css('font-weight', 'bold'):css('margin-right', '0.5em')
    local barContent = mw.html.create('span')
    barContent:addClass('ability-bar-content'):wikitext(content)
    local output = mw.html.create('div')
    if bottom then output = mw.html.create('span') end
    if not flags then flags = {} end
    for k, v in pairs(flags) do
        if type(v) == 'string' then
            output:addClass('flag-' .. k .. '-' .. v)
        end
    end
    output:addClass('ability-bar'):addClass('ability-bar-' .. barTitleClass)
        :attr(flags):attr('test', 'out'):node(barSpace):node(barTitle):node(
            barSpace):node(barContent):css('display', 'block')
    -- if flags then
    -- for k, v in ipairs(flags) do
    --     output:attr(k, v)
    -- end
    -- end
    return tostring(output)
end

-- === NEW: shared abilities parsing helpers ===
local function parseAbilitiesString(abilitiesString)
    local abilities = {}
    for ability in mw.text.gsplit(abilitiesString or '', ";;", true) do
        ability = mw.text.trim(ability)
        if ability ~= "" then
            local name = ability
            local limited, improved = "", ""
            local gifted, adept = false, false

            -- flags
            if mw.ustring.find(ability, "%-g%-") then gifted = true end
            if mw.ustring.find(ability, "%-a%-") then adept = true end

            -- notes (order-insensitive, tolerate any following marker)
            local limitedMatch = mw.ustring.match(ability,
                                                  "%-l%-(.-)(%-[liag]%-|$)") or
                                     mw.ustring.match(ability, "%-l%-(.+)$")
            if limitedMatch then limited = mw.text.trim(limitedMatch) end

            local improvedMatch = mw.ustring.match(ability,
                                                   "%-i%-(.-)(%-[liag]%-|$)") or
                                      mw.ustring.match(ability, "%-i%-(.+)$")
            if improvedMatch then
                improved = mw.text.trim(improvedMatch)
            end

            -- clean name (strip markers + payloads/flags)
            name = mw.ustring.gsub(name, "%-l%-.+", "")
            name = mw.ustring.gsub(name, "%-i%-.+", "")
            name = mw.ustring.gsub(name, "%-g%-", "")
            name = mw.ustring.gsub(name, "%-a%-", "")
            name = mw.text.trim(name)

            table.insert(abilities, {
                name = name,
                limited = limited,
                improved = improved,
                gifted = gifted,
                adept = adept
            })
        end
    end

    table.sort(abilities, function(a, b) return a.name < b.name end)
    return abilities
end

local function buildAETransclusions(abilities)
    local transclusions = {}
    for _, a in ipairs(abilities or {}) do
        local t = {"{{AE|", a.name}
        if a.limited ~= "" then table.insert(t, "|limited=" .. a.limited) end
        if a.improved ~= "" then
            table.insert(t, "|improved=" .. a.improved)
        end
        if a.gifted then table.insert(t, "|gifted=1") end
        if a.adept then table.insert(t, "|adept=1") end
        table.insert(t, "}}")
        table.insert(transclusions, table.concat(t))
    end
    -- no visible separators between AE templates
    return table.concat(transclusions, "")
end
-- === END helpers ===

function p.c(frame)
    local args = frame.args
    local rawAbilities = args['a'] or ''

    -- === TAGS & CATEGORIES via TagBuilder ===
    local TagBuilder = require('Module:TagBuilder')
    local DiceEval = require('Module:DiceEval')

    local tRaw = args['t'] or ''
    local looks = args['l'] or ''
    local ranks = args['r'] or ''
    local name = args['name'] or ''
    local hpDice = args['hp'] or '1d10'
    local mpDice = args['mp'] or '1d10'
    local hpDiceSizeStep = args['hps'] or ''
    local mpDiceSizeStep = args['mps'] or ''
    if hpDiceSizeStep:len() > 0 then
        local hpPlural = 's'
        if hpDiceSizeStep == '1' then hpPlural = '' end
        hpDiceSizeStep = "Gain another hit die for every " .. hpDiceSizeStep ..
                        " additional size" .. hpPlural .. "."
    end
    if mpDiceSizeStep:len() > 0 then
        local mpPlural = 's'
        if mpDiceSizeStep == '1' then mpPlural = '' end
        mpDiceSizeStep = "Gain another mana die for every " .. mpDiceSizeStep ..
                        " additional size" .. mpPlural .. "."
    end
    local baseHp = tostring(DiceEval._eval(hpDice))
    local baseMp = tostring(DiceEval._eval(mpDice))
    local size = args['size'] or '3'
    local br = args['br'] or '0'
    local int = args['int'] or '0'
    local str = args['str'] or '0'
    local mov = args['mov'] or '0'
    local snk = args['snk'] or '0'
    local per = args['per'] or '0'

    local preAbilities = ''
    if (int == '0') then
        preAbilities = preAbilities .. "Normal Intelligence;; "
    end
    if (str == '0') then preAbilities = preAbilities .. "Normal Strength;; " end
    if (mov == '0') then preAbilities = preAbilities .. "Normal Movement;; " end
    if (snk == '0') then preAbilities = preAbilities .. "Normal Sneak;; " end
    if (per == '0') then
        preAbilities = preAbilities .. "Normal Perception;; "
    end

    local abilitiesString = preAbilities .. rawAbilities
    local opts = {
        name = name,
        ns = 'Creature',
        transparent = args.o or args.transparent
    }
    local tagOut, catOut =
        TagBuilder.build(tRaw, 'TagCreatures', 'traits', opts)

    -- === Parse and render main abilities with shared helpers ===
    local abilities = parseAbilitiesString(abilitiesString)
    local inner = buildAETransclusions(abilities)

    local categoryString = '[[Category:Creatures]]'

    local statContent = ''
    statContent = statContent .. "{{Tag|" .. hpDice ..
                      " Hit Dice|❤️|l=:Category:" .. hpDice ..
                      " hit die creatures|c=#e01b24|class=hp-dice-tagged " ..
                      hpDice .. "}}" .. "{{Tag|" .. baseHp ..
                      " HP|❤️|l=:Category:" .. baseHp ..
                      " HP creatures|c=#e01b24|class=cost-tagged hp" .. baseHp ..
                      "}}"
    categoryString = categoryString .. "[[Category:" .. hpDice ..
                         " hit die creatures" .. "]]"
    categoryString = categoryString .. "[[Category:" .. baseHp ..
                         " HP creatures" .. "]]"
    statContent = statContent .. "{{Tag|" .. mpDice ..
                      " Mana Dice|🩵|l=:Category:" .. mpDice ..
                      " mana die creatures|c=#3584e4|class=mp-dice-tagged " ..
                      mpDice .. "}}" .. "{{Tag|" .. baseMp ..
                      " MP|🩵|l=:Category:" .. baseMp ..
                      " MP creatures|c=#3584e4|class=cost-tagged hp" .. baseMp ..
                      "}}"
    categoryString = categoryString .. "[[Category:" .. baseMp ..
                         " MP creatures" .. "]]"
    categoryString = categoryString .. "[[Category:" .. mpDice ..
                         " mana die creatures" .. "]]"
    statContent = statContent .. "{{Tag|Size " .. size ..
                      "|⬆️|l=:Category:Size " .. size ..
                      " creatures|c=#e4a835|class=size-tagged size" .. size ..
                      "}}"
    categoryString = categoryString .. "[[Category:Size " .. size ..
                         " creatures" .. "]]"

    local attributeContent = ''
    attributeContent = ""
    attributeContent = attributeContent .. "{{Tag|" .. int ..
                           "|INT|l=:Category:INT value of " .. int ..
                           " creatures}}"
    categoryString = categoryString .. "[[Category:INT value of " .. int ..
                         " creatures]]"
    attributeContent = attributeContent .. "{{Tag|" .. str ..
                           "|STR|l=:Category:STR value of " .. str ..
                           " creatures}}"
    categoryString = categoryString .. "[[Category:STR value of " .. str ..
                         " creatures]]"
    attributeContent = attributeContent .. "{{Tag|" .. mov ..
                           "|MOV|l=:Category:MOV value of " .. mov ..
                           " creatures}}"
    categoryString = categoryString .. "[[Category:MOV value of " .. mov ..
                         " creatures]]"
    attributeContent = attributeContent .. "{{Tag|" .. snk ..
                           "|SNK|l=:Category:SNK value of " .. snk ..
                           " creatures}}"
    categoryString = categoryString .. "[[Category:SNK value of " .. snk ..
                         " creatures]]"
    attributeContent = attributeContent .. "{{Tag|" .. per ..
                           "|PER|l=:Category:PER value of " .. per ..
                           " creatures}}"
    categoryString = categoryString .. "[[Category:PER value of " .. per ..
                         " creatures]]"
    traitsContent = (tagOut or '') .. "{{Tag|BR " .. br ..
                        "|⚠️|l=:Category:BR " .. br ..
                        " creatures|class=br-tagged br" .. br .. "}}"
    categoryString = categoryString .. "[[Category:BR " .. br .. " creatures]]"

    local out = ""
    out = out .. makeBar(frame, "Stats", statContent, nil, nil, false)
    out = out .. makeBar(frame, "Attributes", attributeContent, nil, nil, false)
    out = out .. makeBar(frame, "Traits", traitsContent, nil, nil, false)
    out = out .. makeBar(frame, "Looks", looks, nil, nil, false)
    out = out .. makeBar(frame, "Innate ranks", ranks, nil, nil, false)
    out = out .. makeBar(frame, "HP increase", hpDiceSizeStep, nil, nil, false)
    out = out .. makeBar(frame, "MP increase", mpDiceSizeStep, nil, nil, false)
    if args['lifespan'] then
        local lifespanText = args['lifespan'] .. " years."
        categoryString = categoryString .. "[[Category:" .. args['lifespan'] ..
                             " year lifespan creatures]]"
        if args['adult'] then
            lifespanText = lifespanText .. " Adult at age " .. args['adult'] ..
                               "."
        end
        out = out .. makeBar(frame, "Lifespan", lifespanText, nil, nil, false)
    end

    out = out .. '<hr>'
    if args['d'] then
        out = out .. '<span class="creature-description>' .. args['d'] ..
                  '</span><hr>'
    end

    -- main abilities block
    out = out .. '<div class="expandable-table"><div>' ..
              frame:preprocess(inner) .. '</div></div>'

    -- === familiar abilities (args['f']) using the same parsing ===
    if args['f'] and mw.text.trim(args['f']) ~= '' then
        local fAbilities = parseAbilitiesString(args['f'])
        local fInner = buildAETransclusions(fAbilities)
        -- You can wrap in a bar or just mirror the main output style; here we label it:
        out = out .. '<hr>' .. makeBar(frame, "Familiar Abilities",
                                       frame:preprocess(
                                           '<div class="expandable-table"><div>' ..
                                               fInner .. '</div></div>'), nil,
                                       nil, false)
    end

    out = out .. (catOut or '')

    local page = mw.title.getCurrentTitle().text
    local namespace = frame:preprocess('{{NAMESPACE}}')
    if ((namespace == "Creature") and (page == name)) then
        out = out .. categoryString
    end

    return frame:preprocess(out)
end

return p