Module:ParentItem

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

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

local Item = require("Module:Item").Item

local GameUIBuilder = require("Module:GameUIBuilder") local Menu = GameUIBuilder.GameUIMenu local Table = GameUIBuilder.GameUITable

local SLOTS = iter(data.SLOT_ICONS):keys:totable

-- From subpage local grid_pos = mw.loadData("Module:ParentItem/slotpos") local ROW_POSITIONS = grid_pos["rows"] local COL_POSITIONS = grid_pos["cols"]

-- Module call from documentation pages -- Returns a table listing the ID and definition for each slot position for an item local function generateSlotPositionTable( frame ) local rows_tbl = Table(nil, nil, "Row Number", iter(ROW_POSITIONS):map(function (e) return {["relative_top"] = e} end):totable) rows_tbl:sortCols local cols_tbl = Table(nil, nil, "Column Number", iter(COL_POSITIONS):map(function (e) return {["relative_right"] = e} end):totable) cols_tbl:sortCols local menu = Menu(nil, nil, "Grid Positions") menu:node(rows_tbl):node(cols_tbl) return menu:as_mw_html end

-- Holds a piece of equipment local Slot = class("Slot")

-- Constructor -- -- Creates a new Slot instance with the given type, id -- and optionally the content (an Item instance) that it is holding.get function Slot:initialize(slot_type, slot_id, content) assert(slot_type, "`slot_type` cannot be nil") assert(iter(SLOTS):any(function (e) return e == slot_type end), string.format("Invalid `slot_type` %s", slot_type)) self.slot_type = slot_type assert(slot_id, "`slot_id` cannot be nil") assert(slot_id:len == 5, "`slot_id` must be 5 characters long") local layer, row, col = slot_id:sub(1, 1), slot_id:sub(2, 3), slot_id:sub(4, 5) assert(layer == "0" or layer == "1", string.format("The layer number in `slot_id` must be either 0 or 1, but found %s", layer)) assert(ROW_POSITIONS[row] ~= nil, string.format("The row number in `slot_id` is invalid. (Found: %s)", row)) assert(COL_POSITIONS[col] ~= nil, string.format("The column number in `slot_id` is invalid. (Found: %s)", col)) self.layer, self.row, self.col = layer, row, col self.relative_top, self.relative_right = ROW_POSITIONS[row], COL_POSITIONS[col] self.content = content end

-- An Item capable of combining the properties of equipped items local ParentItem = Item:subclass("ParentItem")

-- The names of the aggregated stats local AGGREGATED_STAT_NAMES = { {"Fleet XP", "Outpost XP", "Harmonic Shield", "Harmonic Agility", "Harmonic Warfare", "Harmonic Siege", "Fleet Resurgence"},	-- Uncategorized {"DPS", "Energy DPS", "Explosive DPS", "Kinetic DPS", "Alien DPS", "Plasma DPS", "Blight DPS", "Void DPS"}, {"Health"}, {"Shield Energy", "Shield Defense", "Shield Recharge", "Shield Recharge Delay"}, {"Ablative Armor Health", "Ablative Armor Defense", "Ablative Damage Threshold", "Ablative Recovery", "Ablative Energy Recovery", "Ablative Explosive Recovery", "Ablative Kinetic Recovery", "Ablative Alien Recovery", "Ablative Plasma Recovery", "Ablative Blight Recovery", "Ablative Void Recovery"}, {"Screen Energy", "Screen Defense", "Phase Screen Charge", "Phase Screen Drain", "Phase Threshold"}, {"Prime Shift Charge (Attack)", "Prime Shift Charge (Defense)", "Prime Shift Drain"}, }

local FLAT_AGGREGATED_STAT_NAMES = flattenAsArray(AGGREGATED_STAT_NAMES)

-- The names of the stats relating to (damage) resistance local RESISTANCE_STAT_NAMES = { "Energy Resistance", "Explosive Resistance", "Kinetic Resistance", "Alien Resistance", "Plasma Resistance", "Blight Resistance", "Void Resistance", "Energy Nebula Resistance", "Explosive Nebula Resistance", "Kinetic Nebula Resistance", "Alien Nebula Resistance", "Plasma Nebula Resistance", "Blight Nebula Resistance", "Void Nebula Resistance", }

-- Constructor -- -- Creates a new ParentItem instance according to the given data. -- Throws an exception if any field name or value is not a string. -- data: An associative table with each pair (k, v) containing a field name and its corresponding value. function ParentItem:initialize(data) ParentItem.super.initialize(self, data) local slot_ids = zip(duplicate("Weapon"), self.weapon_slot_ids or {}) :chain(zip(duplicate("Hangar"), self.hangar_slot_ids or {})) :chain(zip(duplicate("Spawner"), self.spawner_slot_ids or {})) :chain(zip(duplicate("Armor"), self.armor_slot_ids or {})) :chain(zip(duplicate("Shield"), self.shield_slot_ids or {})) :chain(zip(duplicate("Ablative Armor"), self.ablative_armor_slot_ids or {})) :chain(zip(duplicate("Screen"), self.screen_slot_ids or {})) :chain(zip(duplicate("Operation"), self.operation_slot_ids or {})) :chain(zip(duplicate("Skin"), self.skin_slot_ids or {})) :chain(zip(duplicate("Trigger"), self.trigger_slot_ids or {})) :chain(zip(duplicate("Resistance"), self.resistance_slot_ids or {})) self._slots_by_id = slot_ids:map(function (name, id) return id, Slot(name, id, nil) end):tomap self:recalculate end

