Module:Icon: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
LVL (talk | contribs)
Fix expensive parser function limit errors by removing title.exists check, now uses direct linking for localized pages
LVL (talk | contribs)
Testing fix for many icons on one page
Line 4: Line 4:
local lang_codes = mw.loadJsonData("Data:LangCodes.json")
local lang_codes = mw.loadJsonData("Data:LangCodes.json")


-- Cache for compiled icon data (persists during page render)
-- Cache for compiled icon data, split by type (persists during page render)
local iconDataCache = nil
local heroCacheBuilt = false
local abilityCacheBuilt = false
local itemCacheBuilt = false
local iconDataCache = {}
 
local cachedLangCode, cachedLangData
local cachedLangCode, cachedLangData


Line 25: Line 29:
}
}


-- Build unified lookup table from bot-uploaded JSONs
-- Build lookup table from bot-uploaded JSONs, only loading the relevant JSON per type
local function buildIconData()
-- to reduce CPU cost on pages with many icon calls
    if iconDataCache then
local function buildIconData(iconType)
        return iconDataCache
     -- Only load hero data if not already built
    end
     if iconType == "hero" and not heroCacheBuilt then
   
        local heroData = mw.loadJsonData("Data:HeroData.json")
    local lookup = {}
 
   
        -- Process Heroes
     -- Load bot data
        for heroKey, hero in pairs(heroData) do
     local heroData = mw.loadJsonData("Data:HeroData.json")
            if type(hero) == "table" and hero.Name then
    local abilityData = mw.loadJsonData("Data:AbilityData.json")
                local name = hero.Name
    local itemData = mw.loadJsonData("Data:ItemData.json")
                local lowerName = name:lower()
   
 
    -- Track which hero owns which ability (for linking)
                -- Check for missing image override
    local abilityToHero = {}
                local imageName = missingImageOverrides[lowerName] or (name .. ".png")
   
 
    -- Process Heroes
                iconDataCache[lowerName] = {
    for heroKey, hero in pairs(heroData) do
                    name = name,
        if type(hero) == "table" and hero.Name then
                    key = heroKey,
            local name = hero.Name
                    link = name,
            local lowerName = name:lower()
                    image = imageName,
           
                    type = "hero"
            -- Check for missing image override
                }
            local imageName = missingImageOverrides[lowerName] or (name .. ".png")
             end
           
        end
            lookup[lowerName] = {
 
                name = name,
        heroCacheBuilt = true
                key = heroKey,
 
                link = name,
    -- Only load ability data (and hero data for parent linking) if not already built
                image = imageName,
    elseif iconType == "ability" and not abilityCacheBuilt then
                type = "hero"
        local heroData = mw.loadJsonData("Data:HeroData.json")
            }
        local abilityData = mw.loadJsonData("Data:AbilityData.json")
              
 
            -- Map abilities to parent hero
        -- Track which hero owns which ability (for linking)
             if hero.BoundAbilities then
        local abilityToHero = {}
 
        for heroKey, hero in pairs(heroData) do
             if type(hero) == "table" and hero.BoundAbilities then
                 for slot, ability in pairs(hero.BoundAbilities) do
                 for slot, ability in pairs(hero.BoundAbilities) do
                     if ability.Key then
                     if ability.Key then
                         abilityToHero[ability.Key] = {
                         abilityToHero[ability.Key] = {
                             heroName = name,
                             heroName = hero.Name,
                             abilityName = ability.Name
                             abilityName = ability.Name
                         }
                         }
Line 70: Line 77:
             end
             end
         end
         end
    end
 
   
        -- Process Abilities
