Module:Infobox item: Difference between revisions

m Monster Domosed moved page Module:Infobox Item to Module:Infobox item: Misspelled title
append_label: prefer per-stat StatLinks target over generic attribute-type link (fixes e.g. Debuff Resist → Ability Duration) (with help from vergir-bot LLM)
 
(14 intermediate revisions by 4 users not shown)
Line 4: Line 4:
local data = mw.loadJsonData("Data:ItemCards.json")
local data = mw.loadJsonData("Data:ItemCards.json")
local stat_links = mw.loadJsonData("Data:StatLinks.json")
local stat_links = mw.loadJsonData("Data:StatLinks.json")
local ATTR_TYPE_ICON_MAP = require("Module:Icon/attr")


-- Local references for hot-path globals
-- Local references for hot-path globals
Line 14: Line 15:
local tostring = tostring
local tostring = tostring
local type = type
local type = type
-- Status Effect Icon Map
local STATUS_EFFECT_ICON_MAP = {
    StatusEffectDisarmed = {img='Status_Disarm.png',        link='Disarm',  color='Red'},
    StatusEffectEMP      = {img='Status_Silence.png',      link='Silence',  color='Purple'},
    StatusEffectInvisible= {img='Invisible.png',            link='Invisible',color='Green'},
    StatusEffectStun    = {img='Status_Stun.png',          link='Stun',    color='NoColor'},
    StatusEffectSlow    = {img='Status_Movement_Slow.png', link='Slow',    color='Purple'},
}


-- Pre-computed constants
-- Pre-computed constants
Line 19: Line 29:
     .. " [[{{#invoke:Lang|get_string|Citadel_StatusEffect}}]]"
     .. " [[{{#invoke:Lang|get_string|Citadel_StatusEffect}}]]"


-- Spirit damage constants
-- Property keys to completely ignore during processing
local SPIRIT_ICON = "[[File:Spirit_damage.png|12px]]"
local IGNORED_PROP_KEYS = {
local SPIRIT_COLOR = "#bc8ee8"
    BuildUpPerShot = true,
local SPIRIT_LINK = "Spirit Damage"
}


-- Label key remapping table (replaces if/elseif chain)
-- Label key remapping table (replaces if/elseif chain)
local LABEL_KEY_REMAP = {
local LABEL_KEY_REMAP = {
    BuildUpPerShot = "BuildupPerShot",
     DotDuration    = "DOTDuration",
     DotDuration    = "DOTDuration",
}
-- Item name override mapping (old name -> new name)
local ITEM_NAME_OVERRIDES = {
    ["debuff remover"] = "Dispel Magic",
    ["backstabber"] = "Stalker",
}
}


Line 54: Line 69:


local conditional_label -- lazily initialised
local conditional_label -- lazily initialised
--------------------------------------------------------------------------------
-- Item name resolution
--------------------------------------------------------------------------------
local function resolve_item_name(name)
    -- Check if there's an override for this name (case-insensitive)
    local lowercase_name = name:lower()
    local override = ITEM_NAME_OVERRIDES[lowercase_name]
    if override then
        return override
    end
   
    -- If no override, try to find the item in data and return its proper Name
    -- First try direct key lookup
    if data[name] and not data[name].IsDisabled then
        return data[name].Name or name
    end
   
    -- Then try case-insensitive search by Name field
    local lowercase_search = name:lower()
    for _, v in pairs(data) do
        if v.Name and v.Name:lower() == lowercase_search and not v.IsDisabled then
            return v.Name
        end
    end
   
    -- Fallback to original name
    return name
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Line 72: Line 117:


local function get_json_item(name)
local function get_json_item(name)
     local direct = data[name]
    -- Apply name override before lookup
    local resolved_name = resolve_item_name(name)
   
     local direct = data[resolved_name]
     if direct and not direct.IsDisabled then return direct end
     if direct and not direct.IsDisabled then return direct end


     local fallback
     local fallback
     for _, v in pairs(data) do
     for _, v in pairs(data) do
         if v.Name == name then
         if v.Name == resolved_name then
             if not v.IsDisabled then return v end
             if not v.IsDisabled then return v end
             fallback = fallback or v
             fallback = fallback or v
Line 129: Line 177:


local function get_component_of_sorted(item_input)
local function get_component_of_sorted(item_input)
    -- Apply name override before processing
    local resolved_input = resolve_item_name(item_input)
   
     local target_key
     local target_key
     if data[item_input] then
     if data[resolved_input] then
         target_key = item_input
         target_key = resolved_input
     else
     else
         for k, v in pairs(data) do
         for k, v in pairs(data) do
             if v.Name == item_input and not v.IsDisabled then
             if v.Name == resolved_input and not v.IsDisabled then
                 target_key = k
                 target_key = k
                 break
                 break
