Jump to content

Module:Infobox: Difference between revisions

From Apogea Wiki
Dane (talk | contribs)
Use #spritescale parser function directly instead of StatIcon template (via update-page on MediaWiki MCP Server)
Dane (talk | contribs)
Use stat labels from ItemConfig (via update-page on MediaWiki MCP Server)
 
(20 intermediate revisions by the same user not shown)
Line 1: Line 1:
local p = {}
local p = {}
local ItemConfig = require('Module:ItemConfig')


-- Stat definitions: key -> { text, icon, isBonus }
local function makeIcon(frame, icon, tooltip)
local stats = {
    local img = frame:preprocess('{{#spritescale:' .. icon .. '|stat}}')
    wgt = { text = 'Weight', icon = 'Weight.png' },
     return '<span title="' .. mw.text.encode(tooltip) .. '">' .. img .. '</span>'
    rng = { text = 'Range', icon = 'Range.png' },
end
    dmg = { text = 'Damage', icon = 'Damage.png' },
     eq = { text = 'Equip Size', icon = 'Weight.png' },
    hp = { text = 'HP', icon = 'Health.png', isBonus = true },
    mp = { text = 'MP', icon = 'Mana.png', isBonus = true },
    ab = { text = 'Ability', icon = 'Ability.png', isBonus = true },
    mag = { text = 'Magic', icon = 'Magic.png', isBonus = true },
    arm = { text = 'Armor', icon = 'Armor.png', isBonus = true },
    def = { text = 'Defense', icon = 'Defense.png', isBonus = true },
    ms = { text = 'Move Speed', icon = 'Move_Speed.png', isBonus = true },
    as = { text = 'Attack Speed', icon = 'Attack_Speed.png', isBonus = true },
    hpregen = { text = 'HP Regen', icon = 'Health_Regen.png', isBonus = true },
    mpregen = { text = 'MP Regen', icon = 'Mana_Regen.png', isBonus = true },
}


-- Other icons
-- Format a stat line with positive/negative coloring
local icons = {
local function formatStat(statKey, value)
     type = { icon = 'Question_Mark.png', text = 'Type' },
    if not value or value == "" then
}
        return nil
    end
   
     local label = ItemConfig.getStatLabel(statKey)
    local numValue = tonumber(value)
    if numValue then
        local colorClass, prefix
        if numValue >= 0 then
            colorClass = "color-conifer"
            prefix = "+"
        else
            colorClass = "color-coral"
            prefix = ""
        end
        return string.format('<p>%s: <span class="%s">%s%s</span></p>', label, colorClass, prefix, value)
    else
        return string.format('<p>%s: %s</p>', label, value)
    end
end


local function formatBonus(value)
-- Format a plain text line
     if not value or value == '' then
local function formatLine(text, colorClass)
     if not text or text == "" then
         return nil
         return nil
     end
     end
     local num = tonumber(value)
     if colorClass then
     if num and num > 0 then
        return string.format('<p class="%s">%s</p>', colorClass, text)
         return '+' .. value
     else
         return string.format('<p>%s</p>', text)
     end
     end
    return value
end
end


local function makeIcon(frame, icon, tooltip)
-- Format description with bracketed text on separate lines and colored
     local img = frame:preprocess('{{#spritescale:' .. icon .. '|stat}}')