-- Process Abilities
        for abilityKey, ability in pairs(abilityData) do
    for abilityKey, ability in pairs(abilityData) do
            if type(ability) == "table" and ability.Name then
        if type(ability) == "table" and ability.Name then
                local name = ability.Name
            local name = ability.Name
                local lowerName = name:lower()
            local lowerName = name:lower()
                local parentInfo = abilityToHero[abilityKey]
            local parentInfo = abilityToHero[abilityKey]
 
           
                -- If we already have this ability name, prefer the one linked to a hero
            -- If we already have this ability name, prefer the one linked to a hero
                if iconDataCache[lowerName] and iconDataCache[lowerName].type == "ability" then
            if lookup[lowerName] and lookup[lowerName].type == "ability" then
                    local existingHasParent = iconDataCache[lowerName].link ~= iconDataCache[lowerName].name
                local existingHasParent = lookup[lowerName].link ~= lookup[lowerName].name
                    local newHasParent = parentInfo ~= nil
                local newHasParent = parentInfo ~= nil
 
               
                    -- Skip if existing is linked to hero but this one isn't (prevents orphans from overwriting real ones)
                -- Skip if existing is linked to hero but this one isn't (prevents orphans from overwriting real ones)
                    if existingHasParent and not newHasParent then
                if existingHasParent and not newHasParent then
                        -- Skip this orphan ability
                    -- Skip this orphan ability
                    else
                        -- Overwrite: new has parent and existing doesn't, or both/neither have parents
                        local link = parentInfo and parentInfo.heroName or name
                        iconDataCache[lowerName] = {
                            name = name,
                            key = abilityKey,
                            link = link,
                            image = name .. ".png",
                            type = "ability",
                            class = "theme"
                        }
                    end
                 else
                 else
                     -- Overwrite: new has parent and existing doesn't, or both/neither have parents
                     -- No existing entry, add normally
                     local link = parentInfo and parentInfo.heroName or name
                     local link = parentInfo and parentInfo.heroName or name
                     lookup[lowerName] = {
                     iconDataCache[lowerName] = {
                         name = name,
                         name = name,
                         key = abilityKey,
                         key = abilityKey,
Line 99: Line 117:
                     }
                     }
                 end
                 end
             else
             end
                -- No existing entry, add normally
        end
                 local link = parentInfo and parentInfo.heroName or name
 
                 lookup[lowerName] = {
        abilityCacheBuilt = true
 
    -- Only load item data if not already built
    elseif iconType == "item" and not itemCacheBuilt then
        local itemData = mw.loadJsonData("Data:ItemData.json")
 
        -- Process Items
        for itemKey, item in pairs(itemData) do
            if type(item) == "table" and item.Name then
                 local name = item.Name
                local lowerName = name:lower()
 
                 iconDataCache[lowerName] = {
                     name = name,
                     name = name,
                     key = abilityKey,
                     key = itemKey,
                     link = link,
                     link = name,
                     image = name .. ".png",
                     image = name .. ".png",
                     type = "ability",
                     type = "item"
                    class = "theme"
                 }
                 }
             end
             end
         end
         end
        itemCacheBuilt = true
     end
     end
   
    -- Process Items
    for itemKey, item in pairs(itemData) do
        if type(item) == "table" and item.Name then
            local name = item.Name
            local lowerName = name:lower()
           
            lookup[lowerName] = {
                name = name,
                key = itemKey,
                link = name,
                image = name .. ".png",
                type = "item"
            }
        end
    end
   
    iconDataCache = lookup
    return lookup
end
end


-- Get icon data by name (with alias support)
-- Get icon data by name and type (with alias support)
local function getIconData(iconName)
local function getIconData(iconName, iconType)
     local data = buildIconData()
     buildIconData(iconType)
     local lowerName = iconName:lower()
     local lowerName = iconName:lower()
   
 
     -- Check main lookup
     -- Check main lookup
     if data[lowerName] then
     if iconDataCache[lowerName] then
         return data[lowerName], nil
         return iconDataCache[lowerName], nil
     end
     end
   
 
     -- Check aliases (for edge cases like "doorman" -> "the doorman")
     -- Check aliases (for edge cases like "doorman" -> "the doorman")
     local aliases = {
     local aliases = {
Line 153: Line 165:
         ["debuff remover"] = "dispel magic"
         ["debuff remover"] = "dispel magic"
     }
     }
   
 
     if aliases[lowerName] then
     if aliases[lowerName] then
         return data[aliases[lowerName]], nil
         return iconDataCache[aliases[lowerName]], nil
     end
     end
   
 
     return nil, string.format("[[:Category:Module:Icon ERROR|Icon not found]] ('%s'). [[Category:Module:Icon ERROR]]", iconName)
     return nil, string.format("[[:Category:Module:Icon ERROR|Icon not found]] ('%s'). [[Category:Module:Icon ERROR]]", iconName)
end
end
Line 192: Line 204:
     local name = args[1] or ""
     local name = args[1] or ""
     local customText = args.l1 or ""
     local customText = args.l1 or ""
      
     local iconType = args.type or "item"
 
     -- Set Defaults
     -- Set Defaults
     local size = args.size or "20px"
     local size = args.size or "20px"
Line 202: Line 215:
         if type(k) == "number" and k > 1 then
         if type(k) == "number" and k > 1 then
             local val = mw.text.trim(v)
             local val = mw.text.trim(v)
           
 
             if val == "icon-only" then
             if val == "icon-only" then
                 iconOnly = true
                 iconOnly = true
             elseif val == "no-link" then
             elseif val == "no-link" then
                 noLink = true
                 noLink = true
             elseif val:match("^%d+px$") then  
             elseif val:match("^%d+px$") then
                 size = val
                 size = val
             end
             end
Line 213: Line 226:
     end
     end


     -- Get icon data automatically from JSON
     -- Get icon data automatically from JSON, using type to limit which JSON is loaded
     local iconData, err = getIconData(name)
     local iconData, err = getIconData(name, iconType)
     if not iconData then
     if not iconData then
         return "Error: " .. err
         return "Error: " .. err