Line 182: Line 233:
end
end


local function append_label(val_str, prop_key, label, label_color, link_override)
local function append_label(val_str, prop_key, label, attr_link, link_override, is_main)
     if label == "" then return val_str end
     if label == "" then return val_str end
     local display_label
 
     local link_page = link_override or stat_links[prop_key]
     -- Resolve link priority: Override > JSON Stat Link > Attribute Map Link
     if link_page then
     local link_target = link_override
         if link_page == label then
    if not link_target or link_target == "" then
        link_target = stat_links[prop_key]
     end
    if not link_target or link_target == "" then
        link_target = attr_link
    end
 
    -- Construct label with link if valid
    local display_label = label
    if link_target and link_target ~= "" then
         if link_target == label then
             display_label = "[[" .. label .. "]]"
             display_label = "[[" .. label .. "]]"
         else
         else
             display_label = "[[" .. link_page .. "|" .. label .. "]]"
             display_label = "[[" .. link_target .. "|" .. label .. "]]"
         end
         end
    end
    -- Place label on a new line only for Main stats
    if is_main then
        return val_str .. "<br>" .. display_label
     else
     else
         display_label = label
         return val_str .. " " .. display_label
    end
    if label_color then
        display_label = '<span style="color:' .. label_color .. '">'
            .. display_label .. '</span>'
     end
     end
    return val_str .. " " .. display_label
end
end


Line 210: Line 271:
     if not conditional_label then
     if not conditional_label then
         conditional_label = format(
         conditional_label = format(
             ' <span style="color:#C0C0C0">(%s)</span>',
             '<span style="color:#C0C0C0">%s</span>',
             lang.get_string("Citadel_Shop_ConditionalAttribute")
             lang.get_string("Citadel_Shop_ConditionalAttribute")
         )
         )
     end
     end
     return val_str .. conditional_label
    -- Place conditional text on a new line
     return val_str .. "<br>" .. conditional_label
end
end


Line 224: Line 286:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Spirit damage detection helper
-- Attribute Icon/Style detection helper
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


local function is_spirit_damage(prop_key, css_type)
local function resolve_attr_style(css_type)
     return prop_key == "Damage" and css_type == "tech_damage"
    if not css_type then return nil end
    local attr = ATTR_TYPE_ICON_MAP[css_type]
    if not attr then return nil end
 
    local color = attr.color or "Grey" -- Fallback color
    local img = attr.img or "GenericProperty.png"
    local link = attr.link
    local size = attr.size -- Optional
 
    -- Construct icon using the defined color template: {{Icon/Color|img|size|link=link}}
    local icon_str
    if size and size ~= "" then
        icon_str = format("{{Icon/%s|[[File:%s|%spx|link=%s]]}}", color, img, size, link or "")
    else
        icon_str = format("{{Icon/%s|[[File:%s|%spx|link=%s]]}}", color, img, 18, link or "")
    end
 
     return {
        icon = icon_str,
        link = link
    }
end
 
--------------------------------------------------------------------------------
-- Status Effect Icon helper
--------------------------------------------------------------------------------
 
local function resolve_status_effect_icon(prop_key)
    local effect_config = STATUS_EFFECT_ICON_MAP[prop_key]
    if not effect_config then return nil end
 
    local color = effect_config.color or "NoColor"
    local img = effect_config.img or "GenericProperty.png"
    local link = effect_config.link
 
    -- Construct icon using the defined color template
    local icon_str = format(
        "{{Icon/%s|[[File:%s|18px|link=%s]]}}",
        color, img, link or ""
    )
 
    return {
        icon = icon_str,
        link = link
    }
end
end


Line 236: Line 342:


local function format_value(prop_key, value, css_type, loc_override,
local function format_value(prop_key, value, css_type, loc_override,
                             skip_label, allow_conditional, usage_flags, scale_info)
                             skip_label, allow_conditional, usage_flags, scale_info,
                            disable_attr, is_main)
     local numeric_val = extract_numeric(value)
     local numeric_val = extract_numeric(value)
     if not numeric_val then return nil end
     if not numeric_val then return nil end
