Module:Sandbox/LVL: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
LVL (talk | contribs)
No edit summary
LVL (talk | contribs)
No edit summary
Line 1: Line 1:
local p = {}
local p = {}
local items_data = mw.loadJsonData("Data:ItemData.json")
local lang_module = require('Module:Lang')
local generic_module = require('Module:GenericData')
local util_module = require('Module:Utilities')


local heroes_data      = mw.loadJsonData("Data:HeroData.json")
--exceptions if the link is different from the item name
local attributes_data  = mw.loadJsonData("Data:AttributeData.json")
local linkOverrides = {
local attribute_orders  = mw.loadJsonData("Data:StatInfoboxOrder.json")
    ["Bullet Lifesteal"] = "Bullet Lifesteal (item)",
local util_module      = require('Module:Utilities')
    ["Spirit Lifesteal"] = "Spirit Lifesteal (item)"
local lang_module      = require('Module:Lang')
}
local dictionary_module = require('Module:Dictionary')
local attribute_module  = require('Module:AttributeData')
local hero_data_module  = require('Module:HeroData')


local function get_nested_stat_value(hero_data, stat_key)
--With debug_mode on, it outputs unprocessed wikitext
    if hero_data[stat_key] ~= nil then
--With debug_mode off/unspecified, it processes the wikitext
        return hero_data[stat_key]
local function process_debug_mode(wikitext, debug_mode)
    elseif hero_data.Weapon and hero_data.Weapon[stat_key] ~= nil then
if debug_mode == 'true' then
        return hero_data.Weapon[stat_key]
return wikitext
    elseif hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire[stat_key] ~= nil then
elseif debug_mode == 'false' or debug_mode == nil then
        return hero_data.Weapon.AltFire[stat_key]
return mw.getCurrentFrame():preprocess(wikitext)
    end
else
    return 0
return "debug_mode must be 'true' or 'false'"
end
end
end


local function localize(key, fallback)
--Writes list of items of a certain slot within the min and max soul bounds
    local result = lang_module.get_string(key)
-- Each item is wrapped and separated. For example of wrapping/separator, see
     if result == "" or result == nil then
-- get_item_nav_bulletpoints. 'sep' should not be combined with 'right_wrap',
         result = util_module.add_space_before_cap(fallback) ..
-- as the trailing separator should also be removed from the string
                mw.getCurrentFrame():expandTemplate{title = "MissingValveTranslationTooltip"}
-- filter_mode parameter: nil/"all", "exclude_street_brawl", or "street_brawl_only"
local function write_wrapped_item_list(slot, min_souls, max_souls, template, sep, filter_mode)
     if slot ~= 'Weapon' and slot ~= 'Armor' and slot ~= 'Tech' then
         return 'slot must be Weapon, Armor (Vitality), or Tech (Spirit)'
     end
     end
     return result
      
end
     local min_souls = tonumber(min_souls)
 
     local max_souls = tonumber(max_souls)