local function formatDescription(desc)
    return '<span title="' .. mw.text.encode(tooltip) .. '">' .. img .. '</span>'
     if not desc or desc == "" then
        return {}
    end
   
    local lines = {}
    local remaining = desc
   
    -- Pattern to find [bracketed text]
    while remaining and remaining ~= "" do
        local beforeBracket, bracketContent, afterBracket = remaining:match("^(.-)%[([^%]]+)%](.*)$")
       
        if bracketContent then
            -- Add any text before the bracket as jade-colored description
            if beforeBracket and beforeBracket:match("%S") then
                local trimmed = beforeBracket:match("^%s*(.-)%s*$")
                if trimmed ~= "" then
                    table.insert(lines, string.format('<p class="color-jade">%s</p>', trimmed))
                end
            end
           
            -- Add the bracketed text as columbia-colored (item-important)
            table.insert(lines, string.format('<p class="color-columbia">[%s]</p>', bracketContent))
           
            remaining = afterBracket
        else
            -- No more brackets, add remaining text as jade-colored
            local trimmed = remaining:match("^%s*(.-)%s*$")
            if trimmed ~= "" then
                table.insert(lines, string.format('<p class="color-jade">%s</p>', trimmed))
            end
            remaining = nil
        end
    end
   
    return lines
end
 
-- Split a comma-separated string into a table
local function splitTypes(typeStr)
    if not typeStr or typeStr == "" then
        return {}
    end
    local types = {}
    for t in typeStr:gmatch("[^,]+") do
        local trimmed = t:match("^%s*(.-)%s*$")
        if trimmed and trimmed ~= "" then
            table.insert(types, trimmed)
        end
    end
    return types
end
end


function p.new(frame)
-- Query item data from Cargo
     local builder = {
local function getItemData(name, rarity)
         root = mw.html.create('table'),
     local tables = 'Items'
         frame = frame
    local fields = ItemConfig.cargoFields.items
    local args = {
         where = 'name="' .. name .. '"',
         limit = 1
     }
     }
      
      
     builder.root
     if rarity and rarity ~= '' then
        :addClass('wikitable')
         args.where = args.where .. ' AND rarity="' .. rarity .. '"'
         :addClass('infobox')
    end
        :css('float', 'right')
   
        :css('clear', 'right')
    local result = mw.ext.cargo.query(tables, fields, args)
        :css('margin', '0 0 1em 1em')
        :css('width', '250px')
      
      
     setmetatable(builder, { __index = p })
     if result and result[1] then
     return builder
        return result[1]
    end
     return nil
end
end


function p:addClass(class)
-- Query monster data from Cargo
     self.root:addClass(class)
local function getMonsterData(name)
     return self
     local tables = 'Monsters'
    local fields = 'name,sprite,type,hp,xp,armor,defense,attack_speed,move_speed,respawn'
    local args = {
        where = 'name="' .. name .. '"',
        limit = 1
    }
   
    local result = mw.ext.cargo.query(tables, fields, args)
   
    if result and result[1] then
        return result[1]
    end
     return nil
end
end


function p:addHeader(text)
-- Query NPC data from Cargo
     self.root:tag('tr')
local function getNPCData(name)
         :tag('th')
     local tables = 'NPCs'
            :attr('colspan', 2)
    local fields = 'name,sprite,location,type,trainer'
            :css('text-align', 'center')
    local args = {
            :wikitext(text)
         where = 'name="' .. name .. '"',
     return self
        limit = 1
    }
   
    local result = mw.ext.cargo.query(tables, fields, args)
   
    if result and result[1] then
        return result[1]
    end
     return nil
end
end


function p:addImage(content)
function p.item(frame)
     self.root:tag('tr')
    local args = frame:getParent().args
         :tag('td')
     local itemName = args.name or args[1] or mw.title.getCurrentTitle().text
             :attr('colspan', 2)
    local rarity = args.rarity
             :css('text-align', 'center')
    local float = args.float or "right"
             :wikitext(content)
    local spriteSize = args.spriteSize or "64"
     return self
    local previewMode = args.preview == "true" or args.preview == "1"
end
   
 
    local data
