Module:Item

local item = {}

-- Load modules local fun = require("Module:Functional") local iter = fun.iter local op = fun.op

local data = require("Module:Data") local statistic = require("Module:Statistic") local se = require("Module:StatusEffect")

local CargoUtils = require("Module:CargoUtils") local DurationConverter = require("Module:DurationConverter") local Variables = require("Module:Variables")

local TableUtil = require("Module:TableUtil")

local GameUIBuilder = require("Module:GameUIBuilder") local Label = GameUIBuilder.GameUILabel local Menu = GameUIBuilder.GameUIMenu local Tabber = GameUIBuilder.GameUITabber local Table = GameUIBuilder.GameUITable

-- Item-related constants local LEVEL_SUFFIXES = { "", "I", "Mk II", "II", "Mk III", "III", "Mk IV", "IV", "Mk V", "V", "Elite", "Mk E" } local RARITIES = table.concat(iter(data.RARITY_COLORS):keys:totable, ',') local TIERS = "0.0,1.0,2.0,3.0,4.0,4.5,5.0,5.5,6.0,6.5,7A,7B,7C,8A,8B,8C" local INT_TIERS = "0,1,2,3,4,5,6,7,8" local FACTIONS = table.concat(data.FACTIONS, ',') local ROLES = table.concat(data.ROLES, ',') local CLASSES = table.concat(data.CLASSES, ',') local CATEGORIES = table.concat(iter(data.CATEGORY_ICONS):keys:totable, ',') local PARENT_CATEGORIES = table.concat(iter(data.PARENT_CATEGORY_ICONS):keys:totable, ',') local SLOTS = table.concat(iter(data.SLOT_ICONS):keys:totable, ',')

-- Used for building the item across templates on the same page local PAGE_WIDE_VAR_NAME = "item_attr"

-- Updates the item that is persisted within the page. local function updatePageWideItem(item) -- Only serialize the attributes because string.dump is not allowed local item_attr = iter(item):map(function (k, v)		if k ~= "class" and type(v) ~= "function" then return k, v else return false end	end):filter(op.truth):tomap Variables.vardefine(PAGE_WIDE_VAR_NAME, item_attr) end

-- Returns the item that is persisted within the page. -- Raises an error if it has not been defined yet, unless the `allow_undefined` option is set, in which case returns nil. local function getPageWideItem(allow_undefined) local item_attr = Variables.var(PAGE_WIDE_VAR_NAME) local undefined = (item_attr == "" or item_attr == nil) if undefined and allow_undefined then return nil end assert(not undefined, "You have to first define an item on this page before this template") return item.Item(item_attr) end

-- Mixin that enables storage of an entity's contents -- This uses the property _contains local EntityContainsMixin = {} item.EntityContainsMixin = EntityContainsMixin

-- Gets all of the entity's contents. -- The result is a table { { ["bin_type"] = bin_type, ["contains"] = { ["item"] = item, ["min_amount"] = item_min_amount, ["max_amount"] = item_max_amount } } }, which may be empty. function EntityContainsMixin:getContains local contains_table = item.getContainsTableFor(self.class.name) local contains = contains_table:query{ where = string.format("_pageName = '%s'", self._pageName) } -- Cache query result if not self._contains then self._contains = iter(contains):map(function (v)			local zipped = iter(v.contains):zip(v.min_amounts, v.max_amounts)			local contains = zipped:map(function (c, mi, ma) return {item = c, min_amount = mi, max_amount = ma} end):totable			return {				bin_type = v.bin_type,				contains = contains,			}		end):totable end return mw.clone(self._contains) end

-- Sets the entity's contents and stores them into the Cargo table. -- Also updates each content so that they are converted from strings into Lua objects, making them -- consistent with the contents obtained from querying the record. function EntityContainsMixin:storeContains(contains) self._contains = mw.clone(contains) local contains_table = item.getContainsTableFor(self.class.name) local store = "" iter(contains):enumerate:each(function (i, e)		local items, min_amounts, max_amounts = iter(e.contains):reduce(function (acc, v)			table.insert(acc[1], v.item) table.insert(acc[2], v.min_amount) table.insert(acc[3], v.max_amount) return acc end, {{}, {}, {}})		local out_query = {}		store = store .. contains_table:store({ ["bin_type"] = e.bin_type, ["contains"] = items, ["min_amounts"] = min_amounts, ["max_amounts"] = max_amounts, }, out_query)		self._contains[i] = {			["bin_type"] = out_query.bin_type,			["contains"] = out_query.items,			["min_amounts"] = out_query.min_amounts,			["max_amounts"] = out_query.max_amounts,		}	end) return store end