-- {{#invoke:HeroComparisonTable|write_hero_comparison_table|POWER_INCREASES|SPIRIT_POWER|MAX_POWER|MAX_SPIRIT}}
     if min_souls == nil or max_souls == nil then return 'Min/Max souls must be numerical' end
p.write_hero_comparison_table = function(frame)
      
     local power_increases = tonumber(frame.args[1]) or 0
     -- Normalize filter_mode to handle unset parameter
     local spirit_power    = tonumber(frame.args[2]) or 0
     filter_mode = filter_mode or "all"
     local max_power      = tonumber(frame.args[3]) or 25
      
     local max_spirit      = tonumber(frame.args[4]) or 500
     -- Retrieve all items that fit the bounds and filter criteria
 
     local items = {}
     local display_scaling_icons = (power_increases == 0 and spirit_power == 0)
     for item_key, item_data in pairs(items_data) do
 
         -- future proofing; Disabled will be renamed to IsDisabled soon
     local body_str  = ""
         local this_cost = tonumber(item_data["Cost"])
     local hero_name
        local this_slot = item_data["Slot"]
     local hero_name_en
       
     local hero_icon
         -- Filter logic for Street Brawl items
    local template_args = {}
         local is_street_brawl = item_data["StreetBrawl"] == true
 
         local should_include = true
     local stats_to_include = {
       
        Weapon = {
         if filter_mode == "street_brawl_only" and not is_street_brawl then
            "DPS",
            should_include = false
            "SustainedDPS",
        elseif filter_mode == "exclude_street_brawl" and is_street_brawl then
            "BulletDamage",
             should_include = false
            "RoundsPerSecond",
            "FireRate",
            "ClipSize",
            "ReloadTime",
            "ReloadDelay",
            "BulletsPerShot",
            "BulletsPerBurst",
            "BurstInterShotInterval",
            "LightMeleeDamage",
            "HeavyMeleeDamage",
            "ReloadSingle",
            "BulletSpeed",
            "BulletGravityScale",
            "FalloffStartRange",
            "FalloffEndRange",
            "BonusAttackRange",
            "CritDamageBonusPercent",
            "RoundsPerSecondAtMaxSpin",
            "SpinAcceleration",
            "SpinDeceleration"
         },
         Vitality = {
            "MaxHealth",
            "BaseHealthRegen",
            "BulletResist",
            "TechResist",
            "MeleeResist",
            "BulletLifesteal",
            "CritDamageReceivedPercent",
            "MaxMoveSpeed",
            "SprintSpeed",
            "StaminaCooldown",
            "Stamina",
            "GroundDashSpeed"
         },
         Spirit = {
            "TechPower"
         }
    }
 
    -- Collect and sort heroes alphabetically, skipping disabled/in-development
    local sorted_heroes = {}
    for hero_key, hero_data in pairs(heroes_data) do
         if not hero_data["InDevelopment"] and not hero_data["IsDisabled"] then
             table.insert(sorted_heroes, {key = hero_key, data = hero_data})
         end
         end
    end
          
    table.sort(sorted_heroes, function(a, b)
         if should_include and item_data["Name"] ~= nil and item_data["IsDisabled"] == false and this_cost ~= nil and this_slot ~= nil then
        local function get_sort_name(name)
            if slot == this_slot and this_cost >= min_souls and this_cost < max_souls then
            return (name:gsub("^The ", ""))
                 local item_name_english = lang_module.get_string(item_key, "en") -- Get the English name
         end
                local item_link = linkOverrides[item_name_english] or item_name_english -- Use override link if available
         return get_sort_name(a.data["Name"] or a.key) < get_sort_name(b.data["Name"] or b.key)
                local lang_code = lang_module.get_lang_code() -- Get the language code from the subpage
    end)
                local item_name_local = lang_module.get_string(item_key, lang_code) -- Get the localized name
 
               
    -- Build table body rows
                -- Construct the template based on the type
    for _, hero_entry in ipairs(sorted_heroes) do
                local item_template
        local hero_key  = hero_entry.key
                if template == "ItemIcon" then
        local hero_data = hero_entry.data
                     if lang_code == "en" then
        local row_str  = ""
                         -- For English pages, do not include lang or localized name
 
                         item_template = "{{ItemIcon|" .. item_name_english .. "|size=26px}}"
        hero_name    = localize(hero_key, hero_key)
        hero_name_en = hero_data["Name"]
 
        template_args = {[1] = hero_name_en, l1 = hero_name}
        hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args }
 
        row_str = row_str ..
            '<td style="position: sticky; left: 0; z-index: 10; background-color: #202122; isolation: isolate; overflow: hidden;">' ..
            hero_icon .. "</td>"
 
        for _, category in ipairs(attribute_orders["category_order"]) do
            if stats_to_include[category] ~= nil then
                 for _, attr_key in ipairs(stats_to_include[category]) do
 
                    local base_value  = get_nested_stat_value(hero_data, attr_key)
                    local stat_value  = base_value
                    local scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
                    local scaling_strs = ""
                    local spirit_scale = 0
                    local level_scale  = 0
 
                    if scaling_data ~= nil then
                        for scaling_val, scaling_type in pairs(scaling_data) do
                            local scaling_str = hero_data_module.write_scalar_str(scaling_val, scaling_type, true)
                            if scaling_str ~= "" then scaling_str = " " .. scaling_str end
                            if scaling_str ~= nil then
                                scaling_strs = scaling_strs .. scaling_str
                            end
 
                            if scaling_type == "Spirit" then
                                spirit_scale = scaling_val
                                stat_value  = stat_value + (spirit_power * scaling_val)
                            elseif scaling_type == "Level" then
                                level_scale = scaling_val
                                stat_value  = stat_value + (power_increases * scaling_val)
                            end
                        end
                    end
 
                     if attr_key == "TechPower" and spirit_scale == 0 then
                         spirit_scale = 1.0
                         stat_value  = stat_value + (spirit_power * spirit_scale)
                    end
 
                    local innate_spirit_scale =
                        (hero_data["LevelScaling"] and hero_data["LevelScaling"]["TechPower"]) or 0
 
                    if not display_scaling_icons then scaling_strs = "" end
 
                    if type(stat_value) == "boolean" then
                        stat_value = tostring(stat_value)
                     else
                     else
                         stat_value = util_module.round_to_sig_fig(stat_value, 3)
                         -- For non-English pages, include lang and localized name
                        item_template = "{{ItemIcon|" .. item_name_english .. "|lang=" .. lang_code .. "|l1=" .. item_name_local .. "|size=26px}}"
                     end
                     end
 
                   
                     local cell_inner = string.format(
                     -- Build data attribute for searching (includes both English and localized names)
                        '<span class="stat-num">%s</span><span class="stat-scaling">%s</span>',
                     local search_data = item_name_english
                        stat_value,
                     if lang_code ~= "en" and item_name_local then
                        scaling_strs
                         search_data = search_data .. " " .. item_name_local
                    )
 
                     local hit_once = "false"
                     if (attr_key == "DPS" or attr_key == "SustainedDPS")
                        and hero_data.Weapon
                         and hero_data.Weapon.HitOnceAcrossAllBullets
                    then
                        hit_once = "true"
                     end
                     end
 
                   
                     local data_attrs = string.format(
                    -- Wrap item and its bullet separator in a searchable span
                        'data-stat-name="%s" data-base="%s" data-spirit-scale="%s" data-level-scale="%s" data-innate-spirit-scale="%s" data-sort-value="%s" data-hit-once="%s"',
                     item_template = '<span class="item-nav-search-wrapper"><span class="item-nav-search-item" data-item-name="' .. search_data .. '">' .. item_template .. '</span><span class="item-nav-search-sep"> &bull; </span></span>'
                        attr_key,
                 elseif template == "ItemBox" then
                        tostring(type(base_value) == "number" and util_module.round_to_sig_fig(base_value, 3) or base_value),
                    if lang_code == "en" then
                        tonumber(spirit_scale) or 0,
                        -- For English pages, do not include item_loc or link
                        (attr_key == "TechPower") and "0" or (tonumber(level_scale) or 0),
                        -- Display cost as "Legendary" for street brawl items
                        innate_spirit_scale or 0,
                        if is_street_brawl == true then
                        stat_value,
item_template = "{{ItemBox|itemName=" .. item_name_english .. "|overrideLinkClick=" .. item_link .. "|overrideTier=5" .. "}}"
                        hit_once
                         else
                    )
                             item_template = "{{ItemBox|itemName=" .. item_name_english .. "|overrideLinkClick=" .. item_link .. "}}"
 
                    row_str = row_str ..
                        string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner)
                 end
            end
        end
 
        body_str = body_str .. "<tr>" .. row_str .. "</tr>"
    end
 
    -- Pre-pass: determine which stats have any scaling, and which have both types
    local stats_with_any_scaling  = {}
    local stats_with_both_scaling = {}
    for _, hero_entry in ipairs(sorted_heroes) do
        local hero_data = hero_entry.data
        for _, category in ipairs(attribute_orders["category_order"]) do
            if stats_to_include[category] ~= nil then
                for _, attr_key in ipairs(stats_to_include[category]) do
                    local scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
                    if scaling_data ~= nil and next(scaling_data) ~= nil then
                        stats_with_any_scaling[attr_key] = true
 
                        local has_spirit = false
                        local has_level  = false
                         for _, scaling_type in pairs(scaling_data) do
                             if scaling_type == "Spirit" then has_spirit = true end
                            if scaling_type == "Level" then has_level  = true end
                         end
                         end
                         if has_spirit and has_level then
                    else
                             stats_with_both_scaling[attr_key] = true
                        -- For non-English pages, include item_loc and link
                        local link_local = item_link .. "/" .. lang_code -- Add subpage for non-English languages
                         if is_street_brawl == true then
                             item_template = "{{ItemBox|itemName=" .. item_name_english .. "|overrideLinkClick=" .. link_local ..
                            "|item_price={{#invoke:Lang|get_string|Citadel_ItemDraft_Legendary}}" .. "}}"
                        else
                        item_template = "{{ItemBox|itemName=" .. item_name_english .. "|overrideLinkClick=" .. link_local .. "}}"
                         end
                         end
                     end
                     end
                else
                    return "Invalid template type"
                 end
                 end
                table.insert(items, item_template)
             end
             end
         end
         end
     end
     end
   
    -- Order list alphabetically
    table.sort(items) -- O(nlogn)
   
    -- Add each item to output
    -- Each item is already wrapped in the template, so no need for additional wrapping
    local ret = table.concat(items, sep)
   
    return ret