function p:addRow(label, value, iconKey)
   
    if not value or value == '' then
    -- If preview mode or direct data provided, use args instead of Cargo
         return self
    if previewMode or args.damage or args.armor or args.type then
         data = {
            name = itemName,
            sprite = args.sprite or itemName,
            slot = args.slot,
            type = args.type,
            weight = args.weight,
            rng = args.rng,
            damage = args.damage,
            health = args.health,
            mana = args.mana,
            ability = args.ability,
            magic = args.magic,
            armor = args.armor,
            defense = args.defense,
            move_speed = args.move_speed,
            attack_speed = args.attack_speed,
            size = args.size,
            container = args.container,
            health_regen = args.health_regen,
             mana_regen = args.mana_regen,
             description = args.description,
             rarity = rarity or args.rarity or "common"
        }
     else
        -- Query Cargo for item data
        data = getItemData(itemName, rarity)
       
        if not data then
            return '<span class="error">Item not found: ' .. (itemName or 'nil') .. '</span>'
         end
     end
     end
      
      
     local row = self.root:tag('tr')
     local itemRarity = string.lower(data.rarity or "common")
     local th = row:tag('th')
     local rarityColor = ItemConfig.getRarityColor(itemRarity)
        :css('text-align', 'right')
    local rarityText = ItemConfig.getRarityLabel(itemRarity)
   
    local html = {}
      
      
     local iconData = icons[iconKey]
     -- Container with tooltip-panel styling
     if iconData then
     if float == "none" then
         th:wikitext(makeIcon(self.frame, iconData.icon, label))
         table.insert(html, '<div class="tooltip-panel font-apogea-long infobox">')
     else
     else
         th:wikitext(label)
         table.insert(html, string.format('<div class="tooltip-panel font-apogea-long infobox" style="float:%s; margin-%s:15px; margin-bottom:10px;">',
            float,
            float == "right" and "left" or "right"))
     end
     end
      
      
     row:tag('td')
     -- Sprite
        :css('text-align', 'left')
    local sprite = data.sprite or data.name
         :wikitext(value)
    table.insert(html, string.format('{{Sprite|%s|%s|class=pageimage}}', sprite, spriteSize))
     return self
   
end
    -- Item name with rarity color
 
    table.insert(html, string.format('<p class="font-bitcell color-%s">%s</p>', rarityColor, data.name))
function p:addStat(key, value)
   
     if not value or value == '' then
    -- Description (with bracketed text handling)
         return self
    local descLines = formatDescription(data.description)
    for _, line in ipairs(descLines) do
        table.insert(html, line)
    end
   
    -- Stats (using config labels)
    if data.rng and data.rng ~= "" then
        table.insert(html, formatStat("rng", data.rng))
    end
    if data.damage and data.damage ~= "" then
        table.insert(html, formatStat("damage", data.damage))
    end
    if data.attack_speed and data.attack_speed ~= "" then
        table.insert(html, formatStat("attack_speed", data.attack_speed))
    end
    if data.armor and data.armor ~= "" then
        table.insert(html, formatStat("armor", data.armor))
    end
    if data.defense and data.defense ~= "" then
        table.insert(html, formatStat("defense", data.defense))
    end
    if data.move_speed and data.move_speed ~= "" then
        table.insert(html, formatStat("move_speed", data.move_speed))
    end
    if data.health and data.health ~= "" then
        table.insert(html, formatStat("health", data.health))
    end
    if data.mana and data.mana ~= "" then
        table.insert(html, formatStat("mana", data.mana))
    end
    if data.ability and data.ability ~= "" then
        table.insert(html, formatStat("ability", data.ability))
    end
    if data.magic and data.magic ~= "" then
         table.insert(html, formatStat("magic", data.magic))
     end
    if data.health_regen and data.health_regen ~= "" then
        table.insert(html, formatStat("health_regen", data.health_regen))
    end
    if data.mana_regen and data.mana_regen ~= "" then
        table.insert(html, formatStat("mana_regen", data.mana_regen))
    end
     if data.container and data.container ~= "" then
        table.insert(html, formatStat("container", data.container))
    end
   
    -- Size (special format)
    if data.size and data.size ~= "" then
        table.insert(html, string.format('<p>Size: %s/10</p>', data.size))
    end
   
    -- Weight (special format)
    if data.weight and data.weight ~= "" then
         table.insert(html, string.format('<p>It weighs %s oz.</p>', data.weight))
     end
     end
      
      
     local stat = stats[key]
     -- Rarity text (skip for common)
     if not stat then
     if itemRarity ~= "common" and rarityText ~= "" then
         return self
         table.insert(html, formatLine(rarityText, "color-" .. rarityColor))
     end
     end
      
      
     local displayValue = stat.isBonus and formatBonus(value) or value
    -- Type display (show all types)
     local types = splitTypes(data.type)
    if #types > 0 then
        table.insert(html, formatLine("[" .. table.concat(types, ", ") .. "]", "color-silver"))
    end
      
      
     local row = self.root:tag('tr')
     table.insert(html, '</div>')
    local th = row:tag('th')
        :css('text-align', 'right')
      
      
     if stat.icon then
    -- Categories (skip in preview mode)
         th:wikitext(makeIcon(self.frame, stat.icon, stat.text))
    local categories = ''
    else
     if not previewMode then
         th:wikitext(stat.text)
         for _, t in ipairs(types) do
            categories = categories .. '[[Category:' .. ItemConfig.pluralize(t) .. ']]'
         end
     end
     end
      
      
     row:tag('td')
     return frame:preprocess(table.concat(html, '\n')) .. categories
        :css('text-align', 'left')
        :wikitext(displayValue)
    return self