Line 220: Line 233:


     local langCode, langData = getLangData()
     local langCode, langData = getLangData()
   
 
     -- Determine display name
     -- Determine display name
     local displayName = ""
     local displayName = ""
Line 234: Line 247:
     local baseLink = iconData.link or ""
     local baseLink = iconData.link or ""
     local link = baseLink
     local link = baseLink
   
 
     if langCode ~= "en" and baseLink ~= "" then
     if langCode ~= "en" and baseLink ~= "" then
         link = baseLink .. "/" .. langCode
         link = baseLink .. "/" .. langCode

Revision as of 22:49, 6 March 2026

Main module for rendering game icons.

Note: This module is not intended for direct use. Use the templates below.

Data sources

The module reads from Data:ResourceLookup.json, a pre-indexed lookup table generated by Deadbot at deploy time. When the bot uploads new game files, hero, ability, and item icons update automatically.

An additional resource for other icons can be found at Data:ResourceLookup2.json, a manually maintained table; some entries may rely on a dictionary key.

See Module:Icon/data for manual overrides, legacy items, aliases, and renames used by this module.

Usage

Any of the following templates can be used. The module automatically detects the icon type:

Note: While all four templates work identically, it is recommended to use the appropriate one for clarity (e.g., use {{AbilityIcon|Siphon Life}} even though {{HeroIcon|Siphon Life}} will also work).

Error tracking

Icons that fail to render are added to Category:Module:Icon ERROR.


local p = {}

-- Load language codes
local lang_codes = mw.loadJsonData("Data:LangCodes.json")

-- Cache for compiled icon data, split by type (persists during page render)
local heroCacheBuilt = false
local abilityCacheBuilt = false
local itemCacheBuilt = false
local iconDataCache = {}

local cachedLangCode, cachedLangData

-- Cache for expensive existence checks (persists during page render)
local existenceCache = {}

-- Heroes/Items that don't have images uploaded yet
local missingImageOverrides = {
    -- Unreleased Heroes
    ["boho"] = "Module-Icon missing.png",
    ["druid"] = "Module-Icon missing.png", 
    ["fortuna"] = "Module-Icon missing.png",
    ["graf"] = "Module-Icon missing.png",
    ["gunslinger"] = "Module-Icon missing.png",
    ["skyrunner"] = "Module-Icon missing.png",
    ["swan"] = "Module-Icon missing.png",
    ["thumper"] = "Module-Icon missing.png",
    -- Add any others that show red links
}

-- Build lookup table from bot-uploaded JSONs, only loading the relevant JSON per type
-- to reduce CPU cost on pages with many icon calls
local function buildIconData(iconType)
    -- Only load hero data if not already built
    if iconType == "hero" and not heroCacheBuilt then
        local heroData = mw.loadJsonData("Data:HeroData.json")

        -- Process Heroes
        for heroKey, hero in pairs(heroData) do
            if type(hero) == "table" and hero.Name then
                local name = hero.Name
                local lowerName = name:lower()

                -- Check for missing image override
                local imageName = missingImageOverrides[lowerName] or (name .. ".png")

                iconDataCache[lowerName] = {
                    name = name,
                    key = heroKey,
                    link = name,
                    image = imageName,
                    type = "hero"
                }
            end
        end

        heroCacheBuilt = true

    -- Only load ability data (and hero data for parent linking) if not already built
    elseif iconType == "ability" and not abilityCacheBuilt then
        local heroData = mw.loadJsonData("Data:HeroData.json")
        local abilityData = mw.loadJsonData("Data:AbilityData.json")

        -- Track which hero owns which ability (for linking)
        local abilityToHero = {}

        for heroKey, hero in pairs(heroData) do
            if type(hero) == "table" and hero.BoundAbilities then
                for slot, ability in pairs(hero.BoundAbilities) do
                    if ability.Key then
                        abilityToHero[ability.Key] = {
                            heroName = hero.Name,
                            abilityName = ability.Name
                        }
                    end
                end
            end
        end

        -- Process Abilities
        for abilityKey, ability in pairs(abilityData) do
            if type(ability) == "table" and ability.Name then
                local name = ability.Name
                local lowerName = name:lower()
                local parentInfo = abilityToHero[abilityKey]

                -- If we already have this ability name, prefer the one linked to a hero
                if iconDataCache[lowerName] and iconDataCache[lowerName].type == "ability" then
                    local existingHasParent = iconDataCache[lowerName].link ~= iconDataCache[lowerName].name
                    local newHasParent = parentInfo ~= nil

                    -- Skip if existing is linked to hero but this one isn't (prevents orphans from overwriting real ones)
                    if existingHasParent and not newHasParent then
                        -- Skip this orphan ability
                    else
                        -- Overwrite: new has parent and existing doesn't, or both/neither have parents
                        local link = parentInfo and parentInfo.heroName or name
                        iconDataCache[lowerName] = {
                            name = name,
                            key = abilityKey,
                            link = link,
                            image = name .. ".png",
                            type = "ability",
                            class = "theme"
                        }
                    end
                else
                    -- No existing entry, add normally
                    local link = parentInfo and parentInfo.heroName or name
                    iconDataCache[lowerName] = {
                        name = name,
                        key = abilityKey,
                        link = link,
                        image = name .. ".png",
                        type = "ability",
                        class = "theme"
                    }
                end
            end
        end

        abilityCacheBuilt = true

    -- Only load item data if not already built
    elseif iconType == "item" and not itemCacheBuilt then
        local itemData = mw.loadJsonData("Data:ItemData.json")

        -- Process Items
        for itemKey, item in pairs(itemData) do
            if type(item) == "table" and item.Name then
                local name = item.Name
                local lowerName = name:lower()

                iconDataCache[lowerName] = {
                    name = name,
                    key = itemKey,
                    link = name,
                    image = name .. ".png",
                    type = "item"
                }
            end
        end

        itemCacheBuilt = true
    end
