Jump to content
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
// via Wikitext Extension for VSCode
Tag: Reverted
No edit summary
 
(83 intermediate revisions by the same user not shown)
Line 1: Line 1:
local p = {}
local p = {}


-- Memoize category lookups within a single parse
local function makeBar(frame, title, content, link, flags, bottom)
local _catMemo = {}
    if content == '' then return '' end
    local barSpace = mw.html.create('span')
    barSpace:wikitext(frame:preprocess(' ')):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


-- Resolve a raw ability name to a proper Title in the Ability namespace
            -- notes (order-insensitive, tolerate any following marker)
local function resolveAbilityTitle(raw)
            local limitedMatch = mw.ustring.match(ability,
if not raw or raw == "" then return nil end
                                                  "%-l%-(.-)(%-[liag]%-|$)") or
                                    mw.ustring.match(ability, "%-l%-(.+)$")
            if limitedMatch then limited = mw.text.trim(limitedMatch) end


-- If caller already gave a full title and it exists, use it
            local improvedMatch = mw.ustring.match(ability,
local t = mw.title.new(raw)
                                                  "%-i%-(.-)(%-[liag]%-|$)") or
if t and t.exists then return t end
                                      mw.ustring.match(ability, "%-i%-(.+)$")
            if improvedMatch then
                improved = mw.text.trim(improvedMatch)
            end


-- Otherwise, try in the Ability namespace
            -- clean name (strip markers + payloads/flags)
local abilityNS = mw.site.namespaces["Ability"]
            name = mw.ustring.gsub(name, "%-l%-.+", "")
if abilityNS and abilityNS.id then
            name = mw.ustring.gsub(name, "%-i%-.+", "")
local t2 = mw.title.new(raw, abilityNS.id)  -- "Ability:raw"
            name = mw.ustring.gsub(name, "%-g%-", "")
if t2 and t2.exists then return t2 end
            name = mw.ustring.gsub(name, "%-a%-", "")
end
            name = mw.text.trim(name)


-- Fallback: explicit prefix (covers wikis where namespace lookup differs)
            table.insert(abilities, {
if not mw.ustring.find(raw, "^Ability:") then
                name = name,
local t3 = mw.title.new("Ability:" .. raw)
                limited = limited,
if t3 and t3.exists then return t3 end
                improved = improved,
end
                gifted = gifted,
                adept = adept
            })
        end
    end


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


-- Robust category check using parent categories (handles categories added via templates)
local function buildAETransclusions(abilities)
local function pageInCategory(pagetitle, cat)
    local transclusions = {}
if not pagetitle or pagetitle == "" or not cat or cat == "" then return false end
    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


local memoKey = pagetitle .. "@@" .. cat
-- === Body parts parsing helpers ===
if _catMemo[memoKey] ~= nil then
local function parseBodyPartsString(bodyPartsString)
return _catMemo[memoKey]
    local bodyParts = {}
end
    for part in mw.text.gsplit(bodyPartsString or '', ";;", true) do
        part = mw.text.trim(part)
        if part ~= "" then table.insert(bodyParts, part) end
    end


local title = resolveAbilityTitle(pagetitle)
    table.sort(bodyParts)
if not title then
    return bodyParts
_catMemo[memoKey] = false
end
return false
end


-- Follow redirects
local function buildBPTransclusions(bodyParts)
if title.isRedirect and title.redirectTarget and title.redirectTarget.exists then
    local transclusions = {}
title = title.redirectTarget
    for _, part in ipairs(bodyParts or {}) do
end
        table.insert(transclusions, "{{BP|" .. part .. "}}")
 
    end
local parents = title:getParentCategories()
    return table.concat(transclusions, "")
if not parents then
end
_catMemo[memoKey] = false
return false
end


local want = "Category:" .. cat
-- === Equipment parsing helpers ===
for parentCat in pairs(parents) do
local function parseEquipmentString(equipmentString)
if parentCat == want then
    local equipment = {}
_catMemo[memoKey] = true
    for part in mw.text.gsplit(equipmentString or '', ";;", true) do
return true
        part = mw.text.trim(part)
end
        if part ~= "" then table.insert(equipment, part) end