end
end


function p:finish()
-- Monster infobox (queries from Cargo)
     return tostring(self.root)
function p.monster(frame)
    local args = frame:getParent().args
    local monsterName = args.name or args[1] or mw.title.getCurrentTitle().text
   
    -- Query Cargo for monster data
    local data = getMonsterData(monsterName)
   
    if not data then
        return '<span class="error">Monster not found: ' .. (monsterName or 'nil') .. '</span>'
    end
   
    local root = mw.html.create('table')
    root:addClass('wikitable')
        :addClass('infobox')
        :addClass('monster-infobox')
        :css('float', 'right')
        :css('clear', 'right')
        :css('margin', '0 0 1em 1em')
        :css('width', '250px')
   
    -- Header
    root:tag('tr')
        :tag('th')
            :attr('colspan', 2)
            :css('text-align', 'center')
            :wikitext(data.name or 'Unknown')
   
    -- Image
    local sprite = data.sprite or data.name
    local previewContent = frame:expandTemplate{
        title = 'MonsterPreview',
        args = { sprite, '128', mode = args.mode or 'auto', file = args.file or '', class = 'pageimage' }
    }
    root:tag('tr')
        :tag('td')
            :attr('colspan', 2)
            :css('text-align', 'center')
            :wikitext(previewContent)
   
    -- Stats using centralized config
    local function addStat(key, value)
        if not value or value == '' then return end
        local icon = ItemConfig.getStatIcon(key)
        local label = ItemConfig.getStatLabel(key)
        if not icon then return end
       
        local row = root:tag('tr')
        row:tag('th')
            :css('text-align', 'right')
            :css('width', '32px')
            :wikitext(makeIcon(frame, icon, label))
        row:tag('td')
            :css('text-align', 'left')
            :wikitext(value)
    end
   
    addStat('hp', data.hp)
    addStat('xp', data.xp)
    addStat('armor', data.armor)
    addStat('defense', data.defense)
    addStat('attack_speed', data.attack_speed)
    addStat('move_speed', data.move_speed)
    addStat('respawn', data.respawn)
   
    -- Category is handled by MonsterEntry template
     return tostring(root)
end
end