end

-- Get icon data by name and type (with alias support)
local function getIconData(iconName, iconType)
    buildIconData(iconType)
    local lowerName = iconName:lower()

    -- Check main lookup
    if iconDataCache[lowerName] then
        return iconDataCache[lowerName], nil
    end

    -- Check aliases (for edge cases like "doorman" -> "the doorman")
    local aliases = {
        ["doorman"] = "the doorman",
        ["mo and krill"] = "mo & krill",
        ["mo"] = "mo & krill",
        ["krill"] = "mo & krill",
        ["curse"] = "cursed relic",
        ["debuff remover"] = "dispel magic"
    }

    if aliases[lowerName] then
        return iconDataCache[aliases[lowerName]], nil
    end

    return nil, string.format("[[:Category:Module:Icon ERROR|Icon not found]] ('%s'). [[Category:Module:Icon ERROR]]", iconName)
end

-- Language handling
local function getLangData()
    if cachedLangData then
        return cachedLangCode, cachedLangData
    end

    local title = mw.title.getCurrentTitle().fullText
    local subpageLang = title:match("/([^/]+)$")
    local langCode = (subpageLang and lang_codes[subpageLang]) and subpageLang or "en"

    local langData = {}
    if langCode ~= "en" then
        local success, data = pcall(function()
            return mw.loadJsonData("Data:Lang " .. langCode .. ".json")
        end)
        if success and type(data) == "table" then
            langData = data
        end
    end

    cachedLangCode = langCode
    cachedLangData = langData

    return langCode, langData
end

-- Main render function
function p.render(frame)
    local args = frame:getParent().args
    local name = args[1] or ""
    local customText = args.l1 or ""
    local iconType = args.type or "item"

    -- Set Defaults
    local size = args.size or "20px"
    local noLink = args["no-link"] == "true"
    local iconOnly = args["icon-only"] == "true"

    -- Scan unnamed parameters
    for k, v in pairs(args) do
        if type(k) == "number" and k > 1 then
            local val = mw.text.trim(v)

            if val == "icon-only" then
                iconOnly = true
            elseif val == "no-link" then
                noLink = true
            elseif val:match("^%d+px$") then
                size = val
            end
        end
    end

    -- Get icon data automatically from JSON, using type to limit which JSON is loaded
    local iconData, err = getIconData(name, iconType)
    if not iconData then
        return "Error: " .. err
    end

    local langCode, langData = getLangData()

    -- Determine display name
    local displayName = ""
    if customText ~= "" then
        displayName = customText
    elseif iconData.key and langData[iconData.key] then
        displayName = langData[iconData.key]
    else
        displayName = iconData.name
    end

    -- Localize link if on translated subpage
    local baseLink = iconData.link or ""
    local link = baseLink

    if langCode ~= "en" and baseLink ~= "" then
        link = baseLink .. "/" .. langCode
    end

    -- Style logic (only abilities get theme class by default)
    local class = iconData.class or ""
    local style
    if class:find("theme", 1, true) then
        style = 'class="module-icon-ability" style="position:relative; bottom:2px;"'
    else
        style = 'style="position:relative; bottom:2px;"'
    end

    -- Generate icon HTML
    local imageLink = noLink and "" or link
    local iconHtml = string.format('<span %s>[[File:%s|%s|link=%s]]</span>', style, iconData.image, size, imageLink)

    if iconOnly or displayName == "" then
        return iconHtml
    end

    local textHtml = (noLink or link == "") and displayName or string.format('[[%s|%s]]', link, displayName)
    return '<span style="white-space:nowrap;">' .. iconHtml .. " " .. textHtml .. '</span>'
end

return p