-- Creates a table that stores content information of an entity. -- There should be at most one instance of said entity per page. function item.getContainsTableFor(entity_type) return CargoUtils.CargoTable(string.format("%sContains", entity_type),		{			["bin_type"] = { "String", { size = 1, mandatory = true, ["allowed values"] = "*,?,1,+" }, { "NOT NULL" } },			["contains"] = { "List of String", { mandatory = true }, { "NOT NULL" } },			["min_amounts"] = { "List  of Integer" },			["max_amounts"] = { "List  of Integer" },		},		nil,		{			["_pageName"] = { entity_type, "_pageName" },			["contains"] = { "Item", "_pageName" },		}) end

-- Represents an item in the game. local Item = data.Entity:subclass("Item"):include(statistic.EntityStatsMixin):include(statistic.EntityModifiersMixin):include(item.EntityContainsMixin) item.Item = Item

Item.static.ATTRIBUTE_TABLE = CargoUtils.CargoTable("Item",	{		["name"] = { "String", { mandatory = true }, { "NOT NULL" } },		["image"] = { "File", { mandatory = true }, { "NOT NULL" } },		["rarity"] = { "String", { mandatory = true, ["allowed values"] = RARITIES }, { "NOT NULL" } },		["tier"] = { "String", { ["allowed values"] = TIERS } },		["faction"] = { "String", { ["allowed values"] = FACTIONS } },		["category"] = { "String", { ["allowed values"] = CATEGORIES } },		["description"] = { "Wikitext" },		["main_page"] = { "Page" },		["is_auto"] = { "Boolean" },		["prereq_items"] = { "List of Page" },		["prereq_researches"] = { "List  of Page" },		["prereq_blueprints"] = { "List  of Page" },		["build_helium_3"] = { "Integer" },		["build_mineral_ore"] = { "Integer" },		["build_antimatter"] = { "Integer" },		["build_seconds"] = { "Integer" },		["repair_helium_3"] = { "Integer" },		["repair_mineral_ore"] = { "Integer" }, ["repair_antimatter"] = { "Integer" }, ["repair_seconds"] = { "Integer" },

["background_image"] = { "File" }, ["weapon_slot_ids"] = { "List of Integer" }, ["hangar_slot_ids"] = { "List of Integer" }, ["spawner_slot_ids"] = { "List of Integer" }, ["armor_slot_ids"] = { "List of Integer" }, ["shield_slot_ids"] = { "List of Integer" }, ["ablative_armor_slot_ids"] = { "List of Integer" }, ["screen_slot_ids"] = { "List of Integer" }, ["special_slot_ids"] = { "List of Integer" }, ["operation_slot_ids"] = { "List of Integer" }, ["skin_slot_ids"] = { "List of Integer" }, ["trigger_slot_ids"] = { "List of Integer" }, ["resistance_slot_ids"] = { "List of Integer" }, -- Avoid overriding the reserved attribute `class` ["ship_role"] = { "String", { ["allowed values"] = ROLES } }, ["ship_class"] = { "String", { ["allowed values"] = CLASSES } }, ["firing_arc_widths"] = { "List of Float" }, ["firing_arc_offsets"] = { "List of Float" }, ["priority_target"] = { "String" }, ["priority_range"] = { "Integer" }, ["force_target_range"] = { "Integer" }, ["module_size"] = { "String", { ["allowed values"] = "1x1,1x2,2x2" } }, ["upgrade_helium_3"] = { "Integer" }, ["upgrade_helium_3"] = { "Integer" }, ["upgrade_mineral_ore"] = { "Integer" }, ["upgrade_antimatter"] = { "Integer" }, ["upgrade_seconds"] = { "Integer" }, ["equip_parent"] = { "String", { ["allowed values"] = PARENT_CATEGORIES } }, ["equip_slot"] = { "String", { ["allowed values"] = SLOTS } }, ["equip_wl_tiers"] = { "List of String", { ["allowed values"] = INT_TIERS } }, ["equip_wl_roles"] = { "List of String", { ["allowed values"] = ROLES } }, ["equip_wl_classes"] = { "List of String", { ["allowed values"] = CLASSES } }, ["equip_wl_factions"] = { "List of String", { ["allowed values"] = FACTIONS } }, ["equip_bl_tiers"] = { "List of String", { ["allowed values"] = INT_TIERS } }, ["equip_bl_roles"] = { "List of String", { ["allowed values"] = ROLES } }, ["equip_bl_classes"] = { "List of String", { ["allowed values"] = CLASSES } }, ["equip_bl_factions"] = { "List of String", { ["allowed values"] = FACTIONS } }, ["upgrades"] = { "String" }, },	{ "_pageName" }, {		["prereq_items"] = { "Item", "_pageName" }, ["prereq_researches"] = { "Research", "_pageName" }, ["prereq_blueprints"] = { "Blueprint", "_pageName" }, ["upgrades"] = { "Item", "_pageName" }, })

