WARFRAME Wiki
WARFRAME Wiki
(Documentation done)
(Removing getAugments() since it is only used to determine whether or not to add Category:Augmented Weapons which is unused ever since we migrated weapon infobox builders to M:Weapons/infobox; the category itself is likely unused since most people could just look through T:AugmentedMods)
Line 709: Line 709:
 
end
 
end
 
return nextAttack, Weapon
 
return nextAttack, Weapon
end
 
 
-- TODO: Not sure if we will use this in the future or not since augment mod data being
 
-- stored in M:Weapons/data is not really appropriate
 
local function getAugments(Weapon)
 
local name = Weapon.Name ~= nil and Weapon.Name or Weapon
 
local augments = {}
 
for i, Augment in pairs(WeaponData["Augments"]) do
 
for j, WeapName in pairs(Augment.Weapons) do
 
if (WeapName == name) then
 
table.insert(augments, Augment)
 
end
 
end
 
end
 
return augments
 
 
end
 
end
   
Line 1,378: Line 1,363:
 
end
 
end
 
table.insert(result, CATEGORY_MAP[Weapon['Class']]..CATEGORY_MAP[Weapon['Trigger']]..CATEGORY_MAP[Weapon['Type']])
 
table.insert(result, CATEGORY_MAP[Weapon['Class']]..CATEGORY_MAP[Weapon['Trigger']]..CATEGORY_MAP[Weapon['Type']])
 
-- TODO: Archive this category b/c not sure how important it is or how often
 
-- it is used; augments are moved out of M:Weapons/data and should use M:Mods/data
 
local augments = getAugments(Weapon)
 
if (Table.size(augments) > 0) then
 
table.insert(result, "[[Category:Augmented Weapons]]")
 
end
 
 
 
 
-- Adding appropriate categories to page based on weapon's categorical traits
 
-- Adding appropriate categories to page based on weapon's categorical traits

Revision as of 06:40, 31 July 2021


Weapons contains all of WARFRAME's non-modular weapon data.

Usage

Template

