Pillars of Eternity Wiki
Advertisement
Pillars of Eternity Wiki
Template-info.png Module documentation

This module is used to support Template:Infobox ship, and is used query information about ship types and ship upgrades (e.g. cannons, hulls, sails, etc). Ship upgrades cannot be queried with cargo, as they do not have their own pages/item infoboxes.


-- Module:Ship
-- Description: Used to query information about ship types and ship upgrade types, including icons.
local p = {}
local data = mw.loadData("Module:Ship/data")
local common = require("Module:Common")
local getArgs = require('Module:Arguments').getArgs

local baseStatOverrides = {}

-- Builds content containing a breakdown of a ship stat, given the base ship
-- and the equipped upgrades
-- Right now we don't actually do anything with the breakdown, but perhaps in the
-- future we'll display a tooltip, whatever is permitted by the new unified UI
function p.buildStatContent(frame)
	
	local frameArgs = getArgs(frame);
	local dontFormatStat = false
	
	local shipType = frameArgs["type"];
	local statType = frame.args["stat"]
	
	-- Return "-" if the ship type or stat type is nil
	if not (shipType and statType) then
		return "-"
	end
	
	local upgrades = { ["hull"] = "", ["sails"] = "", ["helm"] = "", ["lanterns"] = "", ["anchor"] = "" }
	
	-- Fill the upgrades table
	for k,_ in pairs(upgrades) do
		upgrades[k] = frameArgs[k] or nil
	end
	
	local adj = collectStatAdjustments(shipType, upgrades, statType, frameArgs)
	
	if (adj == nil) then return '-' end
	
	-- Special treatment for travel_speed, since it is multiplicative
	if (statType == "travel_speed") then
		
		-- Move base ship type travel_speed stat into a modifier
		table.insert(adj.modifiers, { value = adj.base, upgradeType = "ship", upgrade = "Base" })
		
		-- Set base to actual base
		adj.base = 50
		
		local multiplier = 1.0
		
		-- Recalculate final using multiplier
		for i = 1, #adj.modifiers do
			multiplier = multiplier + (adj.modifiers[i].value / 100)
		end
		
		adj.final = adj.base * multiplier
		
		-- In addition, don't format the output number
		dontFormatStat = true
	end
	
	return "<span style=\"white-space:nowrap\">"..formatStatWithIcon(statType, adj.final, dontFormatStat).."</span>"
end

-- Collects a list of stat adjustments given a shipType and a table of upgrades
-- upgrades should be a list of key-value pairs where the key is the upgradeType and
-- the value is the name of the upgrade

-- The returned table contains the base, final and a list of modifiers
-- each containing a value, an upgrade and an upgradeType
function collectStatAdjustments(shipType, upgrades, stat, frameArgs)
	
	local result = { }
	result.base = getShipBaseStatsWithOverrides(shipType, frameArgs)[stat] or 0
	result.final = result.base
	result.modifiers = { }
	
	if (upgrades == nil) then
		return result
	end
	
	-- Loop over all the inputted upgrades
	for t, u in pairs(upgrades) do
		
		-- Get the modifiers for this upgrade
		local mod = getStatModifierForUpgrade(t, u, stat)
		
		if (mod ~= 0) then
			-- Insert a new table into the modifiers array containing the value, upgradeType and upgrade name
			table.insert(result.modifiers, { value = mod, upgradeType = t, upgrade = u })
			
			result.final = result.final + mod
		end
	end
	
	return result
end

function collectAllStatAdjustments(shipType, upgrades, frameArgs)
	
	local baseStats = getShipBaseStatsWithOverrides(shipType, frameArgs)
	local stats = { }
	
	-- Inititialize resulting table
	for k,_ in pairs(data.stats) do
		if (k ~= "crew" and k ~= "defenders") then
			stats[k] = { }
			stats[k].base = baseStats[k] or 0
			stats[k].final = stats[k].base
			stats[k].modifiers = {}
		end
	end
	
	-- Loop over all the inputted upgrades
	for t, u in pairs(upgrades) do
		
		-- Get the modifiers for this upgrade
		local mods = getAllModifiersForUpgrade(t, u)
		
		if (mods ~= nil) then
		
			-- Stat type, stat value
			for st, sv in pairs(mods) do
				
				-- Check if the stat type is actually contained in the resulting table
				if (stats[st] ~= nil) then
					
					-- Insert a new table into the modifiers array containing the value, upgradeType and upgrade name
					table.insert(stats[st].modifiers, { value = sv, upgradeType = t, upgrade = u })
					
					stats[st].final = stats[st].final + sv
				end
			end
			
		end
	end
	
	mw.logObject(stats)
	
	return stats