end
    end


_catMemo[memoKey] = false
    table.sort(equipment)
return false
    return equipment
end
end


 
local function buildEQTransclusions(equipment)
-- Build AE template from an ability object
    local transclusions = {}
local function aeTemplate(a)
    for _, part in ipairs(equipment or {}) do
local parts = {"{{AE|", a.name}
        table.insert(transclusions, "{{EQ|" .. part .. "}}")
if a.limited  and a.limited  ~= "" then table.insert(parts, "|limited=" .. a.limited)   end
    end
if a.improved and a.improved ~= "" then table.insert(parts, "|improved=" .. a.improved) end
    return table.concat(transclusions, "")
if a.gifted                        then table.insert(parts, "|gifted=1")               end
if a.adept                          then table.insert(parts, "|adept=1")                end
table.insert(parts, "}}")
return table.concat(parts)
end
end
-- === END helpers ===


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


-- Prepopulate defaults
    -- === TAGS & CATEGORIES via TagBuilder ===
local abilities = {}
    local TagBuilder = require('Module:TagBuilder')
local indexByName = {}
    local DiceEval = require('Module:DiceEval')


local function addAbility(obj, isDefault)
    local typeRaw = args['c'] or ''
local key = mw.text.trim(obj.name or "")
    local traitRaw = args['t'] or ''
if key == "" then return end
    local looks = args['l'] or ''
obj.name = key
    local description = args['d'] or ''
if indexByName[key] then
    local ranks = args['r'] or ''
abilities[indexByName[key]] = obj
    local instructions = args['i'] or ''
    local regionRaw = args['region'] or args['g'] or ''
    local name = args['name'] or ''
    local attributeSizeIncrease = args['as'] or ''
    local hpDice = args['hp'] or '1d10'
    local mpDice = args['mp'] or '1d10'
    local hpDiceNumberStep = args['hpn'] or ''
    local mpDiceNumberStep = args['mpn'] or ''
    local hpDiceNumberText = 'hit die'
    local mpDiceNumberText = 'mana die'
    if hpDiceNumberStep:len() > 0 then
    if hpDiceNumberStep == '1' then
    hpDiceNumberStep = ''
else
hpDiceNumberStep = hpDiceNumberStep .. ' '
end
        hpDiceNumberText = hpDiceNumberStep .. 'hit dice'
    end
    if mpDiceNumberStep:len() > 0 then
    if mpDiceNumberStep == '1' then
    mpDiceNumberStep = ''
else
else
table.insert(abilities, obj)
mpDiceNumberStep = mpDiceNumberStep .. ' '
indexByName[key] = #abilities
end
end
if isDefault then abilities[indexByName[key]]._isDefault = true end
        mpDiceNumberText = mpDiceNumberStep .. 'mana dice'
end
    end
    local hpDiceSizeStep = args['hps'] or ''
    local mpDiceSizeStep = args['mps'] or ''
    local hpDiceMinSize = args['hpm'] or ''
    local mpDiceMinSize = args['mpm'] or ''
    if hpDiceMinSize:len() > 0 then
        hpDiceMinSize = " above " .. hpDiceMinSize
    end
    if mpDiceMinSize:len() > 0 then
        mpDiceMinSize = " above " .. mpDiceMinSize
    end
    if hpDiceSizeStep:len() > 0 then
        local hpPlural = 's'
        if hpDiceSizeStep == '1' then hpPlural = '' end
        hpDiceSizeStep = "Gain another " .. hpDiceNumberText .. " for every " ..
                            hpDiceSizeStep .. " additional size" .. hpPlural ..
                            hpDiceMinSize .. "."
    end
    if mpDiceSizeStep:len() > 0 then
        local mpPlural = 's'
        if mpDiceSizeStep == '1' then mpPlural = '' end
        mpDiceSizeStep = "Gain another " .. mpDiceNumberText .. " for every " ..
                            mpDiceSizeStep .. " additional size" .. mpPlural ..
                            mpDiceMinSize .. "."
    end
    local baseHp = '0'
    local baseMp = '0'
    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 function removeByName(name)
    local preAbilities = ''