Line 267: Line 374:
     end
     end


     -- Spirit damage: prepend icon before the value
     -- Attribute styling (Icon via template) based on css_type
     local spirit = is_spirit_damage(prop_key, css_type)
     local attr_style
     if spirit then
    if not disable_attr then
         val_str = SPIRIT_ICON .. " " .. val_str
        attr_style = resolve_attr_style(css_type)
    end
   
     if attr_style and attr_style.icon then
         val_str = attr_style.icon .. " " .. val_str
     end
     end


     val_str = append_label(val_str, label_key, label,
     val_str = append_label(val_str, label_key, label,
         spirit and SPIRIT_COLOR or nil,
         attr_style and attr_style.link or nil,
         spirit and SPIRIT_LINK or nil)
         nil, is_main) -- link_override not used here in base calls
 
     return append_conditional(val_str, allow_conditional, usage_flags)
     return append_conditional(val_str, allow_conditional, usage_flags)
end
end
Line 285: Line 397:
local function format_value_enhanced(item_name, prop_key, value, css_type,
local function format_value_enhanced(item_name, prop_key, value, css_type,
                                     loc_override, skip_label,
                                     loc_override, skip_label,
                                     allow_conditional, usage_flags)
                                     allow_conditional, usage_flags,
                                    disable_attr, is_main)
     local numeric_val = extract_numeric(value)
     local numeric_val = extract_numeric(value)
     if not numeric_val then return nil end
     if not numeric_val then return nil end
Line 327: Line 440:
     end
     end


     -- Spirit damage: prepend icon before the value
     -- Attribute styling (Icon via template) based on css_type
     local spirit = is_spirit_damage(prop_key, css_type)
     local attr_style
     if spirit then
    if not disable_attr then
         val_str = SPIRIT_ICON .. " " .. val_str
        attr_style = resolve_attr_style(css_type)
    end
 
     if attr_style and attr_style.icon then
         val_str = attr_style.icon .. " " .. val_str
     end
     end


     val_str = append_label(val_str, label_key, label,
     val_str = append_label(val_str, label_key, label,
         spirit and SPIRIT_COLOR or nil,
         attr_style and attr_style.link or nil,
         spirit and SPIRIT_LINK or nil)
         nil, is_main)
 
     return append_conditional(val_str, allow_conditional, usage_flags)
     return append_conditional(val_str, allow_conditional, usage_flags)
end
end
--------------------------------------------------------------------------------
-- Status Effect formatting helper
--------------------------------------------------------------------------------
local function format_status_effect(prop_key)
    local lang_str = format("[[{{#invoke:Lang|get_string|Citadel_%s}}]]", prop_key)
        .. " [[{{#invoke:Lang|get_string|Citadel_StatusEffect}}]]"
   
    -- Get icon for this status effect
    local icon_style = resolve_status_effect_icon(prop_key)
    if icon_style and icon_style.icon then
        return icon_style.icon .. " " .. lang_str
    end
   
    return lang_str
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Tooltip builder helpers
-- Tooltip builder helpers
Line 358: Line 494:
end
end


