warframe

Weapon comparison table builders as seen on Weapon Comparison and Projectile Speed.


-- Note: Only include the most important stats in comparison table columns so
-- that all tables can show up on [[Weapon Comparison]] w/o the need of sidescrolling
-- on desktop.
-- The more columns the longer the script execution time is, making it more likely 
-- to exceed the allocated time for script execution (7 seconds).
-- See NewPP report for script performance on articles. - User:Cephalon Scientia 2021-08-06

-- If you want to update how formatted values are being displayed for each column, update
-- the appropriate format function/string in the value getter in [[Module:Weapons]].
local p = {}

local Weapon = require('Module:Weapons')
local WeaponData = require('Module:Weapons/data')
local ConclaveWeaponData = require('Module:Weapons/Conclave/data')

-- TODO: Internationalize this module
-- local I18n = require('Module:I18n').loadMessages('MediaWiki:Custom-Weapons/i18n.json'):useUserLang();

-- TODO: Add a function to generate a table for all weapons and all their attacks?
-- Not sure if this would exceed allocated memory.

---	Simple ordered map object for defining automatic comparison wikitables.
--	No support for entry removals, only insertions in sequential order.
--	See https://stevedonovan.github.io/Penlight/api/source/orderedmap.lua.html for sample implementation
--	@table		TableDefinition
local TableDefinition  = {}

---	Adds a new table column entry.
--	@function		TableDefinition:insert
--	@param			{string} getterFuncName Getter function name corresponding to M:Weapons
--	@param			{string} headerName Displayed name of column
function TableDefinition:insert(getterFuncName, headerName)
	assert(getmetatable(self)._keys ~= nil)	-- Must construct object before inserting
	assert(type(getterFuncName) == "string" and type(headerName) == "string")
	self[getterFuncName] = headerName
	table.insert(getmetatable(self)._keys, getterFuncName)
	return self	-- Returning self for function chaining
end

setmetatable(TableDefinition, {
	_keys = nil,
	
	---	Constructor for TableDefinition
	--	Since functions invoked by frame is running on their own instance, don't need to 
	--	reset _keys table or delete any added key-value entries from when
	--	TableDefinition was constructed previously. This constructor assumes only
	--	one TableDefinition is constructed in every function call here.
	--	@function		__call
	__call = function(self)
		getmetatable(self)._keys = {}
		return self
	end,
	
	---	Iterating over table column entries in the order they are added.
	--	@function		__pairs
	__pairs = function(self)
		local i = 0	-- Control variable
		local keys = getmetatable(self)._keys
		local n = #keys
		local index = nil
		return function()
			i = i + 1
			if i > n then return nil end
			index = keys[i]
			return index, self[index]
		end
	end
})

---	Builds a row for comparison table as seen on [[Weapon Comparison]].
--	@function		buildCompRow
--	@param			{table} tableHeaders Wikitable's header names an the specific getter function that it will pull from
--							(e.g. { ["CritChance"] = "[[Critical Chance|Crit<br />Chance]]", ["CritMultiplier"] = "[[Critical Multiplier|Crit Multi]]"} )
--	@param			{table} Weapon A weapon table entry as pulled from <code>/data</code>
--	@returns		{string} Wikitext of resultant wikitable row
local function buildCompRow(tableHeaders, weaponEntry)
	local result = {}
	for attackIndex, _ in ipairs(weaponEntry["Attacks"]) do
		local tableRow = {}
		for getterFuncName, _ in pairs(tableHeaders) do
			-- Protected call needed here to catch errors since if something breaks with getter function
			-- stack backtrace isn't helpful because there's a bunch of unamed tail calls
			-- between this function and M:StatObject
			local noErrors, value = pcall(Weapon._getFormattedValue, weaponEntry, getterFuncName, attackIndex)
			
			if not noErrors then
				error('buildCompRow(tableHeaders, weaponEntry): Error when trying to access "'..getterFuncName..'" attribute of '..mw.dumpObject(weaponEntry)..'\n Original error message: \n'..value)
			end
			
			table.insert(tableRow, value)
		end
		table.insert(result, table.concat(tableRow, ' || '))
	end
	return '|-\n| '..table.concat(result, '\n|-\n| ')
end

