Editing Module:Sandbox/LVLGive feedback
Jump to navigation
Jump to search
The edit can be undone.
Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
| Latest revision | Your text | ||
| 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 | -- Helper function to flatten item data into searchable text using existing Lang JSON files | ||
local | local function extract_search_terms(data, lang_code) | ||
local | -- Load the language file directly for fast, frame-less lookups | ||
local lang_file_name = string.format("Data:Lang_%s.json", lang_code or "en") | |||
local success, lang_data = pcall(mw.loadJsonData, lang_file_name) | |||
if not success then lang_data = {} end | |||
local function | local terms = {} | ||
local function recurse(tbl) | |||
if type(tbl) ~= "table" then return end | |||
for k, v in pairs(tbl) do | |||
-- Only process string keys | |||
if type(k) == "string" then | |||
-- Check if the lang file has a readable string for this key (e.g., "TechResist" -> "TechResist_label") | |||
local term = lang_data[k .. "_label"] or lang_data[k] | |||
if term and type(term) == "string" then | |||
end | -- Clean up any HTML tags or inline attributes from the localization string | ||
term = term:gsub("<[^>]+>", " ") | |||
term = term:gsub("{g:citadel_inline_attribute:'(.-)'}", "%1") | |||
table.insert(terms, string.lower(term)) | |||
else | |||
-- Fallback: Convert CamelCase to spaces (e.g., BonusHealth -> bonus health) | |||
local fallback_term = string.lower(k:gsub("(%u)", " %1"):gsub("^%s", "")) | |||
table.insert(terms, fallback_term) | |||
end | |||
end | |||
if type(v) == "table" then | |||
recurse(v) | |||
elseif type(v) == "string" then | |||
local str = v | |||
-- Clean up description HTML tags and inline attributes | |||
str = str:gsub("<[^>]+>", " ") | |||
str = str:gsub("{g:citadel_inline_attribute:'(.-)'}", "%1") | |||
str = mw.text.decode(str) | |||
table.insert(terms, string.lower(str)) | |||
elseif type(v) == "number" or type(v) == "boolean" then | |||
-- Explicitly ignore numbers and true/false values | |||
-- We only want the stat names (keys) and descriptions (strings) | |||
end | |||
end | |||
end | end | ||
recurse(data) | |||
local result = table.concat(terms, " ") | |||
result = result:gsub("%s+", " ") -- Normalize spaces | |||
return result | return result | ||
end | end | ||
-- === | --exceptions if the link is different from the item name | ||
local linkOverrides = { | |||
["Bullet Lifesteal"] = "Bullet Lifesteal (item)", | |||
["Spirit Lifesteal"] = "Spirit Lifesteal (item)" | |||
} | |||
local function | --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 | 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 | end | ||
-- | local min_souls = tonumber(min_souls) | ||
local max_souls = tonumber(max_souls) | |||
local | if min_souls == nil or max_souls == nil then return 'Min/Max souls must be numerical' end | ||
if | |||
-- 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 | 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 | 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 (names + stats + description) | |||
local search_data = string.lower(item_name_english) | |||
if lang_code ~= "en" and item_name_local then | |||
search_data = search_data .. " " .. string.lower(item_name_local) | |||
end | |||
-- Append flattened item data (stats, descriptions, shop filters) | |||
-- Pass lang_code so it can look up the proper in-game names from Lang JSON | |||
search_data = search_data .. " " .. extract_search_terms(item_data, lang_code) | |||
-- Escape double quotes so it doesn't break the HTML attribute | |||
search_data = search_data:gsub('"', '"') | |||
-- 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"> • </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 | 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 | |||
-- 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"] | |||
-- | |||
local | |||
end | 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"] | |||
local | 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 | 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 | ||
end | end | ||
-- | -- 2. Sort by name | ||
table.sort(active_items, function(a, b) return a.name < b.name end) | |||
local | -- 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 | |||
for _, | 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 | ||
wikitext = wikitext .. "\n|}" | |||
return | |||
return frame:preprocess(wikitext) | |||
end | end | ||
return p | return p | ||