local function process_stats(lines, fmt, counter, start_index, prop_list,
local function process_stat_list(lines, param_fmt, counter, start_index,
                            item_name, seen, formatter)
                                prop_list, seen, formatter)
     local stat_index = start_index
     local stat_index = start_index
     for _, prop_obj in ipairs(prop_list or {}) do
     for _, prop_obj in ipairs(prop_list or {}) do
         local key = prop_obj.Key
         local key = prop_obj.Key
         if not seen[key] and (
         if not seen[key] and not IGNORED_PROP_KEYS[key] and (
             key:match("^StatusEffect")
             key:match("^StatusEffect")
             or is_property_in_lang_data(key, prop_obj.LocTokenOverride)
             or is_property_in_lang_data(key, prop_obj.LocTokenOverride)
Line 369: Line 505:
             local val = formatter(prop_obj)
             local val = formatter(prop_obj)
             if val then
             if val then
                 insert(lines, format(fmt, counter, stat_index, val))
                 insert(lines, format(param_fmt, counter, stat_index, val))
                 stat_index = stat_index + 1
                 stat_index = stat_index + 1
                 seen[key] = true
                 seen[key] = true
Line 377: Line 513:
     return stat_index
     return stat_index
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Unified tooltip builder (handles both default and enhanced modes)
-- Unified tooltip builder (handles both default and enhanced modes)
Line 383: Line 520:
     local item_name = frame.args[1]
     local item_name = frame.args[1]
     if not item_name then return "Item name missing" end
     if not item_name then return "Item name missing" end
    -- Resolve the item name (apply override if exists)
    local resolved_name = resolve_item_name(item_name)


     local item = get_json_item(item_name)
     local item = get_json_item(item_name)
Line 393: Line 533:
         insert(lines, "| enhanced = true")
         insert(lines, "| enhanced = true")
     end
     end
     insert(lines, "| item_name = " .. item_name)
    -- Use resolved name for item_name parameter
     insert(lines, "| item_name = " .. resolved_name)


     if item.StreetBrawl == true then
     if item.StreetBrawl == true then
         insert(lines, "| street_brawl = true")
         insert(lines, "| street_brawl = true")
    end
   
    -- Pass through nocat arg to the Infobox item template if provided
    local nocat = frame.args.nocat
    if nocat and nocat ~= "" then
        insert(lines, "| nocat = " .. nocat)
     end
     end


Line 402: Line 549:


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Innate stats
     -- Innate stats (Main and Alt merged into item_stat sequentially)
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local innate_index = 1
     local innate_index = 1
Line 408: Line 555:
         if section.Type == "Innate" then
         if section.Type == "Innate" then
             for _, obj in ipairs(section.Main or {}) do
             for _, obj in ipairs(section.Main or {}) do
 
                 if not IGNORED_PROP_KEYS[obj.Key] and is_property_in_lang_data(obj.Key, obj.LocTokenOverride) then
                 if is_property_in_lang_data(obj.Key, obj.LocTokenOverride) then
                     local val
                     local val
                     if enhanced then
                     if enhanced then
                         val = format_value_enhanced(item_name, obj.Key, obj.Value, obj.Type, obj.LocTokenOverride, nil, false, obj.UsageFlags)
                         val = format_value_enhanced(
                            resolved_name, obj.Key, obj.Value, obj.Type,  
                            obj.LocTokenOverride, nil, false, obj.UsageFlags,
                            true -- disable_attr
                        )
                     else
                     else
                         val = format_value(obj.Key, obj.Value, obj.Type, obj.LocTokenOverride, false, false, obj.UsageFlags, obj.Scale)
                         val = format_value(
                            obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,  
                            false, false, obj.UsageFlags, obj.Scale,
                            true -- disable_attr
                        )
                     end
                     end
                     if val then
                     if val then
Line 423: Line 577:
             end
             end
             for _, obj in ipairs(section.Alt or {}) do
             for _, obj in ipairs(section.Alt or {}) do
 
                 if not IGNORED_PROP_KEYS[obj.Key] and is_property_in_lang_data(obj.Key, obj.LocTokenOverride) then
                 if is_property_in_lang_data(obj.Key, obj.LocTokenOverride) then
                     local val
                     local val
                     if enhanced then
                     if enhanced then
                         val = format_value_enhanced(
                         val = format_value_enhanced(
                             item_name, obj.Key, obj.Value, obj.Type,
                             resolved_name, obj.Key, obj.Value, obj.Type,
                             obj.LocTokenOverride, nil, false, obj.UsageFlags
                             obj.LocTokenOverride, nil, false, obj.UsageFlags,
                            true -- disable_attr
                         )
                         )
                     else
                     else
                         val = format_value(
                         val = format_value(
                             obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
                             obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
                             false, false, obj.UsageFlags, obj.Scale
                             false, false, obj.UsageFlags, obj.Scale,
                            true -- disable_attr
                         )
                         )
                     end
                     end
Line 447: Line 602:


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Build formatters once (closures capture item_name)
     -- Build formatters once (closures capture resolved_name)
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local main_formatter, alt_formatter
     local main_formatter, alt_formatter
Line 454: Line 609:
         main_formatter = function(obj)
         main_formatter = function(obj)
             if obj.Key:match("^StatusEffect") then
             if obj.Key:match("^StatusEffect") then
                 return format(STATUS_FMT, obj.Key)
                 return format_status_effect(obj.Key)
             end
             end
             return format_value_enhanced(
             return format_value_enhanced(
                 item_name, obj.Key, obj.Value, obj.Type,
                 resolved_name, obj.Key, obj.Value, obj.Type,
                 obj.LocTokenOverride, false, true, obj.UsageFlags
                 obj.LocTokenOverride, false, true, obj.UsageFlags, nil, true
             )
             )
         end
         end
         alt_formatter = function(obj)
         alt_formatter = function(obj)
 
            if obj.Key:match("^StatusEffect") then
                return format_status_effect(obj.Key)
            end
             return format_value_enhanced(
             return format_value_enhanced(
                 item_name, obj.Key, obj.Value, obj.Type,
                 resolved_name, obj.Key, obj.Value, obj.Type,
                 obj.LocTokenOverride, nil, false, obj.UsageFlags
                 obj.LocTokenOverride, nil, false, obj.UsageFlags,
                true -- disable_attr
             )
             )
         end
         end
     else
     else
main_formatter = function(obj)
        main_formatter = function(obj)
    if obj.Key:match("^StatusEffect") then
            if obj.Key:match("^StatusEffect") then
        return format(STATUS_FMT, obj.Key)
                return format_status_effect(obj.Key)
    end
            end
 
            return format_value(
    return format_value(
                obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
        obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
                false, true, obj.UsageFlags, obj.Scale, nil, true
        false, true, obj.UsageFlags, obj.Scale
            )
    )
        end
end
         alt_formatter = function(obj)
         alt_formatter = function(obj)
 
            if obj.Key:match("^StatusEffect") then
                return format_status_effect(obj.Key)
            end
             return format_value(
             return format_value(
                 obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
                 obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
                 false, false, obj.UsageFlags, obj.Scale
                 false, false, obj.UsageFlags, obj.Scale,
                true -- disable_attr
             )
             )
         end
         end
Line 490: Line 649:


     ------------------------------------------------------------------
     ------------------------------------------------------------------
     -- Passive & Active stats (unified loop)
     -- Passive & Active sections — Main and Alt output separately
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     local passive_counter, active_counter = 1, 1
     local passive_counter, active_counter = 1, 1
Line 500: Line 659:
             local prefix = is_active and "active" or "passive"
             local prefix = is_active and "active" or "passive"
             local counter = is_active and active_counter or passive_counter
             local counter = is_active and active_counter or passive_counter
            local stat_fmt = "| " .. prefix .. "%d_stat%d = %s"


             -- Description
             -- Description
Line 509: Line 667:
                     insert(lines, format(
                     insert(lines, format(
                         "| %s%d_description = {{#invoke:Lang|get_string|%s|item_name=%s}}",
                         "| %s%d_description = {{#invoke:Lang|get_string|%s|item_name=%s}}",
                         prefix, counter, loc, item_name
                         prefix, counter, loc, resolved_name
                     ))
                     ))
                 end
                 end
             end
             end


             -- Stats
             -- Main stats → prefix{N}_main{M}
             local seen = {}
             local main_fmt = "| " .. prefix .. "%d_main%d = %s"
             local stat_index = process_stats(
            local main_seen = {}
                 lines, stat_fmt, counter, 1,
             local main_index = 1
                 section.Main, item_name, seen, main_formatter
            for _, prop_obj in ipairs(section.Main or {}) do
            )
                local key = prop_obj.Key
            process_stats(
                 if not main_seen[key] and not IGNORED_PROP_KEYS[key] and (
                lines, stat_fmt, counter, stat_index,
                    key:match("^StatusEffect")
                 section.Alt, item_name, seen, alt_formatter
                    or is_property_in_lang_data(key, prop_obj.LocTokenOverride)
             )
                ) then
                    local val = main_formatter(prop_obj)
                    if val then
                        insert(lines, format(main_fmt, counter, main_index, val))
                        main_index = main_index + 1
                        main_seen[key] = true
                    end
                end
            end
 
            -- Alt stats → prefix{N}_alt{M}
            local alt_fmt = "| " .. prefix .. "%d_alt%d = %s"
            local alt_seen = {}
            local alt_index = 1
            for _, prop_obj in ipairs(section.Alt or {}) do
                 local key = prop_obj.Key
                -- Skip keys already emitted as main stats
                if not main_seen[key] and not alt_seen[key] and not IGNORED_PROP_KEYS[key] and (
                    key:match("^StatusEffect")
                    or is_property_in_lang_data(key, prop_obj.LocTokenOverride)
                ) then
                    local val = alt_formatter(prop_obj)
                    if val then
                        insert(lines, format(alt_fmt, counter, alt_index, val))
                        alt_index = alt_index + 1
                        alt_seen[key] = true
                    end
                 end
             end


             -- Cooldown / ChargeUp
             -- Cooldown / ChargeUp
             local cd = format_section_cooldown(section, item_name, enhanced)
             local cd = format_section_cooldown(section, resolved_name, enhanced)
             if cd.cooldown then
             if cd.cooldown then
                 insert(lines, format("| %s%d_cooldown = %s", prefix, counter, cd.cooldown))
                 insert(lines, format("| %s%d_cooldown = %s", prefix, counter, cd.cooldown))
Line 546: Line 732:
     ------------------------------------------------------------------
     ------------------------------------------------------------------
     for i = 1, 2 do
     for i = 1, 2 do
         local comp = ItemData.get_component_name { args = { item_name, i } }
         local comp = ItemData.get_component_name { args = { resolved_name, i } }
         if comp and comp ~= "" then
         if comp and comp ~= "" then
             insert(lines, format("| component%d_name = %s", i, comp))
             insert(lines, format("| component%d_name = %s", i, comp))
Line 577: Line 763:
     return concat(lines, "\n")
     return concat(lines, "\n")
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Public interface
-- Public interface