-- Obtains the slot type for the given ID. function ParentItem:getSlotType(id) return self._slots_by_id[id].slot_type end

-- Deep copy of a table of Slot instances, including empty ones. function ParentItem:getSlots return mw.clone(iter(self._slots_by_id):values:totable) end

-- Returns a table containing all of the equipment on the ParentItem. function ParentItem:getEquipment return mw.clone(iter(self._slots_by_id):map(function (id, slot) return slot.content end):filter(op.truth):totable) end -- Same as getEquipment, but with all stats modified by other equipment on the object and/or the object itself. function ParentItem:getModifiedEquipment return mw.clone(iter(self._modified_slots_by_id):map(function (id, slot) return slot.content end):filter(op.truth):totable) end -- Deep copy of a table containing the aggregated stats of the object -- { statistic -> value } function ParentItem:getAggregatedStats return mw.clone(self._aggregated_stats) end -- Deep copy of a table containing the health resistance of the object against different damage types -- { statistic -> value } function ParentItem:getResistance return mw.clone(self._resistance) end -- Deep copy of a table containing the shield resistance of the object against different damage types -- { statistic -> value } function ParentItem:getShieldResistance return mw.clone(self._shield_resistance) end -- Deep copy of a table containing the screen resistance of the object against different damage types -- { statistic -> value } function ParentItem:getScreenResistance return mw.clone(self._screen_resistance) end

-- Attempts to get a stat of the object given by its full name. If not found, returns -- the default value instead. function ParentItem:tryGetStat(fullpagename, default_value) return self:getStatistics:get(fullpagename) or default_value end

-- Attempts to get an aggregated stat of the object given by its full name. If not found, returns -- the default value instead. function ParentItem:tryGetAggregatedStat(stat_name, default_value) return self._aggregated_stats[stat_name] or default_value end

-- The total mass of the object, including items with relative mass. function ParentItem:getTotalMass local m = tonumber(self:tryGetStat("Unladen Mass", 0)) m = m + self:tryGetAggregatedStat("Mass", 0) m = m * (1 + self:tryGetAggregatedStat("Weapon Mass", 0)) return m end

-- Other methods -- -- Equips a item to the specified slot ID. -- If defer_recalc does not evaluate to false, does not update the other information of the ParentItem. function ParentItem:equip(item, slot_id, defer_recalc) local slot_name = assert(e.equip_slot, string.format("The item %s does not have a target slot", item.name)) local slot = assert(self._slots_by_id[slot_id], string.format("There is no slot with ID %s for the parent item %s", slot_id, self.name)) if slot.content ~= nil then error(string.format("The slot with ID %s for the parent item %s is already occupied", slot_id, self.name)) end -- Since the slot is an object, this should update both internal collections slot.content = item if not defer_recalc then self:recalculate end end

-- Recalculates the modified equipment of the object. function ParentItem:recalculateModifiedEquipment self._modified_slots_by_id = self:getEquipment local modifiers = {} local function addModifier(stat, inner) modifiers[stat] = modifiers[stat] or {} iter(inner):each(function (applies_to, v)			modifiers[stat][applies_to] = modifiers[stat][applies_to] or {}			table.insert(modifiers[stat][applies_to], v)		end) end -- Get modifiers from equipment iter(self._slots_by_id):each(function (equipment)		iter(equipment:getModifiers:get):each(addModifier)	end) -- Get modifiers from self iter(self:getModifiers:get):each(addModifier) -- Apply the modifiers iter(self._slots_by_id):each(function (equipment)		iter(equipment:getStatistics:get):each(function (stat, base_val) local coeffbase_sum, constant_sum, coeffcurrent_prod = 0, 0, 1 local min_min, max_max = nil, nil local stat_modifier = modifiers[stat] if stat_modifier then iter(stat_modifier):each(function (applies_to, mod_inners)					applies_to = mw.text.split(applies_to, "%s*,%s*")					local applies = iter(applies_to):any(function (e) return e == slot.slot_type or (iter(data.APPLIES_TO_STATS[e]):index(stat)							and iter(data.APPLIES_TO_NAME_SUBSTR[e]):any(function (substr) return e.name:find(substr) end)) end)					if (applies) then						iter(mod_inners):each(function (mod_inner) local mod_value, mod_type = tonumber(mod_inner.value), mod_inner.type if mod_type == "CoeffBase" then coeffbase_sum = coeffbase_sum + (1 + mod_value) elseif mod_type == "Constant" then constant_sum = constant_sum + mod_value elseif mod_type == "CoeffCurrent" then coeffcurrent_prod = coeffcurrent_prod * (1 + mod_value) elseif mod_type == "Min" then min_min = math.min(min_min or mod_value, mod_value) elseif mod_type == "Max" then max_max = math.max(max_max or mod_value, mod_value) else error(string.format("Unknown mod_type '%s'", mod_type)) end end)					end				end) end -- Apply all of the modifiers (this is a bit hacky as it directly sets the stat value) local new_value = math.max(max_max, math.min(min_min, coeffcurrent_prod * (constant_sum + coeffbase_sum * current_val))) self._modified_slots_by_id[id].content._stats[stat] = tostring(new_value) end)	end) end