---	Builds comparison table as seen on [[Weapon Comparison]].
--	@function		buildCompTable
--	@param			{table} tableHeaders Wikitable's header names an the specific getter function that it will pull from
--							(e.g. { { "CritChance", "[[Critical Chance|Crit<br />Chance]]" }, { "CritMultiplier", "[[Critical Multiplier|Crit Multi]]" } } )
--	@param			{table} weaponArray Array of weapon table entries as pulled from <code>/data</code>
--	@returns		{string} Wikitext of resultant wikitable
local function buildCompTable(tableHeaders, weaponArray)
	local styleString = 'border: 1px solid black; border-collapse: collapse;'
	local dataSortType
	local result = {}
	local sortColumnByNumber = {
		["AttackSpeed"] = true,
		["DamageBias"] = true,
		["CritMultiplier"] = true,
		["HeadshotMultiplier"] = true,
		["AmmoMax"] = true,
		["ShotSpeed"] = true,
	}
	
	table.insert(result, '{| cellpadding="1" cellspacing="0" class="listtable sortable" style="font-size:11px;"')
	for getterFuncName, headerName in pairs(tableHeaders) do
		dataSortType = ''
		-- TODO: Should be able to imply data-sort-type based on raw unformatted value using getterFuncName
		-- (e.g. Weapons.getValue(some generic weaponEntry, 1, getterFuncName) )
		if (sortColumnByNumber[getterFuncName]) then
			dataSortType = ' data-sort-type="number"'
		end
		table.insert(result, string.format('! style="%s"%s | %s', styleString, dataSortType, headerName))
	end

	for _, weaponEntry in pairs(weaponArray) do
		local rowStr = buildCompRow(tableHeaders, weaponEntry)
		table.insert(result, rowStr)
	end
	
	table.insert(result, '|}[[Category:Automatic Comparison Table]]')
	return table.concat(result, '\n')
end

