WARFRAME Wiki
WARFRAME Wiki
(Documentation)
(Adding weapons to Category:Hit-Scan Weapons and Category:Projectile Weapons)
 
(805 intermediate revisions by 9 users not shown)
Line 1: Line 1:
--- '''Weapons''' contains all of [[WARFRAME]]'s non-modular [[Weapons|weapon]] data.<br />
+
--- '''Weapons''' contains all of [[WARFRAME]]'s [[Weapons|weapon]] data.<br />
--
+
--
-- @module weapons
+
-- @module weapons
-- @alias p
+
-- @alias p
-- @author [[User:Falterfire|Falterfire]]
+
-- @attribution [[User:Cephalon Scientia|Cephalon Scientia]]
-- @attribution [[User:Flaicher|Flaicher]]
+
-- @attribution [[User:FINNER|FINNER]]
-- @attribution [[User:FINNER|FINNER]]
+
-- @attribution [[User:Falterfire|Falterfire]]
-- @attribution [[User:Cephalon Scientia|Cephalon Scientia]]
+
-- @attribution [[User:Gigamicro|Gigamicro]]
-- @attribution [[User:Gigamicro|Gigamicro]]
+
-- @attribution [[User:Flaicher|Flaicher]]
-- @attribution [[User:Synthtech|Synthtech]]
+
-- @attribution [[User:Synthtech|Synthtech]]
  +
-- @image IconPrimaryWeaponRifle.png
-- @attribution [[User:Calenhed|Calenhed]]
 
  +
-- @require [[Module:StatObject]]
-- @image
 
-- @require [[Module:Icon]]
+
-- @require [[Module:DamageTypes]]
-- @require [[Module:Math]]
+
-- @require [[Module:Polarity]]
-- @require [[Module:String]]
+
-- @require [[Module:Math]]
 
-- @require [[Module:Table]]
 
-- @require [[Module:Table]]
-- @require [[Module:Mods/data]]
+
-- @require [[Module:Tooltips]]
-- @require [[Module:Modular/data]]
+
-- @require [[Module:Version]]
-- @require [[Module:Stances/data]]
+
-- @require [[Module:Stances/data]]
-- @require [[Module:Tooltips]]
+
-- @require [[Module:Weapons/data]]
-- @require [[Module:Version]]
+
-- @require [[Module:Weapons/Conclave/data]]
  +
-- @release stable
-- @require [[Module:Weapons/data]]
 
  +
-- <nowiki>
-- @require [[Module:Weapons/Conclave/data]]
 
-- @release stable
 
-- <nowiki>
 
   
  +
-- TODO: Add LuaDoc style comments to new functions
 
local p = {}
 
local p = {}
   
local ConclaveData = mw.loadData [[Module:Weapons/Conclave/data]]
+
local Delay = require([[Module:Delay]])
local ModData = mw.loadData [[Module:Mods/data]]
+
local WeaponData = Delay.require([[Module:Weapons/data]])
local ModularData = mw.loadData [[Module:Modular/data]]
+
local WarframeData = Delay.require([[Module:Warframes/data]]) -- for use in p.getListWithWarframes
local StanceData = mw.loadData [[Module:Stances/data]]
+
local ConclaveData = Delay.require([[Module:Weapons/Conclave/data]])
local WeaponData = mw.loadData [[Module:Weapons/data]]
+
local Tooltip = Delay.require([[Module:Tooltips]]) -- full, icon
  +
local Version = Delay.require([[Module:Version]]) -- _getVersion, _getVersionDate
  +
local Polarity = Delay.require([[Module:Polarity]]) -- _pols, _polarity
  +
local Math = Delay.require([[Module:Math]]) -- formatnum
  +
local Table = Delay.require([[Module:Table]]) -- size, skpairs
  +
local iterationOrderArray = require([[Module:DamageTypes]]).iterationOrderArray
   
  +
-- TODO: Should decouple from localized names for internationalization
local Icon = require [[Module:Icon]]
 
local Math = require [[Module:Math]]
+
local VARIANT_LIST = {
  +
"Prime", "Prisma", "Wraith", "Vandal", "Vaykor", "Synoid", "Telos", "Secura",
local String = require [[Module:String]]
 
  +
"Sancti", "Rakta", "Mara", "Carmine", "Ceti", "Dex", "MK1", "Kuva", "Tenet"
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"
 
 
}
 
}
   
  +
table.unpack = table.unpack or unpack
local VariantList = {
 
"Prime", "Prisma", "Wraith", "Vandal", "Vaykor", "Synoid",
 
"Telos", "Secura", "Sancti", "Rakta", "Mara", "MK1", "Kuva"
 
}
 
   
  +
local StatObject = require [[Module:StatObject]]
local function makeDTooltip(dt, addText)
 
  +
p.__StatObject = StatObject
local text = (addText) and dt or ''
 
  +
local statRead = StatObject.statRead
return ('<span class="tooltip" data-param="%s" data-param2="DamageTypes">[[File:Dmg%sSmall64.png|x19px|link=]]&nbsp;%s</span>')
 
  +
local statFormat = StatObject.statFormat
:format(dt, dt, text)
 
end
 
   
  +
local indexes = StatObject.meta.indexes
local function getDamageBias(attackEntry)
 
  +
local ors = StatObject.meta.ors
if (attackEntry.Damage ~= nil and Table.size(attackEntry.Damage) > 0) then
 
local totalDmg = 0
+
local unpacks = StatObject.meta.unpacks
  +
local bestDmg = 0
 
local bestElement = nil
+
local passes = StatObject.meta.passes
local elemCount = 0
+
local percent = StatObject.meta.percent
  +
local percents = StatObject.meta.percents
for damageType, dmg in pairs(attackEntry.Damage) do
 
  +
if (dmg > bestDmg) then
 
  +
--- Gets the attack entry from weapon entry.
bestDmg = dmg
 
  +
-- @function p._getAttack
bestElement = damageType
 
  +
-- @param {table} weap Weapon entry
end
 
  +
-- @param[opt] {number|table} atk Attacks table index or Attack entry
totalDmg = totalDmg + dmg
 
  +
-- @return {table} A single weapon+attack struct
if (dmg > 0) then
 
  +
local function getWeaponAttack(weap, atk)
elemCount = elemCount + 1
 
  +
if type(atk) == 'number' then return StatObject.getStruct2(weap,weap.Attacks[atk]) end
end
 
  +
if weap.AttackName then return weap end
end
 
  +
if type(atk) == 'table' then return StatObject.getStruct2(weap,atk) end
-- Make sure there are two damage instances that are above zero
 
  +
local key = atk or weap['_TooltipAttackDisplay'] or 1
-- Exception for physical damage types
 
if (elemCount > 0) then
+
if weap.Attacks == nil then
  +
error('p._getWeaponAttack(weap, atk): Attacks table is nil in '..mw.dumpObject(weap))
return (bestDmg / totalDmg), bestElement
 
end
 
error('getDamageBias(Attack): '..
 
'Damage key in Attack entry has no key-value pairs'..mw.dumpObject(attackEntry))
 
 
end
 
end
  +
return StatObject.getStruct2(weap,weap.Attacks[key])
error('getDamageBias(Attack): Attack entry has no Damage key')
 
 
end
 
end
  +
p._getAttack = getWeaponAttack
  +
p._getWeaponAttack = getWeaponAttack
   
  +
function p._statRead(w, a, ...)
--- Returns the highest damage type of an attack in the form of a percentage
 
  +
return statRead(getWeaponAttack(w, a), ...)
-- followed by a tooltip icon of the damage type
 
  +
end
-- @function getDamageBiasString
 
  +
function p._statFormat(w, a, ...)
-- @param {table} attackEntry Attack table
 
  +
return statFormat(getWeaponAttack(w, a), ...)
-- @returns {string} Resultant text
 
  +
end
local function getDamageBiasString(attackEntry)
 
  +
function p.stat(frame)
local bestPercent, bestElement = getDamageBias(attackEntry)
 
  +
return p._statFormat(p._getWeapon(frame.args[1] or 'Skana Prime'), nil, frame.args[2] or 'Name')
local result = Math.percentage(bestPercent)
 
return result..' '..makeDTooltip(bestElement)
 
 
end
 
end
   
  +
-- Wrapper function for use in StatObject
-- For now using what the tooltips is displaying for DPS calculations
 
  +
local function dmgTooltip(damageType)
-- I know it is not accurate b/c the other columns in comparison table may use a different Attack table by default
 
  +
return Tooltip.full(damageType, 'DamageTypes')
-- Calculates the derived damage stats commonly used in comparing gun performance.
 
-- calculateGunDerivedDamage
 
-- {table} Weapon Weapon table data as seen in M:Weapons/data
 