end

-- Fetches the icon for an upgrade, displaying it in a flink-type format.
-- The first parameter is required, and is the type of upgrade.
-- The second parameter is optional, and is the name of the upgrade.
-- If not present, this will be fetched automatically from the infobox parameters using the type of upgrade as the key
-- It can be used if the upgrade type doesn't line up with a value passed to the infobox, for example
-- with cannon type upgrades, split across the fields guns_port, guns_starboard, etc
-- If the parameter "includeStats" is yes or 1, the stats for the upgrade are shown in a table below it
function p.buildUpgradeContent(frame)
	local parentArgs = frame:getParent().args;
	local upgradeType = frame.args[1]
	local upgrade = frame.args[2] or parentArgs[upgradeType]
	local includeStats = (frame.args["includeStats"] == "1" or frame.args["includeStats"] == "yes")
	return p._buildUpgradeContent(upgradeType, upgrade, includeStats)
end

function p._buildUpgradeContent(upgradeType, upgrade, includeStats, textAfter)
	if not (common.isNotNilOrEmpty(upgradeType) and common.isNotNilOrEmpty(upgrade)) then
		return ""
	end
	
	-- We lay out the icon and title in a two column grid. 
	-- This is a bit tidier than a flexbox, and provides more functionality than doing it "manually"
	local title = mw.html.create("div")
		:cssText("display:grid; grid-template-columns:25px auto")
	    :wikitext("[[File:"..getIconForUpgrade(upgradeType, upgrade).."|20px|link="..upgrade.."]]")
	    :tag("span"):wikitext("[["..upgrade.."]]"..(textAfter or "")):done()
	
	if (includeStats) then
		local mods = getAllModifiersForUpgrade(upgradeType, upgrade)
		
		-- Check if we actually have mods
		if (mods ~= nil) then
			
			-- Create the stat section
			local tbl = mw.html.create("div"):cssText("display:grid; grid-template-columns:repeat(3, auto);")
			
			for k,v in pairs(mods) do
				tbl:tag("span"):css("white-space", "nowrap"):wikitext(formatStatWithIcon(k, v)):done()
			end	
			
			return tostring(title)..tostring(tbl)
		end
	end
	
	return tostring(title)
end

-- Builds a table consisting of the base stats of the ship
-- The table has two rows, the first row contains the sail health, combat speed, and crew
-- while the second row contains the hull health, travel speed, and defenders
-- Each has the icon and the stat beside it
function p.buildShipBaseStatsTable(frame)
	local args = getArgs(frame)
	local baseStats = getShipBaseStatsWithOverrides(args[1], args)
	
	if (baseStats == nil) then return end
	
	-- Originally was a table with two rows and three columns, now a grid layout to accomplish the same 
	local tbl = mw.html.create("div"):cssText("display:grid; grid-template-columns:1fr 1.3fr 1fr;")
	tbl:tag("span"):wikitext(formatStatWithIcon("sail_health", baseStats.sail_health)):done()
	   :tag("span"):wikitext(formatStatWithIcon("combat_speed", baseStats.combat_speed)):done()
	   :tag("span"):wikitext(formatStatWithIcon("crew", baseStats.min_crew_size)):done()
	   :tag("span"):wikitext(formatStatWithIcon("hull_health", baseStats.hull_health)):done()
	   :tag("span"):wikitext(formatStatWithIcon("travel_speed", baseStats.travel_speed)):done()
	   :tag("span"):wikitext(formatStatWithIcon("defenders", baseStats.defenders)):done()
	   :done()
	   
	return tbl