---	Builds comparison table of gun stats as seen on [[Weapon Comparison]].
--	@function		p.getCompTableGuns
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getCompTableGuns(frame)
	local weaponSlot = frame.args ~= nil and frame.args[1]
	local weaponSubtype = frame.args ~= nil and frame.args[2] or nil
	local triggerType = frame.args ~= nil and frame.args[3] or nil
	if (weaponSubtype == "All") then weaponSubtype = nil end
	if (triggerType == "All") then triggerType = nil end
	
	-- Using uniqueNames for portability across multiple languages
	local rangedMelees = {
		["/Lotus/Weapons/Tenno/Melee/Hammer/ThrowingHammer"] = true,	-- Wolf Sledge
		["/Lotus/Weapons/Infested/Melee/InfWarfan/InfWarfanWeapon"] = true,	-- Arum Spinosa
		["/Lotus/Weapons/Tenno/Melee/Warfan/TnBrokenFrameWarfan/TnBrokenFrameWarfanWeapon"] = true,	-- Quassus
		["/Lotus/Weapons/Corpus/Melee/CrpBriefcaseScythe/CrpBriefcaseScythe"] = true,	-- Tenet Grigori
		["/Lotus/Weapons/Corpus/Melee/ShieldAndSword/CrpHammerShield/CrpHammerShield"] = true,	-- Tenet Agendus
	}
	
	--- Filter table entries by a specific key-value pair
	local function filterTable(weaponArray, key, value)
		local temp = {}
		for weaponName, weaponEntry in pairs(weaponArray) do
			if (weaponEntry[key] == value and 
					(weaponSubtype == nil or weaponEntry["Class"] == weaponSubtype) and
					(triggerType == nil or weaponEntry["Trigger"] == triggerType)) then
				temp[weaponName] = weaponEntry
			end
		end
		weaponArray = temp
		return weaponArray
	end

	local weaponArray = {}
	-- TODO: Switch-like if/elses statements can be formatted into a function map
	if (weaponSlot == "Primary") then
		-- TODO: I think WeaponData["Primary"] should all have the same Slot = "Primary" key so this check is not needed
		-- (filtering by class and trigger still needed however)
		weaponArray = filterTable(WeaponData["Primary"], "Slot", "Primary")
	elseif (weaponSlot == "Secondary") then
		-- TODO: I think WeaponData["Secondary"] should all have the same Slot = "Secondary" key so this check is not needed
		-- (filtering by class and trigger still needed however)
		weaponArray = filterTable(WeaponData["Secondary"], "Slot", "Secondary")
	elseif (weaponSlot == "Archgun") then
		weaponArray = filterTable(WeaponData["Archwing"], "Slot", "Archgun")
	elseif (weaponSlot == "Archgun (Atmosphere)") then
		weaponArray = filterTable(WeaponData["Archwing"], "Slot", "Archgun (Atmosphere)")
	elseif (weaponSlot == "Robotic") then
		weaponArray = WeaponData["Companion"]
	elseif (weaponSlot == "Amp") then
		weaponArray = filterTable(WeaponData["Modular"], "Class", "Amp")
	elseif (weaponSlot == "Turret") then
		weaponArray = filterTable(WeaponData["Railjack"], "Class", "Turret")
	elseif (weaponSlot == "Ordnance") then
		weaponArray = filterTable(WeaponData["Railjack"], "Class", "Ordnance")
	-- Unique melee weapons that have thrown/ranged attacks
	elseif (weaponSlot == "Melee") then
		weaponArray = WeaponData["Melee"]
		local temp = {}
		for weaponName, weaponEntry in pairs(weaponArray) do
			if (rangedMelees[weaponEntry["InternalName"]] ~= nil and 
					weaponEntry["Class"] == "Gunblade" or weaponEntry["Class"] == "Glaive") then
				temp[weaponName] = weaponEntry
			end
		end
		weaponArray = temp
	-- Comparing all weapons usuable in normal missions
	elseif (weaponSlot == "All") then
		local weaponArray = WeaponData["Primary"]
		local dbPartitions = { "Secondary", "Companion", "Railjack" }
		for _, partition in pairs(dbPartitions) do
			for weaponName, weaponEntry in pairs(WeaponData["Secondary"]) do
				weaponArray[weaponName] = weaponEntry
			end
		end
		
		for weaponName, weaponEntry in pairs(WeaponData["Archwing"]) do
			if (weaponEntry["Slot"] == "Archgun (Atmosphere)" or weaponEntry["Slot"] == "Archgun") then
				weaponArray[weaponName] = weaponEntry
			end
		end
		
		for weaponName, weaponEntry in pairs(WeaponData["Modular"]) do
			if (weaponEntry["Class"] == "Amp") then
				weaponArray[weaponName] = weaponEntry
			end
		end
		
		for weaponName, weaponEntry in pairs(WeaponData["Melee"]) do
			if (rangedMelees[weaponEntry["InternalName"]] ~= nil and 
					weaponEntry["Class"] == "Gunblade" or weaponEntry["Class"] == "Glaive") then
				weaponArray[weaponName] = weaponEntry
			end
		end
	else
		error('p.getCompTableGuns(frame): Wrong gun/ranged weapon class'..
			'(use "Primary", "Secondary", "Archgun", "Archgun (Atmosphere)", "Robotic", "Amp", "Turret", "Ordnance", Melee", "All")'..
			'[[Category:Invalid Comp Table]]')
	end

	local tableHeaders = TableDefinition()
	tableHeaders
		:insert("NameLink", "Name")
		:insert("Class", "[[Weapons#Weapon_Type|Weapon<br />Type]]")
		:insert("Trigger", "[[Fire Rate|Trigger]]")
		:insert("AttackName", "Attack")
		:insert("DamageBias", "Main<br/>Element")
		:insert("BaseDamage", "Base<br/>[[Damage|Dmg]]")
		:insert("Multishot", "[[Multishot]]")
		:insert("FireRate", "[[Fire Rate|Fire<br/>Rate]]")
		:insert("EffectiveFireRate", "Effective<br/>[[Fire Rate|Fire<br/>Rate]]")
		:insert("CritChance", "[[Critical Chance|Crit]]")
		:insert("CritMultiplier", "[[Critical multiplier|Crit<br/>Dmg]]")
		:insert("StatusChance", "[[Status Chance|Status]]")
		:insert("Reload", "[[Reload Speed|Reload]]")
		:insert("Magazine", "[[Ammo#Magazine Capacity|Mag<br/>Size]]")
		:insert("AvgShotDmg", "Avg<br/>Shot")
		:insert("BurstDps", "Burst<br/>DPS")
		:insert("SustainedDps", "Sust<br/>DPS")
		:insert("AvgProcPerSec", "[[Status Chance|Avg. Procs]]<br/>per sec")
		:insert("AmmoPickup", "[[Ammo Pickup|Ammo<br />Pickup]]")
		:insert("AmmoMax", "[[Ammo Maximum|Ammo<br />Max]]")
		:insert("PunchThrough", "[[Punch Through|PT]]")
		:insert("Accuracy", "[[Accuracy]]")
		:insert("AvgSpread", "Avg. [[Spread]]")
		:insert("MinSpread", "Min. Spread")
		:insert("MaxSpread", "Max. Spread")
		:insert("Disposition", "[[Riven Mods#Disposition|Dispo]]")
		:insert("Mastery", "[[Mastery Rank|MR]]")
		:insert("IntroducedDate", "Introduced")
	
	return buildCompTable(tableHeaders, weaponArray)