end


    -- Build header row
-- for [[Template:Item Navbox]]
    local headers_str = '<th style="position: sticky; left: 0; top: -1px; z-index: 12; background-color: #27292d; isolation: isolate;">Hero</th>'
-- Supports filter parameter via frame.args["filter"]
     local category_data = attribute_module.get_category_data()
function p.get_item_nav_bulletpoints(slot, min_souls, max_souls, debug_mode, filter_mode)
     local postfix_key_map = {
     -- Handle the case where it's called via #invoke (i.e., from wikitext)
        ["ReloadDelay"]              = "StatDesc_ReloadTime_postfix",
     if type(slot) == "table" and slot.args then
         ["BulletsPerShot"]          = "",
         local frame = slot
         ["BulletsPerBurst"]         = "",
         slot = frame.args[1]
         ["BurstInterShotInterval"]   = "StatDesc_ReloadTime_postfix",
         min_souls = frame.args[2]
         ["ReloadSingle"]             = "",
         max_souls = frame.args[3]
         ["BonusAttackRange"]        = "StatDesc_WeaponRangeFalloffMax_postfix",
         debug_mode = frame.args["debug_mode"]
        ["SustainedDPS"]             = "DPS_postfix",
         filter_mode = frame.args["filter"]
        ["RoundsPerSecondAtMaxSpin"] = "",
    end
        ["CritDamageBonusScale"]     = "StatDesc_CritDamageBonusScale_postfix",
        ["BonusCritDamagePercent"]  = "BonusCritDamagePercent_postfix",
    local sep = ''
        ["BulletLifesteal"]          = "BulletLifestealPercentHero_postfix",
        ["GroundDashSpeed"]          = "DashSpeed_postfix"
     local item_list = write_wrapped_item_list(slot, min_souls, max_souls, "ItemIcon", sep, filter_mode)
     }
     return process_debug_mode(item_list, debug_mode)