function p.item(frame)
-- NPC infobox (queries from Cargo)
function p.npc(frame)
     local args = frame:getParent().args
     local args = frame:getParent().args
    local npcName = args.name or args[1] or mw.title.getCurrentTitle().text
   
    -- Query Cargo for NPC data
    local data = getNPCData(npcName)
   
    if not data then
        return '<span class="error">NPC not found: ' .. (npcName or 'nil') .. '</span>'
    end
   
    local root = mw.html.create('table')
    root:addClass('wikitable')
        :addClass('infobox')
        :addClass('npc-infobox')
        :css('float', 'right')
        :css('clear', 'right')
        :css('margin', '0 0 1em 1em')
        :css('width', '250px')
      
      
     local box = p.new(frame)
     -- Header
         :addClass('item-infobox')
    root:tag('tr')
         :tag('th')
            :attr('colspan', 2)
            :css('text-align', 'center')
            :wikitext(data.name or 'Unknown')
      
      
     local sprite = args.sprite or args.name
    -- Image
     local sprite = data.sprite or data.name
     local spriteContent = frame:expandTemplate{
     local spriteContent = frame:expandTemplate{
         title = 'Sprite',
         title = 'Sprite',
         args = { sprite, '64', class = 'pageimage' }
         args = { sprite, '128', class = 'pageimage' }
     }
     }
    root:tag('tr')
        :tag('td')
            :attr('colspan', 2)
            :css('text-align', 'center')
            :wikitext(spriteContent)
      
      
     box:addHeader(args.name or 'Unknown')
     -- Helper to add a row
      :addImage(spriteContent)
    local function addRow(label, value)
      :addRow('Slot', args.slot)
        if not value or value == '' then return end
      :addRow('Type', args.category and string.format('[[:Category:%s|%s]]', args.category, args.category), 'type')
        local row = root:tag('tr')
      :addStat('wgt', args.wgt)
        row:tag('th')
      :addStat('rng', args.rng)
            :css('text-align', 'right')
      :addStat('dmg', args.dmg)
            :wikitext(label)
      :addStat('eq', args.eq)
        row:tag('td')
      :addStat('hp', args.hp)
            :css('text-align', 'left')
      :addStat('mp', args.mp)
            :wikitext(value)
      :addStat('ab', args.ab)
    end
      :addStat('mag', args.mag)
      :addStat('arm', args.arm)
      :addStat('def', args.def)
      :addStat('ms', args.ms)
      :addStat('as', args.as)
      :addStat('hpregen', args.hpregen)
      :addStat('mpregen', args.mpregen)
      
      
     local categories = ''
     -- Type
     if args.category then
    addRow('Type', data.type)
         categories = categories .. '[[Category:' .. args.category .. ']]'
   
    -- Trainer
     if data.trainer and data.trainer ~= '' then
         addRow('Trainer', data.trainer .. ' Trainer')
     end
     end
      
      
     return box:finish() .. categories
    -- Location
    addRow('Location', data.location)
   
    -- Category is handled by NPCEntry template
     return tostring(root)
end
end


return p
return p

Latest revision as of 20:58, 31 January 2026

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

local p = {}
local ItemConfig = require('Module:ItemConfig')

local function makeIcon(frame, icon, tooltip)
    local img = frame:preprocess('{{#spritescale:' .. icon .. '|stat}}')
    return '<span title="' .. mw.text.encode(tooltip) .. '">' .. img .. '</span>'
end

-- Format a stat line with positive/negative coloring
local function formatStat(statKey, value)
    if not value or value == "" then
        return nil
    end
    
    local label = ItemConfig.getStatLabel(statKey)
    local numValue = tonumber(value)
    if numValue then
        local colorClass, prefix
        if numValue >= 0 then
            colorClass = "color-conifer"
            prefix = "+"
        else
            colorClass = "color-coral"
            prefix = ""
        end
        return string.format('<p>%s: <span class="%s">%s%s</span></p>', label, colorClass, prefix, value)
    else
        return string.format('<p>%s: %s</p>', label, value)
    end
end

-- Format a plain text line
local function formatLine(text, colorClass)
    if not text or text == "" then
        return nil
    end
    if colorClass then
        return string.format('<p class="%s">%s</p>', colorClass, text)
    else
        return string.format('<p>%s</p>', text)
    end
end