-- Relationship-related tables Item.static.RELATIONAL_TABLES = { ["ItemStats"] = statistic.getStatsTableFor("Item"), ["ItemModifiers"] = statistic.getModifiersTableFor("Item"), ["ItemAbilities"] = se.getAbilitiesTableFor("Item"), ["ItemContains"] = item.getContainsTableFor("Item"), }

-- Static methods -- -- Returns a new Item instance with properties according to the record stored in the given page. function Item.static:fromPageName(fullpagename) assert(type(fullpagename) == type(""), "`fullpagename` must be a string") local query_args = {where = string.format("_pageName = '%s'", fullpagename)} return Item(self.ATTRIBUTE_TABLE:query(query_args, true)[1]) end

-- If the given name has the given suffix, then returns the base name. -- Otherwise, returns `nil`. function Item.static:matchBaseNameFromDisplayNameAndSuffix(name, suffix) assert(type(name) == type(""), "`name` must be a string") assert(type(suffix) == type(""), "`suffix` must be a string") return name:match(string.format("(.*) %s$", suffix)) end

-- Finds each level of an item given `base_name` allowing for prefixes (including the namespace). -- The `default_suffix` argument is optional and specifies the suffix used in the case of no suffix (defaults to empty string). -- Returns a nested table { { suffix, item } }, ordered by suffix. Throws an exception if there is more than one item with the same suffix. function Item.static:fromBaseName(base_name, default_suffix) assert(type(base_name) == type(""), "`base_name` must be a string") default_suffix = default_suffix or "" local results = Item.ATTRIBUTE_TABLE:query{ where = string.format("name LIKE '%%%s%%'", base_name) } local found_items = iter(results):map(Item):map(function (result)		local base_name, suffix = result:getBaseNameAndLevelSuffix		if suffix and iter(LEVEL_SUFFIXES):index(suffix) then			if (mw.text.trim(suffix) == "") then suffix = default_suffix end			return suffix, result		else			return false		end	end):filter(op.truth) local suffix_priority = iter(LEVEL_SUFFIXES):enumerate:map(function (i, v) return v, i end):tomap local sorted_items = iter(found_items):map(function (suffix, item) return { suffix, item } end):totable table.sort(sorted_items, function (a, b) return (suffix_priority[a[1]] or -1) < (suffix_priority[b[1]] or -1) end) return sorted_items end