end

---	Builds comparison table of gun Conclave stats as seen on [[Weapon Comparison/Conclave]].
--	@function		p.getCompTableConclaveGuns
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getCompTableConclaveGuns(frame)
	local weaponSlot = frame.args ~= nil and frame.args[1]
	local weaponSubtype = frame.args ~= nil and frame.args[2] or nil
	local triggerType = frame.args ~= nil and frame.args[3] or nil
	if (weaponSubtype == "All") then weaponSubtype = nil end
	if (triggerType == "All") then triggerType = nil end
	
	-- TODO: Can be refactored into a local function outside of this scope since
	-- PVE tables also use the same function, just add weaponSubtype and triggerType arguments in function definition
	-- Filter table entries by a specific key-value pair
	local function filterTable(weaponArray, key, value)
		local temp = {}
		for weaponName, weaponEntry in pairs(weaponArray) do
			if (weaponEntry[key] == value and 
					(weaponSubtype == nil or weaponEntry["Class"] == weaponSubtype) and
					(triggerType == nil or weaponEntry["Trigger"] == triggerType)) then
				temp[weaponName] = weaponEntry
			end
		end
		weaponArray = temp
		return weaponArray
	end
	
	local weaponArray = {}
	if (weaponSlot == "Primary") then
		weaponArray = filterTable(ConclaveWeaponData["Primary"], "Slot", "Primary")
	elseif (weaponSlot == "Secondary") then
		weaponArray = filterTable(ConclaveWeaponData["Secondary"], "Slot", "Secondary")
	else
		error('p.getCompTableConclaveGuns(frame): Wrong gun weapon class for Conclave (use "Primary" or "Secondary")[[Category:Invalid Comp Table]]')
	end
	
	local tableHeaders = TableDefinition()
		:insert("NameLink", "Name")
		:insert("AttackName", "Attack")
		:insert("Trigger", "[[Fire Rate|Trigger]]")
		:insert("DamageBias", "Main<br/>Element")
		:insert("PvPBaseDamage", "Base<br/>[[Damage|Dmg]]")
		:insert("PvPImpact", "[[Damage/Impact Damage|Impact]]")
		:insert("PvPPuncture", "[[Damage/Puncture Damage|Puncture]]")
		:insert("PvPSlash", "[[Damage/Slash Damage|Slash]]")
		:insert("Multishot", "[[Multishot]]")
		:insert("TotalDamage", "Total Dmg")
		:insert("EffectiveFireRate", "[[Fire Rate|Fire<br/>Rate]]")
		:insert("BaseDps", "Base DPS")
		:insert("HeadshotMultiplier", "Headshot<br />Multiplier")
		:insert("ShotType", "Shot<br/>Type")
		:insert("ShotSpeed", "[[Projectile Speed|Projectile<br />Speed]]")
		:insert("Magazine", "[[Ammo#Magazine Capacity|Mag<br/>Size]]")
		:insert("AmmoMax", "[[Ammo Maximum|Ammo<br />Max]]")
		:insert("Reload", "[[Reload Speed|Reload]]")
		:insert("Accuracy", "[[Accuracy]]")
		:insert("Mastery", "[[Mastery Rank|MR]]")
		:insert("IntroducedDate", "Introduced")
	
	return buildCompTable(tableHeaders, weaponArray)
end