-- Format description with bracketed text on separate lines and colored
local function formatDescription(desc)
    if not desc or desc == "" then
        return {}
    end
    
    local lines = {}
    local remaining = desc
    
    -- Pattern to find [bracketed text]
    while remaining and remaining ~= "" do
        local beforeBracket, bracketContent, afterBracket = remaining:match("^(.-)%[([^%]]+)%](.*)$")
        
        if bracketContent then
            -- Add any text before the bracket as jade-colored description
            if beforeBracket and beforeBracket:match("%S") then
                local trimmed = beforeBracket:match("^%s*(.-)%s*$")
                if trimmed ~= "" then
                    table.insert(lines, string.format('<p class="color-jade">%s</p>', trimmed))
                end
            end
            
            -- Add the bracketed text as columbia-colored (item-important)
            table.insert(lines, string.format('<p class="color-columbia">[%s]</p>', bracketContent))
            
            remaining = afterBracket
        else
            -- No more brackets, add remaining text as jade-colored
            local trimmed = remaining:match("^%s*(.-)%s*$")
            if trimmed ~= "" then
                table.insert(lines, string.format('<p class="color-jade">%s</p>', trimmed))
            end
            remaining = nil
        end
    end
    
    return lines
end

-- Split a comma-separated string into a table
local function splitTypes(typeStr)
    if not typeStr or typeStr == "" then
        return {}
    end
    local types = {}
    for t in typeStr:gmatch("[^,]+") do
        local trimmed = t:match("^%s*(.-)%s*$")
        if trimmed and trimmed ~= "" then
            table.insert(types, trimmed)
        end
    end
    return types
end

-- Query item data from Cargo
local function getItemData(name, rarity)
    local tables = 'Items'
    local fields = ItemConfig.cargoFields.items
    local args = {
        where = 'name="' .. name .. '"',
        limit = 1
    }
    
    if rarity and rarity ~= '' then
        args.where = args.where .. ' AND rarity="' .. rarity .. '"'
    end
    
    local result = mw.ext.cargo.query(tables, fields, args)
    
    if result and result[1] then
        return result[1]
    end
    return nil
end

-- Query monster data from Cargo
local function getMonsterData(name)
    local tables = 'Monsters'
    local fields = 'name,sprite,type,hp,xp,armor,defense,attack_speed,move_speed,respawn'
    local args = {
        where = 'name="' .. name .. '"',
        limit = 1
    }
    
    local result = mw.ext.cargo.query(tables, fields, args)
    
    if result and result[1] then
        return result[1]
    end
    return nil
end

-- Query NPC data from Cargo
local function getNPCData(name)
    local tables = 'NPCs'
    local fields = 'name,sprite,location,type,trainer'
    local args = {
        where = 'name="' .. name .. '"',
        limit = 1
    }
    
    local result = mw.ext.cargo.query(tables, fields, args)
    
    if result and result[1] then
        return result[1]
    end
    return nil
end