-- Finds each item that matches the given slot, tier, role, class and/or faction. -- Returns a table listing the matched items in no particular order. function Item.static:fromGeneralInfo(equip_slot, tier, role, class, faction) local where = "" if (equip_slot) then where = string.format("equip_slot = '%s'", equip_slot) end if (tier) then where = string.format("%s AND (equip_wl_tiers IS NULL OR equip_wl_tiers HOLDS '%s') AND (equip_bl_tiers IS NULL OR NOT equip_bl_tiers HOLDS '%s')", where, tier, tier) end if (role) then where = string.format("%s AND (equip_wl_roles IS NULL OR equip_wl_roles HOLDS '%s') AND (equip_bl_roles IS NULL OR NOT equip_bl_roles HOLDS '%s')", where, role, role) end if (class) then where = string.format("%s AND (equip_wl_classes IS NULL OR equip_wl_classes HOLDS '%s') AND (equip_bl_classes IS NULL OR NOT equip_bl_classes HOLDS '%s')", where, class, class) end if (faction) then where = string.format("%s AND (equip_wl_factions IS NULL OR equip_wl_factions HOLDS '%s') AND (equip_bl_factions IS NULL OR NOT equip_bl_factions HOLDS '%s')", where, faction, faction) end local results = self.ATTRIBUTE_TABLE:query{where = where} return iter(results):map(function (result) return Item:fromPageName(result._pageName) end):totable end

-- Other methods -- -- Gets the base display name and level suffix of the item. -- Note that the returned level suffix is `nil` if the item has no level suffix. function Item:getBaseNameAndLevelSuffix local name = self.name for _, suffix in ipairs(LEVEL_SUFFIXES) do		local base_name = Item:matchBaseNameFromDisplayNameAndSuffix(name, suffix) if base_name then return base_name, suffix end end return name end

-- Returns the content for a tooltip showing the stats and modifiers of the item (if available). function Item:generateTooltipContent local colored_display_name = data.colorTextByRarity(self.name, self.rarity) local stats, modifiers = self:getStatistics:get, self:getModifiers:get if not (stats or modifiers) then return Label(nil, nil, colored_display_name) end	-- Only return the display name local tooltip_menu = Menu(nil, nil, colored_display_name):css("width", "160px") if (stats) then tooltip_menu:node(stats:asGameUITable) end if (modifiers) then tooltip_menu:node(modifiers:asGameUITable) end return tooltip_menu end

-- Returns an equipment icon which can be selected by the user. -- A tooltip is also displayed whenever the user hovers over the icon. -- `size` is an integer and can be either 64 or 96 function Item:generateSelectableIcon(size) assert(size == 64 or size == 96, "`size` must be either 64 or 96") local div = mw.html.create("div"):addClass(string.format("selectable-item-icon-%d", size)):addClass("tooltip") local fullpagename, category, rarity, image = self._pageName, self.category, self.rarity, self.image assert(category == "Equipment", "Selectable icons are only supported for equipment") local span_mask = mw.html.create("span"):addClass("mask"):wikitext(string.format("", fullpagename)) local span_background = mw.html.create("span"):addClass("background"):wikitext(string.format("", rarity)) local span_item = mw.html.create("span"):addClass("item"):wikitext(string.format("", image))

local rarity_color = frame:expandTemplate{ title = "In-Game Colors", args = {rarity} } local _, level_suffix = self:getBaseNameAndLevelSuffix local span_itemlevel = mw.html.create("span"):addClass("itemlevel"):css("color", rarity_color):wikitext(level_suffix)

local span_tooltip = mw.html.create("span").addClass("tooltip-hover-content") local tooltip_menu = self:generateTooltipContent span_tooltip:node(tooltip_menu) return div:node(span_mask):node(span_background):node(span_item):node(span_itemlevel):node(span_tooltip) end

-- Returns an equipment icon displayed inside an occupied slot -- A tooltip is also displayed whenever the user hovers over the icon. function Item:generateOccupiedSlotIcon local div = mw.html.create("div"):addClass("occupied-slot-icon"):addClass("tooltip") local fullpagename, category, rarity, image = self._pageName, self.category, self.rarity, self.image assert(category == "Equipment", "Selectable icons are only supported for equipment") local span_mask = mw.html.create("span"):addClass("mask"):wikitext(string.format("", fullpagename)) local span_background = mw.html.create("span"):addClass("background"):wikitext(string.format("", rarity)) local span_item = mw.html.create("span"):addClass("item"):wikitext(string.format("", image)) local rarity_color = frame:expandTemplate{ title = "In-Game Colors", args = {rarity} } local _, level_suffix = self:getBaseNameAndLevelSuffix local span_itemlevel = mw.html.create("span"):addClass("itemlevel"):css("color", rarity_color):wikitext(level_suffix) local span_tooltip = mw.html.create("span").addClass("tooltip-hover-content") local tooltip_menu = getTooltipContent span_tooltip:node(tooltip_menu) return div:node(span_mask):node(span_background):node(span_item):node(span_itemlevel):node(span_tooltip) end