local i = indexByName[name]
    if (int == '0') then
if not i then return end
        preAbilities = preAbilities .. "Normal Intelligence;; "
table.remove(abilities, i)
    end
-- rebuild index
    if (str == '0') then preAbilities = preAbilities .. "Normal Strength;; " end
indexByName = {}
    if (mov == '0') then preAbilities = preAbilities .. "Normal Movement;; " end
for idx, ab in ipairs(abilities) do
    if (snk == '0') then preAbilities = preAbilities .. "Normal Sneak;; " end
indexByName[ab.name] = idx
    if (per == '0') then
end
        preAbilities = preAbilities .. "Normal Perception;; "
end
    end


-- Defaults
    local abilitiesString = preAbilities .. rawAbilities
local defaults = {
    local opts = {
"Normal Intelligence",
        name = name,
"Normal Strength",
        ns = 'Creature',
"Normal Movement",
        transparent = args.o or args.transparent
"Normal Sneak",
    }
"Normal Perception",
    local traitOut, traitCatOut = TagBuilder.build(traitRaw, 'TagCreatures',
}
                                                  'traits', opts)
for _, n in ipairs(defaults) do
    local typeOut, typeCatOut = TagBuilder.build(typeRaw, 'TagCreatures',
addAbility({
                                                'types', opts)
name = n, limited = "", improved = "", gifted = false, adept = false
    local regionOut, regionCatOut = TagBuilder.build(regionRaw, 'TagCreatures', 'regions', opts)
}, true)
end


-- Parse incoming abilities
    -- === Parse and render main abilities with shared helpers ===
local incomingNames = {} -- keep a list to check categories against (only incoming)
    local abilities = parseAbilitiesString(abilitiesString)
for ability in mw.text.gsplit(rawAbilities, ";;", true) do
    local inner = buildAETransclusions(abilities)
ability = mw.text.trim(ability)
if ability ~= "" then
local name = ability
local limited, improved = "", ""
local gifted, adept = false, false


if mw.ustring.find(ability, "%-g%-") then gifted = true end
    -- === Parse body parts ===
if mw.ustring.find(ability, "%-a%-") then adept = true end
    local bodyParts = parseBodyPartsString(rawBodyParts)
    local bodyPartsInner = buildBPTransclusions(bodyParts)


local limitedMatch = mw.ustring.match(ability, "%-l%-(.-)(%-[liag]%-|$)")
    -- === Parse equipment ===
or mw.ustring.match(ability, "%-l%-(.+)$")
    local equipment = parseEquipmentString(rawEquipment)
if limitedMatch then limited = mw.text.trim(limitedMatch) end
    local equipmentInner = buildEQTransclusions(equipment)


local improvedMatch = mw.ustring.match(ability, "%-i%-(.-)(%-[liag]%-|$)")
    local categoryString = '[[Category:Creatures]]'
or mw.ustring.match(ability, "%-i%-(.+)$")
    local statContent = ''
if improvedMatch then improved = mw.text.trim(improvedMatch) end
    local inheritedStats = false


-- clean name (strip markers and payloads/flags)
    if (hpDice == 'x') then
name = mw.ustring.gsub(name, "%-l%-.+", "")
        statContent = statContent ..
name = mw.ustring.gsub(name, "%-i%-.+", "")
                          "{{Tag|Variable Hit Dice|❤️|l=:Category:Variable hit die creatures|c=#e01b24|class=hp-dice-tagged x}}"
name = mw.ustring.gsub(name, "%-g%-", "")
        categoryString = categoryString ..
name = mw.ustring.gsub(name, "%-a%-", "")
                            "[[Category:Variable hit die creatures]]"