function p.item(frame)
    local args = frame:getParent().args
    local itemName = args.name or args[1] or mw.title.getCurrentTitle().text
    local rarity = args.rarity
    local float = args.float or "right"
    local spriteSize = args.spriteSize or "64"
    local previewMode = args.preview == "true" or args.preview == "1"
    
    local data
    
    -- If preview mode or direct data provided, use args instead of Cargo
    if previewMode or args.damage or args.armor or args.type then
        data = {
            name = itemName,
            sprite = args.sprite or itemName,
            slot = args.slot,
            type = args.type,
            weight = args.weight,
            rng = args.rng,
            damage = args.damage,
            health = args.health,
            mana = args.mana,
            ability = args.ability,
            magic = args.magic,
            armor = args.armor,
            defense = args.defense,
            move_speed = args.move_speed,
            attack_speed = args.attack_speed,
            size = args.size,
            container = args.container,
            health_regen = args.health_regen,
            mana_regen = args.mana_regen,
            description = args.description,
            rarity = rarity or args.rarity or "common"
        }
    else
        -- Query Cargo for item data
        data = getItemData(itemName, rarity)
        
        if not data then
            return '<span class="error">Item not found: ' .. (itemName or 'nil') .. '</span>'
        end
    end
    
    local itemRarity = string.lower(data.rarity or "common")
    local rarityColor = ItemConfig.getRarityColor(itemRarity)
    local rarityText = ItemConfig.getRarityLabel(itemRarity)
    
    local html = {}
    
    -- Container with tooltip-panel styling
    if float == "none" then
        table.insert(html, '<div class="tooltip-panel font-apogea-long infobox">')
    else
        table.insert(html, string.format('<div class="tooltip-panel font-apogea-long infobox" style="float:%s; margin-%s:15px; margin-bottom:10px;">', 
            float, 
            float == "right" and "left" or "right"))
    end
    
    -- Sprite
    local sprite = data.sprite or data.name
    table.insert(html, string.format('{{Sprite|%s|%s|class=pageimage}}', sprite, spriteSize))
    
    -- Item name with rarity color
    table.insert(html, string.format('<p class="font-bitcell color-%s">%s</p>', rarityColor, data.name))
    
    -- Description (with bracketed text handling)
    local descLines = formatDescription(data.description)
    for _, line in ipairs(descLines) do
        table.insert(html, line)
    end
    
    -- Stats (using config labels)
    if data.rng and data.rng ~= "" then
        table.insert(html, formatStat("rng", data.rng))
    end
    if data.damage and data.damage ~= "" then
        table.insert(html, formatStat("damage", data.damage))
    end
    if data.attack_speed and data.attack_speed ~= "" then
        table.insert(html, formatStat("attack_speed", data.attack_speed))
    end
    if data.armor and data.armor ~= "" then
        table.insert(html, formatStat("armor", data.armor))
    end
    if data.defense and data.defense ~= "" then
        table.insert(html, formatStat("defense", data.defense))
    end
    if data.move_speed and data.move_speed ~= "" then
        table.insert(html, formatStat("move_speed", data.move_speed))
    end
    if data.health and data.health ~= "" then
        table.insert(html, formatStat("health", data.health))
    end
    if data.mana and data.mana ~= "" then
        table.insert(html, formatStat("mana", data.mana))
    end
    if data.ability and data.ability ~= "" then
        table.insert(html, formatStat("ability", data.ability))
    end
    if data.magic and data.magic ~= "" then
        table.insert(html, formatStat("magic", data.magic))
    end
    if data.health_regen and data.health_regen ~= "" then
        table.insert(html, formatStat("health_regen", data.health_regen))
    end
    if data.mana_regen and data.mana_regen ~= "" then
        table.insert(html, formatStat("mana_regen", data.mana_regen))
    end
    if data.container and data.container ~= "" then
        table.insert(html, formatStat("container", data.container))
    end
    
    -- Size (special format)
    if data.size and data.size ~= "" then
        table.insert(html, string.format('<p>Size: %s/10</p>', data.size))
    end
    
    -- Weight (special format)
    if data.weight and data.weight ~= "" then
        table.insert(html, string.format('<p>It weighs %s oz.</p>', data.weight))
    end
    
    -- Rarity text (skip for common)
    if itemRarity ~= "common" and rarityText ~= "" then
        table.insert(html, formatLine(rarityText, "color-" .. rarityColor))
    end
    
    -- Type display (show all types)
    local types = splitTypes(data.type)
    if #types > 0 then
        table.insert(html, formatLine("[" .. table.concat(types, ", ") .. "]", "color-silver"))
    end
    
    table.insert(html, '</div>')
    
    -- Categories (skip in preview mode)
    local categories = ''
    if not previewMode then
        for _, t in ipairs(types) do
            categories = categories .. '[[Category:' .. ItemConfig.pluralize(t) .. ']]'
        end
    end
    
    return frame:preprocess(table.concat(html, '\n')) .. categories