end


    for _, category in ipairs(attribute_orders["category_order"]) do
-- for [[Template:Infobox ShopItems]]
         local category_attrs = attributes_data[category]
-- Supports filter parameter via frame.args["filter"]
         local category_rgb  = category_data[category]["rgb"]
function p.get_item_nav_cards(slot, min_souls, max_souls, debug_mode, filter_mode)
    -- Handle the case where it's called via #invoke (i.e., from wikitext)
    if type(slot) == "table" and slot.args then
         local frame = slot
        slot = frame.args[1]
        min_souls = frame.args[2]
        max_souls = frame.args[3]
         debug_mode = frame.args["debug_mode"]
        filter_mode = frame.args["filter"]
    end
    local sep = ' '
    local item_list = write_wrapped_item_list(slot, min_souls, max_souls, "ItemBox", sep, filter_mode)
    return process_debug_mode(item_list, debug_mode)
end


        if stats_to_include[category] ~= nil then
-- for [[Template:Item Navbox]] subgroup rows
            for _, attr_key in ipairs(stats_to_include[category]) do
-- Writes price tiers and appends Street Brawl section if items exist
                local attr_data = category_attrs[attr_key]
function p.write_item_slot_subgroup(frame)
                local attr_localized
local slot = frame.args[1]
                local postfix
local type = frame.args[2]
local debug_mode = frame.args['debug_mode']
local street_brawl_label = frame.args['street_brawl_label'] or '{{Legendary|{{#invoke:Lang|get_string|Citadel_ItemDraft_Legendary}}}}'
if slot == nil then return "'slot' parameter is required" end
-- Define base args
local template_title = "Navbox subgroup"
local template_args = {
["groupstyle"] = "background-color:" .. util_module.get_slot_color(slot) .. ";width:10%;min-width:70px;border-radius: 8px 0 0 8px",
["grouppadding"] = "5px",
["listpadding"] = "0 0.25rem",
}
local soul_style = "font-size: 12px; text-shadow: 1px 1px rgba(0, 0, 0, 0.3);"
local prices = generic_module.get_item_price_per_tier()
local group_index = 1
for i, souls in ipairs(prices) do
min_souls = souls
--Skip 0 to i1 as no items cost less than 500
if min_souls ~= 0 then
-- Determine upper bound for soul tier
max_souls = prices[i+1]
if max_souls == nil then
max_souls = min_souls * 10
end
-- Generate list for this price tier, excluding Street Brawl items
local list
if type=='get_item_nav_cards' then
list = p.get_item_nav_cards(slot, min_souls, max_souls, debug_mode, "exclude_street_brawl")
elseif type=='get_item_nav_bulletpoints' then
list = p.get_item_nav_bulletpoints(slot, min_souls, max_souls, debug_mode, "exclude_street_brawl")
else
return "'type' should be get_item_nav_cards or get_item_nav_bulletpoints"
end
-- Only add the group if there are items in this tier
if list and list ~= "" then
template_args["group" .. group_index] = frame:expandTemplate{title="Souls", args={[1] = min_souls, ["Shadow"] = soul_style}}
template_args["list" .. group_index] = list
group_index = group_index + 1
end
end
end
-- Add Street Brawl section at the bottom of the slot
local sb_min = 0
local sb_max = 999999
local sb_list
if type=='get_item_nav_cards' then
sb_list = p.get_item_nav_cards(slot, sb_min, sb_max, debug_mode, "street_brawl_only")
elseif type=='get_item_nav_bulletpoints' then
sb_list = p.get_item_nav_bulletpoints(slot, sb_min, sb_max, debug_mode, "street_brawl_only")
end
-- Only add Street Brawl group if there are items for this slot
if sb_list and sb_list ~= "" then
-- Style the link to match other tier headers with white text inside the link to prevent visited link color
local sb_style = "color:#98ffde; font-family:'Retail Demo', sans-serif; text-wrap: nowrap; text-shadow: 0px 0px 2px #2c312e, 1.3px 1.3px rgba(0, 0, 0, 0.2);padding:5px;"
local sb_link_text = "[[Street Brawl{{if_lang}}|<span style=\"" .. sb_style .. "\"><b>" .. street_brawl_label .. "</b></span>]]"
template_args["group" .. group_index] = frame:preprocess(sb_link_text)
template_args["list" .. group_index] = sb_list
end
return frame:expandTemplate{title=template_title, args=template_args}
end


                if attr_data ~= nil then