end

-- Builds the content for each of the gun/cannon fields
-- Takes a single unnamed argument, the field name of the guns
function p.buildGunsContent(frame)
	
	local args = getArgs(frame)
	local fieldName = args[1]
	
	if (fieldName == nil) then
		return ""
	else
		local fieldValue = args[fieldName]
		local guns = mw.text.split(fieldValue, ";", true)
		local output = {}
		
		for i = 1, #guns do
			
			-- Find offsets of quantity value in e.g. "5x" and capture the integer
			local qs, qe, qty = string.find(guns[i], "(%d+)x ")
			
			-- Default to qty 1, and zero the offsets as not to affect the substring
			qs = qty ~= nil and qs or 0
			qe = qty ~= nil and qe or 0
			qty = qty ~= nil and qty or 1
			
			local gun = string.sub(guns[i], qe + 1)
			local content = p._buildUpgradeContent("cannon", gun, false, " (x"..qty..")")
			table.insert(output, content)
		end
		
		return table.concat(output)
	end
end

-- Calculates the total crew count, typically (but not always!) the
-- amount of enemies that the player faces in deck-to-deck combat.
-- active crew + resting crew + defenders + captain
function p.getTotalCrewCount(frame)
	local args = getArgs(frame)
	
	function countCrew(list)
		local count = 0
		
		for i = 1, #list do
			count = count + (tonumber(string.match(list[i], "%d*")) or 0)
		end
		
		return count
	end
	
	-- Get resting and active crew counts
	local activeCrewList = mw.text.split(args["crew"] or "", ";", true)
	local restingCrewList = mw.text.split(args["resting_crew"] or "", ";", true)
	local activeCrewCount = countCrew(activeCrewList)
	local restingCrewCount = countCrew(restingCrewList)

	-- Get defender count
	local defenderCount = getShipBaseStatsWithOverrides(args["type"], args).defenders or 0
	
	-- Total crew count is active + resting + defenders + captain (1)
	local total = activeCrewCount + restingCrewCount + defenderCount + 1
	
	-- Determine if we should also print the breakdown
	if (args["breakdown"] == "yes") then
		local breakdown = {}
		if (activeCrewCount > 0) then table.insert(breakdown, activeCrewCount.." active") end
		if (restingCrewCount > 0) then table.insert(breakdown, restingCrewCount.." resting") end
		if (defenderCount > 0) then table.insert(breakdown, defenderCount.." defenders") end
		table.insert(breakdown, "1 captain")
		return total.." ("..table.concat(breakdown, ", ")..")"
	else
		return total
	end
end

function p.normalizedColorToRgb(frame)
	local args = getArgs(frame)
	return p.normalizedColorStringToRgb(args[1])
end