end

-- Monster infobox (queries from Cargo)
function p.monster(frame)
    local args = frame:getParent().args
    local monsterName = args.name or args[1] or mw.title.getCurrentTitle().text
    
    -- Query Cargo for monster data
    local data = getMonsterData(monsterName)
    
    if not data then
        return '<span class="error">Monster not found: ' .. (monsterName or 'nil') .. '</span>'
    end
    
    local root = mw.html.create('table')
    root:addClass('wikitable')
        :addClass('infobox')
        :addClass('monster-infobox')
        :css('float', 'right')
        :css('clear', 'right')
        :css('margin', '0 0 1em 1em')
        :css('width', '250px')
    
    -- Header
    root:tag('tr')
        :tag('th')
            :attr('colspan', 2)
            :css('text-align', 'center')
            :wikitext(data.name or 'Unknown')
    
    -- Image
    local sprite = data.sprite or data.name
    local previewContent = frame:expandTemplate{
        title = 'MonsterPreview',
        args = { sprite, '128', mode = args.mode or 'auto', file = args.file or '', class = 'pageimage' }
    }
    root:tag('tr')
        :tag('td')
            :attr('colspan', 2)
            :css('text-align', 'center')
            :wikitext(previewContent)
    
    -- Stats using centralized config
    local function addStat(key, value)
        if not value or value == '' then return end
        local icon = ItemConfig.getStatIcon(key)
        local label = ItemConfig.getStatLabel(key)
        if not icon then return end
        
        local row = root:tag('tr')
        row:tag('th')
            :css('text-align', 'right')
            :css('width', '32px')
            :wikitext(makeIcon(frame, icon, label))
        row:tag('td')
            :css('text-align', 'left')
            :wikitext(value)
    end
    
    addStat('hp', data.hp)
    addStat('xp', data.xp)
    addStat('armor', data.armor)
    addStat('defense', data.defense)
    addStat('attack_speed', data.attack_speed)
    addStat('move_speed', data.move_speed)
    addStat('respawn', data.respawn)
    
    -- Category is handled by MonsterEntry template
    return tostring(root)
end

-- NPC infobox (queries from Cargo)
function p.npc(frame)
    local args = frame:getParent().args
    local npcName = args.name or args[1] or mw.title.getCurrentTitle().text
    
    -- Query Cargo for NPC data
    local data = getNPCData(npcName)
    
    if not data then
        return '<span class="error">NPC not found: ' .. (npcName or 'nil') .. '</span>'
    end
    
    local root = mw.html.create('table')
    root:addClass('wikitable')
        :addClass('infobox')
        :addClass('npc-infobox')
        :css('float', 'right')
        :css('clear', 'right')
        :css('margin', '0 0 1em 1em')
        :css('width', '250px')
    
    -- Header
    root:tag('tr')
        :tag('th')
            :attr('colspan', 2)
            :css('text-align', 'center')
            :wikitext(data.name or 'Unknown')
    
    -- Image
    local sprite = data.sprite or data.name
    local spriteContent = frame:expandTemplate{
        title = 'Sprite',
        args = { sprite, '128', class = 'pageimage' }
    }
    root:tag('tr')
        :tag('td')
            :attr('colspan', 2)
            :css('text-align', 'center')
            :wikitext(spriteContent)
    
    -- Helper to add a row
    local function addRow(label, value)
        if not value or value == '' then return end
        local row = root:tag('tr')
        row:tag('th')
            :css('text-align', 'right')
            :wikitext(label)
        row:tag('td')
            :css('text-align', 'left')
            :wikitext(value)
    end
    
    -- Type
    addRow('Type', data.type)
    
    -- Trainer
    if data.trainer and data.trainer ~= '' then
        addRow('Trainer', data.trainer .. ' Trainer')
    end
    
    -- Location
    addRow('Location', data.location)
    
    -- Category is handled by NPCEntry template
    return tostring(root)
end

return p