---	Builds comparison table of melee stats as seen on [[Weapon Comparison]].
--	@function		p.getCompTableMelees
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getCompTableMelees(frame)
	--Changed formatting, now only takes type since only class handled is Melee
	--Keeping old formatting to avoid breaking pages
	local meleeClass = frame.args ~= nil and frame.args[1] or nil
	if (meleeClass == "All") then meleeClass = nil end
	local weaponArray = {}
	weaponArray = WeaponData["Melee"]
	
	if (meleeClass ~= nil) then
		local temp = {}
		for weaponName, weaponEntry in pairs(weaponArray) do
			if (weaponEntry["Class"] == meleeClass) then
				temp[weaponName] = weaponEntry
			end
		end
		weaponArray = temp
	end
	
	local tableHeaders = TableDefinition()
		:insert("NameLink", "Name")
		:insert("Class", "Type")
		:insert("AttackName", "Attack")
		:insert("DamageBias", "Main<br/>Element")
		:insert("BaseDamage", "[[Damage|Normal]]")
		:insert("HeavyAttack", "[[Melee#Heavy Attack|Heavy]]")
		:insert("SlamAttack", "[[Melee#Slam Attack|Slam]]")
		:insert("SlideAttack", "[[Melee#Slide Attack|Slide]]")
		:insert("MeleeRange", "[[Melee#Range|Range]]")
		:insert("SweepRadius", "[[Sweep Radius]]")
		:insert("SlamRadius", "[[Melee#Slam Attack|Slam Radius]]")
		:insert("AttackSpeed", "[[Attack Speed|Speed]]")
		:insert("CritChance", "[[Critical Chance|Crit]]")
		:insert("CritMultiplier", "[[Critical multiplier|Crit Dmg]]")
		:insert("AvgDmgWithAnimSpeedMulti", "Avg Dmg × Atk Spd")
		:insert("StatusChance", "[[Status Chance|Status]]")
		:insert("Disposition", "[[Riven Mods#Disposition|Dispo]]")
		:insert("FollowThrough", "[[Melee#Follow Through|Follow<br />Through]]")
		:insert("BlockAngle", "[[Blocking|Block<br />Angle]]")
		:insert("Mastery", "[[Mastery Rank|MR]]")
		:insert("StancePolarity", "[[Stance]]")
		:insert("IntroducedDate", "Introduced")
	
	return buildCompTable(tableHeaders, weaponArray)
end

---	Builds comparison table of melee conclave stats as seen on [[Weapon Comparison/Conclave]].
--	@function		p.getCompTableConclaveMelees
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getCompTableConclaveMelees(frame)
	local meleeClass = frame.args ~= nil and frame.args[1] or nil
	if (meleeClass == "All") then meleeClass = nil end
	
	local weaponArray = ConclaveWeaponData["Melee"]
	
	-- Can be refactored into a local helper function b/c this is the same loop used
	-- in p.getCompTableMelees(frame)
	local temp = {}
	for weaponName, weaponEntry in pairs(weaponArray) do
		-- TODO: Need slot check for melee b/c Conclave /data is not partitioned yet. Remove slot check once partitioned
		-- since WeaponData["Melee"] will then properly get all melee weapons from Conclave data
		if (weaponEntry["Slot"] == "Melee" and (meleeClass == nil or weaponEntry["Class"] == meleeClass)) then
			temp[weaponName] = weaponEntry
		end
	end
	weaponArray = temp
	
	local tableHeaders = TableDefinition()
		:insert("Name", "Name")
		:insert("Class", "Type")
		:insert("PvPBaseDamage", "[[Damage|Normal]]")
		:insert("PvPImpact", "[[Damage/Impact Damage|Impact]]")
		:insert("PvPPuncture", "[[Damage/Puncture Damage|Puncture]]")
		:insert("PvPSlash", "[[Damage/Slash Damage|Slash]]")
		:insert("SlideAttack", "[[Melee#Slide Attack|Slide]]")
		:insert("AttackSpeed", "[[Attack Speed]]")
		:insert("Mastery", "[[Mastery_Rank|Mastery Rank]]")
		:insert("StancePolarity", "[[Stance]]")
		:insert("IntroducedDate", "Introduced")
	
	return buildCompTable(tableHeaders, weaponArray)
end