-- {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
 
end
   
  +
-- Defining getters/attributes whose names match the associated database key or some custom derived attribute.
local ATTACK_KEY_MAP = {
 
  +
-- Index key will be name of getter function and can be mapped to a single value (getter definition)
Impact = function(attackEntry) return attackEntry['Damage']['Impact'] or 0 end,
 
  +
-- or a table with two values (getter and format function definitions)
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,
 
}
 
   
  +
-- Cheatsheet on adding new keys:
local SHARED_KEY_MAP = {
 
  +
-- StatName = default value -> Get raw value with the same StatName from M:Weapons/data and with no additional formatting (aka default formatting)
Class = function(weaponEntry) return weaponEntry['Class'] end,
 
  +
-- StatName = function(self) return self.StatName + 1 end -> Define custom getter function and use default formatting
Disposition = function(weaponEntry)
 
  +
-- StatName = { default value, '%.2f' } -> Get raw value value with same StatName from M:Weapons/data and use format string for formatting
return weaponEntry['Disposition'] ~= nil and weaponEntry['Disposition'] or 'N/A'
 
  +
-- StatName = { function(self) return ... end, '%.2f' } -> Define custom getter function and use format string for formatting
end,
 
  +
-- StatName = { function(self) return ... end, function(self, returnValue1, returnValue2, ...) return tostring(returnValue) end } - > Define custom getter and format functions
Family = function(weaponEntry) return weaponEntry['Family'] end,
 
  +
-- (Note that format function will pass in return value(s) from getter as well as object self)
Introduced = function(weaponEntry)
 
  +
return weaponEntry['Introduced'] and Version._getVersion(weaponEntry['Introduced'])['Name'] or 'N/A'
 
  +
-- TODO: Put StatObject keys in alphabetical order for navigation
end,
 
  +
StatObject.default = {
IntroducedDate = function(weaponEntry)
 
  +
AttackName = 'Normal Attack',
return weaponEntry['Introduced'] and Version._getVersionDate(weaponEntry['Introduced']) or 'N/A'
 
  +
AmmoCost = nil,
end,
 
  +
AmmoPickup = function(weapAtk)
Link = function(weaponEntry) return '[['..weaponEntry['Link']..']]' end,
 
  +
return weapAtk['AmmoPickup'] or
Mastery = function(weaponEntry) return weaponEntry['Mastery'] or 'N/A' end,
 
  +
weapAtk['Slot'] == 'Primary' and 80 or
Name = function(weaponEntry) return '[['..weaponEntry['Name']..']]' end,
 
  +
weapAtk['Slot'] == 'Secondary' and 40 or
NameLink = function(weaponEntry) return '[['..weaponEntry['Link']..'|'..weaponEntry['Name']..']]' end,
 
  +
weapAtk['Slot'] == 'Archgun (Atmosphere)' and 1000 or
Polarities = function(weaponEntry) return Polarity._pols(weaponEntry['Polarities']) end,
 
  +
0
Traits = function(weaponEntry)
 
  +
end,
local traitString = {}
 
  +
DamageBias = {
for _, trait in ipairs(weaponEntry['Traits']) do
 
  +
function(weapAtk)
table.insert(traitString, trait)
 
  +
if not weapAtk.Damage then
  +
error('DamageBias: no Attack.Damage')
  +
return 0, 0, 0
 
end
 
end
  +
local total, bestdmg, bestdt = 0, 0, nil
return table.concat(traitString, ', ')
 
  +
for dt, dmg in pairs(weapAtk.Damage) do
end,
 
  +
local dmg = dmg
Type = function(weaponEntry) return weaponEntry['Type'] end
 
  +
if dmg >= bestdmg then
}
 
  +
bestdmg, bestdt = dmg, dt
 
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
  +
total = total + dmg
 
end
 
end
  +
return StatObject.ucacheIn(weapAtk, 'DamageBias', { bestdmg / total, bestdt, total })
return ''
 
 
end,
 
end,
  +
{ percent, passes(dmgTooltip), '' }
AvgProcCount = function(weaponEntry)
 
  +
},
local attackEntry = weaponEntry[weaponEntry['TooltipAttackDisplay'] or 'Attack1']
 
  +
BiasPortion = { indexes('DamageBias', 1), percent },
local statusChance = attackEntry['StatusChance'] or 0
 
  +
BiasType = { indexes('DamageBias', 2), function(self, biasType) return Tooltip.icon(biasType, 'DamageTypes') end },
local multishot = attackEntry['Multishot'] or 1
 
  +
BaseDamage = { indexes('DamageBias', 3), '%.2f' },
local numForcedProcs = attackEntry['ForcedProcs'] and Table.size(attackEntry['ForcedProcs']) or 0
 
  +
-- More precise damage values to 4 decimal places for PvP since PvP damage is calculated
return (statusChance + numForcedProcs) * multishot
 
  +
-- based on a floating-point scalar. Damage quantization is more relevant in PvP so more
  +
-- precise numbers needed.
  +
PvPBaseDamage = { indexes('DamageBias', 3), '%.4f' },
  +
TotalDamage = { function(weapAtk)
  +
return statRead(weapAtk, 'BaseDamage') * statRead(weapAtk, 'Multishot')
  +
end, passes(Math.formatnum)
  +
},
  +
-- Including max +60% Progenitor bonus for Kuva/Tenet weapons
  +
TotalDamageWithProgenBonus = { function(weapAtk)
  +
return statRead(weapAtk, 'TotalDamage') * (statRead(weapAtk, 'IsLichWeapon') and 1.6 or 1)
  +
end, passes(Math.formatnum)
  +
},
  +
ChargeTime = { 0, '%.1f s' },
  +
ExplosionDelay = { 0, '%.1f s' },
  +
ExtraHeadshotDmg = { 0, percents('+%.2f%%') },
  +
Falloff = {
  +
function(weapAtk)
  +
local fo = weapAtk['Falloff'] or {}
  +
return fo.StartRange or 0, fo.EndRange or math.huge, 1 - (fo.Reduction or 1)
 
end,
 
end,
  +
{ '%.1f m (100%%) -', '%.1f m', percents('(%.2f%%)') }
AvgProcPerSec = function(weaponEntry)
 
  +
},
local attackEntry = weaponEntry[weaponEntry['TooltipAttackDisplay'] or 'Attack1']
 
  +
FalloffStart = { indexes('Falloff', 1), '%.1f m' },
local statusChance = attackEntry['StatusChance'] or 0
 
  +
FalloffEnd = { indexes('Falloff', 2), '%.1f m' },
local multishot = attackEntry['Multishot'] or 1
 
  +
-- Damage reduction from falloff instead of damage multiplier
local numForcedProcs = attackEntry['ForcedProcs'] and Table.size(attackEntry['ForcedProcs']) or 0
 
  +
FalloffReduction = { function(weapAtk)
local fireRate = (attackEntry['ChargeTime'] ~= nil) and (1 / attackEntry['ChargeTime']) or attackEntry['FireRate']
 
  +
local _, _, falloff = statRead(weapAtk, 'Falloff')
return (statusChance + numForcedProcs) * multishot * fireRate
 
  +
return 1 - falloff
end,
 
  +
end, percent
AvgShotDmg = function(weaponEntry)
 
  +
},
local totalDamage, avgShot = calculateGunDerivedDamage(weaponEntry)
 
  +
FalloffRate = { function(weapAtk)
return avgShot
 
  +
local startdist,enddist,endpercent = statRead(weapAtk, 'Falloff')
end,
 
  +
return -(enddist-startdist)/(endpercent-1)
BurstDps = function(weaponEntry)
 
  +
end, '%.1fm/%%'
local totalDamage, avgShot, avgBurst = calculateGunDerivedDamage(weaponEntry)
 
  +
},
return avgBurst
 
  +
HeadshotMultiplier = { 1, '%.1fx' },
end,
 
  +
Multishot = 1,
ExilusPolarity = function(weaponEntry) return Polarity._polarity(weaponEntry['ExilusPolarity']) end,
 
  +
PunchThrough = { 0, '%.1f m' },
IsSilent = function(weaponEntry) return weaponEntry['IsSilent'] end,
 
  +
ShotSpeed = { nil, function(self, shotSpeed)
Magazine = function(weaponEntry) return weaponEntry['Magazine'] end,
 
  +
if shotSpeed == nil then
MaxAmmo = function(weaponEntry) return weaponEntry['MaxAmmo'] end,
 
  +
return 'N/A'
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
 
}
 
 
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 categoryMap = {
 
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
 
end
  +
return ('%.1f m/s'):format(shotSpeed)
 
table.insert(csvResult, '</pre>')
 
return table.concat(csvResult, '\n')
 
 
end
 
end
  +
},
 
  +
BurstDelay = { 0, '%.4f s' },
function p.isVariant(WeapName)
 
  +
BurstReloadDelay = { 0, '%.2f s' },
for i, var in pairs(VariantList) do
 
  +
BurstsPerSec = { function(weapAtk)
if (string.find(WeapName, var)) then
 
  +
-- There is no delay after last shot in burst
local baseName = string.gsub(WeapName, " ?"..var.." ?-?", "")
 
  +
return 1 / ( (1 / statRead(weapAtk, 'FireRate') ) + statRead(weapAtk, 'BurstDelay') * ( statRead(weapAtk, 'BurstCount') - 1) )
return true, var, baseName
 
  +
end, '%.2f bursts/sec' },
end
 
  +
CritChance = { 0, percent },
  +
CritMultiplier = { 1, '%.2fx' },
  +
ForcedProcs = { unpacks('ForcedProcs'), function(s, ...)
  +
local procs = { ... }
  +
if procs[1] == nil then
  +
return 'No forced procs'
 
end
 
end
  +
local result = {}
return false, "Base", WeapName
 
  +
for _, proc in ipairs(procs) do
end
 
  +
table.insert(result, Tooltip.full(proc, 'DamageTypes'))
 
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
 
else
 
return variant..' '..baseName
 
 
end
 
end
  +
return table.concat(result, ', ')
 
end
 
end
  +
},
 
  +
Radius = { 0, '%.1f m' },
--It's a bit of a mess, but this is for compressing a list with variants
 
  +
StatusChance = { 0, percent },
--So if a list has Braton, Braton Prime, and MK1-Braton it'll list as
 
  +
Disposition = {
-- Braton (MK1, Prime)
 
function p._shortLinkList(Weapons, tooltip)
+
function(weap)
  +
local d = weap['Disposition']
--First grabbing all the pieces and stashing them in a table
 
  +
-- Returning a categorical bin value of 1, 2, 3, 4, or 5 based on where disposition value
local baseNames = {}
 
  +
-- is on the continuous scale of 0.5-1.55. If disposition value is nil then return 0
for key, weap in Table.skpairs(Weapons) do
 
  +
return d or 0, type(d)=='number' and math.floor(5*(d-.3+.009*(d<1 and 1 or -1))) or 0
local isVar, varType, baseName = p.isVariant(weap.Name)
 
  +
end,
if (baseNames[baseName] == nil) then baseNames[baseName] = {} end
 
  +
function(s, v, d)
table.insert(baseNames[baseName], varType)
 
  +
return StatObject.default.Dispo[2](s, d)..(' (%.2fx)'):format(v)
 
end
 
end
  +
},
 
  +
Dispo = { indexes('Disposition', 2), function(s, d)
--Then the fun part: Pulling the table together
 
  +
if d and d == d and d > 0 then
local result = {}
 
  +
return ('●'):rep(math.min(d, 5))..('○'):rep(5 - d)
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
 
end
  +
return '×××××' -- '●○×' --> E2978F E2978B C397
return result
 
  +
end },
  +
Introduced = { function(weap)
  +
return weap['Introduced'] and Version._getVersion(weap['Introduced'])['Name'] or 'N/A'
  +
end, passes(Version._getVersionLink)
  +
},
  +
IntroducedDate = function(weap)
  +
return weap['Introduced'] and Version._getVersionDate(weap['Introduced']) or 'N/A'
  +
end,
  +
IsLichWeapon = function(weap)
  +
return weap['IsLichWeapon'] and true or false
  +
end,
  +
Mastery = 0,
  +
Link = { nil, '[[%s]]' },
  +
Name = { nil, function(s, v) return Tooltip.full(v, 'Weapons') end },
  +
InternalName = '',
  +
NameLink = { function(weap) return weap.Link, weap.Name end, '[[%s|%s]]' },
  +
Polarities = { nil, passes(Polarity._pols) },
  +
Traits = { unpacks('Traits'), { sep = ', ' } },
  +
-- Default nil b/c some attacks don't have an associated accuracy/spread value (like AoE explosions)
  +
Accuracy = { nil, function(self, value)
  +
if (value == nil) then
  +
return 'N/A'
  +
end
  +
return value
 
end
 
end
  +
},
 
  +
-- Inverse of accuracy. Spread of 1 equates to no spread.
function p._getWeapon(weaponName)
 
  +
-- Alternatively, it can be calculated by the average of min and max spread, see AvgSpread getter.
local weapon = WeaponData["Weapons"][weaponName]
 
  +
Spread = { function(weapAtk)
if weapon ~= nil then
 
  +
local accuracy = statRead(weapAtk, 'Accuracy')
return weapon
 
  +
return (accuracy == nil) and nil or 100 / accuracy
  +
end, function(self, value)
  +
if (value == nil) then
  +
return 'N/A'
 
end
 
end
  +
return value
 
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
 
end
  +
},
 
  +
AmmoType = function(weapAtk)
function p._getConclaveWeapon(weaponName)
 
  +
return weapAtk['AmmoType'] or ({
local weapon = ConclaveData["Weapons"][weaponName]
 
  +
['Archgun (Atmosphere)'] = 'Heavy',
if (weapon ~= nil and weapon.Name == weaponName) then
 
  +
['Secondary'] = 'Secondary',
return weapon
 
  +
['Primary'] = 'Primary'
end
 
  +
})[weapAtk['Slot']] or 'None'
 
  +
end,
for key, Weapon in Table.skpairs(ConclaveData["Weapons"]) do
 
  +
-- Not all weapons have an Exilus slot so default to nil
if (Weapon.Name == WeapName or key == WeapName) then
 
  +
ExilusPolarity = { nil, function(self, exilusPolarity)
return Weapon
 
  +
if (exilusPolarity == nil) then
end
 
  +
return 'N/A'
 
end
 
end
  +
return Polarity._polarity(exilusPolarity)
error('p._getConclaveWeapon(weaponName): "'..weaponName..'" does not exist in M:Weapons/Conclave/data')
 
 
end
 
end
  +
},
 
  +
Magazine = 1,
local function getAttack(Weapon, AttackType)
 
  +
AmmoMax = { function(weapAtk)
if (Weapon == nil or AttackType == nil) then return end
 
if (type(Weapon) == "string") then
+
if statRead(weapAtk, 'IsMelee') then
  +
return nil
Weapon = p._getWeapon(Weapon)
 
 
end
 
end
  +
return weapAtk['AmmoMax'] or math.huge
if (not AttackType) then
 
  +
end, passes(Math.formatnum)
return Weapon.Attack1 or Weapon.Damage and Weapon
 
  +
},
  +
Range = { function(weapAtk)
  +
return weapAtk['Range'] or statRead(weapAtk, 'ShotType') == 'Hit-Scan' and 300 or 0
  +
end, '%.1f m'
  +
},
  +
Reload = { ors('Reload', 'RechargeTime', 0), '%.2f s' },
  +
RechargeTime = { function(weapAtk)
  +
return statRead(weapAtk, 'ReloadStyle'):find'[Rr]egen' and statRead(weapAtk, 'Magazine') / statRead(weapAtk, 'ReloadRate') or nil
  +
end, '%.2f s'
  +
},
  +
ReloadRate = { 0, '%.2f rounds/sec' }, -- Used for rechargeable weapons; not necessarily inverse of reload time b/c of presence of reload delay
  +
ReloadDelay = { function(weapAtk)
  +
return weapAtk['ReloadDelay'] or 0
  +
end, '%.2f s'
  +
},
  +
ReloadDelayEmpty = { ors('ReloadDelayEmpty', 'ReloadDelay'), '%.2f s' },
  +
-- Reload speed will be calculated as the inverse of reload time for purposes
  +
-- of keeping how we rank stats consistent for [[Module:Weapons/preprocess]]
  +
-- (larger number = higher stat; a short reload time can be expressed as fast reload
  +
-- speed which would be a larger value in magnitude)
  +
ReloadSpeed = { function(weapAtk)
  +
return 1 / statRead(weapAtk, 'Reload')
  +
end, function(str, reloadSpeed)
  +
return string.format('%.2f%% reload progress per second', reloadSpeed * 100)
  +
end },
  +
ReloadStyle = 'Magazine',
  +
Spool = { 0, '%d rounds' },
  +
SpoolStartFireRate = { 0, '%.1fx' }, -- scalar that is applied to fire rate stat for auto-spool weapons
  +
AvgSpread = { function(weapAtk)
  +
local minSpread = statRead(weapAtk, 'MinSpread')
  +
local maxSpread = statRead(weapAtk, 'MaxSpread')
  +
if (minSpread == nil) then
  +
return nil
 
end
 
end
  +
return (minSpread + maxSpread) / 2
return Weapon[AttackType:find 'Attack' and AttackType or 'Attack'..AttackType]
 
  +
end, function(self, value)
end
 
  +
if (value == nil) then
 
  +
return 'N/A'
--- Loops through all possible attacks that a weapon may have.
 
  +
end
-- @function p._attackLoop
 
  +
return ('%.2f°'):format(value)
-- @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
 
end
  +
},
local function nextAttack(t, k)
 
  +
-- Default nil b/c some attacks don't have an associated accuracy/spread value (like AoE explosions)
if not k then return '1', t['Damage'] and t or t['Attack1'] end
 
  +
MinSpread = { nil, function(self, value)
local v
 
  +
if (value == nil) then
repeat
 
  +
return 'N/A'
k, v = next(t,k)
 
  +
end
until type(v) == 'table' and v['Damage']
 
return k, v
+
return ('%.2f°'):format(value)
 
end
 
end
  +
},
return nextAttack, Weapon
 
  +
MaxSpread = { nil, function(self, value)
end
 
  +
if (value == nil) then
 
  +
return 'N/A'
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 ('%.2f°'):format(value)
 
end
 
end
  +
},
return augments
 
  +
Trigger = 'N/A',
  +
BlockAngle = { 0, '%d&#176;' },
  +
ComboDur = { 0, '%.1f s' },
  +
FollowThrough = { 0, '%.1fx' },
  +
HeavyAttack = { 0, passes(Math.formatnum) },
  +
HeavySlamAttack = { 0, passes(Math.formatnum) },
  +
HeavyRadialDmg = { 0, passes(Math.formatnum) },
  +
HeavySlamRadius = { 0, '%.1f m' },
  +
MeleeRange = { 0, '%.2f m' },
  +
SlamAttack = { 0, passes(Math.formatnum) },
  +
SlamRadialDmg = { function(weapAtk)
  +
return weapAtk.SlamRadialDmg or 0, statRead(weapAtk, 'SlamRadialElement')
  +
end, function(self, dmg, elem)
  +
if elem then
  +
return Tooltip.icon(elem, 'DamageTypes')..' '..Math.formatnum(dmg)
  +
end
  +
return Math.formatnum(dmg)
 
end
 
end
  +
},
 
  +
SlamRadialElement = { nil, function(self, value)
-- Returns all melee weapons.
 
  +
return value ~= nil and Tooltip.full(value, 'DamageTypes') or 'Same damage type distribution as normal attack'
-- If weapType is not nil, only grab for a specific type
 
  +
end
-- For example, if weapType is "Nikana", only pull Nikanas
 
  +
},
-- Else grab all melee weapons
 
  +
-- Slam radial forced proc(s)
local function getMeleeWeapons(weapClass, PvP)
 
  +
SlamRadialProcs = { nil, function(self, proc)
local weaps = {}
 
  +
if type(proc)=='table' then
for i, weap in Table.skpairs(WeaponData["Weapons"]) do
 
  +
local result = {}
if ((weap.Ignore == nil or not weap.Ignore) and weap.Type ~= nil and weap.Type == "Melee") then
 
  +
for _, elem in ipairs(proc) do
local classMatch = (weapClass == nil or weap.Class == weapClass)
 
  +
table.insert(result, Tooltip.full(elem, 'DamageTypes'))
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 table.concat(result, '<br />')
  +
else
  +
return 'N/A'
 
end
 
end
 
return weaps
 
 
end
 
end
  +
},
 
  +
SlamRadius = { 0, '%.1f m' },
--As above, but for Conclave stats
 
  +
SlideAttack = { function(weapAtk)
local function getConclaveMeleeWeapons(weapClass, PvP)
 
  +
return weapAtk.SlamRadialDmg or 0, statRead(weapAtk, 'SlideElement')
local weaps = {}
 
  +
end, function(self, dmg, elem)
local weapClasses = {}
 
if (weapClass ~= nil) then
+
if elem then
  +
return Tooltip.icon(elem, 'DamageTypes')..' '..Math.formatnum(dmg)
weapClasses = String.split(weapClass, ",")
 
 
end
 
end
  +
return Math.formatnum(dmg)
 
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
 
end
  +
},
 
  +
SlideElement = { nil, function(self, value)
--Learning new things... Trying to allow sending in an arbitrary function
 
  +
return value ~= nil and Tooltip.full(value, 'DamageTypes') or 'Same damage type distribution as normal attack'
function p._getWeapons(validateFunction)
 
  +
end
local weaponList = {}
 
  +
},
for weaponName, weaponEntry in Table.skpairs(WeaponData["Weapons"]) do
 
  +
--[[Stances = function(weapAtk)
if (not Table.contains(WeaponData["IgnoreInCount"], weaponName) and
 
  +
if not statRead(weapAtk, 'IsMelee') then return end
(weaponEntry.Ignore == nil or not weaponEntry.Ignore) and validateFunction(weaponEntry)) then
 
  +
return Stances._getAllStancesSameType(statRead(weapAtk, "Class"))
table.insert(weaponList, weaponEntry)
 
  +
-- ^ currently a local function
end
 
  +
end,--]]
  +
-- Not all weapons have an Stance slot so default to nil
  +
StancePolarity = { nil, function(self, stancePolarity)
  +
if (stancePolarity == nil) then
  +
return 'N/A'
 
end
 
end
  +
return Polarity._polarity(stancePolarity)
return weaponList
 
 
end
 
end
  +
},
  +
SweepRadius = { 0, '%.2f m' },
  +
WindUp = { 0, '%.1f s' },
  +
BurstCount = 1,
  +
-- Average crit/proc count from a single attack input
  +
AvgCritCount = function(weapAtk)
  +
return statRead(weapAtk, 'CritChance') * statRead(weapAtk, 'Multishot')
  +
end,
  +
AvgCritPerSec = function(weapAtk)
  +
return statRead(weapAtk, 'AvgCritCount') * statRead(weapAtk, 'EffectiveFireRate')
  +
end,
  +
AvgProcCount = function(weapAtk)
  +
return ( statRead(weapAtk, 'StatusChance') + Table.size(weapAtk['ForcedProcs'] or {}) ) * statRead(weapAtk, 'Multishot')
  +
end,
  +
AvgProcPerSec = function(weapAtk)
  +
return statRead(weapAtk, 'AvgProcCount') * statRead(weapAtk, 'EffectiveFireRate')
  +
end,
  +
InterShotTime = function(weapAtk)
  +
local v = statRead(weapAtk, 'Magazine') == 1 and statRead(weapAtk, 'Reload') + statRead(weapAtk, 'ReloadDelayEmpty') or 0
  +
if v == 0 then v = 1 / statRead(weapAtk, 'FireRate') end
  +
return v
  +
end,
  +
EffectiveFireRate = function(weapAtk)
  +
return 1 / ( statRead(weapAtk, 'ChargeTime') + statRead(weapAtk, 'InterShotTime') )
  +
end,
  +
ShotsPerMag = function(weapAtk)
  +
-- Default to 1 "ammo cost" even if attack does not directly consume ammo (e.g. AoE hits, speargun throws, etc.)
  +
return math.floor(statRead(weapAtk, 'Magazine') / (statRead(weapAtk, 'AmmoCost') or 1))
  +
end,
  +
FireRate = { function(weapAtk)
  +
local dataFireRate = weapAtk['FireRate']
  +
if dataFireRate then return dataFireRate end
  +
-- TODO: Think we can safely remove this calculation of FireRate from BurstFireRate, BurstDelay, and BurstCount
  +
-- for burst-fire attacks since FireRate is also included for those
  +
mw.log('calculating FireRate from Burst stats for '..statRead(weapAtk, 'Name'))
  +
local count = statRead(weapAtk, 'BurstCount')
  +
local fireRate = count / (1 / statRead(weapAtk, 'BurstFireRate') + count * statRead(weapAtk, 'BurstDelay'))
  +
return fireRate
  +
end, '%.3f attacks/sec'
  +
},
  +
BurstFireRate = { function(weapAtk)
  +
return 1 / statRead(weapAtk, 'BurstDelay')
  +
end, '%.2f attacks/sec'
  +
},
  +
--[[
  +
Describing what happens when a gun in WARFRAME is fired using player-made terminology:
   
  +
A particular gun consumes a set number of ammo in order to fire a set number of shots
--Same as getWeapons, but for Conclave data
 
  +
on a single player input for a particular attack.
function p.getConclaveWeapons(validateFunction)
 
  +
local weaps = {}
 
  +
A single player input is defined as:
for i, weap in Table.skpairs(ConclaveData["Weapons"]) do
 
  +
* a single attack button press for semi-auto and burst trigger weapons
if ((weap.Ignore == nil or not weap.Ignore) and validateFunction(weap)) then
 
  +
* the moment the next shot is being fired when the attack button is being held for automatic/held trigger weapons
table.insert(weaps, weap)
 
  +
* the action of holding the attack button for charge trigger weapons
  +
* for duplex-fire trigger weapons, the hold and release of the attack button counts as two inputs
  +
  +
A shot is defined as the base unit of attack of a weapon when unmodded.
  +
* A single attack input can launch several shots as in the case of burst-fire weapons.
  +
* A single shot can shoot more than one projectile, affected by the multishot stat, as in the case of shotguns.
  +
* A single shot can consume more than one unit of ammo (e.g. Tenora's alt-fire) or
  +
less than one unit of ammo (e.g. Ignis and most continuous weapons).
  +
  +
A gun can have multiple attacks which can be triggered using different buttons
  +
and/or types of button inputs (e.g. pressing vs. holding)
  +
]]--
  +
CalcDamage = function(weapAtk)
  +
local weapon, attack = weapAtk, weapAtk
  +
-- Count
  +
-- How many shots are fired in a single player input
  +
local tapShots = statRead(weapAtk, 'BurstCount')
  +
-- How many individual player inputs can occur before depleting a magazine
  +
local magTaps = statRead(weapAtk, 'ShotsPerMag')
  +
-- How many additional projectiles are fired per ammo
  +
local multishot = statRead(weapAtk, 'Multishot')
  +
-- How much ammo is contained in the magazine
  +
local magazine = statRead(weapAtk, 'Magazine')
  +
-- How much ammo can be drawn from reserves (or?, how much ammo can be used without picking up more)
  +
local ammoMax = statRead(weapAtk, 'AmmoMax')
  +
  +
-- Time^-1
  +
local fireRate = statRead(weapAtk, 'FireRate')
  +
-- Time
  +
local shotTime = statRead(weapAtk, 'InterShotTime')
  +
local chargeTime = statRead(weapAtk, 'ChargeTime')
  +
local burstDelayTime = statRead(weapAtk, 'BurstDelay')
  +
local reloadDelayTime = statRead(weapAtk, 'ReloadDelayEmpty')
  +
local reloadTime = statRead(weapAtk, 'Reload')
  +
local tapTime = chargeTime + (tapShots - 1) * burstDelayTime
  +
-- tapTime: The time between the last shot fired and the next valid attack input
  +
-- (omitting latency of course).
  +
-- Note that first shot of any non-charge trigger attack is instantenous
  +
local magDepletionTime = magTaps * tapTime
  +
if magDepletionTime == 0 then -- If attack is not a charged attack
  +
if shotTime == 0 then
  +
shotTime = 1 / fireRate
 
end
 
end
  +
magDepletionTime = magTaps / fireRate
 
end
 
end
  +
local shotDelayTime = math.max(0, shotTime - tapTime)
return weaps
 
end
 
   
  +
-- Multiplier
local function getStances(weapType, pvpOnly)
 
  +
local maxProgenitorBonus = statRead(weapAtk, 'IsLichWeapon') and 1.6 or 1
local stanceTable = {}
 
  +
local avgCritMult = 1 + (statRead(weapAtk, 'CritMultiplier') - 1) * statRead(weapAtk, 'CritChance')
  +
-- Damage
  +
local biasPortion, biasType, hitDamage = statRead(weapAtk, 'DamageBias')
 
 
  +
local avgDmgOnTap = hitDamage * avgCritMult * multishot * tapShots * maxProgenitorBonus
for stanceName, Stance in pairs(StanceData) do
 
  +
local avgDmgPerMag = avgDmgOnTap * magTaps
local typeMatch = (weapType == nil or weapType == 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
 
 
 
  +
-- 1 is needed b/c one whole magazine is not included in reserve ammo count
return stanceTable
 
  +
-- If there is no reserve ammo, that means that weapon can deal an infinite amount of damage theoretically
end
 
  +
local avgLifetimeDmg = (ammoMax ~= nil) and avgDmgPerMag * (1 + (ammoMax / magazine)) or math.huge
 
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
 
 
 
  +
-- Damage / Time
local result = ""
 
  +
local baseDps = hitDamage * multishot / shotTime
  +
local avgSustainedDps = avgDmgPerMag / (magDepletionTime + reloadDelayTime + reloadTime) / tapShots
  +
local avgBurstDps = avgDmgOnTap / (tapTime + shotDelayTime) / tapShots
  +
-- Note that burst DPS can also be calculated as such:
  +
-- local avgBurstDps = (hitDamage * avgCritMults * maxProgenitorBonus) * multishot / shotTime
  +
-- local avgBurstDps = avgDmgPerMag / magDepletionTime
 
 
  +
return StatObject.ucacheIn(weapAtk, 'CalcDamage',
for stanceName, Stance in pairs(stanceTable) do
 
  +
{ hitDamage, avgDmgOnTap, avgBurstDps, avgSustainedDps, avgLifetimeDmg, baseDps, avgDmgPerMag }
if (string.len(result) > 0) then
 
  +
)
result = result.."<br/>"
 
  +
end,
  +
ShotDmg = indexes('CalcDamage', 1), -- Total damage per projectile
  +
AvgShotDmg = indexes('CalcDamage', 2), AvgTapDmg = indexes('CalcDamage', 2), -- Average total damage per each input button
  +
BurstDps = indexes('CalcDamage', 3), -- Average burst damage per second/DPS w/o reloading
  +
SustainedDps = indexes('CalcDamage', 4), -- Average sustained damage per second/DPS w/ reloading
  +
LifetimeDmg = indexes('CalcDamage', 5), -- Average total damage from entire ammo pool
  +
BaseDps = indexes('CalcDamage', 6), -- Base damage per second w/ multishot w/o crit
  +
MagDmg = indexes('CalcDamage', 7), -- Average total damage per magazine
  +
-- Average damage scaled by melee attack speed multiplier (numerator of melee DPS w/o accounting for stances and animation time)
  +
AvgDmgWithAnimSpeedMulti = function(weapAtk)
  +
if statRead(weapAtk, 'IsMelee') then
  +
-- Some melee weapons have attacks with multishot like Redeemer, Vastilok, and Quassus
  +
return statRead(weapAtk, 'BaseDamage') * statRead(weapAtk, 'Multishot') * statRead(weapAtk, 'AttackSpeed')
  +
end
  +
return 0
  +
end,
  +
AttackSpeed = { --[[ors('AttackSpeed', 'FireRate')]]function(weapAtk)
  +
if not statRead(weapAtk, 'IsMelee') then
  +
error('AttackSpeed: Cannot get AttackSpeed attribute for a non-melee weapon; use p.statRead(weapAtk, "FireRate") instead')
  +
end
  +
return statRead(weapAtk, 'FireRate')
  +
end, '%.2fx animation speed'
  +
},
  +
IsMelee = function(weapAtk) return statRead(weapAtk, 'Slot'):find('Melee') or statRead(weapAtk, 'Slot'):find('melee') ~= nil end,
  +
IsSilent = ors('IsSilent', 'IsMelee', false),
  +
HasAoEAttack = function(weap)
  +
for i, attackEntry in pairs(weap['Attacks']) do
  +
if attackEntry['ShotType'] == 'AoE' then
  +
return true
 
end
 
end
  +
end
 
  +
return false
local polarity = ""
 
  +
end,
local link = ""
 
  +
Conclave = false,
if Weapon.Class == "Exalted Weapon" then
 
  +
Image = { 'Panel.png', '[[File:%s|link=]]' },
link = "[["..ModData["Mods"][stanceName].Link.."|"..stanceName.."]]"
 
  +
Attacks = ors('Attacks', p._getAttack, {}),
else
 
  +
Family = nil,
polarity = " ("..Polarity._polarity(ModData["Mods"][stanceName].Polarity)..")"
 
  +
FamilyList = { function(weapAtk)
link = "[["..stanceName.."]]"
 
  +
local family = statRead(weapAtk, 'Family')
end
 
  +
-- assert(family, 'i have no Family :\'(')
 
  +
if not family then return {weapAtk} end
-- Adding tooltip
 
  +
-- return family, statRead(weapAtk, 'Slot')
result = result..Tooltip.getIcon(stanceName, "Mods")
 
  +
local slot = statRead(weapAtk, 'Slot')
 
  +
local result = {}
-- If this is a PvP Stance, add the disclaimer
 
  +
for _, w in pairs(WeaponData[slot] or error('FamilyList: no weapondata for slot '..(slot or '<nil>'))) do
if (Stance.ConclaveOnly ~= nil and Stance.ConclaveOnly) then
 
  +
if w.Family == family then
result = result.." (PvP Only)"
 
  +
table.insert(result, w)
 
end
 
end
 
end
 
end
  +
table.sort(result, function(a,b) return a.Name<b.Name end)
 
 
return result
 
return result
  +
end, function(self, result)
  +
for i,w in ipairs(result) do
  +
result[i]=Tooltip.full(w.Name, 'Weapons', w)
  +
end
  +
return table.concat(result, '<br />')
 
end
 
end
  +
},
  +
BaseName = function(weapAtk) return weapAtk['BaseName'] or ({p._getVariant(statRead(weapAtk, 'Name'))})[3] end,
  +
-- TODO: Add comments to Explosion function for readability
  +
-- TODO: Do not rely on attack name to determine what AoE component is attached to which main direct hit component
  +
---^i suggest an explosion key with either the attack number of any corresponding explosion, nested attack tables, or some other way to make a tree
  +
-- TODO: Use ShotType = "AoE" to determine if attack entry is AoE
  +
Explosion = function(weapAtk)
  +
local weap, atk = weapAtk, weapAtk
  +
-- tbh this is a mess
  +
local explosion = weapAtk['Explosion'] or statRead(weapAtk, 'AttackName'):gsub(' Impact',''):gsub(' Contact','')..' Explosion'
  +
if type(explosion) == 'string' then
  +
explosion = weap.Attacks[tonumber(explosion:gsub('%D',''))] or explosion
  +
elseif type(explosion) == 'number' then
  +
explosion = weap.Attacks[explosion] or explosion
  +
end
  +
local explosions = {}
  +
if type(explosion) ~= 'table' then
  +
for i, v in ipairs(weap.Attacks) do
  +
if p._statRead(weapAtk, v, 'AttackName'):find 'xplosion' then
  +
if p._statRead(weapAtk, v, 'AttackName') == explosion then
  +
explosions[1] = nil
  +
explosion = v
  +
break
  +
end
  +
table.insert(explosions, v)
  +
end
  +
end
  +
explosion = explosions[1] or explosion
  +
end
  +
StatObject.pcacheIn(getWeaponAttack(weap, explosion), 'BaseAttack', atk)
  +
return StatObject.pucacheIn(weapAtk, 'Explosion', explosion)
  +
end,
  +
IsVariant = function(weap)
  +
return StatObject.pucacheIn(weap, 'IsVariant', p._isVariant(statRead(weap, 'Name')))
  +
end,
  +
Variant = indexes('IsVariant', 2),
  +
BaseName = indexes('IsVariant', 3),
  +
Categories = { function(weapAtk)
  +
local cats = { 'Weapons' }
  +
-- Adding editor-defined traits from M:Weapons/data
  +
-- Note to make sure they have a proper category page associated with a trait
  +
for _, trait in ipairs(weapAtk.Traits or {}) do
  +
table.insert(cats, trait..' Weapons')
  +
end
  +
--[=[
  +
local CATEGORY_MAP = {
  +
Primary = 'Primary Weapons',
  +
Secondary = 'Secondary Weapons',
  +
Melee = 'Melee Weapons',
  +
['Archmelee'] = 'Archwing Melee',
  +
['Archgun'] = 'Archwing Gun',
  +
['Archgun (Atmosphere)'] = 'Archwing Gun',
  +
Kitgun = 'Kitgun',
  +
Zaw = 'Zaw',
  +
['Railjack Turret'] = 'Railjack',
  +
['Railjack Armament'] = 'Railjack',
  +
Gear = 'Gear',
 
 
  +
Rifle = 'Assault Rifle',
--- Gets the value of a certain statistic of a weapon.
 
  +
['Sniper Rifle'] = 'Sniper Rifle',
-- @function getValue
 
  +
Shotgun = 'Shotgun',
-- @param {table} Weapon Weapon table
 
  +
Pistol = 'Pistol',
-- @param {string} keyName Name of key
 
  +
['Dual Pistols'] = 'Dual Pistols',
-- @param[opt] {string} attackName Name of attack to search through
 
  +
Bow = 'Bow',
-- @returns {string, number} Value of statistic
 
  +
Launcher = 'Launcher',
local function getValue(Weapon, keyName, attackName)
 
  +
['Arm-Cannon'] = 'Arm-Cannon',
-- Trying to use pcall to get more specific errors
 
  +
['Speargun'] = 'Speargun',
-- for _, map in pairs({ SHARED_KEY_MAP, GUN_KEY_MAP, MELEE_KEY_MAP, ATTACK_KEY_MAP }) do
 
  +
Thrown = 'Thrown',
-- if (map[keyName] ~= nil) then
 
  +
['Shotgun Sidearm'] = 'Shotgun Sidearm',
-- local status, result = pcall(map[keyName], Weapon)
 
-- end
 
-- end
 
-- error('getValue(Weapon, keyName, attackName): Cannot get keyName "'..keyName..'" in '..mw.dumpObject(Weapon))
 
 
 
  +
Prime = 'Prime Weapons',
return SHARED_KEY_MAP[keyName] ~= nil and SHARED_KEY_MAP[keyName](Weapon) or
 
  +
['Never Vaulted'] = 'Never Vaulted',
GUN_KEY_MAP[keyName] ~= nil and GUN_KEY_MAP[keyName](Weapon) or
 
  +
Vaulted = 'Vaulted',
MELEE_KEY_MAP[keyName] ~= nil and MELEE_KEY_MAP[keyName](Weapon) or
 
  +
Wraith = 'Wraith',
ATTACK_KEY_MAP[keyName] ~= nil and ATTACK_KEY_MAP[keyName](Weapon[attackName or Weapon['TooltipAttackDisplay'] or 'Attack1']) or
 
  +
Vandal = 'Vandal',
error('getValue(Weapon, keyName, attackName): Cannot get keyName "'..keyName..'" in '..mw.dumpObject(Weapon))
 
  +
['Kuva Lich'] = 'Kuva Lich',
end
 
  +
['Kuva Lich'] = 'Kuva',
  +
Prisma = 'Prisma',
  +
  +
Grineer = 'Grineer Weapons',
  +
Corpus = 'Corpus Weapons',
  +
Infested = 'Infested Weapons',
  +
Tenno = 'Tenno Weapons',
  +
Sentient = 'Sentient Weapons',
  +
Entrati = 'Entrati Weapons',
  +
Baro = 'Baro Ki\'Teer Offering',
  +
Syndicate = 'Syndicate Offerings',
  +
['Invasion Reward'] = 'Invasion Reward',
  +
  +
['Alt Fire'] = 'Weapons with Alt Fire',
  +
['AoE'] = 'Weapons with Area of Effect]][[Category:Self Interrupt Weapons',
  +
  +
}
  +
--]=]
  +
local bias = p._getValue(weapAtk, "BiasType")
  +
table.insert(cats, bias..' Damage Weapons')
  +
  +
local class = p._getValue(weapAtk, "Class")
  +
table.insert(cats, ({
  +
["Arm Cannon"] = class,
  +
["Bow"] = class,
  +
["Crossbow"] = "Bow",
  +
["Dual Pistols"] = class,
  +
["Dual Shotguns"] = class,
  +
["Exalted Weapon"] = class,
  +
["Launcher"] = class,
  +
["Pistol"] = class,
  +
["Rifle"] = class,
  +
["Shotgun Sidearm"] = class,
  +
["Shotgun"] = class,
  +
["Sniper Rifle"] = class,
  +
["Speargun"] = class,
  +
["Thrown"] = class,
  +
["Tome"] = class,
  +
["Assault Saw"] = class,
  +
["Blade and Whip"] = class,
  +
Claws = class,
  +
Dagger = class,
  +
["Dual Daggers"] = class,
  +
["Dual Nikanas"] = class,
  +
["Dual Swords"] = class,
  +
Fist = class,
  +
Glaive = class,
  +
Gunblade = class,
  +
Hammer = class,
  +
["Heavy Blade"] = class,
  +
["Heavy Scythe"] = class,
  +
Machete = class,
  +
Nikana = class,
  +
Nunchaku = class,
  +
Polearm = class,
  +
Rapier = class,
  +
Scythe = class,
  +
Sparring = class,
  +
Staff = class,
  +
Sword = class,
  +
["Sword and Shield"] = class,
  +
["Two-Handed Nikana"] = class,
  +
Tonfa = class,
  +
Warfan = class,
  +
Whip = class,
  +
["Archgun"] = 'Archwing Gun',
  +
["Archgun (Atmosphere)"] = 'Archwing Gun',
  +
["Archmelee"] = 'Archwing Melee',
  +
["Turret"] = 'Railjack Armaments',
  +
["Ordnance"] = 'Railjack Armaments',
  +
Amp = class
  +
})[class] or "Other Weapons")
   
  +
local family = p._getValue(weapAtk, "Family")
-- Used on [[Template:StanceWeapons]]
 
  +
table.insert(cats, family)
function p.getStanceWeaponList(frame)
 
  +
local stanceName = frame.args ~= nil and frame.args[1] or frame
 
local Stance = StanceData[stanceName]
+
local slot = p._getValue(weapAtk, "Slot")
  +
table.insert(cats, slot..' Weapons')
assert(Stance ~= nil, "p.getStanceWeaponList(frame): "..stanceName.." not found")
 
  +
  +
-- TODO: Move all these trigger categories to be in the format "Category:Trigger Name Weapons"
  +
local trigger = p._getValue(weapAtk, "Trigger")
  +
table.insert(cats, trigger..' Weapons')
  +
-- TODO: remove original trigger cats? (following snippet)
  +
table.insert(cats,(({
  +
["Active"] = trigger,
  +
["Auto Charge"] = 'Charge',
  +
["Auto"] = 'Automatic',
  +
["Auto-Burst"] = 'Burst Fire',
  +
["Auto-Spool"] = 'Automatic',
  +
["Burst"] = 'Burst Fire',
  +
["Charge"] = trigger,
  +
["Duplex"] = 'Duplex Fire',
  +
["Held"] = 'Continuous Weapons',
  +
["Semi-Auto"] = 'Semi-Automatic',
  +
})[trigger]))--modes? " Weapons"?
 
 
  +
local users = p._getValue(weapAtk, "Users") or {}
local weaps = getMeleeWeapons(Stance.WeaponType, Stance.ConclaveOnly)
 
  +
for _, user in ipairs(users) do table.insert(cats, user) end
local result = ""
 
 
 
  +
local variant = p._getValue(weapAtk, "Variant")
for i, weap in Table.skpairs(weaps) do
 
  +
table.insert(cats, variant)
if (string.len(result) > 0) then
 
  +
result = result.."\n"
 
  +
local infAmmo = p._getValue(weapAtk, "AmmoMax") == math.huge
end
 
  +
local accuracy = p._getValue(weapAtk, "Accuracy")
 
  +
local pinpoint = accuracy ~= nil and accuracy >= 100
if (Stance.ConclaveOnly) then
 
  +
local regens = p._getValue(weapAtk, "ReloadRate") > 0
result = result.."*[[Conclave:"..weap.Name.."|"..weap.Name.."]]"
 
  +
local silent = weapAtk.IsSilent -- automatically includes
else
 
  +
local single = p._getValue(weapAtk, "Magazine") == 1 and not p._getValue(weapAtk, "IsMelee")--meh, delet?
result = result.."*[["..weap.Name.."]]"
 
  +
local spools = p._getValue(weapAtk, "Spool") > 0
end
 
  +
 
  +
local isHitScan = p._getValue(weapAtk, "ShotType") == "Hit-Scan"
if (weap.StancePolarity == ModData["Mods"][stanceName].Polarity) then
 
  +
local isProjectile = p._getValue(weapAtk, "ShotType") == "Projectile"
result = result.." ✓"
 
  +
local isAoE = p._getValue(weapAtk, "HasAoEAttack")
end
 
  +
local isCodexSecret = p._getValue(weapAtk, "CodexSecret")
  +
local isTradable = p._getValue(weapAtk, "Tradable")
  +
local isInConclave = p._getValue(weapAtk, "Conclave")
  +
  +
-- Arbitrarily ordering misc categories
  +
if infAmmo then table.insert(cats, 'Infinite Ammo Weapons') end
  +
if pinpoint then table.insert(cats, 'Pinpoint Weapons') end
  +
if regens then table.insert(cats, 'Battery Weapons') end
  +
if silent then
  +
table.insert(cats, 'Silent Weapons')
  +
else
  +
table.insert(cats, 'Alarming Weapons')
  +
end
  +
if single then table.insert(cats, 'Single Shot Weapons') end
  +
if spools then table.insert(cats, 'Spooling Weapons') end
  +
if isHitScan then table.insert(cats, 'Hit-Scan Weapons') end
  +
if isProjectile then table.insert(cats, 'Projectile Weapons') end
  +
if isAoE then table.insert(cats, 'Weapons with Area of Effect') end
  +
if isCodexSecret then table.insert(cats, 'Codex Secret') end
  +
if isTradable then
  +
table.insert(cats, 'Tradeable Weapons')
  +
else
  +
table.insert(cats, 'Untradeable Weapons')
 
end
 
end
  +
if isInConclave then table.insert(cats, 'Available In Conclave') end
 
 
  +
return StatObject.cacheIn(weapAtk, 'Categories', cats)
return result
 
  +
end, function(s, cats)
  +
local wikitextResult = { '' } -- Need to prepend a newline so first asterisk is rendered as a wikitext list
  +
local formatStr = '*[[:Category:%s|%s]][[Category:%s]]'
  +
for _, category in ipairs(cats) do
  +
table.insert(wikitextResult, formatStr:format(category, category, category))
  +
end
  +
return table.concat(wikitextResult, '\n')
 
end
 
end
  +
},
 
  +
SyndicateEffect = { '', function(s, v)
-- Used on [[Template:MeleeCategory]]
 
  +
return (v == '' or type(v) ~= 'string') and '' or Tooltip.icon(({
--Returns a list of stances for a weapon
 
  +
['blight'] = 'Red Veil',
--Adds the polarity for each stance
 
  +
['entropy'] = 'Cephalon Suda',
function p.getWeaponStanceList(frame)
 
  +
['justice'] = 'Steel Meridian',
local WeaponName = frame.args ~= nil and frame.args[1] or frame
 
  +
['purity'] = 'New Loka',
local Weapon = p._getWeapon(WeaponName)
 
  +
['sequence'] = 'The Perrin Sequence',
return getWeaponStanceList(Weapon)
 
  +
['truth'] = 'Arbiters of Hexis',
  +
})[v:lower()] or 'Tenno', 'Factions')
  +
..' '..v
 
end
 
end
  +
},
 
  +
MinProgenitorBonus = function(weap) return weap.IsLichWeapon and statRead(weap, 'BaseDamage') * 0.25 or 0 end,
local function getWeaponGallery(Weapons)
 
  +
ProgenitorBonus = function(weap) return weap.IsLichWeapon and statRead(weap, 'BaseDamage') * 0.6 or 0 end,
local result = { '{| style="margin:auto;text-align:center;"' }
 
  +
Class = '',
local nameRow = ""
 
  +
SniperComboReset = { nil, '%.1f s' },
for i, Weapon in pairs(Weapons) do
 
  +
SniperComboMin = { nil, '%d shot(s)' },
local theImage = Weapon.Image ~= nil and Weapon.Image or "Panel.png"
 
  +
Tradable = { function(weapAtk)
if ((i - 1) % 5 == 0) then
 
  +
if type(weapAtk['Tradable'])=='number' then
table.insert(result, nameRow.."\n|-")
 
  +
assert(weapAtk['Tradable']<=5,
nameRow = "\n|-"
 
  +
'Tradable: Does not support tradeability enums beyond 5; please update [[Module:Weapons/data]] and [[Module:Weapons]] to support more tradeability edge cases')
end
 
  +
return ({
table.insert(result, '| style="width:165px" |[[File:'..theImage..'|150px|link='..Weapon.Name..']]')
 
  +
[0]=false,
nameRow = nameRow..'\n| style="vertical-align: text-top;" |[['..Weapon.Name..']]'
 
  +
[1]='Unranked',
  +
[2]='Parts',
  +
[3]='Lich',
  +
[4]='Built Parts',
  +
[5]='Parent',
  +
})[weapAtk['Tradable']]
 
end
 
end
  +
return weapAtk['Tradable']
table.insert(result, nameRow)
 
  +
end, function(s, tradable)
table.insert(result, '|}')
 
return table.concat(result, '\n')
+
return ({
  +
[false] = 'Untradeable',
  +
Unranked = 'Tradeable unranked w/ no Forma or Catalyst',
  +
Parts = 'Tradeable parts and/or blueprint only',
  +
Lich = 'Tradeable indirectly through [[Lich System|Lich]] trading',
  +
['Built Parts'] = 'Tradeable only fully built components, not blueprints',
  +
Parent = 'Tradeable indirectly, comes with parent companion',
  +
})[tradable] or 'Untradeable?'
 
end
 
end
  +
},
 
  +
SellPrice = { nil, function(self, sellPrice)
-- Used on [[Template:MeleeCategory]]
 
  +
if sellPrice == nil then
function p.getMeleeWeaponGallery(frame)
 
  +
return 'Cannot sell'
local Type = frame.args ~= nil and frame.args[1] or frame
 
if (Type == nil) then
 
return ""
 
 
end
 
end
  +
return Tooltip.icon('Credits', 'Resources')..' '..Math.formatnum(sellPrice)
local WeapArray = getMeleeWeapons(Type)
 
  +
end
local result = "=="..Type.." Weapons==\n"
 
  +
},
result = result..getWeaponGallery(WeapArray)
 
  +
DefaultUpgrades = { nil, function(self, upgradesArr)
return result
 
  +
local result = {}
  +
for _, modIndex in ipairs(upgradesArr or {}) do
  +
table.insert(result, Tooltip.full(modIndex, 'Mods'))
  +
end
  +
return table.concat(result, '<br />')
  +
end
  +
},
  +
Users = { nil, function(self, usersArr)
  +
local result = { '' }
  +
for _, user in ipairs(usersArr or {}) do
  +
table.insert(result, '*[['..user..']]')
  +
end
  +
return table.concat(result, '\n')
  +
end
  +
},
  +
Zoom = { unpacks('Zoom'), { sep = '<br />' } },
  +
Slot = nil,
  +
}
  +
-- Loops for adding to StatObject.default table
  +
-- Damage type getters:
  +
-- <DamageType> = damage type value
  +
-- <DamageType>Distribution = damage type distribution as a percentage
  +
-- PvP<DamageType> = damage type value with precise formatting for PvP purposes
  +
for _, damageType in ipairs(iterationOrderArray) do
  +
StatObject.default[damageType] = {
  +
function(weapAtk) return weapAtk['Damage'][damageType] or 0 end,
  +
function(self, value) return Tooltip.icon(damageType, 'DamageTypes')..' '..Math.formatnum(value) end
  +
}
  +
-- Damage distribution as a percentage
  +
StatObject.default[damageType..'Distribution'] = {
  +
function(weapAtk) return weapAtk['Damage'][damageType] / statRead(weapAtk, 'BaseDamage') end,
  +
function(self, value) return Tooltip.icon(damageType, 'DamageTypes')..' '..Math.percentage(value) end
  +
}
  +
-- More precise damage values to 4 decimal places for PvP
  +
StatObject.default['PvP'..damageType] = {
  +
function(weapAtk) return weapAtk['Damage'][damageType] or 0 end,
  +
Tooltip.icon(damageType, 'DamageTypes')..' %.4f'
  +
}
 
end
 
end
   
  +
-- TODO: Do not rely on localized name to determine a weapon's variant. Decouple localization from data
--- Gets the total count of weapons as used on [[Mastery Rank]].
 
  +
--- Checks if a weapon is a variant or not.
-- @function p.getWeaponCount
 
  +
-- @function p._isVariant
-- @param {table} frame Frame object w/ the first argument being the weaponType,
 
  +
-- @param {string} weaponName Weapon name
-- 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
+
-- @returns {boolean} True if weapon is a variant, false otherwise
  +
-- @returns {string} Weapon's variant name or "Base" if weapon is not a variant
function p.getWeaponCount(frame)
 
  +
-- @returns {string} Weapon name, same as weaponName
local weaponType = frame.args ~= nil and frame.args[1] or frame
 
  +
function p._isVariant(weaponName)
local getFullList = frame.args ~= nil and frame.args[2]
 
  +
for i, var in pairs(VARIANT_LIST) do
local getAll = frame.args ~= nil and frame.args[3]
 
  +
if (var ~= "Dex" or weaponName ~= "Dex Pixia") then
local count = 0
 
  +
if string.find(weaponName, var) then
local fullList = ""
 
  +
return true, var, (string.gsub(weaponName, " ?"..var.." ?-?", ""))
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
 
end
 
end
 
end
  +
return false, "Base", weaponName
if (getFullList ~= nil) then return fullList end
 
return count
 
 
end
 
end
   
  +
--- Builds the full name of a weapon's variant. Does not check if it exists or not.
--- Gets the weapon class of secondary weapons.
 
-- @function getSecondaryCategory
+
-- @function p._buildName
-- @param {table} weapon Weapon table
+
-- @param {string} baseName Weapon's base name (e.g. "Braton")
  +
-- @param[opt] {string} variant Variant name (e.g. "Vandal"); if nil, returns base weapon name instead
-- @returns {string} Category name
 
  +
-- @returns {string} Weapon's variant name (e.g. "Braton Vandal")
local function getSecondaryCategory(weapon)
 
  +
function p._buildName(baseName, variant)
local class = getValue(weapon, "Class")
 
  +
if not variant or variant == 'Base' or variant == '' then
if (class == "Thrown") then
 
return "Thrown"
+
return baseName
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
 
end
return "Other"
+
return (({
  +
-- Prime Laser Rifle is an edge case for Prime naming scheme (at least in EN localization)
  +
Prime = baseName ~= 'Laser Rifle' and '%b %v',
  +
Vandal = '%b %v',
  +
Wraith = '%b %v',
  +
MK1 = '%v-%b',
  +
})[variant] or '%v %b'):gsub('%%v', variant):gsub('%%b', baseName)
 
end
 
end
   
  +
--- Returns a specific weapon table entry from <code>/data</code> or <code>/Conclave/data</code>.
--- Gets the weapon class of primary weapons.
 
-- @function getPrimaryCategory
+
-- @function p._getWeapon
-- @param {table} weapon Weapon table
+
-- @param {string} weaponName Weapon name
  +
-- @param[opt] {boolean} pvp If true, gets PvP stats of weapon instead, false otherwise; defaults to false
-- @returns {string} Category name
 
  +
-- @returns {table} Weapon table
local function getPrimaryCategory(weapon)
 
  +
function p._getWeapon(weaponName, pvp)
local class = getValue(weapon, "Class")
 
  +
weaponName = mw.text.decode(weaponName)
if (class == "Shotgun") then
 
  +
return (pvp and ConclaveData or WeaponData)[weaponName] or
return "Shotgun"
 
  +
error('p._getWeapon(weaponName, pvp): "'..weaponName..
elseif (class == "Bow") then
 
  +
'" does not exist in '..(pvp and '[[Module:Weapons/Conclave/data]]' or '[[Module:Weapons/data]]'))
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
 
end
   
  +
--- Gets the raw value of a certain statistic of a weapon.
--- Builds wikitable of all weapons' innate polarities as seen on [[Polarity]].
 
-- @function p.getPolarityTable
+
-- @function p._getValue
-- @param {table} frame Frame object
+
-- @param {table} Weapon Weapon table
-- @returns {string} Wikitext of resultant wikitable
+
-- @param {string} key Name of key
  +
-- @param[opt] {string} attack Name of attack to search through; defaults to 'Attack1' or what '_TooltipAttackDisplay' is set to
function p.getPolarityTable(frame)
 
  +
-- @returns {string, number} Value of statistic
local tableResult = { [[
 
  +
function p._getValue(weap, key, atk)--, formatted)
{| style="width: 100%; border-collapse: collapse;" cellpadding="2" border="1"
 
  +
-- return (formatted and statFormat or statRead)(weap, atk, key)
|+ '''Weapons with Innate Polarities (ignoring Stance and Exilus slots)'''
 
  +
return p._statRead(weap, atk, key)
! 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
 
end
   
  +
--- Gets the formatted value of a certain statistic of a weapon to be displayed
--- Builds comparison string between two values.
 
  +
-- the wiki.
-- @function p.buildCompareString
 
  +
-- @function p._getFormattedValue
-- @param {string, number} firstVal Value used for comparison
 
  +
-- @param {table} Weapon Weapon table
-- @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 {string} keyName Name of key
-- @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} attackName Name of attack to search through; defaults to 'Attack1'
-- @param[opt] {string} unit The values' unit (e.g. "m" or "seconds")
+
-- @returns {string} Value of statistic
  +
function p._getFormattedValue(weap, key, atk)
-- @param[opt] {table} compareAdjs Two element table that contains the greater than and less than comparative adjectives (e.g. { "Higher", "Lower" } )
 
  +
-- return p._getValue(Weapon, keyName, attackName, true)
-- @param[opt] {string} start What to start the comparison string by for if you want to increase the bullet level (e.g. "\n***")
 
  +
return p._statFormat(weap, atk, key)
-- @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
 
end
   
  +
--- Function that returns a simpler getter function, for multiple _stat*() calls on the same weapon/attack pair.
--- Builds damage comparison string between two attacks.
 
-- @function p.buildComparison
+
-- @function p._statReader
-- @param {table} Attack1 Attack used for comparison
+
-- @param {table} weap Weapon entry
-- @param {table} Attack2 Attack used to compare the first attack against
+
-- @param {number|table} atk Attacks table index or Attack entry
  +
-- @return {function} Getter function
-- @returns {string} Resultant wikitext of comparison string
 
local function buildDamageTypeComparisonString(Attack1, Attack2)
+
function p._statReader(weap, atk)
  +
return function(...) return p._statRead(weap, atk, ...) end
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
 
end
   
  +
--- Function that returns a simpler getter function, for multiple _stat*() calls on the same weapon/attack pair.
-- TODO: The above TODO can be done in conjunction with changing all the "Attack1" and "Attack2" keys
 
  +
-- @function p._statFormatter
-- to a singular "Attack" key with indexed table elements
 
  +
-- @param {table} weap Weapon entry
--- Builds comparison list between two gun weapons.
 
  +
-- @param {number|table} atk Attacks table index or Attack entry
-- @function p.buildComparison
 
  +
-- @return {function} Getter function
-- @param {table} Weapon1 Weapon used for comparison
 
  +
function p._statFormatter(weap, atk)
-- @param {table} Weapon2 Weapon used to compare the first weapon against
 
  +
return function(...) return p._statFormat(weap, atk, ...) end
-- @param {boolean} conclave If true, makes comparison list based on PvP stats, otherwise uses PvE stats; default false
 
  +
end
-- @returns {string} Resultant wikitext of comparison list
 
  +
local function buildGunComparisonString(Weapon1, Weapon2, Conclave)
 
  +
--- Returns a subset of <code>/data</code> or <code>/Conclave/data</code> based on a validation function.
local result = {}
 
  +
-- @function p._getWeapons
-- Adding this assignment to support method chaining w/ colon syntax
 
  +
-- @param {function} validateFunction Function that filters out a weapon by taking in a Weapon table argument
result.insert = function(self, elem) table.insert(self, elem) return self end
 
  +
-- @param[opt] {string} source Name of weapon entry to use
 
  +
-- @param[opt] {boolean} ignoreIgnore If true, ignores the _IgnoreEntry flag, false otherwise; defaults to false
local Att1 = getAttack(Weapon1, Weapon1['TooltipAttackDisplay'] or 'Attack1')
 
  +
-- @param[opt] {function} sortFunc Custom comparison function; false -> no sorting; defaults to sorting in ascending order by weapon name
local Att2 = getAttack(Weapon2, Weapon2['TooltipAttackDisplay'] or 'Attack1')
 
  +
-- @returns {table} Table of weapon table entries as seen in <code>/data</code>
if (Conclave) then
 
  +
function p._getWeapons(validateFunction, source, opts)
result:insert("* [["..Weapon1.Name.."]], compared to [[Conclave:"..Weapon2.Name.."|"..Weapon2.Name.."]]:")
 
  +
opts=opts or {}
else
 
  +
local ignoreIgnore, sortFunc, pvp = opts.ignoreIgnore, opts.sortFunc, opts.pvp
result:insert("* [["..Weapon1.Name.."]] (attack: "..(Att1.AttackName or "Normal").."), compared to [["..Weapon2.Name.."]] (attack: "..(Att2.AttackName or "Normal").."):")
 
  +
validateFunction = validateFunction or function() return true end
  +
local data = pvp and ConclaveData or WeaponData
  +
if source then
  +
data = data[source]
 
end
 
end
  +
 
local dmgString = ""
+
local weaps = {}
  +
for _, weap in pairs(data) do
dmgString = dmgString..buildCompareString(getValue(Weapon1, "BaseDamage"), getValue(Weapon2, "BaseDamage"), "base damage", 0.01)
 
  +
if (ignoreIgnore or not weap['_IgnoreEntry']) and validateFunction(weap) then
dmgString = dmgString..buildDamageTypeComparisonString(Att1, Att2)
 
  +
table.insert(weaps, weap)
if (string.len(dmgString) > 0 and getValue(Weapon1, "BaseDamage") == getValue(Weapon2, "BaseDamage")) then
 
  +
end
dmgString = "\n**Equal base damage, but different composition:"..dmgString
 
 
end
 
end
  +
if sortFunc ~= false then
dmgString = dmgString..buildCompareString(getValue(Weapon1, "TotalDamage"), getValue(Weapon2, "TotalDamage"), "total damage", 0.01)
 
  +
table.sort(weaps, sortFunc or function(a, b) return a.Name < b.Name end)
-- 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
 
end
  +
return weaps
 
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
 
end
   
  +
--- Returns all melee weapons. If weapType is not nil, only grab for a specific type
--- Builds comparison list between two melee or arch-melee weapons.
 
  +
-- For example, if weapType is "Nikana", only pull Nikanas.
-- @function p.buildComparison
 
  +
-- @function p._getMeleeWeapons
-- @param {table} Weapon1 Weapon used for comparison
 
  +
-- @param[opt] {boolean} weapType
-- @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
+
-- @param[opt] {boolean} pvp If true, only gets melee weapons available in Conclave, false otherwise; defaults to false
-- @returns {string} Resultant wikitext of comparison list
+
-- @returns {table} An array of melee weapon table entries as seen in <code>/data</code>
  +
function p._getMeleeWeapons(weapType,pvp)
local function buildMeleeComparisonString(Weapon1, Weapon2, Conclave)
 
  +
return p._getWeapons(weapType and function(weap) return weap.Class==weapType end, 'melee',{['pvp']=pvp==true})
local result = {}
 
  +
end
-- 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')
 
   
  +
--- Main frame invokable function to access any raw/computed attribute/column/key of a weapon entry.
if (Conclave) then
 
  +
-- See default table in M:Weapons to see all valid computed attributes.
result:insert("* "..Weapon1.Name..", compared to [[Conclave:"..Weapon2.Name.."|"..Weapon2.Name.."]]:")
 
  +
-- @function p.getValue
else
 
  +
-- @param {string} weap Weapon name in EN locale
result:insert("* "..Weapon1.Name.." (attack: "..(Att1.AttackName or "Normal").."), compared to [["..Weapon2.Name.."]] (attack: "..(Att2.AttackName or "Normal").."):")
 
  +
-- @param {number} atk Attacks table index
end
 
  +
-- @param {string} k Key name
 
  +
-- @return Raw or computed value associated with k key
local dmgString = ""
 
  +
function p.getValue(frame)
dmgString = dmgString..buildCompareString(getValue(Weapon1, "BaseDamage"), getValue(Weapon2, "BaseDamage"), "base damage", 0.01)
 
  +
-- table.unpack doesn't work on the frame object which is why this is anonymous function is needed
dmgString = dmgString..buildDamageTypeComparisonString(Att1, Att2)
 
  +
local weap, key, atk = (function(t) return t[1], t[2], t[3] end)(frame.args)
if (string.len(dmgString) > 0 and getValue(Weapon1, "BaseDamage") == getValue(Weapon2, "BaseDamage")) then
 
  +
weap = p._getWeapon(weap)
dmgString = "\n**Equal base damage, but different composition:"..dmgString
 
  +
return p._getValue(weap, key, atk)
end
 
  +
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
 
   
  +
--- Main frame invokable function to access any formatted attribute/column/key of a weapon entry.
result:insert(
 
  +
-- See default table in M:Weapons to see all valid computed attributes.
buildCompareString(Att1.FireRate, Att2.FireRate, "[[attack speed]]", 0.01)
 
  +
-- @function p.getFormattedValue
):insert(
 
  +
-- @param {string} weap Weapon name in EN locale
buildCompareString(getValue(Weapon1, "Polarities"), getValue(Weapon2, "Polarities"), "[[Polarity|polarities]]", nil, nil, {"Different", "Different"})
 
  +
-- @param {number} atk Attacks table index
):insert(
 
  +
-- @param {string} k Key name
buildCompareString(Weapon1.Mastery, Weapon2.Mastery, "[[Mastery Rank]] required", 1)
 
  +
-- @return Formatted value associated with k key
)
 
  +
function p.getFormattedValue(frame)
 
  +
local weap, key, atk = (function(t) return t[1], t[2], t[3] end)(frame.args)
-- Quick fix to passing in an empty string for polarity when comparing with an arch-melee weapon
 
  +
weap = p._getWeapon(weap)
if (Weapon1.Type ~= "Arch-Melee" and Weapon2.Type ~= "Arch-Melee") then
 
  +
return p._getFormattedValue(weap, key, atk)
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
 
end
   
--- Builds comparison list between two weapons in PvE.
+
--- Builds a melee weapon gallery as seen on [[Template:MeleeCategory]].
-- @function p.buildComparison
+
-- @function p.getMeleeWeaponGallery
-- @param {table} frame Frame object
+
-- @param {table} frame Frame object w/ first argumenting being string meleeClass
-- @returns {string} Resultant wikitext of comparison list
+
-- @returns {string} Resultant wikitext of gallery
function p.buildComparison(frame)
+
function p.getMeleeWeaponGallery(frame)
local weaponName1 = frame.args[1]
+
local meleeClass = frame.args[1] or ''
  +
local result = { "=="..meleeClass.." Weapons==", '<gallery widths="200" position="center" spacing="small">' }
local weaponName2 = frame.args[2]
 
  +
for i, weap in ipairs(p._getMeleeWeapons(meleeClass)) do
 
  +
table.insert(result, p._statRead(weap, nil, 'Image')..'|'..p._statFormat(weap, nil, 'Name'))
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
 
end
  +
table.insert(result, '</gallery>')
return comparisonString..'[[Category:Automatic Comparison]]'
 
  +
return frame:preprocess(table.concat(result, '\n')) -- annoying that it needs to be preprocessed
 
end
 
end
   
--- Builds comparison list between two weapons in PvP ([[Conclave]]).
+
--- Gets the total count of weapons as used on [[Mastery Rank#Total Mastery]].
-- @function p.buildComparison
+
-- @function p.getWeaponCount
-- @param {table} frame Frame object
+
-- @param {table} frame Frame object w/ the first argument being the weaponSlot and the
  +
-- second argument being a boolean to getFullList
-- @returns {string} Resultant wikitext of comparison list
 
  +
-- @returns {number} Total count of weapons in a certain category/type
function p.buildConclaveComparison(frame)
 
  +
-- @returns {table} List of weapon names that count for mastery in a particular weapon slot
local weaponName1 = frame.args[1]
 
  +
function p._getWeaponCount(slot)
local weaponName2 = frame.args[2]
 
  +
slot = slot and slot:lower()
 
  +
local data = slot and WeaponData[slot] or WeaponData
assert(weaponName1 ~= '' and weaponName2 ~= '', 'p.buildConclaveComparison(frame): Must compare two weapons')
 
  +
local fullList = {}
 
local Weapon1 = p._getConclaveWeapon(weaponName1)
 
local Weapon2 = p._getConclaveWeapon(weaponName2)
 
 
 
  +
for name, weapon in pairs(data) do
local comparisonString = ''
 
  +
if not weapon._IgnoreInMasteryCount then
if (Weapon1.Type == 'Melee' or Weapon1.Type == 'Arch-Melee') then
 
  +
-- TODO: There should be a better way to determine/differentiate if a weapon is a kitgun b/c kitguns and zaws
comparisonString = buildMeleeComparisonString(Weapon1, Weapon2, true)
 
  +
-- are stored in the same M:Weapons/data/modular data store; add a new "Kitgun" or "Zaw" Trait and target that?
else
 
  +
if (slot == 'kitgun' and weapon.Slot == 'Secondary')
comparisonString = buildGunComparisonString(Weapon1, Weapon2, true)
 
  +
or (slot == 'zaw' and weapon.Slot == 'Melee')
  +
or (slot == 'robotic' and weapon.Slot ~= 'Hound')
  +
or (weapon.Slot:lower() == slot)
  +
or slot == nil then
  +
fullList[#fullList + 1] = name
  +
end
  +
end
 
end
 
end
  +
return comparisonString..'[[Category:Automatic Comparison]]'
 
  +
return #fullList, fullList
 
end
 
end
   
  +
--- Gets the total count of weapons as used on [[Mastery Rank#Total Mastery]].
--- Adds weapon categories.
 
-- @function p.buildAutoboxCategories
+
-- @function p.getWeaponCount
  +
-- @param {table} frame Frame object w/ the first argument being the weapon slot
  +
-- @return {number} Total number of weapons that can reward Mastery XP
  +
function p.getWeaponCount(frame)
  +
return (p._getWeaponCount(frame.args and frame.args[1] or nil))
  +
end
  +
  +
--- Builds wikitable of all weapons' innate polarities as seen on [[Polarity]].
  +
-- @function p.getPolarityTable
 
-- @param {table} frame Frame object
 
-- @param {table} frame Frame object
-- @returns {string} Wikitext of category links
+
-- @returns {string} Wikitext of resultant wikitable
function p.buildAutoboxCategories(frame)
+
function p.getPolarityTable(frame)
  +
local colNames = { 'Primary', 'Secondary', 'Melee', 'Archgun', 'Archmelee' }
local WeapName = frame.args ~= nil and frame.args[1] or frame
 
  +
local cols = {} -- Will look like: {['Primary']={},['Secondary']={},['Melee']={},['Archgun']={},['Archmelee']={},}
local Weapon = p._getWeapon(WeapName)
 
  +
local colOrder = {} --{cols['Primary'],cols['Secondary'],cols['Melee'],cols['Archgun'],cols['Archmelee'],}
local result = { "[[Category:Automatic Weapon Box]][[Category:Weapons]]" }
 
  +
local colCounts = {}
if (Weapon == nil or (Weapon.IgnoreCategories ~= nil and Weapon.IgnoreCategories)) then
 
  +
return ""
 
  +
for i, v in ipairs(colNames) do
  +
cols[v] = {}
  +
colOrder[i] = cols[v]
  +
colCounts[v] = 0
 
end
 
end
  +
table.insert(result, categoryMap[Weapon['Class']]..categoryMap[Weapon['Trigger']]..categoryMap[Weapon['Type']])
 
  +
for _, weapon in pairs(WeaponData) do
 
  +
local pols = Table.size(weapon["Polarities"] or {})
-- TODO: Archive this category b/c not sure how important it is or how often
 
  +
local slot = weapon['Slot']
-- it is used; augments are moved out of M:Weapons/data and should use M:Mods/data
 
  +
if pols > 0 and cols[slot] then
local augments = getAugments(Weapon)
 
  +
table.insert(cols[slot], {
if (Table.size(augments) > 0) then
 
  +
'|'..p._getFormattedValue(weapon, 'NameLink'):gsub(' ?%(.*%)', '')..'||'..p._getFormattedValue(weapon, "Polarities"),
table.insert(result, "[[Category:Augmented Weapons]]")
 
  +
pols
  +
})
  +
colCounts[slot] = colCounts[slot] + 1
  +
end
 
end
 
end
  +
 
  +
for i, v in ipairs(colNames) do
-- Adding appropriate categories to page based on weapon's categorical traits
 
  +
colCounts[i] = colCounts[v]
for _, trait in pairs(Weapon['Traits'] or {}) do
 
table.insert(result, categoryMap[trait])
+
table.sort(cols[v], function(a, b)return a[2] > b[2] end)
 
end
 
end
  +
 
  +
local result = {[=[
local bestPercent, bestElement = getDamageBias(Weapon[Weapon['TooltipAttackDisplay'] or 'Attack1'])
 
  +
{| style="width: 100%; border-collapse: collapse;" cellpadding="2" border="1"
if (bestElement == "Impact" or bestElement == "Puncture" or bestElement == "Slash") then
 
  +
|+ '''Weapons with Innate Polarities (ignoring Stance and Exilus slots)'''
if (bestPercent > .38) then
 
  +
! colspan="2" |Primaries
table.insert(result, "[[Category:"..bestElement.." Damage Weapons]]")
 
  +
! colspan="2" |Secondaries
else
 
  +
! colspan="2" |Melees
table.insert(result, "[[Category:Balanced Physical Damage Weapons]]")
 
  +
! colspan="2" |Archguns
  +
! colspan="2" |Archmelees]=]}
  +
for i = 1, math.max(table.unpack(colCounts)) do --row
  +
table.insert(result, '|-')
  +
for _, col in ipairs(colOrder) do --cell
  +
table.insert(result,(col[i] or {'| ||'})[1])
 
end
 
end
 
end
 
end
  +
table.insert(result, '|}')
 
  +
return table.concat(result, '\n')
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
 
end
   
 
--- Builds a table that lists out all weapons with a certain damage type
 
--- 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
 
-- @function p.buildDamageTypeTable
 
-- @param {table} frame Frame object
 
-- @param {table} frame Frame object
 
-- @returns {string} Wikitext of resultant wikitable
 
-- @returns {string} Wikitext of resultant wikitable
 
function p.buildDamageTypeTable(frame)
 
function p.buildDamageTypeTable(frame)
local damageType = frame.args ~= nil and frame.args[1] or frame
+
local damageType = frame.args and frame.args[1] or frame
  +
local mostly = frame.args and (frame.args[2] or '') ~= ''
local Weapons = {}
 
  +
local WeapArray = p._getWeapons(function(weaponEntry)
 
  +
local content = {}
-- Want to ignore Kitgun entries which have 0 as placeholder damage values
 
  +
for k,weap in pairs(WeaponData) do
if (weaponEntry['Type'] == 'Kitgun' or weaponEntry['Type'] == 'Primary Kitgun') then
 
  +
local weapAtk = getWeaponAttack(weap)--could add a loop here
return false
 
  +
local portion, biastype, damage = statRead(weapAtk, 'DamageBias')
  +
local typeDmg = statRead(weapAtk, damageType)
  +
if damage == 0 then typeDmg = weapAtk[damageType] and 1 or 0 end--modular pieces
  +
--Filter for
  +
--a. any of the damage type in any attack - former 'not mostly'
  +
--b. at least one majority damage type - former 'mostly'
  +
--c. a majority of the damage type in the display attack - 'mostly'
  +
--d. any of the damage type in the display attack - 'not mostly'
  +
if biastype == damageType or not mostly and typeDmg > 0 then
  +
table.insert(content, ('| %s || %s || %s || %s || %s || data-sort-value="%s" | %s'):format(
  +
statFormat(weapAtk, 'Name'),
  +
statRead(weapAtk, 'Slot'),
  +
statRead(weapAtk, 'Class'),
  +
statRead(weapAtk, 'AttackName'),
  +
typeDmg,
  +
portion, statFormat(weapAtk, 'DamageBias')
  +
))
 
end
 
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
 
end
  +
table.sort(content)--will sort by tooltip span key
 
  +
local procString = makeDTooltip(damageType, true)
 
  +
return ([[
local procShort = makeDTooltip(damageType)
 
local result = ''
 
local tHeader = string.format([[
 
 
{| class = "listtable sortable" style="margin:auto;"
 
{| class = "listtable sortable" style="margin:auto;"
|+ '''Weapons with %s damage'''
+
|+ '''Weapons with %s%s damage'''
 
|-
 
|-
! Name !! Type !! Class !! %s !! %s%%
+
! Name !! Slot !! Class !! Attack Name !! data-sort-type="number" | %s !! data-sort-type="number" | Majority
]], damageType, procString, procShort)
 
 
local tRows = {}
 
for i, Weapon in pairs(WeapArray) do
 
local thisRow = [[
 
 
|-
 
|-
  +
]]):format(mostly and 'mostly ' or '', damageType, Tooltip.full(damageType, 'DamageTypes'))
| {{Weapon|%s}} || %s || %s || %s || data-sort-value="%s" | %s]]
 
  +
..table.concat(content, '\n|-\n')..'\n|}'
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
 
end
   
  +
--- _isVariant adapter for p._shortLinkList
--- Builds a list of weapons' mastery requirements as seen on [[Template:EquipmentUnlock]].
 
-- @function p.getMasteryShortList
+
local function variantOf(weap)
  +
local full, _, var, base = weap.Name, p._isVariant(weap.Name)
-- @param {table} frame Frame object w/ first argument being a string weaponType
 
  +
return var, base, full
  +
end
  +
  +
--- Builds a list of weapons, with variants being next to base weapon name inside parentheses
  +
-- (e.g. {{Weapon|Braton}} ({{Weapon|MK1-Braton|MK1}}, {{Weapon|Braton Prime|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)
  +
return StatObject.shortLinkList(Weapons, variantOf, tooltip and 'Weapons')
  +
end
  +
  +
--- Builds a list of weapons' mastery requirements as seen on [[Template:EquipmentUnlock]],
  +
-- [[Template:EquipmentUnlock/Primary]], [[Template:EquipmentUnlock/Secondary]],
  +
-- [[Template:EquipmentUnlock/Melee]], etc.
  +
-- @function p.getMasteryShortList
  +
-- @param {table} frame Frame object w/ first argument being a string weaponSlot
 
-- @returns {string} Wikitext of resultant list
 
-- @returns {string} Wikitext of resultant list
 
function p.getMasteryShortList(frame)
 
function p.getMasteryShortList(frame)
local weaponType = frame.args[1]
+
local weaponSlot = frame.args[1]
 
local masteryRank = tonumber(frame.args[2])
 
local masteryRank = tonumber(frame.args[2])
  +
local weapArray = p._getWeapons(function(x)
local checkTypeAndMastery = function(x) return x.Type == weaponType and x.Mastery == masteryRank end
 
  +
return x.Slot == weaponSlot and x.Mastery == masteryRank
local weapArray = p._getWeapons(checkTypeAndMastery)
 
  +
end)
 
  +
return table.concat(StatObject.shortLinkList(weapArray, variantOf, 'Weapons'), ' • ')
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
 
end
   
  +
function p.fullList()
--- Builds a disposition wikitable as seen on [[Riven Mods/Weapon Dispos]].
 
  +
return table.concat(StatObject.shortLinkList(WeaponData, variantOf, 'Weapons'), ' • ')
-- @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
 
end
   
--- Builds a list of PvP weapons.
+
--- Builds a list of PvP weapons as seen on [[PvP#Limitations]].
-- @function p.getConclaveList
+
-- @function p.getConclaveList
-- @param {table} frame Frame object w/ first argument being a string weaponType
+
-- @param {table} frame Frame object w/ first argument being a string weaponSlot
 
-- @returns {string} Wikitext of resultant list
 
-- @returns {string} Wikitext of resultant list
 
function p.getConclaveList(frame)
 
function p.getConclaveList(frame)
local weaponType = frame.args[1]
+
local weaponSlot = frame.args[1] or 'All'
local weapArray = p._getWeapons(function(x) return x.Type == weaponType and x.Conclave end)
+
local weapArray = p._getWeapons(function(weap)
  +
return weap.Conclave == true
 
  +
end, weaponSlot, {pvp=true})
local result = ''
 
  +
return '*'..table.concat(StatObject.shortLinkList(weapArray, variantOf), '\n* ')
local shortList = p._shortLinkList(weapArray, false)
 
for i, pair in Table.skpairs(shortList) do
 
result = result..'\n* '..pair
 
end
 
return result
 
 
end
 
end
   
  +
function p.getListWithWarframes(frame)
--- Builds a row forcomparison table as seen on [[Weapon Comparison]].
 
  +
local date_str = ''
-- @function buildCompRow
 
  +
local list = {'{| class="listtable sortable" style="overflow-y:scroll; max-height:500px"', '|-', '!data-sort-type="date"| Release !! Weapon !! Warframes'}
-- @param {table} tableHeaders Wikitable's header names an the specific getter function that it will pull from
 
  +
local frames = {}
-- (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
+
for _, warframe in pairs(WarframeData.Warframes) do
  +
if warframe.Introduced then
valueName = headerLine[1]
 
  +
date_str = Version._getVersionDate(warframe.Introduced)
attackName = headerLine[3] ~= nil and headerLine[3] or nil
 
  +
if frames[date_str] then
 
  +
table.insert(frames[date_str], '{{WF|' .. warframe.Name .. '}}')
local value = getValue(Weapon, valueName, attackName)
 
  +
else
if (type(value) == 'number') then
 
  +
frames[date_str] = {'{{WF|' .. warframe.Name .. '}}'}
value = Math.round(value, 0.01)
 
  +
end
 
end
 
end
table.insert(result, 'style="'..styleString..'"|'..(value or 'N/A'))
 
 
end
 
end
return '|-\n|'..table.concat(result, '||')
 
end
 
   
  +
for _, weapon in pairs(WeaponData) do
--- Builds comparison table as seen on [[Weapon Comparison]].
 
  +
date_str = Version._getVersionDate(weapon.Introduced)
-- @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
 
   
  +
warframe = frames[date_str] or {'N/A'}
for _, Weap in pairs(Weapons) do
 
  +
table.sort(warframe)
local rowStr = buildCompRow(tableHeaders, Weap)
 
table.insert(result, rowStr)
+
warframe = table.concat(warframe, ', ')
  +
end
 
  +
date_str = date_str:sub(6, 7) .. '.' .. date_str:sub(9, 10) .. '.' .. date_str:sub(0, 4)
table.insert(result, '|}[[Category:Automatic Comparison Table]]')
 
return table.concat(result, '\n')
 
end
 
   
  +
table.insert(list, '|-')
--- Builds comparison table of gun stats as seen on [[Weapon Comparison]].
 
  +
table.insert(list, '|data-sort-value=' .. date_str .. '|{{ver|' .. weapon.Introduced .. '}} ||{{Weapon|' .. weapon.Name .. '}}||' .. warframe)
-- @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
 
end
  +
table.insert(list, '|}')
   
  +
return frame:preprocess(table.concat(list, '\n'))
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
 
end
   
--- Builds comparison table of gun Conclave stats as seen on [[Weapon Comparison/Conclave]].
+
--- Builds a disposition wikitable as seen on [[Riven Mods/Weapon Dispos]].
-- @function p.getCompTableConclaveGuns
+
-- @function p.getRivenDispositionTable
-- @param {table} frame Frame object
+
-- @param {table} frame Frame object w/ first argument being a string weaponSlot
 
-- @returns {string} Wikitext of resultant wikitable
 
-- @returns {string} Wikitext of resultant wikitable
function p.getCompTableConclaveGuns(frame)
+
function p.getRivenDispositionTable(frame)
local Catt = frame.args ~= nil and frame.args[1]
+
local weaponSlot = frame.args[1]
  +
local result = {
local Type = frame.args ~= nil and frame.args[2] or nil
 
  +
'{| class="article-table" border="0" cellpadding="1" cellspacing="1" style="width: 100%"',
if (Type == "All") then Type = nil end
 
  +
'|-',
local WeapArray = {}
 
  +
{'[[a| '}, -- Wikitable header row
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
 
   
  +
-- local ranges = {'○○○○○', '●○○○○', '●●○○○', '●●●○○', '●●●●○', '●●●●●'}
--- Builds comparison table of melee stats as seen on [[Weapon Comparison]].
 
  +
local dispo = {}
-- @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
 
   
  +
for k, weapon in pairs(WeaponData) do
--- Builds comparison table of melee conclave stats as seen on [[Weapon Comparison/Conclave]].
 
  +
if weapon['Disposition'] and (weaponSlot == 'All' or weapon['Slot'] == weaponSlot) then
-- @function p.getCompTableConclaveMelees
 
  +
local disp = p._statFormat(weapon, nil, 'Dispo')
-- @param {table} frame Frame object
 
  +
dispo[disp] = dispo[disp] or {}
-- @returns {string} Wikitext of resultant wikitable
 
  +
table.insert(dispo[disp], weapon)
function p.getCompTableConclaveMelees(frame)
 
  +
end
local Type = frame.args ~= nil and frame.args[1] or nil
 
  +
end
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
 
   
  +
for str, dis in Table.skpairs(dispo) do
--- Builds comparison table of arch-melee stats as seen on [[Weapon Comparison]].
 
  +
table.sort(dis, function(a, b) return a['Disposition'] > b['Disposition'] end)
-- @function p.getCompTableArchMelees
 
  +
local col = { '| style="vertical-align:top; font-size:small" |' }
-- @param {table} frame Frame object
 
  +
for _, weap in ipairs(dis) do
-- @returns {string} Wikitext of resultant wikitable
 
  +
table.insert(col, p._statFormat(weap, nil, 'NameLink')..' ('..weap['Disposition']..')')
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
  +
table.insert(result[3], str)
  +
table.insert(result, table.concat(col, '\n* '))
 
end
 
end
   
  +
result[3] = table.concat(result[3], ']]\n! scope="col" style="text-align:center;"|[[Riven Mods#Disposition|')..']]'
local tableHeaders = { { "NameLink", "Name" } }
 
table.insert(tableHeaders,{ "Class", "Class" })
+
table.insert(result, '|}')
  +
return table.concat(result, '\n')
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
 
end
   

Latest revision as of 23:33, 17 April 2024


Weapons contains all of WARFRAME's 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._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"); if nil, returns base weapon name instead (string; optional)
Returns: Weapon's variant name (e.g. "Braton Vandal") (string)
weapons._getWeapon(weaponName, pvp) (function)
Returns a specific weapon table entry from /data or /Conclave/data.
Parameters:
  • weaponName Weapon name (string)
  • pvp If true, gets PvP stats of weapon instead, false otherwise; defaults to false (boolean; optional)
Returns: Weapon table (table)
weapons._getValue(Weapon, key, attack) (function)
Gets the raw value of a certain statistic of a weapon.
Parameters:
  • Weapon Weapon table (table)
  • key Name of key (string)
  • attack Name of attack to search through; defaults to 'Attack1' or what '_TooltipAttackDisplay' is set to (string; optional)
Returns: Value of statistic (string, number)
weapons._getFormattedValue(Weapon, keyName, attackName) (function)
Gets the formatted value of a certain statistic of a weapon to be displayed the wiki.
Parameters:
  • Weapon Weapon table (table)
  • keyName Name of key (string)
  • attackName Name of attack to search through; defaults to 'Attack1' (string; optional)
Returns: Value of statistic (string)
weapons._statReader(weap, atk) (function)
Function that returns a simpler getter function, for multiple _stat*() calls on the same weapon/attack pair.
Parameters:
  • weap Weapon entry (table)
  • atk Attacks table index or Attack entry (number|table)
Returns: Getter function (function)
weapons._statFormatter(weap, atk) (function)
Function that returns a simpler getter function, for multiple _stat*() calls on the same weapon/attack pair.
Parameters:
  • weap Weapon entry (table)
  • atk Attacks table index or Attack entry (number|table)
Returns: Getter function (function)
weapons._getWeapons(validateFunction, source, ignoreIgnore, sortFunc) (function)
Returns a subset of /data or /Conclave/data based on a validation function.
Parameters:
  • validateFunction Function that filters out a weapon by taking in a Weapon table argument (function)
  • source Name of weapon entry to use (string; optional)
  • ignoreIgnore If true, ignores the _IgnoreEntry flag, false otherwise; defaults to false (boolean; optional)
  • sortFunc Custom comparison function; false -> no sorting; defaults to sorting in ascending order by weapon name (function; optional)
Returns: Table of weapon table entries as seen in /data (table)
weapons._getMeleeWeapons(weapType, pvp) (function)
Returns all melee weapons. If weapType is not nil, only grab for a specific type For example, if weapType is "Nikana", only pull Nikanas.
Parameters:
  • weapType (boolean; optional)
  • pvp If true, only gets melee weapons available in Conclave, false otherwise; defaults to false (boolean; optional)
Returns: An array of melee weapon table entries as seen in /data (table)
weapons.getValue(weap, atk, k) (function)
Main frame invokable function to access any raw/computed attribute/column/key of a weapon entry. See default table in M:Weapons to see all valid computed attributes.
Parameters:
  • weap Weapon name in EN locale (string)
  • atk Attacks table index (number)
  • k Key name (string)
Returns: Raw or computed value associated with k key
weapons.getFormattedValue(weap, atk, k) (function)
Main frame invokable function to access any formatted attribute/column/key of a weapon entry. See default table in M:Weapons to see all valid computed attributes.
Parameters:
  • weap Weapon name in EN locale (string)
  • atk Attacks table index (number)
  • k Key name (string)
Returns: Formatted value associated with k key
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#Total Mastery.
Parameter: frame Frame object w/ the first argument being the weaponSlot and the second argument being a boolean to getFullList (table)
Returns:
  • Total count of weapons in a certain category/type (number)
  • List of weapon names that count for mastery in a particular weapon slot (table)
weapons.getWeaponCount(frame) (function)
Gets the total count of weapons as used on Mastery Rank#Total Mastery.
Parameter: frame Frame object w/ the first argument being the weapon slot (table)
Returns: Total number of weapons that can reward Mastery XP (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.buildDamageTypeTable(frame) (function)
Builds a table that lists out all weapons with a certain damage type
Parameter: frame Frame object (table)
Returns: Wikitext of resultant wikitable (string)
weapons._shortLinkList(Weapon, tooltip) (function)
Builds a list of weapons, with variants being next to base weapon name inside parentheses (e. g. Braton Braton (Braton MK1, BratonPrime 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.getMasteryShortList(frame) (function)
Builds a list of weapons' mastery requirements as seen on Template:EquipmentUnlock, Template:EquipmentUnlock/Primary, Template:EquipmentUnlock/Secondary, Template:EquipmentUnlock/Melee, etc.
Parameter: frame Frame object w/ first argument being a string weaponSlot (table)
Returns: Wikitext of resultant list (string)
weapons.getConclaveList(frame) (function)
Builds a list of PvP weapons as seen on PvP#Limitations.
Parameter: frame Frame object w/ first argument being a string weaponSlot (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 weaponSlot (table)
Returns: Wikitext of resultant wikitable (string)

Created with Docbunto

See Also

Code


---	'''Weapons''' contains all of [[WARFRAME]]'s [[Weapons|weapon]] data.<br />
--	
--	@module			weapons
--	@alias			p
--	@attribution	[[User:Cephalon Scientia|Cephalon Scientia]]
--	@attribution	[[User:FINNER|FINNER]]
--	@attribution	[[User:Falterfire|Falterfire]]
--	@attribution	[[User:Gigamicro|Gigamicro]]
--	@attribution	[[User:Flaicher|Flaicher]]
--	@attribution	[[User:Synthtech|Synthtech]]
--	@image		IconPrimaryWeaponRifle.png
--	@require	[[Module:StatObject]]
--	@require	[[Module:DamageTypes]]
--	@require	[[Module:Polarity]]
--	@require	[[Module:Math]]
--	@require	[[Module:Table]]
--	@require	[[Module:Tooltips]]
--	@require	[[Module:Version]]
--	@require	[[Module:Stances/data]]
--	@require	[[Module:Weapons/data]]
--	@require	[[Module:Weapons/Conclave/data]]
--	@release	stable
--	<nowiki>

-- TODO: Add LuaDoc style comments to new functions
local p = {}

local Delay = require([[Module:Delay]])
local WeaponData = Delay.require([[Module:Weapons/data]])
local WarframeData = Delay.require([[Module:Warframes/data]]) -- for use in p.getListWithWarframes
local ConclaveData = Delay.require([[Module:Weapons/Conclave/data]])
local Tooltip = Delay.require([[Module:Tooltips]]) -- full, icon
local Version = Delay.require([[Module:Version]]) -- _getVersion, _getVersionDate
local Polarity = Delay.require([[Module:Polarity]]) -- _pols, _polarity
local Math = Delay.require([[Module:Math]]) -- formatnum
local Table = Delay.require([[Module:Table]]) -- size, skpairs
local iterationOrderArray = require([[Module:DamageTypes]]).iterationOrderArray

-- TODO: Should decouple from localized names for internationalization
local VARIANT_LIST = {
	"Prime", "Prisma", "Wraith", "Vandal", "Vaykor", "Synoid", "Telos", "Secura",
	"Sancti", "Rakta", "Mara", "Carmine", "Ceti", "Dex", "MK1", "Kuva", "Tenet"
}

table.unpack = table.unpack or unpack

local StatObject = require [[Module:StatObject]]
p.__StatObject = StatObject
local statRead = StatObject.statRead
local statFormat = StatObject.statFormat

local indexes = StatObject.meta.indexes
local ors = StatObject.meta.ors
local unpacks = StatObject.meta.unpacks

local passes = StatObject.meta.passes
local percent = StatObject.meta.percent
local percents = StatObject.meta.percents

---	Gets the attack entry from weapon entry.
--	@function		p._getAttack
--	@param			{table} weap Weapon entry
--	@param[opt]		{number|table} atk Attacks table index or Attack entry
--	@return			{table} A single weapon+attack struct
local function getWeaponAttack(weap, atk)
	if type(atk) == 'number' then return StatObject.getStruct2(weap,weap.Attacks[atk]) end
	if weap.AttackName then return weap end
	if type(atk) == 'table' then return StatObject.getStruct2(weap,atk) end
	local key = atk or weap['_TooltipAttackDisplay'] or 1
	if weap.Attacks == nil then
		error('p._getWeaponAttack(weap, atk): Attacks table is nil in '..mw.dumpObject(weap))
	end
	return StatObject.getStruct2(weap,weap.Attacks[key])
end
p._getAttack = getWeaponAttack
p._getWeaponAttack = getWeaponAttack

function p._statRead(w, a, ...)
	return statRead(getWeaponAttack(w, a), ...)
end
function p._statFormat(w, a, ...)
	return statFormat(getWeaponAttack(w, a), ...)
end
function p.stat(frame)
	return p._statFormat(p._getWeapon(frame.args[1] or 'Skana Prime'), nil, frame.args[2] or 'Name')
end

-- Wrapper function for use in StatObject
local function dmgTooltip(damageType)
	return Tooltip.full(damageType, 'DamageTypes')
end

-- Defining getters/attributes whose names match the associated database key or some custom derived attribute.
-- Index key will be name of getter function and can be mapped to a single value (getter definition) 
-- or a table with two values (getter and format function definitions)

-- Cheatsheet on adding new keys:
-- StatName = default value -> Get raw value with the same StatName from M:Weapons/data and with no additional formatting (aka default formatting)
-- StatName = function(self) return self.StatName + 1 end -> Define custom getter function and use default formatting
-- StatName = { default value, '%.2f' } -> Get raw value value with same StatName from M:Weapons/data and use format string for formatting
-- StatName = { function(self) return ... end, '%.2f' } -> Define custom getter function and use format string for formatting
-- StatName = { function(self) return ... end, function(self, returnValue1, returnValue2, ...) return tostring(returnValue) end } - > Define custom getter and format functions
-- (Note that format function will pass in return value(s) from getter as well as object self)

-- TODO: Put StatObject keys in alphabetical order for navigation
StatObject.default = {
AttackName = 'Normal Attack',
AmmoCost = nil,
AmmoPickup = function(weapAtk)
	return weapAtk['AmmoPickup'] or
		weapAtk['Slot'] == 'Primary' and 80 or 
		weapAtk['Slot'] == 'Secondary' and 40 or 
		weapAtk['Slot'] == 'Archgun (Atmosphere)' and 1000 or
		0
end,
DamageBias = {
	function(weapAtk)
		if not weapAtk.Damage then
			error('DamageBias: no Attack.Damage')
			return 0, 0, 0
		end
		local total, bestdmg, bestdt = 0, 0, nil
		for dt, dmg in pairs(weapAtk.Damage) do
			local dmg = dmg
			if dmg >= bestdmg then
				bestdmg, bestdt = dmg, dt
			end
			total = total + dmg
		end
		return StatObject.ucacheIn(weapAtk, 'DamageBias', { bestdmg / total, bestdt, total })
	end,
	{ percent, passes(dmgTooltip), '' }
},
BiasPortion = { indexes('DamageBias', 1), percent },
BiasType = { indexes('DamageBias', 2), function(self, biasType) return Tooltip.icon(biasType, 'DamageTypes') end },
BaseDamage = { indexes('DamageBias', 3), '%.2f' },
-- More precise damage values to 4 decimal places for PvP since PvP damage is calculated
-- based on a floating-point scalar. Damage quantization is more relevant in PvP so more
-- precise numbers needed.
PvPBaseDamage = { indexes('DamageBias', 3), '%.4f' },
TotalDamage = { function(weapAtk)
	return statRead(weapAtk, 'BaseDamage') * statRead(weapAtk, 'Multishot')
end, passes(Math.formatnum)
},
-- Including max +60% Progenitor bonus for Kuva/Tenet weapons
TotalDamageWithProgenBonus = { function(weapAtk)
	return statRead(weapAtk, 'TotalDamage') * (statRead(weapAtk, 'IsLichWeapon') and 1.6 or 1)
end, passes(Math.formatnum)
},
ChargeTime = { 0, '%.1f s' },
ExplosionDelay = { 0, '%.1f s' },
ExtraHeadshotDmg = { 0, percents('+%.2f%%') },
Falloff = {
	function(weapAtk)
		local fo = weapAtk['Falloff'] or {}
		return fo.StartRange or 0, fo.EndRange or math.huge, 1 - (fo.Reduction or 1)
	end,
	{ '%.1f m (100%%) -', '%.1f m', percents('(%.2f%%)') }
},
FalloffStart = { indexes('Falloff', 1), '%.1f m' },
FalloffEnd = { indexes('Falloff', 2), '%.1f m' },
-- Damage reduction from falloff instead of damage multiplier
FalloffReduction = { function(weapAtk)
	local _, _, falloff = statRead(weapAtk, 'Falloff')
	return 1 - falloff
end, percent
},
FalloffRate = { function(weapAtk)
	local startdist,enddist,endpercent = statRead(weapAtk, 'Falloff')
	return -(enddist-startdist)/(endpercent-1)
end, '%.1fm/%%'
},
HeadshotMultiplier = { 1, '%.1fx' },
Multishot = 1,
PunchThrough = { 0, '%.1f m' },
ShotSpeed = { nil, function(self, shotSpeed)
	if shotSpeed == nil then
		return 'N/A'
	end
	return ('%.1f m/s'):format(shotSpeed)
end
},
BurstDelay = { 0, '%.4f s' },
BurstReloadDelay = { 0, '%.2f s' },
BurstsPerSec = { function(weapAtk)
	-- There is no delay after last shot in burst
	return 1 / ( (1 / statRead(weapAtk, 'FireRate') ) + statRead(weapAtk, 'BurstDelay') * ( statRead(weapAtk, 'BurstCount') - 1) )
end, '%.2f bursts/sec' },
CritChance = { 0, percent },
CritMultiplier = { 1, '%.2fx' },
ForcedProcs = { unpacks('ForcedProcs'), function(s, ...)
	local procs = { ... }
	if procs[1] == nil then
		return 'No forced procs'
	end
	local result = {}
	for _, proc in ipairs(procs) do
		table.insert(result, Tooltip.full(proc, 'DamageTypes'))
	end
	return table.concat(result, ', ')
end
},
Radius = { 0, '%.1f m' },
StatusChance = { 0, percent },
Disposition = {
	function(weap)
		local d = weap['Disposition']
		-- Returning a categorical bin value of 1, 2, 3, 4, or 5 based on where disposition value
		-- is on the continuous scale of 0.5-1.55. If disposition value is nil then return 0
		return d or 0, type(d)=='number' and math.floor(5*(d-.3+.009*(d<1 and 1 or -1))) or 0
	end,
	function(s, v, d)
		return StatObject.default.Dispo[2](s, d)..(' (%.2fx)'):format(v)
	end
},
Dispo = { indexes('Disposition', 2), function(s, d)
	if d and d == d and d > 0 then
		return ('●'):rep(math.min(d, 5))..('○'):rep(5 - d)
	end
	return '×××××' -- '●○×' --> E2978F E2978B C397
end },
Introduced = { function(weap)
	return weap['Introduced'] and Version._getVersion(weap['Introduced'])['Name'] or 'N/A'
end, passes(Version._getVersionLink)
},
IntroducedDate = function(weap)
	return weap['Introduced'] and Version._getVersionDate(weap['Introduced']) or 'N/A'
end,
IsLichWeapon = function(weap)
	return weap['IsLichWeapon'] and true or false
end,
Mastery = 0,
Link = { nil, '[[%s]]' },
Name = { nil, function(s, v) return Tooltip.full(v, 'Weapons') end },
InternalName = '',
NameLink = { function(weap) return weap.Link, weap.Name end, '[[%s|%s]]' },
Polarities = { nil, passes(Polarity._pols) },
Traits = { unpacks('Traits'), { sep = ', ' } },
-- Default nil b/c some attacks don't have an associated accuracy/spread value (like AoE explosions)
Accuracy = { nil, function(self, value)
	if (value == nil) then
		return 'N/A'
	end
	return value
end
},
-- Inverse of accuracy. Spread of 1 equates to no spread.
-- Alternatively, it can be calculated by the average of min and max spread, see AvgSpread getter.
Spread = { function(weapAtk)
	local accuracy = statRead(weapAtk, 'Accuracy')
	return (accuracy == nil) and nil or 100 / accuracy
end, function(self, value)
	if (value == nil) then
		return 'N/A'
	end
	return value
end
},
AmmoType = function(weapAtk)
	return weapAtk['AmmoType'] or ({
		['Archgun (Atmosphere)'] = 'Heavy',
		['Secondary'] = 'Secondary',
		['Primary'] = 'Primary'
	})[weapAtk['Slot']] or 'None'
end,
-- Not all weapons have an Exilus slot so default to nil
ExilusPolarity = { nil, function(self, exilusPolarity)
	if (exilusPolarity == nil) then
		return 'N/A'
	end
	return Polarity._polarity(exilusPolarity)
end
},
Magazine = 1,
AmmoMax = { function(weapAtk)
	if statRead(weapAtk, 'IsMelee') then
		return nil
	end
	return weapAtk['AmmoMax'] or math.huge
end, passes(Math.formatnum)
},
Range = { function(weapAtk)
	return weapAtk['Range'] or statRead(weapAtk, 'ShotType') == 'Hit-Scan' and 300 or 0
end, '%.1f m'
},
Reload = { ors('Reload', 'RechargeTime', 0), '%.2f s' },
RechargeTime = { function(weapAtk)
	return statRead(weapAtk, 'ReloadStyle'):find'[Rr]egen' and statRead(weapAtk, 'Magazine') / statRead(weapAtk, 'ReloadRate') or nil
end, '%.2f s'
},
ReloadRate = { 0, '%.2f rounds/sec' },	-- Used for rechargeable weapons; not necessarily inverse of reload time b/c of presence of reload delay
ReloadDelay = { function(weapAtk)
	return weapAtk['ReloadDelay'] or 0
end, '%.2f s'
},
ReloadDelayEmpty = { ors('ReloadDelayEmpty', 'ReloadDelay'), '%.2f s' },
-- Reload speed will be calculated as the inverse of reload time for purposes
-- of keeping how we rank stats consistent for [[Module:Weapons/preprocess]]
-- (larger number = higher stat; a short reload time can be expressed as fast reload 
-- speed which would be a larger value in magnitude)
ReloadSpeed = { function(weapAtk)
	return 1 / statRead(weapAtk, 'Reload')
end, function(str, reloadSpeed)
	return string.format('%.2f%% reload progress per second', reloadSpeed * 100)
end },
ReloadStyle = 'Magazine',
Spool = { 0, '%d rounds' },
SpoolStartFireRate = { 0, '%.1fx' },	-- scalar that is applied to fire rate stat for auto-spool weapons
AvgSpread = { function(weapAtk)
	local minSpread = statRead(weapAtk, 'MinSpread')
	local maxSpread = statRead(weapAtk, 'MaxSpread')
	if (minSpread == nil) then
		return nil
	end
	return (minSpread + maxSpread) / 2
end, function(self, value)
		if (value == nil) then
			return 'N/A'
		end
		return ('%.2f°'):format(value)
	end
},
-- Default nil b/c some attacks don't have an associated accuracy/spread value (like AoE explosions)
MinSpread = { nil, function(self, value)
	if (value == nil) then
			return 'N/A'
		end
		return ('%.2f°'):format(value)
	end
},
MaxSpread = { nil, function(self, value)
	if (value == nil) then
			return 'N/A'
		end
		return ('%.2f°'):format(value)
	end
},
Trigger = 'N/A',
BlockAngle = { 0, '%d&#176;' },
ComboDur = { 0, '%.1f s' },
FollowThrough = { 0, '%.1fx' },
HeavyAttack = { 0, passes(Math.formatnum) },
HeavySlamAttack = { 0, passes(Math.formatnum) },
HeavyRadialDmg = { 0, passes(Math.formatnum) },
HeavySlamRadius = { 0, '%.1f m' },
MeleeRange = { 0, '%.2f m' },
SlamAttack = { 0, passes(Math.formatnum) },
SlamRadialDmg = { function(weapAtk)
	return weapAtk.SlamRadialDmg or 0, statRead(weapAtk, 'SlamRadialElement')
end, function(self, dmg, elem)
	if elem then
		return Tooltip.icon(elem, 'DamageTypes')..' '..Math.formatnum(dmg)
	end
	return Math.formatnum(dmg)
end
},
SlamRadialElement = { nil, function(self, value)
	return value ~= nil and Tooltip.full(value, 'DamageTypes') or 'Same damage type distribution as normal attack'
end
},
-- Slam radial forced proc(s)
SlamRadialProcs = { nil, function(self, proc)
	if type(proc)=='table' then
		local result = {}
		for _, elem in ipairs(proc) do
			table.insert(result, Tooltip.full(elem, 'DamageTypes'))
		end
		return table.concat(result, '<br />')
	else
		return 'N/A'
	end
end
},
SlamRadius = { 0, '%.1f m' },
SlideAttack = { function(weapAtk)
	return weapAtk.SlamRadialDmg or 0, statRead(weapAtk, 'SlideElement')
end, function(self, dmg, elem)
	if elem then
		return Tooltip.icon(elem, 'DamageTypes')..' '..Math.formatnum(dmg)
	end
	return Math.formatnum(dmg)
end
},
SlideElement = { nil, function(self, value)
	return value ~= nil and Tooltip.full(value, 'DamageTypes') or 'Same damage type distribution as normal attack'
end
},
--[[Stances = function(weapAtk)
	if not statRead(weapAtk, 'IsMelee') then return end
	return Stances._getAllStancesSameType(statRead(weapAtk, "Class"))
	--             ^ currently a local function
end,--]]
-- Not all weapons have an Stance slot so default to nil
StancePolarity = { nil, function(self, stancePolarity)
	if (stancePolarity == nil) then
		return 'N/A'
	end
	return Polarity._polarity(stancePolarity)
end
},
SweepRadius = { 0, '%.2f m' },
WindUp = { 0, '%.1f s' },
BurstCount = 1,
-- Average crit/proc count from a single attack input
AvgCritCount = function(weapAtk)
	return statRead(weapAtk, 'CritChance') * statRead(weapAtk, 'Multishot')
end,
AvgCritPerSec = function(weapAtk)
	return statRead(weapAtk, 'AvgCritCount') * statRead(weapAtk, 'EffectiveFireRate')
end,
AvgProcCount = function(weapAtk)
	return ( statRead(weapAtk, 'StatusChance') + Table.size(weapAtk['ForcedProcs'] or {}) ) * statRead(weapAtk, 'Multishot')
end,
AvgProcPerSec = function(weapAtk)
	return statRead(weapAtk, 'AvgProcCount') * statRead(weapAtk, 'EffectiveFireRate')
end,
InterShotTime = function(weapAtk)
	local v = statRead(weapAtk, 'Magazine') == 1 and statRead(weapAtk, 'Reload') + statRead(weapAtk, 'ReloadDelayEmpty') or 0
	if v == 0 then v = 1 / statRead(weapAtk, 'FireRate') end
	return v
end,
EffectiveFireRate = function(weapAtk)
	return 1 / ( statRead(weapAtk, 'ChargeTime') + statRead(weapAtk, 'InterShotTime') )
end,
ShotsPerMag = function(weapAtk)
	-- Default to 1 "ammo cost" even if attack does not directly consume ammo (e.g. AoE hits, speargun throws, etc.)
	return math.floor(statRead(weapAtk, 'Magazine') / (statRead(weapAtk, 'AmmoCost') or 1))
end,
FireRate = { function(weapAtk)
	local dataFireRate = weapAtk['FireRate']
	if dataFireRate then return dataFireRate end
	-- TODO: Think we can safely remove this calculation of FireRate from BurstFireRate, BurstDelay, and BurstCount
	-- for burst-fire attacks since FireRate is also included for those
	mw.log('calculating FireRate from Burst stats for '..statRead(weapAtk, 'Name'))
	local count = statRead(weapAtk, 'BurstCount')
	local fireRate = count / (1 / statRead(weapAtk, 'BurstFireRate') + count * statRead(weapAtk, 'BurstDelay'))
	return fireRate
end, '%.3f attacks/sec'
},
BurstFireRate = { function(weapAtk)
	return 1 / statRead(weapAtk, 'BurstDelay')
end, '%.2f attacks/sec'
},
--[[
Describing what happens when a gun in WARFRAME is fired using player-made terminology:

A particular gun consumes a set number of ammo in order to fire a set number of shots
on a single player input for a particular attack.

A single player input is defined as:
* a single attack button press for semi-auto and burst trigger weapons
* the moment the next shot is being fired when the attack button is being held for automatic/held trigger weapons
* the action of holding the attack button for charge trigger weapons
* for duplex-fire trigger weapons, the hold and release of the attack button counts as two inputs

A shot is defined as the base unit of attack of a weapon when unmodded.
* A single attack input can launch several shots as in the case of burst-fire weapons.
* A single shot can shoot more than one projectile, affected by the multishot stat, as in the case of shotguns.
* A single shot can consume more than one unit of ammo (e.g. Tenora's alt-fire) or 
less than one unit of ammo (e.g. Ignis and most continuous weapons).

A gun can have multiple attacks which can be triggered using different buttons 
and/or types of button inputs (e.g. pressing vs. holding)
]]--
CalcDamage = function(weapAtk)
	local weapon, attack = weapAtk, weapAtk
	-- Count
	-- How many shots are fired in a single player input
	local tapShots = statRead(weapAtk, 'BurstCount')
	-- How many individual player inputs can occur before depleting a magazine
	local magTaps = statRead(weapAtk, 'ShotsPerMag')
	-- How many additional projectiles are fired per ammo
	local multishot = statRead(weapAtk, 'Multishot')
	-- How much ammo is contained in the magazine
	local magazine = statRead(weapAtk, 'Magazine')
	-- How much ammo can be drawn from reserves (or?, how much ammo can be used without picking up more)
	local ammoMax = statRead(weapAtk, 'AmmoMax')

	-- Time^-1
	local fireRate = statRead(weapAtk, 'FireRate')
	-- Time
	local shotTime = statRead(weapAtk, 'InterShotTime')
	local chargeTime = statRead(weapAtk, 'ChargeTime')
	local burstDelayTime = statRead(weapAtk, 'BurstDelay')
	local reloadDelayTime = statRead(weapAtk, 'ReloadDelayEmpty')
	local reloadTime = statRead(weapAtk, 'Reload')
	local tapTime = chargeTime + (tapShots - 1) * burstDelayTime
	-- tapTime: The time between the last shot fired and the next valid attack input
	-- (omitting latency of course).
	-- Note that first shot of any non-charge trigger attack is instantenous
	local magDepletionTime = magTaps * tapTime
	if magDepletionTime == 0 then -- If attack is not a charged attack
		if shotTime == 0 then
			shotTime = 1 / fireRate
		end
		magDepletionTime = magTaps / fireRate
	end
	local shotDelayTime = math.max(0, shotTime - tapTime)

	-- Multiplier
	local maxProgenitorBonus = statRead(weapAtk, 'IsLichWeapon') and 1.6 or 1
	local avgCritMult = 1 + (statRead(weapAtk, 'CritMultiplier') - 1) * statRead(weapAtk, 'CritChance')
	-- Damage
	local biasPortion, biasType, hitDamage = statRead(weapAtk, 'DamageBias')
	
	local avgDmgOnTap = hitDamage * avgCritMult * multishot * tapShots * maxProgenitorBonus
	local avgDmgPerMag = avgDmgOnTap * magTaps
	
	-- 1 is needed b/c one whole magazine is not included in reserve ammo count
	-- If there is no reserve ammo, that means that weapon can deal an infinite amount of damage theoretically
	local avgLifetimeDmg = (ammoMax ~= nil) and avgDmgPerMag * (1 + (ammoMax / magazine)) or math.huge
	
	-- Damage / Time
	local baseDps = hitDamage * multishot / shotTime
	local avgSustainedDps = avgDmgPerMag / (magDepletionTime + reloadDelayTime + reloadTime) / tapShots
	local avgBurstDps = avgDmgOnTap / (tapTime + shotDelayTime) / tapShots
	-- Note that burst DPS can also be calculated as such:
	-- local avgBurstDps = (hitDamage * avgCritMults * maxProgenitorBonus) * multishot / shotTime
	-- local avgBurstDps = avgDmgPerMag / magDepletionTime
	
	return StatObject.ucacheIn(weapAtk, 'CalcDamage',
			{ hitDamage, avgDmgOnTap, avgBurstDps, avgSustainedDps, avgLifetimeDmg, baseDps, avgDmgPerMag }
	)
end,
ShotDmg = indexes('CalcDamage', 1),	-- Total damage per projectile
AvgShotDmg = indexes('CalcDamage', 2), AvgTapDmg = indexes('CalcDamage', 2),	-- Average total damage per each input button
BurstDps = indexes('CalcDamage', 3),	-- Average burst damage per second/DPS w/o reloading
SustainedDps = indexes('CalcDamage', 4),	-- Average sustained damage per second/DPS w/ reloading
LifetimeDmg = indexes('CalcDamage', 5),	-- Average total damage from entire ammo pool
BaseDps = indexes('CalcDamage', 6),	-- Base damage per second w/ multishot w/o crit
MagDmg = indexes('CalcDamage', 7),	-- Average total damage per magazine
-- Average damage scaled by melee attack speed multiplier (numerator of melee DPS w/o accounting for stances and animation time)
AvgDmgWithAnimSpeedMulti = function(weapAtk)
	if statRead(weapAtk, 'IsMelee') then
		-- Some melee weapons have attacks with multishot like Redeemer, Vastilok, and Quassus
		return statRead(weapAtk, 'BaseDamage') * statRead(weapAtk, 'Multishot') * statRead(weapAtk, 'AttackSpeed')
	end
	return 0
end,
AttackSpeed = { --[[ors('AttackSpeed', 'FireRate')]]function(weapAtk)
	if not statRead(weapAtk, 'IsMelee') then
		error('AttackSpeed: Cannot get AttackSpeed attribute for a non-melee weapon; use p.statRead(weapAtk, "FireRate") instead')
	end
	return statRead(weapAtk, 'FireRate')
end, '%.2fx animation speed'
},
IsMelee = function(weapAtk) return statRead(weapAtk, 'Slot'):find('Melee') or statRead(weapAtk, 'Slot'):find('melee') ~= nil end,
IsSilent = ors('IsSilent', 'IsMelee', false),
HasAoEAttack = function(weap)
	for i, attackEntry in pairs(weap['Attacks']) do
		if attackEntry['ShotType'] == 'AoE' then
			return true
		end
	end
	return false
end,
Conclave = false,
Image = { 'Panel.png', '[[File:%s|link=]]' },
Attacks = ors('Attacks', p._getAttack, {}),
Family = nil,
FamilyList = { function(weapAtk)
	local family = statRead(weapAtk, 'Family')
	-- assert(family, 'i have no Family :\'(')
	if not family then return {weapAtk} end
	-- return family, statRead(weapAtk, 'Slot')
	local slot = statRead(weapAtk, 'Slot')
	local result = {}
	for _, w in pairs(WeaponData[slot] or error('FamilyList: no weapondata for slot '..(slot or '<nil>'))) do
		if w.Family == family then
			table.insert(result, w)
		end
	end
	table.sort(result, function(a,b) return a.Name<b.Name end)
	return result
end, function(self, result)
	for i,w in ipairs(result) do
		result[i]=Tooltip.full(w.Name, 'Weapons', w)
	end
	return table.concat(result, '<br />')
end
},
BaseName = function(weapAtk) return weapAtk['BaseName'] or ({p._getVariant(statRead(weapAtk, 'Name'))})[3] end,
-- TODO: Add comments to Explosion function for readability
-- TODO: Do not rely on attack name to determine what AoE component is attached to which main direct hit component
---^i suggest an explosion key with either the attack number of any corresponding explosion, nested attack tables, or some other way to make a tree
-- TODO: Use ShotType = "AoE" to determine if attack entry is AoE
Explosion = function(weapAtk)
	local weap, atk = weapAtk, weapAtk
	-- tbh this is a mess
	local explosion = weapAtk['Explosion'] or statRead(weapAtk, 'AttackName'):gsub(' Impact',''):gsub(' Contact','')..' Explosion'
	if type(explosion) == 'string' then
		explosion = weap.Attacks[tonumber(explosion:gsub('%D',''))] or explosion
	elseif type(explosion) == 'number' then
		explosion = weap.Attacks[explosion] or explosion
	end
	local explosions = {}
	if type(explosion) ~= 'table' then
		for i, v in ipairs(weap.Attacks) do
			if p._statRead(weapAtk, v, 'AttackName'):find 'xplosion' then
				if p._statRead(weapAtk, v, 'AttackName') == explosion then
					explosions[1] = nil
					explosion = v
					break
				end
				table.insert(explosions, v)
			end
		end
		explosion = explosions[1] or explosion
	end
	StatObject.pcacheIn(getWeaponAttack(weap, explosion), 'BaseAttack', atk)
	return StatObject.pucacheIn(weapAtk, 'Explosion', explosion)
end,
IsVariant = function(weap)
	return StatObject.pucacheIn(weap, 'IsVariant', p._isVariant(statRead(weap, 'Name')))
end,
Variant = indexes('IsVariant', 2),
BaseName = indexes('IsVariant', 3),
Categories = { function(weapAtk)
	local cats = { 'Weapons' }
	-- Adding editor-defined traits from M:Weapons/data
	-- Note to make sure they have a proper category page associated with a trait
	for _, trait in ipairs(weapAtk.Traits or {}) do
		table.insert(cats, trait..' Weapons')
	end
--[=[
local CATEGORY_MAP = {
	Primary = 'Primary Weapons',
	Secondary = 'Secondary Weapons',
	Melee = 'Melee Weapons',
	['Archmelee'] = 'Archwing Melee',
	['Archgun'] = 'Archwing Gun',
	['Archgun (Atmosphere)'] = 'Archwing Gun',
	Kitgun = 'Kitgun',
	Zaw = 'Zaw',
	['Railjack Turret'] = 'Railjack',
	['Railjack Armament'] = 'Railjack',
	Gear = 'Gear',
	
	Rifle = 'Assault Rifle',
	['Sniper Rifle'] = 'Sniper Rifle',
	Shotgun = 'Shotgun',
	Pistol = 'Pistol',
	['Dual Pistols'] = 'Dual Pistols',
	Bow = 'Bow',
	Launcher = 'Launcher',
	['Arm-Cannon'] = 'Arm-Cannon',
	['Speargun'] = 'Speargun',
	Thrown = 'Thrown',
	['Shotgun Sidearm'] = 'Shotgun Sidearm',
	
	Prime = 'Prime Weapons',
	['Never Vaulted'] = 'Never Vaulted',
	Vaulted = 'Vaulted',
	Wraith = 'Wraith',
	Vandal = 'Vandal',
	['Kuva Lich'] = 'Kuva Lich',
	['Kuva Lich'] = 'Kuva',
	Prisma = 'Prisma',
	
	Grineer = 'Grineer Weapons',
	Corpus = 'Corpus Weapons',
	Infested = 'Infested Weapons',
	Tenno = 'Tenno Weapons',
	Sentient = 'Sentient Weapons',
	Entrati = 'Entrati Weapons',
	Baro = 'Baro Ki\'Teer Offering',
	Syndicate = 'Syndicate Offerings',
	['Invasion Reward'] = 'Invasion Reward',
	
	['Alt Fire'] = 'Weapons with Alt Fire',
	['AoE'] = 'Weapons with Area of Effect]][[Category:Self Interrupt Weapons',
	
}
--]=]
	local bias = p._getValue(weapAtk, "BiasType")
	table.insert(cats, bias..' Damage Weapons')
	
	local class = p._getValue(weapAtk, "Class")
	table.insert(cats, ({
		["Arm Cannon"] = class,
		["Bow"] = class,
		["Crossbow"] = "Bow",
		["Dual Pistols"] = class,
		["Dual Shotguns"] = class,
		["Exalted Weapon"] = class,
		["Launcher"] = class,
		["Pistol"] = class,
		["Rifle"] = class,
		["Shotgun Sidearm"] = class,
		["Shotgun"] = class,
		["Sniper Rifle"] = class,
		["Speargun"] = class,
		["Thrown"] = class,
		["Tome"] = class,
		["Assault Saw"] = class,
		["Blade and Whip"] = class,
		Claws = class,
		Dagger = class,
		["Dual Daggers"] = class,
		["Dual Nikanas"] = class,
		["Dual Swords"] = class,
		Fist = class,
		Glaive = class,
		Gunblade = class,
		Hammer = class,
		["Heavy Blade"] = class,
		["Heavy Scythe"] = class,
		Machete = class,
		Nikana = class,
		Nunchaku = class,
		Polearm = class,
		Rapier = class,
		Scythe = class,
		Sparring = class,
		Staff = class,
		Sword = class,
		["Sword and Shield"] = class,
		["Two-Handed Nikana"] = class,
		Tonfa = class,
		Warfan = class,
		Whip = class,
		["Archgun"] = 'Archwing Gun',
		["Archgun (Atmosphere)"] = 'Archwing Gun',
		["Archmelee"] = 'Archwing Melee',
		["Turret"] = 'Railjack Armaments',
		["Ordnance"] = 'Railjack Armaments',
		Amp = class
	})[class] or "Other Weapons")

	local family = p._getValue(weapAtk, "Family")
	table.insert(cats, family)
	
	local slot = p._getValue(weapAtk, "Slot")
	table.insert(cats, slot..' Weapons')
	
	-- TODO: Move all these trigger categories to be in the format "Category:Trigger Name Weapons"
	local trigger = p._getValue(weapAtk, "Trigger")
	table.insert(cats, trigger..' Weapons')
	-- TODO: remove original trigger cats? (following snippet)
	table.insert(cats,(({
		["Active"] = trigger,
		["Auto Charge"] = 'Charge',
		["Auto"] = 'Automatic',
		["Auto-Burst"] = 'Burst Fire',
		["Auto-Spool"] = 'Automatic',
		["Burst"] = 'Burst Fire',
		["Charge"] = trigger,
		["Duplex"] = 'Duplex Fire',
		["Held"] = 'Continuous Weapons',
		["Semi-Auto"] = 'Semi-Automatic',
	})[trigger]))--modes? " Weapons"?
	
	local users = p._getValue(weapAtk, "Users") or {}
	for _, user in ipairs(users) do table.insert(cats, user) end
	
	local variant = p._getValue(weapAtk, "Variant")
	table.insert(cats, variant)
	
	local infAmmo = p._getValue(weapAtk, "AmmoMax") == math.huge
	local accuracy = p._getValue(weapAtk, "Accuracy")
	local pinpoint = accuracy ~= nil and accuracy >= 100
	local regens = p._getValue(weapAtk, "ReloadRate") > 0
	local silent = weapAtk.IsSilent -- automatically includes
	local single = p._getValue(weapAtk, "Magazine") == 1 and not p._getValue(weapAtk, "IsMelee")--meh, delet?
	local spools = p._getValue(weapAtk, "Spool") > 0
	
	local isHitScan = p._getValue(weapAtk, "ShotType") == "Hit-Scan"
	local isProjectile = p._getValue(weapAtk, "ShotType") == "Projectile"
	local isAoE = p._getValue(weapAtk, "HasAoEAttack")
	local isCodexSecret = p._getValue(weapAtk, "CodexSecret")
	local isTradable = p._getValue(weapAtk, "Tradable")
	local isInConclave = p._getValue(weapAtk, "Conclave")
	
	-- Arbitrarily ordering misc categories
	if infAmmo then table.insert(cats, 'Infinite Ammo Weapons') end
	if pinpoint then table.insert(cats, 'Pinpoint Weapons') end
	if regens then table.insert(cats, 'Battery Weapons') end
	if silent then
		table.insert(cats, 'Silent Weapons')
	else
		table.insert(cats, 'Alarming Weapons')
	end
	if single then table.insert(cats, 'Single Shot Weapons') end
	if spools then table.insert(cats, 'Spooling Weapons') end
	if isHitScan then table.insert(cats, 'Hit-Scan Weapons') end
	if isProjectile then table.insert(cats, 'Projectile Weapons') end
	if isAoE then table.insert(cats, 'Weapons with Area of Effect') end
	if isCodexSecret then table.insert(cats, 'Codex Secret') end
	if isTradable then
		table.insert(cats, 'Tradeable Weapons')
	else
		table.insert(cats, 'Untradeable Weapons')
	end
	if isInConclave then table.insert(cats, 'Available In Conclave') end
	
	return StatObject.cacheIn(weapAtk, 'Categories', cats)
end, function(s, cats)
	local wikitextResult = { '' }	-- Need to prepend a newline so first asterisk is rendered as a wikitext list
	local formatStr = '*[[:Category:%s|%s]][[Category:%s]]'
	for _, category in ipairs(cats) do
		table.insert(wikitextResult, formatStr:format(category, category, category))
	end
	return table.concat(wikitextResult, '\n')
end
},
SyndicateEffect = { '', function(s, v)
	return (v == '' or type(v) ~= 'string') and '' or Tooltip.icon(({
		['blight'] = 'Red Veil',
		['entropy'] = 'Cephalon Suda',
		['justice'] = 'Steel Meridian',
		['purity'] = 'New Loka',
		['sequence'] = 'The Perrin Sequence',
		['truth'] = 'Arbiters of Hexis',
	})[v:lower()] or 'Tenno', 'Factions')
	..' '..v
end
},
MinProgenitorBonus = function(weap) return weap.IsLichWeapon and statRead(weap, 'BaseDamage') * 0.25 or 0 end,
ProgenitorBonus = function(weap) return weap.IsLichWeapon and statRead(weap, 'BaseDamage') * 0.6 or 0 end,
Class = '',
SniperComboReset = { nil, '%.1f s' },
SniperComboMin = { nil, '%d shot(s)' },
Tradable = { function(weapAtk)
	if type(weapAtk['Tradable'])=='number' then
		assert(weapAtk['Tradable']<=5,
			'Tradable: Does not support tradeability enums beyond 5; please update [[Module:Weapons/data]] and [[Module:Weapons]] to support more tradeability edge cases')
		return ({
			[0]=false,
			[1]='Unranked',
			[2]='Parts',
			[3]='Lich',
			[4]='Built Parts',
			[5]='Parent',
		})[weapAtk['Tradable']]
	end
	return weapAtk['Tradable']
end, function(s, tradable)
	return ({ 
		[false] = 'Untradeable',
		Unranked = 'Tradeable unranked w/ no Forma or Catalyst',
		Parts = 'Tradeable parts and/or blueprint only',
		Lich = 'Tradeable indirectly through [[Lich System|Lich]] trading',
		['Built Parts'] = 'Tradeable only fully built components, not blueprints',
		Parent = 'Tradeable indirectly, comes with parent companion',
	})[tradable] or 'Untradeable?'
end
},
SellPrice = { nil, function(self, sellPrice)
	if sellPrice == nil then
		return 'Cannot sell'
	end
	return Tooltip.icon('Credits', 'Resources')..' '..Math.formatnum(sellPrice)
end
},
DefaultUpgrades = { nil, function(self, upgradesArr)
	local result = {}
	for _, modIndex in ipairs(upgradesArr or {}) do
		table.insert(result, Tooltip.full(modIndex, 'Mods'))
	end
	return table.concat(result, '<br />')
end
},
Users = { nil, function(self, usersArr)
	local result = { '' }
	for _, user in ipairs(usersArr or {}) do
		table.insert(result, '*[['..user..']]')
	end
	return table.concat(result, '\n')
end
},
Zoom = { unpacks('Zoom'), { sep = '<br />' } },
Slot = nil,
}
-- Loops for adding to StatObject.default table
-- Damage type getters:
-- <DamageType> = damage type value
-- <DamageType>Distribution = damage type distribution as a percentage
-- PvP<DamageType> = damage type value with precise formatting for PvP purposes
for _, damageType in ipairs(iterationOrderArray) do
	StatObject.default[damageType] = {
		function(weapAtk) return weapAtk['Damage'][damageType] or 0 end,
		function(self, value) return Tooltip.icon(damageType, 'DamageTypes')..' '..Math.formatnum(value) end
	}
	-- Damage distribution as a percentage
	StatObject.default[damageType..'Distribution'] = {
		function(weapAtk) return weapAtk['Damage'][damageType] / statRead(weapAtk, 'BaseDamage') end,
		function(self, value) return Tooltip.icon(damageType, 'DamageTypes')..' '..Math.percentage(value) end
	}
	-- More precise damage values to 4 decimal places for PvP
	StatObject.default['PvP'..damageType] = {
		function(weapAtk) return weapAtk['Damage'][damageType] or 0 end,
		Tooltip.icon(damageType, 'DamageTypes')..' %.4f'
	}
end

-- TODO: Do not rely on localized name to determine a weapon's variant. Decouple localization from data
---	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 (var ~= "Dex" or weaponName ~= "Dex Pixia") then
			if string.find(weaponName, var) then
				return true, var, (string.gsub(weaponName, " ?"..var.." ?-?", ""))
			end
		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[opt]		{string} variant Variant name (e.g. "Vandal"); if nil, returns base weapon name instead
--	@returns		{string} Weapon's variant name (e.g. "Braton Vandal")
function p._buildName(baseName, variant)
	if not variant or variant == 'Base' or variant == '' then
		return baseName
	end
	return (({
		-- Prime Laser Rifle is an edge case for Prime naming scheme (at least in EN localization)
		Prime =  baseName ~= 'Laser Rifle' and '%b %v',
		Vandal = '%b %v',
		Wraith = '%b %v',
		MK1 = '%v-%b',
	})[variant] or '%v %b'):gsub('%%v', variant):gsub('%%b', baseName)
end

---	Returns a specific weapon table entry from <code>/data</code> or <code>/Conclave/data</code>.
--	@function		p._getWeapon
--	@param			{string} weaponName Weapon name
--	@param[opt]		{boolean} pvp If true, gets PvP stats of weapon instead, false otherwise; defaults to false
--	@returns		{table} Weapon table
function p._getWeapon(weaponName, pvp)
	weaponName = mw.text.decode(weaponName)
	return (pvp and ConclaveData or WeaponData)[weaponName] or
	error('p._getWeapon(weaponName, pvp): "'..weaponName..
		'" does not exist in '..(pvp and '[[Module:Weapons/Conclave/data]]' or '[[Module:Weapons/data]]'))
end

---	Gets the raw value of a certain statistic of a weapon.
--	@function		p._getValue
--	@param			{table} Weapon Weapon table
--	@param			{string} key Name of key
--	@param[opt]		{string} attack Name of attack to search through; defaults to 'Attack1' or what '_TooltipAttackDisplay' is set to
--	@returns		{string, number} Value of statistic
function p._getValue(weap, key, atk)--, formatted)
	-- return (formatted and statFormat or statRead)(weap, atk, key)
	return p._statRead(weap, atk, key)
end

---	Gets the formatted value of a certain statistic of a weapon to be displayed
--	the wiki.
--	@function		p._getFormattedValue
--	@param			{table} Weapon Weapon table
--	@param			{string} keyName Name of key
--	@param[opt]		{string} attackName Name of attack to search through; defaults to 'Attack1'
--	@returns		{string} Value of statistic
function p._getFormattedValue(weap, key, atk)
	-- return p._getValue(Weapon, keyName, attackName, true)
	return p._statFormat(weap, atk, key)
end

--- Function that returns a simpler getter function, for multiple _stat*() calls on the same weapon/attack pair.
--	@function		p._statReader
--	@param			{table} weap Weapon entry
--	@param			{number|table} atk Attacks table index or Attack entry
--	@return			{function} Getter function
function p._statReader(weap, atk)
	return function(...) return p._statRead(weap, atk, ...) end
end

--- Function that returns a simpler getter function, for multiple _stat*() calls on the same weapon/attack pair.
--	@function		p._statFormatter
--	@param			{table} weap Weapon entry
--	@param			{number|table} atk Attacks table index or Attack entry
--	@return			{function} Getter function
function p._statFormatter(weap, atk)
	return function(...) return p._statFormat(weap, atk, ...) end
end

---	Returns a subset of <code>/data</code> or <code>/Conclave/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
--	@param[opt]		{string} source Name of weapon entry to use
--	@param[opt]		{boolean} ignoreIgnore If true, ignores the _IgnoreEntry flag, false otherwise; defaults to false
--	@param[opt]		{function} sortFunc Custom comparison function; false -> no sorting; defaults to sorting in ascending order by weapon name
--	@returns		{table} Table of weapon table entries as seen in <code>/data</code>
function p._getWeapons(validateFunction, source, opts)
	opts=opts or {}
	local ignoreIgnore, sortFunc, pvp = opts.ignoreIgnore, opts.sortFunc, opts.pvp
	validateFunction = validateFunction or function() return true end
	local data = pvp and ConclaveData or WeaponData
	if source then
		data = data[source]
	end

	local weaps = {}
	for _, weap in pairs(data) do
		if (ignoreIgnore or not weap['_IgnoreEntry']) and validateFunction(weap) then
			table.insert(weaps, weap)
		end
	end
	if sortFunc ~= false then
		table.sort(weaps, sortFunc or function(a, b) return a.Name < b.Name end)
	end
	return weaps
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		p._getMeleeWeapons
--	@param[opt]		{boolean} weapType
--	@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>
function p._getMeleeWeapons(weapType,pvp)
	return p._getWeapons(weapType and function(weap) return weap.Class==weapType end, 'melee',{['pvp']=pvp==true})
end

---	Main frame invokable function to access any raw/computed attribute/column/key of a weapon entry.
--	See default table in M:Weapons to see all valid computed attributes.
--	@function		p.getValue
--	@param			{string} weap Weapon name in EN locale
--	@param			{number} atk Attacks table index
--	@param			{string} k Key name
--	@return			Raw or computed value associated with k key
function p.getValue(frame)
	-- table.unpack doesn't work on the frame object which is why this is anonymous function is needed
	local weap, key, atk = (function(t) return t[1], t[2], t[3] end)(frame.args)
	weap = p._getWeapon(weap)
	return p._getValue(weap, key, atk)
end

---	Main frame invokable function to access any formatted attribute/column/key of a weapon entry.
--	See default table in M:Weapons to see all valid computed attributes.
--	@function		p.getFormattedValue
--	@param			{string} weap Weapon name in EN locale
--	@param			{number} atk Attacks table index
--	@param			{string} k Key name
--	@return			Formatted value associated with k key
function p.getFormattedValue(frame)
	local weap, key, atk = (function(t) return t[1], t[2], t[3] end)(frame.args)
	weap = p._getWeapon(weap)
	return p._getFormattedValue(weap, key, atk)
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[1] or ''
	local result = { "=="..meleeClass.." Weapons==", '<gallery widths="200" position="center" spacing="small">' }
	for i, weap in ipairs(p._getMeleeWeapons(meleeClass)) do
		table.insert(result, p._statRead(weap, nil, 'Image')..'|'..p._statFormat(weap, nil, 'Name'))
	end
	table.insert(result, '</gallery>')
	return frame:preprocess(table.concat(result, '\n')) -- annoying that it needs to be preprocessed
end

---	Gets the total count of weapons as used on [[Mastery Rank#Total Mastery]].
--	@function		p.getWeaponCount
--	@param			{table} frame Frame object w/ the first argument being the weaponSlot and the
--						  second argument being a boolean to getFullList
--	@returns		{number} Total count of weapons in a certain category/type
--	@returns		{table} List of weapon names that count for mastery in a particular weapon slot
function p._getWeaponCount(slot)
	slot = slot and slot:lower()
	local data = slot and WeaponData[slot] or WeaponData
	local fullList = {}
	
	for name, weapon in pairs(data) do
		if not weapon._IgnoreInMasteryCount then
			-- TODO: There should be a better way to determine/differentiate if a weapon is a kitgun b/c kitguns and zaws
			-- are stored in the same M:Weapons/data/modular data store; add a new "Kitgun" or "Zaw" Trait and target that?
			if (slot == 'kitgun' and weapon.Slot == 'Secondary')
				or (slot == 'zaw' and weapon.Slot == 'Melee')
				or (slot == 'robotic' and weapon.Slot ~= 'Hound')
				or (weapon.Slot:lower() == slot)
				or slot == nil then
				fullList[#fullList + 1] = name
			end
		end
	end

	return #fullList, fullList
end

---	Gets the total count of weapons as used on [[Mastery Rank#Total Mastery]].
--	@function		p.getWeaponCount
--	@param			{table} frame Frame object w/ the first argument being the weapon slot
--	@return			{number} Total number of weapons that can reward Mastery XP
function p.getWeaponCount(frame)
	return (p._getWeaponCount(frame.args and frame.args[1] or nil))
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 colNames = { 'Primary', 'Secondary', 'Melee', 'Archgun', 'Archmelee' }
	local cols = {}	-- Will look like: {['Primary']={},['Secondary']={},['Melee']={},['Archgun']={},['Archmelee']={},}
	local colOrder = {}	--{cols['Primary'],cols['Secondary'],cols['Melee'],cols['Archgun'],cols['Archmelee'],}
	local colCounts = {}

	for i, v in ipairs(colNames) do
		cols[v] = {}
		colOrder[i] = cols[v]
		colCounts[v] = 0
	end

	for _, weapon in pairs(WeaponData) do
		local pols = Table.size(weapon["Polarities"] or {})
		local slot = weapon['Slot']
		if pols > 0 and cols[slot] then
			table.insert(cols[slot], {
				'|'..p._getFormattedValue(weapon, 'NameLink'):gsub(' ?%(.*%)', '')..'||'..p._getFormattedValue(weapon, "Polarities"),
				pols
			})
			colCounts[slot] = colCounts[slot] + 1
		end
	end

	for i, v in ipairs(colNames) do
		colCounts[i] = colCounts[v]
		table.sort(cols[v], function(a, b)return a[2] > b[2] end)
	end

	local result = {[=[
{| 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" |Archguns
! colspan="2" |Archmelees]=]}
	for i = 1, math.max(table.unpack(colCounts)) do --row
		table.insert(result, '|-')
		for _, col in ipairs(colOrder) do --cell
			table.insert(result,(col[i] or {'| ||'})[1])
		end
	end
	table.insert(result, '|}')
	return table.concat(result, '\n')
end

---	Builds a table that lists out all weapons with a certain damage type
--	@function		p.buildDamageTypeTable
--	@param			{table} frame Frame object
--	@returns		{string} Wikitext of resultant wikitable
function p.buildDamageTypeTable(frame)
	local damageType = frame.args and frame.args[1] or frame
	local mostly = frame.args and (frame.args[2] or '') ~= ''

	local content = {}
	for k,weap in pairs(WeaponData) do
		local weapAtk = getWeaponAttack(weap)--could add a loop here
		local portion, biastype, damage = statRead(weapAtk, 'DamageBias')
		local typeDmg = statRead(weapAtk, damageType)
		if damage == 0 then typeDmg = weapAtk[damageType] and 1 or 0 end--modular pieces
		--Filter for
		--a. any of the damage type in any attack - former 'not mostly'
		--b. at least one majority damage type - former 'mostly'
		--c. a majority of the damage type in the display attack - 'mostly'
		--d. any of the damage type in the display attack - 'not mostly'
		if biastype == damageType or not mostly and typeDmg > 0 then
			table.insert(content, ('| %s || %s || %s || %s || %s || data-sort-value="%s" | %s'):format(
				statFormat(weapAtk, 'Name'),
				statRead(weapAtk, 'Slot'),
				statRead(weapAtk, 'Class'),
				statRead(weapAtk, 'AttackName'),
				typeDmg,
				portion, statFormat(weapAtk, 'DamageBias')
			))
		end
	end
	table.sort(content)--will sort by tooltip span key

	return ([[
{| class = "listtable sortable" style="margin:auto;"
|+ '''Weapons with %s%s damage'''
|-
! Name !! Slot !! Class !! Attack Name !! data-sort-type="number" | %s !! data-sort-type="number" | Majority
|-
]]):format(mostly and 'mostly ' or '', damageType, Tooltip.full(damageType, 'DamageTypes'))
	..table.concat(content, '\n|-\n')..'\n|}'
end

--- _isVariant adapter for p._shortLinkList
local function variantOf(weap)
	local full, _, var, base = weap.Name, p._isVariant(weap.Name)
	return var, base, full
end

---	Builds a list of weapons, with variants being next to base weapon name inside parentheses
--	(e.g. {{Weapon|Braton}} ({{Weapon|MK1-Braton|MK1}}, {{Weapon|Braton Prime|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)
	return StatObject.shortLinkList(Weapons, variantOf, tooltip and 'Weapons')
end

---	Builds a list of weapons' mastery requirements as seen on [[Template:EquipmentUnlock]],
--	[[Template:EquipmentUnlock/Primary]], [[Template:EquipmentUnlock/Secondary]], 
--	[[Template:EquipmentUnlock/Melee]], etc.
--	@function		p.getMasteryShortList
--	@param			{table} frame Frame object w/ first argument being a string weaponSlot
--	@returns		{string} Wikitext of resultant list
function p.getMasteryShortList(frame)
	local weaponSlot = frame.args[1]
	local masteryRank = tonumber(frame.args[2])
	local weapArray = p._getWeapons(function(x)
		return x.Slot == weaponSlot and x.Mastery == masteryRank
	end)
	return table.concat(StatObject.shortLinkList(weapArray, variantOf, 'Weapons'), ' • ')
end

function p.fullList()
	return table.concat(StatObject.shortLinkList(WeaponData, variantOf, 'Weapons'), ' • ')
end

---	Builds a list of PvP weapons as seen on [[PvP#Limitations]].
--	@function		p.getConclaveList
--	@param			{table} frame Frame object w/ first argument being a string weaponSlot
--	@returns		{string} Wikitext of resultant list
function p.getConclaveList(frame)
	local weaponSlot = frame.args[1] or 'All'
	local weapArray = p._getWeapons(function(weap)
		return weap.Conclave == true
	end, weaponSlot, {pvp=true})
	return '*'..table.concat(StatObject.shortLinkList(weapArray, variantOf), '\n* ')
end

function p.getListWithWarframes(frame)
	local date_str = ''
	local list = {'{| class="listtable sortable" style="overflow-y:scroll; max-height:500px"', '|-', '!data-sort-type="date"| Release !! Weapon !! Warframes'}
	local frames = {}
	
	for _, warframe in pairs(WarframeData.Warframes) do
		if warframe.Introduced then
			date_str = Version._getVersionDate(warframe.Introduced)
			if frames[date_str] then
				table.insert(frames[date_str], '{{WF|' .. warframe.Name .. '}}')
			else
				frames[date_str] = {'{{WF|' .. warframe.Name .. '}}'}
			end
		end
	end

	for _, weapon in pairs(WeaponData) do
		date_str = Version._getVersionDate(weapon.Introduced)

		warframe = frames[date_str] or {'N/A'}
		table.sort(warframe)
		warframe = table.concat(warframe, ', ')
		
		date_str = date_str:sub(6, 7) .. '.' .. date_str:sub(9, 10) .. '.' .. date_str:sub(0, 4)

		table.insert(list, '|-')
		table.insert(list, '|data-sort-value=' .. date_str .. '|{{ver|' .. weapon.Introduced .. '}} ||{{Weapon|' .. weapon.Name .. '}}||' .. warframe)
	end
	table.insert(list, '|}')

	return frame:preprocess(table.concat(list, '\n'))
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 weaponSlot
--	@returns		{string} Wikitext of resultant wikitable
function p.getRivenDispositionTable(frame)
	local weaponSlot = frame.args[1]
	local result = {
		'{| class="article-table" border="0" cellpadding="1" cellspacing="1" style="width: 100%"',
		'|-',
		{'[[a| '},	-- Wikitable header row
		'|-'
	}

	-- local ranges = {'○○○○○', '●○○○○', '●●○○○', '●●●○○', '●●●●○', '●●●●●'}
	local dispo = {}

	for k, weapon in pairs(WeaponData) do
		if weapon['Disposition'] and (weaponSlot == 'All' or weapon['Slot'] == weaponSlot) then
			local disp = p._statFormat(weapon, nil, 'Dispo')
			dispo[disp] = dispo[disp] or {}
			table.insert(dispo[disp], weapon)
		end
	end

	for str, dis in Table.skpairs(dispo) do
		table.sort(dis, function(a, b) return a['Disposition'] > b['Disposition'] end)
		local col = { '| style="vertical-align:top; font-size:small" |' }
		for _, weap in ipairs(dis) do
			table.insert(col, p._statFormat(weap, nil, 'NameLink')..' ('..weap['Disposition']..')')
		end
		table.insert(result[3], str)
		table.insert(result, table.concat(col, '\n* '))
	end

	result[3] = table.concat(result[3], ']]\n! scope="col" style="text-align:center;"|[[Riven Mods#Disposition|')..']]'
	table.insert(result, '|}')
	return table.concat(result, '\n')
end

return p