-- Recalculates aggregated stats of the ojbect function ParentItem:recalculateAggregatedStats self.aggregated_stats = {} iter(self.getModifiedEquipment):each(function (equipment)		iter(FLAT_AGGREGATED_STAT_NAMES):each(function (stat) local value = e:getStatistics:get(stat) if (value) then -- Init if nil self._aggregated_stats[stat] = self._aggregated_stats[stat] or 0 local current_value = tonumber(self._aggregated_stats[stat]) local equipment_value = tonumber(value) if stat == "Recharge Delay" or stat == "Shield Defense" or stat == "Ablative Armor Defense" or stat == "Screen Defense" then -- Use minimum value self._aggregated_stats[stat] = math.min(current_value, equipment_value) else -- Simple addition self._aggregated_stats[stat] = current_value + equipment_value end end end)	end) end

-- Recalculates resistance values of the ojbect function ParentItem:recalculateResistance local total_effective_health = {} local health_resistance = {} local total_effective_shield_energy = {} local shield_resistance = {} local total_effective_screen_energy = {} local screen_resistance = {} local function accumulate(stats, is_health_global) local health = tonumber(self:tryGetStat("Health", 0)) local shield_energy = tonumber(self:tryGetStat("Shield Energy", 0)) local screen_energy = tonumber(self:tryGetStat("Screen Energy", 0)) iter(RESISTANCE_STAT_NAMES):each(function (stat)			local not_resisted = 1 - (tonumber(stats[stat]) or 0)			if not_resisted == 0 then				total_effective_health[stat] = 1				total_effective_shield_energy[stat] = 1				total_effective_screen_energy[stat] = 1			else				if (is_health_global) or (health == 0 and shield_energy == 0 and screen_energy == 0) then					total_effective_health[stat] = (total_effective_health[stat] or 0) + health					screen_resistance[stat] = 1 - (1 - (screen_resistance[stat] or 0)) * not_resisted				else					total_effective_health[stat] = (total_effective_health[stat] or 0) + health / not_resisted				end				total_effective_shield_energy[stat] = (total_effective_shield_energy[stat] or 0) + shield_energy / not_resisted				total_effective_screen_energy[stat] = (total_effective_screen_energy[stat] or 0) + screen_energy / not_resisted			end		end) end -- Incorporate this object's resistance accumulate(self:getStatistics:get, true) -- Incorporate resistance from equipment iter(self:getModifiedEquipment):map(function (equipment)		-- Ablative Armor does not influence resistance		if equipment.slot_type ~= "Ablative Armor" then			accumulate(equipment:getStatistics:get, slot_type == "Resistance")		end	end) -- Calculate the overall resistance local function setStat(tbl, stat, current_resist, current_weight, total_weight) if current_resist <= 0 then tbl[stat] = 0 elseif current_resist < 1 then tbl[stat] = 1 - (1 - current_resist) * (1 - current_weight / total_weight) else tbl[stat] = 1 end end iter(RESISTANCE_STAT_NAMES):each(function (stat)		local tot_health = self._aggregated_stats["Health"]		local tot_eff_health = total_effective_health[stat]		local health_resist = health_resistance[stat]		setStat(self._resistance, stat, health_resist, tot_health, tot_eff_health)		local tot_shield_energy = self._aggregated_stats["Shield Energy"]		local tot_eff_shield_energy = total_effective_shield_energy[stat]		local shield_resist = shield_resistance[stat]		setStat(self._shield_resistance, stat, shield_resist, tot_shield_energy, tot_eff_shield_energy)		local tot_screen_energy = self._aggregated_stats["Screen Energy"]		local tot_eff_screen_energy = total_effective_screen_energy[stat]		local screen_resist = screen_resistance[stat]		setStat(self._screen_resistance, stat, screen_resist, tot_screen_energy, tot_eff_screen_energy)	end) -- Remove irrelevant resistances if ParentItem:tryGetAggregatedStat("Healht", 0) == 0 then self._resistance = nil end if ParentItem:tryGetAggregatedStat("Shield Energy", 0) == 0 then self._shield_resistance = nil end if ParentItem:tryGetAggregatedStat("Screen Energy", 0) == 0 then self._screen_resistance = nil end end

-- Recalculates all values of the object where applicable. function ParentItem:recalculate self:recalculateModifiedEquipment self:recalculateAggregatedStats self:recalculateResistanceValues end

return { generateSlotPositionTable = generateSlotPositionTable, ParentItem = ParentItem, AGGREGATED_STAT_NAMES = AGGREGATED_STAT_NAMES, RESISTANCE_STAT_NAMES = RESISTANCE_STAT_NAMES, }