name = mw.text.trim(name)
        inheritedStats = true
    else
        baseHp = tostring(DiceEval._eval(hpDice))
        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" .. "]]"
    end
    if (mpDice == 'x') then
        statContent = statContent ..
                          "{{Tag|Variable Mana Dice|🩵|l=:Category:Variable mana die creatures|c=#3584e4|class=mp-dice-tagged x}}"
        categoryString = categoryString ..
                            "[[Category:Variable mana die creatures]]"
        inheritedStats = true
    else
        baseMp = tostring(DiceEval._eval(mpDice))
        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" .. "]]"
    end
    if (size == 'x') then
        statContent = statContent ..
                          "{{Tag|Variable Size|⬆️|l=:Category:Variable size creatures|c=#e4a835|class=size-tagged x}}"
        categoryString = categoryString ..
                            "[[Category:Variable size creatures]]"
        inheritedStats = true
    else
        statContent = statContent .. "{{Tag|Size " .. size ..
                          "|⬆️|l=:Category:Size " .. size ..
                          " creatures|c=#e4a835|class=size-tagged size" .. size ..
                          "}}"
        categoryString = categoryString .. "[[Category:Size " .. size ..
                            " creatures" .. "]]"
    end


addAbility({
    local attributeContent = ''
name = name,
    attributeContent = ""
limited = limited,
    if (int ~= 'x') then
improved = improved,
        attributeContent = attributeContent .. "{{Tag|" .. int ..
gifted = gifted,
                              "|INT|l=:Category:INT value of " .. int ..
adept = adept
                              " creatures}}"
}, false)
        categoryString = categoryString .. "[[Category:INT value of " .. int ..
                            " creatures]]"
    end
    if (str ~= 'x') then
        attributeContent = attributeContent .. "{{Tag|" .. str ..
                              "|STR|l=:Category:STR value of " .. str ..
                              " creatures}}"
        categoryString = categoryString .. "[[Category:STR value of " .. str ..
                            " creatures]]"
    end
    if (mov ~= 'x') then
        attributeContent = attributeContent .. "{{Tag|" .. mov ..
                              "|MOV|l=:Category:MOV value of " .. mov ..
                              " creatures}}"
        categoryString = categoryString .. "[[Category:MOV value of " .. mov ..
                            " creatures]]"
    end
    if (snk ~= 'x') then
        attributeContent = attributeContent .. "{{Tag|" .. snk ..
                              "|SNK|l=:Category:SNK value of " .. snk ..
                              " creatures}}"
        categoryString = categoryString .. "[[Category:SNK value of " .. snk ..
                            " creatures]]"
    end
    if (per ~= 'x') then
        attributeContent = attributeContent .. "{{Tag|" .. per ..
                              "|PER|l=:Category:PER value of " .. per ..
                              " creatures}}"
        categoryString = categoryString .. "[[Category:PER value of " .. per ..
                            " creatures]]"
    end
    traitContent = (traitOut or '') .. "{{Tag|BR " .. br ..
                      "|⚠️|l=:Category:BR " .. br ..
                      " creatures|class=br-tagged br" .. br .. "}}"
    typeContent = (typeOut or '')
    regionContent = (regionOut or '')
    categoryString = categoryString .. "[[Category:BR " .. br .. " creatures]]"


table.insert(incomingNames, name)
    local out = ""
end
    out = out .. makeBar(frame, "Stats", statContent, nil, nil, false)
end
    out = out .. makeBar(frame, "Attributes", attributeContent, nil, nil, false)
    out = out .. makeBar(frame, "Traits", traitContent, nil, nil, false)
    out = out .. makeBar(frame, "Creature types", typeContent, nil, nil, false)
    out = out .. makeBar(frame, "Regions", regionContent, 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)
    out = out ..
              makeBar(frame, "Attribute increase", attributeSizeIncrease, nil,
                      nil, false)
    if (inheritedStats) then
        out = out .. makeBar(frame, "Inherited stats",
                            "Variable stats are inherited from the parent {{L|Category|Creatures|creature}}.",
                            nil, nil, false)
    end
    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 .. makeBar(frame, "Instructions", instructions, nil, nil, false)
    out = out .. '<hr>'
    if description:len() > 0 then
        out = out .. '<span class="creature-description>' .. args['d'] ..
                  '</span><hr>'
    end


-- If any incoming ability is in the mapped category, remove the corresponding default
    -- main abilities block