function p.normalizedColorStringToRgb(str)
	
	-- Split by comma
	local rgb = mw.text.split(str or "", ",", true)
	
	if (#rgb ~= 3) then return "0 0 0" end
	
	for i = 1, #rgb do
		-- Convert to number
		rgb[i] = tonumber(rgb[i]) or 0
		
		-- Convert normalized value (0.0 - 1.0) to 8 bit integer (0 - 255)
		rgb[i] = math.floor(rgb[i] * 255)
	end
	
	return table.concat(rgb, " ")
end

-- Fetching data

function upgradeTypeToPluralForm(upgradeType)
	if (common.endsWith(tostring(upgradeType), "s")) then
		return upgradeType
	else
		return upgradeType.."s"
	end
end

-- Check to see if the data module is loaded
function checkDataLoaded()
	if (data == nil)  then
		error("Data is not loaded!")
		return false
	end
	
	return true
end

function isUpgradeTypeValid(upgradeType)
	if (data.upgrades[upgradeType] == nil) then
		error("No upgrade type found with the name \""..upgradeType.."\". Make sure to use the plural form, or check Module:Ship/data")
		return false
	end
	
	return true
end

function isUpgradeValid(upgradeType, upgrade)
	
	if not (checkDataLoaded() and upgradeType and upgrade) then
		return false
	end
	
	if (upgradeType == "" or upgrade == "") then
		return false
	end
	
	if (isUpgradeTypeValid(upgradeType)) then
		
		if (data.upgrades[upgradeType][upgrade] == nil) then
			error("An upgrade of type \""..upgradeType.."\" with the name \""..upgrade.."\" was not found")
			return false
		else
			return true
		end
	end
	
	return false
end

-- Finds a particular stat modifier for an upgrade
-- for example, using "Dragonwing Sails" and "combat_speed" returns 10
-- If a stat mod is not found, 0 is returned
function getStatModifierForUpgrade(upgradeType, upgrade, stat)
	
	upgradeType = upgradeTypeToPluralForm(upgradeType)
	
	if (isUpgradeValid(upgradeType, upgrade) == false) then
		return 0
	end
	
	local upgrade = data.upgrades[upgradeType][upgrade]
	
	-- Returns 0 if upgrade.stats is nil, or if the stat itself wasn't found
	-- otherwise return the stat
	return (upgrade.stats and upgrade.stats[stat]) or 0
end

function getAllModifiersForUpgrade(upgradeType, upgrade)
	
	upgradeType = upgradeTypeToPluralForm(upgradeType)
	
	if (isUpgradeValid(upgradeType, upgrade) == false) then
		return 0
	end
	
	local upgrade = data.upgrades[upgradeType][upgrade]
	return upgrade.stats or nil
end

-- Returns the base stats from the data module.
function getShipBaseStats(shipType)

	if not (checkDataLoaded() and data.ships and data.ships[shipType]) then
		error("No ship type found with the name \""..(shipType or "").."\"")
		return nil
	end
	
	return data.ships[shipType]

end

-- Same as the above, but allows overriding specific stats using the arguments
-- passed to the template. Overrides can be set in cases where a ship uses unique
-- stats that aren't based on the ship type, and should have the same names as
-- those found in Module:Ship/data
function getShipBaseStatsWithOverrides(shipType, args)
	local baseStats = getShipBaseStats(shipType)
	
	-- Copy baseStats to new table, since it is read only
	local baseStatsRw = {}
	
	for k,v in pairs(baseStats) do
		local override = tonumber(args[k])
		baseStatsRw[k] = override or v
	end
	
	return baseStatsRw
end

-- Icons

-- Returns the icon for a ship stat
function getIconForStat(statType)
	
	local icon = data.stats[statType].icon

	if (icon == nil) then
		return "Missing icon.png"
	else
		return data.stats[statType].icon
	end
end

-- Returns the icon filename for an upgrade 
function getIconForUpgrade(upgradeType, name)
	
	upgradeType = upgradeTypeToPluralForm(upgradeType)
	
	if (isUpgradeValid(upgradeType, name) == false) then
		return "Missing icon"
	else
		return data.upgrades[upgradeType][name].icon
	end
end

-- Stat formatting

-- Formats a ship stat, outputting only the value
function formatStat(stat, value)
	
	local fmt = data.stats[stat].format;
	
	if (fmt == nil) then
		error("Format string for stat "..stat.." not found!")
		return tostring(value)
	else
		return string.format(fmt, value)
	end
end

-- Formats a ship stat, outputting the value prefixed with the icon
function formatStatWithIcon(stat, value, dontFormat)
	
	local statInfo = data.stats[stat];
	
	if (statInfo == null) then
		error("Stat info for stat "..stat.." not found!")
		return tostring(value)
	else
		return "[[File:"..statInfo.icon.."|20px|link=|"..statInfo.name.."]] "..(dontFormat == true and value or string.format(statInfo.format, value))
	end

end

-- Formats a ship stat, outputting the value prefixed with the icon and suffixed with the stat label
function formatStatWithIconAndLabel(stat, value)
	local statInfo = data.stats[stat];
	
	if (statInfo == null) then
		error("Stat info for stat "..stat.." not found!")
		return tostring(value)
	else
		return "[[File:"..statInfo.icon.."|20px|link=|"..statInfo.name.."]] "..string.format(statInfo.format, value).." "..statInfo.name
	end
end
return p
Advertisement