Module:Statistic

local statistic = {}

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

local class = require("Module:Class") local data = require("Module:Data")

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

local GameUIBuilder = require("Module:GameUIBuilder") local List = GameUIBuilder.GameUIList local Tabber = GameUIBuilder.GameUITabber

local GameUITemplates = require("Module:GameUITemplates") local BlockInfobox = GameUITemplates.GameUIBlockInfobox local EditButton = GameUITemplates.GameUIEditButton local HideStatsButton = GameUITemplates.GameUIHideStatsButton local HideModifiersButton = GameUITemplates.GameUIHideModifiersButton local InfoboxDescription = GameUITemplates.GameUIInfoboxDescription local InfoboxImage = GameUITemplates.GameUIInfoboxImage local SidebarInfobox = GameUITemplates.GameUISidebarInfobox local ShowStatsButton = GameUITemplates.GameUIShowStatsButton local ShowModifiersButton = GameUITemplates.GameUIShowModifiersButton

-- Legacy page for compatibility, will be deprecated in the future local legacyStat = require("Module:Statistic/Legacy") for k, v in pairs(legacyStat) do	statistic[k] = v end

-- Wrapper that represents an list of statistics in the game. StatsList = class("StatsList")

-- Constructor -- -- Creates a new StatsList instance according to the given table { fullpagename -> value }, which may be empty. -- Use an empty string (instead of nil) to denote an unknown value. function StatsList:initialize(stats) stats = stats or {} self._stats = mw.clone(stats) end

-- Getters & Setters -- -- If `fullpagename` is nil, returns the set of stats in the list. -- Otherwise, returns the stat in the list identified by `fullpagename`. function StatsList:get(fullpagename) if fullpagename == nil then return mw.clone(self._stats) else if type(fullpagename) ~= "string" then error(string.format("`fullpagename` must be nil or a string, but found %s", mw.dumpObject(fullpagename))) end return self._stats[fullpagename] end end

-- Static methods -- -- Appends the HTML display of a statistic/value pair to a GameUIList object. -- `flags` for this method include "hide_if_js" and "use_colon". -- `existing_stat_def` is optional. Pass in a Statistic object to use it in place of a query. function StatsList.static:appendStatisticRowToGameUIList(list, stat_fullpagename, stat_value, flags, existing_stat_def) if type(list) ~= "table" then error(string.format("`list` must be a table, but found %s", mw.dumpObject(list))) end if type(stat_fullpagename) ~= "string" then error(string.format("`stat_fullpagename` must be a string, but found %s", mw.dumpObject(stat_fullpagename))) end flags = flags or {} local stat = existing_stat_def or Statistic:fromPageName(stat_fullpagename) if stat_value == "" then stat_value = nil end local args = { ["image_wikitext"] = string.format("", stat.image), ["left_wikitext"] = stat:getFormattedName, ["right_wikitext"] = stat:getFormattedValue(stat_value), ["tooltip_wikitext"] = stat:getFormattedDescription(stat_value), ["no_js_only"] = flags.hide_if_js, }

if (flags.use_colon) then args.left_wikitext = string.format("%s: %s", args.left_wikitext, args.right_wikitext) args.right_wikitext = nil end list:insertRowAt(args) return list end

-- Returns the header and content for a statistic/value pair as a row in a GameUITable object. -- Optionally, specify `num_whitespace` to add whitespaces to the image definition in order to avoid name collisions yet keep the visuals the same, -- since Mediawiki will automatically trim them. function StatsList.static:getStatisticRowForGameUITable(stat_fullpagename, stat_value, num_whitespace) if type(stat_fullpagename) ~= "string" then error(string.format("`stat_fullpagename` must be a string, but found %s", mw.dumpObject(stat_fullpagename))) end local stat = Statistic:fromPageName(stat_fullpagename) return string.format(" %s", stat.image, string.rep(' ', num_whitespace), stat.name), { ["Value"] = stat:getFormattedValue(stat_value) } end