-- for [[Template:Active Items]]
                    attr_localized = lang_module.get_string(attr_data["label"])
function p.generate_active_items_table(frame)
                    if attr_localized == nil or attr_localized == "" then
    local active_items = {}
                        attr_localized = util_module.add_space_before_cap(attr_key)
   
                    end
    -- 1. Collect active items and their cost
                    postfix = lang_module.get_string(attr_data["postfix"])
    for item_key, item_data in pairs(items_data) do
                    if postfix == nil or postfix == "" then
        -- Convert cost to a number; will be nil if the JSON value is null or missing
                        postfix = ""
        local cost = tonumber(item_data["Cost"])
                    else
       
                        postfix = " (" .. postfix .. ")"
        if item_data["Name"] and
                    end
          item_data["IsDisabled"] == false and
                else
          cost ~= nil and cost > 0 and            -- FILTER: Must have a cost greater than 0
                    attr_localized = dictionary_module.translate(attr_key)
          item_data["Activation"] and
                    postfix = lang_module.get_string(postfix_key_map[attr_key])
          item_data["Activation"] ~= "Passive" then
                    if postfix == nil then
           
                        return "attr_key " .. attr_key .. " must be added to postfix_key_map"
            table.insert(active_items, {
                    end
                name = item_data["Name"],
                    if postfix ~= "" then
                key = item_key,
                        postfix = " (" .. postfix .. ")"
                cost = cost,
                    end
                slot = item_data["Slot"] or "Weapon",
                end
                has_active_desc = item_data["ActiveDescription"] ~= nil
 
            })
                local th_style = 'position: sticky; top: -1px; z-index: 3; background-color: rgb(' .. category_rgb .. ');'
        end
                if stats_with_both_scaling[attr_key] then
    end
                    th_style = th_style .. ' min-width: 130px;'
   
                elseif stats_with_any_scaling[attr_key] then
    -- 2. Sort by name
                    th_style = th_style .. ' min-width: 75px;'
    table.sort(active_items, function(a, b) return a.name < b.name end)
                end
   
 
    -- 3. Generate wiki markup
                headers_str = headers_str ..
    local wikitext = [=[{| class="wikitable mw-collapsible sortable" style="width:100%"
                    '<th style="' .. th_style .. '">' .. attr_localized .. postfix .. "</th>"
|+{{#invoke:Dictionary|translate|Active Items}}
             end
! colspan="2" | {{#invoke:Lang|get_string|Citadel_HeroBuilds_CategoryNameLabel}}
! {{#invoke:Lang|get_string|Citadel_UserFeedback_TypeLabel}}
! {{Souls|{{#invoke:Dictionary|translate|Cost}}}}
! {{#invoke:Lang|get_string|Citadel_Mod_Tooltip_Active}}
! {{#invoke:Lang|get_string|AbilityCooldown_label}}
]=]
   
    -- 4. Add rows for each item
    for _, item in ipairs(active_items) do
        -- Determine background color and item type
        local bg_color, item_type
        if item.slot == "Armor" then
            bg_color = "#86C921"
            item_type = "Armor"
        elseif item.slot == "Tech" then
            bg_color = "#DE9CFF"
            item_type = "Tech"
        else
            bg_color = "#FCAC4D"
             item_type = "Weapon"
         end
         end
       
        -- The cost cell uses 'data-sort-value' to provide a clean number for the client-side sorter,
        -- while the cell's visible content is formatted by the {{Souls}} template.
        wikitext = wikitext .. "\n" .. string.format([=[
|-
! style="background-color:%s" | [[File:%s.png|64x64px]]
! [[%s{{If_lang}}|{{#invoke:Lang|get_string|%s}}]]
| style="text-align:center;font-weight:bold" | {{ItemType|%s|{{#invoke:Lang|get_string|CitadelCategory%s}}}}
| data-sort-value="%d" style="text-align:center;font-size:15px" | {{Souls|%d}}
| {{#invoke:Utilities|process_variables|%s_active_desc|%s}}{{#invoke:Utilities|process_variables|%s_desc|%s}}{{#invoke:Utilities|process_variables|%s_active|%s}}
| style="text-align:center;font-size:16px" | {{#invoke:ItemData|get_prop|%s|AbilityCooldown}}s
]=],
            bg_color,
            item.name,
            item.name, item.key,
            item_type, item_type,
            item.cost, item.cost,
            item.key, item.name, item.key, item.name, item.key, item.name,
            item.name)
     end
     end
     headers_str = "<tr>" .. headers_str .. "</tr>"
      
 
    wikitext = wikitext .. "\n|}"
     return string.format(
   
        '<div id="hero-comparison-container" data-max-power="%s" data-max-spirit="%s">' ..
     return frame:preprocess(wikitext)
        '<div style="overflow: auto; max-height: 70vh; width: 100%%;">' ..
        '<table class="wikitable sortable" style="table-layout: auto; width: 100%%;" id="hero-comparison-table">%s%s</table>' ..
        '</div></div>',
        max_power,
        max_spirit,
        headers_str,
        body_str
    )
end
end


return p
return p

Revision as of 15:24, 22 June 2026

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

local p = {}
local items_data = mw.loadJsonData("Data:ItemData.json")
local lang_module = require('Module:Lang')
local generic_module = require('Module:GenericData')
local util_module = require('Module:Utilities')

--exceptions if the link is different from the item name
local linkOverrides = {
    ["Bullet Lifesteal"] = "Bullet Lifesteal (item)",
    ["Spirit Lifesteal"] = "Spirit Lifesteal (item)"
}

--With debug_mode on, it outputs unprocessed wikitext
--With debug_mode off/unspecified, it processes the wikitext
local function process_debug_mode(wikitext, debug_mode)
	if debug_mode == 'true' then
		return wikitext
	elseif debug_mode == 'false' or debug_mode == nil then
		return mw.getCurrentFrame():preprocess(wikitext)
	else
		return "debug_mode must be 'true' or 'false'"
	end
end

--Writes list of items of a certain slot within the min and max soul bounds
-- Each item is wrapped and separated. For example of wrapping/separator, see
-- get_item_nav_bulletpoints. 'sep' should not be combined with 'right_wrap', 
-- as the trailing separator should also be removed from the string
-- filter_mode parameter: nil/"all", "exclude_street_brawl", or "street_brawl_only"
local function write_wrapped_item_list(slot, min_souls, max_souls, template, sep, filter_mode)
    if slot ~= 'Weapon' and slot ~= 'Armor' and slot ~= 'Tech' then
        return 'slot must be Weapon, Armor (Vitality), or Tech (Spirit)'
    end
    
    local min_souls = tonumber(min_souls)
    local max_souls = tonumber(max_souls)
    if min_souls == nil or max_souls == nil then return 'Min/Max souls must be numerical' end
    
    -- Normalize filter_mode to handle unset parameter
    filter_mode = filter_mode or "all"
    
    -- Retrieve all items that fit the bounds and filter criteria
    local items = {}
    for item_key, item_data in pairs(items_data) do
        -- future proofing; Disabled will be renamed to IsDisabled soon
        local this_cost = tonumber(item_data["Cost"])
        local this_slot = item_data["Slot"]
        
        -- Filter logic for Street Brawl items
        local is_street_brawl = item_data["StreetBrawl"] == true
        local should_include = true
        
        if filter_mode == "street_brawl_only" and not is_street_brawl then
            should_include = false
        elseif filter_mode == "exclude_street_brawl" and is_street_brawl then
            should_include = false
        end
        
        if should_include and item_data["Name"] ~= nil and item_data["IsDisabled"] == false and this_cost ~= nil and this_slot ~= nil then
            if slot == this_slot and this_cost >= min_souls and this_cost < max_souls then
                local item_name_english = lang_module.get_string(item_key, "en") -- Get the English name
                local item_link = linkOverrides[item_name_english] or item_name_english -- Use override link if available
                local lang_code = lang_module.get_lang_code() -- Get the language code from the subpage
                local item_name_local = lang_module.get_string(item_key, lang_code) -- Get the localized name
                
                -- Construct the template based on the type
                local item_template
                if template == "ItemIcon" then
                    if lang_code == "en" then
                        -- For English pages, do not include lang or localized name
                        item_template = "{{ItemIcon|" .. item_name_english .. "|size=26px}}"
                    else
                        -- For non-English pages, include lang and localized name
                        item_template = "{{ItemIcon|" .. item_name_english .. "|lang=" .. lang_code .. "|l1=" .. item_name_local .. "|size=26px}}"
                    end
                    
                    -- Build data attribute for searching (includes both English and localized names)
                    local search_data = item_name_english
                    if lang_code ~= "en" and item_name_local then
                        search_data = search_data .. " " .. item_name_local
                    end
                    
                    -- Wrap item and its bullet separator in a searchable span
                    item_template = '<span class="item-nav-search-wrapper"><span class="item-nav-search-item" data-item-name="' .. search_data .. '">' .. item_template .. '</span><span class="item-nav-search-sep"> &bull; </span></span>'
                elseif template == "ItemBox" then
                    if lang_code == "en" then
                        -- For English pages, do not include item_loc or link
                        -- Display cost as "Legendary" for street brawl items
                        if is_street_brawl == true then
							item_template = "{{ItemBox|itemName=" .. item_name_english .. "|overrideLinkClick=" .. item_link .. "|overrideTier=5" .. "}}"
                        else
                            item_template = "{{ItemBox|itemName=" .. item_name_english .. "|overrideLinkClick=" .. item_link .. "}}"
                        end
                    else
                        -- For non-English pages, include item_loc and link
                        local link_local = item_link .. "/" .. lang_code -- Add subpage for non-English languages
                        if is_street_brawl == true then
                            item_template = "{{ItemBox|itemName=" .. item_name_english .. "|overrideLinkClick=" .. link_local ..
                            "|item_price={{#invoke:Lang|get_string|Citadel_ItemDraft_Legendary}}" .. "}}"
                        else
                        	item_template = "{{ItemBox|itemName=" .. item_name_english .. "|overrideLinkClick=" .. link_local .. "}}"
                        end
                    end
                else
                    return "Invalid template type"
                end
                table.insert(items, item_template)
            end
        end
    end
    
    -- Order list alphabetically
    table.sort(items) -- O(nlogn)
    
    -- Add each item to output
    -- Each item is already wrapped in the template, so no need for additional wrapping
    local ret = table.concat(items, sep)
    
    return ret
end

-- for [[Template:Item Navbox]]
-- Supports filter parameter via frame.args["filter"]
function p.get_item_nav_bulletpoints(slot, min_souls, max_souls, debug_mode, filter_mode)
    -- Handle the case where it's called via #invoke (i.e., from wikitext)
    if type(slot) == "table" and slot.args then
        local frame = slot
        slot = frame.args[1]
        min_souls = frame.args[2]
        max_souls = frame.args[3]
        debug_mode = frame.args["debug_mode"]
        filter_mode = frame.args["filter"]
    end
	
    local sep = ''
	
    local item_list = write_wrapped_item_list(slot, min_souls, max_souls, "ItemIcon", sep, filter_mode)
	
    return process_debug_mode(item_list, debug_mode)
end

-- for [[Template:Infobox ShopItems]]
-- Supports filter parameter via frame.args["filter"]
function p.get_item_nav_cards(slot, min_souls, max_souls, debug_mode, filter_mode)
    -- Handle the case where it's called via #invoke (i.e., from wikitext)
    if type(slot) == "table" and slot.args then
        local frame = slot
        slot = frame.args[1]
        min_souls = frame.args[2]
        max_souls = frame.args[3]
        debug_mode = frame.args["debug_mode"]
        filter_mode = frame.args["filter"]
    end
	
    local sep = ' '
	
    local item_list = write_wrapped_item_list(slot, min_souls, max_souls, "ItemBox", sep, filter_mode)
	
    return process_debug_mode(item_list, debug_mode)
end

-- for [[Template:Item Navbox]] subgroup rows
-- Writes price tiers and appends Street Brawl section if items exist
function p.write_item_slot_subgroup(frame)
	local slot = frame.args[1]
	local type = frame.args[2]
	local debug_mode = frame.args['debug_mode']
	local street_brawl_label = frame.args['street_brawl_label'] or '{{Legendary|{{#invoke:Lang|get_string|Citadel_ItemDraft_Legendary}}}}'
	
	if slot == nil then return "'slot' parameter is required" end
	
	-- Define base args
	local template_title = "Navbox subgroup"
	local template_args = {
		["groupstyle"] = "background-color:" .. util_module.get_slot_color(slot) .. ";width:10%;min-width:70px;border-radius: 8px 0 0 8px",
		["grouppadding"] = "5px",
		["listpadding"] = "0 0.25rem",
	}
	
	local soul_style = "font-size: 12px; text-shadow: 1px 1px rgba(0, 0, 0, 0.3);"
	local prices = generic_module.get_item_price_per_tier()
	local group_index = 1
	
	for i, souls in ipairs(prices) do
		min_souls = souls
		
		--Skip 0 to i1 as no items cost less than 500
		if min_souls ~= 0 then 
			-- Determine upper bound for soul tier
			max_souls = prices[i+1]
			if max_souls == nil then 
				max_souls = min_souls * 10
			end
			
			-- Generate list for this price tier, excluding Street Brawl items
			local list
			if type=='get_item_nav_cards' then
				list = p.get_item_nav_cards(slot, min_souls, max_souls, debug_mode, "exclude_street_brawl")
			elseif type=='get_item_nav_bulletpoints' then
				list = p.get_item_nav_bulletpoints(slot, min_souls, max_souls, debug_mode, "exclude_street_brawl")
			else
				return "'type' should be get_item_nav_cards or get_item_nav_bulletpoints"
			end
			
			-- Only add the group if there are items in this tier
			if list and list ~= "" then
				template_args["group" .. group_index] = frame:expandTemplate{title="Souls", args={[1] = min_souls, ["Shadow"] = soul_style}}
				template_args["list" .. group_index] = list
				group_index = group_index + 1
			end
		end
	end
	
	-- Add Street Brawl section at the bottom of the slot
	local sb_min = 0
	local sb_max = 999999
	
	local sb_list
	if type=='get_item_nav_cards' then
		sb_list = p.get_item_nav_cards(slot, sb_min, sb_max, debug_mode, "street_brawl_only")
	elseif type=='get_item_nav_bulletpoints' then
		sb_list = p.get_item_nav_bulletpoints(slot, sb_min, sb_max, debug_mode, "street_brawl_only")
	end
	
	-- Only add Street Brawl group if there are items for this slot
	if sb_list and sb_list ~= "" then
		-- Style the link to match other tier headers with white text inside the link to prevent visited link color
		local sb_style = "color:#98ffde; font-family:'Retail Demo', sans-serif; text-wrap: nowrap; text-shadow: 0px 0px 2px #2c312e, 1.3px 1.3px rgba(0, 0, 0, 0.2);padding:5px;"
		local sb_link_text = "[[Street Brawl{{if_lang}}|<span style=\"" .. sb_style .. "\"><b>" .. street_brawl_label .. "</b></span>]]"
		template_args["group" .. group_index] = frame:preprocess(sb_link_text)
		template_args["list" .. group_index] = sb_list
	end
	
	return frame:expandTemplate{title=template_title, args=template_args}
end

-- for [[Template:Active Items]]
function p.generate_active_items_table(frame)
    local active_items = {}
    
    -- 1. Collect active items and their cost
    for item_key, item_data in pairs(items_data) do
        -- Convert cost to a number; will be nil if the JSON value is null or missing
        local cost = tonumber(item_data["Cost"])
        
        if item_data["Name"] and 
           item_data["IsDisabled"] == false and 
           cost ~= nil and cost > 0 and             -- FILTER: Must have a cost greater than 0
           item_data["Activation"] and 
           item_data["Activation"] ~= "Passive" then
            
            table.insert(active_items, {
                name = item_data["Name"],
                key = item_key,
                cost = cost,
                slot = item_data["Slot"] or "Weapon",
                has_active_desc = item_data["ActiveDescription"] ~= nil
            })
        end
    end
    
    -- 2. Sort by name
    table.sort(active_items, function(a, b) return a.name < b.name end)
    
    -- 3. Generate wiki markup
    local wikitext = [=[{| class="wikitable mw-collapsible sortable" style="width:100%" 
|+{{#invoke:Dictionary|translate|Active Items}}
! colspan="2" | {{#invoke:Lang|get_string|Citadel_HeroBuilds_CategoryNameLabel}}
! {{#invoke:Lang|get_string|Citadel_UserFeedback_TypeLabel}}
! {{Souls|{{#invoke:Dictionary|translate|Cost}}}}
! {{#invoke:Lang|get_string|Citadel_Mod_Tooltip_Active}}
! {{#invoke:Lang|get_string|AbilityCooldown_label}}
]=]
    
    -- 4. Add rows for each item
    for _, item in ipairs(active_items) do
        -- Determine background color and item type
        local bg_color, item_type
        if item.slot == "Armor" then
            bg_color = "#86C921"
            item_type = "Armor"
        elseif item.slot == "Tech" then
            bg_color = "#DE9CFF"
            item_type = "Tech"
        else
            bg_color = "#FCAC4D"
            item_type = "Weapon"
        end
        
        -- The cost cell uses 'data-sort-value' to provide a clean number for the client-side sorter,
        -- while the cell's visible content is formatted by the {{Souls}} template.
        wikitext = wikitext .. "\n" .. string.format([=[
|-
! style="background-color:%s" | [[File:%s.png|64x64px]]
! [[%s{{If_lang}}|{{#invoke:Lang|get_string|%s}}]]
| style="text-align:center;font-weight:bold" | {{ItemType|%s|{{#invoke:Lang|get_string|CitadelCategory%s}}}}
| data-sort-value="%d" style="text-align:center;font-size:15px" | {{Souls|%d}}
| {{#invoke:Utilities|process_variables|%s_active_desc|%s}}{{#invoke:Utilities|process_variables|%s_desc|%s}}{{#invoke:Utilities|process_variables|%s_active|%s}}
| style="text-align:center;font-size:16px" | {{#invoke:ItemData|get_prop|%s|AbilityCooldown}}s
]=],
            bg_color, 
            item.name, 
            item.name, item.key,
            item_type, item_type,
            item.cost, item.cost,
            item.key, item.name, item.key, item.name, item.key, item.name,
            item.name)
    end
    
    wikitext = wikitext .. "\n|}"
    
    return frame:preprocess(wikitext)
end

return p