local catMap = {
     out = out .. makeBar(frame, "Abilities",
["Normal Intelligence"] = "INT setting abilities",
                        '<div class="expandable-table"><div>' ..
["Normal Strength"]     = "STR setting abilities",
                            frame:preprocess(inner) .. '</div></div>', nil,
["Normal Movement"]    = "MOV setting abilities",
                        nil, false)
["Normal Sneak"]        = "SNK setting abilities",
["Normal Perception"]  = "PER setting abilities",
}


local shouldRemove = {
    -- === body parts block (after abilities, before equipment) ===
["Normal Intelligence"] = false,
     if rawBodyParts ~= '' and #bodyParts > 0 then
["Normal Strength"]     = false,
        out = out .. '<hr>' .. makeBar(frame, "Body Parts",
["Normal Movement"]    = false,
                                      '<div class="expandable-table"><div>' ..
["Normal Sneak"]        = false,
                                          frame:preprocess(bodyPartsInner) ..
["Normal Perception"]  = false,
                                          '</div></div>', nil, nil, false)
}
    end


for _, inName in ipairs(incomingNames) do
    -- === equipment block (after body parts, before familiar abilities) ===
for normalName, cat in pairs(catMap) do
    if rawEquipment ~= '' and #equipment > 0 then
if not shouldRemove[normalName] and pageInCategory(inName, cat) then
        out = out .. '<hr>' .. makeBar(frame, "Equipment",
shouldRemove[normalName] = true
                                      '<div class="expandable-table"><div>' ..
end
                                          frame:preprocess(equipmentInner) ..
end
                                          '</div></div>', nil, nil, false)
end
    end


for normalName, flag in pairs(shouldRemove) do
    -- === familiar abilities (args['f']) using the same parsing ===
if flag then
    if args['f'] and mw.text.trim(args['f']) ~= '' then
removeByName(normalName)
        local fAbilities = parseAbilitiesString(args['f'])
end
        local fInner = buildAETransclusions(fAbilities)
end
        -- 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


-- Sort alphabetically by name (case-insensitive, but stable on original)
    out = out .. (traitCatOut or '') .. (typeCatOut or '') .. (regionCatOut or '')
table.sort(abilities, function(a, b)
local aa, bb = mw.ustring.lower(a.name), mw.ustring.lower(b.name)
if aa == bb then return a.name < b.name end
return aa < bb
end)


-- Assemble output and expand the AE templates right here
    local page = mw.title.getCurrentTitle().text
local outParts = {}
    local namespace = frame:preprocess('{{NAMESPACE}}')
for _, a in ipairs(abilities) do
    if ((namespace == "Creature") and (page == name)) then
table.insert(outParts, aeTemplate(a))
        out = out .. categoryString
end
    end
local inner = table.concat(outParts, "")
local out = '<div class="expandable-table"><div>' .. frame:preprocess(inner) .. '</div></div>'


return out
    return frame:preprocess(out)
end
end


return p
return p

Latest revision as of 16:34, 17 July 2026

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

-- === Body parts parsing helpers ===
local function parseBodyPartsString(bodyPartsString)
    local bodyParts = {}
    for part in mw.text.gsplit(bodyPartsString or '', ";;", true) do
        part = mw.text.trim(part)
        if part ~= "" then table.insert(bodyParts, part) end
    end

    table.sort(bodyParts)
    return bodyParts
end

local function buildBPTransclusions(bodyParts)
    local transclusions = {}
    for _, part in ipairs(bodyParts or {}) do
        table.insert(transclusions, "{{BP|" .. part .. "}}")
    end
    return table.concat(transclusions, "")
end

-- === Equipment parsing helpers ===
local function parseEquipmentString(equipmentString)
    local equipment = {}
    for part in mw.text.gsplit(equipmentString or '', ";;", true) do
        part = mw.text.trim(part)
        if part ~= "" then table.insert(equipment, part) end
    end

    table.sort(equipment)
    return equipment
end

local function buildEQTransclusions(equipment)
    local transclusions = {}
    for _, part in ipairs(equipment or {}) do
        table.insert(transclusions, "{{EQ|" .. part .. "}}")
    end
    return table.concat(transclusions, "")