---	Builds comparison table of archmelee stats as seen on [[Weapon Comparison]].
--	@function		p.getCompTableArchMelees
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getCompTableArchMelees(frame)
	local weaponArray = {}
	weaponArray = Weapon._getWeapons(function(weap)
		return Weapon._getValue(weap, "Slot") == "Archmelee"
	end)
	
	local tableHeaders = TableDefinition()
		:insert("NameLink", "Name")
		:insert("DamageBias", "Main<br/>Element")
		:insert("BaseDamage", "[[Damage|Normal]]")
		:insert("MeleeRange", "[[Melee#Range|Range]]")
		:insert("AttackSpeed", "[[Attack Speed]]")
		:insert("CritChance", "[[Critical Chance]]")
		:insert("CritMultiplier", "[[Critical multiplier|Critical Damage]]")
		:insert("AvgDmgWithAnimSpeedMulti", "Avg Dmg × Atk Spd")
		:insert("StatusChance", "[[Status Chance]]")
		:insert("Mastery", "[[Mastery Rank]]")
		:insert("IntroducedDate", "Introduced")
	
	return buildCompTable(tableHeaders, weaponArray)
end

---	Builds comparison table of projectile flight speeds as seen on [[Projectile Speed]].
--	@function		p.getCompTableSpeedGuns
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getCompTableSpeedGuns(frame)
	local weaponSlot = frame.args ~= nil and frame.args[1]
	
	local supportedSlots = {
		Primary = true,
		Secondary = true,
		Robotic = true,
		["Archgun"] = true,
		["Archgun (Atmosphere)"] = true
	}
	assert(supportedSlots[weaponSlot] == true, 'p.getCompTableSpeedGuns(frame): Wrong gun weapon slot '..
			'(use "Primary", "Secondary", "Robotic", "Archgun", "Archgun (Atmosphere)")[[Category:Invalid Comp Table]]')
	
	local weaponList = Weapon._getWeapons(
			function(weap) 
				return Weapon._getValue(weap, "Slot") == weaponSlot
			end)
	
	-- special sorting for projectile weapons
	local projectileWeaponList = {}
	for k, weaponEntry in ipairs(weaponList) do
		local shotType = Weapon._getValue(weaponEntry, "ShotType")
		if (shotType == "Projectile" or shotType == "Thrown") then
			table.insert(projectileWeaponList, weaponEntry)
		end
	end

	local tableHeaders = TableDefinition()
		:insert("Name", "Name")
		:insert("Class", "Class")
		:insert("ShotSpeed", "Projectile Speed")
	
	return buildCompTable(tableHeaders, projectileWeaponList)
end

---	Builds comparison table of damage falloff as seen on [[Damage Falloff]].
--	@function		p.getCompTableFalloff
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getCompTableFalloff(frame)
	local weaponSlot = frame.args ~= nil and frame.args[1]
	local weaponList = {}
	
	-- TODO: Refactor this function to match p.getCompTableSpeedGuns(frame)
	-- (i.e. remove switch cases + simplify logic)
	if (weaponSlot == "Primary") then
		weaponList = Weapon._getWeapons(function(weap)
			return (Weapon._getValue(weap, "Slot") == "Primary")
		end)
	elseif (weaponSlot == "Secondary") then
		weaponList = Weapon._getWeapons(function(weap)
			return (Weapon._getValue(weap, "Slot") == "Secondary")
		end)
	elseif (weaponSlot == "Robotic") then
		weaponList = Weapon._getWeapons(function(weap)
			return Weapon._getValue(weap, "Slot") == "Robotic"
		end)
	elseif (weaponSlot == "Archgun") then
		weaponList = Weapon._getWeapons(function(weap)
			return Weapon._getValue(weap, "Slot") == "Archgun"
		end)
	else
		error('p.getCompTableFalloff(frame): Wrong gun weapon class '..
			'(use "Primary", "Secondary", "Robotic", or "Archgun")[[Category:Invalid Comp Table]]')
	end

	local tableHeaders = TableDefinition()
		:insert("Name", "Name")
		:insert("Class", "Class")
		:insert("FalloffStart", "Falloff Start")
		:insert("FalloffEnd", "Falloff End")
		:insert("FalloffReduction", "Max Damage<br />Reduction")
	
	return buildCompTable(tableHeaders, weaponList)
end

return p