-- Appends the HTML display of a group/stats pair to a GameUIList object. -- `flags` for this method include "use_colon". function StatsList.static:appendGroupRowToGameUIList(list, group_name, group_stats, flags) if type(list) ~= "table" then error(string.format("`list` must be a table, but found %s", mw.dumpObject(list))) end if type(group_name) ~= "string" then error(string.format("`group_name` must be a string, but found %s", mw.dumpObject(group_name))) end flags = flags or {} local group = StatisticGroup:fromDisplayName(group_name) local args = { ["image_wikitext"] = string.format("", group.image), ["left_wikitext"] = group:getFormattedName, ["right_wikitext"] = group:getFormattedValue(group_stats), ["tooltip_wikitext"] = group:getFormattedDescription(group_stats), }

if (flags.use_colon) then args.left_wikitext = string.format("%s: %s", args.left_wikitext, args.right_wikitext) args.right_wikitext = nil end list:insertRowAt(args) -- Fallback in case of no-JS if (group.value_format) then iter(group:orderStatsValues(group_stats)):map(unpack):each(function (stat_fullpagename, stat_value)			self:appendStatisticRowToGameUIList(list, stat_fullpagename, stat_value, { use_colon = flags.use_colon, hide_if_js = true, })		end) end return list end

-- Other Methods -- -- Returns the visual representation of the list of stats as a GameUIList object. -- Groups stats together when the group has at least two members, except for the group with name `skip_group_name` which will not be grouped at all. -- `flags` for this method include "use_colon". function StatsList:asGameUIList(skip_group_name, flags) flags = flags or {} -- Assign stats to groups -- { fullpagename -> { groups } }, note that {} means the stats has already been -- assigned to all applicable groups, which is different from a nil value which -- means that the stat does not belong to any group. local stat_fullpagenames = iter(self._stats):keys:totable local candidate_groups = iter(StatisticGroup:fromStats(stat_fullpagenames, true)):filter(function (group) return group.name ~= skip_group_name end) local stats_assignment_status = candidate_groups:reduce(function (acc, group)		local children = group.children		-- Find stats that belong to the group		local stats_in_group = iter(self._stats):keys:filter(function (stat_fullpagename) return acc[stat_fullpagename] == nil and iter(children):any(function (child) return stat_fullpagename == child end) end):totable

-- Assign stats to group if it can be displayed properly if #stats_in_group == #children or not group.value_format then iter(stats_in_group):each(function (stat_fullpagename)				acc[stat_fullpagename] = acc[stat_fullpagename] or {}				table.insert(acc[stat_fullpagename], group)			end) end return acc end, {})	-- Display stats and groups	return iter(self._stats):reduce(function (acc, stat_fullpagename, stat_value) local output, stats_assign_status = unpack(acc) -- Check whether the stat has been assigned to a group if (stats_assign_status[stat_fullpagename] == nil) then -- Not assigned to group: Display the stat only StatsList:appendStatisticRowToGameUIList(output, stat_fullpagename, stat_value, { use_colon = flags.use_colon }) else -- Assigned to group: Display the full group iter(stats_assign_status[stat_fullpagename]):each(function (group)				-- Get the stats that are assigned to the group				local stat_fullpagenames_to_assign = {}				iter(group.children):each(function (child) local ind = iter(stats_assign_status[child]):index(group) if ind then table.insert(stat_fullpagenames_to_assign, child) table.remove(stats_assign_status[child], ind)	-- To avoid displaying the same group twice end end)				-- Display all of the stats that are assigned to the group				local stats_values = iter(stat_fullpagenames_to_assign):map(function (stat_fullpagename) return stat_fullpagename, self._stats[stat_fullpagename] end):tomap				StatsList:appendGroupRowToGameUIList(output, group.name, stats_values, { use_colon = flags.use_colon })			end) end return {output, stats_assign_status} end, {List, stats_assignment_status})[1] end

-- Returns the visual representation of the list of stats as a GameUITable object. function StatsList:asGameUITable local tbl_content = iter(self._stats):map(getStatisticRowForGameUITable):tomap return Table(nil, nil, "Statistic", tbl_content) end

-- Creates a table that stores stat information of an entity. -- There should be at most one instance of said entity per page. function statistic.getStatsTableFor(entity_type) return CargoUtils.CargoTable(string.format("%sStats", entity_type),		{			["statistic"] = { "String", { mandatory = true }, { "NOT NULL" } },			["value"] = { "Float" },		},		{ "_pageName", "statistic" },		{			["_pageName"] = { entity_type, "_pageName" },			["statistic"] = { "Statistic", "_pageName" },		}) end

-- Mixin that enables storage of an entity's stats -- This uses the property _stats (see StatsList for more information) local EntityStatsMixin = {}

-- Gets all of the entity's statistics. -- The result is a StatsList instance. function EntityStatsMixin:getStatistics local stats_table = statistic.getStatsTableFor(self.class.name) local stats = stats_table:query{ where = string.format("_pageName = '%s'", self._pageName) } -- Cache query result if not self._stats then self._stats = iter(stats):map(function (v) return v.statistic, v.value end):tomap end return StatsList(self._stats) end

-- Sets the entity's statistics and stores them into the Cargo table. -- Also updates each statistic so that they are converted from strings into Lua objects, making them -- consistent with the statistics obtained from querying the record. function EntityStatsMixin:storeStatistics(stats) self._stats = mw.clone(stats) local stats_table = statistic.getStatsTableFor(self.class.name) local store = "" iter(stats):each(function (fullpagename, value)		local out_query = {}		store = store .. stats_table:store({ ["statistic"] = fullpagename, ["value"] = value, }, out_query)		self._stats[out_query.statistic] = out_query.value	end) return store end

-- Compares two entities according to their stats. -- Returns a table { fullpagename -> compare_result } -- compare_result is -1 if the value of the stat in the first object is worse, 1 if it is better, otherwise 0. function EntityStatsMixin:compareStats(other) local self_stats = self:getStatistics:get or {} local other_stats = other:getStatistics:get or {} local function getValueRelation(stat_fullpagename) relation = Statistic:fromPageName(stat_fullpagename).value_relation if relation == '+' then return 1 end if relation == '-' then return -1 end return 0 end local result = iter(self_stats):map(function (stat_fullpagename, value1)		local value2, comp_result = other_stats[stat_fullpagename], 0		if value2 ~= nil then			-- Note: String comparisons are allowed here			if value1 < value2 then				comp_result = -1 * getValueRelation(stat_fullpagename)			elseif value1 > value2 then				comp_result = 1 * getValueRelation(stat_fullpagename)			end		end		return stat_fullpagename, comp_result	end):tomap -- Handle the case where a field appears in other but not self return iter(other_stats):map(function (stat_fullpagename, _) return stat_fullpagename, 0 end):chain(result):tomap end

-- Creates a table that stores modifier information of an entity. -- There should be at most one instance of said entity per page. function statistic.getModifiersTableFor(entity_type) return CargoUtils.CargoTable(string.format("%sModifiers", entity_type),		{			["statistic"] = { "String", { mandatory = true }, { "NOT NULL" } },			["value"] = { "Float" },			["type"] = { "String", { ["allowed values"] = "Constant,CoeffBase,CoeffCurrent,Min,Max" } },			["applies_to"] = { "List of String", { ["allowed values"] = iter(data.APPLIES_TO_STATS):chain(data.APPLIES_TO_NAME_SUBSTR):keys:totable } },		},		{ "_pageName", "statistic" },		{			["_pageName"] = { entity_type, "_pageName" },			["statistic"] = { "Statistic", "_pageName" },		}) end

-- Wrapper that represents a list of modifiers in the game. ModifiersList = class("ModifiersList")

-- Constructor -- -- Creates a new ModifiersList instance according to the given table { fullpagename -> { applies_to -> { ["value"] -> value, ["type"] = type } } }, which may be empty. -- Use an empty string (instead of nil) to denote an unknown stat. function ModifiersList:initialize(modifiers) modifiers = modifiers or {} self._modifiers = mw.clone(modifiers) end

-- Getters & Setters -- -- If `fullpagename` is nil, returns the set of modifiers for all statistics in the list. -- Otherwise, returns the set of modifiers in the list for the statistic identified by `fullpagename`. function ModifiersList:get(fullpagename) if fullpagename == nil then return mw.clone(self._modifiers) else if type(fullpagename) ~= "string" then error(string.format("`fullpagename` must be nil or a string, but found %s", mw.dumpObject(fullpagename))) end return mw.clone(self._modifiers[fullpagename]) end end

-- Returns the formatted string for the value of "Applies To" function ModifiersList.static:getFormattedAppliesTo(applies_to) if type(applies_to) ~= "string" then error(string.format("`applies_to` must be a string, but found %s", mw.dumpObject(applies_to))) end return mw.getCurrentFrame:expandTemplate{ title = "Colored Text", args = { applies_to, "Statistic" } } end

-- Appends the HTML display of a name/value/type/applies_to tuple to a GameUIList object. -- `flags` for this method include "hide_if_js" and "use_colon". function ModifiersList.static:appendModifierRowToGameUIList(list, stat_fullpagename, modifier_value, modifier_type, applies_to, flags) if type(list) ~= "table" then error(string.format("`list` must be a table, but found %s", mw.dumpObject(list))) end if type(stat_fullpagename) ~= "string" then error(string.format("`stat_fullpagename` must be a string, but found %s", mw.dumpObject(stat_fullpagename))) end flags = flags or {} local stat = Statistic:fromPageName(stat_fullpagename) if modifier_value == "" then modifier_value = nil end local args = { ["image_wikitext"] = string.format("", stat.image), ["left_wikitext"] = stat:getFormattedName, ["right_wikitext"] = stat:getFormattedValue(modifier_value, modifier_type), ["tooltip_wikitext"] = stat:getFormattedDescription(modifier_value), ["no_js_only"] = flags.hide_if_js, }

if (flags.use_colon) then args.left_wikitext = string.format("%s: %s", args.left_wikitext, args.right_wikitext) if (applies_to and applies_to ~= "All") then args.right_wikitext = string.format("Applies to: %s", self:getFormattedAppliesTo(applies_to)) else args.right_wikitext = nil end else if (applies_to and applies_to ~= "All") then args.right_wikitext = string.format("%s (Applies to: %s)", args.right_wikitext, self:getFormattedAppliesTo(applies_to)) end end list:insertRowAt(args) return list end

-- Returns the header and content for a name/value/type/applies_to tuple as a row in a GameUITable object. -- Optionally, specify `num_whitespace` to add whitespaces to the image definition in order to avoid name collisions yet keep the visuals the same, -- since Mediawiki will automatically trim them function ModifiersList.static:getModifierRowForGameUITable(stat_fullpagename, modifier_value, modifier_type, applies_to, num_whitespace) if type(stat_fullpagename) ~= "string" then error(string.format("`stat_fullpagename` must be a string, but found %s", mw.dumpObject(stat_fullpagename))) end local stat = Statistic:fromPageName(stat_fullpagename) return string.format(" %s", stat.image, string.rep(' ', num_whitespace), stat.name), { ["Value"] = stat:getFormattedValue(modifier_value, modifier_type), ["Applies To"] = stat:getFormattedValue(applies_to), } end

-- Other Methods -- -- Returns the visual representation of the list of modifiers as a GameUIList object. -- `flags` for this method include "use_colon". function ModifiersList:asGameUIList(flags) modifiers = mw.clone(self._modifiers) -- List global modifiers first local global_modifiers = modifiers["All"] or {} modifiers["All"] = nil local list = iter(global_modifiers):reduce(function (acc, stat_fullpagename, v)		ModifiersList:appendModifierRowToGameUIList(acc, stat_fullpagename, v.value, v.type, applies_to, { use_colon = flags.use_colon })		return acc	end, List) -- Group by `applies_to` for applies_to, inner in pairs(modifiers) do		if flags.use_colon then iter(inner):each(function (stat_fullpagename, v)				ModifiersList:appendModifierRowToGameUIList(list, stat_fullpagename, v.value, v.type, applies_to, { use_colon = true })			end) else local image = "" local substr = applies_to:gsub(",", " ") local title = substr .. " Modifiers" local description = (tostring("These modifiers only apply to " .. substr .. "s."):gsub("Torpedos", "Torpedoes")) local modifiers_table = iter(inner):map(function (stat_fullpagename, v) return { stat_fullpagename, v.value, v.type } end):totable list:insertRowAt{ ["image_wikitext"] = image, ["left_wikitext"] = title, ["right_wikitext"] = ShowModifiersButton(title, description, modifiers_table):as_mw_html, ["js_only"] = true, }			-- Fallback in case of no-JS list:insertRowAt{ ["image_wikitext"] = image, ["left_wikitext"] = "Applies to", ["right_wikitext"] = substr, ["no_js_only"] = true, }			iter(inner):each(function (stat_fullpagename, v)				ModifiersList:appendModifierRowToGameUIList(list, stat_fullpagename, v.value, v.type, nil, { hide_if_js = true })			end) end end return list end

-- Returns the visual representation of the list of modifiers as a GameUITable object. function ModifiersList:asGameUITable local tbl_content = iter(self._modifiers):map(function (stat_fullpagename, inner)		return iter(inner):enumerate:map(function (i, applies_to, v)			return ModifiersList:getModifierRowForGameUITable(stat_fullpagename, v.value, v.type, applies_to, i - 1) end)	end):flat(1):tomap return Table(nil, nil, "Modifier", tbl_content) end

-- Mixin that enables storage of an entity's modifiers -- This uses the property _modifiers (see ModifiersList for more information) local EntityModifiersMixin = {}

-- Gets all of the entity's modifiers. -- The result is a ModifiersList instance. -- The default `applies_to` value is "All". function EntityModifiersMixin:getModifiers local modifiers_table = statistic.getModifiersTableFor(self.class.name) local modifiers = modifiers_table:query{ where = string.format("_pageName = '%s'", self._pageName) } -- Cache query result if not self._modifiers then self._modifiers = {} iter(modifiers):each(function (v)			self._modifiers[v.statistic] = self._modifiers[v.statistic] or {}			self._modifiers[v.statistic][v.applies_to or "All"] = {				["value"] = v.value,				["type"] = v.type,			}		end) end return ModifiersList(self._modifiers) end

-- Sets the entity's modifiers and stores them into the Cargo table. -- Also updates each modifiers so that they are converted from strings into Lua objects, making them -- consistent with the modifiers obtained from querying the record. function EntityModifiersMixin:storeModifiers(modifiers) self._modifiers = mw.clone(modifiers) local modifiers_table = statistic.getModifiersTableFor(self.class.name) local store = "" iter(modifiers):each(function (fullpagename, inner)		iter(inner):each(function (applies_to, inner2) if applies_to == "All" then applies_to = nil end local out_query = {} store = store .. modifiers_table:store({				["statistic"] = fullpagename,				["value"] = inner2.value,				["type"] = inner2.type,				["applies_to"] = applies_to,			}, out_query) self._modifiers[fullpagename][applies_to][out_query.stat] = { ["type"] = out_query.type, ["value"] = out_query.value, }		end)	end) return store end

-- Compares two entities according to their modifiers. -- Returns a table { fullpagename -> { applies_to -> compare_result } } -- compare_result is -1 if the modifier of the stat in the first object is worse, 1 if it is better, otherwise 0. function EntityModifiersMixin:compareModifiers(other) local self_modifiers = self:getModifiers:get or {} local other_modifiers = other:getModifiers:get or {} local function getValueRelation(stat_fullpagename) relation = Statistic:fromPageName(stat_fullpagename).value_relation if relation == '+' then return 1 end if relation == '-' then return -1 end return 0 end local result = iter(self_modifiers):map(function (stat_fullpagename, inner1)		result[stat_fullpagename] = {}		local inner2 = other_modifiers[stat_fullpagename]		return stat_fullpagename, iter(inner1):map(function (applies_to, v1) local value1, comp_result = v1.value, 0 if inner2 ~= nil then local value2 = inner2[applies_to].value if value2 ~= nil then -- Note: String comparisons are allowed here if value1 < value2 then comp_result = -1 * getValueRelation(stat_fullpagename) elseif value1 > value2 then comp_result = 1 * getValueRelation(stat_fullpagename) end end end return applies_to, comp_result end):tomap	end):tomap -- Handle the case where a field appears in other but not self return iter(other_modifiers):map(function (stat_fullpagename, inner2)		local inner1 = result[stat_fullpagename]		return stat_fullpagename, iter(inner2):map(function (applies_to, _) return applies_to, 0 end):chain(inner1):tomap	end):chain(result):tomap end

-- Represents a statistic in the game. Statistic = data.Entity:subclass("Statistic")

Statistic.static.ATTRIBUTE_TABLE = CargoUtils.CargoTable("Statistic",			{				["name"] = { "String", { mandatory = true }, { "NOT NULL" } },				["image"] = { "File", { mandatory = true }, { "NOT NULL" } },				["value_type"] = { "String", { mandatory = true }, { "NOT NULL" } },				["value_relation"] = { "String", { size = 1, mandatory = true, ["allowed values"] = "+,-,/" } },				["value_units"] = { "String", { mandatory = true }, { "NOT NULL" } },				["tooltip_format"] = { "String",  { mandatory = true }, { "NOT NULL" } },				["main_page"] = { "Page" },			},			{ "_pageName" },			{})

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

-- Colors the text according to predefined keywords. function Statistic.static:applyColors(text) if type(text) ~= "string" then error(string.format("`text` must be a string, but found %s", mw.dumpObject(text))) end -- Split into words local orig_words = mw.text.split(text, ' ') local new_words = iter(orig_words):reduce(function (acc, word)		local output, prev_word = unpack(acc)		local new_word = word		-- Damage types to match against		for _, v in ipairs({ "Energy", "Explosive", "Kinetic", "Alien", "Plasma", "Blight", "Void" }) do			if (word == v) then				-- Do not color "Shield Energy" and "Screen Energy"				if not (prev_word == "Shield" or prev_word == "Screen") then					-- Color the basic damage type					new_word = mw.getCurrentFrame:expandTemplate{ title = "Colored Text", args = { word, v } }					break				end			elseif (word == "Nebula" and prev_word == v) then				-- Color "Nebula" if the previous word is a damage type				new_word = mw.getCurrentFrame:expandTemplate{ title = "Colored Text", args = { "Nebula", v } }				break			end		end		-- Append to output		output = output or {}		table.insert(output, new_word)		return {output, word}	end, {nil, nil})[1] return table.concat(new_words, ' ') end

-- Other methods -- -- Returns the formatted stat name. function Statistic:getFormattedName local colored_name = Statistic:applyColors(self.name) if (self.main_page) then return string.format("%s", self.main_page, colored_name) else return colored_name end end

-- Returns the formatted stat value according to additional information. -- `modifier_type` is nil if the stat is not displayed as a modifier. -- You can force a stat's color by specifing `color_key`. function Statistic:getFormattedValue(value, modifier_type, color_key) local value_units = self.value_units or "" local value_relation = self.value_relation or '/' local prefix, suffix, suffix_no_color, categories = "", "", "", "" -- Get prefix local numeric_value = tonumber(value) if (modifier_type) and (numeric_value) then if modifier_type == "Min" then prefix = "Decrease to a maximum of " elseif modifier_type == "Max" then prefix = "Increase to a minimum of " elseif numeric_value > 0 then prefix = "+" end end -- Modify value and get suffix if (numeric_value) then if (not modifier_type) or (modifier_type == "Constant") or (modifier_type == "Min") or (modifier_type == "Max") then if value_units:find("Percentage") then suffix = value_units:gsub("Percentage", "%%") elseif value_units == "Duration" then value = DurationConverter.formatInGameTime(numeric_value) elseif value_units ~= "None" and value_units ~= "Duration" then suffix_no_color = string.format(" %s", value_units) end else if numeric_value < 1 then numeric_value = numeric_value * 100 suffix = "%" end end if value_units ~= "Duration" then value = mw.language.new('en'):formatNum(numeric_value) end end if modifier_type == "CoeffBase" then suffix = suffix .. " (base)" elseif modifier_type == "CoeffCurrent" then suffix = suffix .. " (current)" end -- Get color key if (not color_key) then if (modifier_type and numeric_value) then if (numeric_value == 0 or value_relation == '/') then color_key = "Statistic" elseif ((numeric_value > 0 and value_relation == '+') or (numeric_value < 0 and value_relation == '-')) then color_key = "Bonus" else color_key = "Penalty" end else color_key = "Statistic" end end -- Get categories if (not value) then value = "???" categories = "" end return mw.getCurrentFrame:expandTemplate{ title = "Colored Text", args = { prefix .. value .. suffix, color_key } } .. suffix_no_color .. categories end

-- Returns the formatted description according to the value of a stat. function Statistic:getFormattedDescription(value) -- Obtain the formatted value local colored_format = Statistic:applyColors(self.tooltip_format) local formatted_value = self:getFormattedValue(value) return string.format(colored_format, formatted_value) end

-- Override base class method function Statistic:generateInfobox local edit_url = mw.getCurrentFrame:preprocess(string.format("", self._pageName)) local infobox = SidebarInfobox(self.name, EditButton(edit_url)) infobox:node(InfoboxImage(self.image)) local tabber, list = Tabber, List StatsList:appendStatisticRowToGameUIList(list, self._pageName, "", nil, self) tabber:insertTabAt("Sample Display", list) infobox:node(tabber) return infobox:as_mw_html end

-- Represents a group of statistics in the game. StatisticGroup = data.Entity:subclass("StatisticGroup")

-- Class-related tables StatisticGroup.static.ATTRIBUTE_TABLE = CargoUtils.CargoTable("StatisticGroup",			{				["name"] = { "String", { mandatory = true }, { "NOT NULL" } },				["image"] = { "File", { mandatory = true }, { "NOT NULL" } },				["tooltip"] = { "String", { mandatory = true }, { "NOT NULL" } },				["children"] = { "List of Page" },				["value_format"] = { "String" },				["tooltip_format"] = { "String" },			},			{ "name" },			{				["children"] = { "Statistic", "_pageName" },			})

-- Static methods -- -- Returns a new Statistic instance with properties according to the record with -- the given display name. function StatisticGroup.static:fromDisplayName(name) if type(name) ~= "string" then error(string,format("`name` must be a string, but found %s", mw.dumpObject(name))) end local query_args = {where = string.format("name = '%s'", name)} return StatisticGroup(self.ATTRIBUTE_TABLE:query(query_args, true)[1]) end

-- Returns a table containing the groups that include at least one of the elements -- in `stat_fullpagenames`. Returns an empty table if no groups are found. -- If `require_multiple_stats` evaluates to `true`, then the groups must contain -- at least two of the given statistics. function StatisticGroup.static:fromStats(stat_fullpagenames, require_multiple_stats) if type(stat_fullpagenames) ~= "table" then error(string.format("`stat_fullpagenames` must be a table, but found %s", mw.dumpObject(stat_fullpagenames))) end local where_holds = iter(stat_fullpagenames):map(function (e) return string.format("children HOLDS '%s'", e) end):totable local min_stats_in_group = 1 if require_multiple_stats then min_stats_in_group = 2 end local results = self.ATTRIBUTE_TABLE:query{ where = table.concat(where_holds, " OR "), groupBy = table.concat(self.ATTRIBUTE_TABLE:getPrimaryKey, ","), having = string.format("COUNT(*) >= %d", min_stats_in_group), }	return iter(results):map(function (e) return self:fromDisplayName(e.name) end):totable end

-- Override base class method function StatisticGroup:generateInfobox -- Convert to tables if (type(self.children) == type("")) then children = mw.text.split(self.children, "%s*,%s*") else children = self.children end local edit_url = mw.getCurrentFrame:preprocess(string.format("", self._pageName)) local infobox = SidebarInfobox(self.name, EditButton(edit_url)) infobox:node(InfoboxImage(self.image)) local tabber = Tabber local list = iter(children):reduce(function (acc, v)		StatsList:appendStatisticRowToGameUIList(acc, v, "")		return acc	end, List) tabber:insertTabAt("Stats", list) infobox:node(tabber)

return infobox:as_mw_html end

-- Other methods -- -- Returns the formatted stat group name. function StatisticGroup:getFormattedName local colored_name = Statistic:applyColors(self.name) if (self._pageName) then return string.format("%s", self._pageName, colored_name) else return colored_name end end

-- Returns a list containing each statistic/value pair in order of the statistic -- group's children, skipping those with no value. function StatisticGroup:orderStatsValues(values) if type(values) ~= "table" or iter(values):is_null then error(string.format("`values` must be a nested table, but found %s", mw.dumpObject(values))) end return iter(self.children):map(function (stat_fullpagename) return {stat_fullpagename, values[stat_fullpagename]} end):totable end

-- Returns the formatted stat value for each stat in a stat group. -- `values` is a table { stat_fullpagename -> value }. function StatisticGroup:getFormattedValue(values) if type(values) ~= "table" or iter(values):is_null then error(string.format("`values` must be a nested table, but found %s", mw.dumpObject(values))) end local ordered_values = iter(self:orderStatsValues(values)) if self.value_format then -- Display on one row local formatted_values = ordered_values:map(unpack):map(function (stat_fullpagename, stat_value)			return Statistic:fromPageName(stat_fullpagename):getFormattedValue(stat_value)		end):totable return string.format(self.value_format, unpack(formatted_values)) else -- Display in popup block return ShowStatsButton(self.name, ordered_values):as_mw_html end end

-- Returns the formatted description according to the values for each stat in a stat group. -- `values` is a table { stat_fullpagename -> value }. function StatisticGroup:getFormattedDescription(values) if type(values) ~= "table" or iter(values):is_null then error(string.format("`values` must be a nested table, but found %s", mw.dumpObject(values))) end if self.tooltip_format then local colored_format = Statistic:applyColors(self.tooltip_format) local formatted_values = iter(self:orderStatsValues(values)):map(unpack):map(function (stat_fullpagename, stat_value)			return Statistic:fromPageName(stat_fullpagename):getFormattedValue(stat_value)		end):totable return string.format(colored_format, unpack(formatted_values)) else return self.tooltip end end

-- Module call from Template:Statistic Definition function statistic.declareTable( frame ) local args = require("Module:Args").getCleanArgs return Statistic.ATTRIBUTE_TABLE:declare(frame) end

-- Module call from Template:Statistic Definition function statistic.define( frame ) local args = require("Module:Args").getCleanArgs if (args.value_type:match("List %((.*)%) of (%w*)")) then error("'value_type' cannot be a list.") end local stat_to_store = Statistic(args) local store = stat_to_store:storeToTable(Statistic.ATTRIBUTE_TABLE) return store .. tostring(stat_to_store:generateInfobox) end

-- Module call from Template:Statistic Group Definition function statistic.declareGroupTable( frame ) local args = require("Module:Args").getCleanArgs return StatisticGroup.ATTRIBUTE_TABLE:declare(frame) end

-- Module call from Template:Statistic Group Definition function statistic.defineGroup( frame ) local args = require("Module:Args").getCleanArgs if (args.children) then local children_tbl = mw.text.split(args.children, ',') args.children = table.concat(iter(children_tbl):map(function (e) return "Statistic:" .. mw.text.trim(e) end):totable, ',') end local group = StatisticGroup(args) local infobox = "" if (not args.hide_infobox) then group._pageName = frame:preprocess("") infobox = group:generateInfobox group._pageName = nil end return group:storeToTable(StatisticGroup.ATTRIBUTE_TABLE) .. tostring(infobox) end

-- Module call from Template:Statistic Infobox function statistic.generateInfobox( frame ) local args = require("Module:Args").getCleanArgs local fullpagename = "Statistic:" .. assert(args.full_name, "Missing argument `full_name`") local general_only = args.general_only local no_categories = args.no_categories return Statistic:fromPageName(fullpagename):generateInfobox(general_only, no_categories) end

-- Module call from JavaScript function statistic.generateStatisticsBlock( frame ) local args = require("Module:Args").getCleanArgs local title = assert(args[1], "Missing argument #1") assert(args[2], "Missing argument #2") local stats_table = {} local i = 2 while (args[i] ~= nil) do		Array2DParser.appendRow(stats_table, args[i]) i = i + 1 end local group = StatisticGroup:fromDisplayName(title) local block = BlockInfobox(group:getFormattedName, HideStatsButton) block:node(InfoboxDescription(group.tooltip)) local stats = iter(stats_table):map(function (v) return v[1], v[2] end):tomap local list = StatsList(stats):asGameUIList(group.name, { ["use_colon"] = true }) block:node(list) return block:as_mw_html end

-- Module call from JavaScript function statistic.generateModifiersBlock( frame ) local args = require("Module:Args").getCleanArgs local title = assert(args[1], "Missing argument #1") local description = assert(args[2], "Missing argument #2") assert(args[3], "Missing argument #3") local mods_table = {} local i = 3 while (args[i] ~= nil) do		Array2DParser.appendRow(mods_table, args[i]) i = i + 1 end local block = BlockInfobox(title, HideModifiersButton) block:node(InfoboxDescription(description)) local modifiers = iter(modifiers_table):map(function (v)		return v[1], { [v[2]] = { ["value"] = v[3], ["type"] = v[4] } }	end):tomap local list = ModifiersList(modifiers):asGameUIList{ ["use_colon"] = true } block:node(list) return block:as_mw_html end

-- Export classes statistic.StatsList = StatsList statistic.EntityStatsMixin = EntityStatsMixin statistic.ModifiersList = ModifiersList statistic.EntityModifiersMixin = EntityModifiersMixin statistic.Statistic = Statistic statistic.StatisticGroup = StatisticGroup

return statistic