end
-- === END helpers ===

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

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

    local typeRaw = args['c'] or ''
    local traitRaw = args['t'] or ''
    local looks = args['l'] or ''
    local description = args['d'] or ''
    local ranks = args['r'] or ''
    local instructions = args['i'] or ''
    local regionRaw = args['region'] or args['g'] or ''
    local name = args['name'] or ''
    local attributeSizeIncrease = args['as'] or ''
    local hpDice = args['hp'] or '1d10'
    local mpDice = args['mp'] or '1d10'
    local hpDiceNumberStep = args['hpn'] or ''
    local mpDiceNumberStep = args['mpn'] or ''
    local hpDiceNumberText = 'hit die'
    local mpDiceNumberText = 'mana die'
    if hpDiceNumberStep:len() > 0 then
    	if hpDiceNumberStep == '1' then
    		hpDiceNumberStep = ''
		else
			hpDiceNumberStep = hpDiceNumberStep .. ' '
		end
        hpDiceNumberText = hpDiceNumberStep .. 'hit dice'
    end
    if mpDiceNumberStep:len() > 0 then
    	if mpDiceNumberStep == '1' then
    		mpDiceNumberStep = ''
		else
			mpDiceNumberStep = mpDiceNumberStep .. ' '
		end
        mpDiceNumberText = mpDiceNumberStep .. 'mana dice'
    end
    local hpDiceSizeStep = args['hps'] or ''
    local mpDiceSizeStep = args['mps'] or ''
    local hpDiceMinSize = args['hpm'] or ''
    local mpDiceMinSize = args['mpm'] or ''
    if hpDiceMinSize:len() > 0 then
        hpDiceMinSize = " above " .. hpDiceMinSize
    end
    if mpDiceMinSize:len() > 0 then
        mpDiceMinSize = " above " .. mpDiceMinSize
    end
    if hpDiceSizeStep:len() > 0 then
        local hpPlural = 's'
        if hpDiceSizeStep == '1' then hpPlural = '' end
        hpDiceSizeStep = "Gain another " .. hpDiceNumberText .. " for every " ..
                             hpDiceSizeStep .. " additional size" .. hpPlural ..
                             hpDiceMinSize .. "."
    end
    if mpDiceSizeStep:len() > 0 then
        local mpPlural = 's'
        if mpDiceSizeStep == '1' then mpPlural = '' end
        mpDiceSizeStep = "Gain another " .. mpDiceNumberText .. " for every " ..
                             mpDiceSizeStep .. " additional size" .. mpPlural ..
                             mpDiceMinSize .. "."
    end
    local baseHp = '0'
    local baseMp = '0'
    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 traitOut, traitCatOut = TagBuilder.build(traitRaw, 'TagCreatures',
                                                   'traits', opts)
    local typeOut, typeCatOut = TagBuilder.build(typeRaw, 'TagCreatures',
                                                 'types', opts)
    local regionOut, regionCatOut = TagBuilder.build(regionRaw, 'TagCreatures', 'regions', opts)

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

    -- === Parse body parts ===
    local bodyParts = parseBodyPartsString(rawBodyParts)
    local bodyPartsInner = buildBPTransclusions(bodyParts)

    -- === Parse equipment ===
    local equipment = parseEquipmentString(rawEquipment)
    local equipmentInner = buildEQTransclusions(equipment)

    local categoryString = '[[Category:Creatures]]'
    local statContent = ''
    local inheritedStats = false

    if (hpDice == 'x') then
        statContent = statContent ..
                          "{{Tag|Variable Hit Dice|❤️|l=:Category:Variable hit die creatures|c=#e01b24|class=hp-dice-tagged x}}"
        categoryString = categoryString ..
                             "[[Category:Variable hit die creatures]]"
        inheritedStats = true
    else
        baseHp = tostring(DiceEval._eval(hpDice))
        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" .. "]]"
    end
    if (mpDice == 'x') then
        statContent = statContent ..
                          "{{Tag|Variable Mana Dice|🩵|l=:Category:Variable mana die creatures|c=#3584e4|class=mp-dice-tagged x}}"
        categoryString = categoryString ..
                             "[[Category:Variable mana die creatures]]"
        inheritedStats = true
    else
        baseMp = tostring(DiceEval._eval(mpDice))
        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" .. "]]"
    end
    if (size == 'x') then
        statContent = statContent ..
                          "{{Tag|Variable Size|⬆️|l=:Category:Variable size creatures|c=#e4a835|class=size-tagged x}}"
        categoryString = categoryString ..
                             "[[Category:Variable size creatures]]"
        inheritedStats = true
    else
        statContent = statContent .. "{{Tag|Size " .. size ..
                          "|⬆️|l=:Category:Size " .. size ..
                          " creatures|c=#e4a835|class=size-tagged size" .. size ..
                          "}}"
        categoryString = categoryString .. "[[Category:Size " .. size ..
                             " creatures" .. "]]"
    end

    local attributeContent = ''
    attributeContent = ""
    if (int ~= 'x') then
        attributeContent = attributeContent .. "{{Tag|" .. int ..
                               "|INT|l=:Category:INT value of " .. int ..
                               " creatures}}"
        categoryString = categoryString .. "[[Category:INT value of " .. int ..
                             " creatures]]"
    end
    if (str ~= 'x') then
        attributeContent = attributeContent .. "{{Tag|" .. str ..
                               "|STR|l=:Category:STR value of " .. str ..
                               " creatures}}"
        categoryString = categoryString .. "[[Category:STR value of " .. str ..
                             " creatures]]"
    end
    if (mov ~= 'x') then
        attributeContent = attributeContent .. "{{Tag|" .. mov ..
                               "|MOV|l=:Category:MOV value of " .. mov ..
                               " creatures}}"
        categoryString = categoryString .. "[[Category:MOV value of " .. mov ..
                             " creatures]]"
    end
    if (snk ~= 'x') then
        attributeContent = attributeContent .. "{{Tag|" .. snk ..
                               "|SNK|l=:Category:SNK value of " .. snk ..
                               " creatures}}"
        categoryString = categoryString .. "[[Category:SNK value of " .. snk ..
                             " creatures]]"
    end
    if (per ~= 'x') then
        attributeContent = attributeContent .. "{{Tag|" .. per ..
                               "|PER|l=:Category:PER value of " .. per ..
                               " creatures}}"
        categoryString = categoryString .. "[[Category:PER value of " .. per ..
                             " creatures]]"
    end
    traitContent = (traitOut or '') .. "{{Tag|BR " .. br ..
                       "|⚠️|l=:Category:BR " .. br ..
                       " creatures|class=br-tagged br" .. br .. "}}"
    typeContent = (typeOut or '')
    regionContent = (regionOut or '')
    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", traitContent, nil, nil, false)
    out = out .. makeBar(frame, "Creature types", typeContent, nil, nil, false)
    out = out .. makeBar(frame, "Regions", regionContent, 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)
    out = out ..
              makeBar(frame, "Attribute increase", attributeSizeIncrease, nil,
                      nil, false)
    if (inheritedStats) then
        out = out .. makeBar(frame, "Inherited stats",
                             "Variable stats are inherited from the parent {{L|Category|Creatures|creature}}.",
                             nil, nil, false)
    end
    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 .. makeBar(frame, "Instructions", instructions, nil, nil, false)
    out = out .. '<hr>'
    if description:len() > 0 then
        out = out .. '<span class="creature-description>' .. args['d'] ..
                  '</span><hr>'
    end

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

    -- === body parts block (after abilities, before equipment) ===
    if rawBodyParts ~= '' and #bodyParts > 0 then
        out = out .. '<hr>' .. makeBar(frame, "Body Parts",
                                       '<div class="expandable-table"><div>' ..
                                           frame:preprocess(bodyPartsInner) ..
                                           '</div></div>', nil, nil, false)
    end

    -- === equipment block (after body parts, before familiar abilities) ===
    if rawEquipment ~= '' and #equipment > 0 then
        out = out .. '<hr>' .. makeBar(frame, "Equipment",
                                       '<div class="expandable-table"><div>' ..
                                           frame:preprocess(equipmentInner) ..
                                           '</div></div>', nil, nil, false)
    end

    -- === 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 .. (traitCatOut or '') .. (typeCatOut or '') .. (regionCatOut 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