In template and articles: {{#invoke:Weapons|function|input1|input2|...}}

Navigation

Quick navigation to submodules:

Product Backlog

Name Type Status Priority Assignee Description Date Issued Last Update
Module:StatObject as OOP paradigm Dev Planning Low

Currently our usage of Module:StatObject is as a static class with statRead and statFormat functions. Update Module:Weapons and Module:StatObject so we can 'instantiate' an actual StatObject object that takes in a weapon table entry as an argument. This way we can just do:

local StatObject = require('Module:StatObject') -- Base class
local WeaponData = require('Module:Weapons/data')

-- Doing some metaprogramming to extend functionality of StatObject class
StatObject.default = {
    Name = { nil, 'Weapon Name: %s' },  -- Sample definition for Name field getter/formatter
    ...
}

local BratonStatObject = StatObject(WeaponData['Braton'])

-- Get raw Name value "Braton" instead of StatObject.statRead(WeaponData['Braton'], 'Name')
local name = BratonStatObject.Name
-- Get formatted Name value "Weapon Name: Braton" (as defined in StatObject.default) instead of StatObject.statFormat(WeaponData['Braton'], 'Name')
print(BratonStatObject.Name)
mw.log(BratonStatObject.Name)
local formattedName = tostring(BratonStatObject.Name)
-- If the above is not possible in Lua then maybe add a __call metamethod to Name key to return its formatted value
formattedName = BratonStatObject.Name()
-- Or add a format() function to instantiated StatObject's metatable, passing in key name as argument
formattedName = BratonStatObject:format('Name')
22:01, 5 December 2022 (UTC)
Include attack name/context in Module:Weapons/ppdata Dev Planning Low

Update Module:Weapons/ppdata/seeder to add attack names associated with the respective stats used for comparing so that Module:Weapons/characteristics can add additional context to the stat comparisons. See https://warframe.fandom.com/wiki/Quassus?commentId=4400000000003635575. For Quassus Quassus's case, JatKusar Jat Kusar has a base 35% crit chance, but since we are comparing against non-normal attacks, Quassus Quassus's Ethereal Daggers will have second highest crit chance (30%) behind TenetExec Tenet Exec's slam shockwaves (38%).

22:09, 5 August 2022 (UTC)
Weapon and Attack classes Refactor and Dev Planning Low
  • Create a new Weapon and Attack class that can be instantiated by passing in a weapon table entry and attack table entry respectively. Each of these classes should contain a statRead() and statFormat() function that can be called to return an particular weapon stat, aggregate data, or computed stat based on /data contents.
21:18, 18 January 2022 (UTC)
ExplosionDelay key Refactor Planning Low
  • Update this to store a table with two values: shortest delay time and longest delay time. This is to support Kompressa Kompressa's variable delay time. For weapons that have a single delay time, use the same value for both table elements.
22:10, 6 January 2022 (UTC)
Reload key Refactor Planning Low
  • Move Reload key to attack tables? Nagantaka and Ambassador have two different reload times depending on attack.
  • 21:35, 19 January 2022 (UTC) update: User:Gigamicro suggests that any duplicate keys nested in Attack table entries should 'override' the base values for the weapon. In this case, add a another Reload key under the appropriate attack that has a different value than the Reload key in the main weapon entry.
Augments in /data Refactor New Low
  • Remove weapon augments list from /data and use Module:Mods and/or Module:Mods/data instead to fetch augment mods data.
    • Would require dev work in M:Mods/data too to index by mod type.
01:37, 31 May 2021 (UTC)
Data validation Dev/database Active Medium

Create Module:Weapons/data/validate subpage of /data for data validation functions

  • Include type checking for each column/attribute
  • Include checking if a table entry has the required keys (the minimum number of keys needed to support basic features in Module:Weapons)
  • Include boundary checking for stat values (e.g. CritChance cannot be negative)
01:37, 31 May 2021 (UTC) 23:33, 1 August 2021 (UTC)
Error handling Clean up New Medium

Change all return statements with "ERROR" to either assert() or error() to standardize error handling.

  • Error messages should be in the form of "functionName(argument names): argument value 1 is not a valid number".
01:37, 31 May 2021 (UTC)
Update database schema Database Active Medium User:Cephalon Scientia

Reworking how attacks are stored in tables for flexibility. Should have one Attack column that contains multiple tables, each representing a unique attack for that weapon. Would probably improve/simplify Weapon Comparison and Template:WeaponInfoboxAutomatic in displaying multiple attacks of a weapon. Right now we are hacking the use of ChargeAttack and SecondaryAttack for some attacks that are not necessarily charged or alt-fire (see Deconstructor's entry in the database). Also include forced proc data for all possible attacks (e.g. Glaives, some forced Impact weapons, etc.).

23:33, 1 August 2021 (UTC) update: There are lots of changes to these tables as I slowly create validation functions to check what keys-value pairs are needed or not, see documentation in Module:Weapons/data/doc for possible key-value pairs. Right now, attacks are stored in generic Attack1, Attack2, ... keys when we were changing the names of attack keys (e.g. NormalAttack became Attack1).

21:18, 18 January 2022 (UTC) update: User:Gigamicro implemented a new attack key (should be named Attacks to match key naming convention) that points to an array of attack tables. Attack1 to Attack9 keys are still in the data, just we now have a new way of indexing the same attack data by reference.

21:35, 19 January 2022 (UTC) update: Attack1 to Attack9 are now depreciated and removed from data tables. All optional keys are now explicitly stored in database (but are still optional b/c we set default values for getter functions in Module:Weapons. Some error clean up is still needed but for the most part, all weapon submodules and weapon tooltips should properly use Attacks table.

01:50, 31 May 2021 (UTC) 21:35, 19 January 2022 (UTC)
Unit tests Testing Archived High User:Cephalon Scientia

Add unit tests in Module:Weapons/testcases for each function in Module:Weapons. See Module:Math/testcases for examples and https://dev.fandom.com/wiki/Global_Lua_Modules/Testharness for documentation on how to format tests.

20:29, 31 July 2021 (UTC) update: Do not feel like it is appropriate to add unit tests using Module:TestHarness to most of the functions in this module since they mostly pertain to building wikitext to display to the reader. We can add a Module:Weapons/testcases subpage for visual tests to ensure rendered wikitext is not broken. Otherwise, I think it is more important to validate the data in Module:Weapons/data which are being formatted and displayed to the reader.

02:01, 31 May 2021 (UTC) 20:29, 31 July 2021 (UTC)

Finished Issues

Name Type Status Priority Assignee Description Date Issued Completion Date
Advantages/disadvantages Refactor Completed Low User:Cephalon Scientia
  • Move advantages/disadvantages builder to a new submodule page for organization.
06:05, 3 October 2021 (UTC) 17:47, 2 November 2021 (UTC)
Railjack Weapons Dev/Edit/Database Long-term support Medium User:Cephalon Scientia
  • Officially support Railjack weapons being in database. This allows Railjack weapons to make use of our infobox builder and do automatic comparisons.
  • Update /data with current user-contributed data in Railjack/Armaments

02:32, 6 September 2021 (UTC) update: Added most Railjack turrets and ordnances to /data. Missing Zetki Photor MK II, Zetki Carcinnox MK II, and Zetki Apoc MK I. First two are not obtainable in-game at this time since they are missing from drop tables, but are still in players' inventories.

00:12, 29 September 2021 (UTC) update: Added Zetki Photor MK II, Zetki Carcinnox MK II, and Zetki Apoc MK I stats according to the Mobile Export.

22:17, 2 August 2021 (UTC) 00:12, 29 September 2021 (UTC)
Update Conclave database schema Database Long-term support Low User:Cephalon Scientia

Remove keys that represent PvE stats as they are irrelevant to PvP. Most other key-value pairs (except those in attack tables) are shared with /data and can be looped through to add shared key-value pairs.

06:18, 10 August 2021 (UTC) 03:10, 16 August 2021 (UTC)
Clean up Clean up Completed Medium User:Cephalon Scientia
  • Remove unused functions. Also remove redundant functions that can be otherwise be used as as simple condition check (e.g. HasAttack() and DoNotHasAttack()).
  • Standardize styling to WARFRAME Wiki:Programming Standards.
01:37, 31 May 2021 (UTC) 06:20, 10 August 2021 (UTC)
Refactoring Refactor Long-term support Medium User:Cephalon Scientia

Refactor these functionalities for code reuse, better performance, better maintainability, and etc.:

  • Weapon comparison tables (see Weapon Comparison) ✔️
    • Includes Conclave ✔️
    • 00:19, 7 August 2021 (UTC) update: Weapon comparison tables are moved to Module:Weapons/comptable submodule for seperations of concerns design
  • Polarity table (see Polarity) ✔️
  • Mastery table (see Mastery Rank) ✔️
  • Highest physical damage type table (see Damage/Impact Damage) ✔️
  • Getter function(s) for weapon statistics/properties ✔️
  • Weapon gallery ✔️
17:43, 3 June 2021 (UTC) 00:19, 7 August 2021 (UTC)
Documentation Documentation New High

Add LuaDoc-style documentation for all functions.

01:37, 31 May 2021 (UTC) 06:35, 31 July 2021 (UTC)
Comparison tables and comparing two weapons Refactor Completed High User:Cephalon Scientia

Refactor BuildCompRow(), BuildCompTable(), BuildGunComparisonString, and their sister functions.

  • BuildCompRow() should have at max 2 nested if/else blocks
  • Do not remove functions such as getCompTableGuns(frame) and p.getCompTableArchGuns(frame) as those will be exposed to articles. Do not think there is a way to pass in table arguments in {{#invoke:}} calls so we could not have a single table builder function. Not true, can pass a multitude of named arguments and use frame.args['argName'] to get those arguments.

20:53, 29 July 2021 (UTC) update: We now use getValue(Weapon, keyName, attackName) and dictionaries (e.g. GUN_KEY_MAP) that contain simple getter functions to get specific weapon stat values. No more complicated nested if/else blocks.

01:37, 31 May 2021 (UTC) 20:53, 29 July 2021 (UTC)
getAttackValue() Refactor Completed Medium User:Cephalon Scientia

Refactor getAttackValue(Weapon, Attack, ValName, giveDefault, asString, forTable).

  • If/else blocks can be converted into a dictionary or objects that can be constructed through the use of tables and metatables.

20:53, 29 July 2021 (UTC) update: getAttackValue() has been removed and its functionality is moved to getValue(Weapon, keyName, attackName) using dictionaries (e.g. GUN_KEY_MAP) that contain simple getter functions.

01:37, 31 May 2021 (UTC) 20:53, 29 July 2021 (UTC)
p.getRivenDispositionTable() Refactor Completed Low
  • Optimized p.getRivenDispositionTable() so we don't literally have to perform over a thousand loops through each exact possible Disposition value to find weapons with that exact Disposition.
  • Remove p.getRivenDispositionList() since it is not invoked by itself on pages and was used by old p.getRivenDispositionTable().
3:42, 21 July 2021 (UTC) 4:49, 21 July 2021 (UTC)
p.buildDamageTypeTable(frame) Refactor Completed Low
  • Use multi-line strings for table building.
  • Updated validate function to only get non-Kitgun weapons and only display damage type distribution of Attack1 or the attack listed in TooltipAttackDisplay.
  • Refactored to modern programming standards.
01:37, 31 May 2021 (UTC) 21:54, 20 July 2021 (UTC)
p.buildAutoboxCategories(frame) Refactor Completed Medium

Implement a map/dictionary for mapping traits and trigger types to category link.

01:37, 31 May 2021 (UTC) 21:54, 20 July 2021 (UTC)
Weapon nav Dev Completed Medium User:FINNER

Add a new function that constructs the same navbox as Template:WeaponNav. Goal is to reduce memory used by calling Template:Weapon 400+ times on every weapon page as well as automating navbox updates whenever a new weapon is added. Right now, Template:Weapon uses ~11MB, sometimes ~20MB on pages like Volnus Prime.

Weapon navigation box generator resides in Module:Weapons/nav.

03:24, 5 June 2021 (UTC) 6:07, 7 June 2021 (UTC)
Weapon infobox Dev Completed Medium User:FINNER

Migrate wikitext from Template:WeaponInfoboxAutomatic into a infobox builder function.

  • This will probably remove the need for getValue() and their equilvalents.

Weapon infobox generator resides in Module:Weapons/infobox.

01:37, 31 May 2021 (UTC) 22:14, 8 June 2021 (UTC)
High lua memory usage Dev/Debugging Completed High User:FINNER

Some weapon pages have unusually high memory usage for Lua scripts, this will be problematic the moment we add new weapons to Template:WeaponNav:

Normally, memory usage is ~37 MB which is why this is odd.

This issue has been fixed when we now generate Template:WeaponNav using this module, instead of calling Template:Weapon 400+ times per page.

02:04, 1 June 2021 (UTC) 18:03, 7 June 2021 (UTC)

Forked Repos

Documentation

Package items

weapons.csvGunComparisonTable(frame) (function)
Builds a CSV table of all WARFRAME's guns with the exception of Kitguns.
Parameter: frame Frame object (table)
Returns: Preformatted text of CSV text (string)
weapons._isVariant(weaponName) (function)
Checks if a weapon is a variant or not.
Parameter: weaponName Weapon name (string)
Returns:
  • True if weapon is a variant, false otherwise (boolean)
  • Weapon's variant name or "Base" if weapon is not a variant (string)
  • Weapon name, same as weaponName (string)
weapons._buildName(baseName, variant) (function)
Builds the full name of a weapon's variant. Does not check if it exists or not.
Parameters:
  • baseName Weapon's base name (e.g. "Braton") (string)
  • variant Variant name (e.g. "Vandal") (string)
Returns: Weapon's variant name (e.g. "Braton Vandal") (string)
weapons._shortLinkList(Weapon, tooltip) (function)
Builds a list of weapons, with variants being next to base weapon name inside parentheses (e. g. Braton (MK1, Prime)).
Parameters:
  • Weapon Weapon table (table)
  • tooltip If true, adds weapon tooltips, false otherwise; defaults to false (boolean)
Returns: Wikitext of resultant list (string)
weapons._getWeapon(weaponName) (function)
Returns a specific weapon table entry from /data.
Parameter: weaponName Weapon name (string)
Returns: Weapon table (table)
weapons._getConclaveWeapon(weaponName) (function)
Returns a specific weapon table entry from /Conclave/data.
Parameter: weaponName Weapon name (string)
Returns: Weapon table (table)
weapons._attackLoop(Weapon) (function)
Loops through all possible attacks that a weapon may have.
Parameter: Weapon Weapon entry as seen in /data (table)
Returns:
  • An iterator function that returns the key-value pair of next attack entry (function)
  • Original weapon entry passed into function (table)
weapons._getWeapons(validateFunction) (function)
Returns a subset of /data based on a validation function.
Parameter: validateFunction Function that filters out a weapon by taking in a Weapon table argument (function)
Returns: Table of weapon table entries as seen in /data (table)
weapons._getConclaveWeapons(validateFunction) (function)
Returns a subset of /Conclave/data based on a validation function.
Parameter: validateFunction Function that filters out a weapon by taking in a Weapon table argument (function)
Returns: Table of weapon table entries as seen in /Conclave/data (table)
weapons.getWeaponStanceList(frame) (function)
Builds list of stances for a particular melee weapon as seen on Template:MeleeCategory.
Parameter: frame Frame object w/ first argument being string weaponName (table)
Returns: Resultant wikitext of comparison list (string)
weapons.getStanceWeaponList(frame) (function)
Builds list of weapons that can equip a particlar stance mod as seen on Template:StanceWeapons.
Parameter: frame Frame object w/ first argument being string stanceName (table)
Returns: Resultant wikitext of comparison list (string)
weapons.getMeleeWeaponGallery(frame) (function)
Builds a melee weapon gallery as seen on Template:MeleeCategory.
Parameter: frame Frame object w/ first argumenting being string meleeClass (table)
Returns: Resultant wikitext of gallery (string)
weapons.getWeaponCount(frame) (function)
Gets the total count of weapons as used on Mastery Rank.
Parameter: frame Frame object w/ the first argument being the weaponType, second argument being a boolean to getFullList, and the third being a boolean to getAll weapons in /data (table)
Returns: Total count of weapons in a certain category/type (number)
weapons.getPolarityTable(frame) (function)
Builds wikitable of all weapons' innate polarities as seen on Polarity.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons.buildComparison(frame) (function)
Builds comparison list between two weapons in PvE.
Parameter: frame Frame object (table)
Returns: Resultant wikitext of comparison list (string)
weapons.buildComparison(frame) (function)
Builds comparison list between two weapons in PvP (Conclave).
Parameter: frame Frame object (table)
Returns: Resultant wikitext of comparison list (string)
weapons.buildAutoboxCategories(frame) (function)
Adds weapon categories.
Parameter: frame Frame object (table)
Returns: Wikitext of category links (string)
weapons.buildDamageTypeTable(frame) (function)
Builds a table that lists out all weapons with a certain damage type and the percentage that it makes up of their base damage of the attack specified in their tooltip on the wiki.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons.getMasteryShortList(frame) (function)
Builds a list of weapons' mastery requirements as seen on Template:EquipmentUnlock.
Parameter: frame Frame object w/ first argument being a string weaponType (table)
Returns: Wikitext of resultant list (string)
weapons.getRivenDispositionTable(frame) (function)
Builds a disposition wikitable as seen on Riven Mods/Weapon Dispos.
Parameter: frame Frame object w/ first argument being a string weaponType (table)
Returns: Wikitext of resultant wikitable (string)
weapons.getConclaveList(frame) (function)
Builds a list of PvP weapons.
Parameter: frame Frame object w/ first argument being a string weaponType (table)
Returns: Wikitext of resultant list (string)
weapons.getCompTableGuns(frame) (function)
Builds comparison table of gun stats as seen on Weapon Comparison.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons.getCompTableConclaveGuns(frame) (function)
Builds comparison table of gun Conclave stats as seen on Weapon Comparison/Conclave.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons.getCompTableMelees(frame) (function)
Builds comparison table of melee stats as seen on Weapon Comparison.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons.getCompTableConclaveMelees(frame) (function)
Builds comparison table of melee conclave stats as seen on Weapon Comparison/Conclave.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons.getCompTableArchMelees(frame) (function)
Builds comparison table of arch-melee stats as seen on Weapon Comparison.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons.getCompTableSpeedGuns(frame) (function)
Builds comparison table of projectile flight speeds as seen on Projectile Speed.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons.getCompTableSpeedMelees(frame) (function)
Builds comparison table of glaive melees' projectile flight speeds as seen on Projectile Speed.
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)

Created with Docbunto

See Also

Code


--- '''Weapons''' contains all of [[WARFRAME]]'s non-modular [[Weapons|weapon]] data.<br />
--  
--  @module	 weapons
--  @alias	  p
--  @author [[User:Falterfire|Falterfire]]
--  @attribution	[[User:Flaicher|Flaicher]]
--  @attribution	[[User:FINNER|FINNER]]
--  @attribution	[[User:Cephalon Scientia|Cephalon Scientia]]
--  @attribution	[[User:Gigamicro|Gigamicro]]
--  @attribution	[[User:Synthtech|Synthtech]]
--  @attribution	[[User:Calenhed|Calenhed]]
--  @image	  
--  @require	[[Module:Icon]]
--  @require	[[Module:Math]]
--  @require	[[Module:String]]
--	@require	[[Module:Table]]
--  @require	[[Module:Mods/data]]
--  @require	[[Module:Modular/data]]
--  @require	[[Module:Stances/data]]
--  @require	[[Module:Tooltips]]
--  @require	[[Module:Version]]
--  @require	[[Module:Weapons/data]]
--  @require	[[Module:Weapons/Conclave/data]]
--  @release	stable
--  <nowiki>

local p = {}

local ConclaveData = mw.loadData [[Module:Weapons/Conclave/data]]
local ModData = mw.loadData [[Module:Mods/data]]
local ModularData = mw.loadData [[Module:Modular/data]]
local StanceData = mw.loadData [[Module:Stances/data]]
local WeaponData = mw.loadData [[Module:Weapons/data]]

local Icon = require [[Module:Icon]]
local Math = require [[Module:Math]]
local String = require [[Module:String]]
local Tooltip = require [[Module:Tooltips]]
local Version = require [[Module:Version]]
local Table = require [[Module:Table]]
local Polarity = require [[Module:Polarity]]

-- TODO: Could use M:DamageTypes instead of this table?
local Elements = {
	"Impact", "Puncture", "Slash", "Heat", "Cold", "Toxin", 
	"Electricity", "Blast", "Corrosive", "Radiation", "Magnetic", "Gas", 
	"Viral", "Void", "True", "MinProgenitorBonus"
}

local VARIANT_LIST = {
	"Prime", "Prisma", "Wraith", "Vandal", "Vaykor", "Synoid", 
	"Telos", "Secura", "Sancti", "Rakta", "Mara", "MK1", "Kuva"
}

local function makeDTooltip(dt, addText)
	local text = (addText) and dt or ''
	return ('<span class="tooltip" data-param="%s" data-param2="DamageTypes">[[File:Dmg%sSmall64.png|x19px|link=]]&nbsp;%s</span>')
		:format(dt, dt, text)
end

--- Returns the damage type with the highest damage distribution in an attack.
--	@function		getDamageBias
--	@param			{table} attackEntry Attack table
--	@returns		{number} Damage distribution as a decimal (e.g. 1 being 100%)
--	@returns		{string} Name of damage type with highest distribution
local function getDamageBias(attackEntry)
	if (attackEntry.Damage ~= nil and Table.size(attackEntry.Damage) > 0) then
		local totalDmg = 0
		local bestDmg = 0
		local bestElement = nil
		local elemCount = 0
		for damageType, dmg in pairs(attackEntry.Damage) do
			if (dmg > bestDmg) then
				bestDmg = dmg
				bestElement = damageType
			end
			totalDmg = totalDmg + dmg
			if (dmg > 0) then
				elemCount = elemCount + 1
			end
		end
		-- Make sure there are two damage instances that are above zero
		-- Exception for physical damage types
		if (elemCount > 0) then
			return (bestDmg / totalDmg), bestElement
		end
		error('getDamageBias(Attack): '..
			'Damage key in Attack entry has no key-value pairs'..mw.dumpObject(attackEntry))
	end
	error('getDamageBias(Attack): Attack entry has no Damage key')
end

---	Returns the highest damage type of an attack in the form of a percentage
--	followed by a tooltip icon of the damage type
--	@function		getDamageBiasString
--	@param			{table} attackEntry Attack table
--	@returns		{string} Resultant text
local function getDamageBiasString(attackEntry)
	local bestPercent, bestElement = getDamageBias(attackEntry)
	local result = Math.percentage(bestPercent)
	return result..' '..makeDTooltip(bestElement)
end

--- Calculates the derived damage stats commonly used in comparing gun performance.
--	Using what attacks the weapons tooltips are displaying for DPS calculations.
--	@function		calculateGunDerivedDamage
--	@param			{table} Weapon Weapon table data as seen in M:Weapons/data
--	@returns		{number} Returns five number stats in a tuple: 
--				* Total damage: final damage when accounting multishot; same as arsenal display
--				* Average shot: damage per single input
--				* Average burst DPS: damage per second without reloading
--				* Average sustained DPS: damage per second w/ reloading
--				* Average lifetime damage: total damage that can be dealt in a single magazine + reserve ammo w/o picking up ammo drops
local function calculateGunDerivedDamage(Weapon)
	local TooltipAttack = Weapon[Weapon['TooltipAttackDisplay'] or 'Attack1'] or {}
	
	local totalDamage = 0
	
	for damageType, value in pairs(TooltipAttack['Damage'] or {}) do
		if (damageType == 'MinProgenitorBonus') then
			-- For a more competitive comparison, assume that Lich weapons have 
			-- max base damage bonus (+60% of a Progenitor Warframe's element);
			-- The value stored in 'MinProgenitorBonus' is based on the minimum +25% bonus
			totalDamage = totalDamage + (value * 0.6/0.25)	
		else
			totalDamage = totalDamage + value
		end
	end
	totalDamage = totalDamage * (TooltipAttack['Multishot'] or 1)
	
	local critChance = TooltipAttack['CritChance']
	local critMultiplier = TooltipAttack['CritMultiplier']
	-- If an attack uses the charge trigger, use that instead since 
	-- theoretically players want to charge an attack for more damage/DPS
	local fireRate = TooltipAttack['ChargeTime'] and (1 / TooltipAttack['ChargeTime']) or TooltipAttack['FireRate']
	if TooltipAttack['ChargeTime'] and TooltipAttack['FireRate'] then
		fireRate = (1 / TooltipAttack['ChargeTime']) < TooltipAttack['FireRate'] and (1 / TooltipAttack['ChargeTime']) or TooltipAttack['FireRate']
	end
	local reloadTime = Weapon['Reload']
	local magazine = Weapon['Magazine']
	local maxAmmo = Weapon['MaxAmmo']
	
	-- TODO: If we are defining average shot as average damage dealt per a single attack input
	-- than this is is inaccurate for burst-fire attacks (being picky on wording, but it is 
	-- for more accurate calculations)
	local avgShot = totalDamage * (1 + critChance * (critMultiplier - 1))
	-- Extra one needed in calculation to account for initial filled mag
	-- If there is no reserve ammo, that means that weapon can deal an infinite amount of damage theoretically
	local avgLifetimeDmg = maxAmmo < math.huge and avgShot * (magazine / (TooltipAttack['AmmoCost'] or 1)) * (1 + (maxAmmo / magazine)) or math.huge
	
	-- Need to ignore the first shot of guns since it is instantaneous and is 
	-- not affected by fire rate (which causes the delay between shots)
	local numShotPerMag = magazine / (TooltipAttack['AmmoCost'] or 1)
	local avgBurst = avgShot * fireRate
	local avgSustained

	if Weapon.Name ~= 'Vectis' and Weapon.Name ~= 'Vectis Prime' then
		avgSustained = avgBurst * numShotPerMag / (fireRate * reloadTime + numShotPerMag)
	else
		avgSustained = avgBurst * numShotPerMag / (fireRate * reloadTime + numShotPerMag - 1)
	end
	
	return totalDamage, avgShot, avgBurst, avgSustained, avgLifetimeDmg
end

-- Getter functions for attack keys and derived stats
local ATTACK_KEY_MAP = {
	Impact = function(attackEntry) return attackEntry['Damage']['Impact'] or 0 end,
	Puncture = function(attackEntry) return attackEntry['Damage']['Puncture'] or 0 end,
	Slash = function(attackEntry) return attackEntry['Damage']['Slash'] or 0 end,
	Cold = function(attackEntry) return attackEntry['Damage']['Cold'] or 0 end,
	Electricity = function(attackEntry) return attackEntry['Damage']['Electricity'] or 0 end,
	Heat = function(attackEntry) return attackEntry['Damage']['Heat'] or 0 end,
	Toxin = function(attackEntry) return attackEntry['Damage']['Toxin'] or 0 end,
	Blast = function(attackEntry) return attackEntry['Damage']['Blast'] or 0 end,
	Corrosive = function(attackEntry) return attackEntry['Damage']['Corrosive'] or 0 end,
	Gas = function(attackEntry) return attackEntry['Damage']['Gas'] or 0 end,
	Magnetic = function(attackEntry) return attackEntry['Damage']['Magnetic'] or 0 end,
	Radiation = function(attackEntry) return attackEntry['Damage']['Radiation'] or 0 end,
	Viral = function(attackEntry) return attackEntry['Damage']['Viral'] or 0 end,
	Void = function(attackEntry) return attackEntry['Damage']['Void'] or 0 end,
	MinProgenitorBonus = function(attackEntry) return attackEntry['Damage']['MinProgenitorBonus'] or 0 end,
	AttackName = function(attackEntry) return attackEntry['AttackName'] or 'Normal' end,
	AmmoCost = function(attackEntry) return attackEntry['AmmoCost'] or 1 end,
	BurstCount = function(attackEntry) return attackEntry['BurstCount'] end,
	BurstFireRate = function(attackEntry) return attackEntry['BurstFireRate'] end,
	CompTableFireRate = function(attackEntry)
		-- if (attackEntry['FireRate'] ~= nil and attackEntry['ChargeTime'] ~= nil) then
		-- 	return attackEntry['FireRate'] > (1 / attackEntry['ChargeTime']) and attackEntry['FireRate'] or 
		-- 		Math.round(1 / attackEntry['ChargeTime'], 0.01)
		if (attackEntry['ChargeTime'] ~= nil) then
			return 1 / attackEntry['ChargeTime']
		end
		return attackEntry['FireRate']
	end,
	BaseDamage = function(attackEntry)
		local total = 0
		for damageType, value in pairs(attackEntry['Damage']) do
			total = total + value
		end
		return total
	end,
	TotalDamage = function(attackEntry)
		local total = 0
		for damageType, value in pairs(attackEntry['Damage']) do
			total = total + value
		end
		return total * (attackEntry['Multishot'] or 1)
	end,
	DamageBias = function(attackEntry) return getDamageBiasString(attackEntry) end,
	ChargeTime = function(attackEntry) return attackEntry['ChargeTime'] end,
	CritChance = function(attackEntry) return Math.percentage(attackEntry['CritChance']) end,
	CritMultiplier = function(attackEntry) return (attackEntry['CritMultiplier'])..'x' end,
	FalloffEnd = function(attackEntry) return attackEntry['Falloff'] and attackEntry['Falloff']['EndRange'] or 600 end,
	FalloffReduction = function(attackEntry) return attackEntry['Falloff'] and attackEntry['Falloff']['Reduction'] or 0.99 end,
	FalloffStart = function(attackEntry) return attackEntry['Falloff'] and attackEntry['Falloff']['StartRange'] or 300 end,
	FireRate = function(attackEntry) return attackEntry['FireRate'] end,
	HeadshotMultiplier = function(attackEntry)
		return attackEntry['HeadshotMultiplier'] and (attackEntry['HeadshotMultiplier'])..'x' or '1x'
	end,
	Multishot = function(attackEntry) return attackEntry['Multishot'] or 1 end,
	PelletName = function(attackEntry) return attackEntry['PelletName'] end,
	PunchThrough = function(attackEntry) return attackEntry['PunchThrough'] or 0 end,
	Radius = function(attackEntry) return attackEntry['Radius'] end,
	ShotSpeed = function(attackEntry) return attackEntry['ShotSpeed'] end,
	ShotType = function(attackEntry) return attackEntry['ShotType'] end,
	StatusChance = function(attackEntry) return Math.percentage(attackEntry['StatusChance']) end,
}

-- Getter functions for shared weapon keys and derived stats
local SHARED_KEY_MAP = {
	Class = function(weaponEntry) return weaponEntry['Class'] end,
	Disposition = function(weaponEntry)
		return weaponEntry['Disposition'] ~= nil and weaponEntry['Disposition'] or 'N/A'
	end,
	Family = function(weaponEntry) return weaponEntry['Family'] end,
	Introduced = function(weaponEntry)
		return weaponEntry['Introduced'] and Version._getVersion(weaponEntry['Introduced'])['Name'] or 'N/A'
	end,
	IntroducedDate = function(weaponEntry)
		return weaponEntry['Introduced'] and Version._getVersionDate(weaponEntry['Introduced']) or 'N/A'
	end,
	Link = function(weaponEntry) return '[['..weaponEntry['Link']..']]' end,
	Mastery = function(weaponEntry) return weaponEntry['Mastery'] or 'N/A' end,
	Name = function(weaponEntry) return '[['..weaponEntry['Name']..']]' end,
	NameLink = function(weaponEntry) return '[['..weaponEntry['Link']..'|'..weaponEntry['Name']..']]' end,
	Polarities = function(weaponEntry) return Polarity._pols(weaponEntry['Polarities']) end,
	Traits = function(weaponEntry)
		local traitString = {}
		for _, trait in ipairs(weaponEntry['Traits']) do
			table.insert(traitString, trait)
		end
		return table.concat(traitString, ', ')
	end,
	Type = function(weaponEntry) return weaponEntry['Type'] end
}

-- Getter functions for gun weapon keys and derived stats
local GUN_KEY_MAP = {
	Accuracy = function(weaponEntry)
		if (weaponEntry['Accuracy'] ~= nil) then return weaponEntry['Accuracy'] end
		return weaponEntry[weaponEntry['TooltipAttackDisplay'] or 'Attack1']['Accuracy']
	end,
	AmmoType = function(weaponEntry)
		if (weaponEntry['AmmoType'] ~= nil) then
			return weaponEntry['AmmoType']
		elseif(weaponEntry['Type'] == 'Secondary') then
			return 'Pistol'
		elseif(weaponEntry['Type'] == 'Primary') then
			local class = Weapon['Class']
			if (class == 'Rifle') then
				return 'Rifle'
			elseif (class == 'Shotgun') then
				return 'Shotgun'
			elseif (class == 'Bow') then
				return 'Bow'
			elseif (class == 'Sniper Rifle' or class == 'Launcher') then
				return "Sniper"
			end
		end
		return ''
	end,
	AvgProcCount = function(weaponEntry)
		local attackEntry = weaponEntry[weaponEntry['TooltipAttackDisplay'] or 'Attack1']
		local statusChance = attackEntry['StatusChance'] or 0
		local multishot = attackEntry['Multishot'] or 1
		local numForcedProcs = attackEntry['ForcedProcs'] and Table.size(attackEntry['ForcedProcs']) or 0
		return (statusChance + numForcedProcs) * multishot
	end,
	AvgProcPerSec = function(weaponEntry)
		local attackEntry = weaponEntry[weaponEntry['TooltipAttackDisplay'] or 'Attack1']
		local statusChance = attackEntry['StatusChance'] or 0
		local multishot = attackEntry['Multishot'] or 1
		local numForcedProcs = attackEntry['ForcedProcs'] and Table.size(attackEntry['ForcedProcs']) or 0
		local fireRate = (attackEntry['ChargeTime'] ~= nil) and (1 / attackEntry['ChargeTime']) or attackEntry['FireRate']
		return (statusChance + numForcedProcs) * multishot * fireRate
	end,
	AvgShotDmg = function(weaponEntry)
		local totalDamage, avgShot = calculateGunDerivedDamage(weaponEntry)
		return avgShot
	end,
	BurstDps = function(weaponEntry)
		local totalDamage, avgShot, avgBurst = calculateGunDerivedDamage(weaponEntry)
		return avgBurst
	end,
	ExilusPolarity = function(weaponEntry) return Polarity._polarity(weaponEntry['ExilusPolarity']) end,
	IsSilent = function(weaponEntry) return weaponEntry['IsSilent'] end,
	Magazine = function(weaponEntry) return weaponEntry['Magazine'] end,
	MaxAmmo = function(weaponEntry) return weaponEntry['MaxAmmo'] end,
	Range = function(weaponEntry) return weaponEntry['Range'] end,
	Reload = function(weaponEntry) return weaponEntry['Reload'] end,
	ReloadDelay = function(weaponEntry) return weaponEntry['ReloadDelay'] or 0 end,
	ReloadDelayEmpty = function(weaponEntry) return weaponEntry['ReloadDelayEmpty'] or 0 end,
	ReloadStyle = function(weaponEntry) return weaponEntry['ReloadStyle'] end,
	Spool = function(weaponEntry) return weaponEntry['Spool'] or 0 end,
	SustainedDps = function(weaponEntry)
		local totalDamage, avgShot, avgBurst, avgSustained = calculateGunDerivedDamage(weaponEntry)
		return avgSustained
	end,
	Trigger = function(weaponEntry) return weaponEntry['Trigger'] end
}

-- Getter functions for melee weapon keys and derived stats
local MELEE_KEY_MAP = {
	BlockAngle = function(weaponEntry) return weaponEntry['BlockAngle'] or 0 end,
	ComboDur = function(weaponEntry) return weaponEntry['ComboDur'] or 0 end,
	FollowThrough = function(weaponEntry) return weaponEntry['FollowThrough'] or 0 end,
	HeavyAttack = function(weaponEntry) return weaponEntry['HeavyAttack'] or 0 end,
	HeavyElement = function(weaponEntry) return weaponEntry['HeavyElement'] end,
	HeavySlamAttack = function(weaponEntry) return weaponEntry['HeavySlamAttack'] end,
	HeavySlamElement = function(weaponEntry) return weaponEntry['HeavySlamElement'] end,
	HeavyRadialDmg = function(weaponEntry) return weaponEntry['HeavyRadialDmg'] or 0 end,
	HeavyRadialElement = function(weaponEntry) return weaponEntry['HeavyRadialElement'] end,
	HeavySlamRadius = function(weaponEntry) return weaponEntry['HeavySlamRadius'] or 0 end,
	MeleeRange = function(weaponEntry) return weaponEntry['MeleeRange'] or 0 end,
	SlamAttack = function(weaponEntry) return weaponEntry['SlamAttack'] or 0 end,
	SlamElement = function(weaponEntry) return weaponEntry['SlamElement'] end,
	SlamRadialDmg = function(weaponEntry) return weaponEntry['SlamRadialDmg'] or 0 end,
	SlamRadialElement = function(weaponEntry) return weaponEntry['SlamRadialElement'] end,
	SlamRadialProc = function(weaponEntry) return weaponEntry['SlamRadialProc'] end,
	SlamRadius = function(weaponEntry) return weaponEntry['SlamRadius'] or 0 end,
	SlideAttack = function(weaponEntry) return weaponEntry['SlideAttack'] or 0 end,
	SlideElement = function(weaponEntry) return weaponEntry['SlideElement'] end,
	Stances = function(weaponEntry) return getWeaponStanceList(weaponEntry) end,
	StancePolarity = function(weaponEntry)
		return weaponEntry['StancePolarity'] ~= nil and
			Polarity._polarity(weaponEntry['StancePolarity']) or 'N/A'
	end,
	WindUp = function(weaponEntry) return weaponEntry['WindUp'] or 0 end
}

-- For mapping weapon traits or types to a category link
local CATEGORY_MAP = {
	Primary = '[[Category:Primary Weapons]]',
	Secondary = '[[Category:Secondary Weapons]]',
	Melee = '[[Category:Melee Weapons]]',
	['Arch-Melee'] = '[[Category:Archwing Melee]]',
	['Arch-Gun'] = '[[Category:Archwing Gun]]',
	['Arch-Gun (Atmosphere)'] = '[[Category:Archwing Gun]]',
	Kitgun = '[[Category:Kitgun]]',
	Zaw = '[[Category:Zaw]]',
	['Railjack Armament'] = '[[Category:Railjack]]',
	Gear = '[[Category:Gear]]',
	
	Rifle = '[[Category:Assault Rifle]]',
	['Sniper Rifle'] = '[[Category:Sniper Rifle]]',
	Shotgun = '[[Category:Shotgun]]',
	Pistol = '[[Category:Pistol]]',
	['Dual Pistols'] = '[[Category:Dual Pistols]]',
	Bow = '[[Category:Bow]]',
	Launcher = '[[Category:Launcher]]',
	['Arm-Cannon'] = '[[Category:Arm-Cannon]]',
	['Speargun'] = '[[Category:Speargun]]',
	Thrown = '[[Category:Thrown]]',
	['Shotgun Sidearm'] = '[[Category:Shotgun Sidearm]]',
	
	Prime = '[[Category:Prime Weapons]]',
	['Never Vaulted'] = '[[Category:Never Vaulted]]',
	Vaulted = '[[Category:Vaulted]]',
	Wraith = '[[Category:Wraith]]',
	Vandal = '[[Category:Vandal]]',
	-- Maybe replace with 'Kuva' category? Though technically Broken Scepter is a "Kuva" weapon which
	-- is why this distinction is made (in the past, editors mislabeled this trait)
	['Kuva Lich'] = '[[Category:Kuva Lich]]',
	Prisma = '[[Category:Prisma]]',
	
	Grineer = '[[Category:Grineer Weapons]]',
	Corpus = '[[Category:Corpus Weapons]]',
	Infested = '[[Category:Infested Weapons]]',
	Tenno = '[[Category:Tenno Weapons]]',
	Sentient = '[[Category:Sentient Weapons]]',
	Entrati = '[[Category:Entrati Weapons]]',
	Baro = '[[Category:Baro Ki\'Teer Offering]]',
	Syndicate = '[[Category:Syndicate Offerings]]',
	['Invasion Reward'] = '[[Category:Invasion Reward]]',
	
	['Alt Fire'] = '[[Category:Weapons with Alt Fire]]',
	['AoE'] = '[[Category:Weapons with Area of Effect]][[Category:Self Interrupt Weapons]]',
	
	Active = '[[Category:Active]]',
	Auto = '[[Category:Automatic]]',
	['Auto-Spool'] = '[[Category:Automatic]]',
	Burst = '[[Category:Burst Fire]]',
	['Auto-Burst'] = '[[Category:Burst Fire]]',
	['Auto Charge'] = '[[Category:Charge]]',
	Charge = '[[Category:Charge]]',
	Duplex = '[[Category:Duplex Fire]]',
	['Semi-Auto'] = '[[Category:Semi-Automatic]]',
	Held = '[[Category:Continuous Weapons]]'
}

---	Builds a CSV table of all WARFRAME's guns with the exception of Kitguns.
--	@function		p.csvGunComparisonTable
--	@param			{table} frame Frame object
--	@returns		{string} Preformatted text of CSV text
function p.csvGunComparisonTable(frame)
	-- Weapon types to show in resultant table
	local weaponTypesFilter = { 'Primary', 'Secondary', 'Robotic', 'Arch-Gun', 'Arch-Gun (Atmosphere)', 'Amp' }
	local tableEntryTemplate = {}	-- Would look like '%s,%s,%s'
	local tableHeader = { 
		'Name',
		'Trigger',
		'AttackName',
		'Impact',
		'Puncture',
		'Slash',
		'Cold',
		'Electricity',
		'Heat',
		'Toxin',
		'Blast',
		'Corrosive',
		'Gas',
		'Magnetic',
		'Radiation',
		'Viral',
		'Void',
		'MinProgenitorBonus',
		'Damage',
		'TotalDmg',
		'CritChance',
		'CritMultiplier',
		'AvgShotDmg',
		'BurstDps',
		'SustainedDps',
		'LifetimeDmg',
		'StatusChance',
		'AvgProcCount',
		'AvgProcPerSec',
		'Multishot',
		'FireRate',
		'Disposition',
		'Mastery',
		'Magazine',
		'MaxAmmo',
		'Reload',
		'ShotType',
		'PunchThrough',
		'Accuracy',
		'Introduced',
		'IntroducedDate',
		'Type',
		'Class',
		'AmmoType'
	}
	
	for i, _ in ipairs(tableHeader) do table.insert(tableEntryTemplate, '%s,') end
	tableEntryTemplate[#tableEntryTemplate] = '%s'	-- Last column
	tableEntryTemplate = table.concat(tableEntryTemplate)
	
	local csvResult = { '<pre>' }
	table.insert(csvResult, string.format(tableEntryTemplate, unpack(tableHeader)))
	
	for weaponName, weaponData in Table.skpairs(WeaponData['Weapons']) do
		if (Table.contains(weaponTypesFilter, weaponData['Type'])) then
			-- Going through all the possible Attack keys and adding them to CSV
			-- (TODO: consolidate these keys into one Attack table with key-value pairs)
			for i = 1, 9, 1 do
				if (weaponData['Attack'..i] ~= nil) then
					local totalDamage, avgShot, avgBurst, avgSustained, avgLifetimeDmg = calculateGunDerivedDamage(weaponData)
					local weaponAttack = weaponData['Attack'..i]
					
					local baseDamage = 0
					for damageType, damageValue in pairs(weaponAttack['Damage']) do
						baseDamage = baseDamage + damageValue
					end
					
					local tableEntryValues = { 
						weaponName,
						tostring(weaponData['Trigger']),
						tostring(weaponAttack['AttackName'] or 'Normal'),
						tostring(weaponAttack['Damage']['Impact'] or 0),
						tostring(weaponAttack['Damage']['Puncture'] or 0),
						tostring(weaponAttack['Damage']['Slash'] or 0),
						tostring(weaponAttack['Damage']['Cold'] or 0),
						tostring(weaponAttack['Damage']['Electricity'] or 0),
						tostring(weaponAttack['Damage']['Heat'] or 0),
						tostring(weaponAttack['Damage']['Toxin'] or 0),
						tostring(weaponAttack['Damage']['Blast'] or 0),
						tostring(weaponAttack['Damage']['Corrosive'] or 0),
						tostring(weaponAttack['Damage']['Gas'] or 0),
						tostring(weaponAttack['Damage']['Magnetic'] or 0),
						tostring(weaponAttack['Damage']['Radiation'] or 0),
						tostring(weaponAttack['Damage']['Viral'] or 0),
						tostring(weaponAttack['Damage']['Void'] or 0),
						tostring(weaponAttack['Damage']['MinProgenitorBonus'] or 0),
						tostring(baseDamage),
						tostring(totalDamage),
						tostring(weaponAttack['CritChance']),
						tostring(weaponAttack['CritMultiplier']),
						tostring(avgShot),
						tostring(avgBurst),
						tostring(avgSustained),
						tostring(avgLifetimeDmg),
						tostring(weaponAttack['StatusChance']),
						tostring((weaponAttack['StatusChance'] or 0) * (weaponAttack['Multishot'] or 1)),
						tostring(GUN_KEY_MAP['AvgProcPerSec'](weaponData)),
						tostring(weaponAttack['Multishot'] or 1),
						-- If an attack uses the charge trigger, use that instead since 
						-- theoretically players want to charge an attack for more damage/DPS
						tostring(weaponAttack['ChargeTime'] and (1 / weaponAttack['ChargeTime']) or weaponAttack['FireRate']),
						tostring(weaponData['Disposition']),
						tostring(weaponData['Mastery']),
						tostring(weaponData['Magazine']),
						tostring(weaponData['MaxAmmo']),
						tostring(weaponData['Reload']),
						tostring(weaponAttack['ShotType']),
						tostring(weaponAttack['PunchThrough'] or 0),
						tostring(weaponData['Accuracy']),
						tostring(weaponData['Introduced'] and Version._getVersion(weaponData['Introduced'])['Name'] or nil),
						tostring(weaponData['Introduced'] and Version._getVersionDate(weaponData['Introduced']) or nil),
						tostring(weaponData['Type']),
						tostring(weaponData['Class']),
						tostring((weaponData['Type'] == 'Secondary' and 'Pistol') or weaponData['AmmoType'] or weaponData['Class'])
					}
					local tableEntry = string.format(tableEntryTemplate, unpack(tableEntryValues))
					table.insert(csvResult, tableEntry)
				end
			end
		end
	end
	
	table.insert(csvResult, '</pre>')
	return table.concat(csvResult, '\n')
end

---	Checks if a weapon is a variant or not.
--	@function		p._isVariant
--	@param			{string} weaponName Weapon name
--	@returns		{boolean} True if weapon is a variant, false otherwise
--	@returns		{string} Weapon's variant name or "Base" if weapon is not a variant
--	@returns		{string} Weapon name, same as weaponName
function p._isVariant(weaponName)
	for i, var in pairs(VARIANT_LIST) do
		if (string.find(weaponName, var)) then
			local baseName = string.gsub(weaponName, " ?"..var.." ?-?", "")
			return true, var, baseName
		end
	end
	return false, "Base", weaponName
end

---	Builds the full name of a weapon's variant. Does not check if it exists or not.
--	@function		p._buildName
--	@param			{string} baseName Weapon's base name (e.g. "Braton")
--	@param			{string} variant Variant name (e.g. "Vandal")
--	@returns		{string} Weapon's variant name (e.g. "Braton Vandal")
function p._buildName(baseName, variant)
	if (variant == nil or variant == 'Base' or variant == '') then
		return baseName
	elseif (baseName == 'Laser Rifle' and variant == 'Prime') then
		return variant..' '..baseName	-- Laser Rifle has a primed version called 'Prime Laser Rifle'
	elseif (variant == 'Prime' or variant == 'Wraith' or variant == 'Vandal') then
		return baseName..' '..variant
	elseif (variant == 'MK1') then
		return 'MK1-'..baseName
	end
	return variant..' '..baseName
end

-- TODO: Function can be refactored
--- Builds a list of weapons, with variants being next to base weapon name inside parentheses
--	(e.g. Braton (MK1, Prime)).
--  @function		p._shortLinkList
--	@param			{table} Weapon Weapon table
--	@param			{boolean} tooltip If true, adds weapon tooltips, false otherwise; defaults to false
--	@returns		{string} Wikitext of resultant list
function p._shortLinkList(Weapons, tooltip)
	-- First grabbing all the pieces and stashing them in a table
	local baseNames = {}
	for key, weap in Table.skpairs(Weapons) do
		local isVar, varType, baseName = p._isVariant(weap.Name)
		if (baseNames[baseName] == nil) then baseNames[baseName] = {} end
		table.insert(baseNames[baseName], varType)
	end
	
	-- Then the fun part: Pulling the table together
	local result = {}
	for baseName, variants in Table.skpairs(baseNames) do
		-- So first, check if "Base" is in the list
		-- Because if it isn't, list all variants separately
		if (Table.contains(variants, "Base")) then
			table.sort(variants)
			-- First, get the basic version
			local thisRow = ""
			if (tooltip) then
				thisRow = "{{Weapon|"..baseName.."}}"
			else
				thisRow = "[["..baseName.."]]"
			end
			-- Then, if there are variants...
			if (Table.size(variants) > 1) then
				-- List them in parentheses one at a time
				thisRow = thisRow.." ("
				local count = 0
				for i, varName in pairs(variants) do
					if (varName ~= "Base") then
						if (count > 0) then thisRow = thisRow..", " end
						if (tooltip) then
							thisRow = thisRow.."{{Weapon|"..p._buildName(baseName, varName).."|"..varName.."}}"
						else
							thisRow = thisRow.."[["..p._buildName(baseName, varName).."|"..varName.."]]"
						end
						count = count + 1
					end
				end
				thisRow = thisRow..")"
			end
			table.insert(result, thisRow)
		else
			for i, varName in pairs(variants) do
				if (tooltip) then
					table.insert(result, "{{Weapon|"..p._buildName(baseName, varName).."}}")
				else
					table.insert(result, "[["..p._buildName(baseName, varName).."]]")
				end
			end
		end
	end
	return result
end

--- Returns a specific weapon table entry from <code>/data</code>.
--	@function		p._getWeapon
--	@param			{string} weaponName Weapon name
--	@returns		{table} Weapon table
function p._getWeapon(weaponName)
	local weapon = WeaponData["Weapons"][weaponName]
	if weapon ~= nil then
		return weapon
	end
	
	for key, Weapon in Table.skpairs(WeaponData["Weapons"]) do
		if (Weapon.Name == WeapName) then return Weapon end
	end
	error('p._getWeapon(weaponName): "'..weaponName..'" does not exist in M:Weapons/data')
end

--- Returns a specific weapon table entry from <code>/Conclave/data</code>.
--	@function		p._getConclaveWeapon
--	@param			{string} weaponName Weapon name
--	@returns		{table} Weapon table
function p._getConclaveWeapon(weaponName)
	local weapon = ConclaveData["Weapons"][weaponName]
	if (weapon ~= nil and weapon.Name == weaponName) then
		return weapon
	end
	
	for key, Weapon in Table.skpairs(ConclaveData["Weapons"]) do
		if (Weapon.Name == WeapName or key == WeapName) then
			return Weapon
		end
	end
	error('p._getConclaveWeapon(weaponName): "'..weaponName..'" does not exist in M:Weapons/Conclave/data')
end

--- Returns a specific attack table from a weapon table entry.
--	@function		getAttack
--	@param			{table, string} Weapon Weapon entry as seen in <code>/data</code> or the name of weapon
--	@param[opt]		{string} attackName Name of attack key; if omitted, will default to "Attack1"
--	@returns		{table} Attack table
local function getAttack(Weapon, attackName)
	if (Weapon == nil or attackName == nil) then return end
	if (type(Weapon) == "string") then
		Weapon = p._getWeapon(Weapon)
	end
	if (not attackName) then
		return Weapon.Attack1 or Weapon.Damage and Weapon
	end
	return Weapon[attackName:find 'Attack' and attackName or 'Attack'..attackName]
end

--- Loops through all possible attacks that a weapon may have.
--	@function		p._attackLoop
--	@param			{table} Weapon Weapon entry as seen in <code>/data</code>
--	@returns		{function} An iterator function that returns the key-value pair of next attack entry
--	@returns		{table} Original weapon entry passed into function
function p._attackLoop(Weapon)
	if (Weapon == nil) then
		return function() return nil end
	end
	local function nextAttack(t, k)
		if not k then return '1', t['Damage'] and t or t['Attack1'] end
		local v
		repeat
			k, v = next(t,k)
		until type(v) == 'table' and v['Damage']
		return k, v
	end
	return nextAttack, Weapon
end

--- Returns all melee weapons. If weapType is not nil, only grab for a specific type
--	For example, if weapType is "Nikana", only pull Nikanas.
--	@function		getMeleeWeapons
--	@param[opt]		{string} weapClass Name of melee class to filter by; if nil then gets all melee weapons
--	@param[opt]		{boolean} PvP If true, only gets melee weapons available in Conclave, false otherwise; defaults to false
--	@returns		{table} An array of melee weapon table entries as seen in <code>/data</code>
local function getMeleeWeapons(weapClass, PvP)
	local weaps = {}
	for i, weap in Table.skpairs(WeaponData["Weapons"]) do
		if ((weap.Ignore == nil or not weap.Ignore) and weap.Type ~= nil and weap.Type == "Melee") then
			local classMatch = (weapClass == nil or weap.Class == weapClass)
			local pvpMatch = (PvP == nil or (PvP and weap.Conclave ~= nil and weap.Conclave))
			if (classMatch and pvpMatch) then
				table.insert(weaps, weap)
			end
		end
	end
	
	return weaps
end

-- TODO: Can probably be removed and refactored into getMeleeWeapons()
--- Returns all melee weapons. If weapType is not nil, only grab for a specific type
--	For example, if weapType is "Nikana", only pull Nikanas.
--	@function		getConclaveMeleeWeapons
--	@param[opt]		{string} weapClass Name of melee class to filter by; if nil then gets all melee weapons
--	@param[opt]		{boolean} PvP If true, only gets melee weapons available in Conclave, false otherwise; defaults to false
--	@returns		{table} An array of melee weapon table entries as seen in <code>/Conclave/data</code>
local function getConclaveMeleeWeapons(weapClass, PvP)
	local weaps = {}
	local weapClasses = {}
	if (weapClass ~= nil) then
		weapClasses = String.split(weapClass, ",")
	end
	
	for i, weap in Table.skpairs(ConclaveData["Weapons"]) do
		if ((weap.Ignore == nil or not weap.Ignore) and weap.Type ~= nil and weap.Type == "Melee") then
			local classMatch = (weapClass == nil or Table.contains(weapClasses, weap.Class))
			local pvpMatch = (PvP == nil or (PvP and weap.Conclave ~= nil and weap.Conclave))
			if (classMatch and pvpMatch) then
				table.insert(weaps, weap)
			end
		end
	end
	
	return weaps
end

--- Gets stance mods for a particular melee class.
--- Returns a subset of <code>/data</code> based on a validation function.
--	@function		p._getWeapons
--	@param			{function} validateFunction Function that filters out a weapon by taking in a Weapon table argument
--	@returns		{table} Table of weapon table entries as seen in <code>/data</code>
function p._getWeapons(validateFunction)
	local weaponList = {}
	for weaponName, weaponEntry in Table.skpairs(WeaponData["Weapons"]) do
		if (not Table.contains(WeaponData["IgnoreInCount"], weaponName) and
				(weaponEntry.Ignore == nil or not weaponEntry.Ignore) and validateFunction(weaponEntry)) then
			table.insert(weaponList, weaponEntry)
		end
	end
	return weaponList
end

-- TODO: Remove b/c unused in this module?
--- Returns a subset of <code>/Conclave/data</code> based on a validation function.
--	@function		p._getConclaveWeapons
--	@param			{function} validateFunction Function that filters out a weapon by taking in a Weapon table argument
--	@returns		{table} Table of weapon table entries as seen in <code>/Conclave/data</code>
function p._getConclaveWeapons(validateFunction)
	local weaps = {}
	for i, weap in Table.skpairs(ConclaveData["Weapons"]) do
		if ((weap.Ignore == nil or not weap.Ignore) and validateFunction(weap)) then
			table.insert(weaps, weap)
		end
	end
	return weaps
end

-- TODO: Move to M:Stances?
--- Gets stance mods for a particular melee class.
--	@function		getWeaponStanceList
--	@param			{string} meleeClass Melee class
--	@param			{boolean} pvpOnly If true, only gets PvP-exclusive stance mods, otherwise gets all applicable stance mods; default false
--	@returns		{table} Table of stance table entries as seen in <code>M:Stance/data</code>
local function getStances(meleeClass, pvpOnly)
	local stanceTable = {}
	
	for stanceName, Stance in pairs(StanceData) do
		local typeMatch = (meleeClass == nil or meleeClass == Stance.WeaponType)
		local pvpMatch = (pvpOnly ~= nil and pvpOnly) or (Stance.ConclaveOnly == nil or not Stance.ConclaveOnly)
		if (typeMatch and pvpMatch) then
			stanceTable[stanceName] = Stance
		end
	end
	
	return stanceTable
end

--- Builds list of stances for a particular melee weapon as seen on [[Template:MeleeCategory]].
--	@function		getWeaponStanceList
--	@param			{table} Weapon Wepaon table
--	@returns		{string} Resultant wikitext of comparison list
local function getWeaponStanceList(Weapon)
	if (Weapon == nil or Weapon.Type ~= "Melee") then return nil end
	local stanceTable = {}
	if Weapon.Class == "Exalted Weapon" then
		stanceTable = getStances(Weapon.Name, Weapon.Conclave)
	else 
		stanceTable = getStances(Weapon.Class, Weapon.Conclave)
	end
	
	local result = ""
	
	for stanceName, Stance in pairs(stanceTable) do
		if (string.len(result) > 0) then
			result = result.."<br/>"
		end
		
		local polarity = ""
		local link = ""
		if Weapon.Class == "Exalted Weapon" then
			link = "[["..ModData["Mods"][stanceName].Link.."|"..stanceName.."]]"
		else
			polarity = " ("..Polarity._polarity(ModData["Mods"][stanceName].Polarity)..")"
			link = "[["..stanceName.."]]"
		end
		
		-- Adding tooltip
		result = result..Tooltip.getIcon(stanceName, "Mods")
		
		-- If this is a PvP Stance, add the disclaimer
		if (Stance.ConclaveOnly ~= nil and Stance.ConclaveOnly) then
			result = result.." (PvP Only)"
		end
	end
	
	return result
end

-- TODO: Pull stance data from M:Stance/data?
--- Builds list of stances for a particular melee weapon as seen on [[Template:MeleeCategory]].
--	@function		p.getWeaponStanceList
--	@param			{table} frame Frame object w/ first argument being string weaponName
--	@returns		{string} Resultant wikitext of comparison list
function p.getWeaponStanceList(frame)
	local weaponName = frame.args ~= nil and frame.args[1] or frame
	local Weapon = p._getWeapon(weaponName)
	return getWeaponStanceList(Weapon)
end

--- Gets the value of a certain statistic of a weapon.
--	@function		getValue
--	@param			{table} Weapon Weapon table
--	@param			{string} keyName Name of key
--	@param[opt]		{string} attackName Name of attack to search through
--	@returns		{string, number} Value of statistic
local function getValue(Weapon, keyName, attackName)
	-- Trying to use pcall to get more specific errors
	-- for _, map in pairs({ SHARED_KEY_MAP, GUN_KEY_MAP, MELEE_KEY_MAP, ATTACK_KEY_MAP }) do
	-- 	if (map[keyName] ~= nil) then
	-- 		local status, result = pcall(map[keyName], Weapon)
	-- 	end
	-- end
	-- error('getValue(Weapon, keyName, attackName): Cannot get keyName "'..keyName..'" in '..mw.dumpObject(Weapon))
	
	return SHARED_KEY_MAP[keyName] ~= nil and SHARED_KEY_MAP[keyName](Weapon) or
		GUN_KEY_MAP[keyName] ~= nil and GUN_KEY_MAP[keyName](Weapon) or 
		MELEE_KEY_MAP[keyName] ~= nil and MELEE_KEY_MAP[keyName](Weapon) or
		ATTACK_KEY_MAP[keyName] ~= nil and ATTACK_KEY_MAP[keyName](Weapon[attackName or Weapon['TooltipAttackDisplay'] or 'Attack1']) or
		error('getValue(Weapon, keyName, attackName): Cannot get keyName "'..keyName..'" in '..mw.dumpObject(Weapon))
end

-- TODO: Move this function to M:Stance?
--- Builds list of weapons that can equip a particlar stance mod as seen on [[Template:StanceWeapons]].
--	@function		p.getStanceWeaponList
--	@param			{table} frame Frame object w/ first argument being string stanceName
--	@returns		{string} Resultant wikitext of comparison list
function p.getStanceWeaponList(frame)
	local stanceName = frame.args ~= nil and frame.args[1] or frame
	local Stance = StanceData[stanceName]
	assert(Stance ~= nil, "p.getStanceWeaponList(frame): "..stanceName.." not found")
	
	local weaps = getMeleeWeapons(Stance.WeaponType, Stance.ConclaveOnly)
	local result = ""
	
	for i, weap in Table.skpairs(weaps) do
		if (string.len(result) > 0) then
			result = result.."\n"
		end
		
		if (Stance.ConclaveOnly) then
			result = result.."*[[Conclave:"..weap.Name.."|"..weap.Name.."]]"
		else
			result = result.."*[["..weap.Name.."]]"
		end
		
		if (weap.StancePolarity == ModData["Mods"][stanceName].Polarity) then
			result = result.." ✓"
		end
	end
	
	return result
end

--- Builds a weapon gallery.
--	@function		getWeaponGallery
--	@param			{table} Weapons Array of weapon table entries to be displayed
--	@returns		{string} Resultant wikitext of gallery
local function getWeaponGallery(Weapons)
	local result = { '{| style="margin:auto;text-align:center;"' }
	local nameRow = ""
	for i, Weapon in pairs(Weapons) do
		local theImage = Weapon.Image ~= nil and Weapon.Image or "Panel.png"
		if ((i - 1) % 5 == 0) then 
			table.insert(result, nameRow.."\n|-") 
			nameRow = "\n|-"
		end
		table.insert(result, '| style="width:165px" |[[File:'..theImage..'|150px|link='..Weapon.Name..']]')
		nameRow = nameRow..'\n| style="vertical-align: text-top;" |[['..Weapon.Name..']]'
	end
	table.insert(result, nameRow)
	table.insert(result, '|}')
	return table.concat(result, '\n')
end

--- Builds a melee weapon gallery as seen on [[Template:MeleeCategory]].
--	@function		p.getMeleeWeaponGallery
--	@param			{table} frame Frame object w/ first argumenting being string meleeClass
--	@returns		{string} Resultant wikitext of gallery
function p.getMeleeWeaponGallery(frame)
	local meleeClass = frame.args ~= nil and frame.args[1] or ""
	local WeapArray = getMeleeWeapons(meleeClass)
	local result = "=="..meleeClass.." Weapons==\n"
	result = result..getWeaponGallery(WeapArray)
	return result
end

--- Gets the total count of weapons as used on [[Mastery Rank]].
--  @function		p.getWeaponCount
--	@param			{table} frame Frame object w/ the first argument being the weaponType, 
--							second argument being a boolean to getFullList, and the third being a boolean to getAll weapons in <code>/data</code>
--	@returns		{number} Total count of weapons in a certain category/type
function p.getWeaponCount(frame)
	local weaponType = frame.args ~= nil and frame.args[1] or frame
	local getFullList = frame.args ~= nil and frame.args[2]
	local getAll = frame.args ~= nil and frame.args[3]
	local count = 0
	local fullList = ""
	for i, val in Table.skpairs(WeaponData["Weapons"]) do
		if (not Table.contains(WeaponData["IgnoreInCount"], i) and getAll ~= "true") or getAll == "true" then
			if (weaponType == nil or weaponType == "") then
				count = count + 1
				if (getFullList ~= nil) then fullList = fullList.."\n# "..val.Name end
			elseif (weaponType == "Warframe") then
				if ((val.Type == "Primary" or val.Type == "Secondary" or val.Type == "Melee") and val.Class ~= "Exalted Weapon") then
					count = count + 1
					if (getFullList ~= nil) then fullList = fullList.."\n# "..val.Name end
				end
			elseif (weaponType == "Archwing") then
				if ((val.Type == "Arch-Gun" or val.Type == "Arch-Melee") and val.Class ~= "Exalted Weapon") then
					count = count + 1
					if (getFullList ~= nil) then fullList = fullList.."\n# "..val.Name end
				end
			elseif (weaponType == "Rest") then
				if (val.Type ~= "Arch-Gun" and val.Type ~= "Arch-Melee" and val.Type ~= "Primary" and 
						val.Type ~= "Secondary" and val.Type ~= "Melee" and val.Class ~= "Exalted Weapon") then
					count = count + 1
					if (getFullList ~= nil) then fullList = fullList.."\n# "..val.Name end
				end
			elseif (val.Type == weaponType and val.Class ~= "Exalted Weapon") then
				count = count + 1
				if (getFullList ~= nil) then fullList = fullList.."\n# "..val.Name end
			end
		end
	end
	if (getFullList ~= nil) then return fullList end
	return count
end

--- Gets the weapon class of secondary weapons.
--  @function		getSecondaryCategory
--	@param			{table} weapon Weapon table
--	@returns		{string} Category name
local function getSecondaryCategory(weapon)
	local class = getValue(weapon, "Class")
	if (class == "Thrown") then
		return "Thrown"
	elseif (class == "Dual Shotguns" or class == "Shotgun Sidearm") then
		return "Shotgun"
	else
		local trigger = getValue(weapon, "Trigger")
		if (trigger == "Semi-Auto" or trigger == "Burst") then
			return "Semi-Auto"
		elseif (trigger == "Auto" or trigger == "Auto-Spool") then
			return "Auto"
		end
	end
	return "Other"
end

--- Gets the weapon class of primary weapons.
--  @function		getPrimaryCategory
--	@param			{table} weapon Weapon table
--	@returns		{string} Category name
local function getPrimaryCategory(weapon)
	local class = getValue(weapon, "Class")
	if (class == "Shotgun") then
		return "Shotgun"
	elseif (class == "Bow") then
		return "Bow"
	elseif (class == "Sniper Rifle") then
		return "Sniper"
	elseif (class == "Rifle") then
		local trigger = getValue(weapon, "Trigger")
		if (trigger == "Semi-Auto" or trigger == "Burst") then
			return "Semi-Auto"
		elseif (trigger == "Auto" or trigger == "Auto-Spool") then
			return "Auto"
		end
	end
	return "Other"
end

--- Builds wikitable of all weapons' innate polarities as seen on [[Polarity]].
--  @function		p.getPolarityTable
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getPolarityTable(frame)
	local tableResult = { [[
{| style="width: 100%; border-collapse: collapse;" cellpadding="2" border="1"
|+ '''Weapons with Innate Polarities (ignoring Stance and Exilus slots)'''
! colspan="2" |Primaries
! colspan="2" |Secondaries
! colspan="2" |Melees
! colspan="2" |Arch-Guns
! colspan="2" |Arch-Melees]] }
	
	local filterBy = function(weaponType)
		return function(weaponEntry) 
			return (weaponEntry['Type'] == weaponType) and string.len(getValue(weaponEntry, "Polarities")) > 0
		end
	end
	
	local meleeEntries = p._getWeapons(filterBy('Melee'))
	local pistolEntries = p._getWeapons(filterBy('Secondary'))
	local primaryEntries = p._getWeapons(filterBy('Primary'))
	local archGunEntries = p._getWeapons(filterBy('Arch-Gun'))
	local archMeleeEntries = p._getWeapons(filterBy('Arch-Melee'))
	
	local meleeCount = Table.size(meleeEntries)
	local pistolCount = Table.size(pistolEntries)
	local primaryCount = Table.size(primaryEntries)
	local archGunCount = Table.size(archGunEntries)
	local archMeleeCount = Table.size(archMeleeEntries)
	
	local maxLen = meleeCount
	if (pistolCount > maxLen) then maxLen = pistolCount end
	if (primaryCount > maxLen) then maxLen = primaryCount end
	if (archGunCount > maxLen) then maxLen = archGunCount end
	if (archMeleeCount > maxLen) then maxLen = archMeleeCount end
	
	local traverseOrder = { primaryCount, pistolCount, meleeCount, archGunCount, archMeleeCount }
	-- Note: Cannot map tables to their size since it would exceed allocated script time
	-- to access these
	local countEntries = {
		[primaryCount] = primaryEntries,
		[pistolCount] = pistolEntries,
		[meleeCount] = meleeEntries,
		[archGunCount] = archGunEntries,
		[archMeleeCount] = archMeleeEntries 
	}
	
	for i = 1, maxLen, 1 do
		table.insert(tableResult, '|-')
		-- Adding each row in table
		for _, entriesCount in ipairs(traverseOrder) do
			if (i <= entriesCount) then
				table.insert(tableResult, '| [['..countEntries[entriesCount][i]['Name']..']] ||'..Polarity._pols(countEntries[entriesCount][i]['Polarities']))
			else
				table.insert(tableResult, '| ||')
			end
		end
	end
	table.insert(tableResult, '|}')
	return table.concat(tableResult, '\n')
end

--- Builds comparison string between two values.
--	@function		p.buildCompareString
--	@param			{string, number} firstVal Value used for comparison
--	@param			{string, number} secondVal Value used to compare the first value against
--	@param			{string} valName Name of statistic that values represent (e.g. "Critical Damage")
--	@param[opt]		{number} digits The decimal to round the values by (e.g. if you want to round to two decimal places put in 0.01)
--	@param[opt]		{string} unit The values' unit (e.g. "m" or "seconds")
--	@param[opt]		{table} compareAdjs Two element table that contains the greater than and less than comparative adjectives (e.g. { "Higher", "Lower" } ) 
--	@param[opt]		{string} start What to start the comparison string by for if you want to increase the bullet level (e.g. "\n***")
--	@returns		{string} Resultant wikitext of comparison string
local function buildCompareString(firstVal, secondVal, valName, digits, unit, compareAdjs, start)
	if (firstVal == nil or secondVal == nil) then
		return ""
	end
	local firstValStr = firstVal
	local secondValStr = secondVal
	if (digits ~= nil) then
		firstValStr = Math.round(firstVal, digits)
		secondValStr = Math.round(secondVal, digits)
	end
	if (unit ~= nil) then
		firstValStr = firstValStr..unit
		secondValStr = secondValStr..unit
	end
	local bigWord = compareAdjs ~= nil and compareAdjs[1] or "Higher"
	local smallWord = compareAdjs ~= nil and compareAdjs[2] or "Lower"
	local start = start ~= nil and start or "\n**"
	
	if (firstVal > secondVal) then
		return start.." "..bigWord.." "..valName.." ("..firstValStr.." vs. "..secondValStr..")"
	elseif (secondVal > firstVal) then
		return start.." "..smallWord.." "..valName.." ("..firstValStr.." vs. "..secondValStr..")"
	else
		return ""
	end
end

--- Builds damage comparison string between two attacks.
--	@function		p.buildComparison
--	@param			{table} Attack1 Attack used for comparison
--	@param			{table} Attack2 Attack used to compare the first attack against
--	@returns		{string} Resultant wikitext of comparison string
local function buildDamageTypeComparisonString(Attack1, Attack2)
	local result = ""
	--ipairs iterates in the order given in Elements, so IPS is always first
	for i, element in ipairs(Elements) do
		local damage1 = Attack1.Damage[element]
		local damage2 = Attack2.Damage[element]
		if (damage1 ~= nil or damage2 ~= nil) then
			if (damage1 == nil) then damage1 = 0 end
			if (damage2 == nil) then damage2 = 0 end
			result = result..buildCompareString(damage1, damage2, Tooltip.getIcon(element, 'DamageTypes').." damage", 0.01, nil, {"Higher", "Lower"}, "\n***")
		end
	end
	return result
end

-- TODO: The above TODO can be done in conjunction with changing all the "Attack1" and "Attack2" keys
-- to a singular "Attack" key with indexed table elements
--- Builds comparison list between two gun weapons.
--	@function		p.buildComparison
--	@param			{table} Weapon1 Weapon used for comparison
--	@param			{table} Weapon2 Weapon used to compare the first weapon against
--	@param			{boolean} conclave If true, makes comparison list based on PvP stats, otherwise uses PvE stats; default false
--	@returns		{string} Resultant wikitext of comparison list
local function buildGunComparisonString(Weapon1, Weapon2, Conclave)
	local result = {}
	-- Adding this assignment to support method chaining w/ colon syntax
	result.insert = function(self, elem) table.insert(self, elem) return self end
	
	local Att1 = getAttack(Weapon1, Weapon1['TooltipAttackDisplay'] or 'Attack1')
	local Att2 = getAttack(Weapon2, Weapon2['TooltipAttackDisplay'] or 'Attack1')
	if (Conclave) then
		result:insert("* [["..Weapon1.Name.."]], compared to [[Conclave:"..Weapon2.Name.."|"..Weapon2.Name.."]]:")
	else
		result:insert("* [["..Weapon1.Name.."]] (attack: "..(Att1.AttackName or "Normal").."), compared to [["..Weapon2.Name.."]] (attack: "..(Att2.AttackName or "Normal").."):")
	end
	
	local dmgString = ""
	dmgString = dmgString..buildCompareString(getValue(Weapon1, "BaseDamage"), getValue(Weapon2, "BaseDamage"), "base damage", 0.01)
	dmgString = dmgString..buildDamageTypeComparisonString(Att1, Att2)
	if (string.len(dmgString) > 0 and getValue(Weapon1, "BaseDamage") == getValue(Weapon2, "BaseDamage")) then
		dmgString = "\n**Equal base damage, but different composition:"..dmgString
	end
	dmgString = dmgString..buildCompareString(getValue(Weapon1, "TotalDamage"), getValue(Weapon2, "TotalDamage"), "total damage", 0.01)
	-- TODO: Find a better way of finding out if an attack is a charge attack; maybe add Trigger = "Charge" to all of those attacks?
	-- result = result..buildCompareString(getValue(Weapon1, "ChargeTime", "Attack3"), getValue(Weapon2, "ChargeTime", "Attack3"), "charge time", 0.01, " s", {"Slower", "Faster"})
	result:insert(dmgString)
	
	if (not Conclave) then
		result:insert(
			buildCompareString((Att1.CritChance * 100), (Att2.CritChance * 100), "base [[critical chance]]", 0.01, "%")
		):insert(
			buildCompareString(Att1.CritMultiplier, Att2.CritMultiplier, "base [[critical multiplier]]", 0.01, "x")
		):insert(
			buildCompareString((Att1.StatusChance * 100), (Att2.StatusChance * 100), "base [[status chance]]", 0.01, "%")
		):insert(
			buildCompareString(getValue(Weapon1, "AvgShotDmg"), getValue(Weapon2, "AvgShotDmg"), "[[Damage#Final_Calculations|average damage per shot]] (max Progenitor bonus if applicable)", 0.01)
		):insert(
			buildCompareString(getValue(Weapon1, "BurstDps"), getValue(Weapon2, "BurstDps"), "[[Damage#Final_Calculations|burst DPS]] (max Progenitor bonus if applicable)", 0.01)
		):insert(buildCompareString(getValue(Weapon1, "SustainedDps"), getValue(Weapon2, "SustainedDps"), "[[Damage#Final_Calculations|sustained DPS]] (max Progenitor bonus if applicable)", 0.01)
		):insert(
			buildCompareString(getValue(Weapon1, "FalloffStart"), getValue(Weapon2, "FalloffStart"), "starting [[Damage Falloff|damage falloff]] distance", 0.1, "m", {"Farther", "Closer"})
		):insert(
			buildCompareString(getValue(Weapon1, "FalloffEnd"), getValue(Weapon2, "FalloffEnd"), "ending damage falloff distance", 0.1, "m", {"Farther", "Closer"})
		):insert(
			buildCompareString(getValue(Weapon1, "FalloffReduction") * 100, getValue(Weapon2, "FalloffReduction") * 100, "max damage reduction at ending falloff distance", 0.01, "%", {"Greater", "Lesser"})
		)
	end
	
	result:insert(
		buildCompareString(Att1.FireRate, Att2.FireRate, "[[fire rate]]", 0.01, " round(s)/sec")
	):insert(
		buildCompareString(Att1.Multishot or 1, Att2.Multishot or 1, "[[multishot]]", 1, " projectile(s)")
	):insert(
		buildCompareString(Weapon1.Magazine, Weapon2.Magazine, "magazine", 1, " round(s)", {"Larger", "Smaller"})
	):insert(
		buildCompareString(Weapon1.MaxAmmo, Weapon2.MaxAmmo, "max ammo capacity", 1, " round(s)", {"Larger", "Smaller"})
	):insert(
		buildCompareString(Weapon1.Reload, Weapon2.Reload, "[[Reload|reload time]]", 0.01, " s", {"Slower", "Faster"})
	):insert(
		buildCompareString(Weapon1.Spool, Weapon2.Spool, "spool-up", 1, " round(s)", {"Slower", "Faster"})
	):insert(
		buildCompareString(getValue(Weapon1, "Accuracy"), getValue(Weapon2, "Accuracy"), "[[Accuracy|accurate]]", 0.01, nil, {"More", "Less"})
	):insert(
		buildCompareString(getValue(Weapon1, "Polarities"), getValue(Weapon2, "Polarities"), "[[Polarity|polarities]]", nil, nil, {"Different", "Different"})
	):insert(
		buildCompareString(Weapon1.Mastery, Weapon2.Mastery, "[[Mastery Rank]] required", 1)
	):insert(
		buildCompareString(Weapon1.Disposition, Weapon2.Disposition, "[[disposition]]", 0.01)
	)
	
	--Handling Syndicate radial effects
	if (Weapon1.SyndicateEffect ~= nil and Weapon2.SyndicateEffect == nil) then
		result:insert("\n** Innate [["..Weapon1.SyndicateEffect.."]] effect")
	elseif (Weapon2.SyndicateEffect ~= nil and Weapon1.SyndicateEffect == nil) then
		result:insert("\n** Lack of an innate [["..Weapon2.SyndicateEffect.."]] effect")
	elseif (Weapon1.SyndicateEffect ~= nil and Weapon2.SyndicateEffect ~= nil and 
			Weapon1.SyndicateEffect ~= Weapon2.SyndicateEffect2) then
		result:insert("\n** Different innate [[Syndicate Radial Effects|Syndicate Effect]]: [["..
			Weapon1.SyndicateEffect.."]] vs. [["..Weapon2.SyndicateEffect.."]]")
	end
	return table.concat(result)
end

--- Builds comparison list between two melee or arch-melee weapons.
--	@function		p.buildComparison
--	@param			{table} Weapon1 Weapon used for comparison
--	@param			{table} Weapon2 Weapon used to compare the first weapon against
--	@param			{boolean} conclave If true, makes comparison list based on PvP stats, otherwise uses PvE stats; default false
--	@returns		{string} Resultant wikitext of comparison list
local function buildMeleeComparisonString(Weapon1, Weapon2, Conclave)
	local result = {}
	-- Adding this assignment to support method chaining w/ colon syntax
	result.insert = function(self, elem) table.insert(self, elem) return self end
	
	local Att1 = getAttack(Weapon1, Weapon1['TooltipAttackDisplay'] or 'Attack1')
	local Att2 = getAttack(Weapon2, Weapon2['TooltipAttackDisplay'] or 'Attack1')

	if (Conclave) then
		result:insert("* "..Weapon1.Name..", compared to [[Conclave:"..Weapon2.Name.."|"..Weapon2.Name.."]]:")
	else
		result:insert("* "..Weapon1.Name.." (attack: "..(Att1.AttackName or "Normal").."), compared to [["..Weapon2.Name.."]] (attack: "..(Att2.AttackName or "Normal").."):")
	end
	
	local dmgString = ""
	dmgString = dmgString..buildCompareString(getValue(Weapon1, "BaseDamage"), getValue(Weapon2, "BaseDamage"), "base damage", 0.01)
	dmgString = dmgString..buildDamageTypeComparisonString(Att1, Att2)
	if (string.len(dmgString) > 0 and getValue(Weapon1, "BaseDamage") == getValue(Weapon2, "BaseDamage")) then
		dmgString = "\n**Equal base damage, but different composition:"..dmgString	
	end
	result:insert(dmgString)
	
	if (not Conclave) then
		result:insert(
			buildCompareString((Att1.CritChance * 100), (Att2.CritChance * 100), "[[critical chance]]", 0.01, "%")
		):insert(
			buildCompareString(Att1.CritMultiplier, Att2.CritMultiplier, "[[critical multiplier]]", 0.01, "x")
		):insert(
			buildCompareString((Att1.StatusChance * 100), (Att2.StatusChance * 100), "[[status chance]]", 0.01, "%")
		):insert(
			buildCompareString(getValue(Weapon1, "MeleeRange"), getValue(Weapon2, "MeleeRange"), "Range", 0.01, " m")
		):insert(
			buildCompareString(Weapon1.Disposition, Weapon2.Disposition, "[[disposition]]", 0.01)
		)
	end

	result:insert(
		buildCompareString(Att1.FireRate, Att2.FireRate, "[[attack speed]]", 0.01)
	):insert(
		buildCompareString(getValue(Weapon1, "Polarities"), getValue(Weapon2, "Polarities"), "[[Polarity|polarities]]", nil, nil, {"Different", "Different"})
	):insert(
		buildCompareString(Weapon1.Mastery, Weapon2.Mastery, "[[Mastery Rank]] required", 1)
	)
	
	-- Quick fix to passing in an empty string for polarity when comparing with an arch-melee weapon
	if (Weapon1.Type ~= "Arch-Melee" and Weapon2.Type ~= "Arch-Melee") then
		result:insert(
			buildCompareString(getValue(Weapon1, "ComboDur"), getValue(Weapon2, "ComboDur"), "[[Melee Combo|Combo Duration]]", 1, " s")
		):insert(
			buildCompareString(getValue(Weapon1, "BlockAngle"), getValue(Weapon2, "BlockAngle"), "Block Angle", 1, "&#176;")
		):insert(
			buildCompareString(Polarity._polarity(Weapon1.StancePolarity), Polarity._polarity(Weapon2.StancePolarity), "[[Stance]] Polarity", nil, nil, {"Different", "Different"})
		)
	end
	return table.concat(result)
end

--- Builds comparison list between two weapons in PvE.
--	@function		p.buildComparison
--	@param			{table} frame Frame object
--	@returns		{string} Resultant wikitext of comparison list
function p.buildComparison(frame)
	local weaponName1 = frame.args[1]
	local weaponName2 = frame.args[2]

	assert(weaponName1 ~= '' and weaponName2 ~= '', 'p.buildComparison(frame): Must compare two weapons')
	
	local Weapon1 = p._getWeapon(weaponName1)
	local Weapon2 = p._getWeapon(weaponName2)
	
	local comparisonString = ''
	if (Weapon1.Type == 'Melee' or Weapon1.Type == 'Arch-Melee') then
		comparisonString = buildMeleeComparisonString(Weapon1, Weapon2)
	else
		comparisonString = buildGunComparisonString(Weapon1, Weapon2)
	end
	return comparisonString..'[[Category:Automatic Comparison]]'
end

--- Builds comparison list between two weapons in PvP ([[Conclave]]).
--	@function		p.buildComparison
--	@param			{table} frame Frame object
--	@returns		{string} Resultant wikitext of comparison list
function p.buildConclaveComparison(frame)
	local weaponName1 = frame.args[1]
	local weaponName2 = frame.args[2]
	
	assert(weaponName1 ~= '' and weaponName2 ~= '', 'p.buildConclaveComparison(frame): Must compare two weapons')
	
	local Weapon1 = p._getConclaveWeapon(weaponName1)
	local Weapon2 = p._getConclaveWeapon(weaponName2)
	
	local comparisonString = ''
	if (Weapon1.Type == 'Melee' or Weapon1.Type == 'Arch-Melee') then
		comparisonString = buildMeleeComparisonString(Weapon1, Weapon2, true)
	else
		comparisonString = buildGunComparisonString(Weapon1, Weapon2, true)
	end
	return comparisonString..'[[Category:Automatic Comparison]]'
end

--- Adds weapon categories.
--	@function		p.buildAutoboxCategories
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of category links
function p.buildAutoboxCategories(frame)
	local WeapName = frame.args ~= nil and frame.args[1] or frame
	local Weapon = p._getWeapon(WeapName)
	local result = { "[[Category:Automatic Weapon Box]][[Category:Weapons]]" }
	if (Weapon == nil or (Weapon.IgnoreCategories ~= nil and Weapon.IgnoreCategories)) then
		return ""
	end
	table.insert(result, CATEGORY_MAP[Weapon['Class']]..CATEGORY_MAP[Weapon['Trigger']]..CATEGORY_MAP[Weapon['Type']])
	
	-- Adding appropriate categories to page based on weapon's categorical traits
	for _, trait in pairs(Weapon['Traits'] or {}) do
		table.insert(result, CATEGORY_MAP[trait])
	end
	
	local bestPercent, bestElement = getDamageBias(Weapon[Weapon['TooltipAttackDisplay'] or 'Attack1'])
	if (bestElement == "Impact" or bestElement == "Puncture" or bestElement == "Slash") then
		if (bestPercent > .38) then
			table.insert(result, "[[Category:"..bestElement.." Damage Weapons]]")
		else
			table.insert(result, "[[Category:Balanced Physical Damage Weapons]]")
		end
	end
	
	for key, value in Table.skpairs(attack.Damage) do
		if (key ~= "Impact" and key ~= "Puncture" and key ~= "Slash") then
			table.insert(result, "[[Category:"..key.." Damage Weapons]]")
		end
	end
	
	return table.concat(result)
end

---	Builds a table that lists out all weapons with a certain damage type
--	and the percentage that it makes up of their base damage of the attack specified
--	in their tooltip on the wiki.
--	@function		p.buildDamageTypeTable
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.buildDamageTypeTable(frame)
	local damageType = frame.args ~= nil and frame.args[1] or frame
	local Weapons = {}
	local WeapArray = p._getWeapons(function(weaponEntry)
		-- Want to ignore Kitgun entries which have 0 as placeholder damage values
		if (weaponEntry['Type'] == 'Kitgun' or weaponEntry['Type'] == 'Primary Kitgun') then
			return false
		end
		local attackEntry = getAttack(weaponEntry, weaponEntry['TooltipAttackDisplay'] or 'Attack1')
		assert(attackEntry ~= nil, 'p.buildDamageTypeTable(frame): "'..weaponEntry.Name..'" has no attack entry for "Attack1" or attack in "TooltipAttackDisplay"')
		local dmg, element = getDamageBias(attackEntry)
		return element == damageType
	end)
	
	local weapLink = function(weap)
		return weap.Link ~= nil and '[['..weap.Link..'|'..weap.Name..']]' or '[['..weap.Name..']]'
	end
	
	local procString = makeDTooltip(damageType, true)
	local procShort = makeDTooltip(damageType)
	local result = ''
	local tHeader = string.format([[
{| class = "listtable sortable" style="margin:auto;"
|+ '''Weapons with %s damage'''
|-
! Name !! Type !! Class !! %s !! %s%%
]], damageType, procString, procShort)

	local tRows = {}
	for i, Weapon in pairs(WeapArray) do
		local thisRow = [[
|-
| {{Weapon|%s}} || %s || %s || %s || data-sort-value="%s" | %s]]
		local weaponLink = weapLink(Weapon)
		assert(Weapon.Type ~= nil, 'p.buildDamageTypeTable(frame): "'..weaponLink..'" has a nil value for Type"')
		local attack = Weapon['TooltipAttackDisplay'] or 'Attack1'
		local damageTypeVal = getValue(Weapon, damageType, attack)
		local damageBias = getValue(Weapon, 'DamageBias', attack)
		thisRow = string.format(thisRow, 
			Weapon.Name,
			Weapon.Type,
			getValue(Weapon, 'Class'),
			damageTypeVal,
			string.match(damageBias, '(%d*%.?%d+)%%'),
			damageBias
		)
		table.insert(tRows, thisRow)
	end
	result = tHeader..table.concat(tRows, '\n')..'\n|}'
	return frame:preprocess(result)
end

--- Builds a list of weapons' mastery requirements as seen on [[Template:EquipmentUnlock]].
--  @function		p.getMasteryShortList
--	@param			{table} frame Frame object w/ first argument being a string weaponType
--	@returns		{string} Wikitext of resultant list
function p.getMasteryShortList(frame)
	local weaponType = frame.args[1]
	local masteryRank = tonumber(frame.args[2])
	local checkTypeAndMastery = function(x) return x.Type == weaponType and x.Mastery == masteryRank end
	local weapArray = p._getWeapons(checkTypeAndMastery)
	
	local result = ''
	local name = ''
	local shortList = p._shortLinkList(weapArray, true)
	for i, pair in Table.skpairs(shortList) do
		if (string.len(result) > 0) then result = result..' • ' end
		result = result..pair
	end
	return frame:preprocess(result)
end

--- Builds a disposition wikitable as seen on [[Riven Mods/Weapon Dispos]].
--  @function		p.getRivenDispositionTable
--	@param			{table} frame Frame object w/ first argument being a string weaponType
--	@returns		{string} Wikitext of resultant wikitable
function p.getRivenDispositionTable(frame)
	local weaponType = frame.args[1]
	local ranges = { 1550, 1300, 1100, 899, 699, 499 }
	local result = {'{| class="article-table" border="0" cellpadding="1" cellspacing="1" style="width: 100%" \n|-'}
	
	for i = 1, 5 do
		table.insert(result, '\n! scope="col" style="text-align:center;"|'..Icon._Dis(ranges[i]/1000))
	end
	table.insert(result, '\n|-')
	
	for dispoRating = 5, 1, -1 do
		table.insert(result, '\n| style="vertical-align:top; font-size:small" |')
		local checkRightDispo = function(weaponEntry)
			if (weaponEntry['Type'] ~= nil and weaponEntry['Disposition'] ~= nil) then
				return (weaponType == 'All' or weaponEntry['Type'] == weaponType) and
					math.min(math.floor(5 * (weaponEntry['Disposition'] - (weaponEntry['Disposition'] < 1 and 0.3 or 0.309))), 5) == dispoRating
			end
			return false
		end
		-- Filtering out weapons that are between certain disposition ranges
		local weapArray = p._getWeapons(checkRightDispo)
		
		-- Building a list of weapons with a particular disposition rating (e.g. 5 dots)
		local weaponDispoList = {}
		
		-- Want to iterate in descending order so highest disposition weapon is near the top of table
		local descendingOrder = function(a, b)
			-- a and b are number indexes
			return weapArray[a]['Disposition'] > weapArray[b]['Disposition']
		end
		for _, weaponEntry in Table.skpairs(weapArray, descendingOrder) do
			table.insert(weaponDispoList, '\n* [['..weaponEntry['Name']..']] ('..weaponEntry['Disposition']..')')
		end
		
		table.insert(result, table.concat(weaponDispoList))
	end
	table.insert(result, '\n|}')
	
	return table.concat(result)
end

--- Builds a list of PvP weapons.
--  @function		p.getConclaveList
--	@param			{table} frame Frame object w/ first argument being a string weaponType
--	@returns		{string} Wikitext of resultant list
function p.getConclaveList(frame)
	local weaponType = frame.args[1]
	local weapArray = p._getWeapons(function(x) return x.Type == weaponType and x.Conclave end)
	
	local result = ''
	local shortList = p._shortLinkList(weapArray, false)
	for i, pair in Table.skpairs(shortList) do
		result = result..'\n* '..pair
	end
	return result
end

--- Builds a row forcomparison 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, Weapon)
	local styleString = ''--'border: 1px solid lightgray;'
	local result =  {}
	local valueName = nil
	local attackName = nil
	
	for i, headerLine in ipairs(tableHeaders) do
		valueName = headerLine[1]
		attackName = headerLine[3] ~= nil and headerLine[3] or nil
		
		local value = getValue(Weapon, valueName, attackName)
		if (type(value) == 'number') then
			value = Math.round(value, 0.01)
		end
		table.insert(result, 'style="'..styleString..'"|'..(value or 'N/A'))
	end
	return '|-\n|'..table.concat(result, '||')
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} Weapons Array of weapon table entries as pulled from <code>/data</code>
--	@returns		{string} Wikitext of resultant wikitable
local function buildCompTable(tableHeaders, Weapons)
	local styleString = 'border: 1px solid black;border-collapse: collapse;'
	local dataSortType
	local result = {}
	table.insert(result, '{| cellpadding="1" cellspacing="0" class="listtable sortable" style="font-size:11px;"')
	for _, headerLine in ipairs(tableHeaders) do
		dataSortType = ''
		if (headerLine[1] == 'DamageBias' or headerLine[1] == 'CritMultiplier' or 
				headerLine[1] == 'HeadshotMultiplier') then
			dataSortType = 'data-sort-type="number"'
		end
		table.insert(result, '! style="'..styleString..'" '..dataSortType..'|'..headerLine[2])
	end

	for _, Weap in pairs(Weapons) do
		local rowStr = buildCompRow(tableHeaders, Weap)
		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 Catt = frame.args ~= nil and frame.args[1]
	local Type = frame.args ~= nil and frame.args[2] or nil
	if (Type == "All") then Type = nil end
	local WeapArray = {}
	if (Catt == "Primary") then WeapArray = p._getWeapons(function(x) 
								if (getValue(x, "Type") == "Primary") then
									if (Type ~= nil) then return getPrimaryCategory(x) == Type else return true end
								end
									return false
								end)
	elseif (Catt == "Secondary") then WeapArray = p._getWeapons(function(x) 
								if (getValue(x, "Type") == "Secondary") then
									if (Type ~= nil) then return getSecondaryCategory(x) == Type else return true end
								end
									return false
								end)
	elseif (Catt == "Arch-Gun") then WeapArray = p._getWeapons(function(x) 
									return getValue(x, "Type") == "Arch-Gun"
								end)
	elseif (Catt == "Arch-Gun (Atmosphere)") then WeapArray = p._getWeapons(function(x) 
								return getValue(x, "Type") == "Arch-Gun (Atmosphere)"
							end)
	elseif (Catt == "Robotic") then WeapArray = p._getWeapons(function(x) 
									return getValue(x, "Type") == "Robotic"
								end)
	elseif (Catt == "Amp") then WeapArray = p._getWeapons(function(x) 
								return getValue(x, "Type") == "Amp"
							end)
	else
		error('p.getCompTableGuns(frame): Wrong gun weapon class'..
			'(use "Primary", "Secondary", "Arch-Gun", "Arch-Gun (Atmosphere)", "Robotic", or "Amp")'..
			'[[Category:Invalid Comp Table]]')
	end

	local tableHeaders = { {"NameLink", "Name"} }
	-- Adding this assignment to support method chaining w/ colon syntax
	tableHeaders.insert = function(self, elem) table.insert(self, elem) return self end
	
	tableHeaders:insert({ "Trigger", "[[Fire Rate|Trigger]]" })
	tableHeaders:insert({ "AttackName", "Attack" })
	tableHeaders:insert({ "DamageBias", "Main<br/>Element" })
	tableHeaders:insert({ "BaseDamage", "Base<br/>[[Damage|Dmg]]" })
	tableHeaders:insert({ "CritChance", "[[Critical Chance|Crit]]" })
	tableHeaders:insert({ "CritMultiplier", "[[Critical multiplier|Crit<br/>Dmg]]" })
	tableHeaders:insert({ "AvgShotDmg", "Avg<br/>Shot" })
	tableHeaders:insert({ "BurstDps", "Burst<br/>DPS" })
	tableHeaders:insert({ "SustainedDps", "Sust<br/>DPS" })
	tableHeaders:insert({ "StatusChance", "[[Status Chance|Status]]" })
	tableHeaders:insert({ "AvgProcPerSec", "[[Status Chance|Avg. Procs]]/<br/>s" })
	tableHeaders:insert({ "CompTableFireRate", "[[Fire Rate|Fire<br/>Rate]]" })
	tableHeaders:insert({ "Disposition", "[[Riven Mods#Disposition|Dispo]]" })
	tableHeaders:insert({ "Mastery", "[[Mastery Rank|MR]]" })
	tableHeaders:insert({ "Magazine", "[[Ammo#Magazine Capacity|Mag<br/>Size]]" })
	tableHeaders:insert({ "MaxAmmo", "[[Ammo|Ammo<br/>Cap]]" })
	tableHeaders:insert({ "Reload", "[[Reload Speed|Reload]]" })
	tableHeaders:insert({ "ShotType", "Shot<br/>Type" })
	tableHeaders:insert({ "PunchThrough", "[[Punch Through|PT]]" })
	tableHeaders:insert({ "Accuracy", "[[Accuracy]]" })
	tableHeaders:insert({ "IntroducedDate", "Intro" })
	
	return buildCompTable(tableHeaders, WeapArray)
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 Catt = frame.args ~= nil and frame.args[1]
	local Type = frame.args ~= nil and frame.args[2] or nil
	if (Type == "All") then Type = nil end
	local WeapArray = {}
	if (Catt == "Primary") then WeapArray = p._getConclaveWeapons(function(x) 
								if (getValue(x, "Type") == "Primary") then
									if (Type ~= nil) then return getPrimaryCategory(x) == Type else return true end
								end
									return false
								end)
	elseif (Catt == "Secondary") then WeapArray = p._getConclaveWeapons(function(x) 
								if (getValue(x, "Type") == "Secondary") then
									if (Type ~= nil) then return getSecondaryCategory(x) == Type else return true end
								end
									return false
								end)
	else
		error('p.getCompTableConclaveGuns(frame): Wrong gun weapon class for Conclave (use "Primary" or "Secondary")[[Category:Invalid Comp Table]]')
	end
	
	local tableHeaders = { {"Name", "Name"} }
	-- Adding this assignment to support method chaining w/ colon syntax
	tableHeaders.insert = function(self, elem) table.insert(self, elem) return self end
	
	tableHeaders:insert({ "Trigger", "[[Fire Rate|Trigger Type]]" })
	tableHeaders:insert({ "DamageBias", "Main<br/>Element" })
	tableHeaders:insert({ "BaseDamage", "[[Damage]]" })
	tableHeaders:insert({ "HeadshotMultiplier", "HS Multiplier" })
	tableHeaders:insert({ "ShotType", "Shot<br/>Type" })
	tableHeaders:insert({ "CompTableFireRate", "[[Fire Rate]]" })
	tableHeaders:insert({ "Magazine", "[[Ammo#Magazine Capacity|Magazine Size]]" })
	tableHeaders:insert({ "Reload", "[[Reload Speed|Reload Time]]" })
	tableHeaders:insert({ "Mastery", "[[Mastery Rank]]" })
	tableHeaders:insert({ "IntroducedDate", "Introduced" })
	
	return buildCompTable(tableHeaders,WeapArray)
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 Type = frame.args ~= nil and frame.args[2] or nil
	if (Type == nil) then Type = frame.args ~= nil and frame.args[1] or nil end 
	if (Type == "All") then Type = nil end
	local WeapArray = {}
	WeapArray = getMeleeWeapons(Type)
	
	local tableHeaders = { {"NameLink", "Name"} }
	-- Adding this assignment to support method chaining w/ colon syntax
	tableHeaders.insert = function(self, elem) table.insert(self, elem) return self end
	
	tableHeaders:insert({ "Class", "Type" })
	tableHeaders:insert({ "DamageBias", "Main<br/>Element" })
	tableHeaders:insert({ "BaseDamage", "[[Damage|Normal]]" })
	tableHeaders:insert({ "HeavyAttack", "[[Melee#Heavy Attack|Heavy]]" })
	tableHeaders:insert({ "SlamAttack", "[[Melee#Slam Attack|Slam]]" })
	tableHeaders:insert({ "SlideAttack", "[[Melee#Slide Attack|Slide]]" })
	tableHeaders:insert({ "MeleeRange", "[[Melee#Range|Range]]" })
	tableHeaders:insert({ "SlamRadius", "[[Melee#Slam Attack|Slam Radius]]" })
	tableHeaders:insert({ "FireRate", "[[Attack Speed|Speed]]" })
	tableHeaders:insert({ "CritChance", "[[Critical Chance|Crit]]" })
	tableHeaders:insert({ "CritMultiplier", "[[Critical multiplier|Crit Dmg]]" })
	tableHeaders:insert({ "StatusChance", "[[Status Chance|Status]]" })
	tableHeaders:insert({ "Disposition", "[[Riven Mods#Disposition|Dispo]]" })
	tableHeaders:insert({ "FollowThrough", "[[Follow Through|Follow<br />Through]]" })
	tableHeaders:insert({ "BlockAngle", "[[Blocking|Block<br />Angle]]" })
	tableHeaders:insert({ "Mastery", "[[Mastery Rank|MR]]" })
	tableHeaders:insert({ "StancePolarity", "[[Stance]]" })
	tableHeaders:insert({ "IntroducedDate", "Intro" })
	
	return buildCompTable(tableHeaders, WeapArray)
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 Type = frame.args ~= nil and frame.args[1] or nil
	if (Type == "All") then Type = nil end
	local WeapArray = {}
	WeapArray = getConclaveMeleeWeapons(Type)
	
	local tableHeaders = { {"Name", "Name"} }
	-- Adding this assignment to support method chaining w/ colon syntax
	tableHeaders.insert = function(self, elem) table.insert(self, elem) return self end
	
	tableHeaders:insert({ "Class", "Type" })
	tableHeaders:insert({ "BaseDamage", "[[Damage|Normal]]" })
	tableHeaders:insert({ "SlideAttack", "[[Melee#Slide Attack|Slide]]" })
	tableHeaders:insert({ "FireRate", "[[Attack Speed]]" })
	tableHeaders:insert({ "Mastery", "[[Mastery_Rank|Mastery Rank]]" })
	tableHeaders:insert({ "StancePolarity", "[[Stance]]" })
	tableHeaders:insert({ "IntroducedDate", "Introduced" })
	
	return buildCompTable(tableHeaders, WeapArray)
end

--- Builds comparison table of arch-melee 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 WeapArray = {}
	WeapArray = p._getWeapons(function(x) 
									return getValue(x, "Type") == "Arch-Melee"
								end)
								
	local tableHeaders = { {"NameLink", "Name"} }
	-- Adding this assignment to support method chaining w/ colon syntax
	tableHeaders.insert = function(self, elem) table.insert(self, elem) return self end
	
	tableHeaders:insert({ "DamageBias", "Main<br/>Element" })
	tableHeaders:insert({ "BaseDamage", "[[Damage|Normal]]" })
	tableHeaders:insert({ "FireRate", "[[Attack Speed]]" })
	tableHeaders:insert({ "CritChance", "[[Critical Chance]]" })
	tableHeaders:insert({ "CritMultiplier", "[[Critical multiplier|Critical Damage]]" })
	tableHeaders:insert({ "StatusChance", "[[Status Chance]]" })
	tableHeaders:insert({ "Mastery", "[[Mastery Rank]]" })
	tableHeaders:insert({ "IntroducedDate", "Introduced" })
	
	return buildCompTable(tableHeaders, WeapArray)
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 weaponType = frame.args ~= nil and frame.args[1]
	local weaponClass = frame.args ~= nil and frame.args[2] or nil
	if (weaponClass == "All") then weaponClass = nil end
	local weaponList = {}
	
	if (weaponType == "Primary") then
		weaponList = p._getWeapons(function(x) 
							if (getValue(x, "Type") == "Primary") then
								return (weaponClass ~= nil) and getPrimaryCategory(x) == weaponClass or true
							end
							return false
						end)
	elseif (weaponType == "Secondary") then
		weaponList = p._getWeapons(
			function(x) 
				if (getValue(x, "Type") == "Secondary") then
					return (weaponClass ~= nil) and getSecondaryCategory(x) == weaponClass or true
				end
				return false
			end)
	elseif (weaponType == "Robotic") then
		weaponList = p._getWeapons(
			function(x) 
				return getValue(x, "Type") == "Robotic"
			end)
	elseif (weaponType == "Arch-Gun") then
		weaponList = p._getWeapons(
			function(x) 
				return getValue(x, "Type") == "Arch-Gun"
			end)
	else
		error('p.getCompTableSpeedGuns(frame): Wrong gun weapon class '..
			'(use "Primary", "Secondary", "Robotic", or "Arch-Gun")[[Category:Invalid Comp Table]]')
	end
	
	-- special sorting for projectile weapons
	local projectileWeaponList = {}
	for k, Weapon in ipairs(weaponList) do
		local shotType = getValue(Weapon, "ShotType")
		if (shotType == "Projectile" or shotType == "Thrown") then
			table.insert(projectileWeaponList, Weapon)
		end
	end

	local tableHeaders = { { "NameLink", "Name" } }
	table.insert(tableHeaders,{ "Class", "Class" })
	table.insert(tableHeaders,{ "ShotSpeed", "Flight Speed" })
	
	return buildCompTable(tableHeaders, projectileWeaponList)
end

--- Builds comparison table of glaive melees' projectile flight speeds as seen on [[Projectile Speed]].
--  @function		p.getCompTableSpeedMelees
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.getCompTableSpeedMelees(frame)
	local weaponList = p._getWeapons(
			function(x)
				return getValue(x, "Class") == "Glaive"
			end)

	local tableHeaders = { {"NameLink", "Name"} }
	table.insert(tableHeaders, { "ShotSpeed", "Flight Speed", "Attack2" })
	
	return buildCompTable(tableHeaders, weaponList)
end

return p