-- Module call from Template:Item Definition -- Takes in one parameter - the name of the Cargo table. If not given, declares -- `ATTRIBUTE_TABLE`. function item.declareTable( frame ) local args = require("Module:Args").getCleanArgs local name = args[1] local cargo_table if name then cargo_table = assert(Item.RELATIONAL_TABLES[name], string.format("The Cargo table '%s' is not registered in this module", name)) else cargo_table = Item.ATTRIBUTE_TABLE end return cargo_table:declare(frame) end

-- Module call from Template:Item Definition function item.define( frame ) local args = require("Module:Args").getCleanArgs if (args.prereq_items) then args.prereq_items = table.concat(iter(mw.text.split(args.prereq_items, '%s*,%s*')):map(function (e) return "Item:" .. e end):totable, ',') end if (args.prereq_researches) then args.prereq_researches = table.concat(iter(mw.text.split(args.prereq_researches, '%s*,%s*')):map(function (e) return "Research:" .. e end):totable, ',') end if (args.prereq_blueprints) then args.prereq_blueprints = table.concat(iter(mw.text.split(args.prereq_blueprints, '%s*,%s*')):map(function (e) return "Blueprint:" .. e end):totable, ',') end assert(args.stats == nil, "The argument `stats` is not allowed in the item definition. Use Template:Item Definition/stats instead") assert(args.modifiers == nil, "The argument `modifiers` is not allowed in the item definition. Use Template:Item Definition/modifiers instead") if (args.build_time) then args.build_seconds = tostring(DurationConverter.durationToSeconds(args.build_time)) args.build_time = nil end if (args.repair_time) then args.repair_seconds = tostring(DurationConverter.durationToSeconds(args.repair_time)) args.repair_time = nil end if (args.upgrade_time) then args.upgrade_seconds = tostring(DurationConverter.durationToSeconds(args.upgrade_time)) args.upgrade_time = nil end local item_to_store = Item(args) local store = item_to_store:storeToTable(Item.ATTRIBUTE_TABLE) -- Persist item page-wide so that other templates can append to it	updatePageWideItem(item_to_store) return store end

-- Checks that the given stats are not included, as they are passed elsewhere. local function assertDoesNotHaveStats(stats) iter{ "Build Helium-3", "Build Mineral Ore", "Build Antimatter", "Build Time", "Upgrade Helium-3", "Upgrade Mineral Ore", "Upgrade Antimatter", "Upgrade Time", }:each(function (s)		assert(stats[s] == nil, string.format("The argument `%s` should be passed through Template:Item Definition/playergen instead", s))	end) iter{ "Weapon Slots", "Hangar Slots", "Spawner Slots", "Armor Slots", "Shield Slots", "Ablative Armor Slots", "Screen Slots", "Special Slots", "Operation Slots", "Skin Slots", "Trigger Slots", "Resistance Slots", }:each(function (s)		assert(stats[s] == nil, string.format("The argument `%s` should be passed through Template:Item Definition/parent instead", s))	end) iter{ "Repair Helium-3", "Repair Mineral Ore", "Repair Antimatter", "Repair Time", }:each(function (s)		assert(stats[s] == nil, string.format("The argument `%s` should be passed through Template:Item Definition/module instead", s))	end) end

-- Module call from Template:Item Definition/stats function item.appendStats( frame ) local args = require("Module:Args").getCleanArgs assertDoesNotHaveStats(args) local stats = iter(args):map(function (k, v) return "Statistic:" .. k, v end):tomap -- Append to existing item local item = getPageWideItem local store = item:storeStatistics(stats) updatePageWideItem(item) return store end

-- Module call from Template:Item Definition/modifiers function item.appendModifiers( frame ) local args = require("Module:Args").getCleanArgs assertDoesNotHaveStats(args) local modifiers = iter(args):map(function (k, v)		local fullpagename = "Statistic:" .. k		local tmp = mw.text.split(modifier_info, "%s*;%s*")		local applies_to = iter(tmp):drop(2)		-- Shortcut for slot mass modifiers		if applies_to:is_null then			for _, slot in ipairs(SLOTS) do				if k:find(slot .. " Mass") then					applies_to = iter{slot}				end			end		end		if applies_to:is_null then applies_to = iter{"All"} end		return fullpagename, applies_to:map(function (e) return e, { ["value"] = tmp[1], ["type"] = tmp[2], }		end):tomap	end):tomap -- Append to existing item local item = getPageWideItem local store = item:storeModifiers(modifiers) updatePageWideItem(item) return store end

-- Module call from Template:Item Definition/abilities function item.appendAbilities( frame ) local args = require("Module:Args").getCleanArgs

local ability = { ["name"] = args.name, ["description"] = args.description, ["status_effect"] = "StatusEffect:" .. args.status_effect, }	-- Append to existing item local item = getPageWideItem local store = item:storeAbilities{ability} updatePageWideItem(item) return store end

-- Module call from Template:Item Definition/contains function item.appendContains( frame ) local args = require("Module:Args").getCleanArgs local bin_type = args.bin_type args.bin_type = nil local contain = { ["bin_type"] = bin_type, ["contains"] = iter(args):map(function (k, v)			local min_amount, max_amount = mw.text.split(v, "%s*;%s*")			return {				["item"] = "Item" .. k,				["min_amount"] = min_amount,				["max_amount"] = max_amount,			}		end):totable }	-- Append to existing item local item = getPageWideItem local store = item:storeContains{contain} updatePageWideItem(item) return store end

-- Returns a string containing the HTML for an infobox for the item local function getItemInfoboxHTML(fullpagename, general_only, no_categories) return tostring(Item:fromPageName(fullpagename):generateInfobox(general_only, no_categories)) end

-- Module call from Template:Item Infobox function item.generateInfobox( frame ) local args = require("Module:Args").getCleanArgs -- Display item stored on this page, if it exists and `full_name` is not given local new_item = getPageWideItem(true) if new_item and not args.full_name then return new_item:generateInfobox end local fullpagename = "Item:" .. assert(args.full_name, "Missing argument `full_name`") local general_only = args.general_only local no_categories = args.no_categories return getItemInfoboxHTML(fullpagename, general_only, no_categories) end

-- Module call from Template:Item Infobox Tabber function item.generateInfoboxTabber( frame ) local args = require("Module:Args").getCleanArgs local base_name = assert(args.base_name, "Missing argument `base_name`") local general_only = args.general_only local no_categories = args.no_categories local items = Item:fromBaseName(base_name, "Mk I") assert(#items > 0, string.format("No items of base name '%s' have a level suffix", base_name)) if #items == 1 then local suffix, item = items[1] local fullpagename = item._pageName return getItemInfoboxHTML(fullpagename, general_only, no_categories) else local tabber = iter(items):reduce(function (acc, inner)			local suffix, item = unpack(inner)			local fullpagename = item._pageName			local infobox = getItemInfoboxHTML(fullpagename, general_only, no_categories)			acc:insertTabAt(suffix, infobox)			return acc		end, Tabber) return mw.html.create("div"):css("width", "fit-content"):node(tabber:as_mw_html) end end

-- Compares the stats and modifiers of multiple items in sequence. -- For example, the `items[1]` is compared with `item[2]`, which in turn is compared with `item[3]`. -- Optionally, can pass in `col_headers` to specify the column headers for the items, otherwise they default to the display names of each item. -- Returns two tables: the first one for stats and the second one for modifiers. These may be nil if stats/modifiers are empty. local function getStatsModifiersProgressionTable( items, col_headers ) -- Set up headers col_headers = col_headers or iter(items):map(function (item) return item.name end):totable -- Get the data local stats, stats_comp, modifiers, modifiers_comp = unpack(iter(items):zip(col_headers):reduce(function (acc, item, col_header) local out_stats, out_stats_comp, out_modifiers, out_modifiers_comp, prev_item = unpack(acc) -- Only get stat value pairs local s = item:getStatistics:get or {} local m = item:getModifiers:get or {} table.insert(out_stats, s)		table.insert(out_modifiers, m)		-- Color the text according to the comparison results local stats_comp, modifiers_comp if prev_item == nil then -- The first item has no previous item to compare against stats_comp = iter(s):map(function (stat_fullpagename, stat_value) return stat_fullpagename, 0 end):tomap modifiers_comp = iter(m):map(function (stat_fullpagename, inner)				return stat_fullpagename, iter(inner):map(function (applies_to, v) return applies_to, 0 end):tomap			end):tomap else stats_comp = item:compareStats(prev_item) modifiers_comp = item:compareModifiers(prev_item) end table.insert(out_stats_comp, stats_comp) table.insert(out_modifiers_comp, modifiers_comp) return {out_stats, out_stats_comp, out_modifiers, out_modifiers_comp, item} end, {{}, {}, {}, {}, nil})) -- Make each item correspond to a column instead stats = TableUtil.transpose(stats) stats_comp = TableUtil.transpose(stats_comp) modifiers = TableUtil.transpose(modifiers) modifiers_comp = TableUtil.transpose(modifiers_comp) local function formatContent(stat_fullpagename, values, values_comp, is_modifier) -- Forrmat the row headers local stat = Statistic:fromPageName(stat_fullpagename) local image, main_page = stat.image, data.main_page local img = "" if (image) then img = string.format("", image) end local name = stat:getFormattedName local out_header = string.format("%s %s", img, name) -- Format the row values local function getColorKey(comp_result) if comp_result < 0 then return "Penalty" elseif comp_result > 0 then return "Bonus" else return "Statistic" end end local out_values = iter(values):zip(values_comp) if (is_modifier) then out_values = out_values:map(function (m, m_comp)				local content = iter(m):map(function (applies_to) local m_inner, m_comp_inner = m[applies_to], m_comp[applies_to] local formatted_value = stat:getFormattedValue(m_inner.value, m_inner.type, getColorKey(m_comp_inner)) return string.format("%s (Applies to: %s)", formatted_value, applies_to) end):totable				return table.concat(content, " ")			end):totable else out_values = out_values:map(function (s, s_comp)				return stat:getFormattedValue(s, nil, getColorKey(s_comp))			end):totable end return out_header, out_values end stats = iter(stats):map(function (n) return formatContent(n, stats[n], stats_comp[n]) end):tomap modifiers = iter(modifiers):map(function (n) return formatContent(n, stats[n], stats_comp[n], true) end):tomap local function convertRowFromTableToMap(row_header, row_content) return row_header, iter(col_headers):zip(row_content):tomap end local prog_stats = nil if not iter(stats):is_null then stats = iter(stats):map(convertRowFromTableToMap):tomap prog_stats = Table(nil, nil, string.format("Progression (Stats)"), stats, true) end local prog_modifiers = nil if not iter(modifiers):is_null then modifiers = iter(modifiers):map(convertRowFromTableToMap):tomap prog_modifiers = Table(nil, nil, string.format("Progression (Modifiers)"), modifiers, true) end return prog_stats, prog_modifiers end

-- Module call from Template:Item Progression function item.generateProgressionMenu( frame ) local args = require("Module:Args").getCleanArgs local base_name = assert(args.base_name, "Missing argument `base_name`") local items_nested = Item:fromBaseName(base_name) assert(#items_nested > 0, string.format("No items of base name '%s' have a level suffix", base_name)) suffixes_items = iter(items_nested):map(unpack) -- Add images to headers local col_headers = suffixes_items:map(function (suffix, item)		local image = item.image		if (image) then			return string.format("%s ", suffix, image)		else			return suffix		end	end):totable items = suffixes_items:values:totable local prog_stats, prog_modifiers = getStatsModifiersProgressionTable(items, col_headers) local children = {} if (prog_stats) then table.insert(children, prog_stats) end if (prog_modifiers) then table.insert(children, prog_modifiers) end if (#children == 0) then table.insert(children, Label({ "game-ui-error-label" }, nil, "No stats nor modifiers were found.")) end -- Color the title according to the rarity of the lowest-level version local first_item = items[1] local rarity = first_item.rarity local colored_name = data.colorTextByRarity(base_name, rarity) local menu = iter(children):reduce(function (acc, child) return acc:node(child) end,		Menu(nil, nil, string.format("%s Details", colored_name))) return menu